tree_sitter 0.0.1

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.
@@ -0,0 +1,45 @@
1
+ use magnus::{RArray, Ruby};
2
+
3
+ #[magnus::wrap(class = "TreeSitter::Point")]
4
+ #[derive(Clone)]
5
+ pub struct Point {
6
+ row: usize,
7
+ column: usize,
8
+ }
9
+
10
+ impl Point {
11
+ pub fn new(row: usize, column: usize) -> Self {
12
+ Self { row, column }
13
+ }
14
+
15
+ pub fn from_ts(point: tree_sitter::Point) -> Self {
16
+ Self {
17
+ row: point.row,
18
+ column: point.column,
19
+ }
20
+ }
21
+
22
+ pub fn row(&self) -> usize {
23
+ self.row
24
+ }
25
+
26
+ pub fn column(&self) -> usize {
27
+ self.column
28
+ }
29
+
30
+ pub fn to_a(&self) -> RArray {
31
+ let ruby = Ruby::get().unwrap();
32
+ let array = ruby.ary_new();
33
+ let _ = array.push(self.row);
34
+ let _ = array.push(self.column);
35
+ array
36
+ }
37
+
38
+ pub fn inspect(&self) -> String {
39
+ format!("#<TreeSitter::Point row={} column={}>", self.row, self.column)
40
+ }
41
+
42
+ pub fn eq(&self, other: &Point) -> bool {
43
+ self.row == other.row && self.column == other.column
44
+ }
45
+ }
@@ -0,0 +1,161 @@
1
+ use crate::language::Language;
2
+ use crate::node::Node;
3
+ use magnus::{Error, RArray, Ruby};
4
+ use std::cell::RefCell;
5
+ use streaming_iterator::StreamingIterator;
6
+
7
+ #[magnus::wrap(class = "TreeSitter::Query")]
8
+ pub struct Query {
9
+ inner: tree_sitter::Query,
10
+ capture_names: Vec<String>,
11
+ }
12
+
13
+ impl Query {
14
+ pub fn new(language: &Language, source: String) -> Result<Self, Error> {
15
+ let ruby = Ruby::get().unwrap();
16
+
17
+ let query = tree_sitter::Query::new(&language.inner, &source).map_err(|e| {
18
+ Error::new(
19
+ ruby.exception_syntax_error(),
20
+ format!("Query syntax error: {}", e),
21
+ )
22
+ })?;
23
+
24
+ let capture_names = query.capture_names().iter().map(|s| s.to_string()).collect();
25
+
26
+ Ok(Self {
27
+ inner: query,
28
+ capture_names,
29
+ })
30
+ }
31
+
32
+ pub fn capture_names(&self) -> RArray {
33
+ let ruby = Ruby::get().unwrap();
34
+ let array = ruby.ary_new();
35
+ for name in &self.capture_names {
36
+ let _ = array.push(name.clone());
37
+ }
38
+ array
39
+ }
40
+
41
+ pub fn pattern_count(&self) -> usize {
42
+ self.inner.pattern_count()
43
+ }
44
+ }
45
+
46
+ #[magnus::wrap(class = "TreeSitter::QueryCursor")]
47
+ pub struct QueryCursor {
48
+ inner: RefCell<tree_sitter::QueryCursor>,
49
+ }
50
+
51
+ impl QueryCursor {
52
+ pub fn new() -> Self {
53
+ Self {
54
+ inner: RefCell::new(tree_sitter::QueryCursor::new()),
55
+ }
56
+ }
57
+
58
+ pub fn matches(
59
+ &self,
60
+ query: &Query,
61
+ node: &Node,
62
+ source: String,
63
+ ) -> RArray {
64
+ let ruby = Ruby::get().unwrap();
65
+ let array = ruby.ary_new();
66
+ let Some(ts_node) = node.get_ts_node_pub() else {
67
+ return array;
68
+ };
69
+
70
+ let mut cursor = self.inner.borrow_mut();
71
+ let mut matches = cursor.matches(&query.inner, ts_node, source.as_bytes());
72
+
73
+ while let Some(m) = matches.next() {
74
+ let captures: Vec<QueryCapture> = m
75
+ .captures
76
+ .iter()
77
+ .map(|c| {
78
+ let capture_name = query.capture_names[c.index as usize].clone();
79
+ QueryCapture {
80
+ name: capture_name,
81
+ node: Node::new(c.node, node.source.clone(), node.tree.clone()),
82
+ }
83
+ })
84
+ .collect();
85
+
86
+ let _ = array.push(QueryMatch {
87
+ pattern_index: m.pattern_index,
88
+ captures,
89
+ });
90
+ }
91
+
92
+ array
93
+ }
94
+
95
+ pub fn captures(
96
+ &self,
97
+ query: &Query,
98
+ node: &Node,
99
+ source: String,
100
+ ) -> RArray {
101
+ let ruby = Ruby::get().unwrap();
102
+ let array = ruby.ary_new();
103
+ let Some(ts_node) = node.get_ts_node_pub() else {
104
+ return array;
105
+ };
106
+
107
+ let mut cursor = self.inner.borrow_mut();
108
+ let mut captures = cursor.captures(&query.inner, ts_node, source.as_bytes());
109
+
110
+ while let Some((m, capture_index)) = captures.next() {
111
+ if let Some(c) = m.captures.get(*capture_index) {
112
+ let capture_name = query.capture_names[c.index as usize].clone();
113
+ let _ = array.push(QueryCapture {
114
+ name: capture_name,
115
+ node: Node::new(c.node, node.source.clone(), node.tree.clone()),
116
+ });
117
+ }
118
+ }
119
+
120
+ array
121
+ }
122
+ }
123
+
124
+ #[magnus::wrap(class = "TreeSitter::QueryMatch")]
125
+ #[derive(Clone)]
126
+ pub struct QueryMatch {
127
+ pattern_index: usize,
128
+ captures: Vec<QueryCapture>,
129
+ }
130
+
131
+ impl QueryMatch {
132
+ pub fn pattern_index(&self) -> usize {
133
+ self.pattern_index
134
+ }
135
+
136
+ pub fn captures(&self) -> RArray {
137
+ let ruby = Ruby::get().unwrap();
138
+ let array = ruby.ary_new();
139
+ for capture in &self.captures {
140
+ let _ = array.push(capture.clone());
141
+ }
142
+ array
143
+ }
144
+ }
145
+
146
+ #[magnus::wrap(class = "TreeSitter::QueryCapture")]
147
+ #[derive(Clone)]
148
+ pub struct QueryCapture {
149
+ name: String,
150
+ node: Node,
151
+ }
152
+
153
+ impl QueryCapture {
154
+ pub fn name(&self) -> String {
155
+ self.name.clone()
156
+ }
157
+
158
+ pub fn node(&self) -> Node {
159
+ self.node.clone()
160
+ }
161
+ }
@@ -0,0 +1,50 @@
1
+ use crate::point::Point;
2
+
3
+ #[magnus::wrap(class = "TreeSitter::Range")]
4
+ #[derive(Clone)]
5
+ pub struct Range {
6
+ start_byte: usize,
7
+ end_byte: usize,
8
+ start_point: Point,
9
+ end_point: Point,
10
+ }
11
+
12
+ impl Range {
13
+ pub fn new(start_byte: usize, end_byte: usize, start_point: Point, end_point: Point) -> Self {
14
+ Self {
15
+ start_byte,
16
+ end_byte,
17
+ start_point,
18
+ end_point,
19
+ }
20
+ }
21
+
22
+ pub fn start_byte(&self) -> usize {
23
+ self.start_byte
24
+ }
25
+
26
+ pub fn end_byte(&self) -> usize {
27
+ self.end_byte
28
+ }
29
+
30
+ pub fn start_point(&self) -> Point {
31
+ self.start_point.clone()
32
+ }
33
+
34
+ pub fn end_point(&self) -> Point {
35
+ self.end_point.clone()
36
+ }
37
+
38
+ pub fn size(&self) -> usize {
39
+ self.end_byte - self.start_byte
40
+ }
41
+
42
+ pub fn inspect(&self) -> String {
43
+ format!(
44
+ "#<TreeSitter::Range start_byte={} end_byte={} size={}>",
45
+ self.start_byte,
46
+ self.end_byte,
47
+ self.size()
48
+ )
49
+ }
50
+ }
@@ -0,0 +1,38 @@
1
+ use crate::language::{get_language_internal, Language};
2
+ use crate::node::Node;
3
+ use magnus::Error;
4
+ use std::sync::Arc;
5
+
6
+ #[magnus::wrap(class = "TreeSitter::Tree")]
7
+ pub struct Tree {
8
+ pub inner: Arc<tree_sitter::Tree>,
9
+ pub source: Arc<String>,
10
+ pub language_name: String,
11
+ }
12
+
13
+ impl Tree {
14
+ pub fn new(tree: tree_sitter::Tree, source: String, language_name: String) -> Self {
15
+ Self {
16
+ inner: Arc::new(tree),
17
+ source: Arc::new(source),
18
+ language_name,
19
+ }
20
+ }
21
+
22
+ pub fn root_node(&self) -> Node {
23
+ let ts_node = self.inner.root_node();
24
+ Node::new(ts_node, self.source.clone(), self.inner.clone())
25
+ }
26
+
27
+ pub fn source(&self) -> String {
28
+ (*self.source).clone()
29
+ }
30
+
31
+ pub fn language(&self) -> Result<Language, Error> {
32
+ let ts_lang = get_language_internal(&self.language_name)?;
33
+ Ok(Language {
34
+ name: self.language_name.clone(),
35
+ inner: ts_lang,
36
+ })
37
+ }
38
+ }
@@ -0,0 +1,236 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TreeSitter
4
+ # Formatting utilities for syntax-aware code manipulation
5
+ module Formatting
6
+ # Detects and works with indentation in source code
7
+ class IndentationDetector
8
+ # Characters considered whitespace for indentation
9
+ INDENT_CHARS = [" ", "\t"].freeze
10
+
11
+ attr_reader :source, :indent_string, :indent_size, :style
12
+
13
+ # Initialize detector with source code
14
+ #
15
+ # @param source [String] The source code to analyze
16
+ def initialize(source)
17
+ @source = source
18
+ @lines = source.lines
19
+ detect
20
+ end
21
+
22
+ # Detect the indentation style used in the source
23
+ #
24
+ # @return [Hash] { style: :spaces|:tabs|:unknown, size: Integer, string: String }
25
+ def detect
26
+ space_indents = []
27
+ tab_count = 0
28
+ space_count = 0
29
+
30
+ @lines.each do |line|
31
+ next if line.strip.empty?
32
+
33
+ leading = line[/\A[ \t]*/]
34
+ next if leading.empty?
35
+
36
+ if leading.include?("\t")
37
+ tab_count += 1
38
+ else
39
+ space_count += 1
40
+ # Track indent sizes for space-indented lines
41
+ space_indents << leading.length if leading.length.positive?
42
+ end
43
+ end
44
+
45
+ if tab_count > space_count
46
+ @style = :tabs
47
+ @indent_size = 1
48
+ @indent_string = "\t"
49
+ elsif space_count.positive?
50
+ @style = :spaces
51
+ @indent_size = detect_space_indent_size(space_indents)
52
+ @indent_string = " " * @indent_size
53
+ else
54
+ # Default to 4 spaces
55
+ @style = :spaces
56
+ @indent_size = 4
57
+ @indent_string = " "
58
+ end
59
+
60
+ { style: @style, size: @indent_size, string: @indent_string }
61
+ end
62
+
63
+ # Get indentation level (count) at a specific line
64
+ #
65
+ # @param line_number [Integer] Zero-based line number
66
+ # @return [Integer] Number of indentation units
67
+ def level_at_line(line_number)
68
+ return 0 if line_number < 0 || line_number >= @lines.length
69
+
70
+ line = @lines[line_number]
71
+ leading = line[/\A[ \t]*/] || ""
72
+
73
+ return leading.count("\t") if @style == :tabs
74
+
75
+ leading.length / [@indent_size, 1].max
76
+ end
77
+
78
+ # Get raw indentation string at a specific line
79
+ #
80
+ # @param line_number [Integer] Zero-based line number
81
+ # @return [String] The indentation whitespace
82
+ def raw_indentation_at_line(line_number)
83
+ return "" if line_number < 0 || line_number >= @lines.length
84
+
85
+ line = @lines[line_number]
86
+ line[/\A[ \t]*/] || ""
87
+ end
88
+
89
+ # Get indentation string at a specific byte position
90
+ #
91
+ # @param byte_pos [Integer] Byte position in source
92
+ # @return [String] The indentation whitespace for that line
93
+ def indentation_at_byte(byte_pos)
94
+ line_number = byte_to_line(byte_pos)
95
+ raw_indentation_at_line(line_number)
96
+ end
97
+
98
+ # Get indentation level at a specific byte position
99
+ #
100
+ # @param byte_pos [Integer] Byte position in source
101
+ # @return [Integer] Indentation level
102
+ def level_at_byte(byte_pos)
103
+ line_number = byte_to_line(byte_pos)
104
+ level_at_line(line_number)
105
+ end
106
+
107
+ # Create indentation string for a given level
108
+ #
109
+ # @param level [Integer] Indentation level
110
+ # @return [String] Indentation whitespace
111
+ def indent_string_for_level(level)
112
+ return "" if level <= 0
113
+
114
+ @indent_string * level
115
+ end
116
+
117
+ # Adjust indentation of content to a target level
118
+ #
119
+ # @param content [String] Content to adjust
120
+ # @param target_level [Integer] Target indentation level
121
+ # @param current_level [Integer, nil] Current base level (auto-detected if nil)
122
+ # @return [String] Re-indented content
123
+ def adjust_indentation(content, target_level, current_level: nil)
124
+ content_lines = content.lines
125
+ return content if content_lines.empty?
126
+
127
+ # Auto-detect current level from first non-empty line
128
+ if current_level.nil?
129
+ first_content_line = content_lines.find { |l| !l.strip.empty? }
130
+ if first_content_line
131
+ leading = first_content_line[/\A[ \t]*/] || ""
132
+ current_level = if @style == :tabs
133
+ leading.count("\t")
134
+ else
135
+ leading.length / [@indent_size, 1].max
136
+ end
137
+ else
138
+ current_level = 0
139
+ end
140
+ end
141
+
142
+ level_diff = target_level - current_level
143
+
144
+ content_lines.map do |line|
145
+ if line.strip.empty?
146
+ line
147
+ else
148
+ leading = line[/\A[ \t]*/] || ""
149
+ rest = line[leading.length..]
150
+
151
+ # Calculate this line's level relative to base
152
+ line_level = if @style == :tabs
153
+ leading.count("\t")
154
+ else
155
+ leading.length / [@indent_size, 1].max
156
+ end
157
+
158
+ # Apply the level difference
159
+ new_level = [line_level + level_diff, 0].max
160
+ indent_string_for_level(new_level) + rest
161
+ end
162
+ end.join
163
+ end
164
+
165
+ # Increase indentation of all lines by one level
166
+ #
167
+ # @param content [String] Content to indent
168
+ # @return [String] Indented content
169
+ def indent(content)
170
+ content.lines.map do |line|
171
+ if line.strip.empty?
172
+ line
173
+ else
174
+ @indent_string + line
175
+ end
176
+ end.join
177
+ end
178
+
179
+ # Decrease indentation of all lines by one level
180
+ #
181
+ # @param content [String] Content to dedent
182
+ # @return [String] Dedented content
183
+ def dedent(content)
184
+ content.lines.map do |line|
185
+ if line.strip.empty?
186
+ line
187
+ elsif @style == :tabs && line.start_with?("\t")
188
+ line[1..]
189
+ elsif @style == :spaces && line.start_with?(@indent_string)
190
+ line[@indent_size..]
191
+ else
192
+ line
193
+ end
194
+ end.join
195
+ end
196
+
197
+ private
198
+
199
+ # Detect the most common space indent size
200
+ def detect_space_indent_size(indents)
201
+ return 4 if indents.empty?
202
+
203
+ # Find GCD of all indent sizes to determine base unit
204
+ differences = []
205
+ sorted = indents.uniq.sort
206
+
207
+ sorted.each_cons(2) do |a, b|
208
+ differences << (b - a)
209
+ end
210
+
211
+ # Also consider the smallest non-zero indent
212
+ differences << sorted.first if sorted.first&.positive?
213
+
214
+ return 4 if differences.empty?
215
+
216
+ # Find GCD
217
+ gcd = differences.reduce { |a, b| a.gcd(b) }
218
+ gcd = 4 if gcd.nil? || gcd <= 0 || gcd > 8
219
+
220
+ gcd
221
+ end
222
+
223
+ # Convert byte position to line number (zero-based)
224
+ def byte_to_line(byte_pos)
225
+ current_byte = 0
226
+ @lines.each_with_index do |line, idx|
227
+ line_end = current_byte + line.bytesize
228
+ return idx if byte_pos < line_end
229
+
230
+ current_byte = line_end
231
+ end
232
+ [@lines.length - 1, 0].max
233
+ end
234
+ end
235
+ end
236
+ end