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

1 Commits

Author SHA1 Message Date
519d80ed6e WIP 2024-06-12 12:39:00 +02:00
36 changed files with 358 additions and 1070 deletions

1
.gitignore vendored
View File

@@ -1,5 +1,4 @@
/.idea/*
!/.idea/dictionaries
!/.idea/runConfigurations
/.gradle/

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">
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. Configure visible severities in **Settings | Tools | Inspection Lens**.
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.
Left-click an inspection to show quick fixes. Middle-click an inspection to navigate to the relevant code in the editor.
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.

View File

@@ -8,7 +8,7 @@ plugins {
}
group = "com.chylex.intellij.inspectionlens"
version = "1.5"
version = "1.3.3"
repositories {
mavenCentral()

View File

@@ -1,11 +1,15 @@
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.components.service
import com.intellij.openapi.fileEditor.FileEditorManager
import com.intellij.openapi.fileEditor.TextEditor
import com.intellij.openapi.project.ProjectManager
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.
@@ -17,7 +21,7 @@ internal object InspectionLens {
* Installs lenses into [editor].
*/
fun install(editor: TextEditor) {
EditorLensFeatures.install(editor.editor, service<InspectionLensPluginDisposableService>().intersect(editor))
LensMarkupModelListener.register(editor.editor, createEditorDisposable(editor))
}
/**
@@ -28,19 +32,30 @@ internal object InspectionLens {
}
/**
* Refreshes lenses in all open editors.
* Uninstalls lenses from all open editors.
*/
private fun refresh() {
fun uninstall() {
forEachOpenEditor {
EditorLensFeatures.refresh(it.editor)
EditorLensManager.remove(it.editor)
}
}
/**
* Schedules a refresh of lenses in all open editors.
* Refreshes lenses in all open editors.
*/
fun scheduleRefresh() {
Refresh.schedule()
fun refresh() {
forEachOpenEditor {
LensMarkupModelListener.refresh(it.editor)
}
}
/**
* Creates a [Disposable] that will be disposed when either the [TextEditor] is disposed or the plugin is unloaded.
*/
private fun createEditorDisposable(textEditor: TextEditor): Disposable {
val pluginLifetime = ApplicationManager.getApplication().getService(InspectionLensPluginDisposableService::class.java).createLifetime()
val editorLifetime = textEditor.createLifetime()
return Lifetime.intersect(pluginLifetime, editorLifetime).createNestedDisposable("InspectionLensIntersectedLifetime")
}
/**
@@ -55,26 +70,4 @@ internal object InspectionLens {
}
}
}
private object Refresh {
private var needsRefresh = false
fun schedule() {
synchronized(this) {
if (!needsRefresh) {
needsRefresh = true
ApplicationManager.getApplication().invokeLater(this::run)
}
}
}
private fun run() {
synchronized(this) {
if (needsRefresh) {
needsRefresh = false
refresh()
}
}
}
}
}

View File

@@ -2,21 +2,11 @@ package com.chylex.intellij.inspectionlens
import com.intellij.openapi.Disposable
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.
*/
@Service
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() {}
}

View File

@@ -4,7 +4,7 @@ import com.intellij.ide.plugins.DynamicPluginListener
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 {
override fun pluginLoaded(pluginDescriptor: IdeaPluginDescriptor) {
@@ -12,4 +12,10 @@ class InspectionLensPluginListener : DynamicPluginListener {
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,6 +1,6 @@
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.openapi.project.Project
import com.intellij.openapi.startup.ProjectActivity

View File

@@ -1,12 +1,12 @@
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.openapi.diagnostic.logger
import com.intellij.profile.codeInspection.InspectionProfileManager
import com.intellij.spellchecker.SpellCheckerSeveritiesProvider
internal object SpellCheckerSupport {
object SpellCheckerSupport {
private val log = logger<SpellCheckerSupport>()
fun load() {

View File

@@ -1,274 +0,0 @@
package com.chylex.intellij.inspectionlens.debug
import com.chylex.intellij.inspectionlens.editor.lens.ColorGenerator
import com.chylex.intellij.inspectionlens.editor.lens.ColorMode
import com.chylex.intellij.inspectionlens.editor.lens.EditorLens
import com.chylex.intellij.inspectionlens.editor.lens.LensSeverity
import com.intellij.codeInsight.daemon.impl.HighlightInfo
import com.intellij.codeInsight.daemon.impl.HighlightInfoType
import com.intellij.grazie.ide.TextProblemSeverities
import com.intellij.lang.annotation.HighlightSeverity
import com.intellij.openapi.Disposable
import com.intellij.openapi.actionSystem.ActionUpdateThread
import com.intellij.openapi.actionSystem.AnAction
import com.intellij.openapi.actionSystem.AnActionEvent
import com.intellij.openapi.components.service
import com.intellij.openapi.editor.Editor
import com.intellij.openapi.editor.EditorFactory
import com.intellij.openapi.editor.colors.EditorColorsManager
import com.intellij.openapi.editor.colors.EditorColorsScheme
import com.intellij.openapi.editor.event.CaretEvent
import com.intellij.openapi.editor.event.CaretListener
import com.intellij.openapi.editor.event.SelectionEvent
import com.intellij.openapi.editor.event.SelectionListener
import com.intellij.openapi.editor.ex.EditorEx
import com.intellij.openapi.project.Project
import com.intellij.openapi.ui.DialogPanel
import com.intellij.openapi.ui.DialogWrapper
import com.intellij.openapi.util.Disposer
import com.intellij.openapi.util.Key
import com.intellij.spellchecker.SpellCheckerSeveritiesProvider
import com.intellij.ui.ColorUtil
import com.intellij.ui.dsl.builder.Align
import com.intellij.ui.dsl.builder.Cell
import com.intellij.ui.dsl.builder.LabelPosition
import com.intellij.ui.dsl.builder.Row
import com.intellij.ui.dsl.builder.panel
import com.intellij.ui.dsl.builder.selected
import javax.swing.Action
import javax.swing.JSlider
class ShowColorEditorAction : AnAction() {
override fun getActionUpdateThread(): ActionUpdateThread {
return ActionUpdateThread.BGT
}
override fun actionPerformed(e: AnActionEvent) {
Dialog(e.project).show()
}
private class Dialog(project: Project?) : DialogWrapper(project) {
private val editorSyncer = EditorSyncer(disposable)
init {
title = "Inspection Lens - Color Editor"
init()
}
@Suppress("DuplicatedCode")
override fun createCenterPanel(): DialogPanel {
return panel {
row {
for (scheme in EditorColorsManager.getInstance().allSchemes.sortedWith(compareBy({ ColorUtil.isDark(it.defaultBackground) }, { it.displayName }))) {
createEditor(scheme)
}
}.resizableRow()
row {
val generateColors = checkBox("Generate colors")
.selected(ColorGenerator.useGenerator)
.onChanged {
ColorGenerator.useGenerator = it.isSelected
refreshColors()
}
slider(0, 15, 1, 5)
.label("Light theme desaturation", LabelPosition.TOP)
.enabledIf(generateColors.selected)
.bindColorValue(ColorGenerator::lightThemeDesaturation) { ColorGenerator.lightThemeDesaturation = it }
slider(0, 15, 1, 5)
.label("Dark theme desaturation", LabelPosition.TOP)
.enabledIf(generateColors.selected)
.bindColorValue(ColorGenerator::darkThemeDesaturation) { ColorGenerator.darkThemeDesaturation = it }
slider(0, 100, 5, 25)
.label("Light theme threshold", LabelPosition.TOP)
.enabledIf(generateColors.selected)
.bindColorValue(ColorGenerator::lightThemeThreshold) { ColorGenerator.lightThemeThreshold = it }
slider(0, 100, 5, 25)
.label("Dark theme threshold", LabelPosition.TOP)
.enabledIf(generateColors.selected)
.bindColorValue(ColorGenerator::darkThemeThreshold) { ColorGenerator.darkThemeThreshold = it }
slider(0, 25, 1, 5)
.label("Light theme line alpha", LabelPosition.TOP)
.enabledIf(generateColors.selected)
.bindColorValue(ColorGenerator::lightThemeLineAlpha) { ColorGenerator.lightThemeLineAlpha = it }
slider(0, 25, 1, 5)
.label("Dark theme line alpha", LabelPosition.TOP)
.enabledIf(generateColors.selected)
.bindColorValue(ColorGenerator::darkThemeLineAlpha) { ColorGenerator.darkThemeLineAlpha = it }
}
}
}
private fun Row.createEditor(scheme: EditorColorsScheme) {
val editor = createPreviewEditor(scheme, disposable)
editorSyncer.add(editor)
panel {
row {
cell(editor.component)
.label(scheme.displayName, LabelPosition.TOP)
.align(Align.FILL)
.resizableColumn()
}.resizableRow()
}.resizableColumn()
}
private fun Cell<JSlider>.bindColorValue(getter: () -> Int, setter: (Int) -> Unit): Cell<JSlider> {
return applyToComponent {
value = getter()
toolTipText = value.toString()
addChangeListener {
toolTipText = value.toString()
setter(value)
refreshColors()
}
}
}
private fun refreshColors() {
for (severity in LensSeverity.values()) {
severity.refreshColors()
}
for (editor in editorSyncer.editors) {
recreateLenses(editor)
}
}
override fun createActions(): Array<Action> {
return arrayOf(okAction)
}
override fun getDimensionServiceKey(): String {
return this::class.java.name
}
}
private class EditorSyncer(private val disposable: Disposable) {
val editors = mutableListOf<Editor>()
fun add(editor: Editor) {
editors.add(editor)
editor.caretModel.addCaretListener(object : CaretListener {
override fun caretPositionChanged(event: CaretEvent) {
syncCarets(editor)
}
override fun caretAdded(event: CaretEvent) {
syncCarets(editor)
}
override fun caretRemoved(event: CaretEvent) {
syncCarets(editor)
}
}, disposable)
editor.selectionModel.addSelectionListener(object : SelectionListener {
override fun selectionChanged(e: SelectionEvent) {
syncCarets(editor)
}
}, disposable)
}
private var isSyncing = false
private fun syncCarets(mainEditor: Editor) {
val caretsAndSelections = mainEditor.caretModel.caretsAndSelections
syncEditors(mainEditor) {
it.caretModel.caretsAndSelections = caretsAndSelections
}
}
private inline fun syncEditors(mainEditor: Editor, action: (Editor) -> Unit) {
if (isSyncing) {
return
}
isSyncing = true
try {
for (editor in editors) {
if (editor !== mainEditor) {
action(editor)
}
}
} finally {
isSyncing = false
}
}
}
private companion object {
private val SEVERITIES = listOf(
HighlightSeverity.ERROR,
HighlightSeverity.WARNING,
HighlightSeverity.WEAK_WARNING,
HighlightSeverity.GENERIC_SERVER_ERROR_OR_WARNING,
TextProblemSeverities.STYLE_SUGGESTION,
SpellCheckerSeveritiesProvider.TYPO,
HighlightSeverity("Other", 50)
)
private val LENSES_KEY = Key.create<MutableList<EditorLens>>("InspectionLens.ShowColorEditorAction.Lenses")
private fun createPreviewEditor(scheme: EditorColorsScheme, disposable: Disposable): Editor {
val editorFactory = EditorFactory.getInstance()
val document = editorFactory.createDocument(('A'..'Z').take(SEVERITIES.size * 3).joinToString(separator = "\n", postfix = "\n\n") { "$it = 0;" })
val editor = editorFactory.createViewer(document) as EditorEx
editor.colorsScheme = scheme
editor.caretModel.moveToOffset(editor.document.textLength)
with(editor.settings) {
additionalColumnsCount = 0
additionalLinesCount = 0
isIndentGuidesShown = false
isLineMarkerAreaShown = false
isLineNumbersShown = true
isRightMarginShown = false
isWhitespacesShown = true
setGutterIconsShown(false)
}
ColorMode.setForEditor(editor, if (ColorUtil.isDark(scheme.defaultBackground)) ColorMode.ALWAYS_DARK else ColorMode.ALWAYS_LIGHT)
recreateLenses(editor)
Disposer.register(disposable) { editorFactory.releaseEditor(editor) }
return editor
}
private fun recreateLenses(editor: Editor) {
LENSES_KEY.get(editor)?.forEach(EditorLens::hide)
val lenses = mutableListOf<EditorLens>()
LENSES_KEY.set(editor, lenses)
for ((index, severity) in SEVERITIES.withIndex()) {
addLens(editor, severity, index, lenses)
addLens(editor, severity, (index * 2) + 1 + SEVERITIES.size, lenses)
}
}
private fun addLens(editor: Editor, severity: HighlightSeverity, line: Int, list: MutableList<EditorLens>) {
val startOffset = editor.document.getLineStartOffset(line)
val endOffset = editor.document.getLineEndOffset(line)
val highlightInfo = HighlightInfo.newHighlightInfo(HighlightInfoType.ERROR)
.severity(severity)
.range(startOffset, endOffset)
.descriptionAndTooltip("Example - ${severity.displayCapitalizedName}")
.createUnconditionally()
EditorLens.show(editor, highlightInfo, service())?.let(list::add)
}
}
}

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,56 +0,0 @@
package com.chylex.intellij.inspectionlens.editor
import com.intellij.openapi.Disposable
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.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(editor: Editor, disposable: Disposable) {
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 features = EditorLensFeatures(editor, markupModel, foldingModel, disposable)
editor.putUserData(EDITOR_KEY, features)
Disposer.register(disposable) { editor.putUserData(EDITOR_KEY, null) }
}
fun refresh(editor: Editor) {
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.openapi.editor.Editor
import com.intellij.openapi.editor.Inlay
@@ -26,19 +25,14 @@ internal value class EditorLensInlay(private val inlay: Inlay<LensRenderer>) {
}
companion object {
fun show(editor: Editor, info: HighlightInfo, settings: LensSettingsState): EditorLensInlay? {
fun show(editor: Editor, info: HighlightInfo): EditorLensInlay? {
val offset = getInlayHintOffset(info)
val priority = getInlayHintPriority(editor, info)
val renderer = LensRenderer(info, settings)
val properties = InlayProperties()
.relatesToPrecedingText(true)
.disableSoftWrapping(true)
.priority(priority)
val renderer = LensRenderer(info)
val properties = InlayProperties().relatesToPrecedingText(true).disableSoftWrapping(true).priority(priority)
return editor.inlayModel.addAfterLineEndElement(offset, properties, renderer)
?.also(renderer::setInlay)
?.let(::EditorLensInlay)
return editor.inlayModel.addAfterLineEndElement(offset, properties, renderer)?.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.openapi.editor.Editor
import com.intellij.openapi.editor.ex.RangeHighlighterEx
import com.intellij.openapi.editor.markup.HighlighterLayer
import com.intellij.openapi.editor.markup.HighlighterTargetArea.LINES_IN_RANGE
import com.intellij.openapi.editor.markup.RangeHighlighter
import com.intellij.openapi.editor.markup.TextAttributes
@JvmInline
internal value class EditorLensLineBackground(private val highlighter: RangeHighlighter) {
val isInvalid
get() = !highlighter.isValid
fun onFoldRegionsChanged(editor: Editor, severity: LensSeverity) {
if (highlighter is RangeHighlighterEx) {
highlighter.textAttributes = getAttributes(editor, highlighter.startOffset, highlighter.endOffset, severity)
@Suppress("RedundantIf")
fun shouldRecreate(info: HighlightInfo): Boolean {
if (!highlighter.isValid) {
return true
}
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) {
@@ -30,20 +42,12 @@ internal value class EditorLensLineBackground(private val highlighter: RangeHigh
val severity = LensSeverity.from(info.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 {
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.getLineAttributes(ColorMode.getFromEditor(editor))
}
}
}

View File

@@ -1,18 +1,31 @@
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.markup.RangeHighlighter
import com.intellij.openapi.util.Key
import java.util.IdentityHashMap
/**
* Manages visible inspection lenses for an [Editor].
*/
internal class EditorLensManager(private val editor: Editor) {
class EditorLensManager private constructor(private val editor: Editor) {
companion object {
private val EDITOR_KEY = Key<EditorLensManager>(EditorLensManager::class.java.name)
fun getOrCreate(editor: Editor): EditorLensManager {
return editor.getUserData(EDITOR_KEY) ?: EditorLensManager(editor).also { editor.putUserData(EDITOR_KEY, it) }
}
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 val settings = service<LensSettingsState>()
private fun show(highlighterWithInfo: HighlighterWithInfo) {
val (highlighter, info) = highlighterWithInfo
@@ -23,14 +36,14 @@ internal class EditorLensManager(private val editor: Editor) {
val existingLens = lenses[highlighter]
if (existingLens != null) {
if (existingLens.update(info, settings)) {
if (existingLens.update(info)) {
return
}
existingLens.hide()
}
val newLens = EditorLens.show(editor, info, settings)
val newLens = EditorLens.show(editor, info)
if (newLens != null) {
lenses[highlighter] = newLens
}
@@ -56,10 +69,6 @@ internal class EditorLensManager(private val editor: Editor) {
}
}
fun onFoldRegionsChanged() {
lenses.values.forEach(EditorLens::onFoldRegionsChanged)
}
sealed interface Command {
fun apply(lensManager: EditorLensManager)
@@ -74,12 +83,6 @@ internal class EditorLensManager(private val editor: Editor) {
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.editor.markup.RangeHighlighter
internal class EditorLensManagerDispatcher(private val lensManager: EditorLensManager) {
class EditorLensManagerDispatcher(private val lensManager: EditorLensManager) {
private var queuedItems = mutableListOf<EditorLensManager.Command>()
private var isEnqueued = false
@@ -15,10 +15,6 @@ internal class EditorLensManagerDispatcher(private val lensManager: EditorLensMa
enqueue(EditorLensManager.Command.Hide(highlighter))
}
fun hideAll() {
enqueue(EditorLensManager.Command.HideAll)
}
private fun enqueue(item: EditorLensManager.Command) {
synchronized(this) {
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,17 +1,23 @@
package com.chylex.intellij.inspectionlens.editor
import com.chylex.intellij.inspectionlens.settings.LensSettingsState
import com.chylex.intellij.inspectionlens.settings.LensSettings
import com.intellij.codeInsight.daemon.impl.HighlightInfo
import com.intellij.openapi.components.service
import com.intellij.openapi.Disposable
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.impl.DocumentMarkupModel
import com.intellij.openapi.editor.impl.event.MarkupModelListener
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].
*/
internal class LensMarkupModelListener(private val lensManagerDispatcher: EditorLensManagerDispatcher) : MarkupModelListener {
private val settings = service<LensSettingsState>()
internal class LensMarkupModelListener private constructor(editor: Editor) : MarkupModelListener {
private val lensManagerDispatcher = EditorLensManagerDispatcher(EditorLensManager.getOrCreate(editor))
private val severityFilter = LensSettings.createSeverityFilter()
override fun afterAdded(highlighter: RangeHighlighterEx) {
showIfValid(highlighter)
@@ -39,25 +45,28 @@ internal class LensMarkupModelListener(private val lensManagerDispatcher: Editor
}
}
fun showAllValid(highlighters: Array<RangeHighlighter>) {
private fun showAllValid(highlighters: Array<RangeHighlighter>) {
highlighters.forEach(::showIfValid)
}
fun hideAll() {
lensManagerDispatcher.hideAll()
}
private fun getFilteredHighlightInfo(highlighter: RangeHighlighter): HighlightInfo? {
return HighlightInfo.fromRangeHighlighter(highlighter)?.takeIf { settings.severityFilter.test(it.severity) }
return HighlightInfo.fromRangeHighlighter(highlighter)?.takeIf { severityFilter.test(it.severity) }
}
private inline fun runWithHighlighterIfValid(highlighter: RangeHighlighter, actionForImmediate: (HighlighterWithInfo) -> Unit, actionForAsync: (HighlighterWithInfo.Async) -> Unit) {
val info = highlighter.takeIf { it.isValid }?.let(::getFilteredHighlightInfo)
val info = getHighlightInfoIfValid(highlighter)
if (info != null) {
processHighlighterWithInfo(HighlighterWithInfo.from(highlighter, info), actionForImmediate, actionForAsync)
}
}
private fun getHighlightInfoIfValid(highlighter: RangeHighlighter): HighlightInfo? {
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)
@@ -66,4 +75,37 @@ internal class LensMarkupModelListener(private val lensManagerDispatcher: Editor
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,21 +1,20 @@
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.editor.lens.ColorMode.Companion.createColorAttributes
import com.intellij.lang.annotation.HighlightSeverity
import com.intellij.ui.ColorUtil
import com.intellij.ui.ColorUtil.toAlpha
import com.intellij.ui.JBColor
import java.awt.Color
import java.awt.Font
import java.util.Collections
import java.util.EnumMap
/**
* Determines properties of inspection lenses based on severity.
*/
@Suppress("UseJBColor", "InspectionUsingGrayColors")
enum class LensSeverity(private val baseColor: Color, private val lightThemeDarkening: Int, private val darkThemeBrightening: Int) {
enum class LensSeverity(baseColor: Color, lightThemeDarkening: Int, darkThemeBrightening: Int) {
ERROR (Color(158, 41, 39), lightThemeDarkening = 2, darkThemeBrightening = 4),
WARNING (Color(190, 145, 23), lightThemeDarkening = 5, darkThemeBrightening = 1),
WEAK_WARNING (Color(117, 109, 86), lightThemeDarkening = 4, darkThemeBrightening = 4),
@@ -24,37 +23,18 @@ enum class LensSeverity(private val baseColor: Color, private val lightThemeDark
TYPO (Color( 73, 156, 84), lightThemeDarkening = 4, darkThemeBrightening = 1),
OTHER (Color(128, 128, 128), lightThemeDarkening = 2, darkThemeBrightening = 2);
private lateinit var textAttributes: EnumMap<ColorMode, LensSeverityTextAttributes>
private lateinit var lineAttributes: EnumMap<ColorMode, LensSeverityTextAttributes>
val textAttributes: LensSeverityTextAttributes
val lineAttributes: LensSeverityTextAttributes
init {
refreshColors()
}
val lightThemeColor = ColorUtil.saturate(ColorUtil.darker(baseColor, lightThemeDarkening), 1)
val darkThemeColor = ColorUtil.desaturate(ColorUtil.brighter(baseColor, darkThemeBrightening), 2)
internal fun refreshColors() {
val theme = if (ColorGenerator.useGenerator) {
ColorGenerator.generate(name, baseColor)
}
else {
val lightThemeTextColor = ColorUtil.saturate(ColorUtil.darker(baseColor, lightThemeDarkening), 1)
val darkThemeTextColor = ColorUtil.desaturate(ColorUtil.brighter(baseColor, darkThemeBrightening), 2)
val textColor = JBColor(lightThemeColor, darkThemeColor)
val lineColor = JBColor(toAlpha(lightThemeColor, 10), toAlpha(darkThemeColor, 13))
val lightThemeLineColor = toAlpha(lightThemeTextColor, 10)
val darkThemeLineColor = toAlpha(darkThemeTextColor, 13)
ColorGenerator.Theme(lightThemeTextColor, lightThemeLineColor, darkThemeTextColor, darkThemeLineColor)
}
textAttributes = createColorAttributes(theme.lightThemeTextColor, theme.darkThemeTextColor) { LensSeverityTextAttributes(foregroundColor = it, fontStyle = Font.ITALIC) }
lineAttributes = createColorAttributes(theme.lightThemeLineColor, theme.darkThemeLineColor) { LensSeverityTextAttributes(backgroundColor = it) }
}
internal fun getTextAttributes(colorMode: ColorMode): LensSeverityTextAttributes {
return textAttributes.getValue(colorMode)
}
internal fun getLineAttributes(colorMode: ColorMode): LensSeverityTextAttributes {
return lineAttributes.getValue(colorMode)
textAttributes = LensSeverityTextAttributes(foregroundColor = textColor, fontStyle = Font.ITALIC)
lineAttributes = LensSeverityTextAttributes(backgroundColor = lineColor)
}
companion object {
@@ -74,7 +54,7 @@ enum class LensSeverity(private val baseColor: Color, private val lightThemeDark
*/
internal fun registerMapping(severity: HighlightSeverity, 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.ui.JBColor

View File

@@ -1,80 +0,0 @@
package com.chylex.intellij.inspectionlens.editor.lens
import com.intellij.ui.ColorUtil
import java.awt.Color
import java.util.Locale
import kotlin.math.abs
import kotlin.math.pow
internal object ColorGenerator {
private const val HACK_BRIGHTNESS_STEP = 1.025F
var useGenerator = false
var lightThemeDesaturation = 1
var lightThemeThreshold = 32
var lightThemeLineAlpha = 10
var darkThemeDesaturation = 2
var darkThemeThreshold = 62
var darkThemeLineAlpha = 13
data class Theme(
val lightThemeTextColor: Color,
val lightThemeLineColor: Color,
val darkThemeTextColor: Color,
val darkThemeLineColor: Color,
)
fun generate(name: String, baseColor: Color): Theme {
val lightThemeTextColor: Color = findColorWithPerceivedBrightness("$name / Light", baseColor, lightThemeDesaturation, 50 downTo -50) { it <= lightThemeThreshold }
val darkThemeTextColor: Color = findColorWithPerceivedBrightness("$name / Dark", baseColor, darkThemeDesaturation, -50..50) { it >= darkThemeThreshold }
val lightThemeLineColor = ColorUtil.toAlpha(lightThemeTextColor, lightThemeLineAlpha)
val darkThemeLineColor = ColorUtil.toAlpha(darkThemeTextColor, darkThemeLineAlpha)
return Theme(lightThemeTextColor, lightThemeLineColor, darkThemeTextColor, darkThemeLineColor)
}
private fun findColorWithPerceivedBrightness(name: String, baseColor: Color, desaturation: Int, steps: IntProgression, testPerceivedBrightness: (Double) -> Boolean): Color {
var finalColor = baseColor
var finalBrightening = 0
var finalPerceivedBrightness = 0.0
for (brightening in steps) {
finalColor = ColorUtil.desaturate(ColorUtil.hackBrightness(baseColor, abs(brightening), if (brightening < 0) 1F / HACK_BRIGHTNESS_STEP else HACK_BRIGHTNESS_STEP), desaturation)
finalBrightening = brightening
finalPerceivedBrightness = getPerceivedBrightness(finalColor)
if (testPerceivedBrightness(finalPerceivedBrightness)) {
break
}
}
println("$name - ${baseColor.red},${baseColor.green},${baseColor.blue} --> step $finalBrightening; brightness ${String.format(Locale.ROOT, "%.2f", finalPerceivedBrightness)}; color ${finalColor.red},${finalColor.green},${finalColor.blue}")
return finalColor
}
private fun getLuminance(r: Double, g: Double, b: Double): Double {
return 0.2126 * srgbToLinearRgb(r) + 0.7152 * srgbToLinearRgb(g) + 0.0722 * srgbToLinearRgb(b)
}
private fun srgbToLinearRgb(channel: Double): Double {
return if (channel <= 0.04045)
channel / 12.92
else
((channel + 0.055) / 1.055).pow(2.4)
}
private fun getPerceivedBrightness(luminance: Double): Double {
return if (luminance <= (216.0 / 24389.0))
luminance * (24389.0 / 27.0)
else
luminance.pow(1.0 / 3.0) * 116 - 16
}
private fun getPerceivedBrightness(color: Color): Double {
val r = color.red / 255.0
val g = color.green / 255.0
val b = color.blue / 255.0
return getPerceivedBrightness(getLuminance(r, g, b))
}
}

View File

@@ -1,35 +0,0 @@
package com.chylex.intellij.inspectionlens.editor.lens
import com.intellij.openapi.editor.Editor
import com.intellij.openapi.util.Key
import com.intellij.ui.JBColor
import java.awt.Color
import java.util.EnumMap
internal enum class ColorMode {
AUTO,
ALWAYS_LIGHT,
ALWAYS_DARK;
companion object {
private val KEY = Key.create<ColorMode>("InspectionLens.ColorMode")
fun getFromEditor(editor: Editor): ColorMode {
return KEY.get(editor, AUTO)
}
fun setForEditor(editor: Editor, colorMode: ColorMode) {
KEY.set(editor, colorMode)
}
inline fun createColorAttributes(lightThemeColor: Color, darkThemeColor: Color, attributesFactory: (JBColor) -> LensSeverityTextAttributes): EnumMap<ColorMode, LensSeverityTextAttributes> {
val result = EnumMap<ColorMode, LensSeverityTextAttributes>(ColorMode::class.java)
result[AUTO] = attributesFactory(JBColor(lightThemeColor, darkThemeColor))
result[ALWAYS_LIGHT] = attributesFactory(JBColor(lightThemeColor, lightThemeColor))
result[ALWAYS_DARK] = attributesFactory(JBColor(darkThemeColor, darkThemeColor))
return result
}
}
}

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,36 +0,0 @@
package com.chylex.intellij.inspectionlens.editor.lens
import com.intellij.codeInsight.daemon.impl.IntentionsUI
import com.intellij.codeInsight.hint.HintManager
import com.intellij.codeInsight.intention.impl.ShowIntentionActionsHandler
import com.intellij.lang.LangBundle
import com.intellij.openapi.editor.Editor
import com.intellij.openapi.project.Project
import com.intellij.psi.PsiDocumentManager
import com.intellij.psi.PsiFile
import com.intellij.psi.util.PsiUtilBase
internal object IntentionsPopup {
fun show(editor: Editor) {
if (!tryShow(editor)) {
HintManager.getInstance().showInformationHint(editor, LangBundle.message("hint.text.no.context.actions.available.at.this.location"))
}
}
private fun tryShow(editor: Editor): Boolean {
val project = editor.project ?: return false
val file = PsiUtilBase.getPsiFileInEditor(editor, project) ?: return false
PsiDocumentManager.getInstance(project).commitAllDocuments()
IntentionsUI.getInstance(project).hide()
HANDLER.showIntentionHint(project, editor, file, showFeedbackOnEmptyMenu = true)
return true
}
private val HANDLER = object : ShowIntentionActionsHandler() {
public override fun showIntentionHint(project: Project, editor: Editor, file: PsiFile, showFeedbackOnEmptyMenu: Boolean) {
super.showIntentionHint(project, editor, file, showFeedbackOnEmptyMenu)
}
}
}

View File

@@ -1,185 +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.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.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, settings: LensSettingsState) : HintRenderer(null), InputHandler {
private val useEditorFont = settings.useEditorFont
private lateinit var inlay: Inlay<*>
private lateinit var severity: LensSeverity
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)
text = description
severity = LensSeverity.from(info.severity)
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) {
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 + 1
val w = inlay.widthInPixels - UNDERLINE_WIDTH_REDUCTION - extraRightPadding
val h = editor.descent
g.color = getTextAttributes(editor).foregroundColor
EffectPainter.LINE_UNDERSCORE.paint(g as Graphics2D, x, y, w, h, font)
}
override fun getTextAttributes(editor: Editor): TextAttributes {
return severity.getTextAttributes(ColorMode.getFromEditor(editor))
}
override fun useEditorFont(): Boolean {
return useEditorFont
}
override fun mouseMoved(event: MouseEvent, translated: Point) {
setHovered(isHoveringText(translated))
}
override fun mouseExited() {
setHovered(false)
}
private fun setHovered(hovered: Boolean) {
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) {
if (!isHoveringText(translated)) {
return
}
if (SwingUtilities.isLeftMouseButton(event) || SwingUtilities.isMiddleMouseButton(event)) {
event.consume()
val editor = inlay.editor
moveToOffset(editor, info.actualStartOffset)
if (SwingUtilities.isLeftMouseButton(event)) {
IntentionsPopup.show(editor)
}
}
}
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
private const val MAX_DESCRIPTION_LENGTH = 120
/**
* Kotlin compiler inspections have an `[UPPERCASE_TAG]` at the beginning.
*/
private val UPPERCASE_TAG_REGEX = Pattern.compile("^\\[[A-Z_]+] ")
private fun getValidDescriptionText(text: String?): String {
return if (text.isNullOrBlank()) " " else addEllipsisOrMissingPeriod(unescapeHtmlEntities(stripUppercaseTag(text)))
}
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): String {
return when {
text.length > MAX_DESCRIPTION_LENGTH -> text.take(MAX_DESCRIPTION_LENGTH).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,69 +1,23 @@
package com.chylex.intellij.inspectionlens.settings
import com.chylex.intellij.inspectionlens.editor.lens.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 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 {
@@ -71,59 +25,31 @@ class LensApplicationConfigurable : BoundConfigurable("Inspection Lens"), Config
}
override fun createPanel(): DialogPanel {
val settings = settingsService.state
val settings = service<LensSettingsState>().state
return panel {
group("Shown Severities") {
for ((id, severity, textAttributes) in allSeverities) {
group("Enabled Severities") {
val knownSeverities = globalSeverities().sortedByDescending(HighlightSeverity::myVal)
for (severity in knownSeverities) {
row {
checkBox(severity.name)
.bindSelectedToNotIn(settings.hiddenSeverities, id, severity)
.gap(RightGap.COLUMNS)
labelWithAttributes("Example", textAttributes)
}.layout(RowLayout.PARENT_GRID)
checkBox(severity.displayCapitalizedName).bindSelectedToNotInSet(settings.disabledSeveritiesReal, severity.name)
}
}
val knownSeverityNames = knownSeverities.map { it.name }.toSet()
val noLongerKnownSeverityNames = settings.disabledSeveritiesReal.filterNot { it in knownSeverityNames }
for (severityName in noLongerKnownSeverityNames) {
row {
checkBox("Other").bindSelected(settings::showUnknownSeverities)
checkBox("Unknown ($severityName)").bindSelectedToNotInSet(settings.disabledSeveritiesReal, severityName)
}
}
group("Appearance") {
row {
checkBox("Use editor font").bindSelected(settings::useEditorFont)
}
}
}
}
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()
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) })
}
}

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
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.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
import com.intellij.util.xmlb.annotations.XCollection
@State(
name = LensApplicationConfigurable.ID,
@@ -16,33 +14,13 @@ import com.intellij.util.xmlb.annotations.XMap
)
class LensSettingsState : SimplePersistentStateComponent<LensSettingsState.State>(State()) {
class State : BaseState() {
@get:XMap
val hiddenSeverities by map<String, StoredSeverity>()
@get:XCollection
private val disabledSeverities by stringSet()
var showUnknownSeverities by property(true)
var useEditorFont by property(true)
}
val disabledSeveritiesReadOnly: Set<String>
get() = disabledSeverities
@get:Synchronized
@set:Synchronized
var severityFilter = createSeverityFilter()
private set
val useEditorFont
get() = state.useEditorFont
override fun loadState(state: State) {
super.loadState(state)
update()
}
fun update() {
severityFilter = createSeverityFilter()
InspectionLens.scheduleRefresh()
}
private fun createSeverityFilter(): LensSeverityFilter {
val state = state
return LensSeverityFilter(state.hiddenSeverities.keys, state.showUnknownSeverities)
@Transient
val disabledSeveritiesReal = ModificationTrackingCollection(disabledSeverities, ::incrementModificationCount)
}
}

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,33 +4,19 @@
<vendor url="https://chylex.com">chylex</vendor>
<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>
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>.
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>
Left-click an inspection to show quick fixes. Middle-click an inspection to navigate to the relevant code in the editor.
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>
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>
<change-notes><![CDATA[
<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>
<ul>
<li>Partially reverted fix for inspections that include HTML in their description due to breaking inspections with angled brackets.</li>
@@ -92,10 +78,4 @@
<projectListeners>
<listener class="com.chylex.intellij.inspectionlens.InspectionLensFileOpenedListener" topic="com.intellij.openapi.fileEditor.FileOpenedSyncListener" />
</projectListeners>
<actions>
<action id="com.chylex.intellij.inspectionlens.debug.ShowColorEditorAction" class="com.chylex.intellij.inspectionlens.debug.ShowColorEditorAction" text="Inspection Lens - Show Color Editor">
<add-to-group group-id="ToolsMenu" anchor="last" />
</action>
</actions>
</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 org.junit.jupiter.api.Assertions.assertEquals
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
inner class IdeaHighlightSeverityAssumptions {