aboutsummaryrefslogtreecommitdiff
path: root/src/dotty/tools/dotc/repl/ammonite/Ansi.scala
diff options
context:
space:
mode:
Diffstat (limited to 'src/dotty/tools/dotc/repl/ammonite/Ansi.scala')
-rw-r--r--src/dotty/tools/dotc/repl/ammonite/Ansi.scala256
1 files changed, 256 insertions, 0 deletions
diff --git a/src/dotty/tools/dotc/repl/ammonite/Ansi.scala b/src/dotty/tools/dotc/repl/ammonite/Ansi.scala
new file mode 100644
index 000000000..37c4de7b5
--- /dev/null
+++ b/src/dotty/tools/dotc/repl/ammonite/Ansi.scala
@@ -0,0 +1,256 @@
+package dotty.tools
+package dotc
+package repl
+package ammonite.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)
+ }
+ }
+}