21
21
--- LSP client (via 'textDocument/completion' request). Custom
22
22
--- preprocessing of response items is possible (with
23
23
--- `MiniCompletion.config.lsp_completion.process_items`), for example
24
- --- with fuzzy matching. By default items which are not snippets and
25
- --- directly start with completed word are kept and sorted according to
26
- --- LSP specification. Supports `additionalTextEdits`, like auto-import
27
- --- and others (see 'Notes' ).
24
+ --- with fuzzy matching. By default items directly starting with completed
25
+ --- word are kept and are sorted according to LSP specification.
26
+ --- Supports `additionalTextEdits`, like auto-import and others (see 'Notes'),
27
+ --- and snippet items (best results require |mini.snippets| dependency ).
28
28
--- - If first stage is not set up or resulted into no candidates, fallback
29
29
--- action is executed. The most tested actions are Neovim's built-in
30
30
--- insert completion (see |ins-completion|).
48
48
--- (same meaning as in |complete-items|) to items.
49
49
---
50
50
--- What it doesn't do:
51
- --- - Snippet expansion.
52
51
--- - Many configurable sources.
53
52
--- - Automatic mapping of `<CR>`, `<Tab>`, etc., as those tend to have highly
54
53
--- variable user expectations. See 'Helpful mappings' for suggestions.
58
57
--- Suggested dependencies (provide extra functionality, will work without them):
59
58
---
60
59
--- - Enabled |MiniIcons| module to highlight LSP kind (requires Neovim>=0.11).
61
- --- Otherwise |MiniCompletion.default_process_items()| does not add highlighting.
60
+ --- If absent, |MiniCompletion.default_process_items()| does not add highlighting.
62
61
--- Also take a look at |MiniIcons.tweak_lsp_kind()|.
62
+ --- - Enabled |MiniSnippets| module for better handling of snippet items.
63
+ --- If absent and custom snippet handling is not configured, |vim.snippet.expand()|
64
+ --- is used on Neovim>=0.10 and nothing extra is done on earlier versions.
65
+ --- See |MiniCompletion.default_snippet_insert()|.
63
66
---
64
67
--- # Setup ~
65
68
---
102
105
--- to trigger such request, i.e. select completion item and wait for
103
106
--- `MiniCompletion.config.delay.info` time plus server response time.
104
107
---
108
+ --- - TODO: how to work with snippets:
109
+ --- - Select item.
110
+ --- - Press <C-y> (|complete_CTRL-Y|) or any non-keyword character.
111
+ --- To cancel snippet expand, press <C-e> (|complete_CTRL-E|).
112
+ --- Snippets are either `Snippet` kind or have `S` in "menu".
113
+ --- Their text to be inserted (`word` in |complete-items|) the same as label
114
+ --- (`abbr` field) to reduce visual flicker during completion list navigation.
115
+ ---
105
116
--- # Comparisons ~
106
117
---
107
118
--- - 'nvim-cmp':
@@ -267,6 +278,10 @@ MiniCompletion.config = {
267
278
-- input items. Common use case is custom filter/sort.
268
279
-- Default: `default_process_items`
269
280
process_items = nil ,
281
+
282
+ -- A function which takes a snippet as string and inserts it at cursor.
283
+ -- Default: `default_snippet_insert`
284
+ snippet_insert = nil ,
270
285
},
271
286
272
287
-- Fallback action as function/string. Executed in Insert mode.
420
435
--- Default processing of LSP items
421
436
---
422
437
--- Steps:
423
- --- - Filter out items not matching `base` and snippet items .
438
+ --- - Filter out items not starting with `base`.
424
439
--- - Sort by LSP specification.
425
440
--- - If |MiniIcons| is enabled, add <kind_hlgroup> based on the "lsp" category.
426
441
---
@@ -429,11 +444,10 @@ end
429
444
---
430
445
--- @return table Array of processed items from LSP response.
431
446
MiniCompletion .default_process_items = function (items , base )
432
- local res = vim .tbl_filter (function (item )
433
- -- Keep items which match the base and are not snippets
434
- local text = item .filterText or H .get_completion_word (item )
435
- return vim .startswith (text , base ) and item .kind ~= 15
436
- end , items )
447
+ local res = vim .tbl_filter (
448
+ function (item ) return vim .startswith (item .filterText or H .get_completion_word (item ), base ) end ,
449
+ items
450
+ )
437
451
438
452
res = vim .deepcopy (res )
439
453
table.sort (res , function (a , b ) return (a .sortText or a .label ) < (b .sortText or b .label ) end )
@@ -449,6 +463,16 @@ MiniCompletion.default_process_items = function(items, base)
449
463
return res
450
464
end
451
465
466
+ MiniCompletion .default_snippet_insert = function (snippet )
467
+ if _G .MiniSnippets then
468
+ local insert = MiniSnippets .config .expand .insert or MiniSnippets .default_insert
469
+ return insert ({ body = snippet })
470
+ end
471
+ if vim .fn .has (' nvim-0.10' ) == 1 then return vim .snippet .expand (snippet ) end
472
+ local pos = vim .api .nvim_win_get_cursor (0 )
473
+ vim .api .nvim_buf_set_text (0 , pos [1 ] - 1 , pos [2 ], pos [1 ] - 1 , pos [2 ], { snippet })
474
+ end
475
+
452
476
-- Helper data ================================================================
453
477
-- Module default config
454
478
H .default_config = vim .deepcopy (MiniCompletion .config )
@@ -703,14 +727,7 @@ H.on_completedonepre = function()
703
727
704
728
-- Do extra actions for LSP completion items
705
729
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
730
+ H .make_lsp_extra_actions (lsp_data )
714
731
715
732
-- Stop processes
716
733
MiniCompletion .stop ({ ' completion' , ' info' })
@@ -918,29 +935,44 @@ H.make_completion_request = function()
918
935
H .completion .lsp .cancel_fun = cancel_fun
919
936
end
920
937
921
- -- This is a truncated version of
922
- -- `vim.lsp.util.text_document_completion_list_to_complete_items` which does
923
- -- not filter and sort items.
924
- -- For extra information see 'Response' section:
938
+ -- Source:
925
939
-- https://microsoft.github.io/language-server-protocol/specifications/specification-3-14/#textDocument_completion
926
940
H .lsp_completion_response_items_to_complete_items = function (items , client_id )
927
941
if vim .tbl_count (items ) == 0 then return {} end
928
942
929
943
local res , item_kinds = {}, vim .lsp .protocol .CompletionItemKind
944
+ local snippet_kind = vim .lsp .protocol .CompletionItemKind .Snippet
945
+ local snippet_inserttextformat = vim .lsp .protocol .InsertTextFormat .Snippet
930
946
for _ , item in pairs (items ) do
931
- local label_details , menu = item .labelDetails , nil
932
- if label_details ~= nil then menu = (label_details .detail or ' ' ) .. (label_details .description or ' ' ) end
947
+ local word = H .get_completion_word (item )
948
+
949
+ local is_snippet_kind = item .kind == snippet_kind
950
+ local is_snippet_format = item .insertTextFormat == snippet_inserttextformat
951
+ -- Treat item as snippet only if it has tabstops. This is important to make
952
+ -- "implicit" expand work with LSP servers that report even regular words
953
+ -- as `InsertTextFormat.Snippet` (like `gopls`).
954
+ local needs_snippet_insert = (is_snippet_kind or is_snippet_format )
955
+ and (word :find (' [^\\ ]%${?%d' ) ~= nil or word :find (' ^%${?%d' ) ~= nil )
956
+
957
+ local details = item .labelDetails or {}
958
+ local snippet_clue = needs_snippet_insert and ' S' or ' '
959
+ local label_detail = (details .detail or ' ' ) .. (details .description or ' ' )
960
+ label_detail = snippet_clue .. ((snippet_clue ~= ' ' and label_detail ~= ' ' ) and ' ' or ' ' ) .. label_detail
961
+
962
+ local lsp_data = { completion_item = item , client_id = client_id , needs_snippet_insert = needs_snippet_insert }
933
963
table.insert (res , {
934
- word = H .get_completion_word (item ),
964
+ -- Show less for snippet items (usually less confusion)
965
+ word = needs_snippet_insert and item .label or word ,
935
966
abbr = item .label ,
936
967
kind = item_kinds [item .kind ] or ' Unknown' ,
937
968
kind_hlgroup = item .kind_hlgroup ,
938
- menu = menu ,
939
- -- Do not set `info` field in favor of trying to first resolve it
969
+ menu = label_detail ,
970
+ -- NOTE: info will be attempted to resolve, use snippet text as fallback
971
+ info = needs_snippet_insert and word or nil ,
940
972
icase = 1 ,
941
973
dup = 1 ,
942
974
empty = 1 ,
943
- user_data = { nvim = { lsp = { completion_item = item , client_id = client_id } } },
975
+ user_data = { nvim = { lsp = lsp_data } },
944
976
})
945
977
end
946
978
return res
@@ -966,11 +998,52 @@ H.make_add_kind_hlgroup = function()
966
998
end
967
999
968
1000
H .get_completion_word = function (item )
969
- -- Completion word (textEdit.newText > insertText > label). This doesn't
970
- -- support snippet expansion.
971
1001
return H .table_get (item , { ' textEdit' , ' newText' }) or item .insertText or item .label or ' '
972
1002
end
973
1003
1004
+ H .make_lsp_extra_actions = function (lsp_data )
1005
+ if lsp_data == nil then return end
1006
+
1007
+ -- Prefer resolved item over the one from 'textDocument/completion'
1008
+ local resolved = (H .info .lsp .result or {})[lsp_data .client_id ]
1009
+ local item = (resolved == nil or resolved .err ) and lsp_data .completion_item or resolved .result
1010
+
1011
+ if item .additionalTextEdits == nil and not lsp_data .needs_snippet_insert then return end
1012
+ local snippet = lsp_data .needs_snippet_insert and H .get_completion_word (item ) or nil
1013
+
1014
+ -- Make extra actions not only after an explicit `<C-y>` (accept completed
1015
+ -- item), but also after implicit non-keyword character. This needs:
1016
+ -- - Keeping track of text after cursor (as non-keyword character might
1017
+ -- insert extra text and move cursor, like in 'mini.pairs')
1018
+ -- - Delay actual execution to operate *after* character is inserted (as it
1019
+ -- is not immediate). This works around that character being "inserted"
1020
+ -- after snippet expansion.
1021
+ local cur = vim .api .nvim_win_get_cursor (0 )
1022
+ local ref_text_after_cursor = vim .api .nvim_buf_get_text (0 , cur [1 ] - 1 , cur [2 ], cur [1 ] - 1 , - 1 , {})[1 ]
1023
+
1024
+ vim .schedule (function ()
1025
+ -- Do nothing if user exited Insert mode
1026
+ if vim .fn .mode () ~= ' i' then return end
1027
+
1028
+ -- Try to apply additional text edits
1029
+ H .apply_additional_text_edits (item .additionalTextEdits , lsp_data .client_id )
1030
+
1031
+ -- Expand snippet: cleanup text from side effects and insert snippet
1032
+ if snippet == nil then return end
1033
+ local from , to = H .completion .start_pos , vim .api .nvim_win_get_cursor (0 )
1034
+
1035
+ -- - NOTE: replace text under condition to preserve extmarks.
1036
+ local cur_text_after_cursor = vim .api .nvim_buf_get_text (0 , to [1 ] - 1 , to [2 ], to [1 ] - 1 , - 1 , {})[1 ]
1037
+ if cur_text_after_cursor ~= ref_text_after_cursor then
1038
+ vim .api .nvim_buf_set_text (0 , to [1 ] - 1 , to [2 ], to [1 ] - 1 , - 1 , { ref_text_after_cursor })
1039
+ end
1040
+ pcall (vim .api .nvim_buf_set_text , 0 , from [1 ] - 1 , from [2 ], to [1 ] - 1 , to [2 ], { ' ' })
1041
+
1042
+ local insert = H .get_config ().lsp_completion .snippet_insert or MiniCompletion .default_snippet_insert
1043
+ insert (snippet )
1044
+ end )
1045
+ end
1046
+
974
1047
H .apply_additional_text_edits = function (edits , client_id )
975
1048
-- Code originally inspired by https://github.com/neovim/neovim/issues/12310
976
1049
if edits == nil then return end
0 commit comments