1use crate::key::{self, Binding};
13use rusty_bubbletea::key::KeyPressMsg;
14use rusty_bubbletea::model::{Cmd, Msg};
15use rusty_bubbletea::mouse::{MouseButton, MouseWheelMsg};
16use rusty_lipgloss::{self, ranges::Range, Style};
17use rusty_x_ansi;
18use std::collections::HashMap;
19
20const DEFAULT_HORIZONTAL_STEP: usize = 6;
23
24pub type Option = Box<dyn FnOnce(&mut Model)>; pub fn with_width(w: usize) -> Option {
36 Box::new(move |m: &mut Model| {
37 m.width = w;
38 })
39}
40
41pub fn with_height(h: usize) -> Option {
44 Box::new(move |m: &mut Model| {
45 m.height = h;
46 })
47}
48
49impl Default for KeyMap {
52 fn default() -> Self {
53 default_key_map()
54 }
55}
56
57pub fn with_key_map(km: KeyMap) -> Option {
59 Box::new(move |m: &mut Model| {
60 m.key_map = km.clone();
61 })
62}
63
64pub fn new(opts: Vec<Option>) -> Model {
65 let mut m = Model {
66 width: 0,
67 height: 0,
68 key_map: default_key_map(),
69 soft_wrap: false,
70 fill_height: false,
71 mouse_wheel_enabled: true,
72 mouse_wheel_delta: 3,
73 y_offset: 0,
74 x_offset: 0,
75 horizontal_step: DEFAULT_HORIZONTAL_STEP,
76 y_position: 0,
77 style: Style::new(),
78 left_gutter_func: None,
79 initialized: false,
80 lines: vec![],
81 longest_line_width: 0,
82 highlight_style: Style::new(),
83 selected_highlight_style: Style::new(),
84 style_line_func: None,
85 highlights: vec![],
86 hi_idx: -1,
87 clone_hack: std::marker::PhantomData,
88 };
89
90 for opt in opts {
91 opt(&mut m);
92 }
93 m.set_initial_values();
94 m
95}
96
97#[derive(Debug, Clone, Copy)]
99pub struct GutterContext {
100 pub index: usize,
103
104 pub total_lines: usize,
106
107 pub soft: bool,
109}
110
111pub type GutterFunc = Box<dyn Fn(GutterContext) -> String + Send + Sync>;
128
129pub struct Model {
131 width: usize,
132 height: usize,
133 pub key_map: KeyMap,
135
136 pub soft_wrap: bool,
139
140 pub fill_height: bool,
143
144 pub mouse_wheel_enabled: bool,
147
148 pub mouse_wheel_delta: usize,
151
152 y_offset: usize,
154
155 x_offset: usize,
157
158 horizontal_step: usize,
161
162 pub y_position: usize,
165
166 pub style: Style,
169
170 pub left_gutter_func: std::option::Option<GutterFunc>,
174
175 #[doc(hidden)]
176 #[allow(dead_code)]
177 clone_hack: std::marker::PhantomData<()>,
178
179 initialized: bool,
180 lines: Vec<String>,
181 longest_line_width: usize,
182
183 pub highlight_style: Style,
185
186 pub selected_highlight_style: Style,
189
190 pub style_line_func: std::option::Option<Box<dyn Fn(usize) -> Style + Send + Sync>>,
193
194 highlights: Vec<HighlightInfo>,
195 hi_idx: isize,
196}
197
198impl Clone for Model {
199 fn clone(&self) -> Self {
200 Model {
201 width: self.width,
202 height: self.height,
203 key_map: self.key_map.clone(),
204 soft_wrap: self.soft_wrap,
205 fill_height: self.fill_height,
206 mouse_wheel_enabled: self.mouse_wheel_enabled,
207 mouse_wheel_delta: self.mouse_wheel_delta,
208 y_offset: self.y_offset,
209 x_offset: self.x_offset,
210 horizontal_step: self.horizontal_step,
211 y_position: self.y_position,
212 style: self.style.clone(),
213 left_gutter_func: None,
214 initialized: self.initialized,
215 lines: self.lines.clone(),
216 longest_line_width: self.longest_line_width,
217 highlight_style: self.highlight_style.clone(),
218 selected_highlight_style: self.selected_highlight_style.clone(),
219 style_line_func: None,
220 highlights: self.highlights.clone(),
221 hi_idx: self.hi_idx,
222 clone_hack: std::marker::PhantomData,
223 }
224 }
225}
226
227impl std::fmt::Debug for Model {
228 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
229 f.debug_struct("viewport::Model")
230 .field("width", &self.width)
231 .field("height", &self.height)
232 .field("y_offset", &self.y_offset)
233 .field("lines", &self.lines.len())
234 .finish()
235 }
236}
237
238impl Model {
239 fn set_initial_values(&mut self) {
240 self.mouse_wheel_enabled = true;
241 self.mouse_wheel_delta = 3;
242 self.horizontal_step = DEFAULT_HORIZONTAL_STEP;
243 self.initialized = true;
244 }
245
246 pub fn height(&self) -> usize {
248 self.height
249 }
250
251 pub fn set_height(&mut self, h: usize) {
253 self.height = h;
254 }
255
256 pub fn width(&self) -> usize {
258 self.width
259 }
260
261 pub fn set_width(&mut self, w: usize) {
263 self.width = w;
264 }
265
266 pub fn at_top(&self) -> bool {
269 self.y_offset() == 0
270 }
271
272 pub fn at_bottom(&self) -> bool {
275 self.y_offset() >= self.max_y_offset()
276 }
277
278 pub fn past_bottom(&self) -> bool {
281 self.y_offset() > self.max_y_offset()
282 }
283
284 pub fn scroll_percent(&self) -> f64 {
286 let (total, _, _) = self.calculate_line(0);
287 if self.height() >= total {
288 return 1.0;
289 }
290 let y = self.y_offset() as f64;
291 let h = self.height() as f64;
292 let t = total as f64;
293 let v = y / (t - h);
294 clamp(v, 0.0, 1.0)
295 }
296
297 pub fn horizontal_scroll_percent(&self) -> f64 {
300 if self.x_offset >= self.longest_line_width.saturating_sub(self.width()) {
301 return 1.0;
302 }
303 let y = self.x_offset as f64;
304 let h = self.width() as f64;
305 let t = self.longest_line_width as f64;
306 let v = y / (t - h);
307 clamp(v, 0.0, 1.0)
308 }
309
310 pub fn set_content(&mut self, s: &str) {
313 self.set_content_lines(&s.split('\n').map(|x| x.to_string()).collect::<Vec<_>>());
314 }
315
316 pub fn set_content_lines(&mut self, lines: &[String]) {
320 self.lines = lines.to_vec();
323 if self.lines.len() == 1 && rusty_x_ansi::string_width(&self.lines[0]) == 0 {
324 self.lines.clear();
325 } else {
326 let mut sub_lines: Vec<String>;
328 let mut i = self.lines.len();
329 while i > 0 {
330 i -= 1;
331 if !self.lines[i].contains('\r') && !self.lines[i].contains('\n') {
332 continue;
333 }
334
335 self.lines[i] = self.lines[i].replace("\r\n", "\n"); sub_lines = self.lines[i].split('\n').map(|x| x.to_string()).collect();
337 if sub_lines.len() > 1 {
338 self.lines
339 .splice(i + 1..i + 1, sub_lines[1..].iter().cloned());
340 self.lines[i] = sub_lines[0].clone();
341 }
342 }
343 }
344
345 self.longest_line_width = max_line_width(&self.lines);
346 self.clear_highlights();
347
348 if self.y_offset() > self.max_y_offset() {
349 self.goto_bottom();
350 }
351 }
352
353 pub fn get_content(&self) -> String {
356 self.lines.join("\n")
357 }
358
359 fn calculate_line(&self, yoffset: usize) -> (usize, usize, usize) {
363 if !self.soft_wrap {
364 let total = self.lines.len();
365 let ridx = yoffset.min(self.lines.len());
366 return (total, ridx, 0);
367 }
368
369 let max_width = self.max_width() as f64;
370 let mut total = 0usize;
371 let mut ridx = self.lines.len();
372 let mut voffset = 0usize;
373
374 for (i, line) in self.lines.iter().enumerate() {
375 let line_height =
376 1usize.max((rusty_x_ansi::string_width(line) as f64 / max_width).ceil() as usize);
377
378 if yoffset >= total && yoffset < total + line_height {
379 ridx = i;
380 voffset = yoffset - total;
381 }
382 total += line_height;
383 }
384
385 if yoffset >= total {
386 ridx = self.lines.len();
387 voffset = 0;
388 }
389
390 (total, ridx, voffset)
391 }
392
393 fn max_y_offset(&self) -> usize {
396 let (total, _, _) = self.calculate_line(0);
397 total
398 .saturating_sub(self.height())
399 .saturating_add(self.style.get_vertical_frame_size())
400 }
401
402 fn max_x_offset(&self) -> usize {
405 self.longest_line_width.saturating_sub(self.width())
406 }
407
408 fn max_width(&self) -> usize {
411 let mut gutter_size = 0;
412 if let Some(g) = &self.left_gutter_func {
413 gutter_size = rusty_x_ansi::string_width(&g(GutterContext {
414 index: 0,
415 total_lines: 0,
416 soft: false,
417 }));
418 }
419 self.width()
420 .saturating_sub(self.style.get_horizontal_frame_size())
421 .saturating_sub(gutter_size)
422 }
423
424 fn max_height(&self) -> usize {
427 self.height()
428 .saturating_sub(self.style.get_vertical_frame_size())
429 }
430
431 fn visible_lines(&self) -> Vec<String> {
434 let max_height = self.max_height();
435 let max_width = self.max_width();
436
437 if max_height == 0 || max_width == 0 {
438 return vec![];
439 }
440
441 let (total, ridx, voffset) = self.calculate_line(self.y_offset());
442 let mut lines: Vec<String> = vec![];
443 if total > 0 {
444 let bottom = clamp(ridx + max_height, ridx, self.lines.len());
445 lines = self.style_lines(self.lines[ridx..bottom].to_vec(), ridx);
446 lines = self.highlight_lines(lines, ridx);
447 }
448
449 while self.fill_height && lines.len() < max_height {
450 lines.push(String::new());
451 }
452
453 if (self.x_offset == 0 && self.longest_line_width <= max_width) || max_width == 0 {
455 let out = self.setup_gutter(lines, total, ridx);
456 return out;
457 }
458
459 if self.soft_wrap {
460 return self.soft_wrap_lines(lines, max_width, max_height, total, ridx, voffset);
461 }
462
463 for line in lines.iter_mut() {
465 *line = rusty_x_ansi::cut(line, self.x_offset, self.x_offset + max_width);
466 }
467 self.setup_gutter(lines, total, ridx)
468 }
469
470 fn style_lines(&self, lines: Vec<String>, offset: usize) -> Vec<String> {
472 match &self.style_line_func {
473 Some(f) => lines
474 .iter()
475 .enumerate()
476 .map(|(i, l)| f(i + offset).render(l))
477 .collect(),
478 None => lines,
479 }
480 }
481
482 fn highlight_lines(&self, lines: Vec<String>, offset: usize) -> Vec<String> {
485 if self.highlights.is_empty() {
486 return lines;
487 }
488 lines
489 .iter()
490 .enumerate()
491 .map(|(i, line)| {
492 let ranges =
493 make_highlight_ranges(&self.highlights, i + offset, &self.highlight_style);
494 if self.hi_idx >= 0 {
495 let sel = &self.highlights[self.hi_idx as usize];
496 if let Some(hi) = sel.lines.get(&(i + offset)) {
497 return rusty_lipgloss::ranges::style_ranges(
500 line,
501 &[rusty_lipgloss::ranges::new_range(
502 hi.0,
503 hi.1,
504 self.selected_highlight_style.clone(),
505 )],
506 );
507 }
508 }
509 rusty_lipgloss::ranges::style_ranges(line, &ranges)
510 })
511 .collect()
512 }
513
514 fn soft_wrap_lines(
515 &self,
516 lines: Vec<String>,
517 max_width: usize,
518 max_height: usize,
519 total: usize,
520 ridx: usize,
521 voffset: usize,
522 ) -> Vec<String> {
523 let mut wrapped_lines: Vec<String> = Vec::with_capacity(max_height);
524
525 let mut idx: usize;
526 let mut line_width: usize;
527 let mut truncated_line: String;
528
529 for (i, line) in lines.iter().enumerate() {
530 line_width = rusty_x_ansi::string_width(line);
533
534 if line_width <= max_width {
535 if let Some(g) = &self.left_gutter_func {
536 let gutter = g(GutterContext {
537 index: i + ridx,
538 total_lines: total,
539 soft: false,
540 });
541 wrapped_lines.push(gutter + line);
542 } else {
543 wrapped_lines.push(line.clone());
544 }
545 continue;
546 }
547
548 idx = 0;
549 while line_width > idx {
550 truncated_line = rusty_x_ansi::cut(line, idx, max_width + idx);
551 if let Some(g) = &self.left_gutter_func {
552 let gutter = g(GutterContext {
553 index: i + ridx,
554 total_lines: total,
555 soft: idx > 0,
556 });
557 wrapped_lines.push(gutter + &truncated_line);
558 } else {
559 wrapped_lines.push(truncated_line);
560 }
561 idx += max_width;
562 }
563 }
564
565 wrapped_lines[voffset..(voffset + max_height).min(wrapped_lines.len())].to_vec()
566 }
567
568 fn setup_gutter(&self, lines: Vec<String>, total: usize, ridx: usize) -> Vec<String> {
570 match &self.left_gutter_func {
571 None => lines,
572 Some(g) => lines
573 .iter()
574 .enumerate()
575 .map(|(i, l)| {
576 let gutter = g(GutterContext {
577 index: i + ridx,
578 total_lines: total,
579 soft: false,
580 });
581 gutter + l
582 })
583 .collect(),
584 }
585 }
586
587 pub fn set_y_offset(&mut self, n: usize) {
589 self.y_offset = clamp(n, 0, self.max_y_offset());
590 }
591
592 pub fn y_offset(&self) -> usize {
594 self.y_offset
595 }
596
597 pub fn ensure_visible(&mut self, line: usize, colstart: usize, colend: usize) {
600 let max_width = self.max_width();
601 if colend <= max_width {
602 self.set_x_offset(0);
603 } else {
604 self.set_x_offset(colstart.saturating_sub(self.horizontal_step)); }
606
607 if line < self.y_offset() || line >= self.y_offset() + self.max_height() {
608 self.set_y_offset(line);
609 }
610 }
611
612 pub fn page_down(&mut self) {
614 if self.at_bottom() {
615 return;
616 }
617 self.scroll_down(self.height());
618 }
619
620 pub fn page_up(&mut self) {
622 if self.at_top() {
623 return;
624 }
625 self.scroll_up(self.height());
626 }
627
628 pub fn half_page_down(&mut self) {
630 if self.at_bottom() {
631 return;
632 }
633 self.scroll_down(self.height() / 2);
634 }
635
636 pub fn half_page_up(&mut self) {
638 if self.at_top() {
639 return;
640 }
641 self.scroll_up(self.height() / 2);
642 }
643
644 pub fn scroll_down(&mut self, n: usize) {
646 if self.at_bottom() || n == 0 || self.lines.is_empty() {
647 return;
648 }
649 self.set_y_offset(self.y_offset() + n);
653 self.hi_idx = self.find_nearest_match();
654 }
655
656 pub fn scroll_up(&mut self, n: usize) {
658 if self.at_top() || n == 0 || self.lines.is_empty() {
659 return;
660 }
661 self.set_y_offset(self.y_offset() - n);
664 self.hi_idx = self.find_nearest_match();
665 }
666
667 pub fn set_horizontal_step(&mut self, n: usize) {
671 self.horizontal_step = n;
672 }
673
674 pub fn x_offset(&self) -> usize {
677 self.x_offset
678 }
679
680 pub fn set_x_offset(&mut self, n: usize) {
683 if self.soft_wrap {
684 return;
685 }
686 self.x_offset = clamp(n, 0, self.max_x_offset());
687 }
688
689 pub fn scroll_left(&mut self, n: usize) {
692 self.set_x_offset(self.x_offset.saturating_sub(n));
695 }
696
697 pub fn scroll_right(&mut self, n: usize) {
700 self.set_x_offset(self.x_offset + n);
701 }
702
703 pub fn total_line_count(&self) -> usize {
706 let (total, _, _) = self.calculate_line(0);
707 total
708 }
709
710 pub fn visible_line_count(&self) -> usize {
713 self.visible_lines().len()
714 }
715
716 pub fn goto_top(&mut self) -> Vec<String> {
718 if self.at_top() {
719 return vec![];
720 }
721 self.set_y_offset(0);
722 self.hi_idx = self.find_nearest_match();
723 self.visible_lines()
724 }
725
726 pub fn goto_bottom(&mut self) -> Vec<String> {
728 self.set_y_offset(self.max_y_offset());
729 self.hi_idx = self.find_nearest_match();
730 self.visible_lines()
731 }
732
733 pub fn set_highlights(&mut self, matches: &[Vec<usize>]) {
739 if matches.is_empty() || self.lines.is_empty() {
740 return;
741 }
742 self.highlights = parse_matches(&self.get_content(), matches);
743 self.hi_idx = self.find_nearest_match();
744 self.show_highlight();
745 }
746
747 pub fn highlights(&self) -> &[HighlightInfo] {
752 &self.highlights
753 }
754
755 pub fn clear_highlights(&mut self) {
757 self.highlights.clear();
758 self.hi_idx = -1;
759 }
760
761 fn show_highlight(&mut self) {
762 if self.hi_idx == -1 {
763 return;
764 }
765 let (line, colstart, colend) = self.highlights[self.hi_idx as usize].coords();
766 self.ensure_visible(line, colstart, colend);
767 }
768
769 pub fn highlight_next(&mut self) {
771 if self.highlights.is_empty() {
772 return;
773 }
774 self.hi_idx = (self.hi_idx + 1) % self.highlights.len() as isize;
775 self.show_highlight();
776 }
777
778 pub fn highlight_previous(&mut self) {
780 if self.highlights.is_empty() {
781 return;
782 }
783 self.hi_idx =
784 (self.hi_idx - 1 + self.highlights.len() as isize) % self.highlights.len() as isize;
785 self.show_highlight();
786 }
787
788 fn find_nearest_match(&self) -> isize {
789 for (i, m) in self.highlights.iter().enumerate() {
790 if m.line_start >= self.y_offset() {
791 return i as isize;
792 }
793 }
794 -1
795 }
796
797 pub fn update(&mut self, msg: &dyn Msg) -> Cmd {
799 self.update_as_model(msg);
800 None
801 }
802
803 fn update_as_model(&mut self, msg: &dyn Msg) {
804 if !self.initialized {
805 self.set_initial_values();
806 }
807
808 if let Some(m) = msg.as_any().downcast_ref::<KeyPressMsg>() {
809 let k = &m.0;
810 if key::matches(k, std::slice::from_ref(&self.key_map.page_down)) {
811 self.page_down();
812 } else if key::matches(k, std::slice::from_ref(&self.key_map.page_up)) {
813 self.page_up();
814 } else if key::matches(k, std::slice::from_ref(&self.key_map.half_page_down)) {
815 self.half_page_down();
816 } else if key::matches(k, std::slice::from_ref(&self.key_map.half_page_up)) {
817 self.half_page_up();
818 } else if key::matches(k, std::slice::from_ref(&self.key_map.down)) {
819 self.scroll_down(1);
820 } else if key::matches(k, std::slice::from_ref(&self.key_map.up)) {
821 self.scroll_up(1);
822 } else if key::matches(k, std::slice::from_ref(&self.key_map.left)) {
823 self.scroll_left(self.horizontal_step);
824 } else if key::matches(k, std::slice::from_ref(&self.key_map.right)) {
825 self.scroll_right(self.horizontal_step);
826 }
827 return;
828 }
829
830 if let Some(m) = msg.as_any().downcast_ref::<MouseWheelMsg>() {
831 if !self.mouse_wheel_enabled {
832 return;
833 }
834 let mouse = &m.0;
835 match mouse.button {
836 MouseButton::MouseWheelDown => {
837 if mouse.mod_keys.contains(rusty_bubbletea::key::KeyMod::SHIFT) {
840 self.scroll_right(self.horizontal_step);
841 return;
842 }
843 self.scroll_down(self.mouse_wheel_delta);
844 }
845 MouseButton::MouseWheelUp => {
846 if mouse.mod_keys.contains(rusty_bubbletea::key::KeyMod::SHIFT) {
849 self.scroll_left(self.horizontal_step);
850 return;
851 }
852 self.scroll_up(self.mouse_wheel_delta);
853 }
854 MouseButton::MouseWheelLeft => {
855 self.scroll_left(self.horizontal_step);
856 }
857 MouseButton::MouseWheelRight => {
858 self.scroll_right(self.horizontal_step);
859 }
860 _ => {}
861 }
862 }
863 }
864
865 pub fn view(&self) -> String {
867 let mut w = self.width();
868 let mut h = self.height();
869 let sw = self.style.get_width();
870 if sw != 0 {
871 w = w.min(sw);
872 }
873 let sh = self.style.get_height();
874 if sh != 0 {
875 h = h.min(sh);
876 }
877
878 if w == 0 || h == 0 {
879 return String::new();
880 }
881
882 let content_width = w - self.style.get_horizontal_frame_size();
883 let content_height = h - self.style.get_vertical_frame_size();
884 let vl = self.visible_lines();
885 let contents = rusty_lipgloss::new_style()
886 .width(content_width) .height(content_height) .render(&vl.join("\n"));
889 self.style
890 .clone()
891 .unset_width()
892 .unset_height() .render(&contents)
894 }
895}
896
897#[derive(Debug, Clone)]
899pub struct KeyMap {
900 pub page_down: Binding,
902 pub page_up: Binding,
904 pub half_page_up: Binding,
906 pub half_page_down: Binding,
908 pub down: Binding,
910 pub up: Binding,
912 pub left: Binding,
914 pub right: Binding,
916}
917
918pub fn default_key_map() -> KeyMap {
920 KeyMap {
921 page_down: key::new_binding(vec![
922 key::with_keys(&["pgdown", "space", "f"]),
923 key::with_help("f/pgdn", "page down"),
924 ]),
925 page_up: key::new_binding(vec![
926 key::with_keys(&["pgup", "b"]),
927 key::with_help("b/pgup", "page up"),
928 ]),
929 half_page_up: key::new_binding(vec![
930 key::with_keys(&["u", "ctrl+u"]),
931 key::with_help("u", "½ page up"),
932 ]),
933 half_page_down: key::new_binding(vec![
934 key::with_keys(&["d", "ctrl+d"]),
935 key::with_help("d", "½ page down"),
936 ]),
937 up: key::new_binding(vec![
938 key::with_keys(&["up", "k"]),
939 key::with_help("↑/k", "up"),
940 ]),
941 down: key::new_binding(vec![
942 key::with_keys(&["down", "j"]),
943 key::with_help("↓/j", "down"),
944 ]),
945 left: key::new_binding(vec![
946 key::with_keys(&["left", "h"]),
947 key::with_help("←/h", "move left"),
948 ]),
949 right: key::new_binding(vec![
950 key::with_keys(&["right", "l"]),
951 key::with_help("→/l", "move right"),
952 ]),
953 }
954}
955
956#[derive(Debug, Clone, PartialEq)]
961pub struct HighlightInfo {
962 pub line_start: usize,
964 pub line_end: usize,
966 pub lines: HashMap<usize, (usize, usize)>,
968}
969
970impl HighlightInfo {
971 fn coords(&self) -> (usize, usize, usize) {
973 for i in self.line_start..=self.line_end {
974 if let Some(hl) = self.lines.get(&i) {
975 return (i, hl.0, hl.1);
976 }
977 }
978 (self.line_start, 0, 0)
979 }
980}
981
982fn parse_matches(content: &str, matches: &[Vec<usize>]) -> Vec<HighlightInfo> {
991 if matches.is_empty() {
992 return vec![];
993 }
994
995 let stripped: Vec<u8> = rusty_x_ansi::strip(content).as_bytes().to_vec();
999
1000 let mut highlights: Vec<HighlightInfo> = Vec::with_capacity(matches.len());
1001
1002 for m in matches {
1003 let (byte_start, byte_end) = (m[0], m[1]);
1004
1005 let mut hi = HighlightInfo {
1007 line_start: 0,
1008 line_end: 0,
1009 lines: HashMap::new(),
1010 };
1011
1012 let mut line = 0usize;
1013 let mut grapheme_pos = 0usize;
1014 let mut previous_lines_offset = 0usize;
1015 let mut byte_pos = 0usize;
1016
1017 while byte_start > byte_pos && byte_pos < stripped.len() {
1020 let c = char_at(&stripped, byte_pos);
1021 if c == '\n' {
1022 previous_lines_offset = grapheme_pos + 1;
1023 line += 1;
1024 }
1025 grapheme_pos += 1usize.max(char_width(c));
1026 byte_pos += char_len(c);
1027 }
1028
1029 hi.line_start = line;
1030 hi.line_end = line;
1031
1032 let grapheme_start = grapheme_pos;
1033
1034 while byte_end > byte_pos && byte_pos < stripped.len() {
1036 let c = char_at(&stripped, byte_pos);
1037 if c == '\n' {
1040 let colstart = grapheme_start.saturating_sub(previous_lines_offset);
1041 let colend = (grapheme_pos.saturating_sub(previous_lines_offset) + 1).max(colstart); if colend > colstart {
1044 hi.lines.insert(line, (colstart, colend));
1045 hi.line_end = line;
1046 }
1047
1048 previous_lines_offset = grapheme_pos + 1;
1049 line += 1;
1050 }
1051
1052 grapheme_pos += 1usize.max(char_width(c));
1053 byte_pos += char_len(c);
1054 }
1055
1056 if byte_pos == byte_end {
1058 let colstart = grapheme_start.saturating_sub(previous_lines_offset);
1059 let colend = (grapheme_pos.saturating_sub(previous_lines_offset)).max(colstart);
1060
1061 if colend > colstart {
1062 hi.lines.insert(line, (colstart, colend));
1063 hi.line_end = line;
1064 }
1065 }
1066
1067 highlights.push(hi);
1068 }
1069
1070 highlights
1071}
1072
1073fn char_at(s: &[u8], byte_pos: usize) -> char {
1076 std::str::from_utf8(&s[byte_pos..])
1077 .ok()
1078 .and_then(|r| r.chars().next())
1079 .unwrap_or('\u{FFFD}')
1080}
1081
1082fn make_highlight_ranges(highlights: &[HighlightInfo], line: usize, style: &Style) -> Vec<Range> {
1083 let mut result: Vec<Range> = vec![];
1084 for hi in highlights {
1085 if let Some(lihi) = hi.lines.get(&line) {
1086 if *lihi == (0, 0) {
1087 continue;
1088 }
1089 result.push(rusty_lipgloss::ranges::new_range(
1090 lihi.0,
1091 lihi.1,
1092 style.clone(),
1093 ));
1094 }
1095 }
1096 result
1097}
1098
1099fn char_width(c: char) -> usize {
1100 unicode_width::UnicodeWidthChar::width(c).unwrap_or(0)
1101}
1102
1103fn char_len(c: char) -> usize {
1104 c.len_utf8()
1105}
1106
1107fn clamp<T: PartialOrd + Copy>(v: T, low: T, high: T) -> T {
1108 if high < low {
1109 return low;
1110 }
1111 if v < low {
1112 low
1113 } else if v > high {
1114 high
1115 } else {
1116 v
1117 }
1118}
1119
1120fn max_line_width(lines: &[String]) -> usize {
1121 let mut result = 0;
1122 for line in lines {
1123 result = result.max(rusty_x_ansi::string_width(line));
1124 }
1125 result
1126}