mirror of
				https://github.com/neovim/neovim.git
				synced 2025-10-26 12:27:24 +00:00 
			
		
		
		
	fix(lsp): handle multiline signature help labels #30460
This commit is contained in:
		 Maria José Solano
					Maria José Solano
				
			
				
					committed by
					
						 GitHub
						GitHub
					
				
			
			
				
	
			
			
			 GitHub
						GitHub
					
				
			
						parent
						
							74a1614772
						
					
				
				
					commit
					e0a5c3bb58
				
			| @@ -1855,7 +1855,7 @@ signature_help({_}, {result}, {ctx}, {config}) | |||||||
| < | < | ||||||
|  |  | ||||||
|     Parameters: ~ |     Parameters: ~ | ||||||
|       • {result}  (`lsp.SignatureHelp`) Response from the language server |       • {result}  (`lsp.SignatureHelp?`) Response from the language server | ||||||
|       • {ctx}     (`lsp.HandlerContext`) Client context |       • {ctx}     (`lsp.HandlerContext`) Client context | ||||||
|       • {config}  (`table`) Configuration table. |       • {config}  (`table`) Configuration table. | ||||||
|                   • border: (default=nil) |                   • border: (default=nil) | ||||||
| @@ -1973,7 +1973,7 @@ convert_signature_help_to_markdown_lines({signature_help}, {ft}, {triggers}) | |||||||
|  |  | ||||||
|     Return (multiple): ~ |     Return (multiple): ~ | ||||||
|         (`string[]?`) table list of lines of converted markdown. |         (`string[]?`) table list of lines of converted markdown. | ||||||
|         (`number[]?`) table of active hl |         (`Range4?`) table of active hl | ||||||
|  |  | ||||||
|     See also: ~ |     See also: ~ | ||||||
|       • https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_signatureHelp |       • https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_signatureHelp | ||||||
|   | |||||||
| @@ -462,6 +462,8 @@ M[ms.textDocument_typeDefinition] = location_handler | |||||||
| --- @see # https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_implementation | --- @see # https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_implementation | ||||||
| M[ms.textDocument_implementation] = location_handler | M[ms.textDocument_implementation] = location_handler | ||||||
|  |  | ||||||
|  | local sig_help_ns = api.nvim_create_namespace('vim_lsp_signature_help') | ||||||
|  |  | ||||||
| --- |lsp-handler| for the method "textDocument/signatureHelp". | --- |lsp-handler| for the method "textDocument/signatureHelp". | ||||||
| --- | --- | ||||||
| --- The active parameter is highlighted with |hl-LspSignatureActiveParameter|. | --- The active parameter is highlighted with |hl-LspSignatureActiveParameter|. | ||||||
| @@ -476,7 +478,7 @@ M[ms.textDocument_implementation] = location_handler | |||||||
| --- ``` | --- ``` | ||||||
| --- | --- | ||||||
| ---@param _ lsp.ResponseError? | ---@param _ lsp.ResponseError? | ||||||
| ---@param result lsp.SignatureHelp  Response from the language server | ---@param result lsp.SignatureHelp? Response from the language server | ||||||
| ---@param ctx lsp.HandlerContext Client context | ---@param ctx lsp.HandlerContext Client context | ||||||
| ---@param config table Configuration table. | ---@param config table Configuration table. | ||||||
| ---     - border:     (default=nil) | ---     - border:     (default=nil) | ||||||
| @@ -509,10 +511,15 @@ function M.signature_help(_, result, ctx, config) | |||||||
|     return |     return | ||||||
|   end |   end | ||||||
|   local fbuf, fwin = util.open_floating_preview(lines, 'markdown', config) |   local fbuf, fwin = util.open_floating_preview(lines, 'markdown', config) | ||||||
|  |   -- Highlight the active parameter. | ||||||
|   if hl then |   if hl then | ||||||
|     -- Highlight the second line if the signature is wrapped in a Markdown code block. |     vim.highlight.range( | ||||||
|     local line = vim.startswith(lines[1], '```') and 1 or 0 |       fbuf, | ||||||
|     api.nvim_buf_add_highlight(fbuf, -1, 'LspSignatureActiveParameter', line, unpack(hl)) |       sig_help_ns, | ||||||
|  |       'LspSignatureActiveParameter', | ||||||
|  |       { hl[1], hl[2] }, | ||||||
|  |       { hl[3], hl[4] } | ||||||
|  |     ) | ||||||
|   end |   end | ||||||
|   return fbuf, fwin |   return fbuf, fwin | ||||||
| end | end | ||||||
|   | |||||||
| @@ -781,24 +781,37 @@ function M.convert_input_to_markdown_lines(input, contents) | |||||||
|   return contents |   return contents | ||||||
| end | end | ||||||
|  |  | ||||||
|  | --- Returns the line/column-based position in `contents` at the given offset. | ||||||
|  | --- | ||||||
|  | ---@param offset integer | ||||||
|  | ---@param contents string[] | ||||||
|  | ---@return { [1]: integer, [2]: integer } | ||||||
|  | local function get_pos_from_offset(offset, contents) | ||||||
|  |   local i = 0 | ||||||
|  |   for l, line in ipairs(contents) do | ||||||
|  |     if offset >= i and offset < i + #line then | ||||||
|  |       return { l - 1, offset - i + 1 } | ||||||
|  |     else | ||||||
|  |       i = i + #line + 1 | ||||||
|  |     end | ||||||
|  |   end | ||||||
|  | end | ||||||
|  |  | ||||||
| --- Converts `textDocument/signatureHelp` response to markdown lines. | --- Converts `textDocument/signatureHelp` response to markdown lines. | ||||||
| --- | --- | ||||||
| ---@param signature_help lsp.SignatureHelp Response of `textDocument/SignatureHelp` | ---@param signature_help lsp.SignatureHelp Response of `textDocument/SignatureHelp` | ||||||
| ---@param ft string|nil filetype that will be use as the `lang` for the label markdown code block | ---@param ft string|nil filetype that will be use as the `lang` for the label markdown code block | ||||||
| ---@param triggers table|nil list of trigger characters from the lsp server. used to better determine parameter offsets | ---@param triggers table|nil list of trigger characters from the lsp server. used to better determine parameter offsets | ||||||
| ---@return string[]|nil table list of lines of converted markdown. | ---@return string[]|nil table list of lines of converted markdown. | ||||||
| ---@return number[]|nil table of active hl | ---@return Range4|nil table of active hl | ||||||
| ---@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_signatureHelp | ---@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_signatureHelp | ||||||
| function M.convert_signature_help_to_markdown_lines(signature_help, ft, triggers) | function M.convert_signature_help_to_markdown_lines(signature_help, ft, triggers) | ||||||
|   if not signature_help.signatures then |  | ||||||
|     return |  | ||||||
|   end |  | ||||||
|   --The active signature. If omitted or the value lies outside the range of |   --The active signature. If omitted or the value lies outside the range of | ||||||
|   --`signatures` the value defaults to zero or is ignored if `signatures.length == 0`. |   --`signatures` the value defaults to zero or is ignored if `signatures.length == 0`. | ||||||
|   --Whenever possible implementors should make an active decision about |   --Whenever possible implementors should make an active decision about | ||||||
|   --the active signature and shouldn't rely on a default value. |   --the active signature and shouldn't rely on a default value. | ||||||
|   local contents = {} |   local contents = {} | ||||||
|   local active_hl |   local active_offset ---@type [integer, integer]|nil | ||||||
|   local active_signature = signature_help.activeSignature or 0 |   local active_signature = signature_help.activeSignature or 0 | ||||||
|   -- If the activeSignature is not inside the valid range, then clip it. |   -- If the activeSignature is not inside the valid range, then clip it. | ||||||
|   -- In 3.15 of the protocol, activeSignature was allowed to be negative |   -- In 3.15 of the protocol, activeSignature was allowed to be negative | ||||||
| @@ -806,9 +819,6 @@ function M.convert_signature_help_to_markdown_lines(signature_help, ft, triggers | |||||||
|     active_signature = 0 |     active_signature = 0 | ||||||
|   end |   end | ||||||
|   local signature = signature_help.signatures[active_signature + 1] |   local signature = signature_help.signatures[active_signature + 1] | ||||||
|   if not signature then |  | ||||||
|     return |  | ||||||
|   end |  | ||||||
|   local label = signature.label |   local label = signature.label | ||||||
|   if ft then |   if ft then | ||||||
|     -- wrap inside a code block for proper rendering |     -- wrap inside a code block for proper rendering | ||||||
| @@ -825,6 +835,8 @@ function M.convert_signature_help_to_markdown_lines(signature_help, ft, triggers | |||||||
|     M.convert_input_to_markdown_lines(signature.documentation, contents) |     M.convert_input_to_markdown_lines(signature.documentation, contents) | ||||||
|   end |   end | ||||||
|   if signature.parameters and #signature.parameters > 0 then |   if signature.parameters and #signature.parameters > 0 then | ||||||
|  |     -- First check if the signature has an activeParameter. If it doesn't check if the response | ||||||
|  |     -- had that property instead. Else just default to 0. | ||||||
|     local active_parameter = (signature.activeParameter or signature_help.activeParameter or 0) |     local active_parameter = (signature.activeParameter or signature_help.activeParameter or 0) | ||||||
|     if active_parameter < 0 then |     if active_parameter < 0 then | ||||||
|       active_parameter = 0 |       active_parameter = 0 | ||||||
| @@ -837,8 +849,8 @@ function M.convert_signature_help_to_markdown_lines(signature_help, ft, triggers | |||||||
|     end |     end | ||||||
|  |  | ||||||
|     local parameter = signature.parameters[active_parameter + 1] |     local parameter = signature.parameters[active_parameter + 1] | ||||||
|     if parameter then |     local parameter_label = parameter.label | ||||||
|       --[=[ |     --[=[ | ||||||
|       --Represents a parameter of a callable-signature. A parameter can |       --Represents a parameter of a callable-signature. A parameter can | ||||||
|       --have a label and a doc-comment. |       --have a label and a doc-comment. | ||||||
|       interface ParameterInformation { |       interface ParameterInformation { | ||||||
| @@ -856,36 +868,47 @@ function M.convert_signature_help_to_markdown_lines(signature_help, ft, triggers | |||||||
|         documentation?: string | MarkupContent; |         documentation?: string | MarkupContent; | ||||||
|       } |       } | ||||||
|       --]=] |       --]=] | ||||||
|       if parameter.label then |     if type(parameter_label) == 'table' then | ||||||
|         if type(parameter.label) == 'table' then |       active_offset = parameter_label | ||||||
|           active_hl = parameter.label |     else | ||||||
|         else |       local offset = 1 ---@type integer|nil | ||||||
|           local offset = 1 |       -- try to set the initial offset to the first found trigger character | ||||||
|           -- try to set the initial offset to the first found trigger character |       for _, t in ipairs(triggers or {}) do | ||||||
|           for _, t in ipairs(triggers or {}) do |         local trigger_offset = signature.label:find(t, 1, true) | ||||||
|             local trigger_offset = signature.label:find(t, 1, true) |         if trigger_offset and (offset == 1 or trigger_offset < offset) then | ||||||
|             if trigger_offset and (offset == 1 or trigger_offset < offset) then |           offset = trigger_offset | ||||||
|               offset = trigger_offset |  | ||||||
|             end |  | ||||||
|           end |  | ||||||
|           for p, param in pairs(signature.parameters) do |  | ||||||
|             offset = signature.label:find(param.label, offset, true) |  | ||||||
|             if not offset then |  | ||||||
|               break |  | ||||||
|             end |  | ||||||
|             if p == active_parameter + 1 then |  | ||||||
|               active_hl = { offset - 1, offset + #parameter.label - 1 } |  | ||||||
|               break |  | ||||||
|             end |  | ||||||
|             offset = offset + #param.label + 1 |  | ||||||
|           end |  | ||||||
|         end |         end | ||||||
|       end |       end | ||||||
|       if parameter.documentation then |       for p, param in pairs(signature.parameters) do | ||||||
|         M.convert_input_to_markdown_lines(parameter.documentation, contents) |         offset = signature.label:find(param.label, offset, true) | ||||||
|  |         if not offset then | ||||||
|  |           break | ||||||
|  |         end | ||||||
|  |         if p == active_parameter + 1 then | ||||||
|  |           active_offset = { offset - 1, offset + #parameter_label - 1 } | ||||||
|  |           break | ||||||
|  |         end | ||||||
|  |         offset = offset + #param.label + 1 | ||||||
|       end |       end | ||||||
|     end |     end | ||||||
|  |     if parameter.documentation then | ||||||
|  |       M.convert_input_to_markdown_lines(parameter.documentation, contents) | ||||||
|  |     end | ||||||
|   end |   end | ||||||
|  |  | ||||||
|  |   local active_hl = nil | ||||||
|  |   if active_offset then | ||||||
|  |     active_hl = {} | ||||||
|  |     -- Account for the start of the markdown block. | ||||||
|  |     if ft then | ||||||
|  |       active_offset[1], active_offset[2] = | ||||||
|  |         active_offset[1] + #contents[1], active_offset[2] + #contents[1] | ||||||
|  |     end | ||||||
|  |  | ||||||
|  |     list_extend(active_hl, get_pos_from_offset(active_offset[1], contents)) | ||||||
|  |     list_extend(active_hl, get_pos_from_offset(active_offset[2], contents)) | ||||||
|  |   end | ||||||
|  |  | ||||||
|   return contents, active_hl |   return contents, active_hl | ||||||
| end | end | ||||||
|  |  | ||||||
|   | |||||||
| @@ -3519,6 +3519,30 @@ describe('LSP', function() | |||||||
|       local expected = { '```cs', 'TestEntity.TestEntity()', '```', 'some doc' } |       local expected = { '```cs', 'TestEntity.TestEntity()', '```', 'some doc' } | ||||||
|       eq(expected, result) |       eq(expected, result) | ||||||
|     end) |     end) | ||||||
|  |  | ||||||
|  |     it('highlights active parameters in multiline signature labels', function() | ||||||
|  |       local _, hl = exec_lua(function() | ||||||
|  |         local signature_help = { | ||||||
|  |           activeSignature = 0, | ||||||
|  |           signatures = { | ||||||
|  |             { | ||||||
|  |               activeParameter = 1, | ||||||
|  |               label = 'fn bar(\n    _: void,\n    _: void,\n) void', | ||||||
|  |               parameters = { | ||||||
|  |                 { label = '_: void' }, | ||||||
|  |                 { label = '_: void' }, | ||||||
|  |               }, | ||||||
|  |             }, | ||||||
|  |           }, | ||||||
|  |         } | ||||||
|  |         return vim.lsp.util.convert_signature_help_to_markdown_lines(signature_help, 'zig', { '(' }) | ||||||
|  |       end) | ||||||
|  |       -- Note that although the higlight positions below are 0-indexed, the 2nd parameter | ||||||
|  |       -- corresponds to the 3rd line because the first line is the ``` from the | ||||||
|  |       -- Markdown block. | ||||||
|  |       local expected = { 3, 4, 3, 11 } | ||||||
|  |       eq(expected, hl) | ||||||
|  |     end) | ||||||
|   end) |   end) | ||||||
|  |  | ||||||
|   describe('lsp.util.get_effective_tabstop', function() |   describe('lsp.util.get_effective_tabstop', function() | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user