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  }