mirror of
https://github.com/chylex/IntelliJ-Inspection-Lens.git
synced 2025-07-05 06:38:58 +02:00
Compare commits
3 Commits
fbe67d6068
...
766ba4c74c
Author | SHA1 | Date | |
---|---|---|---|
766ba4c74c | |||
4e9fdf8fa3 | |||
a754276a6c |
@ -18,7 +18,7 @@ repositories {
|
|||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
intellijPlatform {
|
intellijPlatform {
|
||||||
intellijIdeaUltimate("2023.3.3")
|
intellijIdeaUltimate("2024.2")
|
||||||
bundledPlugin("tanvd.grazi")
|
bundledPlugin("tanvd.grazi")
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -28,10 +28,19 @@ dependencies {
|
|||||||
intellijPlatform {
|
intellijPlatform {
|
||||||
pluginConfiguration {
|
pluginConfiguration {
|
||||||
ideaVersion {
|
ideaVersion {
|
||||||
sinceBuild.set("233.11361.10")
|
sinceBuild.set("242")
|
||||||
untilBuild.set(provider { null })
|
untilBuild.set(provider { null })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pluginVerification {
|
||||||
|
freeArgs.add("-mute")
|
||||||
|
freeArgs.add("TemplateWordInPluginId")
|
||||||
|
|
||||||
|
ides {
|
||||||
|
recommended()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
kotlin {
|
kotlin {
|
||||||
|
@ -2,7 +2,7 @@ rootProject.name = "InspectionLens"
|
|||||||
|
|
||||||
pluginManagement {
|
pluginManagement {
|
||||||
plugins {
|
plugins {
|
||||||
kotlin("jvm") version "1.9.21"
|
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.2.1"
|
id("org.jetbrains.intellij.platform") version "2.6.0" // https://github.com/JetBrains/intellij-platform-gradle-plugin/releases
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -3,6 +3,7 @@ package com.chylex.intellij.inspectionlens
|
|||||||
import com.chylex.intellij.inspectionlens.editor.EditorLensFeatures
|
import com.chylex.intellij.inspectionlens.editor.EditorLensFeatures
|
||||||
import com.intellij.openapi.application.ApplicationManager
|
import com.intellij.openapi.application.ApplicationManager
|
||||||
import com.intellij.openapi.components.service
|
import com.intellij.openapi.components.service
|
||||||
|
import com.intellij.openapi.diagnostic.logger
|
||||||
import com.intellij.openapi.fileEditor.FileEditorManager
|
import com.intellij.openapi.fileEditor.FileEditorManager
|
||||||
import com.intellij.openapi.fileEditor.TextEditor
|
import com.intellij.openapi.fileEditor.TextEditor
|
||||||
import com.intellij.openapi.project.ProjectManager
|
import com.intellij.openapi.project.ProjectManager
|
||||||
@ -13,6 +14,8 @@ import com.intellij.openapi.project.ProjectManager
|
|||||||
internal object InspectionLens {
|
internal object InspectionLens {
|
||||||
const val PLUGIN_ID = "com.chylex.intellij.inspectionlens"
|
const val PLUGIN_ID = "com.chylex.intellij.inspectionlens"
|
||||||
|
|
||||||
|
val LOG = logger<InspectionLens>()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Installs lenses into [editor].
|
* Installs lenses into [editor].
|
||||||
*/
|
*/
|
||||||
|
@ -2,6 +2,7 @@ package com.chylex.intellij.inspectionlens.editor
|
|||||||
|
|
||||||
import com.chylex.intellij.inspectionlens.settings.LensSettingsState
|
import com.chylex.intellij.inspectionlens.settings.LensSettingsState
|
||||||
import com.intellij.codeInsight.daemon.impl.HighlightInfo
|
import com.intellij.codeInsight.daemon.impl.HighlightInfo
|
||||||
|
import com.intellij.codeInsight.daemon.impl.UpdateHighlightersUtil
|
||||||
import com.intellij.openapi.components.service
|
import com.intellij.openapi.components.service
|
||||||
import com.intellij.openapi.editor.ex.RangeHighlighterEx
|
import com.intellij.openapi.editor.ex.RangeHighlighterEx
|
||||||
import com.intellij.openapi.editor.impl.event.MarkupModelListener
|
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) {
|
private inline fun runWithHighlighterIfValid(highlighter: RangeHighlighter, actionForImmediate: (HighlighterWithInfo) -> Unit, actionForAsync: (HighlighterWithInfo.Async) -> Unit) {
|
||||||
val info = highlighter.takeIf { it.isValid }?.let(::getFilteredHighlightInfo)
|
val info = highlighter.takeIf { it.isValid }?.let(::getFilteredHighlightInfo)
|
||||||
if (info != null) {
|
if (info != null && !UpdateHighlightersUtil.isFileLevelOrGutterAnnotation(info)) {
|
||||||
processHighlighterWithInfo(HighlighterWithInfo.from(highlighter, info), actionForImmediate, actionForAsync)
|
processHighlighterWithInfo(HighlighterWithInfo.from(highlighter, info), actionForImmediate, actionForAsync)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,58 +1,132 @@
|
|||||||
package com.chylex.intellij.inspectionlens.editor.lens
|
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.IntentionsUI
|
||||||
|
import com.intellij.codeInsight.daemon.impl.ShowIntentionsPass.IntentionsInfo
|
||||||
import com.intellij.codeInsight.hint.HintManager
|
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.codeInsight.intention.impl.ShowIntentionActionsHandler
|
||||||
import com.intellij.lang.LangBundle
|
import com.intellij.lang.LangBundle
|
||||||
|
import com.intellij.lang.annotation.HighlightSeverity
|
||||||
import com.intellij.openapi.actionSystem.ActionManager
|
import com.intellij.openapi.actionSystem.ActionManager
|
||||||
import com.intellij.openapi.actionSystem.ActionPlaces
|
import com.intellij.openapi.actionSystem.ActionPlaces
|
||||||
import com.intellij.openapi.actionSystem.IdeActions
|
import com.intellij.openapi.actionSystem.IdeActions
|
||||||
import com.intellij.openapi.actionSystem.ex.ActionUtil
|
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.Editor
|
||||||
|
import com.intellij.openapi.editor.Inlay
|
||||||
|
import com.intellij.openapi.project.DumbService
|
||||||
import com.intellij.openapi.project.Project
|
import com.intellij.openapi.project.Project
|
||||||
import com.intellij.psi.PsiDocumentManager
|
import com.intellij.psi.PsiDocumentManager
|
||||||
import com.intellij.psi.PsiFile
|
import com.intellij.psi.PsiFile
|
||||||
import com.intellij.psi.util.PsiUtilBase
|
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 {
|
internal object IntentionsPopup {
|
||||||
fun show(editor: Editor) {
|
private const val INTENTION_SOURCE_CLASS_NAME = "com.intellij.codeInsight.intention.IntentionSource"
|
||||||
if (!tryShow(editor)) {
|
|
||||||
HintManager.getInstance().showInformationHint(editor, LangBundle.message("hint.text.no.context.actions.available.at.this.location"))
|
private val showPopupMethod: ShowPopupMethod? = try {
|
||||||
|
val method = IntentionHintComponent::class.java.declaredMethods.first { method ->
|
||||||
|
val parameterTypes = method.parameterTypes
|
||||||
|
|
||||||
|
method.name == "showPopup" &&
|
||||||
|
parameterTypes.size in 1..2 &&
|
||||||
|
parameterTypes[0] === RelativePoint::class.java &&
|
||||||
|
parameterTypes.getOrNull(1).let { p -> p == null || p.name == INTENTION_SOURCE_CLASS_NAME }
|
||||||
|
}
|
||||||
|
|
||||||
|
method.isAccessible = true
|
||||||
|
|
||||||
|
@Suppress("UNCHECKED_CAST")
|
||||||
|
val args: Array<Any?> = if (method.parameterCount == 1)
|
||||||
|
arrayOf(null)
|
||||||
|
else
|
||||||
|
arrayOf(null, (Class.forName(INTENTION_SOURCE_CLASS_NAME) as Class<Enum<*>>).enumConstants.first { it.name == "OTHER" })
|
||||||
|
|
||||||
|
ShowPopupMethod(method, args)
|
||||||
|
} catch (t: Throwable) {
|
||||||
|
InspectionLens.LOG.warn("Could not initialize intention popup", t)
|
||||||
|
null
|
||||||
|
}
|
||||||
|
|
||||||
|
private class ShowPopupMethod(private val method: Method, private val args: Array<Any?>) {
|
||||||
|
operator fun invoke(component: IntentionHintComponent) {
|
||||||
|
method.invoke(component, *args)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun tryShow(editor: Editor): Boolean {
|
fun show(highlightInfo: HighlightInfo, inlay: Inlay<*>) {
|
||||||
// If the IDE uses the default Show Intentions action and handler,
|
if (!tryShow(highlightInfo, inlay)) {
|
||||||
// use the handler directly to bypass additional logic from the action.
|
showNoActionsAvailable(inlay.editor)
|
||||||
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 {
|
private fun tryShow(highlightInfo: HighlightInfo, inlay: Inlay<*>): Boolean {
|
||||||
|
val editor = inlay.editor
|
||||||
val project = editor.project ?: return false
|
val project = editor.project ?: return false
|
||||||
val file = PsiUtilBase.getPsiFileInEditor(editor, project) ?: return false
|
val file = PsiUtilBase.getPsiFileInEditor(editor, project) ?: return false
|
||||||
|
|
||||||
PsiDocumentManager.getInstance(project).commitAllDocuments()
|
PsiDocumentManager.getInstance(project).commitAllDocuments()
|
||||||
IntentionsUI.getInstance(project).hide()
|
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
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
private fun collectIntentions(editor: Editor, project: Project, file: PsiFile, info: HighlightInfo, offset: Int): IntentionsInfo {
|
||||||
* New IDEA versions mark this class as internal, so the plugin verifier flags references to it as errors.
|
val intentions = mutableListOf<IntentionActionDescriptor>()
|
||||||
*/
|
|
||||||
const val DEFAULT_ACTION_CLASS = "com.intellij.codeInsight.intention.actions.ShowIntentionActionsAction"
|
|
||||||
|
|
||||||
private val HANDLER = object : ShowIntentionActionsHandler() {
|
info.findRegisteredQuickFix { descriptor, _ ->
|
||||||
public override fun showIntentionHint(project: Project, editor: Editor, file: PsiFile, showFeedbackOnEmptyMenu: Boolean) {
|
if (DumbService.getInstance(project).isUsableInCurrentContext(descriptor.action) && ShowIntentionActionsHandler.availableFor(file, editor, offset, descriptor.action)) {
|
||||||
super.showIntentionHint(project, editor, file, showFeedbackOnEmptyMenu)
|
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"))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -125,7 +125,7 @@ class LensRenderer(private var info: HighlightInfo, private val settings: LensSe
|
|||||||
moveToOffset(editor, info.actualStartOffset)
|
moveToOffset(editor, info.actualStartOffset)
|
||||||
|
|
||||||
if ((event.button == MouseEvent.BUTTON1) xor (hoverMode != LensHoverMode.DEFAULT)) {
|
if ((event.button == MouseEvent.BUTTON1) xor (hoverMode != LensHoverMode.DEFAULT)) {
|
||||||
IntentionsPopup.show(editor)
|
IntentionsPopup.show(info, inlay)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
Loading…
Reference in New Issue
Block a user