snippet_spec.lua 9.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313
  1. ---@diagnostic disable: no-unknown
  2. local t = require('test.testutil')
  3. local n = require('test.functional.testnvim')()
  4. local buf_lines = n.buf_lines
  5. local clear = n.clear
  6. local eq = t.eq
  7. local exec_lua = n.exec_lua
  8. local feed = n.feed
  9. local api = n.api
  10. local fn = n.fn
  11. local matches = t.matches
  12. local pcall_err = t.pcall_err
  13. local poke_eventloop = n.poke_eventloop
  14. local retry = t.retry
  15. describe('vim.snippet', function()
  16. before_each(function()
  17. clear()
  18. end)
  19. after_each(clear)
  20. --- @param snippet string[]
  21. --- @param expected string[]
  22. --- @param settings? string
  23. --- @param prefix? string
  24. local function test_expand_success(snippet, expected, settings, prefix)
  25. if settings then
  26. exec_lua(settings)
  27. end
  28. if prefix then
  29. feed('i' .. prefix)
  30. end
  31. exec_lua('vim.snippet.expand(...)', table.concat(snippet, '\n'))
  32. eq(expected, buf_lines(0))
  33. end
  34. local function wait_for_pum()
  35. retry(nil, nil, function()
  36. eq(1, fn.pumvisible())
  37. end)
  38. end
  39. --- @param snippet string
  40. --- @param err string
  41. local function test_expand_fail(snippet, err)
  42. matches(err, pcall_err(exec_lua, string.format('vim.snippet.expand("%s")', snippet)))
  43. end
  44. it('adds base indentation to inserted text', function()
  45. test_expand_success(
  46. { 'function $1($2)', ' $0', 'end' },
  47. { ' function ()', ' ', ' end' },
  48. '',
  49. ' '
  50. )
  51. end)
  52. it('adds indentation based on the start of snippet lines', function()
  53. local curbuf = api.nvim_get_current_buf()
  54. test_expand_success({ 'if $1 then', ' $0', 'end' }, { 'if then', ' ', 'end' })
  55. -- Regression test: #29658
  56. api.nvim_buf_set_lines(curbuf, 0, -1, false, {})
  57. test_expand_success({ '${1:foo^bar}\n' }, { 'foo^bar', '' })
  58. end)
  59. it('replaces tabs with spaces when expandtab is set', function()
  60. test_expand_success(
  61. { 'function $1($2)', '\t$0', 'end' },
  62. { 'function ()', ' ', 'end' },
  63. [[
  64. vim.o.expandtab = true
  65. vim.o.shiftwidth = 2
  66. ]]
  67. )
  68. end)
  69. it('respects tabs when expandtab is not set', function()
  70. test_expand_success(
  71. { 'function $1($2)', '\t$0', 'end' },
  72. { 'function ()', '\t', 'end' },
  73. 'vim.o.expandtab = false'
  74. )
  75. end)
  76. it('inserts known variable value', function()
  77. test_expand_success({ '; print($TM_CURRENT_LINE)' }, { 'foo; print(foo)' }, nil, 'foo')
  78. end)
  79. it('uses default when variable is not set', function()
  80. test_expand_success({ 'print(${TM_CURRENT_WORD:foo})' }, { 'print(foo)' })
  81. end)
  82. it('replaces unknown variables by placeholders', function()
  83. test_expand_success({ 'print($UNKNOWN)' }, { 'print(UNKNOWN)' })
  84. end)
  85. it('does not jump outside snippet range', function()
  86. test_expand_success({ 'function $1($2)', ' $0', 'end' }, { 'function ()', ' ', 'end' })
  87. eq(false, exec_lua('return vim.snippet.active({ direction = -1 })'))
  88. feed('<Tab><Tab>i')
  89. eq(false, exec_lua('return vim.snippet.active( { direction = 1 })'))
  90. end)
  91. it('navigates backwards', function()
  92. test_expand_success({ 'function $1($2) end' }, { 'function () end' })
  93. feed('<Tab><S-Tab>foo')
  94. eq({ 'function foo() end' }, buf_lines(0))
  95. end)
  96. it('visits all tabstops', function()
  97. local function cursor()
  98. return exec_lua('return vim.api.nvim_win_get_cursor(0)')
  99. end
  100. test_expand_success({ 'function $1($2)', ' $0', 'end' }, { 'function ()', ' ', 'end' })
  101. eq({ 1, 9 }, cursor())
  102. feed('<Tab>')
  103. eq({ 1, 10 }, cursor())
  104. feed('<Tab>')
  105. eq({ 2, 2 }, cursor())
  106. end)
  107. it('syncs text of tabstops with equal indexes', function()
  108. test_expand_success({ 'var double = ${1:x} + ${1:x}' }, { 'var double = x + x' })
  109. feed('123')
  110. eq({ 'var double = 123 + 123' }, buf_lines(0))
  111. end)
  112. it('cancels session with changes outside the snippet', function()
  113. test_expand_success({ 'print($1)' }, { 'print()' })
  114. feed('<Esc>O-- A comment')
  115. eq(false, exec_lua('return vim.snippet.active()'))
  116. eq({ '-- A comment', 'print()' }, buf_lines(0))
  117. end)
  118. it('handles non-consecutive tabstops', function()
  119. test_expand_success({ 'class $1($3) {', ' $0', '}' }, { 'class () {', ' ', '}' })
  120. feed('Foo') -- First tabstop
  121. feed('<Tab><Tab>') -- Jump to $0
  122. feed('// Inside') -- Insert text
  123. eq({ 'class Foo() {', ' // Inside', '}' }, buf_lines(0))
  124. end)
  125. it('handles multiline placeholders', function()
  126. test_expand_success(
  127. { 'public void foo() {', ' ${0:// TODO Auto-generated', ' throw;}', '}' },
  128. { 'public void foo() {', ' // TODO Auto-generated', ' throw;', '}' }
  129. )
  130. end)
  131. it('inserts placeholder in all tabstops when the first tabstop has the placeholder', function()
  132. test_expand_success(
  133. { 'for (${1:int} ${2:x} = ${3:0}; $2 < ${4:N}; $2++) {', ' $0', '}' },
  134. { 'for (int x = 0; x < N; x++) {', ' ', '}' }
  135. )
  136. end)
  137. it('inserts placeholder in all tabstops when a later tabstop has the placeholder', function()
  138. test_expand_success(
  139. { 'for (${1:int} $2 = ${3:0}; ${2:x} < ${4:N}; $2++) {', ' $0', '}' },
  140. { 'for (int x = 0; x < N; x++) {', ' ', '}' }
  141. )
  142. end)
  143. it('errors with multiple placeholders for the same index', function()
  144. test_expand_fail(
  145. 'class ${1:Foo} { void ${1:foo}() {} }',
  146. 'multiple placeholders for tabstop $1'
  147. )
  148. end)
  149. it('errors with multiple $0 tabstops', function()
  150. test_expand_fail('function $1() { $0 }$0', 'multiple $0 tabstops')
  151. end)
  152. it('cancels session when deleting the snippet', function()
  153. test_expand_success(
  154. { 'local function $1()', ' $0', 'end' },
  155. { 'local function ()', ' ', 'end' }
  156. )
  157. feed('<esc>Vjjd')
  158. eq(false, exec_lua('return vim.snippet.active()'))
  159. end)
  160. it('cancels session when inserting outside snippet region', function()
  161. feed('i<cr>')
  162. test_expand_success(
  163. { 'local function $1()', ' $0', 'end' },
  164. { '', 'local function ()', ' ', 'end' }
  165. )
  166. feed('<esc>O-- A comment')
  167. eq(false, exec_lua('return vim.snippet.active()'))
  168. end)
  169. it('inserts choice', function()
  170. test_expand_success({ 'console.${1|assert,log,error|}()' }, { 'console.()' })
  171. wait_for_pum()
  172. feed('<Down><C-y>')
  173. eq({ 'console.log()' }, buf_lines(0))
  174. end)
  175. it('closes the choice completion menu when jumping', function()
  176. test_expand_success({ 'console.${1|assert,log,error|}($2)' }, { 'console.()' })
  177. wait_for_pum()
  178. exec_lua('vim.snippet.jump(1)')
  179. eq(0, fn.pumvisible())
  180. end)
  181. it('jumps to next tabstop after inserting choice', function()
  182. test_expand_success(
  183. { '${1|public,protected,private|} function ${2:name}() {', '\t$0', '}' },
  184. { ' function name() {', '\t', '}' }
  185. )
  186. wait_for_pum()
  187. feed('<C-y><Tab>')
  188. poke_eventloop()
  189. feed('foo')
  190. eq({ 'public function foo() {', '\t', '}' }, buf_lines(0))
  191. end)
  192. it('jumps through adjacent tabstops', function()
  193. test_expand_success(
  194. { 'for i=1,${1:to}${2:,step} do\n\t$3\nend' },
  195. { 'for i=1,to,step do', '\t', 'end' }
  196. )
  197. feed('10')
  198. feed('<Tab>')
  199. poke_eventloop()
  200. feed(',2')
  201. eq({ 'for i=1,10,2 do', '\t', 'end' }, buf_lines(0))
  202. end)
  203. it('updates snippet state when built-in completion menu is visible', function()
  204. test_expand_success({ '$1 = function($2)\nend' }, { ' = function()', 'end' })
  205. -- Show the completion menu.
  206. feed('<C-n>')
  207. -- Make sure no item is selected.
  208. feed('<C-p>')
  209. -- Jump forward (the 2nd tabstop).
  210. exec_lua('vim.snippet.jump(1)')
  211. feed('foo')
  212. eq({ ' = function(foo)', 'end' }, buf_lines(0))
  213. end)
  214. it('correctly indents with newlines', function()
  215. local curbuf = api.nvim_get_current_buf()
  216. test_expand_success(
  217. { 'function($2)\n\t$3\nend' },
  218. { 'function()', ' ', 'end' },
  219. [[
  220. vim.opt.sw = 2
  221. vim.opt.expandtab = true
  222. ]]
  223. )
  224. api.nvim_buf_set_lines(curbuf, 0, -1, false, {})
  225. test_expand_success(
  226. { 'function($2)\n$3\nend' },
  227. { 'function()', '', 'end' },
  228. [[
  229. vim.opt.sw = 2
  230. vim.opt.expandtab = true
  231. ]]
  232. )
  233. api.nvim_buf_set_lines(curbuf, 0, -1, false, {})
  234. test_expand_success(
  235. { 'func main() {\n\t$1\n}' },
  236. { 'func main() {', '\t', '}' },
  237. [[
  238. vim.opt.sw = 4
  239. vim.opt.ts = 4
  240. vim.opt.expandtab = false
  241. ]]
  242. )
  243. api.nvim_buf_set_lines(curbuf, 0, -1, false, {})
  244. test_expand_success(
  245. { '${1:name} :: ${2}\n${1:name} ${3}= ${0:undefined}' },
  246. {
  247. 'name :: ',
  248. 'name = undefined',
  249. },
  250. [[
  251. vim.opt.sw = 4
  252. vim.opt.ts = 4
  253. vim.opt.expandtab = false
  254. ]]
  255. )
  256. end)
  257. it('restores snippet navigation keymaps', function()
  258. -- Create a buffer keymap in insert mode that deletes all lines.
  259. local curbuf = api.nvim_get_current_buf()
  260. exec_lua('vim.api.nvim_buf_set_keymap(..., "i", "<Tab>", "<cmd>normal ggdG<cr>", {})', curbuf)
  261. test_expand_success({ 'var $1 = $2' }, { 'var = ' })
  262. -- While the snippet is active, <Tab> should navigate between tabstops.
  263. feed('x')
  264. poke_eventloop()
  265. feed('<Tab>0')
  266. eq({ 'var x = 0' }, buf_lines(0))
  267. exec_lua('vim.snippet.stop()')
  268. -- After exiting the snippet, the buffer keymap should be restored.
  269. feed('<Esc>O<cr><Tab>')
  270. eq({ '' }, buf_lines(0))
  271. end)
  272. end)