1#![forbid(unsafe_code)]
2
3use crate::rope::Rope;
10use crate::wrap::{WrapMode, WrapOptions, display_width, wrap_with_options};
11use std::ops::Range;
12
13#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
15pub struct Viewport {
16 pub width: usize,
18 pub height: usize,
20}
21
22impl Viewport {
23 #[must_use]
25 pub const fn new(width: usize, height: usize) -> Self {
26 Self { width, height }
27 }
28}
29
30#[derive(Debug, Clone, PartialEq, Eq)]
32pub struct ViewLine {
33 pub text: String,
35 pub source_line: usize,
37 pub is_wrap: bool,
39 pub width: usize,
41}
42
43#[derive(Debug, Clone)]
45pub struct TextView {
46 text: Rope,
47 wrap: WrapMode,
48 width: usize,
49 lines: Vec<ViewLine>,
50 max_width: usize,
51 source_line_count: usize,
52}
53
54impl TextView {
55 #[must_use]
57 pub fn new(text: impl Into<Rope>, width: usize, wrap: WrapMode) -> Self {
58 let mut view = Self {
59 text: text.into(),
60 wrap,
61 width,
62 lines: Vec::new(),
63 max_width: 0,
64 source_line_count: 0,
65 };
66 view.rebuild();
67 view
68 }
69
70 pub fn set_text(&mut self, text: impl Into<Rope>) {
72 self.text = text.into();
73 self.rebuild();
74 }
75
76 pub fn set_wrap(&mut self, wrap: WrapMode) {
78 if self.wrap != wrap {
79 self.wrap = wrap;
80 self.rebuild();
81 }
82 }
83
84 pub fn set_width(&mut self, width: usize) {
86 if self.width != width {
87 self.width = width;
88 self.rebuild();
89 }
90 }
91
92 #[inline]
94 #[must_use]
95 pub const fn wrap_mode(&self) -> WrapMode {
96 self.wrap
97 }
98
99 #[inline]
101 #[must_use]
102 pub const fn width(&self) -> usize {
103 self.width
104 }
105
106 #[inline]
108 #[must_use]
109 pub const fn source_line_count(&self) -> usize {
110 self.source_line_count
111 }
112
113 #[inline]
115 #[must_use]
116 pub fn virtual_line_count(&self) -> usize {
117 self.lines.len()
118 }
119
120 #[inline]
122 #[must_use]
123 pub const fn max_width(&self) -> usize {
124 self.max_width
125 }
126
127 #[inline]
129 #[must_use]
130 pub fn lines(&self) -> &[ViewLine] {
131 &self.lines
132 }
133
134 #[must_use]
136 pub fn source_to_virtual(&self, source_line: usize) -> Option<usize> {
137 self.lines
138 .iter()
139 .position(|line| line.source_line == source_line)
140 }
141
142 #[must_use]
144 pub fn virtual_to_source(&self, virtual_line: usize) -> Option<usize> {
145 self.lines.get(virtual_line).map(|line| line.source_line)
146 }
147
148 #[must_use]
150 pub fn clamp_scroll(&self, scroll_y: usize, viewport_height: usize) -> usize {
151 let total = self.lines.len();
152 if total == 0 {
153 return 0;
154 }
155 if viewport_height == 0 {
156 return scroll_y.min(total);
157 }
158 let max_scroll = total.saturating_sub(viewport_height);
159 scroll_y.min(max_scroll)
160 }
161
162 #[must_use]
164 pub fn max_scroll(&self, viewport_height: usize) -> usize {
165 let total = self.lines.len();
166 if total == 0 {
167 return 0;
168 }
169 if viewport_height == 0 {
170 return total;
171 }
172 total.saturating_sub(viewport_height)
173 }
174
175 #[must_use]
177 pub fn visible_range(&self, scroll_y: usize, viewport_height: usize) -> Range<usize> {
178 let total = self.lines.len();
179 if total == 0 || viewport_height == 0 {
180 return 0..0;
181 }
182 let scroll = self.clamp_scroll(scroll_y, viewport_height);
183 let end = (scroll + viewport_height).min(total);
184 scroll..end
185 }
186
187 #[must_use]
189 pub fn visible_lines(&self, scroll_y: usize, viewport_height: usize) -> &[ViewLine] {
190 let range = self.visible_range(scroll_y, viewport_height);
191 &self.lines[range]
192 }
193
194 #[must_use]
197 pub fn scroll_to_line(&self, source_line: usize, viewport_height: usize) -> Option<usize> {
198 let virtual_line = self.source_to_virtual(source_line)?;
199 Some(self.clamp_scroll(virtual_line, viewport_height))
200 }
201
202 #[must_use]
204 pub fn scroll_to_top(&self) -> usize {
205 0
206 }
207
208 #[must_use]
210 pub fn scroll_to_bottom(&self, viewport_height: usize) -> usize {
211 self.max_scroll(viewport_height)
212 }
213
214 #[must_use]
216 pub fn scroll_by_lines(&self, scroll_y: usize, delta: isize, viewport_height: usize) -> usize {
217 let next = (scroll_y as i64) + (delta as i64);
218 let next = if next < 0 { 0 } else { next as usize };
219 self.clamp_scroll(next, viewport_height)
220 }
221
222 #[must_use]
224 pub fn scroll_by_pages(&self, scroll_y: usize, pages: isize, viewport_height: usize) -> usize {
225 if viewport_height == 0 {
226 return self.clamp_scroll(scroll_y, viewport_height);
227 }
228 let delta = (viewport_height as i64) * (pages as i64);
229 let next = (scroll_y as i64) + delta;
230 let next = if next < 0 { 0 } else { next as usize };
231 self.clamp_scroll(next, viewport_height)
232 }
233
234 fn rebuild(&mut self) {
235 self.lines.clear();
236 self.max_width = 0;
237
238 let preserve_indent = self.wrap == WrapMode::Char;
239 let options = WrapOptions::new(self.width)
240 .mode(self.wrap)
241 .preserve_indent(preserve_indent);
242
243 let mut source_lines = 0;
244
245 for (source_line, line) in self.text.lines().enumerate() {
246 source_lines += 1;
247 let mut line_text = line.to_string();
248 if line_text.ends_with('\n') {
249 line_text.pop();
250 }
251 if line_text.ends_with('\r') {
252 line_text.pop();
253 }
254
255 let wrapped = wrap_with_options(&line_text, &options);
256 if wrapped.is_empty() {
257 let width = 0;
258 self.lines.push(ViewLine {
259 text: String::new(),
260 source_line,
261 is_wrap: false,
262 width,
263 });
264 self.max_width = self.max_width.max(width);
265 continue;
266 }
267
268 for (idx, part) in wrapped.into_iter().enumerate() {
269 let width = display_width(&part);
270 self.max_width = self.max_width.max(width);
271 self.lines.push(ViewLine {
272 text: part,
273 source_line,
274 is_wrap: idx > 0,
275 width,
276 });
277 }
278 }
279
280 self.source_line_count = source_lines;
281 }
282}
283
284#[cfg(test)]
285mod tests {
286 use super::{TextView, Viewport};
287 use crate::wrap::WrapMode;
288
289 #[test]
290 fn view_basic_counts() {
291 let view = TextView::new("a\nbb", 10, WrapMode::None);
292 assert_eq!(view.source_line_count(), 2);
293 assert_eq!(view.virtual_line_count(), 2);
294 assert_eq!(view.max_width(), 2);
295 }
296
297 #[test]
298 fn view_wraps_word() {
299 let view = TextView::new("hello world", 5, WrapMode::Word);
300 let lines: Vec<&str> = view.lines().iter().map(|l| l.text.as_str()).collect();
301 assert_eq!(lines, vec!["hello", "world"]);
302 }
303
304 #[test]
305 fn view_wraps_cjk_by_cells() {
306 let view = TextView::new("你好世界", 4, WrapMode::Char);
307 let lines: Vec<&str> = view.lines().iter().map(|l| l.text.as_str()).collect();
308 assert_eq!(lines, vec!["你好", "世界"]);
309 }
310
311 #[test]
312 fn view_strips_crlf() {
313 let view = TextView::new("a\r\nb", 10, WrapMode::None);
314 let lines: Vec<&str> = view.lines().iter().map(|l| l.text.as_str()).collect();
315 assert_eq!(lines, vec!["a", "b"]);
316 }
317
318 #[test]
319 fn visible_range_clamps_scroll() {
320 let view = TextView::new("a\nb\nc", 10, WrapMode::None);
321 let range = view.visible_range(5, 2);
322 assert_eq!(range, 1..3);
323 }
324
325 #[test]
326 fn scroll_to_line_clamps() {
327 let view = TextView::new("a\nb\nc\nd", 10, WrapMode::None);
328 let scroll = view.scroll_to_line(3, 2).expect("line 3 exists");
329 assert_eq!(scroll, 2);
330 }
331
332 #[test]
333 fn scroll_by_pages_moves_in_viewport_steps() {
334 let view = TextView::new("1\n2\n3\n4\n5", 10, WrapMode::None);
335 let scroll = view.scroll_by_pages(0, 1, 2);
336 assert_eq!(scroll, 2);
337 let back = view.scroll_by_pages(scroll, -1, 2);
338 assert_eq!(back, 0);
339 }
340
341 #[test]
342 fn scroll_to_bottom_respects_viewport() {
343 let view = TextView::new("a\nb\nc\nd", 10, WrapMode::None);
344 let bottom = view.scroll_to_bottom(2);
345 assert_eq!(bottom, 2);
346 let top = view.scroll_to_top();
347 assert_eq!(top, 0);
348 }
349
350 #[test]
351 fn visible_lines_returns_slice() {
352 let view = TextView::new("a\nb\nc\nd", 10, WrapMode::None);
353 let visible = view.visible_lines(1, 2);
354 let texts: Vec<&str> = visible.iter().map(|l| l.text.as_str()).collect();
355 assert_eq!(texts, vec!["b", "c"]);
356 }
357
358 #[test]
359 fn viewport_struct_is_copyable() {
360 let viewport = Viewport::new(80, 24);
361 let copy = viewport;
362 assert_eq!(copy.width, 80);
363 assert_eq!(copy.height, 24);
364 }
365
366 #[test]
369 fn empty_text_view() {
370 let view = TextView::new("", 10, WrapMode::None);
371 assert_eq!(view.source_line_count(), 1); assert_eq!(view.virtual_line_count(), 1);
373 assert_eq!(view.max_width(), 0);
374 }
375
376 #[test]
377 fn empty_text_scroll() {
378 let view = TextView::new("", 10, WrapMode::None);
379 assert_eq!(view.max_scroll(5), 0);
380 assert_eq!(view.clamp_scroll(100, 5), 0);
381 assert_eq!(view.visible_range(0, 5), 0..1);
382 }
383
384 #[test]
387 fn source_to_virtual_no_wrap() {
388 let view = TextView::new("a\nb\nc", 10, WrapMode::None);
389 assert_eq!(view.source_to_virtual(0), Some(0));
390 assert_eq!(view.source_to_virtual(1), Some(1));
391 assert_eq!(view.source_to_virtual(2), Some(2));
392 assert_eq!(view.source_to_virtual(3), None);
393 }
394
395 #[test]
396 fn virtual_to_source_no_wrap() {
397 let view = TextView::new("a\nb\nc", 10, WrapMode::None);
398 assert_eq!(view.virtual_to_source(0), Some(0));
399 assert_eq!(view.virtual_to_source(1), Some(1));
400 assert_eq!(view.virtual_to_source(2), Some(2));
401 assert_eq!(view.virtual_to_source(3), None);
402 }
403
404 #[test]
405 fn source_to_virtual_with_wrap() {
406 let view = TextView::new("abcde\nxy", 3, WrapMode::Char);
408 assert_eq!(view.source_to_virtual(0), Some(0)); assert_eq!(view.source_to_virtual(1), Some(2)); }
411
412 #[test]
413 fn virtual_to_source_with_wrap() {
414 let view = TextView::new("abcde\nxy", 3, WrapMode::Char);
415 assert_eq!(view.virtual_to_source(0), Some(0)); assert_eq!(view.virtual_to_source(1), Some(0)); assert_eq!(view.virtual_to_source(2), Some(1)); }
419
420 #[test]
423 fn is_wrap_flag_set_correctly() {
424 let view = TextView::new("abcde", 3, WrapMode::Char);
425 let lines = view.lines();
426 assert!(!lines[0].is_wrap); assert!(lines[1].is_wrap); }
429
430 #[test]
433 fn set_text_recomputes() {
434 let mut view = TextView::new("abc", 10, WrapMode::None);
435 assert_eq!(view.source_line_count(), 1);
436 view.set_text("a\nb\nc");
437 assert_eq!(view.source_line_count(), 3);
438 assert_eq!(view.virtual_line_count(), 3);
439 }
440
441 #[test]
442 fn set_wrap_recomputes() {
443 let mut view = TextView::new("hello world", 5, WrapMode::None);
444 let before = view.virtual_line_count();
445 view.set_wrap(WrapMode::Word);
446 let after = view.virtual_line_count();
447 assert!(after >= before);
449 }
450
451 #[test]
452 fn set_wrap_same_mode_is_noop() {
453 let mut view = TextView::new("hello", 10, WrapMode::None);
454 let count1 = view.virtual_line_count();
455 view.set_wrap(WrapMode::None); assert_eq!(view.virtual_line_count(), count1);
457 }
458
459 #[test]
460 fn set_width_recomputes() {
461 let mut view = TextView::new("abcdef", 3, WrapMode::Char);
462 let count_narrow = view.virtual_line_count();
463 view.set_width(100);
464 let count_wide = view.virtual_line_count();
465 assert!(count_narrow > count_wide);
466 }
467
468 #[test]
469 fn set_width_same_is_noop() {
470 let mut view = TextView::new("abc", 10, WrapMode::None);
471 let count = view.virtual_line_count();
472 view.set_width(10);
473 assert_eq!(view.virtual_line_count(), count);
474 }
475
476 #[test]
479 fn wrap_mode_accessor() {
480 let view = TextView::new("abc", 10, WrapMode::Word);
481 assert_eq!(view.wrap_mode(), WrapMode::Word);
482 }
483
484 #[test]
485 fn width_accessor() {
486 let view = TextView::new("abc", 42, WrapMode::None);
487 assert_eq!(view.width(), 42);
488 }
489
490 #[test]
493 fn max_width_across_lines() {
494 let view = TextView::new("ab\nabcde\nxy", 100, WrapMode::None);
495 assert_eq!(view.max_width(), 5); }
497
498 #[test]
499 fn max_width_with_wide_chars() {
500 let view = TextView::new("\u{4E16}\u{754C}", 100, WrapMode::None); assert_eq!(view.max_width(), 4);
502 }
503
504 #[test]
507 fn clamp_scroll_within_bounds() {
508 let view = TextView::new("a\nb\nc\nd", 10, WrapMode::None); assert_eq!(view.clamp_scroll(0, 2), 0);
510 assert_eq!(view.clamp_scroll(1, 2), 1);
511 assert_eq!(view.clamp_scroll(2, 2), 2); assert_eq!(view.clamp_scroll(3, 2), 2); assert_eq!(view.clamp_scroll(100, 2), 2); }
515
516 #[test]
517 fn clamp_scroll_viewport_larger_than_content() {
518 let view = TextView::new("a\nb", 10, WrapMode::None); assert_eq!(view.clamp_scroll(0, 10), 0);
521 assert_eq!(view.clamp_scroll(5, 10), 0);
522 }
523
524 #[test]
525 fn clamp_scroll_zero_viewport() {
526 let view = TextView::new("a\nb\nc", 10, WrapMode::None);
527 let result = view.clamp_scroll(1, 0);
529 assert_eq!(result, 1);
530 }
531
532 #[test]
535 fn max_scroll_basic() {
536 let view = TextView::new("a\nb\nc\nd\ne", 10, WrapMode::None); assert_eq!(view.max_scroll(3), 2); assert_eq!(view.max_scroll(5), 0); assert_eq!(view.max_scroll(10), 0); }
541
542 #[test]
543 fn max_scroll_zero_viewport() {
544 let view = TextView::new("a\nb\nc", 10, WrapMode::None);
545 assert_eq!(view.max_scroll(0), 3); }
547
548 #[test]
551 fn visible_range_basic() {
552 let view = TextView::new("a\nb\nc\nd\ne", 10, WrapMode::None);
553 assert_eq!(view.visible_range(0, 3), 0..3);
554 assert_eq!(view.visible_range(1, 3), 1..4);
555 assert_eq!(view.visible_range(2, 3), 2..5);
556 }
557
558 #[test]
559 fn visible_range_zero_viewport() {
560 let view = TextView::new("a\nb\nc", 10, WrapMode::None);
561 assert_eq!(view.visible_range(0, 0), 0..0);
562 }
563
564 #[test]
565 fn visible_lines_content() {
566 let view = TextView::new("alpha\nbeta\ngamma\ndelta", 10, WrapMode::None);
567 let visible = view.visible_lines(1, 2);
568 assert_eq!(visible.len(), 2);
569 assert_eq!(visible[0].text, "beta");
570 assert_eq!(visible[1].text, "gamma");
571 }
572
573 #[test]
576 fn scroll_to_line_basic() {
577 let view = TextView::new("a\nb\nc\nd\ne", 10, WrapMode::None);
578 assert_eq!(view.scroll_to_line(0, 3), Some(0));
579 assert_eq!(view.scroll_to_line(2, 3), Some(2));
580 assert_eq!(view.scroll_to_line(4, 3), Some(2)); }
582
583 #[test]
584 fn scroll_to_line_nonexistent() {
585 let view = TextView::new("a\nb", 10, WrapMode::None);
586 assert_eq!(view.scroll_to_line(5, 2), None);
587 }
588
589 #[test]
592 fn scroll_by_lines_positive() {
593 let view = TextView::new("a\nb\nc\nd\ne", 10, WrapMode::None);
594 assert_eq!(view.scroll_by_lines(0, 2, 3), 2);
595 assert_eq!(view.scroll_by_lines(0, 100, 3), 2); }
597
598 #[test]
599 fn scroll_by_lines_negative() {
600 let view = TextView::new("a\nb\nc\nd\ne", 10, WrapMode::None);
601 assert_eq!(view.scroll_by_lines(2, -1, 3), 1);
602 assert_eq!(view.scroll_by_lines(2, -100, 3), 0); }
604
605 #[test]
608 fn scroll_by_pages_forward() {
609 let text = (0..10)
611 .map(|i| format!("line{i}"))
612 .collect::<Vec<_>>()
613 .join("\n");
614 let view = TextView::new(text.as_str(), 100, WrapMode::None);
615 assert_eq!(view.scroll_by_pages(0, 1, 3), 3);
616 assert_eq!(view.scroll_by_pages(0, 2, 3), 6);
617 }
618
619 #[test]
620 fn scroll_by_pages_backward() {
621 let text = (0..10)
622 .map(|i| format!("line{i}"))
623 .collect::<Vec<_>>()
624 .join("\n");
625 let view = TextView::new(text.as_str(), 100, WrapMode::None);
626 assert_eq!(view.scroll_by_pages(6, -1, 3), 3);
627 assert_eq!(view.scroll_by_pages(6, -3, 3), 0); }
629
630 #[test]
631 fn scroll_by_pages_zero_viewport() {
632 let view = TextView::new("a\nb\nc", 10, WrapMode::None);
633 let result = view.scroll_by_pages(0, 1, 0);
635 assert_eq!(result, 0);
636 }
637
638 #[test]
641 fn scroll_to_top_and_bottom() {
642 let view = TextView::new("a\nb\nc\nd\ne", 10, WrapMode::None);
643 assert_eq!(view.scroll_to_top(), 0);
644 assert_eq!(view.scroll_to_bottom(3), 2);
645 assert_eq!(view.scroll_to_bottom(5), 0);
646 assert_eq!(view.scroll_to_bottom(1), 4);
647 }
648
649 #[test]
652 fn trailing_newline_text() {
653 let view = TextView::new("a\nb\n", 10, WrapMode::None);
654 assert_eq!(view.source_line_count(), 3);
655 let lines = view.lines();
657 assert_eq!(lines.last().unwrap().text, "");
658 }
659
660 #[test]
663 fn only_newlines() {
664 let view = TextView::new("\n\n\n", 10, WrapMode::None);
665 assert_eq!(view.source_line_count(), 4); assert_eq!(view.virtual_line_count(), 4);
667 for line in view.lines() {
668 assert_eq!(line.text, "");
669 }
670 }
671
672 #[test]
675 fn view_line_source_line_tracking() {
676 let view = TextView::new("ab\ncd\nef", 10, WrapMode::None);
677 for (i, line) in view.lines().iter().enumerate() {
678 assert_eq!(line.source_line, i);
679 assert!(!line.is_wrap);
680 }
681 }
682
683 #[test]
684 fn view_line_width_tracking() {
685 let view = TextView::new("ab\nabcde\n\u{4E16}", 10, WrapMode::None);
686 assert_eq!(view.lines()[0].width, 2);
687 assert_eq!(view.lines()[1].width, 5);
688 assert_eq!(view.lines()[2].width, 2); }
690
691 #[test]
694 fn viewport_default() {
695 let v = Viewport::default();
696 assert_eq!(v.width, 0);
697 assert_eq!(v.height, 0);
698 }
699
700 #[test]
701 fn viewport_equality() {
702 assert_eq!(Viewport::new(80, 24), Viewport::new(80, 24));
703 assert_ne!(Viewport::new(80, 24), Viewport::new(120, 24));
704 }
705}