mirror of
				https://github.com/chylex/IntelliJ-Inspection-Lens.git
				synced 2025-10-31 20:17:15 +01:00 
			
		
		
		
	Compare commits
	
		
			67 Commits
		
	
	
		
			06c81654e8
			...
			debug-logs
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 2be1144cbf | |||
| fbe67d6068 | |||
| 98d3e5db01 | |||
| 102352a2eb | |||
| 3aeeb32bef | |||
| 0bc85fd69b | |||
| cde4d81afe | |||
| b1d6ed4d30 | |||
| 2a4764fa15 | |||
| 4bd0931d71 | |||
| 0f41b22872 | |||
| 603b35abdb | |||
| 08ed1aadea | |||
| 8936f0e5be | |||
| 89e71d5301 | |||
| 4c80573375 | |||
| 816440a150 | |||
| 4899498522 | |||
| 8ee14ff55e | |||
| 632a052ff9 | |||
| f2ec3c3d9b | |||
| 624254fba3 | |||
| c1b52ec3a5 | |||
| a84bc72fd4 | |||
| e28804ad5d | |||
| 043c02e432 | |||
| 223fceb6b9 | |||
| 640d95cddc | |||
| fcd4d6588d | |||
| 1c92cf000f | |||
| 83c179574a | |||
| 536650510d | |||
| 9f395476df | |||
| 182ad049d9 | |||
| 29cf88cc4b | |||
| a05a97274b | |||
| 9fd6f1b4fa | |||
| c8cacbe252 | |||
| 13f3c86afa | |||
|   | 94602bd8f9 | ||
| 97422e1d42 | |||
| beab4af5ca | |||
| 443b2b9a2d | |||
| e96961e10a | |||
| 7432c57e95 | |||
| 0f49339ca2 | |||
| 13f3002366 | |||
| 44f2fa5c16 | |||
| eb2faa2518 | |||
| c993b4f203 | |||
| eb2d60f22d | |||
| da47687696 | |||
| 86a3a87da2 | |||
| 797efb48a2 | |||
| 33bb46d0be | |||
| 87f6eda572 | |||
| ca4fb0484a | |||
| 0aff0f49ae | |||
| e2384a98a8 | |||
| 7c3910854d | |||
| ce85aa130d | |||
| 0e380a4658 | |||
| fd50ca90b6 | |||
| e6be154f88 | |||
|   | 963c49a3c8 | ||
| cd465ebc0e | |||
| 10461d2927 | 
							
								
								
									
										2
									
								
								.github/FUNDING.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								.github/FUNDING.yml
									
									
									
									
										vendored
									
									
								
							| @@ -1,3 +1 @@ | ||||
| github: chylex | ||||
| patreon: chylex | ||||
| ko_fi: chylex | ||||
|   | ||||
							
								
								
									
										
											BIN
										
									
								
								.github/readme/intellij.png
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										
											BIN
										
									
								
								.github/readme/intellij.png
									
									
									
									
										vendored
									
									
								
							
										
											Binary file not shown.
										
									
								
							| Before Width: | Height: | Size: 20 KiB After Width: | Height: | Size: 17 KiB | 
							
								
								
									
										7
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										7
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							| @@ -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
									
									
								
							
							
						
						
									
										5
									
								
								.idea/.gitignore
									
									
									
										generated
									
									
										vendored
									
									
								
							| @@ -1,5 +0,0 @@ | ||||
| # Default ignored files | ||||
| /shelf/ | ||||
| /workspace.xml | ||||
| # Editor-based HTTP Client requests | ||||
| /httpRequests/ | ||||
							
								
								
									
										6
									
								
								.idea/compiler.xml
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										6
									
								
								.idea/compiler.xml
									
									
									
										generated
									
									
									
								
							| @@ -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
									
								
							
							
						
						
									
										7
									
								
								.idea/dictionaries/default_user.xml
									
									
									
										generated
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,7 @@ | ||||
| <component name="ProjectDictionaryState"> | ||||
|   <dictionary name="default.user"> | ||||
|     <words> | ||||
|       <w>inspectionlens</w> | ||||
|     </words> | ||||
|   </dictionary> | ||||
| </component> | ||||
							
								
								
									
										6
									
								
								.idea/encodings.xml
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										6
									
								
								.idea/encodings.xml
									
									
									
										generated
									
									
									
								
							| @@ -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
									
									
									
								
							
							
						
						
									
										19
									
								
								.idea/gradle.xml
									
									
									
										generated
									
									
									
								
							| @@ -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
									
									
									
								
							
							
						
						
									
										11
									
								
								.idea/vcs.xml
									
									
									
										generated
									
									
									
								
							| @@ -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> | ||||
							
								
								
									
										10
									
								
								README.md
									
									
									
									
									
								
							
							
						
						
									
										10
									
								
								README.md
									
									
									
									
									
								
							| @@ -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. | ||||
|  | ||||
|  | ||||
|  | ||||
|   | ||||
| @@ -1,35 +1,50 @@ | ||||
| @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 = "0.0.1" | ||||
| version = "1.5.2.902" | ||||
|  | ||||
| repositories { | ||||
| 	mavenCentral() | ||||
| 	 | ||||
| 	intellijPlatform { | ||||
| 		defaultRepositories() | ||||
| 	} | ||||
| } | ||||
|  | ||||
| intellij { | ||||
| 	version.set("2022.2") | ||||
| 	updateSinceUntilBuild.set(false) | ||||
| dependencies { | ||||
| 	intellijPlatform { | ||||
| 		intellijIdeaUltimate("2025.1") | ||||
| 		bundledPlugin("tanvd.grazi") | ||||
| 	} | ||||
| 	 | ||||
| 	testImplementation("org.junit.jupiter:junit-jupiter:5.9.2") | ||||
| } | ||||
|  | ||||
| tasks.patchPluginXml { | ||||
| 	sinceBuild.set("222") | ||||
| intellijPlatform { | ||||
| 	pluginConfiguration { | ||||
| 		ideaVersion { | ||||
| 			sinceBuild.set("233.11361.10") | ||||
| 			untilBuild.set(provider { null }) | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
|  | ||||
| tasks.buildSearchableOptions { | ||||
| 	enabled = false | ||||
| kotlin { | ||||
| 	jvmToolchain(17) | ||||
| 	 | ||||
| 	compilerOptions { | ||||
| 		freeCompilerArgs = listOf( | ||||
| 			"-X" + "jvm-default=all", | ||||
| 			"-X" + "lambdas=indy" | ||||
| 		) | ||||
| 	} | ||||
| } | ||||
|  | ||||
| tasks.withType<KotlinCompile> { | ||||
| 	kotlinOptions.jvmTarget = "11" | ||||
| 	kotlinOptions.freeCompilerArgs = listOf( | ||||
| 		"-Xjvm-default=enable" | ||||
| 	) | ||||
| tasks.test { | ||||
| 	useJUnitPlatform() | ||||
| } | ||||
|   | ||||
							
								
								
									
										
											BIN
										
									
								
								gradle/wrapper/gradle-wrapper.jar
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										
											BIN
										
									
								
								gradle/wrapper/gradle-wrapper.jar
									
									
									
									
										vendored
									
									
								
							
										
											Binary file not shown.
										
									
								
							
							
								
								
									
										4
									
								
								gradle/wrapper/gradle-wrapper.properties
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										4
									
								
								gradle/wrapper/gradle-wrapper.properties
									
									
									
									
										vendored
									
									
								
							| @@ -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
									
									
								
							
							
						
						
									
										296
									
								
								gradlew
									
									
									
									
										vendored
									
									
								
							| @@ -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" "$@" | ||||
|   | ||||
							
								
								
									
										37
									
								
								gradlew.bat
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										37
									
								
								gradlew.bat
									
									
									
									
										vendored
									
									
								
							| @@ -13,8 +13,10 @@ | ||||
| @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 | ||||
| @if "%DEBUG%"=="" @echo off | ||||
| @rem ########################################################################## | ||||
| @rem | ||||
| @rem  Gradle startup script for Windows | ||||
| @@ -25,7 +27,8 @@ | ||||
| if "%OS%"=="Windows_NT" setlocal | ||||
|  | ||||
| set DIRNAME=%~dp0 | ||||
| if "%DIRNAME%" == "" set DIRNAME=. | ||||
| 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
									
								
							
							
						
						
									
										
											BIN
										
									
								
								logo.afdesign
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| @@ -1 +1,8 @@ | ||||
| rootProject.name = "InspectionLens" | ||||
|  | ||||
| pluginManagement { | ||||
| 	plugins { | ||||
| 		kotlin("jvm") version "2.1.0" | ||||
| 		id("org.jetbrains.intellij.platform") version "2.2.1" | ||||
| 	} | ||||
| } | ||||
|   | ||||
| @@ -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) | ||||
| 	} | ||||
| } | ||||
| @@ -0,0 +1,83 @@ | ||||
| package com.chylex.intellij.inspectionlens | ||||
|  | ||||
| import com.chylex.intellij.inspectionlens.editor.EditorLensFeatures | ||||
| import com.intellij.openapi.application.ApplicationManager | ||||
| import com.intellij.openapi.components.service | ||||
| import com.intellij.openapi.diagnostic.logger | ||||
| import com.intellij.openapi.fileEditor.FileEditorManager | ||||
| import com.intellij.openapi.fileEditor.TextEditor | ||||
| import com.intellij.openapi.project.ProjectManager | ||||
|  | ||||
| /** | ||||
|  * Handles installation and uninstallation of plugin features in editors. | ||||
|  */ | ||||
| internal object InspectionLens { | ||||
| 	const val PLUGIN_ID = "com.chylex.intellij.inspectionlens" | ||||
| 	 | ||||
| 	val LOG = logger<InspectionLens>() | ||||
| 	 | ||||
| 	/** | ||||
| 	 * Installs lenses into [editor]. | ||||
| 	 */ | ||||
| 	fun install(editor: TextEditor) { | ||||
| 		EditorLensFeatures.install(editor.editor, service<InspectionLensPluginDisposableService>().intersect(editor)) | ||||
| 	} | ||||
| 	 | ||||
| 	/** | ||||
| 	 * Installs lenses into all open editors. | ||||
| 	 */ | ||||
| 	fun install() { | ||||
| 		forEachOpenEditor(::install) | ||||
| 	} | ||||
| 	 | ||||
| 	/** | ||||
| 	 * Refreshes lenses in all open editors. | ||||
| 	 */ | ||||
| 	private fun refresh() { | ||||
| 		forEachOpenEditor { | ||||
| 			EditorLensFeatures.refresh(it.editor) | ||||
| 		} | ||||
| 	} | ||||
| 	 | ||||
| 	/** | ||||
| 	 * 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.filterNot { it.isDisposed }) { | ||||
| 			for (editor in FileEditorManager.getInstance(project).allEditors.filterIsInstance<TextEditor>()) { | ||||
| 				action(editor) | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
| 	 | ||||
| 	private object Refresh { | ||||
| 		private var needsRefresh = false | ||||
| 		 | ||||
| 		fun schedule() { | ||||
| 			synchronized(this) { | ||||
| 				if (!needsRefresh) { | ||||
| 					needsRefresh = true | ||||
| 					ApplicationManager.getApplication().invokeLater(this::run) | ||||
| 				} | ||||
| 			} | ||||
| 		} | ||||
| 		 | ||||
| 		private fun run() { | ||||
| 			synchronized(this) { | ||||
| 				if (needsRefresh) { | ||||
| 					needsRefresh = false | ||||
| 					refresh() | ||||
| 				} | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
| @@ -1,20 +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.fileEditor.ex.FileEditorWithProvider | ||||
| import com.intellij.openapi.vfs.VirtualFile | ||||
| 
 | ||||
| /** | ||||
|  * Listens for newly opened editors, and installs a [LensMarkupModelListener] on them. | ||||
|  * Installs [InspectionLens] in newly opened editors. | ||||
|  */ | ||||
| class LensFileEditorListener : FileEditorManagerListener { | ||||
| 	override fun fileOpenedSync(source: FileEditorManager, file: VirtualFile, editorsWithProviders: MutableList<FileEditorWithProvider>) { | ||||
| class InspectionLensFileOpenedListener : FileOpenedSyncListener { | ||||
| 	override fun fileOpenedSync(source: FileEditorManager, file: VirtualFile, editorsWithProviders: List<FileEditorWithProvider>) { | ||||
| 		InspectionLens.LOG.info("File opened: $file (editor count: ${editorsWithProviders.size})") | ||||
| 		 | ||||
| 		for (editorWrapper in editorsWithProviders) { | ||||
| 			val fileEditor = editorWrapper.fileEditor | ||||
| 			if (fileEditor is TextEditor) { | ||||
| 				LensMarkupModelListener.install(fileEditor) | ||||
| 				InspectionLens.install(fileEditor) | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
| @@ -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() {} | ||||
| } | ||||
|   | ||||
| @@ -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() | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
|   | ||||
| @@ -1,101 +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) { | ||||
| 				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) | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
| @@ -1,55 +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 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(text) | ||||
| 		} | ||||
| 		 | ||||
| 		private fun addMissingPeriod(text: String): String { | ||||
| 			return if (text.endsWith('.')) text else "$text." | ||||
| 		} | ||||
| 		 | ||||
| 		private fun fixBaselineForTextRendering(rect: Rectangle) { | ||||
| 			rect.y += 1 | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
| @@ -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 | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
| @@ -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) | ||||
| 	} | ||||
| } | ||||
| @@ -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") | ||||
| 		} | ||||
| } | ||||
| @@ -0,0 +1,9 @@ | ||||
| package com.chylex.intellij.inspectionlens.debug | ||||
|  | ||||
| import com.intellij.codeInsight.daemon.impl.HighlightInfo | ||||
| import com.intellij.lang.annotation.HighlightSeverity | ||||
| import com.intellij.openapi.editor.markup.RangeHighlighter | ||||
|  | ||||
| data class Highlighter(val hashCode: Int, val layer: Int, val severity: HighlightSeverity, val description: String) { | ||||
| 	constructor(highlighter: RangeHighlighter, info: HighlightInfo) : this(System.identityHashCode(highlighter), highlighter.layer, info.severity, info.description) | ||||
| } | ||||
| @@ -0,0 +1,5 @@ | ||||
| package com.chylex.intellij.inspectionlens.debug | ||||
|  | ||||
| import java.time.Instant | ||||
|  | ||||
| data class LensEvent(val time: Instant, val data: LensEventData) | ||||
| @@ -0,0 +1,7 @@ | ||||
| package com.chylex.intellij.inspectionlens.debug | ||||
|  | ||||
| sealed interface LensEventData { | ||||
| 	data class MarkupModelAfterAdded(val lens: Highlighter) : LensEventData | ||||
| 	data class MarkupModelAttributesChanged(val lens: Highlighter) : LensEventData | ||||
| 	data class MarkupModelBeforeRemoved(val lens: Highlighter) : LensEventData | ||||
| } | ||||
| @@ -0,0 +1,14 @@ | ||||
| package com.chylex.intellij.inspectionlens.debug | ||||
|  | ||||
| import com.intellij.openapi.editor.Editor | ||||
| import java.time.Instant | ||||
|  | ||||
| object LensEventManager { | ||||
| 	val fileNameToEventsMap = mutableMapOf<String, MutableList<LensEvent>>() | ||||
| 	 | ||||
| 	@Synchronized | ||||
| 	fun addEvent(editor: Editor, event: LensEventData) { | ||||
| 		val path = editor.virtualFile?.path ?: return | ||||
| 		fileNameToEventsMap.getOrPut(path, ::mutableListOf).add(LensEvent(Instant.now(), event)) | ||||
| 	} | ||||
| } | ||||
| @@ -0,0 +1,66 @@ | ||||
| package com.chylex.intellij.inspectionlens.editor | ||||
|  | ||||
| import com.chylex.intellij.inspectionlens.InspectionLens | ||||
| import com.intellij.openapi.Disposable | ||||
| import com.intellij.openapi.editor.Editor | ||||
| import com.intellij.openapi.editor.ex.FoldingModelEx | ||||
| import com.intellij.openapi.editor.ex.MarkupModelEx | ||||
| import com.intellij.openapi.editor.impl.DocumentMarkupModel | ||||
| import com.intellij.openapi.util.Disposer | ||||
| import com.intellij.openapi.util.Key | ||||
|  | ||||
| /** | ||||
|  * Manages Inspection Lens features for a single [Editor]. | ||||
|  */ | ||||
| internal class EditorLensFeatures private constructor( | ||||
| 	editor: Editor, | ||||
| 	private val markupModel: MarkupModelEx, | ||||
| 	foldingModel: FoldingModelEx?, | ||||
| 	disposable: Disposable | ||||
| ) { | ||||
| 	private val lensManager = EditorLensManager(editor) | ||||
| 	private val lensManagerDispatcher = EditorLensManagerDispatcher(lensManager) | ||||
| 	private val markupModelListener = LensMarkupModelListener(editor, lensManagerDispatcher) | ||||
| 	 | ||||
| 	init { | ||||
| 		markupModel.addMarkupModelListener(disposable, markupModelListener) | ||||
| 		markupModelListener.showAllValid(markupModel.allHighlighters) | ||||
| 		 | ||||
| 		foldingModel?.addListener(LensFoldingModelListener(lensManager), disposable) | ||||
| 	} | ||||
| 	 | ||||
| 	private fun refresh() { | ||||
| 		markupModelListener.hideAll() | ||||
| 		markupModelListener.showAllValid(markupModel.allHighlighters) | ||||
| 	} | ||||
| 	 | ||||
| 	companion object { | ||||
| 		private val EDITOR_KEY = Key<EditorLensFeatures>(EditorLensFeatures::class.java.name) | ||||
| 		 | ||||
| 		fun install(editor: Editor, disposable: Disposable) { | ||||
| 			if (editor.getUserData(EDITOR_KEY) != null) { | ||||
| 				InspectionLens.LOG.info("Skipped installation to: $editor") | ||||
| 				return | ||||
| 			} | ||||
| 			 | ||||
| 			InspectionLens.LOG.info("Installing to: $editor") | ||||
| 			 | ||||
| 			val markupModel = DocumentMarkupModel.forDocument(editor.document, editor.project, false) as? MarkupModelEx ?: return | ||||
| 			val foldingModel = editor.foldingModel as? FoldingModelEx | ||||
| 			val features = EditorLensFeatures(editor, markupModel, foldingModel, disposable) | ||||
| 			 | ||||
| 			editor.putUserData(EDITOR_KEY, features) | ||||
| 			 | ||||
| 			Disposer.register(disposable) { | ||||
| 				InspectionLens.LOG.info("Installation disposed: $editor", Exception("DISPOSE STACK TRACE")) | ||||
| 				editor.putUserData(EDITOR_KEY, null) | ||||
| 			} | ||||
| 		} | ||||
| 		 | ||||
| 		fun refresh(editor: Editor) { | ||||
| 			val userData = editor.getUserData(EDITOR_KEY) | ||||
| 			InspectionLens.LOG.info("Refreshing: $editor ($userData)") | ||||
| 			userData?.refresh() | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
| @@ -0,0 +1,102 @@ | ||||
| package com.chylex.intellij.inspectionlens.editor | ||||
|  | ||||
| import com.chylex.intellij.inspectionlens.debug.Highlighter | ||||
| import com.chylex.intellij.inspectionlens.editor.lens.EditorLens | ||||
| import com.chylex.intellij.inspectionlens.settings.LensSettingsState | ||||
| import com.intellij.codeInsight.daemon.impl.HighlightInfo | ||||
| 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 val highlighters | ||||
| 		get() = lenses.keys.map { Highlighter(it, HighlightInfo.fromRangeHighlighter(it)!!) } | ||||
| 	 | ||||
| 	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() | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
| @@ -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) | ||||
| 	} | ||||
| } | ||||
| @@ -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 | ||||
| @@ -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() | ||||
| 	} | ||||
| } | ||||
| @@ -0,0 +1,90 @@ | ||||
| package com.chylex.intellij.inspectionlens.editor | ||||
|  | ||||
| import com.chylex.intellij.inspectionlens.InspectionLens | ||||
| import com.chylex.intellij.inspectionlens.debug.Highlighter | ||||
| import com.chylex.intellij.inspectionlens.debug.LensEventData | ||||
| import com.chylex.intellij.inspectionlens.debug.LensEventManager | ||||
| import com.chylex.intellij.inspectionlens.settings.LensSettingsState | ||||
| import com.intellij.codeInsight.daemon.impl.HighlightInfo | ||||
| import com.intellij.openapi.components.service | ||||
| import com.intellij.openapi.editor.Editor | ||||
| 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 editor: Editor, private val lensManagerDispatcher: EditorLensManagerDispatcher) : MarkupModelListener { | ||||
| 	private val settings = service<LensSettingsState>() | ||||
| 	 | ||||
| 	override fun afterAdded(highlighter: RangeHighlighterEx) { | ||||
| 		try { | ||||
| 			getFilteredHighlightInfo(highlighter)?.let { LensEventManager.addEvent(editor, LensEventData.MarkupModelAfterAdded(Highlighter(highlighter, it))) } | ||||
| 			showIfValid(highlighter) | ||||
| 		} catch (e: Exception) { | ||||
| 			InspectionLens.LOG.error("Error showing inspection", e) | ||||
| 		} | ||||
| 	} | ||||
| 	 | ||||
| 	override fun attributesChanged(highlighter: RangeHighlighterEx, renderersChanged: Boolean, fontStyleOrColorChanged: Boolean) { | ||||
| 		try { | ||||
| 			getFilteredHighlightInfo(highlighter)?.let { LensEventManager.addEvent(editor, LensEventData.MarkupModelAttributesChanged(Highlighter(highlighter, it))) } | ||||
| 			showIfValid(highlighter) | ||||
| 		} catch (e: Exception) { | ||||
| 			InspectionLens.LOG.error("Error updating inspection", e) | ||||
| 		} | ||||
| 	} | ||||
| 	 | ||||
| 	override fun beforeRemoved(highlighter: RangeHighlighterEx) { | ||||
| 		try { | ||||
| 			val filteredHighlightInfo = getFilteredHighlightInfo(highlighter) | ||||
| 			if (filteredHighlightInfo != null) { | ||||
| 				LensEventManager.addEvent(editor, LensEventData.MarkupModelBeforeRemoved(Highlighter(highlighter, filteredHighlightInfo))) | ||||
| 				lensManagerDispatcher.hide(highlighter) | ||||
| 			} | ||||
| 		} catch (e: Exception) { | ||||
| 			InspectionLens.LOG.error("Error hiding inspection", e) | ||||
| 		} | ||||
| 	} | ||||
| 	 | ||||
| 	private fun showIfValid(highlighter: RangeHighlighter) { | ||||
| 		runWithHighlighterIfValid(highlighter, lensManagerDispatcher::show, ::showAsynchronously) | ||||
| 	} | ||||
| 	 | ||||
| 	private fun showAsynchronously(highlighterWithInfo: HighlighterWithInfo.Async) { | ||||
| 		highlighterWithInfo.requestDescription { | ||||
| 			if (highlighterWithInfo.highlighter.isValid && highlighterWithInfo.hasDescription) { | ||||
| 				lensManagerDispatcher.show(highlighterWithInfo) | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
| 	 | ||||
| 	fun showAllValid(highlighters: Array<RangeHighlighter>) { | ||||
| 		highlighters.forEach(::showIfValid) | ||||
| 	} | ||||
| 	 | ||||
| 	fun hideAll() { | ||||
| 		lensManagerDispatcher.hideAll() | ||||
| 	} | ||||
| 	 | ||||
| 	private fun getFilteredHighlightInfo(highlighter: RangeHighlighter): HighlightInfo? { | ||||
| 		return HighlightInfo.fromRangeHighlighter(highlighter)?.takeIf { settings.severityFilter.test(it.severity) } | ||||
| 	} | ||||
| 	 | ||||
| 	private inline fun runWithHighlighterIfValid(highlighter: RangeHighlighter, actionForImmediate: (HighlighterWithInfo) -> Unit, actionForAsync: (HighlighterWithInfo.Async) -> Unit) { | ||||
| 		val info = highlighter.takeIf { it.isValid }?.let(::getFilteredHighlightInfo) | ||||
| 		if (info != null) { | ||||
| 			processHighlighterWithInfo(HighlighterWithInfo.from(highlighter, info), actionForImmediate, actionForAsync) | ||||
| 		} | ||||
| 	} | ||||
| 	 | ||||
| 	private inline fun processHighlighterWithInfo(highlighterWithInfo: HighlighterWithInfo, actionForImmediate: (HighlighterWithInfo) -> Unit, actionForAsync: (HighlighterWithInfo.Async) -> Unit) { | ||||
| 		if (highlighterWithInfo is HighlighterWithInfo.Async) { | ||||
| 			actionForAsync(highlighterWithInfo) | ||||
| 		} | ||||
| 		else if (highlighterWithInfo.hasDescription) { | ||||
| 			actionForImmediate(highlighterWithInfo) | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
| @@ -0,0 +1,47 @@ | ||||
| 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) | ||||
| 	} | ||||
| 	 | ||||
| 	override fun toString(): String { | ||||
| 		return "$inlay" | ||||
| 	} | ||||
| 	 | ||||
| 	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) | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
| @@ -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) | ||||
| 		}	 | ||||
| 	} | ||||
| } | ||||
| @@ -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 | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
| @@ -0,0 +1,58 @@ | ||||
| package com.chylex.intellij.inspectionlens.editor.lens | ||||
|  | ||||
| import com.intellij.codeInsight.daemon.impl.IntentionsUI | ||||
| import com.intellij.codeInsight.hint.HintManager | ||||
| import com.intellij.codeInsight.intention.impl.ShowIntentionActionsHandler | ||||
| import com.intellij.lang.LangBundle | ||||
| import com.intellij.openapi.actionSystem.ActionManager | ||||
| import com.intellij.openapi.actionSystem.ActionPlaces | ||||
| import com.intellij.openapi.actionSystem.IdeActions | ||||
| import com.intellij.openapi.actionSystem.ex.ActionUtil | ||||
| import com.intellij.openapi.editor.Editor | ||||
| import com.intellij.openapi.project.Project | ||||
| import com.intellij.psi.PsiDocumentManager | ||||
| import com.intellij.psi.PsiFile | ||||
| import com.intellij.psi.util.PsiUtilBase | ||||
|  | ||||
| internal object IntentionsPopup { | ||||
| 	fun show(editor: Editor) { | ||||
| 		if (!tryShow(editor)) { | ||||
| 			HintManager.getInstance().showInformationHint(editor, LangBundle.message("hint.text.no.context.actions.available.at.this.location")) | ||||
| 		} | ||||
| 	} | ||||
| 	 | ||||
| 	private fun tryShow(editor: Editor): Boolean { | ||||
| 		// If the IDE uses the default Show Intentions action and handler, | ||||
| 		// use the handler directly to bypass additional logic from the action. | ||||
| 		val action = ActionManager.getInstance().getAction(IdeActions.ACTION_SHOW_INTENTION_ACTIONS) | ||||
| 		if (action.javaClass.name === DEFAULT_ACTION_CLASS) { | ||||
| 			return tryShowWithDefaultHandler(editor) | ||||
| 		} | ||||
| 		else { | ||||
| 			ActionUtil.invokeAction(action, editor.component, ActionPlaces.EDITOR_INLAY, null, null) | ||||
| 			return true | ||||
| 		} | ||||
| 	} | ||||
| 	 | ||||
| 	private fun tryShowWithDefaultHandler(editor: Editor): Boolean { | ||||
| 		val project = editor.project ?: return false | ||||
| 		val file = PsiUtilBase.getPsiFileInEditor(editor, project) ?: return false | ||||
| 		 | ||||
| 		PsiDocumentManager.getInstance(project).commitAllDocuments() | ||||
| 		IntentionsUI.getInstance(project).hide() | ||||
| 		 | ||||
| 		HANDLER.showIntentionHint(project, editor, file, showFeedbackOnEmptyMenu = true) | ||||
| 		return true | ||||
| 	} | ||||
| 	 | ||||
| 	/** | ||||
| 	 * New IDEA versions mark this class as internal, so the plugin verifier flags references to it as errors. | ||||
| 	 */ | ||||
| 	const val DEFAULT_ACTION_CLASS = "com.intellij.codeInsight.intention.actions.ShowIntentionActionsAction" | ||||
| 	 | ||||
| 	private val HANDLER = object : ShowIntentionActionsHandler() { | ||||
| 		public override fun showIntentionHint(project: Project, editor: Editor, file: PsiFile, showFeedbackOnEmptyMenu: Boolean) { | ||||
| 			super.showIntentionHint(project, editor, file, showFeedbackOnEmptyMenu) | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
| @@ -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(editor) | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
| 	 | ||||
| 	private fun isHoveringText(): Boolean { | ||||
| 		val bounds = inlay.bounds ?: return false | ||||
| 		val translatedPoint = MouseInfo.getPointerInfo().location.apply { | ||||
| 			SwingUtilities.convertPointFromScreen(this, inlay.editor.contentComponent) | ||||
| 			translate(-bounds.x, -bounds.y) | ||||
| 		} | ||||
| 		 | ||||
| 		return isHoveringText(translatedPoint) | ||||
| 	} | ||||
| 	 | ||||
| 	private fun isHoveringText(point: Point): Boolean { | ||||
| 		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) | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
| @@ -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) | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
| @@ -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) | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
| @@ -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 | ||||
| 	} | ||||
| } | ||||
| @@ -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() | ||||
| 	} | ||||
| } | ||||
| @@ -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") | ||||
| } | ||||
| @@ -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) | ||||
| 	} | ||||
| } | ||||
| @@ -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) | ||||
| } | ||||
| @@ -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) } | ||||
| 	} | ||||
| } | ||||
| @@ -0,0 +1,5 @@ | ||||
| <idea-plugin> | ||||
|   <extensions defaultExtensionNs="com.intellij"> | ||||
|     <postStartupActivity implementation="com.chylex.intellij.inspectionlens.compatibility.GrazieSupport" /> | ||||
|   </extensions> | ||||
| </idea-plugin> | ||||
| @@ -4,19 +4,81 @@ | ||||
|   <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.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,9 +86,14 @@ | ||||
|   ]]></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> | ||||
|    | ||||
|   <applicationListeners> | ||||
| @@ -34,6 +101,6 @@ | ||||
|   </applicationListeners> | ||||
|    | ||||
|   <projectListeners> | ||||
|     <listener class="com.chylex.intellij.inspectionlens.LensFileEditorListener" topic="com.intellij.openapi.fileEditor.FileEditorManagerListener" /> | ||||
|     <listener class="com.chylex.intellij.inspectionlens.InspectionLensFileOpenedListener" topic="com.intellij.openapi.fileEditor.FileOpenedSyncListener" /> | ||||
|   </projectListeners> | ||||
| </idea-plugin> | ||||
|   | ||||
							
								
								
									
										12
									
								
								src/main/resources/META-INF/pluginIcon.svg
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										12
									
								
								src/main/resources/META-INF/pluginIcon.svg
									
									
									
									
									
										Normal 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 | 
| @@ -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)) | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
| @@ -0,0 +1,12 @@ | ||||
| package com.chylex.intellij.inspectionlens.editor.lens | ||||
|  | ||||
| import com.intellij.codeInsight.intention.actions.ShowIntentionActionsAction | ||||
| import org.junit.jupiter.api.Assertions.assertEquals | ||||
| import org.junit.jupiter.api.Test | ||||
|  | ||||
| class IntentionsPopupTest { | ||||
| 	@Test | ||||
| 	fun showIntentionActionsActionClassHasNotChanged() { | ||||
| 		assertEquals(IntentionsPopup.DEFAULT_ACTION_CLASS, ShowIntentionActionsAction::class.java.name) | ||||
| 	} | ||||
| } | ||||
		Reference in New Issue
	
	Block a user