package org.rascalmpl.repl; import java.io.File; import java.io.FilterWriter; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.io.PrintWriter; import java.io.Writer; import java.net.URISyntaxException; import java.util.Arrays; import java.util.List; import java.util.Queue; import java.util.concurrent.ConcurrentLinkedQueue; import org.fusesource.jansi.Ansi; import org.rascalmpl.library.experiments.Compiler.RVM.Interpreter.NoSuchRascalFunction; import org.rascalmpl.library.experiments.Compiler.RVM.Interpreter.ideservices.IDEServices; import org.rascalmpl.library.util.PathConfig; import org.rascalmpl.value.ISourceLocation; import jline.Terminal; import jline.console.ConsoleReader; import jline.console.UserInterruptException; import jline.console.completer.CandidateListCompletionHandler; import jline.console.completer.Completer; import jline.console.history.FileHistory; import jline.console.history.PersistentHistory; import jline.internal.ShutdownHooks; import jline.internal.ShutdownHooks.Task; public abstract class BaseREPL { protected final ConsoleReader reader; private final OutputStream originalStdOut; protected final boolean prettyPrompt; protected final boolean allowColors; protected final Writer stdErr; protected volatile boolean keepRunning = true; private volatile Task historyFlusher = null; private volatile PersistentHistory history = null; private final Queue<String> commandQueue = new ConcurrentLinkedQueue<String>(); protected IDEServices ideServices; public BaseREPL(PathConfig pcfg, InputStream stdin, OutputStream stdout, boolean prettyPrompt, boolean allowColors, File file, Terminal terminal, IDEServices ideServices) throws IOException, URISyntaxException { this(pcfg, stdin, stdout, prettyPrompt, allowColors, file != null ? new FileHistory(file) : null, terminal, ideServices); } public BaseREPL(PathConfig pcfg, InputStream stdin, OutputStream stdout, boolean prettyPrompt, boolean allowColors, ISourceLocation file, Terminal terminal, IDEServices ideServices) throws IOException, URISyntaxException { this(pcfg, stdin, stdout, prettyPrompt, allowColors, file != null ? new SourceLocationHistory(file) : null, terminal, ideServices); } public static char ctrl(char ch) { assert 'A' <= ch && ch <= '_'; return (char)((((int)ch) - 'A') + 1); } private static byte CANCEL_RUNNING_COMMAND = (byte)ctrl('C'); private static byte STOP_REPL = (byte)ctrl('D'); private static byte STACK_TRACE = (byte)ctrl('\\'); private BaseREPL(PathConfig pcfg, InputStream stdin, OutputStream stdout, boolean prettyPrompt, boolean allowColors, PersistentHistory history, Terminal terminal, IDEServices ideServices) throws IOException, URISyntaxException { this.originalStdOut = stdout; if (!(stdin instanceof NotifieableInputStream) && !(stdin.getClass().getCanonicalName().contains("jline"))) { stdin = new NotifieableInputStream(stdin, new byte[] { CANCEL_RUNNING_COMMAND, STOP_REPL, STACK_TRACE }, (Byte b) -> handleEscape(b)); } reader = new ConsoleReader(stdin, stdout, terminal); this.ideServices = ideServices; if (history != null) { this.history = history; reader.setHistory(history); historyFlusher = new Task() { @Override public void run() throws Exception { history.flush(); } }; ShutdownHooks.add(historyFlusher); } reader.setExpandEvents(false); prettyPrompt = prettyPrompt && terminal.isAnsiSupported(); this.prettyPrompt = prettyPrompt; this.allowColors = allowColors; if (prettyPrompt && allowColors) { this.stdErr = new RedErrorWriter(reader.getOutput()); } else if (prettyPrompt) { this.stdErr = new ItalicErrorWriter(reader.getOutput()); } else { this.stdErr = new FilterWriter(reader.getOutput()) { }; // create a basic wrapper to avoid locking on stdout and stderr } initialize(pcfg, reader.getOutput(), stdErr, ideServices); if (supportsCompletion()) { reader.addCompleter(new Completer(){ @Override public int complete(String buffer, int cursor, List<CharSequence> candidates) { try { CompletionResult res = completeFragment(buffer, cursor); candidates.clear(); if (res != null && res.getOffset() > -1 && !res.getSuggestions().isEmpty()) { candidates.addAll(res.getSuggestions()); return res.getOffset(); } return -1; } catch(Throwable t) { // the completer should never fail, this breaks jline return -1; } } }); if (reader.getCompletionHandler() instanceof CandidateListCompletionHandler) { ((CandidateListCompletionHandler)reader.getCompletionHandler()).setPrintSpaceAfterFullCompletion(printSpaceAfterFullCompletion()); } reader.setHandleUserInterrupt(true); } } /** * During the constructor call initialize is called after the REPL is setup enough to have a stdout and std err to write to. * @param pcfg the PathConfig to be used * @param stdout the output stream to write normal output to. * @param stderr the error stream to write error messages on, depending on the environment and options passed, will print in red. * @param ideServices TODO * @throws NoSuchRascalFunction * @throws IOException * @throws URISyntaxException */ protected abstract void initialize(PathConfig pcfg, Writer stdout, Writer stderr, IDEServices ideServices) throws IOException, URISyntaxException; /** * Will be called everytime a new prompt is printed. * @return The string representing the prompt. */ protected abstract String getPrompt(); /** * After a newline is pressed, the current line is handed to this method. * @param line the current line entered. * @throws InterruptedException throw this exception to stop the REPL (instead of calling .stop()) */ protected abstract void handleInput(String line) throws InterruptedException; /** * If a line is canceled with ctrl-C this method is called too handle the reset in the child-class. * @throws InterruptedException throw this exception to stop the REPL (instead of calling .stop()) */ protected abstract void handleReset() throws InterruptedException; /** * Test if completion of statement in the current line is supported * @return true if the completeFragment method can provide completions */ protected abstract boolean supportsCompletion(); /** * If the completion succeeded with one match, should a space be printed aftwards? * @return true if completed fragment should be followed by a space */ protected abstract boolean printSpaceAfterFullCompletion(); /** * If a user hits the TAB key, the current line and the offset is provided to try and complete a fragment of the current line. * @param line The current line. * @param cursor The cursor offset in the line. * @return suggestions for the line. */ protected abstract CompletionResult completeFragment(String line, int cursor); /** * This method gets called from another thread, and indicates the user pressed CTLR-C during a call to handleInput. * * Interrupt the handleInput code as soon as possible, but leave stuff in a valid state. */ protected abstract void cancelRunningCommandRequested(); /** * This method gets called from another thread, and indicates the user pressed CTLR-D during a call to handleInput. * * Quit the code from handleInput as soon as possible, assume the REPL will close after this. */ protected abstract void terminateRequested(); /** * This method gets called from another thread, indicates a user pressed CTRL+\ during a call to handleInput. * * If possible, print the current stack trace. */ protected abstract void stackTraceRequested(); private String previousPrompt = ""; public static final String PRETTY_PROMPT_PREFIX = Ansi.ansi().reset().bold().toString(); public static final String PRETTY_PROMPT_POSTFIX = Ansi.ansi().boldOff().reset().toString(); protected void updatePrompt() { String newPrompt = getPrompt(); if (!newPrompt.equals(previousPrompt)) { previousPrompt = newPrompt; if (prettyPrompt) { reader.setPrompt(PRETTY_PROMPT_PREFIX + newPrompt + PRETTY_PROMPT_POSTFIX); } else { reader.setPrompt(newPrompt); } } } /** * Queue a command (separated by newlines) to be "entered" */ public void queueCommand(String command) { commandQueue.addAll(Arrays.asList(command.split("[\\n\\r]"))); } private volatile boolean handlingInput = false; private boolean handleEscape(Byte b) { if (handlingInput) { if (b == CANCEL_RUNNING_COMMAND) { cancelRunningCommandRequested(); return true; } else if (b == STOP_REPL) { // jline already handles this // but we do have to stop the interpreter terminateRequested(); this.stop(); return true; } else if (b == STACK_TRACE) { stackTraceRequested(); return true; } } return false; } /** * This will run the console in the current thread, and will block until it is either: * <ul> * <li> handleInput throws an InteruptedException. * <li> input reaches the end of the stream * <li> either the input or output stream throws an IOException * </ul> */ public void run() throws IOException { try { updatePrompt(); while(keepRunning) { boolean handledQueue = false; String queuedCommand; while ((queuedCommand = commandQueue.poll()) != null) { handledQueue = true; reader.resetPromptLine(reader.getPrompt(), queuedCommand, 0); reader.println(); reader.getHistory().add(queuedCommand); try { handlingInput = true; handleInput(queuedCommand); } finally { handlingInput = false; } } if (handledQueue) { String oldPrompt = reader.getPrompt(); reader.resetPromptLine("", "", 0); reader.setPrompt(oldPrompt); } updatePrompt(); try { String line = reader.readLine(reader.getPrompt(), null, null); if (line == null) { // EOF break; } try { handlingInput = true; handleInput(line); } finally { handlingInput = false; } } catch (UserInterruptException u) { reader.println(); handleReset(); updatePrompt(); } } } catch (IOException e) { try (PrintWriter err = new PrintWriter(stdErr, true)) { err.println("REPL Failed: "); if (!err.checkError()) { e.printStackTrace(err); } else { e.printStackTrace(); } err.flush(); stdErr.flush(); } throw e; } catch (InterruptedException e) { // we are closing down, so do nothing, the finally clause will take care of it } finally { reader.getOutput().flush(); originalStdOut.flush(); if (historyFlusher != null) { ShutdownHooks.remove(historyFlusher); history.flush(); } reader.shutdown(); } } /** * stop the REPL without waiting for it to stop */ public void stop() { keepRunning = false; reader.shutdown(); } }