1
0
mirror of https://github.com/chylex/IntelliJ-Inspection-Lens.git synced 2025-11-03 17:40:12 +01:00

1 Commits

Author SHA1 Message Date
519d80ed6e WIP 2024-06-12 12:39:00 +02:00
43 changed files with 431 additions and 995 deletions

2
.github/FUNDING.yml vendored
View File

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

2
.gitignore vendored
View File

@@ -1,7 +1,5 @@
/.idea/* /.idea/*
!/.idea/dictionaries
!/.idea/runConfigurations !/.idea/runConfigurations
/.gradle/ /.gradle/
/.intellijPlatform/
/build/ /build/

View File

@@ -1,7 +0,0 @@
<component name="ProjectDictionaryState">
<dictionary name="default.user">
<words>
<w>inspectionlens</w>
</words>
</dictionary>
</component>

View File

@@ -1,10 +1,10 @@
# Inspection Lens <img align="right" src="logo.png" alt="Plugin Logo"> # Inspection Lens <img align="right" src="logo.png" alt="Plugin Logo">
Displays errors, warnings, and other inspections inline. Highlights the background of lines with inspections. Supports light and dark themes out of the box. IntelliJ plugin that shows errors, warnings, and other inspection highlights inline.
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. After installing the plugin, inspection descriptions will appear after the ends of lines, and the lines will be highlighted with a background color. Shown inspection severities are **Errors**, **Warnings**, **Weak Warnings**, **Server Problems**, **Grammar Errors**, **Typos**, and other inspections from plugins or future IntelliJ versions that have a high enough severity level. Each severity has a different color, with support for both light and dark themes.
Configure appearance, behavior of clicking on inspections, and visible severities in **Settings | Tools | Inspection Lens**. Note: The plugin is not customizable outside the ability to disable/enable the plugin without restarting the IDE. If the defaults don't work for you, I recommend trying the [Inline Error](https://plugins.jetbrains.com/plugin/17302-inlineerror) plugin which can be customized, building your own version of Inspection Lens, or proposing your change in the [issue tracker](https://github.com/chylex/IntelliJ-Inspection-Lens/issues).
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,59 +1,48 @@
@file:Suppress("ConvertLambdaToReference") @file:Suppress("ConvertLambdaToReference")
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
plugins { plugins {
kotlin("jvm") kotlin("jvm") version "1.8.0"
id("org.jetbrains.intellij.platform") id("org.jetbrains.intellij") version "1.17.0"
} }
group = "com.chylex.intellij.inspectionlens" group = "com.chylex.intellij.inspectionlens"
version = "1.6.0" version = "1.3.3"
repositories { repositories {
mavenCentral() mavenCentral()
intellijPlatform {
defaultRepositories()
}
} }
dependencies { intellij {
intellijPlatform { version.set("2023.3.3")
intellijIdeaUltimate("2024.2") updateSinceUntilBuild.set(false)
bundledPlugin("tanvd.grazi")
}
testImplementation("org.junit.jupiter:junit-jupiter:5.9.2") plugins.add("tanvd.grazi")
}
intellijPlatform {
pluginConfiguration {
ideaVersion {
sinceBuild.set("242")
untilBuild.set(provider { null })
}
}
pluginVerification {
freeArgs.add("-mute")
freeArgs.add("TemplateWordInPluginId")
ides {
recommended()
}
}
} }
kotlin { kotlin {
jvmToolchain(17) jvmToolchain(17)
}
compilerOptions { dependencies {
freeCompilerArgs = listOf( testImplementation("org.junit.jupiter:junit-jupiter:5.9.2")
"-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.12-bin.zip distributionUrl=https\://services.gradle.org/distributions/gradle-8.4-bin.zip
networkTimeout=10000 networkTimeout=10000
validateDistributionUrl=true validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME zipStoreBase=GRADLE_USER_HOME

6
gradlew vendored
View File

@@ -15,8 +15,6 @@
# 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
#
############################################################################## ##############################################################################
# #
@@ -57,7 +55,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/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt # https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/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/.
@@ -86,7 +84,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 -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || 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,8 +13,6 @@
@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 ##########################################################################
@@ -45,11 +43,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. 1>&2 echo.
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
echo. 1>&2 echo.
echo Please set the JAVA_HOME variable in your environment to match the 1>&2 echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation. 1>&2 echo location of your Java installation.
goto fail goto fail
@@ -59,11 +57,11 @@ set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto execute if exist "%JAVA_EXE%" goto execute
echo. 1>&2 echo.
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
echo. 1>&2 echo.
echo Please set the JAVA_HOME variable in your environment to match the 1>&2 echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation. 1>&2 echo location of your Java installation.
goto fail goto fail

View File

@@ -1,8 +1 @@
rootProject.name = "InspectionLens" rootProject.name = "InspectionLens"
pluginManagement {
plugins {
kotlin("jvm") version "1.9.24" // https://plugins.jetbrains.com/docs/intellij/using-kotlin.html#bundled-stdlib-versions
id("org.jetbrains.intellij.platform") version "2.6.0" // https://github.com/JetBrains/intellij-platform-gradle-plugin/releases
}
}

View File

@@ -1,12 +1,15 @@
package com.chylex.intellij.inspectionlens package com.chylex.intellij.inspectionlens
import com.chylex.intellij.inspectionlens.editor.EditorLensFeatures import com.chylex.intellij.inspectionlens.editor.EditorLensManager
import com.chylex.intellij.inspectionlens.editor.LensMarkupModelListener
import com.intellij.openapi.Disposable
import com.intellij.openapi.application.ApplicationManager import com.intellij.openapi.application.ApplicationManager
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 import com.intellij.openapi.rd.createLifetime
import com.intellij.openapi.rd.createNestedDisposable
import com.jetbrains.rd.util.lifetime.Lifetime
/** /**
* Handles installation and uninstallation of plugin features in editors. * Handles installation and uninstallation of plugin features in editors.
@@ -14,40 +17,45 @@ import java.util.concurrent.atomic.AtomicBoolean
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) LensMarkupModelListener.register(editor.editor, createEditorDisposable(editor))
} }
/** /**
* Installs lenses into all open editors. * Installs lenses into all open editors.
*/ */
fun install() { fun install() {
forEachOpenEditor(EditorLensFeatures::install) forEachOpenEditor(::install)
}
/**
* Uninstalls lenses from all open editors.
*/
fun uninstall() {
forEachOpenEditor {
EditorLensManager.remove(it.editor)
}
} }
/** /**
* Refreshes lenses in all open editors. * Refreshes lenses in all open editors.
*/ */
private fun refresh() { fun refresh() {
forEachOpenEditor(EditorLensFeatures::refresh) forEachOpenEditor {
LensMarkupModelListener.refresh(it.editor)
}
} }
/** /**
* Schedules a refresh of lenses in all open editors. * Creates a [Disposable] that will be disposed when either the [TextEditor] is disposed or the plugin is unloaded.
*/ */
fun scheduleRefresh() { private fun createEditorDisposable(textEditor: TextEditor): Disposable {
Refresh.schedule() val pluginLifetime = ApplicationManager.getApplication().getService(InspectionLensPluginDisposableService::class.java).createLifetime()
val editorLifetime = textEditor.createLifetime()
return Lifetime.intersect(pluginLifetime, editorLifetime).createNestedDisposable("InspectionLensIntersectedLifetime")
} }
/** /**
@@ -56,32 +64,10 @@ 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) { for (project in projectManager.openProjects.filterNot { it.isDisposed }) {
if (project.isDisposed) { for (editor in FileEditorManager.getInstance(project).allEditors.filterIsInstance<TextEditor>()) {
continue
}
for (editor in FileEditorManager.getInstance(project).allEditors) {
if (editor is TextEditor) {
action(editor) action(editor)
} }
} }
} }
}
private object Refresh : Runnable {
private val needsRefresh = AtomicBoolean(false)
fun schedule() {
if (needsRefresh.compareAndSet(false, true)) {
ApplicationManager.getApplication().invokeLater(this)
}
}
override fun run() {
if (needsRefresh.compareAndSet(true, false)) {
refresh()
}
}
}
} }

View File

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

View File

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

View File

@@ -2,21 +2,11 @@ package com.chylex.intellij.inspectionlens
import com.intellij.openapi.Disposable import com.intellij.openapi.Disposable
import com.intellij.openapi.components.Service import com.intellij.openapi.components.Service
import com.intellij.openapi.rd.createLifetime
import com.intellij.openapi.rd.createNestedDisposable
import com.jetbrains.rd.util.lifetime.Lifetime
/** /**
* Gets automatically disposed when the plugin is unloaded. * Gets automatically disposed when the plugin is unloaded.
*/ */
@Service @Service
class InspectionLensPluginDisposableService : Disposable { class InspectionLensPluginDisposableService : Disposable {
/**
* Creates a [Disposable] that will be disposed when either plugin is unloaded, or the [other] [Disposable] is disposed.
*/
fun intersect(other: Disposable): Disposable {
return Lifetime.intersect(createLifetime(), other.createLifetime()).createNestedDisposable("InspectionLensIntersectedLifetime")
}
override fun dispose() {} override fun dispose() {}
} }

View File

@@ -4,7 +4,7 @@ import com.intellij.ide.plugins.DynamicPluginListener
import com.intellij.ide.plugins.IdeaPluginDescriptor import com.intellij.ide.plugins.IdeaPluginDescriptor
/** /**
* Installs [InspectionLens] in open editors when the plugin is loaded. * Installs [InspectionLens] in open editors when the plugin is loaded, and uninstalls it when the plugin is unloaded.
*/ */
class InspectionLensPluginListener : DynamicPluginListener { class InspectionLensPluginListener : DynamicPluginListener {
override fun pluginLoaded(pluginDescriptor: IdeaPluginDescriptor) { override fun pluginLoaded(pluginDescriptor: IdeaPluginDescriptor) {
@@ -12,4 +12,10 @@ class InspectionLensPluginListener : DynamicPluginListener {
InspectionLens.install() InspectionLens.install()
} }
} }
override fun beforePluginUnload(pluginDescriptor: IdeaPluginDescriptor, isUpdate: Boolean) {
if (pluginDescriptor.pluginId.idString == InspectionLens.PLUGIN_ID) {
InspectionLens.uninstall()
}
}
} }

View File

@@ -0,0 +1,25 @@
package com.chylex.intellij.inspectionlens
import com.intellij.openapi.application.ApplicationManager
object InspectionLensRefresher {
private var needsRefresh = false
fun scheduleRefresh() {
synchronized(this) {
if (!needsRefresh) {
needsRefresh = true
ApplicationManager.getApplication().invokeLater(::refresh)
}
}
}
private fun refresh() {
synchronized(this) {
if (needsRefresh) {
needsRefresh = false
InspectionLens.refresh()
}
}
}
}

View File

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

View File

@@ -1,6 +1,6 @@
package com.chylex.intellij.inspectionlens.compatibility package com.chylex.intellij.inspectionlens.compatibility
import com.chylex.intellij.inspectionlens.editor.lens.LensSeverity import com.chylex.intellij.inspectionlens.editor.LensSeverity
import com.intellij.grazie.ide.TextProblemSeverities import com.intellij.grazie.ide.TextProblemSeverities
import com.intellij.openapi.project.Project import com.intellij.openapi.project.Project
import com.intellij.openapi.startup.ProjectActivity import com.intellij.openapi.startup.ProjectActivity

View File

@@ -1,12 +1,12 @@
package com.chylex.intellij.inspectionlens.compatibility package com.chylex.intellij.inspectionlens.compatibility
import com.chylex.intellij.inspectionlens.editor.lens.LensSeverity import com.chylex.intellij.inspectionlens.editor.LensSeverity
import com.intellij.lang.annotation.HighlightSeverity import com.intellij.lang.annotation.HighlightSeverity
import com.intellij.openapi.diagnostic.logger import com.intellij.openapi.diagnostic.logger
import com.intellij.profile.codeInspection.InspectionProfileManager import com.intellij.profile.codeInspection.InspectionProfileManager
import com.intellij.spellchecker.SpellCheckerSeveritiesProvider import com.intellij.spellchecker.SpellCheckerSeveritiesProvider
internal object SpellCheckerSupport { object SpellCheckerSupport {
private val log = logger<SpellCheckerSupport>() private val log = logger<SpellCheckerSupport>()
fun load() { fun load() {

View File

@@ -0,0 +1,36 @@
package com.chylex.intellij.inspectionlens.editor
import com.intellij.codeInsight.daemon.impl.HighlightInfo
import com.intellij.openapi.editor.Editor
internal class EditorLens private constructor(private var inlay: EditorLensInlay, private var lineBackground: EditorLensLineBackground) {
fun update(info: HighlightInfo): Boolean {
val editor = inlay.editor
if (!inlay.tryUpdate(info)) {
inlay = EditorLensInlay.show(editor, info) ?: return false
}
if (lineBackground.shouldRecreate(info)) {
lineBackground.hide(editor)
lineBackground = EditorLensLineBackground.show(editor, info)
}
return true
}
fun hide() {
val editor = inlay.editor
inlay.hide()
lineBackground.hide(editor)
}
companion object {
fun show(editor: Editor, info: HighlightInfo): EditorLens? {
val inlay = EditorLensInlay.show(editor, info) ?: return null
val lineBackground = EditorLensLineBackground.show(editor, info)
return EditorLens(inlay, lineBackground)
}
}
}

View File

@@ -1,62 +0,0 @@
package com.chylex.intellij.inspectionlens.editor
import com.chylex.intellij.inspectionlens.InspectionLensPluginDisposableService
import com.intellij.openapi.Disposable
import com.intellij.openapi.components.service
import com.intellij.openapi.editor.Editor
import com.intellij.openapi.editor.ex.FoldingModelEx
import com.intellij.openapi.editor.ex.MarkupModelEx
import com.intellij.openapi.editor.impl.DocumentMarkupModel
import com.intellij.openapi.fileEditor.TextEditor
import com.intellij.openapi.util.Disposer
import com.intellij.openapi.util.Key
/**
* Manages Inspection Lens features for a single [Editor].
*/
internal class EditorLensFeatures private constructor(
editor: Editor,
private val markupModel: MarkupModelEx,
foldingModel: FoldingModelEx?,
disposable: Disposable
) {
private val lensManager = EditorLensManager(editor)
private val lensManagerDispatcher = EditorLensManagerDispatcher(lensManager)
private val markupModelListener = LensMarkupModelListener(lensManagerDispatcher)
init {
markupModel.addMarkupModelListener(disposable, markupModelListener)
markupModelListener.showAllValid(markupModel.allHighlighters)
foldingModel?.addListener(LensFoldingModelListener(lensManager), disposable)
}
private fun refresh() {
markupModelListener.hideAll()
markupModelListener.showAllValid(markupModel.allHighlighters)
}
companion object {
private val EDITOR_KEY = Key<EditorLensFeatures>(EditorLensFeatures::class.java.name)
fun install(owner: TextEditor) {
val editor = owner.editor
if (editor.getUserData(EDITOR_KEY) != null) {
return
}
val markupModel = DocumentMarkupModel.forDocument(editor.document, editor.project, false) as? MarkupModelEx ?: return
val foldingModel = editor.foldingModel as? FoldingModelEx
val disposable = service<InspectionLensPluginDisposableService>().intersect(owner)
val features = EditorLensFeatures(editor, markupModel, foldingModel, disposable)
editor.putUserData(EDITOR_KEY, features)
Disposer.register(disposable) { editor.putUserData(EDITOR_KEY, null) }
}
fun refresh(owner: TextEditor) {
owner.editor.getUserData(EDITOR_KEY)?.refresh()
}
}
}

View File

@@ -1,6 +1,5 @@
package com.chylex.intellij.inspectionlens.editor.lens package com.chylex.intellij.inspectionlens.editor
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
import com.intellij.openapi.editor.Inlay import com.intellij.openapi.editor.Inlay
@@ -26,19 +25,14 @@ 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, info: HighlightInfo): EditorLensInlay? {
val offset = getInlayHintOffset(info) val offset = getInlayHintOffset(info)
val priority = getInlayHintPriority(editor, info) val priority = getInlayHintPriority(editor, info)
val renderer = LensRenderer(info, settings)
val properties = InlayProperties() val renderer = LensRenderer(info)
.relatesToPrecedingText(true) val properties = InlayProperties().relatesToPrecedingText(true).disableSoftWrapping(true).priority(priority)
.disableSoftWrapping(true)
.priority(priority)
return editor.inlayModel.addAfterLineEndElement(offset, properties, renderer) return editor.inlayModel.addAfterLineEndElement(offset, properties, renderer)?.let(::EditorLensInlay)
?.also(renderer::setInlay)
?.let(::EditorLensInlay)
} }
/** /**

View File

@@ -1,22 +1,34 @@
package com.chylex.intellij.inspectionlens.editor.lens package com.chylex.intellij.inspectionlens.editor
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) {
val isInvalid @Suppress("RedundantIf")
get() = !highlighter.isValid fun shouldRecreate(info: HighlightInfo): Boolean {
if (!highlighter.isValid) {
fun onFoldRegionsChanged(editor: Editor, severity: LensSeverity) { return true
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) {
@@ -30,20 +42,12 @@ 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, attributes, LINES_IN_RANGE)) return EditorLensLineBackground(editor.markupModel.addRangeHighlighter(startOffset, endOffset, layer, severity.lineAttributes, 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

@@ -1,39 +1,49 @@
package com.chylex.intellij.inspectionlens.editor package com.chylex.intellij.inspectionlens.editor
import com.chylex.intellij.inspectionlens.editor.lens.EditorLens
import com.chylex.intellij.inspectionlens.settings.LensSettingsState
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
import com.intellij.openapi.util.Key
import java.util.IdentityHashMap import java.util.IdentityHashMap
/** /**
* Manages visible inspection lenses for an [Editor]. * Manages visible inspection lenses for an [Editor].
*/ */
internal class EditorLensManager(private val editor: Editor) { class EditorLensManager private constructor(private val editor: Editor) {
private val lenses = IdentityHashMap<RangeHighlighter, EditorLens>() companion object {
private val settings = service<LensSettingsState>() private val EDITOR_KEY = Key<EditorLensManager>(EditorLensManager::class.java.name)
private fun show(highlighterWithInfo: HighlighterWithInfo) { fun getOrCreate(editor: Editor): EditorLensManager {
if (editor.isDisposed) { return editor.getUserData(EDITOR_KEY) ?: EditorLensManager(editor).also { editor.putUserData(EDITOR_KEY, it) }
return
} }
fun remove(editor: Editor) {
val manager = editor.getUserData(EDITOR_KEY)
if (manager != null) {
manager.hideAll()
editor.putUserData(EDITOR_KEY, null)
}
}
}
private val lenses = IdentityHashMap<RangeHighlighter, EditorLens>()
private fun show(highlighterWithInfo: HighlighterWithInfo) {
val (highlighter, info) = highlighterWithInfo val (highlighter, info) = highlighterWithInfo
if (!highlighter.isValid) { if (!highlighter.isValid) {
return return
} }
val existingLens = lenses[highlighter] val existingLens = lenses[highlighter]
if (existingLens != null) { if (existingLens != null) {
if (existingLens.update(info, settings)) { if (existingLens.update(info)) {
return return
} }
existingLens.hide() existingLens.hide()
} }
val newLens = EditorLens.show(editor, info, settings) val newLens = EditorLens.show(editor, info)
if (newLens != null) { if (newLens != null) {
lenses[highlighter] = newLens lenses[highlighter] = newLens
} }
@@ -59,10 +69,6 @@ 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)
@@ -77,12 +83,6 @@ internal class EditorLensManager(private val editor: Editor) {
lensManager.hide(highlighter) lensManager.hide(highlighter)
} }
} }
object HideAll : Command {
override fun apply(lensManager: EditorLensManager) {
lensManager.hideAll()
}
}
} }
/** /**

View File

@@ -3,7 +3,7 @@ package com.chylex.intellij.inspectionlens.editor
import com.intellij.openapi.application.ApplicationManager import com.intellij.openapi.application.ApplicationManager
import com.intellij.openapi.editor.markup.RangeHighlighter import com.intellij.openapi.editor.markup.RangeHighlighter
internal class EditorLensManagerDispatcher(private val lensManager: EditorLensManager) { class EditorLensManagerDispatcher(private val lensManager: EditorLensManager) {
private var queuedItems = mutableListOf<EditorLensManager.Command>() private var queuedItems = mutableListOf<EditorLensManager.Command>()
private var isEnqueued = false private var isEnqueued = false
@@ -15,10 +15,6 @@ internal class EditorLensManagerDispatcher(private val lensManager: EditorLensMa
enqueue(EditorLensManager.Command.Hide(highlighter)) enqueue(EditorLensManager.Command.Hide(highlighter))
} }
fun hideAll() {
enqueue(EditorLensManager.Command.HideAll)
}
private fun enqueue(item: EditorLensManager.Command) { private fun enqueue(item: EditorLensManager.Command) {
synchronized(this) { synchronized(this) {
queuedItems.add(item) queuedItems.add(item)

View File

@@ -1,12 +0,0 @@
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,19 +1,23 @@
package com.chylex.intellij.inspectionlens.editor package com.chylex.intellij.inspectionlens.editor
import com.chylex.intellij.inspectionlens.InspectionLens import com.chylex.intellij.inspectionlens.settings.LensSettings
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.UpdateHighlightersUtil.isFileLevelOrGutterAnnotation import com.intellij.openapi.Disposable
import com.intellij.openapi.components.service import com.intellij.openapi.editor.Editor
import com.intellij.openapi.editor.ex.MarkupModelEx
import com.intellij.openapi.editor.ex.RangeHighlighterEx import com.intellij.openapi.editor.ex.RangeHighlighterEx
import com.intellij.openapi.editor.impl.DocumentMarkupModel
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
import com.intellij.openapi.util.Disposer
import com.intellij.openapi.util.Key
/** /**
* 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 constructor(editor: Editor) : MarkupModelListener {
private val settings = service<LensSettingsState>() private val lensManagerDispatcher = EditorLensManagerDispatcher(EditorLensManager.getOrCreate(editor))
private val severityFilter = LensSettings.createSeverityFilter()
override fun afterAdded(highlighter: RangeHighlighterEx) { override fun afterAdded(highlighter: RangeHighlighterEx) {
showIfValid(highlighter) showIfValid(highlighter)
@@ -23,37 +27,14 @@ internal class LensMarkupModelListener(private val lensManagerDispatcher: Editor
showIfValid(highlighter) showIfValid(highlighter)
} }
fun showAllValid(highlighters: Array<RangeHighlighter>) { override fun beforeRemoved(highlighter: RangeHighlighterEx) {
if (InspectionLens.SHOW_LENSES) { if (getFilteredHighlightInfo(highlighter) != null) {
highlighters.forEach(::showIfValid) lensManagerDispatcher.hide(highlighter)
} }
} }
private fun showIfValid(highlighter: RangeHighlighter) { private fun showIfValid(highlighter: RangeHighlighter) {
if (!InspectionLens.SHOW_LENSES) { runWithHighlighterIfValid(highlighter, lensManagerDispatcher::show, ::showAsynchronously)
return
}
val info = highlighter.takeIf { it.isValid }?.let(::getFilteredHighlightInfo)
if (info == null || isFileLevelOrGutterAnnotation(info)) {
return
}
val highlighterWithInfo = HighlighterWithInfo.from(highlighter, info)
processHighlighterWithInfo(highlighterWithInfo, lensManagerDispatcher::show, ::showAsynchronously)
}
private fun getFilteredHighlightInfo(highlighter: RangeHighlighter): HighlightInfo? {
return HighlightInfo.fromRangeHighlighter(highlighter)?.takeIf { settings.severityFilter.test(it.severity) }
}
private inline fun processHighlighterWithInfo(highlighterWithInfo: HighlighterWithInfo, actionForImmediate: (HighlighterWithInfo) -> Unit, actionForAsync: (HighlighterWithInfo.Async) -> Unit) {
if (highlighterWithInfo is HighlighterWithInfo.Async) {
actionForAsync(highlighterWithInfo)
}
else if (highlighterWithInfo.hasDescription) {
actionForImmediate(highlighterWithInfo)
}
} }
private fun showAsynchronously(highlighterWithInfo: HighlighterWithInfo.Async) { private fun showAsynchronously(highlighterWithInfo: HighlighterWithInfo.Async) {
@@ -64,13 +45,67 @@ internal class LensMarkupModelListener(private val lensManagerDispatcher: Editor
} }
} }
override fun beforeRemoved(highlighter: RangeHighlighterEx) { private fun showAllValid(highlighters: Array<RangeHighlighter>) {
if (getFilteredHighlightInfo(highlighter) != null) { highlighters.forEach(::showIfValid)
lensManagerDispatcher.hide(highlighter) }
private fun getFilteredHighlightInfo(highlighter: RangeHighlighter): HighlightInfo? {
return HighlightInfo.fromRangeHighlighter(highlighter)?.takeIf { severityFilter.test(it.severity) }
}
private inline fun runWithHighlighterIfValid(highlighter: RangeHighlighter, actionForImmediate: (HighlighterWithInfo) -> Unit, actionForAsync: (HighlighterWithInfo.Async) -> Unit) {
val info = getHighlightInfoIfValid(highlighter)
if (info != null) {
processHighlighterWithInfo(HighlighterWithInfo.from(highlighter, info), actionForImmediate, actionForAsync)
} }
} }
fun hideAll() { private fun getHighlightInfoIfValid(highlighter: RangeHighlighter): HighlightInfo? {
lensManagerDispatcher.hideAll() return highlighter.takeIf { it.isValid }?.let(::getFilteredHighlightInfo)
}
companion object {
private val EDITOR_KEY = Key<LensMarkupModelListener>(LensMarkupModelListener::class.java.name)
private inline fun processHighlighterWithInfo(highlighterWithInfo: HighlighterWithInfo, actionForImmediate: (HighlighterWithInfo) -> Unit, actionForAsync: (HighlighterWithInfo.Async) -> Unit) {
if (highlighterWithInfo is HighlighterWithInfo.Async) {
actionForAsync(highlighterWithInfo)
}
else if (highlighterWithInfo.hasDescription) {
actionForImmediate(highlighterWithInfo)
}
}
private fun getMarkupModel(editor: Editor): MarkupModelEx? {
return DocumentMarkupModel.forDocument(editor.document, editor.project, false) as? MarkupModelEx
}
/**
* Attaches a new [LensMarkupModelListener] to the [Editor], and reports all existing inspection highlights to [EditorLensManager].
*/
fun register(editor: Editor, disposable: Disposable) {
if (editor.getUserData(EDITOR_KEY) != null) {
return
}
val markupModel = getMarkupModel(editor) ?: return
val listener = LensMarkupModelListener(editor)
editor.putUserData(EDITOR_KEY, listener)
Disposer.register(disposable) { editor.putUserData(EDITOR_KEY, null) }
markupModel.addMarkupModelListener(disposable, listener)
listener.showAllValid(markupModel.allHighlighters)
}
/**
* Recreates all inspection highlights in the [Editor].
*/
fun refresh(editor: Editor) {
val listener = editor.getUserData(EDITOR_KEY) ?: return
val markupModel = getMarkupModel(editor) ?: return
listener.showAllValid(markupModel.allHighlighters)
}
} }
} }

View File

@@ -0,0 +1,61 @@
package com.chylex.intellij.inspectionlens.editor
import com.intellij.codeInsight.daemon.impl.HighlightInfo
import com.intellij.codeInsight.daemon.impl.HintRenderer
import com.intellij.openapi.editor.Editor
import com.intellij.openapi.editor.Inlay
import com.intellij.openapi.editor.markup.TextAttributes
import com.intellij.openapi.util.text.StringUtil
import java.awt.Graphics
import java.awt.Rectangle
/**
* Renders the text of an inspection lens.
*/
class LensRenderer(info: HighlightInfo) : HintRenderer(null) {
private lateinit var severity: LensSeverity
init {
setPropertiesFrom(info)
}
fun setPropertiesFrom(info: HighlightInfo) {
text = getValidDescriptionText(info.description)
severity = LensSeverity.from(info.severity)
}
override fun paint(inlay: Inlay<*>, g: Graphics, r: Rectangle, textAttributes: TextAttributes) {
fixBaselineForTextRendering(r)
super.paint(inlay, g, r, textAttributes)
}
override fun getTextAttributes(editor: Editor): TextAttributes {
return severity.textAttributes
}
override fun useEditorFont(): Boolean {
return true
}
private companion object {
private fun getValidDescriptionText(text: String?): String {
return if (text.isNullOrBlank()) " " else addMissingPeriod(unescapeHtmlEntities(text))
}
private fun unescapeHtmlEntities(potentialHtml: String): String {
return potentialHtml.ifContains('&', StringUtil::unescapeXmlEntities)
}
private fun addMissingPeriod(text: String): String {
return if (text.endsWith('.')) text else "$text."
}
private inline fun String.ifContains(charToTest: Char, action: (String) -> String): String {
return if (this.contains(charToTest)) action(this) else this
}
private fun fixBaselineForTextRendering(rect: Rectangle) {
rect.y += 1
}
}
}

View File

@@ -1,6 +1,6 @@
package com.chylex.intellij.inspectionlens.editor.lens package com.chylex.intellij.inspectionlens.editor
import com.chylex.intellij.inspectionlens.InspectionLens import com.chylex.intellij.inspectionlens.InspectionLensRefresher
import com.chylex.intellij.inspectionlens.compatibility.SpellCheckerSupport import com.chylex.intellij.inspectionlens.compatibility.SpellCheckerSupport
import com.intellij.lang.annotation.HighlightSeverity import com.intellij.lang.annotation.HighlightSeverity
import com.intellij.ui.ColorUtil import com.intellij.ui.ColorUtil
@@ -54,7 +54,7 @@ enum class LensSeverity(baseColor: Color, lightThemeDarkening: Int, darkThemeBri
*/ */
internal fun registerMapping(severity: HighlightSeverity, lensSeverity: LensSeverity) { internal fun registerMapping(severity: HighlightSeverity, lensSeverity: LensSeverity) {
if (mapping.put(severity, lensSeverity) != lensSeverity) { if (mapping.put(severity, lensSeverity) != lensSeverity) {
InspectionLens.scheduleRefresh() InspectionLensRefresher.scheduleRefresh()
} }
} }

View File

@@ -0,0 +1,14 @@
package com.chylex.intellij.inspectionlens.editor
import com.intellij.lang.annotation.HighlightSeverity
import java.util.function.Predicate
class LensSeverityFilter(private val disabledSeverities: Set<String>) : Predicate<HighlightSeverity> {
override fun test(severity: HighlightSeverity): Boolean {
return severity.myVal >= MINIMUM_SEVERITY && severity.name !in disabledSeverities
}
companion object {
val MINIMUM_SEVERITY = HighlightSeverity.TEXT_ATTRIBUTES.myVal + 1
}
}

View File

@@ -1,4 +1,4 @@
package com.chylex.intellij.inspectionlens.editor.lens package com.chylex.intellij.inspectionlens.editor
import com.intellij.openapi.editor.markup.UnmodifiableTextAttributes import com.intellij.openapi.editor.markup.UnmodifiableTextAttributes
import com.intellij.ui.JBColor import com.intellij.ui.JBColor

View File

@@ -1,43 +0,0 @@
package com.chylex.intellij.inspectionlens.editor.lens
import com.chylex.intellij.inspectionlens.settings.LensSettingsState
import com.intellij.codeInsight.daemon.impl.HighlightInfo
import com.intellij.openapi.editor.Editor
internal class EditorLens private constructor(private var inlay: EditorLensInlay, private var lineBackground: EditorLensLineBackground, private var severity: LensSeverity) {
fun update(info: HighlightInfo, settings: LensSettingsState): Boolean {
val editor = inlay.editor
val oldSeverity = severity
severity = LensSeverity.from(info.severity)
if (!inlay.tryUpdate(info)) {
inlay = EditorLensInlay.show(editor, info, settings) ?: return false
}
if (lineBackground.isInvalid || oldSeverity != severity) {
lineBackground.hide(editor)
lineBackground = EditorLensLineBackground.show(editor, info)
}
return true
}
fun onFoldRegionsChanged() {
lineBackground.onFoldRegionsChanged(inlay.editor, severity)
}
fun hide() {
inlay.hide()
lineBackground.hide(inlay.editor)
}
companion object {
fun show(editor: Editor, info: HighlightInfo, settings: LensSettingsState): EditorLens? {
val inlay = EditorLensInlay.show(editor, info, settings) ?: return null
val lineBackground = EditorLensLineBackground.show(editor, info)
val severity = LensSeverity.from(info.severity)
return EditorLens(inlay, lineBackground, severity)
}
}
}

View File

@@ -1,132 +0,0 @@
package com.chylex.intellij.inspectionlens.editor.lens
import com.chylex.intellij.inspectionlens.InspectionLens
import com.intellij.codeInsight.daemon.impl.HighlightInfo
import com.intellij.codeInsight.daemon.impl.HighlightInfo.IntentionActionDescriptor
import com.intellij.codeInsight.daemon.impl.IntentionsUI
import com.intellij.codeInsight.daemon.impl.ShowIntentionsPass.IntentionsInfo
import com.intellij.codeInsight.hint.HintManager
import com.intellij.codeInsight.intention.impl.CachedIntentions
import com.intellij.codeInsight.intention.impl.IntentionHintComponent
import com.intellij.codeInsight.intention.impl.ShowIntentionActionsHandler
import com.intellij.lang.LangBundle
import com.intellij.lang.annotation.HighlightSeverity
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.application.ModalityState
import com.intellij.openapi.application.ReadAction
import com.intellij.openapi.editor.Editor
import com.intellij.openapi.editor.Inlay
import com.intellij.openapi.project.DumbService
import com.intellij.openapi.project.Project
import com.intellij.psi.PsiDocumentManager
import com.intellij.psi.PsiFile
import com.intellij.psi.util.PsiUtilBase
import com.intellij.ui.awt.RelativePoint
import com.intellij.util.concurrency.AppExecutorUtil
import java.lang.reflect.Method
internal object IntentionsPopup {
private const val INTENTION_SOURCE_CLASS_NAME = "com.intellij.codeInsight.intention.IntentionSource"
private val showPopupMethod: ShowPopupMethod? = try {
val method = IntentionHintComponent::class.java.declaredMethods.first { method ->
val parameterTypes = method.parameterTypes
method.name == "showPopup" &&
parameterTypes.size in 1..2 &&
parameterTypes[0] === RelativePoint::class.java &&
parameterTypes.getOrNull(1).let { p -> p == null || p.name == INTENTION_SOURCE_CLASS_NAME }
}
method.isAccessible = true
@Suppress("UNCHECKED_CAST")
val args: Array<Any?> = if (method.parameterCount == 1)
arrayOf(null)
else
arrayOf(null, (Class.forName(INTENTION_SOURCE_CLASS_NAME) as Class<Enum<*>>).enumConstants.first { it.name == "OTHER" })
ShowPopupMethod(method, args)
} catch (t: Throwable) {
InspectionLens.LOG.warn("Could not initialize intention popup", t)
null
}
private class ShowPopupMethod(private val method: Method, private val args: Array<Any?>) {
operator fun invoke(component: IntentionHintComponent) {
method.invoke(component, *args)
}
}
fun show(highlightInfo: HighlightInfo, inlay: Inlay<*>) {
if (!tryShow(highlightInfo, inlay)) {
showNoActionsAvailable(inlay.editor)
}
}
private fun tryShow(highlightInfo: HighlightInfo, inlay: Inlay<*>): Boolean {
val editor = inlay.editor
val project = editor.project ?: return false
val file = PsiUtilBase.getPsiFileInEditor(editor, project) ?: return false
PsiDocumentManager.getInstance(project).commitAllDocuments()
IntentionsUI.getInstance(project).hide()
ReadAction
.nonBlocking<IntentionsInfo> { collectIntentions(editor, project, file, highlightInfo, inlay.offset) }
.finishOnUiThread(ModalityState.current()) { tryShowPopup(project, file, editor, it) }
.submit(AppExecutorUtil.getAppExecutorService())
return true
}
private fun collectIntentions(editor: Editor, project: Project, file: PsiFile, info: HighlightInfo, offset: Int): IntentionsInfo {
val intentions = mutableListOf<IntentionActionDescriptor>()
info.findRegisteredQuickFix { descriptor, _ ->
if (DumbService.getInstance(project).isUsableInCurrentContext(descriptor.action) && ShowIntentionActionsHandler.availableFor(file, editor, offset, descriptor.action)) {
intentions.add(descriptor)
}
null
}
return IntentionsInfo().also {
it.offset = offset
if (info.severity === HighlightSeverity.ERROR) {
it.errorFixesToShow.addAll(intentions)
}
else {
it.inspectionFixesToShow.addAll(intentions)
}
}
}
private fun tryShowPopup(project: Project, file: PsiFile, editor: Editor, intentions: IntentionsInfo) {
try {
showPopup(project, file, editor, intentions)
} catch (t: Throwable) {
InspectionLens.LOG.error("Could not show intention popup", t)
showNoActionsAvailable(editor)
}
}
private fun showPopup(project: Project, file: PsiFile, editor: Editor, intentions: IntentionsInfo) {
if (intentions.isEmpty || showPopupMethod == null) {
val showIntentionsAction = ActionManager.getInstance().getAction(IdeActions.ACTION_SHOW_INTENTION_ACTIONS)
ActionUtil.invokeAction(showIntentionsAction, editor.component, ActionPlaces.EDITOR_INLAY, null, null)
}
else {
val cachedIntentions = CachedIntentions.create(project, file, editor, intentions)
val hintComponent = IntentionHintComponent.showIntentionHint(project, file, editor, false, cachedIntentions)
showPopupMethod.invoke(hintComponent)
}
}
private fun showNoActionsAvailable(editor: Editor) {
HintManager.getInstance().showInformationHint(editor, LangBundle.message("hint.text.no.context.actions.available.at.this.location"))
}
}

View File

@@ -1,199 +0,0 @@
package com.chylex.intellij.inspectionlens.editor.lens
import com.chylex.intellij.inspectionlens.settings.LensHoverMode
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.hints.presentation.InputHandler
import com.intellij.openapi.editor.Editor
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.util.text.StringUtil
import com.intellij.ui.paint.EffectPainter
import java.awt.Cursor
import java.awt.Graphics
import java.awt.Graphics2D
import java.awt.MouseInfo
import java.awt.Point
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.
*/
class LensRenderer(private var info: HighlightInfo, private val settings: LensSettingsState) : HintRenderer(null), InputHandler {
private lateinit var inlay: Inlay<*>
private lateinit var attributes: LensSeverityTextAttributes
private var extraRightPadding = 0
private var hovered = false
init {
setPropertiesFrom(info)
}
fun setInlay(inlay: Inlay<*>) {
check(!this::inlay.isInitialized) { "Inlay already set" }
this.inlay = inlay
}
fun setPropertiesFrom(info: HighlightInfo) {
this.info = info
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) {
fixBaselineForTextRendering(r)
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 {
return attributes
}
override fun useEditorFont(): Boolean {
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(info, inlay)
}
}
}
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 {
/**
* [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 stripUppercaseTag(text: String): String {
if (text.startsWith('[')) {
val matcher = UPPERCASE_TAG_REGEX.matcher(text)
if (matcher.find()) {
return text.substring(matcher.end())
}
}
return text
}
private fun unescapeHtmlEntities(text: String): String {
return if (text.contains('&')) StringUtil.unescapeXmlEntities(text) else text
}
private fun addEllipsisOrMissingPeriod(text: String, maxLength: Int): String {
return when {
text.length > maxLength -> text.take(maxLength).trimEnd { it.isWhitespace() || it == '.' } + ""
!text.endsWith('.') -> "$text."
else -> text
}
}
private fun fixBaselineForTextRendering(rect: Rectangle) {
rect.y += 1
}
private fun moveToOffset(editor: Editor, offset: Int) {
editor.caretModel.moveToOffset(offset)
editor.scrollingModel.scrollToCaret(ScrollType.MAKE_VISIBLE)
}
}
}

View File

@@ -1,31 +0,0 @@
package com.chylex.intellij.inspectionlens.editor.lens
import com.intellij.codeInsight.daemon.impl.SeverityRegistrar
import com.intellij.lang.annotation.HighlightSeverity
import java.util.function.Predicate
class LensSeverityFilter(private val hiddenSeverityIds: Set<String>, private val showUnknownSeverities: Boolean) : Predicate<HighlightSeverity> {
private val knownSeverityIds = getSupportedSeverities().mapTo(HashSet(), HighlightSeverity::getName)
override fun test(severity: HighlightSeverity): Boolean {
if (!isSupported(severity)) {
return false
}
return if (severity.name in knownSeverityIds)
severity.name !in hiddenSeverityIds
else
showUnknownSeverities
}
companion object {
@Suppress("DEPRECATION")
private fun isSupported(severity: HighlightSeverity): Boolean {
return severity > HighlightSeverity.TEXT_ATTRIBUTES && severity !== HighlightSeverity.INFO
}
fun getSupportedSeverities(registrar: SeverityRegistrar = SeverityRegistrar.getSeverityRegistrar(null)): List<HighlightSeverity> {
return registrar.allSeverities.filter(::isSupported)
}
}
}

View File

@@ -1,73 +1,23 @@
package com.chylex.intellij.inspectionlens.settings package com.chylex.intellij.inspectionlens.settings
import com.chylex.intellij.inspectionlens.editor.lens.LensSeverityFilter
import com.intellij.codeInsight.daemon.impl.SeverityRegistrar import com.intellij.codeInsight.daemon.impl.SeverityRegistrar
import com.intellij.lang.annotation.HighlightSeverity import com.intellij.lang.annotation.HighlightSeverity
import com.intellij.openapi.components.service import com.intellij.openapi.components.service
import com.intellij.openapi.editor.event.SelectionEvent
import com.intellij.openapi.editor.event.SelectionListener
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.ui.DisabledTraversalPolicy
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.Row
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
class LensApplicationConfigurable : BoundConfigurable("Inspection Lens"), ConfigurableWithId { class LensApplicationConfigurable : BoundConfigurable("Inspection Lens"), ConfigurableWithId {
companion object { companion object {
const val ID = "InspectionLens" const val ID = "InspectionLens"
private fun globalSeverities(): List<HighlightSeverity> {
return SeverityRegistrar.getSeverityRegistrar(null).allSeverities
} }
private data class DisplayedSeverity(
val id: String,
val severity: StoredSeverity,
val textAttributes: TextAttributes? = null,
) {
constructor(
severity: HighlightSeverity,
registrar: SeverityRegistrar,
) : this(
id = severity.name,
severity = StoredSeverity(severity),
textAttributes = registrar.getHighlightInfoTypeBySeverity(severity).attributesKey.defaultAttributes
)
}
private val settingsService = service<LensSettingsState>()
private val allSeverities by lazy(LazyThreadSafetyMode.NONE) {
val settings = settingsService.state
val registrar = SeverityRegistrar.getSeverityRegistrar(null)
val knownSeverities = LensSeverityFilter.getSupportedSeverities(registrar).map { DisplayedSeverity(it, registrar) }
val knownSeverityIds = knownSeverities.mapTo(HashSet(), DisplayedSeverity::id)
// Update names and priorities of stored severities.
for ((id, knownSeverity, _) in knownSeverities) {
val storedSeverity = settings.hiddenSeverities[id]
if (storedSeverity != null && storedSeverity != knownSeverity) {
settings.hiddenSeverities[id] = knownSeverity
}
}
val unknownSeverities = settings.hiddenSeverities.entries
.filterNot { it.key in knownSeverityIds }
.map { DisplayedSeverity(it.key, it.value) }
(knownSeverities + unknownSeverities).sortedByDescending { it.severity.priority }
} }
override fun getId(): String { override fun getId(): String {
@@ -75,85 +25,31 @@ class LensApplicationConfigurable : BoundConfigurable("Inspection Lens"), Config
} }
override fun createPanel(): DialogPanel { override fun createPanel(): DialogPanel {
val settings = settingsService.state val settings = service<LensSettingsState>().state
lateinit var panel: DialogPanel return panel {
group("Enabled Severities") {
val knownSeverities = globalSeverities().sortedByDescending(HighlightSeverity::myVal)
panel = panel { for (severity in knownSeverities) {
group("Appearance") {
row { row {
checkBox("Use editor font").bindSelected(settings::useEditorFont) checkBox(severity.displayCapitalizedName).bindSelectedToNotInSet(settings.disabledSeveritiesReal, severity.name)
}
row("Max description length:") {
intTextField(LensSettingsState.MAX_DESCRIPTION_LENGTH_RANGE, keyboardStep = 10).bindIntText(settings::maxDescriptionLength)
} }
} }
group("Behavior") { val knownSeverityNames = knownSeverities.map { it.name }.toSet()
row("Hover mode:") { val noLongerKnownSeverityNames = settings.disabledSeveritiesReal.filterNot { it in knownSeverityNames }
val items = LensHoverMode.entries
val renderer = SimpleListCellRenderer.create("", LensHoverMode::description)
comboBox(items, renderer).bindItem(settings::lensHoverMode) { settings.lensHoverMode = it ?: LensHoverMode.DEFAULT }
}
}
group("Shown Severities") { for (severityName in noLongerKnownSeverityNames) {
for ((id, severity, textAttributes) in allSeverities) {
row { row {
checkBox(severity.name) checkBox("Unknown ($severityName)").bindSelectedToNotInSet(settings.disabledSeveritiesReal, severityName)
.bindSelectedToNotIn(settings.hiddenSeverities, id, severity)
.gap(RightGap.COLUMNS)
labelWithAttributes("Example", textAttributes)
}.layout(RowLayout.PARENT_GRID)
}
row {
checkBox("Other").bindSelected(settings::showUnknownSeverities)
}
}
group("Actions") {
row {
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 <T> Cell<JBCheckBox>.bindSelectedToNotInSet(set: ModificationTrackingCollection<T>, value: T): Cell<JBCheckBox> {
} return bindSelected({ value !in set }, { if (it) set.remove(value) else set.add(value) })
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 })
}
private fun Row.labelWithAttributes(text: String, textAttributes: TextAttributes?): Cell<SimpleRendererComponent> {
val label = SimpleRendererComponent(null, null, true)
label.setText(text, textAttributes, false)
label.focusTraversalPolicy = DisabledTraversalPolicy()
val editor = label.editor
editor.setCustomCursor(this, Cursor.getDefaultCursor())
editor.contentComponent.setOpaque(false)
editor.selectionModel.addSelectionListener(object : SelectionListener {
override fun selectionChanged(e: SelectionEvent) {
if (!e.newRange.isEmpty) {
editor.selectionModel.removeSelection(true)
}
}
})
Disposer.register(disposable!!, label)
return cell(label)
}
override fun apply() {
super.apply()
settingsService.update()
} }
} }

View File

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

@@ -0,0 +1,13 @@
package com.chylex.intellij.inspectionlens.settings
import com.chylex.intellij.inspectionlens.editor.LensSeverityFilter
import com.intellij.openapi.components.service
object LensSettings {
private val state
get() = service<LensSettingsState>().state
fun createSeverityFilter(): LensSeverityFilter {
return LensSeverityFilter(state.disabledSeveritiesReadOnly)
}
}

View File

@@ -1,13 +1,11 @@
package com.chylex.intellij.inspectionlens.settings package com.chylex.intellij.inspectionlens.settings
import com.chylex.intellij.inspectionlens.InspectionLens
import com.chylex.intellij.inspectionlens.editor.lens.LensSeverityFilter
import com.intellij.openapi.components.BaseState import com.intellij.openapi.components.BaseState
import com.intellij.openapi.components.SettingsCategory import com.intellij.openapi.components.SettingsCategory
import com.intellij.openapi.components.SimplePersistentStateComponent import com.intellij.openapi.components.SimplePersistentStateComponent
import com.intellij.openapi.components.State import com.intellij.openapi.components.State
import com.intellij.openapi.components.Storage import com.intellij.openapi.components.Storage
import com.intellij.util.xmlb.annotations.XMap import com.intellij.util.xmlb.annotations.XCollection
@State( @State(
name = LensApplicationConfigurable.ID, name = LensApplicationConfigurable.ID,
@@ -16,58 +14,13 @@ import com.intellij.util.xmlb.annotations.XMap
) )
class LensSettingsState : SimplePersistentStateComponent<LensSettingsState.State>(State()) { class LensSettingsState : SimplePersistentStateComponent<LensSettingsState.State>(State()) {
class State : BaseState() { class State : BaseState() {
@get:XMap @get:XCollection
val hiddenSeverities by map<String, StoredSeverity>() private val disabledSeverities by stringSet()
var showUnknownSeverities by property(true) val disabledSeveritiesReadOnly: Set<String>
var useEditorFont by property(true) get() = disabledSeverities
var maxDescriptionLength by property(120)
var lensHoverMode by enum(LensHoverMode.DEFAULT)
}
companion object { @Transient
val MAX_DESCRIPTION_LENGTH_RANGE = 20..1000 val disabledSeveritiesReal = ModificationTrackingCollection(disabledSeverities, ::incrementModificationCount)
}
@get:Synchronized
@set:Synchronized
var severityFilter = createSeverityFilter()
private set
val useEditorFont
get() = state.useEditorFont
val maxDescriptionLength
get() = state.maxDescriptionLength
val lensHoverMode
get() = state.lensHoverMode
override fun loadState(state: 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()
}
fun update() {
severityFilter = createSeverityFilter()
InspectionLens.scheduleRefresh()
}
private fun createSeverityFilter(): LensSeverityFilter {
val state = state
return LensSeverityFilter(state.hiddenSeverities.keys, state.showUnknownSeverities)
} }
} }

View File

@@ -0,0 +1,15 @@
package com.chylex.intellij.inspectionlens.settings
class ModificationTrackingCollection<T>(private val collection: MutableCollection<T>, private val onModified: () -> Unit) : Iterable<T> {
override fun iterator(): Iterator<T> {
return collection.iterator()
}
fun add(element: T): Boolean {
return collection.add(element).also { if (it) onModified() }
}
fun remove(element: T): Boolean {
return collection.remove(element).also { if (it) onModified() }
}
}

View File

@@ -1,9 +0,0 @@
package com.chylex.intellij.inspectionlens.settings
import com.intellij.lang.annotation.HighlightSeverity
import com.intellij.util.xmlb.annotations.Tag
@Tag("severity")
data class StoredSeverity(var name: String = "", var priority: Int = 0) {
constructor(severity: HighlightSeverity) : this(severity.displayCapitalizedName, severity.myVal)
}

View File

@@ -4,52 +4,19 @@
<vendor url="https://chylex.com">chylex</vendor> <vendor url="https://chylex.com">chylex</vendor>
<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. Shows errors, warnings, and other inspection highlights inline.
<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. Left-click an inspection to show quick fixes. Middle-click an inspection to navigate to the relevant code in the editor. After installing the plugin, inspection descriptions will appear after the ends of lines, and the lines will be highlighted with a background color.
Shown inspection severities are <b>Errors</b>, <b>Warnings</b>, <b>Weak Warnings</b>, <b>Server Problems</b>, <b>Grammar Errors</b>, <b>Typos</b>, and other inspections from plugins or future IntelliJ versions that have a high enough severity level.
Each severity has a different color, with support for both light and dark themes.
<br><br> <br><br>
Configure appearance, behavior of clicking on inspections, and visible severities in <b>Settings | Tools | Inspection Lens</b>. Note: The plugin is not customizable outside the ability to disable/enable the plugin without restarting the IDE.
If the defaults don't work for you, I recommend trying the <a href="https://plugins.jetbrains.com/plugin/17302-inlineerror">Inline Error</a> plugin which can be customized, building your own version of Inspection Lens, or proposing your change in the <a href="https://github.com/chylex/IntelliJ-Inspection-Lens/issues">issue tracker</a>.
<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.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>
<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>
<ul>
<li>Fixed warnings in usage of IntelliJ SDK.</li>
</ul>
<b>Version 1.4</b>
<ul>
<li>Added configuration of visible severities to <b>Settings | Tools | Inspection Lens</b>.</li>
</ul>
<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>
@@ -104,17 +71,11 @@
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.InspectionLensFileEditorManagerListener" topic="com.intellij.openapi.fileEditor.FileEditorManagerListener" /> <listener class="com.chylex.intellij.inspectionlens.InspectionLensFileOpenedListener" topic="com.intellij.openapi.fileEditor.FileOpenedSyncListener" />
</projectListeners> </projectListeners>
</idea-plugin> </idea-plugin>

View File

@@ -1,5 +1,6 @@
package com.chylex.intellij.inspectionlens.editor.lens package com.chylex.intellij.inspectionlens
import com.chylex.intellij.inspectionlens.editor.EditorLensInlay
import com.intellij.lang.annotation.HighlightSeverity import com.intellij.lang.annotation.HighlightSeverity
import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Nested import org.junit.jupiter.api.Nested
@@ -47,7 +48,7 @@ class EditorLensTest {
} }
/** /**
* If any of these changes, re-evaluate [EditorLensInlay.MAXIMUM_SEVERITY] and the priority calculations. * If any of these change, re-evaluate [EditorLensInlay.MAXIMUM_SEVERITY] and the priority calculations.
*/ */
@Nested @Nested
inner class IdeaHighlightSeverityAssumptions { inner class IdeaHighlightSeverityAssumptions {