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,274 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
require_relative "tm_language_plist"
|
|
5
|
+
require_relative "tm_language_lexer"
|
|
6
|
+
|
|
7
|
+
module Antares
|
|
8
|
+
module Grammar
|
|
9
|
+
MAX_BYTES = 2 * 1024 * 1024
|
|
10
|
+
MAX_DEPTH = 64
|
|
11
|
+
MAX_RULES = 10_000
|
|
12
|
+
MAX_REGEX_BYTES = 16_384
|
|
13
|
+
TOP_KEYS = %w[$schema name scopeName fileTypes firstLineMatch foldingStartMarker
|
|
14
|
+
foldingStopMarker patterns repository uuid version comment
|
|
15
|
+
information_for_contributors hideFromUser].freeze
|
|
16
|
+
RULE_KEYS = %w[include match captures name begin end beginCaptures endCaptures
|
|
17
|
+
contentName patterns applyEndPatternLast comment].freeze
|
|
18
|
+
UNSUPPORTED_RULE_KEYS = %w[while whileCaptures repository disabled].freeze
|
|
19
|
+
|
|
20
|
+
MatchRule = Struct.new(:regexp, :token, :captures, keyword_init: true)
|
|
21
|
+
BeginRule = Struct.new(:regexp, :end_source, :ending, :dynamic_end, :token,
|
|
22
|
+
:content_token, :begin_captures, :end_captures, :patterns, :apply_end_last,
|
|
23
|
+
keyword_init: true)
|
|
24
|
+
IncludeRule = Struct.new(:target, keyword_init: true)
|
|
25
|
+
GroupRule = Struct.new(:patterns, keyword_init: true)
|
|
26
|
+
Definition = Struct.new(:scope_name, :tag, :patterns, :repository, keyword_init: true)
|
|
27
|
+
|
|
28
|
+
class UniqueHash < Hash
|
|
29
|
+
def []=(key, value)
|
|
30
|
+
raise GrammarError, "duplicate JSON object key #{key.inspect}" if key?(key)
|
|
31
|
+
|
|
32
|
+
super
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
private_constant :UniqueHash
|
|
36
|
+
|
|
37
|
+
module_function
|
|
38
|
+
|
|
39
|
+
def load_tmlanguage(path)
|
|
40
|
+
raise ArgumentError, "path must be a String" unless path.is_a?(String) && !path.include?("\0")
|
|
41
|
+
|
|
42
|
+
source = File.open(path, "rb") do |file|
|
|
43
|
+
raise GrammarError, "tmLanguage must be a regular file" unless file.stat.file?
|
|
44
|
+
raise ResourceLimitError, "tmLanguage exceeds #{MAX_BYTES} bytes" if file.stat.size > MAX_BYTES
|
|
45
|
+
|
|
46
|
+
file.read(MAX_BYTES + 1) || "".b
|
|
47
|
+
end
|
|
48
|
+
raise ResourceLimitError, "tmLanguage exceeds #{MAX_BYTES} bytes" if source.bytesize > MAX_BYTES
|
|
49
|
+
source = source.delete_prefix("\xEF\xBB\xBF".b).force_encoding(Encoding::UTF_8)
|
|
50
|
+
raise GrammarError, "tmLanguage must be valid UTF-8" unless source.valid_encoding?
|
|
51
|
+
|
|
52
|
+
value = parse(source)
|
|
53
|
+
definition = Compiler.new.compile(value)
|
|
54
|
+
TMLanguageLexer.build(definition)
|
|
55
|
+
rescue SystemCallError => error
|
|
56
|
+
raise GrammarError, "cannot read tmLanguage: #{error.message}"
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def parse(source)
|
|
60
|
+
case source.lstrip.getbyte(0)
|
|
61
|
+
when 123
|
|
62
|
+
JSON.parse(source, max_nesting: MAX_DEPTH, object_class: UniqueHash,
|
|
63
|
+
allow_duplicate_key: false)
|
|
64
|
+
when 60
|
|
65
|
+
TMLanguagePlist.parse(source, max_depth: MAX_DEPTH)
|
|
66
|
+
else
|
|
67
|
+
raise GrammarError, "tmLanguage must be JSON or XML plist"
|
|
68
|
+
end
|
|
69
|
+
rescue JSON::ParserError => error
|
|
70
|
+
raise GrammarError, "invalid tmLanguage JSON: #{error.message.byteslice(0, 256)}"
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def token_for_scope(scope)
|
|
74
|
+
return Rouge::Token::Tokens::Text unless scope.is_a?(String)
|
|
75
|
+
|
|
76
|
+
name = scope.split.first.to_s.downcase
|
|
77
|
+
case name
|
|
78
|
+
when /\Ainvalid(?:\.|\z)/ then Rouge::Token::Tokens::Error
|
|
79
|
+
when /\Acomment(?:\.|\z)/ then Rouge::Token::Tokens::Comment
|
|
80
|
+
when /\A(?:string\.)?(?:regexp|regex)(?:\.|\z)/ then Rouge::Token::Tokens::Literal::String::Regex
|
|
81
|
+
when /\Aconstant\.character\.escape(?:\.|\z)/ then Rouge::Token::Tokens::Literal::String::Escape
|
|
82
|
+
when /\Astring(?:\.|\z)/ then Rouge::Token::Tokens::Literal::String
|
|
83
|
+
when /\Aconstant\.numeric(?:\.|\z)/ then Rouge::Token::Tokens::Literal::Number
|
|
84
|
+
when /\Aconstant\.language(?:\.|\z)/ then Rouge::Token::Tokens::Keyword::Constant
|
|
85
|
+
when /\Akeyword(?:\.|\z)/ then Rouge::Token::Tokens::Keyword
|
|
86
|
+
when /\Astorage\.type(?:\.|\z)/ then Rouge::Token::Tokens::Keyword::Type
|
|
87
|
+
when /\Astorage(?:\.|\z)/ then Rouge::Token::Tokens::Keyword::Declaration
|
|
88
|
+
when /\Aentity\.name\.function(?:\.|\z)/ then Rouge::Token::Tokens::Name::Function
|
|
89
|
+
when /\A(?:entity\.name\.(?:class|type)|support\.class)(?:\.|\z)/ then Rouge::Token::Tokens::Name::Class
|
|
90
|
+
when /\Aentity\.other\.attribute-name(?:\.|\z)/ then Rouge::Token::Tokens::Name::Attribute
|
|
91
|
+
when /\Asupport\.function(?:\.|\z)/ then Rouge::Token::Tokens::Name::Builtin
|
|
92
|
+
when /\Avariable(?:\.|\z)/ then Rouge::Token::Tokens::Name::Variable
|
|
93
|
+
when /\Apunctuation(?:\.|\z)/ then Rouge::Token::Tokens::Punctuation
|
|
94
|
+
when /\Amarkup\.heading(?:\.|\z)/ then Rouge::Token::Tokens::Generic::Heading
|
|
95
|
+
when /\Amarkup\.bold(?:\.|\z)/ then Rouge::Token::Tokens::Generic::Strong
|
|
96
|
+
when /\Amarkup\.italic(?:\.|\z)/ then Rouge::Token::Tokens::Generic::Emph
|
|
97
|
+
when /\A(?:constant|support\.constant)(?:\.|\z)/ then Rouge::Token::Tokens::Name::Constant
|
|
98
|
+
when /\Aentity\.name(?:\.|\z)/ then Rouge::Token::Tokens::Name
|
|
99
|
+
else Rouge::Token::Tokens::Text
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
class Compiler
|
|
104
|
+
def initialize
|
|
105
|
+
@rule_count = 0
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def compile(value)
|
|
109
|
+
object(value, "tmLanguage root")
|
|
110
|
+
unknown = value.keys - TOP_KEYS
|
|
111
|
+
raise GrammarError, "unsupported tmLanguage key #{unknown.first.inspect}" unless unknown.empty?
|
|
112
|
+
|
|
113
|
+
scope = string(value["scopeName"], "scopeName")
|
|
114
|
+
patterns = compile_patterns(value.fetch("patterns", []), 0)
|
|
115
|
+
source_repository = value.fetch("repository", {})
|
|
116
|
+
object(source_repository, "repository")
|
|
117
|
+
repository = source_repository.to_h do |name, rule|
|
|
118
|
+
raise GrammarError, "invalid repository name" unless name.is_a?(String) && !name.empty?
|
|
119
|
+
|
|
120
|
+
[name.freeze, compile_rule(rule, 1)]
|
|
121
|
+
end.freeze
|
|
122
|
+
validate_includes(patterns, repository)
|
|
123
|
+
repository.each_value { |rule| validate_includes([rule], repository) }
|
|
124
|
+
tag = scope.split(".").last.to_s.gsub(/[^a-zA-Z0-9_+.-]/, "_")
|
|
125
|
+
raise GrammarError, "scopeName does not contain a usable lexer tag" if tag.empty?
|
|
126
|
+
|
|
127
|
+
Definition.new(scope_name: scope.freeze, tag: tag.downcase.freeze,
|
|
128
|
+
patterns: patterns, repository: repository).freeze
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
private
|
|
132
|
+
|
|
133
|
+
def compile_patterns(value, depth)
|
|
134
|
+
raise GrammarError, "patterns must be an Array" unless value.is_a?(Array)
|
|
135
|
+
raise ResourceLimitError, "tmLanguage nesting exceeds #{MAX_DEPTH}" if depth >= MAX_DEPTH
|
|
136
|
+
|
|
137
|
+
value.map { |rule| compile_rule(rule, depth + 1) }.freeze
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
def compile_rule(value, depth)
|
|
141
|
+
object(value, "grammar rule")
|
|
142
|
+
unsupported = value.keys & UNSUPPORTED_RULE_KEYS
|
|
143
|
+
raise GrammarError, "unsupported grammar rule key #{unsupported.first.inspect}" unless unsupported.empty?
|
|
144
|
+
unknown = value.keys - RULE_KEYS
|
|
145
|
+
raise GrammarError, "unsupported grammar rule key #{unknown.first.inspect}" unless unknown.empty?
|
|
146
|
+
@rule_count += 1
|
|
147
|
+
raise ResourceLimitError, "tmLanguage exceeds #{MAX_RULES} rules" if @rule_count > MAX_RULES
|
|
148
|
+
|
|
149
|
+
kinds = %w[include match begin].select { |key| value.key?(key) }
|
|
150
|
+
raise GrammarError, "grammar rule has incompatible operations" if kinds.length > 1
|
|
151
|
+
|
|
152
|
+
case kinds.first
|
|
153
|
+
when "include" then compile_include(value)
|
|
154
|
+
when "match" then compile_match(value)
|
|
155
|
+
when "begin" then compile_begin(value, depth)
|
|
156
|
+
else
|
|
157
|
+
raise GrammarError, "grammar rule must have one operation" unless value.key?("patterns")
|
|
158
|
+
|
|
159
|
+
compile_group(value, depth)
|
|
160
|
+
end
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
def compile_include(value)
|
|
164
|
+
target = string(value["include"], "include")
|
|
165
|
+
valid = %w[$self $base].include?(target) || target.match?(/\A#[^#\s]+\z/)
|
|
166
|
+
raise GrammarError, "external grammar include #{target.inspect} is unsupported" unless valid
|
|
167
|
+
raise GrammarError, "include rule has incompatible fields" unless (value.keys - %w[include comment]).empty?
|
|
168
|
+
|
|
169
|
+
IncludeRule.new(target: target.freeze).freeze
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
def compile_match(value)
|
|
173
|
+
allowed = %w[match captures name comment]
|
|
174
|
+
raise GrammarError, "match rule has incompatible fields" unless (value.keys - allowed).empty?
|
|
175
|
+
|
|
176
|
+
MatchRule.new(regexp: compile_regexp(value["match"]), token: optional_token(value["name"]),
|
|
177
|
+
captures: compile_captures(value["captures"])).freeze
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
def compile_begin(value, depth)
|
|
181
|
+
raise GrammarError, "begin rule requires end" unless value.key?("end")
|
|
182
|
+
|
|
183
|
+
regexp = compile_regexp(value["begin"])
|
|
184
|
+
ending = string(value["end"], "end")
|
|
185
|
+
compiled_ending = validate_regexp(dynamic_source(ending), "end")
|
|
186
|
+
shared = value["captures"]
|
|
187
|
+
BeginRule.new(regexp: regexp, end_source: ending.freeze, ending: compiled_ending,
|
|
188
|
+
dynamic_end: ending.match?(/(?<!\\)\\[1-9]/), token: optional_token(value["name"]),
|
|
189
|
+
content_token: optional_token(value["contentName"]),
|
|
190
|
+
begin_captures: compile_captures(value["beginCaptures"] || shared),
|
|
191
|
+
end_captures: compile_captures(value["endCaptures"] || shared),
|
|
192
|
+
patterns: compile_patterns(value.fetch("patterns", []), depth),
|
|
193
|
+
apply_end_last: boolean(value.fetch("applyEndPatternLast", false), "applyEndPatternLast")).freeze
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
def compile_group(value, depth)
|
|
197
|
+
raise GrammarError, "pattern group has incompatible fields" unless (value.keys - %w[patterns comment]).empty?
|
|
198
|
+
|
|
199
|
+
GroupRule.new(patterns: compile_patterns(value["patterns"], depth)).freeze
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
def compile_captures(value)
|
|
203
|
+
return {}.freeze if value.nil?
|
|
204
|
+
object(value, "captures")
|
|
205
|
+
|
|
206
|
+
value.to_h do |index, capture|
|
|
207
|
+
raise GrammarError, "capture keys must be decimal indexes" unless index.is_a?(String) && index.match?(/\A\d{1,3}\z/)
|
|
208
|
+
object(capture, "capture")
|
|
209
|
+
unknown = capture.keys - %w[name comment]
|
|
210
|
+
raise GrammarError, "unsupported capture key #{unknown.first.inspect}" unless unknown.empty?
|
|
211
|
+
|
|
212
|
+
[Integer(index, 10), scope_token(string(capture["name"], "capture name"))]
|
|
213
|
+
end.freeze
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
def compile_regexp(value)
|
|
217
|
+
source = string(value, "match")
|
|
218
|
+
validate_regexp(source, "match")
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
def validate_regexp(source, field)
|
|
222
|
+
raise ResourceLimitError, "#{field} regex exceeds #{MAX_REGEX_BYTES} bytes" if source.bytesize > MAX_REGEX_BYTES
|
|
223
|
+
|
|
224
|
+
Regexp.new(source)
|
|
225
|
+
rescue RegexpError => error
|
|
226
|
+
raise GrammarError, "invalid #{field} regex: #{error.message.byteslice(0, 256)}"
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
def dynamic_source(source)
|
|
230
|
+
source.gsub(/(?<!\\)\\[1-9]/, "x")
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
def validate_includes(patterns, repository, seen = {})
|
|
234
|
+
patterns.each do |rule|
|
|
235
|
+
if rule.is_a?(IncludeRule) && rule.target.start_with?("#")
|
|
236
|
+
name = rule.target.delete_prefix("#")
|
|
237
|
+
raise GrammarError, "unknown repository include #{rule.target.inspect}" unless repository.key?(name)
|
|
238
|
+
elsif rule.is_a?(BeginRule) || rule.is_a?(GroupRule)
|
|
239
|
+
next if seen[rule.object_id]
|
|
240
|
+
|
|
241
|
+
seen[rule.object_id] = true
|
|
242
|
+
validate_includes(rule.patterns, repository, seen)
|
|
243
|
+
end
|
|
244
|
+
end
|
|
245
|
+
end
|
|
246
|
+
|
|
247
|
+
def optional_token(value)
|
|
248
|
+
value.nil? ? nil : scope_token(string(value, "scope name"))
|
|
249
|
+
end
|
|
250
|
+
|
|
251
|
+
def scope_token(value)
|
|
252
|
+
Grammar.__send__(:token_for_scope, value)
|
|
253
|
+
end
|
|
254
|
+
|
|
255
|
+
def object(value, name)
|
|
256
|
+
raise GrammarError, "#{name} must be an object" unless value.is_a?(Hash) && value.keys.all? { |key| key.is_a?(String) }
|
|
257
|
+
end
|
|
258
|
+
|
|
259
|
+
def string(value, name)
|
|
260
|
+
raise GrammarError, "#{name} must be a nonempty String" unless value.is_a?(String) && !value.empty?
|
|
261
|
+
|
|
262
|
+
value
|
|
263
|
+
end
|
|
264
|
+
|
|
265
|
+
def boolean(value, name)
|
|
266
|
+
raise GrammarError, "#{name} must be true or false" unless value == true || value == false
|
|
267
|
+
|
|
268
|
+
value
|
|
269
|
+
end
|
|
270
|
+
end
|
|
271
|
+
private_constant :Compiler
|
|
272
|
+
private_class_method :parse, :token_for_scope
|
|
273
|
+
end
|
|
274
|
+
end
|
data/lib/antares/highlighter.rb
CHANGED
|
@@ -51,6 +51,17 @@ module Antares
|
|
|
51
51
|
indexes.map { |index| @tokens.fetch(index) { plain(index) } }
|
|
52
52
|
end
|
|
53
53
|
|
|
54
|
+
def structure
|
|
55
|
+
@structure ||= Structure.build(
|
|
56
|
+
language: @template.class.tag,
|
|
57
|
+
lines: @lines,
|
|
58
|
+
line_count: @line_count,
|
|
59
|
+
tokens_for: method(:tokens_for),
|
|
60
|
+
tokens_in: method(:tokens_in),
|
|
61
|
+
stabilize: method(:stabilize_structure)
|
|
62
|
+
)
|
|
63
|
+
end
|
|
64
|
+
|
|
54
65
|
# Provider contents must already reflect this line edit. Indices are zero based.
|
|
55
66
|
def edit(from_line:, removed:, inserted:)
|
|
56
67
|
values = [from_line, removed, inserted]
|
|
@@ -58,10 +69,16 @@ module Antares
|
|
|
58
69
|
raise RangeError, "edit outside previous document" unless from_line <= @count && from_line + removed <= @count
|
|
59
70
|
updated_count = count
|
|
60
71
|
raise ArgumentError, "line_count disagrees with edit" unless updated_count == @count - removed + inserted
|
|
72
|
+
separator_line = if from_line.positive? &&
|
|
73
|
+
((from_line == @count && inserted.positive?) || (from_line + removed == @count && inserted.zero? && removed.positive?))
|
|
74
|
+
from_line - 1
|
|
75
|
+
end
|
|
76
|
+
restart_from = separator_line || from_line
|
|
77
|
+
source_edit = @source_edit ? nil : (@source && [@source, @offsets, @count, from_line, removed, inserted])
|
|
61
78
|
@last_scanned_lines = 0
|
|
62
79
|
if strategy == :incremental
|
|
63
80
|
@tokens.delete_if { |line, _| line >= frontier } if @old_fingerprints
|
|
64
|
-
start = @checkpoints.keys.select { |line| line <=
|
|
81
|
+
start = @checkpoints.keys.select { |line| line <= restart_from && line <= frontier }.max || 0
|
|
65
82
|
restart = @checkpoints.fetch(start)
|
|
66
83
|
shift_cache(@tokens, from_line, removed, inserted)
|
|
67
84
|
shift_cache(@fingerprints, from_line, removed, inserted)
|
|
@@ -79,7 +96,10 @@ module Antares
|
|
|
79
96
|
@frontier = 0
|
|
80
97
|
end
|
|
81
98
|
@count = updated_count
|
|
99
|
+
@source_edit = source_edit
|
|
82
100
|
@source = @offsets = @driver = nil
|
|
101
|
+
@structure&.edit(from_line: from_line, removed: removed, inserted: inserted)
|
|
102
|
+
@structure&.edit(from_line: separator_line, removed: 1, inserted: 1) if separator_line
|
|
83
103
|
self
|
|
84
104
|
end
|
|
85
105
|
|
|
@@ -146,22 +166,70 @@ module Antares
|
|
|
146
166
|
def source_line(index)
|
|
147
167
|
value = @lines.call(index)
|
|
148
168
|
raise TypeError, "line provider must return a String" unless value.is_a?(String)
|
|
149
|
-
|
|
150
|
-
|
|
169
|
+
encoding = value.encoding
|
|
170
|
+
unless value.valid_encoding? && (encoding == Encoding::UTF_8 || encoding == Encoding::US_ASCII)
|
|
171
|
+
raise EncodingError, "line provider must return valid UTF-8"
|
|
172
|
+
end
|
|
173
|
+
newline = value.index("\n")
|
|
174
|
+
raise ArgumentError, "line provider returned multiple logical lines" if newline && newline != value.length - 1
|
|
151
175
|
# Providers may omit separators, but an empty final line remains empty.
|
|
152
|
-
index < @count - 1 &&
|
|
176
|
+
index < @count - 1 && newline.nil? ? value + "\n" : value
|
|
153
177
|
end
|
|
154
178
|
|
|
155
179
|
def build_source
|
|
156
180
|
return if @source
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
181
|
+
if @source_edit
|
|
182
|
+
build_edited_source
|
|
183
|
+
@source_edit = nil
|
|
184
|
+
return
|
|
185
|
+
end
|
|
186
|
+
parts = Array.new(@count)
|
|
187
|
+
offsets = Array.new(@count + 1, 0)
|
|
188
|
+
bytes = 0
|
|
189
|
+
index = 0
|
|
190
|
+
while index < @count
|
|
191
|
+
line = source_line(index)
|
|
192
|
+
line_bytes = line.bytesize
|
|
193
|
+
raise ResourceLimitError, "line exceeds #{@max_line_bytes} bytes" if line_bytes > @max_line_bytes
|
|
194
|
+
bytes += line_bytes
|
|
195
|
+
raise ResourceLimitError, "document exceeds #{@max_bytes} bytes" if bytes > @max_bytes
|
|
196
|
+
parts[index] = line
|
|
197
|
+
offsets[index + 1] = bytes
|
|
198
|
+
index += 1
|
|
199
|
+
end
|
|
200
|
+
@source, @offsets = parts.join.freeze, offsets.freeze
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
def build_edited_source
|
|
204
|
+
old_source, old_offsets, old_count, from_line, removed, inserted = @source_edit
|
|
205
|
+
first = [from_line - 1, 0].max
|
|
206
|
+
old_suffix = from_line + removed
|
|
207
|
+
new_suffix = from_line + inserted
|
|
208
|
+
prefix_bytes = old_offsets.fetch(first)
|
|
209
|
+
suffix_bytes = old_offsets.fetch(old_suffix)
|
|
210
|
+
offsets = old_offsets[0..first]
|
|
211
|
+
middle = []
|
|
212
|
+
bytes = prefix_bytes
|
|
213
|
+
index = first
|
|
214
|
+
while index < new_suffix
|
|
160
215
|
line = source_line(index)
|
|
161
|
-
|
|
162
|
-
raise ResourceLimitError, "
|
|
163
|
-
|
|
164
|
-
|
|
216
|
+
line_bytes = line.bytesize
|
|
217
|
+
raise ResourceLimitError, "line exceeds #{@max_line_bytes} bytes" if line_bytes > @max_line_bytes
|
|
218
|
+
bytes += line_bytes
|
|
219
|
+
middle << line
|
|
220
|
+
offsets << bytes
|
|
221
|
+
index += 1
|
|
222
|
+
end
|
|
223
|
+
total = bytes + old_source.bytesize - suffix_bytes
|
|
224
|
+
raise ResourceLimitError, "document exceeds #{@max_bytes} bytes" if total > @max_bytes
|
|
225
|
+
|
|
226
|
+
source = String.new(capacity: total)
|
|
227
|
+
source << old_source.byteslice(0, prefix_bytes) << middle.join << old_source.byteslice(suffix_bytes..)
|
|
228
|
+
delta = bytes - suffix_bytes
|
|
229
|
+
boundary = old_suffix + 1
|
|
230
|
+
while boundary <= old_count
|
|
231
|
+
offsets << old_offsets.fetch(boundary) + delta
|
|
232
|
+
boundary += 1
|
|
165
233
|
end
|
|
166
234
|
@source, @offsets = source.freeze, offsets.freeze
|
|
167
235
|
end
|
|
@@ -207,8 +275,9 @@ module Antares
|
|
|
207
275
|
|
|
208
276
|
def advance_window(first, last)
|
|
209
277
|
Timeout.timeout(@max_seconds) { window_tokens(first, last) }
|
|
210
|
-
rescue Timeout::Error
|
|
211
|
-
@fallback_reason =
|
|
278
|
+
rescue Timeout::Error, ResourceLimitError => error
|
|
279
|
+
@fallback_reason = error.is_a?(Timeout::Error) ?
|
|
280
|
+
"window lexing exceeded #{@max_seconds} seconds" : error.message
|
|
212
281
|
(first..last).each { |index| @tokens[index] = plain(index) }
|
|
213
282
|
@frontier = last + 1
|
|
214
283
|
end
|
|
@@ -260,6 +329,7 @@ module Antares
|
|
|
260
329
|
|
|
261
330
|
def fallback!(strategy, reason)
|
|
262
331
|
@strategy, @fallback_reason = strategy, reason
|
|
332
|
+
@source_edit = nil
|
|
263
333
|
@driver = @source = @offsets = nil
|
|
264
334
|
@tokens.clear
|
|
265
335
|
@checkpoints.clear
|
|
@@ -270,6 +340,10 @@ module Antares
|
|
|
270
340
|
|
|
271
341
|
def shift_cache(cache, first, removed, inserted)
|
|
272
342
|
delta = inserted - removed
|
|
343
|
+
if delta.zero?
|
|
344
|
+
removed.times { |offset| cache.delete(first + offset) }
|
|
345
|
+
return
|
|
346
|
+
end
|
|
273
347
|
shifted = {}
|
|
274
348
|
cache.each do |line, value|
|
|
275
349
|
if line < first
|
|
@@ -280,5 +354,11 @@ module Antares
|
|
|
280
354
|
end
|
|
281
355
|
cache.replace(shifted)
|
|
282
356
|
end
|
|
357
|
+
|
|
358
|
+
def stabilize_structure(from_line)
|
|
359
|
+
return 0 if @count.zero?
|
|
360
|
+
advance(until_line: @count - 1, from_line: from_line)
|
|
361
|
+
strategy == :incremental && @driver ? [@driver.line, @count].min : @count
|
|
362
|
+
end
|
|
283
363
|
end
|
|
284
364
|
end
|