chore: initial commit
This commit is contained in:
+904
@@ -0,0 +1,904 @@
|
||||
local commit = require("git.commit")
|
||||
local object = require("git.object")
|
||||
local repo = require("git.core.repo")
|
||||
local util = require("git.core.util")
|
||||
|
||||
local M = {}
|
||||
|
||||
---@alias ow.Git.Cmd.Run fun(r: ow.Git.Repo, args: string[])
|
||||
|
||||
---@type string[]?
|
||||
local cached_cmds
|
||||
|
||||
---@return string[]
|
||||
local function git_cmds()
|
||||
if cached_cmds then
|
||||
return cached_cmds
|
||||
end
|
||||
local out = util.git({ "--list-cmds=main,others,alias" })
|
||||
if not out then
|
||||
return {}
|
||||
end
|
||||
cached_cmds = {}
|
||||
for line in out:gmatch("[^\r\n]+") do
|
||||
if line ~= "" then
|
||||
table.insert(cached_cmds, line)
|
||||
end
|
||||
end
|
||||
table.sort(cached_cmds)
|
||||
return cached_cmds
|
||||
end
|
||||
|
||||
---@param tok string
|
||||
---@return boolean
|
||||
local function is_expansion_target(tok)
|
||||
local first = tok:sub(1, 1)
|
||||
if first == "%" or first == "#" then
|
||||
return true
|
||||
end
|
||||
if tok:match("^<%w+>") then
|
||||
return true
|
||||
end
|
||||
if tok == "~" or tok:sub(1, 2) == "~/" then
|
||||
return true
|
||||
end
|
||||
return false
|
||||
end
|
||||
|
||||
---@param line string
|
||||
---@param i integer
|
||||
---@param buf string[]
|
||||
---@param escapes string?
|
||||
---@return integer
|
||||
local function parse_quoted(line, i, buf, escapes)
|
||||
local quote = line:sub(i, i)
|
||||
local n = #line
|
||||
i = i + 1
|
||||
while i <= n do
|
||||
local c = line:sub(i, i)
|
||||
if c == quote then
|
||||
return i + 1
|
||||
elseif escapes and c == "\\" and i < n then
|
||||
local nxt = line:sub(i + 1, i + 1)
|
||||
if escapes:find(nxt, 1, true) then
|
||||
table.insert(buf, nxt)
|
||||
i = i + 2
|
||||
else
|
||||
table.insert(buf, c)
|
||||
i = i + 1
|
||||
end
|
||||
else
|
||||
table.insert(buf, c)
|
||||
i = i + 1
|
||||
end
|
||||
end
|
||||
return i
|
||||
end
|
||||
|
||||
---@param line string
|
||||
---@return string[]
|
||||
function M.parse_args(line)
|
||||
local args = {}
|
||||
local i, n = 1, #line
|
||||
while i <= n do
|
||||
local c = line:sub(i, i)
|
||||
if c == " " or c == "\t" then
|
||||
i = i + 1
|
||||
else
|
||||
local buf = {}
|
||||
local quoted = false
|
||||
while i <= n do
|
||||
c = line:sub(i, i)
|
||||
if c == " " or c == "\t" then
|
||||
break
|
||||
elseif c == "\\" and i < n then
|
||||
table.insert(buf, line:sub(i + 1, i + 1))
|
||||
i = i + 2
|
||||
elseif c == '"' then
|
||||
quoted = true
|
||||
i = parse_quoted(line, i, buf, '"\\$`')
|
||||
elseif c == "'" then
|
||||
quoted = true
|
||||
i = parse_quoted(line, i, buf, nil)
|
||||
else
|
||||
table.insert(buf, c)
|
||||
i = i + 1
|
||||
end
|
||||
end
|
||||
local tok = table.concat(buf)
|
||||
if not quoted and is_expansion_target(tok) then
|
||||
local expanded = vim.fn.expand(tok) --[[@as string]]
|
||||
if expanded ~= "" then
|
||||
tok = expanded
|
||||
end
|
||||
end
|
||||
table.insert(args, tok)
|
||||
end
|
||||
end
|
||||
return args
|
||||
end
|
||||
|
||||
---@param name string
|
||||
---@return integer buf
|
||||
local function place_split(name)
|
||||
-- bufadd resolves the name the same way nvim_buf_set_name does
|
||||
-- (cwd-prefixing for non-absolute names), so calling it twice with
|
||||
-- the same name returns the same buffer.
|
||||
local buf = vim.fn.bufadd(name)
|
||||
if not vim.api.nvim_buf_is_loaded(buf) then
|
||||
vim.fn.bufload(buf)
|
||||
util.setup_scratch(buf, { bufhidden = "hide" })
|
||||
end
|
||||
local win = vim.fn.bufwinid(buf)
|
||||
if win ~= -1 then
|
||||
vim.api.nvim_set_current_win(win)
|
||||
else
|
||||
util.place_buf(buf, nil)
|
||||
end
|
||||
return buf
|
||||
end
|
||||
|
||||
---@param buf integer
|
||||
local function clear_undo(buf)
|
||||
local saved = vim.bo[buf].undolevels
|
||||
vim.bo[buf].undolevels = -1
|
||||
vim.bo[buf].modifiable = true
|
||||
vim.api.nvim_buf_call(buf, function()
|
||||
vim.cmd('silent! exe "normal! a \\<BS>\\<Esc>"')
|
||||
end)
|
||||
vim.bo[buf].modifiable = false
|
||||
vim.bo[buf].undolevels = saved
|
||||
end
|
||||
|
||||
---@param buf integer
|
||||
local function attach_history_keys(buf)
|
||||
local function bypass(fn)
|
||||
return function()
|
||||
vim.bo[buf].modifiable = true
|
||||
pcall(fn)
|
||||
vim.bo[buf].modifiable = false
|
||||
end
|
||||
end
|
||||
vim.keymap.set(
|
||||
"n",
|
||||
"u",
|
||||
bypass(vim.cmd.undo),
|
||||
{ buffer = buf, desc = "Undo" }
|
||||
)
|
||||
vim.keymap.set(
|
||||
"n",
|
||||
"<C-r>",
|
||||
bypass(vim.cmd.redo),
|
||||
{ buffer = buf, desc = "Redo" }
|
||||
)
|
||||
end
|
||||
|
||||
---@param r ow.Git.Repo
|
||||
---@param args string[]
|
||||
---@param ft string
|
||||
local function run_in_split(r, args, ft)
|
||||
util.git(args, {
|
||||
cwd = r.worktree,
|
||||
on_exit = function(result)
|
||||
if result.code ~= 0 then
|
||||
util.error(
|
||||
"git %s failed: %s",
|
||||
args[1] or "?",
|
||||
vim.trim(result.stderr or "")
|
||||
)
|
||||
return
|
||||
end
|
||||
local stdout = result.stdout or ""
|
||||
local buf = place_split("[Git " .. table.concat(args, " ") .. "]")
|
||||
repo.bind(buf, r)
|
||||
object.attach_dispatch(buf)
|
||||
attach_history_keys(buf)
|
||||
local state = r:state(buf) --[[@as -nil]]
|
||||
vim.bo[buf].filetype = ft
|
||||
-- Force a new undo block so each rerun is its own undo step.
|
||||
vim.bo[buf].undolevels = vim.bo[buf].undolevels
|
||||
local first_run = not state.initialized
|
||||
util.set_buf_lines(buf, 0, -1, util.split_lines(stdout))
|
||||
if first_run then
|
||||
clear_undo(buf)
|
||||
state.initialized = true
|
||||
end
|
||||
end,
|
||||
})
|
||||
end
|
||||
|
||||
---@param r ow.Git.Repo
|
||||
---@param args string[]
|
||||
local function run_to_messages(r, args)
|
||||
local cmd = { "git" }
|
||||
vim.list_extend(cmd, args)
|
||||
local result = vim.system(cmd, {
|
||||
cwd = r.worktree,
|
||||
text = true,
|
||||
env = util.DEFAULT_GIT_ENV,
|
||||
}):wait()
|
||||
local out = vim.trim(result.stdout or "")
|
||||
local err = vim.trim(result.stderr or "")
|
||||
local failed = result.code ~= 0
|
||||
|
||||
local chunks = {}
|
||||
if out ~= "" then
|
||||
table.insert(chunks, { out })
|
||||
end
|
||||
if err ~= "" then
|
||||
if #chunks > 0 then
|
||||
table.insert(chunks, { "\n" })
|
||||
end
|
||||
table.insert(chunks, { err, failed and "ErrorMsg" or nil })
|
||||
end
|
||||
if #chunks == 0 and failed then
|
||||
table.insert(
|
||||
chunks,
|
||||
{ "git exited " .. tostring(result.code), "ErrorMsg" }
|
||||
)
|
||||
end
|
||||
if #chunks > 0 then
|
||||
vim.api.nvim_echo(chunks, failed or err ~= "", {})
|
||||
end
|
||||
end
|
||||
|
||||
---@return integer
|
||||
local function find_or_create_preview_win()
|
||||
for _, w in ipairs(vim.api.nvim_tabpage_list_wins(0)) do
|
||||
if vim.wo[w].previewwindow then
|
||||
return w
|
||||
end
|
||||
end
|
||||
vim.cmd(("botright %dnew"):format(vim.o.previewheight))
|
||||
local w = vim.api.nvim_get_current_win()
|
||||
vim.wo[w].previewwindow = true
|
||||
return w
|
||||
end
|
||||
|
||||
---@param r ow.Git.Repo
|
||||
---@param args string[]
|
||||
local function run_in_preview(r, args)
|
||||
local pwin = find_or_create_preview_win()
|
||||
|
||||
local buf = vim.api.nvim_create_buf(false, true)
|
||||
vim.bo[buf].bufhidden = "wipe"
|
||||
vim.api.nvim_win_set_buf(pwin, buf)
|
||||
|
||||
vim.api.nvim_set_current_win(pwin)
|
||||
local cmd = { "git" }
|
||||
vim.list_extend(cmd, args)
|
||||
local job = vim.fn.jobstart(cmd, {
|
||||
cwd = r.worktree,
|
||||
term = true,
|
||||
})
|
||||
|
||||
if job <= 0 then
|
||||
util.error("failed to start git job")
|
||||
return
|
||||
end
|
||||
|
||||
vim.api.nvim_create_autocmd("BufWipeout", {
|
||||
buffer = buf,
|
||||
once = true,
|
||||
callback = function()
|
||||
pcall(vim.fn.jobstop, job)
|
||||
end,
|
||||
})
|
||||
|
||||
vim.keymap.set("n", "q", "<cmd>pclose<cr>", {
|
||||
buffer = buf,
|
||||
nowait = true,
|
||||
desc = "Close preview",
|
||||
})
|
||||
vim.keymap.set("n", "<C-c>", function()
|
||||
pcall(vim.fn.jobstop, job)
|
||||
end, { buffer = buf, nowait = true, desc = "Cancel git job" })
|
||||
end
|
||||
|
||||
---@param ft string
|
||||
---@return ow.Git.Cmd.Run
|
||||
local function in_split(ft)
|
||||
return function(r, args)
|
||||
run_in_split(r, args, ft)
|
||||
end
|
||||
end
|
||||
|
||||
---@param line string
|
||||
---@return string
|
||||
local function clean_progress_line(line)
|
||||
if not (line:find("\27", 1, true) or line:find("\r", 1, true)) then
|
||||
return line
|
||||
end
|
||||
line = line:gsub("\27%[[%d;?]*[%a]", "")
|
||||
local parts = vim.split(line, "\r", { plain = true })
|
||||
for i = #parts, 1, -1 do
|
||||
if parts[i] ~= "" then
|
||||
return parts[i]
|
||||
end
|
||||
end
|
||||
return ""
|
||||
end
|
||||
|
||||
---@param lines string[]
|
||||
---@param fallback string
|
||||
---@return [string, string?][]
|
||||
local function format_error_dump(lines, fallback)
|
||||
if #lines == 0 then
|
||||
return { { fallback, "ErrorMsg" } }
|
||||
end
|
||||
local chunks = {}
|
||||
local matched = false
|
||||
for i, line in ipairs(lines) do
|
||||
if i > 1 then
|
||||
table.insert(chunks, { "\n" })
|
||||
end
|
||||
if line:match("^fatal:") or line:match("^error:") then
|
||||
table.insert(chunks, { line, "ErrorMsg" })
|
||||
matched = true
|
||||
else
|
||||
table.insert(chunks, { line })
|
||||
end
|
||||
end
|
||||
if not matched then
|
||||
return { { table.concat(lines, "\n"), "ErrorMsg" } }
|
||||
end
|
||||
return chunks
|
||||
end
|
||||
|
||||
---@param r ow.Git.Repo
|
||||
---@param args string[]
|
||||
local function run_streaming(r, args)
|
||||
local title = "git " .. (args[1] or "")
|
||||
local id = "git." .. tostring(vim.uv.hrtime())
|
||||
---@type string[]
|
||||
local accum = {}
|
||||
local partial = ""
|
||||
local last_progress = ""
|
||||
|
||||
---@param text string
|
||||
---@param status "running"|"success"|"failed"
|
||||
local function emit_progress(text, status)
|
||||
vim.api.nvim_echo({ { text } }, false, {
|
||||
id = id,
|
||||
kind = "progress",
|
||||
status = status,
|
||||
title = title,
|
||||
source = "git",
|
||||
})
|
||||
end
|
||||
|
||||
local function on_data(_, data, _)
|
||||
if not data or #data == 0 then
|
||||
return
|
||||
end
|
||||
if #data == 1 and data[1] == "" then
|
||||
return
|
||||
end
|
||||
partial = partial .. data[1]
|
||||
local prev = last_progress
|
||||
for i = 2, #data do
|
||||
local cleaned = clean_progress_line(partial)
|
||||
if cleaned ~= "" then
|
||||
table.insert(accum, cleaned)
|
||||
last_progress = cleaned
|
||||
end
|
||||
partial = data[i]
|
||||
end
|
||||
if partial ~= "" then
|
||||
local cleaned = clean_progress_line(partial)
|
||||
if cleaned ~= "" then
|
||||
last_progress = cleaned
|
||||
end
|
||||
end
|
||||
if last_progress ~= prev then
|
||||
emit_progress(last_progress, "running")
|
||||
end
|
||||
end
|
||||
|
||||
local function on_exit(_, code)
|
||||
if partial ~= "" then
|
||||
local cleaned = clean_progress_line(partial)
|
||||
if cleaned ~= "" then
|
||||
table.insert(accum, cleaned)
|
||||
last_progress = cleaned
|
||||
end
|
||||
partial = ""
|
||||
end
|
||||
if code == 0 then
|
||||
emit_progress(
|
||||
last_progress ~= "" and last_progress or "done",
|
||||
"success"
|
||||
)
|
||||
else
|
||||
emit_progress(("exit %d"):format(code), "failed")
|
||||
local fallback = ("%s failed: exit %d"):format(title, code)
|
||||
vim.api.nvim_echo(format_error_dump(accum, fallback), true, {})
|
||||
end
|
||||
end
|
||||
|
||||
local cmd = { "git" }
|
||||
vim.list_extend(cmd, args)
|
||||
local job = vim.fn.jobstart(cmd, {
|
||||
cwd = r.worktree,
|
||||
pty = true,
|
||||
env = util.DEFAULT_GIT_ENV,
|
||||
on_stdout = on_data,
|
||||
on_stderr = on_data,
|
||||
on_exit = on_exit,
|
||||
})
|
||||
if job <= 0 then
|
||||
util.error("failed to start git job")
|
||||
end
|
||||
end
|
||||
|
||||
---@type table<string, ow.Git.Cmd.Run>
|
||||
local HANDLERS = {
|
||||
log = in_split("git"),
|
||||
diff = in_split("git"),
|
||||
push = run_streaming,
|
||||
fetch = run_streaming,
|
||||
pull = run_streaming,
|
||||
clone = run_streaming,
|
||||
am = run_streaming,
|
||||
["cherry-pick"] = run_streaming,
|
||||
revert = run_streaming,
|
||||
}
|
||||
|
||||
---@param args string[]
|
||||
---@return boolean
|
||||
local function has_message(args)
|
||||
for _, a in ipairs(args) do
|
||||
if
|
||||
a == "-m"
|
||||
or a == "--message"
|
||||
or a:match("^%-%-message=")
|
||||
or a:match("^%-m")
|
||||
then
|
||||
return true
|
||||
end
|
||||
end
|
||||
return false
|
||||
end
|
||||
|
||||
---@param args string[]
|
||||
---@param opts { bang: boolean? }?
|
||||
function M.run(args, opts)
|
||||
local r = repo.resolve()
|
||||
if not r then
|
||||
util.error("not in a git repository")
|
||||
return
|
||||
end
|
||||
|
||||
local bang = opts and opts.bang or false
|
||||
|
||||
if bang then
|
||||
run_in_preview(r, args)
|
||||
return
|
||||
end
|
||||
|
||||
local sub = args[1]
|
||||
if sub == "commit" and not has_message(args) then
|
||||
commit.commit({ args = vim.list_slice(args, 2) })
|
||||
return
|
||||
end
|
||||
|
||||
if sub == "show" then
|
||||
if #args == 2 and args[2]:find(":", 1, true) then
|
||||
object.open(r, args[2])
|
||||
return
|
||||
end
|
||||
run_in_split(r, args, "git")
|
||||
return
|
||||
end
|
||||
|
||||
if sub == "cat-file" then
|
||||
if #args == 3 and args[2] == "-p" then
|
||||
object.open(r, args[3])
|
||||
return
|
||||
end
|
||||
run_in_split(r, args, "git")
|
||||
return
|
||||
end
|
||||
|
||||
local handler = sub and HANDLERS[sub]
|
||||
if handler then
|
||||
handler(r, args)
|
||||
else
|
||||
run_to_messages(r, args)
|
||||
end
|
||||
end
|
||||
|
||||
---@param items string[]
|
||||
---@param lead string
|
||||
---@return string[]
|
||||
local function prefix_filter(items, lead)
|
||||
return vim.tbl_filter(function(it)
|
||||
return vim.startswith(it, lead)
|
||||
end, items)
|
||||
end
|
||||
|
||||
---@param prefix string
|
||||
---@param dir string
|
||||
---@param name_lead string
|
||||
---@param entries string[]
|
||||
---@return string[]
|
||||
local function path_segments(prefix, dir, name_lead, entries)
|
||||
local matches = {}
|
||||
local seen = {}
|
||||
for _, full_path in ipairs(entries) do
|
||||
local rel = dir == "" and full_path or full_path:sub(#dir + 1)
|
||||
local slash = rel:find("/", 1, true)
|
||||
local segment = slash and rel:sub(1, slash) or rel
|
||||
if not seen[segment] and segment:sub(1, #name_lead) == name_lead then
|
||||
seen[segment] = true
|
||||
table.insert(matches, prefix .. dir .. segment)
|
||||
end
|
||||
end
|
||||
return matches
|
||||
end
|
||||
|
||||
---@param r ow.Git.Repo
|
||||
---@param dir string
|
||||
---@return string[]
|
||||
local function list_files(r, dir)
|
||||
return r:get_cached("files:" .. dir, function(self)
|
||||
local args = { "ls-files" }
|
||||
if dir ~= "" then
|
||||
table.insert(args, dir)
|
||||
end
|
||||
local out = util.git(args, { cwd = self.worktree, silent = true })
|
||||
return out and util.split_lines(out) or {}
|
||||
end)
|
||||
end
|
||||
|
||||
---@param r ow.Git.Repo
|
||||
---@return string[]
|
||||
local function list_remotes(r)
|
||||
return r:get_cached("remotes", function(self)
|
||||
local out = util.git(
|
||||
{ "remote" },
|
||||
{ cwd = self.worktree, silent = true }
|
||||
)
|
||||
return out and util.split_lines(out) or {}
|
||||
end)
|
||||
end
|
||||
|
||||
---@type table<string, string[]>
|
||||
local SUBSUB_FALLBACK = {
|
||||
submodule = {
|
||||
"add",
|
||||
"status",
|
||||
"init",
|
||||
"deinit",
|
||||
"update",
|
||||
"summary",
|
||||
"foreach",
|
||||
"sync",
|
||||
"absorbgitdirs",
|
||||
},
|
||||
}
|
||||
|
||||
---@type table<string, string[]>
|
||||
local cached_completions = {}
|
||||
|
||||
---@param sub string
|
||||
---@return string[]
|
||||
local function fetch_completions(sub)
|
||||
if cached_completions[sub] then
|
||||
return cached_completions[sub]
|
||||
end
|
||||
local out = util.git(
|
||||
{ sub, "--git-completion-helper-all" },
|
||||
{ silent = true }
|
||||
) or util.git({ sub, "--git-completion-helper" }, { silent = true })
|
||||
local items = {}
|
||||
if out then
|
||||
for tok in out:gmatch("%S+") do
|
||||
table.insert(items, tok)
|
||||
end
|
||||
end
|
||||
cached_completions[sub] = items
|
||||
return items
|
||||
end
|
||||
|
||||
---@param sub string
|
||||
---@return string[]
|
||||
local function fetch_subsubcommands(sub)
|
||||
local subs = {}
|
||||
for _, it in ipairs(fetch_completions(sub)) do
|
||||
if it:sub(1, 1) ~= "-" and it ~= "--" then
|
||||
table.insert(subs, it)
|
||||
end
|
||||
end
|
||||
if #subs == 0 and SUBSUB_FALLBACK[sub] then
|
||||
return SUBSUB_FALLBACK[sub]
|
||||
end
|
||||
return subs
|
||||
end
|
||||
|
||||
---@param sub string
|
||||
---@return string[]
|
||||
local function fetch_flags(sub)
|
||||
local flags = {}
|
||||
for _, it in ipairs(fetch_completions(sub)) do
|
||||
if it:sub(1, 1) == "-" and it ~= "--" then
|
||||
table.insert(flags, it)
|
||||
end
|
||||
end
|
||||
return flags
|
||||
end
|
||||
|
||||
---@param r ow.Git.Repo
|
||||
---@param lead string
|
||||
---@return string[]
|
||||
local function complete_tracked_paths(r, lead)
|
||||
local dir, name_lead = lead:match("^(.*/)([^/]*)$")
|
||||
dir = dir or ""
|
||||
name_lead = name_lead or lead
|
||||
return path_segments("", dir, name_lead, list_files(r, dir))
|
||||
end
|
||||
|
||||
---@param r ow.Git.Repo
|
||||
---@param lead string
|
||||
---@return string[]
|
||||
local function complete_unstaged_paths(r, lead)
|
||||
local matches = {}
|
||||
for path, entry in pairs(r.status.entries) do
|
||||
if path:sub(1, #lead) == lead then
|
||||
local include = entry.kind == "untracked"
|
||||
or entry.kind == "unmerged"
|
||||
if not include and entry.kind == "changed" then
|
||||
---@cast entry ow.Git.Status.ChangedEntry
|
||||
include = entry.unstaged ~= nil
|
||||
end
|
||||
if include then
|
||||
table.insert(matches, path)
|
||||
end
|
||||
end
|
||||
end
|
||||
table.sort(matches)
|
||||
return matches
|
||||
end
|
||||
|
||||
---@param arg_lead string
|
||||
---@return string[]
|
||||
function M.complete_rev(arg_lead)
|
||||
local r = repo.resolve()
|
||||
if not r then
|
||||
return {}
|
||||
end
|
||||
|
||||
local stage, stage_path_lead = arg_lead:match("^:([0-3]):(.*)$")
|
||||
if stage then
|
||||
local out = util.git(
|
||||
{ "ls-files", "--stage" },
|
||||
{ cwd = r.worktree, silent = true }
|
||||
)
|
||||
if not out then
|
||||
return {}
|
||||
end
|
||||
local matches = {}
|
||||
for _, line in ipairs(util.split_lines(out)) do
|
||||
local row_stage, row_path = line:match("^%S+ %S+ (%d)\t(.*)$")
|
||||
if
|
||||
row_stage == stage
|
||||
and row_path
|
||||
and row_path:sub(1, #stage_path_lead) == stage_path_lead
|
||||
then
|
||||
table.insert(matches, ":" .. stage .. ":" .. row_path)
|
||||
end
|
||||
end
|
||||
return matches
|
||||
end
|
||||
|
||||
local colon = arg_lead:find(":", 1, true)
|
||||
if not colon then
|
||||
local refs = {}
|
||||
vim.list_extend(refs, r:list_refs())
|
||||
vim.list_extend(refs, r:list_pseudo_refs())
|
||||
vim.list_extend(refs, r:list_stash_refs())
|
||||
return prefix_filter(refs, arg_lead)
|
||||
end
|
||||
|
||||
local rev = arg_lead:sub(1, colon - 1)
|
||||
local path_lead = arg_lead:sub(colon + 1)
|
||||
local dir, name_lead = path_lead:match("^(.*/)([^/]*)$")
|
||||
dir = dir or ""
|
||||
name_lead = name_lead or path_lead
|
||||
|
||||
if rev ~= "" then
|
||||
local args = { "ls-tree", rev }
|
||||
if dir ~= "" then
|
||||
table.insert(args, dir)
|
||||
end
|
||||
local out = util.git(args, { cwd = r.worktree, silent = true })
|
||||
if not out then
|
||||
return {}
|
||||
end
|
||||
local matches = {}
|
||||
for _, line in ipairs(util.split_lines(out)) do
|
||||
local typ, full_path = line:match("^%S+ (%S+) %S+\t(.*)$")
|
||||
if typ and full_path then
|
||||
local basename = dir == "" and full_path
|
||||
or full_path:sub(#dir + 1)
|
||||
if typ == "tree" then
|
||||
basename = basename .. "/"
|
||||
end
|
||||
if basename:sub(1, #name_lead) == name_lead then
|
||||
table.insert(matches, rev .. ":" .. dir .. basename)
|
||||
end
|
||||
end
|
||||
end
|
||||
return matches
|
||||
end
|
||||
|
||||
return path_segments(":", dir, name_lead, list_files(r, dir))
|
||||
end
|
||||
|
||||
---@alias ow.Git.Cmd.Handler fun(r: ow.Git.Repo, lead: string, sub: string, idx: integer): string[]
|
||||
---@alias ow.Git.Cmd.Slot ow.Git.Cmd.Handler | ow.Git.Cmd.Handler[]
|
||||
|
||||
---@param r ow.Git.Repo
|
||||
---@param lead string
|
||||
---@return string[]
|
||||
local function complete_remote(r, lead)
|
||||
return prefix_filter(list_remotes(r), lead)
|
||||
end
|
||||
|
||||
---@param r ow.Git.Repo
|
||||
---@param lead string
|
||||
---@return string[]
|
||||
local function complete_ref(r, lead)
|
||||
return prefix_filter(r:list_refs(), lead)
|
||||
end
|
||||
|
||||
---@param r ow.Git.Repo
|
||||
---@param lead string
|
||||
---@return string[]
|
||||
local function complete_pseudo_ref(r, lead)
|
||||
return prefix_filter(r:list_pseudo_refs(), lead)
|
||||
end
|
||||
|
||||
---@param r ow.Git.Repo
|
||||
---@param lead string
|
||||
---@return string[]
|
||||
local function complete_stash_ref(r, lead)
|
||||
return prefix_filter(r:list_stash_refs(), lead)
|
||||
end
|
||||
|
||||
---@param _ ow.Git.Repo
|
||||
---@param lead string
|
||||
---@return string[]
|
||||
local function complete_rev(_, lead)
|
||||
return M.complete_rev(lead)
|
||||
end
|
||||
|
||||
---@param _ ow.Git.Repo
|
||||
---@param lead string
|
||||
---@param sub string
|
||||
---@param idx integer
|
||||
---@return string[]
|
||||
local function complete_subsubcmd(_, lead, sub, idx)
|
||||
if idx ~= 1 then
|
||||
return {}
|
||||
end
|
||||
return prefix_filter(fetch_subsubcommands(sub), lead)
|
||||
end
|
||||
|
||||
local ALL_REFS = { complete_ref, complete_pseudo_ref, complete_stash_ref }
|
||||
local REV_OR_PATH = { complete_rev, complete_tracked_paths }
|
||||
|
||||
---@type table<string, ow.Git.Cmd.Slot[]>
|
||||
local POSITIONAL_HANDLER = {
|
||||
push = { complete_remote, ALL_REFS },
|
||||
pull = { complete_remote, ALL_REFS },
|
||||
fetch = { complete_remote, ALL_REFS },
|
||||
checkout = { REV_OR_PATH },
|
||||
reset = { REV_OR_PATH },
|
||||
restore = { complete_tracked_paths },
|
||||
add = { complete_unstaged_paths },
|
||||
rm = { complete_tracked_paths },
|
||||
mv = { complete_tracked_paths },
|
||||
blame = { complete_tracked_paths },
|
||||
branch = { complete_ref },
|
||||
switch = { complete_ref },
|
||||
merge = { ALL_REFS },
|
||||
rebase = { ALL_REFS },
|
||||
["cherry-pick"] = { ALL_REFS },
|
||||
revert = { ALL_REFS },
|
||||
tag = { ALL_REFS },
|
||||
log = { REV_OR_PATH },
|
||||
diff = { REV_OR_PATH },
|
||||
show = { complete_rev },
|
||||
["cat-file"] = { complete_rev },
|
||||
stash = { complete_subsubcmd },
|
||||
remote = { complete_subsubcmd },
|
||||
worktree = { complete_subsubcmd },
|
||||
bisect = { complete_subsubcmd },
|
||||
submodule = { complete_subsubcmd },
|
||||
}
|
||||
|
||||
---@class ow.Git.Cmd.CompleteState
|
||||
---@field prior string[] -- positional and flag tokens before the current arg_lead
|
||||
---@field after_separator boolean -- whether `--` appeared in prior
|
||||
|
||||
---@param cmd_line string
|
||||
---@return ow.Git.Cmd.CompleteState
|
||||
local function parse_complete_state(cmd_line)
|
||||
local rest = cmd_line:gsub("^%s*%S+%s*", "", 1)
|
||||
local trailing_space = rest == "" or rest:sub(-1):match("%s") ~= nil
|
||||
local tokens = vim.split(vim.trim(rest), "%s+", { trimempty = true })
|
||||
local prior = trailing_space and tokens
|
||||
or vim.list_slice(tokens, 1, #tokens - 1)
|
||||
local after_separator = false
|
||||
for _, t in ipairs(prior) do
|
||||
if t == "--" then
|
||||
after_separator = true
|
||||
break
|
||||
end
|
||||
end
|
||||
return { prior = prior, after_separator = after_separator }
|
||||
end
|
||||
|
||||
---@param prior string[] -- includes the subcommand at index 1
|
||||
---@return integer
|
||||
local function positional_index(prior)
|
||||
local pos = 0
|
||||
for i = 2, #prior do
|
||||
if prior[i]:sub(1, 1) ~= "-" then
|
||||
pos = pos + 1
|
||||
end
|
||||
end
|
||||
return pos + 1
|
||||
end
|
||||
|
||||
---@param arg_lead string
|
||||
---@param cmd_line string
|
||||
---@return string[]
|
||||
function M.complete(arg_lead, cmd_line, _)
|
||||
local state = parse_complete_state(cmd_line)
|
||||
local prior = state.prior
|
||||
|
||||
if #prior == 0 then
|
||||
return prefix_filter(git_cmds(), arg_lead)
|
||||
end
|
||||
|
||||
local sub = prior[1] --[[@as string]]
|
||||
|
||||
if arg_lead:sub(1, 1) == "-" then
|
||||
return prefix_filter(fetch_flags(sub), arg_lead)
|
||||
end
|
||||
|
||||
local r = repo.resolve()
|
||||
if not r then
|
||||
return {}
|
||||
end
|
||||
|
||||
if state.after_separator then
|
||||
return complete_tracked_paths(r, arg_lead)
|
||||
end
|
||||
|
||||
local handlers = POSITIONAL_HANDLER[sub]
|
||||
if not handlers then
|
||||
return complete_tracked_paths(r, arg_lead)
|
||||
end
|
||||
|
||||
local idx = positional_index(prior)
|
||||
local slot = handlers[idx] or handlers[#handlers]
|
||||
if not slot then
|
||||
return {}
|
||||
end
|
||||
if type(slot) == "function" then
|
||||
return slot(r, arg_lead, sub, idx)
|
||||
end
|
||||
local result = {}
|
||||
for _, fn in ipairs(slot) do
|
||||
vim.list_extend(result, fn(r, arg_lead, sub, idx))
|
||||
end
|
||||
return result
|
||||
end
|
||||
|
||||
M._parse_complete_state = parse_complete_state
|
||||
M._positional_index = positional_index
|
||||
|
||||
return M
|
||||
Reference in New Issue
Block a user