vinter 0.2.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'
@@ -93,6 +265,34 @@ module Vinter
93
265
  when 'vim9script'
94
266
  token = advance # Skip 'vim9script'
95
267
  { type: :vim9script_declaration, line: token[:line], column: token[:column] }
268
+ when 'autocmd'
269
+ parse_autocmd_statement
270
+ when 'execute', 'exec'
271
+ parse_execute_statement
272
+ when 'let'
273
+ parse_let_statement
274
+ when 'unlet'
275
+ parse_unlet_statement
276
+ when 'echohl', 'echomsg', 'echoerr', 'echom'
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
96
296
  else
97
297
  @warnings << {
98
298
  message: "Unexpected keyword: #{current_token[:value]}",
@@ -104,9 +304,35 @@ module Vinter
104
304
  nil
105
305
  end
106
306
  elsif current_token[:type] == :identifier
107
- parse_expression_statement
307
+ if current_token[:value] == "echo"
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
317
+ else
318
+ parse_expression_statement
319
+ end
108
320
  elsif current_token[:type] == :comment
109
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
110
336
  else
111
337
  @warnings << {
112
338
  message: "Unexpected token type: #{current_token[:type]}",
@@ -119,822 +345,3827 @@ module Vinter
119
345
  end
120
346
  end
121
347
 
122
- def parse_comment
123
- comment = current_token[:value]
124
- line = current_token[:line]
125
- column = current_token[:column]
126
- advance
127
- { type: :comment, value: comment, line: line, column: column }
128
- end
129
-
130
- def parse_if_statement
131
- token = advance # Skip 'if'
348
+ def parse_range_command
349
+ token = advance # Skip '%'
132
350
  line = token[:line]
133
351
  column = token[:column]
134
- condition = parse_expression
135
352
 
136
- then_branch = []
137
- else_branch = []
353
+ # Parse the command that follows the range
354
+ command = parse_statement
138
355
 
139
- # Parse statements until we hit 'else', 'elseif', or 'endif'
140
- while @position < @tokens.length
141
- if current_token[:type] == :keyword &&
142
- ['else', 'elseif', 'endif'].include?(current_token[:value])
143
- break
144
- end
356
+ {
357
+ type: :range_command,
358
+ range: '%',
359
+ command: command,
360
+ line: line,
361
+ column: column
362
+ }
363
+ end
145
364
 
146
- stmt = parse_statement
147
- then_branch << stmt if stmt
148
- end
365
+ def parse_runtime_statement
366
+ token = advance # Skip 'runtime' or 'runtime!'
367
+ line = token[:line]
368
+ column = token[:column]
149
369
 
150
- # Check for else/elseif
151
- if current_token && current_token[:type] == :keyword
152
- if current_token[:value] == 'else'
153
- advance # Skip 'else'
370
+ is_bang = token[:type] == :runtime_command ||
371
+ (token[:value] == 'runtime' && current_token && current_token[:type] == :operator && current_token[:value] == '!')
154
372
 
155
- # Parse statements until 'endif'
156
- while @position < @tokens.length
157
- if current_token[:type] == :keyword && current_token[:value] == 'endif'
158
- break
159
- end
373
+ # Skip the '!' if it's separate token
374
+ if token[:type] != :runtime_command && is_bang
375
+ advance # Skip '!'
376
+ end
160
377
 
161
- stmt = parse_statement
162
- else_branch << stmt if stmt
163
- end
164
- elsif current_token[:value] == 'elseif'
165
- # This is a simplified handling - elseif should be treated as a nested if
166
- else_branch << parse_if_statement
167
- end
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
168
384
  end
169
385
 
170
- # Expect endif
171
- expect(:keyword) # This should be 'endif'
386
+ pattern = pattern_parts.join('').strip
172
387
 
173
388
  {
174
- type: :if_statement,
175
- condition: condition,
176
- then_branch: then_branch,
177
- else_branch: else_branch,
389
+ type: :runtime_statement,
390
+ bang: is_bang,
391
+ pattern: pattern,
178
392
  line: line,
179
393
  column: column
180
394
  }
181
395
  end
182
396
 
183
- def parse_while_statement
184
- token = advance # Skip 'while'
397
+ def parse_filter_command
398
+ token = advance # Skip 'execute'
185
399
  line = token[:line]
186
400
  column = token[:column]
187
- condition = parse_expression
188
-
189
- body = []
190
-
191
- # Parse statements until we hit 'endwhile'
192
- while @position < @tokens.length
193
- if current_token[:type] == :keyword && current_token[:value] == 'endwhile'
194
- break
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
195
410
  end
196
-
197
- stmt = parse_statement
198
- body << stmt if stmt
199
411
  end
412
+ advance
200
413
 
201
- # Expect endwhile
202
- expect(:keyword) # This should be 'endwhile'
414
+ # Parse arguments - typically string expressions with concatenation
415
+ # Just accept any tokens until we hit a statement terminator or another command
416
+ #expressions = []
417
+ #expr = parse_expression
418
+ #expr = parse_string
419
+ #expressions << expr if expr
203
420
 
421
+ # Return the execute statement
204
422
  {
205
- type: :while_statement,
206
- condition: condition,
207
- body: body,
423
+ type: :filter_command,
424
+ #expressions: expressions,
208
425
  line: line,
209
426
  column: column
210
427
  }
211
428
  end
212
429
 
213
- def parse_for_statement
214
- token = advance # Skip 'for'
430
+ def old_parse_filter_command
431
+ token = advance # Skip 'filter' or 'filt'
215
432
  line = token[:line]
216
433
  column = token[:column]
217
434
 
218
- # Parse the loop variable(s)
219
- if current_token && current_token[:type] == :paren_open
220
- # Handle tuple assignment: for (key, val) in dict
221
- advance # Skip '('
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
222
441
 
223
- loop_vars = []
442
+ # Parse the pattern
443
+ pattern = nil
444
+ pattern_delimiter = nil
224
445
 
225
- loop do
226
- if current_token && current_token[:type] == :identifier
227
- loop_vars << advance[:value]
228
- else
229
- @errors << {
230
- message: "Expected identifier in for loop variables",
231
- position: @position,
232
- line: current_token ? current_token[:line] : 0,
233
- column: current_token ? current_token[:column] : 0
234
- }
235
- break
236
- end
446
+ if current_token && current_token[:type] == :operator && current_token[:value] == '/'
447
+ # Handle /pattern/ form
448
+ pattern_delimiter = '/'
449
+ advance # Skip opening delimiter
237
450
 
238
- if current_token && current_token[:type] == :comma
239
- advance # Skip ','
240
- else
241
- break
242
- end
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
243
457
  end
244
458
 
245
- expect(:paren_close) # Skip ')'
459
+ pattern = pattern_parts.join('')
246
460
 
247
- if !current_token || (current_token[:type] != :identifier || current_token[:value] != 'in')
461
+ # Skip closing delimiter
462
+ if current_token && current_token[:type] == :operator && current_token[:value] == pattern_delimiter
463
+ advance
464
+ else
248
465
  @errors << {
249
- message: "Expected 'in' after for loop variables",
466
+ message: "Expected closing pattern delimiter: #{pattern_delimiter}",
250
467
  position: @position,
251
468
  line: current_token ? current_token[:line] : 0,
252
469
  column: current_token ? current_token[:column] : 0
253
470
  }
254
- else
255
- advance # Skip 'in'
256
471
  end
257
-
258
- iterable = parse_expression
259
-
260
- # Parse the body until 'endfor'
261
- body = []
472
+ else
473
+ # Handle direct pattern form (without delimiters)
474
+ # Parse until we see what appears to be the command
475
+ pattern_parts = []
262
476
  while @position < @tokens.length
263
- if current_token[:type] == :keyword && current_token[:value] == 'endfor'
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]))
264
482
  break
265
483
  end
266
484
 
267
- stmt = parse_statement
268
- body << stmt if stmt
269
- end
270
-
271
- # Expect endfor
272
- expect(:keyword) # This should be 'endfor'
273
-
274
- return {
275
- type: :for_statement,
276
- loop_vars: loop_vars,
277
- iterable: iterable,
278
- body: body,
279
- line: line,
280
- column: column
281
- }
282
- else
283
- # Simple for var in list
284
- if !current_token || current_token[:type] != :identifier
285
- @errors << {
286
- message: "Expected identifier as for loop variable",
287
- position: @position,
288
- line: current_token ? current_token[:line] : 0,
289
- column: current_token ? current_token[:column] : 0
290
- }
291
- return nil
485
+ pattern_parts << current_token[:value]
486
+ advance
292
487
  end
293
488
 
294
- loop_var = advance[:value]
295
-
296
- if !current_token || (current_token[:type] != :identifier || current_token[:value] != 'in')
297
- @errors << {
298
- message: "Expected 'in' after for loop variable",
299
- position: @position,
300
- line: current_token ? current_token[:line] : 0,
301
- column: current_token ? current_token[:column] : 0
302
- }
303
- else
304
- advance # Skip 'in'
305
- end
489
+ pattern = pattern_parts.join('').strip
490
+ end
306
491
 
307
- iterable = parse_expression
492
+ # Parse the command to be filtered
493
+ command = parse_statement
308
494
 
309
- # Parse the body until 'endfor'
310
- body = []
311
- while @position < @tokens.length
312
- if current_token[:type] == :keyword && current_token[:value] == 'endfor'
313
- break
314
- end
495
+ {
496
+ type: :filter_command,
497
+ pattern: pattern,
498
+ has_bang: has_bang,
499
+ command: command,
500
+ line: line,
501
+ column: column
502
+ }
503
+ end
315
504
 
316
- stmt = parse_statement
317
- body << stmt if stmt
318
- end
505
+ def parse_augroup_statement
506
+ token = advance # Skip 'augroup'
507
+ line = token[:line]
508
+ column = token[:column]
319
509
 
320
- # Expect endfor
321
- expect(:keyword) # This should be 'endfor'
510
+ # Get the augroup name
511
+ name = nil
512
+ if current_token && current_token[:type] == :identifier
513
+ name = current_token[:value]
514
+ advance
515
+ else
516
+ @errors << {
517
+ message: "Expected augroup name",
518
+ position: @position,
519
+ line: current_token ? current_token[:line] : 0,
520
+ column: current_token ? current_token[:column] : 0
521
+ }
522
+ end
322
523
 
524
+ # Check for augroup END
525
+ is_end_marker = false
526
+ if name && (name.upcase == "END" || name == "END")
527
+ is_end_marker = true
323
528
  return {
324
- type: :for_statement,
325
- loop_var: loop_var,
326
- iterable: iterable,
327
- body: body,
529
+ type: :augroup_end,
328
530
  line: line,
329
531
  column: column
330
532
  }
331
533
  end
332
- end
333
-
334
- def parse_def_function
335
- token = advance # Skip 'def'
336
- line = token[:line]
337
- column = token[:column]
338
-
339
- name = expect(:identifier)
340
-
341
- # Parse parameter list
342
- expect(:paren_open)
343
- params = parse_parameter_list
344
- expect(:paren_close)
345
-
346
- # Parse optional return type
347
- return_type = nil
348
- if current_token && current_token[:type] == :colon
349
- advance # Skip ':'
350
- return_type = parse_type
351
- end
352
534
 
353
- # Parse function body
535
+ # Parse statements within the augroup until we find 'augroup END'
354
536
  body = []
355
537
  while @position < @tokens.length
356
- if current_token[:type] == :keyword && current_token[:value] == 'enddef'
357
- break
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
549
+ end
358
550
  end
359
551
 
360
552
  stmt = parse_statement
361
553
  body << stmt if stmt
362
554
  end
363
555
 
364
- # Expect enddef
365
- expect(:keyword) # This should be 'enddef'
366
-
367
556
  {
368
- type: :def_function,
369
- name: name ? name[:value] : nil,
370
- params: params,
371
- return_type: return_type,
557
+ type: :augroup_statement,
558
+ name: name,
372
559
  body: body,
373
560
  line: line,
374
561
  column: column
375
562
  }
376
563
  end
377
564
 
565
+ def parse_execute_statement
566
+ token = advance # Skip 'execute'
567
+ line = token[:line]
568
+ column = token[:column]
378
569
 
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
379
576
 
380
- def parse_parameter_list
381
- params = []
577
+ # Return the execute statement
578
+ {
579
+ type: :execute_statement,
580
+ expressions: expressions,
581
+ line: line,
582
+ column: column
583
+ }
584
+ end
382
585
 
383
- # Empty parameter list
384
- if current_token && current_token[:type] == :paren_close
385
- return params
386
- end
586
+ def parse_let_statement
587
+ # binding.pry
588
+ token = advance # Skip 'let'
589
+ line = token[:line]
590
+ column = token[:column]
387
591
 
388
- loop do
389
- # Check for variable args
390
- if current_token && current_token[:type] == :ellipsis
391
- ellipsis_token = advance
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
392
604
 
393
- # Parse type for variable args if present
394
- param_type = nil
395
- if current_token && current_token[:type] == :colon
396
- advance # Skip ':'
397
- param_type = parse_type
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
398
629
  end
399
630
 
400
- params << {
401
- type: :var_args,
402
- param_type: param_type,
403
- line: ellipsis_token[:line],
404
- column: ellipsis_token[:column]
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]
405
652
  }
406
- break
407
- end
653
+ advance
654
+ when :bracket_open
655
+ # Handle array destructuring like: let [key, value] = split(header, ': ')
656
+ bracket_token = advance # Skip '['
657
+
658
+ # Parse the variable names inside the brackets
659
+ destructuring_targets = []
660
+
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
667
+ end
408
668
 
409
- # Get parameter name
410
- if !current_token || current_token[:type] != :identifier
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
411
709
  @errors << {
412
- message: "Expected parameter name",
710
+ message: "Expected variable name after let",
413
711
  position: @position,
414
712
  line: current_token ? current_token[:line] : 0,
415
713
  column: current_token ? current_token[:column] : 0
416
714
  }
417
- break
418
715
  end
716
+ end
419
717
 
420
- param_name = advance
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
421
733
 
422
- # Check for type annotation
423
- param_type = nil
424
- if current_token && current_token[:type] == :colon
425
- advance # Skip ':'
426
- param_type = parse_type
427
- end
734
+ # Parse the value expression
735
+ value = nil
428
736
 
429
- # Check for default value
430
- default_value = nil
431
- if current_token && current_token[:type] == :operator && current_token[:value] == '='
432
- advance # Skip '='
433
- default_value = parse_expression
434
- end
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'
435
740
 
436
- params << {
437
- type: :parameter,
438
- name: param_name[:value],
439
- param_type: param_type,
440
- optional: false, # Set this based on default value
441
- default_value: default_value,
442
- line: param_name[:line],
443
- column: param_name[:column]
444
- }
741
+ # Expect opening parenthesis
742
+ if current_token && current_token[:type] == :paren_open
743
+ paren_token = advance # Skip '('
445
744
 
446
- if current_token && current_token[:type] == :comma
447
- advance
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
+ }
448
768
  else
449
- break
769
+ # If not followed by parenthesis, parse as a normal expression
770
+ @position -= 1 # Go back to the 'function' keyword
771
+ value = parse_expression
450
772
  end
773
+ else
774
+ # Normal expression parsing
775
+ value = parse_expression
451
776
  end
452
-
453
- params
777
+ {
778
+ type: :let_statement,
779
+ target: target,
780
+ operator: operator,
781
+ value: value,
782
+ line: line,
783
+ column: column
784
+ }
454
785
  end
455
786
 
456
- def parse_type
457
- if current_token && current_token[:type] == :identifier
458
- type_name = advance
787
+ def parse_unlet_statement
788
+ token = advance # Skip 'unlet'
789
+ line = token[:line]
790
+ column = token[:column]
459
791
 
460
- # Handle generic types like list<string>
461
- if current_token && current_token[:type] == :operator && current_token[:value] == '<'
462
- advance # Skip '<'
463
- inner_type = parse_type
464
- expect(:operator) # This should be '>'
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
465
804
 
466
- return {
467
- type: :generic_type,
468
- base_type: type_name[:value],
469
- inner_type: inner_type,
470
- line: type_name[:line],
471
- column: type_name[:column]
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
472
826
  }
473
827
  end
474
-
475
- return type_name[:value]
476
- else
477
- @errors << {
478
- message: "Expected type identifier",
479
- position: @position,
480
- line: current_token ? current_token[:line] : 0,
481
- column: current_token ? current_token[:column] : 0
482
- }
483
- advance
484
- return "unknown"
485
828
  end
829
+
830
+ {
831
+ type: :unlet_statement,
832
+ target: target,
833
+ line: line,
834
+ column: column
835
+ }
486
836
  end
487
837
 
488
- def parse_variable_declaration
489
- var_type_token = advance # Skip 'var', 'const', or 'final'
490
- var_type = var_type_token[:value]
491
- line = var_type_token[:line]
492
- column = var_type_token[:column]
838
+ def parse_autocmd_statement
839
+ token = advance # Skip 'autocmd'
840
+ line = token[:line]
841
+ column = token[:column]
493
842
 
494
- if !current_token || current_token[:type] != :identifier
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
495
848
  @errors << {
496
- message: "Expected variable name",
849
+ message: "Expected event name after 'autocmd'",
497
850
  position: @position,
498
851
  line: current_token ? current_token[:line] : 0,
499
852
  column: current_token ? current_token[:column] : 0
500
853
  }
501
- return nil
502
854
  end
503
855
 
504
- name_token = advance
505
- name = name_token[:value]
506
-
507
- # Parse optional type annotation
508
- var_type_annotation = nil
509
- if current_token && current_token[:type] == :colon
510
- advance # Skip ':'
511
- var_type_annotation = parse_type
856
+ # Parse pattern (like *.match or *)
857
+ pattern = nil
858
+ if current_token
859
+ pattern = current_token[:value]
860
+ advance
512
861
  end
513
862
 
514
- # Parse initializer if present
515
- initializer = nil
516
- if current_token && current_token[:type] == :operator && current_token[:value] == '='
517
- advance # Skip '='
518
- initializer = parse_expression
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
519
873
  end
520
874
 
521
- {
522
- type: :variable_declaration,
523
- var_type: var_type,
524
- name: name,
525
- var_type_annotation: var_type_annotation,
526
- initializer: initializer,
875
+ return {
876
+ type: :autocmd_statement,
877
+ event: event,
878
+ pattern: pattern,
879
+ command_parts: command_parts,
527
880
  line: line,
528
881
  column: column
529
882
  }
530
883
  end
531
884
 
532
- def parse_return_statement
533
- token = advance # Skip 'return'
885
+ def parse_echo_statement
886
+ token = advance #Skip 'echo'
534
887
  line = token[:line]
535
888
  column = token[:column]
536
889
 
537
- value = nil
538
- if @position < @tokens.length && current_token[:type] != :semicolon
539
- value = parse_expression
540
- end
890
+ expression = parse_expression
541
891
 
542
892
  {
543
- type: :return_statement,
544
- value: value,
893
+ type: :echo_statement,
894
+ expression: expression,
545
895
  line: line,
546
896
  column: column
547
897
  }
548
898
  end
549
899
 
550
- def parse_expression_statement
551
- expr = parse_expression
552
- {
553
- type: :expression_statement,
554
- expression: expr,
555
- line: expr ? expr[:line] : 0,
556
- column: expr ? expr[:column] : 0
557
- }
558
- end
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
+ }
559
933
 
560
- def parse_expression
561
- return parse_binary_expression
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
942
+ end
943
+
944
+ # Now advance past the (potentially modified) current token
945
+ advance
946
+ { type: :comment, value: comment, line: line, column: column }
562
947
  end
563
948
 
564
- def parse_binary_expression(precedence = 0)
565
- left = parse_primary_expression
949
+ def parse_if_statement
950
+ token = advance # Skip 'if'
951
+ line = token[:line]
952
+ column = token[:column]
566
953
 
567
- while current_token && current_token[:type] == :operator &&
568
- operator_precedence(current_token[:value]) >= precedence
569
- op_token = advance
570
- op = op_token[:value]
571
- op_precedence = operator_precedence(op)
954
+ condition = parse_expression
572
955
 
573
- right = parse_binary_expression(op_precedence + 1)
956
+ then_branch = []
957
+ else_branch = []
574
958
 
575
- left = {
576
- type: :binary_expression,
577
- operator: op,
578
- left: left,
579
- right: right,
580
- line: op_token[:line],
581
- column: op_token[:column]
582
- }
583
- end
959
+ # Check if this might be a one-line if (look ahead for pipe character)
960
+ one_line_if = false
584
961
 
585
- return left
586
- end
962
+ if current_token && current_token[:type] == :operator && current_token[:value] == '|'
963
+ one_line_if = true
964
+ advance # Skip the pipe
587
965
 
588
- def operator_precedence(op)
589
- case op
590
- when '..' # String concatenation
591
- 1
592
- when '||' # Logical OR
593
- 2
594
- when '&&' # Logical AND
595
- 3
596
- when '==', '!=', '>', '<', '>=', '<=' # Comparison
597
- 4
598
- when '+', '-' # Addition, subtraction
599
- 5
600
- when '*', '/', '%' # Multiplication, division, modulo
601
- 6
602
- else
603
- 0
604
- end
605
- end
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
1046
+
1047
+ {
1048
+ type: :if_statement,
1049
+ condition: condition,
1050
+ then_branch: then_branch,
1051
+ else_branch: else_branch,
1052
+ one_line: one_line_if,
1053
+ line: line,
1054
+ column: column
1055
+ }
1056
+ end
1057
+
1058
+ def parse_while_statement
1059
+ token = advance # Skip 'while'
1060
+ line = token[:line]
1061
+ column = token[:column]
1062
+ condition = parse_expression
1063
+
1064
+ body = []
1065
+
1066
+ # Parse statements until we hit 'endwhile'
1067
+ while @position < @tokens.length
1068
+ if current_token[:type] == :keyword && current_token[:value] == 'endwhile'
1069
+ break
1070
+ end
1071
+
1072
+ stmt = parse_statement
1073
+ body << stmt if stmt
1074
+ end
1075
+
1076
+ # Expect endwhile
1077
+ expect(:keyword) # This should be 'endwhile'
1078
+
1079
+ {
1080
+ type: :while_statement,
1081
+ condition: condition,
1082
+ body: body,
1083
+ line: line,
1084
+ column: column
1085
+ }
1086
+ end
1087
+
1088
+ def parse_for_statement
1089
+ token = advance # Skip 'for'
1090
+ line = token[:line]
1091
+ column = token[:column]
1092
+
1093
+ # Two main patterns:
1094
+ # 1. for [key, val] in dict - destructuring with bracket_open
1095
+ # 2. for var in list - simple variable with identifier
1096
+
1097
+ if current_token && current_token[:type] == :bracket_open
1098
+ # Handle destructuring assignment: for [key, val] in dict
1099
+ advance # Skip '['
1100
+
1101
+ loop_vars = []
1102
+
1103
+ loop do
1104
+ if current_token && (current_token[:type] == :identifier ||
1105
+ current_token[:type] == :local_variable ||
1106
+ current_token[:type] == :global_variable ||
1107
+ current_token[:type] == :script_local)
1108
+ loop_vars << advance
1109
+ else
1110
+ @errors << {
1111
+ message: "Expected identifier in for loop variables",
1112
+ position: @position,
1113
+ line: current_token ? current_token[:line] : 0,
1114
+ column: current_token ? current_token[:column] : 0
1115
+ }
1116
+ break
1117
+ end
1118
+
1119
+ if current_token && current_token[:type] == :comma
1120
+ advance # Skip ','
1121
+ else
1122
+ break
1123
+ end
1124
+ end
1125
+
1126
+ expect(:bracket_close) # Skip ']'
1127
+
1128
+ if !current_token || (current_token[:type] != :identifier || current_token[:value] != 'in')
1129
+ @errors << {
1130
+ message: "Expected 'in' after for loop variables",
1131
+ position: @position,
1132
+ line: current_token ? current_token[:line] : 0,
1133
+ column: current_token ? current_token[:column] : 0
1134
+ }
1135
+ else
1136
+ advance # Skip 'in'
1137
+ end
1138
+
1139
+ iterable = parse_expression
1140
+
1141
+ # Parse the body until 'endfor'
1142
+ body = []
1143
+ while @position < @tokens.length
1144
+ if current_token && current_token[:type] == :keyword && current_token[:value] == 'endfor'
1145
+ break
1146
+ end
1147
+
1148
+ stmt = parse_statement
1149
+ body << stmt if stmt
1150
+ end
1151
+
1152
+ # Expect endfor
1153
+ if current_token && current_token[:type] == :keyword && current_token[:value] == 'endfor'
1154
+ advance # Skip 'endfor'
1155
+ else
1156
+ # Only add an error if we haven't reached the end of the file
1157
+ if @position < @tokens.length
1158
+ @errors << {
1159
+ message: "Expected 'endfor' to close for statement",
1160
+ position: @position,
1161
+ line: current_token ? current_token[:line] : 0,
1162
+ column: current_token ? current_token[:column] : 0
1163
+ }
1164
+ end
1165
+ end
1166
+
1167
+ return {
1168
+ type: :for_statement,
1169
+ loop_vars: loop_vars,
1170
+ iterable: iterable,
1171
+ body: body,
1172
+ line: line,
1173
+ column: column
1174
+ }
1175
+ elsif current_token && current_token[:type] == :paren_open
1176
+ # Handle multiple variables in parentheses: for (var1, var2) in list
1177
+ advance # Skip '('
1178
+
1179
+ loop_vars = []
1180
+
1181
+ loop do
1182
+ if current_token && (current_token[:type] == :identifier ||
1183
+ current_token[:type] == :local_variable ||
1184
+ current_token[:type] == :global_variable ||
1185
+ current_token[:type] == :script_local)
1186
+ loop_vars << advance
1187
+ else
1188
+ @errors << {
1189
+ message: "Expected identifier in for loop variables",
1190
+ position: @position,
1191
+ line: current_token ? current_token[:line] : 0,
1192
+ column: current_token ? current_token[:column] : 0
1193
+ }
1194
+ break
1195
+ end
1196
+
1197
+ if current_token && current_token[:type] == :comma
1198
+ advance # Skip ','
1199
+ else
1200
+ break
1201
+ end
1202
+ end
1203
+
1204
+ expect(:paren_close) # Skip ')'
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'
1279
+ end
1280
+
1281
+ iterable = parse_expression
1282
+
1283
+ # Parse the body until 'endfor'
1284
+ body = []
1285
+ while @position < @tokens.length
1286
+ if current_token && current_token[:type] == :keyword && current_token[:value] == 'endfor'
1287
+ break
1288
+ end
1289
+
1290
+ stmt = parse_statement
1291
+ body << stmt if stmt
1292
+ end
1293
+
1294
+ # Expect endfor
1295
+ if current_token && current_token[:type] == :keyword && current_token[:value] == 'endfor'
1296
+ advance # Skip 'endfor'
1297
+ else
1298
+ # Only add an error if we haven't reached the end of the file
1299
+ if @position < @tokens.length
1300
+ @errors << {
1301
+ message: "Expected 'endfor' to close for statement",
1302
+ position: @position,
1303
+ line: current_token ? current_token[:line] : 0,
1304
+ column: current_token ? current_token[:column] : 0
1305
+ }
1306
+ end
1307
+ end
1308
+
1309
+ return {
1310
+ type: :for_statement,
1311
+ loop_var: loop_var,
1312
+ iterable: iterable,
1313
+ body: body,
1314
+ line: line,
1315
+ column: column
1316
+ }
1317
+ end
1318
+ end
1319
+
1320
+ def parse_def_function
1321
+ token = advance # Skip 'def'
1322
+ line = token[:line]
1323
+ column = token[:column]
1324
+
1325
+ name = expect(:identifier)
1326
+
1327
+ # Parse parameter list
1328
+ expect(:paren_open)
1329
+ params = parse_parameter_list
1330
+ expect(:paren_close)
1331
+
1332
+ # Parse optional return type
1333
+ return_type = nil
1334
+ if current_token && current_token[:type] == :colon
1335
+ advance # Skip ':'
1336
+ return_type = parse_type
1337
+ end
1338
+
1339
+ # Parse function body
1340
+ body = []
1341
+ while @position < @tokens.length
1342
+ if current_token[:type] == :keyword && current_token[:value] == 'enddef'
1343
+ break
1344
+ end
1345
+
1346
+ stmt = parse_statement
1347
+ body << stmt if stmt
1348
+ end
1349
+
1350
+ # Expect enddef
1351
+ expect(:keyword) # This should be 'enddef'
1352
+
1353
+ {
1354
+ type: :def_function,
1355
+ name: name ? name[:value] : nil,
1356
+ params: params,
1357
+ return_type: return_type,
1358
+ body: body,
1359
+ line: line,
1360
+ column: column
1361
+ }
1362
+ end
1363
+
1364
+ def parse_parameter_list
1365
+ params = []
1366
+
1367
+ # Empty parameter list
1368
+ if current_token && current_token[:type] == :paren_close
1369
+ return params
1370
+ end
1371
+
1372
+ # Parse parameters until we find a closing parenthesis
1373
+ while @position < @tokens.length && current_token && current_token[:type] != :paren_close
1374
+ # Check for variable args
1375
+ if current_token && current_token[:type] == :ellipsis
1376
+ ellipsis_token = advance
1377
+
1378
+ # Parse type for variable args if present
1379
+ param_type = nil
1380
+ if current_token && current_token[:type] == :colon
1381
+ advance # Skip ':'
1382
+ param_type = parse_type
1383
+ end
1384
+
1385
+ params << {
1386
+ type: :var_args,
1387
+ param_type: param_type,
1388
+ line: ellipsis_token[:line],
1389
+ column: ellipsis_token[:column]
1390
+ }
1391
+
1392
+ # After varargs, we expect closing paren
1393
+ if current_token && current_token[:type] != :paren_close
1394
+ @errors << {
1395
+ message: "Expected closing parenthesis after varargs",
1396
+ position: @position,
1397
+ line: current_token[:line],
1398
+ column: current_token[:column]
1399
+ }
1400
+ end
1401
+
1402
+ break
1403
+ end
1404
+
1405
+ # Get parameter name
1406
+ if !current_token || current_token[:type] != :identifier
1407
+ @errors << {
1408
+ message: "Expected parameter name",
1409
+ position: @position,
1410
+ line: current_token ? current_token[:line] : 0,
1411
+ column: current_token ? current_token[:column] : 0
1412
+ }
1413
+ break
1414
+ end
1415
+
1416
+ param_name = advance
1417
+
1418
+ # Check for type annotation
1419
+ param_type = nil
1420
+ if current_token && current_token[:type] == :colon
1421
+ advance # Skip ':'
1422
+ param_type = parse_type
1423
+ end
1424
+
1425
+ # Check for default value
1426
+ default_value = nil
1427
+ if current_token && current_token[:type] == :operator && current_token[:value] == '='
1428
+ advance # Skip '='
1429
+ default_value = parse_expression
1430
+ end
1431
+
1432
+ params << {
1433
+ type: :parameter,
1434
+ name: param_name[:value],
1435
+ param_type: param_type,
1436
+ optional: default_value != nil,
1437
+ default_value: default_value,
1438
+ line: param_name[:line],
1439
+ column: param_name[:column]
1440
+ }
1441
+
1442
+ # If we have a comma, advance past it and continue
1443
+ if current_token && current_token[:type] == :comma
1444
+ advance
1445
+ # If we don't have a comma, we should have a closing paren
1446
+ elsif current_token && current_token[:type] != :paren_close
1447
+ @errors << {
1448
+ message: "Expected comma or closing parenthesis after parameter",
1449
+ position: @position,
1450
+ line: current_token[:line],
1451
+ column: current_token[:column]
1452
+ }
1453
+ break
1454
+ end
1455
+ end
1456
+
1457
+ params
1458
+ end
1459
+
1460
+ def parse_type
1461
+ if current_token && [:identifier, :keyword].include?(current_token[:type])
1462
+ type_name = advance
1463
+
1464
+ # Handle generic types like list<string>
1465
+ if current_token && current_token[:type] == :operator && current_token[:value] == '<'
1466
+ advance # Skip '<'
1467
+ inner_type = parse_type
1468
+ expect(:operator) # This should be '>'
1469
+
1470
+ return {
1471
+ type: :generic_type,
1472
+ base_type: type_name[:value],
1473
+ inner_type: inner_type,
1474
+ line: type_name[:line],
1475
+ column: type_name[:column]
1476
+ }
1477
+ end
1478
+
1479
+ return type_name[:value]
1480
+ else
1481
+ @errors << {
1482
+ message: "Expected type identifier",
1483
+ position: @position,
1484
+ line: current_token ? current_token[:line] : 0,
1485
+ column: current_token ? current_token[:column] : 0
1486
+ }
1487
+ advance
1488
+ return "unknown"
1489
+ end
1490
+ end
1491
+
1492
+ def parse_variable_declaration
1493
+ var_type_token = advance # Skip 'var', 'const', or 'final'
1494
+ var_type = var_type_token[:value]
1495
+ line = var_type_token[:line]
1496
+ column = var_type_token[:column]
1497
+
1498
+ if !current_token || current_token[:type] != :identifier
1499
+ @errors << {
1500
+ message: "Expected variable name",
1501
+ position: @position,
1502
+ line: current_token ? current_token[:line] : 0,
1503
+ column: current_token ? current_token[:column] : 0
1504
+ }
1505
+ return nil
1506
+ end
1507
+
1508
+ name_token = advance
1509
+ name = name_token[:value]
1510
+
1511
+ # Parse optional type annotation
1512
+ var_type_annotation = nil
1513
+ if current_token && current_token[:type] == :colon
1514
+ advance # Skip ':'
1515
+ var_type_annotation = parse_type
1516
+ end
1517
+
1518
+ # Parse initializer if present
1519
+ initializer = nil
1520
+ if current_token && current_token[:type] == :operator && current_token[:value] == '='
1521
+ advance # Skip '='
1522
+ initializer = parse_expression
1523
+ end
1524
+
1525
+ {
1526
+ type: :variable_declaration,
1527
+ var_type: var_type,
1528
+ name: name,
1529
+ var_type_annotation: var_type_annotation,
1530
+ initializer: initializer,
1531
+ line: line,
1532
+ column: column
1533
+ }
1534
+ end
1535
+
1536
+ def parse_return_statement
1537
+ token = advance # Skip 'return'
1538
+ line = token[:line]
1539
+ column = token[:column]
1540
+
1541
+ value = nil
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]))
1548
+ value = parse_expression
1549
+ end
1550
+
1551
+ {
1552
+ type: :return_statement,
1553
+ value: value,
1554
+ line: line,
1555
+ column: column
1556
+ }
1557
+ end
1558
+
1559
+ def parse_expression_statement
1560
+ # Check if this is an assignment using a compound operator
1561
+ if current_token && current_token[:type] == :identifier
1562
+ variable_name = current_token[:value]
1563
+ variable_token = current_token
1564
+ advance # Move past the identifier
1565
+
1566
+ if current_token && current_token[:type] == :compound_operator
1567
+ operator = current_token[:value]
1568
+ operator_token = current_token
1569
+ advance # Move past the operator
1570
+
1571
+ right = parse_expression
1572
+
1573
+ return {
1574
+ type: :compound_assignment,
1575
+ operator: operator,
1576
+ target: {
1577
+ type: :identifier,
1578
+ name: variable_name,
1579
+ line: variable_token[:line],
1580
+ column: variable_token[:column]
1581
+ },
1582
+ value: right,
1583
+ line: operator_token[:line],
1584
+ column: operator_token[:column]
1585
+ }
1586
+ end
1587
+
1588
+ # If it wasn't a compound assignment, backtrack
1589
+ @position -= 1
1590
+ end
1591
+
1592
+ # Regular expression statement handling
1593
+ expr = parse_expression
1594
+ {
1595
+ type: :expression_statement,
1596
+ expression: expr,
1597
+ line: expr ? expr[:line] : 0,
1598
+ column: expr ? expr[:column] : 0
1599
+ }
1600
+ end
1601
+
1602
+ def parse_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
1669
+ end
1670
+
1671
+ def parse_binary_expression(precedence = 0)
1672
+ left = parse_unary_expression
1673
+
1674
+ # Handle multi-line expressions where operators may appear at line beginnings
1675
+ while current_token &&
1676
+ (current_token[:type] == :operator ||
1677
+ (peek_token && peek_token[:type] == :operator && current_token[:type] == :whitespace))
1678
+
1679
+ # Skip any whitespace before the operator if it's at the beginning of a line
1680
+ if current_token[:type] == :whitespace
1681
+ advance
1682
+ end
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
1740
+ # Now we should be at the operator
1741
+ elsif current_token && current_token[:type] == :operator &&
1742
+ operator_precedence(current_token[:value]) >= precedence
1743
+ op_token = advance
1744
+ op = op_token[:value]
1745
+ op_precedence = operator_precedence(op)
1746
+
1747
+ right = parse_binary_expression(op_precedence + 1)
1748
+
1749
+ left = {
1750
+ type: :binary_expression,
1751
+ operator: op,
1752
+ left: left,
1753
+ right: right,
1754
+ line: op_token[:line],
1755
+ column: op_token[:column]
1756
+ }
1757
+ else
1758
+ break
1759
+ end
1760
+ end
1761
+
1762
+ return left
1763
+ end
1764
+
1765
+ def operator_precedence(op)
1766
+ case op
1767
+ when '..' # String concatenation
1768
+ 1
1769
+ when '||' # Logical OR
1770
+ 2
1771
+ when '&&' # Logical AND
1772
+ 3
1773
+ when '==', '!=', '>', '<', '>=', '<=' # Comparison
1774
+ 4
1775
+ when '+', '-' # Addition, subtraction
1776
+ 5
1777
+ when '*', '/', '%' # Multiplication, division, modulo
1778
+ 6
1779
+ when '.'
1780
+ 7
1781
+ when '!'
1782
+ 8
1783
+ else
1784
+ 0
1785
+ end
1786
+ end
1787
+
1788
+ def parse_primary_expression
1789
+ return nil unless current_token
1790
+
1791
+ token = current_token
1792
+ line = token[:line]
1793
+ column = token[:column]
1794
+
1795
+ # First parse the basic expression
1796
+ expr = nil
1797
+
1798
+ case token[:type]
1799
+ # Add handling for command arg placeholders
1800
+ when :command_arg_placeholder
1801
+ advance
1802
+ expr = {
1803
+ type: :command_arg_placeholder,
1804
+ value: token[:value],
1805
+ line: line,
1806
+ column: column
1807
+ }
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
1923
+ advance
1924
+ expr = {
1925
+ type: :literal,
1926
+ value: token[:value],
1927
+ token_type: :number,
1928
+ line: line,
1929
+ column: column
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
1953
+ when :option_variable
1954
+ # Handle Vim option variables (like &compatible)
1955
+ advance
1956
+ expr = {
1957
+ type: :option_variable,
1958
+ name: token[:value],
1959
+ line: line,
1960
+ column: column
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
+ }
1970
+ when :special_variable
1971
+ # Handle Vim special variables (like v:version)
1972
+ advance
1973
+ expr = {
1974
+ type: :special_variable,
1975
+ name: token[:value],
1976
+ line: line,
1977
+ column: column
1978
+ }
1979
+ when :script_local
1980
+ # Handle script-local variables/functions (like s:var)
1981
+ advance
1982
+
1983
+ # Check if this is a function call
1984
+ if current_token && current_token[:type] == :paren_open
1985
+ return parse_function_call(token[:value], line, column)
1986
+ end
1987
+
1988
+ expr = {
1989
+ type: :script_local,
1990
+ name: token[:value],
1991
+ line: line,
1992
+ column: column
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
+ }
2009
+ when :global_variable
2010
+ # Handle global variables (like g:var)
2011
+ advance
2012
+ expr = {
2013
+ type: :global_variable,
2014
+ name: token[:value],
2015
+ line: line,
2016
+ column: column
2017
+ }
2018
+ when :arg_variable
2019
+ # Handle function argument variables (like a:var)
2020
+ advance
2021
+ expr = {
2022
+ type: :arg_variable,
2023
+ name: token[:value],
2024
+ line: line,
2025
+ column: column
2026
+ }
2027
+ when :local_variable
2028
+ # Handle local variables (like l:var)
2029
+ advance
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)
2040
+ else
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
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
+ }
2075
+ when :paren_open
2076
+ if is_lambda_expression
2077
+ return parse_lambda_expression(line, column)
2078
+ else
2079
+ advance # Skip '('
2080
+ expr = parse_expression
2081
+ expect(:paren_close) # Expect and skip ')'
2082
+ end
2083
+ when :bracket_open
2084
+ expr = parse_list_literal(line, column)
2085
+ when :brace_open
2086
+ expr = parse_vim_lambda_or_dict(line, column)
2087
+ when :backslash
2088
+ # Handle line continuation with backslash
2089
+ advance
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
2102
+ else
2103
+ @errors << {
2104
+ message: "Unexpected token in expression: #{token[:type]}",
2105
+ position: @position,
2106
+ line: line,
2107
+ column: column
2108
+ }
2109
+ advance
2110
+ return nil
2111
+ end
2112
+
2113
+ # Now handle any chained property access or method calls
2114
+ while current_token
2115
+ # Check for property access with dot
2116
+ if current_token[:type] == :operator && current_token[:value] == '.'
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]
2149
+ }
2150
+ else
2151
+ # This is concatenation or other binary operation, break to let binary expression parser handle it
2152
+ break
2153
+ end
2154
+
2155
+ # Check for method call with arrow ->
2156
+ elsif current_token[:type] == :operator && current_token[:value] == '->'
2157
+ arrow_token = advance # Skip '->'
2158
+
2159
+ # Next token should be an identifier (method name)
2160
+ if !current_token || current_token[:type] != :identifier
2161
+ @errors << {
2162
+ message: "Expected method name after '->'",
2163
+ position: @position,
2164
+ line: current_token ? current_token[:line] : 0,
2165
+ column: current_token ? current_token[:column] : 0
2166
+ }
2167
+ break
2168
+ end
2169
+
2170
+ method_name = advance[:value] # Get method name
2171
+
2172
+ # Check for arguments
2173
+ args = []
2174
+ if current_token && current_token[:type] == :paren_open
2175
+ expect(:paren_open) # Skip '('
2176
+
2177
+ # Parse arguments if any
2178
+ unless current_token && current_token[:type] == :paren_close
2179
+ loop do
2180
+ arg = parse_expression
2181
+ args << arg if arg
2182
+
2183
+ if current_token && current_token[:type] == :comma
2184
+ advance # Skip comma
2185
+ else
2186
+ break
2187
+ end
2188
+ end
2189
+ end
2190
+
2191
+ expect(:paren_close) # Skip ')'
2192
+ end
2193
+
2194
+ expr = {
2195
+ type: :method_call,
2196
+ object: expr,
2197
+ method: method_name,
2198
+ arguments: args,
2199
+ line: arrow_token[:line],
2200
+ column: arrow_token[:column]
2201
+ }
2202
+ # Check for indexing with brackets
2203
+ elsif current_token[:type] == :bracket_open
2204
+ bracket_token = advance # Skip '['
2205
+ index_expr = parse_expression
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
+
2224
+ expect(:bracket_close) # Skip ']'
2225
+
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
2246
+ # Check for function call directly on an expression
2247
+ elsif current_token[:type] == :paren_open
2248
+ paren_token = advance # Skip '('
2249
+
2250
+ args = []
2251
+
2252
+ # Parse arguments if any
2253
+ unless current_token && current_token[:type] == :paren_close
2254
+ loop do
2255
+ arg = parse_expression
2256
+ args << arg if arg
2257
+
2258
+ if current_token && current_token[:type] == :comma
2259
+ advance # Skip comma
2260
+ else
2261
+ break
2262
+ end
2263
+ end
2264
+ end
2265
+
2266
+ expect(:paren_close) # Skip ')'
2267
+
2268
+ expr = {
2269
+ type: :call_expression,
2270
+ callee: expr,
2271
+ arguments: args,
2272
+ line: paren_token[:line],
2273
+ column: paren_token[:column]
2274
+ }
2275
+ else
2276
+ # No more chaining
2277
+ break
2278
+ end
2279
+ end
2280
+
2281
+ return expr
2282
+ end
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
+
2363
+ def is_lambda_expression
2364
+ # Save the current position
2365
+ original_position = @position
2366
+
2367
+ # Skip the opening parenthesis
2368
+ advance if current_token && current_token[:type] == :paren_open
2369
+
2370
+ # Check for a parameter list followed by => or ): =>
2371
+ has_params = false
2372
+ has_arrow = false
2373
+
2374
+ # Skip past parameters
2375
+ loop do
2376
+ break if !current_token
2377
+
2378
+ if current_token[:type] == :identifier
2379
+ has_params = true
2380
+ advance
2381
+
2382
+ # Either comma for another parameter, or close paren
2383
+ if current_token && current_token[:type] == :comma
2384
+ advance
2385
+ elsif current_token && current_token[:type] == :paren_close
2386
+ advance
2387
+ break
2388
+ else
2389
+ # Not a valid lambda parameter list
2390
+ break
2391
+ end
2392
+ elsif current_token[:type] == :paren_close
2393
+ # Empty parameter list is valid
2394
+ advance
2395
+ break
2396
+ else
2397
+ # Not a valid lambda parameter list
2398
+ break
2399
+ end
2400
+ end
2401
+
2402
+ # After parameters, check for either => or ): =>
2403
+ if current_token
2404
+ if current_token[:type] == :operator && current_token[:value] == '=>'
2405
+ has_arrow = true
2406
+ elsif current_token[:type] == :colon
2407
+ # There might be a return type annotation
2408
+ advance
2409
+ # Skip the type
2410
+ advance if current_token && [:identifier, :keyword].include?(current_token[:type])
2411
+ # Check for the arrow
2412
+ has_arrow = current_token && current_token[:type] == :operator && current_token[:value] == '=>'
2413
+ end
2414
+ end
2415
+
2416
+ # Reset position to where we started
2417
+ @position = original_position
2418
+
2419
+ return has_params && has_arrow
2420
+ end
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
+
2525
+ def parse_lambda_expression(line, column)
2526
+ expect(:paren_open) # Skip '('
2527
+
2528
+ params = []
2529
+
2530
+ # Parse parameters
2531
+ unless current_token && current_token[:type] == :paren_close
2532
+ loop do
2533
+ if current_token && current_token[:type] == :identifier
2534
+ param_name = current_token[:value]
2535
+ param_token = current_token
2536
+ advance
2537
+
2538
+ # Check for type annotation
2539
+ param_type = nil
2540
+ if current_token && current_token[:type] == :colon
2541
+ advance # Skip ':'
2542
+ param_type = parse_type
2543
+ end
2544
+
2545
+ params << {
2546
+ type: :parameter,
2547
+ name: param_name,
2548
+ param_type: param_type,
2549
+ line: param_token[:line],
2550
+ column: param_token[:column]
2551
+ }
2552
+ else
2553
+ @errors << {
2554
+ message: "Expected parameter name in lambda expression",
2555
+ position: @position,
2556
+ line: current_token ? current_token[:line] : 0,
2557
+ column: current_token ? current_token[:column] : 0
2558
+ }
2559
+ break
2560
+ end
2561
+
2562
+ if current_token && current_token[:type] == :comma
2563
+ advance # Skip comma
2564
+ else
2565
+ break
2566
+ end
2567
+ end
2568
+ end
2569
+
2570
+ expect(:paren_close) # Skip ')'
2571
+
2572
+ # Check for return type annotation
2573
+ return_type = nil
2574
+ if current_token && current_token[:type] == :colon
2575
+ advance # Skip ':'
2576
+ return_type = parse_type
2577
+ end
2578
+
2579
+ # Expect the arrow '=>'
2580
+ arrow_found = false
2581
+ if current_token && current_token[:type] == :operator && current_token[:value] == '=>'
2582
+ arrow_found = true
2583
+ advance # Skip '=>'
2584
+ else
2585
+ @errors << {
2586
+ message: "Expected '=>' in lambda expression",
2587
+ position: @position,
2588
+ line: current_token ? current_token[:line] : 0,
2589
+ column: current_token ? current_token[:column] : 0
2590
+ }
2591
+ end
2592
+
2593
+ # Parse the lambda body - check if it's a block or an expression
2594
+ if arrow_found
2595
+ if current_token && current_token[:type] == :brace_open
2596
+ # Parse block body
2597
+ advance # Skip '{'
2598
+
2599
+ block_body = []
2600
+ brace_count = 1 # Track nested braces
2601
+
2602
+ while @position < @tokens.length && brace_count > 0
2603
+ if current_token[:type] == :brace_open
2604
+ brace_count += 1
2605
+ elsif current_token[:type] == :brace_close
2606
+ brace_count -= 1
2607
+
2608
+ # Skip the final closing brace, but don't process it as a statement
2609
+ if brace_count == 0
2610
+ advance
2611
+ break
2612
+ end
2613
+ end
2614
+
2615
+ stmt = parse_statement
2616
+ block_body << stmt if stmt
2617
+
2618
+ # Break if we've reached the end or found the matching closing brace
2619
+ if brace_count == 0 || @position >= @tokens.length
2620
+ break
2621
+ end
2622
+ end
2623
+
2624
+ body = {
2625
+ type: :block_expression,
2626
+ statements: block_body,
2627
+ line: line,
2628
+ column: column
2629
+ }
2630
+ else
2631
+ # Simple expression body
2632
+ body = parse_expression
2633
+ end
2634
+ else
2635
+ body = nil
2636
+ end
2637
+
2638
+ return {
2639
+ type: :lambda_expression,
2640
+ params: params,
2641
+ return_type: return_type,
2642
+ body: body,
2643
+ line: line,
2644
+ column: column
2645
+ }
2646
+ end
2647
+
2648
+ def parse_dict_literal(line, column)
2649
+ advance # Skip '{'
2650
+ entries = []
2651
+
2652
+ # Handle whitespace after opening brace
2653
+ while current_token && current_token[:type] == :whitespace
2654
+ advance
2655
+ end
2656
+
2657
+ # Empty dictionary
2658
+ if current_token && current_token[:type] == :brace_close
2659
+ advance # Skip '}'
2660
+ return {
2661
+ type: :dict_literal,
2662
+ entries: entries,
2663
+ line: line,
2664
+ column: column
2665
+ }
2666
+ end
2667
+
2668
+ # Parse dictionary entries
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
+
2680
+ # Parse key (string or identifier)
2681
+ key = nil
2682
+ if current_token && (current_token[:type] == :string || current_token[:type] == :identifier)
2683
+ key = current_token[:value]
2684
+ advance # Skip key
2685
+ else
2686
+ @errors << {
2687
+ message: "Expected string or identifier as dictionary key",
2688
+ position: @position,
2689
+ line: current_token ? current_token[:line] : 0,
2690
+ column: current_token ? current_token[:column] : 0
2691
+ }
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
2704
+ end
2705
+
2706
+ # 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
2722
+
2723
+ # Parse value
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
2730
+
2731
+ entries << {
2732
+ key: key,
2733
+ value: value
2734
+ }
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)
2742
+ if current_token && current_token[:type] == :comma
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
2750
+ else
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
2771
+ end
2772
+ end
2773
+
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
2785
+
2786
+ {
2787
+ type: :dict_literal,
2788
+ entries: entries,
2789
+ line: line,
2790
+ column: column
2791
+ }
2792
+ end
2793
+
2794
+ def parse_list_literal(line, column)
2795
+ advance # Skip '['
2796
+ elements = []
2797
+
2798
+ # Empty list
2799
+ if current_token && current_token[:type] == :bracket_close
2800
+ advance # Skip ']'
2801
+ return {
2802
+ type: :list_literal,
2803
+ elements: elements,
2804
+ line: line,
2805
+ column: column
2806
+ }
2807
+ end
2808
+
2809
+ # Parse list elements
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
+
2825
+ # Check if we've reached the end of the list
2826
+ if current_token && current_token[:type] == :bracket_close
2827
+ advance # Skip ']'
2828
+ break
2829
+ end
2830
+
2831
+ element = parse_expression
2832
+ elements << element if element
2833
+
2834
+ # Continue if there's a comma
2835
+ if current_token && current_token[:type] == :comma
2836
+ advance # Skip comma
2837
+ # Or if the next token is a backslash (line continuation)
2838
+ else
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
2849
+
2850
+ # We still want to skip the closing bracket if it's there
2851
+ if current_token && current_token[:type] == :bracket_close
2852
+ advance
2853
+ break
2854
+ end
2855
+ advance
2856
+ #next
2857
+ end
2858
+ end
2859
+
2860
+ # Check if we have a second list immediately following this one (Vim's special syntax)
2861
+ if current_token && current_token[:type] == :bracket_open
2862
+ next_list = parse_list_literal(current_token[:line], current_token[:column])
2863
+
2864
+ return {
2865
+ type: :list_concat_expression,
2866
+ left: {
2867
+ type: :list_literal,
2868
+ elements: elements,
2869
+ line: line,
2870
+ column: column
2871
+ },
2872
+ right: next_list,
2873
+ line: line,
2874
+ column: column
2875
+ }
2876
+ end
2877
+
2878
+ return {
2879
+ type: :list_literal,
2880
+ elements: elements,
2881
+ line: line,
2882
+ column: column
2883
+ }
2884
+ end
2885
+
2886
+ def parse_unary_expression
2887
+ # Check for unary operators (!, -, +)
2888
+ if current_token && current_token[:type] == :operator &&
2889
+ ['!', '-', '+'].include?(current_token[:value])
2890
+
2891
+ op_token = advance # Skip the operator
2892
+ operand = parse_unary_expression # Recursively parse the operand
2893
+
2894
+ return {
2895
+ type: :unary_expression,
2896
+ operator: op_token[:value],
2897
+ operand: operand,
2898
+ line: op_token[:line],
2899
+ column: op_token[:column]
2900
+ }
2901
+ end
2902
+
2903
+ # If no unary operator, parse a primary expression
2904
+ return parse_primary_expression
2905
+ end
2906
+
2907
+ def parse_function_call(name, line, column)
2908
+ expect(:paren_open)
2909
+
2910
+ args = []
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
+
2916
+ # Parse arguments until we find a closing parenthesis
2917
+ while @position < @tokens.length && current_token && current_token[:type] != :paren_close
2918
+ # Skip comments inside parameter lists
2919
+ if current_token && current_token[:type] == :comment
2920
+ advance
2921
+ next
2922
+ end
2923
+ if is_special_function && current_token && current_token[:type] == :string
2924
+ # For functions like map(), filter(), directly add the string as an argument
2925
+ string_token = parse_string
2926
+ args << {
2927
+ type: :literal,
2928
+ value: string_token[:value],
2929
+ token_type: :string,
2930
+ line: string_token[:line],
2931
+ column: string_token[:column]
2932
+ }
2933
+ else
2934
+ # Parse the argument
2935
+ arg = parse_expression
2936
+ args << arg if arg
2937
+ end
2938
+
2939
+ # Break if we hit the closing paren
2940
+ if current_token && current_token[:type] == :paren_close
2941
+ break
2942
+ end
2943
+ # If we have a comma, advance past it and continue
2944
+ if current_token && current_token[:type] == :comma
2945
+ advance
2946
+ # If we don't have a comma and we're not at the end, it's an error
2947
+ elsif current_token && current_token[:type] != :paren_close && current_token[:type] != :comment
2948
+ @errors << {
2949
+ message: "Expected comma or closing parenthesis after argument",
2950
+ position: @position,
2951
+ line: current_token[:line],
2952
+ column: current_token[:column]
2953
+ }
2954
+ break
2955
+ end
2956
+ end
2957
+
2958
+ expect(:paren_close)
2959
+
2960
+ {
2961
+ type: :function_call,
2962
+ name: name,
2963
+ arguments: args,
2964
+ line: line,
2965
+ column: column
2966
+ }
2967
+ end
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
+
3017
+ def parse_import_statement
3018
+ token = advance # Skip 'import'
3019
+ line = token[:line]
3020
+ column = token[:column]
3021
+
3022
+ # Handle 'import autoload'
3023
+ is_autoload = false
3024
+ module_name = nil
3025
+ path = nil
3026
+
3027
+ if current_token && current_token[:type] == :identifier && current_token[:value] == 'autoload'
3028
+ is_autoload = true
3029
+ module_name = advance[:value] # Store "autoload" as the module name
3030
+
3031
+ # After "autoload" keyword, expect a string path
3032
+ if current_token && current_token[:type] == :string
3033
+ path = current_token[:value]
3034
+ advance
3035
+ else
3036
+ @errors << {
3037
+ message: "Expected string path after 'autoload'",
3038
+ position: @position,
3039
+ line: current_token ? current_token[:line] : 0,
3040
+ column: current_token ? current_token[:column] : 0
3041
+ }
3042
+ end
3043
+ else
3044
+ # Regular import with a string path
3045
+ if current_token && current_token[:type] == :string
3046
+ path = current_token[:value]
3047
+
3048
+ # Extract module name from the path
3049
+ # This is simplified logic - you might need more complex extraction
3050
+ module_name = path.gsub(/['"]/, '').split('/').last.split('.').first
3051
+
3052
+ advance
3053
+ else
3054
+ # Handle other import formats
3055
+ module_expr = parse_expression()
3056
+ if module_expr && module_expr[:type] == :literal && module_expr[:token_type] == :string
3057
+ path = module_expr[:value]
3058
+ module_name = path.gsub(/['"]/, '').split('/').last.split('.').first
3059
+ end
3060
+ end
3061
+ end
3062
+
3063
+ # Handle 'as name'
3064
+ as_name = nil
3065
+ if current_token && current_token[:type] == :identifier && current_token[:value] == 'as'
3066
+ advance # Skip 'as'
3067
+ if current_token && current_token[:type] == :identifier
3068
+ as_name = advance[:value]
3069
+ else
3070
+ @errors << {
3071
+ message: "Expected identifier after 'as'",
3072
+ position: @position,
3073
+ line: current_token ? current_token[:line] : 0,
3074
+ column: current_token ? current_token[:column] : 0
3075
+ }
3076
+ end
3077
+ end
3078
+
3079
+ {
3080
+ type: :import_statement,
3081
+ module: module_name,
3082
+ path: path,
3083
+ is_autoload: is_autoload,
3084
+ as_name: as_name,
3085
+ line: line,
3086
+ column: column
3087
+ }
3088
+ end
3089
+ def parse_export_statement
3090
+ token = advance # Skip 'export'
3091
+ line = token[:line]
3092
+ column = token[:column]
3093
+
3094
+ # Export can be followed by var/const/def/function declarations
3095
+ if !current_token
3096
+ @errors << {
3097
+ message: "Expected declaration after export",
3098
+ position: @position,
3099
+ line: line,
3100
+ column: column
3101
+ }
3102
+ return nil
3103
+ end
3104
+
3105
+ exported_item = nil
3106
+
3107
+ if current_token[:type] == :keyword
3108
+ case current_token[:value]
3109
+ when 'def'
3110
+ exported_item = parse_def_function
3111
+ when 'function'
3112
+ exported_item = parse_legacy_function
3113
+ when 'var', 'const', 'final'
3114
+ exported_item = parse_variable_declaration
3115
+ when 'class'
3116
+ # Handle class export when implemented
3117
+ @errors << {
3118
+ message: "Class export not implemented yet",
3119
+ position: @position,
3120
+ line: current_token[:line],
3121
+ column: current_token[:column]
3122
+ }
3123
+ advance
3124
+ return nil
3125
+ else
3126
+ @errors << {
3127
+ message: "Unexpected keyword after export: #{current_token[:value]}",
3128
+ position: @position,
3129
+ line: current_token[:line],
3130
+ column: current_token[:column]
3131
+ }
3132
+ advance
3133
+ return nil
3134
+ end
3135
+ else
3136
+ @errors << {
3137
+ message: "Expected declaration after export",
3138
+ position: @position,
3139
+ line: current_token[:line],
3140
+ column: current_token[:column]
3141
+ }
3142
+ advance
3143
+ return nil
3144
+ end
3145
+
3146
+ {
3147
+ type: :export_statement,
3148
+ export: exported_item,
3149
+ line: line,
3150
+ column: column
3151
+ }
3152
+ end
3153
+
3154
+ def parse_command_definition
3155
+ token = advance # Skip 'command' or 'command!'
3156
+ line = token[:line]
3157
+ column = token[:column]
3158
+
3159
+ # Check if the command has a bang (!)
3160
+ has_bang = false
3161
+ if current_token && current_token[:type] == :operator && current_token[:value] == '!'
3162
+ has_bang = true
3163
+ advance # Skip '!'
3164
+ end
3165
+
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]
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
3183
+ end
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
3196
+ end
3197
+ end
3198
+
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)
3283
+ params = parse_parameter_list_legacy
3284
+ expect(:paren_close)
3285
+
3286
+ # Check for optional attributes (range, dict, abort, closure)
3287
+ attributes = []
3288
+ while current_token
3289
+ if current_token[:type] == :keyword && current_token[:value] == 'abort'
3290
+ attributes << advance[:value]
3291
+ elsif current_token[:type] == :identifier &&
3292
+ ['range', 'dict', 'closure'].include?(current_token[:value])
3293
+ attributes << advance[:value]
3294
+ else
3295
+ break
3296
+ end
3297
+ end
3298
+
3299
+ # Parse function body
3300
+ body = []
3301
+ while @position < @tokens.length
3302
+ if current_token && current_token[:type] == :keyword &&
3303
+ ['endfunction', 'endfunc'].include?(current_token[:value])
3304
+ break
3305
+ end
3306
+
3307
+ stmt = parse_statement
3308
+ body << stmt if stmt
3309
+ end
606
3310
 
607
- def parse_primary_expression
608
- return nil unless current_token
3311
+ # Expect endfunction/endfunc
3312
+ if current_token && current_token[:type] == :keyword &&
3313
+ ['endfunction', 'endfunc'].include?(current_token[:value])
3314
+ advance # Skip 'endfunction' or 'endfunc'
3315
+ else
3316
+ # Only add an error if we haven't reached the end of the file
3317
+ if @position < @tokens.length
3318
+ @errors << {
3319
+ message: "Expected 'endfunction' or 'endfunc'",
3320
+ position: @position,
3321
+ line: current_token ? current_token[:line] : 0,
3322
+ column: current_token ? current_token[:column] : 0
3323
+ }
3324
+ end
3325
+ end
609
3326
 
610
- token = current_token
3327
+ function_name = name ? name[:value] : nil
3328
+ if function_scope
3329
+ function_name = function_scope + function_name if function_name
3330
+ end
3331
+
3332
+ {
3333
+ type: :legacy_function,
3334
+ name: function_name,
3335
+ is_script_local: is_script_local,
3336
+ scope: function_scope,
3337
+ params: params,
3338
+ has_bang: has_bang,
3339
+ attributes: attributes,
3340
+ body: body,
3341
+ line: line,
3342
+ column: column
3343
+ }
3344
+ end
3345
+
3346
+ # Legacy function parameters are different - they can use a:name syntax
3347
+ def parse_parameter_list_legacy
3348
+ params = []
3349
+
3350
+ # Empty parameter list
3351
+ if current_token && current_token[:type] == :paren_close
3352
+ return params
3353
+ end
3354
+
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
3392
+ end
3393
+ end
3394
+
3395
+ # Regular parameter parsing (existing logic)
3396
+ loop do
3397
+ if current_token && current_token[:type] == :identifier
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
+
3407
+ params << {
3408
+ type: :parameter,
3409
+ name: param_name[:value],
3410
+ default_value: default_value,
3411
+ optional: default_value != nil,
3412
+ line: param_name[:line],
3413
+ column: param_name[:column]
3414
+ }
3415
+ elsif current_token && current_token[:type] == :arg_variable
3416
+ param_name = advance
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
+
3426
+ params << {
3427
+ type: :parameter,
3428
+ name: name_without_prefix,
3429
+ default_value: default_value,
3430
+ optional: default_value != nil,
3431
+ line: param_name[:line],
3432
+ column: param_name[:column],
3433
+ is_arg_prefixed: true
3434
+ }
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'
611
3508
  line = token[:line]
612
3509
  column = token[:column]
613
3510
 
614
- case token[:type]
615
- when :number
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
3606
+ @errors << {
3607
+ message: "Expected 'endtry' to close try statement",
3608
+ position: @position,
3609
+ line: current_token ? current_token[:line] : 0,
3610
+ column: current_token ? current_token[:column] : 0
3611
+ }
3612
+ end
3613
+ end
3614
+
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]
616
3656
  advance
617
- {
618
- type: :literal,
619
- value: token[:value],
620
- token_type: :number,
621
- line: line,
622
- column: column
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]
623
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]
624
3746
  when :string
3747
+ # Handle string literal
625
3748
  advance
626
- {
3749
+ return {
627
3750
  type: :literal,
628
3751
  value: token[:value],
629
3752
  token_type: :string,
630
3753
  line: line,
631
3754
  column: column
632
3755
  }
633
- when :identifier
3756
+ when :identifier, :global_variable, :script_local, :arg_variable,
3757
+ :local_variable, :buffer_local, :window_local, :tab_local
3758
+ # Handle variable references
634
3759
  advance
635
-
636
- # Check if this is a function call
637
- if current_token && current_token[:type] == :paren_open
638
- return parse_function_call(token[:value], line, column)
639
- end
640
-
641
- {
642
- type: :identifier,
3760
+ return {
3761
+ type: token[:type],
643
3762
  name: token[:value],
644
3763
  line: line,
645
3764
  column: column
646
3765
  }
647
3766
  when :paren_open
648
- advance # Skip '('
3767
+ # Handle parenthesized expressions
3768
+ advance # Skip '('
649
3769
  expr = parse_expression
650
- expect(:paren_close) # Expect and skip ')'
3770
+ expect(:paren_close) # Skip ')'
651
3771
  return expr
3772
+ when :function
3773
+ # Handle function calls
3774
+ return parse_function_call(token[:value], line, column)
652
3775
  else
653
- @errors << {
654
- message: "Unexpected token in expression: #{token[:type]}",
655
- position: @position,
656
- line: line,
657
- column: column
658
- }
659
- advance
660
- return nil
661
- end
662
- end
663
-
664
- def parse_function_call(name, line, column)
665
- expect(:paren_open)
666
-
667
- args = []
668
-
669
- # Parse arguments
670
- unless current_token && current_token[:type] == :paren_close
671
- loop do
672
- arg = parse_expression
673
- args << arg if arg
674
-
675
- break unless current_token && current_token[:type] == :comma
676
- advance # Skip comma
677
- end
3776
+ # For anything else, use the standard expression parser
3777
+ return parse_expression
678
3778
  end
679
-
680
- expect(:paren_close)
681
-
682
- {
683
- type: :function_call,
684
- name: name,
685
- arguments: args,
686
- line: line,
687
- column: column
688
- }
689
3779
  end
690
3780
 
691
- def parse_import_statement
692
- token = advance # Skip 'import'
3781
+ def parse_silent_command
3782
+ token = advance # Skip 'silent'
693
3783
  line = token[:line]
694
3784
  column = token[:column]
695
3785
 
696
- # Handle 'import autoload'
697
- is_autoload = false
698
- module_name = nil
699
- path = nil
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
700
3792
 
701
- if current_token && current_token[:type] == :identifier && current_token[:value] == 'autoload'
702
- is_autoload = true
703
- module_name = advance[:value] # Store "autoload" as the module name
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
704
3819
 
705
- # After "autoload" keyword, expect a string path
706
- if current_token && current_token[:type] == :string
707
- path = current_token[:value]
708
- advance
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
+ }
709
3833
  else
710
3834
  @errors << {
711
- message: "Expected string path after 'autoload'",
3835
+ message: "Expected command after range operator '%'",
712
3836
  position: @position,
713
- line: current_token ? current_token[:line] : 0,
714
- column: current_token ? current_token[:column] : 0
3837
+ line: current_token ? current_token[:line] : range_token[:line],
3838
+ column: current_token ? current_token[:column] : range_token[:column] + 1
715
3839
  }
716
3840
  end
717
3841
  else
718
- # Regular import with a string path
719
- if current_token && current_token[:type] == :string
720
- path = current_token[:value]
3842
+ # Parse a regular command
3843
+ command = parse_statement
3844
+ end
721
3845
 
722
- # Extract module name from the path
723
- # This is simplified logic - you might need more complex extraction
724
- module_name = path.gsub(/['"]/, '').split('/').last.split('.').first
3846
+ return {
3847
+ type: :silent_command,
3848
+ has_bang: has_bang,
3849
+ command: command,
3850
+ line: line,
3851
+ column: column
3852
+ }
3853
+ end
725
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]
726
3869
  advance
727
- else
728
- # Handle other import formats
729
- module_expr = parse_expression()
730
- if module_expr && module_expr[:type] == :literal && module_expr[:token_type] == :string
731
- path = module_expr[:value]
732
- module_name = path.gsub(/['"]/, '').split('/').last.split('.').first
733
- end
734
3870
  end
735
3871
  end
736
3872
 
737
- # Handle 'as name'
738
- as_name = nil
739
- if current_token && current_token[:type] == :identifier && current_token[:value] == 'as'
740
- advance # Skip 'as'
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
741
3915
  if current_token && current_token[:type] == :identifier
742
- as_name = advance[:value]
3916
+ values << current_token[:value]
3917
+ advance
743
3918
  else
744
- @errors << {
745
- message: "Expected identifier after 'as'",
746
- position: @position,
747
- line: current_token ? current_token[:line] : 0,
748
- column: current_token ? current_token[:column] : 0
749
- }
3919
+ break
750
3920
  end
751
3921
  end
752
-
753
- {
754
- type: :import_statement,
755
- module: module_name,
756
- path: path,
757
- is_autoload: is_autoload,
758
- as_name: as_name,
759
- line: line,
760
- column: column
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
761
3928
  }
762
3929
  end
763
- def parse_export_statement
764
- token = advance # Skip 'export'
3930
+
3931
+ def parse_set_command
3932
+ token = advance # Skip 'set'
765
3933
  line = token[:line]
766
3934
  column = token[:column]
767
3935
 
768
- # Export can be followed by var/const/def/function declarations
769
- if !current_token
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
770
3946
  @errors << {
771
- message: "Expected declaration after export",
3947
+ message: "Expected option name after 'set'",
772
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,
773
3957
  line: line,
774
3958
  column: column
775
3959
  }
776
- return nil
777
3960
  end
778
3961
 
779
- exported_item = nil
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
780
3969
 
781
- if current_token[:type] == :keyword
782
- case current_token[:value]
783
- when 'def'
784
- exported_item = parse_def_function
785
- when 'function'
786
- exported_item = parse_legacy_function
787
- when 'var', 'const', 'final'
788
- exported_item = parse_variable_declaration
789
- when 'class'
790
- # Handle class export when implemented
791
- @errors << {
792
- message: "Class export not implemented yet",
793
- position: @position,
794
- line: current_token[:line],
795
- column: current_token[:column]
796
- }
797
- advance
798
- return nil
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
799
3982
  else
800
- @errors << {
801
- message: "Unexpected keyword after export: #{current_token[:value]}",
802
- position: @position,
803
- line: current_token[:line],
804
- column: current_token[:column]
805
- }
806
- advance
807
- return nil
3983
+ value = parse_expression
808
3984
  end
809
- else
810
- @errors << {
811
- message: "Expected declaration after export",
812
- position: @position,
813
- line: current_token[:line],
814
- column: current_token[:column]
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
815
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]
816
4062
  advance
817
- return nil
818
4063
  end
819
4064
 
820
4065
  {
821
- type: :export_statement,
822
- export: exported_item,
4066
+ type: :syntax_command,
4067
+ subcommand: subcommand,
4068
+ group_name: group_name,
4069
+ pattern: pattern,
4070
+ args: args.join(' ').strip,
823
4071
  line: line,
824
4072
  column: column
825
4073
  }
826
4074
  end
827
4075
 
828
- def parse_legacy_function
829
- token = advance # Skip 'function'
4076
+ def parse_highlight_command
4077
+ token = advance # Skip 'highlight'
830
4078
  line = token[:line]
831
4079
  column = token[:column]
832
4080
 
833
- # Check for bang (!) in function definition
834
- has_bang = false
835
- if current_token && current_token[:type] == :operator && current_token[:value] == '!'
836
- has_bang = true
837
- advance # Skip '!'
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
838
4086
  end
839
4087
 
840
- name = expect(:identifier)
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
841
4095
 
842
- # Parse parameter list
843
- expect(:paren_open)
844
- params = parse_parameter_list_legacy
845
- expect(:paren_close)
4096
+ # Parse = sign
4097
+ if current_token && current_token[:type] == :operator && current_token[:value] == '='
4098
+ advance
846
4099
 
847
- # Check for optional attributes (range, dict, abort, closure)
848
- attributes = []
849
- while current_token && current_token[:type] == :identifier
850
- if ['range', 'dict', 'abort', 'closure'].include?(current_token[:value])
851
- attributes << advance[:value]
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
852
4108
  else
853
- break
4109
+ advance
854
4110
  end
855
4111
  end
856
4112
 
857
- # Parse function body
858
- body = []
859
- while @position < @tokens.length
860
- if current_token && current_token[:type] == :keyword &&
861
- ['endfunction', 'endfunc'].include?(current_token[:value])
862
- break
863
- end
4113
+ {
4114
+ type: :highlight_command,
4115
+ group_name: group_name,
4116
+ attributes: attributes,
4117
+ line: line,
4118
+ column: column
4119
+ }
4120
+ end
864
4121
 
865
- stmt = parse_statement
866
- body << stmt if stmt
867
- end
4122
+ def parse_sleep_command
4123
+ token = advance # Skip 'sleep'
4124
+ line = token[:line]
4125
+ column = token[:column]
868
4126
 
869
- # Expect endfunction/endfunc
870
- end_token = advance # This should be 'endfunction' or 'endfunc'
871
- if end_token && end_token[:type] != :keyword &&
872
- !['endfunction', 'endfunc'].include?(end_token[:value])
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
873
4133
  @errors << {
874
- message: "Expected 'endfunction' or 'endfunc'",
4134
+ message: "Expected duration after 'sleep'",
875
4135
  position: @position,
876
- line: end_token ? end_token[:line] : 0,
877
- column: end_token ? end_token[:column] : 0
4136
+ line: current_token ? current_token[:line] : line,
4137
+ column: current_token ? current_token[:column] : column + 5
878
4138
  }
879
4139
  end
880
4140
 
881
4141
  {
882
- type: :legacy_function,
883
- name: name ? name[:value] : nil,
884
- params: params,
885
- has_bang: has_bang,
886
- attributes: attributes,
887
- body: body,
4142
+ type: :sleep_command,
4143
+ duration: duration,
888
4144
  line: line,
889
4145
  column: column
890
4146
  }
891
4147
  end
892
4148
 
893
- # Legacy function parameters are different - they use a:name syntax
894
- def parse_parameter_list_legacy
895
- params = []
4149
+ def parse_source_command
4150
+ token = advance # Skip 'source'
4151
+ line = token[:line]
4152
+ column = token[:column]
896
4153
 
897
- # Empty parameter list
898
- if current_token && current_token[:type] == :paren_close
899
- return params
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
900
4159
  end
901
4160
 
902
- loop do
903
- # Check for ... (varargs) in legacy function
904
- if current_token && current_token[:type] == :ellipsis
905
- advance
906
- params << { type: :var_args_legacy, name: '...' }
907
- break
908
- end
909
-
910
- if !current_token || current_token[:type] != :identifier
911
- @errors << {
912
- message: "Expected parameter name",
913
- position: @position,
914
- line: current_token ? current_token[:line] : 0,
915
- column: current_token ? current_token[:column] : 0
916
- }
917
- break
918
- end
919
-
920
- param_name = advance
921
-
922
- params << {
923
- type: :parameter,
924
- name: param_name[:value],
925
- line: param_name[:line],
926
- column: param_name[:column]
927
- }
928
-
929
- if current_token && current_token[:type] == :comma
930
- advance
931
- else
932
- break
933
- end
934
- end
4161
+ file_path = path_parts.join('')
935
4162
 
936
- params
4163
+ {
4164
+ type: :source_command,
4165
+ file_path: file_path,
4166
+ line: line,
4167
+ column: column
4168
+ }
937
4169
  end
938
-
939
4170
  end
940
4171
  end