From 1e29734b9471ab761add977cb1d96357931efc2b Mon Sep 17 00:00:00 2001 From: jinzhongjia Date: Thu, 1 Feb 2024 13:19:09 +0800 Subject: [PATCH] [feature] lsp signature help supports (#40), but default is disabled * [feat]: LSP Signature help support, but default is disabled --- TODO.txt | 1 + lua/LspUI/_meta.lua | 14 +- lua/LspUI/api.lua | 3 +- lua/LspUI/config.lua | 33 ++++ lua/LspUI/diagnostic/util.lua | 10 +- lua/LspUI/modules.lua | 1 + lua/LspUI/pos_abstract.lua | 6 +- lua/LspUI/signature/init.lua | 55 +++++- lua/LspUI/signature/util.lua | 307 ++++++++++++++++++++++++++++++++++ 9 files changed, 417 insertions(+), 13 deletions(-) create mode 100644 TODO.txt create mode 100644 lua/LspUI/signature/util.lua diff --git a/TODO.txt b/TODO.txt new file mode 100644 index 0000000..44f5824 --- /dev/null +++ b/TODO.txt @@ -0,0 +1 @@ +- lsp request err not handle diff --git a/lua/LspUI/_meta.lua b/lua/LspUI/_meta.lua index 6be8776..233dfe1 100644 --- a/lua/LspUI/_meta.lua +++ b/lua/LspUI/_meta.lua @@ -43,9 +43,9 @@ -- -- this is just some config for definition, type definition, declaration, reference, implementation --- @class LspUI_pos_config ---- @field secondary_keybind { jump: string?, jump_tab: string?, jump_split: string?, jump_vsplit: string?, quit:string?, hide_main:string?, fold_all:string?, expand_all:string?, enter: string? }? ---- @field main_keybind { back: string?, hide_secondary: string? }? ---- @field transparency number +--- @field secondary_keybind { jump: string?, jump_tab: string?, jump_split: string?, jump_vsplit: string?, quit:string?, hide_main:string?, fold_all:string?, expand_all:string?, enter: string? }? secondary view keybind +--- @field main_keybind { back: string?, hide_secondary: string? }? main view keybind +--- @field transparency number? transparency for pos_config --- @class LspUI_definition_config --- @field enable boolean? whether enable `definition` module @@ -71,6 +71,13 @@ --- @field enable boolean? whether enable `call_hierarchy` module --- @field command_enable boolean? whether enable command for `call_hierarchy` +--- @class LspUI_signature +--- @field enable boolean? whether enable `signature` module +--- @field command_enable boolean? whether enable command for `signature` +--- @field icon string? the icon for float signature +--- @field color {fg: string?, bg: string?}? the color for signature +--- @field debounce (integer|boolean)? whether enable debounce for signature ? defalt is 250 milliseconds, this will reduce calculations when you move the cursor frequently, but it will cause the delay of signature, false will diable it + --- @class LspUI_config config for LspUI --- @field rename LspUI_rename_config? `rename` module --- @field lightbulb LspUI_lightbulb_config? `lightbulb` module @@ -86,3 +93,4 @@ --- @field pos_keybind LspUI_pos_keybind_config? keybind for `definition`, `type definition`, `declaration`, `reference`, implementation --- @field pos_config LspUI_pos_config? keybind for `definition`, `type definition`, `declaration`, `reference`, implementation --- @field call_hierarchy LspUI_call_hierarchy_config? `call_hierarchy` module +--- @field signature LspUI_signature? `signature` module diff --git a/lua/LspUI/api.lua b/lua/LspUI/api.lua index e60723f..465b549 100644 --- a/lua/LspUI/api.lua +++ b/lua/LspUI/api.lua @@ -28,6 +28,7 @@ M.api = { implementation = modules.implementation.run, inlay_hint = modules.inlay_hint.run, call_hierarchy = modules.call_hierarchy.run, + signature = modules.signature.status_line, } -return M +return M.api diff --git a/lua/LspUI/config.lua b/lua/LspUI/config.lua index b50ab5b..141ec5f 100644 --- a/lua/LspUI/config.lua +++ b/lua/LspUI/config.lua @@ -139,12 +139,26 @@ local default_pos_config = { transparency = default_transparency, } +-- TODO: now, this is not avaiable +-- --- @type LspUI_call_hierarchy_config local default_call_hierarchy_config = { enable = true, command_enable = true, } +--- @type LspUI_signature +local default_signature_config = { + enable = false, + command_enable = true, + icon = "✨", + color = { + fg = "#FF8C00", + bg = nil, + }, + debounce = 300, +} + -- default config --- @type LspUI_config local default_config = { @@ -161,6 +175,7 @@ local default_config = { reference = default_reference_config, pos_keybind = default_pos_keybind_config, call_hierarchy = default_call_hierarchy_config, + signature = default_signature_config, } -- Prevent plugins from being initialized multiple times @@ -396,6 +411,24 @@ M.inlay_hint_setup = function(inlay_hint_config) end end +-- separate function for `signature` module +--- @param signature_config LspUI_signature +M.signature_setup = function(signature_config) + M.options.signature = vim.tbl_deep_extend( + "force", + M.options.signature or default_signature_config, + signature_config + ) + + local signature = require("LspUI.signature") + + if signature_config.enable then + signature.init() + else + signature.deinit() + end +end + -- TODO:add separate setup function for call_hierarchy return M diff --git a/lua/LspUI/diagnostic/util.lua b/lua/LspUI/diagnostic/util.lua index ada8d65..bb405ed 100644 --- a/lua/LspUI/diagnostic/util.lua +++ b/lua/LspUI/diagnostic/util.lua @@ -38,13 +38,13 @@ local diagnostic_severity_to_hightlight = function(severity) return arr[severity] or nil end ---- @param diagnostics Diagnostic[] ---- @return Diagnostic[][][] +--- @param diagnostics vim.Diagnostic[] +--- @return vim.Diagnostic[][][] local sort_diagnostics = function(diagnostics) local sorted_diagnostics = {} for _, diagnostic in pairs(diagnostics) do - --- @type Diagnostic[][]? + --- @type vim.Diagnostic[][]? local lnum_diagnostics = sorted_diagnostics[diagnostic.lnum] if lnum_diagnostics == nil then lnum_diagnostics = {} @@ -64,12 +64,12 @@ local sort_diagnostics = function(diagnostics) end -- get next position diagnostics ---- @param sorted_diagnostics Diagnostic[][][] +--- @param sorted_diagnostics vim.Diagnostic[][][] --- @param row integer (row,col) is a tuple, get from `nvim_win_get_cursor`, 1 based --- @param col integer (row,col) is a tuple, get from `nvim_win_get_cursor`, 0 based --- @param search_forward boolean true is down, false is up --- @param buffer_id integer ---- @return Diagnostic[]? +--- @return vim.Diagnostic[]? local next_position_diagnostics = function( sorted_diagnostics, row, diff --git a/lua/LspUI/modules.lua b/lua/LspUI/modules.lua index 563ec04..8c64f1d 100644 --- a/lua/LspUI/modules.lua +++ b/lua/LspUI/modules.lua @@ -11,4 +11,5 @@ return { reference = require("LspUI.reference"), inlay_hint = require("LspUI.inlay_hint"), call_hierarchy = require("LspUI.call_hierarchy"), + signature = require("LspUI.signature"), } diff --git a/lua/LspUI/pos_abstract.lua b/lua/LspUI/pos_abstract.lua index 48b9d66..a3df13a 100644 --- a/lua/LspUI/pos_abstract.lua +++ b/lua/LspUI/pos_abstract.lua @@ -432,10 +432,10 @@ local secondary_view_autocmd = function() buffer = M.secondary_view_buffer(), callback = function() -- get current cursor position - local cursor_position = - api.nvim_win_get_cursor(M.secondary_view_window()) - local lnum = cursor_position[1] + --- @type integer + ---@diagnostic disable-next-line: assign-type-mismatch + local lnum = fn.line(".") local uri, range = get_lsp_position_by_lnum(lnum) if not uri then diff --git a/lua/LspUI/signature/init.lua b/lua/LspUI/signature/init.lua index f3aada6..f01f5ee 100644 --- a/lua/LspUI/signature/init.lua +++ b/lua/LspUI/signature/init.lua @@ -1 +1,54 @@ --- TODO: complete this +local api, fn = vim.api, vim.fn +local config = require("LspUI.config") +local lib_notify = require("LspUI.lib.notify") +local util = require("LspUI.signature.util") + +local M = {} + +local is_initialized = false + +M.init = function() + if not config.options.signature.enable then + return + end + + if is_initialized then + return + end + + is_initialized = true + + local hl_val = { + fg = config.options.signature.color.fg, + italic = true, + -- standout = true, + undercurl = true, + } + + if config.options.signature.color.bg then + hl_val.fg = config.options.signature.color.bg + end + api.nvim_set_hl(0, "LspUI_Signature", hl_val) + + -- init autocmd + util.autocmd() +end + +M.deinit = function() + if not is_initialized then + lib_notify.Info("signature has been deinit") + end + + is_initialized = false + + -- remove autocmd + util.deautocmd() +end + +M.run = function() + lib_notify.Info("signature has no run func") +end + +M.status_line = util.status_line + +return M diff --git a/lua/LspUI/signature/util.lua b/lua/LspUI/signature/util.lua new file mode 100644 index 0000000..4cb65a3 --- /dev/null +++ b/lua/LspUI/signature/util.lua @@ -0,0 +1,307 @@ +local api, lsp, fn = vim.api, vim.lsp, vim.fn +local signature_feature = lsp.protocol.Methods.textDocument_signatureHelp + +local config = require("LspUI.config") +local lib_notify = require("LspUI.lib.notify") +local lib_util = require("LspUI.lib.util") + +local M = {} + +-- this variable records whether there is a virtual_text +--- @type boolean +local is_there_virtual_text = false + +--- @class signature_info +--- @field label string +--- @field hint integer? +--- @field parameters {label: string, doc: (string|lsp.MarkupContent)?}[]? +--- @field doc string? + +--- @param help lsp.SignatureHelp|nil +--- @return signature_info? res len will not be zero +local build_signature_info = function(help) + if not help then + return nil + end + if #help.signatures == 0 then + return nil + end + + local active_signature = help.activeSignature and help.activeSignature + 1 + or 1 + local active_parameter = help.activeParameter and help.activeParameter + 1 + or 1 + + --- @type signature_info + ---@diagnostic disable-next-line: missing-fields + local res = {} + + local signature = help.signatures[active_signature] + if signature.activeParameter then + active_parameter = signature.activeParameter + 1 + end + + res.label = signature.label + ---@diagnostic disable-next-line: assign-type-mismatch + res.doc = type(signature.documentation) == "table" + and signature.documentation.value + or signature.documentation + + if not signature.parameters or (#signature.parameters == 0) then + return res + end + + --- @type lsp.ParameterInformation[] + local parameters = signature.parameters + + --- @type { label: string, doc: (string|lsp.MarkupContent)? }[] + local params = {} + for _, parameter in ipairs(parameters) do + if type(parameter.label) == "string" then + table.insert(params, { + label = parameter.label, + doc = parameter.documentation, + }) + else + local str = string.sub( + signature.label, + parameter.label[1] + 1, + parameter.label[2] + ) + table.insert(params, { + label = str, + doc = parameter.documentation, + }) + end + end + + res.parameters = params + res.hint = active_parameter + + return res +end + +-- this a list to store those buffers which can use signature +--- @type {[number]: boolean} +local buffer_list = {} + +--- @type integer +local signature_group + +local signature_namespace = api.nvim_create_namespace("LspUI_signature") + +--- @type { data: lsp.SignatureHelp?, } +local backup = {} + +--- @param buffer_id number buffer's id +--- @param callback fun(result: lsp.SignatureHelp|nil) callback function +M.request = function(buffer_id, callback) + -- this buffer id maybe invalid + if not api.nvim_buf_is_valid(buffer_id) then + return + end + + local clients = M.get_clients(buffer_id) + if not clients then + return + end + + local params = lsp.util.make_position_params() + -- NOTE: we just use one client to get the lsp signature + local client = clients[1] + -- for _, client in pairs(clients or {}) do + client.request( + signature_feature, + params, + --- @param err lsp.ResponseError + --- @param result lsp.SignatureHelp? + function(err, result, _, _) + if err then + lib_notify.Error( + string.format( + "sorry, lsp %s report siganature error:%d, &s", + client.name, + err.code, + err.message + ) + ) + return + end + callback(result) + end, + buffer_id + ) + -- end +end + +-- get all valid clients for lightbulb +--- @param buffer_id integer +--- @return lsp.Client[]|nil clients array or nil +M.get_clients = function(buffer_id) + local clients = + lsp.get_clients({ bufnr = buffer_id, method = signature_feature }) + return #clients == 0 and nil or clients +end + +--- @type function +local func + +local signature_handle = function() + local current_buffer = api.nvim_get_current_buf() + -- when current buffer can not use signature + if not buffer_list[current_buffer] then + backup.data = nil + return + end + M.request(current_buffer, function(result) + backup.data = result + + local mode_info = vim.api.nvim_get_mode() + local mode = mode_info["mode"] + local is_insert = mode:find("i") ~= nil or mode:find("ic") ~= nil + if not is_insert then + return + end + + M.clean_render(current_buffer) + + local callback_current_buffer = api.nvim_get_current_buf() + -- when call current buffer is not equal to current buffer, return + if callback_current_buffer ~= current_buffer then + return + end + + local current_window = api.nvim_get_current_win() + M.render(result, current_buffer, current_window) + end) +end + +local build_func = function() + if not config.options.signature.debounce then + func = signature_handle + return + end + + --- @type integer + local time = type(config.options.lightbulb.debounce) == "number" + ---@diagnostic disable-next-line: param-type-mismatch + and math.floor(config.options.signature.debounce) + or 300 + + func = lib_util.debounce(signature_handle, time) +end + +--- @param data lsp.SignatureHelp|nil +--- @param buffer_id integer +--- @param windows_id integer +M.render = function(data, buffer_id, windows_id) + local info = build_signature_info(data) + if not info then + return + end + + if not info.hint then + return + end + + --- @type integer + ---@diagnostic disable-next-line: assign-type-mismatch + local row = fn.line(".") == 1 and 1 or fn.line(".") - 2 + --- @type integer + local col = fn.virtcol(".") - 1 + + api.nvim_buf_set_extmark(buffer_id, signature_namespace, row, 0, { + virt_text = { + { + string.format( + "%s %s", + config.options.signature.icon, + info.parameters[info.hint].label + ), + "LspUI_Signature", + }, + }, + virt_text_win_col = col, + hl_mode = "blend", + }) + is_there_virtual_text = true +end + +-- clean signature virtual text +--- @param buffer_id integer +M.clean_render = function(buffer_id) + if not is_there_virtual_text then + return + end + + api.nvim_buf_clear_namespace(buffer_id, signature_namespace, 0, -1) + is_there_virtual_text = false +end + +-- this is autocmd init for signature +M.autocmd = function() + signature_group = + api.nvim_create_augroup("Lspui_signature", { clear = true }) + + -- build debounce function + build_func() + + api.nvim_create_autocmd("LspAttach", { + group = signature_group, + callback = function() + -- get current buffer + local current_buffer = api.nvim_get_current_buf() + + local clients = M.get_clients(current_buffer) + if not clients then + -- no clients support signature help + return + end + + buffer_list[current_buffer] = true + end, + desc = lib_util.command_desc("Lsp attach signature cmd"), + }) + + -- maybe this can also use CurosrHold + api.nvim_create_autocmd({ "CursorMovedI", "InsertEnter" }, { + group = signature_group, + callback = vim.schedule_wrap(func), + desc = lib_util.command_desc("Signature update when CursorHoldI"), + }) + + -- when buffer is deleted, disable buffer siganture + api.nvim_create_autocmd({ "BufDelete" }, { + group = signature_group, + callback = function() + local current_buffer = api.nvim_get_current_buf() + M.clean_render(current_buffer) + if buffer_list[current_buffer] then + buffer_list[current_buffer] = false + end + end, + desc = lib_util.command_desc("Exec signature clean cmd when QuitPre"), + }) + + api.nvim_create_autocmd({ "InsertLeave", "WinLeave" }, { + group = signature_group, + callback = function() + local current_buffer = api.nvim_get_current_buf() + M.clean_render(current_buffer) + end, + desc = lib_util.command_desc( + "Exec signature virtual text clean cmd when InsertLeave or WinLeave" + ), + }) +end + +M.deautocmd = function() + api.nvim_del_augroup_by_id(signature_group) +end + +--- @return signature_info? +M.status_line = function() + return build_signature_info(backup.data) +end + +return M