Skip to content

Commit 533608f

Browse files
committed
feat: sources v2 (#465)
Large rewrite of how sources are handled, adding support for async providers/timeouts, tree based fallbacks, dynamically adding sources and some other goodies Closes #386 Closes #219 Closes #328 Closes #331 Closes #312 Closes #454 Closes #444 Closes #372 Closes #475
1 parent ae5a4ce commit 533608f

File tree

26 files changed

+922
-596
lines changed

26 files changed

+922
-596
lines changed

README.md

Lines changed: 39 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -72,9 +72,7 @@
7272
-- default list of enabled providers defined so that you can extend it
7373
-- elsewhere in your config, without redefining it, via `opts_extend`
7474
sources = {
75-
completion = {
76-
enabled_providers = { 'lsp', 'path', 'snippets', 'buffer' },
77-
},
75+
default = { 'lsp', 'path', 'snippets', 'buffer' },
7876
},
7977

8078
-- experimental auto-brackets support
@@ -83,9 +81,9 @@
8381
-- experimental signature help support
8482
-- signature = { enabled = true }
8583
},
86-
-- allows extending the enabled_providers array elsewhere in your config
84+
-- allows extending the providers array elsewhere in your config
8785
-- without having to redefine it
88-
opts_extend = { "sources.completion.enabled_providers" }
86+
opts_extend = { "sources.default" }
8987
},
9088
```
9189

@@ -291,6 +289,14 @@ MiniDeps.add({
291289
-- however, some LSPs (i.e. tsserver) return characters that would essentially
292290
-- always show the window. We block these by default.
293291
show_on_blocked_trigger_characters = { ' ', '\n', '\t' },
292+
-- or a function like
293+
-- show_on_blocked_trigger_characters = function()
294+
-- local blocked = { ' ', '\n', '\t' }
295+
-- if vim.bo.filetype == 'markdown' then
296+
-- vim.list_extend(blocked, { '.', '/', '(', '[' })
297+
-- end
298+
-- return blocked
299+
-- end
294300
-- When both this and show_on_trigger_character are true, will show the completion window
295301
-- when the cursor comes after a trigger character after accepting an item
296302
show_on_accept_on_trigger_character = true,
@@ -301,6 +307,7 @@ MiniDeps.add({
301307
-- the completion window when the cursor comes after a trigger character when
302308
-- entering insert mode/accepting an item
303309
show_on_x_blocked_trigger_characters = { "'", '"', '(' },
310+
-- or a function, similar to show_on_blocked_trigger_character
304311
},
305312

306313
list = {
@@ -516,8 +523,9 @@ MiniDeps.add({
516523
-- proximity bonus boosts the score of items matching nearby words
517524
use_proximity = true,
518525
max_items = 200,
519-
-- controls which sorts to use and in which order, these three are currently the only allowed options
520-
sorts = { 'label', 'kind', 'score' },
526+
-- controls which sorts to use and in which order, falling back to the next sort if the first one returns nil
527+
-- you may pass a function instead of a string to customize the sorting
528+
sorts = { 'score', 'kind', 'label' },
521529

522530
prebuilt_binaries = {
523531
-- Whether or not to automatically download a prebuilt binary from github. If this is set to `false`
@@ -538,20 +546,23 @@ MiniDeps.add({
538546
},
539547

540548
sources = {
541-
completion = {
542-
-- Static list of providers to enable, or a function to dynamically enable/disable providers based on the context
543-
enabled_providers = { 'lsp', 'path', 'snippets', 'buffer' },
544-
-- Example dynamically picking providers based on the filetype and treesitter node:
545-
-- enabled_providers = function(ctx)
546-
-- local node = vim.treesitter.get_node()
547-
-- if vim.bo.filetype == 'lua' then
548-
-- return { 'lsp', 'path' }
549-
-- elseif node and vim.tbl_contains({ 'comment', 'line_comment', 'block_comment' }, node:type()) then
550-
-- return { 'buffer' }
551-
-- else
552-
-- return { 'lsp', 'path', 'snippets', 'buffer' }
553-
-- end
554-
-- end,
549+
-- Static list of providers to enable, or a function to dynamically enable/disable providers based on the context
550+
default = { 'lsp', 'path', 'snippets', 'buffer' },
551+
-- Example dynamically picking providers based on the filetype and treesitter node:
552+
-- providers = function(ctx)
553+
-- local node = vim.treesitter.get_node()
554+
-- if vim.bo.filetype == 'lua' then
555+
-- return { 'lsp', 'path' }
556+
-- elseif node and vim.tbl_contains({ 'comment', 'line_comment', 'block_comment' }, node:type()) then
557+
-- return { 'buffer' }
558+
-- else
559+
-- return { 'lsp', 'path', 'snippets', 'buffer' }
560+
-- end
561+
-- end
562+
563+
-- You may also define providers per filetype
564+
per_filetype = {
565+
-- lua = { 'lsp', 'path' },
555566
},
556567

557568
-- Please see https://github.com/Saghen/blink.compat for using `nvim-cmp` sources
@@ -560,16 +571,19 @@ MiniDeps.add({
560571
name = 'LSP',
561572
module = 'blink.cmp.sources.lsp',
562573

563-
--- *All* of the providers have the following options available
574+
--- *All* providers have the following options available
564575
--- NOTE: All of these options may be functions to get dynamic behavior
565576
--- See the type definitions for more information.
566-
--- Check the enabled_providers config for an example
567577
enabled = true, -- Whether or not to enable the provider
578+
async = false, -- Whether we should wait for the provider to return before showing the completions
579+
timeout_ms = 2000, -- How long to wait for the provider to return before showing completions and treating it as asynchronous
568580
transform_items = nil, -- Function to transform the items before they're returned
569581
should_show_items = true, -- Whether or not to show the items
570582
max_items = nil, -- Maximum number of items to display in the menu
571583
min_keyword_length = 0, -- Minimum number of characters in the keyword to trigger the provider
572-
fallback_for = {}, -- If any of these providers return 0 items, it will fallback to this provider
584+
-- If this provider returns 0 items, it will fallback to these providers.
585+
-- If multiple providers falback to the same provider, all of the providers must return 0 items for it to fallback
586+
fallbacks = { 'buffer' },
573587
score_offset = 0, -- Boost/penalize the score of the items
574588
override = nil, -- Override the source's functions
575589
},
@@ -607,7 +621,6 @@ MiniDeps.add({
607621
buffer = {
608622
name = 'Buffer',
609623
module = 'blink.cmp.sources.buffer',
610-
fallback_for = { 'lsp' },
611624
opts = {
612625
-- default to all visible buffers
613626
get_bufnrs = function()
@@ -730,9 +743,7 @@ MiniDeps.add({
730743
jump = function(direction) require('luasnip').jump(direction) end,
731744
},
732745
sources = {
733-
completion = {
734-
enabled_providers = { 'lsp', 'path', 'luasnip', 'buffer' },
735-
},
746+
default = { 'lsp', 'path', 'luasnip', 'buffer' },
736747
},
737748
}
738749
}

lua/blink/cmp/completion/list.lua

Lines changed: 46 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -10,14 +10,15 @@
1010
--- @field select_emitter blink.cmp.EventEmitter<blink.cmp.CompletionListSelectEvent>
1111
--- @field accept_emitter blink.cmp.EventEmitter<blink.cmp.CompletionListAcceptEvent>
1212
---
13-
--- @field show fun(context: blink.cmp.Context, items?: blink.cmp.CompletionItem[])
14-
--- @field fuzzy fun(context: blink.cmp.Context, items: blink.cmp.CompletionItem[]): blink.cmp.CompletionItem[]
13+
--- @field show fun(context: blink.cmp.Context, items: table<string, blink.cmp.CompletionItem[]>)
14+
--- @field fuzzy fun(context: blink.cmp.Context, items: table<string, blink.cmp.CompletionItem[]>): blink.cmp.CompletionItem[]
1515
--- @field hide fun()
1616
---
1717
--- @field get_selected_item fun(): blink.cmp.CompletionItem?
18-
--- @field select fun(idx?: number, opts?: { undo_preview?: boolean })
18+
--- @field select fun(idx?: number, opts?: { undo_preview?: boolean, is_explicit_selection?: boolean })
1919
--- @field select_next fun()
2020
--- @field select_prev fun()
21+
--- @field get_item_idx_in_list fun(item?: blink.cmp.CompletionItem): number
2122
---
2223
--- @field undo_preview fun()
2324
--- @field apply_preview fun(item: blink.cmp.CompletionItem)
@@ -54,35 +55,61 @@ local list = {
5455
context = nil,
5556
items = {},
5657
selected_item_idx = nil,
58+
is_explicitly_selected = false,
5759
preview_undo_text_edit = nil,
5860
}
5961

6062
---------- State ----------
6163

62-
function list.show(context, items)
64+
function list.show(context, items_by_source)
6365
-- reset state for new context
6466
local is_new_context = not list.context or list.context.id ~= context.id
65-
if is_new_context then list.preview_undo_text_edit = nil end
67+
if is_new_context then
68+
list.preview_undo_text_edit = nil
69+
list.is_explicitly_selected = false
70+
end
71+
72+
-- if the keyword changed, the list is no longer explicitly selected
73+
local bounds_equal = list.context ~= nil
74+
and list.context.bounds.start_col == context.bounds.start_col
75+
and list.context.bounds.length == context.bounds.length
76+
if not bounds_equal then list.is_explicitly_selected = false end
77+
78+
local previous_selected_item = list.get_selected_item()
6679

80+
-- update the context/list and emit
6781
list.context = context
68-
list.items = list.fuzzy(context, items or list.items)
82+
list.items = list.fuzzy(context, items_by_source)
6983

7084
if #list.items == 0 then
7185
list.hide_emitter:emit({ context = context })
7286
else
7387
list.show_emitter:emit({ items = list.items, context = context })
7488
end
7589

76-
-- todo: some logic to maintain the selection if the user moved the cursor?
77-
list.select(list.config.selection == 'preselect' and 1 or nil, { undo_preview = false })
90+
-- maintain the selection if the user selected an item
91+
local previous_item_idx = list.get_item_idx_in_list(previous_selected_item)
92+
if list.is_explicitly_selected and previous_item_idx ~= nil and previous_item_idx <= 10 then
93+
list.select(previous_item_idx, { undo_preview = false })
94+
95+
-- otherwise, use the default selection
96+
else
97+
list.select(
98+
list.config.selection == 'preselect' and 1 or nil,
99+
{ undo_preview = false, is_explicit_selection = false }
100+
)
101+
end
78102
end
79103

80-
function list.fuzzy(context, items)
104+
function list.fuzzy(context, items_by_source)
81105
local fuzzy = require('blink.cmp.fuzzy')
82-
local sources = require('blink.cmp.sources.lib')
106+
local filtered_items = fuzzy.fuzzy(fuzzy.get_query(), items_by_source)
107+
108+
-- apply the per source max_items
109+
filtered_items = require('blink.cmp.sources.lib').apply_max_items_for_completions(context, filtered_items)
83110

84-
local filtered_items = fuzzy.fuzzy(fuzzy.get_query(), items)
85-
return sources.apply_max_items_for_completions(context, filtered_items)
111+
-- apply the global max_items
112+
return require('blink.cmp.lib.utils').slice(filtered_items, 1, list.config.max_items)
86113
end
87114

88115
function list.hide() list.hide_emitter:emit({ context = list.context }) end
@@ -101,6 +128,8 @@ function list.select(idx, opts)
101128
if list.config.selection == 'auto_insert' and item then list.apply_preview(item) end
102129
end)
103130

131+
--- @diagnostic disable-next-line: assign-type-mismatch
132+
list.is_explicitly_selected = opts.is_explicit_selection == nil and true or opts.is_explicit_selection
104133
list.selected_item_idx = idx
105134
list.select_emitter:emit({ idx = idx, item = item, items = list.items, context = list.context })
106135
end
@@ -149,6 +178,11 @@ function list.select_prev()
149178
list.select(list.selected_item_idx - 1)
150179
end
151180

181+
function list.get_item_idx_in_list(item)
182+
if item == nil then return end
183+
return require('blink.cmp.lib.utils').find_idx(list.items, function(i) return i.label == item.label end)
184+
end
185+
152186
---------- Preview ----------
153187

154188
function list.undo_preview()

lua/blink/cmp/completion/trigger.lua

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
--- @field line string
1717
--- @field bounds blink.cmp.ContextBounds
1818
--- @field trigger { kind: number, character: string | nil }
19+
--- @field providers string[]
1920

2021
--- @class blink.cmp.CompletionTrigger
2122
--- @field buffer_events blink.cmp.BufferEvents
@@ -28,7 +29,7 @@
2829
--- @field is_trigger_character fun(char: string, is_retrigger?: boolean): boolean
2930
--- @field suppress_events_for_callback fun(cb: fun())
3031
--- @field show_if_on_trigger_character fun(opts?: { is_accept?: boolean })
31-
--- @field show fun(opts?: { trigger_character?: string, force?: boolean, send_upstream?: boolean })
32+
--- @field show fun(opts?: { trigger_character?: string, force?: boolean, send_upstream?: boolean, providers?: string[] })
3233
--- @field hide fun()
3334
--- @field within_query_bounds fun(cursor: number[]): boolean
3435
--- @field get_context_bounds fun(regex: string): blink.cmp.ContextBounds
@@ -119,8 +120,17 @@ function trigger.is_trigger_character(char, is_show_on_x)
119120
local sources = require('blink.cmp.sources.lib')
120121
local is_trigger = vim.tbl_contains(sources.get_trigger_characters(), char)
121122

122-
local is_blocked = vim.tbl_contains(config.show_on_blocked_trigger_characters, char)
123-
or (is_show_on_x and vim.tbl_contains(config.show_on_x_blocked_trigger_characters, char))
123+
local show_on_blocked_trigger_characters = type(config.show_on_blocked_trigger_characters) == 'function'
124+
and config.show_on_blocked_trigger_characters()
125+
or config.show_on_blocked_trigger_characters
126+
--- @cast show_on_blocked_trigger_characters string[]
127+
local show_on_x_blocked_trigger_characters = type(config.show_on_x_blocked_trigger_characters) == 'function'
128+
and config.show_on_x_blocked_trigger_characters()
129+
or config.show_on_x_blocked_trigger_characters
130+
--- @cast show_on_x_blocked_trigger_characters string[]
131+
132+
local is_blocked = vim.tbl_contains(show_on_blocked_trigger_characters, char)
133+
or (is_show_on_x and vim.tbl_contains(show_on_x_blocked_trigger_characters, char))
124134

125135
return is_trigger and not is_blocked
126136
end
@@ -165,7 +175,14 @@ function trigger.show(opts)
165175
end
166176

167177
-- update context
168-
if trigger.context == nil then trigger.current_context_id = trigger.current_context_id + 1 end
178+
if trigger.context == nil or opts.providers ~= nil then
179+
trigger.current_context_id = trigger.current_context_id + 1
180+
end
181+
182+
local providers = opts.providers
183+
or (trigger.context and trigger.context.providers)
184+
or require('blink.cmp.sources.lib').get_enabled_provider_ids()
185+
169186
trigger.context = {
170187
id = trigger.current_context_id,
171188
bufnr = vim.api.nvim_get_current_buf(),
@@ -177,6 +194,7 @@ function trigger.show(opts)
177194
or vim.lsp.protocol.CompletionTriggerKind.Invoked,
178195
character = opts.trigger_character,
179196
},
197+
providers = providers,
180198
}
181199

182200
if opts.send_upstream ~= false then trigger.show_emitter:emit({ context = trigger.context }) end

lua/blink/cmp/config/completion/trigger.lua

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,10 @@
22
--- @field show_in_snippet boolean When false, will not show the completion window when in a snippet
33
--- @field show_on_keyword boolean When true, will show the completion window after typing a character that matches the `keyword.regex`
44
--- @field show_on_trigger_character boolean When true, will show the completion window after typing a trigger character
5-
--- @field show_on_blocked_trigger_characters string[] LSPs can indicate when to show the completion window via trigger characters. However, some LSPs (i.e. tsserver) return characters that would essentially always show the window. We block these by default.
5+
--- @field show_on_blocked_trigger_characters string[] | (fun(): string[]) LSPs can indicate when to show the completion window via trigger characters. However, some LSPs (i.e. tsserver) return characters that would essentially always show the window. We block these by default.
66
--- @field show_on_accept_on_trigger_character boolean When both this and show_on_trigger_character are true, will show the completion window when the cursor comes after a trigger character after accepting an item
77
--- @field show_on_insert_on_trigger_character boolean When both this and show_on_trigger_character are true, will show the completion window when the cursor comes after a trigger character when entering insert mode
8-
--- @field show_on_x_blocked_trigger_characters string[] List of trigger characters (on top of `show_on_blocked_trigger_characters`) that won't trigger the completion window when the cursor comes after a trigger character when entering insert mode/accepting an item
8+
--- @field show_on_x_blocked_trigger_characters string[] | (fun(): string[]) List of trigger characters (on top of `show_on_blocked_trigger_characters`) that won't trigger the completion window when the cursor comes after a trigger character when entering insert mode/accepting an item
99

1010
local validate = require('blink.cmp.config.utils').validate
1111
local trigger = {
@@ -26,10 +26,10 @@ function trigger.validate(config)
2626
show_in_snippet = { config.show_in_snippet, 'boolean' },
2727
show_on_keyword = { config.show_on_keyword, 'boolean' },
2828
show_on_trigger_character = { config.show_on_trigger_character, 'boolean' },
29-
show_on_blocked_trigger_characters = { config.show_on_blocked_trigger_characters, 'table' },
29+
show_on_blocked_trigger_characters = { config.show_on_blocked_trigger_characters, { 'function', 'table' } },
3030
show_on_accept_on_trigger_character = { config.show_on_accept_on_trigger_character, 'boolean' },
3131
show_on_insert_on_trigger_character = { config.show_on_insert_on_trigger_character, 'boolean' },
32-
show_on_x_blocked_trigger_characters = { config.show_on_x_blocked_trigger_characters, 'table' },
32+
show_on_x_blocked_trigger_characters = { config.show_on_x_blocked_trigger_characters, { 'function', 'table' } },
3333
})
3434
end
3535

lua/blink/cmp/config/fuzzy.lua

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,22 +2,24 @@
22
--- @field use_typo_resistance boolean When enabled, allows for a number of typos relative to the length of the query. Disabling this matches the behavior of fzf
33
--- @field use_frecency boolean Tracks the most recently/frequently used items and boosts the score of the item
44
--- @field use_proximity boolean Boosts the score of items matching nearby words
5-
--- @field sorts ("label" | "kind" | "score")[] Controls which sorts to use and in which order, these three are currently the only allowed options
5+
--- @field sorts ("label" | "kind" | "score" | blink.cmp.SortFunction)[] Controls which sorts to use and in which order, these three are currently the only allowed options
66
--- @field prebuilt_binaries blink.cmp.PrebuiltBinariesConfig
77

88
--- @class (exact) blink.cmp.PrebuiltBinariesConfig
99
--- @field download boolean Whenther or not to automatically download a prebuilt binary from github. If this is set to `false` you will need to manually build the fuzzy binary dependencies by running `cargo build --release`
1010
--- @field force_version? string When downloading a prebuilt binary, force the downloader to resolve this version. If this is unset then the downloader will attempt to infer the version from the checked out git tag (if any). WARN: Beware that `main` may be incompatible with the version you select
1111
--- @field force_system_triple? string When downloading a prebuilt binary, force the downloader to use this system triple. If this is unset then the downloader will attempt to infer the system triple from `jit.os` and `jit.arch`. Check the latest release for all available system triples. WARN: Beware that `main` may be incompatible with the version you select
1212

13+
--- @alias blink.cmp.SortFunction fun(a: blink.cmp.CompletionItem, b: blink.cmp.CompletionItem): boolean | nil
14+
1315
local validate = require('blink.cmp.config.utils').validate
1416
local fuzzy = {
1517
--- @type blink.cmp.FuzzyConfig
1618
default = {
1719
use_typo_resistance = true,
1820
use_frecency = true,
1921
use_proximity = true,
20-
sorts = { 'label', 'kind', 'score' },
22+
sorts = { 'score', 'kind', 'label' },
2123
prebuilt_binaries = {
2224
download = true,
2325
force_version = nil,

lua/blink/cmp/config/init.lua

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
--- @field enabled fun(): boolean
33
--- @field keymap blink.cmp.KeymapConfig
44
--- @field completion blink.cmp.CompletionConfig
5+
--- @field fuzzy blink.cmp.FuzzyConfig
56
--- @field sources blink.cmp.SourceConfig
67
--- @field signature blink.cmp.SignatureConfig
78
--- @field snippets blink.cmp.SnippetsConfig

0 commit comments

Comments
 (0)