buffer_builtins.go 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416
  1. package edit
  2. import (
  3. "strings"
  4. "unicode"
  5. "unicode/utf8"
  6. "src.elv.sh/pkg/cli"
  7. "src.elv.sh/pkg/cli/tk"
  8. "src.elv.sh/pkg/eval"
  9. "src.elv.sh/pkg/strutil"
  10. "src.elv.sh/pkg/wcwidth"
  11. )
  12. func initBufferBuiltins(app cli.App, nb eval.NsBuilder) {
  13. m := make(map[string]any)
  14. for name, fn := range bufferBuiltinsData {
  15. // Make a lexically scoped copy of fn.
  16. fn := fn
  17. m[name] = func() {
  18. codeArea, ok := focusedCodeArea(app)
  19. if !ok {
  20. return
  21. }
  22. codeArea.MutateState(func(s *tk.CodeAreaState) {
  23. fn(&s.Buffer)
  24. })
  25. }
  26. }
  27. nb.AddGoFns(m)
  28. }
  29. var bufferBuiltinsData = map[string]func(*tk.CodeBuffer){
  30. "move-dot-left": makeMove(moveDotLeft),
  31. "move-dot-right": makeMove(moveDotRight),
  32. "move-dot-left-word": makeMove(moveDotLeftWord),
  33. "move-dot-right-word": makeMove(moveDotRightWord),
  34. "move-dot-left-small-word": makeMove(moveDotLeftSmallWord),
  35. "move-dot-right-small-word": makeMove(moveDotRightSmallWord),
  36. "move-dot-left-alnum-word": makeMove(moveDotLeftAlnumWord),
  37. "move-dot-right-alnum-word": makeMove(moveDotRightAlnumWord),
  38. "move-dot-sol": makeMove(moveDotSOL),
  39. "move-dot-eol": makeMove(moveDotEOL),
  40. "move-dot-up": makeMove(moveDotUp),
  41. "move-dot-down": makeMove(moveDotDown),
  42. "kill-rune-left": makeKill(moveDotLeft),
  43. "kill-rune-right": makeKill(moveDotRight),
  44. "kill-word-left": makeKill(moveDotLeftWord),
  45. "kill-word-right": makeKill(moveDotRightWord),
  46. "kill-small-word-left": makeKill(moveDotLeftSmallWord),
  47. "kill-small-word-right": makeKill(moveDotRightSmallWord),
  48. "kill-left-alnum-word": makeKill(moveDotLeftAlnumWord),
  49. "kill-right-alnum-word": makeKill(moveDotRightAlnumWord),
  50. "kill-line-left": makeKill(moveDotSOL),
  51. "kill-line-right": makeKill(moveDotEOL),
  52. "transpose-rune": makeTransform(transposeRunes),
  53. "transpose-word": makeTransform(transposeWord),
  54. "transpose-small-word": makeTransform(transposeSmallWord),
  55. "transpose-alnum-word": makeTransform(transposeAlnumWord),
  56. }
  57. // A pure function that takes the current buffer and dot, and returns a new
  58. // value for the dot. Used to derive move- and kill- functions that operate on
  59. // the editor state.
  60. type pureMover func(buffer string, dot int) int
  61. func makeMove(m pureMover) func(*tk.CodeBuffer) {
  62. return func(buf *tk.CodeBuffer) {
  63. buf.Dot = m(buf.Content, buf.Dot)
  64. }
  65. }
  66. func makeKill(m pureMover) func(*tk.CodeBuffer) {
  67. return func(buf *tk.CodeBuffer) {
  68. newDot := m(buf.Content, buf.Dot)
  69. if newDot < buf.Dot {
  70. // Dot moved to the left: remove text between new dot and old dot,
  71. // and move the dot itself
  72. buf.Content = buf.Content[:newDot] + buf.Content[buf.Dot:]
  73. buf.Dot = newDot
  74. } else if newDot > buf.Dot {
  75. // Dot moved to the right: remove text between old dot and new dot.
  76. buf.Content = buf.Content[:buf.Dot] + buf.Content[newDot:]
  77. }
  78. }
  79. }
  80. // A pure function that takes the current buffer and dot, and returns a new
  81. // value for the buffer and dot.
  82. type pureTransformer func(buffer string, dot int) (string, int)
  83. func makeTransform(t pureTransformer) func(*tk.CodeBuffer) {
  84. return func(buf *tk.CodeBuffer) {
  85. buf.Content, buf.Dot = t(buf.Content, buf.Dot)
  86. }
  87. }
  88. // Implementation of pure movers.
  89. func moveDotLeft(buffer string, dot int) int {
  90. _, w := utf8.DecodeLastRuneInString(buffer[:dot])
  91. return dot - w
  92. }
  93. func moveDotRight(buffer string, dot int) int {
  94. _, w := utf8.DecodeRuneInString(buffer[dot:])
  95. return dot + w
  96. }
  97. func moveDotSOL(buffer string, dot int) int {
  98. return strutil.FindLastSOL(buffer[:dot])
  99. }
  100. func moveDotEOL(buffer string, dot int) int {
  101. return strutil.FindFirstEOL(buffer[dot:]) + dot
  102. }
  103. func moveDotUp(buffer string, dot int) int {
  104. sol := strutil.FindLastSOL(buffer[:dot])
  105. if sol == 0 {
  106. // Already in the first line.
  107. return dot
  108. }
  109. prevEOL := sol - 1
  110. prevSOL := strutil.FindLastSOL(buffer[:prevEOL])
  111. width := wcwidth.Of(buffer[sol:dot])
  112. return prevSOL + len(wcwidth.Trim(buffer[prevSOL:prevEOL], width))
  113. }
  114. func moveDotDown(buffer string, dot int) int {
  115. eol := strutil.FindFirstEOL(buffer[dot:]) + dot
  116. if eol == len(buffer) {
  117. // Already in the last line.
  118. return dot
  119. }
  120. nextSOL := eol + 1
  121. nextEOL := strutil.FindFirstEOL(buffer[nextSOL:]) + nextSOL
  122. sol := strutil.FindLastSOL(buffer[:dot])
  123. width := wcwidth.Of(buffer[sol:dot])
  124. return nextSOL + len(wcwidth.Trim(buffer[nextSOL:nextEOL], width))
  125. }
  126. func transposeRunes(buffer string, dot int) (string, int) {
  127. if len(buffer) == 0 {
  128. return buffer, dot
  129. }
  130. var newBuffer string
  131. var newDot int
  132. // transpose at the beginning of the buffer transposes the first two
  133. // characters, and at the end the last two
  134. if dot == 0 {
  135. first, firstLen := utf8.DecodeRuneInString(buffer)
  136. if firstLen == len(buffer) {
  137. return buffer, dot
  138. }
  139. second, secondLen := utf8.DecodeRuneInString(buffer[firstLen:])
  140. newBuffer = string(second) + string(first) + buffer[firstLen+secondLen:]
  141. newDot = firstLen + secondLen
  142. } else if dot == len(buffer) {
  143. second, secondLen := utf8.DecodeLastRuneInString(buffer)
  144. if secondLen == len(buffer) {
  145. return buffer, dot
  146. }
  147. first, firstLen := utf8.DecodeLastRuneInString(buffer[:len(buffer)-secondLen])
  148. newBuffer = buffer[:len(buffer)-firstLen-secondLen] + string(second) + string(first)
  149. newDot = len(newBuffer)
  150. } else {
  151. first, firstLen := utf8.DecodeLastRuneInString(buffer[:dot])
  152. second, secondLen := utf8.DecodeRuneInString(buffer[dot:])
  153. newBuffer = buffer[:dot-firstLen] + string(second) + string(first) + buffer[dot+secondLen:]
  154. newDot = dot + secondLen
  155. }
  156. return newBuffer, newDot
  157. }
  158. func moveDotLeftWord(buffer string, dot int) int {
  159. return moveDotLeftGeneralWord(categorizeWord, buffer, dot)
  160. }
  161. func moveDotRightWord(buffer string, dot int) int {
  162. return moveDotRightGeneralWord(categorizeWord, buffer, dot)
  163. }
  164. func transposeWord(buffer string, dot int) (string, int) {
  165. return transposeGeneralWord(categorizeWord, buffer, dot)
  166. }
  167. func categorizeWord(r rune) int {
  168. switch {
  169. case unicode.IsSpace(r):
  170. return 0
  171. default:
  172. return 1
  173. }
  174. }
  175. func moveDotLeftSmallWord(buffer string, dot int) int {
  176. return moveDotLeftGeneralWord(tk.CategorizeSmallWord, buffer, dot)
  177. }
  178. func moveDotRightSmallWord(buffer string, dot int) int {
  179. return moveDotRightGeneralWord(tk.CategorizeSmallWord, buffer, dot)
  180. }
  181. func transposeSmallWord(buffer string, dot int) (string, int) {
  182. return transposeGeneralWord(tk.CategorizeSmallWord, buffer, dot)
  183. }
  184. func moveDotLeftAlnumWord(buffer string, dot int) int {
  185. return moveDotLeftGeneralWord(categorizeAlnum, buffer, dot)
  186. }
  187. func moveDotRightAlnumWord(buffer string, dot int) int {
  188. return moveDotRightGeneralWord(categorizeAlnum, buffer, dot)
  189. }
  190. func transposeAlnumWord(buffer string, dot int) (string, int) {
  191. return transposeGeneralWord(categorizeAlnum, buffer, dot)
  192. }
  193. func categorizeAlnum(r rune) int {
  194. switch {
  195. case tk.IsAlnum(r):
  196. return 1
  197. default:
  198. return 0
  199. }
  200. }
  201. // Word movements are are more complex than one may expect. There are also
  202. // several flavors of word movements supported by Elvish.
  203. //
  204. // To understand word movements, we first need to categorize runes into several
  205. // categories: a whitespace category, plus one or more word category. The
  206. // flavors of word movements are described by their different categorization:
  207. //
  208. // * Plain word: two categories: whitespace, and non-whitespace. This flavor
  209. // corresponds to WORD in vi.
  210. //
  211. // * Small word: whitespace, alphanumeric, and everything else. This flavor
  212. // corresponds to word in vi.
  213. //
  214. // * Alphanumeric word: non-alphanumeric (all treated as whitespace) and
  215. // alphanumeric. This flavor corresponds to word in readline and zsh (when
  216. // moving left; see below for the difference in behavior when moving right).
  217. //
  218. // After fixing the flavor, a "word" is a run of runes in the same
  219. // non-whitespace category. For instance, the text "cd ~/tmp" has:
  220. //
  221. // * Two plain words: "cd" and "~/tmp".
  222. //
  223. // * Three small words: "cd", "~/" and "tmp".
  224. //
  225. // * Two alphanumeric words: "cd" and "tmp".
  226. //
  227. // To move left one word, we always move to the beginning of the last word to
  228. // the left of the dot (excluding the dot). That is:
  229. //
  230. // * If we are in the middle of a word, we will move to its beginning.
  231. //
  232. // * If we are already at the beginning of a word, we will move to the beginning
  233. // of the word before that.
  234. //
  235. // * If we are in a run of whitespaces, we will move to the beginning of the
  236. // word before the run of whitespaces.
  237. //
  238. // Moving right one word works similarly: we move to the beginning of the first
  239. // word to the right of the dot (excluding the dot). This behavior is the same
  240. // as vi and zsh, but differs from GNU readline (used by bash) and fish, which
  241. // moves the dot to one point after the end of the first word to the right of
  242. // the dot.
  243. //
  244. // See the test case for a real-world example of how the different flavors of
  245. // word movements work.
  246. //
  247. // A remark: This definition of "word movement" is general enough to include
  248. // single-rune movements as a special case, where each rune is in its own word
  249. // category (even whitespace runes). Single-rune movements are not implemented
  250. // as such though, to avoid making things unnecessarily complex.
  251. // A function that describes a word flavor by categorizing runes. The return
  252. // value of 0 represents the whitespace category while other values represent
  253. // different word categories.
  254. type categorizer func(rune) int
  255. // Move the dot left one word, using the word flavor described by the
  256. // categorizer.
  257. func moveDotLeftGeneralWord(categorize categorizer, buffer string, dot int) int {
  258. // skip trailing whitespaces left of dot
  259. pos := skipWsLeft(categorize, buffer, dot)
  260. // skip this word
  261. pos = skipSameCatLeft(categorize, buffer, pos)
  262. return pos
  263. }
  264. // Move the dot right one word, using the word flavor described by the
  265. // categorizer.
  266. func moveDotRightGeneralWord(categorize categorizer, buffer string, dot int) int {
  267. // skip leading whitespaces right of dot
  268. pos := skipWsRight(categorize, buffer, dot)
  269. if pos > dot {
  270. // Dot was within whitespaces, and we have now moved to the start of the
  271. // next word.
  272. return pos
  273. }
  274. // Dot was within a word; skip both the word and whitespaces
  275. // skip this word
  276. pos = skipSameCatRight(categorize, buffer, pos)
  277. // skip remaining whitespace
  278. pos = skipWsRight(categorize, buffer, pos)
  279. return pos
  280. }
  281. // Transposes the words around the cursor, using the word flavor described
  282. // by the categorizer.
  283. func transposeGeneralWord(categorize categorizer, buffer string, dot int) (string, int) {
  284. if strings.TrimFunc(buffer, func(r rune) bool { return categorize(r) == 0 }) == "" {
  285. // buffer contains only whitespace
  286. return buffer, dot
  287. }
  288. // after skipping whitespace, find the end of the right word
  289. pos := skipWsRight(categorize, buffer, dot)
  290. var rightEnd int
  291. if pos == len(buffer) {
  292. // there is only whitespace to the right of the dot
  293. rightEnd = skipWsLeft(categorize, buffer, pos)
  294. } else {
  295. rightEnd = skipSameCatRight(categorize, buffer, pos)
  296. }
  297. // if the dot started in the middle of a word, 'pos' is the same as dot,
  298. // so we should skip word characters to the left to find the start of the
  299. // word
  300. rightStart := skipSameCatLeft(categorize, buffer, rightEnd)
  301. leftEnd := skipWsLeft(categorize, buffer, rightStart)
  302. var leftStart int
  303. if leftEnd == 0 {
  304. // right word is the first word, use it as the left word and find a
  305. // new right word
  306. leftStart = rightStart
  307. leftEnd = rightEnd
  308. rightStart = skipWsRight(categorize, buffer, leftEnd)
  309. if rightStart == len(buffer) {
  310. // there is only one word in the buffer
  311. return buffer, dot
  312. }
  313. rightEnd = skipSameCatRight(categorize, buffer, rightStart)
  314. } else {
  315. leftStart = skipSameCatLeft(categorize, buffer, leftEnd)
  316. }
  317. return buffer[:leftStart] + buffer[rightStart:rightEnd] + buffer[leftEnd:rightStart] + buffer[leftStart:leftEnd] + buffer[rightEnd:], rightEnd
  318. }
  319. // Skips all runes to the left of the dot that belongs to the same category.
  320. func skipSameCatLeft(categorize categorizer, buffer string, pos int) int {
  321. if pos == 0 {
  322. return pos
  323. }
  324. r, _ := utf8.DecodeLastRuneInString(buffer[:pos])
  325. cat := categorize(r)
  326. return skipCatLeft(categorize, cat, buffer, pos)
  327. }
  328. // Skips whitespaces to the left of the dot.
  329. func skipWsLeft(categorize categorizer, buffer string, pos int) int {
  330. return skipCatLeft(categorize, 0, buffer, pos)
  331. }
  332. func skipCatLeft(categorize categorizer, cat int, buffer string, pos int) int {
  333. left := strings.TrimRightFunc(buffer[:pos], func(r rune) bool {
  334. return categorize(r) == cat
  335. })
  336. return len(left)
  337. }
  338. // Skips all runes to the right of the dot that belongs to the same
  339. // category.
  340. func skipSameCatRight(categorize categorizer, buffer string, pos int) int {
  341. if pos == len(buffer) {
  342. return pos
  343. }
  344. r, _ := utf8.DecodeRuneInString(buffer[pos:])
  345. cat := categorize(r)
  346. return skipCatRight(categorize, cat, buffer, pos)
  347. }
  348. // Skips whitespaces to the right of the dot.
  349. func skipWsRight(categorize categorizer, buffer string, pos int) int {
  350. return skipCatRight(categorize, 0, buffer, pos)
  351. }
  352. func skipCatRight(categorize categorizer, cat int, buffer string, pos int) int {
  353. right := strings.TrimLeftFunc(buffer[pos:], func(r rune) bool {
  354. return categorize(r) == cat
  355. })
  356. return len(buffer) - len(right)
  357. }