| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240 |
- 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
|