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

8 Commits

20 changed files with 155 additions and 490 deletions

2
.github/FUNDING.yml vendored
View File

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

1
.gitignore vendored
View File

@@ -3,4 +3,5 @@
!/.idea/runConfigurations !/.idea/runConfigurations
/.gradle/ /.gradle/
/.intellijPlatform/
/build/ /build/

View File

@@ -2,9 +2,9 @@
Displays errors, warnings, and other inspections inline. Highlights the background of lines with inspections. Supports light and dark themes out of the box. Displays errors, warnings, and other inspections inline. Highlights the background of lines with inspections. Supports light and dark themes out of the box.
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. Left-click an inspection to show quick fixes. Middle-click an inspection to navigate to the relevant code in the editor.
Left-click an inspection to show quick fixes. Middle-click an inspection to navigate to the relevant code in the editor. Configure appearance, behavior of clicking on inspections, and visible severities in **Settings | Tools | Inspection Lens**.
Inspired by [Error Lens](https://marketplace.visualstudio.com/items?itemName=usernamehw.errorlens) for Visual Studio Code, and [Inline Error](https://plugins.jetbrains.com/plugin/17302-inlineerror) for IntelliJ Platform. Inspired by [Error Lens](https://marketplace.visualstudio.com/items?itemName=usernamehw.errorlens) for Visual Studio Code, and [Inline Error](https://plugins.jetbrains.com/plugin/17302-inlineerror) for IntelliJ Platform.

View File

@@ -1,48 +1,57 @@
@file:Suppress("ConvertLambdaToReference") @file:Suppress("ConvertLambdaToReference")
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
plugins { plugins {
kotlin("jvm") version "1.8.0" kotlin("jvm")
id("org.jetbrains.intellij") version "1.17.0" id("org.jetbrains.intellij.platform")
} }
group = "com.chylex.intellij.inspectionlens" group = "com.chylex.intellij.inspectionlens"
version = "1.5" version = "1.5.1"
repositories { repositories {
mavenCentral() mavenCentral()
intellijPlatform {
defaultRepositories()
}
} }
intellij { dependencies {
version.set("2023.3.3") intellijPlatform {
updateSinceUntilBuild.set(false) intellijIdeaUltimate("2023.3.3")
bundledPlugin("tanvd.grazi")
plugins.add("tanvd.grazi") // https://plugins.jetbrains.com/plugin/12175-grazie-lite/versions
// plugin("tanvd.grazi", "233.13135.14")
}
testImplementation("org.junit.jupiter:junit-jupiter:5.9.2")
}
intellijPlatform {
pluginConfiguration {
ideaVersion {
sinceBuild.set("233.11361.10")
untilBuild.set(provider { null })
}
}
} }
kotlin { kotlin {
jvmToolchain(17) jvmToolchain(17)
compilerOptions {
freeCompilerArgs = listOf(
"-X" + "jvm-default=all"
)
}
} }
dependencies { tasks.withType<Test>().configureEach {
testImplementation("org.junit.jupiter:junit-jupiter:5.9.2")
}
tasks.patchPluginXml {
sinceBuild.set("233.11361.10")
}
tasks.buildSearchableOptions {
enabled = false
}
tasks.test {
useJUnitPlatform() useJUnitPlatform()
} }
tasks.withType<KotlinCompile> { val testSnapshot by intellijPlatformTesting.testIde.registering {
kotlinOptions.freeCompilerArgs = listOf( version = "LATEST-EAP-SNAPSHOT"
"-Xjvm-default=all" useInstaller = false
)
} }

Binary file not shown.

View File

@@ -1,6 +1,6 @@
distributionBase=GRADLE_USER_HOME distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.4-bin.zip distributionUrl=https\://services.gradle.org/distributions/gradle-8.12-bin.zip
networkTimeout=10000 networkTimeout=10000
validateDistributionUrl=true validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME zipStoreBase=GRADLE_USER_HOME

6
gradlew vendored
View File

@@ -15,6 +15,8 @@
# See the License for the specific language governing permissions and # See the License for the specific language governing permissions and
# limitations under the License. # limitations under the License.
# #
# SPDX-License-Identifier: Apache-2.0
#
############################################################################## ##############################################################################
# #
@@ -55,7 +57,7 @@
# Darwin, MinGW, and NonStop. # Darwin, MinGW, and NonStop.
# #
# (3) This script is generated from the Groovy template # (3) This script is generated from the Groovy template
# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt # https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
# within the Gradle project. # within the Gradle project.
# #
# You can find Gradle at https://github.com/gradle/gradle/. # You can find Gradle at https://github.com/gradle/gradle/.
@@ -84,7 +86,7 @@ done
# shellcheck disable=SC2034 # shellcheck disable=SC2034
APP_BASE_NAME=${0##*/} APP_BASE_NAME=${0##*/}
# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) # Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit
# Use the maximum available, or set MAX_FD != -1 to use that value. # Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD=maximum MAX_FD=maximum

22
gradlew.bat vendored
View File

@@ -13,6 +13,8 @@
@rem See the License for the specific language governing permissions and @rem See the License for the specific language governing permissions and
@rem limitations under the License. @rem limitations under the License.
@rem @rem
@rem SPDX-License-Identifier: Apache-2.0
@rem
@if "%DEBUG%"=="" @echo off @if "%DEBUG%"=="" @echo off
@rem ########################################################################## @rem ##########################################################################
@@ -43,11 +45,11 @@ set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1 %JAVA_EXE% -version >NUL 2>&1
if %ERRORLEVEL% equ 0 goto execute if %ERRORLEVEL% equ 0 goto execute
echo. echo. 1>&2
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
echo. echo. 1>&2
echo Please set the JAVA_HOME variable in your environment to match the echo Please set the JAVA_HOME variable in your environment to match the 1>&2
echo location of your Java installation. echo location of your Java installation. 1>&2
goto fail goto fail
@@ -57,11 +59,11 @@ set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto execute if exist "%JAVA_EXE%" goto execute
echo. echo. 1>&2
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
echo. echo. 1>&2
echo Please set the JAVA_HOME variable in your environment to match the echo Please set the JAVA_HOME variable in your environment to match the 1>&2
echo location of your Java installation. echo location of your Java installation. 1>&2
goto fail goto fail

View File

@@ -1 +1,8 @@
rootProject.name = "InspectionLens" rootProject.name = "InspectionLens"
pluginManagement {
plugins {
kotlin("jvm") version "1.9.21"
id("org.jetbrains.intellij.platform") version "2.2.1"
}
}

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

@@ -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

@@ -43,7 +43,7 @@ internal value class EditorLensLineBackground(private val highlighter: RangeHigh
return if (editor.foldingModel.let { it.isOffsetCollapsed(startOffset) || it.isOffsetCollapsed(endOffset) }) return if (editor.foldingModel.let { it.isOffsetCollapsed(startOffset) || it.isOffsetCollapsed(endOffset) })
null null
else else
severity.getLineAttributes(ColorMode.getFromEditor(editor)) severity.lineAttributes
} }
} }
} }

View File

@@ -2,8 +2,13 @@ package com.chylex.intellij.inspectionlens.editor.lens
import com.intellij.codeInsight.daemon.impl.IntentionsUI import com.intellij.codeInsight.daemon.impl.IntentionsUI
import com.intellij.codeInsight.hint.HintManager import com.intellij.codeInsight.hint.HintManager
import com.intellij.codeInsight.intention.actions.ShowIntentionActionsAction
import com.intellij.codeInsight.intention.impl.ShowIntentionActionsHandler import com.intellij.codeInsight.intention.impl.ShowIntentionActionsHandler
import com.intellij.lang.LangBundle import com.intellij.lang.LangBundle
import com.intellij.openapi.actionSystem.ActionManager
import com.intellij.openapi.actionSystem.ActionPlaces
import com.intellij.openapi.actionSystem.IdeActions
import com.intellij.openapi.actionSystem.ex.ActionUtil
import com.intellij.openapi.editor.Editor import com.intellij.openapi.editor.Editor
import com.intellij.openapi.project.Project import com.intellij.openapi.project.Project
import com.intellij.psi.PsiDocumentManager import com.intellij.psi.PsiDocumentManager
@@ -18,6 +23,19 @@ internal object IntentionsPopup {
} }
private fun tryShow(editor: Editor): Boolean { private fun tryShow(editor: Editor): Boolean {
// If the IDE uses the default Show Intentions action and handler,
// use the handler directly to bypass additional logic from the action.
val action = ActionManager.getInstance().getAction(IdeActions.ACTION_SHOW_INTENTION_ACTIONS)
if (action.javaClass === ShowIntentionActionsAction::class.java) {
return tryShowWithDefaultHandler(editor)
}
else {
ActionUtil.invokeAction(action, editor.component, ActionPlaces.EDITOR_INLAY, null, null)
return true
}
}
private fun tryShowWithDefaultHandler(editor: Editor): Boolean {
val project = editor.project ?: return false val project = editor.project ?: return false
val file = PsiUtilBase.getPsiFileInEditor(editor, project) ?: return false val file = PsiUtilBase.getPsiFileInEditor(editor, project) ?: return false

View File

@@ -1,5 +1,6 @@
package com.chylex.intellij.inspectionlens.editor.lens package com.chylex.intellij.inspectionlens.editor.lens
import com.chylex.intellij.inspectionlens.settings.LensHoverMode
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
@@ -16,6 +17,7 @@ import com.intellij.ui.paint.EffectPainter
import java.awt.Cursor import java.awt.Cursor
import java.awt.Graphics import java.awt.Graphics
import java.awt.Graphics2D import java.awt.Graphics2D
import java.awt.MouseInfo
import java.awt.Point import java.awt.Point
import java.awt.Rectangle import java.awt.Rectangle
import java.awt.event.MouseEvent import java.awt.event.MouseEvent
@@ -25,10 +27,9 @@ import javax.swing.SwingUtilities
/** /**
* Renders the text of an inspection lens. * Renders the text of an inspection lens.
*/ */
class LensRenderer(private var info: HighlightInfo, settings: LensSettingsState) : HintRenderer(null), InputHandler { class LensRenderer(private var info: HighlightInfo, private val settings: LensSettingsState) : HintRenderer(null), InputHandler {
private val useEditorFont = settings.useEditorFont
private lateinit var inlay: Inlay<*> private lateinit var inlay: Inlay<*>
private lateinit var severity: LensSeverity private lateinit var attributes: LensSeverityTextAttributes
private var extraRightPadding = 0 private var extraRightPadding = 0
private var hovered = false private var hovered = false
@@ -46,7 +47,7 @@ class LensRenderer(private var info: HighlightInfo, settings: LensSettingsState)
val description = getValidDescriptionText(info.description) val description = getValidDescriptionText(info.description)
text = description text = description
severity = LensSeverity.from(info.severity) attributes = LensSeverity.from(info.severity).textAttributes
extraRightPadding = if (description.lastOrNull() == '.') 2 else 0 extraRightPadding = if (description.lastOrNull() == '.') 2 else 0
} }
@@ -54,7 +55,7 @@ class LensRenderer(private var info: HighlightInfo, settings: LensSettingsState)
fixBaselineForTextRendering(r) fixBaselineForTextRendering(r)
super.paint(inlay, g, r, textAttributes) super.paint(inlay, g, r, textAttributes)
if (hovered) { if (hovered && isHoveringText()) {
paintHoverEffect(inlay, g, r) paintHoverEffect(inlay, g, r)
} }
} }
@@ -67,20 +68,20 @@ class LensRenderer(private var info: HighlightInfo, settings: LensSettingsState)
val font = editor.colorsScheme.getFont(EditorFontType.PLAIN) val font = editor.colorsScheme.getFont(EditorFontType.PLAIN)
val x = r.x + TEXT_HORIZONTAL_PADDING val x = r.x + TEXT_HORIZONTAL_PADDING
val y = r.y + editor.ascent + 1 val y = r.y + editor.ascent
val w = inlay.widthInPixels - UNDERLINE_WIDTH_REDUCTION - extraRightPadding val w = inlay.widthInPixels - UNDERLINE_WIDTH_REDUCTION - extraRightPadding
val h = editor.descent val h = editor.descent
g.color = getTextAttributes(editor).foregroundColor g.color = attributes.foregroundColor
EffectPainter.LINE_UNDERSCORE.paint(g as Graphics2D, x, y, w, h, font) 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.getTextAttributes(ColorMode.getFromEditor(editor)) return attributes
} }
override fun useEditorFont(): Boolean { override fun useEditorFont(): Boolean {
return useEditorFont return settings.useEditorFont
} }
override fun mouseMoved(event: MouseEvent, translated: Point) { override fun mouseMoved(event: MouseEvent, translated: Point) {
@@ -92,6 +93,10 @@ class LensRenderer(private var info: HighlightInfo, settings: LensSettingsState)
} }
private fun setHovered(hovered: Boolean) { private fun setHovered(hovered: Boolean) {
if (hovered && settings.lensHoverMode == LensHoverMode.DISABLED) {
return
}
if (this.hovered == hovered) { if (this.hovered == hovered) {
return return
} }
@@ -108,22 +113,33 @@ class LensRenderer(private var info: HighlightInfo, settings: LensSettingsState)
} }
override fun mousePressed(event: MouseEvent, translated: Point) { override fun mousePressed(event: MouseEvent, translated: Point) {
if (!isHoveringText(translated)) { val hoverMode = settings.lensHoverMode
if (hoverMode == LensHoverMode.DISABLED || !isHoveringText(translated)) {
return return
} }
if (SwingUtilities.isLeftMouseButton(event) || SwingUtilities.isMiddleMouseButton(event)) { if (event.button.let { it == MouseEvent.BUTTON1 || it == MouseEvent.BUTTON2 }) {
event.consume() event.consume()
val editor = inlay.editor val editor = inlay.editor
moveToOffset(editor, info.actualStartOffset) moveToOffset(editor, info.actualStartOffset)
if (SwingUtilities.isLeftMouseButton(event)) { if ((event.button == MouseEvent.BUTTON1) xor (hoverMode != LensHoverMode.DEFAULT)) {
IntentionsPopup.show(editor) IntentionsPopup.show(editor)
} }
} }
} }
private fun isHoveringText(): Boolean {
val bounds = inlay.bounds ?: return false
val translatedPoint = MouseInfo.getPointerInfo().location.apply {
SwingUtilities.convertPointFromScreen(this, inlay.editor.contentComponent)
translate(-bounds.x, -bounds.y)
}
return isHoveringText(translatedPoint)
}
private fun isHoveringText(point: Point): Boolean { private fun isHoveringText(point: Point): Boolean {
return point.x >= HOVER_HORIZONTAL_PADDING return point.x >= HOVER_HORIZONTAL_PADDING
&& point.y >= 4 && point.y >= 4

View File

@@ -2,20 +2,19 @@ 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(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), 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),
@@ -24,37 +23,18 @@ enum class LensSeverity(private val baseColor: Color, private val lightThemeDark
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);
private lateinit var textAttributes: EnumMap<ColorMode, LensSeverityTextAttributes> val textAttributes: LensSeverityTextAttributes
private lateinit var lineAttributes: EnumMap<ColorMode, LensSeverityTextAttributes> val lineAttributes: LensSeverityTextAttributes
init { init {
refreshColors() val lightThemeColor = ColorUtil.saturate(ColorUtil.darker(baseColor, lightThemeDarkening), 1)
} val darkThemeColor = ColorUtil.desaturate(ColorUtil.brighter(baseColor, darkThemeBrightening), 2)
internal fun refreshColors() { val textColor = JBColor(lightThemeColor, darkThemeColor)
val theme = if (ColorGenerator.useGenerator) { val lineColor = JBColor(toAlpha(lightThemeColor, 10), toAlpha(darkThemeColor, 13))
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) textAttributes = LensSeverityTextAttributes(foregroundColor = textColor, fontStyle = Font.ITALIC)
val darkThemeLineColor = toAlpha(darkThemeTextColor, 13) lineAttributes = LensSeverityTextAttributes(backgroundColor = lineColor)
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)
} }
companion object { companion object {

View File

@@ -13,11 +13,13 @@ import com.intellij.openapi.ui.DialogPanel
import com.intellij.openapi.util.Disposer import com.intellij.openapi.util.Disposer
import com.intellij.ui.DisabledTraversalPolicy import com.intellij.ui.DisabledTraversalPolicy
import com.intellij.ui.EditorTextFieldCellRenderer.SimpleRendererComponent import com.intellij.ui.EditorTextFieldCellRenderer.SimpleRendererComponent
import com.intellij.ui.SimpleListCellRenderer
import com.intellij.ui.components.JBCheckBox import com.intellij.ui.components.JBCheckBox
import com.intellij.ui.dsl.builder.Cell import com.intellij.ui.dsl.builder.Cell
import com.intellij.ui.dsl.builder.RightGap import com.intellij.ui.dsl.builder.RightGap
import com.intellij.ui.dsl.builder.Row import com.intellij.ui.dsl.builder.Row
import com.intellij.ui.dsl.builder.RowLayout import com.intellij.ui.dsl.builder.RowLayout
import com.intellij.ui.dsl.builder.bindItem
import com.intellij.ui.dsl.builder.bindSelected import com.intellij.ui.dsl.builder.bindSelected
import com.intellij.ui.dsl.builder.panel import com.intellij.ui.dsl.builder.panel
import java.awt.Cursor import java.awt.Cursor
@@ -74,6 +76,20 @@ class LensApplicationConfigurable : BoundConfigurable("Inspection Lens"), Config
val settings = settingsService.state val settings = settingsService.state
return panel { return panel {
group("Appearance") {
row {
checkBox("Use editor font").bindSelected(settings::useEditorFont)
}
}
group("Behavior") {
row("Hover mode:") {
val items = LensHoverMode.values().toList()
val renderer = SimpleListCellRenderer.create("", LensHoverMode::description)
comboBox(items, renderer).bindItem(settings::lensHoverMode) { settings.lensHoverMode = it ?: LensHoverMode.DEFAULT }
}
}
group("Shown Severities") { group("Shown Severities") {
for ((id, severity, textAttributes) in allSeverities) { for ((id, severity, textAttributes) in allSeverities) {
row { row {
@@ -89,12 +105,6 @@ class LensApplicationConfigurable : BoundConfigurable("Inspection Lens"), Config
checkBox("Other").bindSelected(settings::showUnknownSeverities) checkBox("Other").bindSelected(settings::showUnknownSeverities)
} }
} }
group("Appearance") {
row {
checkBox("Use editor font").bindSelected(settings::useEditorFont)
}
}
} }
} }

View File

@@ -0,0 +1,7 @@
package com.chylex.intellij.inspectionlens.settings
enum class LensHoverMode(val description: String) {
DISABLED("Disabled"),
DEFAULT("Left click shows intentions, middle click jumps to highlight"),
SWAPPED("Left click jumps to highlight, middle click shows intentions")
}

View File

@@ -21,6 +21,7 @@ class LensSettingsState : SimplePersistentStateComponent<LensSettingsState.State
var showUnknownSeverities by property(true) var showUnknownSeverities by property(true)
var useEditorFont by property(true) var useEditorFont by property(true)
var lensHoverMode by enum(LensHoverMode.DEFAULT)
} }
@get:Synchronized @get:Synchronized
@@ -31,6 +32,9 @@ class LensSettingsState : SimplePersistentStateComponent<LensSettingsState.State
val useEditorFont val useEditorFont
get() = state.useEditorFont get() = state.useEditorFont
val lensHoverMode
get() = state.lensHoverMode
override fun loadState(state: State) { override fun loadState(state: State) {
super.loadState(state) super.loadState(state)
update() update()

View File

@@ -6,14 +6,20 @@
<description><![CDATA[ <description><![CDATA[
Displays errors, warnings, and other inspections inline. Highlights the background of lines with inspections. Supports light and dark themes out of the box. Displays errors, warnings, and other inspections inline. Highlights the background of lines with inspections. Supports light and dark themes out of the box.
<br><br> <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. Left-click an inspection to show quick fixes. Middle-click an inspection to navigate to the relevant code in the editor.
<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. Configure appearance, behavior of clicking on inspections, and visible severities in <b>Settings | Tools | Inspection Lens</b>.
<br><br> <br><br>
Inspired by <a href="https://marketplace.visualstudio.com/items?itemName=usernamehw.errorlens">Error Lens</a> for VS Code, and <a href="https://plugins.jetbrains.com/plugin/17302-inlineerror">Inline Error</a> for IntelliJ Platform. Inspired by <a href="https://marketplace.visualstudio.com/items?itemName=usernamehw.errorlens">Error Lens</a> for VS Code, and <a href="https://plugins.jetbrains.com/plugin/17302-inlineerror">Inline Error</a> for IntelliJ Platform.
]]></description> ]]></description>
<change-notes><![CDATA[ <change-notes><![CDATA[
<b>Version 1.5.1</b>
<ul>
<li>Added option to change the behavior of clicking on inspections.</li>
<li>Fixed broken quick fixes in Rider and CLion Nova.</li>
<li>Fixed hover underline not rendering correctly with some combinations of high DPI and line height settings.</li>
</ul>
<b>Version 1.5</b> <b>Version 1.5</b>
<ul> <ul>
<li>Added possibility to left-click an inspection to show quick fixes.</li> <li>Added possibility to left-click an inspection to show quick fixes.</li>
@@ -92,10 +98,4 @@
<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>