tree-sitter-zsh 0.31.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/grammar.js ADDED
@@ -0,0 +1,1904 @@
1
+ /**
2
+ * @file Zsh grammar for tree-sitter
3
+ * @author Max Brunsfeld <maxbrunsfeld@gmail.com>
4
+ * @author Amaan Qureshi <amaanq12@gmail.com>
5
+ * @author George Harker <george@georgeharker.com>
6
+ * @license MIT
7
+ */
8
+
9
+ /// <reference types="tree-sitter-cli/dsl" />
10
+ // @ts-check
11
+
12
+ const SPECIAL_CHARACTERS = [
13
+ '\'', '"',
14
+ '<', '>',
15
+ '{', '}',
16
+ '[', ']',
17
+ '(', ')',
18
+ '`', '$',
19
+ '|', '&', ';',
20
+ '\\', //':',
21
+ '\\s'
22
+ //' ', '\t', '\n'
23
+ ];
24
+
25
+ const PREC = {
26
+ UPDATE: 0,
27
+ ASSIGN: 1,
28
+ TERNARY: 2,
29
+ LOGICAL_OR: 3,
30
+ LOGICAL_AND: 4,
31
+ BITWISE_OR: 5,
32
+ BITWISE_XOR: 6,
33
+ BITWISE_AND: 7,
34
+ EQUALITY: 8,
35
+ COMPARE: 9,
36
+ TEST: 10,
37
+ UNARY: 11,
38
+ SHIFT: 12,
39
+ ADD: 13,
40
+ MULTIPLY: 14,
41
+ EXPONENT: 15,
42
+ NEGATE: 16,
43
+ PREFIX: 17,
44
+ POSTFIX: 18,
45
+ };
46
+
47
+ module.exports = grammar({
48
+ name: 'zsh',
49
+
50
+ conflicts: $ => [
51
+ [$._expression, $.command_name],
52
+ [$.command, $.variable_assignments],
53
+ [$.redirected_statement, $.command],
54
+ [$.pipeline],
55
+ [$.repeat_statement, $.command],
56
+ [$.binary_expression],
57
+ [$._test_command_binary_expression, $.binary_expression],
58
+ ],
59
+
60
+ inline: $ => [
61
+ $._statement,
62
+ $._terminator,
63
+ $._literal,
64
+ $._terminated_statement,
65
+ $._primary_expression,
66
+ $._simple_variable_name,
67
+ $._special_variable_name,
68
+ $._c_word,
69
+ $._statement_not_subshell,
70
+ $._redirect,
71
+ //$.expansion_style
72
+ ],
73
+
74
+ externals: $ => [
75
+ $.heredoc_start,
76
+ $.simple_heredoc_body,
77
+ $._heredoc_body_beginning,
78
+ $.heredoc_content,
79
+ $.heredoc_end,
80
+ $.file_descriptor,
81
+ $._empty_value,
82
+ $._concat,
83
+ $.variable_name, // Variable name followed by an operator like '=' or '+='
84
+ $.simple_variable_name,
85
+ $.special_variable_name,
86
+ $.test_operator,
87
+ $.regex,
88
+ $._regex_no_slash,
89
+ $._regex_no_space,
90
+ $._expansion_word,
91
+ $.extglob_pattern,
92
+ $._raw_dollar,
93
+ $._bare_dollar,
94
+ $._peek_bare_dollar, // peek ahead for $ without consuming
95
+ $._brace_start,
96
+ $._brace_expr_start,
97
+ $._immediate_double_hash,
98
+ $._array_star_token,
99
+ $._array_at_token,
100
+ '}', // CLOSING_BRACE
101
+ ']', // CLOSING_BRACKET
102
+ ')', // CLOSING_PAREN
103
+ '))', // CLOSING_DOUBLE_PAREN
104
+ '<<', // HEREDOC_ARROW
105
+ '<<-', // HEREDOC_ARROW_DASH
106
+ $._hash_pattern,
107
+ $._double_hash_pattern,
108
+ $._enter_pattern,
109
+ $._pattern_start,
110
+ $._pattern_suffix_start,
111
+ /\n/, // NEWLINE
112
+ '(', // OPENING_PAREN
113
+ '((', // DOUBLE_OPENING_PAREN
114
+ '[', // OPENING_BRACKET
115
+ '[[', // TEST_COMMAND_START
116
+ ']]', // TEST_COMMAND_END
117
+ 'esac', // ESAC
118
+ $._zsh_extended_glob_flags,
119
+ '"',
120
+ '`',
121
+ $.__error_recovery,
122
+ ],
123
+
124
+ extras: $ => [
125
+ //$.comment,
126
+ /\s/,
127
+ /\\\r?\n/,
128
+ /\\( |\t|\v|\f)/,
129
+ ],
130
+
131
+ supertypes: $ => [
132
+ $._statement,
133
+ $._expression,
134
+ $._primary_expression,
135
+ ],
136
+
137
+ word: $ => $.word,
138
+
139
+ rules: {
140
+ program: $ => optional($._statements),
141
+
142
+ _statements: $ => prec(1, seq(
143
+ repeat($._terminator), // Allow leading terminators/newlines
144
+ repeat(seq(
145
+ $._statement,
146
+ optional($.comment),
147
+ $._terminator,
148
+ )),
149
+ $._statement,
150
+ optional($.comment),
151
+ optional($._terminator),
152
+ )),
153
+
154
+ // Zsh glob qualifiers: (.) (/) (*) etc.
155
+ // FIXME: these qualifiers need extending to deal with
156
+ //
157
+ zsh_glob_qualifier: $ => token.immediate(seq(
158
+ '(', optional('#q'),
159
+ /[./*@=p%\-^rwxugoaLkamcFNDMsShHbBfFdcaAtImCYoOnPqUGzZ+\d]+/,
160
+ ')',
161
+ )),
162
+
163
+ // Zsh glob modifiers: (:t) (:h) (:r) (:e) etc.
164
+ zsh_glob_modifier: $ => token.immediate(seq(
165
+ '(',
166
+ ':',
167
+ /[threuoOnPAlUsSqQxXfFlW]/,
168
+ optional(/[^)]*/),
169
+ ')',
170
+ )),
171
+
172
+ _terminated_statement: $ => repeat1(seq(
173
+ $._statement,
174
+ $._terminator,
175
+ )),
176
+
177
+ // Statements
178
+
179
+ _statement: $ => choice(
180
+ $._statement_not_subshell,
181
+ $.subshell,
182
+ $.comment,
183
+ ),
184
+
185
+ _statement_not_subshell: $ => choice(
186
+ $.redirected_statement,
187
+ $.variable_assignment,
188
+ $.variable_assignments,
189
+ $.command,
190
+ $.declaration_command,
191
+ $.unset_command,
192
+ $.test_command,
193
+ $.negated_command,
194
+ $.for_statement,
195
+ $.terse_for_statement,
196
+ $.c_style_for_statement,
197
+ $.while_statement,
198
+ $.repeat_statement,
199
+ $.select_statement,
200
+ $.if_statement,
201
+ $.case_statement,
202
+ $.pipeline,
203
+ $.coprocess_statement,
204
+ $.list,
205
+ $.compound_statement,
206
+ $.function_definition,
207
+ ),
208
+
209
+ _statement_not_pipeline: $ => prec(1, choice(
210
+ $.redirected_statement,
211
+ $.variable_assignment,
212
+ $.variable_assignments,
213
+ $.command,
214
+ $.declaration_command,
215
+ $.unset_command,
216
+ $.test_command,
217
+ $.negated_command,
218
+ $.for_statement,
219
+ $.terse_for_statement,
220
+ $.c_style_for_statement,
221
+ $.while_statement,
222
+ $.repeat_statement,
223
+ $.select_statement,
224
+ $.if_statement,
225
+ $.case_statement,
226
+ $.list,
227
+ $.compound_statement,
228
+ $.function_definition,
229
+ $.subshell,
230
+ )),
231
+
232
+ redirected_statement: $ => prec.dynamic(-1, prec.right(-1, choice(
233
+ seq(
234
+ field('body', $._statement),
235
+ field('redirect', choice(
236
+ repeat1(choice(
237
+ $.file_redirect,
238
+ $.heredoc_redirect,
239
+ )),
240
+ )),
241
+ ),
242
+ seq(
243
+ field('body', choice($.if_statement, $.while_statement)),
244
+ $.herestring_redirect,
245
+ ),
246
+ field('redirect', repeat1($._redirect)),
247
+ $.herestring_redirect,
248
+ ))),
249
+
250
+ for_statement: $ => seq(
251
+ 'for',
252
+ field('variable', $._simple_variable_name),
253
+ optional(seq(
254
+ 'in',
255
+ field('value', repeat1($._literal)),
256
+ )),
257
+ $._terminator,
258
+ field('body', $.do_group),
259
+ ),
260
+
261
+ terse_for_statement: $ => seq(
262
+ 'for',
263
+ field('variable', $._simple_variable_name),
264
+ '(',
265
+ $._literal,
266
+ ')',
267
+ $._terminator,
268
+ field('body', $.do_group),
269
+ ),
270
+
271
+ c_style_for_statement: $ => seq(
272
+ 'for',
273
+ '((',
274
+ choice($._for_body),
275
+ '))',
276
+ optional(';'),
277
+ field('body', choice(
278
+ $.do_group,
279
+ $.compound_statement,
280
+ )),
281
+ ),
282
+ _for_body: $ => seq(
283
+ field('initializer', commaSep($._c_expression)),
284
+ $._c_terminator,
285
+ field('condition', commaSep($._c_expression)),
286
+ $._c_terminator,
287
+ field('update', commaSep($._c_expression)),
288
+ ),
289
+
290
+ _c_expression: $ => choice(
291
+ $._c_expression_not_assignment,
292
+ alias($._c_variable_assignment, $.variable_assignment),
293
+ ),
294
+ _c_expression_not_assignment: $ => choice(
295
+ $._c_word,
296
+ $.variable_ref,
297
+ $.expansion,
298
+ $.number,
299
+ $.string,
300
+ alias($._c_unary_expression, $.unary_expression),
301
+ alias($._c_binary_expression, $.binary_expression),
302
+ alias($._c_postfix_expression, $.postfix_expression),
303
+ alias($._c_parenthesized_expression, $.parenthesized_expression),
304
+ $.command_substitution,
305
+ ),
306
+
307
+ _c_variable_assignment: $ => seq(
308
+ field('name', alias($._c_word, $.variable_name)),
309
+ '=',
310
+ field('value', $._c_expression),
311
+ ),
312
+ _c_unary_expression: $ => prec(PREC.PREFIX, seq(
313
+ field('operator', choice('++', '--')),
314
+ $._c_expression_not_assignment,
315
+ )),
316
+ _c_binary_expression: $ => {
317
+ const table = [
318
+ [choice('+=', '-=', '*=', '/=', '%=', '**=', '<<=', '>>=', '&=', '^=', '|='), PREC.UPDATE],
319
+ [choice('||', '-o'), PREC.LOGICAL_OR],
320
+ [choice('&&', '-a'), PREC.LOGICAL_AND],
321
+ ['|', PREC.BITWISE_OR],
322
+ ['^', PREC.BITWISE_XOR],
323
+ ['&', PREC.BITWISE_AND],
324
+ [choice('==', '!='), PREC.EQUALITY],
325
+ [choice('<', '>', '<=', '>='), PREC.COMPARE],
326
+ [choice('<<', '>>'), PREC.SHIFT],
327
+ [choice('+', '-'), PREC.ADD],
328
+ [choice('*', '/', '%'), PREC.MULTIPLY],
329
+ ['**', PREC.EXPONENT],
330
+ ];
331
+
332
+ return choice(...table.map(([operator, precedence]) => {
333
+ // @ts-ignore
334
+ return prec[operator === '**' ? 'right' : 'left'](precedence, seq(
335
+ field('left', $._c_expression_not_assignment),
336
+ // @ts-ignore
337
+ field('operator', operator),
338
+ field('right', $._c_expression_not_assignment),
339
+ ));
340
+ }));
341
+ },
342
+ _c_postfix_expression: $ => prec(PREC.POSTFIX, seq(
343
+ $._c_expression_not_assignment,
344
+ field('operator', choice('++', '--')),
345
+ )),
346
+ _c_parenthesized_expression: $ => seq(
347
+ '(',
348
+ commaSep1($._c_expression),
349
+ ')',
350
+ ),
351
+ _c_word: $ => alias(/[a-zA-Z_][a-zA-Z0-9_]*/, $.word),
352
+
353
+ while_statement: $ => seq(
354
+ choice('while', 'until'),
355
+ field('condition', $._terminated_statement),
356
+ field('body', $.do_group),
357
+ ),
358
+
359
+ repeat_statement: $ => prec.right(seq(
360
+ 'repeat',
361
+ field('count', choice($.number, $.word, $._simple_variable_name, $.expansion)),
362
+ choice(
363
+ seq($._terminator, field('body', $.do_group)),
364
+ $._statement,
365
+ ),
366
+ )),
367
+
368
+ select_statement: $ => seq(
369
+ 'select',
370
+ field('variable', $.variable_name),
371
+ optional(seq(
372
+ 'in',
373
+ repeat1($._literal),
374
+ )),
375
+ $._terminator,
376
+ field('body', $.do_group),
377
+ ),
378
+
379
+ do_group: $ => seq(
380
+ 'do',
381
+ optional($._terminated_statement),
382
+ 'done',
383
+ ),
384
+
385
+ if_statement: $ => seq(
386
+ 'if',
387
+ field('condition', $._terminated_statement),
388
+ 'then',
389
+ optional($._terminated_statement),
390
+ repeat($.elif_clause),
391
+ optional($.else_clause),
392
+ 'fi',
393
+ ),
394
+
395
+ elif_clause: $ => seq(
396
+ 'elif',
397
+ $._terminated_statement,
398
+ 'then',
399
+ optional($._terminated_statement),
400
+ ),
401
+
402
+ else_clause: $ => seq(
403
+ 'else',
404
+ optional($._terminated_statement),
405
+ ),
406
+
407
+ case_statement: $ => seq(
408
+ 'case',
409
+ field('value', $._literal),
410
+ optional($._terminator),
411
+ 'in',
412
+ optional($._terminator),
413
+ optional(seq(
414
+ repeat($.case_item),
415
+ alias($.last_case_item, $.case_item),
416
+ )),
417
+ 'esac',
418
+ ),
419
+
420
+ case_item: $ => seq(
421
+ choice(
422
+ seq(
423
+ optional('('),
424
+ field('value', choice($._literal, $._extglob_blob)),
425
+ repeat(seq('|', field('value', choice($._literal, $._extglob_blob)))),
426
+ ')',
427
+ ),
428
+ ),
429
+ optional($._statements),
430
+ prec(1, choice(
431
+ field('termination', ';;'),
432
+ field('fallthrough', choice(';&', ';;&')),
433
+ )),
434
+ ),
435
+
436
+ last_case_item: $ => seq(
437
+ optional('('),
438
+ field('value', choice($._literal, $._extglob_blob)),
439
+ repeat(seq('|', field('value', choice($._literal, $._extglob_blob)))),
440
+ ')',
441
+ optional($._statements),
442
+ optional(prec(1, ';;')),
443
+ ),
444
+
445
+ function_definition: $ => prec(4, prec.right(seq(
446
+ choice(
447
+ seq(
448
+ 'function',
449
+ field('name', optional($.word)),
450
+ optional(seq(
451
+ '(', ')')),
452
+ ),
453
+ seq(
454
+ field('name', optional($.word)),
455
+ '(', ')',
456
+ ),
457
+ ),
458
+ field(
459
+ 'body',
460
+ choice(
461
+ $.compound_statement,
462
+ $.subshell,
463
+ $.test_command,
464
+ $.if_statement,
465
+ ),
466
+ ),
467
+ field('redirect', optional($._redirect)),
468
+ ))),
469
+
470
+ compound_statement: $ => prec.right(seq(
471
+ //'{',
472
+ alias($._brace_start, '{'),
473
+ optional($._terminated_statement),
474
+ token(prec(-1, '}')),
475
+ optional($.always_clause),
476
+ )),
477
+
478
+ // Always clause that can attach to compound statements
479
+ always_clause: $ => seq(
480
+ 'always',
481
+ field('always', choice(
482
+ $.compound_statement,
483
+ $.command,
484
+ $.pipeline
485
+ ))
486
+ ),
487
+
488
+ // Zsh coprocess: coproc [name] command
489
+ coprocess_statement: $ => prec(2, seq(
490
+ 'coproc',
491
+ optional(field('name', $.word)),
492
+ field('command', $.command)
493
+ )),
494
+
495
+ subshell: $ => seq(
496
+ '(',
497
+ $._statements,
498
+ ')',
499
+ ),
500
+
501
+ pipeline: $ => prec.right(seq(
502
+ $._statement_not_pipeline,
503
+ repeat1(seq(
504
+ choice('|', '|&'),
505
+ $._statement_not_pipeline,
506
+ )),
507
+ )),
508
+
509
+ list: $ => prec.left(-1, seq(
510
+ $._statement,
511
+ choice('&&', '||'),
512
+ $._statement,
513
+ )),
514
+
515
+ // Commands
516
+
517
+ negated_command: $ => seq(
518
+ '!',
519
+ choice(
520
+ prec(2, $.command),
521
+ prec(1, $.variable_assignment),
522
+ $.test_command,
523
+ $.subshell,
524
+ ),
525
+ ),
526
+
527
+ test_command: $ => prec(2, seq(
528
+ choice(
529
+ seq(
530
+ '[',
531
+ optional(choice($._expression, $.redirected_statement)),
532
+ ']'
533
+ ),
534
+ seq(
535
+ '[[',
536
+ choice(
537
+ alias($._test_command_binary_expression, $.binary_expression),
538
+ $._expression,
539
+ ),
540
+ ']]',
541
+ ),
542
+ seq(
543
+ '((',
544
+ optional($._expression),
545
+ '))'
546
+ ),
547
+ )),
548
+ ),
549
+
550
+ _test_command_binary_expression: $ => prec(PREC.COMPARE,
551
+ choice(
552
+ // Regex matching operator
553
+ prec(PREC.COMPARE, seq(
554
+ field('left', $._expression),
555
+ field('operator', '=~'),
556
+ //field('right', alias($._regex_no_space, $.regex)),
557
+ field('right', choice(
558
+ alias($._regex_no_space, $.regex),
559
+ $._expression
560
+ )),
561
+ )),
562
+ // Pattern/string matching operators
563
+ prec(PREC.COMPARE - 1, seq(
564
+ field('left', $._expression),
565
+ field('operator', choice('=', '==', '!=', $.test_operator)),
566
+ field('right', $._expression),
567
+ )),
568
+ ),
569
+ ),
570
+
571
+ declaration_command: $ => prec.left(seq(
572
+ choice('declare', 'typeset', 'export', 'readonly', 'local', 'integer', 'float'),
573
+ repeat(
574
+ choice(
575
+ $.variable_assignment,
576
+ field('argument', $._literal),
577
+ field('redirect', $._redirect),
578
+ )
579
+ ),
580
+ )),
581
+
582
+ unset_command: $ => prec.left(seq(
583
+ choice('unset', 'unsetenv'),
584
+ repeat(
585
+ choice(
586
+ $._literal,
587
+ $._simple_variable_name,
588
+ )
589
+ ),
590
+ )),
591
+
592
+ command: $ => prec.left(seq(
593
+ repeat(choice(
594
+ $.variable_assignment,
595
+ field('redirect', $._redirect),
596
+ )),
597
+ field('name', $.command_name),
598
+ choice(
599
+ repeat(
600
+ choice(
601
+ field('argument', $._literal),
602
+ field('argument', $._bare_dollar),
603
+ field('argument', $.glob_pattern),
604
+ field('argument', seq(
605
+ choice('=~', '=='),
606
+ choice($._literal, $.regex),
607
+ ),
608
+ field('redirect', $.herestring_redirect),
609
+ )
610
+ )),
611
+ $.subshell,
612
+ ),
613
+ )),
614
+
615
+ command_name: $ => $._literal,
616
+
617
+ variable_assignment: $ => seq(
618
+ field('name', choice(
619
+ $.variable_name,
620
+ $.subscript,
621
+ )),
622
+ choice(
623
+ '=',
624
+ '+=',
625
+ ),
626
+ field('value', choice(
627
+ $._literal,
628
+ $.array,
629
+ $._empty_value,
630
+ alias($._comment_word, $.word),
631
+ )),
632
+ ),
633
+
634
+ variable_assignments: $ => seq($.variable_assignment, repeat1($.variable_assignment)),
635
+
636
+ subscript: $ => prec.left(seq(
637
+ field('name', $.variable_name),
638
+ '[',
639
+ optional(field('flags', $.zsh_array_subscript_flags)),
640
+ field('index', choice($._param_arithmetic_expression, $.array_star, $.array_at, $.string)),
641
+ ']',
642
+ )),
643
+
644
+ // Arithmetic expressions for parameter subscripts (avoids subscript recursion)
645
+ _param_arithmetic_expression: $ => prec(1, choice(
646
+ $._param_arithmetic_literal,
647
+ alias($._param_arithmetic_unary_expression, $.unary_expression),
648
+ alias($._param_arithmetic_ternary_expression, $.ternary_expression),
649
+ alias($._param_arithmetic_binary_expression, $.binary_expression),
650
+ alias($._param_arithmetic_postfix_expression, $.postfix_expression),
651
+ alias($._param_arithmetic_parenthesized_expression, $.parenthesized_expression),
652
+ $.command_substitution,
653
+ )),
654
+
655
+ _param_arithmetic_literal: $ => prec(1, choice(
656
+ $.number,
657
+ // Note: no $.subscript to avoid recursion
658
+ $.variable_ref,
659
+ $.expansion,
660
+ $.variable_name,
661
+ $.string,
662
+ $.word, // Allow bare identifiers in parameter arithmetic context
663
+ )),
664
+
665
+ _param_arithmetic_binary_expression: $ => {
666
+ const table = [
667
+ [choice('+=', '-=', '*=', '/=', '%=', '**=', '<<=', '>>=', '&=', '^=', '|='), PREC.UPDATE],
668
+ [choice('=', '=~'), PREC.ASSIGN],
669
+ ['||', PREC.LOGICAL_OR],
670
+ ['&&', PREC.LOGICAL_AND],
671
+ ['|', PREC.BITWISE_OR],
672
+ ['^', PREC.BITWISE_XOR],
673
+ ['&', PREC.BITWISE_AND],
674
+ [choice('==', '!='), PREC.EQUALITY],
675
+ [choice('<', '>', '<=', '>='), PREC.COMPARE],
676
+ [choice('<<', '>>'), PREC.SHIFT],
677
+ [choice('+', '-'), PREC.ADD],
678
+ [choice('*', '/', '%'), PREC.MULTIPLY],
679
+ ['**', PREC.EXPONENT],
680
+ ];
681
+
682
+ return choice(...table.map(([operator, precedence]) => {
683
+ // @ts-ignore
684
+ return prec.left(precedence, seq(
685
+ field('left', $._param_arithmetic_expression),
686
+ // @ts-ignore
687
+ field('operator', operator),
688
+ field('right', $._param_arithmetic_expression),
689
+ ));
690
+ }));
691
+ },
692
+
693
+ _param_arithmetic_ternary_expression: $ => prec.left(PREC.TERNARY, seq(
694
+ field('condition', $._param_arithmetic_expression),
695
+ '?',
696
+ field('consequence', $._param_arithmetic_expression),
697
+ ':',
698
+ field('alternative', $._param_arithmetic_expression),
699
+ )),
700
+
701
+ _param_arithmetic_unary_expression: $ => choice(
702
+ prec(PREC.PREFIX, seq(
703
+ field('operator', tokenLiterals(1, '++', '--')),
704
+ $._param_arithmetic_expression,
705
+ )),
706
+ prec(PREC.UNARY, seq(
707
+ field('operator', tokenLiterals(1, '-', '+', '~')),
708
+ $._param_arithmetic_expression,
709
+ )),
710
+ prec.right(PREC.UNARY, seq(
711
+ field('operator', '!'),
712
+ $._param_arithmetic_expression,
713
+ )),
714
+ ),
715
+
716
+ _param_arithmetic_postfix_expression: $ => prec(PREC.POSTFIX, seq(
717
+ $._param_arithmetic_expression,
718
+ field('operator', choice('++', '--')),
719
+ )),
720
+
721
+ _param_arithmetic_parenthesized_expression: $ => seq(
722
+ '(',
723
+ $._param_arithmetic_expression,
724
+ ')',
725
+ ),
726
+ // Array expansion operators: [*] and [@]
727
+ array_star: $ => $._array_star_token,
728
+ array_at: $ => $._array_at_token,
729
+
730
+ // Zsh array subscript flags: (i) (I) (r) (R) etc.
731
+ // FIXME: delimeter is forced as :, could be any
732
+ zsh_array_subscript_flags: $ => seq(
733
+ '(',
734
+ choice(
735
+ /[wpfrRiIkKe]+/,
736
+ /s:[^:]+:/,
737
+ seq('n:', $._arithmetic_expression,':'),
738
+ seq('b:', $._arithmetic_expression,':'),
739
+ ),
740
+ ')',
741
+ ),
742
+
743
+ file_redirect: $ => prec.left(seq(
744
+ field('descriptor', optional($.file_descriptor)),
745
+ choice(
746
+ seq(
747
+ choice('<', '>', '>>', '&>', '&>>', '<&', '>&', '>|'),
748
+ field('destination', repeat1($._literal)),
749
+ ),
750
+ seq(
751
+ choice('<&-', '>&-'), // close file descriptor
752
+ optional(field('destination', $._literal)),
753
+ ),
754
+ ),
755
+ )),
756
+
757
+ heredoc_redirect: $ => seq(
758
+ field('descriptor', optional($.file_descriptor)),
759
+ choice('<<', '<<-'),
760
+ $.heredoc_start,
761
+ optional(choice(
762
+ alias($._heredoc_pipeline, $.pipeline),
763
+ seq(
764
+ field('redirect', repeat1($._redirect)),
765
+ optional($._heredoc_expression),
766
+ ),
767
+ $._heredoc_expression,
768
+ $._heredoc_command,
769
+ )),
770
+ /\n/,
771
+ choice($._heredoc_body, $._simple_heredoc_body),
772
+ ),
773
+
774
+ _heredoc_pipeline: $ => seq(
775
+ choice('|', '|&'),
776
+ $._statement,
777
+ ),
778
+
779
+ _heredoc_expression: $ => seq(
780
+ field('operator', choice('||', '&&')),
781
+ field('right', $._statement),
782
+ ),
783
+
784
+ _heredoc_command: $ => repeat1(field('argument', $._literal)),
785
+
786
+ _heredoc_body: $ => seq(
787
+ $.heredoc_body,
788
+ $.heredoc_end,
789
+ ),
790
+
791
+ heredoc_body: $ => seq(
792
+ $._heredoc_body_beginning,
793
+ repeat(choice(
794
+ $.expansion,
795
+ $.variable_ref,
796
+ $.command_substitution,
797
+ $.heredoc_content,
798
+ )),
799
+ ),
800
+
801
+ _simple_heredoc_body: $ => seq(
802
+ alias($.simple_heredoc_body, $.heredoc_body),
803
+ $.heredoc_end,
804
+ ),
805
+
806
+ herestring_redirect: $ => prec.left(seq(
807
+ field('descriptor', optional($.file_descriptor)),
808
+ '<<<',
809
+ $._literal,
810
+ )),
811
+
812
+ _redirect: $ => choice($.file_redirect, $.herestring_redirect),
813
+
814
+ // Expressions
815
+
816
+ _expression: $ => choice(
817
+ $._literal,
818
+ $.unary_expression,
819
+ $.ternary_expression,
820
+ $.binary_expression,
821
+ $.postfix_expression,
822
+ $.parenthesized_expression,
823
+ ),
824
+
825
+ // https://tldp.org/LDP/abs/html/opprecedence.html
826
+ binary_expression: $ => {
827
+ const table = [
828
+ [choice('+=', '-=', '*=', '/=', '%=', '**=', '<<=', '>>=', '&=', '^=', '|='), PREC.UPDATE],
829
+ [choice('=', '=~'), PREC.ASSIGN],
830
+ ['||', PREC.LOGICAL_OR],
831
+ ['&&', PREC.LOGICAL_AND],
832
+ ['|', PREC.BITWISE_OR],
833
+ ['^', PREC.BITWISE_XOR],
834
+ ['&', PREC.BITWISE_AND],
835
+ [choice('==', '!='), PREC.EQUALITY],
836
+ [choice('<', '>', '<=', '>='), PREC.COMPARE],
837
+ [$.test_operator, PREC.TEST],
838
+ [choice('<<', '>>'), PREC.SHIFT],
839
+ [choice('+', '-'), PREC.ADD],
840
+ [choice('*', '/', '%'), PREC.MULTIPLY],
841
+ ['**', PREC.EXPONENT],
842
+ ];
843
+
844
+ return choice(
845
+ choice(...table.map(([operator, precedence]) => {
846
+ // @ts-ignore
847
+ return prec[operator === '**' ? 'right' : 'left'](precedence, seq(
848
+ field('left', $._expression),
849
+ // @ts-ignore
850
+ field('operator', operator),
851
+ field('right', $._expression),
852
+ ));
853
+ })),
854
+ prec(PREC.ASSIGN, seq(
855
+ field('left', $._expression),
856
+ field('operator', '=~'),
857
+ field('right', choice(alias($._regex_no_space, $.regex), $._expression)),
858
+ )),
859
+ prec(PREC.EQUALITY, seq(
860
+ field('left', $._expression),
861
+ field('operator', choice('==', '!=')),
862
+ field('right', $._extglob_blob),
863
+ )),
864
+ );
865
+ },
866
+
867
+ ternary_expression: $ => prec.left(PREC.TERNARY, seq(
868
+ field('condition', $._expression),
869
+ '?',
870
+ field('consequence', $._expression),
871
+ ':',
872
+ field('alternative', $._expression),
873
+ )),
874
+
875
+ unary_expression: $ => choice(
876
+ prec(PREC.PREFIX, seq(
877
+ field('operator', tokenLiterals(1, '++', '--')),
878
+ $._expression,
879
+ )),
880
+ prec(PREC.UNARY, seq(
881
+ field('operator', tokenLiterals(1, '-', '+', '~')),
882
+ $._expression,
883
+ )),
884
+ prec.right(PREC.UNARY, seq(
885
+ field('operator', '!'),
886
+ $._expression,
887
+ )),
888
+ prec.right(PREC.TEST, seq(
889
+ field('operator', $.test_operator),
890
+ $._expression,
891
+ )),
892
+ ),
893
+
894
+ postfix_expression: $ => prec(PREC.POSTFIX, seq(
895
+ $._expression,
896
+ field('operator', choice('++', '--')),
897
+ )),
898
+
899
+ parenthesized_expression: $ => seq(
900
+ '(',
901
+ $._expression,
902
+ ')',
903
+ ),
904
+
905
+ // Literals
906
+
907
+ _literal: $ => choice(
908
+ $._primary_expression,
909
+ $.concatenation,
910
+ alias(prec(-2, repeat1($._special_character)), $.word),
911
+ ),
912
+
913
+ _primary_expression: $ => choice(
914
+ $.qualified_expression, // qualified globs should be first
915
+ $.glob_pattern,
916
+ $.word,
917
+ alias($.test_operator, $.word),
918
+ $.string,
919
+ $.raw_string,
920
+ $.translated_string,
921
+ $.ansi_c_string,
922
+ $.number,
923
+ $._expansion_or_variable,
924
+ $.command_substitution,
925
+ $.process_substitution,
926
+ $.arithmetic_expansion,
927
+ $.brace_expression,
928
+ ),
929
+
930
+ // Unified rule for all dollar-based patterns to eliminate competition
931
+ _expansion_or_variable: $ => choice(
932
+ $.expansion, // Try ${...} patterns first
933
+ $.variable_ref, // Fall back to $var patterns
934
+ ),
935
+
936
+ arithmetic_expansion: $ => prec(4, choice(
937
+ seq(
938
+ choice(
939
+ alias(seq($._bare_dollar, '(('), "$(("),
940
+ '(('
941
+ ),
942
+ commaSep1($._arithmetic_expression),
943
+ '))'
944
+ ),
945
+ seq(
946
+ alias(seq($._bare_dollar, '['), "$["),
947
+ $._arithmetic_expression,
948
+ ']'),
949
+ )
950
+ ),
951
+
952
+ brace_expression: $ => seq(
953
+ alias($._brace_expr_start, '{'),
954
+ alias(token.immediate(/\d+/), $.number),
955
+ token.immediate('..'),
956
+ alias(token.immediate(/\d+/), $.number),
957
+ token.immediate('}'),
958
+ ),
959
+
960
+ _glob_innards: $ => token(seq(
961
+ repeat(/[^\s'"*?\[{~(<=]/),
962
+ choice(
963
+ /\*\*/, // **
964
+ /\*/, // *
965
+ /\?/, // ?
966
+ /\[[!^]?[^\]]*\]/, // [...]
967
+ /<[0-9]+-[0-9]+>/, // <1-10>
968
+ /\{[^}]*,[^}]*\}/, // {a,b}
969
+ /~/, // ~
970
+ /\([^)|]+(\|[^)|]*)+\)/ // (a|b|c)
971
+ ),
972
+ repeat(choice(
973
+ /[^\s'"*?\[{~(<=]/, // regular chars
974
+ /\*\*/, // **
975
+ /\*/, // *
976
+ /\?/, // ?
977
+ /\[[!^]?[^\]]*\]/, // [...]
978
+ /<[0-9]+-[0-9]+>/, // <1-10>
979
+ /\{[^}]+,[^}]*\}/, // {a,b}
980
+ /\([^)|]+(\|[^)|]*)+\)/ // (a|b|c)
981
+ ))
982
+ )),
983
+
984
+ qualified_expression: $ => seq(
985
+ field('expression', choice(
986
+ $.expansion, // ${...} - can produce arrays
987
+ $.command_substitution, // $(...) - can produce multiple lines
988
+ $.variable_ref, // $array - if it's an array variable
989
+ // Could also include arithmetic that produces arrays
990
+ )),
991
+ field('qualifier', $.zsh_glob_qualifier), // (ND), (.), etc.
992
+ ),
993
+
994
+ // Unified glob pattern that handles **, *, ?, [] patterns with optional zsh qualifiers/modifiers
995
+ glob_pattern: $ => prec.dynamic(-1, choice(
996
+ // True glob patterns with wildcards
997
+ seq(
998
+ optional(field('flags', $.zsh_extended_glob_flags)),
999
+ field('pattern', $._glob_innards),
1000
+ optional(choice(
1001
+ field('qualifier', $.zsh_glob_qualifier),
1002
+ field('modifier', $.zsh_glob_modifier),
1003
+ seq(
1004
+ field('qualifier', $.zsh_glob_qualifier),
1005
+ field('modifier', $.zsh_glob_modifier),
1006
+ ),
1007
+ )),
1008
+ ),
1009
+ // Words with qualifiers/modifiers (like /path/file(:h))
1010
+ prec(1, seq(
1011
+ optional(field('flags', $.zsh_extended_glob_flags)),
1012
+ field('pattern', $.word),
1013
+ choice(
1014
+ field('qualifier', $.zsh_glob_qualifier),
1015
+ field('modifier', $.zsh_glob_modifier),
1016
+ seq(
1017
+ field('qualifier', $.zsh_glob_qualifier),
1018
+ field('modifier', $.zsh_glob_modifier),
1019
+ ),
1020
+ ),
1021
+ )),
1022
+ )),
1023
+
1024
+
1025
+
1026
+ // Zsh extended glob flags: (#i) (#q) (#a2) etc.
1027
+ zsh_extended_glob_flags: $ => $._zsh_extended_glob_flags,
1028
+
1029
+ _arithmetic_expression: $ => prec(1, choice(
1030
+ $._arithmetic_literal,
1031
+ alias($._arithmetic_unary_expression, $.unary_expression),
1032
+ alias($._arithmetic_ternary_expression, $.ternary_expression),
1033
+ alias($._arithmetic_binary_expression, $.binary_expression),
1034
+ alias($._arithmetic_postfix_expression, $.postfix_expression),
1035
+ alias($._arithmetic_parenthesized_expression, $.parenthesized_expression),
1036
+ $.command_substitution,
1037
+ )),
1038
+
1039
+ _arithmetic_literal: $ => prec(1, choice(
1040
+ $.number,
1041
+ $.subscript,
1042
+ $.variable_ref,
1043
+ $.expansion,
1044
+ $.variable_name,
1045
+ $.string,
1046
+ $.word, // Allow bare identifiers in arithmetic context (e.g., 'start' in $((start)))
1047
+ )),
1048
+
1049
+ _arithmetic_binary_expression: $ => {
1050
+ const table = [
1051
+ [choice('+=', '-=', '*=', '/=', '%=', '**=', '<<=', '>>=', '&=', '^=', '|='), PREC.UPDATE],
1052
+ [choice('=', '=~'), PREC.ASSIGN],
1053
+ ['||', PREC.LOGICAL_OR],
1054
+ ['&&', PREC.LOGICAL_AND],
1055
+ ['|', PREC.BITWISE_OR],
1056
+ ['^', PREC.BITWISE_XOR],
1057
+ ['&', PREC.BITWISE_AND],
1058
+ [choice('==', '!='), PREC.EQUALITY],
1059
+ [choice('<', '>', '<=', '>='), PREC.COMPARE],
1060
+ [choice('<<', '>>'), PREC.SHIFT],
1061
+ [choice('+', '-'), PREC.ADD],
1062
+ [choice('*', '/', '%'), PREC.MULTIPLY],
1063
+ ['**', PREC.EXPONENT],
1064
+ ];
1065
+
1066
+ return choice(...table.map(([operator, precedence]) => {
1067
+ // @ts-ignore
1068
+ return prec.left(precedence, seq(
1069
+ field('left', $._arithmetic_expression),
1070
+ // @ts-ignore
1071
+ field('operator', operator),
1072
+ field('right', $._arithmetic_expression),
1073
+ ));
1074
+ }));
1075
+ },
1076
+
1077
+ _arithmetic_ternary_expression: $ => prec.left(PREC.TERNARY, seq(
1078
+ field('condition', $._arithmetic_expression),
1079
+ '?',
1080
+ field('consequence', $._arithmetic_expression),
1081
+ ':',
1082
+ field('alternative', $._arithmetic_expression),
1083
+ )),
1084
+
1085
+ _arithmetic_unary_expression: $ => choice(
1086
+ prec(PREC.PREFIX, seq(
1087
+ field('operator', tokenLiterals(1, '++', '--')),
1088
+ $._arithmetic_expression,
1089
+ )),
1090
+ prec(PREC.UNARY, seq(
1091
+ field('operator', tokenLiterals(1, '-', '+', '~')),
1092
+ $._arithmetic_expression,
1093
+ )),
1094
+ prec.right(PREC.UNARY, seq(
1095
+ field('operator', '!'),
1096
+ $._arithmetic_expression,
1097
+ )),
1098
+ ),
1099
+
1100
+ _arithmetic_postfix_expression: $ => prec(PREC.POSTFIX, seq(
1101
+ $._arithmetic_expression,
1102
+ field('operator', choice('++', '--')),
1103
+ )),
1104
+
1105
+ _arithmetic_parenthesized_expression: $ => seq(
1106
+ '(',
1107
+ $._arithmetic_expression,
1108
+ ')',
1109
+ ),
1110
+
1111
+
1112
+ concatenation: $ => prec(-1,
1113
+ seq(
1114
+ choice(
1115
+ $._primary_expression,
1116
+ alias($._special_character, $.word),
1117
+ ),
1118
+ repeat1(seq(
1119
+ choice($._concat, alias(/`\s*`/, '``')),
1120
+ choice(
1121
+ $._primary_expression,
1122
+ alias($._special_character, $.word),
1123
+ alias($._comment_word, $.word),
1124
+ // Use PEEK to check for $ without consuming it for concatenation
1125
+ $._peek_bare_dollar,
1126
+ ),
1127
+ )),
1128
+ // Use PEEK for trailing $ in concatenation
1129
+ optional(seq($._concat, $._peek_bare_dollar)),
1130
+ )),
1131
+
1132
+ _special_character: _ => token(prec(-1, choice('{', '}', '[', ']'))),
1133
+
1134
+ string: $ => seq(
1135
+ '"',
1136
+ repeat(seq(
1137
+ choice(
1138
+ seq(optional($._bare_dollar), $.string_content),
1139
+ $.expansion,
1140
+ $.variable_ref,
1141
+ $.command_substitution,
1142
+ $.arithmetic_expansion,
1143
+ ),
1144
+ optional($._concat),
1145
+ )),
1146
+ optional($._raw_dollar),
1147
+ '"',
1148
+ ),
1149
+
1150
+ string_content: _ => token(prec(-1, /([^"`$\\\r\n]|\\(.|\r?\n))+/)),
1151
+
1152
+ translated_string: $ => prec(1, seq($._bare_dollar, $.string)),
1153
+
1154
+ array: $ => seq(
1155
+ '(',
1156
+ repeat($._literal),
1157
+ ')',
1158
+ ),
1159
+
1160
+ raw_string: _ => /'[^']*'/,
1161
+
1162
+ ansi_c_string: _ => /\$'([^']|\\')*'/,
1163
+
1164
+ number: $ => choice(
1165
+ /-?(0x)?[0-9]+(#[0-9A-Za-z@_]+)?/,
1166
+ // the base can be an expansion or command substitution
1167
+ seq(/-?(0x)?[0-9]+#/, choice($.expansion, $.command_substitution)),
1168
+ ),
1169
+
1170
+ // Note: this is different than variable_name
1171
+ // in part it allows subscript,
1172
+ // but more importably, variable_name is used with
1173
+ // assignment like semantics
1174
+ variable_ref: $ => prec.right(1, seq(
1175
+ alias($._bare_dollar, "$"),
1176
+ $._variable_ref,
1177
+ )),
1178
+
1179
+ _variable_ref: $ => prec.right(40, seq(
1180
+ choice(
1181
+ $._special_variable_name,
1182
+ $._simple_variable_name,
1183
+ $.subscript,
1184
+
1185
+ // alias('!', $.special_variable_name),
1186
+ // alias('#', $.special_variable_name),
1187
+ ),
1188
+ optional(field('modifier', $.expansion_modifier))
1189
+ )),
1190
+
1191
+ dollar_variable: $ => prec(1, seq(
1192
+ alias($._bare_dollar, "$"),
1193
+ choice(
1194
+ $._simple_variable_name,
1195
+ $._special_variable_name,
1196
+ ),
1197
+ )),
1198
+
1199
+ // Variable references within expansion contexts (similar to _variable_ref)
1200
+ _expansion_variable_ref: $ => seq(
1201
+ choice(
1202
+ $._simple_variable_name,
1203
+ $._special_variable_name,
1204
+ ),
1205
+ optional(seq( // Postfix subscript operator (left-associating)
1206
+ '[',
1207
+ optional(field('flags', $.zsh_array_subscript_flags)),
1208
+ field('index', choice($._param_arithmetic_expression, $.array_star, $.array_at)),
1209
+ ']'
1210
+ ))
1211
+ ),
1212
+
1213
+ string_expansion: $ => seq($._bare_dollar, $.string),
1214
+
1215
+ // Visible rule for parameter expansion substring operations
1216
+ expansion_substring: $ => $._expansion_max_length,
1217
+
1218
+ _expansion_default_value: $ => choice(
1219
+ $._literal,
1220
+ $._empty_value
1221
+ ),
1222
+
1223
+ // Visible rule for parameter expansion defaults
1224
+ expansion_default: $ => seq(
1225
+ optional(field('name', choice($._expansion_variable_ref, $.expansion))),
1226
+ choice(
1227
+ seq(token.immediate('-'), field('default', $._expansion_default_value)),
1228
+ seq(token.immediate(':'), token.immediate('-'), field('default', $._expansion_default_value)),
1229
+ seq(token.immediate('+'), field('default', $._expansion_default_value)),
1230
+ seq(token.immediate(':'), token.immediate('+'), field('default', $._expansion_default_value)),
1231
+ seq(token.immediate('='), field('default', $._expansion_default_value)),
1232
+ seq(token.immediate(':'), token.immediate('='), field('default', $._expansion_default_value)),
1233
+ seq(token.immediate('='), field('default', $._expansion_default_value)),
1234
+ seq(token.immediate(':'), token.immediate(':'), token.immediate('='), field('default', $._expansion_default_value)),
1235
+ seq(token.immediate('?'), field('default', $._expansion_default_value)),
1236
+ seq(token.immediate(':'), token.immediate('?'), field('default', $._expansion_default_value)),
1237
+ )
1238
+ ),
1239
+
1240
+ // Visible rule for parameter expansion patterns
1241
+ expansion_pattern: $ => seq(
1242
+ field('name', choice($._expansion_variable_ref, $.expansion)),
1243
+ choice(
1244
+ seq($._hash_pattern, $._pattern_suffix_start, field('pattern', $._param_pattern)),
1245
+ seq($._double_hash_pattern, $._pattern_suffix_start, field('pattern', $._param_pattern)),
1246
+ seq(token.immediate('%'), $._pattern_suffix_start, field('pattern', $._param_pattern)),
1247
+ seq(token.immediate('%%'), $._pattern_suffix_start, field('pattern', $._param_pattern)),
1248
+ seq(token.immediate(':'), $._hash_pattern, $._pattern_suffix_start, field('pattern', $._param_pattern)),
1249
+ seq(token.immediate(':'), $._double_hash_pattern, $._pattern_suffix_start, field('pattern', $._param_pattern)),
1250
+ seq(token.immediate(':'), token.immediate('%'), $._pattern_suffix_start, field('pattern', $._param_pattern)),
1251
+ seq(token.immediate(':'), token.immediate('%%'), $._pattern_suffix_start, field('pattern', $._param_pattern)),
1252
+ seq(token.immediate('//'),
1253
+ $._pattern_start,
1254
+ field('pattern', $._param_pattern_no_slash),
1255
+ '/',
1256
+ field('replacement', $._param_replacement)
1257
+ ),
1258
+ seq(token.immediate('/'),
1259
+ $._pattern_start,
1260
+ field('pattern', $._param_pattern_no_slash),
1261
+ '/',
1262
+ field('replacement', $._param_replacement),
1263
+ ),
1264
+ seq(token.immediate(':'), token.immediate('/'),
1265
+ $._pattern_start,
1266
+ field('pattern', $._param_pattern_no_slash),
1267
+ '/',
1268
+ field('replacement', $._param_replacement)),
1269
+ )
1270
+ ),
1271
+
1272
+ // Visible rule for parameter expansion arrays
1273
+ expansion_array: $ => seq(
1274
+ field('name', choice($._expansion_variable_ref, $.expansion)),
1275
+ choice(
1276
+ seq(token.immediate(':'), token.immediate('|'), field('array', $._literal)),
1277
+ seq(token.immediate(':'), token.immediate('*'), field('array', $._literal)),
1278
+ seq(token.immediate(':'), token.immediate('^'), field('array', $._literal)),
1279
+ seq(token.immediate(':'), token.immediate('^'), token.immediate('^'), field('array', $._literal)),
1280
+ )
1281
+ ),
1282
+
1283
+ expansion_modifier: $ => prec.right(4, seq(
1284
+ token.immediate(':'),
1285
+ choice(
1286
+ token(/[aAce]/),
1287
+ token(/h[0-9]*/),
1288
+ token(/[lpPqQr]/),
1289
+ seq(token.immediate('s'), token.immediate('/'),
1290
+ $._pattern_start,
1291
+ field('search', $._param_pattern_no_slash),
1292
+ token.immediate('/'), field('replace', $._param_replacement),
1293
+ optional(seq(
1294
+ token.immediate('/'), optional(
1295
+ seq(token.immediate(':'), /[Gg]/)
1296
+ )
1297
+ )
1298
+ )
1299
+ ),
1300
+ token(/[&ux]/),
1301
+ token(/t[0-9]*/))
1302
+ )),
1303
+
1304
+ // Maintain same order as zsh docs
1305
+ expansion_with_modifier: $ => seq(
1306
+ optional(field('name', choice($._expansion_variable_ref, $.expansion))),
1307
+ field('modifier', $.expansion_modifier)
1308
+ ),
1309
+
1310
+ expansion_style: $ => choice(
1311
+ token.immediate('#'), // ${# var} - length
1312
+ token.immediate('!'), // ${! var} - indirect expansion
1313
+ // Order longer sequences first to resolve conflicts
1314
+ prec(2, token.immediate('^^')), // ${^^ var} - RC_EXPAND_PARAM
1315
+ prec(1, token.immediate('^')), // ${^ var} - RC_EXPAND_PARAM
1316
+ prec(2, token.immediate('==')), // ${== var} - SH_WORD_SPLIT
1317
+ prec(1, token.immediate('=')), // ${= var} - SH_WORD_SPLIT
1318
+ prec(2, token.immediate('~~')), // ${~~ var} - GLOB_SUBST
1319
+ prec(1, token.immediate('~')), // ${~ var} - GLOB_SUBST
1320
+ ),
1321
+
1322
+ expansion: $ => seq(
1323
+ prec(2, alias(seq($._bare_dollar, $._brace_start), "${")),
1324
+ choice(
1325
+ prec.right(10, seq(field('style', $.expansion_style),
1326
+ field('flags', $.expansion_flags), $._expansion_body)),
1327
+ prec.right(10, seq(field('style', $.expansion_style), $._expansion_body)),
1328
+ prec.left(10, seq(field('flags', $.expansion_flags), $._expansion_body)),
1329
+ prec.left($._expansion_body),
1330
+ ),
1331
+ '}',
1332
+ ),
1333
+
1334
+ // Zsh parameter expansion flags: ${(L)var}, ${(j:,:)array}, etc.
1335
+ expansion_flags: $ => token.immediate(/\([^)]+\)/),
1336
+
1337
+ _expansion_body: $ => choice(
1338
+ // HIGHEST PRIORITY: Colon operations (moved to top)
1339
+ $.expansion_with_modifier,
1340
+
1341
+ $.expansion_substring,
1342
+
1343
+ $.expansion_pattern,
1344
+ $.expansion_default,
1345
+ $.expansion_array,
1346
+
1347
+ // Basic variable reference (fallback for ${var})
1348
+ field('name', choice($._expansion_variable_ref,
1349
+ $.expansion, $.string, $.raw_string,
1350
+ $.command_substitution)),
1351
+ ),
1352
+
1353
+ // Base expressions (recursive)
1354
+ _expansion_pattern: $ => choice(
1355
+ $.regex,
1356
+ $.word,
1357
+ $.string,
1358
+ $.raw_string,
1359
+ ),
1360
+
1361
+ // Replacement value for substitutions
1362
+ _expansion_replacement: $ => choice(
1363
+ $.word,
1364
+ $.string,
1365
+ $.raw_string,
1366
+ $.expansion,
1367
+ $.variable_ref,
1368
+ $.command_substitution,
1369
+ ),
1370
+
1371
+ // Values for assignment operations
1372
+ _expansion_value: $ => choice(
1373
+ $.word,
1374
+ $.string,
1375
+ $.raw_string,
1376
+ $.array,
1377
+ $.expansion,
1378
+ $.variable_ref,
1379
+ $.command_substitution,
1380
+ ),
1381
+
1382
+ // Numeric expressions for substring operations
1383
+ _expansion_number: $ => choice(
1384
+ $.number,
1385
+ $.expansion,
1386
+ $.variable_ref,
1387
+ $.arithmetic_expansion,
1388
+ $.command_substitution, // $(echo 5) - allows ${var:$(command)}
1389
+ ),
1390
+ _expansion_expression: $ => prec(1, seq(
1391
+ field('operator', immediateLiterals('=', ':=', '-', ':-', '+', ':+', '?', ':?')),
1392
+ optional(seq(
1393
+ choice(
1394
+ alias($._concatenation_in_expansion, $.concatenation),
1395
+ $.command_substitution,
1396
+ $.word,
1397
+ $.expansion,
1398
+ $.variable_ref,
1399
+ $.array,
1400
+ $.string,
1401
+ $.raw_string,
1402
+ $.ansi_c_string,
1403
+ alias($._expansion_word, $.word),
1404
+ ),
1405
+ )),
1406
+ )),
1407
+
1408
+ _expansion_max_length: $ => seq(
1409
+ field('name', choice($._expansion_variable_ref, $.expansion)),
1410
+ token.immediate(':'),
1411
+ field('offset', choice(
1412
+ $.variable_ref,
1413
+ $.number,
1414
+ $.arithmetic_expansion,
1415
+ $.expansion,
1416
+ $.parenthesized_expression,
1417
+ $.command_substitution,
1418
+ alias($._expansion_max_length_binary_expression, $.binary_expression),
1419
+ /\n/,
1420
+ )),
1421
+ optional(seq(
1422
+ token.immediate(':'),
1423
+ optional(field('length', choice(
1424
+ $.variable_ref,
1425
+ $.number,
1426
+ $.arithmetic_expansion,
1427
+ $.expansion,
1428
+ $.parenthesized_expression,
1429
+ $.command_substitution,
1430
+ alias($._expansion_max_length_binary_expression, $.binary_expression),
1431
+ /\n/,
1432
+ ))),
1433
+ )),
1434
+ ),
1435
+
1436
+ _expansion_max_length_expression: $ => choice(
1437
+ $.variable_ref,
1438
+ $.number,
1439
+ $.expansion,
1440
+ alias($._expansion_max_length_binary_expression, $.binary_expression),
1441
+ ),
1442
+ _expansion_max_length_binary_expression: $ => {
1443
+ const table = [
1444
+ [choice('+', '-'), PREC.ADD],
1445
+ [choice('*', '/', '%'), PREC.MULTIPLY],
1446
+ ];
1447
+
1448
+ return choice(...table.map(([operator, precedence]) => {
1449
+ // @ts-ignore
1450
+ return prec.left(precedence, seq(
1451
+ $._expansion_max_length_expression,
1452
+ // @ts-ignore
1453
+ field('operator', operator),
1454
+ $._expansion_max_length_expression,
1455
+ ));
1456
+ }));
1457
+ },
1458
+
1459
+ _expansion_operator: $ => seq(
1460
+ field('operator', token.immediate('@')),
1461
+ field('operator', immediateLiterals('U', 'u', 'L', 'Q', 'E', 'P', 'A', 'K', 'a', 'k')),
1462
+ ),
1463
+
1464
+ _concatenation_in_expansion: $ => prec(-2, seq(
1465
+ choice(
1466
+ $.word,
1467
+ $.variable_name,
1468
+ $.variable_ref,
1469
+ $.expansion,
1470
+ $.string,
1471
+ $.raw_string,
1472
+ $.ansi_c_string,
1473
+ $.command_substitution,
1474
+ alias($._expansion_word, $.word),
1475
+ $.array,
1476
+ $.process_substitution,
1477
+ ),
1478
+ repeat1(seq(
1479
+ choice($._concat, alias(/`\s*`/, '``')),
1480
+ choice(
1481
+ $.word,
1482
+ $.variable_name,
1483
+ $.variable_ref,
1484
+ $.expansion,
1485
+ $.string,
1486
+ $.raw_string,
1487
+ $.ansi_c_string,
1488
+ $.command_substitution,
1489
+ alias($._expansion_word, $.word),
1490
+ $.array,
1491
+ $.process_substitution,
1492
+ ),
1493
+ )),
1494
+ )),
1495
+
1496
+ _concatenation_in_expansion_regex: $ => prec(-2, seq(
1497
+ choice(
1498
+ $.regex,
1499
+ $.dollar_variable,
1500
+ $.expansion,
1501
+ $.string,
1502
+ $.raw_string,
1503
+ $.ansi_c_string,
1504
+ $.command_substitution,
1505
+ alias($._expansion_word, $.word), // FIXME
1506
+ ),
1507
+ repeat1(seq(
1508
+ choice($._concat, alias(/`\s*`/, '``')),
1509
+ choice(
1510
+ $.regex,
1511
+ $.dollar_variable,
1512
+ $.expansion,
1513
+ $.string,
1514
+ $.raw_string,
1515
+ $.ansi_c_string,
1516
+ $.command_substitution,
1517
+ alias($._expansion_word, $.word), // FIXME
1518
+ ),
1519
+ )),
1520
+ )),
1521
+
1522
+ _concatenation_in_expansion_regex_no_slash: $ => prec(-2, seq(
1523
+ choice(
1524
+ alias($._regex_no_slash, $.regex),
1525
+ $.dollar_variable,
1526
+ $.expansion,
1527
+ $.string,
1528
+ $.raw_string,
1529
+ $.ansi_c_string,
1530
+ $.command_substitution,
1531
+ alias($._expansion_word, $.word),
1532
+ ),
1533
+ repeat1(seq(
1534
+ choice($._concat, alias(/`\s*`/, '``')),
1535
+ choice(
1536
+ alias($._regex_no_slash, $.regex),
1537
+ $.dollar_variable,
1538
+ $.expansion,
1539
+ $.string,
1540
+ $.raw_string,
1541
+ $.ansi_c_string,
1542
+ $.command_substitution,
1543
+ alias($._expansion_word, $.word),
1544
+ ),
1545
+ )),
1546
+ )),
1547
+
1548
+ _concatenation_in_expansion_regex_replacement: $ => prec(-2, seq(
1549
+ choice(
1550
+ alias($._regex_no_slash, $.regex),
1551
+ $.dollar_variable,
1552
+ $.expansion,
1553
+ $.string,
1554
+ $.raw_string,
1555
+ $.ansi_c_string,
1556
+ $.command_substitution,
1557
+ alias($._expansion_word, $.word),
1558
+ ),
1559
+ repeat1(seq(
1560
+ choice($._concat, alias(/`\s*`/, '``')),
1561
+ choice(
1562
+ alias($._regex_no_slash, $.regex),
1563
+ $.dollar_variable,
1564
+ $.expansion,
1565
+ $.string,
1566
+ $.raw_string,
1567
+ $.ansi_c_string,
1568
+ $.command_substitution,
1569
+ alias($._expansion_word, $.word),
1570
+ ),
1571
+ )),
1572
+ )),
1573
+
1574
+ command_substitution: $ => prec(1, choice(
1575
+ seq(alias(seq($._bare_dollar, '('), "$("), $._statements, ')'),
1576
+ seq(alias(seq($._bare_dollar, '('), "$("), field('redirect', $.file_redirect), ')'),
1577
+ prec(1, seq('`', $._statements, '`')),
1578
+ //seq('$`', $._statements, '`'), // not legal zsh
1579
+ )),
1580
+
1581
+ process_substitution: $ => seq(
1582
+ choice('<(', '>(',
1583
+ prec(-1, '=(')), // Lower precedence to allow variable assignment to win
1584
+ $._statements,
1585
+ ')',
1586
+ ),
1587
+
1588
+ _extglob_blob: $ => choice(
1589
+ $.extglob_pattern,
1590
+ seq(
1591
+ $.extglob_pattern,
1592
+ choice($.string, $.expansion, $.command_substitution),
1593
+ optional($.extglob_pattern),
1594
+ ),
1595
+ ),
1596
+
1597
+ comment: $ => token(prec(-20, /#[^\r\n]*/)),
1598
+
1599
+ _comment_word: _ => token(prec(-8, seq(
1600
+ choice(
1601
+ noneOf(...SPECIAL_CHARACTERS),
1602
+ seq('\\', noneOf('\\s')),
1603
+ ),
1604
+ repeat(choice(
1605
+ noneOf(...SPECIAL_CHARACTERS),
1606
+ seq('\\', noneOf('\\s')),
1607
+ '\\ ',
1608
+ )),
1609
+ ))),
1610
+
1611
+ _simple_variable_name: $ => $.simple_variable_name,
1612
+
1613
+ _special_variable_name: $ => $.special_variable_name,
1614
+
1615
+ word: $ => token(seq(
1616
+ choice(
1617
+ noneOf('#', ':', ...SPECIAL_CHARACTERS),
1618
+ seq('\\', noneOf('\\s')),
1619
+ ),
1620
+ repeat(choice(
1621
+ noneOf(...SPECIAL_CHARACTERS),
1622
+ seq('\\', noneOf('\\s')),
1623
+ '\\ ',
1624
+ )),
1625
+ )),
1626
+
1627
+ _c_terminator: _ => choice(';', /\n/, '&'),
1628
+ _terminator: _ => choice(';', ';;', /\n/, '&'),
1629
+
1630
+
1631
+ // Parameter expansion specific rules - no globs allowed
1632
+ _param_variable_ref: $ => choice(
1633
+ $._simple_variable_name,
1634
+ $._special_variable_name,
1635
+ $.subscript,
1636
+ $.expansion, // nested expansions
1637
+ ),
1638
+
1639
+ // Parameter-safe expression system (excludes glob_pattern)
1640
+ _param_expression: $ => choice(
1641
+ $._param_literal,
1642
+ $._param_unary_expression,
1643
+ $._param_ternary_expression,
1644
+ $._param_binary_expression,
1645
+ $._param_postfix_expression,
1646
+ $._param_parenthesized_expression,
1647
+ ),
1648
+
1649
+ _param_binary_expression: $ => {
1650
+ const table = [
1651
+ [choice('+=', '-=', '*=', '/=', '%=', '**=', '<<=', '>>=', '&=', '^=', '|='), PREC.UPDATE],
1652
+ ['||', PREC.LOGICAL_OR],
1653
+ ['&&', PREC.LOGICAL_AND],
1654
+ ['|', PREC.BITWISE_OR],
1655
+ ['^', PREC.BITWISE_XOR],
1656
+ ['&', PREC.BITWISE_AND],
1657
+ [choice('==', '!='), PREC.EQUALITY],
1658
+ [choice('<', '>', '<=', '>='), PREC.COMPARE],
1659
+ [$.test_operator, PREC.TEST],
1660
+ [choice('<<', '>>'), PREC.SHIFT],
1661
+ [choice('+', '-'), PREC.ADD],
1662
+ [choice('*', '/', '%'), PREC.MULTIPLY],
1663
+ ['**', PREC.EXPONENT],
1664
+ ];
1665
+
1666
+ return choice(
1667
+ choice(...table.map(([operator, precedence]) => {
1668
+ // @ts-ignore
1669
+ return prec[operator === '**' ? 'right' : 'left'](precedence, seq(
1670
+ field('left', $._param_expression),
1671
+ // @ts-ignore
1672
+ field('operator', operator),
1673
+ field('right', $._param_expression),
1674
+ ));
1675
+ })),
1676
+ prec.right(PREC.ASSIGN, seq(
1677
+ field('left', $._param_expression),
1678
+ field('operator', '='),
1679
+ field('right', $._param_expression),
1680
+ )),
1681
+ prec.right(PREC.ASSIGN, seq(
1682
+ field('left', $._param_expression),
1683
+ field('operator', '=~'),
1684
+ field('right', choice(alias($._regex_no_space, $.regex), $._param_expression)),
1685
+ )),
1686
+ );
1687
+ },
1688
+
1689
+ _param_ternary_expression: $ => prec.left(PREC.TERNARY, seq(
1690
+ field('condition', $._param_expression),
1691
+ '?',
1692
+ field('consequence', $._param_expression),
1693
+ ':',
1694
+ field('alternative', $._param_expression),
1695
+ )),
1696
+
1697
+ _param_unary_expression: $ => choice(
1698
+ prec(PREC.PREFIX, seq(
1699
+ field('operator', tokenLiterals(1, '++', '--')),
1700
+ $._param_expression,
1701
+ )),
1702
+ prec(PREC.UNARY, seq(
1703
+ field('operator', tokenLiterals(1, '-', '+', '~')),
1704
+ $._param_expression,
1705
+ )),
1706
+ prec.right(PREC.UNARY, seq(
1707
+ field('operator', '!'),
1708
+ $._param_expression,
1709
+ )),
1710
+ prec.right(PREC.TEST, seq(
1711
+ field('operator', $.test_operator),
1712
+ $._param_expression,
1713
+ )),
1714
+ ),
1715
+
1716
+ _param_postfix_expression: $ => prec(PREC.POSTFIX, seq(
1717
+ $._param_expression,
1718
+ field('operator', choice('++', '--')),
1719
+ )),
1720
+
1721
+ _param_parenthesized_expression: $ => seq(
1722
+ '(',
1723
+ $._param_expression,
1724
+ ')',
1725
+ ),
1726
+ _param_assignment_value: $ => choice(
1727
+ $._param_literal,
1728
+ $._param_array,
1729
+
1730
+
1731
+ $.command_substitution,
1732
+ ),
1733
+
1734
+ _param_literal: $ => choice(
1735
+ $.word,
1736
+ $.number,
1737
+ $.expansion,
1738
+ $.variable_ref,
1739
+
1740
+ $.string,
1741
+ $.raw_string,
1742
+ $.ansi_c_string,
1743
+ $.translated_string,
1744
+ // Explicitly exclude glob_pattern and test_operator
1745
+ ),
1746
+
1747
+ _param_array: $ => seq(
1748
+ "(",
1749
+ repeat($._param_literal),
1750
+ ")",
1751
+ ),
1752
+
1753
+ _param_concatenation: $ => prec(-1, seq(
1754
+ choice(
1755
+ $._param_primary_expression,
1756
+ ),
1757
+ repeat1(seq(
1758
+ $._concat, // Same concat token
1759
+ choice(
1760
+ $._param_primary_expression,
1761
+ ),
1762
+ ))
1763
+ )),
1764
+
1765
+ // Parameter-specific primary expressions
1766
+ _param_primary_expression: $ => choice(
1767
+ alias($._expansion_word, $.word),
1768
+ $.string,
1769
+ $.raw_string,
1770
+ $.expansion,
1771
+ $.variable_ref,
1772
+ $.command_substitution,
1773
+ $.arithmetic_expansion,
1774
+ // Note: NOT $.glob_pattern here to avoid the ambiguities
1775
+ ),
1776
+
1777
+ _param_pattern: $ => seq(
1778
+ optional(choice(token.immediate('%'),
1779
+ token.immediate('#'))),
1780
+ choice(
1781
+ $.dollar_variable, // $var - allows ${foo/$pattern/repl}
1782
+ $.regex,
1783
+ $.string,
1784
+ $.raw_string,
1785
+
1786
+ $.glob_pattern,
1787
+ $.expansion, // ${nested} - allows ${foo/${bar}/baz}
1788
+ $.command_substitution, // $(cmd) - allows ${foo/$(pattern)/repl}
1789
+ $.arithmetic_expansion, // $((expr)) - allows ${foo/$((n))/repl}
1790
+
1791
+ // FIXME
1792
+ // seq($.string, $.regex),
1793
+ // seq($.regex, $.string), // FIXME: expand
1794
+ alias($._expansion_word, $.word),
1795
+ alias($._concatenation_in_expansion_regex, $.concatenation),
1796
+ )),
1797
+
1798
+ _param_pattern_no_slash: $ => seq(
1799
+ optional(choice(token.immediate('%'),
1800
+ token.immediate('#'))),
1801
+ choice(
1802
+ alias($._expansion_word, $.word), // FIXME? YES / NO
1803
+ alias($._regex_no_slash, $.regex),
1804
+ $.string,
1805
+ $.raw_string,
1806
+
1807
+ $.glob_pattern,
1808
+ $.expansion, // ${nested} - allows ${foo/${bar}/baz}
1809
+ $.dollar_variable, // $var - allows ${foo/$pattern/repl}
1810
+ $.command_substitution, // $(cmd) - allows ${foo/$(pattern)/repl}
1811
+ $.arithmetic_expansion, // $((expr)) - allows ${foo/$((n))/repl}
1812
+
1813
+ // FIXME
1814
+ // seq($.string, alias($._regex_no_slash, $.regex)),
1815
+ // seq(alias($._regex_no_slash, $.regex), $.string), // FIXME: expand
1816
+ alias($._concatenation_in_expansion_regex_no_slash, $.concatenation),
1817
+ )),
1818
+
1819
+ _param_replacement: $ => choice(
1820
+ alias(prec(-2, repeat1($._special_character)), $._expansion_word),
1821
+ alias($._regex_no_slash, $.regex),
1822
+ alias($._expansion_word, $.word),
1823
+ $.string,
1824
+ $.raw_string,
1825
+ $.expansion, // ${nested} - allows ${foo/bar/${baz}}
1826
+ $.variable_ref, // $var - allows ${foo/bar/$repl/}
1827
+ $.command_substitution, // $(cmd) - allows ${foo/bar/$(repl)}
1828
+ $.arithmetic_expansion, // $((expr)) - allows ${foo/a/$((n))}
1829
+ alias($._concatenation_in_expansion_regex_replacement, $.concatenation),
1830
+ $.array,
1831
+ ),
1832
+ },
1833
+ });
1834
+
1835
+ /**
1836
+ * Returns a regular expression that matches any character except the ones
1837
+ * provided.
1838
+ *
1839
+ * @param {...string} characters
1840
+ *
1841
+ * @returns {RegExp}
1842
+ */
1843
+ function noneOf(...characters) {
1844
+ // const negatedString = characters.map(c => c == '\\' ? '\\\\' : c).join('');
1845
+ const negatedString = characters.map(c => {
1846
+ if (c === '\\') return '\\\\';
1847
+ if (c === '[') return '\\['; // Escape [ in character class
1848
+ if (c === ']') return '\\]'; // Escape ] in character class
1849
+ if (c === '^') return '\\^'; // Escape ^ if it's first
1850
+ if (c === '-') return '\\-'; // Escape - to avoid ranges
1851
+ return c;
1852
+ }).join('');
1853
+ return new RegExp('[^' + negatedString + ']');
1854
+ }
1855
+
1856
+ /**
1857
+ * Creates a rule to optionally match one or more of the rules separated by a comma
1858
+ *
1859
+ * @param {RuleOrLiteral} rule
1860
+ *
1861
+ * @returns {ChoiceRule}
1862
+ */
1863
+ function commaSep(rule) {
1864
+ return optional(commaSep1(rule));
1865
+ }
1866
+
1867
+ /**
1868
+ * Creates a rule to match one or more of the rules separated by a comma
1869
+ *
1870
+ * @param {RuleOrLiteral} rule
1871
+ *
1872
+ * @returns {SeqRule}
1873
+ */
1874
+ function commaSep1(rule) {
1875
+ return seq(rule, repeat(seq(',', rule)));
1876
+ }
1877
+
1878
+ /**
1879
+ *
1880
+ * Turns a list of rules into a choice of immediate rule
1881
+ *
1882
+ * @param {(RegExp | string)[]} literals
1883
+ *
1884
+ * @returns {ChoiceRule}
1885
+ */
1886
+ function immediateLiterals(...literals) {
1887
+ return choice(...literals.map(l => token.immediate(l)));
1888
+ }
1889
+
1890
+ /**
1891
+ *
1892
+ * Turns a list of rules into a choice of aliased token rules
1893
+ *
1894
+ * @param {number} precedence
1895
+ *
1896
+ * @param {(RegExp | string)[]} literals
1897
+ *
1898
+ * @returns {ChoiceRule}
1899
+ */
1900
+ function tokenLiterals(precedence, ...literals) {
1901
+ return choice(...literals.map(l => token(prec(precedence, l))));
1902
+ }
1903
+
1904
+