ruby_tree_sitter 1.6.0-x86_64-darwin

Sign up to get free protection for your applications and to get access to all the features.
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.bundle +0 -0
  29. data/lib/tree_sitter/3.1/tree_sitter.bundle +0 -0
  30. data/lib/tree_sitter/3.2/tree_sitter.bundle +0 -0
  31. data/lib/tree_sitter/3.3/tree_sitter.bundle +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