From c6e9c601937bab82a28b686916a7363a39e08ffc Mon Sep 17 00:00:00 2001 From: Mikhail Pyltsin Date: Mon, 29 Jan 2024 16:48:19 +0100 Subject: [PATCH] IJ-CR-123098 [java-console] IDEA-331307 Provide navigation for logs for jvm languages - resolve only on links - reuse `codePointAt` GitOrigin-RevId: 207034ef37fe94d4cbb6bbe5e3f68c7ce8c0cbdd --- .../messages/JvmAnalysisBundle.properties | 2 + .../console/ClassFinderFilter.kt | 71 ++++++------ ...ClassInfoCache.kt => ClassInfoResolver.kt} | 12 +-- .../console/LogFinderHyperlinkHandler.kt | 14 +-- .../OnFlyMultipleFilesHyperlinkInfo.kt | 60 +++++++++++ .../internal/LogFinderHyperlinkTestBase.kt | 10 +- .../console/JavaLogFinderHyperlinkTest.kt | 41 +++++++ .../impl/MultipleFilesHyperlinkInfo.java | 74 +------------ .../impl/MultipleFilesHyperlinkInfoBase.java | 102 ++++++++++++++++++ 9 files changed, 262 insertions(+), 124 deletions(-) rename jvm/jvm-analysis-impl/src/com/intellij/analysis/customization/console/{ClassInfoCache.kt => ClassInfoResolver.kt} (88%) create mode 100644 jvm/jvm-analysis-impl/src/com/intellij/analysis/customization/console/OnFlyMultipleFilesHyperlinkInfo.kt create mode 100644 platform/lang-impl/src/com/intellij/execution/filters/impl/MultipleFilesHyperlinkInfoBase.java diff --git a/jvm/jvm-analysis-impl/resources/messages/JvmAnalysisBundle.properties b/jvm/jvm-analysis-impl/resources/messages/JvmAnalysisBundle.properties index 593c0feb6ff5..e760a66bc8dc 100644 --- a/jvm/jvm-analysis-impl/resources/messages/JvmAnalysisBundle.properties +++ b/jvm/jvm-analysis-impl/resources/messages/JvmAnalysisBundle.properties @@ -183,6 +183,8 @@ jvm.inspection.logging.condition.disagrees.with.log.statement.problem.descriptor jvm.inspection.logging.condition.disagrees.with.log.statement.fix.name=Change level of the {0, choice, 0#condition|1#call} jvm.inspection.logging.condition.disagrees.with.log.statement.fix.family.name=Change log level +action.find.similar.stack.call.methods.not.found=Similar classes are not found + jvm.inspection.test.failed.line.display.name=Failed line in test jvm.inspections.thread.run.display.name=Call to 'Thread.run()' diff --git a/jvm/jvm-analysis-impl/src/com/intellij/analysis/customization/console/ClassFinderFilter.kt b/jvm/jvm-analysis-impl/src/com/intellij/analysis/customization/console/ClassFinderFilter.kt index e7447a7925d7..9621a1a68aa5 100644 --- a/jvm/jvm-analysis-impl/src/com/intellij/analysis/customization/console/ClassFinderFilter.kt +++ b/jvm/jvm-analysis-impl/src/com/intellij/analysis/customization/console/ClassFinderFilter.kt @@ -3,12 +3,9 @@ package com.intellij.analysis.customization.console import com.intellij.execution.filters.Filter import com.intellij.execution.filters.HyperlinkInfo -import com.intellij.execution.filters.HyperlinkInfoFactory import com.intellij.openapi.editor.markup.EffectType import com.intellij.openapi.editor.markup.TextAttributes import com.intellij.openapi.project.Project -import com.intellij.openapi.util.text.StringUtil -import com.intellij.openapi.vfs.VirtualFile import com.intellij.psi.search.GlobalSearchScope import com.intellij.util.ui.NamedColorUtil import org.jetbrains.annotations.NonNls @@ -16,21 +13,21 @@ import org.jetbrains.annotations.NonNls internal data class ProbableClassName(val from: Int, val to: Int, val fullLine: String, - val shortClassName: String, - val packageName: String, - val virtualFiles: List) + val fullClassName: String) -private val EXCEPTION_IN_THREAD: @NonNls String = "Exception in thread \"" -private val CAUSED_BY: @NonNls String = "Caused by: " -private val AT: @NonNls String = "\tat " +private const val EXCEPTION_IN_THREAD: @NonNls String = "Exception in thread \"" +private const val CAUSED_BY: @NonNls String = "Caused by: " +private const val AT: @NonNls String = "\tat " + +private const val POINT_CODE = '.'.code internal class ClassFinderFilter(private val myProject: Project, myScope: GlobalSearchScope) : Filter { - private val cache = ClassInfoCache(myProject, myScope) + private val cache = ClassInfoResolver(myProject, myScope) override fun applyFilter(line: String, entireLength: Int): Filter.Result? { val textStartOffset = entireLength - line.length - val expectedClasses = findProbableClasses(line, cache) + val expectedClasses = findProbableClasses(line) val results: MutableList = ArrayList() for (probableClass in expectedClasses) { val attributes = hyperLinkAttributes() @@ -57,40 +54,53 @@ internal class ClassFinderFilter(private val myProject: Project, myScope: Global private fun getHyperLink(probableClassName: ProbableClassName): HyperlinkInfo { - return HyperlinkInfoFactory.getInstance() - .createMultipleFilesHyperlinkInfo(probableClassName.virtualFiles, 0, myProject, LogFinderHyperlinkHandler(probableClassName)) + return OnFlyMultipleFilesHyperlinkInfo(cache, probableClassName,0, myProject, LogFinderHyperlinkHandler(probableClassName)) } companion object { - private fun findProbableClasses(line: String, cache: ClassInfoCache): List { - if (line.startsWith(EXCEPTION_IN_THREAD) || line.startsWith(CAUSED_BY) || line.startsWith(AT)) { + private fun findProbableClasses(line: String): List { + if (line.isBlank() || line.startsWith(EXCEPTION_IN_THREAD) || line.startsWith(CAUSED_BY) || line.startsWith(AT)) { return emptyList() } val result = mutableListOf() var start = -1 var pointCount = 0 - for (i in line.indices) { - val c = line.codePointAt(i) - if (start == -1 && isJavaIdentifierStart(c)) { + var i = 0 + var first = true + while (true) { + if (!first) { + val previousPoint = line.codePointAt(i) + i += Character.charCount(previousPoint) + if (i >= line.length) { + break + } + } + else { + first = false + } + + val point = line.codePointAt(i) + if (start == -1 && isJavaIdentifierStart(point)) { start = i continue } - if (start != -1 && c == '.'.code) { + if (start != -1 && point == POINT_CODE) { pointCount++ continue } if (start != -1 && - ((line[i - 1] == '.' && isJavaIdentifierStart(c)) || - (line[i - 1] != '.' && isJavaIdentifierPart(c)))) { - if (i == line.lastIndex && pointCount >= 2) { - addProbableClass(line, start, i + 1, cache, result) + ((line.codePointAt(i - 1) == POINT_CODE && isJavaIdentifierStart(point)) || + (line.codePointAt(i - 1) != POINT_CODE && isJavaIdentifierPart(point)))) { + val charCount = Character.charCount(point) + if (i + charCount >= line.length && pointCount >= 2) { + addProbableClass(line, start, line.length, result) } continue } if (pointCount >= 2) { - addProbableClass(line, start, i, cache, result) + addProbableClass(line, start, i, result) } pointCount = 0 start = -1 @@ -111,7 +121,6 @@ internal class ClassFinderFilter(private val myProject: Project, myScope: Global private fun addProbableClass(line: String, startInclusive: Int, endExclusive: Int, - cache: ClassInfoCache, result: MutableList) { var actualEndExclusive = endExclusive if (actualEndExclusive > 0 && line[actualEndExclusive - 1] == '.') { @@ -119,15 +128,9 @@ internal class ClassFinderFilter(private val myProject: Project, myScope: Global } val fullClassName = line.substring(startInclusive, actualEndExclusive) if (canBeShortenedFullyQualifiedClassName(fullClassName) && isJavaStyle(fullClassName)) { - val packageName = StringUtil.getPackageName(fullClassName) - val className = fullClassName.substring(packageName.length + 1) - val resolvedClasses = cache.resolveClasses(className, packageName) - if (resolvedClasses.classes.isNotEmpty()) { - val probableClassName = ProbableClassName(startInclusive + fullClassName.lastIndexOf(".") + 1, - startInclusive + fullClassName.length, - line, className, packageName, resolvedClasses.classes.values.toList()) - result.add(probableClassName) - } + val probableClassName = ProbableClassName(startInclusive + fullClassName.lastIndexOf(".") + 1, + startInclusive + fullClassName.length, line, fullClassName) + result.add(probableClassName) } } diff --git a/jvm/jvm-analysis-impl/src/com/intellij/analysis/customization/console/ClassInfoCache.kt b/jvm/jvm-analysis-impl/src/com/intellij/analysis/customization/console/ClassInfoResolver.kt similarity index 88% rename from jvm/jvm-analysis-impl/src/com/intellij/analysis/customization/console/ClassInfoCache.kt rename to jvm/jvm-analysis-impl/src/com/intellij/analysis/customization/console/ClassInfoResolver.kt index f5f4b3af9d75..3d91c4d4e21b 100644 --- a/jvm/jvm-analysis-impl/src/com/intellij/analysis/customization/console/ClassInfoCache.kt +++ b/jvm/jvm-analysis-impl/src/com/intellij/analysis/customization/console/ClassInfoResolver.kt @@ -9,9 +9,8 @@ import com.intellij.psi.PsiClass import com.intellij.psi.PsiElement import com.intellij.psi.search.GlobalSearchScope import com.intellij.psi.search.PsiShortNamesCache -import com.intellij.util.containers.ContainerUtil -class ClassInfoCache(val project: Project, private val mySearchScope: GlobalSearchScope) { +class ClassInfoResolver(val project: Project, private val mySearchScope: GlobalSearchScope) { companion object { private fun findClasses(project: Project, @@ -56,15 +55,7 @@ class ClassInfoCache(val project: Project, private val mySearchScope: GlobalSear } } - private val myCache = ContainerUtil.createConcurrentSoftValueMap() - fun resolveClasses(className: String, packageName: String): ClassResolveInfo { - val key = "$packageName.$className" - val cached = myCache[key] - if (cached != null && cached.isValid) { - return cached - } - if (isDumb(project)) { return ClassResolveInfo.EMPTY } @@ -77,7 +68,6 @@ class ClassInfoCache(val project: Project, private val mySearchScope: GlobalSear .toMap() val result = if (mapWithClasses.isEmpty()) ClassResolveInfo.EMPTY else ClassResolveInfo(mapWithClasses) - myCache[key] = result return result } diff --git a/jvm/jvm-analysis-impl/src/com/intellij/analysis/customization/console/LogFinderHyperlinkHandler.kt b/jvm/jvm-analysis-impl/src/com/intellij/analysis/customization/console/LogFinderHyperlinkHandler.kt index c96da319b550..beb8e3dabbe7 100644 --- a/jvm/jvm-analysis-impl/src/com/intellij/analysis/customization/console/LogFinderHyperlinkHandler.kt +++ b/jvm/jvm-analysis-impl/src/com/intellij/analysis/customization/console/LogFinderHyperlinkHandler.kt @@ -23,7 +23,7 @@ import org.jetbrains.uast.* internal class LogFinderHyperlinkHandler(private val probableClassName: ProbableClassName) : HyperlinkInfoFactory.HyperlinkHandler { override fun onLinkFollowed(project: Project, file: VirtualFile, targetEditor: Editor, originalEditor: Editor?) { - LogConsoleLogHandlerCollectors.logHandleClass(project, probableClassName.virtualFiles.size) + LogConsoleLogHandlerCollectors.logHandleClass(project, 1) val psiFile = PsiManager.getInstance(project).findFile(file) if (psiFile == null) return @@ -102,13 +102,14 @@ internal class LogFinderHyperlinkHandler(private val probableClassName: Probable } } -private class LogVisitor(private val probableClassName: ProbableClassName) : PsiRecursiveElementVisitor() { +internal class LogVisitor(private val probableClassName: ProbableClassName) : PsiRecursiveElementVisitor() { val similarClasses = mutableSetOf() val similarCalls = mutableSetOf() + val shortClassName = probableClassName.fullClassName.substringAfterLast('.') + override fun visitElement(element: PsiElement) { val uClass = element.toUElementOfType() - if (uClass != null && - probableClassName.shortClassName == uClass.javaPsi.name) { + if (uClass != null && shortClassName == uClass.javaPsi.name) { similarClasses.add(uClass) } val uCall = element.toUElementOfType() @@ -143,10 +144,9 @@ private class LogVisitor(private val probableClassName: ProbableClassName) : Psi if (logStringArgument == null) return false val calculateValue = LoggingStringPartEvaluator.calculateValue(logStringArgument) ?: return false val fullLine = probableClassName.fullLine - val classFullName = probableClassName.packageName + "." + probableClassName.shortClassName - var startPoint = probableClassName.fullLine.indexOf(classFullName) + var startPoint = probableClassName.fullLine.indexOf(probableClassName.fullClassName) if (startPoint == -1) return false - startPoint += classFullName.length + startPoint += probableClassName.fullClassName.length if (calculateValue.none { it.isConstant && it.text != null }) return false for (value in calculateValue) { if (value.isConstant && value.text != null) { diff --git a/jvm/jvm-analysis-impl/src/com/intellij/analysis/customization/console/OnFlyMultipleFilesHyperlinkInfo.kt b/jvm/jvm-analysis-impl/src/com/intellij/analysis/customization/console/OnFlyMultipleFilesHyperlinkInfo.kt new file mode 100644 index 000000000000..6e78ff49e5b0 --- /dev/null +++ b/jvm/jvm-analysis-impl/src/com/intellij/analysis/customization/console/OnFlyMultipleFilesHyperlinkInfo.kt @@ -0,0 +1,60 @@ +// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. +package com.intellij.analysis.customization.console + +import com.intellij.analysis.JvmAnalysisBundle +import com.intellij.codeInsight.hint.HintUtil +import com.intellij.execution.filters.HyperlinkInfoFactory.HyperlinkHandler +import com.intellij.execution.filters.impl.MultipleFilesHyperlinkInfoBase +import com.intellij.openapi.fileEditor.OpenFileDescriptor +import com.intellij.openapi.project.Project +import com.intellij.openapi.ui.popup.Balloon +import com.intellij.openapi.ui.popup.JBPopupFactory +import com.intellij.openapi.util.text.StringUtil +import com.intellij.psi.PsiFile +import com.intellij.psi.PsiManager +import com.intellij.ui.awt.RelativePoint +import org.jetbrains.annotations.ApiStatus + +@ApiStatus.Internal +class OnFlyMultipleFilesHyperlinkInfo internal constructor(private val myInfoCache: ClassInfoResolver, + private val probableClassName: ProbableClassName, + lineNumber: Int, + project: Project, + action: HyperlinkHandler?) : + MultipleFilesHyperlinkInfoBase(lineNumber, project, action) { + override fun getFiles(project: Project): List { + val packageName = StringUtil.getPackageName(probableClassName.fullClassName) + if (packageName.length == probableClassName.fullClassName.length) return emptyList() + val className = probableClassName.fullClassName.substring(packageName.length + 1) + val resolvedClasses = myInfoCache.resolveClasses(className, packageName) + val currentFiles: MutableList = ArrayList() + for (file in resolvedClasses.classes.values) { + if (!file.isValid) continue + val psiFile = PsiManager.getInstance(project).findFile(file) + if (psiFile != null) { + val navigationElement = psiFile.navigationElement // Sources may be downloaded. + if (navigationElement is PsiFile) { + currentFiles.add(navigationElement) + continue + } + currentFiles.add(psiFile) + } + } + return currentFiles + } + + override fun showNotFound(project: Project, hyperlinkLocationPoint: RelativePoint?) { + if (hyperlinkLocationPoint == null) return + val message = JvmAnalysisBundle.message("action.find.similar.stack.call.methods.not.found") + val label = HintUtil.createWarningLabel(message) + JBPopupFactory.getInstance().createBalloonBuilder(label) + .setFadeoutTime(4000) + .setFillColor(HintUtil.getWarningColor()) + .createBalloon() + .show(RelativePoint(hyperlinkLocationPoint.screenPoint), Balloon.Position.above) + } + + override fun getDescriptor(): OpenFileDescriptor? { + return null + } +} diff --git a/jvm/jvm-analysis-internal-testFramework/src/com/intellij/jvm/analysis/internal/testFramework/internal/LogFinderHyperlinkTestBase.kt b/jvm/jvm-analysis-internal-testFramework/src/com/intellij/jvm/analysis/internal/testFramework/internal/LogFinderHyperlinkTestBase.kt index 2ded6db07921..53d6f04111e8 100644 --- a/jvm/jvm-analysis-internal-testFramework/src/com/intellij/jvm/analysis/internal/testFramework/internal/LogFinderHyperlinkTestBase.kt +++ b/jvm/jvm-analysis-internal-testFramework/src/com/intellij/jvm/analysis/internal/testFramework/internal/LogFinderHyperlinkTestBase.kt @@ -1,6 +1,7 @@ package com.intellij.jvm.analysis.internal.testFramework.internal import com.intellij.analysis.customization.console.ClassLoggingConsoleFilterProvider +import com.intellij.analysis.customization.console.OnFlyMultipleFilesHyperlinkInfo import com.intellij.jvm.analysis.testFramework.LightJvmCodeInsightFixtureTestCase import com.intellij.openapi.editor.LogicalPosition import com.intellij.openapi.options.advanced.AdvancedSettings @@ -41,7 +42,14 @@ abstract class LogFinderHyperlinkTestBase : LightJvmCodeInsightFixtureTestCase() len += logLine.length val result = filter.applyFilter(logLine, len) if (logItem.position == null) { - Assert.assertNull(logItem.toString(), result) + if (result == null) { + continue + } + val hyperlinkInfo = result.firstHyperlinkInfo + if (hyperlinkInfo is OnFlyMultipleFilesHyperlinkInfo) { + val files = hyperlinkInfo.getFiles(project) + Assert.assertTrue(logItem.toString(), files.isEmpty()) + } continue } Assert.assertNotNull(logItem.toString(), result) diff --git a/jvm/jvm-analysis-java-tests/testSrc/com/intellij/customization/console/console/JavaLogFinderHyperlinkTest.kt b/jvm/jvm-analysis-java-tests/testSrc/com/intellij/customization/console/console/JavaLogFinderHyperlinkTest.kt index d91fdffdbdb3..9a45a9e702b6 100644 --- a/jvm/jvm-analysis-java-tests/testSrc/com/intellij/customization/console/console/JavaLogFinderHyperlinkTest.kt +++ b/jvm/jvm-analysis-java-tests/testSrc/com/intellij/customization/console/console/JavaLogFinderHyperlinkTest.kt @@ -211,6 +211,47 @@ public final class Slf4JFluent { ) } + fun testUnicode() { + LoggingTestUtils.addSlf4J(myFixture) + checkColumnFinderJava( + fileName = "Slf4JFluent", + classText = """ +package com.example.úsh.čas; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public final class UčenjaУченья { + private final static Logger log = LoggerFactory.getLogger(UčenjaУченья.class); + + public static void main(String[] args) { + System.out.println(2); + log1(1); + log2(2); + } + + private static void log1(int i) { + String msg = "test" + i; + log.atInfo() + .setMessage(msg) + .log(); + } + + private static void log2(int i) { + String msg = "log2" + i; + log.atInfo() + .log(msg); + } +} +""".trimIndent(), + logItems = listOf( + LogItem("java.exe", null), + LogItem("1", null), + LogItem("20:13:25.878 [main] INFO com.example.úsh.čas.UčenjaУченья - test1", LogicalPosition(16, 8)), + ) + ) + } + fun testSkipException() { LoggingTestUtils.addSlf4J(myFixture) checkColumnFinderJava( diff --git a/platform/lang-impl/src/com/intellij/execution/filters/impl/MultipleFilesHyperlinkInfo.java b/platform/lang-impl/src/com/intellij/execution/filters/impl/MultipleFilesHyperlinkInfo.java index 4f14ba7750ec..ef7d19868a71 100644 --- a/platform/lang-impl/src/com/intellij/execution/filters/impl/MultipleFilesHyperlinkInfo.java +++ b/platform/lang-impl/src/com/intellij/execution/filters/impl/MultipleFilesHyperlinkInfo.java @@ -15,43 +15,24 @@ */ package com.intellij.execution.filters.impl; -import com.intellij.codeInsight.navigation.PsiTargetNavigator; -import com.intellij.execution.ExecutionBundle; -import com.intellij.execution.filters.FileHyperlinkInfo; -import com.intellij.execution.filters.HyperlinkInfoBase; import com.intellij.execution.filters.HyperlinkInfoFactory; -import com.intellij.ide.DataManager; -import com.intellij.ide.util.gotoByName.GotoFileCellRenderer; -import com.intellij.openapi.actionSystem.CommonDataKeys; -import com.intellij.openapi.actionSystem.DataContext; -import com.intellij.openapi.editor.Document; -import com.intellij.openapi.editor.Editor; -import com.intellij.openapi.editor.ex.EditorEx; -import com.intellij.openapi.fileEditor.FileDocumentManager; -import com.intellij.openapi.fileEditor.FileEditorManager; import com.intellij.openapi.fileEditor.OpenFileDescriptor; import com.intellij.openapi.project.Project; import com.intellij.openapi.vfs.VirtualFile; -import com.intellij.openapi.wm.WindowManager; import com.intellij.psi.PsiElement; import com.intellij.psi.PsiFile; import com.intellij.psi.PsiManager; -import com.intellij.ui.awt.RelativePoint; import com.intellij.util.containers.ContainerUtil; import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; -import javax.swing.*; import java.util.ArrayList; import java.util.List; @ApiStatus.Internal -public final class MultipleFilesHyperlinkInfo extends HyperlinkInfoBase implements FileHyperlinkInfo { +public final class MultipleFilesHyperlinkInfo extends MultipleFilesHyperlinkInfoBase { private final List myVirtualFiles; - private final int myLineNumber; - private final Project myProject; - private final HyperlinkInfoFactory.@Nullable HyperlinkHandler myAction; MultipleFilesHyperlinkInfo(@NotNull List virtualFiles, int lineNumber, @NotNull Project project) { this(virtualFiles, lineNumber, project, null); @@ -61,38 +42,13 @@ public final class MultipleFilesHyperlinkInfo extends HyperlinkInfoBase implemen int lineNumber, @NotNull Project project, @Nullable HyperlinkInfoFactory.HyperlinkHandler action) { + super(lineNumber, project, action); myVirtualFiles = virtualFiles; - myLineNumber = lineNumber; - myProject = project; - myAction = action; } @Override - public void navigate(@NotNull final Project project, @Nullable RelativePoint hyperlinkLocationPoint) { - Editor originalEditor; - if (hyperlinkLocationPoint != null) { - DataManager dataManager = DataManager.getInstance(); - DataContext dataContext = dataManager.getDataContext(hyperlinkLocationPoint.getOriginalComponent()); - originalEditor = CommonDataKeys.EDITOR.getData(dataContext); - } else { - originalEditor = null; - } - - JFrame frame = WindowManager.getInstance().getFrame(project); - int width = frame != null ? frame.getSize().width : 200; - GotoFileCellRenderer renderer = new GotoFileCellRenderer(width); - - new PsiTargetNavigator<>(() -> getFiles(project)) - .title(ExecutionBundle.message("popup.title.choose.target.file")) - .presentationProvider(element -> renderer.computePresentation(element)) - .navigate(hyperlinkLocationPoint, ExecutionBundle.message("popup.title.choose.target.file"), project, file -> { - open(file.getVirtualFile(), originalEditor); - return true; - }); - } - @NotNull - private List getFiles(@NotNull Project project) { + public List getFiles(@NotNull Project project) { List currentFiles = new ArrayList<>(); for (VirtualFile file : myVirtualFiles) { if (!file.isValid()) continue; @@ -110,30 +66,6 @@ public final class MultipleFilesHyperlinkInfo extends HyperlinkInfoBase implemen return currentFiles; } - private void open(@NotNull VirtualFile file, Editor originalEditor) { - Document document = FileDocumentManager.getInstance().getDocument(file, myProject); - int offset = 0; - if (document != null && myLineNumber >= 0 && myLineNumber < document.getLineCount()) { - offset = document.getLineStartOffset(myLineNumber); - } - OpenFileDescriptor descriptor = new OpenFileDescriptor(myProject, file, offset); - Editor editor = FileEditorManager.getInstance(myProject).openTextEditor(descriptor, true); - if (myAction != null && editor != null) { - if (editor instanceof EditorEx) { - ((EditorEx)editor).setCaretEnabled(false); - try { - myAction.onLinkFollowed(myProject, file, editor, originalEditor); - } - finally { - ((EditorEx)editor).setCaretEnabled(true); - } - } - else { - myAction.onLinkFollowed(myProject, file, editor, originalEditor); - } - } - } - @Nullable @Override public OpenFileDescriptor getDescriptor() { diff --git a/platform/lang-impl/src/com/intellij/execution/filters/impl/MultipleFilesHyperlinkInfoBase.java b/platform/lang-impl/src/com/intellij/execution/filters/impl/MultipleFilesHyperlinkInfoBase.java new file mode 100644 index 000000000000..7fe634570eba --- /dev/null +++ b/platform/lang-impl/src/com/intellij/execution/filters/impl/MultipleFilesHyperlinkInfoBase.java @@ -0,0 +1,102 @@ +// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. +package com.intellij.execution.filters.impl; + +import com.intellij.codeInsight.navigation.PsiTargetNavigator; +import com.intellij.execution.ExecutionBundle; +import com.intellij.execution.filters.FileHyperlinkInfo; +import com.intellij.execution.filters.HyperlinkInfoBase; +import com.intellij.execution.filters.HyperlinkInfoFactory; +import com.intellij.ide.DataManager; +import com.intellij.ide.util.gotoByName.GotoFileCellRenderer; +import com.intellij.openapi.actionSystem.CommonDataKeys; +import com.intellij.openapi.actionSystem.DataContext; +import com.intellij.openapi.editor.Document; +import com.intellij.openapi.editor.Editor; +import com.intellij.openapi.editor.ex.EditorEx; +import com.intellij.openapi.fileEditor.FileDocumentManager; +import com.intellij.openapi.fileEditor.FileEditorManager; +import com.intellij.openapi.fileEditor.OpenFileDescriptor; +import com.intellij.openapi.project.Project; +import com.intellij.openapi.vfs.VirtualFile; +import com.intellij.openapi.wm.WindowManager; +import com.intellij.psi.PsiFile; +import com.intellij.ui.awt.RelativePoint; +import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import javax.swing.*; +import java.util.List; + +@ApiStatus.Internal +public abstract class MultipleFilesHyperlinkInfoBase extends HyperlinkInfoBase implements FileHyperlinkInfo { + protected final int myLineNumber; + protected final Project myProject; + private final HyperlinkInfoFactory.@Nullable HyperlinkHandler myAction; + + public MultipleFilesHyperlinkInfoBase(int lineNumber, + @NotNull Project project, + @Nullable HyperlinkInfoFactory.HyperlinkHandler action) { + myLineNumber = lineNumber; + myProject = project; + myAction = action; + } + + @Override + public void navigate(@NotNull final Project project, @Nullable RelativePoint hyperlinkLocationPoint) { + Editor originalEditor; + if (hyperlinkLocationPoint != null) { + DataManager dataManager = DataManager.getInstance(); + DataContext dataContext = dataManager.getDataContext(hyperlinkLocationPoint.getOriginalComponent()); + originalEditor = CommonDataKeys.EDITOR.getData(dataContext); + } + else { + originalEditor = null; + } + + JFrame frame = WindowManager.getInstance().getFrame(project); + int width = frame != null ? frame.getSize().width : 200; + GotoFileCellRenderer renderer = new GotoFileCellRenderer(width); + + boolean navigated = new PsiTargetNavigator<>(() -> getFiles(project)) + .title(ExecutionBundle.message("popup.title.choose.target.file")) + .presentationProvider(element -> renderer.computePresentation(element)) + .navigate(hyperlinkLocationPoint, ExecutionBundle.message("popup.title.choose.target.file"), project, file -> { + open(file.getVirtualFile(), originalEditor); + return true; + }); + if (!navigated) { + showNotFound(project, hyperlinkLocationPoint); + } + } + + protected void showNotFound(@NotNull final Project project, @Nullable RelativePoint hyperlinkLocationPoint) { + } + + @NotNull + public abstract List getFiles(@NotNull Project project); + + private void open(@NotNull VirtualFile file, Editor originalEditor) { + Document document = FileDocumentManager.getInstance().getDocument(file, myProject); + int offset = 0; + if (document != null && myLineNumber >= 0 && myLineNumber < document.getLineCount()) { + offset = document.getLineStartOffset(myLineNumber); + } + OpenFileDescriptor descriptor = new OpenFileDescriptor(myProject, file, offset); + Editor editor = FileEditorManager.getInstance(myProject).openTextEditor(descriptor, true); + if (myAction != null && editor != null) { + if (editor instanceof EditorEx) { + ((EditorEx)editor).setCaretEnabled(false); + try { + myAction.onLinkFollowed(myProject, file, editor, originalEditor); + } + finally { + ((EditorEx)editor).setCaretEnabled(true); + } + } + else { + myAction.onLinkFollowed(myProject, file, editor, originalEditor); + } + } + } +}