Skip to content

Commit 012c773

Browse files
committed
feat(completion): add support for item defaults
1 parent d92a20c commit 012c773

File tree

4 files changed

+93
-2
lines changed

4 files changed

+93
-2
lines changed

CHANGELOG.md

+1
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@
3838
- FEATURE: both info and signature help windows now use tree-sitter highlighting:
3939
- Info window uses "markdown" parser (works best on Neovim>=0.10 as its parser is built-in). Special markdown characters are concealed (i.e. hidden) which might result into seemingly unnecessary whitespace as dimensions are computed not accounting for that.
4040
- Signature help uses same parser as in current filetype.
41+
- FEATURE: add support for item defaults in `CompletionList` response.
4142
- BREAKING FEATURE: rework how LSP completion items are converted to Neovim's completion items:
4243
- Show `detail` highlighted as buffer's language at the start of info window, but only if `detail` provides information not already present in `documentation`. It was previously used as extra text in the popup menu (via `menu` field), but this doesn't quite follow LSP specification: `detail` and `documentation` fields can be delayed up until `completionItem/resolve` request which implies they should be treated similarly.
4344
- Show `labelDetails` as a part of the popup menu via `menu` completion item field.

lua/mini/completion.lua

+27-1
Original file line numberDiff line numberDiff line change
@@ -427,9 +427,11 @@ MiniCompletion.completefunc_lsp = function(findstart, base)
427427
process_items = process_items or MiniCompletion.default_process_items
428428
local words = H.process_lsp_response(H.completion.lsp.result, function(response, client_id)
429429
is_incomplete = is_incomplete or response.isIncomplete
430-
-- Response can be `CompletionList` with 'items' field or `CompletionItem[]`
430+
-- Response can be `CompletionList` with 'items' field plus their
431+
-- defaults or `CompletionItem[]`
431432
local items = H.table_get(response, { 'items' }) or response
432433
if type(items) ~= 'table' then return {} end
434+
items = H.apply_item_defaults(items, response.itemDefaults)
433435
items = process_items(items, base)
434436
return H.lsp_completion_response_items_to_complete_items(items, client_id)
435437
end)
@@ -977,6 +979,30 @@ H.make_completion_request = function()
977979
H.completion.lsp.cancel_fun = cancel_fun
978980
end
979981

982+
H.apply_item_defaults = function(items, defaults)
983+
if type(defaults) ~= 'table' then return items end
984+
985+
local edit_range = defaults.editRange
986+
local has_edit_range = type(edit_range) == 'table'
987+
for _, item in ipairs(items) do
988+
item.commitCharacters = item.commitCharacters or defaults.commitCharacters
989+
item.data = item.data or defaults.data
990+
item.insertTextFormat = item.insertTextFormat or defaults.insertTextFormat
991+
item.insertTextMode = item.insertTextMode or defaults.insertTextMode
992+
if has_edit_range then
993+
item.textEdit = item.textEdit or {}
994+
-- Infer new text from `item.textEditText` designed for default edit case
995+
item.textEdit.newText = item.textEdit.newText or item.textEditText or item.label
996+
-- Default `editRange` is range (start+end) or insert+replace ranges
997+
item.textEdit.start = item.textEdit.start or edit_range.start
998+
item.textEdit['end'] = item.textEdit['end'] or edit_range['end']
999+
item.textEdit.insert = item.textEdit.insert or edit_range.insert
1000+
item.textEdit.replace = item.textEdit.replace or edit_range.replace
1001+
end
1002+
end
1003+
return items
1004+
end
1005+
9801006
-- Source:
9811007
-- https://microsoft.github.io/language-server-protocol/specifications/specification-3-14/#textDocument_completion
9821008
H.lsp_completion_response_items_to_complete_items = function(items, client_id)

tests/dir-completion/mock-months-lsp.lua

+3-1
Original file line numberDiff line numberDiff line change
@@ -123,10 +123,12 @@ Months.requests = {
123123

124124
if item.name == 'April' then
125125
res.textEdit = construct_textEdit(item.name, 'InsertReplaceEdit')
126+
res.textEditText = _G.mock_itemdefaults ~= nil and 'New April' or nil
126127
res.filterText = construct_filterText(item.name)
127128
end
128129
if item.name == 'August' then
129130
res.textEdit = construct_textEdit(item.name, 'textEdit')
131+
res.textEditText = _G.mock_itemdefaults ~= nil and 'New August' or nil
130132
res.filterText = construct_filterText(item.name)
131133
end
132134

@@ -136,7 +138,7 @@ Months.requests = {
136138
-- Mock incomplete computation
137139
if _G.mock_isincomplete then items = vim.list_slice(items, 1, 6) end
138140

139-
return { { result = { items = items, isIncomplete = _G.mock_isincomplete } } }
141+
return { { result = { items = items, isIncomplete = _G.mock_isincomplete, itemDefaults = _G.mock_itemdefaults } } }
140142
end,
141143

142144
['completionItem/resolve'] = function(params)

tests/test_completion.lua

+62
Original file line numberDiff line numberDiff line change
@@ -777,6 +777,68 @@ T['Manual completion']['respects `labelDetails` from LSP response'] = function()
777777
child.expect_screenshot()
778778
end
779779

780+
T['Manual completion']['respects `itemDefaults` from LSP response'] = function()
781+
local format_snippet = child.lua_get('vim.lsp.protocol.InsertTextFormat.Snippet')
782+
child.lua([[
783+
MiniCompletion.config.lsp_completion.process_items = function(items, _)
784+
_G.latest_items = vim.deepcopy(items)
785+
return items
786+
end
787+
]])
788+
789+
child.lua([[
790+
_G.mock_itemdefaults = {
791+
commitCharacters = { ')' },
792+
data = { hello = 'world' },
793+
insertTextFormat = vim.lsp.protocol.InsertTextFormat.Snippet,
794+
insertTextMode = 1,
795+
}
796+
]])
797+
798+
-- Mock with `editRange` as regular `Range`
799+
local edit_range = { start = { line = 0, character = 0 }, ['end'] = { line = 0, character = 1 } }
800+
child.lua('_G.mock_itemdefaults.editRange = ' .. vim.inspect(edit_range))
801+
802+
set_lines({})
803+
type_keys('i', '<C-Space>')
804+
805+
local items = child.lua_get('_G.latest_items')
806+
for _, item in ipairs(items) do
807+
eq(item.commitCharacters, { ')' })
808+
eq(item.data, { hello = 'world' })
809+
eq(item.insertTextFormat, format_snippet)
810+
eq(item.insertTextMode, 1)
811+
812+
eq(item.textEdit.newText, item.textEditText or item.label)
813+
eq(item.textEdit.start, edit_range.start)
814+
eq(item.textEdit['end'], edit_range['end'])
815+
end
816+
type_keys('<C-e>')
817+
child.lua('_G.latest_items = nil')
818+
819+
-- Mock with `editRange` as isnert+replace ranges and partial default data
820+
child.lua('_G.mock_itemdefaults.data = nil')
821+
edit_range = {
822+
replace = { start = { line = 0, character = 0 }, ['end'] = { line = 0, character = 1 } },
823+
insert = { start = { line = 0, character = 1 }, ['end'] = { line = 0, character = 2 } },
824+
}
825+
child.lua('_G.mock_itemdefaults.editRange = ' .. vim.inspect(edit_range))
826+
827+
type_keys('<C-Space>')
828+
items = child.lua_get('_G.latest_items')
829+
for _, item in ipairs(items) do
830+
eq(item.commitCharacters, { ')' })
831+
eq(item.data, nil)
832+
eq(item.insertTextFormat, format_snippet)
833+
eq(item.insertTextMode, 1)
834+
835+
eq(item.textEdit.newText, item.textEditText or item.label)
836+
eq(item.textEdit.insert, edit_range.insert)
837+
eq(item.textEdit.replace, edit_range.replace)
838+
end
839+
type_keys('<C-e>')
840+
end
841+
780842
T['Manual completion']['respects `kind_hlgroup` as item field'] = function()
781843
if child.fn.has('nvim-0.11') == 0 then MiniTest.skip('Kind highlighting is available on Neovim>=0.11') end
782844
child.set_size(10, 40)

0 commit comments

Comments
 (0)