@@ -67,6 +67,7 @@ local Context = {
6767last_request_time = nil ,--- @type integer ?
6868pending_requests = {},--- @type function[]
6969isIncomplete = false ,
70+ resolve_handler = nil ,--- @type CompletionResolver ?
7071}
7172
7273--- @nodoc
@@ -84,6 +85,10 @@ function Context:reset()
8485self .isIncomplete = false
8586self .last_request_time = nil
8687self :cancel_pending ()
88+ if self .resolve_handler then
89+ self .resolve_handler :cleanup ()
90+ self .resolve_handler = nil
91+ end
8792end
8893
8994--- @type uv.uv_timer_t ?
105110
106111--- @param window integer
107112--- @param warmup integer
108- --- @return fun ( sample : number ): number
113+ --- @return fun ( sample : integer ): integer
109114local function exp_avg (window ,warmup )
110115local count = 0
111116local sum = 0
@@ -470,6 +475,217 @@ local function request(clients, bufnr, win, ctx, callback)
470475end
471476end
472477
478+ --- @param bufnr integer
479+ --- @return string
480+ local function get_augroup (bufnr )
481+ return string.format (' nvim.lsp.completion_%d' ,bufnr )
482+ end
483+
484+ --- Updates the completion preview popup: configures conceal level, applies Treesitter or
485+ --- fallback syntax, and resizes height to fit content
486+ ---
487+ --- @param winid integer
488+ --- @param bufnr integer
489+ --- @param ft ? string
490+ local function update_popup_window (winid ,bufnr ,ft )
491+ if winid and api .nvim_win_is_valid (winid )and bufnr and api .nvim_buf_is_valid (bufnr )then
492+ vim .wo [winid ].conceallevel = 2
493+ if ft then
494+ local ok = pcall (vim .treesitter .get_parser ,bufnr ,ft )
495+ if ok then
496+ vim .treesitter .start (bufnr ,vim .treesitter .language .get_lang (ft ))
497+ else
498+ vim .bo [bufnr ].filetype = ft
499+ vim .bo [bufnr ].syntax = ' on'
500+ end
501+ end
502+ local all = api .nvim_win_text_height (winid , {}).all
503+ api .nvim_win_set_height (winid ,all )
504+ end
505+ end
506+
507+ --- Handles the LSP completion item resolution process with debounce and timer management.
508+ --- This class resolves the completion item asynchronously and updates the documentation popup.
509+ ---
510+ --- @nodoc
511+ --- @class CompletionResolver
512+ --- @field timer uv.uv_timer_t ? Timer used for debouncing
513+ --- @field request_ids table<integer , integer> Table tracking ongoing requests
514+ --- @field bufnr integer ? Buffer number for which the resolution is triggered
515+ --- @field word string ? Word being completed
516+ --- @field last_request_time integer ? Last request timestamp
517+ --- @field doc_rtt_ms integer Last request timestamp
518+ --- @field doc_compute_new_average fun ( sample : integer ): integer Last request timestamp
519+ local CompletionResolver = {}
520+ CompletionResolver .__index = CompletionResolver
521+
522+ --- Creates a new instance of `resolve_completion_item`.
523+ ---
524+ --- @return CompletionResolver
525+ function CompletionResolver .new ()
526+ local self = setmetatable ({},CompletionResolver )
527+ self .timer = nil
528+ self .request_ids = {}
529+ self .bufnr = nil
530+ self .word = nil
531+ self .last_request_time = nil
532+ self .doc_rtt_ms = 100
533+ self .doc_compute_new_average = exp_avg (10 ,5 )
534+ return self
535+ end
536+
537+ --- Calculates the debounce time for the next LSP request based on the last request time.
538+ --- @return integer The time (in milliseconds )before the next request is made
539+ function CompletionResolver :next_debounce_time ()
540+ if not self .last_request_time then
541+ return self .doc_rtt_ms
542+ end
543+ local ms_since_request = (vim .uv .hrtime ()- self .last_request_time )* ns_to_ms
544+ return math.max ((ms_since_request - self .doc_rtt_ms )* - 1 ,0 )
545+ end
546+
547+ --- Cancels any pending requests.
548+ function CompletionResolver :cancel_pending_requests ()
549+ for client_id ,request_id in pairs (self .request_ids )do
550+ local client = vim .lsp .get_client_by_id (client_id )
551+ if client and client .requests and client .requests [request_id ]then
552+ client :cancel_request (request_id )
553+ end
554+ end
555+ self .request_ids = {}
556+ end
557+
558+ --- Cleans up the timer and cancels any ongoing requests.
559+ function CompletionResolver :cleanup ()
560+ if self .timer and not self .timer :is_closing ()then
561+ self .timer :stop ()
562+ self .timer :close ()
563+ self .timer = nil
564+ end
565+ self :cancel_pending_requests ()
566+ end
567+
568+ --- Checks if the completionItem/resolve request is valid by ensuring the buffer and word are valid.
569+ ---
570+ --- @return boolean , table Validity of the request and the completion info
571+ function CompletionResolver :is_valid ()
572+ local cmp_info = vim .fn .complete_info ({' selected' ,' completed' })
573+ return vim .api .nvim_buf_is_valid (self .bufnr )
574+ and vim .api .nvim_get_current_buf ()== self .bufnr
575+ and vim .startswith (vim .api .nvim_get_mode ().mode ,' i' )
576+ and tonumber (vim .fn .pumvisible ())== 1
577+ and (vim .tbl_get (cmp_info ,' completed' ,' word' )or ' ' )== self .word ,
578+ cmp_info
579+ end
580+
581+ --- Starts the resolution process for the completionItem/resolve.
582+ --- This includes starting a timer and making the LSP request after the debounce period.
583+ ---
584+ --- @param bufnr integer The buffer number where the request is triggered
585+ --- @param param table The parameters for the LSP request
586+ --- @param selected_word string The word being completed
587+ function CompletionResolver :request (bufnr ,param ,selected_word )
588+ self :cleanup ()
589+
590+ self .bufnr = bufnr
591+ self .word = selected_word
592+ local debounce_time = self :next_debounce_time ()
593+
594+ -- Create and start the timer
595+ self .timer = assert (vim .uv .new_timer ())
596+ self .timer :start (
597+ debounce_time ,
598+ 0 ,
599+ vim .schedule_wrap (function ()
600+ local valid ,cmp_info = self :is_valid ()
601+ if not valid then
602+ self :cleanup ()
603+ return
604+ end
605+
606+ -- Cancel any pending requests before sending a new one
607+ self :cancel_pending_requests ()
608+
609+ local client_id = vim .tbl_get (cmp_info .completed ,' user_data' ,' nvim' ,' lsp' ,' client_id' )
610+ local client = client_id and vim .lsp .get_client_by_id (client_id )
611+ if not client then
612+ return
613+ end
614+
615+ local start_time = vim .uv .hrtime ()
616+ self .last_request_time = start_time
617+
618+ local ok ,request_id = client :request (' completionItem/resolve' ,param ,function (err ,result )
619+ local end_time = vim .uv .hrtime ()
620+ local response_time = (end_time - start_time )* ns_to_ms
621+ self .doc_rtt_ms = self .doc_compute_new_average (response_time )
622+
623+ if err or not result or next (result )== nil then
624+ if err then
625+ vim .notify (err .message ,vim .log .levels .WARN )
626+ end
627+ return
628+ end
629+
630+ valid ,cmp_info = self :is_valid ()
631+ if not valid then
632+ return
633+ end
634+
635+ local value = vim .tbl_get (result ,' documentation' ,' value' )
636+ if not value then
637+ return
638+ end
639+ local windata = vim .api .nvim__complete_set (cmp_info .selected , {
640+ info = value ,
641+ })
642+ local kind = vim .tbl_get (result ,' documentation' ,' kind' )
643+ update_popup_window (windata .winid ,windata .bufnr ,kind )
644+ end ,bufnr )
645+
646+ if ok and request_id then
647+ self .request_ids [client .id ]= request_id
648+ end
649+ end )
650+ )
651+ end
652+
653+ --- Defines a CompleteChanged handler to request and display LSP completion item documentation
654+ --- via completionItem/resolve
655+ local function on_completechanged (group ,bufnr )
656+ api .nvim_create_autocmd (' CompleteChanged' , {
657+ group = group ,
658+ buffer = bufnr ,
659+ callback = function (args )
660+ local completed_item = vim .v .event .completed_item or {}
661+ if (completed_item .info or ' ' )~= ' ' then
662+ local data = vim .fn .complete_info ({' selected' })
663+ update_popup_window (data .preview_winid ,data .preview_bufnr )
664+ return
665+ end
666+
667+ if
668+ # lsp .get_clients ({
669+ id = vim .tbl_get (completed_item ,' user_data' ,' nvim' ,' lsp' ,' client_id' ),
670+ method = ' completionItem/resolve' ,
671+ bufnr = args .buf ,
672+ })== 0
673+ then
674+ return
675+ end
676+
677+ -- Retrieve the raw LSP completionItem from completed_item as the parameter for
678+ -- the completionItem/resolve request
679+ local param = vim .tbl_get (completed_item ,' user_data' ,' nvim' ,' lsp' ,' completion_item' )
680+ if param then
681+ Context .resolve_handler = Context .resolve_handler or CompletionResolver .new ()
682+ Context .resolve_handler :request (args .buf ,param ,completed_item .word )
683+ end
684+ end ,
685+ desc = ' Request and display LSP completion item documentation via completionItem/resolve' ,
686+ })
687+ end
688+
473689--- @param bufnr integer
474690--- @param clients vim.lsp.Client[]
475691--- @param ctx ? lsp.CompletionContext
@@ -554,6 +770,14 @@ local function trigger(bufnr, clients, ctx)
554770
555771local start_col = (server_start_boundary or word_boundary )+ 1
556772Context .cursor = {cursor_row ,start_col }
773+ if # matches > 0 and vim .regex (' \\ <popup\\ >' ):match_str (vim .o .completeopt )then
774+ local group = get_augroup (bufnr )
775+ if
776+ # api .nvim_get_autocmds ({buffer = bufnr ,event = ' CompleteChanged' ,group = group })== 0
777+ then
778+ on_completechanged (group ,bufnr )
779+ end
780+ end
557781vim .fn .complete (start_col ,matches )
558782end )
559783
@@ -587,7 +811,7 @@ local function on_insert_char_pre(handle)
587811return
588812end
589813
590- local char = api . nvim_get_vvar ( ' char' )
814+ local char = vim . v . char
591815local matched_clients = handle .triggers [char ]
592816-- Discard pending trigger char, complete the "latest" one.
593817-- Can happen if a mapping inputs multiple trigger chars simultaneously.
@@ -597,11 +821,10 @@ local function on_insert_char_pre(handle)
597821completion_timer :start (25 ,0 ,function ()
598822reset_timer ()
599823vim .schedule (function ()
600- trigger (
601- api .nvim_get_current_buf (),
602- matched_clients ,
603- {triggerKind = protocol .CompletionTriggerKind .TriggerCharacter ,triggerCharacter = char }
604- )
824+ trigger (api .nvim_get_current_buf (),matched_clients , {
825+ triggerKind = protocol .CompletionTriggerKind .TriggerCharacter ,
826+ triggerCharacter = char ,
827+ })
605828end )
606829end )
607830end
@@ -702,12 +925,6 @@ local function on_complete_done()
702925end
703926end
704927
705- --- @param bufnr integer
706- --- @return string
707- local function get_augroup (bufnr )
708- return string.format (' nvim.lsp.completion_%d' ,bufnr )
709- end
710-
711928--- @inlinedoc
712929--- @class vim.lsp.completion.BufferOpts
713930--- @field autotrigger ?boolean (default : false )When true , completion triggers automatically based on the server ' s `triggerCharacters`.
@@ -744,6 +961,7 @@ local function enable_completions(client_id, bufnr, opts)
744961end
745962end ,
746963 })
964+
747965if opts .autotrigger then
748966api .nvim_create_autocmd (' InsertCharPre' , {
749967group = group ,