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

7 Commits

Author SHA1 Message Date
83ba551f82 Debug tools 2025-01-25 11:57:06 +01:00
8936f0e5be Release 1.5 2024-12-26 23:33:36 +01:00
89e71d5301 Make middle-clicking an inspection lens jump to the start of the inspection highlight 2024-12-26 06:53:42 +01:00
4c80573375 Limit description length to 120 characters
Closes #18
2024-12-26 06:27:28 +01:00
816440a150 Strip uppercase tags from the beginning of inspection description
Meant for Kotlin compiler inspections, but could potentially affect others.
2024-12-26 01:54:16 +01:00
4899498522 Make inspection lenses clickable and show popup with intentions 2024-12-25 23:44:30 +01:00
8ee14ff55e Hide line background highlighters in folded regions to fix documentation rendering
Closes #26
2024-12-24 19:38:31 +01:00
15 changed files with 662 additions and 51 deletions

View File

@@ -4,6 +4,8 @@ Displays errors, warnings, and other inspections inline. Highlights the backgrou
By default, the plugin shows **Errors**, **Warnings**, **Weak Warnings**, **Server Problems**, **Grammar Errors**, **Typos**, and other inspections with a high enough severity level. Configure visible severities in **Settings | Tools | Inspection Lens**. By default, the plugin shows **Errors**, **Warnings**, **Weak Warnings**, **Server Problems**, **Grammar Errors**, **Typos**, and other inspections with a high enough severity level. Configure visible severities in **Settings | Tools | Inspection Lens**.
Left-click an inspection to show quick fixes. Middle-click an inspection to navigate to the relevant code in the editor.
Inspired by [Error Lens](https://marketplace.visualstudio.com/items?itemName=usernamehw.errorlens) for Visual Studio Code, and [Inline Error](https://plugins.jetbrains.com/plugin/17302-inlineerror) for IntelliJ Platform. Inspired by [Error Lens](https://marketplace.visualstudio.com/items?itemName=usernamehw.errorlens) for Visual Studio Code, and [Inline Error](https://plugins.jetbrains.com/plugin/17302-inlineerror) for IntelliJ Platform.
![Inspection Lens Screenshot](.github/readme/intellij.png) ![Inspection Lens Screenshot](.github/readme/intellij.png)

View File

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

View File

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

@@ -2,6 +2,7 @@ package com.chylex.intellij.inspectionlens.editor
import com.intellij.openapi.Disposable import com.intellij.openapi.Disposable
import com.intellij.openapi.editor.Editor import com.intellij.openapi.editor.Editor
import com.intellij.openapi.editor.ex.FoldingModelEx
import com.intellij.openapi.editor.ex.MarkupModelEx import com.intellij.openapi.editor.ex.MarkupModelEx
import com.intellij.openapi.editor.impl.DocumentMarkupModel import com.intellij.openapi.editor.impl.DocumentMarkupModel
import com.intellij.openapi.util.Disposer import com.intellij.openapi.util.Disposer
@@ -13,6 +14,7 @@ import com.intellij.openapi.util.Key
internal class EditorLensFeatures private constructor( internal class EditorLensFeatures private constructor(
editor: Editor, editor: Editor,
private val markupModel: MarkupModelEx, private val markupModel: MarkupModelEx,
foldingModel: FoldingModelEx?,
disposable: Disposable disposable: Disposable
) { ) {
private val lensManager = EditorLensManager(editor) private val lensManager = EditorLensManager(editor)
@@ -22,6 +24,8 @@ internal class EditorLensFeatures private constructor(
init { init {
markupModel.addMarkupModelListener(disposable, markupModelListener) markupModel.addMarkupModelListener(disposable, markupModelListener)
markupModelListener.showAllValid(markupModel.allHighlighters) markupModelListener.showAllValid(markupModel.allHighlighters)
foldingModel?.addListener(LensFoldingModelListener(lensManager), disposable)
} }
private fun refresh() { private fun refresh() {
@@ -38,7 +42,8 @@ internal class EditorLensFeatures private constructor(
} }
val markupModel = DocumentMarkupModel.forDocument(editor.document, editor.project, false) as? MarkupModelEx ?: return val markupModel = DocumentMarkupModel.forDocument(editor.document, editor.project, false) as? MarkupModelEx ?: return
val features = EditorLensFeatures(editor, markupModel, disposable) val foldingModel = editor.foldingModel as? FoldingModelEx
val features = EditorLensFeatures(editor, markupModel, foldingModel, disposable)
editor.putUserData(EDITOR_KEY, features) editor.putUserData(EDITOR_KEY, features)
Disposer.register(disposable) { editor.putUserData(EDITOR_KEY, null) } Disposer.register(disposable) { editor.putUserData(EDITOR_KEY, null) }

View File

@@ -56,6 +56,10 @@ internal class EditorLensManager(private val editor: Editor) {
} }
} }
fun onFoldRegionsChanged() {
lenses.values.forEach(EditorLens::onFoldRegionsChanged)
}
sealed interface Command { sealed interface Command {
fun apply(lensManager: EditorLensManager) fun apply(lensManager: EditorLensManager)

View File

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

View File

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

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

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

View File

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

View File

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

View File

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

@@ -3,61 +3,183 @@ package com.chylex.intellij.inspectionlens.editor.lens
import com.chylex.intellij.inspectionlens.settings.LensSettingsState import com.chylex.intellij.inspectionlens.settings.LensSettingsState
import com.intellij.codeInsight.daemon.impl.HighlightInfo import com.intellij.codeInsight.daemon.impl.HighlightInfo
import com.intellij.codeInsight.daemon.impl.HintRenderer import com.intellij.codeInsight.daemon.impl.HintRenderer
import com.intellij.codeInsight.hints.presentation.InputHandler
import com.intellij.openapi.editor.Editor import com.intellij.openapi.editor.Editor
import com.intellij.openapi.editor.Inlay import com.intellij.openapi.editor.Inlay
import com.intellij.openapi.editor.ScrollType
import com.intellij.openapi.editor.colors.EditorFontType
import com.intellij.openapi.editor.ex.EditorEx
import com.intellij.openapi.editor.impl.EditorImpl
import com.intellij.openapi.editor.markup.TextAttributes import com.intellij.openapi.editor.markup.TextAttributes
import com.intellij.openapi.util.text.StringUtil import com.intellij.openapi.util.text.StringUtil
import com.intellij.ui.paint.EffectPainter
import java.awt.Cursor
import java.awt.Graphics import java.awt.Graphics
import java.awt.Graphics2D
import java.awt.Point
import java.awt.Rectangle import java.awt.Rectangle
import java.awt.event.MouseEvent
import java.util.regex.Pattern
import javax.swing.SwingUtilities
/** /**
* Renders the text of an inspection lens. * Renders the text of an inspection lens.
*/ */
class LensRenderer(info: HighlightInfo, settings: LensSettingsState) : HintRenderer(null) { class LensRenderer(private var info: HighlightInfo, settings: LensSettingsState) : HintRenderer(null), InputHandler {
private val useEditorFont = settings.useEditorFont private val useEditorFont = settings.useEditorFont
private lateinit var inlay: Inlay<*>
private lateinit var severity: LensSeverity private lateinit var severity: LensSeverity
private var extraRightPadding = 0
private var hovered = false
init { init {
setPropertiesFrom(info) setPropertiesFrom(info)
} }
fun setInlay(inlay: Inlay<*>) {
check(!this::inlay.isInitialized) { "Inlay already set" }
this.inlay = inlay
}
fun setPropertiesFrom(info: HighlightInfo) { fun setPropertiesFrom(info: HighlightInfo) {
text = getValidDescriptionText(info.description) this.info = info
val description = getValidDescriptionText(info.description)
text = description
severity = LensSeverity.from(info.severity) severity = LensSeverity.from(info.severity)
extraRightPadding = if (description.lastOrNull() == '.') 2 else 0
} }
override fun paint(inlay: Inlay<*>, g: Graphics, r: Rectangle, textAttributes: TextAttributes) { override fun paint(inlay: Inlay<*>, g: Graphics, r: Rectangle, textAttributes: TextAttributes) {
fixBaselineForTextRendering(r) fixBaselineForTextRendering(r)
super.paint(inlay, g, r, textAttributes) super.paint(inlay, g, r, textAttributes)
if (hovered) {
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 { override fun getTextAttributes(editor: Editor): TextAttributes {
return severity.textAttributes return severity.getTextAttributes(ColorMode.getFromEditor(editor))
} }
override fun useEditorFont(): Boolean { override fun useEditorFont(): Boolean {
return useEditorFont 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 { 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 { private fun getValidDescriptionText(text: String?): String {
return if (text.isNullOrBlank()) " " else addMissingPeriod(unescapeHtmlEntities(text)) return if (text.isNullOrBlank()) " " else addEllipsisOrMissingPeriod(unescapeHtmlEntities(stripUppercaseTag(text)))
} }
private fun unescapeHtmlEntities(potentialHtml: String): String { private fun stripUppercaseTag(text: String): String {
return potentialHtml.ifContains('&', StringUtil::unescapeXmlEntities) if (text.startsWith('[')) {
val matcher = UPPERCASE_TAG_REGEX.matcher(text)
if (matcher.find()) {
return text.substring(matcher.end())
}
}
return text
} }
private fun addMissingPeriod(text: String): String { private fun unescapeHtmlEntities(text: String): String {
return if (text.endsWith('.')) text else "$text." return if (text.contains('&')) StringUtil.unescapeXmlEntities(text) else text
} }
private inline fun String.ifContains(charToTest: Char, action: (String) -> String): String { private fun addEllipsisOrMissingPeriod(text: String): String {
return if (this.contains(charToTest)) action(this) else this 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) { private fun fixBaselineForTextRendering(rect: Rectangle) {
rect.y += 1 rect.y += 1
} }
private fun moveToOffset(editor: Editor, offset: Int) {
editor.caretModel.moveToOffset(offset)
editor.scrollingModel.scrollToCaret(ScrollType.MAKE_VISIBLE)
}
} }
} }

View File

@@ -2,19 +2,20 @@ package com.chylex.intellij.inspectionlens.editor.lens
import com.chylex.intellij.inspectionlens.InspectionLens import com.chylex.intellij.inspectionlens.InspectionLens
import com.chylex.intellij.inspectionlens.compatibility.SpellCheckerSupport 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.lang.annotation.HighlightSeverity
import com.intellij.ui.ColorUtil import com.intellij.ui.ColorUtil
import com.intellij.ui.ColorUtil.toAlpha import com.intellij.ui.ColorUtil.toAlpha
import com.intellij.ui.JBColor
import java.awt.Color import java.awt.Color
import java.awt.Font import java.awt.Font
import java.util.Collections import java.util.Collections
import java.util.EnumMap
/** /**
* Determines properties of inspection lenses based on severity. * Determines properties of inspection lenses based on severity.
*/ */
@Suppress("UseJBColor", "InspectionUsingGrayColors") @Suppress("UseJBColor", "InspectionUsingGrayColors")
enum class LensSeverity(baseColor: Color, lightThemeDarkening: Int, darkThemeBrightening: Int) { enum class LensSeverity(private val baseColor: Color, private val lightThemeDarkening: Int, private val darkThemeBrightening: Int) {
ERROR (Color(158, 41, 39), lightThemeDarkening = 2, darkThemeBrightening = 4), ERROR (Color(158, 41, 39), lightThemeDarkening = 2, darkThemeBrightening = 4),
WARNING (Color(190, 145, 23), lightThemeDarkening = 5, darkThemeBrightening = 1), WARNING (Color(190, 145, 23), lightThemeDarkening = 5, darkThemeBrightening = 1),
WEAK_WARNING (Color(117, 109, 86), lightThemeDarkening = 4, darkThemeBrightening = 4), WEAK_WARNING (Color(117, 109, 86), lightThemeDarkening = 4, darkThemeBrightening = 4),
@@ -23,18 +24,37 @@ enum class LensSeverity(baseColor: Color, lightThemeDarkening: Int, darkThemeBri
TYPO (Color( 73, 156, 84), lightThemeDarkening = 4, darkThemeBrightening = 1), TYPO (Color( 73, 156, 84), lightThemeDarkening = 4, darkThemeBrightening = 1),
OTHER (Color(128, 128, 128), lightThemeDarkening = 2, darkThemeBrightening = 2); OTHER (Color(128, 128, 128), lightThemeDarkening = 2, darkThemeBrightening = 2);
val textAttributes: LensSeverityTextAttributes private lateinit var textAttributes: EnumMap<ColorMode, LensSeverityTextAttributes>
val lineAttributes: LensSeverityTextAttributes private lateinit var lineAttributes: EnumMap<ColorMode, LensSeverityTextAttributes>
init { init {
val lightThemeColor = ColorUtil.saturate(ColorUtil.darker(baseColor, lightThemeDarkening), 1) refreshColors()
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 lightThemeLineColor = toAlpha(lightThemeTextColor, 10)
val darkThemeLineColor = toAlpha(darkThemeTextColor, 13)
ColorGenerator.Theme(lightThemeTextColor, lightThemeLineColor, darkThemeTextColor, darkThemeLineColor)
}
val textColor = JBColor(lightThemeColor, darkThemeColor) textAttributes = createColorAttributes(theme.lightThemeTextColor, theme.darkThemeTextColor) { LensSeverityTextAttributes(foregroundColor = it, fontStyle = Font.ITALIC) }
val lineColor = JBColor(toAlpha(lightThemeColor, 10), toAlpha(darkThemeColor, 13)) lineAttributes = createColorAttributes(theme.lightThemeLineColor, theme.darkThemeLineColor) { LensSeverityTextAttributes(backgroundColor = it) }
}
textAttributes = LensSeverityTextAttributes(foregroundColor = textColor, fontStyle = Font.ITALIC)
lineAttributes = LensSeverityTextAttributes(backgroundColor = lineColor) internal fun getTextAttributes(colorMode: ColorMode): LensSeverityTextAttributes {
return textAttributes.getValue(colorMode)
}
internal fun getLineAttributes(colorMode: ColorMode): LensSeverityTextAttributes {
return lineAttributes.getValue(colorMode)
} }
companion object { companion object {

View File

@@ -8,10 +8,21 @@
<br><br> <br><br>
By default, the plugin shows <b>Errors</b>, <b>Warnings</b>, <b>Weak Warnings</b>, <b>Server Problems</b>, <b>Grammar Errors</b>, <b>Typos</b>, and other inspections with a high enough severity level. Configure visible severities in <b>Settings | Tools | Inspection Lens</a>. By default, the plugin shows <b>Errors</b>, <b>Warnings</b>, <b>Weak Warnings</b>, <b>Server Problems</b>, <b>Grammar Errors</b>, <b>Typos</b>, and other inspections with a high enough severity level. Configure visible severities in <b>Settings | Tools | Inspection Lens</a>.
<br><br> <br><br>
Left-click an inspection to show quick fixes. Middle-click an inspection to navigate to the relevant code in the editor.
<br><br>
Inspired by <a href="https://marketplace.visualstudio.com/items?itemName=usernamehw.errorlens">Error Lens</a> for VS Code, and <a href="https://plugins.jetbrains.com/plugin/17302-inlineerror">Inline Error</a> for IntelliJ Platform. Inspired by <a href="https://marketplace.visualstudio.com/items?itemName=usernamehw.errorlens">Error Lens</a> for VS Code, and <a href="https://plugins.jetbrains.com/plugin/17302-inlineerror">Inline Error</a> for IntelliJ Platform.
]]></description> ]]></description>
<change-notes><![CDATA[ <change-notes><![CDATA[
<b>Version 1.5</b>
<ul>
<li>Added possibility to left-click an inspection to show quick fixes.</li>
<li>Added possibility to middle-click an inspection to navigate to relevant code in the editor.</li>
<li>Added option to use UI font instead of editor font.</li>
<li>Long inspection descriptions are now truncated to 120 characters.</li>
<li>Improved descriptions of Kotlin compiler inspections.</li>
<li>Fixed visual artifacts in Rendered Doc comments.</li>
</ul>
<b>Version 1.4.1</b> <b>Version 1.4.1</b>
<ul> <ul>
<li>Fixed warnings in usage of IntelliJ SDK.</li> <li>Fixed warnings in usage of IntelliJ SDK.</li>
@@ -81,4 +92,10 @@
<projectListeners> <projectListeners>
<listener class="com.chylex.intellij.inspectionlens.InspectionLensFileOpenedListener" topic="com.intellij.openapi.fileEditor.FileOpenedSyncListener" /> <listener class="com.chylex.intellij.inspectionlens.InspectionLensFileOpenedListener" topic="com.intellij.openapi.fileEditor.FileOpenedSyncListener" />
</projectListeners> </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> </idea-plugin>