local api = vim.api local helper = require("signup.helper") local M = {} local SignatureHelp = {} SignatureHelp.__index = SignatureHelp function SignatureHelp.new() local instance = setmetatable({ _display_sig_win = function() end, -- function to open signatures window _display_doc_win = function() end, -- function to open documentation window sig_content = nil, -- current signature buffer content sig_winnr = nil, sig_bufnr = nil, sig_win_height = nil, sig_win_showing = false, ns = nil, markid = nil, timer = nil, current_signatures = nil, enabled = false, current_signature_idx = nil, doc_winnr = nil, doc_bufnr = nil, doc_win_showing = false, doc_win_focused = false, config = nil, active_winnr = vim.api.nvim_get_current_win(), }, SignatureHelp) instance._default_config = { silent = false, 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, max_height = 20, max_width = 80, anchor_bias = "below", -- below|above|auto offset_y = 0, offset_x = 0, }, trigger_chars = { "(", "," }, auto_close = true, preview_parameters = true, debounce_time = 100, render_style = { separator = true, compact = true, align_icons = true, }, } return instance end function SignatureHelp:hide_sig_win() if self.sig_win_showing then helper.close_float_window(self.sig_winnr) end helper.delete_buffer(self.sig_bufnr) helper.delete_buffer(self.doc_bufnr) self:hide_doc_win() self.sig_winnr = nil self.sig_bufnr = nil self.doc_bufnr = nil -- we want to nil this here and not when we hide the doc win in case we want to reopen the doc win self.current_signatures = nil self.sig_win_showing = false end function SignatureHelp:hide_doc_win() if self.doc_win_showing then helper.close_float_window(self.doc_winnr) self.doc_winnr = nil self.doc_win_showing = false end 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_bufnr or not api.nvim_buf_is_valid(self.sig_bufnr) 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_bufnr, 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_sig_win(result) if not result or not result.signatures or #result.signatures == 0 then self:hide_sig_win() if self.doc_win_showing then self:hide_doc_win() end return end if not self.sig_win_showing then local ft = vim.bo.ft self._display_sig_win, self._display_doc_win = helper.create_float_windows(result, ft, self.config.win_opts) self.sig_bufnr, self.sig_winnr, self.sig_content = self._display_sig_win() local param_str = helper.get_parameter_label(result) self:set_active_parameter_highlights(param_str, self.sig_content) self.sig_win_showing = true end local param_str = helper.get_parameter_label(result) self:set_active_parameter_highlights(param_str, self.sig_content) end function SignatureHelp:display_doc_win() if self.doc_win_showing then return else self.doc_bufnr, self.doc_winnr = self._display_doc_win() self.doc_win_showing = true self.doc_win_focused = true vim.api.nvim_set_current_win(self.doc_winnr) exit_docs = function() vim.api.nvim_set_current_win(self.active_winnr) vim.fn.feedkeys("i", "t") --put us back into insert mode self.doc_win_focused = false end if self.doc_bufnr ~= nil then vim.keymap.set( "n", "q", exit_docs, { silent = true, noremap = true, nowait = true, buffer = self.doc_bufnr } ) vim.keymap.set( "n", "", exit_docs, { silent = true, noremap = true, nowait = true, buffer = self.doc_bufnr } ) end end 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() self:hide_doc_win() return end if result and result.signatures and #result.signatures > 0 then self.current_signatures = result.signatures self:display_sig_win(result) else self:hide_sig_win() self:hide_doc_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:setup_autocmds() local group = api.nvim_create_augroup("LspSignatureHelp", { clear = true }) local function debounced_trigger() 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() debounced_trigger() end, }) -- make sure signature help windows get closed if we switch out of the doc window -- instead of exiting it using the exit keymaps api.nvim_create_autocmd({ "WinEnter" }, { group = group, callback = function() vim.defer_fn(function() if self.active_winnr == vim.api.nvim_get_current_win() and vim.api.nvim_get_mode().mode == "n" then self:hide_sig_win() self:hide_doc_win() end end, 30) 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() if self.doc_win_focused == false then self:hide_sig_win() self:hide_doc_win() self.normal_mode_active = false end 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_sig_win({ 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_sig_win({ signatures = self.current_signatures, activeParameter = self.current_active_parameter, activeSignature = self.current_signature_idx - 1, }) end function SignatureHelp:toggle_docs() if self.doc_win_showing then self:hide_doc_win() elseif self.sig_win_showing then self:display_doc_win() 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 signature_help:setup_autocmds() -- Store instance for potential reuse M._initialized = true M._instance = signature_help -- Add API methods for external use M.toggle_docs = function() signature_help:toggle_docs() end return signature_help end -- Add version and metadata for lazy.nvim compatibility M.version = "1.0.0" M.dependencies = { "nvim-treesitter/nvim-treesitter", } return M