Skip to content

Commit 1f0c0f3

Browse files
authored
feat: drop source groups in favor of fallback_for (#83)
1 parent b330b61 commit 1f0c0f3

File tree

8 files changed

+161
-103
lines changed

8 files changed

+161
-103
lines changed

README.md

+39-36
Original file line numberDiff line numberDiff line change
@@ -227,48 +227,51 @@ MiniDeps.add({
227227
-- returns no completion items
228228
-- WARN: This API will have breaking changes during the beta
229229
providers = {
230-
{
231-
{ 'blink.cmp.sources.lsp' },
232-
{ 'blink.cmp.sources.path' },
233-
{ 'blink.cmp.sources.snippets', score_offset = -3 },
234-
},
235-
{ { 'blink.cmp.sources.buffer' } },
230+
{ 'blink.cmp.sources.lsp', name = 'LSP' },
231+
{ 'blink.cmp.sources.path', name = 'Path', score_offset = 3 },
232+
{ 'blink.cmp.sources.snippets', score_offset = -3 },
233+
{ 'blink.cmp.sources.buffer', name = 'Buffer', fallback_for = { 'LSP' } },
236234
},
237235
-- FOR REF: full example
238236
providers = {
237+
-- all of these properties work on every source
238+
{
239+
'blink.cmp.sources.lsp',
240+
name = 'LSP',
241+
keyword_length = 0,
242+
score_offset = 0,
243+
trigger_characters = { 'f', 'o', 'o' },
244+
},
245+
-- the following two sources have additional options
239246
{
240-
-- all of these properties work on every source
241-
{
242-
'blink.cmp.sources.lsp',
243-
keyword_length = 0,
244-
score_offset = 0,
245-
trigger_characters = { 'f', 'o', 'o' },
246-
opts = {},
247-
},
248-
-- the follow two sources have additional options
249-
{
250-
'blink.cmp.sources.path',
251-
opts = {
252-
trailing_slash = false,
253-
label_trailing_slash = true,
254-
get_cwd = function(context) return vim.fn.expand(('#%d:p:h'):format(context.bufnr)) end,
255-
show_hidden_files_by_default = true,
256-
}
257-
},
258-
{
259-
'blink.cmp.sources.snippets',
260-
score_offset = -3,
261-
-- similar to https://github.com/garymjr/nvim-snippets
262-
opts = {
263-
friendly_snippets = true,
264-
search_paths = { vim.fn.stdpath('config') .. '/snippets' },
265-
global_snippets = { 'all' },
266-
extended_filetypes = {},
267-
ignored_filetypes = {},
268-
},
247+
'blink.cmp.sources.path',
248+
name = 'Path',
249+
score_offset = 3,
250+
opts = {
251+
trailing_slash = false,
252+
label_trailing_slash = true,
253+
get_cwd = function(context) return vim.fn.expand(('#%d:p:h'):format(context.bufnr)) end,
254+
show_hidden_files_by_default = true,
255+
}
256+
},
257+
{
258+
'blink.cmp.sources.snippets',
259+
name = 'Snippets',
260+
score_offset = -3,
261+
-- similar to https://github.com/garymjr/nvim-snippets
262+
opts = {
263+
friendly_snippets = true,
264+
search_paths = { vim.fn.stdpath('config') .. '/snippets' },
265+
global_snippets = { 'all' },
266+
extended_filetypes = {},
267+
ignored_filetypes = {},
269268
},
270269
},
271-
{ { 'blink.cmp.sources.buffer' } }
270+
{
271+
'blink.cmp.sources.buffer',
272+
name = 'Buffer',
273+
fallback_for = { 'LSP' },
274+
}
272275
}
273276
},
274277

lua/blink/cmp/config.lua

+7-7
Original file line numberDiff line numberDiff line change
@@ -47,10 +47,12 @@
4747
--- @field signature_help? blink.cmp.SignatureHelpTriggerConfig
4848

4949
--- @class blink.cmp.SourceConfig
50-
--- @field providers? blink.cmp.SourceProviderConfig[][]
50+
--- @field providers? blink.cmp.SourceProviderConfig[]
5151
---
5252
--- @class blink.cmp.SourceProviderConfig
5353
--- @field [1]? string
54+
--- @field name string
55+
--- @field fallback_for? string[] | nil
5456
--- @field keyword_length? number | nil
5557
--- @field score_offset? number | nil
5658
--- @field deduplicate? blink.cmp.DeduplicateConfig | nil
@@ -228,12 +230,10 @@ local config = {
228230
-- returns no completion items
229231
-- WARN: This API will have breaking changes during the beta
230232
providers = {
231-
{
232-
{ 'blink.cmp.sources.lsp' },
233-
{ 'blink.cmp.sources.path' },
234-
{ 'blink.cmp.sources.snippets', score_offset = -2 },
235-
},
236-
{ { 'blink.cmp.sources.buffer' } },
233+
{ 'blink.cmp.sources.lsp', name = 'LSP' },
234+
{ 'blink.cmp.sources.path', name = 'Path', score_offset = 3 },
235+
{ 'blink.cmp.sources.snippets', name = 'Snippets', score_offset = -3 },
236+
{ 'blink.cmp.sources.buffer', name = 'Buffer', fallback_for = { 'LSP' } },
237237
},
238238
},
239239

lua/blink/cmp/sources/lib/async.lua

+2-2
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,8 @@
77
--- @field new fun(fn: fun(resolve: fun(result: any), reject: fun(err: any))): blink.cmp.Task
88
---
99
--- @field cancel fun(self: blink.cmp.Task)
10-
--- @field map fun(self: blink.cmp.Task, fn: fun(result: any): blink.cmp.Task | any)
11-
--- @field catch fun(self: blink.cmp.Task, fn: fun(err: any): blink.cmp.Task | any)
10+
--- @field map fun(self: blink.cmp.Task, fn: fun(result: any): blink.cmp.Task | any): blink.cmp.Task
11+
--- @field catch fun(self: blink.cmp.Task, fn: fun(err: any): blink.cmp.Task | any): blink.cmp.Task
1212
---
1313
--- @field on_completion fun(self: blink.cmp.Task, cb: fun(result: any))
1414
--- @field on_failure fun(self: blink.cmp.Task, cb: fun(err: any))

lua/blink/cmp/sources/lib/context.lua

+58-36
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,17 @@
1+
local utils = require('blink.cmp.sources.lib.utils')
12
local async = require('blink.cmp.sources.lib.async')
23
local sources_context = {}
34

45
--- @param context blink.cmp.Context
5-
--- @param sources_groups blink.cmp.Source[][]
6+
--- @param sources blink.cmp.SourceProvider[]
67
--- @param on_completions_callback fun(context: blink.cmp.Context, items: blink.cmp.CompletionItem[])
7-
function sources_context.new(context, sources_groups, on_completions_callback)
8+
function sources_context.new(context, sources, on_completions_callback)
89
local self = setmetatable({}, { __index = sources_context })
910
self.id = context.id
10-
self.sources_groups = sources_groups
11+
self.sources = sources
1112

1213
self.active_request = nil
1314
self.queued_request_context = nil
14-
self.last_sources_group_idx = nil
1515
--- @type fun(context: blink.cmp.Context, items: blink.cmp.CompletionItem[])
1616
self.on_completions_callback = on_completions_callback
1717

@@ -27,26 +27,12 @@ function sources_context:get_completions(context)
2727
return
2828
end
2929

30-
-- Create a task to get the completions for the first sources group,
31-
-- falling back to the next sources group iteratively if there are no items
32-
local request = self:get_completions_for_group(1, self.sources_groups[1], context)
33-
for idx, sources_group in ipairs(self.sources_groups) do
34-
if idx > 1 then
35-
request = request:map(function(res)
36-
if #res.items > 0 then return res end
37-
return self:get_completions_for_group(idx, sources_group, context)
38-
end)
39-
end
40-
end
41-
42-
-- Send response upstream and run the queued request, if it exists
43-
self.active_request = request:map(function(response)
30+
-- Create a task to get the completions, send responses upstream
31+
-- and run the queued request, if it exists
32+
self.active_request = self:get_completions_for_sources(self.sources, context):map(function(response)
4433
self.active_request = nil
4534
-- only send upstream if the response contains something new
46-
if not response.is_cached or response.sources_group_idx ~= self.last_sources_group_idx then
47-
self.on_completions_callback(context, response.items)
48-
end
49-
self.last_sources_group_idx = response.sources_group_idx
35+
if not response.is_cached then self.on_completions_callback(context, response.items) end
5036

5137
-- run the queued request, if it exists
5238
if self.queued_request_context ~= nil then
@@ -57,28 +43,26 @@ function sources_context:get_completions(context)
5743
end)
5844
end
5945

60-
--- @param sources_group_idx number
61-
--- @param sources_group blink.cmp.Source[]
46+
--- @param sources blink.cmp.SourceProvider[]
6247
--- @param context blink.cmp.Context
6348
--- @return blink.cmp.Task
64-
function sources_context:get_completions_for_group(sources_group_idx, sources_group, context)
65-
-- get completions for each source in the group
49+
function sources_context:get_completions_for_sources(sources, context)
50+
local non_fallback_sources = vim.tbl_filter(function(source) return source.config.fallback_for == nil end, sources)
51+
52+
-- get completions for each non-fallback source
6653
local tasks = vim.tbl_map(function(source)
6754
-- the source indicates we should refetch when this character is typed
6855
local trigger_character = context.trigger.character
6956
and vim.tbl_contains(source:get_trigger_characters(), context.trigger.character)
7057

71-
-- The TriggerForIncompleteCompletions kind is handled by the source itself
58+
-- The TriggerForIncompleteCompletions kind is handled by the source provider itself
7259
local source_context = require('blink.cmp.utils').shallow_copy(context)
7360
source_context.trigger = trigger_character
7461
and { kind = vim.lsp.protocol.CompletionTriggerKind.TriggerCharacter, character = context.trigger.character }
7562
or { kind = vim.lsp.protocol.CompletionTriggerKind.Invoked }
7663

77-
return source:get_completions(source_context):catch(function(err)
78-
vim.print(source.name .. ': failed to get completions with error: ' .. err)
79-
return { is_incomplete_forward = false, is_incomplete_backward = false, items = {} }
80-
end)
81-
end, sources_group)
64+
return self:get_completions_with_fallbacks(source_context, source, sources)
65+
end, non_fallback_sources)
8266

8367
-- wait for all the tasks to complete
8468
return async.task
@@ -91,21 +75,59 @@ function sources_context:get_completions_for_group(sources_group_idx, sources_gr
9175
for idx, task_result in ipairs(tasks_results) do
9276
if task_result.status == async.STATUS.COMPLETED then
9377
is_cached = is_cached and (task_result.result.is_cached or false)
94-
local source = sources_group[idx]
78+
local source = sources[idx]
9579
--- @type blink.cmp.CompletionResponse
9680
local response = task_result.result
9781
response.items = source:filter_completions(response)
9882
if source:should_show_completions(context, response) then vim.list_extend(items, response.items) end
9983
end
10084
end
101-
return { sources_group_idx = sources_group_idx, is_cached = is_cached, items = items }
85+
return { is_cached = is_cached, items = items }
10286
end)
10387
:catch(function(err)
104-
vim.print('failed to get completions for group with error: ' .. err)
105-
return { sources_group_idx = sources_group_idx, is_cached = false, items = {} }
88+
vim.print('failed to get completions for sources with error: ' .. err)
89+
return { is_cached = false, items = {} }
10690
end)
10791
end
10892

93+
--- Runs the source's get_completions function, falling back to other sources
94+
--- with fallback_for = { source.name } if the source returns no completion items
95+
--- @param context blink.cmp.Context
96+
--- @param source blink.cmp.SourceProvider
97+
--- @param sources blink.cmp.SourceProvider[]
98+
--- @return blink.cmp.Task
99+
--- TODO: When a source has multiple fallbacks, we may end up with duplicate completion items
100+
function sources_context:get_completions_with_fallbacks(context, source, sources)
101+
local fallback_sources = vim.tbl_filter(
102+
function(fallback_source)
103+
return fallback_source.name ~= source.name
104+
and fallback_source.config.fallback_for ~= nil
105+
and vim.tbl_contains(fallback_source.config.fallback_for, source.name)
106+
end,
107+
sources
108+
)
109+
110+
return source:get_completions(context):map(function(response)
111+
-- source returned completions, no need to fallback
112+
if #response.items > 0 or #fallback_sources == 0 then return response end
113+
114+
-- run fallbacks
115+
return async.task
116+
.await_all(vim.tbl_map(function(fallback) return fallback:get_completions(context) end, fallback_sources))
117+
:map(function(task_results)
118+
local successful_task_results = vim.tbl_filter(
119+
function(task_result) return task_result.status == async.STATUS.COMPLETED end,
120+
task_results
121+
)
122+
local fallback_responses = vim.tbl_map(
123+
function(task_result) return task_result.result end,
124+
successful_task_results
125+
)
126+
return utils.concat_responses(fallback_responses)
127+
end)
128+
end)
129+
end
130+
109131
function sources_context:destroy()
110132
self.on_completions_callback = function() end
111133
if self.active_request ~= nil then self.active_request:cancel() end

lua/blink/cmp/sources/lib/init.lua

+12-20
Original file line numberDiff line numberDiff line change
@@ -3,19 +3,15 @@ local config = require('blink.cmp.config')
33
local sources = {
44
current_context = nil,
55
sources_registered = false,
6-
sources_groups = {},
6+
providers = {},
77
on_completions_callback = function(_, _) end,
88
}
99

1010
function sources.register()
11-
assert(#sources.sources_groups == 0, 'Sources have already been registered')
11+
assert(#sources.providers == 0, 'Sources have already been registered')
1212

13-
for _, sources_group in ipairs(config.sources.providers) do
14-
local group = {}
15-
for _, source_config in ipairs(sources_group) do
16-
table.insert(group, require('blink.cmp.sources.lib.source').new(source_config))
17-
end
18-
table.insert(sources.sources_groups, group)
13+
for _, source_config in ipairs(config.sources.providers) do
14+
table.insert(sources.providers, require('blink.cmp.sources.lib.provider').new(source_config))
1915
end
2016
end
2117

@@ -29,8 +25,7 @@ function sources.get_trigger_characters()
2925
end
3026

3127
local trigger_characters = {}
32-
-- todo: should this be all source groups?
33-
for _, source in pairs(sources.sources_groups[1]) do
28+
for _, source in pairs(sources.providers) do
3429
local source_trigger_characters = source:get_trigger_characters()
3530
for _, char in ipairs(source_trigger_characters) do
3631
if not blocked_trigger_characters[char] then table.insert(trigger_characters, char) end
@@ -48,7 +43,7 @@ function sources.request_completions(context)
4843
if is_new_context then
4944
if sources.current_context ~= nil then sources.current_context:destroy() end
5045
sources.current_context =
51-
require('blink.cmp.sources.lib.context').new(context, sources.sources_groups, sources.on_completions_callback)
46+
require('blink.cmp.sources.lib.context').new(context, sources.providers, sources.on_completions_callback)
5247
end
5348

5449
sources.current_context:get_completions(context)
@@ -68,14 +63,11 @@ end
6863
--- @return fun(): nil Cancelation function
6964
function sources.resolve(item, callback)
7065
local item_source = nil
71-
for _, group in ipairs(sources.sources_groups) do
72-
for _, source in ipairs(group) do
73-
if source.name == item.source then
74-
item_source = source
75-
break
76-
end
66+
for _, source in ipairs(sources.providers) do
67+
if source.name == item.source then
68+
item_source = source
69+
break
7770
end
78-
if item_source ~= nil then break end
7971
end
8072

8173
if item_source == nil then
@@ -105,7 +97,7 @@ function sources.get_signature_help_trigger_characters()
10597
local retrigger_characters = {}
10698

10799
-- todo: should this be all source groups?
108-
for _, source in ipairs(sources.sources_groups[1]) do
100+
for _, source in ipairs(sources.providers) do
109101
local res = source:get_signature_help_trigger_characters()
110102
for _, char in ipairs(res.trigger_characters) do
111103
if not blocked_trigger_characters[char] then table.insert(trigger_characters, char) end
@@ -121,7 +113,7 @@ end
121113
--- @param callback fun(signature_helps: lsp.SignatureHelp)
122114
function sources.get_signature_help(context, callback)
123115
local tasks = {}
124-
for _, source in ipairs(sources.sources_groups[1]) do
116+
for _, source in ipairs(sources.providers) do
125117
table.insert(tasks, source:get_signature_help(context))
126118
end
127119
sources.current_signature_help = async.task.await_all(tasks):map(function(tasks_results)

lua/blink/cmp/sources/lib/source.lua renamed to lua/blink/cmp/sources/lib/provider.lua

+8-2
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,10 @@ local source = {}
44

55
--- @param config blink.cmp.SourceProviderConfig
66
function source.new(config)
7+
assert(type(config.name) == 'string', 'Each source in config.sources.providers must have a "name" of type string')
8+
79
local self = setmetatable({}, { __index = source })
8-
self.name = config[1]
10+
self.name = config.name
911
--- @type blink.cmp.Source
1012
self.module = require(config[1]).new(config.opts or {})
1113
self.config = config
@@ -45,13 +47,17 @@ function source:get_completions(context)
4547
for _, item in ipairs(response.items) do
4648
item.score_offset = (item.score_offset or 0) + (self.config.score_offset or 0)
4749
item.cursor_column = context.cursor[2]
48-
item.source = self.config[1]
50+
item.source = self.name
4951
end
5052

5153
self.last_response = require('blink.cmp.utils').shallow_copy(response)
5254
self.last_response.is_cached = true
5355
return response
5456
end)
57+
:catch(function(err)
58+
vim.print('failed to get completions with error: ' .. err)
59+
return { is_incomplete_forward = false, is_incomplete_backward = false, items = {} }
60+
end)
5561
end
5662

5763
--- @param response blink.cmp.CompletionResponse

0 commit comments

Comments
 (0)