github.com/kolbycrouch/elvish@v0.14.1-0.20210614162631-215b9ac1c423/pkg/edit/builtins.go (about) 1 package edit 2 3 import ( 4 "errors" 5 "strings" 6 "unicode" 7 "unicode/utf8" 8 9 "src.elv.sh/pkg/cli" 10 "src.elv.sh/pkg/cli/mode" 11 "src.elv.sh/pkg/cli/term" 12 "src.elv.sh/pkg/cli/tk" 13 "src.elv.sh/pkg/eval" 14 "src.elv.sh/pkg/parse" 15 "src.elv.sh/pkg/parse/parseutil" 16 "src.elv.sh/pkg/strutil" 17 "src.elv.sh/pkg/ui" 18 "src.elv.sh/pkg/wcwidth" 19 ) 20 21 //elvdoc:fn binding-table 22 // 23 // Converts a normal map into a binding map. 24 25 //elvdoc:fn -dump-buf 26 // 27 // Dumps the current UI buffer as HTML. This command is used to generate 28 // "ttyshots" on the [website](https://elv.sh). 29 // 30 // Example: 31 // 32 // ```elvish 33 // ttyshot = ~/a.html 34 // edit:insert:binding[Ctrl-X] = { edit:-dump-buf > $tty } 35 // ``` 36 37 func dumpBuf(tty cli.TTY) string { 38 return bufToHTML(tty.Buffer()) 39 } 40 41 //elvdoc:fn close-mode 42 // 43 // Closes the current active mode. 44 45 func closeMode(app cli.App) { 46 app.SetAddon(nil, false) 47 } 48 49 //elvdoc:fn end-of-history 50 // 51 // Adds a notification saying "End of history". 52 53 func endOfHistory(app cli.App) { 54 app.Notify("End of history") 55 } 56 57 //elvdoc:fn redraw 58 // 59 // ```elvish 60 // edit:redraw &full=$false 61 // ``` 62 // 63 // Triggers a redraw. 64 // 65 // The `&full` option controls whether to do a full redraw. By default, all 66 // redraws performed by the line editor are incremental redraws, updating only 67 // the part of the screen that has changed from the last redraw. A full redraw 68 // updates the entire command line. 69 70 type redrawOpts struct{ Full bool } 71 72 func (redrawOpts) SetDefaultOptions() {} 73 74 func redraw(app cli.App, opts redrawOpts) { 75 if opts.Full { 76 app.RedrawFull() 77 } else { 78 app.Redraw() 79 } 80 } 81 82 //elvdoc:fn clear 83 // 84 // ```elvish 85 // edit:clear 86 // ``` 87 // 88 // Clears the screen. 89 // 90 // This command should be used in place of the external `clear` command to clear 91 // the screen. 92 93 func clear(app cli.App, tty cli.TTY) { 94 tty.HideCursor() 95 tty.ClearScreen() 96 app.RedrawFull() 97 tty.ShowCursor() 98 } 99 100 //elvdoc:fn insert-raw 101 // 102 // Requests the next terminal input to be inserted uninterpreted. 103 104 func insertRaw(app cli.App, tty cli.TTY) { 105 tty.SetRawInput(1) 106 w := mode.NewStub(mode.StubSpec{ 107 Bindings: tk.FuncBindings(func(w tk.Widget, event term.Event) bool { 108 switch event := event.(type) { 109 case term.KeyEvent: 110 app.CodeArea().MutateState(func(s *tk.CodeAreaState) { 111 s.Buffer.InsertAtDot(string(event.Rune)) 112 }) 113 app.SetAddon(nil, false) 114 return true 115 default: 116 return false 117 } 118 }), 119 Name: " RAW ", 120 }) 121 app.SetAddon(w, false) 122 } 123 124 //elvdoc:fn key 125 // 126 // ```elvish 127 // edit:key $string 128 // ``` 129 // 130 // Parses a string into a key. 131 132 var errMustBeKeyOrString = errors.New("must be key or string") 133 134 func toKey(v interface{}) (ui.Key, error) { 135 switch v := v.(type) { 136 case ui.Key: 137 return v, nil 138 case string: 139 return ui.ParseKey(v) 140 default: 141 return ui.Key{}, errMustBeKeyOrString 142 } 143 } 144 145 //elvdoc:fn return-line 146 // 147 // Causes the Elvish REPL to end the current read iteration and evaluate the 148 // code it just read. 149 150 //elvdoc:fn return-eof 151 // 152 // Causes the Elvish REPL to terminate. Internally, this works by raising a 153 // special exception. 154 155 //elvdoc:fn smart-enter 156 // 157 // Inserts a literal newline if the current code is not syntactically complete 158 // Elvish code. Accepts the current line otherwise. 159 160 func smartEnter(app cli.App) { 161 // TODO(xiaq): Fix the race condition. 162 buf := app.CodeArea().CopyState().Buffer 163 if isSyntaxComplete(buf.Content) { 164 app.CommitCode() 165 } else { 166 app.CodeArea().MutateState(func(s *tk.CodeAreaState) { 167 s.Buffer.InsertAtDot("\n") 168 }) 169 } 170 } 171 172 func isSyntaxComplete(code string) bool { 173 _, err := parse.Parse(parse.Source{Code: code}, parse.Config{}) 174 if err != nil { 175 for _, e := range err.(*parse.Error).Entries { 176 if e.Context.From == len(code) { 177 return false 178 } 179 } 180 } 181 return true 182 } 183 184 //elvdoc:fn wordify 185 // 186 // 187 // ```elvish 188 // edit:wordify $code 189 // ``` 190 // Breaks Elvish code into words. 191 192 func wordify(fm *eval.Frame, code string) { 193 out := fm.OutputChan() 194 for _, s := range parseutil.Wordify(code) { 195 out <- s 196 } 197 } 198 199 func initTTYBuiltins(app cli.App, tty cli.TTY, nb eval.NsBuilder) { 200 nb.AddGoFns("<edit>", map[string]interface{}{ 201 "-dump-buf": func() string { return dumpBuf(tty) }, 202 "insert-raw": func() { insertRaw(app, tty) }, 203 "clear": func() { clear(app, tty) }, 204 }) 205 } 206 207 func initMiscBuiltins(app cli.App, nb eval.NsBuilder) { 208 nb.AddGoFns("<edit>", map[string]interface{}{ 209 "binding-table": makeBindingMap, 210 "close-mode": func() { closeMode(app) }, 211 "end-of-history": func() { endOfHistory(app) }, 212 "key": toKey, 213 "redraw": func(opts redrawOpts) { redraw(app, opts) }, 214 "return-line": app.CommitCode, 215 "return-eof": app.CommitEOF, 216 "smart-enter": func() { smartEnter(app) }, 217 "wordify": wordify, 218 }) 219 } 220 221 var bufferBuiltinsData = map[string]func(*tk.CodeBuffer){ 222 "move-dot-left": makeMove(moveDotLeft), 223 "move-dot-right": makeMove(moveDotRight), 224 "move-dot-left-word": makeMove(moveDotLeftWord), 225 "move-dot-right-word": makeMove(moveDotRightWord), 226 "move-dot-left-small-word": makeMove(moveDotLeftSmallWord), 227 "move-dot-right-small-word": makeMove(moveDotRightSmallWord), 228 "move-dot-left-alnum-word": makeMove(moveDotLeftAlnumWord), 229 "move-dot-right-alnum-word": makeMove(moveDotRightAlnumWord), 230 "move-dot-sol": makeMove(moveDotSOL), 231 "move-dot-eol": makeMove(moveDotEOL), 232 233 "move-dot-up": makeMove(moveDotUp), 234 "move-dot-down": makeMove(moveDotDown), 235 236 "kill-rune-left": makeKill(moveDotLeft), 237 "kill-rune-right": makeKill(moveDotRight), 238 "kill-word-left": makeKill(moveDotLeftWord), 239 "kill-word-right": makeKill(moveDotRightWord), 240 "kill-small-word-left": makeKill(moveDotLeftSmallWord), 241 "kill-small-word-right": makeKill(moveDotRightSmallWord), 242 "kill-left-alnum-word": makeKill(moveDotLeftAlnumWord), 243 "kill-right-alnum-word": makeKill(moveDotRightAlnumWord), 244 "kill-line-left": makeKill(moveDotSOL), 245 "kill-line-right": makeKill(moveDotEOL), 246 } 247 248 func initBufferBuiltins(app cli.App, nb eval.NsBuilder) { 249 nb.AddGoFns("<edit>", bufferBuiltins(app)) 250 } 251 252 func bufferBuiltins(app cli.App) map[string]interface{} { 253 m := make(map[string]interface{}) 254 for name, fn := range bufferBuiltinsData { 255 // Make a lexically scoped copy of fn. 256 fn2 := fn 257 m[name] = func() { 258 app.CodeArea().MutateState(func(s *tk.CodeAreaState) { 259 fn2(&s.Buffer) 260 }) 261 } 262 } 263 return m 264 } 265 266 // A pure function that takes the current buffer and dot, and returns a new 267 // value for the dot. Used to derive move- and kill- functions that operate on 268 // the editor state. 269 type pureMover func(buffer string, dot int) int 270 271 func makeMove(m pureMover) func(*tk.CodeBuffer) { 272 return func(buf *tk.CodeBuffer) { 273 buf.Dot = m(buf.Content, buf.Dot) 274 } 275 } 276 277 func makeKill(m pureMover) func(*tk.CodeBuffer) { 278 return func(buf *tk.CodeBuffer) { 279 newDot := m(buf.Content, buf.Dot) 280 if newDot < buf.Dot { 281 // Dot moved to the left: remove text between new dot and old dot, 282 // and move the dot itself 283 buf.Content = buf.Content[:newDot] + buf.Content[buf.Dot:] 284 buf.Dot = newDot 285 } else if newDot > buf.Dot { 286 // Dot moved to the right: remove text between old dot and new dot. 287 buf.Content = buf.Content[:buf.Dot] + buf.Content[newDot:] 288 } 289 } 290 } 291 292 // Implementation of pure movers. 293 294 //elvdoc:fn move-dot-left 295 // 296 // Moves the dot left one rune. Does nothing if the dot is at the beginning of 297 // the buffer. 298 299 //elvdoc:fn kill-rune-left 300 // 301 // Kills one rune left of the dot. Does nothing if the dot is at the beginning of 302 // the buffer. 303 304 func moveDotLeft(buffer string, dot int) int { 305 _, w := utf8.DecodeLastRuneInString(buffer[:dot]) 306 return dot - w 307 } 308 309 //elvdoc:fn move-dot-right 310 // 311 // Moves the dot right one rune. Does nothing if the dot is at the end of the 312 // buffer. 313 314 //elvdoc:fn kill-rune-left 315 // 316 // Kills one rune right of the dot. Does nothing if the dot is at the end of the 317 // buffer. 318 319 func moveDotRight(buffer string, dot int) int { 320 _, w := utf8.DecodeRuneInString(buffer[dot:]) 321 return dot + w 322 } 323 324 //elvdoc:fn move-dot-sol 325 // 326 // Moves the dot to the start of the current line. 327 328 //elvdoc:fn kill-line-left 329 // 330 // Deletes the text between the dot and the start of the current line. 331 332 func moveDotSOL(buffer string, dot int) int { 333 return strutil.FindLastSOL(buffer[:dot]) 334 } 335 336 //elvdoc:fn move-dot-eol 337 // 338 // Moves the dot to the end of the current line. 339 340 //elvdoc:fn kill-line-right 341 // 342 // Deletes the text between the dot and the end of the current line. 343 344 func moveDotEOL(buffer string, dot int) int { 345 return strutil.FindFirstEOL(buffer[dot:]) + dot 346 } 347 348 //elvdoc:fn move-dot-up 349 // 350 // Moves the dot up one line, trying to preserve the visual horizontal position. 351 // Does nothing if dot is already on the first line of the buffer. 352 353 func moveDotUp(buffer string, dot int) int { 354 sol := strutil.FindLastSOL(buffer[:dot]) 355 if sol == 0 { 356 // Already in the first line. 357 return dot 358 } 359 prevEOL := sol - 1 360 prevSOL := strutil.FindLastSOL(buffer[:prevEOL]) 361 width := wcwidth.Of(buffer[sol:dot]) 362 return prevSOL + len(wcwidth.Trim(buffer[prevSOL:prevEOL], width)) 363 } 364 365 //elvdoc:fn move-dot-down 366 // 367 // Moves the dot down one line, trying to preserve the visual horizontal 368 // position. Does nothing if dot is already on the last line of the buffer. 369 370 func moveDotDown(buffer string, dot int) int { 371 eol := strutil.FindFirstEOL(buffer[dot:]) + dot 372 if eol == len(buffer) { 373 // Already in the last line. 374 return dot 375 } 376 nextSOL := eol + 1 377 nextEOL := strutil.FindFirstEOL(buffer[nextSOL:]) + nextSOL 378 sol := strutil.FindLastSOL(buffer[:dot]) 379 width := wcwidth.Of(buffer[sol:dot]) 380 return nextSOL + len(wcwidth.Trim(buffer[nextSOL:nextEOL], width)) 381 } 382 383 // TODO(xiaq): Document the concepts of words, small words and alnum words. 384 385 //elvdoc:fn move-dot-left-word 386 // 387 // Moves the dot to the beginning of the last word to the left of the dot. 388 389 //elvdoc:fn kill-word-left 390 // 391 // Deletes the the last word to the left of the dot. 392 393 func moveDotLeftWord(buffer string, dot int) int { 394 return moveDotLeftGeneralWord(categorizeWord, buffer, dot) 395 } 396 397 //elvdoc:fn move-dot-right-word 398 // 399 // Moves the dot to the beginning of the first word to the right of the dot. 400 401 //elvdoc:fn kill-word-right 402 // 403 // Deletes the the first word to the right of the dot. 404 405 func moveDotRightWord(buffer string, dot int) int { 406 return moveDotRightGeneralWord(categorizeWord, buffer, dot) 407 } 408 409 func categorizeWord(r rune) int { 410 switch { 411 case unicode.IsSpace(r): 412 return 0 413 default: 414 return 1 415 } 416 } 417 418 //elvdoc:fn move-dot-left-small-word 419 // 420 // Moves the dot to the beginning of the last small word to the left of the dot. 421 422 //elvdoc:fn kill-small-word-left 423 // 424 // Deletes the the last small word to the left of the dot. 425 426 func moveDotLeftSmallWord(buffer string, dot int) int { 427 return moveDotLeftGeneralWord(tk.CategorizeSmallWord, buffer, dot) 428 } 429 430 //elvdoc:fn move-dot-right-small-word 431 // 432 // Moves the dot to the beginning of the first small word to the right of the dot. 433 434 //elvdoc:fn kill-small-word-right 435 // 436 // Deletes the the first small word to the right of the dot. 437 438 func moveDotRightSmallWord(buffer string, dot int) int { 439 return moveDotRightGeneralWord(tk.CategorizeSmallWord, buffer, dot) 440 } 441 442 //elvdoc:fn move-dot-left-alnum-word 443 // 444 // Moves the dot to the beginning of the last alnum word to the left of the dot. 445 446 //elvdoc:fn kill-alnum-word-left 447 // 448 // Deletes the the last alnum word to the left of the dot. 449 450 func moveDotLeftAlnumWord(buffer string, dot int) int { 451 return moveDotLeftGeneralWord(categorizeAlnum, buffer, dot) 452 } 453 454 //elvdoc:fn move-dot-right-alnum-word 455 // 456 // Moves the dot to the beginning of the first alnum word to the right of the dot. 457 458 //elvdoc:fn kill-alnum-word-right 459 // 460 // Deletes the the first alnum word to the right of the dot. 461 462 func moveDotRightAlnumWord(buffer string, dot int) int { 463 return moveDotRightGeneralWord(categorizeAlnum, buffer, dot) 464 } 465 466 func categorizeAlnum(r rune) int { 467 switch { 468 case tk.IsAlnum(r): 469 return 1 470 default: 471 return 0 472 } 473 } 474 475 // Word movements are are more complex than one may expect. There are also 476 // several flavors of word movements supported by Elvish. 477 // 478 // To understand word movements, we first need to categorize runes into several 479 // categories: a whitespace category, plus one or more word category. The 480 // flavors of word movements are described by their different categorization: 481 // 482 // * Plain word: two categories: whitespace, and non-whitespace. This flavor 483 // corresponds to WORD in vi. 484 // 485 // * Small word: whitespace, alphanumeric, and everything else. This flavor 486 // corresponds to word in vi. 487 // 488 // * Alphanumeric word: non-alphanumeric (all treated as whitespace) and 489 // alphanumeric. This flavor corresponds to word in readline and zsh (when 490 // moving left; see below for the difference in behavior when moving right). 491 // 492 // After fixing the flavor, a "word" is a run of runes in the same 493 // non-whitespace category. For instance, the text "cd ~/tmp" has: 494 // 495 // * Two plain words: "cd" and "~/tmp". 496 // 497 // * Three small words: "cd", "~/" and "tmp". 498 // 499 // * Two alphanumeric words: "cd" and "tmp". 500 // 501 // To move left one word, we always move to the beginning of the last word to 502 // the left of the dot (excluding the dot). That is: 503 // 504 // * If we are in the middle of a word, we will move to its beginning. 505 // 506 // * If we are already at the beginning of a word, we will move to the beginning 507 // of the word before that. 508 // 509 // * If we are in a run of whitespaces, we will move to the beginning of the 510 // word before the run of whitespaces. 511 // 512 // Moving right one word works similarly: we move to the beginning of the first 513 // word to the right of the dot (excluding the dot). This behavior is the same 514 // as vi and zsh, but differs from GNU readline (used by bash) and fish, which 515 // moves the dot to one point after the end of the first word to the right of 516 // the dot. 517 // 518 // See the test case for a real-world example of how the different flavors of 519 // word movements work. 520 // 521 // A remark: This definition of "word movement" is general enough to include 522 // single-rune movements as a special case, where each rune is in its own word 523 // category (even whitespace runes). Single-rune movements are not implemented 524 // as such though, to avoid making things unnecessarily complex. 525 526 // A function that describes a word flavor by categorizing runes. The return 527 // value of 0 represents the whitespace category while other values represent 528 // different word categories. 529 type categorizer func(rune) int 530 531 // Move the dot left one word, using the word flavor described by the 532 // categorizer. 533 func moveDotLeftGeneralWord(categorize categorizer, buffer string, dot int) int { 534 left := buffer[:dot] 535 skipCat := func(cat int) { 536 left = strings.TrimRightFunc(left, func(r rune) bool { 537 return categorize(r) == cat 538 }) 539 } 540 541 // skip trailing whitespaces left of dot 542 skipCat(0) 543 544 // get category of last rune 545 r, _ := utf8.DecodeLastRuneInString(left) 546 cat := categorize(r) 547 548 // skip this word 549 skipCat(cat) 550 551 return len(left) 552 } 553 554 // Move the dot right one word, using the word flavor described by the 555 // categorizer. 556 func moveDotRightGeneralWord(categorize categorizer, buffer string, dot int) int { 557 right := buffer[dot:] 558 skipCat := func(cat int) { 559 right = strings.TrimLeftFunc(right, func(r rune) bool { 560 return categorize(r) == cat 561 }) 562 } 563 564 // skip leading whitespaces right of dot 565 skipCat(0) 566 567 // check whether any whitespace was skipped; if whitespace was 568 // skipped, then dot is already successfully moved to next 569 // non-whitespace run 570 if dot < len(buffer)-len(right) { 571 return len(buffer) - len(right) 572 } 573 574 // no whitespace was skipped, so we still have to skip to the next word 575 576 // get category of first rune 577 r, _ := utf8.DecodeRuneInString(right) 578 cat := categorize(r) 579 // skip this word 580 skipCat(cat) 581 // skip remaining whitespace 582 skipCat(0) 583 584 return len(buffer) - len(right) 585 }