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

68 Commits

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
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
632a052ff9 Refactor code and organize packages 2024-12-24 06:39:45 +01:00
f2ec3c3d9b Add dictionary file for IntelliJ 2024-12-24 05:43:41 +01:00
624254fba3 Add option to use UI font for lenses
Closes #23
2024-12-23 23:43:01 +01:00
c1b52ec3a5 Release 1.4.1 2024-08-12 13:36:21 +02:00
a84bc72fd4 Address Plugin DevKit inspections
Closes #24
2024-08-11 22:10:45 +02:00
e28804ad5d Release 1.4 2024-06-15 01:55:33 +02:00
043c02e432 Add settings panel for configuring shown severities
Closes #2
2024-06-15 01:08:48 +02:00
223fceb6b9 Release 1.3.3 2024-04-22 01:24:45 +02:00
640d95cddc Work around missing class error when installed on Gateway Client
Closes #21
2024-04-22 00:06:16 +02:00
fcd4d6588d Revert "Strip HTML from inspection description"
This reverts commit e6be154f

Inspection description is not supposed to contain HTML, and IntelliJ itself does not render HTML in the Problems tool window. Issues with HTML in inspection descriptions should be reported to wherever those inspections are coming from.

Stripping HTML breaks any properly formatted inspections that include anything that looks like an HTML tag, so I'm reverting it.

I might remove unescaping HTML entities later, but I'm not currently aware of it breaking anything, so it can stay.
2024-03-17 02:08:21 +01:00
1c92cf000f Release 1.3.2 2024-02-07 20:51:32 +01:00
83c179574a Fix inspections randomly not disappearing 2024-02-07 20:33:31 +01:00
536650510d Update IDEA to 2023.3.3 2024-02-07 06:40:15 +01:00
9f395476df Release 1.3.1 2023-11-07 21:28:30 +01:00
182ad049d9 Convert a plugin.xml service to a light service 2023-11-07 21:27:42 +01:00
29cf88cc4b Update API usage for 2023.3
Closes #20
2023-11-05 18:06:36 +01:00
a05a97274b Update Gradle to 8.4 & update dependencies 2023-11-05 18:04:51 +01:00
9fd6f1b4fa Update screenshot in README 2023-05-29 12:11:31 +02:00
c8cacbe252 Release 1.3.0 2023-05-28 17:40:28 +02:00
13f3c86afa Fix inspections on LF characters causing line backgrounds to overflow to next line 2023-05-28 16:45:33 +02:00
synopss
94602bd8f9 Add line background colors
Closes #14
2023-05-28 16:08:59 +02:00
97422e1d42 Split and refactor EditorInlayLensManager 2023-05-26 10:49:22 +02:00
beab4af5ca Fix grammar errors 2023-05-25 19:30:01 +02:00
443b2b9a2d Release 1.2.0 2023-05-20 18:10:31 +02:00
e96961e10a Remove IDEA project configuration files 2023-05-20 18:10:31 +02:00
7432c57e95 Fix compiler configuration 2023-05-20 16:07:02 +02:00
0f49339ca2 Refresh existing highlights when a new inspection severity mapping is registered 2023-05-20 15:59:55 +02:00
13f3002366 Add distinct colors for typos and Grazie inspections 2023-05-20 15:59:53 +02:00
44f2fa5c16 Refactor use of TextAttributes for inspection severities 2023-05-20 10:26:21 +02:00
eb2faa2518 Fix markup model listener accessing UI from non-EDT threads in 2023.2 EAP
Closes #17
2023-05-20 06:06:02 +02:00
c993b4f203 Release 1.1.2 2023-04-15 12:49:52 +02:00
eb2d60f22d Add plugin icon 2023-04-15 12:47:02 +02:00
da47687696 Reorganize source code packages 2023-04-15 07:00:24 +02:00
86a3a87da2 Replace deprecated FileEditorManagerListener API 2023-04-12 07:03:11 +02:00
797efb48a2 Replace light/dark theme detection with a standard API 2023-04-12 07:00:44 +02:00
33bb46d0be Update minimum version to 2023.1 2023-04-12 06:36:04 +02:00
87f6eda572 Release 1.1.1 2023-01-07 13:58:37 +01:00
ca4fb0484a Rewrite inlay hint priority calculation to use position in line instead of the whole document 2023-01-07 05:25:38 +01:00
0aff0f49ae Optimize processing of inspection descriptions 2023-01-05 08:37:31 +01:00
e2384a98a8 Sort highlights at the same position by severity 2023-01-05 05:53:23 +01:00
7c3910854d Replace MultiParentDisposable with IntelliJ's Lifetime API 2022-10-24 00:55:54 +02:00
ce85aa130d Release 1.1.0 2022-10-06 01:57:04 +02:00
0e380a4658 Fix false positive compatibility problem in IntelliJ plugin verifier 2022-10-06 00:27:02 +02:00
fd50ca90b6 Update IntelliJ Gradle plugin to 1.9.0 2022-10-06 00:25:55 +02:00
53 changed files with 1789 additions and 580 deletions

2
.github/FUNDING.yml vendored
View File

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

After

Width:  |  Height:  |  Size: 17 KiB

7
.gitignore vendored
View File

@@ -1,8 +1,7 @@
/.idea/*
!/.idea/compiler.xml
!/.idea/encodings.xml
!/.idea/gradle.xml
!/.idea/vcs.xml
!/.idea/dictionaries
!/.idea/runConfigurations
/.gradle/
/.intellijPlatform/
/build/

5
.idea/.gitignore generated vendored
View File

@@ -1,5 +0,0 @@
# Default ignored files
/shelf/
/workspace.xml
# Editor-based HTTP Client requests
/httpRequests/

6
.idea/compiler.xml generated
View File

@@ -1,6 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="CompilerConfiguration">
<bytecodeTargetLevel target="11" />
</component>
</project>

7
.idea/dictionaries/default_user.xml generated Normal file
View File

@@ -0,0 +1,7 @@
<component name="ProjectDictionaryState">
<dictionary name="default.user">
<words>
<w>inspectionlens</w>
</words>
</dictionary>
</component>

6
.idea/encodings.xml generated
View File

@@ -1,6 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="Encoding" defaultCharsetForPropertiesFiles="UTF-8">
<file url="PROJECT" charset="UTF-8" />
</component>
</project>

19
.idea/gradle.xml generated
View File

@@ -1,19 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="GradleMigrationSettings" migrationVersion="1" />
<component name="GradleSettings">
<option name="linkedExternalProjectsSettings">
<GradleProjectSettings>
<option name="delegatedBuild" value="true" />
<option name="testRunner" value="GRADLE" />
<option name="distributionType" value="DEFAULT_WRAPPED" />
<option name="externalProjectPath" value="$PROJECT_DIR$" />
<option name="modules">
<set>
<option value="$PROJECT_DIR$" />
</set>
</option>
</GradleProjectSettings>
</option>
</component>
</project>

11
.idea/vcs.xml generated
View File

@@ -1,11 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="GitSharedSettings">
<option name="FORCE_PUSH_PROHIBITED_PATTERNS">
<list />
</option>
</component>
<component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$" vcs="Git" />
</component>
</project>

View File

@@ -1,9 +1,11 @@
IntelliJ plugin that shows errors, warnings, and other inspection highlights inline.
# Inspection Lens <img align="right" src="logo.png" alt="Plugin Logo">
Simply install the plugin and inspection descriptions will appear on the right side of the lines. Shown inspection severities are **Errors**, **Warnings**, **Weak Warnings**, **Server Problems**, **Typos**, and other inspections from plugins or future IntelliJ versions that have a high enough severity level. Each severity has a different color, with support for both light and dark themes.
Displays errors, warnings, and other inspections inline. Highlights the background of lines with inspections. Supports light and dark themes out of the box.
The plugin is not customizable outside of the ability to disable/enable the plugin without restarting the IDE. If the defaults don't work for you, I would recommend either trying the [Inline Error](https://plugins.jetbrains.com/plugin/17302-inlineerror) plugin which can be customized, or proposing your change in the [issue tracker](https://github.com/chylex/IntelliJ-Inspection-Lens/issues).
By default, the plugin shows **Errors**, **Warnings**, **Weak Warnings**, **Server Problems**, **Grammar Errors**, **Typos**, and other inspections with a high enough severity level. 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.
![Inspection Lens Screenshot](https://raw.githubusercontent.com/chylex/IntelliJ-Inspection-Lens/main/.github/readme/intellij.png)
![Inspection Lens Screenshot](.github/readme/intellij.png)

View File

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

Binary file not shown.

View File

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

296
gradlew vendored
View File

@@ -1,7 +1,7 @@
#!/usr/bin/env sh
#!/bin/sh
#
# Copyright 2015 the original author or authors.
# Copyright © 2015-2021 the original authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
@@ -15,69 +15,103 @@
# See the License for the specific language governing permissions and
# limitations under the License.
#
# SPDX-License-Identifier: Apache-2.0
#
##############################################################################
##
## Gradle start up script for UN*X
##
#
# Gradle start up script for POSIX generated by Gradle.
#
# Important for running:
#
# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
# noncompliant, but you have some other compliant shell such as ksh or
# bash, then to run this script, type that shell name before the whole
# command line, like:
#
# ksh Gradle
#
# Busybox and similar reduced shells will NOT work, because this script
# requires all of these POSIX shell features:
# * functions;
# * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
# «${var#prefix}», «${var%suffix}», and «$( cmd )»;
# * compound commands having a testable exit status, especially «case»;
# * various built-in commands including «command», «set», and «ulimit».
#
# Important for patching:
#
# (2) This script targets any POSIX shell, so it avoids extensions provided
# by Bash, Ksh, etc; in particular arrays are avoided.
#
# The "traditional" practice of packing multiple parameters into a
# space-separated string is a well documented source of bugs and security
# problems, so this is (mostly) avoided, by progressively accumulating
# options in "$@", and eventually passing that to Java.
#
# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
# see the in-line comments for details.
#
# There are tweaks for specific operating systems such as AIX, CygWin,
# Darwin, MinGW, and NonStop.
#
# (3) This script is generated from the Groovy template
# 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/.
#
##############################################################################
# Attempt to set APP_HOME
# Resolve links: $0 may be a link
PRG="$0"
# Need this for relative symlinks.
while [ -h "$PRG" ] ; do
ls=`ls -ld "$PRG"`
link=`expr "$ls" : '.*-> \(.*\)$'`
if expr "$link" : '/.*' > /dev/null; then
PRG="$link"
else
PRG=`dirname "$PRG"`"/$link"
fi
app_path=$0
# Need this for daisy-chained symlinks.
while
APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
[ -h "$app_path" ]
do
ls=$( ls -ld "$app_path" )
link=${ls#*' -> '}
case $link in #(
/*) app_path=$link ;; #(
*) app_path=$APP_HOME$link ;;
esac
done
SAVED="`pwd`"
cd "`dirname \"$PRG\"`/" >/dev/null
APP_HOME="`pwd -P`"
cd "$SAVED" >/dev/null
APP_NAME="Gradle"
APP_BASE_NAME=`basename "$0"`
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# This is normally unused
# 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 -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"
MAX_FD=maximum
warn () {
echo "$*"
}
} >&2
die () {
echo
echo "$*"
echo
exit 1
}
} >&2
# OS specific support (must be 'true' or 'false').
cygwin=false
msys=false
darwin=false
nonstop=false
case "`uname`" in
CYGWIN* )
cygwin=true
;;
Darwin* )
darwin=true
;;
MSYS* | MINGW* )
msys=true
;;
NONSTOP* )
nonstop=true
;;
case "$( uname )" in #(
CYGWIN* ) cygwin=true ;; #(
Darwin* ) darwin=true ;; #(
MSYS* | MINGW* ) msys=true ;; #(
NONSTOP* ) nonstop=true ;;
esac
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
@@ -87,9 +121,9 @@ CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
if [ -n "$JAVA_HOME" ] ; then
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
# IBM's JDK on AIX uses strange locations for the executables
JAVACMD="$JAVA_HOME/jre/sh/java"
JAVACMD=$JAVA_HOME/jre/sh/java
else
JAVACMD="$JAVA_HOME/bin/java"
JAVACMD=$JAVA_HOME/bin/java
fi
if [ ! -x "$JAVACMD" ] ; then
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
@@ -98,88 +132,120 @@ Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
else
JAVACMD="java"
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
JAVACMD=java
if ! command -v java >/dev/null 2>&1
then
die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
fi
# Increase the maximum file descriptors if we can.
if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
MAX_FD_LIMIT=`ulimit -H -n`
if [ $? -eq 0 ] ; then
if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
MAX_FD="$MAX_FD_LIMIT"
fi
ulimit -n $MAX_FD
if [ $? -ne 0 ] ; then
warn "Could not set maximum file descriptor limit: $MAX_FD"
fi
else
warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
fi
fi
# For Darwin, add options to specify how the application appears in the dock
if $darwin; then
GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
fi
# For Cygwin or MSYS, switch paths to Windows format before running java
if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
APP_HOME=`cygpath --path --mixed "$APP_HOME"`
CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
JAVACMD=`cygpath --unix "$JAVACMD"`
# We build the pattern for arguments to be converted via cygpath
ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
SEP=""
for dir in $ROOTDIRSRAW ; do
ROOTDIRS="$ROOTDIRS$SEP$dir"
SEP="|"
done
OURCYGPATTERN="(^($ROOTDIRS))"
# Add a user-defined pattern to the cygpath arguments
if [ "$GRADLE_CYGPATTERN" != "" ] ; then
OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
fi
# Now convert the arguments - kludge to limit ourselves to /bin/sh
i=0
for arg in "$@" ; do
CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
else
eval `echo args$i`="\"$arg\""
fi
i=`expr $i + 1`
done
case $i in
0) set -- ;;
1) set -- "$args0" ;;
2) set -- "$args0" "$args1" ;;
3) set -- "$args0" "$args1" "$args2" ;;
4) set -- "$args0" "$args1" "$args2" "$args3" ;;
5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
case $MAX_FD in #(
max*)
# In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
# shellcheck disable=SC2039,SC3045
MAX_FD=$( ulimit -H -n ) ||
warn "Could not query maximum file descriptor limit"
esac
case $MAX_FD in #(
'' | soft) :;; #(
*)
# In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
# shellcheck disable=SC2039,SC3045
ulimit -n "$MAX_FD" ||
warn "Could not set maximum file descriptor limit to $MAX_FD"
esac
fi
# Escape application args
save () {
for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
echo " "
}
APP_ARGS=`save "$@"`
# Collect all arguments for the java command, stacking in reverse order:
# * args from the command line
# * the main class name
# * -classpath
# * -D...appname settings
# * --module-path (only if needed)
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
# Collect all arguments for the java command, following the shell quoting and substitution rules
eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
# For Cygwin or MSYS, switch paths to Windows format before running java
if "$cygwin" || "$msys" ; then
APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
JAVACMD=$( cygpath --unix "$JAVACMD" )
# Now convert the arguments - kludge to limit ourselves to /bin/sh
for arg do
if
case $arg in #(
-*) false ;; # don't mess with options #(
/?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
[ -e "$t" ] ;; #(
*) false ;;
esac
then
arg=$( cygpath --path --ignore --mixed "$arg" )
fi
# Roll the args list around exactly as many times as the number of
# args, so each arg winds up back in the position where it started, but
# possibly modified.
#
# NB: a `for` loop captures its iteration list before it begins, so
# changing the positional parameters here affects neither the number of
# iterations, nor the values presented in `arg`.
shift # remove old arg
set -- "$@" "$arg" # push replacement arg
done
fi
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Collect all arguments for the java command:
# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
# and any embedded shellness will be escaped.
# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
# treated as '${Hostname}' itself on the command line.
set -- \
"-Dorg.gradle.appname=$APP_BASE_NAME" \
-classpath "$CLASSPATH" \
org.gradle.wrapper.GradleWrapperMain \
"$@"
# Stop when "xargs" is not available.
if ! command -v xargs >/dev/null 2>&1
then
die "xargs is not available"
fi
# Use "xargs" to parse quoted args.
#
# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
#
# In Bash we could simply go:
#
# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
# set -- "${ARGS[@]}" "$@"
#
# but POSIX shell has neither arrays nor command substitution, so instead we
# post-process each arg (as a line of input to sed) to backslash-escape any
# character that might be a shell metacharacter, then use eval to reverse
# that process (while maintaining the separation between arguments), and wrap
# the whole thing up as a single "set" statement.
#
# This will of course break if any of these variables contains a newline or
# an unmatched quote.
#
eval "set -- $(
printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
xargs -n1 |
sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
tr '\n' ' '
)" '"$@"'
exec "$JAVACMD" "$@"

33
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 ##########################################################################
@@ -26,6 +28,7 @@ if "%OS%"=="Windows_NT" setlocal
set DIRNAME=%~dp0
if "%DIRNAME%"=="" set DIRNAME=.
@rem This is normally unused
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%
@@ -40,13 +43,13 @@ if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if "%ERRORLEVEL%" == "0" goto execute
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
@@ -56,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
@@ -75,13 +78,15 @@ set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
:end
@rem End local scope for the variables with windows NT shell
if "%ERRORLEVEL%"=="0" goto mainEnd
if %ERRORLEVEL% equ 0 goto mainEnd
:fail
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem the _cmd.exe /c_ return code!
if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
exit /b 1
set EXIT_CODE=%ERRORLEVEL%
if %EXIT_CODE% equ 0 set EXIT_CODE=1
if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
exit /b %EXIT_CODE%
:mainEnd
if "%OS%"=="Windows_NT" endlocal

BIN
logo.afdesign Normal file

Binary file not shown.

BIN
logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

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

@@ -1,71 +0,0 @@
package com.chylex.intellij.inspectionlens
import com.intellij.codeInsight.daemon.impl.HighlightInfo
import com.intellij.openapi.editor.Editor
import com.intellij.openapi.editor.Inlay
import com.intellij.openapi.editor.InlayProperties
import com.intellij.openapi.editor.markup.RangeHighlighter
import com.intellij.openapi.util.Key
/**
* Manages visible inspection lenses for an [Editor].
*/
class EditorInlayLensManager private constructor(private val editor: Editor) {
companion object {
private val KEY = Key<EditorInlayLensManager>(EditorInlayLensManager::class.java.name)
fun getOrCreate(editor: Editor): EditorInlayLensManager {
return editor.getUserData(KEY) ?: EditorInlayLensManager(editor).also { editor.putUserData(KEY, it) }
}
fun remove(editor: Editor) {
val manager = editor.getUserData(KEY)
if (manager != null) {
manager.hideAll()
editor.putUserData(KEY, null)
}
}
private fun getInlayHintOffset(info: HighlightInfo): Int {
// Ensures a highlight at the end of a line does not overflow to the next line.
return info.actualEndOffset - 1
}
}
private val inlays = mutableMapOf<RangeHighlighter, Inlay<LensRenderer>>()
fun show(highlighterWithInfo: HighlighterWithInfo) {
val (highlighter, info) = highlighterWithInfo
val currentInlay = inlays[highlighter]
if (currentInlay != null && currentInlay.isValid) {
currentInlay.renderer.setPropertiesFrom(info)
currentInlay.update()
}
else {
val offset = getInlayHintOffset(info)
val renderer = LensRenderer(info)
val properties = InlayProperties().relatesToPrecedingText(true).disableSoftWrapping(true).priority(-offset)
editor.inlayModel.addAfterLineEndElement(offset, properties, renderer)?.let {
inlays[highlighter] = it
}
}
}
fun showAll(highlightersWithInfo: Collection<HighlighterWithInfo>) {
executeInInlayBatchMode(highlightersWithInfo.size) { highlightersWithInfo.forEach(::show) }
}
fun hide(highlighter: RangeHighlighter) {
inlays.remove(highlighter)?.dispose()
}
fun hideAll() {
executeInInlayBatchMode(inlays.size) { inlays.values.forEach(Inlay<*>::dispose) }
inlays.clear()
}
private fun executeInInlayBatchMode(operations: Int, block: () -> Unit) {
editor.inlayModel.execute(operations > 1000, block)
}
}

View File

@@ -0,0 +1,87 @@
package com.chylex.intellij.inspectionlens
import com.chylex.intellij.inspectionlens.editor.EditorLensFeatures
import com.intellij.openapi.application.ApplicationManager
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.
*/
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)
}
/**
* Installs lenses into all open editors.
*/
fun install() {
forEachOpenEditor(EditorLensFeatures::install)
}
/**
* Refreshes lenses in all open editors.
*/
private fun refresh() {
forEachOpenEditor(EditorLensFeatures::refresh)
}
/**
* Schedules a refresh of lenses in all open editors.
*/
fun scheduleRefresh() {
Refresh.schedule()
}
/**
* Executes [action] on all open editors.
*/
private inline fun forEachOpenEditor(action: (TextEditor) -> Unit) {
val projectManager = ProjectManager.getInstanceIfCreated() ?: return
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 : Runnable {
private val needsRefresh = AtomicBoolean(false)
fun schedule() {
if (needsRefresh.compareAndSet(false, true)) {
ApplicationManager.getApplication().invokeLater(this)
}
}
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,10 +1,22 @@
package com.chylex.intellij.inspectionlens
import com.intellij.openapi.Disposable
import com.intellij.openapi.components.Service
import com.intellij.openapi.rd.createLifetime
import com.intellij.openapi.rd.createNestedDisposable
import com.jetbrains.rd.util.lifetime.Lifetime
/**
* Gets automatically disposed when the plugin is unloaded.
*/
@Service
class InspectionLensPluginDisposableService : Disposable {
/**
* Creates a [Disposable] that will be disposed when either plugin is unloaded, or the [other] [Disposable] is disposed.
*/
fun intersect(other: Disposable): Disposable {
return Lifetime.intersect(createLifetime(), other.createLifetime()).createNestedDisposable("InspectionLensIntersectedLifetime")
}
override fun dispose() {}
}

View File

@@ -2,42 +2,14 @@ package com.chylex.intellij.inspectionlens
import com.intellij.ide.plugins.DynamicPluginListener
import com.intellij.ide.plugins.IdeaPluginDescriptor
import com.intellij.openapi.fileEditor.FileEditorManager
import com.intellij.openapi.fileEditor.TextEditor
import com.intellij.openapi.project.ProjectManager
/**
* Handles dynamic plugin loading.
*
* On load, it installs the [LensMarkupModelListener] to all open editors.
* On unload, it removes all lenses from all open editors.
* Installs [InspectionLens] in open editors when the plugin is loaded.
*/
class InspectionLensPluginListener : DynamicPluginListener {
companion object {
private const val PLUGIN_ID = "com.chylex.intellij.inspectionlens"
private inline fun ProjectManager.forEachEditor(action: (TextEditor) -> Unit) {
for (project in this.openProjects.filterNot { it.isDisposed }) {
val fileEditorManager = FileEditorManager.getInstance(project)
for (editor in fileEditorManager.allEditors.filterIsInstance<TextEditor>()) {
action(editor)
}
}
}
}
override fun pluginLoaded(pluginDescriptor: IdeaPluginDescriptor) {
if (pluginDescriptor.pluginId.idString == PLUGIN_ID) {
ProjectManager.getInstanceIfCreated()?.forEachEditor(LensMarkupModelListener.Companion::install)
}
}
override fun beforePluginUnload(pluginDescriptor: IdeaPluginDescriptor, isUpdate: Boolean) {
if (pluginDescriptor.pluginId.idString == PLUGIN_ID) {
ProjectManager.getInstanceIfCreated()?.forEachEditor {
EditorInlayLensManager.remove(it.editor)
}
if (pluginDescriptor.pluginId.idString == InspectionLens.PLUGIN_ID) {
InspectionLens.install()
}
}
}

View File

@@ -1,21 +0,0 @@
package com.chylex.intellij.inspectionlens
import com.intellij.openapi.fileEditor.FileEditorManager
import com.intellij.openapi.fileEditor.FileEditorManagerListener
import com.intellij.openapi.fileEditor.TextEditor
import com.intellij.openapi.fileEditor.ex.FileEditorWithProvider
import com.intellij.openapi.vfs.VirtualFile
/**
* Listens for newly opened editors, and installs a [LensMarkupModelListener] on them.
*/
class LensFileEditorListener : FileEditorManagerListener {
override fun fileOpenedSync(source: FileEditorManager, file: VirtualFile, editorsWithProviders: MutableList<FileEditorWithProvider>) {
for (editorWrapper in editorsWithProviders) {
val fileEditor = editorWrapper.fileEditor
if (fileEditor is TextEditor) {
LensMarkupModelListener.install(fileEditor)
}
}
}
}

View File

@@ -1,109 +0,0 @@
package com.chylex.intellij.inspectionlens
import com.chylex.intellij.inspectionlens.util.MultiParentDisposable
import com.intellij.codeInsight.daemon.impl.HighlightInfo
import com.intellij.lang.annotation.HighlightSeverity
import com.intellij.openapi.application.ApplicationManager
import com.intellij.openapi.editor.Editor
import com.intellij.openapi.editor.ex.MarkupModelEx
import com.intellij.openapi.editor.ex.RangeHighlighterEx
import com.intellij.openapi.editor.impl.DocumentMarkupModel
import com.intellij.openapi.editor.impl.event.MarkupModelListener
import com.intellij.openapi.editor.markup.RangeHighlighter
import com.intellij.openapi.fileEditor.TextEditor
/**
* Listens for inspection highlights and reports them to [EditorInlayLensManager].
*/
class LensMarkupModelListener private constructor(editor: Editor) : MarkupModelListener {
private val lens = EditorInlayLensManager.getOrCreate(editor)
override fun afterAdded(highlighter: RangeHighlighterEx) {
showIfValid(highlighter)
}
override fun attributesChanged(highlighter: RangeHighlighterEx, renderersChanged: Boolean, fontStyleOrColorChanged: Boolean) {
showIfValid(highlighter)
}
override fun beforeRemoved(highlighter: RangeHighlighterEx) {
lens.hide(highlighter)
}
private fun showIfValid(highlighter: RangeHighlighter) {
runWithHighlighterIfValid(highlighter, lens::show, ::showAsynchronously)
}
private fun showAllValid(highlighters: Array<RangeHighlighter>) {
val immediateHighlighters = mutableListOf<HighlighterWithInfo>()
for (highlighter in highlighters) {
runWithHighlighterIfValid(highlighter, immediateHighlighters::add, ::showAsynchronously)
}
lens.showAll(immediateHighlighters)
}
private fun showAsynchronously(highlighterWithInfo: HighlighterWithInfo.Async) {
highlighterWithInfo.requestDescription {
if (highlighterWithInfo.highlighter.isValid && highlighterWithInfo.hasDescription) {
val application = ApplicationManager.getApplication()
if (application.isDispatchThread) {
lens.show(highlighterWithInfo)
}
else {
application.invokeLater {
lens.show(highlighterWithInfo)
}
}
}
}
}
companion object {
private val MINIMUM_SEVERITY = HighlightSeverity.TEXT_ATTRIBUTES.myVal + 1
private inline fun runWithHighlighterIfValid(highlighter: RangeHighlighter, actionForImmediate: (HighlighterWithInfo) -> Unit, actionForAsync: (HighlighterWithInfo.Async) -> Unit) {
if (!highlighter.isValid) {
return
}
val info = HighlightInfo.fromRangeHighlighter(highlighter)
if (info == null || info.severity.myVal < MINIMUM_SEVERITY) {
return
}
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)
}
else if (highlighterWithInfo.hasDescription) {
actionForImmediate(highlighterWithInfo)
}
}
/**
* Attaches a new [LensMarkupModelListener] to the document model of the provided [TextEditor], and reports all existing inspection highlights to [EditorInlayLensManager].
*
* The [LensMarkupModelListener] will be disposed when either the [TextEditor] is disposed, or via [InspectionLensPluginDisposableService] when the plugin is unloaded.
*/
fun install(textEditor: TextEditor) {
val editor = textEditor.editor
val markupModel = DocumentMarkupModel.forDocument(editor.document, editor.project, false)
if (markupModel is MarkupModelEx) {
val pluginDisposable = ApplicationManager.getApplication().getService(InspectionLensPluginDisposableService::class.java)
val listenerDisposable = MultiParentDisposable()
listenerDisposable.registerWithParent(textEditor)
listenerDisposable.registerWithParent(pluginDisposable)
val listener = LensMarkupModelListener(editor)
markupModel.addMarkupModelListener(listenerDisposable.self, listener)
listener.showAllValid(markupModel.allHighlighters)
}
}
}
}

View File

@@ -1,56 +0,0 @@
package com.chylex.intellij.inspectionlens
import com.intellij.codeInsight.daemon.impl.HighlightInfo
import com.intellij.codeInsight.daemon.impl.HintRenderer
import com.intellij.openapi.editor.Editor
import com.intellij.openapi.editor.Inlay
import com.intellij.openapi.editor.markup.TextAttributes
import com.intellij.openapi.util.text.StringUtil
import java.awt.Font
import java.awt.Graphics
import java.awt.Rectangle
/**
* Renders the text of an inspection lens.
*/
class LensRenderer(info: HighlightInfo) : HintRenderer(null) {
private lateinit var severity: LensSeverity
init {
setPropertiesFrom(info)
}
fun setPropertiesFrom(info: HighlightInfo) {
text = getValidDescriptionText(info.description)
severity = LensSeverity.from(info.severity)
}
override fun paint(inlay: Inlay<*>, g: Graphics, r: Rectangle, textAttributes: TextAttributes) {
fixBaselineForTextRendering(r)
super.paint(inlay, g, r, textAttributes)
}
override fun getTextAttributes(editor: Editor): TextAttributes {
return ATTRIBUTES_SINGLETON.also { it.foregroundColor = severity.getColor(editor) }
}
override fun useEditorFont(): Boolean {
return true
}
private companion object {
private val ATTRIBUTES_SINGLETON = TextAttributes(null, null, null, null, Font.ITALIC)
private fun getValidDescriptionText(text: String?): String {
return if (text.isNullOrBlank()) " " else addMissingPeriod(StringUtil.unescapeXmlEntities(StringUtil.stripHtml(text, " ")))
}
private fun addMissingPeriod(text: String): String {
return if (text.endsWith('.')) text else "$text."
}
private fun fixBaselineForTextRendering(rect: Rectangle) {
rect.y += 1
}
}
}

View File

@@ -1,59 +0,0 @@
package com.chylex.intellij.inspectionlens
import com.intellij.lang.annotation.HighlightSeverity
import com.intellij.openapi.editor.Editor
import com.intellij.ui.ColorUtil
import java.awt.Color
/**
* Determines properties of inspection lenses based on severity.
*/
enum class LensSeverity(color: Color, darkThemeBrightening: Int, lightThemeDarkening: Int) {
ERROR(
Color(158, 41, 39),
darkThemeBrightening = 4,
lightThemeDarkening = 1,
),
WARNING(
Color(190, 145, 23),
darkThemeBrightening = 1,
lightThemeDarkening = 4,
),
WEAK_WARNING(
Color(117, 109, 86),
darkThemeBrightening = 3,
lightThemeDarkening = 3,
),
SERVER_PROBLEM(
Color(176, 97, 0),
darkThemeBrightening = 2,
lightThemeDarkening = 4,
),
OTHER(
Color(128, 128, 128),
darkThemeBrightening = 2,
lightThemeDarkening = 1,
);
private val darkThemeColor = ColorUtil.desaturate(ColorUtil.brighter(color, darkThemeBrightening), 2)
private val lightThemeColor = ColorUtil.saturate(ColorUtil.darker(color, lightThemeDarkening), 1)
fun getColor(editor: Editor): Color {
val isDarkTheme = ColorUtil.isDark(editor.colorsScheme.defaultBackground)
return if (isDarkTheme) darkThemeColor else lightThemeColor
}
companion object {
fun from(severity: HighlightSeverity) = when (severity) {
HighlightSeverity.ERROR -> ERROR
HighlightSeverity.WARNING -> WARNING
HighlightSeverity.WEAK_WARNING -> WEAK_WARNING
HighlightSeverity.GENERIC_SERVER_ERROR_OR_WARNING -> SERVER_PROBLEM
else -> OTHER
}
}
}

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

@@ -0,0 +1,15 @@
package com.chylex.intellij.inspectionlens.compatibility
import com.chylex.intellij.inspectionlens.editor.lens.LensSeverity
import com.intellij.grazie.ide.TextProblemSeverities
import com.intellij.openapi.project.Project
import com.intellij.openapi.startup.ProjectActivity
class GrazieSupport : ProjectActivity {
override suspend fun execute(project: Project) {
LensSeverity.registerMapping(TextProblemSeverities.GRAMMAR_ERROR, LensSeverity.ERROR)
LensSeverity.registerMapping(TextProblemSeverities.STYLE_ERROR, LensSeverity.GRAZIE)
LensSeverity.registerMapping(TextProblemSeverities.STYLE_WARNING, LensSeverity.GRAZIE)
LensSeverity.registerMapping(TextProblemSeverities.STYLE_SUGGESTION, LensSeverity.GRAZIE)
}
}

View File

@@ -0,0 +1,23 @@
package com.chylex.intellij.inspectionlens.compatibility
import com.chylex.intellij.inspectionlens.editor.lens.LensSeverity
import com.intellij.lang.annotation.HighlightSeverity
import com.intellij.openapi.diagnostic.logger
import com.intellij.profile.codeInspection.InspectionProfileManager
import com.intellij.spellchecker.SpellCheckerSeveritiesProvider
internal object SpellCheckerSupport {
private val log = logger<SpellCheckerSupport>()
fun load() {
typoSeverity?.let { LensSeverity.registerMapping(it, LensSeverity.TYPO) }
}
private val typoSeverity: HighlightSeverity?
get() = try {
SpellCheckerSeveritiesProvider.TYPO
} catch (e: NoClassDefFoundError) {
log.warn("Falling back to registered severity search due to ${e.javaClass.simpleName}: ${e.message}")
InspectionProfileManager.getInstance().severityRegistrar.getSeverity("TYPO")
}
}

View File

@@ -0,0 +1,62 @@
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
/**
* Manages Inspection Lens features for a single [Editor].
*/
internal class EditorLensFeatures private constructor(
editor: Editor,
private val markupModel: MarkupModelEx,
foldingModel: FoldingModelEx?,
disposable: Disposable
) {
private val lensManager = EditorLensManager(editor)
private val lensManagerDispatcher = EditorLensManagerDispatcher(lensManager)
private val markupModelListener = LensMarkupModelListener(lensManagerDispatcher)
init {
markupModel.addMarkupModelListener(disposable, markupModelListener)
markupModelListener.showAllValid(markupModel.allHighlighters)
foldingModel?.addListener(LensFoldingModelListener(lensManager), disposable)
}
private fun refresh() {
markupModelListener.hideAll()
markupModelListener.showAllValid(markupModel.allHighlighters)
}
companion object {
private val EDITOR_KEY = Key<EditorLensFeatures>(EditorLensFeatures::class.java.name)
fun install(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(owner: TextEditor) {
owner.editor.getUserData(EDITOR_KEY)?.refresh()
}
}
}

View File

@@ -0,0 +1,97 @@
package com.chylex.intellij.inspectionlens.editor
import com.chylex.intellij.inspectionlens.editor.lens.EditorLens
import com.chylex.intellij.inspectionlens.settings.LensSettingsState
import com.intellij.openapi.components.service
import com.intellij.openapi.editor.Editor
import com.intellij.openapi.editor.markup.RangeHighlighter
import java.util.IdentityHashMap
/**
* Manages visible inspection lenses for an [Editor].
*/
internal class EditorLensManager(private val editor: Editor) {
private val lenses = IdentityHashMap<RangeHighlighter, EditorLens>()
private val settings = service<LensSettingsState>()
private fun show(highlighterWithInfo: HighlighterWithInfo) {
val (highlighter, info) = highlighterWithInfo
if (!highlighter.isValid) {
return
}
val existingLens = lenses[highlighter]
if (existingLens != null) {
if (existingLens.update(info, settings)) {
return
}
existingLens.hide()
}
val newLens = EditorLens.show(editor, info, settings)
if (newLens != null) {
lenses[highlighter] = newLens
}
else if (existingLens != null) {
lenses.remove(highlighter)
}
}
private fun hide(highlighter: RangeHighlighter) {
lenses.remove(highlighter)?.hide()
}
fun hideAll() {
executeInBatchMode(lenses.size) {
lenses.values.forEach(EditorLens::hide)
lenses.clear()
}
}
fun execute(commands: Collection<Command>) {
executeInBatchMode(commands.size) {
commands.forEach { it.apply(this) }
}
}
fun onFoldRegionsChanged() {
lenses.values.forEach(EditorLens::onFoldRegionsChanged)
}
sealed interface Command {
fun apply(lensManager: EditorLensManager)
class Show(private val highlighter: HighlighterWithInfo) : Command {
override fun apply(lensManager: EditorLensManager) {
lensManager.show(highlighter)
}
}
class Hide(private val highlighter: RangeHighlighter) : Command {
override fun apply(lensManager: EditorLensManager) {
lensManager.hide(highlighter)
}
}
object HideAll : Command {
override fun apply(lensManager: EditorLensManager) {
lensManager.hideAll()
}
}
}
/**
* Batch mode affects both inlays and highlighters used for line colors.
*/
@Suppress("ConvertLambdaToReference")
private inline fun executeInBatchMode(operations: Int, crossinline action: () -> Unit) {
if (operations > 1000) {
editor.inlayModel.execute(true) { action() }
}
else {
action()
}
}
}

View File

@@ -0,0 +1,45 @@
package com.chylex.intellij.inspectionlens.editor
import com.intellij.openapi.application.ApplicationManager
import com.intellij.openapi.editor.markup.RangeHighlighter
internal class EditorLensManagerDispatcher(private val lensManager: EditorLensManager) {
private var queuedItems = mutableListOf<EditorLensManager.Command>()
private var isEnqueued = false
fun show(highlighterWithInfo: HighlighterWithInfo) {
enqueue(EditorLensManager.Command.Show(highlighterWithInfo))
}
fun hide(highlighter: RangeHighlighter) {
enqueue(EditorLensManager.Command.Hide(highlighter))
}
fun hideAll() {
enqueue(EditorLensManager.Command.HideAll)
}
private fun enqueue(item: EditorLensManager.Command) {
synchronized(this) {
queuedItems.add(item)
// Enqueue even if already on dispatch thread to debounce consecutive calls.
if (!isEnqueued) {
isEnqueued = true
ApplicationManager.getApplication().invokeLater(::process)
}
}
}
private fun process() {
var itemsToProcess: List<EditorLensManager.Command>
synchronized(this) {
itemsToProcess = queuedItems
queuedItems = mutableListOf()
isEnqueued = false
}
lensManager.execute(itemsToProcess)
}
}

View File

@@ -1,4 +1,4 @@
package com.chylex.intellij.inspectionlens
package com.chylex.intellij.inspectionlens.editor
import com.intellij.codeInsight.daemon.impl.AsyncDescriptionSupplier
import com.intellij.codeInsight.daemon.impl.HighlightInfo

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,76 @@
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
import com.intellij.openapi.editor.markup.RangeHighlighter
/**
* Listens for inspection highlights and reports them to [EditorLensManager].
*/
internal class LensMarkupModelListener(private val lensManagerDispatcher: EditorLensManagerDispatcher) : MarkupModelListener {
private val settings = service<LensSettingsState>()
override fun afterAdded(highlighter: RangeHighlighterEx) {
showIfValid(highlighter)
}
override fun attributesChanged(highlighter: RangeHighlighterEx, renderersChanged: Boolean, fontStyleOrColorChanged: Boolean) {
showIfValid(highlighter)
}
fun showAllValid(highlighters: Array<RangeHighlighter>) {
if (InspectionLens.SHOW_LENSES) {
highlighters.forEach(::showIfValid)
}
}
private fun showIfValid(highlighter: RangeHighlighter) {
if (!InspectionLens.SHOW_LENSES) {
return
}
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 processHighlighterWithInfo(highlighterWithInfo: HighlighterWithInfo, actionForImmediate: (HighlighterWithInfo) -> Unit, actionForAsync: (HighlighterWithInfo.Async) -> Unit) {
if (highlighterWithInfo is HighlighterWithInfo.Async) {
actionForAsync(highlighterWithInfo)
}
else if (highlighterWithInfo.hasDescription) {
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

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

View File

@@ -0,0 +1,76 @@
package com.chylex.intellij.inspectionlens.editor.lens
import com.chylex.intellij.inspectionlens.settings.LensSettingsState
import com.intellij.codeInsight.daemon.impl.HighlightInfo
import com.intellij.openapi.editor.Editor
import com.intellij.openapi.editor.Inlay
import com.intellij.openapi.editor.InlayProperties
@JvmInline
internal value class EditorLensInlay(private val inlay: Inlay<LensRenderer>) {
val editor
get() = inlay.editor
fun tryUpdate(info: HighlightInfo): Boolean {
if (!inlay.isValid) {
return false
}
inlay.renderer.setPropertiesFrom(info)
inlay.update()
return true
}
fun hide() {
inlay.dispose()
}
companion object {
fun show(editor: Editor, info: HighlightInfo, settings: LensSettingsState): EditorLensInlay? {
val offset = getInlayHintOffset(info)
val priority = getInlayHintPriority(editor, info)
val renderer = LensRenderer(info, settings)
val properties = InlayProperties()
.relatesToPrecedingText(true)
.disableSoftWrapping(true)
.priority(priority)
return editor.inlayModel.addAfterLineEndElement(offset, properties, renderer)
?.also(renderer::setInlay)
?.let(::EditorLensInlay)
}
/**
* Highest allowed severity for the purposes of sorting multiple highlights at the same offset.
* The value is a little higher than the highest [com.intellij.lang.annotation.HighlightSeverity], in case severities with higher values are introduced in the future.
*/
private const val MAXIMUM_SEVERITY = 500
private const val MAXIMUM_POSITION = ((Int.MAX_VALUE / MAXIMUM_SEVERITY) * 2) - 1
private fun getInlayHintOffset(info: HighlightInfo): Int {
// Ensures a highlight at the end of a line does not overflow to the next line.
return info.actualEndOffset - 1
}
fun getInlayHintPriority(position: Int, severity: Int): Int {
// Sorts highlights first by position on the line, then by severity.
val positionBucket = position.coerceIn(0, MAXIMUM_POSITION) * MAXIMUM_SEVERITY
// The multiplication can overflow, but subtracting overflowed result from Int.MAX_VALUE does not break continuity.
val positionFactor = Integer.MAX_VALUE - positionBucket
val severityFactor = severity.coerceIn(0, MAXIMUM_SEVERITY) - MAXIMUM_SEVERITY
return positionFactor + severityFactor
}
private fun getInlayHintPriority(editor: Editor, info: HighlightInfo): Int {
val startOffset = info.actualStartOffset
val positionOnLine = startOffset - getLineStartOffset(editor, startOffset)
return getInlayHintPriority(positionOnLine, info.severity.myVal)
}
private fun getLineStartOffset(editor: Editor, offset: Int): Int {
val position = editor.offsetToLogicalPosition(offset)
return editor.document.getLineStartOffset(position.line)
}
}
}

View File

@@ -0,0 +1,49 @@
package com.chylex.intellij.inspectionlens.editor.lens
import com.intellij.codeInsight.daemon.impl.HighlightInfo
import com.intellij.openapi.editor.Editor
import com.intellij.openapi.editor.ex.RangeHighlighterEx
import com.intellij.openapi.editor.markup.HighlighterLayer
import com.intellij.openapi.editor.markup.HighlighterTargetArea.LINES_IN_RANGE
import com.intellij.openapi.editor.markup.RangeHighlighter
import com.intellij.openapi.editor.markup.TextAttributes
@JvmInline
internal value class EditorLensLineBackground(private val highlighter: RangeHighlighter) {
val isInvalid
get() = !highlighter.isValid
fun onFoldRegionsChanged(editor: Editor, severity: LensSeverity) {
if (highlighter is RangeHighlighterEx) {
highlighter.textAttributes = getAttributes(editor, highlighter.startOffset, highlighter.endOffset, severity)
}
}
fun hide(editor: Editor) {
editor.markupModel.removeHighlighter(highlighter)
}
companion object {
fun show(editor: Editor, info: HighlightInfo): EditorLensLineBackground {
val startOffset = info.actualStartOffset
val endOffset = (info.actualEndOffset - 1).coerceAtLeast(startOffset)
val severity = LensSeverity.from(info.severity)
val layer = getHighlightLayer(severity)
val attributes = getAttributes(editor, startOffset, endOffset, severity)
return EditorLensLineBackground(editor.markupModel.addRangeHighlighter(startOffset, endOffset, layer, attributes, LINES_IN_RANGE))
}
private fun getHighlightLayer(severity: LensSeverity): Int {
return HighlighterLayer.CARET_ROW - 100 - severity.ordinal
}
private fun getAttributes(editor: Editor, startOffset: Int, endOffset: Int, severity: LensSeverity): TextAttributes? {
return if (editor.foldingModel.let { it.isOffsetCollapsed(startOffset) || it.isOffsetCollapsed(endOffset) })
null
else
severity.lineAttributes
}
}
}

View File

@@ -0,0 +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 {
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)
}
}
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()
ReadAction
.nonBlocking<IntentionsInfo> { collectIntentions(editor, project, file, highlightInfo, inlay.offset) }
.finishOnUiThread(ModalityState.current()) { tryShowPopup(project, file, editor, it) }
.submit(AppExecutorUtil.getAppExecutorService())
return true
}
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

@@ -0,0 +1,199 @@
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
import com.intellij.codeInsight.hints.presentation.InputHandler
import com.intellij.openapi.editor.Editor
import com.intellij.openapi.editor.Inlay
import com.intellij.openapi.editor.ScrollType
import com.intellij.openapi.editor.colors.EditorFontType
import com.intellij.openapi.editor.ex.EditorEx
import com.intellij.openapi.editor.impl.EditorImpl
import com.intellij.openapi.editor.markup.TextAttributes
import com.intellij.openapi.util.text.StringUtil
import com.intellij.ui.paint.EffectPainter
import java.awt.Cursor
import java.awt.Graphics
import java.awt.Graphics2D
import java.awt.MouseInfo
import java.awt.Point
import java.awt.Rectangle
import java.awt.event.MouseEvent
import java.util.regex.Pattern
import javax.swing.SwingUtilities
/**
* Renders the text of an inspection lens.
*/
class LensRenderer(private var info: HighlightInfo, private val settings: LensSettingsState) : HintRenderer(null), InputHandler {
private lateinit var inlay: Inlay<*>
private lateinit var attributes: LensSeverityTextAttributes
private var extraRightPadding = 0
private var hovered = false
init {
setPropertiesFrom(info)
}
fun setInlay(inlay: Inlay<*>) {
check(!this::inlay.isInitialized) { "Inlay already set" }
this.inlay = inlay
}
fun setPropertiesFrom(info: HighlightInfo) {
this.info = info
val description = getValidDescriptionText(info.description, settings.maxDescriptionLength)
text = description
attributes = LensSeverity.from(info.severity).textAttributes
extraRightPadding = if (description.lastOrNull() == '.') 2 else 0
}
override fun paint(inlay: Inlay<*>, g: Graphics, r: Rectangle, textAttributes: TextAttributes) {
fixBaselineForTextRendering(r)
super.paint(inlay, g, r, textAttributes)
if (hovered && isHoveringText()) {
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
val w = inlay.widthInPixels - UNDERLINE_WIDTH_REDUCTION - extraRightPadding
val h = editor.descent
g.color = attributes.foregroundColor
EffectPainter.LINE_UNDERSCORE.paint(g as Graphics2D, x, y, w, h, font)
}
override fun getTextAttributes(editor: Editor): TextAttributes {
return attributes
}
override fun useEditorFont(): Boolean {
return settings.useEditorFont
}
override fun mouseMoved(event: MouseEvent, translated: Point) {
setHovered(isHoveringText(translated))
}
override fun mouseExited() {
setHovered(false)
}
private fun setHovered(hovered: Boolean) {
if (hovered && settings.lensHoverMode == LensHoverMode.DISABLED) {
return
}
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) {
val hoverMode = settings.lensHoverMode
if (hoverMode == LensHoverMode.DISABLED || !isHoveringText(translated)) {
return
}
if (event.button.let { it == MouseEvent.BUTTON1 || it == MouseEvent.BUTTON2 }) {
event.consume()
val editor = inlay.editor
moveToOffset(editor, info.actualStartOffset)
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
&& point.x < inlay.widthInPixels - HOVER_HORIZONTAL_PADDING - extraRightPadding
&& point.y < inlay.heightInPixels - 1
}
private companion object {
/**
* [HintRenderer.paintHint] renders padding around text, but not around effects.
*/
private const val TEXT_HORIZONTAL_PADDING = 7
private const val HOVER_HORIZONTAL_PADDING = TEXT_HORIZONTAL_PADDING - 2
private const val UNDERLINE_WIDTH_REDUCTION = (TEXT_HORIZONTAL_PADDING * 2) - 1
/**
* Kotlin compiler inspections have an `[UPPERCASE_TAG]` at the beginning.
*/
private val UPPERCASE_TAG_REGEX = Pattern.compile("^\\[[A-Z_]+] ")
private fun getValidDescriptionText(text: String?, maxLength: Int): String {
return if (text.isNullOrBlank()) " " else addEllipsisOrMissingPeriod(unescapeHtmlEntities(stripUppercaseTag(text)), maxLength)
}
private fun stripUppercaseTag(text: String): String {
if (text.startsWith('[')) {
val matcher = UPPERCASE_TAG_REGEX.matcher(text)
if (matcher.find()) {
return text.substring(matcher.end())
}
}
return text
}
private fun unescapeHtmlEntities(text: String): String {
return if (text.contains('&')) StringUtil.unescapeXmlEntities(text) else text
}
private fun addEllipsisOrMissingPeriod(text: String, maxLength: Int): String {
return when {
text.length > maxLength -> text.take(maxLength).trimEnd { it.isWhitespace() || it == '.' } + ""
!text.endsWith('.') -> "$text."
else -> text
}
}
private fun fixBaselineForTextRendering(rect: Rectangle) {
rect.y += 1
}
private fun moveToOffset(editor: Editor, offset: Int) {
editor.caretModel.moveToOffset(offset)
editor.scrollingModel.scrollToCaret(ScrollType.MAKE_VISIBLE)
}
}
}

View File

@@ -0,0 +1,68 @@
package com.chylex.intellij.inspectionlens.editor.lens
import com.chylex.intellij.inspectionlens.InspectionLens
import com.chylex.intellij.inspectionlens.compatibility.SpellCheckerSupport
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
/**
* Determines properties of inspection lenses based on severity.
*/
@Suppress("UseJBColor", "InspectionUsingGrayColors")
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),
SERVER_PROBLEM (Color(176, 97, 0), lightThemeDarkening = 5, darkThemeBrightening = 2),
GRAZIE (Color( 53, 146, 196), lightThemeDarkening = 3, darkThemeBrightening = 1),
TYPO (Color( 73, 156, 84), lightThemeDarkening = 4, darkThemeBrightening = 1),
OTHER (Color(128, 128, 128), lightThemeDarkening = 2, darkThemeBrightening = 2);
val textAttributes: LensSeverityTextAttributes
val lineAttributes: LensSeverityTextAttributes
init {
val lightThemeColor = ColorUtil.saturate(ColorUtil.darker(baseColor, lightThemeDarkening), 1)
val darkThemeColor = ColorUtil.desaturate(ColorUtil.brighter(baseColor, darkThemeBrightening), 2)
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 {
private val mapping = Collections.synchronizedMap(mapOf(
HighlightSeverity.ERROR to ERROR,
HighlightSeverity.WARNING to WARNING,
HighlightSeverity.WEAK_WARNING to WEAK_WARNING,
HighlightSeverity.GENERIC_SERVER_ERROR_OR_WARNING to SERVER_PROBLEM,
))
init {
SpellCheckerSupport.load()
}
/**
* Registers a mapping from a [HighlightSeverity] to a [LensSeverity], and refreshes all open editors.
*/
internal fun registerMapping(severity: HighlightSeverity, lensSeverity: LensSeverity) {
if (mapping.put(severity, lensSeverity) != lensSeverity) {
InspectionLens.scheduleRefresh()
}
}
/**
* Returns the [LensSeverity] associated with the [HighlightSeverity], or [OTHER] if no explicit mapping is found.
*/
fun from(severity: HighlightSeverity): LensSeverity {
return mapping.getOrDefault(severity, OTHER)
}
}
}

View File

@@ -0,0 +1,31 @@
package com.chylex.intellij.inspectionlens.editor.lens
import com.intellij.codeInsight.daemon.impl.SeverityRegistrar
import com.intellij.lang.annotation.HighlightSeverity
import java.util.function.Predicate
class LensSeverityFilter(private val hiddenSeverityIds: Set<String>, private val showUnknownSeverities: Boolean) : Predicate<HighlightSeverity> {
private val knownSeverityIds = getSupportedSeverities().mapTo(HashSet(), HighlightSeverity::getName)
override fun test(severity: HighlightSeverity): Boolean {
if (!isSupported(severity)) {
return false
}
return if (severity.name in knownSeverityIds)
severity.name !in hiddenSeverityIds
else
showUnknownSeverities
}
companion object {
@Suppress("DEPRECATION")
private fun isSupported(severity: HighlightSeverity): Boolean {
return severity > HighlightSeverity.TEXT_ATTRIBUTES && severity !== HighlightSeverity.INFO
}
fun getSupportedSeverities(registrar: SeverityRegistrar = SeverityRegistrar.getSeverityRegistrar(null)): List<HighlightSeverity> {
return registrar.allSeverities.filter(::isSupported)
}
}
}

View File

@@ -0,0 +1,23 @@
package com.chylex.intellij.inspectionlens.editor.lens
import com.intellij.openapi.editor.markup.UnmodifiableTextAttributes
import com.intellij.ui.JBColor
import java.awt.Font
class LensSeverityTextAttributes(
private val foregroundColor: JBColor? = null,
private val backgroundColor: JBColor? = null,
private val fontStyle: Int = Font.PLAIN,
) : UnmodifiableTextAttributes() {
override fun getForegroundColor(): JBColor? {
return foregroundColor
}
override fun getBackgroundColor(): JBColor? {
return backgroundColor
}
override fun getFontType(): Int {
return fontStyle
}
}

View File

@@ -0,0 +1,159 @@
package com.chylex.intellij.inspectionlens.settings
import com.chylex.intellij.inspectionlens.editor.lens.LensSeverityFilter
import com.intellij.codeInsight.daemon.impl.SeverityRegistrar
import com.intellij.lang.annotation.HighlightSeverity
import com.intellij.openapi.components.service
import com.intellij.openapi.editor.event.SelectionEvent
import com.intellij.openapi.editor.event.SelectionListener
import com.intellij.openapi.editor.markup.TextAttributes
import com.intellij.openapi.options.BoundConfigurable
import com.intellij.openapi.options.ConfigurableWithId
import com.intellij.openapi.ui.DialogPanel
import com.intellij.openapi.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
class LensApplicationConfigurable : BoundConfigurable("Inspection Lens"), ConfigurableWithId {
companion object {
const val ID = "InspectionLens"
}
private data class DisplayedSeverity(
val id: String,
val severity: StoredSeverity,
val textAttributes: TextAttributes? = null,
) {
constructor(
severity: HighlightSeverity,
registrar: SeverityRegistrar,
) : this(
id = severity.name,
severity = StoredSeverity(severity),
textAttributes = registrar.getHighlightInfoTypeBySeverity(severity).attributesKey.defaultAttributes
)
}
private val settingsService = service<LensSettingsState>()
private val allSeverities by lazy(LazyThreadSafetyMode.NONE) {
val settings = settingsService.state
val registrar = SeverityRegistrar.getSeverityRegistrar(null)
val knownSeverities = LensSeverityFilter.getSupportedSeverities(registrar).map { DisplayedSeverity(it, registrar) }
val knownSeverityIds = knownSeverities.mapTo(HashSet(), DisplayedSeverity::id)
// Update names and priorities of stored severities.
for ((id, knownSeverity, _) in knownSeverities) {
val storedSeverity = settings.hiddenSeverities[id]
if (storedSeverity != null && storedSeverity != knownSeverity) {
settings.hiddenSeverities[id] = knownSeverity
}
}
val unknownSeverities = settings.hiddenSeverities.entries
.filterNot { it.key in knownSeverityIds }
.map { DisplayedSeverity(it.key, it.value) }
(knownSeverities + unknownSeverities).sortedByDescending { it.severity.priority }
}
override fun getId(): String {
return ID
}
override fun createPanel(): DialogPanel {
val settings = settingsService.state
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 {
checkBox(severity.name)
.bindSelectedToNotIn(settings.hiddenSeverities, id, severity)
.gap(RightGap.COLUMNS)
labelWithAttributes("Example", textAttributes)
}.layout(RowLayout.PARENT_GRID)
}
row {
checkBox("Other").bindSelected(settings::showUnknownSeverities)
}
}
group("Actions") {
row {
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> {
return bindSelected({ key !in collection }, { if (it) collection.remove(key) else collection[key] = value })
}
private fun Row.labelWithAttributes(text: String, textAttributes: TextAttributes?): Cell<SimpleRendererComponent> {
val label = SimpleRendererComponent(null, null, true)
label.setText(text, textAttributes, false)
label.focusTraversalPolicy = DisabledTraversalPolicy()
val editor = label.editor
editor.setCustomCursor(this, Cursor.getDefaultCursor())
editor.contentComponent.setOpaque(false)
editor.selectionModel.addSelectionListener(object : SelectionListener {
override fun selectionChanged(e: SelectionEvent) {
if (!e.newRange.isEmpty) {
editor.selectionModel.removeSelection(true)
}
}
})
Disposer.register(disposable!!, label)
return cell(label)
}
override fun apply() {
super.apply()
settingsService.update()
}
}

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

@@ -0,0 +1,73 @@
package com.chylex.intellij.inspectionlens.settings
import com.chylex.intellij.inspectionlens.InspectionLens
import com.chylex.intellij.inspectionlens.editor.lens.LensSeverityFilter
import com.intellij.openapi.components.BaseState
import com.intellij.openapi.components.SettingsCategory
import com.intellij.openapi.components.SimplePersistentStateComponent
import com.intellij.openapi.components.State
import com.intellij.openapi.components.Storage
import com.intellij.util.xmlb.annotations.XMap
@State(
name = LensApplicationConfigurable.ID,
storages = [ Storage("chylex.inspectionLens.xml") ],
category = SettingsCategory.UI
)
class LensSettingsState : SimplePersistentStateComponent<LensSettingsState.State>(State()) {
class State : BaseState() {
@get:XMap
val hiddenSeverities by map<String, StoredSeverity>()
var showUnknownSeverities by property(true)
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
@set:Synchronized
var severityFilter = createSeverityFilter()
private set
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()
}
fun update() {
severityFilter = createSeverityFilter()
InspectionLens.scheduleRefresh()
}
private fun createSeverityFilter(): LensSeverityFilter {
val state = state
return LensSeverityFilter(state.hiddenSeverities.keys, state.showUnknownSeverities)
}
}

View File

@@ -0,0 +1,9 @@
package com.chylex.intellij.inspectionlens.settings
import com.intellij.lang.annotation.HighlightSeverity
import com.intellij.util.xmlb.annotations.Tag
@Tag("severity")
data class StoredSeverity(var name: String = "", var priority: Int = 0) {
constructor(severity: HighlightSeverity) : this(severity.displayCapitalizedName, severity.myVal)
}

View File

@@ -1,18 +0,0 @@
package com.chylex.intellij.inspectionlens.util
import com.intellij.openapi.Disposable
import com.intellij.openapi.util.Disposer
import java.lang.ref.WeakReference
/**
* A [Disposable] that can have multiple parents, and will be disposed when any parent is disposed.
* A [WeakReference] and a lambda will remain in memory for every parent that is not disposed.
*/
class MultiParentDisposable {
val self = Disposer.newDisposable()
fun registerWithParent(parent: Disposable) {
val weakSelfReference = WeakReference(self)
Disposer.register(parent) { weakSelfReference.get()?.let(Disposer::dispose) }
}
}

View File

@@ -0,0 +1,5 @@
<idea-plugin>
<extensions defaultExtensionNs="com.intellij">
<postStartupActivity implementation="com.chylex.intellij.inspectionlens.compatibility.GrazieSupport" />
</extensions>
</idea-plugin>

View File

@@ -4,19 +4,89 @@
<vendor url="https://chylex.com">chylex</vendor>
<description><![CDATA[
Shows errors, warnings, and other inspection highlights inline.
Displays errors, warnings, and other inspections inline. Highlights the background of lines with inspections. Supports light and dark themes out of the box.
<br><br>
Simply install the plugin and inspection descriptions will appear on the right side of the lines.
Shown inspection severities are <b>Errors</b>, <b>Warnings</b>, <b>Weak Warnings</b>, <b>Server Problems</b>, <b>Typos</b>, and other inspections from plugins or future IntelliJ versions that have a high enough severity level.
Each severity has a different color, with support for both light and dark themes.
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>
The plugin is not customizable outside of the ability to disable/enable the plugin without restarting the IDE.
If the defaults don't work for you, I would recommend either trying the <a href="https://plugins.jetbrains.com/plugin/17302-inlineerror">Inline Error</a> plugin which can be customized, or proposing your change in the <a href="https://github.com/chylex/IntelliJ-Inspection-Lens/issues">issue tracker</a>.
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>
<li>Added possibility to middle-click an inspection to navigate to relevant code in the editor.</li>
<li>Added option to use UI font instead of editor font.</li>
<li>Long inspection descriptions are now truncated to 120 characters.</li>
<li>Improved descriptions of Kotlin compiler inspections.</li>
<li>Fixed visual artifacts in Rendered Doc comments.</li>
</ul>
<b>Version 1.4.1</b>
<ul>
<li>Fixed warnings in usage of IntelliJ SDK.</li>
</ul>
<b>Version 1.4</b>
<ul>
<li>Added configuration of visible severities to <b>Settings | Tools | Inspection Lens</b>.</li>
</ul>
<b>Version 1.3.3</b>
<ul>
<li>Partially reverted fix for inspections that include HTML in their description due to breaking inspections with angled brackets.</li>
<li>Fixed plugin not working when installed on JetBrains Gateway Client.</li>
</ul>
<b>Version 1.3.2</b>
<ul>
<li>Fixed inspections randomly not disappearing.</li>
</ul>
<b>Version 1.3.1</b>
<ul>
<li>Updated minimum version to IntelliJ 2023.3 due to breaking API changes.</li>
</ul>
<b>Version 1.3.0</b>
<ul>
<li>Added background colors to lines containing inspections. (<a href="https://github.com/chylex/IntelliJ-Inspection-Lens/pull/15">PR #15</a> by <a href="https://github.com/synopss">synopss</a>)</li>
</ul>
<b>Version 1.2.0</b>
<ul>
<li>Support for IntelliJ 2023.2 EAP.</li>
<li>Added distinct colors for typos and Grazie inspections.</li>
</ul>
<b>Version 1.1.2</b>
<ul>
<li>Added plugin icon.</li>
<li>Updated minimum version to IntelliJ 2023.1 due to deprecated APIs.</li>
</ul>
<b>Version 1.1.1</b>
<ul>
<li>Multiple inspections at the same place in the document are now ordered by severity.</li>
<li>Improved performance of processing inspections which do not contain HTML.</li>
</ul>
<b>Version 1.1.0</b>
<ul>
<li>Fixed showing inspections that include HTML in their description. (<a href="https://github.com/chylex/IntelliJ-Inspection-Lens/pull/3">PR #3</a> by <a href="https://github.com/KostkaBrukowa">KostkaBrukowa</a>)</li>
<li>Fixed exception when asynchronous inspections run on a non-EDT thread.</li>
</ul>
<b>Version 1.0.0</b>
<ul>
<li>Initial version with support for IntelliJ 2022.2 and newer.</li>
@@ -24,16 +94,27 @@
]]></change-notes>
<depends>com.intellij.modules.platform</depends>
<depends optional="true" config-file="compatibility/InspectionLens-Grazie.xml">tanvd.grazi</depends>
<extensions defaultExtensionNs="com.intellij">
<applicationService serviceImplementation="com.chylex.intellij.inspectionlens.InspectionLensPluginDisposableService" />
<applicationService serviceImplementation="com.chylex.intellij.inspectionlens.settings.LensSettingsState" />
<applicationConfigurable id="com.chylex.intellij.inspectionlens.settings.LensApplicationConfigurable"
instance="com.chylex.intellij.inspectionlens.settings.LensApplicationConfigurable"
displayName="Inspection Lens"
parentId="tools" />
</extensions>
<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.LensFileEditorListener" topic="com.intellij.openapi.fileEditor.FileEditorManagerListener" />
<listener class="com.chylex.intellij.inspectionlens.InspectionLensFileEditorManagerListener" topic="com.intellij.openapi.fileEditor.FileEditorManagerListener" />
</projectListeners>
</idea-plugin>

View File

@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg width="100%" height="100%" viewBox="0 0 40 40" version="1.1" xmlns="http://www.w3.org/2000/svg" xml:space="preserve" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linecap:square;stroke-miterlimit:1.5;">
<g>
<path d="M13.275,24.427c0.685,0.842 1.456,1.613 2.298,2.298l-11.01,11.01l-2.298,-2.298l11.01,-11.01Z" style="fill:#767676;"/>
<path d="M25.5,2c6.899,0 12.5,5.601 12.5,12.5c-0,6.899 -5.601,12.5 -12.5,12.5c-6.899,-0 -12.5,-5.601 -12.5,-12.5c-0,-6.899 5.601,-12.5 12.5,-12.5Zm-0,3c-5.243,0 -9.5,4.257 -9.5,9.5c-0,5.243 4.257,9.5 9.5,9.5c5.243,-0 9.5,-4.257 9.5,-9.5c-0,-5.243 -4.257,-9.5 -9.5,-9.5Z" style="fill:#8e8e8e;"/>
</g>
<g>
<path d="M19.5,18.5l2,-2l2,2l2,-2l2,2l2,-2l2,2" style="fill:none;stroke:#d2524f;stroke-width:1.15px;"/>
<path d="M19.5,12.5l2,-2l2,2l2,-2l2,2l2,-2l2,2" style="fill:none;stroke:#be9139;stroke-width:1.15px;"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.0 KiB

View File

@@ -0,0 +1,65 @@
package com.chylex.intellij.inspectionlens.editor.lens
import com.intellij.lang.annotation.HighlightSeverity
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Nested
import org.junit.jupiter.api.Test
import org.junit.jupiter.params.ParameterizedTest
import org.junit.jupiter.params.provider.ValueSource
class EditorLensTest {
@Nested
inner class Priority {
@ParameterizedTest(name = "positionAndSeverity = {0}")
@ValueSource(ints = [0, -1, Int.MIN_VALUE])
fun minimumOffset(positionAndSeverity: Int) {
assertEquals(Int.MAX_VALUE, EditorLensInlay.getInlayHintPriority(positionAndSeverity, Int.MAX_VALUE))
}
@ParameterizedTest(name = "position = {0}")
@ValueSource(ints = [8_589_933, Int.MAX_VALUE])
fun maximumOffset(position: Int) {
assertEquals(Int.MIN_VALUE + 295, EditorLensInlay.getInlayHintPriority(position, Int.MIN_VALUE))
}
@ParameterizedTest(name = "severity = {0}")
@ValueSource(ints = [0, 1, 250, 499, 500])
fun firstPriorityBucket(severity: Int) {
assertEquals(Int.MAX_VALUE - 500 + severity, EditorLensInlay.getInlayHintPriority(0, severity))
}
@ParameterizedTest(name = "severity = {0}")
@ValueSource(ints = [0, 1, 250, 499, 500])
fun secondPriorityBucket(severity: Int) {
assertEquals(Int.MAX_VALUE - 1000 + severity, EditorLensInlay.getInlayHintPriority(1, severity))
}
@ParameterizedTest(name = "severity = {0}")
@ValueSource(ints = [0, 1, 250, 499, 500])
fun middlePriorityBucket(severity: Int) {
assertEquals(-353 + severity, EditorLensInlay.getInlayHintPriority(4294967, severity))
}
@ParameterizedTest(name = "severity = {0}")
@ValueSource(ints = [0, 1, 250, 499, 500])
fun penultimatePriorityBucket(severity: Int) {
assertEquals(Int.MIN_VALUE + 295 + 500 + severity, EditorLensInlay.getInlayHintPriority(8_589_932, severity))
}
/**
* If any of these changes, re-evaluate [EditorLensInlay.MAXIMUM_SEVERITY] and the priority calculations.
*/
@Nested
inner class IdeaHighlightSeverityAssumptions {
@Test
fun smallestSeverityHasNotChanged() {
assertEquals(10, HighlightSeverity.DEFAULT_SEVERITIES.minOf(HighlightSeverity::myVal))
}
@Test
fun highestSeverityHasNotChanged() {
assertEquals(400, HighlightSeverity.DEFAULT_SEVERITIES.maxOf(HighlightSeverity::myVal))
}
}
}
}