Skip to content

Commit 5bbcf16

Browse files
authored
feat: Support quoted arguments. (#274)
Everything between a pair of quotes will now be treated as part of a single argument. Example: :DiffviewFileHistory --grep="foo bar baz" -G"lorem ipsum dolor" :DiffviewOpen 'HEAD@{4 days ago}' * feat(arg-parser): Make the scanner more configurable. The scanner can now configurably, and correctly parse quoted args as well as EX command ranges. Provide more data in `CmdLineContext`s. * feat(arg-parser): Better processing of completion candidates. Do all completion candidate filtering and processing in the arg-parser. Properly support completion for |input()|. * refactor(file-history): Flag options into their own class. Better default behavior. Requires far less customization per flag option in order to get sensible behavior.
1 parent 168c8fc commit 5bbcf16

File tree

10 files changed

+379
-362
lines changed

10 files changed

+379
-362
lines changed

lua/diffview/arg_parser.lua

Lines changed: 97 additions & 91 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ local utils = lazy.require("diffview.utils") ---@module "diffview.utils"
55

66
local M = {}
77

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

1111
---@class ArgObject : diffview.Object
@@ -231,55 +231,92 @@ function M.split_ex_range(arg)
231231
end
232232

233233
---@class CmdLineContext
234-
---@field args string[] The complete list of arguments.
235-
---@field arg_lead string
236-
---@field argidx integer Index of the current argument.
237-
---@field divideridx integer
238-
---@field range string? Ex command range.
239-
---@field between boolean The current position is between two arguments.
240-
241-
---Scan an EX command string and split it into individual args.
234+
---@field cmd_line string
235+
---@field args string[] # The tokenized list of arguments.
236+
---@field raw_args string[] # The unprocessed list of arguments. Contains syntax characters, such as quotes.
237+
---@field arg_lead string # The leading part of the current argument.
238+
---@field lead_quote string? # If present: the quote character used for the current argument.
239+
---@field cur_pos integer # The cursor position in the command line.
240+
---@field argidx integer # Index of the current argument.
241+
---@field divideridx integer # The index of the end-of-options token. (default: math.huge)
242+
---@field range string? # Ex command range.
243+
---@field between boolean # The current position is between two arguments.
244+
245+
---@class arg_parser.scan.Opt
246+
---@field cur_pos integer # The current cursor position in the command line.
247+
---@field allow_quoted boolean # Everything between a pair of quotes should be treated as part of a single argument. (default: true)
248+
---@field allow_ex_range boolean # The command line may contain an EX command range. (default: false)
249+
250+
---Tokenize a command line string.
242251
---@param cmd_line string
243-
---@param cur_pos number
252+
---@param opt? arg_parser.scan.Opt
244253
---@return CmdLineContext
245-
function M.scan_ex_args(cmd_line, cur_pos)
254+
function M.scan(cmd_line, opt)
255+
opt = vim.tbl_extend("keep", opt or {}, {
256+
cur_pos = #cmd_line + 1,
257+
allow_quoted = true,
258+
allow_ex_range = false,
259+
}) --[[@as arg_parser.scan.Opt ]]
260+
246261
local args = {}
262+
local raw_args = {}
247263
local arg_lead
248264
local divideridx = math.huge
249265
local argidx
250266
local between = false
251-
local arg = ""
267+
local cur_quote, lead_quote
268+
local arg, raw_arg = "", ""
252269

253270
local h, i = -1, 1
254271

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

258-
if not argidx and i > cur_pos then
275+
if not argidx and i > opt.cur_pos then
259276
argidx = #args + 1
260277
arg_lead = arg
261-
if h < cur_pos then between = true end
278+
lead_quote = cur_quote
279+
if h < opt.cur_pos then between = true end
262280
end
263281

264282
if char == "\\" then
265283
arg = arg .. char
284+
raw_arg = raw_arg .. char
266285
if i < #cmd_line then
267286
i = i + 1
268287
arg = arg .. cmd_line:sub(i, i)
288+
raw_arg = raw_arg .. cmd_line:sub(i, i)
269289
end
270290
h = i
291+
elseif cur_quote then
292+
if char == cur_quote then
293+
cur_quote = nil
294+
else
295+
arg = arg .. char
296+
end
297+
raw_arg = raw_arg .. char
298+
h = i
299+
elseif opt.allow_quoted and (char == [[']] or char == [["]]) then
300+
cur_quote = char
301+
raw_arg = raw_arg .. char
302+
h = i
271303
elseif char:match("%s") then
272304
if arg ~= "" then
273305
table.insert(args, arg)
274306
if arg == "--" and i - 1 < #cmd_line then
275307
divideridx = #args
276308
end
277309
end
310+
if raw_arg ~= "" then
311+
table.insert(raw_args, raw_arg)
312+
end
278313
arg = ""
314+
raw_arg = ""
279315
-- Skip whitespace
280316
i = i + cmd_line:sub(i, -1):match("^%s+()") - 2
281317
else
282318
arg = arg .. char
319+
raw_arg = raw_arg .. char
283320
h = i
284321
end
285322

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

289326
if #arg > 0 then
290327
table.insert(args, arg)
291-
if not arg_lead then arg_lead = arg end
328+
table.insert(raw_args, raw_arg)
329+
if not arg_lead then
330+
arg_lead = arg
331+
lead_quote = cur_quote
332+
end
292333

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

307348
if #args > 0 then
308-
range, args[1] = M.split_ex_range(args[1])
349+
if opt.allow_ex_range then
350+
range, args[1] = M.split_ex_range(args[1])
351+
_, raw_args[1] = M.split_ex_range(raw_args[1])
352+
end
353+
309354
if args[1] == "" then
310355
table.remove(args, 1)
356+
table.remove(raw_args, 1)
311357
argidx = math.max(argidx - 1, 1)
312358
divideridx = math.max(divideridx - 1, 1)
313359
end
314360
end
315361

316362
return {
363+
cmd_line = cmd_line,
317364
args = args,
365+
raw_args = raw_args,
318366
arg_lead = arg_lead or "",
367+
lead_quote = lead_quote,
368+
cur_pos = opt.cur_pos,
319369
argidx = argidx,
320370
divideridx = divideridx,
321371
range = range ~= "" and range or nil,
322372
between = between,
323373
}
324374
end
325375

326-
---Scan a shell-like string and split it into individual args. This scanner
327-
---understands quoted args.
328-
---@param cmd_line string
329-
---@param cur_pos number
330-
---@return CmdLineContext
331-
function M.scan_sh_args(cmd_line, cur_pos)
332-
local args = {}
333-
local arg_lead
334-
local divideridx = math.huge
335-
local argidx
336-
local between = false
337-
local cur_quote
338-
local arg = ""
339-
340-
local h, i = -1, 1
376+
---Filter completion candidates.
377+
---@param arg_lead string
378+
---@param candidates string[]
379+
---@return string[]
380+
function M.filter_candidates(arg_lead, candidates)
381+
arg_lead, _ = vim.pesc(arg_lead)
341382

342-
while i <= #cmd_line do
343-
local char = cmd_line:sub(i, i)
383+
return vim.tbl_filter(function(item)
384+
return item:match(arg_lead)
385+
end, candidates)
386+
end
344387

345-
if not argidx and i > cur_pos then
346-
argidx = #args + 1
347-
arg_lead = arg
348-
if h < cur_pos then between = true end
349-
end
388+
---Process completion candidates.
389+
---@param candidates string[]
390+
---@param ctx CmdLineContext
391+
---@param input_cmp boolean? Completion for |input()|.
392+
---@return string[]
393+
function M.process_candidates(candidates, ctx, input_cmp)
394+
if not candidates then return {} end
350395

351-
if char == "\\" then
352-
if i < #cmd_line then
353-
i = i + 1
354-
arg = arg .. cmd_line:sub(i, i)
355-
end
356-
h = i
357-
elseif cur_quote then
358-
if char == cur_quote then
359-
cur_quote = nil
360-
else
361-
arg = arg .. char
362-
end
363-
h = i
364-
elseif char == [[']] or char == [["]] then
365-
cur_quote = char
366-
h = i
367-
elseif char:match("%s") then
368-
if arg ~= "" then
369-
table.insert(args, arg)
370-
if arg == "--" and i - 1 < #cmd_line then
371-
divideridx = #args
372-
end
373-
end
374-
arg = ""
375-
-- Skip whitespace
376-
i = i + cmd_line:sub(i, -1):match("^%s+()") - 2
377-
else
378-
arg = arg .. char
379-
h = i
380-
end
396+
local cmd_lead = ""
397+
local ex_lead = (ctx.lead_quote or "") .. ctx.arg_lead
381398

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

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

389-
if #arg > 0 then
390-
table.insert(args, arg)
391-
if not arg_lead then arg_lead = arg end
392-
393-
if arg == "--" and cmd_line:sub(#cmd_line, #cmd_line) ~= "-" then
394-
divideridx = #args
407+
local ret = vim.tbl_map(function(v)
408+
if v:match("^" .. vim.pesc(ctx.arg_lead)) then
409+
return cmd_lead .. ex_lead .. v:sub(#ctx.arg_lead + 1)
410+
elseif input_cmp then
411+
return cmd_lead .. v
395412
end
396-
end
397413

398-
if not argidx then
399-
argidx = #args
400-
if cmd_line:sub(#cmd_line, #cmd_line):match("%s") then
401-
argidx = argidx + 1
402-
end
403-
end
414+
return (ctx.lead_quote or "") .. v
415+
end, candidates)
404416

405-
return {
406-
args = args,
407-
arg_lead = arg_lead or "",
408-
argidx = argidx,
409-
divideridx = divideridx,
410-
between = between,
411-
}
417+
return M.filter_candidates(cmd_lead .. ex_lead, ret)
412418
end
413419

414420
function M.ambiguous_bool(value, default, truthy, falsy)

lua/diffview/init.lua

Lines changed: 11 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -105,16 +105,18 @@ function M.init()
105105
]])
106106
end
107107

108-
function M.open(...)
109-
local view = lib.diffview_open(utils.tbl_pack(...))
108+
---@param args string[]
109+
function M.open(args)
110+
local view = lib.diffview_open(args)
110111
if view then
111112
view:open()
112113
end
113114
end
114115

115116
---@param range? { [1]: integer, [2]: integer }
116-
function M.file_history(range, ...)
117-
local view = lib.file_history(range, utils.tbl_pack(...))
117+
---@param args string[]
118+
function M.file_history(range, args)
119+
local view = lib.file_history(range, args)
118120
if view then
119121
view:open()
120122
end
@@ -135,22 +137,12 @@ function M.close(tabpage)
135137
end
136138
end
137139

138-
---@param arg_lead string
139-
---@param items string[]
140-
---@return string[]
141-
function M.filter_completion(arg_lead, items)
142-
arg_lead, _ = vim.pesc(arg_lead)
143-
return vim.tbl_filter(function(item)
144-
return item:match(arg_lead)
145-
end, items)
146-
end
147-
148140
function M.completion(_, cmd_line, cur_pos)
149-
local ctx = arg_parser.scan_ex_args(cmd_line, cur_pos)
141+
local ctx = arg_parser.scan(cmd_line, { cur_pos = cur_pos, allow_ex_range = true })
150142
local cmd = ctx.args[1]
151143

152144
if cmd and M.completers[cmd] then
153-
return M.filter_completion(ctx.arg_lead, M.completers[cmd](ctx))
145+
return arg_parser.process_candidates(M.completers[cmd](ctx), ctx)
154146
end
155147
end
156148

@@ -191,14 +183,14 @@ M.completers = {
191183

192184
if ctx.argidx > ctx.divideridx then
193185
if adapter then
194-
utils.vec_push(candidates, unpack(adapter:path_completion(ctx.arg_lead)))
186+
utils.vec_push(candidates, unpack(adapter:path_candidates(ctx.arg_lead)))
195187
else
196188
utils.vec_push(candidates, unpack(vim.fn.getcompletion(ctx.arg_lead, "file", 0)))
197189
end
198190
elseif adapter then
199191
if not has_rev_arg and ctx.arg_lead:sub(1, 1) ~= "-" then
200192
utils.vec_push(candidates, unpack(adapter.comp.open:get_all_names()))
201-
utils.vec_push(candidates, unpack(adapter:rev_completion(ctx.arg_lead, {
193+
utils.vec_push(candidates, unpack(adapter:rev_candidates(ctx.arg_lead, {
202194
accept_range = true,
203195
})))
204196
else
@@ -221,7 +213,7 @@ M.completers = {
221213
adapter.comp.file_history:get_completion(ctx.arg_lead)
222214
or adapter.comp.file_history:get_all_names()
223215
))
224-
utils.vec_push(candidates, unpack(adapter:path_completion(ctx.arg_lead)))
216+
utils.vec_push(candidates, unpack(adapter:path_candidates(ctx.arg_lead)))
225217
else
226218
utils.vec_push(candidates, unpack(vim.fn.getcompletion(ctx.arg_lead, "file", 0)))
227219
end

lua/diffview/scene/views/file_history/listeners.lua

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -111,9 +111,9 @@ return function(view)
111111
end
112112
end
113113
elseif view.panel.option_panel:is_focused() then
114-
local item = view.panel.option_panel:get_item_at_cursor()
115-
if item then
116-
view.panel.option_panel.emitter:emit("set_option", item[1])
114+
local option = view.panel.option_panel:get_item_at_cursor()
115+
if option then
116+
view.panel.option_panel.emitter:emit("set_option", option.key)
117117
end
118118
end
119119
end,

0 commit comments

Comments
 (0)