llm_coding_tools_core/
util.rs

1//! Shared utilities for tool implementations.
2
3/// Generous estimate of average characters per line for buffer pre-allocation.
4pub const ESTIMATED_CHARS_PER_LINE: usize = 64;
5
6/// A number of characters per line that's likely to not be exceeded in most files.
7pub const LIKELY_CHARS_PER_LINE_MAX: usize = ESTIMATED_CHARS_PER_LINE * 4;
8
9/// Formats a line with its line number for output.
10///
11/// Uses the format: `{spaces}{line_number}\t{content}` where spaces
12/// pad the line number to align with the widest number in the range.
13#[inline]
14pub fn format_numbered_line(line_number: usize, content: &str, max_line_number: usize) -> String {
15    let width = max_line_number.checked_ilog10().unwrap_or(0) as usize + 1;
16    format!("{:>width$}\t{}", line_number, content)
17}
18
19/// Truncates text to a maximum byte length, appending a truncation notice.
20///
21/// Returns `(truncated_text, was_truncated)`.
22pub fn truncate_text(text: &str, max_bytes: usize) -> (&str, bool) {
23    if text.len() <= max_bytes {
24        return (text, false);
25    }
26
27    // Find a valid UTF-8 boundary before max_bytes
28    let mut end = max_bytes;
29    while end > 0 && !text.is_char_boundary(end) {
30        end -= 1;
31    }
32
33    (&text[..end], true)
34}
35
36/// Truncates a single line to a maximum character count.
37pub fn truncate_line(line: &str, max_chars: usize) -> (&str, bool) {
38    // Fast path: UTF-8 guarantees byte_count >= char_count,
39    // so if byte length fits, no truncation needed.
40    if line.len() <= max_chars {
41        return (line, false);
42    }
43
44    // Find byte position at max_chars character boundary
45    let Some((byte_pos, _)) = line.char_indices().nth(max_chars) else {
46        // Fewer than max_chars characters exist
47        return (line, false);
48    };
49
50    (&line[..byte_pos], true)
51}
52
53#[cfg(test)]
54mod tests {
55    use super::*;
56
57    #[test]
58    fn format_numbered_line_pads_correctly() {
59        assert_eq!(format_numbered_line(1, "hello", 9), "1\thello");
60        assert_eq!(format_numbered_line(1, "hello", 10), " 1\thello");
61        assert_eq!(format_numbered_line(1, "hello", 100), "  1\thello");
62    }
63
64    #[test]
65    fn truncate_text_preserves_short_text() {
66        let (text, truncated) = truncate_text("hello", 10);
67        assert_eq!(text, "hello");
68        assert!(!truncated);
69    }
70
71    #[test]
72    fn truncate_text_truncates_long_text() {
73        let (text, truncated) = truncate_text("hello world", 5);
74        assert_eq!(text, "hello");
75        assert!(truncated);
76    }
77
78    #[test]
79    fn truncate_text_respects_utf8_boundaries() {
80        // "héllo" has é which is 2 bytes
81        let (text, truncated) = truncate_text("héllo", 2);
82        assert_eq!(text, "h");
83        assert!(truncated);
84    }
85
86    #[test]
87    fn truncate_line_preserves_short_line() {
88        let (line, truncated) = truncate_line("hello", 10);
89        assert_eq!(line, "hello");
90        assert!(!truncated);
91    }
92
93    #[test]
94    fn truncate_line_truncates_by_char_count() {
95        let (line, truncated) = truncate_line("héllo", 3);
96        assert_eq!(line, "hél");
97        assert!(truncated);
98    }
99}