1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
use std::fmt;
use serde::{Serialize, Serializer};
/// Source location information for AST nodes
#[derive(Clone, Copy, Default)]
pub struct SourceLocation {
pub line: usize,
pub column: usize,
pub start_byte: usize,
pub end_byte: usize,
}
impl fmt::Debug for SourceLocation {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}:{}", self.line, self.column)
}
}
impl Serialize for SourceLocation {
fn serialize<S: Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
s.serialize_str(&format!("{}:{}", self.line, self.column))
}
}
impl SourceLocation {
pub fn new(line: usize, column: usize, start_byte: usize, end_byte: usize) -> Self {
Self {
line,
column,
start_byte,
end_byte,
}
}
/// Extract the source text for this location
pub fn as_str<'a>(&self, source: &'a [u8]) -> Option<&'a str> {
std::str::from_utf8(&source[self.start_byte..self.end_byte]).ok()
}
/// Find the byte position of the start of the line containing this location
fn find_line_start(&self, source: &[u8]) -> usize {
let mut line_start = self.start_byte;
while line_start > 0 && source[line_start - 1] != b'\n' {
line_start -= 1;
}
line_start
}
/// Find the start and end byte positions of the line containing this
/// location Returns (line_start_byte, line_end_byte) including the
/// newline If the previous line is empty (only whitespace), includes it
/// too
pub fn find_line_bounds(&self, source: &[u8]) -> (usize, usize) {
// Find the start of the line
let mut line_start = self.find_line_start(source);
// Check if the previous line is empty (only whitespace)
if line_start > 0 {
let mut prev_line_start = line_start - 1; // Skip the '\n'
while prev_line_start > 0 && source[prev_line_start - 1] != b'\n' {
prev_line_start -= 1;
}
// Check if the line is only whitespace
let prev_line = &source[prev_line_start..line_start - 1];
if prev_line.iter().all(|&b| b == b' ' || b == b'\t') {
line_start = prev_line_start;
}
}
// Find the end of the line (including newline)
let mut line_end = self.start_byte;
while line_end < source.len() && source[line_end] != b'\n' {
line_end += 1;
}
// Include the newline character
if line_end < source.len() && source[line_end] == b'\n' {
line_end += 1;
}
(line_start, line_end)
}
/// Find the start and end byte positions of the line containing this
/// location, including any following blank line
/// Returns (line_start_byte, line_end_byte) including newlines
pub fn find_line_bounds_with_following_blank(&self, source: &[u8]) -> (usize, usize) {
// Find the start of the line
let line_start = self.find_line_start(source);
// Find the end of the line (including newline)
let mut line_end = self.end_byte;
while line_end < source.len() && source[line_end] != b'\n' {
line_end += 1;
}
if line_end < source.len() {
line_end += 1; // Include the newline
}
// Check if the next line is blank (only whitespace)
if line_end < source.len() {
let mut pos = line_end;
let mut is_blank = true;
while pos < source.len() && source[pos] != b'\n' {
if !source[pos].is_ascii_whitespace() {
is_blank = false;
break;
}
pos += 1;
}
// If next line is blank, include it in the removal
if is_blank && pos < source.len() {
line_end = pos + 1; // Include the newline of the blank line
}
}
(line_start, line_end)
}
/// Extract indentation (leading whitespace) from the line containing this
/// location. Returns all leading spaces/tabs from the start of the line.
pub fn extract_line_indentation(&self, source: &[u8]) -> String {
let line_start = self.find_line_start(source);
// Extract all leading whitespace from the line
let mut indent = String::new();
let mut i = line_start;
while i < source.len() && (source[i] == b' ' || source[i] == b'\t') {
indent.push(source[i] as char);
i += 1;
}
indent
}
/// Extract indentation (leading whitespace) up to this location
/// Returns the spaces/tabs from line start up to (but not past) the
/// location
pub fn extract_indentation(&self, source: &[u8]) -> String {
let line_start = self.find_line_start(source);
// Extract indentation (spaces/tabs before first non-whitespace or before
// location)
let mut indent = String::new();
for &byte in &source[line_start..self.start_byte] {
if byte == b' ' || byte == b'\t' {
indent.push(byte as char);
} else {
break;
}
}
indent
}
/// Scan forward from `end_byte` through whitespace to find a `;` and
/// return the byte position immediately after it. Returns `end_byte`
/// unchanged if no semicolon is found within a short distance.
pub fn find_semicolon_end(&self, source: &[u8]) -> usize {
let mut pos = self.end_byte;
while pos < source.len() {
match source[pos] {
b';' => return pos + 1,
b' ' | b'\t' | b'\r' | b'\n' => pos += 1,
_ => break,
}
}
self.end_byte
}
/// Find braces surrounding a range in the source
/// Returns (opening_brace_pos, closing_brace_pos) using depth tracking to
/// find matching braces
pub fn find_braces_around(start: usize, source: &[u8]) -> (usize, usize) {
// Search backwards from start to find '{'
let mut brace_start = start;
while brace_start > 0 && source[brace_start - 1] != b'{' {
brace_start -= 1;
if start - brace_start > 100 {
break;
}
}
if brace_start > 0 && source[brace_start - 1] == b'{' {
brace_start -= 1;
}
// Search forwards from opening brace to find matching closing brace using depth
// tracking
let mut brace_end = brace_start + 1;
let mut depth = 1;
while brace_end < source.len() && depth > 0 {
if source[brace_end] == b'{' {
depth += 1;
} else if source[brace_end] == b'}' {
depth -= 1;
}
brace_end += 1;
}
(brace_start, brace_end)
}
}