mirror of
https://gitflic.ru/project/openide/openide.git
synced 2026-09-27 10:03:11 +07:00
IDEA-205404 When navigating from Java stacktrace try to find the relevant position within line
This commit is contained in:
@@ -16,11 +16,20 @@
|
||||
package com.intellij.execution.filters;
|
||||
|
||||
import com.intellij.openapi.project.DumbAware;
|
||||
import com.intellij.psi.PsiArrayAccessExpression;
|
||||
import com.intellij.psi.PsiJavaToken;
|
||||
import com.intellij.psi.search.GlobalSearchScope;
|
||||
import com.intellij.psi.util.PsiElementFilter;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
public class ExceptionFilter implements Filter, DumbAware {
|
||||
private final ExceptionInfoCache myCache;
|
||||
private PsiElementFilter myNextLineRefiner;
|
||||
|
||||
private static final Pattern EXCEPTION_PATTERN = Pattern.compile("^Exception in thread \".+\" java\\.lang\\.(\\w+):");
|
||||
|
||||
public ExceptionFilter(@NotNull final GlobalSearchScope scope) {
|
||||
myCache = new ExceptionInfoCache(scope);
|
||||
@@ -29,6 +38,20 @@ public class ExceptionFilter implements Filter, DumbAware {
|
||||
@Override
|
||||
public Result applyFilter(final String line, final int textEndOffset) {
|
||||
ExceptionWorker worker = new ExceptionWorker(myCache);
|
||||
return worker.execute(line, textEndOffset);
|
||||
Result result = worker.execute(line, textEndOffset, myNextLineRefiner);
|
||||
myNextLineRefiner = result == null ? getRefinerFromException(line) : worker.getLocationRefiner();
|
||||
return result;
|
||||
}
|
||||
|
||||
private static PsiElementFilter getRefinerFromException(String line) {
|
||||
Matcher matcher = EXCEPTION_PATTERN.matcher(line);
|
||||
if(!matcher.find()) return null;
|
||||
String exceptionName = matcher.group(1);
|
||||
if("ArrayIndexOutOfBoundsException".equals(exceptionName)) {
|
||||
return element -> element instanceof PsiJavaToken &&
|
||||
element.textMatches("[") &&
|
||||
element.getParent() instanceof PsiArrayAccessExpression;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
+70
-4
@@ -15,18 +15,21 @@
|
||||
*/
|
||||
package com.intellij.java.execution.filters;
|
||||
|
||||
import com.intellij.execution.filters.ExceptionExFilterFactory;
|
||||
import com.intellij.execution.filters.ExceptionInfoCache;
|
||||
import com.intellij.execution.filters.ExceptionWorker;
|
||||
import com.intellij.execution.filters.FilterMixin;
|
||||
import com.intellij.execution.filters.*;
|
||||
import com.intellij.openapi.editor.Document;
|
||||
import com.intellij.openapi.editor.Editor;
|
||||
import com.intellij.openapi.editor.EditorFactory;
|
||||
import com.intellij.openapi.editor.LogicalPosition;
|
||||
import com.intellij.openapi.util.TextRange;
|
||||
import com.intellij.openapi.util.Trinity;
|
||||
import com.intellij.psi.PsiClass;
|
||||
import com.intellij.psi.search.GlobalSearchScope;
|
||||
import com.intellij.testFramework.fixtures.LightCodeInsightFixtureTestCase;
|
||||
import org.intellij.lang.annotations.Language;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* @author gregsh
|
||||
@@ -125,4 +128,67 @@ public class ExceptionWorkerTest extends LightCodeInsightFixtureTestCase {
|
||||
assertNull(ExceptionWorker.parseExceptionLine(line));
|
||||
assertNull(ExceptionWorker.parseExceptionLine(line + "\n"));
|
||||
}
|
||||
|
||||
public void testColumnFinder() {
|
||||
@Language("JAVA") String classText =
|
||||
"/** @noinspection ALL*/\n" +
|
||||
"public class SomeClass {\n" +
|
||||
" SomeClass() {\n" +
|
||||
" System.out.println((new int[0])[1]);\n" +
|
||||
" }\n" +
|
||||
" static class Inner implements Runnable {\n" +
|
||||
" int test = 4;\n" +
|
||||
" public void run() {\n" +
|
||||
" System.out.println(test + test() + SomeClass.test());\n" +
|
||||
" }\n" +
|
||||
" int test() { return 0; }\n" +
|
||||
" }\n" +
|
||||
" private static int test() {\n" +
|
||||
" new SomeClass() {};\n" +
|
||||
" return 1;\n" +
|
||||
" }\n" +
|
||||
" public static void main(String[] args) {\n" +
|
||||
" class X implements Runnable {\n" +
|
||||
" public void run() {\n" +
|
||||
" new Runnable() {\n" +
|
||||
" public void run() {\n" +
|
||||
" new Inner().run();this.run();\n" +
|
||||
" }\n" +
|
||||
" }.run();\n" +
|
||||
" }\n" +
|
||||
" }\n" +
|
||||
" new X().run();\n" +
|
||||
" }\n" +
|
||||
"}";
|
||||
myFixture.configureByText("SomeClass.java", classText);
|
||||
Editor editor = myFixture.getEditor();
|
||||
assertEquals(classText, editor.getDocument().getText());
|
||||
List<Trinity<String, Integer, Integer>> traceAndPositions = Arrays.asList(
|
||||
Trinity.create("Exception in thread \"main\" java.lang.ArrayIndexOutOfBoundsException: 1", null, null),
|
||||
Trinity.create("\tat SomeClass.<init>(SomeClass.java:4)", 4, 36),
|
||||
Trinity.create("\tat SomeClass$1.<init>(SomeClass.java:14)", 14, 9),
|
||||
Trinity.create("\tat SomeClass.test(SomeClass.java:14)", 14, 9),
|
||||
Trinity.create("\tat SomeClass.access$000(SomeClass.java:2)", 2, 1),
|
||||
Trinity.create("\tat SomeClass$Inner.run(SomeClass.java:9)", 9, 52),
|
||||
Trinity.create("\tat SomeClass$1X$1.run(SomeClass.java:22)", 22, 25),
|
||||
Trinity.create("\tat SomeClass$1X.run(SomeClass.java:24)", 24, 11),
|
||||
Trinity.create("\tat SomeClass.main(SomeClass.java:27)", 27, 13));
|
||||
ExceptionFilter filter = new ExceptionFilter(myFixture.getFile().getResolveScope());
|
||||
for (Trinity<String, Integer, Integer> line : traceAndPositions) {
|
||||
String stackLine = line.getFirst();
|
||||
Filter.Result result = filter.applyFilter(stackLine, stackLine.length());
|
||||
Integer row = line.getSecond();
|
||||
Integer column = line.getThird();
|
||||
if (row == null) {
|
||||
assertNull(result);
|
||||
}
|
||||
else {
|
||||
HyperlinkInfo info = result.getFirstHyperlinkInfo();
|
||||
assertNotNull(info);
|
||||
info.navigate(getProject());
|
||||
LogicalPosition actualPos = editor.getCaretModel().getLogicalPosition();
|
||||
assertEquals(new LogicalPosition(row - 1, column - 1), actualPos);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,18 +15,23 @@
|
||||
*/
|
||||
package com.intellij.execution.filters;
|
||||
|
||||
import com.intellij.openapi.editor.Document;
|
||||
import com.intellij.openapi.editor.colors.CodeInsightColors;
|
||||
import com.intellij.openapi.editor.colors.EditorColorsManager;
|
||||
import com.intellij.openapi.editor.markup.TextAttributes;
|
||||
import com.intellij.openapi.fileEditor.FileDocumentManager;
|
||||
import com.intellij.openapi.project.Project;
|
||||
import com.intellij.openapi.roots.ProjectFileIndex;
|
||||
import com.intellij.openapi.roots.ProjectRootManager;
|
||||
import com.intellij.openapi.util.Pair;
|
||||
import com.intellij.openapi.util.TextRange;
|
||||
import com.intellij.openapi.util.text.StringUtil;
|
||||
import com.intellij.openapi.vfs.VirtualFile;
|
||||
import com.intellij.psi.PsiClass;
|
||||
import com.intellij.psi.PsiFile;
|
||||
import com.intellij.psi.*;
|
||||
import com.intellij.psi.search.PsiShortNamesCache;
|
||||
import com.intellij.psi.util.ClassUtil;
|
||||
import com.intellij.psi.util.PsiElementFilter;
|
||||
import com.intellij.psi.util.PsiTreeUtil;
|
||||
import com.intellij.util.ArrayUtil;
|
||||
import com.intellij.util.ui.UIUtil;
|
||||
import org.jetbrains.annotations.NonNls;
|
||||
@@ -36,6 +41,7 @@ import org.jetbrains.annotations.Nullable;
|
||||
import java.awt.*;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.function.ToIntFunction;
|
||||
|
||||
public class ExceptionWorker {
|
||||
@NonNls private static final String AT = "at";
|
||||
@@ -49,6 +55,7 @@ public class ExceptionWorker {
|
||||
private String myMethod;
|
||||
private ParsedLine myInfo;
|
||||
private final ExceptionInfoCache myCache;
|
||||
private PsiElementFilter myLocationRefiner;
|
||||
|
||||
public ExceptionWorker(@NotNull ExceptionInfoCache cache) {
|
||||
myProject = cache.getProject();
|
||||
@@ -56,6 +63,10 @@ public class ExceptionWorker {
|
||||
}
|
||||
|
||||
public Filter.Result execute(final String line, final int textEndOffset) {
|
||||
return execute(line, textEndOffset, null);
|
||||
}
|
||||
|
||||
public Filter.Result execute(final String line, final int textEndOffset, PsiElementFilter elementMatcher) {
|
||||
myResult = null;
|
||||
myInfo = parseExceptionLine(line);
|
||||
if (myInfo == null) {
|
||||
@@ -113,12 +124,23 @@ public class ExceptionWorker {
|
||||
else {
|
||||
virtualFiles = virtualFilesInContent;
|
||||
}
|
||||
HyperlinkInfo linkInfo = HyperlinkInfoFactory.getInstance().createMultipleFilesHyperlinkInfo(virtualFiles, myInfo.lineNumber - 1, myProject);
|
||||
ToIntFunction<PsiFile> columnFinder = elementMatcher == null ? null : new ExceptionColumnFinder(elementMatcher, myInfo.lineNumber - 1);
|
||||
HyperlinkInfo linkInfo =
|
||||
HyperlinkInfoFactory.getInstance().createMultipleFilesHyperlinkInfo(virtualFiles, myInfo.lineNumber - 1, myProject, columnFinder);
|
||||
Filter.Result result = new Filter.Result(highlightStartOffset, highlightEndOffset, linkInfo, attributes);
|
||||
if (myMethod.startsWith("access$")) {
|
||||
myLocationRefiner = elementMatcher;
|
||||
} else {
|
||||
myLocationRefiner = new StackFrameMatcher(line, myInfo);
|
||||
}
|
||||
myResult = result;
|
||||
return result;
|
||||
}
|
||||
|
||||
public PsiElementFilter getLocationRefiner() {
|
||||
return myLocationRefiner;
|
||||
}
|
||||
|
||||
private static int getLineNumber(String lineString) {
|
||||
// some quick checks to avoid costly exceptions
|
||||
if (lineString.isEmpty() || lineString.length() > 9 || !Character.isDigit(lineString.charAt(0))) {
|
||||
@@ -305,4 +327,100 @@ public class ExceptionWorker {
|
||||
return new ParsedLine(classFqnRange, methodNameRange, fileLineRange, fileAndLine.substring(0, colonIndex).trim(), lineNumber);
|
||||
}
|
||||
}
|
||||
|
||||
private static class StackFrameMatcher implements PsiElementFilter {
|
||||
private final String myMethodName;
|
||||
private final String myClassName;
|
||||
private final String mySimpleName;
|
||||
private final boolean myHasDollarInName;
|
||||
|
||||
private StackFrameMatcher(@NotNull String line, @NotNull ParsedLine info) {
|
||||
myMethodName = info.methodNameRange.substring(line);
|
||||
myClassName = info.classFqnRange.substring(line);
|
||||
mySimpleName = StringUtil.getShortName(myClassName);
|
||||
myHasDollarInName = mySimpleName.contains("$");
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isAccepted(PsiElement element) {
|
||||
if (!(element instanceof PsiIdentifier)) return false;
|
||||
if (myMethodName.equals("<init>")) {
|
||||
if (myHasDollarInName || element.textMatches(StringUtil.getShortName(myClassName))) {
|
||||
PsiElement parent = element.getParent();
|
||||
while (parent instanceof PsiJavaCodeReferenceElement) {
|
||||
parent = parent.getParent();
|
||||
}
|
||||
if (parent instanceof PsiAnonymousClass) {
|
||||
return isTargetClass(parent) || isTargetClass(((PsiAnonymousClass)parent).getSuperClass());
|
||||
}
|
||||
if (parent instanceof PsiNewExpression) {
|
||||
PsiJavaCodeReferenceElement ref = ((PsiNewExpression)parent).getClassOrAnonymousClassReference();
|
||||
if (ref != null && isTargetClass(ref.resolve())) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (element.textMatches(myMethodName)) {
|
||||
PsiElement parent = element.getParent();
|
||||
if (parent instanceof PsiReferenceExpression) {
|
||||
PsiElement target = ((PsiReferenceExpression)parent).resolve();
|
||||
if (target instanceof PsiMethod && isTargetClass(((PsiMethod)target).getContainingClass())) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private boolean isTargetClass(PsiElement psiClass) {
|
||||
if (!(psiClass instanceof PsiClass)) return false;
|
||||
return isTargetClass((PsiClass)psiClass, myClassName);
|
||||
}
|
||||
|
||||
private static boolean isTargetClass(PsiClass psiClass, String className) {
|
||||
if (psiClass == null) return false;
|
||||
String name = psiClass.getQualifiedName();
|
||||
if (className.equals(name)) return true;
|
||||
if (StringUtil.getShortName(className).contains("$")) {
|
||||
String prefix = StringUtil.substringBeforeLast(className, "$");
|
||||
String suffix = StringUtil.substringAfterLast(className, "$");
|
||||
PsiClass containingClass = PsiTreeUtil.getParentOfType(psiClass, PsiClass.class, true);
|
||||
return suffix != null && isTargetClass(containingClass, prefix) &&
|
||||
ClassUtil.findNonQualifiedClassByIndex(suffix, containingClass, true) == psiClass;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private static class ExceptionColumnFinder implements ToIntFunction<PsiFile> {
|
||||
private final PsiElementFilter myElementMatcher;
|
||||
private final int myLineNumber;
|
||||
|
||||
private ExceptionColumnFinder(@NotNull PsiElementFilter elementMatcher, int lineNumber) {
|
||||
myElementMatcher = elementMatcher;
|
||||
myLineNumber = lineNumber;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int applyAsInt(PsiFile file) {
|
||||
Document document = FileDocumentManager.getInstance().getDocument(file.getVirtualFile());
|
||||
if (document == null || document.getLineCount() <= myLineNumber) return 0;
|
||||
int startOffset = document.getLineStartOffset(myLineNumber);
|
||||
int endOffset = document.getLineEndOffset(myLineNumber);
|
||||
PsiElement element = file.findElementAt(startOffset);
|
||||
List<PsiElement> candidates = new ArrayList<>();
|
||||
while (element != null && element.getTextRange().getStartOffset() < endOffset) {
|
||||
if (myElementMatcher.isAccepted(element)) {
|
||||
candidates.add(element);
|
||||
if (candidates.size() > 1) return 0;
|
||||
}
|
||||
element = PsiTreeUtil.nextLeaf(element);
|
||||
}
|
||||
if (candidates.size() == 1) {
|
||||
return candidates.get(0).getTextRange().getStartOffset() - startOffset;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,9 +18,11 @@ package com.intellij.execution.filters;
|
||||
import com.intellij.openapi.components.ServiceManager;
|
||||
import com.intellij.openapi.project.Project;
|
||||
import com.intellij.openapi.vfs.VirtualFile;
|
||||
import com.intellij.psi.PsiFile;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.function.ToIntFunction;
|
||||
|
||||
/**
|
||||
* @author nik
|
||||
@@ -34,4 +36,18 @@ public abstract class HyperlinkInfoFactory {
|
||||
@NotNull
|
||||
public abstract HyperlinkInfo createMultipleFilesHyperlinkInfo(@NotNull List<VirtualFile> files,
|
||||
int line, @NotNull Project project);
|
||||
|
||||
/**
|
||||
* Creates a hyperlink which points to several files with ability to calculate a position inside line
|
||||
* @param files list of files to navigate to (will be suggested to user)
|
||||
* @param line line number to navigate to
|
||||
* @param project a project
|
||||
* @param columnFinder a function which accepts a selected file and returns a column within the specified line to navigate to
|
||||
* @return newly created HyperlinkInfo which navigates to given line and column
|
||||
*/
|
||||
@NotNull
|
||||
public abstract HyperlinkInfo createMultipleFilesHyperlinkInfo(@NotNull List<VirtualFile> files,
|
||||
int line,
|
||||
@NotNull Project project,
|
||||
ToIntFunction<? super PsiFile> columnFinder);
|
||||
}
|
||||
|
||||
+14
-1
@@ -15,12 +15,16 @@
|
||||
*/
|
||||
package com.intellij.execution.filters.impl;
|
||||
|
||||
import com.intellij.execution.filters.*;
|
||||
import com.intellij.execution.filters.HyperlinkInfo;
|
||||
import com.intellij.execution.filters.HyperlinkInfoFactory;
|
||||
import com.intellij.openapi.project.Project;
|
||||
import com.intellij.openapi.vfs.VirtualFile;
|
||||
import com.intellij.psi.PsiFile;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.function.ToIntFunction;
|
||||
|
||||
/**
|
||||
* @author nik
|
||||
@@ -33,4 +37,13 @@ public class HyperlinkInfoFactoryImpl extends HyperlinkInfoFactory {
|
||||
int line, @NotNull Project project) {
|
||||
return new MultipleFilesHyperlinkInfo(files, line, project);
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
public HyperlinkInfo createMultipleFilesHyperlinkInfo(@NotNull List<VirtualFile> files,
|
||||
int line,
|
||||
@NotNull Project project,
|
||||
@Nullable ToIntFunction<? super PsiFile> columnFinder) {
|
||||
return new MultipleFilesHyperlinkInfo(files, line, project, columnFinder);
|
||||
}
|
||||
}
|
||||
|
||||
+13
-2
@@ -37,6 +37,7 @@ import org.jetbrains.annotations.Nullable;
|
||||
import javax.swing.*;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.function.ToIntFunction;
|
||||
|
||||
/**
|
||||
* @author nik
|
||||
@@ -45,11 +46,20 @@ class MultipleFilesHyperlinkInfo extends HyperlinkInfoBase implements FileHyperl
|
||||
private final List<? extends VirtualFile> myVirtualFiles;
|
||||
private final int myLineNumber;
|
||||
private final Project myProject;
|
||||
private final ToIntFunction<? super PsiFile> myColumnFinder;
|
||||
|
||||
MultipleFilesHyperlinkInfo(@NotNull List<? extends VirtualFile> virtualFiles, int lineNumber, @NotNull Project project) {
|
||||
this(virtualFiles, lineNumber, project, null);
|
||||
}
|
||||
|
||||
MultipleFilesHyperlinkInfo(@NotNull List<? extends VirtualFile> virtualFiles,
|
||||
int lineNumber,
|
||||
@NotNull Project project,
|
||||
@Nullable ToIntFunction<? super PsiFile> columnFinder) {
|
||||
myVirtualFiles = virtualFiles;
|
||||
myLineNumber = lineNumber;
|
||||
myProject = project;
|
||||
myColumnFinder = columnFinder == null ? f -> 0 : columnFinder;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -75,7 +85,8 @@ class MultipleFilesHyperlinkInfo extends HyperlinkInfoBase implements FileHyperl
|
||||
if (currentFiles.isEmpty()) return;
|
||||
|
||||
if (currentFiles.size() == 1) {
|
||||
new OpenFileHyperlinkInfo(myProject, currentFiles.get(0).getVirtualFile(), myLineNumber).navigate(project);
|
||||
PsiFile file = currentFiles.get(0);
|
||||
new OpenFileHyperlinkInfo(myProject, file.getVirtualFile(), myLineNumber, myColumnFinder.applyAsInt(file)).navigate(project);
|
||||
}
|
||||
else {
|
||||
JFrame frame = WindowManager.getInstance().getFrame(project);
|
||||
@@ -86,7 +97,7 @@ class MultipleFilesHyperlinkInfo extends HyperlinkInfoBase implements FileHyperl
|
||||
.setTitle("Choose Target File")
|
||||
.setItemChosenCallback((selectedValue) -> {
|
||||
VirtualFile file = selectedValue.getVirtualFile();
|
||||
new OpenFileHyperlinkInfo(myProject, file, myLineNumber).navigate(project);
|
||||
new OpenFileHyperlinkInfo(myProject, file, myLineNumber, myColumnFinder.applyAsInt(selectedValue)).navigate(project);
|
||||
})
|
||||
.createPopup();
|
||||
if (hyperlinkLocationPoint != null) {
|
||||
|
||||
Reference in New Issue
Block a user