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