377 lines
12 KiB
Lua
377 lines
12 KiB
Lua
local api = vim.api
|
|
|
|
local function get_parameter_label(result)
|
|
local signatures = result.signatures
|
|
|
|
local activeSignature = result.activeSignature or 0
|
|
activeSignature = activeSignature + 1
|
|
local signature = signatures[activeSignature]
|
|
|
|
if signature == nil or signature.parameters == nil then -- no parameter
|
|
return ""
|
|
end
|
|
|
|
local activeParameter = signature.activeParameter or result.activeParameter
|
|
|
|
if activeParameter == nil or activeParameter < 0 then
|
|
return ""
|
|
end
|
|
|
|
if signature.parameters == nil then
|
|
return ""
|
|
end
|
|
|
|
if activeParameter >= #signature.parameters then
|
|
activeParameter = #signature.parameters - 1
|
|
end
|
|
|
|
if signature.parameters[activeParameter + 1] == nil then
|
|
return ""
|
|
end
|
|
|
|
local param = signature.parameters[activeParameter + 1].label
|
|
local param_str
|
|
-- Handle both string and table parameter labels
|
|
if type(param) == "table" then
|
|
local s = param[1]
|
|
local e = param[2]
|
|
param_str = string.sub(signature.label, s, e)
|
|
elseif type(param) == "string" then
|
|
param_str = signature.label
|
|
end
|
|
|
|
return param_str
|
|
end
|
|
|
|
---@class opts table
|
|
---@field brk_chars string string of characters to break lines at
|
|
---@field indent_str string string to indent wrapped lines with
|
|
---@param str string string to wrap into lines
|
|
---@param maxlen integer maximum length of lines
|
|
---@return table table of wrapped lines
|
|
local function _wrap_lines(str, maxlen, opts)
|
|
local atoms = {}
|
|
opts = opts or {}
|
|
local brk_chars = opts.brk_chars or ",()"
|
|
local indent_str = opts.indent_str or " "
|
|
local end_char = string.sub(brk_chars, 1, 1)
|
|
|
|
str = str .. end_char -- add break char at end of string, so we can match the end of the line
|
|
for w in string.gmatch(str, "[^" .. brk_chars .. "]*" .. "[" .. brk_chars .. "]") do
|
|
table.insert(atoms, w)
|
|
end
|
|
atoms[#atoms] = string.gsub(atoms[#atoms], end_char .. "$", "") -- remove break char at end
|
|
|
|
local lines = {}
|
|
lines[1] = ""
|
|
local line_num = 1
|
|
|
|
local padding = opts.padding or 2
|
|
local is = ""
|
|
for i, w in ipairs(atoms) do
|
|
if line_num == 1 then
|
|
is = ""
|
|
else
|
|
is = indent_str
|
|
end
|
|
if string.len(lines[line_num]) + string.len(w) + (padding * 2) <= maxlen then
|
|
lines[line_num] = lines[line_num] .. w
|
|
else
|
|
lines[line_num] = string.rep(" ", padding) .. is .. lines[line_num] .. string.rep(" ", padding) --add padding
|
|
line_num = line_num + 1
|
|
lines[line_num] = string.gsub(w, "^%s", "")
|
|
end
|
|
if i == #atoms then
|
|
lines[line_num] = string.rep(" ", padding) .. is .. lines[line_num] .. string.rep(" ", padding) --add padding to last line
|
|
end
|
|
end
|
|
|
|
return lines
|
|
end
|
|
|
|
--@param signature_help string[]
|
|
--@param maxlen integer
|
|
local function make_signature_lines(signature_help, maxlen)
|
|
if not signature_help.signatures then
|
|
return
|
|
end
|
|
local lines = {}
|
|
local active_signature = signature_help.activeSignature or 0
|
|
if active_signature >= #signature_help.signatures or active_signature < 0 then
|
|
active_signature = 0
|
|
end
|
|
|
|
local signature = signature_help.signatures[active_signature + 1]
|
|
if not signature then
|
|
return
|
|
end
|
|
|
|
vim.list_extend(lines, vim.split(signature.label, "\n", { plain = true, trimempty = true }))
|
|
|
|
local wrapped_lines = {}
|
|
for _, ll in ipairs(lines) do
|
|
vim.list_extend(wrapped_lines, _wrap_lines(ll, maxlen))
|
|
end
|
|
return wrapped_lines
|
|
end
|
|
|
|
local function make_doc_lines(result, ft)
|
|
local active_signature = result.activeSignature or 0
|
|
local signature = result.signatures[active_signature + 1]
|
|
local doc_lines = {}
|
|
if not signature then
|
|
return
|
|
end
|
|
if signature.documentation then
|
|
-- if LSP returns plain string, we treat it as plaintext. This avoids
|
|
-- special characters like underscore or similar from being interpreted
|
|
-- as markdown font modifiers
|
|
if type(signature.documentation) == "string" then
|
|
signature.documentation = { kind = "plaintext", value = signature.documentation }
|
|
end
|
|
vim.lsp.util.convert_input_to_markdown_lines(signature.documentation, doc_lines)
|
|
end
|
|
for _, parameter in ipairs(signature.parameters) do
|
|
if parameter.documentation then
|
|
vim.lsp.util.convert_input_to_markdown_lines(parameter.documentation, doc_lines)
|
|
end
|
|
end
|
|
return doc_lines
|
|
end
|
|
|
|
local _make_floating_popup_size = function(contents, opts)
|
|
opts = opts or {}
|
|
local max_width = opts.max_width or 80
|
|
local max_height = opts.max_height or 10
|
|
local line_widths = {}
|
|
|
|
local width = 0
|
|
for i, line in ipairs(contents) do
|
|
line_widths[i] = vim.fn.strdisplaywidth(line:gsub("%z", "\n"))
|
|
width = math.max(line_widths[i], width)
|
|
end
|
|
|
|
-- local border_width = get_border_size(opts).width
|
|
local border_width = 2 --TODO: do we need to deal with this programmatically?
|
|
local screen_width = api.nvim_win_get_width(0)
|
|
width = math.min(width, screen_width)
|
|
width = math.min(width, max_width)
|
|
|
|
-- make sure borders are always inside the screen
|
|
if width + border_width > screen_width then
|
|
width = screen_width - border_width
|
|
end
|
|
|
|
local height = #contents
|
|
height = 0
|
|
if vim.tbl_isempty(line_widths) then
|
|
for _, line in ipairs(contents) do
|
|
local line_width = vim.fn.strdisplaywidth(line:gsub("%z", "\n"))
|
|
height = height + math.ceil(line_width / max_width)
|
|
end
|
|
else
|
|
for i = 1, #contents do
|
|
height = height + math.max(1, math.ceil(line_widths[i] / max_width))
|
|
end
|
|
end
|
|
if max_height then
|
|
height = math.min(height, max_height)
|
|
end
|
|
|
|
return width, height
|
|
end
|
|
|
|
---@param sig_content table|nil signature window content
|
|
---@param doc_content table|nil documentation window content
|
|
---@param opts table window options
|
|
---@returns (table) Options
|
|
local function _make_floating_popup_options(sig_content, doc_content, opts)
|
|
local sig_width, sig_height = _make_floating_popup_size(sig_content, opts)
|
|
local doc_width, doc_height = _make_floating_popup_size(doc_content, opts)
|
|
local border_height = 2 --TODO: get this from opts
|
|
|
|
local offset_y = opts.offset_y or 0
|
|
local offset_x = opts.offset_x or 0
|
|
local anchor_bias = opts.anchor_bias or "auto"
|
|
local relative = "win"
|
|
local sig_anchor
|
|
local doc_anchor
|
|
local sig_row, sig_col
|
|
local doc_row, doc_col
|
|
local curs_row = vim.fn.winline()
|
|
local curs_col = vim.fn.wincol()
|
|
|
|
local lines_above = curs_row - 1
|
|
local lines_below = vim.fn.winheight(0) - lines_above
|
|
|
|
if -- figure out signature anchor and height
|
|
lines_above < sig_height + border_height
|
|
or (anchor_bias == "below" and lines_below > sig_height + border_height + offset_y)
|
|
or (anchor_bias == "auto" and lines_below > lines_above)
|
|
then
|
|
sig_anchor = "N"
|
|
sig_height = math.min(lines_below - offset_y, sig_height)
|
|
sig_row = curs_row + offset_y
|
|
else
|
|
sig_anchor = "S"
|
|
sig_height = math.min(lines_above - offset_y, sig_height)
|
|
sig_row = curs_row - 1 - offset_y
|
|
end
|
|
|
|
if sig_anchor == "N" then
|
|
lines_below = lines_below - (sig_height + border_height) --new lines_below including sig
|
|
else
|
|
lines_above = lines_above - (sig_height + border_height) --new lines_above including sig
|
|
end
|
|
|
|
if -- figure out documentation anchor and height
|
|
lines_above < doc_height + border_height
|
|
or (anchor_bias == "below" and lines_below > doc_height + border_height)
|
|
or (anchor_bias == "auto" and lines_below > lines_above)
|
|
then
|
|
doc_anchor = "N"
|
|
doc_height = math.min(lines_below, doc_height)
|
|
if sig_anchor == "N" then --docs and sigs below
|
|
doc_row = curs_row + offset_y + (sig_height + border_height) - 1
|
|
else --docs above sigs below
|
|
doc_row = curs_row - 1 - offset_y
|
|
end
|
|
else
|
|
doc_anchor = "S"
|
|
doc_height = math.min(lines_above, doc_height)
|
|
if sig_anchor == "S" then --docs and sigs above
|
|
doc_row = curs_row - (offset_y + sig_height + border_height)
|
|
else --docs below sigs above
|
|
doc_row = curs_row - 1 - offset_y
|
|
end
|
|
end
|
|
|
|
local wincol = vim.fn.wincol()
|
|
if wincol + sig_width + offset_x <= vim.o.columns then
|
|
sig_anchor = sig_anchor .. "W"
|
|
doc_anchor = doc_anchor .. "W"
|
|
sig_col = curs_col - wincol
|
|
doc_col = curs_col - wincol
|
|
else
|
|
sig_anchor = sig_anchor .. "E"
|
|
doc_anchor = doc_anchor .. "E"
|
|
sig_col = curs_col + 1 - wincol
|
|
doc_col = curs_col + 1 - wincol
|
|
end
|
|
local first_col = vim.fn.getwininfo(vim.fn.win_getid())[1].textoff
|
|
|
|
return { --signature window options
|
|
anchor = sig_anchor,
|
|
row = sig_row,
|
|
col = sig_col + first_col + offset_x,
|
|
width = sig_width,
|
|
height = sig_height,
|
|
focusable = opts.focusable,
|
|
relative = relative,
|
|
style = "minimal",
|
|
border = opts.border or "rounded",
|
|
zindex = opts.zindex or 50,
|
|
-- winblend = opts.winblend or 10,
|
|
}, { --documentation window options
|
|
anchor = doc_anchor,
|
|
row = doc_row,
|
|
col = doc_col + first_col + offset_x,
|
|
width = doc_width,
|
|
height = doc_height,
|
|
focusable = opts.focusable,
|
|
relative = relative,
|
|
style = "minimal",
|
|
border = opts.border or "rounded",
|
|
zindex = opts.zindex or 50,
|
|
-- winblend = opts.winblend or 10,
|
|
}
|
|
end
|
|
|
|
---@param result string[] signature help result
|
|
---@param syntax string syntax language for highlighting. i.e. "lua" or "markdown"
|
|
---@param opts table window options
|
|
---@return function function to open signature window
|
|
---@return function function to open documentation window
|
|
local function create_float_windows(result, syntax, opts)
|
|
local npcall = vim.F.npcall
|
|
vim.validate({
|
|
result = { result, "t" },
|
|
syntax = { syntax, "s", true },
|
|
opts = { opts, "t", true },
|
|
})
|
|
opts = opts or {}
|
|
|
|
local bufnr = api.nvim_get_current_buf()
|
|
local existing_sig_win = npcall(api.nvim_buf_get_var, bufnr, "lsp_sig_win")
|
|
local existing_doc_win = npcall(api.nvim_buf_get_var, bufnr, "lsp_doc_win")
|
|
|
|
if existing_sig_win and api.nvim_win_is_valid(existing_sig_win) then
|
|
api.nvim_win_close(existing_sig_win, true)
|
|
end
|
|
if existing_doc_win and api.nvim_win_is_valid(existing_doc_win) then
|
|
api.nvim_win_close(existing_doc_win, true)
|
|
end
|
|
|
|
-- Create the buffers
|
|
local sig_bufnr = api.nvim_create_buf(false, true)
|
|
local doc_bufnr = api.nvim_create_buf(false, true)
|
|
|
|
syntax = syntax or "text"
|
|
vim.bo[sig_bufnr].syntax = syntax
|
|
vim.bo[sig_bufnr].ft = syntax
|
|
vim.treesitter.start(sig_bufnr)
|
|
vim.bo[doc_bufnr].syntax = "markdown"
|
|
vim.bo[doc_bufnr].ft = "markdown"
|
|
vim.treesitter.start(doc_bufnr)
|
|
|
|
local sig_content = make_signature_lines(result, opts.max_width)
|
|
local doc_content = make_doc_lines(result, syntax)
|
|
---@diagnostic disable-next-line: param-type-mismatch
|
|
api.nvim_buf_set_lines(sig_bufnr, 0, -1, true, sig_content)
|
|
---@diagnostic disable-next-line: param-type-mismatch
|
|
api.nvim_buf_set_lines(doc_bufnr, 0, -1, true, doc_content)
|
|
|
|
local sig_float_options, doc_float_options = _make_floating_popup_options(sig_content, doc_content, opts)
|
|
return function()
|
|
local sig_winnr = api.nvim_open_win(sig_bufnr, false, sig_float_options)
|
|
vim.wo[sig_winnr].conceallevel = 2
|
|
vim.wo[sig_winnr].foldenable = false
|
|
vim.wo[sig_winnr].winblend = opts.winblend
|
|
vim.bo[sig_bufnr].modifiable = false
|
|
vim.bo[sig_bufnr].bufhidden = "hide"
|
|
-- save focus_id
|
|
api.nvim_buf_set_var(bufnr, "lsp_sig_win", sig_winnr)
|
|
return sig_bufnr, sig_winnr, sig_content
|
|
end, function()
|
|
local doc_winnr = api.nvim_open_win(doc_bufnr, false, doc_float_options)
|
|
vim.wo[doc_winnr].conceallevel = 2
|
|
vim.wo[doc_winnr].foldenable = false
|
|
vim.wo[doc_winnr].winblend = opts.winblend
|
|
vim.bo[doc_bufnr].modifiable = false
|
|
vim.bo[doc_bufnr].bufhidden = "hide"
|
|
|
|
-- save focus_id
|
|
api.nvim_buf_set_var(doc_bufnr, "lsp_doc_win", doc_winnr)
|
|
return doc_bufnr, doc_winnr
|
|
end
|
|
end
|
|
|
|
local function close_float_window(winnr)
|
|
if winnr and api.nvim_win_is_valid(winnr) then
|
|
pcall(api.nvim_win_close, winnr, true)
|
|
end
|
|
end
|
|
|
|
local function delete_buffer(bufnr)
|
|
if bufnr and api.nvim_buf_is_valid(bufnr) then
|
|
pcall(api.nvim_buf_delete, bufnr, { force = true })
|
|
end
|
|
end
|
|
|
|
return {
|
|
get_parameter_label = get_parameter_label,
|
|
create_float_windows = create_float_windows,
|
|
close_float_window = close_float_window,
|
|
delete_buffer = delete_buffer,
|
|
}
|