summaryrefslogtreecommitdiff
path: root/lua/blink/cmp/completion/brackets
diff options
context:
space:
mode:
authorMike Vink <mike@pionative.com>2025-01-19 13:52:52 +0100
committerMike Vink <mike@pionative.com>2025-01-19 13:52:52 +0100
commitb77413ff8f59f380612074f0c9bd49093d8db695 (patch)
tree32c39a811ba96ed4ab0a1c81cce9f8d518ed7e31 /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.lua37
-rw-r--r--lua/blink/cmp/completion/brackets/init.lua6
-rw-r--r--lua/blink/cmp/completion/brackets/kind.lua52
-rw-r--r--lua/blink/cmp/completion/brackets/semantic.lua109
-rw-r--r--lua/blink/cmp/completion/brackets/utils.lua61
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