vinter 0.3.0 → 0.4.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,10 +1,11 @@
1
1
  module Vinter
2
2
  class Parser
3
- def initialize(tokens)
3
+ def initialize(tokens, source_text = nil)
4
4
  @tokens = tokens
5
5
  @position = 0
6
6
  @errors = []
7
7
  @warnings = []
8
+ @source_text = source_text
8
9
  end
9
10
 
10
11
  def parse
@@ -42,8 +43,30 @@ module Vinter
42
43
  found = current_token ? current_token[:type] : "end of input"
43
44
  line = current_token ? current_token[:line] : 0
44
45
  column = current_token ? current_token[:column] : 0
46
+
47
+ # Get the full line content from the input if available
48
+ line_content = nil
49
+ if line > 0 && @tokens.length > 0
50
+ # Find tokens on the same line
51
+ same_line_tokens = @tokens.select { |t| t[:line] == line }
52
+ if !same_line_tokens.empty?
53
+ # Create a representation of the line with a marker at the error position
54
+ line_representation = same_line_tokens.map { |t| t[:value] }.join('')
55
+ line_content = "Line content: #{line_representation}"
56
+ end
57
+ end
58
+
45
59
  error = "Expected #{expected} but found #{found}"
46
- @errors << { message: error, position: @position, line: line, column: column }
60
+ error += ", at line #{line}, column #{column}"
61
+ error += "\n#{line_content}" if line_content
62
+
63
+ @errors << {
64
+ message: error,
65
+ position: @position,
66
+ line: line,
67
+ column: column,
68
+ line_content: line_content
69
+ }
47
70
  nil
48
71
  end
49
72
  end
@@ -65,15 +88,153 @@ module Vinter
65
88
  { type: :program, body: statements }
66
89
  end
67
90
 
91
+ def parse_mapping_statement
92
+ token = current_token
93
+ advance # Skip the mapping command
94
+ line = token[:line]
95
+ column = token[:column]
96
+ mapping_command = token[:value]
97
+
98
+ # Parse options (like <silent>, <buffer>, etc.)
99
+ options = []
100
+ while current_token &&
101
+ (current_token[:type] == :identifier || current_token[:type] == :special_key) &&
102
+ current_token[:value].start_with?('<') &&
103
+ current_token[:value].end_with?('>')
104
+ options << current_token[:value]
105
+ advance
106
+ end
107
+
108
+ # Parse the key sequence
109
+ key_sequence = nil
110
+ if current_token
111
+ if current_token[:type] == :special_key ||
112
+ (current_token[:value].start_with?('<') && current_token[:value].end_with?('>'))
113
+ key_sequence = current_token[:value]
114
+ advance
115
+ else
116
+ key_sequence = current_token[:value]
117
+ advance
118
+ end
119
+ end
120
+
121
+ # Collect everything else until end of line as the mapping target
122
+ # This is raw text and shouldn't be parsed as an expression
123
+ target_tokens = []
124
+
125
+ # Continue collecting tokens until we hit a newline or comment
126
+ while current_token &&
127
+ current_token[:type] != :comment &&
128
+ (current_token[:type] != :whitespace || current_token[:value].strip != '')
129
+
130
+ # If we hit a newline not preceded by a continuation character, we're done
131
+ if current_token[:value] == "\n" &&
132
+ (target_tokens.empty? || target_tokens.last[:value][-1] != '\\')
133
+ break
134
+ end
135
+
136
+ target_tokens << current_token
137
+ advance
138
+ end
139
+
140
+ # Join the target tokens to form the raw mapping target
141
+ target = target_tokens.map { |t| t[:value] }.join('')
142
+
143
+ {
144
+ type: :mapping_statement,
145
+ command: mapping_command,
146
+ options: options,
147
+ key_sequence: key_sequence,
148
+ target: target,
149
+ line: line,
150
+ column: column
151
+ }
152
+ end
153
+
68
154
  def parse_statement
69
155
  if !current_token
70
156
  return nil
71
157
  end
158
+ start_token = current_token
72
159
 
73
- if current_token[:type] == :keyword
160
+ # Handle pipe as command separator
161
+ if current_token[:type] == :operator && current_token[:value] == '|'
162
+ advance # Skip the pipe
163
+ return parse_statement
164
+ end
165
+
166
+ # Handle endif keyword outside normal if structure (likely from one-line if)
167
+ if current_token[:type] == :keyword && current_token[:value] == 'endif'
168
+ token = advance # Skip the endif
169
+ return {
170
+ type: :endif_marker,
171
+ line: token[:line],
172
+ column: token[:column]
173
+ }
174
+ end
175
+
176
+ # Add special cases for other ending keywords too to be thorough
177
+ if current_token[:type] == :keyword &&
178
+ ['endwhile', 'endfor', 'endfunction', 'endfunc', 'enddef'].include?(current_token[:value])
179
+ token = advance # Skip the keyword
180
+ return {
181
+ type: :"#{token[:value]}_marker",
182
+ line: token[:line],
183
+ column: token[:column]
184
+ }
185
+ end
186
+
187
+ # Now, add specific handling for continue and break
188
+ if current_token[:type] == :keyword && current_token[:value] == 'continue'
189
+ token = advance # Skip 'continue'
190
+ return {
191
+ type: :continue_statement,
192
+ line: token[:line],
193
+ column: token[:column]
194
+ }
195
+ end
196
+
197
+ if current_token[:type] == :keyword && current_token[:value] == 'break'
198
+ token = advance # Skip 'break'
199
+ return {
200
+ type: :break_statement,
201
+ line: token[:line],
202
+ column: token[:column]
203
+ }
204
+ end
205
+
206
+ # Handle try-catch
207
+ if current_token[:type] == :keyword && current_token[:value] == 'try'
208
+ return parse_try_statement
209
+ end
210
+
211
+ # Handle throw
212
+ if current_token[:type] == :keyword && current_token[:value] == 'throw'
213
+ return parse_throw_statement
214
+ end
215
+
216
+ # Skip standalone catch, finally, endtry keywords outside of try blocks
217
+ if current_token[:type] == :keyword &&
218
+ ['catch', 'finally', 'endtry'].include?(current_token[:value])
219
+ token = advance # Skip these keywords
220
+ return nil
221
+ end
222
+
223
+ # Add case for mapping commands
224
+ if current_token[:type] == :keyword &&
225
+ ['nnoremap', 'nmap', 'inoremap', 'imap', 'vnoremap', 'vmap',
226
+ 'xnoremap', 'xmap', 'cnoremap', 'cmap', 'noremap', 'map'].include?(current_token[:value])
227
+ parse_mapping_statement
228
+ elsif current_token[:type] == :runtime_command
229
+ parse_runtime_statement
230
+ elsif current_token[:type] == :keyword && current_token[:value] == 'runtime'
231
+ parse_runtime_statement
232
+ elsif current_token[:type] == :keyword
74
233
  case current_token[:value]
75
234
  when 'if'
76
235
  parse_if_statement
236
+ when 'command'
237
+ parse_command_definition
77
238
  when 'while'
78
239
  parse_while_statement
79
240
  when 'for'
@@ -81,7 +242,18 @@ module Vinter
81
242
  when 'def'
82
243
  parse_def_function
83
244
  when 'function'
84
- parse_legacy_function
245
+ # Only parse as function declaration if it looks like one
246
+ # Function declarations are followed by ! or identifier (function name)
247
+ if peek_token && (
248
+ (peek_token[:type] == :operator && peek_token[:value] == '!') ||
249
+ peek_token[:type] == :identifier ||
250
+ peek_token[:type] == :script_local
251
+ )
252
+ parse_legacy_function
253
+ else
254
+ # Not a function declaration, parse as expression statement
255
+ parse_expression_statement
256
+ end
85
257
  when 'return'
86
258
  parse_return_statement
87
259
  when 'var', 'const', 'final'
@@ -95,12 +267,32 @@ module Vinter
95
267
  { type: :vim9script_declaration, line: token[:line], column: token[:column] }
96
268
  when 'autocmd'
97
269
  parse_autocmd_statement
98
- when 'execute'
270
+ when 'execute', 'exec'
99
271
  parse_execute_statement
100
272
  when 'let'
101
- parse_let_statement
102
- when 'echohl', 'echomsg'
273
+ parse_let_statement
274
+ when 'unlet'
275
+ parse_unlet_statement
276
+ when 'echohl', 'echomsg', 'echoerr', 'echom'
103
277
  parse_echo_statement
278
+ when 'augroup'
279
+ parse_augroup_statement
280
+ when 'silent'
281
+ parse_silent_command
282
+ when 'call'
283
+ parse_call_statement
284
+ when 'delete'
285
+ parse_delete_statement
286
+ when 'set', 'setlocal'
287
+ parse_set_command
288
+ when 'syntax'
289
+ parse_syntax_command
290
+ when 'highlight'
291
+ parse_highlight_command
292
+ when 'sleep'
293
+ parse_sleep_command
294
+ when 'source'
295
+ parse_source_command
104
296
  else
105
297
  @warnings << {
106
298
  message: "Unexpected keyword: #{current_token[:value]}",
@@ -114,11 +306,33 @@ module Vinter
114
306
  elsif current_token[:type] == :identifier
115
307
  if current_token[:value] == "echo"
116
308
  parse_echo_statement
309
+ elsif current_token[:value] == "augroup"
310
+ parse_augroup_statement
311
+ elsif current_token[:value] == "au" || current_token[:value] == "autocmd"
312
+ parse_autocmd_statement
313
+ elsif current_token[:value] == "filter" || current_token[:value] == "filt"
314
+ parse_filter_command
315
+ elsif current_token[:value] == "command"
316
+ parse_command_definition
117
317
  else
118
318
  parse_expression_statement
119
319
  end
120
320
  elsif current_token[:type] == :comment
121
321
  parse_comment
322
+ elsif current_token[:type] == :string && current_token[:value].start_with?('"')
323
+ parse_comment
324
+ # token = current_token
325
+ # line = token[:line]
326
+ # column = token[:column]
327
+ # value = token[:value]
328
+ # advance
329
+ # { type: :comment, value: value, line: line, column: column }
330
+ elsif current_token[:type] == :silent_bang
331
+ parse_silent_command
332
+ elsif current_token[:type] == :identifier && current_token[:value] == 'delete'
333
+ parse_delete_command
334
+ elsif current_token[:type] == :percentage
335
+ parse_range_command
122
336
  else
123
337
  @warnings << {
124
338
  message: "Unexpected token type: #{current_token[:type]}",
@@ -131,224 +345,711 @@ module Vinter
131
345
  end
132
346
  end
133
347
 
134
- def parse_execute_statement
348
+ def parse_range_command
349
+ token = advance # Skip '%'
350
+ line = token[:line]
351
+ column = token[:column]
352
+
353
+ # Parse the command that follows the range
354
+ command = parse_statement
355
+
356
+ {
357
+ type: :range_command,
358
+ range: '%',
359
+ command: command,
360
+ line: line,
361
+ column: column
362
+ }
363
+ end
364
+
365
+ def parse_runtime_statement
366
+ token = advance # Skip 'runtime' or 'runtime!'
367
+ line = token[:line]
368
+ column = token[:column]
369
+
370
+ is_bang = token[:type] == :runtime_command ||
371
+ (token[:value] == 'runtime' && current_token && current_token[:type] == :operator && current_token[:value] == '!')
372
+
373
+ # Skip the '!' if it's separate token
374
+ if token[:type] != :runtime_command && is_bang
375
+ advance # Skip '!'
376
+ end
377
+
378
+ # Collect the pattern argument
379
+ pattern_parts = []
380
+ while @position < @tokens.length &&
381
+ !(current_token[:type] == :keyword || current_token[:value] == "\n")
382
+ pattern_parts << current_token[:value]
383
+ advance
384
+ end
385
+
386
+ pattern = pattern_parts.join('').strip
387
+
388
+ {
389
+ type: :runtime_statement,
390
+ bang: is_bang,
391
+ pattern: pattern,
392
+ line: line,
393
+ column: column
394
+ }
395
+ end
396
+
397
+ def parse_filter_command
135
398
  token = advance # Skip 'execute'
136
399
  line = token[:line]
137
400
  column = token[:column]
401
+ # lets count the parens that are created as we parse through the filter command
402
+ expect(:paren_open)
403
+ open_parens = 1
404
+ while open_parens > 0
405
+ advance
406
+ if current_token[:type] == :paren_open
407
+ open_parens += 1
408
+ elsif current_token[:type] == :paren_close
409
+ open_parens -= 1
410
+ end
411
+ end
412
+ advance
138
413
 
139
414
  # Parse arguments - typically string expressions with concatenation
140
415
  # Just accept any tokens until we hit a statement terminator or another command
141
- expressions = []
142
- expr = parse_expression
143
- expressions << expr if expr
416
+ #expressions = []
417
+ #expr = parse_expression
418
+ #expr = parse_string
419
+ #expressions << expr if expr
144
420
 
145
421
  # Return the execute statement
146
422
  {
147
- type: :execute_statement,
148
- expressions: expressions,
423
+ type: :filter_command,
424
+ #expressions: expressions,
149
425
  line: line,
150
426
  column: column
151
427
  }
152
428
  end
153
429
 
154
- def parse_let_statement
155
- token = advance # Skip 'let'
430
+ def old_parse_filter_command
431
+ token = advance # Skip 'filter' or 'filt'
156
432
  line = token[:line]
157
433
  column = token[:column]
158
434
 
159
- # Parse the target variable
160
- target = nil
161
- if current_token
162
- case current_token[:type]
163
- when :identifier, :global_variable, :script_local, :arg_variable, :option_variable, :special_variable
164
- target = {
165
- type: current_token[:type],
166
- name: current_token[:value],
167
- line: current_token[:line],
168
- column: current_token[:column]
169
- }
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
170
463
  advance
171
464
  else
172
465
  @errors << {
173
- message: "Expected variable name after let",
466
+ message: "Expected closing pattern delimiter: #{pattern_delimiter}",
174
467
  position: @position,
175
468
  line: current_token ? current_token[:line] : 0,
176
469
  column: current_token ? current_token[:column] : 0
177
470
  }
178
471
  end
179
- end
180
-
181
- # Skip the '=' or other assignment operator
182
- operator = nil
183
- if current_token && (
184
- (current_token[:type] == :operator && current_token[:value] == '=') ||
185
- current_token[:type] == :compound_operator)
186
- operator = current_token[:value]
187
- advance
188
472
  else
189
- @errors << {
190
- message: "Expected assignment operator after variable in let statement",
191
- position: @position,
192
- line: current_token ? current_token[:line] : 0,
193
- column: current_token ? current_token[:column] : 0
194
- }
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
195
490
  end
196
491
 
197
- # Parse the value expression
198
- value = parse_expression
492
+ # Parse the command to be filtered
493
+ command = parse_statement
199
494
 
200
495
  {
201
- type: :let_statement,
202
- target: target,
203
- operator: operator,
204
- value: value,
496
+ type: :filter_command,
497
+ pattern: pattern,
498
+ has_bang: has_bang,
499
+ command: command,
205
500
  line: line,
206
501
  column: column
207
502
  }
208
503
  end
209
504
 
210
- def parse_autocmd_statement
211
- token = advance # Skip 'autocmd'
505
+ def parse_augroup_statement
506
+ token = advance # Skip 'augroup'
212
507
  line = token[:line]
213
508
  column = token[:column]
214
509
 
215
- # Parse event name (like BufNewFile)
216
- event = nil
510
+ # Get the augroup name
511
+ name = nil
217
512
  if current_token && current_token[:type] == :identifier
218
- event = advance[:value]
513
+ name = current_token[:value]
514
+ advance
219
515
  else
220
516
  @errors << {
221
- message: "Expected event name after 'autocmd'",
517
+ message: "Expected augroup name",
222
518
  position: @position,
223
519
  line: current_token ? current_token[:line] : 0,
224
520
  column: current_token ? current_token[:column] : 0
225
521
  }
226
522
  end
227
523
 
228
- # Parse pattern (like *.match)
229
- pattern = nil
230
- if current_token
231
- pattern = current_token[:value]
232
- advance
524
+ # Check for augroup END
525
+ is_end_marker = false
526
+ if name && (name.upcase == "END" || name == "END")
527
+ is_end_marker = true
528
+ return {
529
+ type: :augroup_end,
530
+ line: line,
531
+ column: column
532
+ }
233
533
  end
234
534
 
235
- # Parse command (can be complex, including if statements)
236
- commands = []
237
-
238
- # Handle pipe-separated commands
239
- in_command = true
240
- while in_command && @position < @tokens.length
241
- if current_token && current_token[:value] == '|'
242
- advance # Skip '|'
243
- end
244
-
245
- # Parse the command
246
- if current_token && current_token[:type] == :keyword
247
- case current_token[:value]
248
- when 'if'
249
- commands << parse_if_statement
250
- when 'echo'
251
- commands << parse_echo_statement
252
- # Add other command types as needed
253
- else
254
- # Generic command handling
255
- cmd = parse_expression_statement
256
- commands << cmd if cmd
535
+ # Parse statements within the augroup until we find 'augroup END'
536
+ body = []
537
+ while @position < @tokens.length
538
+ # Check for 'augroup END'
539
+ if (current_token[:type] == :keyword && current_token[:value] == 'augroup') ||
540
+ (current_token[:type] == :identifier && current_token[:value] == 'augroup')
541
+ # Look ahead for END
542
+ if peek_token &&
543
+ ((peek_token[:type] == :identifier &&
544
+ (peek_token[:value].upcase == 'END' || peek_token[:value] == 'END')) ||
545
+ (peek_token[:type] == :keyword && peek_token[:value].upcase == 'END'))
546
+ advance # Skip 'augroup'
547
+ advance # Skip 'END'
548
+ break
257
549
  end
258
- elsif current_token && current_token[:type] == :identifier
259
- cmd = parse_expression_statement
260
- commands << cmd if cmd
261
- else
262
- in_command = false
263
550
  end
264
551
 
265
- # Check if we've reached the end of the autocmd command
266
- if !current_token || current_token[:type] == :comment || current_token[:value] == "\n"
267
- in_command = false
268
- end
552
+ stmt = parse_statement
553
+ body << stmt if stmt
269
554
  end
270
555
 
271
- return {
272
- type: :autocmd_statement,
273
- event: event,
274
- pattern: pattern,
275
- commands: commands,
556
+ {
557
+ type: :augroup_statement,
558
+ name: name,
559
+ body: body,
276
560
  line: line,
277
561
  column: column
278
562
  }
279
563
  end
280
564
 
281
- def parse_echo_statement
282
- token = advance #Skip 'echo'
565
+ def parse_execute_statement
566
+ token = advance # Skip 'execute'
283
567
  line = token[:line]
284
568
  column = token[:column]
285
569
 
286
- expression = parse_expression
570
+ # Parse arguments - typically string expressions with concatenation
571
+ # Just accept any tokens until we hit a statement terminator or another command
572
+ expressions = []
573
+ expr = parse_expression
574
+ #expr = parse_string
575
+ expressions << expr if expr
287
576
 
577
+ # Return the execute statement
288
578
  {
289
- type: :echo_statement,
290
- expression: expression,
579
+ type: :execute_statement,
580
+ expressions: expressions,
291
581
  line: line,
292
582
  column: column
293
583
  }
294
584
  end
295
585
 
296
- def parse_comment
297
- comment = current_token[:value]
298
- line = current_token[:line]
299
- column = current_token[:column]
300
- advance
301
- { type: :comment, value: comment, line: line, column: column }
302
- end
303
-
304
- def parse_if_statement
305
- token = advance # Skip 'if'
586
+ def parse_let_statement
587
+ # binding.pry
588
+ token = advance # Skip 'let'
306
589
  line = token[:line]
307
590
  column = token[:column]
308
- condition = parse_expression
309
591
 
310
- then_branch = []
311
- else_branch = []
592
+ # Parse the target variable
593
+ target = nil
594
+ if current_token
595
+ case current_token[:type]
596
+ when :identifier, :global_variable, :script_local, :buffer_local, :window_local, :tab_local, :arg_variable, :option_variable, :special_variable, :local_variable
597
+ target = {
598
+ type: current_token[:type],
599
+ name: current_token[:value],
600
+ line: current_token[:line],
601
+ column: current_token[:column]
602
+ }
603
+ advance
312
604
 
313
- # Parse statements until we hit 'else', 'elseif', or 'endif'
314
- while @position < @tokens.length
315
- if current_token[:type] == :keyword &&
316
- ['else', 'elseif', 'endif'].include?(current_token[:value])
317
- break
318
- end
605
+ # Check for property access with dot notation (e.g., person.job)
606
+ if current_token && current_token[:type] == :operator && current_token[:value] == '.'
607
+ dot_token = advance # Skip '.'
608
+
609
+ # Next token should be an identifier (property name)
610
+ if current_token && (current_token[:type] == :identifier || current_token[:type] == :keyword)
611
+ property_token = advance # Get the property name
612
+
613
+ # Update the target to be a property access
614
+ target = {
615
+ type: :property_access,
616
+ object: target,
617
+ property: property_token[:value],
618
+ line: dot_token[:line],
619
+ column: dot_token[:column]
620
+ }
621
+ 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
+ }
628
+ end
629
+ end
319
630
 
320
- stmt = parse_statement
321
- then_branch << stmt if stmt
322
- end
631
+ # Check for chained indexed access (dictionary key lookup)
632
+ while current_token && current_token[:type] == :bracket_open
633
+ bracket_token = advance # Skip '['
634
+ index_expr = parse_expression
635
+ expect(:bracket_close) # Skip ']'
636
+
637
+ # Update target to be an indexed access
638
+ target = {
639
+ type: :indexed_access,
640
+ object: target,
641
+ index: index_expr,
642
+ line: bracket_token[:line],
643
+ column: bracket_token[:column]
644
+ }
645
+ end
646
+ when :register_access
647
+ target = {
648
+ type: :register_access,
649
+ register: current_token[:value],
650
+ line: current_token[:line],
651
+ column: current_token[:column]
652
+ }
653
+ advance
654
+ when :bracket_open
655
+ # Handle array destructuring like: let [key, value] = split(header, ': ')
656
+ bracket_token = advance # Skip '['
323
657
 
324
- # Check for else/elseif
325
- if current_token && current_token[:type] == :keyword
326
- if current_token[:value] == 'else'
327
- advance # Skip 'else'
658
+ # Parse the variable names inside the brackets
659
+ destructuring_targets = []
328
660
 
329
- # Parse statements until 'endif'
330
- while @position < @tokens.length
331
- if current_token[:type] == :keyword && current_token[:value] == 'endif'
332
- break
661
+ # Parse comma-separated list of variables
662
+ while current_token && current_token[:type] != :bracket_close
663
+ # Skip commas between variables
664
+ if current_token[:type] == :comma
665
+ advance
666
+ next
333
667
  end
334
668
 
335
- stmt = parse_statement
336
- else_branch << stmt if stmt
337
- end
338
- elsif current_token[:value] == 'elseif'
339
- # This is a simplified handling - elseif should be treated as a nested if
340
- else_branch << parse_if_statement
341
- end
669
+ # Parse the variable name
670
+ if current_token && (
671
+ current_token[:type] == :identifier ||
672
+ current_token[:type] == :global_variable ||
673
+ current_token[:type] == :script_local ||
674
+ current_token[:type] == :arg_variable ||
675
+ current_token[:type] == :option_variable ||
676
+ current_token[:type] == :special_variable ||
677
+ current_token[:type] == :local_variable
678
+ )
679
+ destructuring_targets << {
680
+ type: current_token[:type],
681
+ name: current_token[:value],
682
+ line: current_token[:line],
683
+ column: current_token[:column]
684
+ }
685
+ advance
686
+ 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
+ }
693
+ # Try to recover by advancing to next comma or closing bracket
694
+ while current_token && current_token[:type] != :comma && current_token[:type] != :bracket_close
695
+ advance
696
+ end
697
+ end
698
+ end
699
+
700
+ expect(:bracket_close) # Skip ']'
701
+
702
+ target = {
703
+ type: :destructuring_assignment,
704
+ targets: destructuring_targets,
705
+ line: bracket_token[:line],
706
+ column: bracket_token[:column]
707
+ }
708
+ 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
+ }
715
+ end
716
+ end
717
+
718
+ # Skip the '=' or other assignment operator
719
+ operator = nil
720
+ if current_token && (
721
+ (current_token[:type] == :operator && current_token[:value] == '=') ||
722
+ current_token[:type] == :compound_operator)
723
+ operator = current_token[:value]
724
+ advance
725
+ 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
+ }
732
+ end
733
+
734
+ # Parse the value expression
735
+ value = nil
736
+
737
+ # Special handling for function() references in let statements
738
+ if current_token && current_token[:type] == :keyword && current_token[:value] == 'function'
739
+ advance # Skip 'function'
740
+
741
+ # Expect opening parenthesis
742
+ if current_token && current_token[:type] == :paren_open
743
+ paren_token = advance # Skip '('
744
+
745
+ # Parse the function name as a string
746
+ func_name = nil
747
+ if current_token && current_token[:type] == :string
748
+ func_name = current_token[:value]
749
+ advance
750
+ 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
+ }
757
+ end
758
+
759
+ # Expect closing parenthesis
760
+ expect(:paren_close) # Skip ')'
761
+
762
+ value = {
763
+ type: :function_reference,
764
+ function_name: func_name,
765
+ line: line,
766
+ column: column
767
+ }
768
+ else
769
+ # If not followed by parenthesis, parse as a normal expression
770
+ @position -= 1 # Go back to the 'function' keyword
771
+ value = parse_expression
772
+ end
773
+ else
774
+ # Normal expression parsing
775
+ value = parse_expression
776
+ end
777
+ {
778
+ type: :let_statement,
779
+ target: target,
780
+ operator: operator,
781
+ value: value,
782
+ line: line,
783
+ column: column
784
+ }
785
+ end
786
+
787
+ def parse_unlet_statement
788
+ token = advance # Skip 'unlet'
789
+ line = token[:line]
790
+ column = token[:column]
791
+
792
+ # Parse the variable to unlet
793
+ target = nil
794
+ if current_token
795
+ case current_token[:type]
796
+ when :identifier, :global_variable, :script_local, :buffer_local, :window_local, :tab_local, :arg_variable, :option_variable, :special_variable, :local_variable
797
+ target = {
798
+ type: current_token[:type],
799
+ name: current_token[:value],
800
+ line: current_token[:line],
801
+ column: current_token[:column]
802
+ }
803
+ advance
804
+
805
+ # Check for chained indexed access (like variable[key])
806
+ while current_token && current_token[:type] == :bracket_open
807
+ bracket_token = advance # Skip '['
808
+ index_expr = parse_expression
809
+ expect(:bracket_close) # Skip ']'
810
+
811
+ # Update target to be an indexed access
812
+ target = {
813
+ type: :indexed_access,
814
+ object: target,
815
+ index: index_expr,
816
+ line: bracket_token[:line],
817
+ column: bracket_token[:column]
818
+ }
819
+ end
820
+ else
821
+ @errors << {
822
+ message: "Expected variable name after 'unlet'",
823
+ position: @position,
824
+ line: current_token ? current_token[:line] : 0,
825
+ column: current_token ? current_token[:column] : 0
826
+ }
827
+ end
828
+ end
829
+
830
+ {
831
+ type: :unlet_statement,
832
+ target: target,
833
+ line: line,
834
+ column: column
835
+ }
836
+ end
837
+
838
+ def parse_autocmd_statement
839
+ token = advance # Skip 'autocmd'
840
+ line = token[:line]
841
+ column = token[:column]
842
+
843
+ # Parse event name (like BufNewFile or VimEnter)
844
+ event = nil
845
+ if current_token && current_token[:type] == :identifier
846
+ event = advance[:value]
847
+ else
848
+ @errors << {
849
+ message: "Expected event name after 'autocmd'",
850
+ position: @position,
851
+ line: current_token ? current_token[:line] : 0,
852
+ column: current_token ? current_token[:column] : 0
853
+ }
854
+ end
855
+
856
+ # Parse pattern (like *.match or *)
857
+ pattern = nil
858
+ if current_token
859
+ pattern = current_token[:value]
860
+ advance
861
+ end
862
+
863
+ # Parse everything after as the command - collect as raw tokens
864
+ command_parts = []
865
+ while @position < @tokens.length
866
+ if !current_token || current_token[:type] == :comment ||
867
+ (current_token[:value] == "\n" && !current_token[:value].start_with?("\\"))
868
+ break
869
+ end
870
+
871
+ command_parts << current_token
872
+ advance
873
+ end
874
+
875
+ return {
876
+ type: :autocmd_statement,
877
+ event: event,
878
+ pattern: pattern,
879
+ command_parts: command_parts,
880
+ line: line,
881
+ column: column
882
+ }
883
+ end
884
+
885
+ def parse_echo_statement
886
+ token = advance #Skip 'echo'
887
+ line = token[:line]
888
+ column = token[:column]
889
+
890
+ expression = parse_expression
891
+
892
+ {
893
+ type: :echo_statement,
894
+ expression: expression,
895
+ line: line,
896
+ column: column
897
+ }
898
+ end
899
+
900
+ def parse_comment
901
+ original_token = current_token
902
+ line = original_token[:line]
903
+ column = original_token[:column]
904
+ comment = original_token[:value]
905
+
906
+ # puts "INSIDE PARSE COMMENT"
907
+ # puts "Current token: #{current_token.inspect}"
908
+ # puts "Peek token: #{peek_token.inspect}"
909
+
910
+ # Check if the comment contains a newline
911
+ if comment.include?("\n")
912
+ # Split the comment at the newline
913
+ parts = comment.split("\n", 2)
914
+ comment = parts[0] # Only use the part before newline
915
+
916
+ # Create a new token for the content after the newline
917
+ remainder = parts[1].strip
918
+
919
+ if !remainder.empty?
920
+ # Position correctly - we'll advance past the current token
921
+ # But should process the remainder separately later
922
+
923
+ # For debugging only, you can print what's being processed
924
+ # puts "Found comment with newline. Using: '#{comment}', Remainder: '#{remainder}'"
925
+
926
+ # Don't call advance() - we'll modify the current token instead
927
+ @tokens[@position] = {
928
+ type: :string, # Keep original type
929
+ value: comment, # Use only the part before newline
930
+ line: line,
931
+ column: column
932
+ }
933
+
934
+ # Insert the remainder as a new token after the current one
935
+ @tokens.insert(@position + 1, {
936
+ type: :comment, # Same type for consistency
937
+ value: remainder, # Preserve as a string token
938
+ line: line + 1, # Increment line number for the part after newline
939
+ column: 0 # Approximate column based on indentation
940
+ })
941
+ end
342
942
  end
343
943
 
344
- # Expect endif
345
- expect(:keyword) # This should be 'endif'
944
+ # Now advance past the (potentially modified) current token
945
+ advance
946
+ { type: :comment, value: comment, line: line, column: column }
947
+ end
948
+
949
+ def parse_if_statement
950
+ token = advance # Skip 'if'
951
+ line = token[:line]
952
+ column = token[:column]
953
+
954
+ condition = parse_expression
955
+
956
+ then_branch = []
957
+ else_branch = []
958
+
959
+ # Check if this might be a one-line if (look ahead for pipe character)
960
+ one_line_if = false
961
+
962
+ if current_token && current_token[:type] == :operator && current_token[:value] == '|'
963
+ one_line_if = true
964
+ advance # Skip the pipe
965
+
966
+ # Parse the then statement
967
+ stmt = parse_statement
968
+ then_branch << stmt if stmt
969
+
970
+ # Check for the closing pipe and endif
971
+ if current_token && current_token[:type] == :operator && current_token[:value] == '|'
972
+ advance # Skip the pipe
973
+
974
+ # 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
+ }
984
+ end
985
+ end
986
+ 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
1001
+
1002
+ # Check for else/elseif
1003
+ if current_token && current_token[:type] == :keyword
1004
+ if current_token[:value] == 'else'
1005
+ advance # Skip 'else'
1006
+
1007
+ # 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
1016
+ elsif current_token[:value] == 'elseif'
1017
+ elseif_stmt = parse_if_statement
1018
+ else_branch << elseif_stmt if elseif_stmt
1019
+
1020
+ return {
1021
+ type: :if_statement,
1022
+ condition: condition,
1023
+ then_branch: then_branch,
1024
+ else_branch: else_branch,
1025
+ line: line,
1026
+ column: column
1027
+ }
1028
+ end
1029
+ end
1030
+
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
346
1046
 
347
1047
  {
348
1048
  type: :if_statement,
349
1049
  condition: condition,
350
1050
  then_branch: then_branch,
351
1051
  else_branch: else_branch,
1052
+ one_line: one_line_if,
352
1053
  line: line,
353
1054
  column: column
354
1055
  }
@@ -389,16 +1090,22 @@ module Vinter
389
1090
  line = token[:line]
390
1091
  column = token[:column]
391
1092
 
392
- # Parse the loop variable(s)
393
- if current_token && current_token[:type] == :paren_open
394
- # Handle tuple assignment: for (key, val) in dict
395
- advance # Skip '('
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 '['
396
1100
 
397
1101
  loop_vars = []
398
1102
 
399
1103
  loop do
400
- if current_token && current_token[:type] == :identifier
401
- loop_vars << advance[:value]
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
402
1109
  else
403
1110
  @errors << {
404
1111
  message: "Expected identifier in for loop variables",
@@ -416,7 +1123,7 @@ module Vinter
416
1123
  end
417
1124
  end
418
1125
 
419
- expect(:paren_close) # Skip ')'
1126
+ expect(:bracket_close) # Skip ']'
420
1127
 
421
1128
  if !current_token || (current_token[:type] != :identifier || current_token[:value] != 'in')
422
1129
  @errors << {
@@ -434,7 +1141,7 @@ module Vinter
434
1141
  # Parse the body until 'endfor'
435
1142
  body = []
436
1143
  while @position < @tokens.length
437
- if current_token[:type] == :keyword && current_token[:value] == 'endfor'
1144
+ if current_token && current_token[:type] == :keyword && current_token[:value] == 'endfor'
438
1145
  break
439
1146
  end
440
1147
 
@@ -443,7 +1150,19 @@ module Vinter
443
1150
  end
444
1151
 
445
1152
  # Expect endfor
446
- expect(:keyword) # This should be '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
447
1166
 
448
1167
  return {
449
1168
  type: :for_statement,
@@ -453,29 +1172,110 @@ module Vinter
453
1172
  line: line,
454
1173
  column: column
455
1174
  }
456
- else
457
- # Simple for var in list
458
- if !current_token || current_token[:type] != :identifier
459
- @errors << {
460
- message: "Expected identifier as for loop variable",
461
- position: @position,
462
- line: current_token ? current_token[:line] : 0,
463
- column: current_token ? current_token[:column] : 0
464
- }
465
- return nil
466
- end
1175
+ elsif current_token && current_token[:type] == :paren_open
1176
+ # Handle multiple variables in parentheses: for (var1, var2) in list
1177
+ advance # Skip '('
467
1178
 
468
- loop_var = advance[:value]
1179
+ loop_vars = []
469
1180
 
470
- if !current_token || (current_token[:type] != :identifier || current_token[:value] != 'in')
471
- @errors << {
472
- message: "Expected 'in' after for loop variable",
473
- position: @position,
474
- line: current_token ? current_token[:line] : 0,
475
- column: current_token ? current_token[:column] : 0
476
- }
477
- else
478
- advance # Skip 'in'
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 ')'
1205
+
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
1216
+
1217
+ iterable = parse_expression
1218
+
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
1225
+
1226
+ stmt = parse_statement
1227
+ body << stmt if stmt
1228
+ end
1229
+
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
1244
+
1245
+ return {
1246
+ type: :for_statement,
1247
+ loop_vars: loop_vars,
1248
+ iterable: iterable,
1249
+ body: body,
1250
+ line: line,
1251
+ column: column
1252
+ }
1253
+ else
1254
+ # Handle single variable: for var in list
1255
+ if current_token && (current_token[:type] == :identifier ||
1256
+ current_token[:type] == :local_variable ||
1257
+ current_token[:type] == :global_variable ||
1258
+ current_token[:type] == :script_local)
1259
+ loop_var = advance
1260
+ 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'
479
1279
  end
480
1280
 
481
1281
  iterable = parse_expression
@@ -483,7 +1283,7 @@ module Vinter
483
1283
  # Parse the body until 'endfor'
484
1284
  body = []
485
1285
  while @position < @tokens.length
486
- if current_token[:type] == :keyword && current_token[:value] == 'endfor'
1286
+ if current_token && current_token[:type] == :keyword && current_token[:value] == 'endfor'
487
1287
  break
488
1288
  end
489
1289
 
@@ -492,7 +1292,19 @@ module Vinter
492
1292
  end
493
1293
 
494
1294
  # Expect endfor
495
- expect(:keyword) # This should be '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
496
1308
 
497
1309
  return {
498
1310
  type: :for_statement,
@@ -646,7 +1458,7 @@ module Vinter
646
1458
  end
647
1459
 
648
1460
  def parse_type
649
- if current_token && current_token[:type] == :identifier
1461
+ if current_token && [:identifier, :keyword].include?(current_token[:type])
650
1462
  type_name = advance
651
1463
 
652
1464
  # Handle generic types like list<string>
@@ -727,7 +1539,12 @@ module Vinter
727
1539
  column = token[:column]
728
1540
 
729
1541
  value = nil
730
- if @position < @tokens.length && current_token[:type] != :semicolon
1542
+ # Check if we've reached the end of the file, end of line, or a semicolon
1543
+ if @position < @tokens.length &&
1544
+ current_token &&
1545
+ current_token[:type] != :semicolon &&
1546
+ !(current_token[:type] == :keyword &&
1547
+ ['endif', 'endwhile', 'endfor', 'endfunction', 'endfunc'].include?(current_token[:value]))
731
1548
  value = parse_expression
732
1549
  end
733
1550
 
@@ -783,7 +1600,72 @@ module Vinter
783
1600
  end
784
1601
 
785
1602
  def parse_expression
786
- return parse_binary_expression
1603
+ # binding.pry
1604
+ # Special case for empty return statements or standalone keywords that shouldn't be expressions
1605
+ if current_token && current_token[:type] == :keyword &&
1606
+ ['return', 'endif', 'endwhile', 'endfor', 'endfunction', 'endfunc'].include?(current_token[:value])
1607
+ return nil
1608
+ end
1609
+
1610
+ if current_token[:type] == :string
1611
+ string_value = current_token[:value]
1612
+ while current_token && peek_token && [:line_continuation, :identifier].include?(peek_token[:type])
1613
+ # Handle strings with line continuation
1614
+ if ["'", '"'].include? current_token[:value][-1]
1615
+ token_line = current_token[:line]
1616
+ token_column = current_token[:column]
1617
+ advance # Consume the string token
1618
+ return {
1619
+ type: :literal,
1620
+ value: string_value,
1621
+ token_type: :string,
1622
+ line: token_line,
1623
+ column: token_column
1624
+ }
1625
+ else
1626
+ advance
1627
+ string_value += current_token[:value]
1628
+ end
1629
+ end
1630
+ end
1631
+
1632
+ # Parse the condition expression
1633
+ expr = parse_binary_expression
1634
+
1635
+ # Check if this is a ternary expression
1636
+ if current_token && (current_token[:type] == :question_mark || (current_token[:type] == :operator && current_token[:value] == '?'))
1637
+ question_token = advance # Skip '?'
1638
+
1639
+ # Parse the "then" expression
1640
+ then_expr = parse_expression
1641
+
1642
+ # Expect the colon
1643
+ if current_token && current_token[:type] == :colon
1644
+ colon_token = advance # Skip ':'
1645
+
1646
+ # Parse the "else" expression
1647
+ else_expr = parse_expression
1648
+
1649
+ # Return the ternary expression
1650
+ return {
1651
+ type: :ternary_expression,
1652
+ condition: expr,
1653
+ then_expr: then_expr,
1654
+ else_expr: else_expr,
1655
+ line: question_token[:line],
1656
+ column: question_token[:column]
1657
+ }
1658
+ else
1659
+ @errors << {
1660
+ message: "Expected ':' in ternary expression",
1661
+ position: @position,
1662
+ line: current_token ? current_token[:line] : 0,
1663
+ column: current_token ? current_token[:column] : 0
1664
+ }
1665
+ end
1666
+ end
1667
+
1668
+ return expr
787
1669
  end
788
1670
 
789
1671
  def parse_binary_expression(precedence = 0)
@@ -799,9 +1681,65 @@ module Vinter
799
1681
  advance
800
1682
  end
801
1683
 
1684
+ if current_token && current_token[:type] == :operator && current_token[:value] == '.' &&
1685
+ operator_precedence(current_token[:value]) >= precedence
1686
+ op_token = advance # Skip the operator
1687
+
1688
+ # Check if we're dealing with a command placeholder on either side
1689
+ if (left && left[:type] == :command_arg_placeholder) ||
1690
+ (peek_token && peek_token[:type] == :command_arg_placeholder)
1691
+ # Handle command-specific concatenation
1692
+ right = parse_binary_expression(operator_precedence('.') + 1)
1693
+
1694
+ left = {
1695
+ type: :command_concat_expression, # Special type for command concatenation
1696
+ operator: '.',
1697
+ left: left,
1698
+ right: right,
1699
+ line: op_token[:line],
1700
+ column: op_token[:column]
1701
+ }
1702
+ else
1703
+ # Normal expression concatenation
1704
+ right = parse_binary_expression(operator_precedence('.') + 1)
1705
+
1706
+ left = {
1707
+ type: :binary_expression,
1708
+ operator: '.',
1709
+ left: left,
1710
+ right: right,
1711
+ line: op_token[:line],
1712
+ column: op_token[:column]
1713
+ }
1714
+ end
1715
+ elsif current_token && current_token[:type] == :operator &&
1716
+ ['<', '>', '=', '!'].include?(current_token[:value]) &&
1717
+ peek_token && peek_token[:type] == :operator && peek_token[:value] == '='
1718
+
1719
+ # Combine the two operators into one token
1720
+ op_token = current_token
1721
+ op = current_token[:value] + peek_token[:value]
1722
+ advance # Skip the first operator
1723
+ advance # Skip the second operator
1724
+
1725
+ # Now process the combined operator
1726
+ op_precedence = operator_precedence(op)
1727
+
1728
+ if op_precedence >= precedence
1729
+ right = parse_binary_expression(op_precedence + 1)
1730
+
1731
+ left = {
1732
+ type: :binary_expression,
1733
+ operator: op,
1734
+ left: left,
1735
+ right: right,
1736
+ line: op_token[:line],
1737
+ column: op_token[:column]
1738
+ }
1739
+ end
802
1740
  # Now we should be at the operator
803
- if current_token && current_token[:type] == :operator &&
804
- operator_precedence(current_token[:value]) >= precedence
1741
+ elsif current_token && current_token[:type] == :operator &&
1742
+ operator_precedence(current_token[:value]) >= precedence
805
1743
  op_token = advance
806
1744
  op = op_token[:value]
807
1745
  op_precedence = operator_precedence(op)
@@ -858,24 +1796,160 @@ module Vinter
858
1796
  expr = nil
859
1797
 
860
1798
  case token[:type]
861
- when :number
1799
+ # Add handling for command arg placeholders
1800
+ when :command_arg_placeholder
862
1801
  advance
863
1802
  expr = {
864
- type: :literal,
1803
+ type: :command_arg_placeholder,
865
1804
  value: token[:value],
866
- token_type: :number,
867
1805
  line: line,
868
1806
  column: column
869
1807
  }
870
- when :string
1808
+ # Add special handling for keywords that might appear in expressions
1809
+ when :keyword
1810
+ # Special handling for map-related keywords when they appear in expressions
1811
+ if ['map', 'nmap', 'imap', 'vmap', 'xmap', 'noremap', 'nnoremap', 'inoremap', 'vnoremap', 'xnoremap', 'cnoremap', 'cmap'].include?(token[:value])
1812
+ if peek_token[:type] == :paren_open
1813
+ parse_builtin_function_call(token[:value], line, column)
1814
+ else
1815
+ # Treat map commands as identifiers when inside expressions
1816
+ advance
1817
+ return {
1818
+ type: :identifier,
1819
+ name: token[:value],
1820
+ line: line,
1821
+ column: column
1822
+ }
1823
+ end
1824
+ elsif token[:value] == 'type' && current_token && (current_token[:type] == :paren_open || peek_token && peek_token[:type] == :paren_open)
1825
+ # This is the type() function call
1826
+ return parse_builtin_function_call(token[:value], line, column)
1827
+ elsif token[:value] == 'function'
1828
+ # Check if this is a function() call or just 'function' as property name
1829
+ if peek_token && peek_token[:type] == :paren_open
1830
+ advance # Skip 'function'
1831
+ # This is a function() call
1832
+ if current_token && current_token[:type] == :paren_open
1833
+ # This is a function reference call
1834
+ paren_token = advance # Skip '('
1835
+
1836
+ # Parse the function name as a string or arrow function
1837
+ if current_token && current_token[:type] == :string
1838
+ func_name = current_token[:value]
1839
+ advance
1840
+
1841
+ # Check for additional arguments (function() can take multiple arguments)
1842
+ args = []
1843
+ while current_token && current_token[:type] == :comma
1844
+ advance # Skip comma
1845
+ arg = parse_expression
1846
+ args << arg if arg
1847
+ end
1848
+
1849
+ # Expect closing parenthesis
1850
+ expect(:paren_close) # Skip ')'
1851
+
1852
+ return {
1853
+ type: :function_reference,
1854
+ function_name: func_name,
1855
+ arguments: args,
1856
+ line: line,
1857
+ column: column
1858
+ }
1859
+ elsif current_token && current_token[:type] == :brace_open
1860
+ # This is an arrow function inside function()
1861
+ arrow_function = parse_vim_lambda(line, column)
1862
+
1863
+ # Expect closing parenthesis
1864
+ expect(:paren_close) # Skip ')'
1865
+
1866
+ return {
1867
+ type: :function_reference,
1868
+ function_body: arrow_function,
1869
+ line: line,
1870
+ column: column
1871
+ }
1872
+ else
1873
+ @errors << {
1874
+ message: "Expected string or arrow function definition in function() call",
1875
+ position: @position,
1876
+ line: current_token ? current_token[:line] : 0,
1877
+ column: current_token ? current_token[:column] : 0
1878
+ }
1879
+ end
1880
+ end
1881
+ else
1882
+ # If not followed by parenthesis, treat 'function' as an identifier (property name)
1883
+ advance # Skip 'function'
1884
+ return {
1885
+ type: :identifier,
1886
+ name: token[:value],
1887
+ line: line,
1888
+ column: column
1889
+ }
1890
+ end
1891
+ # Legacy Vim allows certain keywords as identifiers in expressions
1892
+ elsif ['return', 'type'].include?(token[:value])
1893
+ # Handle 'return' keyword specially when it appears in an expression context
1894
+ advance
1895
+ @warnings << {
1896
+ message: "Keyword '#{token[:value]}' used in an expression context",
1897
+ position: @position,
1898
+ line: line,
1899
+ column: column
1900
+ }
1901
+ # Check if this is a function call for 'type'
1902
+ if token[:value] == 'type' && current_token && current_token[:type] == :paren_open
1903
+ return parse_function_call(token[:value], line, column)
1904
+ end
1905
+
1906
+ expr = {
1907
+ type: :identifier,
1908
+ name: token[:value],
1909
+ line: line,
1910
+ column: column
1911
+ }
1912
+ else
1913
+ @errors << {
1914
+ message: "Unexpected keyword in expression: #{token[:value]}",
1915
+ position: @position,
1916
+ line: line,
1917
+ column: column
1918
+ }
1919
+ advance
1920
+ return nil
1921
+ end
1922
+ when :number
871
1923
  advance
872
1924
  expr = {
873
1925
  type: :literal,
874
1926
  value: token[:value],
875
- token_type: :string,
1927
+ token_type: :number,
876
1928
  line: line,
877
1929
  column: column
878
1930
  }
1931
+ when :string
1932
+ # parse_string
1933
+ if token[:value].start_with?('"')
1934
+ advance
1935
+ return {
1936
+ type: :comment,
1937
+ value: token[:value],
1938
+ line: line,
1939
+ column: column
1940
+ }
1941
+ else
1942
+ string_value = token[:value]
1943
+ advance
1944
+ expr = {
1945
+ type: :literal,
1946
+ value: string_value,
1947
+ raw_value: string_value, # Store the raw string to preserve escapes
1948
+ token_type: :string,
1949
+ line: line,
1950
+ column: column
1951
+ }
1952
+ end
879
1953
  when :option_variable
880
1954
  # Handle Vim option variables (like &compatible)
881
1955
  advance
@@ -885,6 +1959,14 @@ module Vinter
885
1959
  line: line,
886
1960
  column: column
887
1961
  }
1962
+ when :scoped_option_variable
1963
+ advance
1964
+ expr = {
1965
+ type: :scoped_option_variable,
1966
+ name: token[:value],
1967
+ line: line,
1968
+ column: column
1969
+ }
888
1970
  when :special_variable
889
1971
  # Handle Vim special variables (like v:version)
890
1972
  advance
@@ -909,6 +1991,21 @@ module Vinter
909
1991
  line: line,
910
1992
  column: column
911
1993
  }
1994
+ when :buffer_local
1995
+ # Handle script-local variables/functions (like s:var)
1996
+ advance
1997
+
1998
+ # Check if this is a function call
1999
+ if current_token && current_token[:type] == :paren_open
2000
+ return parse_function_call(token[:value], line, column)
2001
+ end
2002
+
2003
+ expr = {
2004
+ type: :buffer_local,
2005
+ name: token[:value],
2006
+ line: line,
2007
+ column: column
2008
+ }
912
2009
  when :global_variable
913
2010
  # Handle global variables (like g:var)
914
2011
  advance
@@ -927,32 +2024,54 @@ module Vinter
927
2024
  line: line,
928
2025
  column: column
929
2026
  }
930
- when :identifier
2027
+ when :local_variable
2028
+ # Handle local variables (like l:var)
931
2029
  advance
932
-
933
- # Check if this is a function call
934
- if current_token && current_token[:type] == :paren_open
935
- return parse_function_call(token[:value], line, column)
936
- end
937
-
938
- # Special handling for execute command
939
- if token[:value] == 'execute'
940
- # Parse the string expressions for execute
941
- # For now we'll just treat it as a normal identifier
942
- expr = {
943
- type: :identifier,
944
- name: token[:value],
945
- line: line,
946
- column: column
947
- }
2030
+ expr = {
2031
+ type: :local_variable,
2032
+ name: token[:value],
2033
+ line: line,
2034
+ column: column
2035
+ }
2036
+ when :identifier
2037
+ # Special handling for Vim built-in functions
2038
+ if ['has', 'exists', 'empty', 'get', 'type', 'map', 'copy'].include?(token[:value])
2039
+ expr = parse_builtin_function_call(token[:value], line, column)
948
2040
  else
949
- expr = {
950
- type: :identifier,
951
- name: token[:value],
952
- line: line,
953
- column: column
954
- }
2041
+ advance
2042
+
2043
+ # Check if this is a function call
2044
+ if current_token && current_token[:type] == :paren_open
2045
+ expr = parse_function_call(token[:value], line, column)
2046
+ else
2047
+ # Special handling for execute command
2048
+ if token[:value] == 'execute'
2049
+ # Parse the string expressions for execute
2050
+ # For now we'll just treat it as a normal identifier
2051
+ expr = {
2052
+ type: :identifier,
2053
+ name: token[:value],
2054
+ line: line,
2055
+ column: column
2056
+ }
2057
+ else
2058
+ expr = {
2059
+ type: :identifier,
2060
+ name: token[:value],
2061
+ line: line,
2062
+ column: column
2063
+ }
2064
+ end
2065
+ end
955
2066
  end
2067
+ when :namespace_prefix
2068
+ advance
2069
+ expr = {
2070
+ type: :namespace_prefix,
2071
+ name: token[:value],
2072
+ line: line,
2073
+ column: column
2074
+ }
956
2075
  when :paren_open
957
2076
  if is_lambda_expression
958
2077
  return parse_lambda_expression(line, column)
@@ -964,11 +2083,22 @@ module Vinter
964
2083
  when :bracket_open
965
2084
  expr = parse_list_literal(line, column)
966
2085
  when :brace_open
967
- expr = parse_dict_literal(line, column)
2086
+ expr = parse_vim_lambda_or_dict(line, column)
968
2087
  when :backslash
969
2088
  # Handle line continuation with backslash
970
2089
  advance
971
2090
  expr = parse_expression
2091
+ when :register_access
2092
+ advance
2093
+ expr = {
2094
+ type: :register_access,
2095
+ register: token[:value][1..-1], # Remove the @ symbol
2096
+ line: line,
2097
+ column: column
2098
+ }
2099
+ when :line_continuation
2100
+ advance
2101
+ expr = parse_expression
972
2102
  else
973
2103
  @errors << {
974
2104
  message: "Unexpected token in expression: #{token[:type]}",
@@ -984,28 +2114,44 @@ module Vinter
984
2114
  while current_token
985
2115
  # Check for property access with dot
986
2116
  if current_token[:type] == :operator && current_token[:value] == '.'
987
- dot_token = advance # Skip '.'
988
-
989
- # Next token should be an identifier (property name)
990
- if !current_token || current_token[:type] != :identifier
991
- @errors << {
992
- message: "Expected property name after '.'",
993
- position: @position,
994
- line: current_token ? current_token[:line] : 0,
995
- column: current_token ? current_token[:column] : 0
2117
+ # Look ahead to determine if this is property access or concatenation
2118
+ next_token = peek_token
2119
+ is_property_access = next_token && (next_token[:type] == :identifier || next_token[:type] == :keyword)
2120
+
2121
+ # Check if this is a property access (only when right side is identifier/keyword)
2122
+ if is_property_access && (expr[:type] == :identifier || expr[:type] == :global_variable ||
2123
+ expr[:type] == :script_local || expr[:type] == :namespace_prefix ||
2124
+ expr[:type] == :arg_variable || expr[:type] == :buffer_local ||
2125
+ expr[:type] == :window_local || expr[:type] == :tab_local ||
2126
+ expr[:type] == :local_variable || expr[:type] == :property_access ||
2127
+ expr[:type] == :indexed_access)
2128
+
2129
+ dot_token = advance # Skip '.'
2130
+ # Next token should be an identifier (property name)
2131
+ if !current_token || (current_token[:type] != :identifier &&
2132
+ current_token[:type] != :keyword)
2133
+ @errors << {
2134
+ message: "Expected property name after '.'",
2135
+ position: @position,
2136
+ line: current_token ? current_token[:line] : 0,
2137
+ column: current_token ? current_token[:column] : 0
2138
+ }
2139
+ break
2140
+ end
2141
+ property_token = advance
2142
+
2143
+ expr = {
2144
+ type: :property_access,
2145
+ object: expr,
2146
+ property: property_token[:value],
2147
+ line: dot_token[:line],
2148
+ column: dot_token[:column]
996
2149
  }
2150
+ else
2151
+ # This is concatenation or other binary operation, break to let binary expression parser handle it
997
2152
  break
998
2153
  end
999
2154
 
1000
- property_token = advance # Get property name
1001
-
1002
- expr = {
1003
- type: :property_access,
1004
- object: expr,
1005
- property: property_token[:value],
1006
- line: dot_token[:line],
1007
- column: dot_token[:column]
1008
- }
1009
2155
  # Check for method call with arrow ->
1010
2156
  elsif current_token[:type] == :operator && current_token[:value] == '->'
1011
2157
  arrow_token = advance # Skip '->'
@@ -1056,18 +2202,47 @@ module Vinter
1056
2202
  # Check for indexing with brackets
1057
2203
  elsif current_token[:type] == :bracket_open
1058
2204
  bracket_token = advance # Skip '['
1059
-
1060
2205
  index_expr = parse_expression
1061
2206
 
2207
+ # Add support for list slicing with colon
2208
+ end_index = nil
2209
+ if current_token && current_token[:type] == :colon
2210
+ advance # Skip ':'
2211
+ # handle omitted end index case like [6:]
2212
+ if current_token && current_token[:type] == :bracket_close
2213
+ end_index = {
2214
+ type: :implicit_end_index,
2215
+ value: nil,
2216
+ line: current_token[:line],
2217
+ column: current_token[:column]
2218
+ }
2219
+ else
2220
+ end_index = parse_expression
2221
+ end
2222
+ end
2223
+
1062
2224
  expect(:bracket_close) # Skip ']'
1063
2225
 
1064
- expr = {
1065
- type: :indexed_access,
1066
- object: expr,
1067
- index: index_expr,
1068
- line: bracket_token[:line],
1069
- column: bracket_token[:column]
1070
- }
2226
+ if end_index
2227
+ # This is a slice operation
2228
+ expr = {
2229
+ type: :slice_access,
2230
+ object: expr,
2231
+ start_index: index_expr,
2232
+ end_index: end_index,
2233
+ line: bracket_token[:line],
2234
+ column: bracket_token[:column]
2235
+ }
2236
+ else
2237
+ # Regular indexed access
2238
+ expr = {
2239
+ type: :indexed_access,
2240
+ object: expr,
2241
+ index: index_expr,
2242
+ line: bracket_token[:line],
2243
+ column: bracket_token[:column]
2244
+ }
2245
+ end
1071
2246
  # Check for function call directly on an expression
1072
2247
  elsif current_token[:type] == :paren_open
1073
2248
  paren_token = advance # Skip '('
@@ -1106,6 +2281,85 @@ module Vinter
1106
2281
  return expr
1107
2282
  end
1108
2283
 
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
2335
+
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]
2351
+ end
2352
+
2353
+ # Return function call node
2354
+ {
2355
+ type: :builtin_function_call,
2356
+ name: name,
2357
+ arguments: args,
2358
+ line: line,
2359
+ column: column
2360
+ }
2361
+ end
2362
+
1109
2363
  def is_lambda_expression
1110
2364
  # Save the current position
1111
2365
  original_position = @position
@@ -1153,7 +2407,7 @@ module Vinter
1153
2407
  # There might be a return type annotation
1154
2408
  advance
1155
2409
  # Skip the type
1156
- advance if current_token && current_token[:type] == :identifier
2410
+ advance if current_token && [:identifier, :keyword].include?(current_token[:type])
1157
2411
  # Check for the arrow
1158
2412
  has_arrow = current_token && current_token[:type] == :operator && current_token[:value] == '=>'
1159
2413
  end
@@ -1165,6 +2419,109 @@ module Vinter
1165
2419
  return has_params && has_arrow
1166
2420
  end
1167
2421
 
2422
+ def parse_vim_lambda(line, column)
2423
+ advance # Skip opening brace
2424
+
2425
+ # Parse parameters
2426
+ params = []
2427
+
2428
+ # Check if this is a no-parameter lambda (starts with ->)
2429
+ if current_token && current_token[:type] == :operator && current_token[:value] == '->'
2430
+ # No parameters, skip to arrow parsing
2431
+ else
2432
+ # Parse parameter(s)
2433
+ if current_token && (current_token[:type] == :identifier ||
2434
+ current_token[:type] == :local_variable ||
2435
+ current_token[:type] == :global_variable)
2436
+ param_name = current_token[:value]
2437
+ param_line = current_token[:line]
2438
+ param_column = current_token[:column]
2439
+ advance
2440
+
2441
+ params << {
2442
+ type: :parameter,
2443
+ name: param_name,
2444
+ line: param_line,
2445
+ column: param_column
2446
+ }
2447
+
2448
+ # Handle multiple parameters (comma-separated)
2449
+ while current_token && current_token[:type] == :comma
2450
+ advance # Skip comma
2451
+
2452
+ if current_token && (current_token[:type] == :identifier ||
2453
+ current_token[:type] == :local_variable ||
2454
+ current_token[:type] == :global_variable)
2455
+ param_name = current_token[:value]
2456
+ param_line = current_token[:line]
2457
+ param_column = current_token[:column]
2458
+ advance
2459
+
2460
+ params << {
2461
+ type: :parameter,
2462
+ name: param_name,
2463
+ line: param_line,
2464
+ column: param_column
2465
+ }
2466
+ else
2467
+ @errors << {
2468
+ message: "Expected parameter name after comma in lambda function",
2469
+ position: @position,
2470
+ line: current_token ? current_token[:line] : 0,
2471
+ column: current_token ? current_token[:column] : 0
2472
+ }
2473
+ break
2474
+ end
2475
+ end
2476
+ else
2477
+ # For no-parameter lambdas, this is not an error
2478
+ if !(current_token && current_token[:type] == :operator && current_token[:value] == '->')
2479
+ @errors << {
2480
+ message: "Expected parameter name or '->' in lambda function",
2481
+ position: @position,
2482
+ line: current_token ? current_token[:line] : 0,
2483
+ column: current_token ? current_token[:column] : 0
2484
+ }
2485
+ end
2486
+ end
2487
+ end
2488
+
2489
+ # Expect the arrow token
2490
+ if current_token && current_token[:type] == :operator && current_token[:value] == '->'
2491
+ advance # Skip ->
2492
+ else
2493
+ @errors << {
2494
+ message: "Expected '->' in lambda function",
2495
+ position: @position,
2496
+ line: current_token ? current_token[:line] : 0,
2497
+ column: current_token ? current_token[:column] : 0
2498
+ }
2499
+ end
2500
+
2501
+ # Parse the lambda body expression (everything until the closing brace)
2502
+ body = parse_expression
2503
+
2504
+ # Expect closing brace
2505
+ if current_token && current_token[:type] == :brace_close
2506
+ advance # Skip }
2507
+ else
2508
+ @errors << {
2509
+ message: "Expected closing brace for lambda function",
2510
+ position: @position,
2511
+ line: current_token ? current_token[:line] : 0,
2512
+ column: current_token ? current_token[:column] : 0
2513
+ }
2514
+ end
2515
+
2516
+ return {
2517
+ type: :vim_lambda,
2518
+ params: params,
2519
+ body: body,
2520
+ line: line,
2521
+ column: column
2522
+ }
2523
+ end
2524
+
1168
2525
  def parse_lambda_expression(line, column)
1169
2526
  expect(:paren_open) # Skip '('
1170
2527
 
@@ -1292,6 +2649,11 @@ module Vinter
1292
2649
  advance # Skip '{'
1293
2650
  entries = []
1294
2651
 
2652
+ # Handle whitespace after opening brace
2653
+ while current_token && current_token[:type] == :whitespace
2654
+ advance
2655
+ end
2656
+
1295
2657
  # Empty dictionary
1296
2658
  if current_token && current_token[:type] == :brace_close
1297
2659
  advance # Skip '}'
@@ -1304,11 +2666,21 @@ module Vinter
1304
2666
  end
1305
2667
 
1306
2668
  # Parse dictionary entries
1307
- loop do
2669
+ while current_token && current_token[:type] != :brace_close
2670
+ # Skip any backslash line continuation markers and whitespace
2671
+ while current_token && (current_token[:type] == :backslash || current_token[:type] == :whitespace || current_token[:type] == :line_continuation)
2672
+ advance
2673
+ end
2674
+
2675
+ # Break if we reached the end or found closing brace
2676
+ if !current_token || current_token[:type] == :brace_close
2677
+ break
2678
+ end
2679
+
1308
2680
  # Parse key (string or identifier)
1309
2681
  key = nil
1310
2682
  if current_token && (current_token[:type] == :string || current_token[:type] == :identifier)
1311
- key = current_token[:type] == :string ? current_token[:value] : current_token[:value]
2683
+ key = current_token[:value]
1312
2684
  advance # Skip key
1313
2685
  else
1314
2686
  @errors << {
@@ -1317,30 +2689,99 @@ module Vinter
1317
2689
  line: current_token ? current_token[:line] : 0,
1318
2690
  column: current_token ? current_token[:column] : 0
1319
2691
  }
1320
- break
2692
+ # Try to recover by advancing until we find a colon or closing brace
2693
+ while current_token && current_token[:type] != :colon && current_token[:type] != :brace_close
2694
+ advance
2695
+ end
2696
+ if !current_token || current_token[:type] == :brace_close
2697
+ break
2698
+ end
2699
+ end
2700
+
2701
+ # Skip whitespace after key
2702
+ while current_token && current_token[:type] == :whitespace
2703
+ advance
1321
2704
  end
1322
2705
 
1323
2706
  # Expect colon
1324
- expect(:colon)
2707
+ if current_token && current_token[:type] == :colon
2708
+ advance # Skip colon
2709
+ else
2710
+ @errors << {
2711
+ message: "Expected colon after dictionary key",
2712
+ position: @position,
2713
+ line: current_token ? current_token[:line] : 0,
2714
+ column: current_token ? current_token[:column] : 0
2715
+ }
2716
+ end
2717
+
2718
+ # Skip whitespace after colon
2719
+ while current_token && current_token[:type] == :whitespace
2720
+ advance
2721
+ end
1325
2722
 
1326
2723
  # Parse value
1327
- value = parse_expression
2724
+ if current_token && current_token[:type] == :brace_open
2725
+ lambda_or_dict = parse_vim_lambda_or_dict(line, column)
2726
+ value = lambda_or_dict
2727
+ else
2728
+ value = parse_expression
2729
+ end
1328
2730
 
1329
2731
  entries << {
1330
2732
  key: key,
1331
2733
  value: value
1332
2734
  }
1333
2735
 
2736
+ # After parsing an entry, skip whitespace and look for comma or closing brace
2737
+ while current_token && current_token[:type] == :whitespace
2738
+ advance
2739
+ end
2740
+
2741
+ # Check for comma (indicates more entries follow)
1334
2742
  if current_token && current_token[:type] == :comma
1335
- advance # Skip comma
1336
- # Allow trailing comma
1337
- break if current_token && current_token[:type] == :brace_close
2743
+ advance # Skip comma
2744
+ # Skip any line continuations and whitespace after comma
2745
+ while current_token && (current_token[:type] == :whitespace ||
2746
+ current_token[:type] == :backslash ||
2747
+ current_token[:type] == :line_continuation)
2748
+ advance
2749
+ end
1338
2750
  else
1339
- break
2751
+ # No comma - skip any line continuations before checking for closing brace
2752
+ while current_token && (current_token[:type] == :backslash ||
2753
+ current_token[:type] == :line_continuation ||
2754
+ current_token[:type] == :whitespace)
2755
+ advance
2756
+ end
2757
+
2758
+ # After skipping continuations, we should find the closing brace
2759
+ if current_token && current_token[:type] == :brace_close
2760
+ # This is fine - last entry without trailing comma
2761
+ break
2762
+ elsif current_token && current_token[:type] != :brace_close
2763
+ @errors << {
2764
+ message: "Expected comma or closing brace after dictionary entry",
2765
+ position: @position,
2766
+ line: current_token ? current_token[:line] : 0,
2767
+ column: current_token ? current_token[:column] : 0
2768
+ }
2769
+ break
2770
+ end
1340
2771
  end
1341
2772
  end
1342
2773
 
1343
- expect(:brace_close) # Expect and skip '}'
2774
+ # Make sure we have a closing brace
2775
+ if current_token && current_token[:type] == :brace_close
2776
+ advance # Skip '}'
2777
+ else
2778
+ @errors << {
2779
+ message: "Expected closing brace for dictionary",
2780
+ position: @position,
2781
+ line: current_token ? current_token[:line] : 0,
2782
+ column: current_token ? current_token[:column] : 0
2783
+ }
2784
+ end
1344
2785
 
1345
2786
  {
1346
2787
  type: :dict_literal,
@@ -1367,6 +2808,20 @@ module Vinter
1367
2808
 
1368
2809
  # Parse list elements
1369
2810
  while @position < @tokens.length
2811
+ # Handle line continuation characters (backslash)
2812
+ if current_token && [:backslash, :line_continuation].include?(current_token[:type])
2813
+ # Skip the backslash token
2814
+ advance
2815
+
2816
+ # Skip any whitespace that might follow the backslash
2817
+ while current_token && current_token[:type] == :whitespace
2818
+ advance
2819
+ end
2820
+
2821
+ # Don't add a comma - just continue to parse the next element
2822
+ next
2823
+ end
2824
+
1370
2825
  # Check if we've reached the end of the list
1371
2826
  if current_token && current_token[:type] == :bracket_close
1372
2827
  advance # Skip ']'
@@ -1379,23 +2834,26 @@ module Vinter
1379
2834
  # Continue if there's a comma
1380
2835
  if current_token && current_token[:type] == :comma
1381
2836
  advance # Skip comma
2837
+ # Or if the next token is a backslash (line continuation)
1382
2838
  else
1383
- # If no comma and not a closing bracket, then it's an error
1384
- if current_token && current_token[:type] != :bracket_close
1385
- @errors << {
1386
- message: "Expected comma or closing bracket after list element",
1387
- position: @position,
1388
- line: current_token[:line],
1389
- column: current_token[:column]
1390
- }
1391
- end
2839
+ # binding.pry
2840
+ # If no comma and not a closing bracket or backslash, then it's an error
2841
+ # if current_token && current_token[:type] != :bracket_close
2842
+ # @errors << {
2843
+ # message: "Expected comma, backslash, or closing bracket after list element",
2844
+ # position: @position,
2845
+ # line: current_token[:line],
2846
+ # column: current_token[:column]
2847
+ # }
2848
+ # end
1392
2849
 
1393
2850
  # We still want to skip the closing bracket if it's there
1394
2851
  if current_token && current_token[:type] == :bracket_close
1395
2852
  advance
2853
+ break
1396
2854
  end
1397
-
1398
- break
2855
+ advance
2856
+ #next
1399
2857
  end
1400
2858
  end
1401
2859
 
@@ -1451,6 +2909,10 @@ module Vinter
1451
2909
 
1452
2910
  args = []
1453
2911
 
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
+
1454
2916
  # Parse arguments until we find a closing parenthesis
1455
2917
  while @position < @tokens.length && current_token && current_token[:type] != :paren_close
1456
2918
  # Skip comments inside parameter lists
@@ -1458,21 +2920,26 @@ module Vinter
1458
2920
  advance
1459
2921
  next
1460
2922
  end
1461
-
1462
- # Check if the argument might be a lambda expression
1463
- if current_token && current_token[:type] == :paren_open && is_lambda_expression
1464
- arg = parse_lambda_expression(current_token[:line], current_token[:column])
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
+ }
1465
2933
  else
2934
+ # Parse the argument
1466
2935
  arg = parse_expression
2936
+ args << arg if arg
1467
2937
  end
1468
2938
 
1469
- args << arg if arg
1470
-
1471
2939
  # Break if we hit the closing paren
1472
2940
  if current_token && current_token[:type] == :paren_close
1473
2941
  break
1474
2942
  end
1475
-
1476
2943
  # If we have a comma, advance past it and continue
1477
2944
  if current_token && current_token[:type] == :comma
1478
2945
  advance
@@ -1499,6 +2966,54 @@ module Vinter
1499
2966
  }
1500
2967
  end
1501
2968
 
2969
+ def parse_vim_lambda_or_dict(line, column)
2970
+ # Save current position to peek ahead
2971
+ start_position = @position
2972
+
2973
+ advance # Skip opening brace
2974
+
2975
+ # Check if this is a lambda by looking for parameter names followed by arrow
2976
+ is_lambda = false
2977
+ param_names = []
2978
+
2979
+ # Check for immediate arrow (no parameters lambda)
2980
+ if current_token && current_token[:type] == :operator && current_token[:value] == '->'
2981
+ is_lambda = true
2982
+ else
2983
+ # Parse until closing brace or arrow
2984
+ while @position < @tokens.length && current_token[:type] != :brace_close
2985
+ if current_token[:type] == :identifier
2986
+ param_names << current_token[:value]
2987
+ advance
2988
+
2989
+ # Skip comma between parameters
2990
+ if current_token && current_token[:type] == :comma
2991
+ advance
2992
+ next
2993
+ end
2994
+ elsif current_token[:type] == :operator && current_token[:value] == '->'
2995
+ is_lambda = true
2996
+ break
2997
+ else
2998
+ # If we see a colon, this is likely a nested dictionary
2999
+ if current_token && current_token[:type] == :colon
3000
+ break
3001
+ end
3002
+ advance
3003
+ end
3004
+ end
3005
+ end
3006
+
3007
+ # Reset position
3008
+ @position = start_position
3009
+
3010
+ if is_lambda
3011
+ return parse_vim_lambda(line, column)
3012
+ else
3013
+ return parse_dict_literal(line, column)
3014
+ end
3015
+ end
3016
+
1502
3017
  def parse_import_statement
1503
3018
  token = advance # Skip 'import'
1504
3019
  line = token[:line]
@@ -1636,52 +3151,135 @@ module Vinter
1636
3151
  }
1637
3152
  end
1638
3153
 
1639
- def parse_legacy_function
1640
- token = advance # Skip 'function'
3154
+ def parse_command_definition
3155
+ token = advance # Skip 'command' or 'command!'
1641
3156
  line = token[:line]
1642
3157
  column = token[:column]
1643
3158
 
1644
- # Check for bang (!) in function definition
3159
+ # Check if the command has a bang (!)
1645
3160
  has_bang = false
1646
3161
  if current_token && current_token[:type] == :operator && current_token[:value] == '!'
1647
3162
  has_bang = true
1648
3163
  advance # Skip '!'
1649
3164
  end
1650
3165
 
1651
- # For script-local functions or other scoped functions
1652
- is_script_local = false
1653
- function_scope = nil
1654
-
1655
- # Check if we have a script-local function (s:)
1656
- if current_token && current_token[:type] == :script_local
1657
- is_script_local = true
1658
- function_scope = current_token[:value]
1659
- advance # Skip s: prefix
1660
- elsif current_token && current_token[:type] == :identifier && current_token[:value].include?(':')
1661
- # Handle other scoped functions like g: or b:
1662
- parts = current_token[:value].split(':')
1663
- if parts.length == 2
1664
- function_scope = parts[0] + ':'
3166
+ # Parse command options/attributes (starting with hyphen)
3167
+ attributes = []
3168
+ while current_token && (
3169
+ (current_token[:type] == :operator && current_token[:value] == '-') ||
3170
+ (current_token[:type] == :identifier && current_token[:value].start_with?('-'))
3171
+ )
3172
+ # Handle option as a single attribute if it's already combined
3173
+ if current_token[:type] == :identifier && current_token[:value].start_with?('-')
3174
+ attributes << current_token[:value]
1665
3175
  advance
3176
+ else
3177
+ # Otherwise combine the hyphen with the following identifier
3178
+ advance # Skip the hyphen
3179
+ if current_token && current_token[:type] == :identifier
3180
+ attributes << "-#{current_token[:value]}"
3181
+ advance
3182
+ end
1666
3183
  end
1667
- end
1668
-
1669
- # Now handle the function name, which might be separate from the scope
1670
- name = nil
1671
- if !is_script_local && function_scope.nil? && current_token && current_token[:type] == :identifier
1672
- name = current_token
1673
- advance
1674
- elsif is_script_local || !function_scope.nil?
1675
- if current_token && current_token[:type] == :identifier
1676
- name = current_token
1677
- advance
3184
+
3185
+ # If there's an = followed by a value, include it in the attribute
3186
+ if current_token && current_token[:type] == :operator && current_token[:value] == '='
3187
+ attribute = attributes.pop # Take the last attribute we added
3188
+ advance # Skip the '='
3189
+
3190
+ # Get the value (number or identifier)
3191
+ if current_token && (current_token[:type] == :number || current_token[:type] == :identifier)
3192
+ attribute += "=#{current_token[:value]}"
3193
+ attributes << attribute
3194
+ advance
3195
+ end
1678
3196
  end
1679
- else
1680
- name = expect(:identifier)
1681
3197
  end
1682
3198
 
1683
- # Parse parameter list
1684
- expect(:paren_open)
3199
+ # Parse the command name
3200
+ command_name = nil
3201
+ if current_token && current_token[:type] == :identifier
3202
+ command_name = current_token[:value]
3203
+ advance
3204
+ else
3205
+ @errors << {
3206
+ message: "Expected command name",
3207
+ position: @position,
3208
+ line: current_token ? current_token[:line] : 0,
3209
+ column: current_token ? current_token[:column] : 0
3210
+ }
3211
+ end
3212
+
3213
+ # Parse the command implementation - collect all remaining tokens as raw parts
3214
+ implementation_parts = []
3215
+ while @position < @tokens.length
3216
+ # Break on end of line or comment
3217
+ if !current_token || current_token[:type] == :comment ||
3218
+ (current_token[:value] == "\n" && !current_token[:value].start_with?("\\"))
3219
+ break
3220
+ end
3221
+
3222
+ implementation_parts << current_token
3223
+ advance
3224
+ end
3225
+
3226
+ {
3227
+ type: :command_definition,
3228
+ name: command_name,
3229
+ has_bang: has_bang,
3230
+ attributes: attributes,
3231
+ implementation: implementation_parts,
3232
+ line: line,
3233
+ column: column
3234
+ }
3235
+ end
3236
+
3237
+ def parse_legacy_function
3238
+ token = advance # Skip 'function'
3239
+ line = token[:line]
3240
+ column = token[:column]
3241
+
3242
+ # Check for bang (!) in function definition
3243
+ has_bang = false
3244
+ if current_token && current_token[:type] == :operator && current_token[:value] == '!'
3245
+ has_bang = true
3246
+ advance # Skip '!'
3247
+ end
3248
+
3249
+ # For script-local functions or other scoped functions
3250
+ is_script_local = false
3251
+ function_scope = nil
3252
+
3253
+ # Check if we have a script-local function (s:)
3254
+ if current_token && current_token[:type] == :script_local
3255
+ is_script_local = true
3256
+ function_scope = current_token[:value]
3257
+ advance # Skip s: prefix
3258
+ elsif current_token && current_token[:type] == :identifier && current_token[:value].include?(':')
3259
+ # Handle other scoped functions like g: or b:
3260
+ parts = current_token[:value].split(':')
3261
+ if parts.length == 2
3262
+ function_scope = parts[0] + ':'
3263
+ advance
3264
+ end
3265
+ end
3266
+
3267
+ # Now handle the function name, which might be separate from the scope
3268
+ name = nil
3269
+ if !is_script_local && function_scope.nil? && current_token && current_token[:type] == :identifier
3270
+ name = current_token
3271
+ advance
3272
+ elsif is_script_local || !function_scope.nil?
3273
+ if current_token && current_token[:type] == :identifier
3274
+ name = current_token
3275
+ advance
3276
+ end
3277
+ else
3278
+ name = expect(:identifier)
3279
+ end
3280
+
3281
+ # Parse parameter list
3282
+ expect(:paren_open)
1685
3283
  params = parse_parameter_list_legacy
1686
3284
  expect(:paren_close)
1687
3285
 
@@ -1690,7 +3288,7 @@ module Vinter
1690
3288
  while current_token
1691
3289
  if current_token[:type] == :keyword && current_token[:value] == 'abort'
1692
3290
  attributes << advance[:value]
1693
- elsif current_token[:type] == :identifier &&
3291
+ elsif current_token[:type] == :identifier &&
1694
3292
  ['range', 'dict', 'closure'].include?(current_token[:value])
1695
3293
  attributes << advance[:value]
1696
3294
  else
@@ -1711,22 +3309,26 @@ module Vinter
1711
3309
  end
1712
3310
 
1713
3311
  # Expect endfunction/endfunc
1714
- end_token = advance # This should be 'endfunction' or 'endfunc'
1715
- if end_token && end_token[:type] != :keyword &&
1716
- !['endfunction', 'endfunc'].include?(end_token[:value])
1717
- @errors << {
1718
- message: "Expected 'endfunction' or 'endfunc'",
1719
- position: @position,
1720
- line: end_token ? end_token[:line] : 0,
1721
- column: end_token ? end_token[:column] : 0
1722
- }
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
1723
3325
  end
1724
3326
 
1725
3327
  function_name = name ? name[:value] : nil
1726
3328
  if function_scope
1727
3329
  function_name = function_scope + function_name if function_name
1728
3330
  end
1729
-
3331
+
1730
3332
  {
1731
3333
  type: :legacy_function,
1732
3334
  name: function_name,
@@ -1750,54 +3352,820 @@ module Vinter
1750
3352
  return params
1751
3353
  end
1752
3354
 
1753
- loop do
1754
- # Check for ... (varargs) in legacy function
1755
- if current_token && current_token[:type] == :ellipsis
1756
- advance
1757
- params << { type: :var_args_legacy, name: '...' }
1758
- break
3355
+ # Handle special case: varargs at the start of the parameter list
3356
+ if current_token && (
3357
+ (current_token[:type] == :ellipsis) ||
3358
+ (current_token[:type] == :operator && current_token[:value] == '.')
3359
+ )
3360
+ if current_token[:type] == :ellipsis
3361
+ token = advance
3362
+ params << {
3363
+ type: :var_args_legacy,
3364
+ name: '...',
3365
+ line: token[:line],
3366
+ column: token[:column]
3367
+ }
3368
+ return params
3369
+ else
3370
+ # Count consecutive dots
3371
+ dot_count = 0
3372
+ first_dot_token = current_token
3373
+
3374
+ # Store the line and column before we advance
3375
+ dot_line = first_dot_token[:line]
3376
+ dot_column = first_dot_token[:column]
3377
+
3378
+ while current_token && current_token[:type] == :operator && current_token[:value] == '.'
3379
+ dot_count += 1
3380
+ advance
3381
+ end
3382
+
3383
+ if dot_count == 3
3384
+ params << {
3385
+ type: :var_args_legacy,
3386
+ name: '...',
3387
+ line: dot_line,
3388
+ column: dot_column
3389
+ }
3390
+ return params
3391
+ end
1759
3392
  end
3393
+ end
1760
3394
 
3395
+ # Regular parameter parsing (existing logic)
3396
+ loop do
1761
3397
  if current_token && current_token[:type] == :identifier
1762
- # Regular parameter
1763
3398
  param_name = advance
3399
+
3400
+ # Check for default value
3401
+ default_value = nil
3402
+ if current_token && current_token[:type] == :operator && current_token[:value] == '='
3403
+ advance # Skip '='
3404
+ default_value = parse_expression
3405
+ end
3406
+
1764
3407
  params << {
1765
3408
  type: :parameter,
1766
3409
  name: param_name[:value],
3410
+ default_value: default_value,
3411
+ optional: default_value != nil,
1767
3412
  line: param_name[:line],
1768
3413
  column: param_name[:column]
1769
3414
  }
1770
3415
  elsif current_token && current_token[:type] == :arg_variable
1771
- # Parameter with a: prefix
1772
3416
  param_name = advance
1773
- # Extract name without 'a:' prefix
1774
3417
  name_without_prefix = param_name[:value].sub(/^a:/, '')
3418
+
3419
+ # Check for default value
3420
+ default_value = nil
3421
+ if current_token && current_token[:type] == :operator && current_token[:value] == '='
3422
+ advance # Skip '='
3423
+ default_value = parse_expression
3424
+ end
3425
+
1775
3426
  params << {
1776
3427
  type: :parameter,
1777
3428
  name: name_without_prefix,
3429
+ default_value: default_value,
3430
+ optional: default_value != nil,
1778
3431
  line: param_name[:line],
1779
3432
  column: param_name[:column],
1780
3433
  is_arg_prefixed: true
1781
3434
  }
1782
3435
  else
3436
+ # We might be at varargs after other parameters
3437
+ if current_token && (
3438
+ (current_token[:type] == :ellipsis) ||
3439
+ (current_token[:type] == :operator && current_token[:value] == '.')
3440
+ )
3441
+ # Add debug
3442
+ #puts "Found potential varargs token: #{current_token[:type]} #{current_token[:value]}"
3443
+
3444
+ if current_token[:type] == :ellipsis
3445
+ token = current_token # STORE the token before advancing
3446
+ advance
3447
+ params << {
3448
+ type: :var_args_legacy,
3449
+ name: '...',
3450
+ line: token[:line], # Use stored token
3451
+ column: token[:column]
3452
+ }
3453
+ else
3454
+ dot_count = 0
3455
+ first_dot_token = current_token
3456
+
3457
+ # Store line/column BEFORE advancing
3458
+ dot_line = first_dot_token[:line]
3459
+ dot_column = first_dot_token[:column]
3460
+
3461
+ #puts "Starting dot sequence at line #{dot_line}, column #{dot_column}"
3462
+
3463
+ while current_token && current_token[:type] == :operator && current_token[:value] == '.'
3464
+ dot_count += 1
3465
+ #puts "Found dot #{dot_count}"
3466
+ advance
3467
+ end
3468
+
3469
+ if dot_count == 3
3470
+ #puts "Complete varargs found (3 dots)"
3471
+ params << {
3472
+ type: :var_args_legacy,
3473
+ name: '...',
3474
+ line: dot_line, # Use stored values
3475
+ column: dot_column
3476
+ }
3477
+ else
3478
+ #puts "Incomplete varargs: only #{dot_count} dots found"
3479
+ end
3480
+ end
3481
+ break
3482
+ else
3483
+ # Add debug to see what unexpected token we're encountering
3484
+ #puts "Unexpected token in parameter list: #{current_token ? current_token[:type] : 'nil'} #{current_token ? current_token[:value] : ''}"
3485
+
3486
+ # Not a valid parameter or varargs
3487
+ @errors << {
3488
+ message: "Expected parameter name",
3489
+ position: @position,
3490
+ line: current_token ? current_token[:line] : 0,
3491
+ column: current_token ? current_token[:column] : 0
3492
+ }
3493
+ break
3494
+ end
3495
+ end
3496
+ if current_token && current_token[:type] == :comma
3497
+ advance
3498
+ else
3499
+ break
3500
+ end
3501
+ end
3502
+
3503
+ params
3504
+ end
3505
+
3506
+ def parse_try_statement
3507
+ token = advance # Skip 'try'
3508
+ line = token[:line]
3509
+ column = token[:column]
3510
+
3511
+ # Parse the try body
3512
+ body = []
3513
+ catch_clauses = []
3514
+ finally_clause = nil
3515
+
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
+ # Parse catch clauses
3528
+ while @position < @tokens.length &&
3529
+ current_token && current_token[:type] == :keyword &&
3530
+ current_token[:value] == 'catch'
3531
+ catch_token = advance # Skip 'catch'
3532
+ catch_line = catch_token[:line]
3533
+ catch_column = catch_token[:column]
3534
+
3535
+ # Parse the pattern (anything until the next statement)
3536
+ pattern = ''
3537
+ pattern_tokens = []
3538
+
3539
+ # Collect all tokens until we hit a newline or a statement
3540
+ while @position < @tokens.length
3541
+ if !current_token ||
3542
+ (current_token[:type] == :whitespace && current_token[:value].include?("\n")) ||
3543
+ current_token[:type] == :comment
3544
+ break
3545
+ end
3546
+
3547
+ pattern_tokens << current_token
3548
+ pattern += current_token[:value]
3549
+ advance
3550
+ end
3551
+
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
3563
+
3564
+ catch_clauses << {
3565
+ type: :catch_clause,
3566
+ pattern: pattern.strip,
3567
+ body: catch_body,
3568
+ line: catch_line,
3569
+ column: catch_column
3570
+ }
3571
+ end
3572
+
3573
+ # Parse finally clause if present
3574
+ if @position < @tokens.length &&
3575
+ current_token && current_token[:type] == :keyword &&
3576
+ current_token[:value] == 'finally'
3577
+ finally_token = advance # Skip 'finally'
3578
+ finally_line = finally_token[:line]
3579
+ finally_column = finally_token[:column]
3580
+
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
3591
+
3592
+ finally_clause = {
3593
+ type: :finally_clause,
3594
+ body: finally_body,
3595
+ line: finally_line,
3596
+ column: finally_column
3597
+ }
3598
+ end
3599
+
3600
+ # 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
1783
3606
  @errors << {
1784
- message: "Expected parameter name",
3607
+ message: "Expected 'endtry' to close try statement",
1785
3608
  position: @position,
1786
3609
  line: current_token ? current_token[:line] : 0,
1787
3610
  column: current_token ? current_token[:column] : 0
1788
3611
  }
1789
- break
1790
3612
  end
3613
+ end
1791
3614
 
1792
- if current_token && current_token[:type] == :comma
3615
+ return {
3616
+ type: :try_statement,
3617
+ body: body,
3618
+ catch_clauses: catch_clauses,
3619
+ finally_clause: finally_clause,
3620
+ line: line,
3621
+ column: column
3622
+ }
3623
+ end
3624
+
3625
+ # Also add a method to parse throw statements:
3626
+
3627
+ def parse_throw_statement
3628
+ token = advance # Skip 'throw'
3629
+ line = token[:line]
3630
+ column = token[:column]
3631
+
3632
+ # Parse the expression to throw
3633
+ expression = parse_expression
3634
+
3635
+ return {
3636
+ type: :throw_statement,
3637
+ expression: expression,
3638
+ line: line,
3639
+ column: column
3640
+ }
3641
+ end
3642
+
3643
+ def parse_call_statement
3644
+ token = advance # Skip 'call'
3645
+ line = token[:line]
3646
+ column = token[:column]
3647
+
3648
+ # Parse the function call expression that follows 'call'
3649
+ func_expr = nil
3650
+
3651
+ if current_token && current_token[:type] == :script_local
3652
+ # Handle script-local function call (s:func_name)
3653
+ func_name = current_token[:value]
3654
+ func_line = current_token[:line]
3655
+ func_column = current_token[:column]
3656
+ advance
3657
+
3658
+ # Parse arguments
3659
+ args = []
3660
+ if current_token && current_token[:type] == :paren_open
3661
+ expect(:paren_open) # Skip '('
3662
+
3663
+ # Parse arguments if any
3664
+ unless current_token && current_token[:type] == :paren_close
3665
+ loop do
3666
+ arg = parse_expression
3667
+ args << arg if arg
3668
+
3669
+ if current_token && current_token[:type] == :comma
3670
+ advance # Skip comma
3671
+ else
3672
+ break
3673
+ end
3674
+ end
3675
+ end
3676
+
3677
+ expect(:paren_close) # Skip ')'
3678
+ end
3679
+
3680
+ func_expr = {
3681
+ type: :script_local_call,
3682
+ name: func_name,
3683
+ arguments: args,
3684
+ line: func_line,
3685
+ column: func_column
3686
+ }
3687
+ else
3688
+ # For other function calls
3689
+ func_expr = parse_expression
3690
+ end
3691
+
3692
+ {
3693
+ type: :call_statement,
3694
+ expression: func_expr,
3695
+ line: line,
3696
+ column: column
3697
+ }
3698
+ end
3699
+
3700
+ def parse_string
3701
+ # Start with the first string or expression
3702
+ left = parse_primary_term
3703
+
3704
+ # Continue as long as we see the '.' or '..' string concatenation operator
3705
+ while current_token && current_token[:type] == :operator &&
3706
+ (current_token[:value] == '.' || current_token[:value] == '..')
3707
+
3708
+ # Store the operator token
3709
+ op_token = advance # Skip the operator
3710
+ op = op_token[:value]
3711
+
3712
+ # If this is the dot operator, check if it's actually part of '..'
3713
+ if op == '.' && peek_token && peek_token[:type] == :operator && peek_token[:value] == '.'
3714
+ advance # Skip the second dot
3715
+ op = '..'
3716
+ end
3717
+
3718
+ # Parse the right side of the concatenation
3719
+ right = parse_primary_term
3720
+
3721
+ # Create a string concatenation expression
3722
+ left = {
3723
+ type: :string_concatenation,
3724
+ operator: op,
3725
+ left: left,
3726
+ right: right,
3727
+ line: op_token[:line],
3728
+ column: op_token[:column]
3729
+ }
3730
+ end
3731
+
3732
+ return left
3733
+ end
3734
+
3735
+ # Helper function to parse a primary term in string expressions
3736
+ def parse_primary_term
3737
+ if !current_token
3738
+ return nil
3739
+ end
3740
+
3741
+ token = current_token
3742
+ line = token[:line]
3743
+ column = token[:column]
3744
+
3745
+ case token[:type]
3746
+ when :string
3747
+ # Handle string literal
3748
+ advance
3749
+ return {
3750
+ type: :literal,
3751
+ value: token[:value],
3752
+ token_type: :string,
3753
+ line: line,
3754
+ column: column
3755
+ }
3756
+ when :identifier, :global_variable, :script_local, :arg_variable,
3757
+ :local_variable, :buffer_local, :window_local, :tab_local
3758
+ # Handle variable references
3759
+ advance
3760
+ return {
3761
+ type: token[:type],
3762
+ name: token[:value],
3763
+ line: line,
3764
+ column: column
3765
+ }
3766
+ when :paren_open
3767
+ # Handle parenthesized expressions
3768
+ advance # Skip '('
3769
+ expr = parse_expression
3770
+ expect(:paren_close) # Skip ')'
3771
+ return expr
3772
+ when :function
3773
+ # Handle function calls
3774
+ return parse_function_call(token[:value], line, column)
3775
+ else
3776
+ # For anything else, use the standard expression parser
3777
+ return parse_expression
3778
+ end
3779
+ end
3780
+
3781
+ def parse_silent_command
3782
+ token = advance # Skip 'silent'
3783
+ line = token[:line]
3784
+ column = token[:column]
3785
+
3786
+ # Check for ! after silent
3787
+ has_bang = false
3788
+ if current_token && current_token[:type] == :operator && current_token[:value] == '!'
3789
+ has_bang = true
3790
+ advance # Skip '!'
3791
+ end
3792
+
3793
+ # Now parse the command that follows silent
3794
+ # It could be a standard command or a command with a range
3795
+ command = nil
3796
+
3797
+ # Check if the next token is a range operator (% in this case)
3798
+ if current_token && current_token[:type] == :operator && current_token[:value] == '%'
3799
+ range_token = advance # Skip '%'
3800
+
3801
+ # Now we expect a command (like 'delete')
3802
+ if current_token &&
3803
+ (current_token[:type] == :keyword || current_token[:type] == :identifier)
3804
+ cmd_token = advance # Skip the command name
3805
+ cmd_name = cmd_token[:value]
3806
+
3807
+ # Parse any arguments to the command
3808
+ args = []
3809
+ while current_token &&
3810
+ current_token[:type] != :comment &&
3811
+ (current_token[:value] != "\n" ||
3812
+ (current_token[:value] == "\n" &&
3813
+ !current_token[:value].start_with?("\\")))
3814
+
3815
+ # Add the token as an argument
3816
+ args << current_token
3817
+ advance
3818
+ end
3819
+
3820
+ command = {
3821
+ type: :range_command,
3822
+ range: '%',
3823
+ command: {
3824
+ type: :command,
3825
+ name: cmd_name,
3826
+ args: args,
3827
+ line: cmd_token[:line],
3828
+ column: cmd_token[:column]
3829
+ },
3830
+ line: range_token[:line],
3831
+ column: range_token[:column]
3832
+ }
3833
+ else
3834
+ @errors << {
3835
+ message: "Expected command after range operator '%'",
3836
+ position: @position,
3837
+ line: current_token ? current_token[:line] : range_token[:line],
3838
+ column: current_token ? current_token[:column] : range_token[:column] + 1
3839
+ }
3840
+ end
3841
+ else
3842
+ # Parse a regular command
3843
+ command = parse_statement
3844
+ end
3845
+
3846
+ return {
3847
+ type: :silent_command,
3848
+ has_bang: has_bang,
3849
+ command: command,
3850
+ line: line,
3851
+ column: column
3852
+ }
3853
+ end
3854
+
3855
+ def parse_delete_command
3856
+ token = advance # Skip 'delete'
3857
+ line = token[:line]
3858
+ column = token[:column]
3859
+
3860
+ # Check for register argument (could be an underscore '_')
3861
+ register = nil
3862
+ if current_token
3863
+ if current_token[:type] == :identifier && current_token[:value] == '_'
3864
+ register = current_token[:value]
3865
+ advance
3866
+ elsif current_token[:type] == :operator && current_token[:value] == '_'
3867
+ # Handle underscore as an operator (some lexers might classify it this way)
3868
+ register = current_token[:value]
3869
+ advance
3870
+ end
3871
+ end
3872
+
3873
+ return {
3874
+ type: :delete_command,
3875
+ register: register,
3876
+ line: line,
3877
+ column: column
3878
+ }
3879
+ end
3880
+
3881
+ def parse_brace_sequence_value
3882
+ # Handle special brace sequences like {{{,}}} for foldmarker
3883
+ value_parts = []
3884
+
3885
+ while current_token && (current_token[:type] == :brace_open ||
3886
+ current_token[:type] == :brace_close ||
3887
+ current_token[:type] == :comma)
3888
+ value_parts << current_token[:value]
3889
+ advance
3890
+ end
3891
+
3892
+ return {
3893
+ type: :brace_sequence,
3894
+ value: value_parts.join(''),
3895
+ line: current_token ? current_token[:line] : 0,
3896
+ column: current_token ? current_token[:column] : 0
3897
+ }
3898
+ end
3899
+
3900
+ def parse_comma_separated_value
3901
+ # Handle comma-separated option values like menu,menuone,noinsert,noselect
3902
+ values = []
3903
+
3904
+ # Parse first value
3905
+ if current_token && current_token[:type] == :identifier
3906
+ values << current_token[:value]
3907
+ advance
3908
+ else
3909
+ return parse_expression # Fall back to regular expression parsing
3910
+ end
3911
+
3912
+ # Parse additional comma-separated values
3913
+ while current_token && current_token[:type] == :comma
3914
+ advance # Skip comma
3915
+ if current_token && current_token[:type] == :identifier
3916
+ values << current_token[:value]
1793
3917
  advance
1794
3918
  else
1795
3919
  break
1796
3920
  end
1797
3921
  end
3922
+
3923
+ return {
3924
+ type: :comma_separated_value,
3925
+ values: values,
3926
+ line: current_token ? current_token[:line] : 0,
3927
+ column: current_token ? current_token[:column] : 0
3928
+ }
3929
+ end
1798
3930
 
1799
- params
3931
+ def parse_set_command
3932
+ token = advance # Skip 'set'
3933
+ line = token[:line]
3934
+ column = token[:column]
3935
+
3936
+ # Parse option name
3937
+ option_name = nil
3938
+ if current_token && current_token[:type] == :identifier
3939
+ option_name = current_token[:value]
3940
+ advance
3941
+ elsif current_token && current_token[:type] == :option_variable
3942
+ # Handle option variable directly (like &option)
3943
+ option_name = current_token[:value].sub(/^&/, '')
3944
+ advance
3945
+ else
3946
+ @errors << {
3947
+ message: "Expected option name after 'set'",
3948
+ position: @position,
3949
+ line: current_token ? current_token[:line] : line,
3950
+ column: current_token ? current_token[:column] : column + 3
3951
+ }
3952
+ return {
3953
+ type: :set_command,
3954
+ option: nil,
3955
+ value: nil,
3956
+ reset_to_vim_default: false,
3957
+ line: line,
3958
+ column: column
3959
+ }
3960
+ end
3961
+
3962
+ # Check for &vim suffix (reset to Vim default)
3963
+ reset_to_vim_default = false
3964
+ if current_token && current_token[:type] == :option_variable &&
3965
+ (current_token[:value] == '&vim' || current_token[:value] == '&')
3966
+ reset_to_vim_default = true
3967
+ advance
3968
+ end
3969
+
3970
+ # Check for equals sign and value
3971
+ value = nil
3972
+ if current_token && current_token[:type] == :operator && current_token[:value] == '='
3973
+ advance # Skip '='
3974
+
3975
+ # Special handling for foldmarker and similar options that use brace notation
3976
+ if option_name == 'foldmarker' && current_token && current_token[:type] == :brace_open
3977
+ # Parse as special brace sequence value (e.g., {{{,}}})
3978
+ value = parse_brace_sequence_value
3979
+ # Special handling for comma-separated option values
3980
+ elsif ['completeopt', 'formatoptions', 'guioptions', 'wildoptions'].include?(option_name)
3981
+ value = parse_comma_separated_value
3982
+ else
3983
+ value = parse_expression
3984
+ end
3985
+ elsif current_token && current_token[:type] == :operator && current_token[:value] == '-'
3986
+ # Handle syntaxes like 'set option-=value'
3987
+ op = current_token[:value]
3988
+ advance # Skip the operator
3989
+
3990
+ if current_token && current_token[:type] == :operator &&
3991
+ ['+', '=', '^'].include?(current_token[:value])
3992
+ op += current_token[:value]
3993
+ advance # Skip the second part of the operator
3994
+ end
3995
+
3996
+ value = {
3997
+ type: :option_operation,
3998
+ operator: op,
3999
+ value: parse_expression,
4000
+ line: line,
4001
+ column: column
4002
+ }
4003
+ elsif current_token && current_token[:type] == :operator && current_token[:value] == '+'
4004
+ # Handle syntaxes like 'set option+=value'
4005
+ op = current_token[:value]
4006
+ advance # Skip the operator
4007
+
4008
+ if current_token && current_token[:type] == :operator && current_token[:value] == '='
4009
+ op += current_token[:value]
4010
+ advance # Skip the '=' part
4011
+ end
4012
+
4013
+ value = {
4014
+ type: :option_operation,
4015
+ operator: op,
4016
+ value: parse_expression,
4017
+ line: line,
4018
+ column: column
4019
+ }
4020
+ end
4021
+
4022
+ return {
4023
+ type: :set_command,
4024
+ option: option_name,
4025
+ value: value,
4026
+ reset_to_vim_default: reset_to_vim_default,
4027
+ line: line,
4028
+ column: column
4029
+ }
4030
+ end
4031
+
4032
+ def parse_syntax_command
4033
+ token = advance # Skip 'syntax'
4034
+ line = token[:line]
4035
+ column = token[:column]
4036
+
4037
+ # Parse syntax subcommand (match, region, keyword, etc.)
4038
+ subcommand = nil
4039
+ if current_token && current_token[:type] == :identifier
4040
+ subcommand = current_token[:value]
4041
+ advance
4042
+ end
4043
+
4044
+ # Parse syntax group name
4045
+ group_name = nil
4046
+ if current_token && current_token[:type] == :identifier
4047
+ group_name = current_token[:value]
4048
+ advance
4049
+ end
4050
+
4051
+ # Parse pattern (regex or other pattern)
4052
+ pattern = nil
4053
+ if current_token && current_token[:type] == :regex
4054
+ pattern = current_token[:value]
4055
+ advance
4056
+ end
4057
+
4058
+ # Parse any remaining arguments as raw text until end of line
4059
+ args = []
4060
+ while current_token && current_token[:type] != :comment
4061
+ args << current_token[:value]
4062
+ advance
4063
+ end
4064
+
4065
+ {
4066
+ type: :syntax_command,
4067
+ subcommand: subcommand,
4068
+ group_name: group_name,
4069
+ pattern: pattern,
4070
+ args: args.join(' ').strip,
4071
+ line: line,
4072
+ column: column
4073
+ }
4074
+ end
4075
+
4076
+ def parse_highlight_command
4077
+ token = advance # Skip 'highlight'
4078
+ line = token[:line]
4079
+ column = token[:column]
4080
+
4081
+ # Parse highlight group name
4082
+ group_name = nil
4083
+ if current_token && current_token[:type] == :identifier
4084
+ group_name = current_token[:value]
4085
+ advance
4086
+ end
4087
+
4088
+ # Parse highlight attributes as key=value pairs
4089
+ attributes = {}
4090
+ while current_token && current_token[:type] != :comment
4091
+ # Parse attribute name
4092
+ if current_token && current_token[:type] == :identifier
4093
+ attr_name = current_token[:value]
4094
+ advance
4095
+
4096
+ # Parse = sign
4097
+ if current_token && current_token[:type] == :operator && current_token[:value] == '='
4098
+ advance
4099
+
4100
+ # Parse attribute value (can be identifier, number, or hex color)
4101
+ if current_token && (current_token[:type] == :identifier ||
4102
+ current_token[:type] == :number ||
4103
+ current_token[:type] == :hex_color)
4104
+ attributes[attr_name] = current_token[:value]
4105
+ advance
4106
+ end
4107
+ end
4108
+ else
4109
+ advance
4110
+ end
4111
+ end
4112
+
4113
+ {
4114
+ type: :highlight_command,
4115
+ group_name: group_name,
4116
+ attributes: attributes,
4117
+ line: line,
4118
+ column: column
4119
+ }
4120
+ end
4121
+
4122
+ def parse_sleep_command
4123
+ token = advance # Skip 'sleep'
4124
+ line = token[:line]
4125
+ column = token[:column]
4126
+
4127
+ # Parse duration (number with optional suffix)
4128
+ duration = nil
4129
+ if current_token && current_token[:type] == :number
4130
+ duration = current_token[:value]
4131
+ advance
4132
+ else
4133
+ @errors << {
4134
+ message: "Expected duration after 'sleep'",
4135
+ position: @position,
4136
+ line: current_token ? current_token[:line] : line,
4137
+ column: current_token ? current_token[:column] : column + 5
4138
+ }
4139
+ end
4140
+
4141
+ {
4142
+ type: :sleep_command,
4143
+ duration: duration,
4144
+ line: line,
4145
+ column: column
4146
+ }
1800
4147
  end
1801
4148
 
4149
+ def parse_source_command
4150
+ token = advance # Skip 'source'
4151
+ line = token[:line]
4152
+ column = token[:column]
4153
+
4154
+ # Parse file path - collect all tokens until end of line/comment
4155
+ path_parts = []
4156
+ while current_token && current_token[:type] != :comment
4157
+ path_parts << current_token[:value]
4158
+ advance
4159
+ end
4160
+
4161
+ file_path = path_parts.join('')
4162
+
4163
+ {
4164
+ type: :source_command,
4165
+ file_path: file_path,
4166
+ line: line,
4167
+ column: column
4168
+ }
4169
+ end
1802
4170
  end
1803
4171
  end