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  }