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) } } }