1use std::fs;
2use std::path::Path;
3use thiserror::Error;
4
5use crate::models::Symbol;
6
7#[derive(Debug, Error)]
8pub enum SliceError {
9 #[error("Failed to read file '{0}': {1}")]
10 Io(String, #[source] std::io::Error),
11 #[error("Invalid slice range: start {0} > end {1}")]
12 InvalidRange(usize, usize),
13 #[error("File '{0}' is empty")]
14 EmptyFile(String),
15}
16
17fn snap_to_char_boundary_floor(bytes: &[u8], mut idx: usize) -> usize {
19 if idx >= bytes.len() {
20 return bytes.len();
21 }
22 while idx > 0 && (bytes[idx] & 0xC0) == 0x80 {
23 idx -= 1;
24 }
25 idx
26}
27
28fn snap_to_char_boundary_ceil(bytes: &[u8], mut idx: usize) -> usize {
30 if idx >= bytes.len() {
31 return bytes.len();
32 }
33 while idx < bytes.len() && (bytes[idx] & 0xC0) == 0x80 {
34 idx += 1;
35 }
36 idx
37}
38
39pub fn slice_bytes_safe(
41 content: &[u8],
42 start_byte: usize,
43 end_byte: usize,
44) -> Result<&str, SliceError> {
45 if start_byte > end_byte {
46 return Err(SliceError::InvalidRange(start_byte, end_byte));
47 }
48
49 let start = snap_to_char_boundary_floor(content, start_byte.min(content.len()));
50 let end = snap_to_char_boundary_ceil(content, end_byte.min(content.len()));
51
52 std::str::from_utf8(&content[start..end]).map_err(|_| SliceError::InvalidRange(start, end))
53}
54
55pub fn slice_symbol(file_path: &Path, symbol: &Symbol) -> Result<String, SliceError> {
57 let bytes =
58 fs::read(file_path).map_err(|e| SliceError::Io(file_path.display().to_string(), e))?;
59
60 if bytes.is_empty() {
61 return Err(SliceError::EmptyFile(file_path.display().to_string()));
62 }
63
64 if symbol.end_byte <= bytes.len()
66 && symbol.start_byte <= symbol.end_byte
67 && symbol.end_byte > 0
68 && let Ok(slice) = slice_bytes_safe(&bytes, symbol.start_byte, symbol.end_byte)
69 {
70 return Ok(slice.to_string());
71 }
72
73 slice_lines(&bytes, symbol.start_line, symbol.end_line)
75}
76
77pub fn slice_symbol_source(
80 file_path: &Path,
81 symbol: &Symbol,
82 start_byte: usize,
83) -> Result<String, SliceError> {
84 let bytes =
85 fs::read(file_path).map_err(|e| SliceError::Io(file_path.display().to_string(), e))?;
86 if bytes.is_empty() {
87 return Err(SliceError::EmptyFile(file_path.display().to_string()));
88 }
89 if symbol.end_byte <= bytes.len() && start_byte < symbol.end_byte {
90 return Ok(
91 slice_bytes_safe(&bytes, indent_start(&bytes, start_byte), symbol.end_byte)?
92 .to_string(),
93 );
94 }
95 slice_lines(&bytes, symbol.start_line, symbol.end_line)
96}
97
98pub fn slice_symbol_body(file_path: &Path, symbol: &Symbol) -> Result<String, SliceError> {
100 let bytes =
101 fs::read(file_path).map_err(|e| SliceError::Io(file_path.display().to_string(), e))?;
102
103 if bytes.is_empty() {
104 return Err(SliceError::EmptyFile(file_path.display().to_string()));
105 }
106
107 if let (Some(body_start), Some(body_end)) = (symbol.body_start_byte, symbol.body_end_byte)
109 && body_end <= bytes.len()
110 && body_start <= body_end
111 && let Ok(slice) = slice_bytes_safe(&bytes, indent_start(&bytes, body_start), body_end)
112 {
113 return Ok(slice.to_string());
114 }
115
116 if let (Some(b_start), Some(b_end)) = (symbol.body_start_line, symbol.body_end_line) {
118 return slice_lines(&bytes, b_start, b_end);
119 }
120
121 slice_symbol(file_path, symbol)
123}
124
125fn indent_start(bytes: &[u8], start: usize) -> usize {
128 let line_start = bytes[..start]
129 .iter()
130 .rposition(|&b| b == b'\n')
131 .map_or(0, |newline| newline + 1);
132 if bytes[line_start..start]
133 .iter()
134 .all(|&b| b == b' ' || b == b'\t')
135 {
136 line_start
137 } else {
138 start
139 }
140}
141
142pub fn slice_lines(bytes: &[u8], start_line: usize, end_line: usize) -> Result<String, SliceError> {
144 if start_line > end_line {
145 return Err(SliceError::InvalidRange(start_line, end_line));
146 }
147
148 let text = String::from_utf8_lossy(bytes);
149 let mut selected = Vec::new();
150
151 for (idx, line) in text.lines().enumerate() {
152 let line_num = idx + 1;
153 if line_num >= start_line && line_num <= end_line {
154 selected.push(line);
155 }
156 if line_num > end_line {
157 break;
158 }
159 }
160
161 Ok(selected.join("\n"))
162}
163
164#[cfg(test)]
165mod tests {
166 use super::*;
167
168 #[test]
169 fn a_body_on_its_own_line_keeps_its_first_indentation() {
170 let source = b"def f():\n x = 1\n return x\n";
171 let body_start = 13;
172 assert_eq!(&source[body_start..body_start + 5], b"x = 1");
173 assert_eq!(indent_start(source, body_start), 9);
174 let brace = b"fn f() { 1 }";
175 assert_eq!(indent_start(brace, 7), 7);
176 }
177
178 #[test]
179 fn test_slice_bytes_safe() {
180 let sample = "fn hello() {\n println!(\"hi\");\n}\n";
181 let sliced = slice_bytes_safe(sample.as_bytes(), 0, sample.len()).unwrap();
182 assert_eq!(sliced, sample);
183 }
184
185 #[test]
186 fn test_slice_lines() {
187 let sample = "line 1\nline 2\nline 3\nline 4\n";
188 let sliced = slice_lines(sample.as_bytes(), 2, 3).unwrap();
189 assert_eq!(sliced, "line 2\nline 3");
190 }
191}