ソースを参照

feat(neovim): improve diagnostics with `diagnostic-dock`

This module was pretty much entirely vibe-coded. Crazy!
Joe 2 ヶ月 前
コミット
5f12c10097

+ 8 - 2
.config/nvim/custom/bit-browser/lua/bit-browser/init.lua

@@ -105,9 +105,15 @@ end
 
 local function add_item() end
 
-local function next_item() target[3]() end
+local function next_item()
+    target[3]()
+    vim.api.nvim_feedkeys("zz", "n", false)
+end
 
-local function prev_item() target[4]() end
+local function prev_item()
+    target[4]()
+    vim.api.nvim_feedkeys("zz", "n", false)
+end
 
 local function get_target() return target[1] end
 

+ 240 - 0
.config/nvim/custom/diagnostic-dock/lua/diagnostic-dock/init.lua

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

+ 15 - 40
.config/nvim/init.lua

@@ -45,6 +45,8 @@ vim.opt.expandtab = true
 vim.opt.tabstop = 4
 vim.opt.shiftwidth = 4
 
+vim.opt.updatetime = 75
+
 -- === === === === === === === === === === === === === === === === === === ===
 --
 -- Leader
@@ -114,23 +116,10 @@ vim.api.nvim_create_autocmd("LspAttach", {
         vim.keymap.set("n", "Sx", vim.lsp.buf.code_action)
         vim.keymap.set("n", "<Space>", vim.lsp.buf.hover)
         vim.keymap.set("n", "SI", function()
-            local diagnostic_config = vim.diagnostic.config()
             if (vim.lsp.inlay_hint.is_enabled()) then
                 vim.lsp.inlay_hint.enable(false)
-                vim.diagnostic.config({
-                    virtual_lines = {
-                        current_line = true,
-                        ---@diagnostic disable-next-line: need-check-nil, undefined-field
-                        format = diagnostic_config.format
-                    }
-                })
             else
                 vim.lsp.inlay_hint.enable(true)
-                vim.diagnostic.config({
-                    virtual_lines = true,
-                    ---@diagnostic disable-next-line: need-check-nil, undefined-field
-                    format = diagnostic_config.format
-                })
             end
         end)
     end
@@ -171,6 +160,7 @@ for _, plugin in ipairs({
     "save-formatter",
     "status-beast",
     "telescope-shroud",
+    "diagnostic-dock",
 }) do
     vim.opt.runtimepath:prepend(vim.fn.stdpath("config") ..
         "/custom/" .. plugin)
@@ -575,34 +565,17 @@ end
 local function config_status_beast()
     require("status-beast.highlights").setup()
     require("status-beast").setup()
-    vim.api.nvim_create_autocmd({ "BufEnter" },
-        {
-            group = vim.api.nvim_create_augroup('StatusBeast',
-                { clear = true }),
-            callback = function(args)
-                if vim.bo[args.buf].filetype == "aerial" then
-                    return
-                end
-                require("status-beast").setup()
-            end
-        })
-    vim.diagnostic.config({
-        virtual_lines = {
-            current_line = true,
-            format = function(d)
-                return "[" .. d.source .. "] " .. d.message
-            end
-        },
-        float = false,
-        virtual_text = false,
-        update_in_insert = true
+    vim.api.nvim_create_autocmd({ "BufEnter" }, {
+        group = vim.api.nvim_create_augroup('StatusBeast', { clear = true }),
+        callback = function()
+            require("status-beast").setup()
+        end
     })
-    local palette = require("command-palette")
-    palette.add({
-        {
-            "Status Beast", "Options for status bars and columns", { {
-            "Toggle Relnum", "Toggle relative line numbers on/off",
-            ":setl rnu!" } } }
+
+    require("command-palette").add({
+        { "Status Beast", "Options for status bars",
+            { { "Toggle Relnum", "Toggle relative line numbers", ":setl rnu!" } }
+        }
     })
 end
 
@@ -922,6 +895,7 @@ local function config_oil()
     vim.keymap.set("n", "<Leader>f",
         function() require("oil").open_float(nil, { preview = {} }) end)
 end
+
 local function config_diagnostic_dock()
     require("diagnostic-dock").setup()
 end
@@ -947,6 +921,7 @@ local setup_plugins = function()
     config_telescope_shroud()
     config_proj_conf()
     config_status_beast()
+    config_diagnostic_dock()
     config_oil()
 end