From 646485089591650b4f9e3d79cd7cf1339c5a3003 Mon Sep 17 00:00:00 2001 From: Soifou Date: Fri, 20 Feb 2026 12:11:03 +0100 Subject: [PATCH 1/2] fix(sources): fallback resolve when callback is never called MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Some community sources override `source:resolve` but may never call the callback, e.g. they check an option that ends up being nil and causes the async task to hang indefinitely (since it's waiting on a callback that will never come): ```lua function source:resolve(item, callback) if self.opts.resolve then -- this may be nil self.opts.resolve(item, callback) -- callback never called end end ``` The fix detects this by checking if the callback was invoked synchronously after resolve returns. If not, and there’s no return value indicating async work, we fall back to resolving with the original item. I believe returning the original item is a safer option, as it avoids indefinite hangs without requiring to strictly follow the resolve contract. --- lua/blink/cmp/sources/lib/provider/init.lua | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/lua/blink/cmp/sources/lib/provider/init.lua b/lua/blink/cmp/sources/lib/provider/init.lua index 8cd6f74f8..be17dce1a 100644 --- a/lua/blink/cmp/sources/lib/provider/init.lua +++ b/lua/blink/cmp/sources/lib/provider/init.lua @@ -155,12 +155,21 @@ function source:resolve(context, item) self.resolve_cache[item] = async.task.new(function(resolve) if self.module.resolve == nil then return resolve(item) end - return self.module:resolve(item, function(resolved_item) + local callback_called = false + local ok, ret = pcall(self.module.resolve, self.module, item, function(resolved_item) + callback_called = true -- HACK: it's out of spec to update keys not in resolveSupport.properties but some LSPs do it anyway local merged_item = vim.tbl_deep_extend('force', item, resolved_item or {}) local transformed_item = self:transform_items(context, { merged_item })[1] or merged_item vim.schedule(function() resolve(transformed_item) end) end) + + if not ok then + vim.notify('blink.cmp: source ' .. self.id .. ' resolve() error: ' .. tostring(ret), vim.log.levels.WARN) + return resolve(item) + end + + if not callback_called and ret == nil then resolve(item) end end) end return self.resolve_cache[item] From aa69bf1662e74e550647fb0b4620299d07c1764b Mon Sep 17 00:00:00 2001 From: Soifou Date: Wed, 11 Mar 2026 12:10:46 +0100 Subject: [PATCH 2/2] refactor: add timeout and better error handling for resolve() callbacks --- lua/blink/cmp/sources/lib/provider/init.lua | 29 +++++++++++++++++---- 1 file changed, 24 insertions(+), 5 deletions(-) diff --git a/lua/blink/cmp/sources/lib/provider/init.lua b/lua/blink/cmp/sources/lib/provider/init.lua index be17dce1a..9624d38d7 100644 --- a/lua/blink/cmp/sources/lib/provider/init.lua +++ b/lua/blink/cmp/sources/lib/provider/init.lua @@ -25,6 +25,7 @@ local source = {} local async = require('blink.cmp.lib.async') +local utils = require('blink.cmp.lib.utils') function source.new(id, config) assert(type(config.module) == 'string', 'Each source in config.sources.providers must have a "module" of type string') @@ -155,21 +156,39 @@ function source:resolve(context, item) self.resolve_cache[item] = async.task.new(function(resolve) if self.module.resolve == nil then return resolve(item) end - local callback_called = false - local ok, ret = pcall(self.module.resolve, self.module, item, function(resolved_item) - callback_called = true + local finished = false + local ok, err = pcall(self.module.resolve, self.module, item, function(resolved_item) + if finished then return end + finished = true + -- HACK: it's out of spec to update keys not in resolveSupport.properties but some LSPs do it anyway local merged_item = vim.tbl_deep_extend('force', item, resolved_item or {}) local transformed_item = self:transform_items(context, { merged_item })[1] or merged_item vim.schedule(function() resolve(transformed_item) end) end) + local function notify(msg) + utils.notify( + { { 'resolve() callback for source ' }, { self.id, 'DiagnosticInfo' }, { msg } }, + vim.log.levels.WARN + ) + end + if not ok then - vim.notify('blink.cmp: source ' .. self.id .. ' resolve() error: ' .. tostring(ret), vim.log.levels.WARN) + finished = true + notify(' threw an error: ' .. tostring(err)) return resolve(item) end - if not callback_called and ret == nil then resolve(item) end + -- Detect sources that never call the callback (in the timing specified) + local timeout_ms = 3000 + vim.defer_fn(function() + if not finished then + finished = true + notify(' timed out after ' .. timeout_ms .. 'ms. Falling back to unresolved item.') + resolve(item) + end + end, timeout_ms) end) end return self.resolve_cache[item]