Skip to main content

edikt_core/
wrap.rs

1//! Comment wrapping - the shared "use the space as it exists" rule.
2//!
3//! Head/foot comment text wraps to the document's own width envelope so a
4//! comment never makes the file wider than it already is; the caller supplies
5//! the node's indent and the delimiter width (`"# "` = 2, `"// "` = 3). Inline
6//! comments never wrap - the caller skips this. See the wrapping section of
7//! `docs/design/comments-as-first-class.md`.
8
9/// The absolute wrap column for `source`: `clamp(longest line, 80, 100)`. The
10/// ceiling stops a wide file yielding 200-char comment lines; the floor keeps a
11/// flat `.env` from wrapping prose to its 9-char keys.
12pub fn wrap_width(source: &str) -> usize {
13    let longest = source.lines().map(|l| l.chars().count()).max().unwrap_or(0);
14    longest.clamp(80, 100)
15}
16
17/// Wrap `text` into lines whose rendered form (`<indent><delim><line>`) fits in
18/// `width` columns. Greedy fill; a single word longer than the budget overflows
19/// rather than hard-breaking. Returns the text of each line (no indent/delim);
20/// always at least one line (empty for empty text).
21pub fn wrap_comment(text: &str, width: usize, indent: usize, delim_cols: usize) -> Vec<String> {
22    let budget = width.saturating_sub(indent + delim_cols).max(1);
23    let mut lines: Vec<String> = Vec::new();
24    let mut cur = String::new();
25    let mut cur_len = 0usize;
26    for word in text.split_whitespace() {
27        let w = word.chars().count();
28        if cur.is_empty() {
29            cur.push_str(word);
30            cur_len = w;
31        } else if cur_len + 1 + w <= budget {
32            cur.push(' ');
33            cur.push_str(word);
34            cur_len += 1 + w;
35        } else {
36            lines.push(std::mem::take(&mut cur));
37            cur.push_str(word);
38            cur_len = w;
39        }
40    }
41    lines.push(cur);
42    lines
43}
44
45#[cfg(test)]
46mod tests {
47    use super::*;
48
49    #[test]
50    fn width_clamps_to_the_band() {
51        assert_eq!(wrap_width("PORT=8080\n"), 80); // narrow -> floor
52        assert_eq!(wrap_width(&format!("{}\n", "x".repeat(90))), 90); // tracks
53        assert_eq!(wrap_width(&format!("{}\n", "x".repeat(200))), 100); // ceiling
54        assert_eq!(wrap_width(""), 80); // empty -> floor
55    }
56
57    #[test]
58    fn wraps_greedily_to_the_budget() {
59        // width 20, indent 0, delim "# " (2) -> budget 18.
60        let out = wrap_comment("the quick brown fox jumps", 20, 0, 2);
61        assert_eq!(out, vec!["the quick brown", "fox jumps"]);
62    }
63
64    #[test]
65    fn long_word_overflows_not_breaks() {
66        let out = wrap_comment("see https://example.com/a/very/long/path now", 30, 0, 2);
67        // The URL is longer than the 28-col budget but stays on its own line.
68        assert_eq!(
69            out,
70            vec!["see", "https://example.com/a/very/long/path", "now"]
71        );
72    }
73
74    #[test]
75    fn empty_text_is_one_empty_line() {
76        assert_eq!(wrap_comment("", 80, 0, 2), vec![String::new()]);
77    }
78}