aboutsummaryrefslogblamecommitdiff
path: root/compiler/src/dotty/tools/dotc/repl/ammonite/Ansi.scala
blob: 37c4de7b56c17736bfc410fece28a2ea787414c0 (plain) (tree)



















                                                                                         
 



















                                                                         
                                    







                                                            
                                 

















                                                            
                                
















                                                             

                                





                                                  
                                      




                                                        

                                    






                                                               
                                                                

              
                                                        

                                                  

                                                     

























                                                                                             






































                                                                                  
 


















































                                                                               


                     















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