Skip to content

Commit 134ad7a

Browse files
committed
Merge pull request #1233 from felixmulder/topic/repl-syntax-highlighting
Syntax highlighting for REPL using ammonite as base instead of JLine
2 parents 181d7e4 + 1582959 commit 134ad7a

28 files changed

+2431
-80
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
*.class
33
*.log
44
*~
5+
*.swp
56

67
# sbt specific
78
dist/*

project/Build.scala

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,10 @@ object DottyBuild extends Build {
6464
unmanagedSourceDirectories in Compile := Seq((scalaSource in Compile).value),
6565
unmanagedSourceDirectories in Test := Seq((scalaSource in Test).value),
6666

67+
// set system in/out for repl
68+
connectInput in run := true,
69+
outputStrategy := Some(StdoutOutput),
70+
6771
// Generate compiler.properties, used by sbt
6872
resourceGenerators in Compile += Def.task {
6973
val file = (resourceManaged in Compile).value / "compiler.properties"
Lines changed: 262 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,262 @@
1+
package dotty.tools
2+
package dotc
3+
package printing
4+
5+
import parsing.Tokens._
6+
import scala.annotation.switch
7+
import scala.collection.mutable.StringBuilder
8+
9+
/** This object provides functions for syntax highlighting in the REPL */
10+
object SyntaxHighlighting {
11+
val NoColor = Console.RESET
12+
val CommentColor = Console.GREEN
13+
val KeywordColor = Console.CYAN
14+
val LiteralColor = Console.MAGENTA
15+
val TypeColor = Console.GREEN
16+
val AnnotationColor = Console.RED
17+
18+
private def none(str: String) = str
19+
private def keyword(str: String) = KeywordColor + str + NoColor
20+
private def typeDef(str: String) = TypeColor + str + NoColor
21+
private def literal(str: String) = LiteralColor + str + NoColor
22+
private def annotation(str: String) = AnnotationColor + str + NoColor
23+
24+
private val keywords: Seq[String] = for {
25+
index <- IF to FORSOME // All alpha keywords
26+
} yield tokenString(index)
27+
28+
private val interpolationPrefixes =
29+
'A' :: 'B' :: 'C' :: 'D' :: 'E' :: 'F' :: 'G' :: 'H' :: 'I' :: 'J' :: 'K' ::
30+
'L' :: 'M' :: 'N' :: 'O' :: 'P' :: 'Q' :: 'R' :: 'S' :: 'T' :: 'U' :: 'V' ::
31+
'W' :: 'X' :: 'Y' :: 'Z' :: '$' :: '_' :: 'a' :: 'b' :: 'c' :: 'd' :: 'e' ::
32+
'f' :: 'g' :: 'h' :: 'i' :: 'j' :: 'k' :: 'l' :: 'm' :: 'n' :: 'o' :: 'p' ::
33+
'q' :: 'r' :: 's' :: 't' :: 'u' :: 'v' :: 'w' :: 'x' :: 'y' :: 'z' :: Nil
34+
35+
private val typeEnders =
36+
'{' :: '}' :: ')' :: '(' :: '=' :: ' ' :: ',' :: '.' :: '\n' :: Nil
37+
38+
def apply(chars: Iterable[Char]): Vector[Char] = {
39+
var prev: Char = 0
40+
var remaining = chars.toStream
41+
val newBuf = new StringBuilder
42+
43+
@inline def keywordStart =
44+
prev == 0 || prev == ' ' || prev == '{' || prev == '(' || prev == '\n'
45+
46+
@inline def numberStart(c: Char) =
47+
c.isDigit && (!prev.isLetter || prev == '.' || prev == ' ' || prev == '(' || prev == '\u0000')
48+
49+
def takeChar(): Char = takeChars(1).head
50+
def takeChars(x: Int): Seq[Char] = {
51+
val taken = remaining.take(x)
52+
remaining = remaining.drop(x)
53+
taken
54+
}
55+
56+
while (remaining.nonEmpty) {
57+
val n = takeChar()
58+
if (interpolationPrefixes.contains(n)) {
59+
// Interpolation prefixes are a superset of the keyword start chars
60+
val next = remaining.take(3).mkString
61+
if (next.startsWith("\"")) {
62+
newBuf += n
63+
prev = n
64+
if (remaining.nonEmpty) takeChar() // drop 1 for appendLiteral
65+
appendLiteral('"', next == "\"\"\"")
66+
} else {
67+
if (n.isUpper && keywordStart) {
68+
appendWhile(n, !typeEnders.contains(_), typeDef)
69+
} else if (keywordStart) {
70+
append(n, keywords.contains(_), keyword)
71+
} else {
72+
newBuf += n
73+
prev = n
74+
}
75+
}
76+
} else {
77+
(n: @switch) match {
78+
case '/' =>
79+
if (remaining.nonEmpty) {
80+
takeChar() match {
81+
case '/' => eolComment()
82+
case '*' => blockComment()
83+
case x => newBuf += '/'; remaining = x #:: remaining
84+
}
85+
} else newBuf += '/'
86+
case '=' =>
87+
append('=', _ == "=>", keyword)
88+
case '<' =>
89+
append('<', { x => x == "<-" || x == "<:" || x == "<%" }, keyword)
90+
case '>' =>
91+
append('>', { x => x == ">:" }, keyword)
92+
case '#' if prev != ' ' && prev != '.' =>
93+
newBuf append keyword("#")
94+
prev = '#'
95+
case '@' =>
96+
appendWhile('@', _ != ' ', annotation)
97+
case '\"' =>
98+
appendLiteral('\"', multiline = remaining.take(2).mkString == "\"\"")
99+
case '\'' =>
100+
appendLiteral('\'')
101+
case '`' =>
102+
appendTo('`', _ == '`', none)
103+
case c if c.isUpper && keywordStart =>
104+
appendWhile(c, !typeEnders.contains(_), typeDef)
105+
case c if numberStart(c) =>
106+
appendWhile(c, { x => x.isDigit || x == '.' || x == '\u0000'}, literal)
107+
case c =>
108+
newBuf += c; prev = c
109+
}
110+
}
111+
}
112+
113+
def eolComment() = {
114+
newBuf append (CommentColor + "//")
115+
var curr = '/'
116+
while (curr != '\n' && remaining.nonEmpty) {
117+
curr = takeChar()
118+
newBuf += curr
119+
}
120+
prev = curr
121+
newBuf append NoColor
122+
}
123+
124+
def blockComment() = {
125+
newBuf append (CommentColor + "/*")
126+
var curr = '*'
127+
var open = 1
128+
while (open > 0 && remaining.nonEmpty) {
129+
curr = takeChar()
130+
newBuf += curr
131+
132+
if (curr == '*' && remaining.nonEmpty) {
133+
curr = takeChar()
134+
newBuf += curr
135+
if (curr == '/') open -= 1
136+
} else if (curr == '/' && remaining.nonEmpty) {
137+
curr = takeChar()
138+
newBuf += curr
139+
if (curr == '*') open += 1
140+
}
141+
}
142+
prev = curr
143+
newBuf append NoColor
144+
}
145+
146+
def appendLiteral(delim: Char, multiline: Boolean = false) = {
147+
var curr: Char = 0
148+
var continue = true
149+
var closing = 0
150+
val inInterpolation = interpolationPrefixes.contains(prev)
151+
newBuf append (LiteralColor + delim)
152+
153+
def shouldInterpolate =
154+
inInterpolation && curr == '$' && prev != '$' && remaining.nonEmpty
155+
156+
def interpolate() = {
157+
val next = takeChar()
158+
if (next == '$') {
159+
newBuf += curr
160+
newBuf += next
161+
prev = '$'
162+
} else if (next == '{') {
163+
var open = 1 // keep track of open blocks
164+
newBuf append (KeywordColor + curr)
165+
newBuf += next
166+
while (remaining.nonEmpty && open > 0) {
167+
var c = takeChar()
168+
newBuf += c
169+
if (c == '}') open -= 1
170+
else if (c == '{') open += 1
171+
}
172+
newBuf append LiteralColor
173+
} else {
174+
newBuf append (KeywordColor + curr)
175+
newBuf += next
176+
var c: Char = 'a'
177+
while (c.isLetterOrDigit && remaining.nonEmpty) {
178+
c = takeChar()
179+
if (c != '"') newBuf += c
180+
}
181+
newBuf append LiteralColor
182+
if (c == '"') {
183+
newBuf += c
184+
continue = false
185+
}
186+
}
187+
closing = 0
188+
}
189+
190+
while (continue && remaining.nonEmpty) {
191+
curr = takeChar()
192+
if (curr == '\\' && remaining.nonEmpty) {
193+
val next = takeChar()
194+
newBuf append (KeywordColor + curr)
195+
if (next == 'u') {
196+
val code = "u" + takeChars(4).mkString
197+
newBuf append code
198+
} else newBuf += next
199+
newBuf append LiteralColor
200+
closing = 0
201+
} else if (shouldInterpolate) {
202+
interpolate()
203+
} else if (curr == delim && multiline) {
204+
closing += 1
205+
if (closing == 3) continue = false
206+
newBuf += curr
207+
} else if (curr == delim) {
208+
continue = false
209+
newBuf += curr
210+
} else {
211+
newBuf += curr
212+
closing = 0
213+
}
214+
}
215+
newBuf append NoColor
216+
prev = curr
217+
}
218+
219+
def append(c: Char, shouldHL: String => Boolean, highlight: String => String) = {
220+
var curr: Char = 0
221+
val sb = new StringBuilder(s"$c")
222+
while (remaining.nonEmpty && curr != ' ' && curr != '(' && curr != '\n') {
223+
curr = takeChar()
224+
if (curr != ' ' && curr != '\n') sb += curr
225+
}
226+
227+
val str = sb.toString
228+
val toAdd = if (shouldHL(str)) highlight(str) else str
229+
val suffix = if (curr == ' ' || curr == '\n') s"$curr" else ""
230+
newBuf append (toAdd + suffix)
231+
prev = curr
232+
}
233+
234+
def appendWhile(c: Char, pred: Char => Boolean, highlight: String => String) = {
235+
var curr: Char = 0
236+
val sb = new StringBuilder(s"$c")
237+
while (remaining.nonEmpty && pred(curr)) {
238+
curr = takeChar()
239+
if (pred(curr)) sb += curr
240+
}
241+
242+
val str = sb.toString
243+
val suffix = if (!pred(curr)) s"$curr" else ""
244+
newBuf append (highlight(str) + suffix)
245+
prev = curr
246+
}
247+
248+
def appendTo(c: Char, pred: Char => Boolean, highlight: String => String) = {
249+
var curr: Char = 0
250+
val sb = new StringBuilder(s"$c")
251+
while (remaining.nonEmpty && !pred(curr)) {
252+
curr = takeChar()
253+
sb += curr
254+
}
255+
256+
newBuf append highlight(sb.toString)
257+
prev = curr
258+
}
259+
260+
newBuf.toVector
261+
}
262+
}
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
package dotty.tools
2+
package dotc
3+
package repl
4+
5+
import core.Contexts._
6+
import ammonite.terminal._
7+
import LazyList._
8+
import Ansi.Color
9+
import filters._
10+
import BasicFilters._
11+
import GUILikeFilters._
12+
import util.SourceFile
13+
import printing.SyntaxHighlighting
14+
15+
class AmmoniteReader(val interpreter: Interpreter)(implicit ctx: Context) extends InteractiveReader {
16+
val interactive = true
17+
18+
def incompleteInput(str: String): Boolean =
19+
interpreter.delayOutputDuring(interpreter.interpret(str)) match {
20+
case Interpreter.Incomplete => true
21+
case _ => false
22+
}
23+
24+
val reader = new java.io.InputStreamReader(System.in)
25+
val writer = new java.io.OutputStreamWriter(System.out)
26+
val cutPasteFilter = ReadlineFilters.CutPasteFilter()
27+
var history = List.empty[String]
28+
val selectionFilter = GUILikeFilters.SelectionFilter(indent = 2)
29+
val multilineFilter: Filter = Filter("multilineFilter") {
30+
case TermState(lb ~: rest, b, c, _)
31+
if (lb == 10 || lb == 13) && incompleteInput(b.mkString) =>
32+
BasicFilters.injectNewLine(b, c, rest)
33+
}
34+
35+
def readLine(prompt: String): String = {
36+
val historyFilter = new HistoryFilter(
37+
() => history.toVector,
38+
Console.BLUE,
39+
AnsiNav.resetForegroundColor
40+
)
41+
42+
val allFilters = Filter.merge(
43+
UndoFilter(),
44+
historyFilter,
45+
selectionFilter,
46+
GUILikeFilters.altFilter,
47+
GUILikeFilters.fnFilter,
48+
ReadlineFilters.navFilter,
49+
cutPasteFilter,
50+
multilineFilter,
51+
BasicFilters.all
52+
)
53+
54+
Terminal.readLine(
55+
Console.BLUE + prompt + Console.RESET,
56+
reader,
57+
writer,
58+
allFilters,
59+
displayTransform = (buffer, cursor) => {
60+
val ansiBuffer = Ansi.Str.parse(SyntaxHighlighting(buffer))
61+
val (newBuffer, cursorOffset) = SelectionFilter.mangleBuffer(
62+
selectionFilter, ansiBuffer, cursor, Ansi.Reversed.On
63+
)
64+
val newNewBuffer = HistoryFilter.mangleBuffer(
65+
historyFilter, newBuffer, cursor,
66+
Ansi.Color.Green
67+
)
68+
69+
(newNewBuffer, cursorOffset)
70+
}
71+
) match {
72+
case Some(res) =>
73+
history = res :: history;
74+
res
75+
case None => ":q"
76+
}
77+
}
78+
}

0 commit comments

Comments
 (0)