Autocomplete doesn't work after exiting VM
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:
- Press
<C-n>do enter VM, - Press
<C-n>again to select a second word (essential to reproduce the bug) - Press
<ESC>to exit - Press
<i>to enter insert mode - Trigger some auto complete and try to select with
<CR>andup/downarrows 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:
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
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?
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" },
},
},
},
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
Thank you for sharing the code!
I currently switched to using the find registry and substitute instead.
- select the text you want to edit in visual mode
- press
*to put it into find register - 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\0means 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.
Having this issue as well some how
replace with https://github.com/jake-stewart/multicursor.nvim
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 try this config https://github.com/boydaihungst/.config/blob/hyprland/nvim/lua/plugins/blink.lua
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.
I think it might be related to #280 which was fixed in #297
I am not using this plugin anymore. But I can close the issue, if others confirm the fix is working