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  }