From 3ef430b087f40d9e57b294cc1ac759fcb6becb75 Mon Sep 17 00:00:00 2001 From: Jan Lahoda Date: Mon, 30 Sep 2024 13:52:15 +0200 Subject: [PATCH] Support for Markdown javadoc (JEP-467) --- .../modules/java/completion/BaseTask.java | 4 + .../java/completion/JavaCompletionTask.java | 1 + .../nbproject/project.properties | 2 +- .../base/javadoc/JavadocCompletionUtils.java | 34 ++- .../editor/base/javadoc/JavadocImports.java | 10 +- .../javadoc/JavadocCompletionUtilsTest.java | 16 +- .../base/javadoc/JavadocImportsTest.java | 199 +++++++++++++ .../base/javadoc/JavadocTestSupport.java | 16 +- java/java.editor/nbproject/project.properties | 2 +- .../modules/editor/java/GoToSupport.java | 4 +- .../editor/java/JavaCompletionCollector.java | 1 + .../editor/java/JavaCompletionItem.java | 1 + .../netbeans/modules/editor/java/JavaKit.java | 1 + .../modules/editor/java/TypingCompletion.java | 15 + .../modules/editor/java/Utilities.java | 1 + .../editor/javadoc/JavadocCompletionTask.java | 42 +-- .../modules/editor/java/GoToSupportTest.java | 65 +++++ .../editor/java/TypingCompletionUnitTest.java | 16 ++ .../javadoc/JavadocCompletionQueryTest.java | 272 +++++++++++++++++- java/java.lexer/nbproject/project.properties | 2 +- .../netbeans/api/java/lexer/JavaTokenId.java | 4 + .../netbeans/lib/java/lexer/JavaLexer.java | 35 +++ .../netbeans/lib/java/lexer/JavadocLexer.java | 14 + .../lib/java/lexer/JavaLexerBatchTest.java | 79 +++++ .../lib/java/lexer/JavadocLexerTest.java | 14 + java/java.lsp.server/licenseinfo.xml | 5 + .../vscode/language-configuration.json | 111 +++++++ java/java.lsp.server/vscode/package.json | 12 + .../vscode/syntaxes/java.tmLanguage.json | 57 ++++ java/java.source.base/apichanges.xml | 12 + .../nbproject/project.properties | 2 +- .../api/java/source/AssignComments.java | 12 +- .../netbeans/api/java/source/TreeMaker.java | 40 ++- .../api/java/source/TreeUtilities.java | 6 +- .../java/source/builder/TreeFactory.java | 67 ++++- .../java/source/pretty/VeryPretty.java | 61 +++- .../modules/java/source/save/CasualDiff.java | 23 +- .../java/source/save/PositionEstimator.java | 1 + .../transform/ImmutableDocTreeTranslator.java | 20 +- .../api/java/source/gen/DoctreeTest.java | 219 ++++++++++++++ java/java.sourceui/nbproject/project.xml | 8 + .../api/java/source/ui/ElementJavadoc.java | 58 +++- .../java/source/ui/ElementJavadocTest.java | 164 +++++++++++ java/javadoc/nbproject/project.properties | 4 +- .../javadoc/highlighting/Highlighting.java | 12 +- .../javadoc/hints/JavadocGenerator.java | 57 +++- .../highlighting/HighlightingTest.java | 11 + .../modules/javadoc/hints/AddTagFixTest.java | 31 ++ .../javadoc/hints/GenerateJavadocFixTest.java | 101 +++++++ .../javadoc/hints/RemoveTagFixTest.java | 26 ++ 50 files changed, 1879 insertions(+), 91 deletions(-) create mode 100644 java/java.lsp.server/vscode/language-configuration.json create mode 100644 java/java.sourceui/test/unit/src/org/netbeans/api/java/source/ui/ElementJavadocTest.java diff --git a/java/java.completion/src/org/netbeans/modules/java/completion/BaseTask.java b/java/java.completion/src/org/netbeans/modules/java/completion/BaseTask.java index f8869882aa38..2346cd89d170 100644 --- a/java/java.completion/src/org/netbeans/modules/java/completion/BaseTask.java +++ b/java/java.completion/src/org/netbeans/modules/java/completion/BaseTask.java @@ -176,6 +176,7 @@ TokenSequence nextNonWhitespaceToken(TokenSequence ts) case LINE_COMMENT: case BLOCK_COMMENT: case JAVADOC_COMMENT: + case JAVADOC_COMMENT_LINE_RUN: break; default: return ts; @@ -206,6 +207,7 @@ TokenSequence previousNonWhitespaceToken(TokenSequence case LINE_COMMENT: case BLOCK_COMMENT: case JAVADOC_COMMENT: + case JAVADOC_COMMENT_LINE_RUN: break; default: return ts; @@ -461,6 +463,7 @@ private Env getEnvImpl(CompilationController controller, TreePath orig, TreePath case LINE_COMMENT: case BLOCK_COMMENT: case JAVADOC_COMMENT: + case JAVADOC_COMMENT_LINE_RUN: break; case ARROW: scope = controller.getTrees().getScope(blockPath); @@ -490,6 +493,7 @@ private Env getEnvImpl(CompilationController controller, TreePath orig, TreePath case LINE_COMMENT: case BLOCK_COMMENT: case JAVADOC_COMMENT: + case JAVADOC_COMMENT_LINE_RUN: break; case ARROW: return new Env(offset, prefix, controller, path, sourcePositions, scope); diff --git a/java/java.completion/src/org/netbeans/modules/java/completion/JavaCompletionTask.java b/java/java.completion/src/org/netbeans/modules/java/completion/JavaCompletionTask.java index 9ec7273f86f6..be001bce2358 100644 --- a/java/java.completion/src/org/netbeans/modules/java/completion/JavaCompletionTask.java +++ b/java/java.completion/src/org/netbeans/modules/java/completion/JavaCompletionTask.java @@ -1608,6 +1608,7 @@ private void insideMemberSelect(Env env) throws IOException { case LINE_COMMENT: case BLOCK_COMMENT: case JAVADOC_COMMENT: + case JAVADOC_COMMENT_LINE_RUN: break; default: lastNonWhitespaceTokenId = ts.token().id(); diff --git a/java/java.editor.base/nbproject/project.properties b/java/java.editor.base/nbproject/project.properties index 1f620a92a638..808db111121e 100644 --- a/java/java.editor.base/nbproject/project.properties +++ b/java/java.editor.base/nbproject/project.properties @@ -16,7 +16,7 @@ # under the License. spec.version.base=2.91.0 is.autoload=true -javac.source=1.8 +javac.release=17 javac.compilerargs=-Xlint -Xlint:-serial test.config.semantic.includes=\ diff --git a/java/java.editor.base/src/org/netbeans/modules/java/editor/base/javadoc/JavadocCompletionUtils.java b/java/java.editor.base/src/org/netbeans/modules/java/editor/base/javadoc/JavadocCompletionUtils.java index a0682653d7f6..0bf0f69914db 100644 --- a/java/java.editor.base/src/org/netbeans/modules/java/editor/base/javadoc/JavadocCompletionUtils.java +++ b/java/java.editor.base/src/org/netbeans/modules/java/editor/base/javadoc/JavadocCompletionUtils.java @@ -51,7 +51,7 @@ */ public final class JavadocCompletionUtils { - static final Pattern JAVADOC_LINE_BREAK = Pattern.compile("\\n[ \\t]*\\**[ \\t]*\\z"); // NOI18N + static final Pattern JAVADOC_LINE_BREAK = Pattern.compile("(\\n[ \\t]*\\**[ \\t]*\\z)|(\\n[ \\t]*///[ \\t]*\\z)"); // NOI18N static final Pattern JAVADOC_WHITE_SPACE = Pattern.compile("[^ \\t]"); // NOI18N /** * javadoc parser considers whatever number of spaces or standalone newline @@ -62,7 +62,7 @@ public final class JavadocCompletionUtils { static final Pattern JAVADOC_EMPTY = Pattern.compile("(\\s*\\**\\s*\n)*\\s*\\**\\s*\\**"); // NOI18N static final Pattern JAVADOC_FIRST_WHITE_SPACE = Pattern.compile("[ \\t]*\\**[ \\t]*"); // NOI18N private static Set IGNORE_TOKES = EnumSet.of( - JavaTokenId.WHITESPACE, JavaTokenId.BLOCK_COMMENT, JavaTokenId.LINE_COMMENT); + JavaTokenId.WHITESPACE, JavaTokenId.BLOCK_COMMENT, JavaTokenId.LINE_COMMENT, JavaTokenId.JAVADOC_COMMENT_LINE_RUN); private static final Logger LOGGER = Logger.getLogger(JavadocCompletionUtils.class.getName()); /** @@ -196,6 +196,7 @@ public static TokenSequence findJavadocTokenSequence(Compilation break; } case JAVADOC_COMMENT: + case JAVADOC_COMMENT_LINE_RUN: if (token.partType() == PartType.COMPLETE) { return javac.getElements().getDocComment(e) == null ? null : s.embedded(JavadocTokenId.language()); @@ -246,36 +247,39 @@ static boolean isInsideIndent(Token token, int offset) { /** * Is javadoc line break? - * @param token token to test + * @param ts a token sequence positioned to the token to test * @return {@code true} in case the token is something like {@code "\n\t*"} */ - public static boolean isLineBreak(Token token) { - return isLineBreak(token, token.length()); + public static boolean isLineBreak(TokenSequence ts) { + return isLineBreak(ts, ts.token().length()); } /** * Tests if the token part before {@code pos} is a javadoc line break. - * @param token a token to test + * @param ts a token sequence positioned to the token to test * @param pos position in the token * @return {@code true} in case the token is something like {@code "\n\t* |\n\t*"} */ - public static boolean isLineBreak(Token token, int pos) { + public static boolean isLineBreak(TokenSequence ts, int pos) { + Token token = ts.token(); + if (token == null || token.id() != JavadocTokenId.OTHER_TEXT) { - return false; + return ts.isEmpty() || ts.index() == 0; } try { CharSequence text = token.text(); if (pos < token.length()) text = text.subSequence(0, pos); - boolean result = pos > 0 + boolean result = (pos > 0 && JAVADOC_LINE_BREAK.matcher(text).find() - && (pos == token.length() || !isInsideIndent(token, pos)); + && (pos == token.length() || !isInsideIndent(token, pos)) + ); return result; } catch (IndexOutOfBoundsException e) { throw (IndexOutOfBoundsException) new IndexOutOfBoundsException("pos: " + pos + ", token.length: " + token.length() + ", token text: " + token.text()).initCause(e); } } - + public static boolean isWhiteSpace(CharSequence text) { return text != null && text.length() > 0 && !JAVADOC_WHITE_SPACE.matcher(text).find(); } @@ -437,7 +441,8 @@ private static boolean movedToJavadocToken(TokenSequence ts, int of return false; } - if (ts.token().id() != JavaTokenId.JAVADOC_COMMENT) { + if (ts.token().id() != JavaTokenId.JAVADOC_COMMENT && + ts.token().id() != JavaTokenId.JAVADOC_COMMENT_LINE_RUN) { return false; } @@ -456,6 +461,11 @@ private static boolean isEmptyJavadoc(Token token, int offset) { // check special case /**|*/ return offset == 3 && "/***/".contentEquals(text); //NOI18N } + if (token != null && token.id() == JavaTokenId.JAVADOC_COMMENT_LINE_RUN) { + CharSequence text = token.text(); + // check special case ///|\n + return offset == 3 && "///\n".contentEquals(text); //NOI18N + } return false; } diff --git a/java/java.editor.base/src/org/netbeans/modules/java/editor/base/javadoc/JavadocImports.java b/java/java.editor.base/src/org/netbeans/modules/java/editor/base/javadoc/JavadocImports.java index 07ccd639c0e0..2d91d7065f64 100644 --- a/java/java.editor.base/src/org/netbeans/modules/java/editor/base/javadoc/JavadocImports.java +++ b/java/java.editor.base/src/org/netbeans/modules/java/editor/base/javadoc/JavadocImports.java @@ -641,7 +641,7 @@ public static boolean isInsideReference(TokenSequence jdts, int } case OTHER_TEXT: isBeforeWS |= JavadocCompletionUtils.isWhiteSpace(jdt); - isBeforeWS |= JavadocCompletionUtils.isLineBreak(jdt); + isBeforeWS |= JavadocCompletionUtils.isLineBreak(jdts); if (isBeforeWS) { continue; } else { @@ -690,7 +690,9 @@ private static TokenSequence getJavadocTS(CompilationInfo javac, TokenSequence javadoc = null; TokenSequence ts = SourceUtils.getJavaTokenSequence(javac.getTokenHierarchy(), start); - if (ts.moveNext() && ts.token().id() == JavaTokenId.JAVADOC_COMMENT) { + if (ts.moveNext() && + (ts.token().id() == JavaTokenId.JAVADOC_COMMENT || + ts.token().id() == JavaTokenId.JAVADOC_COMMENT_LINE_RUN)) { javadoc = ts.embedded(JavadocTokenId.language()); } @@ -893,14 +895,14 @@ private static void insideTag(DocTreePath tag, JavadocContext jdctx, int caretOf cs = pos < cs.length() ? cs.subSequence(0, pos) : cs; if (JavadocCompletionUtils.isWhiteSpace(cs) - || JavadocCompletionUtils.isLineBreak(jdts.token(), pos)) { + || JavadocCompletionUtils.isLineBreak(jdts, pos)) { noPrefix = true; } else { // broken syntax return; } } else if (!(JavadocCompletionUtils.isWhiteSpace(jdts.token()) - || JavadocCompletionUtils.isLineBreak(jdts.token()))) { + || JavadocCompletionUtils.isLineBreak(jdts))) { // not java reference return; } else if (jdts.moveNext()) { diff --git a/java/java.editor.base/test/unit/src/org/netbeans/modules/java/editor/base/javadoc/JavadocCompletionUtilsTest.java b/java/java.editor.base/test/unit/src/org/netbeans/modules/java/editor/base/javadoc/JavadocCompletionUtilsTest.java index add1ca7dc20d..33926dc0b405 100644 --- a/java/java.editor.base/test/unit/src/org/netbeans/modules/java/editor/base/javadoc/JavadocCompletionUtilsTest.java +++ b/java/java.editor.base/test/unit/src/org/netbeans/modules/java/editor/base/javadoc/JavadocCompletionUtilsTest.java @@ -270,32 +270,32 @@ public void testIsLineBreak() throws Exception { TokenSequence jdts = JavadocCompletionUtils.findJavadocTokenSequence(info, offset); assertTrue(jdts.moveNext()); assertTrue(insertPointer(code, offset), - JavadocCompletionUtils.isLineBreak(jdts.token())); + JavadocCompletionUtils.isLineBreak(jdts)); offset += 1; jdts = JavadocCompletionUtils.findJavadocTokenSequence(info, offset); assertTrue(jdts.moveNext()); // token is INDENT assertFalse(insertPointer(code, offset), - JavadocCompletionUtils.isLineBreak(jdts.token())); + JavadocCompletionUtils.isLineBreak(jdts)); what = " \n"; offset = code.indexOf(what); jdts = JavadocCompletionUtils.findJavadocTokenSequence(info, offset); assertTrue(jdts.moveNext()); assertTrue(insertPointer(code, offset), - JavadocCompletionUtils.isLineBreak(jdts.token(), offset - jdts.offset())); + JavadocCompletionUtils.isLineBreak(jdts, offset - jdts.offset())); what = " * {*i"; offset = code.indexOf(what) + what.length() - 3; jdts = JavadocCompletionUtils.findJavadocTokenSequence(info, offset); assertTrue(jdts.moveNext()); assertFalse(insertPointer(code, offset), - JavadocCompletionUtils.isLineBreak(jdts.token())); + JavadocCompletionUtils.isLineBreak(jdts)); assertTrue(insertPointer(code, offset), - JavadocCompletionUtils.isLineBreak(jdts.token(), offset - jdts.offset())); + JavadocCompletionUtils.isLineBreak(jdts, offset - jdts.offset())); offset = code.indexOf(what); assertFalse(insertPointer(code, offset), - JavadocCompletionUtils.isLineBreak(jdts.token(), offset - jdts.offset())); + JavadocCompletionUtils.isLineBreak(jdts, offset - jdts.offset())); } public void testIsLineBreak2() throws Exception { @@ -319,10 +319,10 @@ public void testIsLineBreak2() throws Exception { assertTrue(jdts.moveNext()); assertTrue(jdts.token().id() == JavadocTokenId.OTHER_TEXT); assertFalse(insertPointer(code, jdts.offset() + jdts.token().length()), - JavadocCompletionUtils.isLineBreak(jdts.token())); + JavadocCompletionUtils.isLineBreak(jdts)); // test OTHER_TEXT(' * |{') assertTrue(insertPointer(code, jdts.offset() + jdts.token().length() - 1), - JavadocCompletionUtils.isLineBreak(jdts.token(), jdts.token().length() - 1)); + JavadocCompletionUtils.isLineBreak(jdts, jdts.token().length() - 1)); } public void testIsWhiteSpace() throws Exception { diff --git a/java/java.editor.base/test/unit/src/org/netbeans/modules/java/editor/base/javadoc/JavadocImportsTest.java b/java/java.editor.base/test/unit/src/org/netbeans/modules/java/editor/base/javadoc/JavadocImportsTest.java index e64ec4698b52..a880930c5850 100644 --- a/java/java.editor.base/test/unit/src/org/netbeans/modules/java/editor/base/javadoc/JavadocImportsTest.java +++ b/java/java.editor.base/test/unit/src/org/netbeans/modules/java/editor/base/javadoc/JavadocImportsTest.java @@ -207,6 +207,124 @@ public void testComputeReferencedElements() throws Exception { assertEquals(exp, sortedResult); } + public void testComputeReferencedElementsMarkdown() throws Exception { + String code = + """ + package p; + import java.io.IOException; + import java.util.Collections; + import java.util.List; + class C { + ///link1 {@link Runnable} + ///link3 {@linkplain Collections#binarySearch(java.util.List, Object) search} + ///{@link java. uncomplete reference} + ///unclosed link {@value Math#PI} + ///@see List + ///@throws IOException + void m() throws java.io.IOException { + } + /// + ///{@link Collections} + /// + int field; + /// {@link IOException + interface InnerInterface {} + /// {@link Collections} + @interface InnerAnnotationType {} + } + /// {@link Collections} + enum TopLevelEnum { + /** {@link Collections} */ E1 + } + """; + //TODO: does not work: + //unclosed link {@value Math#PI\n + prepareTest(code); + + // C.m() + TreePath member = findPath(code, "m() throws"); + assertNotNull(member); + List exp = Arrays.asList( + info.getElements().getTypeElement("java.lang.Runnable"), + info.getElements().getTypeElement("java.lang.Math"), + info.getElements().getTypeElement("java.lang.Object"), + info.getElements().getTypeElement("java.util.Collections"), + info.getElements().getTypeElement("java.util.List"), + info.getElements().getTypeElement("java.io.IOException") + ); + Collections.sort(exp, new ElementComparator()); + Set result = JavadocImports.computeReferencedElements(info, member); + assertNotNull(result); + List sortedResult = new ArrayList(result); + sortedResult.sort(new ElementComparator()); + assertEquals(exp, sortedResult); + + // C.field + member = findPath(code, "field;"); + assertNotNull(member); + exp = Arrays.asList( + info.getElements().getTypeElement("java.util.Collections") + ); + Collections.sort(exp, new ElementComparator()); + result = JavadocImports.computeReferencedElements(info, member); + assertNotNull(result); + sortedResult = new ArrayList(result); + sortedResult.sort(new ElementComparator()); + assertEquals(exp, sortedResult); + + // C.InnerInterface + member = findPath(code, "InnerInterface {"); + assertNotNull(member); + exp = Arrays.asList( + info.getElements().getTypeElement("java.io.IOException") + ); + Collections.sort(exp, new ElementComparator()); + result = JavadocImports.computeReferencedElements(info, member); + assertNotNull(result); + sortedResult = new ArrayList(result); + sortedResult.sort(new ElementComparator()); + assertEquals(exp, sortedResult); + + // C.InnerAnnotationType + member = findPath(code, "InnerAnnotationType {"); + assertNotNull(member); + exp = Arrays.asList( + info.getElements().getTypeElement("java.util.Collections") + ); + Collections.sort(exp, new ElementComparator()); + result = JavadocImports.computeReferencedElements(info, member); + assertNotNull(result); + sortedResult = new ArrayList(result); + sortedResult.sort(new ElementComparator()); + assertEquals(exp, sortedResult); + + // TopLevelEnum + member = findPath(code, "TopLevelEnum {"); + assertNotNull(member); + exp = Arrays.asList( + info.getElements().getTypeElement("java.util.Collections") + ); + Collections.sort(exp, new ElementComparator()); + result = JavadocImports.computeReferencedElements(info, member); + assertNotNull(result); + sortedResult = new ArrayList(result); + sortedResult.sort(new ElementComparator()); + assertEquals(exp, sortedResult); + + // TopLevelEnum.E1 + member = findPath(code, "E1\n"); + assertNotNull(member); + exp = Arrays.asList( + info.getElements().getTypeElement("java.util.Collections") + ); + Collections.sort(exp, new ElementComparator()); + result = JavadocImports.computeReferencedElements(info, member); + assertNotNull(result); + sortedResult = new ArrayList(result); + sortedResult.sort(new ElementComparator()); + assertEquals(exp, sortedResult); + } + public void testComputeTokensOfReferencedElements() throws Exception { String code = "package p;\n" + @@ -286,6 +404,87 @@ public void testComputeTokensOfReferencedElements() throws Exception { // assertEquals(toFind.toString(), exp, tokens); } + public void testComputeTokensOfReferencedElementsMarkdown() throws Exception { + String code = + """ + package p; + import java.util.Collections; + class C { + ///link1 {@link Runnable} + ///link2 {@link Collections#binarySearch(java.util.List, java.lang.Object) search} + ///{@link java. uncomplete reference} ///unclosed link {@value Math#PI} + ///@see java.util.Collections + ///@throws ThrowsUnresolved + /// + void m() throws java.io.IOException { + Collections.binarySearch(Collections.emptyList(), ""); + double pi = Math.PI; + } + } + """; + //TODO: does not work: + //unclosed link {@value Math#PI\n + prepareTest(code); + + TreePath where = findPath(code, "m() throws"); + assertNotNull(where); + TokenSequence jdts = JavadocCompletionUtils.findJavadocTokenSequence(info, null, info.getTrees().getElement(where)); + assertNotNull(jdts); + List exp; + + // toFind java.lang.Runnable + Element toFind = info.getElements().getTypeElement("java.lang.Runnable"); + assertNotNull(toFind); + List tokens = JavadocImports.computeTokensOfReferencedElements(info, where, toFind); + assertNotNull(toFind.toString(), tokens); + jdts.move(code.indexOf("Runnable", code.indexOf("link1"))); + assertTrue(jdts.moveNext()); + exp = Arrays.asList(jdts.token()); + assertEquals(toFind.toString(), exp, tokens); + + // toFind java.util.Collections + toFind = info.getElements().getTypeElement("java.util.Collections"); + assertNotNull(toFind); + tokens = JavadocImports.computeTokensOfReferencedElements(info, where, toFind); + assertNotNull(toFind.toString(), tokens); + exp = new ArrayList(); + jdts.move(code.indexOf("Collections", code.indexOf("link2"))); + assertTrue(jdts.moveNext()); + exp.add(jdts.token()); + jdts.move(code.indexOf("Collections", code.indexOf("///@see"))); + assertTrue(jdts.moveNext()); + exp.add(jdts.token()); + System.err.println("exp:"); + for (Token e : exp) { + System.err.println(e.text()); + } + System.err.println("tokens:"); + for (Token e : tokens) { + System.err.println(e.text()); + } + assertEquals(toFind.toString(), exp, tokens); + + // toFind Math#PI + toFind = findElement(code, "PI;\n"); + assertNotNull(toFind); + tokens = JavadocImports.computeTokensOfReferencedElements(info, where, toFind); + assertNotNull(toFind.toString(), tokens); + jdts.move(code.indexOf("PI", code.indexOf("unclosed link"))); + assertTrue(jdts.moveNext()); + exp = Arrays.asList(jdts.token()); + assertEquals(toFind.toString(), exp, tokens); + + // toFind Collections#binarySearch + toFind = findElement(code, "binarySearch(Collections.emptyList()"); + assertNotNull(toFind); + tokens = JavadocImports.computeTokensOfReferencedElements(info, where, toFind); + assertNotNull(toFind.toString(), tokens); + jdts.move(code.indexOf("binarySearch", code.indexOf("link2"))); + assertTrue(jdts.moveNext()); + exp = Arrays.asList(jdts.token()); +// assertEquals(toFind.toString(), exp, tokens); + } + public void testComputeTokensOfReferencedElementsForParams() throws Exception { String code = "package p;\n" + diff --git a/java/java.editor.base/test/unit/src/org/netbeans/modules/java/editor/base/javadoc/JavadocTestSupport.java b/java/java.editor.base/test/unit/src/org/netbeans/modules/java/editor/base/javadoc/JavadocTestSupport.java index 7c153124956a..8770a3906721 100644 --- a/java/java.editor.base/test/unit/src/org/netbeans/modules/java/editor/base/javadoc/JavadocTestSupport.java +++ b/java/java.editor.base/test/unit/src/org/netbeans/modules/java/editor/base/javadoc/JavadocTestSupport.java @@ -20,7 +20,9 @@ package org.netbeans.modules.java.editor.base.javadoc; import java.io.File; +import java.util.ArrayList; import java.util.Enumeration; +import java.util.List; import javax.swing.text.StyledDocument; import org.netbeans.api.editor.mimelookup.MimePath; import org.netbeans.api.editor.mimelookup.test.MockMimeLookup; @@ -62,10 +64,10 @@ protected void setUp() throws Exception { super.setUp(); MockMimeLookup.setInstances(MimePath.parse("text/x-java"), new JavaKit()); - SourceUtilsTestUtil.prepareTest(new String[0], new Object[] { - new Pool(), - new MockMimeLookup(), - }); + List services = new ArrayList<>(); + services.add(new Pool()); + services.add(new MockMimeLookup()); + SourceUtilsTestUtil.prepareTest(new String[0], services.toArray()); FileUtil.setMIMEType("java", "text/x-java"); if (cache == null) { @@ -115,7 +117,11 @@ protected void prepareTest(String code) throws Exception { assertNotNull(info); assertTrue(info.getDiagnostics().toString(), info.getDiagnostics().isEmpty()); } - + + protected Object[] additionalServices() { + return new Object[0]; + } + /** * Inserts a marker '|' to string {@code s} on position {@code pos}. Useful * for assert's debug messages diff --git a/java/java.editor/nbproject/project.properties b/java/java.editor/nbproject/project.properties index 914e09646b80..9c667f21de38 100644 --- a/java/java.editor/nbproject/project.properties +++ b/java/java.editor/nbproject/project.properties @@ -19,7 +19,7 @@ javadoc.title=Java Editor spec.version.base=2.94.0 test.qa-functional.cp.extra=${editor.dir}/modules/org-netbeans-modules-editor-fold.jar -javac.source=1.8 +javac.release=17 #test.unit.cp.extra= #test.unit.run.cp.extra=${o.n.core.dir}/core/core.jar:${o.n.core.dir}/lib/boot.jar:${libs.xerces.dir}/modules/ext/xerces-2.6.2.jar:${libs.xerces.dir}/modules/ext/xml-commons-dom-ranges-1.0.b2.jar:${retouche/javacimpl.dir}/modules/ext/javac-impl.jar diff --git a/java/java.editor/src/org/netbeans/modules/editor/java/GoToSupport.java b/java/java.editor/src/org/netbeans/modules/editor/java/GoToSupport.java index 8a60126a7266..3e8b08e4e7a2 100644 --- a/java/java.editor/src/org/netbeans/modules/editor/java/GoToSupport.java +++ b/java/java.editor/src/org/netbeans/modules/editor/java/GoToSupport.java @@ -408,7 +408,7 @@ public static Context resolveContext(CompilationInfo controller, Document doc, i boolean insideImportStmt = false; TreePath path = controller.getTreeUtilities().pathFor(exactOffset); - if (token[0] != null && token[0].id() == JavaTokenId.JAVADOC_COMMENT) { + if (token[0] != null && (token[0].id() == JavaTokenId.JAVADOC_COMMENT || token[0].id() == JavaTokenId.JAVADOC_COMMENT_LINE_RUN)) { el = JavadocImports.findReferencedElement(controller, offset); } else { path = adjustPathForModuleName(path); @@ -662,7 +662,7 @@ public void run() { Token t = ts.token(); - if (JavaTokenId.JAVADOC_COMMENT == t.id()) { + if (JavaTokenId.JAVADOC_COMMENT == t.id() || JavaTokenId.JAVADOC_COMMENT_LINE_RUN == t.id()) { // javadoc hyperlinking (references + param names) TokenSequence jdts = ts.embedded(JavadocTokenId.language()); if (JavadocImports.isInsideReference(jdts, offset) || JavadocImports.isInsideParamName(jdts, offset)) { diff --git a/java/java.editor/src/org/netbeans/modules/editor/java/JavaCompletionCollector.java b/java/java.editor/src/org/netbeans/modules/editor/java/JavaCompletionCollector.java index cd61d9094104..a68dc91894fa 100644 --- a/java/java.editor/src/org/netbeans/modules/editor/java/JavaCompletionCollector.java +++ b/java/java.editor/src/org/netbeans/modules/editor/java/JavaCompletionCollector.java @@ -1319,6 +1319,7 @@ private static TokenSequence findLastNonWhitespaceToken(TokenSequen case LINE_COMMENT: case BLOCK_COMMENT: case JAVADOC_COMMENT: + case JAVADOC_COMMENT_LINE_RUN: break; default: return ts; diff --git a/java/java.editor/src/org/netbeans/modules/editor/java/JavaCompletionItem.java b/java/java.editor/src/org/netbeans/modules/editor/java/JavaCompletionItem.java index d8a91390a419..40555e740711 100644 --- a/java/java.editor/src/org/netbeans/modules/editor/java/JavaCompletionItem.java +++ b/java/java.editor/src/org/netbeans/modules/editor/java/JavaCompletionItem.java @@ -4621,6 +4621,7 @@ private static TokenSequence findLastNonWhitespaceToken(TokenSequen case LINE_COMMENT: case BLOCK_COMMENT: case JAVADOC_COMMENT: + case JAVADOC_COMMENT_LINE_RUN: break; default: return ts; diff --git a/java/java.editor/src/org/netbeans/modules/editor/java/JavaKit.java b/java/java.editor/src/org/netbeans/modules/editor/java/JavaKit.java index 5ac389c224c5..adefaee33d02 100644 --- a/java/java.editor/src/org/netbeans/modules/editor/java/JavaKit.java +++ b/java/java.editor/src/org/netbeans/modules/editor/java/JavaKit.java @@ -458,6 +458,7 @@ public void insert(MutableContext context) throws BadLocationException { if (isJavadocTouched) { blockCommentComplete(doc, dotPos, context); } + TypingCompletion.javadocLineRunCompletion(context); } } diff --git a/java/java.editor/src/org/netbeans/modules/editor/java/TypingCompletion.java b/java/java.editor/src/org/netbeans/modules/editor/java/TypingCompletion.java index 5f2172b49d88..0682f52f5f6f 100644 --- a/java/java.editor/src/org/netbeans/modules/editor/java/TypingCompletion.java +++ b/java/java.editor/src/org/netbeans/modules/editor/java/TypingCompletion.java @@ -563,6 +563,21 @@ private static boolean isClosedBlockComment(CharSequence txt, int pos) { return false; } + static boolean javadocLineRunCompletion(TypedBreakInterceptor.MutableContext context) { + TokenSequence ts = javaTokenSequence(context, false); + if (ts == null) { + return false; + } + int dotPosition = context.getCaretOffset(); + ts.move(dotPosition); + if (!((ts.moveNext() || ts.movePrevious()) && ts.token().id() == JavaTokenId.JAVADOC_COMMENT_LINE_RUN)) { + return false; + } + context.setText("\n///", -1, 4, 0, 4); + + return false; + } + private static boolean isAtRowEnd(CharSequence txt, int pos) { int length = txt.length(); for (int i = pos; i < length; i++) { diff --git a/java/java.editor/src/org/netbeans/modules/editor/java/Utilities.java b/java/java.editor/src/org/netbeans/modules/editor/java/Utilities.java index da31e094c582..6ccb51a51e3a 100644 --- a/java/java.editor/src/org/netbeans/modules/editor/java/Utilities.java +++ b/java/java.editor/src/org/netbeans/modules/editor/java/Utilities.java @@ -288,6 +288,7 @@ public static boolean isJavaContext(final Document doc, final int offset, final case INVALID_COMMENT_END: case JAVADOC_COMMENT: case LINE_COMMENT: + case JAVADOC_COMMENT_LINE_RUN: case BLOCK_COMMENT: return false; case STRING_LITERAL: diff --git a/java/java.editor/src/org/netbeans/modules/java/editor/javadoc/JavadocCompletionTask.java b/java/java.editor/src/org/netbeans/modules/java/editor/javadoc/JavadocCompletionTask.java index eac6a5607c25..c328d4ae418d 100644 --- a/java/java.editor/src/org/netbeans/modules/java/editor/javadoc/JavadocCompletionTask.java +++ b/java/java.editor/src/org/netbeans/modules/java/editor/javadoc/JavadocCompletionTask.java @@ -64,6 +64,7 @@ import javax.swing.text.Document; import org.netbeans.api.annotations.common.NonNull; import org.netbeans.api.annotations.common.NullAllowed; +import org.netbeans.api.java.lexer.JavaTokenId; import org.netbeans.api.java.lexer.JavadocTokenId; import org.netbeans.api.java.source.ClassIndex; import org.netbeans.api.java.source.ClasspathInfo; @@ -84,6 +85,7 @@ import org.netbeans.modules.parsing.api.Source; import org.netbeans.modules.parsing.api.UserTask; import org.netbeans.modules.parsing.spi.Parser; +import org.openide.util.Pair; public class JavadocCompletionTask extends UserTask { @@ -188,16 +190,20 @@ private void analyzeContext(JavadocContext jdctx) { return; } jdts.move(this.caretOffset); + JavadocTokenId javadocId; if (!jdts.moveNext() && !jdts.movePrevious()) { // XXX solve /***/ // provide block tags, inline tags, html - return; - } - if (this.caretOffset - jdts.offset() == 0) { - // if position in token == 0 resolve CC according to previous token - jdts.movePrevious(); + // XXX: for Markdown, continuing + javadocId = JavadocTokenId.OTHER_TEXT; + } else { + if (this.caretOffset - jdts.offset() == 0) { + // if position in token == 0 resolve CC according to previous token + jdts.movePrevious(); + } + javadocId = jdts.token().id(); } - switch (jdts.token().id()) { + switch (javadocId) { case TAG: resolveTagToken(jdctx); break; @@ -265,7 +271,9 @@ void resolveInlineTag(DocTreePath tag, JavadocContext jdctx) { private int skipWhitespacesBackwards(final JavadocContext jdctx, final int offset) { if (jdctx.jdts.move(offset) == 0 || !jdctx.jdts.moveNext()) { - jdctx.jdts.movePrevious(); + if (!jdctx.jdts.movePrevious()) { + return offset; + } } do { Token t = jdctx.jdts.token(); @@ -415,13 +423,13 @@ private void insideSeeTag(DocTreePath tag, JavadocContext jdctx) { int pos = caretOffset - jdts.offset(); CharSequence cs = jdts.token().text(); cs = pos < cs.length() ? cs.subSequence(0, pos) : cs; - if (JavadocCompletionUtils.isWhiteSpace(cs) || JavadocCompletionUtils.isLineBreak(jdts.token(), pos)) { + if (JavadocCompletionUtils.isWhiteSpace(cs) || JavadocCompletionUtils.isLineBreak(jdts, pos)) { noPrefix = true; } else { // broken syntax return; } - } else if (!(JavadocCompletionUtils.isWhiteSpace(jdts.token()) || JavadocCompletionUtils.isLineBreak(jdts.token()))) { + } else if (!(JavadocCompletionUtils.isWhiteSpace(jdts.token()) || JavadocCompletionUtils.isLineBreak(jdts))) { // not java reference return; } else if (jdts.moveNext()) { @@ -519,7 +527,7 @@ private void insideParamTag(DocTreePath tag, JavadocContext jdctx) { int pos = caretOffset - jdts.offset(); CharSequence cs = jdts.token().text(); cs = pos < cs.length() ? cs.subSequence(0, pos) : cs; - if (JavadocCompletionUtils.isWhiteSpace(cs) || JavadocCompletionUtils.isLineBreak(jdts.token(), pos)) { + if (JavadocCompletionUtils.isWhiteSpace(cs) || JavadocCompletionUtils.isLineBreak(jdts, pos)) { // none prefix anchorOffset = caretOffset; completeParamName(tag, "", caretOffset, jdctx); // NOI18N @@ -1226,11 +1234,10 @@ private boolean startsWith(String theString, String prefix) { void resolveOtherText(JavadocContext jdctx, TokenSequence jdts) { Token token = jdts.token(); - assert token != null; - assert token.id() == JavadocTokenId.OTHER_TEXT; - CharSequence text = token.text(); - int pos = caretOffset - jdts.offset(); - DocTreePath tag = getTag(jdctx, caretOffset); + assert token == null || token.id() == JavadocTokenId.OTHER_TEXT; + CharSequence text = token == null ? "" : token.text(); + int pos = token == null ? 0 : caretOffset - jdts.offset(); + DocTreePath tag = token == null ? null : getTag(jdctx, caretOffset); if (pos > 0 && pos <= text.length() && text.charAt(pos - 1) == '{') { if (tag != null && !JavadocCompletionUtils.isBlockTag(tag)) { @@ -1244,10 +1251,10 @@ void resolveOtherText(JavadocContext jdctx, TokenSequence jdts) } if (tag != null) { insideTag(tag, jdctx); - if (JavadocCompletionUtils.isBlockTag(tag) && JavadocCompletionUtils.isLineBreak(token, pos)) { + if (JavadocCompletionUtils.isBlockTag(tag) && JavadocCompletionUtils.isLineBreak(jdts, pos)) { resolveBlockTag(null, jdctx); } - } else if (JavadocCompletionUtils.isLineBreak(token, pos)) { + } else if (JavadocCompletionUtils.isLineBreak(jdts, pos)) { resolveBlockTag(null, jdctx); } } @@ -1336,6 +1343,7 @@ private static class JavadocContext { private DocCommentTree comment; private DocSourcePositions positions; private TokenSequence jdts; + private TokenSequence javats; private Document doc; private ReferencesCount count; private TreePath javadocFor; diff --git a/java/java.editor/test/unit/src/org/netbeans/modules/editor/java/GoToSupportTest.java b/java/java.editor/test/unit/src/org/netbeans/modules/editor/java/GoToSupportTest.java index 8deac6817b8f..a506190feb74 100644 --- a/java/java.editor/test/unit/src/org/netbeans/modules/editor/java/GoToSupportTest.java +++ b/java/java.editor/test/unit/src/org/netbeans/modules/editor/java/GoToSupportTest.java @@ -1145,6 +1145,71 @@ public void testBindingVarToolTip() throws Exception { assertEquals("java.lang.String str", tooltip); } + public void testJavadoc() throws Exception { + final boolean[] wasCalled = new boolean[1]; + final String code = """ + package test; + /** + * @see Obj|ect + */ + public class Test { + } + """; + + performTest(code, new UiUtilsCaller() { + @Override public boolean open(FileObject fo, int pos) { + fail("Should not be called."); + return true; + } + + @Override public void beep(boolean goToSource, boolean goToJavadoc) { + fail("Should not be called."); + } + @Override public boolean open(ClasspathInfo info, ElementHandle el, String fileName) { + assertEquals("java.lang.Object", el.getBinaryName()); + wasCalled[0] = true; + return true; + } + @Override public void warnCannotOpen(String displayName) { + fail("Should not be called."); + } + }, false, false); + + assertTrue(wasCalled[0]); + } + + public void testMarkdownJavadoc() throws Exception { + final boolean[] wasCalled = new boolean[1]; + this.sourceLevel = "23"; + final String code = """ + package test; + ///@see Obj|ect + public class Test { + } + """; + + performTest(code, new UiUtilsCaller() { + @Override public boolean open(FileObject fo, int pos) { + fail("Should not be called."); + return true; + } + + @Override public void beep(boolean goToSource, boolean goToJavadoc) { + fail("Should not be called."); + } + @Override public boolean open(ClasspathInfo info, ElementHandle el, String fileName) { + assertEquals("java.lang.Object", el.getBinaryName()); + wasCalled[0] = true; + return true; + } + @Override public void warnCannotOpen(String displayName) { + fail("Should not be called."); + } + }, false, false); + + assertTrue(wasCalled[0]); + } + private String sourceLevel = "1.5"; private FileObject source; diff --git a/java/java.editor/test/unit/src/org/netbeans/modules/editor/java/TypingCompletionUnitTest.java b/java/java.editor/test/unit/src/org/netbeans/modules/editor/java/TypingCompletionUnitTest.java index a0713d608f6c..99efb7f02484 100644 --- a/java/java.editor/test/unit/src/org/netbeans/modules/editor/java/TypingCompletionUnitTest.java +++ b/java/java.editor/test/unit/src/org/netbeans/modules/editor/java/TypingCompletionUnitTest.java @@ -1419,6 +1419,22 @@ public void testX() throws Exception { ctx.assertDocumentTextEquals("{"); } + public void testJavadocLineRun() { + Context ctx = new Context(new JavaKit(), + """ + class Test { + ///| + } + """); + ctx.typeChar('\n'); + ctx.assertDocumentTextEquals(""" + class Test { + /// + ///| + } + """); + } + private boolean isInsideString(String code) throws BadLocationException { int pos = code.indexOf('|'); diff --git a/java/java.editor/test/unit/src/org/netbeans/modules/java/editor/javadoc/JavadocCompletionQueryTest.java b/java/java.editor/test/unit/src/org/netbeans/modules/java/editor/javadoc/JavadocCompletionQueryTest.java index 8bf90d619fab..48f00daca355 100644 --- a/java/java.editor/test/unit/src/org/netbeans/modules/java/editor/javadoc/JavadocCompletionQueryTest.java +++ b/java/java.editor/test/unit/src/org/netbeans/modules/java/editor/javadoc/JavadocCompletionQueryTest.java @@ -23,10 +23,14 @@ import java.util.Arrays; import java.util.Collections; import java.util.List; +import javax.swing.event.ChangeListener; import org.netbeans.junit.NbTestSuite; import org.netbeans.spi.editor.completion.CompletionItem; import org.netbeans.spi.editor.completion.CompletionProvider; import org.netbeans.modules.java.editor.base.javadoc.JavadocTestSupport; +import org.netbeans.spi.java.queries.SourceLevelQueryImplementation2; +import org.openide.filesystems.FileObject; +import org.openide.util.lookup.ServiceProvider; /** * @@ -492,7 +496,7 @@ public void testValue2() throws Exception { "package p;\n" + "class Clazz {\n" + " /**\n" + - " * {@value Mat|\n" + + " * {@value Math|\n" + " */\n" + " Clazz() {\n" + " }\n" + @@ -601,8 +605,244 @@ public void testSummaryCompletionForMethod() throws Exception { "}\n"; performCompletionTest(code, "@summary:"); } - - + + public void testBlockTagsCompletionInMarkdown() throws Exception { + String code = + """ + package p; + class Clazz { + /// + /// | + /// + void method(int p1, int p2) { + } + } + """; + + performCompletionTest(code, "@deprecated:", "@exception:", "@hidden:", "@param:", "@return:", "@see:", "@serialData:", "@since:", "@throws:"); + } + + public void testBlockTagsCompletionInMarkdown2() throws Exception { + String code = + """ + package p; + class Clazz { + /// + ///| + /// + void method(int p1, int p2) { + } + } + """; + + performCompletionTest(code, "@deprecated:", "@exception:", "@hidden:", "@param:", "@return:", "@see:", "@serialData:", "@since:", "@throws:"); + } + + public void testBlockTagsCompletionInMarkdown3() throws Exception { + String code = + """ + package p; + class Clazz { + /// + ///|\s + /// + void method(int p1, int p2) { + } + } + """; + + performCompletionTest(code, "@deprecated:", "@exception:", "@hidden:", "@param:", "@return:", "@see:", "@serialData:", "@since:", "@throws:"); + } + + public void testBlockTagsCompletionInMarkdownStart() throws Exception { + String code = + """ + package p; + class Clazz { + ///| + void method(int p1, int p2) { + } + } + """; + + performCompletionTest(code, "@deprecated:", "@exception:", "@hidden:", "@param:", "@return:", "@see:", "@serialData:", "@since:", "@throws:"); + } + + public void testSeeMarkdown1() throws Exception { + String code = + """ + package p; + class Clazz { + /// + /// @see CharSequence#le| + /// + Clazz() { + } + } + """; + + performCompletionTest(code, "public abstract int length()"); + } + + public void testSeeMarkdown2() throws Exception { + String code = + """ + package p; + class Clazz { + /// + /// @see | + /// + Clazz() { + } + } + """; + + performCompletionTest(code, null, "String", "Clazz"); + } + + public void testSeeMarkdown3() throws Exception { + String code = + """ + package p; + class Clazz { + ///@param i i + ///@see | + /// + Clazz(int i) { + } + } + """; + + performCompletionTest(code, null, "String", "Clazz"); + } + + public void testParamMarkdown() throws Exception { + String code = + """ + package p; + class Clazz { + /// + /// @param | + /// + void method(int p1, int p2) { + } + } + """; + + performCompletionTest(code, "p1:", "p2:"); + } + + public void testJavadocOldStart1() throws Exception { + String code = + """ + package p; + class Clazz { + /**| */ + void method(int p1, int p2) { + } + } + """; + + performCompletionTest(code, "@deprecated:", "@exception:", "@hidden:", "@param:", "@return:", "@see:", "@serialData:", "@since:", "@throws:"); + } + + public void testJavadocOldStart2() throws Exception { + String code = + """ + package p; + class Clazz { + /**@s| */ + void method(int p1, int p2) { + } + } + """; + + performCompletionTest(code, "@see:", "@serialData:", "@since:"); + } + + public void testJavadocOldStart3() throws Exception { + String code = + """ + package p; + class Clazz { + /**@param | */ + void method(int p1, int p2) { + } + } + """; + + performCompletionTest(code, "p1:", "p2:"); + } + + public void testJavadocOldStart4() throws Exception { + String code = + """ + package p; + class Clazz { + /**@see | */ + void method(int p1, int p2) { + } + } + """; + + performCompletionTest(code, null, "String", "Clazz"); + } + + public void testJavadocMarkdownStart1() throws Exception { + String code = + """ + package p; + class Clazz { + ///| + void method(int p1, int p2) { + } + } + """; + + performCompletionTest(code, "@deprecated:", "@exception:", "@hidden:", "@param:", "@return:", "@see:", "@serialData:", "@since:", "@throws:"); + } + + public void testJavadocMarkdownStart2() throws Exception { + String code = + """ + package p; + class Clazz { + ///@s| + void method(int p1, int p2) { + } + } + """; + + performCompletionTest(code, "@see:", "@serialData:", "@since:"); + } + + public void testJavadocMarkdownStart3() throws Exception { + String code = + """ + package p; + class Clazz { + ///@param | + void method(int p1, int p2) { + } + } + """; + + performCompletionTest(code, "p1:", "p2:"); + } + + public void testJavadocMarkdownStart4() throws Exception { + String code = + """ + package p; + class Clazz { + ///@see | + void method(int p1, int p2) { + } + } + """; + + performCompletionTest(code, null, "String", "Clazz"); + } private static String stripHTML(String from) { StringBuilder result = new StringBuilder(); @@ -647,4 +887,30 @@ private void performCompletionTest(String code, String... golden) throws Excepti assertEquals(goldenList, resultStrings); } } + + @Override + protected Object[] additionalServices() { + return new Object[] { + new SourceLevelQueryImplementation2() { + + @Override + public Result getSourceLevel(FileObject javaFile) { + return new Result() { + @Override + public String getSourceLevel() { + return "23"; + } + + @Override + public void addChangeListener(ChangeListener listener) { + } + + @Override + public void removeChangeListener(ChangeListener listener) { + } + }; + } + } + }; + } } diff --git a/java/java.lexer/nbproject/project.properties b/java/java.lexer/nbproject/project.properties index 4ea9ce2e2612..36e48fd27c57 100644 --- a/java/java.lexer/nbproject/project.properties +++ b/java/java.lexer/nbproject/project.properties @@ -17,7 +17,7 @@ is.autoload=true javac.compilerargs=-Xlint:unchecked -javac.source=1.8 +javac.release=17 javadoc.title=Java Lexer API javadoc.apichanges=${basedir}/apichanges.xml diff --git a/java/java.lexer/src/org/netbeans/api/java/lexer/JavaTokenId.java b/java/java.lexer/src/org/netbeans/api/java/lexer/JavaTokenId.java index 17602a5e400a..25738a8da1ae 100644 --- a/java/java.lexer/src/org/netbeans/api/java/lexer/JavaTokenId.java +++ b/java/java.lexer/src/org/netbeans/api/java/lexer/JavaTokenId.java @@ -193,6 +193,7 @@ public enum JavaTokenId implements TokenId { LINE_COMMENT(null, "comment"), // Token includes ending new-line BLOCK_COMMENT(null, "comment"), JAVADOC_COMMENT(null, "comment"), + JAVADOC_COMMENT_LINE_RUN(null, "comment"), // A run of "markdown" javadoc comments, includes ending new-line // Errors INVALID_COMMENT_END("*/", "error"), @@ -262,6 +263,9 @@ protected LanguageEmbedding embedding( case JAVADOC_COMMENT: return LanguageEmbedding.create(JavadocTokenId.language(), 3, (token.partType() == PartType.COMPLETE) ? 2 : 0); + case JAVADOC_COMMENT_LINE_RUN: + return LanguageEmbedding.create(JavadocTokenId.language(), 3, + (token.partType() == PartType.COMPLETE) ? 1 : 0); case STRING_LITERAL: return LanguageEmbedding.create(JavaStringTokenId.language(), 1, (token.partType() == PartType.COMPLETE) ? 1 : 0); diff --git a/java/java.lexer/src/org/netbeans/lib/java/lexer/JavaLexer.java b/java/java.lexer/src/org/netbeans/lib/java/lexer/JavaLexer.java index 49c32619b467..438b8b84ffb3 100644 --- a/java/java.lexer/src/org/netbeans/lib/java/lexer/JavaLexer.java +++ b/java/java.lexer/src/org/netbeans/lib/java/lexer/JavaLexer.java @@ -321,6 +321,13 @@ public Token nextToken() { case '/': switch (nextChar()) { case '/': // in single-line comment + switch (nextChar()) { + case '/': return finishJavadocLineRun(); + case '\r': consumeNewline(); + case '\n': + case EOF: + return token(JavaTokenId.LINE_COMMENT); + } while (true) switch (nextChar()) { case '\r': consumeNewline(); @@ -1428,6 +1435,34 @@ private Token finishFloatExponent() { return token(JavaTokenId.DOUBLE_LITERAL); } } + + private Token finishJavadocLineRun() { + while (true) { + //finish current line: + LINE: while (true) { + switch (nextChar()) { + case '\r': consumeNewline(); + case '\n': break LINE; + case EOF: + return token(JavaTokenId.JAVADOC_COMMENT_LINE_RUN); + } + } + + //at the next line, if it starts with "///", include it in the run, + //otherwise finish the run: + int mark = input.readLength(); + int c; + + while (Character.isWhitespace(c = nextChar()) && c != '\r' && c != '\n' && c != EOF) + ; + + if (c != '/' || nextChar() != '/' || nextChar() != '/') { + input.backup(input.readLengthEOF()- mark); + + return token(JavaTokenId.JAVADOC_COMMENT_LINE_RUN); + } + } + } private Token token(JavaTokenId id) { return token(id, PartType.COMPLETE); diff --git a/java/java.lexer/src/org/netbeans/lib/java/lexer/JavadocLexer.java b/java/java.lexer/src/org/netbeans/lib/java/lexer/JavadocLexer.java index fa7189b48331..c6ab507fee3f 100644 --- a/java/java.lexer/src/org/netbeans/lib/java/lexer/JavadocLexer.java +++ b/java/java.lexer/src/org/netbeans/lib/java/lexer/JavadocLexer.java @@ -194,6 +194,20 @@ private Token otherText(int ch) { leftbr = false; newline = false; break; + case '/': + //TODO: check comment type? + if (newline) { + if (input.read() == '/') { + if (input.read() == '/') { + break; + } else { + input.backup(1); + } + } else { + input.backup(1); + } + newline = false; //for fall-through: + } case '*': if (newline) { break; diff --git a/java/java.lexer/test/unit/src/org/netbeans/lib/java/lexer/JavaLexerBatchTest.java b/java/java.lexer/test/unit/src/org/netbeans/lib/java/lexer/JavaLexerBatchTest.java index 6402fc5c348f..bf4e0b71a8c2 100644 --- a/java/java.lexer/test/unit/src/org/netbeans/lib/java/lexer/JavaLexerBatchTest.java +++ b/java/java.lexer/test/unit/src/org/netbeans/lib/java/lexer/JavaLexerBatchTest.java @@ -812,4 +812,83 @@ public void testTemplates2() { assertFalse(ts.moveNext()); } + public void testMarkdown1() { + String text = """ + ///test + ///@see second line + ///third + + ///another run + ///another line + + """; + InputAttributes attr = new InputAttributes(); + TokenHierarchy hi = TokenHierarchy.create(text, false, JavaTokenId.language(), EnumSet.noneOf(JavaTokenId.class), attr); + TokenSequence ts = hi.tokenSequence(); + + LexerTestUtilities.assertNextTokenEquals(ts, + JavaTokenId.JAVADOC_COMMENT_LINE_RUN, + "///test\n" + + "///@see second line\n" + + "///third\n"); + LexerTestUtilities.assertNextTokenEquals(ts, JavaTokenId.WHITESPACE, "\n"); + LexerTestUtilities.assertNextTokenEquals(ts, + JavaTokenId.JAVADOC_COMMENT_LINE_RUN, + "///another run\n" + + "///another line\n"); + LexerTestUtilities.assertNextTokenEquals(ts, JavaTokenId.WHITESPACE, "\n"); + assertFalse(ts.moveNext()); + } + + public void testMarkdown2() { + String text = """ + ///test + ///@see second line + ///third + + ///another run + ///another line + """; + InputAttributes attr = new InputAttributes(); + TokenHierarchy hi = TokenHierarchy.create(text, false, JavaTokenId.language(), EnumSet.noneOf(JavaTokenId.class), attr); + TokenSequence ts = hi.tokenSequence(); + + LexerTestUtilities.assertNextTokenEquals(ts, + JavaTokenId.JAVADOC_COMMENT_LINE_RUN, + "///test\n" + + "///@see second line\n" + + "///third\n"); + LexerTestUtilities.assertNextTokenEquals(ts, JavaTokenId.WHITESPACE, "\n"); + LexerTestUtilities.assertNextTokenEquals(ts, + JavaTokenId.JAVADOC_COMMENT_LINE_RUN, + "///another run\n" + + "///another line\n"); + assertFalse(ts.moveNext()); + } + + public void testMarkdown3() { + String text = """ + ///test + ///@see second line + ///third + + ///another run + ///another line"""; + InputAttributes attr = new InputAttributes(); + TokenHierarchy hi = TokenHierarchy.create(text, false, JavaTokenId.language(), EnumSet.noneOf(JavaTokenId.class), attr); + TokenSequence ts = hi.tokenSequence(); + + LexerTestUtilities.assertNextTokenEquals(ts, + JavaTokenId.JAVADOC_COMMENT_LINE_RUN, + "///test\n" + + "///@see second line\n" + + "///third\n"); + LexerTestUtilities.assertNextTokenEquals(ts, JavaTokenId.WHITESPACE, "\n"); + LexerTestUtilities.assertNextTokenEquals(ts, + JavaTokenId.JAVADOC_COMMENT_LINE_RUN, + "///another run\n" + + "///another line"); + assertFalse(ts.moveNext()); + } + } diff --git a/java/java.lexer/test/unit/src/org/netbeans/lib/java/lexer/JavadocLexerTest.java b/java/java.lexer/test/unit/src/org/netbeans/lib/java/lexer/JavadocLexerTest.java index 72668f24a651..c0859ba2f826 100644 --- a/java/java.lexer/test/unit/src/org/netbeans/lib/java/lexer/JavadocLexerTest.java +++ b/java/java.lexer/test/unit/src/org/netbeans/lib/java/lexer/JavadocLexerTest.java @@ -143,6 +143,20 @@ public void test233097b() { LexerTestUtilities.assertNextTokenEquals(ts, JavadocTokenId.OTHER_TEXT, "}"); } + public void testMarkdown() { + String text = "///@see\n/// @see"; + + TokenHierarchy hi = TokenHierarchy.create(text, JavadocTokenId.language()); + TokenSequence ts = hi.tokenSequence(); + + LexerTestUtilities.assertNextTokenEquals(ts, JavadocTokenId.OTHER_TEXT, "///"); + LexerTestUtilities.assertNextTokenEquals(ts, JavadocTokenId.TAG, "@see"); + LexerTestUtilities.assertNextTokenEquals(ts, JavadocTokenId.OTHER_TEXT, "\n/// "); + LexerTestUtilities.assertNextTokenEquals(ts, JavadocTokenId.TAG, "@see"); + + assertFalse(ts.moveNext()); + } + // public void testModification1() throws Exception { // PlainDocument doc = new PlainDocument(); // doc.putProperty(Language.class, JavadocTokenId.language()); diff --git a/java/java.lsp.server/licenseinfo.xml b/java/java.lsp.server/licenseinfo.xml index fc390b64bbc9..001c83187ebd 100644 --- a/java/java.lsp.server/licenseinfo.xml +++ b/java/java.lsp.server/licenseinfo.xml @@ -50,4 +50,9 @@ + + vscode/language-configuration.json + + + diff --git a/java/java.lsp.server/vscode/language-configuration.json b/java/java.lsp.server/vscode/language-configuration.json new file mode 100644 index 000000000000..c6b69e53360e --- /dev/null +++ b/java/java.lsp.server/vscode/language-configuration.json @@ -0,0 +1,111 @@ +{ + "comments": { + "lineComment": "//", + "blockComment": [ "/*", "*/" ] + }, + "brackets": [ + ["{", "}"], + ["[", "]"], + ["(", ")"] + ], + "autoClosingPairs": [ + ["{", "}"], + ["[", "]"], + ["(", ")"], + { "open": "\"", "close": "\"", "notIn": ["string"] }, + { "open": "'", "close": "'", "notIn": ["string"] }, + { "open": "/**", "close": " */", "notIn": ["string"] } + ], + "surroundingPairs": [ + ["{", "}"], + ["[", "]"], + ["(", ")"], + ["\"", "\""], + ["'", "'"], + ["<", ">"] + ], + "folding": { + "markers": { + "start": "^\\s*//\\s*(?:(?:#?region\\b)|(?:))" + } + }, + "onEnterRules": [ + { + // e.g. /** | */ + "beforeText": { + "pattern": "^\\s*/\\*\\*(?!/)([^\\*]|\\*(?!/))*$" + }, + "afterText": { + "pattern": "^\\s*\\*/$" + }, + "action": { + "indent": "indentOutdent", + "appendText": " * " + } + }, + { + // e.g. /** ...| + "beforeText": { + "pattern": "^\\s*/\\*\\*(?!/)([^\\*]|\\*(?!/))*$" + }, + "action": { + "indent": "none", + "appendText": " * " + } + }, + { + // e.g. * ...| + "beforeText": { + "pattern": "^(\\t|[ ])*[ ]\\*([ ]([^\\*]|\\*(?!/))*)?$" + }, + "previousLineText": { + "pattern": "(?=^(\\s*(/\\*\\*|\\*)).*)(?=(?!(\\s*\\*/)))" + }, + "action": { + "indent": "none", + "appendText": "* " + } + }, + { + // e.g. */| + "beforeText": { + "pattern": "^(\\t|[ ])*[ ]\\*/\\s*$" + }, + "action": { + "indent": "none", + "removeText": 1 + } + }, + { + // e.g. *-----*/| + "beforeText": { + "pattern": "^(\\t|[ ])*[ ]\\*[^/]*\\*/\\s*$" + }, + "action": { + "indent": "none", + "removeText": 1 + } + }, + { + "beforeText": { + "pattern": "^\\s*(\\bcase\\s.+:|\\bdefault:)$" + }, + "afterText": { + "pattern": "^(?!\\s*(\\bcase\\b|\\bdefault\\b))" + }, + "action": { + "indent": "indent" + } + }, + { + "beforeText": { + "pattern": "^\\s*///.*$" + }, + "action": { + "indent": "none", + "appendText": "///" + } + } + ] +} diff --git a/java/java.lsp.server/vscode/package.json b/java/java.lsp.server/vscode/package.json index e9d41f302d66..5265239746cf 100644 --- a/java/java.lsp.server/vscode/package.json +++ b/java/java.lsp.server/vscode/package.json @@ -35,6 +35,18 @@ "main": "./out/extension.js", "contributes": { "languages": [ + { + "id": "java", + "extensions": [ + ".java", + ".jav" + ], + "aliases": [ + "Java", + "java" + ], + "configuration": "./language-configuration.json" + }, { "id": "javascript", "mimetypes": [ diff --git a/java/java.lsp.server/vscode/syntaxes/java.tmLanguage.json b/java/java.lsp.server/vscode/syntaxes/java.tmLanguage.json index 1357aff68d5f..d990bf88fd93 100644 --- a/java/java.lsp.server/vscode/syntaxes/java.tmLanguage.json +++ b/java/java.lsp.server/vscode/syntaxes/java.tmLanguage.json @@ -655,6 +655,63 @@ } } ] + }, + { + "begin": "^\\s*(///)", + "beginCaptures": { + "1": { + "name": "punctuation.definition.comment.java" + } + }, + "end": "\n", + "endCaptures": { + "0": { + "name": "punctuation.definition.comment.java" + } + }, + "name": "comment.block.javadoc.markdown.java", + "patterns": [ + { + "match": "@(author|deprecated|return|see|serial|since|version)\\b", + "name": "keyword.other.documentation.javadoc.java" + }, + { + "match": "(@param)\\s+(\\S+)", + "captures": { + "1": { + "name": "keyword.other.documentation.javadoc.java" + }, + "2": { + "name": "variable.parameter.java" + } + } + }, + { + "match": "(@(?:exception|throws))\\s+(\\S+)", + "captures": { + "1": { + "name": "keyword.other.documentation.javadoc.java" + }, + "2": { + "name": "entity.name.type.class.java" + } + } + }, + { + "match": "{(@link)\\s+(\\S+)?#([\\w$]+\\s*\\([^\\(\\)]*\\)).*?}", + "captures": { + "1": { + "name": "keyword.other.documentation.javadoc.java" + }, + "2": { + "name": "entity.name.type.class.java" + }, + "3": { + "name": "variable.parameter.java" + } + } + } + ] } ] }, diff --git a/java/java.source.base/apichanges.xml b/java/java.source.base/apichanges.xml index 820ac01a8ab9..8d6347cead60 100644 --- a/java/java.source.base/apichanges.xml +++ b/java/java.source.base/apichanges.xml @@ -25,6 +25,18 @@ Java Source API + + + Adding TreeMaker.RawText + + + + + + Adding TreeMaker.RawText. + + + Adding TreeMaker.RecordComponent diff --git a/java/java.source.base/nbproject/project.properties b/java/java.source.base/nbproject/project.properties index 7d9fff6702d8..e5394e03a911 100644 --- a/java/java.source.base/nbproject/project.properties +++ b/java/java.source.base/nbproject/project.properties @@ -23,7 +23,7 @@ javadoc.name=Java Source Base javadoc.title=Java Source Base javadoc.arch=${basedir}/arch.xml javadoc.apichanges=${basedir}/apichanges.xml -spec.version.base=2.70.0 +spec.version.base=2.71.0 test.qa-functional.cp.extra=${refactoring.java.dir}/modules/ext/nb-javac-api.jar test.unit.run.cp.extra=${o.n.core.dir}/core/core.jar:\ ${o.n.core.dir}/lib/boot.jar:\ diff --git a/java/java.source.base/src/org/netbeans/api/java/source/AssignComments.java b/java/java.source.base/src/org/netbeans/api/java/source/AssignComments.java index b6c70bb4d8ac..05b11d0d5798 100644 --- a/java/java.source.base/src/org/netbeans/api/java/source/AssignComments.java +++ b/java/java.source.base/src/org/netbeans/api/java/source/AssignComments.java @@ -428,7 +428,7 @@ private int countIndent(TokenSequence seq, Tree tree) { } private boolean alreadySeenJavadoc(Token token, TokenSequence seq) { - return (token.id() == JavaTokenId.JAVADOC_COMMENT) && + return (token.id() == JavaTokenId.JAVADOC_COMMENT || token.id() == JavaTokenId.JAVADOC_COMMENT_LINE_RUN) && (mixedJDocTokenIndexes.contains(seq.index())); } @@ -484,7 +484,7 @@ private void lookForTrailing(TokenSequence seq, Tree tree) { // not be eaten although separated by many lines: assign = true; - if (index >= 0 && maxLines < Integer.MAX_VALUE && seq.token().length() > 0 && h.comment.id() == JavaTokenId.JAVADOC_COMMENT) { + if (index >= 0 && maxLines < Integer.MAX_VALUE && seq.token().length() > 0 && (h.comment.id() == JavaTokenId.JAVADOC_COMMENT || h.comment.id() == JavaTokenId.JAVADOC_COMMENT_LINE_RUN)) { TreePath tp = info.getTreeUtilities().pathFor(seq.offset() + 1); // traverse up to last parent that claims the position while (tp.getParentPath() != null && @@ -598,7 +598,7 @@ private void lookForPreceedings(TokenSequence seq, Tree tree) { CommentsCollection result = new CommentsCollection(); while (seq.moveNext() && seq.offset() < reset) { JavaTokenId id = seq.token().id(); - if (id == JavaTokenId.JAVADOC_COMMENT) { + if (id == JavaTokenId.JAVADOC_COMMENT || id == JavaTokenId.JAVADOC_COMMENT_LINE_RUN) { mixedJDocTokenIndexes.add(seq.index()); start = Math.min(seq.offset(), start); end = Math.max(seq.offset() + seq.token().length(), end); @@ -635,6 +635,7 @@ private int findInterestingStart(JCTree tree) { case WHITESPACE: case LINE_COMMENT: case JAVADOC_COMMENT: + case JAVADOC_COMMENT_LINE_RUN: case BLOCK_COMMENT: continue; case LBRACE: @@ -677,7 +678,7 @@ private int getEndPos(Token comment) { private Comment.Style getStyle(JavaTokenId id) { switch (id) { - case JAVADOC_COMMENT: + case JAVADOC_COMMENT, JAVADOC_COMMENT_LINE_RUN: return Comment.Style.JAVADOC; case LINE_COMMENT: return Comment.Style.LINE; @@ -726,7 +727,7 @@ private CommentsCollection getCommentsCollection(TokenSequence ts, if (ts.index() < tokenIndexAlreadyAdded) continue; t = ts.token(); if (isComment(t.id())) { - if (t.id() == JavaTokenId.JAVADOC_COMMENT && + if ((t.id() == JavaTokenId.JAVADOC_COMMENT || t.id() == JavaTokenId.JAVADOC_COMMENT_LINE_RUN) && mixedJDocTokenIndexes.contains(ts.index())) { // skip javadocs already added continue; @@ -761,6 +762,7 @@ private boolean isComment(JavaTokenId tid) { case LINE_COMMENT: case BLOCK_COMMENT: case JAVADOC_COMMENT: + case JAVADOC_COMMENT_LINE_RUN: return true; default: return false; diff --git a/java/java.source.base/src/org/netbeans/api/java/source/TreeMaker.java b/java/java.source.base/src/org/netbeans/api/java/source/TreeMaker.java index 4f7d8beb50c9..2b443de9ba75 100644 --- a/java/java.source.base/src/org/netbeans/api/java/source/TreeMaker.java +++ b/java/java.source.base/src/org/netbeans/api/java/source/TreeMaker.java @@ -30,6 +30,7 @@ import com.sun.source.doctree.InheritDocTree; import com.sun.source.doctree.LinkTree; import com.sun.source.doctree.ParamTree; +import com.sun.source.doctree.RawTextTree; import com.sun.source.doctree.ReferenceTree; import com.sun.source.doctree.SeeTree; import com.sun.source.doctree.SerialDataTree; @@ -3575,7 +3576,7 @@ public DeprecatedTree Deprecated(List text) { return delegate.Deprecated(text); } - /**Creates a new javadoc comment. + /**Creates a new HTML javadoc comment. * * @param fullBody the entire body of the comment * @param tags the block tags of the comment (after the main body) @@ -3586,8 +3587,19 @@ public DocCommentTree DocComment(List fullBody, List fullBody, List tags) { + return delegate.MarkdownDocComment(fullBody, tags); + } + + /**Creates a new HTML javadoc comment. + * * @param firstSentence the javadoc comment's first sentence * @param body the main body of the comment * @param tags the block tags of the comment (after the main body) @@ -3598,6 +3610,18 @@ public DocCommentTree DocComment(List firstSentence, List firstSentence, List body, List tags) { + return delegate.MarkdownDocComment(firstSentence, body, tags); + } + /**Creates the DocTree's ParamTree. * * @param isTypeParameter true if and only if the parameter is a type parameter @@ -3727,6 +3751,16 @@ public TextTree Text(String text) { return delegate.Text(text); } + /**Creates the DocTree's RawTextTree. + * + * @param text the text + * @return newly created RawTextTree + * @since 2.71 + */ + public RawTextTree RawText(String text) { + return delegate.RawText(text); + } + /**Creates the DocTree's ThrowsTree that will produce @throws. * * @param name reference to the documented exception diff --git a/java/java.source.base/src/org/netbeans/api/java/source/TreeUtilities.java b/java/java.source.base/src/org/netbeans/api/java/source/TreeUtilities.java index 737c3d6ad0d6..d8c6a4ea0db3 100644 --- a/java/java.source.base/src/org/netbeans/api/java/source/TreeUtilities.java +++ b/java/java.source.base/src/org/netbeans/api/java/source/TreeUtilities.java @@ -1312,7 +1312,11 @@ public int[] findNameSpan(DocCommentTree docTree, ReferenceTree ref) { tokenSequence.move(pos); - if (!tokenSequence.moveNext() || tokenSequence.token().id() != JavaTokenId.JAVADOC_COMMENT) return null; + if (!tokenSequence.moveNext() || + (tokenSequence.token().id() != JavaTokenId.JAVADOC_COMMENT && + tokenSequence.token().id() != JavaTokenId.JAVADOC_COMMENT_LINE_RUN)) { + return null; + } TokenSequence jdocTS = tokenSequence.embedded(JavadocTokenId.language()); diff --git a/java/java.source.base/src/org/netbeans/modules/java/source/builder/TreeFactory.java b/java/java.source.base/src/org/netbeans/modules/java/source/builder/TreeFactory.java index d28e43c61956..abb39db21f52 100644 --- a/java/java.source.base/src/org/netbeans/modules/java/source/builder/TreeFactory.java +++ b/java/java.source.base/src/org/netbeans/modules/java/source/builder/TreeFactory.java @@ -32,6 +32,7 @@ import com.sun.source.doctree.InheritDocTree; import com.sun.source.doctree.LinkTree; import com.sun.source.doctree.ParamTree; +import com.sun.source.doctree.RawTextTree; import com.sun.source.doctree.ReferenceTree; import com.sun.source.doctree.SeeTree; import com.sun.source.doctree.SerialDataTree; @@ -61,6 +62,9 @@ import com.sun.tools.javac.jvm.ClassReader; import com.sun.tools.javac.model.JavacElements; import com.sun.tools.javac.model.JavacTypes; +import com.sun.tools.javac.parser.Tokens.Comment; +import com.sun.tools.javac.parser.Tokens.Comment.CommentStyle; +import com.sun.tools.javac.tree.DCTree.DCDocComment; import com.sun.tools.javac.tree.DCTree.DCReference; import com.sun.tools.javac.tree.DocTreeMaker; import com.sun.tools.javac.tree.JCTree; @@ -71,6 +75,7 @@ import com.sun.tools.javac.util.Name; import com.sun.tools.javac.util.Names; import com.sun.tools.javac.util.Context; +import com.sun.tools.javac.util.JCDiagnostic; import java.lang.reflect.Constructor; import java.util.ArrayList; import java.util.Collections; @@ -1806,7 +1811,13 @@ public DeprecatedTree Deprecated(List text) { } public DocCommentTree DocComment(List fullBody, List tags) { - return docMake.at(NOPOS).newDocCommentTree(fullBody, tags); + DCDocComment temp = docMake.at(NOPOS).newDocCommentTree(fullBody, tags); + return DocComment(temp.getFirstSentence(), temp.getBody(), temp.getBlockTags()); + } + + public DocCommentTree MarkdownDocComment(List fullBody, List tags) { + DCDocComment temp = docMake.at(NOPOS).newDocCommentTree(fullBody, tags); + return MarkdownDocComment(temp.getFirstSentence(), temp.getBody(), temp.getBlockTags()); } public DocTree Snippet(List attributes, TextTree text){ @@ -1818,10 +1829,18 @@ public DocTree Snippet(List attributes, TextTree text){ } public DocCommentTree DocComment(List firstSentence, List body, List tags) { + return DocComment(HTML_JAVADOC_COMMENT, firstSentence, body, tags); + } + + public DocCommentTree MarkdownDocComment(List firstSentence, List body, List tags) { + return DocComment(MARKDOWN_JAVADOC_COMMENT, firstSentence, body, tags); + } + + private DocCommentTree DocComment(Comment comment, List firstSentence, List body, List tags) { final ArrayList fullBody = new ArrayList<>(firstSentence.size() + body.size()); fullBody.addAll(firstSentence); fullBody.addAll(body); - return docMake.at(NOPOS).newDocCommentTree(fullBody, tags); + return docMake.at(NOPOS).newDocCommentTree(comment, fullBody, tags, Collections.emptyList(), Collections.emptyList()); } public com.sun.source.doctree.ErroneousTree Erroneous(String text, DiagnosticSource diagSource, String code, Object... args) { @@ -1902,6 +1921,10 @@ public ValueTree Value(ReferenceTree ref) { public VersionTree Version(List text) { return docMake.at(NOPOS).newVersionTree(text); } + + public RawTextTree RawText(String text) { + return docMake.at(NOPOS).newRawTextTree(DocTree.Kind.MARKDOWN, text); + } public com.sun.source.doctree.LiteralTree Code(TextTree text) { return docMake.at(NOPOS).newCodeTree(text); @@ -1957,9 +1980,6 @@ public ReferenceTree Reference(ExpressionTree qualExpr, CharSequence member, Lis } paramTypesParam = lbl.toList(); } - for (Constructor cc : DCReference.class.getDeclaredConstructors()) { - System.err.println("cc: " + cc); - } Constructor c = DCReference.class.getDeclaredConstructor(String.class, JCExpression.class, JCTree.class, javax.lang.model.element.Name.class, List.class); c.setAccessible(true); DCReference result = c.newInstance("", (JCTree.JCExpression) qualExpr, qualExpr == null ? null : ((JCTree.JCExpression) qualExpr).getTree(), member != null ? (com.sun.tools.javac.util.Name) names.fromString(member.toString()) : null, paramTypesParam); @@ -1994,4 +2014,41 @@ public ReferenceTree Reference(ExpressionTree qualExpr, CharSequence member, Lis private RuntimeException throwAny(Throwable t) throws T { throw (T) t; } + + private static final Comment HTML_JAVADOC_COMMENT = new CommentImpl(CommentStyle.JAVADOC_BLOCK); + private static final Comment MARKDOWN_JAVADOC_COMMENT = new CommentImpl(CommentStyle.JAVADOC_LINE); + + private static class CommentImpl implements Comment { + + private final CommentStyle style; + + public CommentImpl(CommentStyle style) { + this.style = style; + } + + @Override + public String getText() { + return ""; + } + + @Override + public JCDiagnostic.DiagnosticPosition getPos() { + return null; + } + + @Override + public int getSourcePos(int index) { + return -1; + } + + @Override + public CommentStyle getStyle() { + return style; + } + + @Override + public boolean isDeprecated() { + return false; + } + } } diff --git a/java/java.source.base/src/org/netbeans/modules/java/source/pretty/VeryPretty.java b/java/java.source.base/src/org/netbeans/modules/java/source/pretty/VeryPretty.java index 2faea15be640..559d428ebb64 100644 --- a/java/java.source.base/src/org/netbeans/modules/java/source/pretty/VeryPretty.java +++ b/java/java.source.base/src/org/netbeans/modules/java/source/pretty/VeryPretty.java @@ -55,6 +55,7 @@ import com.sun.source.doctree.LiteralTree; import com.sun.source.doctree.ParamTree; import com.sun.source.doctree.ProvidesTree; +import com.sun.source.doctree.RawTextTree; import com.sun.source.doctree.ReferenceTree; import com.sun.source.doctree.ReturnTree; import com.sun.source.doctree.SeeTree; @@ -72,6 +73,8 @@ import com.sun.source.doctree.VersionTree; import com.sun.source.tree.ExpressionTree; import com.sun.source.tree.SwitchExpressionTree; +import com.sun.source.util.DocTreePathScanner; +import com.sun.source.util.DocTreeScanner; import com.sun.tools.javac.api.JavacTaskImpl; import com.sun.tools.javac.api.JavacTrees; @@ -79,7 +82,9 @@ import static com.sun.tools.javac.code.Flags.*; import com.sun.tools.javac.comp.Operators; import com.sun.tools.javac.main.JavaCompiler; +import com.sun.tools.javac.parser.Tokens; import com.sun.tools.javac.tree.DCTree; +import com.sun.tools.javac.tree.DCTree.DCDocComment; import com.sun.tools.javac.tree.DCTree.DCReference; import com.sun.tools.javac.tree.JCTree; import com.sun.tools.javac.tree.JCTree.*; @@ -96,6 +101,7 @@ import java.lang.reflect.Method; import java.net.URL; import java.util.*; +import java.util.concurrent.atomic.AtomicBoolean; import java.util.logging.Level; import java.util.logging.Logger; @@ -154,6 +160,7 @@ public final class VeryPretty extends JCTree.Visitor implements DocTreeVisitor tree2Tag; private final Map tree2Doc; @@ -2244,7 +2251,7 @@ private void blankLines(DCTree tree, boolean before, boolean suppressMarginAfter if(before) { newline(); toLeftMargin(); - print(" * "); + printDocCommentLineStartText(); } break; case DOC_COMMENT: @@ -2330,10 +2337,20 @@ public Void visitDeprecated(DeprecatedTree node, Void p) { @Override public Void visitDocComment(DocCommentTree node, Void p) { - print("/**"); - newline(); - toLeftMargin(); - print(" * "); + boolean hasMarkdown = + node instanceof DCDocComment c && + c.comment.getStyle() == Tokens.Comment.CommentStyle.JAVADOC_LINE; + + if (!hasMarkdown) { + print("/**"); + newline(); + toLeftMargin(); + docCommentKind = JavaTokenId.JAVADOC_COMMENT; + } else { + docCommentKind = JavaTokenId.JAVADOC_COMMENT_LINE_RUN; + } + + printDocCommentLineStartText(); for (DocTree docTree : node.getFirstSentence()) { doAccept((DCTree)docTree); } @@ -2343,12 +2360,16 @@ public Void visitDocComment(DocCommentTree node, Void p) { for (DocTree docTree : node.getBlockTags()) { newline(); toLeftMargin(); - print(" * "); + printDocCommentLineStartText(); doAccept((DCTree)docTree); } - newline(); - toLeftMargin(); - print(" */"); + + if (!hasMarkdown) { + newline(); + toLeftMargin(); + print(" */"); + } + return null; } @@ -2609,6 +2630,12 @@ public Void visitText(TextTree node, Void p) { return null; } + @Override + public Void visitRawText(RawTextTree node, Void p) { + print(node.getContent()); + return null; + } + @Override public Void visitThrows(ThrowsTree node, Void p) { printTagName(node); @@ -2690,6 +2717,18 @@ public Void visitOther(DocTree node, Void p) { return null; } + public void printDocCommentLineStartText() { + if (docCommentKind == JavaTokenId.JAVADOC_COMMENT_LINE_RUN) { + print("/// "); + } else { + print(" * "); + } + } + + public void setDocCommentKind(JavaTokenId docCommentKind) { + this.docCommentKind = docCommentKind; + } + private final class Linearize extends ErrorAwareTreeScanner> { @Override public Boolean scan(Tree node, java.util.List p) { @@ -3345,7 +3384,7 @@ public void printComment(Comment comment, boolean preceding, boolean printWhites print("/**"); newline(); toLeftMargin(); - print(" * "); + printDocCommentLineStartText(); } } if (!lines.isEmpty()) @@ -3355,7 +3394,7 @@ public void printComment(Comment comment, boolean preceding, boolean printWhites toLeftMargin(); CommentLine line = lines.removeFirst(); if (rawBody) - print(" * "); + printDocCommentLineStartText(); else if (line.body.charAt(line.startPos) == '*') print(' '); line.print(out.col); diff --git a/java/java.source.base/src/org/netbeans/modules/java/source/save/CasualDiff.java b/java/java.source.base/src/org/netbeans/modules/java/source/save/CasualDiff.java index 6a561506eaa0..037eb8251775 100644 --- a/java/java.source.base/src/org/netbeans/modules/java/source/save/CasualDiff.java +++ b/java/java.source.base/src/org/netbeans/modules/java/source/save/CasualDiff.java @@ -658,6 +658,8 @@ private int adjustToPreviousNewLine(int oldPos, int localPointer) { if (c == '\n') { break; + } else if (offset >= 2 && diffContext.origText.startsWith("///", offset - 3)) { + offset -= 3; } else if(c != '*' && !Character.isWhitespace(c)) { return oldPos; } @@ -1226,7 +1228,7 @@ private int removeExtraEnumSemicolon(int insertHint) { semi = true; break; - case LINE_COMMENT: case BLOCK_COMMENT: case JAVADOC_COMMENT: + case LINE_COMMENT, BLOCK_COMMENT, JAVADOC_COMMENT, JAVADOC_COMMENT_LINE_RUN: if (semi) { break out; } @@ -1601,7 +1603,7 @@ private int diffVarDef(JCVariableDecl oldT, JCVariableDecl newT, int[] bounds) { offset = tokenSequence.offset(); tokenId = tokenSequence.token().id(); - if (!((tokenId == JavaTokenId.WHITESPACE || tokenId == JavaTokenId.BLOCK_COMMENT || tokenId == JavaTokenId.JAVADOC_COMMENT) && offset < oldT.sym.pos)) { + if (!((tokenId == JavaTokenId.WHITESPACE || tokenId == JavaTokenId.BLOCK_COMMENT || tokenId == JavaTokenId.JAVADOC_COMMENT || tokenId == JavaTokenId.JAVADOC_COMMENT_LINE_RUN) && offset < oldT.sym.pos)) { break; } @@ -1786,6 +1788,7 @@ private boolean isComment(JavaTokenId tid) { case LINE_COMMENT: case BLOCK_COMMENT: case JAVADOC_COMMENT: + case JAVADOC_COMMENT_LINE_RUN: return true; default: return false; @@ -1796,7 +1799,7 @@ private boolean isNoop(JavaTokenId tid) { switch (tid) { case LINE_COMMENT: case BLOCK_COMMENT: - case JAVADOC_COMMENT: + case JAVADOC_COMMENT_LINE_RUN: case WHITESPACE: return true; default: @@ -4815,6 +4818,13 @@ private int diffAttribute(DCDocComment doc, DCAttribute oldT, DCAttribute newT, } private int diffDocComment(DCDocComment doc, DCDocComment oldT, DCDocComment newT, int[] elementBounds) { + //set the existing token kind, to produce correct line-beginnings: + int commentPos = getOldPos(oldT, oldT); + tokenSequence.move(commentPos); + if (tokenSequence.moveNext()) { + printer.setDocCommentKind(tokenSequence.token().id()); + } + tokenSequence.move(elementBounds[0]); if (!tokenSequence.moveNext()) { return elementBounds[1]; @@ -4828,7 +4838,7 @@ private int diffDocComment(DCDocComment doc, DCDocComment oldT, DCDocComment new if(oldT.firstSentence.isEmpty() && !newT.firstSentence.isEmpty()) { printer.newline(); printer.toLeftMargin(); - printer.print(" * "); + printer.printDocCommentLineStartText(); } localpointer = diffList(doc, oldT.firstSentence, newT.firstSentence, localpointer, Measure.TAGS); localpointer = diffList(doc, oldT.body, newT.body, localpointer, Measure.TAGS); @@ -4872,6 +4882,9 @@ private int diffParam(DCDocComment doc, DCParam oldT, DCParam newT, int[] elemen if(newT.isTypeParameter) { printer.print(">"); } + if (!oldT.description.isEmpty()) { + copyTo(localpointer, localpointer = getOldPos(oldT.description.get(0), doc)); + } localpointer = diffList(doc, oldT.description, newT.description, localpointer, Measure.TAGS); if(localpointer < elementBounds[1]) { copyTo(localpointer, elementBounds[1]); @@ -5277,7 +5290,7 @@ private int diffList( // localPointer = pos[0]; // } if(needStar(pos[0])) { - printer.print(" * "); + printer.printDocCommentLineStartText(); } copyTo(localPointer, localPointer = pos[1], printer); lastdel = null; diff --git a/java/java.source.base/src/org/netbeans/modules/java/source/save/PositionEstimator.java b/java/java.source.base/src/org/netbeans/modules/java/source/save/PositionEstimator.java index c6d73c3c6080..5ee0dcd97900 100644 --- a/java/java.source.base/src/org/netbeans/modules/java/source/save/PositionEstimator.java +++ b/java/java.source.base/src/org/netbeans/modules/java/source/save/PositionEstimator.java @@ -1949,6 +1949,7 @@ public static final boolean isSeparator(JavaTokenId id) { LINE_COMMENT, BLOCK_COMMENT, JAVADOC_COMMENT, + JAVADOC_COMMENT_LINE_RUN, WHITESPACE ); diff --git a/java/java.source.base/src/org/netbeans/modules/java/source/transform/ImmutableDocTreeTranslator.java b/java/java.source.base/src/org/netbeans/modules/java/source/transform/ImmutableDocTreeTranslator.java index 99eb4d05840a..254838c1eb1c 100644 --- a/java/java.source.base/src/org/netbeans/modules/java/source/transform/ImmutableDocTreeTranslator.java +++ b/java/java.source.base/src/org/netbeans/modules/java/source/transform/ImmutableDocTreeTranslator.java @@ -37,6 +37,7 @@ import com.sun.source.doctree.LiteralTree; import com.sun.source.doctree.ParamTree; import com.sun.source.doctree.ProvidesTree; +import com.sun.source.doctree.RawTextTree; import com.sun.source.doctree.ReferenceTree; import com.sun.source.doctree.ReturnTree; import com.sun.source.doctree.SeeTree; @@ -55,7 +56,9 @@ import com.sun.source.doctree.SnippetTree; import com.sun.source.tree.ExpressionTree; import com.sun.source.tree.Tree; +import com.sun.tools.javac.parser.Tokens.Comment.CommentStyle; import com.sun.tools.javac.tree.DCTree; +import com.sun.tools.javac.tree.DCTree.DCDocComment; import com.sun.tools.javac.tree.DCTree.DCReference; import com.sun.tools.javac.util.Name; import java.util.ArrayList; @@ -117,7 +120,13 @@ protected final DocCommentTree rewriteChildren(DocCommentTree tree) { List fullBody = translateDoc(tree.getFullBody()); List blockTags = translateDoc(tree.getBlockTags()); if (fullBody != tree.getFullBody()|| blockTags != tree.getBlockTags()) { - value = make.DocComment(fullBody, blockTags); + boolean markdown = tree instanceof DCDocComment c && + c.comment.getStyle() == CommentStyle.JAVADOC_LINE; + if (markdown) { + value = make.MarkdownDocComment(fullBody, blockTags); + } else { + value = make.DocComment(fullBody, blockTags); + } } return value; } @@ -375,6 +384,10 @@ protected final VersionTree rewriteChildren(VersionTree tree) { return value; } + protected final RawTextTree rewriteChildren(RawTextTree tree) { + return tree; // Nothing to do for a string + } + protected final DocTree rewriteSnippetChildren(DocTree tree) { DocTree value = tree; SnippetTree javadocSnippet = (SnippetTree)tree; @@ -549,6 +562,11 @@ public DocTree visitVersion(VersionTree tree, Object p) { return rewriteChildren(tree); } + @Override + public DocTree visitRawText(RawTextTree tree, Object p) { + return rewriteChildren(tree); + } + @Override public DocTree visitOther(DocTree tree, Object p) { throw new Error("DocTree not overloaded: " + tree); diff --git a/java/java.source.base/test/unit/src/org/netbeans/api/java/source/gen/DoctreeTest.java b/java/java.source.base/test/unit/src/org/netbeans/api/java/source/gen/DoctreeTest.java index 2b06f0d2d186..aa3291a08e22 100644 --- a/java/java.source.base/test/unit/src/org/netbeans/api/java/source/gen/DoctreeTest.java +++ b/java/java.source.base/test/unit/src/org/netbeans/api/java/source/gen/DoctreeTest.java @@ -2287,4 +2287,223 @@ public Void visitVersion(VersionTree node, Void p) { //System.err.println(res); assertEquals(golden, res); } + + public void testAddMarkdownTag() throws Exception { + testFile = new File(getWorkDir(), "Test.java"); + TestUtilities.copyStringToFile(testFile, + """ + package hierbas.del.litoral; + + public class Test { + + /// Test method + /// + /// @param p1 param1 + private void test(int p1, int p2) { + } + } + """); + String golden = + """ + package hierbas.del.litoral; + + public class Test { + + /// Test method + /// + /// @param p1 param1 + /// @param p2 param2 + private void test(int p1, int p2) { + } + } + """; + + JavaSource src = getJavaSource(testFile); + Task task = new Task() { + @Override + public void run(final WorkingCopy wc) throws IOException { + wc.toPhase(JavaSource.Phase.RESOLVED); + final TreeMaker make = wc.getTreeMaker(); + final DocTrees trees = wc.getDocTrees(); + new ErrorAwareTreePathScanner() { + @Override + public Void visitMethod(final MethodTree mt, Void p) { + DocCommentTree docTree = trees.getDocCommentTree(getCurrentPath()); + if (docTree != null) { + ArrayList blockTags = new ArrayList<>(docTree.getBlockTags()); + blockTags.add(make.Param(false, make.DocIdentifier("p2"), List.of(make.Text("param2")))); + wc.rewrite(mt, docTree, make.DocComment(docTree.getFullBody(), blockTags)); + } + return super.visitMethod(mt, p); + } + }.scan(wc.getCompilationUnit(), null); + } + }; + src.runModificationTask(task).commit(); + String res = TestUtilities.copyFileToString(testFile); + //System.err.println(res); + assertEquals(golden, res); + } + + public void testChangeMarkdownParam() throws Exception { + testFile = new File(getWorkDir(), "Test.java"); + TestUtilities.copyStringToFile(testFile, + """ + package hierbas.del.litoral; + + public class Test { + + /// Test method + /// + /// @param p1 param1 + private void test(int p2) { + } + } + """); + String golden = + """ + package hierbas.del.litoral; + + public class Test { + + /// Test method + /// + /// @param p2 param2 + private void test(int p2) { + } + } + """; + + JavaSource src = getJavaSource(testFile); + Task task = new Task() { + @Override + public void run(final WorkingCopy wc) throws IOException { + wc.toPhase(JavaSource.Phase.RESOLVED); + final TreeMaker make = wc.getTreeMaker(); + final DocTrees trees = wc.getDocTrees(); + new ErrorAwareTreePathScanner() { + @Override + public Void visitMethod(final MethodTree mt, Void p) { + DocCommentTree docTree = trees.getDocCommentTree(getCurrentPath()); + DocTreeScanner scanner = new DocTreeScanner() { + @Override + public Void visitParam(ParamTree node, Void p) { + ParamTree newParam = make.Param(false, make.DocIdentifier("p2"), List.of(make.Text("param2"))); + wc.rewrite(mt, node, newParam); + return super.visitParam(node, p); + } + }; + scanner.scan(docTree, null); + return super.visitMethod(mt, p); + } + }.scan(wc.getCompilationUnit(), null); + } + }; + src.runModificationTask(task).commit(); + String res = TestUtilities.copyFileToString(testFile); + //System.err.println(res); + assertEquals(golden, res); + } + + public void testRemoveMarkdownParam() throws Exception { + testFile = new File(getWorkDir(), "Test.java"); + TestUtilities.copyStringToFile(testFile, + """ + package hierbas.del.litoral; + + public class Test { + + /// Test method + /// + /// @param p1 param1 + private void test(int p2) { + } + } + """); + String golden = + """ + package hierbas.del.litoral; + + public class Test { + + /// Test method + /// + private void test(int p2) { + } + } + """; + + JavaSource src = getJavaSource(testFile); + Task task = new Task() { + @Override + public void run(final WorkingCopy wc) throws IOException { + wc.toPhase(JavaSource.Phase.RESOLVED); + final TreeMaker make = wc.getTreeMaker(); + final DocTrees trees = wc.getDocTrees(); + new ErrorAwareTreePathScanner() { + @Override + public Void visitMethod(final MethodTree mt, Void p) { + DocCommentTree docTree = trees.getDocCommentTree(getCurrentPath()); + if (docTree != null) { + wc.rewrite(mt, docTree, make.DocComment(docTree.getFullBody(), List.of())); + } + return super.visitMethod(mt, p); + } + }.scan(wc.getCompilationUnit(), null); + } + }; + src.runModificationTask(task).commit(); + String res = TestUtilities.copyFileToString(testFile); + //System.err.println(res); + assertEquals(golden, res); + } + + public void testNewMarkdownComment() throws Exception { + testFile = new File(getWorkDir(), "Test.java"); + TestUtilities.copyStringToFile(testFile, + """ + package hierbas.del.litoral; + + public class Test { + + private void test(int p2) { + } + } + """); + String golden = + """ + package hierbas.del.litoral; + + public class Test { + + /// Test method + /// @param p2 param2 + private void test(int p2) { + } + } + """; + + JavaSource src = getJavaSource(testFile); + Task task = new Task() { + @Override + public void run(final WorkingCopy wc) throws IOException { + wc.toPhase(JavaSource.Phase.RESOLVED); + final TreeMaker make = wc.getTreeMaker(); + final DocTrees trees = wc.getDocTrees(); + new ErrorAwareTreePathScanner() { + @Override + public Void visitMethod(final MethodTree mt, Void p) { + ParamTree param2 = make.Param(false, make.DocIdentifier("p2"), List.of(make.Text("param2"))); + DocCommentTree newDoc = make.MarkdownDocComment(List.of(make.RawText("Test method")), List.of(param2)); + wc.rewrite(mt, null, newDoc); + return super.visitMethod(mt, p); + } + }.scan(wc.getCompilationUnit(), null); + } + }; + src.runModificationTask(task).commit(); + String res = TestUtilities.copyFileToString(testFile); + //System.err.println(res); + assertEquals(golden, res); + } } diff --git a/java/java.sourceui/nbproject/project.xml b/java/java.sourceui/nbproject/project.xml index 9ac80a3c0929..f1bb6375971b 100644 --- a/java/java.sourceui/nbproject/project.xml +++ b/java/java.sourceui/nbproject/project.xml @@ -86,6 +86,14 @@ + + org.netbeans.libs.flexmark + + + + 1.18 + + org.netbeans.libs.javacapi diff --git a/java/java.sourceui/src/org/netbeans/api/java/source/ui/ElementJavadoc.java b/java/java.sourceui/src/org/netbeans/api/java/source/ui/ElementJavadoc.java index fdabe504448e..832e599852c2 100644 --- a/java/java.sourceui/src/org/netbeans/api/java/source/ui/ElementJavadoc.java +++ b/java/java.sourceui/src/org/netbeans/api/java/source/ui/ElementJavadoc.java @@ -32,6 +32,7 @@ import com.sun.source.doctree.LinkTree; import com.sun.source.doctree.LiteralTree; import com.sun.source.doctree.ParamTree; +import com.sun.source.doctree.RawTextTree; import com.sun.source.doctree.ReferenceTree; import com.sun.source.doctree.ReturnTree; import com.sun.source.doctree.SeeTree; @@ -119,6 +120,11 @@ import com.sun.source.tree.ImportTree; import com.sun.source.tree.Tree; import com.sun.source.util.JavacTask; +import com.vladsch.flexmark.ext.tables.TablesExtension; +import com.vladsch.flexmark.html.HtmlRenderer; +import com.vladsch.flexmark.parser.Parser; +import com.vladsch.flexmark.util.data.DataHolder; +import com.vladsch.flexmark.util.data.MutableDataSet; import javax.lang.model.element.RecordComponentElement; import org.netbeans.api.java.queries.SourceLevelQuery; import org.netbeans.api.java.queries.SourceLevelQuery.Profile; @@ -1275,7 +1281,7 @@ private String noJavadocFound() { private StringBuilder inlineTags(List tags, TreePath docPath, DocCommentTree doc, DocTrees trees, CharSequence inherited) { StringBuilder sb = new StringBuilder(); Integer snippetCount=0; - for (DocTree tag : tags) { + for (DocTree tag : resolveMarkdown(trees, tags)) { switch (tag.getKind()) { case REFERENCE: ReferenceTree refTag = (ReferenceTree)tag; @@ -1287,6 +1293,9 @@ private StringBuilder inlineTags(List tags, TreePath docPath, break; case LINK: linkTag = (LinkTree)tag; + if (linkTag.getReference() == null) { + break; + } sb.append(""); //NOI18N appendReference(sb, linkTag.getReference(), linkTag.getLabel(), docPath, doc, trees); sb.append(""); //NOI18N @@ -1395,6 +1404,53 @@ private StringBuilder inlineTags(List tags, TreePath docPath, return sb; } + private static final char REPLACEMENT = '\uFFFD'; + private List resolveMarkdown(DocTrees trees, List tags) { + if (tags.stream().noneMatch(t -> t.getKind() == DocTree.Kind.MARKDOWN)) { + return tags; + } + + StringBuilder markdownSource = new StringBuilder(); + List replacements = new ArrayList<>(); + + for (DocTree t : tags) { + if (t.getKind() == DocTree.Kind.MARKDOWN) { + markdownSource.append(((RawTextTree) t).getContent()); + } else { + markdownSource.append(REPLACEMENT); + replacements.add(t); + } + } + + TablesExtension tablesExtension = TablesExtension.create(); + + Parser.Builder parserBuilder = Parser.builder(); + tablesExtension.extend(parserBuilder); + + HtmlRenderer.Builder rendererBuilder = HtmlRenderer.builder(); + tablesExtension.extend(rendererBuilder, "HTML"); + + String html = rendererBuilder.build() + .render(parserBuilder.build() + .parse(markdownSource.toString())); + + if (html.startsWith("

")) { + html = html.substring("

".length()); + } + html = html.replace("

", ""); + List result = new ArrayList<>(); + String[] parts = html.split(Pattern.quote("" + REPLACEMENT)); + + result.add(trees.getDocTreeFactory().newTextTree(parts[0])); + + for (int i = 1; i < parts.length; i++) { + result.add(replacements.get(i - 1)); + result.add(trees.getDocTreeFactory().newTextTree(parts[i])); + } + + return result; + } + private void processDocSnippet(StringBuilder sb, SnippetTree javadocSnippet, Integer snippetCount, TreePath docPath,DocCommentTree doc, DocTrees trees) { sb.append("
"); //NOI18N sb.append("
" //NOI18N diff --git a/java/java.sourceui/test/unit/src/org/netbeans/api/java/source/ui/ElementJavadocTest.java b/java/java.sourceui/test/unit/src/org/netbeans/api/java/source/ui/ElementJavadocTest.java new file mode 100644 index 000000000000..13bee62039d3 --- /dev/null +++ b/java/java.sourceui/test/unit/src/org/netbeans/api/java/source/ui/ElementJavadocTest.java @@ -0,0 +1,164 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.netbeans.api.java.source.ui; + +import com.sun.source.util.TreePath; +import java.io.File; +import java.util.ArrayList; +import java.util.List; +import javax.lang.model.element.Element; +import javax.swing.text.Document; +import org.netbeans.api.java.lexer.JavaTokenId; +import org.netbeans.api.java.source.CompilationInfo; +import org.netbeans.api.java.source.JavaSource; +import org.netbeans.api.java.source.JavaSource.Phase; +import org.netbeans.api.java.source.SourceUtilsTestUtil; +import org.netbeans.api.java.source.TestUtilities; +import org.netbeans.api.lexer.Language; +import org.netbeans.junit.NbTestCase; +import org.netbeans.modules.java.JavaDataLoader; +import org.openide.cookies.EditorCookie; +import org.openide.filesystems.FileObject; +import org.openide.filesystems.FileUtil; +import org.openide.loaders.DataObject; + +public class ElementJavadocTest extends NbTestCase { + + private static final String CARET_MARK = ""; + + public ElementJavadocTest(String testName) { + super(testName); + } + + private void prepareTest(String fileName, String code) throws Exception { + int pos = code.indexOf(CARET_MARK); + + if (pos == (-1)) { + throw new AssertionError("Does not have caret position!"); + } + + code = code.substring(0, pos) + code.substring(pos + CARET_MARK.length()); + + List extras = new ArrayList<>(); + extras.add(JavaDataLoader.class); + SourceUtilsTestUtil.prepareTest(new String[] { + "org/netbeans/modules/java/platform/resources/layer.xml", + "org/netbeans/modules/java/j2seplatform/resources/layer.xml" + }, + extras.toArray(new Object[0]) + ); + + clearWorkDir(); + + FileUtil.refreshAll(); + + FileObject workFO = FileUtil.toFileObject(getWorkDir()); + + assertNotNull(workFO); + + sourceRoot = workFO.createFolder("src"); + + FileObject buildRoot = workFO.createFolder("build"); + FileObject cache = workFO.createFolder("cache"); + + FileObject data = FileUtil.createData(sourceRoot, fileName); + File dataFile = FileUtil.toFile(data); + + assertNotNull(dataFile); + + TestUtilities.copyStringToFile(dataFile, code); + + SourceUtilsTestUtil.prepareTest(sourceRoot, buildRoot, cache, new FileObject[0]); + + DataObject od = DataObject.find(data); + EditorCookie ec = od.getCookie(EditorCookie.class); + + assertNotNull(ec); + + doc = ec.openDocument(); + doc.putProperty(Language.class, JavaTokenId.language()); + + JavaSource js = JavaSource.forFileObject(data); + + assertNotNull(js); + + info = SourceUtilsTestUtil.getCompilationInfo(js, Phase.RESOLVED); + + assertNotNull(info); + + selectedPath = info.getTreeUtilities().pathFor(pos); + selectedElement = info.getTrees().getElement(selectedPath); + + assertNotNull(selectedElement); + } + + private FileObject sourceRoot; + private CompilationInfo info; + private Document doc; + private TreePath selectedPath; + private Element selectedElement; + + protected void performTest(String fileName, String code, int pos, String format, String golden) throws Exception { + prepareTest(fileName, code); + + TreePath path = info.getTreeUtilities().pathFor(pos); + + assertEquals(golden, ElementHeaders.getHeader(path, info, format)); + } + + public void testMarkdownTables() throws Exception { + prepareTest("test/Test.java", + "///| header1 | header2 |\n" + + "///|---------|---------|\n" + + "///| cr11 | cr12 |\n" + + "///| cr21 | cr22 |\n" + + "public class Test {\n" + + "}\n"); + + String actualJavadoc = ElementJavadoc.create(info, selectedElement).getText(); + String expectedJavadoc = "
public class Test
extends Object

\n" + + "\n" + + "\n" + + "\n" + + "\n" + + "\n" + + "\n" + + "\n" + + "
header1header2
cr11cr12
cr21cr22
\n" + + "

"; + + assertEquals(expectedJavadoc, actualJavadoc); + } + + public void testLinkNoRef() throws Exception { + prepareTest("test/Test.java", + "///Hello!\n" + + "///{@link }\n" + + "public class Test {\n" + + "}\n"); + + String actualJavadoc = ElementJavadoc.create(info, selectedElement).getText(); + String expectedJavadoc = "

public class Test
extends Object

Hello!\n" + + "\n" + + "

"; + + assertEquals(expectedJavadoc, actualJavadoc); + } + +} diff --git a/java/javadoc/nbproject/project.properties b/java/javadoc/nbproject/project.properties index a925b2aff07f..61991d67b813 100644 --- a/java/javadoc/nbproject/project.properties +++ b/java/javadoc/nbproject/project.properties @@ -16,7 +16,7 @@ # under the License. javac.compilerargs=-Xlint:unchecked -javac.source=1.8 +javac.release=17 # requires nb.javac for compiling of tests on Mac requires.nb.javac=true @@ -27,4 +27,4 @@ test.config.stableBTD.includes=\ **/search/*Test.class # failing -test.config.default.excludes=**/search/*Test.class +test.config.default.excludes=**/search/*Test.class \ No newline at end of file diff --git a/java/javadoc/src/org/netbeans/modules/javadoc/highlighting/Highlighting.java b/java/javadoc/src/org/netbeans/modules/javadoc/highlighting/Highlighting.java index d022a8f55b7d..3d9f206625b7 100644 --- a/java/javadoc/src/org/netbeans/modules/javadoc/highlighting/Highlighting.java +++ b/java/javadoc/src/org/netbeans/modules/javadoc/highlighting/Highlighting.java @@ -363,8 +363,18 @@ private List splitByLines(int sentenceStart, int sentenceEnd) { while (idx < line.length() && (line.charAt(idx) == ' ' || line.charAt(idx) == '\t' || - line.charAt(idx) == '*')) + line.charAt(idx) == '*' || + line.charAt(idx) == '/')) { + if (line.charAt(idx) == '/') { + if (line.length() > idx + 2 && + line.charAt(idx + 1) == '/' && + line.charAt(idx + 2) == '/') { + idx += 3; + continue; + } + break; + } idx++; } diff --git a/java/javadoc/src/org/netbeans/modules/javadoc/hints/JavadocGenerator.java b/java/javadoc/src/org/netbeans/modules/javadoc/hints/JavadocGenerator.java index a076f3b5830c..db600c6d2c59 100644 --- a/java/javadoc/src/org/netbeans/modules/javadoc/hints/JavadocGenerator.java +++ b/java/javadoc/src/org/netbeans/modules/javadoc/hints/JavadocGenerator.java @@ -21,13 +21,19 @@ import com.sun.source.doctree.DocCommentTree; import com.sun.source.doctree.DocTree; +import com.sun.source.tree.ClassTree; import com.sun.source.tree.ExpressionTree; import com.sun.source.tree.MethodTree; import com.sun.source.tree.Tree; +import com.sun.source.tree.VariableTree; +import com.sun.source.util.TreePath; +import com.sun.source.util.TreePathScanner; +import java.util.ArrayList; import java.util.Collections; import java.util.LinkedList; import java.util.List; import java.util.Map; +import java.util.concurrent.atomic.AtomicBoolean; import javax.lang.model.SourceVersion; import javax.lang.model.element.Element; import javax.lang.model.element.ElementKind; @@ -40,6 +46,8 @@ import javax.lang.model.type.TypeKind; import javax.lang.model.type.TypeMirror; import javax.lang.model.type.TypeVariable; +import javax.lang.model.util.Elements; +import javax.lang.model.util.Elements.DocCommentKind; import org.netbeans.api.java.source.CompilationInfo; import org.netbeans.api.java.source.TreeMaker; import org.openide.filesystems.FileObject; @@ -264,7 +272,54 @@ public DocCommentTree generateComment(Element elm, CompilationInfo javac, TreeMa && JavadocUtilities.isDeprecated(javac, elm)) { tags.add(make.Deprecated(Collections.emptyList())); } - return make.DocComment(firstSentence, body, tags); + + boolean[] useMarkdown = new boolean[1]; + TreePath tp = javac.getTrees().getPath(elm); + + if (tp != null) { + new TreePathScanner() { + private boolean seenJavadoc; + @Override + public Void scan(Tree tree, Void p) { + if (seenJavadoc) { + return null; + } + + return super.scan(tree, p); + } + + @Override + public Void visitVariable(VariableTree node, Void p) { + checkJavadoc(); + return super.visitVariable(node, p); + } + + @Override + public Void visitMethod(MethodTree node, Void p) { + checkJavadoc(); + return super.visitMethod(node, p); + } + + @Override + public Void visitClass(ClassTree node, Void p) { + checkJavadoc(); + return super.visitClass(node, p); + } + private void checkJavadoc() { + DocCommentKind kind = javac.getDocTrees().getDocCommentKind(getCurrentPath()); + if (kind != null) { + useMarkdown[0] = kind == DocCommentKind.END_OF_LINE; + seenJavadoc |= useMarkdown[0]; + } + } + }.scan(tp.getCompilationUnit(), null); + } + + if (useMarkdown[0]) { + return make.MarkdownDocComment(firstSentence, body, tags); + } else { + return make.DocComment(firstSentence, body, tags); + } } /** diff --git a/java/javadoc/test/unit/src/org/netbeans/modules/javadoc/highlighting/HighlightingTest.java b/java/javadoc/test/unit/src/org/netbeans/modules/javadoc/highlighting/HighlightingTest.java index 2e6ec5be69ee..5dddd2548d0c 100644 --- a/java/javadoc/test/unit/src/org/netbeans/modules/javadoc/highlighting/HighlightingTest.java +++ b/java/javadoc/test/unit/src/org/netbeans/modules/javadoc/highlighting/HighlightingTest.java @@ -168,6 +168,17 @@ public void testHighlightsForJapaneseWithMultiPeriods() throws Exception { checkHighlights(content, new int[]{3, 9}); } + public void testHighlightsOnMultiLinesMarkdown() throws Exception { + String content + = """ + /// the 1st line + /// the 2nd line. + /// the 3rd line. + /// @author junichi11 + ///"""; + checkHighlights(content, new int[]{4, 17, 21, 34}); + } + /** * Check highlight ranges. * diff --git a/java/javadoc/test/unit/src/org/netbeans/modules/javadoc/hints/AddTagFixTest.java b/java/javadoc/test/unit/src/org/netbeans/modules/javadoc/hints/AddTagFixTest.java index e6f36f157284..9a2ee1496fae 100644 --- a/java/javadoc/test/unit/src/org/netbeans/modules/javadoc/hints/AddTagFixTest.java +++ b/java/javadoc/test/unit/src/org/netbeans/modules/javadoc/hints/AddTagFixTest.java @@ -698,4 +698,35 @@ public void testAddThrowsTagFix_NestedClass_160414() throws Exception { + " public static class MEx extends Exception {}\n" + "}\n"); } + + public void testAddTagMarkdown() throws Exception { + // issue 160414 + HintTest.create() + .input(""" + package test; + public class Test { + /// + /// @param p1 param1 + /// + public void leden(int p1, int p2) { + } + } + """) + .sourceLevel("23") + .run(JavadocHint.class) + .findWarning("5:30-5:36:warning:Missing @param tag for p2") //TODO: test branding + .applyFix("Add @param p2 tag") + .assertCompilable() + .assertOutput(""" + package test; + public class Test { + /// + /// @param p1 param1 + /// @param p2 + /// + public void leden(int p1, int p2) { + } + } + """); + } } diff --git a/java/javadoc/test/unit/src/org/netbeans/modules/javadoc/hints/GenerateJavadocFixTest.java b/java/javadoc/test/unit/src/org/netbeans/modules/javadoc/hints/GenerateJavadocFixTest.java index 80dbcb270ddb..36f86e3ee552 100644 --- a/java/javadoc/test/unit/src/org/netbeans/modules/javadoc/hints/GenerateJavadocFixTest.java +++ b/java/javadoc/test/unit/src/org/netbeans/modules/javadoc/hints/GenerateJavadocFixTest.java @@ -220,4 +220,105 @@ public void testGenerateEnumConstantJavadoc_124114b() throws Exception { " UNOR}\n"); } + public void testMarkdown1() throws Exception { + HintTest.create() + .input(""" + package test; + import java.io.IOException; + /// Test + public class Test { + public int map(int p1, int p2) throws IOException { + return -1; + } + } + """) + .preference(AVAILABILITY_KEY + true, true) + .preference(SCOPE_KEY, "private") + .run(JavadocHint.class) + .assertWarnings("4:15-4:18:hint:Missing javadoc.") + .findWarning("4:15-4:18:hint:Missing javadoc.") + .applyFix() + .assertCompilable() + .assertOutput(""" + package test; + import java.io.IOException; + /// Test + public class Test { + + /// + /// @param p1 + /// @param p2 + /// @return + /// @throws IOException + public int map(int p1, int p2) throws IOException { + return -1; + } + } + """); + } + + public void testMarkdown2() throws Exception { + HintTest.create() + .input(""" + package test; + import java.io.IOException; + /// Test + public class Test { + + /// + /// @param p1 + /// @param p2 + /// @return + /// @throws IOException + public int map(int p1, int p2) throws IOException { + return -1; + } + } + """) + .preference(AVAILABILITY_KEY + true, true) + .preference(SCOPE_KEY, "private") + .run(JavadocHint.class) + .assertWarnings(); + } + + public void testMarkdown3() throws Exception { + HintTest.create() + .input(""" + package test; + import java.io.IOException; + /** Not markdown */ + public class Test { + public int map(int p1, int p2) throws IOException { + return -1; + } + ///markdown + public void test() {} + } + """) + .preference(AVAILABILITY_KEY + true, true) + .preference(SCOPE_KEY, "private") + .run(JavadocHint.class) + .assertWarnings("4:15-4:18:hint:Missing javadoc.") + .findWarning("4:15-4:18:hint:Missing javadoc.") + .applyFix() + .assertCompilable() + .assertOutput(""" + package test; + import java.io.IOException; + /** Not markdown */ + public class Test { + + /// + /// @param p1 + /// @param p2 + /// @return + /// @throws IOException + public int map(int p1, int p2) throws IOException { + return -1; + } + ///markdown + public void test() {} + } + """); + } } diff --git a/java/javadoc/test/unit/src/org/netbeans/modules/javadoc/hints/RemoveTagFixTest.java b/java/javadoc/test/unit/src/org/netbeans/modules/javadoc/hints/RemoveTagFixTest.java index 7a61bcf8581c..902ad53b8fc1 100644 --- a/java/javadoc/test/unit/src/org/netbeans/modules/javadoc/hints/RemoveTagFixTest.java +++ b/java/javadoc/test/unit/src/org/netbeans/modules/javadoc/hints/RemoveTagFixTest.java @@ -388,4 +388,30 @@ public void testRemoveParamTagFix_124353() throws Exception { "}\n"); } + public void testRemoveParamTagFixMarkdown() throws Exception { + HintTest.create() + .input(""" + package test; + class Zima { + /// + /// @param p1 description + void leden() { + } + } + """) + .preference(AVAILABILITY_KEY + true, true) + .preference(SCOPE_KEY, "private") + .run(JavadocHint.class) + .findWarning("3:8-3:29:warning:Unknown @param: p1") + .applyFix("Remove @param tag") + .assertCompilable() + .assertOutput(""" + package test; + class Zima { + /// + void leden() { + } + } + """); + } }