1use crate::text_utils::{
2 char_len, drop_one_leading_whitespace, leading_whitespace, split_at_char,
3 split_at_last_whitespace_before,
4};
5
6pub(crate) fn wrap_line_preserving_whitespace(
7 line: &str,
8 line_width: usize,
9) -> Vec<String> {
10 if line_width == 0 {
11 return vec![String::new()];
12 }
13
14 if char_len(line) <= line_width {
15 return vec![line.to_string()];
16 }
17 if line.trim_matches([' ', '\t']).is_empty() {
18 return vec![String::new()];
19 }
20
21 let trimmed_start = line.trim_start_matches([' ', '\t']);
22 let original_indent_chars = char_len(line) - char_len(trimmed_start);
23 if !trimmed_start.is_empty() && char_len(trimmed_start) <= line_width {
24 let clamped_indent = line_width.saturating_sub(char_len(trimmed_start));
25 if original_indent_chars > 20 && clamped_indent < original_indent_chars {
30 return vec![format!("{}{}", " ".repeat(clamped_indent), trimmed_start)];
31 }
32 }
33
34 let indent = leading_whitespace(line).to_string();
35 let indent_chars = char_len(&indent);
36 let max_continuation_indent =
37 line_width.saturating_sub(8).min(32).min(indent_chars);
38 let continuation_indent = " ".repeat(max_continuation_indent);
39 let continuation_indent_chars = max_continuation_indent;
40 let mut out = Vec::new();
41
42 let mut remainder = line;
43 let mut is_first = true;
44
45 loop {
46 let available = if is_first {
47 line_width
48 } else {
49 line_width.saturating_sub(continuation_indent_chars)
50 };
51
52 if available == 0 {
53 let (w1, w2) = split_at_char(remainder, 1);
54 out.push(if is_first {
55 w1.to_string()
56 } else {
57 format!("{continuation_indent}{w1}")
58 });
59 remainder = w2.unwrap_or("");
60 if remainder.is_empty() {
61 break;
62 }
63 is_first = false;
64 continue;
65 }
66
67 if char_len(remainder) <= available {
68 out.push(if is_first {
69 remainder.to_string()
70 } else {
71 format!("{continuation_indent}{remainder}")
72 });
73 break;
74 }
75
76 let split_byte_idx = split_at_last_whitespace_before(remainder, available)
77 .unwrap_or_else(|| {
78 let (w1, _) = split_at_char(remainder, available);
79 w1.len()
80 });
81
82 let (chunk, rest) = remainder.split_at(split_byte_idx);
83 let chunk_without_trailing_ws = chunk.trim_end_matches([' ', '\t']);
84 if chunk_without_trailing_ws.is_empty() {
85 if is_first {
86 let trimmed_remainder = remainder.trim_start_matches([' ', '\t']);
87 if !trimmed_remainder.is_empty()
88 && char_len(trimmed_remainder) <= line_width
89 {
90 let right_aligned_indent =
91 line_width.saturating_sub(char_len(trimmed_remainder));
92 out.push(format!(
93 "{}{}",
94 " ".repeat(right_aligned_indent),
95 trimmed_remainder
96 ));
97 break;
98 }
99 }
100
101 remainder = rest.trim_start_matches([' ', '\t']);
102 is_first = false;
103 if remainder.is_empty() {
104 out.push(String::new());
105 break;
106 }
107 continue;
108 }
109
110 out.push(if is_first {
111 chunk_without_trailing_ws.to_string()
112 } else {
113 format!("{continuation_indent}{chunk_without_trailing_ws}")
114 });
115
116 remainder = drop_one_leading_whitespace(rest);
117 is_first = false;
118
119 if remainder.is_empty() {
120 break;
121 }
122 }
123
124 out
125}
126
127pub fn wrap_preserve_whitespace(text: &str, line_width: usize) -> Vec<String> {
128 let mut out = Vec::new();
129 for line in text.split('\n') {
130 if line.is_empty() {
131 out.push(String::new());
132 continue;
133 }
134 out.extend(wrap_line_preserving_whitespace(line, line_width));
135 }
136 out
137}
138
139#[cfg(test)]
140mod tests {
141 use super::wrap_preserve_whitespace;
142 use crate::text_utils::char_len;
143
144 #[test]
145 fn preserves_indentation_and_spacing() {
146 let input = "a b c";
147 let out = wrap_preserve_whitespace(input, 80);
148 assert_eq!(out, vec![input.to_string()]);
149
150 let input =
151 " fn main() { println!(\"hi\"); println!(\"there\"); }";
152 let out = wrap_preserve_whitespace(input, 25);
153 assert!(out.len() > 1);
154 assert!(out[0].starts_with(" "));
155 assert!(out[1].starts_with(" "));
156 }
157
158 #[test]
159 fn avoids_vertical_splitting_for_extreme_indentation() {
160 let input = format!("{}Contents", " ".repeat(105));
161 let out = wrap_preserve_whitespace(&input, 80);
162 assert!(out.iter().any(|line| line.contains("Contents")));
163 assert!(out.len() <= 3, "expected compact output, got: {out:?}");
164 assert!(
165 !out.iter().any(|line| line.trim() == "C"),
166 "expected no single-letter vertical split, got: {out:?}"
167 );
168 assert!(
169 out
170 .iter()
171 .filter(|line| !line.is_empty())
172 .all(|line| char_len(line) <= 80),
173 "expected wrapped lines to respect width, got: {out:?}"
174 );
175 }
176
177 #[test]
178 fn preserves_right_position_for_overindented_short_labels() {
179 let input = format!("{}Content", " ".repeat(86));
180 let out = wrap_preserve_whitespace(&input, 80);
181
182 assert_eq!(out.len(), 1, "expected single wrapped line, got: {out:?}");
183 assert_eq!(
184 char_len(&out[0]),
185 80,
186 "expected width-clamped output, got: {out:?}"
187 );
188 assert!(
189 out[0].ends_with("Content"),
190 "expected label text to be preserved, got: {out:?}"
191 );
192 let leading = out[0].chars().take_while(|&ch| ch == ' ').count();
193 assert!(
194 leading >= 70,
195 "expected right-positioned label instead of collapsed continuation indent, got: {out:?}"
196 );
197 }
198}