Skip to content

Commit 2a19f32

Browse files
committed
feat: lsp signature help
1 parent 37e7203 commit 2a19f32

File tree

5 files changed

+259
-25
lines changed

5 files changed

+259
-25
lines changed

lua/noice/config/init.lua

+9-2
Original file line numberDiff line numberDiff line change
@@ -81,18 +81,25 @@ M.defaults = {
8181
---@type NoiceViewOptions
8282
opts = {}, -- merged with defaults from documentation
8383
},
84+
signature = {
85+
enabled = false,
86+
auto_open = true, -- Automatically show signature help when typing a trigger character from the LSP
87+
view = nil, -- when nil, use defaults from documentation
88+
---@type NoiceViewOptions
89+
opts = {}, -- merged with defaults from documentation
90+
},
91+
-- defaults for hover and signature help
92+
documentation = {
8493
view = "hover",
8594
---@type NoiceViewOptions
8695
opts = {
8796
lang = "markdown",
8897
replace = true,
8998
render = "plain",
9099
format = { "{message}" },
91-
buf_options = { iskeyword = '!-~,^*,^|,^",192-255', keywordprg = ":help" },
92100
win_options = { concealcursor = "n", conceallevel = 3 },
93101
},
94102
},
95-
hl_patterns = {
96103
},
97104
markdown = {
98105
hover = {

lua/noice/config/routes.lua

+13-5
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,19 @@ function M.defaults()
3030
})
3131
end
3232

33+
for _, kind in ipairs({ "signature", "hover" }) do
34+
table.insert(ret, {
35+
view = Config.options.lsp[kind].view or Config.options.lsp.documentation.view,
36+
filter = { event = "lsp", kind = kind },
37+
opts = vim.tbl_deep_extend(
38+
"force",
39+
{},
40+
Config.options.lsp.documentation.opts,
41+
Config.options.lsp[kind].opts or {}
42+
),
43+
})
44+
end
45+
3346
return vim.list_extend(ret, {
3447
{
3548
view = Config.options.cmdline.view,
@@ -105,11 +118,6 @@ function M.defaults()
105118
},
106119
opts = { lang = "lua", replace = true, title = "Noice" },
107120
},
108-
{
109-
view = Config.options.lsp.hover.view,
110-
filter = { event = "lsp", kind = "hover" },
111-
opts = Config.options.lsp.hover.opts,
112-
},
113121
{
114122
view = Config.options.lsp.progress.view,
115123
filter = { event = "lsp", kind = "progress" },

lua/noice/config/views.lua

+10-1
Original file line numberDiff line numberDiff line change
@@ -106,12 +106,21 @@ M.defaults = {
106106
view = "popup",
107107
relative = "cursor",
108108
enter = false,
109-
size = "auto",
109+
size = {
110+
width = "auto",
111+
height = "auto",
112+
max_height = 20,
113+
max_width = 120,
114+
},
110115
border = {
111116
style = "none",
112117
padding = { 0, 2 },
113118
},
114119
position = { row = 1, col = 0 },
120+
win_options = {
121+
wrap = true,
122+
linebreak = true,
123+
},
115124
},
116125
cmdline = {
117126
backend = "popup",

lua/noice/source/lsp/init.lua

+70-17
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ local Manager = require("noice.message.manager")
44
local Config = require("noice.config")
55
local Format = require("noice.source.lsp.format")
66
local Util = require("noice.util")
7+
local Message = require("noice.message")
8+
local Signature = require("noice.source.lsp.signature")
79

810
local M = {}
911

@@ -14,51 +16,100 @@ M.event = "lsp"
1416
M.kinds = {
1517
progress = "progress",
1618
hover = "hover",
19+
signature = "signature",
1720
}
1821

22+
---@type table<string, NoiceMessage>
23+
M._messages = {}
24+
25+
function M.get(kind)
26+
if not M._messages[kind] then
27+
M._messages[kind] = Message("lsp", kind)
28+
M._messages[kind].opts.title = kind
29+
end
30+
M._messages[kind]:clear()
31+
return M._messages[kind]
32+
end
33+
1934
function M.setup()
35+
-- vim.api.nvim_win_get_option(win,
2036
if Config.options.lsp.hover.enabled then
2137
vim.lsp.handlers["textDocument/hover"] = Util.protect(M.hover)
2238
end
39+
if Config.options.lsp.signature.enabled then
40+
vim.lsp.handlers["textDocument/signatureHelp"] = Util.protect(M.signature)
41+
end
42+
if Config.options.lsp.signature.auto_open then
43+
require("noice.source.lsp.signature").setup()
44+
end
2345
if Config.options.lsp.progress.enabled then
2446
require("noice.source.lsp.progress").setup()
2547
end
2648
end
2749

50+
---@param message NoiceMessage
51+
function M.augroup(message)
52+
return "noice_lsp_" .. message.id
53+
end
54+
55+
---@param message NoiceMessage
56+
function M.close(message)
57+
pcall(vim.api.nvim_del_augroup_by_name, M.augroup(message))
58+
message.opts.keep = function()
59+
return false
60+
end
61+
Manager.remove(message)
62+
end
63+
2864
---@param message NoiceMessage
2965
function M.auto_close(message)
30-
local open = true
3166
message.opts.timeout = 100
3267
message.opts.keep = function()
33-
return open
68+
return true
3469
end
3570

36-
local group = vim.api.nvim_create_augroup("noice_lsp_" .. message.id, {
71+
local group = vim.api.nvim_create_augroup(M.augroup(message), {
3772
clear = true,
3873
})
3974

4075
vim.api.nvim_create_autocmd({ "CursorMoved", "CursorMovedI", "InsertCharPre" }, {
4176
group = group,
4277
callback = function()
43-
if not Util.buf_has_message(vim.api.nvim_get_current_buf(), message) then
44-
pcall(vim.api.nvim_del_augroup_by_id, group)
45-
Manager.remove(message)
46-
open = false
78+
if not message:on_buf(vim.api.nvim_get_current_buf()) then
79+
M.close(message)
4780
end
4881
end,
4982
})
5083
end
5184

5285
---@param message NoiceMessage
53-
function M.try_enter(message)
54-
for _, buf in ipairs(vim.api.nvim_list_bufs()) do
55-
if Util.buf_has_message(buf, message) then
56-
local win = vim.fn.bufwinid(buf)
57-
if win ~= -1 then
58-
vim.api.nvim_set_current_win(win)
59-
return true
60-
end
86+
function M.close_others(message)
87+
for _, m in pairs(M._messages) do
88+
if m ~= message then
89+
M.close(m)
90+
end
91+
end
92+
end
93+
94+
---@param result SignatureHelp
95+
function M.signature(_, result, ctx, config)
96+
config = config or {}
97+
if not (result and result.signatures) then
98+
if not config.trigger then
99+
vim.notify("No signature help available")
61100
end
101+
return
102+
end
103+
104+
local message = M.get(M.kinds.signature)
105+
M.close_others(message)
106+
107+
if config.trigger or not message:focus() then
108+
result.ft = vim.bo[ctx.bufnr].filetype
109+
result.message = message
110+
Signature.new(result):format()
111+
M.auto_close(message)
112+
Manager.add(message)
62113
end
63114
end
64115

@@ -68,8 +119,10 @@ function M.hover(_, result)
68119
return
69120
end
70121

71-
local message = Format.format(result.contents, "hover")
72-
if not M.try_enter(message) then
122+
local message = M.get(M.kinds.hover)
123+
M.close_others(message)
124+
if not message:focus() then
125+
Format.format(message, result.contents)
73126
M.auto_close(message)
74127
Manager.add(message)
75128
end

lua/noice/source/lsp/signature.lua

+157
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
1+
local require = require("noice.util.lazy")
2+
3+
local NoiceText = require("noice.text")
4+
local Format = require("noice.source.lsp.format")
5+
local Markdown = require("noice.text.markdown")
6+
local Lsp = require("noice.source.lsp")
7+
8+
---@class SignatureInformation
9+
---@field label string
10+
---@field documentation? string|MarkupContent
11+
---@field parameters? ParameterInformation[]
12+
---@field activeParameter? integer
13+
14+
---@class ParameterInformation
15+
---@field label string|{[1]:integer, [2]:integer}
16+
---@field documentation? string|MarkupContent
17+
18+
---@class SignatureHelpContext
19+
---@field triggerKind SignatureHelpTriggerKind
20+
---@field triggerCharacter? string
21+
---@field isRetrigger boolean
22+
---@field activeSignatureHelp? SignatureHelp
23+
24+
---@class SignatureHelp
25+
---@field signatures SignatureInformation[]
26+
---@field activeSignature? integer
27+
---@field activeParameter? integer
28+
---@field ft? string
29+
---@field message NoiceMessage
30+
local M = {}
31+
M.__index = M
32+
33+
---@enum SignatureHelpTriggerKind
34+
M.trigger_kind = {
35+
invoked = 1,
36+
trigger_character = 2,
37+
content_change = 3,
38+
}
39+
40+
-- TODO: add scroll up/down
41+
-- TODO: horz line for Hover should overlap end of code block
42+
-- TODO: markdown links
43+
44+
function M.setup()
45+
vim.api.nvim_create_autocmd("LspAttach", {
46+
callback = function(args)
47+
local bufnr = args.buf
48+
local client = vim.lsp.get_client_by_id(args.data.client_id)
49+
if client.server_capabilities.signatureHelpProvider then
50+
local chars = client.server_capabilities.signatureHelpProvider.triggerCharacters
51+
if #chars > 0 then
52+
vim.api.nvim_create_autocmd({ "TextChangedI", "TextChangedP", "InsertEnter" }, {
53+
buffer = bufnr,
54+
callback = function()
55+
if vim.api.nvim_get_current_buf() ~= bufnr then
56+
return
57+
end
58+
local message = Lsp.get(Lsp.kinds.signature)
59+
if message:win() then
60+
-- no need to fetch signature when signature is already shown
61+
return
62+
end
63+
local cursor = vim.api.nvim_win_get_cursor(0)
64+
local row = cursor[1] - 1
65+
local col = cursor[2]
66+
local _, lines = pcall(vim.api.nvim_buf_get_text, bufnr, row, 0, row, col, {})
67+
local line = vim.trim(lines and lines[1] or "")
68+
local char = line:sub(-1, -1)
69+
if vim.tbl_contains(chars, char) then
70+
local params = vim.lsp.util.make_position_params(0, client.offset_encoding)
71+
vim.lsp.buf_request(
72+
bufnr,
73+
"textDocument/signatureHelp",
74+
params,
75+
vim.lsp.with(require("noice.source.lsp").signature, { trigger = true })
76+
)
77+
end
78+
end,
79+
})
80+
end
81+
end
82+
end,
83+
})
84+
end
85+
86+
---@param help SignatureHelp
87+
function M.new(help)
88+
return setmetatable(help, M)
89+
end
90+
91+
function M:active_parameter(sig_index)
92+
if self.activeSignature and self.signatures[self.activeSignature + 1] and sig_index ~= self.activeSignature + 1 then
93+
return
94+
end
95+
local sig = self.signatures[sig_index]
96+
if sig.activeParameter and sig.parameters[sig.activeParameter + 1] then
97+
return sig.parameters[sig.activeParameter + 1]
98+
end
99+
if self.activeParameter and sig.parameters[self.activeParameter + 1] then
100+
return sig.parameters[self.activeParameter + 1]
101+
end
102+
return sig.parameters[1]
103+
end
104+
105+
---@param sig SignatureInformation
106+
---@param param ParameterInformation
107+
function M:format_active_parameter(sig, param)
108+
local label = param.label
109+
if type(label) == "string" then
110+
local from = sig.label:find(label, 1, true)
111+
if from then
112+
self.message:append(NoiceText("", {
113+
hl_group = "LspSignatureActiveParameter",
114+
col = from - 1,
115+
length = vim.fn.strlen(label),
116+
}))
117+
end
118+
else
119+
self.message:append(NoiceText("", {
120+
hl_group = "LspSignatureActiveParameter",
121+
col = label[1],
122+
length = label[2] - label[1],
123+
}))
124+
end
125+
end
126+
127+
---@param sig SignatureInformation
128+
function M:format_signature(sig_index, sig)
129+
if sig_index ~= 1 then
130+
self.message:append("\n\n")
131+
self.message:append(sig)
132+
end
133+
self.message:append("```" .. (self.ft or ""))
134+
if sig_index ~= 1 then
135+
Markdown.horizontal_line(self.message)
136+
end
137+
self.message:newline()
138+
self.message:append(sig.label)
139+
local param = self:active_parameter(sig_index)
140+
if param then
141+
self:format_active_parameter(sig, param)
142+
end
143+
self.message:append("\n```")
144+
if sig.documentation then
145+
Markdown.horizontal_line(self.message)
146+
self.message:newline()
147+
Format.format(self.message, sig.documentation)
148+
end
149+
end
150+
151+
function M:format()
152+
for s, sig in ipairs(self.signatures) do
153+
self:format_signature(s, sig)
154+
end
155+
end
156+
157+
return M

0 commit comments

Comments
 (0)