completion.lua 23 KB

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