support for resource directories: enable java-specific actions only for directories...
[idea/community.git] / java / debugger / impl / src / com / intellij / debugger / ui / breakpoints / LineBreakpoint.java
1 /*
2  * Copyright 2000-2009 JetBrains s.r.o.
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  * http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16
17 /*
18  * Class LineBreakpoint
19  * @author Jeka
20  */
21 package com.intellij.debugger.ui.breakpoints;
22
23 import com.intellij.debugger.DebuggerBundle;
24 import com.intellij.debugger.DebuggerManagerEx;
25 import com.intellij.debugger.SourcePosition;
26 import com.intellij.debugger.actions.ThreadDumpAction;
27 import com.intellij.debugger.engine.DebugProcessImpl;
28 import com.intellij.debugger.engine.evaluation.EvaluateException;
29 import com.intellij.debugger.engine.evaluation.EvaluationContextImpl;
30 import com.intellij.debugger.impl.DebuggerUtilsEx;
31 import com.intellij.debugger.impl.PositionUtil;
32 import com.intellij.debugger.jdi.StackFrameProxyImpl;
33 import com.intellij.icons.AllIcons;
34 import com.intellij.openapi.application.ApplicationManager;
35 import com.intellij.openapi.diagnostic.Logger;
36 import com.intellij.openapi.editor.Document;
37 import com.intellij.openapi.editor.markup.RangeHighlighter;
38 import com.intellij.openapi.fileEditor.FileDocumentManager;
39 import com.intellij.openapi.module.Module;
40 import com.intellij.openapi.project.Project;
41 import com.intellij.openapi.roots.ProjectFileIndex;
42 import com.intellij.openapi.roots.ProjectRootManager;
43 import com.intellij.openapi.util.Computable;
44 import com.intellij.openapi.util.Key;
45 import com.intellij.openapi.util.registry.Registry;
46 import com.intellij.openapi.vfs.VirtualFile;
47 import com.intellij.psi.*;
48 import com.intellij.psi.impl.java.stubs.index.JavaFullClassNameIndex;
49 import com.intellij.psi.jsp.JspFile;
50 import com.intellij.psi.search.EverythingGlobalScope;
51 import com.intellij.psi.search.GlobalSearchScope;
52 import com.intellij.psi.util.PsiTreeUtil;
53 import com.intellij.ui.classFilter.ClassFilter;
54 import com.intellij.util.Function;
55 import com.intellij.util.Processor;
56 import com.intellij.util.StringBuilderSpinAllocator;
57 import com.intellij.util.containers.ContainerUtil;
58 import com.intellij.xdebugger.XDebuggerUtil;
59 import com.sun.jdi.*;
60 import com.sun.jdi.event.LocatableEvent;
61 import com.sun.jdi.request.BreakpointRequest;
62 import org.jetbrains.annotations.NonNls;
63 import org.jetbrains.annotations.Nullable;
64 import org.jetbrains.jps.model.java.JavaModuleSourceRootTypes;
65
66 import javax.swing.*;
67 import java.util.ArrayList;
68 import java.util.Collection;
69 import java.util.List;
70
71 public class LineBreakpoint extends BreakpointWithHighlighter {
72   private static final Logger LOG = Logger.getInstance("#com.intellij.debugger.ui.breakpoints.LineBreakpoint");
73
74   private String myMethodName;
75   public static final @NonNls Key<LineBreakpoint> CATEGORY = BreakpointCategory.lookup("line_breakpoints");
76
77   protected LineBreakpoint(Project project) {
78     super(project);
79   }
80
81   protected LineBreakpoint(Project project, RangeHighlighter highlighter) {
82     super(project, highlighter);
83   }
84
85   protected Icon getDisabledIcon(boolean isMuted) {
86     final Breakpoint master = DebuggerManagerEx.getInstanceEx(myProject).getBreakpointManager().findMasterBreakpoint(this);
87     if (isMuted) {
88       return master == null? AllIcons.Debugger.Db_muted_disabled_breakpoint : AllIcons.Debugger.Db_muted_dep_line_breakpoint;
89     }
90     else {
91       return master == null? AllIcons.Debugger.Db_disabled_breakpoint : AllIcons.Debugger.Db_dep_line_breakpoint;
92     }
93   }
94
95   protected Icon getSetIcon(boolean isMuted) {
96     if (REMOVE_AFTER_HIT) {
97       return isMuted ? AllIcons.Debugger.Db_muted_temporary_breakpoint : AllIcons.Debugger.Db_temporary_breakpoint;
98     }
99     return isMuted? AllIcons.Debugger.Db_muted_breakpoint : AllIcons.Debugger.Db_set_breakpoint;
100   }
101
102   protected Icon getInvalidIcon(boolean isMuted) {
103     return isMuted? AllIcons.Debugger.Db_muted_invalid_breakpoint : AllIcons.Debugger.Db_invalid_breakpoint;
104   }
105
106   protected Icon getVerifiedIcon(boolean isMuted) {
107     if (REMOVE_AFTER_HIT) {
108       return isMuted ? AllIcons.Debugger.Db_muted_temporary_breakpoint : AllIcons.Debugger.Db_temporary_breakpoint;
109     }
110     return isMuted? AllIcons.Debugger.Db_muted_verified_breakpoint : AllIcons.Debugger.Db_verified_breakpoint;
111   }
112
113   protected Icon getVerifiedWarningsIcon(boolean isMuted) {
114     return isMuted? AllIcons.Debugger.Db_muted_verified_warning_breakpoint : AllIcons.Debugger.Db_verified_warning_breakpoint;
115   }
116
117   public Key<LineBreakpoint> getCategory() {
118     return CATEGORY;
119   }
120
121   protected void reload(PsiFile file) {
122     super.reload(file);
123     myMethodName = findMethodName(file, getHighlighter().getStartOffset());
124   }
125
126   protected void createOrWaitPrepare(DebugProcessImpl debugProcess, String classToBeLoaded) {
127     if (isInScopeOf(debugProcess, classToBeLoaded)) {
128       super.createOrWaitPrepare(debugProcess, classToBeLoaded);
129     }
130   }
131
132   protected void createRequestForPreparedClass(final DebugProcessImpl debugProcess, final ReferenceType classType) {
133     if (!isInScopeOf(debugProcess, classType.name())) {
134       if (LOG.isDebugEnabled()) {
135         LOG.debug(classType.name() + " is out of debug-process scope, breakpoint request won't be created for line " + getLineIndex());
136       }
137       return;
138     }
139     try {
140       List<Location> locs = debugProcess.getPositionManager().locationsOfLine(classType, getSourcePosition());
141       if (!locs.isEmpty()) {
142         for (Location loc : locs) {
143           if (LOG.isDebugEnabled()) {
144             LOG.debug("Found location [codeIndex=" + loc.codeIndex() +"] for reference type " + classType.name() + " at line " + getLineIndex() + "; isObsolete: " + (debugProcess.getVirtualMachineProxy().versionHigher("1.4") && loc.method().isObsolete()));
145           }
146           BreakpointRequest request = debugProcess.getRequestsManager().createBreakpointRequest(LineBreakpoint.this, loc);
147           debugProcess.getRequestsManager().enableRequest(request);
148           if (LOG.isDebugEnabled()) {
149             LOG.debug("Created breakpoint request for reference type " + classType.name() + " at line " + getLineIndex() + "; codeIndex=" + loc.codeIndex());
150           }
151         }
152       }
153       else {
154         // there's no executable code in this class
155         debugProcess.getRequestsManager().setInvalid(LineBreakpoint.this, DebuggerBundle.message(
156           "error.invalid.breakpoint.no.executable.code", (getLineIndex() + 1), classType.name())
157         );
158         if (LOG.isDebugEnabled()) {
159           LOG.debug("No locations of type " + classType.name() + " found at line " + getLineIndex());
160         }
161       }
162     }
163     catch (ClassNotPreparedException ex) {
164       if (LOG.isDebugEnabled()) {
165         LOG.debug("ClassNotPreparedException: " + ex.getMessage());
166       }
167       // there's a chance to add a breakpoint when the class is prepared
168     }
169     catch (ObjectCollectedException ex) {
170       if (LOG.isDebugEnabled()) {
171         LOG.debug("ObjectCollectedException: " + ex.getMessage());
172       }
173       // there's a chance to add a breakpoint when the class is prepared
174     }
175     catch (InvalidLineNumberException ex) {
176       if (LOG.isDebugEnabled()) {
177         LOG.debug("InvalidLineNumberException: " + ex.getMessage());
178       }
179       debugProcess.getRequestsManager().setInvalid(LineBreakpoint.this, DebuggerBundle.message("error.invalid.breakpoint.bad.line.number"));
180     }
181     catch (InternalException ex) {
182       LOG.info(ex);
183     }
184     catch(Exception ex) {
185       LOG.info(ex);
186     }
187     updateUI();
188   }
189
190   private boolean isInScopeOf(DebugProcessImpl debugProcess, String className) {
191     final SourcePosition position = getSourcePosition();
192     if (position != null) {
193       final VirtualFile breakpointFile = position.getFile().getVirtualFile();
194       final ProjectFileIndex fileIndex = ProjectRootManager.getInstance(myProject).getFileIndex();
195       if (breakpointFile != null && fileIndex.isUnderSourceRootOfType(breakpointFile, JavaModuleSourceRootTypes.SOURCES)) {
196         // apply filtering to breakpoints from content sources only, not for sources attached to libraries
197         final Collection<VirtualFile> candidates = findClassCandidatesInSourceContent(className, debugProcess.getSearchScope(), fileIndex);
198         if (LOG.isDebugEnabled()) {
199           LOG.debug("Found "+ (candidates == null? "null" : candidates.size()) + " candidate containing files for class " + className);
200         }
201         if (candidates == null) {
202           return true;
203         }
204         for (VirtualFile classFile : candidates) {
205           if (LOG.isDebugEnabled()) {
206             LOG.debug("Breakpoint file: " + breakpointFile.getPath()+ "; candidate file: " + classFile.getPath());
207           }
208           if (breakpointFile.equals(classFile)) {
209             return true;
210           }
211         }
212         if (LOG.isDebugEnabled()) {
213           final GlobalSearchScope scope = debugProcess.getSearchScope();
214           final boolean contains = scope.contains(breakpointFile);
215           final Project project = getProject();
216           final List<VirtualFile> files = ContainerUtil.map(
217             JavaFullClassNameIndex.getInstance().get(className.hashCode(), project, scope), new Function<PsiClass, VirtualFile>() {
218             @Override
219             public VirtualFile fun(PsiClass aClass) {
220               return aClass.getContainingFile().getVirtualFile();
221             }
222           });
223           final List<VirtualFile> allFiles = ContainerUtil.map(
224             JavaFullClassNameIndex.getInstance().get(className.hashCode(), project, new EverythingGlobalScope(project)), new Function<PsiClass, VirtualFile>() {
225             @Override
226             public VirtualFile fun(PsiClass aClass) {
227               return aClass.getContainingFile().getVirtualFile();
228             }
229           });
230           final VirtualFile contentRoot = fileIndex.getContentRootForFile(breakpointFile);
231           final Module module = fileIndex.getModuleForFile(breakpointFile);
232
233           LOG.debug("Did not find '" +
234                     className + "' in " + scope +
235                     "; contains=" + contains +
236                     "; contentRoot=" + contentRoot +
237                     "; module = " + module +
238                     "; all files in index are: " + files+
239                     "; all possible files are: " + allFiles
240           );
241         }
242         
243         return false;
244       }
245     }
246     return true;
247   }
248
249   @Nullable
250   private Collection<VirtualFile> findClassCandidatesInSourceContent(final String className, final GlobalSearchScope scope, final ProjectFileIndex fileIndex) {
251     final int dollarIndex = className.indexOf("$");
252     final String topLevelClassName = dollarIndex >= 0? className.substring(0, dollarIndex) : className;
253     return ApplicationManager.getApplication().runReadAction(new Computable<Collection<VirtualFile>>() {
254       @Nullable
255       public Collection<VirtualFile> compute() {
256         final PsiClass[] classes = JavaPsiFacade.getInstance(myProject).findClasses(topLevelClassName, scope);
257         if (LOG.isDebugEnabled()) {
258           LOG.debug("Found "+ classes.length + " classes " + topLevelClassName + " in scope "+scope);
259         }
260         if (classes.length == 0) {
261           return null;
262         }
263         final List<VirtualFile> list = new ArrayList<VirtualFile>(classes.length);
264         for (PsiClass aClass : classes) {
265           final PsiFile psiFile = aClass.getContainingFile();
266           
267           if (LOG.isDebugEnabled()) {
268             final StringBuilder msg = new StringBuilder();
269             msg.append("Checking class ").append(aClass.getQualifiedName());
270             msg.append("\n\t").append("PsiFile=").append(psiFile);
271             if (psiFile != null) {
272               final VirtualFile vFile = psiFile.getVirtualFile();
273               msg.append("\n\t").append("VirtualFile=").append(vFile);
274               if (vFile != null) {
275                 msg.append("\n\t").append("isInSourceContent=").append(fileIndex.isUnderSourceRootOfType(vFile, JavaModuleSourceRootTypes.SOURCES));
276               }
277             }
278             LOG.debug(msg.toString());
279           }
280           
281           if (psiFile == null) {
282             return null;
283           }
284           final VirtualFile vFile = psiFile.getVirtualFile();
285           if (vFile == null || !fileIndex.isUnderSourceRootOfType(vFile, JavaModuleSourceRootTypes.SOURCES)) {
286             return null; // this will switch off the check if at least one class is from libraries
287           }
288           list.add(vFile);
289         }
290         return list;
291       }
292     });
293   }
294
295   public boolean evaluateCondition(EvaluationContextImpl context, LocatableEvent event) throws EvaluateException {
296     if(CLASS_FILTERS_ENABLED){
297       String className = null;
298       final ObjectReference thisObject = (ObjectReference)context.getThisObject();
299       if(thisObject != null) {
300         className = thisObject.referenceType().name();
301       }
302       else {
303         final StackFrameProxyImpl frame = context.getFrameProxy();
304         if (frame != null) {
305           className = frame.location().declaringType().name();
306         }
307       }
308       if (className != null) {
309         boolean matches = false;
310         for (ClassFilter classFilter : getClassFilters()) {
311           if (classFilter.isEnabled() && classFilter.matches(className)) {
312             matches = true;
313             break;
314           }
315         }
316         if(!matches) {
317           return false;
318         }
319         for (ClassFilter classFilter : getClassExclusionFilters()) {
320           if (classFilter.isEnabled() && classFilter.matches(className)) {
321             return false;
322           }
323         }
324       }
325     }
326     return super.evaluateCondition(context, event);
327   }
328
329   public String toString() {
330     return getDescription();
331   }
332
333   @Override
334   public String getShortName() {
335     return getDisplayInfoInternal(false, 30);
336   }
337
338   public String getDisplayName() {
339     return getDisplayInfoInternal(true, -1);
340   }
341
342   private String getDisplayInfoInternal(boolean showPackageInfo, int totalTextLength) {
343     final RangeHighlighter highlighter = getHighlighter();
344     if(highlighter.isValid() && isValid()) {
345       final int lineNumber = (highlighter.getDocument().getLineNumber(highlighter.getStartOffset()) + 1);
346       String className = getClassName();
347       final boolean hasClassInfo = className != null && className.length() > 0;
348       final boolean hasMethodInfo = myMethodName != null && myMethodName.length() > 0;
349       if (hasClassInfo || hasMethodInfo) {
350         final StringBuilder info = StringBuilderSpinAllocator.alloc();
351         try {
352           boolean isFile = getSourcePosition().getFile().getName().equals(className);
353           String packageName = null;
354           if (hasClassInfo) {
355             final int dotIndex = className.lastIndexOf(".");
356             if (dotIndex >= 0 && !isFile) {
357               packageName = className.substring(0, dotIndex);
358               className = className.substring(dotIndex + 1); 
359             }
360
361             if (totalTextLength != -1) {
362               if (className.length() + (hasMethodInfo ? myMethodName.length() : 0) > totalTextLength + 3) {
363                 int offset = totalTextLength - (hasMethodInfo ? myMethodName.length() : 0);
364                 if (offset > 0 && offset < className.length()) {
365                   className = className.substring(className.length() - offset);
366                   info.append("...");
367                 }
368               }
369             }
370             
371             info.append(className);
372           }
373           if(hasMethodInfo) {
374             if (isFile) {
375               info.append(":");
376             }
377             else if (hasClassInfo) {
378               info.append(".");
379             }
380             info.append(myMethodName);
381           }
382           if (showPackageInfo && packageName != null) {
383             info.append(" (").append(packageName).append(")");
384           }
385           return DebuggerBundle.message("line.breakpoint.display.name.with.class.or.method", lineNumber, info.toString());
386         }
387         finally {
388           StringBuilderSpinAllocator.dispose(info);
389         }
390       }
391       return DebuggerBundle.message("line.breakpoint.display.name", lineNumber);
392     }
393     return DebuggerBundle.message("status.breakpoint.invalid");
394   }
395
396   private static @Nullable String findMethodName(final PsiFile file, final int offset) {
397     if (file instanceof JspFile) {
398       return null;
399     }
400     if (file instanceof PsiClassOwner) {
401       return ApplicationManager.getApplication().runReadAction(new Computable<String>() {
402         public String compute() {
403           final PsiMethod method = DebuggerUtilsEx.findPsiMethod(file, offset);
404           return method != null? method.getName() + "()" : null;
405         }
406       });
407     }
408     return null;
409   }
410
411   public String getEventMessage(LocatableEvent event) {
412     final Location location = event.location();
413     String sourceName = "Unknown Source";
414     try {
415       sourceName = location.sourceName();
416     }
417     catch (AbsentInformationException e) {
418       sourceName = getSourcePosition().getFile().getName();
419     }
420
421     final boolean printFullTrace = Registry.is("debugger.breakpoint.message.full.trace");
422
423     StringBuilder builder = new StringBuilder();
424     if (printFullTrace) {
425       builder.append(DebuggerBundle.message(
426         "status.line.breakpoint.reached.full.trace",
427         location.declaringType().name() + "." + location.method().name())
428       );
429       try {
430         final List<StackFrame> frames = event.thread().frames();
431         renderTrace(frames, builder);
432       }
433       catch (IncompatibleThreadStateException e) {
434         builder.append("Stacktrace not available: ").append(e.getMessage());
435       }
436     }
437     else {
438       builder.append(DebuggerBundle.message(
439         "status.line.breakpoint.reached",
440         location.declaringType().name() + "." + location.method().name(),
441         sourceName,
442         getLineIndex() + 1
443       ));
444     }
445     return builder.toString();
446   }
447
448   private static void renderTrace(List<StackFrame> frames, StringBuilder buffer) {
449     for (final StackFrame stackFrame : frames) {
450       final Location location = stackFrame.location();
451       buffer.append("\n\t  ").append(ThreadDumpAction.renderLocation(location));
452     }
453   }
454
455   public PsiElement getEvaluationElement() {
456     return PositionUtil.getContextElement(getSourcePosition());
457   }
458
459   protected static LineBreakpoint create(Project project, Document document, int lineIndex) {
460     VirtualFile virtualFile = FileDocumentManager.getInstance().getFile(document);
461     if (virtualFile == null) {
462       return null;
463     }
464
465     final RangeHighlighter highlighter = createHighlighter(project, document, lineIndex);
466     if (highlighter == null) {
467       return null;
468     }
469
470     LineBreakpoint breakpoint = new LineBreakpoint(project, highlighter);
471     return (LineBreakpoint)breakpoint.init();
472   }
473
474   public boolean canMoveTo(SourcePosition position) {
475     if (!super.canMoveTo(position)) {
476       return false;
477     }
478     final Document document = PsiDocumentManager.getInstance(getProject()).getDocument(position.getFile());
479     return canAddLineBreakpoint(myProject, document, position.getLine());
480   }
481
482   public static boolean canAddLineBreakpoint(Project project, final Document document, final int lineIndex) {
483     if (lineIndex < 0 || lineIndex >= document.getLineCount()) {
484       return false;
485     }
486     final BreakpointManager breakpointManager = DebuggerManagerEx.getInstanceEx(project).getBreakpointManager();
487     final LineBreakpoint breakpointAtLine = breakpointManager.findBreakpoint( document, document.getLineStartOffset(lineIndex), CATEGORY);
488     if (breakpointAtLine != null) {
489       // there already exists a line breakpoint at this line
490       return false;
491     }
492     PsiDocumentManager.getInstance(project).commitDocument(document);
493
494     final boolean[] canAdd = new boolean[]{false};
495     XDebuggerUtil.getInstance().iterateLine(project, document, lineIndex, new Processor<PsiElement>() {
496       public boolean process(PsiElement element) {
497         if ((element instanceof PsiWhiteSpace) || (PsiTreeUtil.getParentOfType(element, PsiComment.class, false) != null)) {
498           return true;
499         }
500         PsiElement child = element;
501         while(element != null) {
502
503           final int offset = element.getTextOffset();
504           if (offset >= 0) {
505             if (document.getLineNumber(offset) != lineIndex) {
506               break;
507             }
508           }
509           child = element;
510           element = element.getParent();
511         }
512
513         if(child instanceof PsiMethod && child.getTextRange().getEndOffset() >= document.getLineEndOffset(lineIndex)) {
514           PsiCodeBlock body = ((PsiMethod)child).getBody();
515           if(body == null) {
516             canAdd[0] = false;
517           }
518           else {
519             PsiStatement[] statements = body.getStatements();
520             canAdd[0] = statements.length > 0 && document.getLineNumber(statements[0].getTextOffset()) == lineIndex;
521           }
522         }
523         else {
524           canAdd[0] = true;
525         }
526         return false;
527       }
528     });
529
530     return canAdd[0];
531   }
532
533   public @Nullable String getMethodName() {
534     return myMethodName;
535   }
536 }