8177076: jshell tool: usability of completion
authorjlahoda
Fri, 31 Mar 2017 10:46:37 +0200
changeset 44459 5224425af378
parent 44458 cf6145299178
child 44460 9adfc635dd7b
8177076: jshell tool: usability of completion Summary: Merging completion and documentation completion, assigning Shift-tab shortcut to fix actions. Reviewed-by: rfield
langtools/src/jdk.jshell/share/classes/jdk/internal/jshell/tool/ConsoleIOContext.java
langtools/src/jdk.jshell/share/classes/jdk/internal/jshell/tool/JShellTool.java
langtools/src/jdk.jshell/share/classes/jdk/internal/jshell/tool/resources/l10n.properties
langtools/src/jdk.jshell/share/classes/jdk/jshell/SourceCodeAnalysisImpl.java
langtools/test/jdk/jshell/MergedTabShiftTabTest.java
--- a/langtools/src/jdk.jshell/share/classes/jdk/internal/jshell/tool/ConsoleIOContext.java	Fri Mar 31 08:59:35 2017 +0200
+++ b/langtools/src/jdk.jshell/share/classes/jdk/internal/jshell/tool/ConsoleIOContext.java	Fri Mar 31 10:46:37 2017 +0200
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2015, 2016, Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2015, 2017, 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
@@ -33,7 +33,6 @@
 import java.io.InputStream;
 import java.io.InterruptedIOException;
 import java.io.PrintStream;
-import java.io.UncheckedIOException;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collection;
@@ -44,7 +43,7 @@
 import java.util.Locale;
 import java.util.Map;
 import java.util.Optional;
-import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.function.BooleanSupplier;
 import java.util.function.Function;
 import java.util.stream.Collectors;
 import java.util.stream.Stream;
@@ -59,10 +58,10 @@
 import jdk.internal.jline.console.KeyMap;
 import jdk.internal.jline.console.Operation;
 import jdk.internal.jline.console.UserInterruptException;
-import jdk.internal.jline.console.completer.Completer;
 import jdk.internal.jline.console.history.History;
 import jdk.internal.jline.console.history.MemoryHistory;
 import jdk.internal.jline.extra.EditingHistory;
+import jdk.internal.jline.internal.NonBlockingInputStream;
 import jdk.internal.jshell.tool.StopDetectingInputStream.State;
 import jdk.internal.misc.Signal;
 import jdk.internal.misc.Signal.Handler;
@@ -91,10 +90,14 @@
             term = new JShellUnixTerminal(input);
         }
         term.init();
-        AtomicBoolean allowSmart = new AtomicBoolean();
+        List<CompletionTask> completionTODO = new ArrayList<>();
         in = new ConsoleReader(cmdin, cmdout, term) {
             @Override public KeyMap getKeys() {
-                return new CheckCompletionKeyMap(super.getKeys(), allowSmart);
+                return new CheckCompletionKeyMap(super.getKeys(), completionTODO);
+            }
+            @Override
+            protected boolean complete() throws IOException {
+                return ConsoleIOContext.this.complete(completionTODO);
             }
         };
         in.setExpandEvents(false);
@@ -111,67 +114,7 @@
         });
         in.setBellEnabled(true);
         in.setCopyPasteDetection(true);
-        in.addCompleter(new Completer() {
-            @Override public int complete(String test, int cursor, List<CharSequence> result) {
-                int[] anchor = new int[] {-1};
-                List<Suggestion> suggestions;
-                if (prefix.isEmpty() && test.trim().startsWith("/")) {
-                    suggestions = repl.commandCompletionSuggestions(test, cursor, anchor);
-                } else {
-                    int prefixLength = prefix.length();
-                    suggestions = repl.analysis.completionSuggestions(prefix + test, cursor + prefixLength, anchor);
-                    anchor[0] -= prefixLength;
-                }
-                boolean smart = allowSmart.get() &&
-                                suggestions.stream()
-                                           .anyMatch(Suggestion::matchesType);
-
-                allowSmart.set(!allowSmart.get());
-
-                suggestions.stream()
-                           .filter(s -> !smart || s.matchesType())
-                           .map(Suggestion::continuation)
-                           .forEach(result::add);
-
-                boolean onlySmart = suggestions.stream()
-                                               .allMatch(Suggestion::matchesType);
-
-                if (smart && !onlySmart) {
-                    Optional<String> prefix =
-                            suggestions.stream()
-                                       .map(Suggestion::continuation)
-                                       .reduce(ConsoleIOContext::commonPrefix);
-
-                    String prefixStr = prefix.orElse("").substring(cursor - anchor[0]);
-                    try {
-                        in.putString(prefixStr);
-                        cursor += prefixStr.length();
-                    } catch (IOException ex) {
-                        throw new IllegalStateException(ex);
-                    }
-                    result.add(repl.messageFormat("jshell.console.see.more"));
-                    return cursor; //anchor should not be used.
-                }
-
-                if (result.isEmpty()) {
-                    try {
-                        //provide "empty completion" feedback
-                        //XXX: this only works correctly when there is only one Completer:
-                        in.beep();
-                    } catch (IOException ex) {
-                        throw new UncheckedIOException(ex);
-                    }
-                }
-
-                return anchor[0];
-            }
-        });
-        bind(DOCUMENTATION_SHORTCUT, (Runnable) () -> documentation(repl));
-        for (FixComputer computer : FIX_COMPUTERS) {
-            for (String shortcuts : SHORTCUT_FIXES) {
-                bind(shortcuts + computer.shortcut, (Runnable) () -> fixes(computer));
-            }
-        }
+        bind(FIXES_SHORTCUT, (Runnable) () -> fixes());
         try {
             Signal.handle(new Signal("CONT"), new Handler() {
                 @Override public void handle(Signal sig) {
@@ -248,136 +191,184 @@
         }
     }
 
-    private static final String DOCUMENTATION_SHORTCUT = "\033\133\132"; //Shift-TAB
-    private static final String[] SHORTCUT_FIXES = {
-        "\033\015", //Alt-Enter (Linux)
-        "\033\012", //Alt-Enter (Linux)
-        "\033\133\061\067\176", //F6/Alt-F1 (Mac)
-        "\u001BO3P" //Alt-F1 (Linux)
-    };
+    private static final String FIXES_SHORTCUT = "\033\133\132"; //Shift-TAB
 
-    private String lastDocumentationBuffer;
-    private int lastDocumentationCursor = (-1);
+    private static final String LINE_SEPARATOR = System.getProperty("line.separator");
+    private static final String LINE_SEPARATORS2 = LINE_SEPARATOR + LINE_SEPARATOR;
 
-    private void documentation(JShellTool repl) {
-        String buffer = in.getCursorBuffer().buffer.toString();
-        int cursor = in.getCursorBuffer().cursor;
-        boolean firstInvocation = !buffer.equals(lastDocumentationBuffer) || cursor != lastDocumentationCursor;
-        lastDocumentationBuffer = buffer;
-        lastDocumentationCursor = cursor;
-        List<String> doc;
-        String seeMore;
-        Terminal term = in.getTerminal();
-        if (prefix.isEmpty() && buffer.trim().startsWith("/")) {
-            doc = Arrays.asList(repl.commandDocumentation(buffer, cursor, firstInvocation));
-            seeMore = "jshell.console.see.help";
-        } else {
-            JavadocFormatter formatter = new JavadocFormatter(term.getWidth(),
-                                                              term.isAnsiSupported());
-            Function<Documentation, String> convertor;
-            if (firstInvocation) {
-                convertor = Documentation::signature;
-            } else {
-                convertor = d -> formatter.formatJavadoc(d.signature(), d.javadoc()) +
-                                 (d.javadoc() == null ? repl.messageFormat("jshell.console.no.javadoc")
-                                                      : "");
-            }
-            doc = repl.analysis.documentation(prefix + buffer, cursor + prefix.length(), !firstInvocation)
-                               .stream()
-                               .map(convertor)
-                               .collect(Collectors.toList());
-            seeMore = "jshell.console.see.javadoc";
-        }
-
+    @SuppressWarnings("fallthrough")
+    private boolean complete(List<CompletionTask> todo) {
+        //The completion has multiple states (invoked by subsequent presses of <tab>).
+        //On the first invocation in a given sequence, all steps are precomputed
+        //and placed into the todo list. The todo list is then followed on both the first
+        //and subsequent <tab> presses:
         try {
-            if (doc != null && !doc.isEmpty()) {
-                if (firstInvocation) {
-                    in.println();
-                    in.println(doc.stream().collect(Collectors.joining("\n")));
-                    in.println(repl.messageFormat(seeMore));
-                    in.redrawLine();
-                    in.flush();
+            String text = in.getCursorBuffer().toString();
+            int cursor = in.getCursorBuffer().cursor;
+            if (todo.isEmpty()) {
+                int[] anchor = new int[] {-1};
+                List<Suggestion> suggestions;
+                List<String> doc;
+                boolean command = prefix.isEmpty() && text.trim().startsWith("/");
+                if (command) {
+                    suggestions = repl.commandCompletionSuggestions(text, cursor, anchor);
+                    doc = repl.commandDocumentation(text, cursor, true);
                 } else {
-                    in.println();
-
-                    int height = term.getHeight();
-                    String lastNote = "";
+                    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)
+                                       .collect(Collectors.toList());
+                }
+                long smartCount = suggestions.stream().filter(Suggestion::matchesType).count();
+                boolean hasSmart = smartCount > 0 && smartCount <= in.getAutoprintThreshold();
+                boolean hasBoth = hasSmart &&
+                                  suggestions.stream()
+                                             .map(s -> s.matchesType())
+                                             .distinct()
+                                             .count() == 2;
+                boolean tooManyItems = suggestions.size() > in.getAutoprintThreshold();
+                CompletionTask ordinaryCompletion = new OrdinaryCompletionTask(suggestions, anchor[0], !command && !doc.isEmpty(), hasSmart);
+                CompletionTask allCompletion = new AllSuggestionsCompletionTask(suggestions, anchor[0]);
 
-                    PRINT_DOC: for (Iterator<String> docIt = doc.iterator(); docIt.hasNext(); ) {
-                        String currentDoc = docIt.next();
-                        String[] lines = currentDoc.split("\n");
-                        int firstLine = 0;
-
-                        PRINT_PAGE: while (true) {
-                            in.print(lastNote.replaceAll(".", " ") + ConsoleReader.RESET_LINE);
-
-                            int toPrint = height - 1;
-
-                            while (toPrint > 0 && firstLine < lines.length) {
-                                in.println(lines[firstLine++]);
-                                toPrint--;
-                            }
-
-                            if (firstLine >= lines.length) {
-                                break;
-                            }
-
-                            lastNote = repl.getResourceString("jshell.console.see.next.page");
-                            in.print(lastNote + ConsoleReader.RESET_LINE);
-                            in.flush();
-
-                            while (true) {
-                                int r = in.readCharacter();
+                //the main decission tree:
+                if (command) {
+                    CompletionTask shortDocumentation = new CommandSynopsisTask(doc);
+                    CompletionTask fullDocumentation = new CommandFullDocumentationTask(todo);
 
-                                switch (r) {
-                                    case ' ': continue PRINT_PAGE;
-                                    case 'q':
-                                    case 3:
-                                        break PRINT_DOC;
-                                    default:
-                                        in.beep();
-                                        break;
-                                }
-                            }
+                    if (!doc.isEmpty()) {
+                        if (tooManyItems) {
+                            todo.add(new NoopCompletionTask());
+                            todo.add(allCompletion);
+                        } else {
+                            todo.add(ordinaryCompletion);
                         }
+                        todo.add(shortDocumentation);
+                        todo.add(fullDocumentation);
+                    } else {
+                        todo.add(new NoSuchCommandCompletionTask());
+                    }
+                } else {
+                    if (doc.isEmpty()) {
+                        if (hasSmart) {
+                            todo.add(ordinaryCompletion);
+                        } else if (tooManyItems) {
+                            todo.add(new NoopCompletionTask());
+                        }
+                        if (!hasSmart || hasBoth) {
+                            todo.add(allCompletion);
+                        }
+                    } else {
+                        CompletionTask shortDocumentation = new ExpressionSignaturesTask(doc);
+                        CompletionTask fullDocumentation = new ExpressionJavadocTask(todo);
 
-                        if (docIt.hasNext()) {
-                            lastNote = repl.getResourceString("jshell.console.see.next.javadoc");
-                            in.print(lastNote + ConsoleReader.RESET_LINE);
-                            in.flush();
-
-                            while (true) {
-                                int r = in.readCharacter();
-
-                                switch (r) {
-                                    case ' ': continue PRINT_DOC;
-                                    case 'q':
-                                    case 3:
-                                        break PRINT_DOC;
-                                    default:
-                                        in.beep();
-                                        break;
-                                }
-                            }
+                        if (hasSmart) {
+                            todo.add(ordinaryCompletion);
+                        }
+                        todo.add(shortDocumentation);
+                        if (!hasSmart || hasBoth) {
+                            todo.add(allCompletion);
+                        }
+                        if (tooManyItems) {
+                            todo.add(todo.size() - 1, fullDocumentation);
+                        } else {
+                            todo.add(fullDocumentation);
                         }
                     }
-                    //clear the "press space" line:
-                    in.getCursorBuffer().buffer.replace(0, buffer.length(), lastNote);
-                    in.getCursorBuffer().cursor = 0;
-                    in.killLine();
-                    in.getCursorBuffer().buffer.append(buffer);
-                    in.getCursorBuffer().cursor = cursor;
-                    in.redrawLine();
-                    in.flush();
                 }
-            } else {
-                in.beep();
             }
+
+            boolean success = false;
+            boolean repaint = true;
+
+            OUTER: while (!todo.isEmpty()) {
+                CompletionTask.Result result = todo.remove(0).perform(text, cursor);
+
+                switch (result) {
+                    case CONTINUE:
+                        break;
+                    case SKIP_NOREPAINT:
+                        repaint = false;
+                    case SKIP:
+                        todo.clear();
+                        //intentional fall-through
+                    case FINISH:
+                        success = true;
+                        //intentional fall-through
+                    case NO_DATA:
+                        if (!todo.isEmpty()) {
+                            in.println();
+                            in.println(todo.get(0).description());
+                        }
+                        break OUTER;
+                }
+            }
+
+            if (repaint) {
+                in.redrawLine();
+                in.flush();
+            }
+
+            return success;
         } catch (IOException ex) {
             throw new IllegalStateException(ex);
         }
     }
 
+    private CompletionTask.Result doPrintFullDocumentation(List<CompletionTask> todo, List<String> doc, boolean command) {
+        if (doc != null && !doc.isEmpty()) {
+            Terminal term = in.getTerminal();
+            int pageHeight = term.getHeight() - NEEDED_LINES;
+            List<CompletionTask> thisTODO = new ArrayList<>();
+
+            for (Iterator<String> docIt = doc.iterator(); docIt.hasNext(); ) {
+                String currentDoc = docIt.next();
+                String[] lines = currentDoc.split("\n");
+                int firstLine = 0;
+
+                while (firstLine < lines.length) {
+                    boolean first = firstLine == 0;
+                    String[] thisPageLines =
+                            Arrays.copyOfRange(lines,
+                                               firstLine,
+                                               Math.min(firstLine + pageHeight, lines.length));
+
+                    thisTODO.add(new CompletionTask() {
+                        @Override
+                        public String description() {
+                            String key =  !first ? "jshell.console.see.next.page"
+                                                 : command ? "jshell.console.see.next.command.doc"
+                                                           : "jshell.console.see.next.javadoc";
+
+                            return repl.getResourceString(key);
+                        }
+
+                        @Override
+                        public Result perform(String text, int cursor) throws IOException {
+                            in.println();
+                            for (String line : thisPageLines) {
+                                in.println(line);
+                            }
+                            return Result.FINISH;
+                        }
+                    });
+
+                    firstLine += pageHeight;
+                }
+            }
+
+            todo.addAll(0, thisTODO);
+
+            return CompletionTask.Result.CONTINUE;
+        }
+
+        return CompletionTask.Result.FINISH;
+    }
+    //where:
+        private static final int NEEDED_LINES = 4;
+
     private static String commonPrefix(String str1, String str2) {
         for (int i = 0; i < str2.length(); i++) {
             if (!str1.startsWith(str2.substring(0, i + 1))) {
@@ -388,6 +379,262 @@
         return str2;
     }
 
+    private interface CompletionTask {
+        public String description();
+        public Result perform(String text, int cursor) throws IOException;
+
+        enum Result {
+            NO_DATA,
+            CONTINUE,
+            FINISH,
+            SKIP,
+            SKIP_NOREPAINT;
+        }
+    }
+
+    private final class NoopCompletionTask implements CompletionTask {
+
+        @Override
+        public String description() {
+            throw new UnsupportedOperationException("Should not get here.");
+        }
+
+        @Override
+        public Result perform(String text, int cursor) throws IOException {
+            return Result.FINISH;
+        }
+
+    }
+
+    private final class NoSuchCommandCompletionTask implements CompletionTask {
+
+        @Override
+        public String description() {
+            throw new UnsupportedOperationException("Should not get here.");
+        }
+
+        @Override
+        public Result perform(String text, int cursor) throws IOException {
+            in.println();
+            in.println(repl.getResourceString("jshell.console.no.such.command"));
+            in.println();
+            return Result.SKIP;
+        }
+
+    }
+
+    private final class OrdinaryCompletionTask implements CompletionTask {
+        private final List<Suggestion> suggestions;
+        private final int anchor;
+        private final boolean cont;
+        private final boolean smart;
+
+        public OrdinaryCompletionTask(List<Suggestion> suggestions,
+                                      int anchor,
+                                      boolean cont,
+                                      boolean smart) {
+            this.suggestions = suggestions;
+            this.anchor = anchor;
+            this.cont = cont;
+            this.smart = smart;
+        }
+
+        @Override
+        public String description() {
+            throw new UnsupportedOperationException("Should not get here.");
+        }
+
+        @Override
+        public Result perform(String text, int cursor) throws IOException {
+            List<CharSequence> toShow;
+
+            if (smart) {
+                toShow =
+                    suggestions.stream()
+                               .filter(Suggestion::matchesType)
+                               .map(Suggestion::continuation)
+                               .distinct()
+                               .collect(Collectors.toList());
+            } else {
+                toShow =
+                    suggestions.stream()
+                               .map(Suggestion::continuation)
+                               .distinct()
+                               .collect(Collectors.toList());
+            }
+
+            if (toShow.isEmpty()) {
+                return Result.CONTINUE;
+            }
+
+            Optional<String> prefix =
+                    suggestions.stream()
+                               .map(Suggestion::continuation)
+                               .reduce(ConsoleIOContext::commonPrefix);
+
+            String prefixStr = prefix.orElse("").substring(cursor - anchor);
+            in.putString(prefixStr);
+
+            boolean showItems = toShow.size() > 1 || smart;
+
+            if (showItems) {
+                in.println();
+                in.printColumns(toShow);
+            }
+
+            if (!prefixStr.isEmpty())
+                return showItems ? Result.SKIP : Result.SKIP_NOREPAINT;
+
+            return cont ? Result.CONTINUE : Result.FINISH;
+        }
+
+    }
+
+    private final class AllSuggestionsCompletionTask implements CompletionTask {
+        private final List<Suggestion> suggestions;
+        private final int anchor;
+
+        public AllSuggestionsCompletionTask(List<Suggestion> suggestions,
+                                            int anchor) {
+            this.suggestions = suggestions;
+            this.anchor = anchor;
+        }
+
+        @Override
+        public String description() {
+            if (suggestions.size() <= in.getAutoprintThreshold()) {
+                return repl.getResourceString("jshell.console.completion.all.completions");
+            } else {
+                return repl.messageFormat("jshell.console.completion.all.completions.number", suggestions.size());
+            }
+        }
+
+        @Override
+        public Result perform(String text, int cursor) throws IOException {
+            List<String> candidates =
+                    suggestions.stream()
+                               .map(Suggestion::continuation)
+                               .distinct()
+                               .collect(Collectors.toList());
+
+            Optional<String> prefix =
+                    candidates.stream()
+                              .reduce(ConsoleIOContext::commonPrefix);
+
+            String prefixStr = prefix.map(str -> str.substring(cursor - anchor)).orElse("");
+            in.putString(prefixStr);
+            if (candidates.size() > 1) {
+                in.println();
+                in.printColumns(candidates);
+            }
+            return suggestions.isEmpty() ? Result.NO_DATA : Result.FINISH;
+        }
+
+    }
+
+    private final class CommandSynopsisTask implements CompletionTask {
+
+        private final List<String> synopsis;
+
+        public CommandSynopsisTask(List<String> synposis) {
+            this.synopsis = synposis;
+        }
+
+        @Override
+        public String description() {
+            return repl.getResourceString("jshell.console.see.synopsis");
+        }
+
+        @Override
+        public Result perform(String text, int cursor) throws IOException {
+            try {
+                in.println();
+                in.println(synopsis.stream()
+                                   .map(l -> l.replaceAll("\n", LINE_SEPARATOR))
+                                   .collect(Collectors.joining(LINE_SEPARATORS2)));
+            } catch (IOException ex) {
+                throw new IllegalStateException(ex);
+            }
+            return Result.FINISH;
+        }
+
+    }
+
+    private final class CommandFullDocumentationTask implements CompletionTask {
+
+        private final List<CompletionTask> todo;
+
+        public CommandFullDocumentationTask(List<CompletionTask> todo) {
+            this.todo = todo;
+        }
+
+        @Override
+        public String description() {
+            return repl.getResourceString("jshell.console.see.full.documentation");
+        }
+
+        @Override
+        public Result perform(String text, int cursor) throws IOException {
+            List<String> fullDoc = repl.commandDocumentation(text, cursor, false);
+            return doPrintFullDocumentation(todo, fullDoc, true);
+        }
+
+    }
+
+    private final class ExpressionSignaturesTask implements CompletionTask {
+
+        private final List<String> doc;
+
+        public ExpressionSignaturesTask(List<String> doc) {
+            this.doc = doc;
+        }
+
+        @Override
+        public String description() {
+            throw new UnsupportedOperationException("Should not get here.");
+        }
+
+        @Override
+        public Result perform(String text, int cursor) throws IOException {
+            in.println();
+            in.println(repl.getResourceString("jshell.console.completion.current.signatures"));
+            in.println(doc.stream().collect(Collectors.joining(LINE_SEPARATOR)));
+            return Result.FINISH;
+        }
+
+    }
+
+    private final class ExpressionJavadocTask implements CompletionTask {
+
+        private final List<CompletionTask> todo;
+
+        public ExpressionJavadocTask(List<CompletionTask> todo) {
+            this.todo = todo;
+        }
+
+        @Override
+        public String description() {
+            return repl.getResourceString("jshell.console.see.documentation");
+        }
+
+        @Override
+        public Result perform(String text, int cursor) throws IOException {
+            //schedule showing javadoc:
+            Terminal term = in.getTerminal();
+            JavadocFormatter formatter = new JavadocFormatter(term.getWidth(),
+                                                              term.isAnsiSupported());
+            Function<Documentation, String> convertor = d -> formatter.formatJavadoc(d.signature(), d.javadoc()) +
+                             (d.javadoc() == null ? repl.messageFormat("jshell.console.no.javadoc")
+                                                  : "");
+            List<String> doc = repl.analysis.documentation(prefix + text, cursor + prefix.length(), true)
+                                            .stream()
+                                            .map(convertor)
+                                            .collect(Collectors.toList());
+            return doPrintFullDocumentation(todo, doc, false);
+        }
+
+    }
+
     @Override
     public boolean terminalEditorRunning() {
         Terminal terminal = in.getTerminal();
@@ -428,6 +675,50 @@
         history.fullHistoryReplace(source);
     }
 
+    private static final long ESCAPE_TIMEOUT = 100;
+
+    private void fixes() {
+        try {
+            int c = in.readCharacter();
+
+            if (c == (-1)) {
+                return ;
+            }
+
+            for (FixComputer computer : FIX_COMPUTERS) {
+                if (computer.shortcut == c) {
+                    fixes(computer);
+                    return ;
+                }
+            }
+
+            readOutRemainingEscape(c);
+
+            in.beep();
+            in.println();
+            in.println(repl.getResourceString("jshell.fix.wrong.shortcut"));
+            in.redrawLine();
+            in.flush();
+        } catch (IOException ex) {
+            ex.printStackTrace();
+        }
+    }
+
+    private void readOutRemainingEscape(int c) throws IOException {
+        if (c == '\033') {
+            //escape, consume waiting input:
+            InputStream inp = in.getInput();
+
+            if (inp instanceof NonBlockingInputStream) {
+                NonBlockingInputStream nbis = (NonBlockingInputStream) inp;
+
+                while (nbis.isNonBlockingEnabled() && nbis.peek(ESCAPE_TIMEOUT) > 0) {
+                    in.readCharacter();
+                }
+            }
+        }
+    }
+
     //compute possible options/Fixes based on the selected FixComputer, present them to the user,
     //and perform the selected one:
     private void fixes(FixComputer computer) {
@@ -493,7 +784,7 @@
                 in.flush();
             }
         } catch (IOException ex) {
-            ex.printStackTrace();
+            throw new IllegalStateException(ex);
         }
     }
 
@@ -766,7 +1057,7 @@
         public TestTerminal(StopDetectingInputStream input) throws Exception {
             super(true);
             setAnsiSupported(false);
-            setEchoEnabled(true);
+            setEchoEnabled(false);
             this.input = input;
         }
 
@@ -786,12 +1077,12 @@
     private static final class CheckCompletionKeyMap extends KeyMap {
 
         private final KeyMap del;
-        private final AtomicBoolean allowSmart;
+        private final List<CompletionTask> completionTODO;
 
-        public CheckCompletionKeyMap(KeyMap del, AtomicBoolean allowSmart) {
+        public CheckCompletionKeyMap(KeyMap del, List<CompletionTask> completionTODO) {
             super(del.getName(), del.isViKeyMap());
             this.del = del;
-            this.allowSmart = allowSmart;
+            this.completionTODO = completionTODO;
         }
 
         @Override
@@ -819,7 +1110,7 @@
             Object res = del.getBound(keySeq);
 
             if (res != Operation.COMPLETE) {
-                allowSmart.set(true);
+                completionTODO.clear();
             }
 
             return res;
@@ -835,4 +1126,4 @@
             return "check: " + del.toString();
         }
     }
-}
+    }
--- a/langtools/src/jdk.jshell/share/classes/jdk/internal/jshell/tool/JShellTool.java	Fri Mar 31 08:59:35 2017 +0200
+++ b/langtools/src/jdk.jshell/share/classes/jdk/internal/jshell/tool/JShellTool.java	Fri Mar 31 10:46:37 2017 +0200
@@ -1695,19 +1695,29 @@
         return commandCompletions.completionSuggestions(code, cursor, anchor);
     }
 
-    public String commandDocumentation(String code, int cursor, boolean shortDescription) {
+    public List<String> commandDocumentation(String code, int cursor, boolean shortDescription) {
         code = code.substring(0, cursor);
         int space = code.indexOf(' ');
-
-        if (space != (-1)) {
-            String cmd = code.substring(0, space);
-            Command command = commands.get(cmd);
-            if (command != null) {
-                return getResourceString(command.helpKey + (shortDescription ? ".summary" : ""));
+        String prefix = space != (-1) ? code.substring(0, space) : code;
+        List<String> result = new ArrayList<>();
+
+        List<Entry<String, Command>> toShow =
+                commands.entrySet()
+                        .stream()
+                        .filter(e -> e.getKey().startsWith(prefix))
+                        .filter(e -> e.getValue().kind.showInHelp)
+                        .sorted((e1, e2) -> e1.getKey().compareTo(e2.getKey()))
+                        .collect(Collectors.toList());
+
+        if (toShow.size() == 1) {
+            result.add(getResourceString(toShow.get(0).getValue().helpKey + (shortDescription ? ".summary" : "")));
+        } else {
+            for (Entry<String, Command> e : toShow) {
+                result.add(e.getKey() + "\n" +getResourceString(e.getValue().helpKey + (shortDescription ? ".summary" : "")));
             }
         }
 
-        return null;
+        return result;
     }
 
     // Attempt to stop currently running evaluation
--- a/langtools/src/jdk.jshell/share/classes/jdk/internal/jshell/tool/resources/l10n.properties	Fri Mar 31 08:59:35 2017 +0200
+++ b/langtools/src/jdk.jshell/share/classes/jdk/internal/jshell/tool/resources/l10n.properties	Fri Mar 31 10:46:37 2017 +0200
@@ -153,12 +153,17 @@
 
 jshell.err.corrupted.stored.startup = Corrupted stored startup, using default -- {0}
 
-jshell.console.see.more = <press tab to see more>
-jshell.console.see.javadoc = <press shift-tab again to see javadoc>
-jshell.console.see.help = <press shift-tab again to see detailed help>
-jshell.console.see.next.page = <press space for next page, Q to quit>
-jshell.console.see.next.javadoc = <press space for next javadoc, Q to quit>
-jshell.console.no.javadoc = <no javadoc found>
+jshell.console.see.synopsis = <press tab again to see synopsis>
+jshell.console.see.full.documentation = <press tab again to see full documentation>
+jshell.console.see.documentation = <press tab again to see documentation>
+jshell.console.see.next.page = <press tab again to see next page>
+jshell.console.see.next.javadoc = <press tab to see next documentation>
+jshell.console.see.next.command.doc = <press tab to see next command>
+jshell.console.no.such.command = No such command
+jshell.console.completion.current.signatures = Signatures:
+jshell.console.completion.all.completions.number = <press tab again to see all possible completions; total possible completions: {0}>
+jshell.console.completion.all.completions = <press tab again to see all possible completions>
+jshell.console.no.javadoc = <no documentation found>
 jshell.console.do.nothing = Do nothing
 jshell.console.choice = Choice: \
 
@@ -490,19 +495,16 @@
         After entering the first few letters of a Java identifier,\n\t\t\
         a jshell command, or, in some cases, a jshell command argument,\n\t\t\
         press the <tab> key to complete the input.\n\t\t\
-        If there is more than one completion, show possible completions.\n\n\
-Shift-<tab>\n\t\t\
-        After the name and open parenthesis of a method or constructor invocation,\n\t\t\
-        hold the <shift> key and press the <tab> to see a synopsis of all\n\t\t\
-        matching methods/constructors.\n\n\
-<fix-shortcut> v\n\t\t\
-        After a complete expression, press "<fix-shortcut> v" to introduce a new variable\n\t\t\
-        whose type is based on the type of the expression.\n\t\t\
-        The "<fix-shortcut>" is either Alt-F1 or Alt-Enter, depending on the platform.\n\n\
-<fix-shortcut> i\n\t\t\
-        After an unresolvable identifier, press "<fix-shortcut> i" and jshell will propose\n\t\t\
-        possible fully qualified names based on the content of the specified classpath.\n\t\t\
-        The "<fix-shortcut>" is either Alt-F1 or Alt-Enter, depending on the platform.
+        If there is more than one completion, then possible completions will be shown.\n\t\t\
+        Will show documentation if available and appropriate.\n\n\
+Shift-<tab> v\n\t\t\
+        After a complete expression, hold down <shift> while pressing <tab>,\n\t\t\
+        then release and press "v", the expression will be converted to\n\t\t\
+        a variable declaration whose type is based on the type of the expression.\n\t\t\
+Shift-<tab> i\n\t\t\
+        After an unresolvable identifier, hold down <shift> while pressing <tab>,\n\t\t\
+        then release and press "i", and jshell will propose possible imports\n\t\t\
+        which will resolve the identifier based on the content of the specified classpath.\n\t\t\
 
 help.context.summary = the evaluation context options for /env /reload and /reset
 help.context =\
@@ -924,3 +926,7 @@
 /set format silent errorpre '|  '    \n\
 /set format silent errorpost '%n'    \n\
 /set format silent display ''    \n
+
+jshell.fix.wrong.shortcut =\
+Invalid <fix> character.  Use "i" for auto-import or "v" for variable creation.  For more information see:\n\
+   /help shortcuts
--- a/langtools/src/jdk.jshell/share/classes/jdk/jshell/SourceCodeAnalysisImpl.java	Fri Mar 31 08:59:35 2017 +0200
+++ b/langtools/src/jdk.jshell/share/classes/jdk/jshell/SourceCodeAnalysisImpl.java	Fri Mar 31 10:46:37 2017 +0200
@@ -1277,12 +1277,16 @@
                  .allMatch(param -> param.getSimpleName().toString().startsWith("arg"));
     }
 
+    private static List<Path> availableSourcesOverride; //for tests
     private List<Path> availableSources;
 
     private List<Path> findSources() {
         if (availableSources != null) {
             return availableSources;
         }
+        if (availableSourcesOverride != null) {
+            return availableSources = availableSourcesOverride;
+        }
         List<Path> result = new ArrayList<>();
         Path home = Paths.get(System.getProperty("java.home"));
         Path srcZip = home.resolve("lib").resolve("src.zip");
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/langtools/test/jdk/jshell/MergedTabShiftTabTest.java	Fri Mar 31 10:46:37 2017 +0200
@@ -0,0 +1,565 @@
+/*
+ * Copyright (c) 2017, 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 8177076
+ * @modules
+ *     jdk.compiler/com.sun.tools.javac.api
+ *     jdk.compiler/com.sun.tools.javac.main
+ *     jdk.jshell/jdk.internal.jshell.tool.resources:open
+ *     jdk.jshell/jdk.jshell:open
+ * @library /tools/lib
+ * @build toolbox.ToolBox toolbox.JarTask toolbox.JavacTask
+ * @build Compiler
+ * @build MergedTabShiftTabTest
+ * @run testng MergedTabShiftTabTest
+ */
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.io.OutputStreamWriter;
+import java.io.PrintStream;
+import java.io.Writer;
+import java.lang.reflect.Field;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.text.MessageFormat;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.Locale;
+import java.util.ResourceBundle;
+import java.util.jar.JarEntry;
+import java.util.jar.JarOutputStream;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import jdk.jshell.JShell;
+import jdk.jshell.tool.JavaShellToolBuilder;
+import org.testng.annotations.Test;
+
+@Test
+public class MergedTabShiftTabTest {
+
+    public void testCommand() throws Exception {
+        doRunTest((inputSink, out) -> {
+            inputSink.write("1\n");
+            waitOutput(out, "\u0005");
+            inputSink.write("/\011");
+            waitOutput(out, ".*/edit.*/list.*\n\n" + Pattern.quote(getResource("jshell.console.see.synopsis")) + "\n\r\u0005/");
+            inputSink.write("\011");
+            waitOutput(out,   ".*\n/edit\n" + Pattern.quote(getResource("help.edit.summary")) +
+                            "\n.*\n/list\n" + Pattern.quote(getResource("help.list.summary")) +
+                            ".*\n\n" + Pattern.quote(getResource("jshell.console.see.full.documentation")) + "\n\r\u0005/");
+            inputSink.write("\011");
+            waitOutput(out,  "/!\n" +
+                            Pattern.quote(getResource("help.bang")) + "\n" +
+                            "\n" +
+                            Pattern.quote(getResource("jshell.console.see.next.command.doc")) + "\n" +
+                            "\r\u0005/");
+            inputSink.write("\011");
+            waitOutput(out,  "/-<n>\n" +
+                            Pattern.quote(getResource("help.previous")) + "\n" +
+                            "\n" +
+                            Pattern.quote(getResource("jshell.console.see.next.command.doc")) + "\n" +
+                            "\r\u0005/");
+
+            inputSink.write("lis\011");
+            waitOutput(out, "list $");
+
+            inputSink.write("\011");
+            waitOutput(out, ".*-all.*" +
+                            "\n\n" + Pattern.quote(getResource("jshell.console.see.synopsis")) + "\n\r\u0005/");
+            inputSink.write("\011");
+            waitOutput(out, Pattern.quote(getResource("help.list.summary")) + "\n\n" +
+                            Pattern.quote(getResource("jshell.console.see.full.documentation")) + "\n\r\u0005/list ");
+            inputSink.write("\011");
+            waitOutput(out, Pattern.quote(getResource("help.list").replaceAll("\t", "    ")));
+
+            inputSink.write("\u0003/env \011");
+            waitOutput(out, "\u0005/env -\n" +
+                            "-add-exports    -add-modules    -class-path     -module-path    \n" +
+                            "\r\u0005/env -");
+
+            inputSink.write("\011");
+            waitOutput(out, "-add-exports    -add-modules    -class-path     -module-path    \n" +
+                            "\n" +
+                            Pattern.quote(getResource("jshell.console.see.synopsis")) + "\n" +
+                            "\r\u0005/env -");
+
+            inputSink.write("\011");
+            waitOutput(out, Pattern.quote(getResource("help.env.summary")) + "\n\n" +
+                            Pattern.quote(getResource("jshell.console.see.full.documentation")) + "\n" +
+                            "\r\u0005/env -");
+
+            inputSink.write("\011");
+            waitOutput(out, Pattern.quote(getResource("help.env").replaceAll("\t", "    ")) + "\n" +
+                            "\r\u0005/env -");
+
+            inputSink.write("\011");
+            waitOutput(out, "-add-exports    -add-modules    -class-path     -module-path    \n" +
+                            "\n" +
+                            Pattern.quote(getResource("jshell.console.see.synopsis")) + "\n" +
+                            "\r\u0005/env -");
+
+            inputSink.write("\u0003/exit \011");
+            waitOutput(out, Pattern.quote(getResource("help.exit.summary")) + "\n\n" +
+                            Pattern.quote(getResource("jshell.console.see.full.documentation")) + "\n\r\u0005/exit ");
+            inputSink.write("\011");
+            waitOutput(out, Pattern.quote(getResource("help.exit")) + "\n" +
+                            "\r\u0005/exit ");
+            inputSink.write("\011");
+            waitOutput(out, Pattern.quote(getResource("help.exit.summary")) + "\n\n" +
+                            Pattern.quote(getResource("jshell.console.see.full.documentation")) + "\n\r\u0005/exit ");
+            inputSink.write("\u0003/doesnotexist\011");
+            waitOutput(out, "\u0005/doesnotexist\n" +
+                            Pattern.quote(getResource("jshell.console.no.such.command")) + "\n" +
+                            "\n" +
+                            "\r\u0005/doesnotexist");
+        });
+    }
+
+    public void testExpression() throws Exception {
+        Path classes = prepareZip();
+        doRunTest((inputSink, out) -> {
+            inputSink.write("/env -class-path " + classes.toString() + "\n");
+            waitOutput(out, Pattern.quote(getResource("jshell.msg.set.restore")) + "\n\u0005");
+            inputSink.write("import jshelltest.*;\n");
+            waitOutput(out, "\n\u0005");
+
+            //-> <tab>
+            inputSink.write("\011");
+            waitOutput(out, getMessage("jshell.console.completion.all.completions.number", "[0-9]+"));
+            inputSink.write("\011");
+            waitOutput(out, ".*String.*StringBuilder.*\n\r\u0005");
+
+            //new JShellTes<tab>
+            inputSink.write("new JShellTes\011");
+            waitOutput(out, "t\nJShellTest\\(      JShellTestAux\\(   \n\r\u0005new JShellTest");
+
+            //new JShellTest<tab>
+            inputSink.write("\011");
+            waitOutput(out, "JShellTest\\(      JShellTestAux\\(   \n" +
+                            "\n" +
+                            Pattern.quote(getResource("jshell.console.completion.current.signatures")) + "\n" +
+                            "jshelltest.JShellTest\n" +
+                            "\n" +
+                            Pattern.quote(getResource("jshell.console.see.documentation")) + "\n" +
+                            "\r\u0005new JShellTest");
+            inputSink.write("\011");
+            waitOutput(out, "jshelltest.JShellTest\n" +
+                            "JShellTest 0\n" +
+                            "\r\u0005new JShellTest");
+            inputSink.write("\011");
+            waitOutput(out, "JShellTest\\(      JShellTestAux\\(   \n" +
+                            "\n" +
+                            Pattern.quote(getResource("jshell.console.completion.current.signatures")) + "\n" +
+                            "jshelltest.JShellTest\n" +
+                            "\n" +
+                            Pattern.quote(getResource("jshell.console.see.documentation")) + "\n" +
+                            "\r\u0005new JShellTest");
+
+            //new JShellTest(<tab>
+            inputSink.write("(\011");
+            waitOutput(out, "\\(\n" +
+                            Pattern.quote(getResource("jshell.console.completion.current.signatures")) + "\n" +
+                            "JShellTest\\(String str\\)\n" +
+                            "JShellTest\\(String str, int i\\)\n" +
+                            "\n" +
+                            Pattern.quote(getResource("jshell.console.see.documentation")) + "\n" +
+                            "\r\u0005new JShellTest\\(");
+            inputSink.write("\011");
+            waitOutput(out, "JShellTest\\(String str\\)\n" +
+                            "JShellTest 1\n" +
+                            "1\n1\n1\n1\n1\n1\n1\n1\n1\n1\n1\n1\n1\n1\n1\n1\n1\n1\n" +
+                            "\n" +
+                            Pattern.quote(getResource("jshell.console.see.next.page")) + "\n" +
+                            "\r\u0005new JShellTest\\(");
+            inputSink.write("\011");
+            waitOutput(out, "1\n1\n1\n1\n1\n1\n1\n1\n1\n1\n1\n1\n" +
+                            "\n" +
+                            Pattern.quote(getResource("jshell.console.see.next.javadoc")) + "\n" +
+                            "\r\u0005new JShellTest\\(");
+            inputSink.write("\011");
+            waitOutput(out, "JShellTest\\(String str, int i\\)\n" +
+                            "JShellTest 2\n" +
+                            "\n" +
+                            getMessage("jshell.console.completion.all.completions.number", "[0-9]+") + "\n" +
+                            "\r\u0005new JShellTest\\(");
+            inputSink.write("\011");
+            waitOutput(out, ".*String.*StringBuilder.*\n\r\u0005new JShellTest\\(");
+
+            inputSink.write("\u0003String str = \"\";\nnew JShellTest(");
+            waitOutput(out, "\u0005new JShellTest\\(");
+
+            inputSink.write("\011");
+            waitOutput(out, "\n" +
+                            "str   \n" +
+                            "\n" +
+                            Pattern.quote(getResource("jshell.console.completion.current.signatures")) + "\n" +
+                            "JShellTest\\(String str\\)\n" +
+                            "JShellTest\\(String str, int i\\)\n" +
+                            "\n" +
+                            Pattern.quote(getResource("jshell.console.see.documentation")) + "\n" +
+                            "\r\u0005new JShellTest\\(");
+            inputSink.write("\011");
+            waitOutput(out, "JShellTest\\(String str\\)\n" +
+                            "JShellTest 1\n" +
+                            "1\n1\n1\n1\n1\n1\n1\n1\n1\n1\n1\n1\n1\n1\n1\n1\n1\n1\n" +
+                            "\n" +
+                            Pattern.quote(getResource("jshell.console.see.next.page")) + "\n" +
+                            "\r\u0005new JShellTest\\(");
+            inputSink.write("\011");
+            waitOutput(out, "1\n1\n1\n1\n1\n1\n1\n1\n1\n1\n1\n1\n" +
+                            "\n" +
+                            Pattern.quote(getResource("jshell.console.see.next.javadoc")) + "\n" +
+                            "\r\u0005new JShellTest\\(");
+            inputSink.write("\011");
+            waitOutput(out, "JShellTest\\(String str, int i\\)\n" +
+                            "JShellTest 2\n" +
+                            "\n" +
+                            getMessage("jshell.console.completion.all.completions.number", "[0-9]+") + "\n" +
+                            "\r\u0005new JShellTest\\(");
+            inputSink.write("\011");
+            waitOutput(out, ".*String.*StringBuilder.*\n\r\u0005new JShellTest\\(");
+
+            inputSink.write("\u0003JShellTest t = new JShellTest\011");
+            waitOutput(out, "\u0005JShellTest t = new JShellTest\n" +
+                            "JShellTest\\(   \n" +
+                            "\n" +
+                            Pattern.quote(getResource("jshell.console.completion.current.signatures")) + "\n" +
+                            "jshelltest.JShellTest\n" +
+                            "\n" +
+                            Pattern.quote(getResource("jshell.console.completion.all.completions")) + "\n" +
+                            "\r\u0005JShellTest t = new JShellTest");
+            inputSink.write("\011");
+            waitOutput(out, "JShellTest\\(      JShellTestAux\\(   \n" +
+                            "\n" +
+                            Pattern.quote(getResource("jshell.console.see.documentation")) + "\n" +
+                            "\r\u0005JShellTest t = new JShellTest");
+
+            inputSink.write("\u0003JShellTest t = new \011");
+            waitOutput(out, "\u0005JShellTest t = new \n" +
+                            "JShellTest\\(   \n" +
+                            "\n" +
+                            getMessage("jshell.console.completion.all.completions.number", "[0-9]+") + "\n" +
+                            "\r\u0005JShellTest t = new ");
+            inputSink.write("\011");
+            waitOutput(out, ".*String.*StringBuilder.*\n\r\u0005JShellTest t = new ");
+
+            inputSink.write("\u0003class JShelX{}\n");
+            inputSink.write("new JShel\011");
+            waitOutput(out, "\u0005new JShel\n" +
+                            "JShelX\\(\\)         JShellTest\\(      JShellTestAux\\(   \n" +
+                            "\r\u0005new JShel");
+
+            //no crash:
+            inputSink.write("\u0003new Stringbuil\011");
+            waitOutput(out, "\u0005new Stringbuil\u0007");
+        });
+    }
+
+    private void doRunTest(Test test) throws Exception {
+        PipeInputStream input = new PipeInputStream();
+        StringBuilder out = new StringBuilder();
+        PrintStream outS = new PrintStream(new OutputStream() {
+            @Override public void write(int b) throws IOException {
+                synchronized (out) {
+                    System.out.print((char) b);
+                    out.append((char) b);
+                    out.notifyAll();
+                }
+            }
+        });
+        Thread runner = new Thread(() -> {
+            try {
+                JavaShellToolBuilder.builder()
+                        .in(input, input)
+                        .out(outS)
+                        .err(outS)
+                        .promptCapture(true)
+                        .persistence(new HashMap<>())
+                        .locale(Locale.US)
+                        .run("--no-startup");
+            } catch (Exception ex) {
+                throw new IllegalStateException(ex);
+            }
+        });
+
+        Writer inputSink = new OutputStreamWriter(input.createOutput()) {
+            @Override
+            public void write(String str) throws IOException {
+                super.write(str);
+                flush();
+            }
+        };
+
+        runner.start();
+
+        try {
+            waitOutput(out, "\u0005");
+            test.test(inputSink, out);
+        } finally {
+            inputSink.write("\003\003/exit");
+
+            runner.join(1000);
+            if (runner.isAlive()) {
+                runner.stop();
+            }
+        }
+    }
+
+    interface Test {
+        public void test(Writer inputSink, StringBuilder out) throws Exception;
+    }
+
+    private Path prepareZip() {
+        String clazz1 =
+                "package jshelltest;\n" +
+                "/**JShellTest 0" +
+                " */\n" +
+                "public class JShellTest {\n" +
+                "    /**JShellTest 1\n" +
+                "     * <p>1 <p>1 <p>1 <p>1 <p>1 <p>1 <p>1 <p>1 <p>1 <p>1\n" +
+                "     * <p>1 <p>1 <p>1 <p>1 <p>1 <p>1 <p>1 <p>1 <p>1 <p>1\n" +
+                "     * <p>1 <p>1 <p>1 <p>1 <p>1 <p>1 <p>1 <p>1 <p>1 <p>1\n" +
+                "     */\n" +
+                "    public JShellTest(String str) {}\n" +
+                "    /**JShellTest 2" +
+                "     */\n" +
+                "    public JShellTest(String str, int i) {}\n" +
+                "}\n";
+
+        String clazz2 =
+                "package jshelltest;\n" +
+                "/**JShellTestAux 0" +
+                " */\n" +
+                "public class JShellTestAux {\n" +
+                "    /**JShellTest 1" +
+                "     */\n" +
+                "    public JShellTestAux(String str) { }\n" +
+                "    /**JShellTest 2" +
+                "     */\n" +
+                "    public JShellTestAux(String str, int i) { }\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(clazz1.getBytes());
+            out.putNextEntry(new JarEntry("jshelltest/JShellTestAux.java"));
+            out.write(clazz2.getBytes());
+        } catch (IOException ex) {
+            throw new IllegalStateException(ex);
+        }
+
+        compiler.compile(clazz1, clazz2);
+
+        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();
+
+    private final ResourceBundle resources;
+    {
+        resources = ResourceBundle.getBundle("jdk.internal.jshell.tool.resources.l10n", Locale.US, JShell.class.getModule());
+    }
+
+    private String getResource(String key) {
+        return resources.getString(key);
+    }
+
+    private String getMessage(String key, Object... args) {
+        return MessageFormat.format(resources.getString(key), args);
+    }
+
+    private static final long TIMEOUT;
+
+    static {
+        long factor;
+
+        try {
+            factor = (long) Double.parseDouble(System.getProperty("test.timeout.factor", "1"));
+        } catch (NumberFormatException ex) {
+            factor = 1;
+        }
+        TIMEOUT = 60_000 * factor;
+    }
+
+    private void waitOutput(StringBuilder out, String expected) {
+        expected = expected.replaceAll("\n", System.getProperty("line.separator"));
+        Pattern expectedPattern = Pattern.compile(expected, Pattern.DOTALL);
+        synchronized (out) {
+            long s = System.currentTimeMillis();
+
+            while (true) {
+                Matcher m = expectedPattern.matcher(out);
+                if (m.find()) {
+                    out.delete(0, m.end() + 1);
+                    return ;
+                }
+                long e =  System.currentTimeMillis();
+                if ((e - s) > TIMEOUT) {
+                    throw new IllegalStateException("Timeout waiting for: " + quote(expected) + ", actual output so far: " + quote(out.toString()));
+                }
+                try {
+                    out.wait(TIMEOUT);
+                } catch (InterruptedException ex) {
+                    ex.printStackTrace();
+                }
+            }
+        }
+    }
+
+    private String quote(String original) {
+        StringBuilder output = new StringBuilder();
+
+        for (char c : original.toCharArray()) {
+            if (c < 32) {
+                output.append(String.format("\\u%04X", (int) c));
+            } else {
+                output.append(c);
+            }
+        }
+
+        return output.toString();
+    }
+
+    private static class PipeInputStream extends InputStream {
+
+        private static final int INITIAL_SIZE = 128;
+        private int[] buffer = new int[INITIAL_SIZE];
+        private int start;
+        private int end;
+        private boolean closed;
+
+        @Override
+        public synchronized int read() throws IOException {
+            if (start == end && !closed) {
+                inputNeeded();
+            }
+            while (start == end) {
+                if (closed) {
+                    return -1;
+                }
+                try {
+                    wait();
+                } catch (InterruptedException ex) {
+                    //ignore
+                }
+            }
+            try {
+                return buffer[start];
+            } finally {
+                start = (start + 1) % buffer.length;
+            }
+        }
+
+        @Override
+        public synchronized int read(byte[] b, int off, int len) throws IOException {
+            if (b == null) {
+                throw new NullPointerException();
+            } else if (off < 0 || len < 0 || len > b.length - off) {
+                throw new IndexOutOfBoundsException();
+            } else if (len == 0) {
+                return 0;
+            }
+
+            int c = read();
+            if (c == -1) {
+                return -1;
+            }
+            b[off] = (byte)c;
+
+            int totalRead = 1;
+            while (totalRead < len && start != end) {
+                int r = read();
+                if (r == (-1))
+                    break;
+                b[off + totalRead++] = (byte) r;
+            }
+            return totalRead;
+        }
+
+        protected void inputNeeded() throws IOException {}
+
+        private synchronized void write(int b) {
+            if (closed) {
+                throw new IllegalStateException("Already closed.");
+            }
+            int newEnd = (end + 1) % buffer.length;
+            if (newEnd == start) {
+                //overflow:
+                int[] newBuffer = new int[buffer.length * 2];
+                int rightPart = (end > start ? end : buffer.length) - start;
+                int leftPart = end > start ? 0 : start - 1;
+                System.arraycopy(buffer, start, newBuffer, 0, rightPart);
+                System.arraycopy(buffer, 0, newBuffer, rightPart, leftPart);
+                buffer = newBuffer;
+                start = 0;
+                end = rightPart + leftPart;
+                newEnd = end + 1;
+            }
+            buffer[end] = b;
+            end = newEnd;
+            notifyAll();
+        }
+
+        @Override
+        public synchronized void close() {
+            closed = true;
+            notifyAll();
+        }
+
+        public OutputStream createOutput() {
+            return new OutputStream() {
+                @Override public void write(int b) throws IOException {
+                    PipeInputStream.this.write(b);
+                }
+                @Override
+                public void write(byte[] b, int off, int len) throws IOException {
+                    for (int i = 0 ; i < len ; i++) {
+                        write(Byte.toUnsignedInt(b[off + i]));
+                    }
+                }
+                @Override
+                public void close() throws IOException {
+                    PipeInputStream.this.close();
+                }
+            };
+        }
+
+    }
+
+}