mirror of
				https://github.com/chylex/IntelliJ-Inspection-Lens.git
				synced 2025-11-04 02:40:12 +01:00 
			
		
		
		
	Compare commits
	
		
			1 Commits
		
	
	
		
			main
			...
			test-snaps
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 
						
						
							
						
						ed0b37b86c
	
				 | 
					
					
						
@@ -6,7 +6,7 @@ plugins {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
group = "com.chylex.intellij.inspectionlens"
 | 
			
		||||
version = "1.6.0"
 | 
			
		||||
version = "1.5.1"
 | 
			
		||||
 | 
			
		||||
repositories {
 | 
			
		||||
	mavenCentral()
 | 
			
		||||
@@ -18,8 +18,11 @@ repositories {
 | 
			
		||||
 | 
			
		||||
dependencies {
 | 
			
		||||
	intellijPlatform {
 | 
			
		||||
		intellijIdeaUltimate("2024.2")
 | 
			
		||||
		intellijIdeaUltimate("2023.3.3")
 | 
			
		||||
		bundledPlugin("tanvd.grazi")
 | 
			
		||||
		
 | 
			
		||||
		// https://plugins.jetbrains.com/plugin/12175-grazie-lite/versions
 | 
			
		||||
		// plugin("tanvd.grazi", "233.13135.14")
 | 
			
		||||
	}
 | 
			
		||||
	
 | 
			
		||||
	testImplementation("org.junit.jupiter:junit-jupiter:5.9.2")
 | 
			
		||||
@@ -28,19 +31,10 @@ dependencies {
 | 
			
		||||
intellijPlatform {
 | 
			
		||||
	pluginConfiguration {
 | 
			
		||||
		ideaVersion {
 | 
			
		||||
			sinceBuild.set("242")
 | 
			
		||||
			sinceBuild.set("233.11361.10")
 | 
			
		||||
			untilBuild.set(provider { null })
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	
 | 
			
		||||
	pluginVerification {
 | 
			
		||||
		freeArgs.add("-mute")
 | 
			
		||||
		freeArgs.add("TemplateWordInPluginId")
 | 
			
		||||
		
 | 
			
		||||
		ides {
 | 
			
		||||
			recommended()
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
kotlin {
 | 
			
		||||
@@ -48,12 +42,16 @@ kotlin {
 | 
			
		||||
	
 | 
			
		||||
	compilerOptions {
 | 
			
		||||
		freeCompilerArgs = listOf(
 | 
			
		||||
			"-X" + "jvm-default=all",
 | 
			
		||||
			"-X" + "lambdas=indy"
 | 
			
		||||
			"-X" + "jvm-default=all"
 | 
			
		||||
		)
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
tasks.test {
 | 
			
		||||
tasks.withType<Test>().configureEach {
 | 
			
		||||
	useJUnitPlatform()
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
val testSnapshot by intellijPlatformTesting.testIde.registering {
 | 
			
		||||
	version = "LATEST-EAP-SNAPSHOT"
 | 
			
		||||
	useInstaller = false
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -2,7 +2,7 @@ rootProject.name = "InspectionLens"
 | 
			
		||||
 | 
			
		||||
pluginManagement {
 | 
			
		||||
	plugins {
 | 
			
		||||
		kotlin("jvm") version "1.9.24" // https://plugins.jetbrains.com/docs/intellij/using-kotlin.html#bundled-stdlib-versions
 | 
			
		||||
		id("org.jetbrains.intellij.platform") version "2.6.0" // https://github.com/JetBrains/intellij-platform-gradle-plugin/releases	
 | 
			
		||||
		kotlin("jvm") version "1.9.21"
 | 
			
		||||
		id("org.jetbrains.intellij.platform") version "2.2.1"
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -2,11 +2,10 @@ package com.chylex.intellij.inspectionlens
 | 
			
		||||
 | 
			
		||||
import com.chylex.intellij.inspectionlens.editor.EditorLensFeatures
 | 
			
		||||
import com.intellij.openapi.application.ApplicationManager
 | 
			
		||||
import com.intellij.openapi.diagnostic.logger
 | 
			
		||||
import com.intellij.openapi.components.service
 | 
			
		||||
import com.intellij.openapi.fileEditor.FileEditorManager
 | 
			
		||||
import com.intellij.openapi.fileEditor.TextEditor
 | 
			
		||||
import com.intellij.openapi.project.ProjectManager
 | 
			
		||||
import java.util.concurrent.atomic.AtomicBoolean
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Handles installation and uninstallation of plugin features in editors.
 | 
			
		||||
@@ -14,33 +13,27 @@ import java.util.concurrent.atomic.AtomicBoolean
 | 
			
		||||
internal object InspectionLens {
 | 
			
		||||
	const val PLUGIN_ID = "com.chylex.intellij.inspectionlens"
 | 
			
		||||
	
 | 
			
		||||
	val LOG = logger<InspectionLens>()
 | 
			
		||||
	
 | 
			
		||||
	var SHOW_LENSES = true
 | 
			
		||||
		set(value) {
 | 
			
		||||
			field = value
 | 
			
		||||
			scheduleRefresh()
 | 
			
		||||
		}
 | 
			
		||||
	
 | 
			
		||||
	/**
 | 
			
		||||
	 * Installs lenses into [editor].
 | 
			
		||||
	 */
 | 
			
		||||
	fun install(editor: TextEditor) {
 | 
			
		||||
		EditorLensFeatures.install(editor)
 | 
			
		||||
		EditorLensFeatures.install(editor.editor, service<InspectionLensPluginDisposableService>().intersect(editor))
 | 
			
		||||
	}
 | 
			
		||||
	
 | 
			
		||||
	/**
 | 
			
		||||
	 * Installs lenses into all open editors.
 | 
			
		||||
	 */
 | 
			
		||||
	fun install() {
 | 
			
		||||
		forEachOpenEditor(EditorLensFeatures::install)
 | 
			
		||||
		forEachOpenEditor(::install)
 | 
			
		||||
	}
 | 
			
		||||
	
 | 
			
		||||
	/**
 | 
			
		||||
	 * Refreshes lenses in all open editors.
 | 
			
		||||
	 */
 | 
			
		||||
	private fun refresh() {
 | 
			
		||||
		forEachOpenEditor(EditorLensFeatures::refresh)
 | 
			
		||||
		forEachOpenEditor {
 | 
			
		||||
			EditorLensFeatures.refresh(it.editor)
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	
 | 
			
		||||
	/**
 | 
			
		||||
@@ -56,31 +49,31 @@ internal object InspectionLens {
 | 
			
		||||
	private inline fun forEachOpenEditor(action: (TextEditor) -> Unit) {
 | 
			
		||||
		val projectManager = ProjectManager.getInstanceIfCreated() ?: return
 | 
			
		||||
		
 | 
			
		||||
		for (project in projectManager.openProjects) {
 | 
			
		||||
			if (project.isDisposed) {
 | 
			
		||||
				continue
 | 
			
		||||
			}
 | 
			
		||||
			
 | 
			
		||||
			for (editor in FileEditorManager.getInstance(project).allEditors) {
 | 
			
		||||
				if (editor is TextEditor) {
 | 
			
		||||
					action(editor)
 | 
			
		||||
				}
 | 
			
		||||
		for (project in projectManager.openProjects.filterNot { it.isDisposed }) {
 | 
			
		||||
			for (editor in FileEditorManager.getInstance(project).allEditors.filterIsInstance<TextEditor>()) {
 | 
			
		||||
				action(editor)
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	
 | 
			
		||||
	private object Refresh : Runnable {
 | 
			
		||||
		private val needsRefresh = AtomicBoolean(false)
 | 
			
		||||
	private object Refresh {
 | 
			
		||||
		private var needsRefresh = false
 | 
			
		||||
		
 | 
			
		||||
		fun schedule() {
 | 
			
		||||
			if (needsRefresh.compareAndSet(false, true)) {
 | 
			
		||||
				ApplicationManager.getApplication().invokeLater(this)
 | 
			
		||||
			synchronized(this) {
 | 
			
		||||
				if (!needsRefresh) {
 | 
			
		||||
					needsRefresh = true
 | 
			
		||||
					ApplicationManager.getApplication().invokeLater(this::run)
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
		
 | 
			
		||||
		override fun run() {
 | 
			
		||||
			if (needsRefresh.compareAndSet(true, false)) {
 | 
			
		||||
				refresh()
 | 
			
		||||
		private fun run() {
 | 
			
		||||
			synchronized(this) {
 | 
			
		||||
				if (needsRefresh) {
 | 
			
		||||
					needsRefresh = false
 | 
			
		||||
					refresh()
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 
 | 
			
		||||
@@ -1,22 +0,0 @@
 | 
			
		||||
package com.chylex.intellij.inspectionlens
 | 
			
		||||
 | 
			
		||||
import com.intellij.openapi.fileEditor.FileEditorManager
 | 
			
		||||
import com.intellij.openapi.fileEditor.FileEditorManagerListener
 | 
			
		||||
import com.intellij.openapi.fileEditor.FileOpenedSyncListener
 | 
			
		||||
import com.intellij.openapi.fileEditor.TextEditor
 | 
			
		||||
import com.intellij.openapi.vfs.VirtualFile
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Installs [InspectionLens] in opened editors.
 | 
			
		||||
 *
 | 
			
		||||
 * Used instead of [FileOpenedSyncListener], because [FileOpenedSyncListener] randomly fails to register during IDE startup.
 | 
			
		||||
 */
 | 
			
		||||
class InspectionLensFileEditorManagerListener : FileEditorManagerListener {
 | 
			
		||||
	override fun fileOpened(source: FileEditorManager, file: VirtualFile) {
 | 
			
		||||
		for (editor in source.getEditors(file)) {
 | 
			
		||||
			if (editor is TextEditor) {
 | 
			
		||||
				InspectionLens.install(editor)
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,21 @@
 | 
			
		||||
package com.chylex.intellij.inspectionlens
 | 
			
		||||
 | 
			
		||||
import com.intellij.openapi.fileEditor.FileEditorManager
 | 
			
		||||
import com.intellij.openapi.fileEditor.FileOpenedSyncListener
 | 
			
		||||
import com.intellij.openapi.fileEditor.TextEditor
 | 
			
		||||
import com.intellij.openapi.fileEditor.ex.FileEditorWithProvider
 | 
			
		||||
import com.intellij.openapi.vfs.VirtualFile
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Installs [InspectionLens] in newly opened editors.
 | 
			
		||||
 */
 | 
			
		||||
class InspectionLensFileOpenedListener : FileOpenedSyncListener {
 | 
			
		||||
	override fun fileOpenedSync(source: FileEditorManager, file: VirtualFile, editorsWithProviders: List<FileEditorWithProvider>) {
 | 
			
		||||
		for (editorWrapper in editorsWithProviders) {
 | 
			
		||||
			val fileEditor = editorWrapper.fileEditor
 | 
			
		||||
			if (fileEditor is TextEditor) {
 | 
			
		||||
				InspectionLens.install(fileEditor)
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
@@ -1,25 +0,0 @@
 | 
			
		||||
package com.chylex.intellij.inspectionlens.actions
 | 
			
		||||
 | 
			
		||||
import com.chylex.intellij.inspectionlens.InspectionLens
 | 
			
		||||
import com.intellij.openapi.actionSystem.ActionUpdateThread
 | 
			
		||||
import com.intellij.openapi.actionSystem.AnActionEvent
 | 
			
		||||
import com.intellij.openapi.actionSystem.KeepPopupOnPerform
 | 
			
		||||
import com.intellij.openapi.project.DumbAwareToggleAction
 | 
			
		||||
 | 
			
		||||
class ToggleLensVisibilityAction : DumbAwareToggleAction() {
 | 
			
		||||
	init {
 | 
			
		||||
		templatePresentation.keepPopupOnPerform = KeepPopupOnPerform.IfRequested
 | 
			
		||||
	}
 | 
			
		||||
	
 | 
			
		||||
	override fun getActionUpdateThread(): ActionUpdateThread {
 | 
			
		||||
		return ActionUpdateThread.BGT
 | 
			
		||||
	}
 | 
			
		||||
	
 | 
			
		||||
	override fun isSelected(e: AnActionEvent): Boolean {
 | 
			
		||||
		return InspectionLens.SHOW_LENSES
 | 
			
		||||
	}
 | 
			
		||||
	
 | 
			
		||||
	override fun setSelected(e: AnActionEvent, state: Boolean) {
 | 
			
		||||
		InspectionLens.SHOW_LENSES = state
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
@@ -1,13 +1,10 @@
 | 
			
		||||
package com.chylex.intellij.inspectionlens.editor
 | 
			
		||||
 | 
			
		||||
import com.chylex.intellij.inspectionlens.InspectionLensPluginDisposableService
 | 
			
		||||
import com.intellij.openapi.Disposable
 | 
			
		||||
import com.intellij.openapi.components.service
 | 
			
		||||
import com.intellij.openapi.editor.Editor
 | 
			
		||||
import com.intellij.openapi.editor.ex.FoldingModelEx
 | 
			
		||||
import com.intellij.openapi.editor.ex.MarkupModelEx
 | 
			
		||||
import com.intellij.openapi.editor.impl.DocumentMarkupModel
 | 
			
		||||
import com.intellij.openapi.fileEditor.TextEditor
 | 
			
		||||
import com.intellij.openapi.util.Disposer
 | 
			
		||||
import com.intellij.openapi.util.Key
 | 
			
		||||
 | 
			
		||||
@@ -39,24 +36,21 @@ internal class EditorLensFeatures private constructor(
 | 
			
		||||
	companion object {
 | 
			
		||||
		private val EDITOR_KEY = Key<EditorLensFeatures>(EditorLensFeatures::class.java.name)
 | 
			
		||||
		
 | 
			
		||||
		fun install(owner: TextEditor) {
 | 
			
		||||
			val editor = owner.editor
 | 
			
		||||
		fun install(editor: Editor, disposable: Disposable) {
 | 
			
		||||
			if (editor.getUserData(EDITOR_KEY) != null) {
 | 
			
		||||
				return
 | 
			
		||||
			}
 | 
			
		||||
			
 | 
			
		||||
			val markupModel = DocumentMarkupModel.forDocument(editor.document, editor.project, false) as? MarkupModelEx ?: return
 | 
			
		||||
			val foldingModel = editor.foldingModel as? FoldingModelEx
 | 
			
		||||
			
 | 
			
		||||
			val disposable = service<InspectionLensPluginDisposableService>().intersect(owner)
 | 
			
		||||
			val features = EditorLensFeatures(editor, markupModel, foldingModel, disposable)
 | 
			
		||||
			
 | 
			
		||||
			editor.putUserData(EDITOR_KEY, features)
 | 
			
		||||
			Disposer.register(disposable) { editor.putUserData(EDITOR_KEY, null) }
 | 
			
		||||
		}
 | 
			
		||||
		
 | 
			
		||||
		fun refresh(owner: TextEditor) {
 | 
			
		||||
			owner.editor.getUserData(EDITOR_KEY)?.refresh()
 | 
			
		||||
		fun refresh(editor: Editor) {
 | 
			
		||||
			editor.getUserData(EDITOR_KEY)?.refresh()
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -15,11 +15,8 @@ internal class EditorLensManager(private val editor: Editor) {
 | 
			
		||||
	private val settings = service<LensSettingsState>()
 | 
			
		||||
	
 | 
			
		||||
	private fun show(highlighterWithInfo: HighlighterWithInfo) {
 | 
			
		||||
		if (editor.isDisposed) {
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
		
 | 
			
		||||
		val (highlighter, info) = highlighterWithInfo
 | 
			
		||||
		
 | 
			
		||||
		if (!highlighter.isValid) {
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
 
 | 
			
		||||
@@ -1,9 +1,7 @@
 | 
			
		||||
package com.chylex.intellij.inspectionlens.editor
 | 
			
		||||
 | 
			
		||||
import com.chylex.intellij.inspectionlens.InspectionLens
 | 
			
		||||
import com.chylex.intellij.inspectionlens.settings.LensSettingsState
 | 
			
		||||
import com.intellij.codeInsight.daemon.impl.HighlightInfo
 | 
			
		||||
import com.intellij.codeInsight.daemon.impl.UpdateHighlightersUtil.isFileLevelOrGutterAnnotation
 | 
			
		||||
import com.intellij.openapi.components.service
 | 
			
		||||
import com.intellij.openapi.editor.ex.RangeHighlighterEx
 | 
			
		||||
import com.intellij.openapi.editor.impl.event.MarkupModelListener
 | 
			
		||||
@@ -23,37 +21,14 @@ internal class LensMarkupModelListener(private val lensManagerDispatcher: Editor
 | 
			
		||||
		showIfValid(highlighter)
 | 
			
		||||
	}
 | 
			
		||||
	
 | 
			
		||||
	fun showAllValid(highlighters: Array<RangeHighlighter>) {
 | 
			
		||||
		if (InspectionLens.SHOW_LENSES) {
 | 
			
		||||
			highlighters.forEach(::showIfValid)
 | 
			
		||||
	override fun beforeRemoved(highlighter: RangeHighlighterEx) {
 | 
			
		||||
		if (getFilteredHighlightInfo(highlighter) != null) {
 | 
			
		||||
			lensManagerDispatcher.hide(highlighter)
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	
 | 
			
		||||
	private fun showIfValid(highlighter: RangeHighlighter) {
 | 
			
		||||
		if (!InspectionLens.SHOW_LENSES) {
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
		
 | 
			
		||||
		val info = highlighter.takeIf { it.isValid }?.let(::getFilteredHighlightInfo)
 | 
			
		||||
		if (info == null || isFileLevelOrGutterAnnotation(info)) {
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
		
 | 
			
		||||
		val highlighterWithInfo = HighlighterWithInfo.from(highlighter, info)
 | 
			
		||||
		processHighlighterWithInfo(highlighterWithInfo, lensManagerDispatcher::show, ::showAsynchronously)
 | 
			
		||||
	}
 | 
			
		||||
	
 | 
			
		||||
	private fun getFilteredHighlightInfo(highlighter: RangeHighlighter): HighlightInfo? {
 | 
			
		||||
		return HighlightInfo.fromRangeHighlighter(highlighter)?.takeIf { settings.severityFilter.test(it.severity) }
 | 
			
		||||
	}
 | 
			
		||||
	
 | 
			
		||||
	private inline fun processHighlighterWithInfo(highlighterWithInfo: HighlighterWithInfo, actionForImmediate: (HighlighterWithInfo) -> Unit, actionForAsync: (HighlighterWithInfo.Async) -> Unit) {
 | 
			
		||||
		if (highlighterWithInfo is HighlighterWithInfo.Async) {
 | 
			
		||||
			actionForAsync(highlighterWithInfo)
 | 
			
		||||
		}
 | 
			
		||||
		else if (highlighterWithInfo.hasDescription) {
 | 
			
		||||
			actionForImmediate(highlighterWithInfo)
 | 
			
		||||
		}
 | 
			
		||||
		runWithHighlighterIfValid(highlighter, lensManagerDispatcher::show, ::showAsynchronously)
 | 
			
		||||
	}
 | 
			
		||||
	
 | 
			
		||||
	private fun showAsynchronously(highlighterWithInfo: HighlighterWithInfo.Async) {
 | 
			
		||||
@@ -64,13 +39,31 @@ internal class LensMarkupModelListener(private val lensManagerDispatcher: Editor
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	
 | 
			
		||||
	override fun beforeRemoved(highlighter: RangeHighlighterEx) {
 | 
			
		||||
		if (getFilteredHighlightInfo(highlighter) != null) {
 | 
			
		||||
			lensManagerDispatcher.hide(highlighter)
 | 
			
		||||
		}
 | 
			
		||||
	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)
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -1,132 +1,54 @@
 | 
			
		||||
package com.chylex.intellij.inspectionlens.editor.lens
 | 
			
		||||
 | 
			
		||||
import com.chylex.intellij.inspectionlens.InspectionLens
 | 
			
		||||
import com.intellij.codeInsight.daemon.impl.HighlightInfo
 | 
			
		||||
import com.intellij.codeInsight.daemon.impl.HighlightInfo.IntentionActionDescriptor
 | 
			
		||||
import com.intellij.codeInsight.daemon.impl.IntentionsUI
 | 
			
		||||
import com.intellij.codeInsight.daemon.impl.ShowIntentionsPass.IntentionsInfo
 | 
			
		||||
import com.intellij.codeInsight.hint.HintManager
 | 
			
		||||
import com.intellij.codeInsight.intention.impl.CachedIntentions
 | 
			
		||||
import com.intellij.codeInsight.intention.impl.IntentionHintComponent
 | 
			
		||||
import com.intellij.codeInsight.intention.actions.ShowIntentionActionsAction
 | 
			
		||||
import com.intellij.codeInsight.intention.impl.ShowIntentionActionsHandler
 | 
			
		||||
import com.intellij.lang.LangBundle
 | 
			
		||||
import com.intellij.lang.annotation.HighlightSeverity
 | 
			
		||||
import com.intellij.openapi.actionSystem.ActionManager
 | 
			
		||||
import com.intellij.openapi.actionSystem.ActionPlaces
 | 
			
		||||
import com.intellij.openapi.actionSystem.IdeActions
 | 
			
		||||
import com.intellij.openapi.actionSystem.ex.ActionUtil
 | 
			
		||||
import com.intellij.openapi.application.ModalityState
 | 
			
		||||
import com.intellij.openapi.application.ReadAction
 | 
			
		||||
import com.intellij.openapi.editor.Editor
 | 
			
		||||
import com.intellij.openapi.editor.Inlay
 | 
			
		||||
import com.intellij.openapi.project.DumbService
 | 
			
		||||
import com.intellij.openapi.project.Project
 | 
			
		||||
import com.intellij.psi.PsiDocumentManager
 | 
			
		||||
import com.intellij.psi.PsiFile
 | 
			
		||||
import com.intellij.psi.util.PsiUtilBase
 | 
			
		||||
import com.intellij.ui.awt.RelativePoint
 | 
			
		||||
import com.intellij.util.concurrency.AppExecutorUtil
 | 
			
		||||
import java.lang.reflect.Method
 | 
			
		||||
 | 
			
		||||
internal object IntentionsPopup {
 | 
			
		||||
	private const val INTENTION_SOURCE_CLASS_NAME = "com.intellij.codeInsight.intention.IntentionSource"
 | 
			
		||||
	
 | 
			
		||||
	private val showPopupMethod: ShowPopupMethod? = try {
 | 
			
		||||
		val method = IntentionHintComponent::class.java.declaredMethods.first { method ->
 | 
			
		||||
			val parameterTypes = method.parameterTypes
 | 
			
		||||
			
 | 
			
		||||
			method.name == "showPopup" &&
 | 
			
		||||
			parameterTypes.size in 1..2 &&
 | 
			
		||||
			parameterTypes[0] === RelativePoint::class.java &&
 | 
			
		||||
			parameterTypes.getOrNull(1).let { p -> p == null || p.name == INTENTION_SOURCE_CLASS_NAME }
 | 
			
		||||
		}
 | 
			
		||||
		
 | 
			
		||||
		method.isAccessible = true
 | 
			
		||||
		
 | 
			
		||||
		@Suppress("UNCHECKED_CAST")
 | 
			
		||||
		val args: Array<Any?> = if (method.parameterCount == 1)
 | 
			
		||||
			arrayOf(null)
 | 
			
		||||
		else
 | 
			
		||||
			arrayOf(null, (Class.forName(INTENTION_SOURCE_CLASS_NAME) as Class<Enum<*>>).enumConstants.first { it.name == "OTHER" })
 | 
			
		||||
		
 | 
			
		||||
		ShowPopupMethod(method, args)
 | 
			
		||||
	} catch (t: Throwable) {
 | 
			
		||||
		InspectionLens.LOG.warn("Could not initialize intention popup", t)
 | 
			
		||||
		null
 | 
			
		||||
	}
 | 
			
		||||
	
 | 
			
		||||
	private class ShowPopupMethod(private val method: Method, private val args: Array<Any?>) {
 | 
			
		||||
		operator fun invoke(component: IntentionHintComponent) {
 | 
			
		||||
			method.invoke(component, *args)
 | 
			
		||||
	fun show(editor: Editor) {
 | 
			
		||||
		if (!tryShow(editor)) {
 | 
			
		||||
			HintManager.getInstance().showInformationHint(editor, LangBundle.message("hint.text.no.context.actions.available.at.this.location"))
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	
 | 
			
		||||
	fun show(highlightInfo: HighlightInfo, inlay: Inlay<*>) {
 | 
			
		||||
		if (!tryShow(highlightInfo, inlay)) {
 | 
			
		||||
			showNoActionsAvailable(inlay.editor)
 | 
			
		||||
	private fun tryShow(editor: Editor): Boolean {
 | 
			
		||||
		// If the IDE uses the default Show Intentions action and handler,
 | 
			
		||||
		// use the handler directly to bypass additional logic from the action.
 | 
			
		||||
		val action = ActionManager.getInstance().getAction(IdeActions.ACTION_SHOW_INTENTION_ACTIONS)
 | 
			
		||||
		if (action.javaClass === ShowIntentionActionsAction::class.java) {
 | 
			
		||||
			return tryShowWithDefaultHandler(editor)
 | 
			
		||||
		}
 | 
			
		||||
		else {
 | 
			
		||||
			ActionUtil.invokeAction(action, editor.component, ActionPlaces.EDITOR_INLAY, null, null)
 | 
			
		||||
			return true
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	
 | 
			
		||||
	private fun tryShow(highlightInfo: HighlightInfo, inlay: Inlay<*>): Boolean {
 | 
			
		||||
		val editor = inlay.editor
 | 
			
		||||
	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()
 | 
			
		||||
		
 | 
			
		||||
		ReadAction
 | 
			
		||||
			.nonBlocking<IntentionsInfo> { collectIntentions(editor, project, file, highlightInfo, inlay.offset) }
 | 
			
		||||
			.finishOnUiThread(ModalityState.current()) { tryShowPopup(project, file, editor, it) }
 | 
			
		||||
			.submit(AppExecutorUtil.getAppExecutorService())
 | 
			
		||||
		
 | 
			
		||||
		HANDLER.showIntentionHint(project, editor, file, showFeedbackOnEmptyMenu = true)
 | 
			
		||||
		return true
 | 
			
		||||
	}
 | 
			
		||||
	
 | 
			
		||||
	private fun collectIntentions(editor: Editor, project: Project, file: PsiFile, info: HighlightInfo, offset: Int): IntentionsInfo {
 | 
			
		||||
		val intentions = mutableListOf<IntentionActionDescriptor>()
 | 
			
		||||
		
 | 
			
		||||
		info.findRegisteredQuickFix { descriptor, _ ->
 | 
			
		||||
			if (DumbService.getInstance(project).isUsableInCurrentContext(descriptor.action) && ShowIntentionActionsHandler.availableFor(file, editor, offset, descriptor.action)) {
 | 
			
		||||
				intentions.add(descriptor)
 | 
			
		||||
			}
 | 
			
		||||
			null
 | 
			
		||||
	private val HANDLER = object : ShowIntentionActionsHandler() {
 | 
			
		||||
		public override fun showIntentionHint(project: Project, editor: Editor, file: PsiFile, showFeedbackOnEmptyMenu: Boolean) {
 | 
			
		||||
			super.showIntentionHint(project, editor, file, showFeedbackOnEmptyMenu)
 | 
			
		||||
		}
 | 
			
		||||
		
 | 
			
		||||
		return IntentionsInfo().also {
 | 
			
		||||
			it.offset = offset
 | 
			
		||||
			
 | 
			
		||||
			if (info.severity === HighlightSeverity.ERROR) {
 | 
			
		||||
				it.errorFixesToShow.addAll(intentions)
 | 
			
		||||
			}
 | 
			
		||||
			else {
 | 
			
		||||
				it.inspectionFixesToShow.addAll(intentions)
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	
 | 
			
		||||
	private fun tryShowPopup(project: Project, file: PsiFile, editor: Editor, intentions: IntentionsInfo) {
 | 
			
		||||
		try {
 | 
			
		||||
			showPopup(project, file, editor, intentions)
 | 
			
		||||
		} catch (t: Throwable) {
 | 
			
		||||
			InspectionLens.LOG.error("Could not show intention popup", t)
 | 
			
		||||
			showNoActionsAvailable(editor)
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	
 | 
			
		||||
	private fun showPopup(project: Project, file: PsiFile, editor: Editor, intentions: IntentionsInfo) {
 | 
			
		||||
		if (intentions.isEmpty || showPopupMethod == null) {
 | 
			
		||||
			val showIntentionsAction = ActionManager.getInstance().getAction(IdeActions.ACTION_SHOW_INTENTION_ACTIONS)
 | 
			
		||||
			ActionUtil.invokeAction(showIntentionsAction, editor.component, ActionPlaces.EDITOR_INLAY, null, null)
 | 
			
		||||
		}
 | 
			
		||||
		else {
 | 
			
		||||
			val cachedIntentions = CachedIntentions.create(project, file, editor, intentions)
 | 
			
		||||
			val hintComponent = IntentionHintComponent.showIntentionHint(project, file, editor, false, cachedIntentions)
 | 
			
		||||
			showPopupMethod.invoke(hintComponent)
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	
 | 
			
		||||
	private fun showNoActionsAvailable(editor: Editor) {
 | 
			
		||||
		HintManager.getInstance().showInformationHint(editor, LangBundle.message("hint.text.no.context.actions.available.at.this.location"))
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -44,7 +44,7 @@ class LensRenderer(private var info: HighlightInfo, private val settings: LensSe
 | 
			
		||||
	
 | 
			
		||||
	fun setPropertiesFrom(info: HighlightInfo) {
 | 
			
		||||
		this.info = info
 | 
			
		||||
		val description = getValidDescriptionText(info.description, settings.maxDescriptionLength)
 | 
			
		||||
		val description = getValidDescriptionText(info.description)
 | 
			
		||||
		
 | 
			
		||||
		text = description
 | 
			
		||||
		attributes = LensSeverity.from(info.severity).textAttributes
 | 
			
		||||
@@ -125,7 +125,7 @@ class LensRenderer(private var info: HighlightInfo, private val settings: LensSe
 | 
			
		||||
			moveToOffset(editor, info.actualStartOffset)
 | 
			
		||||
			
 | 
			
		||||
			if ((event.button == MouseEvent.BUTTON1) xor (hoverMode != LensHoverMode.DEFAULT)) {
 | 
			
		||||
				IntentionsPopup.show(info, inlay)
 | 
			
		||||
				IntentionsPopup.show(editor)
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
@@ -155,13 +155,15 @@ class LensRenderer(private var info: HighlightInfo, private val settings: LensSe
 | 
			
		||||
		private const val HOVER_HORIZONTAL_PADDING = TEXT_HORIZONTAL_PADDING - 2
 | 
			
		||||
		private const val UNDERLINE_WIDTH_REDUCTION = (TEXT_HORIZONTAL_PADDING * 2) - 1
 | 
			
		||||
		
 | 
			
		||||
		private const val MAX_DESCRIPTION_LENGTH = 120
 | 
			
		||||
		
 | 
			
		||||
		/**
 | 
			
		||||
		 * Kotlin compiler inspections have an `[UPPERCASE_TAG]` at the beginning.
 | 
			
		||||
		 */
 | 
			
		||||
		private val UPPERCASE_TAG_REGEX = Pattern.compile("^\\[[A-Z_]+] ")
 | 
			
		||||
		
 | 
			
		||||
		private fun getValidDescriptionText(text: String?, maxLength: Int): String {
 | 
			
		||||
			return if (text.isNullOrBlank()) " " else addEllipsisOrMissingPeriod(unescapeHtmlEntities(stripUppercaseTag(text)), maxLength)
 | 
			
		||||
		private fun getValidDescriptionText(text: String?): String {
 | 
			
		||||
			return if (text.isNullOrBlank()) " " else addEllipsisOrMissingPeriod(unescapeHtmlEntities(stripUppercaseTag(text)))
 | 
			
		||||
		}
 | 
			
		||||
		
 | 
			
		||||
		private fun stripUppercaseTag(text: String): String {
 | 
			
		||||
@@ -179,11 +181,11 @@ class LensRenderer(private var info: HighlightInfo, private val settings: LensSe
 | 
			
		||||
			return if (text.contains('&')) StringUtil.unescapeXmlEntities(text) else text
 | 
			
		||||
		}
 | 
			
		||||
		
 | 
			
		||||
		private fun addEllipsisOrMissingPeriod(text: String, maxLength: Int): String {
 | 
			
		||||
		private fun addEllipsisOrMissingPeriod(text: String): String {
 | 
			
		||||
			return when {
 | 
			
		||||
				text.length > maxLength -> text.take(maxLength).trimEnd { it.isWhitespace() || it == '.' } + "…"
 | 
			
		||||
				!text.endsWith('.')     -> "$text."
 | 
			
		||||
				else                    -> text
 | 
			
		||||
				text.length > MAX_DESCRIPTION_LENGTH -> text.take(MAX_DESCRIPTION_LENGTH).trimEnd { it.isWhitespace() || it == '.' } + "…"
 | 
			
		||||
				!text.endsWith('.')                  -> "$text."
 | 
			
		||||
				else                                 -> text
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
		
 | 
			
		||||
 
 | 
			
		||||
@@ -10,7 +10,6 @@ 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
 | 
			
		||||
@@ -20,7 +19,6 @@ 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
 | 
			
		||||
@@ -77,21 +75,16 @@ class LensApplicationConfigurable : BoundConfigurable("Inspection Lens"), Config
 | 
			
		||||
	override fun createPanel(): DialogPanel {
 | 
			
		||||
		val settings = settingsService.state
 | 
			
		||||
		
 | 
			
		||||
		lateinit var panel: DialogPanel
 | 
			
		||||
		
 | 
			
		||||
		panel = panel {
 | 
			
		||||
		return 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 items = LensHoverMode.values().toList()
 | 
			
		||||
					val renderer = SimpleListCellRenderer.create("", LensHoverMode::description)
 | 
			
		||||
					comboBox(items, renderer).bindItem(settings::lensHoverMode) { settings.lensHoverMode = it ?: LensHoverMode.DEFAULT }
 | 
			
		||||
				}
 | 
			
		||||
@@ -112,20 +105,7 @@ class LensApplicationConfigurable : BoundConfigurable("Inspection Lens"), Config
 | 
			
		||||
					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> {
 | 
			
		||||
 
 | 
			
		||||
@@ -21,14 +21,9 @@ class LensSettingsState : SimplePersistentStateComponent<LensSettingsState.State
 | 
			
		||||
		
 | 
			
		||||
		var showUnknownSeverities by property(true)
 | 
			
		||||
		var useEditorFont by property(true)
 | 
			
		||||
		var maxDescriptionLength by property(120)
 | 
			
		||||
		var lensHoverMode by enum(LensHoverMode.DEFAULT)
 | 
			
		||||
	}
 | 
			
		||||
	
 | 
			
		||||
	companion object {
 | 
			
		||||
		val MAX_DESCRIPTION_LENGTH_RANGE = 20..1000
 | 
			
		||||
	}
 | 
			
		||||
	
 | 
			
		||||
	@get:Synchronized
 | 
			
		||||
	@set:Synchronized
 | 
			
		||||
	var severityFilter = createSeverityFilter()
 | 
			
		||||
@@ -37,27 +32,11 @@ class LensSettingsState : SimplePersistentStateComponent<LensSettingsState.State
 | 
			
		||||
	val useEditorFont
 | 
			
		||||
		get() = state.useEditorFont
 | 
			
		||||
	
 | 
			
		||||
	val maxDescriptionLength
 | 
			
		||||
		get() = state.maxDescriptionLength
 | 
			
		||||
	
 | 
			
		||||
	val lensHoverMode
 | 
			
		||||
		get() = state.lensHoverMode
 | 
			
		||||
	
 | 
			
		||||
	override fun loadState(state: State) {
 | 
			
		||||
		super.loadState(state)
 | 
			
		||||
		state.maxDescriptionLength = state.maxDescriptionLength.coerceIn(MAX_DESCRIPTION_LENGTH_RANGE)
 | 
			
		||||
		update()
 | 
			
		||||
	}
 | 
			
		||||
	
 | 
			
		||||
	fun resetState() {
 | 
			
		||||
		val default = State()
 | 
			
		||||
		
 | 
			
		||||
		state.hiddenSeverities.apply { clear(); putAll(default.hiddenSeverities) }
 | 
			
		||||
		state.showUnknownSeverities = default.showUnknownSeverities
 | 
			
		||||
		state.useEditorFont = default.useEditorFont
 | 
			
		||||
		state.maxDescriptionLength = default.maxDescriptionLength
 | 
			
		||||
		state.lensHoverMode = default.lensHoverMode
 | 
			
		||||
		
 | 
			
		||||
		update()
 | 
			
		||||
	}
 | 
			
		||||
	
 | 
			
		||||
 
 | 
			
		||||
@@ -14,19 +14,6 @@
 | 
			
		||||
  ]]></description>
 | 
			
		||||
  
 | 
			
		||||
  <change-notes><![CDATA[
 | 
			
		||||
    <b>Version 1.6.0</b>
 | 
			
		||||
    <ul>
 | 
			
		||||
      <li>Added action to <b>View | Show Inspection Lenses</b> that temporarily toggles visibility of all inspections.</li>
 | 
			
		||||
      <li>File-wide inspections no longer appear.</li>
 | 
			
		||||
      <li>Fixed quick fix popup disappearing when the floating toolbar is enabled.</li>
 | 
			
		||||
      <li>Clicking an inspection now only shows relevant quick fixes (not supported for ReSharper-based languages, which use a non-standard popup).</li>
 | 
			
		||||
      <li>Tried to work around an issue where the IDE randomly fails to load the plugin.</li>
 | 
			
		||||
    </ul>
 | 
			
		||||
    <b>Version 1.5.2</b>
 | 
			
		||||
    <ul>
 | 
			
		||||
      <li>Added option to change maximum description length.</li>
 | 
			
		||||
      <li>Added button to <b>Settings | Tools | Inspection Lens</b> that resets all settings to default.</li>
 | 
			
		||||
    </ul>
 | 
			
		||||
    <b>Version 1.5.1</b>
 | 
			
		||||
    <ul>
 | 
			
		||||
      <li>Added option to change the behavior of clicking on inspections.</li>
 | 
			
		||||
@@ -104,17 +91,11 @@
 | 
			
		||||
                             parentId="tools" />
 | 
			
		||||
  </extensions>
 | 
			
		||||
  
 | 
			
		||||
  <actions>
 | 
			
		||||
    <action id="chylex.InspectionLens.ToggleLensVisibility" class="com.chylex.intellij.inspectionlens.actions.ToggleLensVisibilityAction" text="Show Inspection Lenses">
 | 
			
		||||
      <add-to-group group-id="ViewMenu" anchor="after" relative-to-action="EditorResetFontSizeGlobal" />
 | 
			
		||||
    </action>
 | 
			
		||||
  </actions>
 | 
			
		||||
  
 | 
			
		||||
  <applicationListeners>
 | 
			
		||||
    <listener class="com.chylex.intellij.inspectionlens.InspectionLensPluginListener" topic="com.intellij.ide.plugins.DynamicPluginListener" />
 | 
			
		||||
  </applicationListeners>
 | 
			
		||||
  
 | 
			
		||||
  <projectListeners>
 | 
			
		||||
    <listener class="com.chylex.intellij.inspectionlens.InspectionLensFileEditorManagerListener" topic="com.intellij.openapi.fileEditor.FileEditorManagerListener" />
 | 
			
		||||
    <listener class="com.chylex.intellij.inspectionlens.InspectionLensFileOpenedListener" topic="com.intellij.openapi.fileEditor.FileOpenedSyncListener" />
 | 
			
		||||
  </projectListeners>
 | 
			
		||||
</idea-plugin>
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user