diff --git a/README.md b/README.md index af92d65..43b455d 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,216 @@ # signup.nvim + 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). + +## Features + +- **Signature Help**: Displays function signatures and parameter information in a floating window. +- **Toggle Mode**: Toggle signature help in normal mode. +- **Customizable**: Highly customizable with options for icons, colors, and more. +- **Integration**: Integrates with nvim-treesitter for syntax highlighting. +- **Notifications**: Displays notifications for errors, warnings, and info messages. + +## Installation + +### Using [lazy.nvim](https://github.com/folke/lazy.nvim) + +Add the following to your `init.lua`: + +```lua +require("lazy").setup({ + { + "Dan7h3x/signup.nvim", + config = function() + require("signup").setup({ + -- Your configuration options here + }) + end + } +}) +``` + +### Using Vim-Plug + +Add the following to your `init.vim`: + +```vim +Plug "Dan7h3x/signup.nvim" +``` + +Then, in your `init.lua`: + +```lua +lua << EOF +require('signup').setup({ + -- Your configuration options here +}) +EOF +``` + +## Configuration + +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({ + silent = false, + number = true, + icons = { + parameter = " ", + method = " ", + documentation = " ", + }, + colors = { + parameter = "#86e1fc", + method = "#c099ff", + documentation = "#4fd6be", + }, + border = "rounded", + winblend = 10, + override = true, -- Override default LSP handler for signatureHelp +}) +``` + +### 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 + +Contributions are welcome! Please feel free to submit a pull request or open an issue if you encounter any problems or have suggestions for improvements. + +## License + +This plugin is licensed under the MIT License. See the [LICENSE](LICENSE) file for more details. + +--- + +## Good Color Schemes + +To make your signature help window look even better, you can use the following color schemes: + +### Gruvbox + +```lua +vim.cmd([[ + colorscheme gruvbox + highlight NormalFloat guibg=#3c3836 guifg=#ebdbb2 + highlight FloatBorder guifg=#8ec07c + highlight LspSignatureActiveParameter guifg=#fabd2f guibg=#3c3836 gui=bold + highlight SignatureHelpMethod guifg=#83a598 + highlight SignatureHelpParameter guifg=#b8bb26 + highlight SignatureHelpDocumentation guifg=#d3869b + highlight NotificationInfo guifg=#8ec07c guibg=#3c3836 + highlight NotificationWarn guifg=#fabd2f guibg=#3c3836 + highlight NotificationError guifg=#fb4934 guibg=#3c3836 +]]) +``` + +### Nord + +```lua +vim.cmd([[ + colorscheme nord + highlight NormalFloat guibg=#3b4252 guifg=#e5e9f0 + highlight FloatBorder guifg=#81a1c1 + highlight LspSignatureActiveParameter guifg=#88c0d0 guibg=#3b4252 gui=bold + highlight SignatureHelpMethod guifg=#81a1c1 + highlight SignatureHelpParameter guifg=#a3be8c + highlight SignatureHelpDocumentation guifg=#b48ead + highlight NotificationInfo guifg=#81a1c1 guibg=#3b4252 + highlight NotificationWarn guifg=#ebcb8b guibg=#3b4252 + highlight NotificationError guifg=#bf616a guibg=#3b4252 +]]) +``` + +### OneDark + +```lua +vim.cmd([[ + colorscheme onedark + highlight NormalFloat guibg=#282c34 guifg=#abb2bf + highlight FloatBorder guifg=#61afef + highlight LspSignatureActiveParameter guifg=#e06c75 guibg=#282c34 gui=bold + highlight SignatureHelpMethod guifg=#61afef + highlight SignatureHelpParameter guifg=#98c379 + highlight SignatureHelpDocumentation guifg=#c678dd + highlight NotificationInfo guifg=#61afef guibg=#282c34 + highlight NotificationWarn guifg=#e5c07b guibg=#282c34 + highlight NotificationError guifg=#e06c75 guibg=#282c34 +]]) +``` + +Feel free to customize these color schemes to match your personal preferences! + +--- diff --git a/lua/signup/init.lua b/lua/signup/init.lua new file mode 100644 index 0000000..afbeff3 --- /dev/null +++ b/lua/signup/init.lua @@ -0,0 +1,340 @@ +local api = vim.api + +local M = {} + +local SignatureHelp = {} +SignatureHelp.__index = SignatureHelp + +function SignatureHelp.new() + return setmetatable({ + 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", + }, + border = "rounded", + winblend = 10, + } + }, SignatureHelp) +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 + for index, signature in ipairs(signatures) do + table.insert(labels, #lines + 1) + + local suffix = number and (' ' .. signature_index_comment(index)) or '' + + 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, "```") + + if signature.parameters and #signature.parameters > 0 then + table.insert(lines, "") + 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)) + end + end + + if signature.documentation then + table.insert(lines, "") + table.insert(lines, string.format("%s Documentation:", config.icons.documentation)) + vim.list_extend(lines, vim.split(signature.documentation.value or signature.documentation, "\n")) + end + + if index ~= #signatures then + table.insert(lines, "---") + 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, 4) + + local cursor = api.nvim_win_get_cursor(0) + local row = cursor[1] - api.nvim_win_get_cursor(0)[1] + + local win_config = { + relative = "cursor", + row = row - 7, + col = 0, + width = width, + height = height, + style = "minimal", + border = self.config.border, + } + + 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 + + api.nvim_buf_set_option(self.buf, "modifiable", true) + api.nvim_buf_set_lines(self.buf, 0, -1, false, contents) + api.nvim_buf_set_option(self.buf, "modifiable", false) + api.nvim_win_set_option(self.win, "foldenable", false) + api.nvim_win_set_option(self.win, "wrap", false) + api.nvim_win_set_option(self.win, "winblend", self.config.winblend) + + self.visible = true +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 + self.visible = false + self.current_signatures = nil + 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 .. "\\>") + elseif type(label) == "table" then + 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:display(result) + if not result or not result.signatures or #result.signatures == 0 then + self:hide() + return + end + + local markdown, labels = markdown_for_signature_list(result.signatures, self.config) + + if vim.deep_equal(result.signatures, self.current_signatures) then + return + end + + self.current_signatures = result.signatures + + 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() + else + self:hide() + end +end + +function SignatureHelp:apply_treesitter_highlighting() + if not pcall(require, "nvim-treesitter") then + return + end + + require("nvim-treesitter.highlight").attach(self.buf, "markdown") +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() + return + end + + if result then + self:display(result) + else + self:hide() + if not self.config.silent 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() + end +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(30, function() + self:trigger() + end) + end + + api.nvim_create_autocmd({ "CursorMovedI", "TextChangedI" }, { + group = group, + callback = function() + local cmp_visible = vim.fn.exists('*cmp#visible') == 1 and vim.fn.eval('cmp#visible()') == 1 + 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", "BufHidden", "BufLeave" }, { + group = group, + callback = function() + self:hide() + 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 + }) + + api.nvim_create_autocmd("ColorScheme", { + group = group, + callback = function() + if self.visible then + self:apply_treesitter_highlighting() + self:set_active_parameter_highlights(self.current_signatures.activeParameter, self.current_signatures, {}) + end + end + }) +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() + + 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" }) + + 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, + }, + }) + end + + vim.cmd(string.format([[ + highlight default LspSignatureActiveParameter guifg=#c8d3f5 guibg=#4ec9b0 gui=bold + highlight default link FloatBorder Normal + highlight default NormalFloat guibg=#1e1e1e guifg=#d4d4d4 + 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) + end + end +end + +return M