blink.cmp icon indicating copy to clipboard operation
blink.cmp copied to clipboard

feat(cmdline): add buffer completion for `:s` and `:g` commands

Open soifou opened this issue 7 months ago • 7 comments

Add buffer source support for substitution (:s) and global (:g, :v) commands in cmdline completion. Previously, only search commands (/, ?) used buffer completion. Macro (@) and other ex-commands still use cmdline source.

Note: This behavior is opt-in. To enable buffer source completion for ex-commands, set the enable_in_ex_commands flag in your buffer source options.

Caveat: Enabling this option will temporarily disable Neovim's 'inccommand' feature due to a known redraw issue.

Closes #1625

soifou avatar May 05 '25 17:05 soifou

Hopefully this should covers most of the usual cases for buffer completion, such as substitutions and global commands. There are some commands (like :vimgrep or :match) that aren’t included here, but those are pretty rare in everyday use.

soifou avatar May 05 '25 17:05 soifou

Thanks for this! My only concern is that editing the cmdline.sources would be tricky for end users since they now need to copy the regex as well. Perhaps we could use the buffer:enabled() field on the buffer source so that it enables itself in buffers, search (/ and ?) and substitution (:s and :g). That way, the cmdline.sources only needs to add buffer to the list of enabled providers.

saghen avatar May 05 '25 18:05 saghen

I completely overlooked that the sources can be overridden :facepalm: I agree with your approach, it make more sense, I'll update the PR accordingly!

soifou avatar May 05 '25 18:05 soifou

I force pushed an update, could you take another look? If I'm not mistaken, this should do what you meant.

soifou avatar May 05 '25 19:05 soifou

Sorry, I was too ambiguous before (there's 4 diferent enabled functions :joy:) and I totally missed the notification for this. It seems like there's something wrong with the redraw logic as the menu appears to be outdated, and the first item often doesn't show as selected. Accepting an item also leads to an outdated view. Just speculating but it could be the redraw logic causing the issue:

https://github.com/Saghen/blink.cmp/blob/49423e05ec16169e895e35fc65c2ad806a24b1f3/lua/blink/cmp/lib/window/init.lua?plain=1#L453-L464

Notably, I'm using cmdline.completion.menu.auto_show = true with ghost text disabled on NVIM v0.12.0-nightly+1b8ae43

saghen avatar May 13 '25 17:05 saghen

Ah I hadn’t paid close attention to use strictly buffer:enabled() when you wrote it... but now I get it!

It seems like there's something wrong [...] as the menu appears to be outdated

Indeed I see that. I'm not entirely sure the redraw logic is faulty, though, because it works as expected with global commands; only substitution seems to be off by one char and no item gets highlighted (I'm using the same config as yours).

For instance try the following:

:g/pattern (ok)
:v/pattern (ok)
:s/pattern (off)

Another (minor) issue is that when you accept a command after the pattern ([cmd] below), it gets entirely replaced.

:[range]g[lobal]/{pattern}/[cmd]
:g/foo/del -> :delete

I will dig into that, thanks again for your second look and these updates!

soifou avatar May 14 '25 11:05 soifou

only substitution seems to be off by one char

This is caused by vim.on_key ~~but I'm a bit clueless here.~~

For instance, while typing :s/leo, vim.on_key reports s, /, and l immediately, but delays e until I type o, and so on. This means every character after the second is reported one keystroke late. This is reproductible no matter the pattern I try to find.


EDIT: The culprit is inccommand. Clearing it using :set inccommand= solves the issue. I will work around it and add a note about this.

soifou avatar May 18 '25 09:05 soifou

Wicked PR, thanks!

saghen avatar Jun 01 '25 20:06 saghen

There is one drawback to your change in 5427010. If blink has not yet been initialized and the user triggers a keymap for substitute and init blink at the same time (such as the one below), inccommand will not be disabled. More problematically, it seems that it will remain enabled for future substitutions as well...

This hack is very sensitive 🙄

vim.keymap.set('n', '<Leader>sr', ':%s//g<Left><Left>')

soifou avatar Jun 01 '25 22:06 soifou

Ah, nice catch. Would adding if vim.fn.getcmdtype() == ':' and vim.o.inccommand ~= '' then vim.o.inccommand = '' end after this line, alongside the existing vim.on_key, be enough? Feel free to commit on main if so

saghen avatar Jun 01 '25 22:06 saghen

Would adding [...], alongside the existing vim.on_key, be enough?

No, the issue is that buffer.new is called too late — specifically, after the first character is entered on the command line — while buffer_events.listen is called immediately. Through further testing, I also noticed that vim.on_key is not necessary; CmdlineEnter seems to be sufficient by itself.

We could work around this with a mix of our solutions as follows:

diff --git a/lua/blink/cmp/completion/trigger/init.lua b/lua/blink/cmp/completion/trigger/init.lua
index 6ed52b8..a617528 100644
--- a/lua/blink/cmp/completion/trigger/init.lua
+++ b/lua/blink/cmp/completion/trigger/init.lua
@@ -136,6 +136,7 @@ function trigger.activate()
     -- TODO: should this ignore trigger.kind == 'prefetch'?
     has_context = function() return trigger.context ~= nil end,
     show_in_snippet = config.show_in_snippet,
+    should_disable_inccommand = root_config.sources.providers['buffer'].opts.enable_in_ex_commands,
   })

   trigger.buffer_events:listen({
diff --git a/lua/blink/cmp/lib/buffer_events.lua b/lua/blink/cmp/lib/buffer_events.lua
index e6324ba..1eb60e1 100644
--- a/lua/blink/cmp/lib/buffer_events.lua
+++ b/lua/blink/cmp/lib/buffer_events.lua
@@ -5,6 +5,7 @@
 --- @class blink.cmp.BufferEvents
 --- @field has_context fun(): boolean
 --- @field show_in_snippet boolean
+--- @field should_disable_inccommand boolean
 --- @field ignore_next_text_changed boolean
 --- @field ignore_next_cursor_moved boolean
 --- @field last_char string
@@ -18,6 +19,7 @@
 --- @class blink.cmp.BufferEventsOptions
 --- @field has_context fun(): boolean
 --- @field show_in_snippet boolean
+--- @field should_disable_inccommand boolean

 --- @class blink.cmp.BufferEventsListener
 --- @field on_char_added fun(char: string, is_ignored: boolean)
@@ -32,6 +34,7 @@ function buffer_events.new(opts)
   return setmetatable({
     has_context = opts.has_context,
     show_in_snippet = opts.show_in_snippet,
+    should_disable_inccommand = opts.should_disable_inccommand,
     ignore_next_text_changed = false,
     ignore_next_cursor_moved = false,
     last_char = '',
@@ -95,6 +98,10 @@ local function make_insert_leave(self, on_insert_leave)
   end
 end

+local disable_inccommand = function()
+  if vim.fn.getcmdtype() == ':' and vim.o.inccommand ~= '' then vim.o.inccommand = '' end
+end
+
 --- Normalizes the autocmds + ctrl+c into a common api and handles ignored events
 function buffer_events:listen(opts)
   local snippet = require('blink.cmp.config').snippets
@@ -134,6 +141,17 @@ function buffer_events:listen(opts)
       end)
     end
   end)
+
+  -- HACK: When using buffer completion sources in ex commands
+  -- while 'inccommand' is active, Neovim's UI redraw is delayed by one frame.
+  -- This causes completion popups to appear out of sync with user input,
+  -- due to a known Neovim limitation (see neovim/neovim#9783).
+  -- To work around this, temporarily disable 'inccommand'.
+  -- This sacrifice live substitution previews, but restores correct redraw.
+  if self.should_disable_inccommand then
+    disable_inccommand()
+    vim.api.nvim_create_autocmd('CmdlineEnter', { callback = disable_inccommand })
+  end
 end

 --- Effectively ensures that our autocmd listeners run last, after other registered listeners
diff --git a/lua/blink/cmp/sources/buffer.lua b/lua/blink/cmp/sources/buffer.lua
index 363eb40..32a1417 100644
--- a/lua/blink/cmp/sources/buffer.lua
+++ b/lua/blink/cmp/sources/buffer.lua
@@ -146,18 +146,6 @@ function buffer.new(opts)
     enable_in_ex_commands = { opts.enable_in_ex_commands, 'boolean' },
   }, opts)

-  -- HACK: When using buffer completion sources in ex commands
-  -- while 'inccommand' is active, Neovim's UI redraw is delayed by one frame.
-  -- This causes completion popups to appear out of sync with user input,
-  -- due to a known Neovim limitation (see neovim/neovim#9783).
-  -- To work around this, temporarily disable 'inccommand'.
-  -- This sacrifice live substitution previews, but restores correct redraw.
-  if opts.enable_in_ex_commands then
-    vim.on_key(function()
-      if vim.fn.getcmdtype() == ':' and vim.o.inccommand ~= '' then vim.o.inccommand = '' end
-    end)
-  end
-
   self.opts = opts
   return self
 end

soifou avatar Jun 02 '25 11:06 soifou

The issue linked in the code https://github.com/neovim/neovim/issues/20463 was fixed by https://github.com/neovim/neovim/pull/27950 in May 2024, while this PR was merged in June 2025. It seems that the problem only occurs when selecting the menu item, but not during normal completion. Maybe an option to disable the disabling of incsearch could be added? Additionally, selecting from the menu works but it just doesn't show what's being selected on the menu. Which seems fairly different than what is mentioned in this issue.

Maybe it's unrelated because vim.ui_attach isnt being used by blink?

chancez avatar Oct 31 '25 17:10 chancez