HEX
Server: Apache/2.4.41 (Ubuntu)
System: Linux ip-172-31-42-149 5.15.0-1084-aws #91~20.04.1-Ubuntu SMP Fri May 2 07:00:04 UTC 2025 aarch64
User: ubuntu (1000)
PHP: 7.4.33
Disabled: pcntl_alarm,pcntl_fork,pcntl_waitpid,pcntl_wait,pcntl_wifexited,pcntl_wifstopped,pcntl_wifsignaled,pcntl_wifcontinued,pcntl_wexitstatus,pcntl_wtermsig,pcntl_wstopsig,pcntl_signal,pcntl_signal_get_handler,pcntl_signal_dispatch,pcntl_get_last_error,pcntl_strerror,pcntl_sigprocmask,pcntl_sigwaitinfo,pcntl_sigtimedwait,pcntl_exec,pcntl_getpriority,pcntl_setpriority,pcntl_async_signals,pcntl_unshare,
Upload Files
File: //home/ubuntu/neovim/test/functional/ui/inccommand_user_spec.lua
local t = require('test.testutil')
local n = require('test.functional.testnvim')()
local Screen = require('test.functional.ui.screen')

local api = n.api
local clear = n.clear
local eq = t.eq
local exec_lua = n.exec_lua
local insert = n.insert
local feed = n.feed
local command = n.command
local assert_alive = n.assert_alive

-- Implements a :Replace command that works like :substitute and has multibuffer support.
local setup_replace_cmd = [[
  local function show_replace_preview(use_preview_win, preview_ns, preview_buf, matches)
    -- Find the width taken by the largest line number, used for padding the line numbers
    local highest_lnum = math.max(matches[#matches][1], 1)
    local highest_lnum_width = math.floor(math.log10(highest_lnum))
    local preview_buf_line = 0
    local multibuffer = #matches > 1

    for _, match in ipairs(matches) do
      local buf = match[1]
      local buf_matches = match[2]

      if multibuffer and #buf_matches > 0 and use_preview_win then
        local bufname = vim.api.nvim_buf_get_name(buf)

        if bufname == "" then
          bufname = string.format("Buffer #%d", buf)
        end

        vim.api.nvim_buf_set_lines(
          preview_buf,
          preview_buf_line,
          preview_buf_line,
          0,
          { bufname .. ':' }
        )

        preview_buf_line = preview_buf_line + 1
      end

      for _, buf_match in ipairs(buf_matches) do
        local lnum = buf_match[1]
        local line_matches = buf_match[2]
        local prefix

        if use_preview_win then
          prefix = string.format(
            '|%s%d| ',
            string.rep(' ', highest_lnum_width - math.floor(math.log10(lnum))),
            lnum
          )

          vim.api.nvim_buf_set_lines(
            preview_buf,
            preview_buf_line,
            preview_buf_line,
            0,
            { prefix .. vim.api.nvim_buf_get_lines(buf, lnum - 1, lnum, false)[1] }
          )
        end

        for _, line_match in ipairs(line_matches) do
          vim.api.nvim_buf_add_highlight(
            buf,
            preview_ns,
            'Substitute',
            lnum - 1,
            line_match[1],
            line_match[2]
          )

          if use_preview_win then
            vim.api.nvim_buf_add_highlight(
              preview_buf,
              preview_ns,
              'Substitute',
              preview_buf_line,
              #prefix + line_match[1],
              #prefix + line_match[2]
            )
          end
        end

        preview_buf_line = preview_buf_line + 1
      end
    end

    if use_preview_win then
      return 2
    else
      return 1
    end
  end

  local function do_replace(opts, preview, preview_ns, preview_buf)
    local pat1 = opts.fargs[1]

    if not pat1 then return end

    local pat2 = opts.fargs[2] or ''
    local line1 = opts.line1
    local line2 = opts.line2
    local matches = {}

    -- Get list of valid and listed buffers
    local buffers = vim.tbl_filter(
        function(buf)
          if not (vim.api.nvim_buf_is_valid(buf) and vim.bo[buf].buflisted and buf ~= preview_buf)
          then
            return false
          end

          -- Check if there's at least one window using the buffer
          for _, win in ipairs(vim.api.nvim_tabpage_list_wins(0)) do
            if vim.api.nvim_win_get_buf(win) == buf then
              return true
            end
          end

          return false
        end,
        vim.api.nvim_list_bufs()
    )

    for _, buf in ipairs(buffers) do
      local lines = vim.api.nvim_buf_get_lines(buf, line1 - 1, line2, false)
      local buf_matches = {}

      for i, line in ipairs(lines) do
        local startidx, endidx = 0, 0
        local line_matches = {}
        local num = 1

        while startidx ~= -1 do
          local match = vim.fn.matchstrpos(line, pat1, 0, num)
          startidx, endidx = match[2], match[3]

          if startidx ~= -1 then
            line_matches[#line_matches+1] = { startidx, endidx }
          end

          num = num + 1
        end

        if #line_matches > 0 then
          buf_matches[#buf_matches+1] = { line1 + i - 1, line_matches }
        end
      end

      local new_lines = {}

      for _, buf_match in ipairs(buf_matches) do
        local lnum = buf_match[1]
        local line_matches = buf_match[2]
        local line = lines[lnum - line1 + 1]
        local pat_width_differences = {}

        -- If previewing, only replace the text in current buffer if pat2 isn't empty
        -- Otherwise, always replace the text
        if pat2 ~= '' or not preview then
          if preview then
            for _, line_match in ipairs(line_matches) do
              local startidx, endidx = unpack(line_match)
              local pat_match = line:sub(startidx + 1, endidx)

              pat_width_differences[#pat_width_differences+1] =
                #vim.fn.substitute(pat_match, pat1, pat2, 'g') - #pat_match
            end
          end

          new_lines[lnum] = vim.fn.substitute(line, pat1, pat2, 'g')
        end

        -- Highlight the matches if previewing
        if preview then
          local idx_offset = 0
          for i, line_match in ipairs(line_matches) do
            local startidx, endidx = unpack(line_match)
            -- Starting index of replacement text
            local repl_startidx = startidx + idx_offset
            -- Ending index of the replacement text (if pat2 isn't empty)
            local repl_endidx

            if pat2 ~= '' then
              repl_endidx = endidx + idx_offset + pat_width_differences[i]
            else
              repl_endidx = endidx + idx_offset
            end

            if pat2 ~= '' then
              idx_offset = idx_offset + pat_width_differences[i]
            end

            line_matches[i] = { repl_startidx, repl_endidx }
          end
        end
      end

      for lnum, line in pairs(new_lines) do
        vim.api.nvim_buf_set_lines(buf, lnum - 1, lnum, false, { line })
      end

      matches[#matches+1] = { buf, buf_matches }
    end

    if preview then
      local lnum = vim.api.nvim_win_get_cursor(0)[1]
      -- Use preview window only if preview buffer is provided and range isn't just the current line
      local use_preview_win = (preview_buf ~= nil) and (line1 ~= lnum or line2 ~= lnum)
      return show_replace_preview(use_preview_win, preview_ns, preview_buf, matches)
    end
  end

  local function replace(opts)
    do_replace(opts, false)
  end

  local function replace_preview(opts, preview_ns, preview_buf)
    return do_replace(opts, true, preview_ns, preview_buf)
  end

  -- ":<range>Replace <pat1> <pat2>"
  -- Replaces all occurrences of <pat1> in <range> with <pat2>
  vim.api.nvim_create_user_command(
    'Replace',
    replace,
    { nargs = '*', range = '%', addr = 'lines',
      preview = replace_preview }
  )
]]

describe("'inccommand' for user commands", function()
  local screen

  before_each(function()
    clear()
    screen = Screen.new(40, 17)
    screen:attach()
    exec_lua(setup_replace_cmd)
    command('set cmdwinheight=5')
    insert [[
      text on line 1
      more text on line 2
      oh no, even more text
      will the text ever stop
      oh well
      did the text stop
      why won't it stop
      make the text stop
    ]]
  end)

  it('works with inccommand=nosplit', function()
    command('set inccommand=nosplit')
    feed(':Replace text cats')
    screen:expect([[
        {10:cats} on line 1                        |
        more {10:cats} on line 2                   |
        oh no, even more {10:cats}                 |
        will the {10:cats} ever stop               |
        oh well                               |
        did the {10:cats} stop                     |
        why won't it stop                     |
        make the {10:cats} stop                    |
                                              |
      {1:~                                       }|*7
      :Replace text cats^                      |
    ]])
  end)

  it('works with inccommand=split', function()
    command('set inccommand=split')
    feed(':Replace text cats')
    screen:expect([[
        {10:cats} on line 1                        |
        more {10:cats} on line 2                   |
        oh no, even more {10:cats}                 |
        will the {10:cats} ever stop               |
        oh well                               |
        did the {10:cats} stop                     |
        why won't it stop                     |
        make the {10:cats} stop                    |
                                              |
      {3:[No Name] [+]                           }|
      |1|   {10:cats} on line 1                    |
      |2|   more {10:cats} on line 2               |
      |3|   oh no, even more {10:cats}             |
      |4|   will the {10:cats} ever stop           |
      |6|   did the {10:cats} stop                 |
      {2:[Preview]                               }|
      :Replace text cats^                      |
    ]])
  end)

  it('properly closes preview when inccommand=split', function()
    command('set inccommand=split')
    feed(':Replace text cats<Esc>')
    screen:expect([[
        text on line 1                        |
        more text on line 2                   |
        oh no, even more text                 |
        will the text ever stop               |
        oh well                               |
        did the text stop                     |
        why won't it stop                     |
        make the text stop                    |
      ^                                        |
      {1:~                                       }|*7
                                              |
    ]])
  end)

  it('properly executes command when inccommand=split', function()
    command('set inccommand=split')
    feed(':Replace text cats<CR>')
    screen:expect([[
        cats on line 1                        |
        more cats on line 2                   |
        oh no, even more cats                 |
        will the cats ever stop               |
        oh well                               |
        did the cats stop                     |
        why won't it stop                     |
        make the cats stop                    |
      ^                                        |
      {1:~                                       }|*7
      :Replace text cats                      |
    ]])
  end)

  it('shows preview window only when range is not current line', function()
    command('set inccommand=split')
    feed('gg:.Replace text cats')
    screen:expect([[
        {10:cats} on line 1                        |
        more text on line 2                   |
        oh no, even more text                 |
        will the text ever stop               |
        oh well                               |
        did the text stop                     |
        why won't it stop                     |
        make the text stop                    |
                                              |
      {1:~                                       }|*7
      :.Replace text cats^                     |
    ]])
  end)

  it('does not crash on ambiguous command #18825', function()
    command('set inccommand=split')
    command('command Reply echo 1')
    feed(':R')
    assert_alive()
    feed('e')
    assert_alive()
  end)

  it('no crash if preview callback changes inccommand option', function()
    command('set inccommand=nosplit')
    exec_lua([[
      vim.api.nvim_create_user_command('Replace', function() end, {
        nargs = '*',
        preview = function()
          vim.api.nvim_set_option_value('inccommand', 'split', {})
          return 2
        end,
      })
    ]])
    feed(':R')
    assert_alive()
    feed('e')
    assert_alive()
  end)

  it('no crash when adding highlight after :substitute #21495', function()
    command('set inccommand=nosplit')
    exec_lua([[
      vim.api.nvim_create_user_command("Crash", function() end, {
        preview = function(_, preview_ns, _)
          vim.cmd("%s/text/cats/g")
          vim.api.nvim_buf_add_highlight(0, preview_ns, "Search", 0, 0, -1)
          return 1
        end,
      })
    ]])
    feed(':C')
    screen:expect([[
      {10:  cats on line 1}                        |
        more cats on line 2                   |
        oh no, even more cats                 |
        will the cats ever stop               |
        oh well                               |
        did the cats stop                     |
        why won't it stop                     |
        make the cats stop                    |
                                              |
      {1:~                                       }|*7
      :C^                                      |
    ]])
    assert_alive()
  end)

  it('no crash if preview callback executes undo #20036', function()
    command('set inccommand=nosplit')
    exec_lua([[
      vim.api.nvim_create_user_command('Foo', function() end, {
        nargs = '?',
        preview = function(_, _, _)
          vim.cmd.undo()
        end,
      })
    ]])

    -- Clear undo history
    command('set undolevels=-1')
    feed('ggyyp')
    command('set undolevels=1000')

    feed('yypp:Fo')
    assert_alive()
    feed('<Esc>:Fo')
    assert_alive()
  end)

  local function test_preview_break_undo()
    command('set inccommand=nosplit')
    exec_lua([[
      vim.api.nvim_create_user_command('Test', function() end, {
        nargs = 1,
        preview = function(opts, _, _)
          vim.cmd('norm i' .. opts.args)
          return 1
        end
      })
    ]])
    feed(':Test a.a.a.a.')
    screen:expect([[
        text on line 1                        |
        more text on line 2                   |
        oh no, even more text                 |
        will the text ever stop               |
        oh well                               |
        did the text stop                     |
        why won't it stop                     |
        make the text stop                    |
      a.a.a.a.                                |
      {1:~                                       }|*7
      :Test a.a.a.a.^                          |
    ]])
    feed('<C-V><Esc>u')
    screen:expect([[
        text on line 1                        |
        more text on line 2                   |
        oh no, even more text                 |
        will the text ever stop               |
        oh well                               |
        did the text stop                     |
        why won't it stop                     |
        make the text stop                    |
      a.a.a.                                  |
      {1:~                                       }|*7
      :Test a.a.a.a.{18:^[}u^                       |
    ]])
    feed('<Esc>')
    screen:expect([[
        text on line 1                        |
        more text on line 2                   |
        oh no, even more text                 |
        will the text ever stop               |
        oh well                               |
        did the text stop                     |
        why won't it stop                     |
        make the text stop                    |
      ^                                        |
      {1:~                                       }|*7
                                              |
    ]])
  end

  describe('breaking undo chain in Insert mode works properly', function()
    it('when using i_CTRL-G_u #20248', function()
      command('inoremap . .<C-G>u')
      test_preview_break_undo()
    end)

    it('when setting &l:undolevels to itself #24575', function()
      command('inoremap . .<Cmd>let &l:undolevels = &l:undolevels<CR>')
      test_preview_break_undo()
    end)
  end)

  it('disables preview if preview buffer cannot be created #27086', function()
    command('set inccommand=split')
    api.nvim_buf_set_name(0, '[Preview]')
    exec_lua([[
      vim.api.nvim_create_user_command('Test', function() end, {
        nargs = '*',
        preview = function(_, _, _)
          return 2
        end
      })
    ]])
    eq('split', api.nvim_get_option_value('inccommand', {}))
    feed(':Test')
    eq('nosplit', api.nvim_get_option_value('inccommand', {}))
  end)
end)

describe("'inccommand' with multiple buffers", function()
  local screen

  before_each(function()
    clear()
    screen = Screen.new(40, 17)
    screen:attach()
    exec_lua(setup_replace_cmd)
    command('set cmdwinheight=10')
    insert [[
      foo bar baz
      bar baz foo
      baz foo bar
    ]]
    command('vsplit | enew')
    insert [[
      bar baz foo
      baz foo bar
      foo bar baz
    ]]
  end)

  it('works', function()
    command('set inccommand=nosplit')
    feed(':Replace foo bar')
    screen:expect([[
        bar baz {10:bar}       │  {10:bar} bar baz      |
        baz {10:bar} bar       │  bar baz {10:bar}      |
        {10:bar} bar baz       │  baz {10:bar} bar      |
                          │                   |
      {1:~                   }│{1:~                  }|*11
      {3:[No Name] [+]        }{2:[No Name] [+]      }|
      :Replace foo bar^                        |
    ]])
    feed('<CR>')
    screen:expect([[
        bar baz bar       │  bar bar baz      |
        baz bar bar       │  bar baz bar      |
        bar bar baz       │  baz bar bar      |
      ^                    │                   |
      {1:~                   }│{1:~                  }|*11
      {3:[No Name] [+]        }{2:[No Name] [+]      }|
      :Replace foo bar                        |
    ]])
  end)

  it('works with inccommand=split', function()
    command('set inccommand=split')
    feed(':Replace foo bar')
    screen:expect([[
        bar baz {10:bar}       │  {10:bar} bar baz      |
        baz {10:bar} bar       │  bar baz {10:bar}      |
        {10:bar} bar baz       │  baz {10:bar} bar      |
                          │                   |
      {3:[No Name] [+]        }{2:[No Name] [+]      }|
      Buffer #1:                              |
      |1|   {10:bar} bar baz                       |
      |2|   bar baz {10:bar}                       |
      |3|   baz {10:bar} bar                       |
      Buffer #2:                              |
      |1|   bar baz {10:bar}                       |
      |2|   baz {10:bar} bar                       |
      |3|   {10:bar} bar baz                       |
                                              |
      {1:~                                       }|
      {2:[Preview]                               }|
      :Replace foo bar^                        |
    ]])
    feed('<CR>')
    screen:expect([[
        bar baz bar       │  bar bar baz      |
        baz bar bar       │  bar baz bar      |
        bar bar baz       │  baz bar bar      |
      ^                    │                   |
      {1:~                   }│{1:~                  }|*11
      {3:[No Name] [+]        }{2:[No Name] [+]      }|
      :Replace foo bar                        |
    ]])
  end)
end)