From 97acdfd10516a008c7c4dc5b9e5562f1366b2f04 Mon Sep 17 00:00:00 2001 From: Allan Renucci Date: Mon, 28 May 2018 13:47:48 +0200 Subject: [PATCH 01/18] Use JLine 3 in the REPL --- .../tools/dotc/interactive/Interactive.scala | 6 +- .../dotty/tools/dotc/reporting/Reporter.scala | 6 + .../src/dotty/tools/repl/AmmoniteReader.scala | 142 -------- .../src/dotty/tools/repl/JLineTerminal.scala | 125 +++++++ .../src/dotty/tools/repl/ParseResult.scala | 2 +- .../src/dotty/tools/repl/ReplCompiler.scala | 4 +- .../src/dotty/tools/repl/ReplDriver.scala | 198 +++++------ compiler/src/dotty/tools/repl/package.scala | 2 +- .../src/dotty/tools/repl/terminal/Ansi.scala | 257 -------------- .../dotty/tools/repl/terminal/Filter.scala | 105 ------ .../tools/repl/terminal/FilterTools.scala | 48 --- .../src/dotty/tools/repl/terminal/LICENSE | 25 -- .../dotty/tools/repl/terminal/Protocol.scala | 39 --- .../tools/repl/terminal/SpecialKeys.scala | 84 ----- .../dotty/tools/repl/terminal/Terminal.scala | 326 ----------------- .../src/dotty/tools/repl/terminal/Utils.scala | 236 ------------- .../repl/terminal/filters/BasicFilters.scala | 163 --------- .../terminal/filters/GUILikeFilters.scala | 168 --------- .../repl/terminal/filters/HistoryFilter.scala | 330 ------------------ .../terminal/filters/ReadlineFilters.scala | 211 ----------- .../repl/terminal/filters/UndoFilter.scala | 154 -------- .../dotty/tools/repl/TabcompleteTests.scala | 65 ++-- project/Build.scala | 1 + 23 files changed, 255 insertions(+), 2442 deletions(-) delete mode 100644 compiler/src/dotty/tools/repl/AmmoniteReader.scala create mode 100644 compiler/src/dotty/tools/repl/JLineTerminal.scala delete mode 100644 compiler/src/dotty/tools/repl/terminal/Ansi.scala delete mode 100644 compiler/src/dotty/tools/repl/terminal/Filter.scala delete mode 100644 compiler/src/dotty/tools/repl/terminal/FilterTools.scala delete mode 100644 compiler/src/dotty/tools/repl/terminal/LICENSE delete mode 100644 compiler/src/dotty/tools/repl/terminal/Protocol.scala delete mode 100644 compiler/src/dotty/tools/repl/terminal/SpecialKeys.scala delete mode 100644 compiler/src/dotty/tools/repl/terminal/Terminal.scala delete mode 100644 compiler/src/dotty/tools/repl/terminal/Utils.scala delete mode 100644 compiler/src/dotty/tools/repl/terminal/filters/BasicFilters.scala delete mode 100644 compiler/src/dotty/tools/repl/terminal/filters/GUILikeFilters.scala delete mode 100644 compiler/src/dotty/tools/repl/terminal/filters/HistoryFilter.scala delete mode 100644 compiler/src/dotty/tools/repl/terminal/filters/ReadlineFilters.scala delete mode 100644 compiler/src/dotty/tools/repl/terminal/filters/UndoFilter.scala diff --git a/compiler/src/dotty/tools/dotc/interactive/Interactive.scala b/compiler/src/dotty/tools/dotc/interactive/Interactive.scala index 5c17f62a79da..e051a144b1b3 100644 --- a/compiler/src/dotty/tools/dotc/interactive/Interactive.scala +++ b/compiler/src/dotty/tools/dotc/interactive/Interactive.scala @@ -219,9 +219,9 @@ object Interactive { case _ => getScopeCompletions(ctx) } - val sortedCompletions = completions.toList.sortBy(_.name: Name) - interactiv.println(i"completion with pos = $pos, prefix = $prefix, termOnly = $termOnly, typeOnly = $typeOnly = $sortedCompletions%, %") - (completionPos, sortedCompletions) + val completionList = completions.toList + interactiv.println(i"completion with pos = $pos, prefix = $prefix, termOnly = $termOnly, typeOnly = $typeOnly = $completionList%, %") + (completionPos, completionList) } /** Possible completions of members of `prefix` which are accessible when called inside `boundary` */ diff --git a/compiler/src/dotty/tools/dotc/reporting/Reporter.scala b/compiler/src/dotty/tools/dotc/reporting/Reporter.scala index f97d8b7a246a..511ae7c6c074 100644 --- a/compiler/src/dotty/tools/dotc/reporting/Reporter.scala +++ b/compiler/src/dotty/tools/dotc/reporting/Reporter.scala @@ -23,6 +23,12 @@ object Reporter { simple.report(m) } } + + /** A reporter that ignores reports */ + object NoReporter extends Reporter { + def doReport(m: MessageContainer)(implicit ctx: Context) = () + override def report(m: MessageContainer)(implicit ctx: Context): Unit = () + } } import Reporter._ diff --git a/compiler/src/dotty/tools/repl/AmmoniteReader.scala b/compiler/src/dotty/tools/repl/AmmoniteReader.scala deleted file mode 100644 index ffbc7e61d100..000000000000 --- a/compiler/src/dotty/tools/repl/AmmoniteReader.scala +++ /dev/null @@ -1,142 +0,0 @@ -package dotty.tools -package repl - -import java.io.{ - InputStream, InputStreamReader, OutputStream, OutputStreamWriter -} - -import dotc.core.Contexts.Context -import dotc.printing.SyntaxHighlighting -import dotc.printing.Highlighting -import dotc.interactive.InteractiveDriver - -import terminal._ -import terminal.filters._ -import GUILikeFilters._ -import LazyList._ -import AmmoniteReader._ - -/** Adaptation of the Ammonite shell emulator to fit the Dotty REPL - * - * Credit for the code in the `terminal` goes to Li Haoyi - * who wrote most of it as part of Ammonite. - * - * @param history is a list of the previous input with the latest entry as head - * @param complete takes a function from cursor point and input string to - * `Completions` - */ -private[repl] class AmmoniteReader(out: OutputStream, - history: History, - complete: (Int, String) => Completions) - (implicit ctx: Context) { - - private[this] val reader = new InputStreamReader(System.in) - private[this] val writer = new OutputStreamWriter(out) - - private[this] val cutPasteFilter = ReadlineFilters.CutPasteFilter() - private[this] val selectionFilter = GUILikeFilters.SelectionFilter(indent = 2) - private[this] val multilineFilter = Filter.partial("multilineFilter") { - case TermState(lb ~: rest, b, c, d) if (lb == 10 || lb == 13) => - val source = b.mkString - - if (ParseResult.isIncomplete(source)) - BasicFilters.injectNewLine(b, c, rest, indent = 2) - else - Result(source) // short-circuit the filters - } - - /** Blockingly read line from `System.in` - * - * This entry point into Ammonite handles everything to do with terminal - * emulation. This includes: - * - * - Multiline support - * - Copy-pasting - * - History - * - Syntax highlighting - * - * To facilitate this, filters are used. The terminal emulation, however, - * does not render output - simply input. - */ - def prompt: ParseResult = { - val historyFilter = new HistoryFilter( - () => history.toVector, - Console.BLUE, - AnsiNav.resetForegroundColor - ) - - val autocompleteFilter: Filter = Filter.action("autocompleteFilter")(SpecialKeys.Tab :: Nil) { - case TermState(rest, b, c, _) => - val Completions(newCursor, completions, details) = complete(c, b.mkString) - lazy val prefixDetails = FrontEndUtils.findPrefix(details) - val details2 = details.map { det => - val (left, right) = det.splitAt(prefixDetails.length) - (Highlighting.Green(left) + Highlighting.Cyan(right)).toString - } - - lazy val prefixComp = FrontEndUtils.findPrefix(completions) - val completions2: Seq[String] = for(comp <- completions) yield { - val (left, right) = comp.splitAt(prefixComp.length) - (Highlighting.Green(left).toString ++ right) - } - - val stdout = FrontEndUtils.printCompletions(completions2, details2).mkString - - if (details.nonEmpty || completions.isEmpty) - Printing(TermState(rest, b, c), stdout) - else { - val newBuffer = b.take(newCursor) ++ prefixComp ++ b.drop(c) - Printing(TermState(rest, newBuffer, newCursor + prefixComp.length), stdout) - } - } - - val allFilters = Filter.merge( - UndoFilter(), - historyFilter, - selectionFilter, - GUILikeFilters.altFilter, - GUILikeFilters.fnFilter, - ReadlineFilters.navFilter, - cutPasteFilter, - autocompleteFilter, - multilineFilter, - BasicFilters.all - ) - - def displayTransform(buffer: Vector[Char], cursor: Int): (Ansi.Str, Int) = { - val coloredBuffer = - SyntaxHighlighting(buffer) - - val ansiBuffer = Ansi.Str.parse(coloredBuffer.toVector) - val (newBuffer, cursorOffset) = SelectionFilter.mangleBuffer( - selectionFilter, ansiBuffer, cursor, Ansi.Reversed.On - ) - val newNewBuffer = HistoryFilter.mangleBuffer( - historyFilter, newBuffer, cursor, - Ansi.Color.Green - ) - - (newNewBuffer, cursorOffset) - } - - val prompt = Console.BLUE + "scala> " + Console.RESET - - Terminal - .readLine(prompt, reader, writer, allFilters, displayTransform) - .map { - case Result(source) => ParseResult(source) - case Interrupt => SigKill - } - .getOrElse(Quit) - } -} - -object AmmoniteReader { - type History = List[String] - - def apply(out: OutputStream, - history: History, - complete: (Int, String) => Completions) - (implicit ctx: Context) = - new AmmoniteReader(out, history, complete) -} diff --git a/compiler/src/dotty/tools/repl/JLineTerminal.scala b/compiler/src/dotty/tools/repl/JLineTerminal.scala new file mode 100644 index 000000000000..ebae2ec39f36 --- /dev/null +++ b/compiler/src/dotty/tools/repl/JLineTerminal.scala @@ -0,0 +1,125 @@ +package dotty.tools.repl + +import dotty.tools.dotc.core.Contexts.Context +import dotty.tools.dotc.parsing.Scanners.Scanner +import dotty.tools.dotc.parsing.Tokens._ +import dotty.tools.dotc.printing.SyntaxHighlighting +import dotty.tools.dotc.reporting.Reporter +import dotty.tools.dotc.util.SourceFile +import org.jline.reader +import org.jline.reader.LineReader.Option +import org.jline.reader.Parser.ParseContext +import org.jline.reader._ +import org.jline.reader.impl.history.DefaultHistory +import org.jline.terminal.TerminalBuilder +import org.jline.utils.AttributedString + +final class JLineTerminal { + private val terminal = TerminalBuilder.terminal() + private val history = new DefaultHistory + + private def blue(str: String) = Console.BLUE + str + Console.RESET + private val prompt = blue("scala> ") + private val newLinePrompt = blue(" | ") + + /** Blockingly read line from `System.in` + * + * This entry point into JLine handles everything to do with terminal + * emulation. This includes: + * + * - Multi-line support + * - Copy-pasting + * - History + * - Syntax highlighting + * - Auto-completions + * + * @throws EndOfFileException This exception is thrown when the user types Ctrl-D. + */ + def readLine( + completer: Completer // provide auto-completions + )(implicit ctx: Context): String = { + val lineReader = LineReaderBuilder.builder() + .terminal(terminal) + .history(history) + .completer(completer) + .highlighter(new Highlighter) + .parser(new Parser) + .variable(LineReader.SECONDARY_PROMPT_PATTERN, "%M") + .option(Option.INSERT_TAB, true) // at the beginning of the line, insert tab instead of completing + .option(Option.AUTO_FRESH_LINE, true) // if not at start of line before prompt, move to new line + .build() + + lineReader.readLine(prompt) + } + + /** Provide syntax highlighting */ + private class Highlighter extends reader.Highlighter { + def highlight(reader: LineReader, buffer: String): AttributedString = { + val highlighted = SyntaxHighlighting(buffer).mkString + AttributedString.fromAnsi(highlighted) + } + } + + /** Provide multi-line editing support */ + private class Parser(implicit ctx: Context) extends reader.Parser { + + private class ParsedLine( + val cursor: Int, // The cursor position within the line + val line: String, // The unparsed line + val word: String, // The current word being completed + val wordCursor: Int // The cursor position within the current word + ) extends reader.ParsedLine { + // Using dummy values, not sure what they are used for + def wordIndex = -1 + def words = java.util.Collections.emptyList[String] + } + + def parse(line: String, cursor: Int, context: ParseContext): reader.ParsedLine = { + def parsedLine(word: String, wordCursor: Int) = + new ParsedLine(cursor, line, word, wordCursor) + + def incomplete(): Nothing = throw new EOFError( + // Using dummy values, not sure what they are used for + /* line = */ -1, + /* column = */ -1, + /* message = */ "", + /* missing = */ newLinePrompt) + + context match { + case ParseContext.ACCEPT_LINE => + // TODO: take into account cursor position + if (ParseResult.isIncomplete(line)) incomplete() + else parsedLine("", 0) + // using dummy values, + // resulting parsed line is probably unused + + case ParseContext.COMPLETE => + // Parse to find completions (typically after a Tab). + val source = new SourceFile("", line.toCharArray) + val scanner = new Scanner(source)(ctx.fresh.setReporter(Reporter.NoReporter)) + + // Looking for the current word being completed + // and the cursor position within this word + while (scanner.token != EOF) { + val start = scanner.offset + val token = scanner.token + scanner.nextToken() + val end = scanner.lastOffset + + val isCompletable = + isIdentifier(token) || isKeyword(token) // keywords can start identifiers + def isCurrentWord = cursor >= start && cursor <= end + if (isCompletable && isCurrentWord) { + val word = line.substring(start, end) + val wordCursor = cursor - start + return parsedLine(word, wordCursor) + } + } + parsedLine("", 0) // no word being completed + + case _ => + incomplete() + } + } + } +} diff --git a/compiler/src/dotty/tools/repl/ParseResult.scala b/compiler/src/dotty/tools/repl/ParseResult.scala index 7a16fa0a26ca..d7a7574da058 100644 --- a/compiler/src/dotty/tools/repl/ParseResult.scala +++ b/compiler/src/dotty/tools/repl/ParseResult.scala @@ -141,7 +141,7 @@ object ParseResult { sourceCode match { case CommandExtract(_) | "" => false case _ => { - val reporter = storeReporter + val reporter = newStoreReporter var needsMore = false reporter.withIncompleteHandler(_ => _ => needsMore = true) { parseStats(sourceCode)(ctx.fresh.setReporter(reporter)) diff --git a/compiler/src/dotty/tools/repl/ReplCompiler.scala b/compiler/src/dotty/tools/repl/ReplCompiler.scala index c6e7e2f49a1c..570d8111ec2a 100644 --- a/compiler/src/dotty/tools/repl/ReplCompiler.scala +++ b/compiler/src/dotty/tools/repl/ReplCompiler.scala @@ -42,7 +42,7 @@ class ReplCompiler(val directory: AbstractFile) extends Compiler { def newRun(initCtx: Context, objectIndex: Int) = new Run(this, initCtx) { override protected[this] def rootContext(implicit ctx: Context) = - addMagicImports(super.rootContext.fresh.setReporter(storeReporter)) + addMagicImports(super.rootContext.fresh.setReporter(newStoreReporter)) private def addMagicImports(initCtx: Context): Context = { def addImport(path: TermName)(implicit ctx: Context) = { @@ -228,7 +228,7 @@ class ReplCompiler(val directory: AbstractFile) extends Compiler { wrapped(expr, src, state)(runCtx).flatMap { pkg => val unit = new CompilationUnit(src) unit.untpdTree = pkg - run.compileUnits(unit :: Nil, runCtx.fresh.setReporter(storeReporter)) + run.compileUnits(unit :: Nil, runCtx.fresh.setReporter(newStoreReporter)) if (errorsAllowed || !reporter.hasErrors) unwrapped(unit.tpdTree, src)(runCtx) diff --git a/compiler/src/dotty/tools/repl/ReplDriver.scala b/compiler/src/dotty/tools/repl/ReplDriver.scala index f279d44b7f8a..114d4307122f 100644 --- a/compiler/src/dotty/tools/repl/ReplDriver.scala +++ b/compiler/src/dotty/tools/repl/ReplDriver.scala @@ -1,38 +1,31 @@ -package dotty.tools -package repl - -import java.io.{ InputStream, PrintStream } +package dotty.tools.repl + +import java.io.PrintStream + +import dotty.tools.dotc.ast.Trees._ +import dotty.tools.dotc.ast.{tpd, untpd} +import dotty.tools.dotc.core.Contexts.Context +import dotty.tools.dotc.core.Denotations.Denotation +import dotty.tools.dotc.core.Flags._ +import dotty.tools.dotc.core.Mode +import dotty.tools.dotc.core.NameKinds.SimpleNameKind +import dotty.tools.dotc.core.NameOps._ +import dotty.tools.dotc.core.Names.Name +import dotty.tools.dotc.core.StdNames._ +import dotty.tools.dotc.core.Symbols.{Symbol, defn} +import dotty.tools.dotc.core.Types._ +import dotty.tools.dotc.interactive.Interactive +import dotty.tools.dotc.printing.SyntaxHighlighting +import dotty.tools.dotc.reporting.MessageRendering +import dotty.tools.dotc.reporting.diagnostic.{Message, MessageContainer} +import dotty.tools.dotc.util.Positions.Position +import dotty.tools.dotc.util.{SourceFile, SourcePosition} +import dotty.tools.dotc.{CompilationUnit, Driver, Run} +import dotty.tools.io._ +import org.jline.reader._ import scala.annotation.tailrec - -import dotc.reporting.MessageRendering -import dotc.reporting.diagnostic.MessageContainer -import dotc.ast.untpd -import dotc.ast.tpd -import dotc.interactive.{ SourceTree, Interactive } -import dotc.core.Contexts.Context -import dotc.{ CompilationUnit, Run } -import dotc.core.Mode -import dotc.core.Flags._ -import dotc.core.Types._ -import dotc.core.StdNames._ -import dotc.core.Names.Name -import dotc.core.NameOps._ -import dotc.core.Symbols.{ Symbol, NoSymbol, defn } -import dotc.core.Denotations.Denotation -import dotc.core.Types.{ ExprType, ConstantType } -import dotc.core.NameKinds.SimpleNameKind -import dotc.config.CompilerCommand -import dotc.{ Compiler, Driver } -import dotc.printing.SyntaxHighlighting -import dotc.reporting.diagnostic.Message -import dotc.util.Positions.Position -import dotc.util.SourcePosition - -import io._ - -import AmmoniteReader._ -import results._ +import scala.collection.JavaConverters._ /** The state of the REPL contains necessary bindings instead of having to have * mutation @@ -50,7 +43,6 @@ import results._ * * @param objectIndex the index of the next wrapper * @param valIndex the index of next value binding for free expressions - * @param history a list of user inputs as strings * @param imports a list of tuples of imports on tree form and shown form * @param run the latest run initiated at the start of interpretation. This * run and its context should be used in order to perform any @@ -58,27 +50,13 @@ import results._ */ case class State(objectIndex: Int, valIndex: Int, - history: History, imports: List[untpd.Import], run: Run) { - def withHistory(newEntry: String) = copy(history = newEntry :: history) - - def withHistory(h: History) = copy(history = h) - def newRun(comp: ReplCompiler, rootCtx: Context): State = copy(run = comp.newRun(rootCtx, objectIndex)) } -/** A list of possible completions at the index of `cursor` - * - * @param cursor the index of the users cursor in the input - * @param suggestions the suggested completions as a filtered list of strings - */ -case class Completions(cursor: Int, - suggestions: List[String], - details: List[String]) - /** Main REPL instance, orchestrating input, compilation and presentation */ class ReplDriver(settings: Array[String], out: PrintStream = Console.out, @@ -98,7 +76,7 @@ class ReplDriver(settings: Array[String], } /** the initial, empty state of the REPL session */ - protected[this] def initState = State(0, 0, Nil, Nil, compiler.newRun(rootCtx, 0)) + protected[this] def initState = State(0, 0, Nil, compiler.newRun(rootCtx, 0)) /** Reset state of repl to the initial state * @@ -139,10 +117,7 @@ class ReplDriver(settings: Array[String], @tailrec def loop(state: State): State = { val res = readLine()(state) - if (res == Quit) { - out.println() - state - } + if (res == Quit) state else { // readLine potentially destroys the run, so a new one is needed for the // rest of the interpretation: @@ -162,37 +137,55 @@ class ReplDriver(settings: Array[String], private def withRedirectedOutput(op: => State): State = Console.withOut(out) { Console.withErr(out) { op } } + /** Extract possible completions at the index of `cursor` in `expr` */ - protected[this] final def completions(cursor: Int, expr: String, state0: State): Completions = { - // TODO move some of this logic to `Interactive` + protected[this] final def completions(cursor: Int, expr: String, state0: State): List[Candidate] = { + def makeCandidate(completion: Symbol)(implicit ctx: Context) = { + val displ = completion.name.toString + new Candidate( + /* value = */ displ, + /* displ = */ displ, // displayed value + /* group = */ null, // can be used to group completions together + /* descr = */ null, // TODO use for documentation? + /* suffix = */ null, + /* key = */ null, + /* complete = */ false // if true adds space when completing + ) + } implicit val state = state0.newRun(compiler, rootCtx) compiler .typeCheck(expr, errorsAllowed = true) .map { tree => - val file = new dotc.util.SourceFile("compl", expr) + val file = new SourceFile("", expr.toCharArray) val unit = new CompilationUnit(file) unit.tpdTree = tree - implicit val ctx: Context = state.run.runContext.fresh.setCompilationUnit(unit) - val srcPos = dotc.util.SourcePosition(file, Position(cursor)) - val (startOffset, completions) = Interactive.completions(srcPos) - val query = - if (startOffset < cursor) expr.substring(startOffset, cursor) else "" - - def filterCompletions(name: String) = - (query == "." || name.startsWith(query)) && name != query - - Completions( - Math.min(startOffset, cursor) + { if (query == ".") 1 else 0 }, - completions.map(_.name.show).distinct.filter(filterCompletions), - Nil - ) + implicit val ctx = state.run.runContext.fresh.setCompilationUnit(unit) + val srcPos = SourcePosition(file, Position(cursor)) + val (_, completions) = Interactive.completions(srcPos) + completions.map(makeCandidate) } - .fold(_ => Completions(cursor, Nil, Nil), x => x) + .getOrElse(Nil) } - /** Blockingly read a line, getting back a parse result and new history */ - private def readLine()(implicit state: State): ParseResult = - AmmoniteReader(out, state.history, completions(_, _, state))(state.run.runContext).prompt + // lazy because the REPL tests do not rely on the JLine reader + private lazy val terminal = new JLineTerminal() + + /** Blockingly read a line, getting back a parse result */ + private def readLine()(implicit state: State): ParseResult = { + implicit val ctx = state.run.runContext + val completer: Completer = { (_, line, candidates) => + val comps = completions(line.cursor, line.line, state) + candidates.addAll(comps.asJava) + } + try { + val line = terminal.readLine(completer) + ParseResult(line) + } + catch { + case _: EndOfFileException => // Ctrl+D + Quit + } + } private def extractImports(trees: List[untpd.Tree]): List[untpd.Import] = trees.collect { case imp: untpd.Import => imp } @@ -200,15 +193,14 @@ class ReplDriver(settings: Array[String], private def interpret(res: ParseResult)(implicit state: State): State = { val newState = res match { case parsed: Parsed if parsed.trees.nonEmpty => - compile(parsed) - .withHistory(parsed.sourceCode :: state.history) - .newRun(compiler, rootCtx) + compile(parsed).newRun(compiler, rootCtx) case SyntaxErrors(src, errs, _) => displayErrors(errs) - state.withHistory(src :: state.history) + state - case cmd: Command => interpretCommand(cmd) + case cmd: Command => + interpretCommand(cmd) case SigKill => // TODO state @@ -222,10 +214,8 @@ class ReplDriver(settings: Array[String], /** Compile `parsed` trees and evolve `state` in accordance */ protected[this] final def compile(parsed: Parsed)(implicit state: State): State = { - import dotc.ast.Trees.PackageDef - import untpd.{ PackageDef => _, _ } - def extractNewestWrapper(tree: Tree): Name = tree match { - case PackageDef(_, (obj: ModuleDef) :: Nil) => obj.name.moduleClassName + def extractNewestWrapper(tree: untpd.Tree): Name = tree match { + case PackageDef(_, (obj: untpd.ModuleDef) :: Nil) => obj.name.moduleClassName case _ => nme.NO_NAME } @@ -234,16 +224,14 @@ class ReplDriver(settings: Array[String], .fold( displayErrors, { - case (unit: CompilationUnit, newState: State) => { + case (unit: CompilationUnit, newState: State) => val newestWrapper = extractNewestWrapper(unit.untpdTree) val newImports = newState.imports ++ extractImports(parsed.trees) val newStateWithImports = newState.copy(imports = newImports) // display warnings displayErrors(newState.run.runContext.flushBufferedMessages())(newState) - displayDefinitions(unit.tpdTree, newestWrapper)(newStateWithImports) - } } ) } @@ -253,7 +241,7 @@ class ReplDriver(settings: Array[String], implicit val ctx = state.run.runContext def resAndUnit(denot: Denotation) = { - import scala.util.{ Try, Success } + import scala.util.{Success, Try} val sym = denot.symbol val name = sym.name.show val hasValidNumber = Try(name.drop(3).toInt) match { @@ -288,7 +276,7 @@ class ReplDriver(settings: Array[String], vals.map(rendering.renderVal).flatten ).foreach(str => out.println(SyntaxHighlighting(str))) - state.copy(valIndex = state.valIndex - vals.filter(resAndUnit).length) + state.copy(valIndex = state.valIndex - vals.count(resAndUnit)) } else state @@ -323,52 +311,46 @@ class ReplDriver(settings: Array[String], /** Interpret `cmd` to action and propagate potentially new `state` */ private def interpretCommand(cmd: Command)(implicit state: State): State = cmd match { - case UnknownCommand(cmd) => { + case UnknownCommand(cmd) => out.println(s"""Unknown command: "$cmd", run ":help" for a list of commands""") - state.withHistory(s"$cmd") - } + state - case Help => { + case Help => out.println(Help.text) - state.withHistory(Help.command) - } + state - case Reset => { + case Reset => resetToInitial() initState - } - case Imports => { + case Imports => state.imports.foreach(i => out.println(SyntaxHighlighting(i.show(state.run.runContext)))) - state.withHistory(Imports.command) - } + state case Load(path) => - val loadCmd = s"${Load.command} $path" val file = new java.io.File(path) if (file.exists) { - val contents = scala.io.Source.fromFile(path).mkString + val contents = scala.io.Source.fromFile(file).mkString ParseResult(contents)(state.run.runContext) match { case parsed: Parsed => - compile(parsed).withHistory(loadCmd) + compile(parsed) case SyntaxErrors(_, errors, _) => - displayErrors(errors).withHistory(loadCmd) + displayErrors(errors) case _ => - state.withHistory(loadCmd) + state } } else { out.println(s"""Couldn't find file "${file.getCanonicalPath}"""") - state.withHistory(loadCmd) + state } - case TypeOf(expr) => { + case TypeOf(expr) => compiler.typeOf(expr).fold( displayErrors, res => out.println(SyntaxHighlighting(res)) ) - state.withHistory(s"${TypeOf.command} $expr") - } + state case Quit => // end of the world! diff --git a/compiler/src/dotty/tools/repl/package.scala b/compiler/src/dotty/tools/repl/package.scala index 915f70199ac8..f95334cfb38e 100644 --- a/compiler/src/dotty/tools/repl/package.scala +++ b/compiler/src/dotty/tools/repl/package.scala @@ -8,7 +8,7 @@ import dotc.reporting.{HideNonSensicalMessages, StoreReporter, UniqueMessagePosi package object repl { /** Create empty outer store reporter */ - private[repl] def storeReporter: StoreReporter = + private[repl] def newStoreReporter: StoreReporter = new StoreReporter(null) with UniqueMessagePositions with HideNonSensicalMessages diff --git a/compiler/src/dotty/tools/repl/terminal/Ansi.scala b/compiler/src/dotty/tools/repl/terminal/Ansi.scala deleted file mode 100644 index 207d4a6c88a1..000000000000 --- a/compiler/src/dotty/tools/repl/terminal/Ansi.scala +++ /dev/null @@ -1,257 +0,0 @@ -package dotty.tools -package repl -package terminal - -object Ansi { - - /** - * Represents a single, atomic ANSI escape sequence that results in a - * color, background or decoration being added to the output. - * - * @param escape the actual ANSI escape sequence corresponding to this Attr - */ - case class Attr private[Ansi](escape: Option[String], resetMask: Int, applyMask: Int) { - override def toString = escape.getOrElse("") + Console.RESET - def transform(state: Short) = ((state & ~resetMask) | applyMask).toShort - - def matches(state: Short) = (state & resetMask) == applyMask - def apply(s: Ansi.Str) = s.overlay(this, 0, s.length) - } - - object Attr { - val Reset = new Attr(Some(Console.RESET), Short.MaxValue, 0) - - /** - * Quickly convert string-colors into [[Ansi.Attr]]s - */ - val ParseMap = { - val pairs = for { - cat <- categories - color <- cat.all - str <- color.escape - } yield (str, color) - (pairs :+ (Console.RESET -> Reset)).toMap - } - } - - /** - * Represents a set of [[Ansi.Attr]]s all occupying the same bit-space - * in the state `Short` - */ - sealed abstract class Category() { - val mask: Int - val all: Seq[Attr] - lazy val bitsMap = all.map{ m => m.applyMask -> m}.toMap - def makeAttr(s: Option[String], applyMask: Int) = { - new Attr(s, mask, applyMask) - } - } - - object Color extends Category { - - val mask = 15 << 7 - val Reset = makeAttr(Some("\u001b[39m"), 0 << 7) - val Black = makeAttr(Some(Console.BLACK), 1 << 7) - val Red = makeAttr(Some(Console.RED), 2 << 7) - val Green = makeAttr(Some(Console.GREEN), 3 << 7) - val Yellow = makeAttr(Some(Console.YELLOW), 4 << 7) - val Blue = makeAttr(Some(Console.BLUE), 5 << 7) - val Magenta = makeAttr(Some(Console.MAGENTA), 6 << 7) - val Cyan = makeAttr(Some(Console.CYAN), 7 << 7) - val White = makeAttr(Some(Console.WHITE), 8 << 7) - - val all = Vector( - Reset, Black, Red, Green, Yellow, - Blue, Magenta, Cyan, White - ) - } - - object Back extends Category { - val mask = 15 << 3 - - val Reset = makeAttr(Some("\u001b[49m"), 0 << 3) - val Black = makeAttr(Some(Console.BLACK_B), 1 << 3) - val Red = makeAttr(Some(Console.RED_B), 2 << 3) - val Green = makeAttr(Some(Console.GREEN_B), 3 << 3) - val Yellow = makeAttr(Some(Console.YELLOW_B), 4 << 3) - val Blue = makeAttr(Some(Console.BLUE_B), 5 << 3) - val Magenta = makeAttr(Some(Console.MAGENTA_B), 6 << 3) - val Cyan = makeAttr(Some(Console.CYAN_B), 7 << 3) - val White = makeAttr(Some(Console.WHITE_B), 8 << 3) - - val all = Seq( - Reset, Black, Red, Green, Yellow, - Blue, Magenta, Cyan, White - ) - } - - object Bold extends Category { - val mask = 1 << 0 - val On = makeAttr(Some(Console.BOLD), 1 << 0) - val Off = makeAttr(None , 0 << 0) - val all = Seq(On, Off) - } - - object Underlined extends Category { - val mask = 1 << 1 - val On = makeAttr(Some(Console.UNDERLINED), 1 << 1) - val Off = makeAttr(None, 0 << 1) - val all = Seq(On, Off) - } - - object Reversed extends Category { - val mask = 1 << 2 - val On = makeAttr(Some(Console.REVERSED), 1 << 2) - val Off = makeAttr(None, 0 << 2) - val all = Seq(On, Off) - } - - val hardOffMask = Bold.mask | Underlined.mask | Reversed.mask - val categories = List(Color, Back, Bold, Underlined, Reversed) - - object Str { - @sharable lazy val ansiRegex = "\u001B\\[[;\\d]*m".r - - implicit def parse(raw: CharSequence): Str = { - val chars = new Array[Char](raw.length) - val colors = new Array[Short](raw.length) - var currentIndex = 0 - var currentColor = 0.toShort - - val matches = ansiRegex.findAllMatchIn(raw) - val indices = Seq(0) ++ matches.flatMap { m => Seq(m.start, m.end) } ++ Seq(raw.length) - - for { - Seq(start, end) <- indices.sliding(2).toSeq - if start != end - } { - val frag = raw.subSequence(start, end).toString - if (frag.charAt(0) == '\u001b' && Attr.ParseMap.contains(frag)) { - currentColor = Attr.ParseMap(frag).transform(currentColor) - } else { - var i = 0 - while(i < frag.length){ - chars(currentIndex) = frag(i) - colors(currentIndex) = currentColor - i += 1 - currentIndex += 1 - } - } - } - - Str(chars.take(currentIndex), colors.take(currentIndex)) - } - } - - /** - * An [[Ansi.Str]]'s `color`s array is filled with shorts, each representing - * the ANSI state of one character encoded in its bits. Each [[Attr]] belongs - * to a [[Category]] that occupies a range of bits within each short: - * - * 15 14 13 12 11 10 9 8 7 6 5 4 3 2 1 0 - * |-----------| |--------| |--------| | | |bold - * | | | | |reversed - * | | | |underlined - * | | |foreground-color - * | |background-color - * |unused - * - * - * The `0000 0000 0000 0000` short corresponds to plain text with no decoration - * - */ - type State = Short - - /** - * Encapsulates a string with associated ANSI colors and text decorations. - * - * Contains some basic string methods, as well as some ansi methods to e.g. - * apply particular colors or other decorations to particular sections of - * the [[Ansi.Str]]. [[render]] flattens it out into a `java.lang.String` - * with all the colors present as ANSI escapes. - * - */ - case class Str private(chars: Array[Char], colors: Array[State]) { - require(chars.length == colors.length) - - def ++(other: Str) = Str(chars ++ other.chars, colors ++ other.colors) - def splitAt(index: Int) = { - val (leftChars, rightChars) = chars.splitAt(index) - val (leftColors, rightColors) = colors.splitAt(index) - (new Str(leftChars, leftColors), new Str(rightChars, rightColors)) - } - - def length = chars.length - override def toString = render - - def plainText = new String(chars.toArray) - def render = { - // Pre-size StringBuilder with approximate size (ansi colors tend - // to be about 5 chars long) to avoid re-allocations during growth - val output = new StringBuilder(chars.length + colors.length * 5) - - - var currentState = 0.toShort - /** - * Emit the ansi escapes necessary to transition - * between two states, if necessary. - */ - def emitDiff(nextState: Short) = if (currentState != nextState){ - // Any of these transitions from 1 to 0 within the hardOffMask - // categories cannot be done with a single ansi escape, and need - // you to emit a RESET followed by re-building whatever ansi state - // you previous had from scratch - if ((currentState & ~nextState & hardOffMask) != 0){ - output.append(Console.RESET) - currentState = 0 - } - - var categoryIndex = 0 - while(categoryIndex < categories.length){ - val cat = categories(categoryIndex) - if ((cat.mask & currentState) != (cat.mask & nextState)){ - val attr = cat.bitsMap(nextState & cat.mask) - - if (attr.escape.isDefined) { - output.append(attr.escape.get) - } - } - categoryIndex += 1 - } - } - - var i = 0 - while(i < colors.length){ - // Emit ANSI escapes to change colors where necessary - emitDiff(colors(i)) - currentState = colors(i) - output.append(chars(i)) - i += 1 - } - - // Cap off the left-hand-side of the rendered string with any ansi escape - // codes necessary to rest the state to 0 - emitDiff(0) - output.toString - } - - /** - * Overlays the desired color over the specified range of the [[Ansi.Str]]. - */ - def overlay(overlayColor: Attr, start: Int, end: Int) = { - require(end >= start, - s"end:$end must be greater than start:$end in AnsiStr#overlay call" - ) - val colorsOut = new Array[Short](colors.length) - var i = 0 - while(i < colors.length){ - if (i >= start && i < end) colorsOut(i) = overlayColor.transform(colors(i)) - else colorsOut(i) = colors(i) - i += 1 - } - new Str(chars, colorsOut) - } - } - - -} diff --git a/compiler/src/dotty/tools/repl/terminal/Filter.scala b/compiler/src/dotty/tools/repl/terminal/Filter.scala deleted file mode 100644 index 0d9e3db85f13..000000000000 --- a/compiler/src/dotty/tools/repl/terminal/Filter.scala +++ /dev/null @@ -1,105 +0,0 @@ -package dotty.tools -package repl -package terminal - -/** - * 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 { - def op(ti: 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 -} - -/** - * Convenience constructors to create [[Filter]] instances in a bunch of - * different ways - */ -object Filter { - - /** - * 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 simple(prefixes: String*) - (f: (Vector[Char], Int, TermInfo) => (Vector[Char], Int)): Filter = new Filter { - def op(ti: TermInfo) = { - val matchingPrefixOpt = - prefixes.iterator - .map{ s => ti.ts.inputs.dropPrefix(s.map(_.toInt)) } - .collectFirst{case Some(s) => s} - matchingPrefixOpt.map{ rest => - val (buffer1, cursor1) = f(ti.ts.buffer, ti.ts.cursor, ti) - TermState( - rest, - buffer1, - cursor1 - ) - - } - } - def identifier = prefixes.mkString(":") - } - def action(id: String) - (prefixes: Seq[String], filter: TermInfo => Boolean = _ => true) - (action: TermState => TermAction = x => x): Filter = new Filter { - def op(ti: TermInfo) = { - prefixes.iterator - .map{ prefix => ti.ts.inputs.dropPrefix(prefix.map(_.toInt)) } - .collectFirst { case Some(rest) if filter(ti) => action(ti.ts.copy(inputs = rest)) } - } - def identifier: String = id - } - - def partial(id: String)(f: PartialFunction[TermInfo, TermAction]): Filter = - new Filter { - def op(ti: TermInfo) = f.lift(ti) - def identifier = id - } - - def wrap(id: String)(f: TermInfo => Option[TermAction]): Filter = - new Filter { - def op(ti: TermInfo) = f(ti) - def identifier = id - } - - /** - * Merges multiple [[Filter]]s into one. - */ - def merge(pfs: Filter*) = new Filter { - - def op(v1: TermInfo) = pfs.iterator.map(_.op(v1)).find(_.isDefined).flatten - - def identifier = pfs.iterator.map(_.identifier).mkString(":") - } - val empty = Filter.merge() -} - - -/** - * 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(val identifier: String) extends Filter { - def filter: Filter - def op(ti: TermInfo) = filter.op(ti) -} diff --git a/compiler/src/dotty/tools/repl/terminal/FilterTools.scala b/compiler/src/dotty/tools/repl/terminal/FilterTools.scala deleted file mode 100644 index e7f47879574a..000000000000 --- a/compiler/src/dotty/tools/repl/terminal/FilterTools.scala +++ /dev/null @@ -1,48 +0,0 @@ -package dotty.tools -package repl -package 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 for pattern matching on [[TermState]] */ - val TS = TermState - - def findChunks(b: Vector[Char], c: Int) = { - val chunks = Terminal.splitBuffer(b) - // The index of the first character in each chunk - val chunkStarts = chunks.inits.map(x => x.length + x.sum).toStream.reverse - // Index of the current chunk that contains the cursor - val chunkIndex = chunkStarts.indexWhere(_ > c) match { - case -1 => chunks.length-1 - case x => x - 1 - } - (chunks, chunkStarts, chunkIndex) - } - - def firstRow(cursor: Int, buffer: Vector[Char], width: Int) = - cursor < width && (buffer.indexOf('\n') >= cursor || buffer.indexOf('\n') == -1) - - def lastRow(cursor: Int, buffer: Vector[Char], width: Int) = - (buffer.length - cursor) < width && - (buffer.lastIndexOf('\n') < cursor || buffer.lastIndexOf('\n') == -1) -} diff --git a/compiler/src/dotty/tools/repl/terminal/LICENSE b/compiler/src/dotty/tools/repl/terminal/LICENSE deleted file mode 100644 index b15103580d82..000000000000 --- a/compiler/src/dotty/tools/repl/terminal/LICENSE +++ /dev/null @@ -1,25 +0,0 @@ -License -======= - - -The MIT License (MIT) - -Copyright (c) 2014 Li Haoyi (haoyi.sg@gmail.com) - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER -DEALINGS IN THE SOFTWARE. \ No newline at end of file diff --git a/compiler/src/dotty/tools/repl/terminal/Protocol.scala b/compiler/src/dotty/tools/repl/terminal/Protocol.scala deleted file mode 100644 index 800b745e47c5..000000000000 --- a/compiler/src/dotty/tools/repl/terminal/Protocol.scala +++ /dev/null @@ -1,39 +0,0 @@ -package dotty.tools -package repl -package terminal - -case class TermInfo(ts: TermState, width: Int) - -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 { - // Using unapply instead exposes a dotty/scalac variation. Because - // `TermState` is a case class, scalac generate an unapply with this exact - // signature, that is used by the `TermInfo` and `TermAction` unapplies. - // With dotty, the generated unapply has type `TermState => TermState` - // instead, `unapply(ti: TermAction)` thus becomes a infinite tail - // recursion. See #2335. - def unapplyWorkaround(ti: TermState): Option[(LazyList[Int], Vector[Char], Int, Ansi.Str)] = - Some((ti.inputs, ti.buffer, ti.cursor, ti.msg)) - - def unapply(ti: TermInfo): Option[(LazyList[Int], Vector[Char], Int, Ansi.Str)] = - TermState.unapplyWorkaround(ti.ts) - - def unapply(ti: TermAction): Option[(LazyList[Int], Vector[Char], Int, Ansi.Str)] = - ti match { - case ts: TermState => TermState.unapplyWorkaround(ts) - case _ => None - } -} - -case class ClearScreen(ts: TermState) extends TermAction -case object Exit extends TermAction -case object Interrupt extends TermAction -case class Result(s: String) extends TermAction diff --git a/compiler/src/dotty/tools/repl/terminal/SpecialKeys.scala b/compiler/src/dotty/tools/repl/terminal/SpecialKeys.scala deleted file mode 100644 index 329776c66c2e..000000000000 --- a/compiler/src/dotty/tools/repl/terminal/SpecialKeys.scala +++ /dev/null @@ -1,84 +0,0 @@ -package dotty.tools -package repl -package 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" - - val Tab = 9.toChar.toString - - // 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 HomeLinuxXterm = Alt+"[7~" - val EndRxvt = Alt+"[8~" - - val ShiftUp = Alt+"[1;2A" - val ShiftDown = Alt+"[1;2B" - val ShiftRight = Alt+"[1;2C" - val ShiftLeft = Alt+"[1;2D" - - val FnUp = Alt+"[5~" - val FnDown = Alt+"[6~" - val FnRight = Alt+"[F" - val FnLeft = Alt+"[H" - - val AltUp = Alt*2+"[A" - val AltDown = Alt*2+"[B" - val AltRight = Alt*2+"[C" - val AltLeft = Alt*2+"[D" - - val LinuxCtrlRight = Alt+"[1;5C" - val LinuxCtrlLeft = Alt+"[1;5D" - - val FnAltUp = Alt*2+"[5~" - val FnAltDown = Alt*2+"[6~" - val FnAltRight = Alt+"[1;9F" - val FnAltLeft = Alt+"[1;9H" - - // Same as fn-alt-{up, down} -// val FnShiftUp = Alt*2+"[5~" -// val FnShiftDown = Alt*2+"[6~" - val FnShiftRight = Alt+"[1;2F" - val FnShiftLeft = Alt+"[1;2H" - - val AltShiftUp = Alt+"[1;10A" - val AltShiftDown = Alt+"[1;10B" - val AltShiftRight = Alt+"[1;10C" - val AltShiftLeft = Alt+"[1;10D" - - // Same as fn-alt-{up, down} -// val FnAltShiftUp = Alt*2+"[5~" -// val FnAltShiftDown = Alt*2+"[6~" - val FnAltShiftRight = Alt+"[1;10F" - val FnAltShiftLeft = Alt+"[1;10H" -} diff --git a/compiler/src/dotty/tools/repl/terminal/Terminal.scala b/compiler/src/dotty/tools/repl/terminal/Terminal.scala deleted file mode 100644 index dd3da9a87929..000000000000 --- a/compiler/src/dotty/tools/repl/terminal/Terminal.scala +++ /dev/null @@ -1,326 +0,0 @@ -package dotty.tools -package repl -package terminal - -import scala.annotation.tailrec -import scala.collection.mutable - -/** - * The core logic around a terminal; it defines the base `filters` API - * through which anything (including basic cursor-navigation and typing) - * interacts with the terminal. - * - * Maintains basic invariants, such as "cursor should always be within - * the buffer", and "ansi terminal should reflect most up to date TermState" - */ -object Terminal { - - /** - * Computes how tall a line of text is when wrapped at `width`. - * - * Even 0-character lines still take up one row! - * - * width = 2 - * 0 -> 1 - * 1 -> 1 - * 2 -> 1 - * 3 -> 2 - * 4 -> 2 - * 5 -> 3 - */ - def fragHeight(length: Int, width: Int) = math.max(1, (length - 1) / width + 1) - - def splitBuffer(buffer: Vector[Char]) = { - val frags = mutable.Buffer.empty[Int] - frags.append(0) - for(c <- buffer){ - if (c == '\n') frags.append(0) - else frags(frags.length - 1) = frags.last + 1 - } - frags - } - def calculateHeight(buffer: Vector[Char], - width: Int, - prompt: String): Seq[Int] = { - val rowLengths = splitBuffer(buffer) - - calculateHeight0(rowLengths, width - prompt.length) - } - - /** - * Given a buffer with characters and newlines, calculates how high - * the buffer is and where the cursor goes inside of it. - */ - def calculateHeight0(rowLengths: Seq[Int], - width: Int): Seq[Int] = { - val fragHeights = - rowLengths - .inits - .toVector - .reverse // We want shortest-to-longest, inits gives longest-to-shortest - .filter(_.nonEmpty) // Without the first empty prefix - .map{ x => - fragHeight( - // If the frag barely fits on one line, give it - // an extra spot for the cursor on the next line - x.last + 1, - width - ) - } -// Debug("fragHeights " + fragHeights) - fragHeights - } - - def positionCursor(cursor: Int, - rowLengths: Seq[Int], - fragHeights: Seq[Int], - width: Int) = { - var leftoverCursor = cursor - // Debug("leftoverCursor " + leftoverCursor) - var totalPreHeight = 0 - var done = false - // Don't check if the cursor exceeds the last chunk, because - // even if it does there's nowhere else for it to go - for(i <- 0 until rowLengths.length -1 if !done) { - // length of frag and the '\n' after it - val delta = rowLengths(i) + 1 - // Debug("delta " + delta) - val nextCursor = leftoverCursor - delta - if (nextCursor >= 0) { - // Debug("nextCursor " + nextCursor) - leftoverCursor = nextCursor - totalPreHeight += fragHeights(i) - }else done = true - } - - val cursorY = totalPreHeight + leftoverCursor / width - val cursorX = leftoverCursor % width - - (cursorY, cursorX) - } - - - type Action = (Vector[Char], Int) => (Vector[Char], Int) - type MsgAction = (Vector[Char], Int) => (Vector[Char], Int, String) - - def noTransform(x: Vector[Char], i: Int) = (Ansi.Str.parse(x), i) - /** - * Blockingly reads a line from the given input stream and returns it. - * - * @param prompt The prompt to display when requesting input - * @param reader The input-stream where characters come in, e.g. System.in - * @param writer The output-stream where print-outs go, e.g. System.out - * @param filters A set of actions that can be taken depending on the input, - * @param displayTransform code to manipulate the display of the buffer and - * cursor, without actually changing the logical - * values inside them. - */ - def readLine(prompt: Prompt, - reader: java.io.Reader, - writer: java.io.Writer, - filters: Filter, - displayTransform: (Vector[Char], Int) => (Ansi.Str, Int) = noTransform) - : Option[TermAction] = { - - /** - * 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[TermAction] = { - 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 - def redrawAndNewline() = { - redrawLine( - transformedBuffer, lastState.buffer.length, - oldCursorY + newlineUp, rowLengths, false, newlinePrompt - ) - writer.write(10) - writer.write(13) - writer.flush() - } - 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 res: Result => - redrawAndNewline() - Some(res) - - case Interrupt => - redrawAndNewline() - Some(Interrupt) - - case ClearScreen(ts) => - ansi.clearScreen(2) - ansi.up(9999) - ansi.left(9999) - readChar(ts, ups) - case Exit => - None - } - } - - lazy val ansi = new AnsiNav(writer) - lazy val (width, _, initialConfig) = TTY.init() - try { - readChar(TermState(LazyList.continually(reader.read()), Vector.empty, 0, ""), 0) - } - finally { - - // Don't close these! Closing these closes stdin/stdout, - // which seems to kill the entire program - - // reader.close() - // writer.close() - TTY.stty(initialConfig) - } - } -} -object Prompt { - implicit def construct(prompt: String): Prompt = { - val parsedPrompt = Ansi.Str.parse(prompt) - val index = parsedPrompt.plainText.lastIndexOf('\n') - val (_, last) = parsedPrompt.splitAt(index+1) - Prompt(parsedPrompt, last) - } -} - -case class Prompt(full: Ansi.Str, lastLine: Ansi.Str) diff --git a/compiler/src/dotty/tools/repl/terminal/Utils.scala b/compiler/src/dotty/tools/repl/terminal/Utils.scala deleted file mode 100644 index e7b5dcddbe0e..000000000000 --- a/compiler/src/dotty/tools/repl/terminal/Utils.scala +++ /dev/null @@ -1,236 +0,0 @@ -package dotty.tools -package repl -package terminal - -import java.io.{FileOutputStream, Writer, File => JFile} -import scala.annotation.tailrec - -/** - * Prints stuff to an ad-hoc logging file when running the repl or terminal in - * development mode - * - * Very handy for the common case where you're debugging terminal interactions - * and cannot use `println` because it will stomp all over your already messed - * up terminal state and block debugging. With [[Debug]], you can have a - * separate terminal open tailing the log file and log as verbosely as you - * want without affecting the primary terminal you're using to interact with - * Ammonite. - */ -object Debug { - lazy val debugOutput = - new FileOutputStream(new JFile("terminal/target/log")) - - def apply(s: Any) = - if (System.getProperty("ammonite-sbt-build") == "true") - debugOutput.write((System.currentTimeMillis() + "\t\t" + s + "\n").getBytes) -} - -class AnsiNav(output: Writer) { - def control(n: Int, c: Char) = output.write(s"\033[" + n + c) - - /** - * Move up `n` squares - */ - def up(n: Int) = if (n == 0) "" else control(n, 'A') - /** - * Move down `n` squares - */ - def down(n: Int) = if (n == 0) "" else control(n, 'B') - /** - * Move right `n` squares - */ - def right(n: Int) = if (n == 0) "" else control(n, 'C') - /** - * Move left `n` squares - */ - def left(n: Int) = if (n == 0) "" else control(n, 'D') - - /** - * Clear the screen - * - * n=0: clear from cursor to end of screen - * n=1: clear from cursor to start of screen - * n=2: clear entire screen - */ - def clearScreen(n: Int) = control(n, 'J') - /** - * Clear the current line - * - * n=0: clear from cursor to end of line - * n=1: clear from cursor to start of line - * n=2: clear entire line - */ - def clearLine(n: Int) = control(n, 'K') -} - -object AnsiNav { - val resetUnderline = "\u001b[24m" - val resetForegroundColor = "\u001b[39m" - val resetBackgroundColor = "\u001b[49m" -} - -object TTY { - - // Prefer standard tools. Not sure why we need to do this, but for some - // reason the version installed by gnu-coreutils blows up sometimes giving - // "unable to perform all requested operations" - val pathedTput = if (new java.io.File("/usr/bin/tput").exists()) "/usr/bin/tput" else "tput" - val pathedStty = if (new java.io.File("/bin/stty").exists()) "/bin/stty" else "stty" - - def consoleDim(s: String) = { - import sys.process._ - Seq("bash", "-c", s"$pathedTput $s 2> /dev/tty").!!.trim.toInt - } - def init() = { - stty("-a") - - val width = consoleDim("cols") - val height = consoleDim("lines") -// Debug("Initializing, Width " + width) -// Debug("Initializing, Height " + height) - val initialConfig = stty("-g").trim - stty("-icanon min 1 -icrnl -inlcr -ixon") - sttyFailTolerant("dsusp undef") - stty("-echo") - stty("intr undef") -// Debug("") - (width, height, initialConfig) - } - - private def sttyCmd(s: String) = { - import sys.process._ - Seq("bash", "-c", s"$pathedStty $s < /dev/tty"): ProcessBuilder - } - - def stty(s: String) = - sttyCmd(s).!! - /* - * Executes a stty command for which failure is expected, hence the return - * status can be non-null and errors are ignored. - * This is appropriate for `stty dsusp undef`, since it's unsupported on Linux - * (http://man7.org/linux/man-pages/man3/termios.3.html). - */ - def sttyFailTolerant(s: String) = - sttyCmd(s ++ " 2> /dev/null").! - - def restore(initialConfig: String) = { - stty(initialConfig) - } -} - -/** - * A truly-lazy implementation of scala.Stream - */ -case class LazyList[T](headThunk: () => T, tailThunk: () => LazyList[T]) { - var rendered = false - lazy val head = { - rendered = true - headThunk() - } - - lazy val tail = tailThunk() - - def dropPrefix(prefix: Seq[T]) = { - @tailrec def rec(n: Int, l: LazyList[T]): Option[LazyList[T]] = { - if (n >= prefix.length) Some(l) - else if (prefix(n) == l.head) rec(n + 1, l.tail) - else None - } - rec(0, this) - } - override def toString = { - - @tailrec def rec(l: LazyList[T], res: List[T]): List[T] = { - if (l.rendered) rec(l.tailThunk(), l.head :: res) - else res - } - s"LazyList(${(rec(this, Nil).reverse ++ Seq("...")).mkString(",")})" - } - - def ~:(other: => T) = LazyList(() => other, () => this) -} - -object LazyList { - object ~: { - def unapply[T](x: LazyList[T]) = Some((x.head, x.tail)) - } - - def continually[T](t: => T): LazyList[T] = LazyList(() => t, () =>continually(t)) - - implicit class CS(ctx: StringContext) { - val base = ctx.parts.mkString - object p { - def unapply(s: LazyList[Int]): Option[LazyList[Int]] = { - s.dropPrefix(base.map(_.toInt)) - } - } - } -} - -/** - * Created by haoyi on 8/29/15. - */ -object FrontEndUtils { - import Ansi.Str._ - - private[this] val newLine = System.lineSeparator() - - def transpose[A](xs: List[List[A]]): List[List[A]] = { - @tailrec def transpose(xs: List[List[A]], result: List[List[A]]): List[List[A]] = { - xs.filter(_.nonEmpty) match { - case Nil => result - case ys => transpose(ys.map(_.tail), ys.map(_.head) :: result) - } - } - - transpose(xs, Nil).reverse - } - - def width = TTY.consoleDim("cols") - def height = TTY.consoleDim("lines") - def tabulate(snippetsRaw: Seq[Ansi.Str], width: Int): Iterator[String] = { - val gap = 2 - val snippets = if (snippetsRaw.isEmpty) Seq(Ansi.Str.parse("")) else snippetsRaw - val maxLength = snippets.maxBy(_.length).length + gap - val columns = math.max(1, width / maxLength) - - val grouped = - snippets.toList - .grouped(math.ceil(snippets.length * 1.0 / columns).toInt) - .toList - - transpose(grouped).iterator.flatMap { group => - val last = group.last - group.init.map( - x => x ++ " " * (width / columns - x.length) - ) :+ last :+ Ansi.Str.parse(newLine) - } - .map(_.render) - } - - @tailrec def findPrefix(strings: Seq[String], i: Int = 0): String = { - if (strings.count(_.length > i) == 0) strings(0).take(i) - else if(strings.collect{ case x if x.length > i => x(i)}.distinct.length > 1) - strings(0).take(i) - else findPrefix(strings, i + 1) - } - - def printCompletions(completions: Seq[String], - details: Seq[String]): List[String] = { - - val prelude: List[String] = - if (details.length != 0 || completions.length != 0) List(newLine) - else Nil - - val detailsText = - if (details.length == 0) Nil - else FrontEndUtils.tabulate(details.map(Ansi.Str.parse(_)), FrontEndUtils.width) - - val completionText = - if (completions.length == 0) Nil - else FrontEndUtils.tabulate(completions.map(Ansi.Str.parse(_)), FrontEndUtils.width) - - (prelude ++ detailsText ++ completionText) - } - -} diff --git a/compiler/src/dotty/tools/repl/terminal/filters/BasicFilters.scala b/compiler/src/dotty/tools/repl/terminal/filters/BasicFilters.scala deleted file mode 100644 index be264ce970f8..000000000000 --- a/compiler/src/dotty/tools/repl/terminal/filters/BasicFilters.scala +++ /dev/null @@ -1,163 +0,0 @@ -package dotty.tools -package repl -package terminal -package filters - -import terminal.FilterTools._ -import terminal.LazyList._ -import terminal.SpecialKeys._ -import terminal.Filter -import terminal._ - -import terminal.Filter._ - -/** - * Filters for simple operation of a terminal: cursor-navigation - * (including with all the modifier keys), enter/ctrl-c-exit, etc. - */ -object BasicFilters { - def all = Filter.merge( - navFilter, - exitFilter, - enterFilter, - clearFilter, - //loggingFilter, - typingFilter - ) - - def injectNewLine(b: Vector[Char], c: Int, rest: LazyList[Int], indent: Int = 0) = { - val (first, last) = b.splitAt(c) - TermState(rest, (first :+ '\n') ++ last ++ Vector.fill(indent)(' '), c + 1 + indent) - } - - def navFilter = Filter.merge( - simple(Up)((b, c, m) => moveUp(b, c, m.width)), - simple(Down)((b, c, m) => moveDown(b, c, m.width)), - simple(Right)((b, c, m) => (b, c + 1)), - simple(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.partial("tabFilter") { - case TS(9 ~: rest, b, c, _) => tabColumn(indent, b, c, rest) - } - - def loggingFilter: Filter = Filter.partial("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.partial("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.partial("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.partial("exitFilter") { - case TS(Ctrl('c') ~: rest, b, c, _) => - Interrupt - 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.partial("clearFilter") { - case TS(Ctrl('l') ~: rest, b, c, _) => ClearScreen(TS(rest, b, c)) - } - - def moveStart(b: Vector[Char], c: Int, w: Int) = { - val (_, chunkStarts, chunkIndex) = findChunks(b, c) - val currentColumn = (c - chunkStarts(chunkIndex)) % w - b -> (c - currentColumn) - } - - def moveEnd(b: Vector[Char], c: Int, w: Int) = { - val (chunks, chunkStarts, chunkIndex) = findChunks(b, c) - val currentColumn = (c - chunkStarts(chunkIndex)) % w - val c1 = chunks.lift(chunkIndex + 1) match { - case Some(next) => - val boundary = chunkStarts(chunkIndex + 1) - 1 - if ((boundary - c) > (w - currentColumn)) { - val delta= w - currentColumn - c + delta - } - else boundary - case None => - c + 1 * 9999 - } - b -> c1 - } - - def moveUpDown( - b: Vector[Char], - c: Int, - w: Int, - boundaryOffset: Int, - nextChunkOffset: Int, - checkRes: Int, - check: (Int, Int) => Boolean, - isDown: Boolean - ) = { - val (chunks, chunkStarts, chunkIndex) = findChunks(b, c) - val offset = chunkStarts(chunkIndex + boundaryOffset) - if (check(checkRes, offset)) checkRes - else chunks.lift(chunkIndex + nextChunkOffset) match { - case None => c + nextChunkOffset * 9999 - case Some(next) => - val boundary = chunkStarts(chunkIndex + boundaryOffset) - val currentColumn = (c - chunkStarts(chunkIndex)) % w - - if (isDown) boundary + math.min(currentColumn, next) - else boundary + math.min(currentColumn - next % w, 0) - 1 - } - } - - def moveUp(b: Vector[Char], c: Int, w: Int) = { - b -> moveUpDown(b, c, w, 0, -1, c - w, _ > _, false) - } - - def moveDown(b: Vector[Char], c: Int, w: Int) = { - b -> moveUpDown(b, c, w, 1, 1, c + w, _ <= _, true) - } -} diff --git a/compiler/src/dotty/tools/repl/terminal/filters/GUILikeFilters.scala b/compiler/src/dotty/tools/repl/terminal/filters/GUILikeFilters.scala deleted file mode 100644 index 7a147adfef8b..000000000000 --- a/compiler/src/dotty/tools/repl/terminal/filters/GUILikeFilters.scala +++ /dev/null @@ -1,168 +0,0 @@ -package dotty.tools -package repl -package terminal -package filters - -import terminal.FilterTools._ -import terminal.LazyList.~: -import terminal.SpecialKeys._ -import terminal.DelegateFilter -import terminal._ - -import Filter._ -/** - * 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("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( - - simple(ShiftUp){(b, c, m) => setMark(c); BasicFilters.moveUp(b, c, m.width)}, - simple(ShiftDown){(b, c, m) => setMark(c); BasicFilters.moveDown(b, c, m.width)}, - simple(ShiftRight){(b, c, m) => setMark(c); (b, c + 1)}, - simple(ShiftLeft){(b, c, m) => setMark(c); (b, c - 1)}, - simple(AltShiftUp){(b, c, m) => setMark(c); BasicFilters.moveUp(b, c, m.width)}, - simple(AltShiftDown){(b, c, m) => setMark(c); BasicFilters.moveDown(b, c, m.width)}, - simple(AltShiftRight){(b, c, m) => setMark(c); wordRight(b, c)}, - simple(AltShiftLeft){(b, c, m) => setMark(c); wordLeft(b, c)}, - simple(FnShiftRight){(b, c, m) => setMark(c); BasicFilters.moveEnd(b, c, m.width)}, - simple(FnShiftLeft){(b, c, m) => setMark(c); BasicFilters.moveStart(b, c, m.width)}, - partial(identifier) { - 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( - simple(FnUp)((b, c, m) => (b, c - 9999)), - simple(FnDown)((b, c, m) => (b, c + 9999)), - simple(FnRight)((b, c, m) => BasicFilters.moveEnd(b, c, m.width)), - simple(FnLeft)((b, c, m) => BasicFilters.moveStart(b, c, m.width)) - ) - val altFilter = Filter.merge( - simple(AltUp){(b, c, m) => BasicFilters.moveUp(b, c, m.width)}, - simple(AltDown){(b, c, m) => BasicFilters.moveDown(b, c, m.width)}, - simple(AltRight){(b, c, m) => wordRight(b, c)}, - simple(AltLeft){(b, c, m) => wordLeft(b, c)} - ) - - val fnAltFilter = Filter.merge( - simple(FnAltUp){(b, c, m) => (b, c)}, - simple(FnAltDown){(b, c, m) => (b, c)}, - simple(FnAltRight){(b, c, m) => (b, c)}, - simple(FnAltLeft){(b, c, m) => (b, c)} - ) - val fnAltShiftFilter = Filter.merge( - simple(FnAltShiftRight){(b, c, m) => (b, c)}, - simple(FnAltShiftLeft){(b, c, m) => (b, c)} - ) - - - def consumeWord(b: Vector[Char], c: Int, delta: Int, offset: Int) = { - var current = c - while(b.isDefinedAt(current) && !b(current).isLetterOrDigit) current += delta - while(b.isDefinedAt(current) && b(current).isLetterOrDigit) current += delta - current + offset - } - - // c -1 to move at least one character! Otherwise you get stuck at the start of - // a word. - def wordLeft(b: Vector[Char], c: Int) = b -> consumeWord(b, c - 1, -1, 1) - def wordRight(b: Vector[Char], c: Int) = b -> consumeWord(b, c, 1, 0) -} diff --git a/compiler/src/dotty/tools/repl/terminal/filters/HistoryFilter.scala b/compiler/src/dotty/tools/repl/terminal/filters/HistoryFilter.scala deleted file mode 100644 index 992c70774926..000000000000 --- a/compiler/src/dotty/tools/repl/terminal/filters/HistoryFilter.scala +++ /dev/null @@ -1,330 +0,0 @@ -package dotty.tools -package repl -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("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.partial("historyPrelude") { - case TS(inputs, b, c, _) if activeHistory && prevBuffer.exists(_ != b) => - endHistory() - prevBuffer = None - TS(inputs, b, c) - } - - def filter0: Filter = Filter.partial("filter0") { - // Ways to kick off the history/search if you're not already in it - - // `Ctrl-R` - case TS(18 ~: rest, b, c, _) => wrap(rest, ctrlR(b, c)) - - // `Up` from the first line in the input - case TermInfo(TS(p"\u001b[A$rest", b, c, _), w) if firstRow(c, b, w) && !activeHistory => - wrap(rest, startHistory(b, c)) - - // `Ctrl P` - case TermInfo(TS(p"\u0010$rest", b, c, _), w) if firstRow(c, b, w) && !activeHistory => - wrap(rest, startHistory(b, c)) - - // `Page-Up` from first character starts history - case TermInfo(TS(p"\u001b[5~$rest", b, c, _), w) if c == 0 && !activeHistory => - wrap(rest, startHistory(b, c)) - - // Things you can do when you're already in the history search - - // Navigating up and down the history. Each up or down searches for - // the next thing that matches your current searchTerm - // Up - case TermInfo(TS(p"\u001b[A$rest", b, c, _), w) if searchOrHistoryAnd(firstRow(c, b, w)) => - wrap(rest, up(b, c)) - - // Ctrl P - case TermInfo(TS(p"\u0010$rest", b, c, _), w) if searchOrHistoryAnd(firstRow(c, b, w)) => - wrap(rest, up(b, c)) - - // `Page-Up` from first character cycles history up - case TermInfo(TS(p"\u001b[5~$rest", b, c, _), w) if searchOrHistoryAnd(c == 0) => - wrap(rest, up(b, c)) - - // Down - case TermInfo(TS(p"\u001b[B$rest", b, c, _), w) if searchOrHistoryAnd(lastRow(c, b, w)) => - wrap(rest, down(b, c)) - - // `Ctrl N` - - case TermInfo(TS(p"\u000e$rest", b, c, _), w) if searchOrHistoryAnd(lastRow(c, b, w)) => - wrap(rest, down(b, c)) - // `Page-Down` from last character cycles history down - case TermInfo(TS(p"\u001b[6~$rest", b, c, _), w) if searchOrHistoryAnd(c == b.length - 1) => - wrap(rest, down(b, c)) - - - // Intercept Backspace and delete a character in search-mode, preserving it, but - // letting it fall through and dropping you out of history-mode if you try to make - // edits - case TS(127 ~: rest, buffer, cursor, _) if activeSearch => - wrap(rest, backspace(buffer, cursor)) - - // Any other control characters drop you out of search mode, but only the - // set of `dropHistoryChars` drops you out of history mode - case TS(char ~: inputs, buffer, cursor, _) - if char.toChar.isControl && searchOrHistoryAnd(dropHistoryChars(char)) => - val newBuffer = - // If we're back to -1, it means we've wrapped around and are - // displaying the original search term with a wrap-around message - // in the terminal. Drop the message and just preserve the search term - if (historyIndex == -1) searchTerm.get - // If we're searching for an empty string, special-case this and return - // an empty buffer rather than the first history item (which would be - // the default) because that wouldn't make much sense - else if (searchTerm.exists(_.isEmpty)) Vector() - // Otherwise, pick whatever history entry we're at and use that - else history()(historyIndex).toVector - endHistory() - - TS(char ~: inputs, newBuffer, cursor) - - // Intercept every other printable character when search is on and - // enter it into the current search - case TS(char ~: rest, buffer, cursor, _) if activeSearch => - wrap(rest, printableChar(char.toChar)(buffer, cursor)) - - // If you're not in search but are in history, entering any printable - // characters kicks you out of it and preserves the current buffer. This - // makes it harder for you to accidentally lose work due to history-moves - case TS(char ~: rest, buffer, cursor, _) if activeHistory && !char.toChar.isControl => - historyIndex = -1 - TS(char ~: rest, buffer, cursor) - } -} - -object HistoryFilter { - - def mangleBuffer( - historyFilter: HistoryFilter, - buffer: Ansi.Str, - cursor: Int, - startColor: Ansi.Attr - ) = { - if (!historyFilter.activeSearch) buffer - else { - val (searchStart, searchEnd) = - if (historyFilter.searchTerm.get.isEmpty) (cursor, cursor+1) - else { - val start = buffer.plainText.indexOfSlice(historyFilter.searchTerm.get) - - val end = start + (historyFilter.searchTerm.get.length max 1) - (start, end) - } - - val newStr = buffer.overlay(startColor, searchStart, searchEnd) - newStr - } - } - - /** - * @param startIndex The first index to start looking from - * @param searchTerm The term we're searching from; can be empty - * @param history The history we're searching through - * @param indexIncrement Which direction to search, +1 or -1 - * @param skipped Any buffers which we should skip in our search results, - * e.g. because the user has seen them before. - */ - def findNewHistoryIndex( - startIndex: Int, - searchTerm: Vector[Char], - history: IndexedSeq[String], - indexIncrement: Int, - skipped: Vector[Char] - ) = { - /** - * `Some(i)` means we found a reasonable result at history element `i` - * `None` means we couldn't find anything, and should show a not-found - * error to the user - */ - def rec(i: Int): Option[Int] = history.lift(i) match { - // If i < 0, it means the user is pressing `down` too many times, which - // means it doesn't show anything but we shouldn't show an error - case None if i < 0 => Some(-1) - case None => None - case Some(s) if s.contains(searchTerm) && !s.contentEquals(skipped) => - Some(i) - case _ => rec(i + indexIncrement) - } - - val newHistoryIndex = rec(startIndex) - val foundIndex = newHistoryIndex.find(_ != -1) - val newBuffer = foundIndex match { - case None => searchTerm - case Some(i) => history(i).toVector - } - - val newCursor = foundIndex match { - case None => newBuffer.length - case Some(i) => history(i).indexOfSlice(searchTerm) + searchTerm.length - } - - (newHistoryIndex, newBuffer, newCursor) - } - - val emptySearchMessage = - s" ...enter the string to search for, then `up` for more" - val cannotFindSearchMessage = - s" ...can't be found in history; re-starting search" -} diff --git a/compiler/src/dotty/tools/repl/terminal/filters/ReadlineFilters.scala b/compiler/src/dotty/tools/repl/terminal/filters/ReadlineFilters.scala deleted file mode 100644 index c619238de2d0..000000000000 --- a/compiler/src/dotty/tools/repl/terminal/filters/ReadlineFilters.scala +++ /dev/null @@ -1,211 +0,0 @@ -package dotty.tools -package repl -package terminal -package filters - -import terminal.FilterTools._ -import terminal.SpecialKeys._ -import terminal.{DelegateFilter, Filter, Terminal} - -import Filter._ - -/** - * 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( - simple(Ctrl('p'))((b, c, m) => BasicFilters.moveUp(b, c, m.width)), - simple(Ctrl('n'))((b, c, m) => BasicFilters.moveDown(b, c, m.width)), - simple(Ctrl('b'))((b, c, m) => (b, c - 1)), // <- one char - simple(Ctrl('f'))((b, c, m) => (b, c + 1)), // -> one char - simple(Alt + "b")((b, c, m) => GUILikeFilters.wordLeft(b, c)), // <- one word - simple(Alt + "B")((b, c, m) => GUILikeFilters.wordLeft(b, c)), // <- one word - simple(LinuxCtrlLeft)((b, c, m) => GUILikeFilters.wordLeft(b, c)), // <- one word - simple(Alt + "f")((b, c, m) => GUILikeFilters.wordRight(b, c)), // -> one word - simple(Alt + "F")((b, c, m) => GUILikeFilters.wordRight(b, c)), // -> one word - simple(LinuxCtrlRight)((b, c, m) => GUILikeFilters.wordRight(b, c)), // -> one word - simple(Home)((b, c, m) => BasicFilters.moveStart(b, c, m.width)), // <- one line - simple(HomeScreen)((b, c, m) => BasicFilters.moveStart(b, c, m.width)), // <- one line - simple(HomeLinuxXterm)((b, c, m) => BasicFilters.moveStart(b, c, m.width)), // <- one line - simple(Ctrl('a'))((b, c, m) => BasicFilters.moveStart(b, c, m.width)), - simple(End)((b, c, m) => BasicFilters.moveEnd(b, c, m.width)), // -> one line - simple(EndScreen)((b, c, m) => BasicFilters.moveEnd(b, c, m.width)), // -> one line - simple(EndRxvt)((b, c, m) => BasicFilters.moveEnd(b, c, m.width)), // -> one line - simple(Ctrl('e'))((b, c, m) => BasicFilters.moveEnd(b, c, m.width)), - simple(Alt + "t")((b, c, m) => transposeWord(b, c)), - simple(Alt + "T")((b, c, m) => transposeWord(b, c)), - simple(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("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 cutLineLeft(b: Vector[Char], c: Int) = { - val (allBeforeCursor, allAfterCursor) = b.splitAt(c) - val previousNewlineIndex = allBeforeCursor.lastIndexWhere(_ == '\n') - if (previousNewlineIndex == -1) { - // beginning of input. no leading newline - prepend(allBeforeCursor) - (allAfterCursor, 0) - } else { - val (allBeforeLine, lineBeforeCursor) = allBeforeCursor.splitAt( - previousNewlineIndex - ) - val charsBeforeCursorOnLine = lineBeforeCursor.length - if (charsBeforeCursorOnLine == 1) { - // if only a newline before cursor on line, cut it - prepend(lineBeforeCursor) - (allBeforeLine ++ allAfterCursor, c - 1) - } else { - // if there's more on the line before cursor, cut to beginning of line - prepend(lineBeforeCursor.tail) - val buffer = allBeforeLine ++ "\n" ++ allAfterCursor - val cursor = c - (charsBeforeCursorOnLine - 1) - (buffer, cursor) - } - } - } - - def cutLineRight(b: Vector[Char], c: Int) = { - val (allBeforeCursor, allAfterCursor) = b.splitAt(c) - val nextNewlineIndex = allAfterCursor.indexWhere(_ == '\n') - if (nextNewlineIndex == -1) { - // end of input. no trailing newline - append(allAfterCursor) - (allBeforeCursor, c) - } else { - allAfterCursor.splitAt( - nextNewlineIndex - ) match { - case (Vector(), ('\n' +: allAfterNewline)) => - // if there's only a newline after cursor on line, cut it - append(Vector('\n')) - (allBeforeCursor ++ allAfterNewline, c) - case (restOfLine, allAfterLine) => - // if there's more on the line after cursor, cut to end of line - append(restOfLine) - (allBeforeCursor ++ allAfterLine, 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( - simple(Ctrl('u'))((b, c, m) => cutLineLeft(b, c)), - simple(Ctrl('k'))((b, c, m) => cutLineRight(b, c)), - simple(Alt + "d")((b, c, m) => cutWordRight(b, c)), - simple(Ctrl('w'))((b, c, m) => cutWordLeft(b, c)), - simple(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(identifier) { _ => accumulating = false; None }, - simple(Ctrl('h'))((b, c, m) => cutCharLeft(b, c)), - simple(Ctrl('y'))((b, c, m) => paste(b, c)) - ) - } - -} diff --git a/compiler/src/dotty/tools/repl/terminal/filters/UndoFilter.scala b/compiler/src/dotty/tools/repl/terminal/filters/UndoFilter.scala deleted file mode 100644 index c0b01ea1b7f5..000000000000 --- a/compiler/src/dotty/tools/repl/terminal/filters/UndoFilter.scala +++ /dev/null @@ -1,154 +0,0 @@ -package dotty.tools -package repl -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("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.partial("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/compiler/test/dotty/tools/repl/TabcompleteTests.scala b/compiler/test/dotty/tools/repl/TabcompleteTests.scala index db84b55aa6bf..1f07489e7b9c 100644 --- a/compiler/test/dotty/tools/repl/TabcompleteTests.scala +++ b/compiler/test/dotty/tools/repl/TabcompleteTests.scala @@ -1,78 +1,65 @@ package dotty.tools package repl +import dotty.tools.repl.ReplTest._ import org.junit.Assert._ import org.junit.Test -import dotc.reporting.MessageRendering -import dotc.ast.untpd - -import results._ -import ReplTest._ - /** These tests test input that has proved problematic */ class TabcompleteTests extends ReplTest { /** Returns the `(, )`*/ - private[this] def tabComplete(src: String)(implicit state: State): Completions = - completions(src.length, src, state) + private[this] def tabComplete(src: String)(implicit state: State): List[String] = + completions(src.length, src, state).map(_.value) @Test def tabCompleteList: Unit = fromInitialState { implicit s => val comp = tabComplete("List.r") - assertTrue(s"""Expected single element "range" got: ${comp.suggestions}""", - comp.suggestions.head == "range") + assertEquals(List("range"), comp.distinct) } @Test def tabCompleteListInstance: Unit = fromInitialState { implicit s => val comp = tabComplete("(null: List[Int]).sli") - assertTrue(s"""Expected completions "slice" and "sliding": ${comp.suggestions}""", - comp.suggestions.sorted == List("slice", "sliding")) + assertEquals(List("slice", "sliding"), comp.distinct.sorted) } - @Test def tabCompleteModule: Unit = - fromInitialState{ implicit s => - val comp = tabComplete("scala.Pred") - assertEquals(comp.suggestions,List("Predef")) - } - - @Test def autoCompleteValAssign: Unit = - fromInitialState { implicit s => tabComplete("val x = 5") } - - @Test def tabCompleteNumberDot: Unit = - fromInitialState { implicit s => tabComplete("val x = 5 + 5.") } + @Test def tabCompleteModule: Unit = fromInitialState{ implicit s => + val comp = tabComplete("scala.Pred") + assertEquals(List("Predef"), comp) + } - @Test def tabCompleteInClass: Unit = - fromInitialState { implicit s => - tabComplete("class Foo { def bar: List[Int] = List.a") - } + @Test def tabCompleteInClass: Unit = fromInitialState { implicit s => + val comp = tabComplete("class Foo { def bar: List[Int] = List.ap") + assertEquals(List("apply"), comp) + } @Test def tabCompleteTwiceIn: Unit = { - val src1 = "class Foo { def bar: List[Int] = List.a" - val src2 = "class Foo { def bar: List[Int] = List.app" + val src1 = "class Foo { def bar(xs: List[Int]) = xs.map" + val src2 = "class Foo { def bar(xs: List[Int]) = xs.mapC" fromInitialState { implicit state => - assert(tabComplete(src1).suggestions.nonEmpty) + val comp = tabComplete(src1) + assertEquals(List("map", "mapConserve"), comp.sorted) state } .andThen { implicit state => - assert(tabComplete(src2).suggestions.nonEmpty) + val comp = tabComplete(src2) + assertEquals(List("mapConserve"), comp) } } - @Test def i3309: Unit = - fromInitialState { implicit s => - // TODO: add back '.', once #4397 is fixed - List("\"", ")", "'", "¨", "£", ":", ",", ";", "@", "}", "[", "]") - .foreach(src => assertTrue(tabComplete(src).suggestions.isEmpty)) - } + @Test def i3309: Unit = fromInitialState { implicit s => + // We make sure we do not crash + List("\"", ")", "'", "¨", "£", ":", ",", ";", "@", "}", "[", "]", ".") + .foreach(tabComplete(_)) + } - @Test def sortedCompletions: Unit = + @Test def completeFromPreviousState: Unit = fromInitialState { implicit state => val src = "class Foo { def comp3 = 3; def comp1 = 1; def comp2 = 2 }" compiler.compile(src).stateOrFail } .andThen { implicit state => val expected = List("comp1", "comp2", "comp3") - assertEquals(expected, tabComplete("(new Foo).comp").suggestions) + assertEquals(expected, tabComplete("(new Foo).comp").sorted) } } diff --git a/project/Build.scala b/project/Build.scala index 592457e43542..f37a4cb9380f 100644 --- a/project/Build.scala +++ b/project/Build.scala @@ -518,6 +518,7 @@ object Build { ("org.scala-lang.modules" %% "scala-xml" % "1.1.0").withDottyCompat(scalaVersion.value), "org.scala-lang" % "scala-library" % scalacVersion % "test", Dependencies.compilerInterface(sbtVersion.value), + "org.jline" % "jline" % "3.7.0" // used by the REPL ), // For convenience, change the baseDirectory when running the compiler From a437b74b4f32c37c4b76459ceb3201a80f97a711 Mon Sep 17 00:00:00 2001 From: Allan Renucci Date: Mon, 18 Jun 2018 19:06:16 +0200 Subject: [PATCH 02/18] Cleanup SourceFile API --- compiler/src/dotty/tools/dotc/CompilationUnit.scala | 2 +- .../tools/dotc/fromtasty/ReadTastyTreesFromClasses.scala | 3 +-- compiler/src/dotty/tools/dotc/quoted/QuoteCompiler.scala | 4 ++-- compiler/src/dotty/tools/dotc/util/SourceFile.scala | 7 +++---- compiler/src/dotty/tools/repl/JLineTerminal.scala | 2 +- compiler/src/dotty/tools/repl/ParseResult.scala | 2 +- compiler/src/dotty/tools/repl/ReplDriver.scala | 2 +- .../test/dotty/tools/dotc/ast/UntypedTreeMapTest.scala | 2 +- .../dotty/tools/dotc/parsing/ModifiersParsingTest.scala | 2 +- 9 files changed, 12 insertions(+), 14 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/CompilationUnit.scala b/compiler/src/dotty/tools/dotc/CompilationUnit.scala index f94558e438ee..3fc50449978a 100644 --- a/compiler/src/dotty/tools/dotc/CompilationUnit.scala +++ b/compiler/src/dotty/tools/dotc/CompilationUnit.scala @@ -37,7 +37,7 @@ object CompilationUnit { /** Make a compilation unit for top class `clsd` with the contends of the `unpickled` */ def mkCompilationUnit(clsd: ClassDenotation, unpickled: Tree, forceTrees: Boolean)(implicit ctx: Context): CompilationUnit = - mkCompilationUnit(new SourceFile(clsd.symbol.associatedFile, Seq()), unpickled, forceTrees) + mkCompilationUnit(SourceFile(clsd.symbol.associatedFile, Array.empty), unpickled, forceTrees) /** Make a compilation unit, given picked bytes and unpickled tree */ def mkCompilationUnit(source: SourceFile, unpickled: Tree, forceTrees: Boolean)(implicit ctx: Context): CompilationUnit = { diff --git a/compiler/src/dotty/tools/dotc/fromtasty/ReadTastyTreesFromClasses.scala b/compiler/src/dotty/tools/dotc/fromtasty/ReadTastyTreesFromClasses.scala index 7b3b9635192e..8fbbcdca3d07 100644 --- a/compiler/src/dotty/tools/dotc/fromtasty/ReadTastyTreesFromClasses.scala +++ b/compiler/src/dotty/tools/dotc/fromtasty/ReadTastyTreesFromClasses.scala @@ -43,8 +43,7 @@ class ReadTastyTreesFromClasses extends FrontEnd { case unpickler: tasty.DottyUnpickler => if (cls.tree.isEmpty) None else { - val source = SourceFile(cls.associatedFile, Array()) - val unit = mkCompilationUnit(source, cls.tree, forceTrees = true) + val unit = mkCompilationUnit(cls, cls.tree, forceTrees = true) unit.pickled += (cls -> unpickler.unpickler.bytes) Some(unit) } diff --git a/compiler/src/dotty/tools/dotc/quoted/QuoteCompiler.scala b/compiler/src/dotty/tools/dotc/quoted/QuoteCompiler.scala index 1b478fcc4aba..89f2c4f74396 100644 --- a/compiler/src/dotty/tools/dotc/quoted/QuoteCompiler.scala +++ b/compiler/src/dotty/tools/dotc/quoted/QuoteCompiler.scala @@ -62,12 +62,12 @@ class QuoteCompiler(directory: AbstractFile) extends Compiler { val tree = if (putInClass) inClass(exprUnit.expr) else PickledQuotes.quotedExprToTree(exprUnit.expr) - val source = new SourceFile("", Seq()) + val source = new SourceFile("", "") CompilationUnit.mkCompilationUnit(source, tree, forceTrees = true) case typeUnit: TypeCompilationUnit => assert(!putInClass) val tree = PickledQuotes.quotedTypeToTree(typeUnit.tpe) - val source = new SourceFile("", Seq()) + val source = new SourceFile("", "") CompilationUnit.mkCompilationUnit(source, tree, forceTrees = true) } } diff --git a/compiler/src/dotty/tools/dotc/util/SourceFile.scala b/compiler/src/dotty/tools/dotc/util/SourceFile.scala index 8162543c1c47..d80c49617290 100644 --- a/compiler/src/dotty/tools/dotc/util/SourceFile.scala +++ b/compiler/src/dotty/tools/dotc/util/SourceFile.scala @@ -37,9 +37,8 @@ object ScriptSourceFile { case class SourceFile(file: AbstractFile, content: Array[Char]) extends interfaces.SourceFile { - def this(_file: AbstractFile, codec: Codec) = this(_file, new String(_file.toByteArray, codec.charSet).toCharArray) - def this(sourceName: String, cs: Seq[Char]) = this(new VirtualFile(sourceName), cs.toArray) - def this(file: AbstractFile, cs: Seq[Char]) = this(file, cs.toArray) + def this(file: AbstractFile, codec: Codec) = this(file, new String(file.toByteArray, codec.charSet).toCharArray) + def this(name: String, content: String) = this(new VirtualFile(name), content.toCharArray) /** Tab increment; can be overridden */ def tabInc = 8 @@ -150,7 +149,7 @@ case class SourceFile(file: AbstractFile, content: Array[Char]) extends interfac override def toString = file.toString } -@sharable object NoSource extends SourceFile("", Nil) { +@sharable object NoSource extends SourceFile("", "") { override def exists = false override def atPos(pos: Position): SourcePosition = NoSourcePosition } diff --git a/compiler/src/dotty/tools/repl/JLineTerminal.scala b/compiler/src/dotty/tools/repl/JLineTerminal.scala index ebae2ec39f36..ffa9c54ec813 100644 --- a/compiler/src/dotty/tools/repl/JLineTerminal.scala +++ b/compiler/src/dotty/tools/repl/JLineTerminal.scala @@ -95,7 +95,7 @@ final class JLineTerminal { case ParseContext.COMPLETE => // Parse to find completions (typically after a Tab). - val source = new SourceFile("", line.toCharArray) + val source = new SourceFile("", line) val scanner = new Scanner(source)(ctx.fresh.setReporter(Reporter.NoReporter)) // Looking for the current word being completed diff --git a/compiler/src/dotty/tools/repl/ParseResult.scala b/compiler/src/dotty/tools/repl/ParseResult.scala index d7a7574da058..f629ec0caa67 100644 --- a/compiler/src/dotty/tools/repl/ParseResult.scala +++ b/compiler/src/dotty/tools/repl/ParseResult.scala @@ -99,7 +99,7 @@ object ParseResult { @sharable private[this] val CommandExtract = """(:[\S]+)\s*(.*)""".r private def parseStats(sourceCode: String)(implicit ctx: Context): List[untpd.Tree] = { - val source = new SourceFile("", sourceCode.toCharArray) + val source = new SourceFile("", sourceCode) val parser = new Parser(source) val stats = parser.blockStatSeq() parser.accept(Tokens.EOF) diff --git a/compiler/src/dotty/tools/repl/ReplDriver.scala b/compiler/src/dotty/tools/repl/ReplDriver.scala index 114d4307122f..2fa1461cdc72 100644 --- a/compiler/src/dotty/tools/repl/ReplDriver.scala +++ b/compiler/src/dotty/tools/repl/ReplDriver.scala @@ -156,7 +156,7 @@ class ReplDriver(settings: Array[String], compiler .typeCheck(expr, errorsAllowed = true) .map { tree => - val file = new SourceFile("", expr.toCharArray) + val file = new SourceFile("", expr) val unit = new CompilationUnit(file) unit.tpdTree = tree implicit val ctx = state.run.runContext.fresh.setCompilationUnit(unit) diff --git a/compiler/test/dotty/tools/dotc/ast/UntypedTreeMapTest.scala b/compiler/test/dotty/tools/dotc/ast/UntypedTreeMapTest.scala index 9e7402968513..251bc3ab07a8 100644 --- a/compiler/test/dotty/tools/dotc/ast/UntypedTreeMapTest.scala +++ b/compiler/test/dotty/tools/dotc/ast/UntypedTreeMapTest.scala @@ -14,7 +14,7 @@ class UntpdTreeMapTest extends DottyTest { import untpd._ def parse(code: String): Tree = { - val (_, stats) = new Parser(new SourceFile("", code.toCharArray)).templateStatSeq() + val (_, stats) = new Parser(new SourceFile("", code)).templateStatSeq() stats match { case List(stat) => stat; case stats => untpd.Thicket(stats) } } diff --git a/compiler/test/dotty/tools/dotc/parsing/ModifiersParsingTest.scala b/compiler/test/dotty/tools/dotc/parsing/ModifiersParsingTest.scala index f0ec6ca0be21..32e8535e179a 100644 --- a/compiler/test/dotty/tools/dotc/parsing/ModifiersParsingTest.scala +++ b/compiler/test/dotty/tools/dotc/parsing/ModifiersParsingTest.scala @@ -17,7 +17,7 @@ object ModifiersParsingTest { implicit val ctx: Context = (new ContextBase).initialCtx implicit def parse(code: String): Tree = { - val (_, stats) = new Parser(new SourceFile("", code.toCharArray)).templateStatSeq() + val (_, stats) = new Parser(new SourceFile("", code)).templateStatSeq() stats match { case List(stat) => stat; case stats => Thicket(stats) } } From 801c761e91e61727c0489a5cf61baf27a2e17400 Mon Sep 17 00:00:00 2001 From: Allan Renucci Date: Mon, 18 Jun 2018 19:19:35 +0200 Subject: [PATCH 03/18] Take cursor position into account when inserting a new line --- compiler/src/dotty/tools/repl/JLineTerminal.scala | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/compiler/src/dotty/tools/repl/JLineTerminal.scala b/compiler/src/dotty/tools/repl/JLineTerminal.scala index ffa9c54ec813..c014390ed8ab 100644 --- a/compiler/src/dotty/tools/repl/JLineTerminal.scala +++ b/compiler/src/dotty/tools/repl/JLineTerminal.scala @@ -87,8 +87,8 @@ final class JLineTerminal { context match { case ParseContext.ACCEPT_LINE => - // TODO: take into account cursor position - if (ParseResult.isIncomplete(line)) incomplete() + val lastLineOffset = line.lastIndexOfSlice(System.lineSeparator) + if (cursor <= lastLineOffset || ParseResult.isIncomplete(line)) incomplete() else parsedLine("", 0) // using dummy values, // resulting parsed line is probably unused From 94b2d8106019067ea2890702c63f39578db06662 Mon Sep 17 00:00:00 2001 From: Allan Renucci Date: Tue, 19 Jun 2018 15:41:31 +0200 Subject: [PATCH 04/18] Use JGit in the build instead of shell and bat scripts --- project/VersionUtil.scala | 43 +++++++++++++++---- project/build.sbt | 2 + project/scripts/build/get-scala-commit-date | 16 ------- .../scripts/build/get-scala-commit-date.bat | 9 ---- project/scripts/build/get-scala-commit-sha | 18 -------- .../scripts/build/get-scala-commit-sha.bat | 9 ---- 6 files changed, 36 insertions(+), 61 deletions(-) create mode 100644 project/build.sbt delete mode 100755 project/scripts/build/get-scala-commit-date delete mode 100644 project/scripts/build/get-scala-commit-date.bat delete mode 100755 project/scripts/build/get-scala-commit-sha delete mode 100644 project/scripts/build/get-scala-commit-sha.bat diff --git a/project/VersionUtil.scala b/project/VersionUtil.scala index c127c8fee000..f0297f56e0c0 100644 --- a/project/VersionUtil.scala +++ b/project/VersionUtil.scala @@ -1,18 +1,43 @@ -import scala.sys.process.Process +import org.eclipse.jgit.storage.file.FileRepositoryBuilder +import org.eclipse.jgit.lib.{Constants, ObjectId, Ref, Repository} +import org.eclipse.jgit.revwalk.{RevCommit, RevWalk} + +import java.text.SimpleDateFormat +import java.util.Date object VersionUtil { - def executeScript(scriptName: String) = { - val cmd = - if (System.getProperty("os.name").toLowerCase.contains("windows")) - s"cmd.exe /c project\\scripts\\build\\$scriptName.bat -p" - else s"project/scripts/build/$scriptName" - Process(cmd).lineStream.head.trim + + // Adapted from sbt-git + private class JGit(repo: Repository) { + def headCommit: ObjectId = + repo.exactRef(Constants.HEAD).getObjectId + + def headCommitSha: String = headCommit.name + + def headCommitDate: Date = { + val walk = new RevWalk(repo) + val commit = walk.parseCommit(headCommit) + val seconds = commit.getCommitTime.toLong + val millis = seconds * 1000L + new Date(millis) + } + } + + private lazy val git = { + val repo = new FileRepositoryBuilder() + .setMustExist(true) + .findGitDir() + .build() + new JGit(repo) } /** Seven letters of the SHA hash is considered enough to uniquely identify a * commit, albeit extremely large projects - such as the Linux kernel - need * more letters to stay unique */ - def gitHash = executeScript("get-scala-commit-sha").substring(0, 7) - def commitDate = executeScript("get-scala-commit-date") + def gitHash: String = git.headCommitSha.substring(0, 7) + def commitDate: String = { + val format = new SimpleDateFormat("yyyyMMdd") + format.format(git.headCommitDate) + } } diff --git a/project/build.sbt b/project/build.sbt new file mode 100644 index 000000000000..7631c96def0a --- /dev/null +++ b/project/build.sbt @@ -0,0 +1,2 @@ +// Used by VersionUtil to get gitHash and commitDate +libraryDependencies += "org.eclipse.jgit" % "org.eclipse.jgit" % "4.11.0.201803080745-r" diff --git a/project/scripts/build/get-scala-commit-date b/project/scripts/build/get-scala-commit-date deleted file mode 100755 index ef5b0f540da7..000000000000 --- a/project/scripts/build/get-scala-commit-date +++ /dev/null @@ -1,16 +0,0 @@ -#!/usr/bin/env bash -# -# Usage: get-scala-commit-date [dir] -# Figures out current commit date of a git clone. -# If no dir is given, current working dir is used. -# -# Example build version string: -# 20120312 -# - -[[ $# -eq 0 ]] || cd "$1" - -lastcommitdate=$(git log --format="%ci" HEAD | head -n 1 | cut -d ' ' -f 1) - -# 20120324 -echo "${lastcommitdate//-/}" diff --git a/project/scripts/build/get-scala-commit-date.bat b/project/scripts/build/get-scala-commit-date.bat deleted file mode 100644 index 735a80b927f3..000000000000 --- a/project/scripts/build/get-scala-commit-date.bat +++ /dev/null @@ -1,9 +0,0 @@ -@echo off -for %%X in (bash.exe) do (set FOUND=%%~$PATH:X) -if defined FOUND ( - bash "%~dp0\get-scala-commit-date" 2>NUL -) else ( - rem echo this script does not work with cmd.exe. please, install bash - echo unknown - exit 1 -) diff --git a/project/scripts/build/get-scala-commit-sha b/project/scripts/build/get-scala-commit-sha deleted file mode 100755 index eab90a4215fc..000000000000 --- a/project/scripts/build/get-scala-commit-sha +++ /dev/null @@ -1,18 +0,0 @@ -#!/usr/bin/env bash -# -# Usage: get-scala-commit-sha [dir] -# Figures out current commit sha of a git clone. -# If no dir is given, current working dir is used. -# -# Example build version string: -# 6f1c486d0ba -# - -[[ $# -eq 0 ]] || cd "$1" - -# printf %016s is not portable for 0-padding, has to be a digit. -# so we're stuck disassembling it. -hash=$(git log -1 --format="%H" HEAD) -hash=${hash#g} -hash=${hash:0:10} -echo "$hash" diff --git a/project/scripts/build/get-scala-commit-sha.bat b/project/scripts/build/get-scala-commit-sha.bat deleted file mode 100644 index 6559a191201c..000000000000 --- a/project/scripts/build/get-scala-commit-sha.bat +++ /dev/null @@ -1,9 +0,0 @@ -@echo off -for %%X in (bash.exe) do (set FOUND=%%~$PATH:X) -if defined FOUND ( - bash "%~dp0\get-scala-commit-sha" 2>NUL -) else ( - rem echo this script does not work with cmd.exe. please, install bash - echo unknown - exit 1 -) From 8fd65ace7de80bff984c9569c96f26a481ee3be2 Mon Sep 17 00:00:00 2001 From: Allan Renucci Date: Tue, 19 Jun 2018 16:06:34 +0200 Subject: [PATCH 05/18] Add dependency on jline-terminal-jna for Windows --- project/Build.scala | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/project/Build.scala b/project/Build.scala index f37a4cb9380f..3c1041defda5 100644 --- a/project/Build.scala +++ b/project/Build.scala @@ -518,7 +518,8 @@ object Build { ("org.scala-lang.modules" %% "scala-xml" % "1.1.0").withDottyCompat(scalaVersion.value), "org.scala-lang" % "scala-library" % scalacVersion % "test", Dependencies.compilerInterface(sbtVersion.value), - "org.jline" % "jline" % "3.7.0" // used by the REPL + "org.jline" % "jline" % "3.7.0", // used by the REPL + "org.jline" % "jline-terminal-jna" % "3.7.0" // needed for Windows ), // For convenience, change the baseDirectory when running the compiler From d9c0c54fd58168c7ebc309b062c6b14a22eb97ca Mon Sep 17 00:00:00 2001 From: Allan Renucci Date: Tue, 19 Jun 2018 18:24:12 +0200 Subject: [PATCH 06/18] Polishing --- .../src/dotty/tools/repl/JLineTerminal.scala | 7 ++- .../src/dotty/tools/repl/ReplDriver.scala | 48 +++++++++---------- 2 files changed, 30 insertions(+), 25 deletions(-) diff --git a/compiler/src/dotty/tools/repl/JLineTerminal.scala b/compiler/src/dotty/tools/repl/JLineTerminal.scala index c014390ed8ab..1c2f0b5d92e1 100644 --- a/compiler/src/dotty/tools/repl/JLineTerminal.scala +++ b/compiler/src/dotty/tools/repl/JLineTerminal.scala @@ -14,7 +14,10 @@ import org.jline.reader.impl.history.DefaultHistory import org.jline.terminal.TerminalBuilder import org.jline.utils.AttributedString -final class JLineTerminal { +final class JLineTerminal extends java.io.Closeable { + // import java.util.logging.{Logger, Level} + // Logger.getLogger("org.jline").setLevel(Level.FINEST) + private val terminal = TerminalBuilder.terminal() private val history = new DefaultHistory @@ -52,6 +55,8 @@ final class JLineTerminal { lineReader.readLine(prompt) } + def close() = terminal.close() + /** Provide syntax highlighting */ private class Highlighter extends reader.Highlighter { def highlight(reader: LineReader, buffer: String): AttributedString = { diff --git a/compiler/src/dotty/tools/repl/ReplDriver.scala b/compiler/src/dotty/tools/repl/ReplDriver.scala index 2fa1461cdc72..1bf53989b357 100644 --- a/compiler/src/dotty/tools/repl/ReplDriver.scala +++ b/compiler/src/dotty/tools/repl/ReplDriver.scala @@ -114,19 +114,39 @@ class ReplDriver(settings: Array[String], * `protected final` to facilitate testing. */ final def runUntilQuit(): State = { + val terminal = new JLineTerminal() + + /** Blockingly read a line, getting back a parse result */ + def readLine(state: State): ParseResult = { + val completer: Completer = { (_, line, candidates) => + val comps = completions(line.cursor, line.line, state) + candidates.addAll(comps.asJava) + } + implicit val ctx = state.run.runContext + try { + val line = terminal.readLine(completer) + ParseResult(line) + } + catch { + case _: EndOfFileException => // Ctrl+D + Quit + } + } + @tailrec def loop(state: State): State = { - val res = readLine()(state) + val res = readLine(state) if (res == Quit) state else { // readLine potentially destroys the run, so a new one is needed for the // rest of the interpretation: - implicit val freshState = state.newRun(compiler, rootCtx) - loop(interpret(res)) + val freshState = state.newRun(compiler, rootCtx) + loop(interpret(res)(freshState)) } } - withRedirectedOutput { loop(initState) } + try withRedirectedOutput { loop(initState) } + finally terminal.close() } final def run(input: String)(implicit state: State): State = withRedirectedOutput { @@ -167,26 +187,6 @@ class ReplDriver(settings: Array[String], .getOrElse(Nil) } - // lazy because the REPL tests do not rely on the JLine reader - private lazy val terminal = new JLineTerminal() - - /** Blockingly read a line, getting back a parse result */ - private def readLine()(implicit state: State): ParseResult = { - implicit val ctx = state.run.runContext - val completer: Completer = { (_, line, candidates) => - val comps = completions(line.cursor, line.line, state) - candidates.addAll(comps.asJava) - } - try { - val line = terminal.readLine(completer) - ParseResult(line) - } - catch { - case _: EndOfFileException => // Ctrl+D - Quit - } - } - private def extractImports(trees: List[untpd.Tree]): List[untpd.Import] = trees.collect { case imp: untpd.Import => imp } From f24779991b6600d0a12c9450abc73997e61996f4 Mon Sep 17 00:00:00 2001 From: Allan Renucci Date: Tue, 19 Jun 2018 19:10:56 +0200 Subject: [PATCH 07/18] Fix tests: add jline to test classpath --- compiler/src/dotty/tools/dotc/reporting/Reporter.scala | 2 +- compiler/test/dotty/Jars.scala | 9 +++++---- compiler/test/dotty/tools/dotc/CompilationTests.scala | 3 ++- 3 files changed, 8 insertions(+), 6 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/reporting/Reporter.scala b/compiler/src/dotty/tools/dotc/reporting/Reporter.scala index 511ae7c6c074..f96dcec8635b 100644 --- a/compiler/src/dotty/tools/dotc/reporting/Reporter.scala +++ b/compiler/src/dotty/tools/dotc/reporting/Reporter.scala @@ -25,7 +25,7 @@ object Reporter { } /** A reporter that ignores reports */ - object NoReporter extends Reporter { + @sharable object NoReporter extends Reporter { def doReport(m: MessageContainer)(implicit ctx: Context) = () override def report(m: MessageContainer)(implicit ctx: Context): Unit = () } diff --git a/compiler/test/dotty/Jars.scala b/compiler/test/dotty/Jars.scala index f759e241c5b5..1c80b02b7c86 100644 --- a/compiler/test/dotty/Jars.scala +++ b/compiler/test/dotty/Jars.scala @@ -18,16 +18,17 @@ object Jars { lazy val scalaAsm: String = findJarFromRuntime("scala-asm-6.0.0-scala-1") + /** JLine Jar */ + lazy val jline: String = + findJarFromRuntime("jline-3.7.0") + /** Dotty extras classpath from env or properties */ val dottyExtras: List[String] = sys.env.get("DOTTY_EXTRAS") .map(_.split(":").toList).getOrElse(Properties.dottyExtras) - /** Dotty REPL dependencies */ - val dottyReplDeps: List[String] = dottyLib :: dottyExtras - /** Dotty test dependencies */ val dottyTestDeps: List[String] = - dottyLib :: dottyCompiler :: dottyInterfaces :: dottyExtras + dottyLib :: dottyCompiler :: dottyInterfaces :: jline :: dottyExtras /** Dotty runtime with compiler dependencies, used for quoted.Expr.run */ lazy val dottyRunWithCompiler: List[String] = diff --git a/compiler/test/dotty/tools/dotc/CompilationTests.scala b/compiler/test/dotty/tools/dotc/CompilationTests.scala index fca65becc36f..e0b81251ed29 100644 --- a/compiler/test/dotty/tools/dotc/CompilationTests.scala +++ b/compiler/test/dotty/tools/dotc/CompilationTests.scala @@ -255,7 +255,8 @@ class CompilationTests extends ParallelTesting { defaultOutputDir + libGroup + "/src/:" + // as well as bootstrapped compiler: defaultOutputDir + dotty1Group + "/dotty/:" + - Jars.dottyInterfaces, + // and the other compiler dependecies: + Jars.dottyInterfaces + ":" + Jars.jline, Array("-Ycheck-reentrant") ) From de2a214381a8e10c79359767fbca1bd30baf033e Mon Sep 17 00:00:00 2001 From: Allan Renucci Date: Wed, 20 Jun 2018 14:38:12 +0200 Subject: [PATCH 08/18] In REPL, ENTER means SUBMIT when: - cursor is at end (discarding whitespaces) - and, input line is complete --- compiler/src/dotty/tools/repl/JLineTerminal.scala | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/compiler/src/dotty/tools/repl/JLineTerminal.scala b/compiler/src/dotty/tools/repl/JLineTerminal.scala index 1c2f0b5d92e1..40286b55b3ea 100644 --- a/compiler/src/dotty/tools/repl/JLineTerminal.scala +++ b/compiler/src/dotty/tools/repl/JLineTerminal.scala @@ -92,11 +92,13 @@ final class JLineTerminal extends java.io.Closeable { context match { case ParseContext.ACCEPT_LINE => - val lastLineOffset = line.lastIndexOfSlice(System.lineSeparator) - if (cursor <= lastLineOffset || ParseResult.isIncomplete(line)) incomplete() + // ENTER means SUBMIT when + // - cursor is at end (discarding whitespaces) + // - and, input line is complete + val cursorIsAtEnd = line.indexWhere(!_.isWhitespace, from = cursor) >= 0 + if (cursorIsAtEnd || ParseResult.isIncomplete(line)) incomplete() else parsedLine("", 0) - // using dummy values, - // resulting parsed line is probably unused + // using dummy values, resulting parsed line is probably unused case ParseContext.COMPLETE => // Parse to find completions (typically after a Tab). From a0c3203afd77521656594b34f12e5ac68bd66ab0 Mon Sep 17 00:00:00 2001 From: Allan Renucci Date: Wed, 20 Jun 2018 14:49:42 +0200 Subject: [PATCH 09/18] Fail early if not able to create a terminal --- compiler/src/dotty/tools/repl/JLineTerminal.scala | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/compiler/src/dotty/tools/repl/JLineTerminal.scala b/compiler/src/dotty/tools/repl/JLineTerminal.scala index 40286b55b3ea..7b9f031f9ce3 100644 --- a/compiler/src/dotty/tools/repl/JLineTerminal.scala +++ b/compiler/src/dotty/tools/repl/JLineTerminal.scala @@ -18,7 +18,9 @@ final class JLineTerminal extends java.io.Closeable { // import java.util.logging.{Logger, Level} // Logger.getLogger("org.jline").setLevel(Level.FINEST) - private val terminal = TerminalBuilder.terminal() + private val terminal = TerminalBuilder.builder() + .dumb(false) // fail early if not able to create a terminal + .build() private val history = new DefaultHistory private def blue(str: String) = Console.BLUE + str + Console.RESET From 7ff9103795614be78f32b89b36555e75edf86a9b Mon Sep 17 00:00:00 2001 From: Allan Renucci Date: Fri, 22 Jun 2018 20:29:29 +0200 Subject: [PATCH 10/18] Simplify REPL tests --- .../dotty/tools/repl/ReplCompilerTests.scala | 96 +++++++++---------- compiler/test/dotty/tools/repl/ReplTest.scala | 49 +--------- .../dotty/tools/repl/TabcompleteTests.scala | 3 +- .../test/dotty/tools/repl/TypeTests.scala | 17 ++-- 4 files changed, 54 insertions(+), 111 deletions(-) diff --git a/compiler/test/dotty/tools/repl/ReplCompilerTests.scala b/compiler/test/dotty/tools/repl/ReplCompilerTests.scala index 855e781ff61a..17333f6e6e20 100644 --- a/compiler/test/dotty/tools/repl/ReplCompilerTests.scala +++ b/compiler/test/dotty/tools/repl/ReplCompilerTests.scala @@ -8,48 +8,39 @@ import dotc.core.Contexts.Context import dotc.ast.Trees._ import dotc.ast.untpd -import results._ -import ReplTest._ - class ReplCompilerTests extends ReplTest { - @Test def compileSingle: Unit = fromInitialState { implicit state => - compiler.compile("def foo: 1 = 1").stateOrFail + @Test def compileSingle = fromInitialState { implicit state => + run("def foo: 1 = 1") + assertEquals("def foo: Int(1)", storedOutput().trim) } - @Test def compileTwo = fromInitialState { implicit state => - compiler.compile("def foo: 1 = 1").stateOrFail + run("def foo: 1 = 1") } .andThen { implicit state => - val s2 = compiler.compile("def foo(i: Int): i.type = i").stateOrFail - assert(s2.objectIndex == 2, - s"Wrong object offset: expected 2 got ${s2.objectIndex}") + val s2 = run("def foo(i: Int): i.type = i") + assertEquals(2, s2.objectIndex) } - @Test def inspectSingle = + @Test def inspectWrapper = fromInitialState { implicit state => - val untpdTree = compiler.compile("def foo: 1 = 1").map(_._1.untpdTree) - - untpdTree.fold( - onErrors, - _ match { - case PackageDef(_, List(mod: untpd.ModuleDef)) => - implicit val ctx = state.run.runContext - assert(mod.name.show == "rs$line$1", mod.name.show) - case tree => fail(s"Unexpected structure: $tree") - } - ) + run("def foo = 1") + + }.andThen { implicit state => + storedOutput() // discard output + run("val x = rs$line$1.foo") + assertEquals("val x: Int = 1", storedOutput().trim) } @Test def testVar = fromInitialState { implicit state => - compile("var x = 5") - assertEquals("var x: Int = 5\n", storedOutput()) + run("var x = 5") + assertEquals("var x: Int = 5", storedOutput().trim) } @Test def testRes = fromInitialState { implicit state => - compile { + run { """|def foo = 1 + 1 |val x = 5 + 5 |1 + 1 @@ -57,72 +48,71 @@ class ReplCompilerTests extends ReplTest { |10 + 10""".stripMargin } - val expected = Set("def foo: Int", - "val x: Int = 10", - "val res1: Int = 20", - "val res0: Int = 2", - "var y: Int = 5") + val expected = List( + "def foo: Int", + "val x: Int = 10", + "val res0: Int = 2", + "var y: Int = 5", + "val res1: Int = 20" + ) - expected === storedOutput().split("\n") + assertEquals(expected, storedOutput().split("\n").toList) } @Test def testImportMutable = fromInitialState { implicit state => - compile("import scala.collection.mutable") + run("import scala.collection.mutable") } .andThen { implicit state => - assert(state.imports.nonEmpty, "Didn't add import to `State` after compilation") - - compile("""mutable.Map("one" -> 1)""") - + assertEquals(1, state.imports.size) + run("""mutable.Map("one" -> 1)""") assertEquals( - "val res0: scala.collection.mutable.Map[String, Int] = Map(one -> 1)\n", - storedOutput() + "val res0: scala.collection.mutable.Map[String, Int] = Map(one -> 1)", + storedOutput().trim ) } @Test def rebindVariable = - fromInitialState { implicit s => compile("var x = 5") } + fromInitialState { implicit s => + val state = run("var x = 5") + assertEquals("var x: Int = 5", storedOutput().trim) + state + } .andThen { implicit s => - compile("x = 10") - assertEquals( - """|var x: Int = 5 - |x: Int = 10 - |""".stripMargin, - storedOutput() - ) + run("x = 10") + assertEquals("x: Int = 10", storedOutput().trim) } // FIXME: Tests are not run in isolation, the classloader is corrupted after the first exception - @Ignore def i3305: Unit = { + @Ignore @Test def i3305: Unit = { fromInitialState { implicit s => - compile("null.toString") + run("null.toString") assertTrue(storedOutput().startsWith("java.lang.NullPointerException")) } fromInitialState { implicit s => - compile("def foo: Int = 1 + foo; foo") + run("def foo: Int = 1 + foo; foo") assertTrue(storedOutput().startsWith("def foo: Int\njava.lang.StackOverflowError")) } fromInitialState { implicit s => - compile("""throw new IllegalArgumentException("Hello")""") + run("""throw new IllegalArgumentException("Hello")""") assertTrue(storedOutput().startsWith("java.lang.IllegalArgumentException: Hello")) } fromInitialState { implicit s => - compile("val (x, y) = null") + run("val (x, y) = null") assertTrue(storedOutput().startsWith("scala.MatchError: null")) } } @Test def i2789: Unit = fromInitialState { implicit state => - compile("(x: Int) => println(x)") + run("(x: Int) => println(x)") assertTrue(storedOutput().startsWith("val res0: Int => Unit =")) } @Test def byNameParam: Unit = fromInitialState { implicit state => - compile("def f(g: => Int): Int = g") + run("def f(g: => Int): Int = g") assertTrue(storedOutput().startsWith("def f(g: => Int): Int")) } } diff --git a/compiler/test/dotty/tools/repl/ReplTest.scala b/compiler/test/dotty/tools/repl/ReplTest.scala index 3c5a3df416fb..a4f01bc7e33a 100644 --- a/compiler/test/dotty/tools/repl/ReplTest.scala +++ b/compiler/test/dotty/tools/repl/ReplTest.scala @@ -2,14 +2,12 @@ package dotty.tools package repl import dotty.Jars -import java.io.{ OutputStream, PrintStream, ByteArrayOutputStream } +import java.io.{ PrintStream, ByteArrayOutputStream } import org.junit.{ Before, After } -import dotc.core.Contexts.Context import dotc.reporting.MessageRendering import org.junit.Assert.fail -import dotc.reporting.diagnostic.MessageContainer -import results.Result +import results._ class ReplTest private (out: ByteArrayOutputStream) extends ReplDriver( @@ -26,15 +24,6 @@ class ReplTest private (out: ByteArrayOutputStream) extends ReplDriver( output } - protected implicit def toParsed(expr: String)(implicit state: State): Parsed = { - implicit val ctx = state.run.runContext - ParseResult(expr) match { - case pr: Parsed => pr - case pr => - throw new java.lang.AssertionError(s"Expected parsed, got: $pr") - } - } - /** Make sure the context is new before each test */ @Before def init(): Unit = resetToInitial() @@ -51,37 +40,3 @@ class ReplTest private (out: ByteArrayOutputStream) extends ReplDriver( op(state.newRun(compiler, rootCtx)) } } - - -object ReplTest { - - /** Fail if there are errors */ - def onErrors(xs: Seq[MessageContainer]): Nothing = throw new AssertionError( - s"Expected no errors, got: \n${ xs.map(_.message).mkString("\n") }" - ) - - implicit class TestingStateResult(val res: Result[State]) extends AnyVal { - def stateOrFail: State = res.fold(onErrors, x => x) - } - - implicit class TestingStateResultTuple[A](val res: Result[(A, State)]) extends AnyVal { - def stateOrFail: State = res.fold(onErrors, x => x._2) - } - - implicit class SetEquals(val xs: Iterable[String]) extends AnyVal { - def ===(other: Iterable[String]): Unit = { - val sortedXs = xs.toVector.sorted - val sortedOther = other.toVector.sorted - - val nonMatching = sortedXs - .zip(sortedOther) - .filterNot{ case (a, b) => a == b } - .map { case (a, b) => - s"""expected element: "$a" got "$b"""" - } - - if (nonMatching.nonEmpty) - fail(s"\nNot all elements matched:\n${ nonMatching.mkString("\n") }") - } - } -} diff --git a/compiler/test/dotty/tools/repl/TabcompleteTests.scala b/compiler/test/dotty/tools/repl/TabcompleteTests.scala index 1f07489e7b9c..b96b1759c765 100644 --- a/compiler/test/dotty/tools/repl/TabcompleteTests.scala +++ b/compiler/test/dotty/tools/repl/TabcompleteTests.scala @@ -1,7 +1,6 @@ package dotty.tools package repl -import dotty.tools.repl.ReplTest._ import org.junit.Assert._ import org.junit.Test @@ -56,7 +55,7 @@ class TabcompleteTests extends ReplTest { @Test def completeFromPreviousState: Unit = fromInitialState { implicit state => val src = "class Foo { def comp3 = 3; def comp1 = 1; def comp2 = 2 }" - compiler.compile(src).stateOrFail + run(src) } .andThen { implicit state => val expected = List("comp1", "comp2", "comp3") diff --git a/compiler/test/dotty/tools/repl/TypeTests.scala b/compiler/test/dotty/tools/repl/TypeTests.scala index e751eb362afa..c1be79fc5cc3 100644 --- a/compiler/test/dotty/tools/repl/TypeTests.scala +++ b/compiler/test/dotty/tools/repl/TypeTests.scala @@ -4,23 +4,22 @@ package repl import org.junit.Test import org.junit.Assert._ -import ReplTest._ - class TypeTests extends ReplTest { @Test def typeOf1 = fromInitialState { implicit s => - compiler.typeOf("1") - .fold(onErrors, assertEquals("Int", _)) + run(":type 1") + assertEquals("Int", storedOutput().trim) } @Test def typeOfBlock = fromInitialState { implicit s => - compiler.typeOf("{ /** omg omg omg */ 1 + 5; 1 }") - .fold(onErrors, assertEquals("Int", _)) + run(":type { /** omg omg omg */ 1 + 5; 1 }") + assertEquals("Int", storedOutput().trim) } @Test def typeOfX = - fromInitialState { implicit s => compile("val x = 5") } + fromInitialState { implicit s => run("val x = 5") } .andThen { implicit s => - compiler.typeOf("x") - .fold(onErrors, assertEquals("Int", _)) + storedOutput() // discard output + run(":type x") + assertEquals("Int", storedOutput().trim) } } From 995ae34220698abb8319f5e19f698842fb67d356 Mon Sep 17 00:00:00 2001 From: Allan Renucci Date: Fri, 22 Jun 2018 21:20:25 +0200 Subject: [PATCH 11/18] Simplify compiler run creation in REPL --- .../src/dotty/tools/repl/ParseResult.scala | 16 +++---- .../src/dotty/tools/repl/ReplCompiler.scala | 13 +++--- .../src/dotty/tools/repl/ReplDriver.scala | 44 +++++++------------ compiler/test/dotty/tools/repl/ReplTest.scala | 3 +- 4 files changed, 32 insertions(+), 44 deletions(-) diff --git a/compiler/src/dotty/tools/repl/ParseResult.scala b/compiler/src/dotty/tools/repl/ParseResult.scala index f629ec0caa67..11f9cd0d72e7 100644 --- a/compiler/src/dotty/tools/repl/ParseResult.scala +++ b/compiler/src/dotty/tools/repl/ParseResult.scala @@ -119,17 +119,17 @@ object ParseResult { case TypeOf.command => TypeOf(arg) case _ => UnknownCommand(cmd) } - case _ => { - val stats = parseStats(sourceCode) + case _ => + val reporter = newStoreReporter + val stats = parseStats(sourceCode)(ctx.fresh.setReporter(reporter)) - if (ctx.reporter.hasErrors) { - SyntaxErrors(sourceCode, - ctx.flushBufferedMessages(), - stats) - } + if (reporter.hasErrors) + SyntaxErrors( + sourceCode, + reporter.removeBufferedMessages, + stats) else Parsed(sourceCode, stats) - } } /** Check if the input is incomplete diff --git a/compiler/src/dotty/tools/repl/ReplCompiler.scala b/compiler/src/dotty/tools/repl/ReplCompiler.scala index 570d8111ec2a..0217dd58c09b 100644 --- a/compiler/src/dotty/tools/repl/ReplCompiler.scala +++ b/compiler/src/dotty/tools/repl/ReplCompiler.scala @@ -42,7 +42,7 @@ class ReplCompiler(val directory: AbstractFile) extends Compiler { def newRun(initCtx: Context, objectIndex: Int) = new Run(this, initCtx) { override protected[this] def rootContext(implicit ctx: Context) = - addMagicImports(super.rootContext.fresh.setReporter(newStoreReporter)) + addMagicImports(super.rootContext) private def addMagicImports(initCtx: Context): Context = { def addImport(path: TermName)(implicit ctx: Context) = { @@ -145,13 +145,13 @@ class ReplCompiler(val directory: AbstractFile) extends Compiler { else run.runContext.flushBufferedMessages().errors } - def compile(parsed: Parsed)(implicit state: State): Result[(CompilationUnit, State)] = { + final def compile(parsed: Parsed)(implicit state: State): Result[(CompilationUnit, State)] = { val defs = definitions(parsed.trees, state) val unit = createUnit(defs, parsed.sourceCode) runCompilationUnit(unit, defs.state) } - def typeOf(expr: String)(implicit state: State): Result[String] = + final def typeOf(expr: String)(implicit state: State): Result[String] = typeCheck(expr).map { tree => import dotc.ast.Trees._ implicit val ctx = state.run.runContext @@ -165,7 +165,7 @@ class ReplCompiler(val directory: AbstractFile) extends Compiler { } } - def typeCheck(expr: String, errorsAllowed: Boolean = false)(implicit state: State): Result[tpd.ValDef] = { + final def typeCheck(expr: String, errorsAllowed: Boolean = false)(implicit state: State): Result[tpd.ValDef] = { def wrapped(expr: String, sourceFile: SourceFile, state: State)(implicit ctx: Context): Result[untpd.PackageDef] = { def wrap(trees: Seq[untpd.Tree]): untpd.PackageDef = { @@ -219,16 +219,17 @@ class ReplCompiler(val directory: AbstractFile) extends Compiler { val run = state.run - val reporter = state.run.runContext.reporter + val reporter = newStoreReporter val src = new SourceFile(s"EvaluateExpr", expr) val runCtx = run.runContext.fresh + .setReporter(reporter) .setSetting(run.runContext.settings.YstopAfter, List("frontend")) wrapped(expr, src, state)(runCtx).flatMap { pkg => val unit = new CompilationUnit(src) unit.untpdTree = pkg - run.compileUnits(unit :: Nil, runCtx.fresh.setReporter(newStoreReporter)) + run.compileUnits(unit :: Nil, runCtx) if (errorsAllowed || !reporter.hasErrors) unwrapped(unit.tpdTree, src)(runCtx) diff --git a/compiler/src/dotty/tools/repl/ReplDriver.scala b/compiler/src/dotty/tools/repl/ReplDriver.scala index 1bf53989b357..3549263593bb 100644 --- a/compiler/src/dotty/tools/repl/ReplDriver.scala +++ b/compiler/src/dotty/tools/repl/ReplDriver.scala @@ -51,11 +51,7 @@ import scala.collection.JavaConverters._ case class State(objectIndex: Int, valIndex: Int, imports: List[untpd.Import], - run: Run) { - - def newRun(comp: ReplCompiler, rootCtx: Context): State = - copy(run = comp.newRun(rootCtx, objectIndex)) -} + run: Run) /** Main REPL instance, orchestrating input, compilation and presentation */ class ReplDriver(settings: Array[String], @@ -99,9 +95,9 @@ class ReplDriver(settings: Array[String], rendering = new Rendering(compiler, classLoader) } - protected[this] var rootCtx: Context = _ - protected[this] var compiler: ReplCompiler = _ - protected[this] var rendering: Rendering = _ + private[this] var rootCtx: Context = _ + private[this] var compiler: ReplCompiler = _ + private[this] var rendering: Rendering = _ // initialize the REPL session as part of the constructor so that once `run` // is called, we're in business @@ -135,14 +131,8 @@ class ReplDriver(settings: Array[String], @tailrec def loop(state: State): State = { val res = readLine(state) - if (res == Quit) state - else { - // readLine potentially destroys the run, so a new one is needed for the - // rest of the interpretation: - val freshState = state.newRun(compiler, rootCtx) - loop(interpret(res)(freshState)) - } + else loop(interpret(res)(state)) } try withRedirectedOutput { loop(initState) } @@ -151,12 +141,16 @@ class ReplDriver(settings: Array[String], final def run(input: String)(implicit state: State): State = withRedirectedOutput { val parsed = ParseResult(input)(state.run.runContext) - interpret(parsed)(state.newRun(compiler, rootCtx)) + interpret(parsed) } private def withRedirectedOutput(op: => State): State = Console.withOut(out) { Console.withErr(out) { op } } + private def newRun(state: State) = { + val newRun = compiler.newRun(rootCtx.fresh.setReporter(newStoreReporter), state.objectIndex) + state.copy(run = newRun) + } /** Extract possible completions at the index of `cursor` in `expr` */ protected[this] final def completions(cursor: Int, expr: String, state0: State): List[Candidate] = { @@ -172,7 +166,7 @@ class ReplDriver(settings: Array[String], /* complete = */ false // if true adds space when completing ) } - implicit val state = state0.newRun(compiler, rootCtx) + implicit val state = newRun(state0) compiler .typeCheck(expr, errorsAllowed = true) .map { tree => @@ -193,7 +187,7 @@ class ReplDriver(settings: Array[String], private def interpret(res: ParseResult)(implicit state: State): State = { val newState = res match { case parsed: Parsed if parsed.trees.nonEmpty => - compile(parsed).newRun(compiler, rootCtx) + compile(parsed, state) case SyntaxErrors(src, errs, _) => displayErrors(errs) @@ -213,12 +207,13 @@ class ReplDriver(settings: Array[String], } /** Compile `parsed` trees and evolve `state` in accordance */ - protected[this] final def compile(parsed: Parsed)(implicit state: State): State = { + private def compile(parsed: Parsed, istate: State): State = { def extractNewestWrapper(tree: untpd.Tree): Name = tree match { case PackageDef(_, (obj: untpd.ModuleDef) :: Nil) => obj.name.moduleClassName case _ => nme.NO_NAME } + implicit val state = newRun(istate) compiler .compile(parsed) .fold( @@ -331,14 +326,7 @@ class ReplDriver(settings: Array[String], val file = new java.io.File(path) if (file.exists) { val contents = scala.io.Source.fromFile(file).mkString - ParseResult(contents)(state.run.runContext) match { - case parsed: Parsed => - compile(parsed) - case SyntaxErrors(_, errors, _) => - displayErrors(errors) - case _ => - state - } + run(contents) } else { out.println(s"""Couldn't find file "${file.getCanonicalPath}"""") @@ -346,7 +334,7 @@ class ReplDriver(settings: Array[String], } case TypeOf(expr) => - compiler.typeOf(expr).fold( + compiler.typeOf(expr)(newRun(state)).fold( displayErrors, res => out.println(SyntaxHighlighting(res)) ) diff --git a/compiler/test/dotty/tools/repl/ReplTest.scala b/compiler/test/dotty/tools/repl/ReplTest.scala index a4f01bc7e33a..3e771390ca74 100644 --- a/compiler/test/dotty/tools/repl/ReplTest.scala +++ b/compiler/test/dotty/tools/repl/ReplTest.scala @@ -36,7 +36,6 @@ class ReplTest private (out: ByteArrayOutputStream) extends ReplDriver( op(initState) implicit class TestingState(state: State) { - def andThen[A](op: State => A): A = - op(state.newRun(compiler, rootCtx)) + def andThen[A](op: State => A): A = op(state) } } From 3fd7a062006e90972589140a9634626db356ac7c Mon Sep 17 00:00:00 2001 From: Allan Renucci Date: Fri, 22 Jun 2018 21:44:09 +0200 Subject: [PATCH 12/18] REPL state now store a Context instead of a Run --- .../src/dotty/tools/repl/ReplCompiler.scala | 36 +++++++++---------- .../src/dotty/tools/repl/ReplDriver.scala | 24 ++++++------- compiler/src/dotty/tools/repl/package.scala | 8 ----- 3 files changed, 28 insertions(+), 40 deletions(-) diff --git a/compiler/src/dotty/tools/repl/ReplCompiler.scala b/compiler/src/dotty/tools/repl/ReplCompiler.scala index 0217dd58c09b..0425530a91e7 100644 --- a/compiler/src/dotty/tools/repl/ReplCompiler.scala +++ b/compiler/src/dotty/tools/repl/ReplCompiler.scala @@ -72,7 +72,7 @@ class ReplCompiler(val directory: AbstractFile) extends Compiler { private def definitions(trees: List[untpd.Tree], state: State): Definitions = { import untpd._ - implicit val ctx: Context = state.run.runContext + implicit val ctx: Context = state.context var valIdx = state.valIndex @@ -120,7 +120,7 @@ class ReplCompiler(val directory: AbstractFile) extends Compiler { assert(defs.stats.nonEmpty) - implicit val ctx: Context = defs.state.run.runContext + implicit val ctx: Context = defs.state.context val tmpl = Template(emptyConstructor, Nil, EmptyValDef, defs.stats) val module = ModuleDef(objectName(defs.state), tmpl) @@ -137,12 +137,11 @@ class ReplCompiler(val directory: AbstractFile) extends Compiler { } private def runCompilationUnit(unit: CompilationUnit, state: State): Result[(CompilationUnit, State)] = { - val run = state.run - val reporter = state.run.runContext.reporter - run.compileUnits(unit :: Nil) + val ctx = state.context + ctx.run.compileUnits(unit :: Nil) - if (!reporter.hasErrors) (unit, state).result - else run.runContext.flushBufferedMessages().errors + if (!ctx.reporter.hasErrors) (unit, state).result + else ctx.reporter.removeBufferedMessages(ctx).errors } final def compile(parsed: Parsed)(implicit state: State): Result[(CompilationUnit, State)] = { @@ -154,7 +153,7 @@ class ReplCompiler(val directory: AbstractFile) extends Compiler { final def typeOf(expr: String)(implicit state: State): Result[String] = typeCheck(expr).map { tree => import dotc.ast.Trees._ - implicit val ctx = state.run.runContext + implicit val ctx = state.context tree.rhs match { case Block(xs, _) => xs.last.tpe.widen.show case _ => @@ -218,23 +217,20 @@ class ReplCompiler(val directory: AbstractFile) extends Compiler { } - val run = state.run - val reporter = newStoreReporter - val src = new SourceFile(s"EvaluateExpr", expr) - val runCtx = - run.runContext.fresh - .setReporter(reporter) - .setSetting(run.runContext.settings.YstopAfter, List("frontend")) + val src = new SourceFile("", expr) + implicit val ctx = state.context.fresh + .setReporter(newStoreReporter) + .setSetting(state.context.settings.YstopAfter, List("frontend")) - wrapped(expr, src, state)(runCtx).flatMap { pkg => + wrapped(expr, src, state).flatMap { pkg => val unit = new CompilationUnit(src) unit.untpdTree = pkg - run.compileUnits(unit :: Nil, runCtx) + ctx.run.compileUnits(unit :: Nil, ctx) - if (errorsAllowed || !reporter.hasErrors) - unwrapped(unit.tpdTree, src)(runCtx) + if (errorsAllowed || !ctx.reporter.hasErrors) + unwrapped(unit.tpdTree, src) else { - reporter.removeBufferedMessages(runCtx).errors + ctx.reporter.removeBufferedMessages.errors } } } diff --git a/compiler/src/dotty/tools/repl/ReplDriver.scala b/compiler/src/dotty/tools/repl/ReplDriver.scala index 3549263593bb..b04f94091659 100644 --- a/compiler/src/dotty/tools/repl/ReplDriver.scala +++ b/compiler/src/dotty/tools/repl/ReplDriver.scala @@ -51,7 +51,7 @@ import scala.collection.JavaConverters._ case class State(objectIndex: Int, valIndex: Int, imports: List[untpd.Import], - run: Run) + context: Context) /** Main REPL instance, orchestrating input, compilation and presentation */ class ReplDriver(settings: Array[String], @@ -72,7 +72,7 @@ class ReplDriver(settings: Array[String], } /** the initial, empty state of the REPL session */ - protected[this] def initState = State(0, 0, Nil, compiler.newRun(rootCtx, 0)) + protected[this] def initState = State(0, 0, Nil, rootCtx) /** Reset state of repl to the initial state * @@ -118,7 +118,7 @@ class ReplDriver(settings: Array[String], val comps = completions(line.cursor, line.line, state) candidates.addAll(comps.asJava) } - implicit val ctx = state.run.runContext + implicit val ctx = state.context try { val line = terminal.readLine(completer) ParseResult(line) @@ -140,7 +140,7 @@ class ReplDriver(settings: Array[String], } final def run(input: String)(implicit state: State): State = withRedirectedOutput { - val parsed = ParseResult(input)(state.run.runContext) + val parsed = ParseResult(input)(state.context) interpret(parsed) } @@ -148,8 +148,8 @@ class ReplDriver(settings: Array[String], Console.withOut(out) { Console.withErr(out) { op } } private def newRun(state: State) = { - val newRun = compiler.newRun(rootCtx.fresh.setReporter(newStoreReporter), state.objectIndex) - state.copy(run = newRun) + val run = compiler.newRun(rootCtx.fresh.setReporter(newStoreReporter), state.objectIndex) + state.copy(context = run.runContext) } /** Extract possible completions at the index of `cursor` in `expr` */ @@ -173,7 +173,7 @@ class ReplDriver(settings: Array[String], val file = new SourceFile("", expr) val unit = new CompilationUnit(file) unit.tpdTree = tree - implicit val ctx = state.run.runContext.fresh.setCompilationUnit(unit) + implicit val ctx = state.context.fresh.setCompilationUnit(unit) val srcPos = SourcePosition(file, Position(cursor)) val (_, completions) = Interactive.completions(srcPos) completions.map(makeCandidate) @@ -224,8 +224,8 @@ class ReplDriver(settings: Array[String], val newImports = newState.imports ++ extractImports(parsed.trees) val newStateWithImports = newState.copy(imports = newImports) - // display warnings - displayErrors(newState.run.runContext.flushBufferedMessages())(newState) + val warnings = newState.context.reporter.removeBufferedMessages(newState.context) + displayErrors(warnings)(newState) // display warnings displayDefinitions(unit.tpdTree, newestWrapper)(newStateWithImports) } ) @@ -233,7 +233,7 @@ class ReplDriver(settings: Array[String], /** Display definitions from `tree` */ private def displayDefinitions(tree: tpd.Tree, newestWrapper: Name)(implicit state: State): State = { - implicit val ctx = state.run.runContext + implicit val ctx = state.context def resAndUnit(denot: Denotation) = { import scala.util.{Success, Try} @@ -319,7 +319,7 @@ class ReplDriver(settings: Array[String], initState case Imports => - state.imports.foreach(i => out.println(SyntaxHighlighting(i.show(state.run.runContext)))) + state.imports.foreach(i => out.println(SyntaxHighlighting(i.show(state.context)))) state case Load(path) => @@ -356,7 +356,7 @@ class ReplDriver(settings: Array[String], /** Output errors to `out` */ private def displayErrors(errs: Seq[MessageContainer])(implicit state: State): State = { - errs.map(renderMessage(_)(state.run.runContext)).foreach(out.println) + errs.map(renderMessage(_)(state.context)).foreach(out.println) state } } diff --git a/compiler/src/dotty/tools/repl/package.scala b/compiler/src/dotty/tools/repl/package.scala index f95334cfb38e..c79535a5c7e5 100644 --- a/compiler/src/dotty/tools/repl/package.scala +++ b/compiler/src/dotty/tools/repl/package.scala @@ -19,12 +19,4 @@ package object repl { text.mkString(ctx.settings.pageWidth.value, ctx.settings.printLines.value) } } - - private[repl] implicit class StoreReporterContext(val ctx: Context) extends AnyVal { - def flushBufferedMessages(): List[MessageContainer] = - ctx.reporter match { - case rep: StoreReporter => rep.removeBufferedMessages(ctx) - case _ => Nil - } - } } From 5af8f551ead1af15f4ef65df6c3fd6e434c7ebf4 Mon Sep 17 00:00:00 2001 From: Allan Renucci Date: Fri, 22 Jun 2018 23:26:01 +0200 Subject: [PATCH 13/18] More cleanup --- .../src/dotty/tools/repl/ReplCompiler.scala | 58 +++++++++---------- .../src/dotty/tools/repl/ReplDriver.scala | 12 ++-- .../dotty/tools/repl/ReplCompilerTests.scala | 9 +-- compiler/test/dotty/tools/repl/ReplTest.scala | 13 ++--- .../dotty/tools/repl/TabcompleteTests.scala | 17 +++--- .../test/dotty/tools/repl/TypeTests.scala | 5 +- 6 files changed, 51 insertions(+), 63 deletions(-) diff --git a/compiler/src/dotty/tools/repl/ReplCompiler.scala b/compiler/src/dotty/tools/repl/ReplCompiler.scala index 0425530a91e7..6082d7c29166 100644 --- a/compiler/src/dotty/tools/repl/ReplCompiler.scala +++ b/compiler/src/dotty/tools/repl/ReplCompiler.scala @@ -1,21 +1,24 @@ -package dotty.tools -package repl - -import dotc.ast.Trees._ -import dotc.ast.{ untpd, tpd } -import dotc.{ Run, CompilationUnit, Compiler } -import dotc.core.Decorators._, dotc.core.Flags._, dotc.core.Phases, Phases.Phase -import dotc.core.Names._, dotc.core.Contexts._, dotc.core.StdNames._ -import dotc.core.Constants.Constant -import dotc.util.SourceFile -import dotc.typer.{ ImportInfo, FrontEnd } -import backend.jvm.GenBCode -import dotc.core.NameOps._ -import dotc.util.Positions._ -import dotc.reporting.diagnostic.messages -import io._ - -import results._ +package dotty.tools.repl + +import dotty.tools.backend.jvm.GenBCode +import dotty.tools.dotc.ast.Trees._ +import dotty.tools.dotc.ast.{tpd, untpd} +import dotty.tools.dotc.core.Contexts._ +import dotty.tools.dotc.core.Decorators._ +import dotty.tools.dotc.core.Flags._ +import dotty.tools.dotc.core.Names._ +import dotty.tools.dotc.core.Phases +import dotty.tools.dotc.core.Phases.Phase +import dotty.tools.dotc.core.StdNames._ +import dotty.tools.dotc.reporting.diagnostic.messages +import dotty.tools.dotc.typer.{FrontEnd, ImportInfo} +import dotty.tools.dotc.util.Positions._ +import dotty.tools.dotc.util.SourceFile +import dotty.tools.dotc.{CompilationUnit, Compiler, Run} +import dotty.tools.io._ +import dotty.tools.repl.results._ + +import scala.collection.mutable /** This subclass of `Compiler` replaces the appropriate phases in order to * facilitate the REPL @@ -59,13 +62,11 @@ class ReplCompiler(val directory: AbstractFile) extends Compiler { } } - private[this] var objectNames = Map.empty[Int, TermName] + private[this] val objectNames = mutable.Map.empty[Int, TermName] private def objectName(state: State) = - objectNames.get(state.objectIndex).getOrElse { - val newName = (str.REPL_SESSION_LINE + state.objectIndex).toTermName - objectNames = objectNames + (state.objectIndex -> newName) - newName - } + objectNames.getOrElseUpdate(state.objectIndex, { + (str.REPL_SESSION_LINE + state.objectIndex).toTermName + }) private case class Definitions(stats: List[untpd.Tree], state: State) @@ -77,7 +78,7 @@ class ReplCompiler(val directory: AbstractFile) extends Compiler { var valIdx = state.valIndex val defs = trees.flatMap { - case expr @ Assign(id: Ident, rhs) => + case expr @ Assign(id: Ident, _) => // special case simple reassignment (e.g. x = 3) // in order to print the new value in the REPL val assignName = (id.name ++ str.REPL_ASSIGN_SUFFIX).toTermName @@ -124,7 +125,7 @@ class ReplCompiler(val directory: AbstractFile) extends Compiler { val tmpl = Template(emptyConstructor, Nil, EmptyValDef, defs.stats) val module = ModuleDef(objectName(defs.state), tmpl) - .withMods(new Modifiers(Module | Final)) + .withMods(Modifiers(Module | Final)) .withPos(Position(0, defs.stats.last.pos.end)) PackageDef(Ident(nme.EMPTY_PACKAGE), List(module)) @@ -152,7 +153,6 @@ class ReplCompiler(val directory: AbstractFile) extends Compiler { final def typeOf(expr: String)(implicit state: State): Result[String] = typeCheck(expr).map { tree => - import dotc.ast.Trees._ implicit val ctx = state.context tree.rhs match { case Block(xs, _) => xs.last.tpe.widen.show @@ -180,13 +180,13 @@ class ReplCompiler(val directory: AbstractFile) extends Compiler { PackageDef(Ident(nme.EMPTY_PACKAGE), TypeDef("EvaluateExpr".toTypeName, tmpl) - .withMods(new Modifiers(Final)) + .withMods(Modifiers(Final)) .withPos(Position(0, expr.length)) :: Nil ) } ParseResult(expr) match { - case Parsed(sourceCode, trees) => + case Parsed(_, trees) => wrap(trees).result case SyntaxErrors(_, reported, trees) => if (errorsAllowed) wrap(trees).result diff --git a/compiler/src/dotty/tools/repl/ReplDriver.scala b/compiler/src/dotty/tools/repl/ReplDriver.scala index b04f94091659..7b73de86dcf9 100644 --- a/compiler/src/dotty/tools/repl/ReplDriver.scala +++ b/compiler/src/dotty/tools/repl/ReplDriver.scala @@ -20,7 +20,7 @@ import dotty.tools.dotc.reporting.MessageRendering import dotty.tools.dotc.reporting.diagnostic.{Message, MessageContainer} import dotty.tools.dotc.util.Positions.Position import dotty.tools.dotc.util.{SourceFile, SourcePosition} -import dotty.tools.dotc.{CompilationUnit, Driver, Run} +import dotty.tools.dotc.{CompilationUnit, Driver} import dotty.tools.io._ import org.jline.reader._ @@ -42,11 +42,9 @@ import scala.collection.JavaConverters._ * `valIndex`. * * @param objectIndex the index of the next wrapper - * @param valIndex the index of next value binding for free expressions - * @param imports a list of tuples of imports on tree form and shown form - * @param run the latest run initiated at the start of interpretation. This - * run and its context should be used in order to perform any - * manipulation on `Tree`s and `Symbol`s. + * @param valIndex the index of next value binding for free expressions + * @param imports the list of user defined imports + * @param context the latest compiler context */ case class State(objectIndex: Int, valIndex: Int, @@ -189,7 +187,7 @@ class ReplDriver(settings: Array[String], case parsed: Parsed if parsed.trees.nonEmpty => compile(parsed, state) - case SyntaxErrors(src, errs, _) => + case SyntaxErrors(_, errs, _) => displayErrors(errs) state diff --git a/compiler/test/dotty/tools/repl/ReplCompilerTests.scala b/compiler/test/dotty/tools/repl/ReplCompilerTests.scala index 17333f6e6e20..0b3d20977717 100644 --- a/compiler/test/dotty/tools/repl/ReplCompilerTests.scala +++ b/compiler/test/dotty/tools/repl/ReplCompilerTests.scala @@ -1,12 +1,7 @@ -package dotty.tools -package repl +package dotty.tools.repl import org.junit.Assert._ -import org.junit.{Test, Ignore} - -import dotc.core.Contexts.Context -import dotc.ast.Trees._ -import dotc.ast.untpd +import org.junit.{Ignore, Test} class ReplCompilerTests extends ReplTest { diff --git a/compiler/test/dotty/tools/repl/ReplTest.scala b/compiler/test/dotty/tools/repl/ReplTest.scala index 3e771390ca74..0baeab61399c 100644 --- a/compiler/test/dotty/tools/repl/ReplTest.scala +++ b/compiler/test/dotty/tools/repl/ReplTest.scala @@ -1,13 +1,10 @@ -package dotty.tools -package repl +package dotty.tools.repl -import dotty.Jars -import java.io.{ PrintStream, ByteArrayOutputStream } -import org.junit.{ Before, After } -import dotc.reporting.MessageRendering -import org.junit.Assert.fail +import java.io.{ByteArrayOutputStream, PrintStream} -import results._ +import dotty.Jars +import dotty.tools.dotc.reporting.MessageRendering +import org.junit.{After, Before} class ReplTest private (out: ByteArrayOutputStream) extends ReplDriver( diff --git a/compiler/test/dotty/tools/repl/TabcompleteTests.scala b/compiler/test/dotty/tools/repl/TabcompleteTests.scala index b96b1759c765..c5c84c7b88bc 100644 --- a/compiler/test/dotty/tools/repl/TabcompleteTests.scala +++ b/compiler/test/dotty/tools/repl/TabcompleteTests.scala @@ -1,5 +1,4 @@ -package dotty.tools -package repl +package dotty.tools.repl import org.junit.Assert._ import org.junit.Test @@ -11,27 +10,27 @@ class TabcompleteTests extends ReplTest { private[this] def tabComplete(src: String)(implicit state: State): List[String] = completions(src.length, src, state).map(_.value) - @Test def tabCompleteList: Unit = fromInitialState { implicit s => + @Test def tabCompleteList = fromInitialState { implicit s => val comp = tabComplete("List.r") assertEquals(List("range"), comp.distinct) } - @Test def tabCompleteListInstance: Unit = fromInitialState { implicit s => + @Test def tabCompleteListInstance = fromInitialState { implicit s => val comp = tabComplete("(null: List[Int]).sli") assertEquals(List("slice", "sliding"), comp.distinct.sorted) } - @Test def tabCompleteModule: Unit = fromInitialState{ implicit s => + @Test def tabCompleteModule = fromInitialState{ implicit s => val comp = tabComplete("scala.Pred") assertEquals(List("Predef"), comp) } - @Test def tabCompleteInClass: Unit = fromInitialState { implicit s => + @Test def tabCompleteInClass = fromInitialState { implicit s => val comp = tabComplete("class Foo { def bar: List[Int] = List.ap") assertEquals(List("apply"), comp) } - @Test def tabCompleteTwiceIn: Unit = { + @Test def tabCompleteTwiceIn = { val src1 = "class Foo { def bar(xs: List[Int]) = xs.map" val src2 = "class Foo { def bar(xs: List[Int]) = xs.mapC" @@ -46,13 +45,13 @@ class TabcompleteTests extends ReplTest { } } - @Test def i3309: Unit = fromInitialState { implicit s => + @Test def i3309 = fromInitialState { implicit s => // We make sure we do not crash List("\"", ")", "'", "¨", "£", ":", ",", ";", "@", "}", "[", "]", ".") .foreach(tabComplete(_)) } - @Test def completeFromPreviousState: Unit = + @Test def completeFromPreviousState = fromInitialState { implicit state => val src = "class Foo { def comp3 = 3; def comp1 = 1; def comp2 = 2 }" run(src) diff --git a/compiler/test/dotty/tools/repl/TypeTests.scala b/compiler/test/dotty/tools/repl/TypeTests.scala index c1be79fc5cc3..cd15ddd34dee 100644 --- a/compiler/test/dotty/tools/repl/TypeTests.scala +++ b/compiler/test/dotty/tools/repl/TypeTests.scala @@ -1,8 +1,7 @@ -package dotty.tools -package repl +package dotty.tools.repl -import org.junit.Test import org.junit.Assert._ +import org.junit.Test class TypeTests extends ReplTest { @Test def typeOf1 = fromInitialState { implicit s => From b055bc448a6dcef4ab15899b6cfa137fed6b5ed5 Mon Sep 17 00:00:00 2001 From: Allan Renucci Date: Fri, 22 Jun 2018 23:48:49 +0200 Subject: [PATCH 14/18] Workaround #4709 --- compiler/src/dotty/tools/repl/ReplCompiler.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/compiler/src/dotty/tools/repl/ReplCompiler.scala b/compiler/src/dotty/tools/repl/ReplCompiler.scala index 6082d7c29166..825799429e4b 100644 --- a/compiler/src/dotty/tools/repl/ReplCompiler.scala +++ b/compiler/src/dotty/tools/repl/ReplCompiler.scala @@ -218,7 +218,7 @@ class ReplCompiler(val directory: AbstractFile) extends Compiler { val src = new SourceFile("", expr) - implicit val ctx = state.context.fresh + implicit val ctx: Context = state.context.fresh .setReporter(newStoreReporter) .setSetting(state.context.settings.YstopAfter, List("frontend")) From e8fb8497fb41f906b2ef71d7442e9a52ae1b654c Mon Sep 17 00:00:00 2001 From: Allan Renucci Date: Sat, 23 Jun 2018 18:32:28 +0200 Subject: [PATCH 15/18] Fix dotr script: add JLine jars to the classpath --- dist/bin/common | 1 + dist/bin/dotc | 6 +++++- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/dist/bin/common b/dist/bin/common index 660b25f220b3..63713a1eaab0 100755 --- a/dist/bin/common +++ b/dist/bin/common @@ -119,6 +119,7 @@ SCALA_ASM=$(find_lib "*scala-asm*") SCALA_LIB=$(find_lib "*scala-library*") SCALA_XML=$(find_lib "*scala-xml*") SBT_INTF=$(find_lib "*compiler-interface*") +JLINE=$(find_lib "*jline*") # debug DEBUG_STR=-agentlib:jdwp=transport=dt_socket,server=y,suspend=y,address=5005 diff --git a/dist/bin/dotc b/dist/bin/dotc index 45f361c08043..01bd783a9e14 100755 --- a/dist/bin/dotc +++ b/dist/bin/dotc @@ -63,7 +63,11 @@ classpathArgs () { toolchain+="$SBT_INTF$PSEP" toolchain+="$DOTTY_INTF$PSEP" toolchain+="$DOTTY_LIB$PSEP" - toolchain+="$DOTTY_COMP" + toolchain+="$DOTTY_COMP$PSEP" + + # JLine is mutiple jars + local all_jline=$(echo "$JLINE" | tr " " "$PSEP") + toolchain+="$all_jline" if [[ -n "$bootcp" ]]; then jvm_cp_args="-Xbootclasspath/a:\"$toolchain\"" From 329d2a4dd5f2b74e2ea904fff667b64ece017a59 Mon Sep 17 00:00:00 2001 From: Allan Renucci Date: Mon, 25 Jun 2018 11:51:16 +0200 Subject: [PATCH 16/18] Tweak JLine configurations --- .../src/dotty/tools/repl/JLineTerminal.scala | 21 ++++++++++++------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/compiler/src/dotty/tools/repl/JLineTerminal.scala b/compiler/src/dotty/tools/repl/JLineTerminal.scala index 7b9f031f9ce3..4d493a4738ee 100644 --- a/compiler/src/dotty/tools/repl/JLineTerminal.scala +++ b/compiler/src/dotty/tools/repl/JLineTerminal.scala @@ -7,7 +7,8 @@ import dotty.tools.dotc.printing.SyntaxHighlighting import dotty.tools.dotc.reporting.Reporter import dotty.tools.dotc.util.SourceFile import org.jline.reader -import org.jline.reader.LineReader.Option +import org.jline.reader.LineReader.Option._ +import org.jline.reader.LineReader._ import org.jline.reader.Parser.ParseContext import org.jline.reader._ import org.jline.reader.impl.history.DefaultHistory @@ -49,9 +50,10 @@ final class JLineTerminal extends java.io.Closeable { .completer(completer) .highlighter(new Highlighter) .parser(new Parser) - .variable(LineReader.SECONDARY_PROMPT_PATTERN, "%M") - .option(Option.INSERT_TAB, true) // at the beginning of the line, insert tab instead of completing - .option(Option.AUTO_FRESH_LINE, true) // if not at start of line before prompt, move to new line + .variable(SECONDARY_PROMPT_PATTERN, "%M") + .variable(LIST_MAX, 400) // ask user when number of completions exceed this limit (default is 100) + .option(INSERT_TAB, true) // at the beginning of the line, insert tab instead of completing + .option(AUTO_FRESH_LINE, true) // if not at start of line before prompt, move to new line .build() lineReader.readLine(prompt) @@ -70,11 +72,14 @@ final class JLineTerminal extends java.io.Closeable { /** Provide multi-line editing support */ private class Parser(implicit ctx: Context) extends reader.Parser { + /** + * @param cursor The cursor position within the line + * @param line The unparsed line + * @param word The current word being completed + * @param wordCursor The cursor position within the current word + */ private class ParsedLine( - val cursor: Int, // The cursor position within the line - val line: String, // The unparsed line - val word: String, // The current word being completed - val wordCursor: Int // The cursor position within the current word + val cursor: Int, val line: String, val word: String, val wordCursor: Int ) extends reader.ParsedLine { // Using dummy values, not sure what they are used for def wordIndex = -1 From 77ea7a7054355d7554b3339521febd798a477fed Mon Sep 17 00:00:00 2001 From: Allan Renucci Date: Mon, 25 Jun 2018 15:57:50 +0200 Subject: [PATCH 17/18] Polishing --- .../src/dotty/tools/repl/JLineTerminal.scala | 49 +++++++++++-------- .../src/dotty/tools/repl/ReplDriver.scala | 2 +- .../test/dotty/tools/repl/ScriptedTests.scala | 1 - 3 files changed, 29 insertions(+), 23 deletions(-) diff --git a/compiler/src/dotty/tools/repl/JLineTerminal.scala b/compiler/src/dotty/tools/repl/JLineTerminal.scala index 4d493a4738ee..93b4c94b1dac 100644 --- a/compiler/src/dotty/tools/repl/JLineTerminal.scala +++ b/compiler/src/dotty/tools/repl/JLineTerminal.scala @@ -7,8 +7,6 @@ import dotty.tools.dotc.printing.SyntaxHighlighting import dotty.tools.dotc.reporting.Reporter import dotty.tools.dotc.util.SourceFile import org.jline.reader -import org.jline.reader.LineReader.Option._ -import org.jline.reader.LineReader._ import org.jline.reader.Parser.ParseContext import org.jline.reader._ import org.jline.reader.impl.history.DefaultHistory @@ -44,6 +42,8 @@ final class JLineTerminal extends java.io.Closeable { def readLine( completer: Completer // provide auto-completions )(implicit ctx: Context): String = { + import LineReader.Option._ + import LineReader._ val lineReader = LineReaderBuilder.builder() .terminal(terminal) .history(history) @@ -89,6 +89,8 @@ final class JLineTerminal extends java.io.Closeable { def parse(line: String, cursor: Int, context: ParseContext): reader.ParsedLine = { def parsedLine(word: String, wordCursor: Int) = new ParsedLine(cursor, line, word, wordCursor) + // Used when no word is being completed + def defaultParsedLine = parsedLine("", 0) def incomplete(): Nothing = throw new EOFError( // Using dummy values, not sure what they are used for @@ -97,6 +99,23 @@ final class JLineTerminal extends java.io.Closeable { /* message = */ "", /* missing = */ newLinePrompt) + case class TokenData(token: Token, start: Int, end: Int) + def currentToken: TokenData /* | Null */ = { + val source = new SourceFile("", line) + val scanner = new Scanner(source)(ctx.fresh.setReporter(Reporter.NoReporter)) + while (scanner.token != EOF) { + val start = scanner.offset + val token = scanner.token + scanner.nextToken() + val end = scanner.lastOffset + + val isCurrentToken = cursor >= start && cursor <= end + if (isCurrentToken) + return TokenData(token, start, end) + } + null + } + context match { case ParseContext.ACCEPT_LINE => // ENTER means SUBMIT when @@ -104,32 +123,20 @@ final class JLineTerminal extends java.io.Closeable { // - and, input line is complete val cursorIsAtEnd = line.indexWhere(!_.isWhitespace, from = cursor) >= 0 if (cursorIsAtEnd || ParseResult.isIncomplete(line)) incomplete() - else parsedLine("", 0) + else defaultParsedLine // using dummy values, resulting parsed line is probably unused case ParseContext.COMPLETE => // Parse to find completions (typically after a Tab). - val source = new SourceFile("", line) - val scanner = new Scanner(source)(ctx.fresh.setReporter(Reporter.NoReporter)) - - // Looking for the current word being completed - // and the cursor position within this word - while (scanner.token != EOF) { - val start = scanner.offset - val token = scanner.token - scanner.nextToken() - val end = scanner.lastOffset - - val isCompletable = - isIdentifier(token) || isKeyword(token) // keywords can start identifiers - def isCurrentWord = cursor >= start && cursor <= end - if (isCompletable && isCurrentWord) { + def isCompletable(token: Token) = isIdentifier(token) || isKeyword(token) + currentToken match { + case TokenData(token, start, end) if isCompletable(token) => val word = line.substring(start, end) val wordCursor = cursor - start - return parsedLine(word, wordCursor) - } + parsedLine(word, wordCursor) + case _ => + defaultParsedLine } - parsedLine("", 0) // no word being completed case _ => incomplete() diff --git a/compiler/src/dotty/tools/repl/ReplDriver.scala b/compiler/src/dotty/tools/repl/ReplDriver.scala index 7b73de86dcf9..278a1203d5bb 100644 --- a/compiler/src/dotty/tools/repl/ReplDriver.scala +++ b/compiler/src/dotty/tools/repl/ReplDriver.scala @@ -64,7 +64,7 @@ class ReplDriver(settings: Array[String], /** Create a fresh and initialized context with IDE mode enabled */ private[this] def initialCtx = { val rootCtx = initCtx.fresh.addMode(Mode.ReadPositions).addMode(Mode.Interactive) - val ictx = setup(settings, rootCtx)._2.fresh + val ictx = setup(settings, rootCtx)._2 ictx.base.initialize()(ictx) ictx } diff --git a/compiler/test/dotty/tools/repl/ScriptedTests.scala b/compiler/test/dotty/tools/repl/ScriptedTests.scala index 1e89a9de2104..d5ac633ffc4c 100644 --- a/compiler/test/dotty/tools/repl/ScriptedTests.scala +++ b/compiler/test/dotty/tools/repl/ScriptedTests.scala @@ -63,7 +63,6 @@ class ScriptedTests extends ReplTest with MessageRendering { Source.fromFile(f).getLines().flatMap(filterEmpties).mkString("\n") val actualOutput = { resetToInitial() - init() val inputRes = extractInputs(prompt) val buf = new ArrayBuffer[String] inputRes.foldLeft(initState) { (state, input) => From 926d42695e53747c93f2db06833ed6ab892e33d2 Mon Sep 17 00:00:00 2001 From: Allan Renucci Date: Fri, 29 Jun 2018 13:51:27 +0200 Subject: [PATCH 18/18] Address review comments --- .../src/dotty/tools/dotc/reporting/Reporter.scala | 12 +++++++----- compiler/src/dotty/tools/repl/JLineTerminal.scala | 3 ++- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/reporting/Reporter.scala b/compiler/src/dotty/tools/dotc/reporting/Reporter.scala index f96dcec8635b..16041eb32340 100644 --- a/compiler/src/dotty/tools/dotc/reporting/Reporter.scala +++ b/compiler/src/dotty/tools/dotc/reporting/Reporter.scala @@ -24,7 +24,7 @@ object Reporter { } } - /** A reporter that ignores reports */ + /** A reporter that ignores reports, and doesn't record errors */ @sharable object NoReporter extends Reporter { def doReport(m: MessageContainer)(implicit ctx: Context) = () override def report(m: MessageContainer)(implicit ctx: Context): Unit = () @@ -163,8 +163,10 @@ abstract class Reporter extends interfaces.ReporterResult { finally incompleteHandler = saved } - var errorCount = 0 - var warningCount = 0 + private[this] var _errorCount = 0 + private[this] var _warningCount = 0 + def errorCount = _errorCount + def warningCount = _warningCount def hasErrors = errorCount > 0 def hasWarnings = warningCount > 0 private[this] var errors: List[Error] = Nil @@ -191,10 +193,10 @@ abstract class Reporter extends interfaces.ReporterResult { doReport(m)(ctx.addMode(Mode.Printing)) m match { case m: ConditionalWarning if !m.enablingOption.value => unreportedWarnings(m.enablingOption.name) += 1 - case m: Warning => warningCount += 1 + case m: Warning => _warningCount += 1 case m: Error => errors = m :: errors - errorCount += 1 + _errorCount += 1 case m: Info => // nothing to do here // match error if d is something else } diff --git a/compiler/src/dotty/tools/repl/JLineTerminal.scala b/compiler/src/dotty/tools/repl/JLineTerminal.scala index 93b4c94b1dac..d73bc295fe78 100644 --- a/compiler/src/dotty/tools/repl/JLineTerminal.scala +++ b/compiler/src/dotty/tools/repl/JLineTerminal.scala @@ -50,7 +50,8 @@ final class JLineTerminal extends java.io.Closeable { .completer(completer) .highlighter(new Highlighter) .parser(new Parser) - .variable(SECONDARY_PROMPT_PATTERN, "%M") + .variable(SECONDARY_PROMPT_PATTERN, "%M") // A short word explaining what is "missing". + // This is supplied from the EOFError.getMissing() method .variable(LIST_MAX, 400) // ask user when number of completions exceed this limit (default is 100) .option(INSERT_TAB, true) // at the beginning of the line, insert tab instead of completing .option(AUTO_FRESH_LINE, true) // if not at start of line before prompt, move to new line