diff --git a/src/main/kotlin/com/chylex/intellij/inspectionlens/editor/lens/EditorLensInlay.kt b/src/main/kotlin/com/chylex/intellij/inspectionlens/editor/lens/EditorLensInlay.kt index 0e1d972..d986282 100644 --- a/src/main/kotlin/com/chylex/intellij/inspectionlens/editor/lens/EditorLensInlay.kt +++ b/src/main/kotlin/com/chylex/intellij/inspectionlens/editor/lens/EditorLensInlay.kt @@ -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) } /** diff --git a/src/main/kotlin/com/chylex/intellij/inspectionlens/editor/lens/IntentionsPopup.kt b/src/main/kotlin/com/chylex/intellij/inspectionlens/editor/lens/IntentionsPopup.kt new file mode 100644 index 0000000..650c14f --- /dev/null +++ b/src/main/kotlin/com/chylex/intellij/inspectionlens/editor/lens/IntentionsPopup.kt @@ -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) + } + } +} diff --git a/src/main/kotlin/com/chylex/intellij/inspectionlens/editor/lens/LensRenderer.kt b/src/main/kotlin/com/chylex/intellij/inspectionlens/editor/lens/LensRenderer.kt index 97e8467..797a7ac 100644 --- a/src/main/kotlin/com/chylex/intellij/inspectionlens/editor/lens/LensRenderer.kt +++ b/src/main/kotlin/com/chylex/intellij/inspectionlens/editor/lens/LensRenderer.kt @@ -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)) }