neovim

Neovim text editor
git clone https://git.dasho.dev/neovim.git
Log | Files | Refs | README

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)