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:
parent
beab4af5ca
commit
97422e1d42
src
main/kotlin/com/chylex/intellij/inspectionlens
InspectionLens.kt
editor
test/kotlin/com/chylex/intellij/inspectionlens
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
@ -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) {
|
||||
|
@ -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 {
|
Loading…
Reference in New Issue
Block a user