Skip to content

Commit

Permalink
feat: sources v2 (#465)
Browse files Browse the repository at this point in the history
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
  • Loading branch information
Saghen committed Dec 10, 2024
1 parent 68ba8ae commit aac57a1
Show file tree
Hide file tree
Showing 26 changed files with 922 additions and 596 deletions.
67 changes: 39 additions & 28 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,9 +72,7 @@
-- default list of enabled providers defined so that you can extend it
-- elsewhere in your config, without redefining it, via `opts_extend`
sources = {
completion = {
enabled_providers = { 'lsp', 'path', 'snippets', 'buffer' },
},
default = { 'lsp', 'path', 'snippets', 'buffer' },
},

-- experimental auto-brackets support
Expand All @@ -83,9 +81,9 @@
-- experimental signature help support
-- signature = { enabled = true }
},
-- allows extending the enabled_providers array elsewhere in your config
-- allows extending the providers array elsewhere in your config
-- without having to redefine it
opts_extend = { "sources.completion.enabled_providers" }
opts_extend = { "sources.default" }
},
```

Expand Down Expand Up @@ -291,6 +289,14 @@ MiniDeps.add({
-- however, some LSPs (i.e. tsserver) return characters that would essentially
-- always show the window. We block these by default.
show_on_blocked_trigger_characters = { ' ', '\n', '\t' },
-- or a function like
-- show_on_blocked_trigger_characters = function()
-- local blocked = { ' ', '\n', '\t' }
-- if vim.bo.filetype == 'markdown' then
-- vim.list_extend(blocked, { '.', '/', '(', '[' })
-- end
-- return blocked
-- end
-- 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
show_on_accept_on_trigger_character = true,
Expand All @@ -301,6 +307,7 @@ MiniDeps.add({
-- the completion window when the cursor comes after a trigger character when
-- entering insert mode/accepting an item
show_on_x_blocked_trigger_characters = { "'", '"', '(' },
-- or a function, similar to show_on_blocked_trigger_character
},

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

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

sources = {
completion = {
-- Static list of providers to enable, or a function to dynamically enable/disable providers based on the context
enabled_providers = { 'lsp', 'path', 'snippets', 'buffer' },
-- Example dynamically picking providers based on the filetype and treesitter node:
-- enabled_providers = function(ctx)
-- local node = vim.treesitter.get_node()
-- if vim.bo.filetype == 'lua' then
-- return { 'lsp', 'path' }
-- elseif node and vim.tbl_contains({ 'comment', 'line_comment', 'block_comment' }, node:type()) then
-- return { 'buffer' }
-- else
-- return { 'lsp', 'path', 'snippets', 'buffer' }
-- end
-- end,
-- Static list of providers to enable, or a function to dynamically enable/disable providers based on the context
default = { 'lsp', 'path', 'snippets', 'buffer' },
-- Example dynamically picking providers based on the filetype and treesitter node:
-- providers = function(ctx)
-- local node = vim.treesitter.get_node()
-- if vim.bo.filetype == 'lua' then
-- return { 'lsp', 'path' }
-- elseif node and vim.tbl_contains({ 'comment', 'line_comment', 'block_comment' }, node:type()) then
-- return { 'buffer' }
-- else
-- return { 'lsp', 'path', 'snippets', 'buffer' }
-- end
-- end

-- You may also define providers per filetype
per_filetype = {
-- lua = { 'lsp', 'path' },
},

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

--- *All* of the providers have the following options available
--- *All* providers have the following options available
--- NOTE: All of these options may be functions to get dynamic behavior
--- See the type definitions for more information.
--- Check the enabled_providers config for an example
enabled = true, -- Whether or not to enable the provider
async = false, -- Whether we should wait for the provider to return before showing the completions
timeout_ms = 2000, -- How long to wait for the provider to return before showing completions and treating it as asynchronous
transform_items = nil, -- Function to transform the items before they're returned
should_show_items = true, -- Whether or not to show the items
max_items = nil, -- Maximum number of items to display in the menu
min_keyword_length = 0, -- Minimum number of characters in the keyword to trigger the provider
fallback_for = {}, -- If any of these providers return 0 items, it will fallback to this provider
-- If this provider returns 0 items, it will fallback to these providers.
-- If multiple providers falback to the same provider, all of the providers must return 0 items for it to fallback
fallbacks = { 'buffer' },
score_offset = 0, -- Boost/penalize the score of the items
override = nil, -- Override the source's functions
},
Expand Down Expand Up @@ -607,7 +621,6 @@ MiniDeps.add({
buffer = {
name = 'Buffer',
module = 'blink.cmp.sources.buffer',
fallback_for = { 'lsp' },
opts = {
-- default to all visible buffers
get_bufnrs = function()
Expand Down Expand Up @@ -730,9 +743,7 @@ MiniDeps.add({
jump = function(direction) require('luasnip').jump(direction) end,
},
sources = {
completion = {
enabled_providers = { 'lsp', 'path', 'luasnip', 'buffer' },
},
default = { 'lsp', 'path', 'luasnip', 'buffer' },
},
}
}
Expand Down
58 changes: 46 additions & 12 deletions lua/blink/cmp/completion/list.lua
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,15 @@
--- @field select_emitter blink.cmp.EventEmitter<blink.cmp.CompletionListSelectEvent>
--- @field accept_emitter blink.cmp.EventEmitter<blink.cmp.CompletionListAcceptEvent>
---
--- @field show fun(context: blink.cmp.Context, items?: blink.cmp.CompletionItem[])
--- @field fuzzy fun(context: blink.cmp.Context, items: blink.cmp.CompletionItem[]): blink.cmp.CompletionItem[]
--- @field show fun(context: blink.cmp.Context, items: table<string, blink.cmp.CompletionItem[]>)
--- @field fuzzy fun(context: blink.cmp.Context, items: table<string, blink.cmp.CompletionItem[]>): blink.cmp.CompletionItem[]
--- @field hide fun()
---
--- @field get_selected_item fun(): blink.cmp.CompletionItem?
--- @field select fun(idx?: number, opts?: { undo_preview?: boolean })
--- @field select fun(idx?: number, opts?: { undo_preview?: boolean, is_explicit_selection?: boolean })
--- @field select_next fun()
--- @field select_prev fun()
--- @field get_item_idx_in_list fun(item?: blink.cmp.CompletionItem): number
---
--- @field undo_preview fun()
--- @field apply_preview fun(item: blink.cmp.CompletionItem)
Expand Down Expand Up @@ -54,35 +55,61 @@ local list = {
context = nil,
items = {},
selected_item_idx = nil,
is_explicitly_selected = false,
preview_undo_text_edit = nil,
}

---------- State ----------

function list.show(context, items)
function list.show(context, items_by_source)
-- reset state for new context
local is_new_context = not list.context or list.context.id ~= context.id
if is_new_context then list.preview_undo_text_edit = nil end
if is_new_context then
list.preview_undo_text_edit = nil
list.is_explicitly_selected = false
end

-- if the keyword changed, the list is no longer explicitly selected
local bounds_equal = list.context ~= nil
and list.context.bounds.start_col == context.bounds.start_col
and list.context.bounds.length == context.bounds.length
if not bounds_equal then list.is_explicitly_selected = false end

local previous_selected_item = list.get_selected_item()

-- update the context/list and emit
list.context = context
list.items = list.fuzzy(context, items or list.items)
list.items = list.fuzzy(context, items_by_source)

if #list.items == 0 then
list.hide_emitter:emit({ context = context })
else
list.show_emitter:emit({ items = list.items, context = context })
end

-- todo: some logic to maintain the selection if the user moved the cursor?
list.select(list.config.selection == 'preselect' and 1 or nil, { undo_preview = false })
-- maintain the selection if the user selected an item
local previous_item_idx = list.get_item_idx_in_list(previous_selected_item)
if list.is_explicitly_selected and previous_item_idx ~= nil and previous_item_idx <= 10 then
list.select(previous_item_idx, { undo_preview = false })

-- otherwise, use the default selection
else
list.select(
list.config.selection == 'preselect' and 1 or nil,
{ undo_preview = false, is_explicit_selection = false }
)
end
end

function list.fuzzy(context, items)
function list.fuzzy(context, items_by_source)
local fuzzy = require('blink.cmp.fuzzy')
local sources = require('blink.cmp.sources.lib')
local filtered_items = fuzzy.fuzzy(fuzzy.get_query(), items_by_source)

-- apply the per source max_items
filtered_items = require('blink.cmp.sources.lib').apply_max_items_for_completions(context, filtered_items)

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

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

--- @diagnostic disable-next-line: assign-type-mismatch
list.is_explicitly_selected = opts.is_explicit_selection == nil and true or opts.is_explicit_selection
list.selected_item_idx = idx
list.select_emitter:emit({ idx = idx, item = item, items = list.items, context = list.context })
end
Expand Down Expand Up @@ -149,6 +178,11 @@ function list.select_prev()
list.select(list.selected_item_idx - 1)
end

function list.get_item_idx_in_list(item)
if item == nil then return end
return require('blink.cmp.lib.utils').find_idx(list.items, function(i) return i.label == item.label end)
end

---------- Preview ----------

function list.undo_preview()
Expand Down
26 changes: 22 additions & 4 deletions lua/blink/cmp/completion/trigger.lua
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
--- @field line string
--- @field bounds blink.cmp.ContextBounds
--- @field trigger { kind: number, character: string | nil }
--- @field providers string[]

--- @class blink.cmp.CompletionTrigger
--- @field buffer_events blink.cmp.BufferEvents
Expand All @@ -28,7 +29,7 @@
--- @field is_trigger_character fun(char: string, is_retrigger?: boolean): boolean
--- @field suppress_events_for_callback fun(cb: fun())
--- @field show_if_on_trigger_character fun(opts?: { is_accept?: boolean })
--- @field show fun(opts?: { trigger_character?: string, force?: boolean, send_upstream?: boolean })
--- @field show fun(opts?: { trigger_character?: string, force?: boolean, send_upstream?: boolean, providers?: string[] })
--- @field hide fun()
--- @field within_query_bounds fun(cursor: number[]): boolean
--- @field get_context_bounds fun(regex: string): blink.cmp.ContextBounds
Expand Down Expand Up @@ -119,8 +120,17 @@ function trigger.is_trigger_character(char, is_show_on_x)
local sources = require('blink.cmp.sources.lib')
local is_trigger = vim.tbl_contains(sources.get_trigger_characters(), char)

local is_blocked = vim.tbl_contains(config.show_on_blocked_trigger_characters, char)
or (is_show_on_x and vim.tbl_contains(config.show_on_x_blocked_trigger_characters, char))
local show_on_blocked_trigger_characters = type(config.show_on_blocked_trigger_characters) == 'function'
and config.show_on_blocked_trigger_characters()
or config.show_on_blocked_trigger_characters
--- @cast show_on_blocked_trigger_characters string[]
local show_on_x_blocked_trigger_characters = type(config.show_on_x_blocked_trigger_characters) == 'function'
and config.show_on_x_blocked_trigger_characters()
or config.show_on_x_blocked_trigger_characters
--- @cast show_on_x_blocked_trigger_characters string[]

local is_blocked = vim.tbl_contains(show_on_blocked_trigger_characters, char)
or (is_show_on_x and vim.tbl_contains(show_on_x_blocked_trigger_characters, char))

return is_trigger and not is_blocked
end
Expand Down Expand Up @@ -165,7 +175,14 @@ function trigger.show(opts)
end

-- update context
if trigger.context == nil then trigger.current_context_id = trigger.current_context_id + 1 end
if trigger.context == nil or opts.providers ~= nil then
trigger.current_context_id = trigger.current_context_id + 1
end

local providers = opts.providers
or (trigger.context and trigger.context.providers)
or require('blink.cmp.sources.lib').get_enabled_provider_ids()

trigger.context = {
id = trigger.current_context_id,
bufnr = vim.api.nvim_get_current_buf(),
Expand All @@ -177,6 +194,7 @@ function trigger.show(opts)
or vim.lsp.protocol.CompletionTriggerKind.Invoked,
character = opts.trigger_character,
},
providers = providers,
}

if opts.send_upstream ~= false then trigger.show_emitter:emit({ context = trigger.context }) end
Expand Down
8 changes: 4 additions & 4 deletions lua/blink/cmp/config/completion/trigger.lua
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,10 @@
--- @field show_in_snippet boolean When false, will not show the completion window when in a snippet
--- @field show_on_keyword boolean When true, will show the completion window after typing a character that matches the `keyword.regex`
--- @field show_on_trigger_character boolean When true, will show the completion window after typing a trigger character
--- @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.
--- @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.
--- @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
--- @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
--- @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
--- @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

local validate = require('blink.cmp.config.utils').validate
local trigger = {
Expand All @@ -26,10 +26,10 @@ function trigger.validate(config)
show_in_snippet = { config.show_in_snippet, 'boolean' },
show_on_keyword = { config.show_on_keyword, 'boolean' },
show_on_trigger_character = { config.show_on_trigger_character, 'boolean' },
show_on_blocked_trigger_characters = { config.show_on_blocked_trigger_characters, 'table' },
show_on_blocked_trigger_characters = { config.show_on_blocked_trigger_characters, { 'function', 'table' } },
show_on_accept_on_trigger_character = { config.show_on_accept_on_trigger_character, 'boolean' },
show_on_insert_on_trigger_character = { config.show_on_insert_on_trigger_character, 'boolean' },
show_on_x_blocked_trigger_characters = { config.show_on_x_blocked_trigger_characters, 'table' },
show_on_x_blocked_trigger_characters = { config.show_on_x_blocked_trigger_characters, { 'function', 'table' } },
})
end

Expand Down
6 changes: 4 additions & 2 deletions lua/blink/cmp/config/fuzzy.lua
Original file line number Diff line number Diff line change
Expand Up @@ -2,22 +2,24 @@
--- @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
--- @field use_frecency boolean Tracks the most recently/frequently used items and boosts the score of the item
--- @field use_proximity boolean Boosts the score of items matching nearby words
--- @field sorts ("label" | "kind" | "score")[] Controls which sorts to use and in which order, these three are currently the only allowed options
--- @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
--- @field prebuilt_binaries blink.cmp.PrebuiltBinariesConfig

--- @class (exact) blink.cmp.PrebuiltBinariesConfig
--- @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`
--- @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
--- @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

--- @alias blink.cmp.SortFunction fun(a: blink.cmp.CompletionItem, b: blink.cmp.CompletionItem): boolean | nil

local validate = require('blink.cmp.config.utils').validate
local fuzzy = {
--- @type blink.cmp.FuzzyConfig
default = {
use_typo_resistance = true,
use_frecency = true,
use_proximity = true,
sorts = { 'label', 'kind', 'score' },
sorts = { 'score', 'kind', 'label' },
prebuilt_binaries = {
download = true,
force_version = nil,
Expand Down
1 change: 1 addition & 0 deletions lua/blink/cmp/config/init.lua
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
--- @field enabled fun(): boolean
--- @field keymap blink.cmp.KeymapConfig
--- @field completion blink.cmp.CompletionConfig
--- @field fuzzy blink.cmp.FuzzyConfig
--- @field sources blink.cmp.SourceConfig
--- @field signature blink.cmp.SignatureConfig
--- @field snippets blink.cmp.SnippetsConfig
Expand Down
Loading

0 comments on commit aac57a1

Please sign in to comment.