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

18 Commits
debug ... main

Author SHA1 Message Date
08d0f5fe34 Release 1.6.0 2025-06-26 14:35:19 +02:00
4cd76a8a8c Try replacing FileOpenedSyncListener because it randomly fails to register during IDE startup 2025-06-25 21:47:52 +02:00
05ced823a6 Add action to toggle visibility of all lenses
Closes #12
2025-06-25 15:29:42 +02:00
766ba4c74c Do not show lenses for file-wide and gutter inspections 2025-06-25 03:29:05 +02:00
4e9fdf8fa3 Rewrite intentions popup to only show relevant quick fixes and bypass the floating toolbar
Closes #39
2025-06-25 02:31:12 +02:00
a754276a6c Add plugin verifier configuration 2025-06-25 02:23:33 +02:00
fbe67d6068 Release 1.5.2 2025-01-15 02:05:14 +01:00
98d3e5db01 Add option to change maximum inspection description length
Closes #34
2025-01-15 01:56:02 +01:00
102352a2eb Add button to reset settings to default 2025-01-15 01:37:15 +01:00
3aeeb32bef Configure Kotlin compiler to use invokedynamic for lambdas 2025-01-15 01:36:32 +01:00
0bc85fd69b Work around referencing an internal IntelliJ API class 2025-01-14 23:14:28 +01:00
cde4d81afe Update Gradle to 8.12 and IntelliJ Platform plugin to 2.2.1 2025-01-04 15:34:19 +01:00
b1d6ed4d30 Remove GitHub Sponsors and Patreon from FUNDING.yml 2025-01-04 15:34:19 +01:00
2a4764fa15 Release 1.5.1 2025-01-04 10:28:58 +01:00
4bd0931d71 Add option to change or disable lens hover behavior
Closes #31
2025-01-04 08:23:53 +01:00
0f41b22872 Fix hover underline not disappearing when scrolling without moving the mouse 2025-01-03 22:48:13 +01:00
603b35abdb Fix intentions popup not working in Rider and CLion Nova 2025-01-03 17:42:38 +01:00
08ed1aadea Fix broken inlay underline rendering with some combinations of DPI and line height settings 2025-01-03 16:28:08 +01:00
26 changed files with 428 additions and 579 deletions

2
.github/FUNDING.yml vendored
View File

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

1
.gitignore vendored
View File

@@ -3,4 +3,5 @@
!/.idea/runConfigurations
/.gradle/
/.intellijPlatform/
/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.
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.

View File

@@ -1,48 +1,59 @@
@file:Suppress("ConvertLambdaToReference")
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
plugins {
kotlin("jvm") version "1.8.0"
id("org.jetbrains.intellij") version "1.17.0"
kotlin("jvm")
id("org.jetbrains.intellij.platform")
}
group = "com.chylex.intellij.inspectionlens"
version = "1.5"
version = "1.6.0"
repositories {
mavenCentral()
intellijPlatform {
defaultRepositories()
}
}
intellij {
version.set("2023.3.3")
updateSinceUntilBuild.set(false)
dependencies {
intellijPlatform {
intellijIdeaUltimate("2024.2")
bundledPlugin("tanvd.grazi")
}
plugins.add("tanvd.grazi")
testImplementation("org.junit.jupiter:junit-jupiter:5.9.2")
}
intellijPlatform {
pluginConfiguration {
ideaVersion {
sinceBuild.set("242")
untilBuild.set(provider { null })
}
}
pluginVerification {
freeArgs.add("-mute")
freeArgs.add("TemplateWordInPluginId")
ides {
recommended()
}
}
}
kotlin {
jvmToolchain(17)
}
dependencies {
testImplementation("org.junit.jupiter:junit-jupiter:5.9.2")
}
tasks.patchPluginXml {
sinceBuild.set("233.11361.10")
}
tasks.buildSearchableOptions {
enabled = false
compilerOptions {
freeCompilerArgs = listOf(
"-X" + "jvm-default=all",
"-X" + "lambdas=indy"
)
}
}
tasks.test {
useJUnitPlatform()
}
tasks.withType<KotlinCompile> {
kotlinOptions.freeCompilerArgs = listOf(
"-Xjvm-default=all"
)
}

Binary file not shown.

View File

@@ -1,6 +1,6 @@
distributionBase=GRADLE_USER_HOME
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
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME

6
gradlew vendored
View File

@@ -15,6 +15,8 @@
# See the License for the specific language governing permissions and
# limitations under the License.
#
# SPDX-License-Identifier: Apache-2.0
#
##############################################################################
#
@@ -55,7 +57,7 @@
# Darwin, MinGW, and NonStop.
#
# (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.
#
# You can find Gradle at https://github.com/gradle/gradle/.
@@ -84,7 +86,7 @@ done
# shellcheck disable=SC2034
APP_BASE_NAME=${0##*/}
# 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.
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 limitations under the License.
@rem
@rem SPDX-License-Identifier: Apache-2.0
@rem
@if "%DEBUG%"=="" @echo off
@rem ##########################################################################
@@ -43,11 +45,11 @@ set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if %ERRORLEVEL% equ 0 goto execute
echo.
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
echo. 1>&2
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
echo. 1>&2
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
echo location of your Java installation. 1>&2
goto fail
@@ -57,11 +59,11 @@ set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto execute
echo.
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
echo. 1>&2
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
echo. 1>&2
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
echo location of your Java installation. 1>&2
goto fail

View File

@@ -1 +1,8 @@
rootProject.name = "InspectionLens"
pluginManagement {
plugins {
kotlin("jvm") version "1.9.24" // https://plugins.jetbrains.com/docs/intellij/using-kotlin.html#bundled-stdlib-versions
id("org.jetbrains.intellij.platform") version "2.6.0" // https://github.com/JetBrains/intellij-platform-gradle-plugin/releases
}
}

View File

@@ -2,10 +2,11 @@ package com.chylex.intellij.inspectionlens
import com.chylex.intellij.inspectionlens.editor.EditorLensFeatures
import com.intellij.openapi.application.ApplicationManager
import com.intellij.openapi.components.service
import com.intellij.openapi.diagnostic.logger
import com.intellij.openapi.fileEditor.FileEditorManager
import com.intellij.openapi.fileEditor.TextEditor
import com.intellij.openapi.project.ProjectManager
import java.util.concurrent.atomic.AtomicBoolean
/**
* Handles installation and uninstallation of plugin features in editors.
@@ -13,27 +14,33 @@ import com.intellij.openapi.project.ProjectManager
internal object InspectionLens {
const val PLUGIN_ID = "com.chylex.intellij.inspectionlens"
val LOG = logger<InspectionLens>()
var SHOW_LENSES = true
set(value) {
field = value
scheduleRefresh()
}
/**
* Installs lenses into [editor].
*/
fun install(editor: TextEditor) {
EditorLensFeatures.install(editor.editor, service<InspectionLensPluginDisposableService>().intersect(editor))
EditorLensFeatures.install(editor)
}
/**
* Installs lenses into all open editors.
*/
fun install() {
forEachOpenEditor(::install)
forEachOpenEditor(EditorLensFeatures::install)
}
/**
* Refreshes lenses in all open editors.
*/
private fun refresh() {
forEachOpenEditor {
EditorLensFeatures.refresh(it.editor)
}
forEachOpenEditor(EditorLensFeatures::refresh)
}
/**
@@ -49,31 +56,31 @@ internal object InspectionLens {
private inline fun forEachOpenEditor(action: (TextEditor) -> Unit) {
val projectManager = ProjectManager.getInstanceIfCreated() ?: return
for (project in projectManager.openProjects.filterNot { it.isDisposed }) {
for (editor in FileEditorManager.getInstance(project).allEditors.filterIsInstance<TextEditor>()) {
action(editor)
for (project in projectManager.openProjects) {
if (project.isDisposed) {
continue
}
for (editor in FileEditorManager.getInstance(project).allEditors) {
if (editor is TextEditor) {
action(editor)
}
}
}
}
private object Refresh {
private var needsRefresh = false
private object Refresh : Runnable {
private val needsRefresh = AtomicBoolean(false)
fun schedule() {
synchronized(this) {
if (!needsRefresh) {
needsRefresh = true
ApplicationManager.getApplication().invokeLater(this::run)
}
if (needsRefresh.compareAndSet(false, true)) {
ApplicationManager.getApplication().invokeLater(this)
}
}
private fun run() {
synchronized(this) {
if (needsRefresh) {
needsRefresh = false
refresh()
}
override fun run() {
if (needsRefresh.compareAndSet(true, false)) {
refresh()
}
}
}

View File

@@ -0,0 +1,22 @@
package com.chylex.intellij.inspectionlens
import com.intellij.openapi.fileEditor.FileEditorManager
import com.intellij.openapi.fileEditor.FileEditorManagerListener
import com.intellij.openapi.fileEditor.FileOpenedSyncListener
import com.intellij.openapi.fileEditor.TextEditor
import com.intellij.openapi.vfs.VirtualFile
/**
* Installs [InspectionLens] in opened editors.
*
* Used instead of [FileOpenedSyncListener], because [FileOpenedSyncListener] randomly fails to register during IDE startup.
*/
class InspectionLensFileEditorManagerListener : FileEditorManagerListener {
override fun fileOpened(source: FileEditorManager, file: VirtualFile) {
for (editor in source.getEditors(file)) {
if (editor is TextEditor) {
InspectionLens.install(editor)
}
}
}
}

View File

@@ -1,21 +0,0 @@
package com.chylex.intellij.inspectionlens
import com.intellij.openapi.fileEditor.FileEditorManager
import com.intellij.openapi.fileEditor.FileOpenedSyncListener
import com.intellij.openapi.fileEditor.TextEditor
import com.intellij.openapi.fileEditor.ex.FileEditorWithProvider
import com.intellij.openapi.vfs.VirtualFile
/**
* Installs [InspectionLens] in newly opened editors.
*/
class InspectionLensFileOpenedListener : FileOpenedSyncListener {
override fun fileOpenedSync(source: FileEditorManager, file: VirtualFile, editorsWithProviders: List<FileEditorWithProvider>) {
for (editorWrapper in editorsWithProviders) {
val fileEditor = editorWrapper.fileEditor
if (fileEditor is TextEditor) {
InspectionLens.install(fileEditor)
}
}
}
}

View File

@@ -0,0 +1,25 @@
package com.chylex.intellij.inspectionlens.actions
import com.chylex.intellij.inspectionlens.InspectionLens
import com.intellij.openapi.actionSystem.ActionUpdateThread
import com.intellij.openapi.actionSystem.AnActionEvent
import com.intellij.openapi.actionSystem.KeepPopupOnPerform
import com.intellij.openapi.project.DumbAwareToggleAction
class ToggleLensVisibilityAction : DumbAwareToggleAction() {
init {
templatePresentation.keepPopupOnPerform = KeepPopupOnPerform.IfRequested
}
override fun getActionUpdateThread(): ActionUpdateThread {
return ActionUpdateThread.BGT
}
override fun isSelected(e: AnActionEvent): Boolean {
return InspectionLens.SHOW_LENSES
}
override fun setSelected(e: AnActionEvent, state: Boolean) {
InspectionLens.SHOW_LENSES = state
}
}

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,10 +1,13 @@
package com.chylex.intellij.inspectionlens.editor
import com.chylex.intellij.inspectionlens.InspectionLensPluginDisposableService
import com.intellij.openapi.Disposable
import com.intellij.openapi.components.service
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.fileEditor.TextEditor
import com.intellij.openapi.util.Disposer
import com.intellij.openapi.util.Key
@@ -36,21 +39,24 @@ internal class EditorLensFeatures private constructor(
companion object {
private val EDITOR_KEY = Key<EditorLensFeatures>(EditorLensFeatures::class.java.name)
fun install(editor: Editor, disposable: Disposable) {
fun install(owner: TextEditor) {
val editor = owner.editor
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 disposable = service<InspectionLensPluginDisposableService>().intersect(owner)
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()
fun refresh(owner: TextEditor) {
owner.editor.getUserData(EDITOR_KEY)?.refresh()
}
}
}

View File

@@ -1,7 +1,9 @@
package com.chylex.intellij.inspectionlens.editor
import com.chylex.intellij.inspectionlens.InspectionLens
import com.chylex.intellij.inspectionlens.settings.LensSettingsState
import com.intellij.codeInsight.daemon.impl.HighlightInfo
import com.intellij.codeInsight.daemon.impl.UpdateHighlightersUtil.isFileLevelOrGutterAnnotation
import com.intellij.openapi.components.service
import com.intellij.openapi.editor.ex.RangeHighlighterEx
import com.intellij.openapi.editor.impl.event.MarkupModelListener
@@ -21,43 +23,30 @@ internal class LensMarkupModelListener(private val lensManagerDispatcher: Editor
showIfValid(highlighter)
}
override fun beforeRemoved(highlighter: RangeHighlighterEx) {
if (getFilteredHighlightInfo(highlighter) != null) {
lensManagerDispatcher.hide(highlighter)
fun showAllValid(highlighters: Array<RangeHighlighter>) {
if (InspectionLens.SHOW_LENSES) {
highlighters.forEach(::showIfValid)
}
}
private fun showIfValid(highlighter: RangeHighlighter) {
runWithHighlighterIfValid(highlighter, lensManagerDispatcher::show, ::showAsynchronously)
}
private fun showAsynchronously(highlighterWithInfo: HighlighterWithInfo.Async) {
highlighterWithInfo.requestDescription {
if (highlighterWithInfo.highlighter.isValid && highlighterWithInfo.hasDescription) {
lensManagerDispatcher.show(highlighterWithInfo)
}
if (!InspectionLens.SHOW_LENSES) {
return
}
}
fun showAllValid(highlighters: Array<RangeHighlighter>) {
highlighters.forEach(::showIfValid)
}
fun hideAll() {
lensManagerDispatcher.hideAll()
val info = highlighter.takeIf { it.isValid }?.let(::getFilteredHighlightInfo)
if (info == null || isFileLevelOrGutterAnnotation(info)) {
return
}
val highlighterWithInfo = HighlighterWithInfo.from(highlighter, info)
processHighlighterWithInfo(highlighterWithInfo, lensManagerDispatcher::show, ::showAsynchronously)
}
private fun getFilteredHighlightInfo(highlighter: RangeHighlighter): HighlightInfo? {
return HighlightInfo.fromRangeHighlighter(highlighter)?.takeIf { settings.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)
if (info != null) {
processHighlighterWithInfo(HighlighterWithInfo.from(highlighter, info), actionForImmediate, actionForAsync)
}
}
private inline fun processHighlighterWithInfo(highlighterWithInfo: HighlighterWithInfo, actionForImmediate: (HighlighterWithInfo) -> Unit, actionForAsync: (HighlighterWithInfo.Async) -> Unit) {
if (highlighterWithInfo is HighlighterWithInfo.Async) {
actionForAsync(highlighterWithInfo)
@@ -66,4 +55,22 @@ internal class LensMarkupModelListener(private val lensManagerDispatcher: Editor
actionForImmediate(highlighterWithInfo)
}
}
private fun showAsynchronously(highlighterWithInfo: HighlighterWithInfo.Async) {
highlighterWithInfo.requestDescription {
if (highlighterWithInfo.highlighter.isValid && highlighterWithInfo.hasDescription) {
lensManagerDispatcher.show(highlighterWithInfo)
}
}
}
override fun beforeRemoved(highlighter: RangeHighlighterEx) {
if (getFilteredHighlightInfo(highlighter) != null) {
lensManagerDispatcher.hide(highlighter)
}
}
fun hideAll() {
lensManagerDispatcher.hideAll()
}
}

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) })
null
else
severity.getLineAttributes(ColorMode.getFromEditor(editor))
severity.lineAttributes
}
}
}

View File

@@ -1,36 +1,132 @@
package com.chylex.intellij.inspectionlens.editor.lens
import com.chylex.intellij.inspectionlens.InspectionLens
import com.intellij.codeInsight.daemon.impl.HighlightInfo
import com.intellij.codeInsight.daemon.impl.HighlightInfo.IntentionActionDescriptor
import com.intellij.codeInsight.daemon.impl.IntentionsUI
import com.intellij.codeInsight.daemon.impl.ShowIntentionsPass.IntentionsInfo
import com.intellij.codeInsight.hint.HintManager
import com.intellij.codeInsight.intention.impl.CachedIntentions
import com.intellij.codeInsight.intention.impl.IntentionHintComponent
import com.intellij.codeInsight.intention.impl.ShowIntentionActionsHandler
import com.intellij.lang.LangBundle
import com.intellij.lang.annotation.HighlightSeverity
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.application.ModalityState
import com.intellij.openapi.application.ReadAction
import com.intellij.openapi.editor.Editor
import com.intellij.openapi.editor.Inlay
import com.intellij.openapi.project.DumbService
import com.intellij.openapi.project.Project
import com.intellij.psi.PsiDocumentManager
import com.intellij.psi.PsiFile
import com.intellij.psi.util.PsiUtilBase
import com.intellij.ui.awt.RelativePoint
import com.intellij.util.concurrency.AppExecutorUtil
import java.lang.reflect.Method
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 const val INTENTION_SOURCE_CLASS_NAME = "com.intellij.codeInsight.intention.IntentionSource"
private val showPopupMethod: ShowPopupMethod? = try {
val method = IntentionHintComponent::class.java.declaredMethods.first { method ->
val parameterTypes = method.parameterTypes
method.name == "showPopup" &&
parameterTypes.size in 1..2 &&
parameterTypes[0] === RelativePoint::class.java &&
parameterTypes.getOrNull(1).let { p -> p == null || p.name == INTENTION_SOURCE_CLASS_NAME }
}
method.isAccessible = true
@Suppress("UNCHECKED_CAST")
val args: Array<Any?> = if (method.parameterCount == 1)
arrayOf(null)
else
arrayOf(null, (Class.forName(INTENTION_SOURCE_CLASS_NAME) as Class<Enum<*>>).enumConstants.first { it.name == "OTHER" })
ShowPopupMethod(method, args)
} catch (t: Throwable) {
InspectionLens.LOG.warn("Could not initialize intention popup", t)
null
}
private class ShowPopupMethod(private val method: Method, private val args: Array<Any?>) {
operator fun invoke(component: IntentionHintComponent) {
method.invoke(component, *args)
}
}
private fun tryShow(editor: Editor): Boolean {
fun show(highlightInfo: HighlightInfo, inlay: Inlay<*>) {
if (!tryShow(highlightInfo, inlay)) {
showNoActionsAvailable(inlay.editor)
}
}
private fun tryShow(highlightInfo: HighlightInfo, inlay: Inlay<*>): Boolean {
val editor = inlay.editor
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)
ReadAction
.nonBlocking<IntentionsInfo> { collectIntentions(editor, project, file, highlightInfo, inlay.offset) }
.finishOnUiThread(ModalityState.current()) { tryShowPopup(project, file, editor, it) }
.submit(AppExecutorUtil.getAppExecutorService())
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)
private fun collectIntentions(editor: Editor, project: Project, file: PsiFile, info: HighlightInfo, offset: Int): IntentionsInfo {
val intentions = mutableListOf<IntentionActionDescriptor>()
info.findRegisteredQuickFix { descriptor, _ ->
if (DumbService.getInstance(project).isUsableInCurrentContext(descriptor.action) && ShowIntentionActionsHandler.availableFor(file, editor, offset, descriptor.action)) {
intentions.add(descriptor)
}
null
}
return IntentionsInfo().also {
it.offset = offset
if (info.severity === HighlightSeverity.ERROR) {
it.errorFixesToShow.addAll(intentions)
}
else {
it.inspectionFixesToShow.addAll(intentions)
}
}
}
private fun tryShowPopup(project: Project, file: PsiFile, editor: Editor, intentions: IntentionsInfo) {
try {
showPopup(project, file, editor, intentions)
} catch (t: Throwable) {
InspectionLens.LOG.error("Could not show intention popup", t)
showNoActionsAvailable(editor)
}
}
private fun showPopup(project: Project, file: PsiFile, editor: Editor, intentions: IntentionsInfo) {
if (intentions.isEmpty || showPopupMethod == null) {
val showIntentionsAction = ActionManager.getInstance().getAction(IdeActions.ACTION_SHOW_INTENTION_ACTIONS)
ActionUtil.invokeAction(showIntentionsAction, editor.component, ActionPlaces.EDITOR_INLAY, null, null)
}
else {
val cachedIntentions = CachedIntentions.create(project, file, editor, intentions)
val hintComponent = IntentionHintComponent.showIntentionHint(project, file, editor, false, cachedIntentions)
showPopupMethod.invoke(hintComponent)
}
}
private fun showNoActionsAvailable(editor: Editor) {
HintManager.getInstance().showInformationHint(editor, LangBundle.message("hint.text.no.context.actions.available.at.this.location"))
}
}

View File

@@ -1,5 +1,6 @@
package com.chylex.intellij.inspectionlens.editor.lens
import com.chylex.intellij.inspectionlens.settings.LensHoverMode
import com.chylex.intellij.inspectionlens.settings.LensSettingsState
import com.intellij.codeInsight.daemon.impl.HighlightInfo
import com.intellij.codeInsight.daemon.impl.HintRenderer
@@ -16,6 +17,7 @@ import com.intellij.ui.paint.EffectPainter
import java.awt.Cursor
import java.awt.Graphics
import java.awt.Graphics2D
import java.awt.MouseInfo
import java.awt.Point
import java.awt.Rectangle
import java.awt.event.MouseEvent
@@ -25,10 +27,9 @@ 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
class LensRenderer(private var info: HighlightInfo, private val settings: LensSettingsState) : HintRenderer(null), InputHandler {
private lateinit var inlay: Inlay<*>
private lateinit var severity: LensSeverity
private lateinit var attributes: LensSeverityTextAttributes
private var extraRightPadding = 0
private var hovered = false
@@ -43,10 +44,10 @@ class LensRenderer(private var info: HighlightInfo, settings: LensSettingsState)
fun setPropertiesFrom(info: HighlightInfo) {
this.info = info
val description = getValidDescriptionText(info.description)
val description = getValidDescriptionText(info.description, settings.maxDescriptionLength)
text = description
severity = LensSeverity.from(info.severity)
attributes = LensSeverity.from(info.severity).textAttributes
extraRightPadding = if (description.lastOrNull() == '.') 2 else 0
}
@@ -54,7 +55,7 @@ class LensRenderer(private var info: HighlightInfo, settings: LensSettingsState)
fixBaselineForTextRendering(r)
super.paint(inlay, g, r, textAttributes)
if (hovered) {
if (hovered && isHoveringText()) {
paintHoverEffect(inlay, g, r)
}
}
@@ -67,20 +68,20 @@ class LensRenderer(private var info: HighlightInfo, settings: LensSettingsState)
val font = editor.colorsScheme.getFont(EditorFontType.PLAIN)
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 h = editor.descent
g.color = getTextAttributes(editor).foregroundColor
g.color = attributes.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))
return attributes
}
override fun useEditorFont(): Boolean {
return useEditorFont
return settings.useEditorFont
}
override fun mouseMoved(event: MouseEvent, translated: Point) {
@@ -92,6 +93,10 @@ class LensRenderer(private var info: HighlightInfo, settings: LensSettingsState)
}
private fun setHovered(hovered: Boolean) {
if (hovered && settings.lensHoverMode == LensHoverMode.DISABLED) {
return
}
if (this.hovered == hovered) {
return
}
@@ -108,22 +113,33 @@ class LensRenderer(private var info: HighlightInfo, settings: LensSettingsState)
}
override fun mousePressed(event: MouseEvent, translated: Point) {
if (!isHoveringText(translated)) {
val hoverMode = settings.lensHoverMode
if (hoverMode == LensHoverMode.DISABLED || !isHoveringText(translated)) {
return
}
if (SwingUtilities.isLeftMouseButton(event) || SwingUtilities.isMiddleMouseButton(event)) {
if (event.button.let { it == MouseEvent.BUTTON1 || it == MouseEvent.BUTTON2 }) {
event.consume()
val editor = inlay.editor
moveToOffset(editor, info.actualStartOffset)
if (SwingUtilities.isLeftMouseButton(event)) {
IntentionsPopup.show(editor)
if ((event.button == MouseEvent.BUTTON1) xor (hoverMode != LensHoverMode.DEFAULT)) {
IntentionsPopup.show(info, inlay)
}
}
}
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 {
return point.x >= HOVER_HORIZONTAL_PADDING
&& point.y >= 4
@@ -139,15 +155,13 @@ class LensRenderer(private var info: HighlightInfo, settings: LensSettingsState)
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 getValidDescriptionText(text: String?, maxLength: Int): String {
return if (text.isNullOrBlank()) " " else addEllipsisOrMissingPeriod(unescapeHtmlEntities(stripUppercaseTag(text)), maxLength)
}
private fun stripUppercaseTag(text: String): String {
@@ -165,11 +179,11 @@ class LensRenderer(private var info: HighlightInfo, settings: LensSettingsState)
return if (text.contains('&')) StringUtil.unescapeXmlEntities(text) else text
}
private fun addEllipsisOrMissingPeriod(text: String): String {
private fun addEllipsisOrMissingPeriod(text: String, maxLength: Int): String {
return when {
text.length > MAX_DESCRIPTION_LENGTH -> text.take(MAX_DESCRIPTION_LENGTH).trimEnd { it.isWhitespace() || it == '.' } + ""
!text.endsWith('.') -> "$text."
else -> text
text.length > maxLength -> text.take(maxLength).trimEnd { it.isWhitespace() || it == '.' } + ""
!text.endsWith('.') -> "$text."
else -> text
}
}

View File

@@ -2,20 +2,19 @@ package com.chylex.intellij.inspectionlens.editor.lens
import com.chylex.intellij.inspectionlens.InspectionLens
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()
}
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 lightThemeColor = ColorUtil.saturate(ColorUtil.darker(baseColor, lightThemeDarkening), 1)
val darkThemeColor = ColorUtil.desaturate(ColorUtil.brighter(baseColor, darkThemeBrightening), 2)
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)
val textColor = JBColor(lightThemeColor, darkThemeColor)
val lineColor = JBColor(toAlpha(lightThemeColor, 10), toAlpha(darkThemeColor, 13))
textAttributes = LensSeverityTextAttributes(foregroundColor = textColor, fontStyle = Font.ITALIC)
lineAttributes = LensSeverityTextAttributes(backgroundColor = lineColor)
}
companion object {

View File

@@ -10,14 +10,18 @@ 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.ui.MessageDialogBuilder
import com.intellij.openapi.util.Disposer
import com.intellij.ui.DisabledTraversalPolicy
import com.intellij.ui.EditorTextFieldCellRenderer.SimpleRendererComponent
import com.intellij.ui.SimpleListCellRenderer
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.bindIntText
import com.intellij.ui.dsl.builder.bindItem
import com.intellij.ui.dsl.builder.bindSelected
import com.intellij.ui.dsl.builder.panel
import java.awt.Cursor
@@ -73,7 +77,26 @@ class LensApplicationConfigurable : BoundConfigurable("Inspection Lens"), Config
override fun createPanel(): DialogPanel {
val settings = settingsService.state
return panel {
lateinit var panel: DialogPanel
panel = panel {
group("Appearance") {
row {
checkBox("Use editor font").bindSelected(settings::useEditorFont)
}
row("Max description length:") {
intTextField(LensSettingsState.MAX_DESCRIPTION_LENGTH_RANGE, keyboardStep = 10).bindIntText(settings::maxDescriptionLength)
}
}
group("Behavior") {
row("Hover mode:") {
val items = LensHoverMode.entries
val renderer = SimpleListCellRenderer.create("", LensHoverMode::description)
comboBox(items, renderer).bindItem(settings::lensHoverMode) { settings.lensHoverMode = it ?: LensHoverMode.DEFAULT }
}
}
group("Shown Severities") {
for ((id, severity, textAttributes) in allSeverities) {
row {
@@ -90,12 +113,19 @@ class LensApplicationConfigurable : BoundConfigurable("Inspection Lens"), Config
}
}
group("Appearance") {
group("Actions") {
row {
checkBox("Use editor font").bindSelected(settings::useEditorFont)
button("Reset to Default") {
if (MessageDialogBuilder.yesNo("Reset to Default", "Are you sure you want to reset settings to default?").ask(panel)) {
settingsService.resetState()
reset()
}
}
}
}
}
return panel
}
private fun <K, V> Cell<JBCheckBox>.bindSelectedToNotIn(collection: MutableMap<K, V>, key: K, value: V): Cell<JBCheckBox> {

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,12 @@ class LensSettingsState : SimplePersistentStateComponent<LensSettingsState.State
var showUnknownSeverities by property(true)
var useEditorFont by property(true)
var maxDescriptionLength by property(120)
var lensHoverMode by enum(LensHoverMode.DEFAULT)
}
companion object {
val MAX_DESCRIPTION_LENGTH_RANGE = 20..1000
}
@get:Synchronized
@@ -31,8 +37,27 @@ class LensSettingsState : SimplePersistentStateComponent<LensSettingsState.State
val useEditorFont
get() = state.useEditorFont
val maxDescriptionLength
get() = state.maxDescriptionLength
val lensHoverMode
get() = state.lensHoverMode
override fun loadState(state: State) {
super.loadState(state)
state.maxDescriptionLength = state.maxDescriptionLength.coerceIn(MAX_DESCRIPTION_LENGTH_RANGE)
update()
}
fun resetState() {
val default = State()
state.hiddenSeverities.apply { clear(); putAll(default.hiddenSeverities) }
state.showUnknownSeverities = default.showUnknownSeverities
state.useEditorFont = default.useEditorFont
state.maxDescriptionLength = default.maxDescriptionLength
state.lensHoverMode = default.lensHoverMode
update()
}

View File

@@ -6,14 +6,33 @@
<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.
<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>
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>
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.6.0</b>
<ul>
<li>Added action to <b>View | Show Inspection Lenses</b> that temporarily toggles visibility of all inspections.</li>
<li>File-wide inspections no longer appear.</li>
<li>Fixed quick fix popup disappearing when the floating toolbar is enabled.</li>
<li>Clicking an inspection now only shows relevant quick fixes (not supported for ReSharper-based languages, which use a non-standard popup).</li>
<li>Tried to work around an issue where the IDE randomly fails to load the plugin.</li>
</ul>
<b>Version 1.5.2</b>
<ul>
<li>Added option to change maximum description length.</li>
<li>Added button to <b>Settings | Tools | Inspection Lens</b> that resets all settings to default.</li>
</ul>
<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>
<ul>
<li>Added possibility to left-click an inspection to show quick fixes.</li>
@@ -85,17 +104,17 @@
parentId="tools" />
</extensions>
<actions>
<action id="chylex.InspectionLens.ToggleLensVisibility" class="com.chylex.intellij.inspectionlens.actions.ToggleLensVisibilityAction" text="Show Inspection Lenses">
<add-to-group group-id="ViewMenu" anchor="after" relative-to-action="EditorResetFontSizeGlobal" />
</action>
</actions>
<applicationListeners>
<listener class="com.chylex.intellij.inspectionlens.InspectionLensPluginListener" topic="com.intellij.ide.plugins.DynamicPluginListener" />
</applicationListeners>
<projectListeners>
<listener class="com.chylex.intellij.inspectionlens.InspectionLensFileOpenedListener" topic="com.intellij.openapi.fileEditor.FileOpenedSyncListener" />
<listener class="com.chylex.intellij.inspectionlens.InspectionLensFileEditorManagerListener" topic="com.intellij.openapi.fileEditor.FileEditorManagerListener" />
</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>