1
0
mirror of https://github.com/chylex/IntelliJ-Inspection-Lens.git synced 2025-04-25 00:15:47 +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 8ee14ff55e
commit 4899498522
Signed by: chylex
SSH Key Fingerprint: SHA256:WqM8X/1DDn11LbYM0H5wsqZUjbcKxVsic37L+ERcF4o
3 changed files with 140 additions and 5 deletions
src/main/kotlin/com/chylex/intellij/inspectionlens/editor/lens

View File

@ -36,7 +36,9 @@ internal value class EditorLensInlay(private val inlay: Inlay<LensRenderer>) {
.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,40 @@
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.editor.Editor
import com.intellij.openapi.editor.ScrollType
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 showAt(editor: Editor, offset: Int) {
editor.caretModel.moveToOffset(offset)
editor.scrollingModel.scrollToCaret(ScrollType.MAKE_VISIBLE)
if (!tryShowPopup(editor)) {
HintManager.getInstance().showInformationHint(editor, LangBundle.message("hint.text.no.context.actions.available.at.this.location"));
}
}
private fun tryShowPopup(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
}
private val HANDLER = object : ShowIntentionActionsHandler() {
public override fun showIntentionHint(project: Project, editor: Editor, file: PsiFile, showFeedbackOnEmptyMenu: Boolean) {
super.showIntentionHint(project, editor, file, showFeedbackOnEmptyMenu)
}
}
}

View File

@ -3,43 +3,136 @@ package com.chylex.intellij.inspectionlens.editor.lens
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.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.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, settings: LensSettingsState) : HintRenderer(null) {
class LensRenderer(private var info: HighlightInfo, settings: LensSettingsState) : HintRenderer(null), InputHandler {
private val useEditorFont = settings.useEditorFont
private lateinit var severity: LensSeverity
private lateinit var inlay: Inlay<*>
private lateinit var attributes: LensSeverityTextAttributes
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)
attributes = LensSeverity.from(info.severity).textAttributes
}
override fun paint(inlay: Inlay<*>, g: Graphics, r: Rectangle, textAttributes: TextAttributes) {
fixBaselineForTextRendering(r)
super.paint(inlay, g, r, textAttributes)
if (hovered) {
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 + 1
val w = inlay.widthInPixels - UNDERLINE_WIDTH_REDUCTION
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 severity.textAttributes
return attributes
}
override fun useEditorFont(): Boolean {
return useEditorFont
}
override fun mouseMoved(event: MouseEvent, translated: Point) {
setHovered(isHoveringText(translated))
}
override fun mouseExited() {
setHovered(false)
}
private fun setHovered(hovered: Boolean) {
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) {
if (!SwingUtilities.isLeftMouseButton(event) || !isHoveringText(translated)) {
return
}
event.consume()
IntentionsPopup.showAt(inlay.editor, info.actualStartOffset)
}
private fun isHoveringText(point: Point): Boolean {
return point.x >= HOVER_PADDING_LEFT
&& point.y >= 4
&& point.x < inlay.widthInPixels - HOVER_PADDING_RIGHT
&& 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
/**
* The last character is always a period, which does not take up the full width, so the underline and the hover region are shrunk by an additional pixel.
*/
private const val EXTRA_RIGHT_SIDE_PADDING = 1
private const val UNDERLINE_WIDTH_REDUCTION = (TEXT_HORIZONTAL_PADDING * 2) + EXTRA_RIGHT_SIDE_PADDING
private const val HOVER_PADDING_LEFT = TEXT_HORIZONTAL_PADDING - 2
private const val HOVER_PADDING_RIGHT = HOVER_PADDING_LEFT + EXTRA_RIGHT_SIDE_PADDING
private fun getValidDescriptionText(text: String?): String {
return if (text.isNullOrBlank()) " " else addMissingPeriod(unescapeHtmlEntities(text))
}