mirror of
https://gitflic.ru/project/openide/openide.git
synced 2026-01-05 01:50:56 +07:00
[debugger] intention to control exception breakpoints in editor, IJPL-161898
GitOrigin-RevId: 74a8cd67b65e433aae9012672dd9f8e92f2fb839
This commit is contained in:
committed by
intellij-monorepo-bot
parent
2ee1d6591c
commit
4c63e241d2
@@ -0,0 +1,85 @@
|
||||
// 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.debugger.codeinsight
|
||||
|
||||
import com.intellij.debugger.DebuggerManagerEx
|
||||
import com.intellij.debugger.ui.breakpoints.JavaExceptionBreakpointType
|
||||
import com.intellij.openapi.project.Project
|
||||
import com.intellij.psi.*
|
||||
import com.intellij.psi.util.InheritanceUtil
|
||||
import com.intellij.psi.util.parents
|
||||
import com.intellij.xdebugger.XDebuggerManager
|
||||
import com.intellij.xdebugger.breakpoints.XBreakpoint
|
||||
import com.intellij.xdebugger.codeinsight.ControlExceptionBreakpointSupport
|
||||
import com.intellij.xdebugger.codeinsight.ControlExceptionBreakpointSupport.ExceptionReference
|
||||
import org.jetbrains.uast.*
|
||||
|
||||
open class ControlExceptionBreakpointJVMSupport : ControlExceptionBreakpointSupport {
|
||||
|
||||
protected fun findClassReference(psiElement: PsiElement): PsiClass? {
|
||||
// Note that we use "PSI parents" instead of "UAST parents"
|
||||
// because the latter might skip some elements between UIndentifier and UCallExpression in Kotlin,
|
||||
// leading to missing UReferenceExpressions. See KTIJ-31217.
|
||||
psiElement.parents(true).forEach { parent ->
|
||||
val uastParent = parent.toUElement() ?: return@forEach
|
||||
when (uastParent) {
|
||||
is UReferenceExpression -> {
|
||||
// E.g., catch (Runtime<caret>Exception e)
|
||||
val resolved = uastParent.resolve()
|
||||
return when (resolved) {
|
||||
is PsiClass -> resolved
|
||||
is PsiMethod -> if (resolved.isConstructor) resolved.containingClass else null
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
is UClass -> {
|
||||
// E.g., public class Runtime<caret>Exception extends Exception {
|
||||
val clazz = uastParent.javaPsi
|
||||
// If the initial element is not the class name identifier, ignore it.
|
||||
// Note that direct comparison of UElements is not working correctly.
|
||||
val uastIdentifier = psiElement.toUElement() as? UIdentifier
|
||||
if (uastIdentifier == null || uastIdentifier.name != clazz.name) return null
|
||||
val clazzIdentifier = uastParent.uastAnchor?.sourcePsi?.takeIf { it.text == clazz.name }
|
||||
if (clazzIdentifier != null && psiElement != clazzIdentifier) return null
|
||||
|
||||
return clazz
|
||||
}
|
||||
is UThrowExpression -> {
|
||||
// E.g., throw<caret> new RuntimeException()
|
||||
|
||||
// It would be better to check that psiElement is a throw keyword, but I was unable to implement this.
|
||||
// It means that intention would also be given in the following case: throw foo(<caret>)
|
||||
|
||||
val thrownType = uastParent.thrownExpression.getExpressionType() as? PsiClassType
|
||||
return thrownType?.resolve()
|
||||
}
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
override fun findExceptionReference(project: Project, element: PsiElement): ExceptionReference? {
|
||||
val clazz = findClassReference(element) ?: return null
|
||||
if (!InheritanceUtil.isInheritor(clazz, CommonClassNames.JAVA_LANG_THROWABLE)) return null
|
||||
val qualifiedName = clazz.qualifiedName ?: return null
|
||||
val displayName = clazz.name ?: qualifiedName
|
||||
return JVMExceptionReference(qualifiedName, displayName)
|
||||
}
|
||||
|
||||
open class JVMExceptionReference(
|
||||
private val qualifiedName: String,
|
||||
override val displayName: String,
|
||||
) : ExceptionReference {
|
||||
|
||||
override fun findExistingBreakpoint(project: Project): XBreakpoint<*>? =
|
||||
XDebuggerManager.getInstance(project)
|
||||
.breakpointManager
|
||||
.getBreakpoints(JavaExceptionBreakpointType::class.java)
|
||||
.firstOrNull { it.properties.myQualifiedName == qualifiedName }
|
||||
|
||||
override fun createBreakpoint(project: Project): XBreakpoint<*>? =
|
||||
DebuggerManagerEx.getInstanceEx(project)
|
||||
.breakpointManager
|
||||
.addExceptionBreakpoint(qualifiedName)
|
||||
?.xBreakpoint
|
||||
}
|
||||
}
|
||||
@@ -965,6 +965,7 @@
|
||||
<jdkUpdateCheckContributor implementation="com.intellij.execution.AlternativeSdkRootsProviderForJdkUpdate"/>
|
||||
<debugger.asyncStackTraceProvider
|
||||
implementation="com.intellij.debugger.ui.breakpoints.StackCapturingLineBreakpoint$CaptureAsyncStackTraceProvider"/>
|
||||
<xdebugger.controlExceptionBreakpointSupport implementation="com.intellij.debugger.codeinsight.ControlExceptionBreakpointJVMSupport"/>
|
||||
|
||||
<debugger.compoundRendererProvider implementation="com.intellij.debugger.ui.tree.render.UnboxableTypeRenderer$BooleanRenderer"/>
|
||||
<debugger.compoundRendererProvider implementation="com.intellij.debugger.ui.tree.render.UnboxableTypeRenderer$ByteRenderer"/>
|
||||
|
||||
@@ -411,6 +411,12 @@ f:com.intellij.xdebugger.breakpoints.ui.XBreakpointsGroupingPriorities
|
||||
- sf:BY_TYPE:I
|
||||
- sf:DEFAULT:I
|
||||
- <init>():V
|
||||
com.intellij.xdebugger.codeinsight.ControlExceptionBreakpointSupport
|
||||
- a:findExceptionReference(com.intellij.openapi.project.Project,com.intellij.psi.PsiElement):com.intellij.xdebugger.codeinsight.ControlExceptionBreakpointSupport$ExceptionReference
|
||||
com.intellij.xdebugger.codeinsight.ControlExceptionBreakpointSupport$ExceptionReference
|
||||
- a:createBreakpoint(com.intellij.openapi.project.Project):com.intellij.xdebugger.breakpoints.XBreakpoint
|
||||
- a:findExistingBreakpoint(com.intellij.openapi.project.Project):com.intellij.xdebugger.breakpoints.XBreakpoint
|
||||
- a:getDisplayName():java.lang.String
|
||||
e:com.intellij.xdebugger.evaluation.EvaluationMode
|
||||
- java.lang.Enum
|
||||
- sf:CODE_FRAGMENT:com.intellij.xdebugger.evaluation.EvaluationMode
|
||||
|
||||
@@ -339,3 +339,8 @@ xdebugger.hotswap.status.success=Code has been reloaded
|
||||
xdebugger.hotswap.tooltip.apply=Apply hot swap
|
||||
xdebugger.hotswap.tooltip.description=You changed code during the debug session. You can apply these changes without restarting. All the modified files will be recompiled and reloaded.
|
||||
action.hotswap.hide.text=Hide
|
||||
|
||||
xdebugger.intention.control.exception.breakpoint.family=Create exception breakpoint
|
||||
xdebugger.intention.control.exception.breakpoint.create.text=Create exception breakpoint on {0}
|
||||
xdebugger.intention.control.exception.breakpoint.enable.text=Enable exception breakpoint on {0}
|
||||
xdebugger.intention.control.exception.breakpoint.disable.text=Disable exception breakpoint on {0}
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
// 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.xdebugger.codeinsight
|
||||
|
||||
import com.intellij.openapi.project.Project
|
||||
import com.intellij.openapi.util.NlsSafe
|
||||
import com.intellij.psi.PsiElement
|
||||
import com.intellij.xdebugger.breakpoints.XBreakpoint
|
||||
import org.jetbrains.annotations.ApiStatus
|
||||
|
||||
interface ControlExceptionBreakpointSupport {
|
||||
|
||||
/**
|
||||
* Find a reference to an exception class or something throwable, which can be used as a target of exception breakpoint.
|
||||
*
|
||||
* Note: don't capture [Project] or [PsiElement] to prevent memory leaks because the result might be cached for a long time.
|
||||
*/
|
||||
@ApiStatus.OverrideOnly
|
||||
fun findExceptionReference(project: Project, element: PsiElement): ExceptionReference?
|
||||
|
||||
interface ExceptionReference {
|
||||
val displayName: @NlsSafe String
|
||||
|
||||
fun findExistingBreakpoint(project: Project): XBreakpoint<*>?
|
||||
|
||||
fun createBreakpoint(project: Project): XBreakpoint<*>?
|
||||
}
|
||||
|
||||
}
|
||||
@@ -24,6 +24,7 @@
|
||||
<extensionPoint name="xdebugger.inlineBreakpointsDisabler" interface="com.intellij.xdebugger.breakpoints.InlineBreakpointsDisabler" dynamic="true"/>
|
||||
|
||||
<extensionPoint name="xdebugger.textValueVisualizer" interface="com.intellij.xdebugger.ui.TextValueVisualizer" dynamic="true"/>
|
||||
<extensionPoint name="xdebugger.controlExceptionBreakpointSupport" interface="com.intellij.xdebugger.codeinsight.ControlExceptionBreakpointSupport" dynamic="true"/>
|
||||
|
||||
<extensionPoint name="xdebugger.hotSwapUiExtension" interface="com.intellij.xdebugger.impl.hotswap.HotSwapUiExtension" dynamic="true"/>
|
||||
</extensionPoints>
|
||||
@@ -144,6 +145,11 @@
|
||||
<registryKey defaultValue="false" key="debugger.valueLookupFrontendBackend"
|
||||
description="Provides a way to use frontend-backend implementation of debugger's evaluation popup"/>
|
||||
<platform.entityTypes implementation="com.intellij.xdebugger.impl.evaluate.XDebuggerValueLookupEntityTypesProvider"/>
|
||||
|
||||
<intentionAction>
|
||||
<language>UAST</language>
|
||||
<className>com.intellij.xdebugger.impl.codeinsight.ControlExceptionBreakpointIntentionAction</className>
|
||||
</intentionAction>
|
||||
</extensions>
|
||||
|
||||
<actions>
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
// 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.xdebugger.impl.codeinsight
|
||||
|
||||
import com.intellij.codeInsight.intention.BaseElementAtCaretIntentionAction
|
||||
import com.intellij.codeInspection.util.IntentionFamilyName
|
||||
import com.intellij.icons.AllIcons
|
||||
import com.intellij.openapi.editor.Editor
|
||||
import com.intellij.openapi.extensions.ExtensionPointName
|
||||
import com.intellij.openapi.project.Project
|
||||
import com.intellij.openapi.util.Iconable
|
||||
import com.intellij.psi.PsiElement
|
||||
import com.intellij.psi.PsiFile
|
||||
import com.intellij.xdebugger.XDebuggerBundle
|
||||
import com.intellij.xdebugger.codeinsight.ControlExceptionBreakpointSupport
|
||||
import com.intellij.xdebugger.codeinsight.ControlExceptionBreakpointSupport.ExceptionReference
|
||||
import javax.swing.Icon
|
||||
|
||||
private val supportsExtensionPoint: ExtensionPointName<ControlExceptionBreakpointSupport> =
|
||||
ExtensionPointName.create("com.intellij.xdebugger.controlExceptionBreakpointSupport")
|
||||
|
||||
// It's not a regular source code changing action. There will be no before/after templates, category or description.
|
||||
@Suppress("IntentionDescriptionNotFoundInspection")
|
||||
internal class ControlExceptionBreakpointIntentionAction : BaseElementAtCaretIntentionAction(), Iconable {
|
||||
|
||||
private var foundExceptionReference: ExceptionReference? = null
|
||||
|
||||
// Explicitly remember what action is suggested during isAvailable calculation
|
||||
// to prevent problems in cases when user wanted to disable the breakpoint which was concurrently disabled.
|
||||
private var shouldEnable = false
|
||||
|
||||
override fun getFamilyName(): @IntentionFamilyName String =
|
||||
XDebuggerBundle.message("xdebugger.intention.control.exception.breakpoint.family")
|
||||
|
||||
override fun getIcon(flags: Int): Icon =
|
||||
AllIcons.Debugger.Db_exception_breakpoint
|
||||
|
||||
override fun checkFile(file: PsiFile): Boolean =
|
||||
true
|
||||
|
||||
override fun startInWriteAction(): Boolean =
|
||||
false
|
||||
|
||||
override fun isAvailable(project: Project, editor: Editor, psiElement: PsiElement): Boolean {
|
||||
for (support in supportsExtensionPoint.extensionList) {
|
||||
val exRef = support.findExceptionReference(project, psiElement) ?: continue
|
||||
val displayName = exRef.displayName
|
||||
val breakpoint = exRef.findExistingBreakpoint(project)
|
||||
when {
|
||||
breakpoint == null -> {
|
||||
text = XDebuggerBundle.message("xdebugger.intention.control.exception.breakpoint.create.text", displayName)
|
||||
shouldEnable = true
|
||||
}
|
||||
breakpoint.isEnabled -> {
|
||||
text = XDebuggerBundle.message("xdebugger.intention.control.exception.breakpoint.disable.text", displayName)
|
||||
shouldEnable = false
|
||||
}
|
||||
else -> {
|
||||
text = XDebuggerBundle.message("xdebugger.intention.control.exception.breakpoint.enable.text", displayName)
|
||||
shouldEnable = true
|
||||
}
|
||||
}
|
||||
foundExceptionReference = exRef
|
||||
return true
|
||||
}
|
||||
foundExceptionReference = null
|
||||
return false
|
||||
}
|
||||
|
||||
override fun invoke(project: Project, editor: Editor, element: PsiElement) {
|
||||
val exRef = foundExceptionReference ?: return
|
||||
val breakpoint = exRef.findExistingBreakpoint(project)
|
||||
if (breakpoint == null) {
|
||||
if (shouldEnable) {
|
||||
exRef.createBreakpoint(project)
|
||||
} // otherwise it's nothing to do: breakpoint was already deleted
|
||||
}
|
||||
else {
|
||||
breakpoint.isEnabled = shouldEnable
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
|
||||
package org.jetbrains.kotlin.idea.debugger.test.codeinsight
|
||||
|
||||
import com.intellij.debugger.codeinsight.ControlExceptionBreakpointJVMSupport
|
||||
import com.intellij.xdebugger.codeinsight.ControlExceptionBreakpointSupport
|
||||
import org.jetbrains.kotlin.idea.test.KotlinLightCodeInsightFixtureTestCase
|
||||
import org.jetbrains.kotlin.idea.test.util.elementByOffset
|
||||
|
||||
/**
|
||||
* Tests for Kotlin support in [ControlExceptionBreakpointJVMSupport].
|
||||
*/
|
||||
class ControlExceptionBreakpointKotlinSupportTest : KotlinLightCodeInsightFixtureTestCase() {
|
||||
|
||||
private fun findExceptionReference(text: String): ControlExceptionBreakpointSupport.ExceptionReference? {
|
||||
assertTrue(text.contains("<caret>"))
|
||||
myFixture.configureByText("A.kt", text)
|
||||
myFixture.doHighlighting()
|
||||
val support = ControlExceptionBreakpointJVMSupport()
|
||||
val exRef = support.findExceptionReference(project, myFixture.elementByOffset)
|
||||
return exRef
|
||||
}
|
||||
|
||||
private fun checkAvailable(text: String, exceptionName: String) {
|
||||
val exRef = findExceptionReference(text)
|
||||
assertNotNull(exRef)
|
||||
assertEquals(exceptionName, exRef!!.displayName)
|
||||
}
|
||||
|
||||
private fun checkUnavailable(text: String) {
|
||||
val exRef = findExceptionReference(text)
|
||||
assertNull(exRef)
|
||||
}
|
||||
|
||||
private fun methodBody(body: String) =
|
||||
"fun f() { $body }"
|
||||
|
||||
fun testNew() = checkAvailable(
|
||||
methodBody("val o = Runtime<caret>Exception()"),
|
||||
"RuntimeException"
|
||||
)
|
||||
|
||||
fun testType() = checkAvailable(
|
||||
methodBody("val o: <caret>Throwable = RuntimeException()"),
|
||||
"Throwable"
|
||||
)
|
||||
|
||||
fun testTypeUnavailable() = checkUnavailable(
|
||||
methodBody("val o: <caret>Any = RuntimeException()")
|
||||
)
|
||||
|
||||
fun testCatch() = checkAvailable(
|
||||
methodBody("try { } catch (e: Runtime<caret>Exception) { }"),
|
||||
"RuntimeException"
|
||||
)
|
||||
|
||||
fun testThrow() = checkAvailable(
|
||||
methodBody("try { } catch (e: RuntimeException) { <caret>throw e }"),
|
||||
"RuntimeException"
|
||||
)
|
||||
|
||||
fun testClass() = checkAvailable(
|
||||
"class <caret>A : RuntimeException() {}",
|
||||
"A"
|
||||
)
|
||||
|
||||
fun testClassUnavailable() = checkUnavailable(
|
||||
"class <caret>A {}"
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user