mirror of
				https://github.com/neovim/neovim.git
				synced 2025-10-26 12:27:24 +00:00 
			
		
		
		
	feat(lsp): multi-client support for signature_help
Signatures can be cycled using `<C-s>` when the user enters the floating window.
This commit is contained in:
		 Lewis Russell
					Lewis Russell
				
			
				
					committed by
					
						 Lewis Russell
						Lewis Russell
					
				
			
			
				
	
			
			
			 Lewis Russell
						Lewis Russell
					
				
			
						parent
						
							0da4d89558
						
					
				
				
					commit
					6e68fed374
				
			| @@ -1911,7 +1911,7 @@ make_floating_popup_options({width}, {height}, {opts}) | ||||
|                   |vim.lsp.util.open_floating_preview.Opts|. | ||||
|  | ||||
|     Return: ~ | ||||
|         (`table`) Options | ||||
|         (`vim.api.keyset.win_config`) | ||||
|  | ||||
|                                        *vim.lsp.util.make_formatting_params()* | ||||
| make_formatting_params({options}) | ||||
|   | ||||
| @@ -209,6 +209,8 @@ LSP | ||||
|   `textDocument/rangesFormatting` request). | ||||
| • |vim.lsp.buf.code_action()| actions show client name when there are multiple | ||||
|   clients. | ||||
| • |vim.lsp.buf.signature_help()| can now cycle through different signatures | ||||
|   using `<C-s>` and also support multiple clients. | ||||
|  | ||||
| LUA | ||||
|  | ||||
|   | ||||
| @@ -258,6 +258,33 @@ function M.implementation(opts) | ||||
|   get_locations(ms.textDocument_implementation, opts) | ||||
| end | ||||
|  | ||||
| --- @param results table<integer,{err: lsp.ResponseError?, result: lsp.SignatureHelp?}> | ||||
| local function process_signature_help_results(results) | ||||
|   local signatures = {} --- @type [vim.lsp.Client,lsp.SignatureInformation][] | ||||
|  | ||||
|   -- Pre-process results | ||||
|   for client_id, r in pairs(results) do | ||||
|     local err = r.err | ||||
|     local client = assert(lsp.get_client_by_id(client_id)) | ||||
|     if err then | ||||
|       vim.notify( | ||||
|         client.name .. ': ' .. tostring(err.code) .. ': ' .. err.message, | ||||
|         vim.log.levels.ERROR | ||||
|       ) | ||||
|       api.nvim_command('redraw') | ||||
|     else | ||||
|       local result = r.result --- @type lsp.SignatureHelp | ||||
|       if result and result.signatures and result.signatures[1] then | ||||
|         for _, sig in ipairs(result.signatures) do | ||||
|           signatures[#signatures + 1] = { client, sig } | ||||
|         end | ||||
|       end | ||||
|     end | ||||
|   end | ||||
|  | ||||
|   return signatures | ||||
| end | ||||
|  | ||||
| local sig_help_ns = api.nvim_create_namespace('vim_lsp_signature_help') | ||||
|  | ||||
| --- @class vim.lsp.buf.signature_help.Opts : vim.lsp.util.open_floating_preview.Opts | ||||
| @@ -270,59 +297,80 @@ local sig_help_ns = api.nvim_create_namespace('vim_lsp_signature_help') | ||||
| function M.signature_help(config) | ||||
|   local method = ms.textDocument_signatureHelp | ||||
|  | ||||
|   config = config or {} | ||||
|   config = config and vim.deepcopy(config) or {} | ||||
|   config.focus_id = method | ||||
|  | ||||
|   lsp.buf_request(0, method, client_positional_params(), function(err, result, ctx) | ||||
|     local client = assert(vim.lsp.get_client_by_id(ctx.client_id)) | ||||
|  | ||||
|     if err then | ||||
|       vim.notify( | ||||
|         client.name .. ': ' .. tostring(err.code) .. ': ' .. err.message, | ||||
|         vim.log.levels.ERROR | ||||
|       ) | ||||
|       api.nvim_command('redraw') | ||||
|       return | ||||
|     end | ||||
|  | ||||
|   lsp.buf_request_all(0, method, client_positional_params(), function(results, ctx) | ||||
|     if api.nvim_get_current_buf() ~= ctx.bufnr then | ||||
|       -- Ignore result since buffer changed. This happens for slow language servers. | ||||
|       return | ||||
|     end | ||||
|  | ||||
|     -- When use `autocmd CompleteDone <silent><buffer> lua vim.lsp.buf.signature_help()` to call signatureHelp handler | ||||
|     -- If the completion item doesn't have signatures It will make noise. Change to use `print` that can use `<silent>` to ignore | ||||
|     if not result or not result.signatures or not result.signatures[1] then | ||||
|     local signatures = process_signature_help_results(results) | ||||
|  | ||||
|     if not next(signatures) then | ||||
|       if config.silent ~= true then | ||||
|         print('No signature help available') | ||||
|       end | ||||
|       return | ||||
|     end | ||||
|  | ||||
|     local triggers = | ||||
|       vim.tbl_get(client.server_capabilities, 'signatureHelpProvider', 'triggerCharacters') | ||||
|  | ||||
|     local ft = vim.bo[ctx.bufnr].filetype | ||||
|     local lines, hl = util.convert_signature_help_to_markdown_lines(result, ft, triggers) | ||||
|     if not lines or vim.tbl_isempty(lines) then | ||||
|       if config.silent ~= true then | ||||
|         print('No signature help available') | ||||
|       end | ||||
|     local total = #signatures | ||||
|     local idx = 0 | ||||
|  | ||||
|     --- @param update_win? integer | ||||
|     local function show_signature(update_win) | ||||
|       idx = (idx % total) + 1 | ||||
|       local client, result = signatures[idx][1], signatures[idx][2] | ||||
|       --- @type string[]? | ||||
|       local triggers = | ||||
|         vim.tbl_get(client.server_capabilities, 'signatureHelpProvider', 'triggerCharacters') | ||||
|       local lines, hl = | ||||
|         util.convert_signature_help_to_markdown_lines({ signatures = { result } }, ft, triggers) | ||||
|       if not lines then | ||||
|         return | ||||
|       end | ||||
|  | ||||
|     local fbuf = util.open_floating_preview(lines, 'markdown', config) | ||||
|  | ||||
|     -- Highlight the active parameter. | ||||
|       local sfx = total > 1 and string.format(' (%d/%d) (<C-s> to cycle)', idx, total) or '' | ||||
|       local title = string.format('Signature Help: %s%s', client.name, sfx) | ||||
|       if config.border then | ||||
|         config.title = title | ||||
|       else | ||||
|         table.insert(lines, 1, '# ' .. title) | ||||
|         if hl then | ||||
|           hl[1] = hl[1] + 1 | ||||
|           hl[3] = hl[3] + 1 | ||||
|         end | ||||
|       end | ||||
|  | ||||
|       config._update_win = update_win | ||||
|  | ||||
|       local buf, win = util.open_floating_preview(lines, 'markdown', config) | ||||
|  | ||||
|       if hl then | ||||
|         vim.api.nvim_buf_clear_namespace(buf, sig_help_ns, 0, -1) | ||||
|         vim.hl.range( | ||||
|         fbuf, | ||||
|           buf, | ||||
|           sig_help_ns, | ||||
|           'LspSignatureActiveParameter', | ||||
|           { hl[1], hl[2] }, | ||||
|           { hl[3], hl[4] } | ||||
|         ) | ||||
|       end | ||||
|       return buf, win | ||||
|     end | ||||
|  | ||||
|     local fbuf, fwin = show_signature() | ||||
|  | ||||
|     if total > 1 then | ||||
|       vim.keymap.set('n', '<C-s>', function() | ||||
|         show_signature(fwin) | ||||
|       end, { | ||||
|         buffer = fbuf, | ||||
|         desc = 'Cycle next signature', | ||||
|       }) | ||||
|     end | ||||
|   end) | ||||
| end | ||||
|  | ||||
|   | ||||
| @@ -737,7 +737,7 @@ function M.convert_signature_help_to_markdown_lines(signature_help, ft, triggers | ||||
|   if active_signature >= #signature_help.signatures or active_signature < 0 then | ||||
|     active_signature = 0 | ||||
|   end | ||||
|   local signature = signature_help.signatures[active_signature + 1] | ||||
|   local signature = vim.deepcopy(signature_help.signatures[active_signature + 1]) | ||||
|   local label = signature.label | ||||
|   if ft then | ||||
|     -- wrap inside a code block for proper rendering | ||||
| @@ -804,9 +804,11 @@ function M.convert_signature_help_to_markdown_lines(signature_help, ft, triggers | ||||
|       active_offset[2] = active_offset[2] + #contents[1] | ||||
|     end | ||||
|  | ||||
|     active_hl = {} | ||||
|     list_extend(active_hl, get_pos_from_offset(active_offset[1], contents) or {}) | ||||
|     list_extend(active_hl, get_pos_from_offset(active_offset[2], contents) or {}) | ||||
|     local a_start = get_pos_from_offset(active_offset[1], contents) | ||||
|     local a_end = get_pos_from_offset(active_offset[2], contents) | ||||
|     if a_start and a_end then | ||||
|       active_hl = { a_start[1], a_start[2], a_end[1], a_end[2] } | ||||
|     end | ||||
|   end | ||||
|  | ||||
|   return contents, active_hl | ||||
| @@ -818,7 +820,7 @@ end | ||||
| ---@param width integer window width (in character cells) | ||||
| ---@param height integer window height (in character cells) | ||||
| ---@param opts? vim.lsp.util.open_floating_preview.Opts | ||||
| ---@return table Options | ||||
| ---@return vim.api.keyset.win_config | ||||
| function M.make_floating_popup_options(width, height, opts) | ||||
|   validate('opts', opts, 'table', true) | ||||
|   opts = opts or {} | ||||
| @@ -1500,6 +1502,8 @@ end | ||||
| ---   to display the full window height. | ||||
| --- (default: `'auto'`) | ||||
| --- @field anchor_bias? 'auto'|'above'|'below' | ||||
| --- | ||||
| --- @field _update_win? integer | ||||
|  | ||||
| --- Shows contents in a floating window. | ||||
| --- | ||||
| @@ -1521,6 +1525,13 @@ function M.open_floating_preview(contents, syntax, opts) | ||||
|  | ||||
|   local bufnr = api.nvim_get_current_buf() | ||||
|  | ||||
|   local floating_winnr = opts._update_win | ||||
|  | ||||
|   -- Create/get the buffer | ||||
|   local floating_bufnr --- @type integer | ||||
|   if floating_winnr then | ||||
|     floating_bufnr = api.nvim_win_get_buf(floating_winnr) | ||||
|   else | ||||
|     -- check if this popup is focusable and we need to focus | ||||
|     if opts.focus_id and opts.focusable ~= false and opts.focus then | ||||
|       -- Go back to previous window if we are in a focusable one | ||||
| @@ -1546,18 +1557,17 @@ function M.open_floating_preview(contents, syntax, opts) | ||||
|     if existing_float and api.nvim_win_is_valid(existing_float) then | ||||
|       api.nvim_win_close(existing_float, true) | ||||
|     end | ||||
|  | ||||
|   -- Create the buffer | ||||
|   local floating_bufnr = api.nvim_create_buf(false, true) | ||||
|     floating_bufnr = api.nvim_create_buf(false, true) | ||||
|   end | ||||
|  | ||||
|   -- Set up the contents, using treesitter for markdown | ||||
|   local do_stylize = syntax == 'markdown' and vim.g.syntax_on ~= nil | ||||
|  | ||||
|   if do_stylize then | ||||
|     local width = M._make_floating_popup_size(contents, opts) | ||||
|     contents = M._normalize_markdown(contents, { width = width }) | ||||
|     vim.bo[floating_bufnr].filetype = 'markdown' | ||||
|     vim.treesitter.start(floating_bufnr) | ||||
|     api.nvim_buf_set_lines(floating_bufnr, 0, -1, false, contents) | ||||
|   else | ||||
|     -- Clean up input: trim empty lines | ||||
|     contents = vim.split(table.concat(contents, '\n'), '\n', { trimempty = true }) | ||||
| @@ -1565,30 +1575,31 @@ function M.open_floating_preview(contents, syntax, opts) | ||||
|     if syntax then | ||||
|       vim.bo[floating_bufnr].syntax = syntax | ||||
|     end | ||||
|     api.nvim_buf_set_lines(floating_bufnr, 0, -1, true, contents) | ||||
|   end | ||||
|  | ||||
|   vim.bo[floating_bufnr].modifiable = true | ||||
|   api.nvim_buf_set_lines(floating_bufnr, 0, -1, false, contents) | ||||
|  | ||||
|   if floating_winnr then | ||||
|     api.nvim_win_set_config(floating_winnr, { | ||||
|       border = opts.border, | ||||
|       title = opts.title, | ||||
|     }) | ||||
|   else | ||||
|     -- Compute size of float needed to show (wrapped) lines | ||||
|     if opts.wrap then | ||||
|       opts.wrap_at = opts.wrap_at or api.nvim_win_get_width(0) | ||||
|     else | ||||
|       opts.wrap_at = nil | ||||
|     end | ||||
|  | ||||
|     -- TODO(lewis6991): These function assume the current window to determine options, | ||||
|     -- therefore it won't work for opts._update_win and the current window if the floating | ||||
|     -- window | ||||
|     local width, height = M._make_floating_popup_size(contents, opts) | ||||
|  | ||||
|     local float_option = M.make_floating_popup_options(width, height, opts) | ||||
|   local floating_winnr = api.nvim_open_win(floating_bufnr, false, float_option) | ||||
|  | ||||
|   if do_stylize then | ||||
|     vim.wo[floating_winnr].conceallevel = 2 | ||||
|   end | ||||
|   vim.wo[floating_winnr].foldenable = false -- Disable folding. | ||||
|   vim.wo[floating_winnr].wrap = opts.wrap -- Soft wrapping. | ||||
|   vim.wo[floating_winnr].breakindent = true -- Slightly better list presentation. | ||||
|   vim.wo[floating_winnr].smoothscroll = true -- Scroll by screen-line instead of buffer-line. | ||||
|  | ||||
|   vim.bo[floating_bufnr].modifiable = false | ||||
|   vim.bo[floating_bufnr].bufhidden = 'wipe' | ||||
|     floating_winnr = api.nvim_open_win(floating_bufnr, false, float_option) | ||||
|  | ||||
|     api.nvim_buf_set_keymap( | ||||
|       floating_bufnr, | ||||
| @@ -1604,6 +1615,18 @@ function M.open_floating_preview(contents, syntax, opts) | ||||
|       api.nvim_win_set_var(floating_winnr, opts.focus_id, bufnr) | ||||
|     end | ||||
|     api.nvim_buf_set_var(bufnr, 'lsp_floating_preview', floating_winnr) | ||||
|   end | ||||
|  | ||||
|   if do_stylize then | ||||
|     vim.wo[floating_winnr].conceallevel = 2 | ||||
|   end | ||||
|   vim.wo[floating_winnr].foldenable = false -- Disable folding. | ||||
|   vim.wo[floating_winnr].wrap = opts.wrap -- Soft wrapping. | ||||
|   vim.wo[floating_winnr].breakindent = true -- Slightly better list presentation. | ||||
|   vim.wo[floating_winnr].smoothscroll = true -- Scroll by screen-line instead of buffer-line. | ||||
|  | ||||
|   vim.bo[floating_bufnr].modifiable = false | ||||
|   vim.bo[floating_bufnr].bufhidden = 'wipe' | ||||
|  | ||||
|   return floating_bufnr, floating_winnr | ||||
| end | ||||
|   | ||||
		Reference in New Issue
	
	Block a user