|
@@ -0,0 +1,240 @@
|
|
|
|
|
+local M = {
|
|
|
|
|
+ state = {
|
|
|
|
|
+ buf = nil,
|
|
|
|
|
+ win = nil,
|
|
|
|
|
+ parent_win = nil,
|
|
|
|
|
+ last_signature = nil,
|
|
|
|
|
+ cache = { row = -1, height = -1 }
|
|
|
|
|
+ },
|
|
|
|
|
+ config = {
|
|
|
|
|
+ borders = { "⌌", "-", "⌍", "", "", "", "", "" },
|
|
|
|
|
+ icons = pcall(require, "status-beast.maps") and
|
|
|
|
|
+ require("status-beast.maps").diagnostic_icons or {},
|
|
|
|
|
+ severities = { "Error", "Warn", "Info", "Hint" },
|
|
|
|
|
+ ns = vim.api.nvim_create_namespace("DiagnosticDock"),
|
|
|
|
|
+ max_height = 20,
|
|
|
|
|
+ min_height = 1,
|
|
|
|
|
+ compact_height = 3,
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+local function get_diagnostics(bufnr, winid)
|
|
|
|
|
+ if winid == vim.api.nvim_get_current_win() then
|
|
|
|
|
+ local mode = vim.api.nvim_get_mode().mode
|
|
|
|
|
+ if mode:match("[vV\22]") then
|
|
|
|
|
+ local v_line = vim.fn.line("v") - 1
|
|
|
|
|
+ local cur_line = vim.fn.line(".") - 1
|
|
|
|
|
+ local start_l = math.min(v_line, cur_line)
|
|
|
|
|
+ local end_l = math.max(v_line, cur_line)
|
|
|
|
|
+
|
|
|
|
|
+ local all = vim.diagnostic.get(bufnr)
|
|
|
|
|
+ local filtered = {}
|
|
|
|
|
+ for _, d in ipairs(all) do
|
|
|
|
|
+ if d.lnum >= start_l and d.lnum <= end_l then
|
|
|
|
|
+ table.insert(filtered, d)
|
|
|
|
|
+ end
|
|
|
|
|
+ end
|
|
|
|
|
+ return filtered
|
|
|
|
|
+ end
|
|
|
|
|
+ end
|
|
|
|
|
+
|
|
|
|
|
+ local cursor = vim.api.nvim_win_get_cursor(winid)
|
|
|
|
|
+ return vim.diagnostic.get(bufnr, { lnum = cursor[1] - 1 })
|
|
|
|
|
+end
|
|
|
|
|
+
|
|
|
|
|
+local function ensure_dock_buffer()
|
|
|
|
|
+ if M.state.buf and vim.api.nvim_buf_is_valid(M.state.buf) then
|
|
|
|
|
+ return M.state.buf
|
|
|
|
|
+ end
|
|
|
|
|
+
|
|
|
|
|
+ local buf = vim.api.nvim_create_buf(false, true)
|
|
|
|
|
+ vim.bo[buf].filetype = "diagnostic-dock"
|
|
|
|
|
+ vim.bo[buf].buftype = "nofile"
|
|
|
|
|
+ vim.bo[buf].swapfile = false
|
|
|
|
|
+ vim.bo[buf].bufhidden = "hide"
|
|
|
|
|
+
|
|
|
|
|
+ local close_dock = function()
|
|
|
|
|
+ M.close()
|
|
|
|
|
+ if M.state.parent_win and vim.api.nvim_win_is_valid(M.state.parent_win) then
|
|
|
|
|
+ vim.api.nvim_set_current_win(M.state.parent_win)
|
|
|
|
|
+ end
|
|
|
|
|
+ end
|
|
|
|
|
+
|
|
|
|
|
+ vim.keymap.set("n", "q", close_dock, { buffer = buf, nowait = true })
|
|
|
|
|
+ vim.keymap.set("n", "<Esc>", close_dock, { buffer = buf, nowait = true })
|
|
|
|
|
+
|
|
|
|
|
+ M.state.buf = buf
|
|
|
|
|
+ M.state.last_signature = nil
|
|
|
|
|
+ return buf
|
|
|
|
|
+end
|
|
|
|
|
+
|
|
|
|
|
+local function render(diags)
|
|
|
|
|
+ local buf = ensure_dock_buffer()
|
|
|
|
|
+ local sig = vim.inspect(diags)
|
|
|
|
|
+
|
|
|
|
|
+ if M.state.last_signature == sig then
|
|
|
|
|
+ return buf, vim.api.nvim_buf_line_count(buf)
|
|
|
|
|
+ end
|
|
|
|
|
+
|
|
|
|
|
+ M.state.last_signature = sig
|
|
|
|
|
+ local lines = {}
|
|
|
|
|
+ local highlights = {}
|
|
|
|
|
+
|
|
|
|
|
+ for _, d in ipairs(diags) do
|
|
|
|
|
+ local sev = M.config.severities[d.severity] or "Info"
|
|
|
|
|
+ local icon = M.config.icons[sev] or "●"
|
|
|
|
|
+ local msg = vim.split(d.message, "\n")
|
|
|
|
|
+ local src = d.source and (" " .. d.source) or ""
|
|
|
|
|
+
|
|
|
|
|
+ local header = string.format(" %s %s%s", icon, msg[1], src)
|
|
|
|
|
+ table.insert(lines, header)
|
|
|
|
|
+
|
|
|
|
|
+ table.insert(highlights, {
|
|
|
|
|
+ group = "StatusBeastDiagnostic" .. sev,
|
|
|
|
|
+ line = #lines - 1,
|
|
|
|
|
+ col_start = 1,
|
|
|
|
|
+ col_end = 4
|
|
|
|
|
+ })
|
|
|
|
|
+
|
|
|
|
|
+ if d.source then
|
|
|
|
|
+ local s_start = header:find(d.source, 1, true)
|
|
|
|
|
+ if s_start then
|
|
|
|
|
+ table.insert(highlights, {
|
|
|
|
|
+ group = "Comment",
|
|
|
|
|
+ line = #lines - 1,
|
|
|
|
|
+ col_start = s_start - 1,
|
|
|
|
|
+ col_end = -1
|
|
|
|
|
+ })
|
|
|
|
|
+ end
|
|
|
|
|
+ end
|
|
|
|
|
+
|
|
|
|
|
+ for i = 2, #msg do
|
|
|
|
|
+ table.insert(lines, " " .. msg[i])
|
|
|
|
|
+ end
|
|
|
|
|
+ end
|
|
|
|
|
+
|
|
|
|
|
+ table.insert(lines, "")
|
|
|
|
|
+
|
|
|
|
|
+ vim.api.nvim_buf_set_lines(buf, 0, -1, false, lines)
|
|
|
|
|
+ vim.api.nvim_buf_clear_namespace(buf, M.config.ns, 0, -1)
|
|
|
|
|
+
|
|
|
|
|
+ for _, hl in ipairs(highlights) do
|
|
|
|
|
+ vim.api.nvim_buf_add_highlight(buf, M.config.ns, hl.group, hl.line,
|
|
|
|
|
+ hl.col_start, hl.col_end)
|
|
|
|
|
+ end
|
|
|
|
|
+
|
|
|
|
|
+ return buf, #lines
|
|
|
|
|
+end
|
|
|
|
|
+
|
|
|
|
|
+local function ensure_view_clearance(win, dock_height)
|
|
|
|
|
+ local win_h = vim.api.nvim_win_get_height(win)
|
|
|
|
|
+ local visible_h = win_h - dock_height
|
|
|
|
|
+ local cursor_row = vim.fn.winline()
|
|
|
|
|
+
|
|
|
|
|
+ if cursor_row > visible_h then
|
|
|
|
|
+ local view = vim.api.nvim_win_call(win, vim.fn.winsaveview)
|
|
|
|
|
+ view.topline = view.topline + (cursor_row - visible_h)
|
|
|
|
|
+ vim.api.nvim_win_call(win, function() vim.fn.winrestview(view) end)
|
|
|
|
|
+ end
|
|
|
|
|
+end
|
|
|
|
|
+
|
|
|
|
|
+function M.close()
|
|
|
|
|
+ if M.state.win and vim.api.nvim_win_is_valid(M.state.win) then
|
|
|
|
|
+ vim.api.nvim_win_close(M.state.win, true)
|
|
|
|
|
+ end
|
|
|
|
|
+ M.state.win = nil
|
|
|
|
|
+ M.state.cache = { row = -1, height = -1 }
|
|
|
|
|
+end
|
|
|
|
|
+
|
|
|
|
|
+function M.update_layout(target_win, content_len, focused)
|
|
|
|
|
+ local win_w = vim.api.nvim_win_get_width(target_win)
|
|
|
|
|
+ local win_h = vim.api.nvim_win_get_height(target_win)
|
|
|
|
|
+
|
|
|
|
|
+ local height
|
|
|
|
|
+ if focused then
|
|
|
|
|
+ local limit = math.min(M.config.max_height, math.floor(win_h / 2))
|
|
|
|
|
+ height = math.max(M.config.min_height, math.min(content_len, limit))
|
|
|
|
|
+ else
|
|
|
|
|
+ height = math.min(content_len, M.config.compact_height)
|
|
|
|
|
+ end
|
|
|
|
|
+
|
|
|
|
|
+ local border_h = (M.config.borders[2] ~= "" and 1 or 0) +
|
|
|
|
|
+ (M.config.borders[6] ~= "" and 1 or 0)
|
|
|
|
|
+ local total_h = height + border_h
|
|
|
|
|
+ local row = win_h - total_h
|
|
|
|
|
+
|
|
|
|
|
+ local opts = {
|
|
|
|
|
+ relative = "win",
|
|
|
|
|
+ win = target_win,
|
|
|
|
|
+ row = row,
|
|
|
|
|
+ col = 0,
|
|
|
|
|
+ width = win_w,
|
|
|
|
|
+ height = height,
|
|
|
|
|
+ style = "minimal",
|
|
|
|
|
+ border = M.config.borders,
|
|
|
|
|
+ zindex = 50
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if M.state.win and vim.api.nvim_win_is_valid(M.state.win) then
|
|
|
|
|
+ if M.state.cache.row ~= row or M.state.cache.height ~= height then
|
|
|
|
|
+ vim.api.nvim_win_set_config(M.state.win, opts)
|
|
|
|
|
+ M.state.cache = { row = row, height = height }
|
|
|
|
|
+ end
|
|
|
|
|
+ else
|
|
|
|
|
+ M.state.win = vim.api.nvim_open_win(M.state.buf, false, opts)
|
|
|
|
|
+ vim.wo[M.state.win].winhl = "Normal:Normal,FloatBorder:WinSeparator"
|
|
|
|
|
+ M.state.cache = { row = row, height = height }
|
|
|
|
|
+ end
|
|
|
|
|
+
|
|
|
|
|
+ vim.wo[M.state.win].wrap = focused
|
|
|
|
|
+
|
|
|
|
|
+ if not focused then
|
|
|
|
|
+ ensure_view_clearance(target_win, total_h)
|
|
|
|
|
+ end
|
|
|
|
|
+end
|
|
|
|
|
+
|
|
|
|
|
+function M.open()
|
|
|
|
|
+ local curr_win = vim.api.nvim_get_current_win()
|
|
|
|
|
+ local is_dock_focused = (M.state.win and curr_win == M.state.win)
|
|
|
|
|
+
|
|
|
|
|
+ if is_dock_focused then
|
|
|
|
|
+ if M.state.buf and vim.api.nvim_buf_is_valid(M.state.buf) then
|
|
|
|
|
+ M.update_layout(M.state.parent_win,
|
|
|
|
|
+ vim.api.nvim_buf_line_count(M.state.buf), true)
|
|
|
|
|
+ end
|
|
|
|
|
+ return
|
|
|
|
|
+ end
|
|
|
|
|
+
|
|
|
|
|
+ M.state.parent_win = curr_win
|
|
|
|
|
+ local target_buf = vim.api.nvim_win_get_buf(curr_win)
|
|
|
|
|
+
|
|
|
|
|
+ if vim.bo[target_buf].filetype == "diagnostic-dock" or vim.bo[target_buf].filetype == "butterfly" then
|
|
|
|
|
+ return
|
|
|
|
|
+ end
|
|
|
|
|
+
|
|
|
|
|
+ local diags = get_diagnostics(target_buf, curr_win)
|
|
|
|
|
+ if #diags == 0 then return M.close() end
|
|
|
|
|
+
|
|
|
|
|
+ local _, content_len = render(diags)
|
|
|
|
|
+ M.update_layout(curr_win, content_len, false)
|
|
|
|
|
+
|
|
|
|
|
+ if not vim.b[target_buf].diagnostic_dock_mapped then
|
|
|
|
|
+ vim.keymap.set({ "n", "x" }, "<C-w>", function()
|
|
|
|
|
+ if M.state.win and vim.api.nvim_win_is_valid(M.state.win) then
|
|
|
|
|
+ vim.api.nvim_set_current_win(M.state.win)
|
|
|
|
|
+ end
|
|
|
|
|
+ end, { buffer = target_buf, nowait = true })
|
|
|
|
|
+ vim.b[target_buf].diagnostic_dock_mapped = true
|
|
|
|
|
+ end
|
|
|
|
|
+end
|
|
|
|
|
+
|
|
|
|
|
+function M.setup()
|
|
|
|
|
+ local group = vim.api.nvim_create_augroup("DiagnosticDock", { clear = true })
|
|
|
|
|
+ vim.api.nvim_create_autocmd(
|
|
|
|
|
+ { "CursorHold", "ModeChanged", "BufEnter", "WinEnter" }, {
|
|
|
|
|
+ group = group,
|
|
|
|
|
+ callback = function() vim.schedule(M.open) end
|
|
|
|
|
+ })
|
|
|
|
|
+end
|
|
|
|
|
+
|
|
|
|
|
+return M
|