Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
188 changes: 97 additions & 91 deletions lua/diffview/arg_parser.lua
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ local utils = lazy.require("diffview.utils") ---@module "diffview.utils"

local M = {}

local short_flag_pat = { "^%-(%a)=?(.*)", "^%+(%a)=?(.*)" }
local short_flag_pat = { "^[-+](%a)=?(.*)" }
local long_flag_pat = { "^%-%-(%a[%a%d-]*)=?(.*)", "^%+%+(%a[%a%d-]*)=?(.*)" }

---@class ArgObject : diffview.Object
Expand Down Expand Up @@ -231,55 +231,92 @@ function M.split_ex_range(arg)
end

---@class CmdLineContext
---@field args string[] The complete list of arguments.
---@field arg_lead string
---@field argidx integer Index of the current argument.
---@field divideridx integer
---@field range string? Ex command range.
---@field between boolean The current position is between two arguments.

---Scan an EX command string and split it into individual args.
---@field cmd_line string
---@field args string[] # The tokenized list of arguments.
---@field raw_args string[] # The unprocessed list of arguments. Contains syntax characters, such as quotes.
---@field arg_lead string # The leading part of the current argument.
---@field lead_quote string? # If present: the quote character used for the current argument.
---@field cur_pos integer # The cursor position in the command line.
---@field argidx integer # Index of the current argument.
---@field divideridx integer # The index of the end-of-options token. (default: math.huge)
---@field range string? # Ex command range.
---@field between boolean # The current position is between two arguments.

---@class arg_parser.scan.Opt
---@field cur_pos integer # The current cursor position in the command line.
---@field allow_quoted boolean # Everything between a pair of quotes should be treated as part of a single argument. (default: true)
---@field allow_ex_range boolean # The command line may contain an EX command range. (default: false)

---Tokenize a command line string.
---@param cmd_line string
---@param cur_pos number
---@param opt? arg_parser.scan.Opt
---@return CmdLineContext
function M.scan_ex_args(cmd_line, cur_pos)
function M.scan(cmd_line, opt)
opt = vim.tbl_extend("keep", opt or {}, {
cur_pos = #cmd_line + 1,
allow_quoted = true,
allow_ex_range = false,
}) --[[@as arg_parser.scan.Opt ]]

local args = {}
local raw_args = {}
local arg_lead
local divideridx = math.huge
local argidx
local between = false
local arg = ""
local cur_quote, lead_quote
local arg, raw_arg = "", ""

local h, i = -1, 1

while i <= #cmd_line do
local char = cmd_line:sub(i, i)

if not argidx and i > cur_pos then
if not argidx and i > opt.cur_pos then
argidx = #args + 1
arg_lead = arg
if h < cur_pos then between = true end
lead_quote = cur_quote
if h < opt.cur_pos then between = true end
end

if char == "\\" then
arg = arg .. char
raw_arg = raw_arg .. char
if i < #cmd_line then
i = i + 1
arg = arg .. cmd_line:sub(i, i)
raw_arg = raw_arg .. cmd_line:sub(i, i)
end
h = i
elseif cur_quote then
if char == cur_quote then
cur_quote = nil
else
arg = arg .. char
end
raw_arg = raw_arg .. char
h = i
elseif opt.allow_quoted and (char == [[']] or char == [["]]) then
cur_quote = char
raw_arg = raw_arg .. char
h = i
elseif char:match("%s") then
if arg ~= "" then
table.insert(args, arg)
if arg == "--" and i - 1 < #cmd_line then
divideridx = #args
end
end
if raw_arg ~= "" then
table.insert(raw_args, raw_arg)
end
arg = ""
raw_arg = ""
-- Skip whitespace
i = i + cmd_line:sub(i, -1):match("^%s+()") - 2
else
arg = arg .. char
raw_arg = raw_arg .. char
h = i
end

Expand All @@ -288,7 +325,11 @@ function M.scan_ex_args(cmd_line, cur_pos)

if #arg > 0 then
table.insert(args, arg)
if not arg_lead then arg_lead = arg end
table.insert(raw_args, raw_arg)
if not arg_lead then
arg_lead = arg
lead_quote = cur_quote
end

if arg == "--" and cmd_line:sub(#cmd_line, #cmd_line) ~= "-" then
divideridx = #args
Expand All @@ -305,110 +346,75 @@ function M.scan_ex_args(cmd_line, cur_pos)
local range

if #args > 0 then
range, args[1] = M.split_ex_range(args[1])
if opt.allow_ex_range then
range, args[1] = M.split_ex_range(args[1])
_, raw_args[1] = M.split_ex_range(raw_args[1])
end

if args[1] == "" then
table.remove(args, 1)
table.remove(raw_args, 1)
argidx = math.max(argidx - 1, 1)
divideridx = math.max(divideridx - 1, 1)
end
end

return {
cmd_line = cmd_line,
args = args,
raw_args = raw_args,
arg_lead = arg_lead or "",
lead_quote = lead_quote,
cur_pos = opt.cur_pos,
argidx = argidx,
divideridx = divideridx,
range = range ~= "" and range or nil,
between = between,
}
end

---Scan a shell-like string and split it into individual args. This scanner
---understands quoted args.
---@param cmd_line string
---@param cur_pos number
---@return CmdLineContext
function M.scan_sh_args(cmd_line, cur_pos)
local args = {}
local arg_lead
local divideridx = math.huge
local argidx
local between = false
local cur_quote
local arg = ""

local h, i = -1, 1
---Filter completion candidates.
---@param arg_lead string
---@param candidates string[]
---@return string[]
function M.filter_candidates(arg_lead, candidates)
arg_lead, _ = vim.pesc(arg_lead)

while i <= #cmd_line do
local char = cmd_line:sub(i, i)
return vim.tbl_filter(function(item)
return item:match(arg_lead)
end, candidates)
end

if not argidx and i > cur_pos then
argidx = #args + 1
arg_lead = arg
if h < cur_pos then between = true end
end
---Process completion candidates.
---@param candidates string[]
---@param ctx CmdLineContext
---@param input_cmp boolean? Completion for |input()|.
---@return string[]
function M.process_candidates(candidates, ctx, input_cmp)
if not candidates then return {} end

if char == "\\" then
if i < #cmd_line then
i = i + 1
arg = arg .. cmd_line:sub(i, i)
end
h = i
elseif cur_quote then
if char == cur_quote then
cur_quote = nil
else
arg = arg .. char
end
h = i
elseif char == [[']] or char == [["]] then
cur_quote = char
h = i
elseif char:match("%s") then
if arg ~= "" then
table.insert(args, arg)
if arg == "--" and i - 1 < #cmd_line then
divideridx = #args
end
end
arg = ""
-- Skip whitespace
i = i + cmd_line:sub(i, -1):match("^%s+()") - 2
else
arg = arg .. char
h = i
end
local cmd_lead = ""
local ex_lead = (ctx.lead_quote or "") .. ctx.arg_lead

i = i + 1
if ctx.arg_lead and ctx.arg_lead:find("[^\\]%s") then
ex_lead = (ctx.lead_quote or "") .. ctx.arg_lead:match(".*[^\\]%s(.*)")
end

if cur_quote then
error("The given command line contains a non-terminated string!")
if input_cmp then
cmd_lead = ctx.cmd_line:sub(1, ctx.cur_pos - #ex_lead)
end

if #arg > 0 then
table.insert(args, arg)
if not arg_lead then arg_lead = arg end

if arg == "--" and cmd_line:sub(#cmd_line, #cmd_line) ~= "-" then
divideridx = #args
local ret = vim.tbl_map(function(v)
if v:match("^" .. vim.pesc(ctx.arg_lead)) then
return cmd_lead .. ex_lead .. v:sub(#ctx.arg_lead + 1)
elseif input_cmp then
return cmd_lead .. v
end
end

if not argidx then
argidx = #args
if cmd_line:sub(#cmd_line, #cmd_line):match("%s") then
argidx = argidx + 1
end
end
return (ctx.lead_quote or "") .. v
end, candidates)

return {
args = args,
arg_lead = arg_lead or "",
argidx = argidx,
divideridx = divideridx,
between = between,
}
return M.filter_candidates(cmd_lead .. ex_lead, ret)
end

function M.ambiguous_bool(value, default, truthy, falsy)
Expand Down
30 changes: 11 additions & 19 deletions lua/diffview/init.lua
Original file line number Diff line number Diff line change
Expand Up @@ -105,16 +105,18 @@ function M.init()
]])
end

function M.open(...)
local view = lib.diffview_open(utils.tbl_pack(...))
---@param args string[]
function M.open(args)
local view = lib.diffview_open(args)
if view then
view:open()
end
end

---@param range? { [1]: integer, [2]: integer }
function M.file_history(range, ...)
local view = lib.file_history(range, utils.tbl_pack(...))
---@param args string[]
function M.file_history(range, args)
local view = lib.file_history(range, args)
if view then
view:open()
end
Expand All @@ -135,22 +137,12 @@ function M.close(tabpage)
end
end

---@param arg_lead string
---@param items string[]
---@return string[]
function M.filter_completion(arg_lead, items)
arg_lead, _ = vim.pesc(arg_lead)
return vim.tbl_filter(function(item)
return item:match(arg_lead)
end, items)
end

function M.completion(_, cmd_line, cur_pos)
local ctx = arg_parser.scan_ex_args(cmd_line, cur_pos)
local ctx = arg_parser.scan(cmd_line, { cur_pos = cur_pos, allow_ex_range = true })
local cmd = ctx.args[1]

if cmd and M.completers[cmd] then
return M.filter_completion(ctx.arg_lead, M.completers[cmd](ctx))
return arg_parser.process_candidates(M.completers[cmd](ctx), ctx)
end
end

Expand Down Expand Up @@ -191,14 +183,14 @@ M.completers = {

if ctx.argidx > ctx.divideridx then
if adapter then
utils.vec_push(candidates, unpack(adapter:path_completion(ctx.arg_lead)))
utils.vec_push(candidates, unpack(adapter:path_candidates(ctx.arg_lead)))
else
utils.vec_push(candidates, unpack(vim.fn.getcompletion(ctx.arg_lead, "file", 0)))
end
elseif adapter then
if not has_rev_arg and ctx.arg_lead:sub(1, 1) ~= "-" then
utils.vec_push(candidates, unpack(adapter.comp.open:get_all_names()))
utils.vec_push(candidates, unpack(adapter:rev_completion(ctx.arg_lead, {
utils.vec_push(candidates, unpack(adapter:rev_candidates(ctx.arg_lead, {
accept_range = true,
})))
else
Expand All @@ -221,7 +213,7 @@ M.completers = {
adapter.comp.file_history:get_completion(ctx.arg_lead)
or adapter.comp.file_history:get_all_names()
))
utils.vec_push(candidates, unpack(adapter:path_completion(ctx.arg_lead)))
utils.vec_push(candidates, unpack(adapter:path_candidates(ctx.arg_lead)))
else
utils.vec_push(candidates, unpack(vim.fn.getcompletion(ctx.arg_lead, "file", 0)))
end
Expand Down
6 changes: 3 additions & 3 deletions lua/diffview/scene/views/file_history/listeners.lua
Original file line number Diff line number Diff line change
Expand Up @@ -111,9 +111,9 @@ return function(view)
end
end
elseif view.panel.option_panel:is_focused() then
local item = view.panel.option_panel:get_item_at_cursor()
if item then
view.panel.option_panel.emitter:emit("set_option", item[1])
local option = view.panel.option_panel:get_item_at_cursor()
if option then
view.panel.option_panel.emitter:emit("set_option", option.key)
end
end
end,
Expand Down
Loading