[debugger] intention to control exception breakpoints in editor, IJPL-161898

GitOrigin-RevId: 74a8cd67b65e433aae9012672dd9f8e92f2fb839
This commit is contained in:
Vladimir Parfinenko
2024-09-10 10:58:16 +02:00
committed by intellij-monorepo-bot
parent 2ee1d6591c
commit 4c63e241d2
8 changed files with 282 additions and 0 deletions

View File

@@ -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
}
}

View File

@@ -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"/>

View File

@@ -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

View File

@@ -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}

View File

@@ -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<*>?
}
}

View File

@@ -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>

View File

@@ -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
}
}
}

View File

@@ -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 {}"
)
}