src.elv.sh@v0.21.0-dev.0.20240515223629-06979efb9a2a/pkg/edit/buffer_builtins.go (about) 1 package edit 2 3 import ( 4 "strings" 5 "unicode" 6 "unicode/utf8" 7 8 "src.elv.sh/pkg/cli" 9 "src.elv.sh/pkg/cli/tk" 10 "src.elv.sh/pkg/eval" 11 "src.elv.sh/pkg/strutil" 12 "src.elv.sh/pkg/wcwidth" 13 ) 14 15 func initBufferBuiltins(app cli.App, nb eval.NsBuilder) { 16 m := make(map[string]any) 17 for name, fn := range bufferBuiltinsData { 18 // Make a lexically scoped copy of fn. 19 fn := fn 20 m[name] = func() { 21 codeArea, ok := focusedCodeArea(app) 22 if !ok { 23 return 24 } 25 codeArea.MutateState(func(s *tk.CodeAreaState) { 26 fn(&s.Buffer) 27 }) 28 } 29 } 30 nb.AddGoFns(m) 31 } 32 33 var bufferBuiltinsData = map[string]func(*tk.CodeBuffer){ 34 "move-dot-left": makeMove(moveDotLeft), 35 "move-dot-right": makeMove(moveDotRight), 36 "move-dot-left-word": makeMove(moveDotLeftWord), 37 "move-dot-right-word": makeMove(moveDotRightWord), 38 "move-dot-left-small-word": makeMove(moveDotLeftSmallWord), 39 "move-dot-right-small-word": makeMove(moveDotRightSmallWord), 40 "move-dot-left-alnum-word": makeMove(moveDotLeftAlnumWord), 41 "move-dot-right-alnum-word": makeMove(moveDotRightAlnumWord), 42 "move-dot-sol": makeMove(moveDotSOL), 43 "move-dot-eol": makeMove(moveDotEOL), 44 45 "move-dot-up": makeMove(moveDotUp), 46 "move-dot-down": makeMove(moveDotDown), 47 48 "kill-rune-left": makeKill(moveDotLeft), 49 "kill-rune-right": makeKill(moveDotRight), 50 "kill-word-left": makeKill(moveDotLeftWord), 51 "kill-word-right": makeKill(moveDotRightWord), 52 "kill-small-word-left": makeKill(moveDotLeftSmallWord), 53 "kill-small-word-right": makeKill(moveDotRightSmallWord), 54 "kill-alnum-word-left": makeKill(moveDotLeftAlnumWord), 55 "kill-alnum-word-right": makeKill(moveDotRightAlnumWord), 56 "kill-line-left": makeKill(moveDotSOL), 57 "kill-line-right": makeKill(moveDotEOL), 58 59 "transpose-rune": makeTransform(transposeRunes), 60 "transpose-word": makeTransform(transposeWord), 61 "transpose-small-word": makeTransform(transposeSmallWord), 62 "transpose-alnum-word": makeTransform(transposeAlnumWord), 63 } 64 65 // A pure function that takes the current buffer and dot, and returns a new 66 // value for the dot. Used to derive move- and kill- functions that operate on 67 // the editor state. 68 type pureMover func(buffer string, dot int) int 69 70 func makeMove(m pureMover) func(*tk.CodeBuffer) { 71 return func(buf *tk.CodeBuffer) { 72 buf.Dot = m(buf.Content, buf.Dot) 73 } 74 } 75 76 func makeKill(m pureMover) func(*tk.CodeBuffer) { 77 return func(buf *tk.CodeBuffer) { 78 newDot := m(buf.Content, buf.Dot) 79 if newDot < buf.Dot { 80 // Dot moved to the left: remove text between new dot and old dot, 81 // and move the dot itself 82 buf.Content = buf.Content[:newDot] + buf.Content[buf.Dot:] 83 buf.Dot = newDot 84 } else if newDot > buf.Dot { 85 // Dot moved to the right: remove text between old dot and new dot. 86 buf.Content = buf.Content[:buf.Dot] + buf.Content[newDot:] 87 } 88 } 89 } 90 91 // A pure function that takes the current buffer and dot, and returns a new 92 // value for the buffer and dot. 93 type pureTransformer func(buffer string, dot int) (string, int) 94 95 func makeTransform(t pureTransformer) func(*tk.CodeBuffer) { 96 return func(buf *tk.CodeBuffer) { 97 buf.Content, buf.Dot = t(buf.Content, buf.Dot) 98 } 99 } 100 101 // Implementation of pure movers. 102 103 func moveDotLeft(buffer string, dot int) int { 104 _, w := utf8.DecodeLastRuneInString(buffer[:dot]) 105 return dot - w 106 } 107 108 func moveDotRight(buffer string, dot int) int { 109 _, w := utf8.DecodeRuneInString(buffer[dot:]) 110 return dot + w 111 } 112 113 func moveDotSOL(buffer string, dot int) int { 114 return strutil.FindLastSOL(buffer[:dot]) 115 } 116 117 func moveDotEOL(buffer string, dot int) int { 118 return strutil.FindFirstEOL(buffer[dot:]) + dot 119 } 120 121 func moveDotUp(buffer string, dot int) int { 122 sol := strutil.FindLastSOL(buffer[:dot]) 123 if sol == 0 { 124 // Already in the first line. 125 return dot 126 } 127 prevEOL := sol - 1 128 prevSOL := strutil.FindLastSOL(buffer[:prevEOL]) 129 width := wcwidth.Of(buffer[sol:dot]) 130 return prevSOL + len(wcwidth.Trim(buffer[prevSOL:prevEOL], width)) 131 } 132 133 func moveDotDown(buffer string, dot int) int { 134 eol := strutil.FindFirstEOL(buffer[dot:]) + dot 135 if eol == len(buffer) { 136 // Already in the last line. 137 return dot 138 } 139 nextSOL := eol + 1 140 nextEOL := strutil.FindFirstEOL(buffer[nextSOL:]) + nextSOL 141 sol := strutil.FindLastSOL(buffer[:dot]) 142 width := wcwidth.Of(buffer[sol:dot]) 143 return nextSOL + len(wcwidth.Trim(buffer[nextSOL:nextEOL], width)) 144 } 145 146 func transposeRunes(buffer string, dot int) (string, int) { 147 if len(buffer) == 0 { 148 return buffer, dot 149 } 150 151 var newBuffer string 152 var newDot int 153 // transpose at the beginning of the buffer transposes the first two 154 // characters, and at the end the last two 155 if dot == 0 { 156 first, firstLen := utf8.DecodeRuneInString(buffer) 157 if firstLen == len(buffer) { 158 return buffer, dot 159 } 160 second, secondLen := utf8.DecodeRuneInString(buffer[firstLen:]) 161 newBuffer = string(second) + string(first) + buffer[firstLen+secondLen:] 162 newDot = firstLen + secondLen 163 } else if dot == len(buffer) { 164 second, secondLen := utf8.DecodeLastRuneInString(buffer) 165 if secondLen == len(buffer) { 166 return buffer, dot 167 } 168 first, firstLen := utf8.DecodeLastRuneInString(buffer[:len(buffer)-secondLen]) 169 newBuffer = buffer[:len(buffer)-firstLen-secondLen] + string(second) + string(first) 170 newDot = len(newBuffer) 171 } else { 172 first, firstLen := utf8.DecodeLastRuneInString(buffer[:dot]) 173 second, secondLen := utf8.DecodeRuneInString(buffer[dot:]) 174 newBuffer = buffer[:dot-firstLen] + string(second) + string(first) + buffer[dot+secondLen:] 175 newDot = dot + secondLen 176 } 177 178 return newBuffer, newDot 179 } 180 181 func moveDotLeftWord(buffer string, dot int) int { 182 return moveDotLeftGeneralWord(categorizeWord, buffer, dot) 183 } 184 185 func moveDotRightWord(buffer string, dot int) int { 186 return moveDotRightGeneralWord(categorizeWord, buffer, dot) 187 } 188 189 func transposeWord(buffer string, dot int) (string, int) { 190 return transposeGeneralWord(categorizeWord, buffer, dot) 191 } 192 193 func categorizeWord(r rune) int { 194 switch { 195 case unicode.IsSpace(r): 196 return 0 197 default: 198 return 1 199 } 200 } 201 202 func moveDotLeftSmallWord(buffer string, dot int) int { 203 return moveDotLeftGeneralWord(tk.CategorizeSmallWord, buffer, dot) 204 } 205 206 func moveDotRightSmallWord(buffer string, dot int) int { 207 return moveDotRightGeneralWord(tk.CategorizeSmallWord, buffer, dot) 208 } 209 210 func transposeSmallWord(buffer string, dot int) (string, int) { 211 return transposeGeneralWord(tk.CategorizeSmallWord, buffer, dot) 212 } 213 214 func moveDotLeftAlnumWord(buffer string, dot int) int { 215 return moveDotLeftGeneralWord(categorizeAlnum, buffer, dot) 216 } 217 218 func moveDotRightAlnumWord(buffer string, dot int) int { 219 return moveDotRightGeneralWord(categorizeAlnum, buffer, dot) 220 } 221 222 func transposeAlnumWord(buffer string, dot int) (string, int) { 223 return transposeGeneralWord(categorizeAlnum, buffer, dot) 224 } 225 226 func categorizeAlnum(r rune) int { 227 switch { 228 case tk.IsAlnum(r): 229 return 1 230 default: 231 return 0 232 } 233 } 234 235 // Word movements are are more complex than one may expect. There are also 236 // several flavors of word movements supported by Elvish. 237 // 238 // To understand word movements, we first need to categorize runes into several 239 // categories: a whitespace category, plus one or more word category. The 240 // flavors of word movements are described by their different categorization: 241 // 242 // * Plain word: two categories: whitespace, and non-whitespace. This flavor 243 // corresponds to WORD in vi. 244 // 245 // * Small word: whitespace, alphanumeric, and everything else. This flavor 246 // corresponds to word in vi. 247 // 248 // * Alphanumeric word: non-alphanumeric (all treated as whitespace) and 249 // alphanumeric. This flavor corresponds to word in readline and zsh (when 250 // moving left; see below for the difference in behavior when moving right). 251 // 252 // After fixing the flavor, a "word" is a run of runes in the same 253 // non-whitespace category. For instance, the text "cd ~/tmp" has: 254 // 255 // * Two plain words: "cd" and "~/tmp". 256 // 257 // * Three small words: "cd", "~/" and "tmp". 258 // 259 // * Two alphanumeric words: "cd" and "tmp". 260 // 261 // To move left one word, we always move to the beginning of the last word to 262 // the left of the dot (excluding the dot). That is: 263 // 264 // * If we are in the middle of a word, we will move to its beginning. 265 // 266 // * If we are already at the beginning of a word, we will move to the beginning 267 // of the word before that. 268 // 269 // * If we are in a run of whitespaces, we will move to the beginning of the 270 // word before the run of whitespaces. 271 // 272 // Moving right one word works similarly: we move to the beginning of the first 273 // word to the right of the dot (excluding the dot). This behavior is the same 274 // as vi and zsh, but differs from GNU readline (used by bash) and fish, which 275 // moves the dot to one point after the end of the first word to the right of 276 // the dot. 277 // 278 // See the test case for a real-world example of how the different flavors of 279 // word movements work. 280 // 281 // A remark: This definition of "word movement" is general enough to include 282 // single-rune movements as a special case, where each rune is in its own word 283 // category (even whitespace runes). Single-rune movements are not implemented 284 // as such though, to avoid making things unnecessarily complex. 285 286 // A function that describes a word flavor by categorizing runes. The return 287 // value of 0 represents the whitespace category while other values represent 288 // different word categories. 289 type categorizer func(rune) int 290 291 // Move the dot left one word, using the word flavor described by the 292 // categorizer. 293 func moveDotLeftGeneralWord(categorize categorizer, buffer string, dot int) int { 294 // skip trailing whitespaces left of dot 295 pos := skipWsLeft(categorize, buffer, dot) 296 297 // skip this word 298 pos = skipSameCatLeft(categorize, buffer, pos) 299 300 return pos 301 } 302 303 // Move the dot right one word, using the word flavor described by the 304 // categorizer. 305 func moveDotRightGeneralWord(categorize categorizer, buffer string, dot int) int { 306 // skip leading whitespaces right of dot 307 pos := skipWsRight(categorize, buffer, dot) 308 309 if pos > dot { 310 // Dot was within whitespaces, and we have now moved to the start of the 311 // next word. 312 return pos 313 } 314 315 // Dot was within a word; skip both the word and whitespaces 316 317 // skip this word 318 pos = skipSameCatRight(categorize, buffer, pos) 319 // skip remaining whitespace 320 pos = skipWsRight(categorize, buffer, pos) 321 322 return pos 323 } 324 325 // Transposes the words around the cursor, using the word flavor described 326 // by the categorizer. 327 func transposeGeneralWord(categorize categorizer, buffer string, dot int) (string, int) { 328 if strings.TrimFunc(buffer, func(r rune) bool { return categorize(r) == 0 }) == "" { 329 // buffer contains only whitespace 330 return buffer, dot 331 } 332 333 // after skipping whitespace, find the end of the right word 334 pos := skipWsRight(categorize, buffer, dot) 335 var rightEnd int 336 if pos == len(buffer) { 337 // there is only whitespace to the right of the dot 338 rightEnd = skipWsLeft(categorize, buffer, pos) 339 } else { 340 rightEnd = skipSameCatRight(categorize, buffer, pos) 341 } 342 // if the dot started in the middle of a word, 'pos' is the same as dot, 343 // so we should skip word characters to the left to find the start of the 344 // word 345 rightStart := skipSameCatLeft(categorize, buffer, rightEnd) 346 347 leftEnd := skipWsLeft(categorize, buffer, rightStart) 348 var leftStart int 349 if leftEnd == 0 { 350 // right word is the first word, use it as the left word and find a 351 // new right word 352 leftStart = rightStart 353 leftEnd = rightEnd 354 355 rightStart = skipWsRight(categorize, buffer, leftEnd) 356 if rightStart == len(buffer) { 357 // there is only one word in the buffer 358 return buffer, dot 359 } 360 361 rightEnd = skipSameCatRight(categorize, buffer, rightStart) 362 } else { 363 leftStart = skipSameCatLeft(categorize, buffer, leftEnd) 364 } 365 366 return buffer[:leftStart] + buffer[rightStart:rightEnd] + buffer[leftEnd:rightStart] + buffer[leftStart:leftEnd] + buffer[rightEnd:], rightEnd 367 } 368 369 // Skips all runes to the left of the dot that belongs to the same category. 370 func skipSameCatLeft(categorize categorizer, buffer string, pos int) int { 371 if pos == 0 { 372 return pos 373 } 374 375 r, _ := utf8.DecodeLastRuneInString(buffer[:pos]) 376 cat := categorize(r) 377 return skipCatLeft(categorize, cat, buffer, pos) 378 } 379 380 // Skips whitespaces to the left of the dot. 381 func skipWsLeft(categorize categorizer, buffer string, pos int) int { 382 return skipCatLeft(categorize, 0, buffer, pos) 383 } 384 385 func skipCatLeft(categorize categorizer, cat int, buffer string, pos int) int { 386 left := strings.TrimRightFunc(buffer[:pos], func(r rune) bool { 387 return categorize(r) == cat 388 }) 389 390 return len(left) 391 } 392 393 // Skips all runes to the right of the dot that belongs to the same 394 // category. 395 func skipSameCatRight(categorize categorizer, buffer string, pos int) int { 396 if pos == len(buffer) { 397 return pos 398 } 399 400 r, _ := utf8.DecodeRuneInString(buffer[pos:]) 401 cat := categorize(r) 402 return skipCatRight(categorize, cat, buffer, pos) 403 } 404 405 // Skips whitespaces to the right of the dot. 406 func skipWsRight(categorize categorizer, buffer string, pos int) int { 407 return skipCatRight(categorize, 0, buffer, pos) 408 } 409 410 func skipCatRight(categorize categorizer, cat int, buffer string, pos int) int { 411 right := strings.TrimLeftFunc(buffer[pos:], func(r rune) bool { 412 return categorize(r) == cat 413 }) 414 415 return len(buffer) - len(right) 416 }