@@ -4,6 +4,9 @@ local lsp = vim.lsp
4
4
local protocol = lsp .protocol
5
5
local ms = protocol .Methods
6
6
7
+ local rtt_ms = 50
8
+ local ns_to_ms = 0.000001
9
+
7
10
--- @alias vim.lsp.CompletionResult lsp.CompletionList | lsp.CompletionItem[]
8
11
9
12
-- TODO(mariasolos): Remove this declaration once we figure out a better way to handle
@@ -14,11 +17,96 @@ local ms = protocol.Methods
14
17
--- @field insertTextMode lsp.InsertTextMode ?
15
18
--- @field data any
16
19
20
+ --- @class vim.lsp.completion.BufHandle
21
+ --- @field clients table<integer , vim.lsp.Client>
22
+ --- @field triggers table<string , vim.lsp.Client[]>
23
+
24
+ --- @type table<integer , vim.lsp.completion.BufHandle>
25
+ local buf_handles = {}
26
+
27
+ --- @class vim.lsp.completion.Context
28
+ --- @field last_request_time integer ?
29
+ --- @field pending_requests function[]
30
+ --- @field cursor { [1] : integer , [2] : integer }?
31
+ --- @field isIncomplete boolean
32
+ --- @field suppress_completeDone boolean
33
+ local Context = {
34
+ pending_requests = {},
35
+ isIncomplete = false ,
36
+ suppress_completeDone = false ,
37
+ }
38
+
39
+ function Context :cancel_pending ()
40
+ for _ , cancel in ipairs (self .pending_requests ) do
41
+ cancel ()
42
+ end
43
+
44
+ self .pending_requests = {}
45
+ end
46
+
47
+ function Context :reset ()
48
+ -- Note that the cursor isn't reset here, it needs to survive a `CompleteDone` event.
49
+ self .last_request_time = nil
50
+ self :cancel_pending ()
51
+ end
52
+
53
+ --- @type uv.uv_timer_t ?
54
+ local completion_timer = nil
55
+
56
+ --- @param timer uv.uv_timer_t ?
57
+ --- @return uv.uv_timer_t
58
+ local function new_timer (timer )
59
+ timer = assert (vim .uv .new_timer ())
60
+ return timer
61
+ end
62
+
63
+ --- @param timer uv.uv_timer_t ?
64
+ local function reset_timer (timer )
65
+ if timer then
66
+ timer :stop ()
67
+ timer :close ()
68
+ end
69
+
70
+ timer = nil
71
+ end
72
+
73
+ --- @param window integer
74
+ --- @param warmup integer
75
+ --- @return fun ( sample : number ): number
76
+ local function exp_avg (window , warmup )
77
+ local count = 0
78
+ local sum = 0
79
+ local value = 0
80
+
81
+ return function (sample )
82
+ if count < warmup then
83
+ count = count + 1
84
+ sum = sum + sample
85
+ value = sum / count
86
+ else
87
+ local factor = 2.0 / (window + 1 )
88
+ value = value * (1 - factor ) + sample * factor
89
+ end
90
+ return value
91
+ end
92
+ end
93
+ local compute_new_average = exp_avg (10 , 10 )
94
+
95
+ --- @return number
96
+ local function next_debounce ()
97
+ if not Context .last_request_time then
98
+ return rtt_ms
99
+ end
100
+
101
+ local ms_since_request = (vim .uv .hrtime () - Context .last_request_time ) * ns_to_ms
102
+ return math.max ((ms_since_request - rtt_ms ) * - 1 , 0 )
103
+ end
104
+
17
105
--- @param input string unparsed snippet
18
106
--- @return string parsed snippet
19
107
local function parse_snippet (input )
20
108
local ok , parsed = pcall (function ()
21
- return vim . lsp ._snippet_grammar .parse (input )
109
+ return lsp ._snippet_grammar .parse (input )
22
110
end )
23
111
return ok and tostring (parsed ) or input
24
112
end
@@ -159,7 +247,7 @@ local function adjust_start_col(lnum, line, items, encoding)
159
247
end
160
248
end
161
249
if min_start_char then
162
- return vim . lsp .util ._str_byteindex_enc (line , min_start_char , encoding )
250
+ return lsp .util ._str_byteindex_enc (line , min_start_char , encoding )
163
251
else
164
252
return nil
165
253
end
@@ -246,7 +334,7 @@ function M.omnifunc(findstart, base)
246
334
local params = util .make_position_params (win , client .offset_encoding )
247
335
client .request (ms .textDocument_completion , params , function (err , result )
248
336
if err then
249
- vim . lsp .log .warn (err .message )
337
+ lsp .log .warn (err .message )
250
338
end
251
339
if result and vim .fn .mode () == ' i' then
252
340
local matches
@@ -273,4 +361,257 @@ function M.omnifunc(findstart, base)
273
361
return - 2
274
362
end
275
363
364
+ --- Initializes the completion commands for the given client.
365
+ ---
366
+ --- @param client vim.lsp.Client
367
+ local function init_commands (client )
368
+ local trigger_completion_cmd = ' editor.action.triggerSuggest'
369
+
370
+ -- Check if the command is in the global registry or in the client's commands.
371
+ if not lsp .commands [trigger_completion_cmd ] and not client .commands [trigger_completion_cmd ] then
372
+ client .commands [trigger_completion_cmd ] = function ()
373
+ pcall (M .trigger_completion )
374
+ end
375
+ end
376
+ end
377
+
378
+ --- @param clients table<integer , vim.lsp.Client>
379
+ --- @param bufnr integer
380
+ --- @param win integer
381
+ --- @param callback fun ( responses : table<integer , { err : lsp.ResponseError , result : vim.lsp.CompletionResult } >)
382
+ --- @return function # Cancellation function
383
+ local function request (clients , bufnr , win , callback )
384
+ local responses = {} --- @type table<integer , { err : lsp.ResponseError , result : any } >
385
+ local request_ids = {} --- @type table<integer , integer>
386
+ local remaining_requests = vim .tbl_count (clients )
387
+
388
+ for client_id , client in pairs (clients ) do
389
+ local params = lsp .util .make_position_params (win , client .offset_encoding )
390
+ local ok , request_id = client .request (ms .textDocument_completion , params , function (err , result )
391
+ responses [client_id ] = { err = err , result = result }
392
+ remaining_requests = remaining_requests - 1
393
+ if remaining_requests == 0 then
394
+ callback (responses )
395
+ end
396
+ end , bufnr )
397
+
398
+ if ok then
399
+ request_ids [client_id ] = request_id
400
+ end
401
+ end
402
+
403
+ return function ()
404
+ for client_id , request_id in pairs (request_ids ) do
405
+ local client = lsp .get_client_by_id (client_id )
406
+ if client then
407
+ client .cancel_request (request_id )
408
+ end
409
+ end
410
+ end
411
+ end
412
+
413
+ --- @param handle vim.lsp.completion.BufHandle
414
+ local function insert_char_pre_cb (handle )
415
+ if tonumber (vim .fn .pumvisible ()) == 1 then
416
+ if Context .isIncomplete then
417
+ reset_timer (completion_timer )
418
+
419
+ -- Calling vim.fn.complete while pumvisible will trigger `CompleteDone` for the active completion window,
420
+ -- so we suppress it to avoid resetting the completion context.
421
+ Context .suppress_completeDone = true
422
+
423
+ local debounce_ms = next_debounce ()
424
+ if debounce_ms == 0 then
425
+ vim .schedule (M .trigger_completion )
426
+ else
427
+ completion_timer = new_timer (completion_timer )
428
+ completion_timer :start (debounce_ms , 0 , vim .schedule_wrap (M .trigger_completion ))
429
+ end
430
+ end
431
+
432
+ return
433
+ end
434
+
435
+ local char = api .nvim_get_vvar (' char' )
436
+ if not completion_timer and handle .triggers [char ] then
437
+ completion_timer = assert (vim .uv .new_timer ())
438
+ completion_timer :start (25 , 0 , function ()
439
+ reset_timer (completion_timer )
440
+ vim .schedule (M .trigger_completion )
441
+ end )
442
+ end
443
+ end
444
+
445
+ local function text_changed_p_cb ()
446
+ Context .cursor = api .nvim_win_get_cursor (0 )
447
+ end
448
+
449
+ local function text_changed_i_cb ()
450
+ if not Context .cursor or completion_timer then
451
+ return
452
+ end
453
+
454
+ local cursor = api .nvim_win_get_cursor (0 )
455
+ if cursor [1 ] == Context .cursor [1 ] and cursor [2 ] <= Context .cursor [2 ] then
456
+ completion_timer = new_timer (completion_timer )
457
+ completion_timer :start (150 , 0 , vim .schedule_wrap (M .trigger_completion ))
458
+ elseif cursor [1 ] ~= Context .cursor [1 ] then
459
+ Context .cursor = nil
460
+ end
461
+ end
462
+
463
+ local function insert_leave_cb ()
464
+ reset_timer (completion_timer )
465
+ Context .cursor = nil
466
+ Context :reset ()
467
+ end
468
+
469
+ local function complete_done_cb ()
470
+ if Context .suppress_completeDone then
471
+ Context .suppress_completeDone = false
472
+ return
473
+ end
474
+ end
475
+
476
+ function M .trigger_completion ()
477
+ reset_timer (completion_timer )
478
+ Context :cancel_pending ()
479
+
480
+ local win = api .nvim_get_current_win ()
481
+ local bufnr = api .nvim_get_current_buf ()
482
+ local cursor_row , cursor_col = unpack (api .nvim_win_get_cursor (win )) --- @type integer , integer
483
+ local line = api .nvim_get_current_line ()
484
+ local line_to_cursor = line :sub (1 , cursor_col )
485
+ local clients = (buf_handles [bufnr ] or {}).clients or {}
486
+ local word_boundary = vim .fn .match (line_to_cursor , ' \\ k*$' )
487
+ local start_time = vim .uv .hrtime ()
488
+ Context .last_request_time = start_time
489
+
490
+ local cancel_request = request (clients , bufnr , win , function (responses )
491
+ local end_time = vim .uv .hrtime ()
492
+ rtt_ms = compute_new_average ((end_time - start_time ) * ns_to_ms )
493
+
494
+ Context .pending_requests = {}
495
+ Context .isIncomplete = false
496
+
497
+ local row_changed = api .nvim_win_get_cursor (win )[1 ] ~= cursor_row
498
+ local mode = api .nvim_get_mode ().mode
499
+ if row_changed or not (mode == ' i' or mode == ' ic' ) then
500
+ return
501
+ end
502
+
503
+ local matches = {}
504
+ local server_start_boundary --- @type integer ?
505
+ for client_id , response in pairs (responses ) do
506
+ if response .err then
507
+ lsp .log .warn (response .err .message )
508
+ end
509
+
510
+ local result = response .result
511
+ if result then
512
+ Context .isIncomplete = Context .isIncomplete or result .isIncomplete
513
+ local client = lsp .get_client_by_id (client_id )
514
+ local encoding = client and client .offset_encoding or ' utf-16'
515
+ local client_matches
516
+ client_matches , server_start_boundary =
517
+ M ._convert_results (line , cursor_row - 1 , cursor_col , word_boundary , nil , result , encoding )
518
+ vim .list_extend (matches , client_matches )
519
+ end
520
+ end
521
+ local start_col = (server_start_boundary or word_boundary ) + 1
522
+ vim .fn .complete (start_col , matches )
523
+ end )
524
+
525
+ table.insert (Context .pending_requests , cancel_request )
526
+ end
527
+
528
+ --- @param client_id integer Client ID
529
+ --- @param bufnr integer Buffer handle , or 0 for the current buffer
530
+ function M .attach (client_id , bufnr )
531
+ bufnr = (bufnr == 0 and api .nvim_get_current_buf ()) or bufnr
532
+
533
+ if not buf_handles [bufnr ] then
534
+ buf_handles [bufnr ] = { clients = {}, triggers = {} }
535
+
536
+ -- Attach to buffer events.
537
+ api .nvim_buf_attach (bufnr , false , {
538
+ on_detach = function (_ , buf )
539
+ buf_handles [buf ] = nil
540
+ end ,
541
+ on_reload = function (_ , buf )
542
+ M .attach (client_id , buf )
543
+ end ,
544
+ })
545
+
546
+ -- Set up autocommands.
547
+ local group =
548
+ api .nvim_create_augroup (string.format (' vim/lsp/completion-%d' , bufnr ), { clear = true })
549
+ api .nvim_create_autocmd (' InsertCharPre' , {
550
+ group = group ,
551
+ buffer = bufnr ,
552
+ callback = function ()
553
+ insert_char_pre_cb (buf_handles [bufnr ])
554
+ end ,
555
+ })
556
+ api .nvim_create_autocmd (' TextChangedP' , {
557
+ group = group ,
558
+ buffer = bufnr ,
559
+ callback = function ()
560
+ text_changed_p_cb ()
561
+ end ,
562
+ })
563
+ api .nvim_create_autocmd (' TextChangedI' , {
564
+ group = group ,
565
+ buffer = bufnr ,
566
+ callback = function ()
567
+ text_changed_i_cb ()
568
+ end ,
569
+ })
570
+ api .nvim_create_autocmd (' InsertLeave' , {
571
+ group = group ,
572
+ buffer = bufnr ,
573
+ callback = function ()
574
+ insert_leave_cb ()
575
+ end ,
576
+ })
577
+ api .nvim_create_autocmd (' CompleteDone' , {
578
+ group = group ,
579
+ buffer = bufnr ,
580
+ callback = complete_done_cb ,
581
+ })
582
+ end
583
+
584
+ if not buf_handles [bufnr ].clients [client_id ] then
585
+ local client = lsp .get_client_by_id (client_id )
586
+ assert (client , ' invalid client ID' )
587
+
588
+ -- Add the new client to the buffer's clients.
589
+ init_commands (client )
590
+ buf_handles [bufnr ].clients [client_id ] = client
591
+
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_handles [bufnr ].triggers [char ]
601
+ if not clients_for_trigger then
602
+ clients_for_trigger = {}
603
+ buf_handles [bufnr ].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
+
615
+ -- TODO(mariasolos): Add M.detach if we decide to use the attach approach.
616
+
276
617
return M
0 commit comments