User: joe Date: 14 Nov 26 02:24 Revision: da91a7ffd5c57cd438905d2dec442620a9173966 Summary: Better highlighting of Javadoc inlays TeamCity URL: http://ci.mcdev.io:80/viewModification.html?tab=vcsModificationFiles&modId=10775&personal=false Index: src/main/kotlin/platform/fabric/annotater/javadoc/AnnotaterJavadocInlayPassFactory.kt =================================================================== --- src/main/kotlin/platform/fabric/annotater/javadoc/AnnotaterJavadocInlayPassFactory.kt (revision e7e1b5db590554229bc7a169849408b50b319d53) +++ src/main/kotlin/platform/fabric/annotater/javadoc/AnnotaterJavadocInlayPassFactory.kt (revision da91a7ffd5c57cd438905d2dec442620a9173966) @@ -74,7 +74,12 @@ return Pass(file, editor) } - private class Item(val offset: Int, val javadoc: String, val pointer: SmartPsiElementPointer) + private class Item( + val target: AnnotaterJavadocTarget, + val offset: Int, + val javadoc: String, + val pointer: SmartPsiElementPointer, + ) private class GutterIcon(val offset: Int, val pointer: SmartPsiElementPointer) @@ -84,6 +89,7 @@ private var showJavadocs = false private var items = emptyList() + private var highlights = emptyMap>() private var gutterIcons = emptyList() private var folds = emptyList() @@ -96,6 +102,8 @@ val edited = AnnotaterJavadocInlineEditor.current(myProject)?.takeIf { it.hostEditor === editor }?.target val newItems = mutableListOf() + // A member of each top level class with displayed Javadocs, to highlight a copy of that class with + val outerClassMembers = mutableMapOf() val newGutterIcons = mutableListOf() val newFolds = mutableListOf() for (member in AnnotaterJavadocTarget.declarations(file)) { @@ -111,9 +119,18 @@ if (!showJavadocs || target == edited) { continue } - newItems += Item(AnnotaterJavadocRendering.displayOffset(member), javadoc, pointer) + newItems += Item(target, AnnotaterJavadocRendering.displayOffset(member), javadoc, pointer) + outerClassMembers.putIfAbsent(target.outerClass, member) } items = newItems + highlights = outerClassMembers.values.flatMap { member -> + progress.checkCanceled() + AnnotaterJavadocRendering.highlightReferences( + member, + checkout.javadocStore::getOrNull, + editor.colorsScheme, + ).entries + }.associate { it.toPair() } gutterIcons = newGutterIcons folds = newFolds } @@ -127,7 +144,13 @@ editor.getUserData(Const.INLAYS_KEY)?.forEach { it.dispose() } val inlays = items.filter { it.offset in 0..length }.mapNotNull { item -> - val renderer = AnnotaterJavadocInlayRenderer(editor, item.offset, item.javadoc, item.pointer) + val renderer = AnnotaterJavadocInlayRenderer( + editor, + item.offset, + item.javadoc, + highlights[item.target].orEmpty(), + item.pointer, + ) editor.inlayModel.addBlockElement(item.offset, false, true, 0, renderer) } editor.putUserData(Const.INLAYS_KEY, inlays.takeIf { it.isNotEmpty() }) Index: src/main/kotlin/platform/fabric/annotater/javadoc/AnnotaterJavadocInlayRenderer.kt =================================================================== --- src/main/kotlin/platform/fabric/annotater/javadoc/AnnotaterJavadocInlayRenderer.kt (revision e7e1b5db590554229bc7a169849408b50b319d53) +++ src/main/kotlin/platform/fabric/annotater/javadoc/AnnotaterJavadocInlayRenderer.kt (revision da91a7ffd5c57cd438905d2dec442620a9173966) @@ -40,11 +40,14 @@ /** * Displays an annotater Javadoc above its declaration as the `///` comment it would be in the source, highlighted in * the same way as a doc comment in a Java file. + * + * @param highlights the highlighting of the names that the Javadoc refers to, on top of the lexer's */ class AnnotaterJavadocInlayRenderer( private val editor: Editor, private val initialOffset: Int, private val javadoc: String, + private val highlights: List, private val pointer: SmartPsiElementPointer, ) : EditorCustomElementRenderer { override fun calcWidthInPixels(inlay: Inlay<*>): Int { @@ -97,25 +100,42 @@ val scheme = editor.colorsScheme lines?.takeIf { linesScheme === scheme }?.let { return it } - val text = javadoc.lines().joinToString("\n") { if (it.isEmpty()) "///" else "/// $it" } + val textLines = javadoc.lines().map { if (it.isEmpty()) "///" else "/// $it" } + val text = textLines.joinToString("\n") + val attributes = arrayOfNulls(text.length) val highlighter = LexerEditorHighlighter(JavaFileHighlighter(LanguageLevel.HIGHEST), scheme) highlighter.setText(text) - val lines = mutableListOf>() - var tokens = mutableListOf() val iterator = highlighter.createIterator(0) while (!iterator.atEnd()) { - for ((index, part) in text.substring(iterator.start, iterator.end).split('\n').withIndex()) { - if (index > 0) { - lines += tokens - tokens = mutableListOf() + attributes.fill(iterator.textAttributes, iterator.start, iterator.end) + iterator.advance() - } + } - if (part.isNotEmpty()) { - tokens += Token(part, iterator.textAttributes) + + val lineStarts = textLines.runningFold(0) { start, line -> start + line.length + 1 } + for (highlight in highlights) { + val line = textLines.getOrNull(highlight.line) ?: continue + if (highlight.endColumn > line.length) { + continue - } + } + val lineStart = lineStarts[highlight.line] + for (i in lineStart + highlight.startColumn until lineStart + highlight.endColumn) { + attributes[i] = TextAttributes.merge(attributes[i], highlight.attributes) } - iterator.advance() } - lines += tokens + + // Splits each line into runs of the same attributes + val lines = textLines.mapIndexed { index, line -> + val offset = lineStarts[index] + val tokens = mutableListOf() + var start = 0 + for (end in 1..line.length) { + if (end == line.length || attributes[offset + end] != attributes[offset + start]) { + tokens += Token(line.substring(start, end), attributes[offset + start]) + start = end + } + } + tokens + } this.lines = lines linesScheme = scheme return lines Index: src/main/kotlin/platform/fabric/annotater/javadoc/AnnotaterJavadocRendering.kt =================================================================== --- src/main/kotlin/platform/fabric/annotater/javadoc/AnnotaterJavadocRendering.kt (revision e7e1b5db590554229bc7a169849408b50b319d53) +++ src/main/kotlin/platform/fabric/annotater/javadoc/AnnotaterJavadocRendering.kt (revision da91a7ffd5c57cd438905d2dec442620a9173966) @@ -23,11 +23,16 @@ import com.demonwav.mcdev.platform.fabric.annotater.AnnotaterCheckout import com.demonwav.mcdev.platform.fabric.annotater.AnnotaterCheckoutService import com.demonwav.mcdev.platform.fabric.annotater.AnnotaterSettings +import com.intellij.codeInsight.daemon.impl.analysis.HighlightInfoHolder +import com.intellij.codeInsight.daemon.impl.analysis.HighlightNamesUtil import com.intellij.lang.java.JavaDocumentationProvider +import com.intellij.openapi.editor.colors.EditorColorsScheme +import com.intellij.openapi.editor.markup.TextAttributes import com.intellij.psi.PsiCompiledElement import com.intellij.psi.PsiJavaDocumentedElement import com.intellij.psi.PsiMember import com.intellij.psi.util.PsiTreeUtil +import com.intellij.util.text.CharArrayUtil /** * Renders annotater Javadocs with the IDE's own Java documentation renderer, by rendering the declaration in a copy of @@ -62,4 +67,55 @@ val declaration = copiedDeclaration(member, javadoc) ?: return null return JavaDocumentationProvider.generateDocStatic(declaration, null) } + + class LineHighlight(val line: Int, val startColumn: Int, val endColumn: Int, val attributes: TextAttributes) + + /** + * Highlights the names that the annotater Javadocs in the top level class of [element] refer to, such as + * parameters and linked methods, in the same way as the editor does in doc comments. The editor only highlights + * these after resolving them, so the lexer can't. + */ + fun highlightReferences( + element: PsiMember, + javadoc: (AnnotaterJavadocTarget) -> String?, + scheme: EditorColorsScheme, + ): Map> { + val copy = AnnotaterJavadocSourceCopy.create(element, javadoc = javadoc) ?: return emptyMap() + val file = copy.createPsiFile(element.project) ?: return emptyMap() + val comments = AnnotaterJavadocTarget.declarations(file).zip(copy.targets).mapNotNull { (member, target) -> + val comment = (member as PsiJavaDocumentedElement).docComment + if (target != null && comment != null) target to comment else null -} + } + + val holder = object : HighlightInfoHolder(file) { + override fun getColorsScheme() = scheme + } + HighlightNamesUtil.highlight(file, holder) { visitor -> + for ((_, comment) in comments) { + PsiTreeUtil.processElements(comment) { it.accept(visitor); true } + } + } + + val text = file.viewProvider.contents + val highlights = mutableMapOf>() + for (i in 0 until holder.size()) { + val info = holder.get(i) + val attributes = info.getTextAttributes(null, scheme)?.takeUnless { it.isEmpty } ?: continue + val (target, comment) = comments.firstOrNull { it.second.textRange.containsRange(info.startOffset, info.endOffset) } ?: continue + if (text.subSequence(info.startOffset, info.endOffset).contains('\n')) { + continue + } + val line = text.subSequence(comment.textRange.startOffset, info.startOffset).count { it == '\n' } + // Each line of the comment starts with its indentation, except the first, which starts after it + val lineStart = text.lastIndexOf('\n', info.startOffset - 1) + 1 + val contentStart = CharArrayUtil.shiftForward(text, lineStart, " \t") + highlights.getOrPut(target, ::mutableListOf) += LineHighlight( + line, + info.startOffset - contentStart, + info.endOffset - contentStart, + attributes, + ) + } + return highlights + } +} Index: src/test/kotlin/platform/fabric/annotater/AnnotaterJavadocEditorTest.kt =================================================================== --- src/test/kotlin/platform/fabric/annotater/AnnotaterJavadocEditorTest.kt (revision e7e1b5db590554229bc7a169849408b50b319d53) +++ src/test/kotlin/platform/fabric/annotater/AnnotaterJavadocEditorTest.kt (revision da91a7ffd5c57cd438905d2dec442620a9173966) @@ -43,6 +43,7 @@ import com.intellij.openapi.command.WriteCommandAction import com.intellij.openapi.command.undo.UndoManager import com.intellij.openapi.editor.Editor +import com.intellij.openapi.editor.colors.EditorColorsManager import com.intellij.openapi.editor.ex.EditorEx import com.intellij.openapi.fileEditor.FileDocumentManager import com.intellij.openapi.fileEditor.TextEditor @@ -514,6 +515,22 @@ assertFalse("Existing method docs" in hidden, hidden) } + @Test + @DisplayName("Names that displayed Javadocs refer to are highlighted") + fun referenceHighlights() { + val javadoc = "Clamps a value.\n\nSee [#clamp(int, int, int)]." + val highlights = AnnotaterJavadocRendering.highlightReferences( + findMember("net.minecraft.util.Mth", "clamp"), + { if (it == clamp) javadoc else null }, + EditorColorsManager.getInstance().globalScheme, + )[clamp].orEmpty() + val lines = javadoc.lines().map { if (it.isEmpty()) "///" else "/// $it" } + assertEquals( + listOf(2 to "clamp"), + highlights.map { it.line to lines[it.line].substring(it.startColumn, it.endColumn) }, + ) + } + // TODO: uncomment in 2026.3 // @Test // @DisplayName("Inserting a Javadoc stub formats it")