diff --git a/build.gradle.kts b/build.gradle.kts index 133d35d..9f2ccaa 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -14,6 +14,10 @@ repositories { mavenCentral() } +dependencies { + testImplementation("org.junit.jupiter:junit-jupiter:5.9.0") +} + intellij { version.set("2022.2") updateSinceUntilBuild.set(false) @@ -27,6 +31,10 @@ tasks.buildSearchableOptions { enabled = false } +tasks.test { + useJUnitPlatform() +} + tasks.withType<KotlinCompile> { kotlinOptions.jvmTarget = "11" kotlinOptions.freeCompilerArgs = listOf( diff --git a/src/main/kotlin/com/chylex/intellij/inspectionlens/EditorInlayLensManager.kt b/src/main/kotlin/com/chylex/intellij/inspectionlens/EditorInlayLensManager.kt index 1c981f8..fb0a87e 100644 --- a/src/main/kotlin/com/chylex/intellij/inspectionlens/EditorInlayLensManager.kt +++ b/src/main/kotlin/com/chylex/intellij/inspectionlens/EditorInlayLensManager.kt @@ -1,5 +1,6 @@ package com.chylex.intellij.inspectionlens +import com.chylex.intellij.inspectionlens.EditorInlayLensManager.Companion.MAXIMUM_SEVERITY import com.intellij.codeInsight.daemon.impl.HighlightInfo import com.intellij.openapi.editor.Editor import com.intellij.openapi.editor.Inlay @@ -14,6 +15,13 @@ class EditorInlayLensManager private constructor(private val editor: Editor) { companion object { private val KEY = Key<EditorInlayLensManager>(EditorInlayLensManager::class.java.name) + /** + * Highest allowed severity for the purposes of sorting multiple highlights at the same offset. + * A [MAXIMUM_SEVERITY] of 500 allows for 8 589 933 positions in the document before sorting breaks down. + * 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 + fun getOrCreate(editor: Editor): EditorInlayLensManager { return editor.getUserData(KEY) ?: EditorInlayLensManager(editor).also { editor.putUserData(KEY, it) } } @@ -30,6 +38,19 @@ class EditorInlayLensManager private constructor(private val editor: Editor) { // Ensures a highlight at the end of a line does not overflow to the next line. return info.actualEndOffset - 1 } + + internal fun getInlayHintPriority(offset: Int, severity: Int): Int { + // Sorts highlights first by offset in the document, then by severity. + val positionBucket = offset.coerceAtLeast(0) * MAXIMUM_SEVERITY.toLong() + val positionFactor = (Int.MAX_VALUE - MAXIMUM_SEVERITY - positionBucket).coerceAtLeast(Int.MIN_VALUE + 1L).toInt() + val severityFactor = severity.coerceIn(0, MAXIMUM_SEVERITY) + // The result is between (Int.MIN_VALUE + 1)..Int.MAX_VALUE, allowing for negation without overflow. + return positionFactor + severityFactor + } + + private fun getInlayHintPriority(info: HighlightInfo): Int { + return getInlayHintPriority(info.actualEndOffset, info.severity.myVal) + } } private val inlays = mutableMapOf<RangeHighlighter, Inlay<LensRenderer>>() @@ -43,8 +64,9 @@ class EditorInlayLensManager private constructor(private val editor: Editor) { } else { val offset = getInlayHintOffset(info) + val priority = getInlayHintPriority(info) val renderer = LensRenderer(info) - val properties = InlayProperties().relatesToPrecedingText(true).disableSoftWrapping(true).priority(-offset) + val properties = InlayProperties().relatesToPrecedingText(true).disableSoftWrapping(true).priority(priority) editor.inlayModel.addAfterLineEndElement(offset, properties, renderer)?.let { inlays[highlighter] = it diff --git a/src/test/kotlin/com/chylex/intellij/inspectionlens/EditorInlayLensManagerTest.kt b/src/test/kotlin/com/chylex/intellij/inspectionlens/EditorInlayLensManagerTest.kt new file mode 100644 index 0000000..5de1e40 --- /dev/null +++ b/src/test/kotlin/com/chylex/intellij/inspectionlens/EditorInlayLensManagerTest.kt @@ -0,0 +1,59 @@ +package com.chylex.intellij.inspectionlens + +import com.intellij.lang.annotation.HighlightSeverity +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Nested +import org.junit.jupiter.api.Test +import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.ValueSource + +class EditorInlayLensManagerTest { + @Nested + inner class Priority { + @ParameterizedTest(name = "offsetAndSeverity = {0}") + @ValueSource(ints = [0, -1, Int.MIN_VALUE]) + fun minimumOffset(offsetAndSeverity: Int) { + assertEquals(Int.MAX_VALUE, EditorInlayLensManager.getInlayHintPriority(offsetAndSeverity, Int.MAX_VALUE)) + } + + @ParameterizedTest(name = "offset = {0}") + @ValueSource(ints = [8_589_934, Int.MAX_VALUE]) + fun maximumOffset(offset: Int) { + assertEquals(Int.MIN_VALUE + 1, EditorInlayLensManager.getInlayHintPriority(offset, 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)) + } + + @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)) + } + + @ParameterizedTest(name = "severity = {0}") + @ValueSource(ints = [0, 1, 250, 499, 500]) + fun penultimatePriorityBucket(severity: Int) { + assertEquals(Int.MIN_VALUE + 295 + severity, EditorInlayLensManager.getInlayHintPriority(8_589_933, severity)) + } + + /** + * If any of these change, re-evaluate [EditorInlayLensManager.MAXIMUM_SEVERITY] and the priority calculations. + */ + @Nested + inner class IdeaAssumptions { + @Test + fun smallestSeverityHasNotChanged() { + assertEquals(10, HighlightSeverity.DEFAULT_SEVERITIES.minOf(HighlightSeverity::myVal)) + } + + @Test + fun highestSeverityHasNotChanged() { + assertEquals(400, HighlightSeverity.DEFAULT_SEVERITIES.maxOf(HighlightSeverity::myVal)) + } + } + } +}