Skip to content

Commit fe6932c

Browse files
committed
refactor(completion): prepare for adding snippet support
Details: - Store completion start to make it possible to manually remove inserted candidate before inserting snippet. Also track it during text edits. - Perform LSP related actions in CompleteDonePre only if completed item was from LSp server. Also more explicitly prefer resolved completion item over the one provided in original 'textDocument/completion'. - Compute info lines with fallback to `info` field from completed item. This will be used to show full snippet text as info if there is no `detail` or `doc` from LSP server.
1 parent 2df3f5d commit fe6932c

File tree

1 file changed

+51
-51
lines changed

1 file changed

+51
-51
lines changed

lua/mini/completion.lua

+51-51
Original file line numberDiff line numberDiff line change
@@ -390,7 +390,10 @@ MiniCompletion.completefunc_lsp = function(findstart, base)
390390
-- End completion and wait for LSP callback to re-trigger this
391391
return findstart == 1 and -3 or {}
392392
else
393-
if findstart == 1 then return H.get_completion_start(H.completion.lsp.result) end
393+
if findstart == 1 then
394+
H.completion.start_pos = H.get_completion_start(H.completion.lsp.result)
395+
return H.completion.start_pos[2]
396+
end
394397

395398
local process_items, is_incomplete = H.get_config().lsp_completion.process_items, false
396399
process_items = process_items or MiniCompletion.default_process_items
@@ -479,6 +482,7 @@ H.completion = {
479482
text_changed_id = 0,
480483
timer = vim.loop.new_timer(),
481484
lsp = { id = 0, status = nil, result = nil, cancel_fun = nil },
485+
start_pos = {},
482486
}
483487

484488
-- Cache for completion item info
@@ -697,8 +701,16 @@ H.on_completedonepre = function()
697701
-- visible and pressing keys first hides it with 'CompleteDonePre' event.
698702
if H.completion.lsp.status == 'received' then return end
699703

700-
-- Try to apply additional text edits
701-
H.apply_additional_text_edits()
704+
-- Do extra actions for LSP completion items
705+
local lsp_data = H.table_get(vim.v.completed_item, { 'user_data', 'nvim', 'lsp' })
706+
if lsp_data ~= nil then
707+
-- Prefer resolved item over the one from 'textDocument/completion'
708+
local resolved = (H.info.lsp.result or {})[lsp_data.client_id]
709+
local item = (resolved == nil or resolved.err) and lsp_data.completion_item or resolved.result
710+
711+
-- Try to apply additional text edits
712+
H.apply_additional_text_edits(item.additionalTextEdits, lsp_data.client_id)
713+
end
702714

703715
-- Stop processes
704716
MiniCompletion.stop({ 'completion', 'info' })
@@ -959,40 +971,30 @@ H.get_completion_word = function(item)
959971
return H.table_get(item, { 'textEdit', 'newText' }) or item.insertText or item.label or ''
960972
end
961973

962-
H.apply_additional_text_edits = function()
963-
-- Code originally.inspired by https://github.com/neovim/neovim/issues/12310
964-
965-
-- Try to get `additionalTextEdits`. First from 'completionItem/resolve';
966-
-- then - from selected item. The reason for this is inconsistency in how
967-
-- servers provide `additionTextEdits`: on 'textDocument/completion' or
968-
-- 'completionItem/resolve'.
969-
local resolve_data = H.process_lsp_response(H.info.lsp.result, function(response, client_id)
970-
-- Return nested table because this will be a second argument of
971-
-- `vim.list_extend()` and the whole inner table is a target value here.
972-
return { { edits = response.additionalTextEdits, client_id = client_id } }
973-
end)
974-
local edits, client_id
975-
if #resolve_data >= 1 then
976-
edits, client_id = resolve_data[1].edits, resolve_data[1].client_id
977-
else
978-
local lsp_data = H.table_get(vim.v.completed_item, { 'user_data', 'nvim', 'lsp' }) or {}
979-
edits = H.table_get(lsp_data, { 'completion_item', 'additionalTextEdits' })
980-
client_id = lsp_data.client_id
981-
end
982-
974+
H.apply_additional_text_edits = function(edits, client_id)
975+
-- Code originally inspired by https://github.com/neovim/neovim/issues/12310
983976
if edits == nil then return end
984977
client_id = client_id or 0
985978

986-
-- Use extmark to track relevant cursor position after text edits
979+
-- Prepare extmarks to track relevant positions after text edits
980+
local start_pos = H.completion.start_pos
981+
local start_extmark_id = vim.api.nvim_buf_set_extmark(0, H.ns_id, start_pos[1] - 1, start_pos[2], {})
982+
987983
local cur_pos = vim.api.nvim_win_get_cursor(0)
988-
local extmark_id = vim.api.nvim_buf_set_extmark(0, H.ns_id, cur_pos[1] - 1, cur_pos[2], {})
984+
local cursor_extmark_id = vim.api.nvim_buf_set_extmark(0, H.ns_id, cur_pos[1] - 1, cur_pos[2], {})
989985

986+
-- Do text edits
990987
local offset_encoding = vim.lsp.get_client_by_id(client_id).offset_encoding
991988
vim.lsp.util.apply_text_edits(edits, vim.api.nvim_get_current_buf(), offset_encoding)
992989

993-
local extmark_data = vim.api.nvim_buf_get_extmark_by_id(0, H.ns_id, extmark_id, {})
994-
pcall(vim.api.nvim_buf_del_extmark, 0, H.ns_id, extmark_id)
995-
pcall(vim.api.nvim_win_set_cursor, 0, { extmark_data[1] + 1, extmark_data[2] })
990+
-- Restore relevant positions
991+
local start_data = vim.api.nvim_buf_get_extmark_by_id(0, H.ns_id, start_extmark_id, {})
992+
H.completion.start_pos = { start_data[1] + 1, start_data[2] }
993+
pcall(vim.api.nvim_buf_del_extmark, 0, H.ns_id, start_extmark_id)
994+
995+
local cursor_data = vim.api.nvim_buf_get_extmark_by_id(0, H.ns_id, cursor_extmark_id, {})
996+
pcall(vim.api.nvim_win_set_cursor, 0, { cursor_data[1] + 1, cursor_data[2] })
997+
pcall(vim.api.nvim_buf_del_extmark, 0, H.ns_id, cursor_extmark_id)
996998
end
997999

9981000
-- Completion item info -------------------------------------------------------
@@ -1038,29 +1040,23 @@ end
10381040

10391041
H.info_window_lines = function(info_id)
10401042
local completed_item = H.table_get(H.info, { 'event', 'completed_item' }) or {}
1043+
local lsp_data = H.table_get(completed_item, { 'user_data', 'nvim', 'lsp' })
1044+
local info = completed_item.info or ''
10411045

10421046
-- If popup is not from LSP, try using 'info' field of completion item
1043-
if H.completion.source ~= 'lsp' then
1044-
local text = completed_item.info or ''
1045-
return (not H.is_whitespace(text)) and vim.split(text, '\n') or nil
1046-
end
1047+
if lsp_data == nil then return vim.split(info, '\n') end
10471048

1048-
-- Try to get documentation from LSP's latest completion result
1049+
-- Try to get documentation from LSP's latest resolved info
10491050
if H.info.lsp.status == 'received' then
1050-
local lines = H.process_lsp_response(H.info.lsp.result, H.normalize_item_doc)
1051+
local lines = H.process_lsp_response(H.info.lsp.result, function(x) return H.normalize_item_doc(x, info) end)
10511052
H.info.lsp.status = 'done'
10521053
return lines
10531054
end
10541055

10551056
-- If server doesn't support resolving completion item, reuse first response
1056-
local lsp_data = H.table_get(completed_item, { 'user_data', 'nvim', 'lsp' })
1057-
-- NOTE: If there is no LSP's completion item, then there is no point to
1058-
-- proceed as it should serve as parameters to LSP request
1059-
if lsp_data.completion_item == nil then return end
1060-
10611057
local client = vim.lsp.get_client_by_id(lsp_data.client_id) or {}
10621058
local can_resolve = H.table_get(client.server_capabilities, { 'completionProvider', 'resolveProvider' })
1063-
if not can_resolve then return H.normalize_item_doc(lsp_data.completion_item) end
1059+
if not can_resolve then return H.normalize_item_doc(lsp_data.completion_item, info) end
10641060

10651061
-- Finally, request to resolve current completion to add more documentation
10661062
local bufnr = vim.api.nvim_get_current_buf()
@@ -1405,17 +1401,16 @@ end
14051401
H.pumvisible = function() return vim.fn.pumvisible() > 0 end
14061402

14071403
H.get_completion_start = function(lsp_result)
1408-
local pos = vim.api.nvim_win_get_cursor(0)
1409-
14101404
-- Prefer completion start from LSP response(s)
14111405
for _, response_data in pairs(lsp_result or {}) do
1412-
local server_start = H.get_completion_start_server(response_data, pos[1] - 1)
1406+
local server_start = H.get_completion_start_server(response_data)
14131407
if server_start ~= nil then return server_start end
14141408
end
14151409

14161410
-- Fall back to start position of latest keyword
1411+
local pos = vim.api.nvim_win_get_cursor(0)
14171412
local line = vim.api.nvim_get_current_line()
1418-
return vim.fn.match(line:sub(1, pos[2]), '\\k*$')
1413+
return { pos[1], vim.fn.match(line:sub(1, pos[2]), '\\k*$') }
14191414
end
14201415

14211416
H.get_completion_start_server = function(response_data, line_num)
@@ -1426,7 +1421,7 @@ H.get_completion_start_server = function(response_data, line_num)
14261421
-- NOTE: As per LSP spec, `textEdit` can be either `TextEdit` or `InsertReplaceEdit`
14271422
local range = type(item.textEdit.range) == 'table' and item.textEdit.range or item.textEdit.insert
14281423
-- NOTE: Return immediately, ignoring possibly several conflicting starts
1429-
return range.start.character
1424+
return { range.start.line + 1, range.start.character }
14301425
end
14311426
end
14321427
end
@@ -1498,17 +1493,19 @@ H.map = function(mode, lhs, rhs, opts)
14981493
vim.keymap.set(mode, lhs, rhs, opts)
14991494
end
15001495

1501-
H.normalize_item_doc = function(completion_item)
1496+
H.normalize_item_doc = function(completion_item, fallback_info)
15021497
local detail, doc = completion_item.detail, completion_item.documentation
1498+
-- Fall back to explicit info only of there is no data in completion item
1499+
-- Assume that explicit info is a code that needs highlighting
1500+
detail = (detail == nil and doc == nil) and fallback_info or detail
15031501
if detail == nil and doc == nil then return {} end
15041502

15051503
-- Extract string content. Treat markdown and plain kinds the same.
15061504
-- Show both `detail` and `documentation` if the first provides new info.
15071505
detail, doc = detail or '', (type(doc) == 'table' and doc.value or doc) or ''
1508-
detail = (H.is_whitespace(detail) or doc:find(detail, 1, true) ~= nil) and ''
1509-
-- Wrap details in language's code block to (usually) improve highlighting
1510-
-- This approach seems to work in 'hrsh7th/nvim-cmp'
1511-
or string.format('```%s\n%s\n```\n', vim.bo.filetype:match('^[^%.]*'), vim.trim(detail))
1506+
-- Wrap details in language's code block to (usually) improve highlighting
1507+
-- This approach seems to work in 'hrsh7th/nvim-cmp'
1508+
detail = (H.is_whitespace(detail) or doc:find(detail, 1, true) ~= nil) and '' or (H.wrap_in_codeblock(detail) .. '\n')
15121509
local text = detail .. doc
15131510

15141511
-- Ensure consistent line separators
@@ -1520,9 +1517,12 @@ H.normalize_item_doc = function(completion_item)
15201517
-- Remove padding around code blocks as they are concealed and appear empty
15211518
text = text:gsub('\n*(\n```%S+\n)', '%1'):gsub('(\n```\n?)\n*', '%1')
15221519

1520+
if text == '' and fallback_info ~= '' then text = H.wrap_in_codeblock(fallback_info) end
15231521
return text == '' and {} or vim.split(text, '\n')
15241522
end
15251523

1524+
H.wrap_in_codeblock = function(x) return string.format('```%s\n%s\n```', vim.bo.filetype:match('^[^%.]*'), vim.trim(x)) end
1525+
15261526
-- TODO: Remove after compatibility with Neovim=0.9 is dropped
15271527
H.islist = vim.fn.has('nvim-0.10') == 1 and vim.islist or vim.tbl_islist
15281528

0 commit comments

Comments
 (0)