neovim

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

inccommand_user_spec.lua (23315B)


      1 local t = require('test.testutil')
      2 local n = require('test.functional.testnvim')()
      3 local Screen = require('test.functional.ui.screen')
      4 
      5 local api = n.api
      6 local clear = n.clear
      7 local eq = t.eq
      8 local exec_lua = n.exec_lua
      9 local insert = n.insert
     10 local feed = n.feed
     11 local command = n.command
     12 local assert_alive = n.assert_alive
     13 
     14 -- Implements a :Replace command that works like :substitute and has multibuffer support.
     15 local setup_replace_cmd = [[
     16  local function show_replace_preview(use_preview_win, preview_ns, preview_buf, matches)
     17    -- Find the width taken by the largest line number, used for padding the line numbers
     18    local highest_lnum = math.max(matches[#matches][1], 1)
     19    local highest_lnum_width = math.floor(math.log10(highest_lnum))
     20    local preview_buf_line = 0
     21    local multibuffer = #matches > 1
     22 
     23    for _, match in ipairs(matches) do
     24      local buf = match[1]
     25      local buf_matches = match[2]
     26 
     27      if multibuffer and #buf_matches > 0 and use_preview_win then
     28        local bufname = vim.api.nvim_buf_get_name(buf)
     29 
     30        if bufname == "" then
     31          bufname = string.format("Buffer #%d", buf)
     32        end
     33 
     34        vim.api.nvim_buf_set_lines(
     35          preview_buf,
     36          preview_buf_line,
     37          preview_buf_line,
     38          0,
     39          { bufname .. ':' }
     40        )
     41 
     42        preview_buf_line = preview_buf_line + 1
     43      end
     44 
     45      for _, buf_match in ipairs(buf_matches) do
     46        local lnum = buf_match[1]
     47        local line_matches = buf_match[2]
     48        local prefix
     49 
     50        if use_preview_win then
     51          prefix = string.format(
     52            '|%s%d| ',
     53            string.rep(' ', highest_lnum_width - math.floor(math.log10(lnum))),
     54            lnum
     55          )
     56 
     57          vim.api.nvim_buf_set_lines(
     58            preview_buf,
     59            preview_buf_line,
     60            preview_buf_line,
     61            0,
     62            { prefix .. vim.api.nvim_buf_get_lines(buf, lnum - 1, lnum, false)[1] }
     63          )
     64        end
     65 
     66        for _, line_match in ipairs(line_matches) do
     67          vim.api.nvim_buf_add_highlight(
     68            buf,
     69            preview_ns,
     70            'Substitute',
     71            lnum - 1,
     72            line_match[1],
     73            line_match[2]
     74          )
     75 
     76          if use_preview_win then
     77            vim.api.nvim_buf_add_highlight(
     78              preview_buf,
     79              preview_ns,
     80              'Substitute',
     81              preview_buf_line,
     82              #prefix + line_match[1],
     83              #prefix + line_match[2]
     84            )
     85          end
     86        end
     87 
     88        preview_buf_line = preview_buf_line + 1
     89      end
     90    end
     91 
     92    if use_preview_win then
     93      return 2
     94    else
     95      return 1
     96    end
     97  end
     98 
     99  local function do_replace(opts, preview, preview_ns, preview_buf)
    100    local pat1 = opts.fargs[1]
    101 
    102    if not pat1 then return end
    103 
    104    local pat2 = opts.fargs[2] or ''
    105    local line1 = opts.line1
    106    local line2 = opts.line2
    107    local matches = {}
    108 
    109    -- Get list of valid and listed buffers
    110    local buffers = vim.tbl_filter(
    111        function(buf)
    112          if not (vim.api.nvim_buf_is_valid(buf) and vim.bo[buf].buflisted and buf ~= preview_buf)
    113          then
    114            return false
    115          end
    116 
    117          -- Check if there's at least one window using the buffer
    118          for _, win in ipairs(vim.api.nvim_tabpage_list_wins(0)) do
    119            if vim.api.nvim_win_get_buf(win) == buf then
    120              return true
    121            end
    122          end
    123 
    124          return false
    125        end,
    126        vim.api.nvim_list_bufs()
    127    )
    128 
    129    for _, buf in ipairs(buffers) do
    130      local lines = vim.api.nvim_buf_get_lines(buf, line1 - 1, line2, false)
    131      local buf_matches = {}
    132 
    133      for i, line in ipairs(lines) do
    134        local startidx, endidx = 0, 0
    135        local line_matches = {}
    136        local num = 1
    137 
    138        while startidx ~= -1 do
    139          local match = vim.fn.matchstrpos(line, pat1, 0, num)
    140          startidx, endidx = match[2], match[3]
    141 
    142          if startidx ~= -1 then
    143            line_matches[#line_matches+1] = { startidx, endidx }
    144          end
    145 
    146          num = num + 1
    147        end
    148 
    149        if #line_matches > 0 then
    150          buf_matches[#buf_matches+1] = { line1 + i - 1, line_matches }
    151        end
    152      end
    153 
    154      local new_lines = {}
    155 
    156      for _, buf_match in ipairs(buf_matches) do
    157        local lnum = buf_match[1]
    158        local line_matches = buf_match[2]
    159        local line = lines[lnum - line1 + 1]
    160        local pat_width_differences = {}
    161 
    162        -- If previewing, only replace the text in current buffer if pat2 isn't empty
    163        -- Otherwise, always replace the text
    164        if pat2 ~= '' or not preview then
    165          if preview then
    166            for _, line_match in ipairs(line_matches) do
    167              local startidx, endidx = unpack(line_match)
    168              local pat_match = line:sub(startidx + 1, endidx)
    169 
    170              pat_width_differences[#pat_width_differences+1] =
    171                #vim.fn.substitute(pat_match, pat1, pat2, 'g') - #pat_match
    172            end
    173          end
    174 
    175          new_lines[lnum] = vim.fn.substitute(line, pat1, pat2, 'g')
    176        end
    177 
    178        -- Highlight the matches if previewing
    179        if preview then
    180          local idx_offset = 0
    181          for i, line_match in ipairs(line_matches) do
    182            local startidx, endidx = unpack(line_match)
    183            -- Starting index of replacement text
    184            local repl_startidx = startidx + idx_offset
    185            -- Ending index of the replacement text (if pat2 isn't empty)
    186            local repl_endidx
    187 
    188            if pat2 ~= '' then
    189              repl_endidx = endidx + idx_offset + pat_width_differences[i]
    190            else
    191              repl_endidx = endidx + idx_offset
    192            end
    193 
    194            if pat2 ~= '' then
    195              idx_offset = idx_offset + pat_width_differences[i]
    196            end
    197 
    198            line_matches[i] = { repl_startidx, repl_endidx }
    199          end
    200        end
    201      end
    202 
    203      for lnum, line in pairs(new_lines) do
    204        vim.api.nvim_buf_set_lines(buf, lnum - 1, lnum, false, { line })
    205      end
    206 
    207      matches[#matches+1] = { buf, buf_matches }
    208    end
    209 
    210    if preview then
    211      local lnum = vim.api.nvim_win_get_cursor(0)[1]
    212      -- Use preview window only if preview buffer is provided and range isn't just the current line
    213      local use_preview_win = (preview_buf ~= nil) and (line1 ~= lnum or line2 ~= lnum)
    214      return show_replace_preview(use_preview_win, preview_ns, preview_buf, matches)
    215    end
    216  end
    217 
    218  local function replace(opts)
    219    do_replace(opts, false)
    220  end
    221 
    222  local function replace_preview(opts, preview_ns, preview_buf)
    223    return do_replace(opts, true, preview_ns, preview_buf)
    224  end
    225 
    226  -- ":<range>Replace <pat1> <pat2>"
    227  -- Replaces all occurrences of <pat1> in <range> with <pat2>
    228  vim.api.nvim_create_user_command(
    229    'Replace',
    230    replace,
    231    { nargs = '*', range = '%', addr = 'lines',
    232      preview = replace_preview }
    233  )
    234 ]]
    235 
    236 describe("'inccommand' for user commands", function()
    237  local screen
    238 
    239  before_each(function()
    240    clear()
    241    screen = Screen.new(40, 17)
    242    exec_lua(setup_replace_cmd)
    243    command('set cmdwinheight=5')
    244    insert [[
    245      text on line 1
    246      more text on line 2
    247      oh no, even more text
    248      will the text ever stop
    249      oh well
    250      did the text stop
    251      why won't it stop
    252      make the text stop
    253    ]]
    254  end)
    255 
    256  it("can preview 'nomodifiable' buffer", function()
    257    exec_lua([[
    258      vim.api.nvim_create_user_command("PreviewTest", function() end, {
    259        preview = function(ev)
    260          vim.bo.modifiable = true
    261          vim.api.nvim_buf_set_lines(0, 0, -1, false, {"cats"})
    262          return 2
    263        end,
    264      })
    265    ]])
    266    command('set inccommand=split')
    267 
    268    command('set nomodifiable')
    269    eq(false, api.nvim_get_option_value('modifiable', { buf = 0 }))
    270 
    271    feed(':PreviewTest')
    272 
    273    screen:expect([[
    274      cats                                    |
    275      {1:~                                       }|*8
    276      {3:[No Name] [+]                           }|
    277                                              |
    278      {1:~                                       }|*4
    279      {2:[Preview]                               }|
    280      :PreviewTest^                            |
    281    ]])
    282    feed('<Esc>')
    283    screen:expect([[
    284      text on line 1                          |
    285      more text on line 2                     |
    286      oh no, even more text                   |
    287      will the text ever stop                 |
    288      oh well                                 |
    289      did the text stop                       |
    290      why won't it stop                       |
    291      make the text stop                      |
    292      ^                                        |
    293      {1:~                                       }|*7
    294                                              |
    295    ]])
    296 
    297    eq(false, api.nvim_get_option_value('modifiable', { buf = 0 }))
    298  end)
    299 
    300  it('works with inccommand=nosplit', function()
    301    command('set inccommand=nosplit')
    302    feed(':Replace text cats')
    303    screen:expect([[
    304      {10:cats} on line 1                          |
    305      more {10:cats} on line 2                     |
    306      oh no, even more {10:cats}                   |
    307      will the {10:cats} ever stop                 |
    308      oh well                                 |
    309      did the {10:cats} stop                       |
    310      why won't it stop                       |
    311      make the {10:cats} stop                      |
    312                                              |
    313      {1:~                                       }|*7
    314      :Replace text cats^                      |
    315    ]])
    316  end)
    317 
    318  it('works with inccommand=split', function()
    319    command('set inccommand=split')
    320    feed(':Replace text cats')
    321    screen:expect([[
    322      {10:cats} on line 1                          |
    323      more {10:cats} on line 2                     |
    324      oh no, even more {10:cats}                   |
    325      will the {10:cats} ever stop                 |
    326      oh well                                 |
    327      did the {10:cats} stop                       |
    328      why won't it stop                       |
    329      make the {10:cats} stop                      |
    330                                              |
    331      {3:[No Name] [+]                           }|
    332      |1| {10:cats} on line 1                      |
    333      |2| more {10:cats} on line 2                 |
    334      |3| oh no, even more {10:cats}               |
    335      |4| will the {10:cats} ever stop             |
    336      |6| did the {10:cats} stop                   |
    337      {2:[Preview]                               }|
    338      :Replace text cats^                      |
    339    ]])
    340  end)
    341 
    342  it('properly closes preview when inccommand=split', function()
    343    command('set inccommand=split')
    344    feed(':Replace text cats<Esc>')
    345    screen:expect([[
    346      text on line 1                          |
    347      more text on line 2                     |
    348      oh no, even more text                   |
    349      will the text ever stop                 |
    350      oh well                                 |
    351      did the text stop                       |
    352      why won't it stop                       |
    353      make the text stop                      |
    354      ^                                        |
    355      {1:~                                       }|*7
    356                                              |
    357    ]])
    358  end)
    359 
    360  it('properly executes command when inccommand=split', function()
    361    command('set inccommand=split')
    362    feed(':Replace text cats<CR>')
    363    screen:expect([[
    364      cats on line 1                          |
    365      more cats on line 2                     |
    366      oh no, even more cats                   |
    367      will the cats ever stop                 |
    368      oh well                                 |
    369      did the cats stop                       |
    370      why won't it stop                       |
    371      make the cats stop                      |
    372      ^                                        |
    373      {1:~                                       }|*7
    374      :Replace text cats                      |
    375    ]])
    376  end)
    377 
    378  it('shows preview window only when range is not current line', function()
    379    command('set inccommand=split')
    380    feed('gg:.Replace text cats')
    381    screen:expect([[
    382      {10:cats} on line 1                          |
    383      more text on line 2                     |
    384      oh no, even more text                   |
    385      will the text ever stop                 |
    386      oh well                                 |
    387      did the text stop                       |
    388      why won't it stop                       |
    389      make the text stop                      |
    390                                              |
    391      {1:~                                       }|*7
    392      :.Replace text cats^                     |
    393    ]])
    394  end)
    395 
    396  it('no crash on ambiguous command #18825', function()
    397    command('set inccommand=split')
    398    command('command Reply echo 1')
    399    feed(':R')
    400    assert_alive()
    401    feed('e')
    402    assert_alive()
    403  end)
    404 
    405  it('no crash if preview callback changes inccommand option', function()
    406    command('set inccommand=nosplit')
    407    exec_lua([[
    408      vim.api.nvim_create_user_command('Replace', function() end, {
    409        nargs = '*',
    410        preview = function()
    411          vim.api.nvim_set_option_value('inccommand', 'split', {})
    412          return 2
    413        end,
    414      })
    415    ]])
    416    feed(':R')
    417    assert_alive()
    418    feed('e')
    419    assert_alive()
    420  end)
    421 
    422  it('no crash when adding highlight after :substitute #21495', function()
    423    command('set inccommand=nosplit')
    424    exec_lua([[
    425      vim.api.nvim_create_user_command("Crash", function() end, {
    426        preview = function(_, preview_ns, _)
    427          vim.cmd("%s/text/cats/g")
    428          vim.api.nvim_buf_add_highlight(0, preview_ns, "Search", 0, 0, -1)
    429          return 1
    430        end,
    431      })
    432    ]])
    433    feed(':C')
    434    screen:expect([[
    435      {10:cats on line 1}                          |
    436      more cats on line 2                     |
    437      oh no, even more cats                   |
    438      will the cats ever stop                 |
    439      oh well                                 |
    440      did the cats stop                       |
    441      why won't it stop                       |
    442      make the cats stop                      |
    443                                              |
    444      {1:~                                       }|*7
    445      :C^                                      |
    446    ]])
    447    assert_alive()
    448  end)
    449 
    450  it('no crash if preview callback executes undo #20036', function()
    451    command('set inccommand=nosplit')
    452    exec_lua([[
    453      vim.api.nvim_create_user_command('Foo', function() end, {
    454        nargs = '?',
    455        preview = function(_, _, _)
    456          vim.cmd.undo()
    457        end,
    458      })
    459    ]])
    460 
    461    -- Clear undo history
    462    command('set undolevels=-1')
    463    feed('ggyyp')
    464    command('set undolevels=1000')
    465 
    466    feed('yypp:Fo')
    467    assert_alive()
    468    feed('<Esc>:Fo')
    469    assert_alive()
    470  end)
    471 
    472  local function test_preview_break_undo()
    473    command('set inccommand=nosplit')
    474    exec_lua([[
    475      vim.api.nvim_create_user_command('Test', function() end, {
    476        nargs = 1,
    477        preview = function(opts, _, _)
    478          vim.cmd('norm i' .. opts.args)
    479          return 1
    480        end
    481      })
    482    ]])
    483    feed(':Test a.a.a.a.')
    484    screen:expect([[
    485      text on line 1                          |
    486      more text on line 2                     |
    487      oh no, even more text                   |
    488      will the text ever stop                 |
    489      oh well                                 |
    490      did the text stop                       |
    491      why won't it stop                       |
    492      make the text stop                      |
    493      a.a.a.a.                                |
    494      {1:~                                       }|*7
    495      :Test a.a.a.a.^                          |
    496    ]])
    497    feed('<C-V><Esc>u')
    498    screen:expect([[
    499      text on line 1                          |
    500      more text on line 2                     |
    501      oh no, even more text                   |
    502      will the text ever stop                 |
    503      oh well                                 |
    504      did the text stop                       |
    505      why won't it stop                       |
    506      make the text stop                      |
    507      a.a.a.                                  |
    508      {1:~                                       }|*7
    509      :Test a.a.a.a.{18:^[}u^                       |
    510    ]])
    511    feed('<Esc>')
    512    screen:expect([[
    513      text on line 1                          |
    514      more text on line 2                     |
    515      oh no, even more text                   |
    516      will the text ever stop                 |
    517      oh well                                 |
    518      did the text stop                       |
    519      why won't it stop                       |
    520      make the text stop                      |
    521      ^                                        |
    522      {1:~                                       }|*7
    523                                              |
    524    ]])
    525  end
    526 
    527  describe('breaking undo chain in Insert mode works properly', function()
    528    it('when using i_CTRL-G_u #20248', function()
    529      command('inoremap . .<C-G>u')
    530      test_preview_break_undo()
    531    end)
    532 
    533    it('when setting &l:undolevels to itself #24575', function()
    534      command('inoremap . .<Cmd>let &l:undolevels = &l:undolevels<CR>')
    535      test_preview_break_undo()
    536    end)
    537  end)
    538 
    539  it('disables preview if preview buffer cannot be created #27086', function()
    540    command('set inccommand=split')
    541    api.nvim_buf_set_name(0, '[Preview]')
    542    exec_lua([[
    543      vim.api.nvim_create_user_command('Test', function() end, {
    544        nargs = '*',
    545        preview = function(_, _, _)
    546          return 2
    547        end
    548      })
    549    ]])
    550    eq('split', api.nvim_get_option_value('inccommand', {}))
    551    feed(':Test')
    552    eq('nosplit', api.nvim_get_option_value('inccommand', {}))
    553  end)
    554 
    555  it('does not flush intermediate cursor position at end of message grid', function()
    556    exec_lua([[
    557      vim.api.nvim_create_user_command('Test', function() end, {
    558        nargs = '*',
    559        preview = function(_, _, _)
    560          vim.api.nvim_buf_set_text(0, 0, 0, 1, -1, { "Preview" })
    561          vim.cmd.sleep("1m")
    562          return 1
    563        end
    564      })
    565    ]])
    566    local cursor_goto = screen._handle_grid_cursor_goto
    567    screen._handle_grid_cursor_goto = function(...)
    568      cursor_goto(...)
    569      assert(screen._cursor.col < 12)
    570    end
    571    feed(':Test baz<Left><Left>arb')
    572    screen:expect([[
    573      Preview                                 |
    574      oh no, even more text                   |
    575      will the text ever stop                 |
    576      oh well                                 |
    577      did the text stop                       |
    578      why won't it stop                       |
    579      make the text stop                      |
    580                                              |
    581      {1:~                                       }|*8
    582      :Test barb^az                            |
    583    ]])
    584  end)
    585 
    586  it('works when CmdlineChanged calls wildtrigger() #35246', function()
    587    api.nvim_buf_set_text(0, 0, 0, 1, -1, { '' })
    588    exec_lua([[
    589      vim.api.nvim_create_user_command("Repro", function() end, {
    590        nargs = '+',
    591        preview = function(opts, ns, buf)
    592          vim.api.nvim_buf_set_lines(0, 0, -1, true, { opts.args })
    593          return 2
    594        end
    595      })
    596    ]])
    597    command([[autocmd CmdlineChanged [:/\?] call wildtrigger()]])
    598    command('set wildmode=noselect:lastused,full wildoptions=pum')
    599    feed(':Repro ')
    600    screen:expect([[
    601                                              |
    602      {1:~                                       }|*15
    603      :Repro ^                                 |
    604    ]])
    605    feed('a')
    606    screen:expect([[
    607      a                                       |
    608      {1:~                                       }|*15
    609      :Repro a^                                |
    610    ]])
    611    feed('bc')
    612    screen:expect([[
    613      abc                                     |
    614      {1:~                                       }|*15
    615      :Repro abc^                              |
    616    ]])
    617  end)
    618 
    619  it('no crash with % + preview + file completion #28851', function()
    620    exec_lua([[
    621      local function callback() end
    622      local function preview()
    623        return 0
    624      end
    625 
    626      vim.api.nvim_create_user_command('TestCommand', callback, {
    627        nargs = '?',
    628        complete = 'file',
    629        preview = preview,
    630      })
    631 
    632      vim.cmd.edit('Xtestscript')
    633    ]])
    634    feed(':TestCommand %')
    635    assert_alive()
    636  end)
    637 end)
    638 
    639 describe("'inccommand' with multiple buffers", function()
    640  local screen
    641 
    642  before_each(function()
    643    clear()
    644    screen = Screen.new(40, 17)
    645    exec_lua(setup_replace_cmd)
    646    command('set cmdwinheight=10')
    647    insert [[
    648      foo bar baz
    649      bar baz foo
    650      baz foo bar
    651    ]]
    652    command('vsplit | enew')
    653    insert [[
    654      bar baz foo
    655      baz foo bar
    656      foo bar baz
    657    ]]
    658  end)
    659 
    660  it('works', function()
    661    command('set inccommand=nosplit')
    662    feed(':Replace foo bar')
    663    screen:expect([[
    664      bar baz {10:bar}         {10:bar} bar baz        |
    665      baz {10:bar} bar         bar baz {10:bar}        |
    666      {10:bar} bar baz         baz {10:bar} bar        |
    667                                             |
    668      {1:~                   }{1:~                  }|*11
    669      {3:[No Name] [+]        }{2:[No Name] [+]      }|
    670      :Replace foo bar^                        |
    671    ]])
    672    feed('<CR>')
    673    screen:expect([[
    674      bar baz bar         bar bar baz        |
    675      baz bar bar         bar baz bar        |
    676      bar bar baz         baz bar bar        |
    677      ^                                       |
    678      {1:~                   }{1:~                  }|*11
    679      {3:[No Name] [+]        }{2:[No Name] [+]      }|
    680      :Replace foo bar                        |
    681    ]])
    682  end)
    683 
    684  it('works with inccommand=split', function()
    685    command('set inccommand=split')
    686    feed(':Replace foo bar')
    687    screen:expect([[
    688      bar baz {10:bar}         {10:bar} bar baz        |
    689      baz {10:bar} bar         bar baz {10:bar}        |
    690      {10:bar} bar baz         baz {10:bar} bar        |
    691                                             |
    692      {3:[No Name] [+]        }{2:[No Name] [+]      }|
    693      Buffer #1:                              |
    694      |1| {10:bar} bar baz                         |
    695      |2| bar baz {10:bar}                         |
    696      |3| baz {10:bar} bar                         |
    697      Buffer #2:                              |
    698      |1| bar baz {10:bar}                         |
    699      |2| baz {10:bar} bar                         |
    700      |3| {10:bar} bar baz                         |
    701                                              |
    702      {1:~                                       }|
    703      {2:[Preview]                               }|
    704      :Replace foo bar^                        |
    705    ]])
    706    feed('<CR>')
    707    screen:expect([[
    708      bar baz bar         bar bar baz        |
    709      baz bar bar         bar baz bar        |
    710      bar bar baz         baz bar bar        |
    711      ^                                       |
    712      {1:~                   }{1:~                  }|*11
    713      {3:[No Name] [+]        }{2:[No Name] [+]      }|
    714      :Replace foo bar                        |
    715    ]])
    716  end)
    717 end)