ruby_tree_sitter 1.6.0-x86_64-linux-gnu
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/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
|