tree_sitter 0.1.0-x86_64-linux

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,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
@@ -0,0 +1,306 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "formatting"
4
+
5
+ module TreeSitter
6
+ # Syntax-aware insertions that respect indentation and formatting.
7
+ #
8
+ # @example Insert a new statement with proper indentation
9
+ # Inserter.new(source, tree)
10
+ # .at_end_of(block_node)
11
+ # .insert_statement("return result;")
12
+ # .rewrite
13
+ #
14
+ # @example Insert a sibling function
15
+ # Inserter.new(source, tree)
16
+ # .after(existing_fn)
17
+ # .insert_sibling("fn new_func() {}")
18
+ # .rewrite
19
+ #
20
+ class Inserter
21
+ # Represents a pending insertion
22
+ Insertion = Struct.new(:byte_pos, :content, :newline_before, :newline_after, keyword_init: true)
23
+
24
+ attr_reader :source, :tree
25
+
26
+ # Initialize a new Inserter
27
+ #
28
+ # @param source [String] The source code
29
+ # @param tree [TreeSitter::Tree] The parsed syntax tree
30
+ # @param parser [TreeSitter::Parser, nil] Optional parser for re-parsing
31
+ def initialize(source, tree, parser: nil)
32
+ @source = source.dup.freeze
33
+ @tree = tree
34
+ @parser = parser
35
+ @indent_detector = Formatting::IndentationDetector.new(source)
36
+ @insertions = []
37
+ @insertion_point = nil
38
+ @insertion_context = nil
39
+ end
40
+
41
+ # Set insertion point at the beginning of a node's content (inside the node)
42
+ #
43
+ # @param node [TreeSitter::Node] Container node (e.g., a block)
44
+ # @return [self] For method chaining
45
+ def at_start_of(node)
46
+ # Find the first child's start, or just after the opening
47
+ # For blocks like { ... }, we want to insert after the opening brace
48
+ @insertion_context = :inside_start
49
+ @insertion_node = node
50
+ @target_indent_level = @indent_detector.level_at_byte(node.start_byte) + 1
51
+
52
+ # Find position just after opening delimiter
53
+ first_child = node.named_child(0)
54
+ if first_child
55
+ @insertion_point = first_child.start_byte
56
+ else
57
+ # Empty block - find end of opening line or after opening brace
58
+ node_text = @source[node.start_byte...node.end_byte]
59
+ @insertion_point = if (brace_pos = node_text.index("{"))
60
+ node.start_byte + brace_pos + 1
61
+ else
62
+ node.start_byte + 1
63
+ end
64
+ end
65
+
66
+ self
67
+ end
68
+
69
+ # Set insertion point at the end of a node's content (inside the node)
70
+ #
71
+ # @param node [TreeSitter::Node] Container node
72
+ # @return [self] For method chaining
73
+ def at_end_of(node)
74
+ @insertion_context = :inside_end
75
+ @insertion_node = node
76
+ @target_indent_level = @indent_detector.level_at_byte(node.start_byte) + 1
77
+
78
+ # Find position just before closing delimiter
79
+ node_text = @source[node.start_byte...node.end_byte]
80
+ @insertion_point = if (brace_pos = node_text.rindex("}"))
81
+ node.start_byte + brace_pos
82
+ else
83
+ node.end_byte
84
+ end
85
+
86
+ self
87
+ end
88
+
89
+ # Set insertion point before a node (as sibling)
90
+ #
91
+ # @param node [TreeSitter::Node] Reference node
92
+ # @return [self] For method chaining
93
+ def before(node)
94
+ @insertion_context = :before
95
+ @insertion_node = node
96
+ @insertion_point = node.start_byte
97
+ @target_indent_level = @indent_detector.level_at_byte(node.start_byte)
98
+ self
99
+ end
100
+
101
+ # Set insertion point after a node (as sibling)
102
+ #
103
+ # @param node [TreeSitter::Node] Reference node
104
+ # @return [self] For method chaining
105
+ def after(node)
106
+ @insertion_context = :after
107
+ @insertion_node = node
108
+ @insertion_point = node.end_byte
109
+ @target_indent_level = @indent_detector.level_at_byte(node.start_byte)
110
+ self
111
+ end
112
+
113
+ # Insert a statement with automatic indentation
114
+ #
115
+ # @param content [String] The statement to insert
116
+ # @param newline_before [Boolean] Add newline before (default: context-dependent)
117
+ # @param newline_after [Boolean] Add newline after (default: true)
118
+ # @return [self] For method chaining
119
+ def insert_statement(content, newline_before: nil, newline_after: true)
120
+ raise "No insertion point set. Call at_start_of, at_end_of, before, or after first." unless @insertion_point
121
+
122
+ # Determine newline_before based on context
123
+ newline_before = newline_before? if newline_before.nil?
124
+
125
+ # Adjust indentation of content
126
+ adjusted_content = adjust_content_indentation(content)
127
+
128
+ @insertions << Insertion.new(
129
+ byte_pos: @insertion_point,
130
+ content: adjusted_content,
131
+ newline_before: newline_before,
132
+ newline_after: newline_after,
133
+ )
134
+ self
135
+ end
136
+
137
+ # Insert raw content without indentation adjustment
138
+ #
139
+ # @param content [String] The content to insert
140
+ # @return [self] For method chaining
141
+ def insert_raw(content)
142
+ raise "No insertion point set. Call at_start_of, at_end_of, before, or after first." unless @insertion_point
143
+
144
+ @insertions << Insertion.new(
145
+ byte_pos: @insertion_point,
146
+ content: content,
147
+ newline_before: false,
148
+ newline_after: false,
149
+ )
150
+ self
151
+ end
152
+
153
+ # Insert a sibling node with matching indentation
154
+ #
155
+ # @param content [String] The sibling content
156
+ # @param separator [String] Separator between siblings (default: newlines based on context)
157
+ # @return [self] For method chaining
158
+ def insert_sibling(content, separator: nil)
159
+ raise "No insertion point set. Call before or after first." unless @insertion_point
160
+
161
+ separator ||= "\n\n" # Default to blank line between top-level items
162
+
163
+ # Adjust indentation of content
164
+ adjusted_content = adjust_content_indentation(content)
165
+
166
+ # For after insertion, add separator before content
167
+ # For before insertion, add separator after content
168
+ full_content = case @insertion_context
169
+ when :after
170
+ separator + adjusted_content
171
+ when :before
172
+ adjusted_content + separator
173
+ else
174
+ adjusted_content
175
+ end
176
+
177
+ @insertions << Insertion.new(
178
+ byte_pos: @insertion_point,
179
+ content: full_content,
180
+ newline_before: false,
181
+ newline_after: false,
182
+ )
183
+ self
184
+ end
185
+
186
+ # Insert a block with proper indentation (for block constructs)
187
+ #
188
+ # @param header [String] The block header (e.g., "if condition")
189
+ # @param body [String] The block body content (will be indented)
190
+ # @param open_brace [String] Opening delimiter (default: " {")
191
+ # @param close_brace [String] Closing delimiter (default: "}")
192
+ # @return [self] For method chaining
193
+ def insert_block(header, body, open_brace: " {", close_brace: "}")
194
+ raise "No insertion point set. Call at_start_of, at_end_of, before, or after first." unless @insertion_point
195
+
196
+ indent = @indent_detector.indent_string_for_level(@target_indent_level)
197
+ body_indent = @indent_detector.indent_string_for_level(@target_indent_level + 1)
198
+
199
+ # Build the block with proper indentation
200
+ indented_body = body.lines.map do |line|
201
+ if line.strip.empty?
202
+ line
203
+ else
204
+ body_indent + line.lstrip
205
+ end
206
+ end.join
207
+
208
+ block_content = "#{indent}#{header}#{open_brace}\n#{indented_body}\n#{indent}#{close_brace}"
209
+
210
+ newline_before = newline_before?
211
+
212
+ @insertions << Insertion.new(
213
+ byte_pos: @insertion_point,
214
+ content: block_content,
215
+ newline_before: newline_before,
216
+ newline_after: true,
217
+ )
218
+ self
219
+ end
220
+
221
+ # Apply all insertions
222
+ #
223
+ # @return [String] The source with insertions
224
+ def rewrite
225
+ return @source if @insertions.empty?
226
+
227
+ # Sort by position descending to apply from end to start
228
+ sorted = @insertions.sort_by { |ins| -ins.byte_pos }
229
+
230
+ result = @source.dup
231
+ sorted.each do |insertion|
232
+ content = insertion.content
233
+ content = "\n#{content}" if insertion.newline_before
234
+ content = "#{content}\n" if insertion.newline_after
235
+
236
+ result.insert(insertion.byte_pos, content)
237
+ end
238
+ result
239
+ end
240
+
241
+ # Apply insertions and return both source and new tree
242
+ #
243
+ # @return [Array<String, Tree>] The new source and re-parsed tree
244
+ def rewrite_with_tree
245
+ new_source = rewrite
246
+
247
+ parser = @parser || create_parser_from_tree
248
+ raise "No parser available for re-parsing" unless parser
249
+
250
+ new_tree = parser.parse(new_source)
251
+ [new_source, new_tree]
252
+ end
253
+
254
+ # Reset insertion point to allow setting a new one
255
+ #
256
+ # @return [self] For method chaining
257
+ def reset_position
258
+ @insertion_point = nil
259
+ @insertion_context = nil
260
+ @insertion_node = nil
261
+ @target_indent_level = nil
262
+ self
263
+ end
264
+
265
+ private
266
+
267
+ def newline_before?
268
+ case @insertion_context
269
+ when :inside_start
270
+ # After opening brace, usually need newline
271
+ # But check if there's already content on the same line
272
+ true
273
+ when :inside_end
274
+ # Before closing brace, check if we need newline
275
+ # Look at what's before the insertion point
276
+ before_text = @source[0...@insertion_point]
277
+ last_newline = before_text.rindex("\n")
278
+ content_after_newline = last_newline ? before_text[(last_newline + 1)..] : before_text
279
+ # If there's only whitespace after the last newline, we might not need another
280
+ !content_after_newline.strip.empty?
281
+ when :before, :after
282
+ # Siblings usually don't need newline before (handled by separator)
283
+ false
284
+ else
285
+ true
286
+ end
287
+ end
288
+
289
+ def adjust_content_indentation(content)
290
+ return content if content.strip.empty?
291
+
292
+ @indent_detector.adjust_indentation(content.strip, @target_indent_level, current_level: 0)
293
+ end
294
+
295
+ def create_parser_from_tree
296
+ return unless @tree
297
+
298
+ parser = TreeSitter::Parser.new
299
+ lang = @tree.language
300
+ parser.language = lang.name if lang
301
+ parser
302
+ rescue StandardError
303
+ nil
304
+ end
305
+ end
306
+ end