ruby_tree_sitter 1.1.0 → 1.3.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 7447395b7231af234e93a7cdaa245beb48e72b41121350365d71123ae170baa5
4
- data.tar.gz: 16bdb9be8c40402fcd135a177802108d70eb6c504541e045917af2c2045a109d
3
+ metadata.gz: '08eb24a38613d42859f2f16fd021da1ff1f4d65cfe377ef00044c0d94f34f706'
4
+ data.tar.gz: 64a1df4c4310fa4f93dd664b6a19f91f8fc10cfc0bd49895d75773bae451dbc5
5
5
  SHA512:
6
- metadata.gz: 80f1ba5a83de868997ebe8cb3308f5c5d1acc1ae6e4d18645446f2ebfff10884d27651749b013cff2e2f2aeefedaf26b51012fa490fdb26f6ebd4533b85f293c
7
- data.tar.gz: 7387367fc5cc306f80be04607ae08be2fd0ca5913bb9463129547682230620449d9449fe67d898f208975c4c14ea0d72f0b7ae2c84c84ea1d78e8b84091e4db6
6
+ metadata.gz: 6b592c3285d0d6ba7e93fac74ad446af847f5e35e6493210ac1a538e57ed4793396e3c287626d94d39580ea9b3b3b8f15a77d3d66fdea751b44204d74ce01cd2
7
+ data.tar.gz: e3e253584245fdc79a4c05f38a9525c7647a650faf9cc8d273f5ee5df24db8bcd5cd399cabe712f1840df029ddc2868d43b1b25fe6c966d2f63d583e0f666eb4
data/README.md CHANGED
@@ -1,6 +1,12 @@
1
1
  # Ruby tree-sitter bindings
2
2
 
3
- [![ci](https://github.com/Faveod/ruby-tree-sitter/actions/workflows/ci.yml/badge.svg)](https://github.com/Faveod/ruby-tree-sitter/actions/workflows/ci.yml) [![valgrind](https://github.com/Faveod/ruby-tree-sitter/actions/workflows/valgrind.yml/badge.svg)](https://github.com/Faveod/ruby-tree-sitter/actions/workflows/valgrind.yml) [![asan/ubsan](https://github.com/Faveod/ruby-tree-sitter/actions/workflows/asan.yml/badge.svg)](https://github.com/Faveod/ruby-tree-sitter/actions/workflows/asan.yml)
3
+ [![docs](https://github.com/Faveod/ruby-tree-sitter/actions/workflows/publish-docs.yml/badge.svg)](https://faveod.github.io/ruby-tree-sitter)
4
+ [![rubygems.org](https://github.com/Faveod/ruby-tree-sitter/actions/workflows/publish.yml/badge.svg)](https://rubygems.org/gems/ruby_tree_sitter)
5
+ [![ci](https://github.com/Faveod/ruby-tree-sitter/actions/workflows/ci.yml/badge.svg)](https://github.com/Faveod/ruby-tree-sitter/actions/workflows/ci.yml)
6
+ <!--
7
+ [![valgrind](https://github.com/Faveod/ruby-tree-sitter/actions/workflows/valgrind.yml/badge.svg)](https://github.com/Faveod/ruby-tree-sitter/actions/workflows/valgrind.yml)
8
+ [![asan/ubsan](https://github.com/Faveod/ruby-tree-sitter/actions/workflows/asan.yml/badge.svg)](https://github.com/Faveod/ruby-tree-sitter/actions/workflows/asan.yml)
9
+ -->
4
10
 
5
11
  Ruby bindings for [tree-sitter](https://github.com/tree-sitter/tree-sitter).
6
12
 
@@ -63,8 +69,7 @@ This gem is a binding for `tree-sitter`. It doesn't have a version of
63
69
 
64
70
  You must install `tree-sitter` and make sure that their dynamic library is
65
71
  accessible from `$PATH`, or build the gem with `--disable-sys-libs`, which will
66
- download the latest tagged `tree-sitter` and build against it (see [Build from
67
- source](docs/Contributing.md#build-from-source) .)
72
+ download the latest tagged `tree-sitter` and build against it (see [Build from source](docs/Contributing.md#build-from-source) .)
68
73
 
69
74
  You can either install `tree-sitter` from source or through your go-to package manager.
70
75
 
@@ -133,8 +138,7 @@ bundle config set build.ruby_tree_sitter --disable-sys-libs
133
138
  If you don't want to install from `rubygems`, `git`, or if you don't want to
134
139
  compile on install, then download a native gem from this repository's
135
140
  [releases](https://github.com/Faveod/ruby-tree-sitter/releases), or you can
136
- compile it yourself (see [Build from
137
- source](docs/Contributing.md#build-from-source) .)
141
+ compile it yourself (see [Build from source](docs/Contributing.md#build-from-source) .)
138
142
 
139
143
  In that case, you'd have to point your `Gemfile` to the `gem` as such:
140
144
 
@@ -171,7 +175,7 @@ See `examples` directory.
171
175
 
172
176
  ## Development
173
177
 
174
- See [`docs/README.md`](docs/Contributing.md).
178
+ See [`docs/Contributing.md`](docs/Contributing.md).
175
179
 
176
180
  ## 🚧 👷‍♀️ Notes 👷 🚧
177
181
 
@@ -186,7 +190,7 @@ don't copy them left and right, and then expect them to work without
186
190
  `SEGFAULT`ing and creating a black-hole in your living-room. Assume that you
187
191
  have to work locally with them. If you get a `SEGFAULT`, you can debug the
188
192
  native `C` code using `gdb`. You can read more on `SEGFAULT`s
189
- [here](docs/SIGSEGV.md), and debugging [here](docs/Contributing.md#Debugging).
193
+ [here](docs/SIGSEGV.md), and debugging [here](docs/Contributing#Debugging.md).
190
194
 
191
195
  That said, we do aim at providing an idiomatic `Ruby` interface. It should also
192
196
  provide a _safer_ interface, where you don't have to worry about when and how
@@ -32,7 +32,7 @@ VALUE new_language(const TSLanguage *language) {
32
32
  * with this gem.
33
33
  *
34
34
  * @param name [String] the parser's name.
35
- * @param path [String] the parser's shared library (so, dylib) path on disk.
35
+ * @param path [String, Pathname] the parser's shared library (so, dylib) path on disk.
36
36
  *
37
37
  * @return [Language]
38
38
  */
@@ -154,9 +154,9 @@ static void logger_initialize_stderr(logger_t *logger) {
154
154
  *
155
155
  * You can provide your proper backend. You have to make sure that it
156
156
  * exposes a +printf+, +puts+, or +write+ (lookup is done in that specific
157
- * order). {StringIO} is a perfect candidate.
157
+ * order). {::StringIO} is a perfect candidate.
158
158
  *
159
- * You can also provide a format ({String}) if your backend supports a +printf+.
159
+ * You can also provide a format ({::String}) if your backend supports a +printf+.
160
160
  *
161
161
  * @example
162
162
  * backend = StringIO.new
@@ -166,14 +166,19 @@ static VALUE node_child_by_field_id(VALUE self, VALUE field_id) {
166
166
  /**
167
167
  * Get the node's child with the given field name.
168
168
  *
169
- * @param field_name [String]
169
+ * @param field_name [String, Symbol]
170
170
  *
171
171
  * @return [Node]
172
172
  */
173
173
  static VALUE node_child_by_field_name(VALUE self, VALUE field_name) {
174
- const char *name = StringValuePtr(field_name);
175
- uint32_t length = (uint32_t)RSTRING_LEN(field_name);
176
- return new_node_by_val(ts_node_child_by_field_name(SELF, name, length));
174
+ if (Qtrue == rb_funcall(self, rb_intern("field?"), 1, field_name)) {
175
+ VALUE field_str = rb_funcall(field_name, rb_intern("to_s"), 0);
176
+ const char *name = StringValuePtr(field_str);
177
+ uint32_t length = (uint32_t)RSTRING_LEN(field_str);
178
+ return new_node_by_val(ts_node_child_by_field_name(SELF, name, length));
179
+ } else {
180
+ return Qnil;
181
+ }
177
182
  }
178
183
 
179
184
  /**
@@ -208,7 +208,7 @@ static VALUE parser_set_logger(VALUE self, VALUE logger) {
208
208
  * same arguments. Or you can start parsing from scratch by first calling
209
209
  * {Parser#reset}.
210
210
  * 3. Parsing was cancelled using a cancellation flag that was set by an
211
- * earlier call to {Parsert#cancellation_flag=}. You can resume parsing
211
+ * earlier call to {Parser#cancellation_flag=}. You can resume parsing
212
212
  * from where the parser left out by calling {Parser#parse} again with
213
213
  * the same arguments.
214
214
  *
@@ -140,6 +140,13 @@ static VALUE query_initialize(VALUE self, VALUE language, VALUE source) {
140
140
  SELF = res;
141
141
  }
142
142
 
143
+ rb_iv_set(self, "@text_predicates", rb_ary_new());
144
+ rb_iv_set(self, "@property_predicates", rb_ary_new());
145
+ rb_iv_set(self, "@property_settings", rb_ary_new());
146
+ rb_iv_set(self, "@general_predicates", rb_ary_new());
147
+
148
+ rb_funcall(self, rb_intern("process"), 1, source);
149
+
143
150
  return self;
144
151
  }
145
152
 
@@ -49,7 +49,7 @@ DATA_FROM_VALUE(TSQueryCursor *, query_cursor)
49
49
  *
50
50
  * @return [QueryCursor]
51
51
  */
52
- static VALUE query_cursor_exec(VALUE self, VALUE query, VALUE node) {
52
+ static VALUE query_cursor_exec_static(VALUE self, VALUE query, VALUE node) {
53
53
  VALUE res = query_cursor_allocate(cQueryCursor);
54
54
  query_cursor_t *query_cursor = unwrap(res);
55
55
  ts_query_cursor_exec(query_cursor->data, value_to_query(query),
@@ -57,6 +57,21 @@ static VALUE query_cursor_exec(VALUE self, VALUE query, VALUE node) {
57
57
  return res;
58
58
  }
59
59
 
60
+ /**
61
+ * Start running a given query on a given node.
62
+ *
63
+ * @param query [Query]
64
+ * @param node [Node]
65
+ *
66
+ * @return [QueryCursor]
67
+ */
68
+ static VALUE query_cursor_exec(VALUE self, VALUE query, VALUE node) {
69
+ query_cursor_t *query_cursor = unwrap(self);
70
+ ts_query_cursor_exec(query_cursor->data, value_to_query(query),
71
+ value_to_node(node));
72
+ return self;
73
+ }
74
+
60
75
  /**
61
76
  * Manage the maximum number of in-progress matches allowed by this query
62
77
  * cursor.
@@ -190,13 +205,14 @@ void init_query_cursor(void) {
190
205
  rb_define_alloc_func(cQueryCursor, query_cursor_allocate);
191
206
 
192
207
  /* Module methods */
193
- rb_define_module_function(cQueryCursor, "exec", query_cursor_exec, 2);
208
+ rb_define_module_function(cQueryCursor, "exec", query_cursor_exec_static, 2);
194
209
 
195
210
  /* Class methods */
196
211
  // Accessors
197
212
  DECLARE_ACCESSOR(cQueryCursor, query_cursor, match_limit)
198
213
 
199
214
  // Other
215
+ rb_define_method(cQueryCursor, "exec", query_cursor_exec, 2);
200
216
  rb_define_method(cQueryCursor, "exceed_match_limit?",
201
217
  query_cursor_did_exceed_match_limit, 0);
202
218
  rb_define_method(cQueryCursor, "match_limit", query_cursor_get_match_limit,
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ # splits an array like [rust](https://doc.rust-lang.org/std/primitive.slice.html#method.split)
4
+ def array_split_like_rust(array, &block)
5
+ return enum_for(__method__, array) if !block_given?
6
+
7
+ return [] if array.empty?
8
+
9
+ result = []
10
+ current_slice = []
11
+
12
+ array.each do |element|
13
+ if yield(element)
14
+ result << current_slice
15
+ current_slice = []
16
+ else
17
+ current_slice << element
18
+ end
19
+ end
20
+
21
+ result << current_slice
22
+ result
23
+ end
@@ -13,28 +13,17 @@ module TreeSitter
13
13
  @fields << name.to_sym if name
14
14
  end
15
15
 
16
- @fields
16
+ @fields.to_a
17
17
  end
18
18
 
19
+ # @param field [String, Symbol]
19
20
  def field?(field)
20
- fields.include?(field)
21
+ fields.include?(field.to_sym)
21
22
  end
22
23
 
23
- # FIXME: These APIs (`[]` and `fetch`) need absolute fixing.
24
- # 1. The documentation with the table doesn't work.
25
- # 1. The APIs are very confusing! Make them act similarly to Hash's
26
- # `fetch` and `[]`.
27
- # 1. `[]` should take a single input and return nil if nothing found
28
- # (no exceptions).
29
- # 1. `fetch` should should accept a single argument, potentially a
30
- # default, and raise exception if no default was provided.
31
- # Also allow for the `all:` kwarg.
32
- # 1. `values_at` takes many arguments.
33
- # And I don't think we can move to 1.0 without adressing them.
34
- #
35
24
  # Access node's named children.
36
25
  #
37
- # It's similar to {#fetch}, but differes in input type, return values, and
26
+ # It's similar to {#fetch}, but differs in input type, return values, and
38
27
  # the internal implementation.
39
28
  #
40
29
  # Both of these methods exist for separate use cases, but also because
@@ -60,15 +49,15 @@ module TreeSitter
60
49
  # @return [Node | Array<Node>]
61
50
  def [](*keys)
62
51
  case keys.length
63
- when 0 then raise "#{self.class.name}##{__method__} requires a key."
52
+ when 0 then raise ArgumentError, "#{self.class.name}##{__method__} requires a key."
64
53
  when 1
65
54
  case k = keys.first
66
- when Numeric then named_child(k)
55
+ when Integer then named_child(k)
67
56
  when String, Symbol
68
- raise "Cannot find field #{k}" unless fields.include?(k.to_sym)
57
+ raise IndexError, "Cannot find field #{k}. Available: #{fields}" unless fields.include?(k.to_sym)
69
58
 
70
59
  child_by_field_name(k.to_s)
71
- else raise <<~ERR
60
+ else raise ArgumentError, <<~ERR
72
61
  #{self.class.name}##{__method__} accepts Integer and returns named child at given index,
73
62
  or a (String | Symbol) and returns the child by given field name.
74
63
  ERR
@@ -139,7 +128,7 @@ module TreeSitter
139
128
 
140
129
  # Access node's named children.
141
130
  #
142
- # It's similar to {#fetch}, but differes in input type, return values, and
131
+ # It's similar to {#[]}, but differs in input type, return values, and
143
132
  # the internal implementation.
144
133
  #
145
134
  # Both of these methods exist for separate use cases, but also because
@@ -159,39 +148,18 @@ module TreeSitter
159
148
  # uses named_child | field_name_for_child
160
149
  # child_by_field_name | via each_node
161
150
  # ------------------------------+----------------------
162
- # @param all [Boolean] If `true`, return an array of nodes for all the
163
- # demanded keys, putting `nil` for missing ones. If `false`, return the
164
- # same array after calling `compact`. Defaults to `false`.
165
- #
166
- # See {#fetch_all}.
167
- def fetch(*keys, all: false, **_kwargs)
168
- dict = {}
169
- keys.each.with_index do |k, i|
170
- dict[k.to_s] = i
171
- end
151
+ #
152
+ # See {#[]}.
153
+ def fetch(*keys)
154
+ keys = keys.map(&:to_s)
155
+ key_set = keys.to_set
156
+ fields = {}
157
+ each_field do |f, _c|
158
+ fields[f] = self[f] if key_set.delete(f)
172
159
 
173
- res = {}
174
- each_field do |f, c|
175
- if dict.key?(f)
176
- res[f] = c
177
- dict.delete(f)
178
- end
179
- break if dict.empty?
160
+ break if key_set.empty?
180
161
  end
181
-
182
- res = keys.uniq.map { |k| res[k.to_s] }
183
- res = res.compact if !all
184
- res
185
- end
186
-
187
- # Access all named children of a node, returning `nil` for missing ones.
188
- #
189
- # Equivalent to `fetch(…, all: true)`.
190
- #
191
- # See {#fetch}.
192
- def fetch_all(*keys, **kwargs)
193
- kwargs[:all] = true
194
- fetch(*keys, **kwargs)
162
+ fields.values_at(*keys)
195
163
  end
196
164
  end
197
165
  end
@@ -0,0 +1,191 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'helpers'
4
+
5
+ module TreeSitter
6
+ # Query is a wrapper around a tree-sitter query.
7
+ class Query
8
+ attr_reader :capture_names
9
+ attr_reader :capture_quantifiers
10
+ attr_reader :text_predicates
11
+ attr_reader :property_predicates
12
+ attr_reader :property_settings
13
+ attr_reader :general_predicates
14
+
15
+ private
16
+
17
+ # Called from query.c on initialize.
18
+ #
19
+ # Prepares all the predicates so we could process them in places like
20
+ # {QueryMatch#satisfies_text_predicate?}.
21
+ #
22
+ # This is translation from the [rust bindings](https://github.com/tree-sitter/tree-sitter/blob/e553578696fe86071846ed612ee476d0167369c1/lib/binding_rust/lib.rs#L1860)
23
+ # Because it's a direct translation, it's way too long and we need to shut up rubocop.
24
+ # TODO: refactor + simplify when stable.
25
+ def process(source) # rubocop:disable Metrics/AbcSize,Metrics/CyclomaticComplexity,Metrics/MethodLength,Metrics/PerceivedComplexity
26
+ string_count = self.string_count
27
+ capture_count = self.capture_count
28
+ pattern_count = self.pattern_count
29
+
30
+ # Build a vector of strings to store the capture names.
31
+ capture_names = capture_count.times.map { |i| capture_name_for_id(i) }
32
+
33
+ # Build a vector to store capture qunatifiers.
34
+ capture_quantifiers =
35
+ pattern_count.times.map do |i|
36
+ capture_count.times.map do |j|
37
+ capture_quantifier_for_id(i, j)
38
+ end
39
+ end
40
+
41
+ # Build a vector of strings to represent literal values used in predicates.
42
+ string_values = string_count.times.map { |i| string_value_for_id(i) }
43
+
44
+ # Build a vector of predicates for each pattern.
45
+ pattern_count.times do |i| # rubocop:disable Metrics/BlockLength
46
+ predicate_steps = predicates_for_pattern(i)
47
+ byte_offset = start_byte_for_pattern(i)
48
+ row =
49
+ source.chars.map.with_index
50
+ .take_while { |_, i| i < byte_offset } # rubocop:disable Lint/ShadowingOuterLocalVariable
51
+ .filter { |c, _| c == "\n" }
52
+ .size
53
+ text_predicates = []
54
+ property_predicates = []
55
+ property_settings = []
56
+ general_predicates = []
57
+
58
+ array_split_like_rust(predicate_steps) { |s| s.type == QueryPredicateStep::DONE } # rubocop:disable Metrics/BlockLength
59
+ .each do |p|
60
+ next if p.empty?
61
+
62
+ if p[0] == QueryPredicateStep::STRING
63
+ cap = capture_names[p[0].value_id]
64
+ raise ArgumentError, <<~MSG.chomp
65
+ L#{row}: Expected predicate to start with a function name. Got @#{cap}.
66
+ MSG
67
+ end
68
+
69
+ # Build a predicate for each of the known predicate function names.
70
+ operator_name = string_values[p[0].value_id]
71
+
72
+ case operator_name
73
+ in 'any-eq?' | 'any-not-eq?' | 'eq?' | 'not-eq?'
74
+ if p.size != 3
75
+ raise ArgumentError, <<~MSG.chomp
76
+ L#{row}: Wrong number of arguments to ##{operator_name} predicate. Expected 2, got #{p.size - 1}.
77
+ MSG
78
+ end
79
+
80
+ if p[1].type != QueryPredicateStep::CAPTURE
81
+ lit = string_values[p[1].value_id]
82
+ raise ArgumentError, <<~MSG.chomp
83
+ L#{row}: First argument to ##{operator_name} predicate must be a capture name. Got literal "#{lit}".
84
+ MSG
85
+ end
86
+
87
+ is_positive = %w[eq? any-eq?].include?(operator_name)
88
+ match_all = %w[eq? not-eq?].include?(operator_name)
89
+ # NOTE: in the rust impl, match_all can hit an unreachable! but I am simplifying
90
+ # for readability. Same applies for the other `in` branches.
91
+ text_predicates <<
92
+ if p[2].type == QueryPredicateStep::CAPTURE
93
+ TextPredicateCapture.eq_capture(p[1].value_id, p[2].value_id, is_positive, match_all)
94
+ else
95
+ TextPredicateCapture.eq_string(p[1].value_id, string_values[p[2].value_id], is_positive, match_all)
96
+ end
97
+
98
+ in 'match?' | 'not-match?' | 'any-match?' | 'any-not-match?'
99
+ if p.size != 3
100
+ raise ArgumentError, <<~MSG.chomp
101
+ L#{row}: Wrong number of arguments to ##{operator_name} predicate. Expected 2, got #{p.size - 1}.
102
+ MSG
103
+ end
104
+
105
+ if p[1].type != QueryPredicateStep::CAPTURE
106
+ lit = string_values[p[1].value_id]
107
+ raise ArgumentError, <<~MSG.chomp
108
+ L#{row}: First argument to ##{operator_name} predicate must be a capture name. Got literal "#{lit}".
109
+ MSG
110
+ end
111
+
112
+ if p[2].type == QueryPredicateStep::CAPTURE
113
+ cap = capture_names[p[2].value_id]
114
+ raise ArgumentError, <<~MSG.chomp
115
+ L#{row}: First argument to ##{operator_name} predicate must be a literal. Got capture @#{cap}".
116
+ MSG
117
+ end
118
+
119
+ is_positive = %w[match? any-match?].include?(operator_name)
120
+ match_all = %w[match? not-match?].include?(operator_name)
121
+ regex = /#{string_values[p[2].value_id]}/
122
+
123
+ text_predicates << TextPredicateCapture.match_string(p[1].value_id, regex, is_positive, match_all)
124
+
125
+ in 'set!'
126
+ property_settings << 'todo!'
127
+
128
+ in 'is?' | 'is-not?'
129
+ property_predicates << 'todo!'
130
+
131
+ in 'any-of?' | 'not-any-of?'
132
+ if p.size < 2
133
+ raise ArgumentError, <<~MSG.chomp
134
+ L#{row}: Wrong number of arguments to ##{operator_name} predicate. Expected at least 1, got #{p.size - 1}.
135
+ MSG
136
+ end
137
+
138
+ if p[1].type != QueryPredicateStep::CAPTURE
139
+ lit = string_values[p[1].value_id]
140
+ raise ArgumentError, <<~MSG.chomp
141
+ L#{row}: First argument to ##{operator_name} predicate must be a capture name. Got literal "#{lit}".
142
+ MSG
143
+ end
144
+
145
+ is_positive = operator_name == 'any_of'
146
+ values = []
147
+
148
+ p[2..].each do |arg|
149
+ if arg.type == QueryPredicateStep::CAPTURE
150
+ lit = string_values[arg.value_id]
151
+ raise ArgumentError, <<~MSG.chomp
152
+ L#{row}: First argument to ##{operator_name} predicate must be a capture name. Got literal "#{lit}".
153
+ MSG
154
+ end
155
+ values << string_values[arg.value_id]
156
+ end
157
+
158
+ # TODO: is the map to to_s necessary in ruby?
159
+ text_predicates <<
160
+ TextPredicateCapture.any_string(p[1].value_id, values.map(&:to_s), is_positive, match_all)
161
+ else
162
+ general_predicates <<
163
+ QueryPredicate.new(
164
+ operator_name,
165
+ p[1..].map do |a|
166
+ if a.type == QueryPredicateStep::CAPTURE
167
+ { capture: a.value_id }
168
+ else
169
+ { string: string_values[a.value_id] }
170
+ end
171
+ end,
172
+ )
173
+ end
174
+
175
+ @text_predicates << text_predicates
176
+ @property_predicates << property_predicates
177
+ @property_settings << property_settings
178
+ @general_predicates << general_predicates
179
+ end
180
+
181
+ @capture_names = capture_names
182
+ @capture_quantifiers = capture_quantifiers
183
+ end
184
+ end
185
+
186
+ # TODO
187
+ def parse_property
188
+ # todo
189
+ end
190
+ end
191
+ end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TreeSitter
4
+ # A sequence of {TreeSitter::QueryCapture} associated with a given {TreeSitter::QueryCursor}.
5
+ class QueryCaptures
6
+ def initialize(cursor, query, src)
7
+ @cursor = cursor
8
+ @query = query
9
+ @src = src
10
+ end
11
+
12
+ # Iterator over captures.
13
+ #
14
+ # @yieldparam match [TreeSitter::QueryMatch]
15
+ # @yieldparam capture_index [Integer]
16
+ def each
17
+ return enum_for __method__ if !block_given?
18
+
19
+ while (capture_index, match = @cursor.next_capture)
20
+ next if !match.is_a?(TreeSitter::QueryMatch)
21
+
22
+ if match.satisfies_text_predicate?(@query, @src)
23
+ yield [match, capture_index]
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TreeSitter
4
+ # A Cursor for {Query}.
5
+ class QueryCursor
6
+ # Iterate over all of the matches in the order that they were found.
7
+ #
8
+ # Each match contains the index of the pattern that matched, and a list of
9
+ # captures. Because multiple patterns can match the same set of nodes,
10
+ # one match may contain captures that appear *before* some of the
11
+ # captures from a previous match.
12
+ def matches(query, node, src)
13
+ self.exec(query, node)
14
+ QueryMatches.new(self, query, src)
15
+ end
16
+
17
+ # Iterate over all of the individual captures in the order that they
18
+ # appear.
19
+ #
20
+ # This is useful if you don't care about which pattern matched, and just
21
+ # want a single, ordered sequence of captures.
22
+ def captures(query, node, src)
23
+ self.exec(query, node)
24
+ QueryCaptures.new(self, query, src)
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,100 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'query_captures'
4
+
5
+ module TreeSitter
6
+ # A match for a {Query}.
7
+ class QueryMatch
8
+ # All nodes at a given capture index.
9
+ #
10
+ # @param index [Integer]
11
+ #
12
+ # @return [TreeSitter::Node]
13
+ def nodes_for_capture_index(index) = captures.filter_map { |capture| capture.node if capture.index == index }
14
+
15
+ # Whether the {QueryMatch} satisfies the text predicates in the query.
16
+ #
17
+ # This is a translation from the [rust bindings](https://github.com/tree-sitter/tree-sitter/blob/e553578696fe86071846ed612ee476d0167369c1/lib/binding_rust/lib.rs#L2502).
18
+ # Because it's a direct translation, it's way too long and we need to shut up rubocop.
19
+ # TODO: refactor + simplify when satable.
20
+ def satisfies_text_predicate?(query, src) # rubocop:disable Metrics/AbcSize,Metrics/CyclomaticComplexity,Metrics/MethodLength,Metrics/PerceivedComplexity
21
+ return true if query.text_predicates[pattern_index].nil?
22
+
23
+ query # rubocop:disable Metrics/BlockLength
24
+ .text_predicates[pattern_index]
25
+ .all? do |predicate|
26
+ case predicate.type
27
+ in TextPredicateCapture::EQ_CAPTURE
28
+ fst_nodes = nodes_for_capture_index(predicate.fst)
29
+ snd_nodes = nodes_for_capture_index(predicate.snd)
30
+ res = nil
31
+ consumed = 0
32
+ fst_nodes.zip(snd_nodes).each do |node1, node2|
33
+ text1 = node_text(node1, src)
34
+ text2 = node_text(node2, src)
35
+ if (text1 == text2) != predicate.positive? && predicate.match_all?
36
+ res = false
37
+ break
38
+ end
39
+ if (text1 == text2) == predicate.positive? && !predicate.match_all?
40
+ res = true
41
+ break
42
+ end
43
+ consumed += 1
44
+ end
45
+ (res.nil? && consumed == fst_nodes.length && consumed == snd_nodes.length) \
46
+ || res
47
+
48
+ in TextPredicateCapture::EQ_STRING
49
+ nodes = nodes_for_capture_index(predicate.fst)
50
+ res = true
51
+ nodes.each do |node|
52
+ text = node_text(node, src)
53
+ if (predicate.snd == text) != predicate.positive? && predicate.match_all?
54
+ res = false
55
+ break
56
+ end
57
+ if (predicate.snd == text) == predicate.positive? && !predicate.match_all?
58
+ res = true
59
+ break
60
+ end
61
+ end
62
+ res
63
+
64
+ in TextPredicateCapture::MATCH_STRING
65
+ nodes = nodes_for_capture_index(predicate.fst)
66
+ res = true
67
+ nodes.each do |node|
68
+ text = node_text(node, src)
69
+ if predicate.snd.match?(text) != predicate.positive? && predicate.match_all?
70
+ res = false
71
+ break
72
+ end
73
+ if predicate.snd.match?(text) == predicate.positive? && !predicate.match_all?
74
+ res = true
75
+ break
76
+ end
77
+ end
78
+ res
79
+
80
+ in TextPredicateCapture::ANY_STRING
81
+ nodes = nodes_for_capture_index(predicate.fst)
82
+ res = true
83
+ nodes.each do |node|
84
+ text = node_text(node, src)
85
+ if predicate.snd.any? { |v| v == text } != predicate.positive?
86
+ res = false
87
+ break
88
+ end
89
+ end
90
+ res
91
+
92
+ end
93
+ end
94
+ end
95
+
96
+ private
97
+
98
+ def node_text(node, text) = text.byteslice(node.start_byte...node.end_byte)
99
+ end
100
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TreeSitter
4
+ # A sequence of {QueryMatch} associated with a given {QueryCursor}.
5
+ class QueryMatches
6
+ def initialize(cursor, query, src)
7
+ @cursor = cursor
8
+ @query = query
9
+ @src = src
10
+ end
11
+
12
+ # Iterator over matches.
13
+ #
14
+ # @yieldparam match [TreeSitter::QueryMatch]
15
+ def each
16
+ return enum_for __method__ if !block_given?
17
+
18
+ while match = @cursor.next_match
19
+ if match.satisfies_text_predicate?(@query, @src)
20
+ yield match
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TreeSitter
4
+ # A {Query} predicate generic representation.
5
+ class QueryPredicate
6
+ attr_accessor :operator
7
+ attr_accessor :args
8
+
9
+ def initialize(operator, args)
10
+ @operator = operator
11
+ @args = args
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TreeSitter
4
+ # A representation for text predicates.
5
+ class TextPredicateCapture
6
+ EQ_CAPTURE = 0 # Equality Capture
7
+ EQ_STRING = 1 # Equality String
8
+ MATCH_STRING = 2 # Match String
9
+ ANY_STRING = 3 # Any String
10
+
11
+ attr_reader :fst
12
+ attr_reader :snd
13
+ attr_reader :type
14
+
15
+ # Create a TextPredicateCapture for {EQ_CAPTURE}.
16
+ def self.eq_capture(...) = new(EQ_CAPTURE, ...)
17
+ # Create a TextPredicateCapture for {EQ_STRING}.
18
+ def self.eq_string(...) = new(EQ_STRING, ...)
19
+ # Create a TextPredicateCapture for {MATCH_STRING}.
20
+ def self.match_string(...) = new(MATCH_STRING, ...)
21
+ # Create a TextPredicateCapture for {ANY_STRING}.
22
+ def self.any_string(...) = new(ANY_STRING, ...)
23
+
24
+ def initialize(type, fst, snd, positive, match_all)
25
+ @type = type
26
+ @fst = fst
27
+ @snd = snd
28
+ @positive = positive
29
+ @match_all = match_all
30
+ end
31
+
32
+ # `#eq` is positive, `#not-eq` is not.
33
+ def positive? = @positive
34
+ # `#any-` means don't match all.
35
+ def match_all? = @match_all
36
+ end
37
+ end
@@ -4,5 +4,5 @@ module TreeSitter
4
4
  # The version of the tree-sitter library.
5
5
  TREESITTER_VERSION = '0.22.6'
6
6
  # The current version of the gem.
7
- VERSION = '1.1.0'
7
+ VERSION = '1.3.0'
8
8
  end
data/lib/tree_sitter.rb CHANGED
@@ -6,9 +6,16 @@ end
6
6
 
7
7
  require 'set'
8
8
 
9
+ require 'tree_sitter/tree_sitter'
9
10
  require 'tree_sitter/version'
10
11
 
11
- require 'tree_sitter/tree_sitter'
12
12
  require 'tree_sitter/node'
13
+ require 'tree_sitter/query'
14
+ require 'tree_sitter/query_captures'
15
+ require 'tree_sitter/query_cursor'
16
+ require 'tree_sitter/query_match'
17
+ require 'tree_sitter/query_matches'
18
+ require 'tree_sitter/query_predicate'
19
+ require 'tree_sitter/text_predicate_capture'
13
20
 
14
21
  ObjectSpace.define_finalizer(TreeSitter::Tree.class, proc { TreeSitter::Tree.finalizer })
@@ -1,13 +1,19 @@
1
1
  # frozen_string_literal: true
2
2
  # typed: true
3
3
 
4
+ require 'pathname'
5
+
4
6
  module TreeStand
5
7
  # Global configuration for the gem.
6
8
  # @api private
7
9
  class Config
8
10
  extend T::Sig
9
11
 
10
- sig { returns(String) }
11
- attr_accessor :parser_path
12
+ sig { returns(T.nilable(Pathname)) }
13
+ attr_reader :parser_path
14
+
15
+ def parser_path=(path)
16
+ @parser_path = Pathname(path)
17
+ end
12
18
  end
13
19
  end
@@ -11,16 +11,60 @@ module TreeStand
11
11
  extend Forwardable
12
12
  include Enumerable
13
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.
14
28
  # @!method type
15
29
  # @return [Symbol] the type of the node in the tree-sitter grammar.
16
30
  # @!method error?
17
31
  # @return [bool] true if the node is an error node.
18
32
  def_delegators(
19
33
  :@ts_node,
20
- :type,
34
+ :changed?,
35
+ :child_count,
21
36
  :error?,
37
+ :extra?,
38
+ :has_error?,
39
+ :missing?,
40
+ :named?,
41
+ :named_child_count,
42
+ :type,
22
43
  )
23
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
+
24
68
  sig { returns(TreeStand::Tree) }
25
69
  attr_reader :tree
26
70
 
@@ -32,7 +76,6 @@ module TreeStand
32
76
  def initialize(tree, ts_node)
33
77
  @tree = tree
34
78
  @ts_node = ts_node
35
- @fields = @ts_node.each_field.to_a.map(&:first)
36
79
  end
37
80
 
38
81
  # TreeSitter uses a `TreeSitter::Cursor` to iterate over matches by calling
@@ -131,6 +174,74 @@ module TreeStand
131
174
  enumerator
132
175
  end
133
176
 
177
+ # Enumerate named children.
178
+ # @example
179
+ # node.text # => "3 * 4"
180
+ #
181
+ # @example Iterate over the child nodes
182
+ # node.each_named do |child|
183
+ # print child.text
184
+ # end
185
+ # # prints: 34
186
+ #
187
+ # @example Enumerable methods
188
+ # node.each_named.map(&:text) # => ["3", "4"]
189
+ #
190
+ # @yieldparam child [TreeStand::Node]
191
+ sig do
192
+ params(block: T.nilable(T.proc.params(node: TreeStand::Node).returns(BasicObject)))
193
+ .returns(T::Enumerator[TreeStand::Node])
194
+ end
195
+ def each_named(&block)
196
+ enumerator = Enumerator.new do |yielder|
197
+ @ts_node.each_named do |child|
198
+ yielder << TreeStand::Node.new(@tree, child)
199
+ end
200
+ end
201
+ enumerator.each(&block) if block_given?
202
+ enumerator
203
+ end
204
+
205
+ # Iterate of (field, child).
206
+ #
207
+ # @example
208
+ # node.text # => "3 * 4"
209
+ #
210
+ # @example Iterate over the child nodes
211
+ # node.each_field do |field, child|
212
+ # puts "#{field}: #{child.text}"
213
+ # end
214
+ # # prints:
215
+ # # left: 3
216
+ # # right: 4
217
+ #
218
+ # @example Enumerable methods
219
+ # node.each_field.map { |f, c| "#{f}: #{c}" } # => ["left: 3", "right: 4"]
220
+ #
221
+ # @yieldparam field [Symbol]
222
+ # @yieldparam child [TreeStand::Node]
223
+ sig do
224
+ params(block: T.nilable(T.proc.params(node: TreeStand::Node).returns(BasicObject)))
225
+ .returns(T::Enumerator[[Symbol, TreeStand::Node]])
226
+ end
227
+ def each_field(&block)
228
+ enumerator = Enumerator.new do |yielder|
229
+ @ts_node.each_field do |field, child|
230
+ yielder << [field.to_sym, TreeStand::Node.new(@tree, child)]
231
+ end
232
+ end
233
+ enumerator.each(&block) if block_given?
234
+ enumerator
235
+ end
236
+
237
+ # @example Enumerable methods
238
+ # node.named.map(&:text) # => ["3", "4"]
239
+ alias_method :named, :each_named
240
+
241
+ # @example Enumerable methods
242
+ # node.fields.map { |f, c| "#{f}: #{c}" } # => ["left: 3", "right: 4"]
243
+ alias_method :fields, :each_field
244
+
134
245
  # (see TreeStand::Visitors::TreeWalker)
135
246
  # Backed by {TreeStand::Visitors::TreeWalker}.
136
247
  #
@@ -154,15 +265,6 @@ module TreeStand
154
265
  enumerator
155
266
  end
156
267
 
157
- # @example
158
- # node.text # => "4"
159
- # node.parent.text # => "3 * 4"
160
- # node.parent.parent.text # => "1 + 3 * 4"
161
- sig { returns(TreeStand::Node) }
162
- def parent
163
- TreeStand::Node.new(@tree, @ts_node.parent)
164
- end
165
-
166
268
  # @example
167
269
  # node.text # => "3 * 4"
168
270
  # node.to_a.map(&:text) # => ["3", "*", "4"]
@@ -174,7 +276,7 @@ module TreeStand
174
276
  # wraps the parent {TreeStand::Tree #tree} and has access to the source document.
175
277
  sig { returns(String) }
176
278
  def text
177
- T.must(@tree.document[@ts_node.start_byte...@ts_node.end_byte])
279
+ T.must(@tree.document.byteslice(@ts_node.start_byte...@ts_node.end_byte))
178
280
  end
179
281
 
180
282
  # This class overrides the `method_missing` method to delegate to the
@@ -193,10 +295,20 @@ module TreeStand
193
295
  #
194
296
  # @overload method_missing(method_name, *args, &block)
195
297
  # @raise [NoMethodError]
196
- def method_missing(method, *args, &block)
197
- return super unless @fields.include?(method.to_s)
198
-
199
- TreeStand::Node.new(@tree, T.unsafe(@ts_node).public_send(method, *args, &block))
298
+ def method_missing(method, *args, **kwargs, &block)
299
+ if thinly_wrapped?(method)
300
+ from(
301
+ T.unsafe(@ts_node)
302
+ .public_send(
303
+ THINLY_REMAPPED_METHODS[method] || method,
304
+ *args,
305
+ **kwargs,
306
+ &block
307
+ ),
308
+ )
309
+ else
310
+ super
311
+ end
200
312
  end
201
313
 
202
314
  sig { params(other: Object).returns(T::Boolean) }
@@ -217,8 +329,31 @@ module TreeStand
217
329
 
218
330
  private
219
331
 
220
- def respond_to_missing?(method, *)
221
- @fields.include?(method.to_s) || super
332
+ def respond_to_missing?(method, *_args, **_kwargs)
333
+ thinly_wrapped?(method) || super
334
+ end
335
+
336
+ def thinly_wrapped?(method)
337
+ @ts_node.fields.include?(method) || THINLY_WRAPPED_METHODS.include?(method)
338
+ end
339
+
340
+ # FIXME: Make more generic if needed in other classes.
341
+
342
+ # 1 instance of {TreeStand} from a {TreeSitter} equivalent.
343
+ def from_a(node)
344
+ node.is_a?(TreeSitter::Node) ? TreeStand::Node.new(@tree, node) : node
345
+ end
346
+
347
+ # {TreeSitter} instance, or a collection ({Array, Hash})
348
+ def from(obj)
349
+ case obj
350
+ when Array
351
+ obj.map { |n| from(n) }
352
+ when Hash
353
+ obj.to_h { |k, v| [from(k), from(v)] }
354
+ else
355
+ from_a(obj)
356
+ end
222
357
  end
223
358
  end
224
359
  end
@@ -1,6 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
  # typed: true
3
3
 
4
+ require 'pathname'
5
+
4
6
  module TreeStand
5
7
  # Wrapper around the TreeSitter parser. It looks up the parser by filename in
6
8
  # the configured parsers directory.
@@ -14,6 +16,14 @@ module TreeStand
14
16
  #
15
17
  # # Looks for a parser in `path/to/parser/folder/ruby.{so,dylib}`
16
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
17
27
  class Parser
18
28
  extend T::Sig
19
29
 
@@ -23,14 +33,160 @@ module TreeStand
23
33
  sig { returns(TreeSitter::Parser) }
24
34
  attr_reader :ts_parser
25
35
 
36
+ # A colon-seprated list of paths pointing to directories that can contain parsers.
37
+ # Order matters.
38
+ # Takes precedence over default lookup paths.
39
+ ENV_PARSERS =
40
+ ENV['TREE_SITTER_PARSERS']
41
+ &.split(':')
42
+ &.map { |v| Pathname(v) }
43
+ .freeze
44
+
45
+ # The default paths we use to lookup parsers when no specific
46
+ # {TreeStand::Config#parser_path} is `nil`.
47
+ # Order matters.
48
+ LIBDIRS = [
49
+ 'tree-sitter-parsers',
50
+ '/opt/local/lib',
51
+ '/opt/lib',
52
+ '/usr/local/lib',
53
+ '/usr/lib',
54
+ ].map { |p| Pathname(p) }.freeze
55
+
56
+ # The library directories we need to look into.
57
+ #
58
+ # @return [Array<Pathname>] the list of candidate places to use when searching for parsers.
59
+ #
60
+ # @see ENV_PARSERS
61
+ # @see LIBDIRS
62
+ sig { returns(T::Array[Pathname]) }
63
+ def self.lib_dirs = [
64
+ *ENV_PARSERS,
65
+ *(TreeStand.config.parser_path ? [TreeStand.config.parser_path] : LIBDIRS),
66
+ ].compact
67
+
68
+ # The platform-specific extension of the parser.
69
+ # @return [String] `dylib` or `so` for mac or linux.
70
+ sig { returns(String) }
71
+ def self.ext
72
+ case Gem::Platform.local.os
73
+ in /darwin/ then 'dylib'
74
+ else 'so'
75
+ end
76
+ end
77
+
78
+ # Lookup a parser by name.
79
+ #
80
+ # Precedence:
81
+ # 1. `Env['TREE_SITTER_PARSERS]`
82
+ # 2. {TreeStand::Config#parser_path}
83
+ # 3. {LIBDIRS}
84
+ #
85
+ # If a {TreeStand::Config#parser_path} is `nil`, {LIBDIRS} is used.
86
+ # If a {TreeStand::Config#parser_path} is a {::Pathname}, {LIBDIRS} is ignored.
87
+ sig { params(name: String).returns(T.nilable(Pathname)) }
88
+ def self.search_for_lib(name)
89
+ files = [
90
+ name,
91
+ "tree-sitter-#{name}",
92
+ "libtree-sitter-#{name}",
93
+ ].map { |v| "#{v}.#{ext}" }
94
+
95
+ res = lib_dirs
96
+ .product(files)
97
+ .find do |dir, so|
98
+ path = dir / so
99
+ path = dir / name / so if !path.exist?
100
+ break path.expand_path if path.exist?
101
+ end
102
+ res.is_a?(Array) ? nil : res
103
+ end
104
+
105
+ # Generates a string message on where parser lookup happens.
106
+ #
107
+ # @return [String] A pretty message.
108
+ sig { returns(String) }
109
+ def self.search_lib_message
110
+ indent = 2
111
+ pretty = ->(arr) {
112
+ if arr
113
+ arr
114
+ .compact
115
+ .map { |v| "#{' ' * indent}#{v.expand_path}" }
116
+ .join("\n")
117
+ end
118
+ }
119
+ <<~MSG
120
+ From ENV['TREE_SITTER_PARSERS']:
121
+ #{pretty.call(ENV_PARSERS)}
122
+
123
+ From TreeStand.config.parser_path:
124
+ #{pretty.call([TreeStand.config.parser_path])}
125
+
126
+ From Defaults:
127
+ #{pretty.call(LIBDIRS)}
128
+ MSG
129
+ end
130
+
131
+ # Load a language from configuration or default lookup paths.
132
+ #
133
+ # @example Load java from default paths
134
+ # # This will look for:
135
+ # #
136
+ # # tree-sitter-parsers/(java/)?(libtree-sitter-)?java.{ext}
137
+ # # /opt/local/lib/(java/)?(libtree-sitter-)?java.{ext}
138
+ # # /opt/lib/(java/)?(libtree-sitter-)?java.{ext}
139
+ # # /usr/local/lib/(java/)?(libtree-sitter-)?java.{ext}
140
+ # # /usr/lib/(java/)?(libtree-sitter-)?java.{ext}
141
+ # #
142
+ # java = TreeStand::Parser.language('java')
143
+ #
144
+ # @example Load java from a configured path
145
+ # # This will look for:
146
+ # #
147
+ # # /my/path/(java/)?(libtree-sitter-)?java.{ext}
148
+ # #
149
+ # TreeStand.config.parser_path = '/my/path'
150
+ # java = TreeStand::Parser.language('java')
151
+ #
152
+ # @example Load java from environment variables
153
+ # # This will look for:
154
+ # #
155
+ # # /my/forced/env/path/(java/)?(libtree-sitter-)?java.{ext}
156
+ # # /my/path/(java/)?(libtree-sitter-)?java.{ext}
157
+ # #
158
+ # # … and the same works for the default paths if `TreeStand.config.parser_path`
159
+ # # was `nil`
160
+ # ENV['TREE_SITTER_PARSERS'] = '/my/forced/env/path'
161
+ # TreeStand.config.parser_path = '/my/path'
162
+ # java = TreeStand::Parser.language('java')
163
+ #
164
+ # @param name [String] the name of the parser.
165
+ # This name is used to load the symbol from the compiled parser, replacing `-` with `_`.
166
+ # @return [TreeSitter:language] a language object to use in your parsers.
167
+ # @raise [RuntimeError] if the parser was not found.
168
+ # @see search_for_lib
169
+ sig { params(name: String).returns(TreeSitter::Language) }
170
+ def self.language(name)
171
+ lib = search_for_lib(name)
172
+
173
+ if lib.nil?
174
+ raise <<~MSG
175
+ Failed to load a parser for #{name}.
176
+
177
+ #{search_lib_message}
178
+ MSG
179
+ end
180
+
181
+ # We know that the bindings will accept `lib`, but I don't know how to tell sorbet
182
+ # the types in ext/tree_sitter where `load` is defined.
183
+ TreeSitter::Language.load(name.tr('-', '_'), T.unsafe(lib))
184
+ end
185
+
26
186
  # @param language [String]
27
187
  sig { params(language: String).void }
28
188
  def initialize(language)
29
- @language_string = language
30
- @ts_language = TreeSitter::Language.load(
31
- language,
32
- "#{TreeStand.config.parser_path}/#{language}.so",
33
- )
189
+ @ts_language = Parser.language(language)
34
190
  @ts_parser = TreeSitter::Parser.new.tap do |parser|
35
191
  parser.language = @ts_language
36
192
  end
metadata CHANGED
@@ -1,15 +1,15 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ruby_tree_sitter
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.1.0
4
+ version: 1.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Firas al-Khalil
8
8
  - Derek Stride
9
- autorequire:
9
+ autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2024-05-10 00:00:00.000000000 Z
12
+ date: 2024-06-26 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: sorbet-runtime
@@ -39,7 +39,7 @@ dependencies:
39
39
  - - ">="
40
40
  - !ruby/object:Gem::Version
41
41
  version: '0'
42
- description:
42
+ description:
43
43
  email:
44
44
  - firasalkhalil@gmail.com
45
45
  - derek@stride.host
@@ -75,7 +75,15 @@ files:
75
75
  - ext/tree_sitter/tree_sitter.c
76
76
  - ext/tree_sitter/tree_sitter.h
77
77
  - lib/tree_sitter.rb
78
+ - lib/tree_sitter/helpers.rb
78
79
  - lib/tree_sitter/node.rb
80
+ - lib/tree_sitter/query.rb
81
+ - lib/tree_sitter/query_captures.rb
82
+ - lib/tree_sitter/query_cursor.rb
83
+ - lib/tree_sitter/query_match.rb
84
+ - lib/tree_sitter/query_matches.rb
85
+ - lib/tree_sitter/query_predicate.rb
86
+ - lib/tree_sitter/text_predicate_capture.rb
79
87
  - lib/tree_sitter/version.rb
80
88
  - lib/tree_stand.rb
81
89
  - lib/tree_stand/ast_modifier.rb
@@ -99,7 +107,7 @@ metadata:
99
107
  source_code_uri: https://www.github.com/Faveod/ruby-tree-sitter
100
108
  changelog_uri: https://www.github.com/Faveod/ruby-tree-sitter
101
109
  documentation_uri: https://faveod.github.io/ruby-tree-sitter/
102
- post_install_message:
110
+ post_install_message:
103
111
  rdoc_options: []
104
112
  require_paths:
105
113
  - lib
@@ -115,7 +123,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
115
123
  version: '0'
116
124
  requirements: []
117
125
  rubygems_version: 3.4.19
118
- signing_key:
126
+ signing_key:
119
127
  specification_version: 4
120
128
  summary: Ruby bindings for Tree-Sitter
121
129
  test_files: []