snippet_spec.lua (12198B)
1 ---@diagnostic disable: no-unknown 2 3 local t = require('test.testutil') 4 local n = require('test.functional.testnvim')() 5 6 local buf_lines = n.buf_lines 7 local clear = n.clear 8 local eq = t.eq 9 local exec_lua = n.exec_lua 10 local feed = n.feed 11 local api = n.api 12 local fn = n.fn 13 local matches = t.matches 14 local pcall_err = t.pcall_err 15 local poke_eventloop = n.poke_eventloop 16 local retry = t.retry 17 18 describe('vim.snippet', function() 19 before_each(function() 20 clear() 21 exec_lua(function() 22 local function set_snippet_jump(direction, key) 23 vim.keymap.set({ 'i', 's' }, key, function() 24 if vim.snippet.active({ direction = direction }) then 25 return string.format('<Cmd>lua vim.snippet.jump(%d)<CR>', direction) 26 else 27 return key 28 end 29 end, { silent = true, expr = true }) 30 end 31 32 set_snippet_jump(1, '<Tab>') 33 set_snippet_jump(-1, '<S-Tab>') 34 end) 35 end) 36 37 --- @param snippet string[] 38 --- @param expected string[] 39 --- @param settings? string 40 --- @param prefix? string 41 local function test_expand_success(snippet, expected, settings, prefix) 42 if settings then 43 exec_lua(settings) 44 end 45 if prefix then 46 feed('i' .. prefix) 47 end 48 exec_lua('vim.snippet.expand(...)', table.concat(snippet, '\n')) 49 eq(expected, buf_lines(0)) 50 end 51 52 local function wait_for_pum() 53 retry(nil, nil, function() 54 eq(1, fn.pumvisible()) 55 end) 56 end 57 58 --- @param snippet string 59 --- @param err string 60 local function test_expand_fail(snippet, err) 61 matches(err, pcall_err(exec_lua, string.format('vim.snippet.expand("%s")', snippet))) 62 end 63 64 it('adds base indentation to inserted text', function() 65 test_expand_success( 66 { 'function $1($2)', ' $0', 'end' }, 67 { ' function ()', ' ', ' end' }, 68 '', 69 ' ' 70 ) 71 end) 72 73 it('adds indentation based on the start of snippet lines', function() 74 local curbuf = api.nvim_get_current_buf() 75 76 test_expand_success({ 'if $1 then', ' $0', 'end' }, { 'if then', ' ', 'end' }) 77 78 -- Regression test: #29658 79 api.nvim_buf_set_lines(curbuf, 0, -1, false, {}) 80 test_expand_success({ '${1:foo^bar}\n' }, { 'foo^bar', '' }) 81 82 -- Regression test: #30950 83 api.nvim_buf_set_lines(curbuf, 0, -1, false, {}) 84 test_expand_success({ 'a^ b$1', 'b$2', 'd' }, { 'a^ b', 'b', 'd' }) 85 end) 86 87 it('replaces tabs with spaces when expandtab is set', function() 88 test_expand_success( 89 { 'function $1($2)', '\t$0', 'end' }, 90 { 'function ()', ' ', 'end' }, 91 [[ 92 vim.o.expandtab = true 93 vim.o.shiftwidth = 2 94 ]] 95 ) 96 end) 97 98 it('respects tabs when expandtab is not set', function() 99 test_expand_success( 100 { 'function $1($2)', '\t$0', 'end' }, 101 { 'function ()', '\t', 'end' }, 102 'vim.o.expandtab = false' 103 ) 104 end) 105 106 it('inserts known variable value', function() 107 test_expand_success({ '; print($TM_CURRENT_LINE)' }, { 'foo; print(foo)' }, nil, 'foo') 108 end) 109 110 it('uses default when variable is not set', function() 111 test_expand_success({ 'print(${TM_CURRENT_WORD:foo})' }, { 'print(foo)' }) 112 end) 113 114 it('replaces unknown variables by placeholders', function() 115 test_expand_success({ 'print($UNKNOWN)' }, { 'print(UNKNOWN)' }) 116 end) 117 118 it('highlights active tabstop with SnippetTabstopActive', function() 119 local function get_extmark_details(col, end_col) 120 return api.nvim_buf_get_extmarks(0, -1, { 0, col }, { 0, end_col }, { details = true })[1][4] 121 end 122 123 test_expand_success({ 'local ${1:name} = ${2:value}' }, { 'local name = value' }) 124 eq('SnippetTabstopActive', get_extmark_details(6, 10).hl_group) 125 eq('SnippetTabstop', get_extmark_details(13, 18).hl_group) 126 feed('<Tab>') 127 poke_eventloop() 128 eq('SnippetTabstop', get_extmark_details(6, 10).hl_group) 129 eq('SnippetTabstopActive', get_extmark_details(13, 18).hl_group) 130 end) 131 132 it('does not jump outside snippet range', function() 133 test_expand_success({ 'function $1($2)', ' $0', 'end' }, { 'function ()', ' ', 'end' }) 134 eq(false, exec_lua('return vim.snippet.active({ direction = -1 })')) 135 feed('<Tab><Tab>i') 136 eq(false, exec_lua('return vim.snippet.active( { direction = 1 })')) 137 end) 138 139 it('navigates backwards', function() 140 test_expand_success({ 'function $1($2) end' }, { 'function () end' }) 141 feed('<Tab><S-Tab>foo') 142 eq({ 'function foo() end' }, buf_lines(0)) 143 end) 144 145 it('visits all tabstops', function() 146 local function cursor() 147 return exec_lua('return vim.api.nvim_win_get_cursor(0)') 148 end 149 150 test_expand_success({ 'function $1($2)', ' $0', 'end' }, { 'function ()', ' ', 'end' }) 151 eq({ 1, 9 }, cursor()) 152 feed('<Tab>') 153 eq({ 1, 10 }, cursor()) 154 feed('<Tab>') 155 eq({ 2, 2 }, cursor()) 156 end) 157 158 it('syncs text of tabstops with equal indexes', function() 159 test_expand_success({ 'var double = ${1:x} + ${1:x}' }, { 'var double = x + x' }) 160 feed('123') 161 eq({ 'var double = 123 + 123' }, buf_lines(0)) 162 end) 163 164 it('cancels session with changes outside the snippet', function() 165 test_expand_success({ 'print($1)' }, { 'print()' }) 166 feed('<Esc>O-- A comment') 167 eq(false, exec_lua('return vim.snippet.active()')) 168 eq({ '-- A comment', 'print()' }, buf_lines(0)) 169 end) 170 171 it('handles non-consecutive tabstops', function() 172 test_expand_success({ 'class $1($3) {', ' $0', '}' }, { 'class () {', ' ', '}' }) 173 feed('Foo') -- First tabstop 174 feed('<Tab><Tab>') -- Jump to $0 175 feed('// Inside') -- Insert text 176 eq({ 'class Foo() {', ' // Inside', '}' }, buf_lines(0)) 177 end) 178 179 it('handles directly adjacent tabstops (ascending order)', function() 180 test_expand_success({ '${1:one}${2:-two}${3:-three}' }, { 'one-two-three' }) 181 feed('1') 182 feed('<Tab>') 183 poke_eventloop() 184 feed('2') 185 feed('<Tab>') 186 poke_eventloop() 187 feed('3') 188 feed('<Tab>') 189 poke_eventloop() 190 eq({ '123' }, buf_lines(0)) 191 end) 192 193 it('handles directly adjacent tabstops (descending order)', function() 194 test_expand_success({ '${3:three}${2:-two}${1:-one}' }, { 'three-two-one' }) 195 feed('1') 196 feed('<Tab>') 197 poke_eventloop() 198 feed('2') 199 feed('<Tab>') 200 poke_eventloop() 201 feed('3') 202 feed('<Tab>') 203 poke_eventloop() 204 eq({ '321' }, buf_lines(0)) 205 end) 206 207 it('handles directly adjacent tabstops (mixed order)', function() 208 test_expand_success({ '${3:three}${1:-one}${2:-two}' }, { 'three-one-two' }) 209 feed('1') 210 feed('<Tab>') 211 poke_eventloop() 212 feed('2') 213 feed('<Tab>') 214 poke_eventloop() 215 feed('3') 216 feed('<Tab>') 217 poke_eventloop() 218 eq({ '312' }, buf_lines(0)) 219 end) 220 221 it('handles multiline placeholders', function() 222 test_expand_success( 223 { 'public void foo() {', ' ${0:// TODO Auto-generated', ' throw;}', '}' }, 224 { 'public void foo() {', ' // TODO Auto-generated', ' throw;', '}' } 225 ) 226 end) 227 228 it('inserts placeholder in all tabstops when the first tabstop has the placeholder', function() 229 test_expand_success( 230 { 'for (${1:int} ${2:x} = ${3:0}; $2 < ${4:N}; $2++) {', ' $0', '}' }, 231 { 'for (int x = 0; x < N; x++) {', ' ', '}' } 232 ) 233 end) 234 235 it('inserts placeholder in all tabstops when a later tabstop has the placeholder', function() 236 test_expand_success( 237 { 'for (${1:int} $2 = ${3:0}; ${2:x} < ${4:N}; $2++) {', ' $0', '}' }, 238 { 'for (int x = 0; x < N; x++) {', ' ', '}' } 239 ) 240 end) 241 242 it('errors with multiple placeholders for the same index', function() 243 test_expand_fail( 244 'class ${1:Foo} { void ${1:foo}() {} }', 245 'multiple placeholders for tabstop $1' 246 ) 247 end) 248 249 it('errors with multiple $0 tabstops', function() 250 test_expand_fail('function $1() { $0 }$0', 'multiple $0 tabstops') 251 end) 252 253 it('cancels session when deleting the snippet', function() 254 test_expand_success( 255 { 'local function $1()', ' $0', 'end' }, 256 { 'local function ()', ' ', 'end' } 257 ) 258 feed('<esc>Vjjd') 259 eq(false, exec_lua('return vim.snippet.active()')) 260 end) 261 262 it('cancels session when inserting outside snippet region', function() 263 feed('i<cr>') 264 test_expand_success( 265 { 'local function $1()', ' $0', 'end' }, 266 { '', 'local function ()', ' ', 'end' } 267 ) 268 feed('<esc>O-- A comment') 269 eq(false, exec_lua('return vim.snippet.active()')) 270 end) 271 272 it('stop session when jumping to $0', function() 273 test_expand_success({ 'local ${1:name} = ${2:value}$0' }, { 'local name = value' }) 274 -- Jump to $2 275 feed('<Tab>') 276 poke_eventloop() 277 -- Jump to $0 (stop snippet) 278 feed('<Tab>') 279 poke_eventloop() 280 -- Insert literal \t 281 feed('<Tab>') 282 poke_eventloop() 283 eq({ 'local name = value\t' }, buf_lines(0)) 284 end) 285 286 it('inserts choice', function() 287 test_expand_success({ 'console.${1|assert,log,error|}()' }, { 'console.()' }) 288 wait_for_pum() 289 feed('<Down><C-y>') 290 eq({ 'console.log()' }, buf_lines(0)) 291 end) 292 293 it('closes the choice completion menu when jumping', function() 294 test_expand_success({ 'console.${1|assert,log,error|}($2)' }, { 'console.()' }) 295 wait_for_pum() 296 exec_lua('vim.snippet.jump(1)') 297 eq(0, fn.pumvisible()) 298 end) 299 300 it('jumps to next tabstop after inserting choice', function() 301 test_expand_success( 302 { '${1|public,protected,private|} function ${2:name}() {', '\t$0', '}' }, 303 { ' function name() {', '\t', '}' } 304 ) 305 wait_for_pum() 306 feed('<C-y><Tab>') 307 poke_eventloop() 308 feed('foo') 309 eq({ 'public function foo() {', '\t', '}' }, buf_lines(0)) 310 end) 311 312 it('does not change the chosen text when jumping back to a choice tabstop', function() 313 test_expand_success( 314 { '${1|public,protected,private|} function ${2:name}() {', '\t$0', '}' }, 315 { ' function name() {', '\t', '}' } 316 ) 317 wait_for_pum() 318 feed('<C-n><C-y><Tab>') 319 poke_eventloop() 320 feed('<S-Tab>') 321 poke_eventloop() 322 wait_for_pum() 323 feed('<Tab>') 324 poke_eventloop() 325 feed('foo') 326 eq({ 'protected function foo() {', '\t', '}' }, buf_lines(0)) 327 end) 328 329 it('jumps through adjacent tabstops', function() 330 test_expand_success( 331 { 'for i=1,${1:to}${2:,step} do\n\t$3\nend' }, 332 { 'for i=1,to,step do', '\t', 'end' } 333 ) 334 feed('10') 335 feed('<Tab>') 336 poke_eventloop() 337 feed(',2') 338 -- Make sure changes on previous tabstops does not change following ones 339 feed('<S-Tab>') 340 poke_eventloop() 341 feed('20') 342 eq({ 'for i=1,20,2 do', '\t', 'end' }, buf_lines(0)) 343 end) 344 345 it('updates snippet state when built-in completion menu is visible', function() 346 test_expand_success({ '$1 = function($2)\nend' }, { ' = function()', 'end' }) 347 -- Show the completion menu. 348 feed('<C-n>') 349 -- Make sure no item is selected. 350 feed('<C-p>') 351 -- Jump forward (the 2nd tabstop). 352 exec_lua('vim.snippet.jump(1)') 353 feed('foo') 354 eq({ ' = function(foo)', 'end' }, buf_lines(0)) 355 end) 356 357 it('correctly indents with newlines', function() 358 local curbuf = api.nvim_get_current_buf() 359 test_expand_success( 360 { 'function($2)\n\t$3\nend' }, 361 { 'function()', ' ', 'end' }, 362 [[ 363 vim.opt.sw = 2 364 vim.opt.expandtab = true 365 ]] 366 ) 367 api.nvim_buf_set_lines(curbuf, 0, -1, false, {}) 368 test_expand_success( 369 { 'function($2)\n$3\nend' }, 370 { 'function()', '', 'end' }, 371 [[ 372 vim.opt.sw = 2 373 vim.opt.expandtab = true 374 ]] 375 ) 376 api.nvim_buf_set_lines(curbuf, 0, -1, false, {}) 377 test_expand_success( 378 { 'func main() {\n\t$1\n}' }, 379 { 'func main() {', '\t', '}' }, 380 [[ 381 vim.opt.sw = 4 382 vim.opt.ts = 4 383 vim.opt.expandtab = false 384 ]] 385 ) 386 api.nvim_buf_set_lines(curbuf, 0, -1, false, {}) 387 test_expand_success( 388 { '${1:name} :: ${2}\n${1:name} ${3}= ${0:undefined}' }, 389 { 390 'name :: ', 391 'name = undefined', 392 }, 393 [[ 394 vim.opt.sw = 4 395 vim.opt.ts = 4 396 vim.opt.expandtab = false 397 ]] 398 ) 399 end) 400 401 it('correct visual selection with multi-byte text', function() 402 test_expand_success({ 'function(${1:var})' }, { '口口function(var)' }, nil, '口口') 403 feed('foo') 404 eq({ '口口function(foo)' }, buf_lines(0)) 405 end) 406 end)