1
0
mirror of https://github.com/chylex/IntelliJ-Inspection-Lens.git synced 2025-05-10 08:34:05 +02:00

Make inspection lenses clickable and show popup with intentions

This commit is contained in:
chylex 2024-08-12 12:58:53 +02:00
parent 3bf7abab49
commit 66abf6b349
Signed by: chylex
GPG Key ID: 4DE42C8F19A80548
3 changed files with 140 additions and 2 deletions
src/main/kotlin/com/chylex/intellij/inspectionlens/editor

View File

@ -32,7 +32,9 @@ internal value class EditorLensInlay(private val inlay: Inlay<LensRenderer>) {
val renderer = LensRenderer(info)
val properties = InlayProperties().relatesToPrecedingText(true).disableSoftWrapping(true).priority(priority)
return editor.inlayModel.addAfterLineEndElement(offset, properties, renderer)?.let(::EditorLensInlay)
return editor.inlayModel.addAfterLineEndElement(offset, properties, renderer)
?.also(renderer::setInlay)
?.let(::EditorLensInlay)
}
/**

View File

@ -0,0 +1,67 @@
package com.chylex.intellij.inspectionlens.editor
import com.intellij.codeInsight.daemon.impl.HighlightInfo
import com.intellij.codeInsight.daemon.impl.HighlightInfo.IntentionActionDescriptor
import com.intellij.codeInsight.daemon.impl.ShowIntentionsPass.IntentionsInfo
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.annotation.HighlightSeverity
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.openapi.vfs.findPsiFile
import com.intellij.psi.PsiFile
import com.intellij.ui.awt.RelativePoint
import com.intellij.util.concurrency.AppExecutorUtil
import java.lang.invoke.MethodHandles
internal object IntentionPopup {
fun show(info: HighlightInfo, inlay: Inlay<*>, point: RelativePoint) {
val editor = inlay.editor
val project = editor.project ?: return
val psiFile = editor.virtualFile.findPsiFile(project) ?: return
ReadAction
.nonBlocking<IntentionsInfo> { collectIntentions(editor, project, psiFile, info, inlay.offset) }
.finishOnUiThread(ModalityState.current()) { showPopup(project, psiFile, editor, it, point) }
.submit(AppExecutorUtil.getAppExecutorService())
}
private fun collectIntentions(editor: Editor, project: Project, psiFile: PsiFile, info: HighlightInfo, offset: Int): IntentionsInfo {
val intentions = mutableListOf<IntentionActionDescriptor>()
info.findRegisteredQuickFix { descriptor, _ ->
if (DumbService.getInstance(project).isUsableInCurrentContext(descriptor.action) && ShowIntentionActionsHandler.availableFor(psiFile, 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 val showPopupMethod by lazy {
IntentionHintComponent::class.java.getDeclaredMethod("showPopup", RelativePoint::class.java)
.also { it.isAccessible = true }
.let(MethodHandles.lookup()::unreflect)
}
private fun showPopup(project: Project, psiFile: PsiFile, editor: Editor, intentions: IntentionsInfo, point: RelativePoint) {
val cachedIntentions = CachedIntentions.create(project, psiFile, editor, intentions)
val component = IntentionHintComponent.showIntentionHint(project, psiFile, editor, false, cachedIntentions)
showPopupMethod.invoke(component, point)
}
}

View File

@ -1,25 +1,46 @@
package com.chylex.intellij.inspectionlens.editor
import com.intellij.codeInsight.codeVision.ui.popup.layouter.bottom
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.colors.EditorFontType
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.awt.RelativePoint
import com.intellij.ui.paint.EffectPainter
import com.intellij.util.ui.UIUtil
import java.awt.Cursor
import java.awt.Graphics
import java.awt.Graphics2D
import java.awt.Point
import java.awt.Rectangle
import java.awt.event.MouseEvent
import javax.swing.SwingUtilities
/**
* Renders the text of an inspection lens.
*/
class LensRenderer(info: HighlightInfo) : HintRenderer(null) {
class LensRenderer(private var info: HighlightInfo) : HintRenderer(null), InputHandler {
private lateinit var severity: LensSeverity
private lateinit var inlay: Inlay<*>
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
text = getValidDescriptionText(info.description)
severity = LensSeverity.from(info.severity)
}
@ -27,6 +48,14 @@ class LensRenderer(info: HighlightInfo) : HintRenderer(null) {
override fun paint(inlay: Inlay<*>, g: Graphics, r: Rectangle, textAttributes: TextAttributes) {
fixBaselineForTextRendering(r)
super.paint(inlay, g, r, textAttributes)
if (hovered) {
val editor = inlay.editor as EditorImpl
val font = editor.colorsScheme.getFont(EditorFontType.PLAIN)
g.color = severity.textAttributes.foregroundColor
EffectPainter.LINE_UNDERSCORE.paint(g as Graphics2D, r.x + TEXT_PADDING_SIDE, r.y + editor.ascent + 1, r.width - UNDERLINE_SHRINKAGE, editor.descent, font)
}
}
override fun getTextAttributes(editor: Editor): TextAttributes {
@ -37,7 +66,47 @@ class LensRenderer(info: HighlightInfo) : HintRenderer(null) {
return true
}
override fun mouseMoved(event: MouseEvent, translated: Point) {
setHovered(true)
}
override fun mouseExited() {
setHovered(false)
}
private fun setHovered(hovered: Boolean) {
if (this.hovered == hovered) {
return
}
this.hovered = hovered
inlay.repaint()
val cursor = Cursor.getPredefinedCursor(if (hovered) Cursor.HAND_CURSOR else Cursor.DEFAULT_CURSOR)
val contentComponent = inlay.editor.contentComponent
if (contentComponent.cursor != cursor) {
UIUtil.setCursor(contentComponent, cursor)
}
}
override fun mousePressed(event: MouseEvent, translated: Point) {
if (!SwingUtilities.isLeftMouseButton(event)) {
return
}
val bounds = inlay.bounds ?: return
event.consume()
IntentionPopup.show(info, inlay, RelativePoint(event.component, Point(bounds.x + TEXT_PADDING_SIDE + 1, bounds.bottom + 3)))
}
private companion object {
/**
* [HintRenderer.paintHint] renders padding around text, but not around effects.
*/
private const val TEXT_PADDING_SIDE = 8
private const val UNDERLINE_SHRINKAGE = (TEXT_PADDING_SIDE * 2) + 1
private fun getValidDescriptionText(text: String?): String {
return if (text.isNullOrBlank()) " " else addMissingPeriod(unescapeHtmlEntities(text))
}