1use crate::text_buffer::TextBuffer;
8use unicode_width::UnicodeWidthChar;
9
10#[derive(Clone, Debug, PartialEq, Eq)]
12pub struct WrapLine {
13 pub text: String,
15 pub logical_line: usize,
17 pub start_col: usize,
20}
21
22#[derive(Clone, Debug)]
24pub struct WrapResult {
25 pub lines: Vec<WrapLine>,
27 pub line_number_width: u16,
29}
30
31pub fn wrap_line(text: &str, width: usize) -> Vec<(String, usize)> {
42 if width == 0 {
43 return vec![(text.to_string(), 0)];
44 }
45
46 if text.is_empty() {
47 return vec![(String::new(), 0)];
48 }
49
50 let mut result = Vec::new();
51 let mut current_line = String::new();
52 let mut current_width: usize = 0;
53 let mut line_start_col: usize = 0;
54
55 for (char_col, ch) in text.chars().enumerate() {
56 let ch_width = ch.width().unwrap_or(0);
57
58 if current_width + ch_width > width && !current_line.is_empty() {
59 if let Some(space_byte_idx) = find_last_space(¤t_line) {
61 let before: String = current_line[..space_byte_idx].to_string();
63 let after: String = current_line[space_byte_idx..].trim_start().to_string();
64 let before_char_count = before.chars().count();
65 result.push((before, line_start_col));
66 current_width = display_width_of(&after);
67 line_start_col +=
68 before_char_count + count_trimmed_spaces(¤t_line[space_byte_idx..]);
69 current_line = after;
70 } else {
71 result.push((current_line.clone(), line_start_col));
73 line_start_col = char_col;
74 current_line = String::new();
75 current_width = 0;
76 }
77 }
78
79 current_line.push(ch);
80 current_width += ch_width;
81 }
82
83 if !current_line.is_empty() || result.is_empty() {
84 result.push((current_line, line_start_col));
85 }
86
87 result
88}
89
90pub fn wrap_lines(buffer: &TextBuffer, width: usize) -> WrapResult {
92 let total_lines = buffer.line_count();
93 let mut lines = Vec::new();
94
95 for line_idx in 0..total_lines {
96 if let Some(line_text) = buffer.line(line_idx) {
97 let wrapped = wrap_line(&line_text, width);
98 for (text, start_col) in wrapped {
99 lines.push(WrapLine {
100 text,
101 logical_line: line_idx,
102 start_col,
103 });
104 }
105 }
106 }
107
108 let lnw = line_number_width(total_lines);
109 WrapResult {
110 lines,
111 line_number_width: lnw,
112 }
113}
114
115pub fn line_number_width(line_count: usize) -> u16 {
120 if line_count == 0 {
121 return 1;
122 }
123 let digits = (line_count as f64).log10().floor() as u16 + 1;
124 digits.max(1)
125}
126
127fn display_width_of(text: &str) -> usize {
129 text.chars().map(|c| c.width().unwrap_or(0)).sum()
130}
131
132fn find_last_space(text: &str) -> Option<usize> {
134 text.rfind(' ')
135}
136
137fn count_trimmed_spaces(text: &str) -> usize {
139 text.chars().take_while(|c| *c == ' ').count()
140}
141
142#[cfg(test)]
143mod tests {
144 use super::*;
145
146 #[test]
149 fn short_line_no_wrap() {
150 let result = wrap_line("hello", 20);
151 assert!(result.len() == 1);
152 assert!(result[0].0 == "hello");
153 assert!(result[0].1 == 0);
154 }
155
156 #[test]
157 fn exact_width_no_wrap() {
158 let result = wrap_line("12345", 5);
159 assert!(result.len() == 1);
160 assert!(result[0].0 == "12345");
161 }
162
163 #[test]
164 fn overflow_by_one_char() {
165 let result = wrap_line("123456", 5);
166 assert!(result.len() == 2);
167 }
168
169 #[test]
170 fn word_wrap() {
171 let result = wrap_line("hello world foo", 12);
172 assert!(result.len() == 2);
173 assert!(result[0].0 == "hello world");
174 assert!(result[1].0 == "foo");
175 }
176
177 #[test]
178 fn long_word_break() {
179 let result = wrap_line("abcdefghij", 5);
180 assert!(result.len() == 2);
181 assert!(result[0].0 == "abcde");
182 assert!(result[1].0 == "fghij");
183 }
184
185 #[test]
186 fn cjk_characters_width_2() {
187 let result = wrap_line("日本語テスト", 6);
189 assert!(result.len() == 2);
191 assert!(result[0].0 == "日本語");
192 assert!(result[1].0 == "テスト");
193 }
194
195 #[test]
196 fn mixed_content() {
197 let result = wrap_line("abc日本", 5);
198 assert!(result.len() == 2);
200 assert!(result[0].0 == "abc日");
201 assert!(result[1].0 == "本");
203 }
204
205 #[test]
206 fn empty_line() {
207 let result = wrap_line("", 10);
208 assert!(result.len() == 1);
209 assert!(result[0].0.is_empty());
210 }
211
212 #[test]
213 fn single_char_line() {
214 let result = wrap_line("x", 10);
215 assert!(result.len() == 1);
216 assert!(result[0].0 == "x");
217 }
218
219 #[test]
222 fn line_number_width_small() {
223 assert!(line_number_width(1) == 1);
224 assert!(line_number_width(9) == 1);
225 }
226
227 #[test]
228 fn line_number_width_medium() {
229 assert!(line_number_width(10) == 2);
230 assert!(line_number_width(99) == 2);
231 assert!(line_number_width(100) == 3);
232 }
233
234 #[test]
235 fn line_number_width_zero() {
236 assert!(line_number_width(0) == 1);
237 }
238
239 #[test]
242 fn wrap_buffer_multiline() {
243 let buf = TextBuffer::from_text("short\nthis is a longer line");
244 let result = wrap_lines(&buf, 10);
245 assert!(result.lines.len() >= 3);
248 assert!(result.lines[0].logical_line == 0);
249 assert!(result.lines[1].logical_line == 1);
250 }
251
252 #[test]
253 fn wrap_result_line_number_width() {
254 let buf = TextBuffer::from_text("a\nb\nc\nd\ne\nf\ng\nh\ni\nj");
255 let result = wrap_lines(&buf, 80);
256 assert!(result.line_number_width == 2); }
258}