| /* |
| * Copyright 2000-2013 JetBrains s.r.o. |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| /* |
| * Class LineBreakpoint |
| * @author Jeka |
| */ |
| package com.intellij.debugger.ui.breakpoints; |
| |
| import com.intellij.debugger.DebuggerBundle; |
| import com.intellij.debugger.DebuggerManagerEx; |
| import com.intellij.debugger.SourcePosition; |
| import com.intellij.debugger.actions.ThreadDumpAction; |
| import com.intellij.debugger.engine.ContextUtil; |
| import com.intellij.debugger.engine.DebugProcessImpl; |
| import com.intellij.debugger.engine.evaluation.EvaluateException; |
| import com.intellij.debugger.engine.evaluation.EvaluationContextImpl; |
| import com.intellij.debugger.impl.DebuggerUtilsEx; |
| import com.intellij.debugger.jdi.StackFrameProxyImpl; |
| import com.intellij.icons.AllIcons; |
| import com.intellij.openapi.application.ApplicationManager; |
| import com.intellij.openapi.diagnostic.Logger; |
| import com.intellij.openapi.editor.Document; |
| import com.intellij.openapi.module.Module; |
| import com.intellij.openapi.project.Project; |
| import com.intellij.openapi.roots.ProjectFileIndex; |
| import com.intellij.openapi.roots.ProjectRootManager; |
| import com.intellij.openapi.util.Computable; |
| import com.intellij.openapi.util.Key; |
| import com.intellij.openapi.util.registry.Registry; |
| import com.intellij.openapi.vfs.VirtualFile; |
| import com.intellij.psi.*; |
| import com.intellij.psi.impl.java.stubs.index.JavaFullClassNameIndex; |
| import com.intellij.psi.jsp.JspFile; |
| import com.intellij.psi.search.EverythingGlobalScope; |
| import com.intellij.psi.search.GlobalSearchScope; |
| import com.intellij.psi.util.PsiTreeUtil; |
| import com.intellij.util.Function; |
| import com.intellij.util.Processor; |
| import com.intellij.util.StringBuilderSpinAllocator; |
| import com.intellij.util.containers.ContainerUtil; |
| import com.intellij.xdebugger.XDebuggerUtil; |
| import com.intellij.xdebugger.XSourcePosition; |
| import com.intellij.xdebugger.breakpoints.XBreakpoint; |
| import com.sun.jdi.*; |
| import com.sun.jdi.event.LocatableEvent; |
| import com.sun.jdi.request.BreakpointRequest; |
| import org.jetbrains.annotations.NonNls; |
| import org.jetbrains.annotations.NotNull; |
| import org.jetbrains.annotations.Nullable; |
| import org.jetbrains.jps.model.java.JavaModuleSourceRootTypes; |
| |
| import javax.swing.*; |
| import java.util.ArrayList; |
| import java.util.Collection; |
| import java.util.List; |
| |
| public class LineBreakpoint extends BreakpointWithHighlighter { |
| private static final Logger LOG = Logger.getInstance("#com.intellij.debugger.ui.breakpoints.LineBreakpoint"); |
| |
| @Nullable |
| private String myOwnerMethodName; |
| public static final @NonNls Key<LineBreakpoint> CATEGORY = BreakpointCategory.lookup("line_breakpoints"); |
| |
| protected LineBreakpoint(Project project, XBreakpoint xBreakpoint) { |
| super(project, xBreakpoint); |
| } |
| |
| @Override |
| protected Icon getDisabledIcon(boolean isMuted) { |
| final Breakpoint master = DebuggerManagerEx.getInstanceEx(myProject).getBreakpointManager().findMasterBreakpoint(this); |
| if (isMuted) { |
| return master == null? AllIcons.Debugger.Db_muted_disabled_breakpoint : AllIcons.Debugger.Db_muted_dep_line_breakpoint; |
| } |
| else { |
| return master == null? AllIcons.Debugger.Db_disabled_breakpoint : AllIcons.Debugger.Db_dep_line_breakpoint; |
| } |
| } |
| |
| @Override |
| protected Icon getSetIcon(boolean isMuted) { |
| if (isRemoveAfterHit()) { |
| return isMuted ? AllIcons.Debugger.Db_muted_temporary_breakpoint : AllIcons.Debugger.Db_temporary_breakpoint; |
| } |
| return isMuted? AllIcons.Debugger.Db_muted_breakpoint : AllIcons.Debugger.Db_set_breakpoint; |
| } |
| |
| @Override |
| protected Icon getInvalidIcon(boolean isMuted) { |
| return isMuted? AllIcons.Debugger.Db_muted_invalid_breakpoint : AllIcons.Debugger.Db_invalid_breakpoint; |
| } |
| |
| @Override |
| protected Icon getVerifiedIcon(boolean isMuted) { |
| if (isRemoveAfterHit()) { |
| return isMuted ? AllIcons.Debugger.Db_muted_temporary_breakpoint : AllIcons.Debugger.Db_temporary_breakpoint; |
| } |
| return isMuted? AllIcons.Debugger.Db_muted_verified_breakpoint : AllIcons.Debugger.Db_verified_breakpoint; |
| } |
| |
| @Override |
| protected Icon getVerifiedWarningsIcon(boolean isMuted) { |
| return isMuted? AllIcons.Debugger.Db_muted_verified_warning_breakpoint : AllIcons.Debugger.Db_verified_warning_breakpoint; |
| } |
| |
| @Override |
| public Key<LineBreakpoint> getCategory() { |
| return CATEGORY; |
| } |
| |
| @Override |
| protected void reload(PsiFile file) { |
| super.reload(file); |
| XSourcePosition position = myXBreakpoint.getSourcePosition(); |
| if (position != null) { |
| int offset = position.getOffset(); |
| myOwnerMethodName = findOwnerMethod(file, offset); |
| } |
| } |
| |
| @Override |
| protected void createOrWaitPrepare(DebugProcessImpl debugProcess, String classToBeLoaded) { |
| if (isInScopeOf(debugProcess, classToBeLoaded)) { |
| super.createOrWaitPrepare(debugProcess, classToBeLoaded); |
| } |
| } |
| |
| @Override |
| protected void createRequestForPreparedClass(final DebugProcessImpl debugProcess, final ReferenceType classType) { |
| if (!isInScopeOf(debugProcess, classType.name())) { |
| if (LOG.isDebugEnabled()) { |
| LOG.debug(classType.name() + " is out of debug-process scope, breakpoint request won't be created for line " + getLineIndex()); |
| } |
| return; |
| } |
| try { |
| List<Location> locations = debugProcess.getPositionManager().locationsOfLine(classType, getSourcePosition()); |
| if (!locations.isEmpty()) { |
| for (Location loc : locations) { |
| if (LOG.isDebugEnabled()) { |
| LOG.debug("Found location [codeIndex=" + loc.codeIndex() +"] for reference type " + classType.name() + " at line " + getLineIndex() + "; isObsolete: " + (debugProcess.getVirtualMachineProxy().versionHigher("1.4") && loc.method().isObsolete())); |
| } |
| if (!acceptLocation(debugProcess, classType, loc)) { |
| continue; |
| } |
| final BreakpointRequest request = debugProcess.getRequestsManager().createBreakpointRequest(this, loc); |
| debugProcess.getRequestsManager().enableRequest(request); |
| if (LOG.isDebugEnabled()) { |
| LOG.debug("Created breakpoint request for reference type " + classType.name() + " at line " + getLineIndex() + "; codeIndex=" + loc.codeIndex()); |
| } |
| } |
| } |
| else { |
| // there's no executable code in this class |
| debugProcess.getRequestsManager().setInvalid(this, DebuggerBundle.message( |
| "error.invalid.breakpoint.no.executable.code", (getLineIndex() + 1), classType.name()) |
| ); |
| if (LOG.isDebugEnabled()) { |
| LOG.debug("No locations of type " + classType.name() + " found at line " + getLineIndex()); |
| } |
| } |
| } |
| catch (ClassNotPreparedException ex) { |
| if (LOG.isDebugEnabled()) { |
| LOG.debug("ClassNotPreparedException: " + ex.getMessage()); |
| } |
| // there's a chance to add a breakpoint when the class is prepared |
| } |
| catch (ObjectCollectedException ex) { |
| if (LOG.isDebugEnabled()) { |
| LOG.debug("ObjectCollectedException: " + ex.getMessage()); |
| } |
| // there's a chance to add a breakpoint when the class is prepared |
| } |
| catch (InvalidLineNumberException ex) { |
| if (LOG.isDebugEnabled()) { |
| LOG.debug("InvalidLineNumberException: " + ex.getMessage()); |
| } |
| debugProcess.getRequestsManager().setInvalid(this, DebuggerBundle.message("error.invalid.breakpoint.bad.line.number")); |
| } |
| catch (InternalException ex) { |
| LOG.info(ex); |
| } |
| catch(Exception ex) { |
| LOG.info(ex); |
| } |
| updateUI(); |
| } |
| |
| protected boolean acceptLocation(DebugProcessImpl debugProcess, ReferenceType classType, Location loc) { |
| return true; |
| } |
| |
| private boolean isInScopeOf(DebugProcessImpl debugProcess, String className) { |
| final SourcePosition position = getSourcePosition(); |
| if (position != null) { |
| final VirtualFile breakpointFile = position.getFile().getVirtualFile(); |
| final ProjectFileIndex fileIndex = ProjectRootManager.getInstance(myProject).getFileIndex(); |
| if (breakpointFile != null && fileIndex.isUnderSourceRootOfType(breakpointFile, JavaModuleSourceRootTypes.SOURCES)) { |
| // apply filtering to breakpoints from content sources only, not for sources attached to libraries |
| final Collection<VirtualFile> candidates = findClassCandidatesInSourceContent(className, debugProcess.getSearchScope(), fileIndex); |
| if (LOG.isDebugEnabled()) { |
| LOG.debug("Found "+ (candidates == null? "null" : candidates.size()) + " candidate containing files for class " + className); |
| } |
| if (candidates == null) { |
| return true; |
| } |
| for (VirtualFile classFile : candidates) { |
| if (LOG.isDebugEnabled()) { |
| LOG.debug("Breakpoint file: " + breakpointFile.getPath()+ "; candidate file: " + classFile.getPath()); |
| } |
| if (breakpointFile.equals(classFile)) { |
| return true; |
| } |
| } |
| if (LOG.isDebugEnabled()) { |
| final GlobalSearchScope scope = debugProcess.getSearchScope(); |
| final boolean contains = scope.contains(breakpointFile); |
| final Project project = getProject(); |
| final List<VirtualFile> files = ContainerUtil.map( |
| JavaFullClassNameIndex.getInstance().get(className.hashCode(), project, scope), new Function<PsiClass, VirtualFile>() { |
| @Override |
| public VirtualFile fun(PsiClass aClass) { |
| return aClass.getContainingFile().getVirtualFile(); |
| } |
| }); |
| final List<VirtualFile> allFiles = ContainerUtil.map( |
| JavaFullClassNameIndex.getInstance().get(className.hashCode(), project, new EverythingGlobalScope(project)), new Function<PsiClass, VirtualFile>() { |
| @Override |
| public VirtualFile fun(PsiClass aClass) { |
| return aClass.getContainingFile().getVirtualFile(); |
| } |
| }); |
| final VirtualFile contentRoot = fileIndex.getContentRootForFile(breakpointFile); |
| final Module module = fileIndex.getModuleForFile(breakpointFile); |
| |
| LOG.debug("Did not find '" + |
| className + "' in " + scope + |
| "; contains=" + contains + |
| "; contentRoot=" + contentRoot + |
| "; module = " + module + |
| "; all files in index are: " + files+ |
| "; all possible files are: " + allFiles |
| ); |
| } |
| |
| return false; |
| } |
| } |
| return true; |
| } |
| |
| @Nullable |
| private Collection<VirtualFile> findClassCandidatesInSourceContent(final String className, final GlobalSearchScope scope, final ProjectFileIndex fileIndex) { |
| final int dollarIndex = className.indexOf("$"); |
| final String topLevelClassName = dollarIndex >= 0? className.substring(0, dollarIndex) : className; |
| return ApplicationManager.getApplication().runReadAction(new Computable<Collection<VirtualFile>>() { |
| @Override |
| @Nullable |
| public Collection<VirtualFile> compute() { |
| final PsiClass[] classes = JavaPsiFacade.getInstance(myProject).findClasses(topLevelClassName, scope); |
| if (LOG.isDebugEnabled()) { |
| LOG.debug("Found "+ classes.length + " classes " + topLevelClassName + " in scope "+scope); |
| } |
| if (classes.length == 0) { |
| return null; |
| } |
| final List<VirtualFile> list = new ArrayList<VirtualFile>(classes.length); |
| for (PsiClass aClass : classes) { |
| final PsiFile psiFile = aClass.getContainingFile(); |
| |
| if (LOG.isDebugEnabled()) { |
| final StringBuilder msg = new StringBuilder(); |
| msg.append("Checking class ").append(aClass.getQualifiedName()); |
| msg.append("\n\t").append("PsiFile=").append(psiFile); |
| if (psiFile != null) { |
| final VirtualFile vFile = psiFile.getVirtualFile(); |
| msg.append("\n\t").append("VirtualFile=").append(vFile); |
| if (vFile != null) { |
| msg.append("\n\t").append("isInSourceContent=").append(fileIndex.isUnderSourceRootOfType(vFile, JavaModuleSourceRootTypes.SOURCES)); |
| } |
| } |
| LOG.debug(msg.toString()); |
| } |
| |
| if (psiFile == null) { |
| return null; |
| } |
| final VirtualFile vFile = psiFile.getVirtualFile(); |
| if (vFile == null || !fileIndex.isUnderSourceRootOfType(vFile, JavaModuleSourceRootTypes.SOURCES)) { |
| return null; // this will switch off the check if at least one class is from libraries |
| } |
| list.add(vFile); |
| } |
| return list; |
| } |
| }); |
| } |
| |
| @Override |
| protected String calculateEventClass(EvaluationContextImpl context, LocatableEvent event) throws EvaluateException { |
| String className = null; |
| final ObjectReference thisObject = (ObjectReference)context.getThisObject(); |
| if (thisObject != null) { |
| className = thisObject.referenceType().name(); |
| } |
| else { |
| final StackFrameProxyImpl frame = context.getFrameProxy(); |
| if (frame != null) { |
| className = frame.location().declaringType().name(); |
| } |
| } |
| return className; |
| } |
| |
| public String toString() { |
| return getDescription(); |
| } |
| |
| @Override |
| public String getShortName() { |
| return getDisplayInfoInternal(false, 30); |
| } |
| |
| @Override |
| public String getDisplayName() { |
| return getDisplayInfoInternal(true, -1); |
| } |
| |
| private String getDisplayInfoInternal(boolean showPackageInfo, int totalTextLength) { |
| if(isValid()) { |
| final int lineNumber = myXBreakpoint.getSourcePosition().getLine() + 1; |
| String className = getClassName(); |
| final boolean hasClassInfo = className != null && className.length() > 0; |
| final String methodName = getMethodName(); |
| final String displayName = methodName != null? methodName + "()" : null; |
| final boolean hasMethodInfo = displayName != null && displayName.length() > 0; |
| if (hasClassInfo || hasMethodInfo) { |
| final StringBuilder info = StringBuilderSpinAllocator.alloc(); |
| try { |
| boolean isFile = myXBreakpoint.getSourcePosition().getFile().getName().equals(className); |
| String packageName = null; |
| if (hasClassInfo) { |
| final int dotIndex = className.lastIndexOf("."); |
| if (dotIndex >= 0 && !isFile) { |
| packageName = className.substring(0, dotIndex); |
| className = className.substring(dotIndex + 1); |
| } |
| |
| if (totalTextLength != -1) { |
| if (className.length() + (hasMethodInfo ? displayName.length() : 0) > totalTextLength + 3) { |
| int offset = totalTextLength - (hasMethodInfo ? displayName.length() : 0); |
| if (offset > 0 && offset < className.length()) { |
| className = className.substring(className.length() - offset); |
| info.append("..."); |
| } |
| } |
| } |
| |
| info.append(className); |
| } |
| if(hasMethodInfo) { |
| if (isFile) { |
| info.append(":"); |
| } |
| else if (hasClassInfo) { |
| info.append("."); |
| } |
| info.append(displayName); |
| } |
| if (showPackageInfo && packageName != null) { |
| info.append(" (").append(packageName).append(")"); |
| } |
| return DebuggerBundle.message("line.breakpoint.display.name.with.class.or.method", lineNumber, info.toString()); |
| } |
| finally { |
| StringBuilderSpinAllocator.dispose(info); |
| } |
| } |
| return DebuggerBundle.message("line.breakpoint.display.name", lineNumber); |
| } |
| return DebuggerBundle.message("status.breakpoint.invalid"); |
| } |
| |
| @Nullable |
| private static String findOwnerMethod(final PsiFile file, final int offset) { |
| if (offset < 0 || file instanceof JspFile) { |
| return null; |
| } |
| if (file instanceof PsiClassOwner) { |
| return ApplicationManager.getApplication().runReadAction(new Computable<String>() { |
| @Override |
| public String compute() { |
| final PsiMethod method = DebuggerUtilsEx.findPsiMethod(file, offset); |
| return method != null? method.getName() : null; |
| } |
| }); |
| } |
| return null; |
| } |
| |
| @Override |
| public String getEventMessage(LocatableEvent event) { |
| final Location location = event.location(); |
| String sourceName; |
| try { |
| sourceName = location.sourceName(); |
| } |
| catch (AbsentInformationException e) { |
| sourceName = getFileName(); |
| } |
| |
| final boolean printFullTrace = Registry.is("debugger.breakpoint.message.full.trace"); |
| |
| StringBuilder builder = new StringBuilder(); |
| if (printFullTrace) { |
| builder.append(DebuggerBundle.message( |
| "status.line.breakpoint.reached.full.trace", |
| location.declaringType().name() + "." + location.method().name()) |
| ); |
| try { |
| final List<StackFrame> frames = event.thread().frames(); |
| renderTrace(frames, builder); |
| } |
| catch (IncompatibleThreadStateException e) { |
| builder.append("Stacktrace not available: ").append(e.getMessage()); |
| } |
| } |
| else { |
| builder.append(DebuggerBundle.message( |
| "status.line.breakpoint.reached", |
| location.declaringType().name() + "." + location.method().name(), |
| sourceName, |
| getLineIndex() + 1 |
| )); |
| } |
| return builder.toString(); |
| } |
| |
| private static void renderTrace(List<StackFrame> frames, StringBuilder buffer) { |
| for (final StackFrame stackFrame : frames) { |
| final Location location = stackFrame.location(); |
| buffer.append("\n\t ").append(ThreadDumpAction.renderLocation(location)); |
| } |
| } |
| |
| @Override |
| public PsiElement getEvaluationElement() { |
| return ContextUtil.getContextElement(getSourcePosition()); |
| } |
| |
| public static LineBreakpoint create(@NotNull Project project, XBreakpoint xBreakpoint) { |
| LineBreakpoint breakpoint = new LineBreakpoint(project, xBreakpoint); |
| return (LineBreakpoint)breakpoint.init(); |
| } |
| |
| //@Override |
| //public boolean canMoveTo(SourcePosition position) { |
| // if (!super.canMoveTo(position)) { |
| // return false; |
| // } |
| // final Document document = PsiDocumentManager.getInstance(getProject()).getDocument(position.getFile()); |
| // return canAddLineBreakpoint(myProject, document, position.getLine()); |
| //} |
| |
| public static boolean canAddLineBreakpoint(Project project, final Document document, final int lineIndex) { |
| if (lineIndex < 0 || lineIndex >= document.getLineCount()) { |
| return false; |
| } |
| final BreakpointManager breakpointManager = DebuggerManagerEx.getInstanceEx(project).getBreakpointManager(); |
| final LineBreakpoint breakpointAtLine = breakpointManager.findBreakpoint( document, document.getLineStartOffset(lineIndex), CATEGORY); |
| if (breakpointAtLine != null) { |
| // there already exists a line breakpoint at this line |
| return false; |
| } |
| PsiDocumentManager.getInstance(project).commitDocument(document); |
| |
| final boolean[] canAdd = new boolean[]{false}; |
| XDebuggerUtil.getInstance().iterateLine(project, document, lineIndex, new Processor<PsiElement>() { |
| @Override |
| public boolean process(PsiElement element) { |
| if ((element instanceof PsiWhiteSpace) || (PsiTreeUtil.getParentOfType(element, PsiComment.class, false) != null)) { |
| return true; |
| } |
| PsiElement child = element; |
| while(element != null) { |
| |
| final int offset = element.getTextOffset(); |
| if (offset >= 0) { |
| if (document.getLineNumber(offset) != lineIndex) { |
| break; |
| } |
| } |
| child = element; |
| element = element.getParent(); |
| } |
| |
| if(child instanceof PsiMethod && child.getTextRange().getEndOffset() >= document.getLineEndOffset(lineIndex)) { |
| PsiCodeBlock body = ((PsiMethod)child).getBody(); |
| if(body == null) { |
| canAdd[0] = false; |
| } |
| else { |
| PsiStatement[] statements = body.getStatements(); |
| canAdd[0] = statements.length > 0 && document.getLineNumber(statements[0].getTextOffset()) == lineIndex; |
| } |
| } |
| else { |
| canAdd[0] = true; |
| } |
| return false; |
| } |
| }); |
| |
| return canAdd[0]; |
| } |
| |
| @Nullable |
| public String getMethodName() { |
| return myOwnerMethodName; |
| } |
| |
| } |