From f242ace8a6cf0de0d78767ec325cc922c789f005 Mon Sep 17 00:00:00 2001 From: Andrew Haynes Date: Fri, 2 Oct 2026 23:59:54 -0400 Subject: [PATCH] REFACTOR: Changed status bar significantly to improve performance and be a little smaller --- nvim/.config/nvim/lua/DarthMooMancer/ui.lua | 421 ++++++++++---------- 1 file changed, 214 insertions(+), 207 deletions(-) diff --git a/nvim/.config/nvim/lua/DarthMooMancer/ui.lua b/nvim/.config/nvim/lua/DarthMooMancer/ui.lua index 5e5a7a7..66a2cc4 100644 --- a/nvim/.config/nvim/lua/DarthMooMancer/ui.lua +++ b/nvim/.config/nvim/lua/DarthMooMancer/ui.lua @@ -30,8 +30,14 @@ local function setup_statusline_highlights() derive_mode_hl('StatusModeReplace', 'WarningMsg') derive_mode_hl('StatusModeTerminal', 'Special') derive_hl('StatusGit', { base = 'CursorLine', fg = 'LineNr' }) - derive_hl('StatusLsp', { base = 'StatusLine', fg = 'DiagnosticInfo' }) - derive_hl('StatusLspSpinner', { base = 'StatusLine', fg = 'Constant' }) + -- derive_hl('StatusLsp', { base = 'StatusLine', fg = 'DiagnosticInfo' }) + -- derive_hl('StatusLspSpinner', { base = 'StatusLine', fg = 'Constant' }) + derive_hl( + 'StatusLspWorking', + { base = 'StatusLine', fg = 'DiagnosticInfo' } + ) + derive_hl('StatusLspReady', { base = 'StatusLine', fg = 'Constant' }) + derive_hl('StatusLspNone', { base = 'StatusLine', fg = 'NonText' }) derive_hl( 'StatusDiagError', { base = 'StatusLine', fg = 'DiagnosticError' } @@ -39,7 +45,6 @@ local function setup_statusline_highlights() derive_hl('StatusDiagWarn', { base = 'StatusLine', fg = 'DiagnosticWarn' }) derive_hl('StatusDiagHint', { base = 'StatusLine', fg = 'DiagnosticHint' }) derive_hl('StatusCursor', { base = 'CursorLine', fg = 'Normal' }) - -- derive_hl('StatusGitAdded', { base = 'StatusLine', fg = 'OkMsg' }) derive_hl('StatusGitAdded', { base = 'StatusLine', fg = 'MoreMsg' }) derive_hl( 'StatusGitRemoved', @@ -54,183 +59,142 @@ setup_statusline_highlights() --- Mode text mapping. local mode_map = { - ['n'] = ' NORM ', - ['i'] = ' INS ', - ['v'] = ' VIS ', - ['V'] = ' V-LINE ', - [''] = ' V-BLK ', - ['R'] = ' REPL ', - ['c'] = ' CMD ', - ['t'] = ' TERM ', + n = ' NORM ', + i = ' INS ', + v = ' VIS ', + V = ' V-LINE ', + ['^V'] = ' V-BLK ', + s = ' VIS ', + S = ' V-LINE ', + ['^S'] = ' V-BLK ', + R = ' REPL ', + c = ' CMD ', + r = ' PROMPT ', + t = ' TERM ', + ['!'] = ' SHELL ', } local mode_hl_map = { - ['n'] = 'StatusModeNormal', - ['i'] = 'StatusModeInsert', - ['v'] = 'StatusModeVisual', - ['V'] = 'StatusModeVisual', - [''] = 'StatusModeVisual', - ['R'] = 'StatusModeReplace', - ['c'] = 'StatusModeCommand', - ['t'] = 'StatusModeTerminal', + n = 'StatusModeNormal', + i = 'StatusModeInsert', + v = 'StatusModeVisual', + V = 'StatusModeVisual', + ['^V'] = 'StatusModeVisual', + s = 'StatusModeVisual', + S = 'StatusModeVisual', + ['^S'] = 'StatusModeVisual', + R = 'StatusModeReplace', + c = 'StatusModeCommand', + r = 'StatusModeCommand', + ['!'] = 'StatusModeCommand', + t = 'StatusModeTerminal', } --- Git branch (async, cached per buffer, refreshed every 5s) -local function update_git_branch() - if vim.bo.buftype ~= '' then - return +local function refresh_status(buf) + local b = vim.b[buf] + local left, right = {}, {} + + if b.git_branch and b.git_branch ~= '' then + left[#left + 1] = '%#StatusGit# ' .. b.git_branch .. ' %*' end + local diff = b.git_diff + if diff then + if diff.added > 0 then + left[#left + 1] = '%#StatusGitAdded# +' .. diff.added .. ' %*' + end + if diff.removed > 0 then + left[#left + 1] = '%#StatusGitRemoved# -' .. diff.removed .. ' %*' + end + end + if b.lsp_icon then + left[#left + 1] = '%#' .. b.lsp_hl .. '#' .. b.lsp_icon .. '%*' + end + if b.recording then + right[#right + 1] = '%#StatusRecording# ' .. b.recording .. ' %*' + end + if b.diag_str then + right[#right + 1] = b.diag_str + end + + b.status_left = #left > 0 and table.concat(left) or '' + b.status_right = #right > 0 and table.concat(right) or '' +end + +--- LSP: one icon, three states. +local lsp_icon = { + none = ' ', + working = ' ', + ready = ' ', +} +local lsp_hl = { + none = 'StatusLspNone', + working = 'StatusLspWorking', + ready = 'StatusLspReady', +} +local active_progress = {} + +local function refresh_lsp(buf) + local state = 'ready' + local clients = vim.lsp.get_clients({ bufnr = buf }) + if #clients == 0 then + state = 'none' + else + for _, client in ipairs(clients) do + if active_progress[client.id] then + state = 'working' + break + end + end + vim.b[buf].lsp_icon = lsp_icon[state] + vim.b[buf].lsp_hl = lsp_hl[state] + refresh_status(buf) + end +end + +vim.api.nvim_create_autocmd({ 'LspAttach', 'LspDetach' }, { + callback = function(ev) + refresh_lsp(ev.buf) + end, +}) + +vim.api.nvim_create_autocmd('LspProgress', { + callback = function(ev) + if ev.match == 'begin' or ev.match == 'report' then + active_progress[ev.data.client_id] = true + elseif ev.match == 'end' then + active_progress[ev.data.client_id] = nil + else + return + end + local client = vim.lsp.get_client_by_id(ev.data.client_id) + if client then + for buf in pairs(client.attached_buffers) do + refresh_lsp(buf) + end + end + end, +}) + +--- Git: resolved once per buffer, refreshed on save. +local function refresh_branch(buf) vim.system( { 'git', 'branch', '--show-current' }, { text = true }, function(out) - vim.b.git_branch = (out.code == 0 and out.stdout or ''):gsub( + if not vim.api.nvim_buf_is_valid(buf) then + return + end + vim.b[buf].git_branch = (out.code == 0 and out.stdout or ''):gsub( '%s+', '' ) + refresh_status(buf) end ) end -vim.uv.new_timer():start(5000, 5000, vim.schedule_wrap(update_git_branch)) -vim.api.nvim_create_autocmd({ 'BufEnter', 'FocusGained' }, { - callback = function() - update_git_branch() - end, -}) ---- LSP progress (Neovim 0.10+ built-in handler) -local progress = {} -local spinner_frames = - { '⣾', '⣽', '⣻', '⢿', '⡿', '⣟', '⣯', '⣷' } - -local frame = 0 ---- Clean stale progress messages periodically -local progress_timer = vim.uv.new_timer() -local function start_progress_timer() - if not progress_timer:is_active() then - progress_timer:start(0, 200, function() - local now = vim.loop.now() - for k, v in pairs(progress) do - local ttl = v.done and 2000 or 10000 - if now - v.t > ttl then - progress[k] = nil - end - end - if next(progress) then - frame = (frame % #spinner_frames) + 1 - vim.schedule(function() - vim.cmd('redrawstatus') - end) - else - progress_timer:stop() - vim.schedule(function() - vim.cmd('redrawstatus') - end) - end - end) - end -end - -vim.api.nvim_create_autocmd('LspProgress', { - callback = function(ev) - local d = ev.data - if not d then - return - end - local params = d.params - if not params or not params.token then - return - end - local key = d.client_id .. ':' .. tostring(params.token) - local value = params.value or {} - if value.kind == 'begin' then - progress[key] = { - msg = (value.message or 'working...'):gsub('%%', '%%%%'), - t = vim.loop.now(), - } - start_progress_timer() - elseif value.kind == 'report' then - local msg_parts = {} - if value.message then - msg_parts[#msg_parts + 1] = value.message - end - if value.percentage ~= nil then - msg_parts[#msg_parts + 1] = '(' .. value.percentage .. '%%)' - end - progress[key] = { - msg = table.concat(msg_parts, ' '), - t = vim.loop.now(), - } - elseif value.kind == 'end' then - if progress[key] then - progress[key].done = true - end - vim.cmd('redrawstatus') - end - end, -}) - -vim.api.nvim_create_autocmd({ 'LspAttach', 'LspDetach' }, { - callback = function(args) - local names = {} - for _, c in ipairs(vim.lsp.get_clients({ bufnr = args.buf })) do - table.insert(names, c.name) - end - vim.b[args.buf].lsp_names = #names > 0 - and (' ● ' .. table.concat(names, ', ') .. ' ') - or '' - end, -}) - -local function lsp_section() - local client_ids = {} - for _, c in - ipairs(vim.lsp.get_clients({ bufnr = vim.api.nvim_get_current_buf() })) - do - client_ids[c.id] = true - end - local latest = nil - for key, p in pairs(progress) do - local cid = tonumber(key:match('^(%d+):')) - if cid and client_ids[cid] then - if not latest or p.t > latest.t then - latest = p - end - end - end - if latest and (not latest.done or (vim.loop.now() - latest.t < 2000)) then - return '%#StatusLspSpinner# ○ ' .. latest.msg .. ' %*' - end - return '%#StatusLsp#' .. (vim.b.lsp_names or '') .. '%*' -end - -local diag_cache = {} -local function build_diag_string(counts) - local parts = {} - if counts[1] and counts[1] > 0 then - table.insert(parts, '%#StatusDiagError# E' .. counts[1] .. ' %*') - end - if counts[2] and counts[2] > 0 then - table.insert(parts, '%#StatusDiagWarn# W' .. counts[2] .. ' %*') - end - if counts[4] and counts[4] > 0 then - table.insert(parts, '%#StatusDiagHint# H' .. counts[4] .. ' %*') - end - return table.concat(parts) -end - -local function update_diag_cache(ev) - diag_cache[ev.buf] = build_diag_string(vim.diagnostic.count(ev.buf)) -end -vim.api.nvim_create_autocmd( - 'DiagnosticChanged', - { callback = update_diag_cache } -) - -local function update_git_diff() - if vim.bo.buftype ~= '' then - return - end - local file = vim.fn.expand('%:p') +local function refresh_diff(buf) + local file = vim.fn.fnamemodify(vim.fn.bufname(buf), ':p') if file == '' then return end @@ -238,59 +202,102 @@ local function update_git_diff() { 'git', 'diff', '--numstat', 'HEAD', '--', file }, { text = true }, function(out) - if out.code ~= 0 or out.stdout == '' then - vim.b.git_diff = nil + if not vim.api.nvim_buf_is_valid(buf) then return end - local added, removed = out.stdout:match('(%d+)%s+(%d+)') - vim.b.git_diff = { - added = tonumber(added) or 0, - removed = tonumber(removed) or 0, - } + if out.code ~= 0 or out.stdout == '' then + vim.b[buf].git_diff = nil + else + local added, removed = out.stdout:match('(%d+)%s+(%d+)') + vim.b[buf].git_diff = { + added = tonumber(added) or 0, + removed = tonumber(removed) or 0, + } + end + refresh_status(buf) end ) end -vim.api.nvim_create_autocmd({ 'BufEnter', 'FocusGained', 'BufWritePost' }, { - callback = update_git_diff, + +vim.api.nvim_create_autocmd({ 'BufEnter', 'BufNewFile' }, { + callback = function(ev) + if vim.bo[ev.buf].buftype == '' then + refresh_branch(ev.buf) + refresh_diff(ev.buf) + end + refresh_lsp(ev.buf) + refresh_diag(ev.buf) + end, +}) +vim.api.nvim_create_autocmd('FocusGained', { + callback = function() + local buf = vim.api.nvim_get_current_buf() + if vim.bo[buf].buftype == '' then + refresh_branch(buf) + end + end, +}) +vim.api.nvim_create_autocmd('BufWritePost', { + callback = function(ev) + refresh_diff(ev.buf) + end, +}) + +--- Diagnostics: counted on change, never during a redraw. +local function refresh_diag(buf) + local count = vim.diagnostic.count(buf) + local parts = {} + if count.error > 0 then + parts[#parts + 1] = '%#StatusDiagError# E' .. count.error .. ' %*' + end + if count.warn > 0 then + parts[#parts + 1] = '%#StatusDiagWarn# W' .. count.warn .. ' %*' + end + if count.hint > 0 then + parts[#parts + 1] = '%#StatusDiagHint# H' .. count.hint .. ' %*' + end + vim.b[buf].diag_str = #parts > 0 and table.concat(parts) or '' + refresh_status(buf) +end + +vim.api.nvim_create_autocmd('DiagnosticChanged', { + callback = function(ev) + refresh_diag(ev.buf) + end, +}) + +--- Macro: read once when recording starts, not on every redraw. +vim.api.nvim_create_autocmd('RecordingEnter', { + pattern = '*', + callback = function(ev) + vim.b[ev.buf].recording = '● Rec @' .. vim.fn.reg_recording() + refresh_status(ev.buf) + end, +}) + +vim.api.nvim_create_autocmd('RecordingLeave', { + pattern = '*', + callback = function(ev) + vim.b[ev.buf].recording = nil + refresh_status(ev.buf) + end, }) function _G.__statusline() - local m = vim.fn.mode() - local mode_hl = mode_hl_map[m] or 'StatusModeNormal' - local git = vim.b.git_branch or '' - local cur_buf = vim.api.nvim_get_current_buf() - local parts = { - '%#' .. mode_hl .. '#' .. (mode_map[m] or ' ?? ') .. '%*', - git ~= '' and ('%#StatusGit#  ' .. git .. ' %*') or '', - } - local diff = vim.b.git_diff - if diff and (diff.added > 0 or diff.removed > 0) then - if diff.added > 0 then - table.insert(parts, '%#StatusGitAdded# +' .. diff.added .. ' %*') - end - if diff.removed > 0 then - table.insert( - parts, - '%#StatusGitRemoved# -' .. diff.removed .. ' %*' - ) - end + local key = vim.fn.mode():sub(1, 2) + if key ~= '^V' and key ~= '^S' then + key = key:sub(1, 1) end - table.insert(parts, lsp_section()) - table.insert(parts, '%=') - local recording = vim.fn.reg_recording() - if recording ~= '' then - table.insert( - parts, - '%#StatusRecording# ● Rec @' .. recording .. ' %*' - ) - end - table.insert( - parts, - diag_cache[cur_buf] or build_diag_string(vim.diagnostic.count(cur_buf)) - ) - table.insert(parts, '%#StatusCursor# %l:%-2c %*') - table.insert(parts, '%#' .. mode_hl .. '# %p%% %*') - return table.concat(parts) + local hl = mode_hl_map[key] or 'StatusModeNormal' + local b = vim.b + return table.concat({ + '%#' .. hl .. '#' .. (mode_map[key] or ' ?? ') .. '%*', + b.status_left or '', + '%=', + b.status_right or '', + '%#StatusCursor# %l:%-2c %*', + '%#' .. hl .. '# %p%% %*', + }) end vim.o.statusline = '%!v:lua.__statusline()'