aboutsummaryrefslogtreecommitdiff
path: root/src/dotty/tools/dotc/repl/ammonite/filters/GUILikeFilters.scala
blob: 69a9769c63795a68f6c62a92f84d5d4a4f6d4871 (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
package dotty.tools
package dotc
package repl
package ammonite
package terminal
package filters

import terminal.FilterTools._
import terminal.LazyList.~:
import terminal.SpecialKeys._
import terminal.DelegateFilter
import terminal._

/**
 * Filters have hook into the various {Ctrl,Shift,Fn,Alt}x{Up,Down,Left,Right}
 * combination keys, and make them behave similarly as they would on a normal
 * GUI text editor: alt-{left, right} for word movement, hold-down-shift for
 * text selection, etc.
 */
object GUILikeFilters {
  case class SelectionFilter(indent: Int) extends DelegateFilter {
    def identifier = "SelectionFilter"
    var mark: Option[Int] = None

    def setMark(c: Int) = {
      Debug("setMark\t" + mark + "\t->\t" + c)
      if (mark == None) mark = Some(c)
    }

    def doIndent(
      b: Vector[Char],
      c: Int,
      rest: LazyList[Int],
      slicer: Vector[Char] => Int
    ) = {

      val markValue = mark.get
      val (chunks, chunkStarts, chunkIndex) = FilterTools.findChunks(b, c)
      val min = chunkStarts.lastIndexWhere(_ <= math.min(c, markValue))
      val max = chunkStarts.indexWhere(_ > math.max(c, markValue))
      val splitPoints = chunkStarts.slice(min, max)
      val frags = (0 +: splitPoints :+ 99999).sliding(2).zipWithIndex

      var firstOffset = 0
      val broken =
        for((Seq(l, r), i) <- frags) yield {
          val slice = b.slice(l, r)
          if (i == 0) slice
          else {
            val cut = slicer(slice)

            if (i == 1) firstOffset = cut

            if (cut < 0) slice.drop(-cut)
            else Vector.fill(cut)(' ') ++ slice
          }
        }
      val flattened = broken.flatten.toVector
      val deeperOffset = flattened.length - b.length

      val (newMark, newC) =
        if (mark.get > c) (mark.get + deeperOffset, c + firstOffset)
        else (mark.get + firstOffset, c + deeperOffset)

      mark = Some(newMark)
      TS(rest, flattened, newC)
    }

    def filter = Filter.merge(

      Case(ShiftUp) {(b, c, m) => setMark(c); BasicFilters.moveUp(b, c, m.width)},
      Case(ShiftDown) {(b, c, m) => setMark(c); BasicFilters.moveDown(b, c, m.width)},
      Case(ShiftRight) {(b, c, m) => setMark(c); (b, c + 1)},
      Case(ShiftLeft) {(b, c, m) => setMark(c); (b, c - 1)},
      Case(AltShiftUp) {(b, c, m) => setMark(c); BasicFilters.moveUp(b, c, m.width)},
      Case(AltShiftDown) {(b, c, m) => setMark(c); BasicFilters.moveDown(b, c, m.width)},
      Case(AltShiftRight) {(b, c, m) => setMark(c); wordRight(b, c)},
      Case(AltShiftLeft) {(b, c, m) => setMark(c); wordLeft(b, c)},
      Case(FnShiftRight) {(b, c, m) => setMark(c); BasicFilters.moveEnd(b, c, m.width)},
      Case(FnShiftLeft) {(b, c, m) => setMark(c); BasicFilters.moveStart(b, c, m.width)},
      Filter("fnOtherFilter") {
        case TS(27 ~: 91 ~: 90 ~: rest, b, c, _) if mark.isDefined =>
          doIndent(b, c, rest,
            slice => -math.min(slice.iterator.takeWhile(_ == ' ').size, indent)
          )

        case TS(9 ~: rest, b, c, _) if mark.isDefined => // Tab
          doIndent(b, c, rest,
            slice => indent
          )

        // Intercept every other character.
        case TS(char ~: inputs, buffer, cursor, _) if mark.isDefined =>
          // If it's a special command, just cancel the current selection.
          if (char.toChar.isControl &&
              char != 127 /*backspace*/ &&
              char != 13 /*enter*/ &&
              char != 10 /*enter*/) {
            mark = None
            TS(char ~: inputs, buffer, cursor)
          } else {
            // If it's a  printable character, delete the current
            // selection and write the printable character.
            val Seq(min, max) = Seq(mark.get, cursor).sorted
            mark = None
            val newBuffer = buffer.take(min) ++ buffer.drop(max)
            val newInputs =
              if (char == 127) inputs
              else char ~: inputs
            TS(newInputs, newBuffer, min)
          }
      }
    )
  }

  object SelectionFilter {
    def mangleBuffer(
      selectionFilter: SelectionFilter,
      string: Ansi.Str,
      cursor: Int,
      startColor: Ansi.Attr
    ) = {
      selectionFilter.mark match {
        case Some(mark) if mark != cursor =>
          val Seq(min, max) = Seq(cursor, mark).sorted
          val displayOffset = if (cursor < mark) 0 else -1
          val newStr = string.overlay(startColor, min, max)
          (newStr, displayOffset)
        case _ => (string, 0)
      }
    }
  }

  val fnFilter = Filter.merge(
    Case(FnUp)((b, c, m) => (b, c - 9999)),
    Case(FnDown)((b, c, m) => (b, c + 9999)),
    Case(FnRight)((b, c, m) => BasicFilters.moveEnd(b, c, m.width)),
    Case(FnLeft)((b, c, m) => BasicFilters.moveStart(b, c, m.width))
  )
  val altFilter = Filter.merge(
    Case(AltUp)    {(b, c, m) => BasicFilters.moveUp(b, c, m.width)},
    Case(AltDown)  {(b, c, m) => BasicFilters.moveDown(b, c, m.width)},
    Case(AltRight) {(b, c, m) => wordRight(b, c)},
    Case(AltLeft)  {(b, c, m) => wordLeft(b, c)}
  )

  val fnAltFilter = Filter.merge(
    Case(FnAltUp)    {(b, c, m) => (b, c)},
    Case(FnAltDown)  {(b, c, m) => (b, c)},
    Case(FnAltRight) {(b, c, m) => (b, c)},
    Case(FnAltLeft)  {(b, c, m) => (b, c)}
  )
  val fnAltShiftFilter = Filter.merge(
    Case(FnAltShiftRight) {(b, c, m) => (b, c)},
    Case(FnAltShiftLeft)  {(b, c, m) => (b, c)}
  )


  def consumeWord(b: Vector[Char], c: Int, delta: Int, offset: Int) = {
    var current = c
    while(b.isDefinedAt(current) && !b(current).isLetterOrDigit) current += delta
    while(b.isDefinedAt(current) && b(current).isLetterOrDigit) current += delta
    current + offset
  }

  // c -1 to move at least one character! Otherwise you get stuck at the start of
  // a word.
  def wordLeft(b: Vector[Char], c: Int) = b -> consumeWord(b, c - 1, -1, 1)
  def wordRight(b: Vector[Char], c: Int) = b -> consumeWord(b, c, 1, 0)
}