on 2158366 help sign in
535 lines 20 kB history raw
1use crate::text::StyledGrapheme;
2use helix_core::line_ending::str_is_line_ending;
3use helix_core::unicode::width::UnicodeWidthStr;
4use unicode_segmentation::UnicodeSegmentation;
6const NBSP: &str = "\u{00a0}";
8/// A state machine to pack styled symbols into lines.
9/// Cannot implement it as Iterator since it yields slices of the internal buffer (need streaming
10/// iterators for that).
11pub trait LineComposer<'a> {
12 fn next_line(&mut self) -> Option<(&[StyledGrapheme<'a>], u16)>;
15/// A state machine that wraps lines on word boundaries.
16pub struct WordWrapper<'a, 'b> {
17 symbols: &'b mut dyn Iterator<Item = StyledGrapheme<'a>>,
18 max_line_width: u16,
19 current_line: Vec<StyledGrapheme<'a>>,
20 next_line: Vec<StyledGrapheme<'a>>,
21 /// Removes the leading whitespace from lines
22 trim: bool,
25impl<'a, 'b> WordWrapper<'a, 'b> {
26 pub fn new(
27 symbols: &'b mut dyn Iterator<Item = StyledGrapheme<'a>>,
28 max_line_width: u16,
29 trim: bool,
30 ) -> WordWrapper<'a, 'b> {
31 WordWrapper {
32 symbols,
33 max_line_width,
34 current_line: vec![],
35 next_line: vec![],
36 trim,
37 }
38 }
41impl<'a, 'b> LineComposer<'a> for WordWrapper<'a, 'b> {
42 fn next_line(&mut self) -> Option<(&[StyledGrapheme<'a>], u16)> {
43 if self.max_line_width == 0 {
44 return None;
45 }
46 std::mem::swap(&mut self.current_line, &mut self.next_line);
47 self.next_line.truncate(0);
49 let mut current_line_width = self
50 .current_line
51 .iter()
52 .map(|StyledGrapheme { symbol, .. }| symbol.width() as u16)
53 .sum();
55 let mut symbols_to_last_word_end: usize = 0;
56 let mut width_to_last_word_end: u16 = 0;
57 let mut prev_whitespace = false;
58 let mut symbols_exhausted = true;
59 for StyledGrapheme { symbol, style } in &mut self.symbols {
60 symbols_exhausted = false;
61 let symbol_whitespace = symbol.chars().all(&char::is_whitespace) && symbol != NBSP;
63 // Ignore characters wider that the total max width.
64 if symbol.width() as u16 > self.max_line_width
65 // Skip leading whitespace when trim is enabled.
66 || self.trim && symbol_whitespace && !str_is_line_ending(symbol) && current_line_width == 0
67 {
68 continue;
69 }
71 // Break on newline and discard it.
72 if str_is_line_ending(symbol) {
73 if prev_whitespace {
74 current_line_width = width_to_last_word_end;
75 self.current_line.truncate(symbols_to_last_word_end);
76 }
77 break;
78 }
80 // Mark the previous symbol as word end.
81 if symbol_whitespace && !prev_whitespace {
82 symbols_to_last_word_end = self.current_line.len();
83 width_to_last_word_end = current_line_width;
84 }
86 self.current_line.push(StyledGrapheme { symbol, style });
87 current_line_width += symbol.width() as u16;
89 if current_line_width > self.max_line_width {
90 // If there was no word break in the text, wrap at the end of the line.
91 let (truncate_at, truncated_width) = if symbols_to_last_word_end != 0 {
92 (symbols_to_last_word_end, width_to_last_word_end)
93 } else {
94 (self.current_line.len() - 1, self.max_line_width)
95 };
97 // Push the remainder to the next line but strip leading whitespace:
98 {
99 let remainder = &self.current_line[truncate_at..];
100 if let Some(remainder_nonwhite) =
101 remainder.iter().position(|StyledGrapheme { symbol, .. }| {
102 !symbol.chars().all(&char::is_whitespace)
103 })
104 {
105 self.next_line
106 .extend_from_slice(&remainder[remainder_nonwhite..]);
107 }
108 }
109 self.current_line.truncate(truncate_at);
110 current_line_width = truncated_width;
111 break;
112 }
114 prev_whitespace = symbol_whitespace;
115 }
117 // Even if the iterator is exhausted, pass the previous remainder.
118 if symbols_exhausted && self.current_line.is_empty() {
119 None
120 } else {
121 Some((&self.current_line[..], current_line_width))
122 }
123 }
126/// A state machine that truncates overhanging lines.
127pub struct LineTruncator<'a, 'b> {
128 symbols: &'b mut dyn Iterator<Item = StyledGrapheme<'a>>,
129 max_line_width: u16,
130 current_line: Vec<StyledGrapheme<'a>>,
131 /// Record the offet to skip render
132 horizontal_offset: u16,
135impl<'a, 'b> LineTruncator<'a, 'b> {
136 pub fn new(
137 symbols: &'b mut dyn Iterator<Item = StyledGrapheme<'a>>,
138 max_line_width: u16,
139 ) -> LineTruncator<'a, 'b> {
140 LineTruncator {
141 symbols,
142 max_line_width,
143 horizontal_offset: 0,
144 current_line: vec![],
145 }
146 }
148 pub fn set_horizontal_offset(&mut self, horizontal_offset: u16) {
149 self.horizontal_offset = horizontal_offset;
150 }
153impl<'a, 'b> LineComposer<'a> for LineTruncator<'a, 'b> {
154 fn next_line(&mut self) -> Option<(&[StyledGrapheme<'a>], u16)> {
155 if self.max_line_width == 0 {
156 return None;
157 }
159 self.current_line.truncate(0);
160 let mut current_line_width = 0;
162 let mut skip_rest = false;
163 let mut symbols_exhausted = true;
164 let mut horizontal_offset = self.horizontal_offset as usize;
165 for StyledGrapheme { symbol, style } in &mut self.symbols {
166 symbols_exhausted = false;
168 // Ignore characters wider that the total max width.
169 if symbol.width() as u16 > self.max_line_width {
170 continue;
171 }
173 // Break on newline and discard it.
174 if str_is_line_ending(symbol) {
175 break;
176 }
178 if current_line_width + symbol.width() as u16 > self.max_line_width {
179 // Exhaust the remainder of the line.
180 skip_rest = true;
181 break;
182 }
184 let symbol = if horizontal_offset == 0 {
185 symbol
186 } else {
187 let w = symbol.width();
188 if w > horizontal_offset {
189 let t = trim_offset(symbol, horizontal_offset);
190 horizontal_offset = 0;
191 t
192 } else {
193 horizontal_offset -= w;
194 ""
195 }
196 };
197 current_line_width += symbol.width() as u16;
198 self.current_line.push(StyledGrapheme { symbol, style });
199 }
201 if skip_rest {
202 for StyledGrapheme { symbol, .. } in &mut self.symbols {
203 if str_is_line_ending(symbol) {
204 break;
205 }
206 }
207 }
209 if symbols_exhausted && self.current_line.is_empty() {
210 None
211 } else {
212 Some((&self.current_line[..], current_line_width))
213 }
214 }
217/// This function will return a str slice which start at specified offset.
218/// As src is a unicode str, start offset has to be calculated with each character.
219fn trim_offset(src: &str, mut offset: usize) -> &str {
220 let mut start = 0;
221 for c in UnicodeSegmentation::graphemes(src, true) {
222 let w = c.width();
223 if w <= offset {
224 offset -= w;
225 start += c.len();
226 } else {
227 break;
228 }
229 }
230 &src[start..]
233#[cfg(test)]
234mod test {
235 use super::*;
236 use unicode_segmentation::UnicodeSegmentation;
238 enum Composer {
239 WordWrapper { trim: bool },
240 LineTruncator,
241 }
243 fn run_composer(which: Composer, text: &str, text_area_width: u16) -> (Vec<String>, Vec<u16>) {
244 let style = Default::default();
245 let mut styled =
246 UnicodeSegmentation::graphemes(text, true).map(|g| StyledGrapheme { symbol: g, style });
247 let mut composer: Box<dyn LineComposer> = match which {
248 Composer::WordWrapper { trim } => {
249 Box::new(WordWrapper::new(&mut styled, text_area_width, trim))
250 }
251 Composer::LineTruncator => Box::new(LineTruncator::new(&mut styled, text_area_width)),
252 };
253 let mut lines = vec![];
254 let mut widths = vec![];
255 while let Some((styled, width)) = composer.next_line() {
256 let line = styled
257 .iter()
258 .map(|StyledGrapheme { symbol, .. }| *symbol)
259 .collect::<String>();
260 assert!(width <= text_area_width);
261 lines.push(line);
262 widths.push(width);
263 }
264 (lines, widths)
265 }
267 #[test]
268 fn line_composer_one_line() {
269 let width = 40;
270 for i in 1..width {
271 let text = "a".repeat(i);
272 let (word_wrapper, _) =
273 run_composer(Composer::WordWrapper { trim: true }, &text, width as u16);
274 let (line_truncator, _) = run_composer(Composer::LineTruncator, &text, width as u16);
275 let expected = vec![text];
276 assert_eq!(word_wrapper, expected);
277 assert_eq!(line_truncator, expected);
278 }
279 }
281 #[test]
282 fn line_composer_short_lines() {
283 let width = 20;
284 let text =
285 "abcdefg\nhijklmno\npabcdefg\nhijklmn\nopabcdefghijk\nlmnopabcd\n\n\nefghijklmno";
286 let (word_wrapper, _) = run_composer(Composer::WordWrapper { trim: true }, text, width);
287 let (line_truncator, _) = run_composer(Composer::LineTruncator, text, width);
289 let wrapped: Vec<&str> = text.split('\n').collect();
290 assert_eq!(word_wrapper, wrapped);
291 assert_eq!(line_truncator, wrapped);
292 }
294 #[test]
295 fn line_composer_long_word() {
296 let width = 20;
297 let text = "abcdefghijklmnopabcdefghijklmnopabcdefghijklmnopabcdefghijklmno";
298 let (word_wrapper, _) =
299 run_composer(Composer::WordWrapper { trim: true }, text, width as u16);
300 let (line_truncator, _) = run_composer(Composer::LineTruncator, text, width as u16);
302 let wrapped = vec![
303 &text[..width],
304 &text[width..width * 2],
305 &text[width * 2..width * 3],
306 &text[width * 3..],
307 ];
308 assert_eq!(
309 word_wrapper, wrapped,
310 "WordWrapper should detect the line cannot be broken on word boundary and \
311 break it at line width limit."
312 );
313 assert_eq!(line_truncator, vec![&text[..width]]);
314 }
316 #[test]
317 fn line_composer_long_sentence() {
318 let width = 20;
319 let text =
320 "abcd efghij klmnopabcd efgh ijklmnopabcdefg hijkl mnopab c d e f g h i j k l m n o";
321 let text_multi_space =
322 "abcd efghij klmnopabcd efgh ijklmnopabcdefg hijkl mnopab c d e f g h i j k l \
323 m n o";
324 let (word_wrapper_single_space, _) =
325 run_composer(Composer::WordWrapper { trim: true }, text, width as u16);
326 let (word_wrapper_multi_space, _) = run_composer(
327 Composer::WordWrapper { trim: true },
328 text_multi_space,
329 width as u16,
330 );
331 let (line_truncator, _) = run_composer(Composer::LineTruncator, text, width as u16);
333 let word_wrapped = vec![
334 "abcd efghij",
335 "klmnopabcd efgh",
336 "ijklmnopabcdefg",
337 "hijkl mnopab c d e f",
338 "g h i j k l m n o",
339 ];
340 assert_eq!(word_wrapper_single_space, word_wrapped);
341 assert_eq!(word_wrapper_multi_space, word_wrapped);
343 assert_eq!(line_truncator, vec![&text[..width]]);
344 }
346 #[test]
347 fn line_composer_zero_width() {
348 let width = 0;
349 let text = "abcd efghij klmnopabcd efgh ijklmnopabcdefg hijkl mnopab ";
350 let (word_wrapper, _) = run_composer(Composer::WordWrapper { trim: true }, text, width);
351 let (line_truncator, _) = run_composer(Composer::LineTruncator, text, width);
353 let expected: Vec<&str> = Vec::new();
354 assert_eq!(word_wrapper, expected);
355 assert_eq!(line_truncator, expected);
356 }
358 #[test]
359 fn line_composer_max_line_width_of_1() {
360 let width = 1;
361 let text = "abcd efghij klmnopabcd efgh ijklmnopabcdefg hijkl mnopab ";
362 let (word_wrapper, _) = run_composer(Composer::WordWrapper { trim: true }, text, width);
363 let (line_truncator, _) = run_composer(Composer::LineTruncator, text, width);
365 let expected: Vec<&str> = UnicodeSegmentation::graphemes(text, true)
366 .filter(|g| g.chars().any(|c| !c.is_whitespace()))
367 .collect();
368 assert_eq!(word_wrapper, expected);
369 assert_eq!(line_truncator, vec!["a"]);
370 }
372 #[test]
373 fn line_composer_max_line_width_of_1_double_width_characters() {
374 let width = 1;
375 let text = "コンピュータ上で文字を扱う場合、典型的には文字\naaaによる通信を行う場合にその\
376 両端点では、";
377 let (word_wrapper, _) = run_composer(Composer::WordWrapper { trim: true }, text, width);
378 let (line_truncator, _) = run_composer(Composer::LineTruncator, text, width);
379 assert_eq!(word_wrapper, vec!["", "a", "a", "a"]);
380 assert_eq!(line_truncator, vec!["", "a"]);
381 }
383 /// Tests WordWrapper with words some of which exceed line length and some not.
384 #[test]
385 fn line_composer_word_wrapper_mixed_length() {
386 let width = 20;
387 let text = "abcd efghij klmnopabcdefghijklmnopabcdefghijkl mnopab cdefghi j klmno";
388 let (word_wrapper, _) = run_composer(Composer::WordWrapper { trim: true }, text, width);
389 assert_eq!(
390 word_wrapper,
391 vec![
392 "abcd efghij",
393 "klmnopabcdefghijklmn",
394 "opabcdefghijkl",
395 "mnopab cdefghi j",
396 "klmno",
397 ]
398 )
399 }
401 #[test]
402 fn line_composer_double_width_chars() {
403 let width = 20;
404 let text = "コンピュータ上で文字を扱う場合、典型的には文字による通信を行う場合にその両端点\
405 では、";
406 let (word_wrapper, word_wrapper_width) =
407 run_composer(Composer::WordWrapper { trim: true }, &text, width);
408 let (line_truncator, _) = run_composer(Composer::LineTruncator, &text, width);
409 assert_eq!(line_truncator, vec!["コンピュータ上で文字"]);
410 let wrapped = vec![
411 "コンピュータ上で文字",
412 "を扱う場合、典型的に",
413 "は文字による通信を行",
414 "う場合にその両端点で",
415 "は、",
416 ];
417 assert_eq!(word_wrapper, wrapped);
418 assert_eq!(word_wrapper_width, vec![width, width, width, width, 4]);
419 }
421 #[test]
422 fn line_composer_leading_whitespace_removal() {
423 let width = 20;
424 let text = "AAAAAAAAAAAAAAAAAAAA AAA";
425 let (word_wrapper, _) = run_composer(Composer::WordWrapper { trim: true }, text, width);
426 let (line_truncator, _) = run_composer(Composer::LineTruncator, text, width);
427 assert_eq!(word_wrapper, vec!["AAAAAAAAAAAAAAAAAAAA", "AAA",]);
428 assert_eq!(line_truncator, vec!["AAAAAAAAAAAAAAAAAAAA"]);
429 }
431 /// Tests truncation of leading whitespace.
432 #[test]
433 fn line_composer_lots_of_spaces() {
434 let width = 20;
435 let text = " ";
436 let (word_wrapper, _) = run_composer(Composer::WordWrapper { trim: true }, text, width);
437 let (line_truncator, _) = run_composer(Composer::LineTruncator, text, width);
438 assert_eq!(word_wrapper, vec![""]);
439 assert_eq!(line_truncator, vec![" "]);
440 }
442 /// Tests an input starting with a letter, folowed by spaces - some of the behaviour is
443 /// incidental.
444 #[test]
445 fn line_composer_char_plus_lots_of_spaces() {
446 let width = 20;
447 let text = "a ";
448 let (word_wrapper, _) = run_composer(Composer::WordWrapper { trim: true }, text, width);
449 let (line_truncator, _) = run_composer(Composer::LineTruncator, text, width);
450 // What's happening below is: the first line gets consumed, trailing spaces discarded,
451 // after 20 of which a word break occurs (probably shouldn't). The second line break
452 // discards all whitespace. The result should probably be vec!["a"] but it doesn't matter
453 // that much.
454 assert_eq!(word_wrapper, vec!["a", ""]);
455 assert_eq!(line_truncator, vec!["a "]);
456 }
458 #[test]
459 fn line_composer_word_wrapper_double_width_chars_mixed_with_spaces() {
460 let width = 20;
461 // Japanese seems not to use spaces but we should break on spaces anyway... We're using it
462 // to test double-width chars.
463 // You are more than welcome to add word boundary detection based of alterations of
464 // hiragana and katakana...
465 // This happens to also be a test case for mixed width because regular spaces are single width.
466 let text = "コンピュ ータ上で文字を扱う場合、 典型的には文 字による 通信を行 う場合にその両端点では、";
467 let (word_wrapper, word_wrapper_width) =
468 run_composer(Composer::WordWrapper { trim: true }, text, width);
469 assert_eq!(
470 word_wrapper,
471 vec![
472 "コンピュ",
473 "ータ上で文字を扱う場",
474 "合、 典型的には文",
475 "字による 通信を行",
476 "う場合にその両端点で",
477 "は、",
478 ]
479 );
480 // Odd-sized lines have a space in them.
481 assert_eq!(word_wrapper_width, vec![8, 20, 17, 17, 20, 4]);
482 }
484 /// Ensure words separated by nbsp are wrapped as if they were a single one.
485 #[test]
486 fn line_composer_word_wrapper_nbsp() {
487 let width = 20;
488 let text = "AAAAAAAAAAAAAAA AAAA\u{00a0}AAA";
489 let (word_wrapper, _) = run_composer(Composer::WordWrapper { trim: true }, text, width);
490 assert_eq!(word_wrapper, vec!["AAAAAAAAAAAAAAA", "AAAA\u{00a0}AAA",]);
492 // Ensure that if the character was a regular space, it would be wrapped differently.
493 let text_space = text.replace("\u{00a0}", " ");
494 let (word_wrapper_space, _) =
495 run_composer(Composer::WordWrapper { trim: true }, &text_space, width);
496 assert_eq!(word_wrapper_space, vec!["AAAAAAAAAAAAAAA AAAA", "AAA",]);
497 }
499 #[test]
500 fn line_composer_word_wrapper_preserve_indentation() {
501 let width = 20;
502 let text = "AAAAAAAAAAAAAAAAAAAA AAA";
503 let (word_wrapper, _) = run_composer(Composer::WordWrapper { trim: false }, text, width);
504 assert_eq!(word_wrapper, vec!["AAAAAAAAAAAAAAAAAAAA", " AAA",]);
505 }
507 #[test]
508 fn line_composer_word_wrapper_preserve_indentation_with_wrap() {
509 let width = 10;
510 let text = "AAA AAA AAAAA AA AAAAAA\n B\n C\n D";
511 let (word_wrapper, _) = run_composer(Composer::WordWrapper { trim: false }, text, width);
512 assert_eq!(
513 word_wrapper,
514 vec!["AAA AAA", "AAAAA AA", "AAAAAA", " B", " C", " D"]
515 );
516 }
518 #[test]
519 fn line_composer_word_wrapper_preserve_indentation_lots_of_whitespace() {
520 let width = 10;
521 let text = " 4 Indent\n must wrap!";
522 let (word_wrapper, _) = run_composer(Composer::WordWrapper { trim: false }, text, width);
523 assert_eq!(
524 word_wrapper,
525 vec![
526 " ",
527 " 4",
528 "Indent",
529 " ",
530 " must",
531 "wrap!"
532 ]
533 );
534 }