completion.lua (32675B)
1 --- @brief 2 --- The `vim.lsp.completion` module enables insert-mode completion driven by an LSP server. Call 3 --- `enable()` to make it available through Nvim builtin completion (via the |CompleteDone| event). 4 --- Specify `autotrigger=true` to activate "auto-completion" when you type any of the server-defined 5 --- `triggerCharacters`. Use CTRL-Y to select an item from the completion menu. |complete_CTRL-Y| 6 --- 7 --- Example: activate LSP-driven auto-completion: 8 --- ```lua 9 --- -- Works best with completeopt=noselect. 10 --- -- Use CTRL-Y to select an item. |complete_CTRL-Y| 11 --- vim.cmd[[set completeopt+=menuone,noselect,popup]] 12 --- vim.lsp.start({ 13 --- name = 'ts_ls', 14 --- cmd = …, 15 --- on_attach = function(client, bufnr) 16 --- vim.lsp.completion.enable(true, client.id, bufnr, { 17 --- autotrigger = true, 18 --- convert = function(item) 19 --- return { abbr = item.label:gsub('%b()', '') } 20 --- end, 21 --- }) 22 --- end, 23 --- }) 24 --- ``` 25 --- 26 --- [lsp-autocompletion]() 27 --- 28 --- The LSP `triggerCharacters` field decides when to trigger autocompletion. If you want to trigger 29 --- on EVERY keypress you can either: 30 --- - Extend `client.server_capabilities.completionProvider.triggerCharacters` on `LspAttach`, 31 --- before you call `vim.lsp.completion.enable(… {autotrigger=true})`. See the |lsp-attach| example. 32 --- - Call `vim.lsp.completion.get()` from an |InsertCharPre| autocommand. 33 34 local M = {} 35 36 local api = vim.api 37 local lsp = vim.lsp 38 local protocol = lsp.protocol 39 40 local rtt_ms = 50.0 41 local ns_to_ms = 0.000001 42 43 --- @alias vim.lsp.CompletionResult lsp.CompletionList | lsp.CompletionItem[] 44 45 -- TODO(mariasolos): Remove this declaration once we figure out a better way to handle 46 -- literal/anonymous types (see https://github.com/neovim/neovim/pull/27542/files#r1495259331). 47 --- @nodoc 48 --- @class lsp.ItemDefaults 49 --- @field editRange lsp.Range | { insert: lsp.Range, replace: lsp.Range } | nil 50 --- @field insertTextFormat lsp.InsertTextFormat? 51 --- @field insertTextMode lsp.InsertTextMode? 52 --- @field data any 53 54 --- @nodoc 55 --- @class vim.lsp.completion.BufHandle 56 --- @field clients table<integer, vim.lsp.Client> 57 --- @field triggers table<string, vim.lsp.Client[]> 58 --- @field convert? fun(item: lsp.CompletionItem): table 59 60 --- @type table<integer, vim.lsp.completion.BufHandle> 61 local buf_handles = {} 62 63 --- @nodoc 64 --- @class vim.lsp.completion.Context 65 local Context = { 66 cursor = nil, --- @type [integer, integer]? 67 last_request_time = nil, --- @type integer? 68 pending_requests = {}, --- @type function[] 69 isIncomplete = false, 70 } 71 72 --- @nodoc 73 function Context:cancel_pending() 74 for _, cancel in ipairs(self.pending_requests) do 75 cancel() 76 end 77 78 self.pending_requests = {} 79 end 80 81 --- @nodoc 82 function Context:reset() 83 -- Note that the cursor isn't reset here, it needs to survive a `CompleteDone` event. 84 self.isIncomplete = false 85 self.last_request_time = nil 86 self:cancel_pending() 87 end 88 89 --- @type uv.uv_timer_t? 90 local completion_timer = nil 91 92 --- @return uv.uv_timer_t 93 local function new_timer() 94 return (assert(vim.uv.new_timer())) 95 end 96 97 local function reset_timer() 98 if completion_timer then 99 completion_timer:stop() 100 completion_timer:close() 101 end 102 103 completion_timer = nil 104 end 105 106 --- @param window integer 107 --- @param warmup integer 108 --- @return fun(sample: number): number 109 local function exp_avg(window, warmup) 110 local count = 0 111 local sum = 0 112 local value = 0.0 113 114 return function(sample) 115 if count < warmup then 116 count = count + 1 117 sum = sum + sample 118 value = sum / count 119 else 120 local factor = 2.0 / (window + 1) 121 value = value * (1 - factor) + sample * factor 122 end 123 return value 124 end 125 end 126 local compute_new_average = exp_avg(10, 10) 127 128 --- @return number 129 local function next_debounce() 130 if not Context.last_request_time then 131 return rtt_ms 132 end 133 134 local ms_since_request = (vim.uv.hrtime() - Context.last_request_time) * ns_to_ms 135 return math.max((ms_since_request - rtt_ms) * -1, 0) 136 end 137 138 --- @param input string Unparsed snippet 139 --- @return string # Parsed snippet if successful, else returns its input 140 local function parse_snippet(input) 141 local ok, parsed = pcall(function() 142 return lsp._snippet_grammar.parse(input) 143 end) 144 return ok and tostring(parsed) or input 145 end 146 147 --- @param item lsp.CompletionItem 148 local function apply_snippet(item) 149 if item.textEdit then 150 vim.snippet.expand(item.textEdit.newText) 151 elseif item.insertText then 152 vim.snippet.expand(item.insertText) 153 end 154 end 155 156 --- Returns text that should be inserted when a selecting completion item. The 157 --- precedence is as follows: textEdit.newText > insertText > label 158 --- 159 --- See https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_completion 160 --- 161 --- @param item lsp.CompletionItem 162 --- @param prefix string 163 --- @param match fun(text: string, prefix: string):boolean 164 --- @return string 165 local function get_completion_word(item, prefix, match) 166 if item.insertTextFormat == protocol.InsertTextFormat.Snippet then 167 if item.textEdit or (item.insertText and item.insertText ~= '') then 168 -- Use label instead of text if text has different starting characters. 169 -- label is used as abbr (=displayed), but word is used for filtering 170 -- This is required for things like postfix completion. 171 -- E.g. in lua: 172 -- 173 -- local f = {} 174 -- f@| 175 -- ▲ 176 -- └─ cursor 177 -- 178 -- item.textEdit.newText: table.insert(f, $0) 179 -- label: insert 180 -- 181 -- Typing `i` would remove the candidate because newText starts with `t`. 182 local text = parse_snippet(item.insertText or item.textEdit.newText) 183 local word = #text < #item.label and vim.fn.matchstr(text, '\\k*') or item.label 184 if item.filterText and not match(word, prefix) then 185 return item.filterText 186 else 187 return word 188 end 189 else 190 return item.label 191 end 192 elseif item.textEdit then 193 local word = item.textEdit.newText 194 word = string.gsub(word, '\r\n?', '\n') 195 return word:match('([^\n]*)') or word 196 elseif item.insertText and item.insertText ~= '' then 197 return item.insertText 198 end 199 return item.label 200 end 201 202 --- Applies the given defaults to the completion item, modifying it in place. 203 --- 204 --- @param item lsp.CompletionItem 205 --- @param defaults lsp.ItemDefaults? 206 local function apply_defaults(item, defaults) 207 if not defaults then 208 return 209 end 210 211 item.insertTextFormat = item.insertTextFormat or defaults.insertTextFormat 212 item.insertTextMode = item.insertTextMode or defaults.insertTextMode 213 item.data = item.data or defaults.data 214 if defaults.editRange then 215 local textEdit = item.textEdit or {} 216 item.textEdit = textEdit 217 textEdit.newText = textEdit.newText or item.textEditText or item.insertText or item.label 218 if defaults.editRange.start then 219 textEdit.range = textEdit.range or defaults.editRange 220 elseif defaults.editRange.insert then 221 textEdit.insert = defaults.editRange.insert 222 textEdit.replace = defaults.editRange.replace 223 end 224 end 225 end 226 227 --- @param result vim.lsp.CompletionResult 228 --- @return lsp.CompletionItem[] 229 local function get_items(result) 230 if result.items then 231 -- When we have a list, apply the defaults and return an array of items. 232 for _, item in ipairs(result.items) do 233 ---@diagnostic disable-next-line: param-type-mismatch 234 apply_defaults(item, result.itemDefaults) 235 end 236 return result.items 237 else 238 -- Else just return the items as they are. 239 return result 240 end 241 end 242 243 ---@param item lsp.CompletionItem 244 ---@return string 245 local function get_doc(item) 246 local doc = item.documentation 247 if not doc then 248 return '' 249 end 250 if type(doc) == 'string' then 251 return doc 252 end 253 if type(doc) == 'table' and type(doc.value) == 'string' then 254 return doc.value 255 end 256 257 vim.notify('invalid documentation value: ' .. vim.inspect(doc), vim.log.levels.WARN) 258 return '' 259 end 260 261 ---@param value string 262 ---@param prefix string 263 ---@return boolean 264 ---@return integer? 265 local function match_item_by_value(value, prefix) 266 if prefix == '' then 267 return true, nil 268 end 269 if vim.o.completeopt:find('fuzzy') ~= nil then 270 local score = vim.fn.matchfuzzypos({ value }, prefix)[3] ---@type table 271 return #score > 0, score[1] 272 end 273 274 if vim.o.ignorecase and (not vim.o.smartcase or not prefix:find('%u')) then 275 return vim.startswith(value:lower(), prefix:lower()), nil 276 end 277 return vim.startswith(value, prefix), nil 278 end 279 280 --- Generate kind text for completion color items 281 --- Parse color from doc and return colored symbol ■ 282 --- 283 ---@param item table completion item with kind and documentation 284 ---@return string? kind text or "■" for colors 285 ---@return string? highlight group for colors 286 local function generate_kind(item) 287 if not lsp.protocol.CompletionItemKind[item.kind] then 288 return 'Unknown' 289 end 290 if item.kind ~= lsp.protocol.CompletionItemKind.Color then 291 return lsp.protocol.CompletionItemKind[item.kind] 292 end 293 local doc = get_doc(item) 294 if #doc == 0 then 295 return 296 end 297 298 -- extract hex from RGB format 299 local r, g, b = doc:match('rgb%((%d+)%s*,?%s*(%d+)%s*,?%s*(%d+)%)') 300 local hex = r and string.format('%02x%02x%02x', tonumber(r), tonumber(g), tonumber(b)) 301 or doc:match('#?([%da-fA-F]+)') 302 303 if not hex then 304 return 305 end 306 307 -- expand 3-digit hex to 6-digit 308 if #hex == 3 then 309 hex = hex:gsub('.', '%1%1') 310 end 311 312 if #hex ~= 6 then 313 return 314 end 315 316 hex = hex:lower() 317 local group = ('@lsp.color.%s'):format(hex) 318 if #api.nvim_get_hl(0, { name = group }) == 0 then 319 api.nvim_set_hl(0, group, { fg = '#' .. hex }) 320 end 321 322 return '■', group 323 end 324 325 --- Turns the result of a `textDocument/completion` request into vim-compatible 326 --- |complete-items|. 327 --- 328 --- @param result vim.lsp.CompletionResult Result of `textDocument/completion` 329 --- @param prefix string prefix to filter the completion items 330 --- @param client_id integer? Client ID 331 --- @param server_start_boundary integer? server start boundary 332 --- @param line string? current line content 333 --- @param lnum integer? 0-indexed line number 334 --- @param encoding string? encoding 335 --- @return table[] 336 --- @see complete-items 337 function M._lsp_to_complete_items( 338 result, 339 prefix, 340 client_id, 341 server_start_boundary, 342 line, 343 lnum, 344 encoding 345 ) 346 local items = get_items(result) 347 if vim.tbl_isempty(items) then 348 return {} 349 end 350 351 ---@type fun(item: lsp.CompletionItem):boolean 352 local matches 353 if not prefix:find('%w') then 354 matches = function(_) 355 return true 356 end 357 else 358 ---@param item lsp.CompletionItem 359 matches = function(item) 360 if item.filterText then 361 return match_item_by_value(item.filterText, prefix) 362 end 363 364 if item.textEdit and not item.textEdit.newText then 365 -- server took care of filtering 366 return true 367 end 368 369 return match_item_by_value(item.label, prefix) 370 end 371 end 372 373 local candidates = {} 374 local bufnr = api.nvim_get_current_buf() 375 local user_convert = vim.tbl_get(buf_handles, bufnr, 'convert') 376 local user_cmp = vim.tbl_get(buf_handles, bufnr, 'cmp') 377 for _, item in ipairs(items) do 378 local match, score = matches(item) 379 if match then 380 local word = get_completion_word(item, prefix, match_item_by_value) 381 382 if server_start_boundary and line and lnum and encoding and item.textEdit then 383 --- @type integer? 384 local item_start_char 385 if item.textEdit.range and item.textEdit.range.start.line == lnum then 386 item_start_char = item.textEdit.range.start.character 387 elseif item.textEdit.insert and item.textEdit.insert.start.line == lnum then 388 item_start_char = item.textEdit.insert.start.character 389 end 390 391 if item_start_char then 392 local item_start_byte = vim.str_byteindex(line, encoding, item_start_char, false) 393 if item_start_byte > server_start_boundary then 394 local missing_prefix = line:sub(server_start_boundary + 1, item_start_byte) 395 word = missing_prefix .. word 396 end 397 end 398 end 399 400 local hl_group = '' 401 if 402 item.deprecated 403 or vim.list_contains((item.tags or {}), protocol.CompletionTag.Deprecated) 404 then 405 hl_group = 'DiagnosticDeprecated' 406 end 407 local kind, kind_hlgroup = generate_kind(item) 408 local completion_item = { 409 word = word, 410 abbr = item.label, 411 kind = kind, 412 menu = item.detail or '', 413 info = get_doc(item), 414 icase = 1, 415 dup = 1, 416 empty = 1, 417 abbr_hlgroup = hl_group, 418 kind_hlgroup = kind_hlgroup, 419 user_data = { 420 nvim = { 421 lsp = { 422 completion_item = item, 423 client_id = client_id, 424 }, 425 }, 426 }, 427 _fuzzy_score = score, 428 } 429 if user_convert then 430 completion_item = vim.tbl_extend('keep', user_convert(item), completion_item) 431 end 432 table.insert(candidates, completion_item) 433 end 434 end 435 436 if not user_cmp then 437 local compare_by_sortText_and_label = function(a, b) 438 ---@type lsp.CompletionItem 439 local itema = a.user_data.nvim.lsp.completion_item 440 ---@type lsp.CompletionItem 441 local itemb = b.user_data.nvim.lsp.completion_item 442 return (itema.sortText or itema.label) < (itemb.sortText or itemb.label) 443 end 444 445 local use_fuzzy_sort = vim.o.completeopt:find('fuzzy') ~= nil 446 and vim.o.completeopt:find('nosort') == nil 447 and not result.isIncomplete 448 and #prefix > 0 449 450 local compare_fn = use_fuzzy_sort 451 and function(a, b) 452 local score_a = a._fuzzy_score or 0 453 local score_b = b._fuzzy_score or 0 454 if score_a ~= score_b then 455 return score_a > score_b 456 end 457 return compare_by_sortText_and_label(a, b) 458 end 459 or compare_by_sortText_and_label 460 461 table.sort(candidates, compare_fn) 462 end 463 return candidates 464 end 465 466 --- @param lnum integer 0-indexed 467 --- @param line string 468 --- @param items lsp.CompletionItem[] 469 --- @param encoding 'utf-8'|'utf-16'|'utf-32' 470 --- @return integer? 471 local function adjust_start_col(lnum, line, items, encoding) 472 local min_start_char = nil 473 for _, item in pairs(items) do 474 if item.textEdit then 475 local start_char = nil 476 if item.textEdit.range and item.textEdit.range.start.line == lnum then 477 start_char = item.textEdit.range.start.character 478 elseif item.textEdit.insert and item.textEdit.insert.start.line == lnum then 479 start_char = item.textEdit.insert.start.character 480 end 481 if start_char then 482 if not min_start_char or start_char < min_start_char then 483 min_start_char = start_char 484 end 485 end 486 end 487 end 488 if min_start_char then 489 return vim.str_byteindex(line, encoding, min_start_char, false) 490 else 491 return nil 492 end 493 end 494 495 --- @param line string line content 496 --- @param lnum integer 0-indexed line number 497 --- @param cursor_col integer 498 --- @param client_id integer client ID 499 --- @param client_start_boundary integer 0-indexed word boundary 500 --- @param server_start_boundary? integer 0-indexed word boundary, based on textEdit.range.start.character 501 --- @param result vim.lsp.CompletionResult 502 --- @param encoding 'utf-8'|'utf-16'|'utf-32' 503 --- @return table[] matches 504 --- @return integer? server_start_boundary 505 function M._convert_results( 506 line, 507 lnum, 508 cursor_col, 509 client_id, 510 client_start_boundary, 511 server_start_boundary, 512 result, 513 encoding 514 ) 515 -- Completion response items may be relative to a position different than `client_start_boundary`. 516 -- Concrete example, with lua-language-server: 517 -- 518 -- require('plenary.asy| 519 -- ▲ ▲ ▲ 520 -- │ │ └── cursor_pos: 20 521 -- │ └────── client_start_boundary: 17 522 -- └────────────── textEdit.range.start.character: 9 523 -- .newText = 'plenary.async' 524 -- ^^^ 525 -- prefix (We'd remove everything not starting with `asy`, 526 -- so we'd eliminate the `plenary.async` result 527 -- 528 -- `adjust_start_col` is used to prefer the language server boundary. 529 -- 530 local candidates = get_items(result) 531 local curstartbyte = adjust_start_col(lnum, line, candidates, encoding) 532 if server_start_boundary == nil then 533 server_start_boundary = curstartbyte 534 elseif curstartbyte ~= nil and curstartbyte ~= server_start_boundary then 535 server_start_boundary = client_start_boundary 536 end 537 local prefix = line:sub((server_start_boundary or client_start_boundary) + 1, cursor_col) 538 local matches = 539 M._lsp_to_complete_items(result, prefix, client_id, server_start_boundary, line, lnum, encoding) 540 541 return matches, server_start_boundary 542 end 543 544 -- NOTE: The reason we don't use `lsp.buf_request_all` here is because we want to filter the clients 545 -- that received the request based on the trigger characters. 546 --- @param clients table<integer, vim.lsp.Client> # keys != client_id 547 --- @param bufnr integer 548 --- @param win integer 549 --- @param ctx? lsp.CompletionContext 550 --- @param callback fun(responses: table<integer, { err: lsp.ResponseError, result: vim.lsp.CompletionResult }>) 551 --- @return function # Cancellation function 552 local function request(clients, bufnr, win, ctx, callback) 553 local responses = {} --- @type table<integer, { err: lsp.ResponseError, result: any }> 554 local request_ids = {} --- @type table<integer, integer> 555 local remaining_requests = vim.tbl_count(clients) 556 557 for _, client in pairs(clients) do 558 local client_id = client.id 559 local params = lsp.util.make_position_params(win, client.offset_encoding) 560 --- @cast params lsp.CompletionParams 561 params.context = ctx 562 local ok, request_id = client:request('textDocument/completion', params, function(err, result) 563 responses[client_id] = { err = err, result = result } 564 remaining_requests = remaining_requests - 1 565 if remaining_requests == 0 then 566 callback(responses) 567 end 568 end, bufnr) 569 570 if ok then 571 request_ids[client_id] = request_id 572 end 573 end 574 575 return function() 576 for client_id, request_id in pairs(request_ids) do 577 local client = lsp.get_client_by_id(client_id) 578 if client then 579 client:cancel_request(request_id) 580 end 581 end 582 end 583 end 584 585 --- @param bufnr integer 586 --- @param clients vim.lsp.Client[] 587 --- @param ctx? lsp.CompletionContext 588 local function trigger(bufnr, clients, ctx) 589 reset_timer() 590 Context:cancel_pending() 591 592 if tonumber(vim.fn.pumvisible()) == 1 and not Context.isIncomplete then 593 return 594 end 595 596 local win = api.nvim_get_current_win() 597 local cursor_row = api.nvim_win_get_cursor(win)[1] 598 local start_time = vim.uv.hrtime() --[[@as integer]] 599 Context.last_request_time = start_time 600 601 local cancel_request = request(clients, bufnr, win, ctx, function(responses) 602 local end_time = vim.uv.hrtime() 603 rtt_ms = compute_new_average((end_time - start_time) * ns_to_ms) 604 605 Context.pending_requests = {} 606 Context.isIncomplete = false 607 608 local new_cursor_row, cursor_col = unpack(api.nvim_win_get_cursor(win)) --- @type integer, integer 609 local row_changed = new_cursor_row ~= cursor_row 610 local mode = api.nvim_get_mode().mode 611 if row_changed or not (mode == 'i' or mode == 'ic') then 612 return 613 end 614 615 local line = api.nvim_get_current_line() 616 local line_to_cursor = line:sub(1, cursor_col) 617 local word_boundary = vim.fn.match(line_to_cursor, '\\k*$') 618 619 local matches = {} 620 621 local server_start_boundary --- @type integer? 622 for client_id, response in pairs(responses) do 623 local client = lsp.get_client_by_id(client_id) 624 if response.err then 625 local msg = ('%s: %s %s'):format( 626 client and client.name or 'UNKNOWN', 627 response.err.code or 'NO_CODE', 628 response.err.message 629 ) 630 vim.notify_once(msg, vim.log.levels.WARN) 631 end 632 633 local result = response.result 634 if result and #(result.items or result) > 0 then 635 Context.isIncomplete = Context.isIncomplete or result.isIncomplete 636 local encoding = client and client.offset_encoding or 'utf-16' 637 local client_matches, tmp_server_start_boundary 638 client_matches, tmp_server_start_boundary = M._convert_results( 639 line, 640 cursor_row - 1, 641 cursor_col, 642 client_id, 643 word_boundary, 644 nil, 645 result, 646 encoding 647 ) 648 649 server_start_boundary = tmp_server_start_boundary or server_start_boundary 650 vim.list_extend(matches, client_matches) 651 end 652 end 653 654 --- @type table[] 655 local prev_matches = vim.fn.complete_info({ 'items', 'matches' })['items'] 656 657 --- @param prev_match table 658 prev_matches = vim.tbl_filter(function(prev_match) 659 local client_id = vim.tbl_get(prev_match, 'user_data', 'nvim', 'lsp', 'client_id') 660 if client_id and responses[client_id] ~= nil then 661 return false 662 end 663 return vim.tbl_get(prev_match, 'match') 664 end, prev_matches) 665 666 matches = vim.list_extend(prev_matches, matches) 667 local user_cmp = vim.tbl_get(buf_handles, bufnr, 'cmp') 668 if user_cmp then 669 table.sort(matches, user_cmp) 670 end 671 672 local start_col = (server_start_boundary or word_boundary) + 1 673 Context.cursor = { cursor_row, start_col } 674 vim.fn.complete(start_col, matches) 675 end) 676 677 table.insert(Context.pending_requests, cancel_request) 678 end 679 680 --- @param handle vim.lsp.completion.BufHandle 681 local function on_insert_char_pre(handle) 682 if tonumber(vim.fn.pumvisible()) == 1 then 683 if Context.isIncomplete then 684 reset_timer() 685 686 local debounce_ms = next_debounce() 687 local ctx = { triggerKind = protocol.CompletionTriggerKind.TriggerForIncompleteCompletions } 688 if debounce_ms == 0 then 689 vim.schedule(function() 690 M.get({ ctx = ctx }) 691 end) 692 else 693 completion_timer = new_timer() 694 completion_timer:start( 695 math.floor(debounce_ms), 696 0, 697 vim.schedule_wrap(function() 698 M.get({ ctx = ctx }) 699 end) 700 ) 701 end 702 end 703 704 return 705 end 706 707 local char = api.nvim_get_vvar('char') 708 local matched_clients = handle.triggers[char] 709 -- Discard pending trigger char, complete the "latest" one. 710 -- Can happen if a mapping inputs multiple trigger chars simultaneously. 711 reset_timer() 712 if matched_clients then 713 completion_timer = assert(vim.uv.new_timer()) 714 completion_timer:start(25, 0, function() 715 reset_timer() 716 vim.schedule(function() 717 trigger( 718 api.nvim_get_current_buf(), 719 matched_clients, 720 { triggerKind = protocol.CompletionTriggerKind.TriggerCharacter, triggerCharacter = char } 721 ) 722 end) 723 end) 724 end 725 end 726 727 local function on_insert_leave() 728 reset_timer() 729 Context.cursor = nil 730 Context:reset() 731 end 732 733 local function on_complete_done() 734 local completed_item = api.nvim_get_vvar('completed_item') 735 if not completed_item or not completed_item.user_data or not completed_item.user_data.nvim then 736 Context:reset() 737 return 738 end 739 740 local cursor_row, cursor_col = unpack(api.nvim_win_get_cursor(0)) --- @type integer, integer 741 cursor_row = cursor_row - 1 742 local completion_item = completed_item.user_data.nvim.lsp.completion_item --- @type lsp.CompletionItem 743 local client_id = completed_item.user_data.nvim.lsp.client_id --- @type integer 744 if not completion_item or not client_id then 745 Context:reset() 746 return 747 end 748 749 local bufnr = api.nvim_get_current_buf() 750 local expand_snippet = completion_item.insertTextFormat == protocol.InsertTextFormat.Snippet 751 and (completion_item.textEdit ~= nil or completion_item.insertText ~= nil) 752 753 Context:reset() 754 755 local client = lsp.get_client_by_id(client_id) 756 if not client then 757 return 758 end 759 760 local position_encoding = client.offset_encoding or 'utf-16' 761 local resolve_provider = (client.server_capabilities.completionProvider or {}).resolveProvider 762 763 local function clear_word() 764 if not expand_snippet then 765 return nil 766 end 767 768 -- Remove the already inserted word. 769 api.nvim_buf_set_text( 770 bufnr, 771 Context.cursor[1] - 1, 772 Context.cursor[2] - 1, 773 cursor_row, 774 cursor_col, 775 { '' } 776 ) 777 end 778 779 local function apply_snippet_and_command() 780 if expand_snippet then 781 apply_snippet(completion_item) 782 end 783 784 local command = completion_item.command 785 if command then 786 client:exec_cmd(command, { bufnr = bufnr }) 787 end 788 end 789 790 if completion_item.additionalTextEdits and next(completion_item.additionalTextEdits) then 791 clear_word() 792 lsp.util.apply_text_edits(completion_item.additionalTextEdits, bufnr, position_encoding) 793 apply_snippet_and_command() 794 elseif resolve_provider and type(completion_item) == 'table' then 795 local changedtick = vim.b[bufnr].changedtick 796 797 --- @param result lsp.CompletionItem 798 client:request('completionItem/resolve', completion_item, function(err, result) 799 if changedtick ~= vim.b[bufnr].changedtick then 800 return 801 end 802 803 clear_word() 804 if err then 805 vim.notify_once(err.message, vim.log.levels.WARN) 806 elseif result then 807 if result.additionalTextEdits then 808 lsp.util.apply_text_edits(result.additionalTextEdits, bufnr, position_encoding) 809 end 810 if result.command then 811 completion_item.command = result.command 812 end 813 end 814 apply_snippet_and_command() 815 end, bufnr) 816 else 817 clear_word() 818 apply_snippet_and_command() 819 end 820 end 821 822 ---@param bufnr integer 823 ---@return string 824 local function get_augroup(bufnr) 825 return string.format('nvim.lsp.completion_%d', bufnr) 826 end 827 828 --- @param client_id integer 829 --- @param bufnr integer 830 local function disable_completions(client_id, bufnr) 831 local handle = buf_handles[bufnr] 832 if not handle then 833 return 834 end 835 836 handle.clients[client_id] = nil 837 if not next(handle.clients) then 838 buf_handles[bufnr] = nil 839 api.nvim_del_augroup_by_name(get_augroup(bufnr)) 840 else 841 for char, clients in pairs(handle.triggers) do 842 --- @param c vim.lsp.Client 843 handle.triggers[char] = vim.tbl_filter(function(c) 844 return c.id ~= client_id 845 end, clients) 846 end 847 end 848 end 849 850 --- @inlinedoc 851 --- @class vim.lsp.completion.BufferOpts 852 --- @field autotrigger? boolean (default: false) When true, completion triggers automatically based on the server's `triggerCharacters`. 853 --- @field convert? fun(item: lsp.CompletionItem): table Transforms an LSP CompletionItem to |complete-items|. 854 --- @field cmp? fun(a: table, b: table): boolean Comparator for sorting merged completion items from all servers. 855 856 ---@param client_id integer 857 ---@param bufnr integer 858 ---@param opts vim.lsp.completion.BufferOpts 859 local function enable_completions(client_id, bufnr, opts) 860 local buf_handle = buf_handles[bufnr] 861 if not buf_handle then 862 buf_handle = { clients = {}, triggers = {}, convert = opts.convert, cmp = opts.cmp } 863 buf_handles[bufnr] = buf_handle 864 865 -- Attach to buffer events. 866 api.nvim_buf_attach(bufnr, false, { 867 on_detach = function(_, buf) 868 buf_handles[buf] = nil 869 end, 870 on_reload = function(_, buf) 871 M.enable(true, client_id, buf, opts) 872 end, 873 }) 874 875 -- Set up autocommands. 876 local group = api.nvim_create_augroup(get_augroup(bufnr), { clear = true }) 877 api.nvim_create_autocmd('LspDetach', { 878 group = group, 879 buffer = bufnr, 880 desc = 'vim.lsp.completion: clean up client on detach', 881 callback = function(args) 882 disable_completions(args.data.client_id, args.buf) 883 end, 884 }) 885 api.nvim_create_autocmd('CompleteDone', { 886 group = group, 887 buffer = bufnr, 888 callback = function() 889 local reason = api.nvim_get_vvar('event').reason --- @type string 890 if reason == 'accept' then 891 on_complete_done() 892 end 893 end, 894 }) 895 if opts.autotrigger then 896 api.nvim_create_autocmd('InsertCharPre', { 897 group = group, 898 buffer = bufnr, 899 callback = function() 900 on_insert_char_pre(buf_handles[bufnr]) 901 end, 902 }) 903 api.nvim_create_autocmd('InsertLeave', { 904 group = group, 905 buffer = bufnr, 906 callback = on_insert_leave, 907 }) 908 end 909 end 910 911 if not buf_handle.clients[client_id] then 912 local client = lsp.get_client_by_id(client_id) 913 assert(client, 'invalid client ID') 914 915 -- Add the new client to the buffer's clients. 916 buf_handle.clients[client_id] = client 917 918 -- Add the new client to the clients that should be triggered by its trigger characters. 919 --- @type string[] 920 local triggers = vim.tbl_get( 921 client.server_capabilities, 922 'completionProvider', 923 'triggerCharacters' 924 ) or {} 925 for _, char in ipairs(triggers) do 926 local clients_for_trigger = buf_handle.triggers[char] 927 if not clients_for_trigger then 928 clients_for_trigger = {} 929 buf_handle.triggers[char] = clients_for_trigger 930 end 931 local client_exists = vim.iter(clients_for_trigger):any(function(c) 932 return c.id == client_id 933 end) 934 if not client_exists then 935 table.insert(clients_for_trigger, client) 936 end 937 end 938 end 939 end 940 941 --- Enables or disables completions from the given language client in the given 942 --- buffer. Effects of enabling completions are: 943 --- 944 --- - Calling |vim.lsp.completion.get()| uses the enabled clients to retrieve 945 --- completion candidates 946 --- 947 --- - Accepting a completion candidate using `<c-y>` applies side effects like 948 --- expanding snippets, text edits (e.g. insert import statements) and 949 --- executing associated commands. This works for completions triggered via 950 --- autotrigger, omnifunc or completion.get() 951 --- 952 --- Example: |lsp-attach| |lsp-completion| 953 --- 954 --- Note: the behavior of `autotrigger=true` is controlled by the LSP `triggerCharacters` field. You 955 --- can override it on LspAttach, see |lsp-autocompletion|. 956 --- 957 --- @param enable boolean True to enable, false to disable 958 --- @param client_id integer Client ID 959 --- @param bufnr integer Buffer handle, or 0 for the current buffer 960 --- @param opts? vim.lsp.completion.BufferOpts 961 function M.enable(enable, client_id, bufnr, opts) 962 bufnr = vim._resolve_bufnr(bufnr) 963 964 if enable then 965 enable_completions(client_id, bufnr, opts or {}) 966 else 967 disable_completions(client_id, bufnr) 968 end 969 end 970 971 --- @inlinedoc 972 --- @class vim.lsp.completion.get.Opts 973 --- @field ctx? lsp.CompletionContext Completion context. Defaults to a trigger kind of `invoked`. 974 975 --- Triggers LSP completion once in the current buffer, if LSP completion is enabled 976 --- (see |lsp-attach| |lsp-completion|). 977 --- 978 --- Used by the default LSP |omnicompletion| provider |vim.lsp.omnifunc()|, thus |i_CTRL-X_CTRL-O| 979 --- invokes this in LSP-enabled buffers. Use CTRL-Y to select an item from the completion menu. 980 --- |complete_CTRL-Y| 981 --- 982 --- To invoke manually with CTRL-space, use this mapping: 983 --- ```lua 984 --- -- Use CTRL-space to trigger LSP completion. 985 --- -- Use CTRL-Y to select an item. |complete_CTRL-Y| 986 --- vim.keymap.set('i', '<c-space>', function() 987 --- vim.lsp.completion.get() 988 --- end) 989 --- ``` 990 --- 991 --- @param opts? vim.lsp.completion.get.Opts 992 function M.get(opts) 993 opts = opts or {} 994 local ctx = opts.ctx or { triggerKind = protocol.CompletionTriggerKind.Invoked } 995 local bufnr = api.nvim_get_current_buf() 996 local clients = (buf_handles[bufnr] or {}).clients or {} 997 998 trigger(bufnr, clients, ctx) 999 end 1000 1001 --- Implements 'omnifunc' compatible LSP completion. 1002 --- 1003 --- @see |complete-functions| 1004 --- @see |complete-items| 1005 --- @see |CompleteDone| 1006 --- 1007 --- @param findstart integer 0 or 1, decides behavior 1008 --- @param base integer findstart=0, text to match against 1009 --- 1010 --- @return integer|table Decided by {findstart}: 1011 --- - findstart=0: column where the completion starts, or -2 or -3 1012 --- - findstart=1: list of matches (actually just calls |complete()|) 1013 function M._omnifunc(findstart, base) 1014 lsp.log.debug('omnifunc.findstart', { findstart = findstart, base = base }) 1015 assert(base) -- silence luals 1016 local bufnr = api.nvim_get_current_buf() 1017 local clients = lsp.get_clients({ bufnr = bufnr, method = 'textDocument/completion' }) 1018 local remaining = #clients 1019 if remaining == 0 then 1020 return findstart == 1 and -1 or {} 1021 end 1022 1023 trigger(bufnr, clients, { triggerKind = protocol.CompletionTriggerKind.Invoked }) 1024 1025 -- Return -2 to signal that we should continue completion so that we can 1026 -- async complete. 1027 return -2 1028 end 1029 1030 return M