vinter 0.3.0 → 0.5.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/ast_printer.rb +14 -0
- data/lib/vinter/cli.rb +49 -21
- data/lib/vinter/lexer.rb +402 -20
- data/lib/vinter/linter.rb +28 -3
- data/lib/vinter/parser.rb +2809 -724
- data/lib/vinter.rb +2 -1
- metadata +4 -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,12 +43,123 @@ 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
|
|
50
73
|
|
|
74
|
+
# Helper method to add errors with consistent formatting
|
|
75
|
+
def add_error(message, token = current_token)
|
|
76
|
+
@errors << {
|
|
77
|
+
message: message,
|
|
78
|
+
position: @position,
|
|
79
|
+
line: token ? token[:line] : 0,
|
|
80
|
+
column: token ? token[:column] : 0
|
|
81
|
+
}
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
# Helper method to expect a specific keyword value
|
|
85
|
+
def expect_keyword(value)
|
|
86
|
+
if current_token && current_token[:type] == :keyword && current_token[:value] == value
|
|
87
|
+
advance
|
|
88
|
+
true
|
|
89
|
+
else
|
|
90
|
+
add_error("Expected '#{value}' keyword")
|
|
91
|
+
false
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
# Helper method to parse a body of statements until one or more end keywords
|
|
96
|
+
# Returns array of parsed statements
|
|
97
|
+
def parse_body_until(*end_keywords)
|
|
98
|
+
body = []
|
|
99
|
+
while @position < @tokens.length
|
|
100
|
+
# Check if we've reached an end keyword
|
|
101
|
+
if current_token && current_token[:type] == :keyword &&
|
|
102
|
+
end_keywords.include?(current_token[:value])
|
|
103
|
+
break
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
stmt = parse_statement
|
|
107
|
+
body << stmt if stmt
|
|
108
|
+
end
|
|
109
|
+
body
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
# Helper method to expect and consume an end keyword
|
|
113
|
+
# Returns true if found, false and adds error if not
|
|
114
|
+
def expect_end_keyword(*keywords)
|
|
115
|
+
if current_token && current_token[:type] == :keyword &&
|
|
116
|
+
keywords.include?(current_token[:value])
|
|
117
|
+
advance
|
|
118
|
+
true
|
|
119
|
+
else
|
|
120
|
+
# Only add error if we haven't reached end of file
|
|
121
|
+
if @position < @tokens.length
|
|
122
|
+
add_error("Expected #{keywords.join(' or ')} to close block")
|
|
123
|
+
end
|
|
124
|
+
false
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
# Helper method to parse comma-separated loop variables
|
|
129
|
+
# Used by for loops with brackets or parentheses
|
|
130
|
+
def parse_loop_variables
|
|
131
|
+
loop_vars = []
|
|
132
|
+
|
|
133
|
+
loop do
|
|
134
|
+
if current_token && (current_token[:type] == :identifier ||
|
|
135
|
+
current_token[:type] == :local_variable ||
|
|
136
|
+
current_token[:type] == :global_variable ||
|
|
137
|
+
current_token[:type] == :script_local)
|
|
138
|
+
loop_vars << advance
|
|
139
|
+
else
|
|
140
|
+
add_error("Expected identifier in for loop variables")
|
|
141
|
+
break
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
if current_token && current_token[:type] == :comma
|
|
145
|
+
advance # Skip ','
|
|
146
|
+
else
|
|
147
|
+
break
|
|
148
|
+
end
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
loop_vars
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
# Helper method to expect 'in' keyword in for loops
|
|
155
|
+
def expect_in_keyword
|
|
156
|
+
if !current_token || (current_token[:type] != :identifier || current_token[:value] != 'in')
|
|
157
|
+
add_error("Expected 'in' after for loop variables")
|
|
158
|
+
else
|
|
159
|
+
advance # Skip 'in'
|
|
160
|
+
end
|
|
161
|
+
end
|
|
162
|
+
|
|
51
163
|
def parse_program
|
|
52
164
|
statements = []
|
|
53
165
|
|
|
@@ -65,15 +177,153 @@ module Vinter
|
|
|
65
177
|
{ type: :program, body: statements }
|
|
66
178
|
end
|
|
67
179
|
|
|
180
|
+
def parse_mapping_statement
|
|
181
|
+
token = current_token
|
|
182
|
+
advance # Skip the mapping command
|
|
183
|
+
line = token[:line]
|
|
184
|
+
column = token[:column]
|
|
185
|
+
mapping_command = token[:value]
|
|
186
|
+
|
|
187
|
+
# Parse options (like <silent>, <buffer>, etc.)
|
|
188
|
+
options = []
|
|
189
|
+
while current_token &&
|
|
190
|
+
(current_token[:type] == :identifier || current_token[:type] == :special_key) &&
|
|
191
|
+
current_token[:value].start_with?('<') &&
|
|
192
|
+
current_token[:value].end_with?('>')
|
|
193
|
+
options << current_token[:value]
|
|
194
|
+
advance
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
# Parse the key sequence
|
|
198
|
+
key_sequence = nil
|
|
199
|
+
if current_token
|
|
200
|
+
if current_token[:type] == :special_key ||
|
|
201
|
+
(current_token[:value].start_with?('<') && current_token[:value].end_with?('>'))
|
|
202
|
+
key_sequence = current_token[:value]
|
|
203
|
+
advance
|
|
204
|
+
else
|
|
205
|
+
key_sequence = current_token[:value]
|
|
206
|
+
advance
|
|
207
|
+
end
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
# Collect everything else until end of line as the mapping target
|
|
211
|
+
# This is raw text and shouldn't be parsed as an expression
|
|
212
|
+
target_tokens = []
|
|
213
|
+
|
|
214
|
+
# Continue collecting tokens until we hit a newline or comment
|
|
215
|
+
while current_token &&
|
|
216
|
+
current_token[:type] != :comment &&
|
|
217
|
+
(current_token[:type] != :whitespace || current_token[:value].strip != '')
|
|
218
|
+
|
|
219
|
+
# If we hit a newline not preceded by a continuation character, we're done
|
|
220
|
+
if current_token[:value] == "\n" &&
|
|
221
|
+
(target_tokens.empty? || target_tokens.last[:value][-1] != '\\')
|
|
222
|
+
break
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
target_tokens << current_token
|
|
226
|
+
advance
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
# Join the target tokens to form the raw mapping target
|
|
230
|
+
target = target_tokens.map { |t| t[:value] }.join('')
|
|
231
|
+
|
|
232
|
+
{
|
|
233
|
+
type: :mapping_statement,
|
|
234
|
+
command: mapping_command,
|
|
235
|
+
options: options,
|
|
236
|
+
key_sequence: key_sequence,
|
|
237
|
+
target: target,
|
|
238
|
+
line: line,
|
|
239
|
+
column: column
|
|
240
|
+
}
|
|
241
|
+
end
|
|
242
|
+
|
|
68
243
|
def parse_statement
|
|
69
244
|
if !current_token
|
|
70
245
|
return nil
|
|
71
246
|
end
|
|
247
|
+
start_token = current_token
|
|
72
248
|
|
|
73
|
-
|
|
249
|
+
# Handle pipe as command separator
|
|
250
|
+
if current_token[:type] == :operator && current_token[:value] == '|'
|
|
251
|
+
advance # Skip the pipe
|
|
252
|
+
return parse_statement
|
|
253
|
+
end
|
|
254
|
+
|
|
255
|
+
# Handle endif keyword outside normal if structure (likely from one-line if)
|
|
256
|
+
if current_token[:type] == :keyword && current_token[:value] == 'endif'
|
|
257
|
+
token = advance # Skip the endif
|
|
258
|
+
return {
|
|
259
|
+
type: :endif_marker,
|
|
260
|
+
line: token[:line],
|
|
261
|
+
column: token[:column]
|
|
262
|
+
}
|
|
263
|
+
end
|
|
264
|
+
|
|
265
|
+
# Add special cases for other ending keywords too to be thorough
|
|
266
|
+
if current_token[:type] == :keyword &&
|
|
267
|
+
['endwhile', 'endfor', 'endfunction', 'endfunc', 'enddef'].include?(current_token[:value])
|
|
268
|
+
token = advance # Skip the keyword
|
|
269
|
+
return {
|
|
270
|
+
type: :"#{token[:value]}_marker",
|
|
271
|
+
line: token[:line],
|
|
272
|
+
column: token[:column]
|
|
273
|
+
}
|
|
274
|
+
end
|
|
275
|
+
|
|
276
|
+
# Now, add specific handling for continue and break
|
|
277
|
+
if current_token[:type] == :keyword && current_token[:value] == 'continue'
|
|
278
|
+
token = advance # Skip 'continue'
|
|
279
|
+
return {
|
|
280
|
+
type: :continue_statement,
|
|
281
|
+
line: token[:line],
|
|
282
|
+
column: token[:column]
|
|
283
|
+
}
|
|
284
|
+
end
|
|
285
|
+
|
|
286
|
+
if current_token[:type] == :keyword && current_token[:value] == 'break'
|
|
287
|
+
token = advance # Skip 'break'
|
|
288
|
+
return {
|
|
289
|
+
type: :break_statement,
|
|
290
|
+
line: token[:line],
|
|
291
|
+
column: token[:column]
|
|
292
|
+
}
|
|
293
|
+
end
|
|
294
|
+
|
|
295
|
+
# Handle try-catch
|
|
296
|
+
if current_token[:type] == :keyword && current_token[:value] == 'try'
|
|
297
|
+
return parse_try_statement
|
|
298
|
+
end
|
|
299
|
+
|
|
300
|
+
# Handle throw
|
|
301
|
+
if current_token[:type] == :keyword && current_token[:value] == 'throw'
|
|
302
|
+
return parse_throw_statement
|
|
303
|
+
end
|
|
304
|
+
|
|
305
|
+
# Skip standalone catch, finally, endtry keywords outside of try blocks
|
|
306
|
+
if current_token[:type] == :keyword &&
|
|
307
|
+
['catch', 'finally', 'endtry'].include?(current_token[:value])
|
|
308
|
+
token = advance # Skip these keywords
|
|
309
|
+
return nil
|
|
310
|
+
end
|
|
311
|
+
|
|
312
|
+
# Add case for mapping commands
|
|
313
|
+
if current_token[:type] == :keyword &&
|
|
314
|
+
['nnoremap', 'nmap', 'inoremap', 'imap', 'vnoremap', 'vmap',
|
|
315
|
+
'xnoremap', 'xmap', 'cnoremap', 'cmap', 'noremap', 'map'].include?(current_token[:value])
|
|
316
|
+
parse_mapping_statement
|
|
317
|
+
elsif current_token[:type] == :runtime_command
|
|
318
|
+
parse_runtime_statement
|
|
319
|
+
elsif current_token[:type] == :keyword && current_token[:value] == 'runtime'
|
|
320
|
+
parse_runtime_statement
|
|
321
|
+
elsif current_token[:type] == :keyword
|
|
74
322
|
case current_token[:value]
|
|
75
323
|
when 'if'
|
|
76
324
|
parse_if_statement
|
|
325
|
+
when 'command'
|
|
326
|
+
parse_command_definition
|
|
77
327
|
when 'while'
|
|
78
328
|
parse_while_statement
|
|
79
329
|
when 'for'
|
|
@@ -81,7 +331,18 @@ module Vinter
|
|
|
81
331
|
when 'def'
|
|
82
332
|
parse_def_function
|
|
83
333
|
when 'function'
|
|
84
|
-
|
|
334
|
+
# Only parse as function declaration if it looks like one
|
|
335
|
+
# Function declarations are followed by ! or identifier (function name)
|
|
336
|
+
if peek_token && (
|
|
337
|
+
(peek_token[:type] == :operator && peek_token[:value] == '!') ||
|
|
338
|
+
peek_token[:type] == :identifier ||
|
|
339
|
+
peek_token[:type] == :script_local
|
|
340
|
+
)
|
|
341
|
+
parse_legacy_function
|
|
342
|
+
else
|
|
343
|
+
# Not a function declaration, parse as expression statement
|
|
344
|
+
parse_expression_statement
|
|
345
|
+
end
|
|
85
346
|
when 'return'
|
|
86
347
|
parse_return_statement
|
|
87
348
|
when 'var', 'const', 'final'
|
|
@@ -93,14 +354,36 @@ module Vinter
|
|
|
93
354
|
when 'vim9script'
|
|
94
355
|
token = advance # Skip 'vim9script'
|
|
95
356
|
{ type: :vim9script_declaration, line: token[:line], column: token[:column] }
|
|
357
|
+
when 'scriptencoding'
|
|
358
|
+
parse_scriptencoding
|
|
96
359
|
when 'autocmd'
|
|
97
360
|
parse_autocmd_statement
|
|
98
|
-
when 'execute'
|
|
361
|
+
when 'execute', 'exec'
|
|
99
362
|
parse_execute_statement
|
|
100
363
|
when 'let'
|
|
101
|
-
parse_let_statement
|
|
102
|
-
when '
|
|
364
|
+
parse_let_statement
|
|
365
|
+
when 'unlet'
|
|
366
|
+
parse_unlet_statement
|
|
367
|
+
when 'echohl', 'echomsg', 'echoerr', 'echom'
|
|
103
368
|
parse_echo_statement
|
|
369
|
+
when 'augroup'
|
|
370
|
+
parse_augroup_statement
|
|
371
|
+
when 'silent'
|
|
372
|
+
parse_silent_command
|
|
373
|
+
when 'call'
|
|
374
|
+
parse_call_statement
|
|
375
|
+
when 'delete'
|
|
376
|
+
parse_delete_statement
|
|
377
|
+
when 'set', 'setlocal'
|
|
378
|
+
parse_set_command
|
|
379
|
+
when 'syntax'
|
|
380
|
+
parse_syntax_command
|
|
381
|
+
when 'highlight'
|
|
382
|
+
parse_highlight_command
|
|
383
|
+
when 'sleep'
|
|
384
|
+
parse_sleep_command
|
|
385
|
+
when 'source'
|
|
386
|
+
parse_source_command
|
|
104
387
|
else
|
|
105
388
|
@warnings << {
|
|
106
389
|
message: "Unexpected keyword: #{current_token[:value]}",
|
|
@@ -114,11 +397,51 @@ module Vinter
|
|
|
114
397
|
elsif current_token[:type] == :identifier
|
|
115
398
|
if current_token[:value] == "echo"
|
|
116
399
|
parse_echo_statement
|
|
400
|
+
elsif current_token[:value] == "augroup"
|
|
401
|
+
parse_augroup_statement
|
|
402
|
+
elsif current_token[:value] == "au" || current_token[:value] == "autocmd"
|
|
403
|
+
parse_autocmd_statement
|
|
404
|
+
elsif current_token[:value] == "filter" || current_token[:value] == "filt"
|
|
405
|
+
parse_filter_command
|
|
406
|
+
elsif current_token[:value] == "command"
|
|
407
|
+
parse_command_definition
|
|
117
408
|
else
|
|
118
409
|
parse_expression_statement
|
|
119
410
|
end
|
|
120
411
|
elsif current_token[:type] == :comment
|
|
121
412
|
parse_comment
|
|
413
|
+
elsif current_token[:type] == :string && current_token[:value].start_with?('"')
|
|
414
|
+
parse_comment
|
|
415
|
+
# token = current_token
|
|
416
|
+
# line = token[:line]
|
|
417
|
+
# column = token[:column]
|
|
418
|
+
# value = token[:value]
|
|
419
|
+
# advance
|
|
420
|
+
# { type: :comment, value: value, line: line, column: column }
|
|
421
|
+
elsif current_token[:type] == :silent_bang
|
|
422
|
+
parse_silent_command
|
|
423
|
+
elsif current_token[:type] == :identifier && current_token[:value] == 'delete'
|
|
424
|
+
parse_delete_command
|
|
425
|
+
elsif current_token[:type] == :percentage
|
|
426
|
+
parse_range_command
|
|
427
|
+
elsif current_token[:type] == :global_variable
|
|
428
|
+
name = current_token[:value]
|
|
429
|
+
#advance#skip value
|
|
430
|
+
#advance#operator
|
|
431
|
+
##parse_expression_statement
|
|
432
|
+
#advance#value
|
|
433
|
+
while peek_token[:line] == current_token[:line]
|
|
434
|
+
advance
|
|
435
|
+
end
|
|
436
|
+
advance
|
|
437
|
+
{
|
|
438
|
+
type: :global_variable,
|
|
439
|
+
name: name
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
elsif current_token[:type] == :vimfuncs
|
|
443
|
+
advance
|
|
444
|
+
parse_function_call(current_token[:value], current_token[:line], current_token[:column])
|
|
122
445
|
else
|
|
123
446
|
@warnings << {
|
|
124
447
|
message: "Unexpected token type: #{current_token[:type]}",
|
|
@@ -131,391 +454,666 @@ module Vinter
|
|
|
131
454
|
end
|
|
132
455
|
end
|
|
133
456
|
|
|
134
|
-
def
|
|
135
|
-
token = advance # Skip '
|
|
457
|
+
def parse_range_command
|
|
458
|
+
token = advance # Skip '%'
|
|
136
459
|
line = token[:line]
|
|
137
460
|
column = token[:column]
|
|
138
461
|
|
|
139
|
-
# Parse
|
|
140
|
-
|
|
141
|
-
expressions = []
|
|
142
|
-
expr = parse_expression
|
|
143
|
-
expressions << expr if expr
|
|
462
|
+
# Parse the command that follows the range
|
|
463
|
+
command = parse_statement
|
|
144
464
|
|
|
145
|
-
# Return the execute statement
|
|
146
465
|
{
|
|
147
|
-
type: :
|
|
148
|
-
|
|
466
|
+
type: :range_command,
|
|
467
|
+
range: '%',
|
|
468
|
+
command: command,
|
|
149
469
|
line: line,
|
|
150
470
|
column: column
|
|
151
471
|
}
|
|
152
472
|
end
|
|
153
473
|
|
|
154
|
-
def
|
|
155
|
-
token = advance # Skip '
|
|
474
|
+
def parse_runtime_statement
|
|
475
|
+
token = advance # Skip 'runtime' or 'runtime!'
|
|
156
476
|
line = token[:line]
|
|
157
477
|
column = token[:column]
|
|
158
478
|
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
type: current_token[:type],
|
|
166
|
-
name: current_token[:value],
|
|
167
|
-
line: current_token[:line],
|
|
168
|
-
column: current_token[:column]
|
|
169
|
-
}
|
|
170
|
-
advance
|
|
171
|
-
else
|
|
172
|
-
@errors << {
|
|
173
|
-
message: "Expected variable name after let",
|
|
174
|
-
position: @position,
|
|
175
|
-
line: current_token ? current_token[:line] : 0,
|
|
176
|
-
column: current_token ? current_token[:column] : 0
|
|
177
|
-
}
|
|
178
|
-
end
|
|
479
|
+
is_bang = token[:type] == :runtime_command ||
|
|
480
|
+
(token[:value] == 'runtime' && current_token && current_token[:type] == :operator && current_token[:value] == '!')
|
|
481
|
+
|
|
482
|
+
# Skip the '!' if it's separate token
|
|
483
|
+
if token[:type] != :runtime_command && is_bang
|
|
484
|
+
advance # Skip '!'
|
|
179
485
|
end
|
|
180
486
|
|
|
181
|
-
#
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
operator = current_token[:value]
|
|
487
|
+
# Collect the pattern argument
|
|
488
|
+
pattern_parts = []
|
|
489
|
+
while @position < @tokens.length &&
|
|
490
|
+
!(current_token[:type] == :keyword || current_token[:value] == "\n")
|
|
491
|
+
pattern_parts << current_token[:value]
|
|
187
492
|
advance
|
|
188
|
-
else
|
|
189
|
-
@errors << {
|
|
190
|
-
message: "Expected assignment operator after variable in let statement",
|
|
191
|
-
position: @position,
|
|
192
|
-
line: current_token ? current_token[:line] : 0,
|
|
193
|
-
column: current_token ? current_token[:column] : 0
|
|
194
|
-
}
|
|
195
493
|
end
|
|
196
494
|
|
|
197
|
-
|
|
198
|
-
value = parse_expression
|
|
495
|
+
pattern = pattern_parts.join('').strip
|
|
199
496
|
|
|
200
497
|
{
|
|
201
|
-
type: :
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
value: value,
|
|
498
|
+
type: :runtime_statement,
|
|
499
|
+
bang: is_bang,
|
|
500
|
+
pattern: pattern,
|
|
205
501
|
line: line,
|
|
206
502
|
column: column
|
|
207
503
|
}
|
|
208
504
|
end
|
|
209
505
|
|
|
210
|
-
def
|
|
211
|
-
token = advance # Skip '
|
|
506
|
+
def parse_filter_command
|
|
507
|
+
token = advance # Skip 'execute'
|
|
212
508
|
line = token[:line]
|
|
213
509
|
column = token[:column]
|
|
510
|
+
# lets count the parens that are created as we parse through the filter command
|
|
511
|
+
expect(:paren_open)
|
|
512
|
+
open_parens = 1
|
|
513
|
+
while open_parens > 0
|
|
514
|
+
advance
|
|
515
|
+
if current_token[:type] == :paren_open
|
|
516
|
+
open_parens += 1
|
|
517
|
+
elsif current_token[:type] == :paren_close
|
|
518
|
+
open_parens -= 1
|
|
519
|
+
end
|
|
520
|
+
end
|
|
521
|
+
advance
|
|
214
522
|
|
|
215
|
-
# Parse
|
|
216
|
-
|
|
523
|
+
# Parse arguments - typically string expressions with concatenation
|
|
524
|
+
# Just accept any tokens until we hit a statement terminator or another command
|
|
525
|
+
#expressions = []
|
|
526
|
+
#expr = parse_expression
|
|
527
|
+
#expr = parse_string
|
|
528
|
+
#expressions << expr if expr
|
|
529
|
+
|
|
530
|
+
# Return the execute statement
|
|
531
|
+
{
|
|
532
|
+
type: :filter_command,
|
|
533
|
+
#expressions: expressions,
|
|
534
|
+
line: line,
|
|
535
|
+
column: column
|
|
536
|
+
}
|
|
537
|
+
end
|
|
538
|
+
|
|
539
|
+
|
|
540
|
+
|
|
541
|
+
def parse_augroup_statement
|
|
542
|
+
token = advance # Skip 'augroup'
|
|
543
|
+
line = token[:line]
|
|
544
|
+
column = token[:column]
|
|
545
|
+
|
|
546
|
+
# Get the augroup name
|
|
547
|
+
name = nil
|
|
217
548
|
if current_token && current_token[:type] == :identifier
|
|
218
|
-
|
|
549
|
+
name = current_token[:value]
|
|
550
|
+
advance
|
|
219
551
|
else
|
|
220
|
-
|
|
221
|
-
message: "Expected event name after 'autocmd'",
|
|
222
|
-
position: @position,
|
|
223
|
-
line: current_token ? current_token[:line] : 0,
|
|
224
|
-
column: current_token ? current_token[:column] : 0
|
|
225
|
-
}
|
|
552
|
+
add_error("Expected augroup name")
|
|
226
553
|
end
|
|
227
554
|
|
|
228
|
-
#
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
555
|
+
# Check for augroup END
|
|
556
|
+
if name && name.upcase == "END"
|
|
557
|
+
return {
|
|
558
|
+
type: :augroup_end,
|
|
559
|
+
line: line,
|
|
560
|
+
column: column
|
|
561
|
+
}
|
|
233
562
|
end
|
|
234
563
|
|
|
235
|
-
# Parse
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
when 'if'
|
|
249
|
-
commands << parse_if_statement
|
|
250
|
-
when 'echo'
|
|
251
|
-
commands << parse_echo_statement
|
|
252
|
-
# Add other command types as needed
|
|
253
|
-
else
|
|
254
|
-
# Generic command handling
|
|
255
|
-
cmd = parse_expression_statement
|
|
256
|
-
commands << cmd if cmd
|
|
564
|
+
# Parse statements within the augroup until we find 'augroup END'
|
|
565
|
+
body = []
|
|
566
|
+
while @position < @tokens.length
|
|
567
|
+
# Check for 'augroup END'
|
|
568
|
+
if (current_token[:type] == :keyword && current_token[:value] == 'augroup') ||
|
|
569
|
+
(current_token[:type] == :identifier && current_token[:value] == 'augroup')
|
|
570
|
+
# Look ahead for END
|
|
571
|
+
if peek_token &&
|
|
572
|
+
((peek_token[:type] == :identifier && peek_token[:value].upcase == 'END') ||
|
|
573
|
+
(peek_token[:type] == :keyword && peek_token[:value].upcase == 'END'))
|
|
574
|
+
advance # Skip 'augroup'
|
|
575
|
+
advance # Skip 'END'
|
|
576
|
+
break
|
|
257
577
|
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
578
|
end
|
|
264
579
|
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
in_command = false
|
|
268
|
-
end
|
|
580
|
+
stmt = parse_statement
|
|
581
|
+
body << stmt if stmt
|
|
269
582
|
end
|
|
270
583
|
|
|
271
|
-
|
|
272
|
-
type: :
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
commands: commands,
|
|
584
|
+
{
|
|
585
|
+
type: :augroup_statement,
|
|
586
|
+
name: name,
|
|
587
|
+
body: body,
|
|
276
588
|
line: line,
|
|
277
589
|
column: column
|
|
278
590
|
}
|
|
279
591
|
end
|
|
280
592
|
|
|
281
|
-
def
|
|
282
|
-
token = advance #Skip '
|
|
593
|
+
def parse_execute_statement
|
|
594
|
+
token = advance # Skip 'execute'
|
|
283
595
|
line = token[:line]
|
|
284
596
|
column = token[:column]
|
|
285
597
|
|
|
286
|
-
|
|
598
|
+
# Parse arguments - typically string expressions with concatenation
|
|
599
|
+
# Just accept any tokens until we hit a statement terminator or another command
|
|
600
|
+
expressions = []
|
|
601
|
+
expr = parse_expression
|
|
602
|
+
#expr = parse_string
|
|
603
|
+
expressions << expr if expr
|
|
287
604
|
|
|
605
|
+
# Return the execute statement
|
|
288
606
|
{
|
|
289
|
-
type: :
|
|
290
|
-
|
|
607
|
+
type: :execute_statement,
|
|
608
|
+
expressions: expressions,
|
|
291
609
|
line: line,
|
|
292
610
|
column: column
|
|
293
611
|
}
|
|
294
612
|
end
|
|
295
613
|
|
|
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'
|
|
614
|
+
def parse_let_statement
|
|
615
|
+
# binding.pry
|
|
616
|
+
token = advance # Skip 'let'
|
|
306
617
|
line = token[:line]
|
|
307
618
|
column = token[:column]
|
|
308
|
-
condition = parse_expression
|
|
309
619
|
|
|
310
|
-
|
|
311
|
-
|
|
620
|
+
# Parse the target variable
|
|
621
|
+
target = nil
|
|
622
|
+
if current_token
|
|
623
|
+
case current_token[:type]
|
|
624
|
+
when :identifier, :global_variable, :script_local, :buffer_local, :window_local, :tab_local, :arg_variable, :option_variable, :special_variable, :local_variable
|
|
625
|
+
target = {
|
|
626
|
+
type: current_token[:type],
|
|
627
|
+
name: current_token[:value],
|
|
628
|
+
line: current_token[:line],
|
|
629
|
+
column: current_token[:column]
|
|
630
|
+
}
|
|
631
|
+
advance
|
|
312
632
|
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
633
|
+
# Check for property access with dot notation (e.g., person.job)
|
|
634
|
+
if current_token && current_token[:type] == :operator && current_token[:value] == '.'
|
|
635
|
+
dot_token = advance # Skip '.'
|
|
636
|
+
|
|
637
|
+
# Next token should be an identifier (property name)
|
|
638
|
+
if current_token && (current_token[:type] == :identifier || current_token[:type] == :keyword)
|
|
639
|
+
property_token = advance # Get the property name
|
|
640
|
+
|
|
641
|
+
# Update the target to be a property access
|
|
642
|
+
target = {
|
|
643
|
+
type: :property_access,
|
|
644
|
+
object: target,
|
|
645
|
+
property: property_token[:value],
|
|
646
|
+
line: dot_token[:line],
|
|
647
|
+
column: dot_token[:column]
|
|
648
|
+
}
|
|
649
|
+
else
|
|
650
|
+
add_error("Expected property name after '.'")
|
|
651
|
+
end
|
|
652
|
+
end
|
|
653
|
+
|
|
654
|
+
# Check for chained indexed access (dictionary key lookup)
|
|
655
|
+
while current_token && current_token[:type] == :bracket_open
|
|
656
|
+
bracket_token = advance # Skip '['
|
|
657
|
+
index_expr = parse_expression
|
|
658
|
+
expect(:bracket_close) # Skip ']'
|
|
659
|
+
|
|
660
|
+
# Update target to be an indexed access
|
|
661
|
+
target = {
|
|
662
|
+
type: :indexed_access,
|
|
663
|
+
object: target,
|
|
664
|
+
index: index_expr,
|
|
665
|
+
line: bracket_token[:line],
|
|
666
|
+
column: bracket_token[:column]
|
|
667
|
+
}
|
|
668
|
+
end
|
|
669
|
+
when :register_access
|
|
670
|
+
target = {
|
|
671
|
+
type: :register_access,
|
|
672
|
+
register: current_token[:value],
|
|
673
|
+
line: current_token[:line],
|
|
674
|
+
column: current_token[:column]
|
|
675
|
+
}
|
|
676
|
+
advance
|
|
677
|
+
when :bracket_open
|
|
678
|
+
# Handle array destructuring like: let [key, value] = split(header, ': ')
|
|
679
|
+
bracket_token = advance # Skip '['
|
|
680
|
+
|
|
681
|
+
# Parse the variable names inside the brackets
|
|
682
|
+
destructuring_targets = []
|
|
683
|
+
|
|
684
|
+
# Parse comma-separated list of variables
|
|
685
|
+
while current_token && current_token[:type] != :bracket_close
|
|
686
|
+
# Skip commas between variables
|
|
687
|
+
if current_token[:type] == :comma
|
|
688
|
+
advance
|
|
689
|
+
next
|
|
690
|
+
end
|
|
691
|
+
|
|
692
|
+
# Parse the variable name
|
|
693
|
+
if current_token && (
|
|
694
|
+
current_token[:type] == :identifier ||
|
|
695
|
+
current_token[:type] == :global_variable ||
|
|
696
|
+
current_token[:type] == :script_local ||
|
|
697
|
+
current_token[:type] == :arg_variable ||
|
|
698
|
+
current_token[:type] == :option_variable ||
|
|
699
|
+
current_token[:type] == :special_variable ||
|
|
700
|
+
current_token[:type] == :local_variable
|
|
701
|
+
)
|
|
702
|
+
destructuring_targets << {
|
|
703
|
+
type: current_token[:type],
|
|
704
|
+
name: current_token[:value],
|
|
705
|
+
line: current_token[:line],
|
|
706
|
+
column: current_token[:column]
|
|
707
|
+
}
|
|
708
|
+
advance
|
|
709
|
+
else
|
|
710
|
+
add_error("Expected variable name in destructuring assignment")
|
|
711
|
+
# Try to recover by advancing to next comma or closing bracket
|
|
712
|
+
while current_token && current_token[:type] != :comma && current_token[:type] != :bracket_close
|
|
713
|
+
advance
|
|
714
|
+
end
|
|
715
|
+
end
|
|
716
|
+
end
|
|
717
|
+
|
|
718
|
+
expect(:bracket_close) # Skip ']'
|
|
719
|
+
|
|
720
|
+
target = {
|
|
721
|
+
type: :destructuring_assignment,
|
|
722
|
+
targets: destructuring_targets,
|
|
723
|
+
line: bracket_token[:line],
|
|
724
|
+
column: bracket_token[:column]
|
|
725
|
+
}
|
|
726
|
+
else
|
|
727
|
+
add_error("Expected variable name after let")
|
|
318
728
|
end
|
|
729
|
+
end
|
|
319
730
|
|
|
320
|
-
|
|
321
|
-
|
|
731
|
+
# Skip the '=' or other assignment operator
|
|
732
|
+
operator = nil
|
|
733
|
+
if current_token && (
|
|
734
|
+
(current_token[:type] == :operator && current_token[:value] == '=') ||
|
|
735
|
+
current_token[:type] == :compound_operator)
|
|
736
|
+
operator = current_token[:value]
|
|
737
|
+
advance
|
|
738
|
+
else
|
|
739
|
+
add_error("Expected assignment operator after variable in let statement")
|
|
322
740
|
end
|
|
323
741
|
|
|
324
|
-
#
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
742
|
+
# Parse the value expression
|
|
743
|
+
value = nil
|
|
744
|
+
|
|
745
|
+
# Special handling for function() references in let statements
|
|
746
|
+
if current_token && current_token[:type] == :keyword && current_token[:value] == 'function'
|
|
747
|
+
advance # Skip 'function'
|
|
328
748
|
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
break
|
|
333
|
-
end
|
|
749
|
+
# Expect opening parenthesis
|
|
750
|
+
if current_token && current_token[:type] == :paren_open
|
|
751
|
+
paren_token = advance # Skip '('
|
|
334
752
|
|
|
335
|
-
|
|
336
|
-
|
|
753
|
+
# Parse the function name as a string
|
|
754
|
+
func_name = nil
|
|
755
|
+
if current_token && current_token[:type] == :string
|
|
756
|
+
func_name = current_token[:value]
|
|
757
|
+
advance
|
|
758
|
+
else
|
|
759
|
+
add_error("Expected string with function name in function() call")
|
|
337
760
|
end
|
|
338
|
-
|
|
339
|
-
#
|
|
340
|
-
|
|
761
|
+
|
|
762
|
+
# Expect closing parenthesis
|
|
763
|
+
expect(:paren_close) # Skip ')'
|
|
764
|
+
|
|
765
|
+
value = {
|
|
766
|
+
type: :function_reference,
|
|
767
|
+
function_name: func_name,
|
|
768
|
+
line: line,
|
|
769
|
+
column: column
|
|
770
|
+
}
|
|
771
|
+
else
|
|
772
|
+
# If not followed by parenthesis, parse as a normal expression
|
|
773
|
+
@position -= 1 # Go back to the 'function' keyword
|
|
774
|
+
value = parse_expression
|
|
341
775
|
end
|
|
776
|
+
else
|
|
777
|
+
# Normal expression parsing
|
|
778
|
+
value = parse_expression
|
|
342
779
|
end
|
|
780
|
+
{
|
|
781
|
+
type: :let_statement,
|
|
782
|
+
target: target,
|
|
783
|
+
operator: operator,
|
|
784
|
+
value: value,
|
|
785
|
+
line: line,
|
|
786
|
+
column: column
|
|
787
|
+
}
|
|
788
|
+
end
|
|
789
|
+
|
|
790
|
+
def parse_unlet_statement
|
|
791
|
+
token = advance # Skip 'unlet'
|
|
792
|
+
line = token[:line]
|
|
793
|
+
column = token[:column]
|
|
794
|
+
|
|
795
|
+
# Parse the variable to unlet
|
|
796
|
+
target = nil
|
|
797
|
+
if current_token
|
|
798
|
+
case current_token[:type]
|
|
799
|
+
when :identifier, :global_variable, :script_local, :buffer_local, :window_local, :tab_local, :arg_variable, :option_variable, :special_variable, :local_variable
|
|
800
|
+
target = {
|
|
801
|
+
type: current_token[:type],
|
|
802
|
+
name: current_token[:value],
|
|
803
|
+
line: current_token[:line],
|
|
804
|
+
column: current_token[:column]
|
|
805
|
+
}
|
|
806
|
+
advance
|
|
343
807
|
|
|
344
|
-
|
|
345
|
-
|
|
808
|
+
# Check for chained indexed access (like variable[key])
|
|
809
|
+
while current_token && current_token[:type] == :bracket_open
|
|
810
|
+
bracket_token = advance # Skip '['
|
|
811
|
+
index_expr = parse_expression
|
|
812
|
+
expect(:bracket_close) # Skip ']'
|
|
813
|
+
|
|
814
|
+
# Update target to be an indexed access
|
|
815
|
+
target = {
|
|
816
|
+
type: :indexed_access,
|
|
817
|
+
object: target,
|
|
818
|
+
index: index_expr,
|
|
819
|
+
line: bracket_token[:line],
|
|
820
|
+
column: bracket_token[:column]
|
|
821
|
+
}
|
|
822
|
+
end
|
|
823
|
+
else
|
|
824
|
+
@errors << {
|
|
825
|
+
message: "Expected variable name after 'unlet'",
|
|
826
|
+
position: @position,
|
|
827
|
+
line: current_token ? current_token[:line] : 0,
|
|
828
|
+
column: current_token ? current_token[:column] : 0
|
|
829
|
+
}
|
|
830
|
+
end
|
|
831
|
+
end
|
|
346
832
|
|
|
347
833
|
{
|
|
348
|
-
type: :
|
|
349
|
-
|
|
350
|
-
then_branch: then_branch,
|
|
351
|
-
else_branch: else_branch,
|
|
834
|
+
type: :unlet_statement,
|
|
835
|
+
target: target,
|
|
352
836
|
line: line,
|
|
353
837
|
column: column
|
|
354
838
|
}
|
|
355
839
|
end
|
|
356
840
|
|
|
357
|
-
def
|
|
358
|
-
token = advance # Skip '
|
|
841
|
+
def parse_autocmd_statement
|
|
842
|
+
token = advance # Skip 'autocmd'
|
|
359
843
|
line = token[:line]
|
|
360
844
|
column = token[:column]
|
|
361
|
-
condition = parse_expression
|
|
362
845
|
|
|
363
|
-
|
|
846
|
+
# Parse event name (like BufNewFile or VimEnter)
|
|
847
|
+
event = nil
|
|
848
|
+
if current_token && current_token[:type] == :identifier
|
|
849
|
+
event = advance[:value]
|
|
850
|
+
else
|
|
851
|
+
@errors << {
|
|
852
|
+
message: "Expected event name after 'autocmd'",
|
|
853
|
+
position: @position,
|
|
854
|
+
line: current_token ? current_token[:line] : 0,
|
|
855
|
+
column: current_token ? current_token[:column] : 0
|
|
856
|
+
}
|
|
857
|
+
end
|
|
364
858
|
|
|
365
|
-
# Parse
|
|
859
|
+
# Parse pattern (like *.match or *)
|
|
860
|
+
pattern = nil
|
|
861
|
+
if current_token
|
|
862
|
+
pattern = current_token[:value]
|
|
863
|
+
advance
|
|
864
|
+
end
|
|
865
|
+
|
|
866
|
+
# Parse everything after as the command - collect as raw tokens
|
|
867
|
+
command_parts = []
|
|
366
868
|
while @position < @tokens.length
|
|
367
|
-
if current_token[:type] == :
|
|
869
|
+
if !current_token || current_token[:type] == :comment ||
|
|
870
|
+
(current_token[:value] == "\n" && !current_token[:value].start_with?("\\"))
|
|
368
871
|
break
|
|
369
872
|
end
|
|
370
873
|
|
|
371
|
-
|
|
372
|
-
|
|
874
|
+
command_parts << current_token
|
|
875
|
+
advance
|
|
373
876
|
end
|
|
374
877
|
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
condition: condition,
|
|
381
|
-
body: body,
|
|
878
|
+
return {
|
|
879
|
+
type: :autocmd_statement,
|
|
880
|
+
event: event,
|
|
881
|
+
pattern: pattern,
|
|
882
|
+
command_parts: command_parts,
|
|
382
883
|
line: line,
|
|
383
884
|
column: column
|
|
384
885
|
}
|
|
385
886
|
end
|
|
386
887
|
|
|
387
|
-
def
|
|
388
|
-
token = advance #
|
|
888
|
+
def parse_echo_statement
|
|
889
|
+
token = advance #Skip 'echo'
|
|
389
890
|
line = token[:line]
|
|
390
891
|
column = token[:column]
|
|
391
892
|
|
|
392
|
-
|
|
393
|
-
if current_token && current_token[:type] == :paren_open
|
|
394
|
-
# Handle tuple assignment: for (key, val) in dict
|
|
395
|
-
advance # Skip '('
|
|
893
|
+
expression = parse_expression
|
|
396
894
|
|
|
397
|
-
|
|
895
|
+
{
|
|
896
|
+
type: :echo_statement,
|
|
897
|
+
expression: expression,
|
|
898
|
+
line: line,
|
|
899
|
+
column: column
|
|
900
|
+
}
|
|
901
|
+
end
|
|
398
902
|
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
903
|
+
def parse_comment
|
|
904
|
+
original_token = current_token
|
|
905
|
+
line = original_token[:line]
|
|
906
|
+
column = original_token[:column]
|
|
907
|
+
comment = original_token[:value]
|
|
908
|
+
|
|
909
|
+
# puts "INSIDE PARSE COMMENT"
|
|
910
|
+
# puts "Current token: #{current_token.inspect}"
|
|
911
|
+
# puts "Peek token: #{peek_token.inspect}"
|
|
912
|
+
|
|
913
|
+
# Check if the comment contains a newline
|
|
914
|
+
if comment.include?("\n")
|
|
915
|
+
# Split the comment at the newline
|
|
916
|
+
parts = comment.split("\n", 2)
|
|
917
|
+
comment = parts[0] # Only use the part before newline
|
|
918
|
+
|
|
919
|
+
# Create a new token for the content after the newline
|
|
920
|
+
remainder = parts[1].strip
|
|
921
|
+
|
|
922
|
+
if !remainder.empty?
|
|
923
|
+
# Position correctly - we'll advance past the current token
|
|
924
|
+
# But should process the remainder separately later
|
|
925
|
+
|
|
926
|
+
# For debugging only, you can print what's being processed
|
|
927
|
+
# puts "Found comment with newline. Using: '#{comment}', Remainder: '#{remainder}'"
|
|
928
|
+
|
|
929
|
+
# Don't call advance() - we'll modify the current token instead
|
|
930
|
+
@tokens[@position] = {
|
|
931
|
+
type: :string, # Keep original type
|
|
932
|
+
value: comment, # Use only the part before newline
|
|
933
|
+
line: line,
|
|
934
|
+
column: column
|
|
935
|
+
}
|
|
411
936
|
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
937
|
+
# Insert the remainder as a new token after the current one
|
|
938
|
+
@tokens.insert(@position + 1, {
|
|
939
|
+
type: :comment, # Same type for consistency
|
|
940
|
+
value: remainder, # Preserve as a string token
|
|
941
|
+
line: line + 1, # Increment line number for the part after newline
|
|
942
|
+
column: 0 # Approximate column based on indentation
|
|
943
|
+
})
|
|
417
944
|
end
|
|
945
|
+
end
|
|
418
946
|
|
|
419
|
-
|
|
947
|
+
# Now advance past the (potentially modified) current token
|
|
948
|
+
advance
|
|
949
|
+
{ type: :comment, value: comment, line: line, column: column }
|
|
950
|
+
end
|
|
420
951
|
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
line: current_token ? current_token[:line] : 0,
|
|
426
|
-
column: current_token ? current_token[:column] : 0
|
|
427
|
-
}
|
|
428
|
-
else
|
|
429
|
-
advance # Skip 'in'
|
|
430
|
-
end
|
|
952
|
+
def parse_if_statement
|
|
953
|
+
token = advance # Skip 'if'
|
|
954
|
+
line = token[:line]
|
|
955
|
+
column = token[:column]
|
|
431
956
|
|
|
432
|
-
|
|
957
|
+
condition = parse_expression
|
|
433
958
|
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
while @position < @tokens.length
|
|
437
|
-
if current_token[:type] == :keyword && current_token[:value] == 'endfor'
|
|
438
|
-
break
|
|
439
|
-
end
|
|
959
|
+
then_branch = []
|
|
960
|
+
else_branch = []
|
|
440
961
|
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
end
|
|
962
|
+
# Check if this might be a one-line if (look ahead for pipe character)
|
|
963
|
+
one_line_if = false
|
|
444
964
|
|
|
445
|
-
|
|
446
|
-
|
|
965
|
+
if current_token && current_token[:type] == :operator && current_token[:value] == '|'
|
|
966
|
+
one_line_if = true
|
|
967
|
+
advance # Skip the pipe
|
|
447
968
|
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
iterable: iterable,
|
|
452
|
-
body: body,
|
|
453
|
-
line: line,
|
|
454
|
-
column: column
|
|
455
|
-
}
|
|
456
|
-
else
|
|
457
|
-
# Simple for var in list
|
|
458
|
-
if !current_token || current_token[:type] != :identifier
|
|
459
|
-
@errors << {
|
|
460
|
-
message: "Expected identifier as for loop variable",
|
|
461
|
-
position: @position,
|
|
462
|
-
line: current_token ? current_token[:line] : 0,
|
|
463
|
-
column: current_token ? current_token[:column] : 0
|
|
464
|
-
}
|
|
465
|
-
return nil
|
|
466
|
-
end
|
|
969
|
+
# Parse the then statement
|
|
970
|
+
stmt = parse_statement
|
|
971
|
+
then_branch << stmt if stmt
|
|
467
972
|
|
|
468
|
-
|
|
973
|
+
# Check for the closing pipe and endif
|
|
974
|
+
if current_token && current_token[:type] == :operator && current_token[:value] == '|'
|
|
975
|
+
advance # Skip the pipe
|
|
469
976
|
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
line: current_token ? current_token[:line] : 0,
|
|
475
|
-
column: current_token ? current_token[:column] : 0
|
|
476
|
-
}
|
|
477
|
-
else
|
|
478
|
-
advance # Skip 'in'
|
|
977
|
+
# Expect endif
|
|
978
|
+
unless expect_keyword('endif')
|
|
979
|
+
add_error("Expected 'endif' after '|' in one-line if statement")
|
|
980
|
+
end
|
|
479
981
|
end
|
|
982
|
+
else
|
|
983
|
+
# Parse multi-line if statement
|
|
984
|
+
# Parse statements until 'else', 'elseif', or 'endif'
|
|
985
|
+
then_branch = parse_body_until('else', 'elseif', 'endif')
|
|
480
986
|
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
987
|
+
# Check for else/elseif
|
|
988
|
+
if current_token && current_token[:type] == :keyword
|
|
989
|
+
if current_token[:value] == 'else'
|
|
990
|
+
advance # Skip 'else'
|
|
991
|
+
|
|
992
|
+
# Parse statements until 'endif'
|
|
993
|
+
else_branch = parse_body_until('endif')
|
|
994
|
+
elsif current_token[:value] == 'elseif'
|
|
995
|
+
elseif_stmt = parse_if_statement
|
|
996
|
+
else_branch << elseif_stmt if elseif_stmt
|
|
997
|
+
|
|
998
|
+
return {
|
|
999
|
+
type: :if_statement,
|
|
1000
|
+
condition: condition,
|
|
1001
|
+
then_branch: then_branch,
|
|
1002
|
+
else_branch: else_branch,
|
|
1003
|
+
line: line,
|
|
1004
|
+
column: column
|
|
1005
|
+
}
|
|
488
1006
|
end
|
|
489
|
-
|
|
490
|
-
stmt = parse_statement
|
|
491
|
-
body << stmt if stmt
|
|
492
1007
|
end
|
|
493
1008
|
|
|
494
|
-
# Expect
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
return {
|
|
498
|
-
type: :for_statement,
|
|
499
|
-
loop_var: loop_var,
|
|
500
|
-
iterable: iterable,
|
|
501
|
-
body: body,
|
|
502
|
-
line: line,
|
|
503
|
-
column: column
|
|
504
|
-
}
|
|
1009
|
+
# Expect endif
|
|
1010
|
+
expect_end_keyword('endif')
|
|
505
1011
|
end
|
|
1012
|
+
|
|
1013
|
+
{
|
|
1014
|
+
type: :if_statement,
|
|
1015
|
+
condition: condition,
|
|
1016
|
+
then_branch: then_branch,
|
|
1017
|
+
else_branch: else_branch,
|
|
1018
|
+
one_line: one_line_if,
|
|
1019
|
+
line: line,
|
|
1020
|
+
column: column
|
|
1021
|
+
}
|
|
506
1022
|
end
|
|
507
1023
|
|
|
508
|
-
def
|
|
509
|
-
token = advance # Skip '
|
|
1024
|
+
def parse_while_statement
|
|
1025
|
+
token = advance # Skip 'while'
|
|
510
1026
|
line = token[:line]
|
|
511
1027
|
column = token[:column]
|
|
1028
|
+
condition = parse_expression
|
|
512
1029
|
|
|
513
|
-
|
|
1030
|
+
# Parse statements until 'endwhile'
|
|
1031
|
+
body = parse_body_until('endwhile')
|
|
514
1032
|
|
|
515
|
-
#
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
1033
|
+
# Expect endwhile
|
|
1034
|
+
expect_end_keyword('endwhile')
|
|
1035
|
+
|
|
1036
|
+
{
|
|
1037
|
+
type: :while_statement,
|
|
1038
|
+
condition: condition,
|
|
1039
|
+
body: body,
|
|
1040
|
+
line: line,
|
|
1041
|
+
column: column
|
|
1042
|
+
}
|
|
1043
|
+
end
|
|
1044
|
+
|
|
1045
|
+
def parse_for_statement
|
|
1046
|
+
token = advance # Skip 'for'
|
|
1047
|
+
line = token[:line]
|
|
1048
|
+
column = token[:column]
|
|
1049
|
+
|
|
1050
|
+
# Determine the type of for loop and parse variables accordingly
|
|
1051
|
+
loop_var = nil
|
|
1052
|
+
loop_vars = nil
|
|
1053
|
+
|
|
1054
|
+
if current_token && current_token[:type] == :bracket_open
|
|
1055
|
+
# Handle destructuring with brackets: for [key, val] in dict
|
|
1056
|
+
advance # Skip '['
|
|
1057
|
+
loop_vars = parse_loop_variables
|
|
1058
|
+
expect(:bracket_close)
|
|
1059
|
+
expect_in_keyword
|
|
1060
|
+
elsif current_token && current_token[:type] == :paren_open
|
|
1061
|
+
# Handle multiple variables with parentheses: for (var1, var2) in list
|
|
1062
|
+
advance # Skip '('
|
|
1063
|
+
loop_vars = parse_loop_variables
|
|
1064
|
+
expect(:paren_close)
|
|
1065
|
+
expect_in_keyword
|
|
1066
|
+
else
|
|
1067
|
+
# Handle single variable: for var in list
|
|
1068
|
+
if current_token && (current_token[:type] == :identifier ||
|
|
1069
|
+
current_token[:type] == :local_variable ||
|
|
1070
|
+
current_token[:type] == :global_variable ||
|
|
1071
|
+
current_token[:type] == :script_local)
|
|
1072
|
+
loop_var = advance
|
|
1073
|
+
else
|
|
1074
|
+
add_error("Expected identifier as for loop variable")
|
|
1075
|
+
end
|
|
1076
|
+
expect_in_keyword
|
|
1077
|
+
end
|
|
1078
|
+
|
|
1079
|
+
# Parse the iterable expression
|
|
1080
|
+
iterable = parse_expression
|
|
1081
|
+
|
|
1082
|
+
# Parse the body until 'endfor'
|
|
1083
|
+
body = parse_body_until('endfor')
|
|
1084
|
+
|
|
1085
|
+
# Expect endfor
|
|
1086
|
+
expect_end_keyword('endfor')
|
|
1087
|
+
|
|
1088
|
+
# Build result hash based on whether we have single or multiple vars
|
|
1089
|
+
result = {
|
|
1090
|
+
type: :for_statement,
|
|
1091
|
+
iterable: iterable,
|
|
1092
|
+
body: body,
|
|
1093
|
+
line: line,
|
|
1094
|
+
column: column
|
|
1095
|
+
}
|
|
1096
|
+
|
|
1097
|
+
if loop_vars
|
|
1098
|
+
result[:loop_vars] = loop_vars
|
|
1099
|
+
else
|
|
1100
|
+
result[:loop_var] = loop_var
|
|
1101
|
+
end
|
|
1102
|
+
|
|
1103
|
+
result
|
|
1104
|
+
end
|
|
1105
|
+
|
|
1106
|
+
def parse_def_function
|
|
1107
|
+
token = advance # Skip 'def'
|
|
1108
|
+
line = token[:line]
|
|
1109
|
+
column = token[:column]
|
|
1110
|
+
|
|
1111
|
+
name = expect(:identifier)
|
|
1112
|
+
|
|
1113
|
+
# Parse parameter list
|
|
1114
|
+
expect(:paren_open)
|
|
1115
|
+
params = parse_parameter_list
|
|
1116
|
+
expect(:paren_close)
|
|
519
1117
|
|
|
520
1118
|
# Parse optional return type
|
|
521
1119
|
return_type = nil
|
|
@@ -524,19 +1122,11 @@ module Vinter
|
|
|
524
1122
|
return_type = parse_type
|
|
525
1123
|
end
|
|
526
1124
|
|
|
527
|
-
# Parse function body
|
|
528
|
-
body =
|
|
529
|
-
while @position < @tokens.length
|
|
530
|
-
if current_token[:type] == :keyword && current_token[:value] == 'enddef'
|
|
531
|
-
break
|
|
532
|
-
end
|
|
533
|
-
|
|
534
|
-
stmt = parse_statement
|
|
535
|
-
body << stmt if stmt
|
|
536
|
-
end
|
|
1125
|
+
# Parse function body until 'enddef'
|
|
1126
|
+
body = parse_body_until('enddef')
|
|
537
1127
|
|
|
538
1128
|
# Expect enddef
|
|
539
|
-
|
|
1129
|
+
expect_end_keyword('enddef')
|
|
540
1130
|
|
|
541
1131
|
{
|
|
542
1132
|
type: :def_function,
|
|
@@ -579,12 +1169,7 @@ module Vinter
|
|
|
579
1169
|
|
|
580
1170
|
# After varargs, we expect closing paren
|
|
581
1171
|
if current_token && current_token[:type] != :paren_close
|
|
582
|
-
|
|
583
|
-
message: "Expected closing parenthesis after varargs",
|
|
584
|
-
position: @position,
|
|
585
|
-
line: current_token[:line],
|
|
586
|
-
column: current_token[:column]
|
|
587
|
-
}
|
|
1172
|
+
add_error("Expected closing parenthesis after varargs", current_token)
|
|
588
1173
|
end
|
|
589
1174
|
|
|
590
1175
|
break
|
|
@@ -592,12 +1177,7 @@ module Vinter
|
|
|
592
1177
|
|
|
593
1178
|
# Get parameter name
|
|
594
1179
|
if !current_token || current_token[:type] != :identifier
|
|
595
|
-
|
|
596
|
-
message: "Expected parameter name",
|
|
597
|
-
position: @position,
|
|
598
|
-
line: current_token ? current_token[:line] : 0,
|
|
599
|
-
column: current_token ? current_token[:column] : 0
|
|
600
|
-
}
|
|
1180
|
+
add_error("Expected parameter name")
|
|
601
1181
|
break
|
|
602
1182
|
end
|
|
603
1183
|
|
|
@@ -632,12 +1212,7 @@ module Vinter
|
|
|
632
1212
|
advance
|
|
633
1213
|
# If we don't have a comma, we should have a closing paren
|
|
634
1214
|
elsif current_token && current_token[:type] != :paren_close
|
|
635
|
-
|
|
636
|
-
message: "Expected comma or closing parenthesis after parameter",
|
|
637
|
-
position: @position,
|
|
638
|
-
line: current_token[:line],
|
|
639
|
-
column: current_token[:column]
|
|
640
|
-
}
|
|
1215
|
+
add_error("Expected comma or closing parenthesis after parameter", current_token)
|
|
641
1216
|
break
|
|
642
1217
|
end
|
|
643
1218
|
end
|
|
@@ -646,7 +1221,7 @@ module Vinter
|
|
|
646
1221
|
end
|
|
647
1222
|
|
|
648
1223
|
def parse_type
|
|
649
|
-
if current_token && current_token[:type]
|
|
1224
|
+
if current_token && [:identifier, :keyword].include?(current_token[:type])
|
|
650
1225
|
type_name = advance
|
|
651
1226
|
|
|
652
1227
|
# Handle generic types like list<string>
|
|
@@ -666,12 +1241,7 @@ module Vinter
|
|
|
666
1241
|
|
|
667
1242
|
return type_name[:value]
|
|
668
1243
|
else
|
|
669
|
-
|
|
670
|
-
message: "Expected type identifier",
|
|
671
|
-
position: @position,
|
|
672
|
-
line: current_token ? current_token[:line] : 0,
|
|
673
|
-
column: current_token ? current_token[:column] : 0
|
|
674
|
-
}
|
|
1244
|
+
add_error("Expected type identifier")
|
|
675
1245
|
advance
|
|
676
1246
|
return "unknown"
|
|
677
1247
|
end
|
|
@@ -684,12 +1254,7 @@ module Vinter
|
|
|
684
1254
|
column = var_type_token[:column]
|
|
685
1255
|
|
|
686
1256
|
if !current_token || current_token[:type] != :identifier
|
|
687
|
-
|
|
688
|
-
message: "Expected variable name",
|
|
689
|
-
position: @position,
|
|
690
|
-
line: current_token ? current_token[:line] : 0,
|
|
691
|
-
column: current_token ? current_token[:column] : 0
|
|
692
|
-
}
|
|
1257
|
+
add_error("Expected variable name")
|
|
693
1258
|
return nil
|
|
694
1259
|
end
|
|
695
1260
|
|
|
@@ -727,7 +1292,12 @@ module Vinter
|
|
|
727
1292
|
column = token[:column]
|
|
728
1293
|
|
|
729
1294
|
value = nil
|
|
730
|
-
if
|
|
1295
|
+
# Check if we've reached the end of the file, end of line, or a semicolon
|
|
1296
|
+
if @position < @tokens.length &&
|
|
1297
|
+
current_token &&
|
|
1298
|
+
current_token[:type] != :semicolon &&
|
|
1299
|
+
!(current_token[:type] == :keyword &&
|
|
1300
|
+
['endif', 'endwhile', 'endfor', 'endfunction', 'endfunc'].include?(current_token[:value]))
|
|
731
1301
|
value = parse_expression
|
|
732
1302
|
end
|
|
733
1303
|
|
|
@@ -783,7 +1353,72 @@ module Vinter
|
|
|
783
1353
|
end
|
|
784
1354
|
|
|
785
1355
|
def parse_expression
|
|
786
|
-
|
|
1356
|
+
# binding.pry
|
|
1357
|
+
# Special case for empty return statements or standalone keywords that shouldn't be expressions
|
|
1358
|
+
if current_token && current_token[:type] == :keyword &&
|
|
1359
|
+
['return', 'endif', 'endwhile', 'endfor', 'endfunction', 'endfunc'].include?(current_token[:value])
|
|
1360
|
+
return nil
|
|
1361
|
+
end
|
|
1362
|
+
|
|
1363
|
+
if current_token[:type] == :string
|
|
1364
|
+
string_value = current_token[:value]
|
|
1365
|
+
while current_token && peek_token && [:line_continuation, :identifier].include?(peek_token[:type])
|
|
1366
|
+
# Handle strings with line continuation
|
|
1367
|
+
if ["'", '"'].include? current_token[:value][-1]
|
|
1368
|
+
token_line = current_token[:line]
|
|
1369
|
+
token_column = current_token[:column]
|
|
1370
|
+
advance # Consume the string token
|
|
1371
|
+
return {
|
|
1372
|
+
type: :literal,
|
|
1373
|
+
value: string_value,
|
|
1374
|
+
token_type: :string,
|
|
1375
|
+
line: token_line,
|
|
1376
|
+
column: token_column
|
|
1377
|
+
}
|
|
1378
|
+
else
|
|
1379
|
+
advance
|
|
1380
|
+
string_value += current_token[:value]
|
|
1381
|
+
end
|
|
1382
|
+
end
|
|
1383
|
+
end
|
|
1384
|
+
|
|
1385
|
+
# Parse the condition expression
|
|
1386
|
+
expr = parse_binary_expression
|
|
1387
|
+
|
|
1388
|
+
# Check if this is a ternary expression
|
|
1389
|
+
if current_token && (current_token[:type] == :question_mark || (current_token[:type] == :operator && current_token[:value] == '?'))
|
|
1390
|
+
question_token = advance # Skip '?'
|
|
1391
|
+
|
|
1392
|
+
# Parse the "then" expression
|
|
1393
|
+
then_expr = parse_expression
|
|
1394
|
+
|
|
1395
|
+
# Expect the colon
|
|
1396
|
+
if current_token && current_token[:type] == :colon
|
|
1397
|
+
colon_token = advance # Skip ':'
|
|
1398
|
+
|
|
1399
|
+
# Parse the "else" expression
|
|
1400
|
+
else_expr = parse_expression
|
|
1401
|
+
|
|
1402
|
+
# Return the ternary expression
|
|
1403
|
+
return {
|
|
1404
|
+
type: :ternary_expression,
|
|
1405
|
+
condition: expr,
|
|
1406
|
+
then_expr: then_expr,
|
|
1407
|
+
else_expr: else_expr,
|
|
1408
|
+
line: question_token[:line],
|
|
1409
|
+
column: question_token[:column]
|
|
1410
|
+
}
|
|
1411
|
+
else
|
|
1412
|
+
@errors << {
|
|
1413
|
+
message: "Expected ':' in ternary expression",
|
|
1414
|
+
position: @position,
|
|
1415
|
+
line: current_token ? current_token[:line] : 0,
|
|
1416
|
+
column: current_token ? current_token[:column] : 0
|
|
1417
|
+
}
|
|
1418
|
+
end
|
|
1419
|
+
end
|
|
1420
|
+
|
|
1421
|
+
return expr
|
|
787
1422
|
end
|
|
788
1423
|
|
|
789
1424
|
def parse_binary_expression(precedence = 0)
|
|
@@ -799,9 +1434,65 @@ module Vinter
|
|
|
799
1434
|
advance
|
|
800
1435
|
end
|
|
801
1436
|
|
|
1437
|
+
if current_token && current_token[:type] == :operator && current_token[:value] == '.' &&
|
|
1438
|
+
operator_precedence(current_token[:value]) >= precedence
|
|
1439
|
+
op_token = advance # Skip the operator
|
|
1440
|
+
|
|
1441
|
+
# Check if we're dealing with a command placeholder on either side
|
|
1442
|
+
if (left && left[:type] == :command_arg_placeholder) ||
|
|
1443
|
+
(peek_token && peek_token[:type] == :command_arg_placeholder)
|
|
1444
|
+
# Handle command-specific concatenation
|
|
1445
|
+
right = parse_binary_expression(operator_precedence('.') + 1)
|
|
1446
|
+
|
|
1447
|
+
left = {
|
|
1448
|
+
type: :command_concat_expression, # Special type for command concatenation
|
|
1449
|
+
operator: '.',
|
|
1450
|
+
left: left,
|
|
1451
|
+
right: right,
|
|
1452
|
+
line: op_token[:line],
|
|
1453
|
+
column: op_token[:column]
|
|
1454
|
+
}
|
|
1455
|
+
else
|
|
1456
|
+
# Normal expression concatenation
|
|
1457
|
+
right = parse_binary_expression(operator_precedence('.') + 1)
|
|
1458
|
+
|
|
1459
|
+
left = {
|
|
1460
|
+
type: :binary_expression,
|
|
1461
|
+
operator: '.',
|
|
1462
|
+
left: left,
|
|
1463
|
+
right: right,
|
|
1464
|
+
line: op_token[:line],
|
|
1465
|
+
column: op_token[:column]
|
|
1466
|
+
}
|
|
1467
|
+
end
|
|
1468
|
+
elsif current_token && current_token[:type] == :operator &&
|
|
1469
|
+
['<', '>', '=', '!'].include?(current_token[:value]) &&
|
|
1470
|
+
peek_token && peek_token[:type] == :operator && peek_token[:value] == '='
|
|
1471
|
+
|
|
1472
|
+
# Combine the two operators into one token
|
|
1473
|
+
op_token = current_token
|
|
1474
|
+
op = current_token[:value] + peek_token[:value]
|
|
1475
|
+
advance # Skip the first operator
|
|
1476
|
+
advance # Skip the second operator
|
|
1477
|
+
|
|
1478
|
+
# Now process the combined operator
|
|
1479
|
+
op_precedence = operator_precedence(op)
|
|
1480
|
+
|
|
1481
|
+
if op_precedence >= precedence
|
|
1482
|
+
right = parse_binary_expression(op_precedence + 1)
|
|
1483
|
+
|
|
1484
|
+
left = {
|
|
1485
|
+
type: :binary_expression,
|
|
1486
|
+
operator: op,
|
|
1487
|
+
left: left,
|
|
1488
|
+
right: right,
|
|
1489
|
+
line: op_token[:line],
|
|
1490
|
+
column: op_token[:column]
|
|
1491
|
+
}
|
|
1492
|
+
end
|
|
802
1493
|
# Now we should be at the operator
|
|
803
|
-
|
|
804
|
-
|
|
1494
|
+
elsif current_token && current_token[:type] == :operator &&
|
|
1495
|
+
operator_precedence(current_token[:value]) >= precedence
|
|
805
1496
|
op_token = advance
|
|
806
1497
|
op = op_token[:value]
|
|
807
1498
|
op_precedence = operator_precedence(op)
|
|
@@ -858,24 +1549,160 @@ module Vinter
|
|
|
858
1549
|
expr = nil
|
|
859
1550
|
|
|
860
1551
|
case token[:type]
|
|
861
|
-
|
|
1552
|
+
# Add handling for command arg placeholders
|
|
1553
|
+
when :command_arg_placeholder
|
|
862
1554
|
advance
|
|
863
1555
|
expr = {
|
|
864
|
-
type: :
|
|
1556
|
+
type: :command_arg_placeholder,
|
|
865
1557
|
value: token[:value],
|
|
866
|
-
token_type: :number,
|
|
867
1558
|
line: line,
|
|
868
1559
|
column: column
|
|
869
1560
|
}
|
|
870
|
-
|
|
1561
|
+
# Add special handling for keywords that might appear in expressions
|
|
1562
|
+
when :keyword
|
|
1563
|
+
# Special handling for map-related keywords when they appear in expressions
|
|
1564
|
+
if ['map', 'nmap', 'imap', 'vmap', 'xmap', 'noremap', 'nnoremap', 'inoremap', 'vnoremap', 'xnoremap', 'cnoremap', 'cmap'].include?(token[:value])
|
|
1565
|
+
if peek_token[:type] == :paren_open
|
|
1566
|
+
parse_builtin_function_call(token[:value], line, column)
|
|
1567
|
+
else
|
|
1568
|
+
# Treat map commands as identifiers when inside expressions
|
|
1569
|
+
advance
|
|
1570
|
+
return {
|
|
1571
|
+
type: :identifier,
|
|
1572
|
+
name: token[:value],
|
|
1573
|
+
line: line,
|
|
1574
|
+
column: column
|
|
1575
|
+
}
|
|
1576
|
+
end
|
|
1577
|
+
elsif token[:value] == 'type' && current_token && (current_token[:type] == :paren_open || peek_token && peek_token[:type] == :paren_open)
|
|
1578
|
+
# This is the type() function call
|
|
1579
|
+
return parse_builtin_function_call(token[:value], line, column)
|
|
1580
|
+
elsif token[:value] == 'function'
|
|
1581
|
+
# Check if this is a function() call or just 'function' as property name
|
|
1582
|
+
if peek_token && peek_token[:type] == :paren_open
|
|
1583
|
+
advance # Skip 'function'
|
|
1584
|
+
# This is a function() call
|
|
1585
|
+
if current_token && current_token[:type] == :paren_open
|
|
1586
|
+
# This is a function reference call
|
|
1587
|
+
paren_token = advance # Skip '('
|
|
1588
|
+
|
|
1589
|
+
# Parse the function name as a string or arrow function
|
|
1590
|
+
if current_token && current_token[:type] == :string
|
|
1591
|
+
func_name = current_token[:value]
|
|
1592
|
+
advance
|
|
1593
|
+
|
|
1594
|
+
# Check for additional arguments (function() can take multiple arguments)
|
|
1595
|
+
args = []
|
|
1596
|
+
while current_token && current_token[:type] == :comma
|
|
1597
|
+
advance # Skip comma
|
|
1598
|
+
arg = parse_expression
|
|
1599
|
+
args << arg if arg
|
|
1600
|
+
end
|
|
1601
|
+
|
|
1602
|
+
# Expect closing parenthesis
|
|
1603
|
+
expect(:paren_close) # Skip ')'
|
|
1604
|
+
|
|
1605
|
+
return {
|
|
1606
|
+
type: :function_reference,
|
|
1607
|
+
function_name: func_name,
|
|
1608
|
+
arguments: args,
|
|
1609
|
+
line: line,
|
|
1610
|
+
column: column
|
|
1611
|
+
}
|
|
1612
|
+
elsif current_token && current_token[:type] == :brace_open
|
|
1613
|
+
# This is an arrow function inside function()
|
|
1614
|
+
arrow_function = parse_vim_lambda(line, column)
|
|
1615
|
+
|
|
1616
|
+
# Expect closing parenthesis
|
|
1617
|
+
expect(:paren_close) # Skip ')'
|
|
1618
|
+
|
|
1619
|
+
return {
|
|
1620
|
+
type: :function_reference,
|
|
1621
|
+
function_body: arrow_function,
|
|
1622
|
+
line: line,
|
|
1623
|
+
column: column
|
|
1624
|
+
}
|
|
1625
|
+
else
|
|
1626
|
+
@errors << {
|
|
1627
|
+
message: "Expected string or arrow function definition in function() call",
|
|
1628
|
+
position: @position,
|
|
1629
|
+
line: current_token ? current_token[:line] : 0,
|
|
1630
|
+
column: current_token ? current_token[:column] : 0
|
|
1631
|
+
}
|
|
1632
|
+
end
|
|
1633
|
+
end
|
|
1634
|
+
else
|
|
1635
|
+
# If not followed by parenthesis, treat 'function' as an identifier (property name)
|
|
1636
|
+
advance # Skip 'function'
|
|
1637
|
+
return {
|
|
1638
|
+
type: :identifier,
|
|
1639
|
+
name: token[:value],
|
|
1640
|
+
line: line,
|
|
1641
|
+
column: column
|
|
1642
|
+
}
|
|
1643
|
+
end
|
|
1644
|
+
# Legacy Vim allows certain keywords as identifiers in expressions
|
|
1645
|
+
elsif ['return', 'type'].include?(token[:value])
|
|
1646
|
+
# Handle 'return' keyword specially when it appears in an expression context
|
|
1647
|
+
advance
|
|
1648
|
+
@warnings << {
|
|
1649
|
+
message: "Keyword '#{token[:value]}' used in an expression context",
|
|
1650
|
+
position: @position,
|
|
1651
|
+
line: line,
|
|
1652
|
+
column: column
|
|
1653
|
+
}
|
|
1654
|
+
# Check if this is a function call for 'type'
|
|
1655
|
+
if token[:value] == 'type' && current_token && current_token[:type] == :paren_open
|
|
1656
|
+
return parse_function_call(token[:value], line, column)
|
|
1657
|
+
end
|
|
1658
|
+
|
|
1659
|
+
expr = {
|
|
1660
|
+
type: :identifier,
|
|
1661
|
+
name: token[:value],
|
|
1662
|
+
line: line,
|
|
1663
|
+
column: column
|
|
1664
|
+
}
|
|
1665
|
+
else
|
|
1666
|
+
@errors << {
|
|
1667
|
+
message: "Unexpected keyword in expression: #{token[:value]}",
|
|
1668
|
+
position: @position,
|
|
1669
|
+
line: line,
|
|
1670
|
+
column: column
|
|
1671
|
+
}
|
|
1672
|
+
advance
|
|
1673
|
+
return nil
|
|
1674
|
+
end
|
|
1675
|
+
when :number
|
|
871
1676
|
advance
|
|
872
1677
|
expr = {
|
|
873
1678
|
type: :literal,
|
|
874
1679
|
value: token[:value],
|
|
875
|
-
token_type: :
|
|
1680
|
+
token_type: :number,
|
|
876
1681
|
line: line,
|
|
877
1682
|
column: column
|
|
878
1683
|
}
|
|
1684
|
+
when :string
|
|
1685
|
+
# parse_string
|
|
1686
|
+
if token[:value].start_with?('"')
|
|
1687
|
+
advance
|
|
1688
|
+
return {
|
|
1689
|
+
type: :comment,
|
|
1690
|
+
value: token[:value],
|
|
1691
|
+
line: line,
|
|
1692
|
+
column: column
|
|
1693
|
+
}
|
|
1694
|
+
else
|
|
1695
|
+
string_value = token[:value]
|
|
1696
|
+
advance
|
|
1697
|
+
expr = {
|
|
1698
|
+
type: :literal,
|
|
1699
|
+
value: string_value,
|
|
1700
|
+
raw_value: string_value, # Store the raw string to preserve escapes
|
|
1701
|
+
token_type: :string,
|
|
1702
|
+
line: line,
|
|
1703
|
+
column: column
|
|
1704
|
+
}
|
|
1705
|
+
end
|
|
879
1706
|
when :option_variable
|
|
880
1707
|
# Handle Vim option variables (like &compatible)
|
|
881
1708
|
advance
|
|
@@ -885,6 +1712,14 @@ module Vinter
|
|
|
885
1712
|
line: line,
|
|
886
1713
|
column: column
|
|
887
1714
|
}
|
|
1715
|
+
when :scoped_option_variable
|
|
1716
|
+
advance
|
|
1717
|
+
expr = {
|
|
1718
|
+
type: :scoped_option_variable,
|
|
1719
|
+
name: token[:value],
|
|
1720
|
+
line: line,
|
|
1721
|
+
column: column
|
|
1722
|
+
}
|
|
888
1723
|
when :special_variable
|
|
889
1724
|
# Handle Vim special variables (like v:version)
|
|
890
1725
|
advance
|
|
@@ -909,6 +1744,21 @@ module Vinter
|
|
|
909
1744
|
line: line,
|
|
910
1745
|
column: column
|
|
911
1746
|
}
|
|
1747
|
+
when :buffer_local
|
|
1748
|
+
# Handle script-local variables/functions (like s:var)
|
|
1749
|
+
advance
|
|
1750
|
+
|
|
1751
|
+
# Check if this is a function call
|
|
1752
|
+
if current_token && current_token[:type] == :paren_open
|
|
1753
|
+
return parse_function_call(token[:value], line, column)
|
|
1754
|
+
end
|
|
1755
|
+
|
|
1756
|
+
expr = {
|
|
1757
|
+
type: :buffer_local,
|
|
1758
|
+
name: token[:value],
|
|
1759
|
+
line: line,
|
|
1760
|
+
column: column
|
|
1761
|
+
}
|
|
912
1762
|
when :global_variable
|
|
913
1763
|
# Handle global variables (like g:var)
|
|
914
1764
|
advance
|
|
@@ -927,32 +1777,66 @@ module Vinter
|
|
|
927
1777
|
line: line,
|
|
928
1778
|
column: column
|
|
929
1779
|
}
|
|
930
|
-
when :
|
|
1780
|
+
when :local_variable
|
|
1781
|
+
# Handle local variables (like l:var)
|
|
931
1782
|
advance
|
|
1783
|
+
expr = {
|
|
1784
|
+
type: :local_variable,
|
|
1785
|
+
name: token[:value],
|
|
1786
|
+
line: line,
|
|
1787
|
+
column: column
|
|
1788
|
+
}
|
|
1789
|
+
when :identifier
|
|
1790
|
+
# Special handling for Vim built-in functions
|
|
1791
|
+
if ['has', 'exists', 'empty', 'get', 'type', 'map', 'copy'].include?(token[:value])
|
|
1792
|
+
expr = parse_builtin_function_call(token[:value], line, column)
|
|
1793
|
+
else
|
|
1794
|
+
advance
|
|
932
1795
|
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
1796
|
+
# Check if this is a function call
|
|
1797
|
+
if current_token && current_token[:type] == :paren_open
|
|
1798
|
+
expr = parse_function_call(token[:value], line, column)
|
|
1799
|
+
else
|
|
1800
|
+
# Special handling for execute command
|
|
1801
|
+
if token[:value] == 'execute'
|
|
1802
|
+
# Parse the string expressions for execute
|
|
1803
|
+
# For now we'll just treat it as a normal identifier
|
|
1804
|
+
expr = {
|
|
1805
|
+
type: :identifier,
|
|
1806
|
+
name: token[:value],
|
|
1807
|
+
line: line,
|
|
1808
|
+
column: column
|
|
1809
|
+
}
|
|
1810
|
+
else
|
|
1811
|
+
expr = {
|
|
1812
|
+
type: :identifier,
|
|
1813
|
+
name: token[:value],
|
|
1814
|
+
line: line,
|
|
1815
|
+
column: column
|
|
1816
|
+
}
|
|
1817
|
+
end
|
|
1818
|
+
end
|
|
936
1819
|
end
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
if
|
|
940
|
-
|
|
941
|
-
# For now we'll just treat it as a normal identifier
|
|
942
|
-
expr = {
|
|
943
|
-
type: :identifier,
|
|
944
|
-
name: token[:value],
|
|
945
|
-
line: line,
|
|
946
|
-
column: column
|
|
947
|
-
}
|
|
1820
|
+
when :vimfuncs
|
|
1821
|
+
advance
|
|
1822
|
+
if current_token[:type] == :paren_open
|
|
1823
|
+
expr = parse_function_call(token[:value], line, column)
|
|
948
1824
|
else
|
|
949
1825
|
expr = {
|
|
950
|
-
type: :
|
|
1826
|
+
type: :vimfuncs,
|
|
951
1827
|
name: token[:value],
|
|
952
1828
|
line: line,
|
|
953
1829
|
column: column
|
|
954
1830
|
}
|
|
955
1831
|
end
|
|
1832
|
+
when :namespace_prefix
|
|
1833
|
+
advance
|
|
1834
|
+
expr = {
|
|
1835
|
+
type: :namespace_prefix,
|
|
1836
|
+
name: token[:value],
|
|
1837
|
+
line: line,
|
|
1838
|
+
column: column
|
|
1839
|
+
}
|
|
956
1840
|
when :paren_open
|
|
957
1841
|
if is_lambda_expression
|
|
958
1842
|
return parse_lambda_expression(line, column)
|
|
@@ -964,11 +1848,28 @@ module Vinter
|
|
|
964
1848
|
when :bracket_open
|
|
965
1849
|
expr = parse_list_literal(line, column)
|
|
966
1850
|
when :brace_open
|
|
967
|
-
expr =
|
|
1851
|
+
expr = parse_vim_lambda_or_dict(line, column)
|
|
968
1852
|
when :backslash
|
|
969
1853
|
# Handle line continuation with backslash
|
|
970
1854
|
advance
|
|
971
1855
|
expr = parse_expression
|
|
1856
|
+
when :register_access
|
|
1857
|
+
advance
|
|
1858
|
+
expr = {
|
|
1859
|
+
type: :register_access,
|
|
1860
|
+
register: token[:value][1..-1], # Remove the @ symbol
|
|
1861
|
+
line: line,
|
|
1862
|
+
column: column
|
|
1863
|
+
}
|
|
1864
|
+
when :line_continuation
|
|
1865
|
+
advance
|
|
1866
|
+
expr = parse_expression
|
|
1867
|
+
when :interpolated_string
|
|
1868
|
+
advance
|
|
1869
|
+
expr = {
|
|
1870
|
+
type: :interpolated_string,
|
|
1871
|
+
value: token[:value],
|
|
1872
|
+
}
|
|
972
1873
|
else
|
|
973
1874
|
@errors << {
|
|
974
1875
|
message: "Unexpected token in expression: #{token[:type]}",
|
|
@@ -984,28 +1885,44 @@ module Vinter
|
|
|
984
1885
|
while current_token
|
|
985
1886
|
# Check for property access with dot
|
|
986
1887
|
if current_token[:type] == :operator && current_token[:value] == '.'
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
1888
|
+
# Look ahead to determine if this is property access or concatenation
|
|
1889
|
+
next_token = peek_token
|
|
1890
|
+
is_property_access = next_token && (next_token[:type] == :identifier || next_token[:type] == :keyword)
|
|
1891
|
+
|
|
1892
|
+
# Check if this is a property access (only when right side is identifier/keyword)
|
|
1893
|
+
if is_property_access && (expr[:type] == :identifier || expr[:type] == :global_variable ||
|
|
1894
|
+
expr[:type] == :script_local || expr[:type] == :namespace_prefix ||
|
|
1895
|
+
expr[:type] == :arg_variable || expr[:type] == :buffer_local ||
|
|
1896
|
+
expr[:type] == :window_local || expr[:type] == :tab_local ||
|
|
1897
|
+
expr[:type] == :local_variable || expr[:type] == :property_access ||
|
|
1898
|
+
expr[:type] == :indexed_access)
|
|
1899
|
+
|
|
1900
|
+
dot_token = advance # Skip '.'
|
|
1901
|
+
# Next token should be an identifier (property name)
|
|
1902
|
+
if !current_token || (current_token[:type] != :identifier &&
|
|
1903
|
+
current_token[:type] != :keyword)
|
|
1904
|
+
@errors << {
|
|
1905
|
+
message: "Expected property name after '.'",
|
|
1906
|
+
position: @position,
|
|
1907
|
+
line: current_token ? current_token[:line] : 0,
|
|
1908
|
+
column: current_token ? current_token[:column] : 0
|
|
1909
|
+
}
|
|
1910
|
+
break
|
|
1911
|
+
end
|
|
1912
|
+
property_token = advance
|
|
1913
|
+
|
|
1914
|
+
expr = {
|
|
1915
|
+
type: :property_access,
|
|
1916
|
+
object: expr,
|
|
1917
|
+
property: property_token[:value],
|
|
1918
|
+
line: dot_token[:line],
|
|
1919
|
+
column: dot_token[:column]
|
|
996
1920
|
}
|
|
1921
|
+
else
|
|
1922
|
+
# This is concatenation or other binary operation, break to let binary expression parser handle it
|
|
997
1923
|
break
|
|
998
1924
|
end
|
|
999
1925
|
|
|
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
1926
|
# Check for method call with arrow ->
|
|
1010
1927
|
elsif current_token[:type] == :operator && current_token[:value] == '->'
|
|
1011
1928
|
arrow_token = advance # Skip '->'
|
|
@@ -1056,18 +1973,47 @@ module Vinter
|
|
|
1056
1973
|
# Check for indexing with brackets
|
|
1057
1974
|
elsif current_token[:type] == :bracket_open
|
|
1058
1975
|
bracket_token = advance # Skip '['
|
|
1059
|
-
|
|
1060
1976
|
index_expr = parse_expression
|
|
1061
1977
|
|
|
1978
|
+
# Add support for list slicing with colon
|
|
1979
|
+
end_index = nil
|
|
1980
|
+
if current_token && current_token[:type] == :colon
|
|
1981
|
+
advance # Skip ':'
|
|
1982
|
+
# handle omitted end index case like [6:]
|
|
1983
|
+
if current_token && current_token[:type] == :bracket_close
|
|
1984
|
+
end_index = {
|
|
1985
|
+
type: :implicit_end_index,
|
|
1986
|
+
value: nil,
|
|
1987
|
+
line: current_token[:line],
|
|
1988
|
+
column: current_token[:column]
|
|
1989
|
+
}
|
|
1990
|
+
else
|
|
1991
|
+
end_index = parse_expression
|
|
1992
|
+
end
|
|
1993
|
+
end
|
|
1994
|
+
|
|
1062
1995
|
expect(:bracket_close) # Skip ']'
|
|
1063
1996
|
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1997
|
+
if end_index
|
|
1998
|
+
# This is a slice operation
|
|
1999
|
+
expr = {
|
|
2000
|
+
type: :slice_access,
|
|
2001
|
+
object: expr,
|
|
2002
|
+
start_index: index_expr,
|
|
2003
|
+
end_index: end_index,
|
|
2004
|
+
line: bracket_token[:line],
|
|
2005
|
+
column: bracket_token[:column]
|
|
2006
|
+
}
|
|
2007
|
+
else
|
|
2008
|
+
# Regular indexed access
|
|
2009
|
+
expr = {
|
|
2010
|
+
type: :indexed_access,
|
|
2011
|
+
object: expr,
|
|
2012
|
+
index: index_expr,
|
|
2013
|
+
line: bracket_token[:line],
|
|
2014
|
+
column: bracket_token[:column]
|
|
2015
|
+
}
|
|
2016
|
+
end
|
|
1071
2017
|
# Check for function call directly on an expression
|
|
1072
2018
|
elsif current_token[:type] == :paren_open
|
|
1073
2019
|
paren_token = advance # Skip '('
|
|
@@ -1106,6 +2052,85 @@ module Vinter
|
|
|
1106
2052
|
return expr
|
|
1107
2053
|
end
|
|
1108
2054
|
|
|
2055
|
+
def parse_builtin_function_call(name, line, column)
|
|
2056
|
+
# Skip the function name (already consumed)
|
|
2057
|
+
advance if current_token[:value] == name
|
|
2058
|
+
|
|
2059
|
+
# Check if there's an opening parenthesis
|
|
2060
|
+
if current_token && current_token[:type] == :paren_open
|
|
2061
|
+
advance # Skip '('
|
|
2062
|
+
|
|
2063
|
+
# Parse arguments
|
|
2064
|
+
args = []
|
|
2065
|
+
|
|
2066
|
+
# Functions that take string expressions as code
|
|
2067
|
+
special_functions = ['map', 'reduce', 'sort', 'call', 'eval', 'execute', 'exec']
|
|
2068
|
+
is_special_function = special_functions.include?(name)
|
|
2069
|
+
|
|
2070
|
+
# Parse until closing parenthesis
|
|
2071
|
+
while @position < @tokens.length && current_token && current_token[:type] != :paren_close
|
|
2072
|
+
# Skip whitespace or comments
|
|
2073
|
+
if current_token[:type] == :whitespace || current_token[:type] == :comment
|
|
2074
|
+
advance
|
|
2075
|
+
next
|
|
2076
|
+
end
|
|
2077
|
+
|
|
2078
|
+
# Special handling for string arguments that contain code
|
|
2079
|
+
if is_special_function &&
|
|
2080
|
+
current_token && current_token[:type] == :string
|
|
2081
|
+
string_token = parse_expression
|
|
2082
|
+
args << {
|
|
2083
|
+
type: :literal,
|
|
2084
|
+
value: string_token[:value],
|
|
2085
|
+
token_type: :string,
|
|
2086
|
+
line: string_token[:line],
|
|
2087
|
+
column: string_token[:column]
|
|
2088
|
+
}
|
|
2089
|
+
else
|
|
2090
|
+
arg = parse_expression
|
|
2091
|
+
args << arg if arg
|
|
2092
|
+
end
|
|
2093
|
+
|
|
2094
|
+
if current_token && current_token[:type] == :comma
|
|
2095
|
+
advance
|
|
2096
|
+
elsif current_token && current_token[:type] != :paren_close
|
|
2097
|
+
@errors << {
|
|
2098
|
+
message: "Expected comma or closing parenthesis in #{name} function",
|
|
2099
|
+
position: @position,
|
|
2100
|
+
line: current_token[:line],
|
|
2101
|
+
column: current_token[:column]
|
|
2102
|
+
}
|
|
2103
|
+
break
|
|
2104
|
+
end
|
|
2105
|
+
end
|
|
2106
|
+
|
|
2107
|
+
# Check for closing parenthesis
|
|
2108
|
+
if current_token && current_token[:type] == :paren_close
|
|
2109
|
+
advance # Skip ')'
|
|
2110
|
+
else
|
|
2111
|
+
@errors << {
|
|
2112
|
+
message: "Expected ')' to close #{name} function call",
|
|
2113
|
+
position: @position,
|
|
2114
|
+
line: current_token ? current_token[:line] : 0,
|
|
2115
|
+
column: current_token ? current_token[:column] : 0
|
|
2116
|
+
}
|
|
2117
|
+
end
|
|
2118
|
+
else
|
|
2119
|
+
# Handle legacy Vim script where parentheses might be omitted
|
|
2120
|
+
# Just parse one expression as the argument
|
|
2121
|
+
args = [parse_expression]
|
|
2122
|
+
end
|
|
2123
|
+
|
|
2124
|
+
# Return function call node
|
|
2125
|
+
{
|
|
2126
|
+
type: :builtin_function_call,
|
|
2127
|
+
name: name,
|
|
2128
|
+
arguments: args,
|
|
2129
|
+
line: line,
|
|
2130
|
+
column: column
|
|
2131
|
+
}
|
|
2132
|
+
end
|
|
2133
|
+
|
|
1109
2134
|
def is_lambda_expression
|
|
1110
2135
|
# Save the current position
|
|
1111
2136
|
original_position = @position
|
|
@@ -1153,7 +2178,7 @@ module Vinter
|
|
|
1153
2178
|
# There might be a return type annotation
|
|
1154
2179
|
advance
|
|
1155
2180
|
# Skip the type
|
|
1156
|
-
advance if current_token && current_token[:type]
|
|
2181
|
+
advance if current_token && [:identifier, :keyword].include?(current_token[:type])
|
|
1157
2182
|
# Check for the arrow
|
|
1158
2183
|
has_arrow = current_token && current_token[:type] == :operator && current_token[:value] == '=>'
|
|
1159
2184
|
end
|
|
@@ -1165,6 +2190,109 @@ module Vinter
|
|
|
1165
2190
|
return has_params && has_arrow
|
|
1166
2191
|
end
|
|
1167
2192
|
|
|
2193
|
+
def parse_vim_lambda(line, column)
|
|
2194
|
+
advance # Skip opening brace
|
|
2195
|
+
|
|
2196
|
+
# Parse parameters
|
|
2197
|
+
params = []
|
|
2198
|
+
|
|
2199
|
+
# Check if this is a no-parameter lambda (starts with ->)
|
|
2200
|
+
if current_token && current_token[:type] == :operator && current_token[:value] == '->'
|
|
2201
|
+
# No parameters, skip to arrow parsing
|
|
2202
|
+
else
|
|
2203
|
+
# Parse parameter(s)
|
|
2204
|
+
if current_token && (current_token[:type] == :identifier ||
|
|
2205
|
+
current_token[:type] == :local_variable ||
|
|
2206
|
+
current_token[:type] == :global_variable)
|
|
2207
|
+
param_name = current_token[:value]
|
|
2208
|
+
param_line = current_token[:line]
|
|
2209
|
+
param_column = current_token[:column]
|
|
2210
|
+
advance
|
|
2211
|
+
|
|
2212
|
+
params << {
|
|
2213
|
+
type: :parameter,
|
|
2214
|
+
name: param_name,
|
|
2215
|
+
line: param_line,
|
|
2216
|
+
column: param_column
|
|
2217
|
+
}
|
|
2218
|
+
|
|
2219
|
+
# Handle multiple parameters (comma-separated)
|
|
2220
|
+
while current_token && current_token[:type] == :comma
|
|
2221
|
+
advance # Skip comma
|
|
2222
|
+
|
|
2223
|
+
if current_token && (current_token[:type] == :identifier ||
|
|
2224
|
+
current_token[:type] == :local_variable ||
|
|
2225
|
+
current_token[:type] == :global_variable)
|
|
2226
|
+
param_name = current_token[:value]
|
|
2227
|
+
param_line = current_token[:line]
|
|
2228
|
+
param_column = current_token[:column]
|
|
2229
|
+
advance
|
|
2230
|
+
|
|
2231
|
+
params << {
|
|
2232
|
+
type: :parameter,
|
|
2233
|
+
name: param_name,
|
|
2234
|
+
line: param_line,
|
|
2235
|
+
column: param_column
|
|
2236
|
+
}
|
|
2237
|
+
else
|
|
2238
|
+
@errors << {
|
|
2239
|
+
message: "Expected parameter name after comma in lambda function",
|
|
2240
|
+
position: @position,
|
|
2241
|
+
line: current_token ? current_token[:line] : 0,
|
|
2242
|
+
column: current_token ? current_token[:column] : 0
|
|
2243
|
+
}
|
|
2244
|
+
break
|
|
2245
|
+
end
|
|
2246
|
+
end
|
|
2247
|
+
else
|
|
2248
|
+
# For no-parameter lambdas, this is not an error
|
|
2249
|
+
if !(current_token && current_token[:type] == :operator && current_token[:value] == '->')
|
|
2250
|
+
@errors << {
|
|
2251
|
+
message: "Expected parameter name or '->' in lambda function",
|
|
2252
|
+
position: @position,
|
|
2253
|
+
line: current_token ? current_token[:line] : 0,
|
|
2254
|
+
column: current_token ? current_token[:column] : 0
|
|
2255
|
+
}
|
|
2256
|
+
end
|
|
2257
|
+
end
|
|
2258
|
+
end
|
|
2259
|
+
|
|
2260
|
+
# Expect the arrow token
|
|
2261
|
+
if current_token && current_token[:type] == :operator && current_token[:value] == '->'
|
|
2262
|
+
advance # Skip ->
|
|
2263
|
+
else
|
|
2264
|
+
@errors << {
|
|
2265
|
+
message: "Expected '->' in lambda function",
|
|
2266
|
+
position: @position,
|
|
2267
|
+
line: current_token ? current_token[:line] : 0,
|
|
2268
|
+
column: current_token ? current_token[:column] : 0
|
|
2269
|
+
}
|
|
2270
|
+
end
|
|
2271
|
+
|
|
2272
|
+
# Parse the lambda body expression (everything until the closing brace)
|
|
2273
|
+
body = parse_expression
|
|
2274
|
+
|
|
2275
|
+
# Expect closing brace
|
|
2276
|
+
if current_token && current_token[:type] == :brace_close
|
|
2277
|
+
advance # Skip }
|
|
2278
|
+
else
|
|
2279
|
+
@errors << {
|
|
2280
|
+
message: "Expected closing brace for lambda function",
|
|
2281
|
+
position: @position,
|
|
2282
|
+
line: current_token ? current_token[:line] : 0,
|
|
2283
|
+
column: current_token ? current_token[:column] : 0
|
|
2284
|
+
}
|
|
2285
|
+
end
|
|
2286
|
+
|
|
2287
|
+
return {
|
|
2288
|
+
type: :vim_lambda,
|
|
2289
|
+
params: params,
|
|
2290
|
+
body: body,
|
|
2291
|
+
line: line,
|
|
2292
|
+
column: column
|
|
2293
|
+
}
|
|
2294
|
+
end
|
|
2295
|
+
|
|
1168
2296
|
def parse_lambda_expression(line, column)
|
|
1169
2297
|
expect(:paren_open) # Skip '('
|
|
1170
2298
|
|
|
@@ -1292,6 +2420,11 @@ module Vinter
|
|
|
1292
2420
|
advance # Skip '{'
|
|
1293
2421
|
entries = []
|
|
1294
2422
|
|
|
2423
|
+
# Handle whitespace after opening brace
|
|
2424
|
+
while current_token && current_token[:type] == :whitespace
|
|
2425
|
+
advance
|
|
2426
|
+
end
|
|
2427
|
+
|
|
1295
2428
|
# Empty dictionary
|
|
1296
2429
|
if current_token && current_token[:type] == :brace_close
|
|
1297
2430
|
advance # Skip '}'
|
|
@@ -1304,11 +2437,21 @@ module Vinter
|
|
|
1304
2437
|
end
|
|
1305
2438
|
|
|
1306
2439
|
# Parse dictionary entries
|
|
1307
|
-
|
|
2440
|
+
while current_token && current_token[:type] != :brace_close
|
|
2441
|
+
# Skip any backslash line continuation markers and whitespace
|
|
2442
|
+
while current_token && (current_token[:type] == :backslash || current_token[:type] == :whitespace || current_token[:type] == :line_continuation)
|
|
2443
|
+
advance
|
|
2444
|
+
end
|
|
2445
|
+
|
|
2446
|
+
# Break if we reached the end or found closing brace
|
|
2447
|
+
if !current_token || current_token[:type] == :brace_close
|
|
2448
|
+
break
|
|
2449
|
+
end
|
|
2450
|
+
|
|
1308
2451
|
# Parse key (string or identifier)
|
|
1309
2452
|
key = nil
|
|
1310
2453
|
if current_token && (current_token[:type] == :string || current_token[:type] == :identifier)
|
|
1311
|
-
key = current_token[:
|
|
2454
|
+
key = current_token[:value]
|
|
1312
2455
|
advance # Skip key
|
|
1313
2456
|
else
|
|
1314
2457
|
@errors << {
|
|
@@ -1317,487 +2460,1429 @@ module Vinter
|
|
|
1317
2460
|
line: current_token ? current_token[:line] : 0,
|
|
1318
2461
|
column: current_token ? current_token[:column] : 0
|
|
1319
2462
|
}
|
|
1320
|
-
|
|
2463
|
+
# Try to recover by advancing until we find a colon or closing brace
|
|
2464
|
+
while current_token && current_token[:type] != :colon && current_token[:type] != :brace_close
|
|
2465
|
+
advance
|
|
2466
|
+
end
|
|
2467
|
+
if !current_token || current_token[:type] == :brace_close
|
|
2468
|
+
break
|
|
2469
|
+
end
|
|
2470
|
+
end
|
|
2471
|
+
|
|
2472
|
+
# Skip whitespace after key
|
|
2473
|
+
while current_token && current_token[:type] == :whitespace
|
|
2474
|
+
advance
|
|
1321
2475
|
end
|
|
1322
2476
|
|
|
1323
2477
|
# Expect colon
|
|
1324
|
-
|
|
2478
|
+
if current_token && current_token[:type] == :colon
|
|
2479
|
+
advance # Skip colon
|
|
2480
|
+
else
|
|
2481
|
+
@errors << {
|
|
2482
|
+
message: "Expected colon after dictionary key",
|
|
2483
|
+
position: @position,
|
|
2484
|
+
line: current_token ? current_token[:line] : 0,
|
|
2485
|
+
column: current_token ? current_token[:column] : 0
|
|
2486
|
+
}
|
|
2487
|
+
end
|
|
2488
|
+
|
|
2489
|
+
# Skip whitespace after colon
|
|
2490
|
+
while current_token && current_token[:type] == :whitespace
|
|
2491
|
+
advance
|
|
2492
|
+
end
|
|
2493
|
+
|
|
2494
|
+
# Parse value
|
|
2495
|
+
if current_token && current_token[:type] == :brace_open
|
|
2496
|
+
lambda_or_dict = parse_vim_lambda_or_dict(line, column)
|
|
2497
|
+
value = lambda_or_dict
|
|
2498
|
+
else
|
|
2499
|
+
value = parse_expression
|
|
2500
|
+
end
|
|
2501
|
+
|
|
2502
|
+
entries << {
|
|
2503
|
+
key: key,
|
|
2504
|
+
value: value
|
|
2505
|
+
}
|
|
2506
|
+
|
|
2507
|
+
# After parsing an entry, skip whitespace and look for comma or closing brace
|
|
2508
|
+
while current_token && current_token[:type] == :whitespace
|
|
2509
|
+
advance
|
|
2510
|
+
end
|
|
2511
|
+
|
|
2512
|
+
# Check for comma (indicates more entries follow)
|
|
2513
|
+
if current_token && current_token[:type] == :comma
|
|
2514
|
+
advance # Skip comma
|
|
2515
|
+
# Skip any line continuations and whitespace after comma
|
|
2516
|
+
while current_token && (current_token[:type] == :whitespace ||
|
|
2517
|
+
current_token[:type] == :backslash ||
|
|
2518
|
+
current_token[:type] == :line_continuation)
|
|
2519
|
+
advance
|
|
2520
|
+
end
|
|
2521
|
+
else
|
|
2522
|
+
# No comma - skip any line continuations before checking for closing brace
|
|
2523
|
+
while current_token && (current_token[:type] == :backslash ||
|
|
2524
|
+
current_token[:type] == :line_continuation ||
|
|
2525
|
+
current_token[:type] == :whitespace)
|
|
2526
|
+
advance
|
|
2527
|
+
end
|
|
2528
|
+
|
|
2529
|
+
# After skipping continuations, we should find the closing brace
|
|
2530
|
+
if current_token && current_token[:type] == :brace_close
|
|
2531
|
+
# This is fine - last entry without trailing comma
|
|
2532
|
+
break
|
|
2533
|
+
elsif current_token && current_token[:type] != :brace_close
|
|
2534
|
+
@errors << {
|
|
2535
|
+
message: "Expected comma or closing brace after dictionary entry",
|
|
2536
|
+
position: @position,
|
|
2537
|
+
line: current_token ? current_token[:line] : 0,
|
|
2538
|
+
column: current_token ? current_token[:column] : 0
|
|
2539
|
+
}
|
|
2540
|
+
break
|
|
2541
|
+
end
|
|
2542
|
+
end
|
|
2543
|
+
end
|
|
2544
|
+
|
|
2545
|
+
# Make sure we have a closing brace
|
|
2546
|
+
if current_token && current_token[:type] == :brace_close
|
|
2547
|
+
advance # Skip '}'
|
|
2548
|
+
else
|
|
2549
|
+
@errors << {
|
|
2550
|
+
message: "Expected closing brace for dictionary",
|
|
2551
|
+
position: @position,
|
|
2552
|
+
line: current_token ? current_token[:line] : 0,
|
|
2553
|
+
column: current_token ? current_token[:column] : 0
|
|
2554
|
+
}
|
|
2555
|
+
end
|
|
2556
|
+
|
|
2557
|
+
{
|
|
2558
|
+
type: :dict_literal,
|
|
2559
|
+
entries: entries,
|
|
2560
|
+
line: line,
|
|
2561
|
+
column: column
|
|
2562
|
+
}
|
|
2563
|
+
end
|
|
2564
|
+
|
|
2565
|
+
def parse_list_literal(line, column)
|
|
2566
|
+
advance # Skip '['
|
|
2567
|
+
elements = []
|
|
2568
|
+
|
|
2569
|
+
# Empty list
|
|
2570
|
+
if current_token && current_token[:type] == :bracket_close
|
|
2571
|
+
advance # Skip ']'
|
|
2572
|
+
return {
|
|
2573
|
+
type: :list_literal,
|
|
2574
|
+
elements: elements,
|
|
2575
|
+
line: line,
|
|
2576
|
+
column: column
|
|
2577
|
+
}
|
|
2578
|
+
end
|
|
2579
|
+
|
|
2580
|
+
# Parse list elements
|
|
2581
|
+
while @position < @tokens.length
|
|
2582
|
+
# Handle line continuation characters (backslash)
|
|
2583
|
+
if current_token && [:backslash, :line_continuation].include?(current_token[:type])
|
|
2584
|
+
# Skip the backslash token
|
|
2585
|
+
advance
|
|
2586
|
+
|
|
2587
|
+
# Skip any whitespace that might follow the backslash
|
|
2588
|
+
while current_token && current_token[:type] == :whitespace
|
|
2589
|
+
advance
|
|
2590
|
+
end
|
|
2591
|
+
|
|
2592
|
+
# Don't add a comma - just continue to parse the next element
|
|
2593
|
+
next
|
|
2594
|
+
end
|
|
2595
|
+
|
|
2596
|
+
# Check if we've reached the end of the list
|
|
2597
|
+
if current_token && current_token[:type] == :bracket_close
|
|
2598
|
+
advance # Skip ']'
|
|
2599
|
+
break
|
|
2600
|
+
end
|
|
2601
|
+
|
|
2602
|
+
element = parse_expression
|
|
2603
|
+
elements << element if element
|
|
2604
|
+
|
|
2605
|
+
# Continue if there's a comma
|
|
2606
|
+
if current_token && current_token[:type] == :comma
|
|
2607
|
+
advance # Skip comma
|
|
2608
|
+
# Or if the next token is a backslash (line continuation)
|
|
2609
|
+
else
|
|
2610
|
+
# binding.pry
|
|
2611
|
+
# If no comma and not a closing bracket or backslash, then it's an error
|
|
2612
|
+
# if current_token && current_token[:type] != :bracket_close
|
|
2613
|
+
# @errors << {
|
|
2614
|
+
# message: "Expected comma, backslash, or closing bracket after list element",
|
|
2615
|
+
# position: @position,
|
|
2616
|
+
# line: current_token[:line],
|
|
2617
|
+
# column: current_token[:column]
|
|
2618
|
+
# }
|
|
2619
|
+
# end
|
|
2620
|
+
|
|
2621
|
+
# We still want to skip the closing bracket if it's there
|
|
2622
|
+
if current_token && current_token[:type] == :bracket_close
|
|
2623
|
+
advance
|
|
2624
|
+
break
|
|
2625
|
+
end
|
|
2626
|
+
advance
|
|
2627
|
+
#next
|
|
2628
|
+
end
|
|
2629
|
+
end
|
|
2630
|
+
|
|
2631
|
+
# Check if we have a second list immediately following this one (Vim's special syntax)
|
|
2632
|
+
if current_token && current_token[:type] == :bracket_open
|
|
2633
|
+
next_list = parse_list_literal(current_token[:line], current_token[:column])
|
|
2634
|
+
|
|
2635
|
+
return {
|
|
2636
|
+
type: :list_concat_expression,
|
|
2637
|
+
left: {
|
|
2638
|
+
type: :list_literal,
|
|
2639
|
+
elements: elements,
|
|
2640
|
+
line: line,
|
|
2641
|
+
column: column
|
|
2642
|
+
},
|
|
2643
|
+
right: next_list,
|
|
2644
|
+
line: line,
|
|
2645
|
+
column: column
|
|
2646
|
+
}
|
|
2647
|
+
end
|
|
2648
|
+
|
|
2649
|
+
return {
|
|
2650
|
+
type: :list_literal,
|
|
2651
|
+
elements: elements,
|
|
2652
|
+
line: line,
|
|
2653
|
+
column: column
|
|
2654
|
+
}
|
|
2655
|
+
end
|
|
2656
|
+
|
|
2657
|
+
def parse_unary_expression
|
|
2658
|
+
# Check for unary operators (!, -, +)
|
|
2659
|
+
if current_token && current_token[:type] == :operator &&
|
|
2660
|
+
['!', '-', '+'].include?(current_token[:value])
|
|
2661
|
+
|
|
2662
|
+
op_token = advance # Skip the operator
|
|
2663
|
+
operand = parse_unary_expression # Recursively parse the operand
|
|
2664
|
+
|
|
2665
|
+
return {
|
|
2666
|
+
type: :unary_expression,
|
|
2667
|
+
operator: op_token[:value],
|
|
2668
|
+
operand: operand,
|
|
2669
|
+
line: op_token[:line],
|
|
2670
|
+
column: op_token[:column]
|
|
2671
|
+
}
|
|
2672
|
+
end
|
|
2673
|
+
|
|
2674
|
+
# If no unary operator, parse a primary expression
|
|
2675
|
+
return parse_primary_expression
|
|
2676
|
+
end
|
|
2677
|
+
|
|
2678
|
+
def parse_function_call(name, line, column)
|
|
2679
|
+
expect(:paren_open)
|
|
2680
|
+
|
|
2681
|
+
args = []
|
|
2682
|
+
|
|
2683
|
+
# Special handling for Vim functions that take code strings as arguments
|
|
2684
|
+
special_functions = ['map', 'reduce', 'sort', 'call', 'eval', 'execute', 'exec']
|
|
2685
|
+
is_special_function = special_functions.include?(name)
|
|
2686
|
+
|
|
2687
|
+
# Parse arguments until we find a closing parenthesis
|
|
2688
|
+
while @position < @tokens.length && current_token && current_token[:type] != :paren_close
|
|
2689
|
+
# Skip comments inside parameter lists
|
|
2690
|
+
if current_token && current_token[:type] == :comment
|
|
2691
|
+
advance
|
|
2692
|
+
next
|
|
2693
|
+
end
|
|
2694
|
+
if is_special_function && current_token && current_token[:type] == :string
|
|
2695
|
+
# For functions like map(), filter(), directly add the string as an argument
|
|
2696
|
+
string_token = parse_string
|
|
2697
|
+
args << {
|
|
2698
|
+
type: :literal,
|
|
2699
|
+
value: string_token[:value],
|
|
2700
|
+
token_type: :string,
|
|
2701
|
+
line: string_token[:line],
|
|
2702
|
+
column: string_token[:column]
|
|
2703
|
+
}
|
|
2704
|
+
else
|
|
2705
|
+
# Parse the argument
|
|
2706
|
+
arg = parse_expression
|
|
2707
|
+
args << arg if arg
|
|
2708
|
+
end
|
|
2709
|
+
|
|
2710
|
+
# Break if we hit the closing paren
|
|
2711
|
+
if current_token && current_token[:type] == :paren_close
|
|
2712
|
+
break
|
|
2713
|
+
end
|
|
2714
|
+
# If we have a comma, advance past it and continue
|
|
2715
|
+
if current_token && current_token[:type] == :comma
|
|
2716
|
+
advance
|
|
2717
|
+
# If we don't have a comma and we're not at the end, it's an error
|
|
2718
|
+
elsif current_token && current_token[:type] != :paren_close && current_token[:type] != :comment
|
|
2719
|
+
@errors << {
|
|
2720
|
+
message: "Expected comma or closing parenthesis after argument",
|
|
2721
|
+
position: @position,
|
|
2722
|
+
line: current_token[:line],
|
|
2723
|
+
column: current_token[:column]
|
|
2724
|
+
}
|
|
2725
|
+
break
|
|
2726
|
+
end
|
|
2727
|
+
end
|
|
2728
|
+
|
|
2729
|
+
expect(:paren_close)
|
|
2730
|
+
|
|
2731
|
+
{
|
|
2732
|
+
type: :function_call,
|
|
2733
|
+
name: name,
|
|
2734
|
+
arguments: args,
|
|
2735
|
+
line: line,
|
|
2736
|
+
column: column
|
|
2737
|
+
}
|
|
2738
|
+
end
|
|
2739
|
+
|
|
2740
|
+
def parse_vim_lambda_or_dict(line, column)
|
|
2741
|
+
# Save current position to peek ahead
|
|
2742
|
+
start_position = @position
|
|
2743
|
+
|
|
2744
|
+
advance # Skip opening brace
|
|
2745
|
+
|
|
2746
|
+
# Check if this is a lambda by looking for parameter names followed by arrow
|
|
2747
|
+
is_lambda = false
|
|
2748
|
+
param_names = []
|
|
2749
|
+
|
|
2750
|
+
# Check for immediate arrow (no parameters lambda)
|
|
2751
|
+
if current_token && current_token[:type] == :operator && current_token[:value] == '->'
|
|
2752
|
+
is_lambda = true
|
|
2753
|
+
else
|
|
2754
|
+
# Parse until closing brace or arrow
|
|
2755
|
+
while @position < @tokens.length && current_token[:type] != :brace_close
|
|
2756
|
+
if current_token[:type] == :identifier
|
|
2757
|
+
param_names << current_token[:value]
|
|
2758
|
+
advance
|
|
2759
|
+
|
|
2760
|
+
# Skip comma between parameters
|
|
2761
|
+
if current_token && current_token[:type] == :comma
|
|
2762
|
+
advance
|
|
2763
|
+
next
|
|
2764
|
+
end
|
|
2765
|
+
elsif current_token[:type] == :operator && current_token[:value] == '->'
|
|
2766
|
+
is_lambda = true
|
|
2767
|
+
break
|
|
2768
|
+
else
|
|
2769
|
+
# If we see a colon, this is likely a nested dictionary
|
|
2770
|
+
if current_token && current_token[:type] == :colon
|
|
2771
|
+
break
|
|
2772
|
+
end
|
|
2773
|
+
advance
|
|
2774
|
+
end
|
|
2775
|
+
end
|
|
2776
|
+
end
|
|
2777
|
+
|
|
2778
|
+
# Reset position
|
|
2779
|
+
@position = start_position
|
|
2780
|
+
|
|
2781
|
+
if is_lambda
|
|
2782
|
+
return parse_vim_lambda(line, column)
|
|
2783
|
+
else
|
|
2784
|
+
return parse_dict_literal(line, column)
|
|
2785
|
+
end
|
|
2786
|
+
end
|
|
2787
|
+
|
|
2788
|
+
def parse_import_statement
|
|
2789
|
+
token = advance # Skip 'import'
|
|
2790
|
+
line = token[:line]
|
|
2791
|
+
column = token[:column]
|
|
2792
|
+
|
|
2793
|
+
# Handle 'import autoload'
|
|
2794
|
+
is_autoload = false
|
|
2795
|
+
module_name = nil
|
|
2796
|
+
path = nil
|
|
2797
|
+
|
|
2798
|
+
if current_token && current_token[:type] == :identifier && current_token[:value] == 'autoload'
|
|
2799
|
+
is_autoload = true
|
|
2800
|
+
module_name = advance[:value] # Store "autoload" as the module name
|
|
2801
|
+
|
|
2802
|
+
# After "autoload" keyword, expect a string path
|
|
2803
|
+
if current_token && current_token[:type] == :string
|
|
2804
|
+
path = current_token[:value]
|
|
2805
|
+
advance
|
|
2806
|
+
else
|
|
2807
|
+
@errors << {
|
|
2808
|
+
message: "Expected string path after 'autoload'",
|
|
2809
|
+
position: @position,
|
|
2810
|
+
line: current_token ? current_token[:line] : 0,
|
|
2811
|
+
column: current_token ? current_token[:column] : 0
|
|
2812
|
+
}
|
|
2813
|
+
end
|
|
2814
|
+
else
|
|
2815
|
+
# Regular import with a string path
|
|
2816
|
+
if current_token && current_token[:type] == :string
|
|
2817
|
+
path = current_token[:value]
|
|
2818
|
+
|
|
2819
|
+
# Extract module name from the path
|
|
2820
|
+
# This is simplified logic - you might need more complex extraction
|
|
2821
|
+
module_name = path.gsub(/['"]/, '').split('/').last.split('.').first
|
|
2822
|
+
|
|
2823
|
+
advance
|
|
2824
|
+
else
|
|
2825
|
+
# Handle other import formats
|
|
2826
|
+
module_expr = parse_expression()
|
|
2827
|
+
if module_expr && module_expr[:type] == :literal && module_expr[:token_type] == :string
|
|
2828
|
+
path = module_expr[:value]
|
|
2829
|
+
module_name = path.gsub(/['"]/, '').split('/').last.split('.').first
|
|
2830
|
+
end
|
|
2831
|
+
end
|
|
2832
|
+
end
|
|
2833
|
+
|
|
2834
|
+
# Handle 'as name'
|
|
2835
|
+
as_name = nil
|
|
2836
|
+
if current_token && current_token[:type] == :identifier && current_token[:value] == 'as'
|
|
2837
|
+
advance # Skip 'as'
|
|
2838
|
+
if current_token && current_token[:type] == :identifier
|
|
2839
|
+
as_name = advance[:value]
|
|
2840
|
+
else
|
|
2841
|
+
@errors << {
|
|
2842
|
+
message: "Expected identifier after 'as'",
|
|
2843
|
+
position: @position,
|
|
2844
|
+
line: current_token ? current_token[:line] : 0,
|
|
2845
|
+
column: current_token ? current_token[:column] : 0
|
|
2846
|
+
}
|
|
2847
|
+
end
|
|
2848
|
+
end
|
|
2849
|
+
|
|
2850
|
+
{
|
|
2851
|
+
type: :import_statement,
|
|
2852
|
+
module: module_name,
|
|
2853
|
+
path: path,
|
|
2854
|
+
is_autoload: is_autoload,
|
|
2855
|
+
as_name: as_name,
|
|
2856
|
+
line: line,
|
|
2857
|
+
column: column
|
|
2858
|
+
}
|
|
2859
|
+
end
|
|
2860
|
+
|
|
2861
|
+
def parse_scriptencoding
|
|
2862
|
+
token = advance
|
|
2863
|
+
advance
|
|
2864
|
+
{ type: :scriptencoding, encoding: current_token[:value] }
|
|
2865
|
+
end
|
|
2866
|
+
|
|
2867
|
+
def parse_export_statement
|
|
2868
|
+
token = advance # Skip 'export'
|
|
2869
|
+
line = token[:line]
|
|
2870
|
+
column = token[:column]
|
|
2871
|
+
|
|
2872
|
+
# Export can be followed by var/const/def/function declarations
|
|
2873
|
+
if !current_token
|
|
2874
|
+
@errors << {
|
|
2875
|
+
message: "Expected declaration after export",
|
|
2876
|
+
position: @position,
|
|
2877
|
+
line: line,
|
|
2878
|
+
column: column
|
|
2879
|
+
}
|
|
2880
|
+
return nil
|
|
2881
|
+
end
|
|
2882
|
+
|
|
2883
|
+
exported_item = nil
|
|
2884
|
+
|
|
2885
|
+
if current_token[:type] == :keyword
|
|
2886
|
+
case current_token[:value]
|
|
2887
|
+
when 'def'
|
|
2888
|
+
exported_item = parse_def_function
|
|
2889
|
+
when 'function'
|
|
2890
|
+
exported_item = parse_legacy_function
|
|
2891
|
+
when 'var', 'const', 'final'
|
|
2892
|
+
exported_item = parse_variable_declaration
|
|
2893
|
+
when 'class'
|
|
2894
|
+
# Handle class export when implemented
|
|
2895
|
+
@errors << {
|
|
2896
|
+
message: "Class export not implemented yet",
|
|
2897
|
+
position: @position,
|
|
2898
|
+
line: current_token[:line],
|
|
2899
|
+
column: current_token[:column]
|
|
2900
|
+
}
|
|
2901
|
+
advance
|
|
2902
|
+
return nil
|
|
2903
|
+
else
|
|
2904
|
+
@errors << {
|
|
2905
|
+
message: "Unexpected keyword after export: #{current_token[:value]}",
|
|
2906
|
+
position: @position,
|
|
2907
|
+
line: current_token[:line],
|
|
2908
|
+
column: current_token[:column]
|
|
2909
|
+
}
|
|
2910
|
+
advance
|
|
2911
|
+
return nil
|
|
2912
|
+
end
|
|
2913
|
+
else
|
|
2914
|
+
@errors << {
|
|
2915
|
+
message: "Expected declaration after export",
|
|
2916
|
+
position: @position,
|
|
2917
|
+
line: current_token[:line],
|
|
2918
|
+
column: current_token[:column]
|
|
2919
|
+
}
|
|
2920
|
+
advance
|
|
2921
|
+
return nil
|
|
2922
|
+
end
|
|
2923
|
+
|
|
2924
|
+
{
|
|
2925
|
+
type: :export_statement,
|
|
2926
|
+
export: exported_item,
|
|
2927
|
+
line: line,
|
|
2928
|
+
column: column
|
|
2929
|
+
}
|
|
2930
|
+
end
|
|
2931
|
+
|
|
2932
|
+
def parse_command_definition
|
|
2933
|
+
token = advance # Skip 'command' or 'command!'
|
|
2934
|
+
line = token[:line]
|
|
2935
|
+
column = token[:column]
|
|
2936
|
+
|
|
2937
|
+
# Check if the command has a bang (!)
|
|
2938
|
+
has_bang = false
|
|
2939
|
+
if current_token && current_token[:type] == :operator && current_token[:value] == '!'
|
|
2940
|
+
has_bang = true
|
|
2941
|
+
advance # Skip '!'
|
|
2942
|
+
end
|
|
2943
|
+
|
|
2944
|
+
# Parse command options/attributes (starting with hyphen)
|
|
2945
|
+
attributes = []
|
|
2946
|
+
while current_token && (
|
|
2947
|
+
(current_token[:type] == :operator && current_token[:value] == '-') ||
|
|
2948
|
+
(current_token[:type] == :identifier && current_token[:value].start_with?('-'))
|
|
2949
|
+
)
|
|
2950
|
+
# Handle option as a single attribute if it's already combined
|
|
2951
|
+
if current_token[:type] == :identifier && current_token[:value].start_with?('-')
|
|
2952
|
+
attributes << current_token[:value]
|
|
2953
|
+
advance
|
|
2954
|
+
else
|
|
2955
|
+
# Otherwise combine the hyphen with the following identifier
|
|
2956
|
+
advance # Skip the hyphen
|
|
2957
|
+
if current_token && current_token[:type] == :identifier
|
|
2958
|
+
attributes << "-#{current_token[:value]}"
|
|
2959
|
+
advance
|
|
2960
|
+
end
|
|
2961
|
+
end
|
|
2962
|
+
|
|
2963
|
+
# If there's an = followed by a value, include it in the attribute
|
|
2964
|
+
if current_token && current_token[:type] == :operator && current_token[:value] == '='
|
|
2965
|
+
attribute = attributes.pop # Take the last attribute we added
|
|
2966
|
+
advance # Skip the '='
|
|
2967
|
+
|
|
2968
|
+
# Get the value (number or identifier)
|
|
2969
|
+
if current_token && (current_token[:type] == :number || current_token[:type] == :identifier)
|
|
2970
|
+
attribute += "=#{current_token[:value]}"
|
|
2971
|
+
attributes << attribute
|
|
2972
|
+
advance
|
|
2973
|
+
end
|
|
2974
|
+
end
|
|
2975
|
+
end
|
|
2976
|
+
|
|
2977
|
+
# Parse the command name
|
|
2978
|
+
command_name = nil
|
|
2979
|
+
if current_token && current_token[:type] == :identifier
|
|
2980
|
+
command_name = current_token[:value]
|
|
2981
|
+
advance
|
|
2982
|
+
else
|
|
2983
|
+
@errors << {
|
|
2984
|
+
message: "Expected command name",
|
|
2985
|
+
position: @position,
|
|
2986
|
+
line: current_token ? current_token[:line] : 0,
|
|
2987
|
+
column: current_token ? current_token[:column] : 0
|
|
2988
|
+
}
|
|
2989
|
+
end
|
|
2990
|
+
|
|
2991
|
+
# Parse the command implementation - collect all remaining tokens as raw parts
|
|
2992
|
+
implementation_parts = []
|
|
2993
|
+
while @position < @tokens.length
|
|
2994
|
+
# Break on end of line or comment
|
|
2995
|
+
if !current_token || current_token[:type] == :comment ||
|
|
2996
|
+
(current_token[:value] == "\n" && !current_token[:value].start_with?("\\"))
|
|
2997
|
+
break
|
|
2998
|
+
end
|
|
2999
|
+
|
|
3000
|
+
implementation_parts << current_token
|
|
3001
|
+
advance
|
|
3002
|
+
end
|
|
3003
|
+
|
|
3004
|
+
{
|
|
3005
|
+
type: :command_definition,
|
|
3006
|
+
name: command_name,
|
|
3007
|
+
has_bang: has_bang,
|
|
3008
|
+
attributes: attributes,
|
|
3009
|
+
implementation: implementation_parts,
|
|
3010
|
+
line: line,
|
|
3011
|
+
column: column
|
|
3012
|
+
}
|
|
3013
|
+
end
|
|
3014
|
+
|
|
3015
|
+
def parse_legacy_function
|
|
3016
|
+
token = advance # Skip 'function'
|
|
3017
|
+
line = token[:line]
|
|
3018
|
+
column = token[:column]
|
|
3019
|
+
|
|
3020
|
+
# Check for bang (!) in function definition
|
|
3021
|
+
has_bang = false
|
|
3022
|
+
if current_token && current_token[:type] == :operator && current_token[:value] == '!'
|
|
3023
|
+
has_bang = true
|
|
3024
|
+
advance # Skip '!'
|
|
3025
|
+
end
|
|
3026
|
+
|
|
3027
|
+
# For script-local functions or other scoped functions
|
|
3028
|
+
is_script_local = false
|
|
3029
|
+
function_scope = nil
|
|
3030
|
+
|
|
3031
|
+
# Check if we have a script-local function (s:)
|
|
3032
|
+
if current_token && current_token[:type] == :script_local
|
|
3033
|
+
is_script_local = true
|
|
3034
|
+
function_scope = current_token[:value]
|
|
3035
|
+
advance # Skip s: prefix
|
|
3036
|
+
elsif current_token && current_token[:type] == :identifier && current_token[:value].include?(':')
|
|
3037
|
+
# Handle other scoped functions like g: or b:
|
|
3038
|
+
parts = current_token[:value].split(':')
|
|
3039
|
+
if parts.length == 2
|
|
3040
|
+
function_scope = parts[0] + ':'
|
|
3041
|
+
advance
|
|
3042
|
+
end
|
|
3043
|
+
end
|
|
3044
|
+
|
|
3045
|
+
# Now handle the function name, which might be separate from the scope
|
|
3046
|
+
name = nil
|
|
3047
|
+
if !is_script_local && function_scope.nil? && current_token && current_token[:type] == :identifier
|
|
3048
|
+
name = current_token
|
|
3049
|
+
advance
|
|
3050
|
+
elsif is_script_local || !function_scope.nil?
|
|
3051
|
+
if current_token && current_token[:type] == :identifier
|
|
3052
|
+
name = current_token
|
|
3053
|
+
advance
|
|
3054
|
+
end
|
|
3055
|
+
else
|
|
3056
|
+
name = expect(:identifier)
|
|
3057
|
+
end
|
|
3058
|
+
|
|
3059
|
+
# Parse parameter list
|
|
3060
|
+
expect(:paren_open)
|
|
3061
|
+
params = parse_parameter_list_legacy
|
|
3062
|
+
expect(:paren_close)
|
|
3063
|
+
|
|
3064
|
+
# Check for optional attributes (range, dict, abort, closure)
|
|
3065
|
+
attributes = []
|
|
3066
|
+
while current_token
|
|
3067
|
+
if current_token[:type] == :keyword && current_token[:value] == 'abort'
|
|
3068
|
+
attributes << advance[:value]
|
|
3069
|
+
elsif current_token[:type] == :identifier &&
|
|
3070
|
+
['range', 'dict', 'closure'].include?(current_token[:value])
|
|
3071
|
+
attributes << advance[:value]
|
|
3072
|
+
else
|
|
3073
|
+
break
|
|
3074
|
+
end
|
|
3075
|
+
end
|
|
3076
|
+
|
|
3077
|
+
# Parse function body until 'endfunction' or 'endfunc'
|
|
3078
|
+
body = parse_body_until('endfunction', 'endfunc')
|
|
3079
|
+
|
|
3080
|
+
# Expect endfunction/endfunc
|
|
3081
|
+
expect_end_keyword('endfunction', 'endfunc')
|
|
3082
|
+
|
|
3083
|
+
function_name = name ? name[:value] : nil
|
|
3084
|
+
if function_scope
|
|
3085
|
+
function_name = function_scope + function_name if function_name
|
|
3086
|
+
end
|
|
3087
|
+
|
|
3088
|
+
{
|
|
3089
|
+
type: :legacy_function,
|
|
3090
|
+
name: function_name,
|
|
3091
|
+
is_script_local: is_script_local,
|
|
3092
|
+
scope: function_scope,
|
|
3093
|
+
params: params,
|
|
3094
|
+
has_bang: has_bang,
|
|
3095
|
+
attributes: attributes,
|
|
3096
|
+
body: body,
|
|
3097
|
+
line: line,
|
|
3098
|
+
column: column
|
|
3099
|
+
}
|
|
3100
|
+
end
|
|
3101
|
+
|
|
3102
|
+
# Legacy function parameters are different - they can use a:name syntax
|
|
3103
|
+
def parse_parameter_list_legacy
|
|
3104
|
+
params = []
|
|
3105
|
+
|
|
3106
|
+
# Empty parameter list
|
|
3107
|
+
if current_token && current_token[:type] == :paren_close
|
|
3108
|
+
return params
|
|
3109
|
+
end
|
|
3110
|
+
|
|
3111
|
+
# Handle special case: varargs at the start of the parameter list
|
|
3112
|
+
if current_token && (
|
|
3113
|
+
(current_token[:type] == :ellipsis) ||
|
|
3114
|
+
(current_token[:type] == :operator && current_token[:value] == '.')
|
|
3115
|
+
)
|
|
3116
|
+
if current_token[:type] == :ellipsis
|
|
3117
|
+
token = advance
|
|
3118
|
+
params << {
|
|
3119
|
+
type: :var_args_legacy,
|
|
3120
|
+
name: '...',
|
|
3121
|
+
line: token[:line],
|
|
3122
|
+
column: token[:column]
|
|
3123
|
+
}
|
|
3124
|
+
return params
|
|
3125
|
+
else
|
|
3126
|
+
# Count consecutive dots
|
|
3127
|
+
dot_count = 0
|
|
3128
|
+
first_dot_token = current_token
|
|
3129
|
+
|
|
3130
|
+
# Store the line and column before we advance
|
|
3131
|
+
dot_line = first_dot_token[:line]
|
|
3132
|
+
dot_column = first_dot_token[:column]
|
|
3133
|
+
|
|
3134
|
+
while current_token && current_token[:type] == :operator && current_token[:value] == '.'
|
|
3135
|
+
dot_count += 1
|
|
3136
|
+
advance
|
|
3137
|
+
end
|
|
3138
|
+
|
|
3139
|
+
if dot_count == 3
|
|
3140
|
+
params << {
|
|
3141
|
+
type: :var_args_legacy,
|
|
3142
|
+
name: '...',
|
|
3143
|
+
line: dot_line,
|
|
3144
|
+
column: dot_column
|
|
3145
|
+
}
|
|
3146
|
+
return params
|
|
3147
|
+
end
|
|
3148
|
+
end
|
|
3149
|
+
end
|
|
3150
|
+
|
|
3151
|
+
# Regular parameter parsing (existing logic)
|
|
3152
|
+
loop do
|
|
3153
|
+
if current_token && current_token[:type] == :identifier
|
|
3154
|
+
param_name = advance
|
|
3155
|
+
|
|
3156
|
+
# Check for default value
|
|
3157
|
+
default_value = nil
|
|
3158
|
+
if current_token && current_token[:type] == :operator && current_token[:value] == '='
|
|
3159
|
+
advance # Skip '='
|
|
3160
|
+
default_value = parse_expression
|
|
3161
|
+
end
|
|
3162
|
+
|
|
3163
|
+
params << {
|
|
3164
|
+
type: :parameter,
|
|
3165
|
+
name: param_name[:value],
|
|
3166
|
+
default_value: default_value,
|
|
3167
|
+
optional: default_value != nil,
|
|
3168
|
+
line: param_name[:line],
|
|
3169
|
+
column: param_name[:column]
|
|
3170
|
+
}
|
|
3171
|
+
elsif current_token && current_token[:type] == :arg_variable
|
|
3172
|
+
param_name = advance
|
|
3173
|
+
name_without_prefix = param_name[:value].sub(/^a:/, '')
|
|
3174
|
+
|
|
3175
|
+
# Check for default value
|
|
3176
|
+
default_value = nil
|
|
3177
|
+
if current_token && current_token[:type] == :operator && current_token[:value] == '='
|
|
3178
|
+
advance # Skip '='
|
|
3179
|
+
default_value = parse_expression
|
|
3180
|
+
end
|
|
3181
|
+
|
|
3182
|
+
params << {
|
|
3183
|
+
type: :parameter,
|
|
3184
|
+
name: name_without_prefix,
|
|
3185
|
+
default_value: default_value,
|
|
3186
|
+
optional: default_value != nil,
|
|
3187
|
+
line: param_name[:line],
|
|
3188
|
+
column: param_name[:column],
|
|
3189
|
+
is_arg_prefixed: true
|
|
3190
|
+
}
|
|
3191
|
+
else
|
|
3192
|
+
# We might be at varargs after other parameters
|
|
3193
|
+
if current_token && (
|
|
3194
|
+
(current_token[:type] == :ellipsis) ||
|
|
3195
|
+
(current_token[:type] == :operator && current_token[:value] == '.')
|
|
3196
|
+
)
|
|
3197
|
+
# Add debug
|
|
3198
|
+
#puts "Found potential varargs token: #{current_token[:type]} #{current_token[:value]}"
|
|
3199
|
+
|
|
3200
|
+
if current_token[:type] == :ellipsis
|
|
3201
|
+
token = current_token # STORE the token before advancing
|
|
3202
|
+
advance
|
|
3203
|
+
params << {
|
|
3204
|
+
type: :var_args_legacy,
|
|
3205
|
+
name: '...',
|
|
3206
|
+
line: token[:line], # Use stored token
|
|
3207
|
+
column: token[:column]
|
|
3208
|
+
}
|
|
3209
|
+
else
|
|
3210
|
+
dot_count = 0
|
|
3211
|
+
first_dot_token = current_token
|
|
3212
|
+
|
|
3213
|
+
# Store line/column BEFORE advancing
|
|
3214
|
+
dot_line = first_dot_token[:line]
|
|
3215
|
+
dot_column = first_dot_token[:column]
|
|
3216
|
+
|
|
3217
|
+
#puts "Starting dot sequence at line #{dot_line}, column #{dot_column}"
|
|
3218
|
+
|
|
3219
|
+
while current_token && current_token[:type] == :operator && current_token[:value] == '.'
|
|
3220
|
+
dot_count += 1
|
|
3221
|
+
#puts "Found dot #{dot_count}"
|
|
3222
|
+
advance
|
|
3223
|
+
end
|
|
3224
|
+
|
|
3225
|
+
if dot_count == 3
|
|
3226
|
+
#puts "Complete varargs found (3 dots)"
|
|
3227
|
+
params << {
|
|
3228
|
+
type: :var_args_legacy,
|
|
3229
|
+
name: '...',
|
|
3230
|
+
line: dot_line, # Use stored values
|
|
3231
|
+
column: dot_column
|
|
3232
|
+
}
|
|
3233
|
+
else
|
|
3234
|
+
#puts "Incomplete varargs: only #{dot_count} dots found"
|
|
3235
|
+
end
|
|
3236
|
+
end
|
|
3237
|
+
break
|
|
3238
|
+
else
|
|
3239
|
+
# Add debug to see what unexpected token we're encountering
|
|
3240
|
+
#puts "Unexpected token in parameter list: #{current_token ? current_token[:type] : 'nil'} #{current_token ? current_token[:value] : ''}"
|
|
3241
|
+
|
|
3242
|
+
# Not a valid parameter or varargs
|
|
3243
|
+
@errors << {
|
|
3244
|
+
message: "Expected parameter name",
|
|
3245
|
+
position: @position,
|
|
3246
|
+
line: current_token ? current_token[:line] : 0,
|
|
3247
|
+
column: current_token ? current_token[:column] : 0
|
|
3248
|
+
}
|
|
3249
|
+
break
|
|
3250
|
+
end
|
|
3251
|
+
end
|
|
3252
|
+
if current_token && current_token[:type] == :comma
|
|
3253
|
+
advance
|
|
3254
|
+
else
|
|
3255
|
+
break
|
|
3256
|
+
end
|
|
3257
|
+
end
|
|
3258
|
+
|
|
3259
|
+
params
|
|
3260
|
+
end
|
|
3261
|
+
|
|
3262
|
+
def parse_try_statement
|
|
3263
|
+
token = advance # Skip 'try'
|
|
3264
|
+
line = token[:line]
|
|
3265
|
+
column = token[:column]
|
|
3266
|
+
|
|
3267
|
+
# Parse the try body until catch/finally/endtry
|
|
3268
|
+
body = parse_body_until('catch', 'finally', 'endtry')
|
|
3269
|
+
|
|
3270
|
+
catch_clauses = []
|
|
3271
|
+
finally_clause = nil
|
|
3272
|
+
|
|
3273
|
+
# Parse catch clauses
|
|
3274
|
+
while @position < @tokens.length &&
|
|
3275
|
+
current_token && current_token[:type] == :keyword &&
|
|
3276
|
+
current_token[:value] == 'catch'
|
|
3277
|
+
catch_token = advance # Skip 'catch'
|
|
3278
|
+
catch_line = catch_token[:line]
|
|
3279
|
+
catch_column = catch_token[:column]
|
|
3280
|
+
|
|
3281
|
+
# Parse the pattern (anything until the next statement)
|
|
3282
|
+
pattern = ''
|
|
3283
|
+
pattern_tokens = []
|
|
3284
|
+
|
|
3285
|
+
# Collect all tokens until we hit a newline or a statement
|
|
3286
|
+
while @position < @tokens.length
|
|
3287
|
+
if !current_token ||
|
|
3288
|
+
(current_token[:type] == :whitespace && current_token[:value].include?("\n")) ||
|
|
3289
|
+
current_token[:type] == :comment
|
|
3290
|
+
break
|
|
3291
|
+
end
|
|
3292
|
+
|
|
3293
|
+
pattern_tokens << current_token
|
|
3294
|
+
pattern += current_token[:value]
|
|
3295
|
+
advance
|
|
3296
|
+
end
|
|
1325
3297
|
|
|
1326
|
-
# Parse
|
|
1327
|
-
|
|
3298
|
+
# Parse the catch body until next catch/finally/endtry
|
|
3299
|
+
catch_body = parse_body_until('catch', 'finally', 'endtry')
|
|
1328
3300
|
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
3301
|
+
catch_clauses << {
|
|
3302
|
+
type: :catch_clause,
|
|
3303
|
+
pattern: pattern.strip,
|
|
3304
|
+
body: catch_body,
|
|
3305
|
+
line: catch_line,
|
|
3306
|
+
column: catch_column
|
|
1332
3307
|
}
|
|
3308
|
+
end
|
|
1333
3309
|
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
3310
|
+
# Parse finally clause if present
|
|
3311
|
+
if @position < @tokens.length &&
|
|
3312
|
+
current_token && current_token[:type] == :keyword &&
|
|
3313
|
+
current_token[:value] == 'finally'
|
|
3314
|
+
finally_token = advance # Skip 'finally'
|
|
3315
|
+
finally_line = finally_token[:line]
|
|
3316
|
+
finally_column = finally_token[:column]
|
|
3317
|
+
|
|
3318
|
+
# Parse the finally body until 'endtry'
|
|
3319
|
+
finally_body = parse_body_until('endtry')
|
|
3320
|
+
|
|
3321
|
+
finally_clause = {
|
|
3322
|
+
type: :finally_clause,
|
|
3323
|
+
body: finally_body,
|
|
3324
|
+
line: finally_line,
|
|
3325
|
+
column: finally_column
|
|
3326
|
+
}
|
|
1341
3327
|
end
|
|
1342
3328
|
|
|
1343
|
-
|
|
3329
|
+
# Expect endtry
|
|
3330
|
+
expect_end_keyword('endtry')
|
|
1344
3331
|
|
|
1345
|
-
{
|
|
1346
|
-
type: :
|
|
1347
|
-
|
|
3332
|
+
return {
|
|
3333
|
+
type: :try_statement,
|
|
3334
|
+
body: body,
|
|
3335
|
+
catch_clauses: catch_clauses,
|
|
3336
|
+
finally_clause: finally_clause,
|
|
1348
3337
|
line: line,
|
|
1349
3338
|
column: column
|
|
1350
3339
|
}
|
|
1351
3340
|
end
|
|
1352
3341
|
|
|
1353
|
-
|
|
1354
|
-
advance # Skip '['
|
|
1355
|
-
elements = []
|
|
3342
|
+
# Also add a method to parse throw statements:
|
|
1356
3343
|
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
type: :list_literal,
|
|
1362
|
-
elements: elements,
|
|
1363
|
-
line: line,
|
|
1364
|
-
column: column
|
|
1365
|
-
}
|
|
1366
|
-
end
|
|
3344
|
+
def parse_throw_statement
|
|
3345
|
+
token = advance # Skip 'throw'
|
|
3346
|
+
line = token[:line]
|
|
3347
|
+
column = token[:column]
|
|
1367
3348
|
|
|
1368
|
-
# Parse
|
|
1369
|
-
|
|
1370
|
-
# Check if we've reached the end of the list
|
|
1371
|
-
if current_token && current_token[:type] == :bracket_close
|
|
1372
|
-
advance # Skip ']'
|
|
1373
|
-
break
|
|
1374
|
-
end
|
|
3349
|
+
# Parse the expression to throw
|
|
3350
|
+
expression = parse_expression
|
|
1375
3351
|
|
|
1376
|
-
|
|
1377
|
-
|
|
3352
|
+
return {
|
|
3353
|
+
type: :throw_statement,
|
|
3354
|
+
expression: expression,
|
|
3355
|
+
line: line,
|
|
3356
|
+
column: column
|
|
3357
|
+
}
|
|
3358
|
+
end
|
|
1378
3359
|
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
# If no comma and not a closing bracket, then it's an error
|
|
1384
|
-
if current_token && current_token[:type] != :bracket_close
|
|
1385
|
-
@errors << {
|
|
1386
|
-
message: "Expected comma or closing bracket after list element",
|
|
1387
|
-
position: @position,
|
|
1388
|
-
line: current_token[:line],
|
|
1389
|
-
column: current_token[:column]
|
|
1390
|
-
}
|
|
1391
|
-
end
|
|
3360
|
+
def parse_call_statement
|
|
3361
|
+
token = advance # Skip 'call'
|
|
3362
|
+
line = token[:line]
|
|
3363
|
+
column = token[:column]
|
|
1392
3364
|
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
3365
|
+
# Parse the function call expression that follows 'call'
|
|
3366
|
+
func_expr = nil
|
|
3367
|
+
|
|
3368
|
+
if current_token && current_token[:type] == :script_local
|
|
3369
|
+
# Handle script-local function call (s:func_name)
|
|
3370
|
+
func_name = current_token[:value]
|
|
3371
|
+
func_line = current_token[:line]
|
|
3372
|
+
func_column = current_token[:column]
|
|
3373
|
+
advance
|
|
3374
|
+
|
|
3375
|
+
# Parse arguments
|
|
3376
|
+
args = []
|
|
3377
|
+
if current_token && current_token[:type] == :paren_open
|
|
3378
|
+
expect(:paren_open) # Skip '('
|
|
3379
|
+
|
|
3380
|
+
# Parse arguments if any
|
|
3381
|
+
unless current_token && current_token[:type] == :paren_close
|
|
3382
|
+
loop do
|
|
3383
|
+
arg = parse_expression
|
|
3384
|
+
args << arg if arg
|
|
3385
|
+
|
|
3386
|
+
if current_token && current_token[:type] == :comma
|
|
3387
|
+
advance # Skip comma
|
|
3388
|
+
else
|
|
3389
|
+
break
|
|
3390
|
+
end
|
|
3391
|
+
end
|
|
1396
3392
|
end
|
|
1397
3393
|
|
|
1398
|
-
|
|
3394
|
+
expect(:paren_close) # Skip ')'
|
|
1399
3395
|
end
|
|
1400
|
-
end
|
|
1401
|
-
|
|
1402
|
-
# Check if we have a second list immediately following this one (Vim's special syntax)
|
|
1403
|
-
if current_token && current_token[:type] == :bracket_open
|
|
1404
|
-
next_list = parse_list_literal(current_token[:line], current_token[:column])
|
|
1405
3396
|
|
|
1406
|
-
|
|
1407
|
-
type: :
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
column: column
|
|
1413
|
-
},
|
|
1414
|
-
right: next_list,
|
|
1415
|
-
line: line,
|
|
1416
|
-
column: column
|
|
3397
|
+
func_expr = {
|
|
3398
|
+
type: :script_local_call,
|
|
3399
|
+
name: func_name,
|
|
3400
|
+
arguments: args,
|
|
3401
|
+
line: func_line,
|
|
3402
|
+
column: func_column
|
|
1417
3403
|
}
|
|
3404
|
+
else
|
|
3405
|
+
# For other function calls
|
|
3406
|
+
func_expr = parse_expression
|
|
1418
3407
|
end
|
|
1419
3408
|
|
|
1420
|
-
|
|
1421
|
-
type: :
|
|
1422
|
-
|
|
3409
|
+
{
|
|
3410
|
+
type: :call_statement,
|
|
3411
|
+
expression: func_expr,
|
|
1423
3412
|
line: line,
|
|
1424
3413
|
column: column
|
|
1425
3414
|
}
|
|
1426
3415
|
end
|
|
1427
3416
|
|
|
1428
|
-
def
|
|
1429
|
-
#
|
|
1430
|
-
|
|
1431
|
-
|
|
3417
|
+
def parse_string
|
|
3418
|
+
# Start with the first string or expression
|
|
3419
|
+
left = parse_primary_term
|
|
3420
|
+
|
|
3421
|
+
# Continue as long as we see the '.' or '..' string concatenation operator
|
|
3422
|
+
while current_token && current_token[:type] == :operator &&
|
|
3423
|
+
(current_token[:value] == '.' || current_token[:value] == '..')
|
|
1432
3424
|
|
|
3425
|
+
# Store the operator token
|
|
1433
3426
|
op_token = advance # Skip the operator
|
|
1434
|
-
|
|
3427
|
+
op = op_token[:value]
|
|
1435
3428
|
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
3429
|
+
# If this is the dot operator, check if it's actually part of '..'
|
|
3430
|
+
if op == '.' && peek_token && peek_token[:type] == :operator && peek_token[:value] == '.'
|
|
3431
|
+
advance # Skip the second dot
|
|
3432
|
+
op = '..'
|
|
3433
|
+
end
|
|
3434
|
+
|
|
3435
|
+
# Parse the right side of the concatenation
|
|
3436
|
+
right = parse_primary_term
|
|
3437
|
+
|
|
3438
|
+
# Create a string concatenation expression
|
|
3439
|
+
left = {
|
|
3440
|
+
type: :string_concatenation,
|
|
3441
|
+
operator: op,
|
|
3442
|
+
left: left,
|
|
3443
|
+
right: right,
|
|
1440
3444
|
line: op_token[:line],
|
|
1441
3445
|
column: op_token[:column]
|
|
1442
3446
|
}
|
|
1443
3447
|
end
|
|
1444
3448
|
|
|
1445
|
-
|
|
1446
|
-
return parse_primary_expression
|
|
3449
|
+
return left
|
|
1447
3450
|
end
|
|
1448
3451
|
|
|
1449
|
-
|
|
1450
|
-
|
|
3452
|
+
# Helper function to parse a primary term in string expressions
|
|
3453
|
+
def parse_primary_term
|
|
3454
|
+
if !current_token
|
|
3455
|
+
return nil
|
|
3456
|
+
end
|
|
1451
3457
|
|
|
1452
|
-
|
|
3458
|
+
token = current_token
|
|
3459
|
+
line = token[:line]
|
|
3460
|
+
column = token[:column]
|
|
1453
3461
|
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
#
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
3462
|
+
case token[:type]
|
|
3463
|
+
when :string
|
|
3464
|
+
# Handle string literal
|
|
3465
|
+
advance
|
|
3466
|
+
return {
|
|
3467
|
+
type: :literal,
|
|
3468
|
+
value: token[:value],
|
|
3469
|
+
token_type: :string,
|
|
3470
|
+
line: line,
|
|
3471
|
+
column: column
|
|
3472
|
+
}
|
|
3473
|
+
when :identifier, :global_variable, :script_local, :arg_variable,
|
|
3474
|
+
:local_variable, :buffer_local, :window_local, :tab_local
|
|
3475
|
+
# Handle variable references
|
|
3476
|
+
advance
|
|
3477
|
+
return {
|
|
3478
|
+
type: token[:type],
|
|
3479
|
+
name: token[:value],
|
|
3480
|
+
line: line,
|
|
3481
|
+
column: column
|
|
3482
|
+
}
|
|
3483
|
+
when :paren_open
|
|
3484
|
+
# Handle parenthesized expressions
|
|
3485
|
+
advance # Skip '('
|
|
3486
|
+
expr = parse_expression
|
|
3487
|
+
expect(:paren_close) # Skip ')'
|
|
3488
|
+
return expr
|
|
3489
|
+
when :function
|
|
3490
|
+
# Handle function calls
|
|
3491
|
+
return parse_function_call(token[:value], line, column)
|
|
3492
|
+
else
|
|
3493
|
+
# For anything else, use the standard expression parser
|
|
3494
|
+
return parse_expression
|
|
3495
|
+
end
|
|
3496
|
+
end
|
|
1461
3497
|
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
arg = parse_expression
|
|
1467
|
-
end
|
|
3498
|
+
def parse_silent_command
|
|
3499
|
+
token = advance # Skip 'silent'
|
|
3500
|
+
line = token[:line]
|
|
3501
|
+
column = token[:column]
|
|
1468
3502
|
|
|
1469
|
-
|
|
3503
|
+
# Check for ! after silent
|
|
3504
|
+
has_bang = false
|
|
3505
|
+
if current_token && current_token[:type] == :operator && current_token[:value] == '!'
|
|
3506
|
+
has_bang = true
|
|
3507
|
+
advance # Skip '!'
|
|
3508
|
+
end
|
|
1470
3509
|
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
end
|
|
3510
|
+
# Now parse the command that follows silent
|
|
3511
|
+
# It could be a standard command or a command with a range
|
|
3512
|
+
command = nil
|
|
1475
3513
|
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
3514
|
+
# Check if the next token is a range operator (% in this case)
|
|
3515
|
+
if current_token && current_token[:type] == :operator && current_token[:value] == '%'
|
|
3516
|
+
range_token = advance # Skip '%'
|
|
3517
|
+
|
|
3518
|
+
# Now we expect a command (like 'delete')
|
|
3519
|
+
if current_token &&
|
|
3520
|
+
(current_token[:type] == :keyword || current_token[:type] == :identifier)
|
|
3521
|
+
cmd_token = advance # Skip the command name
|
|
3522
|
+
cmd_name = cmd_token[:value]
|
|
3523
|
+
|
|
3524
|
+
# Parse any arguments to the command
|
|
3525
|
+
args = []
|
|
3526
|
+
while current_token &&
|
|
3527
|
+
current_token[:type] != :comment &&
|
|
3528
|
+
(current_token[:value] != "\n" ||
|
|
3529
|
+
(current_token[:value] == "\n" &&
|
|
3530
|
+
!current_token[:value].start_with?("\\")))
|
|
3531
|
+
|
|
3532
|
+
# Add the token as an argument
|
|
3533
|
+
args << current_token
|
|
3534
|
+
advance
|
|
3535
|
+
end
|
|
3536
|
+
|
|
3537
|
+
command = {
|
|
3538
|
+
type: :range_command,
|
|
3539
|
+
range: '%',
|
|
3540
|
+
command: {
|
|
3541
|
+
type: :command,
|
|
3542
|
+
name: cmd_name,
|
|
3543
|
+
args: args,
|
|
3544
|
+
line: cmd_token[:line],
|
|
3545
|
+
column: cmd_token[:column]
|
|
3546
|
+
},
|
|
3547
|
+
line: range_token[:line],
|
|
3548
|
+
column: range_token[:column]
|
|
3549
|
+
}
|
|
3550
|
+
else
|
|
1481
3551
|
@errors << {
|
|
1482
|
-
message: "Expected
|
|
3552
|
+
message: "Expected command after range operator '%'",
|
|
1483
3553
|
position: @position,
|
|
1484
|
-
line: current_token[:line],
|
|
1485
|
-
column: current_token[:column]
|
|
3554
|
+
line: current_token ? current_token[:line] : range_token[:line],
|
|
3555
|
+
column: current_token ? current_token[:column] : range_token[:column] + 1
|
|
1486
3556
|
}
|
|
1487
|
-
break
|
|
1488
3557
|
end
|
|
3558
|
+
else
|
|
3559
|
+
# Parse a regular command
|
|
3560
|
+
command = parse_statement
|
|
1489
3561
|
end
|
|
1490
3562
|
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
name: name,
|
|
1496
|
-
arguments: args,
|
|
3563
|
+
return {
|
|
3564
|
+
type: :silent_command,
|
|
3565
|
+
has_bang: has_bang,
|
|
3566
|
+
command: command,
|
|
1497
3567
|
line: line,
|
|
1498
3568
|
column: column
|
|
1499
3569
|
}
|
|
1500
3570
|
end
|
|
1501
3571
|
|
|
1502
|
-
def
|
|
1503
|
-
token = advance # Skip '
|
|
3572
|
+
def parse_delete_command
|
|
3573
|
+
token = advance # Skip 'delete'
|
|
1504
3574
|
line = token[:line]
|
|
1505
3575
|
column = token[:column]
|
|
1506
3576
|
|
|
1507
|
-
#
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
# After "autoload" keyword, expect a string path
|
|
1517
|
-
if current_token && current_token[:type] == :string
|
|
1518
|
-
path = current_token[:value]
|
|
3577
|
+
# Check for register argument (could be an underscore '_')
|
|
3578
|
+
register = nil
|
|
3579
|
+
if current_token
|
|
3580
|
+
if current_token[:type] == :identifier && current_token[:value] == '_'
|
|
3581
|
+
register = current_token[:value]
|
|
3582
|
+
advance
|
|
3583
|
+
elsif current_token[:type] == :operator && current_token[:value] == '_'
|
|
3584
|
+
# Handle underscore as an operator (some lexers might classify it this way)
|
|
3585
|
+
register = current_token[:value]
|
|
1519
3586
|
advance
|
|
1520
|
-
else
|
|
1521
|
-
@errors << {
|
|
1522
|
-
message: "Expected string path after 'autoload'",
|
|
1523
|
-
position: @position,
|
|
1524
|
-
line: current_token ? current_token[:line] : 0,
|
|
1525
|
-
column: current_token ? current_token[:column] : 0
|
|
1526
|
-
}
|
|
1527
3587
|
end
|
|
1528
|
-
|
|
1529
|
-
# Regular import with a string path
|
|
1530
|
-
if current_token && current_token[:type] == :string
|
|
1531
|
-
path = current_token[:value]
|
|
3588
|
+
end
|
|
1532
3589
|
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
3590
|
+
return {
|
|
3591
|
+
type: :delete_command,
|
|
3592
|
+
register: register,
|
|
3593
|
+
line: line,
|
|
3594
|
+
column: column
|
|
3595
|
+
}
|
|
3596
|
+
end
|
|
1536
3597
|
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
3598
|
+
def parse_brace_sequence_value
|
|
3599
|
+
# Handle special brace sequences like {{{,}}} for foldmarker
|
|
3600
|
+
value_parts = []
|
|
3601
|
+
|
|
3602
|
+
while current_token && (current_token[:type] == :brace_open ||
|
|
3603
|
+
current_token[:type] == :brace_close ||
|
|
3604
|
+
current_token[:type] == :comma)
|
|
3605
|
+
value_parts << current_token[:value]
|
|
3606
|
+
advance
|
|
3607
|
+
end
|
|
3608
|
+
|
|
3609
|
+
return {
|
|
3610
|
+
type: :brace_sequence,
|
|
3611
|
+
value: value_parts.join(''),
|
|
3612
|
+
line: current_token ? current_token[:line] : 0,
|
|
3613
|
+
column: current_token ? current_token[:column] : 0
|
|
3614
|
+
}
|
|
3615
|
+
end
|
|
3616
|
+
|
|
3617
|
+
def parse_comma_separated_value
|
|
3618
|
+
# Handle comma-separated option values like menu,menuone,noinsert,noselect
|
|
3619
|
+
values = []
|
|
3620
|
+
|
|
3621
|
+
# Parse first value
|
|
3622
|
+
if current_token && current_token[:type] == :identifier
|
|
3623
|
+
values << current_token[:value]
|
|
3624
|
+
advance
|
|
3625
|
+
else
|
|
3626
|
+
return parse_expression # Fall back to regular expression parsing
|
|
1546
3627
|
end
|
|
1547
3628
|
|
|
1548
|
-
#
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
advance # Skip 'as'
|
|
3629
|
+
# Parse additional comma-separated values
|
|
3630
|
+
while current_token && current_token[:type] == :comma
|
|
3631
|
+
advance # Skip comma
|
|
1552
3632
|
if current_token && current_token[:type] == :identifier
|
|
1553
|
-
|
|
3633
|
+
values << current_token[:value]
|
|
3634
|
+
advance
|
|
1554
3635
|
else
|
|
1555
|
-
|
|
1556
|
-
message: "Expected identifier after 'as'",
|
|
1557
|
-
position: @position,
|
|
1558
|
-
line: current_token ? current_token[:line] : 0,
|
|
1559
|
-
column: current_token ? current_token[:column] : 0
|
|
1560
|
-
}
|
|
3636
|
+
break
|
|
1561
3637
|
end
|
|
1562
3638
|
end
|
|
1563
3639
|
|
|
1564
|
-
{
|
|
1565
|
-
type: :
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
as_name: as_name,
|
|
1570
|
-
line: line,
|
|
1571
|
-
column: column
|
|
3640
|
+
return {
|
|
3641
|
+
type: :comma_separated_value,
|
|
3642
|
+
values: values,
|
|
3643
|
+
line: current_token ? current_token[:line] : 0,
|
|
3644
|
+
column: current_token ? current_token[:column] : 0
|
|
1572
3645
|
}
|
|
1573
3646
|
end
|
|
1574
|
-
|
|
1575
|
-
|
|
3647
|
+
|
|
3648
|
+
def parse_set_command
|
|
3649
|
+
token = advance # Skip 'set'
|
|
1576
3650
|
line = token[:line]
|
|
1577
3651
|
column = token[:column]
|
|
1578
3652
|
|
|
1579
|
-
#
|
|
1580
|
-
|
|
3653
|
+
# Parse option name
|
|
3654
|
+
option_name = nil
|
|
3655
|
+
if current_token && current_token[:type] == :identifier
|
|
3656
|
+
option_name = current_token[:value]
|
|
3657
|
+
advance
|
|
3658
|
+
elsif current_token && current_token[:type] == :option_variable
|
|
3659
|
+
# Handle option variable directly (like &option)
|
|
3660
|
+
option_name = current_token[:value].sub(/^&/, '')
|
|
3661
|
+
advance
|
|
3662
|
+
else
|
|
1581
3663
|
@errors << {
|
|
1582
|
-
message: "Expected
|
|
3664
|
+
message: "Expected option name after 'set'",
|
|
1583
3665
|
position: @position,
|
|
3666
|
+
line: current_token ? current_token[:line] : line,
|
|
3667
|
+
column: current_token ? current_token[:column] : column + 3
|
|
3668
|
+
}
|
|
3669
|
+
return {
|
|
3670
|
+
type: :set_command,
|
|
3671
|
+
option: nil,
|
|
3672
|
+
value: nil,
|
|
3673
|
+
reset_to_vim_default: false,
|
|
1584
3674
|
line: line,
|
|
1585
3675
|
column: column
|
|
1586
3676
|
}
|
|
1587
|
-
return nil
|
|
1588
3677
|
end
|
|
1589
3678
|
|
|
1590
|
-
|
|
3679
|
+
# Check for &vim suffix (reset to Vim default)
|
|
3680
|
+
reset_to_vim_default = false
|
|
3681
|
+
if current_token && current_token[:type] == :option_variable &&
|
|
3682
|
+
(current_token[:value] == '&vim' || current_token[:value] == '&')
|
|
3683
|
+
reset_to_vim_default = true
|
|
3684
|
+
advance
|
|
3685
|
+
end
|
|
1591
3686
|
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
position: @position,
|
|
1605
|
-
line: current_token[:line],
|
|
1606
|
-
column: current_token[:column]
|
|
1607
|
-
}
|
|
1608
|
-
advance
|
|
1609
|
-
return nil
|
|
3687
|
+
# Check for equals sign and value
|
|
3688
|
+
value = nil
|
|
3689
|
+
if current_token && current_token[:type] == :operator && current_token[:value] == '='
|
|
3690
|
+
advance # Skip '='
|
|
3691
|
+
|
|
3692
|
+
# Special handling for foldmarker and similar options that use brace notation
|
|
3693
|
+
if option_name == 'foldmarker' && current_token && current_token[:type] == :brace_open
|
|
3694
|
+
# Parse as special brace sequence value (e.g., {{{,}}})
|
|
3695
|
+
value = parse_brace_sequence_value
|
|
3696
|
+
# Special handling for comma-separated option values
|
|
3697
|
+
elsif ['completeopt', 'formatoptions', 'guioptions', 'wildoptions'].include?(option_name)
|
|
3698
|
+
value = parse_comma_separated_value
|
|
1610
3699
|
else
|
|
1611
|
-
|
|
1612
|
-
message: "Unexpected keyword after export: #{current_token[:value]}",
|
|
1613
|
-
position: @position,
|
|
1614
|
-
line: current_token[:line],
|
|
1615
|
-
column: current_token[:column]
|
|
1616
|
-
}
|
|
1617
|
-
advance
|
|
1618
|
-
return nil
|
|
3700
|
+
value = parse_expression
|
|
1619
3701
|
end
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
|
|
3702
|
+
elsif current_token && current_token[:type] == :operator && current_token[:value] == '-'
|
|
3703
|
+
# Handle syntaxes like 'set option-=value'
|
|
3704
|
+
op = current_token[:value]
|
|
3705
|
+
advance # Skip the operator
|
|
3706
|
+
|
|
3707
|
+
if current_token && current_token[:type] == :operator &&
|
|
3708
|
+
['+', '=', '^'].include?(current_token[:value])
|
|
3709
|
+
op += current_token[:value]
|
|
3710
|
+
advance # Skip the second part of the operator
|
|
3711
|
+
end
|
|
3712
|
+
|
|
3713
|
+
value = {
|
|
3714
|
+
type: :option_operation,
|
|
3715
|
+
operator: op,
|
|
3716
|
+
value: parse_expression,
|
|
3717
|
+
line: line,
|
|
3718
|
+
column: column
|
|
3719
|
+
}
|
|
3720
|
+
elsif current_token && current_token[:type] == :operator && current_token[:value] == '+'
|
|
3721
|
+
# Handle syntaxes like 'set option+=value'
|
|
3722
|
+
op = current_token[:value]
|
|
3723
|
+
advance # Skip the operator
|
|
3724
|
+
|
|
3725
|
+
if current_token && current_token[:type] == :operator && current_token[:value] == '='
|
|
3726
|
+
op += current_token[:value]
|
|
3727
|
+
advance # Skip the '=' part
|
|
3728
|
+
end
|
|
3729
|
+
|
|
3730
|
+
value = {
|
|
3731
|
+
type: :option_operation,
|
|
3732
|
+
operator: op,
|
|
3733
|
+
value: parse_expression,
|
|
3734
|
+
line: line,
|
|
3735
|
+
column: column
|
|
1626
3736
|
}
|
|
1627
|
-
advance
|
|
1628
|
-
return nil
|
|
1629
3737
|
end
|
|
1630
3738
|
|
|
1631
|
-
{
|
|
1632
|
-
type: :
|
|
1633
|
-
|
|
3739
|
+
return {
|
|
3740
|
+
type: :set_command,
|
|
3741
|
+
option: option_name,
|
|
3742
|
+
value: value,
|
|
3743
|
+
reset_to_vim_default: reset_to_vim_default,
|
|
1634
3744
|
line: line,
|
|
1635
3745
|
column: column
|
|
1636
3746
|
}
|
|
1637
3747
|
end
|
|
1638
3748
|
|
|
1639
|
-
def
|
|
1640
|
-
token = advance # Skip '
|
|
3749
|
+
def parse_syntax_command
|
|
3750
|
+
token = advance # Skip 'syntax'
|
|
1641
3751
|
line = token[:line]
|
|
1642
3752
|
column = token[:column]
|
|
1643
3753
|
|
|
1644
|
-
#
|
|
1645
|
-
|
|
1646
|
-
if current_token && current_token[:type] == :
|
|
1647
|
-
|
|
1648
|
-
advance
|
|
3754
|
+
# Parse syntax subcommand (match, region, keyword, etc.)
|
|
3755
|
+
subcommand = nil
|
|
3756
|
+
if current_token && current_token[:type] == :identifier
|
|
3757
|
+
subcommand = current_token[:value]
|
|
3758
|
+
advance
|
|
1649
3759
|
end
|
|
1650
3760
|
|
|
1651
|
-
#
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
if current_token && current_token[:type] == :script_local
|
|
1657
|
-
is_script_local = true
|
|
1658
|
-
function_scope = current_token[:value]
|
|
1659
|
-
advance # Skip s: prefix
|
|
1660
|
-
elsif current_token && current_token[:type] == :identifier && current_token[:value].include?(':')
|
|
1661
|
-
# Handle other scoped functions like g: or b:
|
|
1662
|
-
parts = current_token[:value].split(':')
|
|
1663
|
-
if parts.length == 2
|
|
1664
|
-
function_scope = parts[0] + ':'
|
|
1665
|
-
advance
|
|
1666
|
-
end
|
|
3761
|
+
# Parse syntax group name
|
|
3762
|
+
group_name = nil
|
|
3763
|
+
if current_token && current_token[:type] == :identifier
|
|
3764
|
+
group_name = current_token[:value]
|
|
3765
|
+
advance
|
|
1667
3766
|
end
|
|
1668
|
-
|
|
1669
|
-
#
|
|
1670
|
-
|
|
1671
|
-
if
|
|
1672
|
-
|
|
3767
|
+
|
|
3768
|
+
# Parse pattern (regex or other pattern)
|
|
3769
|
+
pattern = nil
|
|
3770
|
+
if current_token && current_token[:type] == :regex
|
|
3771
|
+
pattern = current_token[:value]
|
|
1673
3772
|
advance
|
|
1674
|
-
|
|
3773
|
+
end
|
|
3774
|
+
|
|
3775
|
+
# Parse any remaining arguments as raw text until end of line
|
|
3776
|
+
args = []
|
|
3777
|
+
while current_token && current_token[:type] != :comment
|
|
3778
|
+
args << current_token[:value]
|
|
3779
|
+
advance
|
|
3780
|
+
end
|
|
3781
|
+
|
|
3782
|
+
{
|
|
3783
|
+
type: :syntax_command,
|
|
3784
|
+
subcommand: subcommand,
|
|
3785
|
+
group_name: group_name,
|
|
3786
|
+
pattern: pattern,
|
|
3787
|
+
args: args.join(' ').strip,
|
|
3788
|
+
line: line,
|
|
3789
|
+
column: column
|
|
3790
|
+
}
|
|
3791
|
+
end
|
|
3792
|
+
|
|
3793
|
+
def parse_highlight_command
|
|
3794
|
+
token = advance # Skip 'highlight'
|
|
3795
|
+
line = token[:line]
|
|
3796
|
+
column = token[:column]
|
|
3797
|
+
|
|
3798
|
+
# Parse highlight group name
|
|
3799
|
+
group_name = nil
|
|
3800
|
+
if current_token && current_token[:type] == :identifier
|
|
3801
|
+
group_name = current_token[:value]
|
|
3802
|
+
advance
|
|
3803
|
+
end
|
|
3804
|
+
|
|
3805
|
+
# Parse highlight attributes as key=value pairs
|
|
3806
|
+
attributes = {}
|
|
3807
|
+
while current_token && current_token[:type] != :comment
|
|
3808
|
+
# Parse attribute name
|
|
1675
3809
|
if current_token && current_token[:type] == :identifier
|
|
1676
|
-
|
|
3810
|
+
attr_name = current_token[:value]
|
|
1677
3811
|
advance
|
|
1678
|
-
end
|
|
1679
|
-
else
|
|
1680
|
-
name = expect(:identifier)
|
|
1681
|
-
end
|
|
1682
3812
|
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
expect(:paren_close)
|
|
3813
|
+
# Parse = sign
|
|
3814
|
+
if current_token && current_token[:type] == :operator && current_token[:value] == '='
|
|
3815
|
+
advance
|
|
1687
3816
|
|
|
1688
|
-
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
|
|
3817
|
+
# Parse attribute value (can be identifier, number, or hex color)
|
|
3818
|
+
if current_token && (current_token[:type] == :identifier ||
|
|
3819
|
+
current_token[:type] == :number ||
|
|
3820
|
+
current_token[:type] == :hex_color)
|
|
3821
|
+
attributes[attr_name] = current_token[:value]
|
|
3822
|
+
advance
|
|
3823
|
+
end
|
|
3824
|
+
end
|
|
1696
3825
|
else
|
|
1697
|
-
|
|
3826
|
+
advance
|
|
1698
3827
|
end
|
|
1699
3828
|
end
|
|
1700
3829
|
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
|
|
1704
|
-
|
|
1705
|
-
|
|
1706
|
-
|
|
1707
|
-
|
|
3830
|
+
{
|
|
3831
|
+
type: :highlight_command,
|
|
3832
|
+
group_name: group_name,
|
|
3833
|
+
attributes: attributes,
|
|
3834
|
+
line: line,
|
|
3835
|
+
column: column
|
|
3836
|
+
}
|
|
3837
|
+
end
|
|
1708
3838
|
|
|
1709
|
-
|
|
1710
|
-
|
|
1711
|
-
|
|
3839
|
+
def parse_sleep_command
|
|
3840
|
+
token = advance # Skip 'sleep'
|
|
3841
|
+
line = token[:line]
|
|
3842
|
+
column = token[:column]
|
|
1712
3843
|
|
|
1713
|
-
#
|
|
1714
|
-
|
|
1715
|
-
if
|
|
1716
|
-
|
|
3844
|
+
# Parse duration (number with optional suffix)
|
|
3845
|
+
duration = nil
|
|
3846
|
+
if current_token && current_token[:type] == :number
|
|
3847
|
+
duration = current_token[:value]
|
|
3848
|
+
advance
|
|
3849
|
+
else
|
|
1717
3850
|
@errors << {
|
|
1718
|
-
message: "Expected
|
|
3851
|
+
message: "Expected duration after 'sleep'",
|
|
1719
3852
|
position: @position,
|
|
1720
|
-
line:
|
|
1721
|
-
column:
|
|
3853
|
+
line: current_token ? current_token[:line] : line,
|
|
3854
|
+
column: current_token ? current_token[:column] : column + 5
|
|
1722
3855
|
}
|
|
1723
3856
|
end
|
|
1724
3857
|
|
|
1725
|
-
function_name = name ? name[:value] : nil
|
|
1726
|
-
if function_scope
|
|
1727
|
-
function_name = function_scope + function_name if function_name
|
|
1728
|
-
end
|
|
1729
|
-
|
|
1730
3858
|
{
|
|
1731
|
-
type: :
|
|
1732
|
-
|
|
1733
|
-
is_script_local: is_script_local,
|
|
1734
|
-
scope: function_scope,
|
|
1735
|
-
params: params,
|
|
1736
|
-
has_bang: has_bang,
|
|
1737
|
-
attributes: attributes,
|
|
1738
|
-
body: body,
|
|
3859
|
+
type: :sleep_command,
|
|
3860
|
+
duration: duration,
|
|
1739
3861
|
line: line,
|
|
1740
3862
|
column: column
|
|
1741
3863
|
}
|
|
1742
3864
|
end
|
|
1743
3865
|
|
|
1744
|
-
|
|
1745
|
-
|
|
1746
|
-
|
|
3866
|
+
def parse_source_command
|
|
3867
|
+
token = advance # Skip 'source'
|
|
3868
|
+
line = token[:line]
|
|
3869
|
+
column = token[:column]
|
|
1747
3870
|
|
|
1748
|
-
#
|
|
1749
|
-
|
|
1750
|
-
|
|
3871
|
+
# Parse file path - collect all tokens until end of line/comment
|
|
3872
|
+
path_parts = []
|
|
3873
|
+
while current_token && current_token[:type] != :comment
|
|
3874
|
+
path_parts << current_token[:value]
|
|
3875
|
+
advance
|
|
1751
3876
|
end
|
|
1752
3877
|
|
|
1753
|
-
|
|
1754
|
-
# Check for ... (varargs) in legacy function
|
|
1755
|
-
if current_token && current_token[:type] == :ellipsis
|
|
1756
|
-
advance
|
|
1757
|
-
params << { type: :var_args_legacy, name: '...' }
|
|
1758
|
-
break
|
|
1759
|
-
end
|
|
1760
|
-
|
|
1761
|
-
if current_token && current_token[:type] == :identifier
|
|
1762
|
-
# Regular parameter
|
|
1763
|
-
param_name = advance
|
|
1764
|
-
params << {
|
|
1765
|
-
type: :parameter,
|
|
1766
|
-
name: param_name[:value],
|
|
1767
|
-
line: param_name[:line],
|
|
1768
|
-
column: param_name[:column]
|
|
1769
|
-
}
|
|
1770
|
-
elsif current_token && current_token[:type] == :arg_variable
|
|
1771
|
-
# Parameter with a: prefix
|
|
1772
|
-
param_name = advance
|
|
1773
|
-
# Extract name without 'a:' prefix
|
|
1774
|
-
name_without_prefix = param_name[:value].sub(/^a:/, '')
|
|
1775
|
-
params << {
|
|
1776
|
-
type: :parameter,
|
|
1777
|
-
name: name_without_prefix,
|
|
1778
|
-
line: param_name[:line],
|
|
1779
|
-
column: param_name[:column],
|
|
1780
|
-
is_arg_prefixed: true
|
|
1781
|
-
}
|
|
1782
|
-
else
|
|
1783
|
-
@errors << {
|
|
1784
|
-
message: "Expected parameter name",
|
|
1785
|
-
position: @position,
|
|
1786
|
-
line: current_token ? current_token[:line] : 0,
|
|
1787
|
-
column: current_token ? current_token[:column] : 0
|
|
1788
|
-
}
|
|
1789
|
-
break
|
|
1790
|
-
end
|
|
1791
|
-
|
|
1792
|
-
if current_token && current_token[:type] == :comma
|
|
1793
|
-
advance
|
|
1794
|
-
else
|
|
1795
|
-
break
|
|
1796
|
-
end
|
|
1797
|
-
end
|
|
3878
|
+
file_path = path_parts.join('')
|
|
1798
3879
|
|
|
1799
|
-
|
|
3880
|
+
{
|
|
3881
|
+
type: :source_command,
|
|
3882
|
+
file_path: file_path,
|
|
3883
|
+
line: line,
|
|
3884
|
+
column: column
|
|
3885
|
+
}
|
|
1800
3886
|
end
|
|
1801
|
-
|
|
1802
3887
|
end
|
|
1803
3888
|
end
|