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

import terminal.FilterTools._
import terminal.SpecialKeys._
import terminal.{DelegateFilter, Filter, Terminal}
/**
 * Filters for injection of readline-specific hotkeys, the sort that
 * are available in bash, python and most other interactive command-lines
 */
object ReadlineFilters {
  // www.bigsmoke.us/readline/shortcuts
  // Ctrl-b     <- one char
  // Ctrl-f     -> one char
  // Alt-b      <- one word
  // Alt-f      -> one word
  // Ctrl-a     <- start of line
  // Ctrl-e     -> end of line
  // Ctrl-x-x   Toggle start/end

  // Backspace  <- delete char
  // Del        -> delete char
  // Ctrl-u     <- delete all
  // Ctrl-k     -> delete all
  // Alt-d      -> delete word
  // Ctrl-w     <- delete word

  // Ctrl-u/-   Undo
  // Ctrl-l     clear screen

  // Ctrl-k     -> cut all
  // Alt-d      -> cut word
  // Alt-Backspace  <- cut word
  // Ctrl-y     paste last cut

  /**
   * Basic readline-style navigation, using all the obscure alphabet hotkeys
   * rather than using arrows
   */
  lazy val navFilter = Filter.merge(
    Case(Ctrl('b'))((b, c, m) => (b, c - 1)), // <- one char
    Case(Ctrl('f'))((b, c, m) => (b, c + 1)), // -> one char
    Case(Alt + "b")((b, c, m) => GUILikeFilters.wordLeft(b, c)), // <- one word
    Case(Alt + "B")((b, c, m) => GUILikeFilters.wordLeft(b, c)), // <- one word
    Case(LinuxCtrlLeft)((b, c, m) => GUILikeFilters.wordLeft(b, c)), // <- one word
    Case(Alt + "f")((b, c, m) => GUILikeFilters.wordRight(b, c)), // -> one  word
    Case(Alt + "F")((b, c, m) => GUILikeFilters.wordRight(b, c)), // -> one  word
    Case(LinuxCtrlRight)((b, c, m) => GUILikeFilters.wordRight(b, c)), // -> one word
    Case(Home)((b, c, m) => BasicFilters.moveStart(b, c, m.width)), // <- one line
    Case(HomeScreen)((b, c, m) => BasicFilters.moveStart(b, c, m.width)), // <- one line
    Case(Ctrl('a'))((b, c, m) => BasicFilters.moveStart(b, c, m.width)),
    Case(End)((b, c, m) => BasicFilters.moveEnd(b, c, m.width)), // -> one line
    Case(EndScreen)((b, c, m) => BasicFilters.moveEnd(b, c, m.width)), // -> one line
    Case(Ctrl('e'))((b, c, m) => BasicFilters.moveEnd(b, c, m.width)),
    Case(Alt + "t")((b, c, m) => transposeWord(b, c)),
    Case(Alt + "T")((b, c, m) => transposeWord(b, c)),
    Case(Ctrl('t'))((b, c, m) => transposeLetter(b, c))
  )

  def transposeLetter(b: Vector[Char], c: Int) =
    // If there's no letter before the cursor to transpose, don't do anything
    if (c == 0) (b, c)
    else if (c == b.length) (b.dropRight(2) ++ b.takeRight(2).reverse, c)
    else (b.patch(c-1, b.slice(c-1, c+1).reverse, 2), c + 1)

  def transposeWord(b: Vector[Char], c: Int) = {
    val leftStart0 = GUILikeFilters.consumeWord(b, c - 1, -1, 1)
    val leftEnd0   = GUILikeFilters.consumeWord(b, leftStart0, 1, 0)
    val rightEnd   = GUILikeFilters.consumeWord(b, c, 1, 0)
    val rightStart = GUILikeFilters.consumeWord(b, rightEnd - 1, -1, 1)

    // If no word to the left to transpose, do nothing
    if (leftStart0 == 0 && rightStart == 0) (b, c)
    else {
      val (leftStart, leftEnd) =
        // If there is no word to the *right* to transpose,
        // transpose the two words to the left instead
        if (leftEnd0 == b.length && rightEnd == b.length) {
          val leftStart = GUILikeFilters.consumeWord(b, leftStart0 - 1, -1, 1)
          val leftEnd   = GUILikeFilters.consumeWord(b, leftStart, 1, 0)
          (leftStart, leftEnd)
        }else (leftStart0, leftEnd0)

      val newB =
        b.slice(0, leftStart)         ++
        b.slice(rightStart, rightEnd) ++
        b.slice(leftEnd, rightStart)  ++
        b.slice(leftStart, leftEnd)   ++
        b.slice(rightEnd, b.length)

      (newB, rightEnd)
    }
  }

  /**
   * All the cut-pasting logic, though for many people they simply
   * use these shortcuts for deleting and don't use paste much at all.
   */
  case class CutPasteFilter() extends DelegateFilter {
    def identifier = "CutPasteFilter"
    var accumulating = false
    var currentCut = Vector.empty[Char]
    def prepend(b: Vector[Char]) = {
      if (accumulating) currentCut = b ++ currentCut
      else currentCut = b
      accumulating = true
    }
    def append(b: Vector[Char]) = {
      if (accumulating) currentCut = currentCut ++ b
      else currentCut = b
      accumulating = true
    }
    def cutCharLeft(b: Vector[Char], c: Int) = {
      /* Do not edit current cut. Zsh(zle) & Bash(readline) do not edit the yank ring for Ctrl-h */
      (b patch(from = c - 1, patch = Nil, replaced = 1), c - 1)
    }

    def cutAllLeft(b: Vector[Char], c: Int) = {
      prepend(b.take(c))
      (b.drop(c), 0)
    }
    def cutAllRight(b: Vector[Char], c: Int) = {
      append(b.drop(c))
      (b.take(c), c)
    }

    def cutWordRight(b: Vector[Char], c: Int) = {
      val start = GUILikeFilters.consumeWord(b, c, 1, 0)
      append(b.slice(c, start))
      (b.take(c) ++ b.drop(start), c)
    }

    def cutWordLeft(b: Vector[Char], c: Int) = {
      val start = GUILikeFilters.consumeWord(b, c - 1, -1, 1)
      prepend(b.slice(start, c))
      (b.take(start) ++ b.drop(c), start)
    }

    def paste(b: Vector[Char], c: Int) = {
      accumulating = false
      (b.take(c) ++ currentCut ++ b.drop(c), c + currentCut.length)
    }

    def filter = Filter.merge(
      Case(Ctrl('u'))((b, c, m) => cutAllLeft(b, c)),
      Case(Ctrl('k'))((b, c, m) => cutAllRight(b, c)),
      Case(Alt + "d")((b, c, m) => cutWordRight(b, c)),
      Case(Ctrl('w'))((b, c, m) => cutWordLeft(b, c)),
      Case(Alt + "\u007f")((b, c, m) => cutWordLeft(b, c)),
      // weird hacks to make it run code every time without having to be the one
      // handling the input; ideally we'd change Filter to be something
      // other than a PartialFunction, but for now this will do.

      // If some command goes through that's not appending/prepending to the
      // kill ring, stop appending and allow the next kill to override it
      Filter.wrap("ReadLineFilterWrap") {_ => accumulating = false; None},
      Case(Ctrl('h'))((b, c, m) => cutCharLeft(b, c)),
      Case(Ctrl('y'))((b, c, m) => paste(b, c))
    )
  }
}