diff options
author | Felix Mulder <felix.mulder@gmail.com> | 2016-11-02 11:08:28 +0100 |
---|---|---|
committer | Guillaume Martres <smarter@ubuntu.com> | 2016-11-22 01:35:07 +0100 |
commit | 8a61ff432543a29234193cd1f7c14abd3f3d31a0 (patch) | |
tree | a8147561d307af862c295cfc8100d271063bb0dd /compiler/src/dotty/tools/dotc/repl | |
parent | 6a455fe6da5ff9c741d91279a2dc6fe2fb1b472f (diff) | |
download | dotty-8a61ff432543a29234193cd1f7c14abd3f3d31a0.tar.gz dotty-8a61ff432543a29234193cd1f7c14abd3f3d31a0.tar.bz2 dotty-8a61ff432543a29234193cd1f7c14abd3f3d31a0.zip |
Move compiler and compiler tests to compiler dir
Diffstat (limited to 'compiler/src/dotty/tools/dotc/repl')
25 files changed, 3569 insertions, 0 deletions
diff --git a/compiler/src/dotty/tools/dotc/repl/AbstractFileClassLoader.scala b/compiler/src/dotty/tools/dotc/repl/AbstractFileClassLoader.scala new file mode 100644 index 000000000..a3a463717 --- /dev/null +++ b/compiler/src/dotty/tools/dotc/repl/AbstractFileClassLoader.scala @@ -0,0 +1,31 @@ +package dotty.tools +package dotc +package repl + +import io.AbstractFile + +/** + * A class loader that loads files from a {@link scala.tools.nsc.io.AbstractFile}. + * + * @author Lex Spoon + */ +class AbstractFileClassLoader(root: AbstractFile, parent: ClassLoader) +extends ClassLoader(parent) +{ + override def findClass(name: String): Class[_] = { + var file: AbstractFile = root + val pathParts = name.split("[./]").toList + for (dirPart <- pathParts.init) { + file = file.lookupName(dirPart, true) + if (file == null) { + throw new ClassNotFoundException(name) + } + } + file = file.lookupName(pathParts.last+".class", false) + if (file == null) { + throw new ClassNotFoundException(name) + } + val bytes = file.toByteArray + defineClass(name, bytes, 0, bytes.length) + } +} diff --git a/compiler/src/dotty/tools/dotc/repl/AmmoniteReader.scala b/compiler/src/dotty/tools/dotc/repl/AmmoniteReader.scala new file mode 100644 index 000000000..f3b68e4b0 --- /dev/null +++ b/compiler/src/dotty/tools/dotc/repl/AmmoniteReader.scala @@ -0,0 +1,82 @@ +package dotty.tools +package dotc +package repl + +import core.Contexts._ +import ammonite.terminal._ +import LazyList._ +import Ansi.Color +import filters._ +import BasicFilters._ +import GUILikeFilters._ +import util.SourceFile +import printing.SyntaxHighlighting + +class AmmoniteReader(val interpreter: Interpreter)(implicit ctx: Context) extends InteractiveReader { + val interactive = true + + def incompleteInput(str: String): Boolean = + interpreter.delayOutputDuring(interpreter.interpret(str)) match { + case Interpreter.Incomplete => true + case _ => false + } + + val reader = new java.io.InputStreamReader(System.in) + val writer = new java.io.OutputStreamWriter(System.out) + val cutPasteFilter = ReadlineFilters.CutPasteFilter() + var history = List.empty[String] + val selectionFilter = GUILikeFilters.SelectionFilter(indent = 2) + val multilineFilter: Filter = Filter("multilineFilter") { + case TermState(lb ~: rest, b, c, _) + if (lb == 10 || lb == 13) && incompleteInput(b.mkString) => + BasicFilters.injectNewLine(b, c, rest, indent = 2) + } + + def readLine(prompt: String): String = { + val historyFilter = new HistoryFilter( + () => history.toVector, + Console.BLUE, + AnsiNav.resetForegroundColor + ) + + val allFilters = Filter.merge( + UndoFilter(), + historyFilter, + selectionFilter, + GUILikeFilters.altFilter, + GUILikeFilters.fnFilter, + ReadlineFilters.navFilter, + cutPasteFilter, + multilineFilter, + BasicFilters.all + ) + + Terminal.readLine( + Console.BLUE + prompt + Console.RESET, + reader, + writer, + allFilters, + displayTransform = (buffer, cursor) => { + val coloredBuffer = + if (ctx.useColors) SyntaxHighlighting(buffer) + else buffer + + val ansiBuffer = Ansi.Str.parse(coloredBuffer.toVector) + val (newBuffer, cursorOffset) = SelectionFilter.mangleBuffer( + selectionFilter, ansiBuffer, cursor, Ansi.Reversed.On + ) + val newNewBuffer = HistoryFilter.mangleBuffer( + historyFilter, newBuffer, cursor, + Ansi.Color.Green + ) + + (newNewBuffer, cursorOffset) + } + ) match { + case Some(res) => + history = res :: history; + res + case None => ":q" + } + } +} diff --git a/compiler/src/dotty/tools/dotc/repl/CompilingInterpreter.scala b/compiler/src/dotty/tools/dotc/repl/CompilingInterpreter.scala new file mode 100644 index 000000000..5b3669d5e --- /dev/null +++ b/compiler/src/dotty/tools/dotc/repl/CompilingInterpreter.scala @@ -0,0 +1,966 @@ +package dotty.tools +package dotc +package repl + +import java.io.{ + File, PrintWriter, PrintStream, StringWriter, Writer, OutputStream, + ByteArrayOutputStream => ByteOutputStream +} +import java.lang.{Class, ClassLoader} +import java.net.{URL, URLClassLoader} + +import scala.collection.immutable.ListSet +import scala.collection.mutable +import scala.collection.mutable.{ListBuffer, HashSet, ArrayBuffer} + +//import ast.parser.SyntaxAnalyzer +import io.{PlainFile, VirtualDirectory} +import scala.reflect.io.{PlainDirectory, Directory} +import reporting.{ConsoleReporter, Reporter} +import core.Flags +import util.{SourceFile, NameTransformer} +import io.ClassPath +import ast.Trees._ +import parsing.Parsers._ +import core._ +import dotty.tools.backend.jvm.GenBCode +import Symbols._, Types._, Contexts._, StdNames._, Names._, NameOps._ +import Decorators._ +import scala.util.control.NonFatal +import printing.SyntaxHighlighting + +/** An interpreter for Scala code which is based on the `dotc` compiler. + * + * The overall approach is based on compiling the requested code and then + * using a Java classloader and Java reflection to run the code + * and access its results. + * + * In more detail, a single compiler instance is used + * to accumulate all successfully compiled or interpreted Scala code. To + * "interpret" a line of code, the compiler generates a fresh object that + * includes the line of code and which has public definition(s) to export + * all variables defined by that code. To extract the result of an + * interpreted line to show the user, a second "result object" is created + * which imports the variables exported by the above object and then + * exports a single definition named "result". To accommodate user expressions + * that read from variables or methods defined in previous statements, "import" + * statements are used. + * + * This interpreter shares the strengths and weaknesses of using the + * full compiler-to-Java. The main strength is that interpreted code + * behaves exactly as does compiled code, including running at full speed. + * The main weakness is that redefining classes and methods is not handled + * properly, because rebinding at the Java level is technically difficult. + * + * @author Moez A. Abdel-Gawad + * @author Lex Spoon + * @author Martin Odersky + * + * @param out The output to use for diagnostics + * @param ictx The context to use for initialization of the interpreter, + * needed to access the current classpath. + */ +class CompilingInterpreter( + out: PrintWriter, + ictx: Context, + parentClassLoader: Option[ClassLoader] +) extends Compiler with Interpreter { + import ast.untpd._ + import CompilingInterpreter._ + + ictx.base.initialize()(ictx) + + /** directory to save .class files to */ + val virtualDirectory = + if (ictx.settings.d.isDefault(ictx)) new VirtualDirectory("(memory)", None) + else new PlainDirectory(new Directory(new java.io.File(ictx.settings.d.value(ictx)))) // for now, to help debugging + + /** A GenBCode phase that uses `virtualDirectory` for its output */ + private class REPLGenBCode extends GenBCode { + override def outputDir(implicit ctx: Context) = virtualDirectory + } + + /** Phases of this compiler use `REPLGenBCode` instead of `GenBCode`. */ + override def phases = Phases.replace( + classOf[GenBCode], _ => new REPLGenBCode :: Nil, super.phases) + + /** whether to print out result lines */ + private var printResults: Boolean = true + private var delayOutput: Boolean = false + + val previousOutput = ListBuffer.empty[String] + + override def lastOutput() = { + val prev = previousOutput.toList + previousOutput.clear() + prev + } + + override def delayOutputDuring[T](operation: => T): T = { + val old = delayOutput + try { + delayOutput = true + operation + } finally { + delayOutput = old + } + } + + /** Temporarily be quiet */ + override def beQuietDuring[T](operation: => T): T = { + val wasPrinting = printResults + try { + printResults = false + operation + } finally { + printResults = wasPrinting + } + } + + private def newReporter = + new ConsoleReporter(Console.in, out) { + override def printMessage(msg: String) = + if (!delayOutput) { + out.print(/*clean*/(msg) + "\n") + // Suppress clean for now for compiler messages + // Otherwise we will completely delete all references to + // line$object$ module classes. The previous interpreter did not + // have the project because the module class was written without the final `$' + // and therefore escaped the purge. We can turn this back on once + // we drop the final `$' from module classes. + out.flush() + } else { + previousOutput += (/*clean*/(msg) + "\n") + } + } + + /** the previous requests this interpreter has processed */ + private val prevRequests = new ArrayBuffer[Request]() + + /** the compiler's classpath, as URL's */ + val compilerClasspath: List[URL] = ictx.platform.classPath(ictx).asURLs + + /* A single class loader is used for all commands interpreted by this Interpreter. + It would also be possible to create a new class loader for each command + to interpret. The advantages of the current approach are: + + - Expressions are only evaluated one time. This is especially + significant for I/O, e.g. "val x = Console.readLine" + + The main disadvantage is: + + - Objects, classes, and methods cannot be rebound. Instead, definitions + shadow the old ones, and old code objects refer to the old + definitions. + */ + /** class loader used to load compiled code */ + val classLoader: ClassLoader = { + lazy val parent = new URLClassLoader(compilerClasspath.toArray, + classOf[Interpreter].getClassLoader) + + new AbstractFileClassLoader(virtualDirectory, parentClassLoader.getOrElse(parent)) + } + + // Set the current Java "context" class loader to this interpreter's class loader + Thread.currentThread.setContextClassLoader(classLoader) + + /** Parse a line into a sequence of trees. Returns None if the input is incomplete. */ + private def parse(line: String)(implicit ctx: Context): Option[List[Tree]] = { + var justNeedsMore = false + val reporter = newReporter + reporter.withIncompleteHandler { _ => _ => justNeedsMore = true } { + // simple parse: just parse it, nothing else + def simpleParse(code: String)(implicit ctx: Context): List[Tree] = { + val source = new SourceFile("<console>", code.toCharArray()) + val parser = new Parser(source) + val (selfDef, stats) = parser.templateStatSeq + stats + } + val trees = simpleParse(line)(ctx.fresh.setReporter(reporter)) + if (reporter.hasErrors) { + Some(Nil) // the result did not parse, so stop + } else if (justNeedsMore) { + None + } else { + Some(trees) + } + } + } + + /** Compile a SourceFile. Returns the root context of the run that compiled the file. + */ + def compileSources(sources: List[SourceFile])(implicit ctx: Context): Context = { + val reporter = newReporter + val run = newRun(ctx.fresh.setReporter(reporter)) + run.compileSources(sources) + run.runContext + } + + /** Compile a string. Returns true if there are no + * compilation errors, or false otherwise. + */ + def compileString(code: String)(implicit ctx: Context): Boolean = { + val runCtx = compileSources(List(new SourceFile("<script>", code.toCharArray))) + !runCtx.reporter.hasErrors + } + + override def interpret(line: String)(implicit ctx: Context): Interpreter.Result = { + // if (prevRequests.isEmpty) + // new Run(this) // initialize the compiler // (not sure this is needed) + // parse + parse(line) match { + case None => Interpreter.Incomplete + case Some(Nil) => Interpreter.Error // parse error or empty input + case Some(tree :: Nil) if tree.isTerm && !tree.isInstanceOf[Assign] => + previousOutput.clear() // clear previous error reporting + interpret(s"val $newVarName =\n$line") + case Some(trees) => + previousOutput.clear() // clear previous error reporting + val req = new Request(line, newLineName) + if (!req.compile()) + Interpreter.Error // an error happened during compilation, e.g. a type error + else { + val (resultStrings, succeeded) = req.loadAndRun() + if (delayOutput) + previousOutput ++= resultStrings.map(clean) + else if (printResults || !succeeded) + resultStrings.foreach(x => out.print(clean(x))) + if (succeeded) { + prevRequests += req + Interpreter.Success + } + else Interpreter.Error + } + } + } + + private def loadAndSetValue(objectName: String, value: AnyRef) = { + /** This terrible string is the wrapped class's full name inside the + * classloader: + * lineX$object$$iw$$iw$list$object + */ + val objName: String = List( + currentLineName + INTERPRETER_WRAPPER_SUFFIX, + INTERPRETER_IMPORT_WRAPPER, + INTERPRETER_IMPORT_WRAPPER, + objectName + ).mkString("$") + + try { + val resObj: Class[_] = Class.forName(objName, true, classLoader) + val setMethod = resObj.getDeclaredMethods.find(_.getName == "set") + + setMethod.fold(false) { method => + method.invoke(resObj, value) == null + } + } catch { + case NonFatal(_) => + // Unable to set value on object due to exception during reflection + false + } + } + + /** This bind is implemented by creating an object with a set method and a + * field `value`. The value is then set via Java reflection. + * + * Example: We want to bind a value `List(1,2,3)` to identifier `list` from + * sbt. The bind method accomplishes this by creating the following: + * {{{ + * object ContainerObjectWithUniqueID { + * var value: List[Int] = _ + * def set(x: Any) = value = x.asInstanceOf[List[Int]] + * } + * val list = ContainerObjectWithUniqueID.value + * }}} + * + * Between the object being created and the value being assigned, the value + * inside the object is set via reflection. + */ + override def bind(id: String, boundType: String, value: AnyRef)(implicit ctx: Context): Interpreter.Result = + interpret( + """ + |object %s { + | var value: %s = _ + | def set(x: Any) = value = x.asInstanceOf[%s] + |} + """.stripMargin.format(id + INTERPRETER_WRAPPER_SUFFIX, boundType, boundType) + ) match { + case Interpreter.Success if loadAndSetValue(id + INTERPRETER_WRAPPER_SUFFIX, value) => + val line = "val %s = %s.value".format(id, id + INTERPRETER_WRAPPER_SUFFIX) + interpret(line) + case Interpreter.Error | Interpreter.Incomplete => + out.println("Set failed in bind(%s, %s, %s)".format(id, boundType, value)) + Interpreter.Error + } + + /** Trait collecting info about one of the statements of an interpreter request */ + private trait StatementInfo { + /** The statement */ + def statement: Tree + + /** The names defined previously and referred to in the statement */ + def usedNames: List[Name] + + /** The names defined in the statement */ + val boundNames: List[Name] + + /** Statement is an import that contains a wildcard */ + val importsWildcard: Boolean + + /** The names imported by the statement (if it is an import clause) */ + val importedNames: Seq[Name] + + /** Statement defines an implicit calue or method */ + val definesImplicit: Boolean + } + + /** One line of code submitted by the user for interpretation */ + private class Request(val line: String, val lineName: String)(implicit ctx: Context) { + private val trees = { + val parsed = parse(line) + previousOutput.clear() // clear previous error reporting + parsed match { + case Some(ts) => ts + case None => Nil + } + } + + /** name to use for the object that will compute "line" */ + private def objectName = lineName + INTERPRETER_WRAPPER_SUFFIX + + /** name of the object that retrieves the result from the above object */ + private def resultObjectName = "RequestResult$" + objectName + + private def chooseHandler(stat: Tree): StatementHandler = stat match { + case stat: DefDef => new DefHandler(stat) + case stat: ValDef => new ValHandler(stat) + case stat: PatDef => new PatHandler(stat) + case stat @ Assign(Ident(_), _) => new AssignHandler(stat) + case stat: ModuleDef => new ModuleHandler(stat) + case stat: TypeDef if stat.isClassDef => new ClassHandler(stat) + case stat: TypeDef => new TypeAliasHandler(stat) + case stat: Import => new ImportHandler(stat) +// case DocDef(_, documented) => chooseHandler(documented) + case stat => new GenericHandler(stat) + } + + private val handlers: List[StatementHandler] = trees.map(chooseHandler) + + /** all (public) names defined by these statements */ + private val boundNames = ListSet(handlers.flatMap(_.boundNames): _*).toList + + /** list of names used by this expression */ + private val usedNames: List[Name] = handlers.flatMap(_.usedNames) + + private val (importsPreamble, importsTrailer, accessPath) = + importsCode(usedNames.toSet) + + /** Code to access a variable with the specified name */ + private def fullPath(vname: String): String = s"$objectName$accessPath.`$vname`" + + /** Code to access a variable with the specified name */ + private def fullPath(vname: Name): String = fullPath(vname.toString) + + /** the line of code to compute */ + private def toCompute = line + + /** generate the source code for the object that computes this request + * TODO Reformulate in a functional way + */ + private def objectSourceCode: String = + stringFrom { code => + // header for the wrapper object + code.println(s"object $objectName{") + code.print(importsPreamble) + code.println(toCompute) + handlers.foreach(_.extraCodeToEvaluate(this,code)) + code.println(importsTrailer) + //end the wrapper object + code.println(";}") + } + + /** Types of variables defined by this request. They are computed + after compilation of the main object */ + private var typeOf: Map[Name, String] = _ + + /** generate source code for the object that retrieves the result + from objectSourceCode */ + private def resultObjectSourceCode: String = + stringFrom(code => { + code.println(s"object $resultObjectName") + code.println("{ val result: String = {") + code.println(s"$objectName$accessPath;") // evaluate the object, to make sure its constructor is run + code.print("(\"\"") // print an initial empty string, so later code can + // uniformly be: + morestuff + handlers.foreach(_.resultExtractionCode(this, code)) + code.println("\n)}") + code.println(";}") + }) + + + /** Compile the object file. Returns whether the compilation succeeded. + * If all goes well, the "types" map is computed. */ + def compile(): Boolean = { + val compileCtx = compileSources( + List(new SourceFile("<console>", objectSourceCode.toCharArray))) + !compileCtx.reporter.hasErrors && { + this.typeOf = findTypes(compileCtx) + val resultCtx = compileSources( + List(new SourceFile("<console>", resultObjectSourceCode.toCharArray))) + !resultCtx.reporter.hasErrors + } + } + + /** Dig the types of all bound variables out of the compiler run. + * TODO: Change the interface so that we typecheck, and then transform + * directly. Treating the compiler as less of a blackbox will require + * much less magic here. + */ + private def findTypes(implicit ctx: Context): Map[Name, String] = { + def valAndVarNames = handlers.flatMap(_.valAndVarNames) + def defNames = handlers.flatMap(_.defNames) + + def getTypes(names: List[Name], nameMap: Name => Name): Map[Name, String] = { + /** the outermost wrapper object */ + val outerResObjSym: Symbol = + defn.EmptyPackageClass.info.decl(objectName.toTermName).symbol + + /** the innermost object inside the wrapper, found by + * following accessPath into the outer one. */ + val resObjSym = + (accessPath.split("\\.")).foldLeft(outerResObjSym) { (sym,str) => + if (str.isEmpty) sym + else + ctx.atPhase(ctx.typerPhase.next) { implicit ctx => + sym.info.member(str.toTermName).symbol + } + } + + names.foldLeft(Map.empty[Name,String]) { (map, name) => + val rawType = + ctx.atPhase(ctx.typerPhase.next) { implicit ctx => + resObjSym.info.member(name).info + } + + // the types are all =>T; remove the => + val cleanedType = rawType.widenExpr + + map + (name -> + ctx.atPhase(ctx.typerPhase.next) { implicit ctx => + cleanedType.show + }) + } + } + + val names1 = getTypes(valAndVarNames, n => n.toTermName.fieldName) + val names2 = getTypes(defNames, identity) + names1 ++ names2 + } + + /** Sets both System.{out,err} and Console.{out,err} to supplied + * `os: OutputStream` + */ + private def withOutput[T](os: ByteOutputStream)(op: ByteOutputStream => T) = { + val ps = new PrintStream(os) + val oldOut = System.out + val oldErr = System.err + System.setOut(ps) + System.setErr(ps) + + try { + Console.withOut(os)(Console.withErr(os)(op(os))) + } finally { + System.setOut(oldOut) + System.setErr(oldErr) + } + } + + /** load and run the code using reflection. + * @return A pair consisting of the run's result as a `List[String]`, and + * a boolean indicating whether the run succeeded without throwing + * an exception. + */ + def loadAndRun(): (List[String], Boolean) = { + val interpreterResultObject: Class[_] = + Class.forName(resultObjectName, true, classLoader) + val valMethodRes: java.lang.reflect.Method = + interpreterResultObject.getMethod("result") + try { + withOutput(new ByteOutputStream) { ps => + val rawRes = valMethodRes.invoke(interpreterResultObject).toString + val res = + if (ictx.useColors) new String(SyntaxHighlighting(rawRes).toArray) + else rawRes + val prints = ps.toString("utf-8") + val printList = if (prints != "") prints :: Nil else Nil + + if (!delayOutput) out.print(prints) + + (printList :+ res, true) + } + } catch { + case NonFatal(ex) => + def cause(ex: Throwable): Throwable = + if (ex.getCause eq null) ex else cause(ex.getCause) + val orig = cause(ex) + (stringFrom(str => orig.printStackTrace(str)) :: Nil, false) + } + } + + /** Compute imports that allow definitions from previous + * requests to be visible in a new request. Returns + * three pieces of related code as strings: + * + * 1. A _preamble_: An initial code fragment that should go before + * the code of the new request. + * + * 2. A _trailer_: A code fragment that should go after the code + * of the new request. + * + * 3. An _access path_ which can be traversed to access + * any bindings inside code wrapped by #1 and #2 . + * + * The argument is a set of Names that need to be imported. + * + * Limitations: This method is not as precise as it could be. + * (1) It does not process wildcard imports to see what exactly + * they import. + * (2) If it imports any names from a request, it imports all + * of them, which is not really necessary. + * (3) It imports multiple same-named implicits, but only the + * last one imported is actually usable. + */ + private def importsCode(wanted: Set[Name]): (String, String, String) = { + /** Narrow down the list of requests from which imports + * should be taken. Removes requests which cannot contribute + * useful imports for the specified set of wanted names. + */ + def reqsToUse: List[(Request, StatementInfo)] = { + /** Loop through a list of StatementHandlers and select + * which ones to keep. 'wanted' is the set of + * names that need to be imported. + */ + def select(reqs: List[(Request, StatementInfo)], wanted: Set[Name]): List[(Request, StatementInfo)] = { + reqs match { + case Nil => Nil + + case (req, handler) :: rest => + val keepit = + (handler.definesImplicit || + handler.importsWildcard || + handler.importedNames.exists(wanted.contains(_)) || + handler.boundNames.exists(wanted.contains(_))) + + val newWanted = + if (keepit) { + (wanted + ++ handler.usedNames + -- handler.boundNames + -- handler.importedNames) + } else { + wanted + } + + val restToKeep = select(rest, newWanted) + + if (keepit) + (req, handler) :: restToKeep + else + restToKeep + } + } + + val rhpairs = for { + req <- prevRequests.toList.reverse + handler <- req.handlers + } yield (req, handler) + + select(rhpairs, wanted).reverse + } + + val preamble = new StringBuffer + val trailingBraces = new StringBuffer + val accessPath = new StringBuffer + val impname = INTERPRETER_IMPORT_WRAPPER + val currentImps = mutable.Set[Name]() + + // add code for a new object to hold some imports + def addWrapper(): Unit = { + preamble.append("object " + impname + "{\n") + trailingBraces.append("}\n") + accessPath.append("." + impname) + currentImps.clear + } + + addWrapper() + + // loop through previous requests, adding imports + // for each one + for ((req, handler) <- reqsToUse) { + // If the user entered an import, then just use it + + // add an import wrapping level if the import might + // conflict with some other import + if (handler.importsWildcard || + currentImps.exists(handler.importedNames.contains)) + if (!currentImps.isEmpty) + addWrapper() + + if (handler.statement.isInstanceOf[Import]) + preamble.append(handler.statement.show + ";\n") + + // give wildcard imports a import wrapper all to their own + if (handler.importsWildcard) + addWrapper() + else + currentImps ++= handler.importedNames + + // For other requests, import each bound variable. + // import them explicitly instead of with _, so that + // ambiguity errors will not be generated. Also, quote + // the name of the variable, so that we don't need to + // handle quoting keywords separately. + for (imv <- handler.boundNames) { + if (currentImps.contains(imv)) + addWrapper() + preamble.append("import ") + preamble.append(req.objectName + req.accessPath + ".`" + imv + "`;\n") + currentImps += imv + } + } + + addWrapper() // Add one extra wrapper, to prevent warnings + // in the frequent case of redefining + // the value bound in the last interpreter + // request. + + (preamble.toString, trailingBraces.toString, accessPath.toString) + } + + // ------ Handlers ------------------------------------------ + + /** Class to handle one statement among all the statements included + * in a single interpreter request. + */ + private sealed abstract class StatementHandler(val statement: Tree) extends StatementInfo { + val usedNames: List[Name] = { + val ivt = new UntypedTreeAccumulator[mutable.Set[Name]] { + override def apply(ns: mutable.Set[Name], tree: Tree)(implicit ctx: Context) = + tree match { + case Ident(name) => ns += name + case _ => foldOver(ns, tree) + } + } + ivt.foldOver(HashSet(), statement).toList + } + val boundNames: List[Name] = Nil + def valAndVarNames: List[Name] = Nil + def defNames: List[Name] = Nil + val importsWildcard = false + val importedNames: Seq[Name] = Nil + val definesImplicit = statement match { + case tree: MemberDef => tree.mods.is(Flags.Implicit) + case _ => false + } + + def extraCodeToEvaluate(req: Request, code: PrintWriter) = {} + def resultExtractionCode(req: Request, code: PrintWriter) = {} + } + + private class GenericHandler(statement: Tree) extends StatementHandler(statement) + + private abstract class ValOrPatHandler(statement: Tree) + extends StatementHandler(statement) { + override val boundNames: List[Name] = _boundNames + override def valAndVarNames = boundNames + + override def resultExtractionCode(req: Request, code: PrintWriter): Unit = { + if (!shouldShowResult(req)) return + val resultExtractors = boundNames.map(name => resultExtractor(req, name)) + code.print(resultExtractors.mkString("")) + } + + private def resultExtractor(req: Request, varName: Name): String = { + val prettyName = varName.decode + val varType = string2code(req.typeOf(varName)) + val fullPath = req.fullPath(varName) + + s""" + "$prettyName: $varType = " + { + | if ($fullPath.asInstanceOf[AnyRef] != null) { + | (if ($fullPath.toString().contains('\\n')) "\\n" else "") + + | $fullPath.toString() + "\\n" + | } else { + | "null\\n" + | } + |}""".stripMargin + } + + protected def _boundNames: List[Name] + protected def shouldShowResult(req: Request): Boolean + } + + private class ValHandler(statement: ValDef) extends ValOrPatHandler(statement) { + override def _boundNames = List(statement.name) + + override def shouldShowResult(req: Request): Boolean = + !statement.mods.is(Flags.AccessFlags) && + !(isGeneratedVarName(statement.name.toString) && + req.typeOf(statement.name.encode) == "Unit") + } + + + private class PatHandler(statement: PatDef) extends ValOrPatHandler(statement) { + override def _boundNames = statement.pats.flatMap(findVariableNames) + + override def shouldShowResult(req: Request): Boolean = + !statement.mods.is(Flags.AccessFlags) + + private def findVariableNames(tree: Tree): List[Name] = tree match { + case Ident(name) if name.toString != "_" => List(name) + case _ => VariableNameFinder(Nil, tree).reverse + } + + private object VariableNameFinder extends UntypedDeepFolder[List[Name]]( + (acc: List[Name], t: Tree) => t match { + case _: BackquotedIdent => acc + case Ident(name) if name.isVariableName && name.toString != "_" => name :: acc + case Bind(name, _) if name.isVariableName => name :: acc + case _ => acc + } + ) + } + + private class DefHandler(defDef: DefDef) extends StatementHandler(defDef) { + override val boundNames = List(defDef.name) + override def defNames = boundNames + + override def resultExtractionCode(req: Request, code: PrintWriter): Unit = { + if (!defDef.mods.is(Flags.AccessFlags)) + code.print("+\"" + string2code(defDef.name.toString) + ": " + + string2code(req.typeOf(defDef.name)) + "\\n\"") + } + } + + private class AssignHandler(statement: Assign) extends StatementHandler(statement) { + val lhs = statement.lhs.asInstanceOf[Ident] // an unfortunate limitation + + val helperName = newInternalVarName().toTermName + override val valAndVarNames = List(helperName) + + override def extraCodeToEvaluate(req: Request, code: PrintWriter): Unit = { + code.println(i"val $helperName = ${statement.lhs};") + } + + /** Print out lhs instead of the generated varName */ + override def resultExtractionCode(req: Request, code: PrintWriter): Unit = { + code.print(" + \"" + lhs.show + ": " + + string2code(req.typeOf(helperName.encode)) + + " = \" + " + + string2code(req.fullPath(helperName)) + + " + \"\\n\"") + } + } + + private class ModuleHandler(module: ModuleDef) extends StatementHandler(module) { + override val boundNames = List(module.name) + + override def resultExtractionCode(req: Request, code: PrintWriter): Unit = { + code.println(" + \"defined module " + + string2code(module.name.toString) + + "\\n\"") + } + } + + private class ClassHandler(classdef: TypeDef) + extends StatementHandler(classdef) { + override val boundNames = + List(classdef.name) ::: + (if (classdef.mods.is(Flags.Case)) + List(classdef.name.toTermName) + else + Nil) + + // TODO: MemberDef.keyword does not include "trait"; + // otherwise it could be used here + def keyword: String = + if (classdef.mods.is(Flags.Trait)) "trait" else "class" + + override def resultExtractionCode(req: Request, code: PrintWriter): Unit = { + code.print( + " + \"defined " + + keyword + + " " + + string2code(classdef.name.toString) + + "\\n\"") + } + } + + private class TypeAliasHandler(typeDef: TypeDef) + extends StatementHandler(typeDef) { + override val boundNames = + if (!typeDef.mods.is(Flags.AccessFlags) && !typeDef.rhs.isInstanceOf[TypeBoundsTree]) + List(typeDef.name) + else + Nil + + override def resultExtractionCode(req: Request, code: PrintWriter): Unit = { + code.println(" + \"defined type alias " + + string2code(typeDef.name.toString) + "\\n\"") + } + } + + private class ImportHandler(imp: Import) extends StatementHandler(imp) { + override def resultExtractionCode(req: Request, code: PrintWriter): Unit = { + code.println("+ \"" + imp.show + "\\n\"") + } + + def isWildcardSelector(tree: Tree) = tree match { + case Ident(nme.USCOREkw) => true + case _ => false + } + + /** Whether this import includes a wildcard import */ + override val importsWildcard = imp.selectors.exists(isWildcardSelector) + + /** The individual names imported by this statement */ + override val importedNames: Seq[Name] = + imp.selectors.filterNot(isWildcardSelector).flatMap { + case sel: RefTree => List(sel.name.toTypeName, sel.name.toTermName) + case _ => Nil + } + } + + } // end Request + + // ------- String handling ---------------------------------- + + /** next line number to use */ + private var nextLineNo = 0 + + /** allocate a fresh line name */ + private def newLineName = { + val num = nextLineNo + nextLineNo += 1 + INTERPRETER_LINE_PREFIX + num + } + + private def currentLineName = + INTERPRETER_LINE_PREFIX + (nextLineNo - 1) + + /** next result variable number to use */ + private var nextVarNameNo = 0 + + /** allocate a fresh variable name */ + private def newVarName = { + val num = nextVarNameNo + nextVarNameNo += 1 + INTERPRETER_VAR_PREFIX + num + } + + /** next internal variable number to use */ + private var nextInternalVarNo = 0 + + /** allocate a fresh internal variable name */ + private def newInternalVarName() = { + val num = nextVarNameNo + nextVarNameNo += 1 + INTERPRETER_SYNTHVAR_PREFIX + num + } + + /** Check if a name looks like it was generated by newVarName */ + private def isGeneratedVarName(name: String): Boolean = + name.startsWith(INTERPRETER_VAR_PREFIX) && { + val suffix = name.drop(INTERPRETER_VAR_PREFIX.length) + suffix.forall(_.isDigit) + } + + /** generate a string using a routine that wants to write on a stream */ + private def stringFrom(writer: PrintWriter => Unit): String = { + val stringWriter = new StringWriter() + val stream = new NewLinePrintWriter(stringWriter) + writer(stream) + stream.close() + stringWriter.toString + } + + /** Truncate a string if it is longer than settings.maxPrintString */ + private def truncPrintString(str: String)(implicit ctx: Context): String = { + val maxpr = ctx.settings.XreplLineWidth.value + + if (maxpr <= 0) + return str + + if (str.length <= maxpr) + return str + + val trailer = "..." + if (maxpr >= trailer.length-1) + str.substring(0, maxpr-3) + trailer + "\n" + else + str.substring(0, maxpr-1) + } + + /** Clean up a string for output */ + private def clean(str: String)(implicit ctx: Context) = + truncPrintString(stripWrapperGunk(str)) +} + +/** Utility methods for the Interpreter. */ +object CompilingInterpreter { + val INTERPRETER_WRAPPER_SUFFIX = "$object" + val INTERPRETER_LINE_PREFIX = "line" + val INTERPRETER_VAR_PREFIX = "res" + val INTERPRETER_IMPORT_WRAPPER = "$iw" + val INTERPRETER_SYNTHVAR_PREFIX = "synthvar$" + + /** Delete a directory tree recursively. Use with care! + */ + private[repl] def deleteRecursively(path: File): Unit = { + path match { + case _ if !path.exists => + () + case _ if path.isDirectory => + for (p <- path.listFiles) + deleteRecursively(p) + path.delete + case _ => + path.delete + } + } + + /** Heuristically strip interpreter wrapper prefixes + * from an interpreter output string. + */ + def stripWrapperGunk(str: String): String = { + val wrapregex = "(line[0-9]+\\$object[$.])?(\\$iw[$.])*" + str.replaceAll(wrapregex, "") + } + + /** Convert a string into code that can recreate the string. + * This requires replacing all special characters by escape + * codes. It does not add the surrounding " marks. */ + def string2code(str: String): String = { + /** Convert a character to a backslash-u escape */ + def char2uescape(c: Char): String = { + var rest = c.toInt + val buf = new StringBuilder + for (i <- 1 to 4) { + buf ++= (rest % 16).toHexString + rest = rest / 16 + } + "\\" + "u" + buf.toString.reverse + } + val res = new StringBuilder + for (c <- str) { + if ("'\"\\" contains c) { + res += '\\' + res += c + } else if (!c.isControl) { + res += c + } else { + res ++= char2uescape(c) + } + } + res.toString + } +} diff --git a/compiler/src/dotty/tools/dotc/repl/ConsoleWriter.scala b/compiler/src/dotty/tools/dotc/repl/ConsoleWriter.scala new file mode 100644 index 000000000..9387f366a --- /dev/null +++ b/compiler/src/dotty/tools/dotc/repl/ConsoleWriter.scala @@ -0,0 +1,21 @@ +package dotty.tools +package dotc +package repl +import java.io.Writer + +/** A Writer that writes onto the Scala Console. + * + * @author Lex Spoon + * @version 1.0 + */ +class ConsoleWriter extends Writer { + def close = flush + + def flush = Console.flush + + def write(cbuf: Array[Char], off: Int, len: Int): Unit = + if (len > 0) + write(new String(cbuf, off, len)) + + override def write(str: String): Unit = Console.print(str) +} diff --git a/compiler/src/dotty/tools/dotc/repl/InteractiveReader.scala b/compiler/src/dotty/tools/dotc/repl/InteractiveReader.scala new file mode 100644 index 000000000..07ce23717 --- /dev/null +++ b/compiler/src/dotty/tools/dotc/repl/InteractiveReader.scala @@ -0,0 +1,20 @@ +package dotty.tools +package dotc +package repl + +import dotc.core.Contexts.Context + +/** Reads lines from an input stream */ +trait InteractiveReader { + def readLine(prompt: String): String + val interactive: Boolean +} + +/** The current Scala REPL know how to do this flexibly. + */ +object InteractiveReader { + /** Create an interactive reader */ + def createDefault(in: Interpreter)(implicit ctx: Context): InteractiveReader = { + new AmmoniteReader(in) + } +} diff --git a/compiler/src/dotty/tools/dotc/repl/Interpreter.scala b/compiler/src/dotty/tools/dotc/repl/Interpreter.scala new file mode 100644 index 000000000..edcc5b153 --- /dev/null +++ b/compiler/src/dotty/tools/dotc/repl/Interpreter.scala @@ -0,0 +1,45 @@ +package dotty.tools +package dotc +package repl + +import core.Contexts.Context + +/** This object defines the type of interpreter results */ +object Interpreter { + + /** A result from interpreting one line of input. */ + abstract sealed class Result + + /** The line was interpreted successfully. */ + case object Success extends Result + + /** The line was erroneous in some way. */ + case object Error extends Result + + /** The input was incomplete. The caller should request more input. + */ + case object Incomplete extends Result +} + +/** The exported functionality of the interpreter */ +trait Interpreter { + import Interpreter._ + + /** Interpret one line of input. All feedback, including parse errors and + * evaluation results, are printed via the context's reporter. Values + * defined are available for future interpreted strings. + */ + def interpret(line: String)(implicit ctx: Context): Result + + /** Tries to bind an id to a value, returns the outcome of trying to bind */ + def bind(id: String, boundType: String, value: AnyRef)(implicit ctx: Context): Result + + /** Suppress output during evaluation of `operation`. */ + def beQuietDuring[T](operation: => T): T + + /** Suppresses output and saves it for `lastOutput` to collect */ + def delayOutputDuring[T](operation: => T): T + + /** Gets the last output not printed immediately */ + def lastOutput(): Seq[String] +} diff --git a/compiler/src/dotty/tools/dotc/repl/InterpreterLoop.scala b/compiler/src/dotty/tools/dotc/repl/InterpreterLoop.scala new file mode 100644 index 000000000..b3ac41c55 --- /dev/null +++ b/compiler/src/dotty/tools/dotc/repl/InterpreterLoop.scala @@ -0,0 +1,210 @@ +package dotty.tools +package dotc +package repl + +import java.io.{BufferedReader, File, FileReader, PrintWriter} +import java.io.IOException +import java.lang.{ClassLoader, System} +import scala.concurrent.{Future, Await} +import scala.concurrent.duration.Duration +import reporting.Reporter +import core._ +import Contexts._ +import annotation.tailrec +import scala.concurrent.ExecutionContext.Implicits.global + +/** The interactive shell. It provides a read-eval-print loop around + * the Interpreter class. + * After instantiation, clients should call the `run` method. + * + * @author Moez A. Abdel-Gawad + * @author Lex Spoon + * @author Martin Odersky + */ +class InterpreterLoop(compiler: Compiler, config: REPL.Config)(implicit ctx: Context) { + import config._ + + val interpreter = compiler.asInstanceOf[Interpreter] + + private var in = input(interpreter) + + /** The context class loader at the time this object was created */ + protected val originalClassLoader = + Thread.currentThread.getContextClassLoader + + /** A reverse list of commands to replay if the user + * requests a :replay */ + var replayCommandsRev: List[String] = Nil + + /** A list of commands to replay if the user requests a :replay */ + def replayCommands = replayCommandsRev.reverse + + /** Record a command for replay should the user request a :replay */ + def addReplay(cmd: String) = + replayCommandsRev = cmd :: replayCommandsRev + + /** Close the interpreter */ + def closeInterpreter()(implicit ctx: Context): Unit = { + ctx.reporter.flush() + Thread.currentThread.setContextClassLoader(originalClassLoader) + } + + /** print a friendly help message */ + def printHelp(): Unit = { + printWelcome() + output.println("Type :load followed by a filename to load a Scala file.") + output.println("Type :replay to reset execution and replay all previous commands.") + output.println("Type :quit to exit the interpreter.") + } + + /** Print a welcome message */ + def printWelcome(): Unit = { + output.println(s"Welcome to Scala$version " + " (" + + System.getProperty("java.vm.name") + ", Java " + System.getProperty("java.version") + ")." ) + output.println("Type in expressions to have them evaluated.") + output.println("Type :help for more information.") + output.flush() + } + + val gitHash = ManifestInfo.attributes.getOrElse("Git-Hash", "unknown") + val version = s".next (pre-alpha, git-hash: $gitHash)" + + /** The main read-eval-print loop for the interpreter. It calls + * `command()` for each line of input. + */ + @tailrec final def repl(line: String = in.readLine(prompt)): Unit = + if (line != null) { + val (keepGoing, finalLineOpt) = command(line) + if (keepGoing) { + finalLineOpt.foreach(addReplay) + output.flush() + repl() + } + } + + /** interpret all lines from a specified file */ + def interpretAllFrom(filename: String): Unit = { + import java.nio.file.{Files, Paths} + import scala.collection.JavaConversions._ + try { + val lines = Files.readAllLines(Paths.get(filename)).mkString("\n") + output.println("Loading " + filename + "...") + output.flush + interpreter.interpret(lines) + } catch { + case _: IOException => + output.println("Error opening file: " + filename) + } + } + + /** create a new interpreter and replay all commands so far */ + def replay(): Unit = { + for (cmd <- replayCommands) { + output.println("Replaying: " + cmd) + output.flush() // because maybe cmd will have its own output + command(cmd) + output.println + } + } + + /** Run one command submitted by the user. Three values are returned: + * (1) whether to keep running, (2) the line to record for replay, + * if any. */ + def command(line: String): (Boolean, Option[String]) = { + def withFile(command: String)(action: String => Unit): Unit = { + val spaceIdx = command.indexOf(' ') + if (spaceIdx <= 0) { + output.println("That command requires a filename to be specified.") + return + } + val filename = command.substring(spaceIdx).trim + if (!new File(filename).exists) { + output.println("That file does not exist") + return + } + action(filename) + } + + val helpRegexp = ":h(e(l(p)?)?)?" + val quitRegexp = ":q(u(i(t)?)?)?" + val loadRegexp = ":l(o(a(d)?)?)?.*" + val replayRegexp = ":r(e(p(l(a(y)?)?)?)?)?.*" + val lastOutput = interpreter.lastOutput() + + var shouldReplay: Option[String] = None + + if (line.matches(helpRegexp)) + printHelp() + else if (line.matches(quitRegexp)) + return (false, None) + else if (line.matches(loadRegexp)) { + withFile(line)(f => { + interpretAllFrom(f) + shouldReplay = Some(line) + }) + } + else if (line matches replayRegexp) + replay() + else if (line startsWith ":") + output.println("Unknown command. Type :help for help.") + else + shouldReplay = lastOutput match { // don't interpret twice + case Nil => interpretStartingWith(line) + case oldRes => + oldRes foreach output.print + Some(line) + } + + (true, shouldReplay) + } + + def silentlyRun(cmds: List[String]): Unit = cmds.foreach { cmd => + interpreter.beQuietDuring(interpreter.interpret(cmd)) + } + + def silentlyBind(values: Array[(String, Any)]): Unit = values.foreach { case (id, value) => + interpreter.beQuietDuring( + interpreter.bind(id, value.asInstanceOf[AnyRef].getClass.getName, value.asInstanceOf[AnyRef])) + } + + /** Interpret expressions starting with the first line. + * Read lines until a complete compilation unit is available + * or until a syntax error has been seen. If a full unit is + * read, go ahead and interpret it. Return the full string + * to be recorded for replay, if any. + */ + def interpretStartingWith(code: String): Option[String] = + interpreter.interpret(code) match { + case Interpreter.Success => Some(code) + case _ => None + } +/* + def loadFiles(settings: Settings) { + settings match { + case settings: GenericRunnerSettings => + for (filename <- settings.loadfiles.value) { + val cmd = ":load " + filename + command(cmd) + replayCommandsRev = cmd :: replayCommandsRev + output.println() + } + case _ => + } + } +*/ + def run(): Reporter = { + // loadFiles(settings) + try { + if (!ctx.reporter.hasErrors) { // if there are already errors, no sense to continue + printWelcome() + silentlyRun(config.initialCommands) + silentlyBind(config.boundValues) + repl(in.readLine(prompt)) + silentlyRun(config.cleanupCommands) + } + } finally { + closeInterpreter() + } + ctx.reporter + } +} diff --git a/compiler/src/dotty/tools/dotc/repl/Main.scala b/compiler/src/dotty/tools/dotc/repl/Main.scala new file mode 100644 index 000000000..48ed3e788 --- /dev/null +++ b/compiler/src/dotty/tools/dotc/repl/Main.scala @@ -0,0 +1,28 @@ +package dotty.tools +package dotc +package repl + +/* This REPL was adapted from an old (2008-ish) version of the Scala + * REPL. The original version from which the adaptation was done is found in: + * + * https://github.com/odersky/legacy-svn-scala/tree/spoon + * + * The reason this version was picked instead of a more current one is that + * the older version is much smaller, therefore easier to port. It is also + * considerably less intertwined with nsc than later versions. + * + * There are a number of TODOs: + * + * - figure out why we can launch REPL only with `java`, not with `scala`. + * - make a doti command (urgent, easy) + * - create or port REPL tests (urgent, intermediate) + * - copy improvements of current Scala REPL wrt to this version + * (somewhat urgent, intermediate) + * - re-enable bindSettings (not urgent, easy, see TODO in InterpreterLoop.scala) + * - make string generation more functional (not urgent, easy) + * - better handling of ^C (not urgent, intermediate) + * - syntax highlighting (not urgent, intermediate) + * - integrate with presentation compiler for command completion (not urgent, hard) + */ +/** The main entry point of the REPL */ +object Main extends REPL diff --git a/compiler/src/dotty/tools/dotc/repl/ManifestInfo.scala b/compiler/src/dotty/tools/dotc/repl/ManifestInfo.scala new file mode 100644 index 000000000..206dccd67 --- /dev/null +++ b/compiler/src/dotty/tools/dotc/repl/ManifestInfo.scala @@ -0,0 +1,20 @@ +package dotty.tools.dotc.repl + +import java.net.JarURLConnection +import scala.collection.JavaConversions._ + +object ManifestInfo { + + val attributes: Map[String, String] = { + for { + resourceUrl <- Option(getClass.getResource(getClass.getSimpleName + ".class")) + urlConnection = resourceUrl.openConnection() if urlConnection.isInstanceOf[JarURLConnection] + manifest <- Option(urlConnection.asInstanceOf[JarURLConnection].getManifest) + } yield { + manifest.getMainAttributes.foldLeft(Map[String, String]())( + (map, attribute) => map + (attribute._1.toString -> attribute._2.toString) + ) + } + }.getOrElse(Map()) + +} diff --git a/compiler/src/dotty/tools/dotc/repl/NewLinePrintWriter.scala b/compiler/src/dotty/tools/dotc/repl/NewLinePrintWriter.scala new file mode 100644 index 000000000..8e36a0ae4 --- /dev/null +++ b/compiler/src/dotty/tools/dotc/repl/NewLinePrintWriter.scala @@ -0,0 +1,11 @@ +package dotty.tools +package dotc +package repl +import java.io.{Writer, PrintWriter} + +class NewLinePrintWriter(out: Writer, autoFlush: Boolean) +extends PrintWriter(out, autoFlush) { + def this(out: Writer) = this(out, false) + override def println(): Unit = { print("\n"); flush() } +} + diff --git a/compiler/src/dotty/tools/dotc/repl/REPL.scala b/compiler/src/dotty/tools/dotc/repl/REPL.scala new file mode 100644 index 000000000..211e3c931 --- /dev/null +++ b/compiler/src/dotty/tools/dotc/repl/REPL.scala @@ -0,0 +1,100 @@ +package dotty.tools +package dotc +package repl + +import core.Contexts.Context +import reporting.Reporter +import io.{AbstractFile, PlainFile, VirtualDirectory} +import scala.reflect.io.{PlainDirectory, Directory} +import java.io.{BufferedReader, File => JFile, FileReader, PrintWriter} +import java.net.{URL, URLClassLoader} + +/** A compiler which stays resident between runs. + * Usage: + * + * > scala dotty.tools.dotc.Resident <options> <initial files> + * + * dotc> "more options and files to compile" + * + * ... + * + * dotc> :reset // reset all options to the ones passed on the command line + * + * ... + * + * dotc> :q // quit + */ +class REPL extends Driver { + + lazy val config = new REPL.Config + + override def setup(args: Array[String], rootCtx: Context): (List[String], Context) = { + val (strs, ctx) = super.setup(args, rootCtx) + (strs, config.context(ctx)) + } + + override def newCompiler(implicit ctx: Context): Compiler = + new repl.CompilingInterpreter(config.output, ctx, config.classLoader) + + override def sourcesRequired = false + + override def doCompile(compiler: Compiler, fileNames: List[String])(implicit ctx: Context): Reporter = { + if (fileNames.isEmpty) + new InterpreterLoop(compiler, config).run() + else + ctx.error(s"don't now what to do with $fileNames%, %") + ctx.reporter + } +} + +object REPL { + class Config { + val prompt = "scala> " + val continuationPrompt = " " + val version = ".next (pre-alpha)" + + def context(ctx: Context): Context = ctx + + /** The first interpreted commands always take a couple of seconds due to + * classloading. To bridge the gap, we warm up the interpreter by letting + * it interpret at least a dummy line while waiting for the first line of + * input to be entered. + */ + val initialCommands: List[String] = + "val theAnswerToLifeInTheUniverseAndEverything = 21 * 2" :: Nil + + /** Before exiting, the interpreter will also run the cleanup commands + * issued in the variable below. This is useful if your REPL creates + * things during its run that should be dealt with before shutdown. + */ + val cleanupCommands: List[String] = Nil + + /** Initial values in the REPL can also be bound from runtime. Override + * this variable in the following manner to bind a variable at the start + * of the REPL session: + * + * {{{ + * override val boundValues = Array("exampleList" -> List(1, 1, 2, 3, 5)) + * }}} + * + * This is useful if you've integrated the REPL as part of your project + * and already have objects available during runtime that you'd like to + * inspect. + */ + val boundValues: Array[(String, Any)] = Array.empty[(String, Any)] + + /** To pass a custom ClassLoader to the Dotty REPL, overwride this value */ + val classLoader: Option[ClassLoader] = None + + /** The default input reader */ + def input(in: Interpreter)(implicit ctx: Context): InteractiveReader = { + val emacsShell = System.getProperty("env.emacs", "") != "" + //println("emacsShell="+emacsShell) //debug + if (emacsShell) new SimpleReader() + else InteractiveReader.createDefault(in) + } + + /** The default output writer */ + def output: PrintWriter = new NewLinePrintWriter(new ConsoleWriter, true) + } +} diff --git a/compiler/src/dotty/tools/dotc/repl/SimpleReader.scala b/compiler/src/dotty/tools/dotc/repl/SimpleReader.scala new file mode 100644 index 000000000..5fab47bbe --- /dev/null +++ b/compiler/src/dotty/tools/dotc/repl/SimpleReader.scala @@ -0,0 +1,24 @@ +package dotty.tools +package dotc +package repl + +import java.io.{BufferedReader, PrintWriter} +import dotc.core.Contexts.Context + + +/** Reads using standard JDK API */ +class SimpleReader( + in: BufferedReader, + out: PrintWriter, + val interactive: Boolean) +extends InteractiveReader { + def this() = this(Console.in, new PrintWriter(Console.out), true) + + def readLine(prompt: String) = { + if (interactive) { + out.print(prompt) + out.flush() + } + in.readLine() + } +} diff --git a/compiler/src/dotty/tools/dotc/repl/ammonite/Ansi.scala b/compiler/src/dotty/tools/dotc/repl/ammonite/Ansi.scala new file mode 100644 index 000000000..37c4de7b5 --- /dev/null +++ b/compiler/src/dotty/tools/dotc/repl/ammonite/Ansi.scala @@ -0,0 +1,256 @@ +package dotty.tools +package dotc +package repl +package ammonite.terminal + +object Ansi { + + /** + * Represents a single, atomic ANSI escape sequence that results in a + * color, background or decoration being added to the output. + * + * @param escape the actual ANSI escape sequence corresponding to this Attr + */ + case class Attr private[Ansi](escape: Option[String], resetMask: Int, applyMask: Int) { + override def toString = escape.getOrElse("") + Console.RESET + def transform(state: Short) = ((state & ~resetMask) | applyMask).toShort + + def matches(state: Short) = (state & resetMask) == applyMask + def apply(s: Ansi.Str) = s.overlay(this, 0, s.length) + } + + object Attr { + val Reset = new Attr(Some(Console.RESET), Short.MaxValue, 0) + + /** + * Quickly convert string-colors into [[Ansi.Attr]]s + */ + val ParseMap = { + val pairs = for { + cat <- categories + color <- cat.all + str <- color.escape + } yield (str, color) + (pairs :+ (Console.RESET -> Reset)).toMap + } + } + + /** + * Represents a set of [[Ansi.Attr]]s all occupying the same bit-space + * in the state `Short` + */ + sealed abstract class Category() { + val mask: Int + val all: Seq[Attr] + lazy val bitsMap = all.map{ m => m.applyMask -> m}.toMap + def makeAttr(s: Option[String], applyMask: Int) = { + new Attr(s, mask, applyMask) + } + } + + object Color extends Category { + + val mask = 15 << 7 + val Reset = makeAttr(Some("\u001b[39m"), 0 << 7) + val Black = makeAttr(Some(Console.BLACK), 1 << 7) + val Red = makeAttr(Some(Console.RED), 2 << 7) + val Green = makeAttr(Some(Console.GREEN), 3 << 7) + val Yellow = makeAttr(Some(Console.YELLOW), 4 << 7) + val Blue = makeAttr(Some(Console.BLUE), 5 << 7) + val Magenta = makeAttr(Some(Console.MAGENTA), 6 << 7) + val Cyan = makeAttr(Some(Console.CYAN), 7 << 7) + val White = makeAttr(Some(Console.WHITE), 8 << 7) + + val all = Vector( + Reset, Black, Red, Green, Yellow, + Blue, Magenta, Cyan, White + ) + } + + object Back extends Category { + val mask = 15 << 3 + + val Reset = makeAttr(Some("\u001b[49m"), 0 << 3) + val Black = makeAttr(Some(Console.BLACK_B), 1 << 3) + val Red = makeAttr(Some(Console.RED_B), 2 << 3) + val Green = makeAttr(Some(Console.GREEN_B), 3 << 3) + val Yellow = makeAttr(Some(Console.YELLOW_B), 4 << 3) + val Blue = makeAttr(Some(Console.BLUE_B), 5 << 3) + val Magenta = makeAttr(Some(Console.MAGENTA_B), 6 << 3) + val Cyan = makeAttr(Some(Console.CYAN_B), 7 << 3) + val White = makeAttr(Some(Console.WHITE_B), 8 << 3) + + val all = Seq( + Reset, Black, Red, Green, Yellow, + Blue, Magenta, Cyan, White + ) + } + + object Bold extends Category { + val mask = 1 << 0 + val On = makeAttr(Some(Console.BOLD), 1 << 0) + val Off = makeAttr(None , 0 << 0) + val all = Seq(On, Off) + } + + object Underlined extends Category { + val mask = 1 << 1 + val On = makeAttr(Some(Console.UNDERLINED), 1 << 1) + val Off = makeAttr(None, 0 << 1) + val all = Seq(On, Off) + } + + object Reversed extends Category { + val mask = 1 << 2 + val On = makeAttr(Some(Console.REVERSED), 1 << 2) + val Off = makeAttr(None, 0 << 2) + val all = Seq(On, Off) + } + + val hardOffMask = Bold.mask | Underlined.mask | Reversed.mask + val categories = List(Color, Back, Bold, Underlined, Reversed) + + object Str { + @sharable lazy val ansiRegex = "\u001B\\[[;\\d]*m".r + + implicit def parse(raw: CharSequence): Str = { + val chars = new Array[Char](raw.length) + val colors = new Array[Short](raw.length) + var currentIndex = 0 + var currentColor = 0.toShort + + val matches = ansiRegex.findAllMatchIn(raw) + val indices = Seq(0) ++ matches.flatMap { m => Seq(m.start, m.end) } ++ Seq(raw.length) + + for { + Seq(start, end) <- indices.sliding(2).toSeq + if start != end + } { + val frag = raw.subSequence(start, end).toString + if (frag.charAt(0) == '\u001b' && Attr.ParseMap.contains(frag)) { + currentColor = Attr.ParseMap(frag).transform(currentColor) + } else { + var i = 0 + while(i < frag.length){ + chars(currentIndex) = frag(i) + colors(currentIndex) = currentColor + i += 1 + currentIndex += 1 + } + } + } + + Str(chars.take(currentIndex), colors.take(currentIndex)) + } + } + + /** + * An [[Ansi.Str]]'s `color`s array is filled with shorts, each representing + * the ANSI state of one character encoded in its bits. Each [[Attr]] belongs + * to a [[Category]] that occupies a range of bits within each short: + * + * 15 14 13 12 11 10 9 8 7 6 5 4 3 2 1 0 + * |-----------| |--------| |--------| | | |bold + * | | | | |reversed + * | | | |underlined + * | | |foreground-color + * | |background-color + * |unused + * + * + * The `0000 0000 0000 0000` short corresponds to plain text with no decoration + * + */ + type State = Short + + /** + * Encapsulates a string with associated ANSI colors and text decorations. + * + * Contains some basic string methods, as well as some ansi methods to e.g. + * apply particular colors or other decorations to particular sections of + * the [[Ansi.Str]]. [[render]] flattens it out into a `java.lang.String` + * with all the colors present as ANSI escapes. + * + */ + case class Str private(chars: Array[Char], colors: Array[State]) { + require(chars.length == colors.length) + + def ++(other: Str) = Str(chars ++ other.chars, colors ++ other.colors) + def splitAt(index: Int) = { + val (leftChars, rightChars) = chars.splitAt(index) + val (leftColors, rightColors) = colors.splitAt(index) + (new Str(leftChars, leftColors), new Str(rightChars, rightColors)) + } + + def length = chars.length + override def toString = render + + def plainText = new String(chars.toArray) + def render = { + // Pre-size StringBuilder with approximate size (ansi colors tend + // to be about 5 chars long) to avoid re-allocations during growth + val output = new StringBuilder(chars.length + colors.length * 5) + + + var currentState = 0.toShort + /** + * Emit the ansi escapes necessary to transition + * between two states, if necessary. + */ + def emitDiff(nextState: Short) = if (currentState != nextState){ + // Any of these transitions from 1 to 0 within the hardOffMask + // categories cannot be done with a single ansi escape, and need + // you to emit a RESET followed by re-building whatever ansi state + // you previous had from scratch + if ((currentState & ~nextState & hardOffMask) != 0){ + output.append(Console.RESET) + currentState = 0 + } + + var categoryIndex = 0 + while(categoryIndex < categories.length){ + val cat = categories(categoryIndex) + if ((cat.mask & currentState) != (cat.mask & nextState)){ + val attr = cat.bitsMap(nextState & cat.mask) + + if (attr.escape.isDefined) { + output.append(attr.escape.get) + } + } + categoryIndex += 1 + } + } + + var i = 0 + while(i < colors.length){ + // Emit ANSI escapes to change colors where necessary + emitDiff(colors(i)) + currentState = colors(i) + output.append(chars(i)) + i += 1 + } + + // Cap off the left-hand-side of the rendered string with any ansi escape + // codes necessary to rest the state to 0 + emitDiff(0) + output.toString + } + + /** + * Overlays the desired color over the specified range of the [[Ansi.Str]]. + */ + def overlay(overlayColor: Attr, start: Int, end: Int) = { + require(end >= start, + s"end:$end must be greater than start:$end in AnsiStr#overlay call" + ) + val colorsOut = new Array[Short](colors.length) + var i = 0 + while(i < colors.length){ + if (i >= start && i < end) colorsOut(i) = overlayColor.transform(colors(i)) + else colorsOut(i) = colors(i) + i += 1 + } + new Str(chars, colorsOut) + } + } +} diff --git a/compiler/src/dotty/tools/dotc/repl/ammonite/Filter.scala b/compiler/src/dotty/tools/dotc/repl/ammonite/Filter.scala new file mode 100644 index 000000000..9d34bb0f2 --- /dev/null +++ b/compiler/src/dotty/tools/dotc/repl/ammonite/Filter.scala @@ -0,0 +1,61 @@ +package dotty.tools +package dotc +package repl +package ammonite.terminal + +object Filter { + def apply(id: String)(f: PartialFunction[TermInfo, TermAction]): Filter = + new Filter { + val op = f.lift + def identifier = id + } + + def wrap(id: String)(f: TermInfo => Option[TermAction]): Filter = + new Filter { + val op = f + def identifier = id + } + + /** Merges multiple [[Filter]]s into one. */ + def merge(pfs: Filter*) = new Filter { + val op = (v1: TermInfo) => pfs.iterator.map(_.op(v1)).find(_.isDefined).flatten + def identifier = pfs.iterator.map(_.identifier).mkString(":") + } + + val empty = Filter.merge() +} + +/** + * The way you configure your terminal behavior; a trivial wrapper around a + * function, though you should provide a good `.toString` method to make + * debugging easier. The [[TermInfo]] and [[TermAction]] types are its + * interface to the terminal. + * + * [[Filter]]s are composed sequentially: if a filter returns `None` the next + * filter is tried, while if a filter returns `Some` that ends the cascade. + * While your `op` function interacts with the terminal purely through + * immutable case classes, the Filter itself is free to maintain its own state + * and mutate it whenever, even when returning `None` to continue the cascade. + */ +trait Filter { + val op: TermInfo => Option[TermAction] + + /** + * the `.toString` of this object, except by making it separate we force + * the implementer to provide something and stop them from accidentally + * leaving it as the meaningless default. + */ + def identifier: String + override def toString = identifier +} + +/** + * A filter as an abstract class, letting you provide a [[filter]] instead of + * an `op`, automatically providing a good `.toString` for debugging, and + * providing a reasonable "place" inside the inheriting class/object to put + * state or helpers or other logic associated with the filter. + */ +abstract class DelegateFilter() extends Filter { + def filter: Filter + val op = filter.op +} diff --git a/compiler/src/dotty/tools/dotc/repl/ammonite/FilterTools.scala b/compiler/src/dotty/tools/dotc/repl/ammonite/FilterTools.scala new file mode 100644 index 000000000..c18b6a927 --- /dev/null +++ b/compiler/src/dotty/tools/dotc/repl/ammonite/FilterTools.scala @@ -0,0 +1,80 @@ +package dotty.tools +package dotc +package repl +package ammonite.terminal + +/** + * A collection of helpers that to simpify the common case of building filters + */ +object FilterTools { + val ansiRegex = "\u001B\\[[;\\d]*." + + def offsetIndex(buffer: Vector[Char], in: Int) = { + var splitIndex = 0 + var length = 0 + + while(length < in) { + ansiRegex.r.findPrefixOf(buffer.drop(splitIndex)) match { + case None => + splitIndex += 1 + length += 1 + case Some(s) => + splitIndex += s.length + } + } + splitIndex + } + + /** + * Shorthand to construct a filter in the common case where you're + * switching on the prefix of the input stream and want to run some + * transformation on the buffer/cursor + */ + def Case(s: String) + (f: (Vector[Char], Int, TermInfo) => (Vector[Char], Int)) = new Filter { + val op = new PartialFunction[TermInfo, TermAction] { + def isDefinedAt(x: TermInfo) = { + + def rec(i: Int, c: LazyList[Int]): Boolean = { + if (i >= s.length) true + else if (c.head == s(i)) rec(i + 1, c.tail) + else false + } + rec(0, x.ts.inputs) + } + + def apply(v1: TermInfo) = { + val (buffer1, cursor1) = f(v1.ts.buffer, v1.ts.cursor, v1) + TermState( + v1.ts.inputs.dropPrefix(s.map(_.toInt)).get, + buffer1, + cursor1 + ) + } + + }.lift + def identifier = "Case" + } + + /** Shorthand for pattern matching on [[TermState]] */ + val TS = TermState + + def findChunks(b: Vector[Char], c: Int) = { + val chunks = Terminal.splitBuffer(b) + // The index of the first character in each chunk + val chunkStarts = chunks.inits.map(x => x.length + x.sum).toStream.reverse + // Index of the current chunk that contains the cursor + val chunkIndex = chunkStarts.indexWhere(_ > c) match { + case -1 => chunks.length-1 + case x => x - 1 + } + (chunks, chunkStarts, chunkIndex) + } + + def firstRow(cursor: Int, buffer: Vector[Char], width: Int) = + cursor < width && (buffer.indexOf('\n') >= cursor || buffer.indexOf('\n') == -1) + + def lastRow(cursor: Int, buffer: Vector[Char], width: Int) = + (buffer.length - cursor) < width && + (buffer.lastIndexOf('\n') < cursor || buffer.lastIndexOf('\n') == -1) +} diff --git a/compiler/src/dotty/tools/dotc/repl/ammonite/LICENSE b/compiler/src/dotty/tools/dotc/repl/ammonite/LICENSE new file mode 100644 index 000000000..b15103580 --- /dev/null +++ b/compiler/src/dotty/tools/dotc/repl/ammonite/LICENSE @@ -0,0 +1,25 @@ +License +======= + + +The MIT License (MIT) + +Copyright (c) 2014 Li Haoyi (haoyi.sg@gmail.com) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE.
\ No newline at end of file diff --git a/compiler/src/dotty/tools/dotc/repl/ammonite/Protocol.scala b/compiler/src/dotty/tools/dotc/repl/ammonite/Protocol.scala new file mode 100644 index 000000000..34d31aeca --- /dev/null +++ b/compiler/src/dotty/tools/dotc/repl/ammonite/Protocol.scala @@ -0,0 +1,30 @@ +package dotty.tools +package dotc +package repl +package ammonite.terminal + +case class TermInfo(ts: TermState, width: Int) + +sealed trait TermAction +case class Printing(ts: TermState, stdout: String) extends TermAction +case class TermState( + inputs: LazyList[Int], + buffer: Vector[Char], + cursor: Int, + msg: Ansi.Str = "" +) extends TermAction + +object TermState { + def unapply(ti: TermInfo): Option[(LazyList[Int], Vector[Char], Int, Ansi.Str)] = + TermState.unapply(ti.ts) + + def unapply(ti: TermAction): Option[(LazyList[Int], Vector[Char], Int, Ansi.Str)] = + ti match { + case ts: TermState => TermState.unapply(ts) + case _ => None + } +} + +case class ClearScreen(ts: TermState) extends TermAction +case object Exit extends TermAction +case class Result(s: String) extends TermAction diff --git a/compiler/src/dotty/tools/dotc/repl/ammonite/SpecialKeys.scala b/compiler/src/dotty/tools/dotc/repl/ammonite/SpecialKeys.scala new file mode 100644 index 000000000..d834cc10b --- /dev/null +++ b/compiler/src/dotty/tools/dotc/repl/ammonite/SpecialKeys.scala @@ -0,0 +1,81 @@ +package dotty.tools +package dotc +package repl +package ammonite.terminal + +/** + * One place to assign all the esotic control key input snippets to + * easy-to-remember names + */ +object SpecialKeys { + + /** + * Lets you easily pattern match on characters modified by ctrl, + * or convert a character into its ctrl-ed version + */ + object Ctrl { + def apply(c: Char) = (c - 96).toChar.toString + def unapply(i: Int): Option[Int] = Some(i + 96) + } + + /** + * The string value you get when you hit the alt key + */ + def Alt = "\u001b" + + + val Up = Alt+"[A" + val Down = Alt+"[B" + val Right = Alt+"[C" + val Left = Alt+"[D" + + val Home = Alt+"OH" + val End = Alt+"OF" + + // For some reason Screen makes these print different incantations + // from a normal snippet, so this causes issues like + // https://github.com/lihaoyi/Ammonite/issues/152 unless we special + // case them + val HomeScreen = Alt+"[1~" + val EndScreen = Alt+"[4~" + + val ShiftUp = Alt+"[1;2A" + val ShiftDown = Alt+"[1;2B" + val ShiftRight = Alt+"[1;2C" + val ShiftLeft = Alt+"[1;2D" + + val FnUp = Alt+"[5~" + val FnDown = Alt+"[6~" + val FnRight = Alt+"[F" + val FnLeft = Alt+"[H" + + val AltUp = Alt*2+"[A" + val AltDown = Alt*2+"[B" + val AltRight = Alt*2+"[C" + val AltLeft = Alt*2+"[D" + + val LinuxCtrlRight = Alt+"[1;5C" + val LinuxCtrlLeft = Alt+"[1;5D" + + val FnAltUp = Alt*2+"[5~" + val FnAltDown = Alt*2+"[6~" + val FnAltRight = Alt+"[1;9F" + val FnAltLeft = Alt+"[1;9H" + + // Same as fn-alt-{up, down} +// val FnShiftUp = Alt*2+"[5~" +// val FnShiftDown = Alt*2+"[6~" + val FnShiftRight = Alt+"[1;2F" + val FnShiftLeft = Alt+"[1;2H" + + val AltShiftUp = Alt+"[1;10A" + val AltShiftDown = Alt+"[1;10B" + val AltShiftRight = Alt+"[1;10C" + val AltShiftLeft = Alt+"[1;10D" + + // Same as fn-alt-{up, down} +// val FnAltShiftUp = Alt*2+"[5~" +// val FnAltShiftDown = Alt*2+"[6~" + val FnAltShiftRight = Alt+"[1;10F" + val FnAltShiftLeft = Alt+"[1;10H" +} diff --git a/compiler/src/dotty/tools/dotc/repl/ammonite/Terminal.scala b/compiler/src/dotty/tools/dotc/repl/ammonite/Terminal.scala new file mode 100644 index 000000000..4b18b38e3 --- /dev/null +++ b/compiler/src/dotty/tools/dotc/repl/ammonite/Terminal.scala @@ -0,0 +1,320 @@ +package dotty.tools +package dotc +package repl +package ammonite +package terminal + +import scala.annotation.tailrec +import scala.collection.mutable + +/** + * The core logic around a terminal; it defines the base `filters` API + * through which anything (including basic cursor-navigation and typing) + * interacts with the terminal. + * + * Maintains basic invariants, such as "cursor should always be within + * the buffer", and "ansi terminal should reflect most up to date TermState" + */ +object Terminal { + + /** + * Computes how tall a line of text is when wrapped at `width`. + * + * Even 0-character lines still take up one row! + * + * width = 2 + * 0 -> 1 + * 1 -> 1 + * 2 -> 1 + * 3 -> 2 + * 4 -> 2 + * 5 -> 3 + */ + def fragHeight(length: Int, width: Int) = math.max(1, (length - 1) / width + 1) + + def splitBuffer(buffer: Vector[Char]) = { + val frags = mutable.Buffer.empty[Int] + frags.append(0) + for(c <- buffer){ + if (c == '\n') frags.append(0) + else frags(frags.length - 1) = frags.last + 1 + } + frags + } + def calculateHeight(buffer: Vector[Char], + width: Int, + prompt: String): Seq[Int] = { + val rowLengths = splitBuffer(buffer) + + calculateHeight0(rowLengths, width - prompt.length) + } + + /** + * Given a buffer with characters and newlines, calculates how high + * the buffer is and where the cursor goes inside of it. + */ + def calculateHeight0(rowLengths: Seq[Int], + width: Int): Seq[Int] = { + val fragHeights = + rowLengths + .inits + .toVector + .reverse // We want shortest-to-longest, inits gives longest-to-shortest + .filter(_.nonEmpty) // Without the first empty prefix + .map{ x => + fragHeight( + // If the frag barely fits on one line, give it + // an extra spot for the cursor on the next line + x.last + 1, + width + ) + } +// Debug("fragHeights " + fragHeights) + fragHeights + } + + def positionCursor(cursor: Int, + rowLengths: Seq[Int], + fragHeights: Seq[Int], + width: Int) = { + var leftoverCursor = cursor + // Debug("leftoverCursor " + leftoverCursor) + var totalPreHeight = 0 + var done = false + // Don't check if the cursor exceeds the last chunk, because + // even if it does there's nowhere else for it to go + for(i <- 0 until rowLengths.length -1 if !done) { + // length of frag and the '\n' after it + val delta = rowLengths(i) + 1 + // Debug("delta " + delta) + val nextCursor = leftoverCursor - delta + if (nextCursor >= 0) { + // Debug("nextCursor " + nextCursor) + leftoverCursor = nextCursor + totalPreHeight += fragHeights(i) + }else done = true + } + + val cursorY = totalPreHeight + leftoverCursor / width + val cursorX = leftoverCursor % width + + (cursorY, cursorX) + } + + + type Action = (Vector[Char], Int) => (Vector[Char], Int) + type MsgAction = (Vector[Char], Int) => (Vector[Char], Int, String) + + + def noTransform(x: Vector[Char], i: Int) = (Ansi.Str.parse(x), i) + /** + * Blockingly reads a line from the given input stream and returns it. + * + * @param prompt The prompt to display when requesting input + * @param reader The input-stream where characters come in, e.g. System.in + * @param writer The output-stream where print-outs go, e.g. System.out + * @param filters A set of actions that can be taken depending on the input, + * @param displayTransform code to manipulate the display of the buffer and + * cursor, without actually changing the logical + * values inside them. + */ + def readLine(prompt: Prompt, + reader: java.io.Reader, + writer: java.io.Writer, + filters: Filter, + displayTransform: (Vector[Char], Int) => (Ansi.Str, Int) = noTransform) + : Option[String] = { + + /** + * Erases the previous line and re-draws it with the new buffer and + * cursor. + * + * Relies on `ups` to know how "tall" the previous line was, to go up + * and erase that many rows in the console. Performs a lot of horrific + * math all over the place, incredibly prone to off-by-ones, in order + * to at the end of the day position the cursor in the right spot. + */ + def redrawLine(buffer: Ansi.Str, + cursor: Int, + ups: Int, + rowLengths: Seq[Int], + fullPrompt: Boolean = true, + newlinePrompt: Boolean = false) = { + + + // Enable this in certain cases (e.g. cursor near the value you are + // interested into) see what's going on with all the ansi screen-cursor + // movement + def debugDelay() = if (false){ + Thread.sleep(200) + writer.flush() + } + + + val promptLine = + if (fullPrompt) prompt.full + else prompt.lastLine + + val promptWidth = if(newlinePrompt) 0 else prompt.lastLine.length + val actualWidth = width - promptWidth + + ansi.up(ups) + ansi.left(9999) + ansi.clearScreen(0) + writer.write(promptLine.toString) + if (newlinePrompt) writer.write("\n") + + // I'm not sure why this is necessary, but it seems that without it, a + // cursor that "barely" overshoots the end of a line, at the end of the + // buffer, does not properly wrap and ends up dangling off the + // right-edge of the terminal window! + // + // This causes problems later since the cursor is at the wrong X/Y, + // confusing the rest of the math and ending up over-shooting on the + // `ansi.up` calls, over-writing earlier lines. This prints a single + // space such that instead of dangling it forces the cursor onto the + // next line for-realz. If it isn't dangling the extra space is a no-op + val lineStuffer = ' ' + // Under `newlinePrompt`, we print the thing almost-verbatim, since we + // want to avoid breaking code by adding random indentation. If not, we + // are guaranteed that the lines are short, so we can indent the newlines + // without fear of wrapping + val newlineReplacement = + if (newlinePrompt) { + + Array(lineStuffer, '\n') + } else { + val indent = " " * prompt.lastLine.length + Array('\n', indent:_*) + } + + writer.write( + buffer.render.flatMap{ + case '\n' => newlineReplacement + case x => Array(x) + }.toArray + ) + writer.write(lineStuffer) + + val fragHeights = calculateHeight0(rowLengths, actualWidth) + val (cursorY, cursorX) = positionCursor( + cursor, + rowLengths, + fragHeights, + actualWidth + ) + ansi.up(fragHeights.sum - 1) + ansi.left(9999) + ansi.down(cursorY) + ansi.right(cursorX) + if (!newlinePrompt) ansi.right(prompt.lastLine.length) + + writer.flush() + } + + @tailrec + def readChar(lastState: TermState, ups: Int, fullPrompt: Boolean = true): Option[String] = { + val moreInputComing = reader.ready() + + lazy val (transformedBuffer0, cursorOffset) = displayTransform( + lastState.buffer, + lastState.cursor + ) + + lazy val transformedBuffer = transformedBuffer0 ++ lastState.msg + lazy val lastOffsetCursor = lastState.cursor + cursorOffset + lazy val rowLengths = splitBuffer( + lastState.buffer ++ lastState.msg.plainText + ) + val narrowWidth = width - prompt.lastLine.length + val newlinePrompt = rowLengths.exists(_ >= narrowWidth) + val promptWidth = if(newlinePrompt) 0 else prompt.lastLine.length + val actualWidth = width - promptWidth + val newlineUp = if (newlinePrompt) 1 else 0 + if (!moreInputComing) redrawLine( + transformedBuffer, + lastOffsetCursor, + ups, + rowLengths, + fullPrompt, + newlinePrompt + ) + + lazy val (oldCursorY, _) = positionCursor( + lastOffsetCursor, + rowLengths, + calculateHeight0(rowLengths, actualWidth), + actualWidth + ) + + def updateState(s: LazyList[Int], + b: Vector[Char], + c: Int, + msg: Ansi.Str): (Int, TermState) = { + + val newCursor = math.max(math.min(c, b.length), 0) + val nextUps = + if (moreInputComing) ups + else oldCursorY + newlineUp + + val newState = TermState(s, b, newCursor, msg) + + (nextUps, newState) + } + // `.get` because we assume that *some* filter is going to match each + // character, even if only to dump the character to the screen. If nobody + // matches the character then we can feel free to blow up + filters.op(TermInfo(lastState, actualWidth)).get match { + case Printing(TermState(s, b, c, msg), stdout) => + writer.write(stdout) + val (nextUps, newState) = updateState(s, b, c, msg) + readChar(newState, nextUps) + + case TermState(s, b, c, msg) => + val (nextUps, newState) = updateState(s, b, c, msg) + readChar(newState, nextUps, false) + + case Result(s) => + redrawLine( + transformedBuffer, lastState.buffer.length, + oldCursorY + newlineUp, rowLengths, false, newlinePrompt + ) + writer.write(10) + writer.write(13) + writer.flush() + Some(s) + case ClearScreen(ts) => + ansi.clearScreen(2) + ansi.up(9999) + ansi.left(9999) + readChar(ts, ups) + case Exit => + None + } + } + + lazy val ansi = new AnsiNav(writer) + lazy val (width, _, initialConfig) = TTY.init() + try { + readChar(TermState(LazyList.continually(reader.read()), Vector.empty, 0, ""), 0) + }finally{ + + // Don't close these! Closing these closes stdin/stdout, + // which seems to kill the entire program + + // reader.close() + // writer.close() + TTY.stty(initialConfig) + } + } +} +object Prompt { + implicit def construct(prompt: String): Prompt = { + val parsedPrompt = Ansi.Str.parse(prompt) + val index = parsedPrompt.plainText.lastIndexOf('\n') + val (_, last) = parsedPrompt.splitAt(index+1) + Prompt(parsedPrompt, last) + } +} + +case class Prompt(full: Ansi.Str, lastLine: Ansi.Str) diff --git a/compiler/src/dotty/tools/dotc/repl/ammonite/Utils.scala b/compiler/src/dotty/tools/dotc/repl/ammonite/Utils.scala new file mode 100644 index 000000000..64a2c1476 --- /dev/null +++ b/compiler/src/dotty/tools/dotc/repl/ammonite/Utils.scala @@ -0,0 +1,169 @@ +package dotty.tools +package dotc +package repl +package ammonite.terminal + +import java.io.{FileOutputStream, Writer, File => JFile} +import scala.annotation.tailrec + +/** + * Prints stuff to an ad-hoc logging file when running the repl or terminal in + * development mode + * + * Very handy for the common case where you're debugging terminal interactions + * and cannot use `println` because it will stomp all over your already messed + * up terminal state and block debugging. With [[Debug]], you can have a + * separate terminal open tailing the log file and log as verbosely as you + * want without affecting the primary terminal you're using to interact with + * Ammonite. + */ +object Debug { + lazy val debugOutput = + new FileOutputStream(new JFile("terminal/target/log")) + + def apply(s: Any) = + if (System.getProperty("ammonite-sbt-build") == "true") + debugOutput.write((System.currentTimeMillis() + "\t\t" + s + "\n").getBytes) +} + +class AnsiNav(output: Writer) { + def control(n: Int, c: Char) = output.write(s"\033[" + n + c) + + /** + * Move up `n` squares + */ + def up(n: Int) = if (n == 0) "" else control(n, 'A') + /** + * Move down `n` squares + */ + def down(n: Int) = if (n == 0) "" else control(n, 'B') + /** + * Move right `n` squares + */ + def right(n: Int) = if (n == 0) "" else control(n, 'C') + /** + * Move left `n` squares + */ + def left(n: Int) = if (n == 0) "" else control(n, 'D') + + /** + * Clear the screen + * + * n=0: clear from cursor to end of screen + * n=1: clear from cursor to start of screen + * n=2: clear entire screen + */ + def clearScreen(n: Int) = control(n, 'J') + /** + * Clear the current line + * + * n=0: clear from cursor to end of line + * n=1: clear from cursor to start of line + * n=2: clear entire line + */ + def clearLine(n: Int) = control(n, 'K') +} + +object AnsiNav { + val resetUnderline = "\u001b[24m" + val resetForegroundColor = "\u001b[39m" + val resetBackgroundColor = "\u001b[49m" +} + +object TTY { + + // Prefer standard tools. Not sure why we need to do this, but for some + // reason the version installed by gnu-coreutils blows up sometimes giving + // "unable to perform all requested operations" + val pathedTput = if (new java.io.File("/usr/bin/tput").exists()) "/usr/bin/tput" else "tput" + val pathedStty = if (new java.io.File("/bin/stty").exists()) "/bin/stty" else "stty" + + def consoleDim(s: String) = { + import sys.process._ + Seq("bash", "-c", s"$pathedTput $s 2> /dev/tty").!!.trim.toInt + } + def init() = { + stty("-a") + + val width = consoleDim("cols") + val height = consoleDim("lines") +// Debug("Initializing, Width " + width) +// Debug("Initializing, Height " + height) + val initialConfig = stty("-g").trim + stty("-icanon min 1 -icrnl -inlcr -ixon") + sttyFailTolerant("dsusp undef") + stty("-echo") + stty("intr undef") +// Debug("") + (width, height, initialConfig) + } + + private def sttyCmd(s: String) = { + import sys.process._ + Seq("bash", "-c", s"$pathedStty $s < /dev/tty"): ProcessBuilder + } + + def stty(s: String) = + sttyCmd(s).!! + /* + * Executes a stty command for which failure is expected, hence the return + * status can be non-null and errors are ignored. + * This is appropriate for `stty dsusp undef`, since it's unsupported on Linux + * (http://man7.org/linux/man-pages/man3/termios.3.html). + */ + def sttyFailTolerant(s: String) = + sttyCmd(s ++ " 2> /dev/null").! + + def restore(initialConfig: String) = { + stty(initialConfig) + } +} + +/** + * A truly-lazy implementation of scala.Stream + */ +case class LazyList[T](headThunk: () => T, tailThunk: () => LazyList[T]) { + var rendered = false + lazy val head = { + rendered = true + headThunk() + } + + lazy val tail = tailThunk() + + def dropPrefix(prefix: Seq[T]) = { + @tailrec def rec(n: Int, l: LazyList[T]): Option[LazyList[T]] = { + if (n >= prefix.length) Some(l) + else if (prefix(n) == l.head) rec(n + 1, l.tail) + else None + } + rec(0, this) + } + override def toString = { + + @tailrec def rec(l: LazyList[T], res: List[T]): List[T] = { + if (l.rendered) rec(l.tailThunk(), l.head :: res) + else res + } + s"LazyList(${(rec(this, Nil).reverse ++ Seq("...")).mkString(",")})" + } + + def ~:(other: => T) = LazyList(() => other, () => this) +} + +object LazyList { + object ~: { + def unapply[T](x: LazyList[T]) = Some((x.head, x.tail)) + } + + def continually[T](t: => T): LazyList[T] = LazyList(() => t, () =>continually(t)) + + implicit class CS(ctx: StringContext) { + val base = ctx.parts.mkString + object p { + def unapply(s: LazyList[Int]): Option[LazyList[Int]] = { + s.dropPrefix(base.map(_.toInt)) + } + } + } +} diff --git a/compiler/src/dotty/tools/dotc/repl/ammonite/filters/BasicFilters.scala b/compiler/src/dotty/tools/dotc/repl/ammonite/filters/BasicFilters.scala new file mode 100644 index 000000000..faa97c348 --- /dev/null +++ b/compiler/src/dotty/tools/dotc/repl/ammonite/filters/BasicFilters.scala @@ -0,0 +1,163 @@ +package dotty.tools +package dotc +package repl +package ammonite +package terminal +package filters + +import ammonite.terminal.FilterTools._ +import ammonite.terminal.LazyList._ +import ammonite.terminal.SpecialKeys._ +import ammonite.terminal.Filter +import ammonite.terminal._ + +/** + * Filters for simple operation of a terminal: cursor-navigation + * (including with all the modifier keys), enter/ctrl-c-exit, etc. + */ +object BasicFilters { + def all = Filter.merge( + navFilter, + exitFilter, + enterFilter, + clearFilter, + //loggingFilter, + typingFilter + ) + + def injectNewLine(b: Vector[Char], c: Int, rest: LazyList[Int], indent: Int = 0) = { + val (first, last) = b.splitAt(c) + TermState(rest, (first :+ '\n') ++ last ++ Vector.fill(indent)(' '), c + 1 + indent) + } + + def navFilter = Filter.merge( + Case(Up)((b, c, m) => moveUp(b, c, m.width)), + Case(Down)((b, c, m) => moveDown(b, c, m.width)), + Case(Right)((b, c, m) => (b, c + 1)), + Case(Left)((b, c, m) => (b, c - 1)) + ) + + def tabColumn(indent: Int, b: Vector[Char], c: Int, rest: LazyList[Int]) = { + val (chunks, chunkStarts, chunkIndex) = FilterTools.findChunks(b, c) + val chunkCol = c - chunkStarts(chunkIndex) + val spacesToInject = indent - (chunkCol % indent) + val (lhs, rhs) = b.splitAt(c) + TS(rest, lhs ++ Vector.fill(spacesToInject)(' ') ++ rhs, c + spacesToInject) + } + + def tabFilter(indent: Int): Filter = Filter("tabFilter") { + case TS(9 ~: rest, b, c, _) => tabColumn(indent, b, c, rest) + } + + def loggingFilter: Filter = Filter("loggingFilter") { + case TS(Ctrl('q') ~: rest, b, c, _) => + println("Char Display Mode Enabled! Ctrl-C to exit") + var curr = rest + while (curr.head != 3) { + println("Char " + curr.head) + curr = curr.tail + } + TS(curr, b, c) + } + + def typingFilter: Filter = Filter("typingFilter") { + case TS(p"\u001b[3~$rest", b, c, _) => +// Debug("fn-delete") + val (first, last) = b.splitAt(c) + TS(rest, first ++ last.drop(1), c) + + case TS(127 ~: rest, b, c, _) => // Backspace + val (first, last) = b.splitAt(c) + TS(rest, first.dropRight(1) ++ last, c - 1) + + case TS(char ~: rest, b, c, _) => +// Debug("NORMAL CHAR " + char) + val (first, last) = b.splitAt(c) + TS(rest, (first :+ char.toChar) ++ last, c + 1) + } + + def doEnter(b: Vector[Char], c: Int, rest: LazyList[Int]) = { + val (chunks, chunkStarts, chunkIndex) = FilterTools.findChunks(b, c) + if (chunkIndex == chunks.length - 1) Result(b.mkString) + else injectNewLine(b, c, rest) + } + + def enterFilter: Filter = Filter("enterFilter") { + case TS(13 ~: rest, b, c, _) => doEnter(b, c, rest) // Enter + case TS(10 ~: rest, b, c, _) => doEnter(b, c, rest) // Enter + case TS(10 ~: 13 ~: rest, b, c, _) => doEnter(b, c, rest) // Enter + case TS(13 ~: 10 ~: rest, b, c, _) => doEnter(b, c, rest) // Enter + } + + def exitFilter: Filter = Filter("exitFilter") { + case TS(Ctrl('c') ~: rest, b, c, _) => + Result("") + case TS(Ctrl('d') ~: rest, b, c, _) => + // only exit if the line is empty, otherwise, behave like + // "delete" (i.e. delete one char to the right) + if (b.isEmpty) Exit else { + val (first, last) = b.splitAt(c) + TS(rest, first ++ last.drop(1), c) + } + case TS(-1 ~: rest, b, c, _) => Exit // java.io.Reader.read() produces -1 on EOF + } + + def clearFilter: Filter = Filter("clearFilter") { + case TS(Ctrl('l') ~: rest, b, c, _) => ClearScreen(TS(rest, b, c)) + } + + def moveStart(b: Vector[Char], c: Int, w: Int) = { + val (_, chunkStarts, chunkIndex) = findChunks(b, c) + val currentColumn = (c - chunkStarts(chunkIndex)) % w + b -> (c - currentColumn) + } + + def moveEnd(b: Vector[Char], c: Int, w: Int) = { + val (chunks, chunkStarts, chunkIndex) = findChunks(b, c) + val currentColumn = (c - chunkStarts(chunkIndex)) % w + val c1 = chunks.lift(chunkIndex + 1) match { + case Some(next) => + val boundary = chunkStarts(chunkIndex + 1) - 1 + if ((boundary - c) > (w - currentColumn)) { + val delta= w - currentColumn + c + delta + } + else boundary + case None => + c + 1 * 9999 + } + b -> c1 + } + + def moveUpDown( + b: Vector[Char], + c: Int, + w: Int, + boundaryOffset: Int, + nextChunkOffset: Int, + checkRes: Int, + check: (Int, Int) => Boolean, + isDown: Boolean + ) = { + val (chunks, chunkStarts, chunkIndex) = findChunks(b, c) + val offset = chunkStarts(chunkIndex + boundaryOffset) + if (check(checkRes, offset)) checkRes + else chunks.lift(chunkIndex + nextChunkOffset) match { + case None => c + nextChunkOffset * 9999 + case Some(next) => + val boundary = chunkStarts(chunkIndex + boundaryOffset) + val currentColumn = (c - chunkStarts(chunkIndex)) % w + + if (isDown) boundary + math.min(currentColumn, next) + else boundary + math.min(currentColumn - next % w, 0) - 1 + } + } + + def moveUp(b: Vector[Char], c: Int, w: Int) = { + b -> moveUpDown(b, c, w, 0, -1, c - w, _ > _, false) + } + + def moveDown(b: Vector[Char], c: Int, w: Int) = { + b -> moveUpDown(b, c, w, 1, 1, c + w, _ <= _, true) + } +} diff --git a/compiler/src/dotty/tools/dotc/repl/ammonite/filters/GUILikeFilters.scala b/compiler/src/dotty/tools/dotc/repl/ammonite/filters/GUILikeFilters.scala new file mode 100644 index 000000000..69a9769c6 --- /dev/null +++ b/compiler/src/dotty/tools/dotc/repl/ammonite/filters/GUILikeFilters.scala @@ -0,0 +1,170 @@ +package dotty.tools +package dotc +package repl +package ammonite +package terminal +package filters + +import terminal.FilterTools._ +import terminal.LazyList.~: +import terminal.SpecialKeys._ +import terminal.DelegateFilter +import terminal._ + +/** + * Filters have hook into the various {Ctrl,Shift,Fn,Alt}x{Up,Down,Left,Right} + * combination keys, and make them behave similarly as they would on a normal + * GUI text editor: alt-{left, right} for word movement, hold-down-shift for + * text selection, etc. + */ +object GUILikeFilters { + case class SelectionFilter(indent: Int) extends DelegateFilter { + def identifier = "SelectionFilter" + var mark: Option[Int] = None + + def setMark(c: Int) = { + Debug("setMark\t" + mark + "\t->\t" + c) + if (mark == None) mark = Some(c) + } + + def doIndent( + b: Vector[Char], + c: Int, + rest: LazyList[Int], + slicer: Vector[Char] => Int + ) = { + + val markValue = mark.get + val (chunks, chunkStarts, chunkIndex) = FilterTools.findChunks(b, c) + val min = chunkStarts.lastIndexWhere(_ <= math.min(c, markValue)) + val max = chunkStarts.indexWhere(_ > math.max(c, markValue)) + val splitPoints = chunkStarts.slice(min, max) + val frags = (0 +: splitPoints :+ 99999).sliding(2).zipWithIndex + + var firstOffset = 0 + val broken = + for((Seq(l, r), i) <- frags) yield { + val slice = b.slice(l, r) + if (i == 0) slice + else { + val cut = slicer(slice) + + if (i == 1) firstOffset = cut + + if (cut < 0) slice.drop(-cut) + else Vector.fill(cut)(' ') ++ slice + } + } + val flattened = broken.flatten.toVector + val deeperOffset = flattened.length - b.length + + val (newMark, newC) = + if (mark.get > c) (mark.get + deeperOffset, c + firstOffset) + else (mark.get + firstOffset, c + deeperOffset) + + mark = Some(newMark) + TS(rest, flattened, newC) + } + + def filter = Filter.merge( + + Case(ShiftUp) {(b, c, m) => setMark(c); BasicFilters.moveUp(b, c, m.width)}, + Case(ShiftDown) {(b, c, m) => setMark(c); BasicFilters.moveDown(b, c, m.width)}, + Case(ShiftRight) {(b, c, m) => setMark(c); (b, c + 1)}, + Case(ShiftLeft) {(b, c, m) => setMark(c); (b, c - 1)}, + Case(AltShiftUp) {(b, c, m) => setMark(c); BasicFilters.moveUp(b, c, m.width)}, + Case(AltShiftDown) {(b, c, m) => setMark(c); BasicFilters.moveDown(b, c, m.width)}, + Case(AltShiftRight) {(b, c, m) => setMark(c); wordRight(b, c)}, + Case(AltShiftLeft) {(b, c, m) => setMark(c); wordLeft(b, c)}, + Case(FnShiftRight) {(b, c, m) => setMark(c); BasicFilters.moveEnd(b, c, m.width)}, + Case(FnShiftLeft) {(b, c, m) => setMark(c); BasicFilters.moveStart(b, c, m.width)}, + Filter("fnOtherFilter") { + case TS(27 ~: 91 ~: 90 ~: rest, b, c, _) if mark.isDefined => + doIndent(b, c, rest, + slice => -math.min(slice.iterator.takeWhile(_ == ' ').size, indent) + ) + + case TS(9 ~: rest, b, c, _) if mark.isDefined => // Tab + doIndent(b, c, rest, + slice => indent + ) + + // Intercept every other character. + case TS(char ~: inputs, buffer, cursor, _) if mark.isDefined => + // If it's a special command, just cancel the current selection. + if (char.toChar.isControl && + char != 127 /*backspace*/ && + char != 13 /*enter*/ && + char != 10 /*enter*/) { + mark = None + TS(char ~: inputs, buffer, cursor) + } else { + // If it's a printable character, delete the current + // selection and write the printable character. + val Seq(min, max) = Seq(mark.get, cursor).sorted + mark = None + val newBuffer = buffer.take(min) ++ buffer.drop(max) + val newInputs = + if (char == 127) inputs + else char ~: inputs + TS(newInputs, newBuffer, min) + } + } + ) + } + + object SelectionFilter { + def mangleBuffer( + selectionFilter: SelectionFilter, + string: Ansi.Str, + cursor: Int, + startColor: Ansi.Attr + ) = { + selectionFilter.mark match { + case Some(mark) if mark != cursor => + val Seq(min, max) = Seq(cursor, mark).sorted + val displayOffset = if (cursor < mark) 0 else -1 + val newStr = string.overlay(startColor, min, max) + (newStr, displayOffset) + case _ => (string, 0) + } + } + } + + val fnFilter = Filter.merge( + Case(FnUp)((b, c, m) => (b, c - 9999)), + Case(FnDown)((b, c, m) => (b, c + 9999)), + Case(FnRight)((b, c, m) => BasicFilters.moveEnd(b, c, m.width)), + Case(FnLeft)((b, c, m) => BasicFilters.moveStart(b, c, m.width)) + ) + val altFilter = Filter.merge( + Case(AltUp) {(b, c, m) => BasicFilters.moveUp(b, c, m.width)}, + Case(AltDown) {(b, c, m) => BasicFilters.moveDown(b, c, m.width)}, + Case(AltRight) {(b, c, m) => wordRight(b, c)}, + Case(AltLeft) {(b, c, m) => wordLeft(b, c)} + ) + + val fnAltFilter = Filter.merge( + Case(FnAltUp) {(b, c, m) => (b, c)}, + Case(FnAltDown) {(b, c, m) => (b, c)}, + Case(FnAltRight) {(b, c, m) => (b, c)}, + Case(FnAltLeft) {(b, c, m) => (b, c)} + ) + val fnAltShiftFilter = Filter.merge( + Case(FnAltShiftRight) {(b, c, m) => (b, c)}, + Case(FnAltShiftLeft) {(b, c, m) => (b, c)} + ) + + + def consumeWord(b: Vector[Char], c: Int, delta: Int, offset: Int) = { + var current = c + while(b.isDefinedAt(current) && !b(current).isLetterOrDigit) current += delta + while(b.isDefinedAt(current) && b(current).isLetterOrDigit) current += delta + current + offset + } + + // c -1 to move at least one character! Otherwise you get stuck at the start of + // a word. + def wordLeft(b: Vector[Char], c: Int) = b -> consumeWord(b, c - 1, -1, 1) + def wordRight(b: Vector[Char], c: Int) = b -> consumeWord(b, c, 1, 0) +} diff --git a/compiler/src/dotty/tools/dotc/repl/ammonite/filters/HistoryFilter.scala b/compiler/src/dotty/tools/dotc/repl/ammonite/filters/HistoryFilter.scala new file mode 100644 index 000000000..dac1c9d23 --- /dev/null +++ b/compiler/src/dotty/tools/dotc/repl/ammonite/filters/HistoryFilter.scala @@ -0,0 +1,334 @@ +package dotty.tools +package dotc +package repl +package ammonite +package terminal +package filters + +import terminal.FilterTools._ +import terminal.LazyList._ +import terminal._ + +/** + * Provides history navigation up and down, saving the current line, a well + * as history-search functionality (`Ctrl R` in bash) letting you quickly find + * & filter previous commands by entering a sub-string. + */ +class HistoryFilter( + history: () => IndexedSeq[String], + commentStartColor: String, + commentEndColor: String +) extends DelegateFilter { + + + def identifier = "HistoryFilter" + /** + * `-1` means we haven't started looking at history, `n >= 0` means we're + * currently at history command `n` + */ + var historyIndex = -1 + + /** + * The term we're searching for, if any. + * + * - `None` means we're not searching for anything, e.g. we're just + * browsing history + * + * - `Some(term)` where `term` is not empty is what it normally looks + * like when we're searching for something + * + * - `Some(term)` where `term` is empty only really happens when you + * start searching and delete things, or if you `Ctrl-R` on an empty + * prompt + */ + var searchTerm: Option[Vector[Char]] = None + + /** + * Records the last buffer that the filter has observed while it's in + * search/history mode. If the new buffer differs from this, assume that + * some other filter modified the buffer and drop out of search/history + */ + var prevBuffer: Option[Vector[Char]] = None + + /** + * Kicks the HistoryFilter from passive-mode into search-history mode + */ + def startHistory(b: Vector[Char], c: Int): (Vector[Char], Int, String) = { + if (b.nonEmpty) searchTerm = Some(b) + up(Vector(), c) + } + + def searchHistory( + start: Int, + increment: Int, + buffer: Vector[Char], + skipped: Vector[Char] + ) = { + + def nextHistoryIndexFor(v: Vector[Char]) = { + HistoryFilter.findNewHistoryIndex(start, v, history(), increment, skipped) + } + + val (newHistoryIndex, newBuffer, newMsg, newCursor) = searchTerm match { + // We're not searching for anything, just browsing history. + // Pass in Vector.empty so we scroll through all items + case None => + val (i, b, c) = nextHistoryIndexFor(Vector.empty) + (i, b, "", 99999) + + // We're searching for some item with a particular search term + case Some(b) if b.nonEmpty => + val (i, b1, c) = nextHistoryIndexFor(b) + + val msg = + if (i.nonEmpty) "" + else commentStartColor + HistoryFilter.cannotFindSearchMessage + commentEndColor + + (i, b1, msg, c) + + // We're searching for nothing in particular; in this case, + // show a help message instead of an unhelpful, empty buffer + case Some(b) if b.isEmpty => + val msg = commentStartColor + HistoryFilter.emptySearchMessage + commentEndColor + // The cursor in this case always goes to zero + (Some(start), Vector(), msg, 0) + + } + + historyIndex = newHistoryIndex.getOrElse(-1) + + (newBuffer, newCursor, newMsg) + } + + def activeHistory = searchTerm.nonEmpty || historyIndex != -1 + def activeSearch = searchTerm.nonEmpty + + def up(b: Vector[Char], c: Int) = + searchHistory(historyIndex + 1, 1, b, b) + + def down(b: Vector[Char], c: Int) = + searchHistory(historyIndex - 1, -1, b, b) + + def wrap(rest: LazyList[Int], out: (Vector[Char], Int, String)) = + TS(rest, out._1, out._2, out._3) + + def ctrlR(b: Vector[Char], c: Int) = + if (activeSearch) up(b, c) + else { + searchTerm = Some(b) + up(Vector(), c) + } + + def printableChar(char: Char)(b: Vector[Char], c: Int) = { + searchTerm = searchTerm.map(_ :+ char) + searchHistory(historyIndex.max(0), 1, b :+ char, Vector()) + } + + def backspace(b: Vector[Char], c: Int) = { + searchTerm = searchTerm.map(_.dropRight(1)) + searchHistory(historyIndex, 1, b, Vector()) + } + + /** + * Predicate to check if either we're searching for a term or if we're in + * history-browsing mode and some predicate is true. + * + * Very often we want to capture keystrokes in search-mode more aggressively + * than in history-mode, e.g. search-mode drops you out more aggressively + * than history-mode does, and its up/down keys cycle through history more + * aggressively on every keystroke while history-mode only cycles when you + * reach the top/bottom line of the multi-line input. + */ + def searchOrHistoryAnd(cond: Boolean) = + activeSearch || (activeHistory && cond) + + val dropHistoryChars = Set(9, 13, 10) // Tab or Enter + + def endHistory() = { + historyIndex = -1 + searchTerm = None + } + + def filter = Filter.wrap("historyFilterWrap1") { + (ti: TermInfo) => { + prelude.op(ti) match { + case None => + prevBuffer = Some(ti.ts.buffer) + filter0.op(ti) match { + case Some(ts: TermState) => + prevBuffer = Some(ts.buffer) + Some(ts) + case x => x + } + case some => some + } + } + } + + def prelude: Filter = Filter("historyPrelude") { + case TS(inputs, b, c, _) if activeHistory && prevBuffer.exists(_ != b) => + endHistory() + prevBuffer = None + TS(inputs, b, c) + } + + def filter0: Filter = Filter("filter0") { + // Ways to kick off the history/search if you're not already in it + + // `Ctrl-R` + case TS(18 ~: rest, b, c, _) => wrap(rest, ctrlR(b, c)) + + // `Up` from the first line in the input + case TermInfo(TS(p"\u001b[A$rest", b, c, _), w) if firstRow(c, b, w) && !activeHistory => + wrap(rest, startHistory(b, c)) + + // `Ctrl P` + case TermInfo(TS(p"\u0010$rest", b, c, _), w) if firstRow(c, b, w) && !activeHistory => + wrap(rest, startHistory(b, c)) + + // `Page-Up` from first character starts history + case TermInfo(TS(p"\u001b[5~$rest", b, c, _), w) if c == 0 && !activeHistory => + wrap(rest, startHistory(b, c)) + + // Things you can do when you're already in the history search + + // Navigating up and down the history. Each up or down searches for + // the next thing that matches your current searchTerm + // Up + case TermInfo(TS(p"\u001b[A$rest", b, c, _), w) if searchOrHistoryAnd(firstRow(c, b, w)) => + wrap(rest, up(b, c)) + + // Ctrl P + case TermInfo(TS(p"\u0010$rest", b, c, _), w) if searchOrHistoryAnd(firstRow(c, b, w)) => + wrap(rest, up(b, c)) + + // `Page-Up` from first character cycles history up + case TermInfo(TS(p"\u001b[5~$rest", b, c, _), w) if searchOrHistoryAnd(c == 0) => + wrap(rest, up(b, c)) + + // Down + case TermInfo(TS(p"\u001b[B$rest", b, c, _), w) if searchOrHistoryAnd(lastRow(c, b, w)) => + wrap(rest, down(b, c)) + + // `Ctrl N` + + case TermInfo(TS(p"\u000e$rest", b, c, _), w) if searchOrHistoryAnd(lastRow(c, b, w)) => + wrap(rest, down(b, c)) + // `Page-Down` from last character cycles history down + case TermInfo(TS(p"\u001b[6~$rest", b, c, _), w) if searchOrHistoryAnd(c == b.length - 1) => + wrap(rest, down(b, c)) + + + // Intercept Backspace and delete a character in search-mode, preserving it, but + // letting it fall through and dropping you out of history-mode if you try to make + // edits + case TS(127 ~: rest, buffer, cursor, _) if activeSearch => + wrap(rest, backspace(buffer, cursor)) + + // Any other control characters drop you out of search mode, but only the + // set of `dropHistoryChars` drops you out of history mode + case TS(char ~: inputs, buffer, cursor, _) + if char.toChar.isControl && searchOrHistoryAnd(dropHistoryChars(char)) => + val newBuffer = + // If we're back to -1, it means we've wrapped around and are + // displaying the original search term with a wrap-around message + // in the terminal. Drop the message and just preserve the search term + if (historyIndex == -1) searchTerm.get + // If we're searching for an empty string, special-case this and return + // an empty buffer rather than the first history item (which would be + // the default) because that wouldn't make much sense + else if (searchTerm.exists(_.isEmpty)) Vector() + // Otherwise, pick whatever history entry we're at and use that + else history()(historyIndex).toVector + endHistory() + + TS(char ~: inputs, newBuffer, cursor) + + // Intercept every other printable character when search is on and + // enter it into the current search + case TS(char ~: rest, buffer, cursor, _) if activeSearch => + wrap(rest, printableChar(char.toChar)(buffer, cursor)) + + // If you're not in search but are in history, entering any printable + // characters kicks you out of it and preserves the current buffer. This + // makes it harder for you to accidentally lose work due to history-moves + case TS(char ~: rest, buffer, cursor, _) if activeHistory && !char.toChar.isControl => + historyIndex = -1 + TS(char ~: rest, buffer, cursor) + } +} + +object HistoryFilter { + + def mangleBuffer( + historyFilter: HistoryFilter, + buffer: Ansi.Str, + cursor: Int, + startColor: Ansi.Attr + ) = { + if (!historyFilter.activeSearch) buffer + else { + val (searchStart, searchEnd) = + if (historyFilter.searchTerm.get.isEmpty) (cursor, cursor+1) + else { + val start = buffer.plainText.indexOfSlice(historyFilter.searchTerm.get) + + val end = start + (historyFilter.searchTerm.get.length max 1) + (start, end) + } + + val newStr = buffer.overlay(startColor, searchStart, searchEnd) + newStr + } + } + + /** + * @param startIndex The first index to start looking from + * @param searchTerm The term we're searching from; can be empty + * @param history The history we're searching through + * @param indexIncrement Which direction to search, +1 or -1 + * @param skipped Any buffers which we should skip in our search results, + * e.g. because the user has seen them before. + */ + def findNewHistoryIndex( + startIndex: Int, + searchTerm: Vector[Char], + history: IndexedSeq[String], + indexIncrement: Int, + skipped: Vector[Char] + ) = { + /** + * `Some(i)` means we found a reasonable result at history element `i` + * `None` means we couldn't find anything, and should show a not-found + * error to the user + */ + def rec(i: Int): Option[Int] = history.lift(i) match { + // If i < 0, it means the user is pressing `down` too many times, which + // means it doesn't show anything but we shouldn't show an error + case None if i < 0 => Some(-1) + case None => None + case Some(s) if s.contains(searchTerm) && !s.contentEquals(skipped) => + Some(i) + case _ => rec(i + indexIncrement) + } + + val newHistoryIndex = rec(startIndex) + val foundIndex = newHistoryIndex.find(_ != -1) + val newBuffer = foundIndex match { + case None => searchTerm + case Some(i) => history(i).toVector + } + + val newCursor = foundIndex match { + case None => newBuffer.length + case Some(i) => history(i).indexOfSlice(searchTerm) + searchTerm.length + } + + (newHistoryIndex, newBuffer, newCursor) + } + + val emptySearchMessage = + s" ...enter the string to search for, then `up` for more" + val cannotFindSearchMessage = + s" ...can't be found in history; re-starting search" +} diff --git a/compiler/src/dotty/tools/dotc/repl/ammonite/filters/ReadlineFilters.scala b/compiler/src/dotty/tools/dotc/repl/ammonite/filters/ReadlineFilters.scala new file mode 100644 index 000000000..eb79f2b04 --- /dev/null +++ b/compiler/src/dotty/tools/dotc/repl/ammonite/filters/ReadlineFilters.scala @@ -0,0 +1,165 @@ +package dotty.tools +package dotc +package repl +package ammonite +package terminal +package filters + +import terminal.FilterTools._ +import terminal.SpecialKeys._ +import terminal.{DelegateFilter, Filter, Terminal} +/** + * Filters for injection of readline-specific hotkeys, the sort that + * are available in bash, python and most other interactive command-lines + */ +object ReadlineFilters { + // www.bigsmoke.us/readline/shortcuts + // Ctrl-b <- one char + // Ctrl-f -> one char + // Alt-b <- one word + // Alt-f -> one word + // Ctrl-a <- start of line + // Ctrl-e -> end of line + // Ctrl-x-x Toggle start/end + + // Backspace <- delete char + // Del -> delete char + // Ctrl-u <- delete all + // Ctrl-k -> delete all + // Alt-d -> delete word + // Ctrl-w <- delete word + + // Ctrl-u/- Undo + // Ctrl-l clear screen + + // Ctrl-k -> cut all + // Alt-d -> cut word + // Alt-Backspace <- cut word + // Ctrl-y paste last cut + + /** + * Basic readline-style navigation, using all the obscure alphabet hotkeys + * rather than using arrows + */ + lazy val navFilter = Filter.merge( + Case(Ctrl('b'))((b, c, m) => (b, c - 1)), // <- one char + Case(Ctrl('f'))((b, c, m) => (b, c + 1)), // -> one char + Case(Alt + "b")((b, c, m) => GUILikeFilters.wordLeft(b, c)), // <- one word + Case(Alt + "B")((b, c, m) => GUILikeFilters.wordLeft(b, c)), // <- one word + Case(LinuxCtrlLeft)((b, c, m) => GUILikeFilters.wordLeft(b, c)), // <- one word + Case(Alt + "f")((b, c, m) => GUILikeFilters.wordRight(b, c)), // -> one word + Case(Alt + "F")((b, c, m) => GUILikeFilters.wordRight(b, c)), // -> one word + Case(LinuxCtrlRight)((b, c, m) => GUILikeFilters.wordRight(b, c)), // -> one word + Case(Home)((b, c, m) => BasicFilters.moveStart(b, c, m.width)), // <- one line + Case(HomeScreen)((b, c, m) => BasicFilters.moveStart(b, c, m.width)), // <- one line + Case(Ctrl('a'))((b, c, m) => BasicFilters.moveStart(b, c, m.width)), + Case(End)((b, c, m) => BasicFilters.moveEnd(b, c, m.width)), // -> one line + Case(EndScreen)((b, c, m) => BasicFilters.moveEnd(b, c, m.width)), // -> one line + Case(Ctrl('e'))((b, c, m) => BasicFilters.moveEnd(b, c, m.width)), + Case(Alt + "t")((b, c, m) => transposeWord(b, c)), + Case(Alt + "T")((b, c, m) => transposeWord(b, c)), + Case(Ctrl('t'))((b, c, m) => transposeLetter(b, c)) + ) + + def transposeLetter(b: Vector[Char], c: Int) = + // If there's no letter before the cursor to transpose, don't do anything + if (c == 0) (b, c) + else if (c == b.length) (b.dropRight(2) ++ b.takeRight(2).reverse, c) + else (b.patch(c-1, b.slice(c-1, c+1).reverse, 2), c + 1) + + def transposeWord(b: Vector[Char], c: Int) = { + val leftStart0 = GUILikeFilters.consumeWord(b, c - 1, -1, 1) + val leftEnd0 = GUILikeFilters.consumeWord(b, leftStart0, 1, 0) + val rightEnd = GUILikeFilters.consumeWord(b, c, 1, 0) + val rightStart = GUILikeFilters.consumeWord(b, rightEnd - 1, -1, 1) + + // If no word to the left to transpose, do nothing + if (leftStart0 == 0 && rightStart == 0) (b, c) + else { + val (leftStart, leftEnd) = + // If there is no word to the *right* to transpose, + // transpose the two words to the left instead + if (leftEnd0 == b.length && rightEnd == b.length) { + val leftStart = GUILikeFilters.consumeWord(b, leftStart0 - 1, -1, 1) + val leftEnd = GUILikeFilters.consumeWord(b, leftStart, 1, 0) + (leftStart, leftEnd) + }else (leftStart0, leftEnd0) + + val newB = + b.slice(0, leftStart) ++ + b.slice(rightStart, rightEnd) ++ + b.slice(leftEnd, rightStart) ++ + b.slice(leftStart, leftEnd) ++ + b.slice(rightEnd, b.length) + + (newB, rightEnd) + } + } + + /** + * All the cut-pasting logic, though for many people they simply + * use these shortcuts for deleting and don't use paste much at all. + */ + case class CutPasteFilter() extends DelegateFilter { + def identifier = "CutPasteFilter" + var accumulating = false + var currentCut = Vector.empty[Char] + def prepend(b: Vector[Char]) = { + if (accumulating) currentCut = b ++ currentCut + else currentCut = b + accumulating = true + } + def append(b: Vector[Char]) = { + if (accumulating) currentCut = currentCut ++ b + else currentCut = b + accumulating = true + } + def cutCharLeft(b: Vector[Char], c: Int) = { + /* Do not edit current cut. Zsh(zle) & Bash(readline) do not edit the yank ring for Ctrl-h */ + (b patch(from = c - 1, patch = Nil, replaced = 1), c - 1) + } + + def cutAllLeft(b: Vector[Char], c: Int) = { + prepend(b.take(c)) + (b.drop(c), 0) + } + def cutAllRight(b: Vector[Char], c: Int) = { + append(b.drop(c)) + (b.take(c), c) + } + + def cutWordRight(b: Vector[Char], c: Int) = { + val start = GUILikeFilters.consumeWord(b, c, 1, 0) + append(b.slice(c, start)) + (b.take(c) ++ b.drop(start), c) + } + + def cutWordLeft(b: Vector[Char], c: Int) = { + val start = GUILikeFilters.consumeWord(b, c - 1, -1, 1) + prepend(b.slice(start, c)) + (b.take(start) ++ b.drop(c), start) + } + + def paste(b: Vector[Char], c: Int) = { + accumulating = false + (b.take(c) ++ currentCut ++ b.drop(c), c + currentCut.length) + } + + def filter = Filter.merge( + Case(Ctrl('u'))((b, c, m) => cutAllLeft(b, c)), + Case(Ctrl('k'))((b, c, m) => cutAllRight(b, c)), + Case(Alt + "d")((b, c, m) => cutWordRight(b, c)), + Case(Ctrl('w'))((b, c, m) => cutWordLeft(b, c)), + Case(Alt + "\u007f")((b, c, m) => cutWordLeft(b, c)), + // weird hacks to make it run code every time without having to be the one + // handling the input; ideally we'd change Filter to be something + // other than a PartialFunction, but for now this will do. + + // If some command goes through that's not appending/prepending to the + // kill ring, stop appending and allow the next kill to override it + Filter.wrap("ReadLineFilterWrap") {_ => accumulating = false; None}, + Case(Ctrl('h'))((b, c, m) => cutCharLeft(b, c)), + Case(Ctrl('y'))((b, c, m) => paste(b, c)) + ) + } +} diff --git a/compiler/src/dotty/tools/dotc/repl/ammonite/filters/UndoFilter.scala b/compiler/src/dotty/tools/dotc/repl/ammonite/filters/UndoFilter.scala new file mode 100644 index 000000000..c265a7a4c --- /dev/null +++ b/compiler/src/dotty/tools/dotc/repl/ammonite/filters/UndoFilter.scala @@ -0,0 +1,157 @@ +package dotty.tools +package dotc +package repl +package ammonite +package terminal +package filters + +import terminal.FilterTools._ +import terminal.LazyList.~: +import terminal._ +import scala.collection.mutable + +/** + * A filter that implements "undo" functionality in the ammonite REPL. It + * shares the same `Ctrl -` hotkey that the bash undo, but shares behavior + * with the undo behavior in desktop text editors: + * + * - Multiple `delete`s in a row get collapsed + * - In addition to edits you can undo cursor movements: undo will bring your + * cursor back to location of previous edits before it undoes them + * - Provides "redo" functionality under `Alt -`/`Esc -`: un-undo the things + * you didn't actually want to undo! + * + * @param maxUndo: the maximum number of undo-frames that are stored. + */ +case class UndoFilter(maxUndo: Int = 25) extends DelegateFilter { + def identifier = "UndoFilter" + /** + * The current stack of states that undo/redo would cycle through. + * + * Not really the appropriate data structure, since when it reaches + * `maxUndo` in length we remove one element from the start whenever we + * append one element to the end, which costs `O(n)`. On the other hand, + * It also costs `O(n)` to maintain the buffer of previous states, and + * so `n` is probably going to be pretty small anyway (tens?) so `O(n)` + * is perfectly fine. + */ + val undoBuffer = mutable.Buffer[(Vector[Char], Int)](Vector[Char]() -> 0) + + /** + * The current position in the undoStack that the terminal is currently in. + */ + var undoIndex = 0 + /** + * An enum representing what the user is "currently" doing. Used to + * collapse sequential actions into one undo step: e.g. 10 plai + * chars typed becomes 1 undo step, or 10 chars deleted becomes one undo + * step, but 4 chars typed followed by 3 chars deleted followed by 3 chars + * typed gets grouped into 3 different undo steps + */ + var state = UndoState.Default + def currentUndo = undoBuffer(undoBuffer.length - undoIndex - 1) + + def undo(b: Vector[Char], c: Int) = { + val msg = + if (undoIndex >= undoBuffer.length - 1) UndoFilter.cannotUndoMsg + else { + undoIndex += 1 + state = UndoState.Default + UndoFilter.undoMsg + } + val (b1, c1) = currentUndo + (b1, c1, msg) + } + + def redo(b: Vector[Char], c: Int) = { + val msg = + if (undoIndex <= 0) UndoFilter.cannotRedoMsg + else { + undoIndex -= 1 + state = UndoState.Default + UndoFilter.redoMsg + } + + currentUndo + val (b1, c1) = currentUndo + (b1, c1, msg) + } + + def wrap(bc: (Vector[Char], Int, Ansi.Str), rest: LazyList[Int]) = { + val (b, c, msg) = bc + TS(rest, b, c, msg) + } + + def pushUndos(b: Vector[Char], c: Int) = { + val (lastB, lastC) = currentUndo + // Since we don't have access to the `typingFilter` in this code, we + // instead attempt to reverse-engineer "what happened" to the buffer by + // comparing the old one with the new. + // + // It turns out that it's not that hard to identify the few cases we care + // about, since they're all result in either 0 or 1 chars being different + // between old and new buffers. + val newState = + // Nothing changed means nothing changed + if (lastC == c && lastB == b) state + // if cursor advanced 1, and buffer grew by 1 at the cursor, we're typing + else if (lastC + 1 == c && lastB == b.patch(c-1, Nil, 1)) UndoState.Typing + // cursor moved left 1, and buffer lost 1 char at that point, we're deleting + else if (lastC - 1 == c && lastB.patch(c, Nil, 1) == b) UndoState.Deleting + // cursor didn't move, and buffer lost 1 char at that point, we're also deleting + else if (lastC == c && lastB.patch(c - 1, Nil, 1) == b) UndoState.Deleting + // cursor moved around but buffer didn't change, we're navigating + else if (lastC != c && lastB == b) UndoState.Navigating + // otherwise, sit in the "Default" state where every change is recorded. + else UndoState.Default + + if (state != newState || newState == UndoState.Default && (lastB, lastC) != (b, c)) { + // If something changes: either we enter a new `UndoState`, or we're in + // the `Default` undo state and the terminal buffer/cursor change, then + // truncate the `undoStack` and add a new tuple to the stack that we can + // build upon. This means that we lose all ability to re-do actions after + // someone starts making edits, which is consistent with most other + // editors + state = newState + undoBuffer.remove(undoBuffer.length - undoIndex, undoIndex) + undoIndex = 0 + + if (undoBuffer.length == maxUndo) undoBuffer.remove(0) + + undoBuffer.append(b -> c) + } else if (undoIndex == 0 && (b, c) != undoBuffer(undoBuffer.length - 1)) { + undoBuffer(undoBuffer.length - 1) = (b, c) + } + + state = newState + } + + def filter = Filter.merge( + Filter.wrap("undoFilterWrapped") { + case TS(q ~: rest, b, c, _) => + pushUndos(b, c) + None + }, + Filter("undoFilter") { + case TS(31 ~: rest, b, c, _) => wrap(undo(b, c), rest) + case TS(27 ~: 114 ~: rest, b, c, _) => wrap(undo(b, c), rest) + case TS(27 ~: 45 ~: rest, b, c, _) => wrap(redo(b, c), rest) + } + ) +} + + +sealed class UndoState(override val toString: String) +object UndoState { + val Default = new UndoState("Default") + val Typing = new UndoState("Typing") + val Deleting = new UndoState("Deleting") + val Navigating = new UndoState("Navigating") +} + +object UndoFilter { + val undoMsg = Ansi.Color.Blue(" ...undoing last action, `Alt -` or `Esc -` to redo") + val cannotUndoMsg = Ansi.Color.Blue(" ...no more actions to undo") + val redoMsg = Ansi.Color.Blue(" ...redoing last action") + val cannotRedoMsg = Ansi.Color.Blue(" ...no more actions to redo") +} |