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