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>