1use gpui::*;
2use twrite_core::{
3 HighlightTag, Point as BufferPoint, StyleSpan, StyleValue, UnderlineDecoration,
4 split_line_intervals,
5};
6
7use std::ops::Range;
8
9use crate::editor::Editor;
10use crate::theme::EditorTheme;
11
12#[derive(Debug, Clone)]
14pub struct LineMetrics {
15 pub font_size: Pixels,
17 pub line_height: Pixels,
19 pub is_quote: bool,
21 pub is_code_block: bool,
23 pub is_thematic_break: bool,
25 pub task_state: Option<bool>,
27}
28
29impl LineMetrics {
30 pub fn for_line(
37 raw_line_text: &str,
38 _concealed_display_text: &str,
39 spans: &[StyleSpan],
40 base_font_size: Pixels,
41 base_line_height: Pixels,
42 ) -> Self {
43 let mut font_size = base_font_size;
44 let mut line_height = base_line_height;
45
46 let heading_level = spans
49 .iter()
50 .filter_map(|s| match s.style {
51 StyleValue::Tag(HighlightTag::Heading(level)) => Some(level),
52 _ => None,
53 })
54 .min();
55
56 match heading_level {
57 Some(1) => {
58 font_size = base_font_size * 2.0;
59 line_height = base_line_height * 1.8;
60 }
61 Some(2) => {
62 font_size = base_font_size * 1.5;
63 line_height = base_line_height * 1.4;
64 }
65 Some(3) => {
66 font_size = base_font_size * 1.25;
67 line_height = base_line_height * 1.2;
68 }
69 Some(4) => {
70 font_size = base_font_size * 1.125;
71 line_height = base_line_height * 1.1;
72 }
73 Some(5) => {
74 font_size = base_font_size * 1.0;
75 line_height = base_line_height * 1.0;
76 }
77 Some(6) => {
78 font_size = base_font_size * 0.875;
79 line_height = base_line_height * 0.95;
80 }
81 _ => {}
82 }
83
84 let is_quote = spans
85 .iter()
86 .any(|s| matches!(s.style, StyleValue::Tag(HighlightTag::Blockquote)));
87
88 let is_code_block = spans.iter().any(|s| {
89 (raw_line_text.is_empty() || s.range.len() == raw_line_text.len())
90 && matches!(s.style, StyleValue::Tag(HighlightTag::Code))
91 });
92
93 let is_thematic_break = spans
94 .iter()
95 .any(|s| matches!(s.style, StyleValue::Tag(HighlightTag::HorizontalRule)));
96
97 let task_state = if spans
98 .iter()
99 .any(|s| matches!(s.style, StyleValue::Tag(HighlightTag::TaskChecked)))
100 {
101 Some(true)
102 } else if spans
103 .iter()
104 .any(|s| matches!(s.style, StyleValue::Tag(HighlightTag::TaskUnchecked)))
105 {
106 Some(false)
107 } else {
108 None
109 };
110
111 Self {
112 font_size,
113 line_height,
114 is_quote,
115 is_code_block,
116 is_thematic_break,
117 task_state,
118 }
119 }
120}
121
122#[derive(Debug, Clone, Copy)]
124pub struct RunFonts<'a> {
125 pub base: &'a Font,
127 pub code: &'a Font,
129}
130
131pub fn build_line_text_runs(
133 line_text: &str,
134 spans: &[StyleSpan],
135 selection_line_range: Option<(usize, usize)>,
136 fonts: &RunFonts,
137 theme: &EditorTheme,
138 is_code_block: bool,
139 is_checked_task: bool,
140) -> Vec<TextRun> {
141 if line_text.is_empty() {
142 return Vec::new();
143 }
144
145 let segments = split_line_intervals(line_text.len(), spans, selection_line_range);
146 let mut runs = Vec::with_capacity(segments.len());
147
148 for segment in segments {
149 let resolved = segment.style.map(|s| theme.resolve_style(s));
150
151 let color = if is_checked_task && !segment.is_selected {
152 theme.syntax.comment
153 } else {
154 resolved
155 .as_ref()
156 .map(|r| r.color)
157 .unwrap_or(theme.foreground)
158 };
159
160 let background_color = if segment.is_selected {
161 Some(theme.selection)
162 } else if is_code_block {
163 None
164 } else {
165 resolved.as_ref().and_then(|r| r.background)
166 };
167 let mut run_font = if matches!(segment.style, Some(StyleValue::Tag(HighlightTag::Code))) {
168 fonts.code.clone()
169 } else {
170 fonts.base.clone()
171 };
172 if let Some(r) = resolved.as_ref() {
173 if r.bold {
174 run_font.weight = FontWeight::BOLD;
175 }
176 if r.italic {
177 run_font.style = FontStyle::Italic;
178 }
179 }
180
181 let underline = resolved
182 .as_ref()
183 .and_then(|r| r.underline)
184 .map(|u| match u {
185 UnderlineDecoration::Solid => UnderlineStyle {
186 color: Some(color),
187 thickness: px(1.0),
188 wavy: false,
189 },
190 UnderlineDecoration::Wavy => UnderlineStyle {
191 color: Some(theme.syntax.error),
192 thickness: px(1.2),
193 wavy: true,
194 },
195 });
196
197 let strikethrough = if is_checked_task {
198 Some(StrikethroughStyle {
199 thickness: px(1.0),
200 color: Some(theme.syntax.comment),
201 })
202 } else {
203 resolved.as_ref().and_then(|r| {
204 if r.strikethrough {
205 Some(StrikethroughStyle {
206 color: Some(color),
207 thickness: px(1.0),
208 })
209 } else {
210 None
211 }
212 })
213 };
214
215 runs.push(TextRun {
216 len: segment.range.end - segment.range.start,
217 font: run_font,
218 color,
219 background_color,
220 underline,
221 strikethrough,
222 });
223 }
224
225 let mut merged: Vec<TextRun> = Vec::with_capacity(runs.len());
226 for run in runs {
227 if let Some(last) = merged.last_mut()
228 && last.font == run.font
229 && last.color == run.color
230 && last.background_color == run.background_color
231 && last.underline == run.underline
232 && last.strikethrough == run.strikethrough
233 {
234 last.len += run.len;
235 continue;
236 }
237 merged.push(run);
238 }
239
240 merged
241}
242
243struct PreparedLine {
245 gutter_num: Option<(Point<Pixels>, ShapedLine)>,
246 text_origin: Point<Pixels>,
247 text_line: WrappedLine,
248 line_height: Pixels,
249 quote_bar_quad: Option<PaintQuad>,
250 code_block_bg_quad: Option<PaintQuad>,
251 thematic_break_quad: Option<PaintQuad>,
252 empty_selection_quad: Option<PaintQuad>,
253 cursor_quad: Option<PaintQuad>,
254 task_checkbox_quad: Option<PaintQuad>,
255 search_match_quads: Vec<PaintQuad>,
257}
258
259fn wash_ranges_for_line(
264 matches: &[Range<usize>],
265 cursor: &mut usize,
266 line_start: usize,
267 line_end: usize,
268) -> Vec<Range<usize>> {
269 while *cursor < matches.len() && matches[*cursor].end <= line_start {
270 *cursor += 1;
271 }
272 let mut out = Vec::new();
273 let mut j = *cursor;
274 while j < matches.len() && matches[j].start < line_end {
275 let start = matches[j].start.max(line_start);
276 let end = matches[j].end.min(line_end);
277 if end > start {
278 out.push(start..end);
279 }
280 j += 1;
281 }
282 out
283}
284
285struct EditorCanvasPrepaint {
287 background_quad: PaintQuad,
288 lines: Vec<PreparedLine>,
289}
290
291#[derive(IntoElement)]
293pub struct EditorCanvas {
294 editor: Entity<Editor>,
295}
296
297impl EditorCanvas {
298 pub fn new(editor: Entity<Editor>) -> Self {
300 EditorCanvas { editor }
301 }
302}
303
304impl RenderOnce for EditorCanvas {
305 fn render(self, window: &mut Window, _cx: &mut App) -> impl IntoElement {
306 window.request_animation_frame();
307
308 let editor_handle = self.editor.clone();
309
310 canvas(
311 move |bounds, window, cx| {
312 editor_handle.update(cx, |editor, _| {
313 editor.last_bounds = Some(bounds);
314 editor.frame_stats.record();
316 let host = window.text_style().font();
321 let key = (
322 editor.config.font_family.clone(),
323 editor.config.code_font_family.clone(),
324 host.clone(),
325 );
326 if editor.face_probe_key.as_ref() != Some(&key) {
327 let text_system = window.text_system();
328 let mut probe_one = |family: &str| {
329 let mut base = host.clone();
330 base.family = SharedString::new(family);
334 let base_id = text_system.resolve_font(&base);
335 let mut bold = base.clone();
336 bold.weight = FontWeight::BOLD;
337 let mut italic = base.clone();
338 italic.style = FontStyle::Italic;
339 (
340 text_system.resolve_font(&bold) != base_id,
341 text_system.resolve_font(&italic) != base_id,
342 )
343 };
344 let candidates = editor.config.font_candidates();
345 let (selected, mut availability) = match editor.config.font_family.clone() {
346 Some(explicit) => {
347 let (bold, italic) = probe_one(explicit.as_ref());
348 (
349 Some(explicit),
350 crate::editor::FaceAvailability { bold, italic },
351 )
352 }
353 None => match crate::config::EditorConfig::pick_family(
354 &candidates,
355 &mut probe_one,
356 ) {
357 Some(winner) => {
358 let (bold, italic) = probe_one(winner.as_ref());
359 (
360 Some(winner.clone()),
361 crate::editor::FaceAvailability { bold, italic },
362 )
363 }
364 None => (
365 None,
366 crate::editor::FaceAvailability {
367 bold: false,
368 italic: false,
369 },
370 ),
371 },
372 };
373 if let Some(code_family) = &editor.config.code_font_family
375 && Some(code_family) != selected.as_ref()
376 {
377 let (bold, italic) = probe_one(code_family.as_ref());
378 availability.bold &= bold;
379 availability.italic &= italic;
380 }
381 editor.selected_font_family = selected;
382 editor.face_availability = Some(availability);
383 editor.face_probe_key = Some(key);
384 }
385 });
386 let mut layout_cache =
389 editor_handle.update(cx, |editor, _| std::mem::take(&mut editor.layout_cache));
390 let editor = editor_handle.read(cx);
391 let theme = editor.theme.clone();
392 let config = editor.config.clone();
393 let host_font = window.text_style().font();
394 let font = editor.resolved_base_font(&host_font);
395 let code_font = editor.resolved_code_font(&host_font);
396
397 let gutter_width = if config.line_numbers {
398 px(48.0)
399 } else {
400 px(0.0)
401 };
402 let text_origin_x = bounds.left() + gutter_width + px(12.0);
403
404 let wrap_width = if config.line_wrap {
407 let available = bounds.size.width - gutter_width - px(24.0);
408 Some(available.max(px(50.0)))
409 } else {
410 None
411 };
412
413 let total_lines = editor.buffer.len_lines();
414 let total_bytes = editor.buffer.len_bytes();
415 let scroll_row = editor.scroll_row;
416
417 let cursor_offset = editor.buffer.cursor_offset();
418 let cursor_point = editor.buffer.cursor_point();
419 let selection = editor.selection;
420
421 let is_all_selected = selection.is_some_and(|s| {
422 let range = s.byte_range();
423 !s.is_empty() && range.start == 0 && range.end == total_bytes
424 });
425
426 let mut lines: Vec<PreparedLine> = Vec::new();
427 let mut visible_line_layouts: Vec<crate::editor::VisibleLineLayout> = Vec::new();
428 let mut current_y = bounds.top();
429 let mut computed_cursor_pixel = None;
430 let search_wash: Vec<Range<usize>> = if editor.search_highlight_all {
433 editor.search_matches.clone()
434 } else {
435 Vec::new()
436 };
437 let mut wash_cursor = 0;
438
439 for row in scroll_row..total_lines {
440 if current_y >= bounds.bottom() {
441 break;
442 }
443
444 let raw_line = editor.buffer.line_to_string(row);
445 let line_text = raw_line.trim_end_matches(['\r', '\n']);
446 let line_start_byte = editor.buffer.point_to_offset(BufferPoint::new(row, 0));
447 let line_end_byte = line_start_byte + line_text.len();
448
449 let gutter_num = if config.line_numbers {
450 let is_cursor_row = cursor_point.row == row;
451 let line_num_str = format!("{:>3}", row + 1);
452 let num_color = if is_cursor_row {
453 theme.line_number_active
454 } else {
455 theme.line_number
456 };
457
458 let shaped = window.text_system().shape_line(
459 line_num_str.into(),
460 config.font_size,
461 &[TextRun {
462 len: 3,
463 font: font.clone(),
464 color: num_color,
465 background_color: None,
466 underline: None,
467 strikethrough: None,
468 }],
469 None,
470 );
471
472 Some((point(bounds.left() + px(8.0), current_y), shaped))
473 } else {
474 None
475 };
476
477 let cursor_row = cursor_point.row;
478 let highlighter_rev = editor.highlighter_rev;
479 let cached = layout_cache.cached_input(
480 &editor.buffer,
481 editor.highlighter.as_deref(),
482 highlighter_rev,
483 cursor_row,
484 row,
485 line_text,
486 );
487 let spans = &cached.spans;
488 let concealed = &cached.concealed;
489
490 let metrics = LineMetrics::for_line(
493 line_text,
494 &concealed.display_text,
495 spans,
496 config.font_size,
497 config.line_height,
498 );
499
500 let selection_line_range = if let Some(sel) = selection {
501 let sel_range = sel.byte_range();
502 if sel_range.end > line_start_byte && sel_range.start < line_end_byte {
503 let raw_start = sel_range
504 .start
505 .saturating_sub(line_start_byte)
506 .min(line_text.len());
507 let raw_end = (sel_range.end - line_start_byte).min(line_text.len());
508 let sel_start = concealed.source_to_display(raw_start);
509 let sel_end = concealed.source_to_display(raw_end);
510 if sel_end > sel_start {
511 Some((sel_start, sel_end))
512 } else {
513 None
514 }
515 } else {
516 None
517 }
518 } else {
519 None
520 };
521
522 let has_task = metrics.task_state.is_some();
523 let is_concealed_task =
524 has_task && line_text.len() != concealed.display_text.len();
525 let is_checked_task = is_concealed_task && metrics.task_state == Some(true);
526
527 let (task_checkbox_quad, line_text_origin_x) = if is_concealed_task {
528 let checked = metrics.task_state.unwrap();
529 let indent = line_text.len() - line_text.trim_start().len();
530 let box_size = px(15.0);
531 let box_x = text_origin_x + px((indent as f32) * 8.0);
532 let box_y = current_y + (metrics.line_height - box_size) / 2.0;
533
534 if checked {
535 let quad = fill(
536 Bounds::new(point(box_x, box_y), size(box_size, box_size)),
537 theme.syntax.function,
538 )
539 .corner_radii(px(3.5))
540 .border_widths(px(1.5))
541 .border_color(theme.syntax.function);
542
543 (Some(quad), box_x + px(24.0))
544 } else {
545 let quad = fill(
546 Bounds::new(point(box_x, box_y), size(box_size, box_size)),
547 gpui::hsla(0.65, 0.4, 0.6, 0.1),
548 )
549 .corner_radii(px(3.5))
550 .border_widths(px(1.5))
551 .border_color(theme.syntax.comment);
552 (Some(quad), box_x + px(24.0))
553 }
554 } else {
555 (None, text_origin_x)
556 };
557
558 let fonts = RunFonts {
559 base: &font,
560 code: &code_font,
561 };
562 let runs = build_line_text_runs(
563 &concealed.display_text,
564 &concealed.spans,
565 selection_line_range,
566 &fonts,
567 &theme,
568 metrics.is_code_block,
569 is_checked_task,
570 );
571
572 let text_line = window
573 .text_system()
574 .shape_text(
575 concealed.display_text.clone().into(),
576 metrics.font_size,
577 &runs,
578 wrap_width.filter(|_| cached.allow_wrap),
579 None,
580 )
581 .ok()
582 .and_then(|mut l| l.pop())
583 .unwrap_or_default();
584
585 let line_visual_lines = text_line.wrap_boundaries.len() + 1;
586 let line_total_height = metrics.line_height * line_visual_lines;
587
588 #[allow(unused_mut)]
589 let mut visible_links = Vec::new();
590 for (src_range, url) in &cached.link_src {
592 let disp_start = concealed.source_to_display(src_range.start);
593 let disp_end = concealed.source_to_display(src_range.end);
594 if disp_start < disp_end {
595 let start_pt =
596 text_line.position_for_index(disp_start, metrics.line_height);
597 let end_pt =
598 text_line.position_for_index(disp_end, metrics.line_height);
599 if let (Some(s), Some(e)) = (start_pt, end_pt) {
600 let width = if e.x > s.x { e.x - s.x } else { px(20.0) };
601 visible_links.push(crate::editor::VisibleLink {
602 bounds: Bounds::new(
603 point(line_text_origin_x + s.x, current_y + s.y),
604 size(width.max(px(5.0)), metrics.line_height),
605 ),
606 url: url.clone(),
607 });
608 }
609 }
610 }
611
612 let mut search_match_quads = Vec::new();
613 for sub in wash_ranges_for_line(
614 &search_wash,
615 &mut wash_cursor,
616 line_start_byte,
617 line_end_byte,
618 ) {
619 let raw_start = sub
620 .start
621 .saturating_sub(line_start_byte)
622 .min(line_text.len());
623 let raw_end = sub.end.saturating_sub(line_start_byte).min(line_text.len());
624 let disp_start = concealed.source_to_display(raw_start);
625 let disp_end = concealed.source_to_display(raw_end);
626 if disp_end <= disp_start {
627 continue;
628 }
629 let (Some(s), Some(e)) = (
630 text_line.position_for_index(disp_start, metrics.line_height),
631 text_line.position_for_index(disp_end, metrics.line_height),
632 ) else {
633 continue;
634 };
635 let (x, width) = if s.y == e.y && e.x > s.x {
638 (s.x, e.x - s.x)
639 } else {
640 let edge =
641 (bounds.right() - px(12.0)).max(line_text_origin_x + s.x + px(4.0));
642 (s.x, edge - (line_text_origin_x + s.x))
643 };
644 search_match_quads.push(fill(
645 Bounds::new(
646 point(line_text_origin_x + x, current_y + s.y),
647 size(width.max(px(4.0)), metrics.line_height),
648 ),
649 theme.search_match,
650 ));
651 }
652
653 let quote_bar_quad = if metrics.is_quote {
654 Some(fill(
655 Bounds::new(
656 point(bounds.left() + gutter_width + px(4.0), current_y),
657 size(px(3.0), line_total_height),
658 ),
659 theme.syntax.comment,
660 ))
661 } else {
662 None
663 };
664
665 let code_block_bg_quad = if metrics.is_code_block {
666 let bg_width = (bounds.size.width - gutter_width - px(8.0)).max(px(0.0));
667 Some(fill(
668 Bounds::new(
669 point(bounds.left() + gutter_width + px(4.0), current_y),
670 size(bg_width, line_total_height),
671 ),
672 theme.syntax.code_bg,
673 ))
674 } else {
675 None
676 };
677
678 let thematic_break_quad = if metrics.is_thematic_break {
679 let width = (bounds.size.width - gutter_width - px(24.0)).max(px(0.0));
680 let line_y = current_y + metrics.line_height / 2.0;
681 Some(fill(
682 Bounds::new(point(text_origin_x, line_y), size(width, px(1.0))),
683 theme.syntax.punctuation,
684 ))
685 } else {
686 None
687 };
688
689 let cursor_quad = if cursor_point.row == row {
690 let col_in_line = cursor_offset
691 .saturating_sub(line_start_byte)
692 .min(line_text.len());
693 let col_in_display = concealed.source_to_display(col_in_line);
694 let pos = text_line
695 .position_for_index(col_in_display, metrics.line_height)
696 .unwrap_or(point(px(0.0), px(0.0)));
697
698 computed_cursor_pixel = Some(point(
699 text_origin_x + pos.x,
700 current_y + pos.y + metrics.line_height,
701 ));
702
703 let should_draw_cursor =
704 !is_all_selected && (!config.cursor_blink || editor.cursor_visible);
705
706 if should_draw_cursor {
707 let style = if config.block_cursor {
708 twrite_core::CursorStyle::Block
709 } else {
710 editor.cursor_style
711 };
712
713 match style {
714 twrite_core::CursorStyle::Hidden => None,
715 twrite_core::CursorStyle::Block => Some(fill(
716 Bounds::new(
717 point(text_origin_x + pos.x, current_y + pos.y),
718 size(px(8.5), metrics.line_height),
719 ),
720 theme.cursor,
721 )),
722 twrite_core::CursorStyle::Underline => Some(fill(
723 Bounds::new(
724 point(
725 text_origin_x + pos.x,
726 current_y + pos.y + metrics.line_height - px(2.0),
727 ),
728 size(px(8.5), px(2.0)),
729 ),
730 theme.cursor,
731 )),
732 twrite_core::CursorStyle::Bar => Some(fill(
733 Bounds::new(
734 point(text_origin_x + pos.x, current_y + pos.y),
735 size(px(2.0), metrics.line_height),
736 ),
737 theme.cursor,
738 )),
739 }
740 } else {
741 None
742 }
743 } else {
744 None
745 };
746
747 let empty_selection_quad = if line_text.is_empty() {
748 if let Some(sel) = selection {
749 let sel_range = sel.byte_range();
750 if sel_range.start <= line_start_byte && sel_range.end > line_start_byte
751 {
752 Some(fill(
753 Bounds::new(
754 point(text_origin_x, current_y),
755 size(px(6.0), metrics.line_height),
756 ),
757 theme.selection,
758 ))
759 } else {
760 None
761 }
762 } else {
763 None
764 }
765 } else {
766 None
767 };
768
769 lines.push(PreparedLine {
770 gutter_num,
771 text_origin: point(line_text_origin_x, current_y),
772 text_line,
773 line_height: metrics.line_height,
774 quote_bar_quad,
775 code_block_bg_quad,
776 thematic_break_quad,
777 empty_selection_quad,
778 cursor_quad,
779 task_checkbox_quad,
780 search_match_quads,
781 });
782
783 let checkbox_box_x = if has_task {
784 let indent = line_text.len() - line_text.trim_start().len();
785 text_origin_x + px((indent as f32) * 8.0)
786 } else {
787 px(0.0)
788 };
789
790 visible_line_layouts.push(crate::editor::VisibleLineLayout {
791 row,
792 top: current_y,
793 bottom: current_y + line_total_height,
794 line_start_byte,
795 line_len_bytes: line_text.len(),
796 text_origin_x: line_text_origin_x,
797 line_height: metrics.line_height,
798 is_task_checkbox: has_task,
799 checkbox_box_x,
800 task_state: metrics.task_state,
801 links: visible_links,
802 });
803
804 current_y += line_total_height;
805 }
806
807 editor_handle.update(cx, |editor, _| {
808 editor.layout_cache = layout_cache;
809 editor.last_cursor_pixel = computed_cursor_pixel;
810 editor.visible_lines = visible_line_layouts;
811 });
812
813 EditorCanvasPrepaint {
814 background_quad: fill(bounds, theme.background),
815 lines,
816 }
817 },
818 move |_bounds, prepaint, window, cx| {
819 window.paint_quad(prepaint.background_quad);
820
821 for line in prepaint.lines {
822 let is_break = line.thematic_break_quad.is_some();
823
824 if let Some(code_bg) = line.code_block_bg_quad {
825 window.paint_quad(code_bg);
826 }
827 if let Some(quote_bar) = line.quote_bar_quad {
828 window.paint_quad(quote_bar);
829 }
830 if let Some(thematic_break) = line.thematic_break_quad {
831 window.paint_quad(thematic_break);
832 }
833 if let Some(cb_quad) = line.task_checkbox_quad {
834 window.paint_quad(cb_quad);
835 }
836
837 if let Some((origin, shaped_num)) = line.gutter_num {
838 let _ = shaped_num.paint(origin, line.line_height, window, cx);
840 }
841
842 if !is_break {
843 let _ = line.text_line.paint_background(
844 line.text_origin,
845 line.line_height,
846 TextAlign::Left,
847 None,
848 window,
849 cx,
850 );
851 }
852
853 for quad in line.search_match_quads {
854 window.paint_quad(quad);
855 }
856
857 if let Some(empty_sel) = line.empty_selection_quad {
858 window.paint_quad(empty_sel);
859 }
860
861 if let Some(cursor_quad) = line.cursor_quad {
862 window.paint_quad(cursor_quad);
863 }
864
865 if !is_break {
866 let _ = line.text_line.paint(
867 line.text_origin,
868 line.line_height,
869 TextAlign::Left,
870 None,
871 window,
872 cx,
873 );
874 }
875 }
876 },
877 )
878 .size_full()
879 }
880}
881
882#[cfg(test)]
883mod tests {
884 use super::wash_ranges_for_line;
885 use std::ops::Range;
886
887 fn matches_vec(ranges: &[Range<usize>]) -> Vec<Range<usize>> {
888 ranges.to_vec()
889 }
890
891 #[test]
892 fn wash_ranges_clip_and_advance_linearly() {
893 let matches = matches_vec(&[0..3, 8..11, 20..25]);
894 let mut cursor = 0;
895
896 assert_eq!(
898 wash_ranges_for_line(&matches, &mut cursor, 0, 7),
899 vec![0..3]
900 );
901 assert_eq!(
903 wash_ranges_for_line(&matches, &mut cursor, 7, 15),
904 vec![8..11]
905 );
906 assert_eq!(cursor, 1);
907 assert!(wash_ranges_for_line(&matches, &mut cursor, 12, 18).is_empty());
909 assert_eq!(
910 wash_ranges_for_line(&matches, &mut cursor, 18, 30),
911 vec![20..25]
912 );
913 assert!(wash_ranges_for_line(&matches, &mut cursor, 30, 40).is_empty());
915 }
916
917 #[test]
918 fn wash_ranges_split_cross_line_matches() {
919 let matches = matches_vec(&[5..15, 30..35]);
920 let mut cursor = 0;
921 assert_eq!(
922 wash_ranges_for_line(&matches, &mut cursor, 0, 10),
923 vec![5..10]
924 );
925 assert_eq!(
926 wash_ranges_for_line(&matches, &mut cursor, 10, 20),
927 vec![10..15]
928 );
929 assert_eq!(
930 wash_ranges_for_line(&matches, &mut cursor, 20, 40),
931 vec![30..35]
932 );
933 }
934}