mirror of
https://github.com/openjdk/jdk.git
synced 2026-03-18 11:53:17 +00:00
8164825: jshell tool: Completion for subcommand
Reviewed-by: jlahoda
This commit is contained in:
parent
7dceb3e785
commit
11de22e133
@ -0,0 +1,97 @@
|
||||
/*
|
||||
* Copyright (c) 2016, 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. Oracle designates this
|
||||
* particular file as subject to the "Classpath" exception as provided
|
||||
* by Oracle in the LICENSE file that accompanied this code.
|
||||
*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
package jdk.internal.jshell.tool;
|
||||
|
||||
import java.util.List;
|
||||
import static java.util.Comparator.comparing;
|
||||
import java.util.Map;
|
||||
import java.util.function.BiPredicate;
|
||||
import java.util.function.Supplier;
|
||||
import static java.util.stream.Collectors.toList;
|
||||
import java.util.stream.Stream;
|
||||
import jdk.internal.jshell.tool.JShellTool.CompletionProvider;
|
||||
import jdk.jshell.SourceCodeAnalysis;
|
||||
import jdk.jshell.SourceCodeAnalysis.Suggestion;
|
||||
|
||||
class ContinuousCompletionProvider implements CompletionProvider {
|
||||
|
||||
static final BiPredicate<String, String> STARTSWITH_MATCHER =
|
||||
(word, input) -> word.startsWith(input);
|
||||
static final BiPredicate<String, String> PERFECT_MATCHER =
|
||||
(word, input) -> word.equals(input);
|
||||
|
||||
private final Supplier<Map<String, CompletionProvider>> wordCompletionProviderSupplier;
|
||||
private final BiPredicate<String, String> matcher;
|
||||
|
||||
ContinuousCompletionProvider(
|
||||
Map<String, CompletionProvider> wordCompletionProvider,
|
||||
BiPredicate<String, String> matcher) {
|
||||
this(() -> wordCompletionProvider, matcher);
|
||||
}
|
||||
|
||||
ContinuousCompletionProvider(
|
||||
Supplier<Map<String, CompletionProvider>> wordCompletionProviderSupplier,
|
||||
BiPredicate<String, String> matcher) {
|
||||
this.wordCompletionProviderSupplier = wordCompletionProviderSupplier;
|
||||
this.matcher = matcher;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Suggestion> completionSuggestions(String input, int cursor, int[] anchor) {
|
||||
String prefix = input.substring(0, cursor);
|
||||
int space = prefix.indexOf(' ');
|
||||
|
||||
Stream<SourceCodeAnalysis.Suggestion> result;
|
||||
|
||||
Map<String, CompletionProvider> wordCompletionProvider = wordCompletionProviderSupplier.get();
|
||||
|
||||
if (space == (-1)) {
|
||||
result = wordCompletionProvider.keySet().stream()
|
||||
.distinct()
|
||||
.filter(key -> key.startsWith(prefix))
|
||||
.map(key -> new JShellTool.ArgSuggestion(key + " "));
|
||||
anchor[0] = 0;
|
||||
} else {
|
||||
String rest = prefix.substring(space + 1);
|
||||
String word = prefix.substring(0, space);
|
||||
|
||||
List<CompletionProvider> candidates = wordCompletionProvider.entrySet().stream()
|
||||
.filter(e -> matcher.test(e.getKey(), word))
|
||||
.map(Map.Entry::getValue)
|
||||
.collect(toList());
|
||||
if (candidates.size() == 1) {
|
||||
result = candidates.get(0).completionSuggestions(rest, cursor - space - 1, anchor).stream();
|
||||
} else {
|
||||
result = Stream.empty();
|
||||
}
|
||||
anchor[0] += space + 1;
|
||||
}
|
||||
|
||||
return result.sorted(comparing(Suggestion::continuation))
|
||||
.collect(toList());
|
||||
}
|
||||
|
||||
}
|
||||
@ -35,9 +35,14 @@ import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.Map.Entry;
|
||||
import java.util.function.Function;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
import static java.util.stream.Collectors.joining;
|
||||
import static java.util.stream.Collectors.toMap;
|
||||
import static jdk.internal.jshell.tool.ContinuousCompletionProvider.PERFECT_MATCHER;
|
||||
import jdk.internal.jshell.tool.JShellTool.CompletionProvider;
|
||||
import static jdk.internal.jshell.tool.JShellTool.EMPTY_COMPLETION_PROVIDER;
|
||||
|
||||
/**
|
||||
* Feedback customization support
|
||||
@ -146,6 +151,17 @@ class Feedback {
|
||||
.forEach(m -> m.readOnly = true);
|
||||
}
|
||||
|
||||
JShellTool.CompletionProvider modeCompletions() {
|
||||
return modeCompletions(EMPTY_COMPLETION_PROVIDER);
|
||||
}
|
||||
|
||||
JShellTool.CompletionProvider modeCompletions(CompletionProvider successor) {
|
||||
return new ContinuousCompletionProvider(
|
||||
() -> modeMap.keySet().stream()
|
||||
.collect(toMap(Function.identity(), m -> successor)),
|
||||
PERFECT_MATCHER);
|
||||
}
|
||||
|
||||
{
|
||||
for (FormatCase e : FormatCase.all)
|
||||
selectorMap.put(e.name().toLowerCase(Locale.US), e);
|
||||
|
||||
@ -112,6 +112,7 @@ import static jdk.internal.jshell.debug.InternalDebugControl.DBG_DEP;
|
||||
import static jdk.internal.jshell.debug.InternalDebugControl.DBG_EVNT;
|
||||
import static jdk.internal.jshell.debug.InternalDebugControl.DBG_FMGR;
|
||||
import static jdk.internal.jshell.debug.InternalDebugControl.DBG_GEN;
|
||||
import static jdk.internal.jshell.tool.ContinuousCompletionProvider.STARTSWITH_MATCHER;
|
||||
|
||||
/**
|
||||
* Command line REPL tool for Java using the JShell API.
|
||||
@ -909,6 +910,7 @@ public class JShellTool implements MessageHandler {
|
||||
|
||||
interface CompletionProvider {
|
||||
List<Suggestion> completionSuggestions(String input, int cursor, int[] anchor);
|
||||
|
||||
}
|
||||
|
||||
enum CommandKind {
|
||||
@ -953,14 +955,31 @@ public class JShellTool implements MessageHandler {
|
||||
|
||||
}
|
||||
|
||||
private static final CompletionProvider EMPTY_COMPLETION_PROVIDER = new FixedCompletionProvider();
|
||||
static final CompletionProvider EMPTY_COMPLETION_PROVIDER = new FixedCompletionProvider();
|
||||
private static final CompletionProvider KEYWORD_COMPLETION_PROVIDER = new FixedCompletionProvider("-all ", "-start ", "-history ");
|
||||
private static final CompletionProvider RELOAD_OPTIONS_COMPLETION_PROVIDER = new FixedCompletionProvider("-restore", "-quiet");
|
||||
private static final CompletionProvider SET_MODE_OPTIONS_COMPLETION_PROVIDER = new FixedCompletionProvider("-command", "-quiet", "-delete");
|
||||
private static final CompletionProvider FILE_COMPLETION_PROVIDER = fileCompletions(p -> true);
|
||||
private final Map<String, Command> commands = new LinkedHashMap<>();
|
||||
private void registerCommand(Command cmd) {
|
||||
commands.put(cmd.command, cmd);
|
||||
}
|
||||
|
||||
private static CompletionProvider skipWordThenCompletion(CompletionProvider completionProvider) {
|
||||
return (input, cursor, anchor) -> {
|
||||
List<Suggestion> result = Collections.emptyList();
|
||||
|
||||
int space = input.indexOf(' ');
|
||||
if (space != -1) {
|
||||
String rest = input.substring(space + 1);
|
||||
result = completionProvider.completionSuggestions(rest, cursor - space - 1, anchor);
|
||||
anchor[0] += space + 1;
|
||||
}
|
||||
|
||||
return result;
|
||||
};
|
||||
}
|
||||
|
||||
private static CompletionProvider fileCompletions(Predicate<Path> accept) {
|
||||
return (code, cursor, anchor) -> {
|
||||
int lastSlash = code.lastIndexOf('/');
|
||||
@ -1037,6 +1056,31 @@ public class JShellTool implements MessageHandler {
|
||||
};
|
||||
}
|
||||
|
||||
private static CompletionProvider orMostSpecificCompletion(
|
||||
CompletionProvider left, CompletionProvider right) {
|
||||
return (code, cursor, anchor) -> {
|
||||
int[] leftAnchor = {-1};
|
||||
int[] rightAnchor = {-1};
|
||||
|
||||
List<Suggestion> leftSuggestions = left.completionSuggestions(code, cursor, leftAnchor);
|
||||
List<Suggestion> rightSuggestions = right.completionSuggestions(code, cursor, rightAnchor);
|
||||
|
||||
List<Suggestion> suggestions = new ArrayList<>();
|
||||
|
||||
if (leftAnchor[0] >= rightAnchor[0]) {
|
||||
anchor[0] = leftAnchor[0];
|
||||
suggestions.addAll(leftSuggestions);
|
||||
}
|
||||
|
||||
if (leftAnchor[0] <= rightAnchor[0]) {
|
||||
anchor[0] = rightAnchor[0];
|
||||
suggestions.addAll(rightSuggestions);
|
||||
}
|
||||
|
||||
return suggestions;
|
||||
};
|
||||
}
|
||||
|
||||
// Snippet lists
|
||||
|
||||
Stream<Snippet> allSnippets() {
|
||||
@ -1123,10 +1167,26 @@ public class JShellTool implements MessageHandler {
|
||||
EMPTY_COMPLETION_PROVIDER));
|
||||
registerCommand(new Command("/set",
|
||||
arg -> cmdSet(arg),
|
||||
new FixedCompletionProvider(SET_SUBCOMMANDS)));
|
||||
new ContinuousCompletionProvider(Map.of(
|
||||
// need more completion for format for usability
|
||||
"format", feedback.modeCompletions(),
|
||||
"truncation", feedback.modeCompletions(),
|
||||
"feedback", feedback.modeCompletions(),
|
||||
"mode", skipWordThenCompletion(orMostSpecificCompletion(
|
||||
feedback.modeCompletions(SET_MODE_OPTIONS_COMPLETION_PROVIDER),
|
||||
SET_MODE_OPTIONS_COMPLETION_PROVIDER)),
|
||||
"prompt", feedback.modeCompletions(),
|
||||
"editor", fileCompletions(Files::isExecutable),
|
||||
"start", FILE_COMPLETION_PROVIDER),
|
||||
STARTSWITH_MATCHER)));
|
||||
registerCommand(new Command("/retain",
|
||||
arg -> cmdRetain(arg),
|
||||
new FixedCompletionProvider(RETAIN_SUBCOMMANDS)));
|
||||
new ContinuousCompletionProvider(Map.of(
|
||||
"feedback", feedback.modeCompletions(),
|
||||
"mode", feedback.modeCompletions(),
|
||||
"editor", fileCompletions(Files::isExecutable),
|
||||
"start", FILE_COMPLETION_PROVIDER),
|
||||
STARTSWITH_MATCHER)));
|
||||
registerCommand(new Command("/?",
|
||||
"help.quest",
|
||||
arg -> cmdHelp(arg),
|
||||
@ -1151,36 +1211,18 @@ public class JShellTool implements MessageHandler {
|
||||
registerCommand(new Command("shortcuts",
|
||||
"help.shortcuts",
|
||||
CommandKind.HELP_SUBJECT));
|
||||
|
||||
commandCompletions = new ContinuousCompletionProvider(
|
||||
commands.values().stream()
|
||||
.filter(c -> c.kind.shouldSuggestCompletions)
|
||||
.collect(toMap(c -> c.command, c -> c.completions)),
|
||||
STARTSWITH_MATCHER);
|
||||
}
|
||||
|
||||
private ContinuousCompletionProvider commandCompletions;
|
||||
|
||||
public List<Suggestion> commandCompletionSuggestions(String code, int cursor, int[] anchor) {
|
||||
String prefix = code.substring(0, cursor);
|
||||
int space = prefix.indexOf(' ');
|
||||
Stream<Suggestion> result;
|
||||
|
||||
if (space == (-1)) {
|
||||
result = commands.values()
|
||||
.stream()
|
||||
.distinct()
|
||||
.filter(cmd -> cmd.kind.shouldSuggestCompletions)
|
||||
.map(cmd -> cmd.command)
|
||||
.filter(key -> key.startsWith(prefix))
|
||||
.map(key -> new ArgSuggestion(key + " "));
|
||||
anchor[0] = 0;
|
||||
} else {
|
||||
String arg = prefix.substring(space + 1);
|
||||
String cmd = prefix.substring(0, space);
|
||||
Command[] candidates = findCommand(cmd, c -> true);
|
||||
if (candidates.length == 1) {
|
||||
result = candidates[0].completions.completionSuggestions(arg, cursor - space, anchor).stream();
|
||||
anchor[0] += space + 1;
|
||||
} else {
|
||||
result = Stream.empty();
|
||||
}
|
||||
}
|
||||
|
||||
return result.sorted((s1, s2) -> s1.continuation().compareTo(s2.continuation()))
|
||||
.collect(Collectors.toList());
|
||||
return commandCompletions.completionSuggestions(code, cursor, anchor);
|
||||
}
|
||||
|
||||
public String commandDocumentation(String code, int cursor) {
|
||||
@ -2484,7 +2526,7 @@ public class JShellTool implements MessageHandler {
|
||||
}
|
||||
}
|
||||
|
||||
private static class ArgSuggestion implements Suggestion {
|
||||
static class ArgSuggestion implements Suggestion {
|
||||
|
||||
private final String continuation;
|
||||
|
||||
|
||||
@ -23,7 +23,7 @@
|
||||
|
||||
/*
|
||||
* @test
|
||||
* @bug 8144095
|
||||
* @bug 8144095 8164825
|
||||
* @summary Test Command Completion
|
||||
* @modules jdk.compiler/com.sun.tools.javac.api
|
||||
* jdk.compiler/com.sun.tools.javac.main
|
||||
@ -40,6 +40,7 @@ import java.nio.file.FileSystems;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.function.Predicate;
|
||||
@ -148,6 +149,80 @@ public class CommandCompletionTest extends ReplToolTesting {
|
||||
assertCompletion("/classpath ~/|", false, completions.toArray(new String[completions.size()]));
|
||||
}
|
||||
|
||||
public void testSet() throws IOException {
|
||||
List<String> p1 = listFiles(Paths.get(""));
|
||||
FileSystems.getDefault().getRootDirectories().forEach(s -> p1.add(s.toString()));
|
||||
Collections.sort(p1);
|
||||
|
||||
String[] modes = {"concise ", "normal ", "silent ", "verbose "};
|
||||
String[] options = {"-command", "-delete", "-quiet"};
|
||||
String[] modesWithOptions = Stream.concat(Arrays.stream(options), Arrays.stream(modes)).sorted().toArray(String[]::new);
|
||||
test(false, new String[] {"--no-startup"},
|
||||
a -> assertCompletion(a, "/se|", false, "/set "),
|
||||
a -> assertCompletion(a, "/set |", false, "editor ", "feedback ", "format ", "mode ", "prompt ", "start ", "truncation "),
|
||||
|
||||
// /set editor
|
||||
a -> assertCompletion(a, "/set e|", false, "editor "),
|
||||
a -> assertCompletion(a, "/set editor |", false, p1.toArray(new String[p1.size()])),
|
||||
|
||||
// /set feedback
|
||||
a -> assertCompletion(a, "/set fe|", false, "feedback "),
|
||||
a -> assertCompletion(a, "/set fe |", false, modes),
|
||||
|
||||
// /set format
|
||||
a -> assertCompletion(a, "/set fo|", false, "format "),
|
||||
a -> assertCompletion(a, "/set fo |", false, modes),
|
||||
|
||||
// /set mode
|
||||
a -> assertCompletion(a, "/set mo|", false, "mode "),
|
||||
a -> assertCompletion(a, "/set mo |", false),
|
||||
a -> assertCompletion(a, "/set mo newmode |", false, modesWithOptions),
|
||||
a -> assertCompletion(a, "/set mo newmode -|", false, options),
|
||||
a -> assertCompletion(a, "/set mo newmode -command |", false),
|
||||
a -> assertCompletion(a, "/set mo newmode normal |", false, options),
|
||||
|
||||
// /set prompt
|
||||
a -> assertCompletion(a, "/set pro|", false, "prompt "),
|
||||
a -> assertCompletion(a, "/set pro |", false, modes),
|
||||
|
||||
// /set start
|
||||
a -> assertCompletion(a, "/set st|", false, "start "),
|
||||
a -> assertCompletion(a, "/set st |", false, p1.toArray(new String[p1.size()])),
|
||||
|
||||
// /set truncation
|
||||
a -> assertCompletion(a, "/set tr|", false, "truncation "),
|
||||
a -> assertCompletion(a, "/set tr |", false, modes)
|
||||
);
|
||||
}
|
||||
|
||||
public void testRetain() throws IOException {
|
||||
List<String> p1 = listFiles(Paths.get(""));
|
||||
FileSystems.getDefault().getRootDirectories().forEach(s -> p1.add(s.toString()));
|
||||
Collections.sort(p1);
|
||||
|
||||
String[] modes = {"concise ", "normal ", "silent ", "verbose "};
|
||||
test(false, new String[] {"--no-startup"},
|
||||
a -> assertCompletion(a, "/ret|", false, "/retain "),
|
||||
a -> assertCompletion(a, "/retain |", false, "editor ", "feedback ", "mode ", "start "),
|
||||
|
||||
// /retain editor
|
||||
a -> assertCompletion(a, "/retain e|", false, "editor "),
|
||||
a -> assertCompletion(a, "/retain editor |", false, p1.toArray(new String[p1.size()])),
|
||||
|
||||
// /retain feedback
|
||||
a -> assertCompletion(a, "/retain fe|", false, "feedback "),
|
||||
a -> assertCompletion(a, "/retain fe |", false, modes),
|
||||
|
||||
// /retain mode
|
||||
a -> assertCompletion(a, "/retain mo|", false, "mode "),
|
||||
a -> assertCompletion(a, "/retain mo |", false, modes),
|
||||
|
||||
// /retain start
|
||||
a -> assertCompletion(a, "/retain st|", false, "start "),
|
||||
a -> assertCompletion(a, "/retain st |", false, p1.toArray(new String[p1.size()]))
|
||||
);
|
||||
}
|
||||
|
||||
private void createIfNeeded(Path file) throws IOException {
|
||||
if (!Files.exists(file))
|
||||
Files.createFile(file);
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user