1
0
mirror of https://github.com/chylex/IntelliJ-Inspection-Lens.git synced 2025-04-25 00:15:47 +02:00

Split and refactor EditorInlayLensManager

This commit is contained in:
chylex 2023-05-23 07:26:21 +02:00
parent beab4af5ca
commit 97422e1d42
Signed by: chylex
GPG Key ID: 4DE42C8F19A80548
7 changed files with 195 additions and 121 deletions
src
main/kotlin/com/chylex/intellij/inspectionlens
test/kotlin/com/chylex/intellij/inspectionlens

View File

@ -1,6 +1,6 @@
package com.chylex.intellij.inspectionlens
import com.chylex.intellij.inspectionlens.editor.EditorInlayLensManager
import com.chylex.intellij.inspectionlens.editor.EditorLensManager
import com.chylex.intellij.inspectionlens.editor.LensMarkupModelListener
import com.intellij.openapi.Disposable
import com.intellij.openapi.application.ApplicationManager
@ -36,7 +36,7 @@ internal object InspectionLens {
*/
fun uninstall() {
forEachOpenEditor {
EditorInlayLensManager.remove(it.editor)
EditorLensManager.remove(it.editor)
}
}

View File

@ -1,105 +0,0 @@
package com.chylex.intellij.inspectionlens.editor
import com.intellij.codeInsight.daemon.impl.HighlightInfo
import com.intellij.openapi.editor.Editor
import com.intellij.openapi.editor.Inlay
import com.intellij.openapi.editor.InlayProperties
import com.intellij.openapi.editor.markup.RangeHighlighter
import com.intellij.openapi.util.Key
/**
* Manages visible inspection lenses for an [Editor].
*/
class EditorInlayLensManager private constructor(private val editor: Editor) {
companion object {
private val EDITOR_KEY = Key<EditorInlayLensManager>(EditorInlayLensManager::class.java.name)
/**
* Highest allowed severity for the purposes of sorting multiple highlights at the same offset.
* The value is a little higher than the highest [com.intellij.lang.annotation.HighlightSeverity], in case severities with higher values are introduced in the future.
*/
private const val MAXIMUM_SEVERITY = 500
private const val MAXIMUM_POSITION = ((Int.MAX_VALUE / MAXIMUM_SEVERITY) * 2) - 1
fun getOrCreate(editor: Editor): EditorInlayLensManager {
return editor.getUserData(EDITOR_KEY) ?: EditorInlayLensManager(editor).also { editor.putUserData(EDITOR_KEY, it) }
}
fun remove(editor: Editor) {
val manager = editor.getUserData(EDITOR_KEY)
if (manager != null) {
manager.hideAll()
editor.putUserData(EDITOR_KEY, null)
}
}
private fun getInlayHintOffset(info: HighlightInfo): Int {
// Ensures a highlight at the end of a line does not overflow to the next line.
return info.actualEndOffset - 1
}
internal fun getInlayHintPriority(position: Int, severity: Int): Int {
// Sorts highlights first by position on the line, then by severity.
val positionBucket = position.coerceIn(0, MAXIMUM_POSITION) * MAXIMUM_SEVERITY
// The multiplication can overflow, but subtracting overflowed result from Int.MAX_VALUE does not break continuity.
val positionFactor = Integer.MAX_VALUE - positionBucket
val severityFactor = severity.coerceIn(0, MAXIMUM_SEVERITY) - MAXIMUM_SEVERITY
return positionFactor + severityFactor
}
}
private val inlays = mutableMapOf<RangeHighlighter, Inlay<LensRenderer>>()
private fun show(highlighterWithInfo: HighlighterWithInfo) {
val (highlighter, info) = highlighterWithInfo
val currentInlay = inlays[highlighter]
if (currentInlay != null && currentInlay.isValid) {
currentInlay.renderer.setPropertiesFrom(info)
currentInlay.update()
}
else {
val offset = getInlayHintOffset(info)
val priority = getInlayHintPriority(info)
val renderer = LensRenderer(info)
val properties = InlayProperties().relatesToPrecedingText(true).disableSoftWrapping(true).priority(priority)
editor.inlayModel.addAfterLineEndElement(offset, properties, renderer)?.let {
inlays[highlighter] = it
}
}
}
fun showAll(highlightersWithInfo: Collection<HighlighterWithInfo>) {
executeInInlayBatchMode(highlightersWithInfo.size) { highlightersWithInfo.forEach(::show) }
}
private fun hide(highlighter: RangeHighlighter) {
inlays.remove(highlighter)?.dispose()
}
fun hideAll(highlighters: Collection<RangeHighlighter>) {
executeInInlayBatchMode(highlighters.size) { highlighters.forEach(::hide) }
}
fun hideAll() {
if (inlays.isNotEmpty()) {
executeInInlayBatchMode(inlays.size) { inlays.values.forEach(Inlay<*>::dispose) }
inlays.clear()
}
}
private fun getInlayHintPriority(info: HighlightInfo): Int {
val startOffset = info.actualStartOffset
val positionOnLine = startOffset - getLineStartOffset(startOffset)
return getInlayHintPriority(positionOnLine, info.severity.myVal)
}
private fun getLineStartOffset(offset: Int): Int {
val position = editor.offsetToLogicalPosition(offset)
return editor.document.getLineStartOffset(position.line)
}
private fun executeInInlayBatchMode(operations: Int, block: () -> Unit) {
editor.inlayModel.execute(operations > 1000, block)
}
}

View File

@ -0,0 +1,26 @@
package com.chylex.intellij.inspectionlens.editor
import com.intellij.codeInsight.daemon.impl.HighlightInfo
import com.intellij.openapi.editor.Editor
internal class EditorLens private constructor(private var inlay: EditorLensInlay) {
fun update(info: HighlightInfo): Boolean {
val editor = inlay.editor
if (!inlay.tryUpdate(info)) {
inlay = EditorLensInlay.show(editor, info) ?: return false
}
return true
}
fun hide() {
inlay.hide()
}
companion object {
fun show(editor: Editor, info: HighlightInfo): EditorLens? {
return EditorLensInlay.show(editor, info)?.let(::EditorLens)
}
}
}

View File

@ -0,0 +1,70 @@
package com.chylex.intellij.inspectionlens.editor
import com.intellij.codeInsight.daemon.impl.HighlightInfo
import com.intellij.openapi.editor.Editor
import com.intellij.openapi.editor.Inlay
import com.intellij.openapi.editor.InlayProperties
@JvmInline
internal value class EditorLensInlay(private val inlay: Inlay<LensRenderer>) {
val editor
get() = inlay.editor
fun tryUpdate(info: HighlightInfo): Boolean {
if (!inlay.isValid) {
return false
}
inlay.renderer.setPropertiesFrom(info)
inlay.update()
return true
}
fun hide() {
inlay.dispose()
}
companion object {
fun show(editor: Editor, info: HighlightInfo): EditorLensInlay? {
val offset = getInlayHintOffset(info)
val priority = getInlayHintPriority(editor, info)
val renderer = LensRenderer(info)
val properties = InlayProperties().relatesToPrecedingText(true).disableSoftWrapping(true).priority(priority)
return editor.inlayModel.addAfterLineEndElement(offset, properties, renderer)?.let(::EditorLensInlay)
}
/**
* Highest allowed severity for the purposes of sorting multiple highlights at the same offset.
* The value is a little higher than the highest [com.intellij.lang.annotation.HighlightSeverity], in case severities with higher values are introduced in the future.
*/
private const val MAXIMUM_SEVERITY = 500
private const val MAXIMUM_POSITION = ((Int.MAX_VALUE / MAXIMUM_SEVERITY) * 2) - 1
private fun getInlayHintOffset(info: HighlightInfo): Int {
// Ensures a highlight at the end of a line does not overflow to the next line.
return info.actualEndOffset - 1
}
fun getInlayHintPriority(position: Int, severity: Int): Int {
// Sorts highlights first by position on the line, then by severity.
val positionBucket = position.coerceIn(0, MAXIMUM_POSITION) * MAXIMUM_SEVERITY
// The multiplication can overflow, but subtracting overflowed result from Int.MAX_VALUE does not break continuity.
val positionFactor = Integer.MAX_VALUE - positionBucket
val severityFactor = severity.coerceIn(0, MAXIMUM_SEVERITY) - MAXIMUM_SEVERITY
return positionFactor + severityFactor
}
private fun getInlayHintPriority(editor: Editor, info: HighlightInfo): Int {
val startOffset = info.actualStartOffset
val positionOnLine = startOffset - getLineStartOffset(editor, startOffset)
return getInlayHintPriority(positionOnLine, info.severity.myVal)
}
private fun getLineStartOffset(editor: Editor, offset: Int): Int {
val position = editor.offsetToLogicalPosition(offset)
return editor.document.getLineStartOffset(position.line)
}
}
}

View File

@ -0,0 +1,83 @@
package com.chylex.intellij.inspectionlens.editor
import com.intellij.openapi.editor.Editor
import com.intellij.openapi.editor.markup.RangeHighlighter
import com.intellij.openapi.util.Key
import java.util.IdentityHashMap
/**
* Manages visible inspection lenses for an [Editor].
*/
class EditorLensManager private constructor(private val editor: Editor) {
companion object {
private val EDITOR_KEY = Key<EditorLensManager>(EditorLensManager::class.java.name)
fun getOrCreate(editor: Editor): EditorLensManager {
return editor.getUserData(EDITOR_KEY) ?: EditorLensManager(editor).also { editor.putUserData(EDITOR_KEY, it) }
}
fun remove(editor: Editor) {
val manager = editor.getUserData(EDITOR_KEY)
if (manager != null) {
manager.hideAll()
editor.putUserData(EDITOR_KEY, null)
}
}
}
private val lenses = IdentityHashMap<RangeHighlighter, EditorLens>()
private fun show(highlighterWithInfo: HighlighterWithInfo) {
val (highlighter, info) = highlighterWithInfo
val existingLens = lenses[highlighter]
if (existingLens != null) {
if (existingLens.update(info)) {
return
}
existingLens.hide()
}
val newLens = EditorLens.show(editor, info)
if (newLens != null) {
lenses[highlighter] = newLens
}
else if (existingLens != null) {
lenses.remove(highlighter)
}
}
fun show(highlightersWithInfo: Collection<HighlighterWithInfo>) {
executeInBatchMode(highlightersWithInfo.size) {
highlightersWithInfo.forEach(::show)
}
}
private fun hide(highlighter: RangeHighlighter) {
lenses.remove(highlighter)?.hide()
}
fun hide(highlighters: Collection<RangeHighlighter>) {
executeInBatchMode(highlighters.size) {
highlighters.forEach(::hide)
}
}
fun hideAll() {
executeInBatchMode(lenses.size) {
lenses.values.forEach(EditorLens::hide)
lenses.clear()
}
}
@Suppress("ConvertLambdaToReference")
private inline fun executeInBatchMode(operations: Int, crossinline action: () -> Unit) {
if (operations > 1000) {
editor.inlayModel.execute(true) { action() }
}
else {
action()
}
}
}

View File

@ -14,13 +14,13 @@ import com.intellij.openapi.util.Disposer
import com.intellij.openapi.util.Key
/**
* Listens for inspection highlights and reports them to [EditorInlayLensManager].
* Listens for inspection highlights and reports them to [EditorLensManager].
*/
internal class LensMarkupModelListener private constructor(editor: Editor) : MarkupModelListener {
private val lens = EditorInlayLensManager.getOrCreate(editor)
private val lensManager = EditorLensManager.getOrCreate(editor)
private val showOnDispatchThread = DebouncingInvokeOnDispatchThread(lens::showAll)
private val hideOnDispatchThread = DebouncingInvokeOnDispatchThread(lens::hideAll)
private val showOnDispatchThread = DebouncingInvokeOnDispatchThread(lensManager::show)
private val hideOnDispatchThread = DebouncingInvokeOnDispatchThread(lensManager::hide)
override fun afterAdded(highlighter: RangeHighlighterEx) {
showIfValid(highlighter)
@ -81,7 +81,7 @@ internal class LensMarkupModelListener private constructor(editor: Editor) : Mar
}
/**
* Attaches a new [LensMarkupModelListener] to the [Editor], and reports all existing inspection highlights to [EditorInlayLensManager].
* Attaches a new [LensMarkupModelListener] to the [Editor], and reports all existing inspection highlights to [EditorLensManager].
*/
fun register(editor: Editor, disposable: Disposable) {
if (editor.getUserData(EDITOR_KEY) != null) {

View File

@ -1,6 +1,6 @@
package com.chylex.intellij.inspectionlens
import com.chylex.intellij.inspectionlens.editor.EditorInlayLensManager
import com.chylex.intellij.inspectionlens.editor.EditorLensInlay
import com.intellij.lang.annotation.HighlightSeverity
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Nested
@ -8,47 +8,47 @@ import org.junit.jupiter.api.Test
import org.junit.jupiter.params.ParameterizedTest
import org.junit.jupiter.params.provider.ValueSource
class EditorInlayLensManagerTest {
class EditorLensTest {
@Nested
inner class Priority {
@ParameterizedTest(name = "positionAndSeverity = {0}")
@ValueSource(ints = [0, -1, Int.MIN_VALUE])
fun minimumOffset(positionAndSeverity: Int) {
assertEquals(Int.MAX_VALUE, EditorInlayLensManager.getInlayHintPriority(positionAndSeverity, Int.MAX_VALUE))
assertEquals(Int.MAX_VALUE, EditorLensInlay.getInlayHintPriority(positionAndSeverity, Int.MAX_VALUE))
}
@ParameterizedTest(name = "position = {0}")
@ValueSource(ints = [8_589_933, Int.MAX_VALUE])
fun maximumOffset(position: Int) {
assertEquals(Int.MIN_VALUE + 295, EditorInlayLensManager.getInlayHintPriority(position, Int.MIN_VALUE))
assertEquals(Int.MIN_VALUE + 295, EditorLensInlay.getInlayHintPriority(position, Int.MIN_VALUE))
}
@ParameterizedTest(name = "severity = {0}")
@ValueSource(ints = [0, 1, 250, 499, 500])
fun firstPriorityBucket(severity: Int) {
assertEquals(Int.MAX_VALUE - 500 + severity, EditorInlayLensManager.getInlayHintPriority(0, severity))
assertEquals(Int.MAX_VALUE - 500 + severity, EditorLensInlay.getInlayHintPriority(0, severity))
}
@ParameterizedTest(name = "severity = {0}")
@ValueSource(ints = [0, 1, 250, 499, 500])
fun secondPriorityBucket(severity: Int) {
assertEquals(Int.MAX_VALUE - 1000 + severity, EditorInlayLensManager.getInlayHintPriority(1, severity))
assertEquals(Int.MAX_VALUE - 1000 + severity, EditorLensInlay.getInlayHintPriority(1, severity))
}
@ParameterizedTest(name = "severity = {0}")
@ValueSource(ints = [0, 1, 250, 499, 500])
fun middlePriorityBucket(severity: Int) {
assertEquals(-353 + severity, EditorInlayLensManager.getInlayHintPriority(4294967, severity))
assertEquals(-353 + severity, EditorLensInlay.getInlayHintPriority(4294967, severity))
}
@ParameterizedTest(name = "severity = {0}")
@ValueSource(ints = [0, 1, 250, 499, 500])
fun penultimatePriorityBucket(severity: Int) {
assertEquals(Int.MIN_VALUE + 295 + 500 + severity, EditorInlayLensManager.getInlayHintPriority(8_589_932, severity))
assertEquals(Int.MIN_VALUE + 295 + 500 + severity, EditorLensInlay.getInlayHintPriority(8_589_932, severity))
}
/**
* If any of these change, re-evaluate [EditorInlayLensManager.MAXIMUM_SEVERITY] and the priority calculations.
* If any of these change, re-evaluate [EditorLensInlay.MAXIMUM_SEVERITY] and the priority calculations.
*/
@Nested
inner class IdeaHighlightSeverityAssumptions {