ruby_tree_sitter 1.6.0-aarch64-linux-gnu
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/LICENSE +22 -0
- data/README.md +213 -0
- data/ext/tree_sitter/encoding.c +29 -0
- data/ext/tree_sitter/extconf.rb +149 -0
- data/ext/tree_sitter/input.c +127 -0
- data/ext/tree_sitter/input_edit.c +42 -0
- data/ext/tree_sitter/language.c +219 -0
- data/ext/tree_sitter/logger.c +228 -0
- data/ext/tree_sitter/macros.h +163 -0
- data/ext/tree_sitter/node.c +623 -0
- data/ext/tree_sitter/parser.c +398 -0
- data/ext/tree_sitter/point.c +26 -0
- data/ext/tree_sitter/quantifier.c +43 -0
- data/ext/tree_sitter/query.c +289 -0
- data/ext/tree_sitter/query_capture.c +28 -0
- data/ext/tree_sitter/query_cursor.c +231 -0
- data/ext/tree_sitter/query_error.c +41 -0
- data/ext/tree_sitter/query_match.c +44 -0
- data/ext/tree_sitter/query_predicate_step.c +83 -0
- data/ext/tree_sitter/range.c +35 -0
- data/ext/tree_sitter/repo.rb +128 -0
- data/ext/tree_sitter/symbol_type.c +46 -0
- data/ext/tree_sitter/tree.c +234 -0
- data/ext/tree_sitter/tree_cursor.c +269 -0
- data/ext/tree_sitter/tree_sitter.c +44 -0
- data/ext/tree_sitter/tree_sitter.h +107 -0
- data/lib/tree_sitter/3.0/tree_sitter.so +0 -0
- data/lib/tree_sitter/3.1/tree_sitter.so +0 -0
- data/lib/tree_sitter/3.2/tree_sitter.so +0 -0
- data/lib/tree_sitter/3.3/tree_sitter.so +0 -0
- data/lib/tree_sitter/helpers.rb +23 -0
- data/lib/tree_sitter/mixins/language.rb +167 -0
- data/lib/tree_sitter/node.rb +167 -0
- data/lib/tree_sitter/query.rb +191 -0
- data/lib/tree_sitter/query_captures.rb +30 -0
- data/lib/tree_sitter/query_cursor.rb +27 -0
- data/lib/tree_sitter/query_match.rb +100 -0
- data/lib/tree_sitter/query_matches.rb +39 -0
- data/lib/tree_sitter/query_predicate.rb +14 -0
- data/lib/tree_sitter/text_predicate_capture.rb +37 -0
- data/lib/tree_sitter/version.rb +8 -0
- data/lib/tree_sitter.rb +34 -0
- data/lib/tree_stand/ast_modifier.rb +30 -0
- data/lib/tree_stand/breadth_first_visitor.rb +54 -0
- data/lib/tree_stand/config.rb +19 -0
- data/lib/tree_stand/node.rb +351 -0
- data/lib/tree_stand/parser.rb +87 -0
- data/lib/tree_stand/range.rb +55 -0
- data/lib/tree_stand/tree.rb +123 -0
- data/lib/tree_stand/utils/printer.rb +73 -0
- data/lib/tree_stand/version.rb +7 -0
- data/lib/tree_stand/visitor.rb +127 -0
- data/lib/tree_stand/visitors/tree_walker.rb +37 -0
- data/lib/tree_stand.rb +48 -0
- data/tree_sitter.gemspec +34 -0
- metadata +135 -0
@@ -0,0 +1,54 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
# typed: true
|
3
|
+
|
4
|
+
module TreeStand
|
5
|
+
# Breadth-first traversal through the tree, calling hooks at each stop.
|
6
|
+
class BreadthFirstVisitor
|
7
|
+
extend T::Sig
|
8
|
+
|
9
|
+
sig { params(node: TreeStand::Node).void }
|
10
|
+
def initialize(node)
|
11
|
+
@node = node
|
12
|
+
end
|
13
|
+
|
14
|
+
# Run the visitor on the document and return self. Allows chaining create and visit.
|
15
|
+
# @example
|
16
|
+
# visitor = CountingVisitor.new(node, :predicate).visit
|
17
|
+
sig { returns(T.self_type) }
|
18
|
+
def visit
|
19
|
+
queue = [@node]
|
20
|
+
visit_node(queue) while queue.any?
|
21
|
+
self
|
22
|
+
end
|
23
|
+
|
24
|
+
# @abstract The default implementation does nothing.
|
25
|
+
sig { overridable.params(node: TreeStand::Node).void }
|
26
|
+
def on(node) = nil
|
27
|
+
|
28
|
+
# @abstract The default implementation yields to visit all children.
|
29
|
+
sig { overridable.params(node: TreeStand::Node, block: T.proc.void).void }
|
30
|
+
def around(node, &block) = yield
|
31
|
+
|
32
|
+
private
|
33
|
+
|
34
|
+
def visit_node(queue)
|
35
|
+
node = queue.shift
|
36
|
+
|
37
|
+
if respond_to?("on_#{node.type}")
|
38
|
+
public_send("on_#{node.type}", node)
|
39
|
+
else
|
40
|
+
on(node)
|
41
|
+
end
|
42
|
+
|
43
|
+
if respond_to?("around_#{node.type}")
|
44
|
+
public_send("around_#{node.type}", node) do
|
45
|
+
node.each { |child| queue << child }
|
46
|
+
end
|
47
|
+
else
|
48
|
+
around(node) do
|
49
|
+
node.each { |child| queue << child }
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
# typed: true
|
3
|
+
|
4
|
+
require 'pathname'
|
5
|
+
|
6
|
+
module TreeStand
|
7
|
+
# Global configuration for the gem.
|
8
|
+
# @api private
|
9
|
+
class Config
|
10
|
+
extend T::Sig
|
11
|
+
|
12
|
+
sig { returns(T.nilable(Pathname)) }
|
13
|
+
attr_reader :parser_path
|
14
|
+
|
15
|
+
def parser_path=(path)
|
16
|
+
@parser_path = Pathname(path)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,351 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
# typed: true
|
3
|
+
|
4
|
+
module TreeStand
|
5
|
+
# Wrapper around a TreeSitter node and provides convient
|
6
|
+
# methods that are missing on the original node. This class
|
7
|
+
# overrides the `method_missing` method to delegate to a nodes
|
8
|
+
# named children.
|
9
|
+
class Node
|
10
|
+
extend T::Sig
|
11
|
+
extend Forwardable
|
12
|
+
include Enumerable
|
13
|
+
|
14
|
+
# @!method changed?
|
15
|
+
# @return [Boolean] true if a syntax node has been edited.
|
16
|
+
# @!method child_count
|
17
|
+
# @return [Integer] the number of child nodes.
|
18
|
+
# @!method extra?
|
19
|
+
# @return [Boolean] true if the node is *extra* (e.g. comments).
|
20
|
+
# @!method has_error?
|
21
|
+
# @return [Boolean] true if the node is a syntax error or contains any syntax errors.
|
22
|
+
# @!method missing?
|
23
|
+
# @return [Boolean] true if the parser inserted that node to recover from error.
|
24
|
+
# @!method named?
|
25
|
+
# @return [Boolean] true if the node is not a literal in the grammar.
|
26
|
+
# @!method named_child_count
|
27
|
+
# @return [Integer] the number of *named* children.
|
28
|
+
# @!method type
|
29
|
+
# @return [Symbol] the type of the node in the tree-sitter grammar.
|
30
|
+
# @!method error?
|
31
|
+
# @return [bool] true if the node is an error node.
|
32
|
+
def_delegators(
|
33
|
+
:@ts_node,
|
34
|
+
:changed?,
|
35
|
+
:child_count,
|
36
|
+
:error?,
|
37
|
+
:extra?,
|
38
|
+
:has_error?,
|
39
|
+
:missing?,
|
40
|
+
:named?,
|
41
|
+
:named_child_count,
|
42
|
+
:type,
|
43
|
+
)
|
44
|
+
|
45
|
+
# These are methods defined in {TreeStand::Node} but map to something
|
46
|
+
# in {TreeSitter::Node}, because we want a more idiomatic API.
|
47
|
+
THINLY_REMAPPED_METHODS = {
|
48
|
+
'[]': :[],
|
49
|
+
fetch: :fetch,
|
50
|
+
field: :child_by_field_name,
|
51
|
+
next: :next_sibling,
|
52
|
+
prev: :prev_sibling,
|
53
|
+
next_named: :next_named_sibling,
|
54
|
+
prev_named: :prev_named_sibling,
|
55
|
+
field_names: :fields,
|
56
|
+
}.freeze
|
57
|
+
|
58
|
+
# These are methods from {TreeSitter} that are thinly wrapped to create
|
59
|
+
# {TreeStand::Node} instead.
|
60
|
+
THINLY_WRAPPED_METHODS = (
|
61
|
+
%i[
|
62
|
+
child
|
63
|
+
named_child
|
64
|
+
parent
|
65
|
+
] + THINLY_REMAPPED_METHODS.keys
|
66
|
+
).freeze
|
67
|
+
|
68
|
+
sig { returns(TreeStand::Tree) }
|
69
|
+
attr_reader :tree
|
70
|
+
|
71
|
+
sig { returns(TreeSitter::Node) }
|
72
|
+
attr_reader :ts_node
|
73
|
+
|
74
|
+
# @api private
|
75
|
+
sig { params(tree: TreeStand::Tree, ts_node: TreeSitter::Node).void }
|
76
|
+
def initialize(tree, ts_node)
|
77
|
+
@tree = tree
|
78
|
+
@ts_node = ts_node
|
79
|
+
end
|
80
|
+
|
81
|
+
# TreeSitter uses a `TreeSitter::Cursor` to iterate over matches by calling
|
82
|
+
# `curser#next_match` repeatedly until it returns `nil`.
|
83
|
+
#
|
84
|
+
# This method does all of that for you and collects all of the matches into
|
85
|
+
# an array and each corresponding capture into a hash.
|
86
|
+
#
|
87
|
+
# @example
|
88
|
+
# # This will return a match for each identifier nodes in the tree.
|
89
|
+
# tree_matches = tree.query(<<~QUERY)
|
90
|
+
# (identifier) @identifier
|
91
|
+
# QUERY
|
92
|
+
#
|
93
|
+
# # It is equivalent to:
|
94
|
+
# tree.root_node.query(<<~QUERY)
|
95
|
+
# (identifier) @identifier
|
96
|
+
# QUERY
|
97
|
+
sig { params(query_string: String).returns(T::Array[T::Hash[String, TreeStand::Node]]) }
|
98
|
+
def query(query_string)
|
99
|
+
ts_query = TreeSitter::Query.new(@tree.parser.ts_language, query_string)
|
100
|
+
TreeSitter::QueryCursor
|
101
|
+
.new
|
102
|
+
.matches(ts_query, @tree.ts_tree.root_node, @tree.document)
|
103
|
+
.each_capture_hash
|
104
|
+
.map { |h| h.transform_values! { |n| TreeStand::Node.new(@tree, n) } }
|
105
|
+
end
|
106
|
+
|
107
|
+
# Returns the first captured node that matches the query string or nil if
|
108
|
+
# there was no captured node.
|
109
|
+
#
|
110
|
+
# @example Find the first identifier node.
|
111
|
+
# identifier_node = tree.root_node.find_node("(identifier) @identifier")
|
112
|
+
#
|
113
|
+
# @see #find_node!
|
114
|
+
# @see #query
|
115
|
+
sig { params(query_string: String).returns(T.nilable(TreeStand::Node)) }
|
116
|
+
def find_node(query_string)
|
117
|
+
query(query_string).first&.values&.first
|
118
|
+
end
|
119
|
+
|
120
|
+
# Like {#find_node}, except that if no node is found, raises an
|
121
|
+
# {TreeStand::NodeNotFound} error.
|
122
|
+
#
|
123
|
+
# @see #find_node
|
124
|
+
# @raise [TreeStand::NodeNotFound]
|
125
|
+
sig { params(query_string: String).returns(TreeStand::Node) }
|
126
|
+
def find_node!(query_string)
|
127
|
+
find_node(query_string) || raise(TreeStand::NodeNotFound)
|
128
|
+
end
|
129
|
+
|
130
|
+
sig { returns(TreeStand::Range) }
|
131
|
+
def range
|
132
|
+
TreeStand::Range.new(
|
133
|
+
start_byte: @ts_node.start_byte,
|
134
|
+
end_byte: @ts_node.end_byte,
|
135
|
+
start_point: @ts_node.start_point,
|
136
|
+
end_point: @ts_node.end_point,
|
137
|
+
)
|
138
|
+
end
|
139
|
+
|
140
|
+
# Node includes enumerable so that you can iterate over the child nodes.
|
141
|
+
# @example
|
142
|
+
# node.text # => "3 * 4"
|
143
|
+
#
|
144
|
+
# @example Iterate over the child nodes
|
145
|
+
# node.each do |child|
|
146
|
+
# print child.text
|
147
|
+
# end
|
148
|
+
# # prints: 3*4
|
149
|
+
#
|
150
|
+
# @example Enumerable methods
|
151
|
+
# node.map(&:text) # => ["3", "*", "4"]
|
152
|
+
#
|
153
|
+
# @yieldparam child [TreeStand::Node]
|
154
|
+
sig do
|
155
|
+
override
|
156
|
+
.params(block: T.nilable(T.proc.params(node: TreeStand::Node).returns(BasicObject)))
|
157
|
+
.returns(T::Enumerator[TreeStand::Node])
|
158
|
+
end
|
159
|
+
def each(&block)
|
160
|
+
enumerator = Enumerator.new do |yielder|
|
161
|
+
@ts_node.each do |child|
|
162
|
+
yielder << TreeStand::Node.new(@tree, child)
|
163
|
+
end
|
164
|
+
end
|
165
|
+
enumerator.each(&block) if block_given?
|
166
|
+
enumerator
|
167
|
+
end
|
168
|
+
|
169
|
+
# Enumerate named children.
|
170
|
+
# @example
|
171
|
+
# node.text # => "3 * 4"
|
172
|
+
#
|
173
|
+
# @example Iterate over the child nodes
|
174
|
+
# node.each_named do |child|
|
175
|
+
# print child.text
|
176
|
+
# end
|
177
|
+
# # prints: 34
|
178
|
+
#
|
179
|
+
# @example Enumerable methods
|
180
|
+
# node.each_named.map(&:text) # => ["3", "4"]
|
181
|
+
#
|
182
|
+
# @yieldparam child [TreeStand::Node]
|
183
|
+
sig do
|
184
|
+
params(block: T.nilable(T.proc.params(node: TreeStand::Node).returns(BasicObject)))
|
185
|
+
.returns(T::Enumerator[TreeStand::Node])
|
186
|
+
end
|
187
|
+
def each_named(&block)
|
188
|
+
enumerator = Enumerator.new do |yielder|
|
189
|
+
@ts_node.each_named do |child|
|
190
|
+
yielder << TreeStand::Node.new(@tree, child)
|
191
|
+
end
|
192
|
+
end
|
193
|
+
enumerator.each(&block) if block_given?
|
194
|
+
enumerator
|
195
|
+
end
|
196
|
+
|
197
|
+
# Iterate of (field, child).
|
198
|
+
#
|
199
|
+
# @example
|
200
|
+
# node.text # => "3 * 4"
|
201
|
+
#
|
202
|
+
# @example Iterate over the child nodes
|
203
|
+
# node.each_field do |field, child|
|
204
|
+
# puts "#{field}: #{child.text}"
|
205
|
+
# end
|
206
|
+
# # prints:
|
207
|
+
# # left: 3
|
208
|
+
# # right: 4
|
209
|
+
#
|
210
|
+
# @example Enumerable methods
|
211
|
+
# node.each_field.map { |f, c| "#{f}: #{c}" } # => ["left: 3", "right: 4"]
|
212
|
+
#
|
213
|
+
# @yieldparam field [Symbol]
|
214
|
+
# @yieldparam child [TreeStand::Node]
|
215
|
+
sig do
|
216
|
+
params(block: T.nilable(T.proc.params(node: TreeStand::Node).returns(BasicObject)))
|
217
|
+
.returns(T::Enumerator[[Symbol, TreeStand::Node]])
|
218
|
+
end
|
219
|
+
def each_field(&block)
|
220
|
+
enumerator = Enumerator.new do |yielder|
|
221
|
+
@ts_node.each_field do |field, child|
|
222
|
+
yielder << [field.to_sym, TreeStand::Node.new(@tree, child)]
|
223
|
+
end
|
224
|
+
end
|
225
|
+
enumerator.each(&block) if block_given?
|
226
|
+
enumerator
|
227
|
+
end
|
228
|
+
|
229
|
+
# @example Enumerable methods
|
230
|
+
# node.named.map(&:text) # => ["3", "4"]
|
231
|
+
alias_method :named, :each_named
|
232
|
+
|
233
|
+
# @example Enumerable methods
|
234
|
+
# node.fields.map { |f, c| "#{f}: #{c}" } # => ["left: 3", "right: 4"]
|
235
|
+
alias_method :fields, :each_field
|
236
|
+
|
237
|
+
# (see TreeStand::Visitors::TreeWalker)
|
238
|
+
# Backed by {TreeStand::Visitors::TreeWalker}.
|
239
|
+
#
|
240
|
+
# @example Check the subtree for error nodes
|
241
|
+
# node.walk.any? { |node| node.type == :error }
|
242
|
+
#
|
243
|
+
# @see TreeStand::Visitors::TreeWalker
|
244
|
+
#
|
245
|
+
# @yieldparam node [TreeStand::Node]
|
246
|
+
sig do
|
247
|
+
params(block: T.nilable(T.proc.params(node: TreeStand::Node).returns(BasicObject)))
|
248
|
+
.returns(T::Enumerator[TreeStand::Node])
|
249
|
+
end
|
250
|
+
def walk(&block)
|
251
|
+
enumerator = Enumerator.new do |yielder|
|
252
|
+
Visitors::TreeWalker.new(self) do |child|
|
253
|
+
yielder << child
|
254
|
+
end.visit
|
255
|
+
end
|
256
|
+
enumerator.each(&block) if block_given?
|
257
|
+
enumerator
|
258
|
+
end
|
259
|
+
|
260
|
+
# @example
|
261
|
+
# node.text # => "3 * 4"
|
262
|
+
# node.to_a.map(&:text) # => ["3", "*", "4"]
|
263
|
+
# node.children.map(&:text) # => ["3", "*", "4"]
|
264
|
+
sig { returns(T::Array[TreeStand::Node]) }
|
265
|
+
def children = to_a
|
266
|
+
|
267
|
+
# A convenience method for getting the text of the node. Each {TreeStand::Node}
|
268
|
+
# wraps the parent {TreeStand::Tree #tree} and has access to the source document.
|
269
|
+
sig { returns(String) }
|
270
|
+
def text
|
271
|
+
T.must(@tree.document.byteslice(@ts_node.start_byte...@ts_node.end_byte))
|
272
|
+
end
|
273
|
+
|
274
|
+
# This class overrides the `method_missing` method to delegate to the
|
275
|
+
# node's named children.
|
276
|
+
# @example
|
277
|
+
# node.text # => "3 * 4"
|
278
|
+
#
|
279
|
+
# node.left.text # => "3"
|
280
|
+
# node.operator.text # => "*"
|
281
|
+
# node.right.text # => "4"
|
282
|
+
# node.operand # => NoMethodError
|
283
|
+
# @overload method_missing(field_name)
|
284
|
+
# @param name [Symbol, String]
|
285
|
+
# @return [TreeStand::Node] child node for the given field name
|
286
|
+
# @raise [NoMethodError] Raised if the node does not have child with name `field_name`
|
287
|
+
#
|
288
|
+
# @overload method_missing(method_name, *args, &block)
|
289
|
+
# @raise [NoMethodError]
|
290
|
+
def method_missing(method, *args, **kwargs, &block)
|
291
|
+
if thinly_wrapped?(method)
|
292
|
+
from(
|
293
|
+
T.unsafe(@ts_node)
|
294
|
+
.public_send(
|
295
|
+
THINLY_REMAPPED_METHODS[method] || method,
|
296
|
+
*args,
|
297
|
+
**kwargs,
|
298
|
+
&block
|
299
|
+
),
|
300
|
+
)
|
301
|
+
else
|
302
|
+
super
|
303
|
+
end
|
304
|
+
end
|
305
|
+
|
306
|
+
sig { params(other: Object).returns(T::Boolean) }
|
307
|
+
def ==(other)
|
308
|
+
return false unless other.is_a?(TreeStand::Node)
|
309
|
+
|
310
|
+
T.must(range == other.range && type == other.type && text == other.text)
|
311
|
+
end
|
312
|
+
|
313
|
+
# (see TreeStand::Utils::Printer)
|
314
|
+
# Backed by {TreeStand::Utils::Printer}.
|
315
|
+
#
|
316
|
+
# @see TreeStand::Utils::Printer
|
317
|
+
sig { params(pp: PP).void }
|
318
|
+
def pretty_print(pp)
|
319
|
+
Utils::Printer.new(ralign: 80).print(self, io: pp.output)
|
320
|
+
end
|
321
|
+
|
322
|
+
private
|
323
|
+
|
324
|
+
def respond_to_missing?(method, *_args, **_kwargs)
|
325
|
+
thinly_wrapped?(method) || super
|
326
|
+
end
|
327
|
+
|
328
|
+
def thinly_wrapped?(method)
|
329
|
+
@ts_node.fields.include?(method) || THINLY_WRAPPED_METHODS.include?(method)
|
330
|
+
end
|
331
|
+
|
332
|
+
# FIXME: Make more generic if needed in other classes.
|
333
|
+
|
334
|
+
# 1 instance of {TreeStand} from a {TreeSitter} equivalent.
|
335
|
+
def from_a(node)
|
336
|
+
node.is_a?(TreeSitter::Node) ? TreeStand::Node.new(@tree, node) : node
|
337
|
+
end
|
338
|
+
|
339
|
+
# {TreeSitter} instance, or a collection ({Array, Hash})
|
340
|
+
def from(obj)
|
341
|
+
case obj
|
342
|
+
when Array
|
343
|
+
obj.map { |n| from(n) }
|
344
|
+
when Hash
|
345
|
+
obj.to_h { |k, v| [from(k), from(v)] }
|
346
|
+
else
|
347
|
+
from_a(obj)
|
348
|
+
end
|
349
|
+
end
|
350
|
+
end
|
351
|
+
end
|
@@ -0,0 +1,87 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
# typed: true
|
3
|
+
|
4
|
+
require 'pathname'
|
5
|
+
|
6
|
+
module TreeStand
|
7
|
+
# Wrapper around the TreeSitter parser. It looks up the parser by filename in
|
8
|
+
# the configured parsers directory.
|
9
|
+
# @example
|
10
|
+
# TreeStand.configure do
|
11
|
+
# config.parser_path = "path/to/parser/folder/"
|
12
|
+
# end
|
13
|
+
#
|
14
|
+
# # Looks for a parser in `path/to/parser/folder/sql.{so,dylib}`
|
15
|
+
# sql_parser = TreeStand::Parser.new("sql")
|
16
|
+
#
|
17
|
+
# # Looks for a parser in `path/to/parser/folder/ruby.{so,dylib}`
|
18
|
+
# ruby_parser = TreeStand::Parser.new("ruby")
|
19
|
+
#
|
20
|
+
# If no {TreeStand::Config#parser_path} is setup, {TreeStand} will lookup in a
|
21
|
+
# set of default paths. You can always override any configuration by passing
|
22
|
+
# the environment variable `TREE_SITTER_PARSERS` (colon-separated).
|
23
|
+
#
|
24
|
+
# @see language
|
25
|
+
# @see search_for_lib
|
26
|
+
# @see LIBDIRS
|
27
|
+
class Parser
|
28
|
+
extend T::Sig
|
29
|
+
extend TreeSitter::Mixins::Language
|
30
|
+
|
31
|
+
sig { returns(TreeSitter::Language) }
|
32
|
+
attr_reader :ts_language
|
33
|
+
|
34
|
+
sig { returns(TreeSitter::Parser) }
|
35
|
+
attr_reader :ts_parser
|
36
|
+
|
37
|
+
# @param language [String]
|
38
|
+
sig { params(language: String).void }
|
39
|
+
def initialize(language)
|
40
|
+
@ts_language = Parser.language(language)
|
41
|
+
@ts_parser = TreeSitter::Parser.new.tap do |parser|
|
42
|
+
parser.language = @ts_language
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
# The library directories we need to look into.
|
47
|
+
#
|
48
|
+
# @return [Array<Pathname>] the list of candidate places to use when searching for parsers.
|
49
|
+
#
|
50
|
+
# @see ENV_PARSERS
|
51
|
+
# @see LIBDIRS
|
52
|
+
def self.lib_dirs
|
53
|
+
[
|
54
|
+
*TreeSitter::ENV_PARSERS,
|
55
|
+
*(TreeStand.config.parser_path ? [TreeStand.config.parser_path] : TreeSitter::LIBDIRS),
|
56
|
+
]
|
57
|
+
end
|
58
|
+
|
59
|
+
# Parse the provided document with the TreeSitter parser.
|
60
|
+
# @param tree [TreeStand::Tree, nil] providing the old tree will allow the
|
61
|
+
# parser to take advantage of incremental parsing and improve performance
|
62
|
+
# by re-useing nodes from the old tree.
|
63
|
+
sig { params(document: String, tree: T.nilable(TreeStand::Tree)).returns(TreeStand::Tree) }
|
64
|
+
def parse_string(document, tree: nil)
|
65
|
+
# @todo There's a bug with passing a non-nil tree
|
66
|
+
ts_tree = @ts_parser.parse_string(nil, document)
|
67
|
+
TreeStand::Tree.new(self, ts_tree, document)
|
68
|
+
end
|
69
|
+
|
70
|
+
# (see #parse_string)
|
71
|
+
# @note Like {#parse_string}, except that if the tree contains any parse
|
72
|
+
# errors, raises an {TreeStand::InvalidDocument} error.
|
73
|
+
#
|
74
|
+
# @see #parse_string
|
75
|
+
# @raise [TreeStand::InvalidDocument]
|
76
|
+
sig { params(document: String, tree: T.nilable(TreeStand::Tree)).returns(TreeStand::Tree) }
|
77
|
+
def parse_string!(document, tree: nil)
|
78
|
+
tree = parse_string(document, tree: tree)
|
79
|
+
return tree unless tree.any?(&:error?)
|
80
|
+
|
81
|
+
raise(InvalidDocument, <<~ERROR)
|
82
|
+
Encountered errors in the document. Check the tree for more details.
|
83
|
+
#{tree}
|
84
|
+
ERROR
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
@@ -0,0 +1,55 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
# typed: true
|
3
|
+
|
4
|
+
module TreeStand
|
5
|
+
# Wrapper around a TreeSitter range. This is mainly used to compare ranges.
|
6
|
+
class Range
|
7
|
+
extend T::Sig
|
8
|
+
|
9
|
+
# Point is a Struct containing the row and column from a TreeSitter point.
|
10
|
+
# TreeStand uses this to compare points.
|
11
|
+
# @!attribute [rw] row
|
12
|
+
# @return [Integer]
|
13
|
+
# @!attribute [rw] column
|
14
|
+
# @return [Integer]
|
15
|
+
Point = Struct.new(:row, :column)
|
16
|
+
|
17
|
+
sig { returns(Integer) }
|
18
|
+
attr_reader :start_byte
|
19
|
+
|
20
|
+
sig { returns(Integer) }
|
21
|
+
attr_reader :end_byte
|
22
|
+
|
23
|
+
sig { returns(TreeStand::Range::Point) }
|
24
|
+
attr_reader :start_point
|
25
|
+
|
26
|
+
sig { returns(TreeStand::Range::Point) }
|
27
|
+
attr_reader :end_point
|
28
|
+
|
29
|
+
# @api private
|
30
|
+
sig do
|
31
|
+
params(
|
32
|
+
start_byte: Integer,
|
33
|
+
end_byte: Integer,
|
34
|
+
start_point: T.any(TreeStand::Range::Point, TreeSitter::Point),
|
35
|
+
end_point: T.any(TreeStand::Range::Point, TreeSitter::Point),
|
36
|
+
).void
|
37
|
+
end
|
38
|
+
def initialize(start_byte:, end_byte:, start_point:, end_point:)
|
39
|
+
@start_byte = start_byte
|
40
|
+
@end_byte = end_byte
|
41
|
+
@start_point = Point.new(start_point.row, start_point.column)
|
42
|
+
@end_point = Point.new(end_point.row, end_point.column)
|
43
|
+
end
|
44
|
+
|
45
|
+
sig { params(other: Object).returns(T::Boolean) }
|
46
|
+
def ==(other)
|
47
|
+
return false unless other.is_a?(TreeStand::Range)
|
48
|
+
|
49
|
+
@start_byte == other.start_byte &&
|
50
|
+
@end_byte == other.end_byte &&
|
51
|
+
@start_point == other.start_point &&
|
52
|
+
@end_point == other.end_point
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
@@ -0,0 +1,123 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
# typed: true
|
3
|
+
|
4
|
+
module TreeStand
|
5
|
+
# Wrapper around a TreeSitter tree.
|
6
|
+
#
|
7
|
+
# This class exposes a convient API for working with the tree. There are
|
8
|
+
# dangers in using this class. The tree is mutable and the document can be
|
9
|
+
# changed. This class does not protect against that.
|
10
|
+
#
|
11
|
+
# Some of the moetods on this class edit and re-parse the document updating
|
12
|
+
# the tree. Because the document is re-parsed, the tree will be different. Which
|
13
|
+
# means all outstanding nodes & ranges will be invalid.
|
14
|
+
#
|
15
|
+
# Methods that edit the document are suffixed with `!`, e.g. `#edit!`.
|
16
|
+
#
|
17
|
+
# It's often the case that you will want perfrom multiple edits. One such
|
18
|
+
# pattern is to call #query & #edit on all matches in a loop. It's important
|
19
|
+
# to keep the destructive nature of #edit in mind and re-issue the query
|
20
|
+
# after each edit.
|
21
|
+
#
|
22
|
+
# Another thing to keep in mind is that edits done later in the document will
|
23
|
+
# likely not affect the ranges that occur earlier in the document. This can
|
24
|
+
# be a convient property that could allow you to apply edits in a reverse order.
|
25
|
+
# This is not always possible and depends on the edits you make, beware that
|
26
|
+
# the tree will be different after each edit and this approach may cause bugs.
|
27
|
+
class Tree
|
28
|
+
extend T::Sig
|
29
|
+
extend Forwardable
|
30
|
+
include Enumerable
|
31
|
+
|
32
|
+
sig { returns(String) }
|
33
|
+
attr_reader :document
|
34
|
+
|
35
|
+
sig { returns(TreeSitter::Tree) }
|
36
|
+
attr_reader :ts_tree
|
37
|
+
|
38
|
+
sig { returns(TreeStand::Parser) }
|
39
|
+
attr_reader :parser
|
40
|
+
|
41
|
+
# @!method query(query_string)
|
42
|
+
# (see TreeStand::Node#query)
|
43
|
+
# @note This is a convenience method that calls {TreeStand::Node#query} on
|
44
|
+
# {#root_node}.
|
45
|
+
#
|
46
|
+
# @!method find_node(query_string)
|
47
|
+
# (see TreeStand::Node#find_node)
|
48
|
+
# @note This is a convenience method that calls {TreeStand::Node#find_node} on
|
49
|
+
# {#root_node}.
|
50
|
+
#
|
51
|
+
# @!method find_node!(query_string)
|
52
|
+
# (see TreeStand::Node#find_node!)
|
53
|
+
# @note This is a convenience method that calls {TreeStand::Node#find_node!} on
|
54
|
+
# {#root_node}.
|
55
|
+
#
|
56
|
+
# @!method walk(&block)
|
57
|
+
# (see TreeStand::Node#walk)
|
58
|
+
#
|
59
|
+
# @note This is a convenience method that calls {TreeStand::Node#walk} on
|
60
|
+
# {#root_node}.
|
61
|
+
#
|
62
|
+
# @example Tree includes Enumerable
|
63
|
+
# tree.any? { |node| node.type == :error }
|
64
|
+
#
|
65
|
+
# @!method text
|
66
|
+
# (see TreeStand::Node#text)
|
67
|
+
# @note This is a convenience method that calls {TreeStand::Node#text} on
|
68
|
+
# {#root_node}.
|
69
|
+
def_delegators(
|
70
|
+
:root_node,
|
71
|
+
:query,
|
72
|
+
:find_node,
|
73
|
+
:find_node!,
|
74
|
+
:walk,
|
75
|
+
:text,
|
76
|
+
)
|
77
|
+
|
78
|
+
alias_method :each, :walk
|
79
|
+
|
80
|
+
# @api private
|
81
|
+
sig { params(parser: TreeStand::Parser, tree: TreeSitter::Tree, document: String).void }
|
82
|
+
def initialize(parser, tree, document)
|
83
|
+
@parser = parser
|
84
|
+
@ts_tree = tree
|
85
|
+
@document = document
|
86
|
+
end
|
87
|
+
|
88
|
+
sig { returns(TreeStand::Node) }
|
89
|
+
def root_node
|
90
|
+
TreeStand::Node.new(self, @ts_tree.root_node)
|
91
|
+
end
|
92
|
+
|
93
|
+
# This method replaces the section of the document specified by range and
|
94
|
+
# replaces it with the provided text. Then it will reparse the document and
|
95
|
+
# update the tree!
|
96
|
+
sig { params(range: TreeStand::Range, replacement: String).void }
|
97
|
+
def edit!(range, replacement)
|
98
|
+
new_document = +''
|
99
|
+
new_document << @document[0...range.start_byte]
|
100
|
+
new_document << replacement
|
101
|
+
new_document << @document[range.end_byte..]
|
102
|
+
replace_with_new_doc(new_document)
|
103
|
+
end
|
104
|
+
|
105
|
+
# This method deletes the section of the document specified by range. Then
|
106
|
+
# it will reparse the document and update the tree!
|
107
|
+
sig { params(range: TreeStand::Range).void }
|
108
|
+
def delete!(range)
|
109
|
+
new_document = +''
|
110
|
+
new_document << @document[0...range.start_byte]
|
111
|
+
new_document << @document[range.end_byte..]
|
112
|
+
replace_with_new_doc(new_document)
|
113
|
+
end
|
114
|
+
|
115
|
+
private
|
116
|
+
|
117
|
+
def replace_with_new_doc(new_document)
|
118
|
+
@document = new_document
|
119
|
+
new_tree = @parser.parse_string(@document, tree: self)
|
120
|
+
@ts_tree = new_tree.ts_tree
|
121
|
+
end
|
122
|
+
end
|
123
|
+
end
|