8164825: jshell tool: Completion for subcommand
authorshinyafox
Thu, 01 Sep 2016 11:07:00 +0900
changeset 40765 6f9556cf4404
parent 40764 29ded021f809
child 40766 5e7d12c4fe70
8164825: jshell tool: Completion for subcommand Reviewed-by: jlahoda
langtools/src/jdk.jshell/share/classes/jdk/internal/jshell/tool/ContinuousCompletionProvider.java
langtools/src/jdk.jshell/share/classes/jdk/internal/jshell/tool/Feedback.java
langtools/src/jdk.jshell/share/classes/jdk/internal/jshell/tool/JShellTool.java
langtools/test/jdk/jshell/CommandCompletionTest.java
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/langtools/src/jdk.jshell/share/classes/jdk/internal/jshell/tool/ContinuousCompletionProvider.java	Thu Sep 01 11:07:00 2016 +0900
@@ -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());
+    }
+
+}
--- a/langtools/src/jdk.jshell/share/classes/jdk/internal/jshell/tool/Feedback.java	Wed Aug 31 10:35:51 2016 -0700
+++ b/langtools/src/jdk.jshell/share/classes/jdk/internal/jshell/tool/Feedback.java	Thu Sep 01 11:07:00 2016 +0900
@@ -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
@@ -146,6 +151,17 @@
                 .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);
--- a/langtools/src/jdk.jshell/share/classes/jdk/internal/jshell/tool/JShellTool.java	Wed Aug 31 10:35:51 2016 -0700
+++ b/langtools/src/jdk.jshell/share/classes/jdk/internal/jshell/tool/JShellTool.java	Thu Sep 01 11:07:00 2016 +0900
@@ -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.
@@ -909,6 +910,7 @@
 
     interface CompletionProvider {
         List<Suggestion> completionSuggestions(String input, int cursor, int[] anchor);
+
     }
 
     enum CommandKind {
@@ -953,14 +955,31 @@
 
     }
 
-    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 @@
         };
     }
 
+    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 @@
                 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 @@
         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 @@
         }
     }
 
-    private static class ArgSuggestion implements Suggestion {
+    static class ArgSuggestion implements Suggestion {
 
         private final String continuation;
 
--- a/langtools/test/jdk/jshell/CommandCompletionTest.java	Wed Aug 31 10:35:51 2016 -0700
+++ b/langtools/test/jdk/jshell/CommandCompletionTest.java	Thu Sep 01 11:07:00 2016 +0900
@@ -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.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 @@
         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);