aboutsummaryrefslogtreecommitdiff
path: root/src/dotty/tools/dotc/repl/ammonite/Ansi.scala
blob: 37c4de7b56c17736bfc410fece28a2ea787414c0 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
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)
    }
  }
}