-
Notifications
You must be signed in to change notification settings - Fork 219
/
Copy pathcompletion.lua
1464 lines (1251 loc) · 56 KB
/
completion.lua
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
--- *mini.completion* Completion and signature help
--- *MiniCompletion*
---
--- MIT License Copyright (c) 2021 Evgeni Chasnovski
---
--- ==============================================================================
---
--- Key design ideas:
--- - Have an async (with customizable "debounce" delay) "two-stage chain
--- completion": first try to get completion items from LSP client (if set
--- up) and if no result, fallback to custom action.
---
--- - Managing completion is done as much with Neovim's built-in tools as
--- possible.
---
--- Features:
--- - Two-stage chain completion:
--- - First stage is an LSP completion implemented via
--- |MiniCompletion.completefunc_lsp()|. It should be set up as either
--- |completefunc| or |omnifunc|. It tries to get completion items from
--- LSP client (via 'textDocument/completion' request). Custom
--- preprocessing of response items is possible (with
--- `MiniCompletion.config.lsp_completion.process_items`), for example
--- with fuzzy matching. By default items which are not snippets and
--- directly start with completed word are kept and sorted according to
--- LSP specification. Supports `additionalTextEdits`, like auto-import
--- and others (see 'Notes').
--- - If first stage is not set up or resulted into no candidates, fallback
--- action is executed. The most tested actions are Neovim's built-in
--- insert completion (see |ins-completion|).
---
--- - Automatic display in floating window of completion item info (via
--- 'completionItem/resolve' request) and signature help (with highlighting
--- of active parameter if LSP server provides such information). After
--- opening, window for signature help is fixed and is closed when there is
--- nothing to show, text is different or
--- when leaving Insert mode.
---
--- - Automatic actions are done after some configurable amount of delay. This
--- reduces computational load and allows fast typing (completion and
--- signature help) and item selection (item info)
---
--- - User can force two-stage completion via
--- |MiniCompletion.complete_twostage()| (by default is mapped to
--- `<C-Space>`) or fallback completion via
--- |MiniCompletion.complete_fallback()| (mapped to `<M-Space>`).
---
--- - LSP kind highlighting ("Function", "Keyword", etc.). Requires Neovim>=0.11.
--- By default uses "lsp" category of |MiniIcons| (if enabled). Can be customized
--- via `config.lsp_completion.process_items` by adding field <kind_hlgroup>
--- (same meaning as in |complete-items|) to items.
---
--- What it doesn't do:
--- - Snippet expansion.
--- - Many configurable sources.
--- - Automatic mapping of `<CR>`, `<Tab>`, etc., as those tend to have highly
--- variable user expectations. See 'Helpful key mappings' for suggestions.
---
--- # Dependencies ~
---
--- Suggested dependencies (provide extra functionality, will work without them):
---
--- - Enabled |MiniIcons| module to highlight LSP kind (requires Neovim>=0.11).
--- Otherwise |MiniCompletion.default_process_items()| does not add highlighting.
--- Also take a look at |MiniIcons.tweak_lsp_kind()|.
---
--- # Setup ~
---
--- This module needs a setup with `require('mini.completion').setup({})`
--- (replace `{}` with your `config` table). It will create global Lua table
--- `MiniCompletion` which you can use for scripting or manually (with
--- `:lua MiniCompletion.*`).
---
--- See |MiniCompletion.config| for `config` structure and default values.
---
--- You can override runtime config settings locally to buffer inside
--- `vim.b.minicompletion_config` which should have same structure as
--- `MiniCompletion.config`. See |mini.nvim-buffer-local-config| for more details.
---
--- # Notes ~
---
--- - More appropriate, albeit slightly advanced, LSP completion setup is to set
--- it not on every `BufEnter` event (default), but on every attach of LSP
--- client. To do that:
--- - Use in initial config:
--- `lsp_completion = {source_func = 'omnifunc', auto_setup = false}`.
--- - In `on_attach()` of every LSP client set 'omnifunc' option to exactly
--- `v:lua.MiniCompletion.completefunc_lsp`.
---
--- - Uses `vim.lsp.protocol.CompletionItemKind` map in LSP step to show a readable
--- version of item's kind. Modify it directly to change what is displayed.
--- If you have |mini.icons| enabled, take a look at |MiniIcons.tweak_lsp_kind()|.
---
--- - If you have trouble using custom (overridden) |vim.ui.input| (like from
--- 'stevearc/dressing.nvim'), make automated disable of 'mini.completion'
--- for input buffer. For example, currently for 'dressing.nvim' it can be
--- with `au FileType DressingInput lua vim.b.minicompletion_disable = true`.
---
--- - Support of `additionalTextEdits` tries to handle both types of servers:
--- - When `additionalTextEdits` are supplied in response to
--- 'textDocument/completion' request (like currently in 'pyright').
--- - When `additionalTextEdits` are supplied in response to
--- 'completionItem/resolve' request (like currently in
--- 'typescript-language-server'). In this case to apply edits user needs
--- to trigger such request, i.e. select completion item and wait for
--- `MiniCompletion.config.delay.info` time plus server response time.
---
--- # Comparisons ~
---
--- - 'nvim-cmp':
--- - More complex design which allows multiple sources each in form of
--- separate plugin. `MiniCompletion` has two built in: LSP and fallback.
--- - Supports snippet expansion.
--- - Doesn't have customizable delays for basic actions.
--- - Doesn't allow fallback action.
--- - Doesn't provide signature help.
---
--- # Helpful mappings ~
---
--- To use `<Tab>` and `<S-Tab>` for navigation through completion list, make
--- these mappings: >lua
---
--- local imap_expr = function(lhs, rhs)
--- vim.keymap.set('i', lhs, rhs, { expr = true })
--- end
--- imap_expr('<Tab>', [[pumvisible() ? "\<C-n>" : "\<Tab>"]])
--- imap_expr('<S-Tab>', [[pumvisible() ? "\<C-p>" : "\<S-Tab>"]])
--- <
--- To get more consistent behavior of `<CR>`, you can use this template in
--- your 'init.lua' to make customized mapping: >lua
---
--- local keycode = vim.keycode or function(x)
--- return vim.api.nvim_replace_termcodes(x, true, true, true)
--- end
--- local keys = {
--- ['cr'] = keycode('<CR>'),
--- ['ctrl-y'] = keycode('<C-y>'),
--- ['ctrl-y_cr'] = keycode('<C-y><CR>'),
--- }
---
--- _G.cr_action = function()
--- if vim.fn.pumvisible() ~= 0 then
--- -- If popup is visible, confirm selected item or add new line otherwise
--- local item_selected = vim.fn.complete_info()['selected'] ~= -1
--- return item_selected and keys['ctrl-y'] or keys['ctrl-y_cr']
--- else
--- -- If popup is not visible, use plain `<CR>`. You might want to customize
--- -- according to other plugins. For example, to use 'mini.pairs', replace
--- -- next line with `return require('mini.pairs').cr()`
--- return keys['cr']
--- end
--- end
---
--- vim.keymap.set('i', '<CR>', 'v:lua._G.cr_action()', { expr = true })
--- <
--- # Highlight groups ~
---
--- * `MiniCompletionActiveParameter` - signature active parameter.
--- By default displayed as plain underline.
---
--- To change any highlight group, modify it directly with |:highlight|.
---
--- # Disabling ~
---
--- To disable, set `vim.g.minicompletion_disable` (globally) or
--- `vim.b.minicompletion_disable` (for a buffer) to `true`. Considering high
--- number of different scenarios and customization intentions, writing exact
--- rules for disabling module's functionality is left to user. See
--- |mini.nvim-disabling-recipes| for common recipes.
-- Overall implementation design:
-- - Completion:
-- - On `InsertCharPre` event try to start auto completion. If needed,
-- start timer which after delay will start completion process. Stop this
-- timer if it is not needed.
-- - When timer is activated, first execute LSP source (if set up and there
-- is an active LSP client) by calling built-in complete function
-- (`completefunc` or `omnifunc`) which tries LSP completion by
-- asynchronously sending LSP 'textDocument/completion' request to all
-- LSP clients. When all are done, execute callback which processes
-- results, stores them in LSP cache and reruns built-in complete
-- function which produces completion popup.
-- - If previous step didn't result into any completion, execute (in Insert
-- mode and if no popup) fallback action.
-- - Documentation:
-- - On `CompleteChanged` start auto info with similar to completion timer
-- pattern.
-- - If timer is activated, try these sources of item info:
-- - 'info' field of completion item (see `:h complete-items`).
-- - 'documentation' field of LSP's previously returned result.
-- - 'documentation' field in result of asynchronous
-- 'completeItem/resolve' LSP request.
-- - If info doesn't consist only from whitespace, show floating window
-- with its content. Its dimensions and position are computed based on
-- current state of Neovim's data and content itself (which will be
-- displayed wrapped with `linebreak` option).
-- - Signature help (similar to item info):
-- - On `CursorMovedI` start auto signature (if there is any active LSP
-- client) with similar to completion timer pattern. Better event might
-- be `InsertCharPre` but there are issues with 'autopair-type' plugins.
-- - Check if character left to cursor is appropriate (')' or LSP's
-- signature help trigger characters). If not, do nothing.
-- - If timer is activated, send 'textDocument/signatureHelp' request to
-- all LSP clients. On callback, process their results. Window is opened
-- if not already with the same text (its characteristics are computed
-- similar to item info). For every LSP client it shows only active
-- signature (in case there are many). If LSP response has data about
-- active parameter, it is highlighted with
-- `MiniCompletionActiveParameter` highlight group.
-- Module definition ==========================================================
local MiniCompletion = {}
local H = {}
--- Module setup
---
---@param config table|nil Module config table. See |MiniCompletion.config|.
---
---@usage >lua
--- require('mini.completion').setup() -- use default config
--- -- OR
--- require('mini.completion').setup({}) -- replace {} with your config table
--- <
MiniCompletion.setup = function(config)
-- Export module
_G.MiniCompletion = MiniCompletion
-- Setup config
config = H.setup_config(config)
-- Apply config
H.apply_config(config)
-- Define behavior
H.create_autocommands(config)
-- Create default highlighting
H.create_default_hl()
end
--- Module config
---
--- Default values:
---@eval return MiniDoc.afterlines_to_code(MiniDoc.current.eval_section)
MiniCompletion.config = {
-- Delay (debounce type, in ms) between certain Neovim event and action.
-- This can be used to (virtually) disable certain automatic actions by
-- setting very high delay time (like 10^7).
delay = { completion = 100, info = 100, signature = 50 },
-- Configuration for action windows:
-- - `height` and `width` are maximum dimensions.
-- - `border` defines border (as in `nvim_open_win()`).
window = {
info = { height = 25, width = 80, border = 'none' },
signature = { height = 25, width = 80, border = 'none' },
},
-- Way of how module does LSP completion
lsp_completion = {
-- `source_func` should be one of 'completefunc' or 'omnifunc'.
source_func = 'completefunc',
-- `auto_setup` should be boolean indicating if LSP completion is set up
-- on every `BufEnter` event.
auto_setup = true,
-- A function which takes LSP 'textDocument/completion' response items
-- and word to complete. Output should be a table of the same nature as
-- input items. Common use case is custom filter/sort.
--minidoc_replace_start process_items = --<function: MiniCompletion.default_process_items>,
process_items = function(items, base)
local res = vim.tbl_filter(function(item)
-- Keep items which match the base and are not snippets
local text = item.filterText or H.get_completion_word(item)
return vim.startswith(text, base) and item.kind ~= 15
end, items)
res = vim.deepcopy(res)
table.sort(res, function(a, b) return (a.sortText or a.label) < (b.sortText or b.label) end)
-- Possibly add "kind" highlighting
if _G.MiniIcons ~= nil then
local add_kind_hlgroup = H.make_add_kind_hlgroup()
for _, item in ipairs(res) do
add_kind_hlgroup(item)
end
end
return res
end,
--minidoc_replace_end
},
-- Fallback action. It will always be run in Insert mode. To use Neovim's
-- built-in completion (see `:h ins-completion`), supply its mapping as
-- string. Example: to use 'whole lines' completion, supply '<C-x><C-l>'.
--minidoc_replace_start fallback_action = --<function: like `<C-n>` completion>,
fallback_action = function() vim.api.nvim_feedkeys(H.keys.ctrl_n, 'n', false) end,
--minidoc_replace_end
-- Module mappings. Use `''` (empty string) to disable one. Some of them
-- might conflict with system mappings.
mappings = {
force_twostep = '<C-Space>', -- Force two-step completion
force_fallback = '<A-Space>', -- Force fallback completion
},
-- Whether to set Vim's settings for better experience (modifies
-- `shortmess` and `completeopt`)
set_vim_settings = true,
}
--minidoc_afterlines_end
-- Module functionality =======================================================
--- Run two-stage completion
---
---@param fallback boolean|nil Whether to use fallback completion. Default: `true`.
---@param force boolean|nil Whether to force update of completion popup.
--- Default: `true`.
MiniCompletion.complete_twostage = function(fallback, force)
if H.is_disabled() then return end
if fallback == nil then fallback = true end
if force == nil then force = true end
H.stop_completion()
H.completion.fallback, H.completion.force = fallback, force
H.trigger_twostep()
end
--- Run fallback completion
MiniCompletion.complete_fallback = function()
if H.is_disabled() then return end
H.stop_completion()
H.completion.fallback, H.completion.force = true, true
H.trigger_fallback()
end
--- Stop actions
---
--- This stops currently active (because of module delay or LSP answer delay)
--- actions.
---
--- Designed to be used with |autocmd|. No need to use it directly, everything
--- is setup in |MiniCompletion.setup|.
---
---@param actions table|nil Array containing any of 'completion', 'info', or
--- 'signature' string. Default: array containing all of them.
MiniCompletion.stop = function(actions)
actions = actions or { 'completion', 'info', 'signature' }
for _, n in pairs(actions) do
H.stop_actions[n]()
end
end
--- Module's |complete-function|
---
--- This is the main function which enables two-stage completion. It should be
--- set as one of |completefunc| or |omnifunc|.
---
--- No need to use it directly, everything is setup in |MiniCompletion.setup|.
MiniCompletion.completefunc_lsp = function(findstart, base)
-- Early return
if not H.has_lsp_clients('completionProvider') or H.completion.lsp.status == 'sent' then
if findstart == 1 then return -3 end
return {}
end
-- NOTE: having code for request inside this function enables its use
-- directly with `<C-x><...>`.
if H.completion.lsp.status ~= 'received' then
local current_id = H.completion.lsp.id + 1
H.completion.lsp.id = current_id
H.completion.lsp.status = 'sent'
local bufnr = vim.api.nvim_get_current_buf()
local params = vim.lsp.util.make_position_params()
-- NOTE: it is CRUCIAL to make LSP request on the first call to
-- 'complete-function' (as in Vim's help). This is due to the fact that
-- cursor line and position are different on the first and second calls to
-- 'complete-function'. For example, when calling this function at the end
-- of the line ' he', cursor position on the first call will be
-- (<linenum>, 4) and line will be ' he' but on the second call -
-- (<linenum>, 2) and ' ' (because 2 is a column of completion start).
-- This request is executed only on second call because it returns `-3` on
-- first call (which means cancel and leave completion mode).
-- NOTE: using `buf_request_all()` (instead of `buf_request()`) to easily
-- handle possible fallback and to have all completion suggestions be
-- filtered with one `base` in the other route of this function. Anyway,
-- the most common situation is with one attached LSP client.
local cancel_fun = vim.lsp.buf_request_all(bufnr, 'textDocument/completion', params, function(result)
if not H.is_lsp_current(H.completion, current_id) then return end
H.completion.lsp.status = 'received'
H.completion.lsp.result = result
-- Trigger LSP completion to take 'received' route
H.trigger_lsp()
end)
-- Cache cancel function to disable requests when they are not needed
H.completion.lsp.cancel_fun = cancel_fun
-- End completion and wait for LSP callback
if findstart == 1 then return -3 end
return {}
else
if findstart == 1 then return H.get_completion_start(H.completion.lsp.result) end
local process_items = H.get_config().lsp_completion.process_items
local words = H.process_lsp_response(H.completion.lsp.result, function(response, client_id)
-- Response can be `CompletionList` with 'items' field or `CompletionItem[]`
local items = H.table_get(response, { 'items' }) or response
if type(items) ~= 'table' then return {} end
items = process_items(items, base)
return H.lsp_completion_response_items_to_complete_items(items, client_id)
end)
H.completion.lsp.status = 'done'
-- Maybe trigger fallback action
if vim.tbl_isempty(words) and H.completion.fallback then return H.trigger_fallback() end
-- Track from which source is current popup
H.completion.source = 'lsp'
return words
end
end
--- Default `MiniCompletion.config.lsp_completion.process_items`
---
--- Steps:
--- - Filter out items not matching `base` and snippet items.
--- - Sort by LSP specification.
--- - If |MiniIcons| is enabled, add <kind_hlgroup> based on the "lsp" category.
MiniCompletion.default_process_items = function(items, base)
return H.default_config.lsp_completion.process_items(items, base)
end
-- Helper data ================================================================
-- Module default config
H.default_config = vim.deepcopy(MiniCompletion.config)
-- Track Insert mode changes
H.text_changed_id = 0
-- Namespace for highlighting
H.ns_id = vim.api.nvim_create_namespace('MiniCompletion')
-- Commonly used key sequences
H.keys = {
completefunc = vim.api.nvim_replace_termcodes('<C-x><C-u>', true, false, true),
omnifunc = vim.api.nvim_replace_termcodes('<C-x><C-o>', true, false, true),
ctrl_n = vim.api.nvim_replace_termcodes('<C-g><C-g><C-n>', true, false, true),
}
-- Caches for different actions -----------------------------------------------
-- Field `lsp` is a table describing state of all used LSP requests. It has the
-- following structure:
-- - id: identifier (consecutive numbers).
-- - status: status. One of 'sent', 'received', 'done', 'canceled'.
-- - result: result of request.
-- - cancel_fun: function which cancels current request.
-- Cache for completion
H.completion = {
fallback = true,
force = false,
source = nil,
text_changed_id = 0,
timer = vim.loop.new_timer(),
lsp = { id = 0, status = nil, result = nil, cancel_fun = nil },
}
-- Cache for completion item info
H.info = {
bufnr = nil,
event = nil,
id = 0,
timer = vim.loop.new_timer(),
win_id = nil,
lsp = { id = 0, status = nil, result = nil, cancel_fun = nil },
}
-- Cache for signature help
H.signature = {
bufnr = nil,
text = nil,
timer = vim.loop.new_timer(),
win_id = nil,
lsp = { id = 0, status = nil, result = nil, cancel_fun = nil },
}
-- Helper functionality =======================================================
-- Settings -------------------------------------------------------------------
H.setup_config = function(config)
-- General idea: if some table elements are not present in user-supplied
-- `config`, take them from default config
vim.validate({ config = { config, 'table', true } })
config = vim.tbl_deep_extend('force', vim.deepcopy(H.default_config), config or {})
-- Validate per nesting level to produce correct error message
vim.validate({
delay = { config.delay, 'table' },
window = { config.window, 'table' },
lsp_completion = { config.lsp_completion, 'table' },
fallback_action = {
config.fallback_action,
function(x) return type(x) == 'function' or type(x) == 'string' end,
'function or string',
},
mappings = { config.mappings, 'table' },
set_vim_settings = { config.set_vim_settings, 'boolean' },
})
vim.validate({
['delay.completion'] = { config.delay.completion, 'number' },
['delay.info'] = { config.delay.info, 'number' },
['delay.signature'] = { config.delay.signature, 'number' },
['window.info'] = { config.window.info, 'table' },
['window.signature'] = { config.window.signature, 'table' },
['lsp_completion.source_func'] = {
config.lsp_completion.source_func,
function(x) return x == 'completefunc' or x == 'omnifunc' end,
'one of strings: "completefunc" or "omnifunc"',
},
['lsp_completion.auto_setup'] = { config.lsp_completion.auto_setup, 'boolean' },
['lsp_completion.process_items'] = { config.lsp_completion.process_items, 'function' },
['mappings.force_twostep'] = { config.mappings.force_twostep, 'string' },
['mappings.force_fallback'] = { config.mappings.force_fallback, 'string' },
})
local is_string_or_array = function(x) return type(x) == 'string' or H.islist(x) end
vim.validate({
['window.info.height'] = { config.window.info.height, 'number' },
['window.info.width'] = { config.window.info.width, 'number' },
['window.info.border'] = {
config.window.info.border,
is_string_or_array,
'(mini.completion) `config.window.info.border` can be either string or array.',
},
['window.signature.height'] = { config.window.signature.height, 'number' },
['window.signature.width'] = { config.window.signature.width, 'number' },
['window.signature.border'] = {
config.window.signature.border,
is_string_or_array,
'(mini.completion) `config.window.signature.border` can be either string or array.',
},
})
return config
end
H.apply_config = function(config)
MiniCompletion.config = config
--stylua: ignore start
H.map('i', config.mappings.force_twostep, MiniCompletion.complete_twostage, { desc = 'Complete with two-stage' })
H.map('i', config.mappings.force_fallback, MiniCompletion.complete_fallback, { desc = 'Complete with fallback' })
--stylua: ignore end
if config.set_vim_settings then
-- Don't give ins-completion-menu messages
vim.opt.shortmess:append('c')
if vim.fn.has('nvim-0.9') == 1 then vim.opt.shortmess:append('C') end
-- More common completion behavior
vim.o.completeopt = 'menuone,noinsert,noselect'
end
end
H.create_autocommands = function(config)
local gr = vim.api.nvim_create_augroup('MiniCompletion', {})
local au = function(event, pattern, callback, desc)
vim.api.nvim_create_autocmd(event, { group = gr, pattern = pattern, callback = callback, desc = desc })
end
au('InsertCharPre', '*', H.auto_completion, 'Auto show completion')
au('CompleteChanged', '*', H.auto_info, 'Auto show info')
au('CursorMovedI', '*', H.auto_signature, 'Auto show signature')
au('ModeChanged', 'i*:[^i]*', function() MiniCompletion.stop() end, 'Stop completion')
au('CompleteDonePre', '*', H.on_completedonepre, 'On CompleteDonePre')
au('TextChangedI', '*', H.on_text_changed_i, 'On TextChangedI')
au('TextChangedP', '*', H.on_text_changed_p, 'On TextChangedP')
if config.lsp_completion.auto_setup then
local source_func = config.lsp_completion.source_func
local callback = function() vim.bo[source_func] = 'v:lua.MiniCompletion.completefunc_lsp' end
au('BufEnter', '*', callback, 'Set completion function')
end
au('ColorScheme', '*', H.create_default_hl, 'Ensure colors')
au('FileType', 'TelescopePrompt', function() vim.b.minicompletion_disable = true end, 'Disable locally')
end
H.create_default_hl = function()
vim.api.nvim_set_hl(0, 'MiniCompletionActiveParameter', { default = true, underline = true })
end
H.is_disabled = function() return vim.g.minicompletion_disable == true or vim.b.minicompletion_disable == true end
H.get_config = function(config)
return vim.tbl_deep_extend('force', MiniCompletion.config, vim.b.minicompletion_config or {}, config or {})
end
-- Autocommands ---------------------------------------------------------------
H.auto_completion = function()
if H.is_disabled() then return end
H.completion.timer:stop()
local char_is_trigger = H.is_lsp_trigger(vim.v.char, 'completion')
if char_is_trigger then
-- If character is LSP trigger, force fresh LSP completion later
-- Check LSP trigger before checking for pumvisible because it should be
-- forced even if there are visible candidates
H.stop_completion(false)
elseif H.pumvisible() then
-- Do nothing if popup is visible. `H.pumvisible()` might be `true` even if
-- there is no popup. It is common when manually typing candidate followed
-- by an LSP trigger (like ".").
-- Keep completion source as it is needed all time when popup is visible.
return H.stop_completion(true)
elseif not H.is_char_keyword(vim.v.char) then
-- Stop everything if inserted character is not appropriate. Check this
-- after popup check to allow completion candidates to have bad characters.
return H.stop_completion(false)
end
-- Start non-forced completion with fallback or forced LSP source for trigger
H.completion.fallback, H.completion.force = not char_is_trigger, char_is_trigger
-- Cache id of Insert mode "text changed" event for a later tracking (reduces
-- false positive delayed triggers). The intention is to trigger completion
-- after the delay only if text wasn't changed during waiting. Using only
-- `InsertCharPre` is not enough though, as not every Insert mode change
-- triggers `InsertCharPre` event (notable example - hitting `<CR>`).
-- Also, using `+ 1` here because it is a `Pre` event and needs to cache
-- after inserting character.
H.completion.text_changed_id = H.text_changed_id + 1
-- If completion was requested after 'lsp' source exhausted itself (there
-- were matches on typing start, but they disappeared during filtering), call
-- fallback immediately.
if H.completion.source == 'lsp' then
H.trigger_fallback()
return
end
-- Using delay (of debounce type) actually improves user experience
-- as it allows fast typing without many popups.
H.completion.timer:start(H.get_config().delay.completion, 0, vim.schedule_wrap(H.trigger_twostep))
end
H.auto_info = function()
if H.is_disabled() then return end
H.info.timer:stop()
-- Defer execution because of textlock during `CompleteChanged` event
-- Don't stop timer when closing info window because it is needed
vim.schedule(function() H.close_action_window(H.info, true) end)
-- Stop current LSP request that tries to get not current data
H.cancel_lsp({ H.info })
-- Update metadata before leaving to register a `CompleteChanged` event
H.info.event = vim.v.event
H.info.id = H.info.id + 1
-- Don't even try to show info if nothing is selected in popup
if vim.tbl_isempty(H.info.event.completed_item) then return end
H.info.timer:start(H.get_config().delay.info, 0, vim.schedule_wrap(H.show_info_window))
end
H.auto_signature = function()
if H.is_disabled() then return end
H.signature.timer:stop()
if not H.has_lsp_clients('signatureHelpProvider') then return end
local left_char = H.get_left_char()
local char_is_trigger = left_char == ')' or H.is_lsp_trigger(left_char, 'signature')
if not char_is_trigger then return end
H.signature.timer:start(H.get_config().delay.signature, 0, vim.schedule_wrap(H.show_signature_window))
end
H.on_completedonepre = function()
-- Try to apply additional text edits
H.apply_additional_text_edits()
-- Stop processes
MiniCompletion.stop({ 'completion', 'info' })
end
H.on_text_changed_i = function()
-- Track Insert mode changes
H.text_changed_id = H.text_changed_id + 1
-- Stop 'info' processes in case no completion event is triggered but popup
-- is not visible. See https://github.com/neovim/neovim/issues/15077
H.stop_info()
end
H.on_text_changed_p = function()
-- Track Insert mode changes
H.text_changed_id = H.text_changed_id + 1
end
-- Completion triggers --------------------------------------------------------
H.trigger_twostep = function()
-- Trigger only in Insert mode and if text didn't change after trigger
-- request, unless completion is forced
-- NOTE: check for `text_changed_id` equality is still not 100% solution as
-- there are cases when, for example, `<CR>` is hit just before this check.
-- Because of asynchronous id update and this function call (called after
-- delay), these still match.
local allow_trigger = (vim.fn.mode() == 'i')
and (H.completion.force or (H.completion.text_changed_id == H.text_changed_id))
if not allow_trigger then return end
if H.has_lsp_clients('completionProvider') and H.has_lsp_completion() then
H.trigger_lsp()
elseif H.completion.fallback then
H.trigger_fallback()
end
end
H.trigger_lsp = function()
-- Check for popup visibility is needed to reduce flickering.
-- Possible issue timeline (with 100ms delay with set up LSP):
-- 0ms: Key is pressed.
-- 100ms: LSP is triggered from first key press.
-- 110ms: Another key is pressed.
-- 200ms: LSP callback is processed, triggers complete-function which
-- processes "received" LSP request.
-- 201ms: LSP request is processed, completion is (should be almost
-- immediately) provided, request is marked as "done".
-- 210ms: LSP is triggered from second key press. As previous request is
-- "done", it will once make whole LSP request. Having check for visible
-- popup should prevent here the call to complete-function.
-- When `force` is `true` then presence of popup shouldn't matter.
local no_popup = H.completion.force or (not H.pumvisible())
if no_popup and vim.fn.mode() == 'i' then
local key = H.keys[H.get_config().lsp_completion.source_func]
vim.api.nvim_feedkeys(key, 'n', false)
end
end
H.trigger_fallback = function()
-- Fallback only in Insert mode when no popup is visible
local has_popup = H.pumvisible() and not H.completion.force
if has_popup or vim.fn.mode() ~= 'i' then return end
-- Track from which source is current popup
H.completion.source = 'fallback'
-- Execute fallback action
local fallback_action = H.get_config().fallback_action
if vim.is_callable(fallback_action) then return fallback_action() end
if type(fallback_action) ~= 'string' then return end
-- Having `<C-g><C-g>` also (for some mysterious reason) helps to avoid
-- some weird behavior. For example, if `keys = '<C-x><C-l>'` then Neovim
-- starts new line when there is no suggestions.
local keys = string.format('<C-g><C-g>%s', fallback_action)
local trigger_keys = vim.api.nvim_replace_termcodes(keys, true, false, true)
vim.api.nvim_feedkeys(trigger_keys, 'n', false)
end
-- Stop actions ---------------------------------------------------------------
H.stop_completion = function(keep_source)
H.completion.timer:stop()
H.cancel_lsp({ H.completion })
H.completion.fallback, H.completion.force = true, false
if not keep_source then H.completion.source = nil end
end
H.stop_info = function()
-- Id update is needed to notify that all previous work is not current
H.info.id = H.info.id + 1
H.info.timer:stop()
H.cancel_lsp({ H.info })
H.close_action_window(H.info)
end
H.stop_signature = function()
H.signature.text = nil
H.signature.timer:stop()
H.cancel_lsp({ H.signature })
H.close_action_window(H.signature)
end
H.stop_actions = {
completion = H.stop_completion,
info = H.stop_info,
signature = H.stop_signature,
}
-- LSP ------------------------------------------------------------------------
---@param capability string|table|nil Server capability (possibly nested
--- supplied via table) to check.
---
---@return boolean Whether at least one LSP client supports `capability`.
---@private
H.has_lsp_clients = function(capability)
local clients = H.get_buf_lsp_clients()
if vim.tbl_isempty(clients) then return false end
if not capability then return true end
for _, c in pairs(clients) do
local has_capability = H.table_get(c.server_capabilities, capability)
if has_capability then return true end
end
return false
end
H.has_lsp_completion = function()
local source_func = H.get_config().lsp_completion.source_func
local func = vim.bo[source_func]
return func == 'v:lua.MiniCompletion.completefunc_lsp'
end
H.is_lsp_trigger = function(char, type)
local triggers
local providers = { completion = 'completionProvider', signature = 'signatureHelpProvider' }
for _, client in pairs(H.get_buf_lsp_clients()) do
triggers = H.table_get(client, { 'server_capabilities', providers[type], 'triggerCharacters' })
if vim.tbl_contains(triggers or {}, char) then return true end
end
return false
end
H.cancel_lsp = function(caches)
caches = caches or { H.completion, H.info, H.signature }
for _, c in pairs(caches) do
if vim.tbl_contains({ 'sent', 'received' }, c.lsp.status) then
if c.lsp.cancel_fun then c.lsp.cancel_fun() end
c.lsp.status = 'canceled'
end
c.lsp.result, c.lsp.cancel_fun = nil, nil
end
end
H.process_lsp_response = function(request_result, processor)
if not request_result then return {} end
local res = {}
for client_id, item in pairs(request_result) do
if not item.err and item.result then vim.list_extend(res, processor(item.result, client_id) or {}) end
end
return res
end
H.is_lsp_current = function(cache, id) return cache.lsp.id == id and cache.lsp.status == 'sent' end
-- Completion -----------------------------------------------------------------
-- This is a truncated version of
-- `vim.lsp.util.text_document_completion_list_to_complete_items` which does
-- not filter and sort items.
-- For extra information see 'Response' section:
-- https://microsoft.github.io/language-server-protocol/specifications/specification-3-14/#textDocument_completion
H.lsp_completion_response_items_to_complete_items = function(items, client_id)
if vim.tbl_count(items) == 0 then return {} end
local res, item_kinds = {}, vim.lsp.protocol.CompletionItemKind
for _, item in pairs(items) do
-- Documentation info
local docs = item.documentation
local info = H.table_get(docs, { 'value' })
if not info and type(docs) == 'string' then info = docs end
info = info or ''
table.insert(res, {
word = H.get_completion_word(item),
abbr = item.label,
kind = item_kinds[item.kind] or 'Unknown',
kind_hlgroup = item.kind_hlgroup,
menu = item.detail or '',
info = info,
icase = 1,
dup = 1,
empty = 1,
user_data = { nvim = { lsp = { completion_item = item, client_id = client_id } } },
})
end
return res
end
H.make_add_kind_hlgroup = function()
-- Account for possible effect of `MiniIcons.tweak_lsp_kind()` which modifies
-- only array part of `CompletionItemKind` but not "map" part
if H.kind_map == nil then
-- Cache kind map so as to not recompute it each time (as it will be called
-- in performance sensitive context). Assumes `tweak_lsp_kind()` is called
-- right after `require('mini.icons').setup()`.
H.kind_map = {}
for k, v in pairs(vim.lsp.protocol.CompletionItemKind) do
if type(k) == 'string' and type(v) == 'number' then H.kind_map[v] = k end
end
end
return function(item)
local _, hl, is_default = _G.MiniIcons.get('lsp', H.kind_map[item.kind] or 'Unknown')
item.kind_hlgroup = not is_default and hl or nil
end
end
H.get_completion_word = function(item)
-- Completion word (textEdit.newText > insertText > label). This doesn't
-- support snippet expansion.
return H.table_get(item, { 'textEdit', 'newText' }) or item.insertText or item.label or ''
end
H.apply_additional_text_edits = function()
-- Code originally.inspired by https://github.com/neovim/neovim/issues/12310
-- Try to get `additionalTextEdits`. First from 'completionItem/resolve';
-- then - from selected item. The reason for this is inconsistency in how
-- servers provide `additionTextEdits`: on 'textDocument/completion' or
-- 'completionItem/resolve'.
local resolve_data = H.process_lsp_response(H.info.lsp.result, function(response, client_id)
-- Return nested table because this will be a second argument of
-- `vim.list_extend()` and the whole inner table is a target value here.
return { { edits = response.additionalTextEdits, client_id = client_id } }
end)
local edits, client_id
if #resolve_data >= 1 then
edits, client_id = resolve_data[1].edits, resolve_data[1].client_id
else
local lsp_data = H.table_get(vim.v.completed_item, { 'user_data', 'nvim', 'lsp' }) or {}
edits = H.table_get(lsp_data, { 'completion_item', 'additionalTextEdits' })
client_id = lsp_data.client_id
end
if edits == nil then return end
client_id = client_id or 0
-- Use extmark to track relevant cursor position after text edits
local cur_pos = vim.api.nvim_win_get_cursor(0)
local extmark_id = vim.api.nvim_buf_set_extmark(0, H.ns_id, cur_pos[1] - 1, cur_pos[2], {})
local offset_encoding = vim.lsp.get_client_by_id(client_id).offset_encoding
vim.lsp.util.apply_text_edits(edits, vim.api.nvim_get_current_buf(), offset_encoding)
local extmark_data = vim.api.nvim_buf_get_extmark_by_id(0, H.ns_id, extmark_id, {})
pcall(vim.api.nvim_buf_del_extmark, 0, H.ns_id, extmark_id)
pcall(vim.api.nvim_win_set_cursor, 0, { extmark_data[1] + 1, extmark_data[2] })
end
-- Completion item info -------------------------------------------------------
H.show_info_window = function()
local event = H.info.event
if not event then return end
-- Try first to take lines from LSP request result.
local lines
if H.info.lsp.status == 'received' then
lines = H.process_lsp_response(H.info.lsp.result, function(response)
if not response.documentation then return {} end
local res = vim.lsp.util.convert_input_to_markdown_lines(response.documentation)
return H.normalize_lines(res)
end)
H.info.lsp.status = 'done'
else
lines = H.info_window_lines(H.info.id)
end
-- Don't show anything if there is nothing to show
if not lines or H.is_whitespace(lines) then return end
-- If not already, create a permanent buffer where info will be
-- displayed. For some reason, it is important to have it created not in
-- `setup()` because in that case there is a small flash (which is really a
-- brief open of window at screen top, focus on it, and its close) on the
-- first show of info window.
H.ensure_buffer(H.info, 'MiniCompletion:completion-item-info')
-- Add `lines` to info buffer. Use `wrap_at` to have proper width of
-- 'non-UTF8' section separators.
H.stylize_markdown(H.info.bufnr, lines, { wrap_at = H.get_config().window.info.width })
-- Compute floating window options
local opts = H.info_window_options()
-- Defer execution because of textlock during `CompleteChanged` event
vim.schedule(function()