vinter 0.4.0 → 0.6.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.
data/lib/vinter/parser.rb CHANGED
@@ -1,3 +1,4 @@
1
+ require 'pry'
1
2
  module Vinter
2
3
  class Parser
3
4
  def initialize(tokens, source_text = nil)
@@ -71,6 +72,95 @@ module Vinter
71
72
  end
72
73
  end
73
74
 
75
+ # Helper method to add errors with consistent formatting
76
+ def add_error(message, token = current_token)
77
+ @errors << {
78
+ message: message,
79
+ position: @position,
80
+ line: token ? token[:line] : 0,
81
+ column: token ? token[:column] : 0
82
+ }
83
+ end
84
+
85
+ # Helper method to expect a specific keyword value
86
+ def expect_keyword(value)
87
+ if current_token && current_token[:type] == :keyword && current_token[:value] == value
88
+ advance
89
+ true
90
+ else
91
+ add_error("Expected '#{value}' keyword")
92
+ false
93
+ end
94
+ end
95
+
96
+ # Helper method to parse a body of statements until one or more end keywords
97
+ # Returns array of parsed statements
98
+ def parse_body_until(*end_keywords)
99
+ body = []
100
+ while @position < @tokens.length
101
+ # Check if we've reached an end keyword
102
+ if current_token && current_token[:type] == :keyword &&
103
+ end_keywords.include?(current_token[:value])
104
+ break
105
+ end
106
+
107
+ stmt = parse_statement
108
+ body << stmt if stmt
109
+ end
110
+ body
111
+ end
112
+
113
+ # Helper method to expect and consume an end keyword
114
+ # Returns true if found, false and adds error if not
115
+ def expect_end_keyword(*keywords)
116
+ if current_token && current_token[:type] == :keyword &&
117
+ keywords.include?(current_token[:value])
118
+ advance
119
+ true
120
+ else
121
+ # Only add error if we haven't reached end of file
122
+ if @position < @tokens.length
123
+ add_error("Expected #{keywords.join(' or ')} to close block")
124
+ end
125
+ false
126
+ end
127
+ end
128
+
129
+ # Helper method to parse comma-separated loop variables
130
+ # Used by for loops with brackets or parentheses
131
+ def parse_loop_variables
132
+ loop_vars = []
133
+
134
+ loop do
135
+ if current_token && (current_token[:type] == :identifier ||
136
+ current_token[:type] == :local_variable ||
137
+ current_token[:type] == :global_variable ||
138
+ current_token[:type] == :script_local)
139
+ loop_vars << advance
140
+ else
141
+ add_error("Expected identifier in for loop variables")
142
+ break
143
+ end
144
+
145
+ if current_token && current_token[:type] == :comma
146
+ advance # Skip ','
147
+ else
148
+ break
149
+ end
150
+ end
151
+
152
+ loop_vars
153
+ end
154
+
155
+ # Helper method to expect 'in' keyword in for loops
156
+ def expect_in_keyword
157
+ if !current_token || (current_token[:type] != :identifier || current_token[:value] != 'in')
158
+ add_error("Expected 'in' after for loop variables")
159
+ else
160
+ advance # Skip 'in'
161
+ end
162
+ end
163
+
74
164
  def parse_program
75
165
  statements = []
76
166
 
@@ -155,7 +245,6 @@ module Vinter
155
245
  if !current_token
156
246
  return nil
157
247
  end
158
- start_token = current_token
159
248
 
160
249
  # Handle pipe as command separator
161
250
  if current_token[:type] == :operator && current_token[:value] == '|'
@@ -265,6 +354,8 @@ module Vinter
265
354
  when 'vim9script'
266
355
  token = advance # Skip 'vim9script'
267
356
  { type: :vim9script_declaration, line: token[:line], column: token[:column] }
357
+ when 'scriptencoding'
358
+ parse_scriptencoding
268
359
  when 'autocmd'
269
360
  parse_autocmd_statement
270
361
  when 'execute', 'exec'
@@ -287,7 +378,7 @@ module Vinter
287
378
  parse_set_command
288
379
  when 'syntax'
289
380
  parse_syntax_command
290
- when 'highlight'
381
+ when 'highlight', 'hi'
291
382
  parse_highlight_command
292
383
  when 'sleep'
293
384
  parse_sleep_command
@@ -333,6 +424,24 @@ module Vinter
333
424
  parse_delete_command
334
425
  elsif current_token[:type] == :percentage
335
426
  parse_range_command
427
+ elsif current_token[:type] == :global_variable
428
+ parse_global_variable
429
+ elsif current_token[:type] == :buffer_local
430
+ parse_buffer_var
431
+ elsif current_token[:type] == :vimfuncs
432
+ advance
433
+ parse_function_call(current_token[:value], current_token[:line], current_token[:column])
434
+ elsif current_token[:type] == :builtin_funcs
435
+ parse_builtin_function_call
436
+ elsif current_token[:type] == :compound_operator
437
+ advance
438
+ parse_expression
439
+ elsif current_token[:type] == :string
440
+ parse_string
441
+ elsif current_token[:type] == :comma
442
+ advance
443
+ elsif current_token[:type] == :colon
444
+ parse_command_line_call
336
445
  else
337
446
  @warnings << {
338
447
  message: "Unexpected token type: #{current_token[:type]}",
@@ -345,6 +454,182 @@ module Vinter
345
454
  end
346
455
  end
347
456
 
457
+ def parse_command_line_call
458
+ token = advance # Skip the ':'
459
+ line = token[:line]
460
+ column = token[:column]
461
+
462
+ # Extract the command or range
463
+ command = token[:value][1..] # Remove the leading ':'
464
+
465
+ # Check if the command is a range (e.g., `:2`, `:$`)
466
+ if command.match?(/^\d+$/) || command == '$'
467
+ return {
468
+ type: :range_command,
469
+ range: command,
470
+ line: line,
471
+ column: column
472
+ }
473
+ end
474
+
475
+ # Otherwise, it's a regular command (e.g., `:exe`)
476
+ args = []
477
+ while current_token && current_token[:type] != :newline
478
+ args << current_token[:value]
479
+ advance
480
+ end
481
+
482
+ {
483
+ type: :command_line_call,
484
+ command: command,
485
+ arguments: args,
486
+ line: line,
487
+ column: column
488
+ }
489
+ end
490
+
491
+ def parse_buffer_var
492
+ token = current_token
493
+ line = token[:line]
494
+ column = token[:column]
495
+ name = token[:value]
496
+ advance
497
+
498
+ # Handle property access (e.g., b:foo.bar)
499
+ target = {
500
+ type: :buffer_variable,
501
+ name: name,
502
+ line: line,
503
+ column: column
504
+ }
505
+
506
+ # Property access (dot notation)
507
+ # shared with global (TODO)
508
+ # would almost just take an overly simple check to the = operator and call everything after b: name
509
+ if current_token && current_token[:type] == :operator && current_token[:value] == '.'
510
+ dot_token = advance
511
+ if current_token && (current_token[:type] == :identifier || current_token[:type] == :keyword)
512
+ property_token = advance
513
+ target = {
514
+ type: :property_access,
515
+ object: target,
516
+ property: property_token[:value],
517
+ line: dot_token[:line],
518
+ column: dot_token[:column]
519
+ }
520
+ else
521
+ add_error("Expected property name after '.' in variable")
522
+ end
523
+ end
524
+
525
+ # Indexed access (e.g., b:foo[bar])
526
+ while current_token && current_token[:type] == :bracket_open
527
+ bracket_token = advance
528
+ index_expr = parse_expression
529
+ expect(:bracket_close)
530
+ target = {
531
+ type: :indexed_access,
532
+ object: target,
533
+ index: index_expr,
534
+ line: bracket_token[:line],
535
+ column: bracket_token[:column]
536
+ }
537
+ end
538
+
539
+ # Assignment operator (= or compound)
540
+ operator = nil
541
+ if current_token && (
542
+ (current_token[:type] == :operator && current_token[:value] == '=') ||
543
+ current_token[:type] == :compound_operator)
544
+ operator = current_token[:value]
545
+ advance
546
+ else
547
+ add_error("Expected assignment operator after global variable")
548
+ end
549
+
550
+ # Value expression
551
+ value = parse_expression
552
+
553
+ {
554
+ type: :buffer_variable_assignment,
555
+ target: target,
556
+ operator: operator,
557
+ value: value,
558
+ line: line,
559
+ column: column
560
+ }
561
+ end
562
+
563
+ def parse_global_variable
564
+ token = current_token
565
+ line = token[:line]
566
+ column = token[:column]
567
+ name = token[:value]
568
+ advance # Skip the global variable token
569
+
570
+ # Handle property access (e.g., g:foo.bar)
571
+ target = {
572
+ type: :global_variable,
573
+ name: name,
574
+ line: line,
575
+ column: column
576
+ }
577
+
578
+ # Property access (dot notation)
579
+ if current_token && current_token[:type] == :operator && current_token[:value] == '.'
580
+ dot_token = advance
581
+ if current_token && (current_token[:type] == :identifier || current_token[:type] == :keyword)
582
+ property_token = advance
583
+ target = {
584
+ type: :property_access,
585
+ object: target,
586
+ property: property_token[:value],
587
+ line: dot_token[:line],
588
+ column: dot_token[:column]
589
+ }
590
+ else
591
+ add_error("Expected property name after '.' in global variable")
592
+ end
593
+ end
594
+
595
+ # Indexed access (e.g., g:foo[bar])
596
+ while current_token && current_token[:type] == :bracket_open
597
+ bracket_token = advance
598
+ index_expr = parse_expression
599
+ expect(:bracket_close)
600
+ target = {
601
+ type: :indexed_access,
602
+ object: target,
603
+ index: index_expr,
604
+ line: bracket_token[:line],
605
+ column: bracket_token[:column]
606
+ }
607
+ end
608
+
609
+ # Assignment operator (= or compound)
610
+ operator = nil
611
+ if current_token && (
612
+ (current_token[:type] == :operator && current_token[:value] == '=') ||
613
+ current_token[:type] == :compound_operator)
614
+ operator = current_token[:value]
615
+ advance
616
+ else
617
+ add_error("Expected assignment operator after global variable")
618
+ end
619
+
620
+ # Value expression
621
+ value = parse_expression
622
+
623
+ {
624
+ type: :global_variable_assignment,
625
+ target: target,
626
+ operator: operator,
627
+ value: value,
628
+ line: line,
629
+ column: column
630
+ }
631
+ end
632
+
348
633
  def parse_range_command
349
634
  token = advance # Skip '%'
350
635
  line = token[:line]
@@ -427,80 +712,7 @@ module Vinter
427
712
  }
428
713
  end
429
714
 
430
- def old_parse_filter_command
431
- token = advance # Skip 'filter' or 'filt'
432
- line = token[:line]
433
- column = token[:column]
434
715
 
435
- # Check for bang (!)
436
- has_bang = false
437
- if current_token && current_token[:type] == :operator && current_token[:value] == '!'
438
- has_bang = true
439
- advance # Skip '!'
440
- end
441
-
442
- # Parse the pattern
443
- pattern = nil
444
- pattern_delimiter = nil
445
-
446
- if current_token && current_token[:type] == :operator && current_token[:value] == '/'
447
- # Handle /pattern/ form
448
- pattern_delimiter = '/'
449
- advance # Skip opening delimiter
450
-
451
- # Collect all tokens until closing delimiter
452
- pattern_parts = []
453
- while @position < @tokens.length &&
454
- !(current_token[:type] == :operator && current_token[:value] == pattern_delimiter)
455
- pattern_parts << current_token[:value]
456
- advance
457
- end
458
-
459
- pattern = pattern_parts.join('')
460
-
461
- # Skip closing delimiter
462
- if current_token && current_token[:type] == :operator && current_token[:value] == pattern_delimiter
463
- advance
464
- else
465
- @errors << {
466
- message: "Expected closing pattern delimiter: #{pattern_delimiter}",
467
- position: @position,
468
- line: current_token ? current_token[:line] : 0,
469
- column: current_token ? current_token[:column] : 0
470
- }
471
- end
472
- else
473
- # Handle direct pattern form (without delimiters)
474
- # Parse until we see what appears to be the command
475
- pattern_parts = []
476
- while @position < @tokens.length
477
- # Don't consume tokens that likely belong to the command part
478
- if current_token[:type] == :keyword ||
479
- (current_token[:type] == :identifier &&
480
- ['echo', 'let', 'execute', 'exec', 'autocmd', 'au', 'oldfiles', 'clist', 'command',
481
- 'files', 'highlight', 'jumps', 'list', 'llist', 'marks', 'registers', 'set'].include?(current_token[:value]))
482
- break
483
- end
484
-
485
- pattern_parts << current_token[:value]
486
- advance
487
- end
488
-
489
- pattern = pattern_parts.join('').strip
490
- end
491
-
492
- # Parse the command to be filtered
493
- command = parse_statement
494
-
495
- {
496
- type: :filter_command,
497
- pattern: pattern,
498
- has_bang: has_bang,
499
- command: command,
500
- line: line,
501
- column: column
502
- }
503
- end
504
716
 
505
717
  def parse_augroup_statement
506
718
  token = advance # Skip 'augroup'
@@ -513,18 +725,11 @@ module Vinter
513
725
  name = current_token[:value]
514
726
  advance
515
727
  else
516
- @errors << {
517
- message: "Expected augroup name",
518
- position: @position,
519
- line: current_token ? current_token[:line] : 0,
520
- column: current_token ? current_token[:column] : 0
521
- }
728
+ add_error("Expected augroup name")
522
729
  end
523
730
 
524
731
  # Check for augroup END
525
- is_end_marker = false
526
- if name && (name.upcase == "END" || name == "END")
527
- is_end_marker = true
732
+ if name && name.upcase == "END"
528
733
  return {
529
734
  type: :augroup_end,
530
735
  line: line,
@@ -540,8 +745,7 @@ module Vinter
540
745
  (current_token[:type] == :identifier && current_token[:value] == 'augroup')
541
746
  # Look ahead for END
542
747
  if peek_token &&
543
- ((peek_token[:type] == :identifier &&
544
- (peek_token[:value].upcase == 'END' || peek_token[:value] == 'END')) ||
748
+ ((peek_token[:type] == :identifier && peek_token[:value].upcase == 'END') ||
545
749
  (peek_token[:type] == :keyword && peek_token[:value].upcase == 'END'))
546
750
  advance # Skip 'augroup'
547
751
  advance # Skip 'END'
@@ -619,12 +823,7 @@ module Vinter
619
823
  column: dot_token[:column]
620
824
  }
621
825
  else
622
- @errors << {
623
- message: "Expected property name after '.'",
624
- position: @position,
625
- line: current_token ? current_token[:line] : 0,
626
- column: current_token ? current_token[:column] : 0
627
- }
826
+ add_error("Expected property name after '.'")
628
827
  end
629
828
  end
630
829
 
@@ -684,12 +883,7 @@ module Vinter
684
883
  }
685
884
  advance
686
885
  else
687
- @errors << {
688
- message: "Expected variable name in destructuring assignment",
689
- position: @position,
690
- line: current_token ? current_token[:line] : 0,
691
- column: current_token ? current_token[:column] : 0
692
- }
886
+ add_error("Expected variable name in destructuring assignment")
693
887
  # Try to recover by advancing to next comma or closing bracket
694
888
  while current_token && current_token[:type] != :comma && current_token[:type] != :bracket_close
695
889
  advance
@@ -706,12 +900,7 @@ module Vinter
706
900
  column: bracket_token[:column]
707
901
  }
708
902
  else
709
- @errors << {
710
- message: "Expected variable name after let",
711
- position: @position,
712
- line: current_token ? current_token[:line] : 0,
713
- column: current_token ? current_token[:column] : 0
714
- }
903
+ add_error("Expected variable name after let")
715
904
  end
716
905
  end
717
906
 
@@ -723,12 +912,7 @@ module Vinter
723
912
  operator = current_token[:value]
724
913
  advance
725
914
  else
726
- @errors << {
727
- message: "Expected assignment operator after variable in let statement",
728
- position: @position,
729
- line: current_token ? current_token[:line] : 0,
730
- column: current_token ? current_token[:column] : 0
731
- }
915
+ add_error("Expected assignment operator after variable in let statement")
732
916
  end
733
917
 
734
918
  # Parse the value expression
@@ -748,12 +932,7 @@ module Vinter
748
932
  func_name = current_token[:value]
749
933
  advance
750
934
  else
751
- @errors << {
752
- message: "Expected string with function name in function() call",
753
- position: @position,
754
- line: current_token ? current_token[:line] : 0,
755
- column: current_token ? current_token[:column] : 0
756
- }
935
+ add_error("Expected string with function name in function() call")
757
936
  end
758
937
 
759
938
  # Expect closing parenthesis
@@ -844,9 +1023,11 @@ module Vinter
844
1023
  event = nil
845
1024
  if current_token && current_token[:type] == :identifier
846
1025
  event = advance[:value]
1026
+ elsif current_token[:type] == :operator && current_token[:value] == "!"
1027
+ advance
847
1028
  else
848
1029
  @errors << {
849
- message: "Expected event name after 'autocmd'",
1030
+ message: "Expected event name after 'autocmd' | #{current_token[:type]}",
850
1031
  position: @position,
851
1032
  line: current_token ? current_token[:line] : 0,
852
1033
  column: current_token ? current_token[:column] : 0
@@ -972,32 +1153,14 @@ module Vinter
972
1153
  advance # Skip the pipe
973
1154
 
974
1155
  # Expect endif
975
- if current_token && current_token[:type] == :keyword && current_token[:value] == 'endif'
976
- advance # Skip 'endif'
977
- else
978
- @errors << {
979
- message: "Expected 'endif' after '|' in one-line if statement",
980
- position: @position,
981
- line: current_token ? current_token[:line] : 0,
982
- column: current_token ? current_token[:column] : 0
983
- }
1156
+ unless expect_keyword('endif')
1157
+ add_error("Expected 'endif' after '|' in one-line if statement")
984
1158
  end
985
1159
  end
986
1160
  else
987
- # This is a regular multi-line if statement
988
- # Continue with your existing logic for parsing normal if statements
989
-
990
- # Parse statements until we hit 'else', 'elseif', or 'endif'
991
- while @position < @tokens.length
992
- # Check for the tokens that would terminate this block
993
- if current_token && current_token[:type] == :keyword &&
994
- ['else', 'elseif', 'endif'].include?(current_token[:value])
995
- break
996
- end
997
-
998
- stmt = parse_statement
999
- then_branch << stmt if stmt
1000
- end
1161
+ # Parse multi-line if statement
1162
+ # Parse statements until 'else', 'elseif', or 'endif'
1163
+ then_branch = parse_body_until('else', 'elseif', 'endif')
1001
1164
 
1002
1165
  # Check for else/elseif
1003
1166
  if current_token && current_token[:type] == :keyword
@@ -1005,14 +1168,7 @@ module Vinter
1005
1168
  advance # Skip 'else'
1006
1169
 
1007
1170
  # Parse statements until 'endif'
1008
- while @position < @tokens.length
1009
- if current_token[:type] == :keyword && current_token[:value] == 'endif'
1010
- break
1011
- end
1012
-
1013
- stmt = parse_statement
1014
- else_branch << stmt if stmt
1015
- end
1171
+ else_branch = parse_body_until('endif')
1016
1172
  elsif current_token[:value] == 'elseif'
1017
1173
  elseif_stmt = parse_if_statement
1018
1174
  else_branch << elseif_stmt if elseif_stmt
@@ -1028,228 +1184,63 @@ module Vinter
1028
1184
  end
1029
1185
  end
1030
1186
 
1031
- # Expect endif
1032
- if current_token && current_token[:type] == :keyword && current_token[:value] == 'endif'
1033
- advance # Skip 'endif'
1034
- else
1035
- # Don't add an error if we've already reached the end of the file
1036
- if @position < @tokens.length
1037
- @errors << {
1038
- message: "Expected 'endif' to close if statement",
1039
- position: @position,
1040
- line: current_token ? current_token[:line] : 0,
1041
- column: current_token ? current_token[:column] : 0
1042
- }
1043
- end
1044
- end
1045
- end
1046
-
1047
- {
1048
- type: :if_statement,
1049
- condition: condition,
1050
- then_branch: then_branch,
1051
- else_branch: else_branch,
1052
- one_line: one_line_if,
1053
- line: line,
1054
- column: column
1055
- }
1056
- end
1057
-
1058
- def parse_while_statement
1059
- token = advance # Skip 'while'
1060
- line = token[:line]
1061
- column = token[:column]
1062
- condition = parse_expression
1063
-
1064
- body = []
1065
-
1066
- # Parse statements until we hit 'endwhile'
1067
- while @position < @tokens.length
1068
- if current_token[:type] == :keyword && current_token[:value] == 'endwhile'
1069
- break
1070
- end
1071
-
1072
- stmt = parse_statement
1073
- body << stmt if stmt
1074
- end
1075
-
1076
- # Expect endwhile
1077
- expect(:keyword) # This should be 'endwhile'
1078
-
1079
- {
1080
- type: :while_statement,
1081
- condition: condition,
1082
- body: body,
1083
- line: line,
1084
- column: column
1085
- }
1086
- end
1087
-
1088
- def parse_for_statement
1089
- token = advance # Skip 'for'
1090
- line = token[:line]
1091
- column = token[:column]
1092
-
1093
- # Two main patterns:
1094
- # 1. for [key, val] in dict - destructuring with bracket_open
1095
- # 2. for var in list - simple variable with identifier
1096
-
1097
- if current_token && current_token[:type] == :bracket_open
1098
- # Handle destructuring assignment: for [key, val] in dict
1099
- advance # Skip '['
1100
-
1101
- loop_vars = []
1102
-
1103
- loop do
1104
- if current_token && (current_token[:type] == :identifier ||
1105
- current_token[:type] == :local_variable ||
1106
- current_token[:type] == :global_variable ||
1107
- current_token[:type] == :script_local)
1108
- loop_vars << advance
1109
- else
1110
- @errors << {
1111
- message: "Expected identifier in for loop variables",
1112
- position: @position,
1113
- line: current_token ? current_token[:line] : 0,
1114
- column: current_token ? current_token[:column] : 0
1115
- }
1116
- break
1117
- end
1118
-
1119
- if current_token && current_token[:type] == :comma
1120
- advance # Skip ','
1121
- else
1122
- break
1123
- end
1124
- end
1125
-
1126
- expect(:bracket_close) # Skip ']'
1127
-
1128
- if !current_token || (current_token[:type] != :identifier || current_token[:value] != 'in')
1129
- @errors << {
1130
- message: "Expected 'in' after for loop variables",
1131
- position: @position,
1132
- line: current_token ? current_token[:line] : 0,
1133
- column: current_token ? current_token[:column] : 0
1134
- }
1135
- else
1136
- advance # Skip 'in'
1137
- end
1138
-
1139
- iterable = parse_expression
1140
-
1141
- # Parse the body until 'endfor'
1142
- body = []
1143
- while @position < @tokens.length
1144
- if current_token && current_token[:type] == :keyword && current_token[:value] == 'endfor'
1145
- break
1146
- end
1147
-
1148
- stmt = parse_statement
1149
- body << stmt if stmt
1150
- end
1151
-
1152
- # Expect endfor
1153
- if current_token && current_token[:type] == :keyword && current_token[:value] == 'endfor'
1154
- advance # Skip 'endfor'
1155
- else
1156
- # Only add an error if we haven't reached the end of the file
1157
- if @position < @tokens.length
1158
- @errors << {
1159
- message: "Expected 'endfor' to close for statement",
1160
- position: @position,
1161
- line: current_token ? current_token[:line] : 0,
1162
- column: current_token ? current_token[:column] : 0
1163
- }
1164
- end
1165
- end
1166
-
1167
- return {
1168
- type: :for_statement,
1169
- loop_vars: loop_vars,
1170
- iterable: iterable,
1171
- body: body,
1172
- line: line,
1173
- column: column
1174
- }
1175
- elsif current_token && current_token[:type] == :paren_open
1176
- # Handle multiple variables in parentheses: for (var1, var2) in list
1177
- advance # Skip '('
1178
-
1179
- loop_vars = []
1180
-
1181
- loop do
1182
- if current_token && (current_token[:type] == :identifier ||
1183
- current_token[:type] == :local_variable ||
1184
- current_token[:type] == :global_variable ||
1185
- current_token[:type] == :script_local)
1186
- loop_vars << advance
1187
- else
1188
- @errors << {
1189
- message: "Expected identifier in for loop variables",
1190
- position: @position,
1191
- line: current_token ? current_token[:line] : 0,
1192
- column: current_token ? current_token[:column] : 0
1193
- }
1194
- break
1195
- end
1196
-
1197
- if current_token && current_token[:type] == :comma
1198
- advance # Skip ','
1199
- else
1200
- break
1201
- end
1202
- end
1203
-
1204
- expect(:paren_close) # Skip ')'
1187
+ # Expect endif
1188
+ expect_end_keyword('endif')
1189
+ end
1205
1190
 
1206
- if !current_token || (current_token[:type] != :identifier || current_token[:value] != 'in')
1207
- @errors << {
1208
- message: "Expected 'in' after for loop variables",
1209
- position: @position,
1210
- line: current_token ? current_token[:line] : 0,
1211
- column: current_token ? current_token[:column] : 0
1212
- }
1213
- else
1214
- advance # Skip 'in'
1215
- end
1191
+ {
1192
+ type: :if_statement,
1193
+ condition: condition,
1194
+ then_branch: then_branch,
1195
+ else_branch: else_branch,
1196
+ one_line: one_line_if,
1197
+ line: line,
1198
+ column: column
1199
+ }
1200
+ end
1216
1201
 
1217
- iterable = parse_expression
1202
+ def parse_while_statement
1203
+ token = advance # Skip 'while'
1204
+ line = token[:line]
1205
+ column = token[:column]
1206
+ condition = parse_expression
1218
1207
 
1219
- # Parse the body until 'endfor'
1220
- body = []
1221
- while @position < @tokens.length
1222
- if current_token && current_token[:type] == :keyword && current_token[:value] == 'endfor'
1223
- break
1224
- end
1208
+ # Parse statements until 'endwhile'
1209
+ body = parse_body_until('endwhile')
1225
1210
 
1226
- stmt = parse_statement
1227
- body << stmt if stmt
1228
- end
1211
+ # Expect endwhile
1212
+ expect_end_keyword('endwhile')
1229
1213
 
1230
- # Expect endfor
1231
- if current_token && current_token[:type] == :keyword && current_token[:value] == 'endfor'
1232
- advance # Skip 'endfor'
1233
- else
1234
- # Only add an error if we haven't reached the end of the file
1235
- if @position < @tokens.length
1236
- @errors << {
1237
- message: "Expected 'endfor' to close for statement",
1238
- position: @position,
1239
- line: current_token ? current_token[:line] : 0,
1240
- column: current_token ? current_token[:column] : 0
1241
- }
1242
- end
1243
- end
1214
+ {
1215
+ type: :while_statement,
1216
+ condition: condition,
1217
+ body: body,
1218
+ line: line,
1219
+ column: column
1220
+ }
1221
+ end
1244
1222
 
1245
- return {
1246
- type: :for_statement,
1247
- loop_vars: loop_vars,
1248
- iterable: iterable,
1249
- body: body,
1250
- line: line,
1251
- column: column
1252
- }
1223
+ def parse_for_statement
1224
+ token = advance # Skip 'for'
1225
+ line = token[:line]
1226
+ column = token[:column]
1227
+
1228
+ # Determine the type of for loop and parse variables accordingly
1229
+ loop_var = nil
1230
+ loop_vars = nil
1231
+
1232
+ if current_token && current_token[:type] == :bracket_open
1233
+ # Handle destructuring with brackets: for [key, val] in dict
1234
+ advance # Skip '['
1235
+ loop_vars = parse_loop_variables
1236
+ expect(:bracket_close)
1237
+ expect_in_keyword
1238
+ elsif current_token && current_token[:type] == :paren_open
1239
+ # Handle multiple variables with parentheses: for (var1, var2) in list
1240
+ advance # Skip '('
1241
+ loop_vars = parse_loop_variables
1242
+ expect(:paren_close)
1243
+ expect_in_keyword
1253
1244
  else
1254
1245
  # Handle single variable: for var in list
1255
1246
  if current_token && (current_token[:type] == :identifier ||
@@ -1258,63 +1249,36 @@ module Vinter
1258
1249
  current_token[:type] == :script_local)
1259
1250
  loop_var = advance
1260
1251
  else
1261
- @errors << {
1262
- message: "Expected identifier as for loop variable",
1263
- position: @position,
1264
- line: current_token ? current_token[:line] : 0,
1265
- column: current_token ? current_token[:column] : 0
1266
- }
1267
- loop_var = nil
1268
- end
1269
-
1270
- if !current_token || (current_token[:type] != :identifier || current_token[:value] != 'in')
1271
- @errors << {
1272
- message: "Expected 'in' after for loop variable",
1273
- position: @position,
1274
- line: current_token ? current_token[:line] : 0,
1275
- column: current_token ? current_token[:column] : 0
1276
- }
1277
- else
1278
- advance # Skip 'in'
1252
+ add_error("Expected identifier as for loop variable")
1279
1253
  end
1254
+ expect_in_keyword
1255
+ end
1280
1256
 
1281
- iterable = parse_expression
1257
+ # Parse the iterable expression
1258
+ iterable = parse_expression
1282
1259
 
1283
- # Parse the body until 'endfor'
1284
- body = []
1285
- while @position < @tokens.length
1286
- if current_token && current_token[:type] == :keyword && current_token[:value] == 'endfor'
1287
- break
1288
- end
1260
+ # Parse the body until 'endfor'
1261
+ body = parse_body_until('endfor')
1289
1262
 
1290
- stmt = parse_statement
1291
- body << stmt if stmt
1292
- end
1263
+ # Expect endfor
1264
+ expect_end_keyword('endfor')
1293
1265
 
1294
- # Expect endfor
1295
- if current_token && current_token[:type] == :keyword && current_token[:value] == 'endfor'
1296
- advance # Skip 'endfor'
1297
- else
1298
- # Only add an error if we haven't reached the end of the file
1299
- if @position < @tokens.length
1300
- @errors << {
1301
- message: "Expected 'endfor' to close for statement",
1302
- position: @position,
1303
- line: current_token ? current_token[:line] : 0,
1304
- column: current_token ? current_token[:column] : 0
1305
- }
1306
- end
1307
- end
1266
+ # Build result hash based on whether we have single or multiple vars
1267
+ result = {
1268
+ type: :for_statement,
1269
+ iterable: iterable,
1270
+ body: body,
1271
+ line: line,
1272
+ column: column
1273
+ }
1308
1274
 
1309
- return {
1310
- type: :for_statement,
1311
- loop_var: loop_var,
1312
- iterable: iterable,
1313
- body: body,
1314
- line: line,
1315
- column: column
1316
- }
1275
+ if loop_vars
1276
+ result[:loop_vars] = loop_vars
1277
+ else
1278
+ result[:loop_var] = loop_var
1317
1279
  end
1280
+
1281
+ result
1318
1282
  end
1319
1283
 
1320
1284
  def parse_def_function
@@ -1331,24 +1295,16 @@ module Vinter
1331
1295
 
1332
1296
  # Parse optional return type
1333
1297
  return_type = nil
1334
- if current_token && current_token[:type] == :colon
1298
+ if current_token && current_token[:type] == :colon && current_token[:line] == line
1335
1299
  advance # Skip ':'
1336
1300
  return_type = parse_type
1337
1301
  end
1338
1302
 
1339
- # Parse function body
1340
- body = []
1341
- while @position < @tokens.length
1342
- if current_token[:type] == :keyword && current_token[:value] == 'enddef'
1343
- break
1344
- end
1345
-
1346
- stmt = parse_statement
1347
- body << stmt if stmt
1348
- end
1303
+ # Parse function body until 'enddef'
1304
+ body = parse_body_until('enddef')
1349
1305
 
1350
1306
  # Expect enddef
1351
- expect(:keyword) # This should be 'enddef'
1307
+ expect_end_keyword('enddef')
1352
1308
 
1353
1309
  {
1354
1310
  type: :def_function,
@@ -1391,28 +1347,12 @@ module Vinter
1391
1347
 
1392
1348
  # After varargs, we expect closing paren
1393
1349
  if current_token && current_token[:type] != :paren_close
1394
- @errors << {
1395
- message: "Expected closing parenthesis after varargs",
1396
- position: @position,
1397
- line: current_token[:line],
1398
- column: current_token[:column]
1399
- }
1350
+ add_error("Expected closing parenthesis after varargs", current_token)
1400
1351
  end
1401
1352
 
1402
1353
  break
1403
1354
  end
1404
1355
 
1405
- # Get parameter name
1406
- if !current_token || current_token[:type] != :identifier
1407
- @errors << {
1408
- message: "Expected parameter name",
1409
- position: @position,
1410
- line: current_token ? current_token[:line] : 0,
1411
- column: current_token ? current_token[:column] : 0
1412
- }
1413
- break
1414
- end
1415
-
1416
1356
  param_name = advance
1417
1357
 
1418
1358
  # Check for type annotation
@@ -1444,12 +1384,7 @@ module Vinter
1444
1384
  advance
1445
1385
  # If we don't have a comma, we should have a closing paren
1446
1386
  elsif current_token && current_token[:type] != :paren_close
1447
- @errors << {
1448
- message: "Expected comma or closing parenthesis after parameter",
1449
- position: @position,
1450
- line: current_token[:line],
1451
- column: current_token[:column]
1452
- }
1387
+ add_error("Expected comma or closing parenthesis after parameter", current_token)
1453
1388
  break
1454
1389
  end
1455
1390
  end
@@ -1478,12 +1413,7 @@ module Vinter
1478
1413
 
1479
1414
  return type_name[:value]
1480
1415
  else
1481
- @errors << {
1482
- message: "Expected type identifier",
1483
- position: @position,
1484
- line: current_token ? current_token[:line] : 0,
1485
- column: current_token ? current_token[:column] : 0
1486
- }
1416
+ add_error("Expected type identifier")
1487
1417
  advance
1488
1418
  return "unknown"
1489
1419
  end
@@ -1496,12 +1426,7 @@ module Vinter
1496
1426
  column = var_type_token[:column]
1497
1427
 
1498
1428
  if !current_token || current_token[:type] != :identifier
1499
- @errors << {
1500
- message: "Expected variable name",
1501
- position: @position,
1502
- line: current_token ? current_token[:line] : 0,
1503
- column: current_token ? current_token[:column] : 0
1504
- }
1429
+ add_error("Expected variable name")
1505
1430
  return nil
1506
1431
  end
1507
1432
 
@@ -1517,7 +1442,7 @@ module Vinter
1517
1442
 
1518
1443
  # Parse initializer if present
1519
1444
  initializer = nil
1520
- if current_token && current_token[:type] == :operator && current_token[:value] == '='
1445
+ if current_token && (current_token[:type] == :operator && current_token[:value] == '=')
1521
1446
  advance # Skip '='
1522
1447
  initializer = parse_expression
1523
1448
  end
@@ -1810,7 +1735,7 @@ module Vinter
1810
1735
  # Special handling for map-related keywords when they appear in expressions
1811
1736
  if ['map', 'nmap', 'imap', 'vmap', 'xmap', 'noremap', 'nnoremap', 'inoremap', 'vnoremap', 'xnoremap', 'cnoremap', 'cmap'].include?(token[:value])
1812
1737
  if peek_token[:type] == :paren_open
1813
- parse_builtin_function_call(token[:value], line, column)
1738
+ parse_builtin_function_call
1814
1739
  else
1815
1740
  # Treat map commands as identifiers when inside expressions
1816
1741
  advance
@@ -1823,7 +1748,7 @@ module Vinter
1823
1748
  end
1824
1749
  elsif token[:value] == 'type' && current_token && (current_token[:type] == :paren_open || peek_token && peek_token[:type] == :paren_open)
1825
1750
  # This is the type() function call
1826
- return parse_builtin_function_call(token[:value], line, column)
1751
+ return parse_builtin_function_call
1827
1752
  elsif token[:value] == 'function'
1828
1753
  # Check if this is a function() call or just 'function' as property name
1829
1754
  if peek_token && peek_token[:type] == :paren_open
@@ -1909,6 +1834,8 @@ module Vinter
1909
1834
  line: line,
1910
1835
  column: column
1911
1836
  }
1837
+ elsif token[:value] == 'command'
1838
+ advance
1912
1839
  else
1913
1840
  @errors << {
1914
1841
  message: "Unexpected keyword in expression: #{token[:value]}",
@@ -2036,7 +1963,7 @@ module Vinter
2036
1963
  when :identifier
2037
1964
  # Special handling for Vim built-in functions
2038
1965
  if ['has', 'exists', 'empty', 'get', 'type', 'map', 'copy'].include?(token[:value])
2039
- expr = parse_builtin_function_call(token[:value], line, column)
1966
+ expr = parse_builtin_function_call
2040
1967
  else
2041
1968
  advance
2042
1969
 
@@ -2064,6 +1991,18 @@ module Vinter
2064
1991
  end
2065
1992
  end
2066
1993
  end
1994
+ when :vimfuncs
1995
+ advance
1996
+ if current_token[:type] == :paren_open
1997
+ expr = parse_function_call(token[:value], line, column)
1998
+ else
1999
+ expr = {
2000
+ type: :vimfuncs,
2001
+ name: token[:value],
2002
+ line: line,
2003
+ column: column
2004
+ }
2005
+ end
2067
2006
  when :namespace_prefix
2068
2007
  advance
2069
2008
  expr = {
@@ -2099,6 +2038,13 @@ module Vinter
2099
2038
  when :line_continuation
2100
2039
  advance
2101
2040
  expr = parse_expression
2041
+ when :interpolated_string
2042
+ token = advance
2043
+ value = token[:value]
2044
+ expr = {
2045
+ type: :interpolated_string,
2046
+ value: token[:value],
2047
+ }
2102
2048
  else
2103
2049
  @errors << {
2104
2050
  message: "Unexpected token in expression: #{token[:type]}",
@@ -2117,7 +2063,7 @@ module Vinter
2117
2063
  # Look ahead to determine if this is property access or concatenation
2118
2064
  next_token = peek_token
2119
2065
  is_property_access = next_token && (next_token[:type] == :identifier || next_token[:type] == :keyword)
2120
-
2066
+
2121
2067
  # Check if this is a property access (only when right side is identifier/keyword)
2122
2068
  if is_property_access && (expr[:type] == :identifier || expr[:type] == :global_variable ||
2123
2069
  expr[:type] == :script_local || expr[:type] == :namespace_prefix ||
@@ -2155,6 +2101,7 @@ module Vinter
2155
2101
  # Check for method call with arrow ->
2156
2102
  elsif current_token[:type] == :operator && current_token[:value] == '->'
2157
2103
  arrow_token = advance # Skip '->'
2104
+ advance if current_token[:type] == :line_continuation
2158
2105
 
2159
2106
  # Next token should be an identifier (method name)
2160
2107
  if !current_token || current_token[:type] != :identifier
@@ -2219,6 +2166,12 @@ module Vinter
2219
2166
  else
2220
2167
  end_index = parse_expression
2221
2168
  end
2169
+ # handles array assignment [a,b] = split(thing)
2170
+ elsif current_token[:type] == :comma
2171
+ while current_token[:type] == :comma
2172
+ advance
2173
+ parse_expression
2174
+ end
2222
2175
  end
2223
2176
 
2224
2177
  expect(:bracket_close) # Skip ']'
@@ -2281,76 +2234,19 @@ module Vinter
2281
2234
  return expr
2282
2235
  end
2283
2236
 
2284
- def parse_builtin_function_call(name, line, column)
2285
- # Skip the function name (already consumed)
2286
- advance if current_token[:value] == name
2287
-
2288
- # Check if there's an opening parenthesis
2289
- if current_token && current_token[:type] == :paren_open
2290
- advance # Skip '('
2291
-
2292
- # Parse arguments
2293
- args = []
2294
-
2295
- # Functions that take string expressions as code
2296
- special_functions = ['map', 'reduce', 'sort', 'call', 'eval', 'execute', 'exec']
2297
- is_special_function = special_functions.include?(name)
2298
-
2299
- # Parse until closing parenthesis
2300
- while @position < @tokens.length && current_token && current_token[:type] != :paren_close
2301
- # Skip whitespace or comments
2302
- if current_token[:type] == :whitespace || current_token[:type] == :comment
2303
- advance
2304
- next
2305
- end
2306
-
2307
- # Special handling for string arguments that contain code
2308
- if is_special_function &&
2309
- current_token && current_token[:type] == :string
2310
- string_token = parse_expression
2311
- args << {
2312
- type: :literal,
2313
- value: string_token[:value],
2314
- token_type: :string,
2315
- line: string_token[:line],
2316
- column: string_token[:column]
2317
- }
2318
- else
2319
- arg = parse_expression
2320
- args << arg if arg
2321
- end
2322
-
2323
- if current_token && current_token[:type] == :comma
2324
- advance
2325
- elsif current_token && current_token[:type] != :paren_close
2326
- @errors << {
2327
- message: "Expected comma or closing parenthesis in #{name} function",
2328
- position: @position,
2329
- line: current_token[:line],
2330
- column: current_token[:column]
2331
- }
2332
- break
2333
- end
2334
- end
2237
+ def parse_builtin_function_call
2238
+ token = advance # Consume the `builtin_funcs` token
2239
+ line = token[:line]
2240
+ column = token[:column]
2241
+ name = token[:value]
2335
2242
 
2336
- # Check for closing parenthesis
2337
- if current_token && current_token[:type] == :paren_close
2338
- advance # Skip ')'
2339
- else
2340
- @errors << {
2341
- message: "Expected ')' to close #{name} function call",
2342
- position: @position,
2343
- line: current_token ? current_token[:line] : 0,
2344
- column: current_token ? current_token[:column] : 0
2345
- }
2346
- end
2347
- else
2348
- # Handle legacy Vim script where parentheses might be omitted
2349
- # Just parse one expression as the argument
2350
- args = [parse_expression]
2243
+ # Collect arguments (if any) until the end of the line
2244
+ args = []
2245
+ while current_token && current_token[:type] != :comment && current_token[:value] != "\n"
2246
+ args << current_token[:value]
2247
+ advance
2351
2248
  end
2352
2249
 
2353
- # Return function call node
2354
2250
  {
2355
2251
  type: :builtin_function_call,
2356
2252
  name: name,
@@ -2754,7 +2650,7 @@ module Vinter
2754
2650
  current_token[:type] == :whitespace)
2755
2651
  advance
2756
2652
  end
2757
-
2653
+
2758
2654
  # After skipping continuations, we should find the closing brace
2759
2655
  if current_token && current_token[:type] == :brace_close
2760
2656
  # This is fine - last entry without trailing comma
@@ -2909,42 +2805,30 @@ module Vinter
2909
2805
 
2910
2806
  args = []
2911
2807
 
2912
- # Special handling for Vim functions that take code strings as arguments
2913
- special_functions = ['map', 'reduce', 'sort', 'call', 'eval', 'execute', 'exec']
2914
- is_special_function = special_functions.include?(name)
2915
-
2916
2808
  # Parse arguments until we find a closing parenthesis
2917
2809
  while @position < @tokens.length && current_token && current_token[:type] != :paren_close
2918
- # Skip comments inside parameter lists
2919
- if current_token && current_token[:type] == :comment
2810
+ # Skip line continuations
2811
+ if current_token[:type] == :line_continuation
2920
2812
  advance
2921
2813
  next
2922
2814
  end
2923
- if is_special_function && current_token && current_token[:type] == :string
2924
- # For functions like map(), filter(), directly add the string as an argument
2925
- string_token = parse_string
2926
- args << {
2927
- type: :literal,
2928
- value: string_token[:value],
2929
- token_type: :string,
2930
- line: string_token[:line],
2931
- column: string_token[:column]
2932
- }
2933
- else
2934
- # Parse the argument
2935
- arg = parse_expression
2936
- args << arg if arg
2937
- end
2938
2815
 
2939
- # Break if we hit the closing paren
2816
+ # Parse the argument
2817
+ arg = parse_expression
2818
+ args << arg if arg
2819
+
2820
+ # Break if we hit the closing parenthesis
2940
2821
  if current_token && current_token[:type] == :paren_close
2941
2822
  break
2942
2823
  end
2824
+
2943
2825
  # If we have a comma, advance past it and continue
2944
2826
  if current_token && current_token[:type] == :comma
2945
2827
  advance
2828
+ elsif current_token[:type] == :line_continuation
2829
+ advance
2946
2830
  # If we don't have a comma and we're not at the end, it's an error
2947
- elsif current_token && current_token[:type] != :paren_close && current_token[:type] != :comment
2831
+ elsif current_token && current_token[:type] != :paren_close
2948
2832
  @errors << {
2949
2833
  message: "Expected comma or closing parenthesis after argument",
2950
2834
  position: @position,
@@ -3086,6 +2970,13 @@ module Vinter
3086
2970
  column: column
3087
2971
  }
3088
2972
  end
2973
+
2974
+ def parse_scriptencoding
2975
+ token = advance
2976
+ advance
2977
+ { type: :scriptencoding, encoding: current_token[:value] }
2978
+ end
2979
+
3089
2980
  def parse_export_statement
3090
2981
  token = advance # Skip 'export'
3091
2982
  line = token[:line]
@@ -3188,7 +3079,7 @@ module Vinter
3188
3079
  advance # Skip the '='
3189
3080
 
3190
3081
  # Get the value (number or identifier)
3191
- if current_token && (current_token[:type] == :number || current_token[:type] == :identifier)
3082
+ if current_token && (current_token[:type] == :number || current_token[:type] == :identifier) || current_token[:type] == :operator
3192
3083
  attribute += "=#{current_token[:value]}"
3193
3084
  attributes << attribute
3194
3085
  advance
@@ -3198,6 +3089,7 @@ module Vinter
3198
3089
 
3199
3090
  # Parse the command name
3200
3091
  command_name = nil
3092
+ advance if current_token[:type] == :line_continuation
3201
3093
  if current_token && current_token[:type] == :identifier
3202
3094
  command_name = current_token[:value]
3203
3095
  advance
@@ -3296,33 +3188,11 @@ module Vinter
3296
3188
  end
3297
3189
  end
3298
3190
 
3299
- # Parse function body
3300
- body = []
3301
- while @position < @tokens.length
3302
- if current_token && current_token[:type] == :keyword &&
3303
- ['endfunction', 'endfunc'].include?(current_token[:value])
3304
- break
3305
- end
3306
-
3307
- stmt = parse_statement
3308
- body << stmt if stmt
3309
- end
3191
+ # Parse function body until 'endfunction' or 'endfunc'
3192
+ body = parse_body_until('endfunction', 'endfunc')
3310
3193
 
3311
3194
  # Expect endfunction/endfunc
3312
- if current_token && current_token[:type] == :keyword &&
3313
- ['endfunction', 'endfunc'].include?(current_token[:value])
3314
- advance # Skip 'endfunction' or 'endfunc'
3315
- else
3316
- # Only add an error if we haven't reached the end of the file
3317
- if @position < @tokens.length
3318
- @errors << {
3319
- message: "Expected 'endfunction' or 'endfunc'",
3320
- position: @position,
3321
- line: current_token ? current_token[:line] : 0,
3322
- column: current_token ? current_token[:column] : 0
3323
- }
3324
- end
3325
- end
3195
+ expect_end_keyword('endfunction', 'endfunc')
3326
3196
 
3327
3197
  function_name = name ? name[:value] : nil
3328
3198
  if function_scope
@@ -3508,22 +3378,12 @@ module Vinter
3508
3378
  line = token[:line]
3509
3379
  column = token[:column]
3510
3380
 
3511
- # Parse the try body
3512
- body = []
3381
+ # Parse the try body until catch/finally/endtry
3382
+ body = parse_body_until('catch', 'finally', 'endtry')
3383
+
3513
3384
  catch_clauses = []
3514
3385
  finally_clause = nil
3515
3386
 
3516
- # Parse statements in the try block
3517
- while @position < @tokens.length
3518
- if current_token && current_token[:type] == :keyword &&
3519
- ['catch', 'finally', 'endtry'].include?(current_token[:value])
3520
- break
3521
- end
3522
-
3523
- stmt = parse_statement
3524
- body << stmt if stmt
3525
- end
3526
-
3527
3387
  # Parse catch clauses
3528
3388
  while @position < @tokens.length &&
3529
3389
  current_token && current_token[:type] == :keyword &&
@@ -3549,17 +3409,8 @@ module Vinter
3549
3409
  advance
3550
3410
  end
3551
3411
 
3552
- # Parse the catch body
3553
- catch_body = []
3554
- while @position < @tokens.length
3555
- if current_token && current_token[:type] == :keyword &&
3556
- ['catch', 'finally', 'endtry'].include?(current_token[:value])
3557
- break
3558
- end
3559
-
3560
- stmt = parse_statement
3561
- catch_body << stmt if stmt
3562
- end
3412
+ # Parse the catch body until next catch/finally/endtry
3413
+ catch_body = parse_body_until('catch', 'finally', 'endtry')
3563
3414
 
3564
3415
  catch_clauses << {
3565
3416
  type: :catch_clause,
@@ -3578,16 +3429,8 @@ module Vinter
3578
3429
  finally_line = finally_token[:line]
3579
3430
  finally_column = finally_token[:column]
3580
3431
 
3581
- # Parse the finally body
3582
- finally_body = []
3583
- while @position < @tokens.length
3584
- if current_token && current_token[:type] == :keyword && current_token[:value] == 'endtry'
3585
- break
3586
- end
3587
-
3588
- stmt = parse_statement
3589
- finally_body << stmt if stmt
3590
- end
3432
+ # Parse the finally body until 'endtry'
3433
+ finally_body = parse_body_until('endtry')
3591
3434
 
3592
3435
  finally_clause = {
3593
3436
  type: :finally_clause,
@@ -3598,19 +3441,7 @@ module Vinter
3598
3441
  end
3599
3442
 
3600
3443
  # Expect endtry
3601
- if current_token && current_token[:type] == :keyword && current_token[:value] == 'endtry'
3602
- advance # Skip 'endtry'
3603
- else
3604
- # Only add an error if we haven't reached the end of the file
3605
- if @position < @tokens.length
3606
- @errors << {
3607
- message: "Expected 'endtry' to close try statement",
3608
- position: @position,
3609
- line: current_token ? current_token[:line] : 0,
3610
- column: current_token ? current_token[:column] : 0
3611
- }
3612
- end
3613
- end
3444
+ expect_end_keyword('endtry')
3614
3445
 
3615
3446
  return {
3616
3447
  type: :try_statement,
@@ -3881,14 +3712,14 @@ module Vinter
3881
3712
  def parse_brace_sequence_value
3882
3713
  # Handle special brace sequences like {{{,}}} for foldmarker
3883
3714
  value_parts = []
3884
-
3885
- while current_token && (current_token[:type] == :brace_open ||
3886
- current_token[:type] == :brace_close ||
3715
+
3716
+ while current_token && (current_token[:type] == :brace_open ||
3717
+ current_token[:type] == :brace_close ||
3887
3718
  current_token[:type] == :comma)
3888
3719
  value_parts << current_token[:value]
3889
3720
  advance
3890
3721
  end
3891
-
3722
+
3892
3723
  return {
3893
3724
  type: :brace_sequence,
3894
3725
  value: value_parts.join(''),
@@ -3900,7 +3731,7 @@ module Vinter
3900
3731
  def parse_comma_separated_value
3901
3732
  # Handle comma-separated option values like menu,menuone,noinsert,noselect
3902
3733
  values = []
3903
-
3734
+
3904
3735
  # Parse first value
3905
3736
  if current_token && current_token[:type] == :identifier
3906
3737
  values << current_token[:value]
@@ -3908,7 +3739,7 @@ module Vinter
3908
3739
  else
3909
3740
  return parse_expression # Fall back to regular expression parsing
3910
3741
  end
3911
-
3742
+
3912
3743
  # Parse additional comma-separated values
3913
3744
  while current_token && current_token[:type] == :comma
3914
3745
  advance # Skip comma
@@ -3919,7 +3750,7 @@ module Vinter
3919
3750
  break
3920
3751
  end
3921
3752
  end
3922
-
3753
+
3923
3754
  return {
3924
3755
  type: :comma_separated_value,
3925
3756
  values: values,
@@ -3971,7 +3802,7 @@ module Vinter
3971
3802
  value = nil
3972
3803
  if current_token && current_token[:type] == :operator && current_token[:value] == '='
3973
3804
  advance # Skip '='
3974
-
3805
+
3975
3806
  # Special handling for foldmarker and similar options that use brace notation
3976
3807
  if option_name == 'foldmarker' && current_token && current_token[:type] == :brace_open
3977
3808
  # Parse as special brace sequence value (e.g., {{{,}}})
@@ -4098,8 +3929,8 @@ module Vinter
4098
3929
  advance
4099
3930
 
4100
3931
  # Parse attribute value (can be identifier, number, or hex color)
4101
- if current_token && (current_token[:type] == :identifier ||
4102
- current_token[:type] == :number ||
3932
+ if current_token && (current_token[:type] == :identifier ||
3933
+ current_token[:type] == :number ||
4103
3934
  current_token[:type] == :hex_color)
4104
3935
  attributes[attr_name] = current_token[:value]
4105
3936
  advance