diff --git a/README.md b/README.md index 762945b..6136334 100644 --- a/README.md +++ b/README.md @@ -6,13 +6,18 @@ A little (smart maybe) lsp signature helper for neovim. # Neovim Signature Help Plugin -This Neovim plugin provides a signature help feature for LSP (Language Server Protocol) clients. It displays function signatures and parameter information in a floating window as you type in insert mode or move the cursor in normal mode. The plugin also includes a notification system to display messages with different levels of severity (info, warning, error). +This Neovim plugin provides a signature help feature for LSP (Language Server Protocol) clients. I can't tell much, just watch the showcases. # ScreenShots (WIP) -![Screenshot 1](https://github.com/user-attachments/assets/1ba206b1-cde8-41f4-9abd-f8501c6b16e1) -![Screenshot 2](https://github.com/user-attachments/assets/aba63fe7-a302-4c9d-9f4a-ce28c9c35c03) -![Screenshot 3](https://github.com/user-attachments/assets/986f0bcb-aecf-4483-8210-83b93cfd72d2) +![signup_1](https://github.com/user-attachments/assets/e9319dcf-1d9d-4567-a500-a24d38933cb6) +![signup_2](https://github.com/user-attachments/assets/192b0809-3e66-42bf-9e6e-c1eae744f7b8) +![signup_3](https://github.com/user-attachments/assets/ca43b7a0-63fa-469c-8db0-df7a49dab483) + +![signup_def](https://github.com/user-attachments/assets/6c4d7e09-5baa-418f-a086-e60b4eb4b501) + +We have `dock` mode but its under dev for now, please take low expectations: +![signup_dock](https://github.com/user-attachments/assets/40455737-a952-4a3f-ae1f-fadd7ad68ea2) ## Features @@ -26,17 +31,18 @@ This Neovim plugin provides a signature help feature for LSP (Language Server Pr ### Using [lazy.nvim](https://github.com/folke/lazy.nvim) -Add the following to your `init.lua`: +Add the following to your `init.lua` and use `main` branch always: ```lua require("lazy").setup({ { "Dan7h3x/signup.nvim", branch = "main", - config = function() - require("signup").setup({ - -- Your configuration options here - }) + opts = { + -- Your configuration options here + }, + config = function(_,opts) + require("signup").setup(opts) end } }) @@ -62,109 +68,51 @@ EOF ## Configuration -The plugin comes with a default configuration, but you can customize it according to your preferences. Here are the available options: +The plugin comes with a default configuration, but you can customize it +according to your preferences. Here are the available options: ```lua -require('signup').setup( - { - win = nil, - buf = nil, - timer = nil, - visible = false, - current_signatures = nil, - enabled = false, - normal_mode_active = false, - config = { - silent = false, - number = true, - icons = { - parameter = " ", - method = " ", - documentation = " ", - }, - colors = { - parameter = "#86e1fc", - method = "#c099ff", - documentation = "#4fd6be", - }, - active_parameter_colors = { - bg = "#86e1fc", - fg = "#1a1a1a", - }, - border = "solid", - winblend = 10, - } +opts = { + 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", + }, + border = "solid", + winblend = 10, + auto_close = true, + trigger_chars = { "(", "," }, + max_height = 10, + max_width = 40, + floating_window_above_cur_line = true, + preview_parameters = true, + debounce_time = 30, + dock_toggle_key = "sd", + toggle_key = "", + dock_mode = { + enabled = false, + position = "bottom", + height = 3, + padding = 1, + }, + render_style = { + separator = true, + compact = true, + align_icons = true, + }, } -) -``` - -### Options - -- **silent**: If `true`, suppresses notifications. Default is `false`. -- **number**: If `true`, displays the signature index. Default is `true`. -- **icons**: Custom icons for method, parameter, and documentation. -- **colors**: Custom colors for method, parameter, and documentation. -- **border**: Border style for the floating window. Default is `"rounded"`. -- **winblend**: Transparency level for the floating window. Default is `10`. -- **override**: If `true`, overrides the default LSP handler for `textDocument/signatureHelp`. Default is `true`. - -## Usage - -### Toggle Signature Help in Normal Mode - -You can toggle the signature help in normal mode using the default keybinding ``. You can customize this keybinding in the setup function: - -```lua -require('signup').setup({ - toggle_key = "", -- Customize the toggle key here -}) -``` - -### Trigger Signature Help in Insert Mode - -The signature help is automatically triggered when you move the cursor or change text in insert mode. - -### Notifications - -The plugin includes a notification system to display messages with different levels of severity (info, warning, error). These notifications are displayed in a floating window and automatically disappear after a few seconds. - -## Highlight Groups - -The plugin defines the following highlight groups: - -- **LspSignatureActiveParameter**: Highlight for the active parameter. -- **SignatureHelpMethod**: Highlight for method icons. -- **SignatureHelpParameter**: Highlight for parameter icons. -- **SignatureHelpDocumentation**: Highlight for documentation icons. -- **NotificationInfo**: Highlight for info notifications. -- **NotificationWarn**: Highlight for warning notifications. -- **NotificationError**: Highlight for error notifications. - -## Examples - -### Customizing Icons and Colors - -```lua -require('signup').setup({ - icons = { - parameter = " ", - method = " ", - documentation = " ", - }, - colors = { - parameter = "#ffa500", - method = "#8a2be2", - documentation = "#008000", - }, -}) -``` - -### Disabling Notifications - -```lua -require('signup').setup({ - silent = true, -}) ``` ## Contributing @@ -176,3 +124,7 @@ Contributions are welcome! Please feel free to submit a pull request or open an This plugin is licensed under the MIT License. See the [LICENSE](LICENSE) file for more details. --- + +``` + +``` diff --git a/lua/signup/init.lua b/lua/signup/init.lua index 8416ffe..9fec8fa 100644 --- a/lua/signup/init.lua +++ b/lua/signup/init.lua @@ -6,35 +6,64 @@ local SignatureHelp = {} SignatureHelp.__index = SignatureHelp function SignatureHelp.new() - return setmetatable({ + local instance = setmetatable({ win = nil, buf = nil, + dock_win = nil, + dock_buf = nil, + dock_win_id = "signature_help_dock_" .. vim.api.nvim_get_current_buf(), timer = nil, visible = false, current_signatures = nil, enabled = false, normal_mode_active = false, - config = { - silent = false, - number = true, - icons = { - parameter = " ", - method = " ", - documentation = " ", - }, - colors = { - parameter = "#86e1fc", - method = "#c099ff", - documentation = "#4fd6be", - }, - active_parameter_colors = { - bg = "#86e1fc", - fg = "#1a1a1a", - }, - border = "solid", - winblend = 10, - } + 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", + }, + border = "solid", + winblend = 10, + auto_close = true, + trigger_chars = { "(", "," }, + max_height = 10, + max_width = 40, + floating_window_above_cur_line = true, + preview_parameters = true, + debounce_time = 30, + dock_toggle_key = "sd", + toggle_key = "", + dock_mode = { + enabled = false, + position = "bottom", + height = 3, + padding = 1, + }, + render_style = { + separator = true, + compact = true, + align_icons = true, + }, + } + + return instance end local function signature_index_comment(index) @@ -48,51 +77,95 @@ 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 %s%s", config.icons.method, signature.label, suffix)) + -- 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 - -- table.insert(lines, "") + -- 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 - -- table.insert(lines, string.format(" • %s", param.label)) + -- local param_doc = param.documentation and + -- string.format(" - %s", param.documentation.value or param.documentation) or "" + -- table.insert(lines, string.format(" • %s = %s", param.label, param_doc)) -- end -- end + -- Documentation section if signature.documentation then - table.insert(lines, "") + if config.render_style.separator then + table.insert(lines, string.rep("-", 40)) + end table.insert(lines, string.format("%s Documentation:", config.icons.documentation)) - vim.list_extend(lines, vim.split(signature.documentation.value or signature.documentation, "\n")) + 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 then - table.insert(lines, "---") + 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 width = math.min(45, vim.o.columns) - local height = math.min(#contents, 10) + 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 row = cursor[1] - api.nvim_win_get_cursor(0)[1] + 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 + 1, + row = row_offset - 1, col = 0, - width = width, - height = height, + 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 @@ -115,60 +188,86 @@ end function SignatureHelp:hide() if self.visible then - pcall(api.nvim_win_close, self.win, true) - pcall(api.nvim_buf_delete, self.buf, { force = true }) - self.win = nil - self.buf = nil + -- Store current window and buffer + local current_win = api.nvim_get_current_win() + local current_buf = api.nvim_get_current_buf() + + -- Close appropriate window based on mode + if self.config.dock_mode.enabled then + self:close_dock_window() + else + if self.win and api.nvim_win_is_valid(self.win) then + pcall(api.nvim_win_close, self.win, true) + end + if self.buf and api.nvim_buf_is_valid(self.buf) then + pcall(api.nvim_buf_delete, self.buf, { force = true }) + end + self.win = nil + self.buf = nil + end + self.visible = false - self.current_signatures = nil + + -- Restore focus + pcall(api.nvim_set_current_win, current_win) + pcall(api.nvim_set_current_buf, current_buf) end end --- function SignatureHelp:set_active_parameter_highlights(active_parameter, signatures, labels) --- if not self.buf or not api.nvim_buf_is_valid(self.buf) then return end --- --- api.nvim_buf_clear_namespace(self.buf, -1, 0, -1) --- --- for index, signature in ipairs(signatures) do --- local parameter = signature.activeParameter or active_parameter --- if parameter and parameter >= 0 and parameter < #signature.parameters then --- local label = signature.parameters[parameter + 1].label --- if type(label) == "string" then --- -- vim.fn.matchadd("LspSignatureActiveParameter", "\\<" .. label .. "\\>") --- vim.fn.matchadd("LspSignatureActiveParameter", "\\V\\<" .. vim.fn.escape(label, '\\') .. "\\>") --- elseif type(label) == "table" then --- -- api.nvim_buf_add_highlight(self.buf, -1, "LspSignatureActiveParameter", labels[index], unpack(label)) --- assert(self.buf and labels[index] and label[1] and label[2], "Invalid arguments") --- api.nvim_buf_add_highlight(self.buf, -1, "LspSignatureActiveParameter", labels[index], unpack(label)) --- end --- end --- end --- --- -- Add icon highlights --- local icon_highlights = { --- { self.config.icons.method, "SignatureHelpMethod" }, --- { self.config.icons.parameter, "SignatureHelpParameter" }, --- { self.config.icons.documentation, "SignatureHelpDocumentation" }, --- } --- --- for _, icon_hl in ipairs(icon_highlights) do --- local icon, hl_group = unpack(icon_hl) --- local line_num = 0 --- while line_num < api.nvim_buf_line_count(self.buf) do --- local line = api.nvim_buf_get_lines(self.buf, line_num, line_num + 1, false)[1] --- local start_col = line:find(vim.pesc(icon)) --- if start_col then --- api.nvim_buf_add_highlight(self.buf, -1, hl_group, line_num, start_col + 1, start_col + #icon + 1) --- end --- line_num = line_num + 1 --- end --- end --- end +function SignatureHelp:find_parameter_range(signature_str, parameter_label) + -- Handle both string and table parameter labels + if type(parameter_label) == "table" then + return parameter_label[1], parameter_label[2] + end + + -- Escape special pattern characters in parameter_label + local escaped_label = vim.pesc(parameter_label) + + -- Look for the parameter with word boundaries + local pattern = [[\b]] .. escaped_label .. [[\b]] + local start_pos = signature_str:find(pattern) + + if not start_pos then + -- Fallback: try finding exact match if word boundary search fails + start_pos = signature_str:find(escaped_label) + end + + if not start_pos then return nil, nil end + + local end_pos = start_pos + #parameter_label - 1 + return start_pos, end_pos +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(active_parameter, signatures, labels) if not self.buf or not api.nvim_buf_is_valid(self.buf) then return end + -- Clear existing highlights api.nvim_buf_clear_namespace(self.buf, -1, 0, -1) + -- Iterate over signatures to highlight the active parameter for index, signature in ipairs(signatures) do local parameter = signature.activeParameter or active_parameter if parameter and parameter >= 0 and parameter < #signature.parameters then @@ -178,8 +277,8 @@ function SignatureHelp:set_active_parameter_highlights(active_parameter, signatu local signature_str = signature.label local start_pos, end_pos = self:find_parameter_range(signature_str, label) if start_pos and end_pos then - api.nvim_buf_add_highlight(self.buf, -1, "LspSignatureActiveParameter", labels[index], start_pos - 1, - end_pos - 1) + api.nvim_buf_add_highlight(self.buf, -1, "LspSignatureActiveParameter", labels[index], start_pos, + end_pos) end elseif type(label) == "table" then local start_pos, end_pos = unpack(label) @@ -202,27 +301,39 @@ function SignatureHelp:set_active_parameter_highlights(active_parameter, signatu local line = api.nvim_buf_get_lines(self.buf, line_num, line_num + 1, false)[1] local start_col = line:find(vim.pesc(icon)) if start_col then - api.nvim_buf_add_highlight(self.buf, -1, hl_group, line_num, start_col, start_col + #icon) + api.nvim_buf_add_highlight(self.buf, -1, hl_group, line_num, start_col - 1, start_col + #icon - 1) end line_num = line_num + 1 end end end -function SignatureHelp:find_parameter_range(signature_str, parameter_label) - local start_pos = signature_str:find(parameter_label) - if not start_pos then return nil, nil end +function SignatureHelp:highlight_icons() + local icon_highlights = { + { self.config.icons.method, "SignatureHelpMethod" }, + { self.config.icons.parameter, "SignatureHelpParameter" }, + { self.config.icons.documentation, "SignatureHelpDocumentation" }, + } - local end_pos = start_pos + #parameter_label - 1 - - -- Ensure the parameter label is not part of a larger word - local before_char = signature_str:sub(start_pos - 1, start_pos - 1) - local after_char = signature_str:sub(end_pos + 1, end_pos + 1) - if (start_pos > 1 and not before_char:match("%s")) or (end_pos < #signature_str and not after_char:match("%s")) then - return nil, nil + for _, icon_hl in ipairs(icon_highlights) do + local icon, hl_group = unpack(icon_hl) + local line_num = 0 + while line_num < api.nvim_buf_line_count(self.buf) do + local line = api.nvim_buf_get_lines(self.buf, line_num, line_num + 1, false)[1] + local start_col = line:find(vim.pesc(icon)) + if start_col then + api.nvim_buf_add_highlight( + self.buf, + -1, + hl_group, + line_num, + start_col - 1, + start_col - 1 + #icon + ) + end + line_num = line_num + 1 + end end - - return start_pos - 1, end_pos end function SignatureHelp:display(result) @@ -231,30 +342,67 @@ function SignatureHelp:display(result) return end - local markdown, labels = markdown_for_signature_list(result.signatures, self.config) + -- Store current window and buffer + local current_win = api.nvim_get_current_win() + local current_buf = api.nvim_get_current_buf() - if vim.deep_equal(result.signatures, self.current_signatures) then + -- Prevent duplicate displays of identical content + if self.current_signatures and vim.deep_equal(result.signatures, self.current_signatures) and + result.activeParameter == self.current_active_parameter then return end + local markdown, labels = markdown_for_signature_list(result.signatures, self.config) self.current_signatures = result.signatures + self.current_active_parameter = result.activeParameter if #markdown > 0 then - self:create_float_window(markdown) - api.nvim_buf_set_option(self.buf, "filetype", "markdown") - self:set_active_parameter_highlights(result.activeParameter, result.signatures, labels) - self:apply_treesitter_highlighting() + if self.config.dock_mode.enabled then + local win, buf = self:create_dock_window() + if win and buf then + api.nvim_buf_set_option(buf, "modifiable", true) + api.nvim_buf_set_lines(buf, 0, -1, false, markdown) + api.nvim_buf_set_option(buf, "modifiable", false) + self:set_active_parameter_highlights(result.activeParameter, result.signatures, labels) + self:apply_treesitter_highlighting() + end + else + self:create_float_window(markdown) + api.nvim_buf_set_option(self.buf, "filetype", "markdown") + self:set_active_parameter_highlights(result.activeParameter, result.signatures, labels) + self:apply_treesitter_highlighting() + end else self:hide() end + + -- Restore focus to original window and buffer + api.nvim_set_current_win(current_win) + api.nvim_set_current_buf(current_buf) end function SignatureHelp:apply_treesitter_highlighting() + local buf = self.config.dock_mode.enabled and self.dock_buf or self.buf + if not buf or not api.nvim_buf_is_valid(buf) then + return + end + if not pcall(require, "nvim-treesitter") then return end - require("nvim-treesitter.highlight").attach(self.buf, "markdown") + -- Store current window and buffer + local current_win = api.nvim_get_current_win() + local current_buf = api.nvim_get_current_buf() + + -- Apply treesitter highlighting + pcall(function() + require("nvim-treesitter.highlight").attach(buf, "markdown") + end) + + -- Restore focus + api.nvim_set_current_win(current_win) + api.nvim_set_current_buf(current_buf) end function SignatureHelp:trigger() @@ -270,11 +418,12 @@ function SignatureHelp:trigger() return end - if result then + if result and result.signatures and #result.signatures > 0 then self:display(result) else self:hide() - if not self.config.silent then + -- 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 @@ -364,42 +513,266 @@ function SignatureHelp:setup_autocmds() }) end -function M.setup(opts) - opts = opts or {} - local signature_help = SignatureHelp.new() - signature_help.config = vim.tbl_deep_extend("force", signature_help.config, opts) - signature_help:setup_autocmds() +function SignatureHelp:create_dock_window() + -- Store current window and buffer + local current_win = api.nvim_get_current_win() + local current_buf = api.nvim_get_current_buf() - local toggle_key = opts.toggle_key or "" - vim.keymap.set("n", toggle_key, function() - signature_help:toggle_normal_mode() - end, { noremap = true, silent = true, desc = "Toggle signature help in normal mode" }) + -- Update dock window ID for current buffer + self.dock_win_id = "signature_help_dock_" .. current_buf - if pcall(require, "nvim-treesitter") then - require("nvim-treesitter").define_modules({ - signature_help_highlighting = { - module_path = "signature_help.highlighting", - is_supported = function(lang) - return lang == "markdown" - end, - }, + if not self.dock_win or not api.nvim_win_is_valid(self.dock_win) then + -- Create dock buffer if needed + if not self.dock_buf or not api.nvim_buf_is_valid(self.dock_buf) then + self.dock_buf = api.nvim_create_buf(false, true) + api.nvim_buf_set_option(self.dock_buf, "buftype", "nofile") + api.nvim_buf_set_option(self.dock_buf, "bufhidden", "hide") + api.nvim_buf_set_option(self.dock_buf, "modifiable", false) + api.nvim_buf_set_option(self.dock_buf, "filetype", "markdown") + + -- Set buffer name with ID for easier tracking + api.nvim_buf_set_name(self.dock_buf, self.dock_win_id) + end + + -- Calculate dock position and dimensions + local win_height = api.nvim_win_get_height(current_win) + local win_width = api.nvim_win_get_width(current_win) + local dock_height = math.min(self.config.dock_mode.height, math.floor(win_height * 0.3)) + local padding = self.config.dock_mode.padding + local dock_width = win_width - (padding * 2) + + local row = self.config.dock_mode.position == "bottom" + and win_height - dock_height - padding + or padding + + -- Create dock window with enhanced config + self.dock_win = api.nvim_open_win(self.dock_buf, false, { + relative = "win", + win = current_win, + width = dock_width, + height = dock_height, + row = row, + col = padding, + style = "minimal", + border = self.config.border, + zindex = 45, + focusable = false, -- Make window non-focusable to prevent focus issues + }) + + -- Apply window options + local win_opts = { + wrap = true, + winblend = self.config.winblend, + foldenable = false, + cursorline = false, + winhighlight = "Normal:SignatureHelpDock,FloatBorder:SignatureHelpBorder", + signcolumn = "no", + } + + for opt, value in pairs(win_opts) do + api.nvim_win_set_option(self.dock_win, opt, value) + end + + -- Set up dock window keymaps + local dock_buf_keymaps = { + ["q"] = function() self:hide() end, + [""] = function() self:hide() end, + [""] = function() self:hide() end, + [""] = function() self:next_signature() end, + [""] = function() self:prev_signature() end, + } + + for key, func in pairs(dock_buf_keymaps) do + vim.keymap.set("n", key, func, { buffer = self.dock_buf, silent = true, nowait = true }) + end + + -- Set window ID as a window variable + api.nvim_win_set_var(self.dock_win, "signature_help_id", self.dock_win_id) + end + + -- Ensure focus returns to original window + api.nvim_set_current_win(current_win) + + return self.dock_win, self.dock_buf +end + +function SignatureHelp:close_dock_window() + -- Fast check for existing dock window + if not self.dock_win_id then return end + + -- Try to find window by ID + local wins = api.nvim_list_wins() + for _, win in ipairs(wins) do + local ok, win_id = pcall(api.nvim_win_get_var, win, "signature_help_id") + if ok and win_id == self.dock_win_id then + pcall(api.nvim_win_close, win, true) + break + end + end + + -- Clean up buffer + if self.dock_buf and api.nvim_buf_is_valid(self.dock_buf) then + pcall(api.nvim_buf_delete, self.dock_buf, { force = true }) + end + + -- Reset dock window state + self.dock_win = nil + self.dock_buf = nil +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:toggle_dock_mode() + -- Store current window and buffer + local current_win = api.nvim_get_current_win() + local current_buf = api.nvim_get_current_buf() + + -- Store current signatures + local current_sigs = self.current_signatures + local current_active = self.current_active_parameter + + -- Close existing windows efficiently + if self.config.dock_mode.enabled then + self:close_dock_window() + else + if self.win and api.nvim_win_is_valid(self.win) then + pcall(api.nvim_win_close, self.win, true) + pcall(api.nvim_buf_delete, self.buf, { force = true }) + self.win = nil + self.buf = nil + end + end + + -- Toggle mode + self.config.dock_mode.enabled = not self.config.dock_mode.enabled + + -- Redisplay if we had signatures + if current_sigs then + self:display({ + signatures = current_sigs, + activeParameter = current_active }) end - vim.api.nvim_set_hl(0, "LspSignatureActiveParameter", - { fg = opts.config.active_parameter_colors.fg, bg = opts.config.active_parameter_colors.bg }) - vim.cmd(string.format([[ - highlight default SignatureHelpMethod guifg=%s - highlight default SignatureHelpParameter guifg=%s - highlight default SignatureHelpDocumentation guifg=%s - ]], signature_help.config.colors.method, signature_help.config.colors.parameter, - signature_help.config.colors.documentation)) - if opts.override then - vim.lsp.handlers["textDocument/signatureHelp"] = function(_, result, context, config) - config = vim.tbl_deep_extend("force", signature_help.config, config or {}) - signature_help:display(result) + -- Restore focus + pcall(api.nvim_set_current_win, current_win) + pcall(api.nvim_set_current_buf, current_buf) +end + +function SignatureHelp:setup_keymaps() + -- Setup toggle keys using the actual config + local toggle_key = self.config.toggle_key + local dock_toggle_key = self.config.dock_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 + + if dock_toggle_key then + vim.keymap.set("n", dock_toggle_key, function() + self:toggle_dock_mode() + end, { noremap = true, silent = true, desc = "Toggle between dock and float 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 = { + SignatureHelpDock = { link = "NormalFloat" }, + 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 +M.toggle_dock = function() + if M._instance then + M._instance:toggle_dock_mode() + end +end + +M.toggle_normal_mode = function() + if M._instance then + M._instance:toggle_normal_mode() + end end return M