Skip to content

Commit

Permalink
8164825: jshell tool: Completion for subcommand
Browse files Browse the repository at this point in the history
Reviewed-by: jlahoda
  • Loading branch information
bitterfox committed Sep 1, 2016
1 parent 7dceb3e commit 11de22e
Show file tree
Hide file tree
Showing 4 changed files with 262 additions and 32 deletions.
Original file line number Diff line number Diff line change
@@ -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());
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -35,9 +35,14 @@
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
Expand Down Expand Up @@ -146,6 +151,17 @@ public void markModesReadOnly() {
.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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,7 @@
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.
Expand Down Expand Up @@ -909,6 +910,7 @@ public Command(String command, String helpKey, Function<String,Boolean> run, Com

interface CompletionProvider {
List<Suggestion> completionSuggestions(String input, int cursor, int[] anchor);

}

enum CommandKind {
Expand Down Expand Up @@ -953,14 +955,31 @@ public List<Suggestion> completionSuggestions(String input, int cursor, int[] an

}

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('/');
Expand Down Expand Up @@ -1037,6 +1056,31 @@ private static CompletionProvider reloadCompletion() {
};
}

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() {
Expand Down Expand Up @@ -1123,10 +1167,26 @@ Stream<TypeDeclSnippet> allTypeSnippets() {
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),
Expand All @@ -1151,36 +1211,18 @@ Stream<TypeDeclSnippet> allTypeSnippets() {
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);
}

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();
}
}
private ContinuousCompletionProvider commandCompletions;

return result.sorted((s1, s2) -> s1.continuation().compareTo(s2.continuation()))
.collect(Collectors.toList());
public List<Suggestion> commandCompletionSuggestions(String code, int cursor, int[] anchor) {
return commandCompletions.completionSuggestions(code, cursor, anchor);
}

public String commandDocumentation(String code, int cursor) {
Expand Down Expand Up @@ -2484,7 +2526,7 @@ static class SnippetInfo {
}
}

private static class ArgSuggestion implements Suggestion {
static class ArgSuggestion implements Suggestion {

private final String continuation;

Expand Down
77 changes: 76 additions & 1 deletion langtools/test/jdk/jshell/CommandCompletionTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -40,6 +40,7 @@
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;
Expand Down Expand Up @@ -148,6 +149,80 @@ public void testUserHome() throws IOException {
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);
Expand Down

0 comments on commit 11de22e

Please sign in to comment.