Skip to content

Use JLine 3 in the REPL #4680

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 18 commits into from
Jun 29, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion compiler/src/dotty/tools/dotc/CompilationUnit.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand Down
6 changes: 3 additions & 3 deletions compiler/src/dotty/tools/dotc/interactive/Interactive.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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` */
Expand Down
4 changes: 2 additions & 2 deletions compiler/src/dotty/tools/dotc/quoted/QuoteCompiler.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
}
Expand Down
16 changes: 12 additions & 4 deletions compiler/src/dotty/tools/dotc/reporting/Reporter.scala
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,12 @@ object Reporter {
simple.report(m)
}
}

/** 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 = ()
}
}

import Reporter._
Expand Down Expand Up @@ -157,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
Expand All @@ -185,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
}
Expand Down
7 changes: 3 additions & 4 deletions compiler/src/dotty/tools/dotc/util/SourceFile.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -150,7 +149,7 @@ case class SourceFile(file: AbstractFile, content: Array[Char]) extends interfac
override def toString = file.toString
}

@sharable object NoSource extends SourceFile("<no source>", Nil) {
@sharable object NoSource extends SourceFile("<no source>", "") {
override def exists = false
override def atPos(pos: Position): SourcePosition = NoSourcePosition
}
Expand Down
142 changes: 0 additions & 142 deletions compiler/src/dotty/tools/repl/AmmoniteReader.scala

This file was deleted.

147 changes: 147 additions & 0 deletions compiler/src/dotty/tools/repl/JLineTerminal.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
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.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 extends java.io.Closeable {
// import java.util.logging.{Logger, Level}
// Logger.getLogger("org.jline").setLevel(Level.FINEST)

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
private val prompt = blue("scala> ")
private val newLinePrompt = blue(" | ")

/** Blockingly read line from `System.in`
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we take an InputStream instead of hardcoding System.in ? For tests we shouldn't have to use System.in.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

JlineLine doesn't seem to let you configure the input stream and we should probably not modify System.in. We don't do end to end testing for the REPL, the tests don't rely on JLine

*
* 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 = {
import LineReader.Option._
import LineReader._
val lineReader = LineReaderBuilder.builder()
.terminal(terminal)
.history(history)
.completer(completer)
.highlighter(new Highlighter)
.parser(new Parser)
.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
.build()

lineReader.readLine(prompt)
}

def close() = terminal.close()

/** 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 {

/**
* @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, 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
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)
// 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
/* line = */ -1,
/* column = */ -1,
/* message = */ "",
/* missing = */ newLinePrompt)

case class TokenData(token: Token, start: Int, end: Int)
def currentToken: TokenData /* | Null */ = {
val source = new SourceFile("<completions>", 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
// - 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 defaultParsedLine
// using dummy values, resulting parsed line is probably unused

case ParseContext.COMPLETE =>
// Parse to find completions (typically after a Tab).
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
parsedLine(word, wordCursor)
case _ =>
defaultParsedLine
}

case _ =>
incomplete()
}
}
}
}
Loading