Skip to main content

omni_dev/jev/
input.rs

1//! Shared input-text truncation for Jev calls (#1779).
2//!
3//! Both `route` (an issue's text) and `verify-decision` (a cited source's
4//! text) cap the text they send to Jev the same way: keep the first and last
5//! halves, with a marker in between, so the cut is visible to Jev rather
6//! than silent. The tail matters for `route` because a decision comment
7//! lands late; for `verify-decision`'s source text the same policy is kept
8//! for consistency, though no source has been observed to need it.
9
10/// Marks where an input over the cap was cut.
11pub const TRUNCATION_MARKER: &str = "[... truncated]";
12
13/// Truncates `text` to `max_chars`, keeping its first and last halves with
14/// [`TRUNCATION_MARKER`] between them.
15///
16/// Returns the (possibly unchanged) text and whether it was truncated. Cuts
17/// on a `char` boundary, never inside a multi-byte UTF-8 sequence.
18#[must_use]
19pub fn truncate_middle(text: &str, max_chars: usize) -> (String, bool) {
20    if text.char_indices().nth(max_chars).is_none() {
21        return (text.to_string(), false);
22    }
23    let total = text.chars().count();
24    let head = max_chars / 2;
25    let tail = max_chars - head;
26    let byte_at = |chars: usize| {
27        text.char_indices()
28            .nth(chars)
29            .map_or(text.len(), |(i, _)| i)
30    };
31    let (head_end, tail_start) = (byte_at(head), byte_at(total - tail));
32    let cut = format!(
33        "{}\n\n{TRUNCATION_MARKER}\n\n{}",
34        &text[..head_end],
35        &text[tail_start..]
36    );
37    (cut, true)
38}
39
40#[cfg(test)]
41#[allow(clippy::unwrap_used, clippy::expect_used)]
42mod tests {
43    use super::*;
44
45    #[test]
46    fn short_text_is_unchanged() {
47        let (out, truncated) = truncate_middle("short", 1000);
48        assert_eq!(out, "short");
49        assert!(!truncated);
50    }
51
52    #[test]
53    fn truncates_on_a_char_boundary() {
54        let text = "é".repeat(100);
55        let (full, _) = truncate_middle(&text, usize::MAX);
56        let (out, truncated) = truncate_middle(&text, 20);
57        assert!(truncated);
58        let (head, tail) = out
59            .split_once(&format!("\n\n{TRUNCATION_MARKER}\n\n"))
60            .unwrap();
61        assert_eq!(head.chars().count(), 10);
62        assert_eq!(tail.chars().count(), 10);
63        assert!(full.starts_with(head));
64        assert!(full.ends_with(tail));
65    }
66
67    #[test]
68    fn handles_a_one_char_cap() {
69        // head = 0, tail = 1: no head text, just the very last character.
70        let (out, truncated) = truncate_middle("hello world", 1);
71        assert!(truncated);
72        assert_eq!(out, format!("\n\n{TRUNCATION_MARKER}\n\nd"));
73    }
74
75    #[test]
76    fn exactly_at_the_cap_is_not_truncated() {
77        let (out, truncated) = truncate_middle("hello", 5);
78        assert_eq!(out, "hello");
79        assert!(!truncated);
80    }
81}