808 lines
21 KiB
Lua
808 lines
21 KiB
Lua
local commit = require("git.commit")
|
|
local object = require("git.object")
|
|
local repo = require("git.repo")
|
|
local util = require("git.util")
|
|
|
|
local M = {}
|
|
|
|
---@class ow.Git.Cmd.SplitHandler
|
|
---@field ft string
|
|
---@field needs_rev boolean?
|
|
---@field on_state? fun(state: ow.Git.Repo.BufState, r: ow.Git.Repo, args: string[])
|
|
|
|
---@param r ow.Git.Repo
|
|
---@param args string[] -- diff args including leading "diff"
|
|
---@return string left_ref
|
|
---@return string? right_ref -- nil means worktree
|
|
local function compute_diff_refs(r, args)
|
|
local cached = false
|
|
local positional = {} ---@type string[]
|
|
local saw_separator = false
|
|
for i = 2, #args do
|
|
local a = args[i]
|
|
if saw_separator then
|
|
break
|
|
elseif a == "--" then
|
|
saw_separator = true
|
|
elseif a == "--cached" or a == "--staged" then
|
|
cached = true
|
|
elseif a:sub(1, 1) ~= "-" then
|
|
table.insert(positional, a)
|
|
end
|
|
end
|
|
|
|
local function defaults()
|
|
if cached then
|
|
return "HEAD", ":"
|
|
end
|
|
return ":", nil
|
|
end
|
|
|
|
if #positional == 0 then
|
|
return defaults()
|
|
end
|
|
|
|
local first = positional[1] --[[@as string]]
|
|
if #positional == 1 then
|
|
local lhs, rhs = first:match("^(.-)%.%.%.(.+)$")
|
|
if lhs then
|
|
return (lhs ~= "" and lhs or "HEAD"), rhs
|
|
end
|
|
lhs, rhs = first:match("^(.-)%.%.(.+)$")
|
|
if lhs then
|
|
return (lhs ~= "" and lhs or "HEAD"),
|
|
(rhs ~= "" and rhs or "HEAD")
|
|
end
|
|
if r:rev_parse(first, true) then
|
|
if cached then
|
|
return first, ":"
|
|
end
|
|
return first, nil
|
|
end
|
|
return defaults()
|
|
end
|
|
|
|
local second = positional[2] --[[@as string]]
|
|
local first_ok = r:rev_parse(first, true) ~= nil
|
|
if first_ok and r:rev_parse(second, true) then
|
|
return first, second
|
|
end
|
|
if first_ok then
|
|
if cached then
|
|
return first, ":"
|
|
end
|
|
return first, nil
|
|
end
|
|
return defaults()
|
|
end
|
|
|
|
---@type table<string, ow.Git.Cmd.SplitHandler>
|
|
local SPLIT_HANDLERS = {
|
|
log = { ft = "git" },
|
|
diff = {
|
|
ft = "gitdiff",
|
|
on_state = function(state, r, args)
|
|
local left, right = compute_diff_refs(r, args)
|
|
state.left_ref = left
|
|
state.right_ref = right
|
|
end,
|
|
},
|
|
}
|
|
|
|
M._compute_diff_refs = compute_diff_refs
|
|
|
|
---@type string[]?
|
|
local cached_cmds
|
|
|
|
---@return string[]
|
|
local function git_cmds()
|
|
if cached_cmds then
|
|
return cached_cmds
|
|
end
|
|
local result = vim.system(
|
|
{ "git", "--list-cmds=main,others,alias" },
|
|
{ text = true }
|
|
)
|
|
:wait()
|
|
if result.code ~= 0 then
|
|
util.error("git --list-cmds failed: %s", vim.trim(result.stderr or ""))
|
|
return {}
|
|
end
|
|
cached_cmds = {}
|
|
for line in (result.stdout or ""):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 args string[]
|
|
---@param start integer
|
|
---@return string?
|
|
local function first_positional(args, start)
|
|
for i = start, #args do
|
|
local a = args[i]
|
|
if a:sub(1, 1) ~= "-" then
|
|
return a
|
|
end
|
|
end
|
|
end
|
|
|
|
---@param name string
|
|
---@return integer buf
|
|
local function place_split(name)
|
|
local buf = vim.fn.bufnr("\\V" .. name)
|
|
if buf == -1 then
|
|
buf = util.new_scratch({ name = name, bufhidden = "hide" })
|
|
return buf
|
|
end
|
|
if not vim.api.nvim_buf_is_loaded(buf) then
|
|
vim.fn.bufload(buf)
|
|
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 conf ow.Git.Cmd.SplitHandler
|
|
local function run_in_split(r, args, conf)
|
|
local cmd = { "git" }
|
|
vim.list_extend(cmd, args)
|
|
util.exec(cmd, {
|
|
cwd = r.worktree,
|
|
on_done = function(stdout)
|
|
if not stdout then
|
|
return
|
|
end
|
|
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]]
|
|
state.sha = nil
|
|
state.parent_sha = nil
|
|
if conf.needs_rev then
|
|
local user_rev = first_positional(args, 2) or "HEAD"
|
|
local sha = r:rev_parse(user_rev, true)
|
|
if sha then
|
|
state.sha = sha
|
|
state.parent_sha = r:rev_parse(user_rev .. "^", true)
|
|
end
|
|
end
|
|
if conf.on_state then
|
|
conf.on_state(state, r, args)
|
|
end
|
|
vim.bo[buf].filetype = conf.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)
|
|
vim.system(
|
|
cmd,
|
|
{ cwd = r.worktree, text = true },
|
|
vim.schedule_wrap(function(obj)
|
|
local out = vim.trim(obj.stdout or "")
|
|
local err = vim.trim(obj.stderr or "")
|
|
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, "ErrorMsg" })
|
|
end
|
|
if #chunks == 0 and obj.code ~= 0 then
|
|
table.insert(
|
|
chunks,
|
|
{ "git exited " .. tostring(obj.code), "ErrorMsg" }
|
|
)
|
|
end
|
|
if #chunks > 0 then
|
|
vim.api.nvim_echo(chunks, true, {})
|
|
end
|
|
end)
|
|
)
|
|
end
|
|
|
|
---@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[]
|
|
function M.run(args)
|
|
local r = repo.resolve()
|
|
if not r then
|
|
util.error("not in a git repository")
|
|
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, { ft = "git", needs_rev = true })
|
|
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, { ft = "git", needs_rev = true })
|
|
return
|
|
end
|
|
|
|
local conf = sub and SPLIT_HANDLERS[sub]
|
|
if conf then
|
|
run_in_split(r, args, conf)
|
|
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 cmd = { "git", "ls-files" }
|
|
if dir ~= "" then
|
|
table.insert(cmd, dir)
|
|
end
|
|
local out = util.exec(cmd, { 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.exec(
|
|
{ "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.exec(
|
|
{ "git", sub, "--git-completion-helper-all" },
|
|
{ silent = true }
|
|
) or util.exec(
|
|
{ "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_list in pairs(r.status.entries) do
|
|
if path:sub(1, #lead) == lead then
|
|
for _, e in ipairs(entry_list) do
|
|
if
|
|
e.kind == "unstaged"
|
|
or e.kind == "untracked"
|
|
or e.kind == "unmerged"
|
|
then
|
|
table.insert(matches, path)
|
|
break
|
|
end
|
|
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.exec(
|
|
{ "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 cmd = { "git", "ls-tree", rev }
|
|
if dir ~= "" then
|
|
table.insert(cmd, dir)
|
|
end
|
|
local out = util.exec(cmd, { 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
|