|
--- Slightly *fancier* LSP hover handler. |
|
local lsp_hover = {}; |
|
|
|
---@class hover.opts |
|
--- |
|
---@field border_hl? string Highlight group for the window borders. |
|
---@field name_hl? string Highlight group for the `name`. Defaults to `border_hl`. |
|
---@field name string |
|
--- |
|
---@field min_width? integer |
|
---@field max_width? integer |
|
--- |
|
---@field min_height? integer |
|
---@field max_height? integer |
|
|
|
|
|
--- Configuration for lsp_hovers from different |
|
--- servers. |
|
--- |
|
---@type { default: hover.opts, [string]: hover.opts } |
|
lsp_hover.config = { |
|
default = { |
|
border_hl = "@comment", |
|
name = " LSP/Hover", |
|
|
|
min_width = 20, |
|
max_width = math.floor(vim.o.columns * 0.75), |
|
|
|
min_height = 1, |
|
max_height = math.floor(vim.o.lines * 0.5) |
|
}, |
|
|
|
["^lua_ls"] = { |
|
name = " LuaLS", |
|
border_hl = "@function" |
|
} |
|
}; |
|
|
|
--- Finds matching configuration. |
|
--- NOTE: The output is the merge of the {config} and {default}. |
|
---@param str string |
|
---@return hover.opts |
|
local match = function (str) |
|
---+${lua} |
|
|
|
local ignore = { "default" }; |
|
local config = lsp_hover.config.default or {}; |
|
|
|
---@type string[] |
|
local keys = vim.tbl_keys(lsp_hover.config); |
|
|
|
--- Sorting is nice in-case the same pattern can |
|
--- match multiple servers. |
|
table.sort(keys); |
|
|
|
for _, k in ipairs(keys) do |
|
if vim.list_contains(ignore, k) == false and string.match(str, k) then |
|
return vim.tbl_extend("force", config, lsp_hover.config[k]); |
|
end |
|
end |
|
|
|
return config; |
|
|
|
---_ |
|
end |
|
|
|
--- Get which quadrant to open the window on. |
|
--- |
|
--- ```txt |
|
--- top, left ↑ top, right |
|
--- ← █ → |
|
--- bottom, left ↓ bottom, right |
|
--- ``` |
|
---@param w integer |
|
---@param h integer |
|
---@return [ "left" | "right" | "center", "top" | "bottom" | "center" ] |
|
local function get_quadrant (w, h) |
|
---+${lua} |
|
|
|
---@type integer |
|
local window = vim.api.nvim_get_current_win(); |
|
---@type [ integer, integer ] |
|
local src_c = vim.api.nvim_win_get_cursor(window); |
|
|
|
--- (Terminal) Screen position. |
|
---@class screen.pos |
|
--- |
|
---@field row integer Screen row. |
|
---@field col integer First screen column. |
|
---@field endcol integer Last screen column. |
|
--- |
|
---@field curscol integer Cursor screen column. |
|
local scr_p = vim.fn.screenpos(window, src_c[1], src_c[2]); |
|
|
|
---@type integer, integer Vim's width & height. |
|
local vW, vH = vim.o.columns, vim.o.lines - (vim.o.cmdheight or 0); |
|
---@type "left" | "right", "top" | "bottom" |
|
local x, y; |
|
|
|
if scr_p.curscol - w <= 0 then |
|
--- Not enough spaces on `left`. |
|
if scr_p.curscol + w >= vW then |
|
--- Not enough space on `right`. |
|
return { "center", "center" }; |
|
else |
|
--- Enough spaces on `right`. |
|
x = "right"; |
|
end |
|
else |
|
--- Enough space on `left`. |
|
x = "left"; |
|
end |
|
|
|
if scr_p.row + h >= vH then |
|
--- Not enough spaces on `top`. |
|
if scr_p.row - h <= 0 then |
|
--- Not enough spaces on `bottom`. |
|
return { "center", "center" }; |
|
else |
|
y = "top"; |
|
end |
|
else |
|
y = "bottom"; |
|
end |
|
|
|
return { x, y } |
|
---_ |
|
end |
|
|
|
---@type integer? LSP hover buffer. |
|
lsp_hover.buffer = nil; |
|
---@type integer? LSP hover window. |
|
lsp_hover.window = nil; |
|
|
|
--- Initializes the hover buffer & window. |
|
---@param config table |
|
lsp_hover.__init = function (config) |
|
---+${lua} |
|
|
|
if not config then |
|
return; |
|
end |
|
|
|
if not lsp_hover.buffer or vim.api.nvim_buf_is_valid(lsp_hover.buffer) then |
|
pcall(vim.api.nvim_buf_delete, lsp_hover.buffer, { force = true }); |
|
lsp_hover.buffer = vim.api.nvim_create_buf(false, true); |
|
|
|
vim.api.nvim_buf_set_keymap(lsp_hover.buffer, "n", "q", "", { |
|
desc = "Closes LSP hover window", |
|
callback = function () |
|
pcall(vim.api.nvim_win_close, lsp_hover.window, true); |
|
lsp_hover.window = nil; |
|
end |
|
}); |
|
end |
|
|
|
if not lsp_hover.window then |
|
lsp_hover.window = vim.api.nvim_open_win(lsp_hover.buffer, false, config); |
|
elseif vim.api.nvim_win_is_valid(lsp_hover.window) == false then |
|
pcall(vim.api.nvim_win_close, lsp_hover.window, true); |
|
lsp_hover.window = vim.api.nvim_open_win(lsp_hover.buffer, false, config); |
|
else |
|
vim.api.nvim_win_set_config(lsp_hover.window, config); |
|
end |
|
|
|
---_ |
|
end |
|
|
|
--- Custom hover function. |
|
---@param error table Error. |
|
---@param result table Result of the hover. |
|
---@param context table Context for this hover. |
|
---@param _ table Hover config(we won't use this). |
|
lsp_hover.hover = function (error, result, context, _) |
|
---+${lua} |
|
|
|
if error then |
|
--- Emit error message. |
|
vim.api.nvim_echo({ |
|
{ " Lsp hover: ", "DiagnosticVirtualTextError" }, |
|
{ " " }, |
|
{ error.message, "Comment" } |
|
}, true, {}) |
|
end |
|
|
|
if vim.api.nvim_get_current_buf() ~= context.bufnr then |
|
--- Buffer was changed before the request was |
|
--- resolved. |
|
return; |
|
elseif not result or not result.contents then |
|
--- No result. |
|
vim.api.nvim_echo({ |
|
{ " Lsp hover: ", "DiagnosticVirtualTextInfo" }, |
|
{ " " }, |
|
{ "No information available!", "Comment" } |
|
}, true, {}) |
|
return; |
|
end |
|
|
|
---@type string | table |
|
local content = result.contents; |
|
|
|
---@type string[] |
|
local lines = {}; |
|
local ft; |
|
|
|
--[[ |
|
NOTE: LSP hover contents can be any of the followings, |
|
|
|
1. Literal string. |
|
2. A table(`{ kind = ..., value = ... }`). |
|
3. A list(`{ kind = ..., value = ... }[]`). |
|
]] |
|
if type(content) == "string" then |
|
lines = vim.split(content or "", "\n", { trimempty = true }); |
|
ft = "markdown"; |
|
elseif vim.islist(content) then |
|
content = content[1]; |
|
|
|
lines = vim.split(content.value or "", "\n", { trimempty = true }); |
|
ft = content.kind; |
|
else |
|
lines = vim.split(content.value or "", "\n", { trimempty = true }); |
|
ft = content.kind; |
|
end |
|
|
|
---@type integer LSP client ID. |
|
local client_id = context.client_id; |
|
---@type { name: string } LSP client info. |
|
local client = vim.lsp.get_client_by_id(client_id) or { name = "Unknown" }; |
|
|
|
---@type hover.opts |
|
local config = match(client.name); |
|
|
|
local w = config.min_width or 20; |
|
local h = config.min_height or 1; |
|
|
|
local max_height = config.max_height or 10; |
|
local max_width = config.max_width or 60; |
|
|
|
for _, line in ipairs(lines) do |
|
if vim.fn.strdisplaywidth(line) >= max_width then |
|
w = max_width; |
|
break; |
|
elseif vim.fn.strdisplaywidth(line) > w then |
|
w = vim.fn.strdisplaywidth(line); |
|
end |
|
end |
|
|
|
h = math.max(math.min(#lines, max_height), h); |
|
|
|
--- Window configuration. |
|
local win_conf = { |
|
relative = "cursor", |
|
|
|
row = 1, col = 0, |
|
width = w, height = h, |
|
|
|
style = "minimal", |
|
|
|
footer = { |
|
{ "╼ ", config.border_hl or "FloatBorder" }, |
|
{ config.name, config.name_hl or config.border_hl or "FloatBorder" }, |
|
{ " ╾", config.border_hl or "FloatBorder" }, |
|
}, |
|
footer_pos = "right" |
|
}; |
|
|
|
--- Window borders. |
|
local border = { |
|
{ "╭", config.border_hl or "FloatBorder" }, |
|
{ "─", config.border_hl or "FloatBorder" }, |
|
{ "╮", config.border_hl or "FloatBorder" }, |
|
|
|
{ "│", config.border_hl or "FloatBorder" }, |
|
{ "╯", config.border_hl or "FloatBorder" }, |
|
{ "─", config.border_hl or "FloatBorder" }, |
|
|
|
{ "╰", config.border_hl or "FloatBorder" }, |
|
{ "│", config.border_hl or "FloatBorder" }, |
|
}; |
|
|
|
--- Which quadrant to open the window on. |
|
---@type [ "left" | "right" | "center", "top" | "bottom" | "center" ] |
|
local quad = get_quadrant(w + 2, h + 2); |
|
|
|
if quad[1] == "left" then |
|
win_conf.col = (w * -1) - 1; |
|
elseif quad[1] == "right" then |
|
win_conf.col = 0; |
|
else |
|
win_conf.relative = "editor"; |
|
win_conf.col = math.ceil((vim.o.columns - w) / 2); |
|
end |
|
|
|
if quad[2] == "top" then |
|
win_conf.row = (h * -1) - 2; |
|
|
|
if quad[1] == "left" then |
|
border[5][1] = "┤"; |
|
else |
|
border[7][1] = "├"; |
|
end |
|
elseif quad[2] == "bottom" then |
|
win_conf.row = 1; |
|
|
|
if quad[1] == "left" then |
|
border[3][1] = "┤"; |
|
else |
|
border[1][1] = "├"; |
|
end |
|
else |
|
win_conf.relative = "editor"; |
|
win_conf.row = math.ceil((vim.o.lines - h) / 2); |
|
end |
|
|
|
win_conf.border = border; |
|
lsp_hover.__init(win_conf); |
|
|
|
vim.api.nvim_buf_set_lines(lsp_hover.buffer, 0, -1, false, lines); |
|
|
|
vim.bo[lsp_hover.buffer].ft = ft; |
|
|
|
vim.wo[lsp_hover.window].conceallevel = 3; |
|
vim.wo[lsp_hover.window].concealcursor = "n"; |
|
vim.wo[lsp_hover.window].signcolumn = "no"; |
|
|
|
vim.wo[lsp_hover.window].wrap = true; |
|
vim.wo[lsp_hover.window].linebreak = true; |
|
|
|
if package.loaded["markview"] and package.loaded["markview"].render then |
|
--- If markview is available use it to render stuff. |
|
--- This is for `v25`. |
|
require("markview").render(lsp_hover.buffer, { enable = true, hybrid_mode = false }); |
|
end |
|
|
|
---_ |
|
end |
|
|
|
--- Setup function. |
|
---@param config { default: hover.opts, [string]: hover.opts } | nil |
|
lsp_hover.setup = function (config) |
|
---+${lua} |
|
|
|
if config then |
|
lsp_hover.config = vim.tbl_deep_extend("force", lsp_hover.config, config); |
|
end |
|
|
|
if vim.fn.has("nvim-0.11") == 1 then |
|
vim.api.nvim_create_autocmd("LspAttach", { |
|
callback = function (ev) |
|
vim.api.nvim_buf_set_keymap(ev.buf, "n", "K", "", { |
|
callback = function () |
|
local window = vim.api.nvim_get_current_win(); |
|
|
|
if lsp_hover.window and vim.api.nvim_win_is_valid(lsp_hover.window) then |
|
vim.api.nvim_set_current_win(lsp_hover.window); |
|
else |
|
vim.lsp.buf_request(0, 'textDocument/hover', vim.lsp.util.make_position_params(window, "utf-8"), lsp_hover.hover); |
|
end |
|
end |
|
}); |
|
end |
|
}); |
|
end |
|
|
|
--- TODO, maybe we should remove this. |
|
--- Set-up the new provider. |
|
vim.lsp.handlers["textDocument/hover"] = lsp_hover.hover; |
|
|
|
vim.api.nvim_create_autocmd({ "CursorMoved", "CursorMovedI" }, { |
|
callback = function (event) |
|
if event.buf == lsp_hover.buffer then |
|
--- Don't do anything if the current buffer |
|
--- is the hover buffer. |
|
return; |
|
elseif lsp_hover.window and vim.api.nvim_win_is_valid(lsp_hover.window) then |
|
pcall(vim.api.nvim_win_close, lsp_hover.window, true); |
|
lsp_hover.window = nil; |
|
end |
|
end |
|
}); |
|
|
|
---_ |
|
end |
|
|
|
return lsp_hover; |
Would you happen to know why it might display blank when using
vtsls? It shows up nicely when not using the fancy hover. And similar forjsonlsmaybe others too. It works forgoplsandrust-analyzernicely.