local api = vim.api local helper = require("signup.helper") local M = {} local SignatureHelp = {} SignatureHelp.__index = SignatureHelp function SignatureHelp.new() local instance = setmetatable({ sig_win = nil, sig_buf = nil, doc_win = nil, doc_buf = nil, showing = false, ns = nil, markid = nil, timer = nil, current_signatures = nil, enabled = false, normal_mode_active = false, current_signature_idx = nil, config = nil, }, SignatureHelp) instance._default_config = { silent = false, number = true, icons = { parameter = "", method = "󰡱", documentation = "󱪙", }, colors = { parameter = "#86e1fc", method = "#c099ff", documentation = "#4fd6be", default_value = "#a80888", }, active_parameter_colors = { bg = "#86e1fc", fg = "#1a1a1a", }, hi_parameter = "LspSignatureActiveParameter", win_opts = { border = "rounded", winblend = 10, zindex = 200, focusable = false, max_height = 10, max_width = 80, anchor_bias = "below", -- below|above|auto relative = "cursor", offset_y = 0, offset_x = 0, }, trigger_chars = { "(", "," }, auto_close = true, preview_parameters = true, debounce_time = 100, toggle_key = "", render_style = { separator = true, compact = true, align_icons = true, }, } return instance end -- local function signature_index_comment(index) -- if #vim.bo.commentstring ~= 0 then -- return vim.bo.commentstring:format(index) -- else -- return "(" .. index .. ")" -- end -- end -- local function markdown_for_signature_list(signatures, config) -- local lines, labels = {}, {} -- local number = config.number and #signatures > 1 -- local max_method_len = 0 -- -- -- First pass to calculate alignment -- if config.render_style.align_icons then -- for _, signature in ipairs(signatures) do -- max_method_len = math.max(max_method_len, #signature.label) -- end -- end -- -- for index, signature in ipairs(signatures) do -- if not config.render_style.compact then -- table.insert(lines, "") -- end -- table.insert(labels, #lines + 1) -- -- local suffix = number and (" " .. signature_index_comment(index)) or "" -- local padding = config.render_style.align_icons and string.rep(" ", max_method_len - #signature.label) or " " -- -- -- Method signature with syntax highlighting -- table.insert(lines, string.format("```%s", vim.bo.filetype)) -- -- table.insert(lines, string.format("%s Method:", config.icons.method)) -- table.insert(lines, string.format("%s %s%s%s", config.icons.method, signature.label, padding, suffix)) -- table.insert(lines, "```") -- -- -- -- Parameters section -- -- if signature.parameters and #signature.parameters > 0 then -- -- if config.render_style.separator then -- -- table.insert(lines, string.rep("─", 40)) -- -- end -- -- table.insert(lines, string.format("%s Parameters:", config.icons.parameter)) -- -- for _, param in ipairs(signature.parameters) do -- -- local param_doc = param.documentation and string.format(" - %s", vim.inspect(param.documentation)) -- -- table.insert(lines, string.format(" • %s = %s", vim.inspect(param.label), param_doc)) -- -- end -- -- end -- -- -- Documentation section -- if signature.documentation then -- if config.render_style.separator then -- table.insert(lines, string.rep("─", 40)) -- end -- table.insert(lines, string.format("%s Documentation:", config.icons.documentation)) -- local doc_lines = vim.split(signature.documentation.value or signature.documentation, "\n") -- for _, line in ipairs(doc_lines) do -- table.insert(lines, " " .. line) -- end -- end -- -- if index ~= #signatures and config.render_style.separator then -- table.insert(lines, string.rep("═", 40)) -- end -- end -- return lines, labels -- end -- -- function SignatureHelp:create_float_window(contents) -- local max_width = math.min(self.config.max_width, vim.o.columns) -- local max_height = math.min(self.config.max_height, #contents) -- -- -- Calculate optimal position -- local cursor = api.nvim_win_get_cursor(0) -- local cursor_line = cursor[1] -- local screen_line = vim.fn.screenpos(0, cursor_line, 1).row -- -- local row_offset = self.config.floating_window_above_cur_line and -max_height - 1 or 1 -- if screen_line + row_offset < 1 then -- row_offset = 2 -- Show below if not enough space above -- end -- -- local win_config = { -- relative = "cursor", -- row = row_offset - 1, -- col = 0, -- width = max_width, -- height = max_height, -- style = "minimal", -- border = self.config.border, -- zindex = 50, -- Ensure it's above most other floating windows -- } -- -- if self.win and api.nvim_win_is_valid(self.win) then -- api.nvim_win_set_config(self.win, win_config) -- api.nvim_win_set_buf(self.win, self.buf) -- else -- self.buf = api.nvim_create_buf(false, true) -- self.win = api.nvim_open_win(self.buf, false, win_config) -- end -- -- vim.bo[self.buf].modifiable = true -- api.nvim_buf_set_lines(self.buf, 0, -1, false, contents) -- vim.bo[self.buf].modifiable = false -- vim.wo[self.win][self.buf].foldenable = false -- vim.wo[self.win][self.buf].wrap = true -- vim.wo[self.win][self.buf].winblend = self.config.winblend -- self.visible = true -- end -- function SignatureHelp:hide_sig_win() helper.close_float_window(self.sig_win, self.sig_buf) self.current_signatures = nil self.showing = false end function SignatureHelp:hide_doc_win() helper.close_float_window(self.doc_win, self.doc_buf) end function SignatureHelp:extract_default_value(param_info) -- Check if parameter has documentation that might contain default value if not param_info.documentation then return nil end local doc = type(param_info.documentation) == "string" and param_info.documentation or param_info.documentation.value -- Look for common default value patterns local patterns = { "default:%s*([^%s]+)", "defaults%s+to%s+([^%s]+)", "%(default:%s*([^%)]+)%)", } for _, pattern in ipairs(patterns) do local default = doc:match(pattern) if default then return default end end return nil end function SignatureHelp:set_active_parameter_highlights(param_str, lines) if not self.sig_buf or not api.nvim_buf_is_valid(self.sig_buf) then return end self.ns = api.nvim_create_namespace("lsp_signature_hi_parameter") local hi = self.config.hi_parameter local s, e, line_num for l, line in ipairs(lines) do local ss, ee, _ = string.find(line, param_str, 1, true) if ss ~= nil then line_num = l - 1 s = ss e = ee end end if s and e and s > 0 then self.markid = api.nvim_buf_set_extmark( self.sig_buf, self.ns, line_num, s, { id = self.markid, end_line = line_num, end_col = e, hl_group = hi, strict = false } ) end end function SignatureHelp:display(result) if not result or not result.signatures or #result.signatures == 0 then self:hide_sig_win() return end local content = helper.convert_signature_help_to_lines(result, self.config.win_opts.max_width) if not self.showing then self.sig_buf, self.sig_win = helper.create_float_window(content, "lua", self.config.win_opts) -- TODO: programmatically set file type: local param_str = helper.get_parameter_label(result) self:set_active_parameter_highlights(param_str, content) self.showing = true end local param_str = helper.get_parameter_label(result) self:set_active_parameter_highlights(param_str, content) end function SignatureHelp:trigger() if not self.enabled then return end local params = vim.lsp.util.make_position_params() vim.lsp.buf_request(0, "textDocument/signatureHelp", params, function(err, result, _, _) if err then if not self.config.silent then vim.notify("Error in LSP Signature Help: " .. vim.inspect(err), vim.log.levels.ERROR) end self:hide_sig_win() return end if result and result.signatures and #result.signatures > 0 then self.current_signatures = result.signatures self:display(result) else self:hide_sig_win() -- Only notify if not silent and if there was actually no signature help if not self.config.silent and result then vim.notify("No signature help available", vim.log.levels.INFO) end end end) end function SignatureHelp:check_capability() local clients = vim.lsp.get_clients() for _, client in ipairs(clients) do if client.server_capabilities.signatureHelpProvider then self.enabled = true return end end self.enabled = false end function SignatureHelp:toggle_normal_mode() self.normal_mode_active = not self.normal_mode_active if self.normal_mode_active then self:trigger() else self:hide_sig_win() end end function SignatureHelp:setup_autocmds() local group = api.nvim_create_augroup("LspSignatureHelp", { clear = true }) local function debounced_trigger() -- vim.defer_fn(function() -- self:trigger() -- end, 30) if self.timer then vim.fn.timer_stop(self.timer) end self.timer = vim.fn.timer_start(self.debounce_time, function() self:trigger() end) end api.nvim_create_autocmd({ "InsertEnter", "CursorMovedI" }, { group = group, callback = function() -- vim.notify("trigger") -- local cmp_visible = require("cmp").visible() -- if cmp_visible then -- self:hide() -- elseif vim.fn.pumvisible() == 0 then debounced_trigger() -- else -- self:hide() -- end end, }) api.nvim_create_autocmd({ "CursorMoved" }, { group = group, callback = function() if self.normal_mode_active then debounced_trigger() end end, }) api.nvim_create_autocmd({ "InsertLeave" }, { group = group, callback = function() self:hide_sig_win() self.normal_mode_active = false end, }) api.nvim_create_autocmd("LspAttach", { group = group, callback = function() vim.defer_fn(function() self:check_capability() end, 100) end, }) end -- Add navigation between multiple signatures function SignatureHelp:next_signature() if not self.current_signatures then return end self.current_signature_idx = (self.current_signature_idx or 0) + 1 if self.current_signature_idx > #self.current_signatures then self.current_signature_idx = 1 end self:display({ signatures = self.current_signatures, activeParameter = self.current_active_parameter, activeSignature = self.current_signature_idx - 1, }) end function SignatureHelp:prev_signature() if not self.current_signatures then return end self.current_signature_idx = (self.current_signature_idx or 1) - 1 if self.current_signature_idx < 1 then self.current_signature_idx = #self.current_signatures end self:display({ signatures = self.current_signatures, activeParameter = self.current_active_parameter, activeSignature = self.current_signature_idx - 1, }) end function SignatureHelp:setup_keymaps() -- Setup toggle keys using the actual config local toggle_key = self.config.toggle_key if toggle_key then vim.keymap.set("n", toggle_key, function() self:toggle_normal_mode() end, { noremap = true, silent = true, desc = "Toggle signature help in normal mode" }) end end function M.setup(opts) -- Ensure setup is called only once if M._initialized then return M._instance end opts = opts or {} local signature_help = SignatureHelp.new() -- Deep merge user config with defaults signature_help.config = vim.tbl_deep_extend("force", signature_help._default_config, opts) -- Setup highlights with user config local function setup_highlights() local colors = signature_help.config.colors local highlights = { SignatureHelpBorder = { link = "FloatBorder" }, SignatureHelpMethod = { fg = colors.method }, SignatureHelpParameter = { fg = colors.parameter }, SignatureHelpDocumentation = { fg = colors.documentation }, SignatureHelpDefaultValue = { fg = colors.default_value, italic = true }, LspSignatureActiveParameter = { fg = signature_help.config.active_parameter_colors.fg, bg = signature_help.config.active_parameter_colors.bg, }, } for group, hl_opts in pairs(highlights) do vim.api.nvim_set_hl(0, group, hl_opts) end end -- Setup highlights and ensure they persist across colorscheme changes setup_highlights() vim.api.nvim_create_autocmd("ColorScheme", { group = vim.api.nvim_create_augroup("LspSignatureColors", { clear = true }), callback = setup_highlights, }) -- Setup autocmds and keymaps signature_help:setup_autocmds() signature_help:setup_keymaps() -- Store instance for potential reuse M._initialized = true M._instance = signature_help return signature_help end -- Add version and metadata for lazy.nvim compatibility M.version = "1.0.0" M.dependencies = { "nvim-treesitter/nvim-treesitter", } -- Add API methods for external use return M