Skip to main content

gobject_ast/model/
source_location.rs

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