antares 0.1.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 +7 -0
- data/CHANGELOG.md +5 -0
- data/LICENSE.txt +21 -0
- data/README.md +111 -0
- data/lib/antares/compatibility.rb +478 -0
- data/lib/antares/highlighter.rb +284 -0
- data/lib/antares/lexer_driver.rb +72 -0
- data/lib/antares/lexer_state_snapshot.rb +106 -0
- data/lib/antares/version.rb +5 -0
- data/lib/antares.rb +21 -0
- data/sig/antares.rbs +47 -0
- metadata +67 -0
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "timeout"
|
|
4
|
+
|
|
5
|
+
module Antares
|
|
6
|
+
class Highlighter
|
|
7
|
+
STRATEGIES = %i[auto incremental window full].freeze
|
|
8
|
+
attr_reader :strategy, :requested_strategy, :frontier, :checkpoints, :last_scanned_lines, :fallback_reason
|
|
9
|
+
|
|
10
|
+
def initialize(lexer:, lines:, line_count:, strategy: :auto, checkpoint_interval: 64,
|
|
11
|
+
window_context: 200, max_bytes: 8 * 1024 * 1024, max_lines: 100_000,
|
|
12
|
+
max_line_bytes: 16_384, max_checkpoint_bytes: 256 * 1024, max_seconds: 0.25)
|
|
13
|
+
raise ArgumentError, "unknown strategy #{strategy.inspect}" unless STRATEGIES.include?(strategy)
|
|
14
|
+
raise ArgumentError, "lines and line_count must be callable" unless lines.respond_to?(:call) && line_count.respond_to?(:call)
|
|
15
|
+
{checkpoint_interval: checkpoint_interval, max_bytes: max_bytes, max_lines: max_lines,
|
|
16
|
+
max_line_bytes: max_line_bytes, max_checkpoint_bytes: max_checkpoint_bytes}.each do |name, value|
|
|
17
|
+
raise ArgumentError, "#{name} must be a positive integer" unless value.is_a?(Integer) && value.positive?
|
|
18
|
+
end
|
|
19
|
+
raise ArgumentError, "window_context must be nonnegative" unless window_context.is_a?(Integer) && window_context >= 0
|
|
20
|
+
raise ArgumentError, "max_seconds must be positive" unless max_seconds.is_a?(Numeric) && max_seconds.positive?
|
|
21
|
+
@template = lexer
|
|
22
|
+
@lines, @line_count = lines, line_count
|
|
23
|
+
@requested_strategy = strategy
|
|
24
|
+
@strategy = strategy == :auto ? Antares.compatible?(lexer) : strategy
|
|
25
|
+
@checkpoint_interval, @window_context = checkpoint_interval, window_context
|
|
26
|
+
@max_bytes, @max_lines, @max_line_bytes = max_bytes, max_lines, max_line_bytes
|
|
27
|
+
@max_checkpoint_bytes, @max_seconds = max_checkpoint_bytes, max_seconds
|
|
28
|
+
@tokens, @fingerprints, @checkpoints = {}, {}, {}
|
|
29
|
+
@frontier = @last_scanned_lines = 0
|
|
30
|
+
@count = count
|
|
31
|
+
@dirty_until = 0
|
|
32
|
+
initialize_lexer
|
|
33
|
+
rescue UnsupportedLexerError => error
|
|
34
|
+
raise unless strategy == :auto || strategy == :window || strategy == :full
|
|
35
|
+
@template = lexer
|
|
36
|
+
@strategy = :window if strategy == :auto
|
|
37
|
+
@fallback_reason = error.message
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def tokens_for(index)
|
|
41
|
+
validate_index(index)
|
|
42
|
+
advance(until_line: index) unless @tokens.key?(index) && index < frontier
|
|
43
|
+
@tokens.fetch(index) { plain(index) }
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def tokens_in(range)
|
|
47
|
+
indexes = range.to_a
|
|
48
|
+
return [] if indexes.empty?
|
|
49
|
+
indexes.each { |index| validate_index(index) }
|
|
50
|
+
advance(until_line: indexes.max, from_line: indexes.min)
|
|
51
|
+
indexes.map { |index| @tokens.fetch(index) { plain(index) } }
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# Provider contents must already reflect this line edit. Indices are zero based.
|
|
55
|
+
def edit(from_line:, removed:, inserted:)
|
|
56
|
+
values = [from_line, removed, inserted]
|
|
57
|
+
raise ArgumentError, "edit fields must be nonnegative integers" unless values.all? { |value| value.is_a?(Integer) && value >= 0 }
|
|
58
|
+
raise RangeError, "edit outside previous document" unless from_line <= @count && from_line + removed <= @count
|
|
59
|
+
updated_count = count
|
|
60
|
+
raise ArgumentError, "line_count disagrees with edit" unless updated_count == @count - removed + inserted
|
|
61
|
+
@last_scanned_lines = 0
|
|
62
|
+
if strategy == :incremental
|
|
63
|
+
@tokens.delete_if { |line, _| line >= frontier } if @old_fingerprints
|
|
64
|
+
start = @checkpoints.keys.select { |line| line <= from_line && line <= frontier }.max || 0
|
|
65
|
+
restart = @checkpoints.fetch(start)
|
|
66
|
+
shift_cache(@tokens, from_line, removed, inserted)
|
|
67
|
+
shift_cache(@fingerprints, from_line, removed, inserted)
|
|
68
|
+
shift_cache(@checkpoints, from_line, removed, inserted)
|
|
69
|
+
@old_fingerprints = @fingerprints.dup
|
|
70
|
+
@old_checkpoints = @checkpoints.dup
|
|
71
|
+
@fingerprints.delete_if { |line, _| line > start }
|
|
72
|
+
@checkpoints.delete_if { |line, _| line > start }
|
|
73
|
+
@checkpoints[start] = restart
|
|
74
|
+
@frontier = start
|
|
75
|
+
@dirty_until = from_line + inserted
|
|
76
|
+
@lexer = @checkpoints.fetch(start).restore
|
|
77
|
+
else
|
|
78
|
+
@tokens.clear
|
|
79
|
+
@frontier = 0
|
|
80
|
+
end
|
|
81
|
+
@count = updated_count
|
|
82
|
+
@source = @offsets = @driver = nil
|
|
83
|
+
self
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
# frontier is the first line not yet proven current (an exclusive boundary).
|
|
87
|
+
def advance(until_line:, from_line: nil)
|
|
88
|
+
return self if @count.zero?
|
|
89
|
+
validate_index(until_line)
|
|
90
|
+
@last_scanned_lines = 0
|
|
91
|
+
return self if until_line < frontier && @tokens.key?(until_line) &&
|
|
92
|
+
(from_line.nil? || (from_line..until_line).all? { |line| @tokens.key?(line) })
|
|
93
|
+
Timeout.timeout(@max_seconds) do
|
|
94
|
+
if @count > @max_lines
|
|
95
|
+
fallback!(:window, "document exceeds #{@max_lines} lines")
|
|
96
|
+
end
|
|
97
|
+
case strategy
|
|
98
|
+
when :incremental then advance_incremental(until_line)
|
|
99
|
+
when :full then advance_full
|
|
100
|
+
when :window then advance_window(from_line || until_line, until_line)
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
self
|
|
104
|
+
rescue ResourceLimitError, UnsupportedLexerError => error
|
|
105
|
+
raise if requested_strategy == :incremental && error.is_a?(UnsupportedLexerError)
|
|
106
|
+
fallback!(:window, error.message)
|
|
107
|
+
advance_window(from_line || until_line, until_line)
|
|
108
|
+
self
|
|
109
|
+
rescue Timeout::Error
|
|
110
|
+
fallback!(:window, "lexing exceeded #{@max_seconds} seconds")
|
|
111
|
+
((from_line || until_line)..until_line).each { |index| @tokens[index] = plain(index) }
|
|
112
|
+
@frontier = until_line + 1
|
|
113
|
+
self
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def checkpoint_bytes = checkpoints.values.sum(&:bytesize)
|
|
117
|
+
|
|
118
|
+
private
|
|
119
|
+
|
|
120
|
+
def initialize_lexer
|
|
121
|
+
@lexer = fresh_lexer
|
|
122
|
+
if strategy == :incremental
|
|
123
|
+
raise UnsupportedLexerError, "#{@lexer.class.tag} has a custom stream driver" unless LexerDriver.supported?(@lexer)
|
|
124
|
+
@checkpoints[0] = LexerStateSnapshot.new(@lexer, max_bytes: @max_checkpoint_bytes)
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
def fresh_lexer
|
|
129
|
+
lexer = LexerStateSnapshot.copy(@template)
|
|
130
|
+
lexer.reset!
|
|
131
|
+
lexer
|
|
132
|
+
rescue UnsupportedLexerError
|
|
133
|
+
@template.class.new(@template.options).tap(&:reset!)
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
def count
|
|
137
|
+
value = @line_count.call
|
|
138
|
+
raise ArgumentError, "line_count must return a nonnegative integer" unless value.is_a?(Integer) && value >= 0
|
|
139
|
+
value
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
def validate_index(index)
|
|
143
|
+
raise RangeError, "line outside document" unless index.is_a?(Integer) && index >= 0 && index < @count
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
def source_line(index)
|
|
147
|
+
value = @lines.call(index)
|
|
148
|
+
raise TypeError, "line provider must return a String" unless value.is_a?(String)
|
|
149
|
+
raise EncodingError, "line provider must return valid UTF-8" unless value.valid_encoding? && [Encoding::UTF_8, Encoding::US_ASCII].include?(value.encoding)
|
|
150
|
+
raise ArgumentError, "line provider returned multiple logical lines" if value.count("\n") > 1 || (value.include?("\n") && !value.end_with?("\n"))
|
|
151
|
+
# Providers may omit separators, but an empty final line remains empty.
|
|
152
|
+
index < @count - 1 && !value.end_with?("\n") ? value + "\n" : value
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
def build_source
|
|
156
|
+
return if @source
|
|
157
|
+
source = +""
|
|
158
|
+
offsets = [0]
|
|
159
|
+
@count.times do |index|
|
|
160
|
+
line = source_line(index)
|
|
161
|
+
raise ResourceLimitError, "line exceeds #{@max_line_bytes} bytes" if line.bytesize > @max_line_bytes
|
|
162
|
+
raise ResourceLimitError, "document exceeds #{@max_bytes} bytes" if source.bytesize + line.bytesize > @max_bytes
|
|
163
|
+
source << line
|
|
164
|
+
offsets << source.bytesize
|
|
165
|
+
end
|
|
166
|
+
@source, @offsets = source.freeze, offsets.freeze
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
def advance_incremental(until_line)
|
|
170
|
+
build_source
|
|
171
|
+
@driver ||= LexerDriver.new(@lexer, @source, @offsets, start_line: frontier) do |line, tokens|
|
|
172
|
+
@tokens[line] = tokens
|
|
173
|
+
end
|
|
174
|
+
before = @driver.scanned_lines
|
|
175
|
+
@driver.advance(until_line: until_line) do |line, lexer|
|
|
176
|
+
fingerprint = LexerStateSnapshot.fingerprint(lexer)
|
|
177
|
+
previous = @old_fingerprints&.[](line)
|
|
178
|
+
@fingerprints[line] = fingerprint
|
|
179
|
+
@frontier = line
|
|
180
|
+
if line >= @dirty_until && previous == fingerprint && @tokens.key?(line)
|
|
181
|
+
# Only the declared edited interval changed. Equal state at an aligned
|
|
182
|
+
# old boundary makes the untouched cached suffix reusable.
|
|
183
|
+
@frontier += 1 while @tokens.key?(@frontier)
|
|
184
|
+
@fingerprints.merge!(@old_fingerprints.select { |position, _| position >= line && position <= frontier })
|
|
185
|
+
@checkpoints.merge!(@old_checkpoints.select { |position, _| position >= line && position <= frontier })
|
|
186
|
+
@old_fingerprints = @old_checkpoints = nil
|
|
187
|
+
frontier > until_line
|
|
188
|
+
else
|
|
189
|
+
checkpoint = @checkpoints.keys.select { |position| position < line }.max || 0
|
|
190
|
+
if line - checkpoint >= @checkpoint_interval || line == @count || @checkpoints.key?(line)
|
|
191
|
+
@checkpoints[line] = LexerStateSnapshot.new(lexer, max_bytes: @max_checkpoint_bytes)
|
|
192
|
+
end
|
|
193
|
+
false
|
|
194
|
+
end
|
|
195
|
+
end
|
|
196
|
+
@last_scanned_lines = @driver.scanned_lines - before
|
|
197
|
+
@frontier = [@frontier, @driver.line].max
|
|
198
|
+
@old_fingerprints = @old_checkpoints = nil if frontier >= @count
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
def advance_full
|
|
202
|
+
build_source
|
|
203
|
+
@tokens = split_tokens(fresh_lexer.lex(@source), 0)
|
|
204
|
+
@frontier = @count
|
|
205
|
+
@last_scanned_lines = @count
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
def advance_window(first, last)
|
|
209
|
+
Timeout.timeout(@max_seconds) { window_tokens(first, last) }
|
|
210
|
+
rescue Timeout::Error
|
|
211
|
+
@fallback_reason = "window lexing exceeded #{@max_seconds} seconds"
|
|
212
|
+
(first..last).each { |index| @tokens[index] = plain(index) }
|
|
213
|
+
@frontier = last + 1
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
def window_tokens(first, last)
|
|
217
|
+
first = [first - @window_context, 0].max
|
|
218
|
+
source = +""
|
|
219
|
+
selected = []
|
|
220
|
+
(first..last).each do |index|
|
|
221
|
+
line = source_line(index)
|
|
222
|
+
if line.bytesize > @max_line_bytes || source.bytesize + line.bytesize > @max_bytes
|
|
223
|
+
@tokens[index] = plain(index)
|
|
224
|
+
else
|
|
225
|
+
selected << index
|
|
226
|
+
source << line
|
|
227
|
+
end
|
|
228
|
+
end
|
|
229
|
+
unless selected.empty?
|
|
230
|
+
rows = split_tokens(fresh_lexer.lex(source), 0)
|
|
231
|
+
selected.each_with_index { |index, offset| @tokens[index] = rows.fetch(offset, [].freeze) }
|
|
232
|
+
end
|
|
233
|
+
@frontier = last + 1
|
|
234
|
+
@last_scanned_lines = selected.length
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
def split_tokens(tokens, first)
|
|
238
|
+
result = {first => []}
|
|
239
|
+
line = first
|
|
240
|
+
tokens.each do |type, value|
|
|
241
|
+
next if value.empty?
|
|
242
|
+
unless value.include?("\n")
|
|
243
|
+
row = result[line] ||= []
|
|
244
|
+
row.last&.first == type ? row.last[1] << value : row << [type, value.dup]
|
|
245
|
+
next
|
|
246
|
+
end
|
|
247
|
+
value.each_line do |part|
|
|
248
|
+
row = result[line] ||= []
|
|
249
|
+
row.last&.first == type ? row.last[1] << part : row << [type, part.dup]
|
|
250
|
+
line += 1 if part.end_with?("\n")
|
|
251
|
+
end
|
|
252
|
+
end
|
|
253
|
+
result.transform_values { |row| row.each { |pair| pair.last.freeze; pair.freeze }.freeze }
|
|
254
|
+
end
|
|
255
|
+
|
|
256
|
+
def plain(index)
|
|
257
|
+
value = source_line(index)
|
|
258
|
+
value.empty? ? [].freeze : [[Rouge::Token::Tokens::Text, value.dup.freeze].freeze].freeze
|
|
259
|
+
end
|
|
260
|
+
|
|
261
|
+
def fallback!(strategy, reason)
|
|
262
|
+
@strategy, @fallback_reason = strategy, reason
|
|
263
|
+
@driver = @source = @offsets = nil
|
|
264
|
+
@tokens.clear
|
|
265
|
+
@checkpoints.clear
|
|
266
|
+
@fingerprints.clear
|
|
267
|
+
@old_fingerprints = @old_checkpoints = nil
|
|
268
|
+
@frontier = 0
|
|
269
|
+
end
|
|
270
|
+
|
|
271
|
+
def shift_cache(cache, first, removed, inserted)
|
|
272
|
+
delta = inserted - removed
|
|
273
|
+
shifted = {}
|
|
274
|
+
cache.each do |line, value|
|
|
275
|
+
if line < first
|
|
276
|
+
shifted[line] = value
|
|
277
|
+
elsif line >= first + removed
|
|
278
|
+
shifted[line + delta] = value
|
|
279
|
+
end
|
|
280
|
+
end
|
|
281
|
+
cache.replace(shifted)
|
|
282
|
+
end
|
|
283
|
+
end
|
|
284
|
+
end
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "strscan"
|
|
4
|
+
|
|
5
|
+
module Antares
|
|
6
|
+
# Reuses Rouge's own rule interpreter. Supplying the complete bounded source
|
|
7
|
+
# preserves cross-line matches, fixed anchors and lookbehind at checkpoints.
|
|
8
|
+
class LexerDriver
|
|
9
|
+
attr_reader :lexer, :line, :scanner, :scanned_lines
|
|
10
|
+
|
|
11
|
+
def self.supported?(lexer)
|
|
12
|
+
lexer.is_a?(Rouge::RegexLexer) && lexer.method(:stream_tokens).owner == Rouge::RegexLexer
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def initialize(lexer, source, offsets, start_line: 0, &emit)
|
|
16
|
+
raise UnsupportedLexerError, "#{lexer.class.tag} has a custom stream driver" unless self.class.supported?(lexer)
|
|
17
|
+
@lexer = lexer
|
|
18
|
+
@source = source
|
|
19
|
+
@offsets = offsets
|
|
20
|
+
@line = start_line
|
|
21
|
+
@scanned_lines = 0
|
|
22
|
+
@emit = emit
|
|
23
|
+
@tokens = []
|
|
24
|
+
@scanner = StringScanner.new(source, fixed_anchor: true)
|
|
25
|
+
@scanner.pos = offsets.fetch(start_line)
|
|
26
|
+
lexer.instance_variable_set(:@current_stream, scanner)
|
|
27
|
+
lexer.instance_variable_set(:@output_stream, method(:token).to_proc)
|
|
28
|
+
lexer.instance_variable_set(:@states, lexer.class.states)
|
|
29
|
+
lexer.instance_variable_set(:@null_steps, 0)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# Checkpoint only after an entire Rouge rule callback, never inside a yield:
|
|
33
|
+
# callbacks frequently emit a token before updating their persistent state.
|
|
34
|
+
def advance(until_line:)
|
|
35
|
+
until scanner.eos? || line > until_line
|
|
36
|
+
before = scanner.pos
|
|
37
|
+
success = lexer.step(lexer.state, scanner)
|
|
38
|
+
token(Rouge::Token::Tokens::Error, scanner.getch) unless success
|
|
39
|
+
if scanner.pos > before && @tokens.empty? && scanner.pos == @offsets[line]
|
|
40
|
+
return if block_given? && yield(line, lexer)
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
if scanner.eos?
|
|
44
|
+
finish_line unless @tokens.empty?
|
|
45
|
+
finish_line while line < @offsets.length - 1
|
|
46
|
+
yield(line, lexer) if block_given?
|
|
47
|
+
end
|
|
48
|
+
self
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
private
|
|
52
|
+
|
|
53
|
+
def token(type, value)
|
|
54
|
+
return if value.nil? || value.empty?
|
|
55
|
+
value.each_line do |part|
|
|
56
|
+
if @tokens.last&.first == type
|
|
57
|
+
@tokens.last[1] << part
|
|
58
|
+
else
|
|
59
|
+
@tokens << [type, part.dup]
|
|
60
|
+
end
|
|
61
|
+
finish_line if part.end_with?("\n")
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def finish_line
|
|
66
|
+
@emit.call(line, @tokens.map { |pair| pair[1].freeze; pair.freeze }.freeze)
|
|
67
|
+
@tokens = []
|
|
68
|
+
@line += 1
|
|
69
|
+
@scanned_lines += 1
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "digest"
|
|
4
|
+
require "set"
|
|
5
|
+
|
|
6
|
+
module Antares
|
|
7
|
+
# The RegexLexer engine replaces these on every scan; retaining them would
|
|
8
|
+
# also retain a whole source String and an output callback in every checkpoint.
|
|
9
|
+
class LexerStateSnapshot
|
|
10
|
+
VOLATILE = %i[@current_stream @output_stream @states @null_steps].freeze
|
|
11
|
+
attr_reader :fingerprint, :bytesize
|
|
12
|
+
|
|
13
|
+
def initialize(lexer, max_bytes: 256 * 1024)
|
|
14
|
+
@value = self.class.copy(lexer)
|
|
15
|
+
normalized = Marshal.dump(self.class.normalize(@value))
|
|
16
|
+
raise ResourceLimitError, "lexer checkpoint exceeds #{max_bytes} bytes" if normalized.bytesize > max_bytes
|
|
17
|
+
@bytesize = normalized.bytesize
|
|
18
|
+
@fingerprint = Digest::SHA256.digest(normalized).freeze
|
|
19
|
+
freeze
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def restore = self.class.copy(@value)
|
|
23
|
+
|
|
24
|
+
def self.fingerprint(value)
|
|
25
|
+
Digest::SHA256.digest(Marshal.dump(normalize(value)))
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def self.variables(value)
|
|
29
|
+
names = value.instance_variables.sort
|
|
30
|
+
value.is_a?(Rouge::Lexer) ? names - VOLATILE : names
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def self.immutable?(value)
|
|
34
|
+
value.nil? || value == true || value == false || value.is_a?(Numeric) || value.is_a?(Symbol) ||
|
|
35
|
+
value.is_a?(Module) || value.is_a?(Regexp) || value.is_a?(Encoding) || value.is_a?(Rouge::RegexLexer::State)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def self.copy(value, memo = {})
|
|
39
|
+
return value if immutable?(value)
|
|
40
|
+
return memo[value.object_id] if memo.key?(value.object_id)
|
|
41
|
+
raise UnsupportedLexerError, "lexer state graph exceeds 10000 objects" if memo.length >= 10_000
|
|
42
|
+
case value
|
|
43
|
+
when String
|
|
44
|
+
memo[value.object_id] = value.dup
|
|
45
|
+
when Array
|
|
46
|
+
result = memo[value.object_id] = value.dup.clear
|
|
47
|
+
value.each { |item| result << copy(item, memo) }
|
|
48
|
+
variables(value).each { |name| result.instance_variable_set(name, copy(value.instance_variable_get(name), memo)) }
|
|
49
|
+
result
|
|
50
|
+
when Hash
|
|
51
|
+
raise UnsupportedLexerError, "state Hash has a closure default" if value.default_proc
|
|
52
|
+
result = memo[value.object_id] = value.dup.clear
|
|
53
|
+
result.default = copy(value.default, memo)
|
|
54
|
+
value.each { |key, item| result[copy(key, memo)] = copy(item, memo) }
|
|
55
|
+
variables(value).each { |name| result.instance_variable_set(name, copy(value.instance_variable_get(name), memo)) }
|
|
56
|
+
result
|
|
57
|
+
when Set
|
|
58
|
+
result = memo[value.object_id] = value.dup.clear
|
|
59
|
+
value.each { |item| result.add(copy(item, memo)) }
|
|
60
|
+
variables(value).each { |name| result.instance_variable_set(name, copy(value.instance_variable_get(name), memo)) }
|
|
61
|
+
result
|
|
62
|
+
when Struct
|
|
63
|
+
result = memo[value.object_id] = value.dup
|
|
64
|
+
value.each_pair { |name, item| result[name] = copy(item, memo) }
|
|
65
|
+
result
|
|
66
|
+
when Proc, Method, IO, StringScanner, MatchData
|
|
67
|
+
raise UnsupportedLexerError, "cannot safely snapshot #{value.class}"
|
|
68
|
+
else
|
|
69
|
+
names = variables(value)
|
|
70
|
+
raise UnsupportedLexerError, "opaque state object #{value.class}" if names.empty? && !value.is_a?(Rouge::Lexer)
|
|
71
|
+
result = memo[value.object_id] = value.class.allocate
|
|
72
|
+
names.each { |name| result.instance_variable_set(name, copy(value.instance_variable_get(name), memo)) }
|
|
73
|
+
result
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def self.normalize(value, memo = {})
|
|
78
|
+
case value
|
|
79
|
+
when nil, true, false, Numeric, Symbol then value
|
|
80
|
+
when String then [:string, value.encoding.name, value]
|
|
81
|
+
when Regexp then [:regexp, value.source, value.options]
|
|
82
|
+
when Module then [:module, value.name]
|
|
83
|
+
when Encoding then [:encoding, value.name]
|
|
84
|
+
when Rouge::RegexLexer::State
|
|
85
|
+
# Static states are immutable rule templates. Dynamic states contain
|
|
86
|
+
# closures, so identity is conservative: never converge distinct ones.
|
|
87
|
+
[:state, value.name.is_a?(Symbol) ? value.name : value.object_id]
|
|
88
|
+
else
|
|
89
|
+
return [:reference, memo[value.object_id]] if memo.key?(value.object_id)
|
|
90
|
+
raise UnsupportedLexerError, "lexer state graph exceeds 10000 objects" if memo.length >= 10_000
|
|
91
|
+
memo[value.object_id] = memo.length
|
|
92
|
+
case value
|
|
93
|
+
when Array then [value.class.name, value.map { |item| normalize(item, memo) }, variables(value).map { |name| [name, normalize(value.instance_variable_get(name), memo)] }]
|
|
94
|
+
when Hash
|
|
95
|
+
raise UnsupportedLexerError, "state Hash has a closure default" if value.default_proc
|
|
96
|
+
[value.class.name, normalize(value.default, memo), value.map { |key, item| [normalize(key, memo), normalize(item, memo)] }, variables(value).map { |name| [name, normalize(value.instance_variable_get(name), memo)] }]
|
|
97
|
+
when Set then [value.class.name, value.map { |item| normalize(item, memo) }, variables(value).map { |name| [name, normalize(value.instance_variable_get(name), memo)] }]
|
|
98
|
+
when Struct then [value.class.name, value.each_pair.map { |name, item| [name, normalize(item, memo)] }]
|
|
99
|
+
else
|
|
100
|
+
raise UnsupportedLexerError, "cannot fingerprint #{value.class}" if [Proc, Method, IO, StringScanner, MatchData].any? { |klass| value.is_a?(klass) }
|
|
101
|
+
[value.class.name, variables(value).map { |name| [name, normalize(value.instance_variable_get(name), memo)] }]
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
end
|
data/lib/antares.rb
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "antares/version"
|
|
4
|
+
require "rouge"
|
|
5
|
+
|
|
6
|
+
module Antares
|
|
7
|
+
class Error < StandardError; end
|
|
8
|
+
class UnsupportedLexerError < Error; end
|
|
9
|
+
class ResourceLimitError < Error; end
|
|
10
|
+
|
|
11
|
+
def self.compatible?(lexer)
|
|
12
|
+
klass = lexer.is_a?(Class) ? lexer : lexer.class
|
|
13
|
+
return :window unless Rouge.version == COMPATIBILITY_VERSION
|
|
14
|
+
COMPATIBILITY.fetch(klass.tag, :window)
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
require_relative "antares/compatibility"
|
|
19
|
+
require_relative "antares/lexer_state_snapshot"
|
|
20
|
+
require_relative "antares/lexer_driver"
|
|
21
|
+
require_relative "antares/highlighter"
|
data/sig/antares.rbs
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
module Antares
|
|
2
|
+
VERSION: String
|
|
3
|
+
COMPATIBILITY_VERSION: String
|
|
4
|
+
COMPATIBILITY: Hash[String, :incremental | :window]
|
|
5
|
+
COMPATIBILITY_DETAILS: Hash[String, Hash[Symbol, untyped]]
|
|
6
|
+
|
|
7
|
+
class Error < StandardError
|
|
8
|
+
end
|
|
9
|
+
class UnsupportedLexerError < Error
|
|
10
|
+
end
|
|
11
|
+
class ResourceLimitError < Error
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
type strategy = :auto | :incremental | :window | :full
|
|
15
|
+
type token = [Class, String]
|
|
16
|
+
type row = Array[token]
|
|
17
|
+
|
|
18
|
+
def self.compatible?: (untyped lexer) -> (:incremental | :window)
|
|
19
|
+
|
|
20
|
+
class LexerStateSnapshot
|
|
21
|
+
attr_reader fingerprint: String
|
|
22
|
+
attr_reader bytesize: Integer
|
|
23
|
+
def initialize: (untyped lexer, ?max_bytes: Integer) -> void
|
|
24
|
+
def restore: () -> untyped
|
|
25
|
+
def self.copy: (untyped value, ?Hash[Integer, untyped] memo) -> untyped
|
|
26
|
+
def self.fingerprint: (untyped value) -> String
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
class Highlighter
|
|
30
|
+
attr_reader strategy: strategy
|
|
31
|
+
attr_reader requested_strategy: strategy
|
|
32
|
+
attr_reader frontier: Integer
|
|
33
|
+
attr_reader last_scanned_lines: Integer
|
|
34
|
+
attr_reader fallback_reason: String?
|
|
35
|
+
attr_reader checkpoints: Hash[Integer, LexerStateSnapshot]
|
|
36
|
+
|
|
37
|
+
def initialize: (lexer: untyped, lines: ^(Integer) -> String, line_count: ^() -> Integer,
|
|
38
|
+
?strategy: strategy, ?checkpoint_interval: Integer, ?window_context: Integer,
|
|
39
|
+
?max_bytes: Integer, ?max_lines: Integer, ?max_line_bytes: Integer,
|
|
40
|
+
?max_checkpoint_bytes: Integer, ?max_seconds: Numeric) -> void
|
|
41
|
+
def tokens_for: (Integer index) -> row
|
|
42
|
+
def tokens_in: (Range[Integer] range) -> Array[row]
|
|
43
|
+
def edit: (from_line: Integer, removed: Integer, inserted: Integer) -> self
|
|
44
|
+
def advance: (until_line: Integer, ?from_line: Integer?) -> self
|
|
45
|
+
def checkpoint_bytes: () -> Integer
|
|
46
|
+
end
|
|
47
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: antares
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Yudai Takada
|
|
8
|
+
bindir: exe
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies:
|
|
12
|
+
- !ruby/object:Gem::Dependency
|
|
13
|
+
name: rouge
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - "~>"
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: '5.0'
|
|
19
|
+
type: :runtime
|
|
20
|
+
prerelease: false
|
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
22
|
+
requirements:
|
|
23
|
+
- - "~>"
|
|
24
|
+
- !ruby/object:Gem::Version
|
|
25
|
+
version: '5.0'
|
|
26
|
+
email:
|
|
27
|
+
- t.yudai92@gmail.com
|
|
28
|
+
executables: []
|
|
29
|
+
extensions: []
|
|
30
|
+
extra_rdoc_files: []
|
|
31
|
+
files:
|
|
32
|
+
- CHANGELOG.md
|
|
33
|
+
- LICENSE.txt
|
|
34
|
+
- README.md
|
|
35
|
+
- lib/antares.rb
|
|
36
|
+
- lib/antares/compatibility.rb
|
|
37
|
+
- lib/antares/highlighter.rb
|
|
38
|
+
- lib/antares/lexer_driver.rb
|
|
39
|
+
- lib/antares/lexer_state_snapshot.rb
|
|
40
|
+
- lib/antares/version.rb
|
|
41
|
+
- sig/antares.rbs
|
|
42
|
+
homepage: https://github.com/noxdea/antares
|
|
43
|
+
licenses:
|
|
44
|
+
- MIT
|
|
45
|
+
metadata:
|
|
46
|
+
source_code_uri: https://github.com/noxdea/antares
|
|
47
|
+
changelog_uri: https://github.com/noxdea/antares/blob/main/CHANGELOG.md
|
|
48
|
+
allowed_push_host: https://rubygems.org
|
|
49
|
+
rubygems_mfa_required: 'true'
|
|
50
|
+
rdoc_options: []
|
|
51
|
+
require_paths:
|
|
52
|
+
- lib
|
|
53
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
54
|
+
requirements:
|
|
55
|
+
- - ">="
|
|
56
|
+
- !ruby/object:Gem::Version
|
|
57
|
+
version: '3.1'
|
|
58
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
59
|
+
requirements:
|
|
60
|
+
- - ">="
|
|
61
|
+
- !ruby/object:Gem::Version
|
|
62
|
+
version: '0'
|
|
63
|
+
requirements: []
|
|
64
|
+
rubygems_version: 4.0.19
|
|
65
|
+
specification_version: 4
|
|
66
|
+
summary: Incremental highlighting for Rouge lexers
|
|
67
|
+
test_files: []
|