1 Commits
Author SHA1 Message Date
chylex 86567eddfc Test 2026-01-22 08:05:45 +01:00
21 changed files with 180 additions and 162 deletions
+5 -16
View File
@@ -1,21 +1,12 @@
@file:Suppress("ConvertLambdaToReference") @file:Suppress("ConvertLambdaToReference")
import org.jetbrains.intellij.platform.gradle.TestFrameworkType
plugins { plugins {
kotlin("jvm") kotlin("jvm")
id("org.jetbrains.intellij.platform") id("org.jetbrains.intellij.platform")
} }
group = "com.chylex.intellij.inspectionlens" group = "com.chylex.intellij.inspectionlens"
version = "1.7.0" version = "1.6.1"
idea {
module {
excludeDirs.add(file(".kotlin"))
excludeDirs.add(file("build"))
}
}
repositories { repositories {
mavenCentral() mavenCentral()
@@ -27,21 +18,17 @@ repositories {
dependencies { dependencies {
intellijPlatform { intellijPlatform {
intellijIdeaUltimate("2025.3") intellijIdeaUltimate("261.17801.55-EAP", useInstaller = false)
bundledPlugin("tanvd.grazi") bundledPlugin("tanvd.grazi")
testFramework(TestFrameworkType.JUnit5)
} }
testImplementation("org.junit.jupiter:junit-jupiter:5.9.2") testImplementation("org.junit.jupiter:junit-jupiter:5.9.2")
testRuntimeOnly("org.junit.platform:junit-platform-launcher")
testRuntimeOnly("junit:junit:4.13.2") // https://youtrack.jetbrains.com/projects/IJPL/issues/IJPL-159134
} }
intellijPlatform { intellijPlatform {
pluginConfiguration { pluginConfiguration {
ideaVersion { ideaVersion {
sinceBuild.set("253") sinceBuild.set("242")
untilBuild.set(provider { null }) untilBuild.set(provider { null })
} }
} }
@@ -57,6 +44,8 @@ intellijPlatform {
} }
kotlin { kotlin {
jvmToolchain(25)
compilerOptions { compilerOptions {
freeCompilerArgs = listOf( freeCompilerArgs = listOf(
"-X" + "jvm-default=all", "-X" + "jvm-default=all",
-1
View File
@@ -1,2 +1 @@
kotlin.stdlib.default.dependency=false kotlin.stdlib.default.dependency=false
org.gradle.caching = true
Binary file not shown.
+1 -3
View File
@@ -1,9 +1,7 @@
distributionBase=GRADLE_USER_HOME distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-9.7.1-bin.zip distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.3-bin.zip
networkTimeout=10000 networkTimeout=10000
retries=0
retryBackOffMs=500
validateDistributionUrl=true validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists zipStorePath=wrapper/dists
Vendored
+9 -6
View File
@@ -1,7 +1,7 @@
#!/bin/sh #!/bin/sh
# #
# Copyright © 2015 the original authors. # Copyright © 2015-2021 the original authors.
# #
# Licensed under the Apache License, Version 2.0 (the "License"); # Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License. # you may not use this file except in compliance with the License.
@@ -20,7 +20,7 @@
############################################################################## ##############################################################################
# #
# gradlew start up script for POSIX generated by Gradle. # Gradle start up script for POSIX generated by Gradle.
# #
# Important for running: # Important for running:
# #
@@ -29,7 +29,7 @@
# bash, then to run this script, type that shell name before the whole # bash, then to run this script, type that shell name before the whole
# command line, like: # command line, like:
# #
# ksh gradlew # ksh Gradle
# #
# Busybox and similar reduced shells will NOT work, because this script # Busybox and similar reduced shells will NOT work, because this script
# requires all of these POSIX shell features: # requires all of these POSIX shell features:
@@ -57,7 +57,7 @@
# Darwin, MinGW, and NonStop. # Darwin, MinGW, and NonStop.
# #
# (3) This script is generated from the Groovy template # (3) This script is generated from the Groovy template
# https://github.com/gradle/gradle/blob/3d91ce3b8caaf77ad09f381f43615b715b53f72c/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt # https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
# within the Gradle project. # within the Gradle project.
# #
# You can find Gradle at https://github.com/gradle/gradle/. # You can find Gradle at https://github.com/gradle/gradle/.
@@ -114,6 +114,7 @@ case "$( uname )" in #(
NONSTOP* ) nonstop=true ;; NONSTOP* ) nonstop=true ;;
esac esac
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
# Determine the Java command to use to start the JVM. # Determine the Java command to use to start the JVM.
@@ -171,6 +172,7 @@ fi
# For Cygwin or MSYS, switch paths to Windows format before running java # For Cygwin or MSYS, switch paths to Windows format before running java
if "$cygwin" || "$msys" ; then if "$cygwin" || "$msys" ; then
APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
JAVACMD=$( cygpath --unix "$JAVACMD" ) JAVACMD=$( cygpath --unix "$JAVACMD" )
@@ -203,14 +205,15 @@ fi
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Collect all arguments for the java command: # Collect all arguments for the java command:
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, # * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
# and any embedded shellness will be escaped. # and any embedded shellness will be escaped.
# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be # * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
# treated as '${Hostname}' itself on the command line. # treated as '${Hostname}' itself on the command line.
set -- \ set -- \
"-Dorg.gradle.appname=$APP_BASE_NAME" \ "-Dorg.gradle.appname=$APP_BASE_NAME" \
-jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \ -classpath "$CLASSPATH" \
org.gradle.wrapper.GradleWrapperMain \
"$@" "$@"
# Stop when "xargs" is not available. # Stop when "xargs" is not available.
Vendored
+24 -12
View File
@@ -19,12 +19,12 @@
@if "%DEBUG%"=="" @echo off @if "%DEBUG%"=="" @echo off
@rem ########################################################################## @rem ##########################################################################
@rem @rem
@rem gradlew startup script for Windows @rem Gradle startup script for Windows
@rem @rem
@rem ########################################################################## @rem ##########################################################################
@rem Set local scope for the variables, and ensure extensions are enabled @rem Set local scope for the variables with windows NT shell
setlocal EnableExtensions if "%OS%"=="Windows_NT" setlocal
set DIRNAME=%~dp0 set DIRNAME=%~dp0
if "%DIRNAME%"=="" set DIRNAME=. if "%DIRNAME%"=="" set DIRNAME=.
@@ -51,7 +51,7 @@ echo. 1>&2
echo Please set the JAVA_HOME variable in your environment to match the 1>&2 echo Please set the JAVA_HOME variable in your environment to match the 1>&2
echo location of your Java installation. 1>&2 echo location of your Java installation. 1>&2
"%COMSPEC%" /c exit 1 goto fail
:findJavaFromJavaHome :findJavaFromJavaHome
set JAVA_HOME=%JAVA_HOME:"=% set JAVA_HOME=%JAVA_HOME:"=%
@@ -65,18 +65,30 @@ echo. 1>&2
echo Please set the JAVA_HOME variable in your environment to match the 1>&2 echo Please set the JAVA_HOME variable in your environment to match the 1>&2
echo location of your Java installation. 1>&2 echo location of your Java installation. 1>&2
"%COMSPEC%" /c exit 1 goto fail
:execute :execute
@rem Setup the command line @rem Setup the command line
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
@rem Execute gradlew @rem Execute Gradle
@rem endlocal doesn't take effect until after the line is parsed and variables are expanded "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
@rem which allows us to clear the local environment before executing the java command
endlocal & "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* & call :exitWithErrorLevel
:exitWithErrorLevel :end
@rem Use "%COMSPEC%" /c exit to allow operators to work properly in scripts @rem End local scope for the variables with windows NT shell
"%COMSPEC%" /c exit %ERRORLEVEL% if %ERRORLEVEL% equ 0 goto mainEnd
:fail
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem the _cmd.exe /c_ return code!
set EXIT_CODE=%ERRORLEVEL%
if %EXIT_CODE% equ 0 set EXIT_CODE=1
if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
exit /b %EXIT_CODE%
:mainEnd
if "%OS%"=="Windows_NT" endlocal
:omega
+2 -2
View File
@@ -2,7 +2,7 @@ rootProject.name = "InspectionLens"
pluginManagement { pluginManagement {
plugins { plugins {
kotlin("jvm") version "2.4.0" // https://plugins.jetbrains.com/docs/intellij/using-kotlin.html#bundled-stdlib-versions kotlin("jvm") version "2.3.0" // https://plugins.jetbrains.com/docs/intellij/using-kotlin.html#bundled-stdlib-versions
id("org.jetbrains.intellij.platform") version "2.18.1" // https://github.com/JetBrains/intellij-platform-gradle-plugin/releases id("org.jetbrains.intellij.platform") version "2.10.5" // https://github.com/JetBrains/intellij-platform-gradle-plugin/releases
} }
} }
@@ -8,6 +8,8 @@ import com.intellij.openapi.startup.ProjectActivity
class GrazieSupport : ProjectActivity { class GrazieSupport : ProjectActivity {
override suspend fun execute(project: Project) { override suspend fun execute(project: Project) {
LensSeverity.registerMapping(TextProblemSeverities.GRAMMAR_ERROR, LensSeverity.ERROR) LensSeverity.registerMapping(TextProblemSeverities.GRAMMAR_ERROR, LensSeverity.ERROR)
LensSeverity.registerMapping(TextProblemSeverities.STYLE_ERROR, LensSeverity.GRAZIE)
LensSeverity.registerMapping(TextProblemSeverities.STYLE_WARNING, LensSeverity.GRAZIE)
LensSeverity.registerMapping(TextProblemSeverities.STYLE_SUGGESTION, LensSeverity.GRAZIE) LensSeverity.registerMapping(TextProblemSeverities.STYLE_SUGGESTION, LensSeverity.GRAZIE)
} }
} }
@@ -14,26 +14,26 @@ internal class EditorLensManager(private val editor: Editor) {
private val lenses = IdentityHashMap<RangeHighlighter, EditorLens>() private val lenses = IdentityHashMap<RangeHighlighter, EditorLens>()
private val settings = service<LensSettingsState>() private val settings = service<LensSettingsState>()
private fun show(inspection: Inspection) { private fun show(highlighterWithInfo: HighlighterWithInfo) {
if (editor.isDisposed) { if (editor.isDisposed) {
return return
} }
val highlighter = inspection.highlighter val (highlighter, info) = highlighterWithInfo
if (!highlighter.isValid) { if (!highlighter.isValid) {
return return
} }
val existingLens = lenses[highlighter] val existingLens = lenses[highlighter]
if (existingLens != null) { if (existingLens != null) {
if (existingLens.update(inspection, settings)) { if (existingLens.update(info, settings)) {
return return
} }
existingLens.hide() existingLens.hide()
} }
val newLens = EditorLens.show(editor, inspection, settings) val newLens = EditorLens.show(editor, info, settings)
if (newLens != null) { if (newLens != null) {
lenses[highlighter] = newLens lenses[highlighter] = newLens
} }
@@ -66,9 +66,9 @@ internal class EditorLensManager(private val editor: Editor) {
sealed interface Command { sealed interface Command {
fun apply(lensManager: EditorLensManager) fun apply(lensManager: EditorLensManager)
class Show(private val inspection: Inspection) : Command { class Show(private val highlighter: HighlighterWithInfo) : Command {
override fun apply(lensManager: EditorLensManager) { override fun apply(lensManager: EditorLensManager) {
lensManager.show(inspection) lensManager.show(highlighter)
} }
} }
@@ -88,6 +88,7 @@ internal class EditorLensManager(private val editor: Editor) {
/** /**
* Batch mode affects both inlays and highlighters used for line colors. * Batch mode affects both inlays and highlighters used for line colors.
*/ */
@Suppress("ConvertLambdaToReference")
private inline fun executeInBatchMode(operations: Int, crossinline action: () -> Unit) { private inline fun executeInBatchMode(operations: Int, crossinline action: () -> Unit) {
if (operations > 1000) { if (operations > 1000) {
editor.inlayModel.execute(true) { action() } editor.inlayModel.execute(true) { action() }
@@ -7,8 +7,8 @@ internal class EditorLensManagerDispatcher(private val lensManager: EditorLensMa
private var queuedItems = mutableListOf<EditorLensManager.Command>() private var queuedItems = mutableListOf<EditorLensManager.Command>()
private var isEnqueued = false private var isEnqueued = false
fun show(inspection: Inspection) { fun show(highlighterWithInfo: HighlighterWithInfo) {
enqueue(EditorLensManager.Command.Show(inspection)) enqueue(EditorLensManager.Command.Show(highlighterWithInfo))
} }
fun hide(highlighter: RangeHighlighter) { fun hide(highlighter: RangeHighlighter) {
@@ -0,0 +1,28 @@
package com.chylex.intellij.inspectionlens.editor
import com.intellij.codeInsight.daemon.impl.AsyncDescriptionSupplier
import com.intellij.codeInsight.daemon.impl.HighlightInfo
import com.intellij.openapi.editor.markup.RangeHighlighter
import java.util.function.Consumer
open class HighlighterWithInfo private constructor(val highlighter: RangeHighlighter, private val info: HighlightInfo) {
val hasDescription
get() = info.description != null
operator fun component1() = highlighter
operator fun component2() = info
class Async(highlighter: RangeHighlighter, info: HighlightInfo, private val provider: AsyncDescriptionSupplier) : HighlighterWithInfo(highlighter, info) {
fun requestDescription(callback: Consumer<String?>) {
provider.requestDescription().onSuccess(callback)
}
}
companion object {
fun from(highlighter: RangeHighlighter, info: HighlightInfo) =
if (info is AsyncDescriptionSupplier)
Async(highlighter, info, info)
else
HighlighterWithInfo(highlighter, info)
}
}
@@ -1,6 +0,0 @@
package com.chylex.intellij.inspectionlens.editor
import com.intellij.codeInsight.daemon.impl.HighlightInfo
import com.intellij.openapi.editor.markup.RangeHighlighter
data class Inspection(val highlighter: RangeHighlighter, val info: HighlightInfo)
@@ -2,7 +2,6 @@ package com.chylex.intellij.inspectionlens.editor
import com.chylex.intellij.inspectionlens.InspectionLens import com.chylex.intellij.inspectionlens.InspectionLens
import com.chylex.intellij.inspectionlens.settings.LensSettingsState import com.chylex.intellij.inspectionlens.settings.LensSettingsState
import com.intellij.codeInsight.daemon.impl.AsyncDescriptionSupplier
import com.intellij.codeInsight.daemon.impl.HighlightInfo import com.intellij.codeInsight.daemon.impl.HighlightInfo
import com.intellij.codeInsight.daemon.impl.UpdateHighlightersUtil.isFileLevelOrGutterAnnotation import com.intellij.codeInsight.daemon.impl.UpdateHighlightersUtil.isFileLevelOrGutterAnnotation
import com.intellij.openapi.components.service import com.intellij.openapi.components.service
@@ -40,25 +39,28 @@ internal class LensMarkupModelListener(private val lensManagerDispatcher: Editor
return return
} }
if (info is AsyncDescriptionSupplier) { val highlighterWithInfo = HighlighterWithInfo.from(highlighter, info)
info.requestDescription().onSuccess { processHighlighterWithInfo(highlighterWithInfo, lensManagerDispatcher::show, ::showAsynchronously)
if (highlighter.isValid) {
showInspection(highlighter, info)
}
}
}
else {
showInspection(highlighter, info)
}
} }
private fun getFilteredHighlightInfo(highlighter: RangeHighlighter): HighlightInfo? { private fun getFilteredHighlightInfo(highlighter: RangeHighlighter): HighlightInfo? {
return HighlightInfo.fromRangeHighlighter(highlighter)?.takeIf { settings.severityFilter.test(it.severity) } return HighlightInfo.fromRangeHighlighter(highlighter)?.takeIf { settings.severityFilter.test(it.severity) }
} }
private fun showInspection(highlighter: RangeHighlighter, info: HighlightInfo) { private inline fun processHighlighterWithInfo(highlighterWithInfo: HighlighterWithInfo, actionForImmediate: (HighlighterWithInfo) -> Unit, actionForAsync: (HighlighterWithInfo.Async) -> Unit) {
if (info.description != null) { if (highlighterWithInfo is HighlighterWithInfo.Async) {
lensManagerDispatcher.show(Inspection(highlighter, info)) actionForAsync(highlighterWithInfo)
}
else if (highlighterWithInfo.hasDescription) {
actionForImmediate(highlighterWithInfo)
}
}
private fun showAsynchronously(highlighterWithInfo: HighlighterWithInfo.Async) {
highlighterWithInfo.requestDescription {
if (highlighterWithInfo.highlighter.isValid && highlighterWithInfo.hasDescription) {
lensManagerDispatcher.show(highlighterWithInfo)
}
} }
} }
@@ -1,23 +1,23 @@
package com.chylex.intellij.inspectionlens.editor.lens package com.chylex.intellij.inspectionlens.editor.lens
import com.chylex.intellij.inspectionlens.editor.Inspection
import com.chylex.intellij.inspectionlens.settings.LensSettingsState import com.chylex.intellij.inspectionlens.settings.LensSettingsState
import com.intellij.codeInsight.daemon.impl.HighlightInfo
import com.intellij.openapi.editor.Editor import com.intellij.openapi.editor.Editor
internal class EditorLens private constructor(private var inlay: EditorLensInlay, private var lineBackground: EditorLensLineBackground, private var severity: LensSeverity) { internal class EditorLens private constructor(private var inlay: EditorLensInlay, private var lineBackground: EditorLensLineBackground, private var severity: LensSeverity) {
fun update(inspection: Inspection, settings: LensSettingsState): Boolean { fun update(info: HighlightInfo, settings: LensSettingsState): Boolean {
val editor = inlay.editor val editor = inlay.editor
val oldSeverity = severity val oldSeverity = severity
severity = LensSeverity.from(inspection.info.severity) severity = LensSeverity.from(info.severity)
if (!inlay.tryUpdate(inspection)) { if (!inlay.tryUpdate(info)) {
inlay = EditorLensInlay.show(editor, inspection, settings) ?: return false inlay = EditorLensInlay.show(editor, info, settings) ?: return false
} }
if (lineBackground.isInvalid || oldSeverity != severity) { if (lineBackground.isInvalid || oldSeverity != severity) {
lineBackground.hide(editor) lineBackground.hide(editor)
lineBackground = EditorLensLineBackground.show(editor, inspection.info) lineBackground = EditorLensLineBackground.show(editor, info)
} }
return true return true
@@ -33,9 +33,8 @@ internal class EditorLens private constructor(private var inlay: EditorLensInlay
} }
companion object { companion object {
fun show(editor: Editor, inspection: Inspection, settings: LensSettingsState): EditorLens? { fun show(editor: Editor, info: HighlightInfo, settings: LensSettingsState): EditorLens? {
val info = inspection.info val inlay = EditorLensInlay.show(editor, info, settings) ?: return null
val inlay = EditorLensInlay.show(editor, inspection, settings) ?: return null
val lineBackground = EditorLensLineBackground.show(editor, info) val lineBackground = EditorLensLineBackground.show(editor, info)
val severity = LensSeverity.from(info.severity) val severity = LensSeverity.from(info.severity)
return EditorLens(inlay, lineBackground, severity) return EditorLens(inlay, lineBackground, severity)
@@ -1,6 +1,5 @@
package com.chylex.intellij.inspectionlens.editor.lens package com.chylex.intellij.inspectionlens.editor.lens
import com.chylex.intellij.inspectionlens.editor.Inspection
import com.chylex.intellij.inspectionlens.settings.LensSettingsState import com.chylex.intellij.inspectionlens.settings.LensSettingsState
import com.intellij.codeInsight.daemon.impl.HighlightInfo import com.intellij.codeInsight.daemon.impl.HighlightInfo
import com.intellij.openapi.editor.Editor import com.intellij.openapi.editor.Editor
@@ -12,12 +11,12 @@ internal value class EditorLensInlay(private val inlay: Inlay<LensRenderer>) {
val editor val editor
get() = inlay.editor get() = inlay.editor
fun tryUpdate(inspection: Inspection): Boolean { fun tryUpdate(info: HighlightInfo): Boolean {
if (!inlay.isValid) { if (!inlay.isValid) {
return false return false
} }
inlay.renderer.setPropertiesFrom(inspection) inlay.renderer.setPropertiesFrom(info)
inlay.update() inlay.update()
return true return true
} }
@@ -27,10 +26,10 @@ internal value class EditorLensInlay(private val inlay: Inlay<LensRenderer>) {
} }
companion object { companion object {
fun show(editor: Editor, inspection: Inspection, settings: LensSettingsState): EditorLensInlay? { fun show(editor: Editor, info: HighlightInfo, settings: LensSettingsState): EditorLensInlay? {
val offset = getInlayHintOffset(inspection.info) val offset = getInlayHintOffset(info)
val priority = getInlayHintPriority(editor, inspection.info) val priority = getInlayHintPriority(editor, info)
val renderer = LensRenderer(inspection, settings) val renderer = LensRenderer(info, settings)
val properties = InlayProperties() val properties = InlayProperties()
.relatesToPrecedingText(true) .relatesToPrecedingText(true)
@@ -1,31 +1,31 @@
package com.chylex.intellij.inspectionlens.editor.lens package com.chylex.intellij.inspectionlens.editor.lens
import com.chylex.intellij.inspectionlens.InspectionLens import com.chylex.intellij.inspectionlens.InspectionLens
import com.chylex.intellij.inspectionlens.editor.Inspection
import com.intellij.codeInsight.daemon.impl.HighlightInfo import com.intellij.codeInsight.daemon.impl.HighlightInfo
import com.intellij.codeInsight.daemon.impl.HighlightInfo.IntentionActionDescriptor import com.intellij.codeInsight.daemon.impl.HighlightInfo.IntentionActionDescriptor
import com.intellij.codeInsight.daemon.impl.IntentionsUI import com.intellij.codeInsight.daemon.impl.IntentionsUI
import com.intellij.codeInsight.daemon.impl.ShowIntentionsPass
import com.intellij.codeInsight.daemon.impl.ShowIntentionsPass.IntentionsInfo import com.intellij.codeInsight.daemon.impl.ShowIntentionsPass.IntentionsInfo
import com.intellij.codeInsight.hint.HintManager import com.intellij.codeInsight.hint.HintManager
import com.intellij.codeInsight.intention.impl.CachedIntentions import com.intellij.codeInsight.intention.impl.CachedIntentions
import com.intellij.codeInsight.intention.impl.IntentionHintComponent import com.intellij.codeInsight.intention.impl.IntentionHintComponent
import com.intellij.codeInsight.intention.impl.ShowIntentionActionsHandler import com.intellij.codeInsight.intention.impl.ShowIntentionActionsHandler
import com.intellij.ide.DataManager import com.intellij.lang.LangBundle
import com.intellij.lang.annotation.HighlightSeverity
import com.intellij.openapi.actionSystem.ActionManager import com.intellij.openapi.actionSystem.ActionManager
import com.intellij.openapi.actionSystem.ActionPlaces import com.intellij.openapi.actionSystem.ActionPlaces
import com.intellij.openapi.actionSystem.ActionUiKind
import com.intellij.openapi.actionSystem.AnActionEvent
import com.intellij.openapi.actionSystem.IdeActions import com.intellij.openapi.actionSystem.IdeActions
import com.intellij.openapi.actionSystem.ex.ActionUtil import com.intellij.openapi.actionSystem.ex.ActionUtil
import com.intellij.openapi.application.ModalityState
import com.intellij.openapi.application.ReadAction
import com.intellij.openapi.editor.Editor import com.intellij.openapi.editor.Editor
import com.intellij.openapi.editor.Inlay import com.intellij.openapi.editor.Inlay
import com.intellij.openapi.editor.markup.RangeHighlighter import com.intellij.openapi.project.DumbService
import com.intellij.openapi.project.Project import com.intellij.openapi.project.Project
import com.intellij.psi.PsiDocumentManager import com.intellij.psi.PsiDocumentManager
import com.intellij.psi.PsiFile import com.intellij.psi.PsiFile
import com.intellij.psi.util.PsiUtilBase import com.intellij.psi.util.PsiUtilBase
import com.intellij.ui.awt.RelativePoint import com.intellij.ui.awt.RelativePoint
import com.intellij.util.concurrency.AppExecutorUtil
import java.lang.reflect.Method import java.lang.reflect.Method
internal object IntentionsPopup { internal object IntentionsPopup {
@@ -36,39 +36,38 @@ internal object IntentionsPopup {
val parameterTypes = method.parameterTypes val parameterTypes = method.parameterTypes
method.name == "showPopup" && method.name == "showPopup" &&
parameterTypes.size == 2 && parameterTypes.size in 1..2 &&
parameterTypes[0] === RelativePoint::class.java && parameterTypes[0] === RelativePoint::class.java &&
parameterTypes[1].name == INTENTION_SOURCE_CLASS_NAME parameterTypes.getOrNull(1).let { p -> p == null || p.name == INTENTION_SOURCE_CLASS_NAME }
} }
method.isAccessible = true method.isAccessible = true
@Suppress("UNCHECKED_CAST") @Suppress("UNCHECKED_CAST")
val intentionSourceEnum = Class.forName(INTENTION_SOURCE_CLASS_NAME) as Class<Enum<*>> val args: Array<Any?> = if (method.parameterCount == 1)
val intentionSource = intentionSourceEnum.enumConstants.first { it.name == "OTHER" } arrayOf(null)
else
arrayOf(null, (Class.forName(INTENTION_SOURCE_CLASS_NAME) as Class<Enum<*>>).enumConstants.first { it.name == "OTHER" })
ShowPopupMethod(method, arrayOf(null, intentionSource)) ShowPopupMethod(method, args)
} catch (t: Throwable) { } catch (t: Throwable) {
InspectionLens.LOG.warn("Could not initialize intention popup", t) InspectionLens.LOG.warn("Could not initialize intention popup", t)
null null
} }
val hasShowPopupMethod
get() = showPopupMethod != null
private class ShowPopupMethod(private val method: Method, private val args: Array<Any?>) { private class ShowPopupMethod(private val method: Method, private val args: Array<Any?>) {
operator fun invoke(component: IntentionHintComponent) { operator fun invoke(component: IntentionHintComponent) {
method.invoke(component, *args) method.invoke(component, *args)
} }
} }
fun show(inspection: Inspection, inlay: Inlay<*>) { fun show(highlightInfo: HighlightInfo, inlay: Inlay<*>) {
if (!tryShow(inspection, inlay)) { if (!tryShow(highlightInfo, inlay)) {
HintManager.getInstance().showInformationHint(inlay.editor, "No context actions available at this location") showNoActionsAvailable(inlay.editor)
} }
} }
private fun tryShow(inspection: Inspection, inlay: Inlay<*>): Boolean { private fun tryShow(highlightInfo: HighlightInfo, inlay: Inlay<*>): Boolean {
val editor = inlay.editor val editor = inlay.editor
val project = editor.project ?: return false val project = editor.project ?: return false
val file = PsiUtilBase.getPsiFileInEditor(editor, project) ?: return false val file = PsiUtilBase.getPsiFileInEditor(editor, project) ?: return false
@@ -76,47 +75,49 @@ internal object IntentionsPopup {
PsiDocumentManager.getInstance(project).commitAllDocuments() PsiDocumentManager.getInstance(project).commitAllDocuments()
IntentionsUI.getInstance(project).hide() IntentionsUI.getInstance(project).hide()
val intentions = collectIntentions(editor, file, inspection.highlighter, inlay.offset) ReadAction
if (intentions == null) { .nonBlocking<IntentionsInfo> { collectIntentions(editor, project, file, highlightInfo, inlay.offset) }
return false .finishOnUiThread(ModalityState.current()) { tryShowPopup(project, file, editor, it) }
} .submit(AppExecutorUtil.getAppExecutorService())
try { return true
showIntentionsPopup(project, file, editor, intentions)
return true
} catch (t: Throwable) {
InspectionLens.LOG.error("Could not show intention popup", t)
return false
}
} }
private fun collectIntentions(editor: Editor, file: PsiFile, highlighter: RangeHighlighter, offset: Int): IntentionsInfo? { private fun collectIntentions(editor: Editor, project: Project, file: PsiFile, info: HighlightInfo, offset: Int): IntentionsInfo {
val resolvedInfo = HighlightInfo.fromRangeHighlighter(highlighter) val intentions = mutableListOf<IntentionActionDescriptor>()
if (resolvedInfo == null) {
return null
}
val intentionActions = mutableListOf<IntentionActionDescriptor>() info.findRegisteredQuickFix { descriptor, _ ->
if (DumbService.getInstance(project).isUsableInCurrentContext(descriptor.action) && ShowIntentionActionsHandler.availableFor(file, editor, offset, descriptor.action)) {
resolvedInfo.findRegisteredQuickFix { descriptor, _ -> intentions.add(descriptor)
if (ShowIntentionActionsHandler.availableFor(file, editor, offset, descriptor.action)) {
intentionActions.add(descriptor)
} }
null null
} }
return IntentionsInfo().also { return IntentionsInfo().also {
it.offset = offset it.offset = offset
ShowIntentionsPass.fillIntentionsInfoForHighlightInfo(resolvedInfo, it, intentionActions)
if (info.severity === HighlightSeverity.ERROR) {
it.errorFixesToShow.addAll(intentions)
}
else {
it.inspectionFixesToShow.addAll(intentions)
}
} }
} }
private fun showIntentionsPopup(project: Project, file: PsiFile, editor: Editor, intentions: IntentionsInfo) { private fun tryShowPopup(project: Project, file: PsiFile, editor: Editor, intentions: IntentionsInfo) {
try {
showPopup(project, file, editor, intentions)
} catch (t: Throwable) {
InspectionLens.LOG.error("Could not show intention popup", t)
showNoActionsAvailable(editor)
}
}
private fun showPopup(project: Project, file: PsiFile, editor: Editor, intentions: IntentionsInfo) {
if (intentions.isEmpty || showPopupMethod == null) { if (intentions.isEmpty || showPopupMethod == null) {
val showIntentionsAction = ActionManager.getInstance().getAction(IdeActions.ACTION_SHOW_INTENTION_ACTIONS) val showIntentionsAction = ActionManager.getInstance().getAction(IdeActions.ACTION_SHOW_INTENTION_ACTIONS)
val dataContext = DataManager.getInstance().getDataContext(editor.component) ActionUtil.invokeAction(showIntentionsAction, editor.component, ActionPlaces.EDITOR_INLAY, null, null)
val event = AnActionEvent.createEvent(showIntentionsAction, dataContext, null, ActionPlaces.EDITOR_INLAY, ActionUiKind.NONE, null)
ActionUtil.performAction(showIntentionsAction, event)
} }
else { else {
val cachedIntentions = CachedIntentions.create(project, file, editor, intentions) val cachedIntentions = CachedIntentions.create(project, file, editor, intentions)
@@ -125,4 +126,7 @@ internal object IntentionsPopup {
} }
} }
private fun showNoActionsAvailable(editor: Editor) {
HintManager.getInstance().showInformationHint(editor, LangBundle.message("hint.text.no.context.actions.available.at.this.location"))
}
} }
@@ -1,8 +1,8 @@
package com.chylex.intellij.inspectionlens.editor.lens package com.chylex.intellij.inspectionlens.editor.lens
import com.chylex.intellij.inspectionlens.editor.Inspection
import com.chylex.intellij.inspectionlens.settings.LensHoverMode import com.chylex.intellij.inspectionlens.settings.LensHoverMode
import com.chylex.intellij.inspectionlens.settings.LensSettingsState import com.chylex.intellij.inspectionlens.settings.LensSettingsState
import com.intellij.codeInsight.daemon.impl.HighlightInfo
import com.intellij.codeInsight.daemon.impl.HintRenderer import com.intellij.codeInsight.daemon.impl.HintRenderer
import com.intellij.codeInsight.hints.presentation.InputHandler import com.intellij.codeInsight.hints.presentation.InputHandler
import com.intellij.openapi.editor.Editor import com.intellij.openapi.editor.Editor
@@ -27,14 +27,14 @@ import javax.swing.SwingUtilities
/** /**
* Renders the text of an inspection lens. * Renders the text of an inspection lens.
*/ */
class LensRenderer(private var inspection: Inspection, private val settings: LensSettingsState) : HintRenderer(null), InputHandler { class LensRenderer(private var info: HighlightInfo, private val settings: LensSettingsState) : HintRenderer(null), InputHandler {
private lateinit var inlay: Inlay<*> private lateinit var inlay: Inlay<*>
private lateinit var attributes: LensSeverityTextAttributes private lateinit var attributes: LensSeverityTextAttributes
private var extraRightPadding = 0 private var extraRightPadding = 0
private var hovered = false private var hovered = false
init { init {
setPropertiesFrom(inspection) setPropertiesFrom(info)
} }
fun setInlay(inlay: Inlay<*>) { fun setInlay(inlay: Inlay<*>) {
@@ -42,12 +42,12 @@ class LensRenderer(private var inspection: Inspection, private val settings: Len
this.inlay = inlay this.inlay = inlay
} }
fun setPropertiesFrom(inspection: Inspection) { fun setPropertiesFrom(info: HighlightInfo) {
this.inspection = inspection this.info = info
val description = getValidDescriptionText(inspection.info.description, settings.maxDescriptionLength) val description = getValidDescriptionText(info.description, settings.maxDescriptionLength)
text = description text = description
attributes = LensSeverity.from(inspection.info.severity).textAttributes attributes = LensSeverity.from(info.severity).textAttributes
extraRightPadding = if (description.lastOrNull() == '.') 2 else 0 extraRightPadding = if (description.lastOrNull() == '.') 2 else 0
} }
@@ -55,7 +55,7 @@ class LensRenderer(private var inspection: Inspection, private val settings: Len
fixBaselineForTextRendering(r) fixBaselineForTextRendering(r)
super.paint(inlay, g, r, textAttributes) super.paint(inlay, g, r, textAttributes)
if (hovered) { if (hovered && isHoveringText()) {
paintHoverEffect(inlay, g, r) paintHoverEffect(inlay, g, r)
} }
} }
@@ -85,7 +85,7 @@ class LensRenderer(private var inspection: Inspection, private val settings: Len
} }
override fun mouseMoved(event: MouseEvent, translated: Point) { override fun mouseMoved(event: MouseEvent, translated: Point) {
setHovered(isHoveringText()) setHovered(isHoveringText(translated))
} }
override fun mouseExited() { override fun mouseExited() {
@@ -114,7 +114,7 @@ class LensRenderer(private var inspection: Inspection, private val settings: Len
override fun mousePressed(event: MouseEvent, translated: Point) { override fun mousePressed(event: MouseEvent, translated: Point) {
val hoverMode = settings.lensHoverMode val hoverMode = settings.lensHoverMode
if (hoverMode == LensHoverMode.DISABLED || !isHoveringText()) { if (hoverMode == LensHoverMode.DISABLED || !isHoveringText(translated)) {
return return
} }
@@ -122,21 +122,25 @@ class LensRenderer(private var inspection: Inspection, private val settings: Len
event.consume() event.consume()
val editor = inlay.editor val editor = inlay.editor
moveToOffset(editor, inspection.info.actualStartOffset) moveToOffset(editor, info.actualStartOffset)
if ((event.button == MouseEvent.BUTTON1) xor (hoverMode != LensHoverMode.DEFAULT)) { if ((event.button == MouseEvent.BUTTON1) xor (hoverMode != LensHoverMode.DEFAULT)) {
IntentionsPopup.show(inspection, inlay) IntentionsPopup.show(info, inlay)
} }
} }
} }
private fun isHoveringText(): Boolean { private fun isHoveringText(): Boolean {
val bounds = inlay.bounds ?: return false val bounds = inlay.bounds ?: return false
val point = MouseInfo.getPointerInfo().location.apply { val translatedPoint = MouseInfo.getPointerInfo().location.apply {
SwingUtilities.convertPointFromScreen(this, inlay.editor.contentComponent) SwingUtilities.convertPointFromScreen(this, inlay.editor.contentComponent)
translate(-bounds.x, -bounds.y) translate(-bounds.x, -bounds.y)
} }
return isHoveringText(translatedPoint)
}
private fun isHoveringText(point: Point): Boolean {
return point.x >= HOVER_HORIZONTAL_PADDING return point.x >= HOVER_HORIZONTAL_PADDING
&& point.y >= 4 && point.y >= 4
&& point.x < inlay.widthInPixels - HOVER_HORIZONTAL_PADDING - extraRightPadding && point.x < inlay.widthInPixels - HOVER_HORIZONTAL_PADDING - extraRightPadding
@@ -19,8 +19,9 @@ class LensSeverityFilter(private val hiddenSeverityIds: Set<String>, private val
} }
companion object { companion object {
@Suppress("DEPRECATION")
private fun isSupported(severity: HighlightSeverity): Boolean { private fun isSupported(severity: HighlightSeverity): Boolean {
return severity > HighlightSeverity.TEXT_ATTRIBUTES return severity > HighlightSeverity.TEXT_ATTRIBUTES && severity !== HighlightSeverity.INFO
} }
fun getSupportedSeverities(registrar: SeverityRegistrar = SeverityRegistrar.getSeverityRegistrar(null)): List<HighlightSeverity> { fun getSupportedSeverities(registrar: SeverityRegistrar = SeverityRegistrar.getSeverityRegistrar(null)): List<HighlightSeverity> {
@@ -14,6 +14,7 @@ import com.intellij.openapi.ui.MessageDialogBuilder
import com.intellij.openapi.util.Disposer import com.intellij.openapi.util.Disposer
import com.intellij.ui.DisabledTraversalPolicy import com.intellij.ui.DisabledTraversalPolicy
import com.intellij.ui.EditorTextFieldCellRenderer.SimpleRendererComponent import com.intellij.ui.EditorTextFieldCellRenderer.SimpleRendererComponent
import com.intellij.ui.SimpleListCellRenderer
import com.intellij.ui.components.JBCheckBox import com.intellij.ui.components.JBCheckBox
import com.intellij.ui.dsl.builder.Cell import com.intellij.ui.dsl.builder.Cell
import com.intellij.ui.dsl.builder.RightGap import com.intellij.ui.dsl.builder.RightGap
@@ -23,7 +24,6 @@ import com.intellij.ui.dsl.builder.bindIntText
import com.intellij.ui.dsl.builder.bindItem import com.intellij.ui.dsl.builder.bindItem
import com.intellij.ui.dsl.builder.bindSelected import com.intellij.ui.dsl.builder.bindSelected
import com.intellij.ui.dsl.builder.panel import com.intellij.ui.dsl.builder.panel
import com.intellij.ui.dsl.listCellRenderer.textListCellRenderer
import java.awt.Cursor import java.awt.Cursor
class LensApplicationConfigurable : BoundConfigurable("Inspection Lens"), ConfigurableWithId { class LensApplicationConfigurable : BoundConfigurable("Inspection Lens"), ConfigurableWithId {
@@ -92,7 +92,7 @@ class LensApplicationConfigurable : BoundConfigurable("Inspection Lens"), Config
group("Behavior") { group("Behavior") {
row("Hover mode:") { row("Hover mode:") {
val items = LensHoverMode.entries val items = LensHoverMode.entries
val renderer = textListCellRenderer<LensHoverMode?> { it?.description ?: "" } val renderer = SimpleListCellRenderer.create("", LensHoverMode::description)
comboBox(items, renderer).bindItem(settings::lensHoverMode) { settings.lensHoverMode = it ?: LensHoverMode.DEFAULT } comboBox(items, renderer).bindItem(settings::lensHoverMode) { settings.lensHoverMode = it ?: LensHoverMode.DEFAULT }
} }
} }
-6
View File
@@ -14,12 +14,6 @@
]]></description> ]]></description>
<change-notes><![CDATA[ <change-notes><![CDATA[
<b>Version 1.7.0</b>
<ul>
<li>Updated minimum version to IntelliJ 2025.3 to address API deprecations.</li>
<li>Improved reliability of only showing relevant quick fixes when clicking an inspection.</li>
<li>Fixed lenses not always rendering an underline when hovered.</li>
</ul>
<b>Version 1.6.1</b> <b>Version 1.6.1</b>
<ul> <ul>
<li>Fixed exception caused by accessing disposed editors.</li> <li>Fixed exception caused by accessing disposed editors.</li>
@@ -1,11 +0,0 @@
package com.chylex.intellij.inspectionlens.editor.lens
import org.junit.jupiter.api.Assertions.assertTrue
import org.junit.jupiter.api.Test
class IntentionsPopupTest {
@Test
fun hasShowPopupMethod() {
assertTrue(IntentionsPopup.hasShowPopupMethod)
}
}