1
0
mirror of https://github.com/chylex/IntelliJ-Inspection-Lens.git synced 2025-07-04 21:38:57 +02:00

Compare commits

...

3 Commits

7 changed files with 116 additions and 41 deletions

View File

@ -18,7 +18,7 @@ repositories {
dependencies {
intellijPlatform {
intellijIdeaUltimate("2023.3.3")
intellijIdeaUltimate("2024.2")
bundledPlugin("tanvd.grazi")
}
@ -28,10 +28,19 @@ dependencies {
intellijPlatform {
pluginConfiguration {
ideaVersion {
sinceBuild.set("233.11361.10")
sinceBuild.set("242")
untilBuild.set(provider { null })
}
}
pluginVerification {
freeArgs.add("-mute")
freeArgs.add("TemplateWordInPluginId")
ides {
recommended()
}
}
}
kotlin {

View File

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

View File

@ -3,6 +3,7 @@ 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
@ -13,6 +14,8 @@ import com.intellij.openapi.project.ProjectManager
internal object InspectionLens {
const val PLUGIN_ID = "com.chylex.intellij.inspectionlens"
val LOG = logger<InspectionLens>()
/**
* Installs lenses into [editor].
*/

View File

@ -2,6 +2,7 @@ package com.chylex.intellij.inspectionlens.editor
import com.chylex.intellij.inspectionlens.settings.LensSettingsState
import com.intellij.codeInsight.daemon.impl.HighlightInfo
import com.intellij.codeInsight.daemon.impl.UpdateHighlightersUtil
import com.intellij.openapi.components.service
import com.intellij.openapi.editor.ex.RangeHighlighterEx
import com.intellij.openapi.editor.impl.event.MarkupModelListener
@ -53,7 +54,7 @@ internal class LensMarkupModelListener(private val lensManagerDispatcher: Editor
private inline fun runWithHighlighterIfValid(highlighter: RangeHighlighter, actionForImmediate: (HighlighterWithInfo) -> Unit, actionForAsync: (HighlighterWithInfo.Async) -> Unit) {
val info = highlighter.takeIf { it.isValid }?.let(::getFilteredHighlightInfo)
if (info != null) {
if (info != null && !UpdateHighlightersUtil.isFileLevelOrGutterAnnotation(info)) {
processHighlighterWithInfo(HighlighterWithInfo.from(highlighter, info), actionForImmediate, actionForAsync)
}
}

View File

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

View File

@ -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(editor)
IntentionsPopup.show(info, inlay)
}
}
}

View File

@ -1,12 +0,0 @@
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)
}
}