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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 7a7797098bc2eb7d9b046c4aa38d9d485fbee3016a709bda2f29ad0664b9d0de
4
- data.tar.gz: 807321a96f56c648922fc2b5923d8ccdfea6f6d8c8faa5fc3b844ddd4b6840ac
3
+ metadata.gz: 319f22f198207f7d9d3b9e58c85327b20f7c873e905bc16a38acd6f9199d8b56
4
+ data.tar.gz: c5eeb2b1dd21094be8a901e570540b89a29619273b0c5fcb11b7edc278da9975
5
5
  SHA512:
6
- metadata.gz: 4eb35205f6802d60d1c3070d7e8ff705d556367b4afc9c969cdfad3e560af3be646dcf13b4f7d59eaba0b0ed6cc61b130ce4cd952a529b5a4d5adfcae8339d6b
7
- data.tar.gz: '09ace9f8871da9f426f7efcf77e37962b466dcafbbc053725b2ffda42d51d47bd9c98a57dcaa191d51f7dface73ee2a8885fff5f30619c8c67636ff1e5218113'
6
+ metadata.gz: b1556c23fd42b7b7d9d3163a97ecd353902f12341c203a913a4dffec05cbd6ffe867815f223dbfe8f169fd99c514b8b39ac1fab87fd26f195d0622fff9e62966
7
+ data.tar.gz: 2204f688018212f0b5ec6b13a0906da8a24078fe874dc10c2b9b326668627fb985fdcc11b9682bdbb67f2a452838b8088286260de54bf8941b6501180a7a8704
data/README.md CHANGED
@@ -2,13 +2,6 @@
2
2
 
3
3
  A Ruby gem that provides linting capabilities for Vim9 script files. This linter helps identify syntax errors and enforce best practices for for Vim9 script.
4
4
 
5
- ## Features
6
-
7
- - Lexical analysis of Vim9 script syntax
8
- - Parsing of Vim9 script constructs
9
- - Detection of common errors and code smells
10
- - Command-line interface for easy integration with editors
11
-
12
5
  ## Installation
13
6
 
14
7
  Install the gem:
@@ -17,11 +10,22 @@ Install the gem:
17
10
  gem install vinter
18
11
  ```
19
12
 
13
+ ## Configure
14
+ Vinter will read config files on the following priority order
15
+ - User config (`~/.vinter`)
16
+ - Project config (`path/to/proj/.vinter`)
17
+
18
+ ```yaml
19
+ ignore_rules:
20
+ - missing-vim9script-declaration
21
+ - prefer-def-over-function
22
+ ```
23
+
20
24
  ## Usage
21
25
 
22
26
  ### Command Line
23
27
 
24
- Lint a Vim9 script file:
28
+ Updated vim linter for legacy and vim9script
25
29
 
26
30
  ```bash
27
31
  vinter path/to/your/script.vim
@@ -88,4 +92,4 @@ issues = linter.lint(content)
88
92
  2. Create a feature branch: `git checkout -b my-new-feature`
89
93
  3. Commit your changes: `git commit -am 'Add some feature'`
90
94
  4. Push to the branch: `git push origin my-new-feature`
91
- 5. Submit a pull request
95
+ 5. Submit a pull request
data/bin/vinter CHANGED
File without changes
@@ -0,0 +1,14 @@
1
+ module Vinter
2
+ module ASTPrinter
3
+ def self.print(ast, indent=0)
4
+ spaces = " " * indent * 2
5
+ ast[:body].each do |node|
6
+ puts "#{spaces}(#{node[:type]}|#{node[:value]})"
7
+ if node[:type] == :export_statement
8
+ puts "(#{node[:export][:type]})"
9
+ print(node[:export], indent=1)
10
+ end
11
+ end
12
+ end
13
+ end
14
+ end
data/lib/vinter/cli.rb CHANGED
@@ -6,42 +6,70 @@ module Vinter
6
6
 
7
7
  def run(args)
8
8
  if args.empty?
9
- puts "Usage: vim9-lint [file.vim]"
9
+ puts "Usage: vinter [file.vim|directory] [--print-ast]"
10
10
  return 1
11
11
  end
12
12
 
13
- file_path = args[0]
13
+ target_path = args[0]
14
14
 
15
- unless File.exist?(file_path)
16
- puts "Error: File not found: #{file_path}"
15
+ unless File.exist?(target_path)
16
+ puts "Error: File or directory not found: #{target_path}"
17
17
  return 1
18
18
  end
19
19
 
20
- content = File.read(file_path)
21
- issues = @linter.lint(content)
20
+ vim_files = if File.directory?(target_path)
21
+ find_vim_files(target_path)
22
+ else
23
+ [target_path]
24
+ end
22
25
 
23
- if issues.empty?
24
- puts "No issues found in #{file_path}"
26
+ if vim_files.empty?
27
+ puts "No .vim files found in #{target_path}"
25
28
  return 0
26
- else
27
- puts "Found #{issues.length} issues in #{file_path}:"
29
+ end
30
+
31
+ total_issues = 0
32
+ error_count = 0
33
+
34
+ vim_files.each do |file_path|
35
+ content = File.read(file_path)
36
+ issues = @linter.lint(content)
37
+ total_issues += issues.length
38
+
39
+ if issues.empty?
40
+ puts "No issues found in #{file_path}" if vim_files.length == 1
41
+ else
42
+ puts "Found #{issues.length} issues in #{file_path}:" if vim_files.length > 1
43
+
44
+ issues.each do |issue|
45
+ type_str = case issue[:type]
46
+ when :error then "ERROR"
47
+ when :warning then "WARNING"
48
+ when :rule then "RULE(#{issue[:rule]})"
49
+ else "UNKNOWN"
50
+ end
28
51
 
29
- issues.each do |issue|
30
- type_str = case issue[:type]
31
- when :error then "ERROR"
32
- when :warning then "WARNING"
33
- when :rule then "RULE(#{issue[:rule]})"
34
- else "UNKNOWN"
35
- end
52
+ line = issue[:line] || 1
53
+ column = issue[:column] || 1
36
54
 
37
- line = issue[:line] || 1
38
- column = issue[:column] || 1
55
+ puts "#{file_path}:#{line}:#{column}: #{type_str}: #{issue[:message]}"
56
+ end
39
57
 
40
- puts "#{file_path}:#{line}:#{column}: #{type_str}: #{issue[:message]}"
58
+ error_count += 1 if issues.any? { |i| i[:type] == :error }
41
59
  end
60
+ end
42
61
 
43
- return issues.any? { |i| i[:type] == :error } ? 1 : 0
62
+ if vim_files.length > 1
63
+ puts "\nProcessed #{vim_files.length} files, found #{total_issues} total issues"
44
64
  end
65
+
66
+ return error_count > 0 ? 1 : 0
67
+ end
68
+
69
+ private
70
+
71
+ def find_vim_files(directory)
72
+ Dir.glob(File.join(directory, "**", "*.vim")).sort
45
73
  end
46
74
  end
47
75
  end
data/lib/vinter/lexer.rb CHANGED
@@ -2,17 +2,20 @@ module Vinter
2
2
  class Lexer
3
3
  TOKEN_TYPES = {
4
4
  # Vim9 specific keywords
5
- keyword: /\b(if|else|elseif|endif|while|endwhile|for|endfor|def|enddef|function|endfunction|return|const|var|final|import|export|class|extends|static|enum|type|vim9script|abort|autocmd|echohl|echomsg|let|execute)\b/,
5
+ keyword: /\b(if|else|elseif|endif|while|endwhile|for|endfor|def|enddef|function|endfunction|endfunc|return|const|var|final|import|export|class|extends|static|enum|type|vim9script|scriptencoding|abort|autocmd|echom|echoerr|echohl|echomsg|let|unlet|execute|exec|continue|break|try|catch|finally|endtry|throw|runtime|silent|delete|command|call|set|setlocal|syntax|highlight|sleep|source|nnoremap|nmap|inoremap|imap|vnoremap|vmap|xnoremap|xmap|cnoremap|cmap|noremap|map|var)\b/,
6
+ encodings: /\b(latin1|iso|koi8|macroman|cp437|cp737|cp775|cp850|cp852|cp855|cp857|cp860|cp861|cp862|cp863|cp865|cp866|cp869|cp874|cp1250|cp1251|cp1253|cp1254|cp1255|cp1256|cp1257|cp1258|cp932|euc\-jp|sjis|cp949|euc\-kr|cp936|euc\-cn|cp950|big5|euc\-tw|utf\-8|ucs\-2|ucs\-21e|utf\-16|utf\-16le|ucs\-4|ucs\-4le|ansi|japan|korea|prc|chinese|taiwan|utf8|unicode|ucs2be|ucs\-2be|ucs\-4be|utf\-32|utf\-32le|default)\b/,
7
+ vimfuncs: /\b(win_execute|win_findbuf|win_getid|win_gettype|win_gotoid|win_id2tabwin|win_id2win|win_move_separator|win_move_statusline|win_screenpos|win_splitmove)\b/,
6
8
  # Identifiers can include # and special characters
7
9
  identifier: /\b[a-zA-Z_][a-zA-Z0-9_#]*\b/,
8
10
  # Single-character operators
9
- operator: /[\+\-\*\/=<>!&\|\.]/,
11
+ operator: /[\+\-\*\/=%<>!&\|\.]/,
10
12
  # Multi-character operators handled separately
11
- number: /\b\d+(\.\d+)?\b/,
13
+ number: /\b(0[xX][0-9A-Fa-f]+|0[oO][0-7]+|0[bB][01]+|\d+(\.\d+)?([eE][+-]?\d+)?[smh]?)\b/,
12
14
  # Handle both single and double quoted strings
13
- string: /"([^"\\]|\\.)*"|'([^'\\]|\\.)*'/,
15
+ # string: /"(\\"|[^"])*"|'(\\'|[^'])*'/,
16
+ register_access: /@[a-zA-Z0-9":.%#=*+~_\/\-]/,
14
17
  # Vim9 comments use #
15
- comment: /#.*/,
18
+ comment: /(#|").*/,
16
19
  whitespace: /\s+/,
17
20
  brace_open: /\{/,
18
21
  brace_close: /\}/,
@@ -24,9 +27,11 @@ module Vinter
24
27
  semicolon: /;/,
25
28
  comma: /,/,
26
29
  backslash: /\\/,
30
+ question_mark: /\?/,
31
+ command_separator: /\|/
27
32
  }
28
33
 
29
- CONTINUATION_OPERATORS = %w(. .. + - * / = == != > < >= <= && || ? : -> =>)
34
+ CONTINUATION_OPERATORS = %w(. .. + - * / = == ==# ==? != > < >= <= && || ? : -> =>)
30
35
  def initialize(input)
31
36
  @input = input
32
37
  @tokens = []
@@ -35,10 +40,220 @@ module Vinter
35
40
  @column = 1
36
41
  end
37
42
 
43
+ def should_parse_as_regex
44
+ # Look at recent tokens to determine if we're in a regex context
45
+ recent_tokens = @tokens.last(3)
46
+
47
+ # Check for contexts where regex is expected
48
+ return true if recent_tokens.any? { |t|
49
+ t && t[:type] == :keyword && ['syntax'].include?(t[:value])
50
+ }
51
+
52
+ return true if recent_tokens.any? { |t|
53
+ t && t[:type] == :identifier && ['match', 'region', 'keyword'].include?(t[:value])
54
+ }
55
+
56
+ # Check for comparison operators that often use regex
57
+ return true if recent_tokens.any? { |t|
58
+ t && t[:type] == :operator && ['=~', '!~', '=~#', '!~#', '=~?', '!~?'].include?(t[:value])
59
+ }
60
+
61
+ false
62
+ end
63
+
64
+ def find_unescaped_newline(chunk)
65
+ i = 0
66
+ while i < chunk.length
67
+ if chunk[i] == "\n" && (i == 0 || chunk[i - 1] != '\\')
68
+ return i
69
+ end
70
+ i += 1
71
+ end
72
+ nil # Return nil if no unescaped newline is found
73
+ end
74
+
38
75
  def tokenize
39
76
  until @position >= @input.length
40
77
  chunk = @input[@position..-1]
41
-
78
+
79
+ # First check if the line starts with a quote (comment in Vim)
80
+ # Check if we're at the beginning of a line (optionally after whitespace)
81
+ line_start = @position == 0 || @input[@position - 1] == "\n"
82
+ if !line_start
83
+ # Check if we're after whitespace at the start of a line
84
+ temp_pos = @position - 1
85
+ while temp_pos >= 0 && @input[temp_pos] =~ /[ \t]/
86
+ temp_pos -= 1
87
+ end
88
+ line_start = temp_pos < 0 || @input[temp_pos] == "\n"
89
+ end
90
+
91
+ # If we're at the start of a line and it begins with a quote
92
+ if line_start && chunk.start_with?('"')
93
+ # Find the end of the line
94
+ line_end = find_unescaped_newline(chunk) || chunk.length
95
+ comment_text = chunk[0...line_end]
96
+
97
+ @tokens << {
98
+ type: :comment,
99
+ value: comment_text,
100
+ line: @line_num,
101
+ column: @column
102
+ }
103
+
104
+ @position += comment_text.length
105
+ @column += comment_text.length
106
+ next
107
+ end
108
+
109
+ # --- Interpolated String Handling ---
110
+ if chunk.start_with?("$'")
111
+ i = 2
112
+ string_value = "$'"
113
+ brace_depth = 0
114
+ escaped = false
115
+
116
+ while i < chunk.length
117
+ char = chunk[i]
118
+ string_value += char
119
+
120
+ if char == '\\' && !escaped
121
+ escaped = true
122
+ elsif char == "'" && !escaped && brace_depth == 0
123
+ # End of interpolated string
124
+ i += 1
125
+ break
126
+ elsif char == '{' && !escaped
127
+ brace_depth += 1
128
+ elsif char == '}' && !escaped && brace_depth > 0
129
+ brace_depth -= 1
130
+ elsif escaped
131
+ escaped = false
132
+ end
133
+
134
+ i += 1
135
+ end
136
+
137
+ @tokens << {
138
+ type: :interpolated_string,
139
+ value: string_value,
140
+ line: @line_num,
141
+ column: @column
142
+ }
143
+ @column += string_value.length
144
+ @position += string_value.length
145
+ @line_num += string_value.count("\n")
146
+ next
147
+ end
148
+
149
+ # Handle string literals manually
150
+ if chunk.start_with?("'") || chunk.start_with?('"')
151
+ quote = chunk[0]
152
+ i = 1
153
+ escaped = false
154
+ string_value = quote
155
+
156
+ # Keep going until we find an unescaped closing quote
157
+ while i < chunk.length
158
+ char = chunk[i]
159
+ string_value += char
160
+
161
+ if char == '\\' && !escaped
162
+ escaped = true
163
+ elsif (char == "\n" or char == quote) && !escaped
164
+ i += 1
165
+ break
166
+ elsif escaped
167
+ escaped = false
168
+ end
169
+
170
+ i += 1
171
+ end
172
+
173
+ # Add the string token if we found a closing quote
174
+ if i < chunk.length || (i == chunk.length && chunk[-1] == quote)
175
+ @tokens << {
176
+ type: :string,
177
+ value: string_value,
178
+ line: @line_num,
179
+ column: @column
180
+ }
181
+
182
+ @column += string_value.length
183
+ @position += string_value.length
184
+ @line_num += 1 if string_value.include?("\n")
185
+ next
186
+ end
187
+ end
188
+
189
+ # Add special handling for command options in the tokenize method
190
+ if chunk.start_with?('<q-args>', '<f-args>', '<args>')
191
+ arg_token = chunk.match(/\A(<q-args>|<f-args>|<args>)/)[0]
192
+ @tokens << {
193
+ type: :command_arg_placeholder,
194
+ value: arg_token,
195
+ line: @line_num,
196
+ column: @column
197
+ }
198
+ @column += arg_token.length
199
+ @position += arg_token.length
200
+ next
201
+ end
202
+
203
+ # Special handling for a:000 variable arguments array
204
+ if chunk =~ /\Aa:0+/
205
+ varargs_token = chunk.match(/\Aa:0+/)[0]
206
+ @tokens << {
207
+ type: :arg_variable,
208
+ value: varargs_token,
209
+ line: @line_num,
210
+ column: @column
211
+ }
212
+ @column += varargs_token.length
213
+ @position += varargs_token.length
214
+ next
215
+ end
216
+
217
+ # Also add special handling for 'silent!' keyword
218
+ # Add this after the keyword check in tokenize method
219
+ if chunk.start_with?('silent!')
220
+ @tokens << {
221
+ type: :silent_bang,
222
+ value: 'silent!',
223
+ line: @line_num,
224
+ column: @column
225
+ }
226
+ @column += 7
227
+ @position += 7
228
+ next
229
+ end
230
+
231
+ # Check for keywords first, before other token types
232
+ if match = chunk.match(/\A\b(if|else|elseif|endif|while|endwhile|for|endfor|def|enddef|function|endfunction|endfunc|return|const|var|final|import|export|class|extends|static|enum|type|vim9script|abort|autocmd|echoerr|echohl|echomsg|let|unlet|var|execute|setlocal|syntax|highlight|sleep|source)\b/)
233
+ @tokens << {
234
+ type: :keyword,
235
+ value: match[0],
236
+ line: @line_num,
237
+ column: @column
238
+ }
239
+ @column += match[0].length
240
+ @position += match[0].length
241
+ next
242
+ end
243
+
244
+ # Handle Vim scoped option variables with &l: or &g: prefix
245
+ if match = chunk.match(/\A&[lg]:[a-zA-Z_][a-zA-Z0-9_]*/)
246
+ @tokens << {
247
+ type: :scoped_option_variable,
248
+ value: match[0],
249
+ line: @line_num,
250
+ column: @column
251
+ }
252
+ @column += match[0].length
253
+ @position += match[0].length
254
+ next
255
+ end
256
+
42
257
  # Handle Vim option variables with & prefix
43
258
  if match = chunk.match(/\A&[a-zA-Z_][a-zA-Z0-9_]*/)
44
259
  @tokens << {
@@ -51,7 +266,7 @@ module Vinter
51
266
  @position += match[0].length
52
267
  next
53
268
  end
54
-
269
+
55
270
  # Handle Vim special variables with v: prefix
56
271
  if match = chunk.match(/\Av:[a-zA-Z_][a-zA-Z0-9_]*/)
57
272
  @tokens << {
@@ -64,7 +279,7 @@ module Vinter
64
279
  @position += match[0].length
65
280
  next
66
281
  end
67
-
282
+
68
283
  # Handle script-local identifiers with s: prefix
69
284
  if match = chunk.match(/\As:[a-zA-Z_][a-zA-Z0-9_]*/)
70
285
  @tokens << {
@@ -77,7 +292,47 @@ module Vinter
77
292
  @position += match[0].length
78
293
  next
79
294
  end
80
-
295
+
296
+ # Handle buffer-local identifiers with b: prefix
297
+ if match = chunk.match(/\Ab:[a-zA-Z_][a-zA-Z0-9_]*/)
298
+ @tokens << {
299
+ type: :buffer_local,
300
+ value: match[0],
301
+ line: @line_num,
302
+ column: @column
303
+ }
304
+ @column += match[0].length
305
+ @position += match[0].length
306
+ next
307
+ end
308
+
309
+ # Handle window-local identifiers with w: prefix
310
+ if match = chunk.match(/\Aw:[a-zA-Z_][a-zA-Z0-9_]*/)
311
+ @tokens << {
312
+ type: :window_local,
313
+ value: match[0],
314
+ line: @line_num,
315
+ column: @column
316
+ }
317
+ @column += match[0].length
318
+ @position += match[0].length
319
+ next
320
+ end
321
+
322
+ # Handle tab-local identifiers with t: prefix
323
+ if match = chunk.match(/\At:[a-zA-Z_][a-zA-Z0-9_]*/)
324
+ @tokens << {
325
+ type: :tab_local,
326
+ value: match[0],
327
+ line: @line_num,
328
+ column: @column
329
+ }
330
+ @column += match[0].length
331
+ @position += match[0].length
332
+ next
333
+ end
334
+
335
+
81
336
  # Handle global variables with g: prefix
82
337
  if match = chunk.match(/\Ag:[a-zA-Z_][a-zA-Z0-9_]*/)
83
338
  @tokens << {
@@ -90,9 +345,9 @@ module Vinter
90
345
  @position += match[0].length
91
346
  next
92
347
  end
93
-
348
+
94
349
  # Handle argument variables with a: prefix
95
- if match = chunk.match(/\Aa:[a-zA-Z_][a-zA-Z0-9_]*/)
350
+ if match = chunk.match(/\Aa:[a-zA-Z_][a-zA-Z0-9_]*/) || match = chunk.match(/\Aa:[A-Z0-9]/)
96
351
  @tokens << {
97
352
  type: :arg_variable,
98
353
  value: match[0],
@@ -103,11 +358,11 @@ module Vinter
103
358
  @position += match[0].length
104
359
  next
105
360
  end
106
-
107
- # Handle compound assignment operators
108
- if match = chunk.match(/\A(\+=|-=|\*=|\/=|\.\.=)/)
361
+
362
+ # Handle argument variables with a: prefix
363
+ if match = chunk.match(/\Al:[a-zA-Z_][a-zA-Z0-9_]*/)
109
364
  @tokens << {
110
- type: :compound_operator,
365
+ type: :local_variable,
111
366
  value: match[0],
112
367
  line: @line_num,
113
368
  column: @column
@@ -117,10 +372,23 @@ module Vinter
117
372
  next
118
373
  end
119
374
 
120
- # Handle multi-character operators explicitly
121
- if match = chunk.match(/\A(==|!=|=>|->|\.\.|\|\||&&)/)
375
+ # Add support for standalone namespace prefixes (like g:)
376
+ if match = chunk.match(/\A([sgbwtal]):/)
122
377
  @tokens << {
123
- type: :operator,
378
+ type: :namespace_prefix,
379
+ value: match[0],
380
+ line: @line_num,
381
+ column: @column
382
+ }
383
+ @column += match[0].length
384
+ @position += match[0].length
385
+ next
386
+ end
387
+
388
+ # Handle compound assignment operators
389
+ if match = chunk.match(/\A(\+=|-=|\*=|\/=|\.\.=|\.=)/)
390
+ @tokens << {
391
+ type: :compound_operator,
124
392
  value: match[0],
125
393
  line: @line_num,
126
394
  column: @column
@@ -143,6 +411,98 @@ module Vinter
143
411
  next
144
412
  end
145
413
 
414
+ # Handle multi-character operators explicitly
415
+ if match = chunk.match(/\A(=~#|=~\?|=~|!~#|!~\?|!~|==#|==\?|==|!=#|!=\?|!=|=>\?|=>|>=#|>=\?|>=|<=#|<=\?|<=|->#|->\?|->|\.\.|\|\||&&)/)
416
+ @tokens << {
417
+ type: :operator,
418
+ value: match[0],
419
+ line: @line_num,
420
+ column: @column
421
+ }
422
+ @column += match[0].length
423
+ @position += match[0].length
424
+ next
425
+ end
426
+
427
+ # Handle regex patterns /pattern/ - only in specific contexts
428
+ if chunk.start_with?('/') && should_parse_as_regex
429
+ i = 1
430
+ regex_value = '/'
431
+
432
+ # Keep going until we find the closing slash
433
+ while i < chunk.length
434
+ char = chunk[i]
435
+ regex_value += char
436
+
437
+ if char == '/' && (i == 1 || chunk[i-1] != '\\')
438
+ # Found closing slash
439
+ i += 1
440
+ break
441
+ end
442
+
443
+ i += 1
444
+ end
445
+
446
+ # Add the regex token if we found a closing slash
447
+ if regex_value.end_with?('/')
448
+ @tokens << {
449
+ type: :regex,
450
+ value: regex_value,
451
+ line: @line_num,
452
+ column: @column
453
+ }
454
+ @column += regex_value.length
455
+ @position += regex_value.length
456
+ next
457
+ end
458
+ end
459
+
460
+ # Handle hex colors like #33FF33
461
+ if match = chunk.match(/\A#[0-9A-Fa-f]{6}/)
462
+ @tokens << {
463
+ type: :hex_color,
464
+ value: match[0],
465
+ line: @line_num,
466
+ column: @column
467
+ }
468
+ @column += match[0].length
469
+ @position += match[0].length
470
+ next
471
+ end
472
+
473
+ # Handle register access (@a, @", etc.)
474
+ if chunk =~ /\A@[a-zA-Z0-9":.%#=*+~_\/\-]/
475
+ register_token = chunk.match(/\A@[a-zA-Z0-9":.%#=*+~_\/\-]/)[0]
476
+ @tokens << {
477
+ type: :register_access,
478
+ value: register_token,
479
+ line: @line_num,
480
+ column: @column
481
+ }
482
+ @column += register_token.length
483
+ @position += register_token.length
484
+ next
485
+ end
486
+
487
+ # In the tokenize method, add special handling for common mapping components
488
+ if chunk.start_with?('<CR>', '<Esc>', '<Tab>', '<Space>', '<C-') ||
489
+ (chunk =~ /\A<[A-Za-z0-9\-_]+>/)
490
+ # Extract the special key notation
491
+ match = chunk.match(/\A(<[^>]+>)/)
492
+ if match
493
+ special_key = match[1]
494
+ @tokens << {
495
+ type: :special_key,
496
+ value: special_key,
497
+ line: @line_num,
498
+ column: @column
499
+ }
500
+ @position += special_key.length
501
+ @column += special_key.length
502
+ next
503
+ end
504
+ end
505
+
146
506
  # Skip whitespace but track position
147
507
  if match = chunk.match(/\A(\s+)/)
148
508
  whitespace = match[0]
@@ -161,13 +521,35 @@ module Vinter
161
521
  # Handle backslash for line continuation
162
522
  if chunk.start_with?('\\')
163
523
  @tokens << {
164
- type: :backslash,
524
+ type: :line_continuation,
165
525
  value: '\\',
166
526
  line: @line_num,
167
527
  column: @column
168
528
  }
169
529
  @column += 1
170
530
  @position += 1
531
+
532
+ # If followed by a newline, advance to next line
533
+ if @position < @input.length && @input[@position] == "\n"
534
+ @line_num += 1
535
+ @column = 1
536
+ @position += 1
537
+ end
538
+
539
+ next
540
+ end
541
+
542
+ # Check for special case where 'function' is followed by '('
543
+ # which likely means it's used as a built-in function
544
+ if chunk =~ /\Afunction\s*\(/
545
+ @tokens << {
546
+ type: :identifier, # Treat as identifier, not keyword
547
+ value: 'function',
548
+ line: @line_num,
549
+ column: @column
550
+ }
551
+ @column += 'function'.length
552
+ @position += 'function'.length
171
553
  next
172
554
  end
173
555