Compare commits

...

2 Commits

4 changed files with 101 additions and 2 deletions

View File

@ -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(

View File

@ -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

View File

@ -42,13 +42,23 @@ class LensRenderer(info: HighlightInfo) : HintRenderer(null) {
private val ATTRIBUTES_SINGLETON = TextAttributes(null, null, null, null, Font.ITALIC)
private fun getValidDescriptionText(text: String?): String {
return if (text.isNullOrBlank()) " " else addMissingPeriod(StringUtil.unescapeXmlEntities(StringUtil.stripHtml(text, " ")))
return if (text.isNullOrBlank()) " " else addMissingPeriod(convertHtmlToText(text))
}
private fun convertHtmlToText(potentialHtml: String): String {
return potentialHtml
.ifContains('<') { StringUtil.stripHtml(it, " ") }
.ifContains('&', StringUtil::unescapeXmlEntities)
}
private fun addMissingPeriod(text: String): String {
return if (text.endsWith('.')) text else "$text."
}
private inline fun String.ifContains(charToTest: Char, action: (String) -> String): String {
return if (this.contains(charToTest)) action(this) else this
}
private fun fixBaselineForTextRendering(rect: Rectangle) {
rect.y += 1
}

View File

@ -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))
}
}
}
}