| /* |
| * Copyright (c) 2015, 2018, Oracle and/or its affiliates. All rights reserved. |
| * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. |
| * |
| * This code is free software; you can redistribute it and/or modify it |
| * under the terms of the GNU General Public License version 2 only, as |
| * published by the Free Software Foundation. Oracle designates this |
| * particular file as subject to the "Classpath" exception as provided |
| * by Oracle in the LICENSE file that accompanied this code. |
| * |
| * This code is distributed in the hope that it will be useful, but WITHOUT |
| * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or |
| * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License |
| * version 2 for more details (a copy is included in the LICENSE file that |
| * accompanied this code). |
| * |
| * You should have received a copy of the GNU General Public License version |
| * 2 along with this work; if not, write to the Free Software Foundation, |
| * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. |
| * |
| * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA |
| * or visit www.oracle.com if you need additional information or have any |
| * questions. |
| */ |
| |
| package jdk.internal.jshell.tool; |
| |
| |
| import jdk.jshell.SourceCodeAnalysis.Documentation; |
| import jdk.jshell.SourceCodeAnalysis.QualifiedNames; |
| import jdk.jshell.SourceCodeAnalysis.Suggestion; |
| |
| import java.io.IOException; |
| import java.io.InputStream; |
| import java.io.InterruptedIOException; |
| import java.io.OutputStream; |
| import java.io.PrintStream; |
| import java.nio.charset.Charset; |
| import java.time.Instant; |
| import java.util.ArrayList; |
| import java.util.Arrays; |
| import java.util.Collection; |
| import java.util.Collections; |
| import java.util.HashMap; |
| import java.util.Iterator; |
| import java.util.List; |
| import java.util.ListIterator; |
| import java.util.Map; |
| import java.util.Optional; |
| import java.util.function.Function; |
| import java.util.stream.Collectors; |
| import java.util.stream.Stream; |
| import java.util.stream.StreamSupport; |
| |
| import jdk.internal.shellsupport.doc.JavadocFormatter; |
| import jdk.internal.jshell.tool.StopDetectingInputStream.State; |
| import jdk.internal.misc.Signal; |
| import jdk.internal.misc.Signal.Handler; |
| import jdk.internal.org.jline.keymap.KeyMap; |
| import jdk.internal.org.jline.reader.Binding; |
| import jdk.internal.org.jline.reader.EOFError; |
| import jdk.internal.org.jline.reader.EndOfFileException; |
| import jdk.internal.org.jline.reader.History; |
| import jdk.internal.org.jline.reader.LineReader; |
| import jdk.internal.org.jline.reader.LineReader.Option; |
| import jdk.internal.org.jline.reader.Parser; |
| import jdk.internal.org.jline.reader.UserInterruptException; |
| import jdk.internal.org.jline.reader.Widget; |
| import jdk.internal.org.jline.reader.impl.LineReaderImpl; |
| import jdk.internal.org.jline.reader.impl.completer.ArgumentCompleter.ArgumentLine; |
| import jdk.internal.org.jline.reader.impl.history.DefaultHistory; |
| import jdk.internal.org.jline.terminal.impl.LineDisciplineTerminal; |
| import jdk.internal.org.jline.terminal.Attributes; |
| import jdk.internal.org.jline.terminal.Attributes.ControlChar; |
| import jdk.internal.org.jline.terminal.Attributes.LocalFlag; |
| import jdk.internal.org.jline.terminal.Size; |
| import jdk.internal.org.jline.terminal.Terminal; |
| import jdk.internal.org.jline.terminal.TerminalBuilder; |
| import jdk.internal.org.jline.utils.Display; |
| import jdk.internal.org.jline.utils.NonBlocking; |
| import jdk.internal.org.jline.utils.NonBlockingInputStreamImpl; |
| import jdk.internal.org.jline.utils.NonBlockingReader; |
| import jdk.jshell.ExpressionSnippet; |
| import jdk.jshell.Snippet; |
| import jdk.jshell.Snippet.SubKind; |
| import jdk.jshell.SourceCodeAnalysis.CompletionInfo; |
| import jdk.jshell.VarSnippet; |
| |
| class ConsoleIOContext extends IOContext { |
| |
| private static final String HISTORY_LINE_PREFIX = "HISTORY_LINE_"; |
| |
| final boolean allowIncompleteInputs; |
| final JShellTool repl; |
| final StopDetectingInputStream input; |
| final Attributes originalAttributes; |
| final LineReaderImpl in; |
| final History userInputHistory = new DefaultHistory(); |
| final Instant historyLoad; |
| |
| String prefix = ""; |
| |
| ConsoleIOContext(JShellTool repl, InputStream cmdin, PrintStream cmdout) throws Exception { |
| this.allowIncompleteInputs = Boolean.getBoolean("jshell.test.allow.incomplete.inputs"); |
| this.repl = repl; |
| Map<String, Object> variables = new HashMap<>(); |
| this.input = new StopDetectingInputStream(() -> repl.stop(), |
| ex -> repl.hard("Error on input: %s", ex)); |
| InputStream nonBlockingInput = new NonBlockingInputStreamImpl(null, input) { |
| @Override |
| public int readBuffered(byte[] b) throws IOException { |
| return input.read(b); |
| } |
| }; |
| Terminal terminal; |
| if (System.getProperty("test.jdk") != null) { |
| terminal = new TestTerminal(nonBlockingInput, cmdout); |
| input.setInputStream(cmdin); |
| } else { |
| terminal = TerminalBuilder.builder().inputStreamWrapper(in -> { |
| input.setInputStream(in); |
| return nonBlockingInput; |
| }).build(); |
| } |
| originalAttributes = terminal.getAttributes(); |
| Attributes noIntr = new Attributes(originalAttributes); |
| noIntr.setControlChar(ControlChar.VINTR, 0); |
| terminal.setAttributes(noIntr); |
| terminal.enterRawMode(); |
| LineReaderImpl reader = new LineReaderImpl(terminal, "jshell", variables) { |
| { |
| //jline can handle the CONT signal on its own, but (currently) requires sun.misc for it |
| try { |
| Signal.handle(new Signal("CONT"), new Handler() { |
| @Override public void handle(Signal sig) { |
| try { |
| handleSignal(jdk.internal.org.jline.terminal.Terminal.Signal.CONT); |
| } catch (Exception ex) { |
| ex.printStackTrace(); |
| } |
| } |
| }); |
| } catch (IllegalArgumentException ignored) { |
| //the CONT signal does not exist on this platform |
| } |
| } |
| CompletionState completionState = new CompletionState(); |
| @Override |
| protected boolean doComplete(CompletionType lst, boolean useMenu, boolean prefix) { |
| return ConsoleIOContext.this.complete(completionState); |
| } |
| @Override |
| public Binding readBinding(KeyMap<Binding> keys, KeyMap<Binding> local) { |
| completionState.actionCount++; |
| return super.readBinding(keys, local); |
| } |
| }; |
| |
| reader.setOpt(Option.DISABLE_EVENT_EXPANSION); |
| |
| reader.setParser((line, cursor, context) -> { |
| if (!allowIncompleteInputs && !repl.isComplete(line)) { |
| throw new EOFError(cursor, cursor, line); |
| } |
| return new ArgumentLine(line, cursor); |
| }); |
| |
| reader.getKeyMaps().get(LineReader.MAIN) |
| .bind((Widget) () -> fixes(), FIXES_SHORTCUT); |
| reader.getKeyMaps().get(LineReader.MAIN) |
| .bind((Widget) () -> { throw new UserInterruptException(""); }, "\003"); |
| |
| List<String> loadHistory = new ArrayList<>(); |
| Stream.of(repl.prefs.keys()) |
| .filter(key -> key.startsWith(HISTORY_LINE_PREFIX)) |
| .sorted() |
| .map(key -> repl.prefs.get(key)) |
| .forEach(loadHistory::add); |
| |
| for (ListIterator<String> it = loadHistory.listIterator(); it.hasNext(); ) { |
| String current = it.next(); |
| |
| int trailingBackSlashes = countTrailintBackslashes(current); |
| boolean continuation = trailingBackSlashes % 2 != 0; |
| current = current.substring(0, current.length() - trailingBackSlashes / 2 - (continuation ? 1 : 0)); |
| if (continuation && it.hasNext()) { |
| String next = it.next(); |
| it.remove(); |
| it.previous(); |
| current += "\n" + next; |
| } |
| |
| it.set(current); |
| } |
| |
| historyLoad = Instant.MIN; |
| loadHistory.forEach(line -> reader.getHistory().add(historyLoad, line)); |
| |
| in = reader; |
| } |
| |
| @Override |
| public String readLine(String firstLinePrompt, String continuationPrompt, |
| boolean firstLine, String prefix) throws IOException, InputInterruptedException { |
| assert firstLine || allowIncompleteInputs; |
| this.prefix = prefix; |
| try { |
| in.setVariable(LineReader.SECONDARY_PROMPT_PATTERN, continuationPrompt); |
| return in.readLine(firstLinePrompt); |
| } catch (UserInterruptException ex) { |
| throw (InputInterruptedException) new InputInterruptedException().initCause(ex); |
| } catch (EndOfFileException ex) { |
| return null; |
| } |
| } |
| |
| @Override |
| public boolean interactiveOutput() { |
| return true; |
| } |
| |
| @Override |
| public Iterable<String> history(boolean currentSession) { |
| return StreamSupport.stream(getHistory().spliterator(), false) |
| .filter(entry -> !currentSession || !historyLoad.equals(entry.time())) |
| .map(entry -> entry.line()) |
| .collect(Collectors.toList()); |
| } |
| |
| @Override |
| public void close() throws IOException { |
| //save history: |
| for (String key : repl.prefs.keys()) { |
| if (key.startsWith(HISTORY_LINE_PREFIX)) { |
| repl.prefs.remove(key); |
| } |
| } |
| Collection<String> savedHistory = |
| StreamSupport.stream(in.getHistory().spliterator(), false) |
| .map(History.Entry::line) |
| .flatMap(this::toSplitEntries) |
| .collect(Collectors.toList()); |
| if (!savedHistory.isEmpty()) { |
| int len = (int) Math.ceil(Math.log10(savedHistory.size()+1)); |
| String format = HISTORY_LINE_PREFIX + "%0" + len + "d"; |
| int index = 0; |
| for (String historyLine : savedHistory) { |
| repl.prefs.put(String.format(format, index++), historyLine); |
| } |
| } |
| repl.prefs.flush(); |
| try { |
| in.getTerminal().setAttributes(originalAttributes); |
| in.getTerminal().close(); |
| } catch (Exception ex) { |
| throw new IOException(ex); |
| } |
| input.shutdown(); |
| } |
| |
| private Stream<String> toSplitEntries(String entry) { |
| String[] lines = entry.split("\n"); |
| List<String> result = new ArrayList<>(); |
| |
| for (int i = 0; i < lines.length; i++) { |
| StringBuilder historyLine = new StringBuilder(lines[i]); |
| int trailingBackSlashes = countTrailintBackslashes(historyLine); |
| for (int j = 0; j < trailingBackSlashes; j++) { |
| historyLine.append("\\"); |
| } |
| if (i + 1 < lines.length) { |
| historyLine.append("\\"); |
| } |
| result.add(historyLine.toString()); |
| } |
| |
| return result.stream(); |
| } |
| |
| private int countTrailintBackslashes(CharSequence text) { |
| int count = 0; |
| |
| for (int i = text.length() - 1; i >= 0; i--) { |
| if (text.charAt(i) == '\\') { |
| count++; |
| } else { |
| break; |
| } |
| } |
| |
| return count; |
| } |
| |
| private static final String FIXES_SHORTCUT = "\033\133\132"; //Shift-TAB |
| |
| private static final String LINE_SEPARATOR = System.getProperty("line.separator"); |
| private static final String LINE_SEPARATORS2 = LINE_SEPARATOR + LINE_SEPARATOR; |
| |
| /*XXX:*/private static final int AUTOPRINT_THRESHOLD = 100; |
| @SuppressWarnings("fallthrough") |
| private boolean complete(CompletionState completionState) { |
| //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 (completionState.todo). The todo list is |
| //then followed on both the first and subsequent completion invocations: |
| try { |
| String text = in.getBuffer().toString(); |
| int cursor = in.getBuffer().cursor(); |
| |
| List<CompletionTask> todo = completionState.todo; |
| |
| if (todo.isEmpty() || completionState.actionCount != 1) { |
| ConsoleIOContextTestSupport.willComputeCompletion(); |
| int[] anchor = new int[] {-1}; |
| List<Suggestion> suggestions; |
| List<String> doc; |
| boolean command = prefix.isEmpty() && text.startsWith("/"); |
| if (command) { |
| suggestions = repl.commandCompletionSuggestions(text, cursor, anchor); |
| doc = repl.commandDocumentation(text, cursor, true); |
| } else { |
| 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()*/AUTOPRINT_THRESHOLD; |
| boolean hasBoth = hasSmart && |
| suggestions.stream() |
| .map(s -> s.matchesType()) |
| .distinct() |
| .count() == 2; |
| boolean tooManyItems = suggestions.size() > /*in.getAutoprintThreshold()*/AUTOPRINT_THRESHOLD; |
| CompletionTask ordinaryCompletion = |
| new OrdinaryCompletionTask(suggestions, |
| anchor[0], |
| !command && !doc.isEmpty(), |
| hasBoth); |
| CompletionTask allCompletion = new AllSuggestionsCompletionTask(suggestions, anchor[0]); |
| |
| todo = new ArrayList<>(); |
| |
| //the main decission tree: |
| if (command) { |
| CompletionTask shortDocumentation = new CommandSynopsisTask(doc); |
| CompletionTask fullDocumentation = new CommandFullDocumentationTask(todo); |
| |
| 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 (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); |
| } |
| } |
| } |
| } |
| |
| 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.getTerminal().writer().println(); |
| in.getTerminal().writer().println(todo.get(0).description()); |
| } |
| break OUTER; |
| } |
| } |
| |
| completionState.actionCount = 0; |
| completionState.todo = todo; |
| |
| 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.getTerminal().writer().println(); |
| for (String line : thisPageLines) { |
| in.getTerminal().writer().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))) { |
| return str2.substring(0, i); |
| } |
| } |
| |
| 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.getTerminal().writer().println(); |
| in.getTerminal().writer().println(repl.getResourceString("jshell.console.no.such.command")); |
| in.getTerminal().writer().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 showSmart; |
| |
| public OrdinaryCompletionTask(List<Suggestion> suggestions, |
| int anchor, |
| boolean cont, |
| boolean showSmart) { |
| this.suggestions = suggestions; |
| this.anchor = anchor; |
| this.cont = cont; |
| this.showSmart = showSmart; |
| } |
| |
| @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 (showSmart) { |
| 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 || showSmart; |
| |
| if (showItems) { |
| in.getTerminal().writer().println(); |
| printColumns(toShow); |
| } |
| |
| if (!prefixStr.isEmpty()) |
| return showItems ? Result.FINISH : 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()*/AUTOPRINT_THRESHOLD) { |
| 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.getTerminal().writer().println(); |
| printColumns(candidates); |
| } |
| return suggestions.isEmpty() ? Result.NO_DATA : Result.FINISH; |
| } |
| |
| } |
| |
| private void printColumns(List<? extends CharSequence> candidates) { |
| if (candidates.isEmpty()) return ; |
| int size = candidates.stream().mapToInt(CharSequence::length).max().getAsInt() + 3; |
| int columns = in.getTerminal().getWidth() / size; |
| int c = 0; |
| for (CharSequence cand : candidates) { |
| in.getTerminal().writer().print(cand); |
| for (int s = cand.length(); s < size; s++) { |
| in.getTerminal().writer().print(" "); |
| } |
| if (++c == columns) { |
| in.getTerminal().writer().println(); |
| c = 0; |
| } |
| } |
| if (c != 0) { |
| in.getTerminal().writer().println(); |
| } |
| } |
| |
| 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.getTerminal().writer().println(); |
| in.getTerminal().writer().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.getTerminal().writer().println(); |
| in.getTerminal().writer().println(repl.getResourceString("jshell.console.completion.current.signatures")); |
| in.getTerminal().writer().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(), |
| true); |
| 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(); |
| return !terminal.getAttributes().getLocalFlag(LocalFlag.ICANON); |
| } |
| |
| @Override |
| public void suspend() { |
| } |
| |
| @Override |
| public void resume() { |
| } |
| |
| @Override |
| public void beforeUserCode() { |
| synchronized (this) { |
| inputBytes = null; |
| } |
| input.setState(State.BUFFER); |
| } |
| |
| @Override |
| public void afterUserCode() { |
| input.setState(State.WAIT); |
| } |
| |
| @Override |
| public void replaceLastHistoryEntry(String source) { |
| var it = in.getHistory().iterator(); |
| while (it.hasNext()) { |
| it.next(); |
| } |
| it.remove(); |
| in.getHistory().add(source); |
| } |
| |
| private static final long ESCAPE_TIMEOUT = 100; |
| |
| private boolean fixes() { |
| try { |
| int c = in.getTerminal().reader().read(); |
| |
| if (c == (-1)) { |
| return true; //TODO: true or false??? |
| } |
| |
| for (FixComputer computer : FIX_COMPUTERS) { |
| if (computer.shortcut == c) { |
| fixes(computer); |
| return true; //TODO: true of false??? |
| } |
| } |
| |
| readOutRemainingEscape(c); |
| |
| in.beep(); |
| in.getTerminal().writer().println(); |
| in.getTerminal().writer().println(repl.getResourceString("jshell.fix.wrong.shortcut")); |
| in.redrawLine(); |
| in.flush(); |
| } catch (IOException ex) { |
| ex.printStackTrace(); |
| } |
| return true; |
| } |
| |
| private void readOutRemainingEscape(int c) throws IOException { |
| if (c == '\033') { |
| //escape, consume waiting input: |
| NonBlockingReader inp = in.getTerminal().reader(); |
| |
| while (inp.peek(ESCAPE_TIMEOUT) > 0) { |
| inp.read(); |
| } |
| } |
| } |
| |
| //compute possible options/Fixes based on the selected FixComputer, present them to the user, |
| //and perform the selected one: |
| private void fixes(FixComputer computer) { |
| String input = prefix + in.getBuffer().toString(); |
| int cursor = prefix.length() + in.getBuffer().cursor(); |
| FixResult candidates = computer.compute(repl, input, cursor); |
| |
| try { |
| final boolean printError = candidates.error != null && !candidates.error.isEmpty(); |
| if (printError) { |
| in.getTerminal().writer().println(candidates.error); |
| } |
| if (candidates.fixes.isEmpty()) { |
| in.beep(); |
| if (printError) { |
| in.redrawLine(); |
| in.flush(); |
| } |
| } else if (candidates.fixes.size() == 1 && !computer.showMenu) { |
| if (printError) { |
| in.redrawLine(); |
| in.flush(); |
| } |
| candidates.fixes.get(0).perform(in); |
| } else { |
| List<Fix> fixes = new ArrayList<>(candidates.fixes); |
| fixes.add(0, new Fix() { |
| @Override |
| public String displayName() { |
| return repl.messageFormat("jshell.console.do.nothing"); |
| } |
| |
| @Override |
| public void perform(LineReaderImpl in) throws IOException { |
| in.redrawLine(); |
| } |
| }); |
| |
| Map<Character, Fix> char2Fix = new HashMap<>(); |
| in.getTerminal().writer().println(); |
| for (int i = 0; i < fixes.size(); i++) { |
| Fix fix = fixes.get(i); |
| char2Fix.put((char) ('0' + i), fix); |
| in.getTerminal().writer().println("" + i + ": " + fixes.get(i).displayName()); |
| } |
| in.getTerminal().writer().print(repl.messageFormat("jshell.console.choice")); |
| in.flush(); |
| int read; |
| |
| read = in.readCharacter(); |
| |
| Fix fix = char2Fix.get((char) read); |
| |
| if (fix == null) { |
| in.beep(); |
| fix = fixes.get(0); |
| } |
| |
| in.getTerminal().writer().println(); |
| |
| fix.perform(in); |
| |
| in.flush(); |
| } |
| } catch (IOException ex) { |
| throw new IllegalStateException(ex); |
| } |
| } |
| |
| private byte[] inputBytes; |
| private int inputBytesPointer; |
| |
| @Override |
| public synchronized int readUserInput() throws IOException { |
| while (inputBytes == null || inputBytes.length <= inputBytesPointer) { |
| History prevHistory = in.getHistory(); |
| boolean prevDisableCr = Display.DISABLE_CR; |
| Parser prevParser = in.getParser(); |
| |
| try { |
| in.setParser((line, cursor, context) -> new ArgumentLine(line, cursor)); |
| input.setState(State.WAIT); |
| Display.DISABLE_CR = true; |
| in.setHistory(userInputHistory); |
| inputBytes = (in.readLine("") + System.getProperty("line.separator")).getBytes(); |
| inputBytesPointer = 0; |
| } catch (UserInterruptException ex) { |
| throw new InterruptedIOException(); |
| } finally { |
| in.setParser(prevParser); |
| in.setHistory(prevHistory); |
| input.setState(State.BUFFER); |
| Display.DISABLE_CR = prevDisableCr; |
| } |
| } |
| return inputBytes[inputBytesPointer++]; |
| } |
| |
| /** |
| * A possible action which the user can choose to perform. |
| */ |
| public interface Fix { |
| /** |
| * A name that should be shown to the user. |
| */ |
| public String displayName(); |
| /** |
| * Perform the given action. |
| */ |
| public void perform(LineReaderImpl in) throws IOException; |
| } |
| |
| /** |
| * A factory for {@link Fix}es. |
| */ |
| public abstract static class FixComputer { |
| private final char shortcut; |
| private final boolean showMenu; |
| |
| /** |
| * Construct a new FixComputer. {@code shortcut} defines the key which should trigger this FixComputer. |
| * If {@code showMenu} is {@code false}, and this computer returns exactly one {@code Fix}, |
| * no options will be show to the user, and the given {@code Fix} will be performed. |
| */ |
| public FixComputer(char shortcut, boolean showMenu) { |
| this.shortcut = shortcut; |
| this.showMenu = showMenu; |
| } |
| |
| /** |
| * Compute possible actions for the given code. |
| */ |
| public abstract FixResult compute(JShellTool repl, String code, int cursor); |
| } |
| |
| /** |
| * A list of {@code Fix}es with a possible error that should be shown to the user. |
| */ |
| public static class FixResult { |
| public final List<Fix> fixes; |
| public final String error; |
| |
| public FixResult(List<Fix> fixes, String error) { |
| this.fixes = fixes; |
| this.error = error; |
| } |
| } |
| |
| private static final FixComputer[] FIX_COMPUTERS = new FixComputer[] { |
| new FixComputer('v', false) { //compute "Introduce variable" Fix: |
| private void performToVar(LineReaderImpl in, String type) throws IOException { |
| in.redrawLine(); |
| in.getBuffer().cursor(0); |
| in.putString(type + " = "); |
| in.getBuffer().cursor(in.getBuffer().cursor() - 3); |
| in.flush(); |
| } |
| |
| @Override |
| public FixResult compute(JShellTool repl, String code, int cursor) { |
| String type = repl.analysis.analyzeType(code, cursor); |
| if (type == null) { |
| return new FixResult(Collections.emptyList(), null); |
| } |
| List<Fix> fixes = new ArrayList<>(); |
| fixes.add(new Fix() { |
| @Override |
| public String displayName() { |
| return repl.messageFormat("jshell.console.create.variable"); |
| } |
| |
| @Override |
| public void perform(LineReaderImpl in) throws IOException { |
| performToVar(in, type); |
| } |
| }); |
| int idx = type.lastIndexOf("."); |
| if (idx > 0) { |
| String stype = type.substring(idx + 1); |
| QualifiedNames res = repl.analysis.listQualifiedNames(stype, stype.length()); |
| if (res.isUpToDate() && res.getNames().contains(type) |
| && !res.isResolvable()) { |
| fixes.add(new Fix() { |
| @Override |
| public String displayName() { |
| return "import: " + type + ". " + |
| repl.messageFormat("jshell.console.create.variable"); |
| } |
| |
| @Override |
| public void perform(LineReaderImpl in) throws IOException { |
| repl.processSource("import " + type + ";"); |
| in.getTerminal().writer().println("Imported: " + type); |
| performToVar(in, stype); |
| } |
| }); |
| } |
| } |
| return new FixResult(fixes, null); |
| } |
| }, |
| new FixComputer('m', false) { //compute "Introduce method" Fix: |
| private void performToMethod(LineReaderImpl in, String type, String code) throws IOException { |
| in.redrawLine(); |
| if (!code.trim().endsWith(";")) { |
| in.putString(";"); |
| } |
| in.putString(" }"); |
| in.getBuffer().cursor(0); |
| String afterCursor = type.equals("void") |
| ? "() { " |
| : "() { return "; |
| in.putString(type + " " + afterCursor); |
| // position the cursor where the method name should be entered (before parens) |
| in.getBuffer().cursor(in.getBuffer().cursor() - afterCursor.length()); |
| in.flush(); |
| } |
| |
| private FixResult reject(JShellTool repl, String messageKey) { |
| return new FixResult(Collections.emptyList(), repl.messageFormat(messageKey)); |
| } |
| |
| @Override |
| public FixResult compute(JShellTool repl, String code, int cursor) { |
| final String codeToCursor = code.substring(0, cursor); |
| final String type; |
| final CompletionInfo ci = repl.analysis.analyzeCompletion(codeToCursor); |
| if (!ci.remaining().isEmpty()) { |
| return reject(repl, "jshell.console.exprstmt"); |
| } |
| switch (ci.completeness()) { |
| case COMPLETE: |
| case COMPLETE_WITH_SEMI: |
| case CONSIDERED_INCOMPLETE: |
| break; |
| case EMPTY: |
| return reject(repl, "jshell.console.empty"); |
| case DEFINITELY_INCOMPLETE: |
| case UNKNOWN: |
| default: |
| return reject(repl, "jshell.console.erroneous"); |
| } |
| List<Snippet> snl = repl.analysis.sourceToSnippets(ci.source()); |
| if (snl.size() != 1) { |
| return reject(repl, "jshell.console.erroneous"); |
| } |
| Snippet sn = snl.get(0); |
| switch (sn.kind()) { |
| case EXPRESSION: |
| type = ((ExpressionSnippet) sn).typeName(); |
| break; |
| case STATEMENT: |
| type = "void"; |
| break; |
| case VAR: |
| if (sn.subKind() != SubKind.TEMP_VAR_EXPRESSION_SUBKIND) { |
| // only valid var is an expression turned into a temp var |
| return reject(repl, "jshell.console.exprstmt"); |
| } |
| type = ((VarSnippet) sn).typeName(); |
| break; |
| case IMPORT: |
| case METHOD: |
| case TYPE_DECL: |
| return reject(repl, "jshell.console.exprstmt"); |
| case ERRONEOUS: |
| default: |
| return reject(repl, "jshell.console.erroneous"); |
| } |
| List<Fix> fixes = new ArrayList<>(); |
| fixes.add(new Fix() { |
| @Override |
| public String displayName() { |
| return repl.messageFormat("jshell.console.create.method"); |
| } |
| |
| @Override |
| public void perform(LineReaderImpl in) throws IOException { |
| performToMethod(in, type, codeToCursor); |
| } |
| }); |
| int idx = type.lastIndexOf("."); |
| if (idx > 0) { |
| String stype = type.substring(idx + 1); |
| QualifiedNames res = repl.analysis.listQualifiedNames(stype, stype.length()); |
| if (res.isUpToDate() && res.getNames().contains(type) |
| && !res.isResolvable()) { |
| fixes.add(new Fix() { |
| @Override |
| public String displayName() { |
| return "import: " + type + ". " + |
| repl.messageFormat("jshell.console.create.method"); |
| } |
| |
| @Override |
| public void perform(LineReaderImpl in) throws IOException { |
| repl.processSource("import " + type + ";"); |
| in.getTerminal().writer().println("Imported: " + type); |
| performToMethod(in, stype, codeToCursor); |
| } |
| }); |
| } |
| } |
| return new FixResult(fixes, null); |
| } |
| }, |
| new FixComputer('i', true) { //compute "Add import" Fixes: |
| @Override |
| public FixResult compute(JShellTool repl, String code, int cursor) { |
| QualifiedNames res = repl.analysis.listQualifiedNames(code, cursor); |
| List<Fix> fixes = new ArrayList<>(); |
| for (String fqn : res.getNames()) { |
| fixes.add(new Fix() { |
| @Override |
| public String displayName() { |
| return "import: " + fqn; |
| } |
| |
| @Override |
| public void perform(LineReaderImpl in) throws IOException { |
| repl.processSource("import " + fqn + ";"); |
| in.getTerminal().writer().println("Imported: " + fqn); |
| in.redrawLine(); |
| } |
| }); |
| } |
| if (res.isResolvable()) { |
| return new FixResult(Collections.emptyList(), |
| repl.messageFormat("jshell.console.resolvable")); |
| } else { |
| String error = ""; |
| if (fixes.isEmpty()) { |
| error = repl.messageFormat("jshell.console.no.candidate"); |
| } |
| if (!res.isUpToDate()) { |
| error += repl.messageFormat("jshell.console.incomplete"); |
| } |
| return new FixResult(fixes, error); |
| } |
| } |
| } |
| }; |
| |
| private History getHistory() { |
| return in.getHistory(); |
| } |
| |
| private static final class TestTerminal extends LineDisciplineTerminal { |
| |
| private static final int DEFAULT_HEIGHT = 24; |
| |
| private final NonBlockingReader inputReader; |
| |
| public TestTerminal(InputStream input, OutputStream output) throws Exception { |
| super("test", "ansi", output, Charset.forName("UTF-8")); |
| this.inputReader = NonBlocking.nonBlocking(getName(), input, encoding()); |
| Attributes a = new Attributes(getAttributes()); |
| a.setLocalFlag(LocalFlag.ECHO, false); |
| setAttributes(attributes); |
| int h = DEFAULT_HEIGHT; |
| try { |
| String hp = System.getProperty("test.terminal.height"); |
| if (hp != null && !hp.isEmpty()) { |
| h = Integer.parseInt(hp); |
| } |
| } catch (Throwable ex) { |
| // ignore |
| } |
| setSize(new Size(80, h)); |
| } |
| |
| @Override |
| public NonBlockingReader reader() { |
| return inputReader; |
| } |
| |
| @Override |
| protected void doClose() throws IOException { |
| super.doClose(); |
| slaveInput.close(); |
| inputReader.close(); |
| } |
| |
| } |
| |
| private static final class CompletionState { |
| /**The number of actions since the last completion invocation. Will be 1 when completion is |
| * invoked immediately after the last completion invocation.*/ |
| public int actionCount; |
| /**Precomputed completion actions. Should only be reused if actionCount == 1.*/ |
| public List<CompletionTask> todo = Collections.emptyList(); |
| } |
| |
| } |