Skip to main content

code_kb_core/
slicer.rs

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
17/// Snap byte offset to the nearest UTF-8 character boundary <= index.
18fn 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
28/// Snap byte offset to the nearest UTF-8 character boundary >= index.
29fn 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
39/// Slices exact byte content from an in-memory buffer with safe UTF-8 snapping.
40pub 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
55/// Slice the entire symbol declaration (signature + body) from file on disk.
56pub 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    // Try byte span first
65    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    // Fallback to line slicing
74    slice_lines(&bytes, symbol.start_line, symbol.end_line)
75}
76
77/// Slice the symbol's source as written, from `start_byte` (a decorator above the symbol, or the
78/// symbol's own start) to the symbol's end, starting at the beginning of that line.
79pub 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
98/// Slice only the symbol's implementation body from file on disk.
99pub 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    // Check if body byte spans are present
108    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    // Fallback to body line span
117    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    // If no body defined, return entire symbol
122    slice_symbol(file_path, symbol)
123}
124
125/// The start of `start`'s line when only indentation precedes it, so a body that begins on its
126/// own line (a Python block) keeps its first line's indentation like the lines after it.
127fn 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
142/// Fallback 1-based line slicer
143pub 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}