├── lua ├── blink-cmp.lua └── blink │ └── cmp │ ├── accept │ ├── brackets │ │ ├── init.lua │ │ ├── config.lua │ │ ├── utils.lua │ │ ├── kind.lua │ │ └── semantic.lua │ ├── preview.lua │ ├── text-edits.lua │ └── init.lua │ ├── sources │ ├── lib │ │ ├── provider │ │ │ ├── override.lua │ │ │ ├── config.lua │ │ │ └── init.lua │ │ ├── utils.lua │ │ ├── types.lua │ │ ├── async.lua │ │ ├── context.lua │ │ └── init.lua │ ├── snippets │ │ ├── init.lua │ │ ├── utils.lua │ │ ├── scan.lua │ │ ├── registry.lua │ │ └── builtin.lua │ ├── path │ │ ├── fs.lua │ │ ├── init.lua │ │ └── lib.lua │ ├── buffer.lua │ └── lsp.lua │ ├── fuzzy │ ├── rust.lua │ ├── lsp_item.rs │ ├── init.lua │ ├── lib.rs │ ├── fuzzy.rs │ ├── frecency.rs │ └── download.lua │ ├── trigger │ ├── types.lua │ ├── signature.lua │ └── completion.lua │ ├── health.lua │ ├── types.lua │ ├── windows │ ├── lib │ │ ├── scrollbar │ │ │ ├── geometry.lua │ │ │ ├── init.lua │ │ │ └── win.lua │ │ ├── render.lua │ │ └── docs.lua │ ├── ghost-text.lua │ ├── signature.lua │ └── documentation.lua │ ├── utils.lua │ ├── init.lua │ └── keymap.lua ├── rust-toolchain.toml ├── .github ├── ISSUE_TEMPLATE │ ├── config.yml │ ├── feature_request.yml │ └── bug_report.yml └── workflows │ └── release.yaml ├── .gitignore ├── .stylua.toml ├── Cargo.toml ├── .cargo └── config.toml ├── ROADMAP.md ├── LICENSE ├── LSP_TRACKER.md ├── flake.lock └── flake.nix /lua/blink-cmp.lua: -------------------------------------------------------------------------------- 1 | return require('blink.cmp') 2 | -------------------------------------------------------------------------------- /rust-toolchain.toml: -------------------------------------------------------------------------------- 1 | [toolchain] 2 | channel = "nightly" 3 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: true 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | target/ 2 | .archive.lua 3 | _*.lua 4 | dual/ 5 | result 6 | .direnv 7 | .devenv 8 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.yml: -------------------------------------------------------------------------------- 1 | name: Feature request 2 | description: Suggest an idea 3 | labels: ["feature"] 4 | -------------------------------------------------------------------------------- /.stylua.toml: -------------------------------------------------------------------------------- 1 | column_width = 120 2 | line_endings = "Unix" 3 | indent_type = "Spaces" 4 | indent_width = 2 5 | quote_style = "AutoPreferSingle" 6 | call_parentheses = "Always" 7 | collapse_simple_statement = "Always" 8 | -------------------------------------------------------------------------------- /lua/blink/cmp/accept/brackets/init.lua: -------------------------------------------------------------------------------- 1 | local brackets = {} 2 | 3 | brackets.add_brackets = require('blink.cmp.accept.brackets.kind') 4 | brackets.add_brackets_via_semantic_token = require('blink.cmp.accept.brackets.semantic') 5 | 6 | return brackets 7 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "blink-cmp-fuzzy" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | [lib] 7 | path = "lua/blink/cmp/fuzzy/lib.rs" 8 | crate-type = ["cdylib"] 9 | 10 | [dependencies] 11 | regex = "1.10.5" 12 | lazy_static = "1.5.0" 13 | frizbee = { git = "https://github.com/saghen/frizbee" } 14 | serde = { version = "1.0.204", features = ["derive"] } 15 | heed = "0.20.3" 16 | mlua = { version = "0.10.0", features = ["module", "luajit"] } 17 | -------------------------------------------------------------------------------- /.cargo/config.toml: -------------------------------------------------------------------------------- 1 | [target.x86_64-apple-darwin] 2 | rustflags = [ 3 | "-C", "link-arg=-undefined", 4 | "-C", "link-arg=dynamic_lookup", 5 | ] 6 | 7 | [target.aarch64-apple-darwin] 8 | rustflags = [ 9 | "-C", "link-arg=-undefined", 10 | "-C", "link-arg=dynamic_lookup", 11 | ] 12 | 13 | [target.x86_64-unknown-linux-musl] 14 | rustflags = ["-C", "target-feature=-crt-static"] 15 | 16 | [target.aarch64-unknown-linux-musl] 17 | rustflags = ["-C", "target-feature=-crt-static"] 18 | -------------------------------------------------------------------------------- /ROADMAP.md: -------------------------------------------------------------------------------- 1 | # Ideas to explore 2 | 3 | - [ ] Support auto brackets on function completion (currently experimental) 4 | - [ ] LSP signature help ([ref](https://github.com/ray-x/lsp_signature.nvim)) (currently experimental) 5 | - [x] Ship pre-built binaries 6 | - [ ] Output preview with ghost text, including for snippets 7 | - [ ] Show sources dynamically based on treesitter nodes 8 | - [ ] Apply treesitter highlights to completion labels ([ref](https://github.com/hrsh7th/nvim-cmp/issues/1887#issue-2246686828)) 9 | - [ ] Get query for fuzzy from treesitter node 10 | - [ ] Cmdline support (including noice.nvim support) 11 | -------------------------------------------------------------------------------- /lua/blink/cmp/sources/lib/provider/override.lua: -------------------------------------------------------------------------------- 1 | --- @class blink.cmp.Override : blink.cmp.Source 2 | --- @field new fun(module: blink.cmp.Source, override_config: blink.cmp.SourceOverride): blink.cmp.Override 3 | 4 | local override = {} 5 | 6 | function override.new(module, override_config) 7 | override_config = override_config or {} 8 | 9 | return setmetatable({}, { 10 | __index = function(_, key) 11 | if override_config[key] ~= nil then 12 | return function(self, ...) return override_config[key](self.module, ...) end 13 | end 14 | return module[key] 15 | end, 16 | }) 17 | end 18 | 19 | return override 20 | -------------------------------------------------------------------------------- /lua/blink/cmp/fuzzy/rust.lua: -------------------------------------------------------------------------------- 1 | --- @return string 2 | local function get_lib_extension() 3 | if jit.os:lower() == 'mac' or jit.os:lower() == 'osx' then return '.dylib' end 4 | if jit.os:lower() == 'windows' then return '.dll' end 5 | return '.so' 6 | end 7 | 8 | -- search for the lib in the /target/release directory with and without the lib prefix 9 | -- since MSVC doesn't include the prefix 10 | package.cpath = package.cpath 11 | .. ';' 12 | .. debug.getinfo(1).source:match('@?(.*/)') 13 | .. '../../../../target/release/lib?' 14 | .. get_lib_extension() 15 | .. ';' 16 | .. debug.getinfo(1).source:match('@?(.*/)') 17 | .. '../../../../target/release/?' 18 | .. get_lib_extension() 19 | 20 | return require('blink_cmp_fuzzy') 21 | -------------------------------------------------------------------------------- /lua/blink/cmp/trigger/types.lua: -------------------------------------------------------------------------------- 1 | --- @class blink.cmp.ContextBounds 2 | --- @field line string 3 | --- @field line_number number 4 | --- @field start_col number 5 | --- @field end_col number 6 | --- @field length number 7 | 8 | --- @class blink.cmp.Context 9 | --- @field id number 10 | --- @field bufnr number 11 | --- @field cursor number[] 12 | --- @field line string 13 | --- @field bounds blink.cmp.ContextBounds 14 | --- @field trigger { kind: number, character: string | nil } 15 | 16 | --- @class blink.cmp.SignatureHelpContext 17 | --- @field id number 18 | --- @field bufnr number 19 | --- @field cursor number[] 20 | --- @field line string 21 | --- @field is_retrigger boolean 22 | --- @field active_signature_help lsp.SignatureHelp | nil 23 | -------------------------------------------------------------------------------- /lua/blink/cmp/accept/brackets/config.lua: -------------------------------------------------------------------------------- 1 | return { 2 | -- stylua: ignore 3 | blocked_filetypes = { 4 | 'rust', 'sql', 'ruby', 'perl', 'lisp', 'scheme', 'clojure', 5 | 'prolog', 'vb', 'elixir', 'smalltalk', 'applescript' 6 | }, 7 | per_filetype = { 8 | -- languages with a space 9 | haskell = { ' ', '' }, 10 | fsharp = { ' ', '' }, 11 | ocaml = { ' ', '' }, 12 | erlang = { ' ', '' }, 13 | tcl = { ' ', '' }, 14 | nix = { ' ', '' }, 15 | helm = { ' ', '' }, 16 | 17 | shell = { ' ', '' }, 18 | sh = { ' ', '' }, 19 | bash = { ' ', '' }, 20 | fish = { ' ', '' }, 21 | zsh = { ' ', '' }, 22 | powershell = { ' ', '' }, 23 | 24 | make = { ' ', '' }, 25 | 26 | -- languages with square brackets 27 | wl = { '[', ']' }, 28 | wolfram = { '[', ']' }, 29 | mma = { '[', ']' }, 30 | mathematica = { '[', ']' }, 31 | }, 32 | } 33 | -------------------------------------------------------------------------------- /lua/blink/cmp/fuzzy/lsp_item.rs: -------------------------------------------------------------------------------- 1 | use mlua::prelude::*; 2 | 3 | #[derive(Debug)] 4 | pub struct LspItem { 5 | pub label: String, 6 | pub kind: u32, 7 | pub score_offset: i32, 8 | pub source_id: String, 9 | } 10 | 11 | impl FromLua for LspItem { 12 | fn from_lua(value: LuaValue, _: &Lua) -> LuaResult { 13 | if let Some(tab) = value.as_table() { 14 | let label: String = tab.get("label").unwrap_or_default(); 15 | let kind: u32 = tab.get("kind").unwrap_or_default(); 16 | let score_offset: i32 = tab.get("score_offset").unwrap_or(0); 17 | let source_id: String = tab.get("source_id").unwrap_or_default(); 18 | 19 | Ok(LspItem { 20 | label, 21 | kind, 22 | score_offset, 23 | source_id, 24 | }) 25 | } else { 26 | Err(mlua::Error::FromLuaConversionError { 27 | from: "LuaValue", 28 | to: "LspItem".to_string(), 29 | message: None, 30 | }) 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Liam Dyer 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /lua/blink/cmp/health.lua: -------------------------------------------------------------------------------- 1 | local health = {} 2 | local download = require('blink.cmp.fuzzy.download') 3 | 4 | health.check = function() 5 | vim.health.start('blink.cmp healthcheck') 6 | 7 | local required_executables = { 'curl', 'git' } 8 | for _, executable in ipairs(required_executables) do 9 | if vim.fn.executable(executable) == 0 then 10 | vim.health.error(executable .. ' is not installed') 11 | else 12 | vim.health.ok(executable .. ' is installed') 13 | end 14 | end 15 | 16 | -- check if os is supported 17 | local system_triple = download.get_system_triple_sync() 18 | if system_triple then 19 | vim.health.ok('Your system is supported by pre-built binaries (' .. system_triple .. ')') 20 | else 21 | vim.health.warn( 22 | 'Your system is not supported by pre-built binaries. You must run cargo build --release via your package manager with rust nightly. See the README for more info.' 23 | ) 24 | end 25 | 26 | if 27 | vim.uv.fs_stat(download.lib_path) 28 | or vim.uv.fs_stat(string.gsub(download.lib_path, 'libblink_cmp_fuzzy', 'blink_cmp_fuzzy')) 29 | then 30 | vim.health.ok('blink_cmp_fuzzy lib is downloaded/built') 31 | else 32 | vim.health.warn('blink_cmp_fuzzy lib is not downloaded/built') 33 | end 34 | end 35 | return health 36 | -------------------------------------------------------------------------------- /lua/blink/cmp/types.lua: -------------------------------------------------------------------------------- 1 | --- @class blink.cmp.CompletionItem : lsp.CompletionItem 2 | --- @field score_offset number | nil 3 | --- @field source_id string 4 | --- @field source_name string 5 | --- @field cursor_column number 6 | --- @field client_id number 7 | 8 | return { 9 | -- some plugins mutate the vim.lsp.protocol.CompletionItemKind table 10 | -- so we use our own copy 11 | CompletionItemKind = { 12 | 'Text', 13 | 'Method', 14 | 'Function', 15 | 'Constructor', 16 | 'Field', 17 | 'Variable', 18 | 'Class', 19 | 'Interface', 20 | 'Module', 21 | 'Property', 22 | 'Unit', 23 | 'Value', 24 | 'Enum', 25 | 'Keyword', 26 | 'Snippet', 27 | 'Color', 28 | 'File', 29 | 'Reference', 30 | 'Folder', 31 | 'EnumMember', 32 | 'Constant', 33 | 'Struct', 34 | 'Event', 35 | 'Operator', 36 | 'TypeParameter', 37 | 38 | Text = 1, 39 | Method = 2, 40 | Function = 3, 41 | Constructor = 4, 42 | Field = 5, 43 | Variable = 6, 44 | Class = 7, 45 | Interface = 8, 46 | Module = 9, 47 | Property = 10, 48 | Unit = 11, 49 | Value = 12, 50 | Enum = 13, 51 | Keyword = 14, 52 | Snippet = 15, 53 | Color = 16, 54 | File = 17, 55 | Reference = 18, 56 | Folder = 19, 57 | EnumMember = 20, 58 | Constant = 21, 59 | Struct = 22, 60 | Event = 23, 61 | Operator = 24, 62 | TypeParameter = 25, 63 | }, 64 | } 65 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.yml: -------------------------------------------------------------------------------- 1 | name: Bug Report 2 | description: File a bug report 3 | labels: ["bug"] 4 | body: 5 | - type: checkboxes 6 | id: checklist 7 | attributes: 8 | label: Make sure you have done the following 9 | options: 10 | - label: I have updated to the latest version of `blink.cmp` 11 | required: true 12 | - label: I have read the README 13 | required: true 14 | - type: textarea 15 | id: bug-description 16 | attributes: 17 | label: Bug Description 18 | validations: { required: true } 19 | - type: textarea 20 | id: user-config 21 | attributes: 22 | label: Relevant configuration 23 | description: Copypaste the part of the config relevant to the bug. Do not paste the entire default config. 24 | render: lua 25 | placeholder: | 26 | sources = { 27 | completion = { 28 | enabled_providers = { "lsp", "path", "snippets", "buffer" }, 29 | }, 30 | }, 31 | validations: { required: false } 32 | - type: input 33 | id: version-info 34 | attributes: 35 | label: neovim version 36 | placeholder: "output of `nvim --version`" 37 | validations: { required: true } 38 | - type: input 39 | id: branch-or-tag 40 | attributes: 41 | label: "`blink.cmp` version: branch, tag, or commit" 42 | placeholder: "for example: main or v0.4.0" 43 | validations: { required: true } 44 | -------------------------------------------------------------------------------- /lua/blink/cmp/accept/preview.lua: -------------------------------------------------------------------------------- 1 | --- @param item blink.cmp.CompletionItem 2 | local function preview(item, previous_text_edit) 3 | local text_edits_lib = require('blink.cmp.accept.text-edits') 4 | local text_edit = text_edits_lib.get_from_item(item) 5 | 6 | -- with auto_insert, we may have to undo the previous preview 7 | if previous_text_edit ~= nil then text_edit.range = text_edits_lib.get_undo_text_edit_range(previous_text_edit) end 8 | 9 | -- for snippets, expand them with the default property names 10 | local cursor_pos = { 11 | text_edit.range.start.line + 1, 12 | text_edit.range.start.character + #text_edit.newText, 13 | } 14 | if item.insertTextFormat == vim.lsp.protocol.InsertTextFormat.Snippet then 15 | local expanded_snippet = require('blink.cmp.sources.snippets.utils').safe_parse(text_edit.newText) 16 | text_edit.newText = expanded_snippet and tostring(expanded_snippet) or text_edit.newText 17 | 18 | -- place the cursor at the first tab stop 19 | local tabstops = require('blink.cmp.sources.snippets.utils').get_tab_stops(text_edit.newText) 20 | if tabstops and #tabstops > 0 then 21 | cursor_pos[1] = text_edit.range.start.line + tabstops[1].line 22 | cursor_pos[2] = text_edit.range.start.character + tabstops[1].character 23 | end 24 | end 25 | 26 | text_edits_lib.apply_text_edits(item.client_id, { text_edit }) 27 | vim.api.nvim_win_set_cursor(0, cursor_pos) 28 | 29 | -- return so that it can be undone in the future 30 | return text_edit 31 | end 32 | 33 | return preview 34 | -------------------------------------------------------------------------------- /lua/blink/cmp/sources/lib/provider/config.lua: -------------------------------------------------------------------------------- 1 | --- @class blink.cmp.SourceProviderConfigWrapper 2 | --- @field new fun(config: blink.cmp.SourceProviderConfig): blink.cmp.SourceProviderConfigWrapper 3 | --- @field name string 4 | --- @field module string 5 | --- @field enabled fun(ctx: blink.cmp.Context): boolean 6 | --- @field transform_items fun(ctx: blink.cmp.Context, items: blink.cmp.CompletionItem[]): blink.cmp.CompletionItem[] 7 | --- @field should_show_items fun(ctx: blink.cmp.Context, items: blink.cmp.CompletionItem[]): boolean 8 | --- @field max_items? fun(ctx: blink.cmp.Context, enabled_sources: string[], items: blink.cmp.CompletionItem[]): number 9 | --- @field min_keyword_length fun(ctx: blink.cmp.Context, enabled_sources: string[]): number 10 | --- @field fallback_for fun(ctx: blink.cmp.Context, enabled_sources: string[]): string[] 11 | --- @field score_offset fun(ctx: blink.cmp.Context, enabled_sources: string[]): number 12 | 13 | --- @class blink.cmp.SourceProviderConfig 14 | --- @diagnostic disable-next-line: missing-fields 15 | local wrapper = {} 16 | 17 | function wrapper.new(config) 18 | local function call_or_get(fn_or_val, default) 19 | if fn_or_val == nil then return function() return default end end 20 | return function(...) 21 | if type(fn_or_val) == 'function' then return fn_or_val(...) end 22 | return fn_or_val 23 | end 24 | end 25 | 26 | local self = setmetatable({}, { __index = config }) 27 | self.name = config.name 28 | self.module = config.module 29 | self.enabled = call_or_get(config.enabled, true) 30 | self.transform_items = config.transform_items or function(_, items) return items end 31 | self.should_show_items = call_or_get(config.should_show_items, true) 32 | self.max_items = call_or_get(config.max_items, nil) 33 | self.min_keyword_length = call_or_get(config.min_keyword_length, 0) 34 | self.fallback_for = call_or_get(config.fallback_for, {}) 35 | self.score_offset = call_or_get(config.score_offset, 0) 36 | return self 37 | end 38 | 39 | return wrapper 40 | -------------------------------------------------------------------------------- /LSP_TRACKER.md: -------------------------------------------------------------------------------- 1 | # LSP Support Tracker 2 | 3 | ## Completion Items 4 | 5 | - [x] `completionItem/resolve` <- Used to get information such as documentation which would be too expensive to include normally 6 | 7 | ### Client Capabilities 8 | 9 | - [ ] `dynamicRegistration` 10 | - [ ] `CompletionItem` 11 | - [x] `snippetSupport` 12 | - [ ] `commitCharacterSupport` 13 | - [x] `documentationFormat` 14 | - [x] `deprecatedSupport` 15 | - [ ] `preselectSupport` 16 | - [x] `tagSupport` 17 | - [ ] `insertReplaceSupport` 18 | - [x] `resolveSupport` <- Allows LSPs to resolve additional properties lazily, potentially improving latency 19 | - [x] `insertTextModeSupport` 20 | - [x] `labelDetailsSupport` 21 | - [x] `completionItemKind` 22 | - [x] `contextSupport` 23 | 24 | ### Server Capabilities 25 | 26 | - [x] `triggerCharacters` 27 | - [ ] `allCommitCharacters` 28 | - [x] `resolveProvider` 29 | - [x] `CompletionItem` 30 | - [x] `labelDetailsSupport` 31 | 32 | ### Request Params 33 | 34 | - [x] `CompletionContext` 35 | - [x] `triggerKind` 36 | - [x] `triggerCharacter` 37 | 38 | ### List 39 | 40 | - [x] `isIncomplete` 41 | - [x] `itemDefaults` 42 | - [x] `commitCharacters` 43 | - [ ] `editRange` 44 | - [x] `insertTextFormat` 45 | - [x] `insertTextMode` 46 | - [x] `data` 47 | - [x] `items` 48 | 49 | ### Item 50 | 51 | - [x] `label` 52 | - [x] `labelDetails` 53 | - [x] `kind` 54 | - [x] `tags` 55 | - [x] `detail` 56 | - [x] `documentation` <- both string and markup content 57 | - [x] `deprecated` 58 | - [ ] `preselect` 59 | - [ ] `sortText` 60 | - [x] `filterText` 61 | - [x] `insertText` 62 | - [x] `insertTextFormat` <- regular or snippet 63 | - [ ] `insertTextMode` <- asIs only, not sure we'll support adjustIndentation 64 | - [x] `textEdit` 65 | - [x] `textEditText` 66 | - [x] `additionalTextEdits` <- known issue where applying the main text edit will cause this to be wrong if the additional text edit comes after since the indices will be offset 67 | - [ ] `commitCharacters` 68 | - [ ] `command` 69 | - [x] `data` <- Don't think there's anything special to do here 70 | -------------------------------------------------------------------------------- /lua/blink/cmp/accept/brackets/utils.lua: -------------------------------------------------------------------------------- 1 | local config = require('blink.cmp.config').accept.auto_brackets 2 | local brackets = require('blink.cmp.accept.brackets.config') 3 | local utils = {} 4 | 5 | --- @param snippet string 6 | function utils.snippets_extract_placeholders(snippet) 7 | local placeholders = {} 8 | local pattern = [=[(\$\{(\d+)(:([^}\\]|\\.)*?)?\})]=] 9 | 10 | for _, number, _, _ in snippet:gmatch(pattern) do 11 | table.insert(placeholders, tonumber(number)) 12 | end 13 | 14 | return placeholders 15 | end 16 | 17 | --- @param filetype string 18 | --- @param item blink.cmp.CompletionItem 19 | --- @return string[] 20 | function utils.get_for_filetype(filetype, item) 21 | local default = config.default_brackets 22 | local per_filetype = config.override_brackets_for_filetypes[filetype] or brackets.per_filetype[filetype] 23 | 24 | if type(per_filetype) == 'function' then return per_filetype(item) or default end 25 | return per_filetype or default 26 | end 27 | 28 | --- @param filetype string 29 | --- @param resolution_method 'kind' | 'semantic_token' 30 | --- @return boolean 31 | function utils.should_run_resolution(filetype, resolution_method) 32 | -- resolution method specific 33 | if not config[resolution_method .. '_resolution'].enabled then return false end 34 | local resolution_blocked_filetypes = config[resolution_method .. '_resolution'].blocked_filetypes 35 | if vim.tbl_contains(resolution_blocked_filetypes, filetype) then return false end 36 | 37 | -- global 38 | if not config.enabled then return false end 39 | if vim.tbl_contains(config.force_allow_filetypes, filetype) then return true end 40 | return not vim.tbl_contains(config.blocked_filetypes, filetype) 41 | and not vim.tbl_contains(brackets.blocked_filetypes, filetype) 42 | end 43 | 44 | --- @param text_edit lsp.TextEdit | lsp.InsertReplaceEdit 45 | --- @param bracket string 46 | --- @return boolean 47 | function utils.has_brackets_in_front(text_edit, bracket) 48 | local line = vim.api.nvim_get_current_line() 49 | local col = text_edit.range['end'].character + 1 50 | return line:sub(col, col) == bracket 51 | end 52 | 53 | return utils 54 | -------------------------------------------------------------------------------- /lua/blink/cmp/windows/lib/scrollbar/geometry.lua: -------------------------------------------------------------------------------- 1 | --- Helper for calculating placement of the scrollbar thumb and gutter 2 | 3 | --- @class blink.cmp.ScrollbarGeometry 4 | --- @field width number 5 | --- @field height number 6 | --- @field row number 7 | --- @field col number 8 | --- @field zindex number 9 | --- @field relative string 10 | --- @field win number 11 | 12 | local M = {} 13 | 14 | --- @param target_win number 15 | --- @return number 16 | local function get_win_buf_height(target_win) 17 | local buf = vim.api.nvim_win_get_buf(target_win) 18 | 19 | -- not wrapping, so just get the line count 20 | if not vim.wo[target_win].wrap then return vim.api.nvim_buf_line_count(buf) end 21 | 22 | local width = vim.api.nvim_win_get_width(target_win) 23 | local lines = vim.api.nvim_buf_get_lines(buf, 0, -1, false) 24 | local height = 0 25 | for _, l in ipairs(lines) do 26 | height = height + math.max(1, (math.ceil(vim.fn.strwidth(l) / width))) 27 | end 28 | return height 29 | end 30 | 31 | --- @param target_win number 32 | --- @return { should_hide: boolean, thumb: blink.cmp.ScrollbarGeometry, gutter: blink.cmp.ScrollbarGeometry } 33 | function M.get_geometry(target_win) 34 | local width = vim.api.nvim_win_get_width(target_win) 35 | local height = vim.api.nvim_win_get_height(target_win) 36 | local zindex = vim.api.nvim_win_get_config(target_win).zindex or 1 37 | 38 | local buf_height = get_win_buf_height(target_win) 39 | 40 | local thumb_height = math.max(1, math.floor(height * height / buf_height + 0.5) - 1) 41 | 42 | local start_line = math.max(1, vim.fn.line('w0', target_win) - 1) 43 | local pct = (start_line - 1) / buf_height 44 | local thumb_offset = math.ceil(pct * (height - thumb_height)) 45 | 46 | local common_geometry = { 47 | width = 1, 48 | row = thumb_offset, 49 | col = width, 50 | relative = 'win', 51 | win = target_win, 52 | } 53 | 54 | return { 55 | should_hide = height >= buf_height, 56 | thumb = vim.tbl_deep_extend('force', common_geometry, { height = thumb_height, zindex = zindex + 2 }), 57 | gutter = vim.tbl_deep_extend('force', common_geometry, { row = 0, height = height, zindex = zindex + 1 }), 58 | } 59 | end 60 | 61 | return M 62 | -------------------------------------------------------------------------------- /lua/blink/cmp/sources/snippets/init.lua: -------------------------------------------------------------------------------- 1 | --- @class blink.cmp.SnippetsOpts 2 | --- @field friendly_snippets boolean 3 | --- @field search_paths string[] 4 | --- @field global_snippets string[] 5 | --- @field extended_filetypes table 6 | --- @field ignored_filetypes string[] 7 | 8 | local snippets = {} 9 | 10 | function snippets.new(opts) 11 | local self = setmetatable({}, { __index = snippets }) 12 | --- @type table 13 | self.cache = {} 14 | --- @type blink.cmp.SnippetsOpts 15 | self.registry = require('blink.cmp.sources.snippets.registry').new(opts or {}) 16 | return self 17 | end 18 | 19 | function snippets:get_completions(_, callback) 20 | local filetype = vim.bo.filetype 21 | if vim.tbl_contains(self.registry.config.ignored_filetypes, filetype) then return callback() end 22 | 23 | if not self.cache[filetype] then 24 | local global_snippets = self.registry:get_global_snippets() 25 | local extended_snippets = self.registry:get_extended_snippets(filetype) 26 | local ft_snippets = self.registry:get_snippets_for_ft(filetype) 27 | local snips = vim.list_extend({}, global_snippets) 28 | vim.list_extend(snips, extended_snippets) 29 | vim.list_extend(snips, ft_snippets) 30 | 31 | self.cache[filetype] = snips 32 | end 33 | 34 | local items = vim.tbl_map( 35 | function(item) return self.registry:snippet_to_completion_item(item) end, 36 | self.cache[filetype] 37 | ) 38 | callback({ 39 | is_incomplete_forward = false, 40 | is_incomplete_backward = false, 41 | items = items, 42 | }) 43 | end 44 | 45 | function snippets:resolve(item, callback) 46 | local parsed_snippet = require('blink.cmp.sources.snippets.utils').safe_parse(item.insertText) 47 | local snippet = parsed_snippet and tostring(parsed_snippet) or item.insertText 48 | 49 | local resolved_item = vim.deepcopy(item) 50 | resolved_item.detail = snippet 51 | resolved_item.documentation = { 52 | kind = 'markdown', 53 | value = item.description, 54 | } 55 | callback(resolved_item) 56 | end 57 | 58 | --- For external integrations to force reloading the snippets 59 | function snippets:reload() self.cache = {} end 60 | 61 | return snippets 62 | -------------------------------------------------------------------------------- /lua/blink/cmp/sources/lib/utils.lua: -------------------------------------------------------------------------------- 1 | local utils = {} 2 | 3 | --- Checks if a request should be made, based on the previous response/context 4 | --- and the new context 5 | --- 6 | --- @param new_context blink.cmp.Context 7 | --- @param response blink.cmp.CompletionResponse 8 | --- 9 | --- @return false | 'forward' | 'backward' | 'unknown' 10 | function utils.should_run_request(new_context, response) 11 | local old_context = response.context 12 | -- get the text for the current and queued context 13 | local old_context_query = old_context.line:sub(old_context.bounds.start_col, old_context.cursor[2]) 14 | local new_context_query = new_context.line:sub(new_context.bounds.start_col, new_context.cursor[2]) 15 | 16 | -- check if the texts are overlapping 17 | local is_before = vim.startswith(old_context_query, new_context_query) 18 | local is_after = vim.startswith(new_context_query, old_context_query) 19 | 20 | if is_before and response.is_incomplete_backward then return 'forward' end 21 | if is_after and response.is_incomplete_forward then return 'backward' end 22 | if (is_after == is_before) and (response.is_incomplete_backward or response.is_incomplete_forward) then 23 | return 'unknown' 24 | end 25 | return false 26 | end 27 | 28 | --- @param responses blink.cmp.CompletionResponse[] 29 | --- @return blink.cmp.CompletionResponse 30 | function utils.concat_responses(responses) 31 | local is_cached = true 32 | local is_incomplete_forward = false 33 | local is_incomplete_backward = false 34 | local items = {} 35 | 36 | for _, response in ipairs(responses) do 37 | is_cached = is_cached and response.is_cached 38 | is_incomplete_forward = is_incomplete_forward or response.is_incomplete_forward 39 | is_incomplete_backward = is_incomplete_backward or response.is_incomplete_backward 40 | vim.list_extend(items, response.items) 41 | end 42 | 43 | return { 44 | is_cached = is_cached, 45 | is_incomplete_forward = is_incomplete_forward, 46 | is_incomplete_backward = is_incomplete_backward, 47 | items = items, 48 | } 49 | end 50 | 51 | --- @param item blink.cmp.CompletionItem 52 | --- @return lsp.CompletionItem 53 | function utils.blink_item_to_lsp_item(item) 54 | local lsp_item = vim.deepcopy(item) 55 | lsp_item.cursor_column = nil 56 | lsp_item.score_offset = nil 57 | lsp_item.client_id = nil 58 | lsp_item.source_id = nil 59 | lsp_item.source_name = nil 60 | return lsp_item 61 | end 62 | 63 | return utils 64 | -------------------------------------------------------------------------------- /lua/blink/cmp/sources/path/fs.lua: -------------------------------------------------------------------------------- 1 | local async = require('blink.cmp.sources.lib.async') 2 | local uv = vim.uv 3 | local fs = {} 4 | 5 | --- Scans a directory asynchronously in a loop until 6 | --- it finds all entries 7 | --- @param path string 8 | --- @return blink.cmp.Task 9 | function fs.scan_dir_async(path) 10 | local max_entries = 200 11 | return async.task.new(function(resolve, reject) 12 | uv.fs_opendir(path, function(err, handle) 13 | if err ~= nil or handle == nil then return reject(err) end 14 | 15 | local all_entries = {} 16 | 17 | local function read_dir() 18 | uv.fs_readdir(handle, function(err, entries) 19 | if err ~= nil or entries == nil then return reject(err) end 20 | 21 | vim.list_extend(all_entries, entries) 22 | if #entries == max_entries then 23 | read_dir() 24 | else 25 | resolve(all_entries) 26 | end 27 | end) 28 | end 29 | read_dir() 30 | end, max_entries) 31 | end) 32 | end 33 | 34 | --- @param entries { name: string, type: string }[] 35 | --- @return blink.cmp.Task 36 | function fs.fs_stat_all(cwd, entries) 37 | local tasks = {} 38 | for _, entry in ipairs(entries) do 39 | table.insert( 40 | tasks, 41 | async.task.new(function(resolve, reject) 42 | uv.fs_stat(cwd .. '/' .. entry.name, function(err, stat) 43 | if err then return reject(err) end 44 | resolve({ name = entry.name, type = entry.type, stat = stat }) 45 | end) 46 | end) 47 | ) 48 | end 49 | return async.task.await_all(tasks):map(function(tasks_results) 50 | local resolved_entries = {} 51 | for _, entry in ipairs(tasks_results) do 52 | if entry.status == async.STATUS.COMPLETED then table.insert(resolved_entries, entry.result) end 53 | end 54 | return resolved_entries 55 | end) 56 | end 57 | 58 | --- @param path string 59 | --- @param byte_limit number 60 | --- @return blink.cmp.Task 61 | function fs.read_file(path, byte_limit) 62 | return async.task.new(function(resolve, reject) 63 | uv.fs_open(path, 'r', 438, function(open_err, fd) 64 | if open_err or fd == nil then return reject(open_err) end 65 | uv.fs_read(fd, byte_limit, 0, function(read_err, data) 66 | uv.fs_close(fd, function() end) 67 | if read_err or data == nil then return reject(read_err) end 68 | resolve(data) 69 | end) 70 | end) 71 | end) 72 | end 73 | 74 | return fs 75 | -------------------------------------------------------------------------------- /lua/blink/cmp/sources/lib/types.lua: -------------------------------------------------------------------------------- 1 | --- @class blink.cmp.CompletionTriggerContext 2 | --- @field kind number 3 | --- @field character string | nil 4 | 5 | --- @class blink.cmp.CompletionResponse 6 | --- @field is_incomplete_forward boolean 7 | --- @field is_incomplete_backward boolean 8 | --- @field context blink.cmp.Context 9 | --- @field items blink.cmp.CompletionItem[] 10 | 11 | --- @class blink.cmp.Source 12 | --- @field new fun(config: blink.cmp.SourceProviderConfig): blink.cmp.Source 13 | --- @field enabled? fun(self: blink.cmp.Source, context: blink.cmp.Context): boolean 14 | --- @field get_trigger_characters? (fun(self: blink.cmp.Source): string[]) | nil 15 | --- @field get_completions? fun(self: blink.cmp.Source, context: blink.cmp.Context, callback: fun(response: blink.cmp.CompletionResponse | nil)): (fun(): nil) | nil 16 | --- @field filter_completions? (fun(self: blink.cmp.Source, response: blink.cmp.CompletionResponse): blink.cmp.CompletionItem[]) | nil 17 | --- @field should_show_completions? (fun(self: blink.cmp.Source, context: blink.cmp.Context, response: blink.cmp.CompletionResponse): boolean) | nil 18 | --- @field resolve? (fun(self: blink.cmp.Source, item: blink.cmp.CompletionItem, callback: fun(resolved_item: lsp.CompletionItem | nil)): ((fun(): nil) | nil)) | nil 19 | --- @field get_signature_help_trigger_characters? fun(self: blink.cmp.Source): string[] 20 | --- @field get_signature_help? fun(self: blink.cmp.Source, context: blink.cmp.SignatureHelpContext, callback: fun(signature_help: lsp.SignatureHelp | nil)): (fun(): nil) | nil 21 | --- @field reload? (fun(self: blink.cmp.Source): nil) | nil 22 | 23 | --- @class blink.cmp.SourceOverride 24 | --- @field enabled? fun(self: blink.cmp.Source, context: blink.cmp.Context): boolean 25 | --- @field get_trigger_characters? fun(self: blink.cmp.Source): string[] 26 | --- @field get_completions? fun(self: blink.cmp.Source, context: blink.cmp.Context): blink.cmp.Task 27 | --- @field filter_completions? fun(self: blink.cmp.Source, response: blink.cmp.CompletionResponse): blink.cmp.CompletionItem[] 28 | --- @field should_show_completions? fun(self: blink.cmp.Source, context: blink.cmp.Context, response: blink.cmp.CompletionResponse): boolean 29 | --- @field resolve? fun(self: blink.cmp.Source, item: blink.cmp.CompletionItem): blink.cmp.Task 30 | --- @field get_signature_help_trigger_characters? fun(self: blink.cmp.Source): string[] 31 | --- @field get_signature_help? fun(self: blink.cmp.Source, context: blink.cmp.SignatureHelpContext): blink.cmp.Task 32 | --- @field reload? (fun(self: blink.cmp.Source): nil) | nil 33 | -------------------------------------------------------------------------------- /lua/blink/cmp/windows/lib/scrollbar/init.lua: -------------------------------------------------------------------------------- 1 | --- @class blink.cmp.ScrollbarConfig 2 | --- @field enable_gutter boolean 3 | 4 | --- @class blink.cmp.Scrollbar 5 | --- @field target_win? number 6 | --- @field win? blink.cmp.ScrollbarWin 7 | --- @field autocmd? number 8 | --- 9 | --- @field new fun(opts: blink.cmp.ScrollbarConfig): blink.cmp.Scrollbar 10 | --- @field is_visible fun(self: blink.cmp.Scrollbar): boolean 11 | --- @field is_mounted fun(self: blink.cmp.Scrollbar): boolean 12 | --- @field mount fun(self: blink.cmp.Scrollbar, target_win: number) 13 | --- @field unmount fun(self: blink.cmp.Scrollbar) 14 | 15 | --- @type blink.cmp.Scrollbar 16 | --- @diagnostic disable-next-line: missing-fields 17 | local scrollbar = {} 18 | 19 | function scrollbar.new(opts) 20 | local self = setmetatable({}, { __index = scrollbar }) 21 | self.win = require('blink.cmp.windows.lib.scrollbar.win').new(opts) 22 | return self 23 | end 24 | 25 | function scrollbar:is_visible() return self.win:is_visible() end 26 | 27 | function scrollbar:is_mounted() return self.autocmd ~= nil end 28 | 29 | function scrollbar:mount(target_win) 30 | -- unmount existing scrollbar if the target window changed 31 | if self.target_win ~= target_win then 32 | if not vim.api.nvim_win_is_valid(target_win) then return end 33 | self:unmount() 34 | end 35 | -- ignore if already mounted 36 | if self:is_mounted() then return end 37 | 38 | local geometry = require('blink.cmp.windows.lib.scrollbar.geometry').get_geometry(target_win) 39 | self.win:show_thumb(geometry.thumb) 40 | self.win:show_gutter(geometry.gutter) 41 | 42 | local function update() 43 | if not vim.api.nvim_win_is_valid(target_win) then return self:unmount() end 44 | 45 | local updated_geometry = require('blink.cmp.windows.lib.scrollbar.geometry').get_geometry(target_win) 46 | if updated_geometry.should_hide then return self.win:hide() end 47 | 48 | self.win:show_thumb(updated_geometry.thumb) 49 | self.win:show_gutter(updated_geometry.gutter) 50 | end 51 | -- HACK: for some reason, the autocmds don't fire on the initial mount 52 | -- so we apply after on the next event loop iteration after the windows are definitely setup 53 | vim.schedule(update) 54 | 55 | self.autocmd = vim.api.nvim_create_autocmd( 56 | { 'WinScrolled', 'WinClosed', 'WinResized', 'CursorMoved', 'CursorMovedI' }, 57 | { callback = update } 58 | ) 59 | end 60 | 61 | function scrollbar:unmount() 62 | self.win:hide() 63 | 64 | if self.autocmd then vim.api.nvim_del_autocmd(self.autocmd) end 65 | self.autocmd = nil 66 | end 67 | 68 | return scrollbar 69 | -------------------------------------------------------------------------------- /lua/blink/cmp/accept/brackets/kind.lua: -------------------------------------------------------------------------------- 1 | local config = require('blink.cmp.config').accept.auto_brackets 2 | local utils = require('blink.cmp.accept.brackets.utils') 3 | 4 | --- @param filetype string 5 | --- @param item blink.cmp.CompletionItem 6 | --- @return 'added' | 'check_semantic_token' | 'skipped', lsp.TextEdit | lsp.InsertReplaceEdit, number 7 | local function add_brackets(filetype, item) 8 | local text_edit = item.textEdit 9 | assert(text_edit ~= nil, 'Got nil text edit while adding brackets via kind') 10 | local brackets_for_filetype = utils.get_for_filetype(filetype, item) 11 | 12 | -- if there's already the correct brackets in front, skip but indicate the cursor should move in front of the bracket 13 | -- TODO: what if the brackets_for_filetype[1] == '' or ' ' (haskell/ocaml)? 14 | if utils.has_brackets_in_front(text_edit, brackets_for_filetype[1]) then 15 | return 'skipped', text_edit, #brackets_for_filetype[1] 16 | end 17 | 18 | -- if the item already contains the brackets, conservatively skip adding brackets 19 | -- todo: won't work for snippets when the brackets_for_filetype is { '{', '}' } 20 | -- I've never seen a language like that though 21 | if brackets_for_filetype[1] ~= ' ' and text_edit.newText:match('[\\' .. brackets_for_filetype[1] .. ']') ~= nil then 22 | return 'skipped', text_edit, 0 23 | end 24 | 25 | -- check if configuration incidates we should skip 26 | if not utils.should_run_resolution(filetype, 'kind') then return 'check_semantic_token', text_edit, 0 end 27 | -- not a function, skip 28 | local CompletionItemKind = require('blink.cmp.types').CompletionItemKind 29 | if item.kind ~= CompletionItemKind.Function and item.kind ~= CompletionItemKind.Method then 30 | return 'check_semantic_token', text_edit, 0 31 | end 32 | 33 | text_edit = vim.deepcopy(text_edit) 34 | -- For snippets, we add the cursor position between the brackets as the last placeholder 35 | if item.insertTextFormat == vim.lsp.protocol.InsertTextFormat.Snippet then 36 | local placeholders = utils.snippets_extract_placeholders(text_edit.newText) 37 | local last_placeholder_index = math.max(0, unpack(placeholders)) 38 | text_edit.newText = text_edit.newText 39 | .. brackets_for_filetype[1] 40 | .. '$' 41 | .. tostring(last_placeholder_index + 1) 42 | .. brackets_for_filetype[2] 43 | -- Otherwise, we add as usual 44 | else 45 | text_edit.newText = text_edit.newText .. brackets_for_filetype[1] .. brackets_for_filetype[2] 46 | end 47 | return 'added', text_edit, -#brackets_for_filetype[2] 48 | end 49 | 50 | return add_brackets 51 | -------------------------------------------------------------------------------- /lua/blink/cmp/fuzzy/init.lua: -------------------------------------------------------------------------------- 1 | local config = require('blink.cmp.config') 2 | 3 | local fuzzy = { 4 | rust = require('blink.cmp.fuzzy.rust'), 5 | } 6 | 7 | ---@param db_path string 8 | function fuzzy.init_db(db_path) 9 | fuzzy.rust.init_db(db_path) 10 | 11 | vim.api.nvim_create_autocmd('VimLeavePre', { 12 | callback = fuzzy.rust.destroy_db, 13 | }) 14 | 15 | return fuzzy 16 | end 17 | 18 | ---@param item blink.cmp.CompletionItem 19 | function fuzzy.access(item) fuzzy.rust.access(item) end 20 | 21 | ---@param lines string 22 | function fuzzy.get_words(lines) return fuzzy.rust.get_words(lines) end 23 | 24 | ---@param needle string 25 | ---@param haystack blink.cmp.CompletionItem[]? 26 | ---@return blink.cmp.CompletionItem[] 27 | function fuzzy.filter_items(needle, haystack) 28 | haystack = haystack or {} 29 | 30 | -- get the nearby words 31 | local cursor_row = vim.api.nvim_win_get_cursor(0)[1] 32 | local start_row = math.max(0, cursor_row - 30) 33 | local end_row = math.min(cursor_row + 30, vim.api.nvim_buf_line_count(0)) 34 | local nearby_text = table.concat(vim.api.nvim_buf_get_lines(0, start_row, end_row, false), '\n') 35 | local nearby_words = #nearby_text < 10000 and fuzzy.rust.get_words(nearby_text) or {} 36 | 37 | -- perform fuzzy search 38 | local matched_indices = fuzzy.rust.fuzzy(needle, haystack, { 39 | -- each matching char is worth 4 points and it receives a bonus for capitalization, delimiter and prefix 40 | -- so this should generally be good 41 | -- TODO: make this configurable 42 | min_score = config.fuzzy.use_typo_resistance and (6 * needle:len()) or 0, 43 | max_items = config.fuzzy.max_items, 44 | use_typo_resistance = config.fuzzy.use_typo_resistance, 45 | use_frecency = config.fuzzy.use_frecency, 46 | use_proximity = config.fuzzy.use_proximity, 47 | sorts = config.fuzzy.sorts, 48 | nearby_words = nearby_words, 49 | }) 50 | 51 | local filtered_items = {} 52 | for _, idx in ipairs(matched_indices) do 53 | table.insert(filtered_items, haystack[idx + 1]) 54 | end 55 | return filtered_items 56 | end 57 | 58 | --- Gets the text under the cursor to be used for fuzzy matching 59 | --- @return string 60 | function fuzzy.get_query() 61 | local line = vim.api.nvim_get_current_line() 62 | local cmp_config = config.trigger.completion 63 | local range = require('blink.cmp.utils').get_regex_around_cursor( 64 | cmp_config.keyword_range, 65 | cmp_config.keyword_regex, 66 | cmp_config.exclude_from_prefix_regex 67 | ) 68 | return string.sub(line, range.start_col, range.start_col + range.length - 1) 69 | end 70 | 71 | return fuzzy 72 | -------------------------------------------------------------------------------- /lua/blink/cmp/sources/snippets/utils.lua: -------------------------------------------------------------------------------- 1 | local utils = {} 2 | 3 | --- Parses the json file and notifies the user if there's an error 4 | ---@param path string 5 | ---@param json string 6 | function utils.parse_json_with_error_msg(path, json) 7 | local ok, parsed = pcall(vim.json.decode, json) 8 | if not ok then 9 | vim.notify( 10 | 'Failed to parse json file "' .. path .. '" for blink.cmp snippets. Error: ' .. parsed, 11 | vim.log.levels.ERROR 12 | ) 13 | return {} 14 | end 15 | return parsed 16 | end 17 | 18 | ---@type fun(path: string): string|nil 19 | function utils.read_file(path) 20 | local file = io.open(path, 'r') 21 | if not file then return nil end 22 | local content = file:read('*a') 23 | file:close() 24 | return content 25 | end 26 | 27 | ---@type fun(input: string): vim.snippet.Node|nil 28 | function utils.safe_parse(input) 29 | local safe, parsed = pcall(vim.lsp._snippet_grammar.parse, input) 30 | if not safe then return nil end 31 | return parsed 32 | end 33 | 34 | ---@type fun(snippet: blink.cmp.Snippet, fallback: string): table 35 | function utils.read_snippet(snippet, fallback) 36 | local snippets = {} 37 | local prefix = snippet.prefix or fallback 38 | local description = snippet.description or fallback 39 | local body = snippet.body 40 | 41 | if type(description) == 'table' then description = vim.fn.join(description, '') end 42 | 43 | if type(prefix) == 'table' then 44 | for _, p in ipairs(prefix) do 45 | snippets[p] = { 46 | prefix = p, 47 | body = body, 48 | description = description, 49 | } 50 | end 51 | else 52 | snippets[prefix] = { 53 | prefix = prefix, 54 | body = body, 55 | description = description, 56 | } 57 | end 58 | return snippets 59 | end 60 | 61 | function utils.get_tab_stops(snippet) 62 | local expanded_snippet = require('blink.cmp.sources.snippets.utils').safe_parse(snippet) 63 | if not expanded_snippet then return end 64 | 65 | local tabstops = {} 66 | local grammar = require('vim.lsp._snippet_grammar') 67 | local line = 1 68 | local character = 1 69 | for _, child in ipairs(expanded_snippet.data.children) do 70 | local lines = tostring(child) == '' and {} or vim.split(tostring(child), '\n') 71 | line = line + math.max(#lines - 1, 0) 72 | character = #lines == 0 and character or #lines > 1 and #lines[#lines] or (character + #lines[#lines]) 73 | if child.type == grammar.NodeType.Placeholder or child.type == grammar.NodeType.Tabstop then 74 | table.insert(tabstops, { index = child.data.tabstop, line = line, character = character }) 75 | end 76 | end 77 | 78 | table.sort(tabstops, function(a, b) return a.index < b.index end) 79 | return tabstops 80 | end 81 | 82 | return utils 83 | -------------------------------------------------------------------------------- /lua/blink/cmp/sources/path/init.lua: -------------------------------------------------------------------------------- 1 | -- credit to https://github.com/hrsh7th/cmp-path for the original implementation 2 | -- and https://codeberg.org/FelipeLema/cmp-async-path for the async implementation 3 | 4 | local path = {} 5 | local NAME_REGEX = '\\%([^/\\\\:\\*?<>\'"`\\|]\\)' 6 | local PATH_REGEX = 7 | assert(vim.regex(([[\%(\%(/PAT*[^/\\\\:\\*?<>\'"`\\| .~]\)\|\%(/\.\.\)\)*/\zePAT*$]]):gsub('PAT', NAME_REGEX))) 8 | 9 | function path.new(opts) 10 | local self = setmetatable({}, { __index = path }) 11 | 12 | opts = vim.tbl_deep_extend('keep', opts or {}, { 13 | trailing_slash = false, 14 | label_trailing_slash = true, 15 | get_cwd = function(context) return vim.fn.expand(('#%d:p:h'):format(context.bufnr)) end, 16 | show_hidden_files_by_default = false, 17 | }) 18 | vim.validate({ 19 | trailing_slash = { opts.trailing_slash, 'boolean' }, 20 | label_trailing_slash = { opts.label_trailing_slash, 'boolean' }, 21 | get_cwd = { opts.get_cwd, 'function' }, 22 | show_hidden_files_by_default = { opts.show_hidden_files_by_default, 'boolean' }, 23 | }) 24 | 25 | self.opts = opts 26 | return self 27 | end 28 | 29 | function path:get_trigger_characters() return { '/', '.' } end 30 | 31 | function path:get_completions(context, callback) 32 | -- we use libuv, but the rest of the library expects to be synchronous 33 | callback = vim.schedule_wrap(callback) 34 | 35 | local lib = require('blink.cmp.sources.path.lib') 36 | 37 | local dirname = lib.dirname(PATH_REGEX, self.opts.get_cwd, context) 38 | if not dirname then return callback({ is_incomplete_forward = false, is_incomplete_backward = false, items = {} }) end 39 | 40 | local include_hidden = self.opts.show_hidden_files_by_default 41 | or string.sub(context.line, context.bounds.start_col - 1, context.bounds.start_col - 1) == '.' 42 | lib 43 | .candidates(dirname, include_hidden, self.opts) 44 | :map( 45 | function(candidates) 46 | callback({ is_incomplete_forward = false, is_incomplete_backward = false, items = candidates }) 47 | end 48 | ) 49 | :catch(function() callback() end) 50 | end 51 | 52 | function path:resolve(item, callback) 53 | require('blink.cmp.sources.path.fs') 54 | .read_file(item.data.full_path, 1024) 55 | :map(function(content) 56 | local is_binary = content:find('\0') 57 | 58 | -- binary file 59 | if is_binary then 60 | item.documentation = { 61 | kind = 'plaintext', 62 | value = 'Binary file', 63 | } 64 | -- highlight with markdown 65 | else 66 | local ext = vim.fn.fnamemodify(item.data.path, ':e') 67 | item.documentation = { 68 | kind = 'markdown', 69 | value = '```' .. ext .. '\n' .. content .. '```', 70 | } 71 | end 72 | 73 | return item 74 | end) 75 | :map(function(resolved_item) callback(resolved_item) end) 76 | :catch(function() callback(item) end) 77 | end 78 | 79 | return path 80 | -------------------------------------------------------------------------------- /flake.lock: -------------------------------------------------------------------------------- 1 | { 2 | "nodes": { 3 | "fenix": { 4 | "inputs": { 5 | "nixpkgs": [ 6 | "nixpkgs" 7 | ], 8 | "rust-analyzer-src": "rust-analyzer-src" 9 | }, 10 | "locked": { 11 | "lastModified": 1730097176, 12 | "narHash": "sha256-ufvRff76Y19mkRsmx+mAnxKE9A9VaNWC2mVY6TwumOw=", 13 | "owner": "nix-community", 14 | "repo": "fenix", 15 | "rev": "482b57f3f27a9336e0fbc62fa99ee0f624ccf4d0", 16 | "type": "github" 17 | }, 18 | "original": { 19 | "owner": "nix-community", 20 | "repo": "fenix", 21 | "type": "github" 22 | } 23 | }, 24 | "flake-parts": { 25 | "inputs": { 26 | "nixpkgs-lib": "nixpkgs-lib" 27 | }, 28 | "locked": { 29 | "lastModified": 1727826117, 30 | "narHash": "sha256-K5ZLCyfO/Zj9mPFldf3iwS6oZStJcU4tSpiXTMYaaL0=", 31 | "owner": "hercules-ci", 32 | "repo": "flake-parts", 33 | "rev": "3d04084d54bedc3d6b8b736c70ef449225c361b1", 34 | "type": "github" 35 | }, 36 | "original": { 37 | "owner": "hercules-ci", 38 | "repo": "flake-parts", 39 | "type": "github" 40 | } 41 | }, 42 | "nixpkgs": { 43 | "locked": { 44 | "lastModified": 1716977621, 45 | "narHash": "sha256-Q1UQzYcMJH4RscmpTkjlgqQDX5yi1tZL0O345Ri6vXQ=", 46 | "owner": "cachix", 47 | "repo": "devenv-nixpkgs", 48 | "rev": "4267e705586473d3e5c8d50299e71503f16a6fb6", 49 | "type": "github" 50 | }, 51 | "original": { 52 | "owner": "cachix", 53 | "ref": "rolling", 54 | "repo": "devenv-nixpkgs", 55 | "type": "github" 56 | } 57 | }, 58 | "nixpkgs-lib": { 59 | "locked": { 60 | "lastModified": 1727825735, 61 | "narHash": "sha256-0xHYkMkeLVQAMa7gvkddbPqpxph+hDzdu1XdGPJR+Os=", 62 | "type": "tarball", 63 | "url": "https://github.com/NixOS/nixpkgs/archive/fb192fec7cc7a4c26d51779e9bab07ce6fa5597a.tar.gz" 64 | }, 65 | "original": { 66 | "type": "tarball", 67 | "url": "https://github.com/NixOS/nixpkgs/archive/fb192fec7cc7a4c26d51779e9bab07ce6fa5597a.tar.gz" 68 | } 69 | }, 70 | "root": { 71 | "inputs": { 72 | "fenix": "fenix", 73 | "flake-parts": "flake-parts", 74 | "nixpkgs": "nixpkgs" 75 | } 76 | }, 77 | "rust-analyzer-src": { 78 | "flake": false, 79 | "locked": { 80 | "lastModified": 1730028316, 81 | "narHash": "sha256-FsPsSjqnqMHBgDdM24DFLw4YOw0mFKYFJBcLaI6CvI8=", 82 | "owner": "rust-lang", 83 | "repo": "rust-analyzer", 84 | "rev": "3b3a87fe9bd3f2a79942babc1d1e385b6805c384", 85 | "type": "github" 86 | }, 87 | "original": { 88 | "owner": "rust-lang", 89 | "ref": "nightly", 90 | "repo": "rust-analyzer", 91 | "type": "github" 92 | } 93 | } 94 | }, 95 | "root": "root", 96 | "version": 7 97 | } 98 | -------------------------------------------------------------------------------- /lua/blink/cmp/sources/snippets/scan.lua: -------------------------------------------------------------------------------- 1 | local utils = require('blink.cmp.sources.snippets.utils') 2 | local scan = {} 3 | 4 | function scan.register_snippets(search_paths) 5 | local registry = {} 6 | 7 | for _, path in ipairs(search_paths) do 8 | local files = scan.load_package_json(path) or scan.scan_for_snippets(path) 9 | for ft, file in pairs(files) do 10 | local key 11 | if type(ft) == 'number' then 12 | key = vim.fn.fnamemodify(files[ft], ':t:r') 13 | else 14 | key = ft 15 | end 16 | 17 | if not key then return end 18 | 19 | registry[key] = registry[key] or {} 20 | if type(file) == 'table' then 21 | vim.list_extend(registry[key], file) 22 | else 23 | table.insert(registry[key], file) 24 | end 25 | end 26 | end 27 | 28 | return registry 29 | end 30 | 31 | ---@type fun(self: utils, dir: string, result?: string[]): string[] 32 | ---@return string[] 33 | function scan.scan_for_snippets(dir, result) 34 | result = result or {} 35 | 36 | local stat = vim.uv.fs_stat(dir) 37 | if not stat then return result end 38 | 39 | if stat.type == 'directory' then 40 | local req = vim.uv.fs_scandir(dir) 41 | if not req then return result end 42 | 43 | local function iter() return vim.uv.fs_scandir_next(req) end 44 | 45 | for name, ftype in iter do 46 | local path = string.format('%s/%s', dir, name) 47 | 48 | if ftype == 'directory' then 49 | result[name] = scan.scan_for_snippets(path, result[name] or {}) 50 | else 51 | scan.scan_for_snippets(path, result) 52 | end 53 | end 54 | elseif stat.type == 'file' then 55 | local name = vim.fn.fnamemodify(dir, ':t') 56 | 57 | if name:match('%.json$') then table.insert(result, dir) end 58 | elseif stat.type == 'link' then 59 | local target = vim.uv.fs_readlink(dir) 60 | 61 | if target then scan.scan_for_snippets(target, result) end 62 | end 63 | 64 | return result 65 | end 66 | 67 | --- This will try to load the snippets from the package.json file 68 | ---@param path string 69 | function scan.load_package_json(path) 70 | local file = path .. '/package.json' 71 | -- todo: ideally this is async, although it takes 0.5ms on my system so it might not matter 72 | local data = utils.read_file(file) 73 | if not data then return end 74 | 75 | local pkg = require('blink.cmp.sources.snippets.utils').parse_json_with_error_msg(file, data) 76 | 77 | ---@type {path: string, language: string|string[]}[] 78 | local snippets = vim.tbl_get(pkg, 'contributes', 'snippets') 79 | if not snippets then return end 80 | 81 | local ret = {} ---@type table 82 | for _, s in ipairs(snippets) do 83 | local langs = s.language or {} 84 | langs = type(langs) == 'string' and { langs } or langs 85 | ---@cast langs string[] 86 | for _, lang in ipairs(langs) do 87 | ret[lang] = ret[lang] or {} 88 | table.insert(ret[lang], vim.fs.normalize(vim.fs.joinpath(path, s.path))) 89 | end 90 | end 91 | return ret 92 | end 93 | 94 | return scan 95 | -------------------------------------------------------------------------------- /lua/blink/cmp/sources/buffer.lua: -------------------------------------------------------------------------------- 1 | -- todo: nvim-cmp only updates the lines that got changed which is better 2 | -- but this is *speeeeeed* and simple. should add the better way 3 | -- but ensure it doesn't add too much complexity 4 | 5 | local uv = vim.uv 6 | 7 | ---@return string 8 | local function get_buf_text() 9 | local bufnr = vim.api.nvim_get_current_buf() 10 | local lines = vim.api.nvim_buf_get_lines(bufnr, 0, -1, false) 11 | 12 | -- exclude word under the cursor 13 | local line_number = vim.api.nvim_win_get_cursor(0)[1] 14 | local column = vim.api.nvim_win_get_cursor(0)[2] 15 | local line = lines[line_number] 16 | local start_col = column 17 | while start_col > 1 do 18 | local char = line:sub(start_col, start_col) 19 | if char:match('[%w_\\-]') == nil then 20 | start_col = start_col + 1 21 | break 22 | end 23 | start_col = start_col - 1 24 | end 25 | local end_col = column 26 | while end_col < #line do 27 | local char = line:sub(end_col + 1, end_col + 1) 28 | if char:match('[%w_\\-]') == nil then break end 29 | end_col = end_col + 1 30 | end 31 | lines[line_number] = line:sub(1, start_col) .. ' ' .. line:sub(end_col + 1) 32 | 33 | return table.concat(lines, '\n') 34 | end 35 | 36 | local function words_to_items(words) 37 | local items = {} 38 | for _, word in ipairs(words) do 39 | table.insert(items, { 40 | label = word, 41 | kind = require('blink.cmp.types').CompletionItemKind.Text, 42 | insertTextFormat = vim.lsp.protocol.InsertTextFormat.PlainText, 43 | insertText = word, 44 | }) 45 | end 46 | return items 47 | end 48 | 49 | --- @param buf_text string 50 | --- @param callback fun(items: blink.cmp.CompletionItem[]) 51 | local function run_sync(buf_text, callback) callback(words_to_items(require('blink.cmp.fuzzy').get_words(buf_text))) end 52 | 53 | local function run_async(buf_text, callback) 54 | local worker = uv.new_work( 55 | -- must use ffi directly since the normal one requires the config which isnt present 56 | function(items) return table.concat(require('blink.cmp.fuzzy.rust').get_words(items), '\n') end, 57 | function(words) 58 | local items = words_to_items(vim.split(words, '\n')) 59 | vim.schedule(function() callback(items) end) 60 | end 61 | ) 62 | worker:queue(buf_text) 63 | end 64 | 65 | --- Public API 66 | 67 | --- @class blink.cmp.Source 68 | local buffer = {} 69 | 70 | function buffer.new() return setmetatable({}, { __index = buffer }) end 71 | 72 | function buffer:get_completions(_, callback) 73 | local transformed_callback = function(items) 74 | callback({ is_incomplete_forward = false, is_incomplete_backward = false, items = items }) 75 | end 76 | 77 | local buf_text = get_buf_text() 78 | -- should take less than 2ms 79 | if #buf_text < 20000 then 80 | run_sync(buf_text, transformed_callback) 81 | -- should take less than 10ms 82 | elseif #buf_text < 500000 then 83 | run_async(buf_text, transformed_callback) 84 | -- too big so ignore 85 | else 86 | transformed_callback({}) 87 | end 88 | 89 | -- TODO: cancel run_async 90 | return function() end 91 | end 92 | 93 | return buffer 94 | -------------------------------------------------------------------------------- /lua/blink/cmp/accept/text-edits.lua: -------------------------------------------------------------------------------- 1 | local config = require('blink.cmp.config') 2 | local text_edits = {} 3 | 4 | --- @param item blink.cmp.CompletionItem 5 | --- @return lsp.TextEdit 6 | function text_edits.get_from_item(item) 7 | -- Adjust the position of the text edit to be the current cursor position 8 | -- since the data might be outdated. We compare the cursor column position 9 | -- from when the items were fetched versus the current. 10 | -- hack: is there a better way? 11 | if item.textEdit ~= nil then 12 | -- FIXME: temporarily convert insertReplaceEdit to regular textEdit 13 | if item.textEdit.insert ~= nil then 14 | item.textEdit.range = item.textEdit.insert 15 | elseif item.textEdit.replace ~= nil then 16 | item.textEdit.range = item.textEdit.replace 17 | end 18 | 19 | local text_edit = vim.deepcopy(item.textEdit) 20 | local offset = vim.api.nvim_win_get_cursor(0)[2] - item.cursor_column 21 | text_edit.range['end'].character = text_edit.range['end'].character + offset 22 | return text_edit 23 | end 24 | 25 | -- No text edit so we fallback to our own resolution 26 | return text_edits.guess_text_edit(item) 27 | end 28 | 29 | --- @param client_id number 30 | --- @param edits lsp.TextEdit[] 31 | function text_edits.apply_text_edits(client_id, edits) 32 | local client = vim.lsp.get_client_by_id(client_id) 33 | local offset_encoding = client ~= nil and client.offset_encoding or 'utf-16' 34 | vim.lsp.util.apply_text_edits(edits, vim.api.nvim_get_current_buf(), offset_encoding) 35 | end 36 | 37 | --- @param text_edit lsp.TextEdit 38 | function text_edits.get_undo_text_edit_range(text_edit) 39 | text_edit = vim.deepcopy(text_edit) 40 | local lines = vim.split(text_edit.newText, '\n') 41 | local last_line_len = lines[#lines] and #lines[#lines] or 0 42 | 43 | local range = text_edit.range 44 | range['end'].line = range.start.line + #lines - 1 45 | range['end'].character = #lines > 1 and last_line_len or range.start.character + last_line_len 46 | 47 | return range 48 | end 49 | 50 | function text_edits.undo_text_edit(text_edit) 51 | text_edit = vim.deepcopy(text_edit) 52 | text_edit.range = text_edits.get_undo_text_edit_range(text_edit) 53 | text_edit.newText = '' 54 | 55 | vim.lsp.util.apply_text_edits({ text_edit }, vim.api.nvim_get_current_buf(), 'utf-16') 56 | end 57 | 58 | --- @param item blink.cmp.CompletionItem 59 | --- TODO: doesnt work when the item contains characters not included in the context regex 60 | function text_edits.guess_text_edit(item) 61 | local word = item.textEditText or item.insertText or item.label 62 | 63 | local cmp_config = config.trigger.completion 64 | local range = require('blink.cmp.utils').get_regex_around_cursor( 65 | cmp_config.keyword_range, 66 | cmp_config.keyword_regex, 67 | cmp_config.exclude_from_prefix_regex 68 | ) 69 | local current_line = vim.api.nvim_win_get_cursor(0)[1] 70 | 71 | -- convert to 0-index 72 | return { 73 | range = { 74 | start = { line = current_line - 1, character = range.start_col - 1 }, 75 | ['end'] = { line = current_line - 1, character = range.start_col - 1 + range.length }, 76 | }, 77 | newText = word, 78 | } 79 | end 80 | 81 | return text_edits 82 | -------------------------------------------------------------------------------- /lua/blink/cmp/fuzzy/lib.rs: -------------------------------------------------------------------------------- 1 | use crate::frecency::FrecencyTracker; 2 | use crate::fuzzy::FuzzyOptions; 3 | use crate::lsp_item::LspItem; 4 | use lazy_static::lazy_static; 5 | use mlua::prelude::*; 6 | use regex::Regex; 7 | use std::collections::HashSet; 8 | use std::sync::RwLock; 9 | 10 | mod frecency; 11 | mod fuzzy; 12 | mod lsp_item; 13 | 14 | lazy_static! { 15 | static ref REGEX: Regex = Regex::new(r"[A-Za-z][A-Za-z0-9_\\-]{2,32}").unwrap(); 16 | static ref FRECENCY: RwLock> = RwLock::new(None); 17 | } 18 | 19 | pub fn init_db(_: &Lua, db_path: String) -> LuaResult { 20 | let mut frecency = FRECENCY.write().map_err(|_| { 21 | mlua::Error::RuntimeError("Failed to acquire lock for frecency".to_string()) 22 | })?; 23 | if frecency.is_some() { 24 | return Ok(false); 25 | } 26 | *frecency = Some(FrecencyTracker::new(&db_path)?); 27 | Ok(true) 28 | } 29 | 30 | pub fn destroy_db(_: &Lua, _: ()) -> LuaResult { 31 | let frecency = FRECENCY.write().map_err(|_| { 32 | mlua::Error::RuntimeError("Failed to acquire lock for frecency".to_string()) 33 | })?; 34 | drop(frecency); 35 | 36 | let mut frecency = FRECENCY.write().map_err(|_| { 37 | mlua::Error::RuntimeError("Failed to acquire lock for frecency".to_string()) 38 | })?; 39 | *frecency = None; 40 | 41 | Ok(true) 42 | } 43 | 44 | pub fn access(_: &Lua, item: LspItem) -> LuaResult { 45 | let mut frecency_handle = FRECENCY.write().map_err(|_| { 46 | mlua::Error::RuntimeError("Failed to acquire lock for frecency".to_string()) 47 | })?; 48 | let frecency = frecency_handle.as_mut().ok_or_else(|| { 49 | mlua::Error::RuntimeError("Attempted to use frencecy before initialization".to_string()) 50 | })?; 51 | frecency.access(&item)?; 52 | Ok(true) 53 | } 54 | 55 | pub fn fuzzy( 56 | _lua: &Lua, 57 | (needle, haystack, opts): (String, Vec, FuzzyOptions), 58 | ) -> LuaResult> { 59 | let mut frecency_handle = FRECENCY.write().map_err(|_| { 60 | mlua::Error::RuntimeError("Failed to acquire lock for frecency".to_string()) 61 | })?; 62 | let frecency = frecency_handle.as_mut().ok_or_else(|| { 63 | mlua::Error::RuntimeError("Attempted to use frencecy before initialization".to_string()) 64 | })?; 65 | 66 | Ok(fuzzy::fuzzy(needle, haystack, frecency, opts) 67 | .into_iter() 68 | .map(|i| i as u32) 69 | .collect()) 70 | } 71 | 72 | pub fn get_words(_: &Lua, text: String) -> LuaResult> { 73 | Ok(REGEX 74 | .find_iter(&text) 75 | .map(|m| m.as_str().to_string()) 76 | .collect::>() 77 | .into_iter() 78 | .collect()) 79 | } 80 | 81 | // NOTE: skip_memory_check greatly improves performance 82 | // https://github.com/mlua-rs/mlua/issues/318 83 | #[mlua::lua_module(skip_memory_check)] 84 | fn blink_cmp_fuzzy(lua: &Lua) -> LuaResult { 85 | let exports = lua.create_table()?; 86 | exports.set("fuzzy", lua.create_function(fuzzy)?)?; 87 | exports.set("get_words", lua.create_function(get_words)?)?; 88 | exports.set("init_db", lua.create_function(init_db)?)?; 89 | exports.set("destroy_db", lua.create_function(destroy_db)?)?; 90 | exports.set("access", lua.create_function(access)?)?; 91 | Ok(exports) 92 | } 93 | -------------------------------------------------------------------------------- /lua/blink/cmp/windows/lib/scrollbar/win.lua: -------------------------------------------------------------------------------- 1 | --- Manages creating/updating scrollbar gutter and thumb windows 2 | 3 | --- @class blink.cmp.ScrollbarWin 4 | --- @field enable_gutter boolean 5 | --- @field thumb_win? number 6 | --- @field gutter_win? number 7 | --- @field buf? number 8 | --- 9 | --- @field new fun(opts: blink.cmp.ScrollbarConfig): blink.cmp.ScrollbarWin 10 | --- @field is_visible fun(self: blink.cmp.ScrollbarWin): boolean 11 | --- @field show_thumb fun(self: blink.cmp.ScrollbarWin, geometry: blink.cmp.ScrollbarGeometry) 12 | --- @field show_gutter fun(self: blink.cmp.ScrollbarWin, geometry: blink.cmp.ScrollbarGeometry) 13 | --- @field hide_thumb fun(self: blink.cmp.ScrollbarWin) 14 | --- @field hide_gutter fun(self: blink.cmp.ScrollbarWin) 15 | --- @field hide fun(self: blink.cmp.ScrollbarWin) 16 | --- @field _make_win fun(self: blink.cmp.ScrollbarWin, geometry: blink.cmp.ScrollbarGeometry, hl_group: string): number 17 | 18 | local scrollbar_win = {} 19 | 20 | function scrollbar_win.new(opts) return setmetatable(opts, { __index = scrollbar_win }) end 21 | 22 | function scrollbar_win:is_visible() return self.thumb_win ~= nil and vim.api.nvim_win_is_valid(self.thumb_win) end 23 | 24 | function scrollbar_win:show_thumb(geometry) 25 | -- create window if it doesn't exist 26 | if self.thumb_win == nil or not vim.api.nvim_win_is_valid(self.thumb_win) then 27 | self.thumb_win = self:_make_win(geometry, 'BlinkCmpScrollBarThumb') 28 | end 29 | 30 | -- update with the geometry 31 | local thumb_existing_config = vim.api.nvim_win_get_config(self.thumb_win) 32 | local thumb_config = vim.tbl_deep_extend('force', thumb_existing_config, geometry) 33 | vim.api.nvim_win_set_config(self.thumb_win, thumb_config) 34 | end 35 | 36 | function scrollbar_win:show_gutter(geometry) 37 | if not self.enable_gutter then return end 38 | 39 | -- create window if it doesn't exist 40 | if self.gutter_win == nil or not vim.api.nvim_win_is_valid(self.gutter_win) then 41 | self.gutter_win = self:_make_win(geometry, 'BlinkCmpScrollBarGutter') 42 | end 43 | 44 | -- update with the geometry 45 | local gutter_existing_config = vim.api.nvim_win_get_config(self.gutter_win) 46 | local gutter_config = vim.tbl_deep_extend('force', gutter_existing_config, geometry) 47 | vim.api.nvim_win_set_config(self.gutter_win, gutter_config) 48 | end 49 | 50 | function scrollbar_win:hide_thumb() 51 | if self.thumb_win and vim.api.nvim_win_is_valid(self.thumb_win) then vim.api.nvim_win_close(self.thumb_win, true) end 52 | end 53 | 54 | function scrollbar_win:hide_gutter() 55 | if self.gutter_win and vim.api.nvim_win_is_valid(self.gutter_win) then 56 | vim.api.nvim_win_close(self.gutter_win, true) 57 | end 58 | end 59 | 60 | function scrollbar_win:hide() 61 | self:hide_thumb() 62 | self:hide_gutter() 63 | if self.buf and vim.api.nvim_buf_is_valid(self.buf) then vim.api.nvim_buf_delete(self.buf, { force = true }) end 64 | end 65 | 66 | function scrollbar_win:_make_win(geometry, hl_group) 67 | if self.buf == nil or not vim.api.nvim_buf_is_valid(self.buf) then self.buf = vim.api.nvim_create_buf(false, true) end 68 | 69 | local win_config = vim.tbl_deep_extend('force', geometry, { 70 | style = 'minimal', 71 | focusable = false, 72 | noautocmd = true, 73 | }) 74 | local win = vim.api.nvim_open_win(self.buf, false, win_config) 75 | vim.api.nvim_set_option_value('winhighlight', 'Normal:' .. hl_group .. ',EndOfBuffer:' .. hl_group, { win = win }) 76 | return win 77 | end 78 | 79 | return scrollbar_win 80 | -------------------------------------------------------------------------------- /flake.nix: -------------------------------------------------------------------------------- 1 | { 2 | description = "Set of simple, performant neovim plugins"; 3 | 4 | inputs = { 5 | nixpkgs.url = "github:cachix/devenv-nixpkgs/rolling"; 6 | flake-parts.url = "github:hercules-ci/flake-parts"; 7 | fenix.url = "github:nix-community/fenix"; 8 | fenix.inputs.nixpkgs.follows = "nixpkgs"; 9 | }; 10 | 11 | outputs = inputs@{ flake-parts, nixpkgs, ... }: 12 | flake-parts.lib.mkFlake { inherit inputs; } { 13 | systems = 14 | [ "x86_64-linux" "x86_64-darwin" "aarch64-linux" "aarch64-darwin" ]; 15 | 16 | perSystem = { self, config, self', inputs', pkgs, system, lib, ... }: { 17 | # use fenix overlay 18 | _module.args.pkgs = import nixpkgs { 19 | inherit system; 20 | overlays = [ inputs.fenix.overlays.default ]; 21 | }; 22 | 23 | # define the packages provided by this flake 24 | packages = let 25 | inherit (inputs.fenix.packages.${system}.minimal) toolchain; 26 | inherit (pkgs.stdenv) hostPlatform; 27 | 28 | rustPlatform = pkgs.makeRustPlatform { 29 | cargo = toolchain; 30 | rustc = toolchain; 31 | }; 32 | 33 | src = ./.; 34 | version = "2024-08-02"; 35 | 36 | blink-fuzzy-lib = rustPlatform.buildRustPackage { 37 | pname = "blink-fuzzy-lib"; 38 | inherit src version; 39 | cargoLock = { 40 | lockFile = ./Cargo.lock; 41 | outputHashes = { 42 | "frizbee-0.1.0" = 43 | "sha256-eYth+xOIqwGPkH39OxNCMA9zE+5CTNpsuX8Ue/mySIA="; 44 | }; 45 | }; 46 | }; 47 | 48 | libExt = if hostPlatform.isDarwin then 49 | "dylib" 50 | else if hostPlatform.isWindows then 51 | "dll" 52 | else 53 | "so"; 54 | in { 55 | blink-cmp = pkgs.vimUtils.buildVimPlugin { 56 | pname = "blink-cmp"; 57 | inherit src version; 58 | preInstall = '' 59 | mkdir -p target/release 60 | ln -s ${blink-fuzzy-lib}/lib/libblink_cmp_fuzzy.${libExt} target/release/libblink_cmp_fuzzy.${libExt} 61 | ''; 62 | 63 | meta = { 64 | description = 65 | "Performant, batteries-included completion plugin for Neovim "; 66 | homepage = "https://github.com/saghen/blink.cmp"; 67 | license = lib.licenses.mit; 68 | maintainers = with lib.maintainers; [ redxtech ]; 69 | }; 70 | }; 71 | 72 | default = self'.packages.blink-cmp; 73 | }; 74 | 75 | # builds the native module of the plugin 76 | apps.build-plugin = { 77 | type = "app"; 78 | program = let 79 | buildScript = pkgs.writeShellApplication { 80 | name = "build-plugin"; 81 | runtimeInputs = with pkgs; [ 82 | fenix.complete.toolchain 83 | rust-analyzer-nightly 84 | ]; 85 | text = '' 86 | cargo build --release 87 | ''; 88 | }; 89 | in (lib.getExe buildScript); 90 | }; 91 | 92 | # define the default dev environment 93 | devShells.default = pkgs.mkShell { 94 | name = "blink"; 95 | packages = with pkgs; [ fenix.minimal.toolchain ]; 96 | }; 97 | }; 98 | }; 99 | } 100 | -------------------------------------------------------------------------------- /lua/blink/cmp/accept/init.lua: -------------------------------------------------------------------------------- 1 | local text_edits_lib = require('blink.cmp.accept.text-edits') 2 | local brackets_lib = require('blink.cmp.accept.brackets') 3 | 4 | --- Applies a completion item to the current buffer 5 | --- @param item blink.cmp.CompletionItem 6 | local function accept(item) 7 | require('blink.cmp.trigger.completion').hide() 8 | 9 | -- start the resolve immediately since text changes can invalidate the item 10 | -- with some LSPs (i.e. rust-analyzer) causing them to return the item as-is 11 | -- without i.e. auto-imports 12 | require('blink.cmp.sources.lib') 13 | .resolve(item) 14 | :map(function(resolved_item) 15 | local all_text_edits = 16 | vim.deepcopy(resolved_item and resolved_item.additionalTextEdits or item.additionalTextEdits or {}) 17 | 18 | -- create an undo point 19 | if require('blink.cmp.config').accept.create_undo_point then 20 | vim.api.nvim_feedkeys(vim.api.nvim_replace_termcodes('u', true, true, true), 'n', true) 21 | end 22 | 23 | item = vim.deepcopy(item) 24 | item.textEdit = text_edits_lib.get_from_item(item) 25 | 26 | -- Add brackets to the text edit if needed 27 | local brackets_status, text_edit_with_brackets, offset = brackets_lib.add_brackets(vim.bo.filetype, item) 28 | item.textEdit = text_edit_with_brackets 29 | 30 | -- Snippet 31 | if item.insertTextFormat == vim.lsp.protocol.InsertTextFormat.Snippet then 32 | -- We want to handle offset_encoding and the text edit api can do this for us 33 | -- so we empty the newText and apply 34 | local temp_text_edit = vim.deepcopy(item.textEdit) 35 | temp_text_edit.newText = '' 36 | table.insert(all_text_edits, temp_text_edit) 37 | text_edits_lib.apply_text_edits(item.client_id, all_text_edits) 38 | 39 | -- Expand the snippet 40 | vim.snippet.expand(item.textEdit.newText) 41 | 42 | -- OR Normal: Apply the text edit and move the cursor 43 | else 44 | table.insert(all_text_edits, item.textEdit) 45 | text_edits_lib.apply_text_edits(item.client_id, all_text_edits) 46 | -- TODO: should move the cursor only by the offset since text edit handles everything else? 47 | vim.api.nvim_win_set_cursor(0, { 48 | vim.api.nvim_win_get_cursor(0)[1], 49 | item.textEdit.range.start.character + #item.textEdit.newText + offset, 50 | }) 51 | end 52 | 53 | -- Check semantic tokens for brackets, if needed, and apply additional text edits 54 | if brackets_status == 'check_semantic_token' then 55 | -- TODO: since we apply the additional text edits after, auto imported functions will not 56 | -- get auto brackets. If we apply them before, we have to modify the textEdit to compensate 57 | brackets_lib.add_brackets_via_semantic_token(vim.bo.filetype, item, function() 58 | require('blink.cmp.trigger.completion').show_if_on_trigger_character({ is_accept = true }) 59 | require('blink.cmp.trigger.signature').show_if_on_trigger_character() 60 | end) 61 | else 62 | require('blink.cmp.trigger.completion').show_if_on_trigger_character({ is_accept = true }) 63 | require('blink.cmp.trigger.signature').show_if_on_trigger_character() 64 | end 65 | 66 | -- Notify the rust module that the item was accessed 67 | -- TODO: why is this so slow? (10ms) 68 | vim.schedule(function() require('blink.cmp.fuzzy').access(item) end) 69 | end) 70 | :catch(function(err) vim.notify(err, vim.log.levels.ERROR) end) 71 | end 72 | 73 | return accept 74 | -------------------------------------------------------------------------------- /lua/blink/cmp/utils.lua: -------------------------------------------------------------------------------- 1 | local utils = {} 2 | 3 | --- Shallow copy table 4 | --- @generic T 5 | --- @param t T 6 | --- @return T 7 | function utils.shallow_copy(t) 8 | local t2 = {} 9 | for k, v in pairs(t) do 10 | t2[k] = v 11 | end 12 | return t2 13 | end 14 | 15 | --- Returns the union of the keys of two tables 16 | --- @generic T 17 | --- @param t1 T[] 18 | --- @param t2 T[] 19 | --- @return T[] 20 | function utils.union_keys(t1, t2) 21 | local t3 = {} 22 | for k, _ in pairs(t1) do 23 | t3[k] = true 24 | end 25 | for k, _ in pairs(t2) do 26 | t3[k] = true 27 | end 28 | return vim.tbl_keys(t3) 29 | end 30 | 31 | --- Determines whether the current buffer is a "special" buffer or if the filetype is in the list of ignored filetypes 32 | --- @return boolean 33 | function utils.is_blocked_buffer() 34 | local bufnr = vim.api.nvim_get_current_buf() 35 | local buftype = vim.api.nvim_get_option_value('buftype', { buf = bufnr }) 36 | local blocked_filetypes = require('blink.cmp.config').blocked_filetypes or {} 37 | local buf_filetype = vim.api.nvim_get_option_value('filetype', { buf = bufnr }) 38 | 39 | if vim.tbl_contains(blocked_filetypes, buf_filetype) then return true end 40 | return buftype ~= '' 41 | end 42 | 43 | --- Gets characters around the cursor and returns the range, 0-indexed 44 | --- @param range 'prefix' | 'full' 45 | --- @param regex string 46 | --- @param exclude_from_prefix_regex string 47 | --- @return { start_col: number, length: number } 48 | --- TODO: switch to return start_col, length to simplify downstream logic 49 | function utils.get_regex_around_cursor(range, regex, exclude_from_prefix_regex) 50 | local current_col = vim.api.nvim_win_get_cursor(0)[2] + 1 51 | local line = vim.api.nvim_get_current_line() 52 | 53 | -- Search backward for the start of the word 54 | local start_col = current_col 55 | local length = 0 56 | while start_col > 0 do 57 | local char = line:sub(start_col - 1, start_col - 1) 58 | if char:match(regex) == nil then break end 59 | start_col = start_col - 1 60 | length = length + 1 61 | end 62 | 63 | -- Search forward for the end of the word if configured 64 | if range == 'full' then 65 | while start_col + length < #line do 66 | local col = start_col + length 67 | local char = line:sub(col, col) 68 | if char:match(regex) == nil then break end 69 | length = length + 1 70 | end 71 | end 72 | 73 | -- exclude characters matching exclude_prefix_regex from the beginning of the bounds 74 | if exclude_from_prefix_regex ~= nil then 75 | while length > 0 do 76 | local char = line:sub(start_col, start_col) 77 | if char:match(exclude_from_prefix_regex) == nil then break end 78 | start_col = start_col + 1 79 | length = length - 1 80 | end 81 | end 82 | 83 | return { start_col = start_col, length = length } 84 | end 85 | 86 | --- @param ctx blink.cmp.CompletionRenderContext 87 | --- @return string|nil 88 | function utils.try_get_tailwind_hl(ctx) 89 | local doc = ctx.item.documentation 90 | if ctx.kind == 'Color' and doc then 91 | local content = type(doc) == 'string' and doc or doc.value 92 | if ctx.kind == 'Color' and content and content:match('^#%x%x%x%x%x%x$') then 93 | local hl_name = 'HexColor' .. content:sub(2) 94 | if #vim.api.nvim_get_hl(0, { name = hl_name }) == 0 then 95 | vim.api.nvim_set_hl(0, hl_name, { fg = content }) 96 | end 97 | return hl_name 98 | end 99 | end 100 | end 101 | 102 | return utils 103 | -------------------------------------------------------------------------------- /lua/blink/cmp/windows/ghost-text.lua: -------------------------------------------------------------------------------- 1 | local config = require('blink.cmp.config') 2 | local autocomplete = require('blink.cmp.windows.autocomplete') 3 | local text_edits_lib = require('blink.cmp.accept.text-edits') 4 | local snippets_utils = require('blink.cmp.sources.snippets.utils') 5 | 6 | local ghost_text_config = config.windows.ghost_text 7 | 8 | --- @class blink.cmp.windows.ghost_text 9 | --- @field win integer? 10 | --- @field selected_item blink.cmp.CompletionItem? 11 | --- @field extmark_id integer? 12 | local ghost_text = { 13 | win = nil, 14 | selected_item = nil, 15 | } 16 | 17 | --- @param textEdit lsp.TextEdit 18 | local function get_still_untyped_text(textEdit) 19 | local type_text_length = textEdit.range['end'].character - textEdit.range.start.character 20 | local result = textEdit.newText:sub(type_text_length + 1) 21 | return result 22 | end 23 | 24 | function ghost_text.setup() 25 | -- immediately re-draw the preview when the cursor moves/text changes 26 | vim.api.nvim_create_autocmd({ 'CursorMovedI', 'TextChangedI' }, { 27 | callback = function() 28 | if not ghost_text_config.enabled or ghost_text.win == nil then return end 29 | ghost_text.draw_preview(vim.api.nvim_win_get_buf(ghost_text.win)) 30 | end, 31 | }) 32 | 33 | autocomplete.listen_on_select(function(item) 34 | if ghost_text_config.enabled then ghost_text.show_preview(item) end 35 | end) 36 | autocomplete.listen_on_close(function() ghost_text.clear_preview() end) 37 | 38 | return ghost_text 39 | end 40 | 41 | --- @param selected_item? blink.cmp.CompletionItem 42 | function ghost_text.show_preview(selected_item) 43 | -- nothing to show, clear the preview 44 | if not selected_item then 45 | ghost_text.clear_preview() 46 | return 47 | end 48 | 49 | -- update state and redraw 50 | local changed = ghost_text.selected_item ~= selected_item 51 | ghost_text.selected_item = selected_item 52 | ghost_text.win = vim.api.nvim_get_current_win() 53 | if changed then ghost_text.draw_preview(vim.api.nvim_win_get_buf(ghost_text.win)) end 54 | end 55 | 56 | function ghost_text.clear_preview() 57 | ghost_text.selected_item = nil 58 | ghost_text.win = nil 59 | if ghost_text.extmark_id ~= nil then 60 | vim.api.nvim_buf_del_extmark(0, config.highlight.ns, ghost_text.extmark_id) 61 | ghost_text.extmark_id = nil 62 | end 63 | end 64 | 65 | function ghost_text.draw_preview(bufnr) 66 | if not ghost_text.selected_item then return end 67 | 68 | local text_edit = text_edits_lib.get_from_item(ghost_text.selected_item) 69 | 70 | if ghost_text.selected_item.insertTextFormat == vim.lsp.protocol.InsertTextFormat.Snippet then 71 | local expanded_snippet = snippets_utils.safe_parse(text_edit.newText) 72 | text_edit.newText = expanded_snippet and tostring(expanded_snippet) or text_edit.newText 73 | end 74 | 75 | local display_lines = vim.split(get_still_untyped_text(text_edit), '\n', { plain = true }) or {} 76 | 77 | local virt_lines = {} 78 | if #display_lines > 1 then 79 | for i = 2, #display_lines do 80 | virt_lines[i - 1] = { { display_lines[i], 'BlinkCmpGhostText' } } 81 | end 82 | end 83 | 84 | local cursor_pos = { 85 | text_edit.range.start.line, 86 | text_edit.range['end'].character, 87 | } 88 | 89 | ghost_text.extmark_id = vim.api.nvim_buf_set_extmark(bufnr, config.highlight.ns, cursor_pos[1], cursor_pos[2], { 90 | id = ghost_text.extmark_id, 91 | virt_text_pos = 'inline', 92 | virt_text = { { display_lines[1], 'BlinkCmpGhostText' } }, 93 | virt_lines = virt_lines, 94 | hl_mode = 'combine', 95 | }) 96 | end 97 | 98 | return ghost_text 99 | -------------------------------------------------------------------------------- /.github/workflows/release.yaml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | push: 5 | tags: 6 | - "v*" 7 | 8 | jobs: 9 | build: 10 | name: Build ${{ matrix.target }} 11 | runs-on: ${{ matrix.os }} 12 | permissions: 13 | contents: read 14 | strategy: 15 | matrix: 16 | include: 17 | ## Linux builds 18 | # Glibc 2.31 19 | - os: ubuntu-20.04 20 | target: x86_64-unknown-linux-gnu 21 | artifact_name: target/x86_64-unknown-linux-gnu/release/libblink_cmp_fuzzy.so 22 | - os: ubuntu-20.04 23 | target: aarch64-unknown-linux-gnu 24 | artifact_name: target/aarch64-unknown-linux-gnu/release/libblink_cmp_fuzzy.so 25 | # Musl 1.2.3 26 | - os: ubuntu-latest 27 | target: x86_64-unknown-linux-musl 28 | artifact_name: target/x86_64-unknown-linux-musl/release/libblink_cmp_fuzzy.so 29 | - os: ubuntu-latest 30 | target: aarch64-unknown-linux-musl 31 | artifact_name: target/aarch64-unknown-linux-musl/release/libblink_cmp_fuzzy.so 32 | 33 | ## macOS builds 34 | - os: macos-latest 35 | target: x86_64-apple-darwin 36 | artifact_name: target/x86_64-apple-darwin/release/libblink_cmp_fuzzy.dylib 37 | - os: macos-latest 38 | target: aarch64-apple-darwin 39 | artifact_name: target/aarch64-apple-darwin/release/libblink_cmp_fuzzy.dylib 40 | 41 | ## Windows builds 42 | - os: windows-latest 43 | target: x86_64-pc-windows-msvc 44 | artifact_name: target/x86_64-pc-windows-msvc/release/blink_cmp_fuzzy.dll 45 | 46 | steps: 47 | - uses: actions/checkout@v4 48 | 49 | - name: Install Rust 50 | run: | 51 | rustup toolchain install nightly 52 | rustup default nightly 53 | rustup target add ${{ matrix.target }} 54 | 55 | - name: Build for Linux 56 | if: contains(matrix.os, 'ubuntu') 57 | run: | 58 | cargo install cross --git https://github.com/cross-rs/cross 59 | cross build --release --target ${{ matrix.target }} 60 | mv "${{ matrix.artifact_name }}" "${{ matrix.target }}.so" 61 | 62 | - name: Build for macOS 63 | if: contains(matrix.os, 'macos') 64 | run: | 65 | # Ventura (https://en.wikipedia.org/wiki/MacOS_version_history#Releases) 66 | MACOSX_DEPLOYMENT_TARGET="13" cargo build --release --target ${{ matrix.target }} 67 | mv "${{ matrix.artifact_name }}" "${{ matrix.target }}.dylib" 68 | 69 | - name: Build for Windows 70 | if: contains(matrix.os, 'windows') 71 | run: | 72 | cargo build --release --target ${{ matrix.target }} 73 | mv "${{ matrix.artifact_name }}" "${{ matrix.target }}.dll" 74 | 75 | - name: Upload artifacts 76 | uses: actions/upload-artifact@v4 77 | with: 78 | name: ${{ matrix.target }} 79 | path: ${{ matrix.target }}.* 80 | 81 | release: 82 | name: Release 83 | needs: build 84 | runs-on: ubuntu-latest 85 | permissions: 86 | contents: write 87 | steps: 88 | - name: Download artifacts 89 | uses: actions/download-artifact@v4 90 | 91 | - name: Upload Release Assets 92 | uses: softprops/action-gh-release@v2 93 | with: 94 | name: ${{ github.ref_name }} 95 | tag_name: ${{ github.ref_name }} 96 | token: ${{ github.token }} 97 | files: ./**/* 98 | draft: false 99 | prerelease: false 100 | generate_release_notes: true 101 | -------------------------------------------------------------------------------- /lua/blink/cmp/sources/path/lib.lua: -------------------------------------------------------------------------------- 1 | local lib = {} 2 | 3 | --- @param path_regex vim.regex 4 | --- @param get_cwd fun(context: blink.cmp.Context): string 5 | --- @param context blink.cmp.Context 6 | function lib.dirname(path_regex, get_cwd, context) 7 | -- HACK: move this :sub logic into the context? 8 | -- it's not obvious that you need to avoid going back a char if the start_col == end_col 9 | local line_before_cursor = 10 | context.line:sub(1, context.bounds.start_col - (context.bounds.start_col ~= context.bounds.end_col and 1 or 0)) 11 | local s = path_regex:match_str(line_before_cursor) 12 | if not s then return nil end 13 | 14 | local dirname = string.gsub(string.sub(line_before_cursor, s + 2), '%a*$', '') -- exclude '/' 15 | local prefix = string.sub(line_before_cursor, 1, s + 1) -- include '/' 16 | 17 | local buf_dirname = get_cwd(context) 18 | if vim.api.nvim_get_mode().mode == 'c' then buf_dirname = vim.fn.getcwd() end 19 | if prefix:match('%.%./$') then return vim.fn.resolve(buf_dirname .. '/../' .. dirname) end 20 | if prefix:match('%./$') or prefix:match('"$') or prefix:match("'$") then 21 | return vim.fn.resolve(buf_dirname .. '/' .. dirname) 22 | end 23 | if prefix:match('~/$') then return vim.fn.resolve(vim.fn.expand('~') .. '/' .. dirname) end 24 | local env_var_name = prefix:match('%$([%a_]+)/$') 25 | if env_var_name then 26 | local env_var_value = vim.fn.getenv(env_var_name) 27 | if env_var_value ~= vim.NIL then return vim.fn.resolve(env_var_value .. '/' .. dirname) end 28 | end 29 | if prefix:match('/$') then 30 | local accept = true 31 | -- Ignore URL components 32 | accept = accept and not prefix:match('%a/$') 33 | -- Ignore URL scheme 34 | accept = accept and not prefix:match('%a+:/$') and not prefix:match('%a+://$') 35 | -- Ignore HTML closing tags 36 | accept = accept and not prefix:match(' config.semantic_token_resolution.timeout_ms 46 | or cursor_before_call[1] ~= cursor_after_call[1] 47 | or cursor_before_call[2] ~= cursor_after_call[2] 48 | then 49 | return callback() 50 | end 51 | 52 | for _, token in ipairs(semantic.process_semantic_token_data(result.data, token_types)) do 53 | if 54 | cursor_after_call[1] == token.line 55 | and cursor_after_call[2] >= token.start_col 56 | and cursor_after_call[2] <= token.end_col 57 | and (token.type == 'function' or token.type == 'method') 58 | then 59 | -- add the brackets 60 | local brackets_for_filetype = utils.get_for_filetype(filetype, item) 61 | local line = vim.api.nvim_get_current_line() 62 | local start_col = text_edit.range.start.character + #text_edit.newText 63 | local new_line = line:sub(1, start_col) 64 | .. brackets_for_filetype[1] 65 | .. brackets_for_filetype[2] 66 | .. line:sub(start_col + 1) 67 | vim.api.nvim_set_current_line(new_line) 68 | vim.api.nvim_win_set_cursor(0, { cursor_after_call[1], start_col + #brackets_for_filetype[1] }) 69 | callback() 70 | return 71 | end 72 | end 73 | 74 | callback() 75 | end 76 | ) 77 | end 78 | 79 | function semantic.process_semantic_token_data(data, token_types) 80 | local tokens = {} 81 | local idx = 0 82 | local token_line = 0 83 | local token_start_col = 0 84 | 85 | while (idx + 1) * 5 <= #data do 86 | local delta_token_line = data[idx * 5 + 1] 87 | local delta_token_start_col = data[idx * 5 + 2] 88 | local delta_token_length = data[idx * 5 + 3] 89 | local type = token_types[data[idx * 5 + 4] + 1] 90 | 91 | if delta_token_line > 0 then token_start_col = 0 end 92 | token_line = token_line + delta_token_line 93 | token_start_col = token_start_col + delta_token_start_col 94 | 95 | table.insert(tokens, { 96 | line = token_line + 1, 97 | start_col = token_start_col, 98 | end_col = token_start_col + delta_token_length, 99 | type = type, 100 | }) 101 | 102 | token_start_col = token_start_col + delta_token_length 103 | idx = idx + 1 104 | end 105 | 106 | return tokens 107 | end 108 | 109 | return semantic.add_brackets_via_semantic_token 110 | -------------------------------------------------------------------------------- /lua/blink/cmp/windows/lib/render.lua: -------------------------------------------------------------------------------- 1 | --- @class blink.cmp.Component 2 | --- @field [number] blink.cmp.Component | string 3 | --- @field fill? boolean 4 | --- @field max_width? number 5 | --- @field hl_group? string 6 | --- @field hl_params? table 7 | 8 | --- @class blink.cmp.RenderedComponentTree 9 | --- @field text string 10 | --- @field highlights { start: number, stop: number, group?: string, params?: table }[] 11 | 12 | --- @class blink.cmp.StringsBuild 13 | --- @field text string 14 | --- @field length number 15 | 16 | local renderer = {} 17 | 18 | ---@param text string 19 | ---@param max_width number 20 | ---@return string 21 | function renderer.truncate_text(text, max_width) 22 | if vim.api.nvim_strwidth(text) > max_width then 23 | return vim.fn.strcharpart(text, 0, max_width) .. '…' 24 | else 25 | return text 26 | end 27 | end 28 | 29 | --- Draws the highlights for the rendered component tree 30 | --- as ephemeral extmarks 31 | --- @param rendered blink.cmp.RenderedComponentTree 32 | function renderer.draw_highlights(rendered, bufnr, ns, line_number) 33 | for _, highlight in ipairs(rendered.highlights) do 34 | vim.api.nvim_buf_set_extmark(bufnr, ns, line_number, highlight.start - 1, { 35 | end_col = highlight.stop - 1, 36 | hl_group = highlight.group, 37 | hl_mode = 'combine', 38 | hl_eol = true, 39 | ephemeral = true, 40 | }) 41 | end 42 | end 43 | 44 | --- Gets the concatenated text and length for a list of strings 45 | --- and truncates if necessary when max_width is set 46 | --- @param strings string[] 47 | --- @param max_width? number 48 | --- @return blink.cmp.StringsBuild 49 | function renderer.concat_strings(strings, max_width) 50 | local text = '' 51 | for _, component in ipairs(strings) do 52 | text = text .. component 53 | end 54 | 55 | if max_width then text = renderer.truncate_text(text, max_width) end 56 | return { text = text, length = vim.api.nvim_strwidth(text) } 57 | end 58 | 59 | --- @param components (blink.cmp.Component | string)[] 60 | --- @param lengths number[] 61 | --- @return blink.cmp.RenderedComponentTree 62 | function renderer.render(components, lengths) 63 | local text = '' 64 | local offset = 0 65 | local highlights = {} 66 | 67 | for i, component in ipairs(components) do 68 | if type(component) == 'string' then 69 | text = text .. component 70 | offset = offset + #component 71 | else 72 | local concatenated = renderer.concat_strings(component, component.max_width) 73 | 74 | table.insert(highlights, { 75 | start = offset + 1, 76 | stop = offset + #concatenated.text + 1, 77 | group = component.hl_group, 78 | params = component.hl_params, 79 | }) 80 | 81 | text = text .. concatenated.text 82 | offset = offset + #concatenated.text 83 | 84 | if component.fill then 85 | local spaces = lengths[i] - concatenated.length 86 | text = text .. string.rep(' ', spaces) 87 | offset = offset + spaces 88 | end 89 | end 90 | end 91 | 92 | return { text = text, highlights = highlights } 93 | end 94 | 95 | --- @param component blink.cmp.Component | string 96 | --- @return number 97 | function renderer.get_length(component) 98 | if type(component) == 'string' then 99 | return vim.api.nvim_strwidth(component) 100 | else 101 | local build = renderer.concat_strings(component, component.max_width) 102 | return build.length 103 | end 104 | end 105 | 106 | --- @param components_list (blink.cmp.Component | string)[][] 107 | --- @param min_width number 108 | --- @return number[] 109 | function renderer.get_max_lengths(components_list, min_width) 110 | local lengths = {} 111 | local first_fill 112 | 113 | for _, components in ipairs(components_list) do 114 | for i, component in ipairs(components) do 115 | local length = renderer.get_length(component) 116 | if not lengths[i] or lengths[i] < length then lengths[i] = length end 117 | if component.fill and not first_fill then first_fill = i end 118 | end 119 | end 120 | 121 | for _, length in ipairs(lengths) do 122 | min_width = min_width - length 123 | end 124 | 125 | first_fill = first_fill or 1 126 | if min_width > 0 then lengths[first_fill] = lengths[first_fill] + min_width end 127 | 128 | return lengths 129 | end 130 | 131 | --- @param component blink.cmp.RenderedComponentTree 132 | --- @param offset number 133 | --- @return blink.cmp.RenderedComponentTree 134 | function renderer.add_offset_to_rendered_component(component, offset) 135 | for _, highlight in ipairs(component.highlights) do 136 | highlight.start = highlight.start + offset 137 | highlight.stop = highlight.stop + offset 138 | end 139 | return component 140 | end 141 | 142 | return renderer 143 | -------------------------------------------------------------------------------- /lua/blink/cmp/windows/signature.lua: -------------------------------------------------------------------------------- 1 | local config = require('blink.cmp.config').windows.signature_help 2 | local sources = require('blink.cmp.sources.lib') 3 | local autocomplete = require('blink.cmp.windows.autocomplete') 4 | local signature = {} 5 | 6 | function signature.setup() 7 | signature.win = require('blink.cmp.windows.lib').new({ 8 | min_width = config.min_width, 9 | max_width = config.max_width, 10 | max_height = config.max_height, 11 | border = config.border, 12 | winblend = config.winblend, 13 | winhighlight = config.winhighlight, 14 | scrollbar = config.scrollbar, 15 | wrap = true, 16 | filetype = 'markdown', 17 | }) 18 | 19 | -- todo: deduplicate this 20 | autocomplete.listen_on_position_update(function() 21 | if signature.context then signature.update_position(signature.context) end 22 | end) 23 | 24 | vim.api.nvim_create_autocmd({ 'CursorMovedI', 'WinScrolled', 'WinResized' }, { 25 | callback = function() 26 | if signature.context then signature.update_position(signature.context) end 27 | end, 28 | }) 29 | 30 | return signature 31 | end 32 | 33 | --- @param context blink.cmp.SignatureHelpContext 34 | --- @param signature_help lsp.SignatureHelp | nil 35 | function signature.open_with_signature_help(context, signature_help) 36 | signature.context = context 37 | -- check if there are any signatures in signature_help, since 38 | -- convert_signature_help_to_markdown_lines errors with no signatures 39 | if 40 | signature_help == nil 41 | or #signature_help.signatures == 0 42 | or signature_help.signatures[(signature_help.activeSignature or 0) + 1] == nil 43 | then 44 | signature.win:close() 45 | return 46 | end 47 | 48 | local active_signature = signature_help.signatures[(signature_help.activeSignature or 0) + 1] 49 | 50 | if signature.shown_signature ~= active_signature then 51 | require('blink.cmp.windows.lib.docs').render_detail_and_documentation( 52 | signature.win:get_buf(), 53 | active_signature.label, 54 | active_signature.documentation, 55 | config.max_width 56 | ) 57 | end 58 | signature.shown_signature = active_signature 59 | 60 | -- highlight active parameter 61 | local _, active_highlight = vim.lsp.util.convert_signature_help_to_markdown_lines( 62 | signature_help, 63 | vim.bo.filetype, 64 | sources.get_signature_help_trigger_characters().trigger_characters 65 | ) 66 | if active_highlight ~= nil then 67 | vim.api.nvim_buf_add_highlight( 68 | signature.win:get_buf(), 69 | require('blink.cmp.config').highlight.ns, 70 | 'BlinkCmpSignatureHelpActiveParameter', 71 | 0, 72 | active_highlight[1], 73 | active_highlight[2] 74 | ) 75 | end 76 | 77 | signature.win:open() 78 | signature.update_position(context) 79 | end 80 | 81 | function signature.close() 82 | if not signature.win:is_open() then return end 83 | signature.win:close() 84 | end 85 | 86 | function signature.scroll_up(amount) 87 | local winnr = signature.win:get_win() 88 | local top_line = math.max(1, vim.fn.line('w0', winnr) - 1) 89 | local desired_line = math.max(1, top_line - amount) 90 | 91 | vim.api.nvim_win_set_cursor(signature.win:get_win(), { desired_line, 0 }) 92 | end 93 | 94 | function signature.scroll_down(amount) 95 | local winnr = signature.win:get_win() 96 | local line_count = vim.api.nvim_buf_line_count(signature.win:get_buf()) 97 | local bottom_line = math.max(1, vim.fn.line('w$', winnr) + 1) 98 | local desired_line = math.min(line_count, bottom_line + amount) 99 | 100 | vim.api.nvim_win_set_cursor(signature.win:get_win(), { desired_line, 0 }) 101 | end 102 | 103 | --- @param context blink.cmp.SignatureHelpContext 104 | function signature.update_position() 105 | local win = signature.win 106 | if not win:is_open() then return end 107 | local winnr = win:get_win() 108 | 109 | win:update_size() 110 | 111 | local direction_priority = config.direction_priority 112 | 113 | -- if the autocomplete window is open, we want to place the signature window on the opposite side 114 | local autocomplete_win_config = autocomplete.win:get_win() and vim.api.nvim_win_get_config(autocomplete.win:get_win()) 115 | if autocomplete.win:is_open() then 116 | local cursor_screen_row = vim.fn.winline() 117 | local autocomplete_win_is_up = autocomplete_win_config.row - cursor_screen_row < 0 118 | direction_priority = autocomplete_win_is_up and { 's' } or { 'n' } 119 | end 120 | 121 | local height = win:get_height() 122 | local pos = win:get_vertical_direction_and_height(direction_priority) 123 | 124 | -- couldn't find anywhere to place the window 125 | if not pos then 126 | win:close() 127 | return 128 | end 129 | 130 | -- default to the user's preference but attempt to use the other options 131 | local row = pos.direction == 's' and 1 or -height 132 | local screenpos = vim.fn.screenpos(0, unpack(vim.api.nvim_win_get_cursor(0))) 133 | local col = autocomplete_win_config and (autocomplete_win_config.col - screenpos.col) or 0 134 | vim.api.nvim_win_set_config(winnr, { relative = 'cursor', row = row, col = col }) 135 | vim.api.nvim_win_set_height(winnr, pos.height) 136 | end 137 | 138 | return signature 139 | -------------------------------------------------------------------------------- /lua/blink/cmp/fuzzy/fuzzy.rs: -------------------------------------------------------------------------------- 1 | // TODO: refactor this heresy 2 | 3 | use crate::frecency::FrecencyTracker; 4 | use crate::lsp_item::LspItem; 5 | use mlua::prelude::*; 6 | use mlua::FromLua; 7 | use mlua::Lua; 8 | use std::cmp::Reverse; 9 | use std::collections::HashSet; 10 | 11 | #[derive(Clone, Hash)] 12 | pub struct FuzzyOptions { 13 | use_typo_resistance: bool, 14 | use_frecency: bool, 15 | use_proximity: bool, 16 | nearby_words: Option>, 17 | min_score: u16, 18 | max_items: u32, 19 | sorts: Vec, 20 | } 21 | 22 | impl FromLua for FuzzyOptions { 23 | fn from_lua(value: LuaValue, _lua: &'_ Lua) -> LuaResult { 24 | if let Some(tab) = value.as_table() { 25 | let use_typo_resistance: bool = tab.get("use_typo_resistance").unwrap_or_default(); 26 | let use_frecency: bool = tab.get("use_frecency").unwrap_or_default(); 27 | let use_proximity: bool = tab.get("use_proximity").unwrap_or_default(); 28 | let nearby_words: Option> = tab.get("nearby_words").ok(); 29 | let min_score: u16 = tab.get("min_score").unwrap_or_default(); 30 | let max_items: u32 = tab.get("max_items").unwrap_or_default(); 31 | let sorts: Vec = tab.get("sorts").unwrap_or_default(); 32 | 33 | Ok(FuzzyOptions { 34 | use_typo_resistance, 35 | use_frecency, 36 | use_proximity, 37 | nearby_words, 38 | min_score, 39 | max_items, 40 | sorts, 41 | }) 42 | } else { 43 | Err(mlua::Error::FromLuaConversionError { 44 | from: "LuaValue", 45 | to: "FuzzyOptions".to_string(), 46 | message: None, 47 | }) 48 | } 49 | } 50 | } 51 | 52 | pub fn fuzzy( 53 | needle: String, 54 | haystack: Vec, 55 | frecency: &FrecencyTracker, 56 | opts: FuzzyOptions, 57 | ) -> Vec { 58 | let nearby_words: HashSet = HashSet::from_iter(opts.nearby_words.unwrap_or_default()); 59 | let haystack_labels = haystack.iter().map(|s| s.label.clone()).collect::>(); 60 | 61 | // Fuzzy match with fzrs 62 | let options = frizbee::Options { 63 | prefilter: !opts.use_typo_resistance, 64 | min_score: opts.min_score, 65 | stable_sort: false, 66 | ..Default::default() 67 | }; 68 | let mut matches = frizbee::match_list( 69 | &needle, 70 | &haystack_labels 71 | .iter() 72 | .map(|s| s.as_str()) 73 | .collect::>(), 74 | options, 75 | ); 76 | 77 | // Sort by scores 78 | let match_scores = matches 79 | .iter() 80 | .map(|mtch| { 81 | let frecency_score = if opts.use_frecency { 82 | frecency.get_score(&haystack[mtch.index_in_haystack]) as i32 83 | } else { 84 | 0 85 | }; 86 | let nearby_words_score = if opts.use_proximity { 87 | nearby_words 88 | .get(&haystack_labels[mtch.index_in_haystack]) 89 | .map(|_| 2) 90 | .unwrap_or(0) 91 | } else { 92 | 0 93 | }; 94 | let score_offset = haystack[mtch.index_in_haystack].score_offset; 95 | 96 | (mtch.score as i32) + frecency_score + nearby_words_score + score_offset 97 | }) 98 | .collect::>(); 99 | 100 | // Find the highest score and filter out matches that are unreasonably lower than it 101 | if opts.use_typo_resistance { 102 | let max_score = matches.iter().map(|mtch| mtch.score).max().unwrap_or(0); 103 | let secondary_min_score = max_score.max(16) - 16; 104 | matches = matches 105 | .into_iter() 106 | .filter(|mtch| mtch.score >= secondary_min_score) 107 | .collect::>(); 108 | } 109 | 110 | // Sort matches by sort criteria 111 | for sort in opts.sorts.iter() { 112 | match sort.as_str() { 113 | "kind" => { 114 | matches.sort_by_key(|mtch| haystack[mtch.index_in_haystack].kind); 115 | } 116 | "score" => { 117 | matches.sort_by_cached_key(|mtch| Reverse(match_scores[mtch.index])); 118 | } 119 | "label" => { 120 | matches.sort_by(|a, b| { 121 | let label_a = &haystack[a.index_in_haystack].label; 122 | let label_b = &haystack[b.index_in_haystack].label; 123 | 124 | // Put anything with an underscore at the end 125 | match (label_a.starts_with('_'), label_b.starts_with('_')) { 126 | (true, false) => std::cmp::Ordering::Greater, 127 | (false, true) => std::cmp::Ordering::Less, 128 | _ => label_a.cmp(label_b), 129 | } 130 | }); 131 | } 132 | _ => {} 133 | } 134 | } 135 | 136 | // Grab the top N matches and return the indices 137 | matches 138 | .iter() 139 | .map(|mtch| mtch.index_in_haystack) 140 | .take(opts.max_items as usize) 141 | .collect::>() 142 | } 143 | -------------------------------------------------------------------------------- /lua/blink/cmp/fuzzy/frecency.rs: -------------------------------------------------------------------------------- 1 | use crate::lsp_item::LspItem; 2 | use heed::types::*; 3 | use heed::{Database, Env, EnvOpenOptions}; 4 | use mlua::Result as LuaResult; 5 | use serde::{Deserialize, Serialize}; 6 | use std::fs; 7 | use std::time::{SystemTime, UNIX_EPOCH}; 8 | 9 | #[derive(Clone, Serialize, Deserialize)] 10 | struct CompletionItemKey { 11 | label: String, 12 | kind: u32, 13 | source_id: String, 14 | } 15 | 16 | impl From<&LspItem> for CompletionItemKey { 17 | fn from(item: &LspItem) -> Self { 18 | Self { 19 | label: item.label.clone(), 20 | kind: item.kind, 21 | source_id: item.source_id.clone(), 22 | } 23 | } 24 | } 25 | 26 | #[derive(Debug)] 27 | pub struct FrecencyTracker { 28 | env: Env, 29 | db: Database, SerdeBincode>>, 30 | access_thresholds: Vec<(f64, u64)>, 31 | } 32 | 33 | impl FrecencyTracker { 34 | pub fn new(db_path: &str) -> LuaResult { 35 | fs::create_dir_all(db_path).map_err(|err| { 36 | mlua::Error::RuntimeError( 37 | "Failed to create frecency database directory: ".to_string() + &err.to_string(), 38 | ) 39 | })?; 40 | let env = unsafe { 41 | EnvOpenOptions::new().open(db_path).map_err(|err| { 42 | mlua::Error::RuntimeError( 43 | "Failed to open frecency database: ".to_string() + &err.to_string(), 44 | ) 45 | })? 46 | }; 47 | env.clear_stale_readers().map_err(|err| { 48 | mlua::Error::RuntimeError( 49 | "Failed to clear stale readers for frecency database: ".to_string() 50 | + &err.to_string(), 51 | ) 52 | })?; 53 | 54 | // we will open the default unnamed database 55 | let mut wtxn = env.write_txn().map_err(|err| { 56 | mlua::Error::RuntimeError( 57 | "Failed to open write transaction for frecency database: ".to_string() 58 | + &err.to_string(), 59 | ) 60 | })?; 61 | let db = env.create_database(&mut wtxn, None).map_err(|err| { 62 | mlua::Error::RuntimeError( 63 | "Failed to create frecency database: ".to_string() + &err.to_string(), 64 | ) 65 | })?; 66 | 67 | let access_thresholds = [ 68 | (1., 1000 * 60 * 2), // 2 minutes 69 | (0.2, 1000 * 60 * 60), // 1 hour 70 | (0.1, 1000 * 60 * 60 * 24), // 1 day 71 | (0.05, 1000 * 60 * 60 * 24 * 7), // 1 week 72 | ] 73 | .to_vec(); 74 | 75 | Ok(FrecencyTracker { 76 | env: env.clone(), 77 | db, 78 | access_thresholds, 79 | }) 80 | } 81 | 82 | fn get_accesses(&self, item: &LspItem) -> LuaResult>> { 83 | let rtxn = self.env.read_txn().map_err(|err| { 84 | mlua::Error::RuntimeError( 85 | "Failed to start read transaction for frecency database: ".to_string() 86 | + &err.to_string(), 87 | ) 88 | })?; 89 | self.db 90 | .get(&rtxn, &CompletionItemKey::from(item)) 91 | .map_err(|err| { 92 | mlua::Error::RuntimeError( 93 | "Failed to read from frecency database: ".to_string() + &err.to_string(), 94 | ) 95 | }) 96 | } 97 | 98 | fn get_now(&self) -> u64 { 99 | SystemTime::now() 100 | .duration_since(UNIX_EPOCH) 101 | .unwrap() 102 | .as_secs() 103 | } 104 | 105 | pub fn access(&mut self, item: &LspItem) -> LuaResult<()> { 106 | let mut wtxn = self.env.write_txn().map_err(|err| { 107 | mlua::Error::RuntimeError( 108 | "Failed to start write transaction for frecency database: ".to_string() 109 | + &err.to_string(), 110 | ) 111 | })?; 112 | 113 | let mut accesses = self.get_accesses(item)?.unwrap_or_default(); 114 | accesses.push(self.get_now()); 115 | 116 | self.db 117 | .put(&mut wtxn, &CompletionItemKey::from(item), &accesses) 118 | .map_err(|err| { 119 | mlua::Error::RuntimeError( 120 | "Failed to write to frecency database: ".to_string() + &err.to_string(), 121 | ) 122 | })?; 123 | 124 | wtxn.commit().map_err(|err| { 125 | mlua::Error::RuntimeError( 126 | "Failed to commit write transaction for frecency database: ".to_string() 127 | + &err.to_string(), 128 | ) 129 | })?; 130 | 131 | Ok(()) 132 | } 133 | 134 | pub fn get_score(&self, item: &LspItem) -> i64 { 135 | let accesses = self.get_accesses(item).unwrap_or(None).unwrap_or_default(); 136 | let now = self.get_now(); 137 | let mut score = 0.0; 138 | 'outer: for access in &accesses { 139 | let duration_since = now - access; 140 | for (rank, threshold_duration_since) in &self.access_thresholds { 141 | if duration_since < *threshold_duration_since { 142 | score += rank; 143 | continue 'outer; 144 | } 145 | } 146 | } 147 | score.min(4.) as i64 148 | } 149 | } 150 | -------------------------------------------------------------------------------- /lua/blink/cmp/sources/snippets/registry.lua: -------------------------------------------------------------------------------- 1 | --- Credit to https://github.com/garymjr/nvim-snippets/blob/main/lua/snippets/utils/init.lua 2 | --- for the original implementation 3 | --- Original License: MIT 4 | 5 | ---@class blink.cmp.Snippet 6 | ---@field prefix string 7 | ---@field body string[] | string 8 | ---@field description? string 9 | 10 | local registry = { 11 | builtin_vars = require('blink.cmp.sources.snippets.builtin'), 12 | } 13 | 14 | local utils = require('blink.cmp.sources.snippets.utils') 15 | local default_config = { 16 | friendly_snippets = true, 17 | search_paths = { vim.fn.stdpath('config') .. '/snippets' }, 18 | global_snippets = { 'all' }, 19 | extended_filetypes = {}, 20 | ignored_filetypes = {}, 21 | } 22 | 23 | --- @param config blink.cmp.SnippetsOpts 24 | function registry.new(config) 25 | local self = setmetatable({}, { __index = registry }) 26 | self.config = vim.tbl_deep_extend('force', default_config, config) 27 | 28 | if self.config.friendly_snippets then 29 | for _, path in ipairs(vim.api.nvim_list_runtime_paths()) do 30 | if string.match(path, 'friendly.snippets') then table.insert(self.config.search_paths, path) end 31 | end 32 | end 33 | self.registry = require('blink.cmp.sources.snippets.scan').register_snippets(self.config.search_paths) 34 | 35 | return self 36 | end 37 | 38 | --- @param filetype string 39 | --- @return blink.cmp.Snippet[] 40 | function registry:get_snippets_for_ft(filetype) 41 | local loaded_snippets = {} 42 | local files = self.registry[filetype] 43 | if not files then return loaded_snippets end 44 | 45 | if type(files) == 'table' then 46 | for _, f in ipairs(files) do 47 | local contents = utils.read_file(f) 48 | if contents then 49 | local snippets = utils.parse_json_with_error_msg(f, contents) 50 | for _, key in ipairs(vim.tbl_keys(snippets)) do 51 | local snippet = utils.read_snippet(snippets[key], key) 52 | for _, snippet_def in pairs(snippet) do 53 | table.insert(loaded_snippets, snippet_def) 54 | end 55 | end 56 | end 57 | end 58 | else 59 | local contents = utils.read_file(files) 60 | if contents then 61 | local snippets = utils.parse_json_with_error_msg(files, contents) 62 | for _, key in ipairs(vim.tbl_keys(snippets)) do 63 | local snippet = utils.read_snippet(snippets[key], key) 64 | for _, snippet in pairs(snippet) do 65 | table.insert(loaded_snippets, snippet) 66 | end 67 | end 68 | end 69 | end 70 | 71 | return loaded_snippets 72 | end 73 | 74 | --- @param filetype string 75 | --- @return blink.cmp.Snippet[] 76 | function registry:get_extended_snippets(filetype) 77 | local loaded_snippets = {} 78 | if not filetype then return loaded_snippets end 79 | 80 | local extended_snippets = self.config.extended_filetypes[filetype] or {} 81 | for _, ft in ipairs(extended_snippets) do 82 | if vim.tbl_contains(self.config.extended_filetypes, filetype) then 83 | vim.list_extend(loaded_snippets, self:get_extended_snippets(ft)) 84 | else 85 | vim.list_extend(loaded_snippets, self:get_snippets_for_ft(ft)) 86 | end 87 | end 88 | return loaded_snippets 89 | end 90 | 91 | --- @return blink.cmp.Snippet[] 92 | function registry:get_global_snippets() 93 | local loaded_snippets = {} 94 | local global_snippets = self.config.global_snippets 95 | for _, ft in ipairs(global_snippets) do 96 | if vim.tbl_contains(self.config.extended_filetypes, ft) then 97 | vim.list_extend(loaded_snippets, self:get_extended_snippets(ft)) 98 | else 99 | vim.list_extend(loaded_snippets, self:get_snippets_for_ft(ft)) 100 | end 101 | end 102 | return loaded_snippets 103 | end 104 | 105 | --- @param snippet blink.cmp.Snippet 106 | --- @return blink.cmp.CompletionItem 107 | function registry:snippet_to_completion_item(snippet) 108 | local body = type(snippet.body) == 'string' and snippet.body or table.concat(snippet.body, '\n') 109 | return { 110 | kind = require('blink.cmp.types').CompletionItemKind.Snippet, 111 | label = snippet.prefix, 112 | insertTextFormat = vim.lsp.protocol.InsertTextFormat.Snippet, 113 | insertText = self:expand_vars(body), 114 | description = snippet.description, 115 | } 116 | end 117 | 118 | --- @param snippet string 119 | --- @return string 120 | function registry:parse_body(snippet) 121 | local parse = utils.safe_parse(self:expand_vars(snippet)) 122 | return parse and tostring(parse) or snippet 123 | end 124 | 125 | --- @param snippet string 126 | --- @return string 127 | function registry:expand_vars(snippet) 128 | local lazy_vars = self.builtin_vars.lazy 129 | local eager_vars = self.builtin_vars.eager or {} 130 | 131 | local resolved_snippet = snippet 132 | local parsed_snippet = utils.safe_parse(snippet) 133 | if not parsed_snippet then return snippet end 134 | 135 | for _, child in ipairs(parsed_snippet.data.children) do 136 | local type, data = child.type, child.data 137 | if type == vim.lsp._snippet_grammar.NodeType.Variable then 138 | if eager_vars[data.name] then 139 | resolved_snippet = resolved_snippet:gsub('%$[{]?(' .. data.name .. ')[}]?', eager_vars[data.name]) 140 | elseif lazy_vars[data.name] then 141 | resolved_snippet = resolved_snippet:gsub('%$[{]?(' .. data.name .. ')[}]?', lazy_vars[data.name]()) 142 | end 143 | end 144 | end 145 | 146 | return resolved_snippet 147 | end 148 | 149 | return registry 150 | -------------------------------------------------------------------------------- /lua/blink/cmp/sources/lib/async.lua: -------------------------------------------------------------------------------- 1 | --- Allows chaining of async operations without callback hell 2 | --- 3 | --- @class blink.cmp.Task 4 | --- @field status 1 | 2 | 3 | 4 5 | --- @field result any | nil 6 | --- @field error any | nil 7 | --- @field new fun(fn: fun(resolve: fun(result: any), reject: fun(err: any))): blink.cmp.Task 8 | --- 9 | --- @field cancel fun(self: blink.cmp.Task) 10 | --- @field map fun(self: blink.cmp.Task, fn: fun(result: any): blink.cmp.Task | any): blink.cmp.Task 11 | --- @field catch fun(self: blink.cmp.Task, fn: fun(err: any): blink.cmp.Task | any): blink.cmp.Task 12 | --- 13 | --- @field on_completion fun(self: blink.cmp.Task, cb: fun(result: any)) 14 | --- @field on_failure fun(self: blink.cmp.Task, cb: fun(err: any)) 15 | --- @field on_cancel fun(self: blink.cmp.Task, cb: fun()) 16 | 17 | local STATUS = { 18 | RUNNING = 1, 19 | COMPLETED = 2, 20 | FAILED = 3, 21 | CANCELLED = 4, 22 | } 23 | 24 | local task = { 25 | __task = true, 26 | } 27 | 28 | function task.new(fn) 29 | local self = setmetatable({}, { __index = task }) 30 | self.status = STATUS.RUNNING 31 | self._completion_cbs = {} 32 | self._failure_cbs = {} 33 | self._cancel_cbs = {} 34 | self.result = nil 35 | self.error = nil 36 | 37 | local resolve = function(result) 38 | if self.status ~= STATUS.RUNNING then return end 39 | 40 | self.status = STATUS.COMPLETED 41 | self.result = result 42 | 43 | for _, cb in ipairs(self._completion_cbs) do 44 | cb(result) 45 | end 46 | end 47 | 48 | local reject = function(err) 49 | if self.status ~= STATUS.RUNNING then return end 50 | 51 | self.status = STATUS.FAILED 52 | self.error = err 53 | 54 | for _, cb in ipairs(self._failure_cbs) do 55 | cb(err) 56 | end 57 | end 58 | 59 | local success, cancel_fn_or_err = pcall(function() return fn(resolve, reject) end) 60 | 61 | if not success then 62 | reject(cancel_fn_or_err) 63 | elseif type(cancel_fn_or_err) == 'function' then 64 | self._cancel = cancel_fn_or_err 65 | end 66 | 67 | return self 68 | end 69 | 70 | function task:cancel() 71 | if self.status ~= STATUS.RUNNING then return end 72 | self.status = STATUS.CANCELLED 73 | 74 | if self._cancel ~= nil then self._cancel() end 75 | for _, cb in ipairs(self._cancel_cbs) do 76 | cb() 77 | end 78 | end 79 | 80 | --- mappings 81 | 82 | function task:map(fn) 83 | local chained_task 84 | chained_task = task.new(function(resolve, reject) 85 | self:on_completion(function(result) 86 | local mapped_result = fn(result) 87 | if type(mapped_result) == 'table' and mapped_result.__task then 88 | mapped_result:on_completion(resolve) 89 | mapped_result:on_failure(reject) 90 | mapped_result:on_cancel(function() chained_task:cancel() end) 91 | return 92 | end 93 | resolve(mapped_result) 94 | end) 95 | self:on_failure(reject) 96 | self:on_cancel(function() chained_task:cancel() end) 97 | return function() chained_task:cancel() end 98 | end) 99 | return chained_task 100 | end 101 | 102 | function task:catch(fn) 103 | local chained_task 104 | chained_task = task.new(function(resolve, reject) 105 | self:on_completion(resolve) 106 | self:on_failure(function(err) 107 | local mapped_err = fn(err) 108 | if type(mapped_err) == 'table' and mapped_err.is_task then 109 | mapped_err:on_completion(resolve) 110 | mapped_err:on_failure(reject) 111 | mapped_err:on_cancel(function() chained_task:cancel() end) 112 | return 113 | end 114 | resolve(mapped_err) 115 | end) 116 | self:on_cancel(function() chained_task:cancel() end) 117 | return function() chained_task:cancel() end 118 | end) 119 | return chained_task 120 | end 121 | 122 | --- events 123 | 124 | function task:on_completion(cb) 125 | if self.status == STATUS.COMPLETED then 126 | cb(self.result) 127 | elseif self.status == STATUS.RUNNING then 128 | table.insert(self._completion_cbs, cb) 129 | end 130 | return self 131 | end 132 | 133 | function task:on_failure(cb) 134 | if self.status == STATUS.FAILED then 135 | cb(self.error) 136 | elseif self.status == STATUS.RUNNING then 137 | table.insert(self._failure_cbs, cb) 138 | end 139 | return self 140 | end 141 | 142 | function task:on_cancel(cb) 143 | if self.status == STATUS.CANCELLED then 144 | cb() 145 | elseif self.status == STATUS.RUNNING then 146 | table.insert(self._cancel_cbs, cb) 147 | end 148 | return self 149 | end 150 | 151 | --- utils 152 | 153 | function task.await_all(tasks) 154 | return task.new(function(resolve) 155 | local results = {} 156 | 157 | local function resolve_if_completed() 158 | -- we can't check #results directly because a table like 159 | -- { [2] = { ... } } has a length of 2 160 | for i = 1, #tasks do 161 | if results[i] == nil then return end 162 | end 163 | resolve(results) 164 | end 165 | 166 | for idx, task in ipairs(tasks) do 167 | task:on_completion(function(result) 168 | results[idx] = { status = STATUS.COMPLETED, result = result } 169 | resolve_if_completed() 170 | end) 171 | task:on_failure(function(err) 172 | results[idx] = { status = STATUS.FAILED, err = err } 173 | resolve_if_completed() 174 | end) 175 | task:on_cancel(function() 176 | results[idx] = { status = STATUS.CANCELLED } 177 | resolve_if_completed() 178 | end) 179 | end 180 | end) 181 | end 182 | 183 | return { task = task, STATUS = STATUS } 184 | -------------------------------------------------------------------------------- /lua/blink/cmp/trigger/signature.lua: -------------------------------------------------------------------------------- 1 | -- Handles hiding and showing the signature help window. When a user types a trigger character 2 | -- (provided by the sources), we create a new `context`. This can be used downstream to determine 3 | -- if we should make new requests to the sources or not. When a user types a re-trigger character, 4 | -- we update the context's re-trigger counter. 5 | 6 | -- TODO: ensure this always calls *after* the completion trigger to avoid increasing latency 7 | 8 | local config = require('blink.cmp.config').trigger.signature_help 9 | local sources = require('blink.cmp.sources.lib') 10 | local utils = require('blink.cmp.utils') 11 | 12 | local trigger = { 13 | current_context_id = -1, 14 | --- @type blink.cmp.SignatureHelpContext | nil 15 | context = nil, 16 | event_targets = { 17 | --- @type fun(context: blink.cmp.SignatureHelpContext) 18 | on_show = function() end, 19 | --- @type fun() 20 | on_hide = function() end, 21 | }, 22 | } 23 | 24 | function trigger.activate_autocmds() 25 | local last_chars = {} 26 | vim.api.nvim_create_autocmd('InsertCharPre', { 27 | callback = function() table.insert(last_chars, vim.v.char) end, 28 | }) 29 | 30 | -- decide if we should show the completion window 31 | vim.api.nvim_create_autocmd('TextChangedI', { 32 | callback = function() 33 | -- no characters added so let cursormoved handle it 34 | if #last_chars == 0 then return end 35 | 36 | local res = sources.get_signature_help_trigger_characters() 37 | local trigger_characters = res.trigger_characters 38 | local retrigger_characters = res.retrigger_characters 39 | 40 | for _, last_char in ipairs(last_chars) do 41 | -- ignore if in a special buffer 42 | if utils.is_blocked_buffer() then 43 | trigger.hide() 44 | break 45 | -- character forces a trigger according to the sources, refresh the existing context if it exists 46 | elseif vim.tbl_contains(trigger_characters, last_char) then 47 | trigger.show({ trigger_character = last_char }) 48 | break 49 | -- character forces a re-trigger according to the sources, show if we have a context 50 | elseif vim.tbl_contains(retrigger_characters, last_char) and trigger.context ~= nil then 51 | trigger.show() 52 | break 53 | end 54 | end 55 | 56 | last_chars = {} 57 | end, 58 | }) 59 | 60 | -- check if we've moved outside of the context by diffing against the query boundary 61 | vim.api.nvim_create_autocmd({ 'CursorMovedI', 'InsertEnter' }, { 62 | callback = function(ev) 63 | if utils.is_blocked_buffer() then return end 64 | 65 | -- characters added so let textchanged handle it 66 | if #last_chars ~= 0 then return end 67 | 68 | local cursor_col = vim.api.nvim_win_get_cursor(0)[2] 69 | local char_under_cursor = vim.api.nvim_get_current_line():sub(cursor_col, cursor_col) 70 | local is_on_trigger = 71 | vim.tbl_contains(sources.get_signature_help_trigger_characters().trigger_characters, char_under_cursor) 72 | 73 | if config.show_on_insert_on_trigger_character and is_on_trigger and ev.event == 'InsertEnter' then 74 | trigger.show({ trigger_character = char_under_cursor }) 75 | elseif ev.event == 'CursorMovedI' and trigger.context ~= nil then 76 | trigger.show() 77 | end 78 | end, 79 | }) 80 | 81 | -- definitely leaving the context 82 | vim.api.nvim_create_autocmd({ 'InsertLeave', 'BufLeave' }, { callback = trigger.hide }) 83 | 84 | return trigger 85 | end 86 | 87 | function trigger.show_if_on_trigger_character() 88 | local cursor_col = vim.api.nvim_win_get_cursor(0)[2] 89 | local char_under_cursor = vim.api.nvim_get_current_line():sub(cursor_col, cursor_col) 90 | local is_on_trigger = 91 | vim.tbl_contains(sources.get_signature_help_trigger_characters().trigger_characters, char_under_cursor) 92 | if is_on_trigger then trigger.show({ trigger_character = char_under_cursor }) end 93 | return is_on_trigger 94 | end 95 | 96 | --- @param opts { trigger_character: string } | nil 97 | function trigger.show(opts) 98 | opts = opts or {} 99 | 100 | -- update context 101 | local cursor = vim.api.nvim_win_get_cursor(0) 102 | if trigger.context == nil then trigger.current_context_id = trigger.current_context_id + 1 end 103 | trigger.context = { 104 | id = trigger.current_context_id, 105 | bufnr = vim.api.nvim_get_current_buf(), 106 | cursor = cursor, 107 | line = vim.api.nvim_buf_get_lines(0, cursor[1] - 1, cursor[1], false)[1], 108 | trigger = { 109 | kind = opts.trigger_character and vim.lsp.protocol.CompletionTriggerKind.TriggerCharacter 110 | or vim.lsp.protocol.CompletionTriggerKind.Invoked, 111 | character = opts.trigger_character, 112 | }, 113 | is_retrigger = trigger.context ~= nil, 114 | active_signature_help = trigger.context and trigger.context.active_signature_help or nil, 115 | } 116 | 117 | trigger.event_targets.on_show(trigger.context) 118 | end 119 | 120 | function trigger.listen_on_show(callback) trigger.event_targets.on_show = callback end 121 | 122 | function trigger.hide() 123 | if not trigger.context then return end 124 | 125 | trigger.context = nil 126 | trigger.event_targets.on_hide() 127 | end 128 | 129 | function trigger.listen_on_hide(callback) trigger.event_targets.on_hide = callback end 130 | 131 | function trigger.set_active_signature_help(signature_help) 132 | if not trigger.context then return end 133 | trigger.context.active_signature_help = signature_help 134 | end 135 | 136 | return trigger 137 | -------------------------------------------------------------------------------- /lua/blink/cmp/windows/documentation.lua: -------------------------------------------------------------------------------- 1 | local config = require('blink.cmp.config').windows.documentation 2 | local sources = require('blink.cmp.sources.lib') 3 | local autocomplete = require('blink.cmp.windows.autocomplete') 4 | local signature = require('blink.cmp.windows.signature') 5 | local docs = {} 6 | 7 | function docs.setup() 8 | docs.win = require('blink.cmp.windows.lib').new({ 9 | min_width = config.min_width, 10 | max_width = config.max_width, 11 | max_height = config.max_height, 12 | border = config.border, 13 | winblend = config.winblend, 14 | winhighlight = config.winhighlight, 15 | scrollbar = config.scrollbar, 16 | wrap = true, 17 | filetype = 'markdown', 18 | }) 19 | 20 | autocomplete.listen_on_position_update(function() 21 | if autocomplete.win:is_open() then docs.update_position() end 22 | end) 23 | 24 | local timer = vim.uv.new_timer() 25 | local last_context_id = nil 26 | autocomplete.listen_on_select(function(item, context) 27 | timer:stop() 28 | if docs.win:is_open() or context.id == last_context_id then 29 | last_context_id = context.id 30 | timer:start(config.update_delay_ms, 0, function() 31 | vim.schedule(function() docs.show_item(item) end) 32 | end) 33 | elseif config.auto_show then 34 | timer:start(config.auto_show_delay_ms, 0, function() 35 | last_context_id = context.id 36 | vim.schedule(function() docs.show_item(item) end) 37 | end) 38 | end 39 | end) 40 | autocomplete.listen_on_close(function() docs.win:close() end) 41 | 42 | return docs 43 | end 44 | 45 | function docs.show_item(item) 46 | if item == nil then 47 | docs.win:close() 48 | return 49 | end 50 | 51 | -- TODO: cancellation 52 | -- TODO: only resolve if documentation does not exist 53 | sources 54 | .resolve(item) 55 | :map(function(item) 56 | if item.documentation == nil and item.detail == nil then 57 | docs.win:close() 58 | return 59 | end 60 | 61 | if docs.shown_item ~= item then 62 | require('blink.cmp.windows.lib.docs').render_detail_and_documentation( 63 | docs.win:get_buf(), 64 | item.detail, 65 | item.documentation, 66 | docs.win.config.max_width 67 | ) 68 | end 69 | docs.shown_item = item 70 | 71 | if autocomplete.win:get_win() then 72 | docs.win:open() 73 | vim.api.nvim_win_set_cursor(docs.win:get_win(), { 1, 0 }) -- reset scroll 74 | docs.update_position() 75 | end 76 | end) 77 | :catch(function(err) vim.notify(err, vim.log.levels.ERROR) end) 78 | end 79 | 80 | function docs.scroll_up(amount) 81 | local winnr = docs.win:get_win() 82 | local top_line = math.max(1, vim.fn.line('w0', winnr) - 1) 83 | local desired_line = math.max(1, top_line - amount) 84 | 85 | vim.api.nvim_win_set_cursor(docs.win:get_win(), { desired_line, 0 }) 86 | end 87 | 88 | function docs.scroll_down(amount) 89 | local winnr = docs.win:get_win() 90 | local line_count = vim.api.nvim_buf_line_count(docs.win:get_buf()) 91 | local bottom_line = math.max(1, vim.fn.line('w$', winnr) + 1) 92 | local desired_line = math.min(line_count, bottom_line + amount) 93 | 94 | vim.api.nvim_win_set_cursor(docs.win:get_win(), { desired_line, 0 }) 95 | end 96 | 97 | function docs.update_position() 98 | if not docs.win:is_open() or not autocomplete.win:is_open() then return end 99 | local winnr = docs.win:get_win() 100 | 101 | docs.win:update_size() 102 | 103 | local autocomplete_winnr = autocomplete.win:get_win() 104 | if not autocomplete_winnr then return end 105 | local autocomplete_win_config = vim.api.nvim_win_get_config(autocomplete_winnr) 106 | local autocomplete_win_height = autocomplete.win:get_height() 107 | local autocomplete_border_size = autocomplete.win:get_border_size() 108 | 109 | local cursor_screen_row = vim.fn.screenpos(0, unpack(vim.api.nvim_win_get_cursor(0))).row 110 | 111 | -- decide direction priority based on the autocomplete window's position 112 | local autocomplete_win_is_up = autocomplete_win_config.row - cursor_screen_row < 0 113 | local direction_priority = autocomplete_win_is_up and config.direction_priority.autocomplete_north 114 | or config.direction_priority.autocomplete_south 115 | 116 | -- remove the direction priority of the signature window if it's open 117 | if signature.win and signature.win:is_open() then 118 | direction_priority = vim.tbl_filter( 119 | function(dir) return dir ~= (autocomplete_win_is_up and 's' or 'n') end, 120 | direction_priority 121 | ) 122 | end 123 | 124 | -- decide direction, width and height of window 125 | local width = docs.win:get_width() 126 | local height = docs.win:get_height() 127 | local pos = docs.win:get_direction_with_window_constraints( 128 | autocomplete.win, 129 | direction_priority, 130 | { width = math.min(width, config.desired_min_width), height = math.min(height, config.desired_min_height) } 131 | ) 132 | 133 | -- couldn't find anywhere to place the window 134 | if not pos then 135 | docs.win:close() 136 | return 137 | end 138 | 139 | -- set width and height based on available space 140 | vim.api.nvim_win_set_height(docs.win:get_win(), pos.height) 141 | vim.api.nvim_win_set_width(docs.win:get_win(), pos.width) 142 | 143 | -- set position based on provided direction 144 | 145 | local height = docs.win:get_height() 146 | local width = docs.win:get_width() 147 | 148 | local function set_config(opts) 149 | vim.api.nvim_win_set_config(winnr, { relative = 'win', win = autocomplete_winnr, row = opts.row, col = opts.col }) 150 | end 151 | if pos.direction == 'n' then 152 | if autocomplete_win_is_up then 153 | set_config({ row = -height - autocomplete_border_size.top, col = -autocomplete_border_size.left }) 154 | else 155 | set_config({ row = -1 - height - autocomplete_border_size.top, col = -autocomplete_border_size.left }) 156 | end 157 | elseif pos.direction == 's' then 158 | if autocomplete_win_is_up then 159 | set_config({ 160 | row = 1 + autocomplete_win_height - autocomplete_border_size.top, 161 | col = -autocomplete_border_size.left, 162 | }) 163 | else 164 | set_config({ 165 | row = autocomplete_win_height - autocomplete_border_size.top, 166 | col = -autocomplete_border_size.left, 167 | }) 168 | end 169 | elseif pos.direction == 'e' then 170 | set_config({ 171 | row = -autocomplete_border_size.top, 172 | col = autocomplete_win_config.width + autocomplete_border_size.right, 173 | }) 174 | elseif pos.direction == 'w' then 175 | set_config({ row = -autocomplete_border_size.top, col = -width - autocomplete_border_size.left }) 176 | end 177 | end 178 | 179 | return docs 180 | -------------------------------------------------------------------------------- /lua/blink/cmp/sources/snippets/builtin.lua: -------------------------------------------------------------------------------- 1 | -- credit to https://github.com/L3MON4D3 for these variables 2 | -- see: https://github.com/L3MON4D3/LuaSnip/blob/master/lua/luasnip/util/_builtin_vars.lua 3 | -- and credit to https://github.com/garymjr for his changes 4 | -- see: https://github.com/garymjr/nvim-snippets/blob/main/lua/snippets/utils/builtin.lua 5 | 6 | local builtin = { 7 | lazy = {}, 8 | } 9 | 10 | function builtin.lazy.TM_FILENAME() return vim.fn.expand('%:t') end 11 | 12 | function builtin.lazy.TM_FILENAME_BASE() return vim.fn.expand('%:t:s?\\.[^\\.]\\+$??') end 13 | 14 | function builtin.lazy.TM_DIRECTORY() return vim.fn.expand('%:p:h') end 15 | 16 | function builtin.lazy.TM_FILEPATH() return vim.fn.expand('%:p') end 17 | 18 | function builtin.lazy.CLIPBOARD() return vim.fn.getreg(vim.v.register, true) end 19 | 20 | local function buf_to_ws_part() 21 | local LSP_WORSKPACE_PARTS = 'LSP_WORSKPACE_PARTS' 22 | local ok, ws_parts = pcall(vim.api.nvim_buf_get_var, 0, LSP_WORSKPACE_PARTS) 23 | if not ok then 24 | local file_path = vim.fn.expand('%:p') 25 | 26 | for _, ws in pairs(vim.lsp.buf.list_workspace_folders()) do 27 | if file_path:find(ws, 1, true) == 1 then 28 | ws_parts = { ws, file_path:sub(#ws + 2, -1) } 29 | break 30 | end 31 | end 32 | -- If it can't be extracted from lsp, then we use the file path 33 | if not ok and not ws_parts then ws_parts = { vim.fn.expand('%:p:h'), vim.fn.expand('%:p:t') } end 34 | vim.api.nvim_buf_set_var(0, LSP_WORSKPACE_PARTS, ws_parts) 35 | end 36 | return ws_parts 37 | end 38 | 39 | function builtin.lazy.RELATIVE_FILEPATH() -- The relative (to the opened workspace or folder) file path of the current document 40 | return buf_to_ws_part()[2] 41 | end 42 | 43 | function builtin.lazy.WORKSPACE_FOLDER() -- The path of the opened workspace or folder 44 | return buf_to_ws_part()[1] 45 | end 46 | 47 | function builtin.lazy.WORKSPACE_NAME() -- The name of the opened workspace or folder 48 | local parts = vim.split(buf_to_ws_part()[1] or '', '[\\/]') 49 | return parts[#parts] 50 | end 51 | 52 | function builtin.lazy.CURRENT_YEAR() return os.date('%Y') end 53 | 54 | function builtin.lazy.CURRENT_YEAR_SHORT() return os.date('%y') end 55 | 56 | function builtin.lazy.CURRENT_MONTH() return os.date('%m') end 57 | 58 | function builtin.lazy.CURRENT_MONTH_NAME() return os.date('%B') end 59 | 60 | function builtin.lazy.CURRENT_MONTH_NAME_SHORT() return os.date('%b') end 61 | 62 | function builtin.lazy.CURRENT_DATE() return os.date('%d') end 63 | 64 | function builtin.lazy.CURRENT_DAY_NAME() return os.date('%A') end 65 | 66 | function builtin.lazy.CURRENT_DAY_NAME_SHORT() return os.date('%a') end 67 | 68 | function builtin.lazy.CURRENT_HOUR() return os.date('%H') end 69 | 70 | function builtin.lazy.CURRENT_MINUTE() return os.date('%M') end 71 | 72 | function builtin.lazy.CURRENT_SECOND() return os.date('%S') end 73 | 74 | function builtin.lazy.CURRENT_SECONDS_UNIX() return tostring(os.time()) end 75 | 76 | local function get_timezone_offset(ts) 77 | local utcdate = os.date('!*t', ts) 78 | local localdate = os.date('*t', ts) 79 | localdate.isdst = false -- this is the trick 80 | local diff = os.difftime(os.time(localdate), os.time(utcdate)) 81 | local h, m = math.modf(diff / 3600) 82 | return string.format('%+.4d', 100 * h + 60 * m) 83 | end 84 | 85 | function builtin.lazy.CURRENT_TIMEZONE_OFFSET() 86 | return get_timezone_offset(os.time()):gsub('([+-])(%d%d)(%d%d)$', '%1%2:%3') 87 | end 88 | 89 | math.randomseed(os.time()) 90 | 91 | function builtin.lazy.RANDOM() return string.format('%06d', math.random(999999)) end 92 | 93 | function builtin.lazy.RANDOM_HEX() 94 | return string.format('%06x', math.random(16777216)) --16^6 95 | end 96 | 97 | function builtin.lazy.UUID() 98 | local random = math.random 99 | local template = 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx' 100 | local out 101 | local function subs(c) 102 | local v = (((c == 'x') and random(0, 15)) or random(8, 11)) 103 | return string.format('%x', v) 104 | end 105 | 106 | out = template:gsub('[xy]', subs) 107 | return out 108 | end 109 | 110 | local _comments_cache = {} 111 | local function buffer_comment_chars() 112 | local commentstring = vim.bo.commentstring 113 | if _comments_cache[commentstring] then return _comments_cache[commentstring] end 114 | local comments = { '//', '/*', '*/' } 115 | local placeholder = '%s' 116 | local index_placeholder = commentstring:find(vim.pesc(placeholder)) 117 | if index_placeholder then 118 | index_placeholder = index_placeholder - 1 119 | if index_placeholder + #placeholder == #commentstring then 120 | comments[1] = vim.trim(commentstring:sub(1, -#placeholder - 1)) 121 | else 122 | comments[2] = vim.trim(commentstring:sub(1, index_placeholder)) 123 | comments[3] = vim.trim(commentstring:sub(index_placeholder + #placeholder + 1, -1)) 124 | end 125 | end 126 | _comments_cache[commentstring] = comments 127 | return comments 128 | end 129 | 130 | function builtin.lazy.LINE_COMMENT() return buffer_comment_chars()[1] end 131 | 132 | function builtin.lazy.BLOCK_COMMENT_START() return buffer_comment_chars()[2] end 133 | 134 | function builtin.lazy.BLOCK_COMMENT_END() return buffer_comment_chars()[3] end 135 | 136 | local function get_cursor() 137 | local c = vim.api.nvim_win_get_cursor(0) 138 | c[1] = c[1] - 1 139 | return c 140 | end 141 | 142 | local function get_current_line() 143 | local pos = get_cursor() 144 | return vim.api.nvim_buf_get_lines(0, pos[1], pos[1] + 1, false)[1] 145 | end 146 | 147 | local function word_under_cursor(cur, line) 148 | if line == nil then return end 149 | 150 | local ind_start = 1 151 | local ind_end = #line 152 | 153 | while true do 154 | local tmp = string.find(line, '%W%w', ind_start) 155 | if not tmp then break end 156 | if tmp > cur[2] + 1 then break end 157 | ind_start = tmp + 1 158 | end 159 | 160 | local tmp = string.find(line, '%w%W', cur[2] + 1) 161 | if tmp then ind_end = tmp end 162 | 163 | return string.sub(line, ind_start, ind_end) 164 | end 165 | 166 | local function get_selected_text() 167 | if vim.fn.visualmode() == 'V' then return vim.fn.trim(vim.fn.getreg(vim.v.register, true), '\n', 2) end 168 | return '' 169 | end 170 | 171 | vim.api.nvim_create_autocmd('InsertEnter', { 172 | group = vim.api.nvim_create_augroup('BlinkSnippetsEagerEnter', { clear = true }), 173 | callback = function() 174 | builtin.eager = {} 175 | builtin.eager.TM_CURRENT_LINE = get_current_line() 176 | builtin.eager.TM_CURRENT_WORD = word_under_cursor(get_cursor(), builtin.eager.TM_CURRENT_LINE) 177 | builtin.eager.TM_LINE_INDEX = tostring(get_cursor()[1]) 178 | builtin.eager.TM_LINE_NUMBER = tostring(get_cursor()[1] + 1) 179 | builtin.eager.TM_SELECTED_TEXT = get_selected_text() 180 | end, 181 | }) 182 | 183 | vim.api.nvim_create_autocmd('InsertLeave', { 184 | group = vim.api.nvim_create_augroup('BlinkSnippetsEagerLeave', { clear = true }), 185 | callback = function() builtin.eager = nil end, 186 | }) 187 | 188 | return builtin 189 | -------------------------------------------------------------------------------- /lua/blink/cmp/sources/lib/provider/init.lua: -------------------------------------------------------------------------------- 1 | --- Wraps the sources to respect the configuration options and provide a unified interface 2 | --- @class blink.cmp.SourceProvider 3 | --- @field id string 4 | --- @field name string 5 | --- @field config blink.cmp.SourceProviderConfigWrapper 6 | --- @field module blink.cmp.Source 7 | --- @field last_response blink.cmp.CompletionResponse | nil 8 | --- @field resolve_tasks table 9 | --- 10 | --- @field new fun(id: string, config: blink.cmp.SourceProviderConfig): blink.cmp.SourceProvider 11 | --- @field enabled fun(self: blink.cmp.SourceProvider, context: blink.cmp.Context): boolean 12 | --- @field get_trigger_characters fun(self: blink.cmp.SourceProvider): string[] 13 | --- @field get_completions fun(self: blink.cmp.SourceProvider, context: blink.cmp.Context, enabled_sources: string[]): blink.cmp.Task 14 | --- @field should_show_items fun(self: blink.cmp.SourceProvider, context: blink.cmp.Context, enabled_sources: string[], response: blink.cmp.CompletionResponse): boolean 15 | --- @field resolve fun(self: blink.cmp.SourceProvider, item: blink.cmp.CompletionItem): blink.cmp.Task 16 | --- @field get_signature_help_trigger_characters fun(self: blink.cmp.SourceProvider): string[] 17 | --- @field get_signature_help fun(self: blink.cmp.SourceProvider, context: blink.cmp.SignatureHelpContext): blink.cmp.Task 18 | --- @field reload (fun(self: blink.cmp.SourceProvider): nil) | nil 19 | 20 | --- @type blink.cmp.SourceProvider 21 | --- @diagnostic disable-next-line: missing-fields 22 | local source = {} 23 | 24 | local utils = require('blink.cmp.sources.lib.utils') 25 | local async = require('blink.cmp.sources.lib.async') 26 | 27 | function source.new(id, config) 28 | assert(type(config.name) == 'string', 'Each source in config.sources.providers must have a "name" of type string') 29 | assert(type(config.module) == 'string', 'Each source in config.sources.providers must have a "module" of type string') 30 | 31 | local self = setmetatable({}, { __index = source }) 32 | self.id = id 33 | self.name = config.name 34 | self.module = require('blink.cmp.sources.lib.provider.override').new( 35 | require(config.module).new(config.opts, config), 36 | config.override 37 | ) 38 | self.config = require('blink.cmp.sources.lib.provider.config').new(config) 39 | self.last_response = nil 40 | self.resolve_tasks = {} 41 | 42 | return self 43 | end 44 | 45 | function source:enabled(context) 46 | -- user defined 47 | if not self.config.enabled(context) then return false end 48 | 49 | -- source defined 50 | if self.module.enabled == nil then return true end 51 | return self.module:enabled(context) 52 | end 53 | 54 | --- Completion --- 55 | 56 | function source:get_trigger_characters() 57 | if self.module.get_trigger_characters == nil then return {} end 58 | return self.module:get_trigger_characters() 59 | end 60 | 61 | function source:get_completions(context, enabled_sources) 62 | -- Return the previous successful completions if the context is the same 63 | -- and the data doesn't need to be updated 64 | if self.last_response ~= nil and self.last_response.context.id == context.id then 65 | if utils.should_run_request(context, self.last_response) == false then 66 | return async.task.new(function(resolve) resolve(require('blink.cmp.utils').shallow_copy(self.last_response)) end) 67 | end 68 | end 69 | 70 | return async.task 71 | .new(function(resolve) 72 | if self.module.get_completions == nil then return resolve() end 73 | return self.module:get_completions(context, resolve) 74 | end) 75 | :map(function(response) 76 | if response == nil then response = { is_incomplete_forward = true, is_incomplete_backward = true, items = {} } end 77 | response.context = context 78 | 79 | -- add non-lsp metadata 80 | local source_score_offset = self.config.score_offset(context, enabled_sources) or 0 81 | for _, item in ipairs(response.items) do 82 | item.score_offset = (item.score_offset or 0) + source_score_offset 83 | item.cursor_column = context.cursor[2] 84 | item.source_id = self.id 85 | item.source_name = self.name 86 | end 87 | 88 | -- if the user provided a transform_items function, run it 89 | if self.config.transform_items ~= nil then 90 | response.items = self.config.transform_items(context, response.items) 91 | end 92 | 93 | self.last_response = require('blink.cmp.utils').shallow_copy(response) 94 | self.last_response.is_cached = true 95 | return response 96 | end) 97 | :catch(function(err) 98 | vim.print('failed to get completions with error: ' .. err) 99 | return { is_incomplete_forward = false, is_incomplete_backward = false, items = {} } 100 | end) 101 | end 102 | 103 | function source:should_show_items(context, enabled_sources, response) 104 | -- if keyword length is configured, check if the context is long enough 105 | local min_keyword_length = self.config.min_keyword_length(context, enabled_sources) 106 | local current_keyword_length = context.bounds.length 107 | if current_keyword_length < min_keyword_length then return false end 108 | 109 | if self.config.should_show_items == nil then return true end 110 | return self.config.should_show_items(context, response.items) 111 | end 112 | 113 | --- Resolve --- 114 | 115 | --- @param item blink.cmp.CompletionItem 116 | --- @return blink.cmp.Task 117 | function source:resolve(item) 118 | local tasks = self.resolve_tasks 119 | if tasks[item] == nil or tasks[item].status == async.STATUS.CANCELLED then 120 | tasks[item] = async.task.new(function(resolve) 121 | if self.module.resolve == nil then return resolve(item) end 122 | return self.module:resolve(item, function(resolved_item) 123 | -- use the item's existing documentation and detail if the LSP didn't return it 124 | -- TODO: do we need this? this would be for java but never checked if it's needed 125 | if resolved_item ~= nil and resolved_item.documentation == nil then 126 | resolved_item.documentation = item.documentation 127 | end 128 | if resolved_item ~= nil and resolved_item.detail == nil then resolved_item.detail = item.detail end 129 | 130 | vim.schedule(function() resolve(resolved_item or item) end) 131 | end) 132 | end) 133 | end 134 | return tasks[item] 135 | end 136 | 137 | --- Signature help --- 138 | 139 | function source:get_signature_help_trigger_characters() 140 | if self.module.get_signature_help_trigger_characters == nil then 141 | return { trigger_characters = {}, retrigger_characters = {} } 142 | end 143 | return self.module:get_signature_help_trigger_characters() 144 | end 145 | 146 | function source:get_signature_help(context) 147 | return async.task.new(function(resolve) 148 | if self.module.get_signature_help == nil then return resolve(nil) end 149 | return self.module:get_signature_help(context, function(signature_help) 150 | vim.schedule(function() resolve(signature_help) end) 151 | end) 152 | end) 153 | end 154 | 155 | --- Misc --- 156 | 157 | --- For external integrations to force reloading the source 158 | function source:reload() 159 | if self.module.reload == nil then return end 160 | self.module:reload() 161 | end 162 | 163 | return source 164 | -------------------------------------------------------------------------------- /lua/blink/cmp/fuzzy/download.lua: -------------------------------------------------------------------------------- 1 | local download_config = require('blink.cmp.config').fuzzy.prebuilt_binaries 2 | 3 | local download = {} 4 | 5 | --- @return string 6 | function download.get_lib_extension() 7 | if jit.os:lower() == 'mac' or jit.os:lower() == 'osx' then return '.dylib' end 8 | if jit.os:lower() == 'windows' then return '.dll' end 9 | return '.so' 10 | end 11 | 12 | local root_dir = debug.getinfo(1).source:match('@?(.*/)') 13 | download.lib_path = root_dir .. '../../../../target/release/libblink_cmp_fuzzy' .. download.get_lib_extension() 14 | local version_path = root_dir .. '../../../../target/release/version.txt' 15 | 16 | --- @param callback fun(err: string | nil) 17 | function download.ensure_downloaded(callback) 18 | callback = vim.schedule_wrap(callback) 19 | 20 | if not download_config.download then return callback() end 21 | 22 | download.get_git_tag(function(git_version_err, git_version) 23 | if git_version_err then return callback(git_version_err) end 24 | 25 | download.get_downloaded_version(function(version_err, version) 26 | download.is_downloaded(function(downloaded) 27 | local target_version = download_config.force_version or git_version 28 | 29 | -- not built locally, not a git tag, error 30 | if not downloaded and not target_version then 31 | return callback( 32 | "Can't download from github due to not being on a git tag and no fuzzy.prebuilt_binaries.force_version set, but found no built version of the library. " 33 | .. 'Either run `cargo build --release` via your package manager, switch to a git tag, or set `fuzzy.prebuilt_binaries.force_version` in config. ' 34 | .. 'See the README for more info.' 35 | ) 36 | end 37 | -- built locally, ignore 38 | if downloaded and (version_err or version == nil) then return callback() end 39 | -- already downloaded and the correct version 40 | if version == target_version and downloaded then return callback() end 41 | -- unknown state 42 | if not target_version then 43 | return callback('Unknown error while getting pre-built binary. Consider re-installing') 44 | end 45 | 46 | -- download from github and set version 47 | download.from_github(target_version, function(download_err) 48 | if download_err then return callback(download_err) end 49 | download.set_downloaded_version(target_version, function(set_err) 50 | if set_err then return callback(set_err) end 51 | callback() 52 | end) 53 | end) 54 | end) 55 | end) 56 | end) 57 | end 58 | 59 | --- @param cb fun(downloaded: boolean) 60 | function download.is_downloaded(cb) 61 | vim.uv.fs_stat(download.lib_path, function(err) 62 | if not err then return cb(true) end 63 | 64 | -- If not found, check without 'lib' prefix 65 | vim.uv.fs_stat( 66 | string.gsub(download.lib_path, 'libblink_cmp_fuzzy', 'blink_cmp_fuzzy'), 67 | function(error) cb(not error) end 68 | ) 69 | end) 70 | end 71 | 72 | --- @param cb fun(err: string | nil, tag: string | nil) 73 | function download.get_git_tag(cb) 74 | vim.system({ 'git', 'describe', '--tags', '--exact-match' }, { cwd = root_dir }, function(out) 75 | if out.code == 128 then return cb() end 76 | if out.code ~= 0 then 77 | return cb('While getting git tag, git exited with code ' .. out.code .. ': ' .. out.stderr) 78 | end 79 | local lines = vim.split(out.stdout, '\n') 80 | if not lines[1] then return cb('Expected atleast 1 line of output from git describe') end 81 | return cb(nil, lines[1]) 82 | end) 83 | end 84 | 85 | --- @param tag string 86 | --- @param cb fun(err: string | nil) 87 | function download.from_github(tag, cb) 88 | download.get_system_triple(function(system_triple) 89 | if not system_triple then 90 | return cb( 91 | 'Your system is not supported by pre-built binaries. You must run cargo build --release via your package manager with rust nightly. See the README for more info.' 92 | ) 93 | end 94 | 95 | local url = 'https://github.com/saghen/blink.cmp/releases/download/' 96 | .. tag 97 | .. '/' 98 | .. system_triple 99 | .. download.get_lib_extension() 100 | 101 | vim.system({ 'curl', '--create-dirs', '-fLo', download.lib_path, url }, {}, function(out) 102 | if out.code ~= 0 then cb('Failed to download pre-build binaries: ' .. out.stderr) end 103 | cb() 104 | end) 105 | end) 106 | end 107 | 108 | --- @param cb fun(err: string | nil, last_version: string | nil) 109 | function download.get_downloaded_version(cb) 110 | return vim.uv.fs_open(version_path, 'r', 438, function(open_err, fd) 111 | if open_err or fd == nil then return cb(open_err or 'Unknown error') end 112 | vim.uv.fs_read(fd, 8, 0, function(read_err, data) 113 | vim.uv.fs_close(fd, function() end) 114 | if read_err or data == nil then return cb(read_err or 'Unknown error') end 115 | return cb(nil, data) 116 | end) 117 | end) 118 | end 119 | 120 | --- @param version string 121 | --- @param cb fun(err: string | nil) 122 | function download.set_downloaded_version(version, cb) 123 | return vim.uv.fs_open(version_path, 'w', 438, function(open_err, fd) 124 | if open_err or fd == nil then return cb(open_err or 'Unknown error') end 125 | vim.uv.fs_write(fd, version, 0, function(write_err) 126 | vim.uv.fs_close(fd, function() end) 127 | if write_err then return cb(write_err) end 128 | return cb() 129 | end) 130 | end) 131 | end 132 | 133 | --- @param cb fun(triple: string | nil) 134 | function download.get_system_triple(cb) 135 | if download_config.force_system_triple then return cb(download_config.force_system_triple) end 136 | 137 | if jit.os:lower() == 'mac' or jit.os:lower() == 'osx' then 138 | if jit.arch:lower():match('arm') then return cb('aarch64-apple-darwin') end 139 | if jit.arch:lower():match('x64') then return cb('x86_64-apple-darwin') end 140 | elseif jit.os:lower() == 'windows' then 141 | if jit.arch:lower():match('x64') then return cb('x86_64-pc-windows-msvc') end 142 | elseif jit.os:lower() == 'linux' then 143 | vim.uv.fs_stat('/etc/alpine-release', function(err, is_alpine) 144 | local libc = (not err and is_alpine) and 'musl' or 'gnu' 145 | if jit.arch:lower():match('arm') then return cb('aarch64-unknown-linux-' .. libc) end 146 | if jit.arch:lower():match('x64') then return cb('x86_64-unknown-linux-' .. libc) end 147 | return cb(nil) 148 | end) 149 | else 150 | return cb(nil) 151 | end 152 | end 153 | 154 | function download.get_system_triple_sync() 155 | if download_config.force_system_triple then return download_config.force_system_triple end 156 | 157 | if jit.os:lower() == 'mac' or jit.os:lower() == 'osx' then 158 | if jit.arch:lower():match('arm') then return 'aarch64-apple-darwin' end 159 | if jit.arch:lower():match('x64') then return 'x86_64-apple-darwin' end 160 | elseif jit.os:lower() == 'windows' then 161 | if jit.arch:lower():match('x64') then return 'x86_64-pc-windows-msvc' end 162 | elseif jit.os:lower() == 'linux' then 163 | local success, is_alpine = pcall(vim.uv.fs_stat, '/etc/alpine-release') 164 | local libc = (success and is_alpine) and 'musl' or 'gnu' 165 | 166 | if jit.arch:lower():match('arm') then return 'aarch64-unknown-linux-' .. libc end 167 | if jit.arch:lower():match('x64') then return 'x86_64-unknown-linux-' .. libc end 168 | end 169 | end 170 | 171 | return download 172 | -------------------------------------------------------------------------------- /lua/blink/cmp/sources/lib/context.lua: -------------------------------------------------------------------------------- 1 | local utils = require('blink.cmp.sources.lib.utils') 2 | local async = require('blink.cmp.sources.lib.async') 3 | 4 | --- @class blink.cmp.SourcesContext 5 | --- @field id number 6 | --- @field sources table 7 | --- @field active_request blink.cmp.Task | nil 8 | --- @field queued_request_context blink.cmp.Context | nil 9 | --- @field cached_responses table | nil 10 | --- @field on_completions_callback fun(context: blink.cmp.Context, enabled_sources: string[], responses: table) 11 | --- 12 | --- @field new fun(context: blink.cmp.Context, sources: table, on_completions_callback: fun(context: blink.cmp.Context, items: table)): blink.cmp.SourcesContext 13 | --- @field get_sources fun(self: blink.cmp.SourcesContext): string[] 14 | --- @field get_cached_completions fun(self: blink.cmp.SourcesContext): table | nil 15 | --- @field get_completions fun(self: blink.cmp.SourcesContext, context: blink.cmp.Context) 16 | --- @field get_completions_for_sources fun(self: blink.cmp.SourcesContext, sources: table, context: blink.cmp.Context): blink.cmp.Task 17 | --- @field get_completions_with_fallbacks fun(self: blink.cmp.SourcesContext, context: blink.cmp.Context, source: blink.cmp.SourceProvider, sources: table): blink.cmp.Task 18 | --- @field destroy fun(self: blink.cmp.SourcesContext) 19 | 20 | --- @type blink.cmp.SourcesContext 21 | --- @diagnostic disable-next-line: missing-fields 22 | local sources_context = {} 23 | 24 | function sources_context.new(context, sources, on_completions_callback) 25 | local self = setmetatable({}, { __index = sources_context }) 26 | self.id = context.id 27 | self.sources = sources 28 | 29 | self.active_request = nil 30 | self.queued_request_context = nil 31 | self.on_completions_callback = on_completions_callback 32 | 33 | return self 34 | end 35 | 36 | function sources_context:get_sources() return vim.tbl_keys(self.sources) end 37 | 38 | function sources_context:get_cached_completions() return self.cached_responses end 39 | 40 | function sources_context:get_completions(context) 41 | assert(context.id == self.id, 'Requested completions on a sources context with a different context ID') 42 | 43 | if self.active_request ~= nil and self.active_request.status == async.STATUS.RUNNING then 44 | self.queued_request_context = context 45 | return 46 | end 47 | 48 | -- Create a task to get the completions, send responses upstream 49 | -- and run the queued request, if it exists 50 | self.active_request = self:get_completions_for_sources(self.sources, context):map(function(responses) 51 | self.cached_responses = responses 52 | --- @cast responses table 53 | self.active_request = nil 54 | 55 | -- only send upstream if the responses contain something new 56 | local is_cached = true 57 | for _, response in pairs(responses) do 58 | is_cached = is_cached and (response.is_cached or false) 59 | end 60 | if not is_cached then self.on_completions_callback(context, self:get_sources(), responses) end 61 | 62 | -- run the queued request, if it exists 63 | if self.queued_request_context ~= nil then 64 | local queued_context = self.queued_request_context 65 | self.queued_request_context = nil 66 | self:get_completions(queued_context) 67 | end 68 | end) 69 | end 70 | 71 | function sources_context:get_completions_for_sources(sources, context) 72 | local enabled_sources = vim.tbl_keys(sources) 73 | --- @type blink.cmp.SourceProvider[] 74 | local non_fallback_sources = vim.tbl_filter(function(source) 75 | local fallbacks = source.config.fallback_for and source.config.fallback_for(context, enabled_sources) or {} 76 | fallbacks = vim.tbl_filter(function(fallback) return sources[fallback] end, fallbacks) 77 | return #fallbacks == 0 78 | end, vim.tbl_values(sources)) 79 | 80 | -- get completions for each non-fallback source 81 | local tasks = vim.tbl_map(function(source) 82 | -- the source indicates we should refetch when this character is typed 83 | local trigger_character = context.trigger.character 84 | and vim.tbl_contains(source:get_trigger_characters(), context.trigger.character) 85 | 86 | -- The TriggerForIncompleteCompletions kind is handled by the source provider itself 87 | local source_context = require('blink.cmp.utils').shallow_copy(context) 88 | source_context.trigger = trigger_character 89 | and { kind = vim.lsp.protocol.CompletionTriggerKind.TriggerCharacter, character = context.trigger.character } 90 | or { kind = vim.lsp.protocol.CompletionTriggerKind.Invoked } 91 | 92 | return self:get_completions_with_fallbacks(source_context, source, sources) 93 | end, non_fallback_sources) 94 | 95 | -- wait for all the tasks to complete 96 | return async.task 97 | .await_all(tasks) 98 | :map(function(tasks_results) 99 | local responses = {} 100 | for idx, task_result in ipairs(tasks_results) do 101 | if task_result.status == async.STATUS.COMPLETED then 102 | --- @type blink.cmp.SourceProvider 103 | local source = vim.tbl_values(non_fallback_sources)[idx] 104 | responses[source.id] = task_result.result 105 | end 106 | end 107 | return responses 108 | end) 109 | :catch(function(err) 110 | vim.print('failed to get completions for sources with error: ' .. err) 111 | return {} 112 | end) 113 | end 114 | 115 | --- Runs the source's get_completions function, falling back to other sources 116 | --- with fallback_for = { source.name } if the source returns no completion items 117 | --- TODO: When a source has multiple fallbacks, we may end up with duplicate completion items 118 | function sources_context:get_completions_with_fallbacks(context, source, sources) 119 | local enabled_sources = vim.tbl_keys(sources) 120 | local fallback_sources = vim.tbl_filter( 121 | function(fallback_source) 122 | return fallback_source.id ~= source.id 123 | and fallback_source.config.fallback_for ~= nil 124 | and vim.tbl_contains(fallback_source.config.fallback_for(context), source.id) 125 | end, 126 | vim.tbl_values(sources) 127 | ) 128 | 129 | return source:get_completions(context, enabled_sources):map(function(response) 130 | -- source returned completions, no need to fallback 131 | if #response.items > 0 or #fallback_sources == 0 then return response end 132 | 133 | -- run fallbacks 134 | return async.task 135 | .await_all(vim.tbl_map(function(fallback) return fallback:get_completions(context) end, fallback_sources)) 136 | :map(function(task_results) 137 | local successful_task_results = vim.tbl_filter( 138 | function(task_result) return task_result.status == async.STATUS.COMPLETED end, 139 | task_results 140 | ) 141 | local fallback_responses = vim.tbl_map( 142 | function(task_result) return task_result.result end, 143 | successful_task_results 144 | ) 145 | return utils.concat_responses(fallback_responses) 146 | end) 147 | end) 148 | end 149 | 150 | function sources_context:destroy() 151 | self.on_completions_callback = function() end 152 | if self.active_request ~= nil then self.active_request:cancel() end 153 | end 154 | 155 | return sources_context 156 | -------------------------------------------------------------------------------- /lua/blink/cmp/init.lua: -------------------------------------------------------------------------------- 1 | local cmp = {} 2 | 3 | --- @param opts blink.cmp.Config 4 | cmp.setup = function(opts) 5 | local config = require('blink.cmp.config') 6 | config.merge_with(opts) 7 | 8 | require('blink.cmp.fuzzy.download').ensure_downloaded(function(err) 9 | if err then 10 | vim.notify('Error while downloading blink.cmp pre-built binary: ' .. err, vim.log.levels.ERROR) 11 | return 12 | end 13 | 14 | cmp.add_default_highlights() 15 | 16 | require('blink.cmp.keymap').setup(config.keymap) 17 | 18 | -- STRUCTURE 19 | -- trigger -> sources -> fuzzy (filter/sort) -> windows (render) 20 | 21 | -- trigger controls when to show the window and the current context for caching 22 | -- TODO: add first_trigger event for setting up the rest of the plugin 23 | cmp.trigger = require('blink.cmp.trigger.completion').activate_autocmds() 24 | 25 | -- sources fetch autocomplete items, documentation and signature help 26 | cmp.sources = require('blink.cmp.sources.lib') 27 | cmp.sources.register() 28 | 29 | -- windows render and apply completion items and signature help 30 | cmp.windows = { 31 | autocomplete = require('blink.cmp.windows.autocomplete').setup(), 32 | documentation = require('blink.cmp.windows.documentation').setup(), 33 | ghost_text = require('blink.cmp.windows.ghost-text').setup(), 34 | } 35 | 36 | cmp.trigger.listen_on_show(function(context) cmp.sources.request_completions(context) end) 37 | cmp.trigger.listen_on_hide(function() 38 | cmp.sources.cancel_completions() 39 | cmp.windows.autocomplete.close() 40 | end) 41 | cmp.sources.listen_on_completions(function(context, items) 42 | -- fuzzy combines smith waterman with frecency 43 | -- and bonus from proximity words but I'm still working 44 | -- on tuning the weights 45 | if not cmp.fuzzy then 46 | cmp.fuzzy = require('blink.cmp.fuzzy') 47 | cmp.fuzzy.init_db(vim.fn.stdpath('data') .. '/blink/cmp/fuzzy.db') 48 | end 49 | 50 | -- we avoid adding 0.5-4ms to insertion latency by scheduling for later 51 | vim.schedule(function() 52 | if cmp.trigger.context == nil or cmp.trigger.context.id ~= context.id then return end 53 | 54 | local filtered_items = cmp.fuzzy.filter_items(cmp.fuzzy.get_query(), items) 55 | filtered_items = cmp.sources.apply_max_items_for_completions(context, filtered_items) 56 | if #filtered_items > 0 then 57 | cmp.windows.autocomplete.open_with_items(context, filtered_items) 58 | else 59 | cmp.windows.autocomplete.close() 60 | end 61 | end) 62 | end) 63 | 64 | -- setup signature help if enabled 65 | if config.trigger.signature_help.enabled then cmp.setup_signature_help() end 66 | end) 67 | end 68 | 69 | cmp.setup_signature_help = function() 70 | local signature_trigger = require('blink.cmp.trigger.signature').activate_autocmds() 71 | local signature_window = require('blink.cmp.windows.signature').setup() 72 | 73 | signature_trigger.listen_on_show(function(context) 74 | cmp.sources.cancel_signature_help() 75 | cmp.sources.get_signature_help(context, function(signature_help) 76 | if signature_help ~= nil and signature_trigger.context ~= nil and signature_trigger.context.id == context.id then 77 | signature_trigger.set_active_signature_help(signature_help) 78 | signature_window.open_with_signature_help(context, signature_help) 79 | else 80 | signature_trigger.hide() 81 | end 82 | end) 83 | end) 84 | signature_trigger.listen_on_hide(function() signature_window.close() end) 85 | end 86 | 87 | cmp.add_default_highlights = function() 88 | local use_nvim_cmp = require('blink.cmp.config').highlight.use_nvim_cmp_as_default 89 | 90 | local set_hl = function(hl_group, opts) 91 | opts.default = true 92 | vim.api.nvim_set_hl(0, hl_group, opts) 93 | end 94 | 95 | set_hl('BlinkCmpLabel', { link = use_nvim_cmp and 'CmpItemAbbr' or 'Pmenu' }) 96 | set_hl('BlinkCmpLabelDeprecated', { link = use_nvim_cmp and 'CmpItemAbbrDeprecated' or 'Comment' }) 97 | set_hl('BlinkCmpLabelMatch', { link = use_nvim_cmp and 'CmpItemAbbrMatch' or 'Pmenu' }) 98 | set_hl('BlinkCmpKind', { link = use_nvim_cmp and 'CmpItemKind' or 'Special' }) 99 | for _, kind in ipairs(require('blink.cmp.types').CompletionItemKind) do 100 | set_hl('BlinkCmpKind' .. kind, { link = use_nvim_cmp and 'CmpItemKind' .. kind or 'BlinkCmpKind' }) 101 | end 102 | 103 | set_hl('BlinkCmpScrollBarThumb', { link = 'Visual' }) 104 | set_hl('BlinkCmpScrollBarGutter', { link = 'Pmenu' }) 105 | 106 | set_hl('BlinkCmpGhostText', { link = use_nvim_cmp and 'CmpGhostText' or 'Comment' }) 107 | 108 | set_hl('BlinkCmpMenu', { link = 'Pmenu' }) 109 | set_hl('BlinkCmpMenuBorder', { link = 'Pmenu' }) 110 | set_hl('BlinkCmpMenuSelection', { link = 'PmenuSel' }) 111 | 112 | set_hl('BlinkCmpDoc', { link = 'NormalFloat' }) 113 | set_hl('BlinkCmpDocBorder', { link = 'FloatBorder' }) 114 | set_hl('BlinkCmpDocCursorLine', { link = 'Visual' }) 115 | 116 | set_hl('BlinkCmpSignatureHelp', { link = 'NormalFloat' }) 117 | set_hl('BlinkCmpSignatureHelpBorder', { link = 'FloatBorder' }) 118 | set_hl('BlinkCmpSignatureHelpActiveParameter', { link = 'LspSignatureActiveParameter' }) 119 | end 120 | 121 | ------- Public API ------- 122 | 123 | cmp.show = function() 124 | if cmp.windows.autocomplete.win:is_open() then return end 125 | vim.schedule(function() 126 | cmp.windows.autocomplete.auto_show = true 127 | cmp.trigger.show({ force = true }) 128 | end) 129 | return true 130 | end 131 | 132 | cmp.hide = function() 133 | if not cmp.windows.autocomplete.win:is_open() then return end 134 | vim.schedule(cmp.trigger.hide) 135 | return true 136 | end 137 | 138 | --- @param callback fun(context: blink.cmp.Context) 139 | cmp.on_open = function(callback) cmp.windows.autocomplete.listen_on_open(callback) end 140 | 141 | --- @param callback fun() 142 | cmp.on_close = function(callback) cmp.windows.autocomplete.listen_on_close(callback) end 143 | 144 | cmp.accept = function() 145 | local item = cmp.windows.autocomplete.get_selected_item() 146 | if item == nil then return end 147 | 148 | vim.schedule(function() cmp.windows.autocomplete.accept() end) 149 | return true 150 | end 151 | 152 | cmp.select_and_accept = function() 153 | if not cmp.windows.autocomplete.win:is_open() then return end 154 | 155 | vim.schedule(function() 156 | -- select an item if none is selected 157 | if not cmp.windows.autocomplete.get_selected_item() then 158 | -- avoid running auto_insert since we're about to accept anyway 159 | cmp.windows.autocomplete.select_next({ skip_auto_insert = true }) 160 | end 161 | 162 | local item = cmp.windows.autocomplete.get_selected_item() 163 | if item ~= nil then require('blink.cmp.accept')(item) end 164 | end) 165 | return true 166 | end 167 | 168 | cmp.select_prev = function() 169 | if not cmp.windows.autocomplete.win:is_open() then 170 | if cmp.windows.autocomplete.auto_show then return end 171 | cmp.show() 172 | return true 173 | end 174 | vim.schedule(cmp.windows.autocomplete.select_prev) 175 | return true 176 | end 177 | 178 | cmp.select_next = function() 179 | if not cmp.windows.autocomplete.win:is_open() then 180 | if cmp.windows.autocomplete.auto_show then return end 181 | cmp.show() 182 | return true 183 | end 184 | vim.schedule(cmp.windows.autocomplete.select_next) 185 | return true 186 | end 187 | 188 | cmp.show_documentation = function() 189 | if cmp.windows.documentation.win:is_open() then return end 190 | local item = cmp.windows.autocomplete.get_selected_item() 191 | if not item then return end 192 | vim.schedule(function() cmp.windows.documentation.show_item(item) end) 193 | return true 194 | end 195 | 196 | cmp.hide_documentation = function() 197 | if not cmp.windows.documentation.win:is_open() then return end 198 | vim.schedule(function() cmp.windows.documentation.win:close() end) 199 | return true 200 | end 201 | 202 | cmp.scroll_documentation_up = function() 203 | if not cmp.windows.documentation.win:is_open() then return end 204 | vim.schedule(function() cmp.windows.documentation.scroll_up(4) end) 205 | return true 206 | end 207 | 208 | cmp.scroll_documentation_down = function() 209 | if not cmp.windows.documentation.win:is_open() then return end 210 | vim.schedule(function() cmp.windows.documentation.scroll_down(4) end) 211 | return true 212 | end 213 | 214 | cmp.is_in_snippet = function() return vim.snippet.active() end 215 | 216 | cmp.snippet_forward = function() 217 | if not vim.snippet.active({ direction = 1 }) then return end 218 | vim.schedule(function() vim.snippet.jump(1) end) 219 | return true 220 | end 221 | 222 | cmp.snippet_backward = function() 223 | if not vim.snippet.active({ direction = -1 }) then return end 224 | vim.schedule(function() vim.snippet.jump(-1) end) 225 | return true 226 | end 227 | 228 | --- @param override? lsp.ClientCapabilities 229 | --- @param include_nvim_defaults? boolean 230 | cmp.get_lsp_capabilities = function(override, include_nvim_defaults) 231 | return require('blink.cmp.sources.lib').get_lsp_capabilities(override, include_nvim_defaults) 232 | end 233 | 234 | return cmp 235 | -------------------------------------------------------------------------------- /lua/blink/cmp/windows/lib/docs.lua: -------------------------------------------------------------------------------- 1 | local docs = {} 2 | 3 | --- @param bufnr number 4 | --- @param detail? string 5 | --- @param documentation? lsp.MarkupContent | string 6 | function docs.render_detail_and_documentation(bufnr, detail, documentation, max_width) 7 | local detail_lines = {} 8 | if detail and detail ~= '' then detail_lines = docs.split_lines(detail) end 9 | 10 | local doc_lines = {} 11 | if documentation ~= nil then 12 | local doc = type(documentation) == 'string' and documentation or documentation.value 13 | doc_lines = docs.split_lines(doc) 14 | if type(documentation) ~= 'string' and documentation.kind == 'markdown' then 15 | -- if the rendering seems bugged, it's likely due to this function 16 | doc_lines = docs.combine_markdown_lines(doc_lines) 17 | end 18 | end 19 | 20 | detail_lines, doc_lines = docs.extract_detail_from_doc(detail_lines, doc_lines) 21 | 22 | local combined_lines = vim.list_extend({}, detail_lines) 23 | -- add a blank line for the --- separator 24 | if #detail_lines > 0 and #doc_lines > 0 then table.insert(combined_lines, '') end 25 | vim.list_extend(combined_lines, doc_lines) 26 | 27 | vim.api.nvim_buf_set_lines(bufnr, 0, -1, true, combined_lines) 28 | vim.api.nvim_set_option_value('modified', false, { buf = bufnr }) 29 | 30 | -- Highlight with treesitter 31 | vim.api.nvim_buf_clear_namespace(bufnr, require('blink.cmp.config').highlight.ns, 0, -1) 32 | 33 | if #detail_lines > 0 then docs.highlight_with_treesitter(bufnr, vim.bo.filetype, 0, #detail_lines) end 34 | 35 | -- Only add the separator if there are documentation lines (otherwise only display the detail) 36 | if #detail_lines > 0 and #doc_lines > 0 then 37 | vim.api.nvim_buf_set_extmark(bufnr, require('blink.cmp.config').highlight.ns, #detail_lines, 0, { 38 | virt_text = { { string.rep('─', max_width) } }, 39 | virt_text_pos = 'overlay', 40 | hl_eol = true, 41 | hl_group = 'BlinkCmpDocDetail', 42 | }) 43 | end 44 | 45 | if #doc_lines > 0 then 46 | local start = #detail_lines + (#detail_lines > 0 and 1 or 0) 47 | docs.highlight_with_treesitter(bufnr, 'markdown', start, start + #doc_lines) 48 | end 49 | end 50 | 51 | --- Highlights the given range with treesitter with the given filetype 52 | --- @param bufnr number 53 | --- @param filetype string 54 | --- @param start_line number 55 | --- @param end_line number 56 | --- TODO: fallback to regex highlighting if treesitter fails 57 | --- TODO: only render what's visible 58 | function docs.highlight_with_treesitter(bufnr, filetype, start_line, end_line) 59 | local Range = require('vim.treesitter._range') 60 | 61 | local root_lang = vim.treesitter.language.get_lang(filetype) 62 | if root_lang == nil then return end 63 | 64 | local success, trees = pcall(vim.treesitter.get_parser, bufnr, root_lang) 65 | if not success or not trees then return end 66 | 67 | trees:parse({ start_line, end_line }) 68 | 69 | trees:for_each_tree(function(tree, tstree) 70 | local lang = tstree:lang() 71 | local highlighter_query = vim.treesitter.query.get(lang, 'highlights') 72 | if not highlighter_query then return end 73 | 74 | local root_node = tree:root() 75 | local _, _, root_end_row, _ = root_node:range() 76 | 77 | local iter = highlighter_query:iter_captures(tree:root(), bufnr, start_line, end_line) 78 | local line = start_line 79 | while line < end_line do 80 | local capture, node, metadata, _ = iter(line) 81 | if capture == nil then break end 82 | 83 | local range = { root_end_row + 1, 0, root_end_row + 1, 0 } 84 | if node then range = vim.treesitter.get_range(node, bufnr, metadata and metadata[capture]) end 85 | local start_row, start_col, end_row, end_col = Range.unpack4(range) 86 | 87 | if capture then 88 | local name = highlighter_query.captures[capture] 89 | local hl = 0 90 | if not vim.startswith(name, '_') then hl = vim.api.nvim_get_hl_id_by_name('@' .. name .. '.' .. lang) end 91 | 92 | -- The "priority" attribute can be set at the pattern level or on a particular capture 93 | local priority = ( 94 | tonumber(metadata.priority or metadata[capture] and metadata[capture].priority) 95 | or vim.highlight.priorities.treesitter 96 | ) 97 | 98 | -- The "conceal" attribute can be set at the pattern level or on a particular capture 99 | local conceal = metadata.conceal or metadata[capture] and metadata[capture].conceal 100 | 101 | if hl and end_row >= line then 102 | vim.api.nvim_buf_set_extmark(bufnr, require('blink.cmp.config').highlight.ns, start_row, start_col, { 103 | end_line = end_row, 104 | end_col = end_col, 105 | hl_group = hl, 106 | priority = priority, 107 | conceal = conceal, 108 | }) 109 | end 110 | end 111 | 112 | if start_row > line then line = start_row end 113 | end 114 | end) 115 | end 116 | 117 | --- Combines adjacent paragraph lines together 118 | --- @param lines string[] 119 | --- @return string[] 120 | --- TODO: Likely buggy 121 | function docs.combine_markdown_lines(lines) 122 | local combined_lines = {} 123 | 124 | local special_starting_chars = { '#', '>', '-', '|', '*', '•' } 125 | local in_code_block = false 126 | local prev_is_special = false 127 | for _, line in ipairs(lines) do 128 | if line:match('^%s*```') then in_code_block = not in_code_block end 129 | -- skip separators 130 | if line:match('^[%s\\-]+$') then goto continue end 131 | 132 | local is_special = line:match('^%s*[' .. table.concat(special_starting_chars) .. ']') or line:match('^%s*%d\\.$') 133 | local is_empty = line:match('^%s*$') 134 | local has_linebreak = line:match('%s%s$') 135 | 136 | if #combined_lines == 0 or in_code_block or is_special or prev_is_special or is_empty or has_linebreak then 137 | table.insert(combined_lines, line) 138 | elseif line:match('^%s*$') then 139 | if combined_lines[#combined_lines] ~= '' then table.insert(combined_lines, '') end 140 | else 141 | combined_lines[#combined_lines] = combined_lines[#combined_lines] .. '' .. line 142 | end 143 | 144 | prev_is_special = is_special 145 | ::continue:: 146 | end 147 | 148 | return combined_lines 149 | end 150 | 151 | --- Gets the start and end row of the code block for the given row 152 | --- Or returns nil if there's no code block 153 | --- @param lines string[] 154 | --- @param row number 155 | --- @return number?, number? 156 | function docs.get_code_block_range(lines, row) 157 | -- get the start of the code block 158 | local code_block_start = nil 159 | for i = 1, row do 160 | local line = lines[i] 161 | if line:match('^%s*```') then 162 | if code_block_start == nil then 163 | code_block_start = i 164 | else 165 | code_block_start = nil 166 | end 167 | end 168 | end 169 | if code_block_start == nil then return end 170 | 171 | -- get the end of the code block 172 | local code_block_end = nil 173 | for i = row, #lines do 174 | local line = lines[i] 175 | if line:match('^%s*```') then 176 | code_block_end = i 177 | break 178 | end 179 | end 180 | if code_block_end == nil then return end 181 | 182 | return code_block_start, code_block_end 183 | end 184 | 185 | --- Avoids showing the detail if it's part of the documentation 186 | --- or, if the detail is in a code block in the doc, 187 | --- extracts the code block into the detail 188 | ---@param detail_lines string[] 189 | ---@param doc_lines string[] 190 | ---@return string[], string[] 191 | --- TODO: Also move the code block into detail if it's at the start of the doc 192 | --- and we have no detail 193 | function docs.extract_detail_from_doc(detail_lines, doc_lines) 194 | local detail_str = table.concat(detail_lines, '\n') 195 | local doc_str = table.concat(doc_lines, '\n') 196 | local doc_str_detail_row = doc_str:find(detail_str, 1, true) 197 | 198 | -- didn't find the detail in the doc, so return as is 199 | if doc_str_detail_row == nil or #detail_str == 0 or #doc_str == 0 then 200 | return detail_lines, doc_lines 201 | end 202 | 203 | -- get the line of the match 204 | -- hack: surely there's a better way to do this but it's late 205 | -- and I can't be bothered 206 | local offset = 1 207 | local detail_line = 1 208 | for line_num, line in ipairs(doc_lines) do 209 | if #line + offset > doc_str_detail_row then 210 | detail_line = line_num 211 | break 212 | end 213 | offset = offset + #line + 1 214 | end 215 | 216 | -- extract the code block, if it exists, and use it as the detail 217 | local code_block_start, code_block_end = docs.get_code_block_range(doc_lines, detail_line) 218 | if code_block_start ~= nil and code_block_end ~= nil then 219 | detail_lines = vim.list_slice(doc_lines, code_block_start + 1, code_block_end - 1) 220 | 221 | local doc_lines_start = vim.list_slice(doc_lines, 1, code_block_start - 1) 222 | local doc_lines_end = vim.list_slice(doc_lines, code_block_end + 1, #doc_lines) 223 | vim.list_extend(doc_lines_start, doc_lines_end) 224 | doc_lines = doc_lines_start 225 | else 226 | detail_lines = {} 227 | end 228 | 229 | return detail_lines, doc_lines 230 | end 231 | 232 | function docs.split_lines(text) 233 | local lines = {} 234 | for s in text:gmatch('[^\r\n]+') do 235 | table.insert(lines, s) 236 | end 237 | return lines 238 | end 239 | 240 | return docs 241 | -------------------------------------------------------------------------------- /lua/blink/cmp/keymap.lua: -------------------------------------------------------------------------------- 1 | local utils = require('blink.cmp.utils') 2 | local keymap = {} 3 | 4 | local default_keymap = { 5 | [''] = { 'show', 'show_documentation', 'hide_documentation' }, 6 | [''] = { 'hide' }, 7 | [''] = { 'select_and_accept' }, 8 | 9 | [''] = { 'select_prev', 'fallback' }, 10 | [''] = { 'select_next', 'fallback' }, 11 | [''] = { 'select_prev', 'fallback' }, 12 | [''] = { 'select_next', 'fallback' }, 13 | 14 | [''] = { 'scroll_documentation_up', 'fallback' }, 15 | [''] = { 'scroll_documentation_down', 'fallback' }, 16 | 17 | [''] = { 'snippet_forward', 'fallback' }, 18 | [''] = { 'snippet_backward', 'fallback' }, 19 | } 20 | 21 | local super_tab_keymap = { 22 | [''] = { 'show', 'show_documentation', 'hide_documentation' }, 23 | [''] = { 'hide' }, 24 | 25 | [''] = { 26 | function(cmp) 27 | if cmp.is_in_snippet() then 28 | return cmp.accept() 29 | else 30 | return cmp.select_and_accept() 31 | end 32 | end, 33 | 'snippet_forward', 34 | 'fallback', 35 | }, 36 | [''] = { 'snippet_backward', 'fallback' }, 37 | 38 | [''] = { 'select_prev', 'fallback' }, 39 | [''] = { 'select_next', 'fallback' }, 40 | [''] = { 'select_prev', 'fallback' }, 41 | [''] = { 'select_next', 'fallback' }, 42 | 43 | [''] = { 'scroll_documentation_up', 'fallback' }, 44 | [''] = { 'scroll_documentation_down', 'fallback' }, 45 | } 46 | 47 | local enter_keymap = { 48 | [''] = { 'show', 'show_documentation', 'hide_documentation' }, 49 | [''] = { 'hide' }, 50 | [''] = { 'accept', 'fallback' }, 51 | 52 | [''] = { 'snippet_forward', 'fallback' }, 53 | [''] = { 'snippet_backward', 'fallback' }, 54 | 55 | [''] = { 'select_prev', 'fallback' }, 56 | [''] = { 'select_next', 'fallback' }, 57 | [''] = { 'select_prev', 'fallback' }, 58 | [''] = { 'select_next', 'fallback' }, 59 | 60 | [''] = { 'scroll_documentation_up', 'fallback' }, 61 | [''] = { 'scroll_documentation_down', 'fallback' }, 62 | } 63 | 64 | local snippet_commands = { 'snippet_forward', 'snippet_backward' } 65 | 66 | --- @param opts blink.cmp.KeymapConfig 67 | function keymap.setup(opts) 68 | local mappings = opts 69 | 70 | -- notice for users on old config 71 | if type(opts) == 'table' then 72 | local commands = { 73 | 'show', 74 | 'hide', 75 | 'accept', 76 | 'select_and_accept', 77 | 'select_prev', 78 | 'select_next', 79 | 'show_documentation', 80 | 'hide_documentation', 81 | 'scroll_documentation_up', 82 | 'scroll_documentation_down', 83 | 'snippet_forward', 84 | 'snippet_backward', 85 | } 86 | for key, _ in pairs(opts) do 87 | if vim.tbl_contains(commands, key) then 88 | error('The blink.cmp keymap recently got reworked. Please see the README for the updated configuration') 89 | end 90 | end 91 | 92 | -- Handle preset inside table 93 | if opts.preset then 94 | local preset_keymap = keymap.get_preset_keymap(opts.preset) 95 | 96 | -- Remove 'preset' key from opts to prevent it from being treated as a keymap 97 | opts.preset = nil 98 | -- Merge the preset keymap with the user-defined keymaps 99 | -- User-defined keymaps overwrite the preset keymaps 100 | mappings = vim.tbl_extend('force', preset_keymap, opts) 101 | end 102 | end 103 | 104 | -- handle presets 105 | if type(opts) == 'string' then mappings = keymap.get_preset_keymap(opts) end 106 | 107 | -- we set on the buffer directly to avoid buffer-local keymaps (such as from autopairs) 108 | -- from overriding our mappings. We also use InsertEnter to avoid conflicts with keymaps 109 | -- applied on other autocmds, such as LspAttach used by nvim-lspconfig and most configs 110 | vim.api.nvim_create_autocmd('InsertEnter', { 111 | callback = function() 112 | if utils.is_blocked_buffer() then return end 113 | keymap.apply_keymap_to_current_buffer(mappings) 114 | end, 115 | }) 116 | 117 | -- This is not called when the plugin loads since it first checks if the binary is 118 | -- installed. As a result, when lazy-loaded on InsertEnter, the event may be missed 119 | if vim.api.nvim_get_mode().mode == 'i' and not utils.is_blocked_buffer() then 120 | keymap.apply_keymap_to_current_buffer(mappings) 121 | end 122 | end 123 | 124 | --- Gets the preset keymap for the given preset name 125 | --- @param preset_name string 126 | --- @return table 127 | function keymap.get_preset_keymap(preset_name) 128 | if preset_name == 'default' then 129 | return default_keymap 130 | elseif preset_name == 'super-tab' then 131 | return super_tab_keymap 132 | elseif preset_name == 'enter' then 133 | return enter_keymap 134 | end 135 | 136 | error('Invalid blink.cmp keymap preset: ' .. preset_name) 137 | end 138 | 139 | --- Applies the keymaps to the current buffer 140 | --- @param keys_to_commands table 141 | function keymap.apply_keymap_to_current_buffer(keys_to_commands) 142 | -- skip if we've already applied the keymaps 143 | for _, mapping in ipairs(vim.api.nvim_buf_get_keymap(0, 'i')) do 144 | if mapping.desc == 'blink.cmp' then return end 145 | end 146 | 147 | -- insert mode: uses both snippet and insert commands 148 | for key, commands in pairs(keys_to_commands) do 149 | keymap.set('i', key, function() 150 | for _, command in ipairs(commands) do 151 | -- special case for fallback 152 | if command == 'fallback' then 153 | return keymap.run_non_blink_keymap('i', key) 154 | 155 | -- run user defined functions 156 | elseif type(command) == 'function' then 157 | if command(require('blink.cmp')) then return end 158 | 159 | -- otherwise, run the built-in command 160 | elseif require('blink.cmp')[command]() then 161 | return 162 | end 163 | end 164 | end) 165 | end 166 | 167 | -- snippet mode 168 | for key, commands in pairs(keys_to_commands) do 169 | local has_snippet_command = false 170 | for _, command in ipairs(commands) do 171 | if vim.tbl_contains(snippet_commands, command) then has_snippet_command = true end 172 | end 173 | 174 | if has_snippet_command then 175 | keymap.set('s', key, function() 176 | for _, command in ipairs(keys_to_commands[key] or {}) do 177 | -- special case for fallback 178 | if command == 'fallback' then 179 | return keymap.run_non_blink_keymap('s', key) 180 | 181 | -- run user defined functions 182 | elseif type(command) == 'function' then 183 | if command(require('blink.cmp')) then return end 184 | 185 | -- only run snippet commands 186 | elseif vim.tbl_contains(snippet_commands, command) then 187 | local did_run = require('blink.cmp')[command]() 188 | if did_run then return end 189 | end 190 | end 191 | end) 192 | end 193 | end 194 | end 195 | 196 | --- Gets the first non blink.cmp keymap for the given mode and key 197 | --- @param mode string 198 | --- @param key string 199 | --- @return vim.api.keyset.keymap | nil 200 | function keymap.get_non_blink_mapping_for_key(mode, key) 201 | local normalized_key = vim.api.nvim_replace_termcodes(key, true, true, true) 202 | 203 | -- get buffer local and global mappings 204 | local mappings = vim.api.nvim_buf_get_keymap(0, mode) 205 | vim.list_extend(mappings, vim.api.nvim_get_keymap(mode)) 206 | 207 | for _, mapping in ipairs(mappings) do 208 | local mapping_key = vim.api.nvim_replace_termcodes(mapping.lhs, true, true, true) 209 | if mapping_key == normalized_key and mapping.desc ~= 'blink.cmp' then return mapping end 210 | end 211 | end 212 | 213 | --- Runs the first non blink.cmp keymap for the given mode and key 214 | --- @param mode string 215 | --- @param key string 216 | --- @return string | nil 217 | function keymap.run_non_blink_keymap(mode, key) 218 | local mapping = keymap.get_non_blink_mapping_for_key(mode, key) or {} 219 | 220 | -- TODO: there's likely many edge cases here. the nvim-cmp version is lacking documentation 221 | -- and is quite complex. we should look to see if we can simplify their logic 222 | -- https://github.com/hrsh7th/nvim-cmp/blob/ae644feb7b67bf1ce4260c231d1d4300b19c6f30/lua/cmp/utils/keymap.lua 223 | if type(mapping.callback) == 'function' then 224 | -- with expr = true, which we use, we can't modify the buffer without scheduling 225 | -- so if the keymap does not use expr, we must schedule it 226 | if mapping.expr ~= 1 then 227 | vim.schedule(mapping.callback) 228 | return 229 | end 230 | 231 | local expr = mapping.callback() 232 | if mapping.replace_keycodes == 1 then expr = vim.api.nvim_replace_termcodes(expr, true, true, true) end 233 | return expr 234 | elseif mapping.rhs then 235 | local rhs = vim.api.nvim_replace_termcodes(mapping.rhs, true, true, true) 236 | if mapping.expr == 1 then rhs = vim.api.nvim_eval(rhs) end 237 | return rhs 238 | end 239 | 240 | -- pass the key along as usual 241 | return vim.api.nvim_replace_termcodes(key, true, true, true) 242 | end 243 | 244 | --- @param mode string 245 | --- @param key string 246 | --- @param callback fun(): string | nil 247 | function keymap.set(mode, key, callback) 248 | vim.api.nvim_buf_set_keymap(0, mode, key, '', { 249 | callback = callback, 250 | expr = true, 251 | silent = true, 252 | noremap = true, 253 | replace_keycodes = false, 254 | desc = 'blink.cmp', 255 | }) 256 | end 257 | 258 | return keymap 259 | -------------------------------------------------------------------------------- /lua/blink/cmp/sources/lsp.lua: -------------------------------------------------------------------------------- 1 | --- @class blink.cmp.Source 2 | local lsp = {} 3 | 4 | function lsp.new() return setmetatable({}, { __index = lsp }) end 5 | 6 | ---@param capability string|table|nil Server capability (possibly nested 7 | --- supplied via table) to check. 8 | --- 9 | ---@return boolean Whether at least one LSP client supports `capability`. 10 | ---@private 11 | function lsp:has_capability(capability) 12 | for _, client in pairs(vim.lsp.get_clients({ bufnr = 0 })) do 13 | local has_capability = client.server_capabilities[capability] 14 | if has_capability then return true end 15 | end 16 | return false 17 | end 18 | 19 | --- @param method string 20 | --- @return boolean Whether at least one LSP client supports `method` 21 | --- @private 22 | function lsp:has_method(method) return #vim.lsp.get_clients({ bufnr = 0, method = method }) > 0 end 23 | 24 | --- Completion --- 25 | 26 | function lsp:get_trigger_characters() 27 | local clients = vim.lsp.get_clients({ bufnr = 0 }) 28 | local trigger_characters = {} 29 | 30 | for _, client in pairs(clients) do 31 | local completion_provider = client.server_capabilities.completionProvider 32 | if completion_provider and completion_provider.triggerCharacters then 33 | for _, trigger_character in pairs(completion_provider.triggerCharacters) do 34 | table.insert(trigger_characters, trigger_character) 35 | end 36 | end 37 | end 38 | 39 | return trigger_characters 40 | end 41 | 42 | function lsp:get_clients_with_capability(capability, filter) 43 | local clients = {} 44 | for _, client in pairs(vim.lsp.get_clients(filter)) do 45 | local capabilities = client.server_capabilities or {} 46 | if capabilities[capability] then table.insert(clients, client) end 47 | end 48 | return clients 49 | end 50 | 51 | function lsp:get_completions(context, callback) 52 | -- TODO: offset encoding is global but should be per-client 53 | -- TODO: should make separate LSP requests to return results earlier, in the case of slow LSPs 54 | 55 | -- no providers with completion support 56 | if not self:has_capability('completionProvider') or not self:has_method('textDocument/completion') then 57 | callback({ is_incomplete_forward = false, is_incomplete_backward = false, items = {} }) 58 | return function() end 59 | end 60 | 61 | -- completion context with additional info about how it was triggered 62 | local params = vim.lsp.util.make_position_params() 63 | params.context = { 64 | triggerKind = context.trigger.kind, 65 | } 66 | if context.trigger.kind == vim.lsp.protocol.CompletionTriggerKind.TriggerCharacter then 67 | params.context.triggerCharacter = context.trigger.character 68 | end 69 | 70 | -- special case, the first character of the context is a trigger character, so we adjust the position 71 | -- sent to the LSP server to be the start of the trigger character 72 | -- 73 | -- some LSP do their own filtering before returning results, which we want to avoid 74 | -- since we perform fuzzy matching ourselves. 75 | -- 76 | -- this also avoids having to make multiple calls to the LSP server in case characters are deleted 77 | -- for these special cases 78 | -- i.e. hello.wor| would be sent as hello.|wor 79 | -- TODO: should we still make two calls to the LSP server and merge? 80 | -- TODO: breaks the textEdit resolver since it assumes the request was made from the cursor 81 | -- local trigger_characters = self:get_trigger_characters() 82 | -- local trigger_character_block_list = { ' ', '\n', '\t' } 83 | -- local bounds = context.bounds 84 | -- local trigger_character_before_context = context.line:sub(bounds.start_col - 1, bounds.start_col - 1) 85 | -- if 86 | -- vim.tbl_contains(trigger_characters, trigger_character_before_context) 87 | -- and not vim.tbl_contains(trigger_character_block_list, trigger_character_before_context) 88 | -- then 89 | -- local offset_encoding = vim.lsp.get_clients({ bufnr = 0 })[1].offset_encoding 90 | -- params.position.character = 91 | -- vim.lsp.util.character_offset(0, params.position.line, bounds.start_col - 1, offset_encoding) 92 | -- end 93 | 94 | -- request from each of the clients 95 | -- todo: refactor 96 | return vim.lsp.buf_request_all(0, 'textDocument/completion', params, function(result) 97 | local responses = {} 98 | for client_id, response in pairs(result) do 99 | -- todo: pass error upstream 100 | if response.error or response.result == nil then 101 | responses[client_id] = { is_incomplete_forward = true, is_incomplete_backward = true, items = {} } 102 | 103 | -- as per the spec, we assume it's complete if we get CompletionItem[] 104 | elseif response.result.items == nil then 105 | responses[client_id] = { 106 | is_incomplete_forward = false, 107 | is_incomplete_backward = true, 108 | items = response.result, 109 | } 110 | 111 | -- convert full response to our internal format 112 | else 113 | local defaults = response.result and response.result.itemDefaults or {} 114 | for _, item in ipairs(response.result.items) do 115 | -- add defaults to the item 116 | for key, value in pairs(defaults) do 117 | item[key] = value 118 | end 119 | end 120 | 121 | responses[client_id] = { 122 | is_incomplete_forward = response.result.isIncomplete, 123 | is_incomplete_backward = true, 124 | items = response.result.items, 125 | } 126 | end 127 | end 128 | 129 | -- add client_id and defaults to the items 130 | for client_id, response in pairs(responses) do 131 | for _, item in ipairs(response.items) do 132 | -- todo: terraform lsp doesn't return a .kind in situations like `toset`, is there a default value we need to grab? 133 | -- it doesn't seem to return itemDefaults either 134 | item.kind = item.kind or require('blink.cmp.types').CompletionItemKind.Text 135 | item.client_id = client_id 136 | 137 | -- todo: make configurable 138 | if item.deprecated or (item.tags and vim.tbl_contains(item.tags, 1)) then item.score_offset = -2 end 139 | end 140 | end 141 | 142 | -- combine responses 143 | -- todo: ideally pass multiple responses to the sources 144 | -- so that we can do fine-grained isIncomplete 145 | -- or do caching here 146 | local combined_response = { is_incomplete_forward = false, is_incomplete_backward = false, items = {} } 147 | for _, response in pairs(responses) do 148 | combined_response.is_incomplete_forward = combined_response.is_incomplete_forward 149 | or response.is_incomplete_forward 150 | combined_response.is_incomplete_backward = combined_response.is_incomplete_backward 151 | or response.is_incomplete_backward 152 | vim.list_extend(combined_response.items, response.items) 153 | end 154 | 155 | callback(combined_response) 156 | end) 157 | end 158 | 159 | --- Resolve --- 160 | 161 | function lsp:resolve(item, callback) 162 | local client = vim.lsp.get_client_by_id(item.client_id) 163 | if client == nil or not client.server_capabilities.completionProvider.resolveProvider then 164 | callback(item) 165 | return 166 | end 167 | 168 | -- strip blink specific fields to avoid decoding errors on some LSPs 169 | item = require('blink.cmp.sources.lib.utils').blink_item_to_lsp_item(item) 170 | 171 | local success, request_id = client.request('completionItem/resolve', item, function(error, resolved_item) 172 | if error or resolved_item == nil then callback(item) end 173 | callback(resolved_item) 174 | end) 175 | if not success then callback(item) end 176 | if request_id ~= nil then return function() client.cancel_request(request_id) end end 177 | end 178 | 179 | --- Signature help --- 180 | 181 | function lsp:get_signature_help_trigger_characters() 182 | local clients = vim.lsp.get_clients({ bufnr = 0 }) 183 | local trigger_characters = {} 184 | local retrigger_characters = {} 185 | 186 | for _, client in pairs(clients) do 187 | local signature_help_provider = client.server_capabilities.signatureHelpProvider 188 | if signature_help_provider and signature_help_provider.triggerCharacters then 189 | for _, trigger_character in pairs(signature_help_provider.triggerCharacters) do 190 | table.insert(trigger_characters, trigger_character) 191 | end 192 | end 193 | if signature_help_provider and signature_help_provider.retriggerCharacters then 194 | for _, retrigger_character in pairs(signature_help_provider.retriggerCharacters) do 195 | table.insert(retrigger_characters, retrigger_character) 196 | end 197 | end 198 | end 199 | 200 | return { trigger_characters = trigger_characters, retrigger_characters = retrigger_characters } 201 | end 202 | 203 | function lsp:get_signature_help(context, callback) 204 | -- no providers with signature help support 205 | if not self:has_method('textDocument/signatureHelp') then 206 | callback(nil) 207 | return function() end 208 | end 209 | 210 | local params = vim.lsp.util.make_position_params() 211 | params.context = { 212 | triggerKind = context.trigger.kind, 213 | triggerCharacter = context.trigger.character, 214 | isRetrigger = context.is_retrigger, 215 | activeSignatureHelp = context.active_signature_help, 216 | } 217 | 218 | -- otherwise, we call all clients 219 | -- TODO: some LSPs never response (typescript-tools.nvim) 220 | return vim.lsp.buf_request_all(0, 'textDocument/signatureHelp', params, function(result) 221 | local signature_helps = {} 222 | for client_id, res in pairs(result) do 223 | local signature_help = res.result 224 | if signature_help ~= nil then 225 | signature_help.client_id = client_id 226 | table.insert(signature_helps, signature_help) 227 | end 228 | end 229 | -- todo: pick intelligently 230 | callback(signature_helps[1]) 231 | end) 232 | end 233 | 234 | return lsp 235 | -------------------------------------------------------------------------------- /lua/blink/cmp/sources/lib/init.lua: -------------------------------------------------------------------------------- 1 | local async = require('blink.cmp.sources.lib.async') 2 | local config = require('blink.cmp.config') 3 | 4 | --- @class blink.cmp.Sources 5 | --- @field current_context blink.cmp.SourcesContext | nil 6 | --- @field current_signature_help blink.cmp.Task | nil 7 | --- @field sources_registered boolean 8 | --- @field providers table 9 | --- @field on_completions_callback fun(context: blink.cmp.Context, enabled_sources: table, responses: table) 10 | --- 11 | --- @field register fun() 12 | --- @field get_enabled_providers fun(context?: blink.cmp.Context): table 13 | --- @field get_trigger_characters fun(): string[] 14 | --- @field request_completions fun(context: blink.cmp.Context) 15 | --- @field cancel_completions fun() 16 | --- @field listen_on_completions fun(callback: fun(context: blink.cmp.Context, items: blink.cmp.CompletionItem[])) 17 | --- @field apply_max_items_for_completions fun(context: blink.cmp.Context, items: blink.cmp.CompletionItem[]): blink.cmp.CompletionItem[] 18 | --- @field resolve fun(item: blink.cmp.CompletionItem): blink.cmp.Task 19 | --- @field get_signature_help_trigger_characters fun(): { trigger_characters: string[], retrigger_characters: string[] } 20 | --- @field get_signature_help fun(context: blink.cmp.SignatureHelpContext, callback: fun(signature_help: lsp.SignatureHelp | nil)): (fun(): nil) | nil 21 | --- @field cancel_signature_help fun() 22 | --- @field reload fun() 23 | --- @field get_lsp_capabilities fun(override?: lsp.ClientCapabilities, include_nvim_defaults?: boolean): lsp.ClientCapabilities 24 | 25 | --- @type blink.cmp.Sources 26 | --- @diagnostic disable-next-line: missing-fields 27 | local sources = { 28 | current_context = nil, 29 | sources_registered = false, 30 | providers = {}, 31 | on_completions_callback = function(_, _) end, 32 | } 33 | 34 | function sources.register() 35 | assert(not sources.sources_registered, 'Sources have already been registered') 36 | sources.sources_registered = true 37 | 38 | for key, source_config in pairs(config.sources.providers) do 39 | sources.providers[key] = require('blink.cmp.sources.lib.provider').new(key, source_config) 40 | end 41 | end 42 | 43 | function sources.get_enabled_providers(context) 44 | local mode_providers = type(config.sources.completion.enabled_providers) == 'function' 45 | and config.sources.completion.enabled_providers(context) 46 | or config.sources.completion.enabled_providers 47 | --- @cast mode_providers string[] 48 | 49 | for _, provider in ipairs(mode_providers) do 50 | assert( 51 | sources.providers[provider] ~= nil, 52 | 'Requested provider "' 53 | .. provider 54 | .. '" has not been configured. Available providers: ' 55 | .. vim.fn.join(vim.tbl_keys(sources.providers), ', ') 56 | ) 57 | end 58 | 59 | --- @type table 60 | local providers = {} 61 | for key, provider in pairs(sources.providers) do 62 | if provider:enabled(context) and vim.tbl_contains(mode_providers, key) then providers[key] = provider end 63 | end 64 | return providers 65 | end 66 | 67 | --- Completion --- 68 | 69 | function sources.get_trigger_characters() 70 | local providers = sources.get_enabled_providers() 71 | local blocked_trigger_characters = {} 72 | for _, char in ipairs(config.trigger.completion.blocked_trigger_characters) do 73 | blocked_trigger_characters[char] = true 74 | end 75 | 76 | local trigger_characters = {} 77 | for _, source in pairs(providers) do 78 | local source_trigger_characters = source:get_trigger_characters() 79 | for _, char in ipairs(source_trigger_characters) do 80 | if not blocked_trigger_characters[char] then table.insert(trigger_characters, char) end 81 | end 82 | end 83 | return trigger_characters 84 | end 85 | 86 | function sources.listen_on_completions(callback) 87 | sources.on_completions_callback = function(context, enabled_sources, responses) 88 | local items = {} 89 | for id, response in pairs(responses) do 90 | if sources.providers[id]:should_show_items(context, enabled_sources, response.items) then 91 | vim.list_extend(items, response.items) 92 | end 93 | end 94 | callback(context, items) 95 | end 96 | end 97 | 98 | function sources.request_completions(context) 99 | -- create a new context if the id changed or if we haven't created one yet 100 | local is_new_context = sources.current_context == nil or context.id ~= sources.current_context.id 101 | if is_new_context then 102 | if sources.current_context ~= nil then sources.current_context:destroy() end 103 | sources.current_context = require('blink.cmp.sources.lib.context').new( 104 | context, 105 | sources.get_enabled_providers(context), 106 | sources.on_completions_callback 107 | ) 108 | -- send cached completions if they exist to immediately trigger updates 109 | elseif sources.current_context:get_cached_completions() ~= nil then 110 | sources.on_completions_callback( 111 | context, 112 | sources.current_context:get_sources(), 113 | sources.current_context:get_cached_completions() 114 | ) 115 | end 116 | 117 | sources.current_context:get_completions(context) 118 | end 119 | 120 | function sources.cancel_completions() 121 | if sources.current_context ~= nil then 122 | sources.current_context:destroy() 123 | sources.current_context = nil 124 | end 125 | end 126 | 127 | --- Limits the number of items per source as configured 128 | function sources.apply_max_items_for_completions(context, items) 129 | local enabled_sources = sources.get_enabled_providers(context) 130 | 131 | -- get the configured max items for each source 132 | local total_items_for_sources = {} 133 | local max_items_for_sources = {} 134 | for id, source in pairs(sources.providers) do 135 | max_items_for_sources[id] = source.config.max_items(context, enabled_sources, items) 136 | total_items_for_sources[id] = 0 137 | end 138 | 139 | -- no max items configured, return as-is 140 | if #vim.tbl_keys(max_items_for_sources) == 0 then return items end 141 | 142 | -- apply max items 143 | local filtered_items = {} 144 | for _, item in ipairs(items) do 145 | local max_items = max_items_for_sources[item.source_id] 146 | total_items_for_sources[item.source_id] = total_items_for_sources[item.source_id] + 1 147 | if max_items == nil or total_items_for_sources[item.source_id] <= max_items then 148 | table.insert(filtered_items, item) 149 | end 150 | end 151 | return filtered_items 152 | end 153 | 154 | --- Resolve --- 155 | 156 | function sources.resolve(item) 157 | --- @type blink.cmp.SourceProvider? 158 | local item_source = nil 159 | for _, source in pairs(sources.providers) do 160 | if source.id == item.source_id then 161 | item_source = source 162 | break 163 | end 164 | end 165 | if item_source == nil then return async.task.new(function(resolve) resolve(item) end) end 166 | 167 | return item_source:resolve(item):catch(function(err) vim.print('failed to resolve item with error: ' .. err) end) 168 | end 169 | 170 | --- Signature help --- 171 | 172 | function sources.get_signature_help_trigger_characters() 173 | local blocked_trigger_characters = {} 174 | local blocked_retrigger_characters = {} 175 | for _, char in ipairs(config.trigger.signature_help.blocked_trigger_characters) do 176 | blocked_trigger_characters[char] = true 177 | end 178 | for _, char in ipairs(config.trigger.signature_help.blocked_retrigger_characters) do 179 | blocked_retrigger_characters[char] = true 180 | end 181 | 182 | local trigger_characters = {} 183 | local retrigger_characters = {} 184 | 185 | -- todo: should this be all source groups? 186 | for _, source in pairs(sources.providers) do 187 | local res = source:get_signature_help_trigger_characters() 188 | for _, char in ipairs(res.trigger_characters) do 189 | if not blocked_trigger_characters[char] then table.insert(trigger_characters, char) end 190 | end 191 | for _, char in ipairs(res.retrigger_characters) do 192 | if not blocked_retrigger_characters[char] then table.insert(retrigger_characters, char) end 193 | end 194 | end 195 | return { trigger_characters = trigger_characters, retrigger_characters = retrigger_characters } 196 | end 197 | 198 | function sources.get_signature_help(context, callback) 199 | local tasks = {} 200 | for _, source in pairs(sources.providers) do 201 | table.insert(tasks, source:get_signature_help(context)) 202 | end 203 | sources.current_signature_help = async.task.await_all(tasks):map(function(tasks_results) 204 | local signature_helps = {} 205 | for _, task_result in ipairs(tasks_results) do 206 | if task_result.status == async.STATUS.COMPLETED and task_result.result ~= nil then 207 | table.insert(signature_helps, task_result.result) 208 | end 209 | end 210 | callback(signature_helps[1]) 211 | end) 212 | end 213 | 214 | function sources.cancel_signature_help() 215 | if sources.current_signature_help ~= nil then 216 | sources.current_signature_help:cancel() 217 | sources.current_signature_help = nil 218 | end 219 | end 220 | 221 | --- Misc --- 222 | 223 | --- For external integrations to force reloading the source 224 | function sources.reload() 225 | for _, source in pairs(sources.providers) do 226 | source:reload() 227 | end 228 | end 229 | 230 | function sources.get_lsp_capabilities(override, include_nvim_defaults) 231 | return vim.tbl_deep_extend('force', include_nvim_defaults and vim.lsp.protocol.make_client_capabilities() or {}, { 232 | textDocument = { 233 | completion = { 234 | completionItem = { 235 | snippetSupport = true, 236 | commitCharactersSupport = false, -- todo: 237 | documentationFormat = { 'markdown', 'plaintext' }, 238 | deprecatedSupport = true, 239 | preselectSupport = false, -- todo: 240 | tagSupport = { valueSet = { 1 } }, -- deprecated 241 | insertReplaceSupport = true, -- todo: 242 | resolveSupport = { 243 | properties = { 244 | 'documentation', 245 | 'detail', 246 | 'additionalTextEdits', 247 | -- todo: support more properties? should test if it improves latency 248 | }, 249 | }, 250 | insertTextModeSupport = { 251 | -- todo: support adjustIndentation 252 | valueSet = { 1 }, -- asIs 253 | }, 254 | labelDetailsSupport = true, 255 | }, 256 | completionList = { 257 | itemDefaults = { 258 | 'commitCharacters', 259 | 'editRange', 260 | 'insertTextFormat', 261 | 'insertTextMode', 262 | 'data', 263 | }, 264 | }, 265 | 266 | contextSupport = true, 267 | insertTextMode = 1, -- asIs 268 | }, 269 | }, 270 | }, override or {}) 271 | end 272 | 273 | return sources 274 | -------------------------------------------------------------------------------- /lua/blink/cmp/trigger/completion.lua: -------------------------------------------------------------------------------- 1 | -- Handles hiding and showing the completion window. When a user types a trigger character 2 | -- (provided by the sources) or anything matching the `keyword_regex`, we create a new `context`. 3 | -- This can be used downstream to determine if we should make new requests to the sources or not. 4 | 5 | local config = require('blink.cmp.config').trigger.completion 6 | local sources = require('blink.cmp.sources.lib') 7 | local utils = require('blink.cmp.utils') 8 | 9 | local trigger = { 10 | current_context_id = -1, 11 | --- @type blink.cmp.Context | nil 12 | context = nil, 13 | event_targets = { 14 | --- @type fun(context: blink.cmp.Context) 15 | on_show = function() end, 16 | --- @type fun() 17 | on_hide = function() end, 18 | }, 19 | } 20 | 21 | --- TODO: sweet mother of mary, massive refactor needed 22 | function trigger.activate_autocmds() 23 | local last_char = '' 24 | vim.api.nvim_create_autocmd('InsertCharPre', { 25 | callback = function() 26 | if vim.snippet.active() and not config.show_in_snippet and not trigger.context then return end 27 | last_char = vim.v.char 28 | end, 29 | }) 30 | 31 | -- decide if we should show the completion window 32 | vim.api.nvim_create_autocmd('TextChangedI', { 33 | callback = function() 34 | if vim.snippet.active() and not config.show_in_snippet and not trigger.context then 35 | return 36 | 37 | -- we were told to ignore the text changed event, so we update the context 38 | -- but don't send an on_show event upstream 39 | elseif trigger.ignore_next_text_changed then 40 | if trigger.context ~= nil then trigger.show({ send_upstream = false }) end 41 | trigger.ignore_next_text_changed = false 42 | 43 | -- no characters added so let cursormoved handle it 44 | elseif last_char == '' then 45 | return 46 | 47 | -- ignore if in a special buffer 48 | elseif utils.is_blocked_buffer() then 49 | trigger.hide() 50 | 51 | -- character forces a trigger according to the sources, create a fresh context 52 | elseif vim.tbl_contains(sources.get_trigger_characters(), last_char) then 53 | trigger.context = nil 54 | trigger.show({ trigger_character = last_char }) 55 | 56 | -- character is part of the current context OR in an existing context 57 | elseif last_char:match(config.keyword_regex) ~= nil then 58 | trigger.show() 59 | 60 | -- nothing matches so hide 61 | else 62 | trigger.hide() 63 | end 64 | 65 | last_char = '' 66 | end, 67 | }) 68 | 69 | vim.api.nvim_create_autocmd({ 'CursorMovedI', 'InsertEnter' }, { 70 | callback = function(ev) 71 | if utils.is_blocked_buffer() then return end 72 | if vim.snippet.active() and not config.show_in_snippet and not trigger.context then return end 73 | 74 | -- we were told to ignore the cursor moved event, so we update the context 75 | -- but don't send an on_show event upstream 76 | if trigger.ignore_next_cursor_moved and ev.event == 'CursorMovedI' then 77 | if trigger.context ~= nil then trigger.show({ send_upstream = false }) end 78 | trigger.ignore_next_cursor_moved = false 79 | return 80 | end 81 | 82 | -- characters added so let textchanged handle it 83 | if last_char ~= '' then return end 84 | 85 | local cursor_col = vim.api.nvim_win_get_cursor(0)[2] 86 | local char_under_cursor = vim.api.nvim_get_current_line():sub(cursor_col, cursor_col) 87 | local is_on_trigger = vim.tbl_contains(sources.get_trigger_characters(), char_under_cursor) 88 | local is_on_trigger_for_show_on_insert = is_on_trigger 89 | and not vim.tbl_contains(config.show_on_x_blocked_trigger_characters, char_under_cursor) 90 | local is_on_context_char = char_under_cursor:match(config.keyword_regex) ~= nil 91 | 92 | local insert_enter_on_trigger_character = config.show_on_insert_on_trigger_character 93 | and is_on_trigger_for_show_on_insert 94 | and ev.event == 'InsertEnter' 95 | 96 | -- check if we're still within the bounds of the query used for the context 97 | if trigger.within_query_bounds(vim.api.nvim_win_get_cursor(0)) then 98 | trigger.show() 99 | 100 | -- check if we've entered insert mode on a trigger character 101 | -- or if we've moved onto a trigger character 102 | elseif insert_enter_on_trigger_character or (is_on_trigger and trigger.context ~= nil) then 103 | trigger.context = nil 104 | trigger.show({ trigger_character = char_under_cursor }) 105 | 106 | -- show if we currently have a context, and we've moved outside of it's bounds by 1 char 107 | elseif is_on_context_char and trigger.context ~= nil and cursor_col == trigger.context.bounds.start_col - 1 then 108 | trigger.context = nil 109 | trigger.show() 110 | 111 | -- otherwise hide 112 | else 113 | trigger.hide() 114 | end 115 | end, 116 | }) 117 | 118 | -- definitely leaving the context 119 | -- TODO: handle leaving snippet mode 120 | vim.api.nvim_create_autocmd({ 'InsertLeave', 'BufLeave' }, { callback = trigger.hide }) 121 | 122 | -- manually hide when exiting insert mode with ctrl+c, since it doesn't trigger InsertLeave 123 | local ctrl_c = vim.api.nvim_replace_termcodes('', true, true, true) 124 | vim.on_key(function(key) 125 | if key == ctrl_c then 126 | vim.schedule(function() 127 | local mode = vim.api.nvim_get_mode().mode 128 | if mode ~= 'i' then trigger.hide() end 129 | end) 130 | end 131 | end) 132 | 133 | return trigger 134 | end 135 | 136 | --- Suppresses on_hide and on_show events for the duration of the callback 137 | --- TODO: extract into an autocmd module 138 | --- HACK: there's likely edge cases with this since we can't know for sure 139 | --- if the autocmds will fire for cursor_moved afaik 140 | function trigger.suppress_events_for_callback(cb) 141 | local cursor_before = vim.api.nvim_win_get_cursor(0) 142 | local changed_tick_before = vim.api.nvim_buf_get_changedtick(0) 143 | 144 | cb() 145 | 146 | local cursor_after = vim.api.nvim_win_get_cursor(0) 147 | local changed_tick_after = vim.api.nvim_buf_get_changedtick(0) 148 | 149 | local is_insert_mode = vim.api.nvim_get_mode().mode == 'i' 150 | trigger.ignore_next_text_changed = changed_tick_after ~= changed_tick_before and is_insert_mode 151 | -- TODO: does this guarantee that the CursorMovedI event will fire? 152 | trigger.ignore_next_cursor_moved = (cursor_after[1] ~= cursor_before[1] or cursor_after[2] ~= cursor_before[2]) 153 | and is_insert_mode 154 | end 155 | 156 | --- @param opts { is_accept?: boolean } | nil 157 | function trigger.show_if_on_trigger_character(opts) 158 | if opts and opts.is_accept and not config.show_on_accept_on_trigger_character then return end 159 | 160 | local cursor_col = vim.api.nvim_win_get_cursor(0)[2] 161 | local char_under_cursor = vim.api.nvim_get_current_line():sub(cursor_col, cursor_col) 162 | local is_on_trigger = vim.tbl_contains(sources.get_trigger_characters(), char_under_cursor) 163 | and not vim.tbl_contains(config.show_on_x_blocked_trigger_characters, char_under_cursor) 164 | 165 | if is_on_trigger then trigger.show({ trigger_character = char_under_cursor }) end 166 | return is_on_trigger 167 | end 168 | 169 | --- @param opts { trigger_character?: string, send_upstream?: boolean, force?: boolean } | nil 170 | function trigger.show(opts) 171 | opts = opts or {} 172 | 173 | local cursor = vim.api.nvim_win_get_cursor(0) 174 | -- already triggered at this position, ignore 175 | if 176 | not opts.force 177 | and trigger.context ~= nil 178 | and cursor[1] == trigger.context.cursor[1] 179 | and cursor[2] == trigger.context.cursor[2] 180 | then 181 | return 182 | end 183 | 184 | -- update context 185 | if trigger.context == nil then trigger.current_context_id = trigger.current_context_id + 1 end 186 | trigger.context = { 187 | id = trigger.current_context_id, 188 | bufnr = vim.api.nvim_get_current_buf(), 189 | cursor = cursor, 190 | line = vim.api.nvim_buf_get_lines(0, cursor[1] - 1, cursor[1], false)[1], 191 | bounds = trigger.get_context_bounds(config.keyword_regex), 192 | trigger = { 193 | kind = opts.trigger_character and vim.lsp.protocol.CompletionTriggerKind.TriggerCharacter 194 | or vim.lsp.protocol.CompletionTriggerKind.Invoked, 195 | character = opts.trigger_character, 196 | }, 197 | } 198 | 199 | if opts.send_upstream ~= false then trigger.event_targets.on_show(trigger.context) end 200 | end 201 | 202 | --- @param callback fun(context: blink.cmp.Context) 203 | function trigger.listen_on_show(callback) trigger.event_targets.on_show = callback end 204 | 205 | function trigger.hide() 206 | if not trigger.context then return end 207 | trigger.context = nil 208 | trigger.event_targets.on_hide() 209 | end 210 | 211 | --- @param callback fun() 212 | function trigger.listen_on_hide(callback) trigger.event_targets.on_hide = callback end 213 | 214 | --- @param cursor number[] 215 | --- @return boolean 216 | function trigger.within_query_bounds(cursor) 217 | if not trigger.context then return false end 218 | 219 | local row, col = cursor[1], cursor[2] 220 | local bounds = trigger.context.bounds 221 | return row == bounds.line_number and col >= bounds.start_col and col <= bounds.end_col 222 | end 223 | 224 | ---@return string? 225 | function trigger.get_current_word() 226 | if not trigger.context then return end 227 | 228 | local bounds = trigger.context.bounds 229 | return trigger.context.line:sub(bounds.start_col, bounds.end_col) 230 | end 231 | --- Moves forward and backwards around the cursor looking for word boundaries 232 | --- @param regex string 233 | --- @return blink.cmp.ContextBounds 234 | function trigger.get_context_bounds(regex) 235 | local cursor_line = vim.api.nvim_win_get_cursor(0)[1] 236 | local cursor_col = vim.api.nvim_win_get_cursor(0)[2] 237 | 238 | local line = vim.api.nvim_buf_get_lines(0, cursor_line - 1, cursor_line, false)[1] 239 | local start_col = cursor_col 240 | while start_col > 1 do 241 | local char = line:sub(start_col, start_col) 242 | if char:match(regex) == nil then 243 | start_col = start_col + 1 244 | break 245 | end 246 | start_col = start_col - 1 247 | end 248 | 249 | local end_col = cursor_col 250 | while end_col < #line do 251 | local char = line:sub(end_col + 1, end_col + 1) 252 | if char:match(regex) == nil then break end 253 | end_col = end_col + 1 254 | end 255 | 256 | -- hack: why do we have to math.min here? 257 | start_col = math.min(start_col, end_col) 258 | 259 | local length = end_col - start_col + 1 260 | -- Since sub(1, 1) returns a single char string, we need to check if that single char matches 261 | -- and otherwise mark the length as 0 262 | if start_col == end_col and line:sub(start_col, end_col):match(regex) == nil then length = 0 end 263 | 264 | return { line_number = cursor_line, start_col = start_col, end_col = end_col, length = length } 265 | end 266 | 267 | return trigger 268 | --------------------------------------------------------------------------------