ruby_tree_sitter 1.6.0-arm-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.
Files changed (57) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +22 -0
  3. data/README.md +213 -0
  4. data/ext/tree_sitter/encoding.c +29 -0
  5. data/ext/tree_sitter/extconf.rb +149 -0
  6. data/ext/tree_sitter/input.c +127 -0
  7. data/ext/tree_sitter/input_edit.c +42 -0
  8. data/ext/tree_sitter/language.c +219 -0
  9. data/ext/tree_sitter/logger.c +228 -0
  10. data/ext/tree_sitter/macros.h +163 -0
  11. data/ext/tree_sitter/node.c +623 -0
  12. data/ext/tree_sitter/parser.c +398 -0
  13. data/ext/tree_sitter/point.c +26 -0
  14. data/ext/tree_sitter/quantifier.c +43 -0
  15. data/ext/tree_sitter/query.c +289 -0
  16. data/ext/tree_sitter/query_capture.c +28 -0
  17. data/ext/tree_sitter/query_cursor.c +231 -0
  18. data/ext/tree_sitter/query_error.c +41 -0
  19. data/ext/tree_sitter/query_match.c +44 -0
  20. data/ext/tree_sitter/query_predicate_step.c +83 -0
  21. data/ext/tree_sitter/range.c +35 -0
  22. data/ext/tree_sitter/repo.rb +128 -0
  23. data/ext/tree_sitter/symbol_type.c +46 -0
  24. data/ext/tree_sitter/tree.c +234 -0
  25. data/ext/tree_sitter/tree_cursor.c +269 -0
  26. data/ext/tree_sitter/tree_sitter.c +44 -0
  27. data/ext/tree_sitter/tree_sitter.h +107 -0
  28. data/lib/tree_sitter/3.0/tree_sitter.so +0 -0
  29. data/lib/tree_sitter/3.1/tree_sitter.so +0 -0
  30. data/lib/tree_sitter/3.2/tree_sitter.so +0 -0
  31. data/lib/tree_sitter/3.3/tree_sitter.so +0 -0
  32. data/lib/tree_sitter/helpers.rb +23 -0
  33. data/lib/tree_sitter/mixins/language.rb +167 -0
  34. data/lib/tree_sitter/node.rb +167 -0
  35. data/lib/tree_sitter/query.rb +191 -0
  36. data/lib/tree_sitter/query_captures.rb +30 -0
  37. data/lib/tree_sitter/query_cursor.rb +27 -0
  38. data/lib/tree_sitter/query_match.rb +100 -0
  39. data/lib/tree_sitter/query_matches.rb +39 -0
  40. data/lib/tree_sitter/query_predicate.rb +14 -0
  41. data/lib/tree_sitter/text_predicate_capture.rb +37 -0
  42. data/lib/tree_sitter/version.rb +8 -0
  43. data/lib/tree_sitter.rb +34 -0
  44. data/lib/tree_stand/ast_modifier.rb +30 -0
  45. data/lib/tree_stand/breadth_first_visitor.rb +54 -0
  46. data/lib/tree_stand/config.rb +19 -0
  47. data/lib/tree_stand/node.rb +351 -0
  48. data/lib/tree_stand/parser.rb +87 -0
  49. data/lib/tree_stand/range.rb +55 -0
  50. data/lib/tree_stand/tree.rb +123 -0
  51. data/lib/tree_stand/utils/printer.rb +73 -0
  52. data/lib/tree_stand/version.rb +7 -0
  53. data/lib/tree_stand/visitor.rb +127 -0
  54. data/lib/tree_stand/visitors/tree_walker.rb +37 -0
  55. data/lib/tree_stand.rb +48 -0
  56. data/tree_sitter.gemspec +34 -0
  57. 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