13 Commits
27 changed files with 310 additions and 240 deletions
+18 -5
View File
@@ -1,12 +1,14 @@
@file:Suppress("ConvertLambdaToReference")
import org.jetbrains.intellij.platform.gradle.TestFrameworkType
plugins {
kotlin("jvm")
id("org.jetbrains.intellij.platform")
}
group = "com.chylex.intellij.inspectionlens"
version = "1.5.2.902"
version = "1.6.1"
repositories {
mavenCentral()
@@ -18,25 +20,36 @@ repositories {
dependencies {
intellijPlatform {
intellijIdeaUltimate("2025.1")
intellijIdeaUltimate("2025.3")
bundledPlugin("tanvd.grazi")
testFramework(TestFrameworkType.JUnit5)
}
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 {
pluginConfiguration {
ideaVersion {
sinceBuild.set("233.11361.10")
sinceBuild.set("253")
untilBuild.set(provider { null })
}
}
pluginVerification {
freeArgs.add("-mute")
freeArgs.add("TemplateWordInPluginId")
ides {
recommended()
}
}
}
kotlin {
jvmToolchain(17)
compilerOptions {
freeCompilerArgs = listOf(
"-X" + "jvm-default=all",
+1
View File
@@ -1 +1,2 @@
kotlin.stdlib.default.dependency=false
org.gradle.caching = true
Binary file not shown.
+3 -1
View File
@@ -1,7 +1,9 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.12-bin.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-9.7.1-bin.zip
networkTimeout=10000
retries=0
retryBackOffMs=500
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
Vendored
+6 -9
View File
@@ -1,7 +1,7 @@
#!/bin/sh
#
# Copyright © 2015-2021 the original authors.
# Copyright © 2015 the original authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
@@ -20,7 +20,7 @@
##############################################################################
#
# Gradle start up script for POSIX generated by Gradle.
# gradlew start up script for POSIX generated by Gradle.
#
# Important for running:
#
@@ -29,7 +29,7 @@
# bash, then to run this script, type that shell name before the whole
# command line, like:
#
# ksh Gradle
# ksh gradlew
#
# Busybox and similar reduced shells will NOT work, because this script
# requires all of these POSIX shell features:
@@ -57,7 +57,7 @@
# Darwin, MinGW, and NonStop.
#
# (3) This script is generated from the Groovy template
# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
# https://github.com/gradle/gradle/blob/3d91ce3b8caaf77ad09f381f43615b715b53f72c/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
# within the Gradle project.
#
# You can find Gradle at https://github.com/gradle/gradle/.
@@ -114,7 +114,6 @@ case "$( uname )" in #(
NONSTOP* ) nonstop=true ;;
esac
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
# Determine the Java command to use to start the JVM.
@@ -172,7 +171,6 @@ fi
# For Cygwin or MSYS, switch paths to Windows format before running java
if "$cygwin" || "$msys" ; then
APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
JAVACMD=$( cygpath --unix "$JAVACMD" )
@@ -205,15 +203,14 @@ fi
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Collect all arguments for the java command:
# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
# 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
# treated as '${Hostname}' itself on the command line.
set -- \
"-Dorg.gradle.appname=$APP_BASE_NAME" \
-classpath "$CLASSPATH" \
org.gradle.wrapper.GradleWrapperMain \
-jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \
"$@"
# Stop when "xargs" is not available.
Vendored
+12 -24
View File
@@ -19,12 +19,12 @@
@if "%DEBUG%"=="" @echo off
@rem ##########################################################################
@rem
@rem Gradle startup script for Windows
@rem gradlew startup script for Windows
@rem
@rem ##########################################################################
@rem Set local scope for the variables with windows NT shell
if "%OS%"=="Windows_NT" setlocal
@rem Set local scope for the variables, and ensure extensions are enabled
setlocal EnableExtensions
set DIRNAME=%~dp0
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 location of your Java installation. 1>&2
goto fail
"%COMSPEC%" /c exit 1
:findJavaFromJavaHome
set JAVA_HOME=%JAVA_HOME:"=%
@@ -65,30 +65,18 @@ echo. 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
goto fail
"%COMSPEC%" /c exit 1
:execute
@rem Setup the command line
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
@rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
@rem Execute gradlew
@rem endlocal doesn't take effect until after the line is parsed and variables are expanded
@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
:end
@rem End local scope for the variables with windows NT shell
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
:exitWithErrorLevel
@rem Use "%COMSPEC%" /c exit to allow operators to work properly in scripts
"%COMSPEC%" /c exit %ERRORLEVEL%
+2 -2
View File
@@ -2,7 +2,7 @@ rootProject.name = "InspectionLens"
pluginManagement {
plugins {
kotlin("jvm") version "2.1.0"
id("org.jetbrains.intellij.platform") version "2.2.1"
kotlin("jvm") version "2.4.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
}
}
@@ -2,11 +2,11 @@ package com.chylex.intellij.inspectionlens
import com.chylex.intellij.inspectionlens.editor.EditorLensFeatures
import com.intellij.openapi.application.ApplicationManager
import com.intellij.openapi.components.service
import com.intellij.openapi.diagnostic.logger
import com.intellij.openapi.fileEditor.FileEditorManager
import com.intellij.openapi.fileEditor.TextEditor
import com.intellij.openapi.project.ProjectManager
import java.util.concurrent.atomic.AtomicBoolean
/**
* Handles installation and uninstallation of plugin features in editors.
@@ -16,27 +16,31 @@ internal object InspectionLens {
val LOG = logger<InspectionLens>()
var SHOW_LENSES = true
set(value) {
field = value
scheduleRefresh()
}
/**
* Installs lenses into [editor].
*/
fun install(editor: TextEditor) {
EditorLensFeatures.install(editor.editor, service<InspectionLensPluginDisposableService>().intersect(editor))
EditorLensFeatures.install(editor)
}
/**
* Installs lenses into all open editors.
*/
fun install() {
forEachOpenEditor(::install)
forEachOpenEditor(EditorLensFeatures::install)
}
/**
* Refreshes lenses in all open editors.
*/
private fun refresh() {
forEachOpenEditor {
EditorLensFeatures.refresh(it.editor)
}
forEachOpenEditor(EditorLensFeatures::refresh)
}
/**
@@ -52,31 +56,31 @@ internal object InspectionLens {
private inline fun forEachOpenEditor(action: (TextEditor) -> Unit) {
val projectManager = ProjectManager.getInstanceIfCreated() ?: return
for (project in projectManager.openProjects.filterNot { it.isDisposed }) {
for (editor in FileEditorManager.getInstance(project).allEditors.filterIsInstance<TextEditor>()) {
action(editor)
for (project in projectManager.openProjects) {
if (project.isDisposed) {
continue
}
for (editor in FileEditorManager.getInstance(project).allEditors) {
if (editor is TextEditor) {
action(editor)
}
}
}
}
private object Refresh {
private var needsRefresh = false
private object Refresh : Runnable {
private val needsRefresh = AtomicBoolean(false)
fun schedule() {
synchronized(this) {
if (!needsRefresh) {
needsRefresh = true
ApplicationManager.getApplication().invokeLater(this::run)
}
if (needsRefresh.compareAndSet(false, true)) {
ApplicationManager.getApplication().invokeLater(this)
}
}
private fun run() {
synchronized(this) {
if (needsRefresh) {
needsRefresh = false
refresh()
}
override fun run() {
if (needsRefresh.compareAndSet(true, false)) {
refresh()
}
}
}
@@ -0,0 +1,22 @@
package com.chylex.intellij.inspectionlens
import com.intellij.openapi.fileEditor.FileEditorManager
import com.intellij.openapi.fileEditor.FileEditorManagerListener
import com.intellij.openapi.fileEditor.FileOpenedSyncListener
import com.intellij.openapi.fileEditor.TextEditor
import com.intellij.openapi.vfs.VirtualFile
/**
* Installs [InspectionLens] in opened editors.
*
* Used instead of [FileOpenedSyncListener], because [FileOpenedSyncListener] randomly fails to register during IDE startup.
*/
class InspectionLensFileEditorManagerListener : FileEditorManagerListener {
override fun fileOpened(source: FileEditorManager, file: VirtualFile) {
for (editor in source.getEditors(file)) {
if (editor is TextEditor) {
InspectionLens.install(editor)
}
}
}
}
@@ -1,23 +0,0 @@
package com.chylex.intellij.inspectionlens
import com.intellij.openapi.fileEditor.FileEditorManager
import com.intellij.openapi.fileEditor.FileOpenedSyncListener
import com.intellij.openapi.fileEditor.TextEditor
import com.intellij.openapi.fileEditor.ex.FileEditorWithProvider
import com.intellij.openapi.vfs.VirtualFile
/**
* Installs [InspectionLens] in newly opened editors.
*/
class InspectionLensFileOpenedListener : FileOpenedSyncListener {
override fun fileOpenedSync(source: FileEditorManager, file: VirtualFile, editorsWithProviders: List<FileEditorWithProvider>) {
InspectionLens.LOG.info("File opened: $file (editor count: ${editorsWithProviders.size})")
for (editorWrapper in editorsWithProviders) {
val fileEditor = editorWrapper.fileEditor
if (fileEditor is TextEditor) {
InspectionLens.install(fileEditor)
}
}
}
}
@@ -0,0 +1,25 @@
package com.chylex.intellij.inspectionlens.actions
import com.chylex.intellij.inspectionlens.InspectionLens
import com.intellij.openapi.actionSystem.ActionUpdateThread
import com.intellij.openapi.actionSystem.AnActionEvent
import com.intellij.openapi.actionSystem.KeepPopupOnPerform
import com.intellij.openapi.project.DumbAwareToggleAction
class ToggleLensVisibilityAction : DumbAwareToggleAction() {
init {
templatePresentation.keepPopupOnPerform = KeepPopupOnPerform.IfRequested
}
override fun getActionUpdateThread(): ActionUpdateThread {
return ActionUpdateThread.BGT
}
override fun isSelected(e: AnActionEvent): Boolean {
return InspectionLens.SHOW_LENSES
}
override fun setSelected(e: AnActionEvent, state: Boolean) {
InspectionLens.SHOW_LENSES = state
}
}
@@ -8,8 +8,6 @@ import com.intellij.openapi.startup.ProjectActivity
class GrazieSupport : ProjectActivity {
override suspend fun execute(project: Project) {
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)
}
}
@@ -1,9 +0,0 @@
package com.chylex.intellij.inspectionlens.debug
import com.intellij.codeInsight.daemon.impl.HighlightInfo
import com.intellij.lang.annotation.HighlightSeverity
import com.intellij.openapi.editor.markup.RangeHighlighter
data class Highlighter(val hashCode: Int, val layer: Int, val severity: HighlightSeverity, val description: String) {
constructor(highlighter: RangeHighlighter, info: HighlightInfo) : this(System.identityHashCode(highlighter), highlighter.layer, info.severity, info.description)
}
@@ -1,5 +0,0 @@
package com.chylex.intellij.inspectionlens.debug
import java.time.Instant
data class LensEvent(val time: Instant, val data: LensEventData)
@@ -1,7 +0,0 @@
package com.chylex.intellij.inspectionlens.debug
sealed interface LensEventData {
data class MarkupModelAfterAdded(val lens: Highlighter) : LensEventData
data class MarkupModelAttributesChanged(val lens: Highlighter) : LensEventData
data class MarkupModelBeforeRemoved(val lens: Highlighter) : LensEventData
}
@@ -1,14 +0,0 @@
package com.chylex.intellij.inspectionlens.debug
import com.intellij.openapi.editor.Editor
import java.time.Instant
object LensEventManager {
val fileNameToEventsMap = mutableMapOf<String, MutableList<LensEvent>>()
@Synchronized
fun addEvent(editor: Editor, event: LensEventData) {
val path = editor.virtualFile?.path ?: return
fileNameToEventsMap.getOrPut(path, ::mutableListOf).add(LensEvent(Instant.now(), event))
}
}
@@ -1,11 +1,13 @@
package com.chylex.intellij.inspectionlens.editor
import com.chylex.intellij.inspectionlens.InspectionLens
import com.chylex.intellij.inspectionlens.InspectionLensPluginDisposableService
import com.intellij.openapi.Disposable
import com.intellij.openapi.components.service
import com.intellij.openapi.editor.Editor
import com.intellij.openapi.editor.ex.FoldingModelEx
import com.intellij.openapi.editor.ex.MarkupModelEx
import com.intellij.openapi.editor.impl.DocumentMarkupModel
import com.intellij.openapi.fileEditor.TextEditor
import com.intellij.openapi.util.Disposer
import com.intellij.openapi.util.Key
@@ -20,7 +22,7 @@ internal class EditorLensFeatures private constructor(
) {
private val lensManager = EditorLensManager(editor)
private val lensManagerDispatcher = EditorLensManagerDispatcher(lensManager)
private val markupModelListener = LensMarkupModelListener(editor, lensManagerDispatcher)
private val markupModelListener = LensMarkupModelListener(lensManagerDispatcher)
init {
markupModel.addMarkupModelListener(disposable, markupModelListener)
@@ -37,30 +39,24 @@ internal class EditorLensFeatures private constructor(
companion object {
private val EDITOR_KEY = Key<EditorLensFeatures>(EditorLensFeatures::class.java.name)
fun install(editor: Editor, disposable: Disposable) {
fun install(owner: TextEditor) {
val editor = owner.editor
if (editor.getUserData(EDITOR_KEY) != null) {
InspectionLens.LOG.info("Skipped installation to: $editor")
return
}
InspectionLens.LOG.info("Installing to: $editor")
val markupModel = DocumentMarkupModel.forDocument(editor.document, editor.project, false) as? MarkupModelEx ?: return
val foldingModel = editor.foldingModel as? FoldingModelEx
val disposable = service<InspectionLensPluginDisposableService>().intersect(owner)
val features = EditorLensFeatures(editor, markupModel, foldingModel, disposable)
editor.putUserData(EDITOR_KEY, features)
Disposer.register(disposable) {
InspectionLens.LOG.info("Installation disposed: $editor", Exception("DISPOSE STACK TRACE"))
editor.putUserData(EDITOR_KEY, null)
}
Disposer.register(disposable) { editor.putUserData(EDITOR_KEY, null) }
}
fun refresh(editor: Editor) {
val userData = editor.getUserData(EDITOR_KEY)
InspectionLens.LOG.info("Refreshing: $editor ($userData)")
userData?.refresh()
fun refresh(owner: TextEditor) {
owner.editor.getUserData(EDITOR_KEY)?.refresh()
}
}
}
@@ -1,9 +1,7 @@
package com.chylex.intellij.inspectionlens.editor
import com.chylex.intellij.inspectionlens.debug.Highlighter
import com.chylex.intellij.inspectionlens.editor.lens.EditorLens
import com.chylex.intellij.inspectionlens.settings.LensSettingsState
import com.intellij.codeInsight.daemon.impl.HighlightInfo
import com.intellij.openapi.components.service
import com.intellij.openapi.editor.Editor
import com.intellij.openapi.editor.markup.RangeHighlighter
@@ -16,12 +14,12 @@ internal class EditorLensManager(private val editor: Editor) {
private val lenses = IdentityHashMap<RangeHighlighter, EditorLens>()
private val settings = service<LensSettingsState>()
private val highlighters
get() = lenses.keys.map { Highlighter(it, HighlightInfo.fromRangeHighlighter(it)!!) }
private fun show(highlighterWithInfo: HighlighterWithInfo) {
val (highlighter, info) = highlighterWithInfo
if (editor.isDisposed) {
return
}
val (highlighter, info) = highlighterWithInfo
if (!highlighter.isValid) {
return
}
@@ -90,7 +88,6 @@ internal class EditorLensManager(private val editor: Editor) {
/**
* Batch mode affects both inlays and highlighters used for line colors.
*/
@Suppress("ConvertLambdaToReference")
private inline fun executeInBatchMode(operations: Int, crossinline action: () -> Unit) {
if (operations > 1000) {
editor.inlayModel.execute(true) { action() }
@@ -1,13 +1,10 @@
package com.chylex.intellij.inspectionlens.editor
import com.chylex.intellij.inspectionlens.InspectionLens
import com.chylex.intellij.inspectionlens.debug.Highlighter
import com.chylex.intellij.inspectionlens.debug.LensEventData
import com.chylex.intellij.inspectionlens.debug.LensEventManager
import com.chylex.intellij.inspectionlens.settings.LensSettingsState
import com.intellij.codeInsight.daemon.impl.HighlightInfo
import com.intellij.codeInsight.daemon.impl.UpdateHighlightersUtil.isFileLevelOrGutterAnnotation
import com.intellij.openapi.components.service
import com.intellij.openapi.editor.Editor
import com.intellij.openapi.editor.ex.RangeHighlighterEx
import com.intellij.openapi.editor.impl.event.MarkupModelListener
import com.intellij.openapi.editor.markup.RangeHighlighter
@@ -15,70 +12,41 @@ import com.intellij.openapi.editor.markup.RangeHighlighter
/**
* Listens for inspection highlights and reports them to [EditorLensManager].
*/
internal class LensMarkupModelListener(private val editor: Editor, private val lensManagerDispatcher: EditorLensManagerDispatcher) : MarkupModelListener {
internal class LensMarkupModelListener(private val lensManagerDispatcher: EditorLensManagerDispatcher) : MarkupModelListener {
private val settings = service<LensSettingsState>()
override fun afterAdded(highlighter: RangeHighlighterEx) {
try {
getFilteredHighlightInfo(highlighter)?.let { LensEventManager.addEvent(editor, LensEventData.MarkupModelAfterAdded(Highlighter(highlighter, it))) }
showIfValid(highlighter)
} catch (e: Exception) {
InspectionLens.LOG.error("Error showing inspection", e)
}
showIfValid(highlighter)
}
override fun attributesChanged(highlighter: RangeHighlighterEx, renderersChanged: Boolean, fontStyleOrColorChanged: Boolean) {
try {
getFilteredHighlightInfo(highlighter)?.let { LensEventManager.addEvent(editor, LensEventData.MarkupModelAttributesChanged(Highlighter(highlighter, it))) }
showIfValid(highlighter)
} catch (e: Exception) {
InspectionLens.LOG.error("Error updating inspection", e)
}
showIfValid(highlighter)
}
override fun beforeRemoved(highlighter: RangeHighlighterEx) {
try {
val filteredHighlightInfo = getFilteredHighlightInfo(highlighter)
if (filteredHighlightInfo != null) {
LensEventManager.addEvent(editor, LensEventData.MarkupModelBeforeRemoved(Highlighter(highlighter, filteredHighlightInfo)))
lensManagerDispatcher.hide(highlighter)
}
} catch (e: Exception) {
InspectionLens.LOG.error("Error hiding inspection", e)
fun showAllValid(highlighters: Array<RangeHighlighter>) {
if (InspectionLens.SHOW_LENSES) {
highlighters.forEach(::showIfValid)
}
}
private fun showIfValid(highlighter: RangeHighlighter) {
runWithHighlighterIfValid(highlighter, lensManagerDispatcher::show, ::showAsynchronously)
}
private fun showAsynchronously(highlighterWithInfo: HighlighterWithInfo.Async) {
highlighterWithInfo.requestDescription {
if (highlighterWithInfo.highlighter.isValid && highlighterWithInfo.hasDescription) {
lensManagerDispatcher.show(highlighterWithInfo)
}
if (!InspectionLens.SHOW_LENSES) {
return
}
}
fun showAllValid(highlighters: Array<RangeHighlighter>) {
highlighters.forEach(::showIfValid)
}
fun hideAll() {
lensManagerDispatcher.hideAll()
val info = highlighter.takeIf { it.isValid }?.let(::getFilteredHighlightInfo)
if (info == null || isFileLevelOrGutterAnnotation(info)) {
return
}
val highlighterWithInfo = HighlighterWithInfo.from(highlighter, info)
processHighlighterWithInfo(highlighterWithInfo, lensManagerDispatcher::show, ::showAsynchronously)
}
private fun getFilteredHighlightInfo(highlighter: RangeHighlighter): HighlightInfo? {
return HighlightInfo.fromRangeHighlighter(highlighter)?.takeIf { settings.severityFilter.test(it.severity) }
}
private inline fun runWithHighlighterIfValid(highlighter: RangeHighlighter, actionForImmediate: (HighlighterWithInfo) -> Unit, actionForAsync: (HighlighterWithInfo.Async) -> Unit) {
val info = highlighter.takeIf { it.isValid }?.let(::getFilteredHighlightInfo)
if (info != null) {
processHighlighterWithInfo(HighlighterWithInfo.from(highlighter, info), actionForImmediate, actionForAsync)
}
}
private inline fun processHighlighterWithInfo(highlighterWithInfo: HighlighterWithInfo, actionForImmediate: (HighlighterWithInfo) -> Unit, actionForAsync: (HighlighterWithInfo.Async) -> Unit) {
if (highlighterWithInfo is HighlighterWithInfo.Async) {
actionForAsync(highlighterWithInfo)
@@ -87,4 +55,22 @@ internal class LensMarkupModelListener(private val editor: Editor, private val l
actionForImmediate(highlighterWithInfo)
}
}
private fun showAsynchronously(highlighterWithInfo: HighlighterWithInfo.Async) {
highlighterWithInfo.requestDescription {
if (highlighterWithInfo.highlighter.isValid && highlighterWithInfo.hasDescription) {
lensManagerDispatcher.show(highlighterWithInfo)
}
}
}
override fun beforeRemoved(highlighter: RangeHighlighterEx) {
if (getFilteredHighlightInfo(highlighter) != null) {
lensManagerDispatcher.hide(highlighter)
}
}
fun hideAll() {
lensManagerDispatcher.hideAll()
}
}
@@ -32,10 +32,6 @@ internal class EditorLens private constructor(private var inlay: EditorLensInlay
lineBackground.hide(inlay.editor)
}
override fun toString(): String {
return "$inlay"
}
companion object {
fun show(editor: Editor, info: HighlightInfo, settings: LensSettingsState): EditorLens? {
val inlay = EditorLensInlay.show(editor, info, settings) ?: return null
@@ -14,6 +14,10 @@ internal value class EditorLensLineBackground(private val highlighter: RangeHigh
get() = !highlighter.isValid
fun onFoldRegionsChanged(editor: Editor, severity: LensSeverity) {
if (isInvalid) {
return
}
if (highlighter is RangeHighlighterEx) {
highlighter.textAttributes = getAttributes(editor, highlighter.startOffset, highlighter.endOffset, severity)
}
@@ -1,58 +1,136 @@
package com.chylex.intellij.inspectionlens.editor.lens
import com.chylex.intellij.inspectionlens.InspectionLens
import com.intellij.codeInsight.daemon.impl.HighlightInfo
import com.intellij.codeInsight.daemon.impl.HighlightInfo.IntentionActionDescriptor
import com.intellij.codeInsight.daemon.impl.IntentionsUI
import com.intellij.codeInsight.daemon.impl.ShowIntentionsPass.IntentionsInfo
import com.intellij.codeInsight.hint.HintManager
import com.intellij.codeInsight.intention.impl.CachedIntentions
import com.intellij.codeInsight.intention.impl.IntentionHintComponent
import com.intellij.codeInsight.intention.impl.ShowIntentionActionsHandler
import com.intellij.lang.LangBundle
import com.intellij.ide.DataManager
import com.intellij.lang.annotation.HighlightSeverity
import com.intellij.openapi.actionSystem.ActionManager
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.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.Inlay
import com.intellij.openapi.project.DumbService
import com.intellij.openapi.project.Project
import com.intellij.psi.PsiDocumentManager
import com.intellij.psi.PsiFile
import com.intellij.psi.util.PsiUtilBase
import com.intellij.ui.awt.RelativePoint
import com.intellij.util.concurrency.AppExecutorUtil
import java.lang.reflect.Method
internal object IntentionsPopup {
fun show(editor: Editor) {
if (!tryShow(editor)) {
HintManager.getInstance().showInformationHint(editor, LangBundle.message("hint.text.no.context.actions.available.at.this.location"))
private const val INTENTION_SOURCE_CLASS_NAME = "com.intellij.codeInsight.intention.IntentionSource"
private val showPopupMethod: ShowPopupMethod? = try {
val method = IntentionHintComponent::class.java.declaredMethods.first { method ->
val parameterTypes = method.parameterTypes
method.name == "showPopup" &&
parameterTypes.size in 1..2 &&
parameterTypes[0] === RelativePoint::class.java &&
parameterTypes.getOrNull(1).let { p -> p == null || p.name == INTENTION_SOURCE_CLASS_NAME }
}
method.isAccessible = true
@Suppress("UNCHECKED_CAST")
val args: Array<Any?> = if (method.parameterCount == 1)
arrayOf(null)
else
arrayOf(null, (Class.forName(INTENTION_SOURCE_CLASS_NAME) as Class<Enum<*>>).enumConstants.first { it.name == "OTHER" })
ShowPopupMethod(method, args)
} catch (t: Throwable) {
InspectionLens.LOG.warn("Could not initialize intention popup", t)
null
}
private class ShowPopupMethod(private val method: Method, private val args: Array<Any?>) {
operator fun invoke(component: IntentionHintComponent) {
method.invoke(component, *args)
}
}
private fun tryShow(editor: Editor): Boolean {
// If the IDE uses the default Show Intentions action and handler,
// use the handler directly to bypass additional logic from the action.
val action = ActionManager.getInstance().getAction(IdeActions.ACTION_SHOW_INTENTION_ACTIONS)
if (action.javaClass.name === DEFAULT_ACTION_CLASS) {
return tryShowWithDefaultHandler(editor)
}
else {
ActionUtil.invokeAction(action, editor.component, ActionPlaces.EDITOR_INLAY, null, null)
return true
fun show(highlightInfo: HighlightInfo, inlay: Inlay<*>) {
if (!tryShow(highlightInfo, inlay)) {
showNoActionsAvailable(inlay.editor)
}
}
private fun tryShowWithDefaultHandler(editor: Editor): Boolean {
private fun tryShow(highlightInfo: HighlightInfo, inlay: Inlay<*>): Boolean {
val editor = inlay.editor
val project = editor.project ?: return false
val file = PsiUtilBase.getPsiFileInEditor(editor, project) ?: return false
PsiDocumentManager.getInstance(project).commitAllDocuments()
IntentionsUI.getInstance(project).hide()
HANDLER.showIntentionHint(project, editor, file, showFeedbackOnEmptyMenu = true)
ReadAction
.nonBlocking<IntentionsInfo> { collectIntentions(editor, project, file, highlightInfo, inlay.offset) }
.finishOnUiThread(ModalityState.current()) { tryShowPopup(project, file, editor, it) }
.submit(AppExecutorUtil.getAppExecutorService())
return true
}
/**
* New IDEA versions mark this class as internal, so the plugin verifier flags references to it as errors.
*/
const val DEFAULT_ACTION_CLASS = "com.intellij.codeInsight.intention.actions.ShowIntentionActionsAction"
private val HANDLER = object : ShowIntentionActionsHandler() {
public override fun showIntentionHint(project: Project, editor: Editor, file: PsiFile, showFeedbackOnEmptyMenu: Boolean) {
super.showIntentionHint(project, editor, file, showFeedbackOnEmptyMenu)
private fun collectIntentions(editor: Editor, project: Project, file: PsiFile, info: HighlightInfo, offset: Int): IntentionsInfo {
val intentions = mutableListOf<IntentionActionDescriptor>()
info.findRegisteredQuickFix { descriptor, _ ->
if (DumbService.getInstance(project).isUsableInCurrentContext(descriptor.action) && ShowIntentionActionsHandler.availableFor(file, editor, offset, descriptor.action)) {
intentions.add(descriptor)
}
null
}
return IntentionsInfo().also {
it.offset = offset
if (info.severity === HighlightSeverity.ERROR) {
it.errorFixesToShow.addAll(intentions)
}
else {
it.inspectionFixesToShow.addAll(intentions)
}
}
}
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) {
val showIntentionsAction = ActionManager.getInstance().getAction(IdeActions.ACTION_SHOW_INTENTION_ACTIONS)
val dataContext = DataManager.getInstance().getDataContext(editor.component)
val event = AnActionEvent.createEvent(showIntentionsAction, dataContext, null, ActionPlaces.EDITOR_INLAY, ActionUiKind.NONE, null)
ActionUtil.performAction(showIntentionsAction, event)
}
else {
val cachedIntentions = CachedIntentions.create(project, file, editor, intentions)
val hintComponent = IntentionHintComponent.showIntentionHint(project, file, editor, false, cachedIntentions)
showPopupMethod.invoke(hintComponent)
}
}
private fun showNoActionsAvailable(editor: Editor) {
HintManager.getInstance().showInformationHint(editor, "No context actions available at this location")
}
}
@@ -125,7 +125,7 @@ class LensRenderer(private var info: HighlightInfo, private val settings: LensSe
moveToOffset(editor, info.actualStartOffset)
if ((event.button == MouseEvent.BUTTON1) xor (hoverMode != LensHoverMode.DEFAULT)) {
IntentionsPopup.show(editor)
IntentionsPopup.show(info, inlay)
}
}
}
@@ -19,9 +19,8 @@ class LensSeverityFilter(private val hiddenSeverityIds: Set<String>, private val
}
companion object {
@Suppress("DEPRECATION")
private fun isSupported(severity: HighlightSeverity): Boolean {
return severity > HighlightSeverity.TEXT_ATTRIBUTES && severity !== HighlightSeverity.INFO
return severity > HighlightSeverity.TEXT_ATTRIBUTES
}
fun getSupportedSeverities(registrar: SeverityRegistrar = SeverityRegistrar.getSeverityRegistrar(null)): List<HighlightSeverity> {
@@ -14,7 +14,6 @@ import com.intellij.openapi.ui.MessageDialogBuilder
import com.intellij.openapi.util.Disposer
import com.intellij.ui.DisabledTraversalPolicy
import com.intellij.ui.EditorTextFieldCellRenderer.SimpleRendererComponent
import com.intellij.ui.SimpleListCellRenderer
import com.intellij.ui.components.JBCheckBox
import com.intellij.ui.dsl.builder.Cell
import com.intellij.ui.dsl.builder.RightGap
@@ -24,6 +23,7 @@ import com.intellij.ui.dsl.builder.bindIntText
import com.intellij.ui.dsl.builder.bindItem
import com.intellij.ui.dsl.builder.bindSelected
import com.intellij.ui.dsl.builder.panel
import com.intellij.ui.dsl.listCellRenderer.textListCellRenderer
import java.awt.Cursor
class LensApplicationConfigurable : BoundConfigurable("Inspection Lens"), ConfigurableWithId {
@@ -92,7 +92,7 @@ class LensApplicationConfigurable : BoundConfigurable("Inspection Lens"), Config
group("Behavior") {
row("Hover mode:") {
val items = LensHoverMode.entries
val renderer = SimpleListCellRenderer.create("", LensHoverMode::description)
val renderer = textListCellRenderer<LensHoverMode?> { it?.description ?: "" }
comboBox(items, renderer).bindItem(settings::lensHoverMode) { settings.lensHoverMode = it ?: LensHoverMode.DEFAULT }
}
}
+35 -1
View File
@@ -14,17 +14,34 @@
]]></description>
<change-notes><![CDATA[
<b>Version 1.6.1</b>
<ul>
<li>Fixed exception caused by accessing disposed editors.</li>
<li>Fixed exception caused by accessing disposed line background highlighters.</li>
</ul>
<br>
<b>Version 1.6.0</b>
<ul>
<li>Added action to <b>View | Show Inspection Lenses</b> that temporarily toggles visibility of all inspections.</li>
<li>File-wide inspections no longer appear.</li>
<li>Fixed quick fix popup disappearing when the floating toolbar is enabled.</li>
<li>Clicking an inspection now only shows relevant quick fixes (not supported for ReSharper-based languages, which use a non-standard popup).</li>
<li>Tried to work around an issue where the IDE randomly fails to load the plugin.</li>
</ul>
<br>
<b>Version 1.5.2</b>
<ul>
<li>Added option to change maximum description length.</li>
<li>Added button to <b>Settings | Tools | Inspection Lens</b> that resets all settings to default.</li>
</ul>
<br>
<b>Version 1.5.1</b>
<ul>
<li>Added option to change the behavior of clicking on inspections.</li>
<li>Fixed broken quick fixes in Rider and CLion Nova.</li>
<li>Fixed hover underline not rendering correctly with some combinations of high DPI and line height settings.</li>
</ul>
<br>
<b>Version 1.5</b>
<ul>
<li>Added possibility to left-click an inspection to show quick fixes.</li>
@@ -34,51 +51,62 @@
<li>Improved descriptions of Kotlin compiler inspections.</li>
<li>Fixed visual artifacts in Rendered Doc comments.</li>
</ul>
<br>
<b>Version 1.4.1</b>
<ul>
<li>Fixed warnings in usage of IntelliJ SDK.</li>
</ul>
<br>
<b>Version 1.4</b>
<ul>
<li>Added configuration of visible severities to <b>Settings | Tools | Inspection Lens</b>.</li>
</ul>
<br>
<b>Version 1.3.3</b>
<ul>
<li>Partially reverted fix for inspections that include HTML in their description due to breaking inspections with angled brackets.</li>
<li>Fixed plugin not working when installed on JetBrains Gateway Client.</li>
</ul>
<br>
<b>Version 1.3.2</b>
<ul>
<li>Fixed inspections randomly not disappearing.</li>
</ul>
<br>
<b>Version 1.3.1</b>
<ul>
<li>Updated minimum version to IntelliJ 2023.3 due to breaking API changes.</li>
</ul>
<br>
<b>Version 1.3.0</b>
<ul>
<li>Added background colors to lines containing inspections. (<a href="https://github.com/chylex/IntelliJ-Inspection-Lens/pull/15">PR #15</a> by <a href="https://github.com/synopss">synopss</a>)</li>
</ul>
<br>
<b>Version 1.2.0</b>
<ul>
<li>Support for IntelliJ 2023.2 EAP.</li>
<li>Added distinct colors for typos and Grazie inspections.</li>
</ul>
<br>
<b>Version 1.1.2</b>
<ul>
<li>Added plugin icon.</li>
<li>Updated minimum version to IntelliJ 2023.1 due to deprecated APIs.</li>
</ul>
<br>
<b>Version 1.1.1</b>
<ul>
<li>Multiple inspections at the same place in the document are now ordered by severity.</li>
<li>Improved performance of processing inspections which do not contain HTML.</li>
</ul>
<br>
<b>Version 1.1.0</b>
<ul>
<li>Fixed showing inspections that include HTML in their description. (<a href="https://github.com/chylex/IntelliJ-Inspection-Lens/pull/3">PR #3</a> by <a href="https://github.com/KostkaBrukowa">KostkaBrukowa</a>)</li>
<li>Fixed exception when asynchronous inspections run on a non-EDT thread.</li>
</ul>
<br>
<b>Version 1.0.0</b>
<ul>
<li>Initial version with support for IntelliJ 2022.2 and newer.</li>
@@ -96,11 +124,17 @@
parentId="tools" />
</extensions>
<actions>
<action id="chylex.InspectionLens.ToggleLensVisibility" class="com.chylex.intellij.inspectionlens.actions.ToggleLensVisibilityAction" text="Show Inspection Lenses">
<add-to-group group-id="ViewMenu" anchor="after" relative-to-action="EditorResetFontSizeGlobal" />
</action>
</actions>
<applicationListeners>
<listener class="com.chylex.intellij.inspectionlens.InspectionLensPluginListener" topic="com.intellij.ide.plugins.DynamicPluginListener" />
</applicationListeners>
<projectListeners>
<listener class="com.chylex.intellij.inspectionlens.InspectionLensFileOpenedListener" topic="com.intellij.openapi.fileEditor.FileOpenedSyncListener" />
<listener class="com.chylex.intellij.inspectionlens.InspectionLensFileEditorManagerListener" topic="com.intellij.openapi.fileEditor.FileEditorManagerListener" />
</projectListeners>
</idea-plugin>
@@ -1,12 +0,0 @@
package com.chylex.intellij.inspectionlens.editor.lens
import com.intellij.codeInsight.intention.actions.ShowIntentionActionsAction
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Test
class IntentionsPopupTest {
@Test
fun showIntentionActionsActionClassHasNotChanged() {
assertEquals(IntentionsPopup.DEFAULT_ACTION_CLASS, ShowIntentionActionsAction::class.java.name)
}
}