Skip to content

Conversation

Saghen
Copy link
Owner

@Saghen Saghen commented Jul 24, 2025

The user may exit insert mode and/or move the cursor after accepting an item while blink.cmp waits for the LSP resolve response.

  1. Ignore dot repeat if the user is no longer in insert mode
    • We don't have a way to synchronously enter insert mode, and the dot repeat relies on being synchronous
  2. Resolve the text edit from the original cursor position
  3. Enter insert mode and restore cursor position before expanding snippet

Related to #1491 and #2018

@geril07
Copy link

geril07 commented Jul 24, 2025

I'm wondering about ctx.cursor, sometimes the value is at the start of a typed content. I described it before in #1740.

I investigated a bit and found that completions_emitter can emit(if an lsp is slow) after the user has performed some actions (like typing more characters), which overrides the current ctx with an outdated(at least ctx.cursor is outdated) one (the ctx from when the completions were originally requested).

sources.completions_emitter:on(function(event)
-- schedule for later to avoid adding 0.5-4ms to insertion latency
vim.schedule(function()
-- since this was performed asynchronously, we check if the context has changed
if trigger.context == nil or event.context.id ~= trigger.context.id then return end
-- don't show the list if prefetching results
if trigger.context.trigger.kind == 'prefetch' then return end
-- don't show if all the sources that defined the trigger character returned no items
if event.context.trigger.character ~= nil then
local triggering_source_returned_items = false
for _, source in pairs(event.context.providers) do
local trigger_characters = sources.get_provider_by_id(source):get_trigger_characters()
if
event.items[source]
and #event.items[source] > 0
and vim.tbl_contains(trigger_characters, trigger.context.trigger.character)
then
triggering_source_returned_items = true
break
end
end
if not triggering_source_returned_items then return list.hide() end
end
list.show(event.context, event.items)
end)
end)

UPD:
As I understand how the completion queue works:

function queue:get_completions(context)

  1. First call – requests completions, as expected.
  2. Second call – if the first task is running, saves the context to queued_request_context.
  3. First request is resolved – saves it and emits to completions_emitter, which shows completions using the first context. Then checks if there's a context in queued_request_context, and if so, make a second call.
  4. There's a check for cached completions, which can return true for the second call, and therefore there's no emit for the second call (last context).
    if not is_all_cached then self:emit_completions(items_by_provider, on_items_by_provider) end

rust_analyzer's completions for example aren't treated as cached from the second call.

some logs from this diff:

diff --git a/lua/blink/cmp/completion/init.lua b/lua/blink/cmp/completion/init.lua
index ce1866c..5980033 100644
--- a/lua/blink/cmp/completion/init.lua
+++ b/lua/blink/cmp/completion/init.lua
@@ -50,6 +50,7 @@ function completion.setup()
         if not triggering_source_returned_items then return list.hide() end
       end

+      print('cmp list show', vim.inspect(event.context.cursor), vim.inspect(trigger.context.cursor))
       list.show(event.context, event.items)
     end)
   end)
diff --git a/lua/blink/cmp/sources/lib/init.lua b/lua/blink/cmp/sources/lib/init.lua
index fb97546..2d68bd3 100644
--- a/lua/blink/cmp/sources/lib/init.lua
+++ b/lua/blink/cmp/sources/lib/init.lua
@@ -159,6 +159,7 @@ function sources.emit_completions(context, _items_by_provider)
   for id, items in pairs(_items_by_provider) do
     if sources.providers[id]:should_show_items(context, items) then items_by_provider[id] = items end
   end
+  print('emit_completions', vim.inspect(context.cursor))
   sources.completions_emitter:emit({ context = context, items = items_by_provider })
 end

diff --git a/lua/blink/cmp/sources/lib/tree.lua b/lua/blink/cmp/sources/lib/tree.lua
index 7f5bc61..3773fbb 100644
--- a/lua/blink/cmp/sources/lib/tree.lua
+++ b/lua/blink/cmp/sources/lib/tree.lua
@@ -93,6 +93,7 @@ function tree:get_completions(context, on_items_by_provider)
       should_push_upstream = true

       -- if atleast one of the results wasn't cached, emit the results
+      print('is_all_cached', is_all_cached, vim.inspect(context.cursor))
       if not is_all_cached then self:emit_completions(items_by_provider, on_items_by_provider) end
     end)
     :catch(function(err) vim.print('failed to get completions with error: ' .. err) end)

vtsls/ts_ls logs

is_all_cached false { 79, 60 }
emit_completions { 79, 60 }
is_all_cached true { 79, 61 }
cmp list show { 79, 60 } { 79, 61 }

rust_analyzer logs

is_all_cached false { 84, 5 }
emit_completions { 84, 5 }
cmp list show { 84, 5 } { 84, 6 }
is_all_cached false { 84, 6 }
emit_completions { 84, 6 }
cmp list show { 84, 6 } { 84, 6 }

luals with lazydev same as vtsls/ts_ls, this one you might be able to reproduce easily

out.mp4

-- and set the cursor back to its position before accepting the item, since the snippet expansion
-- inserts at the cursor position
if not vim.api.nvim_get_mode().mode:match('i') then vim.cmd('startinsert') end
vim.api.nvim_win_set_cursor(0, { ctx.cursor[1], ctx.cursor[2] })
Copy link

@geril07 geril07 Jul 29, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

does not behave well if used after 59 row text_edits_lib.apply(temp_text_edit, all_text_edits) in some cases

output.mp4

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants