vim-visual-multi icon indicating copy to clipboard operation
vim-visual-multi copied to clipboard

Autocomplete doesn't work after exiting VM

Open besserwisser opened this issue 11 months ago • 12 comments

The plugin is great! But right now, I cannot use my autocomplete features anymore, after I have used VM.

I am using LazyVim

Steps to reproduce:

  1. Press <C-n> do enter VM,
  2. Press <C-n> again to select a second word (essential to reproduce the bug)
  3. Press <ESC> to exit
  4. Press <i> to enter insert mode
  5. Trigger some auto complete and try to select with <CR> and up/down arrows to select something.

https://github.com/user-attachments/assets/525f5daf-f966-47c3-9a72-335290b8e1ec

Don't have anything configured, just:

return {
  "mg979/vim-visual-multi",
  branch = "master",
}

Also when I enter VM, I get this warnings:

Image

besserwisser avatar Jan 25 '25 06:01 besserwisser

I've been dealing with this for a while too and it's been driving me nuts. Finally narrowed it down to vim-visual-multi with neovim. I'm using blink-cmp.nvim for completions, but it seems like it's also messing up with just vim's completion engine for you too?

I couldn't find any way to turn off visual-multi's completions and even tried mucking about in the vm#comp#init() function to no avail

https://github.com/mg979/vim-visual-multi/blob/a6975e7c1ee157615bbc80fc25e4392f71c344d4/autoload/vm.vim#L66C14-L66C28

sarendipitee avatar Jan 31 '25 21:01 sarendipitee

I was trying an alternative extension called "smoka7/multicursors.nvim" But It has the same issue!! So the error might be more on the "blink-cmp" plugin side 🤔

Do you also use Lazyvim?

I also haven't encountered it in all files yet. Can you reproduce the bug in lua files?

besserwisser avatar Feb 01 '25 09:02 besserwisser

I do use LazyVim yes. And I tried it in some lua, yaml and txt files where it still occurred so doesn't seem to be anything ft specific.

My blink config: { "saghen/blink.cmp", enabled = true, opts = { completion = { list = { selection = { preselect = false, auto_insert = false } } }, keymap = { preset = "enter", [""] = { "select_next", "fallback" }, [""] = { "select_prev", "fallback" }, [""] = { "select_prev", "fallback" }, [""] = { "select_next", "fallback" }, [""] = { "select_prev" }, [""] = { "select_next" }, [""] = { "select_prev", "fallback" }, [""] = { "select_next", "fallback" }, -- TODO figure out how make this work :| [""] = { "show", "show_documentation", "hide_documentation" }, }, }, },

sarendipitee avatar Feb 03 '25 05:02 sarendipitee

If you use cmp try this key mappings. vim-visual-multi has problem with cmp.SelectBehavior.Insert, So I temporarily switch to cmp.SelectBehavior.Select when in visual_multi mode.

      opts.mapping["<Tab>"] = cmp.mapping(function(fallback)
        if is_visible(cmp) then
          local selectBehavior = vim.b.visual_multi and cmp.SelectBehavior.Select or cmp.SelectBehavior.Insert
          cmp.select_next_item { behavior = selectBehavior }
        elseif vim.api.nvim_get_mode().mode ~= "c" and luasnip.expand_or_locally_jumpable() then
          luasnip.expand_or_jump()
        elseif jumpable(1) then
          luasnip.jump(1)
        elseif has_words_before() then
          -- cmp.complete()
          fallback()
        else
          fallback()
        end
      end, { "i", "s" })
      opts.mapping["<S-Tab>"] = cmp.mapping(function(fallback)
        if is_visible(cmp) then
          local selectBehavior = vim.b.visual_multi and cmp.SelectBehavior.Select or cmp.SelectBehavior.Insert
          cmp.select_prev_item { behavior = selectBehavior }
        elseif vim.api.nvim_get_mode().mode ~= "c" and jumpable(-1) then
          luasnip.jump(-1)
        else
          fallback()
        end
      end, { "i", "s" })

Other cmp code:

local function has_words_before()
  local line, col = (unpack or table.unpack)(vim.api.nvim_win_get_cursor(0))
  return col ~= 0 and vim.api.nvim_buf_get_lines(0, line - 1, line, true)[1]:sub(col, col):match "%s" == nil
end

local function is_visible(cmp) return cmp.core.view:visible() or vim.fn.pumvisible() == 1 end

---when inside a snippet, seeks to the nearest luasnip field if possible, and checks if it is jumpable
---@param dir number 1 for forward, -1 for backward; defaults to 1
---@return boolean true if a jumpable luasnip field is found while inside a snippet
local function jumpable(dir)
  local luasnip_ok, luasnip = pcall(require, "luasnip")
  if not luasnip_ok then return false end

  local win_get_cursor = vim.api.nvim_win_get_cursor
  local get_current_buf = vim.api.nvim_get_current_buf

  ---sets the current buffer's luasnip to the one nearest the cursor
  ---@return boolean true if a node is found, false otherwise
  local function seek_luasnip_cursor_node()
    -- TODO(kylo252): upstream this
    -- for outdated versions of luasnip
    if not luasnip.session.current_nodes then return false end

    local node = luasnip.session.current_nodes[get_current_buf()]
    if not node then return false end

    local snippet = node.parent.snippet
    local exit_node = snippet.insert_nodes[0]

    local pos = win_get_cursor(0)
    pos[1] = pos[1] - 1

    -- exit early if we're past the exit node
    if exit_node then
      local exit_pos_end = exit_node.mark:pos_end()
      if (pos[1] > exit_pos_end[1]) or (pos[1] == exit_pos_end[1] and pos[2] > exit_pos_end[2]) then
        snippet:remove_from_jumplist()
        luasnip.session.current_nodes[get_current_buf()] = nil

        return false
      end
    end

    node = snippet.inner_first:jump_into(1, true)
    while node ~= nil and node.next ~= nil and node ~= snippet do
      local n_next = node.next
      local next_pos = n_next and n_next.mark:pos_begin()
      local candidate = n_next ~= snippet and next_pos and (pos[1] < next_pos[1])
        or (pos[1] == next_pos[1] and pos[2] < next_pos[2])

      -- Past unmarked exit node, exit early
      if n_next == nil or n_next == snippet.next then
        snippet:remove_from_jumplist()
        luasnip.session.current_nodes[get_current_buf()] = nil

        return false
      end

      if candidate then
        luasnip.session.current_nodes[get_current_buf()] = node
        return true
      end

      local ok
      ok, node = pcall(node.jump_from, node, 1, true) -- no_move until last stop
      if not ok then
        snippet:remove_from_jumplist()
        luasnip.session.current_nodes[get_current_buf()] = nil

        return false
      end
    end

    -- No candidate, but have an exit node
    if exit_node then
      -- to jump to the exit node, seek to snippet
      luasnip.session.current_nodes[get_current_buf()] = snippet
      return true
    end

    -- No exit node, exit from snippet
    snippet:remove_from_jumplist()
    luasnip.session.current_nodes[get_current_buf()] = nil
    return false
  end

  if dir == -1 then
    return luasnip.in_snippet() and luasnip.jumpable(-1)
  else
    return luasnip.in_snippet() and seek_luasnip_cursor_node() and luasnip.jumpable(1)
  end
end

boydaihungst avatar Feb 12 '25 01:02 boydaihungst

Thank you for sharing the code!

I currently switched to using the find registry and substitute instead.

  1. select the text you want to edit in visual mode
  2. press * to put it into find register
  3. Run command :%s//\0ADDED. This adds to all occurences of the found string "ADDED" (for example "FOO" with "FOOADDED"). The // means that it just references the find register. The \0 means to back reference the occurence. If I don't want the whole file, I just replace the % with the line numbers, e.g. :1,3s//\0ADDED.

It is not perfect (e.g. not so easy to edit inside of a found word), but good enough for me.

besserwisser avatar Feb 12 '25 05:02 besserwisser

Having this issue as well some how

Sirikakire avatar Mar 11 '25 04:03 Sirikakire

replace with https://github.com/jake-stewart/multicursor.nvim

nzlov avatar Apr 01 '25 01:04 nzlov

Has anyone been able to figure this out? This has been frustrating me and I can not for the life of me find a solution. I'm using blink for autocompletions with LazyVim.

Czensored avatar Jun 03 '25 08:06 Czensored

@Czensored try this config https://github.com/boydaihungst/.config/blob/hyprland/nvim/lua/plugins/blink.lua

boydaihungst avatar Jun 03 '25 09:06 boydaihungst

I just changed my config to have tab be accept like this. It's just enter to accept autocompletions that doesn't play well with multi cursor. Interestingly, I had the same problem with multicursors.nvim before I rebound my accept autocomplete key. Not sure what the problem actually is, but I'm happy enough with this solution:

["<Tab>"] = {
  function(cmp)
    if cmp.is_visible() then
      cmp.select_and_accept()
      return true
    end
  end,
  "fallback",
},

Edit:

I found that you can add this autocommand, and it will restore the proper enter functionality after exiting vim-visual-multi:

vim.api.nvim_create_autocmd("User", {
  pattern = "visual_multi_exit",
  callback = function()
    local cmp = require("blink.cmp")
    vim.keymap.set("i", "<CR>", function()
      if cmp.is_visible() then
        cmp.accept()
        return ""
      else
        return vim.api.nvim_replace_termcodes("<CR>", true, false, true)
      end
    end, { expr = true, silent = true })
  end,
})

Just a disclaimer that I prompted this with ChatGPT. The code makes sense and is working perfectly for me, but I tend not to trust what it gives me.

Czensored avatar Jul 13 '25 14:07 Czensored

I think it might be related to #280 which was fixed in #297

eyalk11 avatar Nov 03 '25 02:11 eyalk11

I am not using this plugin anymore. But I can close the issue, if others confirm the fix is working

besserwisser avatar Nov 03 '25 05:11 besserwisser