diff --git a/lua/blink/cmp/completion/init.lua b/lua/blink/cmp/completion/init.lua index 47de236a..a9f75f13 100644 --- a/lua/blink/cmp/completion/init.lua +++ b/lua/blink/cmp/completion/init.lua @@ -30,7 +30,7 @@ function completion.setup() -- since this was performed asynchronously, we check if the context has changed if trigger.context == nil or event.context.id ~= trigger.context.id then return end -- don't show the list if prefetching results - if trigger.prefetch then return end + if event.context.trigger.kind == 'prefetch' then return end -- don't show if all the sources that defined the trigger character returned no items if event.context.trigger.character ~= nil then diff --git a/lua/blink/cmp/completion/trigger/context.lua b/lua/blink/cmp/completion/trigger/context.lua index 24538a35..7d1f5bf5 100644 --- a/lua/blink/cmp/completion/trigger/context.lua +++ b/lua/blink/cmp/completion/trigger/context.lua @@ -14,7 +14,7 @@ --- @field cursor number[] --- @field line string --- @field bounds blink.cmp.ContextBounds ---- @field trigger { kind: number, character: string | nil } +--- @field trigger blink.cmp.ContextTrigger --- @field providers string[] --- --- @field new fun(opts: blink.cmp.ContextOpts): blink.cmp.Context @@ -28,9 +28,18 @@ --- @field get_bounds fun(line: string, cursor: number[]): blink.cmp.ContextBounds --- @field get_regex_around_cursor fun(range: string, regex_str: string, exclude_from_prefix_regex_str: string): { start_col: number, length: number } +--- @class blink.cmp.ContextTrigger +--- @field initial_kind blink.cmp.CompletionTriggerKind The trigger kind when the context was first created +--- @field initial_character? string The trigger character when initial_kind == 'trigger_character' +--- @field kind blink.cmp.CompletionTriggerKind The current trigger kind +--- @field character? string The trigger character when kind == 'trigger_character' + --- @class blink.cmp.ContextOpts --- @field id number --- @field providers string[] +--- @field initial_trigger_kind blink.cmp.CompletionTriggerKind +--- @field initial_trigger_character? string +--- @field trigger_kind blink.cmp.CompletionTriggerKind --- @field trigger_character? string local keyword_regex = vim.regex(require('blink.cmp.config').completion.keyword.regex) @@ -51,8 +60,9 @@ function context.new(opts) line = line, bounds = context.get_bounds(line, cursor), trigger = { - kind = opts.trigger_character and vim.lsp.protocol.CompletionTriggerKind.TriggerCharacter - or vim.lsp.protocol.CompletionTriggerKind.Invoked, + initial_kind = opts.initial_trigger_kind, + initial_character = opts.initial_trigger_character, + kind = opts.trigger_kind, character = opts.trigger_character, }, providers = opts.providers, diff --git a/lua/blink/cmp/completion/trigger/init.lua b/lua/blink/cmp/completion/trigger/init.lua index 4e24f4a0..17765eef 100644 --- a/lua/blink/cmp/completion/trigger/init.lua +++ b/lua/blink/cmp/completion/trigger/init.lua @@ -1,13 +1,13 @@ +--- @alias blink.cmp.CompletionTriggerKind 'manual' | 'prefetch' | 'keyword' | 'trigger_character' +--- -- Handles hiding and showing the completion window. When a user types a trigger character -- (provided by the sources) or anything matching the `keyword_regex`, we create a new `context`. -- This can be used downstream to determine if we should make new requests to the sources or not. - --- @class blink.cmp.CompletionTrigger --- @field buffer_events blink.cmp.BufferEvents --- @field cmdline_events blink.cmp.CmdlineEvents --- @field current_context_id number --- @field context? blink.cmp.Context ---- @field prefetch? boolean --- @field show_emitter blink.cmp.EventEmitter<{ context: blink.cmp.Context }> --- @field hide_emitter blink.cmp.EventEmitter<{}> --- @@ -15,7 +15,7 @@ --- @field is_trigger_character fun(char: string, is_show_on_x?: 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, providers?: string[], prefetch?: boolean }) +--- @field show fun(opts?: { trigger_kind: blink.cmp.CompletionTriggerKind, trigger_character?: string, force?: boolean, send_upstream?: boolean, providers?: string[], prefetch?: boolean }) --- @field hide fun() --- @field within_query_bounds fun(cursor: number[]): boolean --- @field get_bounds fun(regex: vim.regex, line: string, cursor: number[]): blink.cmp.ContextBounds @@ -45,18 +45,18 @@ function trigger.activate() -- we were told to ignore the text changed event, so we update the context -- but don't send an on_show event upstream if is_ignored then - if trigger.context ~= nil then trigger.show({ send_upstream = false }) end + if trigger.context ~= nil then trigger.show({ send_upstream = false, trigger_kind = 'keyword' }) end - -- character forces a trigger according to the sources, create a fresh context + -- character forces a trigger according to the sources, create a fresh context elseif trigger.is_trigger_character(char) and (config.show_on_trigger_character or trigger.context ~= nil) then trigger.context = nil - trigger.show({ trigger_character = char }) + trigger.show({ trigger_kind = 'trigger_character', trigger_character = char }) - -- character is part of a keyword + -- character is part of a keyword elseif keyword_regex:match_str(char) ~= nil and (config.show_on_keyword or trigger.context ~= nil) then - trigger.show() + trigger.show({ trigger_kind = 'keyword' }) - -- nothing matches so hide + -- nothing matches so hide else trigger.hide() end @@ -66,7 +66,7 @@ function trigger.activate() -- we were told to ignore the cursor moved event, so we update the context -- but don't send an on_show event upstream if is_ignored and event == 'CursorMoved' then - if trigger.context ~= nil then trigger.show({ send_upstream = false }) end + if trigger.context ~= nil then trigger.show({ send_upstream = false, trigger_kind = 'keyword' }) end return end @@ -82,21 +82,21 @@ function trigger.activate() -- check if we're still within the bounds of the query used for the context if trigger.context ~= nil and trigger.context:within_query_bounds(cursor) then - trigger.show() + trigger.show({ trigger_kind = 'keyword' }) -- check if we've entered insert mode on a trigger character elseif insert_enter_on_trigger_character then trigger.context = nil - trigger.show({ trigger_character = char_under_cursor }) + trigger.show({ trigger_kind = 'trigger_character', trigger_character = char_under_cursor }) -- show if we currently have a context, and we've moved outside of it's bounds by 1 char elseif is_on_keyword_char and trigger.context ~= nil and cursor_col == trigger.context.bounds.start_col - 1 then trigger.context = nil - trigger.show() + trigger.show({ trigger_kind = 'keyword' }) -- prefetch completions without opening window on InsertEnter elseif event == 'InsertEnter' and config.prefetch_on_insert then - trigger.show({ prefetch = true }) + trigger.show({ trigger_kind = 'prefetch' }) -- otherwise hide else @@ -157,7 +157,7 @@ function trigger.show_if_on_trigger_character(opts) local char_under_cursor = context.get_line():sub(cursor_col, cursor_col) if trigger.is_trigger_character(char_under_cursor, true) then - trigger.show({ trigger_character = char_under_cursor }) + trigger.show({ trigger_kind = 'trigger_character', trigger_character = char_under_cursor }) end end @@ -186,9 +186,14 @@ function trigger.show(opts) or (trigger.context and trigger.context.providers) or require('blink.cmp.sources.lib').get_enabled_provider_ids(context.get_mode()) - trigger.context = - context.new({ id = trigger.current_context_id, providers = providers, trigger_character = opts.trigger_character }) - trigger.prefetch = opts.prefetch == true + trigger.context = context.new({ + id = trigger.current_context_id, + providers = providers, + initial_trigger_kind = trigger.context and trigger.context.trigger.kind or opts.trigger_kind, + initial_trigger_character = trigger.context and trigger.context.trigger.character or opts.trigger_character, + trigger_kind = opts.trigger_kind, + trigger_character = opts.trigger_character, + }) if opts.send_upstream ~= false then trigger.show_emitter:emit({ context = trigger.context }) end end diff --git a/lua/blink/cmp/completion/windows/menu.lua b/lua/blink/cmp/completion/windows/menu.lua index 69285d36..804fc754 100644 --- a/lua/blink/cmp/completion/windows/menu.lua +++ b/lua/blink/cmp/completion/windows/menu.lua @@ -54,7 +54,8 @@ function menu.open_with_items(context, items) if not menu.renderer then menu.renderer = require('blink.cmp.completion.windows.render').new(config.draw) end menu.renderer:draw(context, menu.win:get_buf(), items) - if menu.auto_show then + local auto_show = type(menu.auto_show) == 'function' and menu.auto_show(context, items) or menu.auto_show + if auto_show then menu.open() menu.update_position() end diff --git a/lua/blink/cmp/config/completion/menu.lua b/lua/blink/cmp/config/completion/menu.lua index 82589ce1..d7469255 100644 --- a/lua/blink/cmp/config/completion/menu.lua +++ b/lua/blink/cmp/config/completion/menu.lua @@ -11,7 +11,7 @@ local validate = require('blink.cmp.config.utils').validate --- @field scrollbar boolean Note that the gutter will be disabled when border ~= 'none' --- @field direction_priority ("n" | "s")[] Which directions to show the window, falling back to the next direction when there's not enough space --- @field order blink.cmp.CompletionMenuOrderConfig TODO: implement ---- @field auto_show boolean Whether to automatically show the window when new completion items are available +--- @field auto_show boolean | fun(ctx: blink.cmp.Context, items: blink.cmp.CompletionItem[]): boolean Whether to automatically show the window when new completion items are available --- @field cmdline_position fun(): number[] Screen coordinates (0-indexed) of the command line --- @field draw blink.cmp.Draw Controls how the completion items are rendered on the popup window @@ -151,7 +151,7 @@ function window.validate(config) 'one of: "n", "s"', }, order = { config.order, 'table' }, - auto_show = { config.auto_show, 'boolean' }, + auto_show = { config.auto_show, { 'boolean', 'function' } }, cmdline_position = { config.cmdline_position, 'function' }, draw = { config.draw, 'table' }, }, config) diff --git a/lua/blink/cmp/init.lua b/lua/blink/cmp/init.lua index 15a45d0b..80a5cc88 100644 --- a/lua/blink/cmp/init.lua +++ b/lua/blink/cmp/init.lua @@ -43,7 +43,11 @@ function cmp.show(opts) vim.schedule(function() require('blink.cmp.completion.windows.menu').auto_show = true - require('blink.cmp.completion.trigger').show({ force = true, providers = opts and opts.providers }) + require('blink.cmp.completion.trigger').show({ + force = true, + providers = opts and opts.providers, + trigger_kind = 'manual', + }) end) return true end diff --git a/lua/blink/cmp/sources/lsp.lua b/lua/blink/cmp/sources/lsp.lua index 871d92ef..b6a120cc 100644 --- a/lua/blink/cmp/sources/lsp.lua +++ b/lua/blink/cmp/sources/lsp.lua @@ -74,10 +74,11 @@ function lsp:get_completions(context, callback) local cancel_fns = {} for _, client in pairs(clients) do local params = vim.lsp.util.make_position_params(0, client.offset_encoding) - params.context = { triggerKind = context.trigger.kind } - if context.trigger.kind == CompletionTriggerKind.TriggerCharacter then - params.context.triggerCharacter = context.trigger.character - end + params.context = { + triggerKind = context.trigger.kind == 'trigger_character' and CompletionTriggerKind.TriggerCharacter + or CompletionTriggerKind.Invoked, + } + if context.trigger.kind == 'trigger_character' then params.context.triggerCharacter = context.trigger.character end local _, request_id = client.request('textDocument/completion', params, function(err, result) if err or result == nil then