User: joe Date: 14 Nov 26 02:55 Revision: 7d956b3a97d0db5859a5f4ab77c44b2f76e2baa3 Summary: Shorten fully qualified class references in Javadoc inlay hints by default TeamCity URL: http://ci.mcdev.io:80/viewModification.html?tab=vcsModificationFiles&modId=10776&personal=false Index: src/main/kotlin/platform/fabric/annotater/AnnotaterConfigurable.kt =================================================================== --- src/main/kotlin/platform/fabric/annotater/AnnotaterConfigurable.kt (revision da91a7ffd5c57cd438905d2dec442620a9173966) +++ src/main/kotlin/platform/fabric/annotater/AnnotaterConfigurable.kt (revision 7d956b3a97d0db5859a5f4ab77c44b2f76e2baa3) @@ -78,10 +78,18 @@ row { text(contributionGuidelinesNote()) } + lateinit var showJavadocs: Cell row { - checkBox(MCDevBundle("fabric.annotater.settings.show_javadocs")) + showJavadocs = checkBox(MCDevBundle("fabric.annotater.settings.show_javadocs")) .bindSelected(settings::showAnnotaterJavadocs) } + indent { + row { + checkBox(MCDevBundle("fabric.annotater.settings.shorten_class_names")) + .bindSelected(settings::shortenJavadocClassNames) + .comment(MCDevBundle("fabric.annotater.settings.shorten_class_names.comment")) + } + }.enabledIf(showJavadocs.selected) separator() lateinit var managed: Cell lateinit var custom: Cell Index: src/main/kotlin/platform/fabric/annotater/AnnotaterSettings.kt =================================================================== --- src/main/kotlin/platform/fabric/annotater/AnnotaterSettings.kt (revision da91a7ffd5c57cd438905d2dec442620a9173966) +++ src/main/kotlin/platform/fabric/annotater/AnnotaterSettings.kt (revision 7d956b3a97d0db5859a5f4ab77c44b2f76e2baa3) @@ -34,6 +34,7 @@ data class State( var enabled: Boolean = false, var showAnnotaterJavadocs: Boolean = true, + var shortenJavadocClassNames: Boolean = true, var checkoutMode: CheckoutMode = CheckoutMode.MANAGED, var managedRoot: String = "", var customDirectory: String = "", @@ -71,6 +72,12 @@ state.showAnnotaterJavadocs = showAnnotaterJavadocs } + var shortenJavadocClassNames: Boolean + get() = state.shortenJavadocClassNames + set(shortenJavadocClassNames) { + state.shortenJavadocClassNames = shortenJavadocClassNames + } + var checkoutMode: CheckoutMode get() = state.checkoutMode set(checkoutMode) { Index: src/main/kotlin/platform/fabric/annotater/javadoc/AnnotaterJavadocInlayPassFactory.kt =================================================================== --- src/main/kotlin/platform/fabric/annotater/javadoc/AnnotaterJavadocInlayPassFactory.kt (revision da91a7ffd5c57cd438905d2dec442620a9173966) +++ src/main/kotlin/platform/fabric/annotater/javadoc/AnnotaterJavadocInlayPassFactory.kt (revision 7d956b3a97d0db5859a5f4ab77c44b2f76e2baa3) @@ -89,20 +89,21 @@ private var showJavadocs = false private var items = emptyList() - private var highlights = emptyMap>() + private var decorations = emptyMap() private var gutterIcons = emptyList() private var folds = emptyList() override fun doCollectInformation(progress: ProgressIndicator) { val first = file.classes.firstOrNull() ?: return val checkout = AnnotaterCheckoutService.instance.checkoutFor(first) ?: return - showJavadocs = AnnotaterSettings.instance.showAnnotaterJavadocs + val settings = AnnotaterSettings.instance + showJavadocs = settings.showAnnotaterJavadocs val compiled = file is PsiCompiledElement // The Javadoc being edited is displayed by its editor instead 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 + // A member of each top level class with displayed Javadocs, to decorate a copy of that class with val outerClassMembers = mutableMapOf() val newGutterIcons = mutableListOf() val newFolds = mutableListOf() @@ -123,12 +124,13 @@ outerClassMembers.putIfAbsent(target.outerClass, member) } items = newItems - highlights = outerClassMembers.values.flatMap { member -> + decorations = outerClassMembers.values.flatMap { member -> progress.checkCanceled() - AnnotaterJavadocRendering.highlightReferences( + AnnotaterJavadocRendering.decorateJavadocs( member, checkout.javadocStore::getOrNull, editor.colorsScheme, + settings.shortenJavadocClassNames, ).entries }.associate { it.toPair() } gutterIcons = newGutterIcons @@ -148,7 +150,7 @@ editor, item.offset, item.javadoc, - highlights[item.target].orEmpty(), + decorations[item.target] ?: AnnotaterJavadocRendering.JavadocDecorations.EMPTY, item.pointer, ) editor.inlayModel.addBlockElement(item.offset, false, true, 0, renderer) Index: src/main/kotlin/platform/fabric/annotater/javadoc/AnnotaterJavadocInlayRenderer.kt =================================================================== --- src/main/kotlin/platform/fabric/annotater/javadoc/AnnotaterJavadocInlayRenderer.kt (revision da91a7ffd5c57cd438905d2dec442620a9173966) +++ src/main/kotlin/platform/fabric/annotater/javadoc/AnnotaterJavadocInlayRenderer.kt (revision 7d956b3a97d0db5859a5f4ab77c44b2f76e2baa3) @@ -41,13 +41,13 @@ * 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 + * @param decorations the highlighting and hidden text of the Javadoc, on top of the lexer's highlighting */ class AnnotaterJavadocInlayRenderer( private val editor: Editor, private val initialOffset: Int, private val javadoc: String, - private val highlights: List, + private val decorations: AnnotaterJavadocRendering.JavadocDecorations, private val pointer: SmartPsiElementPointer, ) : EditorCustomElementRenderer { override fun calcWidthInPixels(inlay: Inlay<*>): Int { @@ -112,28 +112,43 @@ } 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 + fun offsets(range: AnnotaterJavadocRendering.LineRange): IntRange? { + val line = textLines.getOrNull(range.line) ?: return null + if (range.endColumn > line.length) { + return null } - val lineStart = lineStarts[highlight.line] - for (i in lineStart + highlight.startColumn until lineStart + highlight.endColumn) { + val lineStart = lineStarts[range.line] + return lineStart + range.startColumn until lineStart + range.endColumn + } + for (highlight in decorations.highlights) { + for (i in offsets(highlight.range) ?: continue) { attributes[i] = TextAttributes.merge(attributes[i], highlight.attributes) } } + val hidden = BooleanArray(text.length) + for (range in decorations.hiddenRanges) { + offsets(range)?.forEach { hidden[it] = true } + } - // Splits each line into runs of the same attributes + // Splits each line into runs of the same attributes, leaving out the hidden text 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 + val tokenText = StringBuilder() + var tokenAttributes: TextAttributes? = null + for (i in lineStarts[index] until lineStarts[index] + line.length) { + if (hidden[i]) { + continue } + if (tokenText.isNotEmpty() && attributes[i] != tokenAttributes) { + tokens += Token(tokenText.toString(), tokenAttributes) + tokenText.clear() - } + } + tokenAttributes = attributes[i] + tokenText.append(text[i]) + } + if (tokenText.isNotEmpty()) { + tokens += Token(tokenText.toString(), tokenAttributes) + } tokens } this.lines = lines Index: src/main/kotlin/platform/fabric/annotater/javadoc/AnnotaterJavadocRendering.kt =================================================================== --- src/main/kotlin/platform/fabric/annotater/javadoc/AnnotaterJavadocRendering.kt (revision da91a7ffd5c57cd438905d2dec442620a9173966) +++ src/main/kotlin/platform/fabric/annotater/javadoc/AnnotaterJavadocRendering.kt (revision 7d956b3a97d0db5859a5f4ab77c44b2f76e2baa3) @@ -28,9 +28,13 @@ import com.intellij.lang.java.JavaDocumentationProvider import com.intellij.openapi.editor.colors.EditorColorsScheme import com.intellij.openapi.editor.markup.TextAttributes +import com.intellij.openapi.util.TextRange +import com.intellij.psi.PsiClass import com.intellij.psi.PsiCompiledElement +import com.intellij.psi.PsiJavaCodeReferenceElement import com.intellij.psi.PsiJavaDocumentedElement import com.intellij.psi.PsiMember +import com.intellij.psi.javadoc.PsiDocComment import com.intellij.psi.util.PsiTreeUtil import com.intellij.util.text.CharArrayUtil @@ -68,18 +72,32 @@ return JavaDocumentationProvider.generateDocStatic(declaration, null) } - class LineHighlight(val line: Int, val startColumn: Int, val endColumn: Int, val attributes: TextAttributes) + class LineRange(val line: Int, val startColumn: Int, val endColumn: Int) + class LineHighlight(val range: LineRange, 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. + * Changes to how a displayed Javadoc looks, on top of the lexer's highlighting. */ - fun highlightReferences( + class JavadocDecorations(val highlights: List, val hiddenRanges: List) { + companion object { + val EMPTY = JavadocDecorations(emptyList(), emptyList()) + } + } + + /** + * Works out how to display the annotater Javadocs in the top level class of [element]. + * + * They're highlighted in the same way as the editor highlights doc comments, such as the parameters and methods + * that they link to. The editor only adds this highlighting after resolving the code, so the lexer can't. With + * [shortenClassNames], the packages of fully qualified class names are hidden. + */ + fun decorateJavadocs( element: PsiMember, javadoc: (AnnotaterJavadocTarget) -> String?, scheme: EditorColorsScheme, - ): Map> { + shortenClassNames: Boolean, + ): 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) -> @@ -87,6 +105,18 @@ if (target != null && comment != null) target to comment else null } + val text = file.viewProvider.contents + fun lineRange(comment: PsiDocComment, start: Int, end: Int): LineRange? { + if (text.subSequence(start, end).contains('\n')) { + return null + } + val line = text.subSequence(comment.textRange.startOffset, start).count { it == '\n' } + // Each line of the comment starts with its indentation, except the first, which starts after it + val lineStart = text.lastIndexOf('\n', start - 1) + 1 + val contentStart = CharArrayUtil.shiftForward(text, lineStart, " \t") + return LineRange(line, start - contentStart, end - contentStart) + } + val holder = object : HighlightInfoHolder(file) { override fun getColorsScheme() = scheme } @@ -95,27 +125,54 @@ 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 (target, comment) = comments.firstOrNull { (_, comment) -> + comment.textRange.containsRange(info.startOffset, info.endOffset) + } ?: continue + val range = lineRange(comment, info.startOffset, info.endOffset) ?: continue + highlights.getOrPut(target, ::mutableListOf) += LineHighlight(range, attributes) - } + } - 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, - ) + + val hiddenRanges = mutableMapOf>() + if (shortenClassNames) { + for ((target, comment) in comments) { + for (reference in PsiTreeUtil.findChildrenOfType(comment, PsiJavaCodeReferenceElement::class.java)) { + val qualifier = packageQualifier(reference) ?: continue + val range = lineRange(comment, qualifier.startOffset, qualifier.endOffset) ?: continue + hiddenRanges.getOrPut(target, ::mutableListOf) += range - } + } - return highlights - } -} + } + } + + return (highlights.keys + hiddenRanges.keys).associateWith { + JavadocDecorations(highlights[it].orEmpty(), hiddenRanges[it].orEmpty()) + } + } + + /** + * The package that qualifies the class that [reference] refers to, such as `net.minecraft.util.` in + * `net.minecraft.util.Mth.Inner`. + */ + private fun packageQualifier(reference: PsiJavaCodeReferenceElement): TextRange? { + // Qualifiers are handled along with the reference that they qualify + if (reference.parent is PsiJavaCodeReferenceElement) { + return null + } + var outermostClass = reference + while (true) { + val qualifier = outermostClass.qualifier as? PsiJavaCodeReferenceElement ?: break + if (qualifier.resolve() !is PsiClass) { + break + } + outermostClass = qualifier + } + if (outermostClass.resolve() !is PsiClass) { + return null + } + val nameStart = outermostClass.referenceNameElement?.textRange?.startOffset ?: return null + return TextRange(reference.textRange.startOffset, nameStart).takeUnless { it.isEmpty } + } +} Index: src/main/resources/messages/MinecraftDevelopment.properties =================================================================== --- src/main/resources/messages/MinecraftDevelopment.properties (revision da91a7ffd5c57cd438905d2dec442620a9173966) +++ src/main/resources/messages/MinecraftDevelopment.properties (revision 7d956b3a97d0db5859a5f4ab77c44b2f76e2baa3) @@ -409,6 +409,8 @@ fabric.annotater.settings.guidelines.link=contribution guidelines fabric.annotater.settings.guidelines.link.version=contribution guidelines for {0} fabric.annotater.settings.show_javadocs=Show annotater Javadocs in code +fabric.annotater.settings.shorten_class_names=Shorten fully qualified class names in inlay hints +fabric.annotater.settings.shorten_class_names.comment=The names are still saved fully qualified, as required by annotater. fabric.annotater.settings.managed=Clone annotater automatically fabric.annotater.settings.managed_root=Clone into\: fabric.annotater.settings.clone_now=Clone or Update Annotater Index: src/test/kotlin/platform/fabric/annotater/AnnotaterJavadocEditorTest.kt =================================================================== --- src/test/kotlin/platform/fabric/annotater/AnnotaterJavadocEditorTest.kt (revision da91a7ffd5c57cd438905d2dec442620a9173966) +++ src/test/kotlin/platform/fabric/annotater/AnnotaterJavadocEditorTest.kt (revision 7d956b3a97d0db5859a5f4ab77c44b2f76e2baa3) @@ -516,21 +516,36 @@ } @Test - @DisplayName("Names that displayed Javadocs refer to are highlighted") - fun referenceHighlights() { + @DisplayName("Displayed Javadocs are highlighted like doc comments in the editor") + fun editorHighlights() { 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 highlights = decorateJavadoc(javadoc, shortenClassNames = true)?.highlights.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) }, + highlights.map { it.range }.map { it.line to lines[it.line].substring(it.startColumn, it.endColumn) }, ) } + @Test + @DisplayName("Displayed Javadocs can hide the packages of class names") + fun shortenedClassNames() { + val javadoc = "See [net.minecraft.util.Mth#clamp(int, int, int)] and [net.minecraft.util.Ticker], " + + "not [net.minecraft.util]." + fun hidden(shortenClassNames: Boolean) = decorateJavadoc(javadoc, shortenClassNames)?.hiddenRanges.orEmpty() + .map { "/// $javadoc".substring(it.startColumn, it.endColumn) } + assertEquals(listOf("net.minecraft.util.", "net.minecraft.util."), hidden(shortenClassNames = true)) + assertEquals(emptyList(), hidden(shortenClassNames = false)) + } + + private fun decorateJavadoc(javadoc: String, shortenClassNames: Boolean) = + AnnotaterJavadocRendering.decorateJavadocs( + findMember("net.minecraft.util.Mth", "clamp"), + { if (it == clamp) javadoc else null }, + EditorColorsManager.getInstance().globalScheme, + shortenClassNames, + )[clamp] + // TODO: uncomment in 2026.3 // @Test // @DisplayName("Inserting a Javadoc stub formats it")