From 16354d00f0eb2c14cfe03dfeef5554a514565bc9 Mon Sep 17 00:00:00 2001 From: "Maxim.Kolmakov" Date: Fri, 21 Jun 2024 16:25:13 +0800 Subject: [PATCH] IJPL-156272 Add freeze analyzer and stack trace viewer to text file that contains stack trace GitOrigin-RevId: 24e9dff91ea035f5c0a50b8e64f4eaa81c2d0729 --- .idea/modules.xml | 1 + .../intellij/unscramble/UnscrambleDialog.java | 77 +------- .../unscramble/UnscrambleListener.java | 16 +- .../intellij/unscramble/UnscrambleUtils.java | 126 +++++++++++++ .../freezeAnalyzer/FreezeAnalyzerTest.kt | 2 +- platform/lang-impl/api-dump-unreviewed.txt | 1 + .../unscramble/AnalyzeStacktraceUtil.java | 29 ++- .../jetbrains/idea/devkit/DevKitIcons.java | 1 + .../resources/icons/expui/freeze.svg | 3 + .../resources/icons/expui/freeze_dark.svg | 10 + .../devkit-core/resources/icons/freeze.svg | 3 + .../resources/icons/freeze_dark.svg | 10 + .../intellij.devkit.plugin.noKotlin.iml | 1 + .../intellij.devkit.stacktrace.iml | 25 +++ .../resources/intellij.devkit.stacktrace.xml | 12 ++ .../DevKitStackTraceBundle.properties | 4 + .../stacktrace/DevKitStackTraceBundle.kt | 14 ++ .../editor/StackTraceEditorProvider.kt | 15 ++ .../editor/StackTraceEditorWithPreview.kt | 23 +++ .../stacktrace/editor/StackTraceFileEditor.kt | 173 ++++++++++++++++++ .../editor/StackTraceFileEditorProvider.kt | 25 +++ .../stacktrace/util/StackTracePluginScope.kt | 21 +++ .../intellij.devkit/plugin-content.yaml | 3 +- .../resources/META-INF/plugin.xml | 1 + 24 files changed, 495 insertions(+), 101 deletions(-) create mode 100644 java/unscramble/src/com/intellij/unscramble/UnscrambleUtils.java create mode 100644 plugins/devkit/devkit-core/resources/icons/expui/freeze.svg create mode 100644 plugins/devkit/devkit-core/resources/icons/expui/freeze_dark.svg create mode 100644 plugins/devkit/devkit-core/resources/icons/freeze.svg create mode 100644 plugins/devkit/devkit-core/resources/icons/freeze_dark.svg create mode 100644 plugins/devkit/intellij.devkit.stacktrace/intellij.devkit.stacktrace.iml create mode 100644 plugins/devkit/intellij.devkit.stacktrace/resources/intellij.devkit.stacktrace.xml create mode 100644 plugins/devkit/intellij.devkit.stacktrace/resources/messages/DevKitStackTraceBundle.properties create mode 100644 plugins/devkit/intellij.devkit.stacktrace/src/org/jetbrains/idea/devkit/stacktrace/DevKitStackTraceBundle.kt create mode 100644 plugins/devkit/intellij.devkit.stacktrace/src/org/jetbrains/idea/devkit/stacktrace/editor/StackTraceEditorProvider.kt create mode 100644 plugins/devkit/intellij.devkit.stacktrace/src/org/jetbrains/idea/devkit/stacktrace/editor/StackTraceEditorWithPreview.kt create mode 100644 plugins/devkit/intellij.devkit.stacktrace/src/org/jetbrains/idea/devkit/stacktrace/editor/StackTraceFileEditor.kt create mode 100644 plugins/devkit/intellij.devkit.stacktrace/src/org/jetbrains/idea/devkit/stacktrace/editor/StackTraceFileEditorProvider.kt create mode 100644 plugins/devkit/intellij.devkit.stacktrace/src/org/jetbrains/idea/devkit/stacktrace/util/StackTracePluginScope.kt diff --git a/.idea/modules.xml b/.idea/modules.xml index 8f87f521d954..30338d9466f6 100644 --- a/.idea/modules.xml +++ b/.idea/modules.xml @@ -332,6 +332,7 @@ + diff --git a/java/unscramble/src/com/intellij/unscramble/UnscrambleDialog.java b/java/unscramble/src/com/intellij/unscramble/UnscrambleDialog.java index ef2bb37e7e12..39b60740342e 100644 --- a/java/unscramble/src/com/intellij/unscramble/UnscrambleDialog.java +++ b/java/unscramble/src/com/intellij/unscramble/UnscrambleDialog.java @@ -46,7 +46,7 @@ public class UnscrambleDialog extends DialogWrapper { private static final @NonNls String PROPERTY_LOG_FILE_HISTORY_URLS = "UNSCRAMBLE_LOG_FILE_URL"; private static final @NonNls String PROPERTY_LOG_FILE_LAST_URL = "UNSCRAMBLE_LOG_FILE_LAST_URL"; private static final @NonNls String PROPERTY_UNSCRAMBLER_NAME_USED = "UNSCRAMBLER_NAME_USED"; - private static final Condition DEADLOCK_CONDITION = state -> state.isDeadlocked(); + private final Project myProject; private JPanel myEditorPanel; @@ -311,84 +311,11 @@ public class UnscrambleDialog extends DialogWrapper { String unscrambledTrace = unscrambleSupport == null ? textToUnscramble : unscrambleSupport.unscramble(project,textToUnscramble, logName, settings); if (unscrambledTrace == null) return null; List threadStates = ThreadDumpParser.parse(unscrambledTrace); - return addConsole(project, threadStates, unscrambledTrace); - } - - private static RunContentDescriptor addConsole(final Project project, final List threadDump, String unscrambledTrace) { - Icon icon = null; - String message = JavaBundle.message("unscramble.unscrambled.stacktrace.tab"); - if (!threadDump.isEmpty()) { - message = JavaBundle.message("unscramble.unscrambled.threaddump.tab"); - icon = AllIcons.Actions.Dump; - } - else { - String name = getExceptionName(unscrambledTrace); - if (name != null) { - message = name; - icon = AllIcons.Actions.Lightning; - } - } - if (ContainerUtil.find(threadDump, DEADLOCK_CONDITION) != null) { - message = JavaBundle.message("unscramble.unscrambled.deadlock.tab"); - icon = AllIcons.Debugger.KillProcess; - } - return AnalyzeStacktraceUtil.addConsole(project, threadDump.size() > 1 ? new ThreadDumpConsoleFactory(project, threadDump) : null, message, unscrambledTrace, icon); + return UnscrambleUtils.addConsole(project, threadStates, unscrambledTrace); } @Override protected String getDimensionServiceKey(){ return "#com.intellij.unscramble.UnscrambleDialog"; } - - private static @Nullable String getExceptionName(String unscrambledTrace) { - @SuppressWarnings("IOResourceOpenedButNotSafelyClosed") - BufferedReader reader = new BufferedReader(new StringReader(unscrambledTrace)); - for (int i = 0; i < 3; i++) { - try { - String line = reader.readLine(); - if (line == null) return null; - String name = getExceptionAbbreviation(line); - if (name != null) return name; - } - catch (IOException e) { - return null; - } - } - return null; - } - - private static @Nullable String getExceptionAbbreviation(String line) { - line = StringUtil.trimStart(line.trim(), "Caused by: "); - int classNameStart = 0; - int classNameEnd = line.length(); - for (int j = 0; j < line.length(); j++) { - char c = line.charAt(j); - if (c == '.' || c == '$') { - classNameStart = j + 1; - continue; - } - if (c == ':') { - classNameEnd = j; - break; - } - if (!StringUtil.isJavaIdentifierPart(c)) { - return null; - } - } - if (classNameStart >= classNameEnd) return null; - String clazz = line.substring(classNameStart, classNameEnd); - String abbreviate = abbreviate(clazz); - return abbreviate.length() > 1 ? abbreviate : clazz; - } - - private static String abbreviate(String s) { - StringBuilder builder = new StringBuilder(); - for (int i = 0; i < s.length(); i++) { - char c = s.charAt(i); - if (Character.isUpperCase(c)) { - builder.append(c); - } - } - return builder.toString(); - } } diff --git a/java/unscramble/src/com/intellij/unscramble/UnscrambleListener.java b/java/unscramble/src/com/intellij/unscramble/UnscrambleListener.java index d84253a4ecaa..22da88bbad96 100644 --- a/java/unscramble/src/com/intellij/unscramble/UnscrambleListener.java +++ b/java/unscramble/src/com/intellij/unscramble/UnscrambleListener.java @@ -36,21 +36,7 @@ class UnscrambleListener extends ClipboardAnalyzeListener { @Override public boolean canHandle(@NotNull String value) { - value = ThreadDumpParser.normalizeText(value); - int linesCount = 0; - for (String line : value.split("\n")) { - line = line.trim(); - if (line.length() == 0) continue; - line = StringUtil.trimEnd(line, "\r"); - if (STACKTRACE_LINE.matcher(line).matches()) { - linesCount++; - } - else { - linesCount = 0; - } - if (linesCount > 2) return true; - } - return false; + return UnscrambleUtils.isStackTrace(value); } @Override diff --git a/java/unscramble/src/com/intellij/unscramble/UnscrambleUtils.java b/java/unscramble/src/com/intellij/unscramble/UnscrambleUtils.java new file mode 100644 index 000000000000..b0a51b9f74c1 --- /dev/null +++ b/java/unscramble/src/com/intellij/unscramble/UnscrambleUtils.java @@ -0,0 +1,126 @@ +// 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.unscramble; + +import com.intellij.execution.ui.RunContentDescriptor; +import com.intellij.icons.AllIcons; +import com.intellij.java.JavaBundle; +import com.intellij.openapi.project.Project; +import com.intellij.openapi.util.Condition; +import com.intellij.openapi.util.text.StringUtil; +import com.intellij.threadDumpParser.ThreadDumpParser; +import com.intellij.threadDumpParser.ThreadState; +import com.intellij.util.containers.ContainerUtil; +import org.jetbrains.annotations.Nullable; + +import javax.swing.*; +import java.io.BufferedReader; +import java.io.IOException; +import java.io.StringReader; +import java.util.List; +import java.util.regex.Pattern; + +final public class UnscrambleUtils { + private static final Condition DEADLOCK_CONDITION = state -> state.isDeadlocked(); + private static final Pattern STACKTRACE_LINE = + Pattern.compile( + "[\t]*at [[_a-zA-Z0-9/]+\\.]+[_a-zA-Z$0-9/]+\\.[a-zA-Z0-9_/]+\\([A-Za-z0-9_/]+\\.(java|kt):[\\d]+\\)+[ [~]*\\[[a-zA-Z0-9\\.\\:/]\\]]*"); + + public static @Nullable String getExceptionName(String unscrambledTrace) { + @SuppressWarnings("IOResourceOpenedButNotSafelyClosed") + BufferedReader reader = new BufferedReader(new StringReader(unscrambledTrace)); + for (int i = 0; i < 3; i++) { + try { + String line = reader.readLine(); + if (line == null) return null; + String name = getExceptionAbbreviation(line); + if (name != null) return name; + } + catch (IOException e) { + return null; + } + } + return null; + } + + private static @Nullable String getExceptionAbbreviation(String line) { + line = StringUtil.trimStart(line.trim(), "Caused by: "); + int classNameStart = 0; + int classNameEnd = line.length(); + for (int j = 0; j < line.length(); j++) { + char c = line.charAt(j); + if (c == '.' || c == '$') { + classNameStart = j + 1; + continue; + } + if (c == ':') { + classNameEnd = j; + break; + } + if (!StringUtil.isJavaIdentifierPart(c)) { + return null; + } + } + if (classNameStart >= classNameEnd) return null; + String clazz = line.substring(classNameStart, classNameEnd); + String abbreviate = abbreviate(clazz); + return abbreviate.length() > 1 ? abbreviate : clazz; + } + + private static String abbreviate(String s) { + StringBuilder builder = new StringBuilder(); + for (int i = 0; i < s.length(); i++) { + char c = s.charAt(i); + if (Character.isUpperCase(c)) { + builder.append(c); + } + } + return builder.toString(); + } + + public static RunContentDescriptor addConsole(final Project project, + final List threadDump, + String unscrambledTrace, + Boolean withExecutor) { + Icon icon = null; + String message = JavaBundle.message("unscramble.unscrambled.stacktrace.tab"); + if (!threadDump.isEmpty()) { + message = JavaBundle.message("unscramble.unscrambled.threaddump.tab"); + icon = AllIcons.Actions.Dump; + } + else { + String name = getExceptionName(unscrambledTrace); + if (name != null) { + message = name; + icon = AllIcons.Actions.Lightning; + } + } + if (ContainerUtil.find(threadDump, DEADLOCK_CONDITION) != null) { + message = JavaBundle.message("unscramble.unscrambled.deadlock.tab"); + icon = AllIcons.Debugger.KillProcess; + } + return AnalyzeStacktraceUtil.addConsole(project, threadDump.size() > 1 ? new ThreadDumpConsoleFactory(project, threadDump) : null, + message, unscrambledTrace, icon, withExecutor); + } + + public static RunContentDescriptor addConsole(final Project project, final List threadDump, String unscrambledTrace) { + return addConsole(project, threadDump, unscrambledTrace, true); + } + + public static boolean isStackTrace(String text) { + text = ThreadDumpParser.normalizeText(text); + int linesCount = 0; + for (String line : text.split("\n")) { + line = line.trim(); + if (line.length() == 0) continue; + line = StringUtil.trimEnd(line, "\r"); + if (STACKTRACE_LINE.matcher(line).matches()) { + linesCount++; + } + else { + linesCount = 0; + } + if (linesCount > 2) return true; + } + return false; + } +} diff --git a/platform/diagnostic/freezeAnalyzer/testSrc/com/intellij/platform/diagnostic/freezeAnalyzer/FreezeAnalyzerTest.kt b/platform/diagnostic/freezeAnalyzer/testSrc/com/intellij/platform/diagnostic/freezeAnalyzer/FreezeAnalyzerTest.kt index c15cc270e5fa..104cc21be284 100644 --- a/platform/diagnostic/freezeAnalyzer/testSrc/com/intellij/platform/diagnostic/freezeAnalyzer/FreezeAnalyzerTest.kt +++ b/platform/diagnostic/freezeAnalyzer/testSrc/com/intellij/platform/diagnostic/freezeAnalyzer/FreezeAnalyzerTest.kt @@ -82,7 +82,7 @@ class FreezeAnalyzerTest { fun testGeneralLockFreeze() { val threadDump = File(this::class.java.classLoader.getResource("freezes/generalLock/generalLock.txt")!!.path).toPath().readText() FreezeAnalyzer.analyzeFreeze(threadDump)?.message.shouldBe("EDT is blocked on com.intellij.codeInsight.completion.CompletionProgressIndicator.blockingWaitForFinish") - FreezeAnalyzer.analyzeFreeze(threadDump)?.threads?.joinToString { it -> it.stackTrace }.shouldStartWith("Possibly locked by com.intellij.codeInsight.completion.JavaMethodCallElement. in DefaultDispatcher-worker-55") + FreezeAnalyzer.analyzeFreeze(threadDump)?.additionalMessage?.shouldBe("Possibly locked by com.intellij.codeInsight.completion.JavaMethodCallElement. in DefaultDispatcher-worker-55") } @Test diff --git a/platform/lang-impl/api-dump-unreviewed.txt b/platform/lang-impl/api-dump-unreviewed.txt index 066075a7bada..cb2ee631ccab 100644 --- a/platform/lang-impl/api-dump-unreviewed.txt +++ b/platform/lang-impl/api-dump-unreviewed.txt @@ -18925,6 +18925,7 @@ f:com.intellij.unscramble.AnalyzeStacktraceUtil - sf:EP_NAME:com.intellij.openapi.extensions.ProjectExtensionPointName - s:addConsole(com.intellij.openapi.project.Project,com.intellij.unscramble.AnalyzeStacktraceUtil$ConsoleFactory,java.lang.String,java.lang.String):V - s:addConsole(com.intellij.openapi.project.Project,com.intellij.unscramble.AnalyzeStacktraceUtil$ConsoleFactory,java.lang.String,java.lang.String,javax.swing.Icon):com.intellij.execution.ui.RunContentDescriptor +- s:addConsole(com.intellij.openapi.project.Project,com.intellij.unscramble.AnalyzeStacktraceUtil$ConsoleFactory,java.lang.String,java.lang.String,javax.swing.Icon,java.lang.Boolean):com.intellij.execution.ui.RunContentDescriptor - s:createEditorPanel(com.intellij.openapi.project.Project,com.intellij.openapi.Disposable):com.intellij.unscramble.AnalyzeStacktraceUtil$StacktraceEditorPanel - s:printStacktrace(com.intellij.execution.ui.ConsoleView,java.lang.String):V com.intellij.unscramble.AnalyzeStacktraceUtil$ConsoleFactory diff --git a/platform/lang-impl/src/com/intellij/unscramble/AnalyzeStacktraceUtil.java b/platform/lang-impl/src/com/intellij/unscramble/AnalyzeStacktraceUtil.java index dcefa1ddafc1..ae8790507e7f 100644 --- a/platform/lang-impl/src/com/intellij/unscramble/AnalyzeStacktraceUtil.java +++ b/platform/lang-impl/src/com/intellij/unscramble/AnalyzeStacktraceUtil.java @@ -59,7 +59,8 @@ public final class AnalyzeStacktraceUtil { @Nullable ConsoleFactory consoleFactory, final @NlsContexts.TabTitle String tabTitle, String text, - @Nullable Icon icon) { + @Nullable Icon icon, + Boolean withExecutor) { final TextConsoleBuilder builder = TextConsoleBuilderFactory.getInstance().createBuilder(project); builder.filters(EP_NAME.getExtensions(project)); final ConsoleView consoleView = builder.getConsole(); @@ -70,14 +71,13 @@ public final class AnalyzeStacktraceUtil { : new MyConsolePanel(consoleView, toolbarActions); final RunContentDescriptor descriptor = new RunContentDescriptor(consoleView, null, consoleComponent, tabTitle, icon) { - @Override - public boolean isContentReuseProhibited() { - return true; - } - }; + @Override + public boolean isContentReuseProhibited() { + return true; + } + }; - final Executor executor = DefaultRunExecutor.getRunExecutorInstance(); - for (AnAction action: consoleView.createConsoleActions()) { + for (AnAction action : consoleView.createConsoleActions()) { toolbarActions.add(action); } final ConsoleViewImpl console = (ConsoleViewImpl)consoleView; @@ -85,7 +85,10 @@ public final class AnalyzeStacktraceUtil { console.getEditor().getSettings().setCaretRowShown(true); toolbarActions.add(ActionManager.getInstance().getAction("AnalyzeStacktraceToolbar")); - RunContentManager.getInstance(project).showRunContent(executor, descriptor); + if (withExecutor) { + final Executor executor = DefaultRunExecutor.getRunExecutorInstance(); + RunContentManager.getInstance(project).showRunContent(executor, descriptor); + } consoleView.allowHeavyFilters(); if (consoleFactory == null) { printStacktrace(consoleView, text); @@ -93,6 +96,14 @@ public final class AnalyzeStacktraceUtil { return descriptor; } + public static RunContentDescriptor addConsole(Project project, + @Nullable ConsoleFactory consoleFactory, + final @NlsContexts.TabTitle String tabTitle, + String text, + @Nullable Icon icon) { + return addConsole(project, consoleFactory, tabTitle, text, icon, true); + } + private static final class MyConsolePanel extends JPanel { MyConsolePanel(ExecutionConsole consoleView, ActionGroup toolbarActions) { super(new BorderLayout()); diff --git a/plugins/devkit/devkit-core/gen/org/jetbrains/idea/devkit/DevKitIcons.java b/plugins/devkit/devkit-core/gen/org/jetbrains/idea/devkit/DevKitIcons.java index c4c06d0040c8..988b8947f3b1 100644 --- a/plugins/devkit/devkit-core/gen/org/jetbrains/idea/devkit/DevKitIcons.java +++ b/plugins/devkit/devkit-core/gen/org/jetbrains/idea/devkit/DevKitIcons.java @@ -18,6 +18,7 @@ public final class DevKitIcons { return IconManager.getInstance().loadRasterizedIcon(path, expUIPath, DevKitIcons.class.getClassLoader(), cacheKey, flags); } /** 16x16 */ public static final @NotNull Icon Add_sdk = load("icons/expui/addSDK.svg", "icons/add_sdk.svg", 641117830, 2); + /** 16x16 */ public static final @NotNull Icon Freeze = load("icons/expui/freeze.svg", "icons/freeze.svg", 754396711, 2); public static final class Gutter { /** 12x12 */ public static final @NotNull Icon DescriptionFile = load("icons/expui/gutter/descriptionFile@14x14.svg", "icons/gutter/descriptionFile.svg", 1318760137, 2); diff --git a/plugins/devkit/devkit-core/resources/icons/expui/freeze.svg b/plugins/devkit/devkit-core/resources/icons/expui/freeze.svg new file mode 100644 index 000000000000..ecb650ee10e3 --- /dev/null +++ b/plugins/devkit/devkit-core/resources/icons/expui/freeze.svg @@ -0,0 +1,3 @@ + + + diff --git a/plugins/devkit/devkit-core/resources/icons/expui/freeze_dark.svg b/plugins/devkit/devkit-core/resources/icons/expui/freeze_dark.svg new file mode 100644 index 000000000000..5368b88af88d --- /dev/null +++ b/plugins/devkit/devkit-core/resources/icons/expui/freeze_dark.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/plugins/devkit/devkit-core/resources/icons/freeze.svg b/plugins/devkit/devkit-core/resources/icons/freeze.svg new file mode 100644 index 000000000000..bcac1ece6b48 --- /dev/null +++ b/plugins/devkit/devkit-core/resources/icons/freeze.svg @@ -0,0 +1,3 @@ + + + diff --git a/plugins/devkit/devkit-core/resources/icons/freeze_dark.svg b/plugins/devkit/devkit-core/resources/icons/freeze_dark.svg new file mode 100644 index 000000000000..b808fbf16a2e --- /dev/null +++ b/plugins/devkit/devkit-core/resources/icons/freeze_dark.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/plugins/devkit/intellij.devkit.plugin/noKotlin/intellij.devkit.plugin.noKotlin.iml b/plugins/devkit/intellij.devkit.plugin/noKotlin/intellij.devkit.plugin.noKotlin.iml index 90d24673ed5d..9ca0c3113f70 100644 --- a/plugins/devkit/intellij.devkit.plugin/noKotlin/intellij.devkit.plugin.noKotlin.iml +++ b/plugins/devkit/intellij.devkit.plugin/noKotlin/intellij.devkit.plugin.noKotlin.iml @@ -17,5 +17,6 @@ + \ No newline at end of file diff --git a/plugins/devkit/intellij.devkit.stacktrace/intellij.devkit.stacktrace.iml b/plugins/devkit/intellij.devkit.stacktrace/intellij.devkit.stacktrace.iml new file mode 100644 index 000000000000..0189e92bce2f --- /dev/null +++ b/plugins/devkit/intellij.devkit.stacktrace/intellij.devkit.stacktrace.iml @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/plugins/devkit/intellij.devkit.stacktrace/resources/intellij.devkit.stacktrace.xml b/plugins/devkit/intellij.devkit.stacktrace/resources/intellij.devkit.stacktrace.xml new file mode 100644 index 000000000000..729bd6223fa7 --- /dev/null +++ b/plugins/devkit/intellij.devkit.stacktrace/resources/intellij.devkit.stacktrace.xml @@ -0,0 +1,12 @@ + + + + + + + + + + + diff --git a/plugins/devkit/intellij.devkit.stacktrace/resources/messages/DevKitStackTraceBundle.properties b/plugins/devkit/intellij.devkit.stacktrace/resources/messages/DevKitStackTraceBundle.properties new file mode 100644 index 000000000000..bbe599fc556a --- /dev/null +++ b/plugins/devkit/intellij.devkit.stacktrace/resources/messages/DevKitStackTraceBundle.properties @@ -0,0 +1,4 @@ +progress.title.freeze.analysis=Analyzing freeze\u2026 +progress.title.parsing.thread.dump=Parsing thread dump\u2026 +stack.trace=Stack Trace +tab.title.freeze.analyzer=Freeze Analyzer diff --git a/plugins/devkit/intellij.devkit.stacktrace/src/org/jetbrains/idea/devkit/stacktrace/DevKitStackTraceBundle.kt b/plugins/devkit/intellij.devkit.stacktrace/src/org/jetbrains/idea/devkit/stacktrace/DevKitStackTraceBundle.kt new file mode 100644 index 000000000000..cf8d07ed3963 --- /dev/null +++ b/plugins/devkit/intellij.devkit.stacktrace/src/org/jetbrains/idea/devkit/stacktrace/DevKitStackTraceBundle.kt @@ -0,0 +1,14 @@ +// Copyright 2000-2022 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. +package org.jetbrains.idea.devkit.stacktrace + +import com.intellij.DynamicBundle +import org.jetbrains.annotations.Nls +import org.jetbrains.annotations.NonNls +import org.jetbrains.annotations.PropertyKey + +object DevKitStackTraceBundle { + private const val BUNDLE_FQN: @NonNls String = "messages.DevKitStackTraceBundle" + private val BUNDLE = DynamicBundle(DevKitStackTraceBundle::class.java, BUNDLE_FQN) + + fun message(key: @PropertyKey(resourceBundle = BUNDLE_FQN) String, vararg params: Any): @Nls String = BUNDLE.getMessage(key, *params) +} diff --git a/plugins/devkit/intellij.devkit.stacktrace/src/org/jetbrains/idea/devkit/stacktrace/editor/StackTraceEditorProvider.kt b/plugins/devkit/intellij.devkit.stacktrace/src/org/jetbrains/idea/devkit/stacktrace/editor/StackTraceEditorProvider.kt new file mode 100644 index 000000000000..63cc568abaea --- /dev/null +++ b/plugins/devkit/intellij.devkit.stacktrace/src/org/jetbrains/idea/devkit/stacktrace/editor/StackTraceEditorProvider.kt @@ -0,0 +1,15 @@ +// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file. +package org.jetbrains.idea.devkit.stacktrace.editor + +import com.intellij.openapi.fileEditor.FileEditor +import com.intellij.openapi.fileEditor.TextEditor +import com.intellij.openapi.fileEditor.TextEditorWithPreviewProvider + +internal class StackTraceEditorProvider : TextEditorWithPreviewProvider(StackTraceFileEditorProvider()) { + + override fun createSplitEditor(firstEditor: TextEditor, secondEditor: FileEditor): FileEditor { + require(secondEditor is StackTraceFileEditor) { "Secondary editor should be StackTraceFileEditor" } + return StackTraceEditorWithPreview(firstEditor, secondEditor) + } + +} \ No newline at end of file diff --git a/plugins/devkit/intellij.devkit.stacktrace/src/org/jetbrains/idea/devkit/stacktrace/editor/StackTraceEditorWithPreview.kt b/plugins/devkit/intellij.devkit.stacktrace/src/org/jetbrains/idea/devkit/stacktrace/editor/StackTraceEditorWithPreview.kt new file mode 100644 index 000000000000..0783a1049523 --- /dev/null +++ b/plugins/devkit/intellij.devkit.stacktrace/src/org/jetbrains/idea/devkit/stacktrace/editor/StackTraceEditorWithPreview.kt @@ -0,0 +1,23 @@ +// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file. +package org.jetbrains.idea.devkit.stacktrace.editor + +import com.intellij.openapi.fileEditor.TextEditor +import com.intellij.openapi.fileEditor.TextEditorWithPreview +import org.jetbrains.idea.devkit.stacktrace.DevKitStackTraceBundle + +/** + * Text and stacktrace preview editor. + */ +internal class StackTraceEditorWithPreview(editor: TextEditor, preview: StackTraceFileEditor) + : TextEditorWithPreview(editor, preview, DevKitStackTraceBundle.message("stack.trace"), Layout.SHOW_EDITOR_AND_PREVIEW, false) { + + init { + preview.setMainEditor(editor.getEditor()) + } + + override fun onLayoutChange(oldValue: Layout?, newValue: Layout?) { + if (newValue == Layout.SHOW_PREVIEW) { + myPreview.preferredFocusedComponent?.requestFocus() ?: myPreview.component.requestFocus() + } + } +} \ No newline at end of file diff --git a/plugins/devkit/intellij.devkit.stacktrace/src/org/jetbrains/idea/devkit/stacktrace/editor/StackTraceFileEditor.kt b/plugins/devkit/intellij.devkit.stacktrace/src/org/jetbrains/idea/devkit/stacktrace/editor/StackTraceFileEditor.kt new file mode 100644 index 000000000000..670e67d4d1e2 --- /dev/null +++ b/plugins/devkit/intellij.devkit.stacktrace/src/org/jetbrains/idea/devkit/stacktrace/editor/StackTraceFileEditor.kt @@ -0,0 +1,173 @@ +package org.jetbrains.idea.devkit.stacktrace.editor + +import com.intellij.execution.ui.RunContentDescriptor +import com.intellij.openapi.application.EDT +import com.intellij.openapi.editor.Editor +import com.intellij.openapi.editor.event.DocumentEvent +import com.intellij.openapi.editor.event.DocumentListener +import com.intellij.openapi.fileEditor.FileDocumentManager +import com.intellij.openapi.fileEditor.FileEditor +import com.intellij.openapi.fileEditor.FileEditorState +import com.intellij.openapi.options.advanced.AdvancedSettings +import com.intellij.openapi.project.Project +import com.intellij.openapi.util.Disposer +import com.intellij.openapi.util.UserDataHolderBase +import com.intellij.openapi.vfs.VirtualFile +import com.intellij.openapi.wm.ToolWindow +import com.intellij.platform.diagnostic.freezeAnalyzer.FreezeAnalyzer +import com.intellij.platform.ide.progress.withBackgroundProgress +import com.intellij.threadDumpParser.ThreadDumpParser.parse +import com.intellij.ui.content.Content +import com.intellij.ui.content.ContentFactory.getInstance +import com.intellij.ui.content.ContentManager +import com.intellij.ui.content.TabbedPaneContentUI +import com.intellij.unscramble.AnalyzeStacktraceUtil +import com.intellij.unscramble.UnscrambleUtils.addConsole +import com.intellij.util.concurrency.annotations.RequiresEdt +import kotlinx.coroutines.* +import kotlinx.coroutines.channels.BufferOverflow +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.flow.debounce +import org.jetbrains.idea.devkit.DevKitIcons +import org.jetbrains.idea.devkit.stacktrace.DevKitStackTraceBundle +import org.jetbrains.idea.devkit.stacktrace.util.StackTracePluginScope +import java.awt.BorderLayout +import java.beans.PropertyChangeListener +import javax.swing.JComponent +import javax.swing.JPanel +import javax.swing.SwingConstants.TOP +import kotlin.time.Duration.Companion.milliseconds + +class StackTraceFileEditor(private val project: Project, private val file: VirtualFile) : FileEditor, UserDataHolderBase() { + private val document = FileDocumentManager.getInstance().getDocument(file) ?: error("Document not found for file: $file") + private val mainPanelWrapper = JPanel(BorderLayout()) + private val mainEditor = MutableStateFlow(null) + private val coroutineScope = StackTracePluginScope.createChildScope(project) + private var myContentManager: ContentManager? = null + + init { + document.addDocumentListener(ReparseContentDocumentListener(), this) + coroutineScope.launch(Dispatchers.EDT) { + myContentManager = getInstance().createContentManager(TabbedPaneContentUI(TOP), false, project) + updateStacktracePane() + } + } + + fun setMainEditor(editor: Editor) { + check(mainEditor.value == null) { "Main editor already set" } + mainEditor.value = editor + } + + override fun getComponent(): JComponent = mainPanelWrapper + + override fun getPreferredFocusedComponent(): JComponent? = myContentManager?.component ?: mainPanelWrapper + + override fun getName(): String = DevKitStackTraceBundle.message("stack.trace") + + override fun setState(state: FileEditorState) {} + + override fun isModified(): Boolean = false + + override fun isValid(): Boolean = true + + override fun addPropertyChangeListener(listener: PropertyChangeListener) {} + + override fun removePropertyChangeListener(listener: PropertyChangeListener) {} + + override fun getFile(): VirtualFile = file + + override fun dispose() { + myContentManager?.removeAllContents(true) + myContentManager?.let { + Disposer.dispose(it) + } + myContentManager = null + coroutineScope.cancel() + } + + @RequiresEdt + private suspend fun updateStacktracePane() { + if (!file.isValid) return + val contentManager = myContentManager ?: return + myContentManager?.removeAllContents(true) + + addThreadContent(contentManager) ?: return + addFreezeAnalysisContent(contentManager) + + mainPanelWrapper.add(contentManager.component, BorderLayout.CENTER) + if (mainPanelWrapper.isShowing) mainPanelWrapper.validate() + mainPanelWrapper.repaint() + } + + private suspend fun addThreadContent(contentManager: ContentManager): Unit? = withContext(Dispatchers.Default) { + withBackgroundProgress(project, DevKitStackTraceBundle.message("progress.title.parsing.thread.dump")) { + val threadStates = parse(document.text) + withContext(Dispatchers.EDT) { + addConsole(project, threadStates, document.text, false) + } + } + }?.let { descriptor -> + contentManager.addContent(createNewContent(descriptor).apply { + executionId = descriptor.executionId + component = descriptor.component + setPreferredFocusedComponent(descriptor.preferredFocusComputable) + putUserData(RunContentDescriptor.DESCRIPTOR_KEY, descriptor) + displayName = descriptor.displayName + descriptor.setAttachedContent(this) + }) + } + + private suspend fun addFreezeAnalysisContent(contentManager: ContentManager) { + withContext(Dispatchers.Default) { + withBackgroundProgress(project, DevKitStackTraceBundle.message("progress.title.freeze.analysis")) { + FreezeAnalyzer.analyzeFreeze(document.text) + } + }?.let { result -> + val freezeDescriptor = AnalyzeStacktraceUtil.addConsole( + project, null, + DevKitStackTraceBundle.message("tab.title.freeze.analyzer"), + "${result.message}\n${result.additionalMessage ?: ""}\n======= Stack Trace: ========= \n${result.threads.joinToString { it -> it.stackTrace }}", + DevKitIcons.Freeze, false + ) + contentManager.addContent(createNewContent(freezeDescriptor).apply { + executionId = freezeDescriptor.executionId + component = freezeDescriptor.component + setPreferredFocusedComponent(freezeDescriptor.preferredFocusComputable) + putUserData(RunContentDescriptor.DESCRIPTOR_KEY, freezeDescriptor) + displayName = freezeDescriptor.displayName + }) + } + } + + private fun createNewContent(descriptor: RunContentDescriptor): Content { + val content = getInstance().createContent( + descriptor.component, descriptor.displayName, true + ).apply { + putUserData(ToolWindow.SHOW_CONTENT_ICON, true) + isPinned = AdvancedSettings.getBoolean("start.run.configurations.pinned") + icon = descriptor.icon + } + return content + } + + private inner class ReparseContentDocumentListener : DocumentListener { + @OptIn(FlowPreview::class) + private val documentChangedRequests = MutableSharedFlow( + replay = 1, + onBufferOverflow = BufferOverflow.DROP_OLDEST + ).apply { + coroutineScope.launch(Dispatchers.EDT) { + debounce(500.milliseconds) + .collectLatest { + updateStacktracePane() + } + } + } + + override fun documentChanged(event: DocumentEvent) { + documentChangedRequests.tryEmit(Unit) + } + } +} \ No newline at end of file diff --git a/plugins/devkit/intellij.devkit.stacktrace/src/org/jetbrains/idea/devkit/stacktrace/editor/StackTraceFileEditorProvider.kt b/plugins/devkit/intellij.devkit.stacktrace/src/org/jetbrains/idea/devkit/stacktrace/editor/StackTraceFileEditorProvider.kt new file mode 100644 index 000000000000..3f835e5781d3 --- /dev/null +++ b/plugins/devkit/intellij.devkit.stacktrace/src/org/jetbrains/idea/devkit/stacktrace/editor/StackTraceFileEditorProvider.kt @@ -0,0 +1,25 @@ +// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file. +package org.jetbrains.idea.devkit.stacktrace.editor + +import com.intellij.openapi.fileEditor.FileEditor +import com.intellij.openapi.fileEditor.FileEditorPolicy +import com.intellij.openapi.fileEditor.WeighedFileEditorProvider +import com.intellij.openapi.project.Project +import com.intellij.openapi.vfs.VfsUtilCore.loadText +import com.intellij.openapi.vfs.VirtualFile +import com.intellij.unscramble.UnscrambleUtils + +/** + * Java stacktrace weighed file editor provider. + */ +class StackTraceFileEditorProvider : WeighedFileEditorProvider() { + override fun accept(project: Project, file: VirtualFile): Boolean { + return file.extension == "txt" && UnscrambleUtils.isStackTrace(loadText(file)) + } + + override fun createEditor(project: Project, file: VirtualFile): FileEditor = StackTraceFileEditor(project, file) + + override fun getEditorTypeId(): String = "stacktrace-preview-editor" + + override fun getPolicy(): FileEditorPolicy = FileEditorPolicy.PLACE_AFTER_DEFAULT_EDITOR +} diff --git a/plugins/devkit/intellij.devkit.stacktrace/src/org/jetbrains/idea/devkit/stacktrace/util/StackTracePluginScope.kt b/plugins/devkit/intellij.devkit.stacktrace/src/org/jetbrains/idea/devkit/stacktrace/util/StackTracePluginScope.kt new file mode 100644 index 000000000000..dd9dfda20df8 --- /dev/null +++ b/plugins/devkit/intellij.devkit.stacktrace/src/org/jetbrains/idea/devkit/stacktrace/util/StackTracePluginScope.kt @@ -0,0 +1,21 @@ +// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file. +package org.jetbrains.idea.devkit.stacktrace.util + +import com.intellij.openapi.components.Service +import com.intellij.openapi.components.service +import com.intellij.openapi.project.Project +import com.intellij.platform.util.coroutines.childScope +import kotlinx.coroutines.CoroutineScope + +@Service(Service.Level.PROJECT) +internal class StackTracePluginScope(private val coroutineScope: CoroutineScope) { + companion object { + fun createChildScope(project: Project): CoroutineScope { + return scope(project).childScope("DevKitStackTracePlugin") + } + + fun scope(project: Project): CoroutineScope { + return project.service().coroutineScope + } + } +} diff --git a/plugins/devkit/intellij.devkit/plugin-content.yaml b/plugins/devkit/intellij.devkit/plugin-content.yaml index 97cdefbc801e..586a0380cac0 100644 --- a/plugins/devkit/intellij.devkit/plugin-content.yaml +++ b/plugins/devkit/intellij.devkit/plugin-content.yaml @@ -23,4 +23,5 @@ - name: intellij.devkit.uiDesigner - name: intellij.devkit.workspaceModel - name: intellij.kotlin.devkit - - name: intellij.devkit.debugger \ No newline at end of file + - name: intellij.devkit.debugger + - name: intellij.devkit.stacktrace \ No newline at end of file diff --git a/plugins/devkit/intellij.devkit/resources/META-INF/plugin.xml b/plugins/devkit/intellij.devkit/resources/META-INF/plugin.xml index 6fb246cd5df6..825897ff10e6 100644 --- a/plugins/devkit/intellij.devkit/resources/META-INF/plugin.xml +++ b/plugins/devkit/intellij.devkit/resources/META-INF/plugin.xml @@ -28,6 +28,7 @@ +