From 31cb437776386625782903943f0ce3f70786a8e4 Mon Sep 17 00:00:00 2001 From: Davy Landman Date: Tue, 28 May 2024 10:50:43 +0200 Subject: [PATCH 01/35] Working on jline3 --- .vscode/launch.json | 11 +- pom.xml | 31 ++ src/org/rascalmpl/interpreter/Evaluator.java | 6 + src/org/rascalmpl/shell/RascalShell2.java | 286 +++++++++++++++++++ 4 files changed, 332 insertions(+), 2 deletions(-) create mode 100644 src/org/rascalmpl/shell/RascalShell2.java diff --git a/.vscode/launch.json b/.vscode/launch.json index aa506d012ad..19e123daa5f 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -4,6 +4,13 @@ // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 "version": "0.2.0", "configurations": [ + { + "type": "java", + "name": "RascalShell2", + "request": "launch", + "mainClass": "org.rascalmpl.shell.RascalShell2", + "projectName": "rascal" + }, { "type": "java", "name": "Launch DocRunner", @@ -38,7 +45,7 @@ "request": "launch", "mainClass": "org.rascalmpl.shell.RascalShell", "projectName": "rascal", - "cwd" : "${workspaceFolder}/../rascal-tutor", + "cwd": "${workspaceFolder}/../rascal-tutor", "vmArgs": "-Xss80m -Xmx2g -ea" }, { @@ -47,7 +54,7 @@ "request": "attach", "projectName": "rascal", "hostName": "localhost", - "port": 9001 + "port": 9213 }, { "type": "java", diff --git a/pom.xml b/pom.xml index 01c9f75078a..6f210da2fdc 100644 --- a/pom.xml +++ b/pom.xml @@ -33,6 +33,7 @@ 2 11 0.28.4 + 3.26.1 @@ -469,6 +470,36 @@ jline 2.14.6 + + org.jline + jline-terminal-jni + ${jline.version} + + + org.jline + jline-terminal-jna + ${jline.version} + + + org.jline + jline-reader + ${jline.version} + + + org.jline + jline-terminal + ${jline.version} + + + org.jline + jline-style + ${jline.version} + + + org.jline + jline-console + ${jline.version} + org.yaml snakeyaml diff --git a/src/org/rascalmpl/interpreter/Evaluator.java b/src/org/rascalmpl/interpreter/Evaluator.java index 5183ce362df..ea38006cf6c 100755 --- a/src/org/rascalmpl/interpreter/Evaluator.java +++ b/src/org/rascalmpl/interpreter/Evaluator.java @@ -1983,4 +1983,10 @@ public void showMessage(IConstructor message) { } } } + + + public void overwritePrintStream(PrintWriter outWriter, PrintWriter errWriter) { + this.curOutWriter = outWriter; + this.curErrWriter = errWriter; + } } diff --git a/src/org/rascalmpl/shell/RascalShell2.java b/src/org/rascalmpl/shell/RascalShell2.java new file mode 100644 index 00000000000..06febee1511 --- /dev/null +++ b/src/org/rascalmpl/shell/RascalShell2.java @@ -0,0 +1,286 @@ +/******************************************************************************* + * Copyright (c) 2009-2015 CWI + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + + * * Jurgen J. Vinju - Jurgen.Vinju@cwi.nl - CWI + * * Tijs van der Storm - Tijs.van.der.Storm@cwi.nl + * * Paul Klint - Paul.Klint@cwi.nl - CWI + * * Arnold Lankamp - Arnold.Lankamp@cwi.nl + *******************************************************************************/ +package org.rascalmpl.shell; + +import java.io.IOException; +import java.io.OutputStream; +import java.io.PrintWriter; +import java.nio.ByteBuffer; +import java.nio.CharBuffer; +import java.nio.charset.CharsetDecoder; +import java.nio.charset.CodingErrorAction; +import java.nio.charset.StandardCharsets; +import java.util.List; + +import org.apache.commons.io.input.NullInputStream; +import org.jline.reader.Candidate; +import org.jline.reader.CompletionMatcher; +import org.jline.reader.LineReader; +import org.jline.reader.LineReaderBuilder; +import org.jline.reader.ParsedLine; +import org.jline.reader.LineReader.Option; +import org.jline.reader.impl.completer.StringsCompleter; +import org.jline.reader.impl.history.DefaultHistory; +import org.jline.terminal.TerminalBuilder; +import org.rascalmpl.exceptions.Throw; +import org.rascalmpl.ideservices.BasicIDEServices; +import org.rascalmpl.ideservices.IDEServices; +import org.rascalmpl.interpreter.Evaluator; +import org.rascalmpl.interpreter.NullRascalMonitor; +import org.rascalmpl.interpreter.control_exceptions.InterruptException; +import org.rascalmpl.interpreter.control_exceptions.QuitException; +import org.rascalmpl.interpreter.env.GlobalEnvironment; +import org.rascalmpl.interpreter.env.ModuleEnvironment; +import org.rascalmpl.interpreter.load.StandardLibraryContributor; +import org.rascalmpl.interpreter.result.Result; +import org.rascalmpl.interpreter.staticErrors.StaticError; +import org.rascalmpl.interpreter.utils.RascalManifest; +import org.rascalmpl.interpreter.utils.Timing; +import org.rascalmpl.parser.gtd.exception.ParseError; +import org.rascalmpl.repl.ReplTextWriter; +import org.rascalmpl.uri.URIResolverRegistry; +import org.rascalmpl.uri.URIUtil; +import org.rascalmpl.values.ValueFactoryFactory; + +import io.usethesource.vallang.ISourceLocation; +import io.usethesource.vallang.IValue; +import io.usethesource.vallang.IValueFactory; + +import static org.rascalmpl.interpreter.utils.ReadEvalPrintDialogMessages.parseErrorMessage; +import static org.rascalmpl.interpreter.utils.ReadEvalPrintDialogMessages.staticErrorMessage; +import static org.rascalmpl.interpreter.utils.ReadEvalPrintDialogMessages.throwMessage; +import static org.rascalmpl.interpreter.utils.ReadEvalPrintDialogMessages.throwableMessage; + + +public class RascalShell2 { + + private static void printVersionNumber(){ + System.err.println("Version: " + RascalManifest.getRascalVersionNumber()); + } + + public static void main(String[] args) throws IOException { + System.setProperty("apple.awt.UIElement", "true"); // turns off the annoying desktop icon + printVersionNumber(); + + try { + var term = TerminalBuilder.builder() + .color(true) + .encoding(StandardCharsets.UTF_8) + //.jni(true) + //.streams(System.in, System.out) + .build(); + + var reader = LineReaderBuilder.builder() + .appName("Rascal REPL") + .completer(new StringsCompleter("IO", "IOMeer", "println", "print", "printlnExp")) + .terminal(term) + .history(new DefaultHistory()) + .build(); + + //IRascalMonitor monitor = IRascalMonitor.buildConsoleMonitor(System.in, System.out, true); + var monitor = new NullRascalMonitor() { + @Override + public void warning(String message, ISourceLocation src) { + reader.printAbove("[WARN] " + message); + } + }; + + IDEServices services = new BasicIDEServices(term.writer(), monitor); + + + GlobalEnvironment heap = new GlobalEnvironment(); + ModuleEnvironment root = heap.addModule(new ModuleEnvironment(ModuleEnvironment.SHELL_MODULE, heap)); + IValueFactory vf = ValueFactoryFactory.getValueFactory(); + Evaluator evaluator = new Evaluator(vf, new NullInputStream(), new FakeOutput(reader), new FakeOutput(reader), root, heap, monitor); + evaluator.overwritePrintStream(new FakePrintStream(reader, false), new FakePrintStream(reader, true)); + evaluator.addRascalSearchPathContributor(StandardLibraryContributor.getInstance()); + + URIResolverRegistry reg = URIResolverRegistry.getInstance(); + + var indentedPrettyPrinter = new ReplTextWriter(true); + + while (true) { + String line = reader.readLine("rascal> "); + try { + + Result value; + + synchronized(evaluator) { + value = evaluator.eval(monitor, line, URIUtil.rootLocation("prompt")); + evaluator.endAllJobs(); + } + + if (value.isVoid()) { + reader.printAbove("ok \n"); + } + else { + reader.printAbove("Result: " + value.toString(1024)); + } + } + catch (InterruptException ie) { + reader.printAbove("Interrupted"); + try { + ie.getRascalStackTrace().prettyPrintedString(evaluator.getErrorPrinter(), indentedPrettyPrinter); + } + catch (IOException e) { + } + } + catch (ParseError pe) { + parseErrorMessage(evaluator.getErrorPrinter(), line, "prompt", pe, indentedPrettyPrinter); + } + catch (StaticError e) { + staticErrorMessage(evaluator.getErrorPrinter(),e, indentedPrettyPrinter); + } + catch (Throw e) { + throwMessage(evaluator.getErrorPrinter(),e, indentedPrettyPrinter); + } + catch (QuitException q) { + reader.printAbove("Quiting REPL"); + break; + } + catch (Throwable e) { + throwableMessage(evaluator.getErrorPrinter(), e, evaluator.getStackTrace(), indentedPrettyPrinter); + } + } + System.exit(0); + } + catch (Throwable e) { + System.err.println("\n\nunexpected error: " + e.getMessage()); + e.printStackTrace(System.err); + System.exit(1); + } + finally { + System.out.flush(); + System.err.flush(); + } + } + + private static class FakeOutput extends OutputStream { + + private final LineReader target; + private final CharsetDecoder decoder; + private final CharBuffer decoded; + + FakeOutput(LineReader target) { + this.target = target; + this.decoder = StandardCharsets.UTF_8.newDecoder(); + this.decoder.replaceWith("?"); + this.decoder.onMalformedInput(CodingErrorAction.REPLACE); + this.decoder.onUnmappableCharacter(CodingErrorAction.REPLACE); + this.decoded = CharBuffer.allocate(1024); + } + + @Override + public void write(int b) throws IOException { + var res = decoder.decode(ByteBuffer.wrap(new byte[]{ (byte)b }), decoded, false); + if (res.isOverflow()) { + flush(); + write(b); + } + } + + @Override + public void write(byte[] b, int off, int len) throws IOException { + var bytes = ByteBuffer.wrap(b, off, len); + while (bytes.hasRemaining()) { + var res = decoder.decode(bytes, decoded, false); + if (res.isOverflow()) { + flush(); + } + else if (res.isError()) { + throw new IOException("Decoding failed with: " + res); + } + } + } + + @Override + public void flush() throws IOException { + try { + decoded.flip(); + if (decoded.hasRemaining()) { + this.target.printAbove(decoded.toString()); + } + } + finally { + decoded.clear(); + } + } + } + + + private static class FakePrintStream extends PrintWriter { + private final LineReader target; + private final CharBuffer buffer; + + public FakePrintStream(LineReader target, boolean autoFlush) { + super(OutputStream.nullOutputStream(), autoFlush, StandardCharsets.UTF_8); + this.target = target; + this.buffer = CharBuffer.allocate(8*1024); + } + + + @Override + public void write(int c) { + makeRoom(1); + this.buffer.append((char)c); + } + + private void makeRoom(int i) { + if (this.buffer.remaining() < i) { + flush(); + } + } + + + @Override + public void write(String s, int off, int len) { + while (len > 0) { + makeRoom(len); + int room = Math.min(buffer.remaining(), len); + buffer.append(s, off, room); + off += room; + len -= room; + } + } + + @Override + public void write(char[] buf, int off, int len) { + while (len > 0) { + makeRoom(len); + int room = Math.min(buffer.remaining(), len); + buffer.put(buf, off, room); + off += room; + len -= room; + } + } + + @Override + public void flush() { + try { + buffer.flip(); + if (buffer.hasRemaining()) { + target.printAbove(buffer.toString()); + } + } + finally { + buffer.clear(); + } + } + + + } + + +} From c67a197710e8843fcabea88910e621127a57eb95 Mon Sep 17 00:00:00 2001 From: Davy Landman Date: Wed, 29 May 2024 12:03:54 +0200 Subject: [PATCH 02/35] Working on jline3 --- pom.xml | 1 + .../repl/TerminalProgressBarMonitor2.java | 447 ++++++++++++++++++ src/org/rascalmpl/shell/RascalShell2.java | 68 ++- 3 files changed, 479 insertions(+), 37 deletions(-) create mode 100644 src/org/rascalmpl/repl/TerminalProgressBarMonitor2.java diff --git a/pom.xml b/pom.xml index 6f210da2fdc..d94ab9a0352 100644 --- a/pom.xml +++ b/pom.xml @@ -469,6 +469,7 @@ jline jline 2.14.6 + provided org.jline diff --git a/src/org/rascalmpl/repl/TerminalProgressBarMonitor2.java b/src/org/rascalmpl/repl/TerminalProgressBarMonitor2.java new file mode 100644 index 00000000000..1e7f2bbf159 --- /dev/null +++ b/src/org/rascalmpl/repl/TerminalProgressBarMonitor2.java @@ -0,0 +1,447 @@ +package org.rascalmpl.repl; + +import java.io.FilterWriter; +import java.io.IOException; +import java.io.InputStream; +import java.io.PrintWriter; +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; +import java.time.Duration; +import java.time.Instant; +import java.util.LinkedList; +import java.util.List; + +import org.jline.terminal.Terminal; +import org.rascalmpl.debug.IRascalMonitor; + +import io.usethesource.vallang.ISourceLocation; + +/** + * The terminal progress bar monitor wraps the standard output stream to be able to monitor + * output and keep the progress bars at the same place in the window while other prints happen + * asyncronously (or even in parallel). It can be passed to the IDEServices API such that + * clients can start progress bars, make them grow and end them using the API in util::Monitor. + * + * This gives the console the ability to show progress almost as clearly as an IDE can with a + * UI experience. + * + * This class only works correctly if the actual _raw_ output stream of the terminal is wrapped + * with an object of this class. + */ +public class TerminalProgressBarMonitor2 extends PrintWriter implements IRascalMonitor { + /** + * We administrate an ordered list of named bars, which will be printed from + * top to bottom just above the next prompt. + */ + private List bars = new LinkedList<>(); + + /** + * The entire width in character columns of the current terminal. Resizes everytime when we start + * the first job. + */ + private int lineWidth; + + private final boolean unicodeEnabled; + + /**x + * Will make everything slow, but easier to spot mistakes + */ + private final boolean debug = false; + + /** + * Used to get updates to the width of the terminal + */ + private final Terminal tm; + + private final PrintWriter writer; + + + @SuppressWarnings("resource") + public TerminalProgressBarMonitor2(Terminal term) { + super(term.writer()); + this.writer = term.writer(); + + this.tm = term; + + this.lineWidth = tm.getWidth(); + this.unicodeEnabled = term.encoding().contains(StandardCharsets.UTF_8); + } + + /** + * Represents one currently running progress bar + */ + private class ProgressBar { + private final long threadId; + private final String threadName; + private final String name; + private int max; + private int current = 0; + private int previousWidth = 0; + private int doneWidth = 0; + private final int barWidthUnicode = lineWidth - "☐ ".length() - " 🕐 00:00:00.000 ".length(); + private final int barWidthAscii = lineWidth - "? ".length() - " - 00:00:00.000 ".length(); + private final Instant startTime; + private Duration duration; + private String message = ""; + + /** + * Stepper is incremented with every jobStep that has an visible effect on the progress bar. + * It is used to index into `clocks` or `twister` to create an animation effect. + */ + private int stepper = 1; + private final String[] clocks = new String[] {"🕐" , "🕑", "🕒", "🕓", "🕔", "🕕", "🕖", "🕗", "🕘", "🕙", "🕛"}; + private final String[] twister = new String[] {"." , ".", "o", "o", "O","O", "O", "o", "o", ".", "."}; + public int nesting = 0; + + ProgressBar(String name, int max) { + this.threadId = Thread.currentThread().getId(); + this.threadName = Thread.currentThread().getName(); + this.name = name; + this.max = Math.max(1, max); + this.startTime = Instant.now(); + this.duration = Duration.ZERO; + this.message = name; + } + + void worked(int amount, String message) { + if (current + amount > max) { + // Fixing this warning helps avoiding to flicker the tick sign on and off, and also makes the progress bar + // a more accurate depiction of the progress of the computation. + warning("Monitor of " + name + " is over max (" + max + ") by " + (current + amount - max), null); + } + + this.current = Math.min(current + amount, max); + this.duration = Duration.between(startTime, Instant.now()); + this.message = message; + } + + /** + * To avoid flickering of all bars at the same time, we only reprint + * the current bar + */ + void update() { + // to avoid flicker we only print if there is a new bar character to draw + if (newWidth() != previousWidth) { + stepper++; + writer.write(ANSI.moveUp(bars.size() - bars.indexOf(this))); + write(); // this moves the cursor already one line down due to `println` + int distance = bars.size() - bars.indexOf(this) - 1; + if (distance > 0) { + // ANSI will move 1 line even if the parameter is 0 + writer.write(ANSI.moveDown(distance)); + } + writer.flush(); + } + } + + int newWidth() { + if (max != 0) { + current = Math.min(max, current); // for robustness sake + var partDone = (current * 1.0) / (max * 1.0); + return (int) Math.floor(barWidthUnicode * partDone); + } + else { + return barWidthUnicode % stepper; + } + } + + /** + * Print the current state of the progress bar + */ + void write() { + previousWidth = doneWidth; + doneWidth = newWidth(); + + // var overWidth = barWidth - doneWidth; + var done = unicodeEnabled + ? (current >= max ? "☑ " : "☐ ") + : (current >= max ? "X " : "O ") + ; + + // capitalize + var msg = message.substring(0, 1).toUpperCase() + message.substring(1, message.length()); + + // fill up and cut off: + msg = threadLabel() + msg; + msg = (msg + " ".repeat(Math.max(0, barWidthUnicode - msg.length()))).substring(0, barWidthUnicode); + + // split + var barWidth = unicodeEnabled ? barWidthUnicode : barWidthAscii; + var frontPart = msg.substring(0, doneWidth); + var backPart = msg.substring(doneWidth, msg.length()); + var clock = unicodeEnabled ? clocks[stepper % clocks.length] : twister[stepper % twister.length]; + + if (barWidth < 1) { + return; // robustness against very small screens. At least don't throw bounds exceptions + } + else if (barWidth <= 3) { // we can print the clock for good measure + writer.println(clock); + return; + } + + var line + = done + + ANSI.darkBackground() + + frontPart + + ANSI.noBackground() + + ANSI.lightBackground() + + backPart + + ANSI.noBackground() + + " " + clock + " " + + String.format("%d:%02d:%02d.%03d", duration.toHoursPart(), duration.toMinutes(), duration.toSecondsPart(), duration.toMillisPart()) + + " " + ; + + writer.println("\r" + line); // note this puts us one line down + } + + private String threadLabel() { + if (threadName.isEmpty()) { + return ""; + } + else if ("main".equals(threadName)) { + return ""; + } + + return threadName + ": "; + } + + @Override + public boolean equals(Object obj) { + return obj instanceof ProgressBar + && ((ProgressBar) obj).name.equals(name) + && ((ProgressBar) obj).threadId == threadId + ; + } + + @Override + public int hashCode() { + return name.hashCode() + 31 * (int) threadId; + } + + public void done() { + this.current = Math.min(current, max); + this.duration = Duration.between(startTime, Instant.now()); + this.message = name; + } + } + + /** + * Clean the screen, ready for the next output (normal output or the next progress bars or both). + * We use fancy ANSI codes here to move the cursor and clean the screen to remove previous versions + * of the bars + */ + private void eraseBars() { + if (!bars.isEmpty()) { + writer.write(ANSI.moveUp(bars.size())); + writer.write(ANSI.clearToEndOfScreen()); + } + writer.flush(); + } + + /** + * ANSI escape codes convenience functions + */ + private static class ANSI { + + + static String moveUp(int n) { + return "\u001B[" + n + "F"; + } + + public static String darkBackground() { + return "\u001B[48;5;242m"; + } + + public static String noBackground() { + return "\u001B[49m"; + } + + public static String lightBackground() { + return "\u001B[48;5;249m"; + } + + static String moveDown(int n) { + return "\u001B[" + n + "E"; + } + + static String clearToEndOfScreen() { + return "\u001B[0J"; + } + + static String hideCursor() { + return "\u001B[?25l"; + } + + static String showCursor() { + return "\u001B[?25h"; + } + } + + /** + * Simply print the bars. No cursor movement here. Hiding the cursor prevents flickering. + */ + private void printBars() { + if (bars.isEmpty()) { + // no more bars to show, so cursor goes back. + writer.write(ANSI.showCursor()); + } + + for (var pb : bars) { + pb.write(); + } + + writer.flush(); + } + + /** + * Find a bar in the ordered list of bars, by name. + * @param name of the bar + * @return the current instance by that name + */ + private ProgressBar findBarByName(String name) { + return bars.stream() + .filter(b -> b.threadId == Thread.currentThread().getId()) + .filter(b -> b.name.equals(name)).findFirst().orElseGet(() -> null); + } + + @Override + public synchronized void jobStart(String name, int workShare, int totalWork) { + if (bars.size() == 0) { + // first new job, we take time to react to window resizing + lineWidth = tm.getWidth(); + // remove the cursor + writer.write(ANSI.hideCursor()); + } + + var pb = findBarByName(name); + + if (pb == null) { + eraseBars(); // to make room for the new bars + bars.add(new ProgressBar(name, totalWork)); + printBars(); // probably one line longer than before! + } + else { + // Zeno-bar: we add the new work to the already existing work + pb.max += totalWork; + pb.nesting++; + pb.update(); + } + } + + @Override + public synchronized void jobStep(String name, String message, int workShare) { + ProgressBar pb = findBarByName(name); + + if (pb != null) { + pb.worked(workShare, message); + pb.update(); + } + } + + @Override + public synchronized int jobEnd(String name, boolean succeeded) { + var pb = findBarByName(name); + + if (pb != null && --pb.nesting == -1) { + eraseBars(); + // write it one last time into the scrollback buffer (on top) + pb.done(); + pb.write(); + bars.remove(pb); + // print the left over bars under this one. + printBars(); + return pb.current; + } + else if (pb != null) { + pb.done(); + pb.update(); + } + + return -1; + } + + @Override + public synchronized boolean jobIsCanceled(String name) { + // ? don't know what this should do + return false; + } + + @Override + public synchronized void jobTodo(String name, int work) { + ProgressBar pb = findBarByName(name); + + if (pb != null) { + pb.max += work; + pb.update(); + } + } + + @Override + public synchronized void warning(String message, ISourceLocation src) { + if (!bars.isEmpty()) { + eraseBars(); + } + writer.println(("[WARNING] " + (src != null ? (src + ": ") : "") + message)); + if (!bars.isEmpty()) { + printBars(); + } + } + + @Override + public synchronized void write(String str, int off, int len){ + if (!bars.isEmpty()) { + eraseBars(); + super.write(str, off, len); + printBars(); + } + else { + super.write(str, off, len); + } + } + + @Override + public void write(char[] cbuf, int off, int len) { + if (!bars.isEmpty()) { + eraseBars(); + super.write(cbuf, off, len); + printBars(); + } + else { + super.write(cbuf, off, len); + } + } + + + /** + * Here we make sure the progress bars are gone just before + * someone wants to print in the console. When the printing + * is ready, we simply add our own progress bars again. + */ + @Override + public synchronized void write(int b) { + if (!bars.isEmpty()) { + eraseBars(); + super.write(b); + printBars(); + } + else { + super.write(b); + } + } + + + + @Override + public synchronized void endAllJobs() { + for (var pb : bars) { + if (pb.nesting >= 0) { + writer.println("[INFO] " + pb.name + " is still at nesting level " + pb.nesting); + } + } + + bars.clear(); + writer.write(ANSI.showCursor()); + writer.flush(); + } +} diff --git a/src/org/rascalmpl/shell/RascalShell2.java b/src/org/rascalmpl/shell/RascalShell2.java index 06febee1511..1b111a06ed6 100644 --- a/src/org/rascalmpl/shell/RascalShell2.java +++ b/src/org/rascalmpl/shell/RascalShell2.java @@ -1,19 +1,23 @@ -/******************************************************************************* - * Copyright (c) 2009-2015 CWI - * All rights reserved. This program and the accompanying materials - * are made available under the terms of the Eclipse Public License v1.0 - * which accompanies this distribution, and is available at - * http://www.eclipse.org/legal/epl-v10.html - * - * Contributors: - - * * Jurgen J. Vinju - Jurgen.Vinju@cwi.nl - CWI - * * Tijs van der Storm - Tijs.van.der.Storm@cwi.nl - * * Paul Klint - Paul.Klint@cwi.nl - CWI - * * Arnold Lankamp - Arnold.Lankamp@cwi.nl - *******************************************************************************/ +/* +Copyright (c) 2024, Swat.engineering +All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + package org.rascalmpl.shell; +import static org.rascalmpl.interpreter.utils.ReadEvalPrintDialogMessages.parseErrorMessage; +import static org.rascalmpl.interpreter.utils.ReadEvalPrintDialogMessages.staticErrorMessage; +import static org.rascalmpl.interpreter.utils.ReadEvalPrintDialogMessages.throwMessage; +import static org.rascalmpl.interpreter.utils.ReadEvalPrintDialogMessages.throwableMessage; + import java.io.IOException; import java.io.OutputStream; import java.io.PrintWriter; @@ -22,23 +26,18 @@ import java.nio.charset.CharsetDecoder; import java.nio.charset.CodingErrorAction; import java.nio.charset.StandardCharsets; -import java.util.List; import org.apache.commons.io.input.NullInputStream; -import org.jline.reader.Candidate; -import org.jline.reader.CompletionMatcher; import org.jline.reader.LineReader; import org.jline.reader.LineReaderBuilder; -import org.jline.reader.ParsedLine; -import org.jline.reader.LineReader.Option; import org.jline.reader.impl.completer.StringsCompleter; import org.jline.reader.impl.history.DefaultHistory; import org.jline.terminal.TerminalBuilder; +import org.rascalmpl.debug.IRascalMonitor; import org.rascalmpl.exceptions.Throw; import org.rascalmpl.ideservices.BasicIDEServices; import org.rascalmpl.ideservices.IDEServices; import org.rascalmpl.interpreter.Evaluator; -import org.rascalmpl.interpreter.NullRascalMonitor; import org.rascalmpl.interpreter.control_exceptions.InterruptException; import org.rascalmpl.interpreter.control_exceptions.QuitException; import org.rascalmpl.interpreter.env.GlobalEnvironment; @@ -47,22 +46,17 @@ import org.rascalmpl.interpreter.result.Result; import org.rascalmpl.interpreter.staticErrors.StaticError; import org.rascalmpl.interpreter.utils.RascalManifest; -import org.rascalmpl.interpreter.utils.Timing; import org.rascalmpl.parser.gtd.exception.ParseError; import org.rascalmpl.repl.ReplTextWriter; +import org.rascalmpl.repl.TerminalProgressBarMonitor; +import org.rascalmpl.repl.TerminalProgressBarMonitor2; import org.rascalmpl.uri.URIResolverRegistry; import org.rascalmpl.uri.URIUtil; import org.rascalmpl.values.ValueFactoryFactory; -import io.usethesource.vallang.ISourceLocation; import io.usethesource.vallang.IValue; import io.usethesource.vallang.IValueFactory; -import static org.rascalmpl.interpreter.utils.ReadEvalPrintDialogMessages.parseErrorMessage; -import static org.rascalmpl.interpreter.utils.ReadEvalPrintDialogMessages.staticErrorMessage; -import static org.rascalmpl.interpreter.utils.ReadEvalPrintDialogMessages.throwMessage; -import static org.rascalmpl.interpreter.utils.ReadEvalPrintDialogMessages.throwableMessage; - public class RascalShell2 { @@ -78,8 +72,6 @@ public static void main(String[] args) throws IOException { var term = TerminalBuilder.builder() .color(true) .encoding(StandardCharsets.UTF_8) - //.jni(true) - //.streams(System.in, System.out) .build(); var reader = LineReaderBuilder.builder() @@ -90,12 +82,14 @@ public static void main(String[] args) throws IOException { .build(); //IRascalMonitor monitor = IRascalMonitor.buildConsoleMonitor(System.in, System.out, true); - var monitor = new NullRascalMonitor() { - @Override - public void warning(String message, ISourceLocation src) { - reader.printAbove("[WARN] " + message); - } - }; + var monitor = new TerminalProgressBarMonitor2(term); + + // var monitor = new NullRascalMonitor() { + // @Override + // public void warning(String message, ISourceLocation src) { + // reader.printAbove("[WARN] " + message); + // } + // }; IDEServices services = new BasicIDEServices(term.writer(), monitor); @@ -103,8 +97,8 @@ public void warning(String message, ISourceLocation src) { GlobalEnvironment heap = new GlobalEnvironment(); ModuleEnvironment root = heap.addModule(new ModuleEnvironment(ModuleEnvironment.SHELL_MODULE, heap)); IValueFactory vf = ValueFactoryFactory.getValueFactory(); - Evaluator evaluator = new Evaluator(vf, new NullInputStream(), new FakeOutput(reader), new FakeOutput(reader), root, heap, monitor); - evaluator.overwritePrintStream(new FakePrintStream(reader, false), new FakePrintStream(reader, true)); + Evaluator evaluator = new Evaluator(vf, new NullInputStream(), OutputStream.nullOutputStream(), OutputStream.nullOutputStream(), root, heap, monitor); + evaluator.overwritePrintStream(monitor, new PrintWriter(monitor, true)); evaluator.addRascalSearchPathContributor(StandardLibraryContributor.getInstance()); URIResolverRegistry reg = URIResolverRegistry.getInstance(); From 08ffabda0944da129af3f509d378b073adaf8725 Mon Sep 17 00:00:00 2001 From: Davy Landman Date: Thu, 30 May 2024 16:21:29 +0200 Subject: [PATCH 03/35] Working on jline3 --- pom.xml | 5 + .../repl/TerminalProgressBarMonitor2.java | 199 ++++++++++++++++-- src/org/rascalmpl/shell/RascalShell2.java | 32 ++- 3 files changed, 206 insertions(+), 30 deletions(-) diff --git a/pom.xml b/pom.xml index d94ab9a0352..72d5503f652 100644 --- a/pom.xml +++ b/pom.xml @@ -501,6 +501,11 @@ jline-console ${jline.version} + + org.jline + jansi + ${jline.version} + org.yaml snakeyaml diff --git a/src/org/rascalmpl/repl/TerminalProgressBarMonitor2.java b/src/org/rascalmpl/repl/TerminalProgressBarMonitor2.java index 1e7f2bbf159..88f8c59f20f 100644 --- a/src/org/rascalmpl/repl/TerminalProgressBarMonitor2.java +++ b/src/org/rascalmpl/repl/TerminalProgressBarMonitor2.java @@ -1,19 +1,22 @@ package org.rascalmpl.repl; +import java.io.FilterOutputStream; import java.io.FilterWriter; import java.io.IOException; import java.io.InputStream; +import java.io.OutputStream; import java.io.PrintWriter; import java.nio.charset.Charset; import java.nio.charset.StandardCharsets; import java.time.Duration; import java.time.Instant; +import java.util.ArrayList; +import java.util.Arrays; import java.util.LinkedList; import java.util.List; import org.jline.terminal.Terminal; import org.rascalmpl.debug.IRascalMonitor; - import io.usethesource.vallang.ISourceLocation; /** @@ -35,6 +38,19 @@ public class TerminalProgressBarMonitor2 extends PrintWriter implements IRascalM */ private List bars = new LinkedList<>(); + /** + * We also keep a list of currently unfinished lines (one for each thread). + * Since there are generally very few threads a simple list beats a hash-map in terms + * of memory allocation and possibly also lookup efficiency. + */ + private List unfinishedLines = new ArrayList<>(3); + + /** + * This writer is there to help with the encoding to what the terminal needs. It writes directly to the + * underlying stream. + */ + private final PrintWriter writer; + /** * The entire width in character columns of the current terminal. Resizes everytime when we start * the first job. @@ -53,18 +69,140 @@ public class TerminalProgressBarMonitor2 extends PrintWriter implements IRascalM */ private final Terminal tm; - private final PrintWriter writer; - @SuppressWarnings("resource") - public TerminalProgressBarMonitor2(Terminal term) { - super(term.writer()); - this.writer = term.writer(); + public TerminalProgressBarMonitor2(Terminal tm) { + super(tm.writer()); - this.tm = term; + this.tm = tm; + this.writer = tm.writer(); this.lineWidth = tm.getWidth(); - this.unicodeEnabled = term.encoding().contains(StandardCharsets.UTF_8); + this.unicodeEnabled = tm.encoding().contains(StandardCharsets.UTF_8); + } + + /** + * Use this for debugging terminal cursor movements, step by step. + */ + private static class AlwaysFlushAlwaysShowCursor extends FilterWriter { + + public AlwaysFlushAlwaysShowCursor(PrintWriter out) { + super(out); + } + + @Override + public void write(int c) throws IOException { + out.write(c); + out.write(ANSI.showCursor()); + out.flush(); + } + + @Override + public void write(char[] cbuf, int off, int len) throws IOException { + out.write(cbuf, off, len); + out.write(ANSI.showCursor()); + out.flush(); + } + + @Override + public void write(String str, int off, int len) throws IOException { + out.write(str, off, len); + out.write(ANSI.showCursor()); + out.flush(); + } + } + + private static class UnfinishedLine { + final long threadId; + private int curCapacity = 512; + private char[] buffer = new char[curCapacity]; + private int curEnd = 0; + + public UnfinishedLine() { + this.threadId = Thread.currentThread().getId(); + } + + /** + * Adding input combines previously unfinished sentences with possible + * new (unfinished) sentences. + * + * The resulting buffer nevers contain any newline character. + */ + private void store(char[] newInput, int offset, int len) { + if (len == 0) { + return; // fast exit + } + + // first ensure capacity of the array + if (curEnd + len >= curCapacity) { + curCapacity *= 2; + buffer = Arrays.copyOf(buffer, curCapacity); + } + + System.arraycopy(newInput, offset, buffer, curEnd, len); + curEnd += len; + } + + /** + * Main workhorse looks for newline characters in the new input. + * - if there are newlines, than whatever is in the buffer can be flushed. + * - all the characters up to the last new new line are flushed immediately. + * - all the new characters after the last newline are buffered. + */ + public void write(char[] n, int offset, int len, PrintWriter out) { + int lastNL = startOfLastLine(n, offset, len); + + if (lastNL == -1) { + store(n, offset, len); + } + else { + flush(out); + out.write(n, offset, lastNL + 1); + store(n, lastNL + 1, len - (lastNL + 1)); + } + } + + /** + * Main workhorse looks for newline characters in the new input. + * - if there are newlines, than whatever is in the buffer can be flushed. + * - all the characters up to the last new new line are flushed immediately. + * - all the new characters after the last newline are buffered. + */ + public void write(String s, int offset, int len, PrintWriter out) { + write(s.toCharArray(), offset, len, out); + } + + /** + * This empties the current buffer onto the stream, + * and resets the cursor. + */ + private void flush(PrintWriter out) { + if (curEnd != 0) { + out.write(buffer, 0, curEnd); + curEnd = 0; + } + } + + /** + * Prints whatever is the last line in the buffer, + * and adds a newline. + */ + public void flushLastLine(PrintWriter out) { + if (curEnd != 0) { + flush(out); + out.write('\n'); + } + } + + private int startOfLastLine(char[] buffer, int offset, int len) { + for (int i = offset + len - 1; i >= offset; i--) { + if (buffer[i] == '\n') { + return i; + } + } + + return -1; + } } /** @@ -192,7 +330,7 @@ else if (barWidth <= 3) { // we can print the clock for good measure + " " ; - writer.println("\r" + line); // note this puts us one line down + writer.println(line); // note this puts us one line down } private String threadLabel() { @@ -244,13 +382,12 @@ private void eraseBars() { */ private static class ANSI { - static String moveUp(int n) { return "\u001B[" + n + "F"; } public static String darkBackground() { - return "\u001B[48;5;242m"; + return "\u001B[48;5;248m"; } public static String noBackground() { @@ -258,7 +395,7 @@ public static String noBackground() { } public static String lightBackground() { - return "\u001B[48;5;249m"; + return "\u001B[48;5;250m"; } static String moveDown(int n) { @@ -304,6 +441,17 @@ private ProgressBar findBarByName(String name) { .filter(b -> b.threadId == Thread.currentThread().getId()) .filter(b -> b.name.equals(name)).findFirst().orElseGet(() -> null); } + + private UnfinishedLine findUnfinishedLine() { + return unfinishedLines.stream() + .filter(l -> l.threadId == Thread.currentThread().getId()) + .findAny() + .orElseGet(() -> { + UnfinishedLine l = new UnfinishedLine(); + unfinishedLines.add(l); + return l; + }); + } @Override public synchronized void jobStart(String name, int workShare, int totalWork) { @@ -389,30 +537,34 @@ public synchronized void warning(String message, ISourceLocation src) { } @Override - public synchronized void write(String str, int off, int len){ + public void write(String s, int off, int len) { if (!bars.isEmpty()) { eraseBars(); - super.write(str, off, len); + findUnfinishedLine().write(s, off, len, writer); printBars(); } else { - super.write(str, off, len); + writer.write(s, off, len); } } + /** + * Here we make sure the progress bars are gone just before + * someone wants to print in the console. When the printing + * is ready, we simply add our own progress bars again. + */ @Override - public void write(char[] cbuf, int off, int len) { + public synchronized void write(char[] cbuf, int off, int len) { if (!bars.isEmpty()) { eraseBars(); - super.write(cbuf, off, len); + findUnfinishedLine().write(cbuf, off, len, writer); printBars(); } else { - super.write(cbuf, off, len); + writer.write(cbuf, off, len); } } - /** * Here we make sure the progress bars are gone just before * someone wants to print in the console. When the printing @@ -422,11 +574,11 @@ public void write(char[] cbuf, int off, int len) { public synchronized void write(int b) { if (!bars.isEmpty()) { eraseBars(); - super.write(b); + findUnfinishedLine().write(new char[] { (char) b },0, 1, writer); printBars(); } else { - super.write(b); + writer.write(b); } } @@ -441,7 +593,10 @@ public synchronized void endAllJobs() { } bars.clear(); + for (UnfinishedLine l : unfinishedLines) { + l.flushLastLine(writer); + } writer.write(ANSI.showCursor()); writer.flush(); } -} +} \ No newline at end of file diff --git a/src/org/rascalmpl/shell/RascalShell2.java b/src/org/rascalmpl/shell/RascalShell2.java index 1b111a06ed6..a850b5759ab 100644 --- a/src/org/rascalmpl/shell/RascalShell2.java +++ b/src/org/rascalmpl/shell/RascalShell2.java @@ -21,6 +21,7 @@ import java.io.IOException; import java.io.OutputStream; import java.io.PrintWriter; +import java.io.StringWriter; import java.nio.ByteBuffer; import java.nio.CharBuffer; import java.nio.charset.CharsetDecoder; @@ -28,12 +29,12 @@ import java.nio.charset.StandardCharsets; import org.apache.commons.io.input.NullInputStream; +import org.jline.jansi.Ansi; import org.jline.reader.LineReader; import org.jline.reader.LineReaderBuilder; import org.jline.reader.impl.completer.StringsCompleter; import org.jline.reader.impl.history.DefaultHistory; import org.jline.terminal.TerminalBuilder; -import org.rascalmpl.debug.IRascalMonitor; import org.rascalmpl.exceptions.Throw; import org.rascalmpl.ideservices.BasicIDEServices; import org.rascalmpl.ideservices.IDEServices; @@ -48,14 +49,17 @@ import org.rascalmpl.interpreter.utils.RascalManifest; import org.rascalmpl.parser.gtd.exception.ParseError; import org.rascalmpl.repl.ReplTextWriter; -import org.rascalmpl.repl.TerminalProgressBarMonitor; import org.rascalmpl.repl.TerminalProgressBarMonitor2; import org.rascalmpl.uri.URIResolverRegistry; import org.rascalmpl.uri.URIUtil; +import org.rascalmpl.values.RascalValueFactory; import org.rascalmpl.values.ValueFactoryFactory; +import org.rascalmpl.values.parsetrees.TreeAdapter; +import io.usethesource.vallang.IConstructor; import io.usethesource.vallang.IValue; import io.usethesource.vallang.IValueFactory; +import io.usethesource.vallang.type.Type; public class RascalShell2 { @@ -105,22 +109,34 @@ public static void main(String[] args) throws IOException { var indentedPrettyPrinter = new ReplTextWriter(true); + while (true) { - String line = reader.readLine("rascal> "); + String line = reader.readLine(Ansi.ansi().reset().bold().toString() + "rascal> " + Ansi.ansi().boldOff().toString()); try { - Result value; + Result result; synchronized(evaluator) { - value = evaluator.eval(monitor, line, URIUtil.rootLocation("prompt")); + result = evaluator.eval(monitor, line, URIUtil.rootLocation("prompt")); evaluator.endAllJobs(); } - if (value.isVoid()) { - reader.printAbove("ok \n"); + if (result.isVoid()) { + monitor.println("ok"); } else { - reader.printAbove("Result: " + value.toString(1024)); + IValue value = result.getValue(); + Type type = result.getStaticType(); + + if (type.isAbstractData() && type.isStrictSubtypeOf(RascalValueFactory.Tree) && !type.isBottom()) { + monitor.write("(" + type.toString() +") `"); + TreeAdapter.yield((IConstructor)value, true, monitor); + monitor.write("`"); + } + else { + indentedPrettyPrinter.write(value, monitor); + } + monitor.println(); } } catch (InterruptException ie) { From b02f63f5675c9c10c9aadb604aca5c61a2b03b2e Mon Sep 17 00:00:00 2001 From: Davy Landman Date: Tue, 15 Oct 2024 17:35:09 +0200 Subject: [PATCH 04/35] Cleaning up pom to drop jline2 related dependencies --- pom.xml | 39 ++++++++++++--------------------------- 1 file changed, 12 insertions(+), 27 deletions(-) diff --git a/pom.xml b/pom.xml index 72d5503f652..efc23fa0b0d 100644 --- a/pom.xml +++ b/pom.xml @@ -33,7 +33,7 @@ 2 11 0.28.4 - 3.26.1 + 3.27.0 @@ -333,10 +333,6 @@ org.fusesource.jansi.internal.* - - jline - org.rascalmpl.jline - @@ -465,45 +461,34 @@ gson 2.10.1 - - jline - jline - 2.14.6 - provided - - - org.jline - jline-terminal-jni - ${jline.version} - - - org.jline - jline-terminal-jna - ${jline.version} - - + org.jline jline-reader ${jline.version} - + org.jline jline-terminal ${jline.version} - + + org.jline + jline-terminal-jni + ${jline.version} + + org.jline jline-style ${jline.version} - + org.jline jline-console ${jline.version} - + org.jline - jansi + jansi-core ${jline.version} From 57b43d18dfdb46e6fcab1af90ac02e8c4ee7bc5c Mon Sep 17 00:00:00 2001 From: Davy Landman Date: Tue, 15 Oct 2024 17:35:32 +0200 Subject: [PATCH 05/35] Removing InputStream and OutputStreams from the evaluator --- src/org/rascalmpl/checker/StaticChecker.java | 125 ------------------ src/org/rascalmpl/interpreter/Evaluator.java | 82 ++++-------- .../interpreter/IEvaluatorContext.java | 12 +- .../rascalmpl/interpreter/TestEvaluator.java | 8 +- .../interpreter/result/AbstractFunction.java | 12 +- .../interpreter/result/JavaMethod.java | 2 +- .../interpreter/utils/JavaBridge.java | 11 +- .../rascalmpl/interpreter/utils/Profiler.java | 4 +- src/org/rascalmpl/library/util/Eval.java | 14 +- .../rascalmpl/library/util/Reflective.java | 17 ++- src/org/rascalmpl/library/util/TermREPL.java | 33 +++-- src/org/rascalmpl/parser/ParserGenerator.java | 8 +- src/org/rascalmpl/repl/ILanguageProtocol.java | 5 +- .../rascalmpl/repl/RascalInterpreterREPL.java | 23 ++-- .../repl/TerminalProgressBarMonitor.java | 2 +- .../rascalmpl/semantics/dynamic/Import.java | 4 +- .../semantics/dynamic/ShellCommand.java | 6 +- src/org/rascalmpl/shell/ModuleRunner.java | 4 +- src/org/rascalmpl/shell/RascalShell2.java | 18 +-- ...ascalJUnitParallelRecursiveTestRunner.java | 5 +- .../infrastructure/RascalJUnitTestRunner.java | 4 +- .../test/infrastructure/TestFramework.java | 8 +- test/org/rascalmpl/MatchFingerprintTest.java | 3 +- .../ParallelEvaluatorsTests.java | 2 +- 24 files changed, 133 insertions(+), 279 deletions(-) delete mode 100644 src/org/rascalmpl/checker/StaticChecker.java diff --git a/src/org/rascalmpl/checker/StaticChecker.java b/src/org/rascalmpl/checker/StaticChecker.java deleted file mode 100644 index acb755fa842..00000000000 --- a/src/org/rascalmpl/checker/StaticChecker.java +++ /dev/null @@ -1,125 +0,0 @@ -/******************************************************************************* - * Copyright (c) 2009-2013 CWI - * All rights reserved. This program and the accompanying materials - * are made available under the terms of the Eclipse Public License v1.0 - * which accompanies this distribution, and is available at - * http://www.eclipse.org/legal/epl-v10.html - * - * Contributors: - * * Jurgen J. Vinju - Jurgen.Vinju@cwi.nl - CWI - * * Mark Hills - Mark.Hills@cwi.nl (CWI) - * * Arnold Lankamp - Arnold.Lankamp@cwi.nl -*******************************************************************************/ -package org.rascalmpl.checker; - -import java.io.OutputStream; -import java.util.HashMap; -import java.util.Map; - -import org.rascalmpl.debug.IRascalMonitor; -import org.rascalmpl.exceptions.ImplementationError; -import org.rascalmpl.interpreter.Configuration; -import org.rascalmpl.interpreter.Evaluator; -import org.rascalmpl.interpreter.env.GlobalEnvironment; -import org.rascalmpl.interpreter.env.ModuleEnvironment; -import org.rascalmpl.interpreter.load.StandardLibraryContributor; -import org.rascalmpl.parser.gtd.exception.ParseError; -import org.rascalmpl.uri.URIUtil; -import org.rascalmpl.values.ValueFactoryFactory; -import org.rascalmpl.values.parsetrees.ITree; - -import io.usethesource.vallang.ISourceLocation; -import io.usethesource.vallang.IValue; -import io.usethesource.vallang.IValueFactory; -import io.usethesource.vallang.type.Type; -import io.usethesource.vallang.type.TypeStore; - -public class StaticChecker { - private final Evaluator eval; - public static final String TYPECHECKER = "typecheckTree"; - private boolean checkerEnabled; - private boolean initialized; - private boolean loaded; - private Type pathConfigConstructor = null; - - public StaticChecker(OutputStream stderr, OutputStream stdout) { - GlobalEnvironment heap = new GlobalEnvironment(); - ModuleEnvironment root = heap.addModule(new ModuleEnvironment("$staticchecker$", heap)); - eval = new Evaluator(ValueFactoryFactory.getValueFactory(), System.in, stderr, stdout, root, heap, IRascalMonitor.buildConsoleMonitor(System.in, System.out)); - eval.addRascalSearchPathContributor(StandardLibraryContributor.getInstance()); - checkerEnabled = false; - initialized = false; - loaded = false; - } - - private IValue eval(IRascalMonitor monitor, String cmd) { - try { - return eval.eval(monitor, cmd, URIUtil.rootLocation("checker")).getValue(); - } catch (ParseError pe) { - throw new ImplementationError("syntax error in static checker modules", pe); - } - } - - public synchronized void load(IRascalMonitor monitor) { - eval(monitor, "import lang::rascal::types::CheckTypes;"); - eval(monitor, "import util::Reflective;"); - TypeStore ts = eval.getHeap().getModule("util::Reflective").getStore(); - pathConfigConstructor = ts.lookupConstructor(ts.lookupAbstractDataType("PathConfig"), "pathConfig").iterator().next(); - loaded = true; - } - - public void init() { - initialized = true; - } - - public Configuration getConfiguration() { - return eval.getConfiguration(); - } - - public boolean isInitialized() { - return initialized; - } - - public synchronized ITree checkModule(IRascalMonitor monitor, ISourceLocation module) { - if (checkerEnabled) { - return (ITree) eval.call(monitor, "check", module, getPathConfig()); - } - return null; - } - - private IValue getPathConfig() { - assert pathConfigConstructor != null; - IValueFactory vf = ValueFactoryFactory.getValueFactory(); - Map kwArgs = new HashMap<>(); - kwArgs.put("srcPath", vf.list(eval.getRascalResolver().collect().toArray(new IValue[0]))); - // default args for the rest - return vf.constructor(pathConfigConstructor, new IValue[0], kwArgs); - } - - public synchronized void disableChecker() { - checkerEnabled = false; - } - - public synchronized void enableChecker(IRascalMonitor monitor) { - if (!loaded) { - load(monitor); - } - checkerEnabled = true; - } - - public boolean isCheckerEnabled() { - return checkerEnabled; - } - - public void addRascalSearchPath(ISourceLocation uri) { - eval.addRascalSearchPath(uri); - } - - public void addClassLoader(ClassLoader classLoader) { - eval.addClassLoader(classLoader); - } - - public Evaluator getEvaluator() { - return eval; - } -} diff --git a/src/org/rascalmpl/interpreter/Evaluator.java b/src/org/rascalmpl/interpreter/Evaluator.java index ea38006cf6c..2b397e1034e 100755 --- a/src/org/rascalmpl/interpreter/Evaluator.java +++ b/src/org/rascalmpl/interpreter/Evaluator.java @@ -22,7 +22,6 @@ import static org.rascalmpl.semantics.dynamic.Import.parseFragments; import java.io.IOException; -import java.io.InputStream; import java.io.OutputStream; import java.io.OutputStreamWriter; import java.io.PrintWriter; @@ -195,17 +194,13 @@ public void decCallNesting() { private final List classLoaders; // sharable if frozen private final ModuleEnvironment rootScope; // sharable if frozen - private final OutputStream defStderr; - private final OutputStream defStdout; private final PrintWriter defOutWriter; private final PrintWriter defErrWriter; - private final InputStream defInput; + private final Reader defInput; - private OutputStream curStderr = null; - private OutputStream curStdout = null; private PrintWriter curOutWriter = null; private PrintWriter curErrWriter = null; - private InputStream curInput = null; + private Reader curInput = null; /** * Probably not sharable @@ -232,26 +227,26 @@ public void decCallNesting() { private static final Object dummy = new Object(); /** - * Promotes the monitor to the outputstream automatically if so required. + * Promotes the monitor to the PrintWriter automatically if so required. */ - public Evaluator(IValueFactory f, InputStream input, OutputStream stderr, OutputStream stdout, IRascalMonitor monitor, ModuleEnvironment scope, GlobalEnvironment heap) { - this(f, input, stderr, monitor instanceof OutputStream ? (OutputStream) monitor : stdout, scope, heap, new ArrayList(Collections.singleton(Evaluator.class.getClassLoader())), new RascalSearchPath()); + public Evaluator(IValueFactory f, Reader input, PrintWriter stderr, PrintWriter stdout, IRascalMonitor monitor, ModuleEnvironment scope, GlobalEnvironment heap) { + this(f, input, stderr, monitor instanceof PrintWriter ? (PrintWriter) monitor : stdout, scope, heap, new ArrayList(Collections.singleton(Evaluator.class.getClassLoader())), new RascalSearchPath()); } /** * If your monitor should wrap stdout (like TerminalProgressBarMonitor) then you can use this constructor. */ - public Evaluator(IValueFactory f, InputStream input, OutputStream stderr, M monitor, ModuleEnvironment scope, GlobalEnvironment heap) { + public Evaluator(IValueFactory f, Reader input, PrintWriter stderr, M monitor, ModuleEnvironment scope, GlobalEnvironment heap) { this(f, input, stderr, monitor, scope, heap, new ArrayList(Collections.singleton(Evaluator.class.getClassLoader())), new RascalSearchPath()); setMonitor(monitor); } - public Evaluator(IValueFactory f, InputStream input, OutputStream stderr, OutputStream stdout, ModuleEnvironment scope, GlobalEnvironment heap, IRascalMonitor monitor) { - this(f, input, stderr, monitor instanceof OutputStream ? (OutputStream) monitor : stdout, scope, heap, new ArrayList(Collections.singleton(Evaluator.class.getClassLoader())), new RascalSearchPath()); + public Evaluator(IValueFactory f, Reader input, PrintWriter stderr, PrintWriter stdout, ModuleEnvironment scope, GlobalEnvironment heap, IRascalMonitor monitor) { + this(f, input, stderr, monitor instanceof PrintWriter ? (PrintWriter) monitor : stdout, scope, heap, new ArrayList(Collections.singleton(Evaluator.class.getClassLoader())), new RascalSearchPath()); setMonitor(monitor); } - public Evaluator(IValueFactory vf, InputStream input, OutputStream stderr, OutputStream stdout, ModuleEnvironment scope, GlobalEnvironment heap, List classLoaders, RascalSearchPath rascalPathResolver) { + public Evaluator(IValueFactory vf, Reader input, PrintWriter stderr, PrintWriter stdout, ModuleEnvironment scope, GlobalEnvironment heap, List classLoaders, RascalSearchPath rascalPathResolver) { super(); this.vf = new RascalFunctionValueFactory(this); @@ -265,10 +260,8 @@ public Evaluator(IValueFactory vf, InputStream input, OutputStream stderr, Outpu this.rascalPathResolver = rascalPathResolver; this.resolverRegistry = rascalPathResolver.getRegistry(); this.defInput = input; - this.defStderr = stderr; - this.defStdout = stdout; - this.defErrWriter = wrapWriter(stderr, true); - this.defOutWriter = wrapWriter(stdout, false); + this.defErrWriter = stderr; + this.defOutWriter = stdout; this.constructorDeclaredListeners = new HashMap(); this.suspendTriggerListeners = new CopyOnWriteArrayList(); @@ -285,10 +278,6 @@ public Evaluator(IValueFactory vf, InputStream input, OutputStream stderr, Outpu setEventTrigger(AbstractInterpreterEventTrigger.newNullEventTrigger()); } - private static PrintWriter wrapWriter(OutputStream out, boolean autoFlush) { - return new PrintWriter(new OutputStreamWriter(out, StandardCharsets.UTF_8), autoFlush); - } - private Evaluator(Evaluator source, ModuleEnvironment scope) { super(); @@ -307,8 +296,6 @@ private Evaluator(Evaluator source, ModuleEnvironment scope) { this.javaBridge = new JavaBridge(classLoaders, vf, config); this.rascalPathResolver = source.rascalPathResolver; this.resolverRegistry = source.resolverRegistry; - this.defStderr = source.defStderr; - this.defStdout = source.defStdout; this.defInput = source.defInput; this.defErrWriter = source.defErrWriter; this.defOutWriter = source.defOutWriter; @@ -398,29 +385,20 @@ public List getClassLoaders() { return Collections.unmodifiableList(classLoaders); } - @Override + @Override public ModuleEnvironment __getRootScope() { return rootScope; } - @Override - public OutputStream getStdOut() { - return curStdout == null ? defStdout : curStdout; - } - @Override - public PrintWriter getOutPrinter() { - return curOutWriter == null ? defOutWriter : curOutWriter; - } - - @Override - public PrintWriter getErrorPrinter() { - return curErrWriter == null ? defErrWriter : curErrWriter; + public PrintWriter getStdOut() { + return curOutWriter == null ? defOutWriter : curOutWriter; } + @Override - public InputStream getInput() { + public Reader getInput() { return curInput == null ? defInput : curInput; } @@ -469,9 +447,9 @@ public boolean isInterrupted() { return interrupt; } - @Override - public OutputStream getStdErr() { - return curStderr == null ? defStderr : curStderr; + @Override + public PrintWriter getStdErr() { + return curErrWriter == null ? defErrWriter : curErrWriter; } public void setTestResultListener(ITestResultListener l) { @@ -629,7 +607,7 @@ else if (main.hasKeywordArguments() && main.getArity() == 0) { } } catch (MatchFailed e) { - getOutPrinter().println("Main function should either have a list[str] as a single parameter like so: \'void main(list[str] args)\', or a set of keyword parameters with defaults like so: \'void main(bool myOption=false, str input=\"\")\'"); + getStdOut().println("Main function should either have a list[str] as a single parameter like so: \'void main(list[str] args)\', or a set of keyword parameters with defaults like so: \'void main(bool myOption=false, str input=\"\")\'"); return null; } finally { @@ -831,7 +809,7 @@ public ParserGenerator getParserGenerator() { synchronized (self) { if (parserGenerator == null) { - parserGenerator = new ParserGenerator(getMonitor(), (monitor instanceof TerminalProgressBarMonitor) ? (OutputStream) getMonitor() : getStdErr(), classLoaders, getValueFactory(), config); + parserGenerator = new ParserGenerator(getMonitor(), (monitor instanceof TerminalProgressBarMonitor) ? (PrintWriter) getMonitor() : getStdErr(), classLoaders, getValueFactory(), config); } } } @@ -1220,13 +1198,13 @@ public Set reloadModules(IRascalMonitor monitor, Set names, ISou } private void reloadModules(IRascalMonitor monitor, Set names, ISourceLocation errorLocation, boolean recurseToExtending, Set affectedModules) { - SaveWarningsMonitor wrapped = new SaveWarningsMonitor(monitor, getErrorPrinter()); + SaveWarningsMonitor wrapped = new SaveWarningsMonitor(monitor, getStdErr()); IRascalMonitor old = setMonitor(wrapped); try { Set onHeap = new HashSet<>(); Set extendingModules = new HashSet<>(); - PrintWriter errStream = getErrorPrinter(); + PrintWriter errStream = getStdErr(); for (String mod : names) { if (heap.existsModule(mod)) { @@ -1513,7 +1491,7 @@ public boolean runTests(IRascalMonitor monitor) { IRascalMonitor old = setMonitor(monitor); try { final boolean[] allOk = new boolean[] { true }; - final ITestResultListener l = testReporter != null ? testReporter : new DefaultTestResultListener(getOutPrinter()); + final ITestResultListener l = testReporter != null ? testReporter : new DefaultTestResultListener(getStdErr()); new TestEvaluator(this, new ITestResultListener() { @@ -1578,17 +1556,13 @@ public IRascalMonitor getMonitor() { return new NullRascalMonitor(); } - public void overrideDefaultWriters(InputStream newInput, OutputStream newStdOut, OutputStream newStdErr) { - this.curStdout = newStdOut; - this.curStderr = newStdErr; + public void overrideDefaultWriters(Reader newInput, PrintWriter newStdOut, PrintWriter newStdErr) { this.curInput = newInput; - this.curErrWriter = wrapWriter(newStdErr, true); - this.curOutWriter = wrapWriter(newStdOut, false); + this.curErrWriter = newStdErr; + this.curOutWriter = newStdOut; } public void revertToDefaultWriters() { - this.curStderr = null; - this.curStdout = null; this.curInput = null; if (curOutWriter != null) { curOutWriter.flush(); @@ -1985,7 +1959,7 @@ public void showMessage(IConstructor message) { } - public void overwritePrintStream(PrintWriter outWriter, PrintWriter errWriter) { + public void overwritePrintWriter(PrintWriter outWriter, PrintWriter errWriter) { this.curOutWriter = outWriter; this.curErrWriter = errWriter; } diff --git a/src/org/rascalmpl/interpreter/IEvaluatorContext.java b/src/org/rascalmpl/interpreter/IEvaluatorContext.java index 49db817c31b..6ffa2197096 100644 --- a/src/org/rascalmpl/interpreter/IEvaluatorContext.java +++ b/src/org/rascalmpl/interpreter/IEvaluatorContext.java @@ -17,9 +17,8 @@ *******************************************************************************/ package org.rascalmpl.interpreter; -import java.io.InputStream; -import java.io.OutputStream; import java.io.PrintWriter; +import java.io.Reader; import java.util.Collection; import java.util.Stack; @@ -41,13 +40,10 @@ public interface IEvaluatorContext extends IRascalMonitor { public StackTrace getStackTrace(); /** for standard IO */ - public PrintWriter getOutPrinter(); - public PrintWriter getErrorPrinter(); - - public OutputStream getStdOut(); - public OutputStream getStdErr(); + public PrintWriter getStdOut(); + public PrintWriter getStdErr(); - public InputStream getInput(); + public Reader getInput(); /** for "internal use" */ public IEvaluator> getEvaluator(); diff --git a/src/org/rascalmpl/interpreter/TestEvaluator.java b/src/org/rascalmpl/interpreter/TestEvaluator.java index 726921716c2..cab11b6268f 100644 --- a/src/org/rascalmpl/interpreter/TestEvaluator.java +++ b/src/org/rascalmpl/interpreter/TestEvaluator.java @@ -130,8 +130,8 @@ private void runTests(ModuleEnvironment env, List tests) { } }, env.getRoot().getStore(), tries, maxDepth, maxWidth); - eval.getOutPrinter().flush(); - eval.getErrorPrinter().flush(); + eval.getStdOut().flush(); + eval.getStdErr().flush(); if (!result.succeeded()) { StringWriter sw = new StringWriter(); @@ -147,8 +147,8 @@ private void runTests(ModuleEnvironment env, List tests) { testResultListener.report(false, test.getName(), test.getAst().getLocation(), e.getMessage(), e); } - eval.getOutPrinter().flush(); - eval.getErrorPrinter().flush(); + eval.getStdOut().flush(); + eval.getStdErr().flush(); } testResultListener.done(); diff --git a/src/org/rascalmpl/interpreter/result/AbstractFunction.java b/src/org/rascalmpl/interpreter/result/AbstractFunction.java index 326a1a19725..e458f680a10 100644 --- a/src/org/rascalmpl/interpreter/result/AbstractFunction.java +++ b/src/org/rascalmpl/interpreter/result/AbstractFunction.java @@ -293,8 +293,8 @@ protected void printStartTrace(IValue[] actuals) { b.append("call >"); printNesting(b); printHeader(b, actuals); - eval.getOutPrinter().println(b.toString()); - eval.getOutPrinter().flush(); + eval.getStdOut().println(b.toString()); + eval.getStdOut().flush(); eval.incCallNesting(); } @@ -318,8 +318,8 @@ protected void printExcept(Throwable e) { b.append(": "); String msg = e.getMessage(); b.append(msg == null ? e.getClass().getSimpleName() : msg); - eval.getOutPrinter().println(b.toString()); - eval.getOutPrinter().flush(); + eval.getStdOut().println(b.toString()); + eval.getStdOut().flush(); } } @@ -336,8 +336,8 @@ protected void printEndTrace(IValue result) { b.append(strval(result)); } - eval.getOutPrinter().println(b); - eval.getOutPrinter().flush(); + eval.getStdOut().println(b); + eval.getStdOut().flush(); } } diff --git a/src/org/rascalmpl/interpreter/result/JavaMethod.java b/src/org/rascalmpl/interpreter/result/JavaMethod.java index 10530bda752..1ce4fd9c8c6 100644 --- a/src/org/rascalmpl/interpreter/result/JavaMethod.java +++ b/src/org/rascalmpl/interpreter/result/JavaMethod.java @@ -91,7 +91,7 @@ private JavaMethod(IEvaluator> eval, Type staticType, Type dynami super(func, eval, staticType, dynamicType, getFormals(func), Names.name(func.getSignature().getName()), isDefault, isTest, varargs, env); this.javaBridge = javaBridge; this.hasReflectiveAccess = hasReflectiveAccess(func); - this.instance = javaBridge.getJavaClassInstance(func, eval.getMonitor(), env.getStore(), eval.getOutPrinter(), eval.getErrorPrinter(), eval.getStdOut(), eval.getStdErr(), eval.getInput(), eval); + this.instance = javaBridge.getJavaClassInstance(func, eval.getMonitor(), env.getStore(), eval.getStdOut(), eval.getStdErr(), eval.getInput(), eval); this.method = javaBridge.lookupJavaMethod(eval, func, env, hasReflectiveAccess); } diff --git a/src/org/rascalmpl/interpreter/utils/JavaBridge.java b/src/org/rascalmpl/interpreter/utils/JavaBridge.java index 201703578f1..bfe2049dd53 100644 --- a/src/org/rascalmpl/interpreter/utils/JavaBridge.java +++ b/src/org/rascalmpl/interpreter/utils/JavaBridge.java @@ -19,6 +19,7 @@ import java.io.InputStream; import java.io.OutputStream; import java.io.PrintWriter; +import java.io.Reader; import java.lang.reflect.Constructor; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; @@ -375,14 +376,11 @@ public Class visitFunction(Type type) throws RuntimeException { } } - public synchronized Object getJavaClassInstance(FunctionDeclaration func, IRascalMonitor monitor, TypeStore store, PrintWriter out, PrintWriter err, OutputStream rawOut, OutputStream rawErr, InputStream in, IEvaluatorContext ctx) { + public synchronized Object getJavaClassInstance(FunctionDeclaration func, IRascalMonitor monitor, TypeStore store, PrintWriter out, PrintWriter err, Reader in, IEvaluatorContext ctx) { String className = getClassName(func); PrintWriter[] outputs = new PrintWriter[] { out, err }; int writers = 0; - - OutputStream[] rawOutputs = new OutputStream[] { rawOut, rawErr }; - int rawWriters = 0; try { for(ClassLoader loader : loaders){ @@ -425,10 +423,7 @@ else if (formals[i].isAssignableFrom(TypeFactory.class)) { else if (formals[i].isAssignableFrom(PrintWriter.class)) { args[i] = outputs[writers++ % 2]; } - else if (formals[i].isAssignableFrom(OutputStream.class)) { - args[i] = rawOutputs[rawWriters++ %2]; - } - else if (formals[i].isAssignableFrom(InputStream.class)) { + else if (formals[i].isAssignableFrom(Reader.class)) { args[i] = in; } else if (formals[i].isAssignableFrom(IRascalMonitor.class)) { diff --git a/src/org/rascalmpl/interpreter/utils/Profiler.java b/src/org/rascalmpl/interpreter/utils/Profiler.java index 40eba860655..0be9d9c7323 100644 --- a/src/org/rascalmpl/interpreter/utils/Profiler.java +++ b/src/org/rascalmpl/interpreter/utils/Profiler.java @@ -160,7 +160,7 @@ public IList getProfileData(){ public void report() { report("FRAMES", frame); - eval.getOutPrinter().println(); + eval.getStdOut().println(); report("ASTS", ast); } @@ -178,7 +178,7 @@ private void report(String title, Map data) { nTicks += e.getValue().getTicks(); } - PrintWriter out = eval.getOutPrinter(); + PrintWriter out = eval.getStdOut(); String nameFormat = "%" + maxName + "s"; out.printf(title + " PROFILE: %d data points, %d ticks, tick = %d milliSecs\n", ast.size(), nTicks, resolution); out.printf(nameFormat + "%8s%9s %s\n", " Scope", "Ticks", "%", "Source"); diff --git a/src/org/rascalmpl/library/util/Eval.java b/src/org/rascalmpl/library/util/Eval.java index 6da394f8621..dd2835f8f46 100644 --- a/src/org/rascalmpl/library/util/Eval.java +++ b/src/org/rascalmpl/library/util/Eval.java @@ -16,8 +16,8 @@ import java.io.File; import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; +import java.io.PrintWriter; +import java.io.Reader; import java.net.URISyntaxException; import java.nio.file.Paths; import java.util.HashMap; @@ -80,12 +80,12 @@ public class Eval { private final Type execConstructor; /* the following four fields are inherited by the configuration of nested evaluators */ - private final OutputStream stderr; - private final OutputStream stdout; - private final InputStream input; + private final PrintWriter stderr; + private final PrintWriter stdout; + private final Reader input; private final IDEServices services; - public Eval(IRascalValueFactory values, OutputStream out, OutputStream err, InputStream in, ClassLoader loader, IDEServices services, TypeStore ts) { + public Eval(IRascalValueFactory values, PrintWriter out, PrintWriter err, Reader in, ClassLoader loader, IDEServices services, TypeStore ts) { super(); this.values = values; this.tr = new TypeReifier(values); @@ -219,7 +219,7 @@ private static class RascalRuntime { private final Evaluator eval; private int duration = -1; - public RascalRuntime(PathConfig pcfg, InputStream input, OutputStream stderr, OutputStream stdout, IDEServices services) throws IOException, URISyntaxException{ + public RascalRuntime(PathConfig pcfg, Reader input, PrintWriter stderr, PrintWriter stdout, IDEServices services) throws IOException, URISyntaxException{ GlobalEnvironment heap = new GlobalEnvironment(); ModuleEnvironment root = heap.addModule(new ModuleEnvironment(ModuleEnvironment.SHELL_MODULE, heap)); IValueFactory vf = ValueFactoryFactory.getValueFactory(); diff --git a/src/org/rascalmpl/library/util/Reflective.java b/src/org/rascalmpl/library/util/Reflective.java index 44bb23ce3b1..371d60474bc 100644 --- a/src/org/rascalmpl/library/util/Reflective.java +++ b/src/org/rascalmpl/library/util/Reflective.java @@ -99,21 +99,23 @@ public IConstructor getProjectPathConfig(ISourceLocation projectRoot, IConstruct } } - IEvaluator getDefaultEvaluator(OutputStream stdout, OutputStream stderr) { + IEvaluator getDefaultEvaluator(PrintWriter stdout, PrintWriter stderr) { GlobalEnvironment heap = new GlobalEnvironment(); ModuleEnvironment root = heap.addModule(new ModuleEnvironment(ModuleEnvironment.SHELL_MODULE, heap)); IValueFactory vf = ValueFactoryFactory.getValueFactory(); - Evaluator evaluator = new Evaluator(vf, System.in, stderr, stdout, root, heap, monitor); + Evaluator evaluator = new Evaluator(vf, Reader.nullReader(), stderr, stdout, root, heap, monitor); evaluator.addRascalSearchPathContributor(StandardLibraryContributor.getInstance()); return evaluator; } public IList evalCommands(IList commands, ISourceLocation loc) { - OutputStream out = new ByteArrayOutputStream(); - OutputStream err = new ByteArrayOutputStream(); + StringWriter out = new StringWriter(); + StringWriter err = new StringWriter(); + PrintWriter outStream = new PrintWriter(out); + PrintWriter errStream = new PrintWriter(err); IListWriter result = values.listWriter(); - IEvaluator evaluator = getDefaultEvaluator(out, err); + IEvaluator evaluator = getDefaultEvaluator(outStream, errStream); int outOffset = 0; int errOffset = 0; @@ -125,11 +127,16 @@ public IList evalCommands(IList commands, ISourceLocation loc) { x = evaluator.eval(evaluator.getMonitor(), ((IString)v).getValue(), loc); } catch (Throwable e) { + errStream.flush(); errOut = err.toString().substring(errOffset); errOffset += errOut.length(); errOut += e.getMessage(); exc = true; } + finally { + outStream.flush(); + errStream.flush(); + } String output = out.toString().substring(outOffset); outOffset += output.length(); if (!exc) { diff --git a/src/org/rascalmpl/library/util/TermREPL.java b/src/org/rascalmpl/library/util/TermREPL.java index f149bb36340..d6728583314 100644 --- a/src/org/rascalmpl/library/util/TermREPL.java +++ b/src/org/rascalmpl/library/util/TermREPL.java @@ -6,6 +6,8 @@ import java.io.InputStream; import java.io.OutputStream; import java.io.OutputStreamWriter; +import java.io.PrintWriter; +import java.io.Reader; import java.io.UnsupportedEncodingException; import java.nio.charset.Charset; import java.util.ArrayList; @@ -13,6 +15,7 @@ import java.util.Map; import java.util.function.Function; + import org.rascalmpl.exceptions.RuntimeExceptionFactory; import org.rascalmpl.ideservices.IDEServices; import org.rascalmpl.interpreter.Evaluator; @@ -42,17 +45,16 @@ import io.usethesource.vallang.IWithKeywordParameters; import io.usethesource.vallang.type.Type; import io.usethesource.vallang.type.TypeFactory; -import jline.TerminalFactory; public class TermREPL { private final IRascalValueFactory vf; private ILanguageProtocol lang; - private final OutputStream out; - private final OutputStream err; - private final InputStream in; + private final PrintWriter out; + private final PrintWriter err; + private final Reader in; - public TermREPL(IRascalValueFactory vf, OutputStream out, OutputStream err, InputStream in) { + public TermREPL(IRascalValueFactory vf, PrintWriter out, PrintWriter err, Reader in) { this.vf = vf; this.out = out; this.err = err; @@ -94,9 +96,9 @@ public ITuple newREPL(IConstructor repl, IString title, IString welcome, IString public static class TheREPL implements ILanguageProtocol { private final REPLContentServerManager contentManager = new REPLContentServerManager(); private final TypeFactory tf = TypeFactory.getInstance(); - private OutputStream stdout; - private OutputStream stderr; - private InputStream input; + private PrintWriter stdout; + private PrintWriter stderr; + private Reader input; private String currentPrompt; private String quit; private final AbstractFunction handler; @@ -105,7 +107,7 @@ public static class TheREPL implements ILanguageProtocol { private final AbstractFunction stacktrace; public TheREPL(IValueFactory vf, IString title, IString welcome, IString prompt, IString quit, ISourceLocation history, - IFunction handler, IFunction completor, IValue stacktrace, InputStream input, OutputStream stderr, OutputStream stdout) { + IFunction handler, IFunction completor, IValue stacktrace, Reader input, PrintWriter stderr, PrintWriter stdout) { this.vf = vf; this.input = input; this.stderr = stderr; @@ -145,7 +147,7 @@ public void stackTraceRequested() { } @Override - public void initialize(InputStream input, OutputStream stdout, OutputStream stderr, IDEServices services) { + public void initialize(Reader input, PrintWriter stdout, PrintWriter stderr, IDEServices services) { this.stdout = stdout; this.stderr = stderr; this.input = input; @@ -293,14 +295,9 @@ private IValue call(IFunction f, Type[] types, IValue[] args) { return f.call(args); } finally { - try { - stdout.flush(); - stderr.flush(); - eval.revertToDefaultWriters(); - } - catch (IOException e) { - // ignore - } + stdout.flush(); + stderr.flush(); + eval.revertToDefaultWriters(); } } } diff --git a/src/org/rascalmpl/parser/ParserGenerator.java b/src/org/rascalmpl/parser/ParserGenerator.java index 19b55ae06bb..514f917dcd1 100644 --- a/src/org/rascalmpl/parser/ParserGenerator.java +++ b/src/org/rascalmpl/parser/ParserGenerator.java @@ -17,6 +17,8 @@ import java.io.FileOutputStream; import java.io.IOException; import java.io.OutputStream; +import java.io.PrintWriter; +import java.io.Reader; import java.util.List; import org.rascalmpl.debug.IRascalMonitor; @@ -52,10 +54,10 @@ public class ParserGenerator { private static final String packageName = "org.rascalmpl.java.parser.object"; private static final boolean debug = false; - public ParserGenerator(IRascalMonitor monitor, OutputStream out, List loaders, IValueFactory factory, Configuration config) { + public ParserGenerator(IRascalMonitor monitor, PrintWriter out, List loaders, IValueFactory factory, Configuration config) { GlobalEnvironment heap = new GlobalEnvironment(); ModuleEnvironment scope = new ModuleEnvironment("$parsergenerator$", heap); - this.evaluator = new Evaluator(ValueFactoryFactory.getValueFactory(), System.in, out, out, scope, heap, monitor); + this.evaluator = new Evaluator(ValueFactoryFactory.getValueFactory(), Reader.nullReader(), out, out, scope, heap, monitor); this.evaluator.getConfiguration().setRascalJavaClassPathProperty(config.getRascalJavaClassPathProperty()); this.evaluator.getConfiguration().setGeneratorProfiling(config.getGeneratorProfilingProperty()); evaluator.addRascalSearchPathContributor(StandardLibraryContributor.getInstance()); @@ -200,7 +202,7 @@ public Class> getNewParser(IRascalMon finally { if (profiler != null) { profiler.pleaseStop(); - evaluator.getOutPrinter().println("PROFILE:"); + evaluator.getStdOut().println("PROFILE:"); profiler.report(); profiler = null; } diff --git a/src/org/rascalmpl/repl/ILanguageProtocol.java b/src/org/rascalmpl/repl/ILanguageProtocol.java index 9a32ca88762..9da826d0389 100755 --- a/src/org/rascalmpl/repl/ILanguageProtocol.java +++ b/src/org/rascalmpl/repl/ILanguageProtocol.java @@ -24,7 +24,8 @@ package org.rascalmpl.repl; import java.io.InputStream; -import java.io.OutputStream; +import java.io.PrintWriter; +import java.io.Reader; import java.util.Map; import org.rascalmpl.ideservices.IDEServices; @@ -37,7 +38,7 @@ public interface ILanguageProtocol { * @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. */ - void initialize(InputStream input, OutputStream stdout, OutputStream stderr, IDEServices services); + void initialize(Reader input, PrintWriter stdout, PrintWriter stderr, IDEServices services); /** * Will be called everytime a new prompt is printed. diff --git a/src/org/rascalmpl/repl/RascalInterpreterREPL.java b/src/org/rascalmpl/repl/RascalInterpreterREPL.java index 17febce23d5..2c06ecdfe83 100644 --- a/src/org/rascalmpl/repl/RascalInterpreterREPL.java +++ b/src/org/rascalmpl/repl/RascalInterpreterREPL.java @@ -10,6 +10,7 @@ import java.io.InputStream; import java.io.OutputStream; import java.io.PrintWriter; +import java.io.Reader; import java.io.Writer; import java.net.URISyntaxException; import java.util.Collection; @@ -71,7 +72,7 @@ public boolean getMeasureCommandTime() { } @Override - public void initialize(InputStream input, OutputStream stdout, OutputStream stderr, IDEServices ideServices) { + public void initialize(Reader input, PrintWriter stdout, PrintWriter stderr, IDEServices ideServices) { eval = constructEvaluator(input, stdout, stderr, ideServices); } @@ -79,7 +80,7 @@ public void initialize(InputStream input, OutputStream stdout, OutputStream stde @Override public PrintWriter getErrorWriter() { - return eval.getErrorPrinter(); + return eval.getStdErr(); } @Override @@ -89,7 +90,7 @@ public InputStream getInput() { @Override public PrintWriter getOutputWriter() { - return eval.getOutPrinter(); + return eval.getStdOut(); } @Override @@ -135,37 +136,37 @@ public IRascalResult evalStatement(String statement, String lastLine) throws Int duration = tm.duration(); } if (measureCommandTime) { - eval.getErrorPrinter().println("\nTime: " + duration + "ms"); + eval.getStdErr().println("\nTime: " + duration + "ms"); } return value; } catch (InterruptException ie) { - eval.getErrorPrinter().println("Interrupted"); + eval.getStdErr().println("Interrupted"); try { - ie.getRascalStackTrace().prettyPrintedString(eval.getErrorPrinter(), indentedPrettyPrinter); + ie.getRascalStackTrace().prettyPrintedString(eval.getStdErr(), indentedPrettyPrinter); } catch (IOException e) { } return null; } catch (ParseError pe) { - parseErrorMessage(eval.getErrorPrinter(), lastLine, "prompt", pe, indentedPrettyPrinter); + parseErrorMessage(eval.getStdErr(), lastLine, "prompt", pe, indentedPrettyPrinter); return null; } catch (StaticError e) { - staticErrorMessage(eval.getErrorPrinter(),e, indentedPrettyPrinter); + staticErrorMessage(eval.getStdErr(),e, indentedPrettyPrinter); return null; } catch (Throw e) { - throwMessage(eval.getErrorPrinter(),e, indentedPrettyPrinter); + throwMessage(eval.getStdErr(),e, indentedPrettyPrinter); return null; } catch (QuitException q) { - eval.getErrorPrinter().println("Quiting REPL"); + eval.getStdErr().println("Quiting REPL"); throw new InterruptedException(); } catch (Throwable e) { - throwableMessage(eval.getErrorPrinter(), e, eval.getStackTrace(), indentedPrettyPrinter); + throwableMessage(eval.getStdErr(), e, eval.getStackTrace(), indentedPrettyPrinter); return null; } } diff --git a/src/org/rascalmpl/repl/TerminalProgressBarMonitor.java b/src/org/rascalmpl/repl/TerminalProgressBarMonitor.java index f2c349fb2cf..9d8b10b8214 100644 --- a/src/org/rascalmpl/repl/TerminalProgressBarMonitor.java +++ b/src/org/rascalmpl/repl/TerminalProgressBarMonitor.java @@ -31,7 +31,7 @@ * This class only works correctly if the actual _raw_ output stream of the terminal is wrapped * with an object of this class. */ -public class TerminalProgressBarMonitor extends FilterOutputStream implements IRascalMonitor { +public class TerminalProgressBarMonitor extends PrintWriter implements IRascalMonitor { /** * We administrate an ordered list of named bars, which will be printed from * top to bottom just above the next prompt. diff --git a/src/org/rascalmpl/semantics/dynamic/Import.java b/src/org/rascalmpl/semantics/dynamic/Import.java index ebb4656494a..6ecd620f373 100644 --- a/src/org/rascalmpl/semantics/dynamic/Import.java +++ b/src/org/rascalmpl/semantics/dynamic/Import.java @@ -250,7 +250,7 @@ else if (eval.getCurrentEnvt() == eval.__getRootScope()) { addImportToCurrentModule(src, name, eval); if (heap.getModule(name).isDeprecated()) { - eval.getErrorPrinter().println(src + ":" + name + " is deprecated, " + heap.getModule(name).getDeprecatedMessage()); + eval.getStdErr().println(src + ":" + name + " is deprecated, " + heap.getModule(name).getDeprecatedMessage()); } return; @@ -312,7 +312,7 @@ public static ModuleEnvironment loadModule(ISourceLocation x, String name, IEval Module module = buildModule(uri, env, eval, jobName); if (isDeprecated(module)) { - eval.getErrorPrinter().println("WARNING: deprecated module " + name + ":" + getDeprecatedMessage(module)); + eval.getStdErr().println("WARNING: deprecated module " + name + ":" + getDeprecatedMessage(module)); } if (module != null) { diff --git a/src/org/rascalmpl/semantics/dynamic/ShellCommand.java b/src/org/rascalmpl/semantics/dynamic/ShellCommand.java index 8aecec7932a..b57e5efd2f7 100644 --- a/src/org/rascalmpl/semantics/dynamic/ShellCommand.java +++ b/src/org/rascalmpl/semantics/dynamic/ShellCommand.java @@ -44,14 +44,14 @@ public Result interpret(IEvaluator> __eval) { ISourceLocation uri = __eval.getRascalResolver().resolveModule(name); if (uri == null) { - __eval.getErrorPrinter().println("module " + name + " can not be found in the search path."); + __eval.getStdErr().println("module " + name + " can not be found in the search path."); } else { services.edit(uri); } } else { - __eval.getErrorPrinter().println("The current Rascal execution environment does not know how to start an editor."); + __eval.getStdErr().println("The current Rascal execution environment does not know how to start an editor."); } return org.rascalmpl.interpreter.result.ResultFactory.nothing(); @@ -81,7 +81,7 @@ public Help(ISourceLocation __param1, IConstructor tree) { public Result interpret(IEvaluator> __eval) { __eval.setCurrentAST(this); - __eval.printHelpMessage(__eval.getOutPrinter()); + __eval.printHelpMessage(__eval.getStdOut()); return org.rascalmpl.interpreter.result.ResultFactory.nothing(); } diff --git a/src/org/rascalmpl/shell/ModuleRunner.java b/src/org/rascalmpl/shell/ModuleRunner.java index 7d78fd3c00b..450375f1335 100644 --- a/src/org/rascalmpl/shell/ModuleRunner.java +++ b/src/org/rascalmpl/shell/ModuleRunner.java @@ -34,8 +34,8 @@ public void run(String args[]) throws IOException { IValue v = eval.main(eval.getMonitor(), module, "main", realArgs); if (v != null && !(v instanceof IInteger)) { - new StandardTextWriter(true).write(v, eval.getOutPrinter()); - eval.getOutPrinter().flush(); + new StandardTextWriter(true).write(v, eval.getStdOut()); + eval.getStdOut().flush(); } System.exit(v instanceof IInteger ? ((IInteger) v).intValue() : 0); diff --git a/src/org/rascalmpl/shell/RascalShell2.java b/src/org/rascalmpl/shell/RascalShell2.java index a850b5759ab..616cc4bea9a 100644 --- a/src/org/rascalmpl/shell/RascalShell2.java +++ b/src/org/rascalmpl/shell/RascalShell2.java @@ -34,6 +34,7 @@ import org.jline.reader.LineReaderBuilder; import org.jline.reader.impl.completer.StringsCompleter; import org.jline.reader.impl.history.DefaultHistory; +import org.jline.style.StyleResolver; import org.jline.terminal.TerminalBuilder; import org.rascalmpl.exceptions.Throw; import org.rascalmpl.ideservices.BasicIDEServices; @@ -102,14 +103,13 @@ public static void main(String[] args) throws IOException { ModuleEnvironment root = heap.addModule(new ModuleEnvironment(ModuleEnvironment.SHELL_MODULE, heap)); IValueFactory vf = ValueFactoryFactory.getValueFactory(); Evaluator evaluator = new Evaluator(vf, new NullInputStream(), OutputStream.nullOutputStream(), OutputStream.nullOutputStream(), root, heap, monitor); - evaluator.overwritePrintStream(monitor, new PrintWriter(monitor, true)); + evaluator.overwritePrintWriter(monitor, new PrintWriter(monitor, true)); evaluator.addRascalSearchPathContributor(StandardLibraryContributor.getInstance()); URIResolverRegistry reg = URIResolverRegistry.getInstance(); var indentedPrettyPrinter = new ReplTextWriter(true); - while (true) { String line = reader.readLine(Ansi.ansi().reset().bold().toString() + "rascal> " + Ansi.ansi().boldOff().toString()); try { @@ -142,26 +142,26 @@ public static void main(String[] args) throws IOException { catch (InterruptException ie) { reader.printAbove("Interrupted"); try { - ie.getRascalStackTrace().prettyPrintedString(evaluator.getErrorPrinter(), indentedPrettyPrinter); + ie.getRascalStackTrace().prettyPrintedString(evaluator.getStdErr(), indentedPrettyPrinter); } catch (IOException e) { } } catch (ParseError pe) { - parseErrorMessage(evaluator.getErrorPrinter(), line, "prompt", pe, indentedPrettyPrinter); + parseErrorMessage(evaluator.getStdErr(), line, "prompt", pe, indentedPrettyPrinter); } catch (StaticError e) { - staticErrorMessage(evaluator.getErrorPrinter(),e, indentedPrettyPrinter); + staticErrorMessage(evaluator.getStdErr(),e, indentedPrettyPrinter); } catch (Throw e) { - throwMessage(evaluator.getErrorPrinter(),e, indentedPrettyPrinter); + throwMessage(evaluator.getStdErr(),e, indentedPrettyPrinter); } catch (QuitException q) { reader.printAbove("Quiting REPL"); break; } catch (Throwable e) { - throwableMessage(evaluator.getErrorPrinter(), e, evaluator.getStackTrace(), indentedPrettyPrinter); + throwableMessage(evaluator.getStdErr(), e, evaluator.getStackTrace(), indentedPrettyPrinter); } } System.exit(0); @@ -230,11 +230,11 @@ public void flush() throws IOException { } - private static class FakePrintStream extends PrintWriter { + private static class FakePrintWriter extends PrintWriter { private final LineReader target; private final CharBuffer buffer; - public FakePrintStream(LineReader target, boolean autoFlush) { + public FakePrintWriter(LineReader target, boolean autoFlush) { super(OutputStream.nullOutputStream(), autoFlush, StandardCharsets.UTF_8); this.target = target; this.buffer = CharBuffer.allocate(8*1024); diff --git a/src/org/rascalmpl/test/infrastructure/RascalJUnitParallelRecursiveTestRunner.java b/src/org/rascalmpl/test/infrastructure/RascalJUnitParallelRecursiveTestRunner.java index bc27c37e117..5fc187e855e 100644 --- a/src/org/rascalmpl/test/infrastructure/RascalJUnitParallelRecursiveTestRunner.java +++ b/src/org/rascalmpl/test/infrastructure/RascalJUnitParallelRecursiveTestRunner.java @@ -13,6 +13,7 @@ import java.io.IOException; import java.io.PrintWriter; +import java.io.Reader; import java.lang.annotation.Annotation; import java.util.ArrayList; import java.util.Collections; @@ -257,7 +258,7 @@ private void processModules() { synchronized(stdout) { evaluator.warning("Could not import " + module + " for testing...", null); evaluator.warning(e.getMessage(), null); - e.printStackTrace(evaluator.getOutPrinter()); + e.printStackTrace(evaluator.getStdOut()); } // register a failing module to make sure we report failure later on. @@ -300,7 +301,7 @@ private void initializeEvaluator() { heap = new GlobalEnvironment(); root = heap.addModule(new ModuleEnvironment("___junit_test___", heap)); - evaluator = new Evaluator(ValueFactoryFactory.getValueFactory(), System.in, System.err, System.out, root, heap, RascalJUnitTestRunner.getCommonMonitor()); + evaluator = new Evaluator(ValueFactoryFactory.getValueFactory(), Reader.nullReader(), new PrintWriter(System.err, true), new PrintWriter(System.out, false), root, heap, RascalJUnitTestRunner.getCommonMonitor()); stdout = new PrintWriter(evaluator.getStdOut()); stderr = new PrintWriter(evaluator.getStdErr()); diff --git a/src/org/rascalmpl/test/infrastructure/RascalJUnitTestRunner.java b/src/org/rascalmpl/test/infrastructure/RascalJUnitTestRunner.java index e2330988525..87843801f92 100644 --- a/src/org/rascalmpl/test/infrastructure/RascalJUnitTestRunner.java +++ b/src/org/rascalmpl/test/infrastructure/RascalJUnitTestRunner.java @@ -12,6 +12,8 @@ import java.io.File; import java.io.IOException; +import java.io.PrintWriter; +import java.io.Reader; import java.lang.annotation.Annotation; import java.net.URISyntaxException; import java.util.ArrayList; @@ -73,7 +75,7 @@ public static IRascalMonitor getCommonMonitor() { heap = new GlobalEnvironment(); root = heap.addModule(new ModuleEnvironment("___junit_test___", heap)); - evaluator = new Evaluator(ValueFactoryFactory.getValueFactory(), System.in, System.err, System.out, root, heap, getCommonMonitor()); + evaluator = new Evaluator(ValueFactoryFactory.getValueFactory(), Reader.nullReader(), new PrintWriter(System.err, true), new PrintWriter(System.out, false), root, heap, getCommonMonitor()); evaluator.addRascalSearchPathContributor(StandardLibraryContributor.getInstance()); evaluator.getConfiguration().setErrors(true); diff --git a/src/org/rascalmpl/test/infrastructure/TestFramework.java b/src/org/rascalmpl/test/infrastructure/TestFramework.java index ace8f0f08d5..12ae1d0a210 100644 --- a/src/org/rascalmpl/test/infrastructure/TestFramework.java +++ b/src/org/rascalmpl/test/infrastructure/TestFramework.java @@ -22,12 +22,14 @@ import java.io.IOException; import java.io.OutputStream; import java.io.PrintWriter; +import java.io.Reader; import java.net.URISyntaxException; import java.nio.charset.StandardCharsets; import java.util.HashSet; import java.util.Set; import org.junit.After; +import org.rascalmpl.ast.Char; import org.rascalmpl.interpreter.Evaluator; import org.rascalmpl.interpreter.env.GlobalEnvironment; import org.rascalmpl.interpreter.env.ModuleEnvironment; @@ -56,10 +58,10 @@ public class TestFramework { heap = new GlobalEnvironment(); root = heap.addModule(new ModuleEnvironment("___test___", heap)); - evaluator = new Evaluator(ValueFactoryFactory.getValueFactory(), System.in, System.err, System.out, RascalJUnitTestRunner.getCommonMonitor(), root, heap); + evaluator = new Evaluator(ValueFactoryFactory.getValueFactory(), Reader.nullReader(), new PrintWriter(System.err, true), new PrintWriter(System.out, false), RascalJUnitTestRunner.getCommonMonitor(), root, heap); - stdout = evaluator.getOutPrinter(); - stderr = evaluator.getErrorPrinter(); + stdout = evaluator.getStdOut(); + stderr = evaluator.getStdErr(); evaluator.addRascalSearchPathContributor(StandardLibraryContributor.getInstance()); diff --git a/test/org/rascalmpl/MatchFingerprintTest.java b/test/org/rascalmpl/MatchFingerprintTest.java index 4b2126b4010..be514a1c16e 100644 --- a/test/org/rascalmpl/MatchFingerprintTest.java +++ b/test/org/rascalmpl/MatchFingerprintTest.java @@ -15,6 +15,7 @@ import static org.junit.Assert.assertNotEquals; import java.io.IOException; +import java.io.Reader; import java.io.StringReader; import org.rascalmpl.interpreter.Evaluator; @@ -47,7 +48,7 @@ public class MatchFingerprintTest extends TestCase { private final GlobalEnvironment heap = new GlobalEnvironment(); private final ModuleEnvironment root = new ModuleEnvironment("root", heap); - private final Evaluator eval = new Evaluator(IRascalValueFactory.getInstance(), System.in, System.err, System.out, root, heap, RascalJUnitTestRunner.getCommonMonitor()); + private final Evaluator eval = new Evaluator(IRascalValueFactory.getInstance(), Reader.nullReader(), System.err, System.out, root, heap, RascalJUnitTestRunner.getCommonMonitor()); private final RascalFunctionValueFactory VF = eval.getFunctionValueFactory(); private final TypeFactory TF = TypeFactory.getInstance(); diff --git a/test/org/rascalmpl/test/functionality/ParallelEvaluatorsTests.java b/test/org/rascalmpl/test/functionality/ParallelEvaluatorsTests.java index 15134bb00bb..64f6d2e3a63 100644 --- a/test/org/rascalmpl/test/functionality/ParallelEvaluatorsTests.java +++ b/test/org/rascalmpl/test/functionality/ParallelEvaluatorsTests.java @@ -50,7 +50,7 @@ public void report(boolean successful, String test, ISourceLocation loc, String if (exception != null) { evaluator.warning("Got exception: " + exception, loc); - exception.printStackTrace(evaluator.getOutPrinter()); + exception.printStackTrace(evaluator.getStdOut()); throw new RuntimeException(exception); } } From 61d43875a004f7551f190d25080c097a6b00e035 Mon Sep 17 00:00:00 2001 From: Davy Landman Date: Tue, 15 Oct 2024 17:49:03 +0200 Subject: [PATCH 06/35] Rewriting away old parts of the jline2 code --- .../shell/EclipseTerminalConnection.java | 167 ++++++++++++++---- .../shell/ShellEvaluatorFactory.java | 6 +- 2 files changed, 132 insertions(+), 41 deletions(-) diff --git a/src/org/rascalmpl/shell/EclipseTerminalConnection.java b/src/org/rascalmpl/shell/EclipseTerminalConnection.java index 66d017901b5..8656f9c8ec6 100644 --- a/src/org/rascalmpl/shell/EclipseTerminalConnection.java +++ b/src/org/rascalmpl/shell/EclipseTerminalConnection.java @@ -17,11 +17,23 @@ import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; +import java.io.PrintWriter; import java.net.InetAddress; import java.net.Socket; +import java.nio.charset.Charset; import java.util.Arrays; +import java.util.function.IntConsumer; +import java.util.function.IntSupplier; + +import org.jline.terminal.Attributes; +import org.jline.terminal.Cursor; +import org.jline.terminal.MouseEvent; +import org.jline.terminal.Size; +import org.jline.terminal.Terminal; +import org.jline.utils.ColorPalette; +import org.jline.utils.InfoCmp.Capability; +import org.jline.utils.NonBlockingReader; -import jline.Terminal; public class EclipseTerminalConnection implements Terminal { public static final byte[] HEADER = new byte[] { 0x42, 0x42 }; @@ -78,70 +90,147 @@ private int askForHeight() { return previousHeight; } } + @Override - public void init() throws Exception { - base.init(); + public Size getSize() { + return new Size(askForWidth(), askForHeight()); } - @Override - public void restore() throws Exception { - base.restore(); + public void setSize(Size size) { + base.setSize(size); } @Override - public void reset() throws Exception { - base.reset(); + public void close() throws IOException { + base.close(); } - @Override - public boolean isSupported() { - return base.isSupported(); + public String getName() { + return base.getName(); } - - @Override - public boolean isAnsiSupported() { - return base.isAnsiSupported(); + public SignalHandler handle(Signal signal, SignalHandler handler) { + return base.handle(signal, handler); } - @Override - public OutputStream wrapOutIfNeeded(OutputStream out) { - return base.wrapOutIfNeeded(out); + public void raise(Signal signal) { + base.raise(signal); } - @Override - public InputStream wrapInIfNeeded(InputStream in) throws IOException { - return base.wrapInIfNeeded(in); + public NonBlockingReader reader() { + return base.reader(); } - @Override - public boolean hasWeirdWrap() { - return base.hasWeirdWrap(); + public PrintWriter writer() { + return base.writer(); } - @Override - public boolean isEchoEnabled() { - return base.isEchoEnabled(); + public Charset encoding() { + return base.encoding(); } - @Override - public void setEchoEnabled(boolean enabled) { - base.setEchoEnabled(enabled); + public InputStream input() { + return base.input(); + } + @Override + public OutputStream output() { + return base.output(); + } + @Override + public boolean canPauseResume() { + return base.canPauseResume(); + } + @Override + public void pause() { + base.pause(); + } + @Override + public void pause(boolean wait) throws InterruptedException { + base.pause(wait); + } + @Override + public void resume() { + base.resume(); + } + @Override + public boolean paused() { + return base.paused(); + } + @Override + public Attributes enterRawMode() { + return base.enterRawMode(); + } + @Override + public boolean echo() { + return base.echo(); + } + @Override + public boolean echo(boolean echo) { + return base.echo(echo); + } + @Override + public Attributes getAttributes() { + return base.getAttributes(); + } + @Override + public void setAttributes(Attributes attr) { + base.setAttributes(attr); + } + @Override + public void flush() { + base.flush(); + } + @Override + public String getType() { + return base.getType(); + } + @Override + public boolean puts(Capability capability, Object... params) { + return base.puts(capability, params); + } + @Override + public boolean getBooleanCapability(Capability capability) { + return base.getBooleanCapability(capability); + } + @Override + public Integer getNumericCapability(Capability capability) { + return base.getNumericCapability(capability); + } + @Override + public String getStringCapability(Capability capability) { + return base.getStringCapability(capability); + } + @Override + public Cursor getCursorPosition(IntConsumer discarded) { + return base.getCursorPosition(discarded); + } + @Override + public boolean hasMouseSupport() { + return base.hasMouseSupport(); + } + @Override + public boolean trackMouse(MouseTracking tracking) { + return base.trackMouse(tracking); + } + @Override + public MouseEvent readMouseEvent() { + return base.readMouseEvent(); + } + @Override + public MouseEvent readMouseEvent(IntSupplier reader) { + return base.readMouseEvent(reader); } - @Override - public String getOutputEncoding() { - return base.getOutputEncoding(); + public boolean hasFocusSupport() { + return base.hasFocusSupport(); } - @Override - public void enableInterruptCharacter() { - base.enableInterruptCharacter(); + public boolean trackFocus(boolean tracking) { + return base.trackFocus(tracking); } - @Override - public void disableInterruptCharacter() { - base.disableInterruptCharacter(); + public ColorPalette getPalette() { + return base.getPalette(); } } diff --git a/src/org/rascalmpl/shell/ShellEvaluatorFactory.java b/src/org/rascalmpl/shell/ShellEvaluatorFactory.java index 63eafa3da38..bfb0b335080 100644 --- a/src/org/rascalmpl/shell/ShellEvaluatorFactory.java +++ b/src/org/rascalmpl/shell/ShellEvaluatorFactory.java @@ -4,6 +4,8 @@ import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; +import java.io.PrintWriter; +import java.io.Reader; import java.net.URISyntaxException; import org.rascalmpl.debug.IRascalMonitor; @@ -27,7 +29,7 @@ public class ShellEvaluatorFactory { - public static Evaluator getDefaultEvaluator(InputStream input, OutputStream stdout, OutputStream stderr, IRascalMonitor monitor) { + public static Evaluator getDefaultEvaluator(Reader input, PrintWriter stdout, PrintWriter stderr, IRascalMonitor monitor) { GlobalEnvironment heap = new GlobalEnvironment(); ModuleEnvironment root = heap.addModule(new ModuleEnvironment(ModuleEnvironment.SHELL_MODULE, heap)); IValueFactory vf = ValueFactoryFactory.getValueFactory(); @@ -46,7 +48,7 @@ public static Evaluator getDefaultEvaluator(InputStream input, OutputStream stdo return evaluator; } - public static Evaluator getDefaultEvaluatorForLocation(File fileOrFolderInProject, InputStream input, OutputStream stdout, OutputStream stderr, IRascalMonitor monitor) { + public static Evaluator getDefaultEvaluatorForLocation(File fileOrFolderInProject, Reader input, PrintWriter stdout, PrintWriter stderr, IRascalMonitor monitor) { GlobalEnvironment heap = new GlobalEnvironment(); ModuleEnvironment root = heap.addModule(new ModuleEnvironment(ModuleEnvironment.SHELL_MODULE, heap)); IValueFactory vf = ValueFactoryFactory.getValueFactory(); From 94b9425a32903b5c711e036ca4ce6fabf9d4a48b Mon Sep 17 00:00:00 2001 From: Davy Landman Date: Tue, 15 Oct 2024 18:34:19 +0200 Subject: [PATCH 07/35] Rewrote TerminalProgressBarMonitor --- .../repl/TerminalProgressBarMonitor.java | 242 ++++++++---------- .../values/parsetrees/TreeAdapter.java | 7 +- 2 files changed, 105 insertions(+), 144 deletions(-) diff --git a/src/org/rascalmpl/repl/TerminalProgressBarMonitor.java b/src/org/rascalmpl/repl/TerminalProgressBarMonitor.java index 9d8b10b8214..f3e1408fcb0 100644 --- a/src/org/rascalmpl/repl/TerminalProgressBarMonitor.java +++ b/src/org/rascalmpl/repl/TerminalProgressBarMonitor.java @@ -1,12 +1,7 @@ package org.rascalmpl.repl; -import java.io.FilterOutputStream; -import java.io.FilterWriter; import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; import java.io.PrintWriter; -import java.nio.charset.Charset; import java.time.Duration; import java.time.Instant; import java.util.ArrayList; @@ -14,10 +9,10 @@ import java.util.LinkedList; import java.util.List; +import org.jline.terminal.Terminal; import org.rascalmpl.debug.IRascalMonitor; + import io.usethesource.vallang.ISourceLocation; -import jline.Terminal; -import jline.internal.Configuration; /** * The terminal progress bar monitor wraps the standard output stream to be able to monitor @@ -45,12 +40,6 @@ public class TerminalProgressBarMonitor extends PrintWriter implements IRascalMo */ private List unfinishedLines = new ArrayList<>(3); - /** - * This writer is there to help with the encoding to what the terminal needs. It writes directly to the - * underlying stream. - */ - private final PrintWriter writer; - /** * The entire width in character columns of the current terminal. Resizes everytime when we start * the first job. @@ -65,66 +54,66 @@ public class TerminalProgressBarMonitor extends PrintWriter implements IRascalMo /**x * Will make everything slow, but easier to spot mistakes */ - private final boolean debug = false; + private static final boolean debug = false; /** * Used to get updates to the width of the terminal */ private final Terminal tm; - private final String encoding; + /** + * Used for cases where the we need an actual PrintWriter instance + */ + private final PrintWriter rawWriter; @SuppressWarnings("resource") - public TerminalProgressBarMonitor(OutputStream out, InputStream in, Terminal tm) { - super(out); + public TerminalProgressBarMonitor(Terminal tm) { + super(debug ? new AlwaysFlushAlwaysShowCursor(tm.writer()) : tm.writer()); - this.encoding = Configuration.getEncoding(); + this.rawWriter = (PrintWriter)super.out; this.tm = tm; - PrintWriter theWriter = new PrintWriter(out, true, Charset.forName(encoding)); - this.writer = debug ? new PrintWriter(new AlwaysFlushAlwaysShowCursor(theWriter)) : theWriter; this.lineWidth = tm.getWidth(); - this.unicodeEnabled = ANSI.isUTF8enabled(theWriter, in); + this.unicodeEnabled = tm.encoding().newEncoder().canEncode(new ProgressBar("", 1).clocks[0]); - assert tm.isSupported() && tm.isAnsiSupported(): "interactive progress bar needs a working ANSI terminal"; assert out.getClass() != TerminalProgressBarMonitor.class : "accidentally wrapping the wrapper."; } /** * Use this for debugging terminal cursor movements, step by step. */ - private static class AlwaysFlushAlwaysShowCursor extends FilterWriter { + private static class AlwaysFlushAlwaysShowCursor extends PrintWriter { public AlwaysFlushAlwaysShowCursor(PrintWriter out) { super(out); } @Override - public void write(int c) throws IOException { - out.write(c); - out.write(ANSI.showCursor()); - out.flush(); + public void write(int c) { + super.write(c); + super.write(ANSI.showCursor()); + super.flush(); } @Override - public void write(char[] cbuf, int off, int len) throws IOException { - out.write(cbuf, off, len); - out.write(ANSI.showCursor()); - out.flush(); + public void write(char[] cbuf, int off, int len) { + super.write(cbuf, off, len); + super.write(ANSI.showCursor()); + super.flush(); } @Override - public void write(String str, int off, int len) throws IOException { - out.write(str, off, len); - out.write(ANSI.showCursor()); - out.flush(); + public void write(String str, int off, int len) { + super.write(str, off, len); + super.write(ANSI.showCursor()); + super.flush(); } } - private static class UnfinishedLine { + private class UnfinishedLine { final long threadId; private int curCapacity = 512; - private byte[] buffer = new byte[curCapacity]; + private char[] buffer = new char[curCapacity]; private int curEnd = 0; public UnfinishedLine() { @@ -137,7 +126,7 @@ public UnfinishedLine() { * * The resulting buffer nevers contain any newline character. */ - private void store(byte[] newInput, int offset, int len) { + private void store(char[] newInput, int offset, int len) { if (len == 0) { return; // fast exit } @@ -152,37 +141,37 @@ private void store(byte[] newInput, int offset, int len) { curEnd += len; } - public void write(byte[] n, OutputStream out) throws IOException { - write(n, 0, n.length, out); - } - /** * Main workhorse looks for newline characters in the new input. * - if there are newlines, than whatever is in the buffer can be flushed. * - all the characters up to the last new new line are flushed immediately. * - all the new characters after the last newline are buffered. */ - public void write(byte[] n, int offset, int len, OutputStream out) throws IOException { + public void write(char[] n, int offset, int len) { int lastNL = startOfLastLine(n, offset, len); if (lastNL == -1) { store(n, offset, len); } else { - flush(out); - out.write(n, offset, lastNL + 1); - out.flush(); + flush(); + rawWrite(n, offset, lastNL + 1); + rawFlush(); store(n, lastNL + 1, len - (lastNL + 1)); } } + public void write(String s, int offset, int len) { + write(s.toCharArray(), offset, len); + } + /** * This empties the current buffer onto the stream, * and resets the cursor. */ - private void flush(OutputStream out) throws IOException { + private void flush() { if (curEnd != 0) { - out.write(buffer, 0, curEnd); + rawWrite(buffer, 0, curEnd); curEnd = 0; } } @@ -191,14 +180,14 @@ private void flush(OutputStream out) throws IOException { * Prints whatever is the last line in the buffer, * and adds a newline. */ - public void flushLastLine(OutputStream out) throws IOException { + public void flushLastLine() { if (curEnd != 0) { - flush(out); - out.write('\n'); + flush(); + rawWrite('\n'); } } - private int startOfLastLine(byte[] buffer, int offset, int len) { + private int startOfLastLine(char[] buffer, int offset, int len) { for (int i = offset + len - 1; i >= offset; i--) { if (buffer[i] == '\n') { return i; @@ -209,6 +198,29 @@ private int startOfLastLine(byte[] buffer, int offset, int len) { } } + private void rawPrintln(String s) { + super.println(s); + } + private void rawWrite(String s) { + super.write(s); + } + + private void rawWrite(int c) { + super.write(c); + } + + private void rawWrite(char[] buf, int offset, int length) { + super.write(buf, offset, length); + } + + private void rawWrite(String buf, int offset, int length) { + super.write(buf, offset, length); + } + + private void rawFlush() { + super.flush(); + } + /** * Represents one currently running progress bar */ @@ -264,14 +276,14 @@ void update() { // to avoid flicker we only print if there is a new bar character to draw if (newWidth() != previousWidth) { stepper++; - writer.write(ANSI.moveUp(bars.size() - bars.indexOf(this))); + rawWrite(ANSI.moveUp(bars.size() - bars.indexOf(this))); write(); // this moves the cursor already one line down due to `println` int distance = bars.size() - bars.indexOf(this) - 1; if (distance > 0) { // ANSI will move 1 line even if the parameter is 0 - writer.write(ANSI.moveDown(distance)); + rawWrite(ANSI.moveDown(distance)); } - writer.flush(); + rawFlush(); } } @@ -323,7 +335,7 @@ void write() { return; // robustness against very small screens. At least don't throw bounds exceptions } else if (barWidth <= 3) { // we can print the clock for good measure - writer.println(clock); + rawPrintln(clock); return; } @@ -340,7 +352,7 @@ else if (barWidth <= 3) { // we can print the clock for good measure + " " ; - writer.println(line); // note this puts us one line down + rawPrintln(line); // note this puts us one line down } @Override @@ -370,40 +382,16 @@ public void done() { */ private void eraseBars() { if (!bars.isEmpty()) { - writer.write(ANSI.moveUp(bars.size())); - writer.write(ANSI.clearToEndOfScreen()); + rawWrite(ANSI.moveUp(bars.size())); + rawWrite(ANSI.clearToEndOfScreen()); } - writer.flush(); + rawFlush(); } /** * ANSI escape codes convenience functions */ private static class ANSI { - static boolean isUTF8enabled(PrintWriter writer, InputStream in) { - try { - int pos = getCursorPosition(writer, in); - // Japanese A (あ) is typically 3 bytes in most encodings, but should be less than 3 ANSI columns - // on screen if-and-only-if unicode is supported. - writer.write("あ"); - writer.flush(); - int newPos = getCursorPosition(writer, in); - int diff = newPos - pos; - - try { - return diff < 3; - } - finally { - while (--diff >= 0) { - writer.write(ANSI.delete()); - } - writer.flush(); - } - } - catch (IOException e) { - return false; - } - } public static String grey8Background() { return "\u001B[48;5;240m"; @@ -413,33 +401,6 @@ public static String brightWhiteForeground() { return "\u001B[97m"; } - static int getCursorPosition(PrintWriter writer, InputStream in) throws IOException { - writer.write(ANSI.printCursorPosition()); - writer.flush(); - - byte[] col = new byte[32]; - int len = in.read(col); - String echo; - - try { - echo = new String(col, 0, len, Configuration.getEncoding()); - } - catch (StringIndexOutOfBoundsException e) { - // this happens if there is some other input on stdin (for example a pipe) - // TODO: the input is now read and can't be processed again. - echo = ""; - } - - if (!echo.startsWith("\u001B[") || !echo.contains(";")) { - return -1; - } - - // terminal responds with ESC[n;mR, where n is the row and m is the column. - echo = echo.split(";")[1]; // take the column part - echo = echo.substring(0, echo.length() - 1); // remove the last R - return Integer.parseInt(echo); - } - public static String delete() { return "\u001B[D\u001B[K"; } @@ -497,7 +458,7 @@ private void printBars() { pb.write(); } - writer.flush(); + rawFlush(); } @@ -538,7 +499,7 @@ public synchronized void jobStart(String name, int workShare, int totalWork) { var pb = findBarByName(name); - writer.write(ANSI.hideCursor()); + rawWrite(ANSI.hideCursor()); if (pb == null) { eraseBars(); // to make room for the new bars @@ -552,8 +513,8 @@ public synchronized void jobStart(String name, int workShare, int totalWork) { pb.update(); } - writer.write(ANSI.showCursor()); - writer.flush(); + rawWrite(ANSI.showCursor()); + rawFlush(); } @Override @@ -561,11 +522,11 @@ public synchronized void jobStep(String name, String message, int workShare) { ProgressBar pb = findBarByName(name); if (pb != null) { - writer.write(ANSI.hideCursor()); + rawWrite(ANSI.hideCursor()); pb.worked(workShare, message); pb.update(); - writer.write(ANSI.showCursor()); - writer.flush(); + rawWrite(ANSI.showCursor()); + rawFlush(); } } @@ -573,7 +534,7 @@ public synchronized void jobStep(String name, String message, int workShare) { public synchronized int jobEnd(String name, boolean succeeded) { var pb = findBarByName(name); - writer.write(ANSI.hideCursor()); + rawWrite(ANSI.hideCursor()); if (pb != null && --pb.nesting == -1) { eraseBars(); @@ -590,7 +551,7 @@ else if (pb != null) { pb.update(); } - writer.write(ANSI.showCursor()); + rawWrite(ANSI.showCursor()); return -1; } @@ -617,7 +578,7 @@ public synchronized void warning(String message, ISourceLocation src) { eraseBars(); } - writer.println(("[WARNING] " + (src != null ? (src + ": ") : "") + message)); + rawPrintln(("[WARNING] " + (src != null ? (src + ": ") : "") + message)); if (!bars.isEmpty()) { printBars(); @@ -634,35 +595,34 @@ public synchronized void warning(String message, ISourceLocation src) { * is before the first character of a line. */ @Override - public synchronized void write(byte[] b) throws IOException { + public void write(String s, int off, int len) { if (!bars.isEmpty()) { eraseBars(); - findUnfinishedLine().write(b, out); + findUnfinishedLine().write(s, off, len); printBars(); } else { - out.write(b); + rawWrite(s, off, len); } } - /** * Here we make sure the progress bars are gone just before * someone wants to print in the console. When the printing * is ready, we simply add our own progress bars again. */ @Override - public synchronized void write(byte[] b, int off, int len) throws IOException { + public synchronized void write(char[] buf, int off, int len) { if (!bars.isEmpty()) { eraseBars(); - findUnfinishedLine().write(b, off, len, out); + findUnfinishedLine().write(buf, off, len); printBars(); } else { // this must be the raw output stream // otherwise rascal prompts (which do not end in newlines) will be buffered - out.write(b, off, len); + rawWrite(buf, off, len); } } @@ -672,14 +632,14 @@ public synchronized void write(byte[] b, int off, int len) throws IOException { * is ready, we simply add our own progress bars again. */ @Override - public synchronized void write(int b) throws IOException { + public synchronized void write(int c) { if (!bars.isEmpty()) { eraseBars(); - findUnfinishedLine().write(new byte[] { (byte) b }, out); + findUnfinishedLine().write(new char[] { (char) c }, 0, 1); printBars(); } else { - out.write(b); + rawWrite(c); } } @@ -694,17 +654,12 @@ public synchronized void endAllJobs() { } for (UnfinishedLine l : unfinishedLines) { - try { - l.flushLastLine(out); - } - catch (IOException e) { - // might happen if the terminal crashes before we stop running - } + l.flushLastLine(); } try { - writer.write(ANSI.showCursor()); - writer.flush(); + rawWrite(ANSI.showCursor()); + rawFlush(); out.flush(); } catch (IOException e) { @@ -713,7 +668,12 @@ public synchronized void endAllJobs() { } @Override - public void close() throws IOException { - endAllJobs(); + public void close() { + try { + endAllJobs(); + } + finally { + super.close(); + } } } diff --git a/src/org/rascalmpl/values/parsetrees/TreeAdapter.java b/src/org/rascalmpl/values/parsetrees/TreeAdapter.java index 4ab329a2a92..3b631fb1245 100644 --- a/src/org/rascalmpl/values/parsetrees/TreeAdapter.java +++ b/src/org/rascalmpl/values/parsetrees/TreeAdapter.java @@ -19,9 +19,10 @@ import java.util.HashMap; import java.util.Iterator; import java.util.Map; -import org.fusesource.jansi.Ansi; -import org.fusesource.jansi.Ansi.Attribute; -import org.fusesource.jansi.Ansi.Color; + +import org.jline.jansi.Ansi; +import org.jline.jansi.Ansi.Attribute; +import org.jline.jansi.Ansi.Color; import org.rascalmpl.exceptions.ImplementationError; import org.rascalmpl.interpreter.utils.LimitedResultWriter; import org.rascalmpl.values.RascalValueFactory; From 1fd834880b6ec16bc6ae0953d1b3ade0b61eba9a Mon Sep 17 00:00:00 2001 From: Davy Landman Date: Tue, 22 Oct 2024 10:56:07 +0200 Subject: [PATCH 08/35] Working on migrating to the new repl api --- src/org/rascalmpl/repl/ItalicErrorWriter.java | 5 +- src/org/rascalmpl/repl/RedErrorWriter.java | 7 +- src/org/rascalmpl/repl/ReplTextWriter.java | 6 +- .../rascalmpl/repl/SourceLocationHistory.java | 8 +- .../repl/TerminalProgressBarMonitor.java | 5 - .../repl/TerminalProgressBarMonitor2.java | 602 ------------------ src/org/rascalmpl/shell/RascalShell2.java | 6 +- 7 files changed, 17 insertions(+), 622 deletions(-) delete mode 100644 src/org/rascalmpl/repl/TerminalProgressBarMonitor2.java diff --git a/src/org/rascalmpl/repl/ItalicErrorWriter.java b/src/org/rascalmpl/repl/ItalicErrorWriter.java index b9a7340b3d1..a650a64e4ac 100644 --- a/src/org/rascalmpl/repl/ItalicErrorWriter.java +++ b/src/org/rascalmpl/repl/ItalicErrorWriter.java @@ -2,8 +2,9 @@ import java.io.Writer; -import org.fusesource.jansi.Ansi; -import org.fusesource.jansi.Ansi.Attribute; +import org.jline.jansi.Ansi; +import org.jline.jansi.Ansi.Attribute; + public class ItalicErrorWriter extends WrappedFilterWriter { public ItalicErrorWriter(Writer out) { diff --git a/src/org/rascalmpl/repl/RedErrorWriter.java b/src/org/rascalmpl/repl/RedErrorWriter.java index 92dac60c344..b2300a3787d 100644 --- a/src/org/rascalmpl/repl/RedErrorWriter.java +++ b/src/org/rascalmpl/repl/RedErrorWriter.java @@ -2,9 +2,10 @@ import java.io.Writer; -import org.fusesource.jansi.Ansi; -import org.fusesource.jansi.Ansi.Attribute; -import org.fusesource.jansi.Ansi.Color; +import org.jline.jansi.Ansi; +import org.jline.jansi.Ansi.Attribute; +import org.jline.jansi.Ansi.Color; + public class RedErrorWriter extends WrappedFilterWriter { public RedErrorWriter(Writer out) { diff --git a/src/org/rascalmpl/repl/ReplTextWriter.java b/src/org/rascalmpl/repl/ReplTextWriter.java index 475c0f0520f..647680fb2e8 100644 --- a/src/org/rascalmpl/repl/ReplTextWriter.java +++ b/src/org/rascalmpl/repl/ReplTextWriter.java @@ -3,9 +3,9 @@ import java.io.IOException; import java.io.StringWriter; -import org.fusesource.jansi.Ansi; -import org.fusesource.jansi.Ansi.Attribute; -import org.fusesource.jansi.Ansi.Color; +import org.jline.jansi.Ansi; +import org.jline.jansi.Ansi.Attribute; +import org.jline.jansi.Ansi.Color; import org.rascalmpl.interpreter.utils.LimitedResultWriter; import io.usethesource.vallang.ISourceLocation; diff --git a/src/org/rascalmpl/repl/SourceLocationHistory.java b/src/org/rascalmpl/repl/SourceLocationHistory.java index 8964873ba40..eb14ea04d84 100644 --- a/src/org/rascalmpl/repl/SourceLocationHistory.java +++ b/src/org/rascalmpl/repl/SourceLocationHistory.java @@ -9,14 +9,14 @@ import java.io.PrintStream; import java.io.Reader; +import org.jline.reader.History.Entry; +import org.jline.utils.Log; import org.rascalmpl.uri.URIResolverRegistry; + import io.usethesource.vallang.ISourceLocation; -import jline.console.history.MemoryHistory; -import jline.console.history.PersistentHistory; -import jline.internal.Log; -public class SourceLocationHistory extends MemoryHistory implements PersistentHistory { +public class SourceLocationHistory extends History implements PersistentHistory { private static final URIResolverRegistry reg = URIResolverRegistry.getInstance(); private final ISourceLocation loc; diff --git a/src/org/rascalmpl/repl/TerminalProgressBarMonitor.java b/src/org/rascalmpl/repl/TerminalProgressBarMonitor.java index f3e1408fcb0..d5175da570c 100644 --- a/src/org/rascalmpl/repl/TerminalProgressBarMonitor.java +++ b/src/org/rascalmpl/repl/TerminalProgressBarMonitor.java @@ -61,16 +61,11 @@ public class TerminalProgressBarMonitor extends PrintWriter implements IRascalMo */ private final Terminal tm; - /** - * Used for cases where the we need an actual PrintWriter instance - */ - private final PrintWriter rawWriter; @SuppressWarnings("resource") public TerminalProgressBarMonitor(Terminal tm) { super(debug ? new AlwaysFlushAlwaysShowCursor(tm.writer()) : tm.writer()); - this.rawWriter = (PrintWriter)super.out; this.tm = tm; this.lineWidth = tm.getWidth(); diff --git a/src/org/rascalmpl/repl/TerminalProgressBarMonitor2.java b/src/org/rascalmpl/repl/TerminalProgressBarMonitor2.java deleted file mode 100644 index 88f8c59f20f..00000000000 --- a/src/org/rascalmpl/repl/TerminalProgressBarMonitor2.java +++ /dev/null @@ -1,602 +0,0 @@ -package org.rascalmpl.repl; - -import java.io.FilterOutputStream; -import java.io.FilterWriter; -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; -import java.io.PrintWriter; -import java.nio.charset.Charset; -import java.nio.charset.StandardCharsets; -import java.time.Duration; -import java.time.Instant; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.LinkedList; -import java.util.List; - -import org.jline.terminal.Terminal; -import org.rascalmpl.debug.IRascalMonitor; -import io.usethesource.vallang.ISourceLocation; - -/** - * The terminal progress bar monitor wraps the standard output stream to be able to monitor - * output and keep the progress bars at the same place in the window while other prints happen - * asyncronously (or even in parallel). It can be passed to the IDEServices API such that - * clients can start progress bars, make them grow and end them using the API in util::Monitor. - * - * This gives the console the ability to show progress almost as clearly as an IDE can with a - * UI experience. - * - * This class only works correctly if the actual _raw_ output stream of the terminal is wrapped - * with an object of this class. - */ -public class TerminalProgressBarMonitor2 extends PrintWriter implements IRascalMonitor { - /** - * We administrate an ordered list of named bars, which will be printed from - * top to bottom just above the next prompt. - */ - private List bars = new LinkedList<>(); - - /** - * We also keep a list of currently unfinished lines (one for each thread). - * Since there are generally very few threads a simple list beats a hash-map in terms - * of memory allocation and possibly also lookup efficiency. - */ - private List unfinishedLines = new ArrayList<>(3); - - /** - * This writer is there to help with the encoding to what the terminal needs. It writes directly to the - * underlying stream. - */ - private final PrintWriter writer; - - /** - * The entire width in character columns of the current terminal. Resizes everytime when we start - * the first job. - */ - private int lineWidth; - - private final boolean unicodeEnabled; - - /**x - * Will make everything slow, but easier to spot mistakes - */ - private final boolean debug = false; - - /** - * Used to get updates to the width of the terminal - */ - private final Terminal tm; - - - @SuppressWarnings("resource") - public TerminalProgressBarMonitor2(Terminal tm) { - super(tm.writer()); - - this.tm = tm; - - this.writer = tm.writer(); - this.lineWidth = tm.getWidth(); - this.unicodeEnabled = tm.encoding().contains(StandardCharsets.UTF_8); - } - - /** - * Use this for debugging terminal cursor movements, step by step. - */ - private static class AlwaysFlushAlwaysShowCursor extends FilterWriter { - - public AlwaysFlushAlwaysShowCursor(PrintWriter out) { - super(out); - } - - @Override - public void write(int c) throws IOException { - out.write(c); - out.write(ANSI.showCursor()); - out.flush(); - } - - @Override - public void write(char[] cbuf, int off, int len) throws IOException { - out.write(cbuf, off, len); - out.write(ANSI.showCursor()); - out.flush(); - } - - @Override - public void write(String str, int off, int len) throws IOException { - out.write(str, off, len); - out.write(ANSI.showCursor()); - out.flush(); - } - } - - private static class UnfinishedLine { - final long threadId; - private int curCapacity = 512; - private char[] buffer = new char[curCapacity]; - private int curEnd = 0; - - public UnfinishedLine() { - this.threadId = Thread.currentThread().getId(); - } - - /** - * Adding input combines previously unfinished sentences with possible - * new (unfinished) sentences. - * - * The resulting buffer nevers contain any newline character. - */ - private void store(char[] newInput, int offset, int len) { - if (len == 0) { - return; // fast exit - } - - // first ensure capacity of the array - if (curEnd + len >= curCapacity) { - curCapacity *= 2; - buffer = Arrays.copyOf(buffer, curCapacity); - } - - System.arraycopy(newInput, offset, buffer, curEnd, len); - curEnd += len; - } - - /** - * Main workhorse looks for newline characters in the new input. - * - if there are newlines, than whatever is in the buffer can be flushed. - * - all the characters up to the last new new line are flushed immediately. - * - all the new characters after the last newline are buffered. - */ - public void write(char[] n, int offset, int len, PrintWriter out) { - int lastNL = startOfLastLine(n, offset, len); - - if (lastNL == -1) { - store(n, offset, len); - } - else { - flush(out); - out.write(n, offset, lastNL + 1); - store(n, lastNL + 1, len - (lastNL + 1)); - } - } - - /** - * Main workhorse looks for newline characters in the new input. - * - if there are newlines, than whatever is in the buffer can be flushed. - * - all the characters up to the last new new line are flushed immediately. - * - all the new characters after the last newline are buffered. - */ - public void write(String s, int offset, int len, PrintWriter out) { - write(s.toCharArray(), offset, len, out); - } - - /** - * This empties the current buffer onto the stream, - * and resets the cursor. - */ - private void flush(PrintWriter out) { - if (curEnd != 0) { - out.write(buffer, 0, curEnd); - curEnd = 0; - } - } - - /** - * Prints whatever is the last line in the buffer, - * and adds a newline. - */ - public void flushLastLine(PrintWriter out) { - if (curEnd != 0) { - flush(out); - out.write('\n'); - } - } - - private int startOfLastLine(char[] buffer, int offset, int len) { - for (int i = offset + len - 1; i >= offset; i--) { - if (buffer[i] == '\n') { - return i; - } - } - - return -1; - } - } - - /** - * Represents one currently running progress bar - */ - private class ProgressBar { - private final long threadId; - private final String threadName; - private final String name; - private int max; - private int current = 0; - private int previousWidth = 0; - private int doneWidth = 0; - private final int barWidthUnicode = lineWidth - "☐ ".length() - " 🕐 00:00:00.000 ".length(); - private final int barWidthAscii = lineWidth - "? ".length() - " - 00:00:00.000 ".length(); - private final Instant startTime; - private Duration duration; - private String message = ""; - - /** - * Stepper is incremented with every jobStep that has an visible effect on the progress bar. - * It is used to index into `clocks` or `twister` to create an animation effect. - */ - private int stepper = 1; - private final String[] clocks = new String[] {"🕐" , "🕑", "🕒", "🕓", "🕔", "🕕", "🕖", "🕗", "🕘", "🕙", "🕛"}; - private final String[] twister = new String[] {"." , ".", "o", "o", "O","O", "O", "o", "o", ".", "."}; - public int nesting = 0; - - ProgressBar(String name, int max) { - this.threadId = Thread.currentThread().getId(); - this.threadName = Thread.currentThread().getName(); - this.name = name; - this.max = Math.max(1, max); - this.startTime = Instant.now(); - this.duration = Duration.ZERO; - this.message = name; - } - - void worked(int amount, String message) { - if (current + amount > max) { - // Fixing this warning helps avoiding to flicker the tick sign on and off, and also makes the progress bar - // a more accurate depiction of the progress of the computation. - warning("Monitor of " + name + " is over max (" + max + ") by " + (current + amount - max), null); - } - - this.current = Math.min(current + amount, max); - this.duration = Duration.between(startTime, Instant.now()); - this.message = message; - } - - /** - * To avoid flickering of all bars at the same time, we only reprint - * the current bar - */ - void update() { - // to avoid flicker we only print if there is a new bar character to draw - if (newWidth() != previousWidth) { - stepper++; - writer.write(ANSI.moveUp(bars.size() - bars.indexOf(this))); - write(); // this moves the cursor already one line down due to `println` - int distance = bars.size() - bars.indexOf(this) - 1; - if (distance > 0) { - // ANSI will move 1 line even if the parameter is 0 - writer.write(ANSI.moveDown(distance)); - } - writer.flush(); - } - } - - int newWidth() { - if (max != 0) { - current = Math.min(max, current); // for robustness sake - var partDone = (current * 1.0) / (max * 1.0); - return (int) Math.floor(barWidthUnicode * partDone); - } - else { - return barWidthUnicode % stepper; - } - } - - /** - * Print the current state of the progress bar - */ - void write() { - previousWidth = doneWidth; - doneWidth = newWidth(); - - // var overWidth = barWidth - doneWidth; - var done = unicodeEnabled - ? (current >= max ? "☑ " : "☐ ") - : (current >= max ? "X " : "O ") - ; - - // capitalize - var msg = message.substring(0, 1).toUpperCase() + message.substring(1, message.length()); - - // fill up and cut off: - msg = threadLabel() + msg; - msg = (msg + " ".repeat(Math.max(0, barWidthUnicode - msg.length()))).substring(0, barWidthUnicode); - - // split - var barWidth = unicodeEnabled ? barWidthUnicode : barWidthAscii; - var frontPart = msg.substring(0, doneWidth); - var backPart = msg.substring(doneWidth, msg.length()); - var clock = unicodeEnabled ? clocks[stepper % clocks.length] : twister[stepper % twister.length]; - - if (barWidth < 1) { - return; // robustness against very small screens. At least don't throw bounds exceptions - } - else if (barWidth <= 3) { // we can print the clock for good measure - writer.println(clock); - return; - } - - var line - = done - + ANSI.darkBackground() - + frontPart - + ANSI.noBackground() - + ANSI.lightBackground() - + backPart - + ANSI.noBackground() - + " " + clock + " " - + String.format("%d:%02d:%02d.%03d", duration.toHoursPart(), duration.toMinutes(), duration.toSecondsPart(), duration.toMillisPart()) - + " " - ; - - writer.println(line); // note this puts us one line down - } - - private String threadLabel() { - if (threadName.isEmpty()) { - return ""; - } - else if ("main".equals(threadName)) { - return ""; - } - - return threadName + ": "; - } - - @Override - public boolean equals(Object obj) { - return obj instanceof ProgressBar - && ((ProgressBar) obj).name.equals(name) - && ((ProgressBar) obj).threadId == threadId - ; - } - - @Override - public int hashCode() { - return name.hashCode() + 31 * (int) threadId; - } - - public void done() { - this.current = Math.min(current, max); - this.duration = Duration.between(startTime, Instant.now()); - this.message = name; - } - } - - /** - * Clean the screen, ready for the next output (normal output or the next progress bars or both). - * We use fancy ANSI codes here to move the cursor and clean the screen to remove previous versions - * of the bars - */ - private void eraseBars() { - if (!bars.isEmpty()) { - writer.write(ANSI.moveUp(bars.size())); - writer.write(ANSI.clearToEndOfScreen()); - } - writer.flush(); - } - - /** - * ANSI escape codes convenience functions - */ - private static class ANSI { - - static String moveUp(int n) { - return "\u001B[" + n + "F"; - } - - public static String darkBackground() { - return "\u001B[48;5;248m"; - } - - public static String noBackground() { - return "\u001B[49m"; - } - - public static String lightBackground() { - return "\u001B[48;5;250m"; - } - - static String moveDown(int n) { - return "\u001B[" + n + "E"; - } - - static String clearToEndOfScreen() { - return "\u001B[0J"; - } - - static String hideCursor() { - return "\u001B[?25l"; - } - - static String showCursor() { - return "\u001B[?25h"; - } - } - - /** - * Simply print the bars. No cursor movement here. Hiding the cursor prevents flickering. - */ - private void printBars() { - if (bars.isEmpty()) { - // no more bars to show, so cursor goes back. - writer.write(ANSI.showCursor()); - } - - for (var pb : bars) { - pb.write(); - } - - writer.flush(); - } - - /** - * Find a bar in the ordered list of bars, by name. - * @param name of the bar - * @return the current instance by that name - */ - private ProgressBar findBarByName(String name) { - return bars.stream() - .filter(b -> b.threadId == Thread.currentThread().getId()) - .filter(b -> b.name.equals(name)).findFirst().orElseGet(() -> null); - } - - private UnfinishedLine findUnfinishedLine() { - return unfinishedLines.stream() - .filter(l -> l.threadId == Thread.currentThread().getId()) - .findAny() - .orElseGet(() -> { - UnfinishedLine l = new UnfinishedLine(); - unfinishedLines.add(l); - return l; - }); - } - - @Override - public synchronized void jobStart(String name, int workShare, int totalWork) { - if (bars.size() == 0) { - // first new job, we take time to react to window resizing - lineWidth = tm.getWidth(); - // remove the cursor - writer.write(ANSI.hideCursor()); - } - - var pb = findBarByName(name); - - if (pb == null) { - eraseBars(); // to make room for the new bars - bars.add(new ProgressBar(name, totalWork)); - printBars(); // probably one line longer than before! - } - else { - // Zeno-bar: we add the new work to the already existing work - pb.max += totalWork; - pb.nesting++; - pb.update(); - } - } - - @Override - public synchronized void jobStep(String name, String message, int workShare) { - ProgressBar pb = findBarByName(name); - - if (pb != null) { - pb.worked(workShare, message); - pb.update(); - } - } - - @Override - public synchronized int jobEnd(String name, boolean succeeded) { - var pb = findBarByName(name); - - if (pb != null && --pb.nesting == -1) { - eraseBars(); - // write it one last time into the scrollback buffer (on top) - pb.done(); - pb.write(); - bars.remove(pb); - // print the left over bars under this one. - printBars(); - return pb.current; - } - else if (pb != null) { - pb.done(); - pb.update(); - } - - return -1; - } - - @Override - public synchronized boolean jobIsCanceled(String name) { - // ? don't know what this should do - return false; - } - - @Override - public synchronized void jobTodo(String name, int work) { - ProgressBar pb = findBarByName(name); - - if (pb != null) { - pb.max += work; - pb.update(); - } - } - - @Override - public synchronized void warning(String message, ISourceLocation src) { - if (!bars.isEmpty()) { - eraseBars(); - } - writer.println(("[WARNING] " + (src != null ? (src + ": ") : "") + message)); - if (!bars.isEmpty()) { - printBars(); - } - } - - @Override - public void write(String s, int off, int len) { - if (!bars.isEmpty()) { - eraseBars(); - findUnfinishedLine().write(s, off, len, writer); - printBars(); - } - else { - writer.write(s, off, len); - } - } - - /** - * Here we make sure the progress bars are gone just before - * someone wants to print in the console. When the printing - * is ready, we simply add our own progress bars again. - */ - @Override - public synchronized void write(char[] cbuf, int off, int len) { - if (!bars.isEmpty()) { - eraseBars(); - findUnfinishedLine().write(cbuf, off, len, writer); - printBars(); - } - else { - writer.write(cbuf, off, len); - } - } - - /** - * Here we make sure the progress bars are gone just before - * someone wants to print in the console. When the printing - * is ready, we simply add our own progress bars again. - */ - @Override - public synchronized void write(int b) { - if (!bars.isEmpty()) { - eraseBars(); - findUnfinishedLine().write(new char[] { (char) b },0, 1, writer); - printBars(); - } - else { - writer.write(b); - } - } - - - - @Override - public synchronized void endAllJobs() { - for (var pb : bars) { - if (pb.nesting >= 0) { - writer.println("[INFO] " + pb.name + " is still at nesting level " + pb.nesting); - } - } - - bars.clear(); - for (UnfinishedLine l : unfinishedLines) { - l.flushLastLine(writer); - } - writer.write(ANSI.showCursor()); - writer.flush(); - } -} \ No newline at end of file diff --git a/src/org/rascalmpl/shell/RascalShell2.java b/src/org/rascalmpl/shell/RascalShell2.java index 616cc4bea9a..5bb3d3ea41c 100644 --- a/src/org/rascalmpl/shell/RascalShell2.java +++ b/src/org/rascalmpl/shell/RascalShell2.java @@ -50,6 +50,7 @@ import org.rascalmpl.interpreter.utils.RascalManifest; import org.rascalmpl.parser.gtd.exception.ParseError; import org.rascalmpl.repl.ReplTextWriter; +import org.rascalmpl.repl.TerminalProgressBarMonitor; import org.rascalmpl.repl.TerminalProgressBarMonitor2; import org.rascalmpl.uri.URIResolverRegistry; import org.rascalmpl.uri.URIUtil; @@ -87,7 +88,7 @@ public static void main(String[] args) throws IOException { .build(); //IRascalMonitor monitor = IRascalMonitor.buildConsoleMonitor(System.in, System.out, true); - var monitor = new TerminalProgressBarMonitor2(term); + var monitor = new TerminalProgressBarMonitor(term); // var monitor = new NullRascalMonitor() { // @Override @@ -102,8 +103,7 @@ public static void main(String[] args) throws IOException { GlobalEnvironment heap = new GlobalEnvironment(); ModuleEnvironment root = heap.addModule(new ModuleEnvironment(ModuleEnvironment.SHELL_MODULE, heap)); IValueFactory vf = ValueFactoryFactory.getValueFactory(); - Evaluator evaluator = new Evaluator(vf, new NullInputStream(), OutputStream.nullOutputStream(), OutputStream.nullOutputStream(), root, heap, monitor); - evaluator.overwritePrintWriter(monitor, new PrintWriter(monitor, true)); + Evaluator evaluator = new Evaluator(vf, term.reader(), new PrintWriter(System.err, true), term.writer(), root, heap, monitor); evaluator.addRascalSearchPathContributor(StandardLibraryContributor.getInstance()); URIResolverRegistry reg = URIResolverRegistry.getInstance(); From 91998c13b2c52f61390861795cedb8e2bb1bc1b3 Mon Sep 17 00:00:00 2001 From: Davy Landman Date: Thu, 24 Oct 2024 12:34:11 +0200 Subject: [PATCH 09/35] Got the first working repl to do multiline --- src/org/rascalmpl/debug/IRascalMonitor.java | 20 +- .../interpreter/BatchProgressMonitor.java | 8 +- .../{TermREPL.java => TermREPL.java_disabled} | 0 .../{BaseREPL.java => BaseREPL.java_disabled} | 0 src/org/rascalmpl/repl/BaseREPL2.java | 153 +++++++++ src/org/rascalmpl/repl/IOutputPrinter.java | 9 + src/org/rascalmpl/repl/IREPLService.java | 86 +++++ ...va => RascalInterpreterREPL.java_disabled} | 0 src/org/rascalmpl/repl/RascalLineParser.java | 275 ++++++++++++++++ .../rascalmpl/repl/RascalReplServices.java | 302 ++++++++++++++++++ ...va => SourceLocationHistory.java_disabled} | 0 .../repl/TerminalProgressBarMonitor.java | 40 ++- src/org/rascalmpl/shell/ModuleRunner.java | 6 +- src/org/rascalmpl/shell/ParseTest.java | 91 ++++++ ...PLRunner.java => REPLRunner.java_disabled} | 0 ...alShell.java => RascalShell.java_disabled} | 0 src/org/rascalmpl/shell/RascalShell2.java | 175 ++-------- .../infrastructure/RascalJUnitTestRunner.java | 25 +- test/org/rascalmpl/MatchFingerprintTest.java | 3 +- .../ParallelEvaluatorsTests.java | 4 +- 20 files changed, 999 insertions(+), 198 deletions(-) rename src/org/rascalmpl/library/util/{TermREPL.java => TermREPL.java_disabled} (100%) rename src/org/rascalmpl/repl/{BaseREPL.java => BaseREPL.java_disabled} (100%) mode change 100755 => 100644 create mode 100644 src/org/rascalmpl/repl/BaseREPL2.java create mode 100644 src/org/rascalmpl/repl/IOutputPrinter.java create mode 100644 src/org/rascalmpl/repl/IREPLService.java rename src/org/rascalmpl/repl/{RascalInterpreterREPL.java => RascalInterpreterREPL.java_disabled} (100%) create mode 100644 src/org/rascalmpl/repl/RascalLineParser.java create mode 100644 src/org/rascalmpl/repl/RascalReplServices.java rename src/org/rascalmpl/repl/{SourceLocationHistory.java => SourceLocationHistory.java_disabled} (100%) create mode 100644 src/org/rascalmpl/shell/ParseTest.java rename src/org/rascalmpl/shell/{REPLRunner.java => REPLRunner.java_disabled} (100%) mode change 100755 => 100644 rename src/org/rascalmpl/shell/{RascalShell.java => RascalShell.java_disabled} (100%) diff --git a/src/org/rascalmpl/debug/IRascalMonitor.java b/src/org/rascalmpl/debug/IRascalMonitor.java index 06452422f98..add6dd439df 100644 --- a/src/org/rascalmpl/debug/IRascalMonitor.java +++ b/src/org/rascalmpl/debug/IRascalMonitor.java @@ -15,18 +15,21 @@ import java.io.InputStream; import java.io.OutputStream; import java.io.PrintStream; +import java.io.PrintWriter; +import java.io.Reader; import java.util.function.BiConsumer; import java.util.function.BiFunction; import java.util.function.Function; import java.util.function.Supplier; + +import org.jline.terminal.Terminal; +import org.jline.utils.InfoCmp.Capability; import org.rascalmpl.interpreter.BatchProgressMonitor; import org.rascalmpl.interpreter.NullRascalMonitor; import org.rascalmpl.repl.TerminalProgressBarMonitor; import io.usethesource.vallang.ISourceLocation; -import jline.Terminal; -import jline.TerminalFactory; public interface IRascalMonitor { /** @@ -158,8 +161,8 @@ default void jobStep(String name, String message) { * and otherwise default to a dumn terminal console progress logger. * @return */ - public static IRascalMonitor buildConsoleMonitor(InputStream in, OutputStream out) { - return buildConsoleMonitor(in, out, inBatchMode()); + public static IRascalMonitor buildConsoleMonitor(Terminal term) { + return buildConsoleMonitor(term, inBatchMode()); } public static boolean inBatchMode() { @@ -168,12 +171,11 @@ public static boolean inBatchMode() { ; } - public static IRascalMonitor buildConsoleMonitor(InputStream in, OutputStream out, boolean batchMode) { - Terminal terminal = TerminalFactory.get(); + public static IRascalMonitor buildConsoleMonitor(Terminal terminal, boolean batchMode) { - return !batchMode && terminal.isAnsiSupported() - ? new TerminalProgressBarMonitor(out, in, terminal) - : new BatchProgressMonitor(new PrintStream(out)) + return !batchMode && TerminalProgressBarMonitor.shouldWorkIn(terminal) + ? new TerminalProgressBarMonitor(terminal) + : new BatchProgressMonitor(terminal.writer()) ; } diff --git a/src/org/rascalmpl/interpreter/BatchProgressMonitor.java b/src/org/rascalmpl/interpreter/BatchProgressMonitor.java index 82793b67d80..43e3768d2c8 100644 --- a/src/org/rascalmpl/interpreter/BatchProgressMonitor.java +++ b/src/org/rascalmpl/interpreter/BatchProgressMonitor.java @@ -24,11 +24,15 @@ public class BatchProgressMonitor implements IRascalMonitor { PrintWriter out; public BatchProgressMonitor() { - this.out = new PrintWriter(System.err); + this(new PrintWriter(System.err, true)); } public BatchProgressMonitor(PrintStream out) { - this.out = new PrintWriter(out); + this(new PrintWriter(out)); + } + + public BatchProgressMonitor(PrintWriter out) { + this.out = out; } @Override diff --git a/src/org/rascalmpl/library/util/TermREPL.java b/src/org/rascalmpl/library/util/TermREPL.java_disabled similarity index 100% rename from src/org/rascalmpl/library/util/TermREPL.java rename to src/org/rascalmpl/library/util/TermREPL.java_disabled diff --git a/src/org/rascalmpl/repl/BaseREPL.java b/src/org/rascalmpl/repl/BaseREPL.java_disabled old mode 100755 new mode 100644 similarity index 100% rename from src/org/rascalmpl/repl/BaseREPL.java rename to src/org/rascalmpl/repl/BaseREPL.java_disabled diff --git a/src/org/rascalmpl/repl/BaseREPL2.java b/src/org/rascalmpl/repl/BaseREPL2.java new file mode 100644 index 00000000000..28ef7bf90c3 --- /dev/null +++ b/src/org/rascalmpl/repl/BaseREPL2.java @@ -0,0 +1,153 @@ +package org.rascalmpl.repl; + +import java.io.IOException; +import java.io.PrintWriter; +import java.util.HashMap; + +import org.checkerframework.checker.nullness.qual.MonotonicNonNull; +import org.jline.reader.LineReader; +import org.jline.reader.LineReaderBuilder; +import org.jline.reader.UserInterruptException; +import org.jline.reader.LineReader.Option; +import org.jline.reader.impl.completer.AggregateCompleter; +import org.jline.reader.impl.history.DefaultHistory; +import org.jline.terminal.Terminal; +import org.jline.utils.ShutdownHooks; + +public class BaseREPL2 { + + private final IREPLService replService; + private final Terminal term; + private final LineReader reader; + private volatile boolean keepRunning = true; + private final @MonotonicNonNull DefaultHistory history; + private String currentPrompt; + private static final String FALLBACK_MIME_TYPE = "text/plain"; + private static final String ANSI_MIME_TYPE = "text/x-ansi"; + private final boolean ansiSupported; + private final String mimeType; + + public BaseREPL2(IREPLService replService, Terminal term) { + this.replService = replService; + this.term = term; + + var reader = LineReaderBuilder.builder() + .appName(replService.name()) + .terminal(term) + .parser(replService.inputParser()) + ; + + if (replService.storeHistory()) { + reader.variable(LineReader.HISTORY_FILE, replService.historyFile()); + this.history = new DefaultHistory(); + reader.history(this.history); + ShutdownHooks.add(this.history::save); + } else { + this.history = null; + } + reader.option(Option.HISTORY_IGNORE_DUPS, replService.historyIgnoreDuplicates()); + + if (replService.supportsCompletion()) { + reader.completer(new AggregateCompleter(replService.completers())); + } + + switch (term.getType()) { + case Terminal.TYPE_DUMB: + this.ansiSupported = false; + this.mimeType = FALLBACK_MIME_TYPE; + break; + case Terminal.TYPE_DUMB_COLOR: + this.ansiSupported = false; + this.mimeType = ANSI_MIME_TYPE; + break; + default: + this.ansiSupported = true; + this.mimeType = ANSI_MIME_TYPE; + break; + } + this.currentPrompt = replService.prompt(ansiSupported); + reader.variable(LineReader.SECONDARY_PROMPT_PATTERN, replService.parseErrorPrompt(ansiSupported)); + + this.reader = reader.build(); + + + + // todo: + // - ctrl + c support + // - ctrl + / support + // - multi-line input + // - highlighting in the prompt? + // - + + } + + public void run() throws IOException { + try { + replService.connect(term); + while (keepRunning) { + String line = reader.readLine(this.currentPrompt); + if (line == null) { + // EOF + break; + } + try { + handleInput(line); + } + catch (UserInterruptException u) { + reader.printAbove(""); + term.flush(); + replService.handleReset(new HashMap<>(), new HashMap<>()); + } + } + } + catch (InterruptedException _e) { + // closing the runner + } + catch (Throwable e) { + + var err = replService.errorWriter(); + if (err.checkError()) { + err = new PrintWriter(System.err, false); + } + + err.println("Unexpected (uncaught) exception, closing the REPL: "); + err.print(e.toString()); + e.printStackTrace(err); + + err.flush(); + + throw e; + } + finally { + try { + replService.flush(); + } catch (Throwable _t) { /* ignore */ } + term.flush(); + if (this.history != null) { + ShutdownHooks.remove(this.history::save); + this.history.save(); + } + } + } + + + private void handleInput(String line) throws InterruptedException { + var result = new HashMap(); + var meta = new HashMap(); + replService.handleInput(line, result, meta); + writeResult(result); + } + + private void writeResult(HashMap result) { + var writer = result.get(this.mimeType); + if (writer == null) { + writer = result.get(FALLBACK_MIME_TYPE); + } + if (writer == null) { + replService.outputWriter().println("Ok"); + } + else { + writer.write(replService.outputWriter()); + } + } +} diff --git a/src/org/rascalmpl/repl/IOutputPrinter.java b/src/org/rascalmpl/repl/IOutputPrinter.java new file mode 100644 index 00000000000..b2158b3b5c1 --- /dev/null +++ b/src/org/rascalmpl/repl/IOutputPrinter.java @@ -0,0 +1,9 @@ +package org.rascalmpl.repl; + +import java.io.PrintWriter; +import java.io.Reader; + +public interface IOutputPrinter { + void write(PrintWriter target); + Reader asReader(); +} diff --git a/src/org/rascalmpl/repl/IREPLService.java b/src/org/rascalmpl/repl/IREPLService.java new file mode 100644 index 00000000000..0e062e20b13 --- /dev/null +++ b/src/org/rascalmpl/repl/IREPLService.java @@ -0,0 +1,86 @@ +package org.rascalmpl.repl; + +import java.io.PrintWriter; +import java.nio.file.Path; +import java.util.Collections; +import java.util.List; +import java.util.Map; + +import org.jline.reader.Completer; +import org.jline.reader.Parser; +import org.jline.reader.impl.DefaultParser; +import org.jline.terminal.Terminal; + +public interface IREPLService { + + String MIME_PLAIN = "text/plain"; + String MIME_ANSI = "text/x-ansi"; + + default boolean supportsCompletion() { + return false; + } + + /** + * Supply completers for this REPL. + * Note that a completor is only triggered on a per word basis, so you might want to overwrite {@see #completionParser()} + */ + default List completers() { + return Collections.emptyList(); + } + + /** + * This parser is respossible for multi-line support, as well as word splitting for completion. + */ + default Parser inputParser() { + return new DefaultParser(); + } + + + default boolean storeHistory() { + return false; + } + + default boolean historyIgnoreDuplicates() { + return true; + } + + default Path historyFile() { + throw new IllegalAccessError("Not implemented if storeHistory is false"); + } + + /** + * Name of the REPL, no ansi allowed + */ + default String name() { return "Rascal REPL"; } + + + /** + * Check if an input is valid, for multi-line support + */ + boolean isInputComplete(String input); + + + // todo see if we really need the meta-data + void handleInput(String input, Map output, Map metadata) throws InterruptedException; + + + // todo see if we really need the meta-data + void handleReset(Map output, Map metadata) throws InterruptedException; + + /** + * Default prompt + */ + String prompt(boolean ansiSupported); + /** + * Continuation prompt + */ + String parseErrorPrompt(boolean ansiSupported); + + void connect(Terminal term); + + PrintWriter errorWriter(); + PrintWriter outputWriter(); + + void flush(); + +} diff --git a/src/org/rascalmpl/repl/RascalInterpreterREPL.java b/src/org/rascalmpl/repl/RascalInterpreterREPL.java_disabled similarity index 100% rename from src/org/rascalmpl/repl/RascalInterpreterREPL.java rename to src/org/rascalmpl/repl/RascalInterpreterREPL.java_disabled diff --git a/src/org/rascalmpl/repl/RascalLineParser.java b/src/org/rascalmpl/repl/RascalLineParser.java new file mode 100644 index 00000000000..54a06549ff2 --- /dev/null +++ b/src/org/rascalmpl/repl/RascalLineParser.java @@ -0,0 +1,275 @@ +package org.rascalmpl.repl; + +import java.util.ArrayList; +import java.util.List; +import java.util.function.Supplier; +import java.util.regex.Pattern; +import java.util.stream.Collectors; + +import org.checkerframework.checker.nullness.qual.MonotonicNonNull; +import org.checkerframework.checker.nullness.qual.Nullable; +import org.jline.reader.EOFError; +import org.jline.reader.ParsedLine; +import org.jline.reader.Parser; +import org.jline.reader.SyntaxError; +import org.rascalmpl.interpreter.IEvaluator; +import org.rascalmpl.interpreter.NullRascalMonitor; +import org.rascalmpl.parser.gtd.exception.ParseError; +import org.rascalmpl.uri.URIUtil; +import org.rascalmpl.values.parsetrees.ITree; +import org.rascalmpl.values.parsetrees.TreeAdapter; + +import io.usethesource.vallang.ISourceLocation; + +public class RascalLineParser implements Parser { + + private static final ISourceLocation PROMPT_LOCATION = URIUtil.rootLocation("prompt"); + private final Supplier> eval; + + public RascalLineParser(Supplier> eval) { + this.eval = eval; + } + + @Override + public ParsedLine parse(String line, int cursor, ParseContext context) throws SyntaxError { + switch (context) { + case UNSPECIFIED: // TODO: check if this is correct + case ACCEPT_LINE: + // we have to verify the input is correct rascal statement + return parseFullRascalCommand(line, cursor); + case COMPLETE: + try { + // lets see, maybe it parses as a rascal expression + return parseFullRascalCommand(line, cursor); + } + catch (EOFError e) { + // otherwise fallback to regex party + return splitWordsOnly(line, cursor); + } + case SECONDARY_PROMPT: + throw new SyntaxError(-1, -1, "Unsupported SECONDARY_PROMPT"); + case SPLIT_LINE: + throw new SyntaxError(-1, -1, "Unsupported SPLIT_LINE"); + default: + throw new UnsupportedOperationException("Unimplemented context: " + context); + } + } + + private ParsedLine splitWordsOnly(String line, int cursor) { + // small line parser, in the future we might be able to use error recovery + var words = new ArrayList(); + parseWords(line, 0, words); + return new ParsedLine() { + private final @MonotonicNonNull LexedWord atCursor = words.stream() + .filter(l -> l.cursorInside(cursor)) + .findFirst() + .orElse(null); + @Override + public String word() { + return atCursor == null ? "" : atCursor.word(); + } + + @Override + public int wordCursor() { + return atCursor == null ? -1 : (cursor - atCursor.begin); + } + + @Override + public int wordIndex() { + return atCursor == null ? -1 : words.indexOf(atCursor); + } + + @Override + public List words() { + return words.stream() + .map(LexedWord::word) + .collect(Collectors.toList()); + } + + @Override + public String line() { + return line; + } + + @Override + public int cursor() { + return cursor; + } + + }; + } + + private void parseWords(String buffer, int position, List words) { + /** are we interpolating inside of a string */ + boolean inString = false; + boolean inLocation = false; + while (position < buffer.length()) { + position = eatWhiteSpace(buffer, position); + if (position >= buffer.length()) { + return; + } + char c = buffer.charAt(position); + boolean isWord = true; + int wordEnd = position; + if (c == '"' || (c == '>' && inString)) { + wordEnd = parseEndedAfter(buffer, position, RASCAL_STRING); + inString = wordEnd != position && buffer.charAt(wordEnd - 1) != '"'; + isWord = false; + } + else if (c == '|' || (c == '>' && inLocation)) { + wordEnd = parseEndedAfter(buffer, position, RASCAL_LOCATION); + inLocation = wordEnd != position && buffer.charAt(position - 1) == '<'; + } + else if (Character.isJavaIdentifierPart(c) || c == ':' || c == '\\') { + wordEnd = parseEndedAfter(buffer, position, RASCAL_NAME_OR_COMMAND); + } + else { + wordEnd++; + isWord = false; + } + + if (wordEnd == position) { + wordEnd = buffer.length(); + } + + if (isWord) { + words.add(new LexedWord(buffer, position, wordEnd)); + } + position = wordEnd; + } + } + + private static class LexedWord { + + private final String buffer; + private final int begin; + private final int end; + + public LexedWord(String buffer, int begin, int end) { + this.buffer = buffer; + this.begin = begin; + this.end = end; + } + + public boolean cursorInside(int cursor) { + // TODO: review around edges + return begin <= cursor && cursor <= end; + } + + String word() { + return buffer.substring(begin, end); + } + } + + private static int parseEndedAfter(String buffer, int position, Pattern parser) { + var matcher = parser.matcher(buffer); + matcher.region(position, buffer.length()); + if (!matcher.find()) { + return position; + } + return position + matcher.end(); + } + + // strings with rudementary interpolation support + private static final Pattern RASCAL_STRING + = Pattern.compile("^[\">]([^\"<\\\\]|([\\\\].))*[\"<]"); + // locations with rudementary interpolation support + private static final Pattern RASCAL_LOCATION + = Pattern.compile("^[\\|\\>][^\\|\\<\\t-\\n\\r ]*[\\|\\<]?"); + + private static final Pattern RASCAL_NAME_OR_COMMAND + = Pattern.compile("^(([:]?[A-Za-z_][A-Za-z0-9_]*)|([\\\\][A-Za-z_][\\-A-Za-z0-9_]*))"); + + // only unicode spaces & multi-line comments + private static final Pattern RASCAL_WHITE_SPACE + = Pattern.compile("^(\\p{Zs}|([/][*]([^*]|([*][^/]))*[*][/]))*"); + + private int eatWhiteSpace(String buffer, int position) { + return parseEndedAfter(buffer, position, RASCAL_WHITE_SPACE); + } + + private ParsedLine parseFullRascalCommand(String line, int cursor) throws SyntaxError { + // TODO: to support inline highlighting, we have to remove the ansi escapes before parsing + try { + return translateTree(eval.get().parseCommand(new NullRascalMonitor(), line, PROMPT_LOCATION), line, cursor); + } + catch (ParseError pe) { + throw new EOFError(pe.getBeginLine(), pe.getBeginColumn(), "Parse error"); + } + catch (Throwable e) { + throw new EOFError(-1, -1, "Unexpected failure during pasing of the command: " + e.getMessage()); + } + } + + private ParsedLine translateTree(ITree command, String line, int cursor) { + // todo: return CompletingParsedLine so that we can also help with quoting completion + return new ParsedLine() { + List wordList = null; + ITree wordTree = null; + + private ITree cursorTree() { + if (wordTree == null) { + wordTree = (ITree)TreeAdapter.locateLexical(command, cursor); + if (wordTree == null) { + wordTree = command; + } + } + return wordTree; + } + + private List wordList() { + if (wordList == null) { + wordList = new ArrayList<>(); + collectWords(command, wordList); + } + return wordList; + } + + private void collectWords(ITree t, List words) { + if (TreeAdapter.isLexical(t)) { + words.add(t); + } + else if (TreeAdapter.isSort(t)) { + for (var c : t.getArgs()) { + if (c instanceof ITree) { + collectWords((ITree)c, words); + } + } + } + } + + @Override + public String word() { + return TreeAdapter.yield(cursorTree()); + } + + @Override + public int wordCursor() { + return cursor - TreeAdapter.getLocation(cursorTree()).getOffset(); + } + + @Override + public int wordIndex() { + return wordList().indexOf(cursorTree()); + } + + @Override + public List words() { + return wordList() + .stream() + .map(TreeAdapter::yield) + .collect(Collectors.toList()); + } + + @Override + public String line() { + return line; + } + + @Override + public int cursor() { + return cursor; + } + }; + } +} diff --git a/src/org/rascalmpl/repl/RascalReplServices.java b/src/org/rascalmpl/repl/RascalReplServices.java new file mode 100644 index 00000000000..0407c91dfa3 --- /dev/null +++ b/src/org/rascalmpl/repl/RascalReplServices.java @@ -0,0 +1,302 @@ +package org.rascalmpl.repl; + +import static org.rascalmpl.interpreter.utils.ReadEvalPrintDialogMessages.parseErrorMessage; +import static org.rascalmpl.interpreter.utils.ReadEvalPrintDialogMessages.staticErrorMessage; +import static org.rascalmpl.interpreter.utils.ReadEvalPrintDialogMessages.throwMessage; +import static org.rascalmpl.interpreter.utils.ReadEvalPrintDialogMessages.throwableMessage; + +import java.io.IOException; +import java.io.PrintWriter; +import java.io.Reader; +import java.io.StringReader; +import java.io.StringWriter; +import java.io.Writer; +import java.util.Map; +import java.util.Objects; +import java.util.function.Function; + +import org.jline.jansi.Ansi; +import org.jline.reader.Parser; +import org.jline.terminal.Terminal; +import org.jline.utils.InfoCmp.Capability; +import org.rascalmpl.exceptions.Throw; +import org.rascalmpl.interpreter.Evaluator; +import org.rascalmpl.interpreter.control_exceptions.InterruptException; +import org.rascalmpl.interpreter.control_exceptions.QuitException; +import org.rascalmpl.interpreter.result.IRascalResult; +import org.rascalmpl.interpreter.result.Result; +import org.rascalmpl.interpreter.staticErrors.StaticError; +import org.rascalmpl.parser.gtd.exception.ParseError; +import org.rascalmpl.uri.URIUtil; +import org.rascalmpl.values.RascalValueFactory; +import org.rascalmpl.values.parsetrees.TreeAdapter; + +import io.usethesource.vallang.IConstructor; +import io.usethesource.vallang.IString; +import io.usethesource.vallang.IValue; +import io.usethesource.vallang.io.StandardTextWriter; +import io.usethesource.vallang.type.Type; + +public class RascalReplServices implements IREPLService { + private final Function buildEvaluator; + private Evaluator eval; + private static final StandardTextWriter ansiIndentedPrinter = new ReplTextWriter(true); + private static final StandardTextWriter plainIndentedPrinter = new StandardTextWriter(true); + private final static int LINE_LIMIT = 200; + private final static int CHAR_LIMIT = LINE_LIMIT * 20; + private String newline = System.lineSeparator(); + + + public RascalReplServices(Function buildEvaluator) { + super(); + this.buildEvaluator = buildEvaluator; + } + + @Override + public void connect(Terminal term) { + if (eval != null) { + throw new IllegalStateException("REPL is already initialized"); + } + newline = term.getStringCapability(Capability.newline); + if (newline == null) { + newline = System.lineSeparator(); + } + this.eval = buildEvaluator.apply(term); + } + + @Override + public Parser inputParser() { + return new RascalLineParser(() -> eval); + } + + @Override + public boolean isInputComplete(String input) { + throw new UnsupportedOperationException("Unimplemented method 'isInputComplete'"); + } + + + @Override + public void handleInput(String input, Map output, Map metadata) + throws InterruptedException { + synchronized(eval) { + Objects.requireNonNull(eval, "Not initialized yet"); + try { + Result value; + value = eval.eval(eval.getMonitor(), input, URIUtil.rootLocation("prompt")); + outputResult(output, value); + } + catch (InterruptException ex) { + reportError(output, (w, sw) -> { + w.println("Interrupted"); + ex.getRascalStackTrace().prettyPrintedString(w, sw); + }); + } + catch (ParseError pe) { + reportError(output, (w, sw) -> { + parseErrorMessage(w, input, "prompt", pe, sw); + }); + } + catch (StaticError e) { + reportError(output, (w, sw) -> { + staticErrorMessage(w, e, sw); + }); + } + catch (Throw e) { + reportError(output, (w, sw) -> { + throwMessage(w,e, sw); + }); + } + catch (QuitException q) { + reportError(output, (w, sw) -> { + w.println("Quiting REPL"); + }); + } + catch (Throwable e) { + reportError(output, (w, sw) -> { + throwableMessage(w, e, eval.getStackTrace(), sw); + }); + } + } + } + + private void outputResult(Map output, IRascalResult result) { + if (result == null || result.getValue() == null) { + output.put(MIME_PLAIN, new StringOutputPrinter("ok", newline)); + return; + } + IValue value = result.getValue(); + Type type = result.getStaticType(); + + if (type.isSubtypeOf(RascalValueFactory.Content) && !type.isBottom()) { + output.put(MIME_PLAIN, new StringOutputPrinter("Serving content", newline)); + // TODO: serve content! + return; + } + + ThrowingWriter resultWriter; + if (type.isAbstractData() && type.isStrictSubtypeOf(RascalValueFactory.Tree) && !type.isBottom()) { + resultWriter = (w, sw) -> { + w.write("(" + type.toString() +") `"); + TreeAdapter.yield((IConstructor)value, sw == ansiIndentedPrinter, w); + w.write("`"); + }; + } + else if (type.isString()) { + resultWriter = (w, sw) -> { + // TODO: do something special for the reader version of IString, when that is released + // for now, we only support write + + try (Writer wrt = new LimitedWriter(new LimitedLineWriter(w, LINE_LIMIT), CHAR_LIMIT)) { + sw.write(value, wrt); + } + catch (/*IOLimitReachedException*/ RuntimeException e) { + // ignore since this is what we wanted + // if we catch IOLimitReachedException we get an IllegalArgument exception instead + // "Self-suppression not permitted" + } + w.println(); + w.println("---"); + try (Writer wrt = new LimitedWriter(new LimitedLineWriter(w, LINE_LIMIT), CHAR_LIMIT)) { + ((IString) value).write(wrt); + } + catch (/*IOLimitReachedException*/ RuntimeException e) { + // ignore since this is what we wanted + // if we catch IOLimitReachedException we get an IllegalArgument exception instead + // "Self-suppression not permitted" + } + w.println(); + w.print("---"); + }; + } + else { + resultWriter = (w, sw) -> { + try (Writer wrt = new LimitedWriter(new LimitedLineWriter(w, LINE_LIMIT), CHAR_LIMIT)) { + sw.write(value, wrt); + } + catch (/*IOLimitReachedException*/ RuntimeException e) { + // ignore since this is what we wanted + // if we catch IOLimitReachedException we get an IllegalArgument exception instead + // "Self-suppression not permitted" + } + }; + } + + ThrowingWriter typePrefixed = (w, sw) -> { + w.write(type.toString()); + w.write(": "); + resultWriter.write(w, sw); + w.println(); + }; + + output.put(MIME_PLAIN, new ExceptionPrinter(typePrefixed, plainIndentedPrinter)); + output.put(MIME_ANSI, new ExceptionPrinter(typePrefixed, ansiIndentedPrinter)); + + } + + private static void reportError(Map output, ThrowingWriter writer) { + output.put(MIME_PLAIN, new ExceptionPrinter(writer, plainIndentedPrinter)); + output.put(MIME_ANSI, new ExceptionPrinter(writer, ansiIndentedPrinter)); + } + + @FunctionalInterface + private static interface ThrowingWriter { + void write(PrintWriter writer, StandardTextWriter prettyPrinter) throws IOException; + } + + private static class ExceptionPrinter implements IOutputPrinter { + private final ThrowingWriter internalWriter; + private final StandardTextWriter prettyPrinter; + + public ExceptionPrinter(ThrowingWriter internalWriter, StandardTextWriter prettyPrinter) { + this.internalWriter = internalWriter; + this.prettyPrinter = prettyPrinter; + } + + @Override + public Reader asReader() { + try (var result = new StringWriter()) { + try (var resultWriter = new PrintWriter(result)) { + write(resultWriter); + } + return new StringReader(result.toString()); + } + catch (IOException ex) { + throw new IllegalStateException("StringWriter close should never throw exception", ex); + } + } + + @Override + public void write(PrintWriter target) { + try { + internalWriter.write(target, prettyPrinter); + } + catch (IOException e) { + target.println("Internal failure: printing exception failed with:"); + target.println(e.toString()); + e.printStackTrace(target); + } + } + } + + private static class StringOutputPrinter implements IOutputPrinter { + private final String value; + private final String newline; + + public StringOutputPrinter(String value, String newline) { + this.value = value; + this.newline = newline; + } + + @Override + public void write(PrintWriter target) { + target.println(value); + } + + @Override + public Reader asReader() { + return new StringReader(value + newline); + } + } + + @Override + public void handleReset(Map output, Map metadata) + throws InterruptedException { + // TODO Auto-generated method stub + throw new UnsupportedOperationException("Unimplemented method 'handleReset'"); + } + + @Override + public String prompt(boolean ansiSupported) { + if (ansiSupported) { + return Ansi.ansi().reset().bold() + "rascal>" + Ansi.ansi().reset(); + } + return "rascal>"; + } + + @Override + public String parseErrorPrompt(boolean ansiSupported) { + if (ansiSupported) { + return Ansi.ansi().reset().bold() + ">>>>>>>" + Ansi.ansi().reset(); + } + return ">>>>>>>"; + } + + + @Override + public PrintWriter errorWriter() { + return eval.getStdErr(); + } + + @Override + public PrintWriter outputWriter() { + return eval.getStdOut(); + } + + @Override + public void flush() { + // TODO figure out why this function is called? + eval.getStdErr().flush(); + eval.getStdOut().flush(); + } + +} diff --git a/src/org/rascalmpl/repl/SourceLocationHistory.java b/src/org/rascalmpl/repl/SourceLocationHistory.java_disabled similarity index 100% rename from src/org/rascalmpl/repl/SourceLocationHistory.java rename to src/org/rascalmpl/repl/SourceLocationHistory.java_disabled diff --git a/src/org/rascalmpl/repl/TerminalProgressBarMonitor.java b/src/org/rascalmpl/repl/TerminalProgressBarMonitor.java index d5175da570c..0174bdf67a8 100644 --- a/src/org/rascalmpl/repl/TerminalProgressBarMonitor.java +++ b/src/org/rascalmpl/repl/TerminalProgressBarMonitor.java @@ -10,6 +10,7 @@ import java.util.List; import org.jline.terminal.Terminal; +import org.jline.utils.InfoCmp.Capability; import org.rascalmpl.debug.IRascalMonitor; import io.usethesource.vallang.ISourceLocation; @@ -61,6 +62,13 @@ public class TerminalProgressBarMonitor extends PrintWriter implements IRascalMo */ private final Terminal tm; + public static boolean shouldWorkIn(Terminal tm) { + return "\r".equals(tm.getStringCapability(Capability.carriage_return)) + && tm.getNumericCapability(Capability.columns) != null + && tm.getNumericCapability(Capability.lines) != null + && ANSI.supportsCapabilities(tm); + } + @SuppressWarnings("resource") public TerminalProgressBarMonitor(Terminal tm) { @@ -194,10 +202,10 @@ private int startOfLastLine(char[] buffer, int offset, int len) { } private void rawPrintln(String s) { - super.println(s); + rawWrite(s + System.lineSeparator()); } private void rawWrite(String s) { - super.write(s); + rawWrite(s, 0, s.length()); } private void rawWrite(int c) { @@ -388,6 +396,14 @@ private void eraseBars() { */ private static class ANSI { + private static boolean supportsCapabilities(Terminal tm) { + Integer cols = tm.getNumericCapability(Capability.max_colors); + if (cols == null || cols < 8) { + return false; + } + return tm.getStringCapability(Capability.clear_screen) != null; + } + public static String grey8Background() { return "\u001B[48;5;240m"; } @@ -396,26 +412,10 @@ public static String brightWhiteForeground() { return "\u001B[97m"; } - public static String delete() { - return "\u001B[D\u001B[K"; - } - static String moveUp(int n) { return "\u001B[" + n + "F"; } - static String overlined() { - return "\u001B[53m"; - } - - static String underlined() { - return "\u001B[4m"; - } - - public static String printCursorPosition() { - return "\u001B[6n"; - } - public static String noBackground() { return "\u001B[49m"; } @@ -424,10 +424,6 @@ public static String normal() { return "\u001B[0m"; } - public static String lightBackground() { - return "\u001B[48;5;250m"; - } - static String moveDown(int n) { return "\u001B[" + n + "E"; } diff --git a/src/org/rascalmpl/shell/ModuleRunner.java b/src/org/rascalmpl/shell/ModuleRunner.java index 450375f1335..03f929ca05b 100644 --- a/src/org/rascalmpl/shell/ModuleRunner.java +++ b/src/org/rascalmpl/shell/ModuleRunner.java @@ -1,8 +1,8 @@ package org.rascalmpl.shell; import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; +import java.io.PrintWriter; +import java.io.Reader; import org.rascalmpl.debug.IRascalMonitor; import org.rascalmpl.interpreter.Evaluator; @@ -15,7 +15,7 @@ public class ModuleRunner implements ShellRunner { private final Evaluator eval; - public ModuleRunner(InputStream input, OutputStream stdout, OutputStream stderr, IRascalMonitor monitor) { + public ModuleRunner(Reader input, PrintWriter stdout, PrintWriter stderr, IRascalMonitor monitor) { eval = ShellEvaluatorFactory.getDefaultEvaluator(input, stdout, stderr, monitor); } diff --git a/src/org/rascalmpl/shell/ParseTest.java b/src/org/rascalmpl/shell/ParseTest.java new file mode 100644 index 00000000000..ba93355ccc3 --- /dev/null +++ b/src/org/rascalmpl/shell/ParseTest.java @@ -0,0 +1,91 @@ +package org.rascalmpl.shell; + +import java.io.IOException; +import java.net.URISyntaxException; +import java.util.AbstractMap; +import java.util.ArrayList; +import java.util.LinkedList; +import java.util.List; +import java.util.Map.Entry; +import java.util.concurrent.TimeUnit; +import java.util.Queue; + +import org.rascalmpl.library.Prelude; +import org.rascalmpl.library.lang.rascal.syntax.RascalParser; +import org.rascalmpl.parser.Parser; +import org.rascalmpl.parser.gtd.result.out.DefaultNodeFlattener; +import org.rascalmpl.parser.uptr.UPTRNodeFactory; +import org.rascalmpl.parser.uptr.action.NoActionExecutor; +import org.rascalmpl.uri.URIResolverRegistry; +import org.rascalmpl.uri.URIUtil; +import org.rascalmpl.values.parsetrees.ITree; +import org.rascalmpl.values.parsetrees.TreeAdapter; + +import io.usethesource.vallang.IConstructor; +import io.usethesource.vallang.ISourceLocation; + +public class ParseTest { + + private static ITree parseFile(ISourceLocation f, char[] data) { + return new RascalParser().parse(Parser.START_MODULE, f.getURI(), data, new NoActionExecutor(), + new DefaultNodeFlattener(), new UPTRNodeFactory(true)); + } + + private static void progress(String s, Object... args) { + System.err.println(String.format(s, args)); + } + + private static List> findFiles() throws URISyntaxException, IOException { + final var reg = URIResolverRegistry.getInstance(); + Queue worklist = new LinkedList<>(); + worklist.add(URIUtil.correctLocation("cwd", null, "/src")); + // URIUtil.createFileLocation("d:\\swat.engineering\\rascal\\rascal\\src")); + List> result = new ArrayList<>(); + ISourceLocation next; + while ((next = worklist.poll()) != null) { + var children = reg.list(next); + for (var c: children) { + if (reg.isDirectory(c)) { + worklist.add(c); + } + else if (c.getPath().endsWith(".rsc")) { + try (var r = reg.getCharacterReader(c)) { + result.add(new AbstractMap.SimpleEntry<>(c, Prelude.consumeInputStream(r).toCharArray())); + } + } + } + } + return result; + + + } + + static final int parsingRounds = 10; + static final int warmupRounds = 2; + + public static void main(String[] args) throws URISyntaxException, IOException { + progress("Collecting rascal files"); + var targets = findFiles(); + progress("Found: %d files, press enter to start parsing", targets.size()); + System.in.read(); + Thread.currentThread().setPriority(Thread.MAX_PRIORITY); + progress("Start parsing"); + + long count = 0; + long totalTime = 0; + + for (int i = 0; i < warmupRounds + parsingRounds; i++) { + var start = System.nanoTime(); + for (var t : targets) { + var tr = parseFile(t.getKey(), t.getValue()); + count += TreeAdapter.getLocation(TreeAdapter.getStartTop(tr)).getLength(); + } + var stop = System.nanoTime(); + progress("%s took: %d ms", i < warmupRounds ? "Warmup" : "Parsing", TimeUnit.NANOSECONDS.toMillis(stop - start)); + if (i >= warmupRounds) { + totalTime += stop - start; + } + } + progress("Done: %d, average time: %d ms", count, TimeUnit.NANOSECONDS.toMillis(totalTime / parsingRounds)); + } +} diff --git a/src/org/rascalmpl/shell/REPLRunner.java b/src/org/rascalmpl/shell/REPLRunner.java_disabled old mode 100755 new mode 100644 similarity index 100% rename from src/org/rascalmpl/shell/REPLRunner.java rename to src/org/rascalmpl/shell/REPLRunner.java_disabled diff --git a/src/org/rascalmpl/shell/RascalShell.java b/src/org/rascalmpl/shell/RascalShell.java_disabled similarity index 100% rename from src/org/rascalmpl/shell/RascalShell.java rename to src/org/rascalmpl/shell/RascalShell.java_disabled diff --git a/src/org/rascalmpl/shell/RascalShell2.java b/src/org/rascalmpl/shell/RascalShell2.java index 5bb3d3ea41c..1df8cddf950 100644 --- a/src/org/rascalmpl/shell/RascalShell2.java +++ b/src/org/rascalmpl/shell/RascalShell2.java @@ -13,55 +13,23 @@ package org.rascalmpl.shell; -import static org.rascalmpl.interpreter.utils.ReadEvalPrintDialogMessages.parseErrorMessage; -import static org.rascalmpl.interpreter.utils.ReadEvalPrintDialogMessages.staticErrorMessage; -import static org.rascalmpl.interpreter.utils.ReadEvalPrintDialogMessages.throwMessage; -import static org.rascalmpl.interpreter.utils.ReadEvalPrintDialogMessages.throwableMessage; - import java.io.IOException; -import java.io.OutputStream; import java.io.PrintWriter; -import java.io.StringWriter; -import java.nio.ByteBuffer; -import java.nio.CharBuffer; -import java.nio.charset.CharsetDecoder; -import java.nio.charset.CodingErrorAction; -import java.nio.charset.StandardCharsets; -import org.apache.commons.io.input.NullInputStream; -import org.jline.jansi.Ansi; -import org.jline.reader.LineReader; -import org.jline.reader.LineReaderBuilder; -import org.jline.reader.impl.completer.StringsCompleter; -import org.jline.reader.impl.history.DefaultHistory; -import org.jline.style.StyleResolver; import org.jline.terminal.TerminalBuilder; -import org.rascalmpl.exceptions.Throw; import org.rascalmpl.ideservices.BasicIDEServices; import org.rascalmpl.ideservices.IDEServices; import org.rascalmpl.interpreter.Evaluator; -import org.rascalmpl.interpreter.control_exceptions.InterruptException; -import org.rascalmpl.interpreter.control_exceptions.QuitException; import org.rascalmpl.interpreter.env.GlobalEnvironment; import org.rascalmpl.interpreter.env.ModuleEnvironment; import org.rascalmpl.interpreter.load.StandardLibraryContributor; -import org.rascalmpl.interpreter.result.Result; -import org.rascalmpl.interpreter.staticErrors.StaticError; import org.rascalmpl.interpreter.utils.RascalManifest; -import org.rascalmpl.parser.gtd.exception.ParseError; -import org.rascalmpl.repl.ReplTextWriter; +import org.rascalmpl.repl.BaseREPL2; +import org.rascalmpl.repl.RascalReplServices; import org.rascalmpl.repl.TerminalProgressBarMonitor; -import org.rascalmpl.repl.TerminalProgressBarMonitor2; -import org.rascalmpl.uri.URIResolverRegistry; -import org.rascalmpl.uri.URIUtil; -import org.rascalmpl.values.RascalValueFactory; import org.rascalmpl.values.ValueFactoryFactory; -import org.rascalmpl.values.parsetrees.TreeAdapter; -import io.usethesource.vallang.IConstructor; -import io.usethesource.vallang.IValue; import io.usethesource.vallang.IValueFactory; -import io.usethesource.vallang.type.Type; public class RascalShell2 { @@ -74,18 +42,27 @@ public static void main(String[] args) throws IOException { System.setProperty("apple.awt.UIElement", "true"); // turns off the annoying desktop icon printVersionNumber(); - try { + //try { var term = TerminalBuilder.builder() .color(true) - .encoding(StandardCharsets.UTF_8) .build(); - var reader = LineReaderBuilder.builder() - .appName("Rascal REPL") - .completer(new StringsCompleter("IO", "IOMeer", "println", "print", "printlnExp")) - .terminal(term) - .history(new DefaultHistory()) - .build(); + + var repl = new BaseREPL2(new RascalReplServices((t) -> { + var monitor = new TerminalProgressBarMonitor(term); + IDEServices services = new BasicIDEServices(term.writer(), monitor); + + + GlobalEnvironment heap = new GlobalEnvironment(); + ModuleEnvironment root = heap.addModule(new ModuleEnvironment(ModuleEnvironment.SHELL_MODULE, heap)); + IValueFactory vf = ValueFactoryFactory.getValueFactory(); + Evaluator evaluator = new Evaluator(vf, term.reader(), new PrintWriter(System.err, true), term.writer(), root, heap, monitor); + evaluator.addRascalSearchPathContributor(StandardLibraryContributor.getInstance()); + return evaluator; + }), term); + repl.run(); + /* + //IRascalMonitor monitor = IRascalMonitor.buildConsoleMonitor(System.in, System.out, true); var monitor = new TerminalProgressBarMonitor(term); @@ -175,122 +152,10 @@ public static void main(String[] args) throws IOException { System.out.flush(); System.err.flush(); } - } - - private static class FakeOutput extends OutputStream { - - private final LineReader target; - private final CharsetDecoder decoder; - private final CharBuffer decoded; - - FakeOutput(LineReader target) { - this.target = target; - this.decoder = StandardCharsets.UTF_8.newDecoder(); - this.decoder.replaceWith("?"); - this.decoder.onMalformedInput(CodingErrorAction.REPLACE); - this.decoder.onUnmappableCharacter(CodingErrorAction.REPLACE); - this.decoded = CharBuffer.allocate(1024); - } - - @Override - public void write(int b) throws IOException { - var res = decoder.decode(ByteBuffer.wrap(new byte[]{ (byte)b }), decoded, false); - if (res.isOverflow()) { - flush(); - write(b); - } - } - - @Override - public void write(byte[] b, int off, int len) throws IOException { - var bytes = ByteBuffer.wrap(b, off, len); - while (bytes.hasRemaining()) { - var res = decoder.decode(bytes, decoded, false); - if (res.isOverflow()) { - flush(); - } - else if (res.isError()) { - throw new IOException("Decoding failed with: " + res); - } - } - } - - @Override - public void flush() throws IOException { - try { - decoded.flip(); - if (decoded.hasRemaining()) { - this.target.printAbove(decoded.toString()); - } - } - finally { - decoded.clear(); - } - } + */ } - private static class FakePrintWriter extends PrintWriter { - private final LineReader target; - private final CharBuffer buffer; - - public FakePrintWriter(LineReader target, boolean autoFlush) { - super(OutputStream.nullOutputStream(), autoFlush, StandardCharsets.UTF_8); - this.target = target; - this.buffer = CharBuffer.allocate(8*1024); - } - - - @Override - public void write(int c) { - makeRoom(1); - this.buffer.append((char)c); - } - - private void makeRoom(int i) { - if (this.buffer.remaining() < i) { - flush(); - } - } - - - @Override - public void write(String s, int off, int len) { - while (len > 0) { - makeRoom(len); - int room = Math.min(buffer.remaining(), len); - buffer.append(s, off, room); - off += room; - len -= room; - } - } - - @Override - public void write(char[] buf, int off, int len) { - while (len > 0) { - makeRoom(len); - int room = Math.min(buffer.remaining(), len); - buffer.put(buf, off, room); - off += room; - len -= room; - } - } - - @Override - public void flush() { - try { - buffer.flip(); - if (buffer.hasRemaining()) { - target.printAbove(buffer.toString()); - } - } - finally { - buffer.clear(); - } - } - - - } } diff --git a/src/org/rascalmpl/test/infrastructure/RascalJUnitTestRunner.java b/src/org/rascalmpl/test/infrastructure/RascalJUnitTestRunner.java index 87843801f92..55a4a983fce 100644 --- a/src/org/rascalmpl/test/infrastructure/RascalJUnitTestRunner.java +++ b/src/org/rascalmpl/test/infrastructure/RascalJUnitTestRunner.java @@ -22,6 +22,10 @@ import java.util.List; import java.util.Queue; +import org.jline.terminal.Terminal; +import org.jline.terminal.TerminalBuilder; +import org.jline.terminal.impl.DumbTerminal; +import org.jline.terminal.impl.DumbTerminalProvider; import org.junit.runner.Description; import org.junit.runner.Result; import org.junit.runner.Runner; @@ -38,7 +42,6 @@ import org.rascalmpl.interpreter.utils.RascalManifest; import org.rascalmpl.library.util.PathConfig; import org.rascalmpl.library.util.PathConfig.RascalConfigMode; -import org.rascalmpl.shell.RascalShell; import org.rascalmpl.shell.ShellEvaluatorFactory; import org.rascalmpl.uri.URIResolverRegistry; import org.rascalmpl.uri.URIUtil; @@ -52,7 +55,22 @@ public class RascalJUnitTestRunner extends Runner { private static class InstanceHolder { - final static IRascalMonitor monitor = IRascalMonitor.buildConsoleMonitor(System.in, System.out); + final static IRascalMonitor monitor; + static { + Terminal tm; + try { + tm = TerminalBuilder.terminal(); + } + catch (IOException e) { + try { + tm = new DumbTerminal(System.in, System.err); + } + catch (IOException e1) { + throw new IllegalStateException("Could not create a terminal representation"); + } + } + monitor = IRascalMonitor.buildConsoleMonitor(tm); + } } public static IRascalMonitor getCommonMonitor() { @@ -70,9 +88,6 @@ public static IRascalMonitor getCommonMonitor() { static { try { - RascalShell.setupWindowsCodepage(); - RascalShell.enableWindowsAnsiEscapesIfPossible(); - heap = new GlobalEnvironment(); root = heap.addModule(new ModuleEnvironment("___junit_test___", heap)); evaluator = new Evaluator(ValueFactoryFactory.getValueFactory(), Reader.nullReader(), new PrintWriter(System.err, true), new PrintWriter(System.out, false), root, heap, getCommonMonitor()); diff --git a/test/org/rascalmpl/MatchFingerprintTest.java b/test/org/rascalmpl/MatchFingerprintTest.java index be514a1c16e..e210f36a191 100644 --- a/test/org/rascalmpl/MatchFingerprintTest.java +++ b/test/org/rascalmpl/MatchFingerprintTest.java @@ -15,6 +15,7 @@ import static org.junit.Assert.assertNotEquals; import java.io.IOException; +import java.io.PrintWriter; import java.io.Reader; import java.io.StringReader; @@ -48,7 +49,7 @@ public class MatchFingerprintTest extends TestCase { private final GlobalEnvironment heap = new GlobalEnvironment(); private final ModuleEnvironment root = new ModuleEnvironment("root", heap); - private final Evaluator eval = new Evaluator(IRascalValueFactory.getInstance(), Reader.nullReader(), System.err, System.out, root, heap, RascalJUnitTestRunner.getCommonMonitor()); + private final Evaluator eval = new Evaluator(IRascalValueFactory.getInstance(), Reader.nullReader(), new PrintWriter(System.err, true), new PrintWriter(System.out), root, heap, RascalJUnitTestRunner.getCommonMonitor()); private final RascalFunctionValueFactory VF = eval.getFunctionValueFactory(); private final TypeFactory TF = TypeFactory.getInstance(); diff --git a/test/org/rascalmpl/test/functionality/ParallelEvaluatorsTests.java b/test/org/rascalmpl/test/functionality/ParallelEvaluatorsTests.java index 64f6d2e3a63..384422742bf 100644 --- a/test/org/rascalmpl/test/functionality/ParallelEvaluatorsTests.java +++ b/test/org/rascalmpl/test/functionality/ParallelEvaluatorsTests.java @@ -3,6 +3,8 @@ import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; +import java.io.PrintWriter; +import java.io.Reader; import java.util.concurrent.BrokenBarrierException; import java.util.concurrent.CyclicBarrier; import java.util.concurrent.TimeUnit; @@ -35,7 +37,7 @@ private static Evaluator freshEvaluator() { var heap = new GlobalEnvironment(); var root = heap.addModule(new ModuleEnvironment("___test___", heap)); - var evaluator = new Evaluator(ValueFactoryFactory.getValueFactory(), System.in, System.err, System.out, root, heap, monitor); + var evaluator = new Evaluator(ValueFactoryFactory.getValueFactory(), Reader.nullReader(), new PrintWriter(System.err, true), new PrintWriter(System.out), root, heap, monitor); evaluator.addRascalSearchPathContributor(StandardLibraryContributor.getInstance()); evaluator.setTestResultListener(new ITestResultListener() { From 3aa2626e38b6cff1eedf28483f38ba6707656f45 Mon Sep 17 00:00:00 2001 From: Davy Landman Date: Thu, 24 Oct 2024 15:06:29 +0200 Subject: [PATCH 10/35] Got ctrl+c to work correctly --- pom.xml | 2 +- src/org/rascalmpl/repl/BaseREPL2.java | 19 +++++++++++-------- .../rascalmpl/repl/RascalReplServices.java | 2 -- src/org/rascalmpl/shell/RascalShell2.java | 2 ++ 4 files changed, 14 insertions(+), 11 deletions(-) diff --git a/pom.xml b/pom.xml index efc23fa0b0d..56d23205047 100644 --- a/pom.xml +++ b/pom.xml @@ -477,7 +477,7 @@ ${jline.version} - org.jline + org.jline jline-style ${jline.version} diff --git a/src/org/rascalmpl/repl/BaseREPL2.java b/src/org/rascalmpl/repl/BaseREPL2.java index 28ef7bf90c3..64cd85d95ef 100644 --- a/src/org/rascalmpl/repl/BaseREPL2.java +++ b/src/org/rascalmpl/repl/BaseREPL2.java @@ -3,12 +3,13 @@ import java.io.IOException; import java.io.PrintWriter; import java.util.HashMap; +import java.util.Map; import org.checkerframework.checker.nullness.qual.MonotonicNonNull; import org.jline.reader.LineReader; +import org.jline.reader.LineReader.Option; import org.jline.reader.LineReaderBuilder; import org.jline.reader.UserInterruptException; -import org.jline.reader.LineReader.Option; import org.jline.reader.impl.completer.AggregateCompleter; import org.jline.reader.impl.history.DefaultHistory; import org.jline.terminal.Terminal; @@ -85,18 +86,20 @@ public void run() throws IOException { try { replService.connect(term); while (keepRunning) { - String line = reader.readLine(this.currentPrompt); - if (line == null) { - // EOF - break; - } try { + String line = reader.readLine(this.currentPrompt); + if (line == null) { + // EOF + break; + } handleInput(line); } catch (UserInterruptException u) { - reader.printAbove(""); + reader.printAbove("> interrupted"); term.flush(); - replService.handleReset(new HashMap<>(), new HashMap<>()); + var out = new HashMap(); + replService.handleReset(out, new HashMap<>()); + writeResult(out); } } } diff --git a/src/org/rascalmpl/repl/RascalReplServices.java b/src/org/rascalmpl/repl/RascalReplServices.java index 0407c91dfa3..8d3835e27f5 100644 --- a/src/org/rascalmpl/repl/RascalReplServices.java +++ b/src/org/rascalmpl/repl/RascalReplServices.java @@ -261,8 +261,6 @@ public Reader asReader() { @Override public void handleReset(Map output, Map metadata) throws InterruptedException { - // TODO Auto-generated method stub - throw new UnsupportedOperationException("Unimplemented method 'handleReset'"); } @Override diff --git a/src/org/rascalmpl/shell/RascalShell2.java b/src/org/rascalmpl/shell/RascalShell2.java index 1df8cddf950..fcf9bf0ddfa 100644 --- a/src/org/rascalmpl/shell/RascalShell2.java +++ b/src/org/rascalmpl/shell/RascalShell2.java @@ -15,6 +15,7 @@ import java.io.IOException; import java.io.PrintWriter; +import java.nio.charset.StandardCharsets; import org.jline.terminal.TerminalBuilder; import org.rascalmpl.ideservices.BasicIDEServices; @@ -45,6 +46,7 @@ public static void main(String[] args) throws IOException { //try { var term = TerminalBuilder.builder() .color(true) + .encoding(StandardCharsets.UTF_8) .build(); From de6d79fa36b6cec5e27ddf33f0daadee494a0994 Mon Sep 17 00:00:00 2001 From: Davy Landman Date: Thu, 24 Oct 2024 17:09:32 +0200 Subject: [PATCH 11/35] Added lines to the continuation prompt --- src/org/rascalmpl/repl/BaseREPL2.java | 6 ++++-- src/org/rascalmpl/repl/IREPLService.java | 4 ++-- src/org/rascalmpl/repl/RascalReplServices.java | 11 ++++++----- 3 files changed, 12 insertions(+), 9 deletions(-) diff --git a/src/org/rascalmpl/repl/BaseREPL2.java b/src/org/rascalmpl/repl/BaseREPL2.java index 64cd85d95ef..753a20f72a1 100644 --- a/src/org/rascalmpl/repl/BaseREPL2.java +++ b/src/org/rascalmpl/repl/BaseREPL2.java @@ -26,6 +26,7 @@ public class BaseREPL2 { private static final String FALLBACK_MIME_TYPE = "text/plain"; private static final String ANSI_MIME_TYPE = "text/x-ansi"; private final boolean ansiSupported; + private final boolean unicodeSupported; private final String mimeType; public BaseREPL2(IREPLService replService, Terminal term) { @@ -66,8 +67,9 @@ public BaseREPL2(IREPLService replService, Terminal term) { this.mimeType = ANSI_MIME_TYPE; break; } - this.currentPrompt = replService.prompt(ansiSupported); - reader.variable(LineReader.SECONDARY_PROMPT_PATTERN, replService.parseErrorPrompt(ansiSupported)); + this.unicodeSupported = term.encoding().newEncoder().canEncode("👍🏽"); + this.currentPrompt = replService.prompt(ansiSupported, unicodeSupported); + reader.variable(LineReader.SECONDARY_PROMPT_PATTERN, replService.parseErrorPrompt(ansiSupported, unicodeSupported)); this.reader = reader.build(); diff --git a/src/org/rascalmpl/repl/IREPLService.java b/src/org/rascalmpl/repl/IREPLService.java index 0e062e20b13..4729b23e35e 100644 --- a/src/org/rascalmpl/repl/IREPLService.java +++ b/src/org/rascalmpl/repl/IREPLService.java @@ -70,11 +70,11 @@ default Path historyFile() { /** * Default prompt */ - String prompt(boolean ansiSupported); + String prompt(boolean ansiSupported, boolean unicodeSupported); /** * Continuation prompt */ - String parseErrorPrompt(boolean ansiSupported); + String parseErrorPrompt(boolean ansiSupported, boolean unicodeSupported); void connect(Terminal term); diff --git a/src/org/rascalmpl/repl/RascalReplServices.java b/src/org/rascalmpl/repl/RascalReplServices.java index 8d3835e27f5..30afb508a15 100644 --- a/src/org/rascalmpl/repl/RascalReplServices.java +++ b/src/org/rascalmpl/repl/RascalReplServices.java @@ -264,19 +264,20 @@ public void handleReset(Map output, Map } @Override - public String prompt(boolean ansiSupported) { + public String prompt(boolean ansiSupported, boolean unicodeSupported) { if (ansiSupported) { - return Ansi.ansi().reset().bold() + "rascal>" + Ansi.ansi().reset(); + return Ansi.ansi().reset().bold() + "rascal> " + Ansi.ansi().reset(); } return "rascal>"; } @Override - public String parseErrorPrompt(boolean ansiSupported) { + public String parseErrorPrompt(boolean ansiSupported, boolean unicodeSupported) { + String errorPrompt = (unicodeSupported ? "│" : "|") + "%N %P> "; if (ansiSupported) { - return Ansi.ansi().reset().bold() + ">>>>>>>" + Ansi.ansi().reset(); + return Ansi.ansi().reset().bold() + errorPrompt + Ansi.ansi().reset(); } - return ">>>>>>>"; + return errorPrompt; } From 38989b9adb08e0cd4d76e07d06765e985532b327 Mon Sep 17 00:00:00 2001 From: Davy Landman Date: Thu, 24 Oct 2024 17:20:37 +0200 Subject: [PATCH 12/35] A bit more tweaking of the prompts --- .vscode/launch.json | 3 ++- src/org/rascalmpl/repl/RascalReplServices.java | 4 ++-- src/org/rascalmpl/shell/RascalShell2.java | 11 ++++++----- 3 files changed, 10 insertions(+), 8 deletions(-) diff --git a/.vscode/launch.json b/.vscode/launch.json index 19e123daa5f..6f7385915ee 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -9,7 +9,8 @@ "name": "RascalShell2", "request": "launch", "mainClass": "org.rascalmpl.shell.RascalShell2", - "projectName": "rascal" + "projectName": "rascal", + "console": "integratedTerminal" }, { "type": "java", diff --git a/src/org/rascalmpl/repl/RascalReplServices.java b/src/org/rascalmpl/repl/RascalReplServices.java index 30afb508a15..3cd72ced38a 100644 --- a/src/org/rascalmpl/repl/RascalReplServices.java +++ b/src/org/rascalmpl/repl/RascalReplServices.java @@ -266,14 +266,14 @@ public void handleReset(Map output, Map @Override public String prompt(boolean ansiSupported, boolean unicodeSupported) { if (ansiSupported) { - return Ansi.ansi().reset().bold() + "rascal> " + Ansi.ansi().reset(); + return Ansi.ansi().reset().bold() + "rascal>" + Ansi.ansi().reset(); } return "rascal>"; } @Override public String parseErrorPrompt(boolean ansiSupported, boolean unicodeSupported) { - String errorPrompt = (unicodeSupported ? "│" : "|") + "%N %P> "; + String errorPrompt = (unicodeSupported ? "│" : "|") + "%N %P>"; if (ansiSupported) { return Ansi.ansi().reset().bold() + errorPrompt + Ansi.ansi().reset(); } diff --git a/src/org/rascalmpl/shell/RascalShell2.java b/src/org/rascalmpl/shell/RascalShell2.java index fcf9bf0ddfa..eaf36fff701 100644 --- a/src/org/rascalmpl/shell/RascalShell2.java +++ b/src/org/rascalmpl/shell/RascalShell2.java @@ -18,6 +18,7 @@ import java.nio.charset.StandardCharsets; import org.jline.terminal.TerminalBuilder; +import org.jline.utils.OSUtils; import org.rascalmpl.ideservices.BasicIDEServices; import org.rascalmpl.ideservices.IDEServices; import org.rascalmpl.interpreter.Evaluator; @@ -44,11 +45,11 @@ public static void main(String[] args) throws IOException { printVersionNumber(); //try { - var term = TerminalBuilder.builder() - .color(true) - .encoding(StandardCharsets.UTF_8) - .build(); - + var termBuilder = TerminalBuilder.builder(); + if (OSUtils.IS_WINDOWS) { + termBuilder.encoding(StandardCharsets.UTF_8); + } + var term = termBuilder.build(); var repl = new BaseREPL2(new RascalReplServices((t) -> { var monitor = new TerminalProgressBarMonitor(term); From 3145e2f047c0cc6be17fc9a670a8e5eaece510b3 Mon Sep 17 00:00:00 2001 From: Davy Landman Date: Wed, 20 Nov 2024 18:25:55 +0100 Subject: [PATCH 13/35] Added initial support for command completion --- src/org/rascalmpl/repl/IOutputPrinter.java | 16 +++- .../repl/RascalCommandCompletion.java | 80 ++++++++++++++----- src/org/rascalmpl/repl/RascalLineParser.java | 4 +- .../rascalmpl/repl/RascalReplServices.java | 42 ++++++---- src/org/rascalmpl/shell/RascalShell2.java | 1 + 5 files changed, 108 insertions(+), 35 deletions(-) diff --git a/src/org/rascalmpl/repl/IOutputPrinter.java b/src/org/rascalmpl/repl/IOutputPrinter.java index b2158b3b5c1..6b055f852fb 100644 --- a/src/org/rascalmpl/repl/IOutputPrinter.java +++ b/src/org/rascalmpl/repl/IOutputPrinter.java @@ -1,9 +1,23 @@ package org.rascalmpl.repl; +import java.io.IOException; import java.io.PrintWriter; import java.io.Reader; +import java.io.StringReader; +import java.io.StringWriter; public interface IOutputPrinter { void write(PrintWriter target); - Reader asReader(); + + default Reader asReader() { + try (var result = new StringWriter()) { + try (var resultWriter = new PrintWriter(result)) { + write(resultWriter); + } + return new StringReader(result.toString()); + } + catch (IOException ex) { + throw new IllegalStateException("StringWriter close should never throw exception", ex); + } + } } diff --git a/src/org/rascalmpl/repl/RascalCommandCompletion.java b/src/org/rascalmpl/repl/RascalCommandCompletion.java index 2ed269379d5..7db1c909d06 100755 --- a/src/org/rascalmpl/repl/RascalCommandCompletion.java +++ b/src/org/rascalmpl/repl/RascalCommandCompletion.java @@ -1,32 +1,39 @@ package org.rascalmpl.repl; + import java.util.ArrayList; import java.util.Collection; import java.util.List; +import java.util.NavigableMap; import java.util.SortedSet; -import java.util.TreeSet; +import java.util.TreeMap; +import java.util.function.BiConsumer; import java.util.regex.Matcher; import java.util.regex.Pattern; import java.util.stream.Collectors; +import org.jline.reader.Candidate; +import org.jline.reader.Completer; +import org.jline.reader.LineReader; +import org.jline.reader.ParsedLine; import org.rascalmpl.interpreter.utils.StringUtils; import org.rascalmpl.interpreter.utils.StringUtils.OffsetLengthTerm; public class RascalCommandCompletion { - private static final TreeSet COMMAND_KEYWORDS; + private static final NavigableMap COMMAND_KEYWORDS; static { - COMMAND_KEYWORDS = new TreeSet<>(); - COMMAND_KEYWORDS.add("set"); - COMMAND_KEYWORDS.add("undeclare"); - COMMAND_KEYWORDS.add("help"); - COMMAND_KEYWORDS.add("edit"); - COMMAND_KEYWORDS.add("unimport"); - COMMAND_KEYWORDS.add("declarations"); - COMMAND_KEYWORDS.add("quit"); - COMMAND_KEYWORDS.add("history"); - COMMAND_KEYWORDS.add("test"); - COMMAND_KEYWORDS.add("modules"); - COMMAND_KEYWORDS.add("clear"); + COMMAND_KEYWORDS = new TreeMap<>(); + COMMAND_KEYWORDS.put(":set", "change a evaluator setting"); + COMMAND_KEYWORDS.put(":undeclare", "undeclare a local variable of the REPL"); + COMMAND_KEYWORDS.put(":help", "print help message"); + COMMAND_KEYWORDS.put(":edit", "open a rascal module in the editor"); + COMMAND_KEYWORDS.put(":unimport", "unload an imported module from the REPL"); + COMMAND_KEYWORDS.put(":declarations", "show declarations"); // TODO figure out what it does + COMMAND_KEYWORDS.put(":quit", "cleanly exit the REPL"); + COMMAND_KEYWORDS.put(":history", "history"); // TODO: figure out what it does + COMMAND_KEYWORDS.put(":test", "run rest modules"); + COMMAND_KEYWORDS.put(":modules", "show imported modules");// TODO: figure out what it does + COMMAND_KEYWORDS.put(":clear", "clear evaluator");// TODO: figure out what it does } @@ -57,16 +64,16 @@ else if (line.trim().equals(":set")) { case "edit": case "unimport": return completeModule.complete(line, line.length()); default: { - if (COMMAND_KEYWORDS.contains(currentCommand)) { + if (COMMAND_KEYWORDS.containsKey(":" + currentCommand)) { return null; // nothing to complete after a full command } List result = null; if (currentCommand.isEmpty()) { - result = new ArrayList<>(COMMAND_KEYWORDS); + result = new ArrayList<>(COMMAND_KEYWORDS.keySet()); } else { - result = COMMAND_KEYWORDS.stream() - .filter(s -> s.startsWith(currentCommand)) + result = COMMAND_KEYWORDS.keySet().stream() + .filter(s -> s.startsWith(":" + currentCommand)) .collect(Collectors.toList()); } if (!result.isEmpty()) { @@ -77,5 +84,42 @@ else if (line.trim().equals(":set")) { } return null; } + public static Completer buildCompleter(NavigableMap setOptions, BiConsumer> completeIdentifier, BiConsumer> completeModule) { + return (LineReader reader, ParsedLine line, List candidates) -> { + var words = line.words(); + if (words.isEmpty() || !words.get(0).startsWith(":")) { + return; + } + if (line.wordIndex() == 0) { + // complete initial command/modifier + generateCandidates(line.word(), COMMAND_KEYWORDS, "interpreter modifiers", candidates); + return; + } + if (line.wordIndex() == 1 || words.size() == 1) { + // complete arguments for first + var arg = words.size() == 1 ? "" : line.word(); + switch (words.get(0)) { + case ":set": + generateCandidates(arg, setOptions, "evaluator settings", candidates); + return; + case "undeclare": + completeIdentifier.accept(arg, candidates); + return; + case "edit": // intended fall-through + case "unimport": + completeModule.accept(arg, candidates); + return; + default: return; + } + } + // for the future it would be nice to also support completing thinks like `:set profiling ` + }; + } + + private static void generateCandidates(String partial, NavigableMap candidates, String group, List target) { + for (var can : candidates.subMap(partial, true, partial + Character.MAX_VALUE, false).entrySet()) { + target.add(new Candidate(can.getKey(), can.getKey(), group, can.getValue(), null, null, true)); + } + } } diff --git a/src/org/rascalmpl/repl/RascalLineParser.java b/src/org/rascalmpl/repl/RascalLineParser.java index 54a06549ff2..c460adca81c 100644 --- a/src/org/rascalmpl/repl/RascalLineParser.java +++ b/src/org/rascalmpl/repl/RascalLineParser.java @@ -71,7 +71,7 @@ public String word() { @Override public int wordCursor() { - return atCursor == null ? -1 : (cursor - atCursor.begin); + return atCursor == null ? 0 : (cursor - atCursor.begin); } @Override @@ -167,7 +167,7 @@ private static int parseEndedAfter(String buffer, int position, Pattern parser) if (!matcher.find()) { return position; } - return position + matcher.end(); + return matcher.end(); } // strings with rudementary interpolation support diff --git a/src/org/rascalmpl/repl/RascalReplServices.java b/src/org/rascalmpl/repl/RascalReplServices.java index 3cd72ced38a..b7c7018be5b 100644 --- a/src/org/rascalmpl/repl/RascalReplServices.java +++ b/src/org/rascalmpl/repl/RascalReplServices.java @@ -9,17 +9,24 @@ import java.io.PrintWriter; import java.io.Reader; import java.io.StringReader; -import java.io.StringWriter; import java.io.Writer; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; import java.util.Map; +import java.util.NavigableMap; import java.util.Objects; +import java.util.SortedMap; +import java.util.TreeMap; import java.util.function.Function; import org.jline.jansi.Ansi; +import org.jline.reader.Completer; import org.jline.reader.Parser; import org.jline.terminal.Terminal; import org.jline.utils.InfoCmp.Capability; import org.rascalmpl.exceptions.Throw; +import org.rascalmpl.interpreter.Configuration; import org.rascalmpl.interpreter.Evaluator; import org.rascalmpl.interpreter.control_exceptions.InterruptException; import org.rascalmpl.interpreter.control_exceptions.QuitException; @@ -211,19 +218,6 @@ public ExceptionPrinter(ThrowingWriter internalWriter, StandardTextWriter pretty this.internalWriter = internalWriter; this.prettyPrinter = prettyPrinter; } - - @Override - public Reader asReader() { - try (var result = new StringWriter()) { - try (var resultWriter = new PrintWriter(result)) { - write(resultWriter); - } - return new StringReader(result.toString()); - } - catch (IOException ex) { - throw new IllegalStateException("StringWriter close should never throw exception", ex); - } - } @Override public void write(PrintWriter target) { @@ -297,5 +291,25 @@ public void flush() { eval.getStdErr().flush(); eval.getStdOut().flush(); } + + private static final NavigableMap commandLineOptions = new TreeMap<>(); + static { + commandLineOptions.put(Configuration.GENERATOR_PROFILING_PROPERTY.substring("rascal.".length()), "enable sampling profiler for generator"); + commandLineOptions.put(Configuration.PROFILING_PROPERTY.substring("rascal.".length()), "enable sampling profiler" ); + commandLineOptions.put(Configuration.ERRORS_PROPERTY.substring("rascal.".length()), "print raw java errors"); + commandLineOptions.put(Configuration.TRACING_PROPERTY.substring("rascal.".length()), "trace all function calls (warning: a lot of output will be generated)"); + } + + @Override + public boolean supportsCompletion() { + return true; + } + + @Override + public List completers() { + var result = new ArrayList(); + result.add(RascalCommandCompletion.buildCompleter(commandLineOptions,(s,c) -> {}, (s,c) -> {})); + return result; + } } diff --git a/src/org/rascalmpl/shell/RascalShell2.java b/src/org/rascalmpl/shell/RascalShell2.java index eaf36fff701..716cf347e5d 100644 --- a/src/org/rascalmpl/shell/RascalShell2.java +++ b/src/org/rascalmpl/shell/RascalShell2.java @@ -51,6 +51,7 @@ public static void main(String[] args) throws IOException { } var term = termBuilder.build(); + var repl = new BaseREPL2(new RascalReplServices((t) -> { var monitor = new TerminalProgressBarMonitor(term); IDEServices services = new BasicIDEServices(term.writer(), monitor); From 79bdf53012caa2617b0f7ade5ef22e9ac7d3fbfb Mon Sep 17 00:00:00 2001 From: Davy Landman Date: Thu, 21 Nov 2024 12:34:36 +0100 Subject: [PATCH 14/35] Added module completion support --- src/org/rascalmpl/repl/BaseRascalREPL.java | 1 + .../rascalmpl/repl/RascalReplServices.java | 19 +- .../RascalCommandCompletion.java | 104 ++++---- .../completers/RascalModuleCompletion.java | 47 ++++ .../repl/{ => jline3}/RascalLineParser.java | 238 +++++++++--------- .../rascalmpl/test/repl/JlineParserTest.java | 57 +++++ 6 files changed, 300 insertions(+), 166 deletions(-) rename src/org/rascalmpl/repl/{ => completers}/RascalCommandCompletion.java (54%) mode change 100755 => 100644 create mode 100644 src/org/rascalmpl/repl/completers/RascalModuleCompletion.java rename src/org/rascalmpl/repl/{ => jline3}/RascalLineParser.java (57%) create mode 100644 test/org/rascalmpl/test/repl/JlineParserTest.java diff --git a/src/org/rascalmpl/repl/BaseRascalREPL.java b/src/org/rascalmpl/repl/BaseRascalREPL.java index 24a3177a7e3..14caabb1e5d 100755 --- a/src/org/rascalmpl/repl/BaseRascalREPL.java +++ b/src/org/rascalmpl/repl/BaseRascalREPL.java @@ -26,6 +26,7 @@ import org.rascalmpl.interpreter.utils.ReadEvalPrintDialogMessages; import org.rascalmpl.interpreter.utils.StringUtils; import org.rascalmpl.interpreter.utils.StringUtils.OffsetLengthTerm; +import org.rascalmpl.repl.completers.RascalCommandCompletion; import org.rascalmpl.uri.URIResolverRegistry; import org.rascalmpl.uri.URIUtil; import org.rascalmpl.values.RascalValueFactory; diff --git a/src/org/rascalmpl/repl/RascalReplServices.java b/src/org/rascalmpl/repl/RascalReplServices.java index b7c7018be5b..1d9f778db87 100644 --- a/src/org/rascalmpl/repl/RascalReplServices.java +++ b/src/org/rascalmpl/repl/RascalReplServices.java @@ -11,12 +11,10 @@ import java.io.StringReader; import java.io.Writer; import java.util.ArrayList; -import java.util.Collections; import java.util.List; import java.util.Map; import java.util.NavigableMap; import java.util.Objects; -import java.util.SortedMap; import java.util.TreeMap; import java.util.function.Function; @@ -28,17 +26,22 @@ import org.rascalmpl.exceptions.Throw; import org.rascalmpl.interpreter.Configuration; import org.rascalmpl.interpreter.Evaluator; +import org.rascalmpl.interpreter.NullRascalMonitor; import org.rascalmpl.interpreter.control_exceptions.InterruptException; import org.rascalmpl.interpreter.control_exceptions.QuitException; import org.rascalmpl.interpreter.result.IRascalResult; import org.rascalmpl.interpreter.result.Result; import org.rascalmpl.interpreter.staticErrors.StaticError; import org.rascalmpl.parser.gtd.exception.ParseError; +import org.rascalmpl.repl.completers.RascalCommandCompletion; +import org.rascalmpl.repl.completers.RascalModuleCompletion; +import org.rascalmpl.repl.jline3.RascalLineParser; import org.rascalmpl.uri.URIUtil; import org.rascalmpl.values.RascalValueFactory; import org.rascalmpl.values.parsetrees.TreeAdapter; import io.usethesource.vallang.IConstructor; +import io.usethesource.vallang.ISourceLocation; import io.usethesource.vallang.IString; import io.usethesource.vallang.IValue; import io.usethesource.vallang.io.StandardTextWriter; @@ -71,9 +74,15 @@ public void connect(Terminal term) { this.eval = buildEvaluator.apply(term); } + private static final ISourceLocation PROMPT_LOCATION = URIUtil.rootLocation("prompt"); + @Override public Parser inputParser() { - return new RascalLineParser(() -> eval); + return new RascalLineParser(prompt -> { + synchronized(eval) { + return eval.parseCommand(new NullRascalMonitor(), prompt, PROMPT_LOCATION); + } + }); } @Override @@ -308,7 +317,9 @@ public boolean supportsCompletion() { @Override public List completers() { var result = new ArrayList(); - result.add(RascalCommandCompletion.buildCompleter(commandLineOptions,(s,c) -> {}, (s,c) -> {})); + var moduleCompleter = new RascalModuleCompletion(m -> eval.getRascalResolver().listModuleEntries(m)); + result.add(new RascalCommandCompletion(commandLineOptions,(s,c) -> {}, (s, c) -> moduleCompleter.completeModuleNames(s, c, false))); + result.add(moduleCompleter); return result; } diff --git a/src/org/rascalmpl/repl/RascalCommandCompletion.java b/src/org/rascalmpl/repl/completers/RascalCommandCompletion.java old mode 100755 new mode 100644 similarity index 54% rename from src/org/rascalmpl/repl/RascalCommandCompletion.java rename to src/org/rascalmpl/repl/completers/RascalCommandCompletion.java index 7db1c909d06..ac4e77ba27a --- a/src/org/rascalmpl/repl/RascalCommandCompletion.java +++ b/src/org/rascalmpl/repl/completers/RascalCommandCompletion.java @@ -1,4 +1,4 @@ -package org.rascalmpl.repl; +package org.rascalmpl.repl.completers; import java.util.ArrayList; @@ -18,26 +18,39 @@ import org.jline.reader.ParsedLine; import org.rascalmpl.interpreter.utils.StringUtils; import org.rascalmpl.interpreter.utils.StringUtils.OffsetLengthTerm; +import org.rascalmpl.repl.CompletionFunction; +import org.rascalmpl.repl.CompletionResult; -public class RascalCommandCompletion { +public class RascalCommandCompletion implements Completer { private static final NavigableMap COMMAND_KEYWORDS; static { COMMAND_KEYWORDS = new TreeMap<>(); - COMMAND_KEYWORDS.put(":set", "change a evaluator setting"); - COMMAND_KEYWORDS.put(":undeclare", "undeclare a local variable of the REPL"); - COMMAND_KEYWORDS.put(":help", "print help message"); - COMMAND_KEYWORDS.put(":edit", "open a rascal module in the editor"); - COMMAND_KEYWORDS.put(":unimport", "unload an imported module from the REPL"); - COMMAND_KEYWORDS.put(":declarations", "show declarations"); // TODO figure out what it does - COMMAND_KEYWORDS.put(":quit", "cleanly exit the REPL"); - COMMAND_KEYWORDS.put(":history", "history"); // TODO: figure out what it does - COMMAND_KEYWORDS.put(":test", "run rest modules"); - COMMAND_KEYWORDS.put(":modules", "show imported modules");// TODO: figure out what it does - COMMAND_KEYWORDS.put(":clear", "clear evaluator");// TODO: figure out what it does + COMMAND_KEYWORDS.put("set", "change a evaluator setting"); + COMMAND_KEYWORDS.put("undeclare", "undeclare a local variable of the REPL"); + COMMAND_KEYWORDS.put("help", "print help message"); + COMMAND_KEYWORDS.put("edit", "open a rascal module in the editor"); + COMMAND_KEYWORDS.put("unimport", "unload an imported module from the REPL"); + COMMAND_KEYWORDS.put("declarations", "show declarations"); // TODO figure out what it does + COMMAND_KEYWORDS.put("quit", "cleanly exit the REPL"); + COMMAND_KEYWORDS.put("history", "history"); // TODO: figure out what it does + COMMAND_KEYWORDS.put("test", "run rest modules"); + COMMAND_KEYWORDS.put("modules", "show imported modules");// TODO: figure out what it does + COMMAND_KEYWORDS.put("clear", "clear evaluator");// TODO: figure out what it does + } + + private final NavigableMap setOptions; + private final BiConsumer> completeIdentifier; + private final BiConsumer> completeModule; + public RascalCommandCompletion(NavigableMap setOptions, BiConsumer> completeIdentifier, BiConsumer> completeModule) { + this.setOptions = setOptions; + this.completeIdentifier = completeIdentifier; + this.completeModule = completeModule; } + private static final Pattern splitCommand = Pattern.compile("^[\\t ]*:(?[a-z]*)([\\t ]|$)"); + /**@deprecated remove this function */ public static CompletionResult complete(String line, int cursor, SortedSet commandOptions, CompletionFunction completeIdentifier, CompletionFunction completeModule) { assert line.trim().startsWith(":"); Matcher m = splitCommand.matcher(line); @@ -64,7 +77,7 @@ else if (line.trim().equals(":set")) { case "edit": case "unimport": return completeModule.complete(line, line.length()); default: { - if (COMMAND_KEYWORDS.containsKey(":" + currentCommand)) { + if (COMMAND_KEYWORDS.containsKey(currentCommand)) { return null; // nothing to complete after a full command } List result = null; @@ -73,7 +86,7 @@ else if (line.trim().equals(":set")) { } else { result = COMMAND_KEYWORDS.keySet().stream() - .filter(s -> s.startsWith(":" + currentCommand)) + .filter(s -> s.startsWith(currentCommand)) .collect(Collectors.toList()); } if (!result.isEmpty()) { @@ -84,37 +97,6 @@ else if (line.trim().equals(":set")) { } return null; } - public static Completer buildCompleter(NavigableMap setOptions, BiConsumer> completeIdentifier, BiConsumer> completeModule) { - return (LineReader reader, ParsedLine line, List candidates) -> { - var words = line.words(); - if (words.isEmpty() || !words.get(0).startsWith(":")) { - return; - } - if (line.wordIndex() == 0) { - // complete initial command/modifier - generateCandidates(line.word(), COMMAND_KEYWORDS, "interpreter modifiers", candidates); - return; - } - if (line.wordIndex() == 1 || words.size() == 1) { - // complete arguments for first - var arg = words.size() == 1 ? "" : line.word(); - switch (words.get(0)) { - case ":set": - generateCandidates(arg, setOptions, "evaluator settings", candidates); - return; - case "undeclare": - completeIdentifier.accept(arg, candidates); - return; - case "edit": // intended fall-through - case "unimport": - completeModule.accept(arg, candidates); - return; - default: return; - } - } - // for the future it would be nice to also support completing thinks like `:set profiling ` - }; - } private static void generateCandidates(String partial, NavigableMap candidates, String group, List target) { for (var can : candidates.subMap(partial, true, partial + Character.MAX_VALUE, false).entrySet()) { @@ -122,4 +104,34 @@ private static void generateCandidates(String partial, NavigableMap candidates) { + var words = line.words(); + if (words.isEmpty() || !words.get(0).equals(":")) { + return; + } + if (line.wordIndex() == 1) { + // complete initial command/modifier + generateCandidates(line.word(), COMMAND_KEYWORDS, "interpreter modifiers", candidates); + return; + } + if (line.wordIndex() == 2) { + // complete arguments for first + switch (words.get(1)) { + case "set": + generateCandidates(line.word(), setOptions, "evaluator settings", candidates); + return; + case "undeclare": + completeIdentifier.accept(line.word(), candidates); + return; + case "edit": // intended fall-through + case "unimport": + completeModule.accept(line.word(), candidates); + return; + default: return; + } + } + // for the future it would be nice to also support completing thinks like `:set profiling ` + } + } diff --git a/src/org/rascalmpl/repl/completers/RascalModuleCompletion.java b/src/org/rascalmpl/repl/completers/RascalModuleCompletion.java new file mode 100644 index 00000000000..301a496edfd --- /dev/null +++ b/src/org/rascalmpl/repl/completers/RascalModuleCompletion.java @@ -0,0 +1,47 @@ +package org.rascalmpl.repl.completers; + +import java.util.List; +import java.util.function.Function; + +import org.jline.reader.Candidate; +import org.jline.reader.Completer; +import org.jline.reader.LineReader; +import org.jline.reader.ParsedLine; + +public class RascalModuleCompletion implements Completer { + + private final Function> searchPathLookup; + + public RascalModuleCompletion(Function> searchPathLookup) { + this.searchPathLookup = searchPathLookup; + } + + @Override + public void complete(LineReader reader, ParsedLine line, List candidates) { + if (line.wordIndex() != 1) { + // we can only complete import/extend statements that reference a module + return; + } + switch (line.words().get(0)) { + case "import": // intended fallthrough + case "extend": + completeModuleNames(line.word(), candidates, true); + return; + default: + return; + } + } + + public void completeModuleNames(String word, List candidates, boolean importStatement) { + // as jline will take care to filter prefixes, we only have to report modules in the directory (or siblings of the name) + // we do not have to filter out prefixes + int rootedIndex = word.lastIndexOf("::"); + String moduleRoot = rootedIndex == -1? "": word.substring(0, rootedIndex); + String modulePrefix = moduleRoot.isEmpty() ? "" : moduleRoot + "::"; + for (var mod : searchPathLookup.apply(moduleRoot)) { + var fullPath = modulePrefix + mod; + var isFullModulePath = !mod.endsWith("::"); + candidates.add(new Candidate(fullPath + (isFullModulePath & importStatement? ";" : ""), fullPath, "modules", null, null, null, isFullModulePath)); + } + } +} diff --git a/src/org/rascalmpl/repl/RascalLineParser.java b/src/org/rascalmpl/repl/jline3/RascalLineParser.java similarity index 57% rename from src/org/rascalmpl/repl/RascalLineParser.java rename to src/org/rascalmpl/repl/jline3/RascalLineParser.java index c460adca81c..fb92dfca32e 100644 --- a/src/org/rascalmpl/repl/RascalLineParser.java +++ b/src/org/rascalmpl/repl/jline3/RascalLineParser.java @@ -1,33 +1,28 @@ -package org.rascalmpl.repl; +package org.rascalmpl.repl.jline3; import java.util.ArrayList; import java.util.List; -import java.util.function.Supplier; +import java.util.function.Function; import java.util.regex.Pattern; import java.util.stream.Collectors; import org.checkerframework.checker.nullness.qual.MonotonicNonNull; -import org.checkerframework.checker.nullness.qual.Nullable; import org.jline.reader.EOFError; import org.jline.reader.ParsedLine; import org.jline.reader.Parser; import org.jline.reader.SyntaxError; -import org.rascalmpl.interpreter.IEvaluator; -import org.rascalmpl.interpreter.NullRascalMonitor; import org.rascalmpl.parser.gtd.exception.ParseError; -import org.rascalmpl.uri.URIUtil; import org.rascalmpl.values.parsetrees.ITree; import org.rascalmpl.values.parsetrees.TreeAdapter; -import io.usethesource.vallang.ISourceLocation; + public class RascalLineParser implements Parser { - private static final ISourceLocation PROMPT_LOCATION = URIUtil.rootLocation("prompt"); - private final Supplier> eval; + private final Function commandParser; - public RascalLineParser(Supplier> eval) { - this.eval = eval; + public RascalLineParser(Function commandParser) { + this.commandParser = commandParser; } @Override @@ -38,6 +33,11 @@ public ParsedLine parse(String line, int cursor, ParseContext context) throws Sy // we have to verify the input is correct rascal statement return parseFullRascalCommand(line, cursor); case COMPLETE: + // for completion purposes, we want a specific kind of grouping + // so we'll use a heuristic for this. + // in the future we might be able to use the parser with error recovery + // but we would still have to think about grouping things together that aren't in the + // parse tree, such as `:` and the `set` try { // lets see, maybe it parses as a rascal expression return parseFullRascalCommand(line, cursor); @@ -59,44 +59,7 @@ private ParsedLine splitWordsOnly(String line, int cursor) { // small line parser, in the future we might be able to use error recovery var words = new ArrayList(); parseWords(line, 0, words); - return new ParsedLine() { - private final @MonotonicNonNull LexedWord atCursor = words.stream() - .filter(l -> l.cursorInside(cursor)) - .findFirst() - .orElse(null); - @Override - public String word() { - return atCursor == null ? "" : atCursor.word(); - } - - @Override - public int wordCursor() { - return atCursor == null ? 0 : (cursor - atCursor.begin); - } - - @Override - public int wordIndex() { - return atCursor == null ? -1 : words.indexOf(atCursor); - } - - @Override - public List words() { - return words.stream() - .map(LexedWord::word) - .collect(Collectors.toList()); - } - - @Override - public String line() { - return line; - } - - @Override - public int cursor() { - return cursor; - } - - }; + return new ParsedLineLexedWords(words, cursor, line); } private void parseWords(String buffer, int position, List words) { @@ -120,8 +83,12 @@ else if (c == '|' || (c == '>' && inLocation)) { wordEnd = parseEndedAfter(buffer, position, RASCAL_LOCATION); inLocation = wordEnd != position && buffer.charAt(position - 1) == '<'; } - else if (Character.isJavaIdentifierPart(c) || c == ':' || c == '\\') { - wordEnd = parseEndedAfter(buffer, position, RASCAL_NAME_OR_COMMAND); + else if (Character.isJavaIdentifierPart(c) || c == '\\') { + wordEnd = parseEndedAfter(buffer, position, RASCAL_NAME); + } + else if (c == ':' && words.isEmpty()) { + // can be a command start + wordEnd++; } else { wordEnd++; @@ -139,6 +106,62 @@ else if (Character.isJavaIdentifierPart(c) || c == ':' || c == '\\') { } } + private final class ParsedLineLexedWords implements ParsedLine { + private final ArrayList words; + private final int cursor; + private final String line; + private final @MonotonicNonNull LexedWord atCursor; + + private ParsedLineLexedWords(ArrayList words, int cursor, String line) { + this.words = words; + this.cursor = cursor; + this.line = line; + if (cursor >= (line.length() - 1)) { + if (words.isEmpty() || !words.get(words.size() - 1).cursorInside(cursor)) { + words.add(new LexedWord(line + " ", cursor, cursor)); + } + } + + atCursor = words.stream() + .filter(l -> l.cursorInside(cursor)) + .findFirst() + .orElse(null); + } + + @Override + public String word() { + return atCursor == null ? "" : atCursor.word(); + } + + @Override + public int wordCursor() { + return atCursor == null ? 0 : (cursor - atCursor.begin); + } + + @Override + public int wordIndex() { + return atCursor == null ? -1 : words.indexOf(atCursor); + } + + @Override + public List words() { + return words.stream() + .map(LexedWord::word) + .collect(Collectors.toList()); + } + + @Override + public String line() { + return line; + } + + @Override + public int cursor() { + return cursor; + } + } + + private static class LexedWord { private final String buffer; @@ -177,8 +200,8 @@ private static int parseEndedAfter(String buffer, int position, Pattern parser) private static final Pattern RASCAL_LOCATION = Pattern.compile("^[\\|\\>][^\\|\\<\\t-\\n\\r ]*[\\|\\<]?"); - private static final Pattern RASCAL_NAME_OR_COMMAND - = Pattern.compile("^(([:]?[A-Za-z_][A-Za-z0-9_]*)|([\\\\][A-Za-z_][\\-A-Za-z0-9_]*))"); + private static final Pattern RASCAL_NAME + = Pattern.compile("^(([A-Za-z_]([A-Za-z0-9_]|::)*)|([\\\\][A-Za-z_]([\\-A-Za-z0-9_]|::)*))"); // only unicode spaces & multi-line comments private static final Pattern RASCAL_WHITE_SPACE @@ -191,7 +214,7 @@ private int eatWhiteSpace(String buffer, int position) { private ParsedLine parseFullRascalCommand(String line, int cursor) throws SyntaxError { // TODO: to support inline highlighting, we have to remove the ansi escapes before parsing try { - return translateTree(eval.get().parseCommand(new NullRascalMonitor(), line, PROMPT_LOCATION), line, cursor); + return translateTree(commandParser.apply(line), line, cursor); } catch (ParseError pe) { throw new EOFError(pe.getBeginLine(), pe.getBeginColumn(), "Parse error"); @@ -201,75 +224,58 @@ private ParsedLine parseFullRascalCommand(String line, int cursor) throws Synta } } + private ParsedLine translateTree(ITree command, String line, int cursor) { // todo: return CompletingParsedLine so that we can also help with quoting completion - return new ParsedLine() { - List wordList = null; - ITree wordTree = null; - - private ITree cursorTree() { - if (wordTree == null) { - wordTree = (ITree)TreeAdapter.locateLexical(command, cursor); - if (wordTree == null) { - wordTree = command; - } - } - return wordTree; - } + var result = new ArrayList(); - private List wordList() { - if (wordList == null) { - wordList = new ArrayList<>(); - collectWords(command, wordList); - } - return wordList; - } + collectWords(command, result, line, 0); + return new ParsedLineLexedWords(result, cursor, line); + } - private void collectWords(ITree t, List words) { - if (TreeAdapter.isLexical(t)) { - words.add(t); - } - else if (TreeAdapter.isSort(t)) { - for (var c : t.getArgs()) { - if (c instanceof ITree) { - collectWords((ITree)c, words); - } - } + private int collectWords(ITree t, List words, String line, int offset) { + boolean isWord; + if (TreeAdapter.isLayout(t)) { + isWord = false; + } + else if (TreeAdapter.isLexical(t) || TreeAdapter.isLiteral(t) || TreeAdapter.isCILiteral(t)) { + isWord = true; + } + else if (TreeAdapter.isSort(t) && TreeAdapter.getSortName(t).equals("QualifiedName")) { + isWord = true; + } + else if (TreeAdapter.isSort(t)) { + var loc = TreeAdapter.getLocation(t); + isWord = false; + for (var c : t.getArgs()) { + if (c instanceof ITree) { + offset = collectWords((ITree)c, words, line, offset); } } + return loc == null ? offset : (loc.getOffset() + loc.getLength()); + } + else if (TreeAdapter.isTop(t)) { + isWord = false; + var args = t.getArgs(); + var preLoc = TreeAdapter.getLocation((ITree)args.get(0)); + offset += preLoc == null ? 0 : preLoc.getLength(); + offset = collectWords((ITree)args.get(1), words, line, offset); + var postLoc = TreeAdapter.getLocation((ITree)args.get(2)); + return offset + (postLoc == null ? 0 : postLoc.getLength()); + } + else { + isWord = false; + } - @Override - public String word() { - return TreeAdapter.yield(cursorTree()); - } - - @Override - public int wordCursor() { - return cursor - TreeAdapter.getLocation(cursorTree()).getOffset(); - } - - @Override - public int wordIndex() { - return wordList().indexOf(cursorTree()); - } - - @Override - public List words() { - return wordList() - .stream() - .map(TreeAdapter::yield) - .collect(Collectors.toList()); - } - - @Override - public String line() { - return line; - } + var loc = TreeAdapter.getLocation(t); + var length = loc == null ? TreeAdapter.yield(t).length() : loc.getLength(); + if (isWord) { + words.add(new LexedWord(line, offset, offset + length)); + return offset + length; + } + else { + return offset + length; + } - @Override - public int cursor() { - return cursor; - } - }; } } diff --git a/test/org/rascalmpl/test/repl/JlineParserTest.java b/test/org/rascalmpl/test/repl/JlineParserTest.java new file mode 100644 index 00000000000..033cb03a2c0 --- /dev/null +++ b/test/org/rascalmpl/test/repl/JlineParserTest.java @@ -0,0 +1,57 @@ +package org.rascalmpl.test.repl; + +import static org.junit.Assert.assertEquals; + +import org.jline.reader.ParsedLine; +import org.jline.reader.Parser.ParseContext; +import org.junit.Test; +import org.rascalmpl.parser.gtd.exception.ParseError; +import org.rascalmpl.repl.jline3.RascalLineParser; +import org.rascalmpl.uri.URIUtil; + +public class JlineParserTest { + + private ParsedLine completeParser(String input) { + return completeParser(input, input.length() - 1); + } + + private ParsedLine completeParser(String input, int cursor) { + var parser = new RascalLineParser(l -> { throw new ParseError("rascal parser not supported in the test yet", URIUtil.invalidURI(), 0, 0, 0, 0, 0, 0); }); + + return parser.parse(input, cursor, ParseContext.COMPLETE); + } + + @Test + public void commandsParsedCorrectly() { + assertEquals("se", completeParser(":se").word()); + assertEquals(":", completeParser(":set prof", 2).words().get(0)); + assertEquals("set", completeParser(":set prof", 2).word()); + assertEquals("prof", completeParser(":set prof", 6).word()); + assertEquals("", completeParser(":set ", 5).word()); + } + + @Test + public void stringsAreNotParsedAsWords() { + assertEquals(1, completeParser("\"long string with multiple spaces\"").words().size()); // word + assertEquals("", completeParser("\"long string with multiple spaces\"",11).word()); + assertEquals("", completeParser("\"long string with multiple spaces\"").word()); + assertEquals(2, completeParser("x = \"long string with multiple spaces\";").words().size()); + } + + @Test + public void stringInterpolation() { + assertEquals("exp", completeParser("\"string string ending\"",11).words().size()); + assertEquals(2, completeParser("\"string string ending\"").words().size()); // at the end always an extra one + } + + @Test + public void qualifiedNames() { + assertEquals("IO::print", completeParser("IO::print").word()); + assertEquals("lang::rascal", completeParser("import lang::rascal").word()); + } + + +} From 27bf01fade2ccec43a55a905c2196912bc683368 Mon Sep 17 00:00:00 2001 From: Davy Landman Date: Thu, 21 Nov 2024 15:32:40 +0100 Subject: [PATCH 15/35] Also added support for keywords and identifier completion --- src/org/rascalmpl/interpreter/Evaluator.java | 27 ++++----- .../interpreter/IEvaluatorContext.java | 4 +- src/org/rascalmpl/repl/BaseREPL2.java | 7 ++- src/org/rascalmpl/repl/IREPLService.java | 4 ++ .../rascalmpl/repl/RascalReplServices.java | 11 +++- .../RascalIdentifierCompletion.java | 52 +++++++++++++++++ .../completers/RascalKeywordCompletion.java | 56 +++++++++++++++++++ .../completers/RascalModuleCompletion.java | 2 +- 8 files changed, 143 insertions(+), 20 deletions(-) create mode 100644 src/org/rascalmpl/repl/completers/RascalIdentifierCompletion.java create mode 100644 src/org/rascalmpl/repl/completers/RascalKeywordCompletion.java diff --git a/src/org/rascalmpl/interpreter/Evaluator.java b/src/org/rascalmpl/interpreter/Evaluator.java index 2b397e1034e..2950bb1c607 100755 --- a/src/org/rascalmpl/interpreter/Evaluator.java +++ b/src/org/rascalmpl/interpreter/Evaluator.java @@ -22,15 +22,11 @@ import static org.rascalmpl.semantics.dynamic.Import.parseFragments; import java.io.IOException; -import java.io.OutputStream; -import java.io.OutputStreamWriter; import java.io.PrintWriter; import java.io.Reader; import java.io.StringReader; import java.net.URI; -import java.nio.charset.StandardCharsets; import java.util.ArrayList; -import java.util.Collection; import java.util.Collections; import java.util.Comparator; import java.util.HashMap; @@ -40,9 +36,10 @@ import java.util.Map; import java.util.Map.Entry; import java.util.Set; +import java.util.SortedMap; import java.util.SortedSet; import java.util.Stack; -import java.util.TreeSet; +import java.util.TreeMap; import java.util.concurrent.CopyOnWriteArrayList; import org.rascalmpl.ast.AbstractAST; @@ -1681,13 +1678,13 @@ public TraversalEvaluator __popTraversalEvaluator() { } @Override - public Collection completePartialIdentifier(String qualifier, String partialIdentifier) { + public Map completePartialIdentifier(String qualifier, String partialIdentifier) { if (partialIdentifier.startsWith("\\")) { partialIdentifier = partialIdentifier.substring(1); } String partialModuleName = qualifier + "::" + partialIdentifier; - SortedSet result = new TreeSet<>(new Comparator() { + SortedMap result = new TreeMap<>(new Comparator() { @Override public int compare(String a, String b) { if (a.charAt(0) == '\\') { @@ -1709,7 +1706,7 @@ public int compare(String a, String b) { return result; } - private void addCompletionsForModule(String qualifier, String partialIdentifier, String partialModuleName, SortedSet result, ModuleEnvironment env, boolean skipPrivate) { + private void addCompletionsForModule(String qualifier, String partialIdentifier, String partialModuleName, SortedMap result, ModuleEnvironment env, boolean skipPrivate) { for (Pair> p : env.getFunctions()) { for (AbstractFunction f : p.getSecond()) { String module = ((ModuleEnvironment)f.getEnv()).getName(); @@ -1719,7 +1716,7 @@ private void addCompletionsForModule(String qualifier, String partialIdentifier, } if (module.startsWith(qualifier)) { - addIt(result, p.getFirst(), qualifier.isEmpty() ? "" : module, module.startsWith(partialModuleName) ? "" : partialIdentifier); + addIt(result, "function", p.getFirst(), qualifier.isEmpty() ? "" : module, module.startsWith(partialModuleName) ? "" : partialIdentifier); } } } @@ -1729,30 +1726,30 @@ private void addCompletionsForModule(String qualifier, String partialIdentifier, if (skipPrivate && env.isNamePrivate(entry.getKey())) { continue; } - addIt(result, entry.getKey(), qualifier, partialIdentifier); + addIt(result, "variable", entry.getKey(), qualifier, partialIdentifier); } for (Type t: env.getAbstractDatatypes()) { if (inQualifiedModule) { - addIt(result, t.getName(), qualifier, partialIdentifier); + addIt(result, "ADT", t.getName(), qualifier, partialIdentifier); } } for (Type t: env.getAliases()) { - addIt(result, t.getName(), qualifier, partialIdentifier); + addIt(result, "alias", t.getName(), qualifier, partialIdentifier); } } if (qualifier.isEmpty()) { Map> annos = env.getAnnotations(); for (Type t: annos.keySet()) { for (String k: annos.get(t).keySet()) { - addIt(result, k, "", partialIdentifier); + addIt(result, "annotation", k, "", partialIdentifier); } } } } - private static void addIt(SortedSet result, String v, String qualifier, String originalTerm) { + private static void addIt(SortedMap result, String category, String v, String qualifier, String originalTerm) { if (v.startsWith(originalTerm) && !v.equals(originalTerm)) { if (v.contains("-")) { v = "\\" + v; @@ -1760,7 +1757,7 @@ private static void addIt(SortedSet result, String v, String qualifier, if (!qualifier.isEmpty() && !v.startsWith(qualifier)) { v = qualifier + "::" + v; } - result.add(v); + result.put(v, category); } } diff --git a/src/org/rascalmpl/interpreter/IEvaluatorContext.java b/src/org/rascalmpl/interpreter/IEvaluatorContext.java index 6ffa2197096..25562ff4b68 100644 --- a/src/org/rascalmpl/interpreter/IEvaluatorContext.java +++ b/src/org/rascalmpl/interpreter/IEvaluatorContext.java @@ -20,6 +20,7 @@ import java.io.PrintWriter; import java.io.Reader; import java.util.Collection; +import java.util.Map; import java.util.Stack; import org.rascalmpl.ast.AbstractAST; @@ -73,5 +74,6 @@ public interface IEvaluatorContext extends IRascalMonitor { public Stack getAccumulators(); - public Collection completePartialIdentifier(String qualifier, String partialIdentifier); + /** @return identifiers and their category (variable, function, etc) */ + public Map completePartialIdentifier(String qualifier, String partialIdentifier); } diff --git a/src/org/rascalmpl/repl/BaseREPL2.java b/src/org/rascalmpl/repl/BaseREPL2.java index 753a20f72a1..a74a0e8c959 100644 --- a/src/org/rascalmpl/repl/BaseREPL2.java +++ b/src/org/rascalmpl/repl/BaseREPL2.java @@ -78,9 +78,12 @@ public BaseREPL2(IREPLService replService, Terminal term) { // todo: // - ctrl + c support // - ctrl + / support - // - multi-line input // - highlighting in the prompt? - // - + // - nested REPLs + // - queued commands (if it's still needed for import IO etc); + // - support for html results + // - measure time thing + // - source location history thingy } diff --git a/src/org/rascalmpl/repl/IREPLService.java b/src/org/rascalmpl/repl/IREPLService.java index 4729b23e35e..2a98e9af9eb 100644 --- a/src/org/rascalmpl/repl/IREPLService.java +++ b/src/org/rascalmpl/repl/IREPLService.java @@ -16,6 +16,10 @@ public interface IREPLService { String MIME_PLAIN = "text/plain"; String MIME_ANSI = "text/x-ansi"; + /** + * Does this language support completion + * @return + */ default boolean supportsCompletion() { return false; } diff --git a/src/org/rascalmpl/repl/RascalReplServices.java b/src/org/rascalmpl/repl/RascalReplServices.java index 1d9f778db87..d201bb9b2ba 100644 --- a/src/org/rascalmpl/repl/RascalReplServices.java +++ b/src/org/rascalmpl/repl/RascalReplServices.java @@ -34,7 +34,9 @@ import org.rascalmpl.interpreter.staticErrors.StaticError; import org.rascalmpl.parser.gtd.exception.ParseError; import org.rascalmpl.repl.completers.RascalCommandCompletion; +import org.rascalmpl.repl.completers.RascalIdentifierCompletion; import org.rascalmpl.repl.completers.RascalModuleCompletion; +import org.rascalmpl.repl.completers.RascalKeywordCompletion; import org.rascalmpl.repl.jline3.RascalLineParser; import org.rascalmpl.uri.URIUtil; import org.rascalmpl.values.RascalValueFactory; @@ -318,8 +320,15 @@ public boolean supportsCompletion() { public List completers() { var result = new ArrayList(); var moduleCompleter = new RascalModuleCompletion(m -> eval.getRascalResolver().listModuleEntries(m)); - result.add(new RascalCommandCompletion(commandLineOptions,(s,c) -> {}, (s, c) -> moduleCompleter.completeModuleNames(s, c, false))); + var idCompleter = new RascalIdentifierCompletion((q, i) -> eval.completePartialIdentifier(q, i)); + result.add(new RascalCommandCompletion( + commandLineOptions, + idCompleter::completePartialIdentifier, + (s, c) -> moduleCompleter.completeModuleNames(s, c, false) + )); result.add(moduleCompleter); + result.add(idCompleter); + result.add(new RascalKeywordCompletion()); return result; } diff --git a/src/org/rascalmpl/repl/completers/RascalIdentifierCompletion.java b/src/org/rascalmpl/repl/completers/RascalIdentifierCompletion.java new file mode 100644 index 00000000000..d5381b61082 --- /dev/null +++ b/src/org/rascalmpl/repl/completers/RascalIdentifierCompletion.java @@ -0,0 +1,52 @@ +package org.rascalmpl.repl.completers; + +import java.util.List; +import java.util.Map; +import java.util.function.BiFunction; + +import org.jline.reader.Candidate; +import org.jline.reader.Completer; +import org.jline.reader.LineReader; +import org.jline.reader.ParsedLine; + +public class RascalIdentifierCompletion implements Completer { + + private final BiFunction> lookupPartialIdentifiers; + + public RascalIdentifierCompletion(BiFunction> lookupPartialIdentifiers) { + this.lookupPartialIdentifiers = lookupPartialIdentifiers; + } + + @Override + public void complete(LineReader reader, ParsedLine line, List candidates) { + boolean canBeIdentifier; + switch (line.words().get(0)) { + case ":": //fallthrough + // completion of settings for the REPL is handled elsewere + // it will also call this function in the 1 case where it's needed + case "import": // fallthrough + // not triggering on import of modules + case "extend": // fallthrough + // not triggering on extend of modules + canBeIdentifier = false; + break; + default: + canBeIdentifier = true; + break; + + } + if (canBeIdentifier) { + completePartialIdentifier(line.word(), candidates); + } + } + + public void completePartialIdentifier(String name, List candidates) { + int qualifiedSplit = name.lastIndexOf("::"); + String qualifier = qualifiedSplit > -1 ? name.substring(0, qualifiedSplit) : ""; + String partial = qualifiedSplit > -1 ? name.substring(qualifiedSplit + 2) : name; + for (var can: lookupPartialIdentifiers.apply(qualifier, partial).entrySet()) { + candidates.add(new Candidate(can.getKey(), can.getKey(), can.getValue(), null, null, null, false)); + } + } + +} diff --git a/src/org/rascalmpl/repl/completers/RascalKeywordCompletion.java b/src/org/rascalmpl/repl/completers/RascalKeywordCompletion.java new file mode 100644 index 00000000000..bfecbaca6ff --- /dev/null +++ b/src/org/rascalmpl/repl/completers/RascalKeywordCompletion.java @@ -0,0 +1,56 @@ +package org.rascalmpl.repl.completers; + +import java.util.List; +import java.util.NavigableMap; +import java.util.TreeMap; + +import org.jline.reader.Candidate; +import org.jline.reader.Completer; +import org.jline.reader.LineReader; +import org.jline.reader.ParsedLine; + +public class RascalKeywordCompletion implements Completer { + + private static final NavigableMap RASCAL_TYPE_KEYWORDS; + static { + RASCAL_TYPE_KEYWORDS = new TreeMap<>(); + RASCAL_TYPE_KEYWORDS.put("void", "a type without any values"); + RASCAL_TYPE_KEYWORDS.put("int", "sequence of digits of arbitrary length"); + RASCAL_TYPE_KEYWORDS.put("real", "real numbers with arbitrary size and precision"); + RASCAL_TYPE_KEYWORDS.put("num", "int/real/rat type"); + RASCAL_TYPE_KEYWORDS.put("bool", "boolean type"); + RASCAL_TYPE_KEYWORDS.put("data", "user-defined type (Algebraic Data Type)."); + RASCAL_TYPE_KEYWORDS.put("datetime", "date/time/datetime values"); + RASCAL_TYPE_KEYWORDS.put("list", "ordered sequence of values"); + RASCAL_TYPE_KEYWORDS.put("lrel", "lists of tuples with relational calculus"); + RASCAL_TYPE_KEYWORDS.put("loc", "source locations"); + RASCAL_TYPE_KEYWORDS.put("map", "a set of key/value pairs"); + RASCAL_TYPE_KEYWORDS.put("node", "untyped trees"); + RASCAL_TYPE_KEYWORDS.put("set", "unordered sequence of values"); + RASCAL_TYPE_KEYWORDS.put("rel", "sets of tuples with relational calculus"); + RASCAL_TYPE_KEYWORDS.put("str", "a sequence of unicode codepoints"); + RASCAL_TYPE_KEYWORDS.put("tuple", "a sequence of elements"); + RASCAL_TYPE_KEYWORDS.put("value", "all possible values"); + } + + @Override + public void complete(LineReader reader, ParsedLine line, List candidates) { + var words = line.words(); + if (words.size() == 1) { + if ("import".startsWith(words.get(0))) { + add(candidates, "import", "statement", "import a module into the repl"); + } + if ("extend".startsWith(words.get(0))) { + add(candidates, "extend", "statement", "extend a module into the repl"); + } + } + for (var can: RASCAL_TYPE_KEYWORDS.subMap(line.word(), true, line.word() + Character.MAX_VALUE, false).entrySet()) { + add(candidates, can.getKey(), "type", can.getValue()); + } + } + + private static void add(List candidates, String value, String group, String description) { + candidates.add(new Candidate(value, value, group, description, null, null, true)); + } + +} diff --git a/src/org/rascalmpl/repl/completers/RascalModuleCompletion.java b/src/org/rascalmpl/repl/completers/RascalModuleCompletion.java index 301a496edfd..9229aa46812 100644 --- a/src/org/rascalmpl/repl/completers/RascalModuleCompletion.java +++ b/src/org/rascalmpl/repl/completers/RascalModuleCompletion.java @@ -41,7 +41,7 @@ public void completeModuleNames(String word, List candidates, boolean for (var mod : searchPathLookup.apply(moduleRoot)) { var fullPath = modulePrefix + mod; var isFullModulePath = !mod.endsWith("::"); - candidates.add(new Candidate(fullPath + (isFullModulePath & importStatement? ";" : ""), fullPath, "modules", null, null, null, isFullModulePath)); + candidates.add(new Candidate(fullPath + (isFullModulePath & importStatement? ";" : ""), fullPath, "modules", null, null, null, false)); } } } From c0387b02fd0912c33f105f396969e64357aac5e7 Mon Sep 17 00:00:00 2001 From: Davy Landman Date: Thu, 21 Nov 2024 16:19:30 +0100 Subject: [PATCH 16/35] Support ctrl+c to interrupt a running rascal command --- src/org/rascalmpl/repl/BaseREPL2.java | 52 +++++++++++++++---- src/org/rascalmpl/repl/IREPLService.java | 10 ++-- .../rascalmpl/repl/RascalReplServices.java | 4 +- src/org/rascalmpl/shell/RascalShell2.java | 1 + 4 files changed, 53 insertions(+), 14 deletions(-) diff --git a/src/org/rascalmpl/repl/BaseREPL2.java b/src/org/rascalmpl/repl/BaseREPL2.java index a74a0e8c959..983491464a3 100644 --- a/src/org/rascalmpl/repl/BaseREPL2.java +++ b/src/org/rascalmpl/repl/BaseREPL2.java @@ -2,8 +2,11 @@ import java.io.IOException; import java.io.PrintWriter; +import java.nio.channels.AcceptPendingException; import java.util.HashMap; import java.util.Map; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicReference; import org.checkerframework.checker.nullness.qual.MonotonicNonNull; import org.jline.reader.LineReader; @@ -13,6 +16,8 @@ import org.jline.reader.impl.completer.AggregateCompleter; import org.jline.reader.impl.history.DefaultHistory; import org.jline.terminal.Terminal; +import org.jline.terminal.Terminal.Signal; +import org.jline.terminal.Terminal.SignalHandler; import org.jline.utils.ShutdownHooks; public class BaseREPL2 { @@ -49,6 +54,7 @@ public BaseREPL2(IREPLService replService, Terminal term) { } reader.option(Option.HISTORY_IGNORE_DUPS, replService.historyIgnoreDuplicates()); + if (replService.supportsCompletion()) { reader.completer(new AggregateCompleter(replService.completers())); } @@ -76,35 +82,40 @@ public BaseREPL2(IREPLService replService, Terminal term) { // todo: - // - ctrl + c support - // - ctrl + / support - // - highlighting in the prompt? + // - ctrl + / support (might not be possible) + // - quiting of the REPL via `:quit` + // - highlighting in the prompt? (future work, as it also hurts other parts) // - nested REPLs // - queued commands (if it's still needed for import IO etc); // - support for html results - // - measure time thing - // - source location history thingy + // - measure time + // - history? } public void run() throws IOException { try { replService.connect(term); + var running = setupInterruptHandler(); + while (keepRunning) { try { + replService.flush(); String line = reader.readLine(this.currentPrompt); if (line == null) { // EOF break; } + running.set(true); handleInput(line); } catch (UserInterruptException u) { - reader.printAbove("> interrupted"); + // only thrown while `readLine` is active + reader.printAbove(">>>>>>> Interrupted"); term.flush(); - var out = new HashMap(); - replService.handleReset(out, new HashMap<>()); - writeResult(out); + } + finally { + running.set(false); } } } @@ -138,6 +149,29 @@ public void run() throws IOException { } } + private AtomicBoolean setupInterruptHandler() { + var running = new AtomicBoolean(false); + var original = new AtomicReference(null); + original.set(term.handle(Signal.INT, (s) -> { + if (running.get()) { + try { + replService.handleInterrupt(); + } + catch (InterruptedException e) { + return; + } + } + else { + var fallback = original.get(); + if (fallback != null) { + fallback.handle(s); + } + } + })); + + return running; + } + private void handleInput(String line) throws InterruptedException { var result = new HashMap(); diff --git a/src/org/rascalmpl/repl/IREPLService.java b/src/org/rascalmpl/repl/IREPLService.java index 2a98e9af9eb..5be73ad6c5e 100644 --- a/src/org/rascalmpl/repl/IREPLService.java +++ b/src/org/rascalmpl/repl/IREPLService.java @@ -67,9 +67,10 @@ default Path historyFile() { // todo see if we really need the meta-data void handleInput(String input, Map output, Map metadata) throws InterruptedException; - - // todo see if we really need the meta-data - void handleReset(Map output, Map metadata) throws InterruptedException; + /** + * Will be called from a different thread then the one that called `handleInput` + */ + void handleInterrupt() throws InterruptedException; /** * Default prompt @@ -85,6 +86,9 @@ default Path historyFile() { PrintWriter errorWriter(); PrintWriter outputWriter(); + /** + * Flush the streams, will be triggered at the end of execution, and before showing the prompt. + */ void flush(); } diff --git a/src/org/rascalmpl/repl/RascalReplServices.java b/src/org/rascalmpl/repl/RascalReplServices.java index d201bb9b2ba..06091dcd189 100644 --- a/src/org/rascalmpl/repl/RascalReplServices.java +++ b/src/org/rascalmpl/repl/RascalReplServices.java @@ -264,8 +264,8 @@ public Reader asReader() { } @Override - public void handleReset(Map output, Map metadata) - throws InterruptedException { + public void handleInterrupt() throws InterruptedException { + eval.interrupt(); } @Override diff --git a/src/org/rascalmpl/shell/RascalShell2.java b/src/org/rascalmpl/shell/RascalShell2.java index 716cf347e5d..a273ac431c3 100644 --- a/src/org/rascalmpl/shell/RascalShell2.java +++ b/src/org/rascalmpl/shell/RascalShell2.java @@ -49,6 +49,7 @@ public static void main(String[] args) throws IOException { if (OSUtils.IS_WINDOWS) { termBuilder.encoding(StandardCharsets.UTF_8); } + termBuilder.dumb(true); // fallback to dumb terminal if detected terminal is not supported var term = termBuilder.build(); From 3d53007e8cde847ee62f94e17364c67a1ceaf081 Mon Sep 17 00:00:00 2001 From: Davy Landman Date: Thu, 21 Nov 2024 16:58:15 +0100 Subject: [PATCH 17/35] Added location completion support --- src/org/rascalmpl/repl/BaseREPL2.java | 9 +- .../rascalmpl/repl/RascalReplServices.java | 4 + .../completers/RascalKeywordCompletion.java | 7 +- .../completers/RascalLocationCompletion.java | 111 ++++++++++++++++++ .../repl/jline3/RascalLineParser.java | 4 +- .../rascalmpl/test/repl/JlineParserTest.java | 9 ++ 6 files changed, 139 insertions(+), 5 deletions(-) create mode 100644 src/org/rascalmpl/repl/completers/RascalLocationCompletion.java diff --git a/src/org/rascalmpl/repl/BaseREPL2.java b/src/org/rascalmpl/repl/BaseREPL2.java index 983491464a3..209cca0da43 100644 --- a/src/org/rascalmpl/repl/BaseREPL2.java +++ b/src/org/rascalmpl/repl/BaseREPL2.java @@ -9,6 +9,7 @@ import java.util.concurrent.atomic.AtomicReference; import org.checkerframework.checker.nullness.qual.MonotonicNonNull; +import org.jline.reader.EndOfFileException; import org.jline.reader.LineReader; import org.jline.reader.LineReader.Option; import org.jline.reader.LineReaderBuilder; @@ -83,13 +84,14 @@ public BaseREPL2(IREPLService replService, Terminal term) { // todo: // - ctrl + / support (might not be possible) - // - quiting of the REPL via `:quit` // - highlighting in the prompt? (future work, as it also hurts other parts) // - nested REPLs // - queued commands (if it's still needed for import IO etc); // - support for html results // - measure time // - history? + // - completion of locations + // - escape & unescape of keywords in imports auto completion } @@ -122,6 +124,11 @@ public void run() throws IOException { catch (InterruptedException _e) { // closing the runner } + catch (EndOfFileException e) { + // user pressed ctrl+d or the terminal :quit command was given + // so exit cleanly + replService.errorWriter().println("Quiting REPL"); + } catch (Throwable e) { var err = replService.errorWriter(); diff --git a/src/org/rascalmpl/repl/RascalReplServices.java b/src/org/rascalmpl/repl/RascalReplServices.java index 06091dcd189..c45f320a140 100644 --- a/src/org/rascalmpl/repl/RascalReplServices.java +++ b/src/org/rascalmpl/repl/RascalReplServices.java @@ -20,6 +20,7 @@ import org.jline.jansi.Ansi; import org.jline.reader.Completer; +import org.jline.reader.EndOfFileException; import org.jline.reader.Parser; import org.jline.terminal.Terminal; import org.jline.utils.InfoCmp.Capability; @@ -37,6 +38,7 @@ import org.rascalmpl.repl.completers.RascalIdentifierCompletion; import org.rascalmpl.repl.completers.RascalModuleCompletion; import org.rascalmpl.repl.completers.RascalKeywordCompletion; +import org.rascalmpl.repl.completers.RascalLocationCompletion; import org.rascalmpl.repl.jline3.RascalLineParser; import org.rascalmpl.uri.URIUtil; import org.rascalmpl.values.RascalValueFactory; @@ -128,6 +130,7 @@ public void handleInput(String input, Map output, Map { w.println("Quiting REPL"); }); + throw new EndOfFileException("Quiting REPL"); } catch (Throwable e) { reportError(output, (w, sw) -> { @@ -329,6 +332,7 @@ public List completers() { result.add(moduleCompleter); result.add(idCompleter); result.add(new RascalKeywordCompletion()); + result.add(new RascalLocationCompletion()); return result; } diff --git a/src/org/rascalmpl/repl/completers/RascalKeywordCompletion.java b/src/org/rascalmpl/repl/completers/RascalKeywordCompletion.java index bfecbaca6ff..0005793a2ff 100644 --- a/src/org/rascalmpl/repl/completers/RascalKeywordCompletion.java +++ b/src/org/rascalmpl/repl/completers/RascalKeywordCompletion.java @@ -44,8 +44,11 @@ public void complete(LineReader reader, ParsedLine line, List candida add(candidates, "extend", "statement", "extend a module into the repl"); } } - for (var can: RASCAL_TYPE_KEYWORDS.subMap(line.word(), true, line.word() + Character.MAX_VALUE, false).entrySet()) { - add(candidates, can.getKey(), "type", can.getValue()); + var firstWord = words.get(0); + if (!firstWord.equals("import") && !firstWord.equals("extend") && !firstWord.equals(":")) { + for (var can: RASCAL_TYPE_KEYWORDS.subMap(line.word(), true, line.word() + Character.MAX_VALUE, false).entrySet()) { + add(candidates, can.getKey(), "type", can.getValue()); + } } } diff --git a/src/org/rascalmpl/repl/completers/RascalLocationCompletion.java b/src/org/rascalmpl/repl/completers/RascalLocationCompletion.java new file mode 100644 index 00000000000..8e205965d67 --- /dev/null +++ b/src/org/rascalmpl/repl/completers/RascalLocationCompletion.java @@ -0,0 +1,111 @@ +package org.rascalmpl.repl.completers; + +import java.io.IOException; +import java.net.URI; +import java.net.URISyntaxException; +import java.util.List; +import java.util.Set; + +import org.jline.reader.Candidate; +import org.jline.reader.Completer; +import org.jline.reader.LineReader; +import org.jline.reader.ParsedLine; +import org.rascalmpl.uri.URIResolverRegistry; +import org.rascalmpl.uri.URIUtil; +import org.rascalmpl.values.IRascalValueFactory; + +import io.usethesource.vallang.ISourceLocation; +import io.usethesource.vallang.IValueFactory; + +public class RascalLocationCompletion implements Completer { + + private static final IValueFactory VF = IRascalValueFactory.getInstance(); + private static final URIResolverRegistry REG = URIResolverRegistry.getInstance(); + + @Override + public void complete(LineReader reader, ParsedLine line, List candidates) { + if (!line.word().startsWith("|")) { + return; + } + try { + String locCandidate = line.word().substring(1); + if (!locCandidate.contains("://")) { + // only complete scheme + completeSchema(candidates, locCandidate); + return; + } + if (completeAuthorities(candidates, locCandidate)) { + // we only had authorities to complete + return; + } + + // so we have at least a partial location + ISourceLocation directory = VF.sourceLocation(new URI(locCandidate)); + String fileName = ""; + if (!REG.isDirectory(directory)) { + // split filename and directory, to get to the actual directory + String fullPath = directory.getPath(); + int lastSeparator = fullPath.lastIndexOf('/'); + fileName = fullPath.substring(lastSeparator + 1); + fullPath = fullPath.substring(0, lastSeparator); + directory = VF.sourceLocation(directory.getScheme(), directory.getAuthority(), fullPath); + if (!REG.isDirectory(directory)) { + return; + } + } + for (String currentFile : REG.listEntries(directory)) { + if (currentFile.startsWith(fileName)) { + add(candidates, URIUtil.getChildLocation(directory, currentFile)); + } + } + } + catch (URISyntaxException|IOException e) { + } + } + + private void add(List candidates, ISourceLocation loc) { + String locCandidate = loc.toString(); + if (REG.isDirectory(loc)) { + // remove trailing | so we can continue + // and add path separator + locCandidate = locCandidate.substring(0, locCandidate.length() - 1); + if (!locCandidate.endsWith("/")) { + locCandidate += "/"; + } + } + candidates.add(new Candidate(locCandidate, locCandidate, "location", null, null, null, false)); + } + + private boolean completeAuthorities(List candidates, String locCandidate) throws URISyntaxException, + IOException { + int lastSeparator = locCandidate.lastIndexOf('/'); + if (lastSeparator > 3 && locCandidate.substring(lastSeparator - 2, lastSeparator + 1).equals("://")) { + // special case, we want to complete authorities (but URI's without a authority are not valid) + String scheme = locCandidate.substring(0, lastSeparator - 2); + String partialAuthority = locCandidate.substring(lastSeparator + 1); + ISourceLocation root = VF.sourceLocation(scheme, "", ""); + for (String candidate: REG.listEntries(root)) { + if (candidate.startsWith(partialAuthority)) { + add(candidates, URIUtil.correctLocation(scheme, candidate, "")); + } + } + return true; + } + return false; + } + + private void completeSchema(List candidates, String locCandidate) { + filterCandidates(REG.getRegisteredInputSchemes(), candidates, locCandidate); + filterCandidates(REG.getRegisteredLogicalSchemes(), candidates, locCandidate); + filterCandidates(REG.getRegisteredOutputSchemes(), candidates, locCandidate); + } + + private void filterCandidates(Set src, List target, String prefix) { + for (String s : src) { + if (s.startsWith(prefix)) { + add(target, URIUtil.rootLocation(s)); + } + } + } + +} diff --git a/src/org/rascalmpl/repl/jline3/RascalLineParser.java b/src/org/rascalmpl/repl/jline3/RascalLineParser.java index fb92dfca32e..80100365055 100644 --- a/src/org/rascalmpl/repl/jline3/RascalLineParser.java +++ b/src/org/rascalmpl/repl/jline3/RascalLineParser.java @@ -76,12 +76,12 @@ private void parseWords(String buffer, int position, List words) { int wordEnd = position; if (c == '"' || (c == '>' && inString)) { wordEnd = parseEndedAfter(buffer, position, RASCAL_STRING); - inString = wordEnd != position && buffer.charAt(wordEnd - 1) != '"'; + inString = wordEnd != buffer.length() && buffer.charAt(wordEnd - 1) != '"'; isWord = false; } else if (c == '|' || (c == '>' && inLocation)) { wordEnd = parseEndedAfter(buffer, position, RASCAL_LOCATION); - inLocation = wordEnd != position && buffer.charAt(position - 1) == '<'; + inLocation = wordEnd != buffer.length() && buffer.charAt(wordEnd - 1) == '<'; } else if (Character.isJavaIdentifierPart(c) || c == '\\') { wordEnd = parseEndedAfter(buffer, position, RASCAL_NAME); diff --git a/test/org/rascalmpl/test/repl/JlineParserTest.java b/test/org/rascalmpl/test/repl/JlineParserTest.java index 033cb03a2c0..d27c2046604 100644 --- a/test/org/rascalmpl/test/repl/JlineParserTest.java +++ b/test/org/rascalmpl/test/repl/JlineParserTest.java @@ -53,5 +53,14 @@ public void qualifiedNames() { assertEquals("lang::rascal", completeParser("import lang::rascal").word()); } + @Test + public void locations() { + assertEquals("|", completeParser("|").word()); + assertEquals("|file", completeParser("|file").word()); + assertEquals("|file://", completeParser("|file://").word()); + assertEquals("|file:///home/dir", completeParser("|file:///home/dir").word()); + assertEquals("|file:///home/dir|", completeParser("|file:///home/dir|").word()); + } + } From 26d6052ef7485e8b1aacfc4bb8a6bb1f48fe5b84 Mon Sep 17 00:00:00 2001 From: Davy Landman Date: Mon, 25 Nov 2024 15:54:44 +0100 Subject: [PATCH 18/35] Tuned completion a bit around locations and strings --- src/org/rascalmpl/repl/completers/RascalLocationCompletion.java | 2 +- src/org/rascalmpl/repl/jline3/RascalLineParser.java | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/src/org/rascalmpl/repl/completers/RascalLocationCompletion.java b/src/org/rascalmpl/repl/completers/RascalLocationCompletion.java index 8e205965d67..6f9965f8731 100644 --- a/src/org/rascalmpl/repl/completers/RascalLocationCompletion.java +++ b/src/org/rascalmpl/repl/completers/RascalLocationCompletion.java @@ -47,7 +47,7 @@ public void complete(LineReader reader, ParsedLine line, List candida String fullPath = directory.getPath(); int lastSeparator = fullPath.lastIndexOf('/'); fileName = fullPath.substring(lastSeparator + 1); - fullPath = fullPath.substring(0, lastSeparator); + fullPath = fullPath.substring(0, lastSeparator + 1); directory = VF.sourceLocation(directory.getScheme(), directory.getAuthority(), fullPath); if (!REG.isDirectory(directory)) { return; diff --git a/src/org/rascalmpl/repl/jline3/RascalLineParser.java b/src/org/rascalmpl/repl/jline3/RascalLineParser.java index 80100365055..bde23a0781a 100644 --- a/src/org/rascalmpl/repl/jline3/RascalLineParser.java +++ b/src/org/rascalmpl/repl/jline3/RascalLineParser.java @@ -77,7 +77,6 @@ private void parseWords(String buffer, int position, List words) { if (c == '"' || (c == '>' && inString)) { wordEnd = parseEndedAfter(buffer, position, RASCAL_STRING); inString = wordEnd != buffer.length() && buffer.charAt(wordEnd - 1) != '"'; - isWord = false; } else if (c == '|' || (c == '>' && inLocation)) { wordEnd = parseEndedAfter(buffer, position, RASCAL_LOCATION); From 922c61e38c6512b08d5073540952e476ff81c4ae Mon Sep 17 00:00:00 2001 From: Davy Landman Date: Mon, 25 Nov 2024 15:57:02 +0100 Subject: [PATCH 19/35] Fixed word lexer test --- test/org/rascalmpl/test/repl/JlineParserTest.java | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/test/org/rascalmpl/test/repl/JlineParserTest.java b/test/org/rascalmpl/test/repl/JlineParserTest.java index d27c2046604..cbf45c1aa0a 100644 --- a/test/org/rascalmpl/test/repl/JlineParserTest.java +++ b/test/org/rascalmpl/test/repl/JlineParserTest.java @@ -33,8 +33,8 @@ public void commandsParsedCorrectly() { @Test public void stringsAreNotParsedAsWords() { assertEquals(1, completeParser("\"long string with multiple spaces\"").words().size()); // word - assertEquals("", completeParser("\"long string with multiple spaces\"",11).word()); - assertEquals("", completeParser("\"long string with multiple spaces\"").word()); + assertEquals("\"long string with multiple spaces\"", completeParser("\"long string with multiple spaces\"",11).word()); + assertEquals("\"long string with multiple spaces\"", completeParser("\"long string with multiple spaces\"").word()); assertEquals(2, completeParser("x = \"long string with multiple spaces\";").words().size()); } @@ -43,8 +43,8 @@ public void stringInterpolation() { assertEquals("exp", completeParser("\"string string ending\"",11).words().size()); - assertEquals(2, completeParser("\"string string ending\"").words().size()); // at the end always an extra one + assertEquals(3, completeParser("\"string string ending\"",11).words().size()); + assertEquals(3, completeParser("\"string string ending\"").words().size()); // at the end always an extra one } @Test From 11a910356fb0f53bdf1606438dfa15e979352b95 Mon Sep 17 00:00:00 2001 From: Davy Landman Date: Mon, 25 Nov 2024 16:57:47 +0100 Subject: [PATCH 20/35] Detect keywords and escape them in completions --- src/org/rascalmpl/repl/BaseREPL2.java | 3 +- .../RascalIdentifierCompletion.java | 4 +- .../completers/RascalModuleCompletion.java | 3 +- .../repl/completers/RascalQualifiedNames.java | 118 ++++++++++++++++++ .../repl/jline3/RascalLineParser.java | 2 +- .../rascalmpl/test/repl/JlineParserTest.java | 2 + 6 files changed, 128 insertions(+), 4 deletions(-) create mode 100644 src/org/rascalmpl/repl/completers/RascalQualifiedNames.java diff --git a/src/org/rascalmpl/repl/BaseREPL2.java b/src/org/rascalmpl/repl/BaseREPL2.java index 209cca0da43..204ac2a8663 100644 --- a/src/org/rascalmpl/repl/BaseREPL2.java +++ b/src/org/rascalmpl/repl/BaseREPL2.java @@ -54,6 +54,7 @@ public BaseREPL2(IREPLService replService, Terminal term) { this.history = null; } reader.option(Option.HISTORY_IGNORE_DUPS, replService.historyIgnoreDuplicates()); + reader.option(Option.DISABLE_EVENT_EXPANSION, true); // stop jline expending escaped characters in the input if (replService.supportsCompletion()) { @@ -74,7 +75,7 @@ public BaseREPL2(IREPLService replService, Terminal term) { this.mimeType = ANSI_MIME_TYPE; break; } - this.unicodeSupported = term.encoding().newEncoder().canEncode("👍🏽"); + this.unicodeSupported = term.encoding().newEncoder().canEncode("💓"); this.currentPrompt = replService.prompt(ansiSupported, unicodeSupported); reader.variable(LineReader.SECONDARY_PROMPT_PATTERN, replService.parseErrorPrompt(ansiSupported, unicodeSupported)); diff --git a/src/org/rascalmpl/repl/completers/RascalIdentifierCompletion.java b/src/org/rascalmpl/repl/completers/RascalIdentifierCompletion.java index d5381b61082..1324dcdb6e0 100644 --- a/src/org/rascalmpl/repl/completers/RascalIdentifierCompletion.java +++ b/src/org/rascalmpl/repl/completers/RascalIdentifierCompletion.java @@ -41,11 +41,13 @@ public void complete(LineReader reader, ParsedLine line, List candida } public void completePartialIdentifier(String name, List candidates) { + name = RascalQualifiedNames.unescape(name); // remove escape that the interpreter cannot deal with int qualifiedSplit = name.lastIndexOf("::"); String qualifier = qualifiedSplit > -1 ? name.substring(0, qualifiedSplit) : ""; String partial = qualifiedSplit > -1 ? name.substring(qualifiedSplit + 2) : name; for (var can: lookupPartialIdentifiers.apply(qualifier, partial).entrySet()) { - candidates.add(new Candidate(can.getKey(), can.getKey(), can.getValue(), null, null, null, false)); + String id = RascalQualifiedNames.escape(can.getKey()); + candidates.add(new Candidate(id, id, can.getValue(), null, null, null, false)); } } diff --git a/src/org/rascalmpl/repl/completers/RascalModuleCompletion.java b/src/org/rascalmpl/repl/completers/RascalModuleCompletion.java index 9229aa46812..b60b2e95a93 100644 --- a/src/org/rascalmpl/repl/completers/RascalModuleCompletion.java +++ b/src/org/rascalmpl/repl/completers/RascalModuleCompletion.java @@ -35,11 +35,12 @@ public void complete(LineReader reader, ParsedLine line, List candida public void completeModuleNames(String word, List candidates, boolean importStatement) { // as jline will take care to filter prefixes, we only have to report modules in the directory (or siblings of the name) // we do not have to filter out prefixes + word = RascalQualifiedNames.unescape(word); // remove escape that the interpreter cannot deal with int rootedIndex = word.lastIndexOf("::"); String moduleRoot = rootedIndex == -1? "": word.substring(0, rootedIndex); String modulePrefix = moduleRoot.isEmpty() ? "" : moduleRoot + "::"; for (var mod : searchPathLookup.apply(moduleRoot)) { - var fullPath = modulePrefix + mod; + var fullPath = RascalQualifiedNames.escape(modulePrefix + mod); var isFullModulePath = !mod.endsWith("::"); candidates.add(new Candidate(fullPath + (isFullModulePath & importStatement? ";" : ""), fullPath, "modules", null, null, null, false)); } diff --git a/src/org/rascalmpl/repl/completers/RascalQualifiedNames.java b/src/org/rascalmpl/repl/completers/RascalQualifiedNames.java new file mode 100644 index 00000000000..620f5382af6 --- /dev/null +++ b/src/org/rascalmpl/repl/completers/RascalQualifiedNames.java @@ -0,0 +1,118 @@ +package org.rascalmpl.repl.completers; + +import java.io.IOException; +import java.io.Reader; +import java.net.URISyntaxException; +import java.util.HashSet; +import java.util.Set; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.util.stream.Collectors; + +import org.rascalmpl.uri.URIResolverRegistry; +import org.rascalmpl.values.ValueFactoryFactory; + +/** + * Make sure we generate escapes before rascal keywords, such that `lang::rascal::syntax` becomes `lang::rascal::\syntax` + */ +public class RascalQualifiedNames { + + private static final Pattern splitIdentifiers = Pattern.compile("[:][:]"); + + public static String escape(String name) { + return splitIdentifiers.splitAsStream(name + " ") // add space such that the last "::" is not lost + .map(RascalQualifiedNames::escapeKeyword) + .collect(Collectors.joining("::")).trim(); + } + public static String unescape(String term) { + return splitIdentifiers.splitAsStream(term + " ") // add space such that the last "::" is not lost + .map(RascalQualifiedNames::unescapeKeyword) + .collect(Collectors.joining("::")).trim() + ; + } + + private static final Set RASCAL_KEYWORDS = new HashSet(); + + private static void assureKeywordsAreScrapped() { + // TODO: replace this with the `util::Reflective::getRascalReservedIdentifiers` + // BUT! it doesn't contain all the keywords, it's missing the `BasicType` ones like `int` etc + if (RASCAL_KEYWORDS.isEmpty()) { + synchronized (RASCAL_KEYWORDS) { + if (!RASCAL_KEYWORDS.isEmpty()) { + return; + } + + String rascalGrammar = ""; + URIResolverRegistry reg = URIResolverRegistry.getInstance(); + try (Reader grammarReader = reg.getCharacterReader(ValueFactoryFactory.getValueFactory().sourceLocation("std", "", "/lang/rascal/syntax/Rascal.rsc"))) { + StringBuilder res = new StringBuilder(); + char[] chunk = new char[8 * 1024]; + int read; + while ((read = grammarReader.read(chunk, 0, chunk.length)) != -1) { + res.append(chunk, 0, read); + } + rascalGrammar = res.toString(); + } + catch (IOException | URISyntaxException e) { + e.printStackTrace(); + } + if (!rascalGrammar.isEmpty()) { + /* + * keyword RascalKeywords + * = "o" + * | "syntax" + * | "keyword" + * | "lexical" + * ... + * ; + */ + Pattern findKeywordSection = Pattern.compile("^\\s*keyword([^=]|\\s)*=(?([^;]|\\s)*);", Pattern.MULTILINE); + Matcher m = findKeywordSection.matcher(rascalGrammar); + if (m.find()) { + String keywords = "|" + m.group("keywords"); + Pattern keywordEntry = Pattern.compile("\\s*[|]\\s*[\"](?[^\"]*)[\"]"); + m = keywordEntry.matcher(keywords); + while (m.find()) { + RASCAL_KEYWORDS.add(m.group("keyword")); + } + } + /* + * syntax BasicType + = \value: "value" + | \loc: "loc" + | \node: "node" + */ + Pattern findBasicTypeSection = Pattern.compile("^\\s*syntax\\s*BasicType([^=]|\\s)*=(?([^;]|\\s)*);", Pattern.MULTILINE); + m = findBasicTypeSection.matcher(rascalGrammar); + if (m.find()) { + String keywords = "|" + m.group("keywords"); + Pattern keywordEntry = Pattern.compile("\\s*[|][^:]*:\\s*[\"](?[^\"]*)[\"]"); + m = keywordEntry.matcher(keywords); + while (m.find()) { + RASCAL_KEYWORDS.add(m.group("keyword")); + } + } + } + if (RASCAL_KEYWORDS.isEmpty()) { + RASCAL_KEYWORDS.add("syntax"); + } + } + } + } + + private static String escapeKeyword(String s) { + assureKeywordsAreScrapped(); + if (RASCAL_KEYWORDS.contains(s.trim())) { + return "\\" + s; + } + return s; + } + + private static String unescapeKeyword(String s) { + if (s.startsWith("\\") && !s.contains("-")) { + return s.substring(1); + } + return s; + } + +} diff --git a/src/org/rascalmpl/repl/jline3/RascalLineParser.java b/src/org/rascalmpl/repl/jline3/RascalLineParser.java index bde23a0781a..f754f157b43 100644 --- a/src/org/rascalmpl/repl/jline3/RascalLineParser.java +++ b/src/org/rascalmpl/repl/jline3/RascalLineParser.java @@ -200,7 +200,7 @@ private static int parseEndedAfter(String buffer, int position, Pattern parser) = Pattern.compile("^[\\|\\>][^\\|\\<\\t-\\n\\r ]*[\\|\\<]?"); private static final Pattern RASCAL_NAME - = Pattern.compile("^(([A-Za-z_]([A-Za-z0-9_]|::)*)|([\\\\][A-Za-z_]([\\-A-Za-z0-9_]|::)*))"); + = Pattern.compile("^((([A-Za-z_][A-Za-z0-9_]*)|([\\\\][A-Za-z_]([\\-A-Za-z0-9_])*))(::)?)+"); // only unicode spaces & multi-line comments private static final Pattern RASCAL_WHITE_SPACE diff --git a/test/org/rascalmpl/test/repl/JlineParserTest.java b/test/org/rascalmpl/test/repl/JlineParserTest.java index cbf45c1aa0a..91e082e1d44 100644 --- a/test/org/rascalmpl/test/repl/JlineParserTest.java +++ b/test/org/rascalmpl/test/repl/JlineParserTest.java @@ -51,6 +51,8 @@ public void stringInterpolation() { public void qualifiedNames() { assertEquals("IO::print", completeParser("IO::print").word()); assertEquals("lang::rascal", completeParser("import lang::rascal").word()); + assertEquals("lang::rascal::\\syntax", completeParser("import lang::rascal::\\syntax").word()); + assertEquals("lang::rascal::\\syntax::Rascal", completeParser("import lang::rascal::\\syntax::Rascal").word()); } @Test From 4adacedd834fbb269b5c7b877120b77421fde0d3 Mon Sep 17 00:00:00 2001 From: Davy Landman Date: Mon, 25 Nov 2024 16:59:12 +0100 Subject: [PATCH 21/35] Updated todo list --- src/org/rascalmpl/repl/BaseREPL2.java | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/org/rascalmpl/repl/BaseREPL2.java b/src/org/rascalmpl/repl/BaseREPL2.java index 204ac2a8663..e4c803199c2 100644 --- a/src/org/rascalmpl/repl/BaseREPL2.java +++ b/src/org/rascalmpl/repl/BaseREPL2.java @@ -91,9 +91,6 @@ public BaseREPL2(IREPLService replService, Terminal term) { // - support for html results // - measure time // - history? - // - completion of locations - // - escape & unescape of keywords in imports auto completion - } public void run() throws IOException { From 2370802aaaefb294bdc3f15799ae8e8f720fc72a Mon Sep 17 00:00:00 2001 From: Davy Landman Date: Mon, 25 Nov 2024 17:10:35 +0100 Subject: [PATCH 22/35] Added queued command support --- src/org/rascalmpl/repl/BaseREPL2.java | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/src/org/rascalmpl/repl/BaseREPL2.java b/src/org/rascalmpl/repl/BaseREPL2.java index e4c803199c2..49ba74016e1 100644 --- a/src/org/rascalmpl/repl/BaseREPL2.java +++ b/src/org/rascalmpl/repl/BaseREPL2.java @@ -2,9 +2,8 @@ import java.io.IOException; import java.io.PrintWriter; -import java.nio.channels.AcceptPendingException; +import java.util.Arrays; import java.util.HashMap; -import java.util.Map; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicReference; @@ -87,7 +86,6 @@ public BaseREPL2(IREPLService replService, Terminal term) { // - ctrl + / support (might not be possible) // - highlighting in the prompt? (future work, as it also hurts other parts) // - nested REPLs - // - queued commands (if it's still needed for import IO etc); // - support for html results // - measure time // - history? @@ -102,6 +100,7 @@ public void run() throws IOException { try { replService.flush(); String line = reader.readLine(this.currentPrompt); + if (line == null) { // EOF break; @@ -154,6 +153,14 @@ public void run() throws IOException { } } + /** + * Queue a command (separated by newlines) to be "entered" + * No support for multi-line input + */ + public void queueCommand(String command) { + reader.addCommandsInBuffer(Arrays.asList(command.split("[\\n\\r]"))); + } + private AtomicBoolean setupInterruptHandler() { var running = new AtomicBoolean(false); var original = new AtomicReference(null); From a58f57e416ad1a3afb47c90ab3e0de1631a0bc0e Mon Sep 17 00:00:00 2001 From: Davy Landman Date: Tue, 17 Dec 2024 11:09:54 +0100 Subject: [PATCH 23/35] Added clear capability --- src/org/rascalmpl/ideservices/BasicIDEServices.java | 10 ++++++++++ src/org/rascalmpl/ideservices/IDEServices.java | 4 ++++ src/org/rascalmpl/repl/BaseREPL2.java | 2 ++ .../repl/completers/RascalCommandCompletion.java | 2 +- src/org/rascalmpl/semantics/dynamic/ShellCommand.java | 11 ++++++++++- src/org/rascalmpl/shell/RascalShell2.java | 5 +++-- 6 files changed, 30 insertions(+), 4 deletions(-) diff --git a/src/org/rascalmpl/ideservices/BasicIDEServices.java b/src/org/rascalmpl/ideservices/BasicIDEServices.java index f799fdecb4a..4f6ea8063ab 100644 --- a/src/org/rascalmpl/ideservices/BasicIDEServices.java +++ b/src/org/rascalmpl/ideservices/BasicIDEServices.java @@ -34,16 +34,26 @@ public class BasicIDEServices implements IDEServices { private final IRascalMonitor monitor; private final PrintWriter stderr; + private final Runnable clearRepl; public BasicIDEServices(PrintWriter stderr, IRascalMonitor monitor){ + this(stderr, monitor, () -> {}); + } + public BasicIDEServices(PrintWriter stderr, IRascalMonitor monitor, Runnable clearRepl){ this.stderr = stderr; this.monitor = monitor; + this.clearRepl = clearRepl; } @Override public PrintWriter stderr() { return stderr; } + + @Override + public void clearRepl() { + this.clearRepl.run(); + } public void browse(ISourceLocation loc, String title, int viewColumn){ browse(loc.getURI(), title, viewColumn); diff --git a/src/org/rascalmpl/ideservices/IDEServices.java b/src/org/rascalmpl/ideservices/IDEServices.java index c5164d95071..10ed5c843fb 100644 --- a/src/org/rascalmpl/ideservices/IDEServices.java +++ b/src/org/rascalmpl/ideservices/IDEServices.java @@ -73,6 +73,10 @@ default void unregisterLanguage(IConstructor language) { throw new UnsupportedOperationException("registerLanguage is not implemented in this environment."); } + default void clearRepl() { + throw new UnsupportedOperationException("Clear REPL is not supported in this IDE Service"); + } + /** * Asks the IDE to apply document edits as defined in the standard library module * analysis::diff::edits::TextEdits, according to the semantics defined in diff --git a/src/org/rascalmpl/repl/BaseREPL2.java b/src/org/rascalmpl/repl/BaseREPL2.java index 49ba74016e1..aa4f9a36912 100644 --- a/src/org/rascalmpl/repl/BaseREPL2.java +++ b/src/org/rascalmpl/repl/BaseREPL2.java @@ -19,6 +19,7 @@ import org.jline.terminal.Terminal.Signal; import org.jline.terminal.Terminal.SignalHandler; import org.jline.utils.ShutdownHooks; +import org.jline.utils.InfoCmp.Capability; public class BaseREPL2 { @@ -89,6 +90,7 @@ public BaseREPL2(IREPLService replService, Terminal term) { // - support for html results // - measure time // - history? + // - possible to tee output } public void run() throws IOException { diff --git a/src/org/rascalmpl/repl/completers/RascalCommandCompletion.java b/src/org/rascalmpl/repl/completers/RascalCommandCompletion.java index ac4e77ba27a..ab7cb49db44 100644 --- a/src/org/rascalmpl/repl/completers/RascalCommandCompletion.java +++ b/src/org/rascalmpl/repl/completers/RascalCommandCompletion.java @@ -35,7 +35,7 @@ public class RascalCommandCompletion implements Completer { COMMAND_KEYWORDS.put("history", "history"); // TODO: figure out what it does COMMAND_KEYWORDS.put("test", "run rest modules"); COMMAND_KEYWORDS.put("modules", "show imported modules");// TODO: figure out what it does - COMMAND_KEYWORDS.put("clear", "clear evaluator");// TODO: figure out what it does + COMMAND_KEYWORDS.put("clear", "clear repl screen"); } private final NavigableMap setOptions; diff --git a/src/org/rascalmpl/semantics/dynamic/ShellCommand.java b/src/org/rascalmpl/semantics/dynamic/ShellCommand.java index b57e5efd2f7..c9d316c8f1e 100644 --- a/src/org/rascalmpl/semantics/dynamic/ShellCommand.java +++ b/src/org/rascalmpl/semantics/dynamic/ShellCommand.java @@ -66,7 +66,16 @@ public Clear(ISourceLocation __param1, IConstructor tree) { @Override public Result interpret(IEvaluator> __eval) { - return null; + IRascalMonitor monitor = __eval.getMonitor(); + + if (monitor instanceof IDEServices) { + IDEServices services = (IDEServices) monitor; + services.clearRepl(); + } + else { + __eval.getStdErr().println("The current Rascal execution environment does not know how to clear the REPL."); + } + return org.rascalmpl.interpreter.result.ResultFactory.nothing(); } } diff --git a/src/org/rascalmpl/shell/RascalShell2.java b/src/org/rascalmpl/shell/RascalShell2.java index a273ac431c3..d19e9c77f90 100644 --- a/src/org/rascalmpl/shell/RascalShell2.java +++ b/src/org/rascalmpl/shell/RascalShell2.java @@ -19,6 +19,7 @@ import org.jline.terminal.TerminalBuilder; import org.jline.utils.OSUtils; +import org.jline.utils.InfoCmp.Capability; import org.rascalmpl.ideservices.BasicIDEServices; import org.rascalmpl.ideservices.IDEServices; import org.rascalmpl.interpreter.Evaluator; @@ -55,13 +56,13 @@ public static void main(String[] args) throws IOException { var repl = new BaseREPL2(new RascalReplServices((t) -> { var monitor = new TerminalProgressBarMonitor(term); - IDEServices services = new BasicIDEServices(term.writer(), monitor); + IDEServices services = new BasicIDEServices(term.writer(), monitor, () -> term.puts(Capability.clear_screen)); GlobalEnvironment heap = new GlobalEnvironment(); ModuleEnvironment root = heap.addModule(new ModuleEnvironment(ModuleEnvironment.SHELL_MODULE, heap)); IValueFactory vf = ValueFactoryFactory.getValueFactory(); - Evaluator evaluator = new Evaluator(vf, term.reader(), new PrintWriter(System.err, true), term.writer(), root, heap, monitor); + Evaluator evaluator = new Evaluator(vf, term.reader(), new PrintWriter(System.err, true), term.writer(), root, heap, services); evaluator.addRascalSearchPathContributor(StandardLibraryContributor.getInstance()); return evaluator; }), term); From 0a911aceb2c498bad8fa18e17bb389704704af34 Mon Sep 17 00:00:00 2001 From: Davy Landman Date: Tue, 17 Dec 2024 16:56:35 +0100 Subject: [PATCH 24/35] Remove old implementation of REPL --- .../DefaultTestResultListener.java | 2 +- src/org/rascalmpl/library/Prelude.java | 2 +- .../library/util/IDEServicesLibrary.java | 4 +- .../rascalmpl/library/util/Reflective.java | 4 +- src/org/rascalmpl/repl/BaseREPL.java_disabled | 451 -------------- src/org/rascalmpl/repl/BaseREPL2.java | 7 +- src/org/rascalmpl/repl/BaseRascalREPL.java | 560 ------------------ .../rascalmpl/repl/CompletionFunction.java | 5 - src/org/rascalmpl/repl/CompletionResult.java | 27 - src/org/rascalmpl/repl/ILanguageProtocol.java | 2 +- src/org/rascalmpl/repl/IOutputPrinter.java | 53 ++ src/org/rascalmpl/repl/IREPLService.java | 28 +- .../repl/NotifieableInputStream.java | 126 ---- .../rascalmpl/repl/RascalReplServices.java | 96 ++- .../repl/SourceLocationHistory.java_disabled | 68 --- .../completers/RascalCommandCompletion.java | 53 -- .../repl/{ => http}/REPLContentServer.java | 2 +- .../{ => http}/REPLContentServerManager.java | 2 +- .../repl/{ => streams}/ItalicErrorWriter.java | 2 +- .../repl/{ => streams}/LimitedLineWriter.java | 2 +- .../repl/{ => streams}/LimitedWriter.java | 2 +- .../{ => streams}/NonClosingFilterWriter.java | 4 +- .../repl/{ => streams}/RedErrorWriter.java | 2 +- .../repl/{ => streams}/ReplTextWriter.java | 2 +- .../{ => streams}/WrappedFilterWriter.java | 2 +- src/org/rascalmpl/shell/RascalShell2.java | 7 +- 26 files changed, 183 insertions(+), 1332 deletions(-) delete mode 100644 src/org/rascalmpl/repl/BaseREPL.java_disabled delete mode 100755 src/org/rascalmpl/repl/BaseRascalREPL.java delete mode 100644 src/org/rascalmpl/repl/CompletionFunction.java delete mode 100644 src/org/rascalmpl/repl/CompletionResult.java delete mode 100644 src/org/rascalmpl/repl/NotifieableInputStream.java delete mode 100644 src/org/rascalmpl/repl/SourceLocationHistory.java_disabled rename src/org/rascalmpl/repl/{ => http}/REPLContentServer.java (99%) rename src/org/rascalmpl/repl/{ => http}/REPLContentServerManager.java (99%) rename src/org/rascalmpl/repl/{ => streams}/ItalicErrorWriter.java (90%) rename src/org/rascalmpl/repl/{ => streams}/LimitedLineWriter.java (98%) rename src/org/rascalmpl/repl/{ => streams}/LimitedWriter.java (97%) rename src/org/rascalmpl/repl/{ => streams}/NonClosingFilterWriter.java (70%) rename src/org/rascalmpl/repl/{ => streams}/RedErrorWriter.java (91%) rename src/org/rascalmpl/repl/{ => streams}/ReplTextWriter.java (99%) rename src/org/rascalmpl/repl/{ => streams}/WrappedFilterWriter.java (95%) diff --git a/src/org/rascalmpl/interpreter/DefaultTestResultListener.java b/src/org/rascalmpl/interpreter/DefaultTestResultListener.java index 9d4ced0c270..af027eb9002 100644 --- a/src/org/rascalmpl/interpreter/DefaultTestResultListener.java +++ b/src/org/rascalmpl/interpreter/DefaultTestResultListener.java @@ -15,7 +15,7 @@ import java.io.PrintWriter; -import org.rascalmpl.repl.ReplTextWriter; +import org.rascalmpl.repl.streams.ReplTextWriter; import io.usethesource.vallang.ISourceLocation; diff --git a/src/org/rascalmpl/library/Prelude.java b/src/org/rascalmpl/library/Prelude.java index 61bcf1889c4..24ec1db3ede 100644 --- a/src/org/rascalmpl/library/Prelude.java +++ b/src/org/rascalmpl/library/Prelude.java @@ -78,7 +78,7 @@ import org.rascalmpl.exceptions.Throw; import org.rascalmpl.ideservices.IDEServices; import org.rascalmpl.interpreter.utils.IResourceLocationProvider; -import org.rascalmpl.repl.LimitedLineWriter; +import org.rascalmpl.repl.streams.LimitedLineWriter; import org.rascalmpl.types.TypeReifier; import org.rascalmpl.unicode.UnicodeOffsetLengthReader; import org.rascalmpl.unicode.UnicodeOutputStreamWriter; diff --git a/src/org/rascalmpl/library/util/IDEServicesLibrary.java b/src/org/rascalmpl/library/util/IDEServicesLibrary.java index 5476c5d2509..3798e7d0e0a 100644 --- a/src/org/rascalmpl/library/util/IDEServicesLibrary.java +++ b/src/org/rascalmpl/library/util/IDEServicesLibrary.java @@ -16,8 +16,8 @@ import org.rascalmpl.exceptions.RuntimeExceptionFactory; import org.rascalmpl.ideservices.IDEServices; -import org.rascalmpl.repl.REPLContentServer; -import org.rascalmpl.repl.REPLContentServerManager; +import org.rascalmpl.repl.http.REPLContentServer; +import org.rascalmpl.repl.http.REPLContentServerManager; import org.rascalmpl.uri.URIUtil; import org.rascalmpl.values.IRascalValueFactory; import org.rascalmpl.values.functions.IFunction; diff --git a/src/org/rascalmpl/library/util/Reflective.java b/src/org/rascalmpl/library/util/Reflective.java index 371d60474bc..dba61d268b8 100644 --- a/src/org/rascalmpl/library/util/Reflective.java +++ b/src/org/rascalmpl/library/util/Reflective.java @@ -43,8 +43,8 @@ import org.rascalmpl.parser.gtd.result.out.DefaultNodeFlattener; import org.rascalmpl.parser.uptr.UPTRNodeFactory; import org.rascalmpl.parser.uptr.action.NoActionExecutor; -import org.rascalmpl.repl.LimitedLineWriter; -import org.rascalmpl.repl.LimitedWriter; +import org.rascalmpl.repl.streams.LimitedLineWriter; +import org.rascalmpl.repl.streams.LimitedWriter; import org.rascalmpl.uri.URIResolverRegistry; import org.rascalmpl.values.RascalValueFactory; import org.rascalmpl.values.ValueFactoryFactory; diff --git a/src/org/rascalmpl/repl/BaseREPL.java_disabled b/src/org/rascalmpl/repl/BaseREPL.java_disabled deleted file mode 100644 index 0f9e66f64d6..00000000000 --- a/src/org/rascalmpl/repl/BaseREPL.java_disabled +++ /dev/null @@ -1,451 +0,0 @@ -package org.rascalmpl.repl; - -import java.io.File; -import java.io.FilterWriter; -import java.io.IOException; -import java.io.InputStream; -import java.io.InputStreamReader; -import java.io.OutputStream; -import java.io.PrintWriter; -import java.io.Reader; -import java.io.Writer; -import java.net.URISyntaxException; -import java.nio.charset.StandardCharsets; -import java.util.Arrays; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Queue; -import java.util.concurrent.ConcurrentLinkedQueue; - -import org.fusesource.jansi.Ansi; -import org.rascalmpl.ideservices.IDEServices; -import org.rascalmpl.library.util.PathConfig; - -import io.usethesource.vallang.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 class BaseREPL { - protected final ConsoleReader reader; - protected final OutputStream originalStdOut; - protected final OutputStream stderr; - protected final InputStream wrappedStream; - - protected final Writer errorWriter; - - protected final boolean prettyPrompt; - protected final boolean allowColors; - - protected volatile boolean keepRunning = true; - private volatile Task historyFlusher = null; - private volatile PersistentHistory history = null; - private final Queue commandQueue = new ConcurrentLinkedQueue(); - protected IDEServices ideServices; - private final ILanguageProtocol language; - private final Terminal underlyingTerminal; - - private static byte CANCEL_RUNNING_COMMAND = (byte)ctrl('C'); - private static byte STOP_REPL = (byte)ctrl('D'); - private static byte STACK_TRACE = (byte)ctrl('\\'); - - - public BaseREPL(ILanguageProtocol language, PathConfig pcfg, InputStream stdin, OutputStream stderr, OutputStream stdout, boolean prettyPrompt, boolean allowColors, File file, Terminal terminal, IDEServices ideServices) throws IOException, URISyntaxException { - this(language, pcfg, stdin, stderr, stdout, prettyPrompt, allowColors, file != null ? new FileHistory(file) : null, terminal, ideServices); - } - - public BaseREPL(ILanguageProtocol language, PathConfig pcfg, InputStream stdin, OutputStream stderr, OutputStream stdout, boolean prettyPrompt, boolean allowColors, ISourceLocation file, Terminal terminal, IDEServices ideServices) throws IOException, URISyntaxException { - this(language, pcfg, stdin, stderr, stdout, prettyPrompt, allowColors, file != null ? new SourceLocationHistory(file) : null, terminal, ideServices); - } - - private BaseREPL(ILanguageProtocol language, PathConfig pcfg, InputStream stdin, OutputStream stderr, OutputStream stdout, boolean prettyPrompt, boolean allowColors, PersistentHistory history, Terminal terminal, IDEServices ideServices) throws IOException, URISyntaxException { - this.originalStdOut = stdout; - this.stderr = stderr; - this.language = language; - - if (!(stdin instanceof NotifieableInputStream) && !(stdin.getClass().getCanonicalName().contains("jline"))) { - if (stdin == System.in) { - // before we wrap it with our own early detection of ctrl+c and friends, we need to see if jline has some specific - // wrappings that we need to do first - stdin = terminal.wrapInIfNeeded(stdin); - } - stdin = new NotifieableInputStream(stdin, new byte[] { CANCEL_RUNNING_COMMAND, STOP_REPL, STACK_TRACE }, this::handleEscape); - wrappedStream = null; - } - else { - wrappedStream = null; - } - 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 != null && terminal.isAnsiSupported(); - this.prettyPrompt = prettyPrompt; - this.allowColors = allowColors; - if (prettyPrompt && allowColors) { - this.errorWriter = new RedErrorWriter(reader.getOutput()); - } - else if (prettyPrompt) { - this.errorWriter = new ItalicErrorWriter(reader.getOutput()); - } - else { - this.errorWriter = new FilterWriter(reader.getOutput()) { }; // create a basic wrapper to avoid locking on stdout and stderr - } - initialize(stdin, terminal.wrapOutIfNeeded(stdout) /*JURGEN LET OP reader.getOutput()*/, terminal.wrapOutIfNeeded(stderr), ideServices); - if (supportsCompletion()) { - reader.addCompleter(new Completer(){ - @Override - public int complete(String buffer, int cursor, List 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); - underlyingTerminal = terminal; - } - - - public static char ctrl(char ch) { - assert 'A' <= ch && ch <= '_'; - return (char)((((int)ch) - 'A') + 1); - } - - - /** - * 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 void initialize(InputStream input, OutputStream stdout, OutputStream stderr, IDEServices services) throws IOException, URISyntaxException { - language.initialize(input, stdout, stderr, services); - } - - /** - * Will be called everytime a new prompt is printed. - * @return The string representing the prompt. - */ - protected String getPrompt() { - return language.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 void handleInput(String line) throws InterruptedException { - Map output = new HashMap<>(); - - underlyingTerminal.disableInterruptCharacter(); - try { - language.handleInput(line, output, new HashMap<>()); - } - finally { - underlyingTerminal.enableInterruptCharacter(); - } - - // TODO: maybe we can do this cleaner, but this works for now - InputStream out = output.get("text/plain"); - - if (out != null) { - copyToReader(out, reader); - } - } - - private static void copyToReader(InputStream source, ConsoleReader target) { - try { - try (Reader reader = new InputStreamReader(source, StandardCharsets.UTF_8)) { - char[] chunk = new char[8*1024]; - int read; - while ((read = reader.read(chunk, 0, chunk.length)) != -1) { - target.print(new String(chunk, 0, read)); - } - } - catch (IOException e) { - target.print("Error printing: " + e); - } - target.flush(); - } - catch (IOException e) { - } - } - - - /** - * 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 void handleReset(Map output, Map metadata) throws InterruptedException { - language.handleReset(output, metadata); - } - - /** - * Test if completion of statement in the current line is supported - * @return true if the completeFragment method can provide completions - */ - protected boolean supportsCompletion() { - return language.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 boolean printSpaceAfterFullCompletion() { - return language.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 CompletionResult completeFragment(String line, int cursor) { - return language.completeFragment(line, 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. - * @throws InterruptedException - */ - protected void cancelRunningCommandRequested() { - language.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. - * @throws InterruptedException - */ - protected void terminateRequested() { - language.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 void stackTraceRequested() { - language.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 != null && !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: - *
    - *
  • handleInput throws an InteruptedException. - *
  • input reaches the end of the stream - *
  • either the input or output stream throws an IOException - *
- */ - public void run() throws IOException { - try { - updatePrompt(); - while(keepRunning) { - - handleCommandQueue(); - - 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(); - reader.flush(); - handleReset(new HashMap<>(), new HashMap<>()); - updatePrompt(); - } - - } - } - catch (InterruptedException e) { - // we are closing down, so do nothing, the finally clause will take care of it - } - catch (Throwable e) { - PrintWriter err = new PrintWriter(errorWriter, true); - - if (!err.checkError()) { - err.println("Unexpected (uncaught) exception, closing the REPL: "); - err.print(e.toString()); - e.printStackTrace(err); - } - else { - System.err.println("Unexpected (uncaught) exception, closing the REPL: "); - System.err.print(e.toString()); - e.printStackTrace(System.err); - } - - err.flush(); - - throw e; - } - finally { - reader.flush(); - originalStdOut.flush(); - if (historyFlusher != null) { - ShutdownHooks.remove(historyFlusher); - history.flush(); - } - } - } - - @Override - protected void finalize() throws Throwable { - // We have to wait until finalize instead of the finally block in the run method - // because jline requires time to reset the terminal using PTY commands over - // the same stream. By delaying the close operation until the garbage collector - // kicks in, jline gets the opportunity to do that. - // BTW, closing the wrappedStream is essential for fixing a memory leak that - // would hold on to entire Evaluator instances after the REPL was closed. - if (wrappedStream != null) { - try { - wrappedStream.close(); - } - catch (IOException e) { - } - } - reader.close(); - } - - private void handleCommandQueue() throws IOException, InterruptedException { - boolean handledQueue = false; - String queuedCommand; - while ((queuedCommand = commandQueue.poll()) != null) { - handledQueue = true; - reader.resetPromptLine(reader.getPrompt(), queuedCommand, 0); - reader.println(); - reader.flush(); - reader.getHistory().add(queuedCommand); - try { - handlingInput = true; - handleInput(queuedCommand); - } - finally { - handlingInput = false; - } - } - if (handledQueue) { - String oldPrompt = reader.getPrompt(); - reader.resetPromptLine("", "", 0); - reader.setPrompt(oldPrompt); - reader.flush(); - } - } - - /** - * stop the REPL without waiting for it to stop - */ - public void stop() { - language.stop(); - keepRunning = false; - reader.close(); - } - - public Terminal getTerminal() { - return reader.getTerminal(); - } - - public InputStream getInput() { - return reader.getInput(); - } -} diff --git a/src/org/rascalmpl/repl/BaseREPL2.java b/src/org/rascalmpl/repl/BaseREPL2.java index aa4f9a36912..b95bd20cacb 100644 --- a/src/org/rascalmpl/repl/BaseREPL2.java +++ b/src/org/rascalmpl/repl/BaseREPL2.java @@ -19,7 +19,6 @@ import org.jline.terminal.Terminal.Signal; import org.jline.terminal.Terminal.SignalHandler; import org.jline.utils.ShutdownHooks; -import org.jline.utils.InfoCmp.Capability; public class BaseREPL2 { @@ -78,11 +77,9 @@ public BaseREPL2(IREPLService replService, Terminal term) { this.unicodeSupported = term.encoding().newEncoder().canEncode("💓"); this.currentPrompt = replService.prompt(ansiSupported, unicodeSupported); reader.variable(LineReader.SECONDARY_PROMPT_PATTERN, replService.parseErrorPrompt(ansiSupported, unicodeSupported)); - this.reader = reader.build(); - // todo: // - ctrl + / support (might not be possible) // - highlighting in the prompt? (future work, as it also hurts other parts) @@ -90,7 +87,9 @@ public BaseREPL2(IREPLService replService, Terminal term) { // - support for html results // - measure time // - history? - // - possible to tee output + // - possible to tee output (future work) + // - check if the REPL close properly closes the right streams + // - fix progress bar speed & proper recovering from intermediate prints } public void run() throws IOException { diff --git a/src/org/rascalmpl/repl/BaseRascalREPL.java b/src/org/rascalmpl/repl/BaseRascalREPL.java deleted file mode 100755 index 14caabb1e5d..00000000000 --- a/src/org/rascalmpl/repl/BaseRascalREPL.java +++ /dev/null @@ -1,560 +0,0 @@ -package org.rascalmpl.repl; - -import java.io.ByteArrayInputStream; -import java.io.IOException; -import java.io.InputStream; -import java.io.PrintWriter; -import java.io.Reader; -import java.io.StringWriter; -import java.io.Writer; -import java.net.URI; -import java.net.URISyntaxException; -import java.nio.charset.StandardCharsets; -import java.util.Collection; -import java.util.HashSet; -import java.util.Map; -import java.util.Set; -import java.util.SortedSet; -import java.util.TreeSet; -import java.util.function.Function; -import java.util.regex.Matcher; -import java.util.regex.Pattern; -import java.util.stream.Collectors; - -import org.rascalmpl.interpreter.asserts.Ambiguous; -import org.rascalmpl.interpreter.result.IRascalResult; -import org.rascalmpl.interpreter.utils.ReadEvalPrintDialogMessages; -import org.rascalmpl.interpreter.utils.StringUtils; -import org.rascalmpl.interpreter.utils.StringUtils.OffsetLengthTerm; -import org.rascalmpl.repl.completers.RascalCommandCompletion; -import org.rascalmpl.uri.URIResolverRegistry; -import org.rascalmpl.uri.URIUtil; -import org.rascalmpl.values.RascalValueFactory; -import org.rascalmpl.values.ValueFactoryFactory; -import org.rascalmpl.values.functions.IFunction; -import org.rascalmpl.values.parsetrees.TreeAdapter; - -import io.usethesource.vallang.IConstructor; -import io.usethesource.vallang.ISourceLocation; -import io.usethesource.vallang.IString; -import io.usethesource.vallang.IValue; -import io.usethesource.vallang.IValueFactory; -import io.usethesource.vallang.IWithKeywordParameters; -import io.usethesource.vallang.io.StandardTextWriter; -import io.usethesource.vallang.type.Type; - -public abstract class BaseRascalREPL implements ILanguageProtocol { - - protected enum State { - FRESH, - CONTINUATION, - DEBUG, - DEBUG_CONTINUATION - } - - private State currentState = State.FRESH; - - protected State getState() { - return currentState; - } - - private final static int LINE_LIMIT = 200; - private final static int CHAR_LIMIT = LINE_LIMIT * 20; - protected String currentPrompt = ReadEvalPrintDialogMessages.PROMPT; - private StringBuffer currentCommand; - protected final StandardTextWriter indentedPrettyPrinter; - private final boolean allowColors; - private final static IValueFactory VF = ValueFactoryFactory.getValueFactory(); - protected final REPLContentServerManager contentManager = new REPLContentServerManager(); - - public BaseRascalREPL(boolean prettyPrompt, boolean allowColors) throws IOException, URISyntaxException { - this.allowColors = allowColors; - - if (allowColors) { - indentedPrettyPrinter = new ReplTextWriter(true); - } - else { - indentedPrettyPrinter = new StandardTextWriter(true); - } - } - - @Override - public String getPrompt() { - return currentPrompt; - } - - private InputStream stringStream(String x) { - return new ByteArrayInputStream(x.getBytes(StandardCharsets.UTF_8)); - } - - - @Override - public void handleInput(String line, Map output, Map metadata) throws InterruptedException { - assert line != null; - - try { - if (line.trim().length() == 0) { - // cancel command - getErrorWriter().println(ReadEvalPrintDialogMessages.CANCELLED); - currentPrompt = ReadEvalPrintDialogMessages.PROMPT; - currentCommand = null; - currentState = State.FRESH; - return; - } - if (currentCommand == null) { - // we are still at a new command so let's see if the line is a full command - if (isStatementComplete(line)) { - - printResult(evalStatement(line, line), output, metadata); - } - else { - currentCommand = new StringBuffer(line); - currentPrompt = ReadEvalPrintDialogMessages.CONTINUE_PROMPT; - currentState = State.CONTINUATION; - return; - } - } - else { - currentCommand.append('\n'); - currentCommand.append(line); - if (isStatementComplete(currentCommand.toString())) { - printResult(evalStatement(currentCommand.toString(), line), output, metadata); - currentPrompt = ReadEvalPrintDialogMessages.PROMPT; - currentCommand = null; - currentState = State.FRESH; - return; - } - } - } catch (IOException ie) { - throw new RuntimeException(ie); - } catch (Ambiguous e) { - getErrorWriter().println("Internal error: ambiguous command: " + TreeAdapter.yield(e.getTree())); - return; - } - finally { - getOutputWriter().flush(); - } - } - - @Override - public void handleReset(Map output, Map metadata) throws InterruptedException { - handleInput("", output, metadata); - } - - @FunctionalInterface - public interface IOConsumer { - void accept(T t) throws IOException; - } - - private static interface OutputWriter { - void writeOutput(Type tp , IOConsumer contentsWriter) throws IOException; - void finishOutput(); - } - - protected void printResult(IRascalResult result, Map output, Map metadata) throws IOException { - if (result == null || result.getValue() == null) { - output.put("text/plain", stringStream("ok\n")); - return; - } - else if (result.getStaticType().isSubtypeOf(RascalValueFactory.Content) && !result.getStaticType().isBottom()) { - // we have interactive output in HTML form to serve - serveContent(result, output, metadata); - return; - } - else { - // otherwise we have simple output to print on the REPL in either text/html or text/plain format: - - final StringWriter out = new StringWriter(); - OutputWriter writer = new OutputWriter() { - @Override - public void writeOutput(Type tp, IOConsumer contentsWriter) throws IOException { - out.write(tp.toString()); - out.write(": "); - contentsWriter.accept(out); - } - - @Override - public void finishOutput() { - out.write('\n'); - } - }; - - writeOutput(result, writer); - - if (out.getBuffer().length() == 0) { - output.put("text/plain", stringStream("ok\n")); - } - else { - output.put("text/plain", stringStream(out.toString())); - } - } - } - - private void serveContent(IRascalResult result, Map output, Map metadata) - throws IOException { - IConstructor provider = (IConstructor) result.getValue(); - String id; - Function target; - - if (provider.has("id")) { - id = ((IString) provider.get("id")).getValue(); - target = liftProviderFunction((IFunction) provider.get("callback")); - } - else { - id = "*static content*"; - target = (r) -> provider.get("response"); - } - - // this installs the provider such that subsequent requests are handled. - REPLContentServer server = contentManager.addServer(id, target); - - // now we need some HTML to show - String URL = "http://localhost:" + server.getListeningPort() + "/"; - - IWithKeywordParameters kp = provider.asWithKeywordParameters(); - - metadata.put("url", URL); - metadata.put("title", kp.hasParameter("title") ? ((IString) kp.getParameter("title")).getValue() : id); - metadata.put("viewColumn", kp.hasParameter("viewColumn") ? kp.getParameter("title").toString() : "1"); - - output.put("text/plain", stringStream("Serving \'" + id + "\' at |" + URL + "|\n")); - output.put("text/html", stringStream("")); - } - - abstract protected Function liftProviderFunction(IFunction callback); - - private void writeOutput(IRascalResult result, OutputWriter target) throws IOException { - IValue value = result.getValue(); - Type type = result.getStaticType(); - - if (type.isAbstractData() && type.isStrictSubtypeOf(RascalValueFactory.Tree) && !type.isBottom()) { - target.writeOutput(type, (StringWriter w) -> { - w.write("(" + type.toString() +") `"); - TreeAdapter.yield((IConstructor)result.getValue(), allowColors, w); - w.write("`"); - }); - } - else if (type.isString()) { - target.writeOutput(type, (StringWriter w) -> { - try (Writer wrt = new LimitedWriter(new LimitedLineWriter(w, LINE_LIMIT), CHAR_LIMIT)) { - indentedPrettyPrinter.write(value, wrt); - } - catch (/*IOLimitReachedException*/ RuntimeException e) { - // ignore since this is what we wanted - // if we catch IOLimitReachedException we get an IllegalArgument exception instead - // "Self-suppression not permitted" - } - w.write("\n---\n"); - try (Writer wrt = new LimitedWriter(new LimitedLineWriter(w, LINE_LIMIT), CHAR_LIMIT)) { - ((IString) value).write(wrt); - } - catch (/*IOLimitReachedException*/ RuntimeException e) { - // ignore since this is what we wanted - // if we catch IOLimitReachedException we get an IllegalArgument exception instead - // "Self-suppression not permitted" - } - w.write("\n---"); - }); - } - else { - target.writeOutput(type, (StringWriter w) -> { - // limit both the lines and the characters - try (Writer wrt = new LimitedWriter(new LimitedLineWriter(w, LINE_LIMIT), CHAR_LIMIT)) { - indentedPrettyPrinter.write(value, wrt); - } - catch (/*IOLimitReachedException*/ RuntimeException e) { - // ignore since this is what we wanted - // if we catch IOLimitReachedException we get an IllegalArgument exception instead - // "Self-suppression not permitted" - } - }); - } - target.finishOutput(); - } - - public abstract PrintWriter getErrorWriter(); - public abstract PrintWriter getOutputWriter(); - public abstract InputStream getInput(); - - public abstract IRascalResult evalStatement(String statement, String lastLine) throws InterruptedException; - - /** - * provide which :set flags (:set profiling true for example) - * @return strings that can be set - */ - protected abstract SortedSet getCommandLineOptions(); - protected abstract Collection completePartialIdentifier(String line, int cursor, String qualifier, String identifier); - protected abstract Collection completeModule(String qualifier, String partialModuleName); - - protected boolean isREPLCommand(String line){ - return line.startsWith(":"); - } - - - private static String prefixTrim(String input) { - int sLength = input.length(); - int offset = 0; - while (offset < sLength && input.charAt(offset) == ' ') { - offset++; - } - return input.substring(offset); - } - - @Override - public CompletionResult completeFragment(String line, int cursor) { - if (currentState == State.FRESH) { - String trimmedLine = prefixTrim(line); - if (isREPLCommand(trimmedLine)) { - return completeREPLCommand(line, cursor); - } - if (trimmedLine.startsWith("import ") || trimmedLine.startsWith("extend ")) { - return completeModule(line, cursor); - } - } - int locationStart = StringUtils.findRascalLocationStart(line, cursor); - - if (locationStart != -1) { - return completeLocation(line, locationStart); - } - return completeIdentifier(line, cursor); - } - - protected CompletionResult completeIdentifier(String line, int cursor) { - OffsetLengthTerm identifier = StringUtils.findRascalIdentifierAtOffset(line, cursor); - if (identifier != null) { - String[] qualified = StringUtils.splitQualifiedName(unescapeKeywords(identifier.term)); - String qualifier = qualified.length == 2 ? qualified[0] : ""; - String qualifee = qualified.length == 2 ? qualified[1] : qualified[0]; - Collection suggestions = completePartialIdentifier(line, cursor, qualifier, qualifee); - if (suggestions != null && ! suggestions.isEmpty()) { - return new CompletionResult(identifier.offset, escapeKeywords(suggestions)); - } - } - return null; - } - - - private static final Pattern splitIdentifiers = Pattern.compile("[:][:]"); - private static Collection escapeKeywords(Collection suggestions) { - return suggestions.stream() - .map(s -> splitIdentifiers.splitAsStream(s + " ") // add space such that the ending "::" is not lost - .map(BaseRascalREPL::escapeKeyword) - .collect(Collectors.joining("::")).trim() - ) - .collect(Collectors.toList()); - } - private static String unescapeKeywords(String term) { - return splitIdentifiers.splitAsStream(term + " ") // add space such that the ending "::" is not lost - .map(BaseRascalREPL::unescapeKeyword) - .collect(Collectors.joining("::")).trim() - ; - } - - private static final Set RASCAL_KEYWORDS = new HashSet(); - - private static void assureKeywordsAreScrapped() { - if (RASCAL_KEYWORDS.isEmpty()) { - synchronized (RASCAL_KEYWORDS) { - if (!RASCAL_KEYWORDS.isEmpty()) { - return; - } - - String rascalGrammar = ""; - try (Reader grammarReader = URIResolverRegistry.getInstance().getCharacterReader(ValueFactoryFactory.getValueFactory().sourceLocation("std", "", "/lang/rascal/syntax/Rascal.rsc"))) { - StringBuilder res = new StringBuilder(); - char[] chunk = new char[8 * 1024]; - int read; - while ((read = grammarReader.read(chunk, 0, chunk.length)) != -1) { - res.append(chunk, 0, read); - } - rascalGrammar = res.toString(); - } - catch (IOException | URISyntaxException e) { - e.printStackTrace(); - } - if (!rascalGrammar.isEmpty()) { - /* - * keyword RascalKeywords - * = "o" - * | "syntax" - * | "keyword" - * | "lexical" - * ... - * ; - */ - Pattern findKeywordSection = Pattern.compile("^\\s*keyword([^=]|\\s)*=(?([^;]|\\s)*);", Pattern.MULTILINE); - Matcher m = findKeywordSection.matcher(rascalGrammar); - if (m.find()) { - String keywords = "|" + m.group("keywords"); - Pattern keywordEntry = Pattern.compile("\\s*[|]\\s*[\"](?[^\"]*)[\"]"); - m = keywordEntry.matcher(keywords); - while (m.find()) { - RASCAL_KEYWORDS.add(m.group("keyword")); - } - } - /* - * syntax BasicType - = \value: "value" - | \loc: "loc" - | \node: "node" - */ - Pattern findBasicTypeSection = Pattern.compile("^\\s*syntax\\s*BasicType([^=]|\\s)*=(?([^;]|\\s)*);", Pattern.MULTILINE); - m = findBasicTypeSection.matcher(rascalGrammar); - if (m.find()) { - String keywords = "|" + m.group("keywords"); - Pattern keywordEntry = Pattern.compile("\\s*[|][^:]*:\\s*[\"](?[^\"]*)[\"]"); - m = keywordEntry.matcher(keywords); - while (m.find()) { - RASCAL_KEYWORDS.add(m.group("keyword")); - } - } - } - if (RASCAL_KEYWORDS.isEmpty()) { - RASCAL_KEYWORDS.add("syntax"); - } - } - } - } - - private static String escapeKeyword(String s) { - assureKeywordsAreScrapped(); - if (RASCAL_KEYWORDS.contains(s)) { - return "\\" + s; - } - return s; - } - private static String unescapeKeyword(String s) { - assureKeywordsAreScrapped(); - if (s.startsWith("\\") && !s.contains("-")) { - return s.substring(1); - } - return s; - } - - - @Override - public boolean supportsCompletion() { - return true; - } - - @Override - public boolean printSpaceAfterFullCompletion() { - return false; - } - - private CompletionResult completeLocation(String line, int locationStart) { - int locationEnd = StringUtils.findRascalLocationEnd(line, locationStart); - try { - String locCandidate = line.substring(locationStart + 1, locationEnd + 1); - if (!locCandidate.contains("://")) { - // only complete scheme - return completeSchema(locCandidate, locationStart + 1); - } - - CompletionResult authorityResult = completeAuthorities(locCandidate, locationStart + 1); - if (authorityResult != null) { - return authorityResult; - } - - - ISourceLocation directory = VF.sourceLocation(new URI(locCandidate)); - String fileName = ""; - URIResolverRegistry reg = URIResolverRegistry.getInstance(); - if (!reg.isDirectory(directory)) { - // split filename and directory - String fullPath = directory.getPath(); - int lastSeparator = fullPath.lastIndexOf('/'); - fileName = fullPath.substring(lastSeparator + 1); - fullPath = fullPath.substring(0, lastSeparator); - directory = VF.sourceLocation(directory.getScheme(), directory.getAuthority(), fullPath); - if (!reg.isDirectory(directory)) { - return null; - } - } - String[] filesInPath = reg.listEntries(directory); - Set result = new TreeSet<>(); // sort it up - for (String currentFile : filesInPath) { - if (currentFile.startsWith(fileName)) { - ISourceLocation currentDir = URIUtil.getChildLocation(directory, currentFile); - boolean isDirectory = reg.isDirectory(currentDir); - result.add(currentDir.getURI().toString() + (isDirectory ? "/" : "|")); - } - } - if (!result.isEmpty()) { - return new CompletionResult(locationStart + 1, result); - } - return null; - } - catch (URISyntaxException|IOException e) { - return null; - } - } - - private CompletionResult completeAuthorities(String locCandidate, int locationStart) throws URISyntaxException, - IOException { - int lastSeparator = locCandidate.lastIndexOf('/'); - if (lastSeparator > 3 && locCandidate.substring(lastSeparator - 2, lastSeparator + 1).equals("://")) { - URIResolverRegistry reg = URIResolverRegistry.getInstance(); - // special case, we want to complete authorities (but URI's without a authority are not valid) - String scheme = locCandidate.substring(0, lastSeparator - 2); - String partialAuthority = locCandidate.substring(lastSeparator + 1); - ISourceLocation root = VF.sourceLocation(scheme, "", ""); - Set result = new TreeSet<>(); - for (String candidate: reg.listEntries(root)) { - if (candidate.startsWith(partialAuthority)) { - result.add(scheme + "://" + candidate); - } - } - if (!result.isEmpty()) { - return new CompletionResult(locationStart, result); - } - } - return null; - } - - private CompletionResult completeSchema(String locCandidate, int start) { - URIResolverRegistry reg = URIResolverRegistry.getInstance(); - Set result = new TreeSet<>(); - filterCandidates(reg.getRegisteredInputSchemes(), result, locCandidate); - filterCandidates(reg.getRegisteredLogicalSchemes(), result, locCandidate); - filterCandidates(reg.getRegisteredOutputSchemes(), result, locCandidate); - if (result.isEmpty()) { - return null; - } - return new CompletionResult(start, result); - } - - private static void filterCandidates(Set src, Set target, String prefix) { - for (String s : src) { - if (s.startsWith(prefix)) { - target.add(s); - } - } - } - - public CompletionResult completeModule(String line, int cursor) { - if (line.trim().equals("import")) { - // special case of an import without any partial module name - Collection suggestions = completeModule("", ""); - if (suggestions != null && ! suggestions.isEmpty()) { - return new CompletionResult(line.length(), escapeKeywords(suggestions)); - } - return null; - } - OffsetLengthTerm identifier = StringUtils.findRascalIdentifierAtOffset(line, line.length()); - if (identifier != null) { - String[] qualified = StringUtils.splitQualifiedName(unescapeKeywords(identifier.term)); - String qualifier = qualified.length == 2 ? qualified[0] : ""; - String qualifee = qualified.length == 2 ? qualified[1] : qualified[0]; - Collection suggestions = completeModule(qualifier, qualifee); - if (suggestions != null && ! suggestions.isEmpty()) { - return new CompletionResult(identifier.offset, escapeKeywords(suggestions)); - } - } - return null; - } - - protected CompletionResult completeREPLCommand(String line, int cursor) { - return RascalCommandCompletion.complete(line, cursor, getCommandLineOptions(), (l,i) -> completeIdentifier(l,i), (l,i) -> completeModule(l,i)); - } -} diff --git a/src/org/rascalmpl/repl/CompletionFunction.java b/src/org/rascalmpl/repl/CompletionFunction.java deleted file mode 100644 index a5407536989..00000000000 --- a/src/org/rascalmpl/repl/CompletionFunction.java +++ /dev/null @@ -1,5 +0,0 @@ -package org.rascalmpl.repl; - -public interface CompletionFunction { - CompletionResult complete(String line, int cursor); -} diff --git a/src/org/rascalmpl/repl/CompletionResult.java b/src/org/rascalmpl/repl/CompletionResult.java deleted file mode 100644 index 4f1ea45b899..00000000000 --- a/src/org/rascalmpl/repl/CompletionResult.java +++ /dev/null @@ -1,27 +0,0 @@ -package org.rascalmpl.repl; - -import java.util.Collection; - -public class CompletionResult { - private final int offset; - private final Collection suggestions; - - public CompletionResult(int offset, Collection suggestions) { - this.offset = offset; - this.suggestions = suggestions; - } - public int getOffset() { - return offset; - } - public Collection getSuggestions() { - return suggestions; - } - - public CompletionResult joinWith(CompletionResult other){ - if(offset != other.offset){ - throw new RuntimeException("Cannot join CompletionResults with different offset"); - } - suggestions.addAll(other.getSuggestions()); - return new CompletionResult(offset, suggestions); - } -} diff --git a/src/org/rascalmpl/repl/ILanguageProtocol.java b/src/org/rascalmpl/repl/ILanguageProtocol.java index 9da826d0389..a85cc6eb084 100755 --- a/src/org/rascalmpl/repl/ILanguageProtocol.java +++ b/src/org/rascalmpl/repl/ILanguageProtocol.java @@ -79,7 +79,7 @@ public interface ILanguageProtocol { * @param cursor The cursor offset in the line. * @return suggestions for the line. */ - CompletionResult completeFragment(String line, int cursor); + void/*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. diff --git a/src/org/rascalmpl/repl/IOutputPrinter.java b/src/org/rascalmpl/repl/IOutputPrinter.java index 6b055f852fb..80b207058fb 100644 --- a/src/org/rascalmpl/repl/IOutputPrinter.java +++ b/src/org/rascalmpl/repl/IOutputPrinter.java @@ -1,14 +1,35 @@ package org.rascalmpl.repl; import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; import java.io.PrintWriter; import java.io.Reader; import java.io.StringReader; import java.io.StringWriter; +import java.io.ByteArrayOutputStream; +import java.io.ByteArrayInputStream; + +/** + * The output of a REPL command is represented by this class, depending on the consumer (terminal/notebook/webserver) a different function might be called. + */ public interface IOutputPrinter { + /** + * Write the output on this print writer interface. + * In case {@linkplain #isBinary()} returns true, it might not be called, but please write a message why binary output should have been used. + * @param target where to write the output to. + */ void write(PrintWriter target); + + /** + * Offer the same output as {@linkplain #write(PrintWriter)} but as a pull based reader. + * The standard implementation takes care to just call the write function with a buffer. + * If you however can provide a streaming reading, override this function instead, depending + * on the consumer, it might be called instead of the write function. + * @return a reader that produces the same contents as the write function, but in a pull style instead of push. + */ default Reader asReader() { try (var result = new StringWriter()) { try (var resultWriter = new PrintWriter(result)) { @@ -20,4 +41,36 @@ default Reader asReader() { throw new IllegalStateException("StringWriter close should never throw exception", ex); } } + + /** + * Some renders support binary output (such as images), if this function returns true, they'll call + * either {@linkplain #write(OutputStream)} or {@linkplain #asInputStream()} instead. + */ + default boolean isBinary() { + return false; + } + + /** + * Write bytes to a stream, it will only be called is {@linkplain #isBinary()} returns true, the renderer supports it, and the renderer opens a dedicated stream per resource. + * @throws IOException function on `OutputStream` can cause IOExceptions + */ + + default void write(OutputStream target) throws IOException { + throw new RuntimeException("Write to output stream only supported in case of binary output (such as images)"); + } + + + /** + * Produce bytes that represent the output of a stream, in a streaming/pull style. Will only be called if {@linkplain #isBinary()} is true, the renderer supports it, and the renderer prefers an inputstream to copy from. + * @return an streaming representation of the bytes that makeup the output of the command + */ + default InputStream asInputStream() { + try (var result = new ByteArrayOutputStream()) { + write(result); + return new ByteArrayInputStream(result.toByteArray()); + } + catch (IOException ex) { + throw new IllegalStateException("Write or Close should not have throw an exception", ex); + } + } } diff --git a/src/org/rascalmpl/repl/IREPLService.java b/src/org/rascalmpl/repl/IREPLService.java index 5be73ad6c5e..d12261c7ed2 100644 --- a/src/org/rascalmpl/repl/IREPLService.java +++ b/src/org/rascalmpl/repl/IREPLService.java @@ -15,6 +15,10 @@ public interface IREPLService { String MIME_PLAIN = "text/plain"; String MIME_ANSI = "text/x-ansi"; + String MIME_HTML = "text/html"; + String MIME_PNG = "image/png"; + String MIME_JPEG = "image/jpeg"; + String MIME_SVG = "image/svg+xml"; /** * Does this language support completion @@ -40,6 +44,10 @@ default Parser inputParser() { } + /** + * Should the history of the REPL be stored + * @return + */ default boolean storeHistory() { return false; } @@ -58,17 +66,12 @@ default Path historyFile() { default String name() { return "Rascal REPL"; } - /** - * Check if an input is valid, for multi-line support - */ - boolean isInputComplete(String input); - - // todo see if we really need the meta-data void handleInput(String input, Map output, Map metadata) throws InterruptedException; /** * Will be called from a different thread then the one that called `handleInput` + * Should try to stop the running command */ void handleInterrupt() throws InterruptedException; @@ -76,14 +79,27 @@ default Path historyFile() { * Default prompt */ String prompt(boolean ansiSupported, boolean unicodeSupported); + /** * Continuation prompt */ String parseErrorPrompt(boolean ansiSupported, boolean unicodeSupported); + /** + * Connect the REPL to the Terminal, most likely want to take a copy of at least the {@link Terminal#writer()}. + * @param term + */ void connect(Terminal term); + /** + * if a REPL service has wrapped the writer for error output, return that instance + * @return + */ PrintWriter errorWriter(); + /** + * if a REPL service has wrapped the writer for regular output, return that instance + * @return + */ PrintWriter outputWriter(); /** diff --git a/src/org/rascalmpl/repl/NotifieableInputStream.java b/src/org/rascalmpl/repl/NotifieableInputStream.java deleted file mode 100644 index 5378f34032f..00000000000 --- a/src/org/rascalmpl/repl/NotifieableInputStream.java +++ /dev/null @@ -1,126 +0,0 @@ -package org.rascalmpl.repl; - -import java.io.IOException; -import java.io.InputStream; -import java.util.concurrent.ArrayBlockingQueue; -import java.util.concurrent.BlockingQueue; -import java.util.concurrent.TimeUnit; -import java.util.function.Function; - -public class NotifieableInputStream extends InputStream { - private final BlockingQueue queue; - private volatile boolean closed; - private volatile IOException toThrow; - private final Thread reader; - private final InputStream peekAt; - - /** - * scan for certain bytes in the stream, and if they are found, call the callback function to see if - * it has to be swallowed. - */ - public NotifieableInputStream(final InputStream peekAt, final byte[] watchFor, - final Function swallow) { - this.queue = new ArrayBlockingQueue<>(8 * 1024); - this.closed = false; - this.toThrow = null; - this.peekAt = peekAt; - this.reader = new Thread(() -> { - try { - reading: while (!closed) { - int b = peekAt.read(); - if (b == -1) { - NotifieableInputStream.this.close(); - return; - } - for (byte c : watchFor) { - if (b == c) { - if (swallow.apply((byte) b)) { - continue reading; - } - break; - } - } - queue.put((byte) b); - } - } - catch (IOException e2) { - if (!e2.getMessage().contains("closed")) { - toThrow = e2; - try { - NotifieableInputStream.this.close(); - } - catch (IOException e1) { - } - } - } - catch (InterruptedException e3) { - Thread.currentThread().interrupt(); - } - }); - reader.setName("InputStream scanner"); - reader.setDaemon(true); - reader.start(); - } - - @Override - public int read(byte[] b) throws IOException { - return read(b, 0, b.length); - } - - @Override - public int read(byte[] b, int off, int len) throws IOException { - if (b == null) { - throw new NullPointerException(); - } - else if ((off < 0) || (off > b.length) || (len < 0) || ((off + len) > b.length) || ((off + len) < 0)) { - throw new IndexOutOfBoundsException(); - } - else if (len == 0) { - return 0; - } - // we have to at least read one (so block until we can) - int atLeastOne = read(); - if (atLeastOne == -1) { - return -1; - } - int index = off; - b[index++] = (byte) atLeastOne; - - // now consume the rest of the available bytes - Byte current; - while ((current = queue.poll()) != null && (index < off + len)) { - b[index++] = current; - } - return index - off; - } - - @Override - public int read() throws IOException { - Byte result; - try { - while ((result = queue.poll(100, TimeUnit.MILLISECONDS)) == null) { - if (closed) { - return -1; - } - if (toThrow != null) { - IOException throwCopy = toThrow; - toThrow = null; - if (throwCopy != null) { - throw throwCopy; - } - } - } - } - catch (InterruptedException e) { - Thread.currentThread().interrupt(); - return -1; - } - return (result & 0xFF); - } - - @Override - public void close() throws IOException { - closed = true; - peekAt.close(); - } -} diff --git a/src/org/rascalmpl/repl/RascalReplServices.java b/src/org/rascalmpl/repl/RascalReplServices.java index c45f320a140..0df0e666f45 100644 --- a/src/org/rascalmpl/repl/RascalReplServices.java +++ b/src/org/rascalmpl/repl/RascalReplServices.java @@ -36,24 +36,34 @@ import org.rascalmpl.parser.gtd.exception.ParseError; import org.rascalmpl.repl.completers.RascalCommandCompletion; import org.rascalmpl.repl.completers.RascalIdentifierCompletion; -import org.rascalmpl.repl.completers.RascalModuleCompletion; import org.rascalmpl.repl.completers.RascalKeywordCompletion; import org.rascalmpl.repl.completers.RascalLocationCompletion; +import org.rascalmpl.repl.completers.RascalModuleCompletion; +import org.rascalmpl.repl.http.REPLContentServer; +import org.rascalmpl.repl.http.REPLContentServerManager; import org.rascalmpl.repl.jline3.RascalLineParser; +import org.rascalmpl.repl.streams.ItalicErrorWriter; +import org.rascalmpl.repl.streams.LimitedLineWriter; +import org.rascalmpl.repl.streams.LimitedWriter; +import org.rascalmpl.repl.streams.RedErrorWriter; +import org.rascalmpl.repl.streams.ReplTextWriter; import org.rascalmpl.uri.URIUtil; import org.rascalmpl.values.RascalValueFactory; +import org.rascalmpl.values.functions.IFunction; import org.rascalmpl.values.parsetrees.TreeAdapter; import io.usethesource.vallang.IConstructor; import io.usethesource.vallang.ISourceLocation; import io.usethesource.vallang.IString; import io.usethesource.vallang.IValue; +import io.usethesource.vallang.IWithKeywordParameters; import io.usethesource.vallang.io.StandardTextWriter; import io.usethesource.vallang.type.Type; public class RascalReplServices implements IREPLService { private final Function buildEvaluator; private Evaluator eval; + private final REPLContentServerManager contentManager = new REPLContentServerManager(); private static final StandardTextWriter ansiIndentedPrinter = new ReplTextWriter(true); private static final StandardTextWriter plainIndentedPrinter = new StandardTextWriter(true); private final static int LINE_LIMIT = 200; @@ -61,6 +71,7 @@ public class RascalReplServices implements IREPLService { private String newline = System.lineSeparator(); + public RascalReplServices(Function buildEvaluator) { super(); this.buildEvaluator = buildEvaluator; @@ -78,6 +89,31 @@ public void connect(Terminal term) { this.eval = buildEvaluator.apply(term); } + public static PrintWriter generateErrorStream(Terminal tm, Writer out) { + // previously we would alway write errors to System.err, but that tends to mess up terminals + // and also our own error print + // so now we try to not write to System.err + if (supportsColors(tm)) { + return new PrintWriter(new RedErrorWriter(out), true); + } + if (supportsItalic(tm)) { + return new PrintWriter(new ItalicErrorWriter(out), true); + } + return new PrintWriter(System.err, true); + + } + + private static boolean supportsColors(Terminal tm) { + Integer cols = tm.getNumericCapability(Capability.max_colors); + return cols != null && cols >= 8; + } + + private static boolean supportsItalic(Terminal tm) { + String ital = tm.getStringCapability(Capability.enter_italics_mode); + return ital != null && !ital.equals(""); + } + + private static final ISourceLocation PROMPT_LOCATION = URIUtil.rootLocation("prompt"); @Override @@ -89,11 +125,6 @@ public Parser inputParser() { }); } - @Override - public boolean isInputComplete(String input) { - throw new UnsupportedOperationException("Unimplemented method 'isInputComplete'"); - } - @Override public void handleInput(String input, Map output, Map metadata) @@ -103,7 +134,7 @@ public void handleInput(String input, Map output, Map value; value = eval.eval(eval.getMonitor(), input, URIUtil.rootLocation("prompt")); - outputResult(output, value); + outputResult(output, value, metadata); } catch (InterruptException ex) { reportError(output, (w, sw) -> { @@ -140,7 +171,7 @@ public void handleInput(String input, Map output, Map output, IRascalResult result) { + private void outputResult(Map output, IRascalResult result, Map metadata) { if (result == null || result.getValue() == null) { output.put(MIME_PLAIN, new StringOutputPrinter("ok", newline)); return; @@ -149,8 +180,7 @@ private void outputResult(Map output, IRascalResult resu Type type = result.getStaticType(); if (type.isSubtypeOf(RascalValueFactory.Content) && !type.isBottom()) { - output.put(MIME_PLAIN, new StringOutputPrinter("Serving content", newline)); - // TODO: serve content! + serveContent(output, (IConstructor)value, metadata); return; } @@ -214,6 +244,52 @@ else if (type.isString()) { } + private Function addEvalLock(IFunction func) { + return a -> { + synchronized(eval) { + return func.call(a); + } + }; + } + + private void serveContent(Map output, IConstructor provider, Map metadata) { + String id; + Function target; + + if (provider.has("id")) { + id = ((IString) provider.get("id")).getValue(); + target = addEvalLock(((IFunction) provider.get("callback"))); + } + else { + id = "*static content*"; + target = (r) -> provider.get("response"); + } + + try { + // this installs the provider such that subsequent requests are handled. + REPLContentServer server = contentManager.addServer(id, target); + + // now we need some HTML to show + String URL = "http://localhost:" + server.getListeningPort() + "/"; + + IWithKeywordParameters kp = provider.asWithKeywordParameters(); + + metadata.put("url", URL); + metadata.put("title", kp.hasParameter("title") ? ((IString) kp.getParameter("title")).getValue() : id); + metadata.put("viewColumn", kp.hasParameter("viewColumn") ? kp.getParameter("title").toString() : "1"); + + output.put(MIME_PLAIN, new StringOutputPrinter("Serving \'" + id + "\' at |" + URL + "|", newline)); + output.put(MIME_HTML, new StringOutputPrinter("", newline)); + } + catch (IOException e) { + reportError(output, (w, sw) -> { + w.println("Could not start webserver to render html content: "); + w.println(e.getMessage()); + }); + } + + } + private static void reportError(Map output, ThrowingWriter writer) { output.put(MIME_PLAIN, new ExceptionPrinter(writer, plainIndentedPrinter)); output.put(MIME_ANSI, new ExceptionPrinter(writer, ansiIndentedPrinter)); diff --git a/src/org/rascalmpl/repl/SourceLocationHistory.java_disabled b/src/org/rascalmpl/repl/SourceLocationHistory.java_disabled deleted file mode 100644 index eb14ea04d84..00000000000 --- a/src/org/rascalmpl/repl/SourceLocationHistory.java_disabled +++ /dev/null @@ -1,68 +0,0 @@ -package org.rascalmpl.repl; - -import static jline.internal.Preconditions.checkNotNull; - -import java.io.BufferedReader; -import java.io.IOException; -import java.io.InputStream; -import java.io.InputStreamReader; -import java.io.PrintStream; -import java.io.Reader; - -import org.jline.reader.History.Entry; -import org.jline.utils.Log; -import org.rascalmpl.uri.URIResolverRegistry; - -import io.usethesource.vallang.ISourceLocation; - - -public class SourceLocationHistory extends History implements PersistentHistory { - - private static final URIResolverRegistry reg = URIResolverRegistry.getInstance(); - private final ISourceLocation loc; - - public SourceLocationHistory(ISourceLocation loc) throws IOException { - this.loc = loc; - load(loc); - } - - public void load(final ISourceLocation loc) throws IOException { - checkNotNull(loc); - if (reg.exists(loc)) { - Log.trace("Loading history from: ", loc); - load(reg.getInputStream(loc)); - } - } - - public void load(final InputStream input) throws IOException { - checkNotNull(input); - load(new InputStreamReader(input)); - } - - public void load(final Reader reader) throws IOException { - checkNotNull(reader); - BufferedReader input = new BufferedReader(reader); - - String item; - while ((item = input.readLine()) != null) { - internalAdd(item); - } - } - - public void flush() throws IOException { - Log.trace("Flushing history"); - - try (PrintStream out = new PrintStream(reg.getOutputStream(loc, false))) { - for (Entry entry : this) { - out.println(entry.value()); - } - } - } - - public void purge() throws IOException { - Log.trace("Purging history"); - clear(); - reg.remove(loc, true); - } - -} diff --git a/src/org/rascalmpl/repl/completers/RascalCommandCompletion.java b/src/org/rascalmpl/repl/completers/RascalCommandCompletion.java index ab7cb49db44..48a995f9f64 100644 --- a/src/org/rascalmpl/repl/completers/RascalCommandCompletion.java +++ b/src/org/rascalmpl/repl/completers/RascalCommandCompletion.java @@ -18,9 +18,6 @@ import org.jline.reader.ParsedLine; import org.rascalmpl.interpreter.utils.StringUtils; import org.rascalmpl.interpreter.utils.StringUtils.OffsetLengthTerm; -import org.rascalmpl.repl.CompletionFunction; -import org.rascalmpl.repl.CompletionResult; - public class RascalCommandCompletion implements Completer { private static final NavigableMap COMMAND_KEYWORDS; static { @@ -48,56 +45,6 @@ public RascalCommandCompletion(NavigableMap setOptions, BiConsum } - - private static final Pattern splitCommand = Pattern.compile("^[\\t ]*:(?[a-z]*)([\\t ]|$)"); - /**@deprecated remove this function */ - public static CompletionResult complete(String line, int cursor, SortedSet commandOptions, CompletionFunction completeIdentifier, CompletionFunction completeModule) { - assert line.trim().startsWith(":"); - Matcher m = splitCommand.matcher(line); - if (m.find()) { - String currentCommand = m.group("command"); - switch(currentCommand) { - case "set": { - OffsetLengthTerm identifier = StringUtils.findRascalIdentifierAtOffset(line, cursor); - if (identifier != null && identifier.offset > m.end("command")) { - Collection suggestions = commandOptions.stream() - .filter(s -> s.startsWith(identifier.term)) - .sorted() - .collect(Collectors.toList()); - if (suggestions != null && ! suggestions.isEmpty()) { - return new CompletionResult(identifier.offset, suggestions); - } - } - else if (line.trim().equals(":set")) { - return new CompletionResult(line.length(), commandOptions); - } - return null; - } - case "undeclare": return completeIdentifier.complete(line, cursor); - case "edit": - case "unimport": return completeModule.complete(line, line.length()); - default: { - if (COMMAND_KEYWORDS.containsKey(currentCommand)) { - return null; // nothing to complete after a full command - } - List result = null; - if (currentCommand.isEmpty()) { - result = new ArrayList<>(COMMAND_KEYWORDS.keySet()); - } - else { - result = COMMAND_KEYWORDS.keySet().stream() - .filter(s -> s.startsWith(currentCommand)) - .collect(Collectors.toList()); - } - if (!result.isEmpty()) { - return new CompletionResult(m.start("command"), result); - } - } - } - } - return null; - } - private static void generateCandidates(String partial, NavigableMap candidates, String group, List target) { for (var can : candidates.subMap(partial, true, partial + Character.MAX_VALUE, false).entrySet()) { target.add(new Candidate(can.getKey(), can.getKey(), group, can.getValue(), null, null, true)); diff --git a/src/org/rascalmpl/repl/REPLContentServer.java b/src/org/rascalmpl/repl/http/REPLContentServer.java similarity index 99% rename from src/org/rascalmpl/repl/REPLContentServer.java rename to src/org/rascalmpl/repl/http/REPLContentServer.java index de811bcb6ef..18068ba02b0 100644 --- a/src/org/rascalmpl/repl/REPLContentServer.java +++ b/src/org/rascalmpl/repl/http/REPLContentServer.java @@ -1,4 +1,4 @@ -package org.rascalmpl.repl; +package org.rascalmpl.repl.http; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; diff --git a/src/org/rascalmpl/repl/REPLContentServerManager.java b/src/org/rascalmpl/repl/http/REPLContentServerManager.java similarity index 99% rename from src/org/rascalmpl/repl/REPLContentServerManager.java rename to src/org/rascalmpl/repl/http/REPLContentServerManager.java index 2c14bb47df6..24bb5331869 100644 --- a/src/org/rascalmpl/repl/REPLContentServerManager.java +++ b/src/org/rascalmpl/repl/http/REPLContentServerManager.java @@ -10,7 +10,7 @@ * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ -package org.rascalmpl.repl; +package org.rascalmpl.repl.http; import java.io.IOException; import java.util.Map.Entry; diff --git a/src/org/rascalmpl/repl/ItalicErrorWriter.java b/src/org/rascalmpl/repl/streams/ItalicErrorWriter.java similarity index 90% rename from src/org/rascalmpl/repl/ItalicErrorWriter.java rename to src/org/rascalmpl/repl/streams/ItalicErrorWriter.java index a650a64e4ac..38c3aa61db2 100644 --- a/src/org/rascalmpl/repl/ItalicErrorWriter.java +++ b/src/org/rascalmpl/repl/streams/ItalicErrorWriter.java @@ -1,4 +1,4 @@ -package org.rascalmpl.repl; +package org.rascalmpl.repl.streams; import java.io.Writer; diff --git a/src/org/rascalmpl/repl/LimitedLineWriter.java b/src/org/rascalmpl/repl/streams/LimitedLineWriter.java similarity index 98% rename from src/org/rascalmpl/repl/LimitedLineWriter.java rename to src/org/rascalmpl/repl/streams/LimitedLineWriter.java index ed5e9a054c6..f16b80454ee 100644 --- a/src/org/rascalmpl/repl/LimitedLineWriter.java +++ b/src/org/rascalmpl/repl/streams/LimitedLineWriter.java @@ -1,4 +1,4 @@ -package org.rascalmpl.repl; +package org.rascalmpl.repl.streams; import java.io.IOException; import java.io.Writer; diff --git a/src/org/rascalmpl/repl/LimitedWriter.java b/src/org/rascalmpl/repl/streams/LimitedWriter.java similarity index 97% rename from src/org/rascalmpl/repl/LimitedWriter.java rename to src/org/rascalmpl/repl/streams/LimitedWriter.java index 9fdd6c98706..f3377baa44a 100644 --- a/src/org/rascalmpl/repl/LimitedWriter.java +++ b/src/org/rascalmpl/repl/streams/LimitedWriter.java @@ -1,4 +1,4 @@ -package org.rascalmpl.repl; +package org.rascalmpl.repl.streams; import java.io.IOException; import java.io.Writer; diff --git a/src/org/rascalmpl/repl/NonClosingFilterWriter.java b/src/org/rascalmpl/repl/streams/NonClosingFilterWriter.java similarity index 70% rename from src/org/rascalmpl/repl/NonClosingFilterWriter.java rename to src/org/rascalmpl/repl/streams/NonClosingFilterWriter.java index 8cd4b4210f8..0e44cab6772 100644 --- a/src/org/rascalmpl/repl/NonClosingFilterWriter.java +++ b/src/org/rascalmpl/repl/streams/NonClosingFilterWriter.java @@ -1,11 +1,11 @@ -package org.rascalmpl.repl; +package org.rascalmpl.repl.streams; import java.io.FilterWriter; import java.io.IOException; import java.io.Writer; -public abstract class NonClosingFilterWriter extends FilterWriter { +abstract class NonClosingFilterWriter extends FilterWriter { protected NonClosingFilterWriter(Writer out) { super(out); diff --git a/src/org/rascalmpl/repl/RedErrorWriter.java b/src/org/rascalmpl/repl/streams/RedErrorWriter.java similarity index 91% rename from src/org/rascalmpl/repl/RedErrorWriter.java rename to src/org/rascalmpl/repl/streams/RedErrorWriter.java index b2300a3787d..60cd23d16aa 100644 --- a/src/org/rascalmpl/repl/RedErrorWriter.java +++ b/src/org/rascalmpl/repl/streams/RedErrorWriter.java @@ -1,4 +1,4 @@ -package org.rascalmpl.repl; +package org.rascalmpl.repl.streams; import java.io.Writer; diff --git a/src/org/rascalmpl/repl/ReplTextWriter.java b/src/org/rascalmpl/repl/streams/ReplTextWriter.java similarity index 99% rename from src/org/rascalmpl/repl/ReplTextWriter.java rename to src/org/rascalmpl/repl/streams/ReplTextWriter.java index 647680fb2e8..b885573672e 100644 --- a/src/org/rascalmpl/repl/ReplTextWriter.java +++ b/src/org/rascalmpl/repl/streams/ReplTextWriter.java @@ -1,4 +1,4 @@ -package org.rascalmpl.repl; +package org.rascalmpl.repl.streams; import java.io.IOException; import java.io.StringWriter; diff --git a/src/org/rascalmpl/repl/WrappedFilterWriter.java b/src/org/rascalmpl/repl/streams/WrappedFilterWriter.java similarity index 95% rename from src/org/rascalmpl/repl/WrappedFilterWriter.java rename to src/org/rascalmpl/repl/streams/WrappedFilterWriter.java index b8122ab72d4..86936a2dd2a 100644 --- a/src/org/rascalmpl/repl/WrappedFilterWriter.java +++ b/src/org/rascalmpl/repl/streams/WrappedFilterWriter.java @@ -1,4 +1,4 @@ -package org.rascalmpl.repl; +package org.rascalmpl.repl.streams; diff --git a/src/org/rascalmpl/shell/RascalShell2.java b/src/org/rascalmpl/shell/RascalShell2.java index d19e9c77f90..fdb0b22cffc 100644 --- a/src/org/rascalmpl/shell/RascalShell2.java +++ b/src/org/rascalmpl/shell/RascalShell2.java @@ -14,12 +14,11 @@ package org.rascalmpl.shell; import java.io.IOException; -import java.io.PrintWriter; import java.nio.charset.StandardCharsets; import org.jline.terminal.TerminalBuilder; -import org.jline.utils.OSUtils; import org.jline.utils.InfoCmp.Capability; +import org.jline.utils.OSUtils; import org.rascalmpl.ideservices.BasicIDEServices; import org.rascalmpl.ideservices.IDEServices; import org.rascalmpl.interpreter.Evaluator; @@ -57,12 +56,10 @@ public static void main(String[] args) throws IOException { var repl = new BaseREPL2(new RascalReplServices((t) -> { var monitor = new TerminalProgressBarMonitor(term); IDEServices services = new BasicIDEServices(term.writer(), monitor, () -> term.puts(Capability.clear_screen)); - - GlobalEnvironment heap = new GlobalEnvironment(); ModuleEnvironment root = heap.addModule(new ModuleEnvironment(ModuleEnvironment.SHELL_MODULE, heap)); IValueFactory vf = ValueFactoryFactory.getValueFactory(); - Evaluator evaluator = new Evaluator(vf, term.reader(), new PrintWriter(System.err, true), term.writer(), root, heap, services); + Evaluator evaluator = new Evaluator(vf, term.reader(), RascalReplServices.generateErrorStream(t, monitor), monitor, root, heap, services); evaluator.addRascalSearchPathContributor(StandardLibraryContributor.getInstance()); return evaluator; }), term); From 43d4b6583444ff5a5cceb21610ab610b38d24e97 Mon Sep 17 00:00:00 2001 From: Davy Landman Date: Wed, 18 Dec 2024 14:47:09 +0100 Subject: [PATCH 25/35] Rewrote everything into output streams to reduce all the strings --- src/org/rascalmpl/repl/BaseREPL2.java | 51 ++--- src/org/rascalmpl/repl/IOutputPrinter.java | 76 -------- src/org/rascalmpl/repl/IREPLService.java | 17 +- .../rascalmpl/repl/RascalReplServices.java | 184 ++++++++++++------ .../repl/output/IAnsiCommandOutput.java | 5 + .../repl/output/IBinaryOutputPrinter.java | 43 ++++ .../rascalmpl/repl/output/ICommandOutput.java | 5 + .../repl/output/IErrorCommandOutput.java | 5 + .../repl/output/IHtmlCommandOutput.java | 5 + .../repl/output/IImageCommandOutput.java | 6 + .../rascalmpl/repl/output/IOutputPrinter.java | 41 ++++ .../repl/output/IWebContentOutput.java | 9 + src/org/rascalmpl/repl/output/MimeTypes.java | 11 ++ .../repl/output/impl/StringOutputPrinter.java | 37 ++++ src/org/rascalmpl/shell/RascalShell2.java | 97 +-------- 15 files changed, 323 insertions(+), 269 deletions(-) delete mode 100644 src/org/rascalmpl/repl/IOutputPrinter.java create mode 100644 src/org/rascalmpl/repl/output/IAnsiCommandOutput.java create mode 100644 src/org/rascalmpl/repl/output/IBinaryOutputPrinter.java create mode 100644 src/org/rascalmpl/repl/output/ICommandOutput.java create mode 100644 src/org/rascalmpl/repl/output/IErrorCommandOutput.java create mode 100644 src/org/rascalmpl/repl/output/IHtmlCommandOutput.java create mode 100644 src/org/rascalmpl/repl/output/IImageCommandOutput.java create mode 100644 src/org/rascalmpl/repl/output/IOutputPrinter.java create mode 100644 src/org/rascalmpl/repl/output/IWebContentOutput.java create mode 100644 src/org/rascalmpl/repl/output/MimeTypes.java create mode 100644 src/org/rascalmpl/repl/output/impl/StringOutputPrinter.java diff --git a/src/org/rascalmpl/repl/BaseREPL2.java b/src/org/rascalmpl/repl/BaseREPL2.java index b95bd20cacb..40601313998 100644 --- a/src/org/rascalmpl/repl/BaseREPL2.java +++ b/src/org/rascalmpl/repl/BaseREPL2.java @@ -19,6 +19,11 @@ import org.jline.terminal.Terminal.Signal; import org.jline.terminal.Terminal.SignalHandler; import org.jline.utils.ShutdownHooks; +import org.rascalmpl.repl.output.IAnsiCommandOutput; +import org.rascalmpl.repl.output.ICommandOutput; +import org.rascalmpl.repl.output.IErrorCommandOutput; +import org.rascalmpl.repl.output.IOutputPrinter; +import org.rascalmpl.repl.output.impl.StringOutputPrinter; public class BaseREPL2 { @@ -28,11 +33,9 @@ public class BaseREPL2 { private volatile boolean keepRunning = true; private final @MonotonicNonNull DefaultHistory history; private String currentPrompt; - private static final String FALLBACK_MIME_TYPE = "text/plain"; - private static final String ANSI_MIME_TYPE = "text/x-ansi"; - private final boolean ansiSupported; + private final boolean ansiColorsSupported; + private final boolean advancedTermFeaturesSupported; private final boolean unicodeSupported; - private final String mimeType; public BaseREPL2(IREPLService replService, Terminal term) { this.replService = replService; @@ -62,21 +65,21 @@ public BaseREPL2(IREPLService replService, Terminal term) { switch (term.getType()) { case Terminal.TYPE_DUMB: - this.ansiSupported = false; - this.mimeType = FALLBACK_MIME_TYPE; + this.ansiColorsSupported = false; + this.advancedTermFeaturesSupported = false; break; case Terminal.TYPE_DUMB_COLOR: - this.ansiSupported = false; - this.mimeType = ANSI_MIME_TYPE; + this.ansiColorsSupported = false; + this.advancedTermFeaturesSupported = true; break; default: - this.ansiSupported = true; - this.mimeType = ANSI_MIME_TYPE; + this.ansiColorsSupported = true; + this.advancedTermFeaturesSupported = true; break; } this.unicodeSupported = term.encoding().newEncoder().canEncode("💓"); - this.currentPrompt = replService.prompt(ansiSupported, unicodeSupported); - reader.variable(LineReader.SECONDARY_PROMPT_PATTERN, replService.parseErrorPrompt(ansiSupported, unicodeSupported)); + this.currentPrompt = replService.prompt(ansiColorsSupported, unicodeSupported); + reader.variable(LineReader.SECONDARY_PROMPT_PATTERN, replService.parseErrorPrompt(ansiColorsSupported, unicodeSupported)); this.reader = reader.build(); @@ -187,22 +190,24 @@ private AtomicBoolean setupInterruptHandler() { private void handleInput(String line) throws InterruptedException { - var result = new HashMap(); - var meta = new HashMap(); - replService.handleInput(line, result, meta); - writeResult(result); + writeResult(replService.handleInput(line)); } - private void writeResult(HashMap result) { - var writer = result.get(this.mimeType); - if (writer == null) { - writer = result.get(FALLBACK_MIME_TYPE); + private void writeResult(ICommandOutput result) { + PrintWriter target = replService.outputWriter(); + if (result instanceof IErrorCommandOutput) { + target = replService.errorWriter(); + result = ((IErrorCommandOutput)result).getError(); } - if (writer == null) { - replService.outputWriter().println("Ok"); + + IOutputPrinter writer; + if (ansiColorsSupported && result instanceof IAnsiCommandOutput) { + writer = ((IAnsiCommandOutput)result).asAnsi(); } else { - writer.write(replService.outputWriter()); + writer = result.asPlain(); } + + writer.write(target); } } diff --git a/src/org/rascalmpl/repl/IOutputPrinter.java b/src/org/rascalmpl/repl/IOutputPrinter.java deleted file mode 100644 index 80b207058fb..00000000000 --- a/src/org/rascalmpl/repl/IOutputPrinter.java +++ /dev/null @@ -1,76 +0,0 @@ -package org.rascalmpl.repl; - -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; -import java.io.PrintWriter; -import java.io.Reader; -import java.io.StringReader; -import java.io.StringWriter; - -import java.io.ByteArrayOutputStream; -import java.io.ByteArrayInputStream; - -/** - * The output of a REPL command is represented by this class, depending on the consumer (terminal/notebook/webserver) a different function might be called. - */ -public interface IOutputPrinter { - /** - * Write the output on this print writer interface. - * In case {@linkplain #isBinary()} returns true, it might not be called, but please write a message why binary output should have been used. - * @param target where to write the output to. - */ - void write(PrintWriter target); - - - /** - * Offer the same output as {@linkplain #write(PrintWriter)} but as a pull based reader. - * The standard implementation takes care to just call the write function with a buffer. - * If you however can provide a streaming reading, override this function instead, depending - * on the consumer, it might be called instead of the write function. - * @return a reader that produces the same contents as the write function, but in a pull style instead of push. - */ - default Reader asReader() { - try (var result = new StringWriter()) { - try (var resultWriter = new PrintWriter(result)) { - write(resultWriter); - } - return new StringReader(result.toString()); - } - catch (IOException ex) { - throw new IllegalStateException("StringWriter close should never throw exception", ex); - } - } - - /** - * Some renders support binary output (such as images), if this function returns true, they'll call - * either {@linkplain #write(OutputStream)} or {@linkplain #asInputStream()} instead. - */ - default boolean isBinary() { - return false; - } - - /** - * Write bytes to a stream, it will only be called is {@linkplain #isBinary()} returns true, the renderer supports it, and the renderer opens a dedicated stream per resource. - * @throws IOException function on `OutputStream` can cause IOExceptions - */ - - default void write(OutputStream target) throws IOException { - throw new RuntimeException("Write to output stream only supported in case of binary output (such as images)"); - } - - - /** - * Produce bytes that represent the output of a stream, in a streaming/pull style. Will only be called if {@linkplain #isBinary()} is true, the renderer supports it, and the renderer prefers an inputstream to copy from. - * @return an streaming representation of the bytes that makeup the output of the command - */ - default InputStream asInputStream() { - try (var result = new ByteArrayOutputStream()) { - write(result); - return new ByteArrayInputStream(result.toByteArray()); - } - catch (IOException ex) { - throw new IllegalStateException("Write or Close should not have throw an exception", ex); - } - } -} diff --git a/src/org/rascalmpl/repl/IREPLService.java b/src/org/rascalmpl/repl/IREPLService.java index d12261c7ed2..e7792540a5c 100644 --- a/src/org/rascalmpl/repl/IREPLService.java +++ b/src/org/rascalmpl/repl/IREPLService.java @@ -4,22 +4,14 @@ import java.nio.file.Path; import java.util.Collections; import java.util.List; -import java.util.Map; import org.jline.reader.Completer; import org.jline.reader.Parser; import org.jline.reader.impl.DefaultParser; import org.jline.terminal.Terminal; +import org.rascalmpl.repl.output.ICommandOutput; public interface IREPLService { - - String MIME_PLAIN = "text/plain"; - String MIME_ANSI = "text/x-ansi"; - String MIME_HTML = "text/html"; - String MIME_PNG = "image/png"; - String MIME_JPEG = "image/jpeg"; - String MIME_SVG = "image/svg+xml"; - /** * Does this language support completion * @return @@ -66,8 +58,7 @@ default Path historyFile() { default String name() { return "Rascal REPL"; } - // todo see if we really need the meta-data - void handleInput(String input, Map output, Map metadata) throws InterruptedException; + ICommandOutput handleInput(String input) throws InterruptedException; /** * Will be called from a different thread then the one that called `handleInput` @@ -78,12 +69,12 @@ default Path historyFile() { /** * Default prompt */ - String prompt(boolean ansiSupported, boolean unicodeSupported); + String prompt(boolean ansiColorsSupported, boolean unicodeSupported); /** * Continuation prompt */ - String parseErrorPrompt(boolean ansiSupported, boolean unicodeSupported); + String parseErrorPrompt(boolean ansiColorSupported, boolean unicodeSupported); /** * Connect the REPL to the Terminal, most likely want to take a copy of at least the {@link Terminal#writer()}. diff --git a/src/org/rascalmpl/repl/RascalReplServices.java b/src/org/rascalmpl/repl/RascalReplServices.java index 0df0e666f45..a2d036d0d60 100644 --- a/src/org/rascalmpl/repl/RascalReplServices.java +++ b/src/org/rascalmpl/repl/RascalReplServices.java @@ -7,9 +7,9 @@ import java.io.IOException; import java.io.PrintWriter; -import java.io.Reader; -import java.io.StringReader; import java.io.Writer; +import java.net.URI; +import java.net.URISyntaxException; import java.util.ArrayList; import java.util.List; import java.util.Map; @@ -42,6 +42,14 @@ import org.rascalmpl.repl.http.REPLContentServer; import org.rascalmpl.repl.http.REPLContentServerManager; import org.rascalmpl.repl.jline3.RascalLineParser; +import org.rascalmpl.repl.output.IAnsiCommandOutput; +import org.rascalmpl.repl.output.ICommandOutput; +import org.rascalmpl.repl.output.IErrorCommandOutput; +import org.rascalmpl.repl.output.IHtmlCommandOutput; +import org.rascalmpl.repl.output.IOutputPrinter; +import org.rascalmpl.repl.output.IWebContentOutput; +import org.rascalmpl.repl.output.MimeTypes; +import org.rascalmpl.repl.output.impl.StringOutputPrinter; import org.rascalmpl.repl.streams.ItalicErrorWriter; import org.rascalmpl.repl.streams.LimitedLineWriter; import org.rascalmpl.repl.streams.LimitedWriter; @@ -53,6 +61,7 @@ import org.rascalmpl.values.parsetrees.TreeAdapter; import io.usethesource.vallang.IConstructor; +import io.usethesource.vallang.IInteger; import io.usethesource.vallang.ISourceLocation; import io.usethesource.vallang.IString; import io.usethesource.vallang.IValue; @@ -125,63 +134,60 @@ public Parser inputParser() { }); } - @Override - public void handleInput(String input, Map output, Map metadata) - throws InterruptedException { + public ICommandOutput handleInput(String input) throws InterruptedException { synchronized(eval) { Objects.requireNonNull(eval, "Not initialized yet"); try { - Result value; - value = eval.eval(eval.getMonitor(), input, URIUtil.rootLocation("prompt")); - outputResult(output, value, metadata); + Result value = eval.eval(eval.getMonitor(), input, URIUtil.rootLocation("prompt")); + return outputResult(value); } catch (InterruptException ex) { - reportError(output, (w, sw) -> { + return reportError((w, sw) -> { w.println("Interrupted"); ex.getRascalStackTrace().prettyPrintedString(w, sw); }); } catch (ParseError pe) { - reportError(output, (w, sw) -> { + return reportError((w, sw) -> { parseErrorMessage(w, input, "prompt", pe, sw); }); } catch (StaticError e) { - reportError(output, (w, sw) -> { + return reportError((w, sw) -> { staticErrorMessage(w, e, sw); }); } catch (Throw e) { - reportError(output, (w, sw) -> { + return reportError((w, sw) -> { throwMessage(w,e, sw); }); } catch (QuitException q) { - reportError(output, (w, sw) -> { + throw new EndOfFileException("Quiting REPL"); + /* + return reportError((w, sw) -> { w.println("Quiting REPL"); }); - throw new EndOfFileException("Quiting REPL"); + */ } catch (Throwable e) { - reportError(output, (w, sw) -> { + return reportError((w, sw) -> { throwableMessage(w, e, eval.getStackTrace(), sw); }); } } } - private void outputResult(Map output, IRascalResult result, Map metadata) { + private ICommandOutput outputResult(IRascalResult result) { if (result == null || result.getValue() == null) { - output.put(MIME_PLAIN, new StringOutputPrinter("ok", newline)); - return; + return () -> new StringOutputPrinter("ok", newline, MimeTypes.PLAIN_TEXT); } IValue value = result.getValue(); Type type = result.getStaticType(); if (type.isSubtypeOf(RascalValueFactory.Content) && !type.isBottom()) { - serveContent(output, (IConstructor)value, metadata); - return; + return serveContent((IConstructor)value); } ThrowingWriter resultWriter; @@ -239,9 +245,25 @@ else if (type.isString()) { w.println(); }; - output.put(MIME_PLAIN, new ExceptionPrinter(typePrefixed, plainIndentedPrinter)); - output.put(MIME_ANSI, new ExceptionPrinter(typePrefixed, ansiIndentedPrinter)); + return new DoubleOutput(typePrefixed); + } + + private static class DoubleOutput implements IAnsiCommandOutput { + private ThrowingWriter writer; + + DoubleOutput(ThrowingWriter writer) { + this.writer = writer; + } + @Override + public IOutputPrinter asAnsi() { + return new ParameterizedPrinterOutput(writer, ansiIndentedPrinter, MimeTypes.ANSI); + } + + @Override + public IOutputPrinter asPlain() { + return new ParameterizedPrinterOutput(writer, plainIndentedPrinter, MimeTypes.PLAIN_TEXT); + } } private Function addEvalLock(IFunction func) { @@ -252,7 +274,7 @@ private Function addEvalLock(IFunction func) { }; } - private void serveContent(Map output, IConstructor provider, Map metadata) { + private ICommandOutput serveContent(IConstructor provider) { String id; Function target; @@ -267,32 +289,86 @@ private void serveContent(Map output, IConstructor provi try { // this installs the provider such that subsequent requests are handled. - REPLContentServer server = contentManager.addServer(id, target); + REPLContentServer server = contentManager.addServer(id, target); // now we need some HTML to show - String URL = "http://localhost:" + server.getListeningPort() + "/"; IWithKeywordParameters kp = provider.asWithKeywordParameters(); + String title = kp.hasParameter("title") ? ((IString) kp.getParameter("title")).getValue() : id; + int viewColumn = kp.hasParameter("viewColumn") ? ((IInteger)kp.getParameter("viewColumn")).intValue() : 1; + URI serverUri = new URI("http", null, "localhost", server.getListeningPort(), "/", null, null); - metadata.put("url", URL); - metadata.put("title", kp.hasParameter("title") ? ((IString) kp.getParameter("title")).getValue() : id); - metadata.put("viewColumn", kp.hasParameter("viewColumn") ? kp.getParameter("title").toString() : "1"); + return new HostedWebContentOutput(id, serverUri, title, viewColumn); - output.put(MIME_PLAIN, new StringOutputPrinter("Serving \'" + id + "\' at |" + URL + "|", newline)); - output.put(MIME_HTML, new StringOutputPrinter("", newline)); } catch (IOException e) { - reportError(output, (w, sw) -> { + return reportError((w, sw) -> { w.println("Could not start webserver to render html content: "); w.println(e.getMessage()); }); } + catch (URISyntaxException e) { + return reportError((w, sw) -> { + w.println("Could not start build the uri: "); + w.println(e.getMessage()); + }); + } + } + + private class HostedWebContentOutput implements IWebContentOutput, IHtmlCommandOutput { + private final String id; + private final URI uri; + private final String title; + private final int viewColumn; + + HostedWebContentOutput(String id, URI uri, String title, int viewColumn) { + this.id = id; + this.uri = uri; + this.title = title; + this.viewColumn = viewColumn; + } + + @Override + public IOutputPrinter asPlain() { + return new StringOutputPrinter("Serving \'" + id + "\' at |" + uri + "|", newline, MimeTypes.PLAIN_TEXT); + } + + @Override + public IOutputPrinter asHtml() { + return new StringOutputPrinter( + "", + newline, MimeTypes.HTML + ); + } + + @Override + public URI webUri() { + return uri; + } + @Override + public String webTitle() { + return title; + } + + @Override + public int webviewColumn() { + return viewColumn; + } } - private static void reportError(Map output, ThrowingWriter writer) { - output.put(MIME_PLAIN, new ExceptionPrinter(writer, plainIndentedPrinter)); - output.put(MIME_ANSI, new ExceptionPrinter(writer, ansiIndentedPrinter)); + private static IErrorCommandOutput reportError(ThrowingWriter writer) { + return new IErrorCommandOutput() { + @Override + public ICommandOutput getError() { + return new DoubleOutput(writer); + } + + @Override + public IOutputPrinter asPlain() { + return new ParameterizedPrinterOutput(writer, plainIndentedPrinter, MimeTypes.PLAIN_TEXT); + } + }; } @FunctionalInterface @@ -300,13 +376,20 @@ private static interface ThrowingWriter { void write(PrintWriter writer, StandardTextWriter prettyPrinter) throws IOException; } - private static class ExceptionPrinter implements IOutputPrinter { + private static class ParameterizedPrinterOutput implements IOutputPrinter { private final ThrowingWriter internalWriter; private final StandardTextWriter prettyPrinter; + private final String mimeType; - public ExceptionPrinter(ThrowingWriter internalWriter, StandardTextWriter prettyPrinter) { + public ParameterizedPrinterOutput(ThrowingWriter internalWriter, StandardTextWriter prettyPrinter, String mimeType) { this.internalWriter = internalWriter; this.prettyPrinter = prettyPrinter; + this.mimeType = mimeType; + } + + @Override + public String mimeType() { + return mimeType; } @Override @@ -322,43 +405,23 @@ public void write(PrintWriter target) { } } - private static class StringOutputPrinter implements IOutputPrinter { - private final String value; - private final String newline; - - public StringOutputPrinter(String value, String newline) { - this.value = value; - this.newline = newline; - } - - @Override - public void write(PrintWriter target) { - target.println(value); - } - - @Override - public Reader asReader() { - return new StringReader(value + newline); - } - } - @Override public void handleInterrupt() throws InterruptedException { eval.interrupt(); } @Override - public String prompt(boolean ansiSupported, boolean unicodeSupported) { - if (ansiSupported) { + public String prompt(boolean ansiColorsSupported, boolean unicodeSupported) { + if (ansiColorsSupported) { return Ansi.ansi().reset().bold() + "rascal>" + Ansi.ansi().reset(); } return "rascal>"; } @Override - public String parseErrorPrompt(boolean ansiSupported, boolean unicodeSupported) { + public String parseErrorPrompt(boolean ansiColorsSupported, boolean unicodeSupported) { String errorPrompt = (unicodeSupported ? "│" : "|") + "%N %P>"; - if (ansiSupported) { + if (ansiColorsSupported) { return Ansi.ansi().reset().bold() + errorPrompt + Ansi.ansi().reset(); } return errorPrompt; @@ -377,7 +440,6 @@ public PrintWriter outputWriter() { @Override public void flush() { - // TODO figure out why this function is called? eval.getStdErr().flush(); eval.getStdOut().flush(); } diff --git a/src/org/rascalmpl/repl/output/IAnsiCommandOutput.java b/src/org/rascalmpl/repl/output/IAnsiCommandOutput.java new file mode 100644 index 00000000000..e7363f320ca --- /dev/null +++ b/src/org/rascalmpl/repl/output/IAnsiCommandOutput.java @@ -0,0 +1,5 @@ +package org.rascalmpl.repl.output; + +public interface IAnsiCommandOutput extends ICommandOutput { + IOutputPrinter asAnsi(); +} diff --git a/src/org/rascalmpl/repl/output/IBinaryOutputPrinter.java b/src/org/rascalmpl/repl/output/IBinaryOutputPrinter.java new file mode 100644 index 00000000000..01b28a62cf5 --- /dev/null +++ b/src/org/rascalmpl/repl/output/IBinaryOutputPrinter.java @@ -0,0 +1,43 @@ +package org.rascalmpl.repl.output; + + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.io.ByteArrayOutputStream; +import java.io.ByteArrayInputStream; + +/** + * Sometimes, an output produced a binary (such as an image or a executable file) + * In that case, you can return a return an overload of this type, which a renderer can add support for + * + *

+ * For example an output under the image/png would most likely have regular {@link IOutputPrinter#write(java.io.PrintWriter)} that prints a message saying it's a image that can't be printed as text, while the render (if it supports it) can cast it to this interface and get the actual bytes + *

+ */ +public interface IBinaryOutputPrinter extends IOutputPrinter { + /** + * Write bytes to a stream. + * @throws IOException functions on `OutputStream` can cause IOExceptions + */ + + default void write(OutputStream target) throws IOException { + throw new RuntimeException("Write to output stream only supported in case of binary output (such as images)"); + } + + + /** + * Produce bytes that represent the output of a stream, in a streaming/pull style. Will only be called if {@linkplain #isBinary()} is true, the renderer supports it, and the renderer prefers an inputstream to copy from. + * @return an streaming representation of the bytes that makeup the output of the command + */ + default InputStream asInputStream() { + try (var result = new ByteArrayOutputStream()) { + write(result); + return new ByteArrayInputStream(result.toByteArray()); + } + catch (IOException ex) { + throw new IllegalStateException("Write or Close should not have throw an exception", ex); + } + } + +} diff --git a/src/org/rascalmpl/repl/output/ICommandOutput.java b/src/org/rascalmpl/repl/output/ICommandOutput.java new file mode 100644 index 00000000000..55faa539a1e --- /dev/null +++ b/src/org/rascalmpl/repl/output/ICommandOutput.java @@ -0,0 +1,5 @@ +package org.rascalmpl.repl.output; + +public interface ICommandOutput { + IOutputPrinter asPlain(); +} diff --git a/src/org/rascalmpl/repl/output/IErrorCommandOutput.java b/src/org/rascalmpl/repl/output/IErrorCommandOutput.java new file mode 100644 index 00000000000..e79c451a2a1 --- /dev/null +++ b/src/org/rascalmpl/repl/output/IErrorCommandOutput.java @@ -0,0 +1,5 @@ +package org.rascalmpl.repl.output; + +public interface IErrorCommandOutput extends ICommandOutput { + ICommandOutput getError(); +} diff --git a/src/org/rascalmpl/repl/output/IHtmlCommandOutput.java b/src/org/rascalmpl/repl/output/IHtmlCommandOutput.java new file mode 100644 index 00000000000..307e64ef876 --- /dev/null +++ b/src/org/rascalmpl/repl/output/IHtmlCommandOutput.java @@ -0,0 +1,5 @@ +package org.rascalmpl.repl.output; + +public interface IHtmlCommandOutput extends ICommandOutput { + IOutputPrinter asHtml(); +} diff --git a/src/org/rascalmpl/repl/output/IImageCommandOutput.java b/src/org/rascalmpl/repl/output/IImageCommandOutput.java new file mode 100644 index 00000000000..d44fd1888d3 --- /dev/null +++ b/src/org/rascalmpl/repl/output/IImageCommandOutput.java @@ -0,0 +1,6 @@ +package org.rascalmpl.repl.output; + + +public interface IImageCommandOutput extends ICommandOutput { + IBinaryOutputPrinter asImage(); +} diff --git a/src/org/rascalmpl/repl/output/IOutputPrinter.java b/src/org/rascalmpl/repl/output/IOutputPrinter.java new file mode 100644 index 00000000000..83c1b1c0c76 --- /dev/null +++ b/src/org/rascalmpl/repl/output/IOutputPrinter.java @@ -0,0 +1,41 @@ +package org.rascalmpl.repl.output; + +import java.io.IOException; +import java.io.PrintWriter; +import java.io.Reader; +import java.io.StringReader; +import java.io.StringWriter; + +/** + * The output of a REPL command is represented by this class, depending on the consumer (terminal/notebook/webserver) a different function might be called. + */ +public interface IOutputPrinter { + + /** + * Write the output on this print writer interface. It should always print something, even if it's a warning saying it cannot be printed + * @param target where to write the output to. + */ + void write(PrintWriter target); + + String mimeType(); + + /** + * Offer the same output as {@linkplain #write(PrintWriter)} but as a pull based reader. + * The standard implementation takes care to just call the write function with a buffer. + * If you however can provide a streaming reading, override this function instead, depending + * on the consumer, it might be called instead of the write function. + * @return a reader that produces the same contents as the write function, but in a pull style instead of push. + */ + default Reader asReader() { + try (var result = new StringWriter()) { + try (var resultWriter = new PrintWriter(result)) { + write(resultWriter); + } + return new StringReader(result.toString()); + } + catch (IOException ex) { + throw new IllegalStateException("StringWriter close should never throw exception", ex); + } + } + +} diff --git a/src/org/rascalmpl/repl/output/IWebContentOutput.java b/src/org/rascalmpl/repl/output/IWebContentOutput.java new file mode 100644 index 00000000000..3bc8040a11b --- /dev/null +++ b/src/org/rascalmpl/repl/output/IWebContentOutput.java @@ -0,0 +1,9 @@ +package org.rascalmpl.repl.output; + +import java.net.URI; + +public interface IWebContentOutput extends ICommandOutput { + URI webUri(); + String webTitle(); + int webviewColumn(); +} diff --git a/src/org/rascalmpl/repl/output/MimeTypes.java b/src/org/rascalmpl/repl/output/MimeTypes.java new file mode 100644 index 00000000000..fb50edf0970 --- /dev/null +++ b/src/org/rascalmpl/repl/output/MimeTypes.java @@ -0,0 +1,11 @@ +package org.rascalmpl.repl.output; + +public class MimeTypes { + public static final String PLAIN_TEXT = "text/plain"; + public static final String ANSI = "text/x-ansi"; + public static final String HTML = "text/html"; + public static final String PNG = "image/png"; + public static final String JPEG = "image/JPEG"; + public static final String SVG = "image/svg+xml"; + +} diff --git a/src/org/rascalmpl/repl/output/impl/StringOutputPrinter.java b/src/org/rascalmpl/repl/output/impl/StringOutputPrinter.java new file mode 100644 index 00000000000..d3da7b064b9 --- /dev/null +++ b/src/org/rascalmpl/repl/output/impl/StringOutputPrinter.java @@ -0,0 +1,37 @@ +package org.rascalmpl.repl.output.impl; + +import java.io.PrintWriter; +import java.io.Reader; +import java.io.StringReader; + +import org.rascalmpl.repl.output.IOutputPrinter; +import org.rascalmpl.repl.output.MimeTypes; + +public class StringOutputPrinter implements IOutputPrinter { + private final String body; + private final String newline; + + public StringOutputPrinter(String body, String newline) { + this(body, newline, MimeTypes.PLAIN_TEXT); + } + public StringOutputPrinter(String body, String newline, String mimeType) { + this.body = body; + this.newline = newline; + } + + @Override + public String mimeType() { + return mimeType(); + } + + @Override + public void write(PrintWriter target) { + target.print(body); + target.print(newline); + } + + @Override + public Reader asReader() { + return new StringReader(body + newline); + } +} diff --git a/src/org/rascalmpl/shell/RascalShell2.java b/src/org/rascalmpl/shell/RascalShell2.java index fdb0b22cffc..9ad4236e5ca 100644 --- a/src/org/rascalmpl/shell/RascalShell2.java +++ b/src/org/rascalmpl/shell/RascalShell2.java @@ -56,6 +56,7 @@ public static void main(String[] args) throws IOException { var repl = new BaseREPL2(new RascalReplServices((t) -> { var monitor = new TerminalProgressBarMonitor(term); IDEServices services = new BasicIDEServices(term.writer(), monitor, () -> term.puts(Capability.clear_screen)); + GlobalEnvironment heap = new GlobalEnvironment(); ModuleEnvironment root = heap.addModule(new ModuleEnvironment(ModuleEnvironment.SHELL_MODULE, heap)); IValueFactory vf = ValueFactoryFactory.getValueFactory(); @@ -64,101 +65,5 @@ public static void main(String[] args) throws IOException { return evaluator; }), term); repl.run(); - /* - - - //IRascalMonitor monitor = IRascalMonitor.buildConsoleMonitor(System.in, System.out, true); - var monitor = new TerminalProgressBarMonitor(term); - - // var monitor = new NullRascalMonitor() { - // @Override - // public void warning(String message, ISourceLocation src) { - // reader.printAbove("[WARN] " + message); - // } - // }; - - IDEServices services = new BasicIDEServices(term.writer(), monitor); - - - GlobalEnvironment heap = new GlobalEnvironment(); - ModuleEnvironment root = heap.addModule(new ModuleEnvironment(ModuleEnvironment.SHELL_MODULE, heap)); - IValueFactory vf = ValueFactoryFactory.getValueFactory(); - Evaluator evaluator = new Evaluator(vf, term.reader(), new PrintWriter(System.err, true), term.writer(), root, heap, monitor); - evaluator.addRascalSearchPathContributor(StandardLibraryContributor.getInstance()); - - URIResolverRegistry reg = URIResolverRegistry.getInstance(); - - var indentedPrettyPrinter = new ReplTextWriter(true); - - while (true) { - String line = reader.readLine(Ansi.ansi().reset().bold().toString() + "rascal> " + Ansi.ansi().boldOff().toString()); - try { - - Result result; - - synchronized(evaluator) { - result = evaluator.eval(monitor, line, URIUtil.rootLocation("prompt")); - evaluator.endAllJobs(); - } - - if (result.isVoid()) { - monitor.println("ok"); - } - else { - IValue value = result.getValue(); - Type type = result.getStaticType(); - - if (type.isAbstractData() && type.isStrictSubtypeOf(RascalValueFactory.Tree) && !type.isBottom()) { - monitor.write("(" + type.toString() +") `"); - TreeAdapter.yield((IConstructor)value, true, monitor); - monitor.write("`"); - } - else { - indentedPrettyPrinter.write(value, monitor); - } - monitor.println(); - } - } - catch (InterruptException ie) { - reader.printAbove("Interrupted"); - try { - ie.getRascalStackTrace().prettyPrintedString(evaluator.getStdErr(), indentedPrettyPrinter); - } - catch (IOException e) { - } - } - catch (ParseError pe) { - parseErrorMessage(evaluator.getStdErr(), line, "prompt", pe, indentedPrettyPrinter); - } - catch (StaticError e) { - staticErrorMessage(evaluator.getStdErr(),e, indentedPrettyPrinter); - } - catch (Throw e) { - throwMessage(evaluator.getStdErr(),e, indentedPrettyPrinter); - } - catch (QuitException q) { - reader.printAbove("Quiting REPL"); - break; - } - catch (Throwable e) { - throwableMessage(evaluator.getStdErr(), e, evaluator.getStackTrace(), indentedPrettyPrinter); - } - } - System.exit(0); - } - catch (Throwable e) { - System.err.println("\n\nunexpected error: " + e.getMessage()); - e.printStackTrace(System.err); - System.exit(1); - } - finally { - System.out.flush(); - System.err.flush(); - } - */ } - - - - } From d793d685a4de70427449eb28bd93cd036a5655ac Mon Sep 17 00:00:00 2001 From: Davy Landman Date: Thu, 19 Dec 2024 12:37:07 +0100 Subject: [PATCH 26/35] Working on support for the parametric repl and compiler --- .../rascalmpl/ideservices/IDEServices.java | 11 +- src/org/rascalmpl/repl/BaseREPL2.java | 2 - .../rascalmpl/repl/RascalReplServices.java | 477 ------------------ .../repl/output/impl/StringOutputPrinter.java | 14 +- .../{ => parametric}/ILanguageProtocol.java | 0 .../repl/rascal/IRascalLanguageProtocol.java | 101 ++++ .../repl/rascal/RascalInterpreterREPL.java | 179 +++++++ .../repl/rascal/RascalReplServices.java | 148 ++++++ .../repl/rascal/RascalValuePrinter.java | 297 +++++++++++ .../semantics/dynamic/ShellCommand.java | 11 +- src/org/rascalmpl/shell/RascalShell2.java | 6 +- 11 files changed, 753 insertions(+), 493 deletions(-) delete mode 100644 src/org/rascalmpl/repl/RascalReplServices.java rename src/org/rascalmpl/repl/{ => parametric}/ILanguageProtocol.java (100%) mode change 100755 => 100644 create mode 100644 src/org/rascalmpl/repl/rascal/IRascalLanguageProtocol.java create mode 100644 src/org/rascalmpl/repl/rascal/RascalInterpreterREPL.java create mode 100644 src/org/rascalmpl/repl/rascal/RascalReplServices.java create mode 100644 src/org/rascalmpl/repl/rascal/RascalValuePrinter.java diff --git a/src/org/rascalmpl/ideservices/IDEServices.java b/src/org/rascalmpl/ideservices/IDEServices.java index 10ed5c843fb..e85d06c4deb 100644 --- a/src/org/rascalmpl/ideservices/IDEServices.java +++ b/src/org/rascalmpl/ideservices/IDEServices.java @@ -15,6 +15,7 @@ import java.io.PrintWriter; import java.net.URI; +import org.jline.terminal.Terminal; import org.rascalmpl.debug.IRascalMonitor; import org.rascalmpl.uri.LogicalMapResolver; import org.rascalmpl.uri.URIResolverRegistry; @@ -73,8 +74,14 @@ default void unregisterLanguage(IConstructor language) { throw new UnsupportedOperationException("registerLanguage is not implemented in this environment."); } - default void clearRepl() { - throw new UnsupportedOperationException("Clear REPL is not supported in this IDE Service"); + /** + * Get access to the current terminal.
+ * used for features such as clearing the terminal, and starting a nested REPL.
+ * Can return null if there is no active terminal. + * @return a terminal if there is one, null otherwise. + */ + default Terminal activeTerminal() { + return null; } /** diff --git a/src/org/rascalmpl/repl/BaseREPL2.java b/src/org/rascalmpl/repl/BaseREPL2.java index 40601313998..1975fe8bbe4 100644 --- a/src/org/rascalmpl/repl/BaseREPL2.java +++ b/src/org/rascalmpl/repl/BaseREPL2.java @@ -3,7 +3,6 @@ import java.io.IOException; import java.io.PrintWriter; import java.util.Arrays; -import java.util.HashMap; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicReference; @@ -23,7 +22,6 @@ import org.rascalmpl.repl.output.ICommandOutput; import org.rascalmpl.repl.output.IErrorCommandOutput; import org.rascalmpl.repl.output.IOutputPrinter; -import org.rascalmpl.repl.output.impl.StringOutputPrinter; public class BaseREPL2 { diff --git a/src/org/rascalmpl/repl/RascalReplServices.java b/src/org/rascalmpl/repl/RascalReplServices.java deleted file mode 100644 index a2d036d0d60..00000000000 --- a/src/org/rascalmpl/repl/RascalReplServices.java +++ /dev/null @@ -1,477 +0,0 @@ -package org.rascalmpl.repl; - -import static org.rascalmpl.interpreter.utils.ReadEvalPrintDialogMessages.parseErrorMessage; -import static org.rascalmpl.interpreter.utils.ReadEvalPrintDialogMessages.staticErrorMessage; -import static org.rascalmpl.interpreter.utils.ReadEvalPrintDialogMessages.throwMessage; -import static org.rascalmpl.interpreter.utils.ReadEvalPrintDialogMessages.throwableMessage; - -import java.io.IOException; -import java.io.PrintWriter; -import java.io.Writer; -import java.net.URI; -import java.net.URISyntaxException; -import java.util.ArrayList; -import java.util.List; -import java.util.Map; -import java.util.NavigableMap; -import java.util.Objects; -import java.util.TreeMap; -import java.util.function.Function; - -import org.jline.jansi.Ansi; -import org.jline.reader.Completer; -import org.jline.reader.EndOfFileException; -import org.jline.reader.Parser; -import org.jline.terminal.Terminal; -import org.jline.utils.InfoCmp.Capability; -import org.rascalmpl.exceptions.Throw; -import org.rascalmpl.interpreter.Configuration; -import org.rascalmpl.interpreter.Evaluator; -import org.rascalmpl.interpreter.NullRascalMonitor; -import org.rascalmpl.interpreter.control_exceptions.InterruptException; -import org.rascalmpl.interpreter.control_exceptions.QuitException; -import org.rascalmpl.interpreter.result.IRascalResult; -import org.rascalmpl.interpreter.result.Result; -import org.rascalmpl.interpreter.staticErrors.StaticError; -import org.rascalmpl.parser.gtd.exception.ParseError; -import org.rascalmpl.repl.completers.RascalCommandCompletion; -import org.rascalmpl.repl.completers.RascalIdentifierCompletion; -import org.rascalmpl.repl.completers.RascalKeywordCompletion; -import org.rascalmpl.repl.completers.RascalLocationCompletion; -import org.rascalmpl.repl.completers.RascalModuleCompletion; -import org.rascalmpl.repl.http.REPLContentServer; -import org.rascalmpl.repl.http.REPLContentServerManager; -import org.rascalmpl.repl.jline3.RascalLineParser; -import org.rascalmpl.repl.output.IAnsiCommandOutput; -import org.rascalmpl.repl.output.ICommandOutput; -import org.rascalmpl.repl.output.IErrorCommandOutput; -import org.rascalmpl.repl.output.IHtmlCommandOutput; -import org.rascalmpl.repl.output.IOutputPrinter; -import org.rascalmpl.repl.output.IWebContentOutput; -import org.rascalmpl.repl.output.MimeTypes; -import org.rascalmpl.repl.output.impl.StringOutputPrinter; -import org.rascalmpl.repl.streams.ItalicErrorWriter; -import org.rascalmpl.repl.streams.LimitedLineWriter; -import org.rascalmpl.repl.streams.LimitedWriter; -import org.rascalmpl.repl.streams.RedErrorWriter; -import org.rascalmpl.repl.streams.ReplTextWriter; -import org.rascalmpl.uri.URIUtil; -import org.rascalmpl.values.RascalValueFactory; -import org.rascalmpl.values.functions.IFunction; -import org.rascalmpl.values.parsetrees.TreeAdapter; - -import io.usethesource.vallang.IConstructor; -import io.usethesource.vallang.IInteger; -import io.usethesource.vallang.ISourceLocation; -import io.usethesource.vallang.IString; -import io.usethesource.vallang.IValue; -import io.usethesource.vallang.IWithKeywordParameters; -import io.usethesource.vallang.io.StandardTextWriter; -import io.usethesource.vallang.type.Type; - -public class RascalReplServices implements IREPLService { - private final Function buildEvaluator; - private Evaluator eval; - private final REPLContentServerManager contentManager = new REPLContentServerManager(); - private static final StandardTextWriter ansiIndentedPrinter = new ReplTextWriter(true); - private static final StandardTextWriter plainIndentedPrinter = new StandardTextWriter(true); - private final static int LINE_LIMIT = 200; - private final static int CHAR_LIMIT = LINE_LIMIT * 20; - private String newline = System.lineSeparator(); - - - - public RascalReplServices(Function buildEvaluator) { - super(); - this.buildEvaluator = buildEvaluator; - } - - @Override - public void connect(Terminal term) { - if (eval != null) { - throw new IllegalStateException("REPL is already initialized"); - } - newline = term.getStringCapability(Capability.newline); - if (newline == null) { - newline = System.lineSeparator(); - } - this.eval = buildEvaluator.apply(term); - } - - public static PrintWriter generateErrorStream(Terminal tm, Writer out) { - // previously we would alway write errors to System.err, but that tends to mess up terminals - // and also our own error print - // so now we try to not write to System.err - if (supportsColors(tm)) { - return new PrintWriter(new RedErrorWriter(out), true); - } - if (supportsItalic(tm)) { - return new PrintWriter(new ItalicErrorWriter(out), true); - } - return new PrintWriter(System.err, true); - - } - - private static boolean supportsColors(Terminal tm) { - Integer cols = tm.getNumericCapability(Capability.max_colors); - return cols != null && cols >= 8; - } - - private static boolean supportsItalic(Terminal tm) { - String ital = tm.getStringCapability(Capability.enter_italics_mode); - return ital != null && !ital.equals(""); - } - - - private static final ISourceLocation PROMPT_LOCATION = URIUtil.rootLocation("prompt"); - - @Override - public Parser inputParser() { - return new RascalLineParser(prompt -> { - synchronized(eval) { - return eval.parseCommand(new NullRascalMonitor(), prompt, PROMPT_LOCATION); - } - }); - } - - @Override - public ICommandOutput handleInput(String input) throws InterruptedException { - synchronized(eval) { - Objects.requireNonNull(eval, "Not initialized yet"); - try { - Result value = eval.eval(eval.getMonitor(), input, URIUtil.rootLocation("prompt")); - return outputResult(value); - } - catch (InterruptException ex) { - return reportError((w, sw) -> { - w.println("Interrupted"); - ex.getRascalStackTrace().prettyPrintedString(w, sw); - }); - } - catch (ParseError pe) { - return reportError((w, sw) -> { - parseErrorMessage(w, input, "prompt", pe, sw); - }); - } - catch (StaticError e) { - return reportError((w, sw) -> { - staticErrorMessage(w, e, sw); - }); - } - catch (Throw e) { - return reportError((w, sw) -> { - throwMessage(w,e, sw); - }); - } - catch (QuitException q) { - throw new EndOfFileException("Quiting REPL"); - /* - return reportError((w, sw) -> { - w.println("Quiting REPL"); - }); - */ - } - catch (Throwable e) { - return reportError((w, sw) -> { - throwableMessage(w, e, eval.getStackTrace(), sw); - }); - } - } - } - - private ICommandOutput outputResult(IRascalResult result) { - if (result == null || result.getValue() == null) { - return () -> new StringOutputPrinter("ok", newline, MimeTypes.PLAIN_TEXT); - } - IValue value = result.getValue(); - Type type = result.getStaticType(); - - if (type.isSubtypeOf(RascalValueFactory.Content) && !type.isBottom()) { - return serveContent((IConstructor)value); - } - - ThrowingWriter resultWriter; - if (type.isAbstractData() && type.isStrictSubtypeOf(RascalValueFactory.Tree) && !type.isBottom()) { - resultWriter = (w, sw) -> { - w.write("(" + type.toString() +") `"); - TreeAdapter.yield((IConstructor)value, sw == ansiIndentedPrinter, w); - w.write("`"); - }; - } - else if (type.isString()) { - resultWriter = (w, sw) -> { - // TODO: do something special for the reader version of IString, when that is released - // for now, we only support write - - try (Writer wrt = new LimitedWriter(new LimitedLineWriter(w, LINE_LIMIT), CHAR_LIMIT)) { - sw.write(value, wrt); - } - catch (/*IOLimitReachedException*/ RuntimeException e) { - // ignore since this is what we wanted - // if we catch IOLimitReachedException we get an IllegalArgument exception instead - // "Self-suppression not permitted" - } - w.println(); - w.println("---"); - try (Writer wrt = new LimitedWriter(new LimitedLineWriter(w, LINE_LIMIT), CHAR_LIMIT)) { - ((IString) value).write(wrt); - } - catch (/*IOLimitReachedException*/ RuntimeException e) { - // ignore since this is what we wanted - // if we catch IOLimitReachedException we get an IllegalArgument exception instead - // "Self-suppression not permitted" - } - w.println(); - w.print("---"); - }; - } - else { - resultWriter = (w, sw) -> { - try (Writer wrt = new LimitedWriter(new LimitedLineWriter(w, LINE_LIMIT), CHAR_LIMIT)) { - sw.write(value, wrt); - } - catch (/*IOLimitReachedException*/ RuntimeException e) { - // ignore since this is what we wanted - // if we catch IOLimitReachedException we get an IllegalArgument exception instead - // "Self-suppression not permitted" - } - }; - } - - ThrowingWriter typePrefixed = (w, sw) -> { - w.write(type.toString()); - w.write(": "); - resultWriter.write(w, sw); - w.println(); - }; - - return new DoubleOutput(typePrefixed); - } - - private static class DoubleOutput implements IAnsiCommandOutput { - private ThrowingWriter writer; - - DoubleOutput(ThrowingWriter writer) { - this.writer = writer; - } - - @Override - public IOutputPrinter asAnsi() { - return new ParameterizedPrinterOutput(writer, ansiIndentedPrinter, MimeTypes.ANSI); - } - - @Override - public IOutputPrinter asPlain() { - return new ParameterizedPrinterOutput(writer, plainIndentedPrinter, MimeTypes.PLAIN_TEXT); - } - } - - private Function addEvalLock(IFunction func) { - return a -> { - synchronized(eval) { - return func.call(a); - } - }; - } - - private ICommandOutput serveContent(IConstructor provider) { - String id; - Function target; - - if (provider.has("id")) { - id = ((IString) provider.get("id")).getValue(); - target = addEvalLock(((IFunction) provider.get("callback"))); - } - else { - id = "*static content*"; - target = (r) -> provider.get("response"); - } - - try { - // this installs the provider such that subsequent requests are handled. - REPLContentServer server = contentManager.addServer(id, target); - - // now we need some HTML to show - - IWithKeywordParameters kp = provider.asWithKeywordParameters(); - String title = kp.hasParameter("title") ? ((IString) kp.getParameter("title")).getValue() : id; - int viewColumn = kp.hasParameter("viewColumn") ? ((IInteger)kp.getParameter("viewColumn")).intValue() : 1; - URI serverUri = new URI("http", null, "localhost", server.getListeningPort(), "/", null, null); - - return new HostedWebContentOutput(id, serverUri, title, viewColumn); - - } - catch (IOException e) { - return reportError((w, sw) -> { - w.println("Could not start webserver to render html content: "); - w.println(e.getMessage()); - }); - } - catch (URISyntaxException e) { - return reportError((w, sw) -> { - w.println("Could not start build the uri: "); - w.println(e.getMessage()); - }); - } - } - - private class HostedWebContentOutput implements IWebContentOutput, IHtmlCommandOutput { - private final String id; - private final URI uri; - private final String title; - private final int viewColumn; - - HostedWebContentOutput(String id, URI uri, String title, int viewColumn) { - this.id = id; - this.uri = uri; - this.title = title; - this.viewColumn = viewColumn; - } - - @Override - public IOutputPrinter asPlain() { - return new StringOutputPrinter("Serving \'" + id + "\' at |" + uri + "|", newline, MimeTypes.PLAIN_TEXT); - } - - @Override - public IOutputPrinter asHtml() { - return new StringOutputPrinter( - "", - newline, MimeTypes.HTML - ); - } - - @Override - public URI webUri() { - return uri; - } - - @Override - public String webTitle() { - return title; - } - - @Override - public int webviewColumn() { - return viewColumn; - } - } - - private static IErrorCommandOutput reportError(ThrowingWriter writer) { - return new IErrorCommandOutput() { - @Override - public ICommandOutput getError() { - return new DoubleOutput(writer); - } - - @Override - public IOutputPrinter asPlain() { - return new ParameterizedPrinterOutput(writer, plainIndentedPrinter, MimeTypes.PLAIN_TEXT); - } - }; - } - - @FunctionalInterface - private static interface ThrowingWriter { - void write(PrintWriter writer, StandardTextWriter prettyPrinter) throws IOException; - } - - private static class ParameterizedPrinterOutput implements IOutputPrinter { - private final ThrowingWriter internalWriter; - private final StandardTextWriter prettyPrinter; - private final String mimeType; - - public ParameterizedPrinterOutput(ThrowingWriter internalWriter, StandardTextWriter prettyPrinter, String mimeType) { - this.internalWriter = internalWriter; - this.prettyPrinter = prettyPrinter; - this.mimeType = mimeType; - } - - @Override - public String mimeType() { - return mimeType; - } - - @Override - public void write(PrintWriter target) { - try { - internalWriter.write(target, prettyPrinter); - } - catch (IOException e) { - target.println("Internal failure: printing exception failed with:"); - target.println(e.toString()); - e.printStackTrace(target); - } - } - } - - @Override - public void handleInterrupt() throws InterruptedException { - eval.interrupt(); - } - - @Override - public String prompt(boolean ansiColorsSupported, boolean unicodeSupported) { - if (ansiColorsSupported) { - return Ansi.ansi().reset().bold() + "rascal>" + Ansi.ansi().reset(); - } - return "rascal>"; - } - - @Override - public String parseErrorPrompt(boolean ansiColorsSupported, boolean unicodeSupported) { - String errorPrompt = (unicodeSupported ? "│" : "|") + "%N %P>"; - if (ansiColorsSupported) { - return Ansi.ansi().reset().bold() + errorPrompt + Ansi.ansi().reset(); - } - return errorPrompt; - } - - - @Override - public PrintWriter errorWriter() { - return eval.getStdErr(); - } - - @Override - public PrintWriter outputWriter() { - return eval.getStdOut(); - } - - @Override - public void flush() { - eval.getStdErr().flush(); - eval.getStdOut().flush(); - } - - private static final NavigableMap commandLineOptions = new TreeMap<>(); - static { - commandLineOptions.put(Configuration.GENERATOR_PROFILING_PROPERTY.substring("rascal.".length()), "enable sampling profiler for generator"); - commandLineOptions.put(Configuration.PROFILING_PROPERTY.substring("rascal.".length()), "enable sampling profiler" ); - commandLineOptions.put(Configuration.ERRORS_PROPERTY.substring("rascal.".length()), "print raw java errors"); - commandLineOptions.put(Configuration.TRACING_PROPERTY.substring("rascal.".length()), "trace all function calls (warning: a lot of output will be generated)"); - } - - @Override - public boolean supportsCompletion() { - return true; - } - - @Override - public List completers() { - var result = new ArrayList(); - var moduleCompleter = new RascalModuleCompletion(m -> eval.getRascalResolver().listModuleEntries(m)); - var idCompleter = new RascalIdentifierCompletion((q, i) -> eval.completePartialIdentifier(q, i)); - result.add(new RascalCommandCompletion( - commandLineOptions, - idCompleter::completePartialIdentifier, - (s, c) -> moduleCompleter.completeModuleNames(s, c, false) - )); - result.add(moduleCompleter); - result.add(idCompleter); - result.add(new RascalKeywordCompletion()); - result.add(new RascalLocationCompletion()); - return result; - } - -} diff --git a/src/org/rascalmpl/repl/output/impl/StringOutputPrinter.java b/src/org/rascalmpl/repl/output/impl/StringOutputPrinter.java index d3da7b064b9..5d856462d8d 100644 --- a/src/org/rascalmpl/repl/output/impl/StringOutputPrinter.java +++ b/src/org/rascalmpl/repl/output/impl/StringOutputPrinter.java @@ -9,14 +9,13 @@ public class StringOutputPrinter implements IOutputPrinter { private final String body; - private final String newline; + - public StringOutputPrinter(String body, String newline) { - this(body, newline, MimeTypes.PLAIN_TEXT); + public StringOutputPrinter(String body) { + this(body, MimeTypes.PLAIN_TEXT); } - public StringOutputPrinter(String body, String newline, String mimeType) { + public StringOutputPrinter(String body, String mimeType) { this.body = body; - this.newline = newline; } @Override @@ -26,12 +25,11 @@ public String mimeType() { @Override public void write(PrintWriter target) { - target.print(body); - target.print(newline); + target.println(body); } @Override public Reader asReader() { - return new StringReader(body + newline); + return new StringReader(body + System.lineSeparator()); } } diff --git a/src/org/rascalmpl/repl/ILanguageProtocol.java b/src/org/rascalmpl/repl/parametric/ILanguageProtocol.java old mode 100755 new mode 100644 similarity index 100% rename from src/org/rascalmpl/repl/ILanguageProtocol.java rename to src/org/rascalmpl/repl/parametric/ILanguageProtocol.java diff --git a/src/org/rascalmpl/repl/rascal/IRascalLanguageProtocol.java b/src/org/rascalmpl/repl/rascal/IRascalLanguageProtocol.java new file mode 100644 index 00000000000..fc8c1609bef --- /dev/null +++ b/src/org/rascalmpl/repl/rascal/IRascalLanguageProtocol.java @@ -0,0 +1,101 @@ +/** + * Copyright (c) 2017, Jurgen J. Vinju, Mauricio Verano, Centrum Wiskunde & Informatica (CWI) All + * rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, are permitted + * provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this list of conditions + * and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, this list of + * conditions and the following disclaimer in the documentation and/or other materials provided with + * the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR + * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND + * FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, + * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY + * WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package org.rascalmpl.repl.rascal; + +import java.io.PrintWriter; +import java.io.Reader; +import java.util.List; +import java.util.Map; + +import org.rascalmpl.debug.IRascalMonitor; +import org.rascalmpl.ideservices.IDEServices; +import org.rascalmpl.repl.output.ICommandOutput; +import org.rascalmpl.values.parsetrees.ITree; + + +public interface IRascalLanguageProtocol { + + + IDEServices buildIDEService(PrintWriter err, IRascalMonitor monitor); + + /** + * During the constructor call initialize is called after the REPL is setup enough to have a stdout and std err to write to. + * @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. + */ + void initialize(Reader input, PrintWriter stdout, PrintWriter stderr, IDEServices services); + + + /** + * Try and parse a command, it's used for the REPL to decide if the command is complete + * @param command + * @return + */ + ITree parseCommand(String command); + + /** + * After a command has succesfully parsed, this function is called to execute the command + * @param command command entered. + * @throws InterruptedException throw this exception to stop the REPL (instead of calling .stop()) + */ + ICommandOutput handleInput(String command) throws InterruptedException; + + /** + * 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. + */ + void cancelRunningCommandRequested(); + + /** + * This method gets called from another thread, indicates a user pressed CTRL+\ during a call to handleInput. + * + * If possible, print the current stack trace. + */ + void stackTraceRequested(); + + + void flush(); + + /** + * Lookup modules that have a certain path prefix (for example util) + * @param modulePrefix a module prefix, can be empty, or contain a full subdirectory path like util + * @return A list of direct siblings of the prefix, either a single module, or a sub directory. It should not be fully qualified, so for the util request it should return Reflective not util::Reflective and directories should be returned as: sub:: + */ + List lookupModules(String modulePrefix); + + /** + * complete identifiers + * @param qualifier optionally empty qualitifer (for example IO::) + * @param partial part to be completed (for example pri) + * @return map from identifiers to their category (used for grouping in the REPL) + */ + Map completePartialIdentifier(String qualifier, String partial); + + /** + * which :set options are available on the repl + * @return map from command line option to description of it + */ + Map availableCommandLineOptions(); +} diff --git a/src/org/rascalmpl/repl/rascal/RascalInterpreterREPL.java b/src/org/rascalmpl/repl/rascal/RascalInterpreterREPL.java new file mode 100644 index 00000000000..89db5caeac7 --- /dev/null +++ b/src/org/rascalmpl/repl/rascal/RascalInterpreterREPL.java @@ -0,0 +1,179 @@ +package org.rascalmpl.repl.rascal; + + +import static org.rascalmpl.interpreter.utils.ReadEvalPrintDialogMessages.parseErrorMessage; +import static org.rascalmpl.interpreter.utils.ReadEvalPrintDialogMessages.staticErrorMessage; +import static org.rascalmpl.interpreter.utils.ReadEvalPrintDialogMessages.throwMessage; +import static org.rascalmpl.interpreter.utils.ReadEvalPrintDialogMessages.throwableMessage; + +import java.io.PrintWriter; +import java.io.Reader; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.function.Function; +import java.util.HashMap; + +import org.jline.reader.EndOfFileException; +import org.rascalmpl.debug.IRascalMonitor; +import org.rascalmpl.exceptions.Throw; +import org.rascalmpl.ideservices.BasicIDEServices; +import org.rascalmpl.ideservices.IDEServices; +import org.rascalmpl.interpreter.Configuration; +import org.rascalmpl.interpreter.Evaluator; +import org.rascalmpl.interpreter.NullRascalMonitor; +import org.rascalmpl.interpreter.control_exceptions.InterruptException; +import org.rascalmpl.interpreter.control_exceptions.QuitException; +import org.rascalmpl.interpreter.env.GlobalEnvironment; +import org.rascalmpl.interpreter.env.ModuleEnvironment; +import org.rascalmpl.interpreter.load.StandardLibraryContributor; +import org.rascalmpl.interpreter.result.Result; +import org.rascalmpl.interpreter.staticErrors.StaticError; +import org.rascalmpl.parser.gtd.exception.ParseError; +import org.rascalmpl.repl.output.ICommandOutput; +import org.rascalmpl.uri.URIUtil; +import org.rascalmpl.values.ValueFactoryFactory; +import org.rascalmpl.values.functions.IFunction; +import org.rascalmpl.values.parsetrees.ITree; + +import io.usethesource.vallang.ISourceLocation; +import io.usethesource.vallang.IValue; +import io.usethesource.vallang.IValueFactory; + +public abstract class RascalInterpreterREPL implements IRascalLanguageProtocol { + private Evaluator eval; + + private final RascalValuePrinter printer; + + private static final ISourceLocation PROMPT_LOCATION = URIUtil.rootLocation("prompt"); + + @Override + public ITree parseCommand(String command) { + Objects.requireNonNull(eval, "Not initialized yet"); + synchronized(eval) { + return eval.parseCommand(new NullRascalMonitor(), command, PROMPT_LOCATION); + } + } + + public RascalInterpreterREPL() { + this.printer = new RascalValuePrinter() { + @Override + protected Function liftProviderFunction(IFunction func) { + return v -> { + Objects.requireNonNull(eval, "Not initialized yet"); + synchronized(eval) { + return func.call(v); + } + }; + } + }; + } + + @Override + public IDEServices buildIDEService(PrintWriter err, IRascalMonitor monitor) { + return new BasicIDEServices(err, monitor); + } + + /** + * You might want to override this function for different cases of where we're building a REPL + * @param input + * @param stdout + * @param stderr + * @param monitor + * @param services + * @return + */ + protected Evaluator buildEvaluator(Reader input, PrintWriter stdout, PrintWriter stderr, IDEServices services) { + GlobalEnvironment heap = new GlobalEnvironment(); + ModuleEnvironment root = heap.addModule(new ModuleEnvironment(ModuleEnvironment.SHELL_MODULE, heap)); + IValueFactory vf = ValueFactoryFactory.getValueFactory(); + Evaluator evaluator = new Evaluator(vf, input, stderr, stdout, root, heap, services); + evaluator.addRascalSearchPathContributor(StandardLibraryContributor.getInstance()); + return evaluator; + } + + @Override + public void initialize(Reader input, PrintWriter stdout, PrintWriter stderr, IDEServices services) { + if (eval != null) { + throw new IllegalStateException("Already initialized"); + } + eval = buildEvaluator(input, stdout, stderr, services); + } + + + @Override + public ICommandOutput handleInput(String command) throws InterruptedException { + Objects.requireNonNull(eval, "Not initialized yet"); + synchronized(eval) { + try { + Result value = eval.eval(eval.getMonitor(), command, PROMPT_LOCATION); + return printer.outputResult(value); + } + catch (InterruptException ex) { + return printer.outputError((w, sw) -> { + w.println("Interrupted"); + ex.getRascalStackTrace().prettyPrintedString(w, sw); + }); + } + catch (ParseError pe) { + return printer.outputError((w, sw) -> { + parseErrorMessage(w, command, "prompt", pe, sw); + }); + } + catch (StaticError e) { + return printer.outputError((w, sw) -> { + staticErrorMessage(w, e, sw); + }); + } + catch (Throw e) { + return printer.outputError((w, sw) -> { + throwMessage(w,e, sw); + }); + } + catch (QuitException q) { + throw new EndOfFileException("Quiting REPL"); + } + catch (Throwable e) { + return printer.outputError((w, sw) -> { + throwableMessage(w, e, eval.getStackTrace(), sw); + }); + } + } + } + + @Override + public void cancelRunningCommandRequested() { + Objects.requireNonNull(eval, "Not initialized yet"); + eval.interrupt(); + eval.endAllJobs(); + } + + @Override + public void stackTraceRequested() { + // TODO Auto-generated method stub + throw new UnsupportedOperationException("Unimplemented method 'stackTraceRequested'"); + } + + @Override + public List lookupModules(String modulePrefix) { + Objects.requireNonNull(eval, "Not initialized yet"); + return eval.getRascalResolver().listModuleEntries(modulePrefix); + } + + @Override + public Map completePartialIdentifier(String qualifier, String partial) { + Objects.requireNonNull(eval, "Not initialized yet"); + return eval.completePartialIdentifier(qualifier, partial); + } + + @Override + public Map availableCommandLineOptions() { + var commandLineOptions = new HashMap(); + commandLineOptions.put(Configuration.GENERATOR_PROFILING_PROPERTY.substring("rascal.".length()), "enable sampling profiler for generator"); + commandLineOptions.put(Configuration.PROFILING_PROPERTY.substring("rascal.".length()), "enable sampling profiler" ); + commandLineOptions.put(Configuration.ERRORS_PROPERTY.substring("rascal.".length()), "print raw java errors"); + commandLineOptions.put(Configuration.TRACING_PROPERTY.substring("rascal.".length()), "trace all function calls (warning: a lot of output will be generated)"); + return commandLineOptions; + } + +} diff --git a/src/org/rascalmpl/repl/rascal/RascalReplServices.java b/src/org/rascalmpl/repl/rascal/RascalReplServices.java new file mode 100644 index 00000000000..b3299a393e1 --- /dev/null +++ b/src/org/rascalmpl/repl/rascal/RascalReplServices.java @@ -0,0 +1,148 @@ +package org.rascalmpl.repl.rascal; + +import java.io.PrintWriter; +import java.io.Writer; +import java.util.ArrayList; +import java.util.List; +import java.util.TreeMap; + +import org.jline.jansi.Ansi; +import org.jline.reader.Completer; +import org.jline.reader.Parser; +import org.jline.terminal.Terminal; +import org.jline.utils.InfoCmp.Capability; +import org.rascalmpl.repl.IREPLService; +import org.rascalmpl.repl.TerminalProgressBarMonitor; +import org.rascalmpl.repl.completers.RascalCommandCompletion; +import org.rascalmpl.repl.completers.RascalIdentifierCompletion; +import org.rascalmpl.repl.completers.RascalKeywordCompletion; +import org.rascalmpl.repl.completers.RascalLocationCompletion; +import org.rascalmpl.repl.completers.RascalModuleCompletion; +import org.rascalmpl.repl.jline3.RascalLineParser; +import org.rascalmpl.repl.output.ICommandOutput; +import org.rascalmpl.repl.streams.ItalicErrorWriter; +import org.rascalmpl.repl.streams.RedErrorWriter; + +public class RascalReplServices implements IREPLService { + private final IRascalLanguageProtocol lang; + + private PrintWriter out; + private PrintWriter err; + + public RascalReplServices(IRascalLanguageProtocol lang) { + this.lang= lang; + } + + @Override + public void connect(Terminal term) { + if (out != null) { + throw new IllegalStateException("Repl Service is already initialized"); + } + var monitor = new TerminalProgressBarMonitor(term); + out = monitor; + err = generateErrorStream(term, monitor); + var service = lang.buildIDEService(err, monitor); + + lang.initialize(term.reader(), out, err, service); + } + + + + private static PrintWriter generateErrorStream(Terminal tm, Writer out) { + // previously we would alway write errors to System.err, but that tends to mess up terminals + // and also our own error print + // so now we try to not write to System.err + if (supportsColors(tm)) { + return new PrintWriter(new RedErrorWriter(out), true); + } + if (supportsItalic(tm)) { + return new PrintWriter(new ItalicErrorWriter(out), true); + } + return new PrintWriter(System.err, true); + + } + + private static boolean supportsColors(Terminal tm) { + Integer cols = tm.getNumericCapability(Capability.max_colors); + return cols != null && cols >= 8; + } + + private static boolean supportsItalic(Terminal tm) { + String ital = tm.getStringCapability(Capability.enter_italics_mode); + return ital != null && !ital.equals(""); + } + + @Override + public Parser inputParser() { + return new RascalLineParser(lang::parseCommand); + } + + @Override + public ICommandOutput handleInput(String input) throws InterruptedException { + return lang.handleInput(input); + } + + + @Override + public void handleInterrupt() throws InterruptedException { + lang.cancelRunningCommandRequested(); + } + + @Override + public String prompt(boolean ansiColorsSupported, boolean unicodeSupported) { + if (ansiColorsSupported) { + return Ansi.ansi().reset().bold() + "rascal>" + Ansi.ansi().reset(); + } + return "rascal>"; + } + + @Override + public String parseErrorPrompt(boolean ansiColorsSupported, boolean unicodeSupported) { + String errorPrompt = (unicodeSupported ? "│" : "|") + "%N %P>"; + if (ansiColorsSupported) { + return Ansi.ansi().reset().bold() + errorPrompt + Ansi.ansi().reset(); + } + return errorPrompt; + } + + @Override + public PrintWriter errorWriter() { + return err; + } + + @Override + public PrintWriter outputWriter() { + return out; + } + + @Override + public void flush() { + lang.flush(); + err.flush(); + out.flush(); + } + + + @Override + public boolean supportsCompletion() { + return true; + } + + @Override + public List completers() { + var result = new ArrayList(); + var moduleCompleter = new RascalModuleCompletion(lang::lookupModules); + var idCompleter = new RascalIdentifierCompletion(lang::completePartialIdentifier); + result.add(new RascalCommandCompletion( + new TreeMap<>(lang.availableCommandLineOptions()), + idCompleter::completePartialIdentifier, + (s, c) -> moduleCompleter.completeModuleNames(s, c, false) + )); + result.add(moduleCompleter); + result.add(idCompleter); + result.add(new RascalKeywordCompletion()); + result.add(new RascalLocationCompletion()); + return result; + } + +} diff --git a/src/org/rascalmpl/repl/rascal/RascalValuePrinter.java b/src/org/rascalmpl/repl/rascal/RascalValuePrinter.java new file mode 100644 index 00000000000..426413f4250 --- /dev/null +++ b/src/org/rascalmpl/repl/rascal/RascalValuePrinter.java @@ -0,0 +1,297 @@ +package org.rascalmpl.repl.rascal; + + +import java.io.IOException; +import java.io.PrintWriter; +import java.io.Writer; +import java.net.URI; +import java.net.URISyntaxException; +import java.util.function.Function; + +import org.rascalmpl.interpreter.result.IRascalResult; +import org.rascalmpl.repl.http.REPLContentServer; +import org.rascalmpl.repl.http.REPLContentServerManager; +import org.rascalmpl.repl.output.IAnsiCommandOutput; +import org.rascalmpl.repl.output.ICommandOutput; +import org.rascalmpl.repl.output.IErrorCommandOutput; +import org.rascalmpl.repl.output.IHtmlCommandOutput; +import org.rascalmpl.repl.output.IOutputPrinter; +import org.rascalmpl.repl.output.IWebContentOutput; +import org.rascalmpl.repl.output.MimeTypes; +import org.rascalmpl.repl.output.impl.StringOutputPrinter; +import org.rascalmpl.repl.streams.LimitedLineWriter; +import org.rascalmpl.repl.streams.LimitedWriter; +import org.rascalmpl.repl.streams.ReplTextWriter; +import org.rascalmpl.values.RascalValueFactory; +import org.rascalmpl.values.functions.IFunction; +import org.rascalmpl.values.parsetrees.TreeAdapter; + +import io.usethesource.vallang.IConstructor; +import io.usethesource.vallang.IInteger; +import io.usethesource.vallang.IString; +import io.usethesource.vallang.IValue; +import io.usethesource.vallang.IWithKeywordParameters; +import io.usethesource.vallang.io.StandardTextWriter; +import io.usethesource.vallang.type.Type; + +/** + * Printing Rascal values to different outputs. + */ +public abstract class RascalValuePrinter { + + private final static int LINE_LIMIT = 200; + private final static int CHAR_LIMIT = LINE_LIMIT * 20; + + private final REPLContentServerManager contentManager = new REPLContentServerManager(); + private static final StandardTextWriter ansiIndentedPrinter = new ReplTextWriter(true); + private static final StandardTextWriter plainIndentedPrinter = new StandardTextWriter(true); + + /** + * Make a generic closure out of a rascal IFunction (might need to wrap the call with a lock on the evaluator) + */ + protected abstract Function liftProviderFunction(IFunction func); + + @FunctionalInterface + public static interface ThrowingWriter { + void write(PrintWriter writer, StandardTextWriter prettyPrinter) throws IOException; + } + + public IErrorCommandOutput outputError(ThrowingWriter writer) { + return new IErrorCommandOutput() { + @Override + public ICommandOutput getError() { + return new DoubleOutput(writer); + } + + @Override + public IOutputPrinter asPlain() { + return new ParameterizedPrinterOutput(writer, plainIndentedPrinter, MimeTypes.PLAIN_TEXT); + } + }; + } + + public ICommandOutput outputResult(IRascalResult result) { + if (result == null || result.getValue() == null) { + return () -> new StringOutputPrinter("ok", MimeTypes.PLAIN_TEXT); + } + IValue value = result.getValue(); + Type type = result.getStaticType(); + + if (type.isSubtypeOf(RascalValueFactory.Content) && !type.isBottom()) { + return serveContent((IConstructor)value); + } + + ThrowingWriter resultWriter; + if (type.isAbstractData() && type.isStrictSubtypeOf(RascalValueFactory.Tree) && !type.isBottom()) { + resultWriter = (w, sw) -> { + w.write("(" + type.toString() +") `"); + TreeAdapter.yield((IConstructor)value, sw == ansiIndentedPrinter, w); + w.write("`"); + }; + } + else if (type.isString()) { + resultWriter = (w, sw) -> { + // TODO: do something special for the reader version of IString, when that is released + // for now, we only support write + + try (Writer wrt = new LimitedWriter(new LimitedLineWriter(w, LINE_LIMIT), CHAR_LIMIT)) { + sw.write(value, wrt); + } + catch (/*IOLimitReachedException*/ RuntimeException e) { + // ignore since this is what we wanted + // if we catch IOLimitReachedException we get an IllegalArgument exception instead + // "Self-suppression not permitted" + } + w.println(); + w.println("---"); + try (Writer wrt = new LimitedWriter(new LimitedLineWriter(w, LINE_LIMIT), CHAR_LIMIT)) { + ((IString) value).write(wrt); + } + catch (/*IOLimitReachedException*/ RuntimeException e) { + // ignore since this is what we wanted + // if we catch IOLimitReachedException we get an IllegalArgument exception instead + // "Self-suppression not permitted" + } + w.println(); + w.print("---"); + }; + } + else { + resultWriter = (w, sw) -> { + try (Writer wrt = new LimitedWriter(new LimitedLineWriter(w, LINE_LIMIT), CHAR_LIMIT)) { + sw.write(value, wrt); + } + catch (/*IOLimitReachedException*/ RuntimeException e) { + // ignore since this is what we wanted + // if we catch IOLimitReachedException we get an IllegalArgument exception instead + // "Self-suppression not permitted" + } + }; + } + + ThrowingWriter typePrefixed = (w, sw) -> { + w.write(type.toString()); + w.write(": "); + resultWriter.write(w, sw); + w.println(); + }; + + return new DoubleOutput(typePrefixed); + } + + private static class DoubleOutput implements IAnsiCommandOutput { + private ThrowingWriter writer; + + DoubleOutput(ThrowingWriter writer) { + this.writer = writer; + } + + @Override + public IOutputPrinter asAnsi() { + return new ParameterizedPrinterOutput(writer, ansiIndentedPrinter, MimeTypes.ANSI); + } + + @Override + public IOutputPrinter asPlain() { + return new ParameterizedPrinterOutput(writer, plainIndentedPrinter, MimeTypes.PLAIN_TEXT); + } + } + + private ICommandOutput serveContent(IConstructor provider) { + String id; + Function target; + + if (provider.has("id")) { + id = ((IString) provider.get("id")).getValue(); + target = liftProviderFunction(((IFunction) provider.get("callback"))); + } + else { + id = "*static content*"; + target = (r) -> provider.get("response"); + } + + try { + // this installs the provider such that subsequent requests are handled. + REPLContentServer server = contentManager.addServer(id, target); + + // now we need some HTML to show + + IWithKeywordParameters kp = provider.asWithKeywordParameters(); + String title = kp.hasParameter("title") ? ((IString) kp.getParameter("title")).getValue() : id; + int viewColumn = kp.hasParameter("viewColumn") ? ((IInteger)kp.getParameter("viewColumn")).intValue() : 1; + URI serverUri = new URI("http", null, "localhost", server.getListeningPort(), "/", null, null); + + return new HostedWebContentOutput(id, serverUri, title, viewColumn); + + } + catch (IOException e) { + return outputError((w, sw) -> { + w.println("Could not start webserver to render html content: "); + w.println(e.getMessage()); + }); + } + catch (URISyntaxException e) { + return outputError((w, sw) -> { + w.println("Could not start build the uri: "); + w.println(e.getMessage()); + }); + } + } + + private static class HostedWebContentOutput implements IWebContentOutput, IHtmlCommandOutput { + private final String id; + private final URI uri; + private final String title; + private final int viewColumn; + + HostedWebContentOutput(String id, URI uri, String title, int viewColumn) { + this.id = id; + this.uri = uri; + this.title = title; + this.viewColumn = viewColumn; + } + + @Override + public IOutputPrinter asPlain() { + return new IOutputPrinter() { + @Override + public void write(PrintWriter target) { + target.print("Serving \'"); + target.print(id); + target.print("\' at |"); + target.print(uri.toASCIIString()); + target.println("|"); + } + @Override + public String mimeType() { + return MimeTypes.PLAIN_TEXT; + } + }; + } + + @Override + public IOutputPrinter asHtml() { + return new IOutputPrinter() { + @Override + public void write(PrintWriter target) { + target.print(""); + } + + @Override + public String mimeType() { + return MimeTypes.HTML; + } + }; + } + + @Override + public URI webUri() { + return uri; + } + + @Override + public String webTitle() { + return title; + } + + @Override + public int webviewColumn() { + return viewColumn; + } + } + + + private static class ParameterizedPrinterOutput implements IOutputPrinter { + private final ThrowingWriter internalWriter; + private final StandardTextWriter prettyPrinter; + private final String mimeType; + + public ParameterizedPrinterOutput(ThrowingWriter internalWriter, StandardTextWriter prettyPrinter, String mimeType) { + this.internalWriter = internalWriter; + this.prettyPrinter = prettyPrinter; + this.mimeType = mimeType; + } + + @Override + public String mimeType() { + return mimeType; + } + + @Override + public void write(PrintWriter target) { + try { + internalWriter.write(target, prettyPrinter); + } + catch (IOException e) { + target.println("Internal failure: printing exception failed with:"); + target.println(e.toString()); + e.printStackTrace(target); + } + } + } + +} diff --git a/src/org/rascalmpl/semantics/dynamic/ShellCommand.java b/src/org/rascalmpl/semantics/dynamic/ShellCommand.java index c9d316c8f1e..e526446702e 100644 --- a/src/org/rascalmpl/semantics/dynamic/ShellCommand.java +++ b/src/org/rascalmpl/semantics/dynamic/ShellCommand.java @@ -13,6 +13,7 @@ *******************************************************************************/ package org.rascalmpl.semantics.dynamic; +import org.jline.utils.InfoCmp.Capability; import org.rascalmpl.ast.Expression; import org.rascalmpl.ast.QualifiedName; import org.rascalmpl.debug.IRascalMonitor; @@ -69,8 +70,14 @@ public Result interpret(IEvaluator> __eval) { IRascalMonitor monitor = __eval.getMonitor(); if (monitor instanceof IDEServices) { - IDEServices services = (IDEServices) monitor; - services.clearRepl(); + var services = (IDEServices) monitor; + var term = services.activeTerminal(); + if (term != null) { + term.puts(Capability.clear_screen); + } + else { + __eval.getStdErr().println("There is not terminal available to clear"); + } } else { __eval.getStdErr().println("The current Rascal execution environment does not know how to clear the REPL."); diff --git a/src/org/rascalmpl/shell/RascalShell2.java b/src/org/rascalmpl/shell/RascalShell2.java index 9ad4236e5ca..781e3f8c699 100644 --- a/src/org/rascalmpl/shell/RascalShell2.java +++ b/src/org/rascalmpl/shell/RascalShell2.java @@ -55,12 +55,14 @@ public static void main(String[] args) throws IOException { var repl = new BaseREPL2(new RascalReplServices((t) -> { var monitor = new TerminalProgressBarMonitor(term); - IDEServices services = new BasicIDEServices(term.writer(), monitor, () -> term.puts(Capability.clear_screen)); + var err = RascalReplServices.generateErrorStream(t, monitor); + + IDEServices services = new BasicIDEServices(err, monitor, () -> term.puts(Capability.clear_screen)); GlobalEnvironment heap = new GlobalEnvironment(); ModuleEnvironment root = heap.addModule(new ModuleEnvironment(ModuleEnvironment.SHELL_MODULE, heap)); IValueFactory vf = ValueFactoryFactory.getValueFactory(); - Evaluator evaluator = new Evaluator(vf, term.reader(), RascalReplServices.generateErrorStream(t, monitor), monitor, root, heap, services); + Evaluator evaluator = new Evaluator(vf, term.reader(), err, monitor, root, heap, services); evaluator.addRascalSearchPathContributor(StandardLibraryContributor.getInstance()); return evaluator; }), term); From 98c3e8b90591168a5d9f6020973fa41316eba5a1 Mon Sep 17 00:00:00 2001 From: Davy Landman Date: Fri, 20 Dec 2024 09:57:48 +0100 Subject: [PATCH 27/35] Finished rewriting REPL framework to prepare for compiler and parametric --- .../ideservices/BasicIDEServices.java | 15 ++-- .../repl/{BaseREPL2.java => BaseREPL.java} | 31 ++------ src/org/rascalmpl/repl/IREPLService.java | 2 + .../rascalmpl/repl/output/IOutputPrinter.java | 8 +- ...ter.java => AsciiStringOutputPrinter.java} | 10 +-- .../repl/parametric/ILanguageProtocol.java | 2 +- .../repl/rascal/IRascalLanguageProtocol.java | 6 +- .../repl/rascal/RascalInterpreterREPL.java | 39 ++++++--- .../repl/rascal/RascalReplServices.java | 17 +++- .../repl/rascal/RascalValuePrinter.java | 79 +++++++++++-------- src/org/rascalmpl/shell/RascalShell2.java | 47 +++-------- 11 files changed, 131 insertions(+), 125 deletions(-) rename src/org/rascalmpl/repl/{BaseREPL2.java => BaseREPL.java} (86%) rename src/org/rascalmpl/repl/output/impl/{StringOutputPrinter.java => AsciiStringOutputPrinter.java} (64%) diff --git a/src/org/rascalmpl/ideservices/BasicIDEServices.java b/src/org/rascalmpl/ideservices/BasicIDEServices.java index 4f6ea8063ab..2e9ccbaa3e8 100644 --- a/src/org/rascalmpl/ideservices/BasicIDEServices.java +++ b/src/org/rascalmpl/ideservices/BasicIDEServices.java @@ -20,6 +20,7 @@ import java.nio.file.Path; import java.nio.file.Paths; +import org.jline.terminal.Terminal; import org.rascalmpl.debug.IRascalMonitor; import org.rascalmpl.uri.URIResolverRegistry; import org.rascalmpl.uri.URIUtil; @@ -34,15 +35,12 @@ public class BasicIDEServices implements IDEServices { private final IRascalMonitor monitor; private final PrintWriter stderr; - private final Runnable clearRepl; + private final Terminal terminal; - public BasicIDEServices(PrintWriter stderr, IRascalMonitor monitor){ - this(stderr, monitor, () -> {}); - } - public BasicIDEServices(PrintWriter stderr, IRascalMonitor monitor, Runnable clearRepl){ + public BasicIDEServices(PrintWriter stderr, IRascalMonitor monitor, Terminal terminal){ this.stderr = stderr; this.monitor = monitor; - this.clearRepl = clearRepl; + this.terminal = terminal; } @Override @@ -51,9 +49,10 @@ public PrintWriter stderr() { } @Override - public void clearRepl() { - this.clearRepl.run(); + public Terminal activeTerminal() { + return terminal; } + public void browse(ISourceLocation loc, String title, int viewColumn){ browse(loc.getURI(), title, viewColumn); diff --git a/src/org/rascalmpl/repl/BaseREPL2.java b/src/org/rascalmpl/repl/BaseREPL.java similarity index 86% rename from src/org/rascalmpl/repl/BaseREPL2.java rename to src/org/rascalmpl/repl/BaseREPL.java index 1975fe8bbe4..ddd70ee3e53 100644 --- a/src/org/rascalmpl/repl/BaseREPL2.java +++ b/src/org/rascalmpl/repl/BaseREPL.java @@ -23,19 +23,18 @@ import org.rascalmpl.repl.output.IErrorCommandOutput; import org.rascalmpl.repl.output.IOutputPrinter; -public class BaseREPL2 { +public class BaseREPL { private final IREPLService replService; private final Terminal term; private final LineReader reader; private volatile boolean keepRunning = true; private final @MonotonicNonNull DefaultHistory history; - private String currentPrompt; + private final String normalPrompt; private final boolean ansiColorsSupported; - private final boolean advancedTermFeaturesSupported; private final boolean unicodeSupported; - public BaseREPL2(IREPLService replService, Terminal term) { + public BaseREPL(IREPLService replService, Terminal term) { this.replService = replService; this.term = term; @@ -60,23 +59,9 @@ public BaseREPL2(IREPLService replService, Terminal term) { if (replService.supportsCompletion()) { reader.completer(new AggregateCompleter(replService.completers())); } - - switch (term.getType()) { - case Terminal.TYPE_DUMB: - this.ansiColorsSupported = false; - this.advancedTermFeaturesSupported = false; - break; - case Terminal.TYPE_DUMB_COLOR: - this.ansiColorsSupported = false; - this.advancedTermFeaturesSupported = true; - break; - default: - this.ansiColorsSupported = true; - this.advancedTermFeaturesSupported = true; - break; - } + this.ansiColorsSupported = !term.getType().equals(Terminal.TYPE_DUMB); this.unicodeSupported = term.encoding().newEncoder().canEncode("💓"); - this.currentPrompt = replService.prompt(ansiColorsSupported, unicodeSupported); + this.normalPrompt = replService.prompt(ansiColorsSupported, unicodeSupported); reader.variable(LineReader.SECONDARY_PROMPT_PATTERN, replService.parseErrorPrompt(ansiColorsSupported, unicodeSupported)); this.reader = reader.build(); @@ -101,7 +86,7 @@ public void run() throws IOException { while (keepRunning) { try { replService.flush(); - String line = reader.readLine(this.currentPrompt); + String line = reader.readLine(this.normalPrompt); if (line == null) { // EOF @@ -112,7 +97,7 @@ public void run() throws IOException { } catch (UserInterruptException u) { // only thrown while `readLine` is active - reader.printAbove(">>>>>>> Interrupted"); + reader.printAbove(replService.interruptedPrompt(ansiColorsSupported, unicodeSupported)); term.flush(); } finally { @@ -206,6 +191,6 @@ private void writeResult(ICommandOutput result) { writer = result.asPlain(); } - writer.write(target); + writer.write(target, unicodeSupported); } } diff --git a/src/org/rascalmpl/repl/IREPLService.java b/src/org/rascalmpl/repl/IREPLService.java index e7792540a5c..b9f9b0c8f8e 100644 --- a/src/org/rascalmpl/repl/IREPLService.java +++ b/src/org/rascalmpl/repl/IREPLService.java @@ -98,4 +98,6 @@ default Path historyFile() { */ void flush(); + String interruptedPrompt(boolean ansiColorsSupported, boolean unicodeSupported); + } diff --git a/src/org/rascalmpl/repl/output/IOutputPrinter.java b/src/org/rascalmpl/repl/output/IOutputPrinter.java index 83c1b1c0c76..06ef62caedf 100644 --- a/src/org/rascalmpl/repl/output/IOutputPrinter.java +++ b/src/org/rascalmpl/repl/output/IOutputPrinter.java @@ -14,8 +14,9 @@ public interface IOutputPrinter { /** * Write the output on this print writer interface. It should always print something, even if it's a warning saying it cannot be printed * @param target where to write the output to. + * @param unicodeSupported if the target can render unicode characters */ - void write(PrintWriter target); + void write(PrintWriter target, boolean unicodeSupported); String mimeType(); @@ -24,12 +25,13 @@ public interface IOutputPrinter { * The standard implementation takes care to just call the write function with a buffer. * If you however can provide a streaming reading, override this function instead, depending * on the consumer, it might be called instead of the write function. + * @param unicodeSupported if the consumer can render unicode characters * @return a reader that produces the same contents as the write function, but in a pull style instead of push. */ - default Reader asReader() { + default Reader asReader(boolean unicodeSupported) { try (var result = new StringWriter()) { try (var resultWriter = new PrintWriter(result)) { - write(resultWriter); + write(resultWriter, unicodeSupported); } return new StringReader(result.toString()); } diff --git a/src/org/rascalmpl/repl/output/impl/StringOutputPrinter.java b/src/org/rascalmpl/repl/output/impl/AsciiStringOutputPrinter.java similarity index 64% rename from src/org/rascalmpl/repl/output/impl/StringOutputPrinter.java rename to src/org/rascalmpl/repl/output/impl/AsciiStringOutputPrinter.java index 5d856462d8d..54219ba7140 100644 --- a/src/org/rascalmpl/repl/output/impl/StringOutputPrinter.java +++ b/src/org/rascalmpl/repl/output/impl/AsciiStringOutputPrinter.java @@ -7,14 +7,14 @@ import org.rascalmpl.repl.output.IOutputPrinter; import org.rascalmpl.repl.output.MimeTypes; -public class StringOutputPrinter implements IOutputPrinter { +public class AsciiStringOutputPrinter implements IOutputPrinter { private final String body; - public StringOutputPrinter(String body) { + public AsciiStringOutputPrinter(String body) { this(body, MimeTypes.PLAIN_TEXT); } - public StringOutputPrinter(String body, String mimeType) { + public AsciiStringOutputPrinter(String body, String mimeType) { this.body = body; } @@ -24,12 +24,12 @@ public String mimeType() { } @Override - public void write(PrintWriter target) { + public void write(PrintWriter target, boolean unicodeSupported) { target.println(body); } @Override - public Reader asReader() { + public Reader asReader(boolean unicodeSupported) { return new StringReader(body + System.lineSeparator()); } } diff --git a/src/org/rascalmpl/repl/parametric/ILanguageProtocol.java b/src/org/rascalmpl/repl/parametric/ILanguageProtocol.java index a85cc6eb084..26d921b6d0e 100644 --- a/src/org/rascalmpl/repl/parametric/ILanguageProtocol.java +++ b/src/org/rascalmpl/repl/parametric/ILanguageProtocol.java @@ -21,7 +21,7 @@ * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY * WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ -package org.rascalmpl.repl; +package org.rascalmpl.repl.parametric; import java.io.InputStream; import java.io.PrintWriter; diff --git a/src/org/rascalmpl/repl/rascal/IRascalLanguageProtocol.java b/src/org/rascalmpl/repl/rascal/IRascalLanguageProtocol.java index fc8c1609bef..cfa00e8ccc7 100644 --- a/src/org/rascalmpl/repl/rascal/IRascalLanguageProtocol.java +++ b/src/org/rascalmpl/repl/rascal/IRascalLanguageProtocol.java @@ -28,16 +28,18 @@ import java.util.List; import java.util.Map; +import org.jline.terminal.Terminal; import org.rascalmpl.debug.IRascalMonitor; import org.rascalmpl.ideservices.IDEServices; import org.rascalmpl.repl.output.ICommandOutput; import org.rascalmpl.values.parsetrees.ITree; + public interface IRascalLanguageProtocol { - IDEServices buildIDEService(PrintWriter err, IRascalMonitor monitor); + IDEServices buildIDEService(PrintWriter err, IRascalMonitor monitor, Terminal term); /** * During the constructor call initialize is called after the REPL is setup enough to have a stdout and std err to write to. @@ -73,7 +75,7 @@ public interface IRascalLanguageProtocol { * * If possible, print the current stack trace. */ - void stackTraceRequested(); + ICommandOutput stackTraceRequested(); void flush(); diff --git a/src/org/rascalmpl/repl/rascal/RascalInterpreterREPL.java b/src/org/rascalmpl/repl/rascal/RascalInterpreterREPL.java index 89db5caeac7..8d882b4c6cb 100644 --- a/src/org/rascalmpl/repl/rascal/RascalInterpreterREPL.java +++ b/src/org/rascalmpl/repl/rascal/RascalInterpreterREPL.java @@ -8,14 +8,16 @@ import java.io.PrintWriter; import java.io.Reader; +import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Objects; import java.util.function.Function; -import java.util.HashMap; import org.jline.reader.EndOfFileException; +import org.jline.terminal.Terminal; import org.rascalmpl.debug.IRascalMonitor; +import org.rascalmpl.exceptions.StackTrace; import org.rascalmpl.exceptions.Throw; import org.rascalmpl.ideservices.BasicIDEServices; import org.rascalmpl.ideservices.IDEServices; @@ -40,7 +42,7 @@ import io.usethesource.vallang.IValue; import io.usethesource.vallang.IValueFactory; -public abstract class RascalInterpreterREPL implements IRascalLanguageProtocol { +public class RascalInterpreterREPL implements IRascalLanguageProtocol { private Evaluator eval; private final RascalValuePrinter printer; @@ -70,8 +72,8 @@ protected Function liftProviderFunction(IFunction func) { } @Override - public IDEServices buildIDEService(PrintWriter err, IRascalMonitor monitor) { - return new BasicIDEServices(err, monitor); + public IDEServices buildIDEService(PrintWriter err, IRascalMonitor monitor, Terminal term) { + return new BasicIDEServices(err, monitor, term); } /** @@ -110,23 +112,26 @@ public ICommandOutput handleInput(String command) throws InterruptedException { return printer.outputResult(value); } catch (InterruptException ex) { - return printer.outputError((w, sw) -> { + return printer.outputError((w, sw, u) -> { + if (u) { + w.print("»» "); + } w.println("Interrupted"); ex.getRascalStackTrace().prettyPrintedString(w, sw); }); } catch (ParseError pe) { - return printer.outputError((w, sw) -> { + return printer.outputError((w, sw, _u) -> { parseErrorMessage(w, command, "prompt", pe, sw); }); } catch (StaticError e) { - return printer.outputError((w, sw) -> { + return printer.outputError((w, sw, _u) -> { staticErrorMessage(w, e, sw); }); } catch (Throw e) { - return printer.outputError((w, sw) -> { + return printer.outputError((w, sw, _u) -> { throwMessage(w,e, sw); }); } @@ -134,7 +139,7 @@ public ICommandOutput handleInput(String command) throws InterruptedException { throw new EndOfFileException("Quiting REPL"); } catch (Throwable e) { - return printer.outputError((w, sw) -> { + return printer.outputError((w, sw, _u) -> { throwableMessage(w, e, eval.getStackTrace(), sw); }); } @@ -149,9 +154,13 @@ public void cancelRunningCommandRequested() { } @Override - public void stackTraceRequested() { - // TODO Auto-generated method stub - throw new UnsupportedOperationException("Unimplemented method 'stackTraceRequested'"); + public ICommandOutput stackTraceRequested() { + StackTrace trace = eval.getStackTrace(); + return printer.prettyPrinted((w, sw, u) -> { + w.println("Current stack trace:"); + trace.prettyPrintedString(w, sw); + w.flush(); + }); } @Override @@ -176,4 +185,10 @@ public Map availableCommandLineOptions() { return commandLineOptions; } + @Override + public void flush() { + eval.getStdErr().flush(); + eval.getStdOut().flush(); + } + } diff --git a/src/org/rascalmpl/repl/rascal/RascalReplServices.java b/src/org/rascalmpl/repl/rascal/RascalReplServices.java index b3299a393e1..c9c346bbe4c 100644 --- a/src/org/rascalmpl/repl/rascal/RascalReplServices.java +++ b/src/org/rascalmpl/repl/rascal/RascalReplServices.java @@ -7,6 +7,8 @@ import java.util.TreeMap; import org.jline.jansi.Ansi; +import org.jline.jansi.AnsiColors; +import org.jline.jansi.Ansi.Color; import org.jline.reader.Completer; import org.jline.reader.Parser; import org.jline.terminal.Terminal; @@ -41,7 +43,7 @@ public void connect(Terminal term) { var monitor = new TerminalProgressBarMonitor(term); out = monitor; err = generateErrorStream(term, monitor); - var service = lang.buildIDEService(err, monitor); + var service = lang.buildIDEService(err, monitor, term); lang.initialize(term.reader(), out, err, service); } @@ -90,6 +92,7 @@ public void handleInterrupt() throws InterruptedException { @Override public String prompt(boolean ansiColorsSupported, boolean unicodeSupported) { + if (ansiColorsSupported) { return Ansi.ansi().reset().bold() + "rascal>" + Ansi.ansi().reset(); } @@ -144,5 +147,17 @@ public List completers() { result.add(new RascalLocationCompletion()); return result; } + + @Override + public String interruptedPrompt(boolean ansiColorsSupported, boolean unicodeSupported) { + String prompt = ">>>>>>> Interrupted"; + if (unicodeSupported) { + prompt = prompt.replace(">", "»"); + } + if (ansiColorsSupported) { + prompt = Ansi.ansi().reset().fgRed().bold() + prompt + Ansi.ansi().reset(); + } + return prompt; + } } diff --git a/src/org/rascalmpl/repl/rascal/RascalValuePrinter.java b/src/org/rascalmpl/repl/rascal/RascalValuePrinter.java index 426413f4250..bc3c8d90aac 100644 --- a/src/org/rascalmpl/repl/rascal/RascalValuePrinter.java +++ b/src/org/rascalmpl/repl/rascal/RascalValuePrinter.java @@ -18,7 +18,7 @@ import org.rascalmpl.repl.output.IOutputPrinter; import org.rascalmpl.repl.output.IWebContentOutput; import org.rascalmpl.repl.output.MimeTypes; -import org.rascalmpl.repl.output.impl.StringOutputPrinter; +import org.rascalmpl.repl.output.impl.AsciiStringOutputPrinter; import org.rascalmpl.repl.streams.LimitedLineWriter; import org.rascalmpl.repl.streams.LimitedWriter; import org.rascalmpl.repl.streams.ReplTextWriter; @@ -53,26 +53,40 @@ public abstract class RascalValuePrinter { @FunctionalInterface public static interface ThrowingWriter { - void write(PrintWriter writer, StandardTextWriter prettyPrinter) throws IOException; + void write(PrintWriter writer, StandardTextWriter prettyPrinter, boolean unicodeSupported) throws IOException; } public IErrorCommandOutput outputError(ThrowingWriter writer) { return new IErrorCommandOutput() { @Override public ICommandOutput getError() { - return new DoubleOutput(writer); + return prettyPrinted(writer); } @Override public IOutputPrinter asPlain() { - return new ParameterizedPrinterOutput(writer, plainIndentedPrinter, MimeTypes.PLAIN_TEXT); + return new PrettyPrintedOutput(writer, plainIndentedPrinter, MimeTypes.PLAIN_TEXT); + } + }; + } + + public ICommandOutput prettyPrinted(ThrowingWriter writer) { + return new IAnsiCommandOutput() { + @Override + public IOutputPrinter asAnsi() { + return new PrettyPrintedOutput(writer, ansiIndentedPrinter, MimeTypes.ANSI); + } + + @Override + public IOutputPrinter asPlain() { + return new PrettyPrintedOutput(writer, plainIndentedPrinter, MimeTypes.PLAIN_TEXT); } }; } public ICommandOutput outputResult(IRascalResult result) { if (result == null || result.getValue() == null) { - return () -> new StringOutputPrinter("ok", MimeTypes.PLAIN_TEXT); + return () -> new AsciiStringOutputPrinter("ok", MimeTypes.PLAIN_TEXT); } IValue value = result.getValue(); Type type = result.getStaticType(); @@ -83,14 +97,14 @@ public ICommandOutput outputResult(IRascalResult result) { ThrowingWriter resultWriter; if (type.isAbstractData() && type.isStrictSubtypeOf(RascalValueFactory.Tree) && !type.isBottom()) { - resultWriter = (w, sw) -> { + resultWriter = (w, sw, _u) -> { w.write("(" + type.toString() +") `"); TreeAdapter.yield((IConstructor)value, sw == ansiIndentedPrinter, w); w.write("`"); }; } else if (type.isString()) { - resultWriter = (w, sw) -> { + resultWriter = (w, sw, u) -> { // TODO: do something special for the reader version of IString, when that is released // for now, we only support write @@ -103,7 +117,7 @@ else if (type.isString()) { // "Self-suppression not permitted" } w.println(); - w.println("---"); + printShortLine(w, u); try (Writer wrt = new LimitedWriter(new LimitedLineWriter(w, LINE_LIMIT), CHAR_LIMIT)) { ((IString) value).write(wrt); } @@ -113,11 +127,11 @@ else if (type.isString()) { // "Self-suppression not permitted" } w.println(); - w.print("---"); + printShortLine(w, u); }; } else { - resultWriter = (w, sw) -> { + resultWriter = (w, sw, _u) -> { try (Writer wrt = new LimitedWriter(new LimitedLineWriter(w, LINE_LIMIT), CHAR_LIMIT)) { sw.write(value, wrt); } @@ -129,31 +143,22 @@ else if (type.isString()) { }; } - ThrowingWriter typePrefixed = (w, sw) -> { + ThrowingWriter typePrefixed = (w, sw, u) -> { w.write(type.toString()); w.write(": "); - resultWriter.write(w, sw); + resultWriter.write(w, sw, u); w.println(); }; - return new DoubleOutput(typePrefixed); + return prettyPrinted(typePrefixed); } - private static class DoubleOutput implements IAnsiCommandOutput { - private ThrowingWriter writer; - - DoubleOutput(ThrowingWriter writer) { - this.writer = writer; + private void printShortLine(PrintWriter writer, boolean unicodeSupported) { + if (unicodeSupported) { + writer.println("───"); } - - @Override - public IOutputPrinter asAnsi() { - return new ParameterizedPrinterOutput(writer, ansiIndentedPrinter, MimeTypes.ANSI); - } - - @Override - public IOutputPrinter asPlain() { - return new ParameterizedPrinterOutput(writer, plainIndentedPrinter, MimeTypes.PLAIN_TEXT); + else { + writer.println("---"); } } @@ -185,13 +190,13 @@ private ICommandOutput serveContent(IConstructor provider) { } catch (IOException e) { - return outputError((w, sw) -> { + return outputError((w, sw, _u) -> { w.println("Could not start webserver to render html content: "); w.println(e.getMessage()); }); } catch (URISyntaxException e) { - return outputError((w, sw) -> { + return outputError((w, sw, _u) -> { w.println("Could not start build the uri: "); w.println(e.getMessage()); }); @@ -215,13 +220,17 @@ private static class HostedWebContentOutput implements IWebContentOutput, IHtmlC public IOutputPrinter asPlain() { return new IOutputPrinter() { @Override - public void write(PrintWriter target) { + public void write(PrintWriter target, boolean unicodeSupported) { + if (unicodeSupported) { + target.print("🔗 "); + } target.print("Serving \'"); target.print(id); target.print("\' at |"); target.print(uri.toASCIIString()); target.println("|"); } + @Override public String mimeType() { return MimeTypes.PLAIN_TEXT; @@ -233,7 +242,7 @@ public String mimeType() { public IOutputPrinter asHtml() { return new IOutputPrinter() { @Override - public void write(PrintWriter target) { + public void write(PrintWriter target, boolean unicodeSupported) { target.print("", MimeTypes.HTML); + } + + @Override + public IOutputPrinter asPlain() { + return new AsciiStringOutputPrinter("Serving visual content at |" + URL + "|", MimeTypes.PLAIN_TEXT); + } + + @Override + public URI webUri() { + return URL; + } + + @Override + public String webTitle() { + // TODO: extract from ADT + return id; + } + + @Override + public int webviewColumn() { + // TODO: extract from ADT + return 1; + } + + }; + } + + private Function liftProviderFunction(IValue callback) { + IFunction func = (IFunction) callback; + + return (t) -> { + // This function will be called from another thread (the webserver) + // That is problematic if the current repl is doing something else at that time. + // The evaluator is already locked by the outer Rascal REPL (if this REPL was started from `startREPL`). + // synchronized(eval) { + return func.call(t); + }; + } + + private ICommandOutput handleJSONResponse(IConstructor response) { + IValue data = response.get("val"); + IWithKeywordParameters kws = response.asWithKeywordParameters(); + + IValue dtf = kws.getParameter("dateTimeFormat"); + IValue dai = kws.getParameter("dateTimeAsInt"); + + JsonValueWriter writer = new JsonValueWriter() + .setCalendarFormat(dtf != null ? ((IString) dtf).getValue() : "yyyy-MM-dd\'T\'HH:mm:ss\'Z\'") + .setDatesAsInt(dai != null ? ((IBool) dai).getValue() : true); + + return new ICommandOutput() { + @Override + public IOutputPrinter asPlain() { + return new IOutputPrinter() { + @Override + public void write(PrintWriter target, boolean unicodeSupported) { + try (var json = new JsonWriter(target)) { + writer.write(json, data); + } + catch (IOException ex) { + target.println("Unexpected IO exception: " + ex); + } + } + @Override + public String mimeType() { + return "application/json"; + } + }; + } + + }; + } + + private ICommandOutput handleFileResponse(IConstructor response) + throws UnsupportedEncodingException { + IString fileMimetype = (IString) response.get("mimeType"); + ISourceLocation file = (ISourceLocation) response.get("file"); + return new ISourceLocationCommandOutput() { + @Override + public ISourceLocation asLocation() { + return file; + } + @Override + public String locationMimeType() { + return fileMimetype.getValue(); + } + + @Override + public IOutputPrinter asPlain() { + return new AsciiStringOutputPrinter("Direct file returned, REPL doesn't support file results", MimeTypes.PLAIN_TEXT); + } + + }; + } + + private ICommandOutput handlePlainTextResponse(IConstructor response) + throws UnsupportedEncodingException { + String content = ((IString) response.get("content")).getValue(); + String contentMimetype = ((IString) response.get("mimeType")).getValue(); + return () -> new AsciiStringOutputPrinter(content, contentMimetype); + } + + @Override + public boolean supportsCompletion() { + return true; + } + + + private IValue call(IFunction f, Type[] types, IValue[] args) { + if (f instanceof AbstractFunction) { + Evaluator eval = (Evaluator) ((AbstractFunction) f).getEval(); + synchronized (eval) { + try { + eval.overrideDefaultWriters(input, stdout, stderr); + return f.call(args); + } + finally { + stdout.flush(); + stderr.flush(); + eval.revertToDefaultWriters(); + } + } + } + else { + throw RuntimeExceptionFactory.illegalArgument(f, "term repl only works with interpreter for now"); + } + } + + @Override + public Map completeFragment(String line, String word) { + IMap result = (IMap)call(completor, new Type[] { tf.stringType(), tf.stringType() }, + new IValue[] { vf.string(line), vf.string(word) }); + + var resultMap = new HashMap(); + var it = result.entryIterator(); + while (it.hasNext()) { + var c = it.next(); + resultMap.put(((IString)c.getKey()).getValue(), ((IString)c.getValue()).getValue()); + } + return resultMap; + } + } +} diff --git a/src/org/rascalmpl/library/util/TermREPL.java_disabled b/src/org/rascalmpl/library/util/TermREPL.java_disabled deleted file mode 100644 index d6728583314..00000000000 --- a/src/org/rascalmpl/library/util/TermREPL.java_disabled +++ /dev/null @@ -1,339 +0,0 @@ -package org.rascalmpl.library.util; - -import java.io.ByteArrayInputStream; -import java.io.ByteArrayOutputStream; -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; -import java.io.OutputStreamWriter; -import java.io.PrintWriter; -import java.io.Reader; -import java.io.UnsupportedEncodingException; -import java.nio.charset.Charset; -import java.util.ArrayList; -import java.util.List; -import java.util.Map; -import java.util.function.Function; - - -import org.rascalmpl.exceptions.RuntimeExceptionFactory; -import org.rascalmpl.ideservices.IDEServices; -import org.rascalmpl.interpreter.Evaluator; -import org.rascalmpl.interpreter.IEvaluatorContext; -import org.rascalmpl.interpreter.result.AbstractFunction; -import org.rascalmpl.library.lang.json.internal.JsonValueWriter; -import org.rascalmpl.repl.BaseREPL; -import org.rascalmpl.repl.CompletionResult; -import org.rascalmpl.repl.ILanguageProtocol; -import org.rascalmpl.repl.REPLContentServer; -import org.rascalmpl.repl.REPLContentServerManager; -import org.rascalmpl.uri.URIResolverRegistry; -import org.rascalmpl.values.IRascalValueFactory; -import org.rascalmpl.values.functions.IFunction; - -import com.google.gson.stream.JsonWriter; - -import io.usethesource.vallang.IBool; -import io.usethesource.vallang.IConstructor; -import io.usethesource.vallang.IInteger; -import io.usethesource.vallang.IList; -import io.usethesource.vallang.ISourceLocation; -import io.usethesource.vallang.IString; -import io.usethesource.vallang.ITuple; -import io.usethesource.vallang.IValue; -import io.usethesource.vallang.IValueFactory; -import io.usethesource.vallang.IWithKeywordParameters; -import io.usethesource.vallang.type.Type; -import io.usethesource.vallang.type.TypeFactory; - -public class TermREPL { - private final IRascalValueFactory vf; - private ILanguageProtocol lang; - private final PrintWriter out; - private final PrintWriter err; - private final Reader in; - - - public TermREPL(IRascalValueFactory vf, PrintWriter out, PrintWriter err, Reader in) { - this.vf = vf; - this.out = out; - this.err = err; - this.in = in; - } - - public ITuple newREPL(IConstructor repl, IString title, IString welcome, IString prompt, IString quit, - ISourceLocation history, IFunction handler, IFunction completor, IFunction stacktrace, IEvaluatorContext eval) { - lang = new TheREPL(vf, title, welcome, prompt, quit, history, handler, completor, stacktrace, in, err, out); - BaseREPL baseRepl; - try { - baseRepl = new BaseREPL(lang, null, in, err, out, true, true, history, TerminalFactory.get(), null); - } - catch (Throwable e) { - throw RuntimeExceptionFactory.io(e.getMessage()); - } - - TypeFactory tf = TypeFactory.getInstance(); - IFunction run = vf.function(tf.functionType(tf.voidType(), tf.tupleEmpty(), tf.tupleEmpty()), - (args, kwargs) -> { - try { - baseRepl.run(); - } - catch (IOException e) { - throw RuntimeExceptionFactory.io(e.getMessage()); - } - return vf.tuple(); - }); - - IFunction send = vf.function(tf.functionType(tf.voidType(), tf.tupleType(tf.stringType()), tf.tupleEmpty()), - (args, kwargs) -> { - baseRepl.queueCommand(((IString)args[0]).getValue()); - return vf.tuple(); - }); - - return vf.tuple(run, send); - } - - public static class TheREPL implements ILanguageProtocol { - private final REPLContentServerManager contentManager = new REPLContentServerManager(); - private final TypeFactory tf = TypeFactory.getInstance(); - private PrintWriter stdout; - private PrintWriter stderr; - private Reader input; - private String currentPrompt; - private String quit; - private final AbstractFunction handler; - private final AbstractFunction completor; - private final IValueFactory vf; - private final AbstractFunction stacktrace; - - public TheREPL(IValueFactory vf, IString title, IString welcome, IString prompt, IString quit, ISourceLocation history, - IFunction handler, IFunction completor, IValue stacktrace, Reader input, PrintWriter stderr, PrintWriter stdout) { - this.vf = vf; - this.input = input; - this.stderr = stderr; - this.stdout = stdout; - - // TODO: these casts mean that TheRepl only works with functions produced by the - // interpreter for now. The reason is that the REPL needs access to environment configuration - // parameters of these functions such as stdout, stdin, etc. - // TODO: rethink the term repl in the compiled context, based on the compiled REPL for Rascal - // which does not exist yet. - this.handler = (AbstractFunction) handler; - this.completor = (AbstractFunction) completor; - this.stacktrace = (AbstractFunction) stacktrace; - this.currentPrompt = prompt.getValue(); - this.quit = quit.getValue(); - } - - @Override - public void cancelRunningCommandRequested() { - handler.getEval().interrupt(); - handler.getEval().__setInterrupt(false); - } - - @Override - public void terminateRequested() { - handler.getEval().interrupt(); - } - - @Override - public void stop() { - handler.getEval().interrupt(); - } - - @Override - public void stackTraceRequested() { - stacktrace.call(new Type[0], new IValue[0], null); - } - - @Override - public void initialize(Reader input, PrintWriter stdout, PrintWriter stderr, IDEServices services) { - this.stdout = stdout; - this.stderr = stderr; - this.input = input; - } - - @Override - public String getPrompt() { - return currentPrompt; - } - - @Override - public void handleInput(String line, Map output, Map metadata) throws InterruptedException { - - if (line.trim().equals(quit)) { - throw new InterruptedException(quit); - } - else { - try { - handler.getEval().__setInterrupt(false); - IConstructor content = (IConstructor) call(handler, new Type[] { tf.stringType() }, new IValue[] { vf.string(line) }); - - if (content.has("id")) { - handleInteractiveContent(output, metadata, content); - } - else { - IConstructor response = (IConstructor) content.get("response"); - switch (response.getName()) { - case "response": - handlePlainTextResponse(output, response); - break; - case "fileResponse": - handleFileResponse(output, response); - break; - case "jsonResponse": - handleJSONResponse(output, response); - } - } - } - catch (IOException e) { - output.put("text/plain", new ByteArrayInputStream(e.getMessage().getBytes())); - } - catch (Throwable e) { - output.put("text/plain", new ByteArrayInputStream(e.getMessage() != null ? e.getMessage().getBytes() : e.getClass().getName().getBytes())); - } - } - } - - private void handleInteractiveContent(Map output, Map metadata, - IConstructor content) throws IOException, UnsupportedEncodingException { - String id = ((IString) content.get("id")).getValue(); - Function callback = liftProviderFunction(content.get("callback")); - REPLContentServer server = contentManager.addServer(id, callback); - - String URL = "http://localhost:" + server.getListeningPort(); - - produceHTMLResponse(id, URL, output, metadata); - } - - private void produceHTMLResponse(String id, String URL, Map output, Map metadata) throws UnsupportedEncodingException{ - String html; - if (metadata.containsKey("origin") && metadata.get("origin").equals("notebook")) - html = " \n
\n
"; - else - html = ""; - - metadata.put("url", URL); - - output.put("text/html", new ByteArrayInputStream(html.getBytes("UTF8"))); - - String message = "Serving visual content at |" + URL + "|"; - output.put("text/plain", new ByteArrayInputStream(message.getBytes("UTF8"))); - - } - - private Function liftProviderFunction(IValue callback) { - IFunction func = (IFunction) callback; - - return (t) -> { - // This function will be called from another thread (the webserver) - // That is problematic if the current repl is doing something else at that time. - // The evaluator is already locked by the outer Rascal REPL (if this REPL was started from `startREPL`). - // synchronized(eval) { - return func.call(t); - }; - } - - private void handleJSONResponse(Map output, IConstructor response) throws IOException { - IValue data = response.get("val"); - IWithKeywordParameters kws = response.asWithKeywordParameters(); - - IValue dtf = kws.getParameter("dateTimeFormat"); - IValue dai = kws.getParameter("dateTimeAsInt"); - - JsonValueWriter writer = new JsonValueWriter() - .setCalendarFormat(dtf != null ? ((IString) dtf).getValue() : "yyyy-MM-dd\'T\'HH:mm:ss\'Z\'") - .setDatesAsInt(dai != null ? ((IBool) dai).getValue() : true); - - final ByteArrayOutputStream baos = new ByteArrayOutputStream(); - - JsonWriter out = new JsonWriter(new OutputStreamWriter(baos, Charset.forName("UTF8"))); - - writer.write(out, data); - out.flush(); - out.close(); - - output.put("application/json", new ByteArrayInputStream(baos.toByteArray())); - } - - private void handleFileResponse(Map output, IConstructor response) - throws UnsupportedEncodingException { - IString fileMimetype = (IString) response.get("mimeType"); - ISourceLocation file = (ISourceLocation) response.get("file"); - try { - output.put(fileMimetype.getValue(), URIResolverRegistry.getInstance().getInputStream(file)); - } - catch (IOException e) { - output.put("text/plain", new ByteArrayInputStream(e.getMessage().getBytes("UTF8"))); - } - } - - private void handlePlainTextResponse(Map output, IConstructor response) - throws UnsupportedEncodingException { - IString content = (IString) response.get("content"); - IString contentMimetype = (IString) response.get("mimeType"); - - output.put(contentMimetype.getValue(), new ByteArrayInputStream(content.getValue().getBytes("UTF8"))); - } - - @Override - public boolean supportsCompletion() { - return true; - } - - @Override - public boolean printSpaceAfterFullCompletion() { - return false; - } - - private IValue call(IFunction f, Type[] types, IValue[] args) { - if (f instanceof AbstractFunction) { - Evaluator eval = (Evaluator) ((AbstractFunction) f).getEval(); - synchronized (eval) { - try { - eval.overrideDefaultWriters(input, stdout, stderr); - return f.call(args); - } - finally { - stdout.flush(); - stderr.flush(); - eval.revertToDefaultWriters(); - } - } - } - else { - throw RuntimeExceptionFactory.illegalArgument(f, "term repl only works with interpreter for now"); - } - } - - @Override - public CompletionResult completeFragment(String line, int cursor) { - ITuple result = (ITuple)call(completor, new Type[] { tf.stringType(), tf.integerType() }, - new IValue[] { vf.string(line), vf.integer(cursor) }); - - List suggestions = new ArrayList<>(); - - for (IValue v: (IList)result.get(1)) { - suggestions.add(((IString)v).getValue()); - } - - if (suggestions.isEmpty()) { - return null; - } - - int offset = ((IInteger)result.get(0)).intValue(); - - return new CompletionResult(offset, suggestions); - } - - @Override - public void handleReset(Map output, Map metadata) throws InterruptedException { - handleInput("", output, metadata); - } - - @Override - public boolean isStatementComplete(String command) { - return true; - } - } -} diff --git a/src/org/rascalmpl/repl/BaseREPL.java b/src/org/rascalmpl/repl/BaseREPL.java index 8c71c9d7fc7..d30f8c8b5c2 100644 --- a/src/org/rascalmpl/repl/BaseREPL.java +++ b/src/org/rascalmpl/repl/BaseREPL.java @@ -69,7 +69,6 @@ public BaseREPL(IREPLService replService, Terminal term) { // todo: // - ctrl + / support (might not be possible) // - highlighting in the prompt? (future work, as it also hurts other parts) - // - nested REPLs // - measure time // - possible to tee output (future work) // - check if the REPL close properly closes the right streams diff --git a/src/org/rascalmpl/repl/output/INotebookOutput.java b/src/org/rascalmpl/repl/output/INotebookOutput.java new file mode 100644 index 00000000000..d6b2b58595f --- /dev/null +++ b/src/org/rascalmpl/repl/output/INotebookOutput.java @@ -0,0 +1,5 @@ +package org.rascalmpl.repl.output; + +public interface INotebookOutput extends IHtmlCommandOutput { + IOutputPrinter asNotebook(); +} diff --git a/src/org/rascalmpl/repl/output/ISourceLocationCommandOutput.java b/src/org/rascalmpl/repl/output/ISourceLocationCommandOutput.java new file mode 100644 index 00000000000..02c4791cb89 --- /dev/null +++ b/src/org/rascalmpl/repl/output/ISourceLocationCommandOutput.java @@ -0,0 +1,8 @@ +package org.rascalmpl.repl.output; + +import io.usethesource.vallang.ISourceLocation; + +public interface ISourceLocationCommandOutput extends ICommandOutput { + ISourceLocation asLocation(); + String locationMimeType(); +} diff --git a/src/org/rascalmpl/repl/parametric/ILanguageProtocol.java b/src/org/rascalmpl/repl/parametric/ILanguageProtocol.java index 26d921b6d0e..764446e2496 100644 --- a/src/org/rascalmpl/repl/parametric/ILanguageProtocol.java +++ b/src/org/rascalmpl/repl/parametric/ILanguageProtocol.java @@ -23,12 +23,12 @@ */ package org.rascalmpl.repl.parametric; -import java.io.InputStream; import java.io.PrintWriter; import java.io.Reader; import java.util.Map; import org.rascalmpl.ideservices.IDEServices; +import org.rascalmpl.repl.output.ICommandOutput; public interface ILanguageProtocol { @@ -53,14 +53,8 @@ public interface ILanguageProtocol { * @param metadata is a map to encode a plain object with meta-data encoded as strings * @throws InterruptedException throw this exception to stop the REPL (instead of calling .stop()) */ - void handleInput(String line, Map output, Map metadata) throws InterruptedException; + ICommandOutput 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()) - */ - void handleReset(Map output, Map metadata) throws InterruptedException; - /** * Test if completion of statement in the current line is supported * @return true if the completeFragment method can provide completions @@ -68,18 +62,12 @@ public interface ILanguageProtocol { 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 - */ - 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. + * If a user hits the TAB key, the current line and the word the cursor is at is provided, you can only provide completions for the current word. * @param line The current line. - * @param cursor The cursor offset in the line. - * @return suggestions for the line. + * @param word which word in the line the user pressed TAB on + * @return suggestions for the word (key: completion, value: category) */ - void/*CompletionResult*/ completeFragment(String line, int cursor); + Map completeFragment(String line, String word); /** * This method gets called from another thread, and indicates the user pressed CTLR-C during a call to handleInput. @@ -88,13 +76,6 @@ public interface ILanguageProtocol { */ 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. - */ - void terminateRequested(); - /** * This method gets called from another thread, indicates a user pressed CTRL+\ during a call to handleInput. * @@ -102,11 +83,4 @@ public interface ILanguageProtocol { */ void stackTraceRequested(); - public abstract boolean isStatementComplete(String command); - - /** - * Tell the language to stop without waiting for it to stop - * @throws InterruptedException - */ - void stop(); } diff --git a/src/org/rascalmpl/repl/parametric/ParametricCompleter.java b/src/org/rascalmpl/repl/parametric/ParametricCompleter.java new file mode 100644 index 00000000000..882211ba615 --- /dev/null +++ b/src/org/rascalmpl/repl/parametric/ParametricCompleter.java @@ -0,0 +1,28 @@ +package org.rascalmpl.repl.parametric; + +import java.util.List; + +import org.jline.reader.Candidate; +import org.jline.reader.Completer; +import org.jline.reader.LineReader; +import org.jline.reader.ParsedLine; + +public class ParametricCompleter implements Completer { + private final ILanguageProtocol lang; + + public ParametricCompleter(ILanguageProtocol lang) { + this.lang = lang; + } + + @Override + public void complete(LineReader reader, ParsedLine line, List candidates) { + var word = line.word(); + if (word == null) { + word = ""; + } + // TODO: in the future consider making the interface more a map of the jline3 candidate interface + lang.completeFragment(line.line(), word) + .forEach((c, g) -> candidates.add(new Candidate(c, c, g, null, null, null, false))); + } + +} diff --git a/src/org/rascalmpl/repl/parametric/ParametricReplService.java b/src/org/rascalmpl/repl/parametric/ParametricReplService.java new file mode 100644 index 00000000000..1c37b4f34e8 --- /dev/null +++ b/src/org/rascalmpl/repl/parametric/ParametricReplService.java @@ -0,0 +1,118 @@ +package org.rascalmpl.repl.parametric; + +import java.io.PrintWriter; +import java.nio.file.Path; +import java.util.Collections; +import java.util.List; + +import org.checkerframework.checker.nullness.qual.Nullable; +import org.jline.reader.Completer; +import org.jline.reader.Parser; +import org.jline.reader.impl.DefaultParser; +import org.jline.terminal.Terminal; +import org.rascalmpl.ideservices.IDEServices; +import org.rascalmpl.repl.IREPLService; +import org.rascalmpl.repl.output.ICommandOutput; +import org.rascalmpl.repl.output.IWebContentOutput; +import org.rascalmpl.repl.streams.StreamUtil; + +public class ParametricReplService implements IREPLService { + + private final ILanguageProtocol lang; + private final IDEServices ide; + private final @Nullable Path historyFile; + private PrintWriter out; + private PrintWriter err; + + public ParametricReplService(ILanguageProtocol lang, IDEServices ide, @Nullable Path historyFile) { + this.lang = lang; + this.ide = ide; + this.historyFile = historyFile; + } + + @Override + public void connect(Terminal term) { + out = term.writer(); + err = StreamUtil.generateErrorStream(term, term.writer()); + lang.initialize(term.reader(), out, err, ide); + } + + @Override + public String prompt(boolean ansiColorsSupported, boolean unicodeSupported) { + return lang.getPrompt(); + } + + @Override + public ICommandOutput handleInput(String input) throws InterruptedException { + var result = lang.handleInput(input); + if (result instanceof IWebContentOutput) { + try { + var webResult = (IWebContentOutput)result; + ide.browse(webResult.webUri(), webResult.webTitle(), webResult.webviewColumn()); + } catch (Throwable _ignored) {} + } + return result; + } + + @Override + public void handleInterrupt() throws InterruptedException { + lang.cancelRunningCommandRequested(); + } + + + @Override + public String parseErrorPrompt(boolean ansiColorSupported, boolean unicodeSupported) { + return ">> Parse error"; + } + + + @Override + public PrintWriter errorWriter() { + return err; + } + + @Override + public PrintWriter outputWriter() { + return out; + } + + @Override + public void flush() { + err.flush(); + out.flush(); + } + + @Override + public String interruptedPrompt(boolean ansiColorsSupported, boolean unicodeSupported) { + return ">> Interrupted"; + } + + + @Override + public boolean supportsCompletion() { + return lang.supportsCompletion(); + } + + @Override + public List completers() { + return Collections.singletonList(new ParametricCompleter(lang)); + } + + @Override + public Parser inputParser() { + // TODO: allow for a user to provide regexes for the + // input parser, to get better completor support and options for multiline + return new DefaultParser(); + } + + @Override + public boolean storeHistory() { + return this.historyFile == null; + } + + @Override + public Path historyFile() { + return this.historyFile; + } + +} diff --git a/src/org/rascalmpl/repl/rascal/RascalInterpreterREPL.java b/src/org/rascalmpl/repl/rascal/RascalInterpreterREPL.java index 864e34243a7..4396843f5c4 100644 --- a/src/org/rascalmpl/repl/rascal/RascalInterpreterREPL.java +++ b/src/org/rascalmpl/repl/rascal/RascalInterpreterREPL.java @@ -46,7 +46,8 @@ /** * In most cases you might want to override the {@link #buildIDEService(PrintWriter, IRascalMonitor, Terminal)} and the {@link #buildEvaluator(Reader, PrintWriter, PrintWriter, IDEServices)} functions. */ -public abstract class RascalInterpreterREPL implements IRascalLanguageProtocol { +public class RascalInterpreterREPL implements IRascalLanguageProtocol { + protected IDEServices services; protected Evaluator eval; private final RascalValuePrinter printer; @@ -104,12 +105,10 @@ protected Evaluator buildEvaluator(Reader input, PrintWriter stdout, PrintWriter return evaluator; } - protected abstract void openWebContent(IWebContentOutput webContent); - @Override public void initialize(Reader input, PrintWriter stdout, PrintWriter stderr, IRascalMonitor monitor, Terminal term) { - var services = buildIDEService(stderr, monitor, term); + services = buildIDEService(stderr, monitor, term); if (eval != null) { throw new IllegalStateException("Already initialized"); } @@ -126,7 +125,8 @@ public ICommandOutput handleInput(String command) throws InterruptedException { var result = printer.outputResult(value); if (result instanceof IWebContentOutput) { try { - openWebContent((IWebContentOutput)result); + var webResult = (IWebContentOutput)result; + services.browse(webResult.webUri(), webResult.webTitle(), webResult.webviewColumn()); } catch (Throwable _ignore) {} } return result; diff --git a/src/org/rascalmpl/repl/jline3/RascalLineParser.java b/src/org/rascalmpl/repl/rascal/RascalLineParser.java similarity index 99% rename from src/org/rascalmpl/repl/jline3/RascalLineParser.java rename to src/org/rascalmpl/repl/rascal/RascalLineParser.java index f754f157b43..888d9a7c465 100644 --- a/src/org/rascalmpl/repl/jline3/RascalLineParser.java +++ b/src/org/rascalmpl/repl/rascal/RascalLineParser.java @@ -1,4 +1,4 @@ -package org.rascalmpl.repl.jline3; +package org.rascalmpl.repl.rascal; import java.util.ArrayList; import java.util.List; diff --git a/src/org/rascalmpl/repl/rascal/RascalReplServices.java b/src/org/rascalmpl/repl/rascal/RascalReplServices.java index 9a8c3343741..888a1d36870 100644 --- a/src/org/rascalmpl/repl/rascal/RascalReplServices.java +++ b/src/org/rascalmpl/repl/rascal/RascalReplServices.java @@ -18,7 +18,6 @@ import org.rascalmpl.repl.completers.RascalKeywordCompletion; import org.rascalmpl.repl.completers.RascalLocationCompletion; import org.rascalmpl.repl.completers.RascalModuleCompletion; -import org.rascalmpl.repl.jline3.RascalLineParser; import org.rascalmpl.repl.output.ICommandOutput; import org.rascalmpl.repl.streams.StreamUtil; diff --git a/src/org/rascalmpl/shell/REPLRunner.java b/src/org/rascalmpl/shell/REPLRunner.java index b85768f0451..c58f996f8cd 100644 --- a/src/org/rascalmpl/shell/REPLRunner.java +++ b/src/org/rascalmpl/shell/REPLRunner.java @@ -1,6 +1,5 @@ package org.rascalmpl.shell; -import java.awt.Desktop; import java.io.IOException; import java.io.PrintWriter; import java.io.Reader; @@ -12,7 +11,6 @@ import org.rascalmpl.ideservices.IDEServices; import org.rascalmpl.interpreter.Evaluator; import org.rascalmpl.repl.BaseREPL; -import org.rascalmpl.repl.output.IWebContentOutput; import org.rascalmpl.repl.rascal.RascalInterpreterREPL; import org.rascalmpl.repl.rascal.RascalReplServices; @@ -32,26 +30,6 @@ protected Evaluator buildEvaluator(Reader input, PrintWriter stdout, PrintWriter IDEServices services) { return ShellEvaluatorFactory.getDefaultEvaluator(input, stdout, stderr, services); } - - @Override - protected void openWebContent(IWebContentOutput webContent) { - try { - // Note that Desktop.isDesktopSupported can not be factored into a class constant because - // it may throw exceptions on headless machines which are ignored below. - if (Desktop.isDesktopSupported()) { - try { - Desktop.getDesktop().browse(webContent.webUri()); - } - catch (IOException e) { - eval.getStdErr().println("failed to display content: " + e.getMessage()); - } - } - } - catch (Throwable e) { - // we fail silently in order to support headless machines - } - - } }, getHistoryFile()), term); repl.run(); } diff --git a/test/org/rascalmpl/test/repl/JlineParserTest.java b/test/org/rascalmpl/test/repl/JlineParserTest.java index 91e082e1d44..392c0e3c712 100644 --- a/test/org/rascalmpl/test/repl/JlineParserTest.java +++ b/test/org/rascalmpl/test/repl/JlineParserTest.java @@ -6,7 +6,7 @@ import org.jline.reader.Parser.ParseContext; import org.junit.Test; import org.rascalmpl.parser.gtd.exception.ParseError; -import org.rascalmpl.repl.jline3.RascalLineParser; +import org.rascalmpl.repl.rascal.RascalLineParser; import org.rascalmpl.uri.URIUtil; public class JlineParserTest { From dc1ce03e664e427615bd455aeaf7d57d94a2f232 Mon Sep 17 00:00:00 2001 From: Davy Landman Date: Fri, 27 Dec 2024 17:03:21 +0100 Subject: [PATCH 32/35] Supported ctrl+\ escape in the REPL --- src/org/rascalmpl/library/util/TermREPL.java | 8 -- src/org/rascalmpl/repl/BaseREPL.java | 5 +- src/org/rascalmpl/repl/IREPLService.java | 8 +- .../completers/RascalCommandCompletion.java | 8 -- .../repl/parametric/ILanguageProtocol.java | 7 -- .../parametric/ParametricReplService.java | 10 +- .../repl/rascal/RascalReplServices.java | 46 ++++++- .../rascal/RascalSpecificSignalsReader.java | 116 ++++++++++++++++++ 8 files changed, 178 insertions(+), 30 deletions(-) create mode 100644 src/org/rascalmpl/repl/rascal/RascalSpecificSignalsReader.java diff --git a/src/org/rascalmpl/library/util/TermREPL.java b/src/org/rascalmpl/library/util/TermREPL.java index 4a94be3c783..6dec31a27c2 100644 --- a/src/org/rascalmpl/library/util/TermREPL.java +++ b/src/org/rascalmpl/library/util/TermREPL.java @@ -153,14 +153,6 @@ public void cancelRunningCommandRequested() { handler.getEval().interrupt(); } - @Override - public void stackTraceRequested() { - var stack = stacktrace.call(new Type[0], new IValue[0], null); - if (stack != null && stack.getDynamicType() == tf.stringType()) { - stderr.println(((IString)stack.getValue()).getValue()); - } - } - @Override public String getPrompt() { return currentPrompt; diff --git a/src/org/rascalmpl/repl/BaseREPL.java b/src/org/rascalmpl/repl/BaseREPL.java index d30f8c8b5c2..0777bea1c83 100644 --- a/src/org/rascalmpl/repl/BaseREPL.java +++ b/src/org/rascalmpl/repl/BaseREPL.java @@ -77,7 +77,7 @@ public BaseREPL(IREPLService replService, Terminal term) { public void run() throws IOException { try { - replService.connect(term); + replService.connect(term, ansiColorsSupported, unicodeSupported); var running = setupInterruptHandler(); while (keepRunning) { @@ -129,6 +129,9 @@ public void run() throws IOException { try { replService.flush(); } catch (Throwable _t) { /* ignore */ } + try { + replService.disconnect(); + } catch (Throwable _t) { /* ignore */ } term.flush(); if (this.history != null) { ShutdownHooks.remove(this.history::save); diff --git a/src/org/rascalmpl/repl/IREPLService.java b/src/org/rascalmpl/repl/IREPLService.java index b9f9b0c8f8e..4459fd937a4 100644 --- a/src/org/rascalmpl/repl/IREPLService.java +++ b/src/org/rascalmpl/repl/IREPLService.java @@ -80,7 +80,12 @@ default Path historyFile() { * Connect the REPL to the Terminal, most likely want to take a copy of at least the {@link Terminal#writer()}. * @param term */ - void connect(Terminal term); + void connect(Terminal term, boolean ansiColorSupported, boolean unicodeSupported ); + + /** + * The REPL is getting terminated/disconnected + */ + void disconnect(); /** * if a REPL service has wrapped the writer for error output, return that instance @@ -99,5 +104,4 @@ default Path historyFile() { void flush(); String interruptedPrompt(boolean ansiColorsSupported, boolean unicodeSupported); - } diff --git a/src/org/rascalmpl/repl/completers/RascalCommandCompletion.java b/src/org/rascalmpl/repl/completers/RascalCommandCompletion.java index 48a995f9f64..da7738d45d0 100644 --- a/src/org/rascalmpl/repl/completers/RascalCommandCompletion.java +++ b/src/org/rascalmpl/repl/completers/RascalCommandCompletion.java @@ -1,23 +1,15 @@ package org.rascalmpl.repl.completers; -import java.util.ArrayList; -import java.util.Collection; import java.util.List; import java.util.NavigableMap; -import java.util.SortedSet; import java.util.TreeMap; import java.util.function.BiConsumer; -import java.util.regex.Matcher; -import java.util.regex.Pattern; -import java.util.stream.Collectors; import org.jline.reader.Candidate; import org.jline.reader.Completer; import org.jline.reader.LineReader; import org.jline.reader.ParsedLine; -import org.rascalmpl.interpreter.utils.StringUtils; -import org.rascalmpl.interpreter.utils.StringUtils.OffsetLengthTerm; public class RascalCommandCompletion implements Completer { private static final NavigableMap COMMAND_KEYWORDS; static { diff --git a/src/org/rascalmpl/repl/parametric/ILanguageProtocol.java b/src/org/rascalmpl/repl/parametric/ILanguageProtocol.java index 764446e2496..73ae712e7e0 100644 --- a/src/org/rascalmpl/repl/parametric/ILanguageProtocol.java +++ b/src/org/rascalmpl/repl/parametric/ILanguageProtocol.java @@ -75,12 +75,5 @@ public interface ILanguageProtocol { * Interrupt the handleInput code as soon as possible, but leave stuff in a valid state. */ void cancelRunningCommandRequested(); - - /** - * This method gets called from another thread, indicates a user pressed CTRL+\ during a call to handleInput. - * - * If possible, print the current stack trace. - */ - void stackTraceRequested(); } diff --git a/src/org/rascalmpl/repl/parametric/ParametricReplService.java b/src/org/rascalmpl/repl/parametric/ParametricReplService.java index 1c37b4f34e8..759b715259a 100644 --- a/src/org/rascalmpl/repl/parametric/ParametricReplService.java +++ b/src/org/rascalmpl/repl/parametric/ParametricReplService.java @@ -30,13 +30,21 @@ public ParametricReplService(ILanguageProtocol lang, IDEServices ide, @Nullable this.historyFile = historyFile; } + @Override - public void connect(Terminal term) { + public void connect(Terminal term, boolean ansiColorSupported, boolean unicodeSupported) { out = term.writer(); err = StreamUtil.generateErrorStream(term, term.writer()); lang.initialize(term.reader(), out, err, ide); } + @Override + public void disconnect() { + if (err != null) { + err.close(); + } + } + @Override public String prompt(boolean ansiColorsSupported, boolean unicodeSupported) { return lang.getPrompt(); diff --git a/src/org/rascalmpl/repl/rascal/RascalReplServices.java b/src/org/rascalmpl/repl/rascal/RascalReplServices.java index 888a1d36870..0d910f5d0ed 100644 --- a/src/org/rascalmpl/repl/rascal/RascalReplServices.java +++ b/src/org/rascalmpl/repl/rascal/RascalReplServices.java @@ -1,5 +1,6 @@ package org.rascalmpl.repl.rascal; +import java.io.IOException; import java.io.PrintWriter; import java.nio.file.Path; import java.util.ArrayList; @@ -18,6 +19,7 @@ import org.rascalmpl.repl.completers.RascalKeywordCompletion; import org.rascalmpl.repl.completers.RascalLocationCompletion; import org.rascalmpl.repl.completers.RascalModuleCompletion; +import org.rascalmpl.repl.output.IAnsiCommandOutput; import org.rascalmpl.repl.output.ICommandOutput; import org.rascalmpl.repl.streams.StreamUtil; @@ -25,6 +27,9 @@ public class RascalReplServices implements IREPLService { private final IRascalLanguageProtocol lang; private final @Nullable Path historyFile; + private boolean unicodeSupported = false; + private boolean ansiSupported = false; + private RascalSpecificSignalsReader in; private PrintWriter out; private PrintWriter err; @@ -34,15 +39,42 @@ public RascalReplServices(IRascalLanguageProtocol lang, @Nullable Path historyFi this.historyFile = historyFile; } + @Override - public void connect(Terminal term) { + public void connect(Terminal term, boolean ansiColorsSupported, boolean unicodeSupported) { if (out != null) { throw new IllegalStateException("Repl Service is already initialized"); } + this.unicodeSupported = unicodeSupported; + this.ansiSupported = ansiColorsSupported; var monitor = new TerminalProgressBarMonitor(term); out = monitor; err = StreamUtil.generateErrorStream(term, monitor); - lang.initialize(term.reader(), out, err, monitor, term); + in = new RascalSpecificSignalsReader(term, lang, this::printOutput); + lang.initialize(in, out, err, monitor, term); + } + + private void printOutput(ICommandOutput cmd) { + if (cmd instanceof IAnsiCommandOutput && ansiSupported) { + ((IAnsiCommandOutput)cmd).asAnsi().write(err, unicodeSupported); + } + cmd.asPlain().write(err, unicodeSupported); + } + + public void disconnect() { + if (in != null) { + try { + in.close(); + } + catch (IOException e) { + } + } + if (err != null) { + err.close(); + } + if (out != null) { + out.close(); + } } @Override @@ -52,10 +84,18 @@ public Parser inputParser() { @Override public ICommandOutput handleInput(String input) throws InterruptedException { - return lang.handleInput(input); + try { + in.startStreamMonitoring(); + return lang.handleInput(input); + } + finally { + in.pauseStreamMonitoring(); + } } + + @Override public void handleInterrupt() throws InterruptedException { lang.cancelRunningCommandRequested(); diff --git a/src/org/rascalmpl/repl/rascal/RascalSpecificSignalsReader.java b/src/org/rascalmpl/repl/rascal/RascalSpecificSignalsReader.java new file mode 100644 index 00000000000..6ea109a0eda --- /dev/null +++ b/src/org/rascalmpl/repl/rascal/RascalSpecificSignalsReader.java @@ -0,0 +1,116 @@ +package org.rascalmpl.repl.rascal; + +import java.io.IOException; +import java.io.Reader; +import java.util.function.Consumer; + +import org.jline.terminal.Terminal; +import org.jline.utils.NonBlockingReader; +import org.rascalmpl.repl.output.ICommandOutput; + +/** + *

+ * Monitor the reader stream of the terminal while a rascal command is running. + * To detect custom signals such as ctrl+\ + *

+ * + *

+ * The runner should use this reader to read from stdin, just to make sure it gets a lock first + * and the contents aren't lost while it's reading + *

+ */ +public class RascalSpecificSignalsReader extends Reader { + private final NonBlockingReader target; + private volatile boolean keepRunning = true; + private volatile boolean checkStream = false; + private final Thread streamMonitor; + private final IRascalLanguageProtocol lang; + private final Consumer printStackTrace; + + public RascalSpecificSignalsReader(Terminal term, IRascalLanguageProtocol lang, Consumer printStackTrace) { + target = term.reader(); + streamMonitor = new Thread(this::monitor, "Rascal's Input stream monitor"); + streamMonitor.setDaemon(true); + streamMonitor.start(); + this.lang = lang; + this.printStackTrace = printStackTrace; + } + + public void startStreamMonitoring() { + checkStream = true; + } + + public void pauseStreamMonitoring() { + checkStream = false; + // stop an active read/poll + target.shutdown(); + } + + private static final int EOF = -1; + private static final int TIMEOUT = -2; + private static final int CTRL_SLASH = '\\' & 0x1F; + + private void monitor() { + while (keepRunning) { + while (checkStream) { + int input; + try { + synchronized (this.lock) { + input = target.peek(1000); + } + } + catch (IOException e) { + input = TIMEOUT; + } + switch (input) { + case EOF: return; + case TIMEOUT: continue; + case CTRL_SLASH: + try { + printStackTrace.accept(lang.stackTraceRequested()); + } + catch (Throwable _ignored) {} + break; + + default: break; + } + // we got here, so let's swallow the character + if (keepRunning) { // but make sure we're not interrupted by now + synchronized (this.lock) { + try { + target.read(); + } + catch (IOException e) { + } + } + } + } + try { + Thread.sleep(1000); + } + catch (InterruptedException e) { + return; + } + } + } + + + + @Override + public int read(char[] cbuf, int off, int len) throws IOException { + synchronized (this.lock) { + return target.read(cbuf, off, len); + } + } + + @Override + public void close() throws IOException { + keepRunning = false; + pauseStreamMonitoring(); + streamMonitor.interrupt(); + synchronized (this.lock) { + target.close(); + } + } + +} From e95c18d5eb5b4671aff0b81084a36154a6130dad Mon Sep 17 00:00:00 2001 From: Davy Landman Date: Fri, 27 Dec 2024 20:34:52 +0100 Subject: [PATCH 33/35] Updated todo list --- src/org/rascalmpl/repl/BaseREPL.java | 1 - 1 file changed, 1 deletion(-) diff --git a/src/org/rascalmpl/repl/BaseREPL.java b/src/org/rascalmpl/repl/BaseREPL.java index 0777bea1c83..28b4e2e9500 100644 --- a/src/org/rascalmpl/repl/BaseREPL.java +++ b/src/org/rascalmpl/repl/BaseREPL.java @@ -67,7 +67,6 @@ public BaseREPL(IREPLService replService, Terminal term) { // todo: - // - ctrl + / support (might not be possible) // - highlighting in the prompt? (future work, as it also hurts other parts) // - measure time // - possible to tee output (future work) From f74a9cb25b8552a9c04c5ed924f03f7c0067395e Mon Sep 17 00:00:00 2001 From: Davy Landman Date: Sun, 29 Dec 2024 11:22:02 +0100 Subject: [PATCH 34/35] Refactored some names --- .../repl/TerminalProgressBarMonitor.java | 63 ++++++++++--------- 1 file changed, 33 insertions(+), 30 deletions(-) diff --git a/src/org/rascalmpl/repl/TerminalProgressBarMonitor.java b/src/org/rascalmpl/repl/TerminalProgressBarMonitor.java index 0174bdf67a8..9d15b503079 100644 --- a/src/org/rascalmpl/repl/TerminalProgressBarMonitor.java +++ b/src/org/rascalmpl/repl/TerminalProgressBarMonitor.java @@ -24,7 +24,7 @@ * This gives the console the ability to show progress almost as clearly as an IDE can with a * UI experience. * - * This class only works correctly if the actual _raw_ output stream of the terminal is wrapped + * This class only works correctly if the actual writer of the terminal is wrapped * with an object of this class. */ public class TerminalProgressBarMonitor extends PrintWriter implements IRascalMonitor { @@ -55,7 +55,7 @@ public class TerminalProgressBarMonitor extends PrintWriter implements IRascalMo /**x * Will make everything slow, but easier to spot mistakes */ - private static final boolean debug = false; + private static final boolean DEBUG = false; /** * Used to get updates to the width of the terminal @@ -72,7 +72,7 @@ public static boolean shouldWorkIn(Terminal tm) { @SuppressWarnings("resource") public TerminalProgressBarMonitor(Terminal tm) { - super(debug ? new AlwaysFlushAlwaysShowCursor(tm.writer()) : tm.writer()); + super(DEBUG ? new AlwaysFlushAlwaysShowCursor(tm.writer()) : tm.writer()); this.tm = tm; @@ -157,9 +157,12 @@ public void write(char[] n, int offset, int len) { store(n, offset, len); } else { + eraseBars(); // since we have some output to write, we now hide the bars flush(); - rawWrite(n, offset, lastNL + 1); - rawFlush(); + directWrite(n, offset, lastNL + 1); + directFlush(); + printBars(); // and print the bars back again + store(n, lastNL + 1, len - (lastNL + 1)); } } @@ -174,7 +177,7 @@ public void write(String s, int offset, int len) { */ private void flush() { if (curEnd != 0) { - rawWrite(buffer, 0, curEnd); + directWrite(buffer, 0, curEnd); curEnd = 0; } } @@ -186,7 +189,7 @@ private void flush() { public void flushLastLine() { if (curEnd != 0) { flush(); - rawWrite('\n'); + directWrite('\n'); } } @@ -201,26 +204,26 @@ private int startOfLastLine(char[] buffer, int offset, int len) { } } - private void rawPrintln(String s) { - rawWrite(s + System.lineSeparator()); + private void directPrintln(String s) { + directWrite(s + System.lineSeparator()); } - private void rawWrite(String s) { - rawWrite(s, 0, s.length()); + private void directWrite(String s) { + directWrite(s, 0, s.length()); } - private void rawWrite(int c) { + private void directWrite(int c) { super.write(c); } - private void rawWrite(char[] buf, int offset, int length) { + private void directWrite(char[] buf, int offset, int length) { super.write(buf, offset, length); } - private void rawWrite(String buf, int offset, int length) { + private void directWrite(String buf, int offset, int length) { super.write(buf, offset, length); } - private void rawFlush() { + private void directFlush() { super.flush(); } @@ -338,7 +341,7 @@ void write() { return; // robustness against very small screens. At least don't throw bounds exceptions } else if (barWidth <= 3) { // we can print the clock for good measure - rawPrintln(clock); + directPrintln(clock); return; } @@ -355,7 +358,7 @@ else if (barWidth <= 3) { // we can print the clock for good measure + " " ; - rawPrintln(line); // note this puts us one line down + directPrintln(line); // note this puts us one line down } @Override @@ -385,10 +388,10 @@ public void done() { */ private void eraseBars() { if (!bars.isEmpty()) { - rawWrite(ANSI.moveUp(bars.size())); - rawWrite(ANSI.clearToEndOfScreen()); + directWrite(ANSI.moveUp(bars.size())); + directWrite(ANSI.clearToEndOfScreen()); } - rawFlush(); + directFlush(); } /** @@ -449,7 +452,7 @@ private void printBars() { pb.write(); } - rawFlush(); + directFlush(); } @@ -477,7 +480,7 @@ private UnfinishedLine findUnfinishedLine() { @Override public synchronized void jobStart(String name, int workShare, int totalWork) { - if (bars.size() == 0) { + if (bars.isEmpty()) { // first new job, we take time to react to window resizing lineWidth = tm.getWidth(); } @@ -490,7 +493,7 @@ public synchronized void jobStart(String name, int workShare, int totalWork) { var pb = findBarByName(name); - rawWrite(ANSI.hideCursor()); + directWrite(ANSI.hideCursor()); if (pb == null) { eraseBars(); // to make room for the new bars @@ -504,8 +507,8 @@ public synchronized void jobStart(String name, int workShare, int totalWork) { pb.update(); } - rawWrite(ANSI.showCursor()); - rawFlush(); + directWrite(ANSI.showCursor()); + directFlush(); } @Override @@ -525,7 +528,7 @@ public synchronized void jobStep(String name, String message, int workShare) { public synchronized int jobEnd(String name, boolean succeeded) { var pb = findBarByName(name); - rawWrite(ANSI.hideCursor()); + directWrite(ANSI.hideCursor()); if (pb != null && --pb.nesting == -1) { eraseBars(); @@ -542,7 +545,7 @@ else if (pb != null) { pb.update(); } - rawWrite(ANSI.showCursor()); + directWrite(ANSI.showCursor()); return -1; } @@ -569,7 +572,7 @@ public synchronized void warning(String message, ISourceLocation src) { eraseBars(); } - rawPrintln(("[WARNING] " + (src != null ? (src + ": ") : "") + message)); + directPrintln(("[WARNING] " + (src != null ? (src + ": ") : "") + message)); if (!bars.isEmpty()) { printBars(); @@ -649,8 +652,8 @@ public synchronized void endAllJobs() { } try { - rawWrite(ANSI.showCursor()); - rawFlush(); + directWrite(ANSI.showCursor()); + directFlush(); out.flush(); } catch (IOException e) { From b516619a033ecea1fa7bd0f4df74cd52fc8324f3 Mon Sep 17 00:00:00 2001 From: Davy Landman Date: Sun, 29 Dec 2024 11:22:33 +0100 Subject: [PATCH 35/35] Fixed performance issues with the progress bar Mostly: only do stuff with the terminal when really needed, don't flash the cursor everytime. Don't remove the progressbar for every incoming write --- .../repl/TerminalProgressBarMonitor.java | 25 ++++++------------- 1 file changed, 8 insertions(+), 17 deletions(-) diff --git a/src/org/rascalmpl/repl/TerminalProgressBarMonitor.java b/src/org/rascalmpl/repl/TerminalProgressBarMonitor.java index 9d15b503079..63874c5d769 100644 --- a/src/org/rascalmpl/repl/TerminalProgressBarMonitor.java +++ b/src/org/rascalmpl/repl/TerminalProgressBarMonitor.java @@ -281,15 +281,17 @@ void worked(int amount, String message) { void update() { // to avoid flicker we only print if there is a new bar character to draw if (newWidth() != previousWidth) { + directWrite(ANSI.hideCursor()); stepper++; - rawWrite(ANSI.moveUp(bars.size() - bars.indexOf(this))); + directWrite(ANSI.moveUp(bars.size() - bars.indexOf(this))); write(); // this moves the cursor already one line down due to `println` int distance = bars.size() - bars.indexOf(this) - 1; if (distance > 0) { // ANSI will move 1 line even if the parameter is 0 - rawWrite(ANSI.moveDown(distance)); + directWrite(ANSI.moveDown(distance)); } - rawFlush(); + directWrite(ANSI.showCursor()); + directFlush(); } } @@ -516,11 +518,8 @@ public synchronized void jobStep(String name, String message, int workShare) { ProgressBar pb = findBarByName(name); if (pb != null) { - rawWrite(ANSI.hideCursor()); pb.worked(workShare, message); pb.update(); - rawWrite(ANSI.showCursor()); - rawFlush(); } } @@ -591,14 +590,10 @@ public synchronized void warning(String message, ISourceLocation src) { @Override public void write(String s, int off, int len) { if (!bars.isEmpty()) { - eraseBars(); - findUnfinishedLine().write(s, off, len); - - printBars(); } else { - rawWrite(s, off, len); + directWrite(s, off, len); } } /** @@ -609,14 +604,12 @@ public void write(String s, int off, int len) { @Override public synchronized void write(char[] buf, int off, int len) { if (!bars.isEmpty()) { - eraseBars(); findUnfinishedLine().write(buf, off, len); - printBars(); } else { // this must be the raw output stream // otherwise rascal prompts (which do not end in newlines) will be buffered - rawWrite(buf, off, len); + directWrite(buf, off, len); } } @@ -628,12 +621,10 @@ public synchronized void write(char[] buf, int off, int len) { @Override public synchronized void write(int c) { if (!bars.isEmpty()) { - eraseBars(); findUnfinishedLine().write(new char[] { (char) c }, 0, 1); - printBars(); } else { - rawWrite(c); + directWrite(c); } }