8177076: jshell tool: usability of completion
Summary: Merging completion and documentation completion, assigning Shift-tab shortcut to fix actions.
Reviewed-by: rfield
--- 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();
+ }
+ };
+ }
+
+ }
+
+}