Skip to content

Commit bcce5c6

Browse files
committed
Use JLine 3 in the REPL
1 parent 76b110b commit bcce5c6

23 files changed

+255
-2442
lines changed

compiler/src/dotty/tools/dotc/interactive/Interactive.scala

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -219,9 +219,9 @@ object Interactive {
219219
case _ => getScopeCompletions(ctx)
220220
}
221221

222-
val sortedCompletions = completions.toList.sortBy(_.name: Name)
223-
interactiv.println(i"completion with pos = $pos, prefix = $prefix, termOnly = $termOnly, typeOnly = $typeOnly = $sortedCompletions%, %")
224-
(completionPos, sortedCompletions)
222+
val completionList = completions.toList
223+
interactiv.println(i"completion with pos = $pos, prefix = $prefix, termOnly = $termOnly, typeOnly = $typeOnly = $completionList%, %")
224+
(completionPos, completionList)
225225
}
226226

227227
/** Possible completions of members of `prefix` which are accessible when called inside `boundary` */

compiler/src/dotty/tools/dotc/reporting/Reporter.scala

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,12 @@ object Reporter {
2323
simple.report(m)
2424
}
2525
}
26+
27+
/** A reporter that ignores reports */
28+
object NoReporter extends Reporter {
29+
def doReport(m: MessageContainer)(implicit ctx: Context) = ()
30+
override def report(m: MessageContainer)(implicit ctx: Context): Unit = ()
31+
}
2632
}
2733

2834
import Reporter._

compiler/src/dotty/tools/repl/AmmoniteReader.scala

Lines changed: 0 additions & 142 deletions
This file was deleted.
Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
package dotty.tools.repl
2+
3+
import dotty.tools.dotc.core.Contexts.Context
4+
import dotty.tools.dotc.parsing.Scanners.Scanner
5+
import dotty.tools.dotc.parsing.Tokens._
6+
import dotty.tools.dotc.printing.SyntaxHighlighting
7+
import dotty.tools.dotc.reporting.Reporter
8+
import dotty.tools.dotc.util.SourceFile
9+
import org.jline.reader
10+
import org.jline.reader.LineReader.Option
11+
import org.jline.reader.Parser.ParseContext
12+
import org.jline.reader._
13+
import org.jline.reader.impl.history.DefaultHistory
14+
import org.jline.terminal.TerminalBuilder
15+
import org.jline.utils.AttributedString
16+
17+
final class JLineTerminal {
18+
private val terminal = TerminalBuilder.terminal()
19+
private val history = new DefaultHistory
20+
21+
private def blue(str: String) = Console.BLUE + str + Console.RESET
22+
private val prompt = blue("scala> ")
23+
private val newLinePrompt = blue(" | ")
24+
25+
/** Blockingly read line from `System.in`
26+
*
27+
* This entry point into JLine handles everything to do with terminal
28+
* emulation. This includes:
29+
*
30+
* - Multi-line support
31+
* - Copy-pasting
32+
* - History
33+
* - Syntax highlighting
34+
* - Auto-completions
35+
*
36+
* @throws EndOfFileException This exception is thrown when the user types Ctrl-D.
37+
*/
38+
def readLine(
39+
completer: Completer // provide auto-completions
40+
)(implicit ctx: Context): String = {
41+
val lineReader = LineReaderBuilder.builder()
42+
.terminal(terminal)
43+
.history(history)
44+
.completer(completer)
45+
.highlighter(new Highlighter)
46+
.parser(new Parser)
47+
.variable(LineReader.SECONDARY_PROMPT_PATTERN, "%M")
48+
.option(Option.INSERT_TAB, true) // at the beginning of the line, insert tab instead of completing
49+
.option(Option.AUTO_FRESH_LINE, true) // if not at start of line before prompt, move to new line
50+
.build()
51+
52+
lineReader.readLine(prompt)
53+
}
54+
55+
/** Provide syntax highlighting */
56+
private class Highlighter extends reader.Highlighter {
57+
def highlight(reader: LineReader, buffer: String): AttributedString = {
58+
val highlighted = SyntaxHighlighting(buffer).mkString
59+
AttributedString.fromAnsi(highlighted)
60+
}
61+
}
62+
63+
/** Provide multi-line editing support */
64+
private class Parser(implicit ctx: Context) extends reader.Parser {
65+
66+
private class ParsedLine(
67+
val cursor: Int, // The cursor position within the line
68+
val line: String, // The unparsed line
69+
val word: String, // The current word being completed
70+
val wordCursor: Int // The cursor position within the current word
71+
) extends reader.ParsedLine {
72+
// Using dummy values, not sure what they are used for
73+
def wordIndex = -1
74+
def words = java.util.Collections.emptyList[String]
75+
}
76+
77+
def parse(line: String, cursor: Int, context: ParseContext): reader.ParsedLine = {
78+
def parsedLine(word: String, wordCursor: Int) =
79+
new ParsedLine(cursor, line, word, wordCursor)
80+
81+
def incomplete(): Nothing = throw new EOFError(
82+
// Using dummy values, not sure what they are used for
83+
/* line = */ -1,
84+
/* column = */ -1,
85+
/* message = */ "",
86+
/* missing = */ newLinePrompt)
87+
88+
context match {
89+
case ParseContext.ACCEPT_LINE =>
90+
// TODO: take into account cursor position
91+
if (ParseResult.isIncomplete(line)) incomplete()
92+
else parsedLine("", 0)
93+
// using dummy values,
94+
// resulting parsed line is probably unused
95+
96+
case ParseContext.COMPLETE =>
97+
// Parse to find completions (typically after a Tab).
98+
val source = new SourceFile("<completions>", line.toCharArray)
99+
val scanner = new Scanner(source)(ctx.fresh.setReporter(Reporter.NoReporter))
100+
101+
// Looking for the current word being completed
102+
// and the cursor position within this word
103+
while (scanner.token != EOF) {
104+
val start = scanner.offset
105+
val token = scanner.token
106+
scanner.nextToken()
107+
val end = scanner.lastOffset
108+
109+
val isCompletable =
110+
isIdentifier(token) || isKeyword(token) // keywords can start identifiers
111+
def isCurrentWord = cursor >= start && cursor <= end
112+
if (isCompletable && isCurrentWord) {
113+
val word = line.substring(start, end)
114+
val wordCursor = cursor - start
115+
return parsedLine(word, wordCursor)
116+
}
117+
}
118+
parsedLine("", 0) // no word being completed
119+
120+
case _ =>
121+
incomplete()
122+
}
123+
}
124+
}
125+
}

compiler/src/dotty/tools/repl/ParseResult.scala

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -141,7 +141,7 @@ object ParseResult {
141141
sourceCode match {
142142
case CommandExtract(_) | "" => false
143143
case _ => {
144-
val reporter = storeReporter
144+
val reporter = newStoreReporter
145145
var needsMore = false
146146
reporter.withIncompleteHandler(_ => _ => needsMore = true) {
147147
parseStats(sourceCode)(ctx.fresh.setReporter(reporter))

compiler/src/dotty/tools/repl/ReplCompiler.scala

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ class ReplCompiler(val directory: AbstractFile) extends Compiler {
4242

4343
def newRun(initCtx: Context, objectIndex: Int) = new Run(this, initCtx) {
4444
override protected[this] def rootContext(implicit ctx: Context) =
45-
addMagicImports(super.rootContext.fresh.setReporter(storeReporter))
45+
addMagicImports(super.rootContext.fresh.setReporter(newStoreReporter))
4646

4747
private def addMagicImports(initCtx: Context): Context = {
4848
def addImport(path: TermName)(implicit ctx: Context) = {
@@ -228,7 +228,7 @@ class ReplCompiler(val directory: AbstractFile) extends Compiler {
228228
wrapped(expr, src, state)(runCtx).flatMap { pkg =>
229229
val unit = new CompilationUnit(src)
230230
unit.untpdTree = pkg
231-
run.compileUnits(unit :: Nil, runCtx.fresh.setReporter(storeReporter))
231+
run.compileUnits(unit :: Nil, runCtx.fresh.setReporter(newStoreReporter))
232232

233233
if (errorsAllowed || !reporter.hasErrors)
234234
unwrapped(unit.tpdTree, src)(runCtx)

0 commit comments

Comments
 (0)