Public Access
better hover
This commit is contained in:
+182
@@ -0,0 +1,182 @@
|
|||||||
|
-- Cycling hover: K shows line diagnostics one at a time, then the LSP hover.
|
||||||
|
-- Repeated K cycles through them; <CR> enters the floating window.
|
||||||
|
local M = {}
|
||||||
|
|
||||||
|
M.win = nil -- floating window handle
|
||||||
|
M.buf = nil -- floating buffer handle
|
||||||
|
M.items = nil -- list of render functions for the current cycle
|
||||||
|
M.idx = 0 -- current position in the cycle
|
||||||
|
M.bufnr = nil -- source buffer the cycle was started from
|
||||||
|
M.lnum = nil -- 0-indexed source line
|
||||||
|
M.col = nil -- 0-indexed source column
|
||||||
|
|
||||||
|
local group = vim.api.nvim_create_augroup("HoverCycle", { clear = true })
|
||||||
|
|
||||||
|
local SEVERITY = { "ERROR", "WARN", "INFO", "HINT" }
|
||||||
|
|
||||||
|
local function float_open()
|
||||||
|
return M.win ~= nil and vim.api.nvim_win_is_valid(M.win)
|
||||||
|
end
|
||||||
|
|
||||||
|
local function close_float()
|
||||||
|
if float_open() then
|
||||||
|
vim.api.nvim_win_close(M.win, true)
|
||||||
|
end
|
||||||
|
M.win = nil
|
||||||
|
M.buf = nil
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Open (or replace) the float with the given lines.
|
||||||
|
local function open_float(lines, ft)
|
||||||
|
close_float()
|
||||||
|
|
||||||
|
local buf = vim.api.nvim_create_buf(false, true)
|
||||||
|
vim.api.nvim_buf_set_lines(buf, 0, -1, false, lines)
|
||||||
|
vim.bo[buf].bufhidden = "wipe"
|
||||||
|
|
||||||
|
local width = 1
|
||||||
|
for _, l in ipairs(lines) do
|
||||||
|
width = math.max(width, vim.fn.strdisplaywidth(l))
|
||||||
|
end
|
||||||
|
width = math.min(width, math.floor(vim.o.columns * 0.8))
|
||||||
|
local height = math.min(math.max(#lines, 1), math.floor(vim.o.lines * 0.6))
|
||||||
|
|
||||||
|
local win = vim.api.nvim_open_win(buf, false, {
|
||||||
|
relative = "cursor",
|
||||||
|
row = 1,
|
||||||
|
col = 0,
|
||||||
|
width = width,
|
||||||
|
height = height,
|
||||||
|
style = "minimal",
|
||||||
|
border = "rounded",
|
||||||
|
focusable = true,
|
||||||
|
})
|
||||||
|
vim.wo[win].wrap = true
|
||||||
|
vim.wo[win].conceallevel = 2
|
||||||
|
vim.wo[win].concealcursor = "nc"
|
||||||
|
|
||||||
|
M.win = win
|
||||||
|
M.buf = buf
|
||||||
|
|
||||||
|
-- Set filetype only now that the buffer is shown in a window, so the
|
||||||
|
-- FileType autocmds (treesitter + render-markdown) render into it.
|
||||||
|
if ft then vim.bo[buf].filetype = ft end
|
||||||
|
vim.bo[buf].modifiable = false
|
||||||
|
|
||||||
|
-- Close when the cursor moves in the source buffer. Switching INTO the
|
||||||
|
-- float (via <CR>) does not move the source cursor, so it survives that.
|
||||||
|
vim.api.nvim_clear_autocmds({ group = group })
|
||||||
|
vim.api.nvim_create_autocmd({ "CursorMoved", "CursorMovedI", "InsertEnter" }, {
|
||||||
|
group = group,
|
||||||
|
buffer = M.bufnr,
|
||||||
|
callback = function() close_float() end,
|
||||||
|
})
|
||||||
|
|
||||||
|
vim.keymap.set("n", "q", close_float, { buffer = buf, nowait = true, silent = true })
|
||||||
|
end
|
||||||
|
|
||||||
|
local function with_header(header, body, ft)
|
||||||
|
local lines = {}
|
||||||
|
if header then
|
||||||
|
lines[1] = header
|
||||||
|
lines[2] = ""
|
||||||
|
end
|
||||||
|
vim.list_extend(lines, body)
|
||||||
|
open_float(lines, ft)
|
||||||
|
end
|
||||||
|
|
||||||
|
local function show_lsp_hover(idx, n)
|
||||||
|
local header = n > 1 and ("Hover [%d/%d]"):format(idx, n) or nil
|
||||||
|
|
||||||
|
local clients = vim.lsp.get_clients({ bufnr = M.bufnr, method = "textDocument/hover" })
|
||||||
|
if #clients == 0 then
|
||||||
|
with_header(header, { "No LSP hover available" })
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
local params = vim.lsp.util.make_position_params(0, clients[1].offset_encoding)
|
||||||
|
vim.lsp.buf_request(M.bufnr, "textDocument/hover", params, function(_, result)
|
||||||
|
if M.idx ~= idx or M.bufnr ~= vim.api.nvim_get_current_buf() then
|
||||||
|
return -- cycle moved on before the response arrived
|
||||||
|
end
|
||||||
|
local body = {}
|
||||||
|
if result and result.contents then
|
||||||
|
body = vim.lsp.util.convert_input_to_markdown_lines(result.contents)
|
||||||
|
end
|
||||||
|
if vim.tbl_isempty(body) then body = { "No hover information" } end
|
||||||
|
|
||||||
|
with_header(header, body, "markdown")
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- True if the diagnostic's range covers the (lnum, col) cursor position.
|
||||||
|
local function under_cursor(d, lnum, col)
|
||||||
|
local end_lnum = d.end_lnum or d.lnum
|
||||||
|
local end_col = d.end_col or (d.col + 1)
|
||||||
|
if lnum < d.lnum or lnum > end_lnum then return false end
|
||||||
|
if lnum == d.lnum and col < d.col then return false end
|
||||||
|
if lnum == end_lnum and col >= math.max(end_col, d.col + 1) then return false end
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Diagnostics whose range is under the cursor, sorted by start column.
|
||||||
|
local function cursor_diags()
|
||||||
|
local diags = vim.tbl_filter(
|
||||||
|
function(d) return under_cursor(d, M.lnum, M.col) end,
|
||||||
|
vim.diagnostic.get(M.bufnr, { lnum = M.lnum }))
|
||||||
|
table.sort(diags, function(a, b) return a.col < b.col end)
|
||||||
|
return diags
|
||||||
|
end
|
||||||
|
|
||||||
|
local function build_items()
|
||||||
|
local items = {}
|
||||||
|
local diags = cursor_diags()
|
||||||
|
|
||||||
|
for _, d in ipairs(diags) do
|
||||||
|
items[#items + 1] = function(idx, n)
|
||||||
|
local sev = SEVERITY[d.severity] or "DIAG"
|
||||||
|
local header = n > 1 and ("Diagnostic [%d/%d] %s"):format(idx, n, sev)
|
||||||
|
or ("Diagnostic %s"):format(sev)
|
||||||
|
with_header(header, vim.split(d.message, "\n", { plain = true }))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
items[#items + 1] = show_lsp_hover
|
||||||
|
return items
|
||||||
|
end
|
||||||
|
|
||||||
|
local function show()
|
||||||
|
M.items[M.idx](M.idx, #M.items)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Mapped to K.
|
||||||
|
function M.hover()
|
||||||
|
if float_open() then
|
||||||
|
M.idx = M.idx % #M.items + 1
|
||||||
|
show()
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
M.bufnr = vim.api.nvim_get_current_buf()
|
||||||
|
local pos = vim.api.nvim_win_get_cursor(0)
|
||||||
|
M.lnum = pos[1] - 1
|
||||||
|
M.col = pos[2]
|
||||||
|
|
||||||
|
-- No diagnostic under cursor -> a single LSP-hover item (no cycle header),
|
||||||
|
-- still rendered in our managed float so <CR> can enter it.
|
||||||
|
M.items = build_items()
|
||||||
|
M.idx = 1
|
||||||
|
show()
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Mapped to <CR>: focus the float if open, otherwise default <CR>.
|
||||||
|
function M.enter()
|
||||||
|
if float_open() then
|
||||||
|
vim.api.nvim_set_current_win(M.win)
|
||||||
|
else
|
||||||
|
vim.api.nvim_feedkeys(
|
||||||
|
vim.api.nvim_replace_termcodes("<CR>", true, false, true), "n", false)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
return M
|
||||||
+4
-9
@@ -10,15 +10,10 @@ for _, mode in ipairs({ "n", "v" }) do
|
|||||||
map(mode, "k", "gk", silent)
|
map(mode, "k", "gk", silent)
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Hover: diagnostics on current line if any, else LSP hover
|
-- Hover: cycle diagnostics on the current line, then LSP hover. <CR> enters.
|
||||||
map("n", "K", function()
|
local hover = require("hover")
|
||||||
local line = vim.api.nvim_win_get_cursor(0)[1] - 1
|
map("n", "K", hover.hover, { desc = "Hover (cycle diagnostics / LSP)" })
|
||||||
if #vim.diagnostic.get(0, { lnum = line }) > 0 then
|
map("n", "<CR>", hover.enter, { desc = "Enter hover window", silent = true })
|
||||||
vim.diagnostic.open_float()
|
|
||||||
else
|
|
||||||
vim.lsp.buf.hover()
|
|
||||||
end
|
|
||||||
end, { desc = "Hover (diagnostic or LSP)" })
|
|
||||||
|
|
||||||
-- LSP
|
-- LSP
|
||||||
map("n", "<leader>gf",
|
map("n", "<leader>gf",
|
||||||
|
|||||||
Reference in New Issue
Block a user