Skip to main content

seshat_core/
snippet.rs

1//! Code snippet truncation utilities.
2//!
3//! Shared by `seshat-graph` (convention evidence) and `seshat-mcp` (response
4//! envelopes) to avoid duplicating the struct and truncation logic.
5
6use serde::{Deserialize, Serialize};
7
8/// Maximum number of lines in a code snippet before truncation.
9pub const MAX_SNIPPET_LINES: usize = 20;
10
11/// A code snippet that may be truncated.
12#[derive(Debug, Clone, Serialize, Deserialize)]
13pub struct CodeSnippet {
14    /// The (possibly truncated) snippet content.
15    pub content: String,
16    /// `true` when the original snippet exceeded [`MAX_SNIPPET_LINES`].
17    pub truncated: bool,
18}
19
20/// Truncate a code snippet to at most [`MAX_SNIPPET_LINES`] lines.
21///
22/// Returns a [`CodeSnippet`] with `truncated: true` when lines were removed.
23pub fn truncate_snippet(raw: &str) -> CodeSnippet {
24    truncate_snippet_to(raw, MAX_SNIPPET_LINES)
25}
26
27/// Truncate a code snippet to at most `max_lines` lines.
28///
29/// Use this when a context-specific limit differs from [`MAX_SNIPPET_LINES`].
30pub fn truncate_snippet_to(raw: &str, max_lines: usize) -> CodeSnippet {
31    let lines: Vec<&str> = raw.lines().collect();
32    if lines.len() > max_lines {
33        CodeSnippet {
34            content: lines[..max_lines].join("\n"),
35            truncated: true,
36        }
37    } else {
38        CodeSnippet {
39            content: raw.to_owned(),
40            truncated: false,
41        }
42    }
43}
44
45// ── Tests ────────────────────────────────────────────────────
46
47#[cfg(test)]
48mod tests {
49    use super::*;
50
51    #[test]
52    fn short_content_is_not_truncated() {
53        let snippet = truncate_snippet("line 1\nline 2\nline 3");
54        assert!(!snippet.truncated);
55        assert_eq!(snippet.content, "line 1\nline 2\nline 3");
56    }
57
58    #[test]
59    fn exact_limit_is_not_truncated() {
60        let lines: Vec<String> = (1..=MAX_SNIPPET_LINES)
61            .map(|i| format!("line {i}"))
62            .collect();
63        let raw = lines.join("\n");
64        let result = truncate_snippet(&raw);
65        assert!(!result.truncated);
66        assert_eq!(result.content, raw);
67    }
68
69    #[test]
70    fn over_limit_is_truncated() {
71        let lines: Vec<String> = (1..=25).map(|i| format!("line {i}")).collect();
72        let raw = lines.join("\n");
73        let result = truncate_snippet(&raw);
74        assert!(result.truncated);
75        let result_lines: Vec<&str> = result.content.lines().collect();
76        assert_eq!(result_lines.len(), MAX_SNIPPET_LINES);
77        assert_eq!(result_lines[0], "line 1");
78        assert_eq!(
79            result_lines[MAX_SNIPPET_LINES - 1],
80            format!("line {MAX_SNIPPET_LINES}")
81        );
82    }
83}