summaryrefslogtreecommitdiff
path: root/lua/blink/cmp/sources/lib
diff options
context:
space:
mode:
authorMike Vink <mike@pionative.com>2025-01-19 13:52:52 +0100
committerMike Vink <mike@pionative.com>2025-01-19 13:52:52 +0100
commitb77413ff8f59f380612074f0c9bd49093d8db695 (patch)
tree32c39a811ba96ed4ab0a1c81cce9f8d518ed7e31 /lua/blink/cmp/sources/lib
Squashed 'mut/neovim/pack/plugins/start/blink.cmp/' content from commit 1cc3b1a
git-subtree-dir: mut/neovim/pack/plugins/start/blink.cmp git-subtree-split: 1cc3b1a908fbcfd15451c4772759549724f38524
Diffstat (limited to 'lua/blink/cmp/sources/lib')
-rw-r--r--lua/blink/cmp/sources/lib/init.lua304
-rw-r--r--lua/blink/cmp/sources/lib/provider/config.lua46
-rw-r--r--lua/blink/cmp/sources/lib/provider/init.lua179
-rw-r--r--lua/blink/cmp/sources/lib/provider/list.lua128
-rw-r--r--lua/blink/cmp/sources/lib/provider/override.lua17
-rw-r--r--lua/blink/cmp/sources/lib/queue.lua68
-rw-r--r--lua/blink/cmp/sources/lib/tree.lua168
-rw-r--r--lua/blink/cmp/sources/lib/types.lua31
-rw-r--r--lua/blink/cmp/sources/lib/utils.lua15
9 files changed, 956 insertions, 0 deletions
diff --git a/lua/blink/cmp/sources/lib/init.lua b/lua/blink/cmp/sources/lib/init.lua
new file mode 100644
index 0000000..9b56cf5
--- /dev/null
+++ b/lua/blink/cmp/sources/lib/init.lua
@@ -0,0 +1,304 @@
+local async = require('blink.cmp.lib.async')
+local config = require('blink.cmp.config')
+
+--- @class blink.cmp.Sources
+--- @field completions_queue blink.cmp.SourcesQueue | nil
+--- @field current_signature_help blink.cmp.Task | nil
+--- @field sources_registered boolean
+--- @field providers table<string, blink.cmp.SourceProvider>
+--- @field completions_emitter blink.cmp.EventEmitter<blink.cmp.SourceCompletionsEvent>
+---
+--- @field get_all_providers fun(): blink.cmp.SourceProvider[]
+--- @field get_enabled_provider_ids fun(mode: blink.cmp.Mode): string[]
+--- @field get_enabled_providers fun(mode: blink.cmp.Mode): table<string, blink.cmp.SourceProvider>
+--- @field get_provider_by_id fun(id: string): blink.cmp.SourceProvider
+--- @field get_trigger_characters fun(mode: blink.cmp.Mode): string[]
+---
+--- @field emit_completions fun(context: blink.cmp.Context, responses: table<string, blink.cmp.CompletionResponse>)
+--- @field request_completions fun(context: blink.cmp.Context)
+--- @field cancel_completions fun()
+--- @field apply_max_items_for_completions fun(context: blink.cmp.Context, items: blink.cmp.CompletionItem[]): blink.cmp.CompletionItem[]
+--- @field listen_on_completions fun(callback: fun(context: blink.cmp.Context, items: blink.cmp.CompletionItem[]))
+--- @field resolve fun(context: blink.cmp.Context, item: blink.cmp.CompletionItem): blink.cmp.Task
+--- @field execute fun(context: blink.cmp.Context, item: blink.cmp.CompletionItem): blink.cmp.Task
+---
+--- @field get_signature_help_trigger_characters fun(mode: blink.cmp.Mode): { trigger_characters: string[], retrigger_characters: string[] }
+--- @field get_signature_help fun(context: blink.cmp.SignatureHelpContext, callback: fun(signature_help: lsp.SignatureHelp | nil))
+--- @field cancel_signature_help fun()
+---
+--- @field reload fun(provider?: string)
+--- @field get_lsp_capabilities fun(override?: lsp.ClientCapabilities, include_nvim_defaults?: boolean): lsp.ClientCapabilities
+
+--- @class blink.cmp.SourceCompletionsEvent
+--- @field context blink.cmp.Context
+--- @field items table<string, blink.cmp.CompletionItem[]>
+
+--- @type blink.cmp.Sources
+--- @diagnostic disable-next-line: missing-fields
+local sources = {
+ completions_queue = nil,
+ providers = {},
+ completions_emitter = require('blink.cmp.lib.event_emitter').new('source_completions', 'BlinkCmpSourceCompletions'),
+}
+
+function sources.get_all_providers()
+ local providers = {}
+ for provider_id, _ in pairs(config.sources.providers) do
+ providers[provider_id] = sources.get_provider_by_id(provider_id)
+ end
+ return providers
+end
+
+function sources.get_enabled_provider_ids(mode)
+ local enabled_providers = mode ~= 'default' and config.sources[mode]
+ or config.sources.per_filetype[vim.bo.filetype]
+ or config.sources.default
+ if type(enabled_providers) == 'function' then return enabled_providers() end
+ --- @cast enabled_providers string[]
+ return enabled_providers
+end
+
+function sources.get_enabled_providers(mode)
+ local mode_providers = sources.get_enabled_provider_ids(mode)
+
+ --- @type table<string, blink.cmp.SourceProvider>
+ local providers = {}
+ for _, provider_id in ipairs(mode_providers) do
+ local provider = sources.get_provider_by_id(provider_id)
+ if provider:enabled() then providers[provider_id] = sources.get_provider_by_id(provider_id) end
+ end
+ return providers
+end
+
+function sources.get_provider_by_id(provider_id)
+ -- TODO: remove in v1.0
+ if not sources.providers[provider_id] and provider_id == 'luasnip' then
+ error(
+ "Luasnip has been moved to the `snippets` source, alongside a new preset system (`snippets.preset = 'luasnip'`). See the documentation for more information."
+ )
+ end
+
+ assert(
+ sources.providers[provider_id] ~= nil or config.sources.providers[provider_id] ~= nil,
+ 'Requested provider "'
+ .. provider_id
+ .. '" has not been configured. Available providers: '
+ .. vim.fn.join(vim.tbl_keys(sources.providers), ', ')
+ )
+
+ -- initialize the provider if it hasn't been initialized yet
+ if not sources.providers[provider_id] then
+ local provider_config = config.sources.providers[provider_id]
+ sources.providers[provider_id] = require('blink.cmp.sources.lib.provider').new(provider_id, provider_config)
+ end
+
+ return sources.providers[provider_id]
+end
+
+--- Completion ---
+
+function sources.get_trigger_characters(mode)
+ local providers = sources.get_enabled_providers(mode)
+ local trigger_characters = {}
+ for _, provider in pairs(providers) do
+ vim.list_extend(trigger_characters, provider:get_trigger_characters())
+ end
+ return trigger_characters
+end
+
+function sources.emit_completions(context, _items_by_provider)
+ local 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
+ sources.completions_emitter:emit({ context = context, items = items_by_provider })
+end
+
+function sources.request_completions(context)
+ -- create a new context if the id changed or if we haven't created one yet
+ if sources.completions_queue == nil or context.id ~= sources.completions_queue.id then
+ if sources.completions_queue ~= nil then sources.completions_queue:destroy() end
+ sources.completions_queue =
+ require('blink.cmp.sources.lib.queue').new(context, sources.get_all_providers(), sources.emit_completions)
+ -- send cached completions if they exist to immediately trigger updates
+ elseif sources.completions_queue:get_cached_completions() ~= nil then
+ sources.emit_completions(
+ context,
+ --- @diagnostic disable-next-line: param-type-mismatch
+ sources.completions_queue:get_cached_completions()
+ )
+ end
+
+ sources.completions_queue:get_completions(context)
+end
+
+function sources.cancel_completions()
+ if sources.completions_queue ~= nil then
+ sources.completions_queue:destroy()
+ sources.completions_queue = nil
+ end
+end
+
+--- Limits the number of items per source as configured
+function sources.apply_max_items_for_completions(context, items)
+ -- get the configured max items for each source
+ local total_items_for_sources = {}
+ local max_items_for_sources = {}
+ for id, source in pairs(sources.providers) do
+ max_items_for_sources[id] = source.config.max_items(context, items)
+ total_items_for_sources[id] = 0
+ end
+
+ -- no max items configured, return as-is
+ if #vim.tbl_keys(max_items_for_sources) == 0 then return items end
+
+ -- apply max items
+ local filtered_items = {}
+ for _, item in ipairs(items) do
+ local max_items = max_items_for_sources[item.source_id]
+ total_items_for_sources[item.source_id] = total_items_for_sources[item.source_id] + 1
+ if max_items == nil or total_items_for_sources[item.source_id] <= max_items then
+ table.insert(filtered_items, item)
+ end
+ end
+ return filtered_items
+end
+
+--- Resolve ---
+
+function sources.resolve(context, item)
+ --- @type blink.cmp.SourceProvider?
+ local item_source = nil
+ for _, source in pairs(sources.providers) do
+ if source.id == item.source_id then
+ item_source = source
+ break
+ end
+ end
+ if item_source == nil then
+ return async.task.new(function(resolve) resolve(item) end)
+ end
+
+ return item_source
+ :resolve(context, item)
+ :catch(function(err) vim.print('failed to resolve item with error: ' .. err) end)
+end
+
+--- Execute ---
+
+function sources.execute(context, item)
+ local item_source = nil
+ for _, source in pairs(sources.providers) do
+ if source.id == item.source_id then
+ item_source = source
+ break
+ end
+ end
+ if item_source == nil then
+ return async.task.new(function(resolve) resolve() end)
+ end
+
+ return item_source
+ :execute(context, item)
+ :catch(function(err) vim.print('failed to execute item with error: ' .. err) end)
+end
+
+--- Signature help ---
+
+function sources.get_signature_help_trigger_characters(mode)
+ local trigger_characters = {}
+ local retrigger_characters = {}
+
+ -- todo: should this be all sources? or should it follow fallbacks?
+ for _, source in pairs(sources.get_enabled_providers(mode)) do
+ local res = source:get_signature_help_trigger_characters()
+ vim.list_extend(trigger_characters, res.trigger_characters)
+ vim.list_extend(retrigger_characters, res.retrigger_characters)
+ end
+ return { trigger_characters = trigger_characters, retrigger_characters = retrigger_characters }
+end
+
+function sources.get_signature_help(context, callback)
+ local tasks = {}
+ for _, source in pairs(sources.providers) do
+ table.insert(tasks, source:get_signature_help(context))
+ end
+
+ sources.current_signature_help = async.task.await_all(tasks):map(function(signature_helps)
+ signature_helps = vim.tbl_filter(function(signature_help) return signature_help ~= nil end, signature_helps)
+ callback(signature_helps[1])
+ end)
+end
+
+function sources.cancel_signature_help()
+ if sources.current_signature_help ~= nil then
+ sources.current_signature_help:cancel()
+ sources.current_signature_help = nil
+ end
+end
+
+--- Misc ---
+
+--- For external integrations to force reloading the source
+function sources.reload(provider)
+ -- Reload specific provider
+ if provider ~= nil then
+ assert(type(provider) == 'string', 'Expected string for provider')
+ assert(
+ sources.providers[provider] ~= nil or config.sources.providers[provider] ~= nil,
+ 'Source ' .. provider .. ' does not exist'
+ )
+ if sources.providers[provider] ~= nil then sources.providers[provider]:reload() end
+ return
+ end
+
+ -- Reload all providers
+ for _, source in pairs(sources.providers) do
+ source:reload()
+ end
+end
+
+function sources.get_lsp_capabilities(override, include_nvim_defaults)
+ return vim.tbl_deep_extend('force', include_nvim_defaults and vim.lsp.protocol.make_client_capabilities() or {}, {
+ textDocument = {
+ completion = {
+ completionItem = {
+ snippetSupport = true,
+ commitCharactersSupport = false, -- todo:
+ documentationFormat = { 'markdown', 'plaintext' },
+ deprecatedSupport = true,
+ preselectSupport = false, -- todo:
+ tagSupport = { valueSet = { 1 } }, -- deprecated
+ insertReplaceSupport = true, -- todo:
+ resolveSupport = {
+ properties = {
+ 'documentation',
+ 'detail',
+ 'additionalTextEdits',
+ -- todo: support more properties? should test if it improves latency
+ },
+ },
+ insertTextModeSupport = {
+ -- todo: support adjustIndentation
+ valueSet = { 1 }, -- asIs
+ },
+ labelDetailsSupport = true,
+ },
+ completionList = {
+ itemDefaults = {
+ 'commitCharacters',
+ 'editRange',
+ 'insertTextFormat',
+ 'insertTextMode',
+ 'data',
+ },
+ },
+
+ contextSupport = true,
+ insertTextMode = 1, -- asIs
+ },
+ },
+ }, override or {})
+end
+
+return sources
diff --git a/lua/blink/cmp/sources/lib/provider/config.lua b/lua/blink/cmp/sources/lib/provider/config.lua
new file mode 100644
index 0000000..cda6a52
--- /dev/null
+++ b/lua/blink/cmp/sources/lib/provider/config.lua
@@ -0,0 +1,46 @@
+--- @class blink.cmp.SourceProviderConfigWrapper
+--- @field new fun(config: blink.cmp.SourceProviderConfig): blink.cmp.SourceProviderConfigWrapper
+---
+--- @field name string
+--- @field module string
+--- @field enabled fun(): boolean
+--- @field async fun(ctx: blink.cmp.Context): boolean
+--- @field timeout_ms fun(ctx: blink.cmp.Context): number
+--- @field transform_items fun(ctx: blink.cmp.Context, items: blink.cmp.CompletionItem[]): blink.cmp.CompletionItem[]
+--- @field should_show_items fun(ctx: blink.cmp.Context, items: blink.cmp.CompletionItem[]): boolean
+--- @field max_items? fun(ctx: blink.cmp.Context, items: blink.cmp.CompletionItem[]): number
+--- @field min_keyword_length fun(ctx: blink.cmp.Context): number
+--- @field fallbacks fun(ctx: blink.cmp.Context): string[]
+--- @field score_offset fun(ctx: blink.cmp.Context): number
+
+--- @class blink.cmp.SourceProviderConfigWrapper
+--- @diagnostic disable-next-line: missing-fields
+local wrapper = {}
+
+function wrapper.new(config)
+ local function call_or_get(fn_or_val, default)
+ if fn_or_val == nil then
+ return function() return default end
+ end
+ return function(...)
+ if type(fn_or_val) == 'function' then return fn_or_val(...) end
+ return fn_or_val
+ end
+ end
+
+ local self = setmetatable({}, { __index = config })
+ self.name = config.name
+ self.module = config.module
+ self.enabled = call_or_get(config.enabled, true)
+ self.async = call_or_get(config.async, false)
+ self.timeout_ms = call_or_get(config.timeout_ms, 2000)
+ self.transform_items = config.transform_items or function(_, items) return items end
+ self.should_show_items = call_or_get(config.should_show_items, true)
+ self.max_items = call_or_get(config.max_items, nil)
+ self.min_keyword_length = call_or_get(config.min_keyword_length, 0)
+ self.fallbacks = call_or_get(config.fallbacks, {})
+ self.score_offset = call_or_get(config.score_offset, 0)
+ return self
+end
+
+return wrapper
diff --git a/lua/blink/cmp/sources/lib/provider/init.lua b/lua/blink/cmp/sources/lib/provider/init.lua
new file mode 100644
index 0000000..0494dcc
--- /dev/null
+++ b/lua/blink/cmp/sources/lib/provider/init.lua
@@ -0,0 +1,179 @@
+--- Wraps the sources to respect the configuration options and provide a unified interface
+--- @class blink.cmp.SourceProvider
+--- @field id string
+--- @field name string
+--- @field config blink.cmp.SourceProviderConfigWrapper
+--- @field module blink.cmp.Source
+--- @field list blink.cmp.SourceProviderList | nil
+--- @field resolve_tasks table<blink.cmp.CompletionItem, blink.cmp.Task>
+---
+--- @field new fun(id: string, config: blink.cmp.SourceProviderConfig): blink.cmp.SourceProvider
+--- @field enabled fun(self: blink.cmp.SourceProvider): boolean
+--- @field get_trigger_characters fun(self: blink.cmp.SourceProvider): string[]
+--- @field get_completions fun(self: blink.cmp.SourceProvider, context: blink.cmp.Context, on_items: fun(items: blink.cmp.CompletionItem[], is_cached: boolean))
+--- @field should_show_items fun(self: blink.cmp.SourceProvider, context: blink.cmp.Context, items: blink.cmp.CompletionItem[]): boolean
+--- @field transform_items fun(self: blink.cmp.SourceProvider, context: blink.cmp.Context, items: blink.cmp.CompletionItem[]): blink.cmp.CompletionItem[]
+--- @field resolve fun(self: blink.cmp.SourceProvider, context: blink.cmp.Context, item: blink.cmp.CompletionItem): blink.cmp.Task
+--- @field execute fun(self: blink.cmp.SourceProvider, context: blink.cmp.Context, item: blink.cmp.CompletionItem, callback: fun()): blink.cmp.Task
+--- @field get_signature_help_trigger_characters fun(self: blink.cmp.SourceProvider): { trigger_characters: string[], retrigger_characters: string[] }
+--- @field get_signature_help fun(self: blink.cmp.SourceProvider, context: blink.cmp.SignatureHelpContext): blink.cmp.Task
+--- @field reload (fun(self: blink.cmp.SourceProvider): nil) | nil
+
+--- @type blink.cmp.SourceProvider
+--- @diagnostic disable-next-line: missing-fields
+local source = {}
+
+local async = require('blink.cmp.lib.async')
+
+function source.new(id, config)
+ assert(type(config.name) == 'string', 'Each source in config.sources.providers must have a "name" of type string')
+ assert(type(config.module) == 'string', 'Each source in config.sources.providers must have a "module" of type string')
+
+ local self = setmetatable({}, { __index = source })
+ self.id = id
+ self.name = config.name
+ self.module = require('blink.cmp.sources.lib.provider.override').new(
+ require(config.module).new(config.opts or {}, config),
+ config.override
+ )
+ self.config = require('blink.cmp.sources.lib.provider.config').new(config)
+ self.list = nil
+ self.resolve_tasks = {}
+
+ return self
+end
+
+function source:enabled()
+ -- user defined
+ if not self.config.enabled() then return false end
+
+ -- source defined
+ if self.module.enabled == nil then return true end
+ return self.module:enabled()
+end
+
+--- Completion ---
+
+function source:get_trigger_characters()
+ if self.module.get_trigger_characters == nil then return {} end
+ return self.module:get_trigger_characters()
+end
+
+function source:get_completions(context, on_items)
+ -- return the previous successful completions if the context is the same
+ -- and the data doesn't need to be updated
+ -- or if the list is async, since we don't want to cause a flash of no items
+ if self.list ~= nil and self.list:is_valid_for_context(context) then
+ self.list:set_on_items(on_items)
+ self.list:emit(true)
+ return
+ end
+
+ -- the source indicates we should refetch when this character is typed
+ local trigger_character = context.trigger.character
+ and vim.tbl_contains(self:get_trigger_characters(), context.trigger.character)
+
+ -- The TriggerForIncompleteCompletions kind is handled by the source provider itself
+ local source_context = require('blink.cmp.lib.utils').shallow_copy(context)
+ source_context.trigger = trigger_character
+ and { kind = vim.lsp.protocol.CompletionTriggerKind.TriggerCharacter, character = context.trigger.character }
+ or { kind = vim.lsp.protocol.CompletionTriggerKind.Invoked }
+
+ local async_initial_items = self.list ~= nil and self.list.context.id == context.id and self.list.items or {}
+ if self.list ~= nil then self.list:destroy() end
+
+ self.list = require('blink.cmp.sources.lib.provider.list').new(
+ self,
+ context,
+ on_items,
+ -- HACK: if the source is async, we're not reusing the previous list and the response was marked as incomplete,
+ -- the user will see a flash of no items from the provider, since the list emits immediately. So we hack around
+ -- this for now
+ { async_initial_items = async_initial_items }
+ )
+end
+
+function source:should_show_items(context, items)
+ -- if keyword length is configured, check if the context is long enough
+ local provider_min_keyword_length = self.config.min_keyword_length(context)
+
+ -- for manual trigger, we ignore the min_keyword_length set globally, but still respect per-provider
+ local global_min_keyword_length = 0
+ if context.trigger.initial_kind ~= 'manual' then
+ local global_min_keyword_length_func_or_num = require('blink.cmp.config').sources.min_keyword_length
+ if type(global_min_keyword_length_func_or_num) == 'function' then
+ global_min_keyword_length = global_min_keyword_length_func_or_num(context)
+ else
+ global_min_keyword_length = global_min_keyword_length_func_or_num
+ end
+ end
+
+ local min_keyword_length = math.max(provider_min_keyword_length, global_min_keyword_length)
+ local current_keyword_length = context.bounds.length
+ if current_keyword_length < min_keyword_length then return false end
+
+ if self.config.should_show_items == nil then return true end
+ return self.config.should_show_items(context, items)
+end
+
+function source:transform_items(context, items)
+ if self.config.transform_items ~= nil then items = self.config.transform_items(context, items) end
+ items = require('blink.cmp.config').sources.transform_items(context, items)
+ return items
+end
+
+--- Resolve ---
+
+function source:resolve(context, item)
+ local tasks = self.resolve_tasks
+ if tasks[item] == nil or tasks[item].status == async.STATUS.CANCELLED then
+ tasks[item] = async.task.new(function(resolve)
+ if self.module.resolve == nil then return resolve(item) end
+
+ return self.module:resolve(item, function(resolved_item)
+ -- 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)
+ end)
+ end
+ return tasks[item]
+end
+
+--- Execute ---
+
+function source:execute(context, item)
+ if self.module.execute == nil then
+ return async.task.new(function(resolve) resolve() end)
+ end
+ return async.task.new(function(resolve) self.module:execute(context, item, resolve) end)
+end
+
+--- Signature help ---
+
+function source:get_signature_help_trigger_characters()
+ if self.module.get_signature_help_trigger_characters == nil then
+ return { trigger_characters = {}, retrigger_characters = {} }
+ end
+ return self.module:get_signature_help_trigger_characters()
+end
+
+function source:get_signature_help(context)
+ return async.task.new(function(resolve)
+ if self.module.get_signature_help == nil then return resolve(nil) end
+ return self.module:get_signature_help(context, function(signature_help)
+ vim.schedule(function() resolve(signature_help) end)
+ end)
+ end)
+end
+
+--- Misc ---
+
+--- For external integrations to force reloading the source
+function source:reload()
+ if self.module.reload == nil then return end
+ self.module:reload()
+end
+
+return source
diff --git a/lua/blink/cmp/sources/lib/provider/list.lua b/lua/blink/cmp/sources/lib/provider/list.lua
new file mode 100644
index 0000000..17d4bdf
--- /dev/null
+++ b/lua/blink/cmp/sources/lib/provider/list.lua
@@ -0,0 +1,128 @@
+--- @class blink.cmp.SourceProviderList
+--- @field provider blink.cmp.SourceProvider
+--- @field context blink.cmp.Context
+--- @field items blink.cmp.CompletionItem[]
+--- @field on_items fun(items: blink.cmp.CompletionItem[], is_cached: boolean)
+--- @field has_completed boolean
+--- @field is_incomplete_backward boolean
+--- @field is_incomplete_forward boolean
+--- @field cancel_completions? fun(): nil
+---
+--- @field new fun(provider: blink.cmp.SourceProvider,context: blink.cmp.Context, on_items: fun(items: blink.cmp.CompletionItem[], is_cached: boolean), opts: blink.cmp.SourceProviderListOpts): blink.cmp.SourceProviderList
+--- @field append fun(self: blink.cmp.SourceProviderList, response: blink.cmp.CompletionResponse)
+--- @field emit fun(self: blink.cmp.SourceProviderList, is_cached?: boolean)
+--- @field destroy fun(self: blink.cmp.SourceProviderList): nil
+--- @field set_on_items fun(self: blink.cmp.SourceProviderList, on_items: fun(items: blink.cmp.CompletionItem[], is_cached: boolean))
+--- @field is_valid_for_context fun(self: blink.cmp.SourceProviderList, context: blink.cmp.Context): boolean
+---
+--- @class blink.cmp.SourceProviderListOpts
+--- @field async_initial_items blink.cmp.CompletionItem[]
+
+--- @type blink.cmp.SourceProviderList
+--- @diagnostic disable-next-line: missing-fields
+local list = {}
+
+function list.new(provider, context, on_items, opts)
+ --- @type blink.cmp.SourceProviderList
+ local self = setmetatable({
+ provider = provider,
+ context = context,
+ items = opts.async_initial_items,
+ on_items = on_items,
+
+ has_completed = false,
+ is_incomplete_backward = true,
+ is_incomplete_forward = true,
+ }, { __index = list })
+
+ -- Immediately fetch completions
+ local default_response = {
+ is_incomplete_forward = true,
+ is_incomplete_backward = true,
+ items = {},
+ }
+ if self.provider.module.get_completions == nil then
+ self:append(default_response)
+ else
+ self.cancel_completions = self.provider.module:get_completions(
+ self.context,
+ function(response) self:append(response or default_response) end
+ )
+ end
+
+ -- if async, immediately send the default response/initial items
+ local is_async = self.provider.config.async(self.context)
+ if is_async and not self.has_completed then self:emit() end
+
+ -- if not async and timeout is set, send the default response after the timeout
+ local timeout_ms = self.provider.config.timeout_ms(self.context)
+ if not is_async and timeout_ms > 0 then
+ vim.defer_fn(function()
+ if not self.has_completed then self:append(default_response) end
+ end, timeout_ms)
+ end
+
+ return self
+end
+
+function list:append(response)
+ if self.has_completed and #response.items == 0 then return end
+
+ if not self.has_completed then
+ self.has_completed = true
+ self.is_incomplete_backward = response.is_incomplete_backward
+ self.is_incomplete_forward = response.is_incomplete_forward
+ self.items = {}
+ end
+
+ -- add metadata and default kind
+ local source_score_offset = self.provider.config.score_offset(self.context) or 0
+ for _, item in ipairs(response.items) do
+ item.score_offset = (item.score_offset or 0) + source_score_offset
+ item.cursor_column = self.context.cursor[2]
+ item.source_id = self.provider.id
+ item.source_name = self.provider.name
+ item.kind = item.kind or require('blink.cmp.types').CompletionItemKind.Property
+ end
+
+ -- combine with existing items
+ local new_items = {}
+ vim.list_extend(new_items, self.items)
+ vim.list_extend(new_items, response.items)
+ self.items = new_items
+
+ -- run provider-local and global transform_items functions
+ self.items = self.provider:transform_items(self.context, self.items)
+
+ self:emit()
+end
+
+function list:emit(is_cached)
+ if is_cached == nil then is_cached = false end
+ self.on_items(self.items, is_cached)
+end
+
+function list:destroy()
+ if self.cancel_completions ~= nil then self.cancel_completions() end
+ self.on_items = function() end
+end
+
+function list:set_on_items(on_items) self.on_items = on_items end
+
+function list:is_valid_for_context(new_context)
+ if self.context.id ~= new_context.id then return false end
+
+ -- get the text for the current and queued context
+ local old_context_query = self.context.line:sub(self.context.bounds.start_col, self.context.cursor[2])
+ local new_context_query = new_context.line:sub(new_context.bounds.start_col, new_context.cursor[2])
+
+ -- check if the texts are overlapping
+ local is_before = vim.startswith(old_context_query, new_context_query)
+ local is_after = vim.startswith(new_context_query, old_context_query)
+
+ return (is_before and not self.is_incomplete_backward)
+ or (is_after and not self.is_incomplete_forward)
+ or (is_after == is_before and not (self.is_incomplete_backward or self.is_incomplete_forward))
+end
+
+return list
diff --git a/lua/blink/cmp/sources/lib/provider/override.lua b/lua/blink/cmp/sources/lib/provider/override.lua
new file mode 100644
index 0000000..96e8edd
--- /dev/null
+++ b/lua/blink/cmp/sources/lib/provider/override.lua
@@ -0,0 +1,17 @@
+--- @class blink.cmp.Override : blink.cmp.Source
+--- @field new fun(module: blink.cmp.Source, override_config: blink.cmp.SourceOverride): blink.cmp.Override
+
+local override = {}
+
+function override.new(module, override_config)
+ override_config = override_config or {}
+
+ return setmetatable({}, {
+ __index = function(_, key)
+ if override_config[key] ~= nil then return function(_, ...) return override_config[key](module, ...) end end
+ return module[key]
+ end,
+ })
+end
+
+return override
diff --git a/lua/blink/cmp/sources/lib/queue.lua b/lua/blink/cmp/sources/lib/queue.lua
new file mode 100644
index 0000000..3947902
--- /dev/null
+++ b/lua/blink/cmp/sources/lib/queue.lua
@@ -0,0 +1,68 @@
+local async = require('blink.cmp.lib.async')
+
+--- @class blink.cmp.SourcesQueue
+--- @field id number
+--- @field providers table<string, blink.cmp.SourceProvider>
+--- @field request blink.cmp.Task | nil
+--- @field queued_request_context blink.cmp.Context | nil
+--- @field cached_items_by_provider table<string, blink.cmp.CompletionResponse> | nil
+--- @field on_completions_callback fun(context: blink.cmp.Context, responses: table<string, blink.cmp.CompletionResponse>)
+---
+--- @field new fun(context: blink.cmp.Context, providers: table<string, blink.cmp.SourceProvider>, on_completions_callback: fun(context: blink.cmp.Context, responses: table<string, blink.cmp.CompletionResponse>)): blink.cmp.SourcesQueue
+--- @field get_cached_completions fun(self: blink.cmp.SourcesQueue): table<string, blink.cmp.CompletionResponse> | nil
+--- @field get_completions fun(self: blink.cmp.SourcesQueue, context: blink.cmp.Context)
+--- @field destroy fun(self: blink.cmp.SourcesQueue)
+
+--- @type blink.cmp.SourcesQueue
+--- @diagnostic disable-next-line: missing-fields
+local queue = {}
+
+function queue.new(context, providers, on_completions_callback)
+ local self = setmetatable({}, { __index = queue })
+ self.id = context.id
+ self.providers = providers
+
+ self.request = nil
+ self.queued_request_context = nil
+ self.on_completions_callback = on_completions_callback
+
+ return self
+end
+
+function queue:get_cached_completions() return self.cached_items_by_provider end
+
+function queue:get_completions(context)
+ assert(context.id == self.id, 'Requested completions on a sources context with a different context ID')
+
+ if self.request ~= nil then
+ if self.request.status == async.STATUS.RUNNING then
+ self.queued_request_context = context
+ return
+ else
+ self.request:cancel()
+ end
+ end
+
+ -- Create a task to get the completions, send responses upstream
+ -- and run the queued request, if it exists
+ local tree = require('blink.cmp.sources.lib.tree').new(context, vim.tbl_values(self.providers))
+ self.request = tree:get_completions(context, function(items_by_provider)
+ self.cached_items_by_provider = items_by_provider
+ self.on_completions_callback(context, items_by_provider)
+
+ -- run the queued request, if it exists
+ local queued_context = self.queued_request_context
+ if queued_context ~= nil then
+ self.queued_request_context = nil
+ self.request:cancel()
+ self:get_completions(queued_context)
+ end
+ end)
+end
+
+function queue:destroy()
+ self.on_completions_callback = function() end
+ if self.request ~= nil then self.request:cancel() end
+end
+
+return queue
diff --git a/lua/blink/cmp/sources/lib/tree.lua b/lua/blink/cmp/sources/lib/tree.lua
new file mode 100644
index 0000000..c89d04d
--- /dev/null
+++ b/lua/blink/cmp/sources/lib/tree.lua
@@ -0,0 +1,168 @@
+--- @class blink.cmp.SourceTreeNode
+--- @field id string
+--- @field source blink.cmp.SourceProvider
+--- @field dependencies blink.cmp.SourceTreeNode[]
+--- @field dependents blink.cmp.SourceTreeNode[]
+
+--- @class blink.cmp.SourceTree
+--- @field nodes blink.cmp.SourceTreeNode[]
+--- @field new fun(context: blink.cmp.Context, all_sources: blink.cmp.SourceProvider[]): blink.cmp.SourceTree
+--- @field get_completions fun(self: blink.cmp.SourceTree, context: blink.cmp.Context, on_items_by_provider: fun(items_by_provider: table<string, blink.cmp.CompletionItem[]>)): blink.cmp.Task
+--- @field emit_completions fun(self: blink.cmp.SourceTree, items_by_provider: table<string, blink.cmp.CompletionItem[]>, on_items_by_provider: fun(items_by_provider: table<string, blink.cmp.CompletionItem[]>)): nil
+--- @field get_top_level_nodes fun(self: blink.cmp.SourceTree): blink.cmp.SourceTreeNode[]
+--- @field detect_cycle fun(node: blink.cmp.SourceTreeNode, visited?: table<string, boolean>, path?: table<string, boolean>): boolean
+
+local utils = require('blink.cmp.lib.utils')
+local async = require('blink.cmp.lib.async')
+
+--- @type blink.cmp.SourceTree
+--- @diagnostic disable-next-line: missing-fields
+local tree = {}
+
+--- @param context blink.cmp.Context
+--- @param all_sources blink.cmp.SourceProvider[]
+function tree.new(context, all_sources)
+ -- only include enabled sources for the given context
+ local sources = vim.tbl_filter(
+ function(source) return vim.tbl_contains(context.providers, source.id) and source:enabled(context) end,
+ all_sources
+ )
+ local source_ids = vim.tbl_map(function(source) return source.id end, sources)
+
+ -- create a node for each source
+ local nodes = vim.tbl_map(
+ function(source) return { id = source.id, source = source, dependencies = {}, dependents = {} } end,
+ sources
+ )
+
+ -- build the tree
+ for idx, source in ipairs(sources) do
+ local node = nodes[idx]
+ for _, fallback_source_id in ipairs(source.config.fallbacks(context, source_ids)) do
+ local fallback_node = nodes[utils.index_of(source_ids, fallback_source_id)]
+ if fallback_node ~= nil then
+ table.insert(node.dependents, fallback_node)
+ table.insert(fallback_node.dependencies, node)
+ end
+ end
+ end
+
+ -- circular dependency check
+ for _, node in ipairs(nodes) do
+ tree.detect_cycle(node)
+ end
+
+ return setmetatable({ nodes = nodes }, { __index = tree })
+end
+
+function tree:get_completions(context, on_items_by_provider)
+ local should_push_upstream = false
+ local items_by_provider = {}
+ local is_all_cached = true
+ local nodes_falling_back = {}
+
+ --- @param node blink.cmp.SourceTreeNode
+ local function get_completions_for_node(node)
+ -- check that all the dependencies have been triggered, and are falling back
+ for _, dependency in ipairs(node.dependencies) do
+ if not nodes_falling_back[dependency.id] then return async.task.empty() end
+ end
+
+ return async.task.new(function(resolve, reject)
+ return node.source:get_completions(context, function(items, is_cached)
+ items_by_provider[node.id] = items
+ is_all_cached = is_all_cached and is_cached
+
+ if should_push_upstream then self:emit_completions(items_by_provider, on_items_by_provider) end
+ if #items ~= 0 then return resolve() end
+
+ -- run dependents if the source returned 0 items
+ nodes_falling_back[node.id] = true
+ local tasks = vim.tbl_map(function(dependent) return get_completions_for_node(dependent) end, node.dependents)
+ async.task.await_all(tasks):map(resolve):catch(reject)
+ end)
+ end)
+ end
+
+ -- run the top level nodes and let them fall back to their dependents if needed
+ local tasks = vim.tbl_map(function(node) return get_completions_for_node(node) end, self:get_top_level_nodes())
+ return async.task
+ .await_all(tasks)
+ :map(function()
+ should_push_upstream = true
+
+ -- if atleast one of the results wasn't cached, emit the results
+ 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)
+end
+
+function tree:emit_completions(items_by_provider, on_items_by_provider)
+ local nodes_falling_back = {}
+ local final_items_by_provider = {}
+
+ local add_node_items
+ add_node_items = function(node)
+ for _, dependency in ipairs(node.dependencies) do
+ if not nodes_falling_back[dependency.id] then return end
+ end
+ local items = items_by_provider[node.id]
+ if items ~= nil and #items > 0 then
+ final_items_by_provider[node.id] = items
+ else
+ nodes_falling_back[node.id] = true
+ for _, dependent in ipairs(node.dependents) do
+ add_node_items(dependent)
+ end
+ end
+ end
+
+ for _, node in ipairs(self:get_top_level_nodes()) do
+ add_node_items(node)
+ end
+
+ on_items_by_provider(final_items_by_provider)
+end
+
+--- Internal ---
+
+function tree:get_top_level_nodes()
+ local top_level_nodes = {}
+ for _, node in ipairs(self.nodes) do
+ if #node.dependencies == 0 then table.insert(top_level_nodes, node) end
+ end
+ return top_level_nodes
+end
+
+--- Helper function to detect cycles using DFS
+--- @param node blink.cmp.SourceTreeNode
+--- @param visited? table<string, boolean>
+--- @param path? table<string, boolean>
+--- @return boolean
+function tree.detect_cycle(node, visited, path)
+ visited = visited or {}
+ path = path or {}
+
+ if path[node.id] then
+ -- Found a cycle - construct the cycle path for error message
+ local cycle = { node.id }
+ for id, _ in pairs(path) do
+ table.insert(cycle, id)
+ end
+ error('Circular dependency detected: ' .. table.concat(cycle, ' -> '))
+ end
+
+ if visited[node.id] then return false end
+
+ visited[node.id] = true
+ path[node.id] = true
+
+ for _, dependent in ipairs(node.dependents) do
+ if tree.detect_cycle(dependent, visited, path) then return true end
+ end
+
+ path[node.id] = nil
+ return false
+end
+
+return tree
diff --git a/lua/blink/cmp/sources/lib/types.lua b/lua/blink/cmp/sources/lib/types.lua
new file mode 100644
index 0000000..b2bd708
--- /dev/null
+++ b/lua/blink/cmp/sources/lib/types.lua
@@ -0,0 +1,31 @@
+--- @class blink.cmp.CompletionTriggerContext
+--- @field kind number
+--- @field character string | nil
+
+--- @class blink.cmp.CompletionResponse
+--- @field is_incomplete_forward boolean
+--- @field is_incomplete_backward boolean
+--- @field items blink.cmp.CompletionItem[]
+
+--- @class blink.cmp.Source
+--- @field new fun(opts: table, config: blink.cmp.SourceProviderConfig): blink.cmp.Source
+--- @field enabled? fun(self: blink.cmp.Source): boolean
+--- @field get_trigger_characters? fun(self: blink.cmp.Source): string[]
+--- @field get_completions? fun(self: blink.cmp.Source, context: blink.cmp.Context, callback: fun(response?: blink.cmp.CompletionResponse)): (fun(): nil) | nil
+--- @field should_show_completions? fun(self: blink.cmp.Source, context: blink.cmp.Context, response: blink.cmp.CompletionResponse): boolean
+--- @field resolve? fun(self: blink.cmp.Source, item: blink.cmp.CompletionItem, callback: fun(resolved_item?: lsp.CompletionItem)): ((fun(): nil) | nil)
+--- @field execute? fun(self: blink.cmp.Source, context: blink.cmp.Context, item: blink.cmp.CompletionItem, callback: fun()): (fun(): nil) | nil
+--- @field get_signature_help_trigger_characters? fun(self: blink.cmp.Source): string[]
+--- @field get_signature_help? fun(self: blink.cmp.Source, context: blink.cmp.SignatureHelpContext, callback: fun(signature_help: lsp.SignatureHelp | nil)): (fun(): nil) | nil
+--- @field reload? fun(self: blink.cmp.Source): nil
+
+--- @class blink.cmp.SourceOverride
+--- @field enabled? fun(self: blink.cmp.Source): boolean
+--- @field get_trigger_characters? fun(self: blink.cmp.Source): string[]
+--- @field get_completions? fun(self: blink.cmp.Source, context: blink.cmp.Context, callback: fun(response: blink.cmp.CompletionResponse | nil)): (fun(): nil) | nil
+--- @field should_show_completions? fun(self: blink.cmp.Source, context: blink.cmp.Context, response: blink.cmp.CompletionResponse): boolean
+--- @field resolve? fun(self: blink.cmp.Source, item: blink.cmp.CompletionItem, callback: fun(resolved_item: lsp.CompletionItem | nil)): ((fun(): nil) | nil)
+--- @field execute? fun(self: blink.cmp.Source, context: blink.cmp.Context, item: blink.cmp.CompletionItem, callback: fun()): (fun(): nil) | nil
+--- @field get_signature_help_trigger_characters? fun(self: blink.cmp.Source): string[]
+--- @field get_signature_help? fun(self: blink.cmp.Source, context: blink.cmp.SignatureHelpContext, callback: fun(signature_help: lsp.SignatureHelp | nil)): (fun(): nil) | nil
+--- @field reload? fun(self: blink.cmp.Source): nil
diff --git a/lua/blink/cmp/sources/lib/utils.lua b/lua/blink/cmp/sources/lib/utils.lua
new file mode 100644
index 0000000..97025bb
--- /dev/null
+++ b/lua/blink/cmp/sources/lib/utils.lua
@@ -0,0 +1,15 @@
+local utils = {}
+
+--- @param item blink.cmp.CompletionItem
+--- @return lsp.CompletionItem
+function utils.blink_item_to_lsp_item(item)
+ local lsp_item = vim.deepcopy(item)
+ lsp_item.score_offset = nil
+ lsp_item.source_id = nil
+ lsp_item.source_name = nil
+ lsp_item.cursor_column = nil
+ lsp_item.client_id = nil
+ return lsp_item
+end
+
+return utils