Skip to content
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

Syntax highlighting for REPL using ammonite as base instead of JLine #1233

Merged
merged 13 commits into from May 10, 2016
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.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Expand Up @@ -2,6 +2,7 @@
*.class
*.log
*~
*.swp

# sbt specific
dist/*
Expand Down
4 changes: 4 additions & 0 deletions project/Build.scala
Expand Up @@ -64,6 +64,10 @@ object DottyBuild extends Build {
unmanagedSourceDirectories in Compile := Seq((scalaSource in Compile).value),
unmanagedSourceDirectories in Test := Seq((scalaSource in Test).value),

// set system in/out for repl
connectInput in run := true,
outputStrategy := Some(StdoutOutput),

// Generate compiler.properties, used by sbt
resourceGenerators in Compile += Def.task {
val file = (resourceManaged in Compile).value / "compiler.properties"
Expand Down
262 changes: 262 additions & 0 deletions src/dotty/tools/dotc/printing/SyntaxHighlighting.scala
@@ -0,0 +1,262 @@
package dotty.tools
package dotc
package printing

import parsing.Tokens._
import scala.annotation.switch
import scala.collection.mutable.StringBuilder

/** This object provides functions for syntax highlighting in the REPL */
object SyntaxHighlighting {
val NoColor = Console.RESET
val CommentColor = Console.GREEN
val KeywordColor = Console.CYAN
val LiteralColor = Console.MAGENTA
val TypeColor = Console.GREEN
val AnnotationColor = Console.RED

private def none(str: String) = str
private def keyword(str: String) = KeywordColor + str + NoColor
private def typeDef(str: String) = TypeColor + str + NoColor
private def literal(str: String) = LiteralColor + str + NoColor
private def annotation(str: String) = AnnotationColor + str + NoColor

private val keywords: Seq[String] = for {
index <- IF to FORSOME // All alpha keywords
} yield tokenString(index)

private val interpolationPrefixes =
'A' :: 'B' :: 'C' :: 'D' :: 'E' :: 'F' :: 'G' :: 'H' :: 'I' :: 'J' :: 'K' ::
'L' :: 'M' :: 'N' :: 'O' :: 'P' :: 'Q' :: 'R' :: 'S' :: 'T' :: 'U' :: 'V' ::
'W' :: 'X' :: 'Y' :: 'Z' :: '$' :: '_' :: 'a' :: 'b' :: 'c' :: 'd' :: 'e' ::
'f' :: 'g' :: 'h' :: 'i' :: 'j' :: 'k' :: 'l' :: 'm' :: 'n' :: 'o' :: 'p' ::
'q' :: 'r' :: 's' :: 't' :: 'u' :: 'v' :: 'w' :: 'x' :: 'y' :: 'z' :: Nil

private val typeEnders =
'{' :: '}' :: ')' :: '(' :: '=' :: ' ' :: ',' :: '.' :: '\n' :: Nil

def apply(chars: Iterable[Char]): Vector[Char] = {
var prev: Char = 0
var remaining = chars.toStream
val newBuf = new StringBuilder

@inline def keywordStart =
prev == 0 || prev == ' ' || prev == '{' || prev == '(' || prev == '\n'

@inline def numberStart(c: Char) =
c.isDigit && (!prev.isLetter || prev == '.' || prev == ' ' || prev == '(' || prev == '\u0000')

def takeChar(): Char = takeChars(1).head
def takeChars(x: Int): Seq[Char] = {
val taken = remaining.take(x)
remaining = remaining.drop(x)
taken
}

while (remaining.nonEmpty) {
val n = takeChar()
if (interpolationPrefixes.contains(n)) {
// Interpolation prefixes are a superset of the keyword start chars
val next = remaining.take(3).mkString
if (next.startsWith("\"")) {
newBuf += n
prev = n
if (remaining.nonEmpty) takeChar() // drop 1 for appendLiteral
appendLiteral('"', next == "\"\"\"")
} else {
if (n.isUpper && keywordStart) {
appendWhile(n, !typeEnders.contains(_), typeDef)
} else if (keywordStart) {
append(n, keywords.contains(_), keyword)
} else {
newBuf += n
prev = n
}
}
} else {
(n: @switch) match {
case '/' =>
if (remaining.nonEmpty) {
takeChar() match {
case '/' => eolComment()
case '*' => blockComment()
case x => newBuf += '/'; remaining = x #:: remaining
}
} else newBuf += '/'
case '=' =>
append('=', _ == "=>", keyword)
case '<' =>
append('<', { x => x == "<-" || x == "<:" || x == "<%" }, keyword)
case '>' =>
append('>', { x => x == ">:" }, keyword)
case '#' if prev != ' ' && prev != '.' =>
newBuf append keyword("#")
prev = '#'
case '@' =>
appendWhile('@', _ != ' ', annotation)
case '\"' =>
appendLiteral('\"', multiline = remaining.take(2).mkString == "\"\"")
case '\'' =>
appendLiteral('\'')
case '`' =>
appendTo('`', _ == '`', none)
case c if c.isUpper && keywordStart =>
appendWhile(c, !typeEnders.contains(_), typeDef)
case c if numberStart(c) =>
appendWhile(c, { x => x.isDigit || x == '.' || x == '\u0000'}, literal)
case c =>
newBuf += c; prev = c
}
}
}

def eolComment() = {
newBuf append (CommentColor + "//")
var curr = '/'
while (curr != '\n' && remaining.nonEmpty) {
curr = takeChar()
newBuf += curr
}
prev = curr
newBuf append NoColor
}

def blockComment() = {
newBuf append (CommentColor + "/*")
var curr = '*'
var open = 1
while (open > 0 && remaining.nonEmpty) {
curr = takeChar()
newBuf += curr

if (curr == '*' && remaining.nonEmpty) {
curr = takeChar()
newBuf += curr
if (curr == '/') open -= 1
} else if (curr == '/' && remaining.nonEmpty) {
curr = takeChar()
newBuf += curr
if (curr == '*') open += 1
}
}
prev = curr
newBuf append NoColor
}

def appendLiteral(delim: Char, multiline: Boolean = false) = {
var curr: Char = 0
var continue = true
var closing = 0
val inInterpolation = interpolationPrefixes.contains(prev)
newBuf append (LiteralColor + delim)

def shouldInterpolate =
inInterpolation && curr == '$' && prev != '$' && remaining.nonEmpty

def interpolate() = {
val next = takeChar()
if (next == '$') {
newBuf += curr
newBuf += next
prev = '$'
} else if (next == '{') {
var open = 1 // keep track of open blocks
newBuf append (KeywordColor + curr)
newBuf += next
while (remaining.nonEmpty && open > 0) {
var c = takeChar()
newBuf += c
if (c == '}') open -= 1
else if (c == '{') open += 1
}
newBuf append LiteralColor
} else {
newBuf append (KeywordColor + curr)
newBuf += next
var c: Char = 'a'
while (c.isLetterOrDigit && remaining.nonEmpty) {
c = takeChar()
if (c != '"') newBuf += c
}
newBuf append LiteralColor
if (c == '"') {
newBuf += c
continue = false
}
}
closing = 0
}

while (continue && remaining.nonEmpty) {
curr = takeChar()
if (curr == '\\' && remaining.nonEmpty) {
val next = takeChar()
newBuf append (KeywordColor + curr)
if (next == 'u') {
val code = "u" + takeChars(4).mkString
newBuf append code
} else newBuf += next
newBuf append LiteralColor
closing = 0
} else if (shouldInterpolate) {
interpolate()
} else if (curr == delim && multiline) {
closing += 1
if (closing == 3) continue = false
newBuf += curr
} else if (curr == delim) {
continue = false
newBuf += curr
} else {
newBuf += curr
closing = 0
}
}
newBuf append NoColor
prev = curr
}

def append(c: Char, shouldHL: String => Boolean, highlight: String => String) = {
var curr: Char = 0
val sb = new StringBuilder(s"$c")
while (remaining.nonEmpty && curr != ' ' && curr != '(' && curr != '\n') {
curr = takeChar()
if (curr != ' ' && curr != '\n') sb += curr
}

val str = sb.toString
val toAdd = if (shouldHL(str)) highlight(str) else str
val suffix = if (curr == ' ' || curr == '\n') s"$curr" else ""
newBuf append (toAdd + suffix)
prev = curr
}

def appendWhile(c: Char, pred: Char => Boolean, highlight: String => String) = {
var curr: Char = 0
val sb = new StringBuilder(s"$c")
while (remaining.nonEmpty && pred(curr)) {
curr = takeChar()
if (pred(curr)) sb += curr
}

val str = sb.toString
val suffix = if (!pred(curr)) s"$curr" else ""
newBuf append (highlight(str) + suffix)
prev = curr
}

def appendTo(c: Char, pred: Char => Boolean, highlight: String => String) = {
var curr: Char = 0
val sb = new StringBuilder(s"$c")
while (remaining.nonEmpty && !pred(curr)) {
curr = takeChar()
sb += curr
}

newBuf append highlight(sb.toString)
prev = curr
}

newBuf.toVector
}
}
78 changes: 78 additions & 0 deletions src/dotty/tools/dotc/repl/AmmoniteReader.scala
@@ -0,0 +1,78 @@
package dotty.tools
package dotc
package repl

import core.Contexts._
import ammonite.terminal._
import LazyList._
import Ansi.Color
import filters._
import BasicFilters._
import GUILikeFilters._
import util.SourceFile
import printing.SyntaxHighlighting

class AmmoniteReader(val interpreter: Interpreter)(implicit ctx: Context) extends InteractiveReader {
val interactive = true

def incompleteInput(str: String): Boolean =
interpreter.delayOutputDuring(interpreter.interpret(str)) match {
case Interpreter.Incomplete => true
case _ => false
}

val reader = new java.io.InputStreamReader(System.in)
val writer = new java.io.OutputStreamWriter(System.out)
val cutPasteFilter = ReadlineFilters.CutPasteFilter()
var history = List.empty[String]
val selectionFilter = GUILikeFilters.SelectionFilter(indent = 2)
val multilineFilter: Filter = Filter("multilineFilter") {
case TermState(lb ~: rest, b, c, _)
if (lb == 10 || lb == 13) && incompleteInput(b.mkString) =>
BasicFilters.injectNewLine(b, c, rest)
}

def readLine(prompt: String): String = {
val historyFilter = new HistoryFilter(
() => history.toVector,
Console.BLUE,
AnsiNav.resetForegroundColor
)

val allFilters = Filter.merge(
UndoFilter(),
historyFilter,
selectionFilter,
GUILikeFilters.altFilter,
GUILikeFilters.fnFilter,
ReadlineFilters.navFilter,
cutPasteFilter,
multilineFilter,
BasicFilters.all
)

Terminal.readLine(
Console.BLUE + prompt + Console.RESET,
reader,
writer,
allFilters,
displayTransform = (buffer, cursor) => {
val ansiBuffer = Ansi.Str.parse(SyntaxHighlighting(buffer))
val (newBuffer, cursorOffset) = SelectionFilter.mangleBuffer(
selectionFilter, ansiBuffer, cursor, Ansi.Reversed.On
)
val newNewBuffer = HistoryFilter.mangleBuffer(
historyFilter, newBuffer, cursor,
Ansi.Color.Green
)

(newNewBuffer, cursorOffset)
}
) match {
case Some(res) =>
history = res :: history;
res
case None => ":q"
}
}
}