-
Notifications
You must be signed in to change notification settings - Fork 127
/
arguments.lua
3158 lines (2873 loc) · 119 KB
/
arguments.lua
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
-- Copyright (c) 2016 Martin Ridgers
-- License: http://opensource.org/licenses/MIT
------------------------------------------------------------------------------
-- NOTE: If you add any settings here update set.cpp to load (lua, lib, arguments).
-- luacheck: no max line length
--------------------------------------------------------------------------------
local _arglink = {}
_arglink.__index = _arglink
setmetatable(_arglink, { __call = function (x, ...) return x._new(...) end })
--------------------------------------------------------------------------------
function _arglink._new(key, matcher)
return setmetatable({
_key = key,
_matcher = matcher,
}, _arglink)
end
--------------------------------------------------------------------------------
local _argmatcher = {}
_argmatcher.__index = _argmatcher
setmetatable(_argmatcher, { __call = function (x, ...) return x._new(...) end })
--------------------------------------------------------------------------------
local _delayinit_generation = 0
local _clear_onuse_coroutine = {}
local _clear_delayinit_coroutine = {}
--------------------------------------------------------------------------------
clink.onbeginedit(function ()
_delayinit_generation = _delayinit_generation + 1
-- Clear dangling coroutine references in matchers. Otherwise if a
-- coroutine doesn't finish before a new edit line begins, there will be
-- references that can't be garbage collected until the next time the
-- matcher performs delayed initialization.
for m,_ in pairs(_clear_onuse_coroutine) do
m._onuse_coroutine = nil
end
for m,a in pairs(_clear_delayinit_coroutine) do
for i,_ in pairs(a) do
m._init_coroutine[i] = nil
end
end
_clear_onuse_coroutine = {}
_clear_delayinit_coroutine = {}
end)
--------------------------------------------------------------------------------
local _argreader = {}
_argreader.__index = _argreader
setmetatable(_argreader, { __call = function (x, ...) return x._new(...) end })
--------------------------------------------------------------------------------
function _argreader._new(root, line_state)
local reader = setmetatable({
_matcher = root,
_realmatcher = root,
_user_data = {},
_line_state = line_state,
_arg_index = 1,
_stack = {},
_cmd_wordbreak = clink.is_cmd_wordbreak(line_state),
}, _argreader)
return reader
end
--------------------------------------------------------------------------------
--[[
local enable_tracing = true
local debug_print = true
function _argreader:trace(...)
if self._tracing then
if debug_print then
os.debugprint(...)
else
print(...)
end
end
end
function _argreader:starttracing(word)
if enable_tracing then
self._tracing = true
self._dbgword = word
self:trace()
self:trace(word, "BEGIN", self._matcher, "stack", #self._stack, "arg_index", self._arg_index)
end
end
--]]
--------------------------------------------------------------------------------
local function do_delayed_init(list, matcher, arg_index)
-- Don't init while generating matches from history, as that could be
-- excessively expensive (could run thousands of callbacks).
if clink.co_state._argmatcher_fromhistory and clink.co_state._argmatcher_fromhistory.argmatcher then
return
end
-- Track flags initialization as position 0.
if matcher._flags and list == matcher._flags._args[1] then
arg_index = 0
end
-- New edit line starts a new generation number. Reset any delay init
-- callbacks that didn't finish.
if (matcher._init_generation or 0) < _delayinit_generation then
matcher._init_coroutine = nil
matcher._init_generation = _delayinit_generation
end
local _, ismain = coroutine.running()
local async_delayinit = not ismain or not clink._in_generate()
-- Start the delay init callback if it hasn't already started.
local c = matcher._init_coroutine and matcher._init_coroutine[arg_index]
if not c then
if not matcher._init_coroutine then
matcher._init_coroutine = {}
end
-- Run the delayinit callback in a coroutine so typing is responsive.
c = coroutine.create(function ()
-- Invoke the delayinit callback and add the results to the arg
-- slot's list of matches.
local addees = list.delayinit(matcher, arg_index)
matcher:_add(list, addees)
-- Mark the init callback as finished.
local mic = matcher._init_coroutine
if mic then -- Avoid error if argmatcher was reset in the meantime.
mic[arg_index] = nil
end
local cdc = _clear_delayinit_coroutine[matcher]
if cdc then -- It may have been cleared by a new edit session.
cdc[arg_index] = nil
end
list.delayinit = nil
-- If originally started from not-main, then reclassify.
if async_delayinit then
clink._signal_delayed_init()
clink.reclassifyline()
end
end)
matcher._init_coroutine[arg_index] = c
-- Make sure the delayinit coroutine runs to completion, even if a new
-- prompt generation begins (which would normally orphan coroutines).
clink.runcoroutineuntilcomplete(c)
-- Set up to be able to efficiently clear dangling coroutine references,
-- e.g. in case a coroutine doesn't finish before a new edit line.
if not _clear_delayinit_coroutine[matcher] then
_clear_delayinit_coroutine[matcher] = {}
end
_clear_delayinit_coroutine[matcher][arg_index] = c
end
-- Finish (run) the coroutine immediately only when the main coroutine is
-- generating matches.
if not async_delayinit then
clink._finish_coroutine(c)
else
-- Run the coroutine up to the first yield, so that if it doesn't need
-- to yield at all then it completes right now.
local ok, ret = coroutine.resume(c)
if not ok and ret and settings.get("lua.debug") then
print("")
print("coroutine failed:")
_co_error_handler(c, ret)
end
end
end
--------------------------------------------------------------------------------
local function do_onuse_callback(argmatcher, command_word)
-- Don't init while generating matches from history, as that could be
-- excessively expensive (could run thousands of callbacks).
if clink.co_state._argmatcher_fromhistory and clink.co_state._argmatcher_fromhistory.argmatcher then
return
end
if (argmatcher._onuse_generation or 0) < _delayinit_generation then
argmatcher._onuse_generation = _delayinit_generation
else
return
end
local _, ismain = coroutine.running()
local async_delayinit = not ismain or not clink._in_generate()
-- Start the delay init callback if it hasn't already started.
local c = argmatcher._onuse_coroutine
if clink.DEBUG and c then
print("\n\n\x1b[7m...already... "..tostring(c).."\x1b[m")
print("\n\n")
pause()
end
if not c then
-- Run the delayinit callback in a coroutine so typing is responsive.
c = coroutine.create(function ()
argmatcher._delayinit_func(argmatcher, command_word)
argmatcher._onuse_coroutine = nil
_clear_onuse_coroutine[argmatcher] = nil
if async_delayinit then
clink._signal_delayed_init()
clink.reclassifyline()
end
end)
argmatcher._onuse_coroutine = c
-- Make sure the delayinit coroutine runs to completion, even if a new
-- prompt generation begins (which would normally orphan coroutines).
clink.runcoroutineuntilcomplete(c)
-- Set up to be able to efficiently clear dangling coroutine references,
-- e.g. in case a coroutine doesn't finish before a new edit line.
_clear_onuse_coroutine[argmatcher] = argmatcher
end
-- Finish (run) the coroutine immediately only when the main coroutine is
-- generating matches.
if not async_delayinit then
clink._finish_coroutine(c)
else
-- Run the coroutine up to the first yield, so that if it doesn't need
-- to yield at all then it completes right now.
local ok, ret = coroutine.resume(c)
if not ok and ret and settings.get("lua.debug") then
print("")
print("coroutine failed:")
_co_error_handler(c, ret)
end
end
end
--------------------------------------------------------------------------------
local function is_word_present(word, arg, t, arg_match_type)
for _, i in ipairs(arg) do
local it = type(i)
if it == "function" then
t = 'o' --other (placeholder; superseded by :classifyword).
elseif i == word or (it == "table" and i.match == word) then
return arg_match_type, true
end
end
return t, false
end
--------------------------------------------------------------------------------
local function get_classify_color(code)
if code == "a" then
local color = settings.get("color.arg")
if color ~= "" then
return color
end
return settings.get("color.input")
end
local name
if code == "c" then name = "color.cmd"
elseif code == "d" then name = "color.doskey"
elseif code == "f" then name = "color.flag"
elseif code == "x" then name = "color.executable"
elseif code == "u" then name = "color.unrecognized"
elseif code == "o" then name = "color.input"
elseif code == "n" then name = "color.unexpected"
end
if name then
return settings.get(name)
end
return ""
end
--------------------------------------------------------------------------------
local function parse_chaincommand_modes(modes)
local mode = "cmd"
local expand_aliases
modes = modes or ""
for _, m in ipairs(string.explode(modes:lower(), ", ")) do
if m == "doskey" then
expand_aliases = true
elseif m == "cmd" or m == "start" or m == "run" then
mode = m
end
end
return mode, expand_aliases
end
--------------------------------------------------------------------------------
local function break_slash(line_state, word_index, word_classifications)
-- If word_index not specified, then find the command word. This loops to
-- find it instead of using getcommandwordindex() so that it can work even
-- when chain command parsing is re-parsing an existing line_state
-- starting from a later word index.
if not word_index then
for i = 1, line_state:getwordcount() do
local info = line_state:getwordinfo(i)
if not info.redir then
if info.quoted then
return
end
word_index = i
break
end
end
end
-- If the first character is not a forward slash but a forward slash is
-- present later in the word, then split the word into two.
local word = line_state:getword(word_index)
local slash = word:find("/")
if slash and slash > 1 then
-- In `xyz/whatever` the `xyz` can never be an alias because it isn't
-- whitespace delimited.
local ls = line_state:_break_word(word_index, slash - 1)
if ls then
if word_classifications then
word_classifications:_break_word(word_index, slash - 1)
end
return ls, word_classifications
end
end
end
--------------------------------------------------------------------------------
function _argreader:lookup_link(arg, arg_index, word, word_index, line_state)
if word and arg then
local link, forced
if arg._links then
local info = line_state:getwordinfo(word_index)
if info then -- word_index may be -1 when expanding a doskey alias.
local pos = info.offset + info.length
if line_state:getline():sub(pos, pos) == "=" then
link = arg._links[word.."="]
end
end
if not link then
link = arg._links[word]
end
end
if arg.onlink then
local override = arg.onlink(link, arg_index, word, word_index, line_state, self._user_data)
if override == false then
link = nil
forced = true
elseif getmetatable(override) == _argmatcher then
link = override
forced = true
end
end
return link, forced
end
end
--------------------------------------------------------------------------------
-- Advancing twice from the same context indicates a cycle. Disable the reader.
local __cycles = 0
function _argreader:_detect_arg_cycle()
local d = self._cycle_detection
if not d then
d = {
matcher=self._matcher,
realmatcher=self._realmatcher,
arg_index=self._arg_index,
stack_depth=#self._stack,
}
self._cycle_detection = d
elseif d.matcher == self._matcher and
d.realmatcher == self._realmatcher and
d.arg_index == self._arg_index and
d.stack_depth == #self._stack then
self._disabled = true
if clink.DEBUG then
__cycles = __cycles + 1
clink.print("\x1b[s\x1b[H\x1b[0;97;41m cycle detected ("..__cycles..") \x1b[m\x1b[K\x1b[u", NONL)
end
return true
end
end
--------------------------------------------------------------------------------
-- NOTE: line_state may not be self._line_state if it came from extra.
function _argreader:start_chained_command(line_state, word_index, mode, expand_aliases)
self._no_cmd = nil
self._chain_command = true
self._chain_command_expand_aliases = expand_aliases
mode = mode or "cmd"
for i = word_index, line_state:getwordcount() do
local info = line_state:getwordinfo(i)
if not info.redir then
if info.quoted then
return line_state
end
local alias = info.alias
if expand_aliases and not alias then
local word = line_state:getword(i)
if word ~= "" then
local got = os.getalias(word)
alias = got and got ~= ""
if (not alias) ~= (not info.alias) then
local ls = line_state:_set_alias(i, alias)
if ls then
self._line_state = ls
line_state = ls
end
end
end
end
if not alias and mode == "cmd" then
local ls, wc = break_slash(line_state, i, self._word_classifier)
if ls then
self._line_state = ls
if self._word_classifier then
self._word_classifier = wc
end
end
end
self._no_cmd = not (mode == "cmd" or mode == "start")
break
end
end
end
--------------------------------------------------------------------------------
-- When extra isn't nil, skip classifying the word. This only happens when
-- parsing extra words from expanding a doskey alias.
--
-- On return, the _argreader should be primed for generating matches for the
-- NEXT word in the line.
--
-- Returns TRUE when chaining due to chaincommand().
local default_flag_nowordbreakchars = "'`=+;,"
function _argreader:update(word, word_index, extra, last_onadvance) -- luacheck: no unused
self._chain_command = nil
self._chain_command_expand_aliases = nil
self._cycle_detection = nil
if self._disabled then
return
end
::retry::
local arg_match_type = "a" --arg
local line_state = extra and extra.line_state or self._line_state
--[[
self._dbgword = word
self:trace(word, "update")
--]]
-- When a flag ends with : or = but doesn't link to another matcher, and if
-- the next word is adjacent, then treat the next word as an argument to the
-- flag instead of advancing to the next argument position.
if last_onadvance then -- luacheck: ignore 542
-- Nothing to do here.
elseif self._phantomposition then
-- Skip past a phantom position.
self._phantomposition = nil
return
elseif not self._noflags and
self._matcher._flags and
self._matcher:_is_flag(word) and
word:find("..[:=]$") then
-- Check if the word does not link to another matcher.
local flagarg = self._matcher._flags._args[1]
if not self:lookup_link(flagarg, 0, word, word_index, line_state) then
-- Check if the next word is adjacent.
local thiswordinfo = line_state:getwordinfo(word_index)
local nextwordinfo = line_state:getwordinfo(word_index + 1)
if nextwordinfo then
local thisend = thiswordinfo.offset + thiswordinfo.length + (thiswordinfo.quoted and 1 or 0)
local nextbegin = nextwordinfo.offset - (nextwordinfo.quoted and 1 or 0)
if thisend >= nextbegin then
-- Treat the next word as though there were a linked matcher
-- that generates file matches.
self._phantomposition = true
end
end
end
end
-- Check for flags and switch matcher if the word is a flag.
local is_flag
local next_is_flag
local end_flags
local matcher = self._matcher
local realmatcher = self._realmatcher
local pushed_flags
if not self._noflags then
is_flag = matcher:_is_flag(word)
end
if is_flag then
if matcher._flags and not last_onadvance then
local arg = matcher._flags._args[1]
if arg then
if arg.delayinit then
do_delayed_init(arg, matcher, 0)
end
if arg.onarg and clink._in_generate() then
arg.onarg(0, word, word_index, line_state, self._user_data)
end
end
if word == matcher._endofflags then
self._noflags = true
end_flags = true
end
self:_push(matcher._flags, matcher)
arg_match_type = "f" --flag
pushed_flags = true
else
return
end
end
if not is_flag and realmatcher._flagsanywhere == false then
self._noflags = true
elseif not self._noflags then
next_is_flag = matcher:_is_flag(line_state:getword(word_index + 1))
end
-- Update matcher after possible _push.
matcher = self._matcher
realmatcher = self._realmatcher
local arg_index = self._arg_index
local arg = matcher._args[arg_index]
-- Determine next arg index.
local react, react_modes
if arg and not is_flag then
if arg.delayinit then
do_delayed_init(arg, realmatcher, arg_index)
end
if arg.onadvance then
react, react_modes = arg.onadvance(arg_index, word, word_index, line_state, self._user_data)
if react then
-- 1 = Ignore; advance to next arg_index.
-- 0 = Repeat; stay on the arg_index.
-- -1 = Chain; behave like :chaincommand().
if react ~= 1 and react ~= 0 and react ~= -1 then
react = nil
end
end
if react ~= -1 then
react_modes = nil
end
end
end
if last_onadvance then
-- When in last_onadvance mode, bail out unless chaining or advancing
-- is needed.
if react ~= 1 and react ~= -1 and not ((not arg) and matcher._chain_command) then
return
end
end
local next_arg_index = arg_index + ((react ~= 0) and 1 or 0)
-- Merge two adjacent words separated only by nowordbreakchars.
if arg and (arg.nowordbreakchars or is_flag) and not self._cmd_wordbreak then
-- Internal CMD commands and Batch scripts never use nowordbreakchars.
-- Flags in other commands default to certain punctuation marks as
-- nowordbreakchars. This more accurately reflects how the command
-- line will actually be parsed, especially for commas.
--
-- UNLESS the character is immediately preceded by ":", so that e.g.
-- "-Q:+x" can still be interpreted as two words, "-Q:" and "+x".
-- This exception is handled inside _unbreak_word() itself.
local nowordbreakchars = arg.nowordbreakchars or default_flag_nowordbreakchars
local adjusted, skip_word, len = line_state:_unbreak_word(word_index, nowordbreakchars)
if adjusted then
self._line_state = adjusted
line_state = adjusted
if self._word_classifier then
self._word_classifier:_unbreak_word(word_index, len, skip_word)
end
if skip_word then
if is_flag then
next_is_flag = matcher:_is_flag(line_state:getword(word_index + 1))
self:_pop(next_is_flag)
end
return
end
word = line_state:getword(word_index)
end
end
-- If the arg has looping characters defined and a looping character
-- separates this word from the next, then don't advance to the next
-- argument index.
if not react and arg and arg.loopchars and arg.loopchars ~= "" and word_index < line_state:getwordcount() then
local thiswordinfo = line_state:getwordinfo(word_index)
local nextwordinfo = line_state:getwordinfo(word_index + 1)
local s = thiswordinfo.offset + thiswordinfo.length + (thiswordinfo.quoted and 1 or 0)
local e = nextwordinfo.offset - 1 - (nextwordinfo.quoted and 1 or 0)
if s == e then
-- Two words are separated by a looping character, and the
-- looping char is a natural word break char (e.g. semicolon).
local line = line_state:getline()
if arg.loopchars:find(line:sub(s, e), 1, true) then
next_arg_index = arg_index
end
elseif s - 1 == e then
local line = line_state:getline()
if arg.loopchars:find(line:sub(e, e), 1, true) then
-- End word is immediately preceded by a looping character.
-- This is reached when getwordbreakinfo() splits a word due to
-- a looping char that is not a natural word break char.
next_arg_index = arg_index
end
end
end
-- If arg_index is out of bounds we should loop if set or return to the
-- previous matcher if possible.
if next_arg_index > #matcher._args then
if matcher._loop then
self._arg_index = math.min(math.max(matcher._loop, 1), #matcher._args)
else
-- If next word is a flag, don't pop. Flags are not positional, so
-- a matcher can only be exhausted by a word that exceeds the number
-- of argument slots the matcher has.
if is_flag then
self._arg_index = next_arg_index
elseif not pushed_flags and next_is_flag then
self._arg_index = next_arg_index
elseif not self:_pop(next_is_flag) then
-- Popping must use the _arg_index as is, without incrementing
-- (it was already incremented before it got pushed).
self._arg_index = next_arg_index
end
end
else
if end_flags then
self:_pop(next_is_flag)
end
self._arg_index = next_arg_index
end
if react == -1 then
-- Chain due to request by `onadvance` callback.
local mode, expand_aliases = parse_chaincommand_modes(react_modes)
self:start_chained_command(line_state, word_index, mode, expand_aliases)
return true -- chaincommand.
end
-- Some matchers have no args at all. Or ran out of args.
if not arg then
if matcher._chain_command then
-- Chain due to :chaincommand() used in argmatcher.
self:start_chained_command(line_state, word_index, matcher._chain_command_mode, matcher._chain_command_expand_aliases)
return true -- chaincommand.
end
if self._word_classifier and not extra then
if matcher._no_file_generation then
self._word_classifier:classifyword(word_index, "n", false) --none
else
self._word_classifier:classifyword(word_index, "o", false) --other
end
end
return
end
-- If onadvance chose to ignore the arg index, then retry.
if react == 1 then
if self:_detect_arg_cycle() then
return
end
goto retry
elseif last_onadvance then
return
end
-- Run delayinit and onarg (is_flag runs them further above).
if not is_flag then
if arg.delayinit then
do_delayed_init(arg, realmatcher, arg_index)
end
if arg.onarg and clink._in_generate() then
arg.onarg(arg_index, word, word_index, line_state, self._user_data)
end
end
-- Generate matches from history.
if self._fromhistory_matcher then
if self._fromhistory_matcher == matcher and self._fromhistory_argindex == arg_index then
if clink.co_state._argmatcher_fromhistory.builder then
clink.co_state._argmatcher_fromhistory.builder:addmatch(word, "word")
end
end
end
-- Parse the word type.
if self._word_classifier and not extra then
local aidx = is_flag and 0 or arg_index
if realmatcher._classify_func and realmatcher._classify_func(aidx, word, word_index, line_state, self._word_classifier, self._user_data) then -- luacheck: ignore 542
-- The classifier function says it handled the word.
else
-- Use the argmatcher's data to classify the word.
local t = "o" --other
if arg._links and arg._links[word] then
t = arg_match_type
else
-- For performance reasons, don't run argmatcher functions
-- during classify. If that's needed, a script can provide a
-- :classify function to complement a :generate function.
local matched = false
if arg_match_type == "f" then
-- When the word is a flag and ends with : or = then check
-- if the word concatenated with an adjacent following word
-- matches a known flag. When so, classify both words.
if word:sub(-1):match("[:=]") then
if arg._links and arg._links[word] then
t = arg_match_type
else
local this_info = line_state:getwordinfo(word_index)
local next_info = line_state:getwordinfo(word_index + 1)
if this_info and next_info and this_info.offset + this_info.length == next_info.offset then
local combined_word = word..line_state:getword(word_index + 1)
for _, i in ipairs(arg) do
if type(i) ~= "function" and i == combined_word then
t = arg_match_type
self._word_classifier:classifyword(word_index + 1, t, false)
matched = true
break
end
end
end
end
elseif end_flags then
t = arg_match_type
end
end
if not matched then
local this_info = line_state:getwordinfo(word_index)
local pos = this_info.offset + this_info.length
local line = line_state:getline()
if line:sub(pos, pos) == "=" then
-- If "word" is immediately followed by an equal sign,
-- then check if "word=" is a recognized argument.
t, matched = is_word_present(word.."=", arg, t, arg_match_type)
if matched then
self._word_classifier:applycolor(pos, 1, get_classify_color(t))
end
end
if not matched then
if arg.loopchars and arg.loopchars ~= "" then
-- If the arg has looping characters defined, then
-- split the word and apply colors to the sub-words.
pos = this_info.offset
local split = string.explode(word, arg.loopchars, '"')
for _, w in ipairs(split) do
t, matched = is_word_present(w, arg, t, arg_match_type)
if matched then
local i = line:find(w, pos, true)
if i then
self._word_classifier:applycolor(i, #w, get_classify_color(t))
pos = i + #w
end
end
end
t = nil
else
t, matched = is_word_present(word, arg, t, arg_match_type) -- luacheck: no unused
end
end
end
end
if t then
self._word_classifier:classifyword(word_index, t, false)
end
end
end
-- Does the word lead to another matcher?
local linked, forced = self:lookup_link(arg, is_flag and 0 or arg_index, word, word_index, line_state)
if linked then
if not forced and is_flag and word:match("..[:=]$") then
local info = line_state:getwordinfo(word_index)
if info and
line_state:getcursor() ~= info.offset + info.length and
line_state:getline():sub(info.offset + info.length, info.offset + info.length) == " " then
-- Don't follow linked parser on `--foo=` flag if there's a
-- space after the `:` or `=` unless the cursor is on the space.
linked = nil
end
end
if linked then
if linked._delayinit_func then
do_onuse_callback(linked, nil)
end
self:_push(linked)
end
end
-- If it's a flag and doesn't have a linked matcher, then pop to restore the
-- matcher that should be active for the next word.
if not linked and is_flag then
self:_pop(next_is_flag)
end
end
--------------------------------------------------------------------------------
-- Consumes extra words from a doskey alias before parsing the real line_state.
function _argreader:consume_extra(extra)
-- line_state and count must be updated inside the loop, because
-- self:update() can swap to a different line_state.
::next_word::
local line_state = extra.line_state
local count = line_state:getwordcount()
local word_index = extra.next_index
if word_index > count then
extra.done = true
return
end
extra.next_index = word_index + 1
local info = line_state:getwordinfo(word_index)
if info.redir then
goto next_word
end
local word = line_state:getword(word_index)
if self:update(word, word_index, extra) then
line_state = extra.line_state -- self:update() can swap to a different line_state.
local lookup = line_state:getword(word_index);
extra.next_index = 2
line_state:_shift(word_index)
return true, lookup
end
goto next_word
end
--------------------------------------------------------------------------------
-- When matcher is a flags matcher, its outer matcher must be passed in (as
-- realmatcher) so that delayinit can be given the real matcher from the API's
-- perspective.
function _argreader:_push(matcher, realmatcher)
-- v0.4.9 effectively pushed flag matchers, but not arg matchers.
-- if not self._matcher._deprecated or self._matcher._is_flag_matcher or matcher._is_flag_matcher then
if not matcher._deprecated or matcher._is_flag_matcher then
table.insert(self._stack, { self._matcher, self._arg_index, self._realmatcher, self._noflags, self._user_data })
--[[
self:trace(self._dbgword, "push", matcher, "stack", #self._stack)
else
self:trace(self._dbgword, "set", matcher, "stack", #self._stack)
--if self._tracing then pause() end
--]]
end
self._matcher = matcher
self._arg_index = 1
self._realmatcher = realmatcher or matcher
self._noflags = nil
if not realmatcher then -- Don't start new user data when switching to flags matcher.
self._user_data = {}
end
end
--------------------------------------------------------------------------------
function _argreader:_pop(next_is_flag)
if #self._stack <= 0 then
return false
end
while #self._stack > 0 do
if self._matcher and self._matcher._no_file_generation then
-- :nofiles() dead-ends the parser.
return false
end
self._matcher, self._arg_index, self._realmatcher, self._noflags, self._user_data = table.unpack(table.remove(self._stack))
if self._matcher._loop then
-- Matcher is looping; stop popping so it can handle the argument.
break
end
if next_is_flag and self._matcher._flags then
-- Matcher has flags and next_is_flag; stop popping so it can
-- handle the flag.
break
end
if self._arg_index <= #self._matcher._args then
-- Matcher has arguments remaining; stop popping so it can handle
-- the argument.
break
end
if #self._matcher._args == 0 and self._matcher._flags then
-- A matcher with flags but no args is a special case that means
-- match one file argument.
-- REVIEW: Consider giving it a file argument to eliminate the
-- special case treatments?
if next_is_flag or self._arg_index == 1 then
-- Stop popping so the matcher can handle the flag or argument.
break
end
end
end
--[[
self:trace("", "pop =>", self._matcher, "stack", #self._stack, "arg_index", self._arg_index, "realmatcher", self._realmatcher, "noflags", self._noflags)
self._dbgword = ""
--]]
return true
end
--------------------------------------------------------------------------------
local function append_uniq_chars(chars, find, add)
chars = chars or ""
find = find or "[]"
for i = 1, #add do
local c = add:sub(i, i)
if not chars:find(c, 1, true) then
local byte = string.byte(c)
local pct = ""
if byte < 97 or byte > 122 then -- <'a' or >'z'
pct = "%"
end
-- Update the list.
chars = chars .. c
-- Update the find expression.
find = find:sub(1, #find - 1) .. pct .. c .. "]"
end
end
return chars, find
end
--------------------------------------------------------------------------------
local function apply_options_to_list(addee, list)
if addee.nosort then
list.nosort = true
end
if type(addee.delayinit) == "function" then
list.delayinit = addee.delayinit
end
if type(addee.onadvance) == "function" then
list.onadvance = addee.onadvance
end
if type(addee.onarg) == "function" then
list.onarg = addee.onarg
end
if type(addee.onlink) == "function" then
list.onlink = addee.onlink
end
if addee.fromhistory then
list.fromhistory = true
end
if type(addee.loopchars) == "string" then
-- Apply looping characters, but avoid duplicates.
list.loopchars, list.loopcharsfind = append_uniq_chars(list.loopchars, list.loopcharsfind, addee.loopchars)
end
if type(addee.nowordbreakchars) == "string" then
-- Apply non-wordbreak characters, but avoid duplicates.
list.nowordbreakchars = append_uniq_chars(list.nowordbreakchars, nil, addee.nowordbreakchars)
end
end
--------------------------------------------------------------------------------
local function apply_options_to_builder(reader, arg, builder)
-- Disable sorting, if requested. This goes first because it is
-- unconditional and should take effect immediately.
if arg.nosort then
builder:setnosort()
end
-- Delay initialize the argmatcher, if needed.
if arg.delayinit then
do_delayed_init(arg, reader._realmatcher, reader._arg_index)
end
-- Generate matches from history, if requested.
if arg.fromhistory then
local _, ismain = coroutine.running()
if ismain then
clink.co_state._argmatcher_fromhistory.argmatcher = reader._matcher
clink.co_state._argmatcher_fromhistory.argslot = reader._arg_index
clink.co_state._argmatcher_fromhistory.builder = builder
-- Let the C++ code iterate through the history and call back into
-- Lua to parse individual history lines.
clink._generate_from_history()
-- Clear references. Clear builder because it goes out of scope,
-- and clear other references to facilitate garbage collection.
clink.co_state._argmatcher_fromhistory = {}
else
-- Generating from history can take a long time, depending on the
-- size of the history. It isn't suitable to run in a suggestions
-- coroutine. However, the menu-complete family of completion
-- commands reuse available match results, which then sees no
-- matches. So, the match pipeline needs to be informed that the