mirror of
https://github.com/neovim/neovim
synced 2025-07-20 13:22:26 +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:
committed by
Lewis Russell
parent
0da4d89558
commit
6e68fed374
@ -1911,7 +1911,7 @@ make_floating_popup_options({width}, {height}, {opts})
|
|||||||
|vim.lsp.util.open_floating_preview.Opts|.
|
|vim.lsp.util.open_floating_preview.Opts|.
|
||||||
|
|
||||||
Return: ~
|
Return: ~
|
||||||
(`table`) Options
|
(`vim.api.keyset.win_config`)
|
||||||
|
|
||||||
*vim.lsp.util.make_formatting_params()*
|
*vim.lsp.util.make_formatting_params()*
|
||||||
make_formatting_params({options})
|
make_formatting_params({options})
|
||||||
|
@ -209,6 +209,8 @@ LSP
|
|||||||
`textDocument/rangesFormatting` request).
|
`textDocument/rangesFormatting` request).
|
||||||
• |vim.lsp.buf.code_action()| actions show client name when there are multiple
|
• |vim.lsp.buf.code_action()| actions show client name when there are multiple
|
||||||
clients.
|
clients.
|
||||||
|
• |vim.lsp.buf.signature_help()| can now cycle through different signatures
|
||||||
|
using `<C-s>` and also support multiple clients.
|
||||||
|
|
||||||
LUA
|
LUA
|
||||||
|
|
||||||
|
@ -258,6 +258,33 @@ function M.implementation(opts)
|
|||||||
get_locations(ms.textDocument_implementation, opts)
|
get_locations(ms.textDocument_implementation, opts)
|
||||||
end
|
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')
|
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
|
--- @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)
|
function M.signature_help(config)
|
||||||
local method = ms.textDocument_signatureHelp
|
local method = ms.textDocument_signatureHelp
|
||||||
|
|
||||||
config = config or {}
|
config = config and vim.deepcopy(config) or {}
|
||||||
config.focus_id = method
|
config.focus_id = method
|
||||||
|
|
||||||
lsp.buf_request(0, method, client_positional_params(), function(err, result, ctx)
|
lsp.buf_request_all(0, method, client_positional_params(), function(results, 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
|
|
||||||
|
|
||||||
if api.nvim_get_current_buf() ~= ctx.bufnr then
|
if api.nvim_get_current_buf() ~= ctx.bufnr then
|
||||||
-- Ignore result since buffer changed. This happens for slow language servers.
|
-- Ignore result since buffer changed. This happens for slow language servers.
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
|
|
||||||
-- When use `autocmd CompleteDone <silent><buffer> lua vim.lsp.buf.signature_help()` to call signatureHelp handler
|
local signatures = process_signature_help_results(results)
|
||||||
-- 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
|
if not next(signatures) then
|
||||||
if config.silent ~= true then
|
if config.silent ~= true then
|
||||||
print('No signature help available')
|
print('No signature help available')
|
||||||
end
|
end
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
|
|
||||||
local triggers =
|
|
||||||
vim.tbl_get(client.server_capabilities, 'signatureHelpProvider', 'triggerCharacters')
|
|
||||||
|
|
||||||
local ft = vim.bo[ctx.bufnr].filetype
|
local ft = vim.bo[ctx.bufnr].filetype
|
||||||
local lines, hl = util.convert_signature_help_to_markdown_lines(result, ft, triggers)
|
local total = #signatures
|
||||||
if not lines or vim.tbl_isempty(lines) then
|
local idx = 0
|
||||||
if config.silent ~= true then
|
|
||||||
print('No signature help available')
|
--- @param update_win? integer
|
||||||
end
|
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
|
return
|
||||||
end
|
end
|
||||||
|
|
||||||
local fbuf = util.open_floating_preview(lines, 'markdown', config)
|
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)
|
||||||
-- Highlight the active parameter.
|
if config.border then
|
||||||
|
config.title = title
|
||||||
|
else
|
||||||
|
table.insert(lines, 1, '# ' .. title)
|
||||||
if hl then
|
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(
|
vim.hl.range(
|
||||||
fbuf,
|
buf,
|
||||||
sig_help_ns,
|
sig_help_ns,
|
||||||
'LspSignatureActiveParameter',
|
'LspSignatureActiveParameter',
|
||||||
{ hl[1], hl[2] },
|
{ hl[1], hl[2] },
|
||||||
{ hl[3], hl[4] }
|
{ hl[3], hl[4] }
|
||||||
)
|
)
|
||||||
end
|
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)
|
||||||
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
|
if active_signature >= #signature_help.signatures or active_signature < 0 then
|
||||||
active_signature = 0
|
active_signature = 0
|
||||||
end
|
end
|
||||||
local signature = signature_help.signatures[active_signature + 1]
|
local signature = vim.deepcopy(signature_help.signatures[active_signature + 1])
|
||||||
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
|
||||||
@ -804,9 +804,11 @@ function M.convert_signature_help_to_markdown_lines(signature_help, ft, triggers
|
|||||||
active_offset[2] = active_offset[2] + #contents[1]
|
active_offset[2] = active_offset[2] + #contents[1]
|
||||||
end
|
end
|
||||||
|
|
||||||
active_hl = {}
|
local a_start = get_pos_from_offset(active_offset[1], contents)
|
||||||
list_extend(active_hl, get_pos_from_offset(active_offset[1], contents) or {})
|
local a_end = get_pos_from_offset(active_offset[2], contents)
|
||||||
list_extend(active_hl, get_pos_from_offset(active_offset[2], contents) or {})
|
if a_start and a_end then
|
||||||
|
active_hl = { a_start[1], a_start[2], a_end[1], a_end[2] }
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
return contents, active_hl
|
return contents, active_hl
|
||||||
@ -818,7 +820,7 @@ end
|
|||||||
---@param width integer window width (in character cells)
|
---@param width integer window width (in character cells)
|
||||||
---@param height integer window height (in character cells)
|
---@param height integer window height (in character cells)
|
||||||
---@param opts? vim.lsp.util.open_floating_preview.Opts
|
---@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)
|
function M.make_floating_popup_options(width, height, opts)
|
||||||
validate('opts', opts, 'table', true)
|
validate('opts', opts, 'table', true)
|
||||||
opts = opts or {}
|
opts = opts or {}
|
||||||
@ -1500,6 +1502,8 @@ end
|
|||||||
--- to display the full window height.
|
--- to display the full window height.
|
||||||
--- (default: `'auto'`)
|
--- (default: `'auto'`)
|
||||||
--- @field anchor_bias? 'auto'|'above'|'below'
|
--- @field anchor_bias? 'auto'|'above'|'below'
|
||||||
|
---
|
||||||
|
--- @field _update_win? integer
|
||||||
|
|
||||||
--- Shows contents in a floating window.
|
--- 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 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
|
-- check if this popup is focusable and we need to focus
|
||||||
if opts.focus_id and opts.focusable ~= false and opts.focus then
|
if opts.focus_id and opts.focusable ~= false and opts.focus then
|
||||||
-- Go back to previous window if we are in a focusable one
|
-- 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
|
if existing_float and api.nvim_win_is_valid(existing_float) then
|
||||||
api.nvim_win_close(existing_float, true)
|
api.nvim_win_close(existing_float, true)
|
||||||
end
|
end
|
||||||
|
floating_bufnr = api.nvim_create_buf(false, true)
|
||||||
-- Create the buffer
|
end
|
||||||
local floating_bufnr = api.nvim_create_buf(false, true)
|
|
||||||
|
|
||||||
-- Set up the contents, using treesitter for markdown
|
-- Set up the contents, using treesitter for markdown
|
||||||
local do_stylize = syntax == 'markdown' and vim.g.syntax_on ~= nil
|
local do_stylize = syntax == 'markdown' and vim.g.syntax_on ~= nil
|
||||||
|
|
||||||
if do_stylize then
|
if do_stylize then
|
||||||
local width = M._make_floating_popup_size(contents, opts)
|
local width = M._make_floating_popup_size(contents, opts)
|
||||||
contents = M._normalize_markdown(contents, { width = width })
|
contents = M._normalize_markdown(contents, { width = width })
|
||||||
vim.bo[floating_bufnr].filetype = 'markdown'
|
vim.bo[floating_bufnr].filetype = 'markdown'
|
||||||
vim.treesitter.start(floating_bufnr)
|
vim.treesitter.start(floating_bufnr)
|
||||||
api.nvim_buf_set_lines(floating_bufnr, 0, -1, false, contents)
|
|
||||||
else
|
else
|
||||||
-- Clean up input: trim empty lines
|
-- Clean up input: trim empty lines
|
||||||
contents = vim.split(table.concat(contents, '\n'), '\n', { trimempty = true })
|
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
|
if syntax then
|
||||||
vim.bo[floating_bufnr].syntax = syntax
|
vim.bo[floating_bufnr].syntax = syntax
|
||||||
end
|
end
|
||||||
api.nvim_buf_set_lines(floating_bufnr, 0, -1, true, contents)
|
|
||||||
end
|
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
|
-- Compute size of float needed to show (wrapped) lines
|
||||||
if opts.wrap then
|
if opts.wrap then
|
||||||
opts.wrap_at = opts.wrap_at or api.nvim_win_get_width(0)
|
opts.wrap_at = opts.wrap_at or api.nvim_win_get_width(0)
|
||||||
else
|
else
|
||||||
opts.wrap_at = nil
|
opts.wrap_at = nil
|
||||||
end
|
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 width, height = M._make_floating_popup_size(contents, opts)
|
||||||
|
|
||||||
local float_option = M.make_floating_popup_options(width, height, 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
|
floating_winnr = api.nvim_open_win(floating_bufnr, false, float_option)
|
||||||
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'
|
|
||||||
|
|
||||||
api.nvim_buf_set_keymap(
|
api.nvim_buf_set_keymap(
|
||||||
floating_bufnr,
|
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)
|
api.nvim_win_set_var(floating_winnr, opts.focus_id, bufnr)
|
||||||
end
|
end
|
||||||
api.nvim_buf_set_var(bufnr, 'lsp_floating_preview', floating_winnr)
|
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
|
return floating_bufnr, floating_winnr
|
||||||
end
|
end
|
||||||
|
Reference in New Issue
Block a user