From 34437a79cada28032dc21b13ed538fc20afe9f21 Mon Sep 17 00:00:00 2001 From: Jan Lahoda Date: Mon, 29 Sep 2025 10:47:47 +0200 Subject: [PATCH] 8368848: JShell's code completion not always working for multi-snippet inputs --- .../classes/jdk/jshell/OuterWrapMap.java | 17 ++- .../share/classes/jdk/jshell/SnippetMaps.java | 6 +- .../jdk/jshell/SourceCodeAnalysisImpl.java | 118 +++++++++++++++--- .../jdk/jshell/CompletionSuggestionTest.java | 10 ++ 4 files changed, 125 insertions(+), 26 deletions(-) diff --git a/src/jdk.jshell/share/classes/jdk/jshell/OuterWrapMap.java b/src/jdk.jshell/share/classes/jdk/jshell/OuterWrapMap.java index 6f7f3fe42c34c..61fb3bdfc9a3f 100644 --- a/src/jdk.jshell/share/classes/jdk/jshell/OuterWrapMap.java +++ b/src/jdk.jshell/share/classes/jdk/jshell/OuterWrapMap.java @@ -73,7 +73,11 @@ private CompoundWrap wrappedInClass(String className, String imports, List OuterWrap wrapInClass(Set except, Collection plus, List snippets, List wraps) { - String imports = state.maps.packageAndImportsExcept(except, plus); + List extraImports = + plus.stream() + .map(psi -> psi.importLine(state)) + .toList(); + String imports = state.maps.packageAndImportsExcept(except, extraImports); // className is unique to the set of snippets and their version (seq) String className = REPL_CLASS_PREFIX + snippets.stream() .sorted((sn1, sn2) -> sn1.key().index() - sn2.key().index()) @@ -86,9 +90,16 @@ OuterWrap wrapInClass(Set except, Collection plus, } OuterWrap wrapInTrialClass(Wrap wrap) { - String imports = state.maps.packageAndImportsExcept(null, null); + return wrapInTrialClass(List.of(), List.of(), wrap); + } + + OuterWrap wrapInTrialClass(List extraImports, List preWraps, Wrap wrap) { + String imports = state.maps.packageAndImportsExcept(null, extraImports); + List allWraps = new ArrayList<>(); + allWraps.addAll(preWraps); + allWraps.add(wrap); CompoundWrap w = wrappedInClass(REPL_DOESNOTMATTER_CLASS_NAME, imports, - Collections.singletonList(wrap)); + allWraps); return new OuterWrap(w); } diff --git a/src/jdk.jshell/share/classes/jdk/jshell/SnippetMaps.java b/src/jdk.jshell/share/classes/jdk/jshell/SnippetMaps.java index bd0c9d86db3ec..992d97a7040f0 100644 --- a/src/jdk.jshell/share/classes/jdk/jshell/SnippetMaps.java +++ b/src/jdk.jshell/share/classes/jdk/jshell/SnippetMaps.java @@ -105,7 +105,7 @@ List snippetList() { return new ArrayList<>(snippets); } - String packageAndImportsExcept(Set except, Collection plus) { + String packageAndImportsExcept(Set except, Collection extraImports) { StringBuilder sb = new StringBuilder(); sb.append("package ").append(REPL_PACKAGE).append(";\n"); for (Snippet si : keyIndexToSnippet) { @@ -113,9 +113,7 @@ String packageAndImportsExcept(Set except, Collection plus) { sb.append(si.importLine(state)); } } - if (plus != null) { - plus.forEach(psi -> sb.append(psi.importLine(state))); - } + extraImports.forEach(sb::append); return sb.toString(); } diff --git a/src/jdk.jshell/share/classes/jdk/jshell/SourceCodeAnalysisImpl.java b/src/jdk.jshell/share/classes/jdk/jshell/SourceCodeAnalysisImpl.java index 045734d9f5597..054611ae175f9 100644 --- a/src/jdk.jshell/share/classes/jdk/jshell/SourceCodeAnalysisImpl.java +++ b/src/jdk.jshell/share/classes/jdk/jshell/SourceCodeAnalysisImpl.java @@ -300,17 +300,8 @@ private List completionSuggestionsImpl(String code, int cursor, int[ identifier = m.group(); } } - code = code.substring(0, cursor); - if (code.trim().isEmpty()) { //TODO: comment handling - code += ";"; - } - boolean[] moduleImport = new boolean[1]; - OuterWrap codeWrap = switch (guessKind(code, moduleImport)) { - case IMPORT -> moduleImport[0] ? proc.outerMap.wrapImport(Wrap.simpleWrap(code), null) - : proc.outerMap.wrapImport(Wrap.simpleWrap(code + "any.any"), null); - case CLASS, METHOD -> proc.outerMap.wrapInTrialClass(Wrap.classMemberWrap(code)); - default -> proc.outerMap.wrapInTrialClass(Wrap.methodWrap(code)); - }; + + OuterWrap codeWrap = wrapCodeForCompletion(code, cursor, true); String[] requiredPrefix = new String[] {identifier}; return computeSuggestions(codeWrap, code, cursor, requiredPrefix, anchor).stream() .filter(s -> filteringText(s).startsWith(requiredPrefix[0]) && !s.continuation().equals(REPL_DOESNOTMATTER_CLASS_NAME)) @@ -1740,15 +1731,11 @@ public List documentation(String code, int cursor, boolean comput }; private List documentationImpl(String code, int cursor, boolean computeJavadoc) { - code = code.substring(0, cursor); - if (code.trim().isEmpty()) { //TODO: comment handling - code += ";"; - } - - if (guessKind(code) == Kind.IMPORT) + OuterWrap codeWrap = wrapCodeForCompletion(code, cursor, false); + if (codeWrap == null) { + //import: return Collections.emptyList(); - - OuterWrap codeWrap = proc.outerMap.wrapInTrialClass(Wrap.methodWrap(code)); + } return proc.taskFactory.analyze(codeWrap, List.of(keepParameterNames), at -> { SourcePositions sp = at.trees().getSourcePositions(); CompilationUnitTree topLevel = at.firstCuTree(); @@ -2429,6 +2416,99 @@ public static void waitCurrentBackgroundTasksFinished() throws Exception { INDEXER.submit(() -> {}).get(); } + private OuterWrap wrapCodeForCompletion(String code, int cursor, boolean wrapImports) { + code = code.substring(0, cursor); + if (code.trim().isEmpty()) { //TODO: comment handling + code += ";"; + } + + List imports = new ArrayList<>(); + List declarationParts = new ArrayList<>(); + String lastImport = null; + boolean lastImportIsModuleImport = false; + Wrap declarationWrap = null; + Wrap pendingWrap = null; + String input = code; + boolean cont = true; + int startOffset = 0; + + while (cont) { + if (lastImport != null) { + imports.add(lastImport); + lastImport = null; + } + if (declarationWrap != null) { + declarationParts.add(declarationWrap); + declarationWrap = null; + pendingWrap = null; + } + + String current; + SourceCodeAnalysis.CompletionInfo completeness = analyzeCompletion(input); + int newStartOffset; + + if (completeness.completeness().isComplete() && !completeness.remaining().isBlank()) { + current = input.substring(0, input.length() - completeness.remaining().length()); + newStartOffset = startOffset + input.length() - completeness.remaining().length(); + input = completeness.remaining(); + cont = true; + } else { + current = input; + cont = false; + newStartOffset = startOffset; + } + + boolean[] moduleImport = new boolean[1]; + + switch (guessKind(current, moduleImport)) { + case IMPORT -> { + lastImport = current; + lastImportIsModuleImport = moduleImport[0]; + } + case CLASS, METHOD -> { + pendingWrap = declarationWrap = Wrap.classMemberWrap(whitespaces(code, startOffset) + current); + } + case VARIABLE -> { + declarationWrap = Wrap.classMemberWrap(whitespaces(code, startOffset) + current); + pendingWrap = Wrap.methodWrap(whitespaces(code, startOffset) + current); + } + default -> { + pendingWrap = declarationWrap = Wrap.methodWrap(whitespaces(code, startOffset) + current); + } + } + + startOffset = newStartOffset; + } + + if (lastImport != null) { + if (wrapImports) { + return proc.outerMap.wrapImport(Wrap.simpleWrap(whitespaces(code, startOffset) + lastImport + (!lastImportIsModuleImport ? "any.any" : "")), null); + } else { + return null; + } + } + + if (pendingWrap != null) { + return proc.outerMap.wrapInTrialClass(imports, declarationParts, pendingWrap); + } + + throw new IllegalStateException("No pending wrap for: " + code); + } + + private static String whitespaces(String input, int offset) { + StringBuilder result = new StringBuilder(); + + for (int i = 0; i < offset; i++) { + if (input.charAt(i) == '\n') { + result.append('\n'); + } else { + result.append(' '); + } + } + + return result.toString(); + } + /** * A candidate for continuation of the given user's input. */ diff --git a/test/langtools/jdk/jshell/CompletionSuggestionTest.java b/test/langtools/jdk/jshell/CompletionSuggestionTest.java index 19f7b89a1b30e..7907fa4d027ee 100644 --- a/test/langtools/jdk/jshell/CompletionSuggestionTest.java +++ b/test/langtools/jdk/jshell/CompletionSuggestionTest.java @@ -941,4 +941,14 @@ public void testAnnotation() { assertEval("import static java.lang.annotation.RetentionPolicy.*;"); assertCompletion("@AnnA(C|", true, "CLASS"); } + + @Test + public void testMultiSnippet() { + assertCompletion("String s = \"\"; s.len|", true, "length()"); + assertCompletion("String s() { return \"\"; } s().len|", true, "length()"); + assertCompletion("String s() { return \"\"; } import java.util.List; List.o|", true, "of("); + assertCompletion("String s() { return \"\"; } import java.ut| ", true, "util."); + assertCompletion("class S { public int length() { return 0; } } new S().len|", true, "length()"); + assertSignature("void f() { } f(|", "void f()"); + } }