From 043c02e4324ac6b20b3e5622efb9842c94938e1a Mon Sep 17 00:00:00 2001 From: chylex <contact@chylex.com> Date: Wed, 12 Jun 2024 12:39:00 +0200 Subject: [PATCH] Add settings panel for configuring shown severities Closes #2 --- README.md | 6 +- .../editor/EditorLensManager.kt | 6 + .../editor/EditorLensManagerDispatcher.kt | 4 + .../editor/LensMarkupModelListener.kt | 12 +- .../editor/LensSeverityFilter.kt | 31 +++++ .../settings/LensApplicationConfigurable.kt | 127 ++++++++++++++++++ .../settings/LensSettingsState.kt | 44 ++++++ .../inspectionlens/settings/StoredSeverity.kt | 9 ++ src/main/resources/META-INF/plugin.xml | 17 ++- 9 files changed, 242 insertions(+), 14 deletions(-) create mode 100644 src/main/kotlin/com/chylex/intellij/inspectionlens/editor/LensSeverityFilter.kt create mode 100644 src/main/kotlin/com/chylex/intellij/inspectionlens/settings/LensApplicationConfigurable.kt create mode 100644 src/main/kotlin/com/chylex/intellij/inspectionlens/settings/LensSettingsState.kt create mode 100644 src/main/kotlin/com/chylex/intellij/inspectionlens/settings/StoredSeverity.kt diff --git a/README.md b/README.md index 6ee8d48..87b963c 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,8 @@ # Inspection Lens <img align="right" src="logo.png" alt="Plugin Logo"> -IntelliJ plugin that shows errors, warnings, and other inspection highlights inline. +Displays errors, warnings, and other inspections inline. Highlights the background of lines with inspections. Supports light and dark themes out of the box. -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. - -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). +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**. 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. diff --git a/src/main/kotlin/com/chylex/intellij/inspectionlens/editor/EditorLensManager.kt b/src/main/kotlin/com/chylex/intellij/inspectionlens/editor/EditorLensManager.kt index 1bd3873..fc20759 100644 --- a/src/main/kotlin/com/chylex/intellij/inspectionlens/editor/EditorLensManager.kt +++ b/src/main/kotlin/com/chylex/intellij/inspectionlens/editor/EditorLensManager.kt @@ -83,6 +83,12 @@ class EditorLensManager private constructor(private val editor: Editor) { lensManager.hide(highlighter) } } + + object HideAll : Command { + override fun apply(lensManager: EditorLensManager) { + lensManager.hideAll() + } + } } /** diff --git a/src/main/kotlin/com/chylex/intellij/inspectionlens/editor/EditorLensManagerDispatcher.kt b/src/main/kotlin/com/chylex/intellij/inspectionlens/editor/EditorLensManagerDispatcher.kt index 6db54b7..f189ceb 100644 --- a/src/main/kotlin/com/chylex/intellij/inspectionlens/editor/EditorLensManagerDispatcher.kt +++ b/src/main/kotlin/com/chylex/intellij/inspectionlens/editor/EditorLensManagerDispatcher.kt @@ -15,6 +15,10 @@ class EditorLensManagerDispatcher(private val lensManager: EditorLensManager) { enqueue(EditorLensManager.Command.Hide(highlighter)) } + fun hideAll() { + enqueue(EditorLensManager.Command.HideAll) + } + private fun enqueue(item: EditorLensManager.Command) { synchronized(this) { queuedItems.add(item) diff --git a/src/main/kotlin/com/chylex/intellij/inspectionlens/editor/LensMarkupModelListener.kt b/src/main/kotlin/com/chylex/intellij/inspectionlens/editor/LensMarkupModelListener.kt index 8a2fa22..c59c10d 100644 --- a/src/main/kotlin/com/chylex/intellij/inspectionlens/editor/LensMarkupModelListener.kt +++ b/src/main/kotlin/com/chylex/intellij/inspectionlens/editor/LensMarkupModelListener.kt @@ -1,8 +1,9 @@ package com.chylex.intellij.inspectionlens.editor +import com.chylex.intellij.inspectionlens.settings.LensSettingsState import com.intellij.codeInsight.daemon.impl.HighlightInfo -import com.intellij.lang.annotation.HighlightSeverity 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 @@ -48,12 +49,16 @@ internal class LensMarkupModelListener private constructor(editor: Editor) : Mar highlighters.forEach(::showIfValid) } + private fun hideAll() { + lensManagerDispatcher.hideAll() + } + companion object { private val EDITOR_KEY = Key<LensMarkupModelListener>(LensMarkupModelListener::class.java.name) - private val MINIMUM_SEVERITY = HighlightSeverity.TEXT_ATTRIBUTES.myVal + 1 + private val SETTINGS_SERVICE = service<LensSettingsState>() private fun getFilteredHighlightInfo(highlighter: RangeHighlighter): HighlightInfo? { - return HighlightInfo.fromRangeHighlighter(highlighter)?.takeIf { it.severity.myVal >= MINIMUM_SEVERITY } + return HighlightInfo.fromRangeHighlighter(highlighter)?.takeIf { SETTINGS_SERVICE.severityFilter.test(it.severity) } } private inline fun runWithHighlighterIfValid(highlighter: RangeHighlighter, actionForImmediate: (HighlighterWithInfo) -> Unit, actionForAsync: (HighlighterWithInfo.Async) -> Unit) { @@ -101,6 +106,7 @@ internal class LensMarkupModelListener private constructor(editor: Editor) : Mar val listener = editor.getUserData(EDITOR_KEY) ?: return val markupModel = getMarkupModel(editor) ?: return + listener.hideAll() listener.showAllValid(markupModel.allHighlighters) } } diff --git a/src/main/kotlin/com/chylex/intellij/inspectionlens/editor/LensSeverityFilter.kt b/src/main/kotlin/com/chylex/intellij/inspectionlens/editor/LensSeverityFilter.kt new file mode 100644 index 0000000..ab72705 --- /dev/null +++ b/src/main/kotlin/com/chylex/intellij/inspectionlens/editor/LensSeverityFilter.kt @@ -0,0 +1,31 @@ +package com.chylex.intellij.inspectionlens.editor + +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) + } + } +} diff --git a/src/main/kotlin/com/chylex/intellij/inspectionlens/settings/LensApplicationConfigurable.kt b/src/main/kotlin/com/chylex/intellij/inspectionlens/settings/LensApplicationConfigurable.kt new file mode 100644 index 0000000..1176180 --- /dev/null +++ b/src/main/kotlin/com/chylex/intellij/inspectionlens/settings/LensApplicationConfigurable.kt @@ -0,0 +1,127 @@ +package com.chylex.intellij.inspectionlens.settings + +import com.chylex.intellij.inspectionlens.editor.LensSeverityFilter +import com.intellij.codeInsight.daemon.impl.SeverityRegistrar +import com.intellij.lang.annotation.HighlightSeverity +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.ConfigurableWithId +import com.intellij.openapi.ui.DialogPanel +import com.intellij.openapi.util.Disposer +import com.intellij.ui.DisabledTraversalPolicy +import com.intellij.ui.EditorTextFieldCellRenderer.SimpleRendererComponent +import com.intellij.ui.components.JBCheckBox +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.bindSelected +import com.intellij.ui.dsl.builder.panel +import java.awt.Cursor + +class LensApplicationConfigurable : BoundConfigurable("Inspection Lens"), ConfigurableWithId { + companion object { + const val ID = "InspectionLens" + + private fun getTextAttributes(registrar: SeverityRegistrar, severity: HighlightSeverity): TextAttributes? { + return registrar.getHighlightInfoTypeBySeverity(severity).attributesKey.defaultAttributes + } + } + + 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 = getTextAttributes(registrar, severity) + ) + } + + 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 { + return ID + } + + override fun createPanel(): DialogPanel { + val settings = settingsService.state + + return panel { + group("Shown Severities") { + for ((id, severity, textAttributes) in allSeverities) { + row { + checkBox(severity.name) + .bindSelectedToNotIn(settings.hiddenSeverities, id, severity) + .gap(RightGap.COLUMNS) + + labelWithAttributes("Example", textAttributes) + }.layout(RowLayout.PARENT_GRID) + } + + row { + checkBox("Other").bindSelected(settings::showUnknownSeverities) + } + } + } + } + + 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() + } +} diff --git a/src/main/kotlin/com/chylex/intellij/inspectionlens/settings/LensSettingsState.kt b/src/main/kotlin/com/chylex/intellij/inspectionlens/settings/LensSettingsState.kt new file mode 100644 index 0000000..c376aa2 --- /dev/null +++ b/src/main/kotlin/com/chylex/intellij/inspectionlens/settings/LensSettingsState.kt @@ -0,0 +1,44 @@ +package com.chylex.intellij.inspectionlens.settings + +import com.chylex.intellij.inspectionlens.InspectionLensRefresher +import com.chylex.intellij.inspectionlens.editor.LensSeverityFilter +import com.intellij.openapi.components.BaseState +import com.intellij.openapi.components.SettingsCategory +import com.intellij.openapi.components.SimplePersistentStateComponent +import com.intellij.openapi.components.State +import com.intellij.openapi.components.Storage +import com.intellij.util.xmlb.annotations.XMap + +@State( + name = LensApplicationConfigurable.ID, + storages = [ Storage("chylex.inspectionLens.xml") ], + category = SettingsCategory.UI +) +class LensSettingsState : SimplePersistentStateComponent<LensSettingsState.State>(State()) { + class State : BaseState() { + @get:XMap + val hiddenSeverities by map<String, StoredSeverity>() + + var showUnknownSeverities by property(true) + } + + @get:Synchronized + @set:Synchronized + var severityFilter = createSeverityFilter() + private set + + override fun loadState(state: State) { + super.loadState(state) + update() + } + + fun update() { + severityFilter = createSeverityFilter() + InspectionLensRefresher.scheduleRefresh() + } + + private fun createSeverityFilter(): LensSeverityFilter { + val state = state + return LensSeverityFilter(state.hiddenSeverities.keys, state.showUnknownSeverities) + } +} diff --git a/src/main/kotlin/com/chylex/intellij/inspectionlens/settings/StoredSeverity.kt b/src/main/kotlin/com/chylex/intellij/inspectionlens/settings/StoredSeverity.kt new file mode 100644 index 0000000..7eeee21 --- /dev/null +++ b/src/main/kotlin/com/chylex/intellij/inspectionlens/settings/StoredSeverity.kt @@ -0,0 +1,9 @@ +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) +} diff --git a/src/main/resources/META-INF/plugin.xml b/src/main/resources/META-INF/plugin.xml index c6e604f..952b122 100644 --- a/src/main/resources/META-INF/plugin.xml +++ b/src/main/resources/META-INF/plugin.xml @@ -4,14 +4,9 @@ <vendor url="https://chylex.com">chylex</vendor> <description><![CDATA[ - Shows errors, warnings, and other inspection highlights inline. + 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> - 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> - 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>. + 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>. <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. ]]></description> @@ -63,6 +58,14 @@ <depends>com.intellij.modules.platform</depends> <depends optional="true" config-file="compatibility/InspectionLens-Grazie.xml">tanvd.grazi</depends> + <extensions defaultExtensionNs="com.intellij"> + <applicationService serviceImplementation="com.chylex.intellij.inspectionlens.settings.LensSettingsState" /> + <applicationConfigurable id="com.chylex.intellij.inspectionlens.settings.LensApplicationConfigurable" + instance="com.chylex.intellij.inspectionlens.settings.LensApplicationConfigurable" + displayName="Inspection Lens" + parentId="tools" /> + </extensions> + <applicationListeners> <listener class="com.chylex.intellij.inspectionlens.InspectionLensPluginListener" topic="com.intellij.ide.plugins.DynamicPluginListener" /> </applicationListeners>