refactor(git): rework module around clearer Status and Repo split

This commit is contained in:
2026-05-06 00:45:56 +02:00
parent 1b43fa6a1c
commit 80d6d465cf
17 changed files with 821 additions and 775 deletions
+1 -1
View File
@@ -242,7 +242,7 @@ vim.keymap.set("n", "<leader>gH", function()
require("git.diff").split({ rev = "HEAD", vertical = false }) require("git.diff").split({ rev = "HEAD", vertical = false })
end) end)
vim.keymap.set("n", "<leader>gg", function() vim.keymap.set("n", "<leader>gg", function()
require("git.sidebar").toggle() require("git.status_view").toggle()
end) end)
vim.keymap.set("n", "<leader>gc", function() vim.keymap.set("n", "<leader>gc", function()
require("git.commit").commit() require("git.commit").commit()
+1 -1
View File
@@ -77,7 +77,7 @@ vim.opt.inccommand = "split"
vim.opt.winborder = "rounded" vim.opt.winborder = "rounded"
vim.opt.confirm = true vim.opt.confirm = true
vim.opt.statusline = "%{expand('%:.')} %{%v:lua.require('git.status').statusline()%} %3(%m%)" vim.opt.statusline = "%{expand('%:.')} %{%v:lua.require('git.statusline').render()%} %3(%m%)"
.. " %=" .. " %="
.. " %{%v:lua.vim.diagnostic.status()%}" .. " %{%v:lua.vim.diagnostic.status()%}"
.. " %{&filetype} %{&fileencoding} %{&fileformat}" .. " %{&filetype} %{&fileencoding} %{&fileformat}"
+8 -8
View File
@@ -5,11 +5,11 @@ local util = require("git.util")
local M = {} local M = {}
---@class ow.Git.SplitHandler ---@class ow.Git.Cmd.SplitHandler
---@field ft string ---@field ft string
---@field needs_rev boolean? ---@field needs_rev boolean?
---@type table<string, ow.Git.SplitHandler> ---@type table<string, ow.Git.Cmd.SplitHandler>
local SPLIT_HANDLERS = { local SPLIT_HANDLERS = {
log = { ft = "git" }, log = { ft = "git" },
diff = { ft = "diff" }, diff = { ft = "diff" },
@@ -74,7 +74,7 @@ end
---@param r ow.Git.Repo ---@param r ow.Git.Repo
---@param args string[] ---@param args string[]
---@param conf ow.Git.SplitHandler ---@param conf ow.Git.Cmd.SplitHandler
local function run_in_split(r, args, conf) local function run_in_split(r, args, conf)
local cmd = { "git" } local cmd = { "git" }
vim.list_extend(cmd, args) vim.list_extend(cmd, args)
@@ -86,7 +86,7 @@ local function run_in_split(r, args, conf)
end end
local name = "[git " .. table.concat(args, " ") .. "]" local name = "[git " .. table.concat(args, " ") .. "]"
local buf = place_split(name) local buf = place_split(name)
repo.attach(buf, r) repo.bind(buf, r)
object.attach_dispatch(buf) object.attach_dispatch(buf)
local state = r:state(buf) --[[@as -nil]] local state = r:state(buf) --[[@as -nil]]
state.sha = nil state.sha = nil
@@ -178,7 +178,7 @@ end
---@param args string[] ---@param args string[]
function M.run(args) function M.run(args)
local r = repo.find() local r = repo.resolve()
if not r then if not r then
util.warning("not in a git repository") util.warning("not in a git repository")
return return
@@ -193,7 +193,7 @@ function M.run(args)
if sub == "show" then if sub == "show" then
local arg = first_positional(args, 2) local arg = first_positional(args, 2)
if arg and arg:find(":", 1, true) then if arg and arg:find(":", 1, true) then
object.open_object(r, arg) object.open(r, arg)
return return
end end
run_in_split(r, args, { ft = "git", needs_rev = true }) run_in_split(r, args, { ft = "git", needs_rev = true })
@@ -204,7 +204,7 @@ function M.run(args)
if vim.list_contains(args, "-p") then if vim.list_contains(args, "-p") then
local rev = first_positional(args, 2) local rev = first_positional(args, 2)
if rev then if rev then
object.open_object(r, rev) object.open(r, rev)
return return
end end
end end
@@ -223,7 +223,7 @@ end
---@param arg_lead string ---@param arg_lead string
---@return string[] ---@return string[]
function M.complete_rev(arg_lead) function M.complete_rev(arg_lead)
local r = repo.find() local r = repo.resolve()
if not r then if not r then
return {} return {}
end end
+1 -1
View File
@@ -7,7 +7,7 @@ local M = {}
---@param opts { amend: boolean? }? ---@param opts { amend: boolean? }?
function M.commit(opts) function M.commit(opts)
local amend = opts and opts.amend or false local amend = opts and opts.amend or false
local r = repo.find() local r = repo.resolve()
if not r then if not r then
util.warning("not in a git repository") util.warning("not in a git repository")
return return
+13 -5
View File
@@ -4,6 +4,14 @@ local util = require("git.util")
local M = {} local M = {}
---@class ow.Git.Diff.Side
---@field buf integer
---@field name string?
---@class ow.Git.Diff.Pair
---@field left ow.Git.Diff.Side
---@field right ow.Git.Diff.Side
---@param win integer ---@param win integer
---@param enabled boolean ---@param enabled boolean
function M.set_diff(win, enabled) function M.set_diff(win, enabled)
@@ -28,7 +36,7 @@ end
---@param left_win integer ---@param left_win integer
---@param right_win integer ---@param right_win integer
---@param pair ow.Git.DiffPair ---@param pair ow.Git.Diff.Pair
function M.update_pair(left_win, right_win, pair) function M.update_pair(left_win, right_win, pair)
M.set_diff(left_win, false) M.set_diff(left_win, false)
M.set_diff(right_win, false) M.set_diff(right_win, false)
@@ -58,11 +66,11 @@ local function place_pair(buf_a, buf_b, a_left, vertical)
end end
end end
---@param opts ow.Git.SplitOpts ---@param opts ow.Git.Diff.SplitOpts
---@param buf integer ---@param buf integer
---@param rev ow.Git.Revision ---@param rev ow.Git.Revision
local function uri_split(opts, buf, rev) local function uri_split(opts, buf, rev)
local r = repo.find(buf) local r = repo.resolve(buf)
if not r then if not r then
util.warning("git URI buffer has no worktree") util.warning("git URI buffer has no worktree")
return return
@@ -127,11 +135,11 @@ local function uri_split(opts, buf, rev)
place_pair(buf, object.buf_for(r, other_rev, content), left, opts.vertical) place_pair(buf, object.buf_for(r, other_rev, content), left, opts.vertical)
end end
---@class ow.Git.SplitOpts ---@class ow.Git.Diff.SplitOpts
---@field rev string? ---@field rev string?
---@field vertical boolean ---@field vertical boolean
---@param opts ow.Git.SplitOpts ---@param opts ow.Git.Diff.SplitOpts
function M.split(opts) function M.split(opts)
local cur_buf = vim.api.nvim_get_current_buf() local cur_buf = vim.api.nvim_get_current_buf()
local cur_path = vim.api.nvim_buf_get_name(cur_buf) local cur_path = vim.api.nvim_buf_get_name(cur_buf)
+35 -12
View File
@@ -20,25 +20,28 @@ function M.init()
local group = vim.api.nvim_create_augroup("ow.git", { clear = true }) local group = vim.api.nvim_create_augroup("ow.git", { clear = true })
vim.api.nvim_create_autocmd( vim.api.nvim_create_autocmd({ "BufReadPost", "BufNewFile" }, {
{ "BufReadPost", "BufNewFile", "BufWritePost", "FileChangedShellPost" }, group = group,
{ callback = function(args)
group = group, require("git.repo").track(args.buf)
callback = function(args) end,
require("git.repo").refresh(args.buf) })
end, vim.api.nvim_create_autocmd({ "BufWritePost", "FileChangedShellPost" }, {
} group = group,
) callback = function(args)
require("git.repo").refresh(args.buf)
end,
})
vim.api.nvim_create_autocmd({ "BufDelete", "BufWipeout" }, { vim.api.nvim_create_autocmd({ "BufDelete", "BufWipeout" }, {
group = group, group = group,
callback = function(args) callback = function(args)
require("git.repo").unregister(args.buf) require("git.repo").unbind(args.buf)
end, end,
}) })
vim.api.nvim_create_autocmd("FocusGained", { vim.api.nvim_create_autocmd("FocusGained", {
group = group, group = group,
callback = function(args) callback = function()
require("git.repo").refresh(args.buf) require("git.repo").refresh_all()
end, end,
}) })
vim.api.nvim_create_autocmd("VimLeavePre", { vim.api.nvim_create_autocmd("VimLeavePre", {
@@ -48,6 +51,22 @@ function M.init()
end, end,
}) })
vim.api.nvim_create_autocmd({ "VimEnter", "DirChanged", "TabEnter" }, {
group = group,
callback = function()
require("git.repo").update_cwd_repo()
end,
})
vim.api.nvim_create_autocmd("TabClosed", {
group = group,
callback = function(args)
local tab = tonumber(args.file) --[[@as integer?]]
if tab then
require("git.repo").release_tab(tab)
end
end,
})
vim.api.nvim_create_autocmd("BufReadCmd", { vim.api.nvim_create_autocmd("BufReadCmd", {
pattern = "git://*", pattern = "git://*",
group = group, group = group,
@@ -118,6 +137,10 @@ function M.init()
return require("git.cmd").complete(...) return require("git.cmd").complete(...)
end, end,
}) })
vim.api.nvim_create_user_command("Grefresh", function()
require("git.repo").refresh_all()
end, { desc = "Refresh git status for all repos" })
end end
return M return M
+10 -8
View File
@@ -12,7 +12,7 @@ local cr = vim.api.nvim_replace_termcodes("<CR>", true, false, true)
---@param buf integer ---@param buf integer
local function attach_dispatch(buf) local function attach_dispatch(buf)
vim.keymap.set("n", "<CR>", function() vim.keymap.set("n", "<CR>", function()
local r = repo.find(buf) local r = repo.resolve(buf)
-- Anchor past the leading graph chars (matches the leading sha -- Anchor past the leading graph chars (matches the leading sha
-- column, not any hex word that happens to appear later in the -- column, not any hex word that happens to appear later in the
-- subject). -- subject).
@@ -22,7 +22,7 @@ local function attach_dispatch(buf)
:match("^[*|/\\_ ]*(%x%x%x%x%x%x%x+)") :match("^[*|/\\_ ]*(%x%x%x%x%x%x%x+)")
if sha then if sha then
---@cast r -nil ---@cast r -nil
require("git.object").open_object(r, sha, { split = false }) require("git.object").open(r, sha, { split = false })
else else
vim.api.nvim_feedkeys(cr, "n", false) vim.api.nvim_feedkeys(cr, "n", false)
end end
@@ -50,7 +50,7 @@ end
---@param buf integer ---@param buf integer
local function populate(buf) local function populate(buf)
local r = repo.find(buf) local r = repo.resolve(buf)
local state = r and r:state(buf) local state = r and r:state(buf)
if not r or not state then if not r or not state then
return return
@@ -100,7 +100,7 @@ function M.read_uri(buf)
if not r then if not r then
return return
end end
repo.attach(buf, r) repo.bind(buf, r)
vim.bo[buf].swapfile = false vim.bo[buf].swapfile = false
vim.bo[buf].bufhidden = "hide" vim.bo[buf].bufhidden = "hide"
@@ -113,7 +113,7 @@ function M.read_uri(buf)
populate(buf) populate(buf)
end end
---@class ow.Git.LogOpts ---@class ow.Git.Log.OpenOpts
---@field max_count integer? ---@field max_count integer?
---@type table<string, fun(s: string): any> ---@type table<string, fun(s: string): any>
@@ -121,17 +121,17 @@ M.opt_parsers = {
max_count = tonumber, max_count = tonumber,
} }
---@param opts ow.Git.LogOpts? ---@param opts ow.Git.Log.OpenOpts?
function M.open(opts) function M.open(opts)
opts = opts or {} opts = opts or {}
local r = repo.find() local r = repo.resolve()
if not r then if not r then
util.warning("not in a git repository") util.warning("not in a git repository")
return return
end end
local buf = vim.fn.bufadd(M.URI_PREFIX .. r.worktree) local buf = vim.fn.bufadd(M.URI_PREFIX .. r.worktree)
repo.attach(buf, r) repo.bind(buf, r)
local state = r:state(buf) --[[@as -nil]] local state = r:state(buf) --[[@as -nil]]
state.log_max_count = opts.max_count state.log_max_count = opts.max_count
local was_loaded = vim.api.nvim_buf_is_loaded(buf) local was_loaded = vim.api.nvim_buf_is_loaded(buf)
@@ -183,4 +183,6 @@ function M.complete_glog(arg_lead)
return matches return matches
end end
repo.on_uri_refresh(M.URI_PREFIX, M.read_uri)
return M return M
+11 -9
View File
@@ -145,7 +145,7 @@ end
---@return integer ---@return integer
function M.buf_for(r, rev, content) function M.buf_for(r, rev, content)
local buf = vim.fn.bufadd(M.format_uri(rev)) local buf = vim.fn.bufadd(M.format_uri(rev))
repo.attach(buf, r) repo.bind(buf, r)
if content then if content then
local state = r:state(buf) --[[@as -nil]] local state = r:state(buf) --[[@as -nil]]
state.pending_content = content state.pending_content = content
@@ -163,12 +163,12 @@ function M.read_uri(buf)
end end
local rev_str = rev:format() local rev_str = rev:format()
local r = repo.find(buf) local r = repo.resolve(buf)
if not r then if not r then
util.error("git BufReadCmd %s: cannot resolve worktree", name) util.error("git BufReadCmd %s: cannot resolve worktree", name)
return return
end end
repo.attach(buf, r) repo.bind(buf, r)
local state = r:state(buf) --[[@as -nil]] local state = r:state(buf) --[[@as -nil]]
vim.bo[buf].swapfile = false vim.bo[buf].swapfile = false
@@ -275,7 +275,7 @@ local function load_blob(r, blob, path, sha)
vim.api.nvim_set_current_buf(buf) vim.api.nvim_set_current_buf(buf)
end end
---@param s ow.Git.BufState ---@param s ow.Git.Repo.BufState
---@param section ow.Git.DiffSection ---@param section ow.Git.DiffSection
local function open_section(s, section) local function open_section(s, section)
if not section.blob_a or not section.blob_b then if not section.blob_a or not section.blob_b then
@@ -300,13 +300,13 @@ local function open_section(s, section)
vim.api.nvim_set_current_buf(buf) vim.api.nvim_set_current_buf(buf)
end end
---@class ow.Git.OpenObjectOpts ---@class ow.Git.Object.OpenOpts
---@field split (false|"above"|"below"|"left"|"right")? ---@field split (false|"above"|"below"|"left"|"right")?
---@param r ow.Git.Repo ---@param r ow.Git.Repo
---@param rev string ---@param rev string
---@param opts ow.Git.OpenObjectOpts? ---@param opts ow.Git.Object.OpenOpts?
function M.open_object(r, rev, opts) function M.open(r, rev, opts)
local parsed = Revision.parse(rev) local parsed = Revision.parse(rev)
if parsed.base then if parsed.base then
local sha = r:rev_parse(parsed.base, true) local sha = r:rev_parse(parsed.base, true)
@@ -341,7 +341,7 @@ function M.open_under_cursor()
or line:match("^tree (%x+)$") or line:match("^tree (%x+)$")
or line:match("^object (%x+)$") or line:match("^object (%x+)$")
if sha then if sha then
M.open_object(r, sha, { split = false }) M.open(r, sha, { split = false })
return true return true
end end
@@ -351,7 +351,7 @@ function M.open_under_cursor()
local nav_rev = entry_type == "blob" local nav_rev = entry_type == "blob"
and Revision.new({ base = s.sha, path = entry_name }):format() and Revision.new({ base = s.sha, path = entry_name }):format()
or entry_sha or entry_sha
M.open_object(r, nav_rev, { split = false }) M.open(r, nav_rev, { split = false })
return true return true
end end
@@ -384,4 +384,6 @@ function M.open_under_cursor()
return false return false
end end
repo.on_uri_refresh(M.URI_PREFIX, M.read_uri)
return M return M
+239 -296
View File
@@ -1,7 +1,9 @@
local status = require("git.status") local status = require("git.status")
local util = require("git.util") local util = require("git.util")
---@param buf integer? local M = {}
---@param buf? integer
---@return integer ---@return integer
local function expand_buf(buf) local function expand_buf(buf)
if not buf or buf == 0 then if not buf or buf == 0 then
@@ -10,7 +12,7 @@ local function expand_buf(buf)
return buf return buf
end end
---@class ow.Git.BufState ---@class ow.Git.Repo.BufState
---@field repo ow.Git.Repo ---@field repo ow.Git.Repo
---@field sha string? ---@field sha string?
---@field parent_sha string? ---@field parent_sha string?
@@ -19,110 +21,58 @@ end
---@field log_max_count integer? ---@field log_max_count integer?
---@field pending_content string? ---@field pending_content string?
---@class ow.Git.StatusEntry ---@alias ow.Git.Repo.Event "refresh"
---@field section "Untracked"|"Unstaged"|"Staged"|"Unmerged"
---@field x string
---@field y string
---@field path string
---@field orig string?
---@class ow.Git.BranchInfo local global = util.Emitter.new()
---@field head string?
---@field upstream string?
---@field ahead integer
---@field behind integer
---@class ow.Git.PorcelainGroups
---@field Untracked ow.Git.StatusEntry[]
---@field Unstaged ow.Git.StatusEntry[]
---@field Staged ow.Git.StatusEntry[]
---@field Unmerged ow.Git.StatusEntry[]
---@class ow.Git.Repo ---@class ow.Git.Repo
---@field gitdir string ---@field gitdir string
---@field worktree string ---@field worktree string
---@field buffers table<integer, ow.Git.BufState> ---@field buffers table<integer, ow.Git.Repo.BufState>
---@field watcher? uv.uv_fs_event_t ---@field tabs table<integer, true>
---@field refresh fun(self: ow.Git.Repo) ---@field status ow.Git.Status
---@field refresh_handle ow.Git.Util.DebounceHandle ---@field private _events ow.Git.Util.Emitter<ow.Git.Repo.Event>
---@field private refresh_listeners (fun(r: ow.Git.Repo, porcelain_stdout: string?))[] ---@field private _watcher? uv.uv_fs_event_t
---@field private _schedule_refresh fun(self: ow.Git.Repo)
---@field private _refresh_handle ow.Git.Util.DebounceHandle
local Repo = {} local Repo = {}
Repo.__index = Repo Repo.__index = Repo
---@param line string local STATUS_CMD = {
---@return string x, string y, string path, string? orig "git",
local function parse_porcelain_line(line) "--no-optional-locks",
local x = line:sub(1, 1) "-c",
local y = line:sub(2, 2) "core.quotePath=false",
local rest = line:sub(4) "status",
local orig "--porcelain=v1",
if x == "R" or x == "C" or y == "R" or y == "C" then "--branch",
local arrow = rest:find(" -> ", 1, true) "--ignored",
if arrow then }
orig = rest:sub(1, arrow - 1)
rest = rest:sub(arrow + 4)
end
end
return x, y, rest, orig
end
---@param r ow.Git.Repo ---@private
local function do_refresh(r) function Repo:_fetch_status()
vim.system( vim.system(
{ STATUS_CMD,
"git", { cwd = self.worktree, text = true },
"-c",
"core.quotePath=false",
"status",
"--porcelain=v1",
"--branch",
},
{ cwd = r.worktree, text = true },
vim.schedule_wrap(function(obj) vim.schedule_wrap(function(obj)
local statuses = {} if obj.code ~= 0 then
if obj.code == 0 then
for line in (obj.stdout or ""):gmatch("[^\r\n]+") do
if line:sub(1, 2) ~= "##" then
local x, y, path = parse_porcelain_line(line)
statuses[vim.fs.joinpath(r.worktree, path)] =
status.format(x .. y)
end
end
else
util.warning( util.warning(
"git status failed: %s", "git status failed: %s",
vim.trim(obj.stderr or "") vim.trim(obj.stderr or "")
) )
return
end end
local dirty = false self.status = status.parse(obj.stdout or "")
for buf in pairs(r.buffers) do self._events:emit("refresh", self.status)
if not vim.api.nvim_buf_is_valid(buf) then global:emit("refresh", self, self.status)
r.buffers[buf] = nil
else
local name = vim.api.nvim_buf_get_name(buf)
local object = require("git.object")
local log = require("git.log")
if name:sub(1, #object.URI_PREFIX) == object.URI_PREFIX then
object.read_uri(buf)
elseif name:sub(1, #log.URI_PREFIX) == log.URI_PREFIX then
log.read_uri(buf)
else
local s = statuses[vim.fn.resolve(name)]
if vim.b[buf].git_status ~= s then
vim.b[buf].git_status = s
dirty = true
end
end
end
end
if dirty then
vim.cmd.redrawstatus({ bang = true })
end
r:notify_refresh(obj.code == 0 and obj.stdout or nil)
end) end)
) )
end end
function Repo:refresh()
self:_schedule_refresh()
end
---@param gitdir string ---@param gitdir string
---@param worktree string ---@param worktree string
---@return ow.Git.Repo ---@return ow.Git.Repo
@@ -131,10 +81,14 @@ function Repo.new(gitdir, worktree)
gitdir = gitdir, gitdir = gitdir,
worktree = worktree, worktree = worktree,
buffers = {}, buffers = {},
refresh_listeners = {}, tabs = {},
status = status.parse(""),
_events = util.Emitter.new(),
}, Repo) }, Repo)
self.refresh, self.refresh_handle = util.debounce(do_refresh, 50) self._schedule_refresh, self._refresh_handle =
util.debounce(Repo._fetch_status, 50)
self:start_watcher() self:start_watcher()
self:refresh()
return self return self
end end
@@ -148,7 +102,7 @@ function Repo:start_watcher()
) )
return return
end end
local ok, err = watcher:start( local ok, start_err = watcher:start(
self.gitdir, self.gitdir,
{ recursive = true }, { recursive = true },
function(err_, filename) function(err_, filename)
@@ -163,67 +117,35 @@ function Repo:start_watcher()
end end
) )
if not ok then if not ok then
util.warning("failed to watch %s: %s", self.gitdir, tostring(err)) util.warning("failed to watch %s: %s", self.gitdir, tostring(start_err))
watcher:close() watcher:close()
return return
end end
self.watcher = watcher self._watcher = watcher
end end
function Repo:stop_watcher() function Repo:close()
if self.watcher then if self._watcher then
self.watcher:stop() self._watcher:stop()
self.watcher:close() self._watcher:close()
self.watcher = nil self._watcher = nil
end end
self.refresh_handle.close() self._refresh_handle.close()
end end
---@param buf integer ---@param event ow.Git.Repo.Event
function Repo:add_buffer(buf) ---@param fn fun(...)
buf = expand_buf(buf) ---@return fun() unsubscribe
if not self.buffers[buf] then function Repo:on(event, fn)
self.buffers[buf] = { repo = self } return self._events:on(event, fn)
end
end end
---@param buf integer ---@param buf? integer
function Repo:remove_buffer(buf) ---@return ow.Git.Repo.BufState?
self.buffers[expand_buf(buf)] = nil
end
---@param buf integer
---@return ow.Git.BufState?
function Repo:state(buf) function Repo:state(buf)
return self.buffers[expand_buf(buf)] return self.buffers[expand_buf(buf)]
end end
---@return boolean
function Repo:has_buffers()
return next(self.buffers) ~= nil
end
---@param fn fun(r: ow.Git.Repo, porcelain_stdout: string?)
---@return fun() unsubscribe
function Repo:on_refresh(fn)
table.insert(self.refresh_listeners, fn)
return function()
for i, f in ipairs(self.refresh_listeners) do
if f == fn then
table.remove(self.refresh_listeners, i)
return
end
end
end
end
---@param porcelain_stdout string?
function Repo:notify_refresh(porcelain_stdout)
for _, fn in ipairs(self.refresh_listeners) do
fn(self, porcelain_stdout)
end
end
---@return string? ---@return string?
function Repo:head() function Repo:head()
local f = io.open(vim.fs.joinpath(self.gitdir, "HEAD"), "r") local f = io.open(vim.fs.joinpath(self.gitdir, "HEAD"), "r")
@@ -278,27 +200,117 @@ function Repo:rev_parse(rev, short)
return trimmed ~= "" and trimmed or nil return trimmed ~= "" and trimmed or nil
end end
local M = {} ---@type table<string, ow.Git.Repo> keyed by worktree
local repos = {}
---@type table<string, ow.Git.Repo> ---@param event ow.Git.Repo.Event
local repo_by_gitdir = {} ---@param fn fun(...)
---@return fun() unsubscribe
function M.on(event, fn)
return global:on(event, fn)
end
---@type table<integer, ow.Git.Repo> ---@param prefix string
local repo_by_buf = {} ---@param fn fun(buf: integer)
---@return fun() unsubscribe
function M.on_uri_refresh(prefix, fn)
return M.on("refresh", function(r)
for buf in pairs(r.buffers) do
if vim.api.nvim_buf_is_valid(buf) then
local name = vim.api.nvim_buf_get_name(buf)
if name:sub(1, #prefix) == prefix then
fn(buf)
end
end
end
end)
end
---@param r ow.Git.Repo
local function release(r)
if repos[r.worktree] ~= r then
return
end
if next(r.buffers) ~= nil or next(r.tabs) ~= nil then
return
end
r:close()
repos[r.worktree] = nil
end
---@param buf integer
---@return ow.Git.Repo?
local function find_by_buf(buf)
for _, r in pairs(repos) do
if r.buffers[buf] then
return r
end
end
return nil
end
---@param path string ---@param path string
---@return ow.Git.Repo? ---@return ow.Git.Repo?
function M.resolve(path) local function find_by_path(path)
path = vim.fn.resolve(path) if path == "" then
return nil
end
if repos[path] then
return repos[path]
end
for wt, r in pairs(repos) do
if path:sub(1, #wt + 1) == wt .. "/" then
return r
end
end
return nil
end
---@param buf integer
---@return string
local function path_for_buf(buf)
local path = vim.api.nvim_buf_get_name(buf)
if path == "" or path:match("^%a+://") then
return vim.fn.getcwd()
end
return vim.fn.resolve(path)
end
---@param arg? integer | string bufnr (default current) or worktree path
---@return ow.Git.Repo?
function M.find(arg)
if type(arg) == "string" then
return find_by_path(arg)
end
local buf = expand_buf(arg)
return find_by_buf(buf) or find_by_path(path_for_buf(buf))
end
---@param arg? integer | string bufnr (default current) or worktree path
---@return ow.Git.Repo?
function M.resolve(arg)
local existing = M.find(arg)
if existing then
return existing
end
local path
if type(arg) == "string" then
path = vim.fn.resolve(arg)
else
path = path_for_buf(expand_buf(arg))
end
local found = vim.fs.find(".git", { upward = true, path = path })[1] local found = vim.fs.find(".git", { upward = true, path = path })[1]
if not found then if not found then
return nil return nil
end end
local worktree = vim.fs.dirname(found)
if repos[worktree] then
return repos[worktree]
end
local stat = vim.uv.fs_stat(found) local stat = vim.uv.fs_stat(found)
if not stat then if not stat then
return nil return nil
end end
local worktree = vim.fs.dirname(found)
local gitdir local gitdir
if stat.type == "directory" then if stat.type == "directory" then
gitdir = found gitdir = found
@@ -314,192 +326,123 @@ function M.resolve(path)
util.warning(".git file at %s has no `gitdir:` line", found) util.warning(".git file at %s has no `gitdir:` line", found)
return nil return nil
end end
if rel:match("^/") then gitdir = vim.fs.normalize(
gitdir = rel rel:match("^/") and rel or vim.fs.joinpath(worktree, rel)
else )
gitdir = vim.fs.joinpath(worktree, rel)
end
gitdir = vim.fs.normalize(gitdir)
end
local r = repo_by_gitdir[gitdir]
if not r then
r = Repo.new(gitdir, worktree)
repo_by_gitdir[gitdir] = r
end end
local r = Repo.new(gitdir, worktree)
repos[worktree] = r
return r return r
end end
---@param buf integer? ---@param buf? integer
---@return ow.Git.Repo? ---@return ow.Git.Repo.BufState?
function M.find(buf)
buf = expand_buf(buf)
local existing = repo_by_buf[buf]
if existing then
return existing
end
local path = vim.api.nvim_buf_get_name(buf)
if path == "" or path:match("^%a+://") then
path = vim.fn.getcwd()
end
return M.resolve(path)
end
---@param buf integer?
---@return ow.Git.BufState?
function M.state(buf) function M.state(buf)
buf = expand_buf(buf) buf = expand_buf(buf)
local r = repo_by_buf[buf] local r = find_by_buf(buf)
return r and r.buffers[buf] return r and r.buffers[buf]
end end
---@param buf integer ---@param buf? integer
---@param r ow.Git.Repo ---@param r ow.Git.Repo
function M.attach(buf, r) function M.bind(buf, r)
buf = expand_buf(buf) buf = expand_buf(buf)
if repo_by_buf[buf] == r then local prev = find_by_buf(buf)
if prev == r then
return return
end end
if repo_by_buf[buf] then if prev then
repo_by_buf[buf]:remove_buffer(buf) prev.buffers[buf] = nil
release(prev)
end end
r:add_buffer(buf) r.buffers[buf] = { repo = r }
repo_by_buf[buf] = r
end end
---@param buf integer ---@param buf? integer
---@return ow.Git.Repo? function M.unbind(buf)
function M.register(buf)
buf = expand_buf(buf) buf = expand_buf(buf)
local r = M.find(buf) local r = find_by_buf(buf)
if not r then
return nil
end
if repo_by_buf[buf] ~= r then
M.attach(buf, r)
end
return r
end
---@param buf integer
function M.unregister(buf)
buf = expand_buf(buf)
local r = repo_by_buf[buf]
if not r then if not r then
return return
end end
repo_by_buf[buf] = nil r.buffers[buf] = nil
r:remove_buffer(buf) release(r)
if not r:has_buffers() then
r:stop_watcher()
repo_by_gitdir[r.gitdir] = nil
end
end end
---@param buf integer ---@param buf integer
function M.refresh(buf) ---@return boolean
buf = expand_buf(buf) local function is_worktree_buf(buf)
if not vim.api.nvim_buf_is_valid(buf) or vim.bo[buf].buftype ~= "" then if not vim.api.nvim_buf_is_valid(buf) or vim.bo[buf].buftype ~= "" then
return return false
end end
local path = vim.api.nvim_buf_get_name(buf) local path = vim.api.nvim_buf_get_name(buf)
if path == "" or path:match("^%a+://") then return path ~= "" and not path:match("^%a+://")
end
---@param buf? integer
function M.track(buf)
buf = expand_buf(buf)
if not is_worktree_buf(buf) then
return return
end end
local r = M.register(buf) local r = M.resolve(buf)
if not r then if r and not r.buffers[buf] then
vim.b[buf].git_status = nil M.bind(buf, r)
end
end
---@param buf? integer
function M.refresh(buf)
local r = find_by_buf(expand_buf(buf))
if r then
r:refresh()
end
end
function M.refresh_all()
for _, r in pairs(repos) do
r:refresh()
end
end
function M.update_cwd_repo()
local tab = vim.api.nvim_get_current_tabpage()
local new = M.resolve(vim.fn.getcwd())
local old
for _, r in pairs(repos) do
if r.tabs[tab] then
old = r
break
end
end
if new == old then
return return
end end
r:refresh() if old then
old.tabs[tab] = nil
release(old)
end
if new then
new.tabs[tab] = true
new:refresh()
end
end
---@param tab integer
function M.release_tab(tab)
for _, r in pairs(repos) do
if r.tabs[tab] then
r.tabs[tab] = nil
release(r)
return
end
end
end end
function M.stop_all() function M.stop_all()
for _, r in pairs(repo_by_gitdir) do for _, r in pairs(repos) do
r:stop_watcher() r:close()
end end
end end
---@param line string
---@return ow.Git.BranchInfo
function M.parse_branch_line(line)
local info = { ahead = 0, behind = 0 }
local content = line:sub(4)
local arrow = content:find("...", 1, true)
if not arrow then
info.head = content
return info
end
info.head = content:sub(1, arrow - 1)
local rest = content:sub(arrow + 3)
local bracket = rest:find(" %[")
if not bracket then
info.upstream = rest
return info
end
info.upstream = rest:sub(1, bracket - 1)
local inside = rest:match("%[([^%]]+)%]")
if inside then
info.ahead = (tonumber(inside:match("ahead (%d+)")) or 0) --[[@as integer]]
info.behind = (tonumber(inside:match("behind (%d+)")) or 0) --[[@as integer]]
end
return info
end
---@param stdout string
---@return ow.Git.BranchInfo, ow.Git.PorcelainGroups
function M.parse_porcelain(stdout)
local branch = { ahead = 0, behind = 0 }
---@type ow.Git.PorcelainGroups
local groups = {
Untracked = {},
Unstaged = {},
Staged = {},
Unmerged = {},
}
for line in stdout:gmatch("[^\r\n]+") do
if line:sub(1, 2) == "##" then
branch = M.parse_branch_line(line)
else
local x, y, path, orig = parse_porcelain_line(line)
if x == "?" and y == "?" then
table.insert(groups.Untracked, {
section = "Untracked",
x = x,
y = y,
path = path,
orig = orig,
})
elseif status.UNMERGED[x .. y] then
table.insert(groups.Unmerged, {
section = "Unmerged",
x = x,
y = y,
path = path,
orig = orig,
})
else
if x ~= " " then
table.insert(groups.Staged, {
section = "Staged",
x = x,
y = y,
path = path,
orig = orig,
})
end
if y ~= " " then
table.insert(groups.Unstaged, {
section = "Unstaged",
x = x,
y = y,
path = path,
orig = orig,
})
end
end
end
end
return branch, groups
end
return M return M
+157 -31
View File
@@ -1,6 +1,6 @@
local M = {} local M = {}
M.UNMERGED = { local UNMERGED = {
DD = true, DD = true,
AU = true, AU = true,
UD = true, UD = true,
@@ -10,48 +10,174 @@ M.UNMERGED = {
UU = true, UU = true,
} }
---@param code string ---@alias ow.Git.Status.EntryKind "untracked"|"unstaged"|"staged"|"unmerged"|"ignored"
---@return string? char
---@return string? hl_group ---@class ow.Git.Status.Entry
function M.indicator(code) ---@field path string
if code == "" then ---@field kind ow.Git.Status.EntryKind
return nil ---@field char string
---@field hl string
---@field orig string?
---@class ow.Git.Status.BranchInfo
---@field head string?
---@field upstream string?
---@field ahead integer
---@field behind integer
---@class ow.Git.Status
---@field branch ow.Git.Status.BranchInfo
---@field entries table<string, ow.Git.Status.Entry[]>
local Status = {}
Status.__index = Status
---@param kind ow.Git.Status.EntryKind
---@return ow.Git.Status.Entry[]
function Status:by_kind(kind)
local out = {}
for _, list in pairs(self.entries) do
for _, e in ipairs(list) do
if e.kind == kind then
table.insert(out, e)
end
end
end end
if code == "??" then return out
return "?", "GitUntracked" end
end
if code == "!!" then ---@param x string
return "!", "GitIgnored" ---@return string char, string hl
end local function staged_attrs(x)
if M.UNMERGED[code] then if x == "R" then
return "U", "GitUnmerged"
end
local x, y = code:sub(1, 1), code:sub(2, 2)
if x == "R" or y == "R" then
return "R", "GitRenamed" return "R", "GitRenamed"
end end
if y == " " and x ~= " " then return x, "GitStaged"
return x, "GitStaged" end
---@param y string
---@return string char, string hl
local function unstaged_attrs(y)
if y == "R" then
return "R", "GitRenamed"
end end
if y == "D" then if y == "D" then
return "D", "GitDeleted" return "D", "GitDeleted"
end end
return "M", "GitUnstaged" return y, "GitUnstaged"
end end
---@param code string ---@param line string
---@return string? ---@return ow.Git.Status.BranchInfo
function M.format(code) local function parse_branch_line(line)
local char, hl = M.indicator(code) local info = { ahead = 0, behind = 0 }
if not char then local content = line:sub(4)
return nil local arrow = content:find("...", 1, true)
if not arrow then
info.head = content
return info
end end
return string.format("%%#%s#%s%%*", hl, char) info.head = content:sub(1, arrow - 1)
local rest = content:sub(arrow + 3)
local bracket = rest:find(" %[")
if not bracket then
info.upstream = rest
return info
end
info.upstream = rest:sub(1, bracket - 1)
local inside = rest:match("%[([^%]]+)%]")
if inside then
info.ahead = (tonumber(inside:match("ahead (%d+)")) or 0) --[[@as integer]]
info.behind = (tonumber(inside:match("behind (%d+)")) or 0) --[[@as integer]]
end
return info
end end
---@return string ---@param line string
function M.statusline() ---@return string x, string y, string path, string? orig
return vim.b.git_status or "" local function parse_status_line(line)
local x = line:sub(1, 1)
local y = line:sub(2, 2)
local rest = line:sub(4)
local orig
if x == "R" or x == "C" or y == "R" or y == "C" then
local arrow = rest:find(" -> ", 1, true)
if arrow then
orig = rest:sub(1, arrow - 1)
rest = rest:sub(arrow + 4)
end
end
return x, y, rest, orig
end
---@param entries table<string, ow.Git.Status.Entry[]>
---@param entry ow.Git.Status.Entry
local function add(entries, entry)
local key = entry.path
if key:sub(-1) == "/" then
key = key:sub(1, -2)
end
local list = entries[key] or {}
table.insert(list, entry)
entries[key] = list
end
---@param stdout string
---@return ow.Git.Status
function M.parse(stdout)
local branch = { ahead = 0, behind = 0 }
---@type table<string, ow.Git.Status.Entry[]>
local entries = {}
for line in stdout:gmatch("[^\r\n]+") do
if line:sub(1, 2) == "##" then
branch = parse_branch_line(line)
else
local x, y, path, orig = parse_status_line(line)
if x == "?" and y == "?" then
add(entries, {
path = path,
kind = "untracked",
char = "?",
hl = "GitUntracked",
})
elseif x == "!" and y == "!" then
add(entries, {
path = path,
kind = "ignored",
char = "i",
hl = "GitIgnored",
})
elseif UNMERGED[x .. y] then
add(entries, {
path = path,
kind = "unmerged",
char = "!",
hl = "GitUnmerged",
})
else
if x ~= " " then
local char, hl = staged_attrs(x)
add(entries, {
path = path,
kind = "staged",
char = char,
hl = hl,
orig = orig,
})
end
if y ~= " " then
local char, hl = unstaged_attrs(y)
add(entries, {
path = path,
kind = "unstaged",
char = char,
hl = hl,
orig = orig,
})
end
end
end
end
return setmetatable({ branch = branch, entries = entries }, Status)
end end
return M return M
+152 -322
View File
@@ -2,32 +2,18 @@ local Revision = require("git.revision")
local diff = require("git.diff") local diff = require("git.diff")
local object = require("git.object") local object = require("git.object")
local repo = require("git.repo") local repo = require("git.repo")
local status = require("git.status")
local util = require("git.util") local util = require("git.util")
local M = {} local M = {}
local SECTIONS = { ---@type ow.Git.Status.EntryKind[]
"Untracked", local KINDS = { "untracked", "unstaged", "staged", "unmerged" }
"Unstaged", local WINDOW_WIDTH = 50
"Staged",
"Unmerged",
"Unpushed",
"Unpulled",
}
local SIDEBAR_WIDTH = 50
---@class ow.Git.CommitEntry ---@class ow.Git.StatusView.State
---@field section "Unpushed"|"Unpulled"
---@field sha string
---@field subject string?
---@alias ow.Git.SidebarEntry ow.Git.StatusEntry | ow.Git.CommitEntry
---@class ow.Git.SidebarState
---@field repo ow.Git.Repo ---@field repo ow.Git.Repo
---@field lines table<integer, ow.Git.SidebarEntry> ---@field lines table<integer, ow.Git.Status.Entry>
---@field sidebar_win integer? ---@field win integer?
---@field invocation_win integer? ---@field invocation_win integer?
---@field diff_left_win integer? ---@field diff_left_win integer?
---@field diff_right_win integer? ---@field diff_right_win integer?
@@ -35,181 +21,56 @@ local SIDEBAR_WIDTH = 50
---@field last_shown_key string? ---@field last_shown_key string?
---@field last_render_key string? ---@field last_render_key string?
---@type table<integer, ow.Git.SidebarState> ---@type table<integer, ow.Git.StatusView.State>
local state = {} local state = {}
local group = vim.api.nvim_create_augroup("ow.git.sidebar", { clear = false }) local group =
local ns = vim.api.nvim_create_namespace("ow.git.sidebar") vim.api.nvim_create_augroup("ow.git.status_win", { clear = false })
local ns = vim.api.nvim_create_namespace("ow.git.status_win")
---@return integer? win ---@return integer? win
---@return integer? bufnr ---@return integer? bufnr
local function find_sidebar() local function find_view()
for _, win in ipairs(vim.api.nvim_tabpage_list_wins(0)) do for _, win in ipairs(vim.api.nvim_tabpage_list_wins(0)) do
local buf = vim.api.nvim_win_get_buf(win) local buf = vim.api.nvim_win_get_buf(win)
if vim.bo[buf].filetype == "gitsidebar" then if vim.bo[buf].filetype == "gitstatus" then
return win, buf return win, buf
end end
end end
end end
---@param s ow.Git.SidebarState ---@param s ow.Git.StatusView.State
---@return integer? ---@return integer?
local function sidebar_win_for(s) local function win_for(s)
local win = s.sidebar_win local win = s.win
if win and vim.api.nvim_win_is_valid(win) then if win and vim.api.nvim_win_is_valid(win) then
return win return win
end end
win = find_sidebar() win = find_view()
s.sidebar_win = win s.win = win
return win return win
end end
---@param entry ow.Git.SidebarEntry ---@param entry ow.Git.Status.Entry
---@return string? ---@return string line
local function entry_code(entry) ---@return string hl_group
if entry.section == "Untracked" then ---@return integer hl_len
return "??"
elseif entry.section == "Unmerged" then
return entry.x .. entry.y
elseif entry.section == "Staged" then
return entry.x .. " "
elseif entry.section == "Unstaged" then
return " " .. entry.y
end
end
---@param entry ow.Git.SidebarEntry
---@return string? line
---@return string? hl_group
---@return integer? hl_len
local function format_entry(entry) local function format_entry(entry)
if entry.sha then
return string.format(" %s %s", entry.sha, entry.subject or ""),
"GitSha",
#entry.sha
end
local code = entry_code(entry)
if not code then
return nil
end
local char, hl = status.indicator(code)
if not char then
return nil
end
local label = entry.orig and (entry.orig .. " -> " .. entry.path) local label = entry.orig and (entry.orig .. " -> " .. entry.path)
or entry.path or entry.path
return string.format(" %s %s", char, label), hl, #char return string.format(" %s %s", entry.char, label), entry.hl, #entry.char
end end
---@param stdout string ---@param kind ow.Git.Status.EntryKind
---@return ow.Git.BranchInfo, table<string, ow.Git.SidebarEntry[]> ---@return string
local function parse_porcelain(stdout) local function display_name(kind)
local branch, groups = repo.parse_porcelain(stdout) return (kind:gsub("^%l", string.upper))
---@cast groups table<string, ow.Git.SidebarEntry[]>
groups.Unpushed = {}
groups.Unpulled = {}
return branch, groups
end
---@param worktree string
---@param branch ow.Git.BranchInfo
---@param groups table<string, ow.Git.SidebarEntry[]>
local function enrich_with_log(worktree, branch, groups)
local fetches = {}
if branch.upstream and branch.ahead > 0 then
table.insert(
fetches,
{ section = "Unpushed", range = "@{upstream}..HEAD" }
)
end
if branch.upstream and branch.behind > 0 then
table.insert(
fetches,
{ section = "Unpulled", range = "HEAD..@{upstream}" }
)
end
local pending = {}
for _, f in ipairs(fetches) do
table.insert(pending, {
f = f,
sys = vim.system({
"git",
"log",
"--max-count=200",
"--format=%h %s",
f.range,
}, { cwd = worktree, text = true }),
})
end
for _, p in ipairs(pending) do
local result = p.sys:wait()
if result.code == 0 then
for line in (result.stdout or ""):gmatch("[^\r\n]+") do
local sha, subject = line:match("^(%S+)%s+(.+)$")
if sha then
table.insert(groups[p.f.section], {
section = p.f.section,
sha = sha,
subject = subject,
})
end
end
else
util.error(
"git log %s failed: %s",
p.f.range,
vim.trim(result.stderr or "")
)
end
end
end
---@param worktree string
---@param prefetched_stdout string?
---@param callback fun(branch: ow.Git.BranchInfo, groups: table<string, ow.Git.SidebarEntry[]>)
local function fetch_status(worktree, prefetched_stdout, callback)
if prefetched_stdout then
local branch, groups = parse_porcelain(prefetched_stdout)
enrich_with_log(worktree, branch, groups)
callback(branch, groups)
return
end
vim.system(
{
"git",
"-c",
"core.quotePath=false",
"status",
"--porcelain=v1",
"--branch",
},
{ cwd = worktree, text = true },
vim.schedule_wrap(function(obj)
if obj.code ~= 0 then
util.error("git status failed: %s", vim.trim(obj.stderr or ""))
local branch = { ahead = 0, behind = 0 }
local groups = {
Untracked = {},
Unstaged = {},
Staged = {},
Unmerged = {},
Unpushed = {},
Unpulled = {},
}
callback(branch, groups)
return
end
local branch, groups = parse_porcelain(obj.stdout or "")
enrich_with_log(worktree, branch, groups)
callback(branch, groups)
end)
)
end end
---@param bufnr integer ---@param bufnr integer
---@param branch ow.Git.BranchInfo ---@param status ow.Git.Status
---@param groups table<string, ow.Git.SidebarEntry[]> local function render(bufnr, status)
local function render(bufnr, branch, groups) local branch = status.branch
local lines = { "Head: " .. (branch.head or "?") } local lines = { "Head: " .. (branch.head or "?") }
if branch.upstream then if branch.upstream then
local push = "Push: " .. branch.upstream local push = "Push: " .. branch.upstream
@@ -225,24 +86,23 @@ local function render(bufnr, branch, groups)
local meta = {} local meta = {}
local marks = {} local marks = {}
for _, section in ipairs(SECTIONS) do for _, kind in ipairs(KINDS) do
local entries = groups[section] local entries = status:by_kind(kind)
if entries and #entries > 0 then if #entries > 0 then
table.insert(lines, string.format("%s (%d)", section, #entries)) table.insert(
lines,
string.format("%s (%d)", display_name(kind), #entries)
)
for _, entry in ipairs(entries) do for _, entry in ipairs(entries) do
local line, hl, hl_len = format_entry(entry) local line, hl, hl_len = format_entry(entry)
if line then table.insert(lines, line)
table.insert(lines, line) meta[#lines] = entry
meta[#lines] = entry table.insert(marks, {
if hl and hl_len then row = #lines - 1,
table.insert(marks, { col = 2,
row = #lines - 1, end_col = 2 + hl_len,
col = 2, hl = hl,
end_col = 2 + hl_len, })
hl = hl,
})
end
end
end end
table.insert(lines, "") table.insert(lines, "")
end end
@@ -261,89 +121,78 @@ local function render(bufnr, branch, groups)
state[bufnr].lines = meta state[bufnr].lines = meta
end end
---@param branch ow.Git.BranchInfo ---@param status ow.Git.Status
---@param groups table<string, ow.Git.SidebarEntry[]>
---@return string ---@return string
local function fingerprint(branch, groups) local function fingerprint(status)
local branch = status.branch
local parts = { local parts = {
branch.head or "", branch.head or "",
branch.upstream or "", branch.upstream or "",
tostring(branch.ahead), tostring(branch.ahead),
tostring(branch.behind), tostring(branch.behind),
} }
for _, section in ipairs(SECTIONS) do for _, kind in ipairs(KINDS) do
local entries = groups[section] for _, e in ipairs(status:by_kind(kind)) do
if entries then table.insert(
for _, e in ipairs(entries) do parts,
table.insert( e.kind
parts, .. ":"
e.section .. e.path
.. ":" .. ":"
.. (e.path or e.sha or "") .. (e.orig or "")
.. ":" .. ":"
.. (e.orig or "") .. e.char
.. ":" )
.. (e.x or "")
.. ":"
.. (e.y or "")
)
end
end end
end end
return table.concat(parts, "\0") return table.concat(parts, "\0")
end end
---@param bufnr integer ---@param bufnr integer
---@param prefetched_stdout string? local function refresh(bufnr)
local function refresh(bufnr, prefetched_stdout)
local s = state[bufnr] local s = state[bufnr]
if not s then if not s then
return return
end end
local saved_path, saved_sha local saved_path
local sidebar_win = sidebar_win_for(s) local status_win = win_for(s)
if sidebar_win then if status_win then
local lnum = vim.api.nvim_win_get_cursor(sidebar_win)[1] local lnum = vim.api.nvim_win_get_cursor(status_win)[1]
local entry = s.lines[lnum] local entry = s.lines[lnum]
if entry then if entry then
saved_path = entry.path saved_path = entry.path
saved_sha = entry.sha
end end
end end
fetch_status(s.repo.worktree, prefetched_stdout, function(branch, groups) if not vim.api.nvim_buf_is_valid(bufnr) then
if not vim.api.nvim_buf_is_valid(bufnr) then return
return end
end local status = s.repo.status
s.last_shown_key = nil s.last_shown_key = nil
local fp = fingerprint(branch, groups) local fp = fingerprint(status)
if fp == s.last_render_key then if fp == s.last_render_key then
return return
end end
s.last_render_key = fp s.last_render_key = fp
render(bufnr, branch, groups) render(bufnr, status)
if not saved_path and not saved_sha then if not saved_path then
return return
end end
for lnum, entry in pairs(s.lines) do for lnum, entry in pairs(s.lines) do
if if entry.path == saved_path then
(saved_path and entry.path == saved_path) local win = win_for(s)
or (saved_sha and entry.sha == saved_sha) if win then
then pcall(vim.api.nvim_win_set_cursor, win, { lnum, 0 })
local win = sidebar_win_for(s)
if win then
pcall(vim.api.nvim_win_set_cursor, win, { lnum, 0 })
end
break
end end
break
end end
end) end
end end
---@param bufnr integer ---@param bufnr integer
---@return ow.Git.SidebarState? ---@return ow.Git.StatusView.State?
---@return ow.Git.SidebarEntry? ---@return ow.Git.Status.Entry?
local function current_entry(bufnr) local function current_entry(bufnr)
local s = state[bufnr] local s = state[bufnr]
if not s then if not s then
@@ -353,17 +202,9 @@ local function current_entry(bufnr)
return s, s.lines[lnum] return s, s.lines[lnum]
end end
---@class ow.Git.DiffSide
---@field buf integer
---@field name string?
---@class ow.Git.DiffPair
---@field left ow.Git.DiffSide
---@field right ow.Git.DiffSide
---@param r ow.Git.Repo ---@param r ow.Git.Repo
---@param path string ---@param path string
---@return ow.Git.DiffSide ---@return ow.Git.Diff.Side
local function head_pane(r, path) local function head_pane(r, path)
local rev = Revision.new({ base = "HEAD", path = path }) local rev = Revision.new({ base = "HEAD", path = path })
return { return {
@@ -374,16 +215,16 @@ end
---@param r ow.Git.Repo ---@param r ow.Git.Repo
---@param path string ---@param path string
---@return ow.Git.DiffSide ---@return ow.Git.Diff.Side
local function worktree_pane(r, path) local function worktree_pane(r, path)
local buf = vim.fn.bufadd(vim.fs.joinpath(r.worktree, path)) local buf = vim.fn.bufadd(vim.fs.joinpath(r.worktree, path))
vim.fn.bufload(buf) vim.fn.bufload(buf)
return { buf = buf, name = nil } return { buf = buf, name = nil }
end end
---@param s ow.Git.SidebarState ---@param s ow.Git.StatusView.State
---@param entry ow.Git.StatusEntry ---@param entry ow.Git.Status.Entry
---@return ow.Git.DiffSide ---@return ow.Git.Diff.Side
local function index_pane(s, entry) local function index_pane(s, entry)
local rev = Revision.new({ stage = 0, path = entry.path }) local rev = Revision.new({ stage = 0, path = entry.path })
return { return {
@@ -392,39 +233,39 @@ local function index_pane(s, entry)
} }
end end
---@param s ow.Git.SidebarState ---@param s ow.Git.StatusView.State
---@param entry ow.Git.StatusEntry ---@param entry ow.Git.Status.Entry
---@return ow.Git.DiffSide? ---@return ow.Git.Diff.Side?
local function older_pane(s, entry) local function older_pane(s, entry)
if entry.section == "Staged" then if entry.kind == "staged" then
if entry.x == "A" then if entry.char == "A" then
return nil return nil
end end
return head_pane(s.repo, entry.orig or entry.path) return head_pane(s.repo, entry.orig or entry.path)
end end
if entry.section == "Unstaged" then if entry.kind == "unstaged" then
return index_pane(s, entry) return index_pane(s, entry)
end end
return nil return nil
end end
---@param s ow.Git.SidebarState ---@param s ow.Git.StatusView.State
---@param entry ow.Git.StatusEntry ---@param entry ow.Git.Status.Entry
---@return ow.Git.DiffSide? ---@return ow.Git.Diff.Side?
local function newer_pane(s, entry) local function newer_pane(s, entry)
if entry.section == "Staged" then if entry.kind == "staged" then
if entry.x == "D" then if entry.char == "D" then
return nil return nil
end end
return index_pane(s, entry) return index_pane(s, entry)
end end
if entry.section == "Unstaged" then if entry.kind == "unstaged" then
if entry.y == "D" then if entry.char == "D" then
return nil return nil
end end
return worktree_pane(s.repo, entry.path) return worktree_pane(s.repo, entry.path)
end end
if entry.section == "Untracked" then if entry.kind == "untracked" then
return worktree_pane(s.repo, entry.path) return worktree_pane(s.repo, entry.path)
end end
return nil return nil
@@ -439,14 +280,14 @@ local function reset_diff_win(win)
end) end)
end end
---@param s ow.Git.SidebarState ---@param s ow.Git.StatusView.State
---@return integer? ---@return integer?
local function invocation_win_for(s) local function invocation_win_for(s)
local win = s.invocation_win local win = s.invocation_win
if not win or not vim.api.nvim_win_is_valid(win) then if not win or not vim.api.nvim_win_is_valid(win) then
return nil return nil
end end
if win == s.sidebar_win then if win == s.win then
return nil return nil
end end
if if
@@ -458,11 +299,11 @@ local function invocation_win_for(s)
return win return win
end end
---@param s ow.Git.SidebarState ---@param s ow.Git.StatusView.State
---@param sidebar_win integer ---@param status_win integer
---@return integer? left ---@return integer? left
---@return integer? right ---@return integer? right
local function adopt_diff_wins(s, sidebar_win) local function adopt_diff_wins(s, status_win)
local left = s.diff_left_win local left = s.diff_left_win
local right = s.diff_right_win local right = s.diff_right_win
if left and not vim.api.nvim_win_is_valid(left) then if left and not vim.api.nvim_win_is_valid(left) then
@@ -475,7 +316,7 @@ local function adopt_diff_wins(s, sidebar_win)
return left, right return left, right
end end
for _, win in ipairs(vim.api.nvim_tabpage_list_wins(0)) do for _, win in ipairs(vim.api.nvim_tabpage_list_wins(0)) do
if win ~= sidebar_win then if win ~= status_win then
local role = vim.w[win].git_diff_role local role = vim.w[win].git_diff_role
if role == "left" and not left then if role == "left" and not left then
left = win left = win
@@ -487,10 +328,10 @@ local function adopt_diff_wins(s, sidebar_win)
return left, right return left, right
end end
---@param entry ow.Git.StatusEntry ---@param entry ow.Git.Status.Entry
---@return string ---@return string
local function entry_key(entry) local function entry_key(entry)
return entry.section .. "|" .. entry.path .. "|" .. (entry.orig or "") return entry.kind .. "|" .. entry.path .. "|" .. (entry.orig or "")
end end
---@param target_win integer ---@param target_win integer
@@ -506,11 +347,11 @@ local function vsplit_at(target_win, dir)
return win return win
end end
---@param s ow.Git.SidebarState ---@param s ow.Git.StatusView.State
---@param sidebar_win integer ---@param status_win integer
---@param right_win integer? ---@param right_win integer?
---@return integer ---@return integer
local function ensure_right_win(s, sidebar_win, right_win) local function ensure_right_win(s, status_win, right_win)
if right_win then if right_win then
return right_win return right_win
end end
@@ -518,46 +359,38 @@ local function ensure_right_win(s, sidebar_win, right_win)
if target then if target then
right_win = target right_win = target
else else
right_win = vsplit_at(sidebar_win, "right") right_win = vsplit_at(status_win, "right")
vim.api.nvim_win_set_width(sidebar_win, SIDEBAR_WIDTH) vim.api.nvim_win_set_width(status_win, WINDOW_WIDTH)
end end
reset_diff_win(right_win) reset_diff_win(right_win)
return right_win return right_win
end end
---@param s ow.Git.SidebarState ---@param s ow.Git.StatusView.State
---@param entry ow.Git.SidebarEntry ---@param entry ow.Git.Status.Entry
---@param focus_left boolean ---@param focus_left boolean
local function view_entry(s, entry, focus_left) local function view_entry(s, entry, focus_left)
if not entry.path then local status_win = win_for(s)
return if not status_win then
end
---@cast entry ow.Git.StatusEntry
local sidebar_win = sidebar_win_for(s)
if not sidebar_win then
return return
end end
local left = older_pane(s, entry) local left = older_pane(s, entry)
local right = newer_pane(s, entry) local right = newer_pane(s, entry)
if not left and not right then if not left and not right then
util.warning( util.warning("no content for %s entry: %s", entry.kind, entry.path)
"no content for %s entry: %s",
string.lower(entry.section),
entry.path
)
return return
end end
local key = entry_key(entry) local key = entry_key(entry)
local left_win, right_win = adopt_diff_wins(s, sidebar_win) local left_win, right_win = adopt_diff_wins(s, status_win)
local want_pair = left and right local want_pair = left and right
if s.last_shown_key == key then if s.last_shown_key == key then
local intact = (want_pair and left_win and right_win) local intact = (want_pair and left_win and right_win)
or (not want_pair and right_win and not left_win) or (not want_pair and right_win and not left_win)
if intact then if intact then
local target = focus_left and (left_win or right_win) or sidebar_win local target = focus_left and (left_win or right_win) or status_win
vim.api.nvim_set_current_win(target) vim.api.nvim_set_current_win(target)
return return
end end
@@ -569,22 +402,22 @@ local function view_entry(s, entry, focus_left)
left_win = nil left_win = nil
s.diff_left_win = nil s.diff_left_win = nil
end end
right_win = ensure_right_win(s, sidebar_win, right_win) right_win = ensure_right_win(s, status_win, right_win)
s.diff_right_win = right_win s.diff_right_win = right_win
vim.w[right_win].git_diff_role = "right" vim.w[right_win].git_diff_role = "right"
local side = left or right local side = left or right
---@cast side ow.Git.DiffSide ---@cast side ow.Git.Diff.Side
diff.set_diff(right_win, false) diff.set_diff(right_win, false)
vim.api.nvim_win_set_buf(right_win, side.buf) vim.api.nvim_win_set_buf(right_win, side.buf)
if side.name then if side.name then
util.set_buf_name(side.buf, side.name) util.set_buf_name(side.buf, side.name)
end end
s.last_shown_key = key s.last_shown_key = key
vim.api.nvim_set_current_win(focus_left and right_win or sidebar_win) vim.api.nvim_set_current_win(focus_left and right_win or status_win)
return return
end end
---@cast left ow.Git.DiffSide ---@cast left ow.Git.Diff.Side
---@cast right ow.Git.DiffSide ---@cast right ow.Git.Diff.Side
if left_win and not right_win then if left_win and not right_win then
right_win = vsplit_at(left_win, "right") right_win = vsplit_at(left_win, "right")
@@ -593,7 +426,7 @@ local function view_entry(s, entry, focus_left)
left_win = vsplit_at(right_win, "left") left_win = vsplit_at(right_win, "left")
reset_diff_win(left_win) reset_diff_win(left_win)
elseif not (left_win or right_win) then elseif not (left_win or right_win) then
right_win = ensure_right_win(s, sidebar_win, nil) right_win = ensure_right_win(s, status_win, nil)
left_win = vsplit_at(right_win, "left") left_win = vsplit_at(right_win, "left")
reset_diff_win(left_win) reset_diff_win(left_win)
local combined = vim.api.nvim_win_get_width(left_win) local combined = vim.api.nvim_win_get_width(left_win)
@@ -611,7 +444,7 @@ local function view_entry(s, entry, focus_left)
diff.update_pair(left_win, right_win, { left = left, right = right }) diff.update_pair(left_win, right_win, { left = left, right = right })
s.last_shown_key = key s.last_shown_key = key
vim.api.nvim_set_current_win(focus_left and left_win or sidebar_win) vim.api.nvim_set_current_win(focus_left and left_win or status_win)
end end
---@param focus_left boolean ---@param focus_left boolean
@@ -625,11 +458,10 @@ end
local function action_stage() local function action_stage()
local s, entry = current_entry(vim.api.nvim_get_current_buf()) local s, entry = current_entry(vim.api.nvim_get_current_buf())
if not s or not entry or not entry.path then if not s or not entry then
return return
end end
---@cast entry ow.Git.StatusEntry if entry.kind == "staged" then
if entry.section == "Staged" then
return return
end end
vim.system( vim.system(
@@ -645,11 +477,10 @@ end
local function action_unstage() local function action_unstage()
local s, entry = current_entry(vim.api.nvim_get_current_buf()) local s, entry = current_entry(vim.api.nvim_get_current_buf())
if not s or not entry or not entry.path then if not s or not entry then
return return
end end
---@cast entry ow.Git.StatusEntry if entry.kind ~= "staged" then
if entry.section ~= "Staged" then
return return
end end
local cmd = { "git", "restore", "--staged", "--" } local cmd = { "git", "restore", "--staged", "--" }
@@ -673,17 +504,16 @@ end
local function action_discard() local function action_discard()
local s, entry = current_entry(vim.api.nvim_get_current_buf()) local s, entry = current_entry(vim.api.nvim_get_current_buf())
if not s or not entry or not entry.path then if not s or not entry then
return return
end end
---@cast entry ow.Git.StatusEntry if entry.kind == "staged" then
if entry.section == "Staged" then
util.warning("file has staged changes, unstage first with 'u'") util.warning("file has staged changes, unstage first with 'u'")
return return
end end
local prompt, action local prompt, action
if entry.section == "Untracked" then if entry.kind == "untracked" then
local is_dir = entry.path:sub(-1) == "/" local is_dir = entry.path:sub(-1) == "/"
prompt = string.format( prompt = string.format(
"Delete untracked %s %s?", "Delete untracked %s %s?",
@@ -698,7 +528,7 @@ local function action_discard()
end end
refresh(vim.api.nvim_get_current_buf()) refresh(vim.api.nvim_get_current_buf())
end end
elseif entry.section == "Unstaged" then elseif entry.kind == "unstaged" then
prompt = string.format("Discard changes to %s?", entry.path) prompt = string.format("Discard changes to %s?", entry.path)
action = function() action = function()
vim.system( vim.system(
@@ -725,7 +555,7 @@ end
local function action_help() local function action_help()
print(table.concat({ print(table.concat({
"git status sidebar", "git status window",
" <Tab> preview diff (keep focus)", " <Tab> preview diff (keep focus)",
" <CR> open diff (focus left pane)", " <CR> open diff (focus left pane)",
" s stage file", " s stage file",
@@ -737,7 +567,7 @@ end
---@param r ow.Git.Repo ---@param r ow.Git.Repo
local function open(r) local function open(r)
local existing = find_sidebar() local existing = find_view()
if existing then if existing then
vim.api.nvim_set_current_win(existing) vim.api.nvim_set_current_win(existing)
return return
@@ -745,7 +575,7 @@ local function open(r)
local previous_win = vim.api.nvim_get_current_win() local previous_win = vim.api.nvim_get_current_win()
local bufnr, win = util.new_scratch({ split = "left" }) local bufnr, win = util.new_scratch({ split = "left" })
vim.bo[bufnr].filetype = "gitsidebar" vim.bo[bufnr].filetype = "gitstatus"
vim.wo[win].number = false vim.wo[win].number = false
vim.wo[win].relativenumber = false vim.wo[win].relativenumber = false
@@ -753,12 +583,12 @@ local function open(r)
vim.wo[win].signcolumn = "no" vim.wo[win].signcolumn = "no"
vim.wo[win].cursorline = true vim.wo[win].cursorline = true
vim.wo[win].winfixwidth = true vim.wo[win].winfixwidth = true
vim.api.nvim_win_set_width(win, SIDEBAR_WIDTH) vim.api.nvim_win_set_width(win, WINDOW_WIDTH)
state[bufnr] = { state[bufnr] = {
repo = r, repo = r,
lines = {}, lines = {},
sidebar_win = win, win = win,
invocation_win = previous_win, invocation_win = previous_win,
} }
@@ -781,8 +611,8 @@ local function open(r)
k("X", action_discard, "Discard worktree changes") k("X", action_discard, "Discard worktree changes")
k("g?", action_help, "Help") k("g?", action_help, "Help")
state[bufnr].unsubscribe = r:on_refresh(function(_, porcelain_stdout) state[bufnr].unsubscribe = r:on("refresh", function()
refresh(bufnr, porcelain_stdout) refresh(bufnr)
end) end)
vim.api.nvim_create_autocmd({ "BufWipeout", "BufDelete" }, { vim.api.nvim_create_autocmd({ "BufWipeout", "BufDelete" }, {
buffer = bufnr, buffer = bufnr,
@@ -804,12 +634,12 @@ local function open(r)
end end
function M.toggle() function M.toggle()
local sidebar_win = find_sidebar() local status_win = find_view()
if sidebar_win then if status_win then
vim.api.nvim_win_close(sidebar_win, false) vim.api.nvim_win_close(status_win, false)
return return
end end
local r = repo.find() local r = repo.resolve()
if not r then if not r then
util.warning("not in a git repository") util.warning("not in a git repository")
return return
+31
View File
@@ -0,0 +1,31 @@
local repo = require("git.repo")
local M = {}
---@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
return ""
end
local parts = {}
for _, e in ipairs(list) 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)
return M
+42 -6
View File
@@ -1,11 +1,11 @@
local M = {} local M = {}
---@class ow.Git.ScratchOpts ---@class ow.Git.Util.ScratchOpts
---@field name string? ---@field name string?
---@field bufhidden ("hide"|"wipe")? ---@field bufhidden ("hide"|"wipe")?
---@param buf integer ---@param buf integer
---@param opts ow.Git.ScratchOpts ---@param opts ow.Git.Util.ScratchOpts
local function setup_scratch(buf, opts) local function setup_scratch(buf, opts)
vim.bo[buf].buftype = "nofile" vim.bo[buf].buftype = "nofile"
vim.bo[buf].bufhidden = opts.bufhidden or "wipe" vim.bo[buf].bufhidden = opts.bufhidden or "wipe"
@@ -43,10 +43,10 @@ function M.place_buf(buf, split)
return win return win
end end
---@class ow.Git.NewScratchOpts : ow.Git.ScratchOpts ---@class ow.Git.Util.NewScratchOpts : ow.Git.Util.ScratchOpts
---@field split (false|"above"|"below"|"left"|"right")? ---@field split (false|"above"|"below"|"left"|"right")?
---@param opts ow.Git.NewScratchOpts? ---@param opts ow.Git.Util.NewScratchOpts?
---@return integer buf ---@return integer buf
---@return integer win ---@return integer win
function M.new_scratch(opts) function M.new_scratch(opts)
@@ -170,14 +170,14 @@ function M.debounce(fn, delay)
} }
end end
---@class ow.Git.ExecOpts ---@class ow.Git.Util.ExecOpts
---@field cwd string? ---@field cwd string?
---@field stdin string? ---@field stdin string?
---@field silent boolean? ---@field silent boolean?
---@field on_done fun(stdout: string?)? ---@field on_done fun(stdout: string?)?
---@param cmd string[] ---@param cmd string[]
---@param opts ow.Git.ExecOpts? ---@param opts ow.Git.Util.ExecOpts?
---@return string? ---@return string?
function M.exec(cmd, opts) function M.exec(cmd, opts)
opts = opts or {} opts = opts or {}
@@ -209,4 +209,40 @@ function M.exec(cmd, opts)
return handle(vim.system(cmd, sys_opts):wait()) return handle(vim.system(cmd, sys_opts):wait())
end end
---@class ow.Git.Util.Emitter<T>
---@field private _listeners table<T, (fun(...))[]>
local Emitter = {}
Emitter.__index = Emitter
---@return ow.Git.Util.Emitter
function Emitter.new()
return setmetatable({ _listeners = {} }, Emitter)
end
---@param event T
---@param fn fun(...)
---@return fun() unsubscribe
function Emitter:on(event, fn)
local list = self._listeners[event] or {}
self._listeners[event] = list
table.insert(list, fn)
return function()
for i, f in ipairs(list) do
if f == fn then
table.remove(list, i)
return
end
end
end
end
---@param event T
function Emitter:emit(event, ...)
for _, fn in ipairs(self._listeners[event] or {}) do
fn(...)
end
end
M.Emitter = Emitter
return M return M
+75 -28
View File
@@ -41,29 +41,88 @@ end)
---@class nvim_tree.api.decorator.UserDecorator ---@class nvim_tree.api.decorator.UserDecorator
local Decorator = require("nvim-tree.api").Decorator local Decorator = require("nvim-tree.api").Decorator
---@class GitIgnoreDecorator: nvim_tree.api.decorator.UserDecorator local repo = require("git.repo")
local GitIgnoreDecorator = Decorator:extend()
function GitIgnoreDecorator:new() repo.on("refresh", function()
require("nvim-tree.api").tree.reload()
end)
do
local events = require("nvim-tree.api").events
local function on_change(data)
if not data then
return
end
local seen = {}
local function fire(path)
if not path then
return
end
local r = repo.find(path)
if r and not seen[r] then
seen[r] = true
r:refresh()
end
end
fire(data.fname)
fire(data.folder_name)
fire(data.old_name)
fire(data.new_name)
end
events.subscribe(events.Event.FileCreated, on_change)
events.subscribe(events.Event.FileRemoved, on_change)
events.subscribe(events.Event.FolderCreated, on_change)
events.subscribe(events.Event.FolderRemoved, on_change)
events.subscribe(events.Event.NodeRenamed, on_change)
end
---@class ow.GitDecorator: nvim_tree.api.Decorator
local GitDecorator = Decorator:extend()
function GitDecorator:new()
self.enabled = true self.enabled = true
self.icon_placement = "after"
self.highlight_range = "name" self.highlight_range = "name"
self.icon_placement = "none"
self.file_hl = "NvimTreeGitFileIgnoredHL"
self.folder_hl = "NvimTreeGitFolderIgnoredHL"
end end
---@param node Node ---@param node Node
---@return string? highlight_group ---@return ow.Git.Status.Entry[]?
function GitIgnoreDecorator:highlight_group(node) local function entries_for(node)
local status = node.git_status local r = repo.find(node.absolute_path)
if not status then if not r then
return return
end end
local rel = vim.fs.relpath(r.worktree, node.absolute_path)
return rel and r.status.entries[rel]
end
if status.file == "!!" then ---@param node Node
return self.file_hl ---@return { str: string, hl: string[] }[]?
elseif status.dir and status.dir.direct == "!!" then function GitDecorator.icons(_, node)
return self.folder_hl local list = entries_for(node)
if not list then
return
end
local out = {}
for _, entry in ipairs(list) do
if entry.kind ~= "ignored" then
table.insert(out, { str = entry.char, hl = { entry.hl } })
end
end
return out
end
---@param node Node
---@return string?
function GitDecorator.highlight_group(_, node)
local list = entries_for(node)
if not list then
return
end
for _, entry in ipairs(list) do
if entry.kind == "ignored" then
return entry.hl
end
end end
end end
@@ -120,8 +179,7 @@ require("nvim-tree").setup({
end, end,
special_files = {}, special_files = {},
decorators = { decorators = {
"Git", GitDecorator,
GitIgnoreDecorator,
"Open", "Open",
"Modified", "Modified",
"Bookmark", "Bookmark",
@@ -140,7 +198,6 @@ require("nvim-tree").setup({
}, },
}, },
icons = { icons = {
git_placement = "after",
diagnostics_placement = "signcolumn", diagnostics_placement = "signcolumn",
bookmarks_placement = "after", bookmarks_placement = "after",
symlink_arrow = " -> ", symlink_arrow = " -> ",
@@ -157,15 +214,6 @@ require("nvim-tree").setup({
}, },
glyphs = { glyphs = {
modified = "*", modified = "*",
git = {
unstaged = "M",
staged = "M",
unmerged = "!",
renamed = "R",
untracked = "?",
deleted = "D",
ignored = " ",
},
}, },
}, },
}, },
@@ -186,7 +234,6 @@ require("nvim-tree").setup({
enable = true, enable = true,
}, },
filters = { filters = {
git_ignored = false,
custom = { "^\\.git$" }, custom = { "^\\.git$" },
}, },
live_filter = { live_filter = {
@@ -219,7 +266,7 @@ require("nvim-tree").setup({
}, },
sync_root_with_cwd = true, sync_root_with_cwd = true,
git = { git = {
show_on_open_dirs = false, enable = false,
}, },
}) })
+1 -3
View File
@@ -53,9 +53,7 @@ local highlights = {
DiffAdd = { bg = "#1a2f22" }, DiffAdd = { bg = "#1a2f22" },
DiffChange = { bg = "#15304a" }, DiffChange = { bg = "#15304a" },
DiffDelete = { bg = "#311c1e" }, DiffDelete = { bg = "#311c1e" },
-- GitDeleted = { fg = c.red }, Changed = { fg = c.yellow },
-- GitUnstaged = { fg = c.yellow },
-- GitUntracked = { fg = c.green },
} }
for kind, color in pairs(completion_kind_colors) do for kind, color in pairs(completion_kind_colors) do
highlights["LspKind" .. kind] = { fg = color } highlights["LspKind" .. kind] = { fg = color }
-44
View File
@@ -1,44 +0,0 @@
if exists("b:current_syntax")
finish
endif
syntax match gitSidebarLabel /\v^(Head|Push)\ze:/
syntax match gitSidebarBranch /\v(^(Head|Push):\s+)@<=\S+/
syntax match gitSidebarAhead /\v\+\d+/
syntax match gitSidebarBehind /\v-\d+/
syntax region gitSidebarUntrackedHeader start=/\v^Untracked>/ end=/\v^$/
syntax region gitSidebarUnstagedHeader start=/\v^Unstaged>/ end=/\v^$/
syntax region gitSidebarStagedHeader start=/\v^Staged>/ end=/\v^$/
syntax region gitSidebarUnmergedHeader start=/\v^Unmerged>/ end=/\v^$/
syntax region gitSidebarUnpushedHeader start=/\v^Unpushed>/ end=/\v^$/
syntax region gitSidebarUnpulledHeader start=/\v^Unpulled>/ end=/\v^$/
syntax match gitSidebarUntrackedLabel /\v^Untracked/ contained containedin=gitSidebarUntrackedHeader
syntax match gitSidebarUnstagedLabel /\v^Unstaged/ contained containedin=gitSidebarUnstagedHeader
syntax match gitSidebarStagedLabel /\v^Staged/ contained containedin=gitSidebarStagedHeader
syntax match gitSidebarUnmergedLabel /\v^Unmerged/ contained containedin=gitSidebarUnmergedHeader
syntax match gitSidebarUnpushedLabel /\v^Unpushed/ contained containedin=gitSidebarUnpushedHeader
syntax match gitSidebarUnpulledLabel /\v^Unpulled/ contained containedin=gitSidebarUnpulledHeader
syntax match gitSidebarHeaderCount /\v\(\zs\d+\ze\)/ contained containedin=gitSidebarUntrackedHeader,
\ gitSidebarUnstagedHeader,
\ gitSidebarStagedHeader,
\ gitSidebarUnmergedHeader,
\ gitSidebarUnpushedHeader,
\ gitSidebarUnpulledHeader
highlight default link gitSidebarLabel Label
highlight default link gitSidebarBranch None
highlight default link gitSidebarAhead GitUnpushed
highlight default link gitSidebarBehind GitUnpulled
highlight default link gitSidebarHeaderCount Number
highlight default link gitSidebarUntrackedLabel gitSidebarLabel
highlight default link gitSidebarUnstagedLabel gitSidebarLabel
highlight default link gitSidebarStagedLabel gitSidebarLabel
highlight default link gitSidebarUnmergedLabel gitSidebarLabel
highlight default link gitSidebarUnpushedLabel gitSidebarLabel
highlight default link gitSidebarUnpulledLabel gitSidebarLabel
let b:current_syntax = "gitSidebar"
+44
View File
@@ -0,0 +1,44 @@
if exists("b:current_syntax")
finish
endif
syntax match gitStatusLabel /\v^(Head|Push)\ze:/
syntax match gitStatusBranch /\v(^(Head|Push):\s+)@<=\S+/
syntax match gitStatusAhead /\v\+\d+/
syntax match gitStatusBehind /\v-\d+/
syntax region gitStatusUntrackedHeader start=/\v^Untracked>/ end=/\v^$/
syntax region gitStatusUnstagedHeader start=/\v^Unstaged>/ end=/\v^$/
syntax region gitStatusStagedHeader start=/\v^Staged>/ end=/\v^$/
syntax region gitStatusUnmergedHeader start=/\v^Unmerged>/ end=/\v^$/
syntax region gitStatusUnpushedHeader start=/\v^Unpushed>/ end=/\v^$/
syntax region gitStatusUnpulledHeader start=/\v^Unpulled>/ end=/\v^$/
syntax match gitStatusUntrackedLabel /\v^Untracked/ contained containedin=gitStatusUntrackedHeader
syntax match gitStatusUnstagedLabel /\v^Unstaged/ contained containedin=gitStatusUnstagedHeader
syntax match gitStatusStagedLabel /\v^Staged/ contained containedin=gitStatusStagedHeader
syntax match gitStatusUnmergedLabel /\v^Unmerged/ contained containedin=gitStatusUnmergedHeader
syntax match gitStatusUnpushedLabel /\v^Unpushed/ contained containedin=gitStatusUnpushedHeader
syntax match gitStatusUnpulledLabel /\v^Unpulled/ contained containedin=gitStatusUnpulledHeader
syntax match gitStatusHeaderCount /\v\(\zs\d+\ze\)/ contained containedin=gitStatusUntrackedHeader,
\ gitStatusUnstagedHeader,
\ gitStatusStagedHeader,
\ gitStatusUnmergedHeader,
\ gitStatusUnpushedHeader,
\ gitStatusUnpulledHeader
highlight default link gitStatusLabel Label
highlight default link gitStatusBranch None
highlight default link gitStatusAhead GitUnpushed
highlight default link gitStatusBehind GitUnpulled
highlight default link gitStatusHeaderCount Number
highlight default link gitStatusUntrackedLabel gitStatusLabel
highlight default link gitStatusUnstagedLabel gitStatusLabel
highlight default link gitStatusStagedLabel gitStatusLabel
highlight default link gitStatusUnmergedLabel gitStatusLabel
highlight default link gitStatusUnpushedLabel gitStatusLabel
highlight default link gitStatusUnpulledLabel gitStatusLabel
let b:current_syntax = "gitStatus"