diff options
| author | Mike Vink <mike@pionative.com> | 2025-01-19 13:52:52 +0100 |
|---|---|---|
| committer | Mike Vink <mike@pionative.com> | 2025-01-19 13:52:52 +0100 |
| commit | b77413ff8f59f380612074f0c9bd49093d8db695 (patch) | |
| tree | 32c39a811ba96ed4ab0a1c81cce9f8d518ed7e31 /lua/blink/cmp/completion/brackets | |
Squashed 'mut/neovim/pack/plugins/start/blink.cmp/' content from commit 1cc3b1a
git-subtree-dir: mut/neovim/pack/plugins/start/blink.cmp
git-subtree-split: 1cc3b1a908fbcfd15451c4772759549724f38524
Diffstat (limited to 'lua/blink/cmp/completion/brackets')
| -rw-r--r-- | lua/blink/cmp/completion/brackets/config.lua | 37 | ||||
| -rw-r--r-- | lua/blink/cmp/completion/brackets/init.lua | 6 | ||||
| -rw-r--r-- | lua/blink/cmp/completion/brackets/kind.lua | 52 | ||||
| -rw-r--r-- | lua/blink/cmp/completion/brackets/semantic.lua | 109 | ||||
| -rw-r--r-- | lua/blink/cmp/completion/brackets/utils.lua | 61 |
5 files changed, 265 insertions, 0 deletions
diff --git a/lua/blink/cmp/completion/brackets/config.lua b/lua/blink/cmp/completion/brackets/config.lua new file mode 100644 index 0000000..9201a08 --- /dev/null +++ b/lua/blink/cmp/completion/brackets/config.lua @@ -0,0 +1,37 @@ +return { + -- stylua: ignore + blocked_filetypes = { + 'sql', 'ruby', 'perl', 'lisp', 'scheme', 'clojure', + 'prolog', 'vb', 'elixir', 'smalltalk', 'applescript' + }, + per_filetype = { + -- languages with a space + haskell = { ' ', '' }, + fsharp = { ' ', '' }, + ocaml = { ' ', '' }, + erlang = { ' ', '' }, + tcl = { ' ', '' }, + nix = { ' ', '' }, + helm = { ' ', '' }, + + shell = { ' ', '' }, + sh = { ' ', '' }, + bash = { ' ', '' }, + fish = { ' ', '' }, + zsh = { ' ', '' }, + powershell = { ' ', '' }, + + make = { ' ', '' }, + + -- languages with square brackets + wl = { '[', ']' }, + wolfram = { '[', ']' }, + mma = { '[', ']' }, + mathematica = { '[', ']' }, + context = { '[', ']' }, + + -- languages with curly brackets + tex = { '{', '}' }, + plaintex = { '{', '}' }, + }, +} diff --git a/lua/blink/cmp/completion/brackets/init.lua b/lua/blink/cmp/completion/brackets/init.lua new file mode 100644 index 0000000..511b42b --- /dev/null +++ b/lua/blink/cmp/completion/brackets/init.lua @@ -0,0 +1,6 @@ +local brackets = {} + +brackets.add_brackets = require('blink.cmp.completion.brackets.kind') +brackets.add_brackets_via_semantic_token = require('blink.cmp.completion.brackets.semantic') + +return brackets diff --git a/lua/blink/cmp/completion/brackets/kind.lua b/lua/blink/cmp/completion/brackets/kind.lua new file mode 100644 index 0000000..f09f180 --- /dev/null +++ b/lua/blink/cmp/completion/brackets/kind.lua @@ -0,0 +1,52 @@ +local utils = require('blink.cmp.completion.brackets.utils') + +--- @param ctx blink.cmp.Context +--- @param filetype string +--- @param item blink.cmp.CompletionItem +--- @return 'added' | 'check_semantic_token' | 'skipped', lsp.TextEdit | lsp.InsertReplaceEdit, number +local function add_brackets(ctx, filetype, item) + local text_edit = item.textEdit + assert(text_edit ~= nil, 'Got nil text edit while adding brackets via kind') + local brackets_for_filetype = utils.get_for_filetype(filetype, item) + + -- skip if we're not in default mode + if ctx.mode ~= 'default' then return 'skipped', text_edit, 0 end + + -- if there's already the correct brackets in front, skip but indicate the cursor should move in front of the bracket + -- TODO: what if the brackets_for_filetype[1] == '' or ' ' (haskell/ocaml)? + -- TODO: should this check semantic tokens and still move the cursor in that case? + if utils.has_brackets_in_front(text_edit, brackets_for_filetype[1]) then + local offset = utils.can_have_brackets(item, brackets_for_filetype) and #brackets_for_filetype[1] or 0 + return 'skipped', text_edit, offset + end + + -- if the item already contains the brackets, conservatively skip adding brackets + -- todo: won't work for snippets when the brackets_for_filetype is { '{', '}' } + -- I've never seen a language like that though + if brackets_for_filetype[1] ~= ' ' and text_edit.newText:match('[\\' .. brackets_for_filetype[1] .. ']') ~= nil then + return 'skipped', text_edit, 0 + end + + -- check if configuration incidates we should skip + if not utils.should_run_resolution(filetype, 'kind') then return 'check_semantic_token', text_edit, 0 end + -- cannot have brackets, skip + if not utils.can_have_brackets(item, brackets_for_filetype) then return 'check_semantic_token', text_edit, 0 end + + text_edit = vim.deepcopy(text_edit) + -- For snippets, we add the cursor position between the brackets as the last placeholder + if item.insertTextFormat == vim.lsp.protocol.InsertTextFormat.Snippet then + local placeholders = utils.snippets_extract_placeholders(text_edit.newText) + local last_placeholder_index = math.max(0, unpack(placeholders)) + text_edit.newText = text_edit.newText + .. brackets_for_filetype[1] + .. '$' + .. tostring(last_placeholder_index + 1) + .. brackets_for_filetype[2] + -- Otherwise, we add as usual + else + text_edit.newText = text_edit.newText .. brackets_for_filetype[1] .. brackets_for_filetype[2] + end + return 'added', text_edit, -#brackets_for_filetype[2] +end + +return add_brackets diff --git a/lua/blink/cmp/completion/brackets/semantic.lua b/lua/blink/cmp/completion/brackets/semantic.lua new file mode 100644 index 0000000..c64afbd --- /dev/null +++ b/lua/blink/cmp/completion/brackets/semantic.lua @@ -0,0 +1,109 @@ +local config = require('blink.cmp.config').completion.accept.auto_brackets +local utils = require('blink.cmp.completion.brackets.utils') + +local semantic = {} + +--- Asynchronously use semantic tokens to determine if brackets should be added +--- @param filetype string +--- @param item blink.cmp.CompletionItem +--- @param callback fun() +function semantic.add_brackets_via_semantic_token(filetype, item, callback) + if not utils.should_run_resolution(filetype, 'semantic_token') then return callback() end + + local text_edit = item.textEdit + assert(text_edit ~= nil, 'Got nil text edit while adding brackets via semantic tokens') + local client = vim.lsp.get_client_by_id(item.client_id) + if client == nil then return callback() end + + local capabilities = client.server_capabilities.semanticTokensProvider + if not capabilities or not capabilities.legend or (not capabilities.range and not capabilities.full) then + return callback() + end + + local token_types = client.server_capabilities.semanticTokensProvider.legend.tokenTypes + local params = { + textDocument = vim.lsp.util.make_text_document_params(), + range = capabilities.range and { + start = { line = text_edit.range.start.line, character = text_edit.range.start.character }, + ['end'] = { line = text_edit.range.start.line + 1, character = 0 }, + } or nil, + } + + local cursor_before_call = vim.api.nvim_win_get_cursor(0) + + local start_time = vim.uv.hrtime() + client.request( + capabilities.range and 'textDocument/semanticTokens/range' or 'textDocument/semanticTokens/full', + params, + function(err, result) + if err ~= nil or result == nil or #result.data == 0 then return callback() end + + -- cancel if it's been too long, or if the cursor moved + local ms_since_call = (vim.uv.hrtime() - start_time) / 1000000 + local cursor_after_call = vim.api.nvim_win_get_cursor(0) + if + ms_since_call > config.semantic_token_resolution.timeout_ms + or cursor_before_call[1] ~= cursor_after_call[1] + or cursor_before_call[2] ~= cursor_after_call[2] + then + return callback() + end + + for _, token in ipairs(semantic.process_semantic_token_data(result.data, token_types)) do + if + cursor_after_call[1] == token.line + and cursor_after_call[2] >= token.start_col + and cursor_after_call[2] <= token.end_col + and (token.type == 'function' or token.type == 'method') + then + -- add the brackets + local brackets_for_filetype = utils.get_for_filetype(filetype, item) + local line = vim.api.nvim_get_current_line() + local start_col = text_edit.range.start.character + #text_edit.newText + local new_line = line:sub(1, start_col) + .. brackets_for_filetype[1] + .. brackets_for_filetype[2] + .. line:sub(start_col + 1) + vim.api.nvim_set_current_line(new_line) + vim.api.nvim_win_set_cursor(0, { cursor_after_call[1], start_col + #brackets_for_filetype[1] }) + callback() + return + end + end + + callback() + end + ) +end + +function semantic.process_semantic_token_data(data, token_types) + local tokens = {} + local idx = 0 + local token_line = 0 + local token_start_col = 0 + + while (idx + 1) * 5 <= #data do + local delta_token_line = data[idx * 5 + 1] + local delta_token_start_col = data[idx * 5 + 2] + local delta_token_length = data[idx * 5 + 3] + local type = token_types[data[idx * 5 + 4] + 1] + + if delta_token_line > 0 then token_start_col = 0 end + token_line = token_line + delta_token_line + token_start_col = token_start_col + delta_token_start_col + + table.insert(tokens, { + line = token_line + 1, + start_col = token_start_col, + end_col = token_start_col + delta_token_length, + type = type, + }) + + token_start_col = token_start_col + delta_token_length + idx = idx + 1 + end + + return tokens +end + +return semantic.add_brackets_via_semantic_token diff --git a/lua/blink/cmp/completion/brackets/utils.lua b/lua/blink/cmp/completion/brackets/utils.lua new file mode 100644 index 0000000..cf84f11 --- /dev/null +++ b/lua/blink/cmp/completion/brackets/utils.lua @@ -0,0 +1,61 @@ +local config = require('blink.cmp.config').completion.accept.auto_brackets +local CompletionItemKind = require('blink.cmp.types').CompletionItemKind +local brackets = require('blink.cmp.completion.brackets.config') +local utils = {} + +--- @param snippet string +function utils.snippets_extract_placeholders(snippet) + local placeholders = {} + local pattern = [=[(\$\{(\d+)(:([^}\\]|\\.)*?)?\})]=] + + for _, number, _, _ in snippet:gmatch(pattern) do + table.insert(placeholders, tonumber(number)) + end + + return placeholders +end + +--- @param filetype string +--- @param item blink.cmp.CompletionItem +--- @return string[] +function utils.get_for_filetype(filetype, item) + local default = config.default_brackets + local per_filetype = config.override_brackets_for_filetypes[filetype] or brackets.per_filetype[filetype] + + if type(per_filetype) == 'function' then return per_filetype(item) or default end + return per_filetype or default +end + +--- @param filetype string +--- @param resolution_method 'kind' | 'semantic_token' +--- @return boolean +function utils.should_run_resolution(filetype, resolution_method) + -- resolution method specific + if not config[resolution_method .. '_resolution'].enabled then return false end + local resolution_blocked_filetypes = config[resolution_method .. '_resolution'].blocked_filetypes + if vim.tbl_contains(resolution_blocked_filetypes, filetype) then return false end + + -- global + if not config.enabled then return false end + if vim.tbl_contains(config.force_allow_filetypes, filetype) then return true end + return not vim.tbl_contains(config.blocked_filetypes, filetype) + and not vim.tbl_contains(brackets.blocked_filetypes, filetype) +end + +--- @param text_edit lsp.TextEdit | lsp.InsertReplaceEdit +--- @param bracket string +--- @return boolean +function utils.has_brackets_in_front(text_edit, bracket) + local line = vim.api.nvim_get_current_line() + local col = text_edit.range['end'].character + 1 + return line:sub(col, col) == bracket +end + +--- @param item blink.cmp.CompletionItem +--- @param _ string[] +-- TODO: for edge cases, we should probably also take brackets themselves into consideration +function utils.can_have_brackets(item, _) + return item.kind == CompletionItemKind.Function or item.kind == CompletionItemKind.Method +end + +return utils |
