1use std::{
2 borrow::Cow,
3 fs::File,
4 io::{self, BufReader},
5 num::NonZeroUsize,
6 ops::{Add, Div, Mul, Rem},
7 path::Path,
8};
9
10use anyhow::{Context, Error, bail};
11use ignore::overrides::OverrideBuilder;
12use two_face::re_exports::syntect::{
13 easy::HighlightLines,
14 highlighting::{Style, Theme},
15 parsing::SyntaxSet,
16};
17
18use crate::line_reader::{BufReadExt, LinesSplitEndings};
19
20pub fn relative_path(base: &Path, target: &Path) -> String {
21 match target.strip_prefix(base) {
22 Ok(relative) => {
23 if relative.as_os_str().is_empty() {
25 if target.is_file() {
26 target
27 .file_name()
28 .and_then(|s| s.to_str())
29 .unwrap_or(".")
30 .to_string()
31 } else {
32 ".".to_string()
33 }
34 } else {
35 relative.to_string_lossy().to_string()
36 }
37 }
38 Err(_) => {
39 target.to_string_lossy().to_string()
41 }
42 }
43}
44pub fn group_by<I, T, F>(iter: I, predicate: F) -> Vec<Vec<T>>
45where
46 I: IntoIterator<Item = T>,
47 F: Fn(&T, &T) -> bool,
48{
49 let mut result = Vec::new();
50 let mut current_group = Vec::new();
51
52 for item in iter {
53 if current_group.is_empty() || predicate(current_group.last().unwrap(), &item) {
54 current_group.push(item);
55 } else {
56 result.push(std::mem::take(&mut current_group));
57 current_group.push(item);
58 }
59 }
60
61 if !current_group.is_empty() {
62 result.push(current_group);
63 }
64
65 result
66}
67
68pub fn surrounding_line_window<R>(
69 reader: R,
70 start: usize,
71 end: usize,
72) -> impl Iterator<Item = (usize, String)>
73where
74 R: BufReadExt,
75{
76 assert!(
77 start <= end,
78 "Expected start <= end, found start={start}, end={end}"
79 );
80
81 reader
82 .lines_with_endings()
83 .enumerate()
84 .skip(start)
85 .take(end - start + 1)
86 .map(move |(idx, line_result)| {
87 let line = match line_result {
88 Ok((content, _ending)) => String::from_utf8_lossy(&content).into_owned(),
89 Err(e) => {
90 log::error!("Error reading line {idx}: {e}");
91 String::new()
92 }
93 };
94 (idx, line)
95 })
96}
97
98pub fn read_lines_range(
99 path: &Path,
100 start: usize,
101 end: usize,
102) -> io::Result<impl Iterator<Item = (usize, String)>> {
103 let file = File::open(path)?;
104 let reader = BufReader::new(file);
105
106 Ok(surrounding_line_window(reader, start, end))
107}
108
109pub fn largest_range_centered_on(
134 centre: usize,
135 lower_bound: usize,
136 upper_bound: usize,
137 max_size: NonZeroUsize,
138) -> anyhow::Result<(usize, usize)> {
139 if !(lower_bound <= centre && centre <= upper_bound) {
140 bail!(
141 "Expected start<=pos<=end, found start={lower_bound}, pos={centre}, end={upper_bound}",
142 );
143 }
144 let max_size = max_size.get();
145
146 let mut cur_size = 1;
147 let mut cur_start = centre;
148 let mut cur_end = centre;
149 while cur_size < max_size && (cur_start > lower_bound || cur_end < upper_bound) {
150 if cur_end < upper_bound {
151 cur_end += 1;
152 cur_size += 1;
153 }
154 if cur_size < max_size && cur_start > lower_bound {
155 cur_start -= 1;
156 cur_size += 1;
157 }
158 }
159
160 Ok((cur_start, cur_end))
161}
162
163#[allow(clippy::type_complexity)]
164pub fn split_indexed_lines<T>(
165 indexed_lines: Vec<(usize, T)>,
166 line_idx: usize,
167 num_lines_to_show: u16,
168) -> anyhow::Result<(Vec<(usize, T)>, (usize, T), Vec<(usize, T)>)> {
169 let file_start = indexed_lines.first().context("No lines found")?.0;
170 let file_end = indexed_lines.last().context("No lines found")?.0;
171 let (new_start, new_end) = largest_range_centered_on(
172 line_idx,
173 file_start,
174 file_end,
175 NonZeroUsize::new(num_lines_to_show as usize).context("preview will have height 0")?,
176 )?;
177
178 let mut filtered_lines = indexed_lines
179 .into_iter()
180 .skip_while(|(idx, _)| *idx < new_start)
181 .take_while(|(idx, _)| *idx <= new_end)
182 .collect::<Vec<_>>();
183
184 let position = filtered_lines
185 .iter()
186 .position(|(idx, _)| *idx == line_idx)
187 .context("Couldn't find line in file")?;
188 let after = filtered_lines.split_off(position + 1);
189 let current = filtered_lines.pop().unwrap();
190
191 Ok((filtered_lines, current, after))
192}
193
194fn needs_replacement(c: char) -> bool {
196 c == '\t' || c == '\n' || c == '\r' || c.is_control()
197}
198
199pub fn strip_control_chars(text: &str) -> Cow<'_, str> {
206 if !text.chars().any(needs_replacement) {
207 return Cow::Borrowed(text);
208 }
209
210 let mut result = String::with_capacity(text.len());
211 for c in text.chars() {
212 match c {
213 '\t' => result.push_str(" "),
214 '\n' => result.push(' '),
215 '\r' => {} c if c.is_control() => result.push('�'),
217 c => result.push(c),
218 }
219 }
220 Cow::Owned(result)
221}
222
223pub fn ceil_div<T>(a: T, b: T) -> T
224where
225 T: Add<Output = T>
226 + Div<Output = T>
227 + Mul<Output = T>
228 + Rem<Output = T>
229 + From<bool>
230 + From<u8>
231 + PartialEq
232 + PartialOrd
233 + Copy,
234{
235 a / b + T::from((a * b > T::from(0)) && (a % b) != T::from(0))
237}
238
239pub type HighlightedLine = Vec<(Option<Style>, String)>;
240
241struct HighlightedLinesIterator<'a> {
242 lines: LinesSplitEndings<BufReader<File>>,
243 highlighter: HighlightLines<'a>,
244 syntax_set: &'a SyntaxSet,
245 current_idx: usize,
246 start_idx: usize,
247 end_idx: Option<usize>,
248 full_highlighting: bool,
249}
250
251impl<'a> HighlightedLinesIterator<'a> {
252 pub fn new(
253 path: &Path,
254 theme: &'a Theme,
255 syntax_set: &'a SyntaxSet,
256 start_idx: Option<usize>,
257 end_idx: Option<usize>,
258 full_highlighting: bool,
259 ) -> io::Result<Self> {
260 let start_idx = start_idx.unwrap_or(0);
261 if let Some(end_idx) = end_idx {
262 #[allow(clippy::manual_assert)]
263 if start_idx > end_idx {
264 panic!("Expected start <= end, found start={start_idx}, end={end_idx}");
265 }
266 }
267
268 let file = File::open(path)?;
269 let reader = BufReader::new(file);
270 let mut lines = reader.lines_with_endings();
271
272 if !full_highlighting {
274 for _ in 0..start_idx {
275 if lines.next().is_none() {
276 break;
277 }
278 }
279 }
280
281 let syntax = syntax_set
282 .find_syntax_for_file(path)?
283 .unwrap_or_else(|| syntax_set.find_syntax_plain_text());
284
285 Ok(Self {
286 lines,
287 highlighter: HighlightLines::new(syntax, theme),
288 syntax_set,
289 current_idx: if full_highlighting { 0 } else { start_idx },
290 start_idx,
291 end_idx,
292 full_highlighting,
293 })
294 }
295}
296
297impl Iterator for HighlightedLinesIterator<'_> {
298 type Item = (usize, HighlightedLine);
299
300 fn next(&mut self) -> Option<Self::Item> {
301 if let Some(end) = self.end_idx
302 && self.current_idx > end
303 {
304 return None;
305 }
306
307 loop {
308 let idx = self.current_idx;
309 self.current_idx += 1;
310
311 debug_assert!(
312 self.full_highlighting || idx >= self.start_idx,
313 "Should have skipped early lines before iteration"
314 );
315
316 match self.lines.next() {
317 Some(Ok((content, _ending))) => {
318 let line = String::from_utf8_lossy(&content).into_owned();
320
321 let highlighted_res = self.highlighter.highlight_line(&line, self.syntax_set);
322 if let Err(ref e) = highlighted_res {
323 log::error!("Highlighting error at line {idx}: {e}");
324 }
325
326 if idx < self.start_idx {
327 continue;
328 }
329
330 let highlighted = match highlighted_res {
331 Ok(line) => line
332 .into_iter()
333 .map(|(style, text)| (Some(style), text.to_owned()))
334 .collect(),
335 Err(_) => {
336 vec![(None, line)]
337 }
338 };
339 return Some((idx, highlighted));
340 }
341 Some(Err(e)) => {
342 log::error!("Error reading line {}: {e}", self.current_idx);
343 return None;
344 }
345 None => return None, }
347 }
348 }
349}
350
351pub fn read_lines_range_highlighted<'a>(
352 path: &'a Path,
353 start: Option<usize>,
354 end: Option<usize>,
355 theme: &'a Theme,
356 syntax_set: &'a SyntaxSet,
357 full_highlighting: bool,
358) -> io::Result<impl Iterator<Item = (usize, HighlightedLine)> + 'a> {
359 HighlightedLinesIterator::new(path, theme, syntax_set, start, end, full_highlighting)
360}
361
362#[allow(dead_code)]
363pub fn split_while<T, F>(vec: &[T], predicate: F) -> (&[T], &[T])
364where
365 F: Fn(&T) -> bool,
366{
367 match vec.iter().position(|x| !predicate(x)) {
368 Some(index) => vec.split_at(index),
369 None => (vec, &[]),
370 }
371}
372
373pub fn last_n<T>(vec: &[T], n: usize) -> &[T] {
374 &vec[vec.len().saturating_sub(n)..]
375}
376
377pub fn last_n_chars(s: &str, n: usize) -> &str {
378 if n == 0 || s.is_empty() {
379 return "";
380 }
381 let char_count = s.chars().count();
382 if n >= char_count {
383 return s;
384 }
385
386 let (idx, _) = s.char_indices().rev().nth(n - 1).unwrap();
387 &s[idx..]
388}
389
390#[derive(Debug, Clone, PartialEq, Eq)]
391pub enum Either<T, S> {
392 Left(T),
393 Right(S),
394}
395
396pub fn is_regex_error(e: &Error) -> bool {
397 e.downcast_ref::<regex::Error>().is_some() || e.downcast_ref::<fancy_regex::Error>().is_some()
398}
399
400pub fn add_overrides(
401 overrides: &mut OverrideBuilder,
402 files: &str,
403 prefix: &str,
404) -> anyhow::Result<()> {
405 for file in files.split(',') {
406 let file = file.trim();
407 if !file.is_empty() {
408 overrides.add(&format!("{prefix}{file}"))?;
409 }
410 }
411 Ok(())
412}
413
414#[cfg(test)]
415mod tests {
416 use std::io::Write;
417 use tempfile::NamedTempFile;
418 use two_face::re_exports::syntect::highlighting::ThemeSet;
419
420 use super::*;
421
422 fn create_test_file(contents: &str) -> NamedTempFile {
423 let mut file = NamedTempFile::new().unwrap();
424 file.write_all(contents.as_bytes()).unwrap();
425 file
426 }
427
428 #[test]
429 fn test_relative_path_file_in_dir() {
430 assert_eq!(
432 relative_path(Path::new("/foo/bar/"), Path::new("/foo/bar/baz.rs")),
433 "baz.rs"
434 );
435 assert_eq!(
436 relative_path(Path::new("/foo/bar"), Path::new("/foo/bar/baz.rs")),
437 "baz.rs"
438 );
439 }
440
441 #[test]
442 fn test_relative_path_same_file() {
443 let test_file = create_test_file("");
446 assert_eq!(
447 relative_path(test_file.path(), test_file.path()),
448 test_file.path().file_name().unwrap().to_string_lossy()
449 );
450 }
451
452 #[test]
453 fn test_relative_path_same_dir() {
454 assert_eq!(
456 relative_path(Path::new("/foo/bar/"), Path::new("/foo/bar/")),
457 "."
458 );
459 }
460
461 #[test]
462 fn test_relative_path_nested() {
463 assert_eq!(
465 relative_path(Path::new("/foo"), Path::new("/foo/bar/baz.rs")),
466 "bar/baz.rs"
467 );
468 assert_eq!(
469 relative_path(Path::new("/foo/"), Path::new("/foo/bar/baz/")),
470 "bar/baz"
471 );
472 }
473
474 #[test]
475 fn test_relative_path_not_ancestor() {
476 assert_eq!(
478 relative_path(
479 Path::new("/foo/bar"),
480 Path::new("/completely/different/file.rs")
481 ),
482 "/completely/different/file.rs"
483 );
484 assert_eq!(
485 relative_path(Path::new("/foo/bar/"), Path::new("/foo/other.rs")),
486 "/foo/other.rs"
487 );
488 }
489
490 #[test]
491 fn test_relative_path_parent_to_child() {
492 assert_eq!(
494 relative_path(Path::new("/foo"), Path::new("/foo/bar/baz/qux.rs")),
495 "bar/baz/qux.rs"
496 );
497 }
498
499 #[test]
500 fn test_relative_path_root_cases() {
501 assert_eq!(
503 relative_path(Path::new("/"), Path::new("/foo/bar.rs")),
504 "foo/bar.rs"
505 );
506 assert_eq!(relative_path(Path::new("/"), Path::new("/")), ".");
508 }
509
510 #[test]
511 fn test_relative_path_no_trailing_slash() {
512 assert_eq!(
514 relative_path(Path::new("/foo/bar"), Path::new("/foo/bar/baz")),
515 "baz"
516 );
517 }
518
519 #[test]
520 fn test_relative_path_relative_paths() {
521 assert_eq!(
523 relative_path(Path::new("foo/bar"), Path::new("foo/bar/baz.rs")),
524 "baz.rs"
525 );
526 assert_eq!(
527 relative_path(Path::new("foo"), Path::new("bar/baz.rs")),
528 "bar/baz.rs" );
530 }
531
532 #[test]
533 fn test_vec() {
534 let numbers = vec![1, 2, 2, 3, 4, 4, 4, 5];
535 let grouped = group_by(numbers, |a, b| a == b);
536 assert_eq!(
537 grouped,
538 vec![vec![1], vec![2, 2], vec![3], vec![4, 4, 4], vec![5]]
539 );
540 }
541
542 #[test]
543 fn test_array() {
544 let numbers = [1, 2, 2, 3, 4, 4, 4, 5];
545 let grouped = group_by(numbers, |a, b| a == b);
546 assert_eq!(
547 grouped,
548 vec![vec![1], vec![2, 2], vec![3], vec![4, 4, 4], vec![5]]
549 );
550 }
551
552 #[test]
553 fn test_range() {
554 let grouped = group_by(1..=5, |a, b| b - a <= 1);
555 assert_eq!(grouped, vec![vec![1, 2, 3, 4, 5]]);
556 }
557
558 #[test]
559 fn test_chain() {
560 let first = [1, 2];
561 let second = [2, 3];
562 let grouped = group_by(first.into_iter().chain(second), |a, b| a == b);
563 assert_eq!(grouped, vec![vec![1], vec![2, 2], vec![3]]);
564 }
565
566 #[test]
567 fn test_empty() {
568 let empty: Vec<i32> = vec![];
569 let grouped = group_by(empty, |a, b| a == b);
570 assert_eq!(grouped, Vec::<Vec<i32>>::new());
571 }
572
573 #[test]
574 fn test_single() {
575 let single = std::iter::once(1);
576 let grouped = group_by(single, |a, b| a == b);
577 assert_eq!(grouped, vec![vec![1]]);
578 }
579
580 #[test]
581 fn test_string_slice() {
582 let words = ["apple", "app", "banana", "ban", "cat"];
583 let grouped = group_by(words, |a, b| a.starts_with(b) || b.starts_with(a));
584 assert_eq!(
585 grouped,
586 vec![vec!["apple", "app"], vec!["banana", "ban"], vec!["cat"]]
587 );
588 }
589
590 #[test]
591 fn test_read_lines_in_range() {
592 let contents = "line1\nline2\nline3\nline4\nline5";
593 let file = create_test_file(contents);
594 let path = file.path();
595
596 let result = read_lines_range(path, 1, 3).unwrap().collect::<Vec<_>>();
597
598 assert_eq!(
599 result,
600 vec![
601 (1, "line2".to_string()),
602 (2, "line3".to_string()),
603 (3, "line4".to_string())
604 ]
605 );
606 }
607
608 #[test]
609 fn test_read_single_line() {
610 let contents = "line1\nline2\nline3\nline4\nline5";
611 let file = create_test_file(contents);
612 let path = file.path();
613
614 let result = read_lines_range(path, 2, 2).unwrap().collect::<Vec<_>>();
615
616 assert_eq!(result, vec![(2, "line3".to_string())]);
617 }
618
619 #[test]
620 fn test_read_from_beginning() {
621 let contents = "line1\nline2\nline3\nline4\nline5";
622 let file = create_test_file(contents);
623 let path = file.path();
624
625 let result = read_lines_range(path, 0, 2).unwrap().collect::<Vec<_>>();
626
627 assert_eq!(
628 result,
629 vec![
630 (0, "line1".to_string()),
631 (1, "line2".to_string()),
632 (2, "line3".to_string())
633 ]
634 );
635 }
636
637 #[test]
638 fn test_read_to_end() {
639 let contents = "line1\nline2\nline3\nline4\nline5";
640 let file = create_test_file(contents);
641 let path = file.path();
642
643 let result = read_lines_range(path, 3, 4).unwrap().collect::<Vec<_>>();
644
645 assert_eq!(
646 result,
647 vec![(3, "line4".to_string()), (4, "line5".to_string())]
648 );
649 }
650
651 #[test]
652 fn test_read_all_lines() {
653 let contents = "line1\nline2\nline3\nline4\nline5";
654 let file = create_test_file(contents);
655 let path = file.path();
656
657 let result = read_lines_range(path, 0, 4).unwrap().collect::<Vec<_>>();
658
659 assert_eq!(
660 result,
661 vec![
662 (0, "line1".to_string()),
663 (1, "line2".to_string()),
664 (2, "line3".to_string()),
665 (3, "line4".to_string()),
666 (4, "line5".to_string())
667 ]
668 );
669 }
670
671 #[test]
672 fn test_empty_file() {
673 let contents = "";
674 let file = create_test_file(contents);
675 let path = file.path();
676
677 let result = read_lines_range(path, 0, 2).unwrap().collect::<Vec<_>>();
678
679 assert_eq!(result, Vec::<(usize, String)>::new());
680 }
681
682 #[test]
683 fn test_range_exceeds_file_length() {
684 let contents = "line1\nline2\nline3";
685 let file = create_test_file(contents);
686 let path = file.path();
687
688 let result = read_lines_range(path, 1, 10).unwrap().collect::<Vec<_>>();
689
690 assert_eq!(
691 result,
692 vec![(1, "line2".to_string()), (2, "line3".to_string())]
693 );
694 }
695
696 #[test]
697 fn test_start_exceeds_file_length() {
698 let contents = "line1\nline2\nline3";
699 let file = create_test_file(contents);
700 let path = file.path();
701
702 let result = read_lines_range(path, 5, 10).unwrap().collect::<Vec<_>>();
703
704 assert_eq!(result, Vec::<(usize, String)>::new());
705 }
706
707 #[test]
708 #[should_panic(expected = "Expected start <= end")]
709 fn test_start_greater_than_end() {
710 let contents = "line1\nline2\nline3";
711 let file = create_test_file(contents);
712 let path = file.path();
713
714 let _ = read_lines_range(path, 3, 1);
715 }
716
717 #[test]
718 fn test_with_empty_lines() {
719 let contents = "line1\n\nline3\n\nline5";
720 let file = create_test_file(contents);
721 let path = file.path();
722
723 let result = read_lines_range(path, 0, 4).unwrap().collect::<Vec<_>>();
724
725 assert_eq!(
726 result,
727 vec![
728 (0, "line1".to_string()),
729 (1, "".to_string()),
730 (2, "line3".to_string()),
731 (3, "".to_string()),
732 (4, "line5".to_string())
733 ]
734 );
735 }
736
737 #[test]
738 fn test_file_not_found() {
739 let path = Path::new("non_existent_file.txt");
740
741 let result = read_lines_range(path, 0, 5);
742
743 assert!(result.is_err());
744 }
745
746 #[test]
747 fn test_largest_window_around() {
748 assert!(largest_range_centered_on(5, 0, 0, NonZeroUsize::new(1).unwrap()).is_err());
749
750 assert_eq!(
751 largest_range_centered_on(5, 0, 9, NonZeroUsize::new(1).unwrap()).unwrap(),
752 (5, 5)
753 );
754 assert_eq!(
755 largest_range_centered_on(5, 0, 9, NonZeroUsize::new(2).unwrap()).unwrap(),
756 (5, 6)
757 );
758 assert_eq!(
759 largest_range_centered_on(5, 0, 9, NonZeroUsize::new(3).unwrap()).unwrap(),
760 (4, 6)
761 );
762 assert_eq!(
763 largest_range_centered_on(5, 0, 9, NonZeroUsize::new(4).unwrap()).unwrap(),
764 (4, 7)
765 );
766 assert_eq!(
767 largest_range_centered_on(5, 0, 9, NonZeroUsize::new(5).unwrap()).unwrap(),
768 (3, 7)
769 );
770 assert_eq!(
771 largest_range_centered_on(5, 0, 9, NonZeroUsize::new(6).unwrap()).unwrap(),
772 (3, 8)
773 );
774 assert_eq!(
775 largest_range_centered_on(5, 0, 9, NonZeroUsize::new(7).unwrap()).unwrap(),
776 (2, 8)
777 );
778 assert_eq!(
779 largest_range_centered_on(5, 0, 9, NonZeroUsize::new(8).unwrap()).unwrap(),
780 (2, 9)
781 );
782 assert_eq!(
783 largest_range_centered_on(5, 0, 9, NonZeroUsize::new(9).unwrap()).unwrap(),
784 (1, 9)
785 );
786 assert_eq!(
787 largest_range_centered_on(5, 0, 9, NonZeroUsize::new(10).unwrap()).unwrap(),
788 (0, 9)
789 );
790 assert_eq!(
791 largest_range_centered_on(5, 0, 9, NonZeroUsize::new(11).unwrap()).unwrap(),
792 (0, 9)
793 );
794 assert_eq!(
795 largest_range_centered_on(5, 0, 9, NonZeroUsize::new(999).unwrap()).unwrap(),
796 (0, 9)
797 );
798
799 assert_eq!(
800 largest_range_centered_on(0, 0, 9, NonZeroUsize::new(1).unwrap()).unwrap(),
801 (0, 0)
802 );
803 assert_eq!(
804 largest_range_centered_on(0, 0, 9, NonZeroUsize::new(2).unwrap()).unwrap(),
805 (0, 1)
806 );
807 assert_eq!(
808 largest_range_centered_on(0, 0, 9, NonZeroUsize::new(100).unwrap()).unwrap(),
809 (0, 9)
810 );
811
812 assert_eq!(
813 largest_range_centered_on(5, 3, 5, NonZeroUsize::new(1).unwrap()).unwrap(),
814 (5, 5)
815 );
816 assert_eq!(
817 largest_range_centered_on(5, 3, 5, NonZeroUsize::new(2).unwrap()).unwrap(),
818 (4, 5)
819 );
820 assert_eq!(
821 largest_range_centered_on(5, 3, 5, NonZeroUsize::new(100).unwrap()).unwrap(),
822 (3, 5)
823 );
824 }
825
826 #[test]
827 fn test_sanitize_normal_text() {
828 assert_eq!(strip_control_chars("hello world"), "hello world");
829 }
830
831 #[test]
832 fn test_sanitize_tabs() {
833 assert_eq!(strip_control_chars("hello\tworld"), "hello world");
834 assert_eq!(strip_control_chars("\t\t"), " ");
835 }
836
837 #[test]
838 fn test_sanitize_newlines() {
839 assert_eq!(strip_control_chars("hello\nworld"), "hello world");
840 assert_eq!(strip_control_chars("\n\n"), " ");
841 }
842
843 #[test]
844 fn test_sanitize_control_chars() {
845 assert_eq!(strip_control_chars("hello\u{4}world"), "hello�world");
846 assert_eq!(strip_control_chars("test\u{7}"), "test�");
847 assert_eq!(strip_control_chars("\u{1b}[0m"), "�[0m");
848 }
849
850 #[test]
851 fn test_sanitize_unicode() {
852 assert_eq!(strip_control_chars("héllo→世界"), "héllo→世界");
853 }
854
855 #[test]
856 fn test_sanitize_empty_string() {
857 assert_eq!(strip_control_chars(""), "");
858 }
859
860 #[test]
861 fn test_sanitize_only_control_chars() {
862 assert_eq!(strip_control_chars("\u{1}\u{2}\u{3}\u{4}"), "����");
863 }
864
865 #[test]
866 fn test_sanitize_carriage_returns() {
867 assert_eq!(strip_control_chars("hello\rworld"), "helloworld");
869 assert_eq!(strip_control_chars("hello\r\nworld"), "hello world");
870 assert_eq!(strip_control_chars("\r\r\r"), "");
871 }
872
873 #[test]
874 fn test_ceil_div() {
875 assert_eq!(ceil_div(1, 1), 1);
876 assert_eq!(ceil_div(2, 1), 2);
877 assert_eq!(ceil_div(1, 2), 1);
878 assert_eq!(ceil_div(0, 1), 0);
879 assert_eq!(ceil_div(2, 3), 1);
880 assert_eq!(ceil_div(27, 4), 7);
881 assert_eq!(ceil_div(26, 9), 3);
882 assert_eq!(ceil_div(27, 9), 3);
883 assert_eq!(ceil_div(28, 9), 4);
884 assert_eq!(ceil_div(-2, 3), 0);
885 assert_eq!(ceil_div(-4, 3), -1);
886 assert_eq!(ceil_div(2, -3), 0);
887 assert_eq!(ceil_div(4, -3), -1);
888 assert_eq!(ceil_div(-2, -3), 1);
889 assert_eq!(ceil_div(-4, -3), 2);
890 }
891
892 fn get_theme() -> Theme {
893 let ts = ThemeSet::load_defaults();
894 ts.themes["base16-ocean.dark"].clone()
895 }
896
897 fn get_syntax() -> SyntaxSet {
898 two_face::syntax::extra_no_newlines()
899 }
900
901 #[allow(clippy::type_complexity)]
902 fn extract_text(
903 highlighted_lines: &[(usize, Vec<(Option<Style>, String)>)],
904 ) -> Vec<(usize, String)> {
905 highlighted_lines
906 .iter()
907 .map(|(idx, styles)| {
908 let text = styles
909 .iter()
910 .map(|(_, text)| text.clone())
911 .collect::<String>();
912 (*idx, text)
913 })
914 .collect()
915 }
916
917 #[test]
918 fn test_read_lines_in_range_highlighted() {
919 for full in [true, false] {
920 let contents = "line1\nline2\nline3\nline4\nline5";
921 let file = create_test_file(contents);
922
923 let result = read_lines_range_highlighted(
924 file.path(),
925 Some(1),
926 Some(3),
927 &get_theme(),
928 &get_syntax(),
929 full,
930 )
931 .unwrap()
932 .collect::<Vec<_>>();
933
934 assert_eq!(
935 extract_text(&result),
936 vec![
937 (1, "line2".to_string()),
938 (2, "line3".to_string()),
939 (3, "line4".to_string())
940 ]
941 );
942 }
943 }
944
945 #[test]
946 fn test_read_single_line_highlighted() {
947 for full in [true, false] {
948 let contents = "line1\nline2\nline3\nline4\nline5";
949 let file = create_test_file(contents);
950
951 let result = read_lines_range_highlighted(
952 file.path(),
953 Some(2),
954 Some(2),
955 &get_theme(),
956 &get_syntax(),
957 full,
958 )
959 .unwrap()
960 .collect::<Vec<_>>();
961
962 assert_eq!(extract_text(&result), vec![(2, "line3".to_string())]);
963 }
964 }
965
966 #[test]
967 fn test_read_from_beginning_highlighted() {
968 for full in [true, false] {
969 let contents = "line1\nline2\nline3\nline4\nline5";
970 let file = create_test_file(contents);
971
972 let result = read_lines_range_highlighted(
973 file.path(),
974 Some(0),
975 Some(2),
976 &get_theme(),
977 &get_syntax(),
978 full,
979 )
980 .unwrap()
981 .collect::<Vec<_>>();
982
983 assert_eq!(
984 extract_text(&result),
985 vec![
986 (0, "line1".to_string()),
987 (1, "line2".to_string()),
988 (2, "line3".to_string())
989 ]
990 );
991 }
992 }
993
994 #[test]
995 fn test_read_to_end_highlighted() {
996 for full in [true, false] {
997 let contents = "line1\nline2\nline3\nline4\nline5";
998 let file = create_test_file(contents);
999
1000 let result = read_lines_range_highlighted(
1001 file.path(),
1002 Some(3),
1003 Some(4),
1004 &get_theme(),
1005 &get_syntax(),
1006 full,
1007 )
1008 .unwrap()
1009 .collect::<Vec<_>>();
1010
1011 assert_eq!(
1012 extract_text(&result),
1013 vec![(3, "line4".to_string()), (4, "line5".to_string())]
1014 );
1015 }
1016 }
1017
1018 #[test]
1019 fn test_read_all_lines_highlighted() {
1020 for full in [true, false] {
1021 let contents = "line1\nline2\nline3\nline4\nline5";
1022 let file = create_test_file(contents);
1023
1024 let result = read_lines_range_highlighted(
1025 file.path(),
1026 Some(0),
1027 Some(4),
1028 &get_theme(),
1029 &get_syntax(),
1030 full,
1031 )
1032 .unwrap()
1033 .collect::<Vec<_>>();
1034
1035 assert_eq!(
1036 extract_text(&result),
1037 vec![
1038 (0, "line1".to_string()),
1039 (1, "line2".to_string()),
1040 (2, "line3".to_string()),
1041 (3, "line4".to_string()),
1042 (4, "line5".to_string())
1043 ]
1044 );
1045 }
1046 }
1047
1048 #[test]
1049 fn test_empty_file_highlighted() {
1050 for full in [true, false] {
1051 let contents = "";
1052 let file = create_test_file(contents);
1053
1054 let result = read_lines_range_highlighted(
1055 file.path(),
1056 Some(0),
1057 Some(2),
1058 &get_theme(),
1059 &get_syntax(),
1060 full,
1061 )
1062 .unwrap()
1063 .collect::<Vec<_>>();
1064
1065 assert_eq!(extract_text(&result), Vec::<(usize, String)>::new());
1066 }
1067 }
1068
1069 #[test]
1070 fn test_range_exceeds_file_length_highlighted() {
1071 for full in [true, false] {
1072 let contents = "line1\nline2\nline3";
1073 let file = create_test_file(contents);
1074
1075 let result = read_lines_range_highlighted(
1076 file.path(),
1077 Some(1),
1078 Some(10),
1079 &get_theme(),
1080 &get_syntax(),
1081 full,
1082 )
1083 .unwrap()
1084 .collect::<Vec<_>>();
1085
1086 assert_eq!(
1087 extract_text(&result),
1088 vec![(1, "line2".to_string()), (2, "line3".to_string())]
1089 );
1090 }
1091 }
1092
1093 #[test]
1094 fn test_start_exceeds_file_length_highlighted() {
1095 for full in [true, false] {
1096 let contents = "line1\nline2\nline3";
1097 let file = create_test_file(contents);
1098
1099 let result = read_lines_range_highlighted(
1100 file.path(),
1101 Some(5),
1102 Some(10),
1103 &get_theme(),
1104 &get_syntax(),
1105 full,
1106 )
1107 .unwrap()
1108 .collect::<Vec<_>>();
1109
1110 assert_eq!(extract_text(&result), Vec::<(usize, String)>::new());
1111 }
1112 }
1113
1114 #[test]
1115 #[should_panic(expected = "Expected start <= end")]
1116 fn test_start_greater_than_end_highlighted() {
1117 for full in [true, false] {
1118 let contents = "line1\nline2\nline3";
1119 let file = create_test_file(contents);
1120 let _ = read_lines_range_highlighted(
1121 file.path(),
1122 Some(3),
1123 Some(1),
1124 &get_theme(),
1125 &get_syntax(),
1126 full,
1127 );
1128 }
1129 }
1130
1131 #[test]
1132 fn test_with_empty_lines_highlighted() {
1133 for full in [true, false] {
1134 let contents = "line1\n\nline3\n\nline5";
1135 let file = create_test_file(contents);
1136
1137 let result = read_lines_range_highlighted(
1138 file.path(),
1139 Some(0),
1140 Some(4),
1141 &get_theme(),
1142 &get_syntax(),
1143 full,
1144 )
1145 .unwrap()
1146 .collect::<Vec<_>>();
1147
1148 assert_eq!(
1149 extract_text(&result),
1150 vec![
1151 (0, "line1".to_string()),
1152 (1, "".to_string()),
1153 (2, "line3".to_string()),
1154 (3, "".to_string()),
1155 (4, "line5".to_string())
1156 ]
1157 );
1158 }
1159 }
1160
1161 #[test]
1162 fn test_file_not_found_highlighted() {
1163 let path = Path::new("non_existent_file.txt");
1164
1165 let theme = get_theme();
1166 let syntax = get_syntax();
1167 let result = read_lines_range_highlighted(path, Some(0), Some(5), &theme, &syntax, true);
1168
1169 assert!(result.is_err());
1170 }
1171
1172 #[test]
1173 fn test_highlighting_preserves_content() {
1174 let contents = "fn main() {\n println!(\"Hello, world!\");\n}";
1175 let file = create_test_file(contents);
1176
1177 let result = read_lines_range_highlighted(
1178 file.path(),
1179 Some(0),
1180 Some(2),
1181 &get_theme(),
1182 &get_syntax(),
1183 true,
1184 )
1185 .unwrap()
1186 .collect::<Vec<_>>();
1187
1188 assert_eq!(
1189 extract_text(&result),
1190 vec![
1191 (0, "fn main() {".to_string()),
1192 (1, " println!(\"Hello, world!\");".to_string()),
1193 (2, "}".to_string())
1194 ]
1195 );
1196 }
1197
1198 #[test]
1199 fn test_split_empty() {
1200 let empty: Vec<i32> = vec![];
1201 let (prefix, rest) = split_while(&empty, |&x| x > 0);
1202 assert_eq!(prefix, &[] as &[i32]);
1203 assert_eq!(rest, &[] as &[i32]);
1204 }
1205
1206 #[test]
1207 fn test_all_satisfy() {
1208 let vec = vec![1, 2, 3, 4, 5];
1209 let (prefix, rest) = split_while(&vec, |&x| x > 0);
1210 assert_eq!(prefix, &[1, 2, 3, 4, 5]);
1211 assert_eq!(rest, &[] as &[i32]);
1212 }
1213
1214 #[test]
1215 fn test_none_satisfy() {
1216 let vec = vec![1, 2, 3, 4, 5];
1217 let (prefix, rest) = split_while(&vec, |&x| x > 5);
1218 assert_eq!(prefix, &[] as &[i32]);
1219 assert_eq!(rest, &[1, 2, 3, 4, 5]);
1220 }
1221
1222 #[test]
1223 fn test_some_satisfy() {
1224 let vec = vec![1, 2, 3, 4, 5];
1225 let (prefix, rest) = split_while(&vec, |&x| x < 3);
1226 assert_eq!(prefix, &[1, 2]);
1227 assert_eq!(rest, &[3, 4, 5]);
1228 }
1229
1230 #[test]
1231 fn test_only_first_satisfies() {
1232 let vec = vec![1, 2, 3, 4, 5];
1233 let (prefix, rest) = split_while(&vec, |&x| x == 1);
1234 assert_eq!(prefix, &[1]);
1235 assert_eq!(rest, &[2, 3, 4, 5]);
1236 }
1237
1238 #[test]
1239 fn test_only_last_fails() {
1240 let vec = vec![1, 2, 3, 4, 5];
1241 let (prefix, rest) = split_while(&vec, |&x| x < 5);
1242 assert_eq!(prefix, &[1, 2, 3, 4]);
1243 assert_eq!(rest, &[5]);
1244 }
1245
1246 #[test]
1247 fn test_with_strings() {
1248 let vec = vec!["apple", "banana", "cherry", "date", "elderberry"];
1249 let (prefix, rest) = split_while(&vec, |&s| s.starts_with('a') || s.starts_with('b'));
1250 assert_eq!(prefix, &["apple", "banana"]);
1251 assert_eq!(rest, &["cherry", "date", "elderberry"]);
1252 }
1253
1254 #[test]
1255 fn test_owned_vec() {
1256 let vec = vec![1, 2, 3, 4, 5];
1258 let (prefix, rest) = split_while(&vec, |&x| x < 3);
1259 assert_eq!(prefix, vec![1, 2]);
1260 assert_eq!(rest, vec![3, 4, 5]);
1261 }
1262
1263 #[test]
1264 fn test_last_n_empty() {
1265 let vec: Vec<usize> = vec![];
1266 assert_eq!(last_n(&vec, 0), &(vec![] as Vec<usize>));
1267 assert_eq!(last_n(&vec, 3), &(vec![] as Vec<usize>));
1268 }
1269
1270 #[test]
1271 fn test_last_n_non_empty() {
1272 let vec = (0..10).collect::<Vec<usize>>();
1273 assert_eq!(last_n(&vec, 0), &(vec![] as Vec<usize>));
1274 assert_eq!(last_n(&vec, 1), &vec![9]);
1275 assert_eq!(last_n(&vec, 3), &vec![7, 8, 9]);
1276 assert_eq!(last_n(&vec, 10), &vec);
1277 assert_eq!(last_n(&vec, 200), &vec);
1278 }
1279
1280 #[test]
1281 fn test_last_string_n_empty() {
1282 let s = "".chars().collect::<Vec<_>>();
1283 assert_eq!(last_n(&s, 0), &(vec![] as Vec<char>));
1284 assert_eq!(last_n(&s, 3), &(vec![] as Vec<char>));
1285 }
1286
1287 #[test]
1288 fn test_last_n_string_non_empty() {
1289 let s = "abcdefghijkl".chars().collect::<Vec<_>>();
1290 assert_eq!(last_n(&s, 0), "".chars().collect::<Vec<_>>());
1291 assert_eq!(last_n(&s, 1), "l".chars().collect::<Vec<_>>());
1292 assert_eq!(last_n(&s, 3), "jkl".chars().collect::<Vec<_>>());
1293 assert_eq!(last_n(&s, 10), "cdefghijkl".chars().collect::<Vec<_>>());
1294 assert_eq!(last_n(&s, 12), "abcdefghijkl".chars().collect::<Vec<_>>());
1295 assert_eq!(last_n(&s, 13), "abcdefghijkl".chars().collect::<Vec<_>>());
1296 assert_eq!(last_n(&s, 200), "abcdefghijkl".chars().collect::<Vec<_>>());
1297 }
1298
1299 #[test]
1300 fn test_empty_string() {
1301 assert_eq!(last_n_chars("", 5), "");
1302 }
1303
1304 #[test]
1305 fn test_zero_chars() {
1306 assert_eq!(last_n_chars("hello", 0), "");
1307 }
1308
1309 #[test]
1310 fn test_ascii_full_string() {
1311 assert_eq!(last_n_chars("hello", 5), "hello");
1312 }
1313
1314 #[test]
1315 fn test_ascii_partial_string() {
1316 assert_eq!(last_n_chars("hello", 3), "llo");
1317 }
1318
1319 #[test]
1320 fn test_ascii_more_than_string() {
1321 assert_eq!(last_n_chars("hello", 10), "hello");
1322 }
1323
1324 #[test]
1325 fn test_unicode_chars() {
1326 assert_eq!(last_n_chars("héllö wörld", 5), "wörld");
1327 }
1328
1329 #[test]
1330 fn test_multibyte_chars() {
1331 let s = "こんにちは世界";
1332 assert_eq!(last_n_chars(s, 2), "世界");
1333 }
1334
1335 #[test]
1336 fn test_emoji() {
1337 let s = "Hello 👋 World 🌍";
1338 assert_eq!(last_n_chars(s, 9), "👋 World 🌍");
1339 }
1340
1341 #[allow(clippy::similar_names)]
1342 #[test]
1343 fn test_typescript_syntax_available() {
1344 let syntax_set = two_face::syntax::extra_no_newlines();
1345
1346 let ts_syntax = syntax_set.find_syntax_by_extension("ts");
1347 assert!(ts_syntax.is_some(), "TypeScript syntax should be available");
1348 assert_eq!(ts_syntax.unwrap().name, "TypeScript");
1349
1350 let tsx_syntax = syntax_set.find_syntax_by_extension("tsx");
1351 assert!(
1352 tsx_syntax.is_some(),
1353 "TypeScript React syntax should be available"
1354 );
1355 assert_eq!(tsx_syntax.unwrap().name, "TypeScriptReact");
1356
1357 let js_syntax = syntax_set.find_syntax_by_extension("js");
1358 assert!(js_syntax.is_some(), "JavaScript syntax should be available");
1359 }
1360}