mirror of
https://github.com/folke/snacks.nvim
synced 2025-08-04 10:49:08 +00:00
feat(explorer): added git status. Closes #817
This commit is contained in:
parent
09bf19f15b
commit
5cae48d93c
11 changed files with 278 additions and 32 deletions
|
@ -16,6 +16,7 @@ local expanded = {} ---@type table<string, boolean>
|
|||
---@field last? boolean
|
||||
---@field sort? string
|
||||
---@field internal? boolean internal parent directories not part of fd output
|
||||
---@field status? string
|
||||
|
||||
---@class snacks.picker.explorer.State
|
||||
---@field cwd string
|
||||
|
@ -43,6 +44,9 @@ function State.new(picker)
|
|||
picker.list.win:on({ "WinEnter", "BufEnter" }, function()
|
||||
self:follow()
|
||||
end)
|
||||
picker.list.win:on("TermClose", function()
|
||||
self:update()
|
||||
end, { pattern = "*lazygit" })
|
||||
-- schedule initial follow
|
||||
if self.opts.follow_file then
|
||||
self.on_find = function()
|
||||
|
@ -69,7 +73,7 @@ end
|
|||
---@param path string
|
||||
function State:show(path)
|
||||
local picker = self.picker()
|
||||
if not picker then
|
||||
if not picker or picker.closed then
|
||||
return
|
||||
end
|
||||
path = vim.fs.normalize(path)
|
||||
|
@ -202,6 +206,9 @@ function State:setup(opts, ctx)
|
|||
end
|
||||
picker.list:set_target()
|
||||
self.on_find = function()
|
||||
if picker.closed then
|
||||
return
|
||||
end
|
||||
for item, idx in picker:iter() do
|
||||
if not item.internal then
|
||||
picker.list:view(idx)
|
||||
|
@ -220,7 +227,7 @@ end
|
|||
function State:update(opts)
|
||||
opts = opts or {}
|
||||
local picker = self.picker()
|
||||
if not picker then
|
||||
if not picker or picker.closed then
|
||||
return
|
||||
end
|
||||
if opts.target ~= false then
|
||||
|
@ -439,8 +446,13 @@ end
|
|||
function M.explorer(opts, ctx)
|
||||
local state = M.get_state(ctx.picker)
|
||||
opts = state:setup(opts, ctx)
|
||||
opts.notify = false
|
||||
|
||||
local Git = require("snacks.picker.source.git")
|
||||
|
||||
local files = require("snacks.picker.source.files").files(opts, ctx)
|
||||
local git = Git.status(opts, ctx)
|
||||
|
||||
local dirs = {} ---@type table<string, snacks.picker.explorer.Item>
|
||||
local last = {} ---@type table<snacks.picker.finder.Item, snacks.picker.finder.Item>
|
||||
|
||||
|
@ -456,10 +468,13 @@ function M.explorer(opts, ctx)
|
|||
local cwd = state.cwd
|
||||
dirs[cwd] = root
|
||||
|
||||
local items = {} ---@type table<string, snacks.picker.explorer.Item>
|
||||
---@async
|
||||
return function(cb)
|
||||
if state.on_find then
|
||||
ctx.picker.matcher.task:on("done", vim.schedule_wrap(state.on_find))
|
||||
end
|
||||
items[cwd] = root
|
||||
cb(root)
|
||||
|
||||
---@param item snacks.picker.explorer.Item
|
||||
|
@ -486,10 +501,27 @@ function M.explorer(opts, ctx)
|
|||
last[parent] = item
|
||||
end
|
||||
end
|
||||
items[item.file] = item
|
||||
-- add to picker
|
||||
cb(item)
|
||||
end
|
||||
|
||||
-- gather git status in a separate coroutine,
|
||||
-- so that both git and fd can run in parallel
|
||||
local git_status = {} ---@type table<string, string>
|
||||
local git_async ---@type snacks.picker.Async?
|
||||
if opts.git_status then
|
||||
git_async = require("snacks.picker.util.async").new(function()
|
||||
git(function(item)
|
||||
local path = Snacks.picker.util.path(item)
|
||||
if path then
|
||||
git_status[path] = item.status
|
||||
end
|
||||
end)
|
||||
end)
|
||||
end
|
||||
|
||||
-- get files and directories
|
||||
files(function(item)
|
||||
---@cast item snacks.picker.explorer.Item
|
||||
item.cwd = nil -- we use absolute paths
|
||||
|
@ -507,30 +539,46 @@ function M.explorer(opts, ctx)
|
|||
end
|
||||
|
||||
-- Add parents when needed
|
||||
if item.file:sub(1, #cwd) == cwd and #item.file > #cwd then
|
||||
local path = item.file
|
||||
local to = #cwd + 1 ---@type number?
|
||||
while to do
|
||||
to = path:find("/", to + 1, true)
|
||||
if not to then
|
||||
break
|
||||
end
|
||||
local dir = path:sub(1, to - 1)
|
||||
if not dirs[dir] then
|
||||
dirs[dir] = {
|
||||
text = dir,
|
||||
file = dir,
|
||||
dir = true,
|
||||
open = state:is_open(dir),
|
||||
internal = true,
|
||||
}
|
||||
add(dirs[dir])
|
||||
end
|
||||
for dir in Snacks.picker.util.parents(item.file, cwd) do
|
||||
if not dirs[dir] then
|
||||
dirs[dir] = {
|
||||
text = dir,
|
||||
file = dir,
|
||||
dir = true,
|
||||
open = state:is_open(dir),
|
||||
internal = true,
|
||||
}
|
||||
add(dirs[dir])
|
||||
end
|
||||
end
|
||||
|
||||
add(item)
|
||||
end)
|
||||
|
||||
-- wait for git status to finish
|
||||
if git_async then
|
||||
git_async:wait()
|
||||
end
|
||||
|
||||
local function add_git_status(path, status)
|
||||
local item = items[path]
|
||||
if item then
|
||||
if item.status then
|
||||
item.status = Git.merge_status(item.status, status)
|
||||
else
|
||||
item.status = status
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- Add git status to files and parents
|
||||
for path, status in pairs(git_status) do
|
||||
add_git_status(path, status)
|
||||
add_git_status(cwd, status)
|
||||
for dir in Snacks.picker.util.parents(path, cwd) do
|
||||
add_git_status(dir, status)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -253,4 +253,91 @@ function M.stash(opts, ctx)
|
|||
}, ctx)
|
||||
end
|
||||
|
||||
---@class snacks.picker.git.Status
|
||||
---@field xy string
|
||||
---@field status "modified" | "deleted" | "added" | "untracked" | "renamed" | "copied" | "ignored"
|
||||
---@field unmerged? boolean
|
||||
---@field staged? boolean
|
||||
---@field priority? number
|
||||
|
||||
---@param xy string
|
||||
---@return snacks.picker.git.Status
|
||||
function M.git_status(xy)
|
||||
local ss = {
|
||||
A = "added",
|
||||
D = "deleted",
|
||||
M = "modified",
|
||||
R = "renamed",
|
||||
C = "copied",
|
||||
["?"] = "untracked",
|
||||
["!"] = "ignored",
|
||||
}
|
||||
local prios = "!?CRDAM"
|
||||
|
||||
---@param status string
|
||||
---@param unmerged? boolean
|
||||
---@param staged? boolean
|
||||
local function s(status, unmerged, staged)
|
||||
local prio = (prios:find(status, 1, true) or 0) + (unmerged and 20 or 0)
|
||||
if not staged and not status:find("[!]") then
|
||||
prio = prio + 10
|
||||
end
|
||||
return {
|
||||
xy = xy,
|
||||
status = ss[status],
|
||||
unmerged = unmerged,
|
||||
staged = staged,
|
||||
priority = prio,
|
||||
}
|
||||
end
|
||||
---@param c string
|
||||
local function f(c)
|
||||
return xy:gsub("T", "M"):match(c) --[[@as string?]]
|
||||
end
|
||||
|
||||
if f("%?%?") then
|
||||
return s("?")
|
||||
elseif f("!!") then
|
||||
return s("!")
|
||||
elseif f("UU") then
|
||||
return s("M", true)
|
||||
elseif f("DD") then
|
||||
return s("D", true)
|
||||
elseif f("AA") then
|
||||
return s("A", true)
|
||||
elseif f("U") then
|
||||
return s(f("A") and "A" or "D", true)
|
||||
end
|
||||
|
||||
local m = f("^([MADRC])")
|
||||
if m then
|
||||
return s(m, nil, true)
|
||||
end
|
||||
m = f("([MADRC])$")
|
||||
if m then
|
||||
return s(m)
|
||||
end
|
||||
error("unknown status: " .. xy)
|
||||
end
|
||||
|
||||
---@param a string
|
||||
---@param b string
|
||||
function M.merge_status(a, b)
|
||||
if a == b then
|
||||
return a
|
||||
end
|
||||
local as = M.git_status(a)
|
||||
local bs = M.git_status(b)
|
||||
if as.unmerged or bs.unmerged then
|
||||
return as.priority > bs.priority and as.xy or bs.xy
|
||||
end
|
||||
if not as.staged or not bs.staged then
|
||||
if as.status == bs.status then
|
||||
return as.staged and b or a
|
||||
end
|
||||
return " M"
|
||||
end
|
||||
return "M "
|
||||
end
|
||||
|
||||
return M
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue