completion.lua 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789
  1. local M = {}
  2. local api = vim.api
  3. local lsp = vim.lsp
  4. local protocol = lsp.protocol
  5. local ms = protocol.Methods
  6. local rtt_ms = 50
  7. local ns_to_ms = 0.000001
  8. --- @alias vim.lsp.CompletionResult lsp.CompletionList | lsp.CompletionItem[]
  9. -- TODO(mariasolos): Remove this declaration once we figure out a better way to handle
  10. -- literal/anonymous types (see https://github.com/neovim/neovim/pull/27542/files#r1495259331).
  11. --- @nodoc
  12. --- @class lsp.ItemDefaults
  13. --- @field editRange lsp.Range | { insert: lsp.Range, replace: lsp.Range } | nil
  14. --- @field insertTextFormat lsp.InsertTextFormat?
  15. --- @field insertTextMode lsp.InsertTextMode?
  16. --- @field data any
  17. --- @nodoc
  18. --- @class vim.lsp.completion.BufHandle
  19. --- @field clients table<integer, vim.lsp.Client>
  20. --- @field triggers table<string, vim.lsp.Client[]>
  21. --- @field convert? fun(item: lsp.CompletionItem): table
  22. --- @type table<integer, vim.lsp.completion.BufHandle>
  23. local buf_handles = {}
  24. --- @nodoc
  25. --- @class vim.lsp.completion.Context
  26. local Context = {
  27. cursor = nil, --- @type [integer, integer]?
  28. last_request_time = nil, --- @type integer?
  29. pending_requests = {}, --- @type function[]
  30. isIncomplete = false,
  31. }
  32. --- @nodoc
  33. function Context:cancel_pending()
  34. for _, cancel in ipairs(self.pending_requests) do
  35. cancel()
  36. end
  37. self.pending_requests = {}
  38. end
  39. --- @nodoc
  40. function Context:reset()
  41. -- Note that the cursor isn't reset here, it needs to survive a `CompleteDone` event.
  42. self.isIncomplete = false
  43. self.last_request_time = nil
  44. self:cancel_pending()
  45. end
  46. --- @type uv.uv_timer_t?
  47. local completion_timer = nil
  48. --- @return uv.uv_timer_t
  49. local function new_timer()
  50. return assert(vim.uv.new_timer())
  51. end
  52. local function reset_timer()
  53. if completion_timer then
  54. completion_timer:stop()
  55. completion_timer:close()
  56. end
  57. completion_timer = nil
  58. end
  59. --- @param window integer
  60. --- @param warmup integer
  61. --- @return fun(sample: number): number
  62. local function exp_avg(window, warmup)
  63. local count = 0
  64. local sum = 0
  65. local value = 0
  66. return function(sample)
  67. if count < warmup then
  68. count = count + 1
  69. sum = sum + sample
  70. value = sum / count
  71. else
  72. local factor = 2.0 / (window + 1)
  73. value = value * (1 - factor) + sample * factor
  74. end
  75. return value
  76. end
  77. end
  78. local compute_new_average = exp_avg(10, 10)
  79. --- @return number
  80. local function next_debounce()
  81. if not Context.last_request_time then
  82. return rtt_ms
  83. end
  84. local ms_since_request = (vim.uv.hrtime() - Context.last_request_time) * ns_to_ms
  85. return math.max((ms_since_request - rtt_ms) * -1, 0)
  86. end
  87. --- @param input string Unparsed snippet
  88. --- @return string # Parsed snippet if successful, else returns its input
  89. local function parse_snippet(input)
  90. local ok, parsed = pcall(function()
  91. return lsp._snippet_grammar.parse(input)
  92. end)
  93. return ok and tostring(parsed) or input
  94. end
  95. --- @param item lsp.CompletionItem
  96. local function apply_snippet(item)
  97. if item.textEdit then
  98. vim.snippet.expand(item.textEdit.newText)
  99. elseif item.insertText then
  100. vim.snippet.expand(item.insertText)
  101. end
  102. end
  103. --- Returns text that should be inserted when a selecting completion item. The
  104. --- precedence is as follows: textEdit.newText > insertText > label
  105. ---
  106. --- See https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_completion
  107. ---
  108. --- @param item lsp.CompletionItem
  109. --- @param prefix string
  110. --- @param match fun(text: string, prefix: string):boolean
  111. --- @return string
  112. local function get_completion_word(item, prefix, match)
  113. if item.insertTextFormat == protocol.InsertTextFormat.Snippet then
  114. if item.textEdit or (item.insertText and item.insertText ~= '') then
  115. -- Use label instead of text if text has different starting characters.
  116. -- label is used as abbr (=displayed), but word is used for filtering
  117. -- This is required for things like postfix completion.
  118. -- E.g. in lua:
  119. --
  120. -- local f = {}
  121. -- f@|
  122. -- ▲
  123. -- └─ cursor
  124. --
  125. -- item.textEdit.newText: table.insert(f, $0)
  126. -- label: insert
  127. --
  128. -- Typing `i` would remove the candidate because newText starts with `t`.
  129. local text = parse_snippet(item.insertText or item.textEdit.newText)
  130. local word = #text < #item.label and vim.fn.matchstr(text, '\\k*') or item.label
  131. if item.filterText and not match(word, prefix) then
  132. return item.filterText
  133. else
  134. return word
  135. end
  136. else
  137. return item.label
  138. end
  139. elseif item.textEdit then
  140. local word = item.textEdit.newText
  141. return word:match('^(%S*)') or word
  142. elseif item.insertText and item.insertText ~= '' then
  143. return item.insertText
  144. end
  145. return item.label
  146. end
  147. --- Applies the given defaults to the completion item, modifying it in place.
  148. ---
  149. --- @param item lsp.CompletionItem
  150. --- @param defaults lsp.ItemDefaults?
  151. local function apply_defaults(item, defaults)
  152. if not defaults then
  153. return
  154. end
  155. item.insertTextFormat = item.insertTextFormat or defaults.insertTextFormat
  156. item.insertTextMode = item.insertTextMode or defaults.insertTextMode
  157. item.data = item.data or defaults.data
  158. if defaults.editRange then
  159. local textEdit = item.textEdit or {}
  160. item.textEdit = textEdit
  161. textEdit.newText = textEdit.newText or item.textEditText or item.insertText or item.label
  162. if defaults.editRange.start then
  163. textEdit.range = textEdit.range or defaults.editRange
  164. elseif defaults.editRange.insert then
  165. textEdit.insert = defaults.editRange.insert
  166. textEdit.replace = defaults.editRange.replace
  167. end
  168. end
  169. end
  170. --- @param result vim.lsp.CompletionResult
  171. --- @return lsp.CompletionItem[]
  172. local function get_items(result)
  173. if result.items then
  174. -- When we have a list, apply the defaults and return an array of items.
  175. for _, item in ipairs(result.items) do
  176. ---@diagnostic disable-next-line: param-type-mismatch
  177. apply_defaults(item, result.itemDefaults)
  178. end
  179. return result.items
  180. else
  181. -- Else just return the items as they are.
  182. return result
  183. end
  184. end
  185. ---@param item lsp.CompletionItem
  186. ---@return string
  187. local function get_doc(item)
  188. local doc = item.documentation
  189. if not doc then
  190. return ''
  191. end
  192. if type(doc) == 'string' then
  193. return doc
  194. end
  195. if type(doc) == 'table' and type(doc.value) == 'string' then
  196. return doc.value
  197. end
  198. vim.notify('invalid documentation value: ' .. vim.inspect(doc), vim.log.levels.WARN)
  199. return ''
  200. end
  201. ---@param value string
  202. ---@param prefix string
  203. ---@return boolean
  204. local function match_item_by_value(value, prefix)
  205. if prefix == '' then
  206. return true
  207. end
  208. if vim.o.completeopt:find('fuzzy') ~= nil then
  209. return next(vim.fn.matchfuzzy({ value }, prefix)) ~= nil
  210. end
  211. if vim.o.ignorecase and (not vim.o.smartcase or not prefix:find('%u')) then
  212. return vim.startswith(value:lower(), prefix:lower())
  213. end
  214. return vim.startswith(value, prefix)
  215. end
  216. --- Turns the result of a `textDocument/completion` request into vim-compatible
  217. --- |complete-items|.
  218. ---
  219. --- @private
  220. --- @param result vim.lsp.CompletionResult Result of `textDocument/completion`
  221. --- @param prefix string prefix to filter the completion items
  222. --- @param client_id integer? Client ID
  223. --- @return table[]
  224. --- @see complete-items
  225. function M._lsp_to_complete_items(result, prefix, client_id)
  226. local items = get_items(result)
  227. if vim.tbl_isempty(items) then
  228. return {}
  229. end
  230. ---@type fun(item: lsp.CompletionItem):boolean
  231. local matches
  232. if not prefix:find('%w') then
  233. matches = function(_)
  234. return true
  235. end
  236. else
  237. ---@param item lsp.CompletionItem
  238. matches = function(item)
  239. if item.filterText then
  240. return match_item_by_value(item.filterText, prefix)
  241. end
  242. if item.textEdit then
  243. -- server took care of filtering
  244. return true
  245. end
  246. return match_item_by_value(item.label, prefix)
  247. end
  248. end
  249. local candidates = {}
  250. local bufnr = api.nvim_get_current_buf()
  251. local user_convert = vim.tbl_get(buf_handles, bufnr, 'convert')
  252. for _, item in ipairs(items) do
  253. if matches(item) then
  254. local word = get_completion_word(item, prefix, match_item_by_value)
  255. local hl_group = ''
  256. if
  257. item.deprecated
  258. or vim.list_contains((item.tags or {}), protocol.CompletionTag.Deprecated)
  259. then
  260. hl_group = 'DiagnosticDeprecated'
  261. end
  262. local completion_item = {
  263. word = word,
  264. abbr = item.label,
  265. kind = protocol.CompletionItemKind[item.kind] or 'Unknown',
  266. menu = item.detail or '',
  267. info = get_doc(item),
  268. icase = 1,
  269. dup = 1,
  270. empty = 1,
  271. abbr_hlgroup = hl_group,
  272. user_data = {
  273. nvim = {
  274. lsp = {
  275. completion_item = item,
  276. client_id = client_id,
  277. },
  278. },
  279. },
  280. }
  281. if user_convert then
  282. completion_item = vim.tbl_extend('keep', user_convert(item), completion_item)
  283. end
  284. table.insert(candidates, completion_item)
  285. end
  286. end
  287. ---@diagnostic disable-next-line: no-unknown
  288. table.sort(candidates, function(a, b)
  289. ---@type lsp.CompletionItem
  290. local itema = a.user_data.nvim.lsp.completion_item
  291. ---@type lsp.CompletionItem
  292. local itemb = b.user_data.nvim.lsp.completion_item
  293. return (itema.sortText or itema.label) < (itemb.sortText or itemb.label)
  294. end)
  295. return candidates
  296. end
  297. --- @param lnum integer 0-indexed
  298. --- @param line string
  299. --- @param items lsp.CompletionItem[]
  300. --- @param encoding string
  301. --- @return integer?
  302. local function adjust_start_col(lnum, line, items, encoding)
  303. local min_start_char = nil
  304. for _, item in pairs(items) do
  305. if item.textEdit and item.textEdit.range.start.line == lnum then
  306. if min_start_char and min_start_char ~= item.textEdit.range.start.character then
  307. return nil
  308. end
  309. min_start_char = item.textEdit.range.start.character
  310. end
  311. end
  312. if min_start_char then
  313. return vim.str_byteindex(line, encoding, min_start_char, false)
  314. else
  315. return nil
  316. end
  317. end
  318. --- @private
  319. --- @param line string line content
  320. --- @param lnum integer 0-indexed line number
  321. --- @param cursor_col integer
  322. --- @param client_id integer client ID
  323. --- @param client_start_boundary integer 0-indexed word boundary
  324. --- @param server_start_boundary? integer 0-indexed word boundary, based on textEdit.range.start.character
  325. --- @param result vim.lsp.CompletionResult
  326. --- @param encoding string
  327. --- @return table[] matches
  328. --- @return integer? server_start_boundary
  329. function M._convert_results(
  330. line,
  331. lnum,
  332. cursor_col,
  333. client_id,
  334. client_start_boundary,
  335. server_start_boundary,
  336. result,
  337. encoding
  338. )
  339. -- Completion response items may be relative to a position different than `client_start_boundary`.
  340. -- Concrete example, with lua-language-server:
  341. --
  342. -- require('plenary.asy|
  343. -- ▲ ▲ ▲
  344. -- │ │ └── cursor_pos: 20
  345. -- │ └────── client_start_boundary: 17
  346. -- └────────────── textEdit.range.start.character: 9
  347. -- .newText = 'plenary.async'
  348. -- ^^^
  349. -- prefix (We'd remove everything not starting with `asy`,
  350. -- so we'd eliminate the `plenary.async` result
  351. --
  352. -- `adjust_start_col` is used to prefer the language server boundary.
  353. --
  354. local candidates = get_items(result)
  355. local curstartbyte = adjust_start_col(lnum, line, candidates, encoding)
  356. if server_start_boundary == nil then
  357. server_start_boundary = curstartbyte
  358. elseif curstartbyte ~= nil and curstartbyte ~= server_start_boundary then
  359. server_start_boundary = client_start_boundary
  360. end
  361. local prefix = line:sub((server_start_boundary or client_start_boundary) + 1, cursor_col)
  362. local matches = M._lsp_to_complete_items(result, prefix, client_id)
  363. return matches, server_start_boundary
  364. end
  365. --- @param clients table<integer, vim.lsp.Client> # keys != client_id
  366. --- @param bufnr integer
  367. --- @param win integer
  368. --- @param callback fun(responses: table<integer, { err: lsp.ResponseError, result: vim.lsp.CompletionResult }>)
  369. --- @return function # Cancellation function
  370. local function request(clients, bufnr, win, callback)
  371. local responses = {} --- @type table<integer, { err: lsp.ResponseError, result: any }>
  372. local request_ids = {} --- @type table<integer, integer>
  373. local remaining_requests = vim.tbl_count(clients)
  374. for _, client in pairs(clients) do
  375. local client_id = client.id
  376. local params = lsp.util.make_position_params(win, client.offset_encoding)
  377. local ok, request_id = client:request(ms.textDocument_completion, params, function(err, result)
  378. responses[client_id] = { err = err, result = result }
  379. remaining_requests = remaining_requests - 1
  380. if remaining_requests == 0 then
  381. callback(responses)
  382. end
  383. end, bufnr)
  384. if ok then
  385. request_ids[client_id] = request_id
  386. end
  387. end
  388. return function()
  389. for client_id, request_id in pairs(request_ids) do
  390. local client = lsp.get_client_by_id(client_id)
  391. if client then
  392. client:cancel_request(request_id)
  393. end
  394. end
  395. end
  396. end
  397. local function trigger(bufnr, clients)
  398. reset_timer()
  399. Context:cancel_pending()
  400. if tonumber(vim.fn.pumvisible()) == 1 and not Context.isIncomplete then
  401. return
  402. end
  403. local win = api.nvim_get_current_win()
  404. local cursor_row, cursor_col = unpack(api.nvim_win_get_cursor(win)) --- @type integer, integer
  405. local line = api.nvim_get_current_line()
  406. local line_to_cursor = line:sub(1, cursor_col)
  407. local word_boundary = vim.fn.match(line_to_cursor, '\\k*$')
  408. local start_time = vim.uv.hrtime()
  409. Context.last_request_time = start_time
  410. local cancel_request = request(clients, bufnr, win, function(responses)
  411. local end_time = vim.uv.hrtime()
  412. rtt_ms = compute_new_average((end_time - start_time) * ns_to_ms)
  413. Context.pending_requests = {}
  414. Context.isIncomplete = false
  415. local row_changed = api.nvim_win_get_cursor(win)[1] ~= cursor_row
  416. local mode = api.nvim_get_mode().mode
  417. if row_changed or not (mode == 'i' or mode == 'ic') then
  418. return
  419. end
  420. local matches = {}
  421. local server_start_boundary --- @type integer?
  422. for client_id, response in pairs(responses) do
  423. if response.err then
  424. vim.notify_once(response.err.message, vim.log.levels.WARN)
  425. end
  426. local result = response.result
  427. if result then
  428. Context.isIncomplete = Context.isIncomplete or result.isIncomplete
  429. local client = lsp.get_client_by_id(client_id)
  430. local encoding = client and client.offset_encoding or 'utf-16'
  431. local client_matches
  432. client_matches, server_start_boundary = M._convert_results(
  433. line,
  434. cursor_row - 1,
  435. cursor_col,
  436. client_id,
  437. word_boundary,
  438. nil,
  439. result,
  440. encoding
  441. )
  442. vim.list_extend(matches, client_matches)
  443. end
  444. end
  445. local start_col = (server_start_boundary or word_boundary) + 1
  446. Context.cursor = { cursor_row, start_col }
  447. vim.fn.complete(start_col, matches)
  448. end)
  449. table.insert(Context.pending_requests, cancel_request)
  450. end
  451. --- @param handle vim.lsp.completion.BufHandle
  452. local function on_insert_char_pre(handle)
  453. if tonumber(vim.fn.pumvisible()) == 1 then
  454. if Context.isIncomplete then
  455. reset_timer()
  456. local debounce_ms = next_debounce()
  457. if debounce_ms == 0 then
  458. vim.schedule(M.trigger)
  459. else
  460. completion_timer = new_timer()
  461. completion_timer:start(debounce_ms, 0, vim.schedule_wrap(M.trigger))
  462. end
  463. end
  464. return
  465. end
  466. local char = api.nvim_get_vvar('char')
  467. local matched_clients = handle.triggers[char]
  468. if not completion_timer and matched_clients then
  469. completion_timer = assert(vim.uv.new_timer())
  470. completion_timer:start(25, 0, function()
  471. reset_timer()
  472. vim.schedule(function()
  473. trigger(api.nvim_get_current_buf(), matched_clients)
  474. end)
  475. end)
  476. end
  477. end
  478. local function on_insert_leave()
  479. reset_timer()
  480. Context.cursor = nil
  481. Context:reset()
  482. end
  483. local function on_complete_done()
  484. local completed_item = api.nvim_get_vvar('completed_item')
  485. if not completed_item or not completed_item.user_data or not completed_item.user_data.nvim then
  486. Context:reset()
  487. return
  488. end
  489. local cursor_row, cursor_col = unpack(api.nvim_win_get_cursor(0)) --- @type integer, integer
  490. cursor_row = cursor_row - 1
  491. local completion_item = completed_item.user_data.nvim.lsp.completion_item --- @type lsp.CompletionItem
  492. local client_id = completed_item.user_data.nvim.lsp.client_id --- @type integer
  493. if not completion_item or not client_id then
  494. Context:reset()
  495. return
  496. end
  497. local bufnr = api.nvim_get_current_buf()
  498. local expand_snippet = completion_item.insertTextFormat == protocol.InsertTextFormat.Snippet
  499. and (completion_item.textEdit ~= nil or completion_item.insertText ~= nil)
  500. Context:reset()
  501. local client = lsp.get_client_by_id(client_id)
  502. if not client then
  503. return
  504. end
  505. local position_encoding = client.offset_encoding or 'utf-16'
  506. local resolve_provider = (client.server_capabilities.completionProvider or {}).resolveProvider
  507. local function clear_word()
  508. if not expand_snippet then
  509. return nil
  510. end
  511. -- Remove the already inserted word.
  512. api.nvim_buf_set_text(
  513. bufnr,
  514. Context.cursor[1] - 1,
  515. Context.cursor[2] - 1,
  516. cursor_row,
  517. cursor_col,
  518. { '' }
  519. )
  520. end
  521. local function apply_snippet_and_command()
  522. if expand_snippet then
  523. apply_snippet(completion_item)
  524. end
  525. local command = completion_item.command
  526. if command then
  527. client:exec_cmd(command, { bufnr = bufnr })
  528. end
  529. end
  530. if completion_item.additionalTextEdits and next(completion_item.additionalTextEdits) then
  531. clear_word()
  532. lsp.util.apply_text_edits(completion_item.additionalTextEdits, bufnr, position_encoding)
  533. apply_snippet_and_command()
  534. elseif resolve_provider and type(completion_item) == 'table' then
  535. local changedtick = vim.b[bufnr].changedtick
  536. --- @param result lsp.CompletionItem
  537. client:request(ms.completionItem_resolve, completion_item, function(err, result)
  538. if changedtick ~= vim.b[bufnr].changedtick then
  539. return
  540. end
  541. clear_word()
  542. if err then
  543. vim.notify_once(err.message, vim.log.levels.WARN)
  544. elseif result then
  545. if result.additionalTextEdits then
  546. lsp.util.apply_text_edits(result.additionalTextEdits, bufnr, position_encoding)
  547. end
  548. if result.command then
  549. completion_item.command = result.command
  550. end
  551. end
  552. apply_snippet_and_command()
  553. end, bufnr)
  554. else
  555. clear_word()
  556. apply_snippet_and_command()
  557. end
  558. end
  559. ---@param bufnr integer
  560. ---@return string
  561. local function get_augroup(bufnr)
  562. return string.format('nvim.lsp.completion_%d', bufnr)
  563. end
  564. --- @class vim.lsp.completion.BufferOpts
  565. --- @field autotrigger? boolean Default: false When true, completion triggers automatically based on the server's `triggerCharacters`.
  566. --- @field convert? fun(item: lsp.CompletionItem): table Transforms an LSP CompletionItem to |complete-items|.
  567. ---@param client_id integer
  568. ---@param bufnr integer
  569. ---@param opts vim.lsp.completion.BufferOpts
  570. local function enable_completions(client_id, bufnr, opts)
  571. local buf_handle = buf_handles[bufnr]
  572. if not buf_handle then
  573. buf_handle = { clients = {}, triggers = {}, convert = opts.convert }
  574. buf_handles[bufnr] = buf_handle
  575. -- Attach to buffer events.
  576. api.nvim_buf_attach(bufnr, false, {
  577. on_detach = function(_, buf)
  578. buf_handles[buf] = nil
  579. end,
  580. on_reload = function(_, buf)
  581. M.enable(true, client_id, buf, opts)
  582. end,
  583. })
  584. -- Set up autocommands.
  585. local group = api.nvim_create_augroup(get_augroup(bufnr), { clear = true })
  586. api.nvim_create_autocmd('CompleteDone', {
  587. group = group,
  588. buffer = bufnr,
  589. callback = function()
  590. local reason = api.nvim_get_vvar('event').reason --- @type string
  591. if reason == 'accept' then
  592. on_complete_done()
  593. end
  594. end,
  595. })
  596. if opts.autotrigger then
  597. api.nvim_create_autocmd('InsertCharPre', {
  598. group = group,
  599. buffer = bufnr,
  600. callback = function()
  601. on_insert_char_pre(buf_handles[bufnr])
  602. end,
  603. })
  604. api.nvim_create_autocmd('InsertLeave', {
  605. group = group,
  606. buffer = bufnr,
  607. callback = on_insert_leave,
  608. })
  609. end
  610. end
  611. if not buf_handle.clients[client_id] then
  612. local client = lsp.get_client_by_id(client_id)
  613. assert(client, 'invalid client ID')
  614. -- Add the new client to the buffer's clients.
  615. buf_handle.clients[client_id] = client
  616. -- Add the new client to the clients that should be triggered by its trigger characters.
  617. --- @type string[]
  618. local triggers = vim.tbl_get(
  619. client.server_capabilities,
  620. 'completionProvider',
  621. 'triggerCharacters'
  622. ) or {}
  623. for _, char in ipairs(triggers) do
  624. local clients_for_trigger = buf_handle.triggers[char]
  625. if not clients_for_trigger then
  626. clients_for_trigger = {}
  627. buf_handle.triggers[char] = clients_for_trigger
  628. end
  629. local client_exists = vim.iter(clients_for_trigger):any(function(c)
  630. return c.id == client_id
  631. end)
  632. if not client_exists then
  633. table.insert(clients_for_trigger, client)
  634. end
  635. end
  636. end
  637. end
  638. --- @param client_id integer
  639. --- @param bufnr integer
  640. local function disable_completions(client_id, bufnr)
  641. local handle = buf_handles[bufnr]
  642. if not handle then
  643. return
  644. end
  645. handle.clients[client_id] = nil
  646. if not next(handle.clients) then
  647. buf_handles[bufnr] = nil
  648. api.nvim_del_augroup_by_name(get_augroup(bufnr))
  649. else
  650. for char, clients in pairs(handle.triggers) do
  651. --- @param c vim.lsp.Client
  652. handle.triggers[char] = vim.tbl_filter(function(c)
  653. return c.id ~= client_id
  654. end, clients)
  655. end
  656. end
  657. end
  658. --- Enables or disables completions from the given language client in the given buffer.
  659. ---
  660. --- @param enable boolean True to enable, false to disable
  661. --- @param client_id integer Client ID
  662. --- @param bufnr integer Buffer handle, or 0 for the current buffer
  663. --- @param opts? vim.lsp.completion.BufferOpts
  664. function M.enable(enable, client_id, bufnr, opts)
  665. bufnr = vim._resolve_bufnr(bufnr)
  666. if enable then
  667. enable_completions(client_id, bufnr, opts or {})
  668. else
  669. disable_completions(client_id, bufnr)
  670. end
  671. end
  672. --- Trigger LSP completion in the current buffer.
  673. function M.trigger()
  674. local bufnr = api.nvim_get_current_buf()
  675. local clients = (buf_handles[bufnr] or {}).clients or {}
  676. trigger(bufnr, clients)
  677. end
  678. --- Implements 'omnifunc' compatible LSP completion.
  679. ---
  680. --- @see |complete-functions|
  681. --- @see |complete-items|
  682. --- @see |CompleteDone|
  683. ---
  684. --- @param findstart integer 0 or 1, decides behavior
  685. --- @param base integer findstart=0, text to match against
  686. ---
  687. --- @return integer|table Decided by {findstart}:
  688. --- - findstart=0: column where the completion starts, or -2 or -3
  689. --- - findstart=1: list of matches (actually just calls |complete()|)
  690. function M._omnifunc(findstart, base)
  691. vim.lsp.log.debug('omnifunc.findstart', { findstart = findstart, base = base })
  692. assert(base) -- silence luals
  693. local bufnr = api.nvim_get_current_buf()
  694. local clients = lsp.get_clients({ bufnr = bufnr, method = ms.textDocument_completion })
  695. local remaining = #clients
  696. if remaining == 0 then
  697. return findstart == 1 and -1 or {}
  698. end
  699. trigger(bufnr, clients)
  700. -- Return -2 to signal that we should continue completion so that we can
  701. -- async complete.
  702. return -2
  703. end
  704. return M