feat(git): add custom status sidebar and diff viewer
This commit is contained in:
@@ -14,7 +14,7 @@ syntax match gitlogGraphLine /^[*|\\\/_ ]\+$/
|
|||||||
\ contains=gitlogGraph
|
\ contains=gitlogGraph
|
||||||
|
|
||||||
highlight default link gitlogGraph Comment
|
highlight default link gitlogGraph Comment
|
||||||
highlight default link gitlogHash Identifier
|
highlight default link gitlogHash GitSha
|
||||||
highlight default link gitlogDate Number
|
highlight default link gitlogDate Number
|
||||||
highlight default link gitlogAuthor String
|
highlight default link gitlogAuthor String
|
||||||
highlight default link gitlogRef Constant
|
highlight default link gitlogRef Constant
|
||||||
|
|||||||
@@ -0,0 +1,73 @@
|
|||||||
|
local repo = require("git.repo")
|
||||||
|
|
||||||
|
local HIGHLIGHTS = {
|
||||||
|
GitDeleted = "Removed",
|
||||||
|
GitIgnored = "Comment",
|
||||||
|
GitUnstaged = "Changed",
|
||||||
|
GitRenamed = "GitStaged",
|
||||||
|
GitSha = "Identifier",
|
||||||
|
GitStaged = "Constant",
|
||||||
|
GitUnmerged = "Todo",
|
||||||
|
GitUnpulled = "Removed",
|
||||||
|
GitUnpushed = "Added",
|
||||||
|
GitUntracked = "Added",
|
||||||
|
}
|
||||||
|
|
||||||
|
local M = {}
|
||||||
|
|
||||||
|
function M.status()
|
||||||
|
return vim.b.git_status or ""
|
||||||
|
end
|
||||||
|
|
||||||
|
---@param path string
|
||||||
|
---@return string?
|
||||||
|
function M.head(path)
|
||||||
|
return repo.head(path)
|
||||||
|
end
|
||||||
|
|
||||||
|
function M.setup()
|
||||||
|
for name, link in pairs(HIGHLIGHTS) do
|
||||||
|
vim.api.nvim_set_hl(0, name, { link = link, default = true })
|
||||||
|
end
|
||||||
|
vim.filetype.add({
|
||||||
|
pattern = {
|
||||||
|
["git://[^/]+/(.+)"] = function(_, bufnr, inner)
|
||||||
|
return vim.filetype.match({ filename = inner, buf = bufnr })
|
||||||
|
end,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
local group = vim.api.nvim_create_augroup("ow.git", { clear = true })
|
||||||
|
vim.api.nvim_create_autocmd(
|
||||||
|
{ "BufReadPost", "BufNewFile", "BufWritePost", "FileChangedShellPost" },
|
||||||
|
{
|
||||||
|
group = group,
|
||||||
|
callback = function(args)
|
||||||
|
repo.refresh_buf(args.buf)
|
||||||
|
end,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
vim.api.nvim_create_autocmd({ "BufDelete", "BufWipeout" }, {
|
||||||
|
group = group,
|
||||||
|
callback = function(args)
|
||||||
|
repo.unregister(args.buf)
|
||||||
|
end,
|
||||||
|
})
|
||||||
|
vim.api.nvim_create_autocmd("FocusGained", {
|
||||||
|
group = group,
|
||||||
|
callback = function()
|
||||||
|
repo.refresh_buf(vim.api.nvim_get_current_buf())
|
||||||
|
end,
|
||||||
|
})
|
||||||
|
vim.api.nvim_create_autocmd("VimLeavePre", {
|
||||||
|
group = group,
|
||||||
|
callback = function()
|
||||||
|
repo.stop_all()
|
||||||
|
end,
|
||||||
|
})
|
||||||
|
|
||||||
|
vim.keymap.set("n", "<leader>gg", function()
|
||||||
|
require("git.status_win").toggle()
|
||||||
|
end, { desc = "Toggle git status sidebar" })
|
||||||
|
end
|
||||||
|
|
||||||
|
return M
|
||||||
@@ -1,15 +1,5 @@
|
|||||||
local util = require("util")
|
local util = require("util")
|
||||||
|
|
||||||
local HIGHLIGHTS = {
|
|
||||||
GitDeleted = "Statement",
|
|
||||||
GitDirty = "Statement",
|
|
||||||
GitIgnored = "Comment",
|
|
||||||
GitMerge = "Constant",
|
|
||||||
GitNew = "PreProc",
|
|
||||||
GitRenamed = "PreProc",
|
|
||||||
GitStaged = "Constant",
|
|
||||||
}
|
|
||||||
|
|
||||||
local UNMERGED = {
|
local UNMERGED = {
|
||||||
DD = true,
|
DD = true,
|
||||||
AU = true,
|
AU = true,
|
||||||
@@ -20,36 +10,44 @@ local UNMERGED = {
|
|||||||
UU = true,
|
UU = true,
|
||||||
}
|
}
|
||||||
|
|
||||||
---@param code string
|
---@param code string porcelain v1 XY code
|
||||||
---@return string?
|
---@return string? char
|
||||||
local function format(code)
|
---@return string? hl_group
|
||||||
|
local function indicator(code)
|
||||||
if code == "" then
|
if code == "" then
|
||||||
return nil
|
return nil
|
||||||
end
|
end
|
||||||
local char, hl
|
|
||||||
if code == "??" then
|
if code == "??" then
|
||||||
char, hl = "?", "GitNew"
|
return "?", "GitUntracked"
|
||||||
elseif code == "!!" then
|
end
|
||||||
char, hl = "!", "GitIgnored"
|
if code == "!!" then
|
||||||
elseif UNMERGED[code] then
|
return "!", "GitIgnored"
|
||||||
char, hl = "U", "GitMerge"
|
end
|
||||||
else
|
if UNMERGED[code] then
|
||||||
|
return "U", "GitUnmerged"
|
||||||
|
end
|
||||||
local x, y = code:sub(1, 1), code:sub(2, 2)
|
local x, y = code:sub(1, 1), code:sub(2, 2)
|
||||||
if x == "R" or y == "R" then
|
if x == "R" or y == "R" then
|
||||||
char, hl = "R", "GitRenamed"
|
return "R", "GitRenamed"
|
||||||
elseif y == "M" or y == "T" then
|
|
||||||
char, hl = "M", "GitDirty"
|
|
||||||
elseif y == "D" then
|
|
||||||
char, hl = "D", "GitDeleted"
|
|
||||||
elseif y == " " and x == "D" then
|
|
||||||
char, hl = "D", "GitStaged"
|
|
||||||
elseif y == " " and x == "A" then
|
|
||||||
char, hl = "A", "GitStaged"
|
|
||||||
elseif y == " " and x ~= " " then
|
|
||||||
char, hl = "M", "GitStaged"
|
|
||||||
else
|
|
||||||
char, hl = "M", "GitDirty"
|
|
||||||
end
|
end
|
||||||
|
if y == " " and x ~= " " then
|
||||||
|
return x, "GitStaged"
|
||||||
|
end
|
||||||
|
if y == "D" then
|
||||||
|
return "D", "GitDeleted"
|
||||||
|
end
|
||||||
|
if y == "M" or y == "T" then
|
||||||
|
return "M", "GitUnstaged"
|
||||||
|
end
|
||||||
|
return "M", "GitUnstaged"
|
||||||
|
end
|
||||||
|
|
||||||
|
---@param code string
|
||||||
|
---@return string?
|
||||||
|
local function format(code)
|
||||||
|
local char, hl = indicator(code)
|
||||||
|
if not char then
|
||||||
|
return nil
|
||||||
end
|
end
|
||||||
return string.format("%%#%s#%s%%*", hl, char)
|
return string.format("%%#%s#%s%%*", hl, char)
|
||||||
end
|
end
|
||||||
@@ -243,46 +241,45 @@ local function refresh_buf(buf)
|
|||||||
repo:refresh()
|
repo:refresh()
|
||||||
end
|
end
|
||||||
|
|
||||||
local M = {}
|
local function stop_all()
|
||||||
|
|
||||||
function M.status()
|
|
||||||
return vim.b.git_status or ""
|
|
||||||
end
|
|
||||||
|
|
||||||
function M.setup()
|
|
||||||
for name, link in pairs(HIGHLIGHTS) do
|
|
||||||
vim.api.nvim_set_hl(0, name, { link = link, default = true })
|
|
||||||
end
|
|
||||||
local group = vim.api.nvim_create_augroup("ow.git", { clear = true })
|
|
||||||
vim.api.nvim_create_autocmd(
|
|
||||||
{ "BufReadPost", "BufNewFile", "BufWritePost", "FileChangedShellPost" },
|
|
||||||
{
|
|
||||||
group = group,
|
|
||||||
callback = function(args)
|
|
||||||
refresh_buf(args.buf)
|
|
||||||
end,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
vim.api.nvim_create_autocmd({ "BufDelete", "BufWipeout" }, {
|
|
||||||
group = group,
|
|
||||||
callback = function(args)
|
|
||||||
unregister(args.buf)
|
|
||||||
end,
|
|
||||||
})
|
|
||||||
vim.api.nvim_create_autocmd("FocusGained", {
|
|
||||||
group = group,
|
|
||||||
callback = function()
|
|
||||||
refresh_buf(vim.api.nvim_get_current_buf())
|
|
||||||
end,
|
|
||||||
})
|
|
||||||
vim.api.nvim_create_autocmd("VimLeavePre", {
|
|
||||||
group = group,
|
|
||||||
callback = function()
|
|
||||||
for _, repo in pairs(repo_by_gitdir) do
|
for _, repo in pairs(repo_by_gitdir) do
|
||||||
repo:stop_watcher()
|
repo:stop_watcher()
|
||||||
end
|
end
|
||||||
end,
|
|
||||||
})
|
|
||||||
end
|
end
|
||||||
|
|
||||||
return M
|
---@param path string
|
||||||
|
---@return string?
|
||||||
|
local function head(path)
|
||||||
|
local gitdir = resolve(path)
|
||||||
|
if not gitdir then
|
||||||
|
return nil
|
||||||
|
end
|
||||||
|
local f = io.open(vim.fs.joinpath(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
|
||||||
|
|
||||||
|
return {
|
||||||
|
UNMERGED = UNMERGED,
|
||||||
|
head = head,
|
||||||
|
indicator = indicator,
|
||||||
|
refresh_buf = refresh_buf,
|
||||||
|
resolve = resolve,
|
||||||
|
stop_all = stop_all,
|
||||||
|
unregister = unregister,
|
||||||
|
}
|
||||||
@@ -0,0 +1,915 @@
|
|||||||
|
local log = require("log")
|
||||||
|
local repo = require("git.repo")
|
||||||
|
|
||||||
|
local M = {}
|
||||||
|
|
||||||
|
local SECTIONS = {
|
||||||
|
"Untracked",
|
||||||
|
"Unstaged",
|
||||||
|
"Staged",
|
||||||
|
"Unmerged",
|
||||||
|
"Unpushed",
|
||||||
|
"Unpulled",
|
||||||
|
}
|
||||||
|
local SIDEBAR_WIDTH = 50
|
||||||
|
|
||||||
|
---@class ow.Git.FileEntry
|
||||||
|
---@field section string
|
||||||
|
---@field path string
|
||||||
|
---@field orig string?
|
||||||
|
---@field x string?
|
||||||
|
---@field y string?
|
||||||
|
|
||||||
|
---@class ow.Git.CommitEntry
|
||||||
|
---@field section string
|
||||||
|
---@field sha string
|
||||||
|
---@field subject string?
|
||||||
|
|
||||||
|
---@alias ow.Git.StatusEntry ow.Git.FileEntry | ow.Git.CommitEntry
|
||||||
|
|
||||||
|
---@class ow.Git.StatusState
|
||||||
|
---@field gitdir string
|
||||||
|
---@field worktree string
|
||||||
|
---@field lines table<integer, ow.Git.StatusEntry>
|
||||||
|
---@field diff_left_win integer?
|
||||||
|
---@field diff_right_win integer?
|
||||||
|
---@field user_aucmd integer?
|
||||||
|
---@field last_shown_key string?
|
||||||
|
|
||||||
|
---@type table<integer, ow.Git.StatusState>
|
||||||
|
local state = {}
|
||||||
|
|
||||||
|
local group =
|
||||||
|
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? bufnr
|
||||||
|
local function find_sidebar()
|
||||||
|
for _, win in ipairs(vim.api.nvim_tabpage_list_wins(0)) do
|
||||||
|
local buf = vim.api.nvim_win_get_buf(win)
|
||||||
|
if vim.bo[buf].filetype == "gitstatus" then
|
||||||
|
return win, buf
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
---@param entry ow.Git.StatusEntry
|
||||||
|
---@return string?
|
||||||
|
local function entry_code(entry)
|
||||||
|
if entry.section == "Untracked" then
|
||||||
|
return "??"
|
||||||
|
elseif entry.section == "Unmerged" then
|
||||||
|
return (entry.x or " ") .. (entry.y or " ")
|
||||||
|
elseif entry.section == "Staged" then
|
||||||
|
return (entry.x or " ") .. " "
|
||||||
|
elseif entry.section == "Unstaged" then
|
||||||
|
return " " .. (entry.y or " ")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
---@param entry ow.Git.StatusEntry
|
||||||
|
---@return string? line
|
||||||
|
---@return string? hl_group
|
||||||
|
---@return integer? hl_len byte length of the symbol portion at column 2
|
||||||
|
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 = repo.indicator(code)
|
||||||
|
if not char then
|
||||||
|
return nil
|
||||||
|
end
|
||||||
|
local label = entry.orig and (entry.orig .. " -> " .. entry.path)
|
||||||
|
or entry.path
|
||||||
|
return string.format(" %s %s", char, label), hl, #char
|
||||||
|
end
|
||||||
|
|
||||||
|
---@class ow.Git.BranchInfo
|
||||||
|
---@field head string?
|
||||||
|
---@field upstream string?
|
||||||
|
---@field ahead integer
|
||||||
|
---@field behind integer
|
||||||
|
|
||||||
|
---@param line string '## branch.line' from porcelain v1
|
||||||
|
---@return ow.Git.BranchInfo
|
||||||
|
local function 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 worktree string
|
||||||
|
---@param callback fun(branch: ow.Git.BranchInfo, groups: table<string, ow.Git.StatusEntry[]>)
|
||||||
|
local function fetch_status(worktree, callback)
|
||||||
|
vim.system({
|
||||||
|
"git",
|
||||||
|
"-c",
|
||||||
|
"core.quotePath=false",
|
||||||
|
"status",
|
||||||
|
"--porcelain=v1",
|
||||||
|
"--branch",
|
||||||
|
}, { cwd = worktree, text = true }, function(obj)
|
||||||
|
vim.schedule(function()
|
||||||
|
local branch = { ahead = 0, behind = 0 }
|
||||||
|
local groups = {
|
||||||
|
Untracked = {},
|
||||||
|
Unstaged = {},
|
||||||
|
Staged = {},
|
||||||
|
Unmerged = {},
|
||||||
|
Unpushed = {},
|
||||||
|
Unpulled = {},
|
||||||
|
}
|
||||||
|
if obj.code == 0 then
|
||||||
|
for line in (obj.stdout or ""):gmatch("[^\r\n]+") do
|
||||||
|
if line:sub(1, 2) == "##" then
|
||||||
|
branch = parse_branch_line(line)
|
||||||
|
else
|
||||||
|
local x = line:sub(1, 1)
|
||||||
|
local y = line:sub(2, 2)
|
||||||
|
local rest = line:sub(4)
|
||||||
|
local orig
|
||||||
|
local arrow = rest:find(" -> ", 1, true)
|
||||||
|
if arrow then
|
||||||
|
orig = rest:sub(1, arrow - 1)
|
||||||
|
rest = rest:sub(arrow + 4)
|
||||||
|
end
|
||||||
|
local entry = {
|
||||||
|
section = nil,
|
||||||
|
path = rest,
|
||||||
|
orig = orig,
|
||||||
|
x = x,
|
||||||
|
y = y,
|
||||||
|
}
|
||||||
|
if x == "?" and y == "?" then
|
||||||
|
entry.section = "Untracked"
|
||||||
|
table.insert(groups.Untracked, entry)
|
||||||
|
elseif repo.UNMERGED[x .. y] then
|
||||||
|
entry.section = "Unmerged"
|
||||||
|
table.insert(groups.Unmerged, entry)
|
||||||
|
else
|
||||||
|
if x ~= " " then
|
||||||
|
table.insert(
|
||||||
|
groups.Staged,
|
||||||
|
vim.tbl_extend(
|
||||||
|
"force",
|
||||||
|
entry,
|
||||||
|
{ section = "Staged" }
|
||||||
|
)
|
||||||
|
)
|
||||||
|
end
|
||||||
|
if y ~= " " then
|
||||||
|
table.insert(
|
||||||
|
groups.Unstaged,
|
||||||
|
vim.tbl_extend(
|
||||||
|
"force",
|
||||||
|
entry,
|
||||||
|
{ section = "Unstaged" }
|
||||||
|
)
|
||||||
|
)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
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
|
||||||
|
if #fetches == 0 then
|
||||||
|
callback(branch, groups)
|
||||||
|
return
|
||||||
|
end
|
||||||
|
local pending = #fetches
|
||||||
|
for _, f in ipairs(fetches) do
|
||||||
|
vim.system({
|
||||||
|
"git",
|
||||||
|
"log",
|
||||||
|
"--format=%h %s",
|
||||||
|
f.range,
|
||||||
|
}, { cwd = worktree, text = true }, function(
|
||||||
|
log_obj
|
||||||
|
)
|
||||||
|
vim.schedule(function()
|
||||||
|
if log_obj.code == 0 then
|
||||||
|
for line in
|
||||||
|
(log_obj.stdout or ""):gmatch("[^\r\n]+")
|
||||||
|
do
|
||||||
|
local sha, subject =
|
||||||
|
line:match("^(%S+)%s+(.+)$")
|
||||||
|
if sha then
|
||||||
|
table.insert(groups[f.section], {
|
||||||
|
section = f.section,
|
||||||
|
sha = sha,
|
||||||
|
subject = subject,
|
||||||
|
})
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
pending = pending - 1
|
||||||
|
if pending == 0 then
|
||||||
|
callback(branch, groups)
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
|
||||||
|
---@param bufnr integer
|
||||||
|
---@param branch ow.Git.BranchInfo
|
||||||
|
---@param groups table<string, ow.Git.StatusEntry[]>
|
||||||
|
local function render(bufnr, branch, groups)
|
||||||
|
local lines = { "Head: " .. (branch.head or "?") }
|
||||||
|
if branch.upstream then
|
||||||
|
local push = "Push: " .. branch.upstream
|
||||||
|
if branch.ahead > 0 then
|
||||||
|
push = push .. " +" .. branch.ahead
|
||||||
|
end
|
||||||
|
if branch.behind > 0 then
|
||||||
|
push = push .. " -" .. branch.behind
|
||||||
|
end
|
||||||
|
table.insert(lines, push)
|
||||||
|
end
|
||||||
|
table.insert(lines, "")
|
||||||
|
|
||||||
|
local meta = {}
|
||||||
|
local marks = {}
|
||||||
|
for _, section in ipairs(SECTIONS) do
|
||||||
|
local entries = groups[section]
|
||||||
|
if entries and #entries > 0 then
|
||||||
|
table.insert(lines, string.format("%s (%d)", section, #entries))
|
||||||
|
for _, entry in ipairs(entries) do
|
||||||
|
local line, hl, hl_len = format_entry(entry)
|
||||||
|
if line then
|
||||||
|
table.insert(lines, line)
|
||||||
|
meta[#lines] = entry
|
||||||
|
if hl and hl_len then
|
||||||
|
table.insert(marks, {
|
||||||
|
row = #lines - 1,
|
||||||
|
col = 2,
|
||||||
|
end_col = 2 + hl_len,
|
||||||
|
hl = hl,
|
||||||
|
})
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
table.insert(lines, "")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
vim.bo[bufnr].modifiable = true
|
||||||
|
vim.api.nvim_buf_set_lines(bufnr, 0, -1, false, lines)
|
||||||
|
vim.bo[bufnr].modifiable = false
|
||||||
|
vim.api.nvim_buf_clear_namespace(bufnr, ns, 0, -1)
|
||||||
|
for _, m in ipairs(marks) do
|
||||||
|
vim.api.nvim_buf_set_extmark(bufnr, ns, m.row, m.col, {
|
||||||
|
end_col = m.end_col,
|
||||||
|
hl_group = m.hl,
|
||||||
|
})
|
||||||
|
end
|
||||||
|
state[bufnr].lines = meta
|
||||||
|
end
|
||||||
|
|
||||||
|
---@param bufnr integer
|
||||||
|
local function refresh(bufnr)
|
||||||
|
local s = state[bufnr]
|
||||||
|
if not s then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
local saved_path, saved_sha
|
||||||
|
for _, win in ipairs(vim.api.nvim_list_wins()) do
|
||||||
|
if vim.api.nvim_win_get_buf(win) == bufnr then
|
||||||
|
local lnum = vim.api.nvim_win_get_cursor(win)[1]
|
||||||
|
local entry = s.lines[lnum]
|
||||||
|
if entry then
|
||||||
|
saved_path = entry.path
|
||||||
|
saved_sha = entry.sha
|
||||||
|
end
|
||||||
|
break
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
fetch_status(s.worktree, function(branch, groups)
|
||||||
|
if not vim.api.nvim_buf_is_valid(bufnr) then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
render(bufnr, branch, groups)
|
||||||
|
if not saved_path and not saved_sha then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
for lnum, entry in pairs(s.lines) do
|
||||||
|
if
|
||||||
|
(saved_path and entry.path == saved_path)
|
||||||
|
or (saved_sha and entry.sha == saved_sha)
|
||||||
|
then
|
||||||
|
for _, win in ipairs(vim.api.nvim_list_wins()) do
|
||||||
|
if vim.api.nvim_win_get_buf(win) == bufnr then
|
||||||
|
pcall(vim.api.nvim_win_set_cursor, win, { lnum, 0 })
|
||||||
|
end
|
||||||
|
end
|
||||||
|
break
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
|
||||||
|
---@param bufnr integer
|
||||||
|
---@return ow.Git.StatusState?
|
||||||
|
---@return ow.Git.StatusEntry?
|
||||||
|
local function current_entry(bufnr)
|
||||||
|
local s = state[bufnr]
|
||||||
|
if not s then
|
||||||
|
return nil, nil
|
||||||
|
end
|
||||||
|
local lnum = vim.api.nvim_win_get_cursor(0)[1]
|
||||||
|
return s, s.lines[lnum]
|
||||||
|
end
|
||||||
|
|
||||||
|
---@param buf integer
|
||||||
|
---@param worktree string
|
||||||
|
---@param path string
|
||||||
|
local function attach_index_writer(buf, worktree, path)
|
||||||
|
vim.api.nvim_create_autocmd("BufWriteCmd", {
|
||||||
|
buffer = buf,
|
||||||
|
callback = function()
|
||||||
|
local body = table.concat(
|
||||||
|
vim.api.nvim_buf_get_lines(buf, 0, -1, false),
|
||||||
|
"\n"
|
||||||
|
) .. "\n"
|
||||||
|
local hash = vim.system(
|
||||||
|
{ "git", "hash-object", "-w", "--stdin" },
|
||||||
|
{ cwd = worktree, stdin = body, text = true }
|
||||||
|
):wait()
|
||||||
|
if hash.code ~= 0 then
|
||||||
|
log.error("git hash-object failed: %s", hash.stderr or "")
|
||||||
|
return
|
||||||
|
end
|
||||||
|
local sha = vim.trim(hash.stdout or "")
|
||||||
|
local mode = "100644"
|
||||||
|
local ls = vim.system(
|
||||||
|
{ "git", "ls-files", "-s", "--", path },
|
||||||
|
{ cwd = worktree, text = true }
|
||||||
|
):wait()
|
||||||
|
if ls.code == 0 and ls.stdout then
|
||||||
|
local m = ls.stdout:match("^(%d+)")
|
||||||
|
if m then
|
||||||
|
mode = m
|
||||||
|
end
|
||||||
|
end
|
||||||
|
local upd = vim.system({
|
||||||
|
"git",
|
||||||
|
"update-index",
|
||||||
|
"--cacheinfo",
|
||||||
|
mode .. "," .. sha .. "," .. path,
|
||||||
|
}, { cwd = worktree, text = true }):wait()
|
||||||
|
if upd.code ~= 0 then
|
||||||
|
log.error("git update-index failed: %s", upd.stderr or "")
|
||||||
|
return
|
||||||
|
end
|
||||||
|
vim.bo[buf].modified = false
|
||||||
|
end,
|
||||||
|
})
|
||||||
|
end
|
||||||
|
|
||||||
|
---@param worktree string
|
||||||
|
---@param ref string '' for index, 'HEAD' for HEAD
|
||||||
|
---@param path string
|
||||||
|
---@param is_index boolean? true to hook :w to update the git index
|
||||||
|
---@return integer
|
||||||
|
local function git_show_buf(worktree, ref, path, is_index)
|
||||||
|
local result = vim.system(
|
||||||
|
{ "git", "show", ref .. ":" .. path },
|
||||||
|
{ cwd = worktree, text = true }
|
||||||
|
):wait()
|
||||||
|
local content = result.code == 0 and (result.stdout or "") or ""
|
||||||
|
local lines = vim.split(content, "\n", { plain = true, trimempty = false })
|
||||||
|
if #lines > 0 and lines[#lines] == "" then
|
||||||
|
table.remove(lines)
|
||||||
|
end
|
||||||
|
local buf = vim.api.nvim_create_buf(false, true)
|
||||||
|
vim.api.nvim_buf_set_lines(buf, 0, -1, false, lines)
|
||||||
|
vim.bo[buf].buftype = is_index and "acwrite" or "nofile"
|
||||||
|
vim.bo[buf].bufhidden = "wipe"
|
||||||
|
vim.bo[buf].swapfile = false
|
||||||
|
if not is_index then
|
||||||
|
vim.bo[buf].modifiable = false
|
||||||
|
end
|
||||||
|
if is_index then
|
||||||
|
attach_index_writer(buf, worktree, path)
|
||||||
|
end
|
||||||
|
vim.bo[buf].modified = false
|
||||||
|
return buf
|
||||||
|
end
|
||||||
|
|
||||||
|
---@return integer
|
||||||
|
local function empty_buf()
|
||||||
|
local buf = vim.api.nvim_create_buf(false, true)
|
||||||
|
vim.bo[buf].buftype = "nofile"
|
||||||
|
vim.bo[buf].bufhidden = "wipe"
|
||||||
|
vim.bo[buf].swapfile = false
|
||||||
|
vim.bo[buf].modifiable = false
|
||||||
|
vim.bo[buf].modified = false
|
||||||
|
return buf
|
||||||
|
end
|
||||||
|
|
||||||
|
---@param abs_path string
|
||||||
|
---@return integer
|
||||||
|
local function load_file_buf(abs_path)
|
||||||
|
for _, buf in ipairs(vim.api.nvim_list_bufs()) do
|
||||||
|
if
|
||||||
|
vim.api.nvim_buf_is_loaded(buf)
|
||||||
|
and vim.api.nvim_buf_get_name(buf) == abs_path
|
||||||
|
then
|
||||||
|
return buf
|
||||||
|
end
|
||||||
|
end
|
||||||
|
local buf = vim.fn.bufadd(abs_path)
|
||||||
|
vim.fn.bufload(buf)
|
||||||
|
return buf
|
||||||
|
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 worktree string
|
||||||
|
---@param path string
|
||||||
|
---@param content boolean
|
||||||
|
---@return ow.Git.DiffSide
|
||||||
|
local function head_pane(worktree, path, content)
|
||||||
|
return {
|
||||||
|
buf = content and git_show_buf(worktree, "HEAD", path) or empty_buf(),
|
||||||
|
name = "git://HEAD/" .. path,
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
---@param worktree string
|
||||||
|
---@param path string
|
||||||
|
---@param exists boolean
|
||||||
|
---@return ow.Git.DiffSide
|
||||||
|
local function worktree_pane(worktree, path, exists)
|
||||||
|
if exists then
|
||||||
|
return {
|
||||||
|
buf = load_file_buf(vim.fs.joinpath(worktree, path)),
|
||||||
|
name = nil,
|
||||||
|
}
|
||||||
|
end
|
||||||
|
return { buf = empty_buf(), name = "git://worktree/" .. path }
|
||||||
|
end
|
||||||
|
|
||||||
|
---@param s ow.Git.StatusState
|
||||||
|
---@param entry ow.Git.FileEntry
|
||||||
|
---@return ow.Git.DiffSide
|
||||||
|
local function index_pane(s, entry)
|
||||||
|
local in_index = not (
|
||||||
|
entry.section == "Untracked"
|
||||||
|
or (entry.section == "Staged" and entry.x == "D")
|
||||||
|
)
|
||||||
|
return {
|
||||||
|
buf = in_index and git_show_buf(s.worktree, "", entry.path, true)
|
||||||
|
or empty_buf(),
|
||||||
|
name = "git://index/" .. entry.path,
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
---@param s ow.Git.StatusState
|
||||||
|
---@param entry ow.Git.FileEntry
|
||||||
|
---@return ow.Git.DiffSide?
|
||||||
|
local function other_pane(s, entry)
|
||||||
|
local p = entry.path
|
||||||
|
local worktree = s.worktree
|
||||||
|
if entry.section == "Staged" then
|
||||||
|
if entry.x == "A" then
|
||||||
|
return head_pane(worktree, p, false)
|
||||||
|
end
|
||||||
|
if entry.x == "D" then
|
||||||
|
return head_pane(worktree, p, true)
|
||||||
|
end
|
||||||
|
-- HEAD holds the pre-rename path
|
||||||
|
return head_pane(worktree, entry.orig or p, true)
|
||||||
|
end
|
||||||
|
if entry.section == "Unstaged" then
|
||||||
|
return worktree_pane(worktree, p, entry.y ~= "D")
|
||||||
|
end
|
||||||
|
if entry.section == "Untracked" then
|
||||||
|
return worktree_pane(worktree, p, true)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
---@param s ow.Git.StatusState
|
||||||
|
---@param entry ow.Git.StatusEntry
|
||||||
|
---@return ow.Git.DiffPair?
|
||||||
|
local function compute_pair(s, entry)
|
||||||
|
if not entry.path then
|
||||||
|
return nil
|
||||||
|
end
|
||||||
|
---@cast entry ow.Git.FileEntry
|
||||||
|
local other = other_pane(s, entry)
|
||||||
|
if not other then
|
||||||
|
return nil
|
||||||
|
end
|
||||||
|
return { left = index_pane(s, entry), right = other }
|
||||||
|
end
|
||||||
|
|
||||||
|
---@param win integer
|
||||||
|
local function reset_diff_win(win)
|
||||||
|
vim.api.nvim_win_call(win, function()
|
||||||
|
vim.cmd(
|
||||||
|
"setlocal winfixwidth< number< relativenumber< signcolumn< wrap< cursorline<"
|
||||||
|
)
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
|
||||||
|
---@param sidebar_win integer
|
||||||
|
---@return integer?
|
||||||
|
local function find_default_main_win(sidebar_win)
|
||||||
|
local non_sidebar = {}
|
||||||
|
for _, win in ipairs(vim.api.nvim_tabpage_list_wins(0)) do
|
||||||
|
if win ~= sidebar_win then
|
||||||
|
table.insert(non_sidebar, win)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
if #non_sidebar ~= 1 then
|
||||||
|
return nil
|
||||||
|
end
|
||||||
|
local buf = vim.api.nvim_win_get_buf(non_sidebar[1])
|
||||||
|
if
|
||||||
|
vim.api.nvim_buf_get_name(buf) == ""
|
||||||
|
and vim.bo[buf].buftype == ""
|
||||||
|
and not vim.bo[buf].modified
|
||||||
|
and vim.api.nvim_buf_line_count(buf) == 1
|
||||||
|
and vim.api.nvim_buf_get_lines(buf, 0, 1, false)[1] == ""
|
||||||
|
then
|
||||||
|
return non_sidebar[1]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
---@param win integer
|
||||||
|
---@param enabled boolean
|
||||||
|
local function set_diff(win, enabled)
|
||||||
|
vim.api.nvim_win_call(win, function()
|
||||||
|
vim.cmd(enabled and "diffthis" or "diffoff")
|
||||||
|
end)
|
||||||
|
if enabled then
|
||||||
|
vim.wo[win].foldenable = true
|
||||||
|
vim.wo[win].foldlevel = 0
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
---@param s ow.Git.StatusState
|
||||||
|
---@param sidebar_win integer
|
||||||
|
---@return integer? left
|
||||||
|
---@return integer? right
|
||||||
|
local function adopt_diff_wins(s, sidebar_win)
|
||||||
|
local left = s.diff_left_win
|
||||||
|
local right = s.diff_right_win
|
||||||
|
if left and not vim.api.nvim_win_is_valid(left) then
|
||||||
|
left = nil
|
||||||
|
end
|
||||||
|
if right and not vim.api.nvim_win_is_valid(right) then
|
||||||
|
right = nil
|
||||||
|
end
|
||||||
|
if left and right then
|
||||||
|
return left, right
|
||||||
|
end
|
||||||
|
for _, win in ipairs(vim.api.nvim_tabpage_list_wins(0)) do
|
||||||
|
if win ~= sidebar_win then
|
||||||
|
local role = vim.w[win].git_diff_role
|
||||||
|
if role == "left" and not left then
|
||||||
|
left = win
|
||||||
|
elseif role == "right" and not right then
|
||||||
|
right = win
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return left, right
|
||||||
|
end
|
||||||
|
|
||||||
|
---@param entry ow.Git.StatusEntry
|
||||||
|
---@return string
|
||||||
|
local function entry_key(entry)
|
||||||
|
return entry.section
|
||||||
|
.. "|"
|
||||||
|
.. (entry.path or entry.sha or "")
|
||||||
|
.. "|"
|
||||||
|
.. (entry.orig or "")
|
||||||
|
end
|
||||||
|
|
||||||
|
---@param s ow.Git.StatusState
|
||||||
|
---@param entry ow.Git.StatusEntry
|
||||||
|
---@param focus_left boolean
|
||||||
|
local function show_diff(s, entry, focus_left)
|
||||||
|
local sidebar_win = find_sidebar()
|
||||||
|
if not sidebar_win then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
local left_win, right_win = adopt_diff_wins(s, sidebar_win)
|
||||||
|
local key = entry_key(entry)
|
||||||
|
|
||||||
|
if s.last_shown_key == key and left_win and right_win then
|
||||||
|
if focus_left then
|
||||||
|
vim.api.nvim_set_current_win(left_win)
|
||||||
|
else
|
||||||
|
vim.api.nvim_set_current_win(sidebar_win)
|
||||||
|
end
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
local pair = compute_pair(s, entry)
|
||||||
|
if not pair then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
if left_win and not right_win then
|
||||||
|
vim.api.nvim_set_current_win(left_win)
|
||||||
|
vim.cmd("rightbelow vertical split")
|
||||||
|
right_win = vim.api.nvim_get_current_win()
|
||||||
|
reset_diff_win(right_win)
|
||||||
|
elseif right_win and not left_win then
|
||||||
|
vim.api.nvim_set_current_win(right_win)
|
||||||
|
vim.cmd("leftabove vertical split")
|
||||||
|
left_win = vim.api.nvim_get_current_win()
|
||||||
|
reset_diff_win(left_win)
|
||||||
|
elseif not (left_win or right_win) then
|
||||||
|
local default_main = find_default_main_win(sidebar_win)
|
||||||
|
if default_main then
|
||||||
|
right_win = default_main
|
||||||
|
reset_diff_win(right_win)
|
||||||
|
vim.api.nvim_set_current_win(default_main)
|
||||||
|
vim.cmd("leftabove vertical split")
|
||||||
|
left_win = vim.api.nvim_get_current_win()
|
||||||
|
reset_diff_win(left_win)
|
||||||
|
else
|
||||||
|
-- No reusable default-empty window. Open the diff pair by
|
||||||
|
-- splitting from the sidebar. winfixwidth keeps the sidebar at 50
|
||||||
|
-- when there are other windows to absorb the split; if the
|
||||||
|
-- sidebar is the only window in the tab, the split has to take
|
||||||
|
-- from the sidebar itself, so restore the width explicitly.
|
||||||
|
vim.api.nvim_set_current_win(sidebar_win)
|
||||||
|
vim.cmd("rightbelow vertical split")
|
||||||
|
right_win = vim.api.nvim_get_current_win()
|
||||||
|
reset_diff_win(right_win)
|
||||||
|
vim.cmd("leftabove vertical split")
|
||||||
|
left_win = vim.api.nvim_get_current_win()
|
||||||
|
reset_diff_win(left_win)
|
||||||
|
vim.api.nvim_win_set_width(sidebar_win, SIDEBAR_WIDTH)
|
||||||
|
end
|
||||||
|
local combined = vim.api.nvim_win_get_width(left_win)
|
||||||
|
+ vim.api.nvim_win_get_width(right_win)
|
||||||
|
vim.api.nvim_win_set_width(left_win, math.floor(combined / 2))
|
||||||
|
end
|
||||||
|
|
||||||
|
assert(left_win and right_win, "diff windows must be set")
|
||||||
|
vim.w[left_win].git_diff_role = "left"
|
||||||
|
vim.w[right_win].git_diff_role = "right"
|
||||||
|
s.diff_left_win = left_win
|
||||||
|
s.diff_right_win = right_win
|
||||||
|
|
||||||
|
vim.api.nvim_win_set_buf(left_win, pair.left.buf)
|
||||||
|
vim.api.nvim_win_set_buf(right_win, pair.right.buf)
|
||||||
|
for _, side in ipairs({ pair.left, pair.right }) do
|
||||||
|
if side.name then
|
||||||
|
pcall(vim.api.nvim_buf_set_name, side.buf, side.name)
|
||||||
|
local ft = vim.filetype.match({ buf = side.buf })
|
||||||
|
if ft then
|
||||||
|
vim.bo[side.buf].filetype = ft
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
set_diff(left_win, true)
|
||||||
|
set_diff(right_win, true)
|
||||||
|
s.last_shown_key = key
|
||||||
|
|
||||||
|
if focus_left then
|
||||||
|
vim.api.nvim_set_current_win(left_win)
|
||||||
|
else
|
||||||
|
vim.api.nvim_set_current_win(sidebar_win)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
---@param focus_left boolean
|
||||||
|
local function preview_or_open(focus_left)
|
||||||
|
local s, entry = current_entry(vim.api.nvim_get_current_buf())
|
||||||
|
if not s or not entry then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
show_diff(s, entry, focus_left)
|
||||||
|
end
|
||||||
|
|
||||||
|
local function action_stage()
|
||||||
|
local s, entry = current_entry(vim.api.nvim_get_current_buf())
|
||||||
|
if not s or not entry or not entry.path then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
---@cast entry ow.Git.FileEntry
|
||||||
|
if entry.section == "Staged" then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
vim.system({ "git", "add", "--", entry.path }, { cwd = s.worktree })
|
||||||
|
end
|
||||||
|
|
||||||
|
local function action_unstage()
|
||||||
|
local s, entry = current_entry(vim.api.nvim_get_current_buf())
|
||||||
|
if not s or not entry or not entry.path then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
---@cast entry ow.Git.FileEntry
|
||||||
|
if entry.section ~= "Staged" then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
local cmd = { "git", "restore", "--staged", "--" }
|
||||||
|
if entry.orig then
|
||||||
|
table.insert(cmd, entry.orig)
|
||||||
|
end
|
||||||
|
table.insert(cmd, entry.path)
|
||||||
|
vim.system(cmd, { cwd = s.worktree })
|
||||||
|
end
|
||||||
|
|
||||||
|
local function action_discard()
|
||||||
|
local s, entry = current_entry(vim.api.nvim_get_current_buf())
|
||||||
|
if not s or not entry or not entry.path then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
---@cast entry ow.Git.FileEntry
|
||||||
|
if entry.section == "Staged" then
|
||||||
|
log.warning("file has staged changes; unstage first with 'u'")
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
local prompt, action
|
||||||
|
if entry.section == "Untracked" then
|
||||||
|
prompt = string.format("Delete untracked file %s?", entry.path)
|
||||||
|
action = function()
|
||||||
|
os.remove(vim.fs.joinpath(s.worktree, entry.path))
|
||||||
|
refresh(vim.api.nvim_get_current_buf())
|
||||||
|
end
|
||||||
|
elseif entry.section == "Unstaged" then
|
||||||
|
prompt = string.format("Discard changes to %s?", entry.path)
|
||||||
|
action = function()
|
||||||
|
vim.system(
|
||||||
|
{ "git", "checkout", "--", entry.path },
|
||||||
|
{ cwd = s.worktree }
|
||||||
|
)
|
||||||
|
end
|
||||||
|
else
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
if vim.fn.confirm(prompt, "&Yes\n&No", 2) == 1 then
|
||||||
|
action()
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local function action_help()
|
||||||
|
print(table.concat({
|
||||||
|
"git status sidebar",
|
||||||
|
" <Tab> preview diff (keep focus)",
|
||||||
|
" <CR> open diff (focus left pane)",
|
||||||
|
" s stage file",
|
||||||
|
" u unstage file",
|
||||||
|
" X discard worktree changes (untracked: delete file)",
|
||||||
|
" g? show this help",
|
||||||
|
}, "\n"))
|
||||||
|
end
|
||||||
|
|
||||||
|
---@param worktree string
|
||||||
|
local function open(worktree)
|
||||||
|
local existing = find_sidebar()
|
||||||
|
if existing then
|
||||||
|
vim.api.nvim_set_current_win(existing)
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
local gitdir = repo.resolve(worktree)
|
||||||
|
if not gitdir then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
local previous_win = vim.api.nvim_get_current_win()
|
||||||
|
vim.cmd("leftabove vertical new")
|
||||||
|
local win = vim.api.nvim_get_current_win()
|
||||||
|
local bufnr = vim.api.nvim_get_current_buf()
|
||||||
|
|
||||||
|
vim.bo[bufnr].buftype = "nofile"
|
||||||
|
vim.bo[bufnr].bufhidden = "wipe"
|
||||||
|
vim.bo[bufnr].swapfile = false
|
||||||
|
vim.bo[bufnr].filetype = "gitstatus"
|
||||||
|
vim.bo[bufnr].modifiable = false
|
||||||
|
|
||||||
|
vim.wo[win].number = false
|
||||||
|
vim.wo[win].relativenumber = false
|
||||||
|
vim.wo[win].wrap = false
|
||||||
|
vim.wo[win].signcolumn = "no"
|
||||||
|
vim.wo[win].cursorline = true
|
||||||
|
vim.wo[win].winfixwidth = true
|
||||||
|
vim.api.nvim_win_set_width(win, SIDEBAR_WIDTH)
|
||||||
|
|
||||||
|
state[bufnr] = { gitdir = gitdir, worktree = worktree, lines = {} }
|
||||||
|
|
||||||
|
local function k(lhs, rhs, desc)
|
||||||
|
vim.keymap.set(
|
||||||
|
"n",
|
||||||
|
lhs,
|
||||||
|
rhs,
|
||||||
|
{ buffer = bufnr, silent = true, desc = desc }
|
||||||
|
)
|
||||||
|
end
|
||||||
|
k("<Tab>", function()
|
||||||
|
preview_or_open(false)
|
||||||
|
end, "Preview diff")
|
||||||
|
k("<CR>", function()
|
||||||
|
preview_or_open(true)
|
||||||
|
end, "Open diff")
|
||||||
|
k("s", action_stage, "Stage file")
|
||||||
|
k("u", action_unstage, "Unstage file")
|
||||||
|
k("X", action_discard, "Discard worktree changes")
|
||||||
|
k("g?", action_help, "Help")
|
||||||
|
|
||||||
|
state[bufnr].user_aucmd = vim.api.nvim_create_autocmd("User", {
|
||||||
|
pattern = "GitRefresh",
|
||||||
|
group = group,
|
||||||
|
callback = function(args)
|
||||||
|
if args.data and args.data.gitdir == gitdir then
|
||||||
|
refresh(bufnr)
|
||||||
|
end
|
||||||
|
end,
|
||||||
|
})
|
||||||
|
vim.api.nvim_create_autocmd({ "BufWipeout", "BufDelete" }, {
|
||||||
|
buffer = bufnr,
|
||||||
|
group = group,
|
||||||
|
callback = function()
|
||||||
|
local s = state[bufnr]
|
||||||
|
if not s then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
if s.user_aucmd then
|
||||||
|
pcall(vim.api.nvim_del_autocmd, s.user_aucmd)
|
||||||
|
end
|
||||||
|
state[bufnr] = nil
|
||||||
|
end,
|
||||||
|
})
|
||||||
|
|
||||||
|
vim.api.nvim_set_current_win(previous_win)
|
||||||
|
refresh(bufnr)
|
||||||
|
end
|
||||||
|
|
||||||
|
function M.toggle()
|
||||||
|
local sidebar_win = find_sidebar()
|
||||||
|
if sidebar_win then
|
||||||
|
vim.api.nvim_win_close(sidebar_win, false)
|
||||||
|
return
|
||||||
|
end
|
||||||
|
local path = vim.api.nvim_buf_get_name(vim.api.nvim_get_current_buf())
|
||||||
|
if path == "" then
|
||||||
|
path = vim.fn.getcwd()
|
||||||
|
end
|
||||||
|
local _, worktree = repo.resolve(path)
|
||||||
|
if not worktree then
|
||||||
|
log.warning("not in a git repository")
|
||||||
|
return
|
||||||
|
end
|
||||||
|
open(worktree)
|
||||||
|
end
|
||||||
|
|
||||||
|
return M
|
||||||
@@ -111,8 +111,8 @@ require("nvim-tree").setup({
|
|||||||
full_name = true,
|
full_name = true,
|
||||||
root_folder_label = function(path)
|
root_folder_label = function(path)
|
||||||
local label = vim.fn.fnamemodify(path, ":~")
|
local label = vim.fn.fnamemodify(path, ":~")
|
||||||
local git_head = vim.fn.FugitiveHead()
|
local git_head = require("git").head(path)
|
||||||
if git_head ~= "" then
|
if git_head then
|
||||||
label = label .. (" %s"):format(git_head)
|
label = label .. (" %s"):format(git_head)
|
||||||
end
|
end
|
||||||
return label
|
return label
|
||||||
|
|||||||
+3
-3
@@ -50,9 +50,9 @@ local highlights = {
|
|||||||
DiffAdd = { bg = "#1a2f22" },
|
DiffAdd = { bg = "#1a2f22" },
|
||||||
DiffChange = { bg = "#15304a" },
|
DiffChange = { bg = "#15304a" },
|
||||||
DiffDelete = { bg = "#311c1e" },
|
DiffDelete = { bg = "#311c1e" },
|
||||||
GitDeleted = { fg = c.red },
|
-- GitDeleted = { fg = c.red },
|
||||||
GitDirty = { fg = c.yellow },
|
-- GitUnstaged = { fg = c.yellow },
|
||||||
GitNew = { fg = c.green },
|
-- 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 }
|
||||||
|
|||||||
@@ -1,35 +1,3 @@
|
|||||||
local function open_git_status()
|
|
||||||
local previous_win = vim.api.nvim_get_current_win()
|
|
||||||
vim.cmd("leftabove vertical G")
|
|
||||||
vim.api.nvim_win_set_width(0, 50)
|
|
||||||
vim.api.nvim_set_option_value("winfixwidth", true, { scope = "local" })
|
|
||||||
vim.api.nvim_set_current_win(previous_win)
|
|
||||||
end
|
|
||||||
|
|
||||||
local function get_git_status_win()
|
|
||||||
local current_tabpage = vim.api.nvim_get_current_tabpage()
|
|
||||||
for _, win in ipairs(vim.api.nvim_tabpage_list_wins(current_tabpage)) do
|
|
||||||
local buf = vim.api.nvim_win_get_buf(win)
|
|
||||||
local buftype = vim.api.nvim_get_option_value("buftype", { buf = buf })
|
|
||||||
if
|
|
||||||
buftype == "nowrite"
|
|
||||||
and vim.api.nvim_buf_get_name(buf):match("^fugitive://.*%.git//$")
|
|
||||||
then
|
|
||||||
return win
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
local function toggle_git_status()
|
|
||||||
local win = get_git_status_win()
|
|
||||||
if win then
|
|
||||||
vim.api.nvim_win_close(win, false)
|
|
||||||
return
|
|
||||||
end
|
|
||||||
|
|
||||||
open_git_status()
|
|
||||||
end
|
|
||||||
|
|
||||||
vim.api.nvim_create_user_command("Glog", function(opts)
|
vim.api.nvim_create_user_command("Glog", function(opts)
|
||||||
local mods = opts.mods ~= "" and (opts.mods .. " ") or ""
|
local mods = opts.mods ~= "" and (opts.mods .. " ") or ""
|
||||||
vim.cmd(
|
vim.cmd(
|
||||||
@@ -58,7 +26,6 @@ end)
|
|||||||
vim.keymap.set("n", "<leader>gp", function()
|
vim.keymap.set("n", "<leader>gp", function()
|
||||||
vim.cmd.G("push")
|
vim.cmd.G("push")
|
||||||
end)
|
end)
|
||||||
vim.keymap.set("n", "<leader>gg", toggle_git_status)
|
|
||||||
|
|
||||||
vim.api.nvim_create_autocmd("User", {
|
vim.api.nvim_create_autocmd("User", {
|
||||||
pattern = "GitRefresh",
|
pattern = "GitRefresh",
|
||||||
|
|||||||
@@ -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"
|
||||||
Reference in New Issue
Block a user