From 11871727278381febd05d1ee1a17f98fb2e32b26 Mon Sep 17 00:00:00 2001 From: Liam Dyer Date: Wed, 27 Nov 2024 15:16:36 -0500 Subject: [PATCH] feat: nuke the debt (#389) Sweeping refactor of the codebase touching just about everything: reworked config with validation, trigger, new list component for managing state, stateless windows, etc. This will set us up for cmdline completions (#323), sources v3 (keywords, async, fallback tree, ...), and documentation. After which, we can hopefully release v1 --- README.md | 549 ++++++++-------- lua/blink/cmp/accept/brackets/init.lua | 6 - .../cmp/{ => completion}/accept/init.lua | 22 +- lua/blink/cmp/completion/accept/prefix.lua | 58 ++ .../cmp/{ => completion}/accept/preview.lua | 8 +- .../brackets/config.lua | 0 lua/blink/cmp/completion/brackets/init.lua | 6 + .../{accept => completion}/brackets/kind.lua | 3 +- .../brackets/semantic.lua | 4 +- .../{accept => completion}/brackets/utils.lua | 4 +- lua/blink/cmp/completion/init.lua | 67 ++ lua/blink/cmp/completion/list.lua | 177 ++++++ lua/blink/cmp/completion/prefetch.lua | 29 + .../completion.lua => completion/trigger.lua} | 185 +++--- .../cmp/completion/windows/documentation.lua | 202 ++++++ .../windows/ghost_text.lua} | 44 +- lua/blink/cmp/completion/windows/menu.lua | 120 ++++ .../windows/render/column.lua | 2 +- .../windows/render/context.lua | 10 +- .../{ => completion}/windows/render/init.lua | 7 +- .../completion/windows/render/tailwind.lua | 17 + .../{ => completion}/windows/render/text.lua | 0 .../{ => completion}/windows/render/types.lua | 0 lua/blink/cmp/config.lua | 590 ------------------ lua/blink/cmp/config/appearance.lua | 58 ++ lua/blink/cmp/config/completion/accept.lua | 72 +++ .../cmp/config/completion/documentation.lua | 94 +++ .../cmp/config/completion/ghost_text.lua | 19 + lua/blink/cmp/config/completion/init.lua | 42 ++ lua/blink/cmp/config/completion/keyword.lua | 35 ++ lua/blink/cmp/config/completion/list.lua | 44 ++ lua/blink/cmp/config/completion/menu.lua | 183 ++++++ lua/blink/cmp/config/completion/trigger.lua | 30 + lua/blink/cmp/config/fuzzy.lua | 44 ++ lua/blink/cmp/config/init.lua | 55 ++ lua/blink/cmp/config/keymap.lua | 152 +++++ lua/blink/cmp/config/shared.lua | 2 + lua/blink/cmp/config/signature.lua | 72 +++ lua/blink/cmp/config/snippets.lua | 26 + lua/blink/cmp/config/sources.lua | 94 +++ lua/blink/cmp/config/utils.lua | 13 + lua/blink/cmp/fuzzy/init.lua | 26 +- lua/blink/cmp/health.lua | 5 +- lua/blink/cmp/highlights.lua | 42 ++ lua/blink/cmp/init.lua | 244 +++----- lua/blink/cmp/keymap.lua | 261 -------- lua/blink/cmp/keymap/apply.lua | 81 +++ lua/blink/cmp/keymap/fallback.lua | 51 ++ lua/blink/cmp/keymap/init.lua | 36 ++ lua/blink/cmp/keymap/presets.lua | 72 +++ lua/blink/cmp/{sources => }/lib/async.lua | 0 lua/blink/cmp/lib/buffer_events.lua | 120 ++++ lua/blink/cmp/lib/event_emitter.lua | 33 + .../text-edits.lua => lib/text_edits.lua} | 9 +- lua/blink/cmp/{ => lib}/utils.lua | 71 --- .../cmp/{windows/lib => lib/window}/docs.lua | 8 +- .../cmp/{windows/lib => lib/window}/init.lua | 2 +- .../lib => lib/window}/scrollbar/geometry.lua | 0 .../lib => lib/window}/scrollbar/init.lua | 6 +- .../lib => lib/window}/scrollbar/win.lua | 0 lua/blink/cmp/signature/init.lua | 23 + lua/blink/cmp/signature/list.lua | 1 + lua/blink/cmp/signature/trigger.lua | 132 ++++ .../signature.lua => signature/window.lua} | 70 ++- lua/blink/cmp/snippets.lua | 0 lua/blink/cmp/sources/lib/context.lua | 4 +- lua/blink/cmp/sources/lib/init.lua | 85 +-- lua/blink/cmp/sources/lib/provider/init.lua | 8 +- lua/blink/cmp/sources/path/fs.lua | 2 +- lua/blink/cmp/trigger/signature.lua | 136 ---- lua/blink/cmp/trigger/types.lua | 22 - lua/blink/cmp/windows/autocomplete.lua | 306 --------- lua/blink/cmp/windows/documentation.lua | 193 ------ 73 files changed, 2900 insertions(+), 2294 deletions(-) delete mode 100644 lua/blink/cmp/accept/brackets/init.lua rename lua/blink/cmp/{ => completion}/accept/init.lua (83%) create mode 100644 lua/blink/cmp/completion/accept/prefix.lua rename lua/blink/cmp/{ => completion}/accept/preview.lua (66%) rename lua/blink/cmp/{accept => completion}/brackets/config.lua (100%) create mode 100644 lua/blink/cmp/completion/brackets/init.lua rename lua/blink/cmp/{accept => completion}/brackets/kind.lua (94%) rename lua/blink/cmp/{accept => completion}/brackets/semantic.lua (96%) rename lua/blink/cmp/{accept => completion}/brackets/utils.lua (92%) create mode 100644 lua/blink/cmp/completion/init.lua create mode 100644 lua/blink/cmp/completion/list.lua create mode 100644 lua/blink/cmp/completion/prefetch.lua rename lua/blink/cmp/{trigger/completion.lua => completion/trigger.lua} (54%) create mode 100644 lua/blink/cmp/completion/windows/documentation.lua rename lua/blink/cmp/{windows/ghost-text.lua => completion/windows/ghost_text.lua} (67%) create mode 100644 lua/blink/cmp/completion/windows/menu.lua rename lua/blink/cmp/{ => completion}/windows/render/column.lua (98%) rename lua/blink/cmp/{ => completion}/windows/render/context.lua (94%) rename lua/blink/cmp/{ => completion}/windows/render/init.lua (95%) create mode 100644 lua/blink/cmp/completion/windows/render/tailwind.lua rename lua/blink/cmp/{ => completion}/windows/render/text.lua (100%) rename lua/blink/cmp/{ => completion}/windows/render/types.lua (100%) delete mode 100644 lua/blink/cmp/config.lua create mode 100644 lua/blink/cmp/config/appearance.lua create mode 100644 lua/blink/cmp/config/completion/accept.lua create mode 100644 lua/blink/cmp/config/completion/documentation.lua create mode 100644 lua/blink/cmp/config/completion/ghost_text.lua create mode 100644 lua/blink/cmp/config/completion/init.lua create mode 100644 lua/blink/cmp/config/completion/keyword.lua create mode 100644 lua/blink/cmp/config/completion/list.lua create mode 100644 lua/blink/cmp/config/completion/menu.lua create mode 100644 lua/blink/cmp/config/completion/trigger.lua create mode 100644 lua/blink/cmp/config/fuzzy.lua create mode 100644 lua/blink/cmp/config/init.lua create mode 100644 lua/blink/cmp/config/keymap.lua create mode 100644 lua/blink/cmp/config/shared.lua create mode 100644 lua/blink/cmp/config/signature.lua create mode 100644 lua/blink/cmp/config/snippets.lua create mode 100644 lua/blink/cmp/config/sources.lua create mode 100644 lua/blink/cmp/config/utils.lua create mode 100644 lua/blink/cmp/highlights.lua delete mode 100644 lua/blink/cmp/keymap.lua create mode 100644 lua/blink/cmp/keymap/apply.lua create mode 100644 lua/blink/cmp/keymap/fallback.lua create mode 100644 lua/blink/cmp/keymap/init.lua create mode 100644 lua/blink/cmp/keymap/presets.lua rename lua/blink/cmp/{sources => }/lib/async.lua (100%) create mode 100644 lua/blink/cmp/lib/buffer_events.lua create mode 100644 lua/blink/cmp/lib/event_emitter.lua rename lua/blink/cmp/{accept/text-edits.lua => lib/text_edits.lua} (96%) rename lua/blink/cmp/{ => lib}/utils.lua (51%) rename lua/blink/cmp/{windows/lib => lib/window}/docs.lua (96%) rename lua/blink/cmp/{windows/lib => lib/window}/init.lua (99%) rename lua/blink/cmp/{windows/lib => lib/window}/scrollbar/geometry.lua (100%) rename lua/blink/cmp/{windows/lib => lib/window}/scrollbar/init.lua (88%) rename lua/blink/cmp/{windows/lib => lib/window}/scrollbar/win.lua (100%) create mode 100644 lua/blink/cmp/signature/init.lua create mode 100644 lua/blink/cmp/signature/list.lua create mode 100644 lua/blink/cmp/signature/trigger.lua rename lua/blink/cmp/{windows/signature.lua => signature/window.lua} (69%) create mode 100644 lua/blink/cmp/snippets.lua delete mode 100644 lua/blink/cmp/trigger/signature.lua delete mode 100644 lua/blink/cmp/trigger/types.lua delete mode 100644 lua/blink/cmp/windows/autocomplete.lua delete mode 100644 lua/blink/cmp/windows/documentation.lua diff --git a/README.md b/README.md index 28b86e4a..7f8ebaff 100644 --- a/README.md +++ b/README.md @@ -56,15 +56,15 @@ -- your own keymap. keymap = { preset = 'default' }, - highlight = { - -- sets the fallback highlight groups to nvim-cmp's highlight groups - -- useful for when your theme doesn't support blink.cmp - -- will be removed in a future release, assuming themes add support + appearance = { + -- Sets the fallback highlight groups to nvim-cmp's highlight groups + -- Useful for when your theme doesn't support blink.cmp + -- will be removed in a future release use_nvim_cmp_as_default = true, + -- Set to 'mono' for 'Nerd Font Mono' or 'normal' for 'Nerd Font' + -- Adjusts spacing to ensure icons are aligned + nerd_font_variant = 'mono' }, - -- set to 'mono' for 'Nerd Font Mono' or 'normal' for 'Nerd Font' - -- adjusts spacing to ensure icons are aligned - nerd_font_variant = 'mono', -- default list of enabled providers defined so that you can extend it -- elsewhere in your config, without redefining it, via `opts_extend` @@ -75,10 +75,10 @@ }, -- experimental auto-brackets support - -- accept = { auto_brackets = { enabled = true } } + -- completion = { accept = { auto_brackets = { enabled = true } } } -- experimental signature help support - -- trigger = { signature_help = { enabled = true } } + -- signature = { enabled = true } }, -- allows extending the enabled_providers array elsewhere in your config -- without having to redefine it @@ -170,10 +170,6 @@ MiniDeps.add({ ```lua { - -- The keymap can be: - -- - A preset ('default' | 'super-tab' | 'enter') - -- - A table of keys => command[] (optionally with a "preset" key to merge with a preset) - -- -- When specifying 'preset' in the keymap table, the custom key mappings are merged with the preset, -- and any conflicting keys will overwrite the preset mappings. -- The "fallback" command will run the next non blink keymap. @@ -210,8 +206,8 @@ MiniDeps.add({ -- [''] = { 'snippet_backward', 'fallback' }, -- -- "super-tab" keymap - -- you may want to set `trigger.completion.show_in_snippet = false` - -- or use `window.autocomplete.selection = "manual" | "auto_insert"` + -- you may want to set `completion.trigger.show_in_snippet = false` + -- or use `completion.list.selection = "manual" | "auto_insert"` -- -- [''] = { 'show', 'show_documentation', 'hide_documentation' }, -- [''] = { 'hide', 'fallback' }, @@ -235,7 +231,7 @@ MiniDeps.add({ -- [''] = { 'scroll_documentation_down', 'fallback' }, -- -- "enter" keymap - -- you may want to set `window.autocomplete.selection = "manual" | "auto_insert"` + -- you may want to set `completion.list.selection = "manual" | "auto_insert"` -- -- [''] = { 'show', 'show_documentation', 'hide_documentation' }, -- [''] = { 'hide', 'fallback' }, @@ -253,184 +249,113 @@ MiniDeps.add({ -- [''] = { 'scroll_documentation_down', 'fallback' }, keymap = 'default', - accept = { - create_undo_point = true, - -- Function used to expand snippets, for luasnip users, you may use:: - -- function(snippet) require('luasnip').lsp_expand(snippet) end - -- See the "Luasnip" section for info on setting up the luasnip source - expand_snippet = vim.snippet.expand, + -- Disables keymaps, completions and signature help for these filetypes + blocked_filetypes = {}, - auto_brackets = { - enabled = false, - default_brackets = { '(', ')' }, - override_brackets_for_filetypes = {}, - -- Overrides the default blocked filetypes - force_allow_filetypes = {}, - blocked_filetypes = {}, - -- Synchronously use the kind of the item to determine if brackets should be added - kind_resolution = { - enabled = true, - blocked_filetypes = { 'typescriptreact', 'javascriptreact', 'vue' }, - }, - -- Asynchronously use semantic token to determine if brackets should be added - semantic_token_resolution = { - enabled = true, - blocked_filetypes = {}, - -- How long to wait for semantic tokens to return before assuming no brackets should be added - timeout_ms = 400, - }, - }, + snippets = { + -- Function to use when expanding LSP provided snippets + expand = function(snippet) vim.snippet.expand(snippet) end, + -- Function to use when checking if a snippet is active + active = function(filter) vim.snippet.active(filter) end, + -- Function to use when jumping between tab stops in a snippet, where direction can be negative or positive + jump = function(direction) vim.snippet.jump(direction) end, }, - trigger = { - completion = { + completion = { + keyword = { -- 'prefix' will fuzzy match on the text before the cursor -- 'full' will fuzzy match on the text before *and* after the cursor -- example: 'foo_|_bar' will match 'foo_' for 'prefix' and 'foo__bar' for 'full' - keyword_range = 'prefix', - -- regex used to get the text when fuzzy matching - -- changing this may break some sources, so please report if you run into issues - -- TODO: shouldnt this also affect the accept command? should this also be per language? - keyword_regex = '[%w_\\-]', - -- after matching with keyword_regex, any characters matching this regex at the prefix will be excluded + range = 'prefix', + -- Regex used to get the text when fuzzy matching + regex = '[%w_\\-]', + -- After matching with regex, any characters matching this regex at the prefix will be excluded exclude_from_prefix_regex = '[\\-]', + }, + + trigger = { + -- When false, will not show the completion window automatically when in a snippet + show_in_snippet = true, -- 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 - blocked_trigger_characters = { ' ', '\n', '\t' }, - -- when true, will show the completion window when the cursor comes after a trigger character after accepting an item + -- always show the window. We block these by default. + show_on_blocked_trigger_characters = { ' ', '\n', '\t' }, + -- When 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, - -- when true, will show the completion window when the cursor comes after a trigger character when entering insert mode + -- When true, will show the completion window when the cursor comes after a trigger character + -- when entering insert mode show_on_insert_on_trigger_character = true, - -- list of additional trigger characters that won't trigger the completion window when the cursor comes after a trigger character when entering insert mode/accepting an item + -- 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 show_on_x_blocked_trigger_characters = { "'", '"', '(' }, - -- when false, will not show the completion window automatically when in a snippet - show_in_snippet = true, - }, - - signature_help = { - enabled = false, - blocked_trigger_characters = {}, - blocked_retrigger_characters = {}, - -- when true, will show the signature help window when the cursor comes after a trigger character when entering insert mode - show_on_insert_on_trigger_character = true, - }, - }, - - fuzzy = { - -- when enabled, allows for a number of typos relative to the length of the query - -- disabling this matches the behavior of fzf - use_typo_resistance = true, - -- frencency tracks the most recently/frequently used items and boosts the score of the item - use_frecency = true, - -- 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' }, - - prebuilt_binaries = { - -- Whether 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` - download = true, - -- 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). - -- - -- Beware that if the FFI ABI changes while tracking main then this may result in blink breaking. - force_version = nil, - -- 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 - -- - -- Beware that if the FFI ABI changes while tracking main then this may result in blink breaking. - force_system_triple = nil, - }, - }, - - sources = { - -- list of enabled providers - completion = { - enabled_providers = { 'lsp', 'path', 'snippets', 'buffer' }, }, - -- Please see https://github.com/Saghen/blink.compat for using `nvim-cmp` sources - providers = { - lsp = { - name = 'LSP', - module = 'blink.cmp.sources.lsp', - - --- *All* of the 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 - enabled = true, -- whether or not to enable the provider - 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 return - min_keyword_length = 0, -- minimum number of characters to trigger the provider - fallback_for = {}, -- if any of these providers return 0 items, it will fallback to this provider - score_offset = 0, -- boost/penalize the score of the items - override = nil, -- override the source's functions - }, - path = { - name = 'Path', - module = 'blink.cmp.sources.path', - score_offset = 3, - opts = { - trailing_slash = false, - label_trailing_slash = true, - get_cwd = function(context) return vim.fn.expand(('#%d:p:h'):format(context.bufnr)) end, - show_hidden_files_by_default = false, - } + list = { + -- Maximum number of items to display + max_items = 200, + -- Controls if completion items will be selected automatically, + -- and whether selection automatically inserts + selection = 'preselect', + -- Controls how the completion items are selected + -- 'preselect' will automatically select the first item in the completion list + -- 'manual' will not select any item by default + -- 'auto_insert' will not select any item by default, and insert the completion items automatically + -- when selecting them + -- + -- You may want to bind a key to the `cancel` command, which will undo the selection + -- when using 'auto_insert' + cycle = { + -- When `true`, calling `select_next` at the *bottom* of the completion list + -- will select the *first* completion item. + from_bottom = true, + -- When `true`, calling `select_prev` at the *top* of the completion list + -- will select the *last* completion item. + from_top = true, }, - snippets = { - name = 'Snippets', - module = 'blink.cmp.sources.snippets', - score_offset = -3, - opts = { - friendly_snippets = true, - search_paths = { vim.fn.stdpath('config') .. '/snippets' }, - global_snippets = { 'all' }, - extended_filetypes = {}, - ignored_filetypes = {}, - get_filetype = function(context) - return vim.bo.filetype - end - } + }, - --- Example usage for disabling the snippet provider after pressing trigger characters (i.e. ".") - -- enabled = function(ctx) return ctx ~= nil and ctx.trigger.kind == vim.lsp.protocol.CompletionTriggerKind.TriggerCharacter end, - }, - buffer = { - name = 'Buffer', - module = 'blink.cmp.sources.buffer', - fallback_for = { 'lsp' }, + accept = { + -- Create an undo point when accepting a completion item + create_undo_point = true, + -- Experimental auto-brackets support + auto_brackets = { + -- Whether to auto-insert brackets for functions + enabled = false, + -- Default brackets to use for unknown languages + default_brackets = { '(', ')' }, + -- Overrides the default blocked filetypes + override_brackets_for_filetypes = {}, + -- Synchronously use the kind of the item to determine if brackets should be added + kind_resolution = { + enabled = true, + blocked_filetypes = { 'typescriptreact', 'javascriptreact', 'vue' }, + }, + -- Asynchronously use semantic token to determine if brackets should be added + semantic_token_resolution = { + enabled = true, + blocked_filetypes = {}, + -- How long to wait for semantic tokens to return before assuming no brackets should be added + timeout_ms = 400, + }, }, }, - }, - windows = { - autocomplete = { + menu = { + enabled = true, min_width = 15, max_height = 10, border = 'none', winblend = 0, winhighlight = 'Normal:BlinkCmpMenu,FloatBorder:BlinkCmpMenuBorder,CursorLine:BlinkCmpMenuSelection,Search:None', - -- keep the cursor X lines away from the top/bottom of the window + -- Keep the cursor X lines away from the top/bottom of the window scrolloff = 2, - -- note that the gutter will be disabled when border ~= 'none' + -- Note that the gutter will be disabled when border ~= 'none' scrollbar = true, - -- which directions to show the window, + -- Which directions to show the window, -- falling back to the next direction when there's not enough space direction_priority = { 's', 'n' }, - -- Controls whether the completion window will automatically show when typing - auto_show = true, - -- Controls how the completion items are selected - -- 'preselect' will automatically select the first item in the completion list - -- 'manual' will not select any item by default - -- 'auto_insert' will not select any item by default, and insert the completion items automatically when selecting them - -- - -- When using 'auto_insert', you may want to bind a key to the `cancel` command, which will undo the selection - selection = 'preselect', -- Controls how the completion items are rendered on the popup window draw = { -- Aligns the keyword you've typed to a component in the menu @@ -455,7 +380,7 @@ MiniDeps.add({ ellipsis = false, text = function(ctx) return ctx.kind_icon .. ctx.icon_gap end, highlight = function(ctx) - return require('blink.cmp.utils').get_tailwind_hl(ctx) or 'BlinkCmpKind' .. ctx.kind + return require('blink.cmp.lib.utils').get_tailwind_hl(ctx) or 'BlinkCmpKind' .. ctx.kind end, }, @@ -464,7 +389,7 @@ MiniDeps.add({ width = { fill = true }, text = function(ctx) return ctx.kind end, highlight = function(ctx) - return require('blink.cmp.utils').get_tailwind_hl(ctx) or 'BlinkCmpKind' .. ctx.kind + return require('blink.cmp.lib.utils').get_tailwind_hl(ctx) or 'BlinkCmpKind' .. ctx.kind end, }, @@ -496,106 +421,215 @@ MiniDeps.add({ }, }, }, - -- Controls the cycling behavior when reaching the beginning or end of the completion list. - cycle = { - -- When `true`, calling `select_next` at the *bottom* of the completion list will select the *first* completion item. - from_bottom = true, - -- When `true`, calling `select_prev` at the *top* of the completion list will select the *last* completion item. - from_top = true, - }, }, + documentation = { - min_width = 10, - max_width = 60, - max_height = 20, - border = 'padded', - winblend = 0, - winhighlight = 'Normal:BlinkCmpDoc,FloatBorder:BlinkCmpDocBorder,CursorLine:BlinkCmpDocCursorLine,Search:None', - -- note that the gutter will be disabled when border ~= 'none' - scrollbar = true, - -- which directions to show the documentation window, - -- for each of the possible autocomplete window directions, - -- falling back to the next direction when there's not enough space - direction_priority = { - autocomplete_north = { 'e', 'w', 'n', 's' }, - autocomplete_south = { 'e', 'w', 's', 'n' }, - }, -- Controls whether the documentation window will automatically show when selecting a completion item auto_show = false, + -- Delay before showing the documentation window auto_show_delay_ms = 500, + -- Delay before updating the documentation window when selecting a new item, + -- while an existing item is still visible update_delay_ms = 50, - -- whether to use treesitter highlighting, disable if you run into performance issues - -- WARN: temporary, eventually blink will support regex highlighting + -- Whether to use treesitter highlighting, disable if you run into performance issues treesitter_highlighting = true, + window = { + min_width = 10, + max_width = 60, + max_height = 20, + border = 'padded', + winblend = 0, + winhighlight = 'Normal:BlinkCmpDoc,FloatBorder:BlinkCmpDocBorder,CursorLine:BlinkCmpDocCursorLine,Search:None', + -- Note that the gutter will be disabled when border ~= 'none' + scrollbar = true, + -- Which directions to show the documentation window, + -- for each of the possible menu window directions, + -- falling back to the next direction when there's not enough space + direction_priority = { + menu_north = { 'e', 'w', 'n', 's' }, + menu_south = { 'e', 'w', 's', 'n' }, + }, + }, + }, + -- Displays a preview of the selected item on the current line + ghost_text = { + enabled = false, }, - signature_help = { + }, + + -- Experimental signature help support + signature = { + enabled = false, + trigger = { + blocked_trigger_characters = {}, + blocked_retrigger_characters = {}, + -- When true, will show the signature help window when the cursor comes after a trigger character when entering insert mode + show_on_insert_on_trigger_character = true, + }, + window = { min_width = 1, max_width = 100, max_height = 10, border = 'padded', winblend = 0, winhighlight = 'Normal:BlinkCmpSignatureHelp,FloatBorder:BlinkCmpSignatureHelpBorder', - -- note that the gutter will be disabled when border ~= 'none' - scrollbar = false, - - -- which directions to show the window, - -- falling back to the next direction when there's not enough space + scrollbar = false, -- Note that the gutter will be disabled when border ~= 'none' + -- Which directions to show the window, + -- falling back to the next direction when there's not enough space, + -- or another window is in the way direction_priority = { 'n', 's' }, - -- whether to use treesitter highlighting, disable if you run into performance issues - -- WARN: temporary, eventually blink will support regex highlighting + -- Disable if you run into performance issues treesitter_highlighting = true, }, - ghost_text = { - enabled = false, - }, }, - highlight = { - ns = vim.api.nvim_create_namespace('blink_cmp'), - -- sets the fallback highlight groups to nvim-cmp's highlight groups - -- useful for when your theme doesn't support blink.cmp - -- will be removed in a future release, assuming themes add support - use_nvim_cmp_as_default = false, + + fuzzy = { + -- when enabled, allows for a number of typos relative to the length of the query + -- disabling this matches the behavior of fzf + use_typo_resistance = true, + -- frencency tracks the most recently/frequently used items and boosts the score of the item + use_frecency = true, + -- 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' }, + + prebuilt_binaries = { + -- Whether 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` + download = true, + -- 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). + -- + -- Beware that if the FFI ABI changes while tracking main then this may result in blink breaking. + force_version = nil, + -- 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 + -- + -- Beware that if the FFI ABI changes while tracking main then this may result in blink breaking. + force_system_triple = nil, + }, }, - -- set to 'mono' for 'Nerd Font Mono' or 'normal' for 'Nerd Font' - -- adjusts spacing to ensure icons are aligned - nerd_font_variant = 'mono', + 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()) + -- return { 'buffer' } + -- else + -- return { 'lsp', 'path', 'snippets', 'buffer' } + -- end + -- end + }, + + -- Please see https://github.com/Saghen/blink.compat for using `nvim-cmp` sources + providers = { + lsp = { + name = 'LSP', + module = 'blink.cmp.sources.lsp', - -- don't show completions or signature help for these filetypes. Keymaps are also disabled. - blocked_filetypes = {}, + --- *All* of the 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 + 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 + score_offset = 0, -- Boost/penalize the score of the items + override = nil, -- Override the source's functions + }, + path = { + name = 'Path', + module = 'blink.cmp.sources.path', + score_offset = 3, + opts = { + trailing_slash = false, + label_trailing_slash = true, + get_cwd = function(context) return vim.fn.expand(('#%d:p:h'):format(context.bufnr)) end, + show_hidden_files_by_default = false, + } + }, + snippets = { + name = 'Snippets', + module = 'blink.cmp.sources.snippets', + score_offset = -3, + opts = { + friendly_snippets = true, + search_paths = { vim.fn.stdpath('config') .. '/snippets' }, + global_snippets = { 'all' }, + extended_filetypes = {}, + ignored_filetypes = {}, + get_filetype = function(context) + return vim.bo.filetype + end + } + + --- Example usage for disabling the snippet provider after pressing trigger characters (i.e. ".") + -- enabled = function(ctx) + -- return ctx ~= nil and ctx.trigger.kind == vim.lsp.protocol.CompletionTriggerKind.TriggerCharacter + -- end, + }, + buffer = { + name = 'Buffer', + module = 'blink.cmp.sources.buffer', + fallback_for = { 'lsp' }, + }, + }, + }, - kind_icons = { - Text = '󰉿', - Method = '󰊕', - Function = '󰊕', - Constructor = '󰒓', - - Field = '󰜢', - Variable = '󰆦', - Property = '󰖷', - - Class = '󱡠', - Interface = '󱡠', - Struct = '󱡠', - Module = '󰅩', - - Unit = '󰪚', - Value = '󰦨', - Enum = '󰦨', - EnumMember = '󰦨', - - Keyword = '󰻾', - Constant = '󰏿', - - Snippet = '󱄽', - Color = '󰏘', - File = '󰈔', - Reference = '󰬲', - Folder = '󰉋', - Event = '󱐋', - Operator = '󰪚', - TypeParameter = '󰬛', + appearance = { + highlight_ns = vim.api.nvim_create_namespace('blink_cmp'), + -- Sets the fallback highlight groups to nvim-cmp's highlight groups + -- Useful for when your theme doesn't support blink.cmp + -- Will be removed in a future release + use_nvim_cmp_as_default = false, + -- Set to 'mono' for 'Nerd Font Mono' or 'normal' for 'Nerd Font' + -- Adjusts spacing to ensure icons are aligned + nerd_font_variant = 'mono', + kind_icons = { + Text = '󰉿', + Method = '󰊕', + Function = '󰊕', + Constructor = '󰒓', + + Field = '󰜢', + Variable = '󰆦', + Property = '󰖷', + + Class = '󱡠', + Interface = '󱡠', + Struct = '󱡠', + Module = '󰅩', + + Unit = '󰪚', + Value = '󰦨', + Enum = '󰦨', + EnumMember = '󰦨', + + Keyword = '󰻾', + Constant = '󰏿', + + Snippet = '󱄽', + Color = '󰏘', + File = '󰈔', + Reference = '󰬲', + Folder = '󰉋', + Event = '󱐋', + Operator = '󰪚', + TypeParameter = '󰬛', + }, }, } ``` @@ -653,8 +687,15 @@ There's currently no `blink.cmp` native source for [luasnip](https://github.com/ -- lock compat to tagged versions, if you've also locked blink.cmp to tagged versions { 'saghen/blink.compat', version = '*', opts = { impersonate_nvim_cmp = true } } }, opts = { - accept = { - expand_snippet = function(snippet) require('luasnip').lsp_expand(snippet) end, + snippets = { + expand = function(snippet) require('luasnip').lsp_expand(snippet) end, + active = function(filter) + if filter and filter.direction then + return require('luasnip').jumpable(filter.direction) + end + return require('luasnip').expandable() + end, + jump = function(direction) require('luasnip').jump(direction) end, }, sources = { completion = { @@ -690,7 +731,7 @@ There's currently no `blink.cmp` native source for [luasnip](https://github.com/ ```lua --- @module 'blink.cmp' --- @type blink.cmp.Draw -windows.autocomplete.draw = { +completion.menu.draw = { -- Aligns the keyword you've typed to a component in the menu align_to_component = 'label', -- or 'none' to disable -- Left and right padding, optionally { left, right } for different padding on each side @@ -713,7 +754,7 @@ windows.autocomplete.draw = { ellipsis = false, text = function(ctx) return ctx.kind_icon .. ctx.icon_gap end, highlight = function(ctx) - return require('blink.cmp.utils').get_tailwind_hl(ctx) or 'BlinkCmpKind' .. ctx.kind + return require('blink.cmp.lib.utils').get_tailwind_hl(ctx) or 'BlinkCmpKind' .. ctx.kind end, }, @@ -722,7 +763,7 @@ windows.autocomplete.draw = { width = { fill = true }, text = function(ctx) return ctx.kind end, highlight = function(ctx) - return require('blink.cmp.utils').get_tailwind_hl(ctx) or 'BlinkCmpKind' .. ctx.kind + return require('blink.cmp.lib.utils').get_tailwind_hl(ctx) or 'BlinkCmpKind' .. ctx.kind end, }, @@ -753,7 +794,7 @@ windows.autocomplete.draw = { highlight = 'BlinkCmpLabelDescription', }, }, -}, +} ``` @@ -768,8 +809,8 @@ For a setup similar to nvim-cmp, use the following config: ```lua { - windows = { - autocomplete = { + completion = { + menu = { draw = { columns = { { "label", "label_description", gap = 1 }, { "kind_icon", "kind" } }, }, @@ -786,7 +827,7 @@ The plugin use a 4 stage pipeline: trigger -> sources -> fuzzy -> render 3. **Fuzzy:** Rust <-> Lua FFI which performs both filtering and sorting of the items - **Filtering:** The fuzzy matching uses smith-waterman, same as FZF, but implemented in SIMD for ~6x the performance of FZF (TODO: add benchmarks). Due to the SIMD's performance, the prefiltering phase on FZF was dropped to allow for typos. Similar to fzy/fzf, additional points are given to prefix matches, characters with capitals (to promote camelCase/PascalCase first char matching) and matches after delimiters (to promote snake_case first char matching) - **Sorting:** Combines fuzzy matching score with frecency and proximity bonus. Each completion item may also include a `score_offset` which will be added to this score to demote certain sources. The `snippets` source takes advantage of this to avoid taking precedence over the LSP source. The parameters here still need to be tuned, so please let me know if you find some magical parameters! -4. **Windows:** Responsible for placing the autocomplete, documentation and function parameters windows. All of the rendering can be overridden following a syntax similar to incline.nvim. It uses the neovim window decoration provider to provide next to no overhead from highlighting. +4. **Windows:** Responsible for placing the menu, documentation and function parameters windows. All of the rendering can be overridden following a syntax similar to incline.nvim. It uses the neovim window decoration provider to provide next to no overhead from highlighting. ## Compared to nvim-cmp diff --git a/lua/blink/cmp/accept/brackets/init.lua b/lua/blink/cmp/accept/brackets/init.lua deleted file mode 100644 index b17ad30a..00000000 --- a/lua/blink/cmp/accept/brackets/init.lua +++ /dev/null @@ -1,6 +0,0 @@ -local brackets = {} - -brackets.add_brackets = require('blink.cmp.accept.brackets.kind') -brackets.add_brackets_via_semantic_token = require('blink.cmp.accept.brackets.semantic') - -return brackets diff --git a/lua/blink/cmp/accept/init.lua b/lua/blink/cmp/completion/accept/init.lua similarity index 83% rename from lua/blink/cmp/accept/init.lua rename to lua/blink/cmp/completion/accept/init.lua index 1a3cd49d..533d4b5e 100644 --- a/lua/blink/cmp/accept/init.lua +++ b/lua/blink/cmp/completion/accept/init.lua @@ -1,16 +1,18 @@ -local text_edits_lib = require('blink.cmp.accept.text-edits') -local brackets_lib = require('blink.cmp.accept.brackets') +local text_edits_lib = require('blink.cmp.lib.text_edits') +local brackets_lib = require('blink.cmp.completion.brackets') --- Applies a completion item to the current buffer --- @param ctx blink.cmp.Context --- @param item blink.cmp.CompletionItem -local function accept(ctx, item) +--- @param callback fun() +local function accept(ctx, item, callback) local sources = require('blink.cmp.sources.lib') - require('blink.cmp.trigger.completion').hide() + require('blink.cmp.completion.trigger').hide() -- let the source execute the item itself if it indicates it can if sources.should_execute(item) then sources.execute(ctx, item) + callback() return end @@ -48,7 +50,7 @@ local function accept(ctx, item) text_edits_lib.apply(all_text_edits) -- Expand the snippet - require('blink.cmp.config').accept.expand_snippet(item.textEdit.newText) + require('blink.cmp.config').snippets.expand(item.textEdit.newText) -- OR Normal: Apply the text edit and move the cursor else @@ -66,12 +68,14 @@ local function accept(ctx, item) -- TODO: since we apply the additional text edits after, auto imported functions will not -- get auto brackets. If we apply them before, we have to modify the textEdit to compensate brackets_lib.add_brackets_via_semantic_token(vim.bo.filetype, item, function() - require('blink.cmp.trigger.completion').show_if_on_trigger_character({ is_accept = true }) - require('blink.cmp.trigger.signature').show_if_on_trigger_character() + require('blink.cmp.completion.trigger').show_if_on_trigger_character({ is_accept = true }) + require('blink.cmp.signature.trigger').show_if_on_trigger_character() + callback() end) else - require('blink.cmp.trigger.completion').show_if_on_trigger_character({ is_accept = true }) - require('blink.cmp.trigger.signature').show_if_on_trigger_character() + require('blink.cmp.completion.trigger').show_if_on_trigger_character({ is_accept = true }) + require('blink.cmp.signature.trigger').show_if_on_trigger_character() + callback() end -- Notify the rust module that the item was accessed diff --git a/lua/blink/cmp/completion/accept/prefix.lua b/lua/blink/cmp/completion/accept/prefix.lua new file mode 100644 index 00000000..3c517151 --- /dev/null +++ b/lua/blink/cmp/completion/accept/prefix.lua @@ -0,0 +1,58 @@ +local PAIRS_AND_INVALID_CHARS = {} +string.gsub('\'"=$()[]<>{} \t\n\r', '.', function(char) PAIRS_AND_INVALID_CHARS[string.byte(char)] = true end) + +local CLOSING_PAIR = { + [string.byte('<')] = string.byte('>'), + [string.byte('[')] = string.byte(']'), + [string.byte('(')] = string.byte(')'), + [string.byte('{')] = string.byte('}'), + [string.byte('"')] = string.byte('"'), + [string.byte("'")] = string.byte("'"), +} + +local ALPHANUMERIC = {} +string.gsub( + 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789', + '.', + function(char) ALPHANUMERIC[string.byte(char)] = true end +) + +--- Gets the prefix of the given text, stopping at brackets and quotes +--- @param text string +--- @return string +local function get_prefix_before_brackets_and_quotes(text) + local closing_pairs_stack = {} + local word = '' + + local add = function(char) + word = word .. string.char(char) + + -- if we've seen the opening pair, and we've just received the closing pair, + -- remove it from the closing pairs stack + if closing_pairs_stack[#closing_pairs_stack] == char then + table.remove(closing_pairs_stack, #closing_pairs_stack) + -- if the character is an opening pair, add it to the closing pairs stack + elseif CLOSING_PAIR[char] ~= nil then + table.insert(closing_pairs_stack, CLOSING_PAIR[char]) + end + end + + local has_alphanumeric = false + for i = 1, #text do + local char = string.byte(text, i) + if PAIRS_AND_INVALID_CHARS[char] == nil then + add(char) + has_alphanumeric = has_alphanumeric or ALPHANUMERIC[char] + elseif not has_alphanumeric or #closing_pairs_stack ~= 0 then + add(char) + -- if we had an alphanumeric, and the closing pairs stack *just* emptied, + -- because the current character is a closing pair, we exit + if has_alphanumeric and #closing_pairs_stack == 0 then break end + else + break + end + end + return word +end + +return get_prefix_before_brackets_and_quotes diff --git a/lua/blink/cmp/accept/preview.lua b/lua/blink/cmp/completion/accept/preview.lua similarity index 66% rename from lua/blink/cmp/accept/preview.lua rename to lua/blink/cmp/completion/accept/preview.lua index 26bf15fc..73edb29a 100644 --- a/lua/blink/cmp/accept/preview.lua +++ b/lua/blink/cmp/completion/accept/preview.lua @@ -1,13 +1,13 @@ --- @param item blink.cmp.CompletionItem local function preview(item) - local text_edits_lib = require('blink.cmp.accept.text-edits') + local text_edits_lib = require('blink.cmp.lib.text_edits') local text_edit = text_edits_lib.get_from_item(item) if item.insertTextFormat == vim.lsp.protocol.InsertTextFormat.Snippet then local expanded_snippet = require('blink.cmp.sources.snippets.utils').safe_parse(text_edit.newText) - text_edit.newText = require('blink.cmp.utils').get_prefix_before_brackets_and_quotes( - expanded_snippet and tostring(expanded_snippet) or text_edit.newText - ) + local snippet = expanded_snippet and tostring(expanded_snippet) or text_edit.newText + local get_prefix_before_brackets_and_quotes = require('blink.cmp.completion.accept.prefix') + text_edit.newText = get_prefix_before_brackets_and_quotes(snippet) end local undo_text_edit = text_edits_lib.get_undo_text_edit(text_edit) diff --git a/lua/blink/cmp/accept/brackets/config.lua b/lua/blink/cmp/completion/brackets/config.lua similarity index 100% rename from lua/blink/cmp/accept/brackets/config.lua rename to lua/blink/cmp/completion/brackets/config.lua diff --git a/lua/blink/cmp/completion/brackets/init.lua b/lua/blink/cmp/completion/brackets/init.lua new file mode 100644 index 00000000..511b42b1 --- /dev/null +++ b/lua/blink/cmp/completion/brackets/init.lua @@ -0,0 +1,6 @@ +local brackets = {} + +brackets.add_brackets = require('blink.cmp.completion.brackets.kind') +brackets.add_brackets_via_semantic_token = require('blink.cmp.completion.brackets.semantic') + +return brackets diff --git a/lua/blink/cmp/accept/brackets/kind.lua b/lua/blink/cmp/completion/brackets/kind.lua similarity index 94% rename from lua/blink/cmp/accept/brackets/kind.lua rename to lua/blink/cmp/completion/brackets/kind.lua index 68bc4530..29bb5b6d 100644 --- a/lua/blink/cmp/accept/brackets/kind.lua +++ b/lua/blink/cmp/completion/brackets/kind.lua @@ -1,5 +1,4 @@ -local config = require('blink.cmp.config').accept.auto_brackets -local utils = require('blink.cmp.accept.brackets.utils') +local utils = require('blink.cmp.completion.brackets.utils') --- @param filetype string --- @param item blink.cmp.CompletionItem diff --git a/lua/blink/cmp/accept/brackets/semantic.lua b/lua/blink/cmp/completion/brackets/semantic.lua similarity index 96% rename from lua/blink/cmp/accept/brackets/semantic.lua rename to lua/blink/cmp/completion/brackets/semantic.lua index 7d9ff7b8..c64afbd8 100644 --- a/lua/blink/cmp/accept/brackets/semantic.lua +++ b/lua/blink/cmp/completion/brackets/semantic.lua @@ -1,5 +1,5 @@ -local config = require('blink.cmp.config').accept.auto_brackets -local utils = require('blink.cmp.accept.brackets.utils') +local config = require('blink.cmp.config').completion.accept.auto_brackets +local utils = require('blink.cmp.completion.brackets.utils') local semantic = {} diff --git a/lua/blink/cmp/accept/brackets/utils.lua b/lua/blink/cmp/completion/brackets/utils.lua similarity index 92% rename from lua/blink/cmp/accept/brackets/utils.lua rename to lua/blink/cmp/completion/brackets/utils.lua index 0789591b..03646af3 100644 --- a/lua/blink/cmp/accept/brackets/utils.lua +++ b/lua/blink/cmp/completion/brackets/utils.lua @@ -1,5 +1,5 @@ -local config = require('blink.cmp.config').accept.auto_brackets -local brackets = require('blink.cmp.accept.brackets.config') +local config = require('blink.cmp.config').completion.accept.auto_brackets +local brackets = require('blink.cmp.completion.brackets.config') local utils = {} --- @param snippet string diff --git a/lua/blink/cmp/completion/init.lua b/lua/blink/cmp/completion/init.lua new file mode 100644 index 00000000..7983a235 --- /dev/null +++ b/lua/blink/cmp/completion/init.lua @@ -0,0 +1,67 @@ +local config = require('blink.cmp.config') +local completion = {} + +function completion.setup() + -- trigger controls when to show the window and the current context for caching + local trigger = require('blink.cmp.completion.trigger') + trigger.activate() + + -- sources fetch completion items and documentation + local sources = require('blink.cmp.sources.lib') + + -- manages the completion list state: + -- fuzzy matching items + -- when to show/hide the windows + -- selection + -- accepting and previewing items + local list = require('blink.cmp.completion.list') + + -- trigger -> sources: request completion items from the sources on show + trigger.show_emitter:on(function(event) sources.request_completions(event.context) end) + trigger.hide_emitter:on(function() + sources.cancel_completions() + list.hide() + end) + + -- sources -> list + sources.completions_emitter:on(function(event) + -- schedule for later to avoid adding 0.5-4ms to insertion latency + vim.schedule(function() + -- 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 + list.show(event.context, event.items) + end) + end) + + --- list -> windows: ghost text and completion menu + -- setup completion menu + if config.completion.menu.enabled then + list.show_emitter:on( + function(event) require('blink.cmp.completion.windows.menu').open_with_items(event.context, event.items) end + ) + list.hide_emitter:on(function() require('blink.cmp.completion.windows.menu').close() end) + list.select_emitter:on(function(event) + require('blink.cmp.completion.windows.menu').set_selected_item_idx(event.idx) + if config.completion.documentation.auto_show then + require('blink.cmp.completion.windows.documentation').auto_show_item(event.context, event.item) + end + end) + end + + -- setup ghost text + if config.completion.ghost_text.enabled then + list.select_emitter:on( + function(event) require('blink.cmp.completion.windows.ghost_text').show_preview(event.item) end + ) + list.hide_emitter:on(function() require('blink.cmp.completion.windows.ghost_text').clear_preview() end) + end + + -- run 'resolve' on the item ahead of time to avoid delays + -- when accepting the item or showing documentation + list.select_emitter:on(function(event) + if event.item == nil then return end + require('blink.cmp.completion.prefetch')(event.context, event.item) + end) +end + +return completion diff --git a/lua/blink/cmp/completion/list.lua b/lua/blink/cmp/completion/list.lua new file mode 100644 index 00000000..bcdda8ca --- /dev/null +++ b/lua/blink/cmp/completion/list.lua @@ -0,0 +1,177 @@ +--- Manages most of the state for the completion list such that downstream consumers can be mostly stateless +--- @class (exact) blink.cmp.CompletionList +--- @field config blink.cmp.CompletionListConfig +--- @field context? blink.cmp.Context +--- @field items blink.cmp.CompletionItem[] +--- @field selected_item_idx? number +--- @field preview_undo_text_edit? lsp.TextEdit +--- @field show_emitter blink.cmp.EventEmitter +--- @field hide_emitter blink.cmp.EventEmitter +--- @field select_emitter blink.cmp.EventEmitter +--- @field accept_emitter blink.cmp.EventEmitter +--- +--- @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 hide fun() +--- +--- @field get_selected_item fun(): blink.cmp.CompletionItem? +--- @field select fun(idx?: number) +--- @field select_next fun() +--- @field select_prev fun() +--- +--- @field undo_preview fun() +--- @field apply_preview fun(item: blink.cmp.CompletionItem) +--- @field accept fun(): boolean Applies the currently selected item, returning true if it succeeded + +--- @class blink.cmp.CompletionListShowEvent +--- @field items blink.cmp.CompletionItem[] +--- @field context blink.cmp.Context + +--- @class blink.cmp.CompletionListHideEvent +--- @field context blink.cmp.Context + +--- @class blink.cmp.CompletionListSelectEvent +--- @field idx? number +--- @field item? blink.cmp.CompletionItem +--- @field items blink.cmp.CompletionItem[] +--- @field context blink.cmp.Context + +--- @class blink.cmp.CompletionListAcceptEvent +--- @field item blink.cmp.CompletionItem +--- @field context blink.cmp.Context + +--- @type blink.cmp.CompletionList +--- @diagnostic disable-next-line: missing-fields +local list = { + select_emitter = require('blink.cmp.lib.event_emitter').new('select', 'BlinkCmpListSelect'), + accept_emitter = require('blink.cmp.lib.event_emitter').new('accept', 'BlinkCmpAccept'), + show_emitter = require('blink.cmp.lib.event_emitter').new('show', 'BlinkCmpShow'), + hide_emitter = require('blink.cmp.lib.event_emitter').new('hide', 'BlinkCmpHide'), + config = require('blink.cmp.config').completion.list, + context = nil, + items = {}, + selected_item_idx = nil, + preview_undo_text_edit = nil, +} + +---------- State ---------- + +function list.show(context, items) + -- 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 + + list.context = context + list.items = list.fuzzy(context, items or list.items) + + 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) +end + +function list.fuzzy(context, items) + local fuzzy = require('blink.cmp.fuzzy') + local sources = require('blink.cmp.sources.lib') + + local filtered_items = fuzzy.fuzzy(fuzzy.get_query(), items) + return sources.apply_max_items_for_completions(context, filtered_items) +end + +function list.hide() list.hide_emitter:emit({ context = list.context }) end + +---------- Selection ---------- + +function list.get_selected_item() return list.items[list.selected_item_idx] end + +function list.select(idx) + local item = list.items[idx] + + list.undo_preview() + if list.config.selection == 'auto_insert' and item then list.apply_preview(item) end + + list.selected_item_idx = idx + list.select_emitter:emit({ idx = idx, item = item, items = list.items, context = list.context }) +end + +function list.select_next() + if #list.items == 0 then return end + + -- haven't selected anything yet, select the first item + if list.selected_item_idx == nil then return list.select(1) end + + -- end of the list + if list.selected_item_idx == #list.items then + -- cycling around has been disabled, ignore + if not list.config.cycle.from_bottom then return end + + -- preselect is not enabled, we go back to no selection + if list.config.selection ~= 'preselect' then return list.select(nil) end + + -- otherwise, we cycle around + list.select(1) + end + + -- typical case, select the next item + list.select(list.selected_item_idx + 1) +end + +function list.select_prev() + if #list.items == 0 then return end + + -- haven't selected anything yet, select the last item + if list.selected_item_idx == nil then return list.select(#list.items) end + + -- start of the list + if list.selected_item_idx == 1 then + -- cycling around has been disabled, ignore + if not list.config.cycle.from_top then return end + + -- auto_insert is enabled, we go back to no selection + if list.config.selection == 'auto_insert' then return list.select(nil) end + + -- otherwise, we cycle around + list.select(#list.items) + end + + -- typical case, select the previous item + list.select(list.selected_item_idx - 1) +end + +---------- Preview ---------- + +function list.undo_preview() + if list.preview_undo_text_edit == nil then return end + + require('blink.cmp.lib.text_edits').apply({ list.preview_undo_text_edit }) + list.preview_undo_text_edit = nil +end + +function list.apply_preview(item) + require('blink.cmp.completion.trigger').suppress_events_for_callback(function() + -- undo the previous preview if it exists + if list.preview_undo_text_edit ~= nil then + require('blink.cmp.lib.text_edits').apply({ list.preview_undo_text_edit }) + end + -- apply the new preview + list.preview_undo_text_edit = require('blink.cmp.completion.preview')(item) + end) +end + +---------- Accept ---------- + +function list.accept() + local item = list.get_selected_item() + if item == nil then return false end + + list.undo_preview() + local accept = require('blink.cmp.completion.accept') + accept(list.context, item, function() list.accept_emitter:emit({ item = item, context = list.context }) end) + return true +end + +return list diff --git a/lua/blink/cmp/completion/prefetch.lua b/lua/blink/cmp/completion/prefetch.lua new file mode 100644 index 00000000..2e07c7a1 --- /dev/null +++ b/lua/blink/cmp/completion/prefetch.lua @@ -0,0 +1,29 @@ +-- Run `resolve` on the item ahead of time to avoid delays +-- when accepting the item or showing documentation + +local last_context_id = nil +local last_request = nil +local timer = vim.uv.new_timer() + +--- @param context blink.cmp.Context +--- @param item blink.cmp.CompletionItem +local function prefetch_resolve(context, item) + if not item then return end + + local resolve = vim.schedule_wrap(function() + if last_request ~= nil then last_request:cancel() end + last_request = require('blink.cmp.sources.lib').resolve(item) + end) + + -- immediately resolve if the context has changed + if last_context_id ~= context.id then + last_context_id = context.id + resolve() + end + + -- otherwise, wait for the debounce period + timer:stop() + timer:start(50, 0, resolve) +end + +return prefetch_resolve diff --git a/lua/blink/cmp/trigger/completion.lua b/lua/blink/cmp/completion/trigger.lua similarity index 54% rename from lua/blink/cmp/trigger/completion.lua rename to lua/blink/cmp/completion/trigger.lua index 8c1a9d96..0d69f9f4 100644 --- a/lua/blink/cmp/trigger/completion.lua +++ b/lua/blink/cmp/completion/trigger.lua @@ -2,143 +2,124 @@ -- (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. -local config = require('blink.cmp.config').trigger.completion -local sources = require('blink.cmp.sources.lib') -local utils = require('blink.cmp.utils') - +--- @class blink.cmp.ContextBounds +--- @field line string +--- @field line_number number +--- @field start_col number +--- @field end_col number +--- @field length number + +--- @class blink.cmp.Context +--- @field id number +--- @field bufnr number +--- @field cursor number[] +--- @field line string +--- @field bounds blink.cmp.ContextBounds +--- @field trigger { kind: number, character: string | nil } + +--- @class blink.cmp.CompletionTrigger +--- @field buffer_events blink.cmp.BufferEvents +--- @field current_context_id number +--- @field context? blink.cmp.Context +--- @field show_emitter blink.cmp.EventEmitter +--- @field hide_emitter blink.cmp.EventEmitter<{}> +--- +--- @field activate fun() +--- @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 }): boolean +--- @field show fun(opts?: { trigger_character?: string }) +--- @field hide fun() +--- @field within_query_bounds fun(cursor: number[]): boolean +--- @field get_context_bounds fun(regex: string): blink.cmp.ContextBounds + +local keyword_config = require('blink.cmp.config').completion.keyword +local config = require('blink.cmp.config').completion.trigger + +--- @type blink.cmp.CompletionTrigger +--- @diagnostic disable-next-line: missing-fields local trigger = { current_context_id = -1, - --- @type blink.cmp.Context | nil - context = nil, - event_targets = { - --- @type fun(context: blink.cmp.Context) - on_show = function() end, - --- @type fun() - on_hide = function() end, - }, + show_emitter = require('blink.cmp.lib.event_emitter').new('show'), + hide_emitter = require('blink.cmp.lib.event_emitter').new('hide'), } ---- TODO: sweet mother of mary, massive refactor needed -function trigger.activate_autocmds() - local last_char = '' - vim.api.nvim_create_autocmd('InsertCharPre', { - callback = function() - if vim.snippet.active() and not config.show_in_snippet and not trigger.context then return end - last_char = vim.v.char - end, +function trigger.activate() + trigger.buffer_events = require('blink.cmp.lib.buffer_events').new({ + has_context = function() return trigger.context ~= nil end, + show_in_snippet = config.show_in_snippet, }) - - -- decide if we should show the completion window - vim.api.nvim_create_autocmd('TextChangedI', { - callback = function() - if vim.snippet.active() and not config.show_in_snippet and not trigger.context then - return - + trigger.buffer_events:listen({ + on_char_added = function(char, is_ignored) -- we were told to ignore the text changed event, so we update the context -- but don't send an on_show event upstream - elseif trigger.ignore_next_text_changed then + if is_ignored then if trigger.context ~= nil then trigger.show({ send_upstream = false }) end - trigger.ignore_next_text_changed = false - - -- no characters added so let cursormoved handle it - elseif last_char == '' then - return - - -- ignore if in a special buffer - elseif utils.is_blocked_buffer() then - trigger.hide() -- character forces a trigger according to the sources, create a fresh context - elseif vim.tbl_contains(sources.get_trigger_characters(), last_char) then + elseif trigger.is_trigger_character(char) then trigger.context = nil - trigger.show({ trigger_character = last_char }) + trigger.show({ trigger_character = char }) -- character is part of the current context OR in an existing context - elseif last_char:match(config.keyword_regex) ~= nil then + elseif char:match(keyword_config.regex) ~= nil then trigger.show() -- nothing matches so hide else trigger.hide() end - - last_char = '' end, - }) - - vim.api.nvim_create_autocmd({ 'CursorMovedI', 'InsertEnter' }, { - callback = function(ev) - if utils.is_blocked_buffer() then return end - if vim.snippet.active() and not config.show_in_snippet and not trigger.context then return end - + on_cursor_moved = function(event, is_ignored) -- we were told to ignore the cursor moved event, so we update the context -- but don't send an on_show event upstream - if trigger.ignore_next_cursor_moved and ev.event == 'CursorMovedI' then + if is_ignored and event == 'CursorMovedI' then if trigger.context ~= nil then trigger.show({ send_upstream = false }) end - trigger.ignore_next_cursor_moved = false return end - -- characters added so let textchanged handle it - if last_char ~= '' then return end - local cursor_col = vim.api.nvim_win_get_cursor(0)[2] local char_under_cursor = vim.api.nvim_get_current_line():sub(cursor_col, cursor_col) - local is_on_trigger = vim.tbl_contains(sources.get_trigger_characters(), char_under_cursor) - local is_on_trigger_for_show_on_insert = is_on_trigger - and not vim.tbl_contains(config.show_on_x_blocked_trigger_characters, char_under_cursor) - local is_on_context_char = char_under_cursor:match(config.keyword_regex) ~= nil + local is_on_trigger_for_show = trigger.is_trigger_character(char_under_cursor) + local is_on_trigger_for_show_on_insert = trigger.is_trigger_character(char_under_cursor, true) + local is_on_context_char = char_under_cursor:match(keyword_config.regex) ~= nil local insert_enter_on_trigger_character = config.show_on_insert_on_trigger_character and is_on_trigger_for_show_on_insert - and ev.event == 'InsertEnter' + and event == 'InsertEnter' -- check if we're still within the bounds of the query used for the context if trigger.within_query_bounds(vim.api.nvim_win_get_cursor(0)) then trigger.show() - -- check if we've entered insert mode on a trigger character - -- or if we've moved onto a trigger character - elseif insert_enter_on_trigger_character or (is_on_trigger and trigger.context ~= nil) then + -- check if we've entered insert mode on a trigger character + -- or if we've moved onto a trigger character (by accepting for example) + elseif insert_enter_on_trigger_character or (is_on_trigger_for_show and trigger.context ~= nil) then trigger.context = nil trigger.show({ trigger_character = char_under_cursor }) - -- show if we currently have a context, and we've moved outside of it's bounds by 1 char + -- show if we currently have a context, and we've moved outside of it's bounds by 1 char elseif is_on_context_char and trigger.context ~= nil and cursor_col == trigger.context.bounds.start_col - 1 then trigger.context = nil trigger.show() - -- otherwise hide + -- otherwise hide else trigger.hide() end end, + on_insert_leave = function() trigger.hide() end, }) +end - -- definitely leaving the context - -- TODO: handle leaving snippet mode - vim.api.nvim_create_autocmd({ 'InsertLeave', 'BufLeave' }, { - callback = function() - last_char = '' - trigger.hide() - end, - }) +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) - -- manually hide when exiting insert mode with ctrl+c, since it doesn't trigger InsertLeave - local ctrl_c = vim.api.nvim_replace_termcodes('', true, true, true) - vim.on_key(function(key) - if key == ctrl_c then - vim.schedule(function() - local mode = vim.api.nvim_get_mode().mode - if mode ~= 'i' then - last_char = '' - trigger.hide() - end - end) - end - end) + 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)) - return trigger + return is_trigger and not is_blocked end --- Suppresses on_hide and on_show events for the duration of the callback @@ -146,29 +127,17 @@ end --- HACK: there's likely edge cases with this since we can't know for sure --- if the autocmds will fire for cursor_moved afaik function trigger.suppress_events_for_callback(cb) - local cursor_before = vim.api.nvim_win_get_cursor(0) - local changed_tick_before = vim.api.nvim_buf_get_changedtick(0) - - cb() - - local cursor_after = vim.api.nvim_win_get_cursor(0) - local changed_tick_after = vim.api.nvim_buf_get_changedtick(0) - - local is_insert_mode = vim.api.nvim_get_mode().mode == 'i' - trigger.ignore_next_text_changed = changed_tick_after ~= changed_tick_before and is_insert_mode - -- TODO: does this guarantee that the CursorMovedI event will fire? - trigger.ignore_next_cursor_moved = (cursor_after[1] ~= cursor_before[1] or cursor_after[2] ~= cursor_before[2]) - and is_insert_mode + if not trigger.buffer_events then return cb() end + trigger.buffer_events:suppress_events_for_callback(cb) end --- @param opts { is_accept?: boolean } | nil function trigger.show_if_on_trigger_character(opts) - if opts and opts.is_accept and not config.show_on_accept_on_trigger_character then return end + if opts and opts.is_accept and not config.show_on_accept_on_trigger_character then return false end local cursor_col = vim.api.nvim_win_get_cursor(0)[2] local char_under_cursor = vim.api.nvim_get_current_line():sub(cursor_col, cursor_col) - local is_on_trigger = vim.tbl_contains(sources.get_trigger_characters(), char_under_cursor) - and not vim.tbl_contains(config.show_on_x_blocked_trigger_characters, char_under_cursor) + local is_on_trigger = trigger.is_trigger_character(char_under_cursor, true) if is_on_trigger then trigger.show({ trigger_character = char_under_cursor }) end return is_on_trigger @@ -196,7 +165,7 @@ function trigger.show(opts) bufnr = vim.api.nvim_get_current_buf(), cursor = cursor, line = vim.api.nvim_buf_get_lines(0, cursor[1] - 1, cursor[1], false)[1], - bounds = trigger.get_context_bounds(config.keyword_regex), + bounds = trigger.get_context_bounds(keyword_config.regex), trigger = { kind = opts.trigger_character and vim.lsp.protocol.CompletionTriggerKind.TriggerCharacter or vim.lsp.protocol.CompletionTriggerKind.Invoked, @@ -204,21 +173,15 @@ function trigger.show(opts) }, } - if opts.send_upstream ~= false then trigger.event_targets.on_show(trigger.context) end + if opts.send_upstream ~= false then trigger.show_emitter:emit({ context = trigger.context }) end end ---- @param callback fun(context: blink.cmp.Context) -function trigger.listen_on_show(callback) trigger.event_targets.on_show = callback end - function trigger.hide() if not trigger.context then return end trigger.context = nil - trigger.event_targets.on_hide() + trigger.hide_emitter:emit() end ---- @param callback fun() -function trigger.listen_on_hide(callback) trigger.event_targets.on_hide = callback end - --- @param cursor number[] --- @return boolean function trigger.within_query_bounds(cursor) diff --git a/lua/blink/cmp/completion/windows/documentation.lua b/lua/blink/cmp/completion/windows/documentation.lua new file mode 100644 index 00000000..4c5546ca --- /dev/null +++ b/lua/blink/cmp/completion/windows/documentation.lua @@ -0,0 +1,202 @@ +--- @class blink.cmp.CompletionDocumentationWindow +--- @field win blink.cmp.Window +--- @field last_context_id? number +--- @field auto_show_timer uv_timer_t +--- @field shown_item? blink.cmp.CompletionItem +--- +--- @field auto_show_item fun(context: blink.cmp.Context, item: blink.cmp.CompletionItem) +--- @field show_item fun(item: blink.cmp.CompletionItem) +--- @field update_position fun() +--- @field scroll_up fun(amount: number) +--- @field scroll_down fun(amount: number) +--- @field close fun() + +local config = require('blink.cmp.config').completion.documentation +local win_config = config.window + +local sources = require('blink.cmp.sources.lib') +local menu = require('blink.cmp.completion.windows.menu') + +--- @type blink.cmp.CompletionDocumentationWindow +--- @diagnostic disable-next-line: missing-fields +local docs = { + win = require('blink.cmp.lib.window').new({ + min_width = win_config.min_width, + max_width = win_config.max_width, + max_height = win_config.max_height, + border = win_config.border, + winblend = win_config.winblend, + winhighlight = win_config.winhighlight, + scrollbar = win_config.scrollbar, + wrap = true, + }), + last_context_id = nil, + auto_show_timer = vim.uv.new_timer(), +} + +menu.position_update_emitter:on(docs.update_position) +menu.close_emitter:on(function() docs.win:close() end) + +function docs.auto_show_item(context, item) + docs.auto_show_timer:stop() + if docs.win:is_open() or context.id == docs.last_context_id then + docs.last_context_id = context.id + docs.auto_show_timer:start(config.update_delay_ms, 0, function() + vim.schedule(function() docs.show_item(item) end) + end) + elseif config.auto_show then + docs.auto_show_timer:start(config.auto_show_delay_ms, 0, function() + docs.last_context_id = context.id + vim.schedule(function() docs.show_item(item) end) + end) + end +end + +function docs.show_item(item) + docs.auto_show_timer:stop() + if item == nil or not menu.win:is_open() then return docs.win:close() end + + -- TODO: cancellation + -- TODO: only resolve if documentation does not exist + sources + .resolve(item) + :map(function(item) + if item.documentation == nil and item.detail == nil then + docs.win:close() + return + end + + if docs.shown_item ~= item then + require('blink.cmp.lib.window.docs').render_detail_and_documentation( + docs.win:get_buf(), + item.detail, + item.documentation, + docs.win.config.max_width, + config.treesitter_highlighting + ) + end + docs.shown_item = item + + if menu.win:get_win() then + docs.win:open() + vim.api.nvim_win_set_cursor(docs.win:get_win(), { 1, 0 }) -- reset scroll + docs.update_position() + end + end) + :catch(function(err) vim.notify(err, vim.log.levels.ERROR) end) +end + +function docs.scroll_up(amount) + local winnr = docs.win:get_win() + local top_line = math.max(1, vim.fn.line('w0', winnr) - 1) + local desired_line = math.max(1, top_line - amount) + + vim.api.nvim_win_set_cursor(docs.win:get_win(), { desired_line, 0 }) +end + +function docs.scroll_down(amount) + local winnr = docs.win:get_win() + local line_count = vim.api.nvim_buf_line_count(docs.win:get_buf()) + local bottom_line = math.max(1, vim.fn.line('w$', winnr) + 1) + local desired_line = math.min(line_count, bottom_line + amount) + + vim.api.nvim_win_set_cursor(docs.win:get_win(), { desired_line, 0 }) +end + +function docs.update_position() + if not docs.win:is_open() or not menu.win:is_open() then return end + local winnr = docs.win:get_win() + + docs.win:update_size() + + local menu_winnr = menu.win:get_win() + if not menu_winnr then return end + local menu_win_config = vim.api.nvim_win_get_config(menu_winnr) + local menu_win_height = menu.win:get_height() + local menu_border_size = menu.win:get_border_size() + + local cursor_win_row = vim.fn.winline() + + -- decide direction priority based on the menu window's position + local menu_win_is_up = menu_win_config.row - cursor_win_row < 0 + local direction_priority = menu_win_is_up and win_config.direction_priority.menu_north + or win_config.direction_priority.menu_south + + -- remove the direction priority of the signature window if it's open + local signature = require('blink.cmp.signature.window') + if signature.win and signature.win:is_open() then + direction_priority = vim.tbl_filter( + function(dir) return dir ~= (menu_win_is_up and 's' or 'n') end, + direction_priority + ) + end + + -- decide direction, width and height of window + local win_width = docs.win:get_width() + local win_height = docs.win:get_height() + local pos = docs.win:get_direction_with_window_constraints(menu.win, direction_priority, { + width = math.min(win_width, win_config.desired_min_width), + height = math.min(win_height, win_config.desired_min_height), + }) + + -- couldn't find anywhere to place the window + if not pos then + docs.win:close() + return + end + + -- set width and height based on available space + vim.api.nvim_win_set_height(docs.win:get_win(), pos.height) + vim.api.nvim_win_set_width(docs.win:get_win(), pos.width) + + -- set position based on provided direction + + local height = docs.win:get_height() + local width = docs.win:get_width() + + local function set_config(opts) + vim.api.nvim_win_set_config(winnr, { relative = 'win', win = menu_winnr, row = opts.row, col = opts.col }) + end + if pos.direction == 'n' then + if menu_win_is_up then + set_config({ row = -height - menu_border_size.top, col = -menu_border_size.left }) + else + set_config({ row = -1 - height - menu_border_size.top, col = -menu_border_size.left }) + end + elseif pos.direction == 's' then + if menu_win_is_up then + set_config({ + row = 1 + menu_win_height - menu_border_size.top, + col = -menu_border_size.left, + }) + else + set_config({ + row = menu_win_height - menu_border_size.top, + col = -menu_border_size.left, + }) + end + elseif pos.direction == 'e' then + if menu_win_is_up and menu_win_height < height then + set_config({ + row = menu_win_height - menu_border_size.top - height, + col = menu_win_config.width + menu_border_size.right, + }) + else + set_config({ + row = -menu_border_size.top, + col = menu_win_config.width + menu_border_size.right, + }) + end + elseif pos.direction == 'w' then + if menu_win_is_up and menu_win_height < height then + set_config({ + row = menu_win_height - menu_border_size.top - height, + col = -width - menu_border_size.left, + }) + else + set_config({ row = -menu_border_size.top, col = -width - menu_border_size.left }) + end + end +end + +return docs diff --git a/lua/blink/cmp/windows/ghost-text.lua b/lua/blink/cmp/completion/windows/ghost_text.lua similarity index 67% rename from lua/blink/cmp/windows/ghost-text.lua rename to lua/blink/cmp/completion/windows/ghost_text.lua index c217b9fa..5e5ee7b8 100644 --- a/lua/blink/cmp/windows/ghost-text.lua +++ b/lua/blink/cmp/completion/windows/ghost_text.lua @@ -1,17 +1,23 @@ -local config = require('blink.cmp.config') -local autocomplete = require('blink.cmp.windows.autocomplete') -local text_edits_lib = require('blink.cmp.accept.text-edits') +local config = require('blink.cmp.config').completion.ghost_text +local highlight_ns = require('blink.cmp.config').appearance.highlight_ns +local text_edits_lib = require('blink.cmp.lib.text_edits') local snippets_utils = require('blink.cmp.sources.snippets.utils') -local ghost_text_config = config.windows.ghost_text - ---- @class blink.cmp.windows.ghost_text +--- @class blink.cmp.windows.GhostText --- @field win integer? --- @field selected_item blink.cmp.CompletionItem? --- @field extmark_id integer? +--- +--- @field show_preview fun(item: blink.cmp.CompletionItem) +--- @field clear_preview fun() +--- @field draw_preview fun(bufnr: number) + +--- @type blink.cmp.windows.GhostText +--- @diagnostic disable-next-line: missing-fields local ghost_text = { win = nil, selected_item = nil, + extmark_id = nil, } --- @param textEdit lsp.TextEdit @@ -20,22 +26,12 @@ local function get_still_untyped_text(textEdit) return textEdit.newText:sub(type_text_length + 1) end -function ghost_text.setup() - -- immediately re-draw the preview when the cursor moves/text changes - vim.api.nvim_create_autocmd({ 'CursorMovedI', 'TextChangedI' }, { - callback = function() - if not ghost_text_config.enabled or ghost_text.win == nil then return end - ghost_text.draw_preview(vim.api.nvim_win_get_buf(ghost_text.win)) - end, - }) - - autocomplete.listen_on_select(function(item) - if ghost_text_config.enabled then ghost_text.show_preview(item) end - end) - autocomplete.listen_on_close(function() ghost_text.clear_preview() end) - - return ghost_text -end +-- immediately re-draw the preview when the cursor moves/text changes +vim.api.nvim_create_autocmd({ 'CursorMovedI', 'TextChangedI' }, { + callback = function() + if config.enabled and ghost_text.win then ghost_text.draw_preview(vim.api.nvim_win_get_buf(ghost_text.win)) end + end, +}) --- @param selected_item? blink.cmp.CompletionItem function ghost_text.show_preview(selected_item) @@ -56,7 +52,7 @@ function ghost_text.clear_preview() ghost_text.selected_item = nil ghost_text.win = nil if ghost_text.extmark_id ~= nil then - vim.api.nvim_buf_del_extmark(0, config.highlight.ns, ghost_text.extmark_id) + vim.api.nvim_buf_del_extmark(0, highlight_ns, ghost_text.extmark_id) ghost_text.extmark_id = nil end end @@ -85,7 +81,7 @@ function ghost_text.draw_preview(bufnr) text_edit.range['end'].character, } - ghost_text.extmark_id = vim.api.nvim_buf_set_extmark(bufnr, config.highlight.ns, cursor_pos[1], cursor_pos[2], { + ghost_text.extmark_id = vim.api.nvim_buf_set_extmark(bufnr, highlight_ns, cursor_pos[1], cursor_pos[2], { id = ghost_text.extmark_id, virt_text_pos = 'inline', virt_text = { { display_lines[1], 'BlinkCmpGhostText' } }, diff --git a/lua/blink/cmp/completion/windows/menu.lua b/lua/blink/cmp/completion/windows/menu.lua new file mode 100644 index 00000000..32af8d35 --- /dev/null +++ b/lua/blink/cmp/completion/windows/menu.lua @@ -0,0 +1,120 @@ +--- @class blink.cmp.CompletionMenu +--- @field win blink.cmp.Window +--- @field items blink.cmp.CompletionItem[] +--- @field renderer blink.cmp.Renderer +--- @field selected_item_idx? number +--- @field context blink.cmp.Context? +--- @field open_emitter blink.cmp.EventEmitter<{}> +--- @field close_emitter blink.cmp.EventEmitter<{}> +--- @field position_update_emitter blink.cmp.EventEmitter<{}> +--- +--- @field open_with_items fun(context: blink.cmp.Context, items: blink.cmp.CompletionItem[]) +--- @field open fun() +--- @field close fun() +--- @field set_selected_item_idx fun(idx?: number) +--- @field update_position fun() + +local config = require('blink.cmp.config').completion.menu + +--- @type blink.cmp.CompletionMenu +--- @diagnostic disable-next-line: missing-fields +local menu = { + win = require('blink.cmp.lib.window').new({ + min_width = config.min_width, + max_height = config.max_height, + border = config.border, + winblend = config.winblend, + winhighlight = config.winhighlight, + cursorline = false, + scrolloff = config.scrolloff, + scrollbar = config.scrollbar, + }), + items = {}, + context = nil, + open_emitter = require('blink.cmp.lib.event_emitter').new('completion_menu_open', 'BlinkCmpCompletionMenuOpen'), + close_emitter = require('blink.cmp.lib.event_emitter').new('completion_menu_close', 'BlinkCmpCompletionMenuClose'), + position_update_emitter = require('blink.cmp.lib.event_emitter').new( + 'completion_menu_position_update', + 'BlinkCmpCompletionMenuPositionUpdate' + ), +} + +vim.api.nvim_create_autocmd({ 'CursorMovedI', 'WinScrolled', 'WinResized' }, { + callback = function() menu.update_position() end, +}) + +--- @param context blink.cmp.Context +--- @param items blink.cmp.CompletionItem[] +function menu.open_with_items(context, items) + menu.context = context + menu.items = items + + if not menu.renderer then menu.renderer = require('blink.cmp.completion.windows.render').new(config.draw) end + menu.renderer:draw(menu.win:get_buf(), items) + + menu.open() + menu.update_position() + + -- it's possible for the window to close after updating the position + -- if there was nowhere to place the window + if not menu.win:is_open() then return end +end + +function menu.open() + if menu.win:is_open() then return end + + menu.win:open() + if menu.selected_item_idx ~= nil then + vim.api.nvim_win_set_cursor(menu.win:get_win(), { menu.selected_item_idx, 0 }) + end + + menu.open_emitter:emit() +end + +function menu.close() + if not menu.win:is_open() then return end + + menu.win:close() + menu.close_emitter:emit() +end + +function menu.set_selected_item_idx(idx) + menu.win:set_option_value('cursorline', idx ~= nil) + menu.selected_item_idx = idx + if menu.win:is_open() then vim.api.nvim_win_set_cursor(menu.win:get_win(), { idx or 1, 0 }) end +end + +--- TODO: Don't switch directions if the context is the same +function menu.update_position() + local context = menu.context + if context == nil then return end + + local win = menu.win + if not win:is_open() then return end + local winnr = win:get_win() + + win:update_size() + + local border_size = win:get_border_size() + local pos = win:get_vertical_direction_and_height(config.direction_priority) + + -- couldn't find anywhere to place the window + if not pos then + win:close() + return + end + + local start_col = menu.renderer:get_alignment_start_col() + + -- place the window at the start col of the current text we're fuzzy matching against + -- so the window doesnt move around as we type + local cursor_col = vim.api.nvim_win_get_cursor(0)[2] + local col = context.bounds.start_col - cursor_col - (context.bounds.length == 0 and 0 or 1) - border_size.left + local row = pos.direction == 's' and 1 or -pos.height - border_size.vertical + vim.api.nvim_win_set_config(winnr, { relative = 'cursor', row = row, col = col - start_col }) + vim.api.nvim_win_set_height(winnr, pos.height) + + menu.position_update_emitter:emit() +end + +return menu diff --git a/lua/blink/cmp/windows/render/column.lua b/lua/blink/cmp/completion/windows/render/column.lua similarity index 98% rename from lua/blink/cmp/windows/render/column.lua rename to lua/blink/cmp/completion/windows/render/column.lua index bf2f6e0d..b9a75d06 100644 --- a/lua/blink/cmp/windows/render/column.lua +++ b/lua/blink/cmp/completion/windows/render/column.lua @@ -10,7 +10,7 @@ --- @field get_line_text fun(self: blink.cmp.DrawColumn, line_idx: number): string --- @field get_line_highlights fun(self: blink.cmp.DrawColumn, line_idx: number): blink.cmp.DrawHighlight[] -local text_lib = require('blink.cmp.windows.render.text') +local text_lib = require('blink.cmp.completion.windows.render.text') --- @type blink.cmp.DrawColumn --- @diagnostic disable-next-line: missing-fields diff --git a/lua/blink/cmp/windows/render/context.lua b/lua/blink/cmp/completion/windows/render/context.lua similarity index 94% rename from lua/blink/cmp/windows/render/context.lua rename to lua/blink/cmp/completion/windows/render/context.lua index f0e2a24f..225b2e9a 100644 --- a/lua/blink/cmp/windows/render/context.lua +++ b/lua/blink/cmp/completion/windows/render/context.lua @@ -32,18 +32,20 @@ end --- @param matched_indices number[] --- @return blink.cmp.DrawItemContext function context.new(draw, item, matched_indices) - local config = require('blink.cmp.config') + local config = require('blink.cmp.config').appearance local kind = require('blink.cmp.types').CompletionItemKind[item.kind] or 'Unknown' local kind_icon = config.kind_icons[kind] or config.kind_icons.Field - -- Some LSPs can return labels with newlines. - -- Escape them to avoid errors in nvim_buf_set_lines when rendering the autocomplete menu. local icon_spacing = config.nerd_font_variant == 'mono' and '' or ' ' + + -- Some LSPs can return labels with newlines + -- Escape them to avoid errors in nvim_buf_set_lines when rendering the completion menu local newline_char = '↲' .. icon_spacing local label = item.label:gsub('\n', newline_char) .. (kind == 'Snippet' and '~' or '') + if config.nerd_font_variant == 'normal' then label = label:gsub('…', '… ') end + local label_detail = (item.labelDetails and item.labelDetails.detail or ''):gsub('\n', newline_char) local label_description = (item.labelDetails and item.labelDetails.description or ''):gsub('\n', newline_char) - if config.nerd_font_variant == 'normal' then label = label:gsub('…', '… ') end return { self = draw, diff --git a/lua/blink/cmp/windows/render/init.lua b/lua/blink/cmp/completion/windows/render/init.lua similarity index 95% rename from lua/blink/cmp/windows/render/init.lua rename to lua/blink/cmp/completion/windows/render/init.lua index 1b42e7f7..cb6bc340 100644 --- a/lua/blink/cmp/windows/render/init.lua +++ b/lua/blink/cmp/completion/windows/render/init.lua @@ -42,7 +42,10 @@ function renderer.new(draw) self.def = draw self.columns = vim.tbl_map( function(column_definition) - return require('blink.cmp.windows.render.column').new(column_definition.components, column_definition.gap) + return require('blink.cmp.completion.windows.render.column').new( + column_definition.components, + column_definition.gap + ) end, columns_definitions ) @@ -51,7 +54,7 @@ end function renderer:draw(bufnr, items) -- gather contexts - local ctxs = require('blink.cmp.windows.render.context').get_from_items(self.def, items) + local ctxs = require('blink.cmp.completion.windows.render.context').get_from_items(self.def, items) -- render the columns for _, column in ipairs(self.columns) do diff --git a/lua/blink/cmp/completion/windows/render/tailwind.lua b/lua/blink/cmp/completion/windows/render/tailwind.lua new file mode 100644 index 00000000..5f877bab --- /dev/null +++ b/lua/blink/cmp/completion/windows/render/tailwind.lua @@ -0,0 +1,17 @@ +local tailwind = {} + +--- @param ctx blink.cmp.DrawItemContext +--- @return string|nil +function tailwind.get_hl(ctx) + local doc = ctx.item.documentation + if ctx.kind == 'Color' and doc then + local content = type(doc) == 'string' and doc or doc.value + if content and content:match('^#%x%x%x%x%x%x$') then + local hl_name = 'HexColor' .. content:sub(2) + if #vim.api.nvim_get_hl(0, { name = hl_name }) == 0 then vim.api.nvim_set_hl(0, hl_name, { fg = content }) end + return hl_name + end + end +end + +return tailwind diff --git a/lua/blink/cmp/windows/render/text.lua b/lua/blink/cmp/completion/windows/render/text.lua similarity index 100% rename from lua/blink/cmp/windows/render/text.lua rename to lua/blink/cmp/completion/windows/render/text.lua diff --git a/lua/blink/cmp/windows/render/types.lua b/lua/blink/cmp/completion/windows/render/types.lua similarity index 100% rename from lua/blink/cmp/windows/render/types.lua rename to lua/blink/cmp/completion/windows/render/types.lua diff --git a/lua/blink/cmp/config.lua b/lua/blink/cmp/config.lua deleted file mode 100644 index 63ef386f..00000000 --- a/lua/blink/cmp/config.lua +++ /dev/null @@ -1,590 +0,0 @@ ---- @alias blink.cmp.KeymapCommand ---- | 'fallback' Fallback to the built-in behavior ---- | 'show' Show the completion window ---- | 'hide' Hide the completion window ---- | 'cancel' Cancel the current completion, undoing the preview from auto_insert ---- | 'accept' Accept the current completion item ---- | 'select_and_accept' Select the current completion item and accept it ---- | 'select_prev' Select the previous completion item ---- | 'select_next' Select the next completion item ---- | 'show_documentation' Show the documentation window ---- | 'hide_documentation' Hide the documentation window ---- | 'scroll_documentation_up' Scroll the documentation window up ---- | 'scroll_documentation_down' Scroll the documentation window down ---- | 'snippet_forward' Move the cursor forward to the next snippet placeholder ---- | 'snippet_backward' Move the cursor backward to the previous snippet placeholder ---- | (fun(cmp: table): boolean?) Custom function where returning true will prevent the next command from running - ---- @alias blink.cmp.KeymapPreset ---- | 'default' mappings similar to built-in completion ---- | 'super-tab' mappings similar to vscode (tab to accept, arrow keys to navigate) ---- | 'enter' mappings similar to 'super-tab' but with 'enter' to accept - ---- @class blink.cmp.KeymapConfig ---- @field preset? blink.cmp.KeymapPreset ---- @field [string]? blink.cmp.KeymapCommand[]> Table of keys => commands[] - ---- @class blink.cmp.AcceptConfig ---- @field expand_snippet? fun(string) ---- @field create_undo_point? boolean Create an undo point when accepting a completion item ---- @field auto_brackets? blink.cmp.AutoBracketsConfig - ---- @class blink.cmp.AutoBracketsConfig ---- @field enabled? boolean ---- @field default_brackets? string[] ---- @field override_brackets_for_filetypes? table ---- @field force_allow_filetypes? string[] Overrides the default blocked filetypes ---- @field blocked_filetypes? string[] ---- @field kind_resolution? blink.cmp.AutoBracketResolutionConfig Synchronously use the kind of the item to determine if brackets should be added ---- @field semantic_token_resolution? blink.cmp.AutoBracketSemanticTokenResolutionConfig Asynchronously use semantic token to determine if brackets should be added - ---- @class blink.cmp.AutoBracketResolutionConfig ---- @field enabled? boolean ---- @field blocked_filetypes? string[] ---- ---- @class blink.cmp.AutoBracketSemanticTokenResolutionConfig : blink.cmp.AutoBracketResolutionConfig ---- @field timeout_ms? number How long to wait for semantic tokens to return before assuming no brackets should be added - ---- @class blink.cmp.CompletionTriggerConfig ---- @field keyword_range? 'prefix' | 'full' ---- @field keyword_regex? string ---- @field exclude_from_prefix_regex? string ---- @field blocked_trigger_characters? string[] ---- @field show_on_accept_on_trigger_character? boolean When 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 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 additional 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_in_snippet? boolean When false, will not show the completion window when in a snippet ---- ---- @class blink.cmp.SignatureHelpTriggerConfig ---- @field enabled? boolean ---- @field blocked_trigger_characters? string[] ---- @field blocked_retrigger_characters? string[] ---- @field show_on_insert_on_trigger_character? boolean When true, will show the signature help window when the cursor comes after a trigger character when entering insert mode ---- ---- @class blink.cmp.TriggerConfig ---- @field completion? blink.cmp.CompletionTriggerConfig ---- @field signature_help? blink.cmp.SignatureHelpTriggerConfig - ---- @class blink.cmp.SourceConfig ---- @field completion? blink.cmp.SourceModeConfig ---- @field providers? table ---- ---- @class blink.cmp.SourceModeConfig ---- @field enabled_providers? string[] | fun(ctx?: blink.cmp.Context): string[] ---- ---- @class blink.cmp.SourceProviderConfig ---- @field name? string ---- @field module? string ---- @field enabled? boolean | fun(ctx?: blink.cmp.Context): boolean ---- @field opts? table ---- @field transform_items? fun(ctx: blink.cmp.Context, items: blink.cmp.CompletionItem[]): blink.cmp.CompletionItem[] ---- @field should_show_items? boolean | number | fun(ctx: blink.cmp.Context, items: blink.cmp.CompletionItem[]): boolean ---- @field max_items? number | fun(ctx: blink.cmp.Context, enabled_sources: string[], items: blink.cmp.CompletionItem[]): number ---- @field min_keyword_length? number | fun(ctx: blink.cmp.Context, enabled_sources: string[]): number ---- @field fallback_for? string[] | fun(ctx: blink.cmp.Context, enabled_sources: string[]): string[] ---- @field score_offset? number | fun(ctx: blink.cmp.Context, enabled_sources: string[]): number ---- @field deduplicate? blink.cmp.DeduplicateConfig ---- @field override? blink.cmp.SourceOverride ---- ---- @class blink.cmp.DeduplicateConfig ---- @field enabled? boolean ---- @field priority? number - ---- @class blink.cmp.PrebuiltBinariesConfig ---- @field download? boolean ---- @field force_version? string | nil ---- @field force_system_triple? string | nil - ---- @class blink.cmp.FuzzyConfig ---- @field use_typo_resistance? boolean ---- @field use_frecency? boolean ---- @field use_proximity? boolean ---- @field max_items? number ---- @field sorts? ("label" | "kind" | "score")[] ---- @field prebuilt_binaries? blink.cmp.PrebuiltBinariesConfig - ---- @class blink.cmp.WindowConfig ---- @field autocomplete? blink.cmp.AutocompleteConfig ---- @field documentation? blink.cmp.DocumentationConfig ---- @field signature_help? blink.cmp.SignatureHelpConfig ---- @field ghost_text? GhostTextConfig - ---- @class blink.cmp.HighlightConfig ---- @field ns? number ---- @field use_nvim_cmp_as_default? boolean - ---- @class blink.cmp.AutocompleteConfig ---- @field min_width? number ---- @field max_height? number ---- @field border? blink.cmp.WindowBorder ---- @field scrollbar? boolean ---- @field order? "top_down" | "bottom_up" ---- @field direction_priority? ("n" | "s")[] ---- @field auto_show? boolean ---- @field selection? "preselect" | "manual" | "auto_insert" ---- @field winblend? number ---- @field winhighlight? string ---- @field scrolloff? number ---- @field draw? blink.cmp.Draw ---- @field cycle? blink.cmp.AutocompleteConfig.CycleConfig - ---- @class blink.cmp.AutocompleteConfig.CycleConfig ---- @field from_bottom? boolean When `true`, calling `select_next` at the *bottom* of the completion list will select the *first* completion item. ---- @field from_top? boolean When `true`, calling `select_prev` at the *top* of the completion list will select the *last* completion item. - ---- @class blink.cmp.DocumentationDirectionPriorityConfig ---- @field autocomplete_north? ("n" | "s" | "e" | "w")[] ---- @field autocomplete_south? ("n" | "s" | "e" | "w")[] ---- ---- @alias blink.cmp.WindowBorderChar string | table ---- @alias blink.cmp.WindowBorder 'single' | 'double' | 'rounded' | 'solid' | 'shadow' | 'padded' | 'none' | blink.cmp.WindowBorderChar[] ---- ---- @class blink.cmp.DocumentationConfig ---- @field min_width? number ---- @field max_width? number ---- @field max_height? number ---- @field desired_min_width? number ---- @field desired_min_height? number ---- @field border? blink.cmp.WindowBorder ---- @field winblend? number ---- @field winhighlight? string ---- @field scrollbar? boolean ---- @field direction_priority? blink.cmp.DocumentationDirectionPriorityConfig ---- @field auto_show? boolean ---- @field auto_show_delay_ms? number Delay before showing the documentation window ---- @field update_delay_ms? number Delay before updating the documentation window ---- @field treesitter_highlighting? boolean Whether to use treesitter highlighting, disable if you run into performance issues - ---- @class blink.cmp.SignatureHelpConfig ---- @field min_width? number ---- @field max_width? number ---- @field max_height? number ---- @field border? blink.cmp.WindowBorder ---- @field winblend? number ---- @field winhighlight? string ---- @field scrollbar? boolean ---- @field direction_priority? ("n" | "s")[] ---- @field treesitter_highlighting? boolean Whether to use treesitter highlighting, disable if you run into performance issues - ---- @class GhostTextConfig ---- @field enabled? boolean - ---- @class blink.cmp.Config ---- @field keymap? blink.cmp.KeymapConfig | blink.cmp.KeymapPreset ---- @field accept? blink.cmp.AcceptConfig ---- @field trigger? blink.cmp.TriggerConfig ---- @field fuzzy? blink.cmp.FuzzyConfig ---- @field sources? blink.cmp.SourceConfig ---- @field windows? blink.cmp.WindowConfig ---- @field highlight? blink.cmp.HighlightConfig ---- @field nerd_font_variant? 'mono' | 'normal' ---- @field kind_icons? table ---- @field blocked_filetypes? string[] - ---- @type blink.cmp.Config -local config = { - -- The keymap can be: - -- - A preset ('default' | 'super-tab' | 'enter') - -- - A table of keys => command[] (optionally with a "preset" key to merge with a preset) - -- - -- When specifying 'preset' in the keymap table, the custom key mappings are merged with the preset, - -- and any conflicting keys will overwrite the preset mappings. - -- The "fallback" command will run the next non blink keymap. - -- - -- Example: - -- - -- keymap = { - -- preset = 'default', - -- [''] = { 'select_prev', 'fallback' }, - -- [''] = { 'select_next', 'fallback' }, - -- - -- -- disable a keymap from the preset - -- [''] = {}, - -- }, - -- - -- When defining your own keymaps without a preset, no keybinds will be assigned automatically. - -- - -- Available commands: - -- show, hide, cancel, accept, select_and_accept, select_prev, select_next, show_documentation, hide_documentation, - -- scroll_documentation_up, scroll_documentation_down, snippet_forward, snippet_backward, fallback - -- - -- "default" keymap - -- [''] = { 'show', 'show_documentation', 'hide_documentation' }, - -- [''] = { 'hide' }, - -- [''] = { 'select_and_accept' }, - -- - -- [''] = { 'select_prev', 'fallback' }, - -- [''] = { 'select_next', 'fallback' }, - -- - -- [''] = { 'scroll_documentation_up', 'fallback' }, - -- [''] = { 'scroll_documentation_down', 'fallback' }, - -- - -- [''] = { 'snippet_forward', 'fallback' }, - -- [''] = { 'snippet_backward', 'fallback' }, - -- - -- "super-tab" keymap - -- you may want to set `trigger.completion.show_in_snippet = false` - -- or use `window.autocomplete.selection = "manual" | "auto_insert"` - -- - -- [''] = { 'show', 'show_documentation', 'hide_documentation' }, - -- [''] = { 'hide', 'fallback' }, - -- - -- [''] = { - -- function(cmp) - -- if cmp.is_in_snippet() then return cmp.accept() - -- else return cmp.select_and_accept() end - -- end, - -- 'snippet_forward', - -- 'fallback' - -- }, - -- [''] = { 'snippet_backward', 'fallback' }, - -- - -- [''] = { 'select_prev', 'fallback' }, - -- [''] = { 'select_next', 'fallback' }, - -- [''] = { 'select_prev', 'fallback' }, - -- [''] = { 'select_next', 'fallback' }, - -- - -- [''] = { 'scroll_documentation_up', 'fallback' }, - -- [''] = { 'scroll_documentation_down', 'fallback' }, - -- - -- "enter" keymap - -- you may want to set `window.autocomplete.selection = "manual" | "auto_insert"` - -- - -- [''] = { 'show', 'show_documentation', 'hide_documentation' }, - -- [''] = { 'hide', 'fallback' }, - -- [''] = { 'accept', 'fallback' }, - -- - -- [''] = { 'snippet_forward', 'fallback' }, - -- [''] = { 'snippet_backward', 'fallback' }, - -- - -- [''] = { 'select_prev', 'fallback' }, - -- [''] = { 'select_next', 'fallback' }, - -- [''] = { 'select_prev', 'fallback' }, - -- [''] = { 'select_next', 'fallback' }, - -- - -- [''] = { 'scroll_documentation_up', 'fallback' }, - -- [''] = { 'scroll_documentation_down', 'fallback' }, - keymap = 'default', - - accept = { - create_undo_point = true, - auto_brackets = { - enabled = false, - default_brackets = { '(', ')' }, - override_brackets_for_filetypes = {}, - -- Overrides the default blocked filetypes - force_allow_filetypes = {}, - blocked_filetypes = {}, - -- Synchronously use the kind of the item to determine if brackets should be added - kind_resolution = { - enabled = true, - blocked_filetypes = { 'typescriptreact', 'javascriptreact', 'vue' }, - }, - -- Asynchronously use semantic token to determine if brackets should be added - semantic_token_resolution = { - enabled = true, - blocked_filetypes = {}, - -- How long to wait for semantic tokens to return before assuming no brackets should be added - timeout_ms = 400, - }, - }, - expand_snippet = vim.snippet.expand, - }, - - trigger = { - completion = { - -- 'prefix' will fuzzy match on the text before the cursor - -- 'full' will fuzzy match on the text before *and* after the cursor - -- example: 'foo_|_bar' will match 'foo_' for 'prefix' and 'foo__bar' for 'full' - keyword_range = 'prefix', - -- regex used to get the text when fuzzy matching - -- changing this may break some sources, so please report if you run into issues - -- TODO: shouldnt this also affect the accept command? should this also be per language? - keyword_regex = '[%w_\\-]', - -- after matching with keyword_regex, any characters matching this regex at the prefix will be excluded - exclude_from_prefix_regex = '[\\-]', - -- LSPs can indicate when to show the completion window via trigger characters - -- however, some LSPs (*cough* tsserver *cough*) return characters that would essentially - -- always show the window. We block these by default - blocked_trigger_characters = { ' ', '\n', '\t' }, - -- when 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, - -- when true, will show the completion window when the cursor comes after a trigger character when entering insert mode - show_on_insert_on_trigger_character = true, - -- list of additional trigger characters that won't trigger the completion window when the cursor comes after a trigger character when entering insert mode/accepting an item - show_on_x_blocked_trigger_characters = { "'", '"', '(' }, - -- when false, will not show the completion window when in a snippet - show_in_snippet = true, - }, - - signature_help = { - enabled = false, - blocked_trigger_characters = {}, - blocked_retrigger_characters = {}, - -- when true, will show the signature help window when the cursor comes after a trigger character when entering insert mode - show_on_insert_on_trigger_character = true, - }, - }, - - fuzzy = { - -- when enabled, allows for a number of typos relative to the length of the query - -- disabling this matches the behavior of fzf - use_typo_resistance = true, - -- frencency tracks the most recently/frequently used items and boosts the score of the item - use_frecency = true, - -- proximity bonus boosts the score of items with a value in the buffer - 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' }, - - prebuilt_binaries = { - -- Whether 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` - download = true, - -- When downloading a prebuilt binary force the downloader to resolve this version. If this is uset - -- then the downloader will attempt to infer the version from the checked out git tag (if any). - -- - -- Beware that if the FFI ABI changes while tracking main then this may result in blink breaking. - force_version = nil, - -- 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`. - -- - -- Beware that if the FFI ABI changes while tracking main then this may result in blink breaking. - force_system_triple = nil, - }, - }, - - sources = { - -- list of enabled providers - completion = { - enabled_providers = { 'lsp', 'path', 'snippets', 'buffer' }, - }, - - -- table of providers to configure - providers = { - lsp = { - name = 'LSP', - module = 'blink.cmp.sources.lsp', - }, - path = { - name = 'Path', - module = 'blink.cmp.sources.path', - score_offset = 3, - }, - snippets = { - name = 'Snippets', - module = 'blink.cmp.sources.snippets', - score_offset = -3, - }, - buffer = { - name = 'Buffer', - module = 'blink.cmp.sources.buffer', - fallback_for = { 'lsp' }, - }, - }, - }, - - windows = { - autocomplete = { - min_width = 15, - max_height = 10, - border = 'none', - winblend = 0, - winhighlight = 'Normal:BlinkCmpMenu,FloatBorder:BlinkCmpMenuBorder,CursorLine:BlinkCmpMenuSelection,Search:None', - -- keep the cursor X lines away from the top/bottom of the window - scrolloff = 2, - -- note that the gutter will be disabled when border ~= 'none' - scrollbar = true, - -- TODO: implement - order = 'top_down', - -- which directions to show the window, - -- falling back to the next direction when there's not enough space - direction_priority = { 's', 'n' }, - -- Controls whether the completion window will automatically show when typing - auto_show = true, - -- Controls how the completion items are selected - -- 'preselect' will automatically select the first item in the completion list - -- 'manual' will not select any item by default - -- 'auto_insert' will not select any item by default, and insert the completion items automatically when selecting them - selection = 'preselect', - -- Controls how the completion items are rendered on the popup window - draw = { - -- Aligns the keyword you've typed to a component in the menu - align_to_component = 'label', -- or 'none' to disable - -- Left and right padding, optionally { left, right } for different padding on each side - padding = 1, - -- Gap between columns - gap = 1, - -- Components to render, grouped by column - columns = { { 'kind_icon' }, { 'label', 'label_description', gap = 1 } }, - -- Definitions for possible components to render. Each component defines: - -- ellipsis: whether to add an ellipsis when truncating the text - -- width: control the min, max and fill behavior of the component - -- text function: will be called for each item - -- highlight function: will be called only when the line appears on screen - components = { - kind_icon = { - ellipsis = false, - text = function(ctx) return ctx.kind_icon .. ctx.icon_gap end, - highlight = function(ctx) - return require('blink.cmp.utils').get_tailwind_hl(ctx) or 'BlinkCmpKind' .. ctx.kind - end, - }, - - kind = { - ellipsis = false, - width = { fill = true }, - text = function(ctx) return ctx.kind end, - highlight = function(ctx) - return require('blink.cmp.utils').get_tailwind_hl(ctx) or 'BlinkCmpKind' .. ctx.kind - end, - }, - - label = { - width = { fill = true, max = 60 }, - text = function(ctx) return ctx.label .. ctx.label_detail end, - highlight = function(ctx) - -- label and label details - local label = ctx.label - local highlights = { - { 0, #label, group = ctx.deprecated and 'BlinkCmpLabelDeprecated' or 'BlinkCmpLabel' }, - } - if ctx.label_detail then - table.insert(highlights, { #label, #label + #ctx.label_detail, group = 'BlinkCmpLabelDetail' }) - end - - -- characters matched on the label by the fuzzy matcher - for _, idx in ipairs(ctx.label_matched_indices) do - table.insert(highlights, { idx, idx + 1, group = 'BlinkCmpLabelMatch' }) - end - - return highlights - end, - }, - - label_description = { - width = { max = 30 }, - text = function(ctx) return ctx.label_description end, - highlight = 'BlinkCmpLabelDescription', - }, - }, - }, - -- Controls the cycling behavior when reaching the beginning or end of the completion list. - cycle = { - -- When `true`, calling `select_next` at the *bottom* of the completion list will select the *first* completion item. - from_bottom = true, - -- When `true`, calling `select_prev` at the *top* of the completion list will select the *last* completion item. - from_top = true, - }, - }, - documentation = { - max_width = 80, - max_height = 20, - desired_min_width = 50, - desired_min_height = 10, - border = 'padded', - winblend = 0, - winhighlight = 'Normal:BlinkCmpDoc,FloatBorder:BlinkCmpDocBorder,CursorLine:BlinkCmpDocCursorLine,Search:None', - -- note that the gutter will be disabled when border ~= 'none' - scrollbar = true, - -- which directions to show the documentation window, - -- for each of the possible autocomplete window directions, - -- falling back to the next direction when there's not enough space - direction_priority = { - autocomplete_north = { 'e', 'w', 'n', 's' }, - autocomplete_south = { 'e', 'w', 's', 'n' }, - }, - -- Controls whether the documentation window will automatically show when selecting a completion item - auto_show = false, - auto_show_delay_ms = 500, - update_delay_ms = 50, - -- whether to use treesitter highlighting, disable if you run into performance issues - -- WARN: temporary, eventually blink will support regex highlighting - treesitter_highlighting = true, - }, - signature_help = { - min_width = 1, - max_width = 100, - max_height = 10, - border = 'padded', - winblend = 0, - winhighlight = 'Normal:BlinkCmpSignatureHelp,FloatBorder:BlinkCmpSignatureHelpBorder', - -- note that the gutter will be disabled when border ~= 'none' - scrollbar = false, - - -- which directions to show the window, - -- falling back to the next direction when there's not enough space - direction_priority = { 'n', 's' }, - -- whether to use treesitter highlighting, disable if you run into performance issues - -- WARN: temporary, eventually blink will support regex highlighting - treesitter_highlighting = true, - }, - ghost_text = { - enabled = false, - }, - }, - - highlight = { - ns = vim.api.nvim_create_namespace('blink_cmp'), - -- sets the fallback highlight groups to nvim-cmp's highlight groups - -- useful for when your theme doesn't support blink.cmp - -- will be removed in a future release, assuming themes add support - use_nvim_cmp_as_default = false, - }, - - -- set to 'mono' for 'Nerd Font Mono' or 'normal' for 'Nerd Font' - -- adjusts spacing to ensure icons are aligned - nerd_font_variant = 'mono', - - -- don't show completions or signature help for these filetypes. Keymaps are also disabled. - blocked_filetypes = {}, - - kind_icons = { - Text = '󰉿', - Method = '󰊕', - Function = '󰊕', - Constructor = '󰒓', - - Field = '󰜢', - Variable = '󰆦', - Property = '󰖷', - - Class = '󱡠', - Interface = '󱡠', - Struct = '󱡠', - Module = '󰅩', - - Unit = '󰪚', - Value = '󰦨', - Enum = '󰦨', - EnumMember = '󰦨', - - Keyword = '󰻾', - Constant = '󰏿', - - Snippet = '󱄽', - Color = '󰏘', - File = '󰈔', - Reference = '󰬲', - Folder = '󰉋', - Event = '󱐋', - Operator = '󰪚', - TypeParameter = '󰬛', - }, -} - ---- @class blink.cmp.Config -local M = {} - ---- @param opts blink.cmp.Config -function M.merge_with(opts) - config = vim.tbl_deep_extend('force', config, opts or {}) - - -- TODO: remove on 1.0 - if type(config.windows.autocomplete.draw) == 'string' or type(config.windows.autocomplete.draw) == 'function' then - error('The blink.cmp autocomplete draw has been rewritten, please see the README for the new configuration') - end -end - -return setmetatable(M, { __index = function(_, k) return config[k] end }) diff --git a/lua/blink/cmp/config/appearance.lua b/lua/blink/cmp/config/appearance.lua new file mode 100644 index 00000000..3864196d --- /dev/null +++ b/lua/blink/cmp/config/appearance.lua @@ -0,0 +1,58 @@ +--- @class (exact) blink.cmp.AppearanceConfig +--- @field highlight_ns number +--- @field use_nvim_cmp_as_default boolean Sets the fallback highlight groups to nvim-cmp's highlight groups. Useful for when your theme doesn't support blink.cmp, will be removed in a future release. +--- @field nerd_font_variant 'mono' | 'normal' Set to 'mono' for 'Nerd Font Mono' or 'normal' for 'Nerd Font'. Adjusts spacing to ensure icons are aligned +--- @field kind_icons table + +local validate = require('blink.cmp.config.utils').validate +local appearance = { + --- @type blink.cmp.AppearanceConfig + default = { + highlight_ns = vim.api.nvim_create_namespace('blink_cmp'), + use_nvim_cmp_as_default = false, + nerd_font_variant = 'mono', + kind_icons = { + Text = '󰉿', + Method = '󰊕', + Function = '󰊕', + Constructor = '󰒓', + + Field = '󰜢', + Variable = '󰆦', + Property = '󰖷', + + Class = '󱡠', + Interface = '󱡠', + Struct = '󱡠', + Module = '󰅩', + + Unit = '󰪚', + Value = '󰦨', + Enum = '󰦨', + EnumMember = '󰦨', + + Keyword = '󰻾', + Constant = '󰏿', + + Snippet = '󱄽', + Color = '󰏘', + File = '󰈔', + Reference = '󰬲', + Folder = '󰉋', + Event = '󱐋', + Operator = '󰪚', + TypeParameter = '󰬛', + }, + }, +} + +function appearance.validate(config) + validate('appearance', { + highlight_ns = { config.highlight_ns, 'number' }, + use_nvim_cmp_as_default = { config.use_nvim_cmp_as_default, 'boolean' }, + nerd_font_variant = { config.nerd_font_variant, 'string' }, + kind_icons = { config.kind_icons, 'table' }, + }) +end + +return appearance diff --git a/lua/blink/cmp/config/completion/accept.lua b/lua/blink/cmp/config/completion/accept.lua new file mode 100644 index 00000000..716b190f --- /dev/null +++ b/lua/blink/cmp/config/completion/accept.lua @@ -0,0 +1,72 @@ +--- @class (exact) blink.cmp.CompletionAcceptConfig +--- @field create_undo_point boolean Create an undo point when accepting a completion item +--- @field auto_brackets blink.cmp.AutoBracketsConfig + +--- @class (exact) blink.cmp.AutoBracketsConfig +--- @field enabled boolean Whether to auto-insert brackets for functions +--- @field default_brackets string[] Default brackets to use for unknown languages +--- @field override_brackets_for_filetypes table +--- @field force_allow_filetypes string[] Overrides the default blocked filetypes +--- @field blocked_filetypes string[] +--- @field kind_resolution blink.cmp.AutoBracketResolutionConfig Synchronously use the kind of the item to determine if brackets should be added +--- @field semantic_token_resolution blink.cmp.AutoBracketSemanticTokenResolutionConfig Asynchronously use semantic token to determine if brackets should be added + +--- @class (exact) blink.cmp.AutoBracketResolutionConfig +--- @field enabled boolean +--- @field blocked_filetypes string[] + +--- @class (exact) blink.cmp.AutoBracketSemanticTokenResolutionConfig +--- @field enabled boolean +--- @field blocked_filetypes string[] +--- @field timeout_ms number How long to wait for semantic tokens to return before assuming no brackets should be added + +local validate = require('blink.cmp.config.utils').validate +local accept = { + --- @type blink.cmp.CompletionAcceptConfig + default = { + create_undo_point = true, + auto_brackets = { + enabled = false, + default_brackets = { '(', ')' }, + override_brackets_for_filetypes = {}, + force_allow_filetypes = {}, + blocked_filetypes = {}, + kind_resolution = { + enabled = true, + blocked_filetypes = { 'typescriptreact', 'javascriptreact', 'vue' }, + }, + semantic_token_resolution = { + enabled = true, + blocked_filetypes = {}, + timeout_ms = 400, + }, + }, + }, +} + +function accept.validate(config) + validate('completion.accept', { + create_undo_point = { config.create_undo_point, 'boolean' }, + auto_brackets = { config.auto_brackets, 'table' }, + }) + validate('completion.accept.auto_brackets', { + enabled = { config.auto_brackets.enabled, 'boolean' }, + default_brackets = { config.auto_brackets.default_brackets, 'table' }, + override_brackets_for_filetypes = { config.auto_brackets.override_brackets_for_filetypes, 'table' }, + force_allow_filetypes = { config.auto_brackets.force_allow_filetypes, 'table' }, + blocked_filetypes = { config.auto_brackets.blocked_filetypes, 'table' }, + kind_resolution = { config.auto_brackets.kind_resolution, 'table' }, + semantic_token_resolution = { config.auto_brackets.semantic_token_resolution, 'table' }, + }) + validate('completion.accept.auto_brackets.kind_resolution', { + enabled = { config.auto_brackets.kind_resolution.enabled, 'boolean' }, + blocked_filetypes = { config.auto_brackets.kind_resolution.blocked_filetypes, 'table' }, + }) + validate('completion.accept.auto_brackets.semantic_token_resolution', { + enabled = { config.auto_brackets.semantic_token_resolution.enabled, 'boolean' }, + blocked_filetypes = { config.auto_brackets.semantic_token_resolution.blocked_filetypes, 'table' }, + timeout_ms = { config.auto_brackets.semantic_token_resolution.timeout_ms, 'number' }, + }) +end + +return accept diff --git a/lua/blink/cmp/config/completion/documentation.lua b/lua/blink/cmp/config/completion/documentation.lua new file mode 100644 index 00000000..4adddc47 --- /dev/null +++ b/lua/blink/cmp/config/completion/documentation.lua @@ -0,0 +1,94 @@ +--- @class (exact) blink.cmp.CompletionDocumentationConfig +--- @field auto_show boolean Controls whether the documentation window will automatically show when selecting a completion item +--- @field auto_show_delay_ms number Delay before showing the documentation window +--- @field update_delay_ms number Delay before updating the documentation window when selecting a new item, while an existing item is still visible +--- @field treesitter_highlighting boolean Whether to use treesitter highlighting, disable if you run into performance issues +--- @field window blink.cmp.CompletionDocumentationWindowConfig + +--- @class (exact) blink.cmp.CompletionDocumentationWindowConfig +--- @field min_width number +--- @field max_width number +--- @field max_height number +--- @field desired_min_width number +--- @field desired_min_height number +--- @field border blink.cmp.WindowBorder +--- @field winblend number +--- @field winhighlight string +--- @field scrollbar boolean Note that the gutter will be disabled when border ~= 'none' +--- @field direction_priority blink.cmp.CompletionDocumentationDirectionPriorityConfig Which directions to show the window, for each of the possible menu window directions, falling back to the next direction when there's not enough space + +--- @class (exact) blink.cmp.CompletionDocumentationDirectionPriorityConfig +--- @field menu_north ("n" | "s" | "e" | "w")[] +--- @field menu_south ("n" | "s" | "e" | "w")[] + +local validate = require('blink.cmp.config.utils').validate +local documentation = { + --- @type blink.cmp.CompletionDocumentationConfig + default = { + auto_show = false, + auto_show_delay_ms = 500, + update_delay_ms = 50, + treesitter_highlighting = true, + window = { + min_width = 10, + max_width = 60, + max_height = 20, + desired_min_width = 50, + desired_min_height = 10, + border = 'padded', + winblend = 0, + winhighlight = 'Normal:BlinkCmpDoc,FloatBorder:BlinkCmpDocBorder', + scrollbar = true, + direction_priority = { + menu_north = { 'e', 'w', 'n', 's' }, + menu_south = { 'e', 'w', 's', 'n' }, + }, + }, + }, +} + +function documentation.validate(config) + validate('completion.documentation', { + auto_show = { config.auto_show, 'boolean' }, + auto_show_delay_ms = { config.auto_show_delay_ms, 'number' }, + update_delay_ms = { config.update_delay_ms, 'number' }, + treesitter_highlighting = { config.treesitter_highlighting, 'boolean' }, + window = { config.window, 'table' }, + }) + validate('completion.documentation.window', { + min_width = { config.window.min_width, 'number' }, + max_width = { config.window.max_width, 'number' }, + max_height = { config.window.max_height, 'number' }, + border = { config.window.border, 'string' }, + winblend = { config.window.winblend, 'number' }, + winhighlight = { config.window.winhighlight, 'string' }, + scrollbar = { config.window.scrollbar, 'boolean' }, + direction_priority = { config.window.direction_priority, 'table' }, + }) + validate('completion.documentation.window.direction_priority', { + menu_north = { + config.window.direction_priority.menu_north, + function(directions) + if type(directions) ~= 'table' or #directions == 0 then return false end + for _, direction in ipairs(directions) do + if not vim.tbl_contains({ 'n', 's', 'e', 'w' }, direction) then return false end + end + return true + end, + 'one of: "n", "s", "e", "w"', + }, + menu_south = { + config.window.direction_priority.menu_south, + function(directions) + if type(directions) ~= 'table' or #directions == 0 then return false end + for _, direction in ipairs(directions) do + if not vim.tbl_contains({ 'n', 's', 'e', 'w' }, direction) then return false end + end + return true + end, + 'one of: "n", "s", "e", "w"', + }, + }) +end + +return documentation diff --git a/lua/blink/cmp/config/completion/ghost_text.lua b/lua/blink/cmp/config/completion/ghost_text.lua new file mode 100644 index 00000000..afbf7e46 --- /dev/null +++ b/lua/blink/cmp/config/completion/ghost_text.lua @@ -0,0 +1,19 @@ +--- Displays a preview of the selected item on the current line +--- @class (exact) blink.cmp.CompletionGhostTextConfig +--- @field enabled boolean + +local validate = require('blink.cmp.config.utils').validate +local ghost_text = { + --- @type blink.cmp.CompletionGhostTextConfig + default = { + enabled = false, + }, +} + +function ghost_text.validate(config) + validate('completion.ghost_text', { + enabled = { config.enabled, 'boolean' }, + }) +end + +return ghost_text diff --git a/lua/blink/cmp/config/completion/init.lua b/lua/blink/cmp/config/completion/init.lua new file mode 100644 index 00000000..054cc21f --- /dev/null +++ b/lua/blink/cmp/config/completion/init.lua @@ -0,0 +1,42 @@ +--- @class (exact) blink.cmp.CompletionConfig +--- @field keyword blink.cmp.CompletionKeywordConfig +--- @field trigger blink.cmp.CompletionTriggerConfig +--- @field list blink.cmp.CompletionListConfig +--- @field accept blink.cmp.CompletionAcceptConfig +--- @field menu blink.cmp.CompletionMenuConfig +--- @field documentation blink.cmp.CompletionDocumentationConfig +--- @field ghost_text blink.cmp.CompletionGhostTextConfig + +local validate = require('blink.cmp.config.utils').validate +local completion = { + default = { + keyword = require('blink.cmp.config.completion.keyword').default, + trigger = require('blink.cmp.config.completion.trigger').default, + list = require('blink.cmp.config.completion.list').default, + accept = require('blink.cmp.config.completion.accept').default, + menu = require('blink.cmp.config.completion.menu').default, + documentation = require('blink.cmp.config.completion.documentation').default, + ghost_text = require('blink.cmp.config.completion.ghost_text').default, + }, +} + +function completion.validate(config) + validate('completion', { + keyword = { config.keyword, 'table' }, + trigger = { config.trigger, 'table' }, + list = { config.list, 'table' }, + accept = { config.accept, 'table' }, + menu = { config.menu, 'table' }, + documentation = { config.documentation, 'table' }, + ghost_text = { config.ghost_text, 'table' }, + }) + require('blink.cmp.config.completion.keyword').validate(config.keyword) + require('blink.cmp.config.completion.trigger').validate(config.trigger) + require('blink.cmp.config.completion.list').validate(config.list) + require('blink.cmp.config.completion.accept').validate(config.accept) + require('blink.cmp.config.completion.menu').validate(config.menu) + require('blink.cmp.config.completion.documentation').validate(config.documentation) + require('blink.cmp.config.completion.ghost_text').validate(config.ghost_text) +end + +return completion diff --git a/lua/blink/cmp/config/completion/keyword.lua b/lua/blink/cmp/config/completion/keyword.lua new file mode 100644 index 00000000..d5b257c2 --- /dev/null +++ b/lua/blink/cmp/config/completion/keyword.lua @@ -0,0 +1,35 @@ +--- @class (exact) blink.cmp.CompletionKeywordConfig +--- 'prefix' will fuzzy match on the text before the cursor +--- 'full' will fuzzy match on the text before *and* after the cursor +--- example: 'foo_|_bar' will match 'foo_' for 'prefix' and 'foo__bar' for 'full' +--- @field range blink.cmp.CompletionKeywordRange +--- @field regex string Regex used to get the text when fuzzy matching +--- @field exclude_from_prefix_regex string After matching with regex, any characters matching this regex at the prefix will be excluded +--- +--- @alias blink.cmp.CompletionKeywordRange +--- | 'prefix' Fuzzy match on the text before the cursor (example: 'foo_|bar' will match 'foo_') +--- | 'full' Fuzzy match on the text before *and* after the cursor (example: 'foo_|_bar' will match 'foo__bar') + +local validate = require('blink.cmp.config.utils').validate +local keyword = { + --- @type blink.cmp.CompletionKeywordConfig + default = { + range = 'prefix', + regex = '[%w_\\-]', + exclude_from_prefix_regex = '[\\-]', + }, +} + +function keyword.validate(config) + validate('completion.keyword', { + range = { + config.range, + function(range) return vim.tbl_contains({ 'prefix', 'full' }, range) end, + 'one of: prefix, full', + }, + regex = { config.regex, 'string' }, + exclude_from_prefix_regex = { config.exclude_from_prefix_regex, 'string' }, + }) +end + +return keyword diff --git a/lua/blink/cmp/config/completion/list.lua b/lua/blink/cmp/config/completion/list.lua new file mode 100644 index 00000000..2e974556 --- /dev/null +++ b/lua/blink/cmp/config/completion/list.lua @@ -0,0 +1,44 @@ +--- @class (exact) blink.cmp.CompletionListConfig +--- @field max_items number Maximum number of items to display +--- @field selection blink.cmp.CompletionListSelection Controls if completion items will be selected automatically, and whether selection automatically inserts +--- @field cycle blink.cmp.CompletionListCycleConfig + +--- @alias blink.cmp.CompletionListSelection +--- | 'preselect' Select the first item in the completion list +--- | 'manual' Don't select any item by default +--- | 'auto_insert' Don't select any item by default, and insert the completion items automatically when selecting them. You may want to bind a key to the `cancel` command when using this option, which will undo the selection and hide the completiom menu + +--- @class (exact) blink.cmp.CompletionListCycleConfig +--- @field from_bottom boolean When `true`, calling `select_next` at the *bottom* of the completion list will select the *first* completion item. +--- @field from_top boolean When `true`, calling `select_prev` at the *top* of the completion list will select the *last* completion item. + +local validate = require('blink.cmp.config.utils').validate +local list = { + --- @type blink.cmp.CompletionListConfig + default = { + max_items = 200, + selection = 'preselect', + cycle = { + from_bottom = true, + from_top = true, + }, + }, +} + +function list.validate(config) + validate('completion.list', { + max_items = { config.max_items, 'number' }, + selection = { + config.selection, + function() return vim.tbl_contains({ 'preselect', 'manual', 'auto_insert' }, config.selection) end, + 'one of: preselect, manual, auto_insert', + }, + cycle = { config.cycle, 'table' }, + }) + validate('completion.list.cycle', { + from_bottom = { config.cycle.from_bottom, 'boolean' }, + from_top = { config.cycle.from_top, 'boolean' }, + }) +end + +return list diff --git a/lua/blink/cmp/config/completion/menu.lua b/lua/blink/cmp/config/completion/menu.lua new file mode 100644 index 00000000..a7ce44f8 --- /dev/null +++ b/lua/blink/cmp/config/completion/menu.lua @@ -0,0 +1,183 @@ +--- @class (exact) blink.cmp.CompletionMenuConfig +--- @field enabled boolean +--- @field min_width number +--- @field max_height number +--- @field border blink.cmp.WindowBorder +--- @field scrollbar boolean Note that the gutter will be disabled when border ~= 'none' +--- @field order blink.cmp.CompletionMenuOrderConfig TODO: implement +--- @field direction_priority ("n" | "s")[] Which directions to show the window, falling back to the next direction when there's not enough space +--- @field winblend number +--- @field winhighlight string +--- @field scrolloff number Keep the cursor X lines away from the top/bottom of the window +--- @field draw blink.cmp.Draw Controls how the completion items are rendered on the popup window + +--- @class (exact) blink.cmp.CompletionMenuOrderConfig +--- @field n 'top_down' | 'bottom_up' +--- @field s 'top_down' | 'bottom_up' + +local validate = require('blink.cmp.config.utils').validate +local window = { + --- @type blink.cmp.CompletionMenuConfig + default = { + enabled = true, + min_width = 15, + max_height = 10, + border = 'none', + winblend = 0, + winhighlight = 'Normal:BlinkCmpMenu,FloatBorder:BlinkCmpMenuBorder,CursorLine:BlinkCmpMenuSelection,Search:None', + -- keep the cursor X lines away from the top/bottom of the window + scrolloff = 2, + -- note that the gutter will be disabled when border ~= 'none' + scrollbar = true, + -- which directions to show the window, + -- falling back to the next direction when there's not enough space + direction_priority = { 's', 'n' }, + -- which direction previous/next items show up + -- TODO: implement + order = { n = 'bottom_up', s = 'top_down' }, + + -- Controls how the completion items are rendered on the popup window + draw = { + -- Aligns the keyword you've typed to a component in the menu + align_to_component = 'label', -- or 'none' to disable + -- Left and right padding, optionally { left, right } for different padding on each side + padding = 1, + -- Gap between columns + gap = 1, + -- Components to render, grouped by column + columns = { { 'kind_icon' }, { 'label', 'label_description', gap = 1 } }, + -- Definitions for possible components to render. Each component defines: + -- ellipsis: whether to add an ellipsis when truncating the text + -- width: control the min, max and fill behavior of the component + -- text function: will be called for each item + -- highlight function: will be called only when the line appears on screen + components = { + kind_icon = { + ellipsis = false, + text = function(ctx) return ctx.kind_icon .. ctx.icon_gap end, + highlight = function(ctx) + return require('blink.cmp.completion.windows.render.tailwind').get_hl(ctx) or ('BlinkCmpKind' .. ctx.kind) + end, + }, + + kind = { + ellipsis = false, + width = { fill = true }, + text = function(ctx) return ctx.kind end, + highlight = function(ctx) + return require('blink.cmp.completion.windows.render.tailwind').get_hl(ctx) or ('BlinkCmpKind' .. ctx.kind) + end, + }, + + label = { + width = { fill = true, max = 60 }, + text = function(ctx) return ctx.label .. ctx.label_detail end, + highlight = function(ctx) + -- label and label details + local label = ctx.label + local highlights = { + { 0, #label, group = ctx.deprecated and 'BlinkCmpLabelDeprecated' or 'BlinkCmpLabel' }, + } + if ctx.label_detail then + table.insert(highlights, { #label, #label + #ctx.label_detail, group = 'BlinkCmpLabelDetail' }) + end + + -- characters matched on the label by the fuzzy matcher + for _, idx in ipairs(ctx.label_matched_indices) do + table.insert(highlights, { idx, idx + 1, group = 'BlinkCmpLabelMatch' }) + end + + return highlights + end, + }, + + label_description = { + width = { max = 30 }, + text = function(ctx) return ctx.label_description end, + highlight = 'BlinkCmpLabelDescription', + }, + }, + }, + }, +} + +function window.validate(config) + validate('completion.window', { + enabled = { config.enabled, 'boolean' }, + min_width = { config.min_width, 'number' }, + max_height = { config.max_height, 'number' }, + border = { config.border, 'string' }, + scrollbar = { config.scrollbar, 'boolean' }, + order = { config.order, 'table' }, + direction_priority = { config.direction_priority, 'table' }, + winblend = { config.winblend, 'number' }, + winhighlight = { config.winhighlight, 'string' }, + scrolloff = { config.scrolloff, 'number' }, + draw = { config.draw, 'table' }, + }) + validate('completion.window.order', { + n = { config.order.n, { 'string', 'nil' } }, + s = { config.order.s, { 'string', 'nil' } }, + }) + validate('completion.window.direction_priority', { + n = { config.direction_priority.n, { 'string', 'nil' } }, + s = { config.direction_priority.s, { 'string', 'nil' } }, + }) + + validate('completion.window.draw', { + align_to_component = { + config.draw.align_to_component, + function(align) + if align == 'none' then return true end + for _, column in ipairs(config.draw.columns) do + for _, component in ipairs(column) do + if component == align then return true end + end + end + return false + end, + '"none" or one of the components defined in the "columns"', + }, + padding = { + config.draw.padding, + function(padding) + if type(padding) == 'number' then return true end + if type(padding) ~= 'table' or #padding ~= 2 then return false end + if type(padding[1]) == 'number' and type(padding[2]) == 'number' then return true end + return false + end, + 'a number or a tuple of 2 numbers (i.e. [1, 2])', + }, + gap = { config.draw.gap, 'number' }, + columns = { + config.draw.columns, + function(columns) + local available_components = vim.tbl_keys(config.draw.components) + + if type(columns) ~= 'table' or #columns == 0 then return false end + for _, column in ipairs(columns) do + if #column == 0 then return false end + for _, component in ipairs(column) do + if not vim.tbl_contains(available_components, component) then return false end + end + if column.gap ~= nil and type(column.gap) ~= 'number' then return false end + end + return true + end, + 'a table of tables, where each table contains a list of components and an optional gap. List of available components: ' + .. table.concat(vim.tbl_keys(config.draw.components), ', '), + }, + components = { config.draw.components, 'table' }, + }) + + for component, definition in pairs(config.draw.components) do + validate('completion.window.draw.components.' .. component, { + ellipsis = { definition.ellipsis, 'boolean', true }, + width = { definition.width, 'table', true }, + text = { definition.text, 'function' }, + highlight = { definition.highlight, { 'string', 'function' }, true }, + }) + end +end + +return window diff --git a/lua/blink/cmp/config/completion/trigger.lua b/lua/blink/cmp/config/completion/trigger.lua new file mode 100644 index 00000000..037f4476 --- /dev/null +++ b/lua/blink/cmp/config/completion/trigger.lua @@ -0,0 +1,30 @@ +--- @class (exact) blink.cmp.CompletionTriggerConfig +--- @field show_in_snippet boolean When false, will not show the completion window when in a snippet +--- @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_accept_on_trigger_character boolean When 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 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 + +local validate = require('blink.cmp.config.utils').validate +local trigger = { + --- @type blink.cmp.CompletionTriggerConfig + default = { + show_in_snippet = true, + show_on_blocked_trigger_characters = { ' ', '\n', '\t' }, + show_on_accept_on_trigger_character = true, + show_on_insert_on_trigger_character = true, + show_on_x_blocked_trigger_characters = { "'", '"', '(' }, + }, +} + +function trigger.validate(config) + validate('completion.trigger', { + show_in_snippet = { config.show_in_snippet, 'boolean' }, + show_on_blocked_trigger_characters = { config.show_on_blocked_trigger_characters, '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' }, + }) +end + +return trigger diff --git a/lua/blink/cmp/config/fuzzy.lua b/lua/blink/cmp/config/fuzzy.lua new file mode 100644 index 00000000..b91727d6 --- /dev/null +++ b/lua/blink/cmp/config/fuzzy.lua @@ -0,0 +1,44 @@ +--- @class (exact) blink.cmp.FuzzyConfig +--- @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 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 + +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' }, + prebuilt_binaries = { + download = true, + force_version = nil, + force_system_triple = nil, + }, + }, +} + +function fuzzy.validate(config) + validate('fuzzy', { + use_typo_resistance = { config.use_typo_resistance, 'boolean' }, + use_frecency = { config.use_frecency, 'boolean' }, + use_proximity = { config.use_proximity, 'boolean' }, + sorts = { config.sorts, { 'string' } }, + prebuilt_binaries = { config.prebuilt_binaries, 'table' }, + }) + validate('fuzzy.prebuilt_binaries', { + download = { config.prebuilt_binaries.download, 'boolean' }, + force_version = { config.prebuilt_binaries.force_version, { 'string', 'nil' } }, + force_system_triple = { config.prebuilt_binaries.force_system_triple, { 'string', 'nil' } }, + }) +end + +return fuzzy diff --git a/lua/blink/cmp/config/init.lua b/lua/blink/cmp/config/init.lua new file mode 100644 index 00000000..3ff982fe --- /dev/null +++ b/lua/blink/cmp/config/init.lua @@ -0,0 +1,55 @@ +--- @class (exact) blink.cmp.ConfigStrict +--- @field blocked_filetypes string[] +--- @field keymap blink.cmp.KeymapConfig +--- @field completion blink.cmp.CompletionConfig +--- @field sources blink.cmp.SourceConfig +--- @field signature blink.cmp.SignatureConfig +--- @field snippets blink.cmp.SnippetsConfig +--- @field appearance blink.cmp.AppearanceConfig + +--- @class (exact) blink.cmp.Config : blink.cmp.ConfigStrict +--- HACK: for some reason lua-language-server treats this as Partial +--- but this seems to be a bug. See https://github.com/LuaLS/lua-language-server/issues/2561 +--- Much easier than copying every class and marking everything as optional for now :) + +local validate = require('blink.cmp.config.utils').validate +--- @type blink.cmp.ConfigStrict +local config = { + blocked_filetypes = {}, + keymap = require('blink.cmp.config.keymap').default, + completion = require('blink.cmp.config.completion').default, + fuzzy = require('blink.cmp.config.fuzzy').default, + sources = require('blink.cmp.config.sources').default, + signature = require('blink.cmp.config.signature').default, + snippets = require('blink.cmp.config.snippets').default, + appearance = require('blink.cmp.config.appearance').default, +} + +--- @type blink.cmp.Config +local M = {} + +--- @param self blink.cmp.ConfigStrict +function M.validate(self) + validate('config', { + blocked_filetypes = { self.blocked_filetypes, 'table' }, + keymap = { self.keymap, 'table' }, + completion = { self.completion, 'table' }, + sources = { self.sources, 'table' }, + signature = { self.signature, 'table' }, + snippets = { self.snippets, 'table' }, + appearance = { self.appearance, 'table' }, + }) + require('blink.cmp.config.completion').validate(self.completion) + require('blink.cmp.config.sources').validate(self.sources) + require('blink.cmp.config.signature').validate(self.signature) + require('blink.cmp.config.snippets').validate(self.snippets) + require('blink.cmp.config.appearance').validate(self.appearance) +end + +--- @param user_config blink.cmp.Config +function M.merge_with(user_config) + config = vim.tbl_deep_extend('force', config, user_config) + M.validate(config) +end + +return setmetatable(M, { __index = function(_, k) return config[k] end }) diff --git a/lua/blink/cmp/config/keymap.lua b/lua/blink/cmp/config/keymap.lua new file mode 100644 index 00000000..6aed00ec --- /dev/null +++ b/lua/blink/cmp/config/keymap.lua @@ -0,0 +1,152 @@ +--- @alias blink.cmp.KeymapCommand +--- | 'fallback' Fallback to the built-in behavior +--- | 'show' Show the completion window +--- | 'hide' Hide the completion window +--- | 'cancel' Cancel the current completion, undoing the preview from auto_insert +--- | 'accept' Accept the current completion item +--- | 'select_and_accept' Select the current completion item and accept it +--- | 'select_prev' Select the previous completion item +--- | 'select_next' Select the next completion item +--- | 'show_documentation' Show the documentation window +--- | 'hide_documentation' Hide the documentation window +--- | 'scroll_documentation_up' Scroll the documentation window up +--- | 'scroll_documentation_down' Scroll the documentation window down +--- | 'snippet_forward' Move the cursor forward to the next snippet placeholder +--- | 'snippet_backward' Move the cursor backward to the previous snippet placeholder +--- | (fun(cmp: table): boolean?) Custom function where returning true will prevent the next command from running + +--- @alias blink.cmp.KeymapPreset +--- Mappings similar to the built-in completion: +--- ```lua +--- { +--- [''] = { 'show', 'show_documentation', 'hide_documentation' }, +--- [''] = { 'hide' }, +--- [''] = { 'select_and_accept' }, +--- +--- [''] = { 'select_prev', 'fallback' }, +--- [''] = { 'select_next', 'fallback' }, +--- +--- [''] = { 'scroll_documentation_up', 'fallback' }, +--- [''] = { 'scroll_documentation_down', 'fallback' }, +--- +--- [''] = { 'snippet_forward', 'fallback' }, +--- [''] = { 'snippet_backward', 'fallback' }, +--- } +--- ``` +--- | 'default' +--- Mappings simliar to VSCode. +--- You may want to set `completion.trigger.show_in_snippet = false` or use `completion.list.selection = "manual" | "auto_insert"` when using this mapping: +--- ```lua +--- { +--- [''] = { 'show', 'show_documentation', 'hide_documentation' }, +--- [''] = { 'hide', 'fallback' }, +--- +--- [''] = { +--- function(cmp) +--- if cmp.is_in_snippet() then return cmp.accept() +--- else return cmp.select_and_accept() end +--- end, +--- 'snippet_forward', +--- 'fallback' +--- }, +--- [''] = { 'snippet_backward', 'fallback' }, +--- +--- [''] = { 'select_prev', 'fallback' }, +--- [''] = { 'select_next', 'fallback' }, +--- [''] = { 'select_prev', 'fallback' }, +--- [''] = { 'select_next', 'fallback' }, +--- +--- [''] = { 'scroll_documentation_up', 'fallback' }, +--- [''] = { 'scroll_documentation_down', 'fallback' }, +--- } +--- ``` +--- | 'super-tab' +--- Similar to 'super-tab' but with `enter` to accept +--- You may want to set `completion.list.selection = "manual" | "auto_insert"` when using this keymap: +--- ```lua +--- { +--- [''] = { 'show', 'show_documentation', 'hide_documentation' }, +--- [''] = { 'hide', 'fallback' }, +--- [''] = { 'accept', 'fallback' }, +--- +--- [''] = { 'snippet_forward', 'fallback' }, +--- [''] = { 'snippet_backward', 'fallback' }, +--- +--- [''] = { 'select_prev', 'fallback' }, +--- [''] = { 'select_next', 'fallback' }, +--- [''] = { 'select_prev', 'fallback' }, +--- [''] = { 'select_next', 'fallback' }, +--- +--- [''] = { 'scroll_documentation_up', 'fallback' }, +--- [''] = { 'scroll_documentation_down', 'fallback' }, +--- } +--- ``` +--- | 'enter' + +--- When specifying 'preset' in the keymap table, the custom key mappings are merged with the preset, and any conflicting keys will overwrite the preset mappings. +--- The "fallback" command will run the next non blink keymap. +--- +--- Example: +--- +--- keymap = { +--- preset = 'default', +--- [''] = { 'select_prev', 'fallback' }, +--- [''] = { 'select_next', 'fallback' }, +--- +--- -- disable a keymap from the preset +--- [''] = {}, +--- }, +--- +--- When defining your own keymaps without a preset, no keybinds will be assigned automatically. +--- @class (exact) blink.cmp.KeymapConfig +--- @field preset? blink.cmp.KeymapPreset +--- @field [string] blink.cmp.KeymapCommand[]> Table of keys => commands[] + +local keymap = { + --- @type blink.cmp.KeymapConfig + default = { + preset = 'default', + }, +} + +--- @param config blink.cmp.KeymapConfig +function keymap.validate(config) + local commands = { + 'fallback', + 'show', + 'hide', + 'cancel', + 'accept', + 'select_and_accept', + 'select_prev', + 'select_next', + 'show_documentation', + 'hide_documentation', + 'scroll_documentation_up', + 'scroll_documentation_down', + 'snippet_forward', + 'snippet_backward', + } + local presets = { 'default', 'super-tab', 'enter' } + + vim.validate({ preset = {} }) + local validation_schema = {} + for key, command_or_preset in pairs(config) do + if key == 'preset' then + validation_schema[key] = { + command_or_preset, + function(preset) return vim.tbl_contains(presets, preset) end, + '"preset" must be one of: ' .. table.concat(presets, ', '), + } + else + validation_schema[key] = { + command_or_preset, + function(command) return vim.tbl_contains(commands, command) end, + '"' .. key .. '" must be one of: ' .. table.concat(commands, ', '), + } + end + end + vim.validate(validation_schema) +end + +return keymap diff --git a/lua/blink/cmp/config/shared.lua b/lua/blink/cmp/config/shared.lua new file mode 100644 index 00000000..1ab7a605 --- /dev/null +++ b/lua/blink/cmp/config/shared.lua @@ -0,0 +1,2 @@ +--- @alias blink.cmp.WindowBorderChar string | table +--- @alias blink.cmp.WindowBorder 'single' | 'double' | 'rounded' | 'solid' | 'shadow' | 'padded' | 'none' | blink.cmp.WindowBorderChar[] diff --git a/lua/blink/cmp/config/signature.lua b/lua/blink/cmp/config/signature.lua new file mode 100644 index 00000000..6cb092c1 --- /dev/null +++ b/lua/blink/cmp/config/signature.lua @@ -0,0 +1,72 @@ +--- @class (exact) blink.cmp.SignatureConfig +--- @field enabled boolean +--- @field trigger blink.cmp.SignatureTriggerConfig +--- @field window blink.cmp.SignatureWindowConfig + +--- @class (exact) blink.cmp.SignatureTriggerConfig +--- @field blocked_trigger_characters string[] +--- @field blocked_retrigger_characters string[] +--- @field show_on_insert_on_trigger_character boolean When true, will show the signature help window when the cursor comes after a trigger character when entering insert mode + +--- @class (exact) blink.cmp.SignatureWindowConfig +--- @field min_width number +--- @field max_width number +--- @field max_height number +--- @field border blink.cmp.WindowBorder +--- @field winblend number +--- @field winhighlight string +--- @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, or another window is in the way. +--- @field treesitter_highlighting boolean Disable if you run into performance issues + +local validate = require('blink.cmp.config.utils').validate +local signature = { + --- @type blink.cmp.SignatureConfig + default = { + enabled = false, + trigger = { + enabled = true, + blocked_trigger_characters = {}, + blocked_retrigger_characters = {}, + show_on_insert_on_trigger_character = true, + }, + window = { + min_width = 1, + max_width = 100, + max_height = 10, + border = 'padded', + winblend = 0, + winhighlight = 'Normal:BlinkCmpSignatureHelp,FloatBorder:BlinkCmpSignatureHelpBorder', + scrollbar = false, + direction_priority = { 'n', 's' }, + treesitter_highlighting = true, + }, + }, +} + +function signature.validate(config) + validate('signature', { + enabled = { config.enabled, 'boolean' }, + trigger = { config.trigger, 'table' }, + window = { config.window, 'table' }, + }) + validate('signature.trigger', { + enabled = { config.trigger.enabled, 'boolean' }, + blocked_trigger_characters = { config.trigger.blocked_trigger_characters, 'table' }, + blocked_retrigger_characters = { config.trigger.blocked_retrigger_characters, 'table' }, + show_on_insert_on_trigger_character = { config.trigger.show_on_insert_on_trigger_character, 'boolean' }, + }) + validate('signature.window', { + min_width = { config.window.min_width, 'number' }, + max_width = { config.window.max_width, 'number' }, + max_height = { config.window.max_height, 'number' }, + border = { config.window.border, 'string' }, + winblend = { config.window.winblend, 'number' }, + winhighlight = { config.window.winhighlight, 'string' }, + scrollbar = { config.window.scrollbar, 'boolean' }, + direction_priority = { config.window.direction_priority, 'table' }, + treesitter_highlighting = { config.window.treesitter_highlighting, 'boolean' }, + }) +end + +return signature diff --git a/lua/blink/cmp/config/snippets.lua b/lua/blink/cmp/config/snippets.lua new file mode 100644 index 00000000..93142ca2 --- /dev/null +++ b/lua/blink/cmp/config/snippets.lua @@ -0,0 +1,26 @@ +--- @class (exact) blink.cmp.SnippetsConfig +--- @field expand fun(snippet: string) Function to use when expanding LSP provided snippets +--- @field active fun(filter?: { direction?: number }): boolean Function to use when checking if a snippet is active +--- @field jump fun(direction: number) Function to use when jumping between tab stops in a snippet, where direction can be negative or positive + +local validate = require('blink.cmp.config.utils').validate +local snippets = { + --- @type blink.cmp.SnippetsConfig + default = { + -- NOTE: we wrap these in functions to reduce startup by 1-2ms + -- when using lazy.nvim + expand = function(snippet) vim.snippet.expand(snippet) end, + active = function(filter) vim.snippet.active(filter) end, + jump = function(direction) vim.snippet.jump(direction) end, + }, +} + +function snippets.validate(config) + validate('snippets', { + expand = { config.expand, 'function' }, + active = { config.active, 'function' }, + jump = { config.jump, 'function' }, + }) +end + +return snippets diff --git a/lua/blink/cmp/config/sources.lua b/lua/blink/cmp/config/sources.lua new file mode 100644 index 00000000..044c020a --- /dev/null +++ b/lua/blink/cmp/config/sources.lua @@ -0,0 +1,94 @@ +--- @class blink.cmp.SourceConfig +--- @field completion blink.cmp.SourceModeConfig +--- @field providers table + +--- @class blink.cmp.SourceModeConfig +--- Static list of providers to enable, or a function to dynamically enable/disable providers based on the context +--- +--- Example dynamically picking providers based on the filetype and treesitter node: +--- ```lua +--- 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()) +--- return { 'buffer' } +--- else +--- return { 'lsp', 'path', 'snippets', 'buffer' } +--- end +--- end +--- ``` +--- @field enabled_providers string[] | fun(ctx?: blink.cmp.Context): string[] + +--- @class blink.cmp.SourceProviderConfig +--- @field name? string +--- @field module? string +--- @field enabled? boolean | fun(ctx?: blink.cmp.Context): boolean Whether or not to enable the provider +--- @field opts? table +--- @field transform_items? fun(ctx: blink.cmp.Context, items: blink.cmp.CompletionItem[]): blink.cmp.CompletionItem[] Function to transform the items before they're returned +--- @field should_show_items? boolean | number | fun(ctx: blink.cmp.Context, items: blink.cmp.CompletionItem[]): boolean Whether or not to show the items +--- @field max_items? number | fun(ctx: blink.cmp.Context, enabled_sources: string[], items: blink.cmp.CompletionItem[]): number Maximum number of items to display in the menu +--- @field min_keyword_length? number | fun(ctx: blink.cmp.Context, enabled_sources: string[]): number Minimum number of characters in the keyword to trigger the provider +--- @field fallback_for? string[] | fun(ctx: blink.cmp.Context, enabled_sources: string[]): string[] If any of these providers return 0 items, it will fallback to this provider +--- @field score_offset? number | fun(ctx: blink.cmp.Context, enabled_sources: string[]): number Boost/penalize the score of the items +--- @field deduplicate? blink.cmp.DeduplicateConfig TODO: implement +--- @field override? blink.cmp.SourceOverride Override the source's functions + +local validate = require('blink.cmp.config.utils').validate +local sources = { + --- @type blink.cmp.SourceConfig + default = { + completion = { + enabled_providers = { 'lsp', 'path', 'snippets', 'buffer' }, + }, + providers = { + lsp = { + name = 'LSP', + module = 'blink.cmp.sources.lsp', + }, + path = { + name = 'Path', + module = 'blink.cmp.sources.path', + score_offset = 3, + }, + snippets = { + name = 'Snippets', + module = 'blink.cmp.sources.snippets', + score_offset = -3, + }, + buffer = { + name = 'Buffer', + module = 'blink.cmp.sources.buffer', + fallback_for = { 'lsp' }, + }, + }, + }, +} + +function sources.validate(config) + validate('sources', { + completion = { config.completion, 'table' }, + providers = { config.providers, 'table' }, + }) + validate('sources.completion', { + enabled_providers = { config.completion.enabled_providers, 'table' }, + }) + for key, provider in pairs(config.providers) do + validate('sources.providers.' .. key, { + name = { provider.name, 'string' }, + module = { provider.module, 'string' }, + enabled = { provider.enabled, 'boolean', true }, + opts = { provider.opts, 'table', true }, + transform_items = { provider.transform_items, 'function', true }, + should_show_items = { provider.should_show_items, { 'boolean', 'function' }, true }, + max_items = { provider.max_items, { 'number', 'function' }, true }, + min_keyword_length = { provider.min_keyword_length, { 'number', 'function' }, true }, + fallback_for = { provider.fallback_for, { 'table', 'function' }, true }, + score_offset = { provider.score_offset, { 'number', 'function' }, true }, + deduplicate = { provider.deduplicate, 'table', true }, + override = { provider.override, 'table', true }, + }) + end +end + +return sources diff --git a/lua/blink/cmp/config/utils.lua b/lua/blink/cmp/config/utils.lua new file mode 100644 index 00000000..d0bcf967 --- /dev/null +++ b/lua/blink/cmp/config/utils.lua @@ -0,0 +1,13 @@ +local utils = {} + +---@param path string The path to the field being validated +---@param tbl table The table to validate +---@see vim.validate +---@return boolean is_valid +---@return string|nil error_message +function utils.validate(path, tbl) + local _, err = pcall(vim.validate, tbl) + if err then error(path .. '.' .. err) end +end + +return utils diff --git a/lua/blink/cmp/fuzzy/init.lua b/lua/blink/cmp/fuzzy/init.lua index db09bb2f..785124d2 100644 --- a/lua/blink/cmp/fuzzy/init.lua +++ b/lua/blink/cmp/fuzzy/init.lua @@ -2,21 +2,22 @@ local config = require('blink.cmp.config') local fuzzy = { rust = require('blink.cmp.fuzzy.rust'), + has_init_db = false, } ----@param db_path string -function fuzzy.init_db(db_path) - fuzzy.rust.init_db(db_path) +function fuzzy.init_db() + fuzzy.rust.init_db(vim.fn.stdpath('data') .. '/blink/cmp/fuzzy.db') vim.api.nvim_create_autocmd('VimLeavePre', { callback = fuzzy.rust.destroy_db, }) - - return fuzzy end ---@param item blink.cmp.CompletionItem -function fuzzy.access(item) fuzzy.rust.access(item) end +function fuzzy.access(item) + fuzzy.init_db() + fuzzy.rust.access(item) +end ---@param lines string function fuzzy.get_words(lines) return fuzzy.rust.get_words(lines) end @@ -27,6 +28,8 @@ function fuzzy.fuzzy_matched_indices(needle, haystack) return fuzzy.rust.fuzzy_m ---@param haystack blink.cmp.CompletionItem[]? ---@return blink.cmp.CompletionItem[] function fuzzy.fuzzy(needle, haystack) + fuzzy.init_db() + haystack = haystack or {} -- get the nearby words @@ -42,7 +45,7 @@ function fuzzy.fuzzy(needle, haystack) -- so this should generally be good -- TODO: make this configurable min_score = config.fuzzy.use_typo_resistance and (6 * needle:len()) or 0, - max_items = config.fuzzy.max_items, + max_items = config.completion.list.max_items, use_typo_resistance = config.fuzzy.use_typo_resistance, use_frecency = config.fuzzy.use_frecency, use_proximity = config.fuzzy.use_proximity, @@ -61,12 +64,9 @@ end --- @return string function fuzzy.get_query() local line = vim.api.nvim_get_current_line() - local cmp_config = config.trigger.completion - local range = require('blink.cmp.utils').get_regex_around_cursor( - cmp_config.keyword_range, - cmp_config.keyword_regex, - cmp_config.exclude_from_prefix_regex - ) + local keyword = config.completion.keyword + local range = + require('blink.cmp.lib.utils').get_regex_around_cursor(keyword.range, keyword.regex, keyword.exclude_from_prefix_regex) return string.sub(line, range.start_col, range.start_col + range.length - 1) end diff --git a/lua/blink/cmp/health.lua b/lua/blink/cmp/health.lua index e4870022..3417772e 100644 --- a/lua/blink/cmp/health.lua +++ b/lua/blink/cmp/health.lua @@ -1,7 +1,6 @@ local health = {} -local download = require('blink.cmp.fuzzy.download') -health.check = function() +function health.check() vim.health.start('blink.cmp healthcheck') local required_executables = { 'curl', 'git' } @@ -14,6 +13,7 @@ health.check = function() end -- check if os is supported + local download = require('blink.cmp.fuzzy.download') local system_triple = download.get_system_triple_sync() if system_triple then vim.health.ok('Your system is supported by pre-built binaries (' .. system_triple .. ')') @@ -32,4 +32,5 @@ health.check = function() vim.health.warn('blink_cmp_fuzzy lib is not downloaded/built') end end + return health diff --git a/lua/blink/cmp/highlights.lua b/lua/blink/cmp/highlights.lua new file mode 100644 index 00000000..1956ff70 --- /dev/null +++ b/lua/blink/cmp/highlights.lua @@ -0,0 +1,42 @@ +local highlights = {} + +function highlights.setup() + local use_nvim_cmp = require('blink.cmp.config').appearance.use_nvim_cmp_as_default + + local set_hl = function(hl_group, opts) + opts.default = true + vim.api.nvim_set_hl(0, hl_group, opts) + end + + if use_nvim_cmp then + set_hl('BlinkCmpLabel', { link = 'CmpItemAbbr' }) + set_hl('BlinkCmpLabelMatch', { link = 'CmpItemAbbrMatch' }) + end + + set_hl('BlinkCmpLabelDeprecated', { link = use_nvim_cmp and 'CmpItemAbbrDeprecated' or 'NonText' }) + set_hl('BlinkCmpLabelDetail', { link = use_nvim_cmp and 'CmpItemMenu' or 'NonText' }) + set_hl('BlinkCmpLabelDescription', { link = use_nvim_cmp and 'CmpItemMenu' or 'NonText' }) + set_hl('BlinkCmpKind', { link = use_nvim_cmp and 'CmpItemKind' or 'Special' }) + for _, kind in ipairs(require('blink.cmp.types').CompletionItemKind) do + set_hl('BlinkCmpKind' .. kind, { link = use_nvim_cmp and 'CmpItemKind' .. kind or 'BlinkCmpKind' }) + end + + set_hl('BlinkCmpScrollBarThumb', { link = 'PmenuThumb' }) + set_hl('BlinkCmpScrollBarGutter', { link = 'PmenuSbar' }) + + set_hl('BlinkCmpGhostText', { link = use_nvim_cmp and 'CmpGhostText' or 'NonText' }) + + set_hl('BlinkCmpMenu', { link = 'Pmenu' }) + set_hl('BlinkCmpMenuBorder', { link = 'Pmenu' }) + set_hl('BlinkCmpMenuSelection', { link = 'PmenuSel' }) + + set_hl('BlinkCmpDoc', { link = 'NormalFloat' }) + set_hl('BlinkCmpDocBorder', { link = 'NormalFloat' }) + set_hl('BlinkCmpDocCursorLine', { link = 'Visual' }) + + set_hl('BlinkCmpSignatureHelp', { link = 'NormalFloat' }) + set_hl('BlinkCmpSignatureHelpBorder', { link = 'NormalFloat' }) + set_hl('BlinkCmpSignatureHelpActiveParameter', { link = 'LspSignatureActiveParameter' }) +end + +return highlights diff --git a/lua/blink/cmp/init.lua b/lua/blink/cmp/init.lua index 3847c4f6..5e900d7c 100644 --- a/lua/blink/cmp/init.lua +++ b/lua/blink/cmp/init.lua @@ -1,7 +1,7 @@ local cmp = {} --- @param opts blink.cmp.Config -cmp.setup = function(opts) +function cmp.setup(opts) local config = require('blink.cmp.config') config.merge_with(opts) @@ -11,236 +11,132 @@ cmp.setup = function(opts) return end - cmp.add_default_highlights() - - require('blink.cmp.keymap').setup(config.keymap) - - -- STRUCTURE - -- trigger -> sources -> fuzzy (filter/sort) -> windows (render) - - -- trigger controls when to show the window and the current context for caching - -- TODO: add first_trigger event for setting up the rest of the plugin - cmp.trigger = require('blink.cmp.trigger.completion').activate_autocmds() - - -- sources fetch autocomplete items, documentation and signature help - cmp.sources = require('blink.cmp.sources.lib') - cmp.sources.register() - - -- windows render and apply completion items and signature help - cmp.windows = { - autocomplete = require('blink.cmp.windows.autocomplete').setup(), - documentation = require('blink.cmp.windows.documentation').setup(), - ghost_text = require('blink.cmp.windows.ghost-text').setup(), - } - - cmp.trigger.listen_on_show(function(context) cmp.sources.request_completions(context) end) - cmp.trigger.listen_on_hide(function() - cmp.sources.cancel_completions() - cmp.windows.autocomplete.close() - end) - cmp.sources.listen_on_completions(function(context, items) - -- fuzzy combines smith waterman with frecency - -- and bonus from proximity words but I'm still working - -- on tuning the weights - if not cmp.fuzzy then - cmp.fuzzy = require('blink.cmp.fuzzy') - cmp.fuzzy.init_db(vim.fn.stdpath('data') .. '/blink/cmp/fuzzy.db') - end - - -- we avoid adding 0.5-4ms to insertion latency by scheduling for later - vim.schedule(function() - if cmp.trigger.context == nil or cmp.trigger.context.id ~= context.id then return end - - local filtered_items = cmp.fuzzy.fuzzy(cmp.fuzzy.get_query(), items) - filtered_items = cmp.sources.apply_max_items_for_completions(context, filtered_items) - if #filtered_items > 0 then - cmp.windows.autocomplete.open_with_items(context, filtered_items) - else - cmp.windows.autocomplete.close() - end - end) - end) - - -- setup signature help if enabled - if config.trigger.signature_help.enabled then cmp.setup_signature_help() end + -- setup highlights, keymap, completion and signature help + require('blink.cmp.highlights').setup() + require('blink.cmp.keymap').setup() + require('blink.cmp.completion').setup() + if config.signature.enabled then require('blink.cmp.signature').setup() end end) end -cmp.setup_signature_help = function() - local signature_trigger = require('blink.cmp.trigger.signature').activate_autocmds() - local signature_window = require('blink.cmp.windows.signature').setup() - - signature_trigger.listen_on_show(function(context) - cmp.sources.cancel_signature_help() - cmp.sources.get_signature_help(context, function(signature_help) - if signature_help ~= nil and signature_trigger.context ~= nil and signature_trigger.context.id == context.id then - signature_trigger.set_active_signature_help(signature_help) - signature_window.open_with_signature_help(context, signature_help) - else - signature_trigger.hide() - end - end) - end) - signature_trigger.listen_on_hide(function() signature_window.close() end) -end - -cmp.add_default_highlights = function() - local use_nvim_cmp = require('blink.cmp.config').highlight.use_nvim_cmp_as_default - - local set_hl = function(hl_group, opts) - opts.default = true - vim.api.nvim_set_hl(0, hl_group, opts) - end - - set_hl('BlinkCmpLabel', { link = use_nvim_cmp and 'CmpItemAbbr' or 'Pmenu' }) - set_hl('BlinkCmpLabelDeprecated', { link = use_nvim_cmp and 'CmpItemAbbrDeprecated' or 'NonText' }) - set_hl('BlinkCmpLabelMatch', { link = use_nvim_cmp and 'CmpItemAbbrMatch' or 'Pmenu' }) - set_hl('BlinkCmpLabelDetail', { link = use_nvim_cmp and 'CmpItemMenu' or 'NonText' }) - set_hl('BlinkCmpLabelDescription', { link = use_nvim_cmp and 'CmpItemMenu' or 'NonText' }) - set_hl('BlinkCmpKind', { link = use_nvim_cmp and 'CmpItemKind' or 'Special' }) - for _, kind in ipairs(require('blink.cmp.types').CompletionItemKind) do - set_hl('BlinkCmpKind' .. kind, { link = use_nvim_cmp and 'CmpItemKind' .. kind or 'BlinkCmpKind' }) - end - - set_hl('BlinkCmpScrollBarThumb', { link = 'PmenuThumb' }) - set_hl('BlinkCmpScrollBarGutter', { link = 'PmenuSbar' }) - - set_hl('BlinkCmpGhostText', { link = use_nvim_cmp and 'CmpGhostText' or 'NonText' }) - - set_hl('BlinkCmpMenu', { link = 'Pmenu' }) - set_hl('BlinkCmpMenuBorder', { link = 'Pmenu' }) - set_hl('BlinkCmpMenuSelection', { link = 'PmenuSel' }) - - set_hl('BlinkCmpDoc', { link = 'NormalFloat' }) - set_hl('BlinkCmpDocBorder', { link = 'NormalFloat' }) - set_hl('BlinkCmpDocCursorLine', { link = 'Visual' }) - - set_hl('BlinkCmpSignatureHelp', { link = 'NormalFloat' }) - set_hl('BlinkCmpSignatureHelpBorder', { link = 'NormalFloat' }) - set_hl('BlinkCmpSignatureHelpActiveParameter', { link = 'LspSignatureActiveParameter' }) -end - ------- Public API ------- -cmp.show = function() - if cmp.windows.autocomplete.win:is_open() then return end - vim.schedule(function() - cmp.windows.autocomplete.auto_show = true - cmp.trigger.show({ force = true }) - end) +function cmp.show() + if require('blink.cmp.completion.windows.menu').win:is_open() then return end + + vim.schedule(function() require('blink.cmp.completion.trigger').show({ force = true }) end) return true end -cmp.hide = function() - if not cmp.windows.autocomplete.win:is_open() then return end - vim.schedule(cmp.trigger.hide) +function cmp.hide() + if not require('blink.cmp.completion.windows.menu').win:is_open() then return end + + vim.schedule(require('blink.cmp.completion.trigger').hide) return true end -cmp.cancel = function() - if not cmp.windows.autocomplete.win:is_open() then return end +function cmp.cancel() + if not require('blink.cmp.completion.windows.menu').win:is_open() then return end vim.schedule(function() - cmp.windows.autocomplete.undo_preview() - cmp.trigger.hide() + require('blink.cmp.completion.list').undo_preview() + require('blink.cmp.completion.trigger').hide() end) return true end ---- @param callback fun(context: blink.cmp.Context) -cmp.on_open = function(callback) cmp.windows.autocomplete.listen_on_open(callback) end - ---- @param callback fun() -cmp.on_close = function(callback) cmp.windows.autocomplete.listen_on_close(callback) end +function cmp.accept() + if not require('blink.cmp.completion.windows.menu').win:is_open() then return end -cmp.accept = function() - local item = cmp.windows.autocomplete.get_selected_item() + local completion_list = require('blink.cmp.completion.list') + local item = completion_list.get_selected_item() if item == nil then return end - vim.schedule(function() cmp.windows.autocomplete.accept() end) + vim.schedule(function() completion_list.accept() end) return true end -cmp.select_and_accept = function() - local autocomplete = require('blink.cmp.windows.autocomplete') - if not autocomplete.win:is_open() then return end +function cmp.select_and_accept() + if not require('blink.cmp.completion.windows.menu').win:is_open() then return end + local completion_list = require('blink.cmp.completion.list') vim.schedule(function() -- select an item if none is selected - if not autocomplete.get_selected_item() then - -- avoid running auto_insert since we're about to accept anyway - autocomplete.select_next({ skip_auto_insert = true }) - end - - local ctx = autocomplete.context - local item = autocomplete.get_selected_item() - if item ~= nil and ctx ~= nil then require('blink.cmp.accept')(ctx, item) end + if not completion_list.get_selected_item() then completion_list.select_next({ skip_auto_insert = true }) end + completion_list.accept() end) return true end -cmp.select_prev = function() - if not cmp.windows.autocomplete.win:is_open() then - if cmp.windows.autocomplete.auto_show then return end - cmp.show() - return true - end - vim.schedule(cmp.windows.autocomplete.select_prev) +function cmp.select_prev() + if not require('blink.cmp.completion.windows.menu').win:is_open() then return end + require('blink.cmp.completion.list').select_prev() return true end -cmp.select_next = function() - if not cmp.windows.autocomplete.win:is_open() then - if cmp.windows.autocomplete.auto_show then return end - cmp.show() - return true - end - vim.schedule(cmp.windows.autocomplete.select_next) +function cmp.select_next() + if not require('blink.cmp.completion.windows.menu').win:is_open() then return end + require('blink.cmp.completion.list').select_next() return true end -cmp.show_documentation = function() - if cmp.windows.documentation.win:is_open() then return end - local item = cmp.windows.autocomplete.get_selected_item() +function cmp.show_documentation() + local menu = require('blink.cmp.completion.windows.menu') + local documentation = require('blink.cmp.completion.windows.documentation') + if documentation.win:is_open() or not menu.win:is_open() then return end + + local item = require('blink.cmp.completion.list').get_selected_item() if not item then return end - vim.schedule(function() cmp.windows.documentation.show_item(item) end) + + vim.schedule(function() documentation.show_item(item) end) return true end -cmp.hide_documentation = function() - if not cmp.windows.documentation.win:is_open() then return end - vim.schedule(function() cmp.windows.documentation.win:close() end) +function cmp.hide_documentation() + local documentation = require('blink.cmp.completion.windows.documentation') + if not documentation.win:is_open() then return end + + vim.schedule(function() documentation.win:close() end) return true end -cmp.scroll_documentation_up = function() - if not cmp.windows.documentation.win:is_open() then return end - vim.schedule(function() cmp.windows.documentation.scroll_up(4) end) +--- @param count? number +function cmp.scroll_documentation_up(count) + local documentation = require('blink.cmp.completion.windows.documentation') + if not documentation.win:is_open() then return end + + vim.schedule(function() documentation.scroll_up(count or 4) end) return true end -cmp.scroll_documentation_down = function() - if not cmp.windows.documentation.win:is_open() then return end - vim.schedule(function() cmp.windows.documentation.scroll_down(4) end) +--- @param count? number +function cmp.scroll_documentation_down(count) + local documentation = require('blink.cmp.completion.windows.documentation') + if not documentation.win:is_open() then return end + + vim.schedule(function() documentation.scroll_down(count or 4) end) return true end -cmp.is_in_snippet = function() return vim.snippet.active() end +--- @param filter? { direction?: number } +function cmp.snippet_active(filter) return require('blink.cmp.config').snippets.active(filter) end -cmp.snippet_forward = function() - if not vim.snippet.active({ direction = 1 }) then return end - vim.schedule(function() vim.snippet.jump(1) end) +function cmp.snippet_forward() + local snippets = require('blink.cmp.config').snippets + if not snippets.active({ direction = 1 }) then return end + vim.schedule(function() snippets.jump(1) end) return true end -cmp.snippet_backward = function() - if not vim.snippet.active({ direction = -1 }) then return end - vim.schedule(function() vim.snippet.jump(-1) end) +function cmp.snippet_backward() + local snippets = require('blink.cmp.config').snippets + if not snippets.active({ direction = -1 }) then return end + vim.schedule(function() snippets.jump(-1) end) return true end --- @param override? lsp.ClientCapabilities --- @param include_nvim_defaults? boolean -cmp.get_lsp_capabilities = function(override, include_nvim_defaults) +function cmp.get_lsp_capabilities(override, include_nvim_defaults) return require('blink.cmp.sources.lib').get_lsp_capabilities(override, include_nvim_defaults) end diff --git a/lua/blink/cmp/keymap.lua b/lua/blink/cmp/keymap.lua deleted file mode 100644 index ec35d09a..00000000 --- a/lua/blink/cmp/keymap.lua +++ /dev/null @@ -1,261 +0,0 @@ -local utils = require('blink.cmp.utils') -local keymap = {} - -local default_keymap = { - [''] = { 'show', 'show_documentation', 'hide_documentation' }, - [''] = { 'hide' }, - [''] = { 'select_and_accept' }, - - [''] = { 'select_prev', 'fallback' }, - [''] = { 'select_next', 'fallback' }, - [''] = { 'select_prev', 'fallback' }, - [''] = { 'select_next', 'fallback' }, - - [''] = { 'scroll_documentation_up', 'fallback' }, - [''] = { 'scroll_documentation_down', 'fallback' }, - - [''] = { 'snippet_forward', 'fallback' }, - [''] = { 'snippet_backward', 'fallback' }, -} - -local super_tab_keymap = { - [''] = { 'show', 'show_documentation', 'hide_documentation' }, - [''] = { 'hide' }, - - [''] = { - function(cmp) - if cmp.is_in_snippet() then - return cmp.accept() - else - return cmp.select_and_accept() - end - end, - 'snippet_forward', - 'fallback', - }, - [''] = { 'snippet_backward', 'fallback' }, - - [''] = { 'select_prev', 'fallback' }, - [''] = { 'select_next', 'fallback' }, - [''] = { 'select_prev', 'fallback' }, - [''] = { 'select_next', 'fallback' }, - - [''] = { 'scroll_documentation_up', 'fallback' }, - [''] = { 'scroll_documentation_down', 'fallback' }, -} - -local enter_keymap = { - [''] = { 'show', 'show_documentation', 'hide_documentation' }, - [''] = { 'hide' }, - [''] = { 'accept', 'fallback' }, - - [''] = { 'snippet_forward', 'fallback' }, - [''] = { 'snippet_backward', 'fallback' }, - - [''] = { 'select_prev', 'fallback' }, - [''] = { 'select_next', 'fallback' }, - [''] = { 'select_prev', 'fallback' }, - [''] = { 'select_next', 'fallback' }, - - [''] = { 'scroll_documentation_up', 'fallback' }, - [''] = { 'scroll_documentation_down', 'fallback' }, -} - -local snippet_commands = { 'snippet_forward', 'snippet_backward' } - ---- @param opts blink.cmp.KeymapConfig -function keymap.setup(opts) - local mappings = opts - - -- notice for users on old config - if type(opts) == 'table' then - local commands = { - 'show', - 'hide', - 'cancel', - 'accept', - 'select_and_accept', - 'select_prev', - 'select_next', - 'show_documentation', - 'hide_documentation', - 'scroll_documentation_up', - 'scroll_documentation_down', - 'snippet_forward', - 'snippet_backward', - } - for key, _ in pairs(opts) do - if vim.tbl_contains(commands, key) then - error('The blink.cmp keymap recently got reworked. Please see the README for the updated configuration') - end - end - - -- Handle preset inside table - if opts.preset then - local preset_keymap = keymap.get_preset_keymap(opts.preset) - - -- Remove 'preset' key from opts to prevent it from being treated as a keymap - opts.preset = nil - -- Merge the preset keymap with the user-defined keymaps - -- User-defined keymaps overwrite the preset keymaps - mappings = vim.tbl_extend('force', preset_keymap, opts) - end - end - - -- handle presets - if type(opts) == 'string' then mappings = keymap.get_preset_keymap(opts) end - - -- we set on the buffer directly to avoid buffer-local keymaps (such as from autopairs) - -- from overriding our mappings. We also use InsertEnter to avoid conflicts with keymaps - -- applied on other autocmds, such as LspAttach used by nvim-lspconfig and most configs - vim.api.nvim_create_autocmd('InsertEnter', { - callback = function() - if utils.is_blocked_buffer() then return end - keymap.apply_keymap_to_current_buffer(mappings) - end, - }) - - -- This is not called when the plugin loads since it first checks if the binary is - -- installed. As a result, when lazy-loaded on InsertEnter, the event may be missed - if vim.api.nvim_get_mode().mode == 'i' and not utils.is_blocked_buffer() then - keymap.apply_keymap_to_current_buffer(mappings) - end -end - ---- Gets the preset keymap for the given preset name ---- @param preset_name string ---- @return table -function keymap.get_preset_keymap(preset_name) - if preset_name == 'default' then - return default_keymap - elseif preset_name == 'super-tab' then - return super_tab_keymap - elseif preset_name == 'enter' then - return enter_keymap - end - - error('Invalid blink.cmp keymap preset: ' .. preset_name) -end - ---- Applies the keymaps to the current buffer ---- @param keys_to_commands table -function keymap.apply_keymap_to_current_buffer(keys_to_commands) - -- skip if we've already applied the keymaps - for _, mapping in ipairs(vim.api.nvim_buf_get_keymap(0, 'i')) do - if mapping.desc == 'blink.cmp' then return end - end - - -- insert mode: uses both snippet and insert commands - for key, commands in pairs(keys_to_commands) do - if #commands > 0 then - keymap.set('i', key, function() - for _, command in ipairs(commands) do - -- special case for fallback - if command == 'fallback' then - return keymap.run_non_blink_keymap('i', key) - - -- run user defined functions - elseif type(command) == 'function' then - if command(require('blink.cmp')) then return end - - -- otherwise, run the built-in command - elseif require('blink.cmp')[command]() then - return - end - end - end) - end - end - - -- snippet mode - for key, commands in pairs(keys_to_commands) do - local has_snippet_command = false - for _, command in ipairs(commands) do - if vim.tbl_contains(snippet_commands, command) then has_snippet_command = true end - end - - if has_snippet_command and #commands > 0 then - keymap.set('s', key, function() - for _, command in ipairs(keys_to_commands[key] or {}) do - -- special case for fallback - if command == 'fallback' then - return keymap.run_non_blink_keymap('s', key) - - -- run user defined functions - elseif type(command) == 'function' then - if command(require('blink.cmp')) then return end - - -- only run snippet commands - elseif vim.tbl_contains(snippet_commands, command) then - local did_run = require('blink.cmp')[command]() - if did_run then return end - end - end - end) - end - end -end - ---- Gets the first non blink.cmp keymap for the given mode and key ---- @param mode string ---- @param key string ---- @return vim.api.keyset.keymap | nil -function keymap.get_non_blink_mapping_for_key(mode, key) - local normalized_key = vim.api.nvim_replace_termcodes(key, true, true, true) - - -- get buffer local and global mappings - local mappings = vim.api.nvim_buf_get_keymap(0, mode) - vim.list_extend(mappings, vim.api.nvim_get_keymap(mode)) - - for _, mapping in ipairs(mappings) do - local mapping_key = vim.api.nvim_replace_termcodes(mapping.lhs, true, true, true) - if mapping_key == normalized_key and mapping.desc ~= 'blink.cmp' then return mapping end - end -end - ---- Runs the first non blink.cmp keymap for the given mode and key ---- @param mode string ---- @param key string ---- @return string | nil -function keymap.run_non_blink_keymap(mode, key) - local mapping = keymap.get_non_blink_mapping_for_key(mode, key) or {} - - -- TODO: there's likely many edge cases here. the nvim-cmp version is lacking documentation - -- and is quite complex. we should look to see if we can simplify their logic - -- https://github.com/hrsh7th/nvim-cmp/blob/ae644feb7b67bf1ce4260c231d1d4300b19c6f30/lua/cmp/utils/keymap.lua - if type(mapping.callback) == 'function' then - -- with expr = true, which we use, we can't modify the buffer without scheduling - -- so if the keymap does not use expr, we must schedule it - if mapping.expr ~= 1 then - vim.schedule(mapping.callback) - return - end - - local expr = mapping.callback() - if mapping.replace_keycodes == 1 then expr = vim.api.nvim_replace_termcodes(expr, true, true, true) end - return expr - elseif mapping.rhs then - local rhs = vim.api.nvim_replace_termcodes(mapping.rhs, true, true, true) - if mapping.expr == 1 then rhs = vim.api.nvim_eval(rhs) end - return rhs - end - - -- pass the key along as usual - return vim.api.nvim_replace_termcodes(key, true, true, true) -end - ---- @param mode string ---- @param key string ---- @param callback fun(): string | nil -function keymap.set(mode, key, callback) - vim.api.nvim_buf_set_keymap(0, mode, key, '', { - callback = callback, - expr = true, - silent = true, - noremap = true, - replace_keycodes = false, - desc = 'blink.cmp', - }) -end - -return keymap diff --git a/lua/blink/cmp/keymap/apply.lua b/lua/blink/cmp/keymap/apply.lua new file mode 100644 index 00000000..c56052dc --- /dev/null +++ b/lua/blink/cmp/keymap/apply.lua @@ -0,0 +1,81 @@ +local apply = {} + +local snippet_commands = { 'snippet_forward', 'snippet_backward' } + +--- Applies the keymaps to the current buffer +--- @param keys_to_commands table +function apply.keymap_to_current_buffer(keys_to_commands) + -- skip if we've already applied the keymaps + for _, mapping in ipairs(vim.api.nvim_buf_get_keymap(0, 'i')) do + if mapping.desc == 'blink.cmp' then return end + end + + -- insert mode: uses both snippet and insert commands + for key, commands in pairs(keys_to_commands) do + if #commands == 0 then goto continue end + + apply.set('i', key, function() + for _, command in ipairs(commands) do + -- special case for fallback + if command == 'fallback' then + return require('blink.cmp.keymap.fallback').run_non_blink_keymap('i', key) + + -- run user defined functions + elseif type(command) == 'function' then + if command(require('blink.cmp')) then return end + + -- otherwise, run the built-in command + elseif require('blink.cmp')[command]() then + return + end + end + end) + + ::continue:: + end + + -- snippet mode + for key, commands in pairs(keys_to_commands) do + local has_snippet_command = false + for _, command in ipairs(commands) do + if vim.tbl_contains(snippet_commands, command) then has_snippet_command = true end + end + if not has_snippet_command or #commands == 0 then goto continue end + + apply.set('s', key, function() + for _, command in ipairs(keys_to_commands[key] or {}) do + -- special case for fallback + if command == 'fallback' then + return require('blink.cmp.keymap.fallback').run_non_blink_keymap('s', key) + + -- run user defined functions + elseif type(command) == 'function' then + if command(require('blink.cmp')) then return end + + -- only run snippet commands + elseif vim.tbl_contains(snippet_commands, command) then + local did_run = require('blink.cmp')[command]() + if did_run then return end + end + end + end) + + ::continue:: + end +end + +--- @param mode string +--- @param key string +--- @param callback fun(): string | nil +function apply.set(mode, key, callback) + vim.api.nvim_buf_set_keymap(0, mode, key, '', { + callback = callback, + expr = true, + silent = true, + noremap = true, + replace_keycodes = false, + desc = 'blink.cmp', + }) +end + +return apply diff --git a/lua/blink/cmp/keymap/fallback.lua b/lua/blink/cmp/keymap/fallback.lua new file mode 100644 index 00000000..bfaee7d9 --- /dev/null +++ b/lua/blink/cmp/keymap/fallback.lua @@ -0,0 +1,51 @@ +local fallback = {} + +--- Gets the first non blink.cmp keymap for the given mode and key +--- @param mode string +--- @param key string +--- @return vim.api.keyset.keymap | nil +function fallback.get_non_blink_mapping_for_key(mode, key) + local normalized_key = vim.api.nvim_replace_termcodes(key, true, true, true) + + -- get buffer local and global mappings + local mappings = vim.api.nvim_buf_get_keymap(0, mode) + vim.list_extend(mappings, vim.api.nvim_get_keymap(mode)) + + for _, mapping in ipairs(mappings) do + local mapping_key = vim.api.nvim_replace_termcodes(mapping.lhs, true, true, true) + if mapping_key == normalized_key and mapping.desc ~= 'blink.cmp' then return mapping end + end +end + +--- Runs the first non blink.cmp keymap for the given mode and key +--- @param mode string +--- @param key string +--- @return string | nil +function fallback.run_non_blink_keymap(mode, key) + local mapping = fallback.get_non_blink_mapping_for_key(mode, key) or {} + + -- TODO: there's likely many edge cases here. the nvim-cmp version is lacking documentation + -- and is quite complex. we should look to see if we can simplify their logic + -- https://github.com/hrsh7th/nvim-cmp/blob/ae644feb7b67bf1ce4260c231d1d4300b19c6f30/lua/cmp/utils/keymap.lua + if type(mapping.callback) == 'function' then + -- with expr = true, which we use, we can't modify the buffer without scheduling + -- so if the keymap does not use expr, we must schedule it + if mapping.expr ~= 1 then + vim.schedule(mapping.callback) + return + end + + local expr = mapping.callback() + if mapping.replace_keycodes == 1 then expr = vim.api.nvim_replace_termcodes(expr, true, true, true) end + return expr + elseif mapping.rhs then + local rhs = vim.api.nvim_replace_termcodes(mapping.rhs, true, true, true) + if mapping.expr == 1 then rhs = vim.api.nvim_eval(rhs) end + return rhs + end + + -- pass the key along as usual + return vim.api.nvim_replace_termcodes(key, true, true, true) +end + +return fallback diff --git a/lua/blink/cmp/keymap/init.lua b/lua/blink/cmp/keymap/init.lua new file mode 100644 index 00000000..0e402749 --- /dev/null +++ b/lua/blink/cmp/keymap/init.lua @@ -0,0 +1,36 @@ +local utils = require('blink.cmp.lib.utils') +local keymap = {} + +function keymap.setup() + local mappings = vim.deepcopy(require('blink.cmp.config').keymap) + + -- Handle preset + if mappings.preset then + local preset_keymap = require('blink.cmp.keymap.presets').get(mappings.preset) + + -- Remove 'preset' key from opts to prevent it from being treated as a keymap + mappings.preset = nil + + -- Merge the preset keymap with the user-defined keymaps + -- User-defined keymaps overwrite the preset keymaps + mappings = vim.tbl_extend('force', preset_keymap, mappings) + end + + -- We set on the buffer directly to avoid buffer-local keymaps (such as from autopairs) + -- from overriding our mappings. We also use InsertEnter to avoid conflicts with keymaps + -- applied on other autocmds, such as LspAttach used by nvim-lspconfig and most configs + vim.api.nvim_create_autocmd('InsertEnter', { + callback = function() + if utils.is_blocked_buffer() then return end + require('blink.cmp.keymap.apply').keymap_to_current_buffer(mappings) + end, + }) + + -- This is not called when the plugin loads since it first checks if the binary is + -- installed. As a result, when lazy-loaded on InsertEnter, the event may be missed + if vim.api.nvim_get_mode().mode == 'i' and not utils.is_blocked_buffer() then + require('blink.cmp.keymap.apply').keymap_to_current_buffer(mappings) + end +end + +return keymap diff --git a/lua/blink/cmp/keymap/presets.lua b/lua/blink/cmp/keymap/presets.lua new file mode 100644 index 00000000..212c9081 --- /dev/null +++ b/lua/blink/cmp/keymap/presets.lua @@ -0,0 +1,72 @@ +local presets = { + default = { + [''] = { 'show', 'show_documentation', 'hide_documentation' }, + [''] = { 'hide', 'fallback' }, + [''] = { 'select_and_accept' }, + + [''] = { 'select_prev', 'fallback' }, + [''] = { 'select_next', 'fallback' }, + [''] = { 'select_prev', 'fallback' }, + [''] = { 'select_next', 'fallback' }, + + [''] = { 'scroll_documentation_up', 'fallback' }, + [''] = { 'scroll_documentation_down', 'fallback' }, + + [''] = { 'snippet_forward', 'fallback' }, + [''] = { 'snippet_backward', 'fallback' }, + }, + + super_tab = { + [''] = { 'show', 'show_documentation', 'hide_documentation' }, + [''] = { 'hide', 'fallback' }, + + [''] = { + function(cmp) + if cmp.is_in_snippet() then + return cmp.accept() + else + return cmp.select_and_accept() + end + end, + 'snippet_forward', + 'fallback', + }, + [''] = { 'snippet_backward', 'fallback' }, + + [''] = { 'select_prev', 'fallback' }, + [''] = { 'select_next', 'fallback' }, + [''] = { 'select_prev', 'fallback' }, + [''] = { 'select_next', 'fallback' }, + + [''] = { 'scroll_documentation_up', 'fallback' }, + [''] = { 'scroll_documentation_down', 'fallback' }, + }, + + enter = { + [''] = { 'show', 'show_documentation', 'hide_documentation' }, + [''] = { 'hide', 'fallback' }, + [''] = { 'accept', 'fallback' }, + + [''] = { 'snippet_forward', 'fallback' }, + [''] = { 'snippet_backward', 'fallback' }, + + [''] = { 'select_prev', 'fallback' }, + [''] = { 'select_next', 'fallback' }, + [''] = { 'select_prev', 'fallback' }, + [''] = { 'select_next', 'fallback' }, + + [''] = { 'scroll_documentation_up', 'fallback' }, + [''] = { 'scroll_documentation_down', 'fallback' }, + }, +} + +--- Gets the preset keymap for the given preset name +--- @param name string +--- @return table +function presets.get(name) + local preset = presets[name] + if preset == nil then error('Invalid blink.cmp keymap preset: ' .. name) end + return preset +end + +return presets diff --git a/lua/blink/cmp/sources/lib/async.lua b/lua/blink/cmp/lib/async.lua similarity index 100% rename from lua/blink/cmp/sources/lib/async.lua rename to lua/blink/cmp/lib/async.lua diff --git a/lua/blink/cmp/lib/buffer_events.lua b/lua/blink/cmp/lib/buffer_events.lua new file mode 100644 index 00000000..c69cc062 --- /dev/null +++ b/lua/blink/cmp/lib/buffer_events.lua @@ -0,0 +1,120 @@ +--- Exposes three events (cursor moved, char added, insert leave) for triggers to use. +--- Notably, when "char added" is fired, the "cursor moved" event will not be fired. +--- Unlike in regular neovim, ctrl + c and buffer switching will trigger "insert leave" + +--- @class blink.cmp.BufferEvents +--- @field has_context fun(): boolean +--- @field show_in_snippet boolean +--- @field ignore_next_text_changed boolean +--- @field ignore_next_cursor_moved boolean +--- +--- @field new fun(opts: blink.cmp.BufferEventsOptions): blink.cmp.BufferEvents +--- @field listen fun(self: blink.cmp.BufferEvents, opts: blink.cmp.BufferEventsListener) +--- @field suppress_events_for_callback fun(self: blink.cmp.BufferEvents, cb: fun()) + +--- @class blink.cmp.BufferEventsOptions +--- @field has_context? fun(): boolean +--- @field show_in_snippet? boolean + +--- @class blink.cmp.BufferEventsListener +--- @field on_char_added fun(char: string, is_ignored: boolean) +--- @field on_cursor_moved fun(event: 'CursorMovedI' | 'InsertEnter', is_ignored: boolean) +--- @field on_insert_leave fun() + +--- @type blink.cmp.BufferEvents +--- @diagnostic disable-next-line: missing-fields +local buffer_events = {} + +function buffer_events.new(opts) + return setmetatable({ + has_context = opts.has_context, + show_in_snippet = opts.show_in_snippet or true, + ignore_next_text_changed = false, + ignore_next_cursor_moved = false, + }, { __index = buffer_events }) +end + +--- Normalizes the autocmds + ctrl+c into a common api and handles ignored events +function buffer_events:listen(opts) + local utils = require('blink.cmp.lib.utils') + local snippet = require('blink.cmp.config').snippets + + local last_char = '' + vim.api.nvim_create_autocmd('InsertCharPre', { + callback = function() + if snippet.active() and not self.show_in_snippet and not self.has_context() then return end + last_char = vim.v.char + end, + }) + + vim.api.nvim_create_autocmd('TextChangedI', { + callback = function() + if utils.is_blocked_buffer() then return end + if snippet.active() and not self.show_in_snippet and not self.has_context() then return end + + -- no characters added so let cursormoved handle it + if last_char == '' then return end + + opts.on_char_added(last_char, self.ignore_next_text_changed) + self.ignore_next_text_changed = false + + last_char = '' + end, + }) + + vim.api.nvim_create_autocmd({ 'CursorMovedI', 'InsertEnter' }, { + callback = function(ev) + -- characters added so let textchanged handle it + if last_char ~= '' then return end + + if utils.is_blocked_buffer() then return end + if snippet.active() and not self.show_in_snippet and not self.has_context() then return end + + opts.on_cursor_moved(ev.event, ev.event == 'CursorMovedI' and self.ignore_next_cursor_moved) + if ev.event == 'CursorMovedI' then self.ignore_next_cursor_moved = false end + end, + }) + + -- definitely leaving the context + vim.api.nvim_create_autocmd({ 'InsertLeave', 'BufLeave' }, { + callback = function() + last_char = '' + opts.on_insert_leave() + end, + }) + + -- ctrl+c doesn't trigger InsertLeave so handle it separately + local ctrl_c = vim.api.nvim_replace_termcodes('', true, true, true) + vim.on_key(function(key) + if key == ctrl_c then + vim.schedule(function() + local mode = vim.api.nvim_get_mode().mode + if mode ~= 'i' then + last_char = '' + opts.on_insert_leave() + end + end) + end + end) +end + +--- Suppresses autocmd events for the duration of the callback +--- HACK: there's likely edge cases with this since we can't know for sure +--- if the autocmds will fire for cursor_moved afaik +function buffer_events:suppress_events_for_callback(cb) + local cursor_before = vim.api.nvim_win_get_cursor(0) + local changed_tick_before = vim.api.nvim_buf_get_changedtick(0) + + cb() + + local cursor_after = vim.api.nvim_win_get_cursor(0) + local changed_tick_after = vim.api.nvim_buf_get_changedtick(0) + + local is_insert_mode = vim.api.nvim_get_mode().mode == 'i' + self.ignore_next_text_changed = changed_tick_after ~= changed_tick_before and is_insert_mode + -- TODO: does this guarantee that the CursorMovedI event will fire? + self.ignore_next_cursor_moved = (cursor_after[1] ~= cursor_before[1] or cursor_after[2] ~= cursor_before[2]) + and is_insert_mode +end + +return buffer_events diff --git a/lua/blink/cmp/lib/event_emitter.lua b/lua/blink/cmp/lib/event_emitter.lua new file mode 100644 index 00000000..03a60a33 --- /dev/null +++ b/lua/blink/cmp/lib/event_emitter.lua @@ -0,0 +1,33 @@ +--- @class blink.cmp.EventEmitter : { event: string, autocmd?: string, listeners: table, new: ( fun(event: string, autocmd: string): blink.cmp.EventEmitter ), on: ( fun(self: blink.cmp.EventEmitter, callback: fun(data: T)) ), off: ( fun(self: blink.cmp.EventEmitter, callback: fun(data: T)) ), emit: ( fun(self: blink.cmp.EventEmitter, data?: table) ) }; +--- TODO: is there a better syntax for this? + +local event_emitter = {} + +--- @param event string +--- @param autocmd? string +function event_emitter.new(event, autocmd) + local self = setmetatable({}, { __index = event_emitter }) + self.event = event + self.autocmd = autocmd + self.listeners = {} + return self +end + +function event_emitter:on(callback) table.insert(self.listeners, callback) end + +function event_emitter:off(callback) + for idx, cb in ipairs(self.listeners) do + if cb == callback then table.remove(self.listeners, idx) end + end +end + +function event_emitter:emit(data) + data = data or {} + data.event = self.event + for _, callback in ipairs(self.listeners) do + callback(data) + end + if self.autocmd then vim.api.nvim_exec_autocmds('User', { pattern = self.autocmd, modeline = false, data = data }) end +end + +return event_emitter diff --git a/lua/blink/cmp/accept/text-edits.lua b/lua/blink/cmp/lib/text_edits.lua similarity index 96% rename from lua/blink/cmp/accept/text-edits.lua rename to lua/blink/cmp/lib/text_edits.lua index 39dd5c22..fb89cbba 100644 --- a/lua/blink/cmp/accept/text-edits.lua +++ b/lua/blink/cmp/lib/text_edits.lua @@ -130,12 +130,9 @@ end function text_edits.guess(item) local word = item.insertText or item.label - local cmp_config = config.trigger.completion - local range = require('blink.cmp.utils').get_regex_around_cursor( - cmp_config.keyword_range, - cmp_config.keyword_regex, - cmp_config.exclude_from_prefix_regex - ) + local keyword = config.completion.keyword + local range = + require('blink.cmp.lib.utils').get_regex_around_cursor(keyword.range, keyword.regex, keyword.exclude_from_prefix_regex) local current_line = vim.api.nvim_win_get_cursor(0)[1] -- convert to 0-index diff --git a/lua/blink/cmp/utils.lua b/lua/blink/cmp/lib/utils.lua similarity index 51% rename from lua/blink/cmp/utils.lua rename to lua/blink/cmp/lib/utils.lua index 22481e1c..daf6fe6b 100644 --- a/lua/blink/cmp/utils.lua +++ b/lua/blink/cmp/lib/utils.lua @@ -83,75 +83,4 @@ function utils.get_regex_around_cursor(range, regex, exclude_from_prefix_regex) return { start_col = start_col, length = length } end ---- @param ctx blink.cmp.DrawItemContext ---- @return string|nil -function utils.get_tailwind_hl(ctx) - local doc = ctx.item.documentation - if ctx.kind == 'Color' and doc then - local content = type(doc) == 'string' and doc or doc.value - if content and content:match('^#%x%x%x%x%x%x$') then - local hl_name = 'HexColor' .. content:sub(2) - if #vim.api.nvim_get_hl(0, { name = hl_name }) == 0 then vim.api.nvim_set_hl(0, hl_name, { fg = content }) end - return hl_name - end - end -end - -local PAIRS_AND_INVALID_CHARS = {} -string.gsub('\'"=$()[]<>{} \t\n\r', '.', function(char) PAIRS_AND_INVALID_CHARS[string.byte(char)] = true end) - -local CLOSING_PAIR = { - [string.byte('<')] = string.byte('>'), - [string.byte('[')] = string.byte(']'), - [string.byte('(')] = string.byte(')'), - [string.byte('{')] = string.byte('}'), - [string.byte('"')] = string.byte('"'), - [string.byte("'")] = string.byte("'"), -} - -local ALPHANUMERIC = {} -string.gsub( - 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789', - '.', - function(char) ALPHANUMERIC[string.byte(char)] = true end -) - ---- Gets the prefix of the given text, stopping at brackets and quotes ---- @param text string ---- @return string -function utils.get_prefix_before_brackets_and_quotes(text) - local closing_pairs_stack = {} - local word = '' - - local add = function(char) - word = word .. string.char(char) - - -- if we've seen the opening pair, and we've just received the closing pair, - -- remove it from the closing pairs stack - if closing_pairs_stack[#closing_pairs_stack] == char then - table.remove(closing_pairs_stack, #closing_pairs_stack) - -- if the character is an opening pair, add it to the closing pairs stack - elseif CLOSING_PAIR[char] ~= nil then - table.insert(closing_pairs_stack, CLOSING_PAIR[char]) - end - end - - local has_alphanumeric = false - for i = 1, #text do - local char = string.byte(text, i) - if PAIRS_AND_INVALID_CHARS[char] == nil then - add(char) - has_alphanumeric = has_alphanumeric or ALPHANUMERIC[char] - elseif not has_alphanumeric or #closing_pairs_stack ~= 0 then - add(char) - -- if we had an alphanumeric, and the closing pairs stuck *just* emptied, - -- because the current character is a closing pair, we exit - if has_alphanumeric and #closing_pairs_stack == 0 then break end - else - break - end - end - return word -end - return utils diff --git a/lua/blink/cmp/windows/lib/docs.lua b/lua/blink/cmp/lib/window/docs.lua similarity index 96% rename from lua/blink/cmp/windows/lib/docs.lua rename to lua/blink/cmp/lib/window/docs.lua index 318ac4ba..2248e801 100644 --- a/lua/blink/cmp/windows/lib/docs.lua +++ b/lua/blink/cmp/lib/window/docs.lua @@ -1,3 +1,5 @@ +local highlight_ns = require('blink.cmp.config').appearance.highlight_ns + local docs = {} --- @param bufnr number @@ -30,13 +32,13 @@ function docs.render_detail_and_documentation(bufnr, detail, documentation, max_ vim.api.nvim_set_option_value('modified', false, { buf = bufnr }) -- Highlight with treesitter - vim.api.nvim_buf_clear_namespace(bufnr, require('blink.cmp.config').highlight.ns, 0, -1) + vim.api.nvim_buf_clear_namespace(bufnr, highlight_ns, 0, -1) if #detail_lines > 0 and use_treesitter_highlighting then docs.highlight_with_treesitter(bufnr, vim.bo.filetype, 0, #detail_lines) end -- Only add the separator if there are documentation lines (otherwise only display the detail) if #detail_lines > 0 and #doc_lines > 0 then - vim.api.nvim_buf_set_extmark(bufnr, require('blink.cmp.config').highlight.ns, #detail_lines, 0, { + vim.api.nvim_buf_set_extmark(bufnr, highlight_ns, #detail_lines, 0, { virt_text = { { string.rep('─', max_width) } }, virt_text_pos = 'overlay', hl_eol = true, @@ -101,7 +103,7 @@ function docs.highlight_with_treesitter(bufnr, filetype, start_line, end_line) local conceal = metadata.conceal or metadata[capture] and metadata[capture].conceal if hl and end_row >= line then - vim.api.nvim_buf_set_extmark(bufnr, require('blink.cmp.config').highlight.ns, start_row, start_col, { + vim.api.nvim_buf_set_extmark(bufnr, highlight_ns, start_row, start_col, { end_line = end_row, end_col = end_col, hl_group = hl, diff --git a/lua/blink/cmp/windows/lib/init.lua b/lua/blink/cmp/lib/window/init.lua similarity index 99% rename from lua/blink/cmp/windows/lib/init.lua rename to lua/blink/cmp/lib/window/init.lua index 7d84317d..2ebf4735 100644 --- a/lua/blink/cmp/windows/lib/init.lua +++ b/lua/blink/cmp/lib/window/init.lua @@ -57,7 +57,7 @@ function win.new(config) } if self.config.scrollbar then - self.scrollbar = require('blink.cmp.windows.lib.scrollbar').new({ + self.scrollbar = require('blink.cmp.lib.window.scrollbar').new({ enable_gutter = self.config.border == 'none' or self.config.border == 'padded', }) end diff --git a/lua/blink/cmp/windows/lib/scrollbar/geometry.lua b/lua/blink/cmp/lib/window/scrollbar/geometry.lua similarity index 100% rename from lua/blink/cmp/windows/lib/scrollbar/geometry.lua rename to lua/blink/cmp/lib/window/scrollbar/geometry.lua diff --git a/lua/blink/cmp/windows/lib/scrollbar/init.lua b/lua/blink/cmp/lib/window/scrollbar/init.lua similarity index 88% rename from lua/blink/cmp/windows/lib/scrollbar/init.lua rename to lua/blink/cmp/lib/window/scrollbar/init.lua index 4b3f76d5..81af5d58 100644 --- a/lua/blink/cmp/windows/lib/scrollbar/init.lua +++ b/lua/blink/cmp/lib/window/scrollbar/init.lua @@ -18,7 +18,7 @@ local scrollbar = {} function scrollbar.new(opts) local self = setmetatable({}, { __index = scrollbar }) - self.win = require('blink.cmp.windows.lib.scrollbar.win').new(opts) + self.win = require('blink.cmp.lib.window.scrollbar.win').new(opts) return self end @@ -35,14 +35,14 @@ function scrollbar:mount(target_win) -- ignore if already mounted if self:is_mounted() then return end - local geometry = require('blink.cmp.windows.lib.scrollbar.geometry').get_geometry(target_win) + local geometry = require('blink.cmp.lib.window.scrollbar.geometry').get_geometry(target_win) self.win:show_thumb(geometry.thumb) self.win:show_gutter(geometry.gutter) local function update() if not vim.api.nvim_win_is_valid(target_win) then return self:unmount() end - local updated_geometry = require('blink.cmp.windows.lib.scrollbar.geometry').get_geometry(target_win) + local updated_geometry = require('blink.cmp.lib.window.scrollbar.geometry').get_geometry(target_win) if updated_geometry.should_hide then return self.win:hide() end self.win:show_thumb(updated_geometry.thumb) diff --git a/lua/blink/cmp/windows/lib/scrollbar/win.lua b/lua/blink/cmp/lib/window/scrollbar/win.lua similarity index 100% rename from lua/blink/cmp/windows/lib/scrollbar/win.lua rename to lua/blink/cmp/lib/window/scrollbar/win.lua diff --git a/lua/blink/cmp/signature/init.lua b/lua/blink/cmp/signature/init.lua new file mode 100644 index 00000000..be8d64b5 --- /dev/null +++ b/lua/blink/cmp/signature/init.lua @@ -0,0 +1,23 @@ +local signature = {} + +function signature.setup() + local trigger = require('blink.cmp.signature.trigger').activate() + local window = require('blink.cmp.signature.window').setup() + + local sources = require('blink.cmp.sources.lib') + + trigger.listen_on_show(function(context) + sources.cancel_signature_help() + sources.get_signature_help(context, function(signature_help) + if signature_help ~= nil and trigger.context ~= nil and trigger.context.id == context.id then + trigger.set_active_signature_help(signature_help) + window.open_with_signature_help(context, signature_help) + else + trigger.hide() + end + end) + end) + trigger.listen_on_hide(function() window.close() end) +end + +return signature diff --git a/lua/blink/cmp/signature/list.lua b/lua/blink/cmp/signature/list.lua new file mode 100644 index 00000000..3a07f4a4 --- /dev/null +++ b/lua/blink/cmp/signature/list.lua @@ -0,0 +1 @@ +-- TODO: manage signature help state diff --git a/lua/blink/cmp/signature/trigger.lua b/lua/blink/cmp/signature/trigger.lua new file mode 100644 index 00000000..cec8e148 --- /dev/null +++ b/lua/blink/cmp/signature/trigger.lua @@ -0,0 +1,132 @@ +-- Handles hiding and showing the signature help window. When a user types a trigger character +-- (provided by the sources), we create a new `context`. This can be used downstream to determine +-- if we should make new requests to the sources or not. When a user types a re-trigger character, +-- we update the context's re-trigger counter. +-- TODO: ensure this always calls *after* the completion trigger to avoid increasing latency + +--- @class blink.cmp.SignatureHelpContext +--- @field id number +--- @field bufnr number +--- @field cursor number[] +--- @field line string +--- @field is_retrigger boolean +--- @field active_signature_help lsp.SignatureHelp | nil + +--- @class blink.cmp.SignatureTrigger +--- @field current_context_id number +--- @field context? blink.cmp.SignatureHelpContext +--- @field show_emitter blink.cmp.EventEmitter<{ context: blink.cmp.SignatureHelpContext }> +--- @field hide_emitter blink.cmp.EventEmitter<{}> +--- @field buffer_events? blink.cmp.BufferEvents +--- +--- @field activate fun() +--- @field is_trigger_character fun(char: string, is_retrigger?: boolean): boolean +--- @field show_if_on_trigger_character fun(): boolean +--- @field show fun(opts?: { trigger_character: string }) +--- @field hide fun() + +local config = require('blink.cmp.config').signature.trigger +local utils = require('blink.cmp.lib.utils') + +--- @type blink.cmp.SignatureTrigger +--- @diagnostic disable-next-line: missing-fields +local trigger = { + current_context_id = -1, + --- @type blink.cmp.SignatureHelpContext | nil + context = nil, + show_emitter = require('blink.cmp.lib.event_emitter').new('signature_help_show'), + hide_emitter = require('blink.cmp.lib.event_emitter').new('signature_help_hide'), +} + +function trigger.activate() + trigger.buffer_events = require('blink.cmp.lib.buffer_events').new({ + show_in_snippet = true, + has_context = function() return trigger.context ~= nil end, + }) + trigger.buffer_events:listen({ + on_char_added = function(char) + local is_on_trigger = trigger.is_trigger_character(char) + local is_on_retrigger = trigger.is_trigger_character(char, true) + + -- ignore if in a special buffer + if utils.is_blocked_buffer() then + return trigger.hide() + -- character forces a trigger according to the sources, refresh the existing context if it exists + elseif is_on_trigger then + return trigger.show({ trigger_character = char }) + -- character forces a re-trigger according to the sources, show if we have a context + elseif is_on_retrigger and trigger.context ~= nil then + return trigger.show() + end + end, + on_cursor_moved = function(event) + local cursor_col = vim.api.nvim_win_get_cursor(0)[2] + local char_under_cursor = vim.api.nvim_get_current_line():sub(cursor_col, cursor_col) + local is_on_trigger = trigger.is_trigger_character(char_under_cursor) + + if config.show_on_insert_on_trigger_character and is_on_trigger and event == 'InsertEnter' then + trigger.show({ trigger_character = char_under_cursor }) + elseif event == 'CursorMovedI' and trigger.context ~= nil then + trigger.show() + end + end, + on_insert_leave = function() trigger.hide() end, + }) +end + +function trigger.is_trigger_character(char, is_retrigger) + local res = require('blink.cmp.sources.lib').get_signature_help_trigger_characters() + local trigger_characters = is_retrigger and res.retrigger_characters or res.trigger_characters + local is_trigger = vim.tbl_contains(trigger_characters, char) + + local blocked_trigger_characters = is_retrigger and config.blocked_retrigger_characters + or config.blocked_trigger_characters + local is_blocked = vim.tbl_contains(blocked_trigger_characters, char) + + return is_trigger and not is_blocked +end + +function trigger.show_if_on_trigger_character() + local cursor_col = vim.api.nvim_win_get_cursor(0)[2] + local char_under_cursor = vim.api.nvim_get_current_line():sub(cursor_col, cursor_col) + local is_on_trigger = trigger.is_trigger_character(char_under_cursor) + if is_on_trigger then trigger.show({ trigger_character = char_under_cursor }) end + return is_on_trigger +end + +function trigger.show(opts) + opts = opts or {} + + -- update context + local cursor = vim.api.nvim_win_get_cursor(0) + if trigger.context == nil then trigger.current_context_id = trigger.current_context_id + 1 end + trigger.context = { + id = trigger.current_context_id, + bufnr = vim.api.nvim_get_current_buf(), + cursor = cursor, + line = vim.api.nvim_buf_get_lines(0, cursor[1] - 1, cursor[1], false)[1], + trigger = { + kind = opts.trigger_character and vim.lsp.protocol.CompletionTriggerKind.TriggerCharacter + or vim.lsp.protocol.CompletionTriggerKind.Invoked, + character = opts.trigger_character, + }, + is_retrigger = trigger.context ~= nil, + active_signature_help = trigger.context and trigger.context.active_signature_help or nil, + } + + trigger.show_emitter:emit({ context = trigger.context }) +end + +function trigger.hide() + if not trigger.context then return end + + trigger.context = nil + trigger.hide_emitter:emit() +end + +function trigger.set_active_signature_help(signature_help) + if not trigger.context then return end + trigger.context.active_signature_help = signature_help +end + +return trigger diff --git a/lua/blink/cmp/windows/signature.lua b/lua/blink/cmp/signature/window.lua similarity index 69% rename from lua/blink/cmp/windows/signature.lua rename to lua/blink/cmp/signature/window.lua index f0d200dc..908f06ab 100644 --- a/lua/blink/cmp/windows/signature.lua +++ b/lua/blink/cmp/signature/window.lua @@ -1,10 +1,19 @@ -local config = require('blink.cmp.config').windows.signature_help +--- @class blink.cmp.SignatureWindow +--- @field win blink.cmp.Window +--- @field context? blink.cmp.SignatureHelpContext +--- +--- @field open_with_signature_help fun(context: blink.cmp.SignatureHelpContext, signature_help?: lsp.SignatureHelp) +--- @field close fun() +--- @field scroll_up fun(amount: number) +--- @field scroll_down fun(amount: number) +--- @field update_position fun() + +local config = require('blink.cmp.config').signature.window local sources = require('blink.cmp.sources.lib') -local autocomplete = require('blink.cmp.windows.autocomplete') -local signature = {} +local menu = require('blink.cmp.completion.windows.menu') -function signature.setup() - signature.win = require('blink.cmp.windows.lib').new({ +local signature = { + win = require('blink.cmp.lib.window').new({ min_width = config.min_width, max_width = config.max_width, max_height = config.max_height, @@ -13,21 +22,17 @@ function signature.setup() winhighlight = config.winhighlight, scrollbar = config.scrollbar, wrap = true, - }) - - -- todo: deduplicate this - autocomplete.listen_on_position_update(function() + }), + context = nil, +} + +-- todo: deduplicate this +menu.position_update_emitter:on(function() signature.update_position(signature.context) end) +vim.api.nvim_create_autocmd({ 'CursorMovedI', 'WinScrolled', 'WinResized' }, { + callback = function() if signature.context then signature.update_position(signature.context) end - end) - - vim.api.nvim_create_autocmd({ 'CursorMovedI', 'WinScrolled', 'WinResized' }, { - callback = function() - if signature.context then signature.update_position(signature.context) end - end, - }) - - return signature -end + end, +}) --- @param context blink.cmp.SignatureHelpContext --- @param signature_help lsp.SignatureHelp | nil @@ -47,7 +52,7 @@ function signature.open_with_signature_help(context, signature_help) local active_signature = signature_help.signatures[(signature_help.activeSignature or 0) + 1] if signature.shown_signature ~= active_signature then - require('blink.cmp.windows.lib.docs').render_detail_and_documentation( + require('blink.cmp.lib.window.docs').render_detail_and_documentation( signature.win:get_buf(), active_signature.label, active_signature.documentation, @@ -113,12 +118,12 @@ function signature.update_position() local direction_priority = config.direction_priority - -- if the autocomplete window is open, we want to place the signature window on the opposite side - local autocomplete_win_config = autocomplete.win:get_win() and vim.api.nvim_win_get_config(autocomplete.win:get_win()) - if autocomplete.win:is_open() then + -- if the menu window is open, we want to place the signature window on the opposite side + local menu_win_config = menu.win:get_win() and vim.api.nvim_win_get_config(menu.win:get_win()) + if menu.win:is_open() then local cursor_screen_row = vim.fn.winline() - local autocomplete_win_is_up = autocomplete_win_config.row - cursor_screen_row < 0 - direction_priority = autocomplete_win_is_up and { 's' } or { 'n' } + local menu_win_is_up = menu_win_config.row - cursor_screen_row < 0 + direction_priority = menu_win_is_up and { 's' } or { 'n' } end local pos = win:get_vertical_direction_and_height(direction_priority) @@ -134,16 +139,15 @@ function signature.update_position() local height = win:get_height() -- default to the user's preference but attempt to use the other options - if autocomplete_win_config then - assert(autocomplete_win_config.relative == 'win', 'The autocomplete window must be relative to a window') + if menu_win_config then + assert(menu_win_config.relative == 'win', 'The menu window must be relative to a window') local cursor_screen_row = vim.fn.winline() - local autocomplete_win_is_up = autocomplete_win_config.row - cursor_screen_row < 0 + local menu_win_is_up = menu_win_config.row - cursor_screen_row < 0 vim.api.nvim_win_set_config(winnr, { - relative = autocomplete_win_config.relative, - win = autocomplete_win_config.win, - row = autocomplete_win_is_up and autocomplete_win_config.row + autocomplete.win:get_height() + 1 - or autocomplete_win_config.row - height - 1, - col = autocomplete_win_config.col, + relative = menu_win_config.relative, + win = menu_win_config.win, + row = menu_win_is_up and menu_win_config.row + menu.win:get_height() + 1 or menu_win_config.row - height - 1, + col = menu_win_config.col, }) else vim.api.nvim_win_set_config(winnr, { relative = 'cursor', row = pos.direction == 's' and 1 or -height, col = 0 }) diff --git a/lua/blink/cmp/snippets.lua b/lua/blink/cmp/snippets.lua new file mode 100644 index 00000000..e69de29b diff --git a/lua/blink/cmp/sources/lib/context.lua b/lua/blink/cmp/sources/lib/context.lua index 3808eec5..f196fcc8 100644 --- a/lua/blink/cmp/sources/lib/context.lua +++ b/lua/blink/cmp/sources/lib/context.lua @@ -1,5 +1,5 @@ local utils = require('blink.cmp.sources.lib.utils') -local async = require('blink.cmp.sources.lib.async') +local async = require('blink.cmp.lib.async') --- @class blink.cmp.SourcesContext --- @field id number @@ -84,7 +84,7 @@ function sources_context:get_completions_for_sources(sources, context) and vim.tbl_contains(source:get_trigger_characters(), context.trigger.character) -- The TriggerForIncompleteCompletions kind is handled by the source provider itself - local source_context = require('blink.cmp.utils').shallow_copy(context) + 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 } diff --git a/lua/blink/cmp/sources/lib/init.lua b/lua/blink/cmp/sources/lib/init.lua index 29d082ff..0524e547 100644 --- a/lua/blink/cmp/sources/lib/init.lua +++ b/lua/blink/cmp/sources/lib/init.lua @@ -1,4 +1,4 @@ -local async = require('blink.cmp.sources.lib.async') +local async = require('blink.cmp.lib.async') local config = require('blink.cmp.config') --- @class blink.cmp.Sources @@ -6,11 +6,12 @@ local config = require('blink.cmp.config') --- @field current_signature_help blink.cmp.Task | nil --- @field sources_registered boolean --- @field providers table ---- @field on_completions_callback fun(context: blink.cmp.Context, enabled_sources: table, responses: table) +--- @field completions_emitter blink.cmp.EventEmitter --- ---- @field register fun() --- @field get_enabled_providers fun(context?: blink.cmp.Context): table --- @field get_trigger_characters fun(): string[] +--- +--- @field emit_completions fun(context: blink.cmp.Context, enabled_sources: table, responses: table) --- @field request_completions fun(context: blink.cmp.Context) --- @field cancel_completions fun() --- @field listen_on_completions fun(callback: fun(context: blink.cmp.Context, items: blink.cmp.CompletionItem[])) @@ -18,30 +19,26 @@ local config = require('blink.cmp.config') --- @field resolve fun(item: blink.cmp.CompletionItem): blink.cmp.Task --- @field should_execute fun(item: blink.cmp.CompletionItem): boolean --- @field execute fun(context: blink.cmp.Context, item: blink.cmp.CompletionItem): blink.cmp.Task +--- --- @field get_signature_help_trigger_characters fun(): { trigger_characters: string[], retrigger_characters: string[] } ---- @field get_signature_help fun(context: blink.cmp.SignatureHelpContext, callback: fun(signature_help: lsp.SignatureHelp | nil)): (fun(): nil) | nil +--- @field get_signature_help fun(context: blink.cmp.SignatureHelpContext, callback: fun(signature_help: lsp.SignatureHelp | nil)) --- @field cancel_signature_help fun() +--- --- @field reload fun() --- @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 blink.cmp.CompletionItem[] + --- @type blink.cmp.Sources --- @diagnostic disable-next-line: missing-fields local sources = { current_context = nil, - sources_registered = false, providers = {}, - on_completions_callback = function(_, _) end, + completions_emitter = require('blink.cmp.lib.event_emitter').new('source_completions', 'BlinkCmpSourceCompletions'), } -function sources.register() - assert(not sources.sources_registered, 'Sources have already been registered') - sources.sources_registered = true - - for key, source_config in pairs(config.sources.providers) do - sources.providers[key] = require('blink.cmp.sources.lib.provider').new(key, source_config) - end -end - function sources.get_enabled_providers(context) local mode_providers = type(config.sources.completion.enabled_providers) == 'function' and config.sources.completion.enabled_providers(context) @@ -50,12 +47,17 @@ function sources.get_enabled_providers(context) for _, provider in ipairs(mode_providers) do assert( - sources.providers[provider] ~= nil, + sources.providers[provider] ~= nil or config.sources.providers[provider] ~= nil, 'Requested provider "' .. provider .. '" 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] then + sources.providers[provider] = + require('blink.cmp.sources.lib.provider').new(provider, config.sources.providers[provider]) + end end --- @type table @@ -70,31 +72,21 @@ end function sources.get_trigger_characters() local providers = sources.get_enabled_providers() - local blocked_trigger_characters = {} - for _, char in ipairs(config.trigger.completion.blocked_trigger_characters) do - blocked_trigger_characters[char] = true - end - local trigger_characters = {} for _, source in pairs(providers) do - local source_trigger_characters = source:get_trigger_characters() - for _, char in ipairs(source_trigger_characters) do - if not blocked_trigger_characters[char] then table.insert(trigger_characters, char) end - end + vim.list_extend(trigger_characters, source:get_trigger_characters()) end return trigger_characters end -function sources.listen_on_completions(callback) - sources.on_completions_callback = function(context, enabled_sources, responses) - local items = {} - for id, response in pairs(responses) do - if sources.providers[id]:should_show_items(context, enabled_sources, response.items) then - vim.list_extend(items, response.items) - end +function sources.emit_completions(context, enabled_sources, responses) + local items = {} + for id, response in pairs(responses) do + if sources.providers[id]:should_show_items(context, enabled_sources, response.items) then + vim.list_extend(items, response.items) end - callback(context, items) end + sources.completions_emitter:emit({ context = context, items = items }) end function sources.request_completions(context) @@ -105,13 +97,14 @@ function sources.request_completions(context) sources.current_context = require('blink.cmp.sources.lib.context').new( context, sources.get_enabled_providers(context), - sources.on_completions_callback + sources.emit_completions ) -- send cached completions if they exist to immediately trigger updates elseif sources.current_context:get_cached_completions() ~= nil then - sources.on_completions_callback( + sources.emit_completions( context, sources.current_context:get_sources(), + --- @diagnostic disable-next-line: param-type-mismatch sources.current_context:get_cached_completions() ) end @@ -196,27 +189,14 @@ end --- Signature help --- function sources.get_signature_help_trigger_characters() - local blocked_trigger_characters = {} - local blocked_retrigger_characters = {} - for _, char in ipairs(config.trigger.signature_help.blocked_trigger_characters) do - blocked_trigger_characters[char] = true - end - for _, char in ipairs(config.trigger.signature_help.blocked_retrigger_characters) do - blocked_retrigger_characters[char] = true - end - local trigger_characters = {} local retrigger_characters = {} - -- todo: should this be all source groups? - for _, source in pairs(sources.providers) do + -- todo: should this be all sources? or should it follow fallbacks? + for _, source in pairs(sources.get_enabled_providers()) do local res = source:get_signature_help_trigger_characters() - for _, char in ipairs(res.trigger_characters) do - if not blocked_trigger_characters[char] then table.insert(trigger_characters, char) end - end - for _, char in ipairs(res.retrigger_characters) do - if not blocked_retrigger_characters[char] then table.insert(retrigger_characters, char) end - end + 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 @@ -226,6 +206,7 @@ function sources.get_signature_help(context, callback) 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(tasks_results) local signature_helps = {} for _, task_result in ipairs(tasks_results) do diff --git a/lua/blink/cmp/sources/lib/provider/init.lua b/lua/blink/cmp/sources/lib/provider/init.lua index cd4644c5..1866d113 100644 --- a/lua/blink/cmp/sources/lib/provider/init.lua +++ b/lua/blink/cmp/sources/lib/provider/init.lua @@ -15,7 +15,7 @@ --- @field resolve fun(self: blink.cmp.SourceProvider, item: blink.cmp.CompletionItem): blink.cmp.Task --- @field should_execute fun(self: blink.cmp.SourceProvider, item: blink.cmp.CompletionItem): boolean --- @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): string[] +--- @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 @@ -24,7 +24,7 @@ local source = {} local utils = require('blink.cmp.sources.lib.utils') -local async = require('blink.cmp.sources.lib.async') +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') @@ -65,7 +65,7 @@ function source:get_completions(context, enabled_sources) -- and the data doesn't need to be updated if self.last_response ~= nil and self.last_response.context.id == context.id then if utils.should_run_request(context, self.last_response) == false then - return async.task.new(function(resolve) resolve(require('blink.cmp.utils').shallow_copy(self.last_response)) end) + return async.task.new(function(resolve) resolve(require('blink.cmp.lib.utils').shallow_copy(self.last_response)) end) end end @@ -92,7 +92,7 @@ function source:get_completions(context, enabled_sources) response.items = self.config.transform_items(context, response.items) end - self.last_response = require('blink.cmp.utils').shallow_copy(response) + self.last_response = require('blink.cmp.lib.utils').shallow_copy(response) self.last_response.is_cached = true return response end) diff --git a/lua/blink/cmp/sources/path/fs.lua b/lua/blink/cmp/sources/path/fs.lua index 77567be5..c3fe0909 100644 --- a/lua/blink/cmp/sources/path/fs.lua +++ b/lua/blink/cmp/sources/path/fs.lua @@ -1,4 +1,4 @@ -local async = require('blink.cmp.sources.lib.async') +local async = require('blink.cmp.lib.async') local uv = vim.uv local fs = {} diff --git a/lua/blink/cmp/trigger/signature.lua b/lua/blink/cmp/trigger/signature.lua deleted file mode 100644 index eea855dc..00000000 --- a/lua/blink/cmp/trigger/signature.lua +++ /dev/null @@ -1,136 +0,0 @@ --- Handles hiding and showing the signature help window. When a user types a trigger character --- (provided by the sources), we create a new `context`. This can be used downstream to determine --- if we should make new requests to the sources or not. When a user types a re-trigger character, --- we update the context's re-trigger counter. - --- TODO: ensure this always calls *after* the completion trigger to avoid increasing latency - -local config = require('blink.cmp.config').trigger.signature_help -local sources = require('blink.cmp.sources.lib') -local utils = require('blink.cmp.utils') - -local trigger = { - current_context_id = -1, - --- @type blink.cmp.SignatureHelpContext | nil - context = nil, - event_targets = { - --- @type fun(context: blink.cmp.SignatureHelpContext) - on_show = function() end, - --- @type fun() - on_hide = function() end, - }, -} - -function trigger.activate_autocmds() - local last_chars = {} - vim.api.nvim_create_autocmd('InsertCharPre', { - callback = function() table.insert(last_chars, vim.v.char) end, - }) - - -- decide if we should show the completion window - vim.api.nvim_create_autocmd('TextChangedI', { - callback = function() - -- no characters added so let cursormoved handle it - if #last_chars == 0 then return end - - local res = sources.get_signature_help_trigger_characters() - local trigger_characters = res.trigger_characters - local retrigger_characters = res.retrigger_characters - - for _, last_char in ipairs(last_chars) do - -- ignore if in a special buffer - if utils.is_blocked_buffer() then - trigger.hide() - break - -- character forces a trigger according to the sources, refresh the existing context if it exists - elseif vim.tbl_contains(trigger_characters, last_char) then - trigger.show({ trigger_character = last_char }) - break - -- character forces a re-trigger according to the sources, show if we have a context - elseif vim.tbl_contains(retrigger_characters, last_char) and trigger.context ~= nil then - trigger.show() - break - end - end - - last_chars = {} - end, - }) - - -- check if we've moved outside of the context by diffing against the query boundary - vim.api.nvim_create_autocmd({ 'CursorMovedI', 'InsertEnter' }, { - callback = function(ev) - if utils.is_blocked_buffer() then return end - - -- characters added so let textchanged handle it - if #last_chars ~= 0 then return end - - local cursor_col = vim.api.nvim_win_get_cursor(0)[2] - local char_under_cursor = vim.api.nvim_get_current_line():sub(cursor_col, cursor_col) - local is_on_trigger = - vim.tbl_contains(sources.get_signature_help_trigger_characters().trigger_characters, char_under_cursor) - - if config.show_on_insert_on_trigger_character and is_on_trigger and ev.event == 'InsertEnter' then - trigger.show({ trigger_character = char_under_cursor }) - elseif ev.event == 'CursorMovedI' and trigger.context ~= nil then - trigger.show() - end - end, - }) - - -- definitely leaving the context - vim.api.nvim_create_autocmd({ 'InsertLeave', 'BufLeave' }, { callback = trigger.hide }) - - return trigger -end - -function trigger.show_if_on_trigger_character() - local cursor_col = vim.api.nvim_win_get_cursor(0)[2] - local char_under_cursor = vim.api.nvim_get_current_line():sub(cursor_col, cursor_col) - local is_on_trigger = - vim.tbl_contains(sources.get_signature_help_trigger_characters().trigger_characters, char_under_cursor) - if is_on_trigger then trigger.show({ trigger_character = char_under_cursor }) end - return is_on_trigger -end - ---- @param opts { trigger_character: string } | nil -function trigger.show(opts) - opts = opts or {} - - -- update context - local cursor = vim.api.nvim_win_get_cursor(0) - if trigger.context == nil then trigger.current_context_id = trigger.current_context_id + 1 end - trigger.context = { - id = trigger.current_context_id, - bufnr = vim.api.nvim_get_current_buf(), - cursor = cursor, - line = vim.api.nvim_buf_get_lines(0, cursor[1] - 1, cursor[1], false)[1], - trigger = { - kind = opts.trigger_character and vim.lsp.protocol.CompletionTriggerKind.TriggerCharacter - or vim.lsp.protocol.CompletionTriggerKind.Invoked, - character = opts.trigger_character, - }, - is_retrigger = trigger.context ~= nil, - active_signature_help = trigger.context and trigger.context.active_signature_help or nil, - } - - trigger.event_targets.on_show(trigger.context) -end - -function trigger.listen_on_show(callback) trigger.event_targets.on_show = callback end - -function trigger.hide() - if not trigger.context then return end - - trigger.context = nil - trigger.event_targets.on_hide() -end - -function trigger.listen_on_hide(callback) trigger.event_targets.on_hide = callback end - -function trigger.set_active_signature_help(signature_help) - if not trigger.context then return end - trigger.context.active_signature_help = signature_help -end - -return trigger diff --git a/lua/blink/cmp/trigger/types.lua b/lua/blink/cmp/trigger/types.lua deleted file mode 100644 index d356cc1b..00000000 --- a/lua/blink/cmp/trigger/types.lua +++ /dev/null @@ -1,22 +0,0 @@ ---- @class blink.cmp.ContextBounds ---- @field line string ---- @field line_number number ---- @field start_col number ---- @field end_col number ---- @field length number - ---- @class blink.cmp.Context ---- @field id number ---- @field bufnr number ---- @field cursor number[] ---- @field line string ---- @field bounds blink.cmp.ContextBounds ---- @field trigger { kind: number, character: string | nil } - ---- @class blink.cmp.SignatureHelpContext ---- @field id number ---- @field bufnr number ---- @field cursor number[] ---- @field line string ---- @field is_retrigger boolean ---- @field active_signature_help lsp.SignatureHelp | nil diff --git a/lua/blink/cmp/windows/autocomplete.lua b/lua/blink/cmp/windows/autocomplete.lua deleted file mode 100644 index d6d6353f..00000000 --- a/lua/blink/cmp/windows/autocomplete.lua +++ /dev/null @@ -1,306 +0,0 @@ ---- @class blink.cmp.CompletionWindowEventTargets ---- @field on_open table ---- @field on_close table ---- @field on_position_update table ---- @field on_select table - ---- @class blink.cmp.CompletionWindow ---- @field win blink.cmp.Window ---- @field items blink.cmp.CompletionItem[] ---- @field renderer blink.cmp.Renderer ---- @field has_selected? boolean ---- @field auto_show boolean ---- @field context blink.cmp.Context? ---- @field event_targets blink.cmp.CompletionWindowEventTargets ---- @field preview_undo_text_edit? lsp.TextEdit ---- @field preview_context_id? number ---- ---- @field setup fun(): blink.cmp.CompletionWindow ---- ---- @field open_with_items fun(context: blink.cmp.Context, items: blink.cmp.CompletionItem[]) ---- @field open fun() ---- @field close fun() ---- @field listen_on_open fun(callback: fun()) ---- @field listen_on_close fun(callback: fun()) ---- ---- @field update_position fun(context: blink.cmp.Context) ---- @field listen_on_position_update fun(callback: fun()) ---- ---- @field accept fun(): boolean? ---- @field undo_preview fun() ---- ---- @field select fun(line: number, skip_auto_insert?: boolean) ---- @field select_next fun(opts?: { skip_auto_insert?: boolean }) ---- @field select_prev fun(opts?: { skip_auto_insert?: boolean }) ---- @field get_selected_item fun(): blink.cmp.CompletionItem? ---- @field set_has_selected fun(selected: boolean) ---- @field listen_on_select fun(callback: fun(item: blink.cmp.CompletionItem?, context: blink.cmp.Context)) ---- @field emit_on_select fun(item: blink.cmp.CompletionItem?, context: blink.cmp.Context) - -local config = require('blink.cmp.config') -local text_edits_lib = require('blink.cmp.accept.text-edits') -local autocmp_config = config.windows.autocomplete - ---- @type blink.cmp.CompletionWindow ---- @diagnostic disable-next-line: missing-fields -local autocomplete = { - items = {}, - has_selected = nil, - -- hack: ideally this doesn't get mutated by the public API - auto_show = autocmp_config.auto_show, - context = nil, - event_targets = { - on_position_update = {}, - on_select = {}, - on_close = {}, - on_open = {}, - }, -} - -function autocomplete.setup() - autocomplete.win = require('blink.cmp.windows.lib').new({ - min_width = autocmp_config.min_width, - max_height = autocmp_config.max_height, - border = autocmp_config.border, - winblend = autocmp_config.winblend, - winhighlight = autocmp_config.winhighlight, - cursorline = false, - scrolloff = autocmp_config.scrolloff, - scrollbar = autocmp_config.scrollbar, - }) - - vim.api.nvim_create_autocmd({ 'CursorMovedI', 'WinScrolled', 'WinResized' }, { - callback = function() - if autocomplete.context ~= nil then autocomplete.update_position(autocomplete.context) end - end, - }) - - -- prefetch the resolved item - local last_context_id = nil - local last_request = nil - local timer = vim.uv.new_timer() - autocomplete.listen_on_select(function(item, context) - if not item then return end - - local resolve = vim.schedule_wrap(function() - if last_request ~= nil then last_request:cancel() end - last_request = require('blink.cmp.sources.lib').resolve(item) - end) - - -- immediately resolve if the context has changed - if last_context_id ~= context.id then - last_context_id = context.id - resolve() - end - - -- otherwise, wait for the debounce period - timer:stop() - timer:start(50, 0, resolve) - end) - - return autocomplete -end - ----------- Visibility ---------- - ---- @param context blink.cmp.Context ---- @param items blink.cmp.CompletionItem[] -function autocomplete.open_with_items(context, items) - autocomplete.context = context - autocomplete.items = items - if not autocomplete.renderer then - autocomplete.renderer = require('blink.cmp.windows.render').new(autocmp_config.draw) - end - autocomplete.renderer:draw(autocomplete.win:get_buf(), items) - - vim.iter(autocomplete.event_targets.on_open):each(function(callback) callback() end) - - if not autocomplete.auto_show then return end - - autocomplete.win:open() - autocomplete.update_position(context) - - -- it's possible for the window to close after updating the position - -- if there was nowhere to place the window - if not autocomplete.win:is_open() then return end - - autocomplete.set_has_selected(autocmp_config.selection == 'preselect') - - -- todo: some logic to maintain the selection if the user moved the cursor? - vim.api.nvim_win_set_cursor(autocomplete.win:get_win(), { 1, 0 }) - - autocomplete.emit_on_select(autocomplete.get_selected_item(), context) -end - -function autocomplete.open() - if autocomplete.win:is_open() then return end - - autocomplete.win:open() - autocomplete.set_has_selected(autocmp_config.selection == 'preselect') - - vim.iter(autocomplete.event_targets.on_open):each(function(callback) callback() end) -end - -function autocomplete.close() - if not autocomplete.win:is_open() then return end - autocomplete.auto_show = autocmp_config.auto_show - autocomplete.win:close() - autocomplete.set_has_selected(autocmp_config.selection == 'preselect') - - vim.iter(autocomplete.event_targets.on_close):each(function(callback) callback() end) -end - ---- Add a listener for when the autocomplete window opens ---- This is useful for hiding GitHub Copilot ghost text and similar functionality. -function autocomplete.listen_on_open(callback) table.insert(autocomplete.event_targets.on_open, callback) end - ---- Add a listener for when the autocomplete window closes -function autocomplete.listen_on_close(callback) table.insert(autocomplete.event_targets.on_close, callback) end - ---- TODO: Don't switch directions if the context is the same -function autocomplete.update_position(context) - local win = autocomplete.win - if not win:is_open() then return end - local winnr = win:get_win() - - win:update_size() - - local border_size = win:get_border_size() - local pos = win:get_vertical_direction_and_height(autocmp_config.direction_priority) - - -- couldn't find anywhere to place the window - if not pos then - win:close() - return - end - - local start_col = autocomplete.renderer:get_alignment_start_col() - - -- place the window at the start col of the current text we're fuzzy matching against - -- so the window doesnt move around as we type - local cursor_col = vim.api.nvim_win_get_cursor(0)[2] - local col = context.bounds.start_col - cursor_col - (context.bounds.length == 0 and 0 or 1) - border_size.left - local row = pos.direction == 's' and 1 or -pos.height - border_size.vertical - vim.api.nvim_win_set_config(winnr, { relative = 'cursor', row = row, col = col - start_col }) - vim.api.nvim_win_set_height(winnr, pos.height) - - for _, callback in ipairs(autocomplete.event_targets.on_position_update) do - callback() - end -end - -function autocomplete.listen_on_position_update(callback) - table.insert(autocomplete.event_targets.on_position_update, callback) -end - ----------- Selection/Accept ---------- - -function autocomplete.accept() - local context = autocomplete.context - if context == nil then return end - - local selected_item = autocomplete.get_selected_item() - if selected_item == nil then return end - - -- undo the preview if it exists - if autocomplete.preview_undo_text_edit ~= nil and autocomplete.preview_context_id == autocomplete.context.id then - text_edits_lib.apply({ autocomplete.preview_undo_text_edit }) - autocomplete.preview_undo_text_edit = nil - autocomplete.preview_context_id = nil - end - - -- apply - require('blink.cmp.accept')(context, selected_item) - return true -end - -function autocomplete.undo_preview() - if autocomplete.preview_undo_text_edit ~= nil and autocomplete.preview_context_id == autocomplete.context.id then - text_edits_lib.apply({ autocomplete.preview_undo_text_edit }) - autocomplete.preview_undo_text_edit = nil - autocomplete.preview_context_id = nil - end -end - -function autocomplete.select(line, skip_auto_insert) - autocomplete.set_has_selected(true) - vim.api.nvim_win_set_cursor(autocomplete.win:get_win(), { line, 0 }) - - local selected_item = autocomplete.get_selected_item() - - -- when auto_insert is enabled, we immediately apply the text edit - if config.windows.autocomplete.selection == 'auto_insert' and selected_item ~= nil and not skip_auto_insert then - require('blink.cmp.trigger.completion').suppress_events_for_callback(function() - -- undo the previous preview if it exists - if autocomplete.preview_context_id == autocomplete.context.id and autocomplete.preview_undo_text_edit ~= nil then - require('blink.cmp.accept.text-edits').apply({ autocomplete.preview_undo_text_edit }) - end - - autocomplete.preview_undo_text_edit = require('blink.cmp.accept.preview')(selected_item) - autocomplete.preview_context_id = autocomplete.context.id - end) - end - - autocomplete.emit_on_select(selected_item, autocomplete.context) -end - -function autocomplete.select_next(opts) - if not autocomplete.win:is_open() then return end - - local cycle_from_bottom = config.windows.autocomplete.cycle.from_bottom - local l = #autocomplete.items - local line = vim.api.nvim_win_get_cursor(autocomplete.win:get_win())[1] - -- We need to ajust the disconnect between the line position - -- on the window and the selected item - if not autocomplete.has_selected then line = line - 1 end - if autocomplete.has_selected and l == 1 then return end - if line == l then - -- at the end of completion list and the config is not enabled: do nothing - if not cycle_from_bottom then return end - line = 1 - else - line = line + 1 - end - - autocomplete.select(line, opts and opts.skip_auto_insert) -end - -function autocomplete.select_prev(opts) - if not autocomplete.win:is_open() then return end - - local cycle_from_top = config.windows.autocomplete.cycle.from_top - local l = #autocomplete.items - local line = vim.api.nvim_win_get_cursor(autocomplete.win:get_win())[1] - if autocomplete.has_selected and l == 1 then return end - if line <= 1 then - if not cycle_from_top then return end - line = l - else - line = line - 1 - end - - autocomplete.select(line, opts and opts.skip_auto_insert) -end - -function autocomplete.get_selected_item() - if not autocomplete.win:is_open() then return end - if not autocomplete.has_selected then return end - local line = vim.api.nvim_win_get_cursor(autocomplete.win:get_win())[1] - return autocomplete.items[line] -end - -function autocomplete.set_has_selected(selected) - if not autocomplete.win:is_open() then return end - autocomplete.has_selected = selected - autocomplete.win:set_option_value('cursorline', selected) -end - -function autocomplete.listen_on_select(callback) table.insert(autocomplete.event_targets.on_select, callback) end - -function autocomplete.emit_on_select(item, context) - for _, callback in ipairs(autocomplete.event_targets.on_select) do - callback(item, context) - end -end - -return autocomplete diff --git a/lua/blink/cmp/windows/documentation.lua b/lua/blink/cmp/windows/documentation.lua deleted file mode 100644 index a0e82ada..00000000 --- a/lua/blink/cmp/windows/documentation.lua +++ /dev/null @@ -1,193 +0,0 @@ -local config = require('blink.cmp.config').windows.documentation -local sources = require('blink.cmp.sources.lib') -local autocomplete = require('blink.cmp.windows.autocomplete') -local signature = require('blink.cmp.windows.signature') -local docs = {} - -function docs.setup() - docs.win = require('blink.cmp.windows.lib').new({ - min_width = config.min_width, - max_width = config.max_width, - max_height = config.max_height, - border = config.border, - winblend = config.winblend, - winhighlight = config.winhighlight, - scrollbar = config.scrollbar, - wrap = true, - }) - - autocomplete.listen_on_position_update(function() - if autocomplete.win:is_open() then docs.update_position() end - end) - - local timer = vim.uv.new_timer() - local last_context_id = nil - autocomplete.listen_on_select(function(item, context) - timer:stop() - if docs.win:is_open() or context.id == last_context_id then - last_context_id = context.id - timer:start(config.update_delay_ms, 0, function() - vim.schedule(function() docs.show_item(item) end) - end) - elseif config.auto_show then - timer:start(config.auto_show_delay_ms, 0, function() - last_context_id = context.id - vim.schedule(function() docs.show_item(item) end) - end) - end - end) - autocomplete.listen_on_close(function() docs.win:close() end) - - return docs -end - -function docs.show_item(item) - if item == nil then - docs.win:close() - return - end - - -- TODO: cancellation - -- TODO: only resolve if documentation does not exist - sources - .resolve(item) - :map(function(item) - if item.documentation == nil and item.detail == nil then - docs.win:close() - return - end - - if docs.shown_item ~= item then - require('blink.cmp.windows.lib.docs').render_detail_and_documentation( - docs.win:get_buf(), - item.detail, - item.documentation, - docs.win.config.max_width, - config.treesitter_highlighting - ) - end - docs.shown_item = item - - if autocomplete.win:get_win() then - docs.win:open() - vim.api.nvim_win_set_cursor(docs.win:get_win(), { 1, 0 }) -- reset scroll - docs.update_position() - end - end) - :catch(function(err) vim.notify(err, vim.log.levels.ERROR) end) -end - -function docs.scroll_up(amount) - local winnr = docs.win:get_win() - local top_line = math.max(1, vim.fn.line('w0', winnr) - 1) - local desired_line = math.max(1, top_line - amount) - - vim.api.nvim_win_set_cursor(docs.win:get_win(), { desired_line, 0 }) -end - -function docs.scroll_down(amount) - local winnr = docs.win:get_win() - local line_count = vim.api.nvim_buf_line_count(docs.win:get_buf()) - local bottom_line = math.max(1, vim.fn.line('w$', winnr) + 1) - local desired_line = math.min(line_count, bottom_line + amount) - - vim.api.nvim_win_set_cursor(docs.win:get_win(), { desired_line, 0 }) -end - -function docs.update_position() - if not docs.win:is_open() or not autocomplete.win:is_open() then return end - local winnr = docs.win:get_win() - - docs.win:update_size() - - local autocomplete_winnr = autocomplete.win:get_win() - if not autocomplete_winnr then return end - local autocomplete_win_config = vim.api.nvim_win_get_config(autocomplete_winnr) - local autocomplete_win_height = autocomplete.win:get_height() - local autocomplete_border_size = autocomplete.win:get_border_size() - - local cursor_win_row = vim.fn.winline() - - -- decide direction priority based on the autocomplete window's position - local autocomplete_win_is_up = autocomplete_win_config.row - cursor_win_row < 0 - local direction_priority = autocomplete_win_is_up and config.direction_priority.autocomplete_north - or config.direction_priority.autocomplete_south - - -- remove the direction priority of the signature window if it's open - if signature.win and signature.win:is_open() then - direction_priority = vim.tbl_filter( - function(dir) return dir ~= (autocomplete_win_is_up and 's' or 'n') end, - direction_priority - ) - end - - -- decide direction, width and height of window - local width = docs.win:get_width() - local height = docs.win:get_height() - local pos = docs.win:get_direction_with_window_constraints( - autocomplete.win, - direction_priority, - { width = math.min(width, config.desired_min_width), height = math.min(height, config.desired_min_height) } - ) - - -- couldn't find anywhere to place the window - if not pos then - docs.win:close() - return - end - - -- set width and height based on available space - vim.api.nvim_win_set_height(docs.win:get_win(), pos.height) - vim.api.nvim_win_set_width(docs.win:get_win(), pos.width) - - -- set position based on provided direction - - local height = docs.win:get_height() - local width = docs.win:get_width() - - local function set_config(opts) - vim.api.nvim_win_set_config(winnr, { relative = 'win', win = autocomplete_winnr, row = opts.row, col = opts.col }) - end - if pos.direction == 'n' then - if autocomplete_win_is_up then - set_config({ row = -height - autocomplete_border_size.top, col = -autocomplete_border_size.left }) - else - set_config({ row = -1 - height - autocomplete_border_size.top, col = -autocomplete_border_size.left }) - end - elseif pos.direction == 's' then - if autocomplete_win_is_up then - set_config({ - row = 1 + autocomplete_win_height - autocomplete_border_size.top, - col = -autocomplete_border_size.left, - }) - else - set_config({ - row = autocomplete_win_height - autocomplete_border_size.top, - col = -autocomplete_border_size.left, - }) - end - elseif pos.direction == 'e' then - if autocomplete_win_is_up and autocomplete_win_height < height then - set_config({ - row = autocomplete_win_height - autocomplete_border_size.top - height, - col = autocomplete_win_config.width + autocomplete_border_size.right, - }) - else - set_config({ - row = -autocomplete_border_size.top, - col = autocomplete_win_config.width + autocomplete_border_size.right, - }) - end - elseif pos.direction == 'w' then - if autocomplete_win_is_up and autocomplete_win_height < height then - set_config({ - row = autocomplete_win_height - autocomplete_border_size.top - height, - col = -width - autocomplete_border_size.left, - }) - else - set_config({ row = -autocomplete_border_size.top, col = -width - autocomplete_border_size.left }) - end - end -end - -return docs