1
0
mirror of https://github.com/chylex/IntelliJ-Inspection-Lens.git synced 2025-09-17 23:24:49 +02:00

19 Commits

Author SHA1 Message Date
2be1144cbf Debug logs 2025-05-06 10:23:34 +02:00
fbe67d6068 Release 1.5.2 2025-01-15 02:05:14 +01:00
98d3e5db01 Add option to change maximum inspection description length
Closes #34
2025-01-15 01:56:02 +01:00
102352a2eb Add button to reset settings to default 2025-01-15 01:37:15 +01:00
3aeeb32bef Configure Kotlin compiler to use invokedynamic for lambdas 2025-01-15 01:36:32 +01:00
0bc85fd69b Work around referencing an internal IntelliJ API class 2025-01-14 23:14:28 +01:00
cde4d81afe Update Gradle to 8.12 and IntelliJ Platform plugin to 2.2.1 2025-01-04 15:34:19 +01:00
b1d6ed4d30 Remove GitHub Sponsors and Patreon from FUNDING.yml 2025-01-04 15:34:19 +01:00
2a4764fa15 Release 1.5.1 2025-01-04 10:28:58 +01:00
4bd0931d71 Add option to change or disable lens hover behavior
Closes #31
2025-01-04 08:23:53 +01:00
0f41b22872 Fix hover underline not disappearing when scrolling without moving the mouse 2025-01-03 22:48:13 +01:00
603b35abdb Fix intentions popup not working in Rider and CLion Nova 2025-01-03 17:42:38 +01:00
08ed1aadea Fix broken inlay underline rendering with some combinations of DPI and line height settings 2025-01-03 16:28:08 +01:00
8936f0e5be Release 1.5 2024-12-26 23:33:36 +01:00
89e71d5301 Make middle-clicking an inspection lens jump to the start of the inspection highlight 2024-12-26 06:53:42 +01:00
4c80573375 Limit description length to 120 characters
Closes #18
2024-12-26 06:27:28 +01:00
816440a150 Strip uppercase tags from the beginning of inspection description
Meant for Kotlin compiler inspections, but could potentially affect others.
2024-12-26 01:54:16 +01:00
4899498522 Make inspection lenses clickable and show popup with intentions 2024-12-25 23:44:30 +01:00
8ee14ff55e Hide line background highlighters in folded regions to fix documentation rendering
Closes #26
2024-12-24 19:38:31 +01:00
29 changed files with 508 additions and 99 deletions

2
.github/FUNDING.yml vendored
View File

@@ -1,3 +1 @@
github: chylex
patreon: chylex
ko_fi: chylex ko_fi: chylex

1
.gitignore vendored
View File

@@ -3,4 +3,5 @@
!/.idea/runConfigurations !/.idea/runConfigurations
/.gradle/ /.gradle/
/.intellijPlatform/
/build/ /build/

View File

@@ -2,7 +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.
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.

View File

@@ -1,48 +1,50 @@
@file:Suppress("ConvertLambdaToReference") @file:Suppress("ConvertLambdaToReference")
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
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.4.1" version = "1.5.2.902"
repositories { repositories {
mavenCentral() mavenCentral()
intellijPlatform {
defaultRepositories()
}
} }
intellij { dependencies {
version.set("2023.3.3") intellijPlatform {
updateSinceUntilBuild.set(false) intellijIdeaUltimate("2025.1")
bundledPlugin("tanvd.grazi")
}
plugins.add("tanvd.grazi") testImplementation("org.junit.jupiter:junit-jupiter:5.9.2")
}
intellijPlatform {
pluginConfiguration {
ideaVersion {
sinceBuild.set("233.11361.10")
untilBuild.set(provider { null })
}
}
} }
kotlin { kotlin {
jvmToolchain(17) jvmToolchain(17)
}
dependencies { compilerOptions {
testImplementation("org.junit.jupiter:junit-jupiter:5.9.2") freeCompilerArgs = listOf(
} "-X" + "jvm-default=all",
"-X" + "lambdas=indy"
tasks.patchPluginXml { )
sinceBuild.set("233.11361.10") }
}
tasks.buildSearchableOptions {
enabled = false
} }
tasks.test { tasks.test {
useJUnitPlatform() useJUnitPlatform()
} }
tasks.withType<KotlinCompile> {
kotlinOptions.freeCompilerArgs = listOf(
"-Xjvm-default=all"
)
}

Binary file not shown.

View File

@@ -1,6 +1,6 @@
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-8.12-bin.zip
networkTimeout=10000 networkTimeout=10000
validateDistributionUrl=true validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME zipStoreBase=GRADLE_USER_HOME

6
gradlew vendored
View File

@@ -15,6 +15,8 @@
# 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
#
############################################################################## ##############################################################################
# #
@@ -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/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
# within the Gradle project. # within the Gradle project.
# #
# You can find Gradle at https://github.com/gradle/gradle/. # You can find Gradle at https://github.com/gradle/gradle/.
@@ -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

22
gradlew.bat vendored
View File

@@ -13,6 +13,8 @@
@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 ##########################################################################
@@ -43,11 +45,11 @@ 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 goto fail
@@ -57,11 +59,11 @@ 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 goto fail

View File

@@ -1 +1,8 @@
rootProject.name = "InspectionLens" rootProject.name = "InspectionLens"
pluginManagement {
plugins {
kotlin("jvm") version "2.1.0"
id("org.jetbrains.intellij.platform") version "2.2.1"
}
}

View File

@@ -3,6 +3,7 @@ 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.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
@@ -13,6 +14,8 @@ 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>()
/** /**
* Installs lenses into [editor]. * Installs lenses into [editor].
*/ */

View File

@@ -11,6 +11,8 @@ import com.intellij.openapi.vfs.VirtualFile
*/ */
class InspectionLensFileOpenedListener : FileOpenedSyncListener { class InspectionLensFileOpenedListener : FileOpenedSyncListener {
override fun fileOpenedSync(source: FileEditorManager, file: VirtualFile, editorsWithProviders: List<FileEditorWithProvider>) { override fun fileOpenedSync(source: FileEditorManager, file: VirtualFile, editorsWithProviders: List<FileEditorWithProvider>) {
InspectionLens.LOG.info("File opened: $file (editor count: ${editorsWithProviders.size})")
for (editorWrapper in editorsWithProviders) { for (editorWrapper in editorsWithProviders) {
val fileEditor = editorWrapper.fileEditor val fileEditor = editorWrapper.fileEditor
if (fileEditor is TextEditor) { if (fileEditor is TextEditor) {

View File

@@ -0,0 +1,9 @@
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)
}

View File

@@ -0,0 +1,5 @@
package com.chylex.intellij.inspectionlens.debug
import java.time.Instant
data class LensEvent(val time: Instant, val data: LensEventData)

View File

@@ -0,0 +1,7 @@
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
}

View File

@@ -0,0 +1,14 @@
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))
}
}

View File

@@ -1,7 +1,9 @@
package com.chylex.intellij.inspectionlens.editor package com.chylex.intellij.inspectionlens.editor
import com.chylex.intellij.inspectionlens.InspectionLens
import com.intellij.openapi.Disposable import com.intellij.openapi.Disposable
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.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.util.Disposer import com.intellij.openapi.util.Disposer
@@ -13,15 +15,18 @@ import com.intellij.openapi.util.Key
internal class EditorLensFeatures private constructor( internal class EditorLensFeatures private constructor(
editor: Editor, editor: Editor,
private val markupModel: MarkupModelEx, private val markupModel: MarkupModelEx,
foldingModel: FoldingModelEx?,
disposable: Disposable disposable: Disposable
) { ) {
private val lensManager = EditorLensManager(editor) private val lensManager = EditorLensManager(editor)
private val lensManagerDispatcher = EditorLensManagerDispatcher(lensManager) private val lensManagerDispatcher = EditorLensManagerDispatcher(lensManager)
private val markupModelListener = LensMarkupModelListener(lensManagerDispatcher) private val markupModelListener = LensMarkupModelListener(editor, lensManagerDispatcher)
init { init {
markupModel.addMarkupModelListener(disposable, markupModelListener) markupModel.addMarkupModelListener(disposable, markupModelListener)
markupModelListener.showAllValid(markupModel.allHighlighters) markupModelListener.showAllValid(markupModel.allHighlighters)
foldingModel?.addListener(LensFoldingModelListener(lensManager), disposable)
} }
private fun refresh() { private fun refresh() {
@@ -34,18 +39,28 @@ internal class EditorLensFeatures private constructor(
fun install(editor: Editor, disposable: Disposable) { fun install(editor: Editor, disposable: Disposable) {
if (editor.getUserData(EDITOR_KEY) != null) { if (editor.getUserData(EDITOR_KEY) != null) {
InspectionLens.LOG.info("Skipped installation to: $editor")
return return
} }
InspectionLens.LOG.info("Installing to: $editor")
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 features = EditorLensFeatures(editor, markupModel, disposable) val foldingModel = editor.foldingModel as? FoldingModelEx
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) {
InspectionLens.LOG.info("Installation disposed: $editor", Exception("DISPOSE STACK TRACE"))
editor.putUserData(EDITOR_KEY, null)
}
} }
fun refresh(editor: Editor) { fun refresh(editor: Editor) {
editor.getUserData(EDITOR_KEY)?.refresh() val userData = editor.getUserData(EDITOR_KEY)
InspectionLens.LOG.info("Refreshing: $editor ($userData)")
userData?.refresh()
} }
} }
} }

View File

@@ -1,7 +1,9 @@
package com.chylex.intellij.inspectionlens.editor 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.editor.lens.EditorLens
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.components.service import com.intellij.openapi.components.service
import com.intellij.openapi.editor.Editor import com.intellij.openapi.editor.Editor
import com.intellij.openapi.editor.markup.RangeHighlighter import com.intellij.openapi.editor.markup.RangeHighlighter
@@ -14,6 +16,9 @@ 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 val highlighters
get() = lenses.keys.map { Highlighter(it, HighlightInfo.fromRangeHighlighter(it)!!) }
private fun show(highlighterWithInfo: HighlighterWithInfo) { private fun show(highlighterWithInfo: HighlighterWithInfo) {
val (highlighter, info) = highlighterWithInfo val (highlighter, info) = highlighterWithInfo
@@ -56,6 +61,10 @@ internal class EditorLensManager(private val editor: Editor) {
} }
} }
fun onFoldRegionsChanged() {
lenses.values.forEach(EditorLens::onFoldRegionsChanged)
}
sealed interface Command { sealed interface Command {
fun apply(lensManager: EditorLensManager) fun apply(lensManager: EditorLensManager)

View File

@@ -0,0 +1,12 @@
package com.chylex.intellij.inspectionlens.editor
import com.intellij.openapi.editor.ex.FoldingListener
/**
* Listens for code folding events and reports them to [EditorLensManager].
*/
internal class LensFoldingModelListener(private val lensManager: EditorLensManager) : FoldingListener {
override fun onFoldProcessingEnd() {
lensManager.onFoldRegionsChanged()
}
}

View File

@@ -1,8 +1,13 @@
package com.chylex.intellij.inspectionlens.editor 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.chylex.intellij.inspectionlens.settings.LensSettingsState
import com.intellij.codeInsight.daemon.impl.HighlightInfo import com.intellij.codeInsight.daemon.impl.HighlightInfo
import com.intellij.openapi.components.service import com.intellij.openapi.components.service
import com.intellij.openapi.editor.Editor
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
import com.intellij.openapi.editor.markup.RangeHighlighter import com.intellij.openapi.editor.markup.RangeHighlighter
@@ -10,20 +15,36 @@ import com.intellij.openapi.editor.markup.RangeHighlighter
/** /**
* Listens for inspection highlights and reports them to [EditorLensManager]. * Listens for inspection highlights and reports them to [EditorLensManager].
*/ */
internal class LensMarkupModelListener(private val lensManagerDispatcher: EditorLensManagerDispatcher) : MarkupModelListener { internal class LensMarkupModelListener(private val editor: Editor, private val lensManagerDispatcher: EditorLensManagerDispatcher) : MarkupModelListener {
private val settings = service<LensSettingsState>() private val settings = service<LensSettingsState>()
override fun afterAdded(highlighter: RangeHighlighterEx) { override fun afterAdded(highlighter: RangeHighlighterEx) {
showIfValid(highlighter) try {
getFilteredHighlightInfo(highlighter)?.let { LensEventManager.addEvent(editor, LensEventData.MarkupModelAfterAdded(Highlighter(highlighter, it))) }
showIfValid(highlighter)
} catch (e: Exception) {
InspectionLens.LOG.error("Error showing inspection", e)
}
} }
override fun attributesChanged(highlighter: RangeHighlighterEx, renderersChanged: Boolean, fontStyleOrColorChanged: Boolean) { override fun attributesChanged(highlighter: RangeHighlighterEx, renderersChanged: Boolean, fontStyleOrColorChanged: Boolean) {
showIfValid(highlighter) try {
getFilteredHighlightInfo(highlighter)?.let { LensEventManager.addEvent(editor, LensEventData.MarkupModelAttributesChanged(Highlighter(highlighter, it))) }
showIfValid(highlighter)
} catch (e: Exception) {
InspectionLens.LOG.error("Error updating inspection", e)
}
} }
override fun beforeRemoved(highlighter: RangeHighlighterEx) { override fun beforeRemoved(highlighter: RangeHighlighterEx) {
if (getFilteredHighlightInfo(highlighter) != null) { try {
lensManagerDispatcher.hide(highlighter) 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)
} }
} }

View File

@@ -4,15 +4,18 @@ 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
internal class EditorLens private constructor(private var inlay: EditorLensInlay, private var lineBackground: EditorLensLineBackground) { 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(info: HighlightInfo, settings: LensSettingsState): Boolean {
val editor = inlay.editor val editor = inlay.editor
val oldSeverity = severity
severity = LensSeverity.from(info.severity)
if (!inlay.tryUpdate(info)) { if (!inlay.tryUpdate(info)) {
inlay = EditorLensInlay.show(editor, info, settings) ?: return false inlay = EditorLensInlay.show(editor, info, settings) ?: return false
} }
if (lineBackground.shouldRecreate(info)) { if (lineBackground.isInvalid || oldSeverity != severity) {
lineBackground.hide(editor) lineBackground.hide(editor)
lineBackground = EditorLensLineBackground.show(editor, info) lineBackground = EditorLensLineBackground.show(editor, info)
} }
@@ -20,18 +23,25 @@ internal class EditorLens private constructor(private var inlay: EditorLensInlay
return true return true
} }
fun hide() { fun onFoldRegionsChanged() {
val editor = inlay.editor lineBackground.onFoldRegionsChanged(inlay.editor, severity)
}
fun hide() {
inlay.hide() inlay.hide()
lineBackground.hide(editor) lineBackground.hide(inlay.editor)
}
override fun toString(): String {
return "$inlay"
} }
companion object { companion object {
fun show(editor: Editor, info: HighlightInfo, settings: LensSettingsState): EditorLens? { fun show(editor: Editor, info: HighlightInfo, settings: LensSettingsState): EditorLens? {
val inlay = EditorLensInlay.show(editor, info, settings) ?: return null val inlay = EditorLensInlay.show(editor, info, settings) ?: return null
val lineBackground = EditorLensLineBackground.show(editor, info) val lineBackground = EditorLensLineBackground.show(editor, info)
return EditorLens(inlay, lineBackground) val severity = LensSeverity.from(info.severity)
return EditorLens(inlay, lineBackground, severity)
} }
} }
} }

View File

@@ -36,7 +36,9 @@ internal value class EditorLensInlay(private val inlay: Inlay<LensRenderer>) {
.disableSoftWrapping(true) .disableSoftWrapping(true)
.priority(priority) .priority(priority)
return editor.inlayModel.addAfterLineEndElement(offset, properties, renderer)?.let(::EditorLensInlay) return editor.inlayModel.addAfterLineEndElement(offset, properties, renderer)
?.also(renderer::setInlay)
?.let(::EditorLensInlay)
} }
/** /**

View File

@@ -2,33 +2,21 @@ package com.chylex.intellij.inspectionlens.editor.lens
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
import com.intellij.openapi.editor.ex.RangeHighlighterEx
import com.intellij.openapi.editor.markup.HighlighterLayer import com.intellij.openapi.editor.markup.HighlighterLayer
import com.intellij.openapi.editor.markup.HighlighterTargetArea.LINES_IN_RANGE import com.intellij.openapi.editor.markup.HighlighterTargetArea.LINES_IN_RANGE
import com.intellij.openapi.editor.markup.RangeHighlighter import com.intellij.openapi.editor.markup.RangeHighlighter
import com.intellij.openapi.editor.markup.TextAttributes
@JvmInline @JvmInline
internal value class EditorLensLineBackground(private val highlighter: RangeHighlighter) { internal value class EditorLensLineBackground(private val highlighter: RangeHighlighter) {
@Suppress("RedundantIf") val isInvalid
fun shouldRecreate(info: HighlightInfo): Boolean { get() = !highlighter.isValid
if (!highlighter.isValid) {
return true fun onFoldRegionsChanged(editor: Editor, severity: LensSeverity) {
if (highlighter is RangeHighlighterEx) {
highlighter.textAttributes = getAttributes(editor, highlighter.startOffset, highlighter.endOffset, severity)
} }
val severity = LensSeverity.from(info.severity)
val currentTextAttributes = highlighter.getTextAttributes(null)
val newTextAttributes = severity.lineAttributes
if (currentTextAttributes !== newTextAttributes) {
return true
}
val currentLayer = highlighter.layer
val newLayer = getHighlightLayer(severity)
if (currentLayer != newLayer) {
return true
}
return false
} }
fun hide(editor: Editor) { fun hide(editor: Editor) {
@@ -42,12 +30,20 @@ internal value class EditorLensLineBackground(private val highlighter: RangeHigh
val severity = LensSeverity.from(info.severity) val severity = LensSeverity.from(info.severity)
val layer = getHighlightLayer(severity) val layer = getHighlightLayer(severity)
val attributes = getAttributes(editor, startOffset, endOffset, severity)
return EditorLensLineBackground(editor.markupModel.addRangeHighlighter(startOffset, endOffset, layer, severity.lineAttributes, LINES_IN_RANGE)) return EditorLensLineBackground(editor.markupModel.addRangeHighlighter(startOffset, endOffset, layer, attributes, LINES_IN_RANGE))
} }
private fun getHighlightLayer(severity: LensSeverity): Int { private fun getHighlightLayer(severity: LensSeverity): Int {
return HighlighterLayer.CARET_ROW - 100 - severity.ordinal return HighlighterLayer.CARET_ROW - 100 - severity.ordinal
} }
private fun getAttributes(editor: Editor, startOffset: Int, endOffset: Int, severity: LensSeverity): TextAttributes? {
return if (editor.foldingModel.let { it.isOffsetCollapsed(startOffset) || it.isOffsetCollapsed(endOffset) })
null
else
severity.lineAttributes
}
} }
} }

View File

@@ -0,0 +1,58 @@
package com.chylex.intellij.inspectionlens.editor.lens
import com.intellij.codeInsight.daemon.impl.IntentionsUI
import com.intellij.codeInsight.hint.HintManager
import com.intellij.codeInsight.intention.impl.ShowIntentionActionsHandler
import com.intellij.lang.LangBundle
import com.intellij.openapi.actionSystem.ActionManager
import com.intellij.openapi.actionSystem.ActionPlaces
import com.intellij.openapi.actionSystem.IdeActions
import com.intellij.openapi.actionSystem.ex.ActionUtil
import com.intellij.openapi.editor.Editor
import com.intellij.openapi.project.Project
import com.intellij.psi.PsiDocumentManager
import com.intellij.psi.PsiFile
import com.intellij.psi.util.PsiUtilBase
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 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
}
}
private fun tryShowWithDefaultHandler(editor: Editor): Boolean {
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)
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)
}
}
}

View File

@@ -1,63 +1,199 @@
package com.chylex.intellij.inspectionlens.editor.lens package com.chylex.intellij.inspectionlens.editor.lens
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.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.openapi.editor.Editor import com.intellij.openapi.editor.Editor
import com.intellij.openapi.editor.Inlay import com.intellij.openapi.editor.Inlay
import com.intellij.openapi.editor.ScrollType
import com.intellij.openapi.editor.colors.EditorFontType
import com.intellij.openapi.editor.ex.EditorEx
import com.intellij.openapi.editor.impl.EditorImpl
import com.intellij.openapi.editor.markup.TextAttributes import com.intellij.openapi.editor.markup.TextAttributes
import com.intellij.openapi.util.text.StringUtil import com.intellij.openapi.util.text.StringUtil
import com.intellij.ui.paint.EffectPainter
import java.awt.Cursor
import java.awt.Graphics import java.awt.Graphics
import java.awt.Graphics2D
import java.awt.MouseInfo
import java.awt.Point
import java.awt.Rectangle import java.awt.Rectangle
import java.awt.event.MouseEvent
import java.util.regex.Pattern
import javax.swing.SwingUtilities
/** /**
* Renders the text of an inspection lens. * Renders the text of an inspection lens.
*/ */
class LensRenderer(info: HighlightInfo, settings: LensSettingsState) : HintRenderer(null) { class LensRenderer(private var info: HighlightInfo, private val settings: LensSettingsState) : HintRenderer(null), InputHandler {
private val useEditorFont = settings.useEditorFont private lateinit var inlay: Inlay<*>
private lateinit var severity: LensSeverity private lateinit var attributes: LensSeverityTextAttributes
private var extraRightPadding = 0
private var hovered = false
init { init {
setPropertiesFrom(info) setPropertiesFrom(info)
} }
fun setInlay(inlay: Inlay<*>) {
check(!this::inlay.isInitialized) { "Inlay already set" }
this.inlay = inlay
}
fun setPropertiesFrom(info: HighlightInfo) { fun setPropertiesFrom(info: HighlightInfo) {
text = getValidDescriptionText(info.description) this.info = info
severity = LensSeverity.from(info.severity) val description = getValidDescriptionText(info.description, settings.maxDescriptionLength)
text = description
attributes = LensSeverity.from(info.severity).textAttributes
extraRightPadding = if (description.lastOrNull() == '.') 2 else 0
} }
override fun paint(inlay: Inlay<*>, g: Graphics, r: Rectangle, textAttributes: TextAttributes) { override fun paint(inlay: Inlay<*>, g: Graphics, r: Rectangle, textAttributes: TextAttributes) {
fixBaselineForTextRendering(r) fixBaselineForTextRendering(r)
super.paint(inlay, g, r, textAttributes) super.paint(inlay, g, r, textAttributes)
if (hovered && isHoveringText()) {
paintHoverEffect(inlay, g, r)
}
}
private fun paintHoverEffect(inlay: Inlay<*>, g: Graphics, r: Rectangle) {
val editor = inlay.editor
if (editor !is EditorImpl) {
return
}
val font = editor.colorsScheme.getFont(EditorFontType.PLAIN)
val x = r.x + TEXT_HORIZONTAL_PADDING
val y = r.y + editor.ascent
val w = inlay.widthInPixels - UNDERLINE_WIDTH_REDUCTION - extraRightPadding
val h = editor.descent
g.color = attributes.foregroundColor
EffectPainter.LINE_UNDERSCORE.paint(g as Graphics2D, x, y, w, h, font)
} }
override fun getTextAttributes(editor: Editor): TextAttributes { override fun getTextAttributes(editor: Editor): TextAttributes {
return severity.textAttributes return attributes
} }
override fun useEditorFont(): Boolean { override fun useEditorFont(): Boolean {
return useEditorFont return settings.useEditorFont
}
override fun mouseMoved(event: MouseEvent, translated: Point) {
setHovered(isHoveringText(translated))
}
override fun mouseExited() {
setHovered(false)
}
private fun setHovered(hovered: Boolean) {
if (hovered && settings.lensHoverMode == LensHoverMode.DISABLED) {
return
}
if (this.hovered == hovered) {
return
}
this.hovered = hovered
val editor = inlay.editor
if (editor is EditorEx) {
val cursor = if (hovered) Cursor.getPredefinedCursor(Cursor.HAND_CURSOR) else null
editor.setCustomCursor(this::class.java, cursor)
}
inlay.repaint()
}
override fun mousePressed(event: MouseEvent, translated: Point) {
val hoverMode = settings.lensHoverMode
if (hoverMode == LensHoverMode.DISABLED || !isHoveringText(translated)) {
return
}
if (event.button.let { it == MouseEvent.BUTTON1 || it == MouseEvent.BUTTON2 }) {
event.consume()
val editor = inlay.editor
moveToOffset(editor, info.actualStartOffset)
if ((event.button == MouseEvent.BUTTON1) xor (hoverMode != LensHoverMode.DEFAULT)) {
IntentionsPopup.show(editor)
}
}
}
private fun isHoveringText(): Boolean {
val bounds = inlay.bounds ?: return false
val translatedPoint = MouseInfo.getPointerInfo().location.apply {
SwingUtilities.convertPointFromScreen(this, inlay.editor.contentComponent)
translate(-bounds.x, -bounds.y)
}
return isHoveringText(translatedPoint)
}
private fun isHoveringText(point: Point): Boolean {
return point.x >= HOVER_HORIZONTAL_PADDING
&& point.y >= 4
&& point.x < inlay.widthInPixels - HOVER_HORIZONTAL_PADDING - extraRightPadding
&& point.y < inlay.heightInPixels - 1
} }
private companion object { private companion object {
private fun getValidDescriptionText(text: String?): String { /**
return if (text.isNullOrBlank()) " " else addMissingPeriod(unescapeHtmlEntities(text)) * [HintRenderer.paintHint] renders padding around text, but not around effects.
*/
private const val TEXT_HORIZONTAL_PADDING = 7
private const val HOVER_HORIZONTAL_PADDING = TEXT_HORIZONTAL_PADDING - 2
private const val UNDERLINE_WIDTH_REDUCTION = (TEXT_HORIZONTAL_PADDING * 2) - 1
/**
* Kotlin compiler inspections have an `[UPPERCASE_TAG]` at the beginning.
*/
private val UPPERCASE_TAG_REGEX = Pattern.compile("^\\[[A-Z_]+] ")
private fun getValidDescriptionText(text: String?, maxLength: Int): String {
return if (text.isNullOrBlank()) " " else addEllipsisOrMissingPeriod(unescapeHtmlEntities(stripUppercaseTag(text)), maxLength)
} }
private fun unescapeHtmlEntities(potentialHtml: String): String { private fun stripUppercaseTag(text: String): String {
return potentialHtml.ifContains('&', StringUtil::unescapeXmlEntities) if (text.startsWith('[')) {
val matcher = UPPERCASE_TAG_REGEX.matcher(text)
if (matcher.find()) {
return text.substring(matcher.end())
}
}
return text
} }
private fun addMissingPeriod(text: String): String { private fun unescapeHtmlEntities(text: String): String {
return if (text.endsWith('.')) text else "$text." return if (text.contains('&')) StringUtil.unescapeXmlEntities(text) else text
} }
private inline fun String.ifContains(charToTest: Char, action: (String) -> String): String { private fun addEllipsisOrMissingPeriod(text: String, maxLength: Int): String {
return if (this.contains(charToTest)) action(this) else this return when {
text.length > maxLength -> text.take(maxLength).trimEnd { it.isWhitespace() || it == '.' } + ""
!text.endsWith('.') -> "$text."
else -> text
}
} }
private fun fixBaselineForTextRendering(rect: Rectangle) { private fun fixBaselineForTextRendering(rect: Rectangle) {
rect.y += 1 rect.y += 1
} }
private fun moveToOffset(editor: Editor, offset: Int) {
editor.caretModel.moveToOffset(offset)
editor.scrollingModel.scrollToCaret(ScrollType.MAKE_VISIBLE)
}
} }
} }

View File

@@ -10,14 +10,18 @@ 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
import com.intellij.ui.SimpleListCellRenderer
import com.intellij.ui.components.JBCheckBox import com.intellij.ui.components.JBCheckBox
import com.intellij.ui.dsl.builder.Cell import com.intellij.ui.dsl.builder.Cell
import com.intellij.ui.dsl.builder.RightGap import com.intellij.ui.dsl.builder.RightGap
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 java.awt.Cursor import java.awt.Cursor
@@ -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 = SimpleListCellRenderer.create("", LensHoverMode::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,12 +113,19 @@ 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> {

View File

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

View File

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

View File

@@ -6,12 +6,34 @@
<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>
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.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>
<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>
<b>Version 1.5</b>
<ul>
<li>Added possibility to left-click an inspection to show quick fixes.</li>
<li>Added possibility to middle-click an inspection to navigate to relevant code in the editor.</li>
<li>Added option to use UI font instead of editor font.</li>
<li>Long inspection descriptions are now truncated to 120 characters.</li>
<li>Improved descriptions of Kotlin compiler inspections.</li>
<li>Fixed visual artifacts in Rendered Doc comments.</li>
</ul>
<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>

View File

@@ -0,0 +1,12 @@
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)
}
}