From a7932bab5a1810bee5a336b98f2091f70ccf2a81 Mon Sep 17 00:00:00 2001 From: Oscar Wallberg Date: Sat, 9 May 2026 19:54:49 +0200 Subject: [PATCH] feat(git/statusline): expose status via b:git_status, opt-in via enable() --- init.lua | 2 + lua/core/options.lua | 2 +- lua/git/repo.lua | 40 ++++++++------- lua/git/statusline.lua | 110 +++++++++++++++++++++++++++++++++++------ lua/git/util.lua | 6 +++ 5 files changed, 124 insertions(+), 36 deletions(-) diff --git a/init.lua b/init.lua index 1912fb4..d0fe2a4 100644 --- a/init.lua +++ b/init.lua @@ -95,3 +95,5 @@ require("ts").setup({ "https://github.com/tree-sitter-grammars/tree-sitter-yaml", "https://github.com/georgeharker/tree-sitter-zsh", }) + +require("git.statusline").enable() diff --git a/lua/core/options.lua b/lua/core/options.lua index 3573a26..d8ddbb8 100644 --- a/lua/core/options.lua +++ b/lua/core/options.lua @@ -77,7 +77,7 @@ vim.opt.inccommand = "split" vim.opt.winborder = "rounded" vim.opt.confirm = true -vim.opt.statusline = "%{expand('%:.')} %{%v:lua.require('git.statusline').render()%} %3(%m%)" +vim.opt.statusline = "%{expand('%:.')} %{%get(b:, 'git_status_string', '')%} %3(%m%)" .. " %=" .. " %{%v:lua.vim.diagnostic.status()%}" .. " %{&filetype} %{&fileencoding} %{&fileformat}" diff --git a/lua/git/repo.lua b/lua/git/repo.lua index 61d55a4..da601f4 100644 --- a/lua/git/repo.lua +++ b/lua/git/repo.lua @@ -220,24 +220,26 @@ end ---@return string? function Repo:head() - local f = io.open(vim.fs.joinpath(self.gitdir, "HEAD"), "r") - if not f then + return self:get_cached("head", function(self) + local f = io.open(vim.fs.joinpath(self.gitdir, "HEAD"), "r") + if not f then + return nil + end + local first = f:read("*l") + f:close() + if not first then + return nil + end + local branch = first:match("^ref:%s*refs/heads/(%S+)") + if branch then + return branch + end + local sha = first:match("^(%x+)") + if sha then + return sha:sub(1, 7) + end return nil - end - local first = f:read("*l") - f:close() - if not first then - return nil - end - local branch = first:match("^ref:%s*refs/heads/(%S+)") - if branch then - return branch - end - local sha = first:match("^(%x+)") - if sha then - return sha:sub(1, 7) - end - return nil + end) end ---@return string[] @@ -408,7 +410,7 @@ end ---@return string local function path_for_buf(buf) local path = vim.api.nvim_buf_get_name(buf) - if path == "" or path:match("^%a+://") then + if path == "" or util.is_uri(path) then return vim.fn.getcwd() end return vim.fn.resolve(path) @@ -514,7 +516,7 @@ local function is_worktree_buf(buf) return false end local path = vim.api.nvim_buf_get_name(buf) - return path ~= "" and not path:match("^%a+://") + return path ~= "" and not util.is_uri(path) end ---@param buf? integer diff --git a/lua/git/statusline.lua b/lua/git/statusline.lua index 24186b7..c680ba4 100644 --- a/lua/git/statusline.lua +++ b/lua/git/statusline.lua @@ -1,31 +1,109 @@ local repo = require("git.repo") +local util = require("git.util") local M = {} +---@class ow.Git.Statusline.Status +---@field head string? +---@field entries ow.Git.Status.Entry[] +---@field unstaged boolean +---@field staged boolean +---@field conflict boolean + +---@param entries ow.Git.Status.Entry[] +---@param head string? +---@return ow.Git.Statusline.Status +local function build(entries, head) + local out = { + head = head, + entries = entries, + unstaged = false, + staged = false, + conflict = false, + } + for _, e in ipairs(entries) do + if e.kind == "unstaged" or e.kind == "untracked" then + out.unstaged = true + elseif e.kind == "staged" then + out.staged = true + elseif e.kind == "unmerged" then + out.conflict = true + end + end + return out +end + +---@param entries ow.Git.Status.Entry[] ---@return string -function M.render() - local r = repo.find() - if not r then - return "" - end - local name = vim.api.nvim_buf_get_name(0) - if name == "" then - return "" - end - local rel = vim.fs.relpath(r.worktree, vim.fn.resolve(name)) - local list = rel and r.status.entries[rel] - if not list then +local function render(entries) + if #entries == 0 then return "" end local parts = {} - for _, e in ipairs(list) do + for _, e in ipairs(entries) do table.insert(parts, string.format("%%#%s#%s%%*", e.hl, e.char)) end return table.concat(parts, " ") end -repo.on("refresh", function() - vim.cmd.redrawstatus({ bang = true }) -end) +---@param buf integer +local function clear(buf) + vim.b[buf].git_status = nil + vim.b[buf].git_status_string = nil +end + +---@param buf integer +---@param r ow.Git.Repo? +local function update_buf(buf, r) + if not vim.api.nvim_buf_is_valid(buf) then + return + end + local name = vim.api.nvim_buf_get_name(buf) + if name == "" or util.is_uri(name) then + return clear(buf) + end + r = r or repo.find(buf) + if not r then + return clear(buf) + end + local rel = vim.fs.relpath(r.worktree, vim.fn.resolve(name)) + if not rel then + return clear(buf) + end + local entries = r.status.entries[rel] or {} + vim.b[buf].git_status = build(entries, r:head()) + vim.b[buf].git_status_string = render(entries) +end + +local enabled = false + +function M.enable() + if enabled then + return + end + enabled = true + repo.on("refresh", function(r) + for buf in pairs(r.buffers) do + if vim.api.nvim_buf_is_loaded(buf) then + update_buf(buf, r) + end + end + vim.cmd.redrawstatus({ bang = true }) + end) + vim.api.nvim_create_autocmd("BufWinEnter", { + group = vim.api.nvim_create_augroup( + "ow.git.statusline", + { clear = true } + ), + callback = function(args) + update_buf(args.buf, nil) + end, + }) + for _, buf in ipairs(vim.api.nvim_list_bufs()) do + if vim.api.nvim_buf_is_loaded(buf) then + update_buf(buf, nil) + end + end +end return M diff --git a/lua/git/util.lua b/lua/git/util.lua index bb81df2..e247d50 100644 --- a/lua/git/util.lua +++ b/lua/git/util.lua @@ -20,6 +20,12 @@ function M.setup_scratch(buf, opts) end end +---@param name string +---@return boolean +function M.is_uri(name) + return name:match("^%a+://") ~= nil +end + ---@param buf integer ---@param name string function M.set_buf_name(buf, name)