29 Commits
Author SHA1 Message Date
chylex 369f2138bc Release 1.7.0 2026-09-13 21:32:08 +02:00
chylex b953a49ab1 Exclude some folders from IDEA project 2026-09-13 21:32:08 +02:00
chylex d59eddf21f Rework intentions popup to improve reliability 2026-09-13 21:32:08 +02:00
chylex 7dd4184957 Fix lenses not always rendering underline when hovered 2026-09-13 14:50:19 +02:00
chylex 145b7edf6b Fix plugin verifier issues 2026-09-13 14:28:05 +02:00
chylex ac5374372a Fix unit tests 2026-09-13 14:28:05 +02:00
chylex b13df2468e Update minimum IDEA version to 2025.3 2026-09-13 14:28:05 +02:00
chylex 95b89dc8e9 Update Gradle to 9.7.1 and IntelliJ Platform plugin to 2.18.1 2026-09-13 11:48:15 +02:00
chylex caff100d67 Release 1.6.1 2025-12-24 07:30:06 +01:00
chylex 04323478cc Fix not checking if line background highlighters are valid before updating them
Closes #45
2025-12-24 07:12:59 +01:00
chylex 41063fb895 Fix accessing disposed editors when showing lenses
Closes #43
2025-10-01 22:43:05 +02:00
chylex 08d0f5fe34 Release 1.6.0 2025-06-26 14:35:19 +02:00
chylex 4cd76a8a8c Try replacing FileOpenedSyncListener because it randomly fails to register during IDE startup 2025-06-25 21:47:52 +02:00
chylex 05ced823a6 Add action to toggle visibility of all lenses
Closes #12
2025-06-25 15:29:42 +02:00
chylex 766ba4c74c Do not show lenses for file-wide and gutter inspections 2025-06-25 03:29:05 +02:00
chylex 4e9fdf8fa3 Rewrite intentions popup to only show relevant quick fixes and bypass the floating toolbar
Closes #39
2025-06-25 02:31:12 +02:00
chylex a754276a6c Add plugin verifier configuration 2025-06-25 02:23:33 +02:00
chylex fbe67d6068 Release 1.5.2 2025-01-15 02:05:14 +01:00
chylex 98d3e5db01 Add option to change maximum inspection description length
Closes #34
2025-01-15 01:56:02 +01:00
chylex 102352a2eb Add button to reset settings to default 2025-01-15 01:37:15 +01:00
chylex 3aeeb32bef Configure Kotlin compiler to use invokedynamic for lambdas 2025-01-15 01:36:32 +01:00
chylex 0bc85fd69b Work around referencing an internal IntelliJ API class 2025-01-14 23:14:28 +01:00
chylex cde4d81afe Update Gradle to 8.12 and IntelliJ Platform plugin to 2.2.1 2025-01-04 15:34:19 +01:00
chylex b1d6ed4d30 Remove GitHub Sponsors and Patreon from FUNDING.yml 2025-01-04 15:34:19 +01:00
chylex 2a4764fa15 Release 1.5.1 2025-01-04 10:28:58 +01:00
chylex 4bd0931d71 Add option to change or disable lens hover behavior
Closes #31
2025-01-04 08:23:53 +01:00
chylex 0f41b22872 Fix hover underline not disappearing when scrolling without moving the mouse 2025-01-03 22:48:13 +01:00
chylex 603b35abdb Fix intentions popup not working in Rider and CLion Nova 2025-01-03 17:42:38 +01:00
chylex 08ed1aadea Fix broken inlay underline rendering with some combinations of DPI and line height settings 2025-01-03 16:28:08 +01:00
32 changed files with 519 additions and 246 deletions
-2
View File
@@ -1,3 +1 @@
github: chylex
patreon: chylex
ko_fi: chylex ko_fi: chylex
+1
View File
@@ -3,4 +3,5 @@
!/.idea/runConfigurations !/.idea/runConfigurations
/.gradle/ /.gradle/
/.intellijPlatform/
/build/ /build/
+2 -2
View File
@@ -2,9 +2,9 @@
Displays errors, warnings, and other inspections inline. Highlights the background of lines with inspections. Supports light and dark themes out of the box. Displays errors, warnings, and other inspections inline. Highlights the background of lines with inspections. Supports light and dark themes out of the box.
By default, the plugin shows **Errors**, **Warnings**, **Weak Warnings**, **Server Problems**, **Grammar Errors**, **Typos**, and other inspections with a high enough severity level. Configure visible severities in **Settings | Tools | Inspection Lens**. By default, the plugin shows **Errors**, **Warnings**, **Weak Warnings**, **Server Problems**, **Grammar Errors**, **Typos**, and other inspections with a high enough severity level. Left-click an inspection to show quick fixes. Middle-click an inspection to navigate to the relevant code in the editor.
Left-click an inspection to show quick fixes. Middle-click an inspection to navigate to the relevant code in the editor. Configure appearance, behavior of clicking on inspections, and visible severities in **Settings | Tools | Inspection Lens**.
Inspired by [Error Lens](https://marketplace.visualstudio.com/items?itemName=usernamehw.errorlens) for Visual Studio Code, and [Inline Error](https://plugins.jetbrains.com/plugin/17302-inlineerror) for IntelliJ Platform. Inspired by [Error Lens](https://marketplace.visualstudio.com/items?itemName=usernamehw.errorlens) for Visual Studio Code, and [Inline Error](https://plugins.jetbrains.com/plugin/17302-inlineerror) for IntelliJ Platform.
+46 -24
View File
@@ -1,48 +1,70 @@
@file:Suppress("ConvertLambdaToReference") @file:Suppress("ConvertLambdaToReference")
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile import org.jetbrains.intellij.platform.gradle.TestFrameworkType
plugins { plugins {
kotlin("jvm") version "1.8.0" kotlin("jvm")
id("org.jetbrains.intellij") version "1.17.0" id("org.jetbrains.intellij.platform")
} }
group = "com.chylex.intellij.inspectionlens" group = "com.chylex.intellij.inspectionlens"
version = "1.5" version = "1.7.0"
idea {
module {
excludeDirs.add(file(".kotlin"))
excludeDirs.add(file("build"))
}
}
repositories { repositories {
mavenCentral() mavenCentral()
intellijPlatform {
defaultRepositories()
} }
intellij {
version.set("2023.3.3")
updateSinceUntilBuild.set(false)
plugins.add("tanvd.grazi")
}
kotlin {
jvmToolchain(17)
} }
dependencies { dependencies {
intellijPlatform {
intellijIdeaUltimate("2025.3")
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
} }
tasks.patchPluginXml { intellijPlatform {
sinceBuild.set("233.11361.10") pluginConfiguration {
ideaVersion {
sinceBuild.set("253")
untilBuild.set(provider { null })
}
} }
tasks.buildSearchableOptions { pluginVerification {
enabled = false freeArgs.add("-mute")
freeArgs.add("TemplateWordInPluginId")
ides {
recommended()
}
}
}
kotlin {
compilerOptions {
freeCompilerArgs = listOf(
"-X" + "jvm-default=all",
"-X" + "lambdas=indy"
)
}
} }
tasks.test { tasks.test {
useJUnitPlatform() useJUnitPlatform()
} }
tasks.withType<KotlinCompile> {
kotlinOptions.freeCompilerArgs = listOf(
"-Xjvm-default=all"
)
}
+1
View File
@@ -1 +1,2 @@
kotlin.stdlib.default.dependency=false 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 distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.4-bin.zip distributionUrl=https\://services.gradle.org/distributions/gradle-9.7.1-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 -10
View File
@@ -1,7 +1,7 @@
#!/bin/sh #!/bin/sh
# #
# Copyright © 2015-2021 the original authors. # Copyright © 2015 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.
@@ -15,10 +15,12 @@
# See the License for the specific language governing permissions and # See the License for the specific language governing permissions and
# limitations under the License. # limitations under the License.
# #
# SPDX-License-Identifier: Apache-2.0
#
############################################################################## ##############################################################################
# #
# Gradle start up script for POSIX generated by Gradle. # gradlew start up script for POSIX generated by Gradle.
# #
# Important for running: # Important for running:
# #
@@ -27,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 Gradle # ksh gradlew
# #
# 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:
@@ -55,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/HEAD/subprojects/plugins/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. # within the Gradle project.
# #
# You can find Gradle at https://github.com/gradle/gradle/. # You can find Gradle at https://github.com/gradle/gradle/.
@@ -84,7 +86,7 @@ done
# shellcheck disable=SC2034 # shellcheck disable=SC2034
APP_BASE_NAME=${0##*/} APP_BASE_NAME=${0##*/}
# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) # Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit
# Use the maximum available, or set MAX_FD != -1 to use that value. # Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD=maximum MAX_FD=maximum
@@ -112,7 +114,6 @@ 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.
@@ -170,7 +171,6 @@ 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,15 +203,14 @@ 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, 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. # 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" \
-classpath "$CLASSPATH" \ -jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \
org.gradle.wrapper.GradleWrapperMain \
"$@" "$@"
# Stop when "xargs" is not available. # Stop when "xargs" is not available.
Vendored
+24 -34
View File
@@ -13,16 +13,18 @@
@rem See the License for the specific language governing permissions and @rem See the License for the specific language governing permissions and
@rem limitations under the License. @rem limitations under the License.
@rem @rem
@rem SPDX-License-Identifier: Apache-2.0
@rem
@if "%DEBUG%"=="" @echo off @if "%DEBUG%"=="" @echo off
@rem ########################################################################## @rem ##########################################################################
@rem @rem
@rem Gradle startup script for Windows @rem gradlew startup script for Windows
@rem @rem
@rem ########################################################################## @rem ##########################################################################
@rem Set local scope for the variables with windows NT shell @rem Set local scope for the variables, and ensure extensions are enabled
if "%OS%"=="Windows_NT" setlocal setlocal EnableExtensions
set DIRNAME=%~dp0 set DIRNAME=%~dp0
if "%DIRNAME%"=="" set DIRNAME=. if "%DIRNAME%"=="" set DIRNAME=.
@@ -43,13 +45,13 @@ set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1 %JAVA_EXE% -version >NUL 2>&1
if %ERRORLEVEL% equ 0 goto execute if %ERRORLEVEL% equ 0 goto execute
echo. echo. 1>&2
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
echo. echo. 1>&2
echo Please set the JAVA_HOME variable in your environment to match the echo Please set the JAVA_HOME variable in your environment to match the 1>&2
echo location of your Java installation. echo location of your Java installation. 1>&2
goto fail "%COMSPEC%" /c exit 1
:findJavaFromJavaHome :findJavaFromJavaHome
set JAVA_HOME=%JAVA_HOME:"=% set JAVA_HOME=%JAVA_HOME:"=%
@@ -57,36 +59,24 @@ set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto execute if exist "%JAVA_EXE%" goto execute
echo. echo. 1>&2
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
echo. echo. 1>&2
echo Please set the JAVA_HOME variable in your environment to match the echo Please set the JAVA_HOME variable in your environment to match the 1>&2
echo location of your Java installation. echo location of your Java installation. 1>&2
goto fail "%COMSPEC%" /c exit 1
:execute :execute
@rem Setup the command line @rem Setup the command line
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
@rem Execute Gradle @rem Execute gradlew
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* @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 :exitWithErrorLevel
@rem End local scope for the variables with windows NT shell @rem Use "%COMSPEC%" /c exit to allow operators to work properly in scripts
if %ERRORLEVEL% equ 0 goto mainEnd "%COMSPEC%" /c exit %ERRORLEVEL%
: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
+7
View File
@@ -1 +1,8 @@
rootProject.name = "InspectionLens" rootProject.name = "InspectionLens"
pluginManagement {
plugins {
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,10 +2,11 @@ package com.chylex.intellij.inspectionlens
import com.chylex.intellij.inspectionlens.editor.EditorLensFeatures import com.chylex.intellij.inspectionlens.editor.EditorLensFeatures
import com.intellij.openapi.application.ApplicationManager 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.FileEditorManager
import com.intellij.openapi.fileEditor.TextEditor import com.intellij.openapi.fileEditor.TextEditor
import com.intellij.openapi.project.ProjectManager import com.intellij.openapi.project.ProjectManager
import java.util.concurrent.atomic.AtomicBoolean
/** /**
* Handles installation and uninstallation of plugin features in editors. * Handles installation and uninstallation of plugin features in editors.
@@ -13,27 +14,33 @@ import com.intellij.openapi.project.ProjectManager
internal object InspectionLens { internal object InspectionLens {
const val PLUGIN_ID = "com.chylex.intellij.inspectionlens" const val PLUGIN_ID = "com.chylex.intellij.inspectionlens"
val LOG = logger<InspectionLens>()
var SHOW_LENSES = true
set(value) {
field = value
scheduleRefresh()
}
/** /**
* Installs lenses into [editor]. * Installs lenses into [editor].
*/ */
fun install(editor: TextEditor) { fun install(editor: TextEditor) {
EditorLensFeatures.install(editor.editor, service<InspectionLensPluginDisposableService>().intersect(editor)) EditorLensFeatures.install(editor)
} }
/** /**
* Installs lenses into all open editors. * Installs lenses into all open editors.
*/ */
fun install() { fun install() {
forEachOpenEditor(::install) forEachOpenEditor(EditorLensFeatures::install)
} }
/** /**
* Refreshes lenses in all open editors. * Refreshes lenses in all open editors.
*/ */
private fun refresh() { private fun refresh() {
forEachOpenEditor { forEachOpenEditor(EditorLensFeatures::refresh)
EditorLensFeatures.refresh(it.editor)
}
} }
/** /**
@@ -49,32 +56,32 @@ internal object InspectionLens {
private inline fun forEachOpenEditor(action: (TextEditor) -> Unit) { private inline fun forEachOpenEditor(action: (TextEditor) -> Unit) {
val projectManager = ProjectManager.getInstanceIfCreated() ?: return val projectManager = ProjectManager.getInstanceIfCreated() ?: return
for (project in projectManager.openProjects.filterNot { it.isDisposed }) { for (project in projectManager.openProjects) {
for (editor in FileEditorManager.getInstance(project).allEditors.filterIsInstance<TextEditor>()) { if (project.isDisposed) {
continue
}
for (editor in FileEditorManager.getInstance(project).allEditors) {
if (editor is TextEditor) {
action(editor) action(editor)
} }
} }
} }
}
private object Refresh { private object Refresh : Runnable {
private var needsRefresh = false private val needsRefresh = AtomicBoolean(false)
fun schedule() { fun schedule() {
synchronized(this) { if (needsRefresh.compareAndSet(false, true)) {
if (!needsRefresh) { ApplicationManager.getApplication().invokeLater(this)
needsRefresh = true
ApplicationManager.getApplication().invokeLater(this::run)
}
} }
} }
private fun run() { override fun run() {
synchronized(this) { if (needsRefresh.compareAndSet(true, false)) {
if (needsRefresh) {
needsRefresh = false
refresh() 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,21 +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>) {
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 { 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)
} }
} }
@@ -1,10 +1,13 @@
package com.chylex.intellij.inspectionlens.editor package com.chylex.intellij.inspectionlens.editor
import com.chylex.intellij.inspectionlens.InspectionLensPluginDisposableService
import com.intellij.openapi.Disposable import com.intellij.openapi.Disposable
import com.intellij.openapi.components.service
import com.intellij.openapi.editor.Editor import com.intellij.openapi.editor.Editor
import com.intellij.openapi.editor.ex.FoldingModelEx import com.intellij.openapi.editor.ex.FoldingModelEx
import com.intellij.openapi.editor.ex.MarkupModelEx import com.intellij.openapi.editor.ex.MarkupModelEx
import com.intellij.openapi.editor.impl.DocumentMarkupModel import com.intellij.openapi.editor.impl.DocumentMarkupModel
import com.intellij.openapi.fileEditor.TextEditor
import com.intellij.openapi.util.Disposer import com.intellij.openapi.util.Disposer
import com.intellij.openapi.util.Key import com.intellij.openapi.util.Key
@@ -36,21 +39,24 @@ internal class EditorLensFeatures private constructor(
companion object { companion object {
private val EDITOR_KEY = Key<EditorLensFeatures>(EditorLensFeatures::class.java.name) 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) { if (editor.getUserData(EDITOR_KEY) != null) {
return return
} }
val markupModel = DocumentMarkupModel.forDocument(editor.document, editor.project, false) as? MarkupModelEx ?: return val markupModel = DocumentMarkupModel.forDocument(editor.document, editor.project, false) as? MarkupModelEx ?: return
val foldingModel = editor.foldingModel as? FoldingModelEx val foldingModel = editor.foldingModel as? FoldingModelEx
val disposable = service<InspectionLensPluginDisposableService>().intersect(owner)
val features = EditorLensFeatures(editor, markupModel, foldingModel, disposable) val features = EditorLensFeatures(editor, markupModel, foldingModel, disposable)
editor.putUserData(EDITOR_KEY, features) editor.putUserData(EDITOR_KEY, features)
Disposer.register(disposable) { editor.putUserData(EDITOR_KEY, null) } Disposer.register(disposable) { editor.putUserData(EDITOR_KEY, null) }
} }
fun refresh(editor: Editor) { fun refresh(owner: TextEditor) {
editor.getUserData(EDITOR_KEY)?.refresh() owner.editor.getUserData(EDITOR_KEY)?.refresh()
} }
} }
} }
@@ -14,23 +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(highlighterWithInfo: HighlighterWithInfo) { private fun show(inspection: Inspection) {
val (highlighter, info) = highlighterWithInfo if (editor.isDisposed) {
return
}
val highlighter = inspection.highlighter
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(info, settings)) { if (existingLens.update(inspection, settings)) {
return return
} }
existingLens.hide() existingLens.hide()
} }
val newLens = EditorLens.show(editor, info, settings) val newLens = EditorLens.show(editor, inspection, settings)
if (newLens != null) { if (newLens != null) {
lenses[highlighter] = newLens lenses[highlighter] = newLens
} }
@@ -63,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 highlighter: HighlighterWithInfo) : Command { class Show(private val inspection: Inspection) : Command {
override fun apply(lensManager: EditorLensManager) { override fun apply(lensManager: EditorLensManager) {
lensManager.show(highlighter) lensManager.show(inspection)
} }
} }
@@ -85,7 +88,6 @@ 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(highlighterWithInfo: HighlighterWithInfo) { fun show(inspection: Inspection) {
enqueue(EditorLensManager.Command.Show(highlighterWithInfo)) enqueue(EditorLensManager.Command.Show(inspection))
} }
fun hide(highlighter: RangeHighlighter) { fun hide(highlighter: RangeHighlighter) {
@@ -1,28 +0,0 @@
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)
}
}
@@ -0,0 +1,6 @@
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)
@@ -1,7 +1,10 @@
package com.chylex.intellij.inspectionlens.editor package com.chylex.intellij.inspectionlens.editor
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.openapi.components.service import com.intellij.openapi.components.service
import com.intellij.openapi.editor.ex.RangeHighlighterEx import com.intellij.openapi.editor.ex.RangeHighlighterEx
import com.intellij.openapi.editor.impl.event.MarkupModelListener import com.intellij.openapi.editor.impl.event.MarkupModelListener
@@ -21,49 +24,51 @@ internal class LensMarkupModelListener(private val lensManagerDispatcher: Editor
showIfValid(highlighter) showIfValid(highlighter)
} }
override fun beforeRemoved(highlighter: RangeHighlighterEx) { fun showAllValid(highlighters: Array<RangeHighlighter>) {
if (getFilteredHighlightInfo(highlighter) != null) { if (InspectionLens.SHOW_LENSES) {
lensManagerDispatcher.hide(highlighter) highlighters.forEach(::showIfValid)
} }
} }
private fun showIfValid(highlighter: RangeHighlighter) { private fun showIfValid(highlighter: RangeHighlighter) {
runWithHighlighterIfValid(highlighter, lensManagerDispatcher::show, ::showAsynchronously) if (!InspectionLens.SHOW_LENSES) {
return
} }
private fun showAsynchronously(highlighterWithInfo: HighlighterWithInfo.Async) { val info = highlighter.takeIf { it.isValid }?.let(::getFilteredHighlightInfo)
highlighterWithInfo.requestDescription { if (info == null || isFileLevelOrGutterAnnotation(info)) {
if (highlighterWithInfo.highlighter.isValid && highlighterWithInfo.hasDescription) { return
lensManagerDispatcher.show(highlighterWithInfo)
}
}
} }
fun showAllValid(highlighters: Array<RangeHighlighter>) { if (info is AsyncDescriptionSupplier) {
highlighters.forEach(::showIfValid) info.requestDescription().onSuccess {
if (highlighter.isValid) {
showInspection(highlighter, info)
}
}
}
else {
showInspection(highlighter, info)
} }
fun hideAll() {
lensManagerDispatcher.hideAll()
} }
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 inline fun runWithHighlighterIfValid(highlighter: RangeHighlighter, actionForImmediate: (HighlighterWithInfo) -> Unit, actionForAsync: (HighlighterWithInfo.Async) -> Unit) { private fun showInspection(highlighter: RangeHighlighter, info: HighlightInfo) {
val info = highlighter.takeIf { it.isValid }?.let(::getFilteredHighlightInfo) if (info.description != null) {
if (info != null) { lensManagerDispatcher.show(Inspection(highlighter, info))
processHighlighterWithInfo(HighlighterWithInfo.from(highlighter, info), actionForImmediate, actionForAsync)
} }
} }
private inline fun processHighlighterWithInfo(highlighterWithInfo: HighlighterWithInfo, actionForImmediate: (HighlighterWithInfo) -> Unit, actionForAsync: (HighlighterWithInfo.Async) -> Unit) { override fun beforeRemoved(highlighter: RangeHighlighterEx) {
if (highlighterWithInfo is HighlighterWithInfo.Async) { if (getFilteredHighlightInfo(highlighter) != null) {
actionForAsync(highlighterWithInfo) lensManagerDispatcher.hide(highlighter)
}
else if (highlighterWithInfo.hasDescription) {
actionForImmediate(highlighterWithInfo)
} }
} }
fun hideAll() {
lensManagerDispatcher.hideAll()
}
} }
@@ -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(info: HighlightInfo, settings: LensSettingsState): Boolean { fun update(inspection: Inspection, settings: LensSettingsState): Boolean {
val editor = inlay.editor val editor = inlay.editor
val oldSeverity = severity val oldSeverity = severity
severity = LensSeverity.from(info.severity) severity = LensSeverity.from(inspection.info.severity)
if (!inlay.tryUpdate(info)) { if (!inlay.tryUpdate(inspection)) {
inlay = EditorLensInlay.show(editor, info, settings) ?: return false inlay = EditorLensInlay.show(editor, inspection, settings) ?: return false
} }
if (lineBackground.isInvalid || oldSeverity != severity) { if (lineBackground.isInvalid || oldSeverity != severity) {
lineBackground.hide(editor) lineBackground.hide(editor)
lineBackground = EditorLensLineBackground.show(editor, info) lineBackground = EditorLensLineBackground.show(editor, inspection.info)
} }
return true return true
@@ -33,8 +33,9 @@ internal class EditorLens private constructor(private var inlay: EditorLensInlay
} }
companion object { companion object {
fun show(editor: Editor, info: HighlightInfo, settings: LensSettingsState): EditorLens? { fun show(editor: Editor, inspection: Inspection, settings: LensSettingsState): EditorLens? {
val inlay = EditorLensInlay.show(editor, info, settings) ?: return null val info = inspection.info
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,5 +1,6 @@
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
@@ -11,12 +12,12 @@ internal value class EditorLensInlay(private val inlay: Inlay<LensRenderer>) {
val editor val editor
get() = inlay.editor get() = inlay.editor
fun tryUpdate(info: HighlightInfo): Boolean { fun tryUpdate(inspection: Inspection): Boolean {
if (!inlay.isValid) { if (!inlay.isValid) {
return false return false
} }
inlay.renderer.setPropertiesFrom(info) inlay.renderer.setPropertiesFrom(inspection)
inlay.update() inlay.update()
return true return true
} }
@@ -26,10 +27,10 @@ internal value class EditorLensInlay(private val inlay: Inlay<LensRenderer>) {
} }
companion object { companion object {
fun show(editor: Editor, info: HighlightInfo, settings: LensSettingsState): EditorLensInlay? { fun show(editor: Editor, inspection: Inspection, settings: LensSettingsState): EditorLensInlay? {
val offset = getInlayHintOffset(info) val offset = getInlayHintOffset(inspection.info)
val priority = getInlayHintPriority(editor, info) val priority = getInlayHintPriority(editor, inspection.info)
val renderer = LensRenderer(info, settings) val renderer = LensRenderer(inspection, settings)
val properties = InlayProperties() val properties = InlayProperties()
.relatesToPrecedingText(true) .relatesToPrecedingText(true)
@@ -14,6 +14,10 @@ internal value class EditorLensLineBackground(private val highlighter: RangeHigh
get() = !highlighter.isValid get() = !highlighter.isValid
fun onFoldRegionsChanged(editor: Editor, severity: LensSeverity) { fun onFoldRegionsChanged(editor: Editor, severity: LensSeverity) {
if (isInvalid) {
return
}
if (highlighter is RangeHighlighterEx) { if (highlighter is RangeHighlighterEx) {
highlighter.textAttributes = getAttributes(editor, highlighter.startOffset, highlighter.endOffset, severity) highlighter.textAttributes = getAttributes(editor, highlighter.startOffset, highlighter.endOffset, severity)
} }
@@ -1,36 +1,128 @@
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.editor.Inspection
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.IntentionsUI
import com.intellij.codeInsight.daemon.impl.ShowIntentionsPass
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.IntentionHintComponent
import com.intellij.codeInsight.intention.impl.ShowIntentionActionsHandler import com.intellij.codeInsight.intention.impl.ShowIntentionActionsHandler
import com.intellij.lang.LangBundle import com.intellij.ide.DataManager
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.editor.Editor import com.intellij.openapi.editor.Editor
import com.intellij.openapi.editor.Inlay
import com.intellij.openapi.editor.markup.RangeHighlighter
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 java.lang.reflect.Method
internal object IntentionsPopup { internal object IntentionsPopup {
fun show(editor: Editor) { private const val INTENTION_SOURCE_CLASS_NAME = "com.intellij.codeInsight.intention.IntentionSource"
if (!tryShow(editor)) {
HintManager.getInstance().showInformationHint(editor, LangBundle.message("hint.text.no.context.actions.available.at.this.location")) private val showPopupMethod: ShowPopupMethod? = try {
val method = IntentionHintComponent::class.java.declaredMethods.first { method ->
val parameterTypes = method.parameterTypes
method.name == "showPopup" &&
parameterTypes.size == 2 &&
parameterTypes[0] === RelativePoint::class.java &&
parameterTypes[1].name == INTENTION_SOURCE_CLASS_NAME
}
method.isAccessible = true
@Suppress("UNCHECKED_CAST")
val intentionSourceEnum = Class.forName(INTENTION_SOURCE_CLASS_NAME) as Class<Enum<*>>
val intentionSource = intentionSourceEnum.enumConstants.first { it.name == "OTHER" }
ShowPopupMethod(method, arrayOf(null, intentionSource))
} catch (t: Throwable) {
InspectionLens.LOG.warn("Could not initialize intention popup", t)
null
}
val hasShowPopupMethod
get() = showPopupMethod != 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 { fun show(inspection: Inspection, inlay: Inlay<*>) {
if (!tryShow(inspection, inlay)) {
HintManager.getInstance().showInformationHint(inlay.editor, "No context actions available at this location")
}
}
private fun tryShow(inspection: Inspection, inlay: Inlay<*>): Boolean {
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
PsiDocumentManager.getInstance(project).commitAllDocuments() PsiDocumentManager.getInstance(project).commitAllDocuments()
IntentionsUI.getInstance(project).hide() IntentionsUI.getInstance(project).hide()
HANDLER.showIntentionHint(project, editor, file, showFeedbackOnEmptyMenu = true) val intentions = collectIntentions(editor, file, inspection.highlighter, inlay.offset)
return true if (intentions == null) {
return false
} }
private val HANDLER = object : ShowIntentionActionsHandler() { try {
public override fun showIntentionHint(project: Project, editor: Editor, file: PsiFile, showFeedbackOnEmptyMenu: Boolean) { showIntentionsPopup(project, file, editor, intentions)
super.showIntentionHint(project, editor, file, showFeedbackOnEmptyMenu) 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? {
val resolvedInfo = HighlightInfo.fromRangeHighlighter(highlighter)
if (resolvedInfo == null) {
return null
}
val intentionActions = mutableListOf<IntentionActionDescriptor>()
resolvedInfo.findRegisteredQuickFix { descriptor, _ ->
if (ShowIntentionActionsHandler.availableFor(file, editor, offset, descriptor.action)) {
intentionActions.add(descriptor)
}
null
}
return IntentionsInfo().also {
it.offset = offset
ShowIntentionsPass.fillIntentionsInfoForHighlightInfo(resolvedInfo, it, intentionActions)
}
}
private fun showIntentionsPopup(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)
}
}
} }
@@ -1,7 +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.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
@@ -16,6 +17,7 @@ import com.intellij.ui.paint.EffectPainter
import java.awt.Cursor import java.awt.Cursor
import java.awt.Graphics import java.awt.Graphics
import java.awt.Graphics2D import java.awt.Graphics2D
import java.awt.MouseInfo
import java.awt.Point import java.awt.Point
import java.awt.Rectangle import java.awt.Rectangle
import java.awt.event.MouseEvent import java.awt.event.MouseEvent
@@ -25,15 +27,14 @@ import javax.swing.SwingUtilities
/** /**
* Renders the text of an inspection lens. * Renders the text of an inspection lens.
*/ */
class LensRenderer(private var info: HighlightInfo, settings: LensSettingsState) : HintRenderer(null), InputHandler { class LensRenderer(private var inspection: Inspection, private val settings: LensSettingsState) : HintRenderer(null), InputHandler {
private val useEditorFont = settings.useEditorFont
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(info) setPropertiesFrom(inspection)
} }
fun setInlay(inlay: Inlay<*>) { fun setInlay(inlay: Inlay<*>) {
@@ -41,12 +42,12 @@ class LensRenderer(private var info: HighlightInfo, settings: LensSettingsState)
this.inlay = inlay this.inlay = inlay
} }
fun setPropertiesFrom(info: HighlightInfo) { fun setPropertiesFrom(inspection: Inspection) {
this.info = info this.inspection = inspection
val description = getValidDescriptionText(info.description) val description = getValidDescriptionText(inspection.info.description, settings.maxDescriptionLength)
text = description text = description
attributes = LensSeverity.from(info.severity).textAttributes attributes = LensSeverity.from(inspection.info.severity).textAttributes
extraRightPadding = if (description.lastOrNull() == '.') 2 else 0 extraRightPadding = if (description.lastOrNull() == '.') 2 else 0
} }
@@ -67,7 +68,7 @@ class LensRenderer(private var info: HighlightInfo, settings: LensSettingsState)
val font = editor.colorsScheme.getFont(EditorFontType.PLAIN) val font = editor.colorsScheme.getFont(EditorFontType.PLAIN)
val x = r.x + TEXT_HORIZONTAL_PADDING val x = r.x + TEXT_HORIZONTAL_PADDING
val y = r.y + editor.ascent + 1 val y = r.y + editor.ascent
val w = inlay.widthInPixels - UNDERLINE_WIDTH_REDUCTION - extraRightPadding val w = inlay.widthInPixels - UNDERLINE_WIDTH_REDUCTION - extraRightPadding
val h = editor.descent val h = editor.descent
@@ -80,11 +81,11 @@ class LensRenderer(private var info: HighlightInfo, settings: LensSettingsState)
} }
override fun useEditorFont(): Boolean { override fun useEditorFont(): Boolean {
return useEditorFont return settings.useEditorFont
} }
override fun mouseMoved(event: MouseEvent, translated: Point) { override fun mouseMoved(event: MouseEvent, translated: Point) {
setHovered(isHoveringText(translated)) setHovered(isHoveringText())
} }
override fun mouseExited() { override fun mouseExited() {
@@ -92,6 +93,10 @@ class LensRenderer(private var info: HighlightInfo, settings: LensSettingsState)
} }
private fun setHovered(hovered: Boolean) { private fun setHovered(hovered: Boolean) {
if (hovered && settings.lensHoverMode == LensHoverMode.DISABLED) {
return
}
if (this.hovered == hovered) { if (this.hovered == hovered) {
return return
} }
@@ -108,23 +113,30 @@ class LensRenderer(private var info: HighlightInfo, settings: LensSettingsState)
} }
override fun mousePressed(event: MouseEvent, translated: Point) { override fun mousePressed(event: MouseEvent, translated: Point) {
if (!isHoveringText(translated)) { val hoverMode = settings.lensHoverMode
if (hoverMode == LensHoverMode.DISABLED || !isHoveringText()) {
return return
} }
if (SwingUtilities.isLeftMouseButton(event) || SwingUtilities.isMiddleMouseButton(event)) { if (event.button.let { it == MouseEvent.BUTTON1 || it == MouseEvent.BUTTON2 }) {
event.consume() event.consume()
val editor = inlay.editor val editor = inlay.editor
moveToOffset(editor, info.actualStartOffset) moveToOffset(editor, inspection.info.actualStartOffset)
if (SwingUtilities.isLeftMouseButton(event)) { if ((event.button == MouseEvent.BUTTON1) xor (hoverMode != LensHoverMode.DEFAULT)) {
IntentionsPopup.show(editor) IntentionsPopup.show(inspection, inlay)
} }
} }
} }
private fun isHoveringText(point: Point): Boolean { private fun isHoveringText(): Boolean {
val bounds = inlay.bounds ?: return false
val point = MouseInfo.getPointerInfo().location.apply {
SwingUtilities.convertPointFromScreen(this, inlay.editor.contentComponent)
translate(-bounds.x, -bounds.y)
}
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
@@ -139,15 +151,13 @@ class LensRenderer(private var info: HighlightInfo, settings: LensSettingsState)
private const val HOVER_HORIZONTAL_PADDING = TEXT_HORIZONTAL_PADDING - 2 private const val HOVER_HORIZONTAL_PADDING = TEXT_HORIZONTAL_PADDING - 2
private const val UNDERLINE_WIDTH_REDUCTION = (TEXT_HORIZONTAL_PADDING * 2) - 1 private const val UNDERLINE_WIDTH_REDUCTION = (TEXT_HORIZONTAL_PADDING * 2) - 1
private const val MAX_DESCRIPTION_LENGTH = 120
/** /**
* Kotlin compiler inspections have an `[UPPERCASE_TAG]` at the beginning. * Kotlin compiler inspections have an `[UPPERCASE_TAG]` at the beginning.
*/ */
private val UPPERCASE_TAG_REGEX = Pattern.compile("^\\[[A-Z_]+] ") private val UPPERCASE_TAG_REGEX = Pattern.compile("^\\[[A-Z_]+] ")
private fun getValidDescriptionText(text: String?): String { private fun getValidDescriptionText(text: String?, maxLength: Int): String {
return if (text.isNullOrBlank()) " " else addEllipsisOrMissingPeriod(unescapeHtmlEntities(stripUppercaseTag(text))) return if (text.isNullOrBlank()) " " else addEllipsisOrMissingPeriod(unescapeHtmlEntities(stripUppercaseTag(text)), maxLength)
} }
private fun stripUppercaseTag(text: String): String { private fun stripUppercaseTag(text: String): String {
@@ -165,9 +175,9 @@ class LensRenderer(private var info: HighlightInfo, settings: LensSettingsState)
return if (text.contains('&')) StringUtil.unescapeXmlEntities(text) else text return if (text.contains('&')) StringUtil.unescapeXmlEntities(text) else text
} }
private fun addEllipsisOrMissingPeriod(text: String): String { private fun addEllipsisOrMissingPeriod(text: String, maxLength: Int): String {
return when { return when {
text.length > MAX_DESCRIPTION_LENGTH -> text.take(MAX_DESCRIPTION_LENGTH).trimEnd { it.isWhitespace() || it == '.' } + "…" text.length > maxLength -> text.take(maxLength).trimEnd { it.isWhitespace() || it == '.' } + "…"
!text.endsWith('.') -> "$text." !text.endsWith('.') -> "$text."
else -> text else -> text
} }
@@ -19,9 +19,8 @@ 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 && severity !== HighlightSeverity.INFO return severity > HighlightSeverity.TEXT_ATTRIBUTES
} }
fun getSupportedSeverities(registrar: SeverityRegistrar = SeverityRegistrar.getSeverityRegistrar(null)): List<HighlightSeverity> { fun getSupportedSeverities(registrar: SeverityRegistrar = SeverityRegistrar.getSeverityRegistrar(null)): List<HighlightSeverity> {
@@ -10,6 +10,7 @@ import com.intellij.openapi.editor.markup.TextAttributes
import com.intellij.openapi.options.BoundConfigurable import com.intellij.openapi.options.BoundConfigurable
import com.intellij.openapi.options.ConfigurableWithId import com.intellij.openapi.options.ConfigurableWithId
import com.intellij.openapi.ui.DialogPanel import com.intellij.openapi.ui.DialogPanel
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
@@ -18,8 +19,11 @@ import com.intellij.ui.dsl.builder.Cell
import com.intellij.ui.dsl.builder.RightGap import com.intellij.ui.dsl.builder.RightGap
import com.intellij.ui.dsl.builder.Row import com.intellij.ui.dsl.builder.Row
import com.intellij.ui.dsl.builder.RowLayout import com.intellij.ui.dsl.builder.RowLayout
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.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 {
@@ -73,7 +77,26 @@ class LensApplicationConfigurable : BoundConfigurable("Inspection Lens"), Config
override fun createPanel(): DialogPanel { override fun createPanel(): DialogPanel {
val settings = settingsService.state val settings = settingsService.state
return panel { lateinit var panel: DialogPanel
panel = panel {
group("Appearance") {
row {
checkBox("Use editor font").bindSelected(settings::useEditorFont)
}
row("Max description length:") {
intTextField(LensSettingsState.MAX_DESCRIPTION_LENGTH_RANGE, keyboardStep = 10).bindIntText(settings::maxDescriptionLength)
}
}
group("Behavior") {
row("Hover mode:") {
val items = LensHoverMode.entries
val renderer = textListCellRenderer<LensHoverMode?> { it?.description ?: "" }
comboBox(items, renderer).bindItem(settings::lensHoverMode) { settings.lensHoverMode = it ?: LensHoverMode.DEFAULT }
}
}
group("Shown Severities") { group("Shown Severities") {
for ((id, severity, textAttributes) in allSeverities) { for ((id, severity, textAttributes) in allSeverities) {
row { row {
@@ -90,13 +113,20 @@ class LensApplicationConfigurable : BoundConfigurable("Inspection Lens"), Config
} }
} }
group("Appearance") { group("Actions") {
row { row {
checkBox("Use editor font").bindSelected(settings::useEditorFont) button("Reset to Default") {
if (MessageDialogBuilder.yesNo("Reset to Default", "Are you sure you want to reset settings to default?").ask(panel)) {
settingsService.resetState()
reset()
} }
} }
} }
} }
}
return panel
}
private fun <K, V> Cell<JBCheckBox>.bindSelectedToNotIn(collection: MutableMap<K, V>, key: K, value: V): Cell<JBCheckBox> { private fun <K, V> Cell<JBCheckBox>.bindSelectedToNotIn(collection: MutableMap<K, V>, key: K, value: V): Cell<JBCheckBox> {
return bindSelected({ key !in collection }, { if (it) collection.remove(key) else collection[key] = value }) return bindSelected({ key !in collection }, { if (it) collection.remove(key) else collection[key] = value })
@@ -0,0 +1,7 @@
package com.chylex.intellij.inspectionlens.settings
enum class LensHoverMode(val description: String) {
DISABLED("Disabled"),
DEFAULT("Left click shows intentions, middle click jumps to highlight"),
SWAPPED("Left click jumps to highlight, middle click shows intentions")
}
@@ -21,6 +21,12 @@ class LensSettingsState : SimplePersistentStateComponent<LensSettingsState.State
var showUnknownSeverities by property(true) var showUnknownSeverities by property(true)
var useEditorFont by property(true) var useEditorFont by property(true)
var maxDescriptionLength by property(120)
var lensHoverMode by enum(LensHoverMode.DEFAULT)
}
companion object {
val MAX_DESCRIPTION_LENGTH_RANGE = 20..1000
} }
@get:Synchronized @get:Synchronized
@@ -31,8 +37,27 @@ class LensSettingsState : SimplePersistentStateComponent<LensSettingsState.State
val useEditorFont val useEditorFont
get() = state.useEditorFont get() = state.useEditorFont
val maxDescriptionLength
get() = state.maxDescriptionLength
val lensHoverMode
get() = state.lensHoverMode
override fun loadState(state: State) { override fun loadState(state: State) {
super.loadState(state) super.loadState(state)
state.maxDescriptionLength = state.maxDescriptionLength.coerceIn(MAX_DESCRIPTION_LENGTH_RANGE)
update()
}
fun resetState() {
val default = State()
state.hiddenSeverities.apply { clear(); putAll(default.hiddenSeverities) }
state.showUnknownSeverities = default.showUnknownSeverities
state.useEditorFont = default.useEditorFont
state.maxDescriptionLength = default.maxDescriptionLength
state.lensHoverMode = default.lensHoverMode
update() update()
} }
+54 -3
View File
@@ -6,14 +6,48 @@
<description><![CDATA[ <description><![CDATA[
Displays errors, warnings, and other inspections inline. Highlights the background of lines with inspections. Supports light and dark themes out of the box. Displays errors, warnings, and other inspections inline. Highlights the background of lines with inspections. Supports light and dark themes out of the box.
<br><br> <br><br>
By default, the plugin shows <b>Errors</b>, <b>Warnings</b>, <b>Weak Warnings</b>, <b>Server Problems</b>, <b>Grammar Errors</b>, <b>Typos</b>, and other inspections with a high enough severity level. Configure visible severities in <b>Settings | Tools | Inspection Lens</a>. By default, the plugin shows <b>Errors</b>, <b>Warnings</b>, <b>Weak Warnings</b>, <b>Server Problems</b>, <b>Grammar Errors</b>, <b>Typos</b>, and other inspections with a high enough severity level. Left-click an inspection to show quick fixes. Middle-click an inspection to navigate to the relevant code in the editor.
<br><br> <br><br>
Left-click an inspection to show quick fixes. Middle-click an inspection to navigate to the relevant code in the editor. Configure appearance, behavior of clicking on inspections, and visible severities in <b>Settings | Tools | Inspection Lens</b>.
<br><br> <br><br>
Inspired by <a href="https://marketplace.visualstudio.com/items?itemName=usernamehw.errorlens">Error Lens</a> for VS Code, and <a href="https://plugins.jetbrains.com/plugin/17302-inlineerror">Inline Error</a> for IntelliJ Platform. Inspired by <a href="https://marketplace.visualstudio.com/items?itemName=usernamehw.errorlens">Error Lens</a> for VS Code, and <a href="https://plugins.jetbrains.com/plugin/17302-inlineerror">Inline Error</a> for IntelliJ Platform.
]]></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>
<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> <b>Version 1.5</b>
<ul> <ul>
<li>Added possibility to left-click an inspection to show quick fixes.</li> <li>Added possibility to left-click an inspection to show quick fixes.</li>
@@ -23,51 +57,62 @@
<li>Improved descriptions of Kotlin compiler inspections.</li> <li>Improved descriptions of Kotlin compiler inspections.</li>
<li>Fixed visual artifacts in Rendered Doc comments.</li> <li>Fixed visual artifacts in Rendered Doc comments.</li>
</ul> </ul>
<br>
<b>Version 1.4.1</b> <b>Version 1.4.1</b>
<ul> <ul>
<li>Fixed warnings in usage of IntelliJ SDK.</li> <li>Fixed warnings in usage of IntelliJ SDK.</li>
</ul> </ul>
<br>
<b>Version 1.4</b> <b>Version 1.4</b>
<ul> <ul>
<li>Added configuration of visible severities to <b>Settings | Tools | Inspection Lens</b>.</li> <li>Added configuration of visible severities to <b>Settings | Tools | Inspection Lens</b>.</li>
</ul> </ul>
<br>
<b>Version 1.3.3</b> <b>Version 1.3.3</b>
<ul> <ul>
<li>Partially reverted fix for inspections that include HTML in their description due to breaking inspections with angled brackets.</li> <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> <li>Fixed plugin not working when installed on JetBrains Gateway Client.</li>
</ul> </ul>
<br>
<b>Version 1.3.2</b> <b>Version 1.3.2</b>
<ul> <ul>
<li>Fixed inspections randomly not disappearing.</li> <li>Fixed inspections randomly not disappearing.</li>
</ul> </ul>
<br>
<b>Version 1.3.1</b> <b>Version 1.3.1</b>
<ul> <ul>
<li>Updated minimum version to IntelliJ 2023.3 due to breaking API changes.</li> <li>Updated minimum version to IntelliJ 2023.3 due to breaking API changes.</li>
</ul> </ul>
<br>
<b>Version 1.3.0</b> <b>Version 1.3.0</b>
<ul> <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> <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> </ul>
<br>
<b>Version 1.2.0</b> <b>Version 1.2.0</b>
<ul> <ul>
<li>Support for IntelliJ 2023.2 EAP.</li> <li>Support for IntelliJ 2023.2 EAP.</li>
<li>Added distinct colors for typos and Grazie inspections.</li> <li>Added distinct colors for typos and Grazie inspections.</li>
</ul> </ul>
<br>
<b>Version 1.1.2</b> <b>Version 1.1.2</b>
<ul> <ul>
<li>Added plugin icon.</li> <li>Added plugin icon.</li>
<li>Updated minimum version to IntelliJ 2023.1 due to deprecated APIs.</li> <li>Updated minimum version to IntelliJ 2023.1 due to deprecated APIs.</li>
</ul> </ul>
<br>
<b>Version 1.1.1</b> <b>Version 1.1.1</b>
<ul> <ul>
<li>Multiple inspections at the same place in the document are now ordered by severity.</li> <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> <li>Improved performance of processing inspections which do not contain HTML.</li>
</ul> </ul>
<br>
<b>Version 1.1.0</b> <b>Version 1.1.0</b>
<ul> <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 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> <li>Fixed exception when asynchronous inspections run on a non-EDT thread.</li>
</ul> </ul>
<br>
<b>Version 1.0.0</b> <b>Version 1.0.0</b>
<ul> <ul>
<li>Initial version with support for IntelliJ 2022.2 and newer.</li> <li>Initial version with support for IntelliJ 2022.2 and newer.</li>
@@ -85,11 +130,17 @@
parentId="tools" /> parentId="tools" />
</extensions> </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> <applicationListeners>
<listener class="com.chylex.intellij.inspectionlens.InspectionLensPluginListener" topic="com.intellij.ide.plugins.DynamicPluginListener" /> <listener class="com.chylex.intellij.inspectionlens.InspectionLensPluginListener" topic="com.intellij.ide.plugins.DynamicPluginListener" />
</applicationListeners> </applicationListeners>
<projectListeners> <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> </projectListeners>
</idea-plugin> </idea-plugin>
@@ -0,0 +1,11 @@
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)
}
}