antares 0.1.0 → 0.2.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/CHANGELOG.md +8 -0
- data/README.md +206 -82
- data/lib/antares/compatibility.rb +72 -72
- data/lib/antares/grammar.rb +274 -0
- data/lib/antares/highlighter.rb +93 -13
- data/lib/antares/structure.rb +430 -0
- data/lib/antares/tm_language_lexer.rb +193 -0
- data/lib/antares/tm_language_plist.rb +146 -0
- data/lib/antares/version.rb +1 -1
- data/lib/antares.rb +7 -0
- data/sig/antares.rbs +63 -0
- metadata +26 -2
|
@@ -0,0 +1,430 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Antares
|
|
4
|
+
Bracket = Struct.new(:open_line, :open_column, :close_line, :close_column,
|
|
5
|
+
:depth, :kind, keyword_init: true)
|
|
6
|
+
Region = Struct.new(:start_line, :end_line, :kind, :label,
|
|
7
|
+
:start_column, :end_column, keyword_init: true)
|
|
8
|
+
|
|
9
|
+
class Structure
|
|
10
|
+
OPEN = {"(" => ")", "[" => "]", "{" => "}"}.freeze
|
|
11
|
+
CLOSE = OPEN.invert.freeze
|
|
12
|
+
REGION_MARKER = /\A\s*(?:\#|\/\/|\/\*+|<!--)\s*\#?\s*(end)?region\b/i
|
|
13
|
+
PROVIDER_METHODS = %i[fold_regions brackets bracket_at context_at selection_ranges edit].freeze
|
|
14
|
+
Line = Struct.new(:text, :tokens, :brackets, :indent, :blank,
|
|
15
|
+
:comment, :marker, :label, keyword_init: true)
|
|
16
|
+
|
|
17
|
+
@providers = {}
|
|
18
|
+
@providers_lock = Mutex.new
|
|
19
|
+
|
|
20
|
+
class << self
|
|
21
|
+
def register(language, provider)
|
|
22
|
+
raise ArgumentError, "provider must respond to new" unless provider.respond_to?(:new)
|
|
23
|
+
|
|
24
|
+
@providers_lock.synchronize { @providers[language_key(language)] = provider }
|
|
25
|
+
provider
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def unregister(language)
|
|
29
|
+
@providers_lock.synchronize { @providers.delete(language_key(language)) }
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def build(language:, **arguments)
|
|
33
|
+
provider = language && @providers_lock.synchronize { @providers[language_key(language)] }
|
|
34
|
+
structure = (provider || self).new(**arguments)
|
|
35
|
+
missing = PROVIDER_METHODS.reject { |method| structure.respond_to?(method) }
|
|
36
|
+
raise Error, "structure provider is missing #{missing.join(', ')}" unless missing.empty?
|
|
37
|
+
|
|
38
|
+
structure
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
private
|
|
42
|
+
|
|
43
|
+
def language_key(language)
|
|
44
|
+
raise ArgumentError, "language must be a String or Symbol" unless language.is_a?(String) || language.is_a?(Symbol)
|
|
45
|
+
|
|
46
|
+
value = language.to_s.downcase
|
|
47
|
+
raise ArgumentError, "language must be nonempty" if value.empty? || value.include?("\0")
|
|
48
|
+
|
|
49
|
+
value.freeze
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def initialize(lines:, line_count:, tokens_for:, tokens_in:, stabilize: nil)
|
|
54
|
+
@lines = lines
|
|
55
|
+
@line_count = line_count
|
|
56
|
+
@tokens_for = tokens_for
|
|
57
|
+
@tokens_in = tokens_in
|
|
58
|
+
@stabilize = stabilize
|
|
59
|
+
@line_data = []
|
|
60
|
+
@dirty_from = 0
|
|
61
|
+
@brackets_dirty = true
|
|
62
|
+
@folds_dirty = true
|
|
63
|
+
@fold_brackets_dirty = true
|
|
64
|
+
@derived_dirty = true
|
|
65
|
+
@derived_regions = [].freeze
|
|
66
|
+
@fold_start_lines = {}.freeze
|
|
67
|
+
@pending_old_lines = nil
|
|
68
|
+
@token_kinds = {}
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def fold_regions(range = nil)
|
|
72
|
+
refresh(folds: true)
|
|
73
|
+
select_range(@fold_regions, range) { |region| [region.start_line, region.end_line] }
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def brackets(range = nil)
|
|
77
|
+
refresh(brackets: true)
|
|
78
|
+
select_range(@brackets, range) { |bracket| [bracket.open_line, bracket.close_line] }
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def bracket_at(line, column)
|
|
82
|
+
validate_position(line, column, brackets: true)
|
|
83
|
+
@bracket_positions[[line, column]]
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def context_at(line)
|
|
87
|
+
validate_line(line)
|
|
88
|
+
refresh(folds: true)
|
|
89
|
+
@fold_regions.select { |region| region.start_line <= line && line <= region.end_line }
|
|
90
|
+
.sort_by { |region| [region.start_line, -region.end_line] }
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def selection_ranges(line, column)
|
|
94
|
+
validate_position(line, column, brackets: true, folds: true)
|
|
95
|
+
ranges = []
|
|
96
|
+
token = selection_spans(@line_data.fetch(line)).find { |span| span[0] <= column && column < span[1] }
|
|
97
|
+
ranges << selection_region(line, token) if token
|
|
98
|
+
containing_brackets(line, column).each do |bracket|
|
|
99
|
+
ranges << Region.new(start_line: bracket.open_line, end_line: bracket.close_line,
|
|
100
|
+
start_column: bracket.open_column + 1, end_column: bracket.close_column,
|
|
101
|
+
kind: :block, label: "inside #{bracket.kind}")
|
|
102
|
+
ranges << Region.new(start_line: bracket.open_line, end_line: bracket.close_line,
|
|
103
|
+
start_column: bracket.open_column, end_column: bracket.close_column + 1,
|
|
104
|
+
kind: :block, label: bracket.kind)
|
|
105
|
+
end
|
|
106
|
+
ranges << Region.new(start_line: line, end_line: line, start_column: 0,
|
|
107
|
+
end_column: @line_data.fetch(line).text.delete_suffix("\n").length,
|
|
108
|
+
kind: :line, label: @line_data.fetch(line).label)
|
|
109
|
+
ranges.concat(context_at(line).reverse)
|
|
110
|
+
ranges.uniq { |region| [region.start_line, region.start_column, region.end_line, region.end_column] }
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
# The provider must already reflect the edit, matching Highlighter#edit.
|
|
114
|
+
def edit(from_line:, removed:, inserted:)
|
|
115
|
+
if @pending_old_lines || removed != inserted
|
|
116
|
+
@folds_dirty = @fold_brackets_dirty = @derived_dirty = true
|
|
117
|
+
@pending_old_lines = nil
|
|
118
|
+
else
|
|
119
|
+
@pending_old_lines = [from_line, @line_data.slice(from_line, removed) || []]
|
|
120
|
+
end
|
|
121
|
+
@line_data[from_line, removed] = Array.new(inserted)
|
|
122
|
+
@dirty_from = [@dirty_from || from_line, from_line].min
|
|
123
|
+
@brackets_dirty = true
|
|
124
|
+
if removed != inserted
|
|
125
|
+
@folds_dirty = true
|
|
126
|
+
@fold_brackets_dirty = true
|
|
127
|
+
end
|
|
128
|
+
self
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
private
|
|
132
|
+
|
|
133
|
+
def refresh(brackets: false, folds: false)
|
|
134
|
+
count = line_count
|
|
135
|
+
if @line_data.length != count
|
|
136
|
+
@brackets_dirty = @folds_dirty = @fold_brackets_dirty = @derived_dirty = true
|
|
137
|
+
@line_data.fill(nil, @line_data.length...count) if @line_data.length < count
|
|
138
|
+
@line_data.slice!(count..) if @line_data.length > count
|
|
139
|
+
end
|
|
140
|
+
refresh_lines(count) if @dirty_from
|
|
141
|
+
rebuild_brackets if brackets && @brackets_dirty
|
|
142
|
+
if folds && @folds_dirty
|
|
143
|
+
rebuild_brackets if @fold_brackets_dirty && @brackets_dirty
|
|
144
|
+
rebuild_folds
|
|
145
|
+
end
|
|
146
|
+
self
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
def refresh_lines(count)
|
|
150
|
+
start = @dirty_from
|
|
151
|
+
if start.zero? && @line_data.none?
|
|
152
|
+
@tokens_in.call(0...count).each_with_index { |tokens, index| @line_data[index] = scan_line(index, tokens) }
|
|
153
|
+
@dirty_from = nil
|
|
154
|
+
return
|
|
155
|
+
end
|
|
156
|
+
finish = @stabilize ? @stabilize.call(start) : count
|
|
157
|
+
unless finish.is_a?(Integer) && finish >= start && finish <= count
|
|
158
|
+
raise ArgumentError, "stabilize must return a line boundary inside the document"
|
|
159
|
+
end
|
|
160
|
+
index = start
|
|
161
|
+
while index < finish
|
|
162
|
+
tokens = @tokens_for.call(index)
|
|
163
|
+
old = old_line(index)
|
|
164
|
+
updated = scan_line(index, tokens)
|
|
165
|
+
if bracket_boundary_changed?(old, updated)
|
|
166
|
+
@fold_brackets_dirty = @folds_dirty = @derived_dirty = true
|
|
167
|
+
end
|
|
168
|
+
if derived_line_changed?(old, updated, index)
|
|
169
|
+
@folds_dirty = @derived_dirty = true
|
|
170
|
+
end
|
|
171
|
+
@line_data[index] = updated
|
|
172
|
+
index += 1
|
|
173
|
+
end
|
|
174
|
+
@pending_old_lines = nil
|
|
175
|
+
@dirty_from = nil
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
def scan_line(index, tokens)
|
|
179
|
+
text = @lines.call(index)
|
|
180
|
+
column = 0
|
|
181
|
+
brackets = []
|
|
182
|
+
significant = false
|
|
183
|
+
comment = true
|
|
184
|
+
tokens.each do |type, value|
|
|
185
|
+
finish = column + value.length
|
|
186
|
+
token_kind = @token_kinds[type] ||= classify_token(type.qualname)
|
|
187
|
+
if comment && !value.strip.empty?
|
|
188
|
+
significant = true
|
|
189
|
+
comment = token_kind == :comment
|
|
190
|
+
end
|
|
191
|
+
if token_kind == :punctuation
|
|
192
|
+
value.each_char.with_index { |character, offset| brackets << [character, column + offset] if OPEN.key?(character) || CLOSE.key?(character) }
|
|
193
|
+
end
|
|
194
|
+
column = finish
|
|
195
|
+
end
|
|
196
|
+
stripped = text.strip
|
|
197
|
+
comment &&= significant
|
|
198
|
+
marker = comment && (match = REGION_MARKER.match(text)) ? (match[1] ? :close : :open) : nil
|
|
199
|
+
Line.new(text: text, tokens: tokens, brackets: brackets.freeze,
|
|
200
|
+
indent: indentation(text), blank: stripped.empty?, comment: comment,
|
|
201
|
+
marker: marker, label: stripped.freeze).freeze
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
def rebuild_brackets
|
|
205
|
+
@brackets = build_brackets.freeze
|
|
206
|
+
@bracket_positions = {}
|
|
207
|
+
@brackets.each do |bracket|
|
|
208
|
+
@bracket_positions[[bracket.open_line, bracket.open_column]] = bracket
|
|
209
|
+
@bracket_positions[[bracket.close_line, bracket.close_column]] = bracket
|
|
210
|
+
end
|
|
211
|
+
@brackets_dirty = false
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
def rebuild_folds
|
|
215
|
+
if @derived_dirty
|
|
216
|
+
@derived_regions = derived_regions.freeze
|
|
217
|
+
end
|
|
218
|
+
regions = bracket_regions + @derived_regions
|
|
219
|
+
@fold_regions = regions.uniq { |region| [region.start_line, region.end_line, region.kind] }
|
|
220
|
+
.sort_by { |region| [region.start_line, -region.end_line, region.kind.to_s] }.freeze
|
|
221
|
+
@fold_start_lines = @fold_regions.each_with_object({}) do |region, starts|
|
|
222
|
+
starts[region.start_line] = true
|
|
223
|
+
end.freeze
|
|
224
|
+
@derived_dirty = false
|
|
225
|
+
@folds_dirty = @fold_brackets_dirty = false
|
|
226
|
+
end
|
|
227
|
+
|
|
228
|
+
def old_line(index)
|
|
229
|
+
return @line_data[index] unless @pending_old_lines
|
|
230
|
+
|
|
231
|
+
first, lines = @pending_old_lines
|
|
232
|
+
index >= first && index < first + lines.length ? lines[index - first] : @line_data[index]
|
|
233
|
+
end
|
|
234
|
+
|
|
235
|
+
def derived_line_changed?(old, updated, index)
|
|
236
|
+
return true unless old
|
|
237
|
+
return false if old.equal?(updated)
|
|
238
|
+
return true unless old.indent == updated.indent && old.blank == updated.blank &&
|
|
239
|
+
old.comment == updated.comment && old.marker == updated.marker
|
|
240
|
+
|
|
241
|
+
old.label != updated.label && @fold_start_lines.key?(index)
|
|
242
|
+
end
|
|
243
|
+
|
|
244
|
+
def bracket_boundary_changed?(old, updated)
|
|
245
|
+
return true unless old
|
|
246
|
+
return false if old.brackets == updated.brackets
|
|
247
|
+
|
|
248
|
+
!locally_balanced?(old.brackets) || !locally_balanced?(updated.brackets)
|
|
249
|
+
end
|
|
250
|
+
|
|
251
|
+
def locally_balanced?(brackets)
|
|
252
|
+
stack = []
|
|
253
|
+
brackets.each do |character, _column|
|
|
254
|
+
if OPEN.key?(character)
|
|
255
|
+
stack << character
|
|
256
|
+
elsif stack.last == CLOSE.fetch(character)
|
|
257
|
+
stack.pop
|
|
258
|
+
else
|
|
259
|
+
return false
|
|
260
|
+
end
|
|
261
|
+
end
|
|
262
|
+
stack.empty?
|
|
263
|
+
end
|
|
264
|
+
|
|
265
|
+
def build_brackets
|
|
266
|
+
stack = []
|
|
267
|
+
pairs = []
|
|
268
|
+
@line_data.each_with_index do |line, line_index|
|
|
269
|
+
line.brackets.each do |character, column|
|
|
270
|
+
if OPEN.key?(character)
|
|
271
|
+
stack << [character, line_index, column, stack.length]
|
|
272
|
+
elsif stack.last&.first == CLOSE.fetch(character)
|
|
273
|
+
open, open_line, open_column, depth = stack.pop
|
|
274
|
+
pairs << Bracket.new(open_line: open_line, open_column: open_column,
|
|
275
|
+
close_line: line_index, close_column: column, depth: depth,
|
|
276
|
+
kind: "#{open}#{character}").freeze
|
|
277
|
+
else
|
|
278
|
+
stack.pop
|
|
279
|
+
end
|
|
280
|
+
end
|
|
281
|
+
end
|
|
282
|
+
pairs.sort_by { |bracket| [bracket.open_line, bracket.open_column] }
|
|
283
|
+
end
|
|
284
|
+
|
|
285
|
+
def bracket_regions
|
|
286
|
+
@brackets.filter_map do |bracket|
|
|
287
|
+
next if bracket.open_line == bracket.close_line
|
|
288
|
+
Region.new(start_line: bracket.open_line, end_line: bracket.close_line,
|
|
289
|
+
kind: :block, label: @line_data.fetch(bracket.open_line).label).freeze
|
|
290
|
+
end
|
|
291
|
+
end
|
|
292
|
+
|
|
293
|
+
def derived_regions
|
|
294
|
+
regions = []
|
|
295
|
+
indentation = []
|
|
296
|
+
markers = []
|
|
297
|
+
comment_start = nil
|
|
298
|
+
previous = nil
|
|
299
|
+
bracket_starts = @brackets.each_with_object({}) do |bracket, starts|
|
|
300
|
+
starts[bracket.open_line] = true if bracket.open_line < bracket.close_line
|
|
301
|
+
end
|
|
302
|
+
@line_data.each_with_index do |line, index|
|
|
303
|
+
if line.comment && !line.marker
|
|
304
|
+
comment_start ||= index
|
|
305
|
+
else
|
|
306
|
+
add_region(regions, comment_start, index - 1, :comment, @line_data.fetch(comment_start).label) if comment_start
|
|
307
|
+
comment_start = nil
|
|
308
|
+
end
|
|
309
|
+
markers << [index, line.label] if line.marker == :open
|
|
310
|
+
if line.marker == :close && markers.any?
|
|
311
|
+
start_line, label = markers.pop
|
|
312
|
+
add_region(regions, start_line, index, :region, label)
|
|
313
|
+
end
|
|
314
|
+
unless line.blank
|
|
315
|
+
while indentation.last && line.indent < indentation.last[2]
|
|
316
|
+
start_line, label, = indentation.pop
|
|
317
|
+
add_region(regions, start_line, previous[0], :block, label)
|
|
318
|
+
end
|
|
319
|
+
if previous && line.indent > previous[1] && !previous[3] && !bracket_starts[previous[0]]
|
|
320
|
+
indentation << [previous[0], previous[2], line.indent]
|
|
321
|
+
end
|
|
322
|
+
previous = [index, line.indent, line.label, line.comment]
|
|
323
|
+
end
|
|
324
|
+
end
|
|
325
|
+
add_region(regions, comment_start, @line_data.length - 1, :comment,
|
|
326
|
+
@line_data.fetch(comment_start).label) if comment_start
|
|
327
|
+
indentation.reverse_each do |start_line, label, _|
|
|
328
|
+
add_region(regions, start_line, previous[0], :block, label)
|
|
329
|
+
end if previous
|
|
330
|
+
regions
|
|
331
|
+
end
|
|
332
|
+
|
|
333
|
+
def add_region(regions, start_line, end_line, kind, label)
|
|
334
|
+
return unless start_line && end_line > start_line
|
|
335
|
+
regions << Region.new(start_line: start_line, end_line: end_line,
|
|
336
|
+
kind: kind, label: label).freeze
|
|
337
|
+
end
|
|
338
|
+
|
|
339
|
+
def containing_brackets(line, column)
|
|
340
|
+
@brackets.select do |bracket|
|
|
341
|
+
([bracket.open_line, bracket.open_column] <=> [line, column]) <= 0 &&
|
|
342
|
+
([line, column] <=> [bracket.close_line, bracket.close_column]) <= 0
|
|
343
|
+
end.sort_by { |bracket| -bracket.depth }
|
|
344
|
+
end
|
|
345
|
+
|
|
346
|
+
def selection_region(line, span)
|
|
347
|
+
Region.new(start_line: line, end_line: line, start_column: span[0],
|
|
348
|
+
end_column: span[1], kind: span[2], label: span[3])
|
|
349
|
+
end
|
|
350
|
+
|
|
351
|
+
def selection_spans(line)
|
|
352
|
+
column = 0
|
|
353
|
+
line.tokens.filter_map do |type, value|
|
|
354
|
+
span = nil
|
|
355
|
+
finish = column + value.length
|
|
356
|
+
unless value.strip.empty?
|
|
357
|
+
token_kind = @token_kinds[type] ||= classify_token(type.qualname)
|
|
358
|
+
kind = token_kind == :comment ? :comment : (token_kind == :string ? :string : :token)
|
|
359
|
+
span_finish = value.end_with?("\n") ? finish - 1 : finish
|
|
360
|
+
span = [column, span_finish, kind, value.delete_suffix("\n")] if span_finish > column
|
|
361
|
+
end
|
|
362
|
+
column = finish
|
|
363
|
+
span
|
|
364
|
+
end
|
|
365
|
+
end
|
|
366
|
+
|
|
367
|
+
def select_range(values, range)
|
|
368
|
+
return values.dup unless range
|
|
369
|
+
first, last = range_bounds(range)
|
|
370
|
+
values.select do |value|
|
|
371
|
+
value_first, value_last = yield(value)
|
|
372
|
+
value_last >= first && value_first <= last
|
|
373
|
+
end
|
|
374
|
+
end
|
|
375
|
+
|
|
376
|
+
def range_bounds(range)
|
|
377
|
+
raise ArgumentError, "range must have integer bounds" unless range.is_a?(Range) && range.begin.is_a?(Integer) && range.end.is_a?(Integer)
|
|
378
|
+
count = line_count
|
|
379
|
+
last = range.exclude_end? ? range.end - 1 : range.end
|
|
380
|
+
endpoints = [range.begin, range.end]
|
|
381
|
+
outside = endpoints.any?(&:negative?) || endpoints.any? { |value| value > count }
|
|
382
|
+
outside ||= !range.exclude_end? && endpoints.include?(count)
|
|
383
|
+
raise RangeError, "range outside document" if outside
|
|
384
|
+
[range.begin, last]
|
|
385
|
+
end
|
|
386
|
+
|
|
387
|
+
def validate_position(line, column, brackets: false, folds: false)
|
|
388
|
+
validate_line(line)
|
|
389
|
+
raise ArgumentError, "column must be a nonnegative integer" unless column.is_a?(Integer) && column >= 0
|
|
390
|
+
refresh(brackets: brackets, folds: folds)
|
|
391
|
+
text = @line_data.fetch(line).text
|
|
392
|
+
last = text.end_with?("\n") ? text.length - 1 : text.length
|
|
393
|
+
raise RangeError, "column outside line" if column > last
|
|
394
|
+
end
|
|
395
|
+
|
|
396
|
+
def validate_line(line)
|
|
397
|
+
raise RangeError, "line outside document" unless line.is_a?(Integer) && line >= 0 && line < line_count
|
|
398
|
+
end
|
|
399
|
+
|
|
400
|
+
def line_count
|
|
401
|
+
value = @line_count.call
|
|
402
|
+
raise ArgumentError, "line_count must return a nonnegative integer" unless value.is_a?(Integer) && value >= 0
|
|
403
|
+
value
|
|
404
|
+
end
|
|
405
|
+
|
|
406
|
+
def indentation(text)
|
|
407
|
+
column = 0
|
|
408
|
+
text.each_char do |character|
|
|
409
|
+
case character
|
|
410
|
+
when " " then column += 1
|
|
411
|
+
when "\t" then column += 8 - (column % 8)
|
|
412
|
+
else break
|
|
413
|
+
end
|
|
414
|
+
end
|
|
415
|
+
column
|
|
416
|
+
end
|
|
417
|
+
|
|
418
|
+
def comment_token?(name) = name == "Comment" || name.start_with?("Comment.")
|
|
419
|
+
def string_token?(name) = name == "Literal.String" || name.start_with?("Literal.String.")
|
|
420
|
+
def punctuation_token?(name) = name == "Punctuation" || name.start_with?("Punctuation.")
|
|
421
|
+
|
|
422
|
+
def classify_token(name)
|
|
423
|
+
return :comment if comment_token?(name)
|
|
424
|
+
return :string if string_token?(name)
|
|
425
|
+
return :punctuation if punctuation_token?(name)
|
|
426
|
+
|
|
427
|
+
:token
|
|
428
|
+
end
|
|
429
|
+
end
|
|
430
|
+
end
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "timeout"
|
|
4
|
+
|
|
5
|
+
module Antares
|
|
6
|
+
class TMLanguageLexer < Rouge::Lexer
|
|
7
|
+
MAX_SOURCE_BYTES = 8 * 1024 * 1024
|
|
8
|
+
MAX_SECONDS = 0.2
|
|
9
|
+
MAX_INCLUDE_DEPTH = 64
|
|
10
|
+
MAX_CONTEXT_DEPTH = 256
|
|
11
|
+
MAX_NULL_STEPS = 32
|
|
12
|
+
Context = Struct.new(:patterns, :ending, :token, :delimiter_token, :end_captures,
|
|
13
|
+
:apply_end_last, keyword_init: true)
|
|
14
|
+
|
|
15
|
+
class << self
|
|
16
|
+
attr_reader :definition
|
|
17
|
+
|
|
18
|
+
def build(definition)
|
|
19
|
+
Class.new(self).tap do |lexer|
|
|
20
|
+
lexer.instance_variable_set(:@definition, definition)
|
|
21
|
+
lexer.define_singleton_method(:tag) { definition.tag }
|
|
22
|
+
end.new
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def initialize(options = {})
|
|
27
|
+
super
|
|
28
|
+
@max_source_bytes = Integer(self.options.fetch("max_bytes", MAX_SOURCE_BYTES))
|
|
29
|
+
@max_seconds = Float(self.options.fetch("max_seconds", MAX_SECONDS))
|
|
30
|
+
raise ArgumentError, "max_bytes must be positive" unless @max_source_bytes.positive?
|
|
31
|
+
raise ArgumentError, "max_seconds must be positive and finite" unless @max_seconds.positive? && @max_seconds.finite?
|
|
32
|
+
rescue ArgumentError, TypeError
|
|
33
|
+
raise ArgumentError, "invalid tmLanguage lexer limits"
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def reset!
|
|
37
|
+
@expanded = {}
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def stream_tokens(source, &emit)
|
|
41
|
+
raise ResourceLimitError, "source exceeds #{@max_source_bytes} bytes" if source.bytesize > @max_source_bytes
|
|
42
|
+
|
|
43
|
+
Timeout.timeout(@max_seconds, ResourceLimitError) { tokenize(source, &emit) }
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
private
|
|
47
|
+
|
|
48
|
+
def tokenize(source, &emit)
|
|
49
|
+
definition = self.class.definition
|
|
50
|
+
contexts = [Context.new(patterns: definition.patterns, token: Rouge::Token::Tokens::Text)]
|
|
51
|
+
position = 0
|
|
52
|
+
null_steps = 0
|
|
53
|
+
while position < source.length
|
|
54
|
+
context = contexts.last
|
|
55
|
+
ending = context.ending&.match(source, position)
|
|
56
|
+
rule, matching = next_rule(expanded(context.patterns), source, position)
|
|
57
|
+
use_ending = ending && (!matching || ending.begin(0) < matching.begin(0) ||
|
|
58
|
+
(ending.begin(0) == matching.begin(0) && !context.apply_end_last))
|
|
59
|
+
boundary = use_ending ? ending&.begin(0) : matching&.begin(0)
|
|
60
|
+
unless boundary
|
|
61
|
+
emit.call(context.token, source[position..])
|
|
62
|
+
break
|
|
63
|
+
end
|
|
64
|
+
if boundary > position
|
|
65
|
+
emit.call(context.token, source[position...boundary])
|
|
66
|
+
position = boundary
|
|
67
|
+
null_steps = 0
|
|
68
|
+
next
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
before = position
|
|
72
|
+
if use_ending
|
|
73
|
+
emit_match(source, ending, context.delimiter_token || context.token,
|
|
74
|
+
context.end_captures || {}, &emit)
|
|
75
|
+
position = ending.end(0)
|
|
76
|
+
contexts.pop
|
|
77
|
+
elsif rule.is_a?(Grammar::MatchRule)
|
|
78
|
+
raise GrammarError, "match regex consumed no input" if matching.end(0) == position
|
|
79
|
+
|
|
80
|
+
emit_match(source, matching, rule.token || context.token, rule.captures, &emit)
|
|
81
|
+
position = matching.end(0)
|
|
82
|
+
else
|
|
83
|
+
emit_match(source, matching, rule.token || context.token, rule.begin_captures, &emit)
|
|
84
|
+
position = matching.end(0)
|
|
85
|
+
raise ResourceLimitError, "grammar context nesting exceeds #{MAX_CONTEXT_DEPTH}" if contexts.length >= MAX_CONTEXT_DEPTH
|
|
86
|
+
|
|
87
|
+
contexts << Context.new(patterns: rule.patterns,
|
|
88
|
+
ending: end_regexp(rule, matching),
|
|
89
|
+
token: rule.content_token || rule.token || context.token,
|
|
90
|
+
delimiter_token: rule.token || context.token,
|
|
91
|
+
end_captures: rule.end_captures,
|
|
92
|
+
apply_end_last: rule.apply_end_last)
|
|
93
|
+
end
|
|
94
|
+
null_steps = position == before ? null_steps + 1 : 0
|
|
95
|
+
raise GrammarError, "grammar made too many zero-width transitions" if null_steps > MAX_NULL_STEPS
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def next_rule(patterns, source, position)
|
|
100
|
+
selected_rule = selected_match = nil
|
|
101
|
+
patterns.each do |rule|
|
|
102
|
+
matching = rule.regexp.match(source, position)
|
|
103
|
+
next unless matching
|
|
104
|
+
next if selected_match && selected_match.begin(0) <= matching.begin(0)
|
|
105
|
+
|
|
106
|
+
selected_rule = rule
|
|
107
|
+
selected_match = matching
|
|
108
|
+
end
|
|
109
|
+
[selected_rule, selected_match]
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def expanded(patterns)
|
|
113
|
+
@expanded[patterns.object_id] ||= begin
|
|
114
|
+
cycle = [false]
|
|
115
|
+
count = [0]
|
|
116
|
+
rules = expand_patterns(patterns, {patterns.object_id => true}, 0, cycle, count).freeze
|
|
117
|
+
raise GrammarError, "recursive include contains no matching rules" if rules.empty? && cycle.first
|
|
118
|
+
|
|
119
|
+
rules
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def expand_patterns(patterns, active, depth, cycle, count)
|
|
124
|
+
raise ResourceLimitError, "grammar include nesting exceeds #{MAX_INCLUDE_DEPTH}" if depth >= MAX_INCLUDE_DEPTH
|
|
125
|
+
|
|
126
|
+
patterns.flat_map { |rule| expand_rule(rule, active, depth, cycle, count) }
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
def expand_rule(rule, active, depth, cycle, count)
|
|
130
|
+
raise ResourceLimitError, "grammar include nesting exceeds #{MAX_INCLUDE_DEPTH}" if depth >= MAX_INCLUDE_DEPTH
|
|
131
|
+
|
|
132
|
+
if rule.is_a?(Grammar::MatchRule) || rule.is_a?(Grammar::BeginRule)
|
|
133
|
+
count[0] += 1
|
|
134
|
+
raise ResourceLimitError, "expanded grammar exceeds #{Grammar::MAX_RULES} rules" if count[0] > Grammar::MAX_RULES
|
|
135
|
+
|
|
136
|
+
return [rule]
|
|
137
|
+
end
|
|
138
|
+
return expand_patterns(rule.patterns, active, depth + 1, cycle, count) if rule.is_a?(Grammar::GroupRule)
|
|
139
|
+
|
|
140
|
+
target = if %w[$self $base].include?(rule.target)
|
|
141
|
+
self.class.definition.patterns
|
|
142
|
+
else
|
|
143
|
+
self.class.definition.repository.fetch(rule.target.delete_prefix("#"))
|
|
144
|
+
end
|
|
145
|
+
target_patterns = target.is_a?(Array) ? target : (target.patterns if target.is_a?(Grammar::GroupRule))
|
|
146
|
+
return expand_rule(target, active, depth + 1, cycle, count) unless target_patterns
|
|
147
|
+
|
|
148
|
+
key = target_patterns.object_id
|
|
149
|
+
if active[key]
|
|
150
|
+
cycle[0] = true
|
|
151
|
+
return []
|
|
152
|
+
end
|
|
153
|
+
expand_patterns(target_patterns, active.merge(key => true), depth + 1, cycle, count)
|
|
154
|
+
rescue KeyError
|
|
155
|
+
raise GrammarError, "unknown repository include #{rule.target.inspect}"
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
def end_regexp(rule, matching)
|
|
159
|
+
return rule.ending unless rule.dynamic_end
|
|
160
|
+
|
|
161
|
+
source = rule.end_source.gsub(/(?<!\\)\\([1-9])/) do
|
|
162
|
+
index = Regexp.last_match(1).to_i
|
|
163
|
+
capture = matching[index]
|
|
164
|
+
raise GrammarError, "end capture #{index} did not participate in begin regex" unless capture
|
|
165
|
+
|
|
166
|
+
Regexp.escape(capture)
|
|
167
|
+
end
|
|
168
|
+
raise ResourceLimitError, "expanded end regex exceeds #{Grammar::MAX_REGEX_BYTES} bytes" if source.bytesize > Grammar::MAX_REGEX_BYTES
|
|
169
|
+
|
|
170
|
+
Regexp.new(source)
|
|
171
|
+
rescue RegexpError, IndexError => error
|
|
172
|
+
raise GrammarError, "invalid expanded end regex: #{error.message.byteslice(0, 256)}"
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
def emit_match(source, matching, base_token, captures)
|
|
176
|
+
first = matching.begin(0)
|
|
177
|
+
last = matching.end(0)
|
|
178
|
+
ranges = captures.filter_map do |index, token|
|
|
179
|
+
start = matching.begin(index)
|
|
180
|
+
finish = matching.end(index)
|
|
181
|
+
[start, finish, token, index] if start && finish && finish > start
|
|
182
|
+
rescue IndexError
|
|
183
|
+
raise GrammarError, "capture #{index} does not exist in regex"
|
|
184
|
+
end
|
|
185
|
+
boundaries = ([first, last] + ranges.flat_map { |range| range.first(2) }).uniq.sort
|
|
186
|
+
boundaries.each_cons(2) do |start, finish|
|
|
187
|
+
range = ranges.select { |candidate| candidate[0] <= start && finish <= candidate[1] }
|
|
188
|
+
.min_by { |candidate| [candidate[1] - candidate[0], -candidate[3]] }
|
|
189
|
+
yield(range ? range[2] : base_token, source[start...finish])
|
|
190
|
+
end
|
|
191
|
+
end
|
|
192
|
+
end
|
|
193
|
+
end
|