aboutsummaryrefslogtreecommitdiff
path: root/compiler/src/dotty/tools/dotc/repl
diff options
context:
space:
mode:
authorFelix Mulder <felix.mulder@gmail.com>2016-11-02 11:08:28 +0100
committerGuillaume Martres <smarter@ubuntu.com>2016-11-22 01:35:07 +0100
commit8a61ff432543a29234193cd1f7c14abd3f3d31a0 (patch)
treea8147561d307af862c295cfc8100d271063bb0dd /compiler/src/dotty/tools/dotc/repl
parent6a455fe6da5ff9c741d91279a2dc6fe2fb1b472f (diff)
downloaddotty-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')
-rw-r--r--compiler/src/dotty/tools/dotc/repl/AbstractFileClassLoader.scala31
-rw-r--r--compiler/src/dotty/tools/dotc/repl/AmmoniteReader.scala82
-rw-r--r--compiler/src/dotty/tools/dotc/repl/CompilingInterpreter.scala966
-rw-r--r--compiler/src/dotty/tools/dotc/repl/ConsoleWriter.scala21
-rw-r--r--compiler/src/dotty/tools/dotc/repl/InteractiveReader.scala20
-rw-r--r--compiler/src/dotty/tools/dotc/repl/Interpreter.scala45
-rw-r--r--compiler/src/dotty/tools/dotc/repl/InterpreterLoop.scala210
-rw-r--r--compiler/src/dotty/tools/dotc/repl/Main.scala28
-rw-r--r--compiler/src/dotty/tools/dotc/repl/ManifestInfo.scala20
-rw-r--r--compiler/src/dotty/tools/dotc/repl/NewLinePrintWriter.scala11
-rw-r--r--compiler/src/dotty/tools/dotc/repl/REPL.scala100
-rw-r--r--compiler/src/dotty/tools/dotc/repl/SimpleReader.scala24
-rw-r--r--compiler/src/dotty/tools/dotc/repl/ammonite/Ansi.scala256
-rw-r--r--compiler/src/dotty/tools/dotc/repl/ammonite/Filter.scala61
-rw-r--r--compiler/src/dotty/tools/dotc/repl/ammonite/FilterTools.scala80
-rw-r--r--compiler/src/dotty/tools/dotc/repl/ammonite/LICENSE25
-rw-r--r--compiler/src/dotty/tools/dotc/repl/ammonite/Protocol.scala30
-rw-r--r--compiler/src/dotty/tools/dotc/repl/ammonite/SpecialKeys.scala81
-rw-r--r--compiler/src/dotty/tools/dotc/repl/ammonite/Terminal.scala320
-rw-r--r--compiler/src/dotty/tools/dotc/repl/ammonite/Utils.scala169
-rw-r--r--compiler/src/dotty/tools/dotc/repl/ammonite/filters/BasicFilters.scala163
-rw-r--r--compiler/src/dotty/tools/dotc/repl/ammonite/filters/GUILikeFilters.scala170
-rw-r--r--compiler/src/dotty/tools/dotc/repl/ammonite/filters/HistoryFilter.scala334
-rw-r--r--compiler/src/dotty/tools/dotc/repl/ammonite/filters/ReadlineFilters.scala165
-rw-r--r--compiler/src/dotty/tools/dotc/repl/ammonite/filters/UndoFilter.scala157
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")
+}