diff --git a/build.xml b/build.xml index fa2607b2..1a8f4bc4 100644 --- a/build.xml +++ b/build.xml @@ -48,6 +48,14 @@ patches/7610.diff patches/8036-draft.diff patches/8038-draft.diff + patches/8210.diff + patches/8237.diff + patches/8242.diff + patches/8245.diff + patches/8255.diff + patches/8260.diff + patches/8280.diff + patches/8289.diff patches/mvn-sh.diff patches/project-marker-jdk.diff patches/generate-dependencies.diff diff --git a/patches/8210.diff b/patches/8210.diff new file mode 100644 index 00000000..31bcf104 --- /dev/null +++ b/patches/8210.diff @@ -0,0 +1,435 @@ +diff --git a/java/java.editor/nbproject/project.xml b/java/java.editor/nbproject/project.xml +index 0b5a6e5d585a..7c09e3513d2a 100644 +--- a/java/java.editor/nbproject/project.xml ++++ b/java/java.editor/nbproject/project.xml +@@ -558,6 +558,10 @@ + + + ++ ++ org.netbeans.modules.lexer.nbbridge ++ ++ + + org.netbeans.modules.masterfs + +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 bddc6dbc8e8c..09437294e148 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 +@@ -221,26 +221,35 @@ public static Completion.Kind elementKind2CompletionItemKind(ElementKind kind) { + } + } + +- public static Supplier> addImport(Document doc, int offset, ElementHandle handle) { ++ public static Supplier> addImportAndInjectPackageIfNeeded(Document doc, int offset, ElementHandle handle) { + return () -> { +- AtomicReference pkg = new AtomicReference<>(); +- List textEdits = modify2TextEdits(JavaSource.forDocument(doc), copy -> { +- copy.toPhase(JavaSource.Phase.RESOLVED); +- String fqn = SourceUtils.resolveImport(copy, copy.getTreeUtilities().pathFor(offset), handle.getQualifiedName()); +- if (fqn != null) { +- int idx = fqn.lastIndexOf('.'); +- if (idx >= 0) { +- pkg.set(fqn.substring(0, idx + 1)); +- } +- } +- }); +- if (textEdits.isEmpty() && pkg.get() != null) { +- textEdits.add(new TextEdit(offset, offset, pkg.get())); ++ ResolvedImport resolved = addImport(doc, offset, handle); ++ List edits; ++ String insertName = resolved.insertName(); ++ int dotIdx = insertName.lastIndexOf('.'); ++ if (dotIdx >= 0) { ++ edits = new ArrayList<>(resolved.importEdits().size() + 1); ++ edits.addAll(resolved.importEdits()); ++ edits.add(new TextEdit(offset, offset, insertName.substring(0, dotIdx + 1))); ++ } else { ++ edits = resolved.importEdits(); + } +- return textEdits; ++ return edits; + }; + } + ++ public static ResolvedImport addImport(Document doc, int offset, ElementHandle handle) { ++ AtomicReference insertName = new AtomicReference<>(); ++ List textEdits = modify2TextEdits(JavaSource.forDocument(doc), copy -> { ++ copy.toPhase(JavaSource.Phase.RESOLVED); ++ insertName.set(SourceUtils.resolveImport(copy, copy.getTreeUtilities().pathFor(offset), handle.getQualifiedName())); ++ }); ++ ++ return new ResolvedImport(textEdits, insertName.get()); ++ } ++ ++ public record ResolvedImport(List importEdits, String insertName) {} ++ + public static boolean isOfKind(Element e, EnumSet kinds) { + if (kinds.contains(e.getKind())) { + return true; +@@ -666,24 +675,19 @@ public Completion createAttributeValueItem(CompilationInfo info, String value, S + + @Override + public Completion createStaticMemberItem(CompilationInfo info, DeclaredType type, Element memberElem, TypeMirror memberType, boolean multipleVersions, int substitutionOffset, boolean isDeprecated, boolean addSemicolon, boolean smartType) { +- //TODO: prefer static imports (but would be much slower?) +- //TODO: should be resolveImport instead of addImports: +- Map imports = (Map) info.getCachedValue(KEY_IMPORT_TEXT_EDITS); ++ Map imports = (Map) info.getCachedValue(KEY_IMPORT_TEXT_EDITS); + if (imports == null) { + info.putCachedValue(KEY_IMPORT_TEXT_EDITS, imports = new HashMap<>(), CompilationInfo.CacheClearPolicy.ON_TASK_END); + } +- TextEdit currentClassImport = imports.computeIfAbsent(type.asElement(), toImport -> { +- List textEdits = modify2TextEdits(JavaSource.forFileObject(info.getFileObject()), wc -> { +- wc.toPhase(JavaSource.Phase.ELEMENTS_RESOLVED); +- wc.rewrite(info.getCompilationUnit(), GeneratorUtilities.get(wc).addImports(wc.getCompilationUnit(), new HashSet<>(Arrays.asList(toImport)))); +- }); +- return textEdits.isEmpty() ? null : textEdits.get(0); ++ ResolvedImport currentClassImport = imports.computeIfAbsent(type.asElement(), toImport -> { ++ return addImport(doc, offset, ElementHandle.create(toImport)); + }); + String label = type.asElement().getSimpleName() + "." + memberElem.getSimpleName(); + String sortText = memberElem.getSimpleName().toString(); + String memberTypeName; + StringBuilder labelDetail = new StringBuilder(); +- StringBuilder insertText = new StringBuilder(label); ++ StringBuilder insertText = new StringBuilder(); ++ insertText.append(currentClassImport.insertName()).append(".").append(memberElem.getSimpleName()); + boolean asTemplate = false; + if (memberElem.getKind().isField()) { + memberTypeName = Utilities.getTypeName(info, memberType, false).toString(); +@@ -740,12 +744,12 @@ public Completion createStaticMemberItem(CompilationInfo info, DeclaredType type + .labelDescription(memberTypeName) + .insertText(insertText.toString()) + .insertTextFormat(asTemplate ? Completion.TextFormat.Snippet : Completion.TextFormat.PlainText) +- .sortText(String.format("%04d%s", (memberElem.getKind().isField() ? 720 : 750) + (smartType ? 1000 : 0), sortText)); ++ .sortText(String.format("%04d%s", (memberElem.getKind().isField() ? 720 : 750) + (smartType ? 0: 1000), sortText)); + if (labelDetail.length() > 0) { + builder.labelDetail(labelDetail.toString()); + } +- if (currentClassImport != null) { +- builder.additionalTextEdits(Collections.singletonList(currentClassImport)); ++ if (!currentClassImport.importEdits().isEmpty()) { ++ builder.additionalTextEdits(currentClassImport.importEdits()); + } + ElementHandle handle = SUPPORTED_ELEMENT_KINDS.contains(memberElem.getKind().name()) ? ElementHandle.create(memberElem) : null; + if (handle != null) { +@@ -1041,7 +1045,7 @@ private Completion createTypeItem(CompilationInfo info, String prefix, ElementHa + if (handle != null) { + builder.documentation(getDocumentation(doc, off, handle)); + if (!addSimpleName && !inImport) { +- builder.additionalTextEdits(addImport(doc, off, handle)); ++ builder.additionalTextEdits(addImportAndInjectPackageIfNeeded(doc, off, handle)); + } + } + if (isDeprecated) { +diff --git a/java/java.editor/src/org/netbeans/modules/java/editor/javadoc/JavadocCompletionCollector.java b/java/java.editor/src/org/netbeans/modules/java/editor/javadoc/JavadocCompletionCollector.java +index 0289b2dcb13f..f7955ef64cf0 100644 +--- a/java/java.editor/src/org/netbeans/modules/java/editor/javadoc/JavadocCompletionCollector.java ++++ b/java/java.editor/src/org/netbeans/modules/java/editor/javadoc/JavadocCompletionCollector.java +@@ -276,7 +276,7 @@ private Completion createTypeItem(ElementHandle handle, TypeElement + } + if (handle != null) { + builder.documentation(JavaCompletionCollector.getDocumentation(doc, offset, handle)); +- builder.additionalTextEdits(JavaCompletionCollector.addImport(doc, offset, handle)); ++ builder.additionalTextEdits(JavaCompletionCollector.addImportAndInjectPackageIfNeeded(doc, offset, handle)); + } + if (isDeprecated) { + builder.addTag(Completion.Tag.Deprecated); +diff --git a/java/java.editor/test/unit/src/org/netbeans/modules/editor/java/JavaCompletionCollectorTest.java b/java/java.editor/test/unit/src/org/netbeans/modules/editor/java/JavaCompletionCollectorTest.java +new file mode 100644 +index 000000000000..d6e3d5669e65 +--- /dev/null ++++ b/java/java.editor/test/unit/src/org/netbeans/modules/editor/java/JavaCompletionCollectorTest.java +@@ -0,0 +1,291 @@ ++/* ++ * 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.modules.editor.java; ++ ++import java.util.ArrayList; ++import java.util.List; ++import java.util.concurrent.atomic.AtomicBoolean; ++import java.util.stream.Collectors; ++import javax.swing.text.Document; ++import static junit.framework.TestCase.assertNotNull; ++import static junit.framework.TestCase.assertTrue; ++import org.netbeans.api.editor.mimelookup.MimePath; ++import org.netbeans.api.java.lexer.JavaTokenId; ++import org.netbeans.api.java.source.JavaSource; ++import org.netbeans.api.java.source.SourceUtilsTestUtil; ++import org.netbeans.api.java.source.TestUtilities; ++import org.netbeans.api.lsp.Completion; ++import org.netbeans.api.lsp.Completion.Context; ++import org.netbeans.api.lsp.Completion.TriggerKind; ++import org.netbeans.api.lsp.TextEdit; ++import org.netbeans.junit.NbTestCase; ++import org.netbeans.spi.editor.mimelookup.MimeDataProvider; ++import org.openide.cookies.EditorCookie; ++import org.openide.filesystems.FileObject; ++import org.openide.filesystems.FileUtil; ++import org.openide.filesystems.MIMEResolver; ++import org.openide.util.Lookup; ++import org.openide.util.lookup.Lookups; ++ ++public class JavaCompletionCollectorTest extends NbTestCase { ++ ++ public JavaCompletionCollectorTest(String name) { ++ super(name); ++ } ++ ++ public void testStaticMembersAndImports() throws Exception { ++ AtomicBoolean found = new AtomicBoolean(); ++ runJavaCollector(List.of(new FileDescription("test/Test.java", ++ """ ++ package test; ++ public class Test { ++ public void test() { ++ if (call(|)) { ++ } ++ } ++ private boolean call(test.other.E e) { ++ return false; ++ } ++ } ++ """), ++ new FileDescription("test/other/E.java", ++ """ ++ package test.other; ++ public enum E { ++ A, B, C; ++ } ++ """)), ++ completions -> { ++ for (Completion completion : completions) { ++ if (completion.getLabel().equals("E.A")) { ++ assertEquals("14-14:\\nimport test.other.E;\\n\\n", ++ completion.getAdditionalTextEdits() ++ .get() ++ .stream() ++ .map(JavaCompletionCollectorTest::textEdit2String) ++ .collect(Collectors.joining(", "))); ++ assertEquals("E.A", ++ completion.getInsertText()); ++ found.set(true); ++ } ++ } ++ }); ++ assertTrue(found.get()); ++ } ++ ++ public void testStaticMembersAndNoImports() throws Exception { ++ AtomicBoolean found = new AtomicBoolean(); ++ runJavaCollector(List.of(new FileDescription("test/Test.java", ++ """ ++ package test; ++ public class Test { ++ public void test() { ++ if (call(|)) { ++ } ++ } ++ private boolean call(Outter.E e) { ++ return false; ++ } ++ } ++ class Outter { ++ public enum E { ++ A, B, C; ++ } ++ } ++ """)), ++ completions -> { ++ for (Completion completion : completions) { ++ if (completion.getLabel().equals("E.A")) { ++ assertNull(completion.getAdditionalTextEdits()); ++ assertEquals("Outter.E.A", ++ completion.getInsertText()); ++ found.set(true); ++ } ++ } ++ }); ++ assertTrue(found.get()); ++ } ++ ++ public void testTypeFromIndex1() throws Exception { ++ AtomicBoolean found = new AtomicBoolean(); ++ runJavaCollector(List.of(new FileDescription("test/Test.java", ++ """ ++ package test; ++ public class Test { ++ public void test() { ++ EEE| ++ } ++ } ++ """), ++ new FileDescription("test/other/EEE.java", ++ """ ++ package test.other; ++ public class EEE { ++ } ++ """)), ++ completions -> { ++ for (Completion completion : completions) { ++ if (completion.getLabel().equals("EEE")) { ++ assertEquals("14-14:\\nimport test.other.EEE;\\n\\n", ++ completion.getAdditionalTextEdits() ++ .get() ++ .stream() ++ .map(JavaCompletionCollectorTest::textEdit2String) ++ .collect(Collectors.joining(", "))); ++ assertEquals("EEE", ++ completion.getInsertText()); ++ found.set(true); ++ } ++ } ++ }); ++ assertTrue(found.get()); ++ } ++ ++ public void testTypeFromIndex2() throws Exception { ++ AtomicBoolean found = new AtomicBoolean(); ++ runJavaCollector(List.of(new FileDescription("test/Test.java", ++ """ ++ package test; ++ public class Test { ++ public void test() { ++ EEE| ++ } ++ interface EEE {} ++ } ++ """), ++ new FileDescription("test/other/EEE.java", ++ """ ++ package test.other; ++ public class EEE { ++ } ++ """)), ++ completions -> { ++ for (Completion completion : completions) { ++ if (completion.getLabel().equals("EEE") && ++ completion.getKind() == Completion.Kind.Class) { ++ assertEquals("67-67:test.other.", ++ completion.getAdditionalTextEdits() ++ .get() ++ .stream() ++ .map(JavaCompletionCollectorTest::textEdit2String) ++ .collect(Collectors.joining(", "))); ++ assertEquals("EEE", ++ completion.getInsertText()); ++ found.set(true); ++ } ++ } ++ }); ++ assertTrue(found.get()); ++ } ++ ++ private void runJavaCollector(List files, Validator> validator) throws Exception { ++ SourceUtilsTestUtil.prepareTest(new String[]{"org/netbeans/modules/java/editor/resources/layer.xml"}, new Object[]{new MIMEResolverImpl(), new MIMEDataProvider()}); ++ ++ FileObject scratch = SourceUtilsTestUtil.makeScratchDir(this); ++ FileObject cache = scratch.createFolder("cache"); ++ FileObject src = scratch.createFolder("src"); ++ FileObject mainFile = null; ++ int caretPosition = -1; ++ ++ for (FileDescription testFile : files) { ++ FileObject testFO = FileUtil.createData(src, testFile.fileName); ++ String code = testFile.code; ++ ++ if (mainFile == null) { ++ mainFile = testFO; ++ caretPosition = code.indexOf('|'); ++ ++ assertTrue(caretPosition >= 0); ++ ++ code = code.substring(0, caretPosition) + code.substring(caretPosition + 1); ++ } ++ ++ TestUtilities.copyStringToFile(testFO, code); ++ } ++ ++ assertNotNull(mainFile); ++ ++ if (sourceLevel != null) { ++ SourceUtilsTestUtil.setSourceLevel(mainFile, sourceLevel); ++ } ++ ++ SourceUtilsTestUtil.prepareTest(src, FileUtil.createFolder(scratch, "test-build"), cache); ++ SourceUtilsTestUtil.compileRecursively(src); ++ ++ EditorCookie ec = mainFile.getLookup().lookup(EditorCookie.class); ++ Document doc = ec.openDocument(); ++ JavaCompletionCollector collector = new JavaCompletionCollector(); ++ Context ctx = new Context(TriggerKind.Invoked, null); ++ List completions = new ArrayList<>(); ++ ++ JavaSource.forDocument(doc).runUserActionTask(cc -> { ++ cc.toPhase(JavaSource.Phase.RESOLVED); ++ }, true); ++ collector.collectCompletions(doc, caretPosition, ctx, completions::add); ++ validator.validate(completions); ++ } ++ ++ private static String textEdit2String(TextEdit te) { ++ return te.getStartOffset() + "-" + te.getEndOffset() + ":" + te.getNewText().replace("\n", "\\n"); ++ } ++ ++ private String sourceLevel; ++ ++ private final void setSourceLevel(String sourceLevel) { ++ this.sourceLevel = sourceLevel; ++ } ++ ++ interface Validator { ++ public void validate(T t) throws Exception; ++ } ++ ++ static class MIMEResolverImpl extends MIMEResolver { ++ ++ public String findMIMEType(FileObject fo) { ++ if ("java".equals(fo.getExt())) { ++ return "text/x-java"; ++ } else { ++ return null; ++ } ++ } ++ } ++ ++ static class MIMEDataProvider implements MimeDataProvider { ++ ++ @Override ++ public Lookup getLookup(MimePath mimePath) { ++ if ("text/x-java".equals(mimePath.getPath())) { ++ return Lookups.fixed(JavaTokenId.language()); ++ } ++ return null; ++ } ++ ++ } ++ ++ private static final class FileDescription { ++ ++ final String fileName; ++ final String code; ++ ++ public FileDescription(String fileName, String code) { ++ this.fileName = fileName; ++ this.code = code; ++ } ++ } ++} diff --git a/patches/8237.diff b/patches/8237.diff new file mode 100644 index 00000000..81a73761 --- /dev/null +++ b/patches/8237.diff @@ -0,0 +1,358 @@ +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 ac22658fe54f..567481a37952 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 +@@ -91,6 +91,10 @@ public static interface ItemFactory { + + T createExecutableItem(CompilationInfo info, ExecutableElement elem, ExecutableType type, int substitutionOffset, ReferencesCount referencesCount, boolean isInherited, boolean isDeprecated, boolean inImport, boolean addSemicolon, boolean smartType, int assignToVarOffset, boolean memberRef); + ++ default T createExecutableItem(CompilationInfo info, ExecutableElement elem, ExecutableType type, int substitutionOffset, ReferencesCount referencesCount, boolean isInherited, boolean isDeprecated, boolean inImport, boolean addSemicolon, boolean afterConstructorTypeParams, boolean smartType, int assignToVarOffset, boolean memberRef) { ++ return createExecutableItem(info, elem, type, substitutionOffset, referencesCount, isInherited, isDeprecated, inImport, addSemicolon, smartType, assignToVarOffset, memberRef); ++ } ++ + T createThisOrSuperConstructorItem(CompilationInfo info, ExecutableElement elem, ExecutableType type, int substitutionOffset, boolean isDeprecated, String name); + + T createOverrideMethodItem(CompilationInfo info, ExecutableElement elem, ExecutableType type, int substitutionOffset, boolean implement); +@@ -1866,7 +1870,7 @@ private void insideMemberSelect(Env env) throws IOException { + String typeName = controller.getElementUtilities().getElementName(el, true) + "." + prefix; //NOI18N + TypeMirror tm = controller.getTreeUtilities().parseType(typeName, env.getScope().getEnclosingClass()); + if (tm != null && tm.getKind() == TypeKind.DECLARED) { +- addMembers(env, tm, ((DeclaredType) tm).asElement(), EnumSet.of(CONSTRUCTOR), null, inImport, insideNew, false, switchItemAdder); ++ addMembers(env, tm, ((DeclaredType) tm).asElement(), EnumSet.of(CONSTRUCTOR), null, inImport, insideNew, false, false, switchItemAdder); + } + } + } +@@ -1901,7 +1905,7 @@ private void insideMemberSelect(Env env) throws IOException { + } + } + } +- addMembers(env, type, el, kinds, baseType, inImport, insideNew, false, switchItemAdder); ++ addMembers(env, type, el, kinds, baseType, inImport, insideNew, false, false, switchItemAdder); + } + break; + default: +@@ -1914,7 +1918,7 @@ private void insideMemberSelect(Env env) throws IOException { + String typeName = controller.getElementUtilities().getElementName(el, true) + "." + prefix; //NOI18N + TypeMirror tm = controller.getTreeUtilities().parseType(typeName, env.getScope().getEnclosingClass()); + if (tm != null && tm.getKind() == TypeKind.DECLARED) { +- addMembers(env, tm, ((DeclaredType) tm).asElement(), EnumSet.of(CONSTRUCTOR), null, inImport, insideNew, false, switchItemAdder); ++ addMembers(env, tm, ((DeclaredType) tm).asElement(), EnumSet.of(CONSTRUCTOR), null, inImport, insideNew, false, false, switchItemAdder); + } + } + if (exs != null && !exs.isEmpty()) { +@@ -1939,7 +1943,7 @@ private void insideMemberSelect(Env env) throws IOException { + for (ElementHandle teHandle : ci.getDeclaredTypes(el.getSimpleName().toString(), ClassIndex.NameKind.SIMPLE_NAME, EnumSet.allOf(ClassIndex.SearchScope.class))) { + TypeElement te = teHandle.resolve(controller); + if (te != null && trees.isAccessible(scope, te)) { +- addMembers(env, te.asType(), te, kinds, baseType, inImport, insideNew, true, switchItemAdder); ++ addMembers(env, te.asType(), te, kinds, baseType, inImport, insideNew, true, false, switchItemAdder); + } + } + } +@@ -2215,7 +2219,7 @@ private void insideNewClass(Env env) throws IOException { + case GTGTGT: + controller = env.getController(); + TypeMirror tm = controller.getTrees().getTypeMirror(new TreePath(path, nc.getIdentifier())); +- addMembers(env, tm, ((DeclaredType) tm).asElement(), EnumSet.of(CONSTRUCTOR), null, false, false, false); ++ addMembers(env, tm, ((DeclaredType) tm).asElement(), EnumSet.of(CONSTRUCTOR), null, false, false, false, true, addSwitchItemDefault); + break; + } + } +@@ -4073,10 +4077,10 @@ public boolean accept(Element e, TypeMirror t) { + } + + private void addMembers(final Env env, final TypeMirror type, final Element elem, final EnumSet kinds, final DeclaredType baseType, final boolean inImport, final boolean insideNew, final boolean autoImport) throws IOException { +- addMembers(env, type, elem, kinds, baseType, inImport, insideNew, autoImport, addSwitchItemDefault); ++ addMembers(env, type, elem, kinds, baseType, inImport, insideNew, autoImport, false, addSwitchItemDefault); + } + +- private void addMembers(final Env env, final TypeMirror type, final Element elem, final EnumSet kinds, final DeclaredType baseType, final boolean inImport, final boolean insideNew, final boolean autoImport, AddSwitchRelatedItem addSwitchItem) throws IOException { ++ private void addMembers(final Env env, final TypeMirror type, final Element elem, final EnumSet kinds, final DeclaredType baseType, final boolean inImport, final boolean insideNew, final boolean autoImport, final boolean afterConstructorTypeParams, AddSwitchRelatedItem addSwitchItem) throws IOException { + Set smartTypes = getSmartTypes(env); + final TreePath path = env.getPath(); + TypeMirror actualType = type; +@@ -4220,7 +4224,7 @@ && isOfKindAndType(e.getEnclosingElement().asType(), e, kinds, baseType, scope, + break; + case CONSTRUCTOR: + ExecutableType et = (ExecutableType) asMemberOf(e, actualType, types); +- results.add(itemFactory.createExecutableItem(env.getController(), (ExecutableElement) e, et, anchorOffset, autoImport ? env.getReferencesCount() : null, typeElem != e.getEnclosingElement(), elements.isDeprecated(e), inImport, false, isOfSmartType(env, actualType, smartTypes), env.assignToVarPos(), false)); ++ results.add(itemFactory.createExecutableItem(env.getController(), (ExecutableElement) e, et, anchorOffset, autoImport ? env.getReferencesCount() : null, typeElem != e.getEnclosingElement(), elements.isDeprecated(e), inImport, false, afterConstructorTypeParams, isOfSmartType(env, actualType, smartTypes), env.assignToVarPos(), false)); + break; + case METHOD: + et = (ExecutableType) asMemberOf(e, actualType, types); +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 09437294e148..4eb78d72617c 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 +@@ -482,17 +482,22 @@ public Completion createVariableItem(CompilationInfo info, String varName, int s + + @Override + public Completion createExecutableItem(CompilationInfo info, ExecutableElement elem, ExecutableType type, int substitutionOffset, ReferencesCount referencesCount, boolean isInherited, boolean isDeprecated, boolean inImport, boolean addSemicolon, boolean smartType, int assignToVarOffset, boolean memberRef) { +- return createExecutableItem(info, elem, type, null, null, substitutionOffset, referencesCount, isInherited, isDeprecated, inImport, addSemicolon, smartType, assignToVarOffset, memberRef); ++ return createExecutableItem(info, elem, type, substitutionOffset, referencesCount, isInherited, isDeprecated, inImport, addSemicolon, false, smartType, assignToVarOffset, memberRef); ++ } ++ ++ @Override ++ public Completion createExecutableItem(CompilationInfo info, ExecutableElement elem, ExecutableType type, int substitutionOffset, ReferencesCount referencesCount, boolean isInherited, boolean isDeprecated, boolean inImport, boolean addSemicolon, boolean afterConstructorTypeParams, boolean smartType, int assignToVarOffset, boolean memberRef) { ++ return createExecutableItem(info, elem, type, null, null, substitutionOffset, referencesCount, isInherited, isDeprecated, inImport, addSemicolon, afterConstructorTypeParams, smartType, assignToVarOffset, memberRef); + } + + @Override + public Completion createTypeCastableExecutableItem(CompilationInfo info, ExecutableElement elem, ExecutableType type, TypeMirror castType, int substitutionOffset, ReferencesCount referencesCount, boolean isInherited, boolean isDeprecated, boolean inImport, boolean addSemicolon, boolean smartType, int assignToVarOffset, boolean memberRef) { +- return createExecutableItem(info, elem, type, null, castType, substitutionOffset, referencesCount, isInherited, isDeprecated, inImport, addSemicolon, smartType, assignToVarOffset, memberRef); ++ return createExecutableItem(info, elem, type, null, castType, substitutionOffset, referencesCount, isInherited, isDeprecated, inImport, addSemicolon, false, smartType, assignToVarOffset, memberRef); + } + + @Override + public Completion createThisOrSuperConstructorItem(CompilationInfo info, ExecutableElement elem, ExecutableType type, int substitutionOffset, boolean isDeprecated, String name) { +- return createExecutableItem(info, elem, type, name, null, substitutionOffset, null, false, isDeprecated, false, false, false, -1, false); ++ return createExecutableItem(info, elem, type, name, null, substitutionOffset, null, false, isDeprecated, false, false, false, false, -1, false); + } + + @Override +@@ -1054,14 +1059,16 @@ private Completion createTypeItem(CompilationInfo info, String prefix, ElementHa + return builder.build(); + } + +- private Completion createExecutableItem(CompilationInfo info, ExecutableElement elem, ExecutableType type, String name, TypeMirror castType, int substitutionOffset, ReferencesCount referencesCount, boolean isInherited, boolean isDeprecated, boolean inImport, boolean addSemicolon, boolean smartType, int assignToVarOffset, boolean memberRef) { ++ private Completion createExecutableItem(CompilationInfo info, ExecutableElement elem, ExecutableType type, String name, TypeMirror castType, int substitutionOffset, ReferencesCount referencesCount, boolean isInherited, boolean isDeprecated, boolean inImport, boolean addSemicolon, boolean afterConstructorTypeParams, boolean smartType, int assignToVarOffset, boolean memberRef) { + String simpleName = name != null ? name : (elem.getKind() == ElementKind.METHOD ? elem : elem.getEnclosingElement()).getSimpleName().toString(); + Iterator it = elem.getParameters().iterator(); + Iterator tIt = type.getParameterTypes().iterator(); + StringBuilder labelDetail = new StringBuilder(); + StringBuilder insertText = new StringBuilder(); + StringBuilder sortParams = new StringBuilder(); +- insertText.append(simpleName); ++ if (!afterConstructorTypeParams) { ++ insertText.append(simpleName); ++ } + labelDetail.append("("); + CodeStyle cs = CodeStyle.getDefault(doc); + if (!inImport && !memberRef) { +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 09945ede0eaa..6c16e6df828d 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 +@@ -191,12 +191,12 @@ public static JavaCompletionItem createVariableItem(CompilationInfo info, String + return new VariableItem(info, null, varName, substitutionOffset, newVarName, smartType, -1); + } + +- public static JavaCompletionItem createExecutableItem(CompilationInfo info, ExecutableElement elem, ExecutableType type, TypeMirror castType, int substitutionOffset, ReferencesCount referencesCount, boolean isInherited, boolean isDeprecated, boolean inImport, boolean addSemicolon, boolean smartType, int assignToVarOffset, boolean memberRef, WhiteListQuery.WhiteList whiteList) { ++ public static JavaCompletionItem createExecutableItem(CompilationInfo info, ExecutableElement elem, ExecutableType type, TypeMirror castType, int substitutionOffset, ReferencesCount referencesCount, boolean isInherited, boolean isDeprecated, boolean inImport, boolean addSemicolon, boolean afterConstructorTypeParams, boolean smartType, int assignToVarOffset, boolean memberRef, WhiteListQuery.WhiteList whiteList) { + switch (elem.getKind()) { + case METHOD: + return new MethodItem(info, elem, type, castType, substitutionOffset, referencesCount, isInherited, isDeprecated, inImport, addSemicolon, smartType, assignToVarOffset, memberRef, whiteList); + case CONSTRUCTOR: +- return new ConstructorItem(info, elem, type, substitutionOffset, isDeprecated, smartType, null, whiteList); ++ return new ConstructorItem(info, elem, type, substitutionOffset, isDeprecated, afterConstructorTypeParams, smartType, null, whiteList); + default: + throw new IllegalArgumentException("kind=" + elem.getKind()); + } +@@ -204,7 +204,7 @@ public static JavaCompletionItem createExecutableItem(CompilationInfo info, Exec + + public static JavaCompletionItem createThisOrSuperConstructorItem(CompilationInfo info, ExecutableElement elem, ExecutableType type, int substitutionOffset, boolean isDeprecated, String name, WhiteListQuery.WhiteList whiteList) { + if (elem.getKind() == ElementKind.CONSTRUCTOR) { +- return new ConstructorItem(info, elem, type, substitutionOffset, isDeprecated, false, name, whiteList); ++ return new ConstructorItem(info, elem, type, substitutionOffset, isDeprecated, false, false, name, whiteList); + } + throw new IllegalArgumentException("kind=" + elem.getKind()); + } +@@ -2489,6 +2489,7 @@ static class ConstructorItem extends WhiteListJavaCompletionItem modifiers; + private List params; + private boolean isAbstract; +@@ -2497,11 +2498,12 @@ static class ConstructorItem extends WhiteListJavaCompletionItem(); +@@ -2551,7 +2553,7 @@ public CharSequence getSortText() { + + @Override + public CharSequence getInsertPrefix() { +- return simpleName; ++ return insertPrefix; + } + + @Override +diff --git a/java/java.editor/src/org/netbeans/modules/editor/java/JavaCompletionItemFactory.java b/java/java.editor/src/org/netbeans/modules/editor/java/JavaCompletionItemFactory.java +index ac08274cb7e4..3078d30455a7 100644 +--- a/java/java.editor/src/org/netbeans/modules/editor/java/JavaCompletionItemFactory.java ++++ b/java/java.editor/src/org/netbeans/modules/editor/java/JavaCompletionItemFactory.java +@@ -109,12 +109,17 @@ public JavaCompletionItem createVariableItem(CompilationInfo info, String varNam + + @Override + public JavaCompletionItem createExecutableItem(CompilationInfo info, ExecutableElement elem, ExecutableType type, int substitutionOffset, ReferencesCount referencesCount, boolean isInherited, boolean isDeprecated, boolean inImport, boolean addSemicolon, boolean smartType, int assignToVarOffset, boolean memberRef) { +- return JavaCompletionItem.createExecutableItem(info, elem, type, null, substitutionOffset, referencesCount, isInherited, isDeprecated, inImport, addSemicolon, smartType, assignToVarOffset, memberRef, whiteList); ++ return createExecutableItem(info, elem, type, substitutionOffset, referencesCount, isInherited, isDeprecated, inImport, addSemicolon, false, smartType, assignToVarOffset, memberRef); ++ } ++ ++ @Override ++ public JavaCompletionItem createExecutableItem(CompilationInfo info, ExecutableElement elem, ExecutableType type, int substitutionOffset, ReferencesCount referencesCount, boolean isInherited, boolean isDeprecated, boolean inImport, boolean addSemicolon, boolean afterConstructorTypeParams, boolean smartType, int assignToVarOffset, boolean memberRef) { ++ return JavaCompletionItem.createExecutableItem(info, elem, type, null, substitutionOffset, referencesCount, isInherited, isDeprecated, inImport, addSemicolon, afterConstructorTypeParams, smartType, assignToVarOffset, memberRef, whiteList); + } + + @Override + public JavaCompletionItem createTypeCastableExecutableItem(CompilationInfo info, ExecutableElement elem, ExecutableType type, TypeMirror castType, int substitutionOffset, ReferencesCount referencesCount, boolean isInherited, boolean isDeprecated, boolean inImport, boolean addSemicolon, boolean smartType, int assignToVarOffset, boolean memberRef) { +- return JavaCompletionItem.createExecutableItem(info, elem, type, castType, substitutionOffset, referencesCount, isInherited, isDeprecated, inImport, addSemicolon, smartType, assignToVarOffset, memberRef, whiteList); ++ return JavaCompletionItem.createExecutableItem(info, elem, type, castType, substitutionOffset, referencesCount, isInherited, isDeprecated, inImport, addSemicolon, false, smartType, assignToVarOffset, memberRef, whiteList); + } + + @Override +diff --git a/java/java.editor/src/org/netbeans/modules/java/editor/javadoc/JavadocCompletionItem.java b/java/java.editor/src/org/netbeans/modules/java/editor/javadoc/JavadocCompletionItem.java +index 22151927877c..9f35779388b7 100644 +--- a/java/java.editor/src/org/netbeans/modules/java/editor/javadoc/JavadocCompletionItem.java ++++ b/java/java.editor/src/org/netbeans/modules/java/editor/javadoc/JavadocCompletionItem.java +@@ -212,7 +212,7 @@ public CompletionItem createNameItem(String name, int startOffset) { + @Override + public CompletionItem createJavadocExecutableItem(CompilationInfo info, ExecutableElement e, ExecutableType et, int startOffset, boolean isInherited, boolean isDeprecated) { + CompletionItem delegate = JavaCompletionItem.createExecutableItem( +- info, e, et, null, startOffset, null, isInherited, isDeprecated, false, false, false, -1, false, null); ++ info, e, et, null, startOffset, null, isInherited, isDeprecated, false, false, false, false, -1, false, null); + return new JavadocExecutableItem(delegate, e, startOffset); + } + +diff --git a/java/java.editor/test/unit/src/org/netbeans/modules/editor/java/JavaCompletionCollectorTest.java b/java/java.editor/test/unit/src/org/netbeans/modules/editor/java/JavaCompletionCollectorTest.java +index d6e3d5669e65..bc1e4bdb87cf 100644 +--- a/java/java.editor/test/unit/src/org/netbeans/modules/editor/java/JavaCompletionCollectorTest.java ++++ b/java/java.editor/test/unit/src/org/netbeans/modules/editor/java/JavaCompletionCollectorTest.java +@@ -19,7 +19,9 @@ + package org.netbeans.modules.editor.java; + + import java.util.ArrayList; ++import java.util.HashSet; + import java.util.List; ++import java.util.Set; + import java.util.concurrent.atomic.AtomicBoolean; + import java.util.stream.Collectors; + import javax.swing.text.Document; +@@ -32,6 +34,7 @@ + import org.netbeans.api.java.source.TestUtilities; + import org.netbeans.api.lsp.Completion; + import org.netbeans.api.lsp.Completion.Context; ++import org.netbeans.api.lsp.Completion.TextFormat; + import org.netbeans.api.lsp.Completion.TriggerKind; + import org.netbeans.api.lsp.TextEdit; + import org.netbeans.junit.NbTestCase; +@@ -194,6 +197,108 @@ public class EEE { + assertTrue(found.get()); + } + ++ public void testNewWithTypeParameters() throws Exception { ++ Set found = new HashSet<>(); ++ ++ runJavaCollector(List.of(new FileDescription("test/Test.java", ++ """ ++ package test; ++ import java.util.*; ++ public class Test { ++ public void test() { ++ Map m = new HashMap<>| ++ } ++ } ++ """)), ++ completions -> { ++ for (Completion completion : completions) { ++ if ("HashMap".equals(completion.getLabel())) { ++ if ("()".equals(completion.getLabelDetail())) { ++ assertNull(completion.getTextEdit()); ++ assertNull(completion.getAdditionalTextEdits()); ++ assertEquals("()", completion.getInsertText()); ++ found.add("()"); ++ } else if (completion.getLabelDetail().contains("float")) { ++ assertNull(completion.getTextEdit()); ++ assertNull(completion.getAdditionalTextEdits()); ++ assertEquals(TextFormat.Snippet, completion.getInsertTextFormat()); ++ String insert = completion.getInsertText(); ++ assertNotNull(insert); ++ insert = insert.replaceFirst(":[^<>}]+\\}", ":\\}"); ++ insert = insert.replaceFirst(":[^<>}]+\\}", ":\\}"); ++ assertEquals("(${1:}, ${2:})$0", insert); ++ found.add("(int, float)"); ++ } ++ } ++ } ++ }); ++ assertEquals(Set.of("()", "(int, float)"), found); ++ } ++ ++ public void testNewWithoutTypeParameters() throws Exception { ++ Set found = new HashSet<>(); ++ ++ runJavaCollector(List.of(new FileDescription("test/Test.java", ++ """ ++ package test; ++ import java.util.*; ++ public class Test { ++ public void test() { ++ Map m = new HashMap| ++ } ++ } ++ """)), ++ completions -> { ++ for (Completion completion : completions) { ++ if ("HashMap".equals(completion.getLabel())) { ++ if ("()".equals(completion.getLabelDetail())) { ++ assertNull(completion.getTextEdit()); ++ assertNull(completion.getAdditionalTextEdits()); ++ assertEquals("HashMap()", completion.getInsertText()); ++ found.add("()"); ++ } else if (completion.getLabelDetail().contains("float")) { ++ assertNull(completion.getTextEdit()); ++ assertNull(completion.getAdditionalTextEdits()); ++ assertEquals(TextFormat.Snippet, completion.getInsertTextFormat()); ++ String insert = completion.getInsertText(); ++ assertNotNull(insert); ++ insert = insert.replaceFirst(":[^<>}]+\\}", ":\\}"); ++ insert = insert.replaceFirst(":[^<>}]+\\}", ":\\}"); ++ assertEquals("HashMap(${1:}, ${2:})$0", insert); ++ found.add("(int, float)"); ++ } ++ } ++ } ++ }); ++ assertEquals(Set.of("()", "(int, float)"), found); ++ } ++ ++ public void testAfterDot() throws Exception { ++ Set found = new HashSet<>(); ++ ++ runJavaCollector(List.of(new FileDescription("test/Test.java", ++ """ ++ package test; ++ import java.util.*; ++ public class Test { ++ public static Map test() { ++ Map m = Test.| ++ } ++ } ++ """)), ++ completions -> { ++ for (Completion completion : completions) { ++ if ("test".equals(completion.getLabel())) { ++ assertNull(completion.getTextEdit()); ++ assertNull(completion.getAdditionalTextEdits()); ++ assertEquals("test()", completion.getInsertText()); ++ found.add(completion.getLabelDetail()); ++ } ++ } ++ }); ++ assertEquals(Set.of("()"), found); ++ } ++ + private void runJavaCollector(List files, Validator> validator) throws Exception { + SourceUtilsTestUtil.prepareTest(new String[]{"org/netbeans/modules/java/editor/resources/layer.xml"}, new Object[]{new MIMEResolverImpl(), new MIMEDataProvider()}); + diff --git a/patches/8242.diff b/patches/8242.diff new file mode 100644 index 00000000..ab2fc6b0 --- /dev/null +++ b/patches/8242.diff @@ -0,0 +1,140 @@ +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 4eb78d72617c..788c3055aa8a 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 +@@ -39,6 +39,7 @@ + import java.util.Iterator; + import java.util.List; + import java.util.Map; ++import java.util.Objects; + import java.util.Set; + import java.util.concurrent.Callable; + import java.util.concurrent.CompletableFuture; +@@ -824,6 +825,7 @@ public Completion createInitializeAllConstructorItem(CompilationInfo info, boole + } + labelDetail.append(") - generate"); + sortParams.append(')'); ++ ElementHandle parentPath = ElementHandle.create(parent); + return CompletionCollector.newBuilder(simpleName) + .kind(Completion.Kind.Constructor) + .labelDetail(labelDetail.toString()) +@@ -834,7 +836,11 @@ public Completion createInitializeAllConstructorItem(CompilationInfo info, boole + wc.toPhase(JavaSource.Phase.ELEMENTS_RESOLVED); + TreePath tp = wc.getTreeUtilities().pathFor(substitutionOffset); + if (TreeUtilities.CLASS_TREE_KINDS.contains(tp.getLeaf().getKind())) { +- if (parent == wc.getTrees().getElement(tp)) { ++ Element currentType = wc.getTrees().getElement(tp); ++ ElementHandle currentTypePath = ++ currentType != null ? ElementHandle.create(currentType) ++ : null; ++ if (Objects.equals(parentPath, currentTypePath)) { + ArrayList fieldElements = new ArrayList<>(); + for (VariableElement fieldElement : fields) { + if (fieldElement != null && fieldElement.getKind().isField()) { +diff --git a/java/java.editor/test/unit/src/org/netbeans/modules/editor/java/JavaCompletionCollectorTest.java b/java/java.editor/test/unit/src/org/netbeans/modules/editor/java/JavaCompletionCollectorTest.java +index bc1e4bdb87cf..328a1b5bf62a 100644 +--- a/java/java.editor/test/unit/src/org/netbeans/modules/editor/java/JavaCompletionCollectorTest.java ++++ b/java/java.editor/test/unit/src/org/netbeans/modules/editor/java/JavaCompletionCollectorTest.java +@@ -18,6 +18,7 @@ + */ + package org.netbeans.modules.editor.java; + ++import java.io.OutputStream; + import java.util.ArrayList; + import java.util.HashSet; + import java.util.List; +@@ -48,6 +49,8 @@ + + public class JavaCompletionCollectorTest extends NbTestCase { + ++ private FileObject primaryTestFO; ++ + public JavaCompletionCollectorTest(String name) { + super(name); + } +@@ -299,21 +302,62 @@ public static Map test() { + assertEquals(Set.of("()"), found); + } + ++ public void testAdditionalEditsGenerateConstructorAfterReparse() throws Exception { ++ AtomicBoolean found = new AtomicBoolean(); ++ runJavaCollector(List.of(new FileDescription("test/Test.java", ++ """ ++ package test; ++ public class Test { ++ private final int i; ++ | ++ } ++ """)), ++ completions -> { ++ for (Completion completion : completions) { ++ if (completion.getLabel().equals("Test") && ++ "(int i) - generate".equals(completion.getLabelDetail())) { ++ //force full reparse: ++ byte[] content = primaryTestFO.asBytes(); ++ try (OutputStream out = primaryTestFO.getOutputStream()) { ++ out.write(content); ++ } ++ assertEquals(null, ++ completion.getInsertText()); ++ assertEquals("63-63:", ++ textEdit2String(completion.getTextEdit())); ++ assertEquals(""" ++ 59-59: ++ public Test(int i) { ++ this.i = i; ++ } ++ """.replace("\n", "\\n"), ++ completion.getAdditionalTextEdits() ++ .get() ++ .stream() ++ .map(JavaCompletionCollectorTest::textEdit2String) ++ .collect(Collectors.joining(", "))); ++ found.set(true); ++ } ++ } ++ }); ++ assertTrue(found.get()); ++ } ++ + private void runJavaCollector(List files, Validator> validator) throws Exception { + SourceUtilsTestUtil.prepareTest(new String[]{"org/netbeans/modules/java/editor/resources/layer.xml"}, new Object[]{new MIMEResolverImpl(), new MIMEDataProvider()}); + + FileObject scratch = SourceUtilsTestUtil.makeScratchDir(this); + FileObject cache = scratch.createFolder("cache"); + FileObject src = scratch.createFolder("src"); +- FileObject mainFile = null; ++ primaryTestFO = null; + int caretPosition = -1; + + for (FileDescription testFile : files) { + FileObject testFO = FileUtil.createData(src, testFile.fileName); + String code = testFile.code; + +- if (mainFile == null) { +- mainFile = testFO; ++ if (primaryTestFO == null) { ++ primaryTestFO = testFO; + caretPosition = code.indexOf('|'); + + assertTrue(caretPosition >= 0); +@@ -324,16 +368,16 @@ private void runJavaCollector(List files, Validator text) { + } + + public DocCommentTree DocComment(List fullBody, List tags) { +- DCDocComment temp = docMake.at(NOPOS).newDocCommentTree(fullBody, tags); +- return DocComment(temp.getFirstSentence(), temp.getBody(), temp.getBlockTags()); ++ return DocComment(HTML_JAVADOC_COMMENT, fullBody, tags); + } + + public DocCommentTree MarkdownDocComment(List fullBody, List tags) { +- DCDocComment temp = docMake.at(NOPOS).newDocCommentTree(fullBody, tags); +- return MarkdownDocComment(temp.getFirstSentence(), temp.getBody(), temp.getBlockTags()); ++ return DocComment(MARKDOWN_JAVADOC_COMMENT, fullBody, tags); + } + ++ private DocCommentTree DocComment(Comment comment, List fullBody, List tags) { ++ return docMake.at(NOPOS).newDocCommentTree(comment, fullBody, tags, Collections.emptyList(), Collections.emptyList()); ++ } ++ + public DocTree Snippet(List attributes, TextTree text){ + try { + return (DocTree) docMake.getClass().getMethod("newSnippetTree", List.class, TextTree.class).invoke(docMake.at(NOPOS), attributes, text); +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 037eb8251775..8b77849102a8 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 +@@ -45,6 +45,7 @@ + import com.sun.tools.javac.tree.DCTree.DCLink; + import com.sun.tools.javac.tree.DCTree.DCLiteral; + import com.sun.tools.javac.tree.DCTree.DCParam; ++import com.sun.tools.javac.tree.DCTree.DCRawText; + import com.sun.tools.javac.tree.DCTree.DCReference; + import com.sun.tools.javac.tree.DCTree.DCReturn; + import com.sun.tools.javac.tree.DCTree.DCSee; +@@ -4722,6 +4723,9 @@ private int diffDocTree(DCDocComment doc, DCTree oldT, DCTree newT, int[] elemen + case TEXT: + localpointer = diffText(doc, (DCText)oldT, (DCText)newT, elementBounds); + break; ++ case MARKDOWN: ++ localpointer = diffRawText(doc, (DCRawText)oldT, (DCRawText)newT, elementBounds); ++ break; + case AUTHOR: + localpointer = diffAuthor(doc, (DCAuthor)oldT, (DCAuthor)newT, elementBounds); + break; +@@ -4944,6 +4948,15 @@ private int diffText(DCDocComment doc, DCText oldT, DCText newT, int[] elementBo + return elementBounds[1]; + } + ++ private int diffRawText(DCDocComment doc, DCTree.DCRawText oldT, DCTree.DCRawText newT, int[] elementBounds) { ++ if(oldT.code.equals(newT.code)) { ++ copyTo(elementBounds[0], elementBounds[1]); ++ } else { ++ printer.print(newT.code); ++ } ++ return elementBounds[1]; ++ } ++ + private int diffAuthor(DCDocComment doc, DCAuthor oldT, DCAuthor newT, int[] elementBounds) { + int localpointer = oldT.name.isEmpty()? elementBounds[1] : getOldPos(oldT.name.get(0), doc); + copyTo(elementBounds[0], localpointer); +diff --git a/java/java.source.base/test/unit/src/org/netbeans/api/java/source/gen/RewriteInCommentTest.java b/java/java.source.base/test/unit/src/org/netbeans/api/java/source/gen/RewriteInCommentTest.java +index 5c3ce2e65646..5effccaf023d 100644 +--- a/java/java.source.base/test/unit/src/org/netbeans/api/java/source/gen/RewriteInCommentTest.java ++++ b/java/java.source.base/test/unit/src/org/netbeans/api/java/source/gen/RewriteInCommentTest.java +@@ -18,6 +18,13 @@ + */ + package org.netbeans.api.java.source.gen; + ++import com.sun.source.doctree.DocCommentTree; ++import com.sun.source.doctree.LinkTree; ++import com.sun.source.doctree.RawTextTree; ++import com.sun.source.doctree.ReferenceTree; ++import com.sun.source.util.DocTreePath; ++import com.sun.source.util.DocTreePathScanner; ++import com.sun.source.util.TreePath; + import java.io.File; + import java.io.IOException; + import org.netbeans.api.java.source.ModificationResult; +@@ -134,7 +141,128 @@ public void run(WorkingCopy copy) throws Exception { + + assertEquals(code.replace("test", "foo"), mr.getResultingSource(fo)); + } +- ++ ++ public void testDoNotBreakFormatting() throws Exception { ++ File f = new File(getWorkDir(), "TestClass.java"); ++ String code = """ ++ package foo; ++ /** ++ * First line. ++ * Test {@link #test}. ++ */ ++ public class TestClass{ ++ } ++ """; ++ TestUtilities.copyStringToFile(f, code); ++ FileObject fo = FileUtil.toFileObject(f); ++ JavaSource javaSource = JavaSource.forFileObject(fo); ++ ModificationResult mr = javaSource.runModificationTask(new Task() { ++ ++ public void run(WorkingCopy copy) throws Exception { ++ copy.toPhase(Phase.RESOLVED); ++ ++ TreePath topLevelClass = new TreePath(new TreePath(copy.getCompilationUnit()), ++ copy.getCompilationUnit().getTypeDecls().get(0)); ++ DocCommentTree docComment = copy.getDocTrees().getDocCommentTree(topLevelClass); ++ ++ new DocTreePathScanner<>() { ++ @Override ++ public Object visitReference(ReferenceTree rt, Object p) { ++ copy.rewrite(topLevelClass.getLeaf(), rt, copy.getTreeMaker().Reference(null, "newName", null)); ++ return null; ++ } ++ ++ @Override ++ public Object visitLink(LinkTree lt, Object p) { ++ return super.visitLink(lt, p); ++ } ++ }.scan(new DocTreePath(topLevelClass, docComment), null); ++ } ++ }); ++ ++ assertEquals(code.replace("test", "newName"), mr.getResultingSource(fo)); ++ } ++ ++ public void testDoNotBreakFormattingMarkdown() throws Exception { ++ File f = new File(getWorkDir(), "TestClass.java"); ++ String code = """ ++ package foo; ++ ++ /// First line. ++ /// Test {@link #test}. ++ public class TestClass{ ++ } ++ """; ++ TestUtilities.copyStringToFile(f, code); ++ FileObject fo = FileUtil.toFileObject(f); ++ JavaSource javaSource = JavaSource.forFileObject(fo); ++ ModificationResult mr = javaSource.runModificationTask(new Task() { ++ ++ public void run(WorkingCopy copy) throws Exception { ++ copy.toPhase(Phase.RESOLVED); ++ ++ TreePath topLevelClass = new TreePath(new TreePath(copy.getCompilationUnit()), ++ copy.getCompilationUnit().getTypeDecls().get(0)); ++ DocCommentTree docComment = copy.getDocTrees().getDocCommentTree(topLevelClass); ++ ++ new DocTreePathScanner<>() { ++ @Override ++ public Object visitReference(ReferenceTree rt, Object p) { ++ copy.rewrite(topLevelClass.getLeaf(), rt, copy.getTreeMaker().Reference(null, "newName", null)); ++ return null; ++ } ++ ++ @Override ++ public Object visitLink(LinkTree lt, Object p) { ++ return super.visitLink(lt, p); ++ } ++ }.scan(new DocTreePath(topLevelClass, docComment), null); ++ } ++ }); ++ ++ assertEquals(code.replace("test", "newName"), mr.getResultingSource(fo)); ++ } ++ ++ public void testMarkdownChangeText() throws Exception { ++ File f = new File(getWorkDir(), "TestClass.java"); ++ String code = """ ++ package foo; ++ ++ /// First line. ++ /// Second line. ++ public class TestClass{ ++ } ++ """; ++ TestUtilities.copyStringToFile(f, code); ++ FileObject fo = FileUtil.toFileObject(f); ++ JavaSource javaSource = JavaSource.forFileObject(fo); ++ ModificationResult mr = javaSource.runModificationTask(new Task() { ++ ++ public void run(WorkingCopy copy) throws Exception { ++ copy.toPhase(Phase.RESOLVED); ++ ++ TreePath topLevelClass = new TreePath(new TreePath(copy.getCompilationUnit()), ++ copy.getCompilationUnit().getTypeDecls().get(0)); ++ DocCommentTree docComment = copy.getDocTrees().getDocCommentTree(topLevelClass); ++ ++ new DocTreePathScanner<>() { ++ @Override ++ public Object visitDocComment(DocCommentTree dct, Object p) { ++ //XXX: need to translate full body, as the split body has different split of the text trees, and the differ uses fullbody: ++ return scan(dct.getFullBody(), p); ++ } ++ @Override ++ public Object visitRawText(RawTextTree text, Object p) { ++ copy.rewrite(topLevelClass.getLeaf(), text, copy.getTreeMaker().RawText(text.getContent().replace("line", "nueText"))); ++ return null; ++ } ++ }.scan(new DocTreePath(topLevelClass, docComment), null); ++ } ++ }); ++ ++ assertEquals(code.replace("line", "nueText"), mr.getResultingSource(fo)); ++ } ++ + String getGoldenPckg() { + return ""; + } diff --git a/patches/8255.diff b/patches/8255.diff new file mode 100644 index 00000000..72dd7b79 --- /dev/null +++ b/patches/8255.diff @@ -0,0 +1,90 @@ +diff --git a/java/java.source.base/src/org/netbeans/api/java/source/ClassIndex.java b/java/java.source.base/src/org/netbeans/api/java/source/ClassIndex.java +index 849672370e84..45bc3fea4ce7 100644 +--- a/java/java.source.base/src/org/netbeans/api/java/source/ClassIndex.java ++++ b/java/java.source.base/src/org/netbeans/api/java/source/ClassIndex.java +@@ -830,6 +830,8 @@ private void createQueriesForRoots (final ClassPath cp, final boolean sources, f + if (ci != null) { + ci.addClassIndexImplListener(spiListener); + queries.add (ci); ++ } else { ++ spiListener.attachClassIndexManagerListener(); + } + } + } +diff --git a/java/java.source.base/test/unit/src/org/netbeans/api/java/source/ClassIndexTest.java b/java/java.source.base/test/unit/src/org/netbeans/api/java/source/ClassIndexTest.java +index 674da8164972..49f0c32e8b00 100644 +--- a/java/java.source.base/test/unit/src/org/netbeans/api/java/source/ClassIndexTest.java ++++ b/java/java.source.base/test/unit/src/org/netbeans/api/java/source/ClassIndexTest.java +@@ -31,6 +31,8 @@ + import javax.lang.model.element.ElementKind; + import javax.lang.model.element.TypeElement; + import javax.swing.event.ChangeListener; ++import javax.tools.JavaFileObject; ++import javax.tools.ToolProvider; + import org.netbeans.api.java.classpath.ClassPath; + import org.netbeans.api.java.classpath.GlobalPathRegistry; + import org.netbeans.api.java.platform.JavaPlatformManager; +@@ -92,6 +94,7 @@ public static NbTestSuite suite() { + suite.addTest(new ClassIndexTest("testPackageUsages")); //NOI18N + suite.addTest(new ClassIndexTest("testNullRootPassedToClassIndexEvent")); //NOI18N + suite.addTest(new ClassIndexTest("testFindSymbols")); //NOI18N ++ suite.addTest(new ClassIndexTest("testQueryIndexRefreshQueryAgain")); //NOI18N + return suite; + } + +@@ -576,6 +579,55 @@ public void testFindSymbols() throws Exception { + assertEquals(new HashSet(Arrays.asList("test.foo:[foo]", "test.Test:[foo]")), actualResult); + } + ++ public void testQueryIndexRefreshQueryAgain() throws Exception { ++ final FileObject wd = FileUtil.toFileObject(getWorkDir()); ++ final FileObject root = FileUtil.createFolder(wd,"src"); //NOI18N ++ final FileObject classes = FileUtil.createFolder(wd,"classes"); //NOI18N ++ sourcePath = ClassPathSupport.createClassPath(root); ++ final FileObject t1 = createJavaFile( ++ root, ++ "org.me.test", //NOI18N ++ "T1", //NOI18N ++ "package org.me.test;\n"+ //NOI18N ++ "public class T1 extends java.util.ArrayList {}"); //NOI18N ++ //compile binary dependency: ++ JavaFileObject libraryJFO = ++ FileObjects.memoryFileObject("lib", ++ "TestLib.java", ++ """ ++ package lib; ++ public class TestLib {} ++ """); ++ ToolProvider.getSystemJavaCompiler() ++ .getTask(null, ++ null, ++ null, ++ List.of("-d", ++ FileUtil.toFile(classes).getAbsolutePath()), ++ null, ++ List.of(libraryJFO)) ++ .call(); ++ ++ compilePath = ClassPathSupport.createClassPath(classes); ++ bootPath = JavaPlatformManager.getDefault().getDefaultPlatform().getBootstrapLibraries(); ++ ++ final ClassIndex ci = ClasspathInfo.create(bootPath, compilePath, sourcePath).getClassIndex(); ++ Set> result; ++ result = ci.getDeclaredTypes("TestLib", NameKind.PREFIX, Set.of(ClassIndex.SearchScope.DEPENDENCIES)); ++ assertElementHandles(new String[] {}, result); ++ ++ GlobalPathRegistry.getDefault().register(ClassPath.BOOT, new ClassPath[] {bootPath}); ++ GlobalPathRegistry.getDefault().register(ClassPath.COMPILE, new ClassPath[] {compilePath}); ++ GlobalPathRegistry.getDefault().register(ClassPath.SOURCE, new ClassPath[] {sourcePath}); ++ ++ IndexingManager.getDefault().refreshAllIndices(true, true, root); ++ SourceUtils.waitScanFinished(); ++ ++ result = ci.getDeclaredTypes("TestLib", NameKind.PREFIX, Set.of(ClassIndex.SearchScope.DEPENDENCIES)); ++ assertNotNull(result); ++ assertElementHandles(new String[] {"lib.TestLib"}, result); ++ } ++ + private FileObject createJavaFile ( + final FileObject root, + final String pkg, diff --git a/patches/8260.diff b/patches/8260.diff new file mode 100644 index 00000000..cf49878c --- /dev/null +++ b/patches/8260.diff @@ -0,0 +1,549 @@ +diff --git a/java/java.openjdk.project/nbproject/project.properties b/java/java.openjdk.project/nbproject/project.properties +index 9e592633d479..a785a66d13dd 100644 +--- a/java/java.openjdk.project/nbproject/project.properties ++++ b/java/java.openjdk.project/nbproject/project.properties +@@ -16,7 +16,7 @@ + # specific language governing permissions and limitations + # under the License. + # +-javac.source=1.8 ++javac.release=17 + javac.compilerargs=-Xlint -Xlint:-serial + cp.extra=${tools.jar} + requires.nb.javac=true +diff --git a/java/java.openjdk.project/src/org/netbeans/modules/java/openjdk/common/ShortcutUtils.java b/java/java.openjdk.project/src/org/netbeans/modules/java/openjdk/common/ShortcutUtils.java +index fa2842cf88d6..2194ccb4b2fd 100644 +--- a/java/java.openjdk.project/src/org/netbeans/modules/java/openjdk/common/ShortcutUtils.java ++++ b/java/java.openjdk.project/src/org/netbeans/modules/java/openjdk/common/ShortcutUtils.java +@@ -89,6 +89,10 @@ public boolean shouldUseCustomTest(String repoName, String pathInRepo) { + } + + private boolean matches(String repoName, String pathInRepo, String key) { ++ if (pathInRepo == null) { ++ return false; ++ } ++ + String include = null; + String exclude = null; + try { +diff --git a/java/java.openjdk.project/src/org/netbeans/modules/java/openjdk/jtreg/ClassPathProviderImpl.java b/java/java.openjdk.project/src/org/netbeans/modules/java/openjdk/jtreg/ClassPathProviderImpl.java +index 31e95ffdc6fa..5772df63938c 100644 +--- a/java/java.openjdk.project/src/org/netbeans/modules/java/openjdk/jtreg/ClassPathProviderImpl.java ++++ b/java/java.openjdk.project/src/org/netbeans/modules/java/openjdk/jtreg/ClassPathProviderImpl.java +@@ -36,7 +36,10 @@ + import java.util.regex.Pattern; + + import org.netbeans.api.java.classpath.ClassPath; ++import org.netbeans.api.java.lexer.JavaTokenId; + import org.netbeans.api.java.source.JavaSource; ++import org.netbeans.api.lexer.TokenHierarchy; ++import org.netbeans.api.lexer.TokenSequence; + import org.netbeans.api.project.libraries.Library; + import org.netbeans.api.project.libraries.LibraryManager; + import org.netbeans.api.queries.FileEncodingQuery; +@@ -142,31 +145,27 @@ public ClassPath findClassPath(FileObject file, String type) { + } else { + if (file.isFolder()) return null; + +- roots.add(file.getParent()); +- try (Reader r = new InputStreamReader(file.getInputStream(), FileEncodingQuery.getEncoding(file))) { +- StringBuilder content = new StringBuilder(); +- int read; +- +- while ((read = r.read()) != (-1)) { +- content.append((char) read); +- } ++ String content = getFileContent(file); + ++ try { + Pattern library = Pattern.compile("@library (.*)\n"); + Matcher m = library.matcher(content.toString()); + + if (m.find()) { + List libDirs = new ArrayList<>(); +- try (InputStream in = testRootFile.getInputStream()) { +- Properties p = new Properties(); +- p.load(in); +- String externalLibRoots = p.getProperty("external.lib.roots"); +- if (externalLibRoots != null) { +- for (String extLib : externalLibRoots.split("\\s+")) { +- FileObject libDir = BuildUtils.getFileObject(testRoot, extLib); +- +- if (libDir != null) { +- libDirs.add(libDir); +- } ++ Properties p = new Properties(); ++ if (testRootFile != null) { ++ try (InputStream in = testRootFile.getInputStream()) { ++ p.load(in); ++ } ++ } ++ String externalLibRoots = p.getProperty("external.lib.roots"); ++ if (externalLibRoots != null) { ++ for (String extLib : externalLibRoots.split("\\s+")) { ++ FileObject libDir = BuildUtils.getFileObject(testRoot, extLib); ++ ++ if (libDir != null) { ++ libDirs.add(libDir); + } + } + } +@@ -185,6 +184,20 @@ public ClassPath findClassPath(FileObject file, String type) { + } catch (IOException ex) { + Exceptions.printStackTrace(ex); + } ++ ++ String pckge = ""; ++ ++ pckge = packageClause(content); ++ ++ FileObject packageDir = file.getParent(); ++ ++ if (!pckge.isEmpty()) { ++ for (String s : pckge.split("\\.")) { ++ packageDir = packageDir.getParent(); ++ } ++ } ++ ++ roots.add(packageDir); + } + + //XXX: +@@ -203,6 +216,44 @@ private FileObject resolve(FileObject file, FileObject root, String spec) { + } + } + ++ private String getFileContent(FileObject file) { ++ try (Reader r = new InputStreamReader(file.getInputStream(), FileEncodingQuery.getEncoding(file))) { ++ StringBuilder contentBuilder = new StringBuilder(); ++ int read; ++ ++ while ((read = r.read()) != (-1)) { ++ contentBuilder.append((char) read); ++ } ++ ++ return contentBuilder.toString(); ++ } catch (IOException ex) { ++ Exceptions.printStackTrace(ex); ++ return ""; ++ } ++ } ++ ++ private String packageClause(String fileContent) { ++ TokenSequence ts = ++ TokenHierarchy.create(fileContent, JavaTokenId.language()) ++ .tokenSequence(JavaTokenId.language()); ++ while (ts.moveNext()) { ++ if (ts.token().id() == JavaTokenId.PACKAGE) { ++ StringBuilder pckge = new StringBuilder(); ++ ++ while (ts.moveNext()) { ++ switch (ts.token().id()) { ++ case IDENTIFIER, DOT -> pckge.append(ts.token().text()); ++ case BLOCK_COMMENT, JAVADOC_COMMENT, WHITESPACE, ++ JAVADOC_COMMENT_LINE_RUN, LINE_COMMENT -> {} ++ default -> {return pckge.toString();} ++ } ++ } ++ } ++ } ++ ++ return ""; ++ } ++ + private void initializeUsagesQuery(FileObject root) { + try { + ClassLoader cl = JavaSource.class.getClassLoader(); +diff --git a/java/java.openjdk.project/src/org/netbeans/modules/java/openjdk/jtreg/TestRootDescription.java b/java/java.openjdk.project/src/org/netbeans/modules/java/openjdk/jtreg/TestRootDescription.java +index 20a0a812e6c4..9356af3fc860 100644 +--- a/java/java.openjdk.project/src/org/netbeans/modules/java/openjdk/jtreg/TestRootDescription.java ++++ b/java/java.openjdk.project/src/org/netbeans/modules/java/openjdk/jtreg/TestRootDescription.java +@@ -46,6 +46,11 @@ public static TestRootDescription findRootDescriptionFor(FileObject file) { + if (testRoot != null) { + return new TestRootDescription(testProperties, search, testRoot); + } ++ ++ if (search.getNameExt().equals("lib") && search.getFileObject("../jdk/TEST.ROOT") != null) { ++ return new TestRootDescription(null, search, null); ++ } ++ + search = search.getParent(); + } + +diff --git a/java/java.openjdk.project/src/org/netbeans/modules/java/openjdk/project/JDKProject.java b/java/java.openjdk.project/src/org/netbeans/modules/java/openjdk/project/JDKProject.java +index 4e98d449e2e9..3a4596293bf6 100644 +--- a/java/java.openjdk.project/src/org/netbeans/modules/java/openjdk/project/JDKProject.java ++++ b/java/java.openjdk.project/src/org/netbeans/modules/java/openjdk/project/JDKProject.java +@@ -197,10 +197,11 @@ public JDKProject(FileObject projectDir, @NullAllowed ModuleRepository moduleRep + roots.clear(); + roots.addAll(newRoots); + } +- String testRoots = moduleRepository.moduleTests(currentModule.name); + +- if (testRoots != null) { +- addRoots(RootKind.TEST_SOURCES, Arrays.asList(Pair.of(testRoots, null))); ++ List testRoots = moduleRepository.moduleTests(currentModule.name); ++ ++ for (String testRoot : testRoots) { ++ addRoots(RootKind.TEST_SOURCES, Arrays.asList(Pair.of(testRoot, null))); + } + + } +diff --git a/java/java.openjdk.project/src/org/netbeans/modules/java/openjdk/project/ModuleDescription.java b/java/java.openjdk.project/src/org/netbeans/modules/java/openjdk/project/ModuleDescription.java +index 0a9cd27ca0de..edc24316b705 100644 +--- a/java/java.openjdk.project/src/org/netbeans/modules/java/openjdk/project/ModuleDescription.java ++++ b/java/java.openjdk.project/src/org/netbeans/modules/java/openjdk/project/ModuleDescription.java +@@ -411,20 +411,45 @@ private boolean validate(FileObject repo, FileObject project) { + return true; + } + +- public String moduleTests(String moduleName) { +- String open = explicitOpen ? "open/" : ""; +- //TODO? for now, tests are assigned to java.base, java.compiler and java.xml, depending on the location of the tests: +- switch (moduleName) { +- case "java.base": +- return consolidatedRepository ? "${jdkRoot}/" + open + "test/jdk/" : "${jdkRoot}/jdk/test/"; +- case "java.compiler": +- return consolidatedRepository ? "${jdkRoot}/test/" + open + "langtools/" : "${jdkRoot}/langtools/test/"; +- case "java.xml": +- return consolidatedRepository ? "${jdkRoot}/test/" + open + "jaxp/" : "${jdkRoot}/jaxp/test/"; +- case "jdk.scripting.nashorn": +- return consolidatedRepository ? "${jdkRoot}/test/" + open + "nashorn/" : "${jdkRoot}/nashorn/test/"; ++ public List moduleTests(String moduleName) { ++ if (!consolidatedRepository) { ++ switch (moduleName) { ++ case "java.base": ++ return List.of("${jdkRoot}/jdk/test/"); ++ case "java.compiler": ++ return List.of("${jdkRoot}/langtools/test/"); ++ case "java.xml": ++ return List.of("${jdkRoot}/jaxp/test/"); ++ case "jdk.scripting.nashorn": ++ return List.of("${jdkRoot}/nashorn/test/"); ++ } ++ return List.of(); + } +- return null; ++ ++ List result = new ArrayList<>(); ++ ++ for (String dir : explicitOpen ? new String[] {"open/", "closed/"} ++ : new String[] {""}) { ++ //TODO? for now, tests are assigned to java.base, java.compiler and java.xml, depending on the location of the tests: ++ switch (moduleName) { ++ case "java.base": ++ result.add("${jdkRoot}/" + dir + "test/jdk/"); ++ result.add("${jdkRoot}/" + dir + "test/hotspot/"); ++ result.add("${jdkRoot}/" + dir + "test/lib/"); ++ break; ++ case "java.compiler": ++ result.add("${jdkRoot}/" + dir + "test/langtools/"); ++ break; ++ case "java.xml": ++ result.add("${jdkRoot}/" + dir + "test/jaxp/"); ++ break; ++ case "jdk.scripting.nashorn": ++ result.add("${jdkRoot}/" + dir + "test/nashorn/"); ++ break; ++ } ++ } ++ ++ return result; + } + + public Collection allDependencies(ModuleDescription module) { +diff --git a/java/java.openjdk.project/src/org/netbeans/modules/java/openjdk/project/SourcesImpl.java b/java/java.openjdk.project/src/org/netbeans/modules/java/openjdk/project/SourcesImpl.java +index a8b617ddae21..c9abc8655831 100644 +--- a/java/java.openjdk.project/src/org/netbeans/modules/java/openjdk/project/SourcesImpl.java ++++ b/java/java.openjdk.project/src/org/netbeans/modules/java/openjdk/project/SourcesImpl.java +@@ -63,9 +63,12 @@ public class SourcesImpl implements Sources, FileChangeListener, ChangeListener + public static final String SOURCES_TYPE_JDK_PROJECT_TESTS = "jdk-project-sources-tests"; + public static final String SOURCES_TYPE_JDK_PROJECT_NATIVE = "jdk-project-sources-native"; + ++ @SuppressWarnings("this-escape") + private final ChangeSupport cs = new ChangeSupport(this); + private final JDKProject project; +- private final Map root2SourceGroup = new HashMap(); ++ private final Map root2SourceGroup = new HashMap<>(); ++ private final Map> key2SourceGroups = new HashMap<>(); ++ private final Set seen = new HashSet<>(); + + public SourcesImpl(JDKProject project) { + this.project = project; +@@ -75,48 +78,83 @@ public SourcesImpl(JDKProject project) { + } + } + +- private boolean initialized; +- private final Map> key2SourceGroups = new HashMap<>(); ++ private int changeCount = 0; ++ private int changeCountForCurrentValues = -1; + + @Override +- public synchronized SourceGroup[] getSourceGroups(String type) { +- if (!initialized) { +- recompute(); +- initialized = true; +- } +- +- List groups = key2SourceGroups.get(type); +- if (groups != null) +- return groups.toArray(new SourceGroup[0]); ++ public SourceGroup[] getSourceGroups(String type) { ++ while (true) { ++ int currentChangeCount; ++ Map root2SourceGroupsCopy; + +- return new SourceGroup[0]; ++ synchronized (this) { ++ currentChangeCount = changeCount; ++ ++ if (changeCountForCurrentValues == currentChangeCount) { ++ return key2SourceGroups.getOrDefault(type, List.of()) ++ .toArray(SourceGroup[]::new); ++ } ++ ++ root2SourceGroupsCopy = new HashMap<>(root2SourceGroup); ++ } ++ ++ RecomputeResult recomputed = recompute(project, root2SourceGroupsCopy); ++ ++ synchronized (this) { ++ if (currentChangeCount == changeCount) { ++ //no intervening change, apply results: ++ root2SourceGroup.clear(); ++ root2SourceGroup.putAll(recomputed.root2SourceGroup()); ++ key2SourceGroups.clear(); ++ key2SourceGroups.putAll(recomputed.key2SourceGroups()); ++ ++ Set added = new HashSet<>(recomputed.seen()); ++ added.removeAll(seen); ++ Set removed = new HashSet<>(seen); ++ removed.removeAll(recomputed.seen()); ++ ++ for (File a : added) { ++ FileUtil.addFileChangeListener(this, a); ++ seen.add(a); ++ FileOwnerQuery.markExternalOwner(Utilities.toURI(a), null, FileOwnerQuery.EXTERNAL_ALGORITHM_TRANSIENT); ++ FileOwnerQuery.markExternalOwner(Utilities.toURI(a), project, FileOwnerQuery.EXTERNAL_ALGORITHM_TRANSIENT); ++ } ++ for (File r : removed) { ++ FileUtil.removeFileChangeListener(this, r); ++ seen.remove(r); ++ FileOwnerQuery.markExternalOwner(Utilities.toURI(r), null, FileOwnerQuery.EXTERNAL_ALGORITHM_TRANSIENT); ++ } ++ ++ changeCountForCurrentValues = currentChangeCount; ++ return key2SourceGroups.getOrDefault(type, List.of()) ++ .toArray(SourceGroup[]::new); ++ } ++ } ++ } + } + +- private final Set seen = new HashSet<>(); + +- private synchronized void recompute() { +- key2SourceGroups.clear(); ++ private static RecomputeResult recompute(JDKProject project, Map root2SourceGroup) { ++ Map> key2SourceGroups = new HashMap<>(); ++ Set seen = new HashSet<>(); + + for (SourceGroup sg : GenericSources.genericOnly(project).getSourceGroups(TYPE_GENERIC)) { +- addSourceGroup(TYPE_GENERIC, sg); ++ addSourceGroup(key2SourceGroups, TYPE_GENERIC, sg); + } + +- Set newFiles = new HashSet<>(); + for (Root root : project.getRoots()) { + URL srcURL = root.getLocation(); + + if ("file".equals(srcURL.getProtocol())) { + try { +- newFiles.add(Utilities.toFile(srcURL.toURI())); ++ seen.add(Utilities.toFile(srcURL.toURI())); + } catch (URISyntaxException ex) { + Exceptions.printStackTrace(ex); + } + } + + FileObject src = URLMapper.findFileObject(srcURL); +- if (src == null) { +- root2SourceGroup.remove(root); +- } else { ++ if (src != null) { + SourceGroup sg = root2SourceGroup.get(root); + + if (sg == null) { +@@ -126,41 +164,27 @@ private synchronized void recompute() { + } + + if (root.kind == RootKind.NATIVE_SOURCES) { +- addSourceGroup(SOURCES_TYPE_JDK_PROJECT_NATIVE, sg); ++ addSourceGroup(key2SourceGroups, SOURCES_TYPE_JDK_PROJECT_NATIVE, sg); + } else { +- addSourceGroup(JavaProjectConstants.SOURCES_TYPE_JAVA, sg); ++ addSourceGroup(key2SourceGroups, JavaProjectConstants.SOURCES_TYPE_JAVA, sg); + } + + if (root.kind == RootKind.TEST_SOURCES) { +- addSourceGroup(SOURCES_TYPE_JDK_PROJECT_TESTS, sg); ++ addSourceGroup(key2SourceGroups, SOURCES_TYPE_JDK_PROJECT_TESTS, sg); + } + +- addSourceGroup(SOURCES_TYPE_JDK_PROJECT, sg); ++ addSourceGroup(key2SourceGroups, SOURCES_TYPE_JDK_PROJECT, sg); + + if (!FileUtil.isParentOf(project.getProjectDirectory(), src)) { +- addSourceGroup(TYPE_GENERIC, GenericSources.group(project, src, root.displayName, root.displayName, null, null)); ++ addSourceGroup(key2SourceGroups, TYPE_GENERIC, GenericSources.group(project, src, root.displayName, root.displayName, null, null)); + } + } + } +- Set added = new HashSet<>(newFiles); +- added.removeAll(seen); +- Set removed = new HashSet<>(seen); +- removed.removeAll(newFiles); +- for (File a : added) { +- FileUtil.addFileChangeListener(this, a); +- seen.add(a); +- FileOwnerQuery.markExternalOwner(Utilities.toURI(a), null, FileOwnerQuery.EXTERNAL_ALGORITHM_TRANSIENT); +- FileOwnerQuery.markExternalOwner(Utilities.toURI(a), project, FileOwnerQuery.EXTERNAL_ALGORITHM_TRANSIENT); +- } +- for (File r : removed) { +- FileUtil.removeFileChangeListener(this, r); +- seen.remove(r); +- FileOwnerQuery.markExternalOwner(Utilities.toURI(r), null, FileOwnerQuery.EXTERNAL_ALGORITHM_TRANSIENT); +- } +- cs.fireChange(); ++ ++ return new RecomputeResult(root2SourceGroup, key2SourceGroups, seen); + } + +- private void addSourceGroup(String type, SourceGroup sg) { ++ private static void addSourceGroup(Map> key2SourceGroups, String type, SourceGroup sg) { + List groups = key2SourceGroups.get(type); + + if (groups == null) { +@@ -170,6 +194,7 @@ private void addSourceGroup(String type, SourceGroup sg) { + groups.add(sg); + } + ++ private record RecomputeResult(Map root2SourceGroup, Map> key2SourceGroups, Set seen) {} + @Override public void addChangeListener(ChangeListener listener) { + cs.addChangeListener(listener); + } +@@ -180,7 +205,7 @@ private void addSourceGroup(String type, SourceGroup sg) { + + @Override + public void fileFolderCreated(FileEvent fe) { +- recompute(); ++ changed(); + } + + @Override +@@ -191,12 +216,12 @@ public void fileChanged(FileEvent fe) { } + + @Override + public void fileDeleted(FileEvent fe) { +- recompute(); ++ changed(); + } + + @Override + public void fileRenamed(FileRenameEvent fe) { +- recompute(); ++ changed(); + } + + @Override +@@ -204,7 +229,14 @@ public void fileAttributeChanged(FileAttributeEvent fe) { } + + @Override + public void stateChanged(ChangeEvent e) { +- recompute(); ++ changed(); ++ } ++ ++ private void changed() { ++ synchronized (this) { ++ changeCount++; ++ } ++ cs.fireChange(); + } + + private static final class SourceGroupImpl implements SourceGroup { +diff --git a/java/java.openjdk.project/test/unit/src/org/netbeans/modules/java/openjdk/jtreg/ClassPathProviderImplTest.java b/java/java.openjdk.project/test/unit/src/org/netbeans/modules/java/openjdk/jtreg/ClassPathProviderImplTest.java +index 6ca6924302de..88c26c37ae01 100644 +--- a/java/java.openjdk.project/test/unit/src/org/netbeans/modules/java/openjdk/jtreg/ClassPathProviderImplTest.java ++++ b/java/java.openjdk.project/test/unit/src/org/netbeans/modules/java/openjdk/jtreg/ClassPathProviderImplTest.java +@@ -20,6 +20,7 @@ + + import java.io.File; + import java.io.IOException; ++import java.io.OutputStream; + import java.io.OutputStreamWriter; + import java.io.Writer; + import java.util.Arrays; +@@ -121,15 +122,59 @@ public void testExternalLibRoots() throws Exception { + new HashSet<>(Arrays.asList(sourceCP.getRoots()))); + } + ++ public void testInPackage() throws Exception { ++ File workDir = getWorkDir(); ++ ++ FileUtil.createFolder(new File(workDir, "src/share/classes")); ++ FileObject testRoot = createData("test/TEST.ROOT", ""); ++ FileObject testFile = FileUtil.createData(new File(workDir, "test/feature/pack/Test.java")); ++ writeContent(testFile, ++ """ ++ /* package wrong.package.clause; */ ++ @Ann("package another.wrong.package.clause") ++ package /**/ // ++ feature. ++ pack; ++ """); ++ ClassPath sourceCP = new ClassPathProviderImpl().findClassPath(testFile, ClassPath.SOURCE); ++ ++ Assert.assertArrayEquals(new FileObject[] {testFile.getParent().getParent().getParent()}, ++ sourceCP.getRoots()); ++ } ++ ++ public void testInFolder() throws Exception { ++ File workDir = getWorkDir(); ++ ++ FileUtil.createFolder(new File(workDir, "src/share/classes")); ++ FileObject testRoot = createData("test/jdk/TEST.ROOT", ""); ++ FileObject testFile = FileUtil.createData(new File(workDir, "test/lib/feature/pack/Test.java")); ++ writeContent(testFile, ++ """ ++ /* package wrong.package.clause; */ ++ @Ann("package another.wrong.package.clause") ++ package /**/ // ++ feature. ++ pack; ++ """); ++ ClassPath sourceCP = new ClassPathProviderImpl().findClassPath(testFile, ClassPath.SOURCE); ++ ++ Assert.assertArrayEquals(new FileObject[] {testFile.getParent().getParent().getParent()}, ++ sourceCP.getRoots()); ++ } ++ + private FileObject createData(String relPath, String content) throws IOException { + File workDir = getWorkDir(); + FileObject file = FileUtil.createData(new File(workDir, relPath)); + ++ writeContent(file, content); ++ ++ return file; ++ } ++ ++ private void writeContent(FileObject file, String content) throws IOException { + try (Writer w = new OutputStreamWriter(file.getOutputStream())) { + w.write(content); + } +- +- return file; + } + + } diff --git a/patches/8280.diff b/patches/8280.diff new file mode 100644 index 00000000..8b1c1588 --- /dev/null +++ b/patches/8280.diff @@ -0,0 +1,88 @@ +diff --git a/java/java.lsp.server/src/org/netbeans/modules/java/lsp/server/debugging/launch/NbLaunchRequestHandler.java b/java/java.lsp.server/src/org/netbeans/modules/java/lsp/server/debugging/launch/NbLaunchRequestHandler.java +index 8d96ac88be89..ea6138764b46 100644 +--- a/java/java.lsp.server/src/org/netbeans/modules/java/lsp/server/debugging/launch/NbLaunchRequestHandler.java ++++ b/java/java.lsp.server/src/org/netbeans/modules/java/lsp/server/debugging/launch/NbLaunchRequestHandler.java +@@ -35,6 +35,7 @@ + import java.util.regex.Matcher; + import java.util.regex.Pattern; + import java.util.stream.Collectors; ++import javax.lang.model.element.ElementKind; + import javax.lang.model.element.TypeElement; + + import org.apache.commons.lang3.StringUtils; +@@ -206,15 +207,60 @@ public CompletableFuture launch(Map launchArguments, Debug + filePath = projectFilePath; + } + boolean preferProjActions = true; // True when we prefer project actions to the current (main) file actions. +- if (filePath == null || mainFilePath != null) { +- // main overides the current file +- preferProjActions = false; +- filePath = mainFilePath; +- } + FileObject file = null; + File nativeImageFile = null; + if (!isNative) { +- file = getFileObject(filePath); ++ if (filePath == null || mainFilePath != null) { ++ // main overides the current file ++ preferProjActions = false; ++ ++ file = getFileObject(mainFilePath); ++ ++ if (file == null) { ++ LspServerState state = Lookup.getDefault().lookup(LspServerState.class); ++ ++ if (state != null) { ++ for (FileObject workspaceFolder : state.getClientWorkspaceFolders()) { ++ file = workspaceFolder.getFileObject(mainFilePath); ++ ++ if (file != null) { ++ break; ++ } ++ } ++ ++ if (file == null) { ++ return state.openedProjects().thenCompose(prjs -> { ++ FileObject[] sourceRoots = ++ Arrays.stream(prjs) ++ .flatMap(p -> Arrays.stream(ProjectUtils.getSources(p).getSourceGroups(JavaProjectConstants.SOURCES_TYPE_JAVA))) ++ .map(sg -> sg.getRootFolder()) ++ .toArray(s -> new FileObject[s]); ++ ++ ClasspathInfo cpInfo = ClasspathInfo.create(ClassPath.EMPTY, ClassPath.EMPTY, ClassPathSupport.createClassPath(sourceRoots)); ++ FileObject mainClassFile = SourceUtils.getFile(ElementHandle.createTypeElementHandle(ElementKind.CLASS, mainFilePath), cpInfo); ++ if (mainClassFile == null) { ++ CompletableFuture currentResult = new CompletableFuture<>(); ++ ErrorUtilities.completeExceptionally(currentResult, ++ "The main class specified as: \"" + mainFilePath + "\" cannot be found as neither an absolute path, a path relative to any workspace folder or a class name.", ++ ResponseErrorCode.ServerNotInitialized); ++ return currentResult; ++ } else { ++ Map newLaunchArguments = new HashMap<>(launchArguments); ++ newLaunchArguments.put("mainClass", mainClassFile.toURI().toString()); ++ return launch(newLaunchArguments, context); ++ } ++ }); ++ } ++ } else { ++ ErrorUtilities.completeExceptionally(resultFuture, ++ "Failed to launch debuggee VM. Wrong context.", ++ ResponseErrorCode.ServerNotInitialized); ++ return resultFuture; ++ } ++ } ++ } else { ++ file = getFileObject(filePath); ++ } + if (file == null) { + ErrorUtilities.completeExceptionally(resultFuture, + "Missing file: " + filePath, +@@ -265,7 +311,7 @@ private static FileObject getFileObject(String filePath) { + try { + URI uri = new URI(filePath); + ioFile = Utilities.toFile(uri); +- } catch (URISyntaxException ex) { ++ } catch (URISyntaxException | IllegalArgumentException ex) { + // Not a valid file + } + } diff --git a/patches/8289.diff b/patches/8289.diff new file mode 100644 index 00000000..bfe45fb6 --- /dev/null +++ b/patches/8289.diff @@ -0,0 +1,117 @@ +diff --git a/java/java.file.launcher/src/org/netbeans/modules/java/file/launcher/queries/MultiSourceRootProvider.java b/java/java.file.launcher/src/org/netbeans/modules/java/file/launcher/queries/MultiSourceRootProvider.java +index ff0d0ea144..aef5606fb4 100644 +--- a/java/java.file.launcher/src/org/netbeans/modules/java/file/launcher/queries/MultiSourceRootProvider.java ++++ b/java/java.file.launcher/src/org/netbeans/modules/java/file/launcher/queries/MultiSourceRootProvider.java +@@ -30,6 +30,7 @@ import java.util.Collections; + import java.util.EnumSet; + import java.util.HashSet; + import java.util.List; ++import java.util.Locale; + import java.util.Map; + import java.util.Objects; + import java.util.Set; +@@ -87,6 +88,7 @@ public class MultiSourceRootProvider implements ClassPathProvider { + private static final Set MODULAR_DIRECTORY_OPTIONS = new HashSet<>(Arrays.asList( + "--module-path", "-p" + )); ++ private static final Set CLASSPATH_OPTIONS = Set.of("--class-path", "-cp", "-classpath"); + + //TODO: the cache will probably be never cleared, as the ClassPath/value refers to the key(?) + private Map file2SourceCP = new WeakHashMap<>(); +@@ -341,6 +343,19 @@ public class MultiSourceRootProvider implements ClassPathProvider { + + if (optionKeys.contains(currentOption)) { + for (String piece : parsed.get(i + 1).split(File.pathSeparator)) { ++ boolean hasStar = false; ++ boolean isClassPath = CLASSPATH_OPTIONS.contains(currentOption); ++ ++ if (isClassPath && piece.endsWith("*") && piece.length() > 1) { ++ char sep = piece.charAt(piece.length() - 2); ++ ++ if (sep == File.separatorChar || ++ sep == '/') { ++ hasStar = true; ++ piece = piece.substring(0, piece.length() - 2); ++ } ++ } ++ + File pieceFile = new File(piece); + + if (!pieceFile.isAbsolute()) { +@@ -367,6 +382,23 @@ public class MultiSourceRootProvider implements ClassPathProvider { + } else { + expandedPaths = Collections.emptyList(); + } ++ } else if (hasStar && isClassPath) { ++ if (!toRemoveFSListeners.remove(f.getAbsolutePath()) && ++ addedFSListeners.add(f.getAbsolutePath())) { ++ FileUtil.addFileChangeListener(this, f); ++ } ++ ++ File[] children = f.listFiles(); ++ ++ if (children != null) { ++ expandedPaths = Arrays.stream(children) ++ .filter(c -> c.getName() ++ .toLowerCase(Locale.ROOT) ++ .endsWith(".jar")) ++ .toList(); ++ } else { ++ expandedPaths = Collections.emptyList(); ++ } + } else { + expandedPaths = Arrays.asList(f); + } +diff --git a/java/java.file.launcher/test/unit/src/org/netbeans/modules/java/file/launcher/queries/MultiSourceRootProviderTest.java b/java/java.file.launcher/test/unit/src/org/netbeans/modules/java/file/launcher/queries/MultiSourceRootProviderTest.java +index c89798daad..0e98a5958f 100644 +--- a/java/java.file.launcher/test/unit/src/org/netbeans/modules/java/file/launcher/queries/MultiSourceRootProviderTest.java ++++ b/java/java.file.launcher/test/unit/src/org/netbeans/modules/java/file/launcher/queries/MultiSourceRootProviderTest.java +@@ -310,6 +310,48 @@ public class MultiSourceRootProviderTest extends NbTestCase { + assertSame(cp, provider.findClassPath(FileUtil.toFileObject(packDir), ClassPath.SOURCE)); + } + ++ public void testExpandClassPath() throws Exception { ++ FileObject wd = FileUtil.toFileObject(getWorkDir()); ++ FileObject test = FileUtil.createData(wd, "src/pack/Test1.java"); ++ FileObject libsDir = FileUtil.createFolder(wd, "libs"); ++ FileObject lib1Jar = FileUtil.createData(libsDir, "lib1.jar"); ++ FileObject lib2Jar = FileUtil.createData(libsDir, "lib2.jar"); ++ FileObject lib3Dir = FileUtil.createFolder(libsDir, "lib3"); ++ FileObject lib4Zip = FileUtil.createData(libsDir, "lib4.zip"); ++ ++ TestUtilities.copyStringToFile(test, "package pack;"); ++ ++ testResult.setOptions("--class-path " + FileUtil.getRelativePath(wd, libsDir) + "/*"); ++ testResult.setWorkDirectory(FileUtil.toFileObject(getWorkDir()).toURI()); ++ ++ MultiSourceRootProvider provider = new MultiSourceRootProvider(); ++ ClassPath compileCP = provider.findClassPath(test, ClassPath.COMPILE); ++ ++ assertEquals(new HashSet<>(Arrays.asList(FileUtil.getArchiveRoot(lib1Jar), ++ FileUtil.getArchiveRoot(lib2Jar))), ++ new HashSet<>(Arrays.asList(compileCP.getRoots()))); ++ ++ lib4Zip.delete(); ++ ++ assertEquals(new HashSet<>(Arrays.asList(FileUtil.getArchiveRoot(lib1Jar), ++ FileUtil.getArchiveRoot(lib2Jar))), ++ new HashSet<>(Arrays.asList(compileCP.getRoots()))); ++ ++ FileObject lib5Jar = FileUtil.createData(libsDir, "lib5.jar"); ++ ++ assertEquals(new HashSet<>(Arrays.asList(FileUtil.getArchiveRoot(lib1Jar), ++ FileUtil.getArchiveRoot(lib2Jar), ++ FileUtil.getArchiveRoot(lib5Jar))), ++ new HashSet<>(Arrays.asList(compileCP.getRoots()))); ++ ++ lib1Jar.delete(); ++ ++ assertEquals(new HashSet<>(Arrays.asList(FileUtil.getArchiveRoot(lib2Jar), ++ FileUtil.getArchiveRoot(lib5Jar))), ++ new HashSet<>(Arrays.asList(compileCP.getRoots()))); ++ ++ } ++ + @Override + protected void setUp() throws Exception { + super.setUp(); diff --git a/vscode/package.json b/vscode/package.json index c3bb4794..e61f461e 100644 --- a/vscode/package.json +++ b/vscode/package.json @@ -265,7 +265,7 @@ "properties": { "mainClass": { "type": "string", - "description": "%jdk.debugger.configuration.mainClass.description%", + "markdownDescription": "%jdk.debugger.configuration.mainClass.markdownDescription%", "default": "${file}" }, "classPaths": { diff --git a/vscode/package.nls.ja.json b/vscode/package.nls.ja.json index 48acea78..5c25cb5d 100755 --- a/vscode/package.nls.ja.json +++ b/vscode/package.nls.ja.json @@ -46,7 +46,7 @@ "jdk.configuration.runConfig.cwd.description": "作業ディレクトリ", "jdk.configuration.disableNbJavac.description": "拡張オプション: nb-javacライブラリを無効化すると、選択したJDKからのjavacが使用されます。選択したJDKは少なくともJDK 23である必要があります。", "jdk.configuration.disableProjectSearchLimit.description": "拡張オプション: プロジェクト情報が含まれているフォルダの検索に対する制限を無効化します。", - "jdk.debugger.configuration.mainClass.description": "プログラムのメイン・クラスへの絶対パス。", + "jdk.debugger.configuration.mainClass.markdownDescription": "Main class specification. Supported formats:\n - an absolute path\n - a path relative to any of the workspace folders\n - a fully qualified name of a class.", "jdk.debugger.configuration.classPaths.description": "JVMの起動のためのクラスパス。", "jdk.debugger.configuration.console.description": "プログラムを起動する指定されたコンソール。", "jdk.debugger.configuration.args.description": "実行クラスの引数", diff --git a/vscode/package.nls.json b/vscode/package.nls.json index 0e579fa9..a5c382a4 100644 --- a/vscode/package.nls.json +++ b/vscode/package.nls.json @@ -46,7 +46,7 @@ "jdk.configuration.runConfig.cwd.description": "Working directory", "jdk.configuration.disableNbJavac.description": "Advanced option: disable nb-javac library, javac from the selected JDK will be used. The selected JDK must be at least JDK 23.", "jdk.configuration.disableProjectSearchLimit.description": "Advanced option: disable limits on searching in containing folders for project information.", - "jdk.debugger.configuration.mainClass.description": "Absolute path to the program main class.", + "jdk.debugger.configuration.mainClass.markdownDescription": "Main class specification. Supported formats:\n - an absolute path\n - a path relative to any of the workspace folders\n - a fully qualified name of a class.", "jdk.debugger.configuration.classPaths.description": "The classpaths for launching the JVM.", "jdk.debugger.configuration.console.description": "The specified console to launch the program.", "jdk.debugger.configuration.args.description": "Arguments for the executed class", diff --git a/vscode/package.nls.zh-cn.json b/vscode/package.nls.zh-cn.json index 5b85d099..ad4b993a 100755 --- a/vscode/package.nls.zh-cn.json +++ b/vscode/package.nls.zh-cn.json @@ -46,7 +46,7 @@ "jdk.configuration.runConfig.cwd.description": "工作目录", "jdk.configuration.disableNbJavac.description": "高级选项:禁用 nb-javac 库,将使用来自所选 JDK 的 javac。所选 JDK 必须至少为 JDK 23。", "jdk.configuration.disableProjectSearchLimit.description": "高级选项:禁用在包含项目信息的文件夹中搜索的限制。", - "jdk.debugger.configuration.mainClass.description": "程序主类的绝对路径。", + "jdk.debugger.configuration.mainClass.markdownDescription": "Main class specification. Supported formats:\n - an absolute path\n - a path relative to any of the workspace folders\n - a fully qualified name of a class.", "jdk.debugger.configuration.classPaths.description": "用于启动 JVM 的类路径。", "jdk.debugger.configuration.console.description": "用于启动程序的指定控制台。", "jdk.debugger.configuration.args.description": "所执行类的参数",