aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.gitignore1
-rw-r--r--project/Build.scala4
-rw-r--r--src/dotty/tools/dotc/printing/SyntaxHighlighting.scala262
-rw-r--r--src/dotty/tools/dotc/repl/AmmoniteReader.scala78
-rw-r--r--src/dotty/tools/dotc/repl/CompilingInterpreter.scala51
-rw-r--r--src/dotty/tools/dotc/repl/InteractiveReader.scala15
-rw-r--r--src/dotty/tools/dotc/repl/Interpreter.scala6
-rw-r--r--src/dotty/tools/dotc/repl/InterpreterLoop.scala33
-rw-r--r--src/dotty/tools/dotc/repl/JLineReader.scala1
-rw-r--r--src/dotty/tools/dotc/repl/REPL.scala4
-rw-r--r--src/dotty/tools/dotc/repl/SimpleReader.scala1
-rw-r--r--src/dotty/tools/dotc/repl/ammonite/Ansi.scala256
-rw-r--r--src/dotty/tools/dotc/repl/ammonite/Filter.scala61
-rw-r--r--src/dotty/tools/dotc/repl/ammonite/FilterTools.scala80
-rw-r--r--src/dotty/tools/dotc/repl/ammonite/LICENSE25
-rw-r--r--src/dotty/tools/dotc/repl/ammonite/Protocol.scala30
-rw-r--r--src/dotty/tools/dotc/repl/ammonite/SpecialKeys.scala81
-rw-r--r--src/dotty/tools/dotc/repl/ammonite/Terminal.scala320
-rw-r--r--src/dotty/tools/dotc/repl/ammonite/Utils.scala169
-rw-r--r--src/dotty/tools/dotc/repl/ammonite/filters/BasicFilters.scala164
-rw-r--r--src/dotty/tools/dotc/repl/ammonite/filters/GUILikeFilters.scala170
-rw-r--r--src/dotty/tools/dotc/repl/ammonite/filters/HistoryFilter.scala334
-rw-r--r--src/dotty/tools/dotc/repl/ammonite/filters/ReadlineFilters.scala165
-rw-r--r--src/dotty/tools/dotc/repl/ammonite/filters/UndoFilter.scala157
-rw-r--r--test/test/TestREPL.scala4
-rw-r--r--test/test/TypeStealer.scala (renamed from test/test/DottyRepl.scala)2
-rw-r--r--tests/repl/imports.check4
-rw-r--r--tests/repl/multilines.check33
28 files changed, 2431 insertions, 80 deletions
diff --git a/.gitignore b/.gitignore
index f485f1b53..ce4e4a440 100644
--- a/.gitignore
+++ b/.gitignore
@@ -2,6 +2,7 @@
*.class
*.log
*~
+*.swp
# sbt specific
dist/*
diff --git a/project/Build.scala b/project/Build.scala
index f64dd0f33..35482303e 100644
--- a/project/Build.scala
+++ b/project/Build.scala
@@ -64,6 +64,10 @@ object DottyBuild extends Build {
unmanagedSourceDirectories in Compile := Seq((scalaSource in Compile).value),
unmanagedSourceDirectories in Test := Seq((scalaSource in Test).value),
+ // set system in/out for repl
+ connectInput in run := true,
+ outputStrategy := Some(StdoutOutput),
+
// Generate compiler.properties, used by sbt
resourceGenerators in Compile += Def.task {
val file = (resourceManaged in Compile).value / "compiler.properties"
diff --git a/src/dotty/tools/dotc/printing/SyntaxHighlighting.scala b/src/dotty/tools/dotc/printing/SyntaxHighlighting.scala
new file mode 100644
index 000000000..14b3a5050
--- /dev/null
+++ b/src/dotty/tools/dotc/printing/SyntaxHighlighting.scala
@@ -0,0 +1,262 @@
+package dotty.tools
+package dotc
+package printing
+
+import parsing.Tokens._
+import scala.annotation.switch
+import scala.collection.mutable.StringBuilder
+
+/** This object provides functions for syntax highlighting in the REPL */
+object SyntaxHighlighting {
+ val NoColor = Console.RESET
+ val CommentColor = Console.GREEN
+ val KeywordColor = Console.CYAN
+ val LiteralColor = Console.MAGENTA
+ val TypeColor = Console.GREEN
+ val AnnotationColor = Console.RED
+
+ private def none(str: String) = str
+ private def keyword(str: String) = KeywordColor + str + NoColor
+ private def typeDef(str: String) = TypeColor + str + NoColor
+ private def literal(str: String) = LiteralColor + str + NoColor
+ private def annotation(str: String) = AnnotationColor + str + NoColor
+
+ private val keywords: Seq[String] = for {
+ index <- IF to FORSOME // All alpha keywords
+ } yield tokenString(index)
+
+ private val interpolationPrefixes =
+ 'A' :: 'B' :: 'C' :: 'D' :: 'E' :: 'F' :: 'G' :: 'H' :: 'I' :: 'J' :: 'K' ::
+ 'L' :: 'M' :: 'N' :: 'O' :: 'P' :: 'Q' :: 'R' :: 'S' :: 'T' :: 'U' :: 'V' ::
+ 'W' :: 'X' :: 'Y' :: 'Z' :: '$' :: '_' :: 'a' :: 'b' :: 'c' :: 'd' :: 'e' ::
+ 'f' :: 'g' :: 'h' :: 'i' :: 'j' :: 'k' :: 'l' :: 'm' :: 'n' :: 'o' :: 'p' ::
+ 'q' :: 'r' :: 's' :: 't' :: 'u' :: 'v' :: 'w' :: 'x' :: 'y' :: 'z' :: Nil
+
+ private val typeEnders =
+ '{' :: '}' :: ')' :: '(' :: '=' :: ' ' :: ',' :: '.' :: '\n' :: Nil
+
+ def apply(chars: Iterable[Char]): Vector[Char] = {
+ var prev: Char = 0
+ var remaining = chars.toStream
+ val newBuf = new StringBuilder
+
+ @inline def keywordStart =
+ prev == 0 || prev == ' ' || prev == '{' || prev == '(' || prev == '\n'
+
+ @inline def numberStart(c: Char) =
+ c.isDigit && (!prev.isLetter || prev == '.' || prev == ' ' || prev == '(' || prev == '\u0000')
+
+ def takeChar(): Char = takeChars(1).head
+ def takeChars(x: Int): Seq[Char] = {
+ val taken = remaining.take(x)
+ remaining = remaining.drop(x)
+ taken
+ }
+
+ while (remaining.nonEmpty) {
+ val n = takeChar()
+ if (interpolationPrefixes.contains(n)) {
+ // Interpolation prefixes are a superset of the keyword start chars
+ val next = remaining.take(3).mkString
+ if (next.startsWith("\"")) {
+ newBuf += n
+ prev = n
+ if (remaining.nonEmpty) takeChar() // drop 1 for appendLiteral
+ appendLiteral('"', next == "\"\"\"")
+ } else {
+ if (n.isUpper && keywordStart) {
+ appendWhile(n, !typeEnders.contains(_), typeDef)
+ } else if (keywordStart) {
+ append(n, keywords.contains(_), keyword)
+ } else {
+ newBuf += n
+ prev = n
+ }
+ }
+ } else {
+ (n: @switch) match {
+ case '/' =>
+ if (remaining.nonEmpty) {
+ takeChar() match {
+ case '/' => eolComment()
+ case '*' => blockComment()
+ case x => newBuf += '/'; remaining = x #:: remaining
+ }
+ } else newBuf += '/'
+ case '=' =>
+ append('=', _ == "=>", keyword)
+ case '<' =>
+ append('<', { x => x == "<-" || x == "<:" || x == "<%" }, keyword)
+ case '>' =>
+ append('>', { x => x == ">:" }, keyword)
+ case '#' if prev != ' ' && prev != '.' =>
+ newBuf append keyword("#")
+ prev = '#'
+ case '@' =>
+ appendWhile('@', _ != ' ', annotation)
+ case '\"' =>
+ appendLiteral('\"', multiline = remaining.take(2).mkString == "\"\"")
+ case '\'' =>
+ appendLiteral('\'')
+ case '`' =>
+ appendTo('`', _ == '`', none)
+ case c if c.isUpper && keywordStart =>
+ appendWhile(c, !typeEnders.contains(_), typeDef)
+ case c if numberStart(c) =>
+ appendWhile(c, { x => x.isDigit || x == '.' || x == '\u0000'}, literal)
+ case c =>
+ newBuf += c; prev = c
+ }
+ }
+ }
+
+ def eolComment() = {
+ newBuf append (CommentColor + "//")
+ var curr = '/'
+ while (curr != '\n' && remaining.nonEmpty) {
+ curr = takeChar()
+ newBuf += curr
+ }
+ prev = curr
+ newBuf append NoColor
+ }
+
+ def blockComment() = {
+ newBuf append (CommentColor + "/*")
+ var curr = '*'
+ var open = 1
+ while (open > 0 && remaining.nonEmpty) {
+ curr = takeChar()
+ newBuf += curr
+
+ if (curr == '*' && remaining.nonEmpty) {
+ curr = takeChar()
+ newBuf += curr
+ if (curr == '/') open -= 1
+ } else if (curr == '/' && remaining.nonEmpty) {
+ curr = takeChar()
+ newBuf += curr
+ if (curr == '*') open += 1
+ }
+ }
+ prev = curr
+ newBuf append NoColor
+ }
+
+ def appendLiteral(delim: Char, multiline: Boolean = false) = {
+ var curr: Char = 0
+ var continue = true
+ var closing = 0
+ val inInterpolation = interpolationPrefixes.contains(prev)
+ newBuf append (LiteralColor + delim)
+
+ def shouldInterpolate =
+ inInterpolation && curr == '$' && prev != '$' && remaining.nonEmpty
+
+ def interpolate() = {
+ val next = takeChar()
+ if (next == '$') {
+ newBuf += curr
+ newBuf += next
+ prev = '$'
+ } else if (next == '{') {
+ var open = 1 // keep track of open blocks
+ newBuf append (KeywordColor + curr)
+ newBuf += next
+ while (remaining.nonEmpty && open > 0) {
+ var c = takeChar()
+ newBuf += c
+ if (c == '}') open -= 1
+ else if (c == '{') open += 1
+ }
+ newBuf append LiteralColor
+ } else {
+ newBuf append (KeywordColor + curr)
+ newBuf += next
+ var c: Char = 'a'
+ while (c.isLetterOrDigit && remaining.nonEmpty) {
+ c = takeChar()
+ if (c != '"') newBuf += c
+ }
+ newBuf append LiteralColor
+ if (c == '"') {
+ newBuf += c
+ continue = false
+ }
+ }
+ closing = 0
+ }
+
+ while (continue && remaining.nonEmpty) {
+ curr = takeChar()
+ if (curr == '\\' && remaining.nonEmpty) {
+ val next = takeChar()
+ newBuf append (KeywordColor + curr)
+ if (next == 'u') {
+ val code = "u" + takeChars(4).mkString
+ newBuf append code
+ } else newBuf += next
+ newBuf append LiteralColor
+ closing = 0
+ } else if (shouldInterpolate) {
+ interpolate()
+ } else if (curr == delim && multiline) {
+ closing += 1
+ if (closing == 3) continue = false
+ newBuf += curr
+ } else if (curr == delim) {
+ continue = false
+ newBuf += curr
+ } else {
+ newBuf += curr
+ closing = 0
+ }
+ }
+ newBuf append NoColor
+ prev = curr
+ }
+
+ def append(c: Char, shouldHL: String => Boolean, highlight: String => String) = {
+ var curr: Char = 0
+ val sb = new StringBuilder(s"$c")
+ while (remaining.nonEmpty && curr != ' ' && curr != '(' && curr != '\n') {
+ curr = takeChar()
+ if (curr != ' ' && curr != '\n') sb += curr
+ }
+
+ val str = sb.toString
+ val toAdd = if (shouldHL(str)) highlight(str) else str
+ val suffix = if (curr == ' ' || curr == '\n') s"$curr" else ""
+ newBuf append (toAdd + suffix)
+ prev = curr
+ }
+
+ def appendWhile(c: Char, pred: Char => Boolean, highlight: String => String) = {
+ var curr: Char = 0
+ val sb = new StringBuilder(s"$c")
+ while (remaining.nonEmpty && pred(curr)) {
+ curr = takeChar()
+ if (pred(curr)) sb += curr
+ }
+
+ val str = sb.toString
+ val suffix = if (!pred(curr)) s"$curr" else ""
+ newBuf append (highlight(str) + suffix)
+ prev = curr
+ }
+
+ def appendTo(c: Char, pred: Char => Boolean, highlight: String => String) = {
+ var curr: Char = 0
+ val sb = new StringBuilder(s"$c")
+ while (remaining.nonEmpty && !pred(curr)) {
+ curr = takeChar()
+ sb += curr
+ }
+
+ newBuf append highlight(sb.toString)
+ prev = curr
+ }
+
+ newBuf.toVector
+ }
+}
diff --git a/src/dotty/tools/dotc/repl/AmmoniteReader.scala b/src/dotty/tools/dotc/repl/AmmoniteReader.scala
new file mode 100644
index 000000000..e399105b3
--- /dev/null
+++ b/src/dotty/tools/dotc/repl/AmmoniteReader.scala
@@ -0,0 +1,78 @@
+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)
+ }
+
+ 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 ansiBuffer = Ansi.Str.parse(SyntaxHighlighting(buffer))
+ 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/src/dotty/tools/dotc/repl/CompilingInterpreter.scala b/src/dotty/tools/dotc/repl/CompilingInterpreter.scala
index bc898488d..b3b7ab13c 100644
--- a/src/dotty/tools/dotc/repl/CompilingInterpreter.scala
+++ b/src/dotty/tools/dotc/repl/CompilingInterpreter.scala
@@ -78,6 +78,25 @@ class CompilingInterpreter(out: PrintWriter, ictx: Context) extends Compiler wit
/** whether to print out result lines */
private var printResults: Boolean = true
+ private var delayOutput: Boolean = false
+
+ var previousOutput: List[String] = Nil
+
+ override def lastOutput() = {
+ val prev = previousOutput
+ previousOutput = Nil
+ 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 = {
@@ -92,14 +111,18 @@ class CompilingInterpreter(out: PrintWriter, ictx: Context) extends Compiler wit
private def newReporter = new ConsoleReporter(Console.in, out) {
override def printMessage(msg: String) = {
- 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()
+ 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") :: previousOutput
+ }
}
}
@@ -188,7 +211,9 @@ class CompilingInterpreter(out: PrintWriter, ictx: Context) extends Compiler wit
Interpreter.Error // an error happened during compilation, e.g. a type error
else {
val (interpreterResultString, succeeded) = req.loadAndRun()
- if (printResults || !succeeded)
+ if (delayOutput)
+ previousOutput = clean(interpreterResultString) :: previousOutput
+ else if (printResults || !succeeded)
out.print(clean(interpreterResultString))
if (succeeded) {
prevRequests += req
@@ -727,10 +752,10 @@ class CompilingInterpreter(out: PrintWriter, ictx: Context) extends Compiler wit
return str
val trailer = "..."
- if (maxpr >= trailer.length+1)
- return str.substring(0, maxpr-3) + trailer
-
- str.substring(0, maxpr)
+ if (maxpr >= trailer.length-1)
+ str.substring(0, maxpr-3) + trailer + "\n"
+ else
+ str.substring(0, maxpr-1)
}
/** Clean up a string for output */
diff --git a/src/dotty/tools/dotc/repl/InteractiveReader.scala b/src/dotty/tools/dotc/repl/InteractiveReader.scala
index 29ecd3c9d..6ec6a0463 100644
--- a/src/dotty/tools/dotc/repl/InteractiveReader.scala
+++ b/src/dotty/tools/dotc/repl/InteractiveReader.scala
@@ -2,6 +2,8 @@ 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
@@ -14,13 +16,14 @@ object InteractiveReader {
/** Create an interactive reader. Uses JLine if the
* library is available, but otherwise uses a
* SimpleReader. */
- def createDefault(): InteractiveReader = {
+ def createDefault(in: Interpreter)(implicit ctx: Context): InteractiveReader = {
try {
- new JLineReader()
- } catch {
- case e =>
- //out.println("jline is not available: " + e) //debug
- new SimpleReader()
+ new AmmoniteReader(in)
+ } catch { case e =>
+ //out.println("jline is not available: " + e) //debug
+ e.printStackTrace()
+ println("Could not use ammonite, falling back to simple reader")
+ new SimpleReader()
}
}
}
diff --git a/src/dotty/tools/dotc/repl/Interpreter.scala b/src/dotty/tools/dotc/repl/Interpreter.scala
index ea587a097..6a292dfe2 100644
--- a/src/dotty/tools/dotc/repl/Interpreter.scala
+++ b/src/dotty/tools/dotc/repl/Interpreter.scala
@@ -33,4 +33,10 @@ trait Interpreter {
/** 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(): List[String]
}
diff --git a/src/dotty/tools/dotc/repl/InterpreterLoop.scala b/src/dotty/tools/dotc/repl/InterpreterLoop.scala
index 4ac9602e7..14a50fdf1 100644
--- a/src/dotty/tools/dotc/repl/InterpreterLoop.scala
+++ b/src/dotty/tools/dotc/repl/InterpreterLoop.scala
@@ -18,16 +18,16 @@ import scala.concurrent.ExecutionContext.Implicits.global
* After instantiation, clients should call the `run` method.
*
* @author Moez A. Abdel-Gawad
- * @author Lex Spoon
+ * @author Lex Spoon
* @author Martin Odersky
*/
class InterpreterLoop(compiler: Compiler, config: REPL.Config)(implicit ctx: Context) {
import config._
- private var in = input
-
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
@@ -74,10 +74,9 @@ class InterpreterLoop(compiler: Compiler, config: REPL.Config)(implicit ctx: Con
* line of input to be entered.
*/
def firstLine(): String = {
- val futLine = Future(in.readLine(prompt))
interpreter.beQuietDuring(
interpreter.interpret("val theAnswerToLifeInTheUniverseAndEverything = 21 * 2"))
- Await.result(futLine, Duration.Inf)
+ in.readLine(prompt)
}
/** The main read-eval-print loop for the interpreter. It calls
@@ -149,6 +148,7 @@ class InterpreterLoop(compiler: Compiler, config: REPL.Config)(implicit ctx: Con
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
@@ -167,7 +167,12 @@ class InterpreterLoop(compiler: Compiler, config: REPL.Config)(implicit ctx: Con
else if (line startsWith ":")
output.println("Unknown command. Type :help for help.")
else
- shouldReplay = interpretStartingWith(line)
+ shouldReplay = lastOutput match { // don't interpret twice
+ case Nil => interpretStartingWith(line)
+ case oldRes =>
+ oldRes foreach output.print
+ Some(line)
+ }
(true, shouldReplay)
}
@@ -178,23 +183,11 @@ class InterpreterLoop(compiler: Compiler, config: REPL.Config)(implicit ctx: Con
* read, go ahead and interpret it. Return the full string
* to be recorded for replay, if any.
*/
- def interpretStartingWith(code: String): Option[String] = {
+ def interpretStartingWith(code: String): Option[String] =
interpreter.interpret(code) match {
case Interpreter.Success => Some(code)
- case Interpreter.Error => None
- case Interpreter.Incomplete =>
- if (in.interactive && code.endsWith("\n\n")) {
- output.println("You typed two blank lines. Starting a new command.")
- None
- } else {
- val nextLine = in.readLine(continuationPrompt)
- if (nextLine == null)
- None // end of file
- else
- interpretStartingWith(code + "\n" + nextLine)
- }
+ case _ => None
}
- }
/*
def loadFiles(settings: Settings) {
settings match {
diff --git a/src/dotty/tools/dotc/repl/JLineReader.scala b/src/dotty/tools/dotc/repl/JLineReader.scala
index 592b19df5..73463cd7c 100644
--- a/src/dotty/tools/dotc/repl/JLineReader.scala
+++ b/src/dotty/tools/dotc/repl/JLineReader.scala
@@ -2,6 +2,7 @@ package dotty.tools
package dotc
package repl
+import dotc.core.Contexts.Context
import jline.console.ConsoleReader
/** Adaptor for JLine
diff --git a/src/dotty/tools/dotc/repl/REPL.scala b/src/dotty/tools/dotc/repl/REPL.scala
index e5ff2d3af..1f5e3347b 100644
--- a/src/dotty/tools/dotc/repl/REPL.scala
+++ b/src/dotty/tools/dotc/repl/REPL.scala
@@ -46,11 +46,11 @@ object REPL {
val version = ".next (pre-alpha)"
/** The default input reader */
- def input(implicit ctx: Context): InteractiveReader = {
+ def input(in: Interpreter)(implicit ctx: Context): InteractiveReader = {
val emacsShell = System.getProperty("env.emacs", "") != ""
//println("emacsShell="+emacsShell) //debug
if (ctx.settings.Xnojline.value || emacsShell) new SimpleReader()
- else InteractiveReader.createDefault()
+ else InteractiveReader.createDefault(in)
}
/** The default output writer */
diff --git a/src/dotty/tools/dotc/repl/SimpleReader.scala b/src/dotty/tools/dotc/repl/SimpleReader.scala
index 9fd563382..5fab47bbe 100644
--- a/src/dotty/tools/dotc/repl/SimpleReader.scala
+++ b/src/dotty/tools/dotc/repl/SimpleReader.scala
@@ -3,6 +3,7 @@ package dotc
package repl
import java.io.{BufferedReader, PrintWriter}
+import dotc.core.Contexts.Context
/** Reads using standard JDK API */
diff --git a/src/dotty/tools/dotc/repl/ammonite/Ansi.scala b/src/dotty/tools/dotc/repl/ammonite/Ansi.scala
new file mode 100644
index 000000000..37c4de7b5
--- /dev/null
+++ b/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/src/dotty/tools/dotc/repl/ammonite/Filter.scala b/src/dotty/tools/dotc/repl/ammonite/Filter.scala
new file mode 100644
index 000000000..9d34bb0f2
--- /dev/null
+++ b/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/src/dotty/tools/dotc/repl/ammonite/FilterTools.scala b/src/dotty/tools/dotc/repl/ammonite/FilterTools.scala
new file mode 100644
index 000000000..c18b6a927
--- /dev/null
+++ b/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/src/dotty/tools/dotc/repl/ammonite/LICENSE b/src/dotty/tools/dotc/repl/ammonite/LICENSE
new file mode 100644
index 000000000..b15103580
--- /dev/null
+++ b/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/src/dotty/tools/dotc/repl/ammonite/Protocol.scala b/src/dotty/tools/dotc/repl/ammonite/Protocol.scala
new file mode 100644
index 000000000..34d31aeca
--- /dev/null
+++ b/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/src/dotty/tools/dotc/repl/ammonite/SpecialKeys.scala b/src/dotty/tools/dotc/repl/ammonite/SpecialKeys.scala
new file mode 100644
index 000000000..d834cc10b
--- /dev/null
+++ b/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/src/dotty/tools/dotc/repl/ammonite/Terminal.scala b/src/dotty/tools/dotc/repl/ammonite/Terminal.scala
new file mode 100644
index 000000000..4b18b38e3
--- /dev/null
+++ b/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/src/dotty/tools/dotc/repl/ammonite/Utils.scala b/src/dotty/tools/dotc/repl/ammonite/Utils.scala
new file mode 100644
index 000000000..64a2c1476
--- /dev/null
+++ b/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/src/dotty/tools/dotc/repl/ammonite/filters/BasicFilters.scala b/src/dotty/tools/dotc/repl/ammonite/filters/BasicFilters.scala
new file mode 100644
index 000000000..ebbcf2148
--- /dev/null
+++ b/src/dotty/tools/dotc/repl/ammonite/filters/BasicFilters.scala
@@ -0,0 +1,164 @@
+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]) = {
+ val (first, last) = b.splitAt(c)
+ TermState(rest, (first :+ '\n') ++ last, c + 1)
+ }
+
+
+ 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/src/dotty/tools/dotc/repl/ammonite/filters/GUILikeFilters.scala b/src/dotty/tools/dotc/repl/ammonite/filters/GUILikeFilters.scala
new file mode 100644
index 000000000..69a9769c6
--- /dev/null
+++ b/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/src/dotty/tools/dotc/repl/ammonite/filters/HistoryFilter.scala b/src/dotty/tools/dotc/repl/ammonite/filters/HistoryFilter.scala
new file mode 100644
index 000000000..dac1c9d23
--- /dev/null
+++ b/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/src/dotty/tools/dotc/repl/ammonite/filters/ReadlineFilters.scala b/src/dotty/tools/dotc/repl/ammonite/filters/ReadlineFilters.scala
new file mode 100644
index 000000000..eb79f2b04
--- /dev/null
+++ b/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/src/dotty/tools/dotc/repl/ammonite/filters/UndoFilter.scala b/src/dotty/tools/dotc/repl/ammonite/filters/UndoFilter.scala
new file mode 100644
index 000000000..c265a7a4c
--- /dev/null
+++ b/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")
+}
diff --git a/test/test/TestREPL.scala b/test/test/TestREPL.scala
index d01038c43..0fe05794f 100644
--- a/test/test/TestREPL.scala
+++ b/test/test/TestREPL.scala
@@ -20,7 +20,7 @@ class TestREPL(script: String) extends REPL {
override lazy val config = new REPL.Config {
override val output = new NewLinePrintWriter(out)
- override def input(implicit ctx: Context) = new InteractiveReader {
+ override def input(in: Interpreter)(implicit ctx: Context) = new InteractiveReader {
val lines = script.lines
def readLine(prompt: String): String = {
val line = lines.next
@@ -44,4 +44,4 @@ class TestREPL(script: String) extends REPL {
assert(false)
}
}
-} \ No newline at end of file
+}
diff --git a/test/test/DottyRepl.scala b/test/test/TypeStealer.scala
index 74f6ee248..ae48d9a5b 100644
--- a/test/test/DottyRepl.scala
+++ b/test/test/TypeStealer.scala
@@ -6,7 +6,7 @@ import scala.tools.nsc.Settings
* Dotty requires a mangled bootclasspath to start. It means that `console` mode of sbt doesn't work for us.
* At least I(Dmitry) wasn't able to make sbt fork in console
*/
-object DottyRepl {
+object TypeStealer {
def main(args: Array[String]): Unit = {
def repl = new ILoop {}
diff --git a/tests/repl/imports.check b/tests/repl/imports.check
index 3fa103283..3a7e9341e 100644
--- a/tests/repl/imports.check
+++ b/tests/repl/imports.check
@@ -2,9 +2,7 @@ scala> import scala.collection.mutable
import scala.collection.mutable
scala> val buf = mutable.ListBuffer[Int]()
buf: scala.collection.mutable.ListBuffer[Int] = ListBuffer()
-scala> object o {
- | val xs = List(1, 2, 3)
- | }
+scala> object o { val xs = List(1, 2, 3) }
defined module o
scala> import o._
import o._
diff --git a/tests/repl/multilines.check b/tests/repl/multilines.check
deleted file mode 100644
index 3bc32707e..000000000
--- a/tests/repl/multilines.check
+++ /dev/null
@@ -1,33 +0,0 @@
-scala> val x = """alpha
- |
- | omega"""
-x: String =
-alpha
-
-omega
-scala> val y = """abc
- | |def
- | |ghi
- | """.stripMargin
-y: String =
-abc
-def
-ghi
-
-scala> val z = {
- | def square(x: Int) = x * x
- | val xs = List(1, 2, 3)
- | square(xs)
- | }
-<console>:8: error: type mismatch:
- found : scala.collection.immutable.List[Int](xs)
- required: Int
- square(xs)
- ^
-scala> val z = {
- | def square(x: Int) = x * x
- | val xs = List(1, 2, 3)
- | xs.map(square)
- | }
-z: scala.collection.immutable.List[Int] = List(1, 4, 9)
-scala> :quit