From 53bd25f7e2082a787936ae833b14f873a07ff22c Mon Sep 17 00:00:00 2001 From: Felix Mulder Date: Fri, 22 Apr 2016 16:15:03 +0200 Subject: Initial implementation featuring two different highlighters One was implemted by hand and the other by using dotty's parser. The one built by hand is shorter, and behaves correctly. The scanner one is unfortunately not ready for testing - there are too many things that are workarounds for it to be a good solution as of now The code added from Ammonite is licensed under MIT, not sure where to put the license - but will add it once I know. --- .gitignore | 1 + src/dotty/tools/dotc/parsing/Scanners.scala | 49 ++- src/dotty/tools/dotc/parsing/Tokens.scala | 7 +- src/dotty/tools/dotc/repl/AmmoniteReader.scala | 71 ++++ .../tools/dotc/repl/CompilingInterpreter.scala | 8 +- src/dotty/tools/dotc/repl/InteractiveReader.scala | 15 +- src/dotty/tools/dotc/repl/JLineReader.scala | 3 +- src/dotty/tools/dotc/repl/SimpleReader.scala | 3 +- src/dotty/tools/dotc/repl/SyntaxHighlighter.scala | 396 +++++++++++++++++++++ src/dotty/tools/dotc/repl/ammonite/Ansi.scala | 267 ++++++++++++++ src/dotty/tools/dotc/repl/ammonite/Filter.scala | 65 ++++ .../tools/dotc/repl/ammonite/FilterTools.scala | 86 +++++ src/dotty/tools/dotc/repl/ammonite/Protocol.scala | 27 ++ .../tools/dotc/repl/ammonite/SpecialKeys.scala | 82 +++++ src/dotty/tools/dotc/repl/ammonite/Terminal.scala | 322 +++++++++++++++++ src/dotty/tools/dotc/repl/ammonite/Utils.scala | 167 +++++++++ .../dotc/repl/ammonite/filters/BasicFilters.scala | 164 +++++++++ .../repl/ammonite/filters/GUILikeFilters.scala | 164 +++++++++ .../dotc/repl/ammonite/filters/HistoryFilter.scala | 327 +++++++++++++++++ .../repl/ammonite/filters/ReadlineFilters.scala | 166 +++++++++ .../dotc/repl/ammonite/filters/UndoFilter.scala | 155 ++++++++ test/test/TestREPL.scala | 4 +- 22 files changed, 2523 insertions(+), 26 deletions(-) create mode 100644 src/dotty/tools/dotc/repl/AmmoniteReader.scala create mode 100644 src/dotty/tools/dotc/repl/SyntaxHighlighter.scala create mode 100644 src/dotty/tools/dotc/repl/ammonite/Ansi.scala create mode 100644 src/dotty/tools/dotc/repl/ammonite/Filter.scala create mode 100644 src/dotty/tools/dotc/repl/ammonite/FilterTools.scala create mode 100644 src/dotty/tools/dotc/repl/ammonite/Protocol.scala create mode 100644 src/dotty/tools/dotc/repl/ammonite/SpecialKeys.scala create mode 100644 src/dotty/tools/dotc/repl/ammonite/Terminal.scala create mode 100644 src/dotty/tools/dotc/repl/ammonite/Utils.scala create mode 100644 src/dotty/tools/dotc/repl/ammonite/filters/BasicFilters.scala create mode 100644 src/dotty/tools/dotc/repl/ammonite/filters/GUILikeFilters.scala create mode 100644 src/dotty/tools/dotc/repl/ammonite/filters/HistoryFilter.scala create mode 100644 src/dotty/tools/dotc/repl/ammonite/filters/ReadlineFilters.scala create mode 100644 src/dotty/tools/dotc/repl/ammonite/filters/UndoFilter.scala 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/src/dotty/tools/dotc/parsing/Scanners.scala b/src/dotty/tools/dotc/parsing/Scanners.scala index 6fe86502f..91332b01c 100644 --- a/src/dotty/tools/dotc/parsing/Scanners.scala +++ b/src/dotty/tools/dotc/parsing/Scanners.scala @@ -45,6 +45,9 @@ object Scanners { /** the string value of a literal */ var strVal: String = null + /** the started parsing of a literal */ + var startedLiteral: String = null + /** the base of a number */ var base: Int = 0 @@ -174,8 +177,13 @@ object Scanners { } - class Scanner(source: SourceFile, override val startFrom: Offset = 0)(implicit ctx: Context) extends ScannerCommon(source)(ctx) { - val keepComments = ctx.settings.YkeepComments.value + class Scanner( + source: SourceFile, + override val startFrom: Offset = 0, + preserveWhitespace: Boolean = false + )(implicit ctx: Context) extends ScannerCommon(source)(ctx) { + val keepComments = ctx.settings.YkeepComments.value + val whitespace = new StringBuilder /** All doc comments as encountered, each list contains doc comments from * the same block level. Starting with the deepest level and going upward @@ -239,13 +247,13 @@ object Scanners { /** Are we directly in a string interpolation expression? */ - private def inStringInterpolation = + def inStringInterpolation = sepRegions.nonEmpty && sepRegions.head == STRINGLIT /** Are we directly in a multiline string interpolation expression? * @pre inStringInterpolation */ - private def inMultiLineInterpolation = + def inMultiLineInterpolation = inStringInterpolation && sepRegions.tail.nonEmpty && sepRegions.tail.head == STRINGPART /** read next token and return last offset @@ -316,7 +324,7 @@ object Scanners { token = if (pastBlankLine()) NEWLINES else NEWLINE } - postProcessToken() + if (!preserveWhitespace) postProcessToken() // print("[" + this +"]") } @@ -375,9 +383,20 @@ object Scanners { offset = charOffset - 1 (ch: @switch) match { case ' ' | '\t' | CR | LF | FF => - nextChar() - fetchToken() - case 'A' | 'B' | 'C' | 'D' | 'E' | + if (preserveWhitespace) { + while ((' ' :: '\t' :: CR :: LF :: FF :: Nil) contains ch) { + whitespace += ch + nextChar() + } + token = WHITESPACE + strVal = whitespace.toString + whitespace.clear() + } else { + nextChar() + fetchToken() + } + case c @ ( + 'A' | 'B' | 'C' | 'D' | 'E' | 'F' | 'G' | 'H' | 'I' | 'J' | 'K' | 'L' | 'M' | 'N' | 'O' | 'P' | 'Q' | 'R' | 'S' | 'T' | @@ -388,12 +407,14 @@ object Scanners { 'k' | 'l' | 'm' | 'n' | 'o' | 'p' | 'q' | 'r' | 's' | 't' | 'u' | 'v' | 'w' | 'x' | 'y' | - 'z' => + 'z') => putChar(ch) nextChar() getIdentRest() - if (ch == '"' && token == IDENTIFIER) + if (ch == '"' && token == IDENTIFIER) { token = INTERPOLATIONID + startedLiteral = "\"" + } case '<' => // is XMLSTART? def fetchLT() = { val last = if (charOffset >= 2) buf(charOffset - 2) else ' ' @@ -494,9 +515,11 @@ object Scanners { getLitChar() if (ch == '\'') { nextChar() + startedLiteral = null token = CHARLIT setStrVal() } else { + startedLiteral = "\'" error("unclosed character literal") } } @@ -686,8 +709,12 @@ object Scanners { if (ch == '"') { setStrVal() nextChar() + startedLiteral = null token = STRINGLIT - } else error("unclosed string literal") + } else { + startedLiteral = "\"" + error("unclosed string literal") + } } private def getRawStringLit(): Unit = { diff --git a/src/dotty/tools/dotc/parsing/Tokens.scala b/src/dotty/tools/dotc/parsing/Tokens.scala index b490cd133..3ca86d624 100644 --- a/src/dotty/tools/dotc/parsing/Tokens.scala +++ b/src/dotty/tools/dotc/parsing/Tokens.scala @@ -141,7 +141,7 @@ abstract class TokensCommon { object Tokens extends TokensCommon { final val minToken = EMPTY - final val maxToken = XMLSTART + final val maxToken = COMMENT final val INTERPOLATIONID = 10; enter(INTERPOLATIONID, "string interpolator") final val SYMBOLLIT = 11; enter(SYMBOLLIT, "symbol literal") // TODO: deprecate @@ -188,6 +188,11 @@ object Tokens extends TokensCommon { /** XML mode */ final val XMLSTART = 96; enter(XMLSTART, "$XMLSTART$<") // TODO: deprecate + /** Whitespace */ + final val WHITESPACE = 97; enter(WHITESPACE, "whitespace") + final val COMMENT = 98; enter(COMMENT, "comment") + + final val alphaKeywords = tokenRange(IF, FORSOME) final val symbolicKeywords = tokenRange(USCORE, VIEWBOUND) final val symbolicTokens = tokenRange(COMMA, VIEWBOUND) diff --git a/src/dotty/tools/dotc/repl/AmmoniteReader.scala b/src/dotty/tools/dotc/repl/AmmoniteReader.scala new file mode 100644 index 000000000..0c4dd80ee --- /dev/null +++ b/src/dotty/tools/dotc/repl/AmmoniteReader.scala @@ -0,0 +1,71 @@ +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 + +class AmmoniteReader extends InteractiveReader { + val interactive = true + + 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) => // Enter + + BasicFilters.injectNewLine(b, c, rest) + } + def readLine(prompt: String)(implicit ctx: Context): String = { + val historyFilter = new HistoryFilter( + () => history.toVector, + Console.BLUE, + AnsiNav.resetForegroundColor + ) + + val allFilters = Filter.merge( + UndoFilter(), + historyFilter, + selectionFilter, + GUILikeFilters.altFilter, + GUILikeFilters.fnFilter, + ReadlineFilters.navFilter, + //autocompleteFilter, + 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 ansiBuffer = Ansi.Str.parse(SyntaxHighlighting(new SourceFile("", 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(s) => history = s :: history; s + case None => ":q" + } + } +} diff --git a/src/dotty/tools/dotc/repl/CompilingInterpreter.scala b/src/dotty/tools/dotc/repl/CompilingInterpreter.scala index bc898488d..cfcce106e 100644 --- a/src/dotty/tools/dotc/repl/CompilingInterpreter.scala +++ b/src/dotty/tools/dotc/repl/CompilingInterpreter.scala @@ -727,10 +727,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..6bab0a0c6 100644 --- a/src/dotty/tools/dotc/repl/InteractiveReader.scala +++ b/src/dotty/tools/dotc/repl/InteractiveReader.scala @@ -2,9 +2,11 @@ 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 + def readLine(prompt: String)(implicit ctx: Context): String val interactive: Boolean } @@ -16,11 +18,12 @@ object InteractiveReader { * SimpleReader. */ def createDefault(): InteractiveReader = { try { - new JLineReader() - } catch { - case e => - //out.println("jline is not available: " + e) //debug - new SimpleReader() + new AmmoniteReader() + } 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/JLineReader.scala b/src/dotty/tools/dotc/repl/JLineReader.scala index 592b19df5..c7bdebb54 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 @@ -11,5 +12,5 @@ class JLineReader extends InteractiveReader { val interactive = true - def readLine(prompt: String) = reader.readLine(prompt) + def readLine(prompt: String)(implicit ctx: Context) = reader.readLine(prompt) } diff --git a/src/dotty/tools/dotc/repl/SimpleReader.scala b/src/dotty/tools/dotc/repl/SimpleReader.scala index 9fd563382..b69fcdd2a 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 */ @@ -13,7 +14,7 @@ class SimpleReader( extends InteractiveReader { def this() = this(Console.in, new PrintWriter(Console.out), true) - def readLine(prompt: String) = { + def readLine(prompt: String)(implicit ctx: Context) = { if (interactive) { out.print(prompt) out.flush() diff --git a/src/dotty/tools/dotc/repl/SyntaxHighlighter.scala b/src/dotty/tools/dotc/repl/SyntaxHighlighter.scala new file mode 100644 index 000000000..ebea3f8b2 --- /dev/null +++ b/src/dotty/tools/dotc/repl/SyntaxHighlighter.scala @@ -0,0 +1,396 @@ +package dotty.tools +package dotc +package repl + +import ammonite.terminal.FilterTools._ +import ammonite.terminal.LazyList._ +import ammonite.terminal.SpecialKeys._ +import ammonite.terminal.Filter +import ammonite.terminal._ +import scala.annotation.switch +import scala.collection.mutable.StringBuilder + +/** This object provides functions for syntax highlighting in the REPL */ +object SyntaxHighlighting { + private def none(str: String) = str + private def keyword(str: String) = Console.CYAN + str + Console.RESET + private def typeDef(str: String) = Console.GREEN + str + Console.RESET + private def literal(str: String) = Console.MAGENTA + str + Console.RESET + private def annotation(str: String) = Console.RED + str + Console.RESET + + private val keywords = + "abstract" :: "class" :: "case" :: "catch" :: "def" :: + "do" :: "extends" :: "else" :: "false" :: "finally" :: + "final" :: "for" :: "forSome" :: "if" :: "import" :: + "implicit" :: "lazy" :: "match" :: "null" :: "new" :: + "object" :: "override" :: "private" :: "protected" :: "package" :: + "return" :: "sealed" :: "super" :: "true" :: "trait" :: + "type" :: "try" :: "this" :: "throw" :: "val" :: + "var" :: "with" :: "while" :: "yield" :: Nil + + 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 = + '{' :: '}' :: ')' :: '(' :: '=' :: ' ' :: ',' :: '.' :: Nil + + def apply(buffer: Iterable[Char]): Vector[Char] = { + var prev: Char = 0 + var iter = buffer.toIterator + val newBuf = new StringBuilder + + @inline def keywordStart = + prev == 0 || prev == ' ' || prev == '{' || prev == '(' + + @inline def numberStart(c: Char) = + c.isDigit && (!prev.isLetter || prev == '.' || prev == ' ' || prev == '(' || prev == '\u0000') + + while(iter.hasNext) { + val n = iter.next + if (interpolationPrefixes.contains(n)) { + // Interpolation prefixes are a superset of the keyword start chars + val next = iter.take(3).mkString + if (next.startsWith("\"")) { + newBuf += n + prev = n + appendLiteral('"', next.toIterator.drop(1), next == "\"\"\"") + } else { + val (dup, _ ) = iter.duplicate + iter = next.toIterator ++ dup + 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 '=' => + append('=', _ == "=>", keyword) + case '<' => + append('<', { x => x == "<-" || x == "<:" || x == "<%" }, keyword) + case '>' => + append('>', { x => x == ">:" }, keyword) + case '#' => + newBuf append keyword("#") + prev = '#' + case '@' => + appendWhile('@', _ != ' ', annotation) + case '\"' => iter.take(2).mkString match { + case "\"\"" => appendLiteral('\"', Iterator.empty, multiline = true) + case lit => appendLiteral('\"', lit.toIterator) + } + case '\'' => + appendLiteral('\'', Iterator.empty) + case '`' => + appendUntil('`', _ == '`', 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 appendLiteral(delim: Char, nextTwo: Iterator[Char], multiline: Boolean = false) = { + var curr: Char = 0 + var continue = true + var closing = 0 + val inInterpolation = interpolationPrefixes.contains(prev) + newBuf append (Console.MAGENTA + delim) + + + while (continue && (iter.hasNext || nextTwo.hasNext)) { + curr = if(nextTwo.hasNext) nextTwo.next else iter.next + if (curr == '\\' && (iter.hasNext || nextTwo.hasNext)) { + val next = if (nextTwo.hasNext) nextTwo.next else iter.next + newBuf append (Console.CYAN + curr) + if (next == 'u') { + val code = "u" + iter.take(4).mkString + newBuf append code + } else newBuf += next + newBuf append Console.MAGENTA + closing = 0 + } else if (inInterpolation && curr == '$' && prev != '$' && (iter.hasNext || nextTwo.hasNext)) { //TODO - break me out! + val next: Char = if (nextTwo.hasNext) nextTwo.next else iter.next + if (next == '$') { + newBuf += curr + newBuf += next + prev = '$' + } else if (next == '{') { + newBuf append (Console.CYAN + curr) + newBuf += next + if (iter.hasNext) { + var c = iter.next + while (iter.hasNext && c != '}') { + newBuf += c + c = iter.next + } + newBuf += c + newBuf append Console.MAGENTA + } + } else { //TODO - break me out + newBuf append (Console.CYAN + curr) + newBuf += next + var c: Char = 'a' + while (c.isLetterOrDigit && (iter.hasNext || nextTwo.hasNext)) { + c = if (nextTwo.hasNext) nextTwo.next else iter.next + if (c != '"') newBuf += c + } + newBuf append Console.MAGENTA + if (c == '"') newBuf += c + } + closing = 0 + } 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 Console.RESET + prev = curr + } + + def append(c: Char, shouldHL: String => Boolean, highlight: String => String, pre: Iterator[Char] = Iterator.empty) = { + var curr: Char = 0 + val sb = new StringBuilder(s"$c") + while ((pre.hasNext || iter.hasNext) && curr != ' ' && curr != '(') { + curr = if (pre.hasNext) pre.next else iter.next + if (curr != ' ') sb += curr + } + + val str = sb.toString + val toAdd = if (shouldHL(str)) highlight(str) else str + val suffix = if (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 (iter.hasNext && pred(curr)) { + curr = iter.next + 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 appendUntil(c: Char, pred: Char => Boolean, highlight: String => String) = { + var curr: Char = 0 + val sb = new StringBuilder(s"$c") + while (iter.hasNext && !pred(curr)) { + curr = iter.next + sb += curr + } + + newBuf append (highlight(sb.toString)) + prev = curr + } + + newBuf.toVector + } + + import util.SourceFile + import parsing.Scanners.Scanner + import dotc.core.Contexts._ + import dotc.parsing.Tokens._ + import reporting._ + def apply(source: SourceFile)(implicit ctx: Context): Vector[Char] = { + val freshCtx = ctx.fresh.setReporter(new Reporter { + def doReport(d: Diagnostic)(implicit ctx: Context) = () + }) + val scanner = new Scanner(source, preserveWhitespace = true)(freshCtx) + val buf = new StringBuilder() + var prev = List(EMPTY) + + var realLength = 0 + /** `accept` is used to allow for `realLength` to be used to infer "magically + * missing tokens" + */ + def accept(str: String): String = { + realLength += str.length + str + } + + while (scanner.token != EOF) { + buf append (scanner.token match { + // Whitespace + case WHITESPACE => accept({ + if (prev.head == ERROR) scanner.litBuf.toString + else "" + } + scanner.strVal) + + // Identifiers + case BACKQUOTED_IDENT => + accept(s"""`${scanner.name.show}`""") + case id if identifierTokens contains id => { + val name = accept(scanner.name.show) + if (name.head.isUpper) typeDef(name) else name + } + + // Literals + case INTERPOLATIONID => + parseInterpStr() + case STRINGLIT => + literal(accept(s""""${scanner.strVal}"""")) + case CHARLIT => + literal(accept(s"""'${scanner.strVal}'""")) + case SYMBOLLIT => + accept("'" + scanner.strVal) + case lit if literalTokens contains lit => + literal(accept(scanner.strVal)) + + // Unclosed literals caught using startedLiteral var + case ERROR => + val start = scanner.startedLiteral + accept(if (start != null) start else "") + + // Keywords + case EQUALS | COLON => accept(scanner.name.show) + case k if alphaKeywords.contains(k) || symbolicKeywords.contains(k) => + keyword(accept(scanner.name.show)) + + // Other minor tokens (i.e. '{' etc) + case EMPTY => "" + case XMLSTART => accept("<") + case _ => accept(tokenString(scanner.token).replaceAll("\'", "")) + }) + prev = scanner.token :: prev + scanner.nextToken() + } + + def parseInterpStr(): String = { + // print InterpolationID 's' etc + val sb = new StringBuilder + sb append accept(scanner.name.show) + prev = scanner.token :: prev + scanner.nextToken() + + /** + * The composition of interpolated strings: + * s"hello $guy!" + * ^ ^^^^^^ ^ ^ + * | | | | + * | | | | + * | | | STRINGLIT + * | | IDENTIFIER + * | STRINGPART + * INTERPOLATIONID + * + * As such, get tokens until EOF or STRINGLIT is encountered + */ + def scan() = scanner.token match { + case STRINGPART => { + val delim = + if (scanner.inMultiLineInterpolation) "\"\"\"" + else "\"" + + if (prev.head == INTERPOLATIONID) literal(accept(delim)) + else "" + } + literal(accept(scanner.strVal)) + + case id if identifierTokens contains id => { + val name = scanner.name.show + // $ symbols are not caught by the scanner, infer them + val prefix = if (prev.head == STRINGPART) "$" else "" + accept(prefix + name) + } + case WHITESPACE => accept({ + // Whitespace occurs in interpolated strings where there + // is an error - e.g. unclosed string literal + // + // Or in blocks i.e. ${...WHITESPACE...} + if (prev.head == ERROR) scanner.litBuf.toString + else "" + } + scanner.strVal) + case CHARLIT => + literal(accept(s"""'${scanner.strVal}'""")) + case SYMBOLLIT => + accept("'" + scanner.strVal) + case lit if literalTokens contains lit => + literal(accept(scanner.strVal)) + case LBRACE => + // An `LBRACE` will only occur if it precedes a block, ergo we can + // infer "${" + accept("${") + case RBRACE => accept("}") + case ERROR => { + // FIXME: the behaviour here is weird, the check on line 329 clashes + // with encountering an error in the interpolated string. + // + // This clause should be the one taking care of the errors! + "" + } + case _ if prev.head == INTERPOLATIONID => + accept("\"") + case x => println(s"Unknown symbol: ${scanner.token}"); ??? + } + + while (scanner.token != EOF && scanner.token != STRINGLIT) { + sb append scan + prev = scanner.token :: prev + scanner.nextToken() + } + + val delim = + if (scanner.inMultiLineInterpolation) "\"\"\"" + else "\"" + + if (scanner.token == STRINGLIT) { + // If the last part of an interpolated string is a literal, it will end + // in `STRINGLIT` + if (prev.head == INTERPOLATIONID) sb append literal(accept(delim)) + sb append literal(accept(scanner.strVal + delim)) + } else if (prev.head == ERROR && prev.tail.head != IDENTIFIER) { + // If error entity to occur in an interpolation + val litBuf = scanner.litBuf.toString + val expectedLength = source.content.length + realLength += litBuf.length + val str = + if (realLength + 4 == expectedLength) "\"\"\"" + litBuf + "$" + else if (realLength + 3 == expectedLength) "\"\"\"" + litBuf + else if (realLength + 2 == expectedLength) "\"" + litBuf + "$" + else "\"" + litBuf + + sb append str + prev = -1 :: prev.tail // make sure outer doesn't print this as well + } else if (prev.head == ERROR && prev.tail.head == IDENTIFIER) { + // If an error is preceeded by an identifier, i.e. later in the interpolation + val litBuf = scanner.litBuf.toString + val expLen = source.content.length + + val suffix = "" //TODO + sb append (litBuf + suffix) + } else if (prev.head == IDENTIFIER) { + sb append scanner.litBuf.toString + } else if (prev.head == INTERPOLATIONID && scanner.token == EOF) { + sb append accept(delim) + sb append accept(scanner.litBuf.toString) + } + sb.toString + } + + buf.toVector + } +} 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..3cd2fc26c --- /dev/null +++ b/src/dotty/tools/dotc/repl/ammonite/Ansi.scala @@ -0,0 +1,267 @@ +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 = Vector( + Color, + Back, + Bold, + Underlined, + Reversed + ) + + object Str { + + lazy val ansiRegex = "\u001B\\[[;\\d]*m".r + + implicit def parse(raw: CharSequence): Str = { + // This will + 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..b917f64d2 --- /dev/null +++ b/src/dotty/tools/dotc/repl/ammonite/Filter.scala @@ -0,0 +1,65 @@ +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..4eed208b1 --- /dev/null +++ b/src/dotty/tools/dotc/repl/ammonite/FilterTools.scala @@ -0,0 +1,86 @@ +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/Protocol.scala b/src/dotty/tools/dotc/repl/ammonite/Protocol.scala new file mode 100644 index 000000000..360220f27 --- /dev/null +++ b/src/dotty/tools/dotc/repl/ammonite/Protocol.scala @@ -0,0 +1,27 @@ +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..1a3278523 --- /dev/null +++ b/src/dotty/tools/dotc/repl/ammonite/SpecialKeys.scala @@ -0,0 +1,82 @@ +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..b4d080868 --- /dev/null +++ b/src/dotty/tools/dotc/repl/ammonite/Terminal.scala @@ -0,0 +1,322 @@ +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 { + + val ansiRegex = "\u001B\\[[;\\d]*m".r + + /** + * 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..ca2baf8bf --- /dev/null +++ b/src/dotty/tools/dotc/repl/ammonite/Utils.scala @@ -0,0 +1,167 @@ +package dotty.tools +package dotc +package repl +package ammonite.terminal + +import java.io.{OutputStream, ByteArrayOutputStream, Writer} +import scala.annotation.tailrec + +/** + * Prints stuff to an ad-hoc logging file when running the ammonite-repl or + * ammonite-terminal in development mode in its SBT project. + * + * 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 = { + if (System.getProperty("ammonite-sbt-build") != "true") ??? + else new java.io.FileOutputStream(new java.io.File("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..2c10ed5a7 --- /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..0f62df493 --- /dev/null +++ b/src/dotty/tools/dotc/repl/ammonite/filters/GUILikeFilters.scala @@ -0,0 +1,164 @@ +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..d2ecfaa09 --- /dev/null +++ b/src/dotty/tools/dotc/repl/ammonite/filters/HistoryFilter.scala @@ -0,0 +1,327 @@ +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..d5fb9715e --- /dev/null +++ b/src/dotty/tools/dotc/repl/ammonite/filters/ReadlineFilters.scala @@ -0,0 +1,166 @@ +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..4e0398aa8 --- /dev/null +++ b/src/dotty/tools/dotc/repl/ammonite/filters/UndoFilter.scala @@ -0,0 +1,155 @@ +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..53fcc0730 100644 --- a/test/test/TestREPL.scala +++ b/test/test/TestREPL.scala @@ -22,7 +22,7 @@ class TestREPL(script: String) extends REPL { override def input(implicit ctx: Context) = new InteractiveReader { val lines = script.lines - def readLine(prompt: String): String = { + def readLine(prompt: String)(implicit ctx: Context): String = { val line = lines.next if (line.startsWith(prompt) || line.startsWith(continuationPrompt)) { output.println(line) @@ -44,4 +44,4 @@ class TestREPL(script: String) extends REPL { assert(false) } } -} \ No newline at end of file +} -- cgit v1.2.3 From 96cedcdcd82148f091989836eb4959b2c3ec3382 Mon Sep 17 00:00:00 2001 From: Felix Mulder Date: Tue, 26 Apr 2016 18:23:29 +0200 Subject: Highlight comments, remove scanner wrapping syntax highlighter --- src/dotty/tools/dotc/repl/SyntaxHighlighter.scala | 361 +++++++--------------- 1 file changed, 119 insertions(+), 242 deletions(-) diff --git a/src/dotty/tools/dotc/repl/SyntaxHighlighter.scala b/src/dotty/tools/dotc/repl/SyntaxHighlighter.scala index ebea3f8b2..1988ea7ad 100644 --- a/src/dotty/tools/dotc/repl/SyntaxHighlighter.scala +++ b/src/dotty/tools/dotc/repl/SyntaxHighlighter.scala @@ -2,6 +2,7 @@ package dotty.tools package dotc package repl +import parsing.Tokens._ import ammonite.terminal.FilterTools._ import ammonite.terminal.LazyList._ import ammonite.terminal.SpecialKeys._ @@ -12,29 +13,29 @@ 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) = Console.CYAN + str + Console.RESET - private def typeDef(str: String) = Console.GREEN + str + Console.RESET - private def literal(str: String) = Console.MAGENTA + str + Console.RESET - private def annotation(str: String) = Console.RED + str + Console.RESET + 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 = - "abstract" :: "class" :: "case" :: "catch" :: "def" :: - "do" :: "extends" :: "else" :: "false" :: "finally" :: - "final" :: "for" :: "forSome" :: "if" :: "import" :: - "implicit" :: "lazy" :: "match" :: "null" :: "new" :: - "object" :: "override" :: "private" :: "protected" :: "package" :: - "return" :: "sealed" :: "super" :: "true" :: "trait" :: - "type" :: "try" :: "this" :: "throw" :: "val" :: - "var" :: "with" :: "while" :: "yield" :: Nil + 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 + '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 = '{' :: '}' :: ')' :: '(' :: '=' :: ' ' :: ',' :: '.' :: Nil @@ -50,7 +51,7 @@ object SyntaxHighlighting { @inline def numberStart(c: Char) = c.isDigit && (!prev.isLetter || prev == '.' || prev == ' ' || prev == '(' || prev == '\u0000') - while(iter.hasNext) { + while (iter.hasNext) { val n = iter.next if (interpolationPrefixes.contains(n)) { // Interpolation prefixes are a superset of the keyword start chars @@ -73,6 +74,18 @@ object SyntaxHighlighting { } } else { (n: @switch) match { + case '/' => + if (iter.hasNext) { + iter.next match { + case '/' => eolComment() + case '*' => blockComment() + case x => { + newBuf += '/' + val (dup, _) = iter.duplicate + iter = List(x).toIterator ++ dup + } + } + } else newBuf += '/' case '=' => append('=', _ == "=>", keyword) case '<' => @@ -102,55 +115,97 @@ object SyntaxHighlighting { } } - def appendLiteral(delim: Char, nextTwo: Iterator[Char], multiline: Boolean = false) = { + def eolComment() = { + newBuf append (CommentColor + "//") + var curr = '/' + while (curr != '\n' && iter.hasNext) { + curr = iter.next + newBuf += curr + } + prev = curr + newBuf append NoColor + } + + def blockComment() = { + newBuf append (CommentColor + "/*") + var curr = '*' + var open = 1 + while (open > 0 && iter.hasNext) { + curr = iter.next + newBuf += curr + + if (curr == '*' && iter.hasNext) { + curr = iter.next + newBuf += curr + if (curr == '/') open -= 1 + } else if (curr == '/' && iter.hasNext) { + curr = iter.next + newBuf += curr + if (curr == '*') open += 1 + } + } + prev = curr + newBuf append NoColor + } + + def appendLiteral(delim: Char, succ: Iterator[Char], multiline: Boolean = false) = { var curr: Char = 0 var continue = true var closing = 0 val inInterpolation = interpolationPrefixes.contains(prev) - newBuf append (Console.MAGENTA + delim) + newBuf append (LiteralColor + delim) + + def shouldInterpolate = + inInterpolation && curr == '$' && prev != '$' && (iter.hasNext || succ.hasNext) + def interpolate() = { + val next: Char = if (succ.hasNext) succ.next else iter.next + if (next == '$') { + newBuf += curr + newBuf += next + prev = '$' + } else if (next == '{') { + newBuf append (KeywordColor + curr) + newBuf += next + if (iter.hasNext) { + var c = iter.next + while (iter.hasNext && c != '}') { + newBuf += c + c = iter.next + } + newBuf += c + newBuf append LiteralColor + } + } else { + newBuf append (KeywordColor + curr) + newBuf += next + var c: Char = 'a' + while (c.isLetterOrDigit && (iter.hasNext || succ.hasNext)) { + c = if (succ.hasNext) succ.next else iter.next + if (c != '"') newBuf += c + } + newBuf append LiteralColor + if (c == '"') { + newBuf += c + continue = false + } + } + closing = 0 + } - while (continue && (iter.hasNext || nextTwo.hasNext)) { - curr = if(nextTwo.hasNext) nextTwo.next else iter.next - if (curr == '\\' && (iter.hasNext || nextTwo.hasNext)) { - val next = if (nextTwo.hasNext) nextTwo.next else iter.next - newBuf append (Console.CYAN + curr) + while (continue && (iter.hasNext || succ.hasNext)) { + curr = if(succ.hasNext) succ.next else iter.next + if (curr == '\\' && (iter.hasNext || succ.hasNext)) { + val next = if (succ.hasNext) succ.next else iter.next + newBuf append (KeywordColor + curr) if (next == 'u') { val code = "u" + iter.take(4).mkString newBuf append code } else newBuf += next - newBuf append Console.MAGENTA - closing = 0 - } else if (inInterpolation && curr == '$' && prev != '$' && (iter.hasNext || nextTwo.hasNext)) { //TODO - break me out! - val next: Char = if (nextTwo.hasNext) nextTwo.next else iter.next - if (next == '$') { - newBuf += curr - newBuf += next - prev = '$' - } else if (next == '{') { - newBuf append (Console.CYAN + curr) - newBuf += next - if (iter.hasNext) { - var c = iter.next - while (iter.hasNext && c != '}') { - newBuf += c - c = iter.next - } - newBuf += c - newBuf append Console.MAGENTA - } - } else { //TODO - break me out - newBuf append (Console.CYAN + curr) - newBuf += next - var c: Char = 'a' - while (c.isLetterOrDigit && (iter.hasNext || nextTwo.hasNext)) { - c = if (nextTwo.hasNext) nextTwo.next else iter.next - if (c != '"') newBuf += c - } - newBuf append Console.MAGENTA - if (c == '"') newBuf += c - } + newBuf append LiteralColor closing = 0 + } else if (shouldInterpolate) { + interpolate() } else if (curr == delim && multiline) { closing += 1 if (closing == 3) continue = false @@ -163,8 +218,13 @@ object SyntaxHighlighting { closing = 0 } } - newBuf append Console.RESET + newBuf append NoColor prev = curr + + if (succ.hasNext) { + val (dup, _) = iter.duplicate + iter = succ ++ dup + } } def append(c: Char, shouldHL: String => Boolean, highlight: String => String, pre: Iterator[Char] = Iterator.empty) = { @@ -210,187 +270,4 @@ object SyntaxHighlighting { newBuf.toVector } - - import util.SourceFile - import parsing.Scanners.Scanner - import dotc.core.Contexts._ - import dotc.parsing.Tokens._ - import reporting._ - def apply(source: SourceFile)(implicit ctx: Context): Vector[Char] = { - val freshCtx = ctx.fresh.setReporter(new Reporter { - def doReport(d: Diagnostic)(implicit ctx: Context) = () - }) - val scanner = new Scanner(source, preserveWhitespace = true)(freshCtx) - val buf = new StringBuilder() - var prev = List(EMPTY) - - var realLength = 0 - /** `accept` is used to allow for `realLength` to be used to infer "magically - * missing tokens" - */ - def accept(str: String): String = { - realLength += str.length - str - } - - while (scanner.token != EOF) { - buf append (scanner.token match { - // Whitespace - case WHITESPACE => accept({ - if (prev.head == ERROR) scanner.litBuf.toString - else "" - } + scanner.strVal) - - // Identifiers - case BACKQUOTED_IDENT => - accept(s"""`${scanner.name.show}`""") - case id if identifierTokens contains id => { - val name = accept(scanner.name.show) - if (name.head.isUpper) typeDef(name) else name - } - - // Literals - case INTERPOLATIONID => - parseInterpStr() - case STRINGLIT => - literal(accept(s""""${scanner.strVal}"""")) - case CHARLIT => - literal(accept(s"""'${scanner.strVal}'""")) - case SYMBOLLIT => - accept("'" + scanner.strVal) - case lit if literalTokens contains lit => - literal(accept(scanner.strVal)) - - // Unclosed literals caught using startedLiteral var - case ERROR => - val start = scanner.startedLiteral - accept(if (start != null) start else "") - - // Keywords - case EQUALS | COLON => accept(scanner.name.show) - case k if alphaKeywords.contains(k) || symbolicKeywords.contains(k) => - keyword(accept(scanner.name.show)) - - // Other minor tokens (i.e. '{' etc) - case EMPTY => "" - case XMLSTART => accept("<") - case _ => accept(tokenString(scanner.token).replaceAll("\'", "")) - }) - prev = scanner.token :: prev - scanner.nextToken() - } - - def parseInterpStr(): String = { - // print InterpolationID 's' etc - val sb = new StringBuilder - sb append accept(scanner.name.show) - prev = scanner.token :: prev - scanner.nextToken() - - /** - * The composition of interpolated strings: - * s"hello $guy!" - * ^ ^^^^^^ ^ ^ - * | | | | - * | | | | - * | | | STRINGLIT - * | | IDENTIFIER - * | STRINGPART - * INTERPOLATIONID - * - * As such, get tokens until EOF or STRINGLIT is encountered - */ - def scan() = scanner.token match { - case STRINGPART => { - val delim = - if (scanner.inMultiLineInterpolation) "\"\"\"" - else "\"" - - if (prev.head == INTERPOLATIONID) literal(accept(delim)) - else "" - } + literal(accept(scanner.strVal)) - - case id if identifierTokens contains id => { - val name = scanner.name.show - // $ symbols are not caught by the scanner, infer them - val prefix = if (prev.head == STRINGPART) "$" else "" - accept(prefix + name) - } - case WHITESPACE => accept({ - // Whitespace occurs in interpolated strings where there - // is an error - e.g. unclosed string literal - // - // Or in blocks i.e. ${...WHITESPACE...} - if (prev.head == ERROR) scanner.litBuf.toString - else "" - } + scanner.strVal) - case CHARLIT => - literal(accept(s"""'${scanner.strVal}'""")) - case SYMBOLLIT => - accept("'" + scanner.strVal) - case lit if literalTokens contains lit => - literal(accept(scanner.strVal)) - case LBRACE => - // An `LBRACE` will only occur if it precedes a block, ergo we can - // infer "${" - accept("${") - case RBRACE => accept("}") - case ERROR => { - // FIXME: the behaviour here is weird, the check on line 329 clashes - // with encountering an error in the interpolated string. - // - // This clause should be the one taking care of the errors! - "" - } - case _ if prev.head == INTERPOLATIONID => - accept("\"") - case x => println(s"Unknown symbol: ${scanner.token}"); ??? - } - - while (scanner.token != EOF && scanner.token != STRINGLIT) { - sb append scan - prev = scanner.token :: prev - scanner.nextToken() - } - - val delim = - if (scanner.inMultiLineInterpolation) "\"\"\"" - else "\"" - - if (scanner.token == STRINGLIT) { - // If the last part of an interpolated string is a literal, it will end - // in `STRINGLIT` - if (prev.head == INTERPOLATIONID) sb append literal(accept(delim)) - sb append literal(accept(scanner.strVal + delim)) - } else if (prev.head == ERROR && prev.tail.head != IDENTIFIER) { - // If error entity to occur in an interpolation - val litBuf = scanner.litBuf.toString - val expectedLength = source.content.length - realLength += litBuf.length - val str = - if (realLength + 4 == expectedLength) "\"\"\"" + litBuf + "$" - else if (realLength + 3 == expectedLength) "\"\"\"" + litBuf - else if (realLength + 2 == expectedLength) "\"" + litBuf + "$" - else "\"" + litBuf - - sb append str - prev = -1 :: prev.tail // make sure outer doesn't print this as well - } else if (prev.head == ERROR && prev.tail.head == IDENTIFIER) { - // If an error is preceeded by an identifier, i.e. later in the interpolation - val litBuf = scanner.litBuf.toString - val expLen = source.content.length - - val suffix = "" //TODO - sb append (litBuf + suffix) - } else if (prev.head == IDENTIFIER) { - sb append scanner.litBuf.toString - } else if (prev.head == INTERPOLATIONID && scanner.token == EOF) { - sb append accept(delim) - sb append accept(scanner.litBuf.toString) - } - sb.toString - } - - buf.toVector - } } -- cgit v1.2.3 From d04984596c6abfa27b217b12a42caca26f0c269f Mon Sep 17 00:00:00 2001 From: Felix Mulder Date: Wed, 27 Apr 2016 12:00:29 +0200 Subject: Add multiline support using ammonite multilineFilter --- src/dotty/tools/dotc/repl/AmmoniteReader.scala | 22 ++-- src/dotty/tools/dotc/repl/InteractiveReader.scala | 6 +- src/dotty/tools/dotc/repl/InterpreterLoop.scala | 24 ++--- src/dotty/tools/dotc/repl/JLineReader.scala | 2 +- src/dotty/tools/dotc/repl/REPL.scala | 4 +- src/dotty/tools/dotc/repl/SimpleReader.scala | 2 +- src/dotty/tools/dotc/repl/SyntaxHighlighter.scala | 116 ++++++++++------------ test/test/TestREPL.scala | 2 +- 8 files changed, 83 insertions(+), 95 deletions(-) diff --git a/src/dotty/tools/dotc/repl/AmmoniteReader.scala b/src/dotty/tools/dotc/repl/AmmoniteReader.scala index 0c4dd80ee..a3b2a1c56 100644 --- a/src/dotty/tools/dotc/repl/AmmoniteReader.scala +++ b/src/dotty/tools/dotc/repl/AmmoniteReader.scala @@ -11,9 +11,15 @@ import BasicFilters._ import GUILikeFilters._ import util.SourceFile -class AmmoniteReader extends InteractiveReader { +class AmmoniteReader(val interpreter: Interpreter)(implicit ctx: Context) extends InteractiveReader { val interactive = true + def incompleteInput(str: String): Boolean = + interpreter.beQuietDuring(interpreter.interpret(str)) match { + case Interpreter.Incomplete => true + case _ => false // TODO: should perhaps save output here? + } + val reader = new java.io.InputStreamReader(System.in) val writer = new java.io.OutputStreamWriter(System.out) val cutPasteFilter = ReadlineFilters.CutPasteFilter() @@ -21,11 +27,11 @@ class AmmoniteReader extends InteractiveReader { val selectionFilter = GUILikeFilters.SelectionFilter(indent = 2) val multilineFilter: Filter = Filter("multilineFilter") { case TermState(lb ~: rest, b, c, _) - if (lb == 10 || lb == 13) => // Enter - + if (lb == 10 || lb == 13) && incompleteInput(b.mkString) => BasicFilters.injectNewLine(b, c, rest) } - def readLine(prompt: String)(implicit ctx: Context): String = { + + def readLine(prompt: String): String = { val historyFilter = new HistoryFilter( () => history.toVector, Console.BLUE, @@ -39,9 +45,8 @@ class AmmoniteReader extends InteractiveReader { GUILikeFilters.altFilter, GUILikeFilters.fnFilter, ReadlineFilters.navFilter, - //autocompleteFilter, cutPasteFilter, - //multilineFilter, + multilineFilter, BasicFilters.all ) @@ -52,7 +57,6 @@ class AmmoniteReader extends InteractiveReader { allFilters, displayTransform = (buffer, cursor) => { val ansiBuffer = Ansi.Str.parse(SyntaxHighlighting(buffer)) - //val ansiBuffer = Ansi.Str.parse(SyntaxHighlighting(new SourceFile("", buffer))) val (newBuffer, cursorOffset) = SelectionFilter.mangleBuffer( selectionFilter, ansiBuffer, cursor, Ansi.Reversed.On ) @@ -64,7 +68,9 @@ class AmmoniteReader extends InteractiveReader { (newNewBuffer, cursorOffset) } ) match { - case Some(s) => history = s :: history; s + case Some(res) => + history = res :: history; + res case None => ":q" } } diff --git a/src/dotty/tools/dotc/repl/InteractiveReader.scala b/src/dotty/tools/dotc/repl/InteractiveReader.scala index 6bab0a0c6..6ec6a0463 100644 --- a/src/dotty/tools/dotc/repl/InteractiveReader.scala +++ b/src/dotty/tools/dotc/repl/InteractiveReader.scala @@ -6,7 +6,7 @@ import dotc.core.Contexts.Context /** Reads lines from an input stream */ trait InteractiveReader { - def readLine(prompt: String)(implicit ctx: Context): String + def readLine(prompt: String): String val interactive: Boolean } @@ -16,9 +16,9 @@ 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 AmmoniteReader() + new AmmoniteReader(in) } catch { case e => //out.println("jline is not available: " + e) //debug e.printStackTrace() diff --git a/src/dotty/tools/dotc/repl/InterpreterLoop.scala b/src/dotty/tools/dotc/repl/InterpreterLoop.scala index 4ac9602e7..64414fec3 100644 --- a/src/dotty/tools/dotc/repl/InterpreterLoop.scala +++ b/src/dotty/tools/dotc/repl/InterpreterLoop.scala @@ -24,10 +24,10 @@ import scala.concurrent.ExecutionContext.Implicits.global 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,10 @@ 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)) + val line = in.readLine(prompt) interpreter.beQuietDuring( interpreter.interpret("val theAnswerToLifeInTheUniverseAndEverything = 21 * 2")) - Await.result(futLine, Duration.Inf) + line } /** The main read-eval-print loop for the interpreter. It calls @@ -178,23 +178,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 c7bdebb54..73463cd7c 100644 --- a/src/dotty/tools/dotc/repl/JLineReader.scala +++ b/src/dotty/tools/dotc/repl/JLineReader.scala @@ -12,5 +12,5 @@ class JLineReader extends InteractiveReader { val interactive = true - def readLine(prompt: String)(implicit ctx: Context) = reader.readLine(prompt) + def readLine(prompt: String) = reader.readLine(prompt) } 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 b69fcdd2a..5fab47bbe 100644 --- a/src/dotty/tools/dotc/repl/SimpleReader.scala +++ b/src/dotty/tools/dotc/repl/SimpleReader.scala @@ -14,7 +14,7 @@ class SimpleReader( extends InteractiveReader { def this() = this(Console.in, new PrintWriter(Console.out), true) - def readLine(prompt: String)(implicit ctx: Context) = { + def readLine(prompt: String) = { if (interactive) { out.print(prompt) out.flush() diff --git a/src/dotty/tools/dotc/repl/SyntaxHighlighter.scala b/src/dotty/tools/dotc/repl/SyntaxHighlighter.scala index 1988ea7ad..edb0ba040 100644 --- a/src/dotty/tools/dotc/repl/SyntaxHighlighter.scala +++ b/src/dotty/tools/dotc/repl/SyntaxHighlighter.scala @@ -40,9 +40,9 @@ object SyntaxHighlighting { private val typeEnders = '{' :: '}' :: ')' :: '(' :: '=' :: ' ' :: ',' :: '.' :: Nil - def apply(buffer: Iterable[Char]): Vector[Char] = { + def apply(chars: Iterable[Char]): Vector[Char] = { var prev: Char = 0 - var iter = buffer.toIterator + var remaining = chars.toStream val newBuf = new StringBuilder @inline def keywordStart = @@ -51,18 +51,24 @@ object SyntaxHighlighting { @inline def numberStart(c: Char) = c.isDigit && (!prev.isLetter || prev == '.' || prev == ' ' || prev == '(' || prev == '\u0000') - while (iter.hasNext) { - val n = iter.next + 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 = iter.take(3).mkString + val next = remaining.take(3).mkString if (next.startsWith("\"")) { newBuf += n prev = n - appendLiteral('"', next.toIterator.drop(1), next == "\"\"\"") + if (remaining.nonEmpty) takeChar() // drop 1 for appendLiteral + appendLiteral('"', next == "\"\"\"") } else { - val (dup, _ ) = iter.duplicate - iter = next.toIterator ++ dup if (n.isUpper && keywordStart) { appendWhile(n, !typeEnders.contains(_), typeDef) } else if (keywordStart) { @@ -75,15 +81,11 @@ object SyntaxHighlighting { } else { (n: @switch) match { case '/' => - if (iter.hasNext) { - iter.next match { + if (remaining.nonEmpty) { + takeChar() match { case '/' => eolComment() case '*' => blockComment() - case x => { - newBuf += '/' - val (dup, _) = iter.duplicate - iter = List(x).toIterator ++ dup - } + case x => newBuf += '/'; remaining = x #:: remaining } } else newBuf += '/' case '=' => @@ -92,19 +94,17 @@ object SyntaxHighlighting { append('<', { x => x == "<-" || x == "<:" || x == "<%" }, keyword) case '>' => append('>', { x => x == ">:" }, keyword) - case '#' => + case '#' if prev != ' ' && prev != '.' => newBuf append keyword("#") prev = '#' case '@' => appendWhile('@', _ != ' ', annotation) - case '\"' => iter.take(2).mkString match { - case "\"\"" => appendLiteral('\"', Iterator.empty, multiline = true) - case lit => appendLiteral('\"', lit.toIterator) - } + case '\"' => + appendLiteral('\"', multiline = remaining.take(2).mkString == "\"\"") case '\'' => - appendLiteral('\'', Iterator.empty) + appendLiteral('\'') case '`' => - appendUntil('`', _ == '`', none) + appendTo('`', _ == '`', none) case c if c.isUpper && keywordStart => appendWhile(c, !typeEnders.contains(_), typeDef) case c if numberStart(c) => @@ -118,8 +118,8 @@ object SyntaxHighlighting { def eolComment() = { newBuf append (CommentColor + "//") var curr = '/' - while (curr != '\n' && iter.hasNext) { - curr = iter.next + while (curr != '\n' && remaining.nonEmpty) { + curr = takeChar() newBuf += curr } prev = curr @@ -130,16 +130,16 @@ object SyntaxHighlighting { newBuf append (CommentColor + "/*") var curr = '*' var open = 1 - while (open > 0 && iter.hasNext) { - curr = iter.next + while (open > 0 && remaining.nonEmpty) { + curr = takeChar() newBuf += curr - if (curr == '*' && iter.hasNext) { - curr = iter.next + if (curr == '*' && remaining.nonEmpty) { + curr = takeChar() newBuf += curr if (curr == '/') open -= 1 - } else if (curr == '/' && iter.hasNext) { - curr = iter.next + } else if (curr == '/' && remaining.nonEmpty) { + curr = takeChar() newBuf += curr if (curr == '*') open += 1 } @@ -148,7 +148,7 @@ object SyntaxHighlighting { newBuf append NoColor } - def appendLiteral(delim: Char, succ: Iterator[Char], multiline: Boolean = false) = { + def appendLiteral(delim: Char, multiline: Boolean = false) = { var curr: Char = 0 var continue = true var closing = 0 @@ -156,32 +156,31 @@ object SyntaxHighlighting { newBuf append (LiteralColor + delim) def shouldInterpolate = - inInterpolation && curr == '$' && prev != '$' && (iter.hasNext || succ.hasNext) + inInterpolation && curr == '$' && prev != '$' && remaining.nonEmpty def interpolate() = { - val next: Char = if (succ.hasNext) succ.next else iter.next + 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 - if (iter.hasNext) { - var c = iter.next - while (iter.hasNext && c != '}') { - newBuf += c - c = iter.next - } + while (remaining.nonEmpty && open > 0) { + var c = takeChar() newBuf += c - newBuf append LiteralColor + 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 && (iter.hasNext || succ.hasNext)) { - c = if (succ.hasNext) succ.next else iter.next + while (c.isLetterOrDigit && remaining.nonEmpty) { + c = takeChar() if (c != '"') newBuf += c } newBuf append LiteralColor @@ -193,13 +192,13 @@ object SyntaxHighlighting { closing = 0 } - while (continue && (iter.hasNext || succ.hasNext)) { - curr = if(succ.hasNext) succ.next else iter.next - if (curr == '\\' && (iter.hasNext || succ.hasNext)) { - val next = if (succ.hasNext) succ.next else iter.next + while (continue && remaining.nonEmpty) { + curr = takeChar() + if (curr == '\\' && remaining.nonEmpty) { + val next = takeChar() newBuf append (KeywordColor + curr) if (next == 'u') { - val code = "u" + iter.take(4).mkString + val code = "u" + takeChars(4).mkString newBuf append code } else newBuf += next newBuf append LiteralColor @@ -220,18 +219,13 @@ object SyntaxHighlighting { } newBuf append NoColor prev = curr - - if (succ.hasNext) { - val (dup, _) = iter.duplicate - iter = succ ++ dup - } } - def append(c: Char, shouldHL: String => Boolean, highlight: String => String, pre: Iterator[Char] = Iterator.empty) = { + def append(c: Char, shouldHL: String => Boolean, highlight: String => String) = { var curr: Char = 0 val sb = new StringBuilder(s"$c") - while ((pre.hasNext || iter.hasNext) && curr != ' ' && curr != '(') { - curr = if (pre.hasNext) pre.next else iter.next + while (remaining.nonEmpty && curr != ' ' && curr != '(') { + curr = takeChar() if (curr != ' ') sb += curr } @@ -245,8 +239,8 @@ object SyntaxHighlighting { def appendWhile(c: Char, pred: Char => Boolean, highlight: String => String) = { var curr: Char = 0 val sb = new StringBuilder(s"$c") - while (iter.hasNext && pred(curr)) { - curr = iter.next + while (remaining.nonEmpty && pred(curr)) { + curr = takeChar() if (pred(curr)) sb += curr } @@ -256,15 +250,15 @@ object SyntaxHighlighting { prev = curr } - def appendUntil(c: Char, pred: Char => Boolean, highlight: String => String) = { + def appendTo(c: Char, pred: Char => Boolean, highlight: String => String) = { var curr: Char = 0 val sb = new StringBuilder(s"$c") - while (iter.hasNext && !pred(curr)) { - curr = iter.next + while (remaining.nonEmpty && !pred(curr)) { + curr = takeChar() sb += curr } - newBuf append (highlight(sb.toString)) + newBuf append highlight(sb.toString) prev = curr } diff --git a/test/test/TestREPL.scala b/test/test/TestREPL.scala index 53fcc0730..19a376b48 100644 --- a/test/test/TestREPL.scala +++ b/test/test/TestREPL.scala @@ -22,7 +22,7 @@ class TestREPL(script: String) extends REPL { override def input(implicit ctx: Context) = new InteractiveReader { val lines = script.lines - def readLine(prompt: String)(implicit ctx: Context): String = { + def readLine(prompt: String): String = { val line = lines.next if (line.startsWith(prompt) || line.startsWith(continuationPrompt)) { output.println(line) -- cgit v1.2.3 From 57670a38ca55cc04c9d765bdf04584cad5581d41 Mon Sep 17 00:00:00 2001 From: Felix Mulder Date: Wed, 27 Apr 2016 13:38:38 +0200 Subject: Stop interpreter from interpreting twice on enter --- src/dotty/tools/dotc/repl/AmmoniteReader.scala | 4 ++-- .../tools/dotc/repl/CompilingInterpreter.scala | 25 +++++++++++++++++++++- src/dotty/tools/dotc/repl/Interpreter.scala | 6 ++++++ src/dotty/tools/dotc/repl/InterpreterLoop.scala | 9 ++++++-- 4 files changed, 39 insertions(+), 5 deletions(-) diff --git a/src/dotty/tools/dotc/repl/AmmoniteReader.scala b/src/dotty/tools/dotc/repl/AmmoniteReader.scala index a3b2a1c56..0a49b8ea3 100644 --- a/src/dotty/tools/dotc/repl/AmmoniteReader.scala +++ b/src/dotty/tools/dotc/repl/AmmoniteReader.scala @@ -15,9 +15,9 @@ class AmmoniteReader(val interpreter: Interpreter)(implicit ctx: Context) extend val interactive = true def incompleteInput(str: String): Boolean = - interpreter.beQuietDuring(interpreter.interpret(str)) match { + interpreter.delayOutputDuring(interpreter.interpret(str)) match { case Interpreter.Incomplete => true - case _ => false // TODO: should perhaps save output here? + case _ => false } val reader = new java.io.InputStreamReader(System.in) diff --git a/src/dotty/tools/dotc/repl/CompilingInterpreter.scala b/src/dotty/tools/dotc/repl/CompilingInterpreter.scala index cfcce106e..d322aa404 100644 --- a/src/dotty/tools/dotc/repl/CompilingInterpreter.scala +++ b/src/dotty/tools/dotc/repl/CompilingInterpreter.scala @@ -78,6 +78,27 @@ 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: String = null + + override def lastOutput() = + if (previousOutput == null) None + else { + val ret = Some(previousOutput) + previousOutput = null + ret + } + + 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 = { @@ -188,7 +209,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) + else if (printResults || !succeeded) out.print(clean(interpreterResultString)) if (succeeded) { prevRequests += req diff --git a/src/dotty/tools/dotc/repl/Interpreter.scala b/src/dotty/tools/dotc/repl/Interpreter.scala index ea587a097..590baae0d 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(): Option[String] } diff --git a/src/dotty/tools/dotc/repl/InterpreterLoop.scala b/src/dotty/tools/dotc/repl/InterpreterLoop.scala index 64414fec3..53fd09c07 100644 --- a/src/dotty/tools/dotc/repl/InterpreterLoop.scala +++ b/src/dotty/tools/dotc/repl/InterpreterLoop.scala @@ -18,7 +18,7 @@ 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) { @@ -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 = interpreter.lastOutput() match { // don't interpret twice + case Some(oldRes) => + output.print(oldRes) + Some(line) + case _ => interpretStartingWith(line) + } (true, shouldReplay) } -- cgit v1.2.3 From 0f91408cb2d616c0ee0332b80e0d006bae3f9e9d Mon Sep 17 00:00:00 2001 From: Felix Mulder Date: Wed, 27 Apr 2016 13:44:19 +0200 Subject: Fix interpret dummy line before prompt displayed --- src/dotty/tools/dotc/repl/InterpreterLoop.scala | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/dotty/tools/dotc/repl/InterpreterLoop.scala b/src/dotty/tools/dotc/repl/InterpreterLoop.scala index 53fd09c07..98e47b635 100644 --- a/src/dotty/tools/dotc/repl/InterpreterLoop.scala +++ b/src/dotty/tools/dotc/repl/InterpreterLoop.scala @@ -74,10 +74,9 @@ class InterpreterLoop(compiler: Compiler, config: REPL.Config)(implicit ctx: Con * line of input to be entered. */ def firstLine(): String = { - val line = in.readLine(prompt) interpreter.beQuietDuring( interpreter.interpret("val theAnswerToLifeInTheUniverseAndEverything = 21 * 2")) - line + in.readLine(prompt) } /** The main read-eval-print loop for the interpreter. It calls @@ -171,7 +170,7 @@ class InterpreterLoop(compiler: Compiler, config: REPL.Config)(implicit ctx: Con case Some(oldRes) => output.print(oldRes) Some(line) - case _ => interpretStartingWith(line) + case None => interpretStartingWith(line) } (true, shouldReplay) -- cgit v1.2.3 From d1ec1407b88f70f4172ad48163f6c7e677a38958 Mon Sep 17 00:00:00 2001 From: Felix Mulder Date: Wed, 27 Apr 2016 14:14:50 +0200 Subject: Fix highlighting tokens after newline predated by '=' --- src/dotty/tools/dotc/repl/SyntaxHighlighter.scala | 6 +++--- src/dotty/tools/dotc/repl/ammonite/filters/BasicFilters.scala | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/dotty/tools/dotc/repl/SyntaxHighlighter.scala b/src/dotty/tools/dotc/repl/SyntaxHighlighter.scala index edb0ba040..527bcffc2 100644 --- a/src/dotty/tools/dotc/repl/SyntaxHighlighter.scala +++ b/src/dotty/tools/dotc/repl/SyntaxHighlighter.scala @@ -38,7 +38,7 @@ object SyntaxHighlighting { 'q' :: 'r' :: 's' :: 't' :: 'u' :: 'v' :: 'w' :: 'x' :: 'y' :: 'z' :: Nil private val typeEnders = - '{' :: '}' :: ')' :: '(' :: '=' :: ' ' :: ',' :: '.' :: Nil + '{' :: '}' :: ')' :: '(' :: '=' :: ' ' :: ',' :: '.' :: '\n' :: Nil def apply(chars: Iterable[Char]): Vector[Char] = { var prev: Char = 0 @@ -46,7 +46,7 @@ object SyntaxHighlighting { val newBuf = new StringBuilder @inline def keywordStart = - prev == 0 || prev == ' ' || prev == '{' || prev == '(' + prev == 0 || prev == ' ' || prev == '{' || prev == '(' || prev == '\n' @inline def numberStart(c: Char) = c.isDigit && (!prev.isLetter || prev == '.' || prev == ' ' || prev == '(' || prev == '\u0000') @@ -224,7 +224,7 @@ object SyntaxHighlighting { 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 != '(') { + while (remaining.nonEmpty && curr != ' ' && curr != '(' && curr != '\n') { curr = takeChar() if (curr != ' ') sb += curr } diff --git a/src/dotty/tools/dotc/repl/ammonite/filters/BasicFilters.scala b/src/dotty/tools/dotc/repl/ammonite/filters/BasicFilters.scala index 2c10ed5a7..b6329a109 100644 --- a/src/dotty/tools/dotc/repl/ammonite/filters/BasicFilters.scala +++ b/src/dotty/tools/dotc/repl/ammonite/filters/BasicFilters.scala @@ -21,7 +21,7 @@ object BasicFilters { exitFilter, enterFilter, clearFilter, - loggingFilter, + //loggingFilter, typingFilter ) -- cgit v1.2.3 From 4d53d338e59beb240716508e0b6d32c182a17df6 Mon Sep 17 00:00:00 2001 From: Felix Mulder Date: Wed, 27 Apr 2016 14:31:17 +0200 Subject: Fix error messages not being doubled and being on a new line --- .../tools/dotc/repl/CompilingInterpreter.scala | 38 ++++++++++++---------- src/dotty/tools/dotc/repl/Interpreter.scala | 2 +- src/dotty/tools/dotc/repl/InterpreterLoop.scala | 6 ++-- 3 files changed, 24 insertions(+), 22 deletions(-) diff --git a/src/dotty/tools/dotc/repl/CompilingInterpreter.scala b/src/dotty/tools/dotc/repl/CompilingInterpreter.scala index d322aa404..b3b7ab13c 100644 --- a/src/dotty/tools/dotc/repl/CompilingInterpreter.scala +++ b/src/dotty/tools/dotc/repl/CompilingInterpreter.scala @@ -80,15 +80,13 @@ class CompilingInterpreter(out: PrintWriter, ictx: Context) extends Compiler wit private var printResults: Boolean = true private var delayOutput: Boolean = false - var previousOutput: String = null - - override def lastOutput() = - if (previousOutput == null) None - else { - val ret = Some(previousOutput) - previousOutput = null - ret - } + var previousOutput: List[String] = Nil + + override def lastOutput() = { + val prev = previousOutput + previousOutput = Nil + prev + } override def delayOutputDuring[T](operation: => T): T = { val old = delayOutput @@ -113,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 + } } } @@ -210,7 +212,7 @@ class CompilingInterpreter(out: PrintWriter, ictx: Context) extends Compiler wit else { val (interpreterResultString, succeeded) = req.loadAndRun() if (delayOutput) - previousOutput = clean(interpreterResultString) + previousOutput = clean(interpreterResultString) :: previousOutput else if (printResults || !succeeded) out.print(clean(interpreterResultString)) if (succeeded) { diff --git a/src/dotty/tools/dotc/repl/Interpreter.scala b/src/dotty/tools/dotc/repl/Interpreter.scala index 590baae0d..6a292dfe2 100644 --- a/src/dotty/tools/dotc/repl/Interpreter.scala +++ b/src/dotty/tools/dotc/repl/Interpreter.scala @@ -38,5 +38,5 @@ trait Interpreter { def delayOutputDuring[T](operation: => T): T /** Gets the last output not printed immediately */ - def lastOutput(): Option[String] + def lastOutput(): List[String] } diff --git a/src/dotty/tools/dotc/repl/InterpreterLoop.scala b/src/dotty/tools/dotc/repl/InterpreterLoop.scala index 98e47b635..c6c750b56 100644 --- a/src/dotty/tools/dotc/repl/InterpreterLoop.scala +++ b/src/dotty/tools/dotc/repl/InterpreterLoop.scala @@ -167,10 +167,10 @@ class InterpreterLoop(compiler: Compiler, config: REPL.Config)(implicit ctx: Con output.println("Unknown command. Type :help for help.") else shouldReplay = interpreter.lastOutput() match { // don't interpret twice - case Some(oldRes) => - output.print(oldRes) + case Nil => interpretStartingWith(line) + case oldRes => + oldRes foreach output.print Some(line) - case None => interpretStartingWith(line) } (true, shouldReplay) -- cgit v1.2.3 From b5d6df22ab6bae982b6c840e1563d4bcbadd384b Mon Sep 17 00:00:00 2001 From: Felix Mulder Date: Wed, 27 Apr 2016 15:00:02 +0200 Subject: Fix `:...` commands printing erroneous messages on next newline --- src/dotty/tools/dotc/repl/InterpreterLoop.scala | 3 ++- src/dotty/tools/dotc/repl/ammonite/SpecialKeys.scala | 1 - 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/dotty/tools/dotc/repl/InterpreterLoop.scala b/src/dotty/tools/dotc/repl/InterpreterLoop.scala index c6c750b56..14a50fdf1 100644 --- a/src/dotty/tools/dotc/repl/InterpreterLoop.scala +++ b/src/dotty/tools/dotc/repl/InterpreterLoop.scala @@ -148,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 @@ -166,7 +167,7 @@ class InterpreterLoop(compiler: Compiler, config: REPL.Config)(implicit ctx: Con else if (line startsWith ":") output.println("Unknown command. Type :help for help.") else - shouldReplay = interpreter.lastOutput() match { // don't interpret twice + shouldReplay = lastOutput match { // don't interpret twice case Nil => interpretStartingWith(line) case oldRes => oldRes foreach output.print diff --git a/src/dotty/tools/dotc/repl/ammonite/SpecialKeys.scala b/src/dotty/tools/dotc/repl/ammonite/SpecialKeys.scala index 1a3278523..3b6a4a5a2 100644 --- a/src/dotty/tools/dotc/repl/ammonite/SpecialKeys.scala +++ b/src/dotty/tools/dotc/repl/ammonite/SpecialKeys.scala @@ -9,7 +9,6 @@ package ammonite.terminal */ object SpecialKeys { - /** * Lets you easily pattern match on characters modified by ctrl, * or convert a character into its ctrl-ed version -- cgit v1.2.3 From f010c62b8dc8baaaa9b87784d8664997bee95fa4 Mon Sep 17 00:00:00 2001 From: Felix Mulder Date: Wed, 27 Apr 2016 15:08:26 +0200 Subject: Revert Scanners and Tokens to their original form Since we decided to go with the non dotty-scanner approach these are unnecessary to have altered, might just as well revert them. --- src/dotty/tools/dotc/parsing/Scanners.scala | 49 +++++++---------------------- src/dotty/tools/dotc/parsing/Tokens.scala | 7 +---- test/test/TestREPL.scala | 2 +- tests/repl/imports.check | 4 +-- tests/repl/multilines.check | 33 ------------------- 5 files changed, 14 insertions(+), 81 deletions(-) delete mode 100644 tests/repl/multilines.check diff --git a/src/dotty/tools/dotc/parsing/Scanners.scala b/src/dotty/tools/dotc/parsing/Scanners.scala index 91332b01c..6fe86502f 100644 --- a/src/dotty/tools/dotc/parsing/Scanners.scala +++ b/src/dotty/tools/dotc/parsing/Scanners.scala @@ -45,9 +45,6 @@ object Scanners { /** the string value of a literal */ var strVal: String = null - /** the started parsing of a literal */ - var startedLiteral: String = null - /** the base of a number */ var base: Int = 0 @@ -177,13 +174,8 @@ object Scanners { } - class Scanner( - source: SourceFile, - override val startFrom: Offset = 0, - preserveWhitespace: Boolean = false - )(implicit ctx: Context) extends ScannerCommon(source)(ctx) { - val keepComments = ctx.settings.YkeepComments.value - val whitespace = new StringBuilder + class Scanner(source: SourceFile, override val startFrom: Offset = 0)(implicit ctx: Context) extends ScannerCommon(source)(ctx) { + val keepComments = ctx.settings.YkeepComments.value /** All doc comments as encountered, each list contains doc comments from * the same block level. Starting with the deepest level and going upward @@ -247,13 +239,13 @@ object Scanners { /** Are we directly in a string interpolation expression? */ - def inStringInterpolation = + private def inStringInterpolation = sepRegions.nonEmpty && sepRegions.head == STRINGLIT /** Are we directly in a multiline string interpolation expression? * @pre inStringInterpolation */ - def inMultiLineInterpolation = + private def inMultiLineInterpolation = inStringInterpolation && sepRegions.tail.nonEmpty && sepRegions.tail.head == STRINGPART /** read next token and return last offset @@ -324,7 +316,7 @@ object Scanners { token = if (pastBlankLine()) NEWLINES else NEWLINE } - if (!preserveWhitespace) postProcessToken() + postProcessToken() // print("[" + this +"]") } @@ -383,20 +375,9 @@ object Scanners { offset = charOffset - 1 (ch: @switch) match { case ' ' | '\t' | CR | LF | FF => - if (preserveWhitespace) { - while ((' ' :: '\t' :: CR :: LF :: FF :: Nil) contains ch) { - whitespace += ch - nextChar() - } - token = WHITESPACE - strVal = whitespace.toString - whitespace.clear() - } else { - nextChar() - fetchToken() - } - case c @ ( - 'A' | 'B' | 'C' | 'D' | 'E' | + nextChar() + fetchToken() + case 'A' | 'B' | 'C' | 'D' | 'E' | 'F' | 'G' | 'H' | 'I' | 'J' | 'K' | 'L' | 'M' | 'N' | 'O' | 'P' | 'Q' | 'R' | 'S' | 'T' | @@ -407,14 +388,12 @@ object Scanners { 'k' | 'l' | 'm' | 'n' | 'o' | 'p' | 'q' | 'r' | 's' | 't' | 'u' | 'v' | 'w' | 'x' | 'y' | - 'z') => + 'z' => putChar(ch) nextChar() getIdentRest() - if (ch == '"' && token == IDENTIFIER) { + if (ch == '"' && token == IDENTIFIER) token = INTERPOLATIONID - startedLiteral = "\"" - } case '<' => // is XMLSTART? def fetchLT() = { val last = if (charOffset >= 2) buf(charOffset - 2) else ' ' @@ -515,11 +494,9 @@ object Scanners { getLitChar() if (ch == '\'') { nextChar() - startedLiteral = null token = CHARLIT setStrVal() } else { - startedLiteral = "\'" error("unclosed character literal") } } @@ -709,12 +686,8 @@ object Scanners { if (ch == '"') { setStrVal() nextChar() - startedLiteral = null token = STRINGLIT - } else { - startedLiteral = "\"" - error("unclosed string literal") - } + } else error("unclosed string literal") } private def getRawStringLit(): Unit = { diff --git a/src/dotty/tools/dotc/parsing/Tokens.scala b/src/dotty/tools/dotc/parsing/Tokens.scala index 3ca86d624..b490cd133 100644 --- a/src/dotty/tools/dotc/parsing/Tokens.scala +++ b/src/dotty/tools/dotc/parsing/Tokens.scala @@ -141,7 +141,7 @@ abstract class TokensCommon { object Tokens extends TokensCommon { final val minToken = EMPTY - final val maxToken = COMMENT + final val maxToken = XMLSTART final val INTERPOLATIONID = 10; enter(INTERPOLATIONID, "string interpolator") final val SYMBOLLIT = 11; enter(SYMBOLLIT, "symbol literal") // TODO: deprecate @@ -188,11 +188,6 @@ object Tokens extends TokensCommon { /** XML mode */ final val XMLSTART = 96; enter(XMLSTART, "$XMLSTART$<") // TODO: deprecate - /** Whitespace */ - final val WHITESPACE = 97; enter(WHITESPACE, "whitespace") - final val COMMENT = 98; enter(COMMENT, "comment") - - final val alphaKeywords = tokenRange(IF, FORSOME) final val symbolicKeywords = tokenRange(USCORE, VIEWBOUND) final val symbolicTokens = tokenRange(COMMA, VIEWBOUND) diff --git a/test/test/TestREPL.scala b/test/test/TestREPL.scala index 19a376b48..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 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) - | } -: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 -- cgit v1.2.3 From 43f3916e149aa0c17b34d39b39193620415f2664 Mon Sep 17 00:00:00 2001 From: Felix Mulder Date: Thu, 28 Apr 2016 10:36:00 +0200 Subject: Fix stdin/out for repl launched by SBT Launching the repl with: `runMain dotty.tools.dotc.repl.Main` is now working correctly --- project/Build.scala | 4 ++++ 1 file changed, 4 insertions(+) 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" -- cgit v1.2.3 From 4fadce464b2ecf0d35f1d6ff00283d6ada2ff0be Mon Sep 17 00:00:00 2001 From: Felix Mulder Date: Thu, 28 Apr 2016 10:59:29 +0200 Subject: Rename old DottyRepl (used for power mode) using ILoop to TypeStealer --- test/test/DottyRepl.scala | 25 ------------------------- test/test/TypeStealer.scala | 25 +++++++++++++++++++++++++ 2 files changed, 25 insertions(+), 25 deletions(-) delete mode 100644 test/test/DottyRepl.scala create mode 100644 test/test/TypeStealer.scala diff --git a/test/test/DottyRepl.scala b/test/test/DottyRepl.scala deleted file mode 100644 index 74f6ee248..000000000 --- a/test/test/DottyRepl.scala +++ /dev/null @@ -1,25 +0,0 @@ -package test -import scala.tools.nsc.interpreter._ -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 { - def main(args: Array[String]): Unit = { - def repl = new ILoop {} - - val settings = new Settings - settings.Yreplsync.value = true - - - //use when launching normally outside SBT - settings.usejavacp.value = true - - //an alternative to 'usejavacp' setting, when launching from within SBT - //settings.embeddedDefaults[Repl.type] - - repl.process(settings) - } -} diff --git a/test/test/TypeStealer.scala b/test/test/TypeStealer.scala new file mode 100644 index 000000000..ae48d9a5b --- /dev/null +++ b/test/test/TypeStealer.scala @@ -0,0 +1,25 @@ +package test +import scala.tools.nsc.interpreter._ +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 TypeStealer { + def main(args: Array[String]): Unit = { + def repl = new ILoop {} + + val settings = new Settings + settings.Yreplsync.value = true + + + //use when launching normally outside SBT + settings.usejavacp.value = true + + //an alternative to 'usejavacp' setting, when launching from within SBT + //settings.embeddedDefaults[Repl.type] + + repl.process(settings) + } +} -- cgit v1.2.3 From 29fc55a67349145cbb23edf3f0fc0307bf9d515e Mon Sep 17 00:00:00 2001 From: Felix Mulder Date: Thu, 28 Apr 2016 11:01:33 +0200 Subject: Add Ammonite's MIT license --- .../tools/dotc/printing/SyntaxHighlighting.scala | 262 ++++++++++++++++++++ src/dotty/tools/dotc/repl/AmmoniteReader.scala | 1 + src/dotty/tools/dotc/repl/SyntaxHighlighter.scala | 267 --------------------- src/dotty/tools/dotc/repl/ammonite/Ansi.scala | 39 ++- src/dotty/tools/dotc/repl/ammonite/Filter.scala | 12 +- .../tools/dotc/repl/ammonite/FilterTools.scala | 20 +- src/dotty/tools/dotc/repl/ammonite/LICENSE | 25 ++ src/dotty/tools/dotc/repl/ammonite/Protocol.scala | 27 ++- .../tools/dotc/repl/ammonite/SpecialKeys.scala | 72 +++--- src/dotty/tools/dotc/repl/ammonite/Terminal.scala | 4 +- src/dotty/tools/dotc/repl/ammonite/Utils.scala | 38 +-- .../dotc/repl/ammonite/filters/BasicFilters.scala | 36 +-- .../repl/ammonite/filters/GUILikeFilters.scala | 74 +++--- .../dotc/repl/ammonite/filters/HistoryFilter.scala | 79 +++--- .../repl/ammonite/filters/ReadlineFilters.scala | 31 ++- .../dotc/repl/ammonite/filters/UndoFilter.scala | 14 +- 16 files changed, 509 insertions(+), 492 deletions(-) create mode 100644 src/dotty/tools/dotc/printing/SyntaxHighlighting.scala delete mode 100644 src/dotty/tools/dotc/repl/SyntaxHighlighter.scala create mode 100644 src/dotty/tools/dotc/repl/ammonite/LICENSE diff --git a/src/dotty/tools/dotc/printing/SyntaxHighlighting.scala b/src/dotty/tools/dotc/printing/SyntaxHighlighting.scala new file mode 100644 index 000000000..cbb9a5b39 --- /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 != ' ') sb += curr + } + + val str = sb.toString + val toAdd = if (shouldHL(str)) highlight(str) else str + val suffix = if (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 index 0a49b8ea3..e399105b3 100644 --- a/src/dotty/tools/dotc/repl/AmmoniteReader.scala +++ b/src/dotty/tools/dotc/repl/AmmoniteReader.scala @@ -10,6 +10,7 @@ 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 diff --git a/src/dotty/tools/dotc/repl/SyntaxHighlighter.scala b/src/dotty/tools/dotc/repl/SyntaxHighlighter.scala deleted file mode 100644 index 527bcffc2..000000000 --- a/src/dotty/tools/dotc/repl/SyntaxHighlighter.scala +++ /dev/null @@ -1,267 +0,0 @@ -package dotty.tools -package dotc -package repl - -import parsing.Tokens._ -import ammonite.terminal.FilterTools._ -import ammonite.terminal.LazyList._ -import ammonite.terminal.SpecialKeys._ -import ammonite.terminal.Filter -import ammonite.terminal._ -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 != ' ') sb += curr - } - - val str = sb.toString - val toAdd = if (shouldHL(str)) highlight(str) else str - val suffix = if (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/ammonite/Ansi.scala b/src/dotty/tools/dotc/repl/ammonite/Ansi.scala index 3cd2fc26c..37c4de7b5 100644 --- a/src/dotty/tools/dotc/repl/ammonite/Ansi.scala +++ b/src/dotty/tools/dotc/repl/ammonite/Ansi.scala @@ -18,6 +18,7 @@ object Ansi { 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) @@ -38,7 +39,7 @@ object Ansi { * Represents a set of [[Ansi.Attr]]s all occupying the same bit-space * in the state `Short` */ - sealed abstract class Category(){ + sealed abstract class Category() { val mask: Int val all: Seq[Attr] lazy val bitsMap = all.map{ m => m.applyMask -> m}.toMap @@ -47,7 +48,7 @@ object Ansi { } } - object Color extends Category{ + object Color extends Category { val mask = 15 << 7 val Reset = makeAttr(Some("\u001b[39m"), 0 << 7) @@ -66,7 +67,7 @@ object Ansi { ) } - object Back extends Category{ + object Back extends Category { val mask = 15 << 3 val Reset = makeAttr(Some("\u001b[49m"), 0 << 3) @@ -84,20 +85,22 @@ object Ansi { Blue, Magenta, Cyan, White ) } - object Bold extends Category{ + + 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{ + 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{ + + object Reversed extends Category { val mask = 1 << 2 val On = makeAttr(Some(Console.REVERSED), 1 << 2) val Off = makeAttr(None, 0 << 2) @@ -105,22 +108,14 @@ object Ansi { } val hardOffMask = Bold.mask | Underlined.mask | Reversed.mask - val categories = Vector( - Color, - Back, - Bold, - Underlined, - Reversed - ) + val categories = List(Color, Back, Bold, Underlined, Reversed) object Str { - - lazy val ansiRegex = "\u001B\\[[;\\d]*m".r + @sharable lazy val ansiRegex = "\u001B\\[[;\\d]*m".r implicit def parse(raw: CharSequence): Str = { - // This will - val chars = new Array[Char](raw.length) - val colors = new Array[Short](raw.length) + val chars = new Array[Char](raw.length) + val colors = new Array[Short](raw.length) var currentIndex = 0 var currentColor = 0.toShort @@ -147,7 +142,6 @@ object Ansi { Str(chars.take(currentIndex), colors.take(currentIndex)) } - } /** @@ -187,6 +181,7 @@ object Ansi { val (leftColors, rightColors) = colors.splitAt(index) (new Str(leftChars, leftColors), new Str(rightChars, rightColors)) } + def length = chars.length override def toString = render @@ -238,12 +233,9 @@ object Ansi { // 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]]. */ @@ -260,8 +252,5 @@ object Ansi { } new Str(chars, colorsOut) } - } - - } diff --git a/src/dotty/tools/dotc/repl/ammonite/Filter.scala b/src/dotty/tools/dotc/repl/ammonite/Filter.scala index b917f64d2..9d34bb0f2 100644 --- a/src/dotty/tools/dotc/repl/ammonite/Filter.scala +++ b/src/dotty/tools/dotc/repl/ammonite/Filter.scala @@ -4,28 +4,24 @@ package repl package ammonite.terminal object Filter { - def apply(id: String)(f: PartialFunction[TermInfo, TermAction]): Filter = - new Filter{ + new Filter { val op = f.lift def identifier = id } def wrap(id: String)(f: TermInfo => Option[TermAction]): Filter = - new Filter{ + new Filter { val op = f def identifier = id } - /** - * Merges multiple [[Filter]]s into one. - */ + /** 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() } diff --git a/src/dotty/tools/dotc/repl/ammonite/FilterTools.scala b/src/dotty/tools/dotc/repl/ammonite/FilterTools.scala index 4eed208b1..c18b6a927 100644 --- a/src/dotty/tools/dotc/repl/ammonite/FilterTools.scala +++ b/src/dotty/tools/dotc/repl/ammonite/FilterTools.scala @@ -2,6 +2,7 @@ package dotty.tools package dotc package repl package ammonite.terminal + /** * A collection of helpers that to simpify the common case of building filters */ @@ -12,8 +13,8 @@ object FilterTools { var splitIndex = 0 var length = 0 - while(length < in){ - ansiRegex.r.findPrefixOf(buffer.drop(splitIndex)) match{ + while(length < in) { + ansiRegex.r.findPrefixOf(buffer.drop(splitIndex)) match { case None => splitIndex += 1 length += 1 @@ -24,9 +25,6 @@ object FilterTools { 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 @@ -58,12 +56,9 @@ object FilterTools { def identifier = "Case" } - /** - * Shorthand for pattern matching on [[TermState]] - */ + /** 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 @@ -76,11 +71,10 @@ object FilterTools { (chunks, chunkStarts, chunkIndex) } - def firstRow(cursor: Int, buffer: Vector[Char], width: Int) = { + 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) = { + + 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 index 360220f27..34d31aeca 100644 --- a/src/dotty/tools/dotc/repl/ammonite/Protocol.scala +++ b/src/dotty/tools/dotc/repl/ammonite/Protocol.scala @@ -7,21 +7,24 @@ 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)] = { +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 - } + 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 index 3b6a4a5a2..d834cc10b 100644 --- a/src/dotty/tools/dotc/repl/ammonite/SpecialKeys.scala +++ b/src/dotty/tools/dotc/repl/ammonite/SpecialKeys.scala @@ -13,7 +13,7 @@ object SpecialKeys { * Lets you easily pattern match on characters modified by ctrl, * or convert a character into its ctrl-ed version */ - object Ctrl{ + object Ctrl { def apply(c: Char) = (c - 96).toChar.toString def unapply(i: Int): Option[Int] = Some(i + 96) } @@ -21,61 +21,61 @@ object SpecialKeys { /** * The string value you get when you hit the alt key */ - def Alt = "\u001b" + def Alt = "\u001b" - val Up = Alt+"[A" - val Down = Alt+"[B" + val Up = Alt+"[A" + val Down = Alt+"[B" val Right = Alt+"[C" - val Left = Alt+"[D" + val Left = Alt+"[D" - val Home = Alt+"OH" - val End = Alt+"OF" + 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 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 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 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 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 LinuxCtrlLeft = Alt+"[1;5D" - val FnAltUp = Alt*2+"[5~" - val FnAltDown = Alt*2+"[6~" - val FnAltRight = Alt+"[1;9F" - val FnAltLeft = Alt+"[1;9H" + 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 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 AltShiftUp = Alt+"[1;10A" + val AltShiftDown = Alt+"[1;10B" val AltShiftRight = Alt+"[1;10C" - val AltShiftLeft = Alt+"[1;10D" + val AltShiftLeft = Alt+"[1;10D" // Same as fn-alt-{up, down} -// val FnAltShiftUp = Alt*2+"[5~" -// val FnAltShiftDown = Alt*2+"[6~" +// val FnAltShiftUp = Alt*2+"[5~" +// val FnAltShiftDown = Alt*2+"[6~" val FnAltShiftRight = Alt+"[1;10F" - val FnAltShiftLeft = Alt+"[1;10H" + 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 index b4d080868..4b18b38e3 100644 --- a/src/dotty/tools/dotc/repl/ammonite/Terminal.scala +++ b/src/dotty/tools/dotc/repl/ammonite/Terminal.scala @@ -17,8 +17,6 @@ import scala.collection.mutable */ object Terminal { - val ansiRegex = "\u001B\\[[;\\d]*m".r - /** * Computes how tall a line of text is when wrapped at `width`. * @@ -104,7 +102,7 @@ object Terminal { } - type Action = (Vector[Char], Int) => (Vector[Char], Int) + type Action = (Vector[Char], Int) => (Vector[Char], Int) type MsgAction = (Vector[Char], Int) => (Vector[Char], Int, String) diff --git a/src/dotty/tools/dotc/repl/ammonite/Utils.scala b/src/dotty/tools/dotc/repl/ammonite/Utils.scala index ca2baf8bf..64a2c1476 100644 --- a/src/dotty/tools/dotc/repl/ammonite/Utils.scala +++ b/src/dotty/tools/dotc/repl/ammonite/Utils.scala @@ -3,12 +3,12 @@ package dotc package repl package ammonite.terminal -import java.io.{OutputStream, ByteArrayOutputStream, Writer} +import java.io.{FileOutputStream, Writer, File => JFile} import scala.annotation.tailrec /** - * Prints stuff to an ad-hoc logging file when running the ammonite-repl or - * ammonite-terminal in development mode in its SBT project. + * 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 @@ -17,18 +17,16 @@ import scala.annotation.tailrec * want without affecting the primary terminal you're using to interact with * Ammonite. */ -object Debug{ - lazy val debugOutput = { - if (System.getProperty("ammonite-sbt-build") != "true") ??? - else new java.io.FileOutputStream(new java.io.File("terminal/target/log")) - } - def apply(s: Any) = { +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){ +class AnsiNav(output: Writer) { def control(n: Int, c: Char) = output.write(s"\033[" + n + c) /** @@ -65,13 +63,14 @@ class AnsiNav(output: Writer){ */ def clearLine(n: Int) = control(n, 'K') } -object AnsiNav{ + +object AnsiNav { val resetUnderline = "\u001b[24m" val resetForegroundColor = "\u001b[39m" val resetBackgroundColor = "\u001b[49m" } -object TTY{ +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 @@ -123,7 +122,7 @@ object TTY{ /** * A truly-lazy implementation of scala.Stream */ -case class LazyList[T](headThunk: () => T, tailThunk: () => LazyList[T]){ +case class LazyList[T](headThunk: () => T, tailThunk: () => LazyList[T]) { var rendered = false lazy val head = { rendered = true @@ -148,17 +147,20 @@ case class LazyList[T](headThunk: () => T, tailThunk: () => LazyList[T]){ } s"LazyList(${(rec(this, Nil).reverse ++ Seq("...")).mkString(",")})" } + def ~:(other: => T) = LazyList(() => other, () => this) } -object LazyList{ - object ~:{ + +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){ + implicit class CS(ctx: StringContext) { val base = ctx.parts.mkString - object p{ + 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 index b6329a109..ebbcf2148 100644 --- a/src/dotty/tools/dotc/repl/ammonite/filters/BasicFilters.scala +++ b/src/dotty/tools/dotc/repl/ammonite/filters/BasicFilters.scala @@ -102,24 +102,21 @@ object BasicFilters { } 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) = { + 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) = { + + 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{ + val c1 = chunks.lift(chunkIndex + 1) match { case Some(next) => val boundary = chunkStarts(chunkIndex + 1) - 1 if ((boundary - c) > (w - currentColumn)) { @@ -133,19 +130,20 @@ object BasicFilters { b -> c1 } - - def moveUpDown(b: Vector[Char], - c: Int, - w: Int, - boundaryOffset: Int, - nextChunkOffset: Int, - checkRes: Int, - check: (Int, Int) => Boolean, - isDown: Boolean) = { + 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{ + else chunks.lift(chunkIndex + nextChunkOffset) match { case None => c + nextChunkOffset * 9999 case Some(next) => val boundary = chunkStarts(chunkIndex + boundaryOffset) @@ -155,9 +153,11 @@ object BasicFilters { 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 index 0f62df493..69a9769c6 100644 --- a/src/dotty/tools/dotc/repl/ammonite/filters/GUILikeFilters.scala +++ b/src/dotty/tools/dotc/repl/ammonite/filters/GUILikeFilters.scala @@ -18,17 +18,21 @@ import terminal._ * text selection, etc. */ object GUILikeFilters { - case class SelectionFilter(indent: Int) extends DelegateFilter{ + 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) = { + + 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) @@ -42,7 +46,7 @@ object GUILikeFilters { for((Seq(l, r), i) <- frags) yield { val slice = b.slice(l, r) if (i == 0) slice - else{ + else { val cut = slicer(slice) if (i == 1) firstOffset = cut @@ -64,16 +68,16 @@ object GUILikeFilters { 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)}, + 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, @@ -94,7 +98,7 @@ object GUILikeFilters { char != 10 /*enter*/) { mark = None TS(char ~: inputs, buffer, cursor) - }else{ + } 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 @@ -108,12 +112,15 @@ object GUILikeFilters { } ) } - object SelectionFilter{ - def mangleBuffer(selectionFilter: SelectionFilter, - string: Ansi.Str, - cursor: Int, - startColor: Ansi.Attr) = { - selectionFilter.mark match{ + + 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 @@ -131,21 +138,21 @@ object GUILikeFilters { 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)} + 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)} + 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)} + Case(FnAltShiftRight) {(b, c, m) => (b, c)}, + Case(FnAltShiftLeft) {(b, c, m) => (b, c)} ) @@ -160,5 +167,4 @@ object GUILikeFilters { // 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 index d2ecfaa09..dac1c9d23 100644 --- a/src/dotty/tools/dotc/repl/ammonite/filters/HistoryFilter.scala +++ b/src/dotty/tools/dotc/repl/ammonite/filters/HistoryFilter.scala @@ -14,9 +14,11 @@ import terminal._ * 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{ +class HistoryFilter( + history: () => IndexedSeq[String], + commentStartColor: String, + commentEndColor: String +) extends DelegateFilter { def identifier = "HistoryFilter" @@ -56,15 +58,18 @@ class HistoryFilter(history: () => IndexedSeq[String], up(Vector(), c) } - def searchHistory(start: Int, - increment: Int, - buffer: Vector[Char], - skipped: Vector[Char]) = { + 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{ + + 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 => @@ -97,22 +102,23 @@ class HistoryFilter(history: () => IndexedSeq[String], def activeHistory = searchTerm.nonEmpty || historyIndex != -1 def activeSearch = searchTerm.nonEmpty - def up(b: Vector[Char], c: Int) = { + + def up(b: Vector[Char], c: Int) = searchHistory(historyIndex + 1, 1, b, b) - } - def down(b: Vector[Char], c: Int) = { + + def down(b: Vector[Char], c: Int) = searchHistory(historyIndex - 1, -1, b, b) - } - def wrap(rest: LazyList[Int], out: (Vector[Char], Int, String)) = { + + 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) = { + + 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()) @@ -133,9 +139,8 @@ class HistoryFilter(history: () => IndexedSeq[String], * 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) = { + def searchOrHistoryAnd(cond: Boolean) = activeSearch || (activeHistory && cond) - } val dropHistoryChars = Set(9, 13, 10) // Tab or Enter @@ -149,7 +154,7 @@ class HistoryFilter(history: () => IndexedSeq[String], prelude.op(ti) match { case None => prevBuffer = Some(ti.ts.buffer) - filter0.op(ti) match{ + filter0.op(ti) match { case Some(ts: TermState) => prevBuffer = Some(ts.buffer) Some(ts) @@ -253,13 +258,14 @@ class HistoryFilter(history: () => IndexedSeq[String], } } -object HistoryFilter{ +object HistoryFilter { - - def mangleBuffer(historyFilter: HistoryFilter, - buffer: Ansi.Str, - cursor: Int, - startColor: Ansi.Attr) = { + def mangleBuffer( + historyFilter: HistoryFilter, + buffer: Ansi.Str, + cursor: Int, + startColor: Ansi.Attr + ) = { if (!historyFilter.activeSearch) buffer else { val (searchStart, searchEnd) = @@ -274,8 +280,8 @@ object HistoryFilter{ 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 @@ -284,17 +290,19 @@ object HistoryFilter{ * @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]) = { + 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{ + 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) @@ -306,22 +314,21 @@ object HistoryFilter{ val newHistoryIndex = rec(startIndex) val foundIndex = newHistoryIndex.find(_ != -1) - val newBuffer = foundIndex match{ + val newBuffer = foundIndex match { case None => searchTerm case Some(i) => history(i).toVector } - val newCursor = foundIndex match{ + 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 index d5fb9715e..eb79f2b04 100644 --- a/src/dotty/tools/dotc/repl/ammonite/filters/ReadlineFilters.scala +++ b/src/dotty/tools/dotc/repl/ammonite/filters/ReadlineFilters.scala @@ -38,8 +38,8 @@ object ReadlineFilters { // Ctrl-y paste last cut /** - * Basic readline-style navigation, using all the obscure alphabet - * hotkeys rather than using arrows + * 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 @@ -61,17 +61,16 @@ object ReadlineFilters { Case(Ctrl('t'))((b, c, m) => transposeLetter(b, c)) ) - def transposeLetter(b: Vector[Char], c: Int) = { + 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 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 @@ -80,18 +79,19 @@ object ReadlineFilters { 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){ + if (leftEnd0 == b.length && rightEnd == b.length) { val leftStart = GUILikeFilters.consumeWord(b, leftStart0 - 1, -1, 1) - val leftEnd = GUILikeFilters.consumeWord(b, leftStart, 1, 0) + 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) + b.slice(0, leftStart) ++ + b.slice(rightStart, rightEnd) ++ + b.slice(leftEnd, rightStart) ++ + b.slice(leftStart, leftEnd) ++ + b.slice(rightEnd, b.length) + (newB, rightEnd) } } @@ -100,7 +100,7 @@ object ReadlineFilters { * 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{ + case class CutPasteFilter() extends DelegateFilter { def identifier = "CutPasteFilter" var accumulating = false var currentCut = Vector.empty[Char] @@ -157,10 +157,9 @@ object ReadlineFilters { // 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}, + 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 index 4e0398aa8..c265a7a4c 100644 --- a/src/dotty/tools/dotc/repl/ammonite/filters/UndoFilter.scala +++ b/src/dotty/tools/dotc/repl/ammonite/filters/UndoFilter.scala @@ -50,6 +50,7 @@ case class UndoFilter(maxUndo: Int = 25) extends DelegateFilter { */ 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 @@ -61,6 +62,7 @@ case class UndoFilter(maxUndo: Int = 25) extends DelegateFilter { val (b1, c1) = currentUndo (b1, c1, msg) } + def redo(b: Vector[Char], c: Int) = { val msg = if (undoIndex <= 0) UndoFilter.cannotRedoMsg @@ -74,13 +76,13 @@ case class UndoFilter(maxUndo: Int = 25) extends DelegateFilter { 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 @@ -114,10 +116,10 @@ case class UndoFilter(maxUndo: Int = 25) extends DelegateFilter { undoBuffer.remove(undoBuffer.length - undoIndex, undoIndex) undoIndex = 0 - if(undoBuffer.length == maxUndo) undoBuffer.remove(0) + if (undoBuffer.length == maxUndo) undoBuffer.remove(0) undoBuffer.append(b -> c) - }else if (undoIndex == 0 && (b, c) != undoBuffer(undoBuffer.length - 1)) { + } else if (undoIndex == 0 && (b, c) != undoBuffer(undoBuffer.length - 1)) { undoBuffer(undoBuffer.length - 1) = (b, c) } @@ -147,9 +149,9 @@ object UndoState { val Navigating = new UndoState("Navigating") } -object UndoFilter{ - val undoMsg = Ansi.Color.Blue(" ...undoing last action, `Alt -` or `Esc -` to redo") +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 redoMsg = Ansi.Color.Blue(" ...redoing last action") val cannotRedoMsg = Ansi.Color.Blue(" ...no more actions to redo") } -- cgit v1.2.3 From 1582959b3ed6c676e5e3265282046363d2c06cec Mon Sep 17 00:00:00 2001 From: Felix Mulder Date: Fri, 29 Apr 2016 10:52:13 +0200 Subject: Fix keywords sometimes not highlighted in multiln When enter pressed immediately after keyword, the highlighting would be aborted --- src/dotty/tools/dotc/printing/SyntaxHighlighting.scala | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/dotty/tools/dotc/printing/SyntaxHighlighting.scala b/src/dotty/tools/dotc/printing/SyntaxHighlighting.scala index cbb9a5b39..14b3a5050 100644 --- a/src/dotty/tools/dotc/printing/SyntaxHighlighting.scala +++ b/src/dotty/tools/dotc/printing/SyntaxHighlighting.scala @@ -221,12 +221,12 @@ object SyntaxHighlighting { val sb = new StringBuilder(s"$c") while (remaining.nonEmpty && curr != ' ' && curr != '(' && curr != '\n') { curr = takeChar() - if (curr != ' ') sb += curr + if (curr != ' ' && curr != '\n') sb += curr } val str = sb.toString val toAdd = if (shouldHL(str)) highlight(str) else str - val suffix = if (curr == ' ') " " else "" + val suffix = if (curr == ' ' || curr == '\n') s"$curr" else "" newBuf append (toAdd + suffix) prev = curr } -- cgit v1.2.3