mirror of
https://github.com/openjdk/jdk.git
synced 2026-01-28 03:58:21 +00:00
8366691: JShell should support a more convenient completion
Reviewed-by: asotona
This commit is contained in:
parent
6df78c4585
commit
76a0732ba5
@ -345,18 +345,21 @@ class ConsoleIOContext extends IOContext {
|
||||
ConsoleIOContextTestSupport.willComputeCompletion();
|
||||
int[] anchor = new int[] {-1};
|
||||
List<Suggestion> suggestions;
|
||||
List<String> doc;
|
||||
List<AttributedString> doc;
|
||||
boolean command = prefix.isEmpty() && text.startsWith("/");
|
||||
if (command) {
|
||||
suggestions = repl.commandCompletionSuggestions(text, cursor, anchor);
|
||||
doc = repl.commandDocumentation(text, cursor, true);
|
||||
doc = repl.commandDocumentation(text, cursor, true)
|
||||
.stream()
|
||||
.map(AttributedString::new)
|
||||
.toList();
|
||||
} else {
|
||||
int prefixLength = prefix.length();
|
||||
suggestions = repl.analysis.completionSuggestions(prefix + text, cursor + prefixLength, anchor);
|
||||
anchor[0] -= prefixLength;
|
||||
doc = repl.analysis.documentation(prefix + text, cursor + prefix.length(), false)
|
||||
.stream()
|
||||
.map(Documentation::signature)
|
||||
.map(this::renderSignature)
|
||||
.toList();
|
||||
}
|
||||
long smartCount = suggestions.stream().filter(Suggestion::matchesType).count();
|
||||
@ -502,6 +505,41 @@ class ConsoleIOContext extends IOContext {
|
||||
}
|
||||
}
|
||||
|
||||
private AttributedString renderSignature(Documentation doc) {
|
||||
int activeParamIndex = doc.activeParameterIndex();
|
||||
String signature = doc.signature();
|
||||
|
||||
if (activeParamIndex == (-1)) {
|
||||
return new AttributedString(signature);
|
||||
}
|
||||
|
||||
int lparen = signature.indexOf('(');
|
||||
int rparen = signature.indexOf(')', lparen);
|
||||
|
||||
if (lparen == (-1) || rparen == (-1)) {
|
||||
return new AttributedString(signature);
|
||||
}
|
||||
|
||||
AttributedStringBuilder result = new AttributedStringBuilder();
|
||||
|
||||
result.append(signature.substring(0, lparen + 1), AttributedStyle.DEFAULT);
|
||||
|
||||
String[] params = signature.substring(lparen + 1, rparen).split(", *");
|
||||
String sep = "";
|
||||
|
||||
for (int i = 0; i < params.length; i++) {
|
||||
result.append(sep);
|
||||
result.append(params[i], i == activeParamIndex ? AttributedStyle.BOLD
|
||||
: AttributedStyle.DEFAULT);
|
||||
|
||||
sep = ", ";
|
||||
}
|
||||
|
||||
result.append(signature.substring(rparen), AttributedStyle.DEFAULT);
|
||||
|
||||
return result.toAttributedString();
|
||||
}
|
||||
|
||||
private CompletionTask.Result doPrintFullDocumentation(List<CompletionTask> todo, List<String> doc, boolean command) {
|
||||
if (doc != null && !doc.isEmpty()) {
|
||||
Terminal term = in.getTerminal();
|
||||
@ -722,9 +760,9 @@ class ConsoleIOContext extends IOContext {
|
||||
|
||||
private final class CommandSynopsisTask implements CompletionTask {
|
||||
|
||||
private final List<String> synopsis;
|
||||
private final List<AttributedString> synopsis;
|
||||
|
||||
public CommandSynopsisTask(List<String> synposis) {
|
||||
public CommandSynopsisTask(List<AttributedString> synposis) {
|
||||
this.synopsis = synposis;
|
||||
}
|
||||
|
||||
@ -738,6 +776,7 @@ class ConsoleIOContext extends IOContext {
|
||||
// try {
|
||||
in.getTerminal().writer().println();
|
||||
in.getTerminal().writer().println(synopsis.stream()
|
||||
.map(doc -> doc.toAnsi(in.getTerminal()))
|
||||
.map(l -> l.replaceAll("\n", LINE_SEPARATOR))
|
||||
.collect(Collectors.joining(LINE_SEPARATORS2)));
|
||||
// } catch (IOException ex) {
|
||||
@ -771,9 +810,9 @@ class ConsoleIOContext extends IOContext {
|
||||
|
||||
private final class ExpressionSignaturesTask implements CompletionTask {
|
||||
|
||||
private final List<String> doc;
|
||||
private final List<AttributedString> doc;
|
||||
|
||||
public ExpressionSignaturesTask(List<String> doc) {
|
||||
public ExpressionSignaturesTask(List<AttributedString> doc) {
|
||||
this.doc = doc;
|
||||
}
|
||||
|
||||
@ -786,7 +825,9 @@ class ConsoleIOContext extends IOContext {
|
||||
public Result perform(String text, int cursor) throws IOException {
|
||||
in.getTerminal().writer().println();
|
||||
in.getTerminal().writer().println(repl.getResourceString("jshell.console.completion.current.signatures"));
|
||||
in.getTerminal().writer().println(doc.stream().collect(Collectors.joining(LINE_SEPARATOR)));
|
||||
in.getTerminal().writer().println(doc.stream()
|
||||
.map(doc -> doc.toAnsi(in.getTerminal()))
|
||||
.collect(Collectors.joining(LINE_SEPARATOR)));
|
||||
return Result.FINISH;
|
||||
}
|
||||
|
||||
|
||||
@ -118,7 +118,7 @@ public abstract class JavadocHelper implements AutoCloseable {
|
||||
StandardJavaFileManager fm = compiler.getStandardFileManager(null, null, null);
|
||||
try {
|
||||
fm.setLocationFromPaths(StandardLocation.SOURCE_PATH, sourceLocations);
|
||||
return new OnDemandJavadocHelper(mainTask, fm);
|
||||
return new OnDemandJavadocHelper(mainTask, fm, sourceLocations);
|
||||
} catch (IOException ex) {
|
||||
try {
|
||||
fm.close();
|
||||
@ -135,6 +135,21 @@ public abstract class JavadocHelper implements AutoCloseable {
|
||||
}
|
||||
@Override
|
||||
public void close() throws IOException {}
|
||||
|
||||
@Override
|
||||
public String getResolvedDocComment(StoredElement forElement) throws IOException {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public StoredElement getHandle(Element forElement) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Collection<? extends Path> getSourceLocations() {
|
||||
return List.of();
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
@ -147,6 +162,7 @@ public abstract class JavadocHelper implements AutoCloseable {
|
||||
* @throws IOException if something goes wrong in the search
|
||||
*/
|
||||
public abstract String getResolvedDocComment(Element forElement) throws IOException;
|
||||
public abstract String getResolvedDocComment(StoredElement forElement) throws IOException;
|
||||
|
||||
/**Returns an element representing the same given program element, but the returned element will
|
||||
* be resolved from source, if it can be found. Returns the original element if the source for
|
||||
@ -158,6 +174,9 @@ public abstract class JavadocHelper implements AutoCloseable {
|
||||
*/
|
||||
public abstract Element getSourceElement(Element forElement) throws IOException;
|
||||
|
||||
public abstract StoredElement getHandle(Element forElement);
|
||||
public abstract Collection<? extends Path> getSourceLocations();
|
||||
|
||||
/**Closes the helper.
|
||||
*
|
||||
* @throws IOException if something foes wrong during the close
|
||||
@ -165,16 +184,20 @@ public abstract class JavadocHelper implements AutoCloseable {
|
||||
@Override
|
||||
public abstract void close() throws IOException;
|
||||
|
||||
public record StoredElement(String module, String binaryName, String handle) {}
|
||||
|
||||
private static final class OnDemandJavadocHelper extends JavadocHelper {
|
||||
private final JavacTask mainTask;
|
||||
private final JavaFileManager baseFileManager;
|
||||
private final StandardJavaFileManager fm;
|
||||
private final Map<String, Pair<JavacTask, TreePath>> signature2Source = new HashMap<>();
|
||||
private final Collection<? extends Path> sourceLocations;
|
||||
|
||||
private OnDemandJavadocHelper(JavacTask mainTask, StandardJavaFileManager fm) {
|
||||
private OnDemandJavadocHelper(JavacTask mainTask, StandardJavaFileManager fm, Collection<? extends Path> sourceLocations) {
|
||||
this.mainTask = mainTask;
|
||||
this.baseFileManager = ((JavacTaskImpl) mainTask).getContext().get(JavaFileManager.class);
|
||||
this.fm = fm;
|
||||
this.sourceLocations = sourceLocations;
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -187,6 +210,16 @@ public abstract class JavadocHelper implements AutoCloseable {
|
||||
return getResolvedDocComment(sourceElement.fst, sourceElement.snd);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getResolvedDocComment(StoredElement forElement) throws IOException {
|
||||
Pair<JavacTask, TreePath> sourceElement = getSourceElement(forElement);
|
||||
|
||||
if (sourceElement == null)
|
||||
return null;
|
||||
|
||||
return getResolvedDocComment(sourceElement.fst, sourceElement.snd);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Element getSourceElement(Element forElement) throws IOException {
|
||||
Pair<JavacTask, TreePath> sourceElement = getSourceElement(mainTask, forElement);
|
||||
@ -202,7 +235,30 @@ public abstract class JavadocHelper implements AutoCloseable {
|
||||
return result;
|
||||
}
|
||||
|
||||
private String getResolvedDocComment(JavacTask task, TreePath el) throws IOException {
|
||||
@Override
|
||||
public StoredElement getHandle(Element forElement) {
|
||||
TypeElement type = topLevelType(forElement);
|
||||
|
||||
if (type == null)
|
||||
return null;
|
||||
|
||||
Elements elements = mainTask.getElements();
|
||||
ModuleElement module = elements.getModuleOf(type);
|
||||
String moduleName = module == null || module.isUnnamed()
|
||||
? null
|
||||
: module.getQualifiedName().toString();
|
||||
String binaryName = elements.getBinaryName(type).toString();
|
||||
String handle = elementSignature(forElement);
|
||||
|
||||
return new StoredElement(moduleName, binaryName, handle);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Collection<? extends Path> getSourceLocations() {
|
||||
return sourceLocations;
|
||||
}
|
||||
|
||||
private String getResolvedDocComment(JavacTask task, TreePath el) throws IOException {
|
||||
DocTrees trees = DocTrees.instance(task);
|
||||
Element element = trees.getElement(el);
|
||||
String docComment = trees.getDocComment(el);
|
||||
@ -634,7 +690,7 @@ public abstract class JavadocHelper implements AutoCloseable {
|
||||
.filter(supMethod -> task.getElements().overrides(method, supMethod, type));
|
||||
}
|
||||
|
||||
/* Find types from which methods in type may inherit javadoc, in the proper order.*/
|
||||
/* Find types from which methods in binaryName may inherit javadoc, in the proper order.*/
|
||||
private Stream<Element> superTypeForInheritDoc(JavacTask task, Element type) {
|
||||
TypeElement clazz = (TypeElement) type;
|
||||
Stream<Element> result = interfaces(clazz);
|
||||
@ -701,6 +757,35 @@ public abstract class JavadocHelper implements AutoCloseable {
|
||||
return exc != null ? exc.toString() : null;
|
||||
}
|
||||
|
||||
private Pair<JavacTask, TreePath> getSourceElement(StoredElement el) throws IOException {
|
||||
if (el == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
String handle = el.handle();
|
||||
Pair<JavacTask, TreePath> cached = signature2Source.get(handle);
|
||||
|
||||
if (cached != null) {
|
||||
return cached.fst != null ? cached : null;
|
||||
}
|
||||
|
||||
Pair<JavacTask, CompilationUnitTree> source = findSource(el.module(), el.binaryName());
|
||||
|
||||
if (source == null)
|
||||
return null;
|
||||
|
||||
fillElementCache(source.fst, source.snd);
|
||||
|
||||
cached = signature2Source.get(handle);
|
||||
|
||||
if (cached != null) {
|
||||
return cached;
|
||||
} else {
|
||||
signature2Source.put(handle, Pair.of(null, null));
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private Pair<JavacTask, TreePath> getSourceElement(JavacTask origin, Element el) throws IOException {
|
||||
String handle = elementSignature(el);
|
||||
Pair<JavacTask, TreePath> cached = signature2Source.get(handle);
|
||||
|
||||
@ -28,6 +28,12 @@ package jdk.jshell;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.function.Supplier;
|
||||
import javax.lang.model.element.Element;
|
||||
import javax.lang.model.element.ElementKind;
|
||||
import javax.lang.model.type.TypeMirror;
|
||||
import javax.lang.model.util.Elements;
|
||||
import javax.lang.model.util.Types;
|
||||
|
||||
/**
|
||||
* Provides analysis utilities for source code input.
|
||||
@ -64,6 +70,18 @@ public abstract class SourceCodeAnalysis {
|
||||
*/
|
||||
public abstract List<Suggestion> completionSuggestions(String input, int cursor, int[] anchor);
|
||||
|
||||
/**
|
||||
* Compute possible follow-ups for the given input.
|
||||
* Uses information from the current {@code JShell} state, including
|
||||
* type information, to filter the suggestions.
|
||||
* @param input the user input, so far
|
||||
* @param cursor the current position of the cursors in the given {@code input} text
|
||||
* @param convertor convert the given {@linkplain ElementSuggestion} to a custom completion suggestions.
|
||||
* @return list of candidate continuations of the given input.
|
||||
* @since 26
|
||||
*/
|
||||
public abstract <S> List<S> completionSuggestions(String input, int cursor, ElementSuggestionConvertor<S> convertor);
|
||||
|
||||
/**
|
||||
* Compute documentation for the given user's input. Multiple {@code Documentation} objects may
|
||||
* be returned when multiple elements match the user's input (like for overloaded methods).
|
||||
@ -315,6 +333,135 @@ public abstract class SourceCodeAnalysis {
|
||||
boolean matchesType();
|
||||
}
|
||||
|
||||
/**
|
||||
* A description of an {@linkplain Element} that is a possible continuation of
|
||||
* a given snippet.
|
||||
*
|
||||
* @apiNote Instances of this interface and instances of the returned {@linkplain Elements}
|
||||
* should only be used and held during the execution of the
|
||||
* {@link #completionSuggestions(java.lang.String, int, jdk.jshell.SourceCodeAnalysis.ElementSuggestionConvertor) }
|
||||
* method. Their use outside of the context of the method is not supported and
|
||||
* the effect is undefined.
|
||||
*
|
||||
* @since 26
|
||||
*/
|
||||
public sealed interface ElementSuggestion permits SourceCodeAnalysisImpl.ElementSuggestionImpl {
|
||||
/**
|
||||
* {@return a possible continuation {@linkplain Element}, or {@code null}
|
||||
* if this item does not represent an {@linkplain Element}.}
|
||||
*/
|
||||
Element element();
|
||||
/**
|
||||
* {@return a possible continuation keyword, or {@code null}
|
||||
* if this item does not represent a keyword.}
|
||||
*/
|
||||
String keyword();
|
||||
/**
|
||||
* {@return {@code true} if this {@linkplain Element}'s type fits into
|
||||
* the context.}
|
||||
*
|
||||
* Typically used when the type of the element fits the expected type.
|
||||
*/
|
||||
boolean matchesType();
|
||||
/**
|
||||
* {@return the offset in the original snippet at which point this {@linkplain Element}
|
||||
* should be inserted.}
|
||||
*/
|
||||
int anchor();
|
||||
/**
|
||||
* {@return a {@linkplain Supplier} for the javadoc documentation for this Element.}
|
||||
*
|
||||
* @apiNote The instance returned from this method is safe to hold for extended
|
||||
* periods of time, and can be called outside of the context of the
|
||||
* {@link #completionSuggestions(java.lang.String, int, jdk.jshell.SourceCodeAnalysis.ElementSuggestionConvertor) } method.
|
||||
*/
|
||||
Supplier<String> documentation();
|
||||
}
|
||||
|
||||
/**
|
||||
* Permit access to completion state.
|
||||
*
|
||||
* @since 26
|
||||
*/
|
||||
public sealed interface CompletionState permits SourceCodeAnalysisImpl.CompletionStateImpl {
|
||||
/**
|
||||
* {@return true if the given element is available using the simple name at
|
||||
* the place of the cursor.}
|
||||
*
|
||||
* @param el {@linkplain Element} to check
|
||||
*/
|
||||
public boolean availableUsingSimpleName(Element el);
|
||||
/**
|
||||
* {@return flags describing the overall completion context.}
|
||||
*/
|
||||
public Set<CompletionContext> completionContext();
|
||||
/**
|
||||
* {@return if the context is a qualified expression
|
||||
* (i.e. {@link CompletionContext#QUALIFIED} is set),
|
||||
* the type of the selector expression; {@code null} otherwise.}
|
||||
*/
|
||||
public TypeMirror selectorType();
|
||||
/**
|
||||
* {@return an implementation of some utility methods for
|
||||
* operating on elements}
|
||||
*/
|
||||
Elements elementUtils();
|
||||
/**
|
||||
* {@return an implementation of some utility methods for
|
||||
* operating on types}
|
||||
*/
|
||||
Types typeUtils();
|
||||
}
|
||||
|
||||
/**
|
||||
* Various flags describing the context in which the completion happens.
|
||||
*
|
||||
* @since 26
|
||||
*/
|
||||
public enum CompletionContext {
|
||||
/**
|
||||
* The context is inside annotation attributes.
|
||||
*/
|
||||
ANNOTATION_ATTRIBUTE,
|
||||
/**
|
||||
* Parentheses should not be filled for methods and constructor
|
||||
* in the current context.
|
||||
*
|
||||
* Typically used in the import or method reference contexts.
|
||||
*/
|
||||
NO_PAREN,
|
||||
/**
|
||||
* Interpret {@link ElementKind#ANNOTATION_TYPE}s as annotation uses. Typically means
|
||||
* they should be prefixed with {@code @}.
|
||||
*/
|
||||
TYPES_AS_ANNOTATIONS,
|
||||
/**
|
||||
* The context is in a qualified expression (like member access). Simple
|
||||
* names only should be used.
|
||||
*/
|
||||
QUALIFIED,
|
||||
;
|
||||
}
|
||||
|
||||
/**
|
||||
* A convertor from a list of {@linkplain ElementSuggestion} to a list
|
||||
* of custom target completion items.
|
||||
*
|
||||
* @param <S> a custom target completion type.
|
||||
* @since 26
|
||||
*/
|
||||
public interface ElementSuggestionConvertor<S> {
|
||||
/**
|
||||
* Convert a list of {@linkplain ElementSuggestion} to a list
|
||||
* of custom completion items.
|
||||
*
|
||||
* @param state the state of the completion
|
||||
* @param suggestions the input suggestions
|
||||
* @return the converted suggestions
|
||||
*/
|
||||
public List<S> convert(CompletionState state, List<? extends ElementSuggestion> suggestions);
|
||||
}
|
||||
|
||||
/**
|
||||
* A documentation for a candidate for continuation of the given user's input.
|
||||
*/
|
||||
@ -333,6 +480,18 @@ public abstract class SourceCodeAnalysis {
|
||||
* @return the javadoc, or null if not found or not requested
|
||||
*/
|
||||
String javadoc();
|
||||
|
||||
/**
|
||||
* If this {@code Documentation} is created for a method invocation,
|
||||
* return the current parameter index.
|
||||
*
|
||||
* @implNote the default implementation returns {@code -1}
|
||||
* @return the active parameter index, or {@code -1} if not available
|
||||
* @since 26
|
||||
*/
|
||||
default int activeParameterIndex() {
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@ -114,11 +114,13 @@ import java.util.Map;
|
||||
import java.util.NoSuchElementException;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.Callable;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.function.BiConsumer;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.function.Function;
|
||||
import java.util.function.Supplier;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
import java.util.stream.Collectors;
|
||||
@ -152,8 +154,13 @@ import static jdk.jshell.SourceCodeAnalysis.Completeness.DEFINITELY_INCOMPLETE;
|
||||
import static jdk.jshell.TreeDissector.printType;
|
||||
|
||||
import static java.util.stream.Collectors.joining;
|
||||
import static javax.lang.model.element.ElementKind.CONSTRUCTOR;
|
||||
import static javax.lang.model.element.ElementKind.MODULE;
|
||||
import static javax.lang.model.element.ElementKind.PACKAGE;
|
||||
|
||||
import javax.lang.model.type.IntersectionType;
|
||||
import javax.lang.model.util.Elements;
|
||||
import jdk.internal.shellsupport.doc.JavadocHelper.StoredElement;
|
||||
|
||||
/**
|
||||
* The concrete implementation of SourceCodeAnalysis.
|
||||
@ -278,18 +285,76 @@ class SourceCodeAnalysisImpl extends SourceCodeAnalysis {
|
||||
|
||||
@Override
|
||||
public List<Suggestion> completionSuggestions(String code, int cursor, int[] anchor) {
|
||||
suspendIndexing();
|
||||
ElementSuggestionConvertor<Suggestion> convertor = (state, suggestions) -> {
|
||||
Set<String> haveParams = suggestions.stream()
|
||||
.map(s -> s.element())
|
||||
.filter(el -> el != null)
|
||||
.filter(IS_CONSTRUCTOR.or(IS_METHOD))
|
||||
.filter(c -> !((ExecutableElement)c).getParameters().isEmpty())
|
||||
.map(this::simpleContinuationName)
|
||||
.collect(toSet());
|
||||
List<Suggestion> result = new ArrayList<>();
|
||||
|
||||
for (ElementSuggestion s : suggestions) {
|
||||
Element el = s.element();
|
||||
if (el != null) {
|
||||
String continuation = continuationName(state, el);
|
||||
|
||||
switch (el.getKind()) {
|
||||
case CONSTRUCTOR, METHOD -> {
|
||||
if (state.completionContext().contains(CompletionContext.ANNOTATION_ATTRIBUTE)) {
|
||||
continuation += " = ";
|
||||
} else if (!state.completionContext().contains(CompletionContext.NO_PAREN)) {
|
||||
// add trailing open or matched parenthesis, as approriate:
|
||||
continuation += haveParams.contains(continuation) ? "(" : "()";
|
||||
}
|
||||
}
|
||||
case ANNOTATION_TYPE -> {
|
||||
if (state.completionContext().contains(CompletionContext.TYPES_AS_ANNOTATIONS)) {
|
||||
boolean hasAnyAttributes =
|
||||
ElementFilter.methodsIn(el.getEnclosedElements())
|
||||
.stream()
|
||||
.anyMatch(attribute -> attribute.getParameters().isEmpty());
|
||||
String paren = hasAnyAttributes ? "(" : "";
|
||||
continuation = "@" + continuation + paren;
|
||||
}
|
||||
}
|
||||
case PACKAGE ->
|
||||
// add trailing dot to package names
|
||||
continuation += ".";
|
||||
}
|
||||
|
||||
result.add(new SuggestionImpl(continuation, s.matchesType()));
|
||||
} else if (s.keyword() != null) {
|
||||
result.add(new SuggestionImpl(s.keyword(), s.matchesType()));
|
||||
}
|
||||
|
||||
anchor[0] = s.anchor();
|
||||
}
|
||||
|
||||
Collections.sort(result, Comparator.comparing(Suggestion::continuation));
|
||||
|
||||
return result;
|
||||
};
|
||||
try {
|
||||
return completionSuggestionsImpl(code, cursor, anchor);
|
||||
return completionSuggestions(code, cursor, convertor);
|
||||
} catch (Throwable exc) {
|
||||
proc.debug(exc, "Exception thrown in SourceCodeAnalysisImpl.completionSuggestions");
|
||||
return Collections.emptyList();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public <Suggestion> List<Suggestion> completionSuggestions(String code, int cursor, ElementSuggestionConvertor<Suggestion> convertor) {
|
||||
suspendIndexing();
|
||||
try {
|
||||
return completionSuggestionsImpl(code, cursor, convertor);
|
||||
} finally {
|
||||
resumeIndexing();
|
||||
}
|
||||
}
|
||||
|
||||
private List<Suggestion> completionSuggestionsImpl(String code, int cursor, int[] anchor) {
|
||||
private <Suggestion> List<Suggestion> completionSuggestionsImpl(String code, int cursor, ElementSuggestionConvertor<Suggestion> suggestionConvertor) {
|
||||
code = code.substring(0, cursor);
|
||||
Matcher m = JAVA_IDENTIFIER.matcher(code);
|
||||
String identifier = "";
|
||||
@ -302,31 +367,27 @@ class SourceCodeAnalysisImpl extends SourceCodeAnalysis {
|
||||
}
|
||||
|
||||
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))
|
||||
.sorted(Comparator.comparing(Suggestion::continuation))
|
||||
.toList();
|
||||
return computeSuggestions(codeWrap, code, cursor, identifier, suggestionConvertor);
|
||||
}
|
||||
|
||||
private static String filteringText(Suggestion suggestion) {
|
||||
return suggestion instanceof SuggestionImpl impl
|
||||
? impl.filteringText
|
||||
: suggestion.continuation();
|
||||
}
|
||||
private static List<String> COMPLETION_EXTRA_PARAMETERS = List.of("-parameters");
|
||||
|
||||
private List<Suggestion> computeSuggestions(OuterWrap code, String inputCode, int cursor, String[] requiredPrefix, int[] anchor) {
|
||||
return proc.taskFactory.analyze(code, at -> {
|
||||
private <Suggestion> List<Suggestion> computeSuggestions(OuterWrap code, String inputCode, int cursor, String prefix, ElementSuggestionConvertor<Suggestion> suggestionConvertor) {
|
||||
return proc.taskFactory.analyze(code, COMPLETION_EXTRA_PARAMETERS, at -> {
|
||||
try (JavadocHelper javadoc = JavadocHelper.create(at.task, findSources())) {
|
||||
SourcePositions sp = at.trees().getSourcePositions();
|
||||
CompilationUnitTree topLevel = at.firstCuTree();
|
||||
List<Suggestion> result = new ArrayList<>();
|
||||
TreePath tp = pathFor(topLevel, sp, code, cursor);
|
||||
if (tp != null) {
|
||||
List<ElementSuggestion> result = new ArrayList<>();
|
||||
Scope scope = at.trees().getScope(tp);
|
||||
Collection<? extends Element> scopeContent = scopeContent(at, scope, IDENTITY);
|
||||
Set<CompletionContext> completionContext = EnumSet.noneOf(CompletionContext.class);
|
||||
Predicate<Element> accessibility = createAccessibilityFilter(at, tp);
|
||||
Predicate<Element> smartTypeFilter;
|
||||
Predicate<Element> smartFilter;
|
||||
Iterable<TypeMirror> targetTypes = findTargetType(at, tp);
|
||||
TypeMirror selectorType = null;
|
||||
if (targetTypes != null) {
|
||||
if (tp.getLeaf().getKind() == Kind.MEMBER_REFERENCE) {
|
||||
Types types = at.getTypes();
|
||||
@ -386,24 +447,24 @@ class SourceCodeAnalysisImpl extends SourceCodeAnalysis {
|
||||
}
|
||||
switch (tp.getLeaf().getKind()) {
|
||||
case MEMBER_REFERENCE, MEMBER_SELECT: {
|
||||
completionContext.add(CompletionContext.QUALIFIED);
|
||||
|
||||
javax.lang.model.element.Name identifier;
|
||||
ExpressionTree expression;
|
||||
Function<Boolean, String> paren;
|
||||
if (tp.getLeaf().getKind() == Kind.MEMBER_SELECT) {
|
||||
MemberSelectTree mst = (MemberSelectTree)tp.getLeaf();
|
||||
identifier = mst.getIdentifier();
|
||||
expression = mst.getExpression();
|
||||
paren = DEFAULT_PAREN;
|
||||
} else {
|
||||
MemberReferenceTree mst = (MemberReferenceTree)tp.getLeaf();
|
||||
identifier = mst.getName();
|
||||
expression = mst.getQualifierExpression();
|
||||
paren = NO_PAREN;
|
||||
completionContext.add(CompletionContext.NO_PAREN);
|
||||
}
|
||||
if (identifier.contentEquals("*"))
|
||||
break;
|
||||
TreePath exprPath = new TreePath(tp, expression);
|
||||
TypeMirror site = at.trees().getTypeMirror(exprPath);
|
||||
selectorType = at.trees().getTypeMirror(exprPath);
|
||||
boolean staticOnly = isStaticContext(at, exprPath);
|
||||
ImportTree it = findImport(tp);
|
||||
|
||||
@ -413,17 +474,14 @@ class SourceCodeAnalysisImpl extends SourceCodeAnalysis {
|
||||
? ((MemberSelectTree) it.getQualifiedIdentifier()).getExpression().toString() + "."
|
||||
: "";
|
||||
|
||||
addModuleElements(at, qualifiedPrefix, result);
|
||||
addModuleElements(at, javadoc, selectStart, qualifiedPrefix + prefix, result);
|
||||
|
||||
requiredPrefix[0] = qualifiedPrefix + requiredPrefix[0];
|
||||
anchor[0] = selectStart;
|
||||
|
||||
return result;
|
||||
break;
|
||||
}
|
||||
|
||||
boolean isImport = it != null;
|
||||
|
||||
List<? extends Element> members = membersOf(at, site, staticOnly && !isImport && tp.getLeaf().getKind() == Kind.MEMBER_SELECT);
|
||||
List<? extends Element> members = membersOf(at, selectorType, staticOnly && !isImport && tp.getLeaf().getKind() == Kind.MEMBER_SELECT);
|
||||
Predicate<Element> filter = accessibility;
|
||||
|
||||
if (isNewClass(tp)) { // new xxx.|
|
||||
@ -434,7 +492,7 @@ class SourceCodeAnalysisImpl extends SourceCodeAnalysis {
|
||||
}
|
||||
return true;
|
||||
});
|
||||
addElements(membersOf(at, members), constructorFilter, smartFilter, result);
|
||||
addElements(javadoc, membersOf(at, members), constructorFilter, smartFilter, cursor, prefix, result);
|
||||
|
||||
filter = filter.and(IS_PACKAGE);
|
||||
} else if (isThrowsClause(tp)) {
|
||||
@ -442,7 +500,7 @@ class SourceCodeAnalysisImpl extends SourceCodeAnalysis {
|
||||
filter = filter.and(IS_PACKAGE.or(IS_CLASS).or(IS_INTERFACE));
|
||||
smartFilter = IS_PACKAGE.negate().and(smartTypeFilter);
|
||||
} else if (isImport) {
|
||||
paren = NO_PAREN;
|
||||
completionContext.add(CompletionContext.NO_PAREN);
|
||||
if (!it.isStatic()) {
|
||||
filter = filter.and(IS_PACKAGE.or(IS_CLASS).or(IS_INTERFACE));
|
||||
}
|
||||
@ -452,7 +510,7 @@ class SourceCodeAnalysisImpl extends SourceCodeAnalysis {
|
||||
|
||||
filter = filter.and(staticOnly ? STATIC_ONLY : INSTANCE_ONLY);
|
||||
|
||||
addElements(members, filter, smartFilter, paren, result);
|
||||
addElements(javadoc, members, filter, smartFilter, cursor, prefix, result);
|
||||
break;
|
||||
}
|
||||
case IDENTIFIER:
|
||||
@ -466,35 +524,43 @@ class SourceCodeAnalysisImpl extends SourceCodeAnalysis {
|
||||
if (enclosingExpression != null) { // expr.new IDENT|
|
||||
TypeMirror site = at.trees().getTypeMirror(new TreePath(tp, enclosingExpression));
|
||||
filter = filter.and(el -> el.getEnclosingElement().getKind() == ElementKind.CLASS && !el.getEnclosingElement().getModifiers().contains(Modifier.STATIC));
|
||||
addElements(membersOf(at, membersOf(at, site, false)), filter, smartFilter, result);
|
||||
addElements(javadoc, membersOf(at, membersOf(at, site, false)), filter, smartFilter, cursor, prefix, result);
|
||||
} else {
|
||||
addScopeElements(at, scope, listEnclosed, filter, smartFilter, result);
|
||||
addScopeElements(at, javadoc, scope, listEnclosed, filter, smartFilter, cursor, prefix, result);
|
||||
}
|
||||
break;
|
||||
}
|
||||
if (isThrowsClause(tp)) {
|
||||
Predicate<Element> accept = accessibility.and(STATIC_ONLY)
|
||||
.and(IS_PACKAGE.or(IS_CLASS).or(IS_INTERFACE));
|
||||
addScopeElements(at, scope, IDENTITY, accept, IS_PACKAGE.negate().and(smartTypeFilter), result);
|
||||
addElements(javadoc, scopeContent, accept, IS_PACKAGE.negate().and(smartTypeFilter), cursor, prefix, result);
|
||||
break;
|
||||
}
|
||||
if (isAnnotation(tp)) {
|
||||
completionContext.add(CompletionContext.TYPES_AS_ANNOTATIONS);
|
||||
|
||||
if (getAnnotationAttributeNameOrNull(tp.getParentPath(), true) != null) {
|
||||
//nested annotation
|
||||
result = completionSuggestionsImpl(inputCode, cursor - 1, anchor);
|
||||
requiredPrefix[0] = "@" + requiredPrefix[0];
|
||||
return result;
|
||||
return completionSuggestionsImpl(inputCode, cursor - 1, (state, items) -> {
|
||||
CompletionState newState = new CompletionStateImpl(((CompletionStateImpl) state).scopeContent, completionContext, state.selectorType(), state.elementUtils(), state.typeUtils());
|
||||
return suggestionConvertor.convert(newState,
|
||||
items.stream()
|
||||
.filter(s -> s.element().getKind() == ElementKind.ANNOTATION_TYPE)
|
||||
.filter(s -> s.element().getSimpleName().toString().startsWith(prefix))
|
||||
.map(s -> new ElementSuggestionImpl(s.element(), s.keyword(), s.matchesType(), s.anchor(), s.documentation()))
|
||||
.<ElementSuggestion>toList());
|
||||
});
|
||||
}
|
||||
|
||||
Predicate<Element> accept = accessibility.and(STATIC_ONLY)
|
||||
.and(IS_PACKAGE.or(IS_CLASS).or(IS_INTERFACE));
|
||||
addScopeElements(at, scope, IDENTITY, accept, IS_PACKAGE.negate().and(smartTypeFilter), result);
|
||||
addElements(javadoc, scopeContent, accept, IS_PACKAGE.negate().and(smartTypeFilter), cursor - 1, prefix, result);
|
||||
break;
|
||||
}
|
||||
ImportTree it = findImport(tp);
|
||||
if (it != null) {
|
||||
if (it.isModule()) {
|
||||
addModuleElements(at, "", result);
|
||||
addModuleElements(at, javadoc, cursor, prefix, result);
|
||||
} else {
|
||||
// the context of the identifier is an import, look for
|
||||
// package names that start with the identifier.
|
||||
@ -503,20 +569,20 @@ class SourceCodeAnalysisImpl extends SourceCodeAnalysis {
|
||||
// JShell to change to use the default package, and that
|
||||
// change is done, then this should use some variation
|
||||
// of membersOf(at, at.getElements().getPackageElement("").asType(), false)
|
||||
addElements(listPackages(at, ""),
|
||||
addElements(javadoc, listPackages(at, ""),
|
||||
it.isStatic()
|
||||
? STATIC_ONLY.and(accessibility)
|
||||
: accessibility,
|
||||
smartFilter, result);
|
||||
smartFilter, cursor, prefix, result);
|
||||
|
||||
result.add(new SuggestionImpl("module ", false));
|
||||
result.add(new ElementSuggestionImpl(null, "module ", false, cursor, () -> null)); //TODO: better javadoc?
|
||||
}
|
||||
}
|
||||
break;
|
||||
case CLASS: {
|
||||
Predicate<Element> accept = accessibility.and(IS_TYPE);
|
||||
addScopeElements(at, scope, IDENTITY, accept, smartFilter, result);
|
||||
addElements(primitivesOrVoid(at), TRUE, smartFilter, result);
|
||||
addElements(javadoc, scopeContent, accept, smartFilter, cursor, prefix, result);
|
||||
addElements(javadoc, primitivesOrVoid(at), TRUE, smartFilter, cursor, prefix, result);
|
||||
break;
|
||||
}
|
||||
case BLOCK:
|
||||
@ -553,6 +619,8 @@ class SourceCodeAnalysisImpl extends SourceCodeAnalysis {
|
||||
accept = accept.and(IS_TYPE);
|
||||
}
|
||||
} else if (tp.getParentPath().getLeaf().getKind() == Kind.ANNOTATION) {
|
||||
completionContext.add(CompletionContext.ANNOTATION_ATTRIBUTE);
|
||||
|
||||
AnnotationTree annotation = (AnnotationTree) tp.getParentPath().getLeaf();
|
||||
Element annotationType = at.trees().getElement(tp.getParentPath());
|
||||
Set<String> present = annotation.getArguments()
|
||||
@ -563,15 +631,18 @@ class SourceCodeAnalysisImpl extends SourceCodeAnalysis {
|
||||
.filter(var -> var.getKind() == Kind.IDENTIFIER)
|
||||
.map(var -> ((IdentifierTree) var).getName().toString())
|
||||
.collect(Collectors.toSet());
|
||||
addElements(ElementFilter.methodsIn(annotationType.getEnclosedElements()), el -> !present.contains(el.getSimpleName().toString()), TRUE, _ -> " = ", result);
|
||||
addElements(javadoc, ElementFilter.methodsIn(annotationType.getEnclosedElements()), el -> !present.contains(el.getSimpleName().toString()), TRUE, cursor, prefix, /*_ -> " = ", */result);
|
||||
break;
|
||||
} else if (getAnnotationAttributeNameOrNull(tp, true) instanceof String attributeName) {
|
||||
completionContext.add(CompletionContext.ANNOTATION_ATTRIBUTE);
|
||||
|
||||
Element annotationType = tp.getParentPath().getParentPath().getLeaf().getKind() == Kind.ANNOTATION
|
||||
? at.trees().getElement(tp.getParentPath().getParentPath())
|
||||
: at.trees().getElement(tp.getParentPath().getParentPath().getParentPath());
|
||||
if (sp.getEndPosition(topLevel, tp.getParentPath().getLeaf()) == (-1)) {
|
||||
//synthetic 'value':
|
||||
addElements(ElementFilter.methodsIn(annotationType.getEnclosedElements()), TRUE, TRUE, _ -> " = ", result);
|
||||
//TODO: filter out existing:
|
||||
addElements(javadoc, ElementFilter.methodsIn(annotationType.getEnclosedElements()), TRUE, TRUE, cursor, prefix, result);
|
||||
boolean hasValue = findAnnotationAttributeIfAny(annotationType, "value").isPresent();
|
||||
if (!hasValue) {
|
||||
break;
|
||||
@ -588,29 +659,18 @@ class SourceCodeAnalysisImpl extends SourceCodeAnalysis {
|
||||
if (relevantAttributeType.getKind() == TypeKind.DECLARED &&
|
||||
at.getTypes().asElement(relevantAttributeType) instanceof Element attributeTypeEl) {
|
||||
if (attributeTypeEl.getKind() == ElementKind.ANNOTATION_TYPE) {
|
||||
boolean hasAnyAttributes =
|
||||
ElementFilter.methodsIn(attributeTypeEl.getEnclosedElements())
|
||||
.stream()
|
||||
.anyMatch(attribute -> attribute.getParameters().isEmpty());
|
||||
String paren = hasAnyAttributes ? "(" : "";
|
||||
String name = scopeContent(at, scope, IDENTITY).contains(attributeTypeEl)
|
||||
? attributeTypeEl.getSimpleName().toString() //simple name ought to be enough:
|
||||
: ((TypeElement) attributeTypeEl).getQualifiedName().toString();
|
||||
result.add(new SuggestionImpl("@" + name + paren, true));
|
||||
completionContext.add(CompletionContext.TYPES_AS_ANNOTATIONS);
|
||||
|
||||
addElements(javadoc, List.of(attributeTypeEl), TRUE, TRUE, cursor, prefix, result);
|
||||
break;
|
||||
} else if (attributeTypeEl.getKind() == ElementKind.ENUM) {
|
||||
String typeName = scopeContent(at, scope, IDENTITY).contains(attributeTypeEl)
|
||||
? attributeTypeEl.getSimpleName().toString() //simple name ought to be enough:
|
||||
: ((TypeElement) attributeTypeEl).getQualifiedName().toString();
|
||||
result.add(new SuggestionImpl(typeName, true));
|
||||
result.addAll(ElementFilter.fieldsIn(attributeTypeEl.getEnclosedElements())
|
||||
.stream()
|
||||
.filter(e -> e.getKind() == ElementKind.ENUM_CONSTANT)
|
||||
.map(c -> new SuggestionImpl(scopeContent(at, scope, IDENTITY).contains(c)
|
||||
? c.getSimpleName().toString()
|
||||
: typeName + "." + c.getSimpleName(), c.getSimpleName().toString(),
|
||||
true))
|
||||
.toList());
|
||||
List<Element> elements = new ArrayList<>();
|
||||
elements.add(attributeTypeEl);
|
||||
ElementFilter.fieldsIn(attributeTypeEl.getEnclosedElements())
|
||||
.stream()
|
||||
.filter(e -> e.getKind() == ElementKind.ENUM_CONSTANT)
|
||||
.forEach(elements::add);
|
||||
addElements(javadoc, elements, TRUE, TRUE, cursor, prefix, result);
|
||||
break;
|
||||
}
|
||||
}
|
||||
@ -625,7 +685,7 @@ class SourceCodeAnalysisImpl extends SourceCodeAnalysis {
|
||||
insertPrimitiveTypes = false;
|
||||
}
|
||||
|
||||
addScopeElements(at, scope, IDENTITY, accept, smartFilter, result);
|
||||
addElements(javadoc, scopeContent, accept, smartFilter, cursor, prefix, result);
|
||||
|
||||
if (insertPrimitiveTypes) {
|
||||
Tree parent = tp.getParentPath().getLeaf();
|
||||
@ -637,22 +697,32 @@ class SourceCodeAnalysisImpl extends SourceCodeAnalysis {
|
||||
case TYPE_PARAMETER, CLASS, INTERFACE, ENUM, RECORD -> FALSE;
|
||||
default -> TRUE;
|
||||
};
|
||||
addElements(primitivesOrVoid(at), accept, smartFilter, result);
|
||||
addElements(javadoc, primitivesOrVoid(at), accept, smartFilter, cursor, prefix, result);
|
||||
}
|
||||
|
||||
boolean hasBooleanSmartType = targetTypes != null &&
|
||||
StreamSupport.stream(targetTypes.spliterator(), false)
|
||||
.anyMatch(tm -> tm.getKind() == TypeKind.BOOLEAN);
|
||||
if (hasBooleanSmartType) {
|
||||
result.add(new SuggestionImpl("true", true));
|
||||
result.add(new SuggestionImpl("false", true));
|
||||
for (String booleanKeyword : new String[] {"false", "true"}) {
|
||||
if (booleanKeyword.startsWith(prefix)) {
|
||||
result.add(new ElementSuggestionImpl(null, booleanKeyword, true, cursor, () -> null));
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
CompletionState completionState = new CompletionStateImpl(scopeContent, completionContext, selectorType, at.getElements(), at.getTypes());
|
||||
|
||||
return suggestionConvertor.convert(completionState, result);
|
||||
}
|
||||
anchor[0] = cursor;
|
||||
return result;
|
||||
} catch (IOException ex) {
|
||||
//TODO:
|
||||
ex.printStackTrace();
|
||||
}
|
||||
return Collections.emptyList();
|
||||
});
|
||||
}
|
||||
|
||||
@ -1162,59 +1232,74 @@ class SourceCodeAnalysisImpl extends SourceCodeAnalysis {
|
||||
IS_PACKAGE.test(encl);
|
||||
};
|
||||
private final Function<Element, Iterable<? extends Element>> IDENTITY = Collections::singletonList;
|
||||
private final Function<Boolean, String> DEFAULT_PAREN = hasParams -> hasParams ? "(" : "()";
|
||||
private final Function<Boolean, String> NO_PAREN = hasParams -> "";
|
||||
|
||||
private void addElements(Iterable<? extends Element> elements, Predicate<Element> accept, Predicate<Element> smart, List<Suggestion> result) {
|
||||
addElements(elements, accept, smart, DEFAULT_PAREN, result);
|
||||
}
|
||||
private void addElements(Iterable<? extends Element> elements, Predicate<Element> accept, Predicate<Element> smart, Function<Boolean, String> paren, List<Suggestion> result) {
|
||||
Set<String> hasParams = Util.stream(elements)
|
||||
.filter(accept)
|
||||
.filter(IS_CONSTRUCTOR.or(IS_METHOD))
|
||||
.filter(c -> !((ExecutableElement)c).getParameters().isEmpty())
|
||||
.map(this::simpleName)
|
||||
.collect(toSet());
|
||||
|
||||
private void addElements(JavadocHelper javadoc, Iterable<? extends Element> elements, Predicate<Element> accept, Predicate<Element> smart, int anchor, String prefix, List<ElementSuggestion> result) {
|
||||
for (Element c : elements) {
|
||||
if (!accept.test(c))
|
||||
if (!accept.test(c) || !simpleContinuationName(c).startsWith(prefix))
|
||||
continue;
|
||||
if (c.getKind() == ElementKind.METHOD &&
|
||||
c.getSimpleName().contentEquals(Util.DOIT_METHOD_NAME) &&
|
||||
((ExecutableElement) c).getParameters().isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
String simpleName = simpleName(c);
|
||||
switch (c.getKind()) {
|
||||
case CONSTRUCTOR:
|
||||
case METHOD:
|
||||
// add trailing open or matched parenthesis, as approriate
|
||||
simpleName += paren.apply(hasParams.contains(simpleName));
|
||||
break;
|
||||
case PACKAGE:
|
||||
// add trailing dot to package names
|
||||
simpleName += ".";
|
||||
break;
|
||||
}
|
||||
result.add(new SuggestionImpl(simpleName, smart.test(c)));
|
||||
StoredElement stored = javadoc.getHandle(c);
|
||||
Collection<? extends Path> sourceLocations = javadoc.getSourceLocations();
|
||||
result.add(new ElementSuggestionImpl(c, null, smart.test(c), anchor, () -> {
|
||||
return proc.taskFactory.analyze(proc.outerMap.wrapInTrialClass(Wrap.methodWrap(";")), task -> {
|
||||
try (JavadocHelper nestedJavadoc = JavadocHelper.create(task.task, sourceLocations)) {
|
||||
return nestedJavadoc.getResolvedDocComment(stored);
|
||||
} catch (IOException ex) {
|
||||
ex.printStackTrace();
|
||||
}
|
||||
return null;
|
||||
});
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
private void addModuleElements(AnalyzeTask at,
|
||||
private void addModuleElements(AnalyzeTask at, JavadocHelper javadoc, int anchor,
|
||||
String prefix,
|
||||
List<Suggestion> result) {
|
||||
List<ElementSuggestion> result) {
|
||||
for (ModuleElement me : at.getElements().getAllModuleElements()) {
|
||||
if (!me.getQualifiedName().toString().startsWith(prefix)) {
|
||||
continue;
|
||||
}
|
||||
result.add(new SuggestionImpl(me.getQualifiedName().toString(),
|
||||
false));
|
||||
result.add(new ElementSuggestionImpl(me, null, false, anchor, () -> null)); //TODO: better javadoc!
|
||||
}
|
||||
}
|
||||
|
||||
private String simpleName(Element el) {
|
||||
return el.getKind() == ElementKind.CONSTRUCTOR ? el.getEnclosingElement().getSimpleName().toString()
|
||||
: el.getSimpleName().toString();
|
||||
private String simpleContinuationName(Element el) {
|
||||
return switch (el.getKind()) {
|
||||
case CONSTRUCTOR -> el.getEnclosingElement().getSimpleName().toString();
|
||||
case MODULE -> ((ModuleElement) el).getQualifiedName().toString();
|
||||
default -> el.getSimpleName().toString();
|
||||
};
|
||||
}
|
||||
|
||||
private String continuationName(CompletionState state, Element el) {
|
||||
if (state.completionContext().contains(CompletionContext.QUALIFIED)) {
|
||||
return simpleContinuationName(el);
|
||||
} else if (state.availableUsingSimpleName(el)) {
|
||||
return el.getSimpleName().toString();
|
||||
} else {
|
||||
return (switch (el.getKind()) {
|
||||
case PACKAGE -> ((PackageElement) el).getQualifiedName();
|
||||
case ANNOTATION_TYPE, CLASS, ENUM, INTERFACE, RECORD ->
|
||||
primitiveLikeClass(el)
|
||||
? el.getSimpleName()
|
||||
: continuationName(state, el.getEnclosingElement()) + "." + el.getSimpleName();
|
||||
case ENUM_CONSTANT, FIELD, METHOD ->
|
||||
el.getModifiers().contains(Modifier.STATIC)
|
||||
? continuationName(state, el.getEnclosingElement()) + "." + el.getSimpleName()
|
||||
: el.getSimpleName();
|
||||
default -> simpleContinuationName(el);
|
||||
}).toString();
|
||||
}
|
||||
}
|
||||
|
||||
private boolean primitiveLikeClass(Element el) {
|
||||
return el.asType().getKind().isPrimitive() ||
|
||||
el.asType().getKind() == TypeKind.VOID;
|
||||
}
|
||||
|
||||
private List<? extends Element> membersOf(AnalyzeTask at, TypeMirror site, boolean shouldGenerateDotClassItem) {
|
||||
@ -1625,8 +1710,8 @@ class SourceCodeAnalysisImpl extends SourceCodeAnalysis {
|
||||
};
|
||||
}
|
||||
|
||||
private void addScopeElements(AnalyzeTask at, Scope scope, Function<Element, Iterable<? extends Element>> elementConvertor, Predicate<Element> filter, Predicate<Element> smartFilter, List<Suggestion> result) {
|
||||
addElements(scopeContent(at, scope, elementConvertor), filter, smartFilter, result);
|
||||
private void addScopeElements(AnalyzeTask at, JavadocHelper javadoc, Scope scope, Function<Element, Iterable<? extends Element>> elementConvertor, Predicate<Element> filter, Predicate<Element> smartFilter, int anchor, String prefix, List<ElementSuggestion> result) {
|
||||
addElements(javadoc, scopeContent(at, scope, elementConvertor), filter, smartFilter, anchor, prefix, result);
|
||||
}
|
||||
|
||||
private Iterable<Pair<ExecutableElement, ExecutableType>> methodCandidates(AnalyzeTask at, TreePath invocation) {
|
||||
@ -1759,6 +1844,7 @@ class SourceCodeAnalysisImpl extends SourceCodeAnalysis {
|
||||
Stream<Element> elements;
|
||||
Iterable<Pair<ExecutableElement, ExecutableType>> candidates;
|
||||
List<? extends ExpressionTree> arguments;
|
||||
int parameterIndex = -1;
|
||||
|
||||
if (tp.getLeaf().getKind() == Kind.METHOD_INVOCATION || tp.getLeaf().getKind() == Kind.NEW_CLASS) {
|
||||
if (tp.getLeaf().getKind() == Kind.METHOD_INVOCATION) {
|
||||
@ -1783,6 +1869,10 @@ class SourceCodeAnalysisImpl extends SourceCodeAnalysis {
|
||||
}
|
||||
|
||||
elements = Util.stream(candidates).map(method -> method.fst);
|
||||
|
||||
if (prevPath != null) {
|
||||
parameterIndex = arguments.indexOf(prevPath.getLeaf());
|
||||
}
|
||||
} else if (tp.getLeaf().getKind() == Kind.IDENTIFIER || tp.getLeaf().getKind() == Kind.MEMBER_SELECT) {
|
||||
Element el = at.trees().getElement(tp);
|
||||
|
||||
@ -1820,7 +1910,8 @@ class SourceCodeAnalysisImpl extends SourceCodeAnalysis {
|
||||
List<Documentation> result = Collections.emptyList();
|
||||
|
||||
try (JavadocHelper helper = JavadocHelper.create(at.task, findSources())) {
|
||||
result = elements.map(el -> constructDocumentation(at, helper, el, computeJavadoc))
|
||||
int parameterIndexFin = parameterIndex;
|
||||
result = elements.map(el -> constructDocumentation(at, helper, el, parameterIndexFin, computeJavadoc))
|
||||
.filter(Objects::nonNull)
|
||||
.toList();
|
||||
} catch (IOException ex) {
|
||||
@ -1831,7 +1922,7 @@ class SourceCodeAnalysisImpl extends SourceCodeAnalysis {
|
||||
});
|
||||
}
|
||||
|
||||
private Documentation constructDocumentation(AnalyzeTask at, JavadocHelper helper, Element el, boolean computeJavadoc) {
|
||||
private Documentation constructDocumentation(AnalyzeTask at, JavadocHelper helper, Element el, int parameterIndex, boolean computeJavadoc) {
|
||||
String javadoc = null;
|
||||
try {
|
||||
if (hasSyntheticParameterNames(el)) {
|
||||
@ -1844,7 +1935,7 @@ class SourceCodeAnalysisImpl extends SourceCodeAnalysis {
|
||||
proc.debug(ex, "SourceCodeAnalysisImpl.element2String(..., " + el + ")");
|
||||
}
|
||||
String signature = Util.expunge(elementHeader(at, el, !hasSyntheticParameterNames(el), true));
|
||||
return new DocumentationImpl(signature, javadoc);
|
||||
return new DocumentationImpl(signature, javadoc, parameterIndex);
|
||||
}
|
||||
|
||||
public void close() {
|
||||
@ -1857,27 +1948,7 @@ class SourceCodeAnalysisImpl extends SourceCodeAnalysis {
|
||||
}
|
||||
}
|
||||
|
||||
private static final class DocumentationImpl implements Documentation {
|
||||
|
||||
private final String signature;
|
||||
private final String javadoc;
|
||||
|
||||
public DocumentationImpl(String signature, String javadoc) {
|
||||
this.signature = signature;
|
||||
this.javadoc = javadoc;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String signature() {
|
||||
return signature;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String javadoc() {
|
||||
return javadoc;
|
||||
}
|
||||
|
||||
}
|
||||
private record DocumentationImpl(String signature, String javadoc, int activeParameterIndex) implements Documentation {}
|
||||
|
||||
private boolean isEmptyArgumentsContext(List<? extends ExpressionTree> arguments) {
|
||||
if (arguments.size() == 1) {
|
||||
@ -2520,7 +2591,6 @@ class SourceCodeAnalysisImpl extends SourceCodeAnalysis {
|
||||
private static class SuggestionImpl implements Suggestion {
|
||||
|
||||
private final String continuation;
|
||||
private final String filteringText;
|
||||
private final boolean matchesType;
|
||||
|
||||
/**
|
||||
@ -2530,19 +2600,7 @@ class SourceCodeAnalysisImpl extends SourceCodeAnalysis {
|
||||
* @param matchesType does the candidate match the target type
|
||||
*/
|
||||
public SuggestionImpl(String continuation, boolean matchesType) {
|
||||
this(continuation, continuation, matchesType);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a {@code Suggestion} instance.
|
||||
*
|
||||
* @param continuation a candidate continuation of the user's input
|
||||
* @param filteringText a text that should be used for filtering
|
||||
* @param matchesType does the candidate match the target type
|
||||
*/
|
||||
public SuggestionImpl(String continuation, String filteringText, boolean matchesType) {
|
||||
this.continuation = continuation;
|
||||
this.filteringText = filteringText;
|
||||
this.matchesType = matchesType;
|
||||
}
|
||||
|
||||
@ -2620,4 +2678,53 @@ class SourceCodeAnalysisImpl extends SourceCodeAnalysis {
|
||||
}
|
||||
}
|
||||
|
||||
record ElementSuggestionImpl(Element element, String keyword, boolean matchesType, int anchor, Supplier<String> documentation) implements ElementSuggestion {
|
||||
}
|
||||
|
||||
final static class CompletionStateImpl implements CompletionState {
|
||||
|
||||
private final Collection<? extends Element> scopeContent;
|
||||
private final Set<CompletionContext> completionContext;
|
||||
private final TypeMirror selectorType;
|
||||
private final Elements elementUtils;
|
||||
private final Types typeUtils;
|
||||
|
||||
public CompletionStateImpl(Collection<? extends Element> scopeContent,
|
||||
Set<CompletionContext> completionContext,
|
||||
TypeMirror selectorType,
|
||||
Elements elementUtils,
|
||||
Types typeUtils) {
|
||||
this.scopeContent = scopeContent;
|
||||
this.completionContext = completionContext;
|
||||
this.selectorType = selectorType;
|
||||
this.elementUtils = elementUtils;
|
||||
this.typeUtils = typeUtils;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean availableUsingSimpleName(Element el) {
|
||||
return scopeContent.contains(el);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Set<CompletionContext> completionContext() {
|
||||
return completionContext;
|
||||
}
|
||||
|
||||
@Override
|
||||
public TypeMirror selectorType() {
|
||||
return selectorType;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Elements elementUtils() {
|
||||
return elementUtils;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Types typeUtils() {
|
||||
return typeUtils;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
303
test/langtools/jdk/jshell/CompletionAPITest.java
Normal file
303
test/langtools/jdk/jshell/CompletionAPITest.java
Normal file
@ -0,0 +1,303 @@
|
||||
/*
|
||||
* Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved.
|
||||
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
|
||||
*
|
||||
* This code is free software; you can redistribute it and/or modify it
|
||||
* under the terms of the GNU General Public License version 2 only, as
|
||||
* published by the Free Software Foundation.
|
||||
*
|
||||
* This code is distributed in the hope that it will be useful, but WITHOUT
|
||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
||||
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
|
||||
* version 2 for more details (a copy is included in the LICENSE file that
|
||||
* accompanied this code).
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License version
|
||||
* 2 along with this work; if not, write to the Free Software Foundation,
|
||||
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
|
||||
*
|
||||
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
|
||||
* or visit www.oracle.com if you need additional information or have any
|
||||
* questions.
|
||||
*/
|
||||
|
||||
/*
|
||||
* @test
|
||||
* @bug 8366691
|
||||
* @summary Test JShell Completion API
|
||||
* @library /tools/lib
|
||||
* @modules jdk.compiler/com.sun.tools.javac.api
|
||||
* jdk.compiler/com.sun.tools.javac.main
|
||||
* jdk.jdeps/com.sun.tools.javap
|
||||
* jdk.jshell/jdk.jshell:open
|
||||
* @build toolbox.ToolBox toolbox.JarTask toolbox.JavacTask
|
||||
* @build KullaTesting TestingInputStream Compiler
|
||||
* @run junit CompletionAPITest
|
||||
*/
|
||||
|
||||
import java.io.IOException;
|
||||
import java.lang.ref.Reference;
|
||||
import java.lang.ref.WeakReference;
|
||||
import java.lang.reflect.Field;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.Arrays;
|
||||
import java.util.EnumSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
import java.util.function.BiConsumer;
|
||||
import java.util.function.Supplier;
|
||||
import java.util.jar.JarEntry;
|
||||
import java.util.jar.JarOutputStream;
|
||||
import java.util.stream.Collectors;
|
||||
import javax.lang.model.element.Element;
|
||||
import javax.lang.model.element.ElementKind;
|
||||
import javax.lang.model.element.ExecutableElement;
|
||||
import javax.lang.model.element.QualifiedNameable;
|
||||
import javax.lang.model.element.TypeElement;
|
||||
import javax.lang.model.type.DeclaredType;
|
||||
import jdk.jshell.SourceCodeAnalysis.CompletionContext;
|
||||
import jdk.jshell.SourceCodeAnalysis.CompletionState;
|
||||
import jdk.jshell.SourceCodeAnalysis.ElementSuggestion;
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
public class CompletionAPITest extends KullaTesting {
|
||||
|
||||
private static final long TIMEOUT = 2_000;
|
||||
|
||||
@Test
|
||||
public void testAPI() {
|
||||
waitIndexingFinished();
|
||||
assertEval("String str = \"\";");
|
||||
List<String> actual;
|
||||
actual = completionSuggestions("str.", (state, suggestions) -> {
|
||||
assertEquals(EnumSet.of(CompletionContext.QUALIFIED), state.completionContext());
|
||||
});
|
||||
assertTrue(actual.contains("java.lang.String.length()"), String.valueOf(actual));
|
||||
actual = completionSuggestions("java.lang.", (state, suggestions) -> {
|
||||
assertEquals(EnumSet.of(CompletionContext.QUALIFIED), state.completionContext());
|
||||
});
|
||||
assertTrue(actual.contains("java.lang.String"), String.valueOf(actual));
|
||||
actual = completionSuggestions("java.", (state, suggestions) -> {
|
||||
assertEquals(EnumSet.of(CompletionContext.QUALIFIED), state.completionContext());
|
||||
});
|
||||
assertTrue(actual.contains("java.lang"), String.valueOf(actual));
|
||||
assertEval("@interface Ann2 { }");
|
||||
assertEval("@interface Ann1 { Ann2 value(); }");
|
||||
actual = completionSuggestions("@Ann", (state, suggestions) -> {
|
||||
assertEquals(EnumSet.of(CompletionContext.TYPES_AS_ANNOTATIONS), state.completionContext());
|
||||
});
|
||||
assertTrue(actual.containsAll(Set.of("Ann1", "Ann2")), String.valueOf(actual));
|
||||
actual = completionSuggestions("@Ann1(", (state, suggestions) -> {
|
||||
assertEquals(EnumSet.of(CompletionContext.ANNOTATION_ATTRIBUTE,
|
||||
CompletionContext.TYPES_AS_ANNOTATIONS),
|
||||
state.completionContext());
|
||||
});
|
||||
assertTrue(actual.contains("Ann2"), String.valueOf(actual));
|
||||
actual = completionSuggestions("import static java.lang.String.", (state, suggestions) -> {
|
||||
assertEquals(EnumSet.of(CompletionContext.QUALIFIED, CompletionContext.NO_PAREN), state.completionContext());
|
||||
});
|
||||
assertTrue(actual.contains("java.lang.String.valueOf(int arg0)"), String.valueOf(actual));
|
||||
actual = completionSuggestions("java.util.function.IntFunction<String> f = String::", (state, suggestions) -> {
|
||||
assertEquals(EnumSet.of(CompletionContext.QUALIFIED, CompletionContext.NO_PAREN), state.completionContext());
|
||||
});
|
||||
assertTrue(actual.contains("java.lang.String.valueOf(int arg0)"), String.valueOf(actual));
|
||||
actual = completionSuggestions("str.^len", (state, suggestions) -> {
|
||||
assertEquals(EnumSet.of(CompletionContext.QUALIFIED), state.completionContext());
|
||||
});
|
||||
assertTrue(actual.contains("java.lang.String.length()"), String.valueOf(actual));
|
||||
actual = completionSuggestions("^@Depr", (state, suggestions) -> {
|
||||
assertEquals(EnumSet.of(CompletionContext.TYPES_AS_ANNOTATIONS), state.completionContext());
|
||||
});
|
||||
assertTrue(actual.contains("java.lang.Deprecated"), String.valueOf(actual));
|
||||
assertEval("import java.util.*;");
|
||||
actual = completionSuggestions("^ArrayL", (state, suggestions) -> {
|
||||
TypeElement arrayList =
|
||||
suggestions.stream()
|
||||
.filter(el -> el.element() != null)
|
||||
.map(el -> el.element())
|
||||
.filter(el -> el.getKind() == ElementKind.CLASS)
|
||||
.map(el -> (TypeElement) el)
|
||||
.filter(el -> el.getQualifiedName().contentEquals("java.util.ArrayList"))
|
||||
.findAny()
|
||||
.orElseThrow();
|
||||
assertTrue(state.availableUsingSimpleName(arrayList));
|
||||
assertEquals(EnumSet.noneOf(CompletionContext.class), state.completionContext());
|
||||
});
|
||||
assertTrue(actual.contains("java.util.ArrayList"), String.valueOf(actual));
|
||||
completionSuggestions("(new java.util.ArrayList<String>()).", (state, suggestions) -> {
|
||||
List<String> elsWithTypes =
|
||||
suggestions.stream()
|
||||
.filter(el -> el.element() != null)
|
||||
.map(el -> el.element())
|
||||
.filter(el -> el.getKind() == ElementKind.METHOD)
|
||||
.map(el -> el.getSimpleName() + state.typeUtils()
|
||||
.asMemberOf((DeclaredType) state.selectorType(), el)
|
||||
.toString())
|
||||
.toList();
|
||||
assertTrue(elsWithTypes.contains("add(java.lang.String)boolean"));
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testDocumentation() {
|
||||
waitIndexingFinished();
|
||||
|
||||
Path classes = prepareZip();
|
||||
getState().addToClasspath(classes.toString());
|
||||
|
||||
AtomicReference<Supplier<String>> documentation = new AtomicReference<>();
|
||||
AtomicReference<Reference<Element>> clazz = new AtomicReference<>();
|
||||
completionSuggestions("jshelltest.JShellTest", (state, suggestions) -> {
|
||||
ElementSuggestion test =
|
||||
suggestions.stream()
|
||||
.filter(el -> el.element() != null)
|
||||
.filter(el -> el.element().getKind() == ElementKind.CLASS)
|
||||
.filter(el -> ((TypeElement) el.element()).getQualifiedName().contentEquals("jshelltest.JShellTest"))
|
||||
.findAny()
|
||||
.orElseThrow();
|
||||
documentation.set(test.documentation());
|
||||
clazz.set(new WeakReference<>(test.element()));
|
||||
});
|
||||
|
||||
//throw away the JavacTaskPool, so that the cached javac instances are dropped:
|
||||
getState().addToClasspath("undefined");
|
||||
|
||||
long start = System.currentTimeMillis();
|
||||
|
||||
while (clazz.get().get() != null && (System.currentTimeMillis() - start) < TIMEOUT) {
|
||||
System.gc();
|
||||
}
|
||||
|
||||
assertNull(clazz.get().get());
|
||||
assertEquals("JShellTest 0 ", documentation.get().get());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSignature() {
|
||||
waitIndexingFinished();
|
||||
|
||||
assertEval("void test(int i) {}");
|
||||
assertEval("void test(int i, int j) {}");
|
||||
assertSignature("test(|", true, "void test(int i):0", "void test(int i, int j):0");
|
||||
assertSignature("test(0, |", true, "void test(int i, int j):1");
|
||||
}
|
||||
|
||||
private List<String> completionSuggestions(String input,
|
||||
BiConsumer<CompletionState, List<? extends ElementSuggestion>> validator) {
|
||||
int expectedAnchor = input.indexOf('^');
|
||||
|
||||
if (expectedAnchor != (-1)) {
|
||||
input = input.substring(0, expectedAnchor) + input.substring(expectedAnchor + 1);
|
||||
}
|
||||
|
||||
AtomicInteger mergedAnchor = new AtomicInteger(-1);
|
||||
|
||||
List<String> result = getAnalysis().completionSuggestions(input, input.length(), (state, suggestions) -> {
|
||||
validator.accept(state, suggestions);
|
||||
|
||||
if (expectedAnchor != (-1)) {
|
||||
for (ElementSuggestion sugg : suggestions) {
|
||||
if (mergedAnchor.get() == (-1)) {
|
||||
mergedAnchor.set(sugg.anchor());
|
||||
} else {
|
||||
assertEquals(mergedAnchor.get(), sugg.anchor());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return suggestions.stream()
|
||||
.map(this::convertElement)
|
||||
.toList();
|
||||
});
|
||||
|
||||
if (expectedAnchor != (-1)) {
|
||||
assertEquals(expectedAnchor, mergedAnchor.get());
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private String convertElement(ElementSuggestion suggestion) {
|
||||
if (suggestion.keyword() != null) {
|
||||
return suggestion.keyword();
|
||||
}
|
||||
|
||||
Element el = suggestion.element();
|
||||
|
||||
if (el.getKind().isClass() || el.getKind().isInterface() || el.getKind() == ElementKind.PACKAGE) {
|
||||
String qualifiedName = ((QualifiedNameable) el).getQualifiedName().toString();
|
||||
if (qualifiedName.startsWith("REPL.$JShell$")) {
|
||||
String[] parts = qualifiedName.split("\\.", 3);
|
||||
|
||||
return parts[2];
|
||||
} else {
|
||||
return qualifiedName;
|
||||
}
|
||||
} else if (el.getKind().isField()) {
|
||||
return ((QualifiedNameable) el.getEnclosingElement()).getQualifiedName().toString() +
|
||||
"." +
|
||||
el.getSimpleName();
|
||||
} else if (el.getKind() == ElementKind.CONSTRUCTOR || el.getKind() == ElementKind.METHOD) {
|
||||
String name = el.getKind() == ElementKind.CONSTRUCTOR ? "" : "." + el.getSimpleName();
|
||||
ExecutableElement method = (ExecutableElement) el;
|
||||
|
||||
return ((QualifiedNameable) el.getEnclosingElement()).getQualifiedName().toString() +
|
||||
name +
|
||||
method.getParameters()
|
||||
.stream()
|
||||
.map(var -> var.asType().toString() + " " + var.getSimpleName())
|
||||
.collect(Collectors.joining(", ", "(", ")"));
|
||||
} else {
|
||||
return el.getSimpleName().toString();
|
||||
}
|
||||
}
|
||||
|
||||
private Path prepareZip() {
|
||||
String clazz =
|
||||
"package jshelltest;\n" +
|
||||
"/**JShellTest 0" +
|
||||
" */\n" +
|
||||
"public class JShellTest {\n" +
|
||||
"}\n";
|
||||
|
||||
Path srcZip = Paths.get("src.zip");
|
||||
|
||||
try (JarOutputStream out = new JarOutputStream(Files.newOutputStream(srcZip))) {
|
||||
out.putNextEntry(new JarEntry("jshelltest/JShellTest.java"));
|
||||
out.write(clazz.getBytes());
|
||||
} catch (IOException ex) {
|
||||
throw new IllegalStateException(ex);
|
||||
}
|
||||
|
||||
compiler.compile(clazz);
|
||||
|
||||
try {
|
||||
Field availableSources = Class.forName("jdk.jshell.SourceCodeAnalysisImpl").getDeclaredField("availableSourcesOverride");
|
||||
availableSources.setAccessible(true);
|
||||
availableSources.set(null, Arrays.asList(srcZip));
|
||||
} catch (NoSuchFieldException | IllegalArgumentException | IllegalAccessException | ClassNotFoundException ex) {
|
||||
throw new IllegalStateException(ex);
|
||||
}
|
||||
|
||||
return compiler.getClassDir();
|
||||
}
|
||||
//where:
|
||||
private final Compiler compiler = new Compiler();
|
||||
|
||||
static {
|
||||
try {
|
||||
//disable reading of paramater names, to improve stability:
|
||||
Class<?> analysisClass = Class.forName("jdk.jshell.SourceCodeAnalysisImpl");
|
||||
Field params = analysisClass.getDeclaredField("COMPLETION_EXTRA_PARAMETERS");
|
||||
params.setAccessible(true);
|
||||
params.set(null, List.of());
|
||||
} catch (ReflectiveOperationException ex) {
|
||||
throw new IllegalStateException(ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -44,6 +44,7 @@ import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.Set;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.function.BiFunction;
|
||||
import java.util.function.Function;
|
||||
import java.util.jar.JarEntry;
|
||||
@ -901,7 +902,7 @@ public class CompletionSuggestionTest extends KullaTesting {
|
||||
|
||||
@Test
|
||||
public void testAnnotation() {
|
||||
assertCompletion("@Deprec|", "Deprecated");
|
||||
assertCompletion("@Deprec|", "@Deprecated(");
|
||||
assertCompletion("@Deprecated(|", "forRemoval = ", "since = ");
|
||||
assertCompletion("@Deprecated(forRemoval = |", true, "false", "true");
|
||||
assertCompletion("@Deprecated(forRemoval = true, |", "since = ");
|
||||
@ -951,4 +952,16 @@ public class CompletionSuggestionTest extends KullaTesting {
|
||||
assertCompletion("class S { public int length() { return 0; } } new S().len|", true, "length()");
|
||||
assertSignature("void f() { } f(|", "void f()");
|
||||
}
|
||||
|
||||
static {
|
||||
try {
|
||||
//disable reading of paramater names, to improve stability:
|
||||
Class<?> analysisClass = Class.forName("jdk.jshell.SourceCodeAnalysisImpl");
|
||||
Field params = analysisClass.getDeclaredField("COMPLETION_EXTRA_PARAMETERS");
|
||||
params.setAccessible(true);
|
||||
params.set(null, List.of());
|
||||
} catch (ReflectiveOperationException ex) {
|
||||
throw new IllegalStateException(ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -44,6 +44,7 @@ import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.TreeMap;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.function.Function;
|
||||
import java.util.function.Predicate;
|
||||
import java.util.function.Supplier;
|
||||
import java.util.stream.Collectors;
|
||||
@ -969,11 +970,21 @@ public class KullaTesting {
|
||||
}
|
||||
|
||||
public void assertSignature(String code, String... expected) {
|
||||
assertSignature(code, false, expected);
|
||||
}
|
||||
|
||||
public void assertSignature(String code, boolean includeActive, String... expected) {
|
||||
int cursor = code.indexOf('|');
|
||||
code = code.replace("|", "");
|
||||
assertTrue(cursor > -1, "'|' expected, but not found in: " + code);
|
||||
List<Documentation> documentation = getAnalysis().documentation(code, cursor, false);
|
||||
Set<String> docSet = documentation.stream().map(doc -> doc.signature()).collect(Collectors.toSet());
|
||||
Function<Documentation, String> convert;
|
||||
if (includeActive) {
|
||||
convert = doc -> doc.signature() + ":" + doc.activeParameterIndex();
|
||||
} else {
|
||||
convert = doc -> doc.signature();
|
||||
}
|
||||
Set<String> docSet = documentation.stream().map(convert).collect(Collectors.toSet());
|
||||
Set<String> expectedSet = Stream.of(expected).collect(Collectors.toSet());
|
||||
assertEquals(expectedSet, docSet, "Input: " + code);
|
||||
}
|
||||
|
||||
@ -103,8 +103,8 @@ public class ToolTabSnippetTest extends UITesting {
|
||||
inputSink.write("(" + TAB);
|
||||
waitOutput(out, "\\(\n" +
|
||||
resource("jshell.console.completion.current.signatures") + "\n" +
|
||||
"JShellTest\\(String str\\)\n" +
|
||||
"JShellTest\\(String str, int i\\)\n" +
|
||||
"JShellTest\\(\\u001B\\[1mString str\\u001B\\[0m\\)\n" +
|
||||
"JShellTest\\(\\u001B\\[1mString str\\u001B\\[0m, int i\\)\n" +
|
||||
"\n" +
|
||||
resource("jshell.console.see.documentation") +
|
||||
REDRAW_PROMPT + "new JShellTest\\(");
|
||||
@ -138,8 +138,8 @@ public class ToolTabSnippetTest extends UITesting {
|
||||
"str \n" +
|
||||
"\n" +
|
||||
resource("jshell.console.completion.current.signatures") + "\n" +
|
||||
"JShellTest\\(String str\\)\n" +
|
||||
"JShellTest\\(String str, int i\\)\n" +
|
||||
"JShellTest\\(\\u001B\\[1mString str\\u001B\\[0m\\)\n" +
|
||||
"JShellTest\\(\\u001B\\[1mString str\\u001B\\[0m, int i\\)\n" +
|
||||
"\n" +
|
||||
resource("jshell.console.see.documentation") +
|
||||
REDRAW_PROMPT + "new JShellTest\\(");
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user