1use std::collections::BTreeMap;
4
5use crate::app::types::ViewLineMapping;
6use crate::app::BufferMetadata;
7use crate::model::buffer::Buffer;
8use crate::model::cursor::SelectionMode;
9use crate::model::event::{BufferId, EventLog, LeafId, SplitDirection};
10use crate::primitives::ansi::AnsiParser;
11use crate::primitives::ansi_background::AnsiBackground;
12use crate::primitives::display_width::char_width;
13use crate::state::{EditorState, ViewMode};
14use crate::view::folding::FoldManager;
15use crate::view::split::SplitManager;
16use crate::view::theme::color_to_rgb;
17use crate::view::ui::tabs::TabsRenderer;
18use crate::view::ui::view_pipeline::{
19 should_show_line_number, LineStart, ViewLine, ViewLineIterator,
20};
21use crate::view::virtual_text::VirtualTextPosition;
22use fresh_core::api::{ViewTokenStyle, ViewTransformPayload};
23use ratatui::layout::Rect;
24use ratatui::style::{Color, Modifier, Style};
25use ratatui::text::{Line, Span};
26use ratatui::widgets::{Block, Borders, Clear, Paragraph};
27use ratatui::Frame;
28use std::collections::{HashMap, HashSet};
29use std::ops::Range;
30
31const MAX_SAFE_LINE_WIDTH: usize = 10_000;
37
38fn compute_inline_diff(old_text: &str, new_text: &str) -> (Vec<Range<usize>>, Vec<Range<usize>>) {
42 let old_chars: Vec<char> = old_text.chars().collect();
43 let new_chars: Vec<char> = new_text.chars().collect();
44
45 let mut old_ranges = Vec::new();
46 let mut new_ranges = Vec::new();
47
48 let prefix_len = old_chars
50 .iter()
51 .zip(new_chars.iter())
52 .take_while(|(a, b)| a == b)
53 .count();
54
55 let old_remaining = old_chars.len() - prefix_len;
57 let new_remaining = new_chars.len() - prefix_len;
58 let suffix_len = old_chars
59 .iter()
60 .rev()
61 .zip(new_chars.iter().rev())
62 .take(old_remaining.min(new_remaining))
63 .take_while(|(a, b)| a == b)
64 .count();
65
66 let old_start = prefix_len;
68 let old_end = old_chars.len().saturating_sub(suffix_len);
69 let new_start = prefix_len;
70 let new_end = new_chars.len().saturating_sub(suffix_len);
71
72 if old_start < old_end {
73 old_ranges.push(old_start..old_end);
74 }
75 if new_start < new_end {
76 new_ranges.push(new_start..new_end);
77 }
78
79 (old_ranges, new_ranges)
80}
81
82fn push_span_with_map(
83 spans: &mut Vec<Span<'static>>,
84 map: &mut Vec<Option<usize>>,
85 text: String,
86 style: Style,
87 source: Option<usize>,
88) {
89 if text.is_empty() {
90 return;
91 }
92 for ch in text.chars() {
96 let width = char_width(ch);
97 for _ in 0..width {
98 map.push(source);
99 }
100 }
101 spans.push(Span::styled(text, style));
102}
103
104fn debug_tag_style() -> Style {
106 Style::default()
107 .fg(Color::DarkGray)
108 .add_modifier(Modifier::DIM)
109}
110
111fn fold_placeholder_style(theme: &crate::view::theme::Theme) -> ViewTokenStyle {
112 let fg = color_to_rgb(theme.line_number_fg).or_else(|| color_to_rgb(theme.editor_fg));
113 ViewTokenStyle {
114 fg,
115 bg: None,
116 bold: false,
117 italic: true,
118 }
119}
120
121fn dim_color_for_tilde(color: Color) -> Color {
124 match color {
125 Color::Rgb(r, g, b) => {
126 Color::Rgb(r / 2, g / 2, b / 2)
128 }
129 Color::Indexed(idx) => {
130 if idx < 16 {
135 Color::Rgb(50, 50, 50) } else {
137 Color::Rgb(40, 40, 40) }
139 }
140 Color::Black => Color::Rgb(15, 15, 15),
142 Color::White => Color::Rgb(128, 128, 128),
143 Color::Red => Color::Rgb(100, 30, 30),
144 Color::Green => Color::Rgb(30, 100, 30),
145 Color::Yellow => Color::Rgb(100, 100, 30),
146 Color::Blue => Color::Rgb(30, 30, 100),
147 Color::Magenta => Color::Rgb(100, 30, 100),
148 Color::Cyan => Color::Rgb(30, 100, 100),
149 Color::Gray => Color::Rgb(64, 64, 64),
150 Color::DarkGray => Color::Rgb(40, 40, 40),
151 Color::LightRed => Color::Rgb(128, 50, 50),
152 Color::LightGreen => Color::Rgb(50, 128, 50),
153 Color::LightYellow => Color::Rgb(128, 128, 50),
154 Color::LightBlue => Color::Rgb(50, 50, 128),
155 Color::LightMagenta => Color::Rgb(128, 50, 128),
156 Color::LightCyan => Color::Rgb(50, 128, 128),
157 Color::Reset => Color::Rgb(50, 50, 50),
158 }
159}
160
161struct SpanAccumulator {
166 text: String,
167 style: Style,
168 first_source: Option<usize>,
169}
170
171impl SpanAccumulator {
172 fn new() -> Self {
173 Self {
174 text: String::new(),
175 style: Style::default(),
176 first_source: None,
177 }
178 }
179
180 fn push(
183 &mut self,
184 ch: char,
185 style: Style,
186 source: Option<usize>,
187 spans: &mut Vec<Span<'static>>,
188 map: &mut Vec<Option<usize>>,
189 ) {
190 if !self.text.is_empty() && style != self.style {
192 self.flush(spans, map);
193 }
194
195 if self.text.is_empty() {
197 self.style = style;
198 self.first_source = source;
199 }
200
201 self.text.push(ch);
202
203 let width = char_width(ch);
205 for _ in 0..width {
206 map.push(source);
207 }
208 }
209
210 fn flush(&mut self, spans: &mut Vec<Span<'static>>, _map: &mut Vec<Option<usize>>) {
212 if !self.text.is_empty() {
213 spans.push(Span::styled(std::mem::take(&mut self.text), self.style));
214 self.first_source = None;
215 }
216 }
217}
218
219fn push_debug_tag(spans: &mut Vec<Span<'static>>, map: &mut Vec<Option<usize>>, text: String) {
221 if text.is_empty() {
222 return;
223 }
224 for ch in text.chars() {
226 let width = char_width(ch);
227 for _ in 0..width {
228 map.push(None);
229 }
230 }
231 spans.push(Span::styled(text, debug_tag_style()));
232}
233
234#[derive(Default)]
236struct DebugSpanTracker {
237 active_highlight: Option<Range<usize>>,
239 active_overlays: Vec<Range<usize>>,
241}
242
243impl DebugSpanTracker {
244 fn get_opening_tags(
246 &mut self,
247 byte_pos: Option<usize>,
248 highlight_spans: &[crate::primitives::highlighter::HighlightSpan],
249 viewport_overlays: &[(crate::view::overlay::Overlay, Range<usize>)],
250 ) -> Vec<String> {
251 let mut tags = Vec::new();
252
253 if let Some(bp) = byte_pos {
254 if let Some(span) = highlight_spans.iter().find(|s| s.range.start == bp) {
256 tags.push(format!("<hl:{}-{}>", span.range.start, span.range.end));
257 self.active_highlight = Some(span.range.clone());
258 }
259
260 for (overlay, range) in viewport_overlays.iter() {
262 if range.start == bp {
263 let overlay_type = match &overlay.face {
264 crate::view::overlay::OverlayFace::Underline { .. } => "ul",
265 crate::view::overlay::OverlayFace::Background { .. } => "bg",
266 crate::view::overlay::OverlayFace::Foreground { .. } => "fg",
267 crate::view::overlay::OverlayFace::Style { .. } => "st",
268 crate::view::overlay::OverlayFace::ThemedStyle { .. } => "ts",
269 };
270 tags.push(format!("<{}:{}-{}>", overlay_type, range.start, range.end));
271 self.active_overlays.push(range.clone());
272 }
273 }
274 }
275
276 tags
277 }
278
279 fn get_closing_tags(&mut self, byte_pos: Option<usize>) -> Vec<String> {
281 let mut tags = Vec::new();
282
283 if let Some(bp) = byte_pos {
284 if let Some(ref range) = self.active_highlight {
286 if bp >= range.end {
287 tags.push("</hl>".to_string());
288 self.active_highlight = None;
289 }
290 }
291
292 let mut closed_indices = Vec::new();
294 for (i, range) in self.active_overlays.iter().enumerate() {
295 if bp >= range.end {
296 tags.push("</ov>".to_string());
297 closed_indices.push(i);
298 }
299 }
300 for i in closed_indices.into_iter().rev() {
302 self.active_overlays.remove(i);
303 }
304 }
305
306 tags
307 }
308}
309
310struct ViewData {
312 lines: Vec<ViewLine>,
314}
315
316struct ViewAnchor {
317 start_line_idx: usize,
318 start_line_skip: usize,
319}
320
321struct ComposeLayout {
322 render_area: Rect,
323 left_pad: u16,
324 right_pad: u16,
325}
326
327struct SelectionContext {
328 ranges: Vec<Range<usize>>,
329 block_rects: Vec<(usize, usize, usize, usize)>,
330 cursor_positions: Vec<usize>,
331 primary_cursor_position: usize,
332}
333
334struct DecorationContext {
335 highlight_spans: Vec<crate::primitives::highlighter::HighlightSpan>,
336 semantic_token_spans: Vec<crate::primitives::highlighter::HighlightSpan>,
337 viewport_overlays: Vec<(crate::view::overlay::Overlay, Range<usize>)>,
338 virtual_text_lookup: HashMap<usize, Vec<crate::view::virtual_text::VirtualText>>,
339 diagnostic_lines: HashSet<usize>,
341 diagnostic_inline_texts: HashMap<usize, (String, Style)>,
344 line_indicators: BTreeMap<usize, crate::view::margin::LineIndicator>,
346 fold_indicators: BTreeMap<usize, FoldIndicator>,
348}
349
350#[derive(Clone, Copy, Debug)]
351struct FoldIndicator {
352 collapsed: bool,
353}
354
355struct LineRenderOutput {
356 lines: Vec<Line<'static>>,
357 cursor: Option<(u16, u16)>,
358 last_line_end: Option<LastLineEnd>,
359 content_lines_rendered: usize,
360 view_line_mappings: Vec<ViewLineMapping>,
361}
362
363#[derive(Clone, Copy, Debug, PartialEq, Eq)]
364struct LastLineEnd {
365 pos: (u16, u16),
366 terminated_with_newline: bool,
367}
368
369struct BufferLayoutOutput {
372 view_line_mappings: Vec<ViewLineMapping>,
373 render_output: LineRenderOutput,
374 render_area: Rect,
375 compose_layout: ComposeLayout,
376 effective_editor_bg: Color,
377 view_mode: ViewMode,
378 left_column: usize,
379 gutter_width: usize,
380 buffer_ends_with_newline: bool,
381 selection: SelectionContext,
382}
383
384struct SplitLayout {
385 tabs_rect: Rect,
386 content_rect: Rect,
387 scrollbar_rect: Rect,
388 horizontal_scrollbar_rect: Rect,
389}
390
391struct ViewPreferences {
392 view_mode: ViewMode,
393 compose_width: Option<u16>,
394 compose_column_guides: Option<Vec<u16>>,
395 view_transform: Option<ViewTransformPayload>,
396 rulers: Vec<usize>,
397 show_line_numbers: bool,
399 highlight_current_line: bool,
401}
402
403struct LineRenderInput<'a> {
404 state: &'a EditorState,
405 theme: &'a crate::view::theme::Theme,
406 view_lines: &'a [ViewLine],
408 view_anchor: ViewAnchor,
409 render_area: Rect,
410 gutter_width: usize,
411 selection: &'a SelectionContext,
412 decorations: &'a DecorationContext,
413 visible_line_count: usize,
414 lsp_waiting: bool,
415 is_active: bool,
416 line_wrap: bool,
417 estimated_lines: usize,
418 left_column: usize,
420 relative_line_numbers: bool,
422 session_mode: bool,
424 software_cursor_only: bool,
426 show_line_numbers: bool,
428 byte_offset_mode: bool,
431 show_tilde: bool,
433 highlight_current_line: bool,
435 cell_theme_map: &'a mut Vec<crate::app::types::CellThemeInfo>,
437 screen_width: u16,
439}
440
441struct CharStyleContext<'a> {
443 byte_pos: Option<usize>,
444 token_style: Option<&'a fresh_core::api::ViewTokenStyle>,
445 ansi_style: Style,
446 is_cursor: bool,
447 is_selected: bool,
448 theme: &'a crate::view::theme::Theme,
449 highlight_color: Option<Color>,
451 highlight_theme_key: Option<&'static str>,
453 semantic_token_color: Option<Color>,
455 viewport_overlays: &'a [(crate::view::overlay::Overlay, Range<usize>)],
456 primary_cursor_position: usize,
457 is_active: bool,
458 skip_primary_cursor_reverse: bool,
463 is_cursor_line_highlighted: bool,
465 current_line_bg: Color,
467}
468
469struct CharStyleOutput {
471 style: Style,
472 is_secondary_cursor: bool,
473 fg_theme_key: Option<&'static str>,
475 bg_theme_key: Option<&'static str>,
477 region: &'static str,
479}
480
481struct LeftMarginContext<'a> {
483 state: &'a EditorState,
484 theme: &'a crate::view::theme::Theme,
485 is_continuation: bool,
486 line_start_byte: Option<usize>,
488 gutter_num: usize,
490 estimated_lines: usize,
491 diagnostic_lines: &'a HashSet<usize>,
492 line_indicators: &'a BTreeMap<usize, crate::view::margin::LineIndicator>,
494 fold_indicators: &'a BTreeMap<usize, FoldIndicator>,
496 cursor_line_start_byte: usize,
498 cursor_line_number: usize,
500 relative_line_numbers: bool,
502 show_line_numbers: bool,
504 byte_offset_mode: bool,
506 highlight_current_line: bool,
508 is_active: bool,
510}
511
512fn inline_diagnostic_style(priority: i32, theme: &crate::view::theme::Theme) -> Style {
515 match priority {
516 100 => Style::default().fg(theme.diagnostic_error_fg),
517 50 => Style::default().fg(theme.diagnostic_warning_fg),
518 30 => Style::default().fg(theme.diagnostic_info_fg),
519 _ => Style::default().fg(theme.diagnostic_hint_fg),
520 }
521}
522
523fn render_left_margin(
525 ctx: &LeftMarginContext,
526 line_spans: &mut Vec<Span<'static>>,
527 line_view_map: &mut Vec<Option<usize>>,
528) {
529 if !ctx.state.margins.left_config.enabled {
530 return;
531 }
532
533 let lookup_key = ctx.line_start_byte;
534 let indicator_is_cursor_line = lookup_key.is_some_and(|k| k == ctx.cursor_line_start_byte);
536 let indicator_bg = if indicator_is_cursor_line && ctx.highlight_current_line && ctx.is_active {
537 Some(ctx.theme.current_line_bg)
538 } else {
539 None
540 };
541
542 if ctx.is_continuation {
544 let mut style = Style::default();
545 if let Some(bg) = indicator_bg {
546 style = style.bg(bg);
547 }
548 push_span_with_map(line_spans, line_view_map, " ".to_string(), style, None);
549 } else if lookup_key.is_some_and(|k| ctx.diagnostic_lines.contains(&k)) {
550 let mut style = Style::default().fg(ratatui::style::Color::Red);
552 if let Some(bg) = indicator_bg {
553 style = style.bg(bg);
554 }
555 push_span_with_map(line_spans, line_view_map, "●".to_string(), style, None);
556 } else if lookup_key.is_some_and(|k| {
557 ctx.fold_indicators.contains_key(&k) && !ctx.line_indicators.contains_key(&k)
558 }) {
559 let fold = ctx.fold_indicators.get(&lookup_key.unwrap()).unwrap();
561 let symbol = if fold.collapsed { "▸" } else { "▾" };
562 let mut style = Style::default().fg(ctx.theme.line_number_fg);
563 if let Some(bg) = indicator_bg {
564 style = style.bg(bg);
565 }
566 push_span_with_map(line_spans, line_view_map, symbol.to_string(), style, None);
567 } else if let Some(indicator) = lookup_key.and_then(|k| ctx.line_indicators.get(&k)) {
568 let mut style = Style::default().fg(indicator.color);
570 if let Some(bg) = indicator_bg {
571 style = style.bg(bg);
572 }
573 push_span_with_map(
574 line_spans,
575 line_view_map,
576 indicator.symbol.clone(),
577 style,
578 None,
579 );
580 } else {
581 let mut style = Style::default();
583 if let Some(bg) = indicator_bg {
584 style = style.bg(bg);
585 }
586 push_span_with_map(line_spans, line_view_map, " ".to_string(), style, None);
587 }
588
589 let is_cursor_line = lookup_key.is_some_and(|k| k == ctx.cursor_line_start_byte);
590 let use_cursor_line_bg = is_cursor_line && ctx.highlight_current_line && ctx.is_active;
591
592 if ctx.is_continuation {
594 let blank = " ".repeat(ctx.state.margins.left_config.width);
596 let mut style = Style::default().fg(ctx.theme.line_number_fg);
597 if use_cursor_line_bg {
598 style = style.bg(ctx.theme.current_line_bg);
599 }
600 push_span_with_map(line_spans, line_view_map, blank, style, None);
601 } else if ctx.byte_offset_mode && ctx.show_line_numbers {
602 let rendered_text = format!(
604 "{:>width$}",
605 ctx.gutter_num,
606 width = ctx.state.margins.left_config.width
607 );
608 let mut margin_style = if is_cursor_line {
609 Style::default().fg(ctx.theme.editor_fg)
610 } else {
611 Style::default().fg(ctx.theme.line_number_fg)
612 };
613 if use_cursor_line_bg {
614 margin_style = margin_style.bg(ctx.theme.current_line_bg);
615 }
616 push_span_with_map(line_spans, line_view_map, rendered_text, margin_style, None);
617 } else if ctx.relative_line_numbers {
618 let display_num = if is_cursor_line {
620 ctx.gutter_num + 1
622 } else {
623 ctx.gutter_num.abs_diff(ctx.cursor_line_number)
625 };
626 let rendered_text = format!(
627 "{:>width$}",
628 display_num,
629 width = ctx.state.margins.left_config.width
630 );
631 let mut margin_style = if is_cursor_line {
633 Style::default().fg(ctx.theme.editor_fg)
634 } else {
635 Style::default().fg(ctx.theme.line_number_fg)
636 };
637 if use_cursor_line_bg {
638 margin_style = margin_style.bg(ctx.theme.current_line_bg);
639 }
640 push_span_with_map(line_spans, line_view_map, rendered_text, margin_style, None);
641 } else {
642 let margin_content = ctx.state.margins.render_line(
643 ctx.gutter_num,
644 crate::view::margin::MarginPosition::Left,
645 ctx.estimated_lines,
646 ctx.show_line_numbers,
647 );
648 let (rendered_text, style_opt) = margin_content.render(ctx.state.margins.left_config.width);
649
650 let mut margin_style =
652 style_opt.unwrap_or_else(|| Style::default().fg(ctx.theme.line_number_fg));
653 if use_cursor_line_bg {
654 margin_style = margin_style.bg(ctx.theme.current_line_bg);
655 }
656
657 push_span_with_map(line_spans, line_view_map, rendered_text, margin_style, None);
658 }
659
660 if ctx.state.margins.left_config.show_separator {
662 let mut separator_style = Style::default().fg(ctx.theme.line_number_fg);
663 if use_cursor_line_bg {
664 separator_style = separator_style.bg(ctx.theme.current_line_bg);
665 }
666 push_span_with_map(
667 line_spans,
668 line_view_map,
669 ctx.state.margins.left_config.separator.clone(),
670 separator_style,
671 None,
672 );
673 }
674}
675
676#[inline]
680fn span_color_at(
681 spans: &[crate::primitives::highlighter::HighlightSpan],
682 cursor: &mut usize,
683 byte_pos: usize,
684) -> Option<Color> {
685 while *cursor < spans.len() {
686 let span = &spans[*cursor];
687 if span.range.end <= byte_pos {
688 *cursor += 1;
689 } else if span.range.start > byte_pos {
690 return None;
691 } else {
692 return Some(span.color);
693 }
694 }
695 None
696}
697
698fn span_info_at(
700 spans: &[crate::primitives::highlighter::HighlightSpan],
701 cursor: &mut usize,
702 byte_pos: usize,
703) -> (Option<Color>, Option<&'static str>, Option<&'static str>) {
704 while *cursor < spans.len() {
705 let span = &spans[*cursor];
706 if span.range.end <= byte_pos {
707 *cursor += 1;
708 } else if span.range.start > byte_pos {
709 return (None, None, None);
710 } else {
711 let theme_key = span.category.as_ref().map(|c| c.theme_key());
712 let display_name = span.category.as_ref().map(|c| c.display_name());
713 return (Some(span.color), theme_key, display_name);
714 }
715 }
716 (None, None, None)
717}
718
719fn compute_char_style(ctx: &CharStyleContext) -> CharStyleOutput {
722 use crate::view::overlay::OverlayFace;
723
724 let highlight_color = ctx.highlight_color;
725
726 let mut fg_theme_key: Option<&'static str> = None;
728 let mut bg_theme_key: Option<&'static str> = Some("editor.bg");
729 let mut region: &'static str = "Editor Content";
730
731 let overlays: Vec<&crate::view::overlay::Overlay> = if let Some(bp) = ctx.byte_pos {
733 ctx.viewport_overlays
734 .iter()
735 .filter(|(_, range)| range.contains(&bp))
736 .map(|(overlay, _)| overlay)
737 .collect()
738 } else {
739 Vec::new()
740 };
741
742 let mut style = if let Some(ts) = ctx.token_style {
745 let mut s = Style::default();
746 if let Some((r, g, b)) = ts.fg {
747 s = s.fg(ratatui::style::Color::Rgb(r, g, b));
748 } else {
749 s = s.fg(ctx.theme.editor_fg);
750 fg_theme_key = Some("editor.fg");
751 }
752 if let Some((r, g, b)) = ts.bg {
753 s = s.bg(ratatui::style::Color::Rgb(r, g, b));
754 }
755 if ts.bold {
756 s = s.add_modifier(Modifier::BOLD);
757 }
758 if ts.italic {
759 s = s.add_modifier(Modifier::ITALIC);
760 }
761 region = "Plugin Token";
762 s
763 } else if ctx.ansi_style.fg.is_some()
764 || ctx.ansi_style.bg.is_some()
765 || !ctx.ansi_style.add_modifier.is_empty()
766 {
767 let mut s = Style::default();
769 if let Some(fg) = ctx.ansi_style.fg {
770 s = s.fg(fg);
771 } else {
772 s = s.fg(ctx.theme.editor_fg);
773 fg_theme_key = Some("editor.fg");
774 }
775 if let Some(bg) = ctx.ansi_style.bg {
776 s = s.bg(bg);
777 bg_theme_key = None; }
779 s = s.add_modifier(ctx.ansi_style.add_modifier);
780 region = "ANSI Escape";
781 s
782 } else if let Some(color) = highlight_color {
783 fg_theme_key = ctx.highlight_theme_key;
785 Style::default().fg(color)
786 } else {
787 fg_theme_key = Some("editor.fg");
789 Style::default().fg(ctx.theme.editor_fg)
790 };
791
792 if let Some(color) = highlight_color {
795 if ctx.ansi_style.fg.is_none()
796 && (ctx.ansi_style.bg.is_some() || !ctx.ansi_style.add_modifier.is_empty())
797 {
798 style = style.fg(color);
799 fg_theme_key = ctx.highlight_theme_key;
800 }
801 }
802
803 if ctx.token_style.is_none() {
808 if let Some(color) = ctx.semantic_token_color {
809 style = style.fg(color);
810 }
813 }
814
815 for overlay in &overlays {
817 match &overlay.face {
818 OverlayFace::Underline {
819 color,
820 style: _underline_style,
821 } => {
822 style = style.add_modifier(Modifier::UNDERLINED).fg(*color);
823 if let Some(key) = overlay.theme_key {
824 fg_theme_key = Some(key);
825 }
826 }
827 OverlayFace::Background { color } => {
828 style = style.bg(*color);
829 if let Some(key) = overlay.theme_key {
830 bg_theme_key = Some(key);
831 }
832 }
833 OverlayFace::Foreground { color } => {
834 style = style.fg(*color);
835 if let Some(key) = overlay.theme_key {
836 fg_theme_key = Some(key);
837 }
838 }
839 OverlayFace::Style {
840 style: overlay_style,
841 } => {
842 style = style.patch(*overlay_style);
843 if let Some(key) = overlay.theme_key {
845 if overlay_style.bg.is_some() {
846 bg_theme_key = Some(key);
847 }
848 if overlay_style.fg.is_some() {
849 fg_theme_key = Some(key);
850 }
851 }
852 }
853 OverlayFace::ThemedStyle {
854 fallback_style,
855 fg_theme,
856 bg_theme,
857 } => {
858 let mut themed_style = *fallback_style;
859 if let Some(fg_key) = fg_theme {
860 if let Some(color) = ctx.theme.resolve_theme_key(fg_key) {
861 themed_style = themed_style.fg(color);
862 }
863 }
864 if let Some(bg_key) = bg_theme {
865 if let Some(color) = ctx.theme.resolve_theme_key(bg_key) {
866 themed_style = themed_style.bg(color);
867 }
868 }
869 style = style.patch(themed_style);
870 }
872 }
873 }
874
875 if ctx.is_cursor_line_highlighted && !ctx.is_selected && style.bg.is_none() {
877 style = style.bg(ctx.current_line_bg);
878 }
879
880 if ctx.is_selected {
882 style = style.bg(ctx.theme.selection_bg);
883 bg_theme_key = Some("editor.selection_bg");
884 region = "Selection";
885 }
886
887 let is_secondary_cursor = ctx.is_cursor && ctx.byte_pos != Some(ctx.primary_cursor_position);
892 if ctx.is_active {
893 if ctx.is_cursor {
894 if ctx.skip_primary_cursor_reverse {
895 if is_secondary_cursor {
896 style = style.add_modifier(Modifier::REVERSED);
897 }
898 } else {
899 style = style.add_modifier(Modifier::REVERSED);
900 }
901 region = "Cursor";
902 }
903 } else if ctx.is_cursor {
904 style = style.fg(ctx.theme.editor_fg).bg(ctx.theme.inactive_cursor);
905 fg_theme_key = Some("editor.fg");
906 bg_theme_key = Some("editor.inactive_cursor");
907 region = "Inactive Cursor";
908 }
909
910 CharStyleOutput {
911 style,
912 is_secondary_cursor,
913 fg_theme_key,
914 bg_theme_key,
915 region,
916 }
917}
918
919pub struct SplitRenderer;
921
922impl SplitRenderer {
923 #[allow(clippy::too_many_arguments)]
942 #[allow(clippy::type_complexity)]
943 pub fn render_content(
944 frame: &mut Frame,
945 area: Rect,
946 split_manager: &SplitManager,
947 buffers: &mut HashMap<BufferId, EditorState>,
948 buffer_metadata: &HashMap<BufferId, BufferMetadata>,
949 event_logs: &mut HashMap<BufferId, EventLog>,
950 composite_buffers: &mut HashMap<BufferId, crate::model::composite_buffer::CompositeBuffer>,
951 composite_view_states: &mut HashMap<
952 (LeafId, BufferId),
953 crate::view::composite_view::CompositeViewState,
954 >,
955 theme: &crate::view::theme::Theme,
956 ansi_background: Option<&AnsiBackground>,
957 background_fade: f32,
958 lsp_waiting: bool,
959 large_file_threshold_bytes: u64,
960 _line_wrap: bool,
961 estimated_line_length: usize,
962 highlight_context_bytes: usize,
963 mut split_view_states: Option<&mut HashMap<LeafId, crate::view::split::SplitViewState>>,
964 grouped_subtrees: &HashMap<LeafId, crate::view::split::SplitNode>,
965 hide_cursor: bool,
966 hovered_tab: Option<(crate::view::split::TabTarget, LeafId, bool)>, hovered_close_split: Option<LeafId>,
968 hovered_maximize_split: Option<LeafId>,
969 is_maximized: bool,
970 relative_line_numbers: bool,
971 tab_bar_visible: bool,
972 use_terminal_bg: bool,
973 session_mode: bool,
974 software_cursor_only: bool,
975 show_vertical_scrollbar: bool,
976 show_horizontal_scrollbar: bool,
977 diagnostics_inline_text: bool,
978 show_tilde: bool,
979 cell_theme_map: &mut Vec<crate::app::types::CellThemeInfo>,
980 screen_width: u16,
981 ) -> (
982 Vec<(LeafId, BufferId, Rect, Rect, usize, usize)>,
983 HashMap<LeafId, crate::view::ui::tabs::TabLayout>, Vec<(LeafId, u16, u16, u16)>, Vec<(LeafId, u16, u16, u16)>, HashMap<LeafId, Vec<ViewLineMapping>>, Vec<(LeafId, BufferId, Rect, usize, usize, usize)>, Vec<(
989 crate::model::event::ContainerId,
990 SplitDirection,
991 u16,
992 u16,
993 u16,
994 )>, ) {
996 let _span = tracing::trace_span!("render_content").entered();
997
998 #[derive(Copy, Clone, PartialEq, Eq)]
1012 enum RenderKind {
1013 Normal,
1014 GroupTabBarOnly,
1015 InnerLeaf,
1016 }
1017
1018 let base_visible = split_manager.get_visible_buffers(area);
1019 let active_split_id = split_manager.active_split();
1020 let has_multiple_splits = base_visible.len() > 1;
1021
1022 let mut visible_buffers: Vec<(LeafId, LeafId, BufferId, Rect, RenderKind)> = Vec::new();
1026 for (main_split_id, main_buffer_id, split_area) in &base_visible {
1027 let active_group = split_view_states
1028 .as_deref()
1029 .and_then(|svs| svs.get(main_split_id))
1030 .and_then(|vs| vs.active_group_tab);
1031
1032 if let Some(group_leaf) = active_group {
1033 if let Some(grouped) = grouped_subtrees.get(&group_leaf) {
1034 let split_tab_bar_visible = tab_bar_visible
1036 && !split_view_states
1037 .as_deref()
1038 .and_then(|svs| svs.get(main_split_id))
1039 .is_some_and(|vs| vs.suppress_chrome);
1040 let main_layout = Self::split_layout(
1041 *split_area,
1042 split_tab_bar_visible,
1043 show_vertical_scrollbar,
1044 show_horizontal_scrollbar,
1045 );
1046 let inner_leaves = grouped.get_leaves_with_rects(main_layout.content_rect);
1047 visible_buffers.push((
1048 *main_split_id,
1049 *main_split_id,
1050 *main_buffer_id,
1051 *split_area,
1052 RenderKind::GroupTabBarOnly,
1053 ));
1054 for (inner_leaf, inner_buffer, inner_rect) in &inner_leaves {
1055 if let Some(svs) = split_view_states.as_deref_mut() {
1061 if let Some(vs) = svs.get_mut(inner_leaf) {
1062 vs.viewport.resize(inner_rect.width, inner_rect.height);
1063 }
1064 }
1065 visible_buffers.push((
1066 *main_split_id,
1067 *inner_leaf,
1068 *inner_buffer,
1069 *inner_rect,
1070 RenderKind::InnerLeaf,
1071 ));
1072 }
1073 continue;
1074 }
1075 }
1076
1077 visible_buffers.push((
1078 *main_split_id,
1079 *main_split_id,
1080 *main_buffer_id,
1081 *split_area,
1082 RenderKind::Normal,
1083 ));
1084 }
1085
1086 let mut split_areas = Vec::new();
1088 let mut horizontal_scrollbar_areas: Vec<(LeafId, BufferId, Rect, usize, usize, usize)> =
1089 Vec::new();
1090 let mut tab_layouts: HashMap<LeafId, crate::view::ui::tabs::TabLayout> = HashMap::new();
1091 let mut close_split_areas = Vec::new();
1092 let mut maximize_split_areas = Vec::new();
1093 let mut view_line_mappings: HashMap<LeafId, Vec<ViewLineMapping>> = HashMap::new();
1094
1095 for (main_split_id, split_id, buffer_id, split_area, kind) in visible_buffers {
1097 let is_active = split_id == active_split_id;
1098 let is_inner_group_leaf = kind == RenderKind::InnerLeaf;
1099 let skip_content = kind == RenderKind::GroupTabBarOnly;
1100 let _ = main_split_id; let split_tab_bar_visible = !is_inner_group_leaf
1104 && tab_bar_visible
1105 && !split_view_states
1106 .as_deref()
1107 .and_then(|svs| svs.get(&split_id))
1108 .is_some_and(|vs| vs.suppress_chrome);
1109 let split_show_tilde = show_tilde
1111 && !split_view_states
1112 .as_deref()
1113 .and_then(|svs| svs.get(&split_id))
1114 .is_some_and(|vs| vs.hide_tilde);
1115
1116 let layout = if is_inner_group_leaf {
1117 SplitLayout {
1119 tabs_rect: Rect::new(split_area.x, split_area.y, 0, 0),
1120 content_rect: Rect::new(
1121 split_area.x,
1122 split_area.y,
1123 split_area.width.saturating_sub(if show_vertical_scrollbar {
1124 1
1125 } else {
1126 0
1127 }),
1128 split_area.height,
1129 ),
1130 scrollbar_rect: Rect::new(
1131 split_area.x + split_area.width.saturating_sub(1),
1132 split_area.y,
1133 if show_vertical_scrollbar { 1 } else { 0 },
1134 split_area.height,
1135 ),
1136 horizontal_scrollbar_rect: Rect::new(0, 0, 0, 0),
1137 }
1138 } else {
1139 Self::split_layout(
1140 split_area,
1141 split_tab_bar_visible,
1142 show_vertical_scrollbar,
1143 show_horizontal_scrollbar,
1144 )
1145 };
1146 let (split_buffers, tab_scroll_offset) = if is_inner_group_leaf {
1147 (Vec::new(), 0)
1148 } else {
1149 Self::split_buffers_for_tabs(split_view_states.as_deref(), split_id, buffer_id)
1150 };
1151
1152 let tab_hover_for_split = hovered_tab.and_then(|(hover_buf, hover_split, is_close)| {
1154 if hover_split == split_id {
1155 Some((hover_buf, is_close))
1156 } else {
1157 None
1158 }
1159 });
1160
1161 if split_tab_bar_visible {
1163 let active_target = split_view_states
1168 .as_deref()
1169 .and_then(|svs| svs.get(&split_id))
1170 .map(|vs| vs.active_target())
1171 .unwrap_or(crate::view::split::TabTarget::Buffer(buffer_id));
1172 let group_names: HashMap<LeafId, String> = grouped_subtrees
1174 .iter()
1175 .filter_map(|(leaf_id, node)| {
1176 if let crate::view::split::SplitNode::Grouped { name, .. } = node {
1177 Some((*leaf_id, name.clone()))
1178 } else {
1179 None
1180 }
1181 })
1182 .collect();
1183 let tab_layout = TabsRenderer::render_for_split(
1185 frame,
1186 layout.tabs_rect,
1187 &split_buffers,
1188 buffers,
1189 buffer_metadata,
1190 composite_buffers,
1191 active_target,
1192 theme,
1193 is_active,
1194 tab_scroll_offset,
1195 tab_hover_for_split,
1196 &group_names,
1197 );
1198
1199 tab_layouts.insert(split_id, tab_layout);
1201 let tab_row = layout.tabs_rect.y;
1202
1203 let show_maximize_btn = has_multiple_splits || is_maximized;
1207 let show_close_btn = has_multiple_splits && !is_maximized;
1208
1209 if show_maximize_btn || show_close_btn {
1210 let mut btn_x = layout.tabs_rect.x + layout.tabs_rect.width.saturating_sub(2);
1213
1214 if show_close_btn {
1216 let is_hovered = hovered_close_split == Some(split_id);
1217 let close_fg = if is_hovered {
1218 theme.tab_close_hover_fg
1219 } else {
1220 theme.line_number_fg
1221 };
1222 let close_button = Paragraph::new("×")
1223 .style(Style::default().fg(close_fg).bg(theme.tab_separator_bg));
1224 let close_area = Rect::new(btn_x, tab_row, 1, 1);
1225 frame.render_widget(close_button, close_area);
1226 close_split_areas.push((split_id, tab_row, btn_x, btn_x + 1));
1227 btn_x = btn_x.saturating_sub(2); }
1229
1230 if show_maximize_btn {
1232 let is_hovered = hovered_maximize_split == Some(split_id);
1233 let max_fg = if is_hovered {
1234 theme.tab_close_hover_fg
1235 } else {
1236 theme.line_number_fg
1237 };
1238 let icon = if is_maximized { "⧉" } else { "□" };
1240 let max_button = Paragraph::new(icon)
1241 .style(Style::default().fg(max_fg).bg(theme.tab_separator_bg));
1242 let max_area = Rect::new(btn_x, tab_row, 1, 1);
1243 frame.render_widget(max_button, max_area);
1244 maximize_split_areas.push((split_id, tab_row, btn_x, btn_x + 1));
1245 }
1246 }
1247 }
1248
1249 if skip_content {
1253 view_line_mappings.insert(split_id, Vec::new());
1254 continue;
1255 }
1256
1257 let state_opt = buffers.get_mut(&buffer_id);
1259 let event_log_opt = event_logs.get_mut(&buffer_id);
1260
1261 if let Some(state) = state_opt {
1262 if state.is_composite_buffer {
1264 let initial_focus_hunk = composite_buffers
1266 .get_mut(&buffer_id)
1267 .and_then(|c| c.initial_focus_hunk.take());
1268 if let Some(composite) = composite_buffers.get(&buffer_id) {
1269 if let Some(ref mut svs) = split_view_states {
1272 if let Some(split_vs) = svs.get_mut(&split_id) {
1273 if split_vs.viewport.width != layout.content_rect.width
1274 || split_vs.viewport.height != layout.content_rect.height
1275 {
1276 split_vs.viewport.resize(
1277 layout.content_rect.width,
1278 layout.content_rect.height,
1279 );
1280 }
1281 }
1282 }
1283
1284 let pane_count = composite.pane_count();
1286 let view_state = composite_view_states
1287 .entry((split_id, buffer_id))
1288 .or_insert_with(|| {
1289 crate::view::composite_view::CompositeViewState::new(
1290 buffer_id, pane_count,
1291 )
1292 });
1293
1294 if let Some(hunk_index) = initial_focus_hunk {
1298 let mut target_row = None;
1299 let mut hunk_count = 0usize;
1301 for (row_idx, row) in composite.alignment.rows.iter().enumerate() {
1302 if row.row_type
1303 == crate::model::composite_buffer::RowType::HunkHeader
1304 {
1305 if hunk_count == hunk_index {
1306 target_row = Some(row_idx);
1307 break;
1308 }
1309 hunk_count += 1;
1310 }
1311 }
1312 if let Some(row) = target_row {
1313 let viewport_height =
1314 layout.content_rect.height.saturating_sub(1) as usize;
1315 let context_above = viewport_height / 3;
1316 view_state.cursor_row = row;
1317 view_state.scroll_row = row.saturating_sub(context_above);
1318 }
1319 }
1320
1321 Self::render_composite_buffer(
1323 frame,
1324 layout.content_rect,
1325 composite,
1326 buffers,
1327 theme,
1328 is_active,
1329 view_state,
1330 use_terminal_bg,
1331 split_show_tilde,
1332 );
1333
1334 let total_rows = composite.row_count();
1336 let content_height = layout.content_rect.height.saturating_sub(1) as usize; let (thumb_start, thumb_end) = if show_vertical_scrollbar {
1338 Self::render_composite_scrollbar(
1339 frame,
1340 layout.scrollbar_rect,
1341 total_rows,
1342 view_state.scroll_row,
1343 content_height,
1344 is_active,
1345 )
1346 } else {
1347 (0, 0)
1348 };
1349
1350 split_areas.push((
1352 split_id,
1353 buffer_id,
1354 layout.content_rect,
1355 layout.scrollbar_rect,
1356 thumb_start,
1357 thumb_end,
1358 ));
1359 if show_horizontal_scrollbar {
1360 horizontal_scrollbar_areas.push((
1361 split_id,
1362 buffer_id,
1363 layout.horizontal_scrollbar_rect,
1364 0, 0,
1366 0,
1367 ));
1368 }
1369 }
1370 view_line_mappings.insert(split_id, Vec::new());
1371 continue;
1372 }
1373
1374 let view_state_opt = split_view_states
1378 .as_deref()
1379 .and_then(|vs| vs.get(&split_id));
1380 let viewport_clone =
1381 view_state_opt
1382 .map(|vs| vs.viewport.clone())
1383 .unwrap_or_else(|| {
1384 crate::view::viewport::Viewport::new(
1385 layout.content_rect.width,
1386 layout.content_rect.height,
1387 )
1388 });
1389 let mut viewport = viewport_clone;
1390
1391 let split_cursors = split_view_states
1393 .as_deref()
1394 .and_then(|vs| vs.get(&split_id))
1395 .map(|vs| vs.cursors.clone())
1396 .unwrap_or_default();
1397 let hidden_ranges: Vec<(usize, usize)> = split_view_states
1400 .as_deref()
1401 .and_then(|vs| vs.get(&split_id))
1402 .map(|vs| {
1403 vs.folds
1404 .resolved_ranges(&state.buffer, &state.marker_list)
1405 .into_iter()
1406 .map(|r| (r.start_byte, r.end_byte))
1407 .collect()
1408 })
1409 .unwrap_or_default();
1410
1411 {
1412 let _span = tracing::trace_span!("sync_viewport_to_content").entered();
1413 Self::sync_viewport_to_content(
1414 &mut viewport,
1415 &mut state.buffer,
1416 &split_cursors,
1417 layout.content_rect,
1418 &hidden_ranges,
1419 );
1420 }
1421 let view_prefs =
1422 Self::resolve_view_preferences(state, split_view_states.as_deref(), split_id);
1423
1424 let effective_highlight_current_line =
1427 view_prefs.highlight_current_line && state.show_cursors;
1428
1429 let mut empty_folds = FoldManager::new();
1430 let folds = split_view_states
1431 .as_deref_mut()
1432 .and_then(|vs| vs.get_mut(&split_id))
1433 .map(|vs| &mut vs.folds)
1434 .unwrap_or(&mut empty_folds);
1435
1436 let _render_buf_span = tracing::trace_span!("render_buffer_in_split").entered();
1437 let split_view_mappings = Self::render_buffer_in_split(
1438 frame,
1439 state,
1440 &split_cursors,
1441 &mut viewport,
1442 folds,
1443 event_log_opt,
1444 layout.content_rect,
1445 is_active,
1446 theme,
1447 ansi_background,
1448 background_fade,
1449 lsp_waiting,
1450 view_prefs.view_mode,
1451 view_prefs.compose_width,
1452 view_prefs.compose_column_guides,
1453 view_prefs.view_transform,
1454 estimated_line_length,
1455 highlight_context_bytes,
1456 buffer_id,
1457 hide_cursor,
1458 relative_line_numbers,
1459 use_terminal_bg,
1460 session_mode,
1461 software_cursor_only,
1462 &view_prefs.rulers,
1463 view_prefs.show_line_numbers,
1464 effective_highlight_current_line,
1465 diagnostics_inline_text,
1466 split_show_tilde,
1467 cell_theme_map,
1468 screen_width,
1469 );
1470
1471 drop(_render_buf_span);
1472
1473 view_line_mappings.insert(split_id, split_view_mappings);
1475
1476 let buffer_len = state.buffer.len();
1479 let (total_lines, top_line) = {
1480 let _span = tracing::trace_span!("scrollbar_line_counts").entered();
1481 Self::scrollbar_line_counts(
1482 state,
1483 &viewport,
1484 large_file_threshold_bytes,
1485 buffer_len,
1486 )
1487 };
1488
1489 let (thumb_start, thumb_end) = if show_vertical_scrollbar {
1491 Self::render_scrollbar(
1492 frame,
1493 state,
1494 &viewport,
1495 layout.scrollbar_rect,
1496 is_active,
1497 theme,
1498 large_file_threshold_bytes,
1499 total_lines,
1500 top_line,
1501 )
1502 } else {
1503 (0, 0)
1504 };
1505
1506 let max_content_width = if show_horizontal_scrollbar && !viewport.line_wrap_enabled
1508 {
1509 let mcw = Self::compute_max_line_length(state, &mut viewport);
1510 let visible_width = viewport.width as usize;
1512 let max_scroll = mcw.saturating_sub(visible_width);
1513 if viewport.left_column > max_scroll {
1514 viewport.left_column = max_scroll;
1515 }
1516 mcw
1517 } else {
1518 0
1519 };
1520
1521 let (hthumb_start, hthumb_end) = if show_horizontal_scrollbar {
1523 Self::render_horizontal_scrollbar(
1524 frame,
1525 &viewport,
1526 layout.horizontal_scrollbar_rect,
1527 is_active,
1528 max_content_width,
1529 )
1530 } else {
1531 (0, 0)
1532 };
1533
1534 if let Some(view_states) = split_view_states.as_deref_mut() {
1539 if let Some(view_state) = view_states.get_mut(&split_id) {
1540 tracing::trace!(
1541 "Writing back viewport: top_byte={}, skip_ensure_visible={}",
1542 viewport.top_byte,
1543 viewport.should_skip_ensure_visible()
1544 );
1545 view_state.viewport = viewport.clone();
1546 }
1547 }
1548
1549 split_areas.push((
1551 split_id,
1552 buffer_id,
1553 layout.content_rect,
1554 layout.scrollbar_rect,
1555 thumb_start,
1556 thumb_end,
1557 ));
1558 if show_horizontal_scrollbar {
1559 horizontal_scrollbar_areas.push((
1560 split_id,
1561 buffer_id,
1562 layout.horizontal_scrollbar_rect,
1563 max_content_width,
1564 hthumb_start,
1565 hthumb_end,
1566 ));
1567 }
1568 }
1569 }
1570
1571 let separators = split_manager.get_separators(area);
1574 for (direction, x, y, length) in separators {
1575 Self::render_separator(frame, direction, x, y, length, theme);
1576 }
1577 let mut grouped_separator_areas: Vec<(
1583 crate::model::event::ContainerId,
1584 SplitDirection,
1585 u16,
1586 u16,
1587 u16,
1588 )> = Vec::new();
1589 for (main_split_id, _main_buffer_id, split_area) in &base_visible {
1590 let active_group = split_view_states
1591 .as_deref()
1592 .and_then(|svs| svs.get(main_split_id))
1593 .and_then(|vs| vs.active_group_tab);
1594 if let Some(group_leaf) = active_group {
1595 if let Some(grouped) = grouped_subtrees.get(&group_leaf) {
1596 let split_tab_bar_visible = tab_bar_visible
1597 && !split_view_states
1598 .as_deref()
1599 .and_then(|svs| svs.get(main_split_id))
1600 .is_some_and(|vs| vs.suppress_chrome);
1601 let main_layout = Self::split_layout(
1602 *split_area,
1603 split_tab_bar_visible,
1604 show_vertical_scrollbar,
1605 show_horizontal_scrollbar,
1606 );
1607 if let crate::view::split::SplitNode::Grouped { layout, .. } = grouped {
1608 for (id, direction, x, y, length) in
1609 layout.get_separators_with_ids(main_layout.content_rect)
1610 {
1611 Self::render_separator(frame, direction, x, y, length, theme);
1612 grouped_separator_areas.push((id, direction, x, y, length));
1613 }
1614 }
1615 }
1616 }
1617 }
1618
1619 (
1620 split_areas,
1621 tab_layouts,
1622 close_split_areas,
1623 maximize_split_areas,
1624 view_line_mappings,
1625 horizontal_scrollbar_areas,
1626 grouped_separator_areas,
1627 )
1628 }
1629
1630 #[allow(clippy::too_many_arguments)]
1634 pub fn compute_content_layout(
1635 area: Rect,
1636 split_manager: &SplitManager,
1637 buffers: &mut HashMap<BufferId, EditorState>,
1638 split_view_states: &mut HashMap<LeafId, crate::view::split::SplitViewState>,
1639 theme: &crate::view::theme::Theme,
1640 lsp_waiting: bool,
1641 estimated_line_length: usize,
1642 highlight_context_bytes: usize,
1643 relative_line_numbers: bool,
1644 use_terminal_bg: bool,
1645 session_mode: bool,
1646 software_cursor_only: bool,
1647 tab_bar_visible: bool,
1648 show_vertical_scrollbar: bool,
1649 show_horizontal_scrollbar: bool,
1650 diagnostics_inline_text: bool,
1651 show_tilde: bool,
1652 ) -> HashMap<LeafId, Vec<ViewLineMapping>> {
1653 let visible_buffers = split_manager.get_visible_buffers(area);
1654 let active_split_id = split_manager.active_split();
1655 let mut view_line_mappings: HashMap<LeafId, Vec<ViewLineMapping>> = HashMap::new();
1656
1657 for (split_id, buffer_id, split_area) in visible_buffers {
1658 let is_active = split_id == active_split_id;
1659
1660 let split_tab_bar_visible = tab_bar_visible
1662 && !split_view_states
1663 .get(&split_id)
1664 .map_or(false, |vs| vs.suppress_chrome);
1665
1666 let layout = Self::split_layout(
1667 split_area,
1668 split_tab_bar_visible,
1669 show_vertical_scrollbar,
1670 show_horizontal_scrollbar,
1671 );
1672
1673 let state = match buffers.get_mut(&buffer_id) {
1674 Some(s) => s,
1675 None => continue,
1676 };
1677
1678 if state.is_composite_buffer {
1680 view_line_mappings.insert(split_id, Vec::new());
1681 continue;
1682 }
1683
1684 let viewport_clone = split_view_states
1686 .get(&split_id)
1687 .map(|vs| vs.viewport.clone())
1688 .unwrap_or_else(|| {
1689 crate::view::viewport::Viewport::new(
1690 layout.content_rect.width,
1691 layout.content_rect.height,
1692 )
1693 });
1694 let mut viewport = viewport_clone;
1695
1696 let split_cursors = split_view_states
1698 .get(&split_id)
1699 .map(|vs| vs.cursors.clone())
1700 .unwrap_or_default();
1701 let hidden_ranges: Vec<(usize, usize)> = split_view_states
1704 .get(&split_id)
1705 .map(|vs| {
1706 vs.folds
1707 .resolved_ranges(&state.buffer, &state.marker_list)
1708 .into_iter()
1709 .map(|r| (r.start_byte, r.end_byte))
1710 .collect()
1711 })
1712 .unwrap_or_default();
1713
1714 Self::sync_viewport_to_content(
1715 &mut viewport,
1716 &mut state.buffer,
1717 &split_cursors,
1718 layout.content_rect,
1719 &hidden_ranges,
1720 );
1721 let view_prefs =
1722 Self::resolve_view_preferences(state, Some(&*split_view_states), split_id);
1723
1724 let effective_highlight_current_line =
1725 view_prefs.highlight_current_line && state.show_cursors;
1726
1727 let mut empty_folds = FoldManager::new();
1728 let folds = split_view_states
1729 .get_mut(&split_id)
1730 .map(|vs| &mut vs.folds)
1731 .unwrap_or(&mut empty_folds);
1732
1733 let layout_output = Self::compute_buffer_layout(
1734 state,
1735 &split_cursors,
1736 &mut viewport,
1737 folds,
1738 layout.content_rect,
1739 is_active,
1740 theme,
1741 lsp_waiting,
1742 view_prefs.view_mode,
1743 view_prefs.compose_width,
1744 view_prefs.view_transform,
1745 estimated_line_length,
1746 highlight_context_bytes,
1747 relative_line_numbers,
1748 use_terminal_bg,
1749 session_mode,
1750 software_cursor_only,
1751 view_prefs.show_line_numbers,
1752 effective_highlight_current_line,
1753 diagnostics_inline_text,
1754 show_tilde,
1755 None, );
1757
1758 view_line_mappings.insert(split_id, layout_output.view_line_mappings);
1759
1760 if let Some(view_state) = split_view_states.get_mut(&split_id) {
1762 view_state.viewport = viewport;
1763 }
1764 }
1765
1766 view_line_mappings
1767 }
1768
1769 fn render_separator(
1771 frame: &mut Frame,
1772 direction: SplitDirection,
1773 x: u16,
1774 y: u16,
1775 length: u16,
1776 theme: &crate::view::theme::Theme,
1777 ) {
1778 match direction {
1779 SplitDirection::Horizontal => {
1780 let line_area = Rect::new(x, y, length, 1);
1782 let line_text = "─".repeat(length as usize);
1783 let paragraph =
1784 Paragraph::new(line_text).style(Style::default().fg(theme.split_separator_fg));
1785 frame.render_widget(paragraph, line_area);
1786 }
1787 SplitDirection::Vertical => {
1788 for offset in 0..length {
1790 let cell_area = Rect::new(x, y + offset, 1, 1);
1791 let paragraph =
1792 Paragraph::new("│").style(Style::default().fg(theme.split_separator_fg));
1793 frame.render_widget(paragraph, cell_area);
1794 }
1795 }
1796 }
1797 }
1798
1799 #[allow(clippy::too_many_arguments)]
1802 fn render_composite_buffer(
1803 frame: &mut Frame,
1804 area: Rect,
1805 composite: &crate::model::composite_buffer::CompositeBuffer,
1806 buffers: &mut HashMap<BufferId, EditorState>,
1807 theme: &crate::view::theme::Theme,
1808 _is_active: bool,
1809 view_state: &mut crate::view::composite_view::CompositeViewState,
1810 use_terminal_bg: bool,
1811 show_tilde: bool,
1812 ) {
1813 use crate::model::composite_buffer::{CompositeLayout, RowType};
1814
1815 let effective_editor_bg = if use_terminal_bg {
1817 ratatui::style::Color::Reset
1818 } else {
1819 theme.editor_bg
1820 };
1821
1822 let scroll_row = view_state.scroll_row;
1823 let cursor_row = view_state.cursor_row;
1824
1825 frame.render_widget(Clear, area);
1827
1828 let pane_count = composite.sources.len();
1830 if pane_count == 0 {
1831 return;
1832 }
1833
1834 let show_separator = match &composite.layout {
1836 CompositeLayout::SideBySide { show_separator, .. } => *show_separator,
1837 _ => false,
1838 };
1839
1840 let separator_width = if show_separator { 1 } else { 0 };
1842 let total_separators = (pane_count.saturating_sub(1)) as u16 * separator_width;
1843 let available_width = area.width.saturating_sub(total_separators);
1844
1845 let pane_widths: Vec<u16> = match &composite.layout {
1846 CompositeLayout::SideBySide { ratios, .. } => {
1847 let default_ratio = 1.0 / pane_count as f32;
1848 ratios
1849 .iter()
1850 .chain(std::iter::repeat(&default_ratio))
1851 .take(pane_count)
1852 .map(|r| (available_width as f32 * r).round() as u16)
1853 .collect()
1854 }
1855 _ => {
1856 let pane_width = available_width / pane_count as u16;
1858 vec![pane_width; pane_count]
1859 }
1860 };
1861
1862 view_state.pane_widths = pane_widths.clone();
1864
1865 let header_height = 1u16;
1867 let mut x_offset = area.x;
1868 for (idx, (source, &width)) in composite.sources.iter().zip(&pane_widths).enumerate() {
1869 let header_area = Rect::new(x_offset, area.y, width, header_height);
1870 let is_focused = idx == view_state.focused_pane;
1871
1872 let header_style = if is_focused {
1873 Style::default()
1874 .fg(theme.tab_active_fg)
1875 .bg(theme.tab_active_bg)
1876 } else {
1877 Style::default()
1878 .fg(theme.tab_inactive_fg)
1879 .bg(theme.tab_inactive_bg)
1880 };
1881
1882 let header_text = format!(" {} ", source.label);
1883 let header = Paragraph::new(header_text).style(header_style);
1884 frame.render_widget(header, header_area);
1885
1886 x_offset += width + separator_width;
1887 }
1888
1889 let content_y = area.y + header_height;
1891 let content_height = area.height.saturating_sub(header_height);
1892 let visible_rows = content_height as usize;
1893
1894 let alignment = &composite.alignment;
1896 let total_rows = alignment.rows.len();
1897
1898 struct PaneRenderData {
1901 lines: Vec<ViewLine>,
1902 line_to_view_line: HashMap<usize, usize>,
1903 highlight_spans: Vec<crate::primitives::highlighter::HighlightSpan>,
1904 }
1905
1906 let mut pane_render_data: Vec<Option<PaneRenderData>> = Vec::new();
1907
1908 for (pane_idx, source) in composite.sources.iter().enumerate() {
1909 if let Some(source_state) = buffers.get_mut(&source.buffer_id) {
1910 let visible_lines: Vec<usize> = alignment
1912 .rows
1913 .iter()
1914 .skip(scroll_row)
1915 .take(visible_rows)
1916 .filter_map(|row| row.get_pane_line(pane_idx))
1917 .map(|r| r.line)
1918 .collect();
1919
1920 let first_line = visible_lines.iter().copied().min();
1921 let last_line = visible_lines.iter().copied().max();
1922
1923 if let (Some(first_line), Some(last_line)) = (first_line, last_line) {
1924 let top_byte = source_state
1926 .buffer
1927 .line_start_offset(first_line)
1928 .unwrap_or(0);
1929 let end_byte = source_state
1930 .buffer
1931 .line_start_offset(last_line + 1)
1932 .unwrap_or(source_state.buffer.len());
1933
1934 let highlight_spans = source_state.highlighter.highlight_viewport(
1936 &source_state.buffer,
1937 top_byte,
1938 end_byte,
1939 theme,
1940 1024, );
1942
1943 let pane_width = pane_widths.get(pane_idx).copied().unwrap_or(80);
1945 let mut viewport =
1946 crate::view::viewport::Viewport::new(pane_width, content_height);
1947 viewport.top_byte = top_byte;
1948 viewport.line_wrap_enabled = false;
1949
1950 let pane_width = pane_widths.get(pane_idx).copied().unwrap_or(80) as usize;
1951 let gutter_width = 4; let content_width = pane_width.saturating_sub(gutter_width);
1953
1954 let lines_needed = last_line - first_line + 10;
1957 let empty_folds = FoldManager::new();
1958 let view_data = Self::build_view_data(
1959 source_state,
1960 &viewport,
1961 None, 80, lines_needed, false, content_width,
1966 gutter_width,
1967 &ViewMode::Source, &empty_folds,
1969 theme,
1970 );
1971
1972 let mut line_to_view_line: HashMap<usize, usize> = HashMap::new();
1974 let mut current_line = first_line;
1975 for (idx, view_line) in view_data.lines.iter().enumerate() {
1976 if should_show_line_number(view_line) {
1977 line_to_view_line.insert(current_line, idx);
1978 current_line += 1;
1979 }
1980 }
1981
1982 pane_render_data.push(Some(PaneRenderData {
1983 lines: view_data.lines,
1984 line_to_view_line,
1985 highlight_spans,
1986 }));
1987 } else {
1988 pane_render_data.push(None);
1989 }
1990 } else {
1991 pane_render_data.push(None);
1992 }
1993 }
1994
1995 for view_row in 0..visible_rows {
1997 let display_row = scroll_row + view_row;
1998 if display_row >= total_rows {
1999 if show_tilde {
2000 let mut x = area.x;
2002 for &width in &pane_widths {
2003 let tilde_area = Rect::new(x, content_y + view_row as u16, width, 1);
2004 let tilde =
2005 Paragraph::new("~").style(Style::default().fg(theme.line_number_fg));
2006 frame.render_widget(tilde, tilde_area);
2007 x += width + separator_width;
2008 }
2009 }
2010 continue;
2011 }
2012
2013 let aligned_row = &alignment.rows[display_row];
2014 let is_cursor_row = display_row == cursor_row;
2015 let selection_cols = view_state.selection_column_range(display_row);
2017
2018 let row_bg = match aligned_row.row_type {
2020 RowType::Addition => Some(theme.diff_add_bg),
2021 RowType::Deletion => Some(theme.diff_remove_bg),
2022 RowType::Modification => Some(theme.diff_modify_bg),
2023 RowType::HunkHeader => Some(theme.current_line_bg),
2024 RowType::Context => None,
2025 };
2026
2027 let inline_diffs: Vec<Vec<Range<usize>>> = if aligned_row.row_type
2029 == RowType::Modification
2030 {
2031 let mut line_contents: Vec<Option<String>> = Vec::new();
2033 for (pane_idx, source) in composite.sources.iter().enumerate() {
2034 if let Some(line_ref) = aligned_row.get_pane_line(pane_idx) {
2035 if let Some(source_state) = buffers.get(&source.buffer_id) {
2036 line_contents.push(
2037 source_state
2038 .buffer
2039 .get_line(line_ref.line)
2040 .map(|line| String::from_utf8_lossy(&line).to_string()),
2041 );
2042 } else {
2043 line_contents.push(None);
2044 }
2045 } else {
2046 line_contents.push(None);
2047 }
2048 }
2049
2050 if line_contents.len() >= 2 {
2052 if let (Some(old_text), Some(new_text)) = (&line_contents[0], &line_contents[1])
2053 {
2054 let (old_ranges, new_ranges) = compute_inline_diff(old_text, new_text);
2055 vec![old_ranges, new_ranges]
2056 } else {
2057 vec![Vec::new(); composite.sources.len()]
2058 }
2059 } else {
2060 vec![Vec::new(); composite.sources.len()]
2061 }
2062 } else {
2063 vec![Vec::new(); composite.sources.len()]
2065 };
2066
2067 let mut x_offset = area.x;
2069 for (pane_idx, (_source, &width)) in
2070 composite.sources.iter().zip(&pane_widths).enumerate()
2071 {
2072 let pane_area = Rect::new(x_offset, content_y + view_row as u16, width, 1);
2073
2074 let left_column = view_state
2076 .get_pane_viewport(pane_idx)
2077 .map(|v| v.left_column)
2078 .unwrap_or(0);
2079
2080 let source_line_opt = aligned_row.get_pane_line(pane_idx);
2082
2083 if let Some(source_line_ref) = source_line_opt {
2084 let pane_data = pane_render_data.get(pane_idx).and_then(|opt| opt.as_ref());
2086 let view_line_opt = pane_data.and_then(|data| {
2087 data.line_to_view_line
2088 .get(&source_line_ref.line)
2089 .and_then(|&idx| data.lines.get(idx))
2090 });
2091 let highlight_spans = pane_data
2092 .map(|data| data.highlight_spans.as_slice())
2093 .unwrap_or(&[]);
2094
2095 let gutter_width = 4usize;
2096 let max_content_width = width.saturating_sub(gutter_width as u16) as usize;
2097
2098 let is_focused_pane = pane_idx == view_state.focused_pane;
2099
2100 let bg = if is_cursor_row && is_focused_pane {
2103 theme.current_line_bg
2104 } else {
2105 row_bg.unwrap_or(effective_editor_bg)
2106 };
2107
2108 let pane_selection_cols = if is_focused_pane {
2110 selection_cols
2111 } else {
2112 None
2113 };
2114
2115 let line_num = format!("{:>3} ", source_line_ref.line + 1);
2117 let line_num_style = Style::default().fg(theme.line_number_fg).bg(bg);
2118
2119 let is_cursor_pane = is_focused_pane;
2120 let cursor_column = view_state.cursor_column;
2121
2122 let inline_ranges = inline_diffs.get(pane_idx).cloned().unwrap_or_default();
2124
2125 let highlight_bg = match aligned_row.row_type {
2127 RowType::Deletion => Some(theme.diff_remove_highlight_bg),
2128 RowType::Addition => Some(theme.diff_add_highlight_bg),
2129 RowType::Modification => {
2130 if pane_idx == 0 {
2131 Some(theme.diff_remove_highlight_bg)
2132 } else {
2133 Some(theme.diff_add_highlight_bg)
2134 }
2135 }
2136 _ => None,
2137 };
2138
2139 let mut spans = vec![Span::styled(line_num, line_num_style)];
2141
2142 if let Some(view_line) = view_line_opt {
2143 Self::render_view_line_content(
2145 &mut spans,
2146 view_line,
2147 highlight_spans,
2148 left_column,
2149 max_content_width,
2150 bg,
2151 theme,
2152 is_cursor_row && is_cursor_pane,
2153 cursor_column,
2154 &inline_ranges,
2155 highlight_bg,
2156 pane_selection_cols,
2157 );
2158 } else {
2159 tracing::warn!(
2165 "ViewLine missing for composite buffer: pane={}, line={}, pane_data={}",
2166 pane_idx,
2167 source_line_ref.line,
2168 pane_data.is_some()
2169 );
2170 let base_style = Style::default().fg(theme.editor_fg).bg(bg);
2172 let padding = " ".repeat(max_content_width);
2173 spans.push(Span::styled(padding, base_style));
2174 }
2175
2176 let line = Line::from(spans);
2177 let para = Paragraph::new(line);
2178 frame.render_widget(para, pane_area);
2179 } else {
2180 let is_focused_pane = pane_idx == view_state.focused_pane;
2182 let pane_has_selection = is_focused_pane
2184 && selection_cols
2185 .map(|(start, end)| start == 0 && end == usize::MAX)
2186 .unwrap_or(false);
2187
2188 let bg = if pane_has_selection {
2189 theme.selection_bg
2190 } else if is_cursor_row && is_focused_pane {
2191 theme.current_line_bg
2192 } else {
2193 row_bg.unwrap_or(effective_editor_bg)
2194 };
2195 let style = Style::default().fg(theme.line_number_fg).bg(bg);
2196
2197 let is_cursor_pane = pane_idx == view_state.focused_pane;
2199 if is_cursor_row && is_cursor_pane && view_state.cursor_column == 0 {
2200 let cursor_style = Style::default().fg(theme.editor_bg).bg(theme.editor_fg);
2202 let gutter_width = 4usize;
2203 let max_content_width = width.saturating_sub(gutter_width as u16) as usize;
2204 let padding = " ".repeat(max_content_width.saturating_sub(1));
2205 let line = Line::from(vec![
2206 Span::styled(" ", style),
2207 Span::styled(" ", cursor_style),
2208 Span::styled(padding, Style::default().bg(bg)),
2209 ]);
2210 let para = Paragraph::new(line);
2211 frame.render_widget(para, pane_area);
2212 } else {
2213 let gap_style = Style::default().bg(bg);
2215 let empty_content = " ".repeat(width as usize);
2216 let para = Paragraph::new(empty_content).style(gap_style);
2217 frame.render_widget(para, pane_area);
2218 }
2219 }
2220
2221 x_offset += width;
2222
2223 if show_separator && pane_idx < pane_count - 1 {
2225 let sep_area =
2226 Rect::new(x_offset, content_y + view_row as u16, separator_width, 1);
2227 let sep =
2228 Paragraph::new("│").style(Style::default().fg(theme.split_separator_fg));
2229 frame.render_widget(sep, sep_area);
2230 x_offset += separator_width;
2231 }
2232 }
2233 }
2234 }
2235
2236 #[allow(clippy::too_many_arguments)]
2238 fn render_view_line_content(
2239 spans: &mut Vec<Span<'static>>,
2240 view_line: &ViewLine,
2241 highlight_spans: &[crate::primitives::highlighter::HighlightSpan],
2242 left_column: usize,
2243 max_width: usize,
2244 bg: Color,
2245 theme: &crate::view::theme::Theme,
2246 show_cursor: bool,
2247 cursor_column: usize,
2248 inline_ranges: &[Range<usize>],
2249 highlight_bg: Option<Color>,
2250 selection_cols: Option<(usize, usize)>, ) {
2252 let text = &view_line.text;
2253 let char_source_bytes = &view_line.char_source_bytes;
2254
2255 let chars: Vec<char> = text.chars().collect();
2257 let mut col = 0usize;
2258 let mut rendered = 0usize;
2259 let mut current_span_text = String::new();
2260 let mut current_style: Option<Style> = None;
2261 let mut hl_cursor = 0usize;
2262
2263 for (char_idx, ch) in chars.iter().enumerate() {
2264 let char_width = char_width(*ch);
2265
2266 if col < left_column {
2268 col += char_width;
2269 continue;
2270 }
2271
2272 if rendered >= max_width {
2274 break;
2275 }
2276
2277 let byte_pos = char_source_bytes.get(char_idx).and_then(|b| *b);
2279
2280 let highlight_color =
2282 byte_pos.and_then(|bp| span_color_at(highlight_spans, &mut hl_cursor, bp));
2283
2284 let in_inline_range = inline_ranges.iter().any(|r| r.contains(&char_idx));
2286
2287 let in_selection = selection_cols
2289 .map(|(start, end)| col >= start && col < end)
2290 .unwrap_or(false);
2291
2292 let char_bg = if in_selection {
2294 theme.selection_bg
2295 } else if in_inline_range {
2296 highlight_bg.unwrap_or(bg)
2297 } else {
2298 bg
2299 };
2300
2301 let char_style = if let Some(color) = highlight_color {
2303 Style::default().fg(color).bg(char_bg)
2304 } else {
2305 Style::default().fg(theme.editor_fg).bg(char_bg)
2306 };
2307
2308 let final_style = if show_cursor && col == cursor_column {
2310 Style::default().fg(theme.editor_bg).bg(theme.editor_fg)
2312 } else {
2313 char_style
2314 };
2315
2316 if let Some(style) = current_style {
2318 if style != final_style && !current_span_text.is_empty() {
2319 spans.push(Span::styled(std::mem::take(&mut current_span_text), style));
2320 }
2321 }
2322
2323 current_style = Some(final_style);
2324 current_span_text.push(*ch);
2325 col += char_width;
2326 rendered += char_width;
2327 }
2328
2329 if !current_span_text.is_empty() {
2331 if let Some(style) = current_style {
2332 spans.push(Span::styled(current_span_text, style));
2333 }
2334 }
2335
2336 if rendered < max_width {
2338 let padding_len = max_width - rendered;
2339 let cursor_visual = cursor_column.saturating_sub(left_column);
2341
2342 if show_cursor && cursor_visual >= rendered && cursor_visual < max_width {
2344 let cursor_offset = cursor_visual - rendered;
2346 let cursor_style = Style::default().fg(theme.editor_bg).bg(theme.editor_fg);
2347 let normal_style = Style::default().bg(bg);
2348
2349 if cursor_offset > 0 {
2351 spans.push(Span::styled(" ".repeat(cursor_offset), normal_style));
2352 }
2353 spans.push(Span::styled(" ", cursor_style));
2355 let remaining = padding_len.saturating_sub(cursor_offset + 1);
2357 if remaining > 0 {
2358 spans.push(Span::styled(" ".repeat(remaining), normal_style));
2359 }
2360 } else {
2361 spans.push(Span::styled(
2363 " ".repeat(padding_len),
2364 Style::default().bg(bg),
2365 ));
2366 }
2367 }
2368 }
2369
2370 fn render_composite_scrollbar(
2372 frame: &mut Frame,
2373 scrollbar_rect: Rect,
2374 total_rows: usize,
2375 scroll_row: usize,
2376 viewport_height: usize,
2377 is_active: bool,
2378 ) -> (usize, usize) {
2379 let height = scrollbar_rect.height as usize;
2380 if height == 0 || total_rows == 0 {
2381 return (0, 0);
2382 }
2383
2384 let thumb_size_raw = if total_rows > 0 {
2386 ((viewport_height as f64 / total_rows as f64) * height as f64).ceil() as usize
2387 } else {
2388 1
2389 };
2390
2391 let max_scroll = total_rows.saturating_sub(viewport_height);
2393
2394 let thumb_size = if max_scroll == 0 {
2396 height
2397 } else {
2398 let max_thumb_size = (height as f64 * 0.8).floor() as usize;
2400 thumb_size_raw.max(1).min(max_thumb_size).min(height)
2401 };
2402
2403 let thumb_start = if max_scroll > 0 {
2405 let scroll_ratio = scroll_row.min(max_scroll) as f64 / max_scroll as f64;
2406 let max_thumb_start = height.saturating_sub(thumb_size);
2407 (scroll_ratio * max_thumb_start as f64) as usize
2408 } else {
2409 0
2410 };
2411
2412 let thumb_end = thumb_start + thumb_size;
2413
2414 let track_color = if is_active {
2416 Color::DarkGray
2417 } else {
2418 Color::Black
2419 };
2420 let thumb_color = if is_active {
2421 Color::Gray
2422 } else {
2423 Color::DarkGray
2424 };
2425
2426 for row in 0..height {
2428 let cell_area = Rect::new(scrollbar_rect.x, scrollbar_rect.y + row as u16, 1, 1);
2429
2430 let style = if row >= thumb_start && row < thumb_end {
2431 Style::default().bg(thumb_color)
2432 } else {
2433 Style::default().bg(track_color)
2434 };
2435
2436 let paragraph = Paragraph::new(" ").style(style);
2437 frame.render_widget(paragraph, cell_area);
2438 }
2439
2440 (thumb_start, thumb_end)
2441 }
2442
2443 fn split_layout(
2444 split_area: Rect,
2445 tab_bar_visible: bool,
2446 show_vertical_scrollbar: bool,
2447 show_horizontal_scrollbar: bool,
2448 ) -> SplitLayout {
2449 let tabs_height = if tab_bar_visible { 1u16 } else { 0u16 };
2450 let scrollbar_width = if show_vertical_scrollbar { 1u16 } else { 0u16 };
2451 let hscrollbar_height = if show_horizontal_scrollbar {
2452 1u16
2453 } else {
2454 0u16
2455 };
2456
2457 let tabs_rect = Rect::new(split_area.x, split_area.y, split_area.width, tabs_height);
2458 let content_rect = Rect::new(
2459 split_area.x,
2460 split_area.y + tabs_height,
2461 split_area.width.saturating_sub(scrollbar_width),
2462 split_area
2463 .height
2464 .saturating_sub(tabs_height)
2465 .saturating_sub(hscrollbar_height),
2466 );
2467 let scrollbar_rect = Rect::new(
2468 split_area.x + split_area.width.saturating_sub(scrollbar_width),
2469 split_area.y + tabs_height,
2470 scrollbar_width,
2471 split_area
2472 .height
2473 .saturating_sub(tabs_height)
2474 .saturating_sub(hscrollbar_height),
2475 );
2476 let horizontal_scrollbar_rect = Rect::new(
2477 split_area.x,
2478 split_area.y + split_area.height.saturating_sub(hscrollbar_height),
2479 split_area.width.saturating_sub(scrollbar_width),
2480 hscrollbar_height,
2481 );
2482
2483 SplitLayout {
2484 tabs_rect,
2485 content_rect,
2486 scrollbar_rect,
2487 horizontal_scrollbar_rect,
2488 }
2489 }
2490
2491 fn split_buffers_for_tabs(
2492 split_view_states: Option<&HashMap<LeafId, crate::view::split::SplitViewState>>,
2493 split_id: LeafId,
2494 buffer_id: BufferId,
2495 ) -> (Vec<crate::view::split::TabTarget>, usize) {
2496 if let Some(view_states) = split_view_states {
2497 if let Some(view_state) = view_states.get(&split_id) {
2498 return (
2499 view_state.open_buffers.clone(),
2500 view_state.tab_scroll_offset,
2501 );
2502 }
2503 }
2504 (vec![crate::view::split::TabTarget::Buffer(buffer_id)], 0)
2505 }
2506
2507 fn sync_viewport_to_content(
2508 viewport: &mut crate::view::viewport::Viewport,
2509 buffer: &mut crate::model::buffer::Buffer,
2510 cursors: &crate::model::cursor::Cursors,
2511 content_rect: Rect,
2512 hidden_ranges: &[(usize, usize)],
2513 ) {
2514 let size_changed =
2515 viewport.width != content_rect.width || viewport.height != content_rect.height;
2516
2517 if size_changed {
2518 viewport.resize(content_rect.width, content_rect.height);
2519 }
2520
2521 let primary = *cursors.primary();
2526 viewport.ensure_visible(buffer, &primary, hidden_ranges);
2527 }
2528
2529 fn resolve_view_preferences(
2530 _state: &EditorState,
2531 split_view_states: Option<&HashMap<LeafId, crate::view::split::SplitViewState>>,
2532 split_id: LeafId,
2533 ) -> ViewPreferences {
2534 if let Some(view_states) = split_view_states {
2535 if let Some(view_state) = view_states.get(&split_id) {
2536 return ViewPreferences {
2537 view_mode: view_state.view_mode.clone(),
2538 compose_width: view_state.compose_width,
2539 compose_column_guides: view_state.compose_column_guides.clone(),
2540 view_transform: view_state.view_transform.clone(),
2541 rulers: view_state.rulers.clone(),
2542 show_line_numbers: view_state.show_line_numbers,
2543 highlight_current_line: view_state.highlight_current_line,
2544 };
2545 }
2546 }
2547
2548 ViewPreferences {
2550 view_mode: ViewMode::Source,
2551 compose_width: None,
2552 compose_column_guides: None,
2553 view_transform: None,
2554 rulers: Vec::new(),
2555 show_line_numbers: true,
2556 highlight_current_line: true,
2557 }
2558 }
2559
2560 fn scrollbar_line_counts(
2561 state: &mut EditorState,
2562 viewport: &crate::view::viewport::Viewport,
2563 large_file_threshold_bytes: u64,
2564 buffer_len: usize,
2565 ) -> (usize, usize) {
2566 if buffer_len > large_file_threshold_bytes as usize {
2567 return (0, 0);
2568 }
2569
2570 if viewport.line_wrap_enabled {
2572 return Self::scrollbar_visual_row_counts(state, viewport, buffer_len);
2573 }
2574
2575 let total_lines = if buffer_len > 0 {
2576 state.buffer.get_line_number(buffer_len.saturating_sub(1)) + 1
2577 } else {
2578 1
2579 };
2580
2581 let top_line = if viewport.top_byte < buffer_len {
2582 state.buffer.get_line_number(viewport.top_byte)
2583 } else {
2584 0
2585 };
2586
2587 (total_lines, top_line)
2588 }
2589
2590 fn scrollbar_visual_row_counts(
2598 state: &mut EditorState,
2599 viewport: &crate::view::viewport::Viewport,
2600 buffer_len: usize,
2601 ) -> (usize, usize) {
2602 use crate::primitives::line_wrapping::{wrap_line, WrapConfig};
2603
2604 if buffer_len == 0 {
2605 return (1, 0);
2606 }
2607
2608 let buf_version = state.buffer.version();
2609 let cache = &state.scrollbar_row_cache;
2610
2611 let cache_fully_valid = cache.valid
2613 && cache.buffer_version == buf_version
2614 && cache.viewport_width == viewport.width
2615 && cache.wrap_indent == viewport.wrap_indent
2616 && cache.top_byte == viewport.top_byte
2617 && cache.top_view_line_offset == viewport.top_view_line_offset;
2618
2619 if cache_fully_valid {
2620 return (cache.total_visual_rows, cache.top_visual_row);
2621 }
2622
2623 let total_rows_valid = cache.valid
2625 && cache.buffer_version == buf_version
2626 && cache.viewport_width == viewport.width
2627 && cache.wrap_indent == viewport.wrap_indent;
2628
2629 let gutter_width = viewport.gutter_width(&state.buffer);
2630 let wrap_config = WrapConfig::new(
2631 viewport.width as usize,
2632 gutter_width,
2633 true,
2634 viewport.wrap_indent,
2635 );
2636
2637 let line_count = state
2638 .buffer
2639 .line_count()
2640 .unwrap_or_else(|| (buffer_len / state.buffer.estimated_line_length()).max(1));
2641
2642 let mut total_visual_rows = 0;
2643 let mut top_visual_row = 0;
2644 let mut found_top = false;
2645
2646 if total_rows_valid {
2647 total_visual_rows = cache.total_visual_rows;
2649 for line_idx in 0..line_count {
2650 let line_start = state
2651 .buffer
2652 .line_start_offset(line_idx)
2653 .unwrap_or(buffer_len);
2654
2655 if line_start >= viewport.top_byte {
2656 top_visual_row = total_visual_rows.min(
2657 0,
2660 );
2661 break;
2664 }
2665 }
2666 let mut rows_before_top = 0;
2668 for line_idx in 0..line_count {
2669 let line_start = state
2670 .buffer
2671 .line_start_offset(line_idx)
2672 .unwrap_or(buffer_len);
2673
2674 if line_start >= viewport.top_byte {
2675 top_visual_row = rows_before_top + viewport.top_view_line_offset;
2676 found_top = true;
2677 break;
2678 }
2679
2680 let line_content = if let Some(bytes) = state.buffer.get_line(line_idx) {
2681 String::from_utf8_lossy(&bytes)
2682 .trim_end_matches('\n')
2683 .trim_end_matches('\r')
2684 .to_string()
2685 } else {
2686 break;
2687 };
2688
2689 let segments = wrap_line(&line_content, &wrap_config);
2690 rows_before_top += segments.len().max(1);
2691 }
2692
2693 if !found_top {
2694 top_visual_row = total_visual_rows.saturating_sub(1);
2695 }
2696 } else {
2697 for line_idx in 0..line_count {
2699 let line_start = state
2700 .buffer
2701 .line_start_offset(line_idx)
2702 .unwrap_or(buffer_len);
2703
2704 if !found_top && line_start >= viewport.top_byte {
2705 top_visual_row = total_visual_rows + viewport.top_view_line_offset;
2706 found_top = true;
2707 }
2708
2709 let line_content = if let Some(bytes) = state.buffer.get_line(line_idx) {
2710 String::from_utf8_lossy(&bytes)
2711 .trim_end_matches('\n')
2712 .trim_end_matches('\r')
2713 .to_string()
2714 } else {
2715 break;
2716 };
2717
2718 let segments = wrap_line(&line_content, &wrap_config);
2719 total_visual_rows += segments.len().max(1);
2720 }
2721
2722 if !found_top {
2723 top_visual_row = total_visual_rows.saturating_sub(1);
2724 }
2725
2726 total_visual_rows = total_visual_rows.max(1);
2727 }
2728
2729 state.scrollbar_row_cache = crate::state::ScrollbarRowCache {
2731 buffer_version: buf_version,
2732 viewport_width: viewport.width,
2733 wrap_indent: viewport.wrap_indent,
2734 total_visual_rows,
2735 top_byte: viewport.top_byte,
2736 top_visual_row,
2737 top_view_line_offset: viewport.top_view_line_offset,
2738 valid: true,
2739 };
2740
2741 (total_visual_rows, top_visual_row)
2742 }
2743
2744 #[allow(clippy::too_many_arguments)]
2747 fn render_scrollbar(
2748 frame: &mut Frame,
2749 state: &EditorState,
2750 viewport: &crate::view::viewport::Viewport,
2751 scrollbar_rect: Rect,
2752 is_active: bool,
2753 _theme: &crate::view::theme::Theme,
2754 large_file_threshold_bytes: u64,
2755 total_lines: usize,
2756 top_line: usize,
2757 ) -> (usize, usize) {
2758 let height = scrollbar_rect.height as usize;
2759 if height == 0 {
2760 return (0, 0);
2761 }
2762
2763 let buffer_len = state.buffer.len();
2764 let viewport_top = viewport.top_byte;
2765 let viewport_height_lines = height;
2771
2772 let (thumb_start, thumb_size) = if buffer_len > large_file_threshold_bytes as usize {
2774 let thumb_start = if buffer_len > 0 {
2776 ((viewport_top as f64 / buffer_len as f64) * height as f64) as usize
2777 } else {
2778 0
2779 };
2780 (thumb_start, 1)
2781 } else {
2782 let thumb_size_raw = if total_lines > 0 {
2787 ((viewport_height_lines as f64 / total_lines as f64) * height as f64).ceil()
2788 as usize
2789 } else {
2790 1
2791 };
2792
2793 let max_scroll_line = total_lines.saturating_sub(viewport_height_lines);
2797
2798 let thumb_size = if max_scroll_line == 0 {
2801 height
2802 } else {
2803 let max_thumb_size = (height as f64 * 0.8).floor() as usize;
2805 thumb_size_raw.max(1).min(max_thumb_size).min(height)
2806 };
2807
2808 let thumb_start = if max_scroll_line > 0 {
2812 let scroll_ratio = top_line.min(max_scroll_line) as f64 / max_scroll_line as f64;
2814 let max_thumb_start = height.saturating_sub(thumb_size);
2815 (scroll_ratio * max_thumb_start as f64) as usize
2816 } else {
2817 0
2819 };
2820
2821 (thumb_start, thumb_size)
2822 };
2823
2824 let thumb_end = thumb_start + thumb_size;
2825
2826 let track_color = if is_active {
2828 Color::DarkGray
2829 } else {
2830 Color::Black
2831 };
2832 let thumb_color = if is_active {
2833 Color::Gray
2834 } else {
2835 Color::DarkGray
2836 };
2837
2838 for row in 0..height {
2840 let cell_area = Rect::new(scrollbar_rect.x, scrollbar_rect.y + row as u16, 1, 1);
2841
2842 let style = if row >= thumb_start && row < thumb_end {
2843 Style::default().bg(thumb_color)
2845 } else {
2846 Style::default().bg(track_color)
2848 };
2849
2850 let paragraph = Paragraph::new(" ").style(style);
2851 frame.render_widget(paragraph, cell_area);
2852 }
2853
2854 (thumb_start, thumb_end)
2856 }
2857
2858 fn compute_max_line_length(
2863 state: &mut EditorState,
2864 viewport: &mut crate::view::viewport::Viewport,
2865 ) -> usize {
2866 let buffer_len = state.buffer.len();
2867 let visible_width = viewport.width as usize;
2868
2869 if buffer_len == 0 {
2870 return viewport.max_line_length_seen.max(visible_width);
2871 }
2872
2873 let visible_lines = viewport.height as usize + 5; let mut lines_scanned = 0usize;
2876 let mut iter = state.buffer.line_iterator(viewport.top_byte, 80);
2877 loop {
2878 if lines_scanned >= visible_lines {
2879 break;
2880 }
2881 match iter.next_line() {
2882 Some((_byte_offset, content)) => {
2883 let display_len = content.len();
2884 if display_len > viewport.max_line_length_seen {
2885 viewport.max_line_length_seen = display_len;
2886 }
2887 lines_scanned += 1;
2888 }
2889 None => break,
2890 }
2891 }
2892
2893 viewport.max_line_length_seen.max(visible_width)
2896 }
2897
2898 fn render_horizontal_scrollbar(
2902 frame: &mut Frame,
2903 viewport: &crate::view::viewport::Viewport,
2904 hscrollbar_rect: Rect,
2905 is_active: bool,
2906 max_content_width: usize,
2907 ) -> (usize, usize) {
2908 let width = hscrollbar_rect.width as usize;
2909 if width == 0 || hscrollbar_rect.height == 0 {
2910 return (0, 0);
2911 }
2912
2913 let track_color = if is_active {
2914 Color::DarkGray
2915 } else {
2916 Color::Black
2917 };
2918
2919 if viewport.line_wrap_enabled {
2921 for col in 0..width {
2922 let cell_area = Rect::new(hscrollbar_rect.x + col as u16, hscrollbar_rect.y, 1, 1);
2923 let paragraph = Paragraph::new(" ").style(Style::default().bg(track_color));
2924 frame.render_widget(paragraph, cell_area);
2925 }
2926 return (0, width);
2927 }
2928
2929 let visible_width = viewport.width as usize;
2930 let left_column = viewport.left_column;
2931
2932 let max_scroll = max_content_width.saturating_sub(visible_width);
2934
2935 let (thumb_start, thumb_size) = if max_scroll == 0 {
2936 (0, width)
2937 } else {
2938 let thumb_size_raw =
2940 ((visible_width as f64 / max_content_width as f64) * width as f64).ceil() as usize;
2941 let thumb_size = thumb_size_raw.max(2).min(width); let scroll_ratio = left_column.min(max_scroll) as f64 / max_scroll as f64;
2945 let max_thumb_start = width.saturating_sub(thumb_size);
2946 let thumb_start = (scroll_ratio * max_thumb_start as f64).round() as usize;
2947
2948 (thumb_start, thumb_size)
2949 };
2950
2951 let thumb_end = thumb_start + thumb_size;
2952
2953 let thumb_color = if is_active {
2954 Color::Gray
2955 } else {
2956 Color::DarkGray
2957 };
2958
2959 for col in 0..width {
2961 let cell_area = Rect::new(hscrollbar_rect.x + col as u16, hscrollbar_rect.y, 1, 1);
2962
2963 let style = if col >= thumb_start && col < thumb_end {
2964 Style::default().bg(thumb_color)
2965 } else {
2966 Style::default().bg(track_color)
2967 };
2968
2969 let paragraph = Paragraph::new(" ").style(style);
2970 frame.render_widget(paragraph, cell_area);
2971 }
2972
2973 (thumb_start, thumb_end)
2974 }
2975
2976 #[allow(clippy::too_many_arguments)]
2977 fn build_view_data(
2978 state: &mut EditorState,
2979 viewport: &crate::view::viewport::Viewport,
2980 view_transform: Option<ViewTransformPayload>,
2981 estimated_line_length: usize,
2982 visible_count: usize,
2983 line_wrap_enabled: bool,
2984 content_width: usize,
2985 gutter_width: usize,
2986 view_mode: &ViewMode,
2987 folds: &FoldManager,
2988 theme: &crate::view::theme::Theme,
2989 ) -> ViewData {
2990 let adjusted_visible_count = Self::fold_adjusted_visible_count(
2991 &state.buffer,
2992 &state.marker_list,
2993 folds,
2994 viewport.top_byte,
2995 visible_count,
2996 );
2997
2998 let is_binary = state.buffer.is_binary();
3000 let line_ending = state.buffer.line_ending();
3001
3002 let base_tokens = Self::build_base_tokens(
3004 &mut state.buffer,
3005 viewport.top_byte,
3006 estimated_line_length,
3007 adjusted_visible_count,
3008 is_binary,
3009 line_ending,
3010 );
3011
3012 let has_view_transform = view_transform.is_some();
3014 let mut tokens = view_transform.map(|vt| vt.tokens).unwrap_or(base_tokens);
3015
3016 let is_compose = matches!(view_mode, ViewMode::PageView);
3019 if is_compose && !state.soft_breaks.is_empty() {
3020 let viewport_end = tokens
3021 .iter()
3022 .filter_map(|t| t.source_offset)
3023 .next_back()
3024 .unwrap_or(viewport.top_byte)
3025 + 1;
3026 let soft_breaks = state.soft_breaks.query_viewport(
3027 viewport.top_byte,
3028 viewport_end,
3029 &state.marker_list,
3030 );
3031 if !soft_breaks.is_empty() {
3032 tokens = Self::apply_soft_breaks(tokens, &soft_breaks);
3033 }
3034 }
3035
3036 if is_compose && !state.conceals.is_empty() {
3039 let viewport_end = tokens
3040 .iter()
3041 .filter_map(|t| t.source_offset)
3042 .next_back()
3043 .unwrap_or(viewport.top_byte)
3044 + 1;
3045 let conceal_ranges =
3046 state
3047 .conceals
3048 .query_viewport(viewport.top_byte, viewport_end, &state.marker_list);
3049 if !conceal_ranges.is_empty() {
3050 tokens = Self::apply_conceal_ranges(tokens, &conceal_ranges);
3051 }
3052 }
3053
3054 let effective_width = if line_wrap_enabled {
3059 if let Some(col) = viewport.wrap_column {
3060 col.min(content_width)
3062 } else {
3063 content_width
3064 }
3065 } else {
3066 MAX_SAFE_LINE_WIDTH
3067 };
3068 let hanging_indent = line_wrap_enabled && viewport.wrap_indent;
3069 tokens =
3070 Self::apply_wrapping_transform(tokens, effective_width, gutter_width, hanging_indent);
3071
3072 let is_binary = state.buffer.is_binary();
3077 let ansi_aware = !is_binary; let at_buffer_end = if has_view_transform {
3079 false
3082 } else {
3083 let max_source_offset = tokens
3084 .iter()
3085 .filter_map(|t| t.source_offset)
3086 .max()
3087 .unwrap_or(0);
3088 max_source_offset + 2 >= state.buffer.len()
3089 };
3090 let source_lines: Vec<ViewLine> = ViewLineIterator::new(
3091 &tokens,
3092 is_binary,
3093 ansi_aware,
3094 state.buffer_settings.tab_size,
3095 at_buffer_end,
3096 )
3097 .collect();
3098
3099 let lines = Self::inject_virtual_lines(source_lines, state);
3101 let placeholder_style = fold_placeholder_style(theme);
3102 let lines = Self::apply_folding(
3103 lines,
3104 &state.buffer,
3105 &state.marker_list,
3106 folds,
3107 &placeholder_style,
3108 );
3109
3110 ViewData { lines }
3111 }
3112
3113 fn fold_adjusted_visible_count(
3114 buffer: &Buffer,
3115 marker_list: &crate::model::marker::MarkerList,
3116 folds: &FoldManager,
3117 top_byte: usize,
3118 visible_count: usize,
3119 ) -> usize {
3120 if folds.is_empty() {
3121 return visible_count;
3122 }
3123
3124 let start_line = buffer.get_line_number(top_byte);
3125 let mut total = visible_count;
3126
3127 let mut ranges = folds.resolved_ranges(buffer, marker_list);
3128 if ranges.is_empty() {
3129 return visible_count;
3130 }
3131 ranges.sort_by_key(|range| range.header_line);
3132
3133 let mut min_header_line = start_line;
3134 if let Some(containing_end) = ranges
3135 .iter()
3136 .filter(|range| start_line >= range.start_line && start_line <= range.end_line)
3137 .map(|range| range.end_line)
3138 .max()
3139 {
3140 let hidden_remaining = containing_end.saturating_sub(start_line).saturating_add(1);
3141 total = total.saturating_add(hidden_remaining);
3142 min_header_line = containing_end.saturating_add(1);
3143 }
3144
3145 let mut end_line = start_line.saturating_add(total);
3146
3147 for range in ranges {
3148 if range.header_line < min_header_line {
3149 continue;
3150 }
3151 if range.header_line > end_line {
3152 break;
3153 }
3154 let hidden = range
3155 .end_line
3156 .saturating_sub(range.start_line)
3157 .saturating_add(1);
3158 total = total.saturating_add(hidden);
3159 end_line = start_line.saturating_add(total);
3160 }
3161
3162 total
3163 }
3164
3165 fn apply_folding(
3166 lines: Vec<ViewLine>,
3167 buffer: &Buffer,
3168 marker_list: &crate::model::marker::MarkerList,
3169 folds: &FoldManager,
3170 placeholder_style: &ViewTokenStyle,
3171 ) -> Vec<ViewLine> {
3172 if folds.is_empty() {
3173 return lines;
3174 }
3175
3176 let collapsed_ranges = folds.resolved_ranges(buffer, marker_list);
3177 if collapsed_ranges.is_empty() {
3178 return lines;
3179 }
3180
3181 let collapsed_header_bytes = folds.collapsed_header_bytes(buffer, marker_list);
3182
3183 let mut next_source_byte: Vec<Option<usize>> = vec![None; lines.len()];
3185 let mut next_byte: Option<usize> = None;
3186 for (idx, line) in lines.iter().enumerate().rev() {
3187 next_source_byte[idx] = next_byte;
3188 if let Some(byte) = Self::view_line_source_byte(line) {
3189 next_byte = Some(byte);
3190 }
3191 }
3192
3193 let mut filtered = Vec::with_capacity(lines.len());
3194 for (idx, mut line) in lines.into_iter().enumerate() {
3195 let source_byte = Self::view_line_source_byte(&line);
3196
3197 if let Some(byte) = source_byte {
3198 if Self::is_hidden_byte(byte, &collapsed_ranges) {
3199 continue;
3200 }
3201
3202 if let Some(placeholder) = collapsed_header_bytes.get(&byte) {
3203 if next_source_byte[idx] != Some(byte) {
3205 let raw_text = placeholder
3206 .as_deref()
3207 .filter(|s| !s.trim().is_empty())
3208 .unwrap_or("...");
3209 let text = if raw_text.starts_with(' ') {
3210 raw_text.to_string()
3211 } else {
3212 format!(" {}", raw_text)
3213 };
3214 Self::append_fold_placeholder(&mut line, &text, placeholder_style);
3215 }
3216 }
3217 } else if let Some(next_byte) = next_source_byte[idx] {
3218 if Self::is_hidden_byte(next_byte, &collapsed_ranges) {
3219 continue;
3220 }
3221 }
3222
3223 filtered.push(line);
3224 }
3225
3226 filtered
3227 }
3228
3229 fn view_line_source_byte(line: &ViewLine) -> Option<usize> {
3231 line.char_source_bytes.iter().find_map(|m| *m)
3232 }
3233
3234 fn is_hidden_byte(byte: usize, ranges: &[crate::view::folding::ResolvedFoldRange]) -> bool {
3236 ranges
3237 .iter()
3238 .any(|range| byte >= range.start_byte && byte < range.end_byte)
3239 }
3240
3241 fn append_fold_placeholder(line: &mut ViewLine, text: &str, style: &ViewTokenStyle) {
3242 if text.is_empty() {
3243 return;
3244 }
3245
3246 let mut removed_newline: Option<(char, Option<usize>, Option<ViewTokenStyle>)> = None;
3249 if line.ends_with_newline {
3250 if let Some(last_char) = line.text.chars().last() {
3251 if last_char == '\n' {
3252 let removed = line.text.pop();
3253 if removed.is_some() {
3254 let removed_source = line.char_source_bytes.pop().unwrap_or(None);
3255 let removed_style = line.char_styles.pop().unwrap_or(None);
3256 line.char_visual_cols.pop();
3257 let width = char_width(last_char);
3258 for _ in 0..width {
3259 line.visual_to_char.pop();
3260 }
3261 removed_newline = Some((last_char, removed_source, removed_style));
3262 }
3263 }
3264 }
3265 }
3266
3267 let mut col = line.visual_to_char.len();
3268 for ch in text.chars() {
3269 let char_idx = line.char_source_bytes.len();
3270 let width = char_width(ch);
3271 line.text.push(ch);
3272 line.char_source_bytes.push(None);
3273 line.char_styles.push(Some(style.clone()));
3274 line.char_visual_cols.push(col);
3275 for _ in 0..width {
3276 line.visual_to_char.push(char_idx);
3277 }
3278 col += width;
3279 }
3280
3281 if let Some((ch, source, style)) = removed_newline {
3282 let char_idx = line.char_source_bytes.len();
3283 let width = char_width(ch);
3284 line.text.push(ch);
3285 line.char_source_bytes.push(source);
3286 line.char_styles.push(style);
3287 line.char_visual_cols.push(col);
3288 for _ in 0..width {
3289 line.visual_to_char.push(char_idx);
3290 }
3291 }
3292 }
3293
3294 fn create_virtual_line(text: &str, style: ratatui::style::Style) -> ViewLine {
3296 use fresh_core::api::ViewTokenStyle;
3297
3298 let text = text.to_string();
3299 let len = text.chars().count();
3300
3301 let token_style = ViewTokenStyle {
3303 fg: style.fg.and_then(|c| match c {
3304 ratatui::style::Color::Rgb(r, g, b) => Some((r, g, b)),
3305 _ => None,
3306 }),
3307 bg: style.bg.and_then(|c| match c {
3308 ratatui::style::Color::Rgb(r, g, b) => Some((r, g, b)),
3309 _ => None,
3310 }),
3311 bold: style.add_modifier.contains(ratatui::style::Modifier::BOLD),
3312 italic: style
3313 .add_modifier
3314 .contains(ratatui::style::Modifier::ITALIC),
3315 };
3316
3317 ViewLine {
3318 text,
3319 char_source_bytes: vec![None; len],
3321 char_styles: vec![Some(token_style); len],
3323 char_visual_cols: (0..len).collect(),
3325 visual_to_char: (0..len).collect(),
3327 tab_starts: HashSet::new(),
3328 line_start: LineStart::AfterInjectedNewline,
3330 ends_with_newline: true,
3331 }
3332 }
3333
3334 fn inject_virtual_lines(source_lines: Vec<ViewLine>, state: &EditorState) -> Vec<ViewLine> {
3336 use crate::view::virtual_text::VirtualTextPosition;
3337
3338 let viewport_start = source_lines
3342 .first()
3343 .and_then(|l| l.char_source_bytes.iter().find_map(|m| *m))
3344 .unwrap_or(0);
3345 let viewport_end = source_lines
3346 .iter()
3347 .rev()
3348 .find_map(|l| l.char_source_bytes.iter().rev().find_map(|m| *m))
3349 .map(|b| b + 1)
3350 .unwrap_or(viewport_start);
3351
3352 let virtual_lines = state.virtual_texts.query_lines_in_range(
3354 &state.marker_list,
3355 viewport_start,
3356 viewport_end,
3357 );
3358
3359 if virtual_lines.is_empty() {
3361 return source_lines;
3362 }
3363
3364 let mut result = Vec::with_capacity(source_lines.len() + virtual_lines.len());
3366
3367 for source_line in source_lines {
3368 let line_start_byte = source_line.char_source_bytes.iter().find_map(|m| *m);
3370 let line_end_byte = source_line
3371 .char_source_bytes
3372 .iter()
3373 .rev()
3374 .find_map(|m| *m)
3375 .map(|b| b + 1);
3376
3377 if let (Some(start), Some(end)) = (line_start_byte, line_end_byte) {
3379 for (anchor_pos, vtext) in &virtual_lines {
3380 if *anchor_pos >= start
3381 && *anchor_pos < end
3382 && vtext.position == VirtualTextPosition::LineAbove
3383 {
3384 result.push(Self::create_virtual_line(&vtext.text, vtext.style));
3385 }
3386 }
3387 }
3388
3389 result.push(source_line.clone());
3391
3392 if let (Some(start), Some(end)) = (line_start_byte, line_end_byte) {
3394 for (anchor_pos, vtext) in &virtual_lines {
3395 if *anchor_pos >= start
3396 && *anchor_pos < end
3397 && vtext.position == VirtualTextPosition::LineBelow
3398 {
3399 result.push(Self::create_virtual_line(&vtext.text, vtext.style));
3400 }
3401 }
3402 }
3403 }
3404
3405 result
3406 }
3407
3408 fn apply_soft_breaks(
3417 tokens: Vec<fresh_core::api::ViewTokenWire>,
3418 soft_breaks: &[(usize, u16)],
3419 ) -> Vec<fresh_core::api::ViewTokenWire> {
3420 use fresh_core::api::{ViewTokenWire, ViewTokenWireKind};
3421
3422 if soft_breaks.is_empty() {
3423 return tokens;
3424 }
3425
3426 let mut output = Vec::with_capacity(tokens.len() + soft_breaks.len() * 2);
3427 let mut break_idx = 0;
3428
3429 for token in tokens {
3430 let offset = match token.source_offset {
3431 Some(o) => o,
3432 None => {
3433 output.push(token);
3435 continue;
3436 }
3437 };
3438
3439 while break_idx < soft_breaks.len() && soft_breaks[break_idx].0 < offset {
3442 break_idx += 1;
3443 }
3444
3445 if break_idx < soft_breaks.len() && soft_breaks[break_idx].0 == offset {
3446 let indent = soft_breaks[break_idx].1;
3447 break_idx += 1;
3448
3449 match &token.kind {
3450 ViewTokenWireKind::Space => {
3451 output.push(ViewTokenWire {
3453 source_offset: None,
3454 kind: ViewTokenWireKind::Newline,
3455 style: None,
3456 });
3457 for _ in 0..indent {
3458 output.push(ViewTokenWire {
3459 source_offset: None,
3460 kind: ViewTokenWireKind::Space,
3461 style: None,
3462 });
3463 }
3464 }
3465 _ => {
3466 output.push(ViewTokenWire {
3468 source_offset: None,
3469 kind: ViewTokenWireKind::Newline,
3470 style: None,
3471 });
3472 for _ in 0..indent {
3473 output.push(ViewTokenWire {
3474 source_offset: None,
3475 kind: ViewTokenWireKind::Space,
3476 style: None,
3477 });
3478 }
3479 output.push(token);
3480 }
3481 }
3482 } else {
3483 output.push(token);
3484 }
3485 }
3486
3487 output
3488 }
3489
3490 fn apply_conceal_ranges(
3498 tokens: Vec<fresh_core::api::ViewTokenWire>,
3499 conceal_ranges: &[(std::ops::Range<usize>, Option<&str>)],
3500 ) -> Vec<fresh_core::api::ViewTokenWire> {
3501 use fresh_core::api::{ViewTokenWire, ViewTokenWireKind};
3502 use std::collections::HashSet;
3503
3504 if conceal_ranges.is_empty() {
3505 return tokens;
3506 }
3507
3508 let mut output = Vec::with_capacity(tokens.len());
3509 let mut emitted_replacements: HashSet<usize> = HashSet::new();
3510
3511 let is_concealed = |byte_offset: usize| -> Option<usize> {
3513 for (idx, (range, _)) in conceal_ranges.iter().enumerate() {
3514 if byte_offset >= range.start && byte_offset < range.end {
3515 return Some(idx);
3516 }
3517 }
3518 None
3519 };
3520
3521 for token in tokens {
3522 let offset = match token.source_offset {
3523 Some(o) => o,
3524 None => {
3525 output.push(token);
3527 continue;
3528 }
3529 };
3530
3531 match &token.kind {
3532 ViewTokenWireKind::Text(text) => {
3533 let mut current_byte = offset;
3537 let mut visible_start: Option<usize> = None; let mut visible_chars = String::new();
3539
3540 for ch in text.chars() {
3541 let ch_len = ch.len_utf8();
3542
3543 if let Some(cidx) = is_concealed(current_byte) {
3544 if !visible_chars.is_empty() {
3546 output.push(ViewTokenWire {
3547 source_offset: visible_start,
3548 kind: ViewTokenWireKind::Text(std::mem::take(
3549 &mut visible_chars,
3550 )),
3551 style: token.style.clone(),
3552 });
3553 visible_start = None;
3554 }
3555
3556 if let Some(repl) = conceal_ranges[cidx].1 {
3564 if !emitted_replacements.contains(&cidx) {
3565 emitted_replacements.insert(cidx);
3566 if !repl.is_empty() {
3567 let mut chars = repl.chars();
3568 if let Some(first_ch) = chars.next() {
3569 output.push(ViewTokenWire {
3571 source_offset: Some(conceal_ranges[cidx].0.start),
3572 kind: ViewTokenWireKind::Text(first_ch.to_string()),
3573 style: None,
3574 });
3575 let rest: String = chars.collect();
3576 if !rest.is_empty() {
3577 output.push(ViewTokenWire {
3579 source_offset: None,
3580 kind: ViewTokenWireKind::Text(rest),
3581 style: None,
3582 });
3583 }
3584 }
3585 }
3586 }
3587 }
3588 } else {
3589 if visible_start.is_none() {
3591 visible_start = Some(current_byte);
3592 }
3593 visible_chars.push(ch);
3594 }
3595
3596 current_byte += ch_len;
3597 }
3598
3599 if !visible_chars.is_empty() {
3601 output.push(ViewTokenWire {
3602 source_offset: visible_start,
3603 kind: ViewTokenWireKind::Text(visible_chars),
3604 style: token.style.clone(),
3605 });
3606 }
3607 }
3608 ViewTokenWireKind::Space
3609 | ViewTokenWireKind::Newline
3610 | ViewTokenWireKind::Break => {
3611 if is_concealed(offset).is_some() {
3613 } else {
3615 output.push(token);
3616 }
3617 }
3618 ViewTokenWireKind::BinaryByte(_) => {
3619 if is_concealed(offset).is_some() {
3620 } else {
3622 output.push(token);
3623 }
3624 }
3625 }
3626 }
3627
3628 output
3629 }
3630
3631 fn build_base_tokens(
3632 buffer: &mut Buffer,
3633 top_byte: usize,
3634 estimated_line_length: usize,
3635 visible_count: usize,
3636 is_binary: bool,
3637 line_ending: crate::model::buffer::LineEnding,
3638 ) -> Vec<fresh_core::api::ViewTokenWire> {
3639 use crate::model::buffer::LineEnding;
3640 use fresh_core::api::{ViewTokenWire, ViewTokenWireKind};
3641
3642 let mut tokens = Vec::new();
3643
3644 if is_binary {
3647 return Self::build_base_tokens_binary(
3648 buffer,
3649 top_byte,
3650 estimated_line_length,
3651 visible_count,
3652 );
3653 }
3654
3655 let mut iter = buffer.line_iterator(top_byte, estimated_line_length);
3656 let mut lines_seen = 0usize;
3657 let max_lines = visible_count.saturating_add(4);
3658
3659 while lines_seen < max_lines {
3660 if let Some((line_start, line_content)) = iter.next_line() {
3661 let mut byte_offset = 0usize;
3662 let content_bytes = line_content.as_bytes();
3663 let mut skip_next_lf = false; let mut chars_this_line = 0usize; for ch in line_content.chars() {
3666 if chars_this_line >= MAX_SAFE_LINE_WIDTH {
3669 tokens.push(ViewTokenWire {
3670 source_offset: None,
3671 kind: ViewTokenWireKind::Break,
3672 style: None,
3673 });
3674 chars_this_line = 0;
3675 lines_seen += 1;
3677 if lines_seen >= max_lines {
3678 break;
3679 }
3680 }
3681 chars_this_line += 1;
3682
3683 let ch_len = ch.len_utf8();
3684 let source_offset = Some(line_start + byte_offset);
3685
3686 match ch {
3687 '\r' => {
3688 let is_crlf_file = line_ending == LineEnding::CRLF;
3692 let next_byte = content_bytes.get(byte_offset + 1);
3693 if is_crlf_file && next_byte == Some(&b'\n') {
3694 tokens.push(ViewTokenWire {
3696 source_offset,
3697 kind: ViewTokenWireKind::Newline,
3698 style: None,
3699 });
3700 skip_next_lf = true;
3702 byte_offset += ch_len;
3703 continue;
3704 }
3705 tokens.push(ViewTokenWire {
3707 source_offset,
3708 kind: ViewTokenWireKind::BinaryByte(ch as u8),
3709 style: None,
3710 });
3711 }
3712 '\n' if skip_next_lf => {
3713 skip_next_lf = false;
3715 byte_offset += ch_len;
3716 continue;
3717 }
3718 '\n' => {
3719 tokens.push(ViewTokenWire {
3720 source_offset,
3721 kind: ViewTokenWireKind::Newline,
3722 style: None,
3723 });
3724 }
3725 ' ' => {
3726 tokens.push(ViewTokenWire {
3727 source_offset,
3728 kind: ViewTokenWireKind::Space,
3729 style: None,
3730 });
3731 }
3732 '\t' => {
3733 tokens.push(ViewTokenWire {
3735 source_offset,
3736 kind: ViewTokenWireKind::Text(ch.to_string()),
3737 style: None,
3738 });
3739 }
3740 _ if Self::is_control_char(ch) => {
3741 tokens.push(ViewTokenWire {
3743 source_offset,
3744 kind: ViewTokenWireKind::BinaryByte(ch as u8),
3745 style: None,
3746 });
3747 }
3748 _ => {
3749 if let Some(last) = tokens.last_mut() {
3751 if let ViewTokenWireKind::Text(ref mut s) = last.kind {
3752 let expected_offset = last.source_offset.map(|o| o + s.len());
3754 if expected_offset == Some(line_start + byte_offset) {
3755 s.push(ch);
3756 byte_offset += ch_len;
3757 continue;
3758 }
3759 }
3760 }
3761 tokens.push(ViewTokenWire {
3762 source_offset,
3763 kind: ViewTokenWireKind::Text(ch.to_string()),
3764 style: None,
3765 });
3766 }
3767 }
3768 byte_offset += ch_len;
3769 }
3770 lines_seen += 1;
3771 } else {
3772 break;
3773 }
3774 }
3775
3776 if tokens.is_empty() {
3778 tokens.push(ViewTokenWire {
3779 source_offset: Some(top_byte),
3780 kind: ViewTokenWireKind::Text(String::new()),
3781 style: None,
3782 });
3783 }
3784
3785 tokens
3786 }
3787
3788 fn build_base_tokens_binary(
3791 buffer: &mut Buffer,
3792 top_byte: usize,
3793 estimated_line_length: usize,
3794 visible_count: usize,
3795 ) -> Vec<fresh_core::api::ViewTokenWire> {
3796 use fresh_core::api::{ViewTokenWire, ViewTokenWireKind};
3797
3798 let mut tokens = Vec::new();
3799 let max_lines = visible_count.saturating_add(4);
3800 let buffer_len = buffer.len();
3801
3802 if top_byte >= buffer_len {
3803 tokens.push(ViewTokenWire {
3804 source_offset: Some(top_byte),
3805 kind: ViewTokenWireKind::Text(String::new()),
3806 style: None,
3807 });
3808 return tokens;
3809 }
3810
3811 let estimated_bytes = estimated_line_length * max_lines * 2;
3813 let bytes_to_read = estimated_bytes.min(buffer_len - top_byte);
3814
3815 let raw_bytes = buffer.slice_bytes(top_byte..top_byte + bytes_to_read);
3817
3818 let mut byte_offset = 0usize;
3819 let mut lines_seen = 0usize;
3820 let mut current_text = String::new();
3821 let mut current_text_start: Option<usize> = None;
3822
3823 let flush_text =
3825 |tokens: &mut Vec<ViewTokenWire>, text: &mut String, start: &mut Option<usize>| {
3826 if !text.is_empty() {
3827 tokens.push(ViewTokenWire {
3828 source_offset: *start,
3829 kind: ViewTokenWireKind::Text(std::mem::take(text)),
3830 style: None,
3831 });
3832 *start = None;
3833 }
3834 };
3835
3836 while byte_offset < raw_bytes.len() && lines_seen < max_lines {
3837 let b = raw_bytes[byte_offset];
3838 let source_offset = top_byte + byte_offset;
3839
3840 match b {
3841 b'\n' => {
3842 flush_text(&mut tokens, &mut current_text, &mut current_text_start);
3843 tokens.push(ViewTokenWire {
3844 source_offset: Some(source_offset),
3845 kind: ViewTokenWireKind::Newline,
3846 style: None,
3847 });
3848 lines_seen += 1;
3849 }
3850 b' ' => {
3851 flush_text(&mut tokens, &mut current_text, &mut current_text_start);
3852 tokens.push(ViewTokenWire {
3853 source_offset: Some(source_offset),
3854 kind: ViewTokenWireKind::Space,
3855 style: None,
3856 });
3857 }
3858 _ => {
3859 if Self::is_binary_unprintable(b) {
3862 flush_text(&mut tokens, &mut current_text, &mut current_text_start);
3864 tokens.push(ViewTokenWire {
3866 source_offset: Some(source_offset),
3867 kind: ViewTokenWireKind::BinaryByte(b),
3868 style: None,
3869 });
3870 } else {
3871 if current_text_start.is_none() {
3874 current_text_start = Some(source_offset);
3875 }
3876 current_text.push(b as char);
3877 }
3878 }
3879 }
3880 byte_offset += 1;
3881 }
3882
3883 flush_text(&mut tokens, &mut current_text, &mut current_text_start);
3885
3886 if tokens.is_empty() {
3888 tokens.push(ViewTokenWire {
3889 source_offset: Some(top_byte),
3890 kind: ViewTokenWireKind::Text(String::new()),
3891 style: None,
3892 });
3893 }
3894
3895 tokens
3896 }
3897
3898 fn is_binary_unprintable(b: u8) -> bool {
3910 if b == 0x09 || b == 0x0A {
3914 return false;
3915 }
3916 if b < 0x20 {
3919 return true;
3920 }
3921 if b == 0x7F {
3923 return true;
3924 }
3925 if b >= 0x80 {
3928 return true;
3929 }
3930 false
3931 }
3932
3933 fn is_control_char(ch: char) -> bool {
3936 let code = ch as u32;
3937 if code >= 128 {
3939 return false;
3940 }
3941 let b = code as u8;
3942 if b == 0x09 || b == 0x0A || b == 0x1B {
3944 return false;
3945 }
3946 b < 0x20 || b == 0x7F
3949 }
3950
3951 pub fn build_base_tokens_for_hook(
3953 buffer: &mut Buffer,
3954 top_byte: usize,
3955 estimated_line_length: usize,
3956 visible_count: usize,
3957 is_binary: bool,
3958 line_ending: crate::model::buffer::LineEnding,
3959 ) -> Vec<fresh_core::api::ViewTokenWire> {
3960 Self::build_base_tokens(
3961 buffer,
3962 top_byte,
3963 estimated_line_length,
3964 visible_count,
3965 is_binary,
3966 line_ending,
3967 )
3968 }
3969
3970 fn apply_wrapping_transform(
3971 tokens: Vec<fresh_core::api::ViewTokenWire>,
3972 content_width: usize,
3973 gutter_width: usize,
3974 hanging_indent: bool,
3975 ) -> Vec<fresh_core::api::ViewTokenWire> {
3976 use crate::primitives::visual_layout::visual_width;
3977 use fresh_core::api::{ViewTokenWire, ViewTokenWireKind};
3978
3979 const MIN_CONTINUATION_CONTENT_WIDTH: usize = 10;
3981
3982 let available_width = content_width.saturating_sub(gutter_width);
3984
3985 if available_width < 2 {
3989 return tokens;
3990 }
3991
3992 let mut wrapped = Vec::new();
3993 let mut current_line_width: usize = 0;
3994
3995 let mut line_indent: usize = 0;
3998 let mut measuring_indent = hanging_indent;
4000 let mut on_continuation = false;
4002
4003 #[inline]
4011 fn effective_width(
4012 available_width: usize,
4013 _line_indent: usize,
4014 _on_continuation: bool,
4015 ) -> usize {
4016 available_width
4017 }
4018
4019 fn emit_break_with_indent(
4023 wrapped: &mut Vec<ViewTokenWire>,
4024 current_line_width: &mut usize,
4025 indent_string: &str,
4026 ) {
4027 wrapped.push(ViewTokenWire {
4028 source_offset: None,
4029 kind: ViewTokenWireKind::Break,
4030 style: None,
4031 });
4032 *current_line_width = 0;
4033 if !indent_string.is_empty() {
4034 wrapped.push(ViewTokenWire {
4035 source_offset: None,
4036 kind: ViewTokenWireKind::Text(indent_string.to_string()),
4037 style: None,
4038 });
4039 *current_line_width = indent_string.len();
4040 }
4041 }
4042
4043 let mut cached_indent_string = String::new();
4046 let mut cached_indent_len: usize = 0;
4047
4048 for token in tokens {
4049 match &token.kind {
4050 ViewTokenWireKind::Newline => {
4051 wrapped.push(token);
4053 current_line_width = 0;
4054 line_indent = 0;
4055 cached_indent_string.clear();
4056 cached_indent_len = 0;
4057 measuring_indent = hanging_indent;
4058 on_continuation = false;
4059 }
4060 ViewTokenWireKind::Text(text) => {
4061 if measuring_indent {
4063 let mut ws_char_count = 0usize;
4064 let mut ws_visual_width = 0usize;
4065 for c in text.chars() {
4066 if c == ' ' {
4067 ws_visual_width += 1;
4068 ws_char_count += 1;
4069 } else if c == '\t' {
4070 let tab_stop = 4;
4072 let col = line_indent + ws_visual_width;
4073 ws_visual_width += tab_stop - (col % tab_stop);
4074 ws_char_count += 1;
4075 } else {
4076 break;
4077 }
4078 }
4079 if ws_char_count == text.chars().count() {
4080 line_indent += ws_visual_width;
4082 } else {
4083 line_indent += ws_visual_width;
4085 measuring_indent = false;
4086 }
4087 if line_indent + MIN_CONTINUATION_CONTENT_WIDTH > available_width {
4089 line_indent = 0;
4090 }
4091 if line_indent != cached_indent_len {
4093 cached_indent_string = " ".repeat(line_indent);
4094 cached_indent_len = line_indent;
4095 }
4096 }
4097
4098 let eff_width = effective_width(available_width, line_indent, on_continuation);
4099
4100 let text_visual_width = visual_width(text, current_line_width);
4102
4103 if current_line_width > 0 && current_line_width + text_visual_width > eff_width
4105 {
4106 on_continuation = true;
4107 emit_break_with_indent(
4108 &mut wrapped,
4109 &mut current_line_width,
4110 &cached_indent_string,
4111 );
4112 }
4113
4114 let eff_width = effective_width(available_width, line_indent, on_continuation);
4115
4116 let text_visual_width = visual_width(text, current_line_width);
4118
4119 if text_visual_width > eff_width
4123 && !crate::primitives::ansi::contains_ansi_codes(text)
4124 {
4125 use unicode_segmentation::UnicodeSegmentation;
4126
4127 let graphemes: Vec<(usize, &str)> = text.grapheme_indices(true).collect();
4129 let mut grapheme_idx = 0;
4130 let source_base = token.source_offset;
4131
4132 while grapheme_idx < graphemes.len() {
4133 let eff_width =
4134 effective_width(available_width, line_indent, on_continuation);
4135 let remaining_width = eff_width.saturating_sub(current_line_width);
4138 if remaining_width == 0 {
4139 if on_continuation && current_line_width >= eff_width {
4145 } else {
4147 on_continuation = true;
4148 emit_break_with_indent(
4149 &mut wrapped,
4150 &mut current_line_width,
4151 &cached_indent_string,
4152 );
4153 continue;
4154 }
4155 }
4156
4157 let mut chunk_visual_width = 0;
4158 let mut chunk_grapheme_count = 0;
4159 let mut col = current_line_width;
4160
4161 for &(_byte_offset, grapheme) in &graphemes[grapheme_idx..] {
4162 let g_width = if grapheme == "\t" {
4163 crate::primitives::visual_layout::tab_expansion_width(col)
4164 } else {
4165 crate::primitives::display_width::str_width(grapheme)
4166 };
4167
4168 if chunk_visual_width + g_width > remaining_width
4169 && chunk_grapheme_count > 0
4170 {
4171 break;
4172 }
4173
4174 chunk_visual_width += g_width;
4175 chunk_grapheme_count += 1;
4176 col += g_width;
4177 }
4178
4179 if chunk_grapheme_count == 0 {
4180 chunk_grapheme_count = 1;
4182 let grapheme = graphemes[grapheme_idx].1;
4183 chunk_visual_width = if grapheme == "\t" {
4184 crate::primitives::visual_layout::tab_expansion_width(
4185 current_line_width,
4186 )
4187 } else {
4188 crate::primitives::display_width::str_width(grapheme)
4189 };
4190 }
4191
4192 let chunk_start_byte = graphemes[grapheme_idx].0;
4194 let chunk_end_byte =
4195 if grapheme_idx + chunk_grapheme_count < graphemes.len() {
4196 graphemes[grapheme_idx + chunk_grapheme_count].0
4197 } else {
4198 text.len()
4199 };
4200 let chunk = text[chunk_start_byte..chunk_end_byte].to_string();
4201 let chunk_source = source_base.map(|b| b + chunk_start_byte);
4202
4203 wrapped.push(ViewTokenWire {
4204 source_offset: chunk_source,
4205 kind: ViewTokenWireKind::Text(chunk),
4206 style: token.style.clone(),
4207 });
4208
4209 current_line_width += chunk_visual_width;
4210 grapheme_idx += chunk_grapheme_count;
4211
4212 let eff_width =
4213 effective_width(available_width, line_indent, on_continuation);
4214 if current_line_width >= eff_width {
4216 on_continuation = true;
4217 emit_break_with_indent(
4218 &mut wrapped,
4219 &mut current_line_width,
4220 &cached_indent_string,
4221 );
4222 }
4223 }
4224 } else {
4225 wrapped.push(token);
4226 current_line_width += text_visual_width;
4227 }
4228 }
4229 ViewTokenWireKind::Space => {
4230 if measuring_indent {
4232 line_indent += 1;
4233 if line_indent + MIN_CONTINUATION_CONTENT_WIDTH > available_width {
4235 line_indent = 0;
4236 }
4237 }
4238
4239 let eff_width = effective_width(available_width, line_indent, on_continuation);
4240 if current_line_width + 1 > eff_width {
4242 on_continuation = true;
4243 emit_break_with_indent(
4244 &mut wrapped,
4245 &mut current_line_width,
4246 &cached_indent_string,
4247 );
4248 }
4249 wrapped.push(token);
4250 current_line_width += 1;
4251 }
4252 ViewTokenWireKind::Break => {
4253 wrapped.push(token);
4255 current_line_width = 0;
4256 on_continuation = true;
4257 if line_indent > 0 {
4259 wrapped.push(ViewTokenWire {
4260 source_offset: None,
4261 kind: ViewTokenWireKind::Text(" ".repeat(line_indent)),
4262 style: None,
4263 });
4264 current_line_width = line_indent;
4265 }
4266 }
4267 ViewTokenWireKind::BinaryByte(_) => {
4268 if measuring_indent {
4270 measuring_indent = false;
4271 }
4272
4273 let eff_width = effective_width(available_width, line_indent, on_continuation);
4274 let byte_display_width = 4;
4276 if current_line_width + byte_display_width > eff_width {
4277 on_continuation = true;
4278 emit_break_with_indent(
4279 &mut wrapped,
4280 &mut current_line_width,
4281 &cached_indent_string,
4282 );
4283 }
4284 wrapped.push(token);
4285 current_line_width += byte_display_width;
4286 }
4287 }
4288 }
4289
4290 wrapped
4291 }
4292
4293 fn calculate_view_anchor(view_lines: &[ViewLine], top_byte: usize) -> ViewAnchor {
4294 for (idx, line) in view_lines.iter().enumerate() {
4297 if let Some(first_source) = line.char_source_bytes.iter().find_map(|m| *m) {
4299 if first_source >= top_byte {
4300 let mut start_idx = idx;
4303 while start_idx > 0 {
4304 let prev_line = &view_lines[start_idx - 1];
4305 let prev_has_source =
4307 prev_line.char_source_bytes.iter().any(|m| m.is_some());
4308 if !prev_has_source {
4309 start_idx -= 1;
4310 } else {
4311 break;
4312 }
4313 }
4314 return ViewAnchor {
4315 start_line_idx: start_idx,
4316 start_line_skip: 0,
4317 };
4318 }
4319 }
4320 }
4321
4322 ViewAnchor {
4324 start_line_idx: 0,
4325 start_line_skip: 0,
4326 }
4327 }
4328
4329 fn calculate_compose_layout(
4330 area: Rect,
4331 view_mode: &ViewMode,
4332 compose_width: Option<u16>,
4333 ) -> ComposeLayout {
4334 let should_compose = view_mode == &ViewMode::PageView || compose_width.is_some();
4338
4339 if !should_compose {
4340 return ComposeLayout {
4341 render_area: area,
4342 left_pad: 0,
4343 right_pad: 0,
4344 };
4345 }
4346
4347 let target_width = compose_width.unwrap_or(area.width);
4348 let clamped_width = target_width.min(area.width).max(1);
4349 if clamped_width >= area.width {
4350 return ComposeLayout {
4351 render_area: area,
4352 left_pad: 0,
4353 right_pad: 0,
4354 };
4355 }
4356
4357 let pad_total = area.width - clamped_width;
4358 let left_pad = pad_total / 2;
4359 let right_pad = pad_total - left_pad;
4360
4361 ComposeLayout {
4362 render_area: Rect::new(area.x + left_pad, area.y, clamped_width, area.height),
4363 left_pad,
4364 right_pad,
4365 }
4366 }
4367
4368 fn render_compose_margins(
4369 frame: &mut Frame,
4370 area: Rect,
4371 layout: &ComposeLayout,
4372 _view_mode: &ViewMode,
4373 theme: &crate::view::theme::Theme,
4374 effective_editor_bg: ratatui::style::Color,
4375 ) {
4376 if layout.left_pad == 0 && layout.right_pad == 0 {
4378 return;
4379 }
4380
4381 const PAPER_EDGE_WIDTH: u16 = 1;
4384
4385 let desk_style = Style::default().bg(theme.compose_margin_bg);
4386 let paper_style = Style::default().bg(effective_editor_bg);
4387
4388 if layout.left_pad > 0 {
4389 let paper_edge = PAPER_EDGE_WIDTH.min(layout.left_pad);
4390 let desk_width = layout.left_pad.saturating_sub(paper_edge);
4391
4392 if desk_width > 0 {
4394 let desk_rect = Rect::new(area.x, area.y, desk_width, area.height);
4395 frame.render_widget(Block::default().style(desk_style), desk_rect);
4396 }
4397
4398 if paper_edge > 0 {
4400 let paper_rect = Rect::new(area.x + desk_width, area.y, paper_edge, area.height);
4401 frame.render_widget(Block::default().style(paper_style), paper_rect);
4402 }
4403 }
4404
4405 if layout.right_pad > 0 {
4406 let paper_edge = PAPER_EDGE_WIDTH.min(layout.right_pad);
4407 let desk_width = layout.right_pad.saturating_sub(paper_edge);
4408 let right_start = area.x + layout.left_pad + layout.render_area.width;
4409
4410 if paper_edge > 0 {
4412 let paper_rect = Rect::new(right_start, area.y, paper_edge, area.height);
4413 frame.render_widget(Block::default().style(paper_style), paper_rect);
4414 }
4415
4416 if desk_width > 0 {
4418 let desk_rect =
4419 Rect::new(right_start + paper_edge, area.y, desk_width, area.height);
4420 frame.render_widget(Block::default().style(desk_style), desk_rect);
4421 }
4422 }
4423 }
4424
4425 fn selection_context(
4426 state: &EditorState,
4427 cursors: &crate::model::cursor::Cursors,
4428 ) -> SelectionContext {
4429 if !state.show_cursors {
4432 return SelectionContext {
4433 ranges: Vec::new(),
4434 block_rects: Vec::new(),
4435 cursor_positions: Vec::new(),
4436 primary_cursor_position: cursors.primary().position,
4437 };
4438 }
4439
4440 let ranges: Vec<Range<usize>> = cursors
4441 .iter()
4442 .filter_map(|(_, cursor)| {
4443 if cursor.selection_mode == SelectionMode::Block {
4446 None
4447 } else {
4448 cursor.selection_range()
4449 }
4450 })
4451 .collect();
4452
4453 let block_rects: Vec<(usize, usize, usize, usize)> = cursors
4454 .iter()
4455 .filter_map(|(_, cursor)| {
4456 if cursor.selection_mode == SelectionMode::Block {
4457 if let Some(anchor) = cursor.block_anchor {
4458 let cur_line = state.buffer.get_line_number(cursor.position);
4460 let cur_line_start = state.buffer.line_start_offset(cur_line).unwrap_or(0);
4461 let cur_col = cursor.position.saturating_sub(cur_line_start);
4462
4463 Some((
4465 anchor.line.min(cur_line),
4466 anchor.column.min(cur_col),
4467 anchor.line.max(cur_line),
4468 anchor.column.max(cur_col),
4469 ))
4470 } else {
4471 None
4472 }
4473 } else {
4474 None
4475 }
4476 })
4477 .collect();
4478
4479 let cursor_positions: Vec<usize> =
4480 cursors.iter().map(|(_, cursor)| cursor.position).collect();
4481
4482 SelectionContext {
4483 ranges,
4484 block_rects,
4485 cursor_positions,
4486 primary_cursor_position: cursors.primary().position,
4487 }
4488 }
4489
4490 #[allow(clippy::too_many_arguments)]
4491 fn decoration_context(
4492 state: &mut EditorState,
4493 viewport_start: usize,
4494 viewport_end: usize,
4495 primary_cursor_position: usize,
4496 folds: &FoldManager,
4497 theme: &crate::view::theme::Theme,
4498 highlight_context_bytes: usize,
4499 view_mode: &ViewMode,
4500 diagnostics_inline_text: bool,
4501 ) -> DecorationContext {
4502 use crate::view::folding::indent_folding;
4503
4504 let viewport_size = viewport_end.saturating_sub(viewport_start);
4507 let highlight_start = viewport_start.saturating_sub(viewport_size);
4508 let highlight_end = viewport_end
4509 .saturating_add(viewport_size)
4510 .min(state.buffer.len());
4511
4512 let highlight_spans = state.highlighter.highlight_viewport(
4513 &state.buffer,
4514 highlight_start,
4515 highlight_end,
4516 theme,
4517 highlight_context_bytes,
4518 );
4519
4520 state.reference_highlight_overlay.update(
4522 &state.buffer,
4523 &mut state.overlays,
4524 &mut state.marker_list,
4525 &mut state.reference_highlighter,
4526 primary_cursor_position,
4527 viewport_start,
4528 viewport_end,
4529 highlight_context_bytes,
4530 theme.semantic_highlight_bg,
4531 );
4532
4533 state.bracket_highlight_overlay.update(
4535 &state.buffer,
4536 &mut state.overlays,
4537 &mut state.marker_list,
4538 primary_cursor_position,
4539 );
4540
4541 let is_compose = matches!(view_mode, ViewMode::PageView);
4544 let md_emphasis_ns =
4545 fresh_core::overlay::OverlayNamespace::from_string("md-emphasis".to_string());
4546 let mut semantic_token_spans = Vec::new();
4547 let mut viewport_overlays = Vec::new();
4548 for (overlay, range) in
4549 state
4550 .overlays
4551 .query_viewport(viewport_start, viewport_end, &state.marker_list)
4552 {
4553 if crate::services::lsp::semantic_tokens::is_semantic_token_overlay(overlay) {
4554 if let crate::view::overlay::OverlayFace::Foreground { color } = &overlay.face {
4555 semantic_token_spans.push(crate::primitives::highlighter::HighlightSpan {
4556 range,
4557 color: *color,
4558 category: None,
4559 });
4560 }
4561 continue;
4562 }
4563
4564 if !is_compose && overlay.namespace.as_ref() == Some(&md_emphasis_ns) {
4567 continue;
4568 }
4569
4570 viewport_overlays.push((overlay.clone(), range));
4571 }
4572
4573 viewport_overlays.sort_by_key(|(overlay, _)| overlay.priority);
4578
4579 let diagnostic_ns = crate::services::lsp::diagnostics::lsp_diagnostic_namespace();
4582 let diagnostic_lines: HashSet<usize> = viewport_overlays
4583 .iter()
4584 .filter_map(|(overlay, range)| {
4585 if overlay.namespace.as_ref() == Some(&diagnostic_ns) {
4586 return Some(indent_folding::find_line_start_byte(
4587 &state.buffer,
4588 range.start,
4589 ));
4590 }
4591 None
4592 })
4593 .collect();
4594
4595 let diagnostic_inline_texts: HashMap<usize, (String, Style)> = if diagnostics_inline_text {
4598 let mut by_line: HashMap<usize, (String, Style, i32)> = HashMap::new();
4599 for (overlay, range) in &viewport_overlays {
4600 if overlay.namespace.as_ref() != Some(&diagnostic_ns) {
4601 continue;
4602 }
4603 if let Some(ref message) = overlay.message {
4604 let line_start =
4605 indent_folding::find_line_start_byte(&state.buffer, range.start);
4606 let priority = overlay.priority;
4607 let dominated = by_line
4608 .get(&line_start)
4609 .is_some_and(|(_, _, existing_pri)| *existing_pri >= priority);
4610 if !dominated {
4611 let style = inline_diagnostic_style(priority, theme);
4612 let first_line = message.lines().next().unwrap_or(message);
4614 by_line.insert(line_start, (first_line.to_string(), style, priority));
4615 }
4616 }
4617 }
4618 by_line
4619 .into_iter()
4620 .map(|(k, (msg, style, _))| (k, (msg, style)))
4621 .collect()
4622 } else {
4623 HashMap::new()
4624 };
4625
4626 let virtual_text_lookup: HashMap<usize, Vec<crate::view::virtual_text::VirtualText>> =
4627 state
4628 .virtual_texts
4629 .build_lookup(&state.marker_list, viewport_start, viewport_end)
4630 .into_iter()
4631 .map(|(position, texts)| (position, texts.into_iter().cloned().collect()))
4632 .collect();
4633
4634 let mut line_indicators = state.margins.get_indicators_for_viewport(
4637 viewport_start,
4638 viewport_end,
4639 |byte_offset| indent_folding::find_line_start_byte(&state.buffer, byte_offset),
4640 );
4641
4642 let diff_indicators =
4645 Self::diff_indicators_for_viewport(state, viewport_start, viewport_end);
4646 for (key, diff_ind) in diff_indicators {
4647 line_indicators.entry(key).or_insert(diff_ind);
4648 }
4649
4650 let fold_indicators =
4651 Self::fold_indicators_for_viewport(state, folds, viewport_start, viewport_end);
4652
4653 DecorationContext {
4654 highlight_spans,
4655 semantic_token_spans,
4656 viewport_overlays,
4657 virtual_text_lookup,
4658 diagnostic_lines,
4659 diagnostic_inline_texts,
4660 line_indicators,
4661 fold_indicators,
4662 }
4663 }
4664
4665 fn fold_indicators_for_viewport(
4666 state: &EditorState,
4667 folds: &FoldManager,
4668 viewport_start: usize,
4669 viewport_end: usize,
4670 ) -> BTreeMap<usize, FoldIndicator> {
4671 let mut indicators = BTreeMap::new();
4672
4673 for range in folds.resolved_ranges(&state.buffer, &state.marker_list) {
4675 indicators.insert(range.header_byte, FoldIndicator { collapsed: true });
4676 }
4677
4678 if !state.folding_ranges.is_empty() {
4679 for range in &state.folding_ranges {
4681 let start_line = range.start_line as usize;
4682 let end_line = range.end_line as usize;
4683 if end_line <= start_line {
4684 continue;
4685 }
4686 if let Some(line_byte) = state.buffer.line_start_offset(start_line) {
4687 indicators
4688 .entry(line_byte)
4689 .or_insert(FoldIndicator { collapsed: false });
4690 }
4691 }
4692 } else {
4693 use crate::view::folding::indent_folding;
4695 let tab_size = state.buffer_settings.tab_size;
4696 let max_lookahead = crate::config::INDENT_FOLD_INDICATOR_MAX_SCAN;
4697 let bytes = state.buffer.slice_bytes(viewport_start..viewport_end);
4698 if !bytes.is_empty() {
4699 let foldable =
4700 indent_folding::foldable_lines_in_bytes(&bytes, tab_size, max_lookahead);
4701 for line_idx in foldable {
4702 let byte_off = Self::byte_offset_of_line_in_bytes(&bytes, line_idx);
4703 indicators
4704 .entry(viewport_start + byte_off)
4705 .or_insert(FoldIndicator { collapsed: false });
4706 }
4707 }
4708 }
4709
4710 indicators
4711 }
4712
4713 fn diff_indicators_for_viewport(
4720 state: &EditorState,
4721 viewport_start: usize,
4722 viewport_end: usize,
4723 ) -> BTreeMap<usize, crate::view::margin::LineIndicator> {
4724 use crate::view::folding::indent_folding;
4725 let diff = state.buffer.diff_since_saved();
4726 if diff.equal || diff.byte_ranges.is_empty() {
4727 return BTreeMap::new();
4728 }
4729
4730 let mut indicators = BTreeMap::new();
4731 let indicator = crate::view::margin::LineIndicator::new(
4732 "│",
4733 Color::Rgb(100, 149, 237), 5, );
4736
4737 let bytes = state.buffer.slice_bytes(viewport_start..viewport_end);
4738 if bytes.is_empty() {
4739 return indicators;
4740 }
4741
4742 for range in &diff.byte_ranges {
4743 let lo = range.start.max(viewport_start);
4745 let hi = range.end.min(viewport_end);
4746 if lo >= hi {
4747 continue;
4748 }
4749
4750 let line_start = indent_folding::find_line_start_byte(&state.buffer, lo);
4752 if line_start >= viewport_start && line_start < viewport_end {
4753 indicators
4754 .entry(line_start)
4755 .or_insert_with(|| indicator.clone());
4756 }
4757
4758 let rel_lo = lo - viewport_start;
4760 let rel_hi = (hi - viewport_start).min(bytes.len());
4761 for (i, &byte) in bytes[rel_lo..rel_hi].iter().enumerate() {
4762 if byte == b'\n' {
4763 let next_line_start = viewport_start + rel_lo + i + 1;
4764 if next_line_start < viewport_end {
4765 indicators
4766 .entry(next_line_start)
4767 .or_insert_with(|| indicator.clone());
4768 }
4769 }
4770 }
4771 }
4772
4773 indicators
4774 }
4775
4776 fn byte_offset_of_line_in_bytes(bytes: &[u8], line_idx: usize) -> usize {
4779 let mut current_line = 0;
4780 for (i, &b) in bytes.iter().enumerate() {
4781 if current_line == line_idx {
4782 return i;
4783 }
4784 if b == b'\n' {
4785 current_line += 1;
4786 }
4787 }
4788 bytes.len()
4790 }
4791
4792 fn calculate_viewport_end(
4795 state: &mut EditorState,
4796 viewport_start: usize,
4797 estimated_line_length: usize,
4798 visible_count: usize,
4799 ) -> usize {
4800 let mut iter_temp = state
4801 .buffer
4802 .line_iterator(viewport_start, estimated_line_length);
4803 let mut viewport_end = viewport_start;
4804 for _ in 0..visible_count {
4805 if let Some((line_start, line_content)) = iter_temp.next_line() {
4806 viewport_end = line_start + line_content.len();
4807 } else {
4808 break;
4809 }
4810 }
4811 viewport_end
4812 }
4813
4814 fn render_view_lines(input: LineRenderInput<'_>) -> LineRenderOutput {
4815 use crate::view::folding::indent_folding;
4816
4817 let LineRenderInput {
4818 state,
4819 theme,
4820 view_lines,
4821 view_anchor,
4822 render_area,
4823 gutter_width,
4824 selection,
4825 decorations,
4826 visible_line_count,
4827 lsp_waiting,
4828 is_active,
4829 line_wrap,
4830 estimated_lines,
4831 left_column,
4832 relative_line_numbers,
4833 session_mode,
4834 software_cursor_only,
4835 show_line_numbers,
4836 byte_offset_mode,
4837 show_tilde,
4838 highlight_current_line,
4839 cell_theme_map,
4840 screen_width,
4841 } = input;
4842
4843 if screen_width > 0 {
4845 let gutter_info = crate::app::types::CellThemeInfo {
4846 fg_key: Some("editor.line_number_fg"),
4847 bg_key: Some("editor.line_number_bg"),
4848 region: "Line Numbers",
4849 syntax_category: None,
4850 };
4851 let content_info = crate::app::types::CellThemeInfo {
4852 fg_key: Some("editor.fg"),
4853 bg_key: Some("editor.bg"),
4854 region: "Editor Content",
4855 syntax_category: None,
4856 };
4857 let sw = screen_width as usize;
4858 for row in render_area.y..render_area.y + render_area.height {
4859 for col in render_area.x..render_area.x + render_area.width {
4860 let idx = row as usize * sw + col as usize;
4861 if let Some(cell) = cell_theme_map.get_mut(idx) {
4862 *cell = if col < render_area.x + gutter_width as u16 {
4863 gutter_info.clone()
4864 } else {
4865 content_info.clone()
4866 };
4867 }
4868 }
4869 }
4870 }
4871
4872 let selection_ranges = &selection.ranges;
4873 let block_selections = &selection.block_rects;
4874 let cursor_positions = &selection.cursor_positions;
4875 let primary_cursor_position = selection.primary_cursor_position;
4876
4877 let cursor_line_start_byte =
4879 indent_folding::find_line_start_byte(&state.buffer, primary_cursor_position);
4880
4881 let highlight_spans = &decorations.highlight_spans;
4882 let semantic_token_spans = &decorations.semantic_token_spans;
4883 let viewport_overlays = &decorations.viewport_overlays;
4884 let virtual_text_lookup = &decorations.virtual_text_lookup;
4885 let diagnostic_lines = &decorations.diagnostic_lines;
4886 let line_indicators = &decorations.line_indicators;
4887
4888 let mut hl_cursor = 0usize;
4890 let mut sem_cursor = 0usize;
4891
4892 let mut lines = Vec::new();
4893 let mut view_line_mappings = Vec::new();
4894 let mut lines_rendered = 0usize;
4895 let mut view_iter_idx = view_anchor.start_line_idx;
4896 let mut cursor_screen_x = 0u16;
4897 let mut cursor_screen_y = 0u16;
4898 let mut have_cursor = false;
4899 let mut last_line_end: Option<LastLineEnd> = None;
4900 let mut last_gutter_num: Option<usize> = None;
4901 let mut trailing_empty_line_rendered = false;
4902 let mut is_on_cursor_line = false;
4903
4904 let is_empty_buffer = state.buffer.is_empty();
4905
4906 let mut last_visible_x: u16 = 0;
4908 let _view_start_line_skip = view_anchor.start_line_skip; loop {
4911 let current_view_line = if let Some(vl) = view_lines.get(view_iter_idx) {
4913 vl
4914 } else if is_empty_buffer && lines_rendered == 0 {
4915 static EMPTY_LINE: std::sync::OnceLock<ViewLine> = std::sync::OnceLock::new();
4917 EMPTY_LINE.get_or_init(|| ViewLine {
4918 text: String::new(),
4919 char_source_bytes: Vec::new(),
4920 char_styles: Vec::new(),
4921 char_visual_cols: Vec::new(),
4922 visual_to_char: Vec::new(),
4923 tab_starts: HashSet::new(),
4924 line_start: LineStart::Beginning,
4925 ends_with_newline: false,
4926 })
4927 } else {
4928 break;
4929 };
4930
4931 let line_content = current_view_line.text.clone();
4933 let line_has_newline = current_view_line.ends_with_newline;
4934 let line_char_source_bytes = ¤t_view_line.char_source_bytes;
4935 let line_char_styles = ¤t_view_line.char_styles;
4936 let line_visual_to_char = ¤t_view_line.visual_to_char;
4937 let line_tab_starts = ¤t_view_line.tab_starts;
4938 let _line_start_type = current_view_line.line_start;
4939
4940 let line_chars_for_ws: Vec<char> = line_content.chars().collect();
4944 let first_non_ws_idx = line_chars_for_ws
4945 .iter()
4946 .position(|&c| c != ' ' && c != '\n' && c != '\r');
4947 let last_non_ws_idx = line_chars_for_ws
4948 .iter()
4949 .rposition(|&c| c != ' ' && c != '\n' && c != '\r');
4950
4951 let _source_byte_at_col = |vis_col: usize| -> Option<usize> {
4953 let char_idx = line_visual_to_char.get(vis_col).copied()?;
4954 line_char_source_bytes.get(char_idx).copied().flatten()
4955 };
4956
4957 view_iter_idx += 1;
4958
4959 if lines_rendered >= visible_line_count {
4960 break;
4961 }
4962
4963 let show_line_number = should_show_line_number(current_view_line);
4966
4967 let is_continuation = !show_line_number;
4969
4970 let line_start_byte: Option<usize> = if !is_continuation {
4972 line_char_source_bytes
4973 .iter()
4974 .find_map(|opt| *opt)
4975 .or_else(|| {
4976 if line_content.is_empty()
4980 && _line_start_type == LineStart::AfterSourceNewline
4981 {
4982 Some(state.buffer.len())
4983 } else {
4984 None
4985 }
4986 })
4987 } else {
4988 None
4989 };
4990
4991 if !is_continuation {
4994 is_on_cursor_line = line_start_byte.is_some_and(|b| b == cursor_line_start_byte);
4995 }
4996
4997 let gutter_num = if let Some(byte) = line_start_byte {
4999 let n = if byte_offset_mode {
5000 byte
5001 } else {
5002 state.buffer.get_line_number(byte)
5003 };
5004 last_gutter_num = Some(n);
5005 n
5006 } else if !is_continuation {
5007 last_gutter_num.map_or(0, |n| n + 1)
5011 } else {
5012 0
5013 };
5014
5015 lines_rendered += 1;
5016
5017 let left_col = left_column;
5019
5020 let mut line_spans = Vec::new();
5022 let mut line_view_map: Vec<Option<usize>> = Vec::new();
5023 let mut last_seg_y: Option<u16> = None;
5024 let mut _last_seg_width: usize = 0;
5025
5026 let mut span_acc = SpanAccumulator::new();
5029
5030 render_left_margin(
5032 &LeftMarginContext {
5033 state,
5034 theme,
5035 is_continuation,
5036 line_start_byte,
5037 gutter_num,
5038 estimated_lines,
5039 diagnostic_lines,
5040 line_indicators,
5041 fold_indicators: &decorations.fold_indicators,
5042 cursor_line_start_byte,
5043 cursor_line_number: state.primary_cursor_line_number.value(),
5044 relative_line_numbers,
5045 show_line_numbers,
5046 byte_offset_mode,
5047 highlight_current_line,
5048 is_active,
5049 },
5050 &mut line_spans,
5051 &mut line_view_map,
5052 );
5053
5054 let mut byte_index = 0; let mut display_char_idx = 0usize; let mut col_offset = 0usize; let visible_lines_remaining = visible_line_count.saturating_sub(lines_rendered);
5064 let max_visible_chars = if line_wrap {
5065 (render_area.width as usize)
5068 .saturating_mul(visible_lines_remaining.max(1))
5069 .saturating_add(200)
5070 } else {
5071 (render_area.width as usize).saturating_add(100)
5073 };
5074 let max_chars_to_process = left_col.saturating_add(max_visible_chars);
5075
5076 let line_has_ansi = line_content.contains('\x1b');
5079 let mut ansi_parser = if line_has_ansi {
5080 Some(AnsiParser::new())
5081 } else {
5082 None
5083 };
5084 let mut visible_char_count = 0usize;
5086
5087 let mut debug_tracker = if state.debug_highlight_mode {
5089 Some(DebugSpanTracker::default())
5090 } else {
5091 None
5092 };
5093
5094 let mut first_line_byte_pos: Option<usize> = None;
5096 let mut last_line_byte_pos: Option<usize> = None;
5097
5098 let chars_iterator = line_content.chars().peekable();
5099 for ch in chars_iterator {
5100 let byte_pos = line_char_source_bytes
5103 .get(display_char_idx)
5104 .copied()
5105 .flatten();
5106
5107 if let Some(bp) = byte_pos {
5109 if first_line_byte_pos.is_none() {
5110 first_line_byte_pos = Some(bp);
5111 }
5112 last_line_byte_pos = Some(bp);
5113 }
5114
5115 let ansi_style = if let Some(ref mut parser) = ansi_parser {
5118 match parser.parse_char(ch) {
5119 Some(style) => style,
5120 None => {
5121 if let Some(bp) = byte_pos {
5125 if bp == primary_cursor_position && !have_cursor {
5126 cursor_screen_x = gutter_width as u16
5128 + col_offset.saturating_sub(left_col) as u16;
5129 cursor_screen_y = lines_rendered.saturating_sub(1) as u16;
5130 have_cursor = true;
5131 }
5132 }
5133 byte_index += ch.len_utf8();
5134 display_char_idx += 1;
5135 continue;
5137 }
5138 }
5139 } else {
5140 Style::default()
5142 };
5143
5144 if visible_char_count > max_chars_to_process {
5147 break;
5150 }
5151
5152 if col_offset >= left_col {
5154 let is_tab_start = line_tab_starts.contains(&col_offset);
5156
5157 let is_cursor = byte_pos
5161 .map(|bp| {
5162 if !cursor_positions.contains(&bp) || bp >= state.buffer.len() {
5163 return false;
5164 }
5165 let prev_char_idx = display_char_idx.saturating_sub(1);
5168 let prev_byte_pos =
5169 line_char_source_bytes.get(prev_char_idx).copied().flatten();
5170 display_char_idx == 0 || prev_byte_pos != Some(bp)
5172 })
5173 .unwrap_or(false);
5174
5175 let is_in_block_selection = block_selections.iter().any(
5179 |(start_line, start_col, end_line, end_col)| {
5180 gutter_num >= *start_line
5181 && gutter_num <= *end_line
5182 && byte_index >= *start_col
5183 && byte_index <= *end_col
5184 },
5185 );
5186
5187 let is_primary_cursor = is_cursor && byte_pos == Some(primary_cursor_position);
5193 let exclude_from_selection = is_cursor && !(is_active && is_primary_cursor);
5194
5195 let is_selected = !exclude_from_selection
5196 && (byte_pos.is_some_and(|bp| {
5197 selection_ranges.iter().any(|range| range.contains(&bp))
5198 }) || is_in_block_selection);
5199
5200 let token_style = line_char_styles
5203 .get(display_char_idx)
5204 .and_then(|s| s.as_ref());
5205
5206 let (highlight_color, highlight_theme_key, highlight_display_name) =
5208 match byte_pos {
5209 Some(bp) => span_info_at(highlight_spans, &mut hl_cursor, bp),
5210 None => (None, None, None),
5211 };
5212 let semantic_token_color = match byte_pos {
5213 Some(bp) => span_color_at(semantic_token_spans, &mut sem_cursor, bp),
5214 None => None,
5215 };
5216
5217 let CharStyleOutput {
5218 mut style,
5219 is_secondary_cursor,
5220 fg_theme_key,
5221 bg_theme_key,
5222 region: cell_region,
5223 } = compute_char_style(&CharStyleContext {
5224 byte_pos,
5225 token_style,
5226 ansi_style,
5227 is_cursor,
5228 is_selected,
5229 theme,
5230 highlight_color,
5231 highlight_theme_key,
5232 semantic_token_color,
5233 viewport_overlays,
5234 primary_cursor_position,
5235 is_active,
5236 skip_primary_cursor_reverse: session_mode,
5237 is_cursor_line_highlighted: is_on_cursor_line
5238 && highlight_current_line
5239 && is_active,
5240 current_line_bg: theme.current_line_bg,
5241 });
5242
5243 if screen_width > 0 {
5245 let screen_col = render_area.x
5246 + gutter_width as u16
5247 + col_offset.saturating_sub(left_col) as u16;
5248 let screen_row = render_area.y + lines.len() as u16;
5249 let idx = screen_row as usize * screen_width as usize + screen_col as usize;
5250 if let Some(cell) = cell_theme_map.get_mut(idx) {
5251 *cell = crate::app::types::CellThemeInfo {
5252 fg_key: fg_theme_key,
5253 bg_key: bg_theme_key,
5254 region: cell_region,
5255 syntax_category: highlight_display_name,
5256 };
5257 }
5258 }
5259
5260 let indicator_buf: String;
5264 let mut is_whitespace_indicator = false;
5265
5266 let ws_show_tab = is_tab_start && {
5270 let ws = &state.buffer_settings.whitespace;
5271 match (first_non_ws_idx, last_non_ws_idx) {
5272 (None, _) | (_, None) => ws.tabs_leading || ws.tabs_trailing,
5273 (Some(first), Some(last)) => {
5274 if display_char_idx < first {
5275 ws.tabs_leading
5276 } else if display_char_idx > last {
5277 ws.tabs_trailing
5278 } else {
5279 ws.tabs_inner
5280 }
5281 }
5282 }
5283 };
5284 let ws_show_space = ch == ' ' && !is_tab_start && {
5285 let ws = &state.buffer_settings.whitespace;
5286 match (first_non_ws_idx, last_non_ws_idx) {
5287 (None, _) | (_, None) => ws.spaces_leading || ws.spaces_trailing,
5288 (Some(first), Some(last)) => {
5289 if display_char_idx < first {
5290 ws.spaces_leading
5291 } else if display_char_idx > last {
5292 ws.spaces_trailing
5293 } else {
5294 ws.spaces_inner
5295 }
5296 }
5297 }
5298 };
5299
5300 let display_char: &str = if is_cursor && lsp_waiting && is_active {
5301 "⋯"
5302 } else if debug_tracker.is_some() && ch == '\r' {
5303 "\\r"
5305 } else if debug_tracker.is_some() && ch == '\n' {
5306 "\\n"
5308 } else if ch == '\n' {
5309 ""
5310 } else if ws_show_tab {
5311 is_whitespace_indicator = true;
5313 indicator_buf = "→".to_string();
5314 &indicator_buf
5315 } else if ws_show_space {
5316 is_whitespace_indicator = true;
5318 indicator_buf = "·".to_string();
5319 &indicator_buf
5320 } else {
5321 indicator_buf = ch.to_string();
5322 &indicator_buf
5323 };
5324
5325 if is_whitespace_indicator && !is_cursor && !is_selected {
5327 style = style.fg(theme.whitespace_indicator_fg);
5328 }
5329
5330 if let Some(bp) = byte_pos {
5331 if let Some(vtexts) = virtual_text_lookup.get(&bp) {
5332 for vtext in vtexts
5333 .iter()
5334 .filter(|v| v.position == VirtualTextPosition::BeforeChar)
5335 {
5336 span_acc.flush(&mut line_spans, &mut line_view_map);
5338 let extra_space = if ch == '\n' { " " } else { "" };
5340 let text_with_space = format!("{}{} ", extra_space, vtext.text);
5341 push_span_with_map(
5342 &mut line_spans,
5343 &mut line_view_map,
5344 text_with_space,
5345 vtext.style,
5346 None,
5347 );
5348 }
5349 }
5350 }
5351
5352 if !display_char.is_empty() {
5353 if let Some(ref mut tracker) = debug_tracker {
5355 span_acc.flush(&mut line_spans, &mut line_view_map);
5357 let opening_tags = tracker.get_opening_tags(
5358 byte_pos,
5359 highlight_spans,
5360 viewport_overlays,
5361 );
5362 for tag in opening_tags {
5363 push_debug_tag(&mut line_spans, &mut line_view_map, tag);
5364 }
5365 }
5366
5367 if debug_tracker.is_some() {
5369 if let Some(bp) = byte_pos {
5370 push_debug_tag(
5371 &mut line_spans,
5372 &mut line_view_map,
5373 format!("[{}]", bp),
5374 );
5375 }
5376 }
5377
5378 for c in display_char.chars() {
5381 span_acc.push(c, style, byte_pos, &mut line_spans, &mut line_view_map);
5382 }
5383
5384 if let Some(ref mut tracker) = debug_tracker {
5387 span_acc.flush(&mut line_spans, &mut line_view_map);
5389 let next_byte_pos = byte_pos.map(|bp| bp + ch.len_utf8());
5391 let closing_tags = tracker.get_closing_tags(next_byte_pos);
5392 for tag in closing_tags {
5393 push_debug_tag(&mut line_spans, &mut line_view_map, tag);
5394 }
5395 }
5396 }
5397
5398 if !have_cursor {
5401 if let Some(bp) = byte_pos {
5402 if bp == primary_cursor_position && char_width(ch) == 0 {
5403 cursor_screen_x = gutter_width as u16
5405 + col_offset.saturating_sub(left_col) as u16;
5406 cursor_screen_y = lines.len() as u16;
5407 have_cursor = true;
5408 }
5409 }
5410 }
5411
5412 if let Some(bp) = byte_pos {
5413 if let Some(vtexts) = virtual_text_lookup.get(&bp) {
5414 for vtext in vtexts
5415 .iter()
5416 .filter(|v| v.position == VirtualTextPosition::AfterChar)
5417 {
5418 let text_with_space = format!(" {}", vtext.text);
5419 push_span_with_map(
5420 &mut line_spans,
5421 &mut line_view_map,
5422 text_with_space,
5423 vtext.style,
5424 None,
5425 );
5426 }
5427 }
5428 }
5429
5430 if is_cursor && ch == '\n' {
5431 let should_add_indicator =
5432 if is_active { is_secondary_cursor } else { true };
5433 if should_add_indicator {
5434 span_acc.flush(&mut line_spans, &mut line_view_map);
5437 let cursor_style = if is_active {
5438 Style::default()
5439 .fg(theme.editor_fg)
5440 .bg(theme.editor_bg)
5441 .add_modifier(Modifier::REVERSED)
5442 } else {
5443 Style::default()
5444 .fg(theme.editor_fg)
5445 .bg(theme.inactive_cursor)
5446 };
5447 push_span_with_map(
5448 &mut line_spans,
5449 &mut line_view_map,
5450 " ".to_string(),
5451 cursor_style,
5452 byte_pos,
5453 );
5454 }
5455 }
5456 }
5457
5458 byte_index += ch.len_utf8();
5459 display_char_idx += 1; let ch_width = char_width(ch);
5463 col_offset += ch_width;
5464 visible_char_count += ch_width;
5465 }
5466
5467 span_acc.flush(&mut line_spans, &mut line_view_map);
5469
5470 let content_is_empty = line_content.is_empty();
5474 if line_spans.is_empty() || !line_wrap || content_is_empty {
5475 last_seg_y = Some(lines.len() as u16);
5476 }
5477
5478 if !line_has_newline {
5479 let line_len_chars = line_content.chars().count();
5480
5481 let last_char_idx = line_len_chars.saturating_sub(1);
5483 let after_last_char_idx = line_len_chars;
5484
5485 let last_char_buf_pos =
5486 line_char_source_bytes.get(last_char_idx).copied().flatten();
5487 let after_last_char_buf_pos = line_char_source_bytes
5488 .get(after_last_char_idx)
5489 .copied()
5490 .flatten();
5491
5492 let cursor_at_end = cursor_positions.iter().any(|&pos| {
5493 let matches_after = after_last_char_buf_pos.is_some_and(|bp| pos == bp);
5496 let expected_after_pos = last_char_buf_pos
5501 .map(|p| p + 1)
5502 .unwrap_or(state.buffer.len());
5503 let matches_fallback =
5504 after_last_char_buf_pos.is_none() && pos == expected_after_pos;
5505
5506 matches_after || matches_fallback
5507 });
5508
5509 if cursor_at_end {
5510 let is_primary_at_end = after_last_char_buf_pos
5512 .is_some_and(|bp| bp == primary_cursor_position)
5513 || (after_last_char_buf_pos.is_none()
5514 && primary_cursor_position >= state.buffer.len());
5515
5516 if let Some(seg_y) = last_seg_y {
5518 if is_primary_at_end {
5519 cursor_screen_x = if line_len_chars == 0 {
5524 gutter_width as u16
5525 } else {
5526 gutter_width as u16 + col_offset.saturating_sub(left_col) as u16
5529 };
5530 cursor_screen_y = seg_y;
5531 have_cursor = true;
5532 }
5533 }
5534
5535 let should_add_indicator = if is_active {
5539 software_cursor_only || !is_primary_at_end
5540 } else {
5541 true
5542 };
5543 if should_add_indicator {
5544 let cursor_style = if is_active {
5545 Style::default()
5546 .fg(theme.editor_fg)
5547 .bg(theme.editor_bg)
5548 .add_modifier(Modifier::REVERSED)
5549 } else {
5550 Style::default()
5551 .fg(theme.editor_fg)
5552 .bg(theme.inactive_cursor)
5553 };
5554 push_span_with_map(
5555 &mut line_spans,
5556 &mut line_view_map,
5557 " ".to_string(),
5558 cursor_style,
5559 None,
5560 );
5561 }
5562 }
5563 }
5564
5565 let current_y = lines.len() as u16;
5568 last_seg_y = Some(current_y);
5569
5570 if !line_spans.is_empty() {
5571 let mut nearest_fallback: Option<(u16, usize)> = None; for (screen_x, source_offset) in line_view_map.iter().enumerate() {
5582 if let Some(src) = source_offset {
5583 if *src == primary_cursor_position && !have_cursor {
5585 cursor_screen_x = screen_x as u16;
5586 cursor_screen_y = current_y;
5587 have_cursor = true;
5588 }
5589 if !have_cursor && *src >= primary_cursor_position {
5591 let dist = *src - primary_cursor_position;
5592 if nearest_fallback.is_none() || dist < nearest_fallback.unwrap().1 {
5593 nearest_fallback = Some((screen_x as u16, dist));
5594 }
5595 }
5596 last_visible_x = screen_x as u16;
5597 }
5598 }
5599 if !have_cursor {
5601 if let Some((fallback_x, _)) = nearest_fallback {
5602 cursor_screen_x = fallback_x;
5603 cursor_screen_y = current_y;
5604 have_cursor = true;
5605 }
5606 }
5607 }
5608
5609 if let Some(lsb) = line_start_byte {
5612 if let Some((message, diag_style)) = decorations.diagnostic_inline_texts.get(&lsb) {
5613 let content_width =
5614 render_area.width.saturating_sub(gutter_width as u16) as usize;
5615 let used = visible_char_count;
5616 let available = content_width.saturating_sub(used);
5617 let gap = 2usize;
5618 let min_text = 10usize;
5619
5620 if available > gap + min_text {
5621 let max_chars = available - gap;
5623 let display: String = if message.chars().count() > max_chars {
5624 let truncated: String =
5625 message.chars().take(max_chars.saturating_sub(1)).collect();
5626 format!("{}…", truncated)
5627 } else {
5628 message.clone()
5629 };
5630 let display_width = display.chars().count();
5631
5632 let padding = available.saturating_sub(display_width);
5634 let cursor_line_active =
5635 is_on_cursor_line && highlight_current_line && is_active;
5636 if padding > 0 {
5637 let pad_style = if cursor_line_active {
5638 Style::default().bg(theme.current_line_bg)
5639 } else {
5640 Style::default()
5641 };
5642 push_span_with_map(
5643 &mut line_spans,
5644 &mut line_view_map,
5645 " ".repeat(padding),
5646 pad_style,
5647 None,
5648 );
5649 visible_char_count += padding;
5650 }
5651
5652 let effective_diag_style = if cursor_line_active && diag_style.bg.is_none()
5654 {
5655 diag_style.bg(theme.current_line_bg)
5656 } else {
5657 *diag_style
5658 };
5659 push_span_with_map(
5660 &mut line_spans,
5661 &mut line_view_map,
5662 display,
5663 effective_diag_style,
5664 None,
5665 );
5666 visible_char_count += display_width;
5667 }
5668 }
5669 }
5670
5671 if !line_wrap {
5674 let content_width = render_area.width.saturating_sub(gutter_width as u16) as usize;
5676 let remaining_cols = content_width.saturating_sub(visible_char_count);
5677
5678 if remaining_cols > 0 {
5679 let fill_style: Option<Style> = if let (Some(start), Some(end)) =
5688 (first_line_byte_pos, last_line_byte_pos)
5689 {
5690 viewport_overlays
5691 .iter()
5692 .filter(|(overlay, range)| {
5693 overlay.extend_to_line_end
5694 && range.start <= end
5695 && range.end > start
5696 })
5697 .max_by_key(|(o, _)| o.priority)
5698 .and_then(|(overlay, _)| {
5699 match &overlay.face {
5700 crate::view::overlay::OverlayFace::Background { color } => {
5701 Some(Style::default().fg(*color).bg(*color))
5703 }
5704 crate::view::overlay::OverlayFace::Style { style } => {
5705 style.bg.map(|bg| Style::default().fg(bg).bg(bg))
5708 }
5709 crate::view::overlay::OverlayFace::ThemedStyle {
5710 fallback_style,
5711 bg_theme,
5712 ..
5713 } => {
5714 let bg = bg_theme
5716 .as_ref()
5717 .and_then(|key| theme.resolve_theme_key(key))
5718 .or(fallback_style.bg);
5719 bg.map(|bg| Style::default().fg(bg).bg(bg))
5720 }
5721 _ => None,
5722 }
5723 })
5724 } else {
5725 None
5726 };
5727
5728 if let Some(fill_bg) = fill_style {
5729 let fill_text = " ".repeat(remaining_cols);
5730 push_span_with_map(
5731 &mut line_spans,
5732 &mut line_view_map,
5733 fill_text,
5734 fill_bg,
5735 None,
5736 );
5737 }
5738 }
5739 }
5740
5741 if is_on_cursor_line && highlight_current_line && is_active {
5745 let content_width = render_area.width.saturating_sub(gutter_width as u16) as usize;
5746 let remaining_cols = content_width.saturating_sub(visible_char_count);
5747 if remaining_cols > 0 {
5748 span_acc.flush(&mut line_spans, &mut line_view_map);
5749 line_spans.push(Span::styled(
5750 " ".repeat(remaining_cols),
5751 Style::default().bg(theme.current_line_bg),
5752 ));
5753 }
5754 }
5755
5756 let prev_line_end_byte = view_line_mappings
5758 .last()
5759 .map(|prev: &ViewLineMapping| prev.line_end_byte)
5760 .unwrap_or(0);
5761
5762 let line_end_byte = if current_view_line.ends_with_newline {
5764 current_view_line
5766 .char_source_bytes
5767 .iter()
5768 .rev()
5769 .find_map(|m| *m)
5770 .unwrap_or(prev_line_end_byte)
5771 } else {
5772 if let Some((char_idx, &Some(last_byte_start))) = current_view_line
5774 .char_source_bytes
5775 .iter()
5776 .enumerate()
5777 .rev()
5778 .find(|(_, m)| m.is_some())
5779 {
5780 if let Some(last_char) = current_view_line.text.chars().nth(char_idx) {
5782 last_byte_start + last_char.len_utf8()
5783 } else {
5784 last_byte_start
5785 }
5786 } else if matches!(current_view_line.line_start, LineStart::AfterSourceNewline)
5787 && prev_line_end_byte + 2 >= state.buffer.len()
5788 {
5789 state.buffer.len()
5792 } else {
5793 prev_line_end_byte
5797 }
5798 };
5799
5800 let content_map = if line_view_map.len() >= gutter_width {
5803 line_view_map[gutter_width..].to_vec()
5804 } else {
5805 Vec::new()
5806 };
5807 view_line_mappings.push(ViewLineMapping {
5808 char_source_bytes: content_map.clone(),
5809 visual_to_char: (0..content_map.len()).collect(),
5810 line_end_byte,
5811 });
5812
5813 let line_was_empty = line_spans.is_empty();
5815 lines.push(Line::from(line_spans));
5816
5817 let is_iterator_trailing_empty = line_content.is_empty()
5823 && !line_has_newline
5824 && _line_start_type == LineStart::AfterSourceNewline;
5825 if is_iterator_trailing_empty {
5826 trailing_empty_line_rendered = true;
5827 }
5828
5829 if let Some(y) = last_seg_y {
5832 let end_x = if line_was_empty {
5836 gutter_width as u16
5837 } else {
5838 last_visible_x.saturating_add(1)
5839 };
5840 let line_len_chars = line_content.chars().count();
5841
5842 if !is_iterator_trailing_empty {
5845 last_line_end = Some(LastLineEnd {
5846 pos: (end_x, y),
5847 terminated_with_newline: line_has_newline,
5848 });
5849 }
5850
5851 if line_has_newline && line_len_chars > 0 {
5852 let newline_idx = line_len_chars.saturating_sub(1);
5853 if let Some(Some(src_newline)) = line_char_source_bytes.get(newline_idx) {
5854 if *src_newline == primary_cursor_position {
5855 if line_len_chars == 1 {
5859 cursor_screen_x = gutter_width as u16;
5861 cursor_screen_y = y;
5862 } else {
5863 cursor_screen_x = end_x;
5866 cursor_screen_y = y;
5867 }
5868 have_cursor = true;
5869 }
5870 }
5871 }
5872 }
5873
5874 if lines_rendered >= visible_line_count {
5875 break;
5876 }
5877 }
5878
5879 if let Some(ref end) = last_line_end {
5883 if end.terminated_with_newline
5884 && lines_rendered < visible_line_count
5885 && !trailing_empty_line_rendered
5886 {
5887 let mut implicit_line_spans = Vec::new();
5889 let implicit_line_byte = state.buffer.len();
5891 let implicit_gutter_num = if byte_offset_mode {
5892 implicit_line_byte
5893 } else {
5894 last_gutter_num.map_or(0, |n| n + 1)
5895 };
5896
5897 let implicit_is_cursor_line = implicit_line_byte == cursor_line_start_byte;
5898 let implicit_cursor_bg =
5899 if implicit_is_cursor_line && highlight_current_line && is_active {
5900 Some(theme.current_line_bg)
5901 } else {
5902 None
5903 };
5904
5905 if state.margins.left_config.enabled {
5906 if decorations.diagnostic_lines.contains(&implicit_line_byte) {
5908 let mut style = Style::default().fg(ratatui::style::Color::Red);
5909 if let Some(bg) = implicit_cursor_bg {
5910 style = style.bg(bg);
5911 }
5912 implicit_line_spans.push(Span::styled("●", style));
5913 } else {
5914 let mut style = Style::default();
5915 if let Some(bg) = implicit_cursor_bg {
5916 style = style.bg(bg);
5917 }
5918 implicit_line_spans.push(Span::styled(" ", style));
5919 }
5920
5921 let rendered_text = if byte_offset_mode && show_line_numbers {
5923 format!(
5924 "{:>width$}",
5925 implicit_gutter_num,
5926 width = state.margins.left_config.width
5927 )
5928 } else {
5929 let estimated_lines = state.buffer.line_count().unwrap_or(
5930 (state.buffer.len() / state.buffer.estimated_line_length()).max(1),
5931 );
5932 let margin_content = state.margins.render_line(
5933 implicit_gutter_num,
5934 crate::view::margin::MarginPosition::Left,
5935 estimated_lines,
5936 show_line_numbers,
5937 );
5938 margin_content.render(state.margins.left_config.width).0
5939 };
5940 let mut margin_style = Style::default().fg(theme.line_number_fg);
5941 if let Some(bg) = implicit_cursor_bg {
5942 margin_style = margin_style.bg(bg);
5943 }
5944 implicit_line_spans.push(Span::styled(rendered_text, margin_style));
5945
5946 if state.margins.left_config.show_separator {
5948 let mut sep_style = Style::default().fg(theme.line_number_fg);
5949 if let Some(bg) = implicit_cursor_bg {
5950 sep_style = sep_style.bg(bg);
5951 }
5952 implicit_line_spans.push(Span::styled(
5953 state.margins.left_config.separator.to_string(),
5954 sep_style,
5955 ));
5956 }
5957 }
5958
5959 if let Some(bg) = implicit_cursor_bg {
5961 let gutter_w = if state.margins.left_config.enabled {
5962 state.margins.left_total_width()
5963 } else {
5964 0
5965 };
5966 let content_width = render_area.width.saturating_sub(gutter_w as u16) as usize;
5967 if content_width > 0 {
5968 implicit_line_spans.push(Span::styled(
5969 " ".repeat(content_width),
5970 Style::default().bg(bg),
5971 ));
5972 }
5973 }
5974
5975 let implicit_y = lines.len() as u16;
5976 lines.push(Line::from(implicit_line_spans));
5977 lines_rendered += 1;
5978
5979 let buffer_len = state.buffer.len();
5982
5983 view_line_mappings.push(ViewLineMapping {
5984 char_source_bytes: Vec::new(),
5985 visual_to_char: Vec::new(),
5986 line_end_byte: buffer_len,
5987 });
5988
5989 if primary_cursor_position == state.buffer.len() && !have_cursor {
5995 cursor_screen_x = gutter_width as u16;
5996 cursor_screen_y = implicit_y;
5997 have_cursor = true;
5998 }
5999 }
6000 }
6001
6002 if let Some(ref end) = last_line_end {
6009 if end.terminated_with_newline {
6010 let last_mapped_byte = view_line_mappings
6011 .last()
6012 .map(|m| m.line_end_byte)
6013 .unwrap_or(0);
6014 let near_buffer_end = last_mapped_byte + 2 >= state.buffer.len();
6015 let already_mapped = view_line_mappings.last().is_some_and(|m| {
6016 m.char_source_bytes.is_empty() && m.line_end_byte == state.buffer.len()
6017 });
6018 if near_buffer_end && !already_mapped {
6019 view_line_mappings.push(ViewLineMapping {
6020 char_source_bytes: Vec::new(),
6021 visual_to_char: Vec::new(),
6022 line_end_byte: state.buffer.len(),
6023 });
6024 }
6025 }
6026 }
6027
6028 if show_tilde {
6038 let eof_fg = dim_color_for_tilde(theme.line_number_fg);
6039 let eof_style = Style::default().fg(eof_fg);
6040 while lines.len() < render_area.height as usize {
6041 let tilde_line = format!(
6043 "~{}",
6044 " ".repeat(render_area.width.saturating_sub(1) as usize)
6045 );
6046 lines.push(Line::styled(tilde_line, eof_style));
6047 }
6048 }
6049
6050 LineRenderOutput {
6051 lines,
6052 cursor: have_cursor.then_some((cursor_screen_x, cursor_screen_y)),
6053 last_line_end,
6054 content_lines_rendered: lines_rendered,
6055 view_line_mappings,
6056 }
6057 }
6058
6059 fn resolve_cursor_fallback(
6060 current_cursor: Option<(u16, u16)>,
6061 primary_cursor_position: usize,
6062 buffer_len: usize,
6063 buffer_ends_with_newline: bool,
6064 last_line_end: Option<LastLineEnd>,
6065 lines_rendered: usize,
6066 gutter_width: usize,
6067 ) -> Option<(u16, u16)> {
6068 if current_cursor.is_some() || primary_cursor_position != buffer_len {
6069 return current_cursor;
6070 }
6071
6072 if buffer_ends_with_newline {
6073 if let Some(end) = last_line_end {
6074 let y = if end.terminated_with_newline {
6080 end.pos.1.saturating_add(1)
6081 } else {
6082 end.pos.1
6083 };
6084 return Some((gutter_width as u16, y));
6085 }
6086 return Some((gutter_width as u16, lines_rendered as u16));
6087 }
6088
6089 last_line_end.map(|end| end.pos)
6090 }
6091
6092 #[allow(clippy::too_many_arguments)]
6096 fn compute_buffer_layout(
6097 state: &mut EditorState,
6098 cursors: &crate::model::cursor::Cursors,
6099 viewport: &mut crate::view::viewport::Viewport,
6100 folds: &mut FoldManager,
6101 area: Rect,
6102 is_active: bool,
6103 theme: &crate::view::theme::Theme,
6104 lsp_waiting: bool,
6105 view_mode: ViewMode,
6106 compose_width: Option<u16>,
6107 view_transform: Option<ViewTransformPayload>,
6108 estimated_line_length: usize,
6109 highlight_context_bytes: usize,
6110 relative_line_numbers: bool,
6111 use_terminal_bg: bool,
6112 session_mode: bool,
6113 software_cursor_only: bool,
6114 show_line_numbers: bool,
6115 highlight_current_line: bool,
6116 diagnostics_inline_text: bool,
6117 show_tilde: bool,
6118 cell_theme_map: Option<(&mut Vec<crate::app::types::CellThemeInfo>, u16)>,
6119 ) -> BufferLayoutOutput {
6120 let _span = tracing::trace_span!("compute_buffer_layout").entered();
6121
6122 state.margins.configure_for_line_numbers(show_line_numbers);
6124
6125 let effective_editor_bg = if use_terminal_bg {
6127 ratatui::style::Color::Reset
6128 } else {
6129 theme.editor_bg
6130 };
6131
6132 let line_wrap = viewport.line_wrap_enabled;
6133
6134 let overlay_count = state.overlays.all().len();
6135 if overlay_count > 0 {
6136 tracing::trace!("render_content: {} overlays present", overlay_count);
6137 }
6138
6139 let visible_count = viewport.visible_line_count();
6140
6141 let buffer_len = state.buffer.len();
6142 let byte_offset_mode = state.buffer.line_count().is_none();
6143 let estimated_lines = if byte_offset_mode {
6144 buffer_len.max(1)
6147 } else {
6148 state.buffer.line_count().unwrap_or(1)
6149 };
6150 state
6151 .margins
6152 .update_width_for_buffer(estimated_lines, show_line_numbers);
6153 let gutter_width = state.margins.left_total_width();
6154
6155 let compose_layout = Self::calculate_compose_layout(area, &view_mode, compose_width);
6156 let render_area = compose_layout.render_area;
6157
6158 let view_transform_for_rebuild = view_transform.clone();
6160
6161 let view_data = {
6162 let _span = tracing::trace_span!("build_view_data").entered();
6163 Self::build_view_data(
6164 state,
6165 viewport,
6166 view_transform,
6167 estimated_line_length,
6168 visible_count,
6169 line_wrap,
6170 render_area.width as usize,
6171 gutter_width,
6172 &view_mode,
6173 folds,
6174 theme,
6175 )
6176 };
6177
6178 let sync_scrolled = if viewport.sync_scroll_to_end {
6181 viewport.sync_scroll_to_end = false;
6182 viewport.scroll_to_end_of_view(&view_data.lines)
6183 } else {
6184 false
6185 };
6186
6187 let (view_data, view_transform_for_rebuild) = if sync_scrolled {
6190 viewport.top_view_line_offset = 0;
6191 let rebuilt = Self::build_view_data(
6192 state,
6193 viewport,
6194 view_transform_for_rebuild,
6195 estimated_line_length,
6196 visible_count,
6197 line_wrap,
6198 render_area.width as usize,
6199 gutter_width,
6200 &view_mode,
6201 folds,
6202 theme,
6203 );
6204 viewport.scroll_to_end_of_view(&rebuilt.lines);
6205 (rebuilt, None)
6206 } else {
6207 (view_data, Some(view_transform_for_rebuild))
6208 };
6209
6210 let primary = *cursors.primary();
6212 let scrolled = viewport.ensure_visible_in_layout(&view_data.lines, &primary, gutter_width);
6213
6214 let view_data = if scrolled {
6217 if let Some(vt) = view_transform_for_rebuild {
6218 viewport.top_view_line_offset = 0;
6219 let rebuilt = Self::build_view_data(
6220 state,
6221 viewport,
6222 vt,
6223 estimated_line_length,
6224 visible_count,
6225 line_wrap,
6226 render_area.width as usize,
6227 gutter_width,
6228 &view_mode,
6229 folds,
6230 theme,
6231 );
6232 let _ = viewport.ensure_visible_in_layout(&rebuilt.lines, &primary, gutter_width);
6233 rebuilt
6234 } else {
6235 view_data
6236 }
6237 } else {
6238 view_data
6239 };
6240
6241 let view_anchor = Self::calculate_view_anchor(&view_data.lines, viewport.top_byte);
6242
6243 let selection = Self::selection_context(state, cursors);
6244
6245 tracing::trace!(
6246 "Rendering buffer with {} cursors at positions: {:?}, primary at {}, is_active: {}, buffer_len: {}",
6247 selection.cursor_positions.len(),
6248 selection.cursor_positions,
6249 selection.primary_cursor_position,
6250 is_active,
6251 state.buffer.len()
6252 );
6253
6254 if !selection.cursor_positions.is_empty()
6255 && !selection
6256 .cursor_positions
6257 .contains(&selection.primary_cursor_position)
6258 {
6259 tracing::warn!(
6260 "Primary cursor position {} not found in cursor_positions list: {:?}",
6261 selection.primary_cursor_position,
6262 selection.cursor_positions
6263 );
6264 }
6265
6266 let adjusted_visible_count = Self::fold_adjusted_visible_count(
6267 &state.buffer,
6268 &state.marker_list,
6269 folds,
6270 viewport.top_byte,
6271 visible_count,
6272 );
6273
6274 let _ = state
6278 .buffer
6279 .populate_line_cache(viewport.top_byte, adjusted_visible_count);
6280
6281 let viewport_start = viewport.top_byte;
6282 let viewport_end = Self::calculate_viewport_end(
6283 state,
6284 viewport_start,
6285 estimated_line_length,
6286 adjusted_visible_count,
6287 );
6288
6289 let decorations = Self::decoration_context(
6290 state,
6291 viewport_start,
6292 viewport_end,
6293 selection.primary_cursor_position,
6294 folds,
6295 theme,
6296 highlight_context_bytes,
6297 &view_mode,
6298 diagnostics_inline_text,
6299 );
6300
6301 let calculated_offset = viewport.top_view_line_offset;
6302
6303 tracing::trace!(
6304 top_byte = viewport.top_byte,
6305 top_view_line_offset = viewport.top_view_line_offset,
6306 calculated_offset,
6307 view_data_lines = view_data.lines.len(),
6308 "view line offset calculation"
6309 );
6310 let (view_lines_to_render, adjusted_view_anchor) =
6311 if calculated_offset > 0 && calculated_offset < view_data.lines.len() {
6312 let sliced = &view_data.lines[calculated_offset..];
6313 let adjusted_anchor = Self::calculate_view_anchor(sliced, viewport.top_byte);
6314 (sliced, adjusted_anchor)
6315 } else {
6316 (&view_data.lines[..], view_anchor)
6317 };
6318
6319 let mut dummy_map = Vec::new();
6321 let (map_ref, sw) = match cell_theme_map {
6322 Some((map, w)) => (map, w),
6323 None => (&mut dummy_map, 0u16),
6324 };
6325
6326 let render_output = Self::render_view_lines(LineRenderInput {
6327 state,
6328 theme,
6329 view_lines: view_lines_to_render,
6330 view_anchor: adjusted_view_anchor,
6331 render_area,
6332 gutter_width,
6333 selection: &selection,
6334 decorations: &decorations,
6335 visible_line_count: visible_count,
6336 lsp_waiting,
6337 is_active,
6338 line_wrap,
6339 estimated_lines,
6340 left_column: viewport.left_column,
6341 relative_line_numbers,
6342 session_mode,
6343 software_cursor_only,
6344 show_line_numbers,
6345 byte_offset_mode,
6346 show_tilde,
6347 highlight_current_line,
6348 cell_theme_map: map_ref,
6349 screen_width: sw,
6350 });
6351
6352 let view_line_mappings = render_output.view_line_mappings.clone();
6353
6354 let buffer_ends_with_newline = if !state.buffer.is_empty() {
6355 let last_char = state.get_text_range(state.buffer.len() - 1, state.buffer.len());
6356 last_char == "\n"
6357 } else {
6358 false
6359 };
6360
6361 BufferLayoutOutput {
6362 view_line_mappings,
6363 render_output,
6364 render_area,
6365 compose_layout,
6366 effective_editor_bg,
6367 view_mode,
6368 left_column: viewport.left_column,
6369 gutter_width,
6370 buffer_ends_with_newline,
6371 selection,
6372 }
6373 }
6374
6375 #[allow(clippy::too_many_arguments)]
6377 fn draw_buffer_in_split(
6378 frame: &mut Frame,
6379 state: &EditorState,
6380 cursors: &crate::model::cursor::Cursors,
6381 layout_output: BufferLayoutOutput,
6382 event_log: Option<&mut EventLog>,
6383 area: Rect,
6384 is_active: bool,
6385 theme: &crate::view::theme::Theme,
6386 ansi_background: Option<&AnsiBackground>,
6387 background_fade: f32,
6388 hide_cursor: bool,
6389 software_cursor_only: bool,
6390 rulers: &[usize],
6391 compose_column_guides: Option<Vec<u16>>,
6392 ) {
6393 let render_area = layout_output.render_area;
6394 let effective_editor_bg = layout_output.effective_editor_bg;
6395 let gutter_width = layout_output.gutter_width;
6396 let starting_line_num = 0; Self::render_compose_margins(
6399 frame,
6400 area,
6401 &layout_output.compose_layout,
6402 &layout_output.view_mode,
6403 theme,
6404 effective_editor_bg,
6405 );
6406
6407 let mut lines = layout_output.render_output.lines;
6408 let background_x_offset = layout_output.left_column;
6409
6410 if let Some(bg) = ansi_background {
6411 Self::apply_background_to_lines(
6412 &mut lines,
6413 render_area.width,
6414 bg,
6415 effective_editor_bg,
6416 theme.editor_fg,
6417 background_fade,
6418 background_x_offset,
6419 starting_line_num,
6420 );
6421 }
6422
6423 frame.render_widget(Clear, render_area);
6424 let editor_block = Block::default()
6425 .borders(Borders::NONE)
6426 .style(Style::default().bg(effective_editor_bg));
6427 frame.render_widget(Paragraph::new(lines).block(editor_block), render_area);
6428
6429 let cursor = Self::resolve_cursor_fallback(
6430 layout_output.render_output.cursor,
6431 layout_output.selection.primary_cursor_position,
6432 state.buffer.len(),
6433 layout_output.buffer_ends_with_newline,
6434 layout_output.render_output.last_line_end,
6435 layout_output.render_output.content_lines_rendered,
6436 gutter_width,
6437 );
6438
6439 let cursor_screen_pos = if is_active && state.show_cursors && !hide_cursor {
6440 cursor.map(|(cx, cy)| {
6441 let screen_x = render_area.x.saturating_add(cx);
6442 let max_y = render_area.height.saturating_sub(1);
6443 let screen_y = render_area.y.saturating_add(cy.min(max_y));
6444 (screen_x, screen_y)
6445 })
6446 } else {
6447 None
6448 };
6449
6450 if !rulers.is_empty() {
6452 let ruler_cols: Vec<u16> = rulers.iter().map(|&r| r as u16).collect();
6453 Self::render_ruler_bg(
6454 frame,
6455 &ruler_cols,
6456 theme.ruler_bg,
6457 render_area,
6458 gutter_width,
6459 layout_output.render_output.content_lines_rendered,
6460 layout_output.left_column,
6461 );
6462 }
6463
6464 if let Some(guides) = compose_column_guides {
6466 let guide_style = Style::default()
6467 .fg(theme.line_number_fg)
6468 .add_modifier(Modifier::DIM);
6469 Self::render_column_guides(
6470 frame,
6471 &guides,
6472 guide_style,
6473 render_area,
6474 gutter_width,
6475 layout_output.render_output.content_lines_rendered,
6476 0,
6477 );
6478 }
6479
6480 if let Some((screen_x, screen_y)) = cursor_screen_pos {
6481 frame.set_cursor_position((screen_x, screen_y));
6482
6483 if software_cursor_only {
6489 let buf = frame.buffer_mut();
6490 let area = buf.area;
6491 if screen_x < area.x + area.width && screen_y < area.y + area.height {
6492 let cell = &mut buf[(screen_x, screen_y)];
6493 if !cell.modifier.contains(Modifier::REVERSED) {
6497 cell.set_char(' ');
6498 cell.fg = theme.editor_fg;
6499 cell.bg = theme.editor_bg;
6500 cell.modifier.insert(Modifier::REVERSED);
6501 }
6502 }
6503 }
6504
6505 if let Some(event_log) = event_log {
6506 let cursor_pos = cursors.primary().position;
6507 let buffer_len = state.buffer.len();
6508 event_log.log_render_state(cursor_pos, screen_x, screen_y, buffer_len);
6509 }
6510 }
6511 }
6512
6513 #[allow(clippy::too_many_arguments)]
6517 fn render_buffer_in_split(
6518 frame: &mut Frame,
6519 state: &mut EditorState,
6520 cursors: &crate::model::cursor::Cursors,
6521 viewport: &mut crate::view::viewport::Viewport,
6522 folds: &mut FoldManager,
6523 event_log: Option<&mut EventLog>,
6524 area: Rect,
6525 is_active: bool,
6526 theme: &crate::view::theme::Theme,
6527 ansi_background: Option<&AnsiBackground>,
6528 background_fade: f32,
6529 lsp_waiting: bool,
6530 view_mode: ViewMode,
6531 compose_width: Option<u16>,
6532 compose_column_guides: Option<Vec<u16>>,
6533 view_transform: Option<ViewTransformPayload>,
6534 estimated_line_length: usize,
6535 highlight_context_bytes: usize,
6536 _buffer_id: BufferId,
6537 hide_cursor: bool,
6538 relative_line_numbers: bool,
6539 use_terminal_bg: bool,
6540 session_mode: bool,
6541 software_cursor_only: bool,
6542 rulers: &[usize],
6543 show_line_numbers: bool,
6544 highlight_current_line: bool,
6545 diagnostics_inline_text: bool,
6546 show_tilde: bool,
6547 cell_theme_map: &mut Vec<crate::app::types::CellThemeInfo>,
6548 screen_width: u16,
6549 ) -> Vec<ViewLineMapping> {
6550 let layout_output = Self::compute_buffer_layout(
6551 state,
6552 cursors,
6553 viewport,
6554 folds,
6555 area,
6556 is_active,
6557 theme,
6558 lsp_waiting,
6559 view_mode.clone(),
6560 compose_width,
6561 view_transform,
6562 estimated_line_length,
6563 highlight_context_bytes,
6564 relative_line_numbers,
6565 use_terminal_bg,
6566 session_mode,
6567 software_cursor_only,
6568 show_line_numbers,
6569 highlight_current_line,
6570 diagnostics_inline_text,
6571 show_tilde,
6572 Some((cell_theme_map, screen_width)),
6573 );
6574
6575 let view_line_mappings = layout_output.view_line_mappings.clone();
6576
6577 Self::draw_buffer_in_split(
6578 frame,
6579 state,
6580 cursors,
6581 layout_output,
6582 event_log,
6583 area,
6584 is_active,
6585 theme,
6586 ansi_background,
6587 background_fade,
6588 hide_cursor,
6589 software_cursor_only,
6590 rulers,
6591 compose_column_guides,
6592 );
6593
6594 view_line_mappings
6595 }
6596
6597 fn render_column_guides(
6600 frame: &mut Frame,
6601 columns: &[u16],
6602 style: Style,
6603 render_area: Rect,
6604 gutter_width: usize,
6605 content_height: usize,
6606 left_column: usize,
6607 ) {
6608 let guide_height = content_height.min(render_area.height as usize);
6609 for &col in columns {
6610 let Some(scrolled_col) = (col as usize).checked_sub(left_column) else {
6612 continue;
6613 };
6614 let guide_x = render_area.x + gutter_width as u16 + scrolled_col as u16;
6615 if guide_x < render_area.x + render_area.width {
6616 for row in 0..guide_height {
6617 let cell = &mut frame.buffer_mut()[(guide_x, render_area.y + row as u16)];
6618 cell.set_symbol("│");
6619 if let Some(fg) = style.fg {
6620 cell.set_fg(fg);
6621 }
6622 if !style.add_modifier.is_empty() {
6623 cell.set_style(Style::default().add_modifier(style.add_modifier));
6624 }
6625 }
6626 }
6627 }
6628 }
6629
6630 fn render_ruler_bg(
6634 frame: &mut Frame,
6635 columns: &[u16],
6636 color: Color,
6637 render_area: Rect,
6638 gutter_width: usize,
6639 content_height: usize,
6640 left_column: usize,
6641 ) {
6642 let guide_height = content_height.min(render_area.height as usize);
6643 for &col in columns {
6644 let Some(scrolled_col) = (col as usize).checked_sub(left_column) else {
6645 continue;
6646 };
6647 let guide_x = render_area.x + gutter_width as u16 + scrolled_col as u16;
6648 if guide_x < render_area.x + render_area.width {
6649 for row in 0..guide_height {
6650 let cell = &mut frame.buffer_mut()[(guide_x, render_area.y + row as u16)];
6651 cell.set_bg(color);
6652 }
6653 }
6654 }
6655 }
6656
6657 #[allow(dead_code)]
6664 fn apply_hyperlink_overlays(
6665 frame: &mut Frame,
6666 viewport_overlays: &[(crate::view::overlay::Overlay, Range<usize>)],
6667 view_line_mappings: &[ViewLineMapping],
6668 render_area: Rect,
6669 gutter_width: usize,
6670 cursor_screen_pos: Option<(u16, u16)>,
6671 ) {
6672 let hyperlink_overlays: Vec<_> = viewport_overlays
6673 .iter()
6674 .filter(|(overlay, _)| overlay.url.is_some())
6675 .collect();
6676
6677 if hyperlink_overlays.is_empty() {
6678 return;
6679 }
6680
6681 let buf = frame.buffer_mut();
6682 for (screen_row, mapping) in view_line_mappings.iter().enumerate() {
6683 let y = render_area.y + screen_row as u16;
6684 if y >= render_area.y + render_area.height {
6685 break;
6686 }
6687 for (overlay, range) in &hyperlink_overlays {
6688 let url = overlay.url.as_ref().unwrap();
6689 let mut run_start: Option<u16> = None;
6691 let content_x_offset = render_area.x + gutter_width as u16;
6692 for (char_idx, maybe_byte) in mapping.char_source_bytes.iter().enumerate() {
6693 let in_range = maybe_byte
6694 .map(|b| b >= range.start && b < range.end)
6695 .unwrap_or(false);
6696 let screen_x = content_x_offset + char_idx as u16;
6697 if in_range && screen_x < render_area.x + render_area.width {
6698 if run_start.is_none() {
6699 run_start = Some(screen_x);
6700 }
6701 } else if let Some(start_x) = run_start.take() {
6702 Self::apply_osc8_to_cells(
6703 buf,
6704 start_x,
6705 screen_x,
6706 y,
6707 url,
6708 cursor_screen_pos,
6709 );
6710 }
6711 }
6712 if let Some(start_x) = run_start {
6714 let end_x = content_x_offset + mapping.char_source_bytes.len() as u16;
6715 let end_x = end_x.min(render_area.x + render_area.width);
6716 Self::apply_osc8_to_cells(buf, start_x, end_x, y, url, cursor_screen_pos);
6717 }
6718 }
6719 }
6720 }
6721
6722 #[allow(dead_code)]
6730 fn apply_osc8_to_cells(
6731 buf: &mut ratatui::buffer::Buffer,
6732 start_x: u16,
6733 end_x: u16,
6734 y: u16,
6735 url: &str,
6736 cursor_pos: Option<(u16, u16)>,
6737 ) {
6738 let area = *buf.area();
6739 if y < area.y || y >= area.y + area.height {
6740 return;
6741 }
6742 let max_x = area.x + area.width;
6743 let cursor_x = cursor_pos.and_then(|(cx, cy)| if cy == y { Some(cx) } else { None });
6746 let mut x = start_x;
6747 while x < end_x {
6748 if x >= max_x {
6749 break;
6750 }
6751 let chunk_size = if cursor_x == Some(x + 1) { 1 } else { 2 };
6754
6755 let mut chunk = String::new();
6756 let chunk_start = x;
6757 for _ in 0..chunk_size {
6758 if x >= end_x || x >= max_x {
6759 break;
6760 }
6761 let sym = buf[(x, y)].symbol().to_string();
6762 chunk.push_str(&sym);
6763 x += 1;
6764 }
6765 if !chunk.is_empty() {
6766 let actual_chunk_len = x - chunk_start;
6767 let hyperlink = format!("\x1B]8;;{}\x07{}\x1B]8;;\x07", url, chunk);
6768 buf[(chunk_start, y)].set_symbol(&hyperlink);
6769 for cx in (chunk_start + 1)..chunk_start + actual_chunk_len {
6772 buf[(cx, y)].set_symbol("");
6773 }
6774 }
6775 }
6776 }
6777
6778 #[allow(clippy::too_many_arguments)]
6789 fn apply_background_to_lines(
6790 lines: &mut Vec<Line<'static>>,
6791 area_width: u16,
6792 background: &AnsiBackground,
6793 theme_bg: Color,
6794 default_fg: Color,
6795 fade: f32,
6796 x_offset: usize,
6797 y_offset: usize,
6798 ) {
6799 if area_width == 0 {
6800 return;
6801 }
6802
6803 let width = area_width as usize;
6804
6805 for (y, line) in lines.iter_mut().enumerate() {
6806 let mut existing: Vec<(char, Style)> = Vec::new();
6808 let spans = std::mem::take(&mut line.spans);
6809 for span in spans {
6810 let style = span.style;
6811 for ch in span.content.chars() {
6812 existing.push((ch, style));
6813 }
6814 }
6815
6816 let mut chars_with_style = Vec::with_capacity(width);
6817 for x in 0..width {
6818 let sample_x = x_offset + x;
6819 let sample_y = y_offset + y;
6820
6821 let (ch, mut style) = if x < existing.len() {
6822 existing[x]
6823 } else {
6824 (' ', Style::default().fg(default_fg))
6825 };
6826
6827 if let Some(bg_color) = background.faded_color(sample_x, sample_y, theme_bg, fade) {
6828 if style.bg.is_none() || matches!(style.bg, Some(Color::Reset)) {
6829 style = style.bg(bg_color);
6830 }
6831 }
6832
6833 chars_with_style.push((ch, style));
6834 }
6835
6836 line.spans = Self::compress_chars(chars_with_style);
6837 }
6838 }
6839
6840 fn compress_chars(chars: Vec<(char, Style)>) -> Vec<Span<'static>> {
6841 if chars.is_empty() {
6842 return vec![];
6843 }
6844
6845 let mut spans = Vec::new();
6846 let mut current_style = chars[0].1;
6847 let mut current_text = String::new();
6848 current_text.push(chars[0].0);
6849
6850 for (ch, style) in chars.into_iter().skip(1) {
6851 if style == current_style {
6852 current_text.push(ch);
6853 } else {
6854 spans.push(Span::styled(current_text.clone(), current_style));
6855 current_text.clear();
6856 current_text.push(ch);
6857 current_style = style;
6858 }
6859 }
6860
6861 spans.push(Span::styled(current_text, current_style));
6862 spans
6863 }
6864}
6865
6866#[cfg(test)]
6867mod tests {
6868 use crate::model::filesystem::StdFileSystem;
6869 use std::sync::Arc;
6870
6871 fn test_fs() -> Arc<dyn crate::model::filesystem::FileSystem + Send + Sync> {
6872 Arc::new(StdFileSystem)
6873 }
6874 use super::*;
6875 use crate::model::buffer::Buffer;
6876 use crate::primitives::display_width::str_width;
6877 use crate::view::theme;
6878 use crate::view::theme::Theme;
6879 use crate::view::viewport::Viewport;
6880 use lsp_types::FoldingRange;
6881
6882 fn render_output_for(
6883 content: &str,
6884 cursor_pos: usize,
6885 ) -> (LineRenderOutput, usize, bool, usize) {
6886 render_output_for_with_gutters(content, cursor_pos, false)
6887 }
6888
6889 fn render_output_for_with_gutters(
6890 content: &str,
6891 cursor_pos: usize,
6892 gutters_enabled: bool,
6893 ) -> (LineRenderOutput, usize, bool, usize) {
6894 let mut state = EditorState::new(20, 6, 1024, test_fs());
6895 state.buffer = Buffer::from_str(content, 1024, test_fs());
6896 let mut cursors = crate::model::cursor::Cursors::new();
6897 cursors.primary_mut().position = cursor_pos.min(state.buffer.len());
6898 let viewport = Viewport::new(20, 4);
6900 state.margins.left_config.enabled = gutters_enabled;
6902
6903 let render_area = Rect::new(0, 0, 20, 4);
6904 let visible_count = viewport.visible_line_count();
6905 let gutter_width = state.margins.left_total_width();
6906 let theme = Theme::load_builtin(theme::THEME_DARK).unwrap();
6907 let empty_folds = FoldManager::new();
6908
6909 let view_data = SplitRenderer::build_view_data(
6910 &mut state,
6911 &viewport,
6912 None,
6913 content.len().max(1),
6914 visible_count,
6915 false, render_area.width as usize,
6917 gutter_width,
6918 &ViewMode::Source, &empty_folds,
6920 &theme,
6921 );
6922 let view_anchor = SplitRenderer::calculate_view_anchor(&view_data.lines, 0);
6923
6924 let estimated_lines = (state.buffer.len() / state.buffer.estimated_line_length()).max(1);
6925 state.margins.update_width_for_buffer(estimated_lines, true);
6926 let gutter_width = state.margins.left_total_width();
6927
6928 let selection = SplitRenderer::selection_context(&state, &cursors);
6929 let _ = state
6930 .buffer
6931 .populate_line_cache(viewport.top_byte, visible_count);
6932 let viewport_start = viewport.top_byte;
6933 let viewport_end = SplitRenderer::calculate_viewport_end(
6934 &mut state,
6935 viewport_start,
6936 content.len().max(1),
6937 visible_count,
6938 );
6939 let decorations = SplitRenderer::decoration_context(
6940 &mut state,
6941 viewport_start,
6942 viewport_end,
6943 selection.primary_cursor_position,
6944 &empty_folds,
6945 &theme,
6946 100_000, &ViewMode::Source, false, );
6950
6951 let mut dummy_theme_map = Vec::new();
6952 let output = SplitRenderer::render_view_lines(LineRenderInput {
6953 state: &state,
6954 theme: &theme,
6955 view_lines: &view_data.lines,
6956 view_anchor,
6957 render_area,
6958 gutter_width,
6959 selection: &selection,
6960 decorations: &decorations,
6961 visible_line_count: visible_count,
6962 lsp_waiting: false,
6963 is_active: true,
6964 line_wrap: viewport.line_wrap_enabled,
6965 estimated_lines,
6966 left_column: viewport.left_column,
6967 relative_line_numbers: false,
6968 session_mode: false,
6969 software_cursor_only: false,
6970 show_line_numbers: true, byte_offset_mode: false, show_tilde: true,
6973 highlight_current_line: true,
6974 cell_theme_map: &mut dummy_theme_map,
6975 screen_width: 0,
6976 });
6977
6978 (
6979 output,
6980 state.buffer.len(),
6981 content.ends_with('\n'),
6982 selection.primary_cursor_position,
6983 )
6984 }
6985
6986 #[test]
6987 fn test_folding_hides_lines_and_adds_placeholder() {
6988 let content = "header\nline1\nline2\ntail\n";
6989 let mut state = EditorState::new(40, 6, 1024, test_fs());
6990 state.buffer = Buffer::from_str(content, 1024, test_fs());
6991
6992 let start = state.buffer.line_start_offset(1).unwrap();
6993 let end = state.buffer.line_start_offset(3).unwrap();
6994 let mut folds = FoldManager::new();
6995 folds.add(&mut state.marker_list, start, end, Some("...".to_string()));
6996
6997 let viewport = Viewport::new(40, 6);
6998 let gutter_width = state.margins.left_total_width();
6999 let theme = Theme::load_builtin(theme::THEME_DARK).unwrap();
7000 let view_data = SplitRenderer::build_view_data(
7001 &mut state,
7002 &viewport,
7003 None,
7004 content.len().max(1),
7005 viewport.visible_line_count(),
7006 false,
7007 40,
7008 gutter_width,
7009 &ViewMode::Source,
7010 &folds,
7011 &theme,
7012 );
7013
7014 let lines: Vec<String> = view_data.lines.iter().map(|l| l.text.clone()).collect();
7015 assert!(lines.iter().any(|l| l.contains("header")));
7016 assert!(lines.iter().any(|l| l.contains("tail")));
7017 assert!(!lines.iter().any(|l| l.contains("line1")));
7018 assert!(!lines.iter().any(|l| l.contains("line2")));
7019 assert!(lines
7020 .iter()
7021 .any(|l| l.contains("header") && l.contains("...")));
7022 }
7023
7024 #[test]
7025 fn test_fold_indicators_collapsed_and_expanded() {
7026 let content = "a\nb\nc\nd\n";
7027 let mut state = EditorState::new(40, 6, 1024, test_fs());
7028 state.buffer = Buffer::from_str(content, 1024, test_fs());
7029
7030 state.folding_ranges = vec![
7031 FoldingRange {
7032 start_line: 0,
7033 end_line: 1,
7034 start_character: None,
7035 end_character: None,
7036 kind: None,
7037 collapsed_text: None,
7038 },
7039 FoldingRange {
7040 start_line: 1,
7041 end_line: 2,
7042 start_character: None,
7043 end_character: None,
7044 kind: None,
7045 collapsed_text: None,
7046 },
7047 ];
7048
7049 let start = state.buffer.line_start_offset(1).unwrap();
7050 let end = state.buffer.line_start_offset(2).unwrap();
7051 let mut folds = FoldManager::new();
7052 folds.add(&mut state.marker_list, start, end, None);
7053
7054 let indicators =
7055 SplitRenderer::fold_indicators_for_viewport(&state, &folds, 0, state.buffer.len());
7056
7057 assert_eq!(indicators.get(&0).map(|i| i.collapsed), Some(true));
7059 let line1_byte = state.buffer.line_start_offset(1).unwrap();
7061 assert_eq!(
7062 indicators.get(&line1_byte).map(|i| i.collapsed),
7063 Some(false)
7064 );
7065 }
7066
7067 #[test]
7068 fn last_line_end_tracks_trailing_newline() {
7069 let output = render_output_for("abc\n", 4);
7070 assert_eq!(
7071 output.0.last_line_end,
7072 Some(LastLineEnd {
7073 pos: (3, 0),
7074 terminated_with_newline: true
7075 })
7076 );
7077 }
7078
7079 #[test]
7080 fn last_line_end_tracks_no_trailing_newline() {
7081 let output = render_output_for("abc", 3);
7082 assert_eq!(
7083 output.0.last_line_end,
7084 Some(LastLineEnd {
7085 pos: (3, 0),
7086 terminated_with_newline: false
7087 })
7088 );
7089 }
7090
7091 #[test]
7092 fn cursor_after_newline_places_on_next_line() {
7093 let (output, buffer_len, buffer_newline, cursor_pos) = render_output_for("abc\n", 4);
7094 let cursor = SplitRenderer::resolve_cursor_fallback(
7095 output.cursor,
7096 cursor_pos,
7097 buffer_len,
7098 buffer_newline,
7099 output.last_line_end,
7100 output.content_lines_rendered,
7101 0, );
7103 assert_eq!(cursor, Some((0, 1)));
7104 }
7105
7106 #[test]
7107 fn cursor_at_end_without_newline_stays_on_line() {
7108 let (output, buffer_len, buffer_newline, cursor_pos) = render_output_for("abc", 3);
7109 let cursor = SplitRenderer::resolve_cursor_fallback(
7110 output.cursor,
7111 cursor_pos,
7112 buffer_len,
7113 buffer_newline,
7114 output.last_line_end,
7115 output.content_lines_rendered,
7116 0, );
7118 assert_eq!(cursor, Some((3, 0)));
7119 }
7120
7121 fn count_all_cursors(output: &LineRenderOutput) -> Vec<(u16, u16)> {
7127 let mut cursor_positions = Vec::new();
7128
7129 let primary_cursor = output.cursor;
7131 if let Some(cursor_pos) = primary_cursor {
7132 cursor_positions.push(cursor_pos);
7133 }
7134
7135 for (line_idx, line) in output.lines.iter().enumerate() {
7137 let mut col = 0u16;
7138 for span in line.spans.iter() {
7139 if span
7141 .style
7142 .add_modifier
7143 .contains(ratatui::style::Modifier::REVERSED)
7144 {
7145 let pos = (col, line_idx as u16);
7146 if primary_cursor != Some(pos) {
7149 cursor_positions.push(pos);
7150 }
7151 }
7152 col += str_width(&span.content) as u16;
7154 }
7155 }
7156
7157 cursor_positions
7158 }
7159
7160 fn dump_render_output(content: &str, cursor_pos: usize, output: &LineRenderOutput) {
7162 eprintln!("\n=== RENDER DEBUG ===");
7163 eprintln!("Content: {:?}", content);
7164 eprintln!("Cursor position: {}", cursor_pos);
7165 eprintln!("Hardware cursor (output.cursor): {:?}", output.cursor);
7166 eprintln!("Last line end: {:?}", output.last_line_end);
7167 eprintln!("Content lines rendered: {}", output.content_lines_rendered);
7168 eprintln!("\nRendered lines:");
7169 for (line_idx, line) in output.lines.iter().enumerate() {
7170 eprintln!(" Line {}: {} spans", line_idx, line.spans.len());
7171 for (span_idx, span) in line.spans.iter().enumerate() {
7172 let has_reversed = span
7173 .style
7174 .add_modifier
7175 .contains(ratatui::style::Modifier::REVERSED);
7176 let bg_color = format!("{:?}", span.style.bg);
7177 eprintln!(
7178 " Span {}: {:?} (REVERSED: {}, BG: {})",
7179 span_idx, span.content, has_reversed, bg_color
7180 );
7181 }
7182 }
7183 eprintln!("===================\n");
7184 }
7185
7186 fn get_final_cursor(content: &str, cursor_pos: usize) -> Option<(u16, u16)> {
7189 let (output, buffer_len, buffer_newline, cursor_pos) =
7190 render_output_for(content, cursor_pos);
7191
7192 let all_cursors = count_all_cursors(&output);
7194
7195 assert!(
7198 all_cursors.len() <= 1,
7199 "Expected at most 1 cursor in rendered output, found {} at positions: {:?}",
7200 all_cursors.len(),
7201 all_cursors
7202 );
7203
7204 let final_cursor = SplitRenderer::resolve_cursor_fallback(
7205 output.cursor,
7206 cursor_pos,
7207 buffer_len,
7208 buffer_newline,
7209 output.last_line_end,
7210 output.content_lines_rendered,
7211 0, );
7213
7214 if all_cursors.len() > 1 || (all_cursors.len() == 1 && Some(all_cursors[0]) != final_cursor)
7216 {
7217 dump_render_output(content, cursor_pos, &output);
7218 }
7219
7220 if let Some(rendered_cursor) = all_cursors.first() {
7222 assert_eq!(
7223 Some(*rendered_cursor),
7224 final_cursor,
7225 "Rendered cursor at {:?} doesn't match final cursor {:?}",
7226 rendered_cursor,
7227 final_cursor
7228 );
7229 }
7230
7231 assert!(
7233 final_cursor.is_some(),
7234 "Expected a final cursor position, but got None. Rendered cursors: {:?}",
7235 all_cursors
7236 );
7237
7238 final_cursor
7239 }
7240
7241 fn check_typing_at_cursor(
7243 content: &str,
7244 cursor_pos: usize,
7245 char_to_type: char,
7246 ) -> (Option<(u16, u16)>, String) {
7247 let cursor_before = get_final_cursor(content, cursor_pos);
7249
7250 let mut new_content = content.to_string();
7252 if cursor_pos <= content.len() {
7253 new_content.insert(cursor_pos, char_to_type);
7254 }
7255
7256 (cursor_before, new_content)
7257 }
7258
7259 #[test]
7260 fn e2e_cursor_at_start_of_nonempty_line() {
7261 let cursor = get_final_cursor("abc", 0);
7263 assert_eq!(cursor, Some((0, 0)), "Cursor should be at column 0, line 0");
7264
7265 let (cursor_pos, new_content) = check_typing_at_cursor("abc", 0, 'X');
7266 assert_eq!(
7267 new_content, "Xabc",
7268 "Typing should insert at cursor position"
7269 );
7270 assert_eq!(cursor_pos, Some((0, 0)));
7271 }
7272
7273 #[test]
7274 fn e2e_cursor_in_middle_of_line() {
7275 let cursor = get_final_cursor("abc", 1);
7277 assert_eq!(cursor, Some((1, 0)), "Cursor should be at column 1, line 0");
7278
7279 let (cursor_pos, new_content) = check_typing_at_cursor("abc", 1, 'X');
7280 assert_eq!(
7281 new_content, "aXbc",
7282 "Typing should insert at cursor position"
7283 );
7284 assert_eq!(cursor_pos, Some((1, 0)));
7285 }
7286
7287 #[test]
7288 fn e2e_cursor_at_end_of_line_no_newline() {
7289 let cursor = get_final_cursor("abc", 3);
7291 assert_eq!(
7292 cursor,
7293 Some((3, 0)),
7294 "Cursor should be at column 3, line 0 (after last char)"
7295 );
7296
7297 let (cursor_pos, new_content) = check_typing_at_cursor("abc", 3, 'X');
7298 assert_eq!(new_content, "abcX", "Typing should append at end");
7299 assert_eq!(cursor_pos, Some((3, 0)));
7300 }
7301
7302 #[test]
7303 fn e2e_cursor_at_empty_line() {
7304 let cursor = get_final_cursor("\n", 0);
7306 assert_eq!(
7307 cursor,
7308 Some((0, 0)),
7309 "Cursor on empty line should be at column 0"
7310 );
7311
7312 let (cursor_pos, new_content) = check_typing_at_cursor("\n", 0, 'X');
7313 assert_eq!(new_content, "X\n", "Typing should insert before newline");
7314 assert_eq!(cursor_pos, Some((0, 0)));
7315 }
7316
7317 #[test]
7318 fn e2e_cursor_after_newline_at_eof() {
7319 let cursor = get_final_cursor("abc\n", 4);
7321 assert_eq!(
7322 cursor,
7323 Some((0, 1)),
7324 "Cursor after newline at EOF should be on next line"
7325 );
7326
7327 let (cursor_pos, new_content) = check_typing_at_cursor("abc\n", 4, 'X');
7328 assert_eq!(new_content, "abc\nX", "Typing should insert on new line");
7329 assert_eq!(cursor_pos, Some((0, 1)));
7330 }
7331
7332 #[test]
7333 fn e2e_cursor_on_newline_with_content() {
7334 let cursor = get_final_cursor("abc\n", 3);
7336 assert_eq!(
7337 cursor,
7338 Some((3, 0)),
7339 "Cursor on newline after content should be after last char"
7340 );
7341
7342 let (cursor_pos, new_content) = check_typing_at_cursor("abc\n", 3, 'X');
7343 assert_eq!(new_content, "abcX\n", "Typing should insert before newline");
7344 assert_eq!(cursor_pos, Some((3, 0)));
7345 }
7346
7347 #[test]
7348 fn e2e_cursor_multiline_start_of_second_line() {
7349 let cursor = get_final_cursor("abc\ndef", 4);
7351 assert_eq!(
7352 cursor,
7353 Some((0, 1)),
7354 "Cursor at start of second line should be at column 0, line 1"
7355 );
7356
7357 let (cursor_pos, new_content) = check_typing_at_cursor("abc\ndef", 4, 'X');
7358 assert_eq!(
7359 new_content, "abc\nXdef",
7360 "Typing should insert at start of second line"
7361 );
7362 assert_eq!(cursor_pos, Some((0, 1)));
7363 }
7364
7365 #[test]
7366 fn e2e_cursor_multiline_end_of_first_line() {
7367 let cursor = get_final_cursor("abc\ndef", 3);
7369 assert_eq!(
7370 cursor,
7371 Some((3, 0)),
7372 "Cursor on newline of first line should be after content"
7373 );
7374
7375 let (cursor_pos, new_content) = check_typing_at_cursor("abc\ndef", 3, 'X');
7376 assert_eq!(
7377 new_content, "abcX\ndef",
7378 "Typing should insert before newline"
7379 );
7380 assert_eq!(cursor_pos, Some((3, 0)));
7381 }
7382
7383 #[test]
7384 fn e2e_cursor_empty_buffer() {
7385 let cursor = get_final_cursor("", 0);
7387 assert_eq!(
7388 cursor,
7389 Some((0, 0)),
7390 "Cursor in empty buffer should be at origin"
7391 );
7392
7393 let (cursor_pos, new_content) = check_typing_at_cursor("", 0, 'X');
7394 assert_eq!(
7395 new_content, "X",
7396 "Typing in empty buffer should insert character"
7397 );
7398 assert_eq!(cursor_pos, Some((0, 0)));
7399 }
7400
7401 #[test]
7402 fn e2e_cursor_empty_buffer_with_gutters() {
7403 let (output, buffer_len, buffer_newline, cursor_pos) =
7407 render_output_for_with_gutters("", 0, true);
7408
7409 let gutter_width = {
7413 let mut state = EditorState::new(20, 6, 1024, test_fs());
7414 state.margins.left_config.enabled = true;
7415 state.margins.update_width_for_buffer(1, true);
7416 state.margins.left_total_width()
7417 };
7418 assert!(gutter_width > 0, "Gutter width should be > 0 when enabled");
7419
7420 assert_eq!(
7424 output.cursor,
7425 Some((gutter_width as u16, 0)),
7426 "RENDERED cursor in empty buffer should be at gutter_width ({}), got {:?}",
7427 gutter_width,
7428 output.cursor
7429 );
7430
7431 let final_cursor = SplitRenderer::resolve_cursor_fallback(
7432 output.cursor,
7433 cursor_pos,
7434 buffer_len,
7435 buffer_newline,
7436 output.last_line_end,
7437 output.content_lines_rendered,
7438 gutter_width,
7439 );
7440
7441 assert_eq!(
7443 final_cursor,
7444 Some((gutter_width as u16, 0)),
7445 "Cursor in empty buffer with gutters should be at gutter_width, not column 0"
7446 );
7447 }
7448
7449 #[test]
7450 fn e2e_cursor_between_empty_lines() {
7451 let cursor = get_final_cursor("\n\n", 1);
7453 assert_eq!(cursor, Some((0, 1)), "Cursor on second empty line");
7454
7455 let (cursor_pos, new_content) = check_typing_at_cursor("\n\n", 1, 'X');
7456 assert_eq!(new_content, "\nX\n", "Typing should insert on second line");
7457 assert_eq!(cursor_pos, Some((0, 1)));
7458 }
7459
7460 #[test]
7461 fn e2e_cursor_at_eof_after_multiple_lines() {
7462 let cursor = get_final_cursor("abc\ndef\nghi", 11);
7464 assert_eq!(
7465 cursor,
7466 Some((3, 2)),
7467 "Cursor at EOF after 'i' should be at column 3, line 2"
7468 );
7469
7470 let (cursor_pos, new_content) = check_typing_at_cursor("abc\ndef\nghi", 11, 'X');
7471 assert_eq!(new_content, "abc\ndef\nghiX", "Typing should append at end");
7472 assert_eq!(cursor_pos, Some((3, 2)));
7473 }
7474
7475 #[test]
7476 fn e2e_cursor_at_eof_with_trailing_newline() {
7477 let cursor = get_final_cursor("abc\ndef\nghi\n", 12);
7479 assert_eq!(
7480 cursor,
7481 Some((0, 3)),
7482 "Cursor after trailing newline should be on line 3"
7483 );
7484
7485 let (cursor_pos, new_content) = check_typing_at_cursor("abc\ndef\nghi\n", 12, 'X');
7486 assert_eq!(
7487 new_content, "abc\ndef\nghi\nX",
7488 "Typing should insert on new line"
7489 );
7490 assert_eq!(cursor_pos, Some((0, 3)));
7491 }
7492
7493 #[test]
7494 fn e2e_jump_to_end_of_buffer_no_trailing_newline() {
7495 let content = "abc\ndef\nghi";
7497
7498 let cursor_at_start = get_final_cursor(content, 0);
7500 assert_eq!(cursor_at_start, Some((0, 0)), "Cursor starts at beginning");
7501
7502 let cursor_at_eof = get_final_cursor(content, 11);
7504 assert_eq!(
7505 cursor_at_eof,
7506 Some((3, 2)),
7507 "After Ctrl+End, cursor at column 3, line 2"
7508 );
7509
7510 let (cursor_before_typing, new_content) = check_typing_at_cursor(content, 11, 'X');
7512 assert_eq!(cursor_before_typing, Some((3, 2)));
7513 assert_eq!(new_content, "abc\ndef\nghiX", "Character appended at end");
7514
7515 let cursor_after_typing = get_final_cursor(&new_content, 12);
7517 assert_eq!(
7518 cursor_after_typing,
7519 Some((4, 2)),
7520 "After typing, cursor moved to column 4"
7521 );
7522
7523 let cursor_moved_away = get_final_cursor(&new_content, 0);
7525 assert_eq!(cursor_moved_away, Some((0, 0)), "Cursor moved to start");
7526 }
7529
7530 #[test]
7531 fn e2e_jump_to_end_of_buffer_with_trailing_newline() {
7532 let content = "abc\ndef\nghi\n";
7534
7535 let cursor_at_start = get_final_cursor(content, 0);
7537 assert_eq!(cursor_at_start, Some((0, 0)), "Cursor starts at beginning");
7538
7539 let cursor_at_eof = get_final_cursor(content, 12);
7541 assert_eq!(
7542 cursor_at_eof,
7543 Some((0, 3)),
7544 "After Ctrl+End, cursor at column 0, line 3 (new line)"
7545 );
7546
7547 let (cursor_before_typing, new_content) = check_typing_at_cursor(content, 12, 'X');
7549 assert_eq!(cursor_before_typing, Some((0, 3)));
7550 assert_eq!(
7551 new_content, "abc\ndef\nghi\nX",
7552 "Character inserted on new line"
7553 );
7554
7555 let cursor_after_typing = get_final_cursor(&new_content, 13);
7557 assert_eq!(
7558 cursor_after_typing,
7559 Some((1, 3)),
7560 "After typing, cursor should be at column 1, line 3"
7561 );
7562
7563 let cursor_moved_away = get_final_cursor(&new_content, 4);
7565 assert_eq!(
7566 cursor_moved_away,
7567 Some((0, 1)),
7568 "Cursor moved to start of line 1 (position 4 = start of 'def')"
7569 );
7570 }
7571
7572 #[test]
7573 fn e2e_jump_to_end_of_empty_buffer() {
7574 let content = "";
7576
7577 let cursor_at_eof = get_final_cursor(content, 0);
7578 assert_eq!(
7579 cursor_at_eof,
7580 Some((0, 0)),
7581 "Empty buffer: cursor at origin"
7582 );
7583
7584 let (cursor_before_typing, new_content) = check_typing_at_cursor(content, 0, 'X');
7586 assert_eq!(cursor_before_typing, Some((0, 0)));
7587 assert_eq!(new_content, "X", "Character inserted");
7588
7589 let cursor_after_typing = get_final_cursor(&new_content, 1);
7591 assert_eq!(
7592 cursor_after_typing,
7593 Some((1, 0)),
7594 "After typing, cursor at column 1"
7595 );
7596
7597 let cursor_moved_away = get_final_cursor(&new_content, 0);
7599 assert_eq!(
7600 cursor_moved_away,
7601 Some((0, 0)),
7602 "Cursor moved back to start"
7603 );
7604 }
7605
7606 #[test]
7607 fn e2e_jump_to_end_of_single_empty_line() {
7608 let content = "\n";
7610
7611 let cursor_on_newline = get_final_cursor(content, 0);
7613 assert_eq!(
7614 cursor_on_newline,
7615 Some((0, 0)),
7616 "Cursor on the newline character"
7617 );
7618
7619 let cursor_at_eof = get_final_cursor(content, 1);
7621 assert_eq!(
7622 cursor_at_eof,
7623 Some((0, 1)),
7624 "After Ctrl+End, cursor on line 1"
7625 );
7626
7627 let (cursor_before_typing, new_content) = check_typing_at_cursor(content, 1, 'X');
7629 assert_eq!(cursor_before_typing, Some((0, 1)));
7630 assert_eq!(new_content, "\nX", "Character on second line");
7631
7632 let cursor_after_typing = get_final_cursor(&new_content, 2);
7633 assert_eq!(
7634 cursor_after_typing,
7635 Some((1, 1)),
7636 "After typing, cursor at column 1, line 1"
7637 );
7638
7639 let cursor_moved_away = get_final_cursor(&new_content, 0);
7641 assert_eq!(
7642 cursor_moved_away,
7643 Some((0, 0)),
7644 "Cursor moved to the newline on line 0"
7645 );
7646 }
7647 use crate::model::buffer::LineEnding;
7658 use fresh_core::api::{ViewTokenWire, ViewTokenWireKind};
7659
7660 fn extract_token_offsets(tokens: &[ViewTokenWire]) -> Vec<(String, Option<usize>)> {
7662 tokens
7663 .iter()
7664 .map(|t| {
7665 let kind_str = match &t.kind {
7666 ViewTokenWireKind::Text(s) => format!("Text({})", s),
7667 ViewTokenWireKind::Newline => "Newline".to_string(),
7668 ViewTokenWireKind::Space => "Space".to_string(),
7669 ViewTokenWireKind::Break => "Break".to_string(),
7670 ViewTokenWireKind::BinaryByte(b) => format!("Byte(0x{:02x})", b),
7671 };
7672 (kind_str, t.source_offset)
7673 })
7674 .collect()
7675 }
7676
7677 #[test]
7680 fn test_build_base_tokens_crlf_single_line() {
7681 let content = b"abc\r\n";
7683 let mut buffer = Buffer::from_bytes(content.to_vec(), test_fs());
7684 buffer.set_line_ending(LineEnding::CRLF);
7685
7686 let tokens = SplitRenderer::build_base_tokens_for_hook(
7687 &mut buffer,
7688 0, 80, 10, false, LineEnding::CRLF,
7693 );
7694
7695 let offsets = extract_token_offsets(&tokens);
7696
7697 assert!(
7700 offsets
7701 .iter()
7702 .any(|(kind, off)| kind == "Text(abc)" && *off == Some(0)),
7703 "Expected Text(abc) at offset 0, got: {:?}",
7704 offsets
7705 );
7706 assert!(
7707 offsets
7708 .iter()
7709 .any(|(kind, off)| kind == "Newline" && *off == Some(3)),
7710 "Expected Newline at offset 3 (\\r position), got: {:?}",
7711 offsets
7712 );
7713
7714 let newline_count = offsets.iter().filter(|(k, _)| k == "Newline").count();
7716 assert_eq!(
7717 newline_count, 1,
7718 "Should have exactly 1 Newline token for CRLF, got {}: {:?}",
7719 newline_count, offsets
7720 );
7721 }
7722
7723 #[test]
7726 fn test_build_base_tokens_crlf_multiple_lines() {
7727 let content = b"abc\r\ndef\r\nghi\r\n";
7732 let mut buffer = Buffer::from_bytes(content.to_vec(), test_fs());
7733 buffer.set_line_ending(LineEnding::CRLF);
7734
7735 let tokens = SplitRenderer::build_base_tokens_for_hook(
7736 &mut buffer,
7737 0,
7738 80,
7739 10,
7740 false,
7741 LineEnding::CRLF,
7742 );
7743
7744 let offsets = extract_token_offsets(&tokens);
7745
7746 assert!(
7753 offsets
7754 .iter()
7755 .any(|(kind, off)| kind == "Text(abc)" && *off == Some(0)),
7756 "Line 1: Expected Text(abc) at 0, got: {:?}",
7757 offsets
7758 );
7759 assert!(
7760 offsets
7761 .iter()
7762 .any(|(kind, off)| kind == "Newline" && *off == Some(3)),
7763 "Line 1: Expected Newline at 3, got: {:?}",
7764 offsets
7765 );
7766
7767 assert!(
7769 offsets
7770 .iter()
7771 .any(|(kind, off)| kind == "Text(def)" && *off == Some(5)),
7772 "Line 2: Expected Text(def) at 5, got: {:?}",
7773 offsets
7774 );
7775 assert!(
7776 offsets
7777 .iter()
7778 .any(|(kind, off)| kind == "Newline" && *off == Some(8)),
7779 "Line 2: Expected Newline at 8, got: {:?}",
7780 offsets
7781 );
7782
7783 assert!(
7785 offsets
7786 .iter()
7787 .any(|(kind, off)| kind == "Text(ghi)" && *off == Some(10)),
7788 "Line 3: Expected Text(ghi) at 10, got: {:?}",
7789 offsets
7790 );
7791 assert!(
7792 offsets
7793 .iter()
7794 .any(|(kind, off)| kind == "Newline" && *off == Some(13)),
7795 "Line 3: Expected Newline at 13, got: {:?}",
7796 offsets
7797 );
7798
7799 let newline_count = offsets.iter().filter(|(k, _)| k == "Newline").count();
7801 assert_eq!(newline_count, 3, "Should have 3 Newline tokens");
7802 }
7803
7804 #[test]
7807 fn test_build_base_tokens_lf_mode_for_comparison() {
7808 let content = b"abc\ndef\n";
7812 let mut buffer = Buffer::from_bytes(content.to_vec(), test_fs());
7813 buffer.set_line_ending(LineEnding::LF);
7814
7815 let tokens = SplitRenderer::build_base_tokens_for_hook(
7816 &mut buffer,
7817 0,
7818 80,
7819 10,
7820 false,
7821 LineEnding::LF,
7822 );
7823
7824 let offsets = extract_token_offsets(&tokens);
7825
7826 assert!(
7828 offsets
7829 .iter()
7830 .any(|(kind, off)| kind == "Text(abc)" && *off == Some(0)),
7831 "LF Line 1: Expected Text(abc) at 0"
7832 );
7833 assert!(
7834 offsets
7835 .iter()
7836 .any(|(kind, off)| kind == "Newline" && *off == Some(3)),
7837 "LF Line 1: Expected Newline at 3"
7838 );
7839 assert!(
7840 offsets
7841 .iter()
7842 .any(|(kind, off)| kind == "Text(def)" && *off == Some(4)),
7843 "LF Line 2: Expected Text(def) at 4"
7844 );
7845 assert!(
7846 offsets
7847 .iter()
7848 .any(|(kind, off)| kind == "Newline" && *off == Some(7)),
7849 "LF Line 2: Expected Newline at 7"
7850 );
7851 }
7852
7853 #[test]
7856 fn test_build_base_tokens_crlf_in_lf_mode_shows_control_char() {
7857 let content = b"abc\r\n";
7859 let mut buffer = Buffer::from_bytes(content.to_vec(), test_fs());
7860 buffer.set_line_ending(LineEnding::LF); let tokens = SplitRenderer::build_base_tokens_for_hook(
7863 &mut buffer,
7864 0,
7865 80,
7866 10,
7867 false,
7868 LineEnding::LF,
7869 );
7870
7871 let offsets = extract_token_offsets(&tokens);
7872
7873 assert!(
7875 offsets.iter().any(|(kind, _)| kind == "Byte(0x0d)"),
7876 "LF mode should render \\r as control char <0D>, got: {:?}",
7877 offsets
7878 );
7879 }
7880
7881 #[test]
7884 fn test_build_base_tokens_crlf_from_middle() {
7885 let content = b"abc\r\ndef\r\nghi\r\n";
7888 let mut buffer = Buffer::from_bytes(content.to_vec(), test_fs());
7889 buffer.set_line_ending(LineEnding::CRLF);
7890
7891 let tokens = SplitRenderer::build_base_tokens_for_hook(
7892 &mut buffer,
7893 5, 80,
7895 10,
7896 false,
7897 LineEnding::CRLF,
7898 );
7899
7900 let offsets = extract_token_offsets(&tokens);
7901
7902 assert!(
7906 offsets
7907 .iter()
7908 .any(|(kind, off)| kind == "Text(def)" && *off == Some(5)),
7909 "Starting from byte 5: Expected Text(def) at 5, got: {:?}",
7910 offsets
7911 );
7912 assert!(
7913 offsets
7914 .iter()
7915 .any(|(kind, off)| kind == "Text(ghi)" && *off == Some(10)),
7916 "Starting from byte 5: Expected Text(ghi) at 10, got: {:?}",
7917 offsets
7918 );
7919 }
7920
7921 #[test]
7924 fn test_crlf_highlight_span_lookup() {
7925 use crate::view::ui::view_pipeline::ViewLineIterator;
7926
7927 let content = b"int x;\r\nint y;\r\n";
7932 let mut buffer = Buffer::from_bytes(content.to_vec(), test_fs());
7933 buffer.set_line_ending(LineEnding::CRLF);
7934
7935 let tokens = SplitRenderer::build_base_tokens_for_hook(
7937 &mut buffer,
7938 0,
7939 80,
7940 10,
7941 false,
7942 LineEnding::CRLF,
7943 );
7944
7945 let offsets = extract_token_offsets(&tokens);
7947 eprintln!("Tokens: {:?}", offsets);
7948
7949 let view_lines: Vec<_> = ViewLineIterator::new(&tokens, false, false, 4, false).collect();
7951 assert_eq!(view_lines.len(), 2, "Should have 2 view lines");
7952
7953 eprintln!(
7956 "Line 1 char_source_bytes: {:?}",
7957 view_lines[0].char_source_bytes
7958 );
7959 assert_eq!(
7960 view_lines[0].char_source_bytes.len(),
7961 7,
7962 "Line 1 should have 7 chars: 'i','n','t',' ','x',';','\\n'"
7963 );
7964 assert_eq!(
7966 view_lines[0].char_source_bytes[0],
7967 Some(0),
7968 "Line 1 'i' -> byte 0"
7969 );
7970 assert_eq!(
7971 view_lines[0].char_source_bytes[4],
7972 Some(4),
7973 "Line 1 'x' -> byte 4"
7974 );
7975 assert_eq!(
7976 view_lines[0].char_source_bytes[5],
7977 Some(5),
7978 "Line 1 ';' -> byte 5"
7979 );
7980 assert_eq!(
7981 view_lines[0].char_source_bytes[6],
7982 Some(6),
7983 "Line 1 newline -> byte 6 (\\r pos)"
7984 );
7985
7986 eprintln!(
7988 "Line 2 char_source_bytes: {:?}",
7989 view_lines[1].char_source_bytes
7990 );
7991 assert_eq!(
7992 view_lines[1].char_source_bytes.len(),
7993 7,
7994 "Line 2 should have 7 chars: 'i','n','t',' ','y',';','\\n'"
7995 );
7996 assert_eq!(
7998 view_lines[1].char_source_bytes[0],
7999 Some(8),
8000 "Line 2 'i' -> byte 8"
8001 );
8002 assert_eq!(
8003 view_lines[1].char_source_bytes[4],
8004 Some(12),
8005 "Line 2 'y' -> byte 12"
8006 );
8007 assert_eq!(
8008 view_lines[1].char_source_bytes[5],
8009 Some(13),
8010 "Line 2 ';' -> byte 13"
8011 );
8012 assert_eq!(
8013 view_lines[1].char_source_bytes[6],
8014 Some(14),
8015 "Line 2 newline -> byte 14 (\\r pos)"
8016 );
8017
8018 let simulated_highlight_spans = [
8022 (0usize..3usize, "keyword"),
8024 (8usize..11usize, "keyword"),
8026 ];
8027
8028 for (line_idx, view_line) in view_lines.iter().enumerate() {
8030 for (char_idx, byte_pos) in view_line.char_source_bytes.iter().enumerate() {
8031 if let Some(bp) = byte_pos {
8032 let in_span = simulated_highlight_spans
8033 .iter()
8034 .find(|(range, _)| range.contains(bp))
8035 .map(|(_, name)| *name);
8036
8037 let expected_in_keyword = char_idx < 3;
8039 let actually_in_keyword = in_span == Some("keyword");
8040
8041 if expected_in_keyword != actually_in_keyword {
8042 panic!(
8043 "CRLF offset drift detected! Line {} char {} (byte {}): expected keyword={}, got keyword={}",
8044 line_idx + 1, char_idx, bp, expected_in_keyword, actually_in_keyword
8045 );
8046 }
8047 }
8048 }
8049 }
8050 }
8051
8052 #[test]
8055 fn test_apply_wrapping_transform_breaks_long_lines() {
8056 use fresh_core::api::{ViewTokenWire, ViewTokenWireKind};
8057
8058 let long_text = "x".repeat(25_000);
8060 let tokens = vec![
8061 ViewTokenWire {
8062 kind: ViewTokenWireKind::Text(long_text),
8063 source_offset: Some(0),
8064 style: None,
8065 },
8066 ViewTokenWire {
8067 kind: ViewTokenWireKind::Newline,
8068 source_offset: Some(25_000),
8069 style: None,
8070 },
8071 ];
8072
8073 let wrapped =
8075 SplitRenderer::apply_wrapping_transform(tokens, MAX_SAFE_LINE_WIDTH, 0, false);
8076
8077 let break_count = wrapped
8079 .iter()
8080 .filter(|t| matches!(t.kind, ViewTokenWireKind::Break))
8081 .count();
8082
8083 assert!(
8084 break_count >= 2,
8085 "25K char line should have at least 2 breaks at 10K width, got {}",
8086 break_count
8087 );
8088
8089 let total_chars: usize = wrapped
8091 .iter()
8092 .filter_map(|t| match &t.kind {
8093 ViewTokenWireKind::Text(s) => Some(s.len()),
8094 _ => None,
8095 })
8096 .sum();
8097
8098 assert_eq!(
8099 total_chars, 25_000,
8100 "Total character count should be preserved after wrapping"
8101 );
8102 }
8103
8104 #[test]
8106 fn test_apply_wrapping_transform_preserves_short_lines() {
8107 use fresh_core::api::{ViewTokenWire, ViewTokenWireKind};
8108
8109 let short_text = "x".repeat(100);
8111 let tokens = vec![
8112 ViewTokenWire {
8113 kind: ViewTokenWireKind::Text(short_text.clone()),
8114 source_offset: Some(0),
8115 style: None,
8116 },
8117 ViewTokenWire {
8118 kind: ViewTokenWireKind::Newline,
8119 source_offset: Some(100),
8120 style: None,
8121 },
8122 ];
8123
8124 let wrapped =
8126 SplitRenderer::apply_wrapping_transform(tokens, MAX_SAFE_LINE_WIDTH, 0, false);
8127
8128 let break_count = wrapped
8130 .iter()
8131 .filter(|t| matches!(t.kind, ViewTokenWireKind::Break))
8132 .count();
8133
8134 assert_eq!(
8135 break_count, 0,
8136 "Short lines should not have any breaks, got {}",
8137 break_count
8138 );
8139
8140 let text_tokens: Vec<_> = wrapped
8142 .iter()
8143 .filter_map(|t| match &t.kind {
8144 ViewTokenWireKind::Text(s) => Some(s.clone()),
8145 _ => None,
8146 })
8147 .collect();
8148
8149 assert_eq!(text_tokens.len(), 1, "Should have exactly one Text token");
8150 assert_eq!(
8151 text_tokens[0], short_text,
8152 "Text content should be unchanged"
8153 );
8154 }
8155
8156 #[test]
8159 fn test_large_single_line_sequential_data_preserved() {
8160 use crate::view::ui::view_pipeline::ViewLineIterator;
8161 use fresh_core::api::{ViewTokenWire, ViewTokenWireKind};
8162
8163 let num_markers = 5_000; let content: String = (1..=num_markers).map(|i| format!("[{:05}]", i)).collect();
8167
8168 let tokens = vec![
8170 ViewTokenWire {
8171 kind: ViewTokenWireKind::Text(content.clone()),
8172 source_offset: Some(0),
8173 style: None,
8174 },
8175 ViewTokenWire {
8176 kind: ViewTokenWireKind::Newline,
8177 source_offset: Some(content.len()),
8178 style: None,
8179 },
8180 ];
8181
8182 let wrapped =
8184 SplitRenderer::apply_wrapping_transform(tokens, MAX_SAFE_LINE_WIDTH, 0, false);
8185
8186 let view_lines: Vec<_> = ViewLineIterator::new(&wrapped, false, false, 4, false).collect();
8188
8189 let mut reconstructed = String::new();
8191 for line in &view_lines {
8192 let text = line.text.trim_end_matches('\n');
8194 reconstructed.push_str(text);
8195 }
8196
8197 assert_eq!(
8199 reconstructed.len(),
8200 content.len(),
8201 "Reconstructed content length should match original"
8202 );
8203
8204 for i in 1..=num_markers {
8206 let marker = format!("[{:05}]", i);
8207 assert!(
8208 reconstructed.contains(&marker),
8209 "Missing marker {} after pipeline",
8210 marker
8211 );
8212 }
8213
8214 let pos_100 = reconstructed.find("[00100]").expect("Should find [00100]");
8216 let pos_1000 = reconstructed.find("[01000]").expect("Should find [01000]");
8217 let pos_3000 = reconstructed.find("[03000]").expect("Should find [03000]");
8218 assert!(
8219 pos_100 < pos_1000 && pos_1000 < pos_3000,
8220 "Markers should be in sequential order: {} < {} < {}",
8221 pos_100,
8222 pos_1000,
8223 pos_3000
8224 );
8225
8226 assert!(
8228 view_lines.len() >= 3,
8229 "35KB content should produce multiple visual lines at 10K width, got {}",
8230 view_lines.len()
8231 );
8232
8233 for (i, line) in view_lines.iter().enumerate() {
8235 assert!(
8236 line.text.len() <= MAX_SAFE_LINE_WIDTH + 10, "ViewLine {} exceeds safe width: {} chars",
8238 i,
8239 line.text.len()
8240 );
8241 }
8242 }
8243
8244 fn strip_osc8(s: &str) -> String {
8246 let mut result = String::with_capacity(s.len());
8247 let bytes = s.as_bytes();
8248 let mut i = 0;
8249 while i < bytes.len() {
8250 if i + 3 < bytes.len()
8251 && bytes[i] == 0x1b
8252 && bytes[i + 1] == b']'
8253 && bytes[i + 2] == b'8'
8254 && bytes[i + 3] == b';'
8255 {
8256 i += 4;
8257 while i < bytes.len() && bytes[i] != 0x07 {
8258 i += 1;
8259 }
8260 if i < bytes.len() {
8261 i += 1;
8262 }
8263 } else {
8264 result.push(bytes[i] as char);
8265 i += 1;
8266 }
8267 }
8268 result
8269 }
8270
8271 fn read_row(buf: &ratatui::buffer::Buffer, y: u16) -> String {
8274 let width = buf.area().width;
8275 let mut s = String::new();
8276 let mut col = 0u16;
8277 while col < width {
8278 let cell = &buf[(col, y)];
8279 let stripped = strip_osc8(cell.symbol());
8280 let chars = stripped.chars().count();
8281 if chars > 1 {
8282 s.push_str(&stripped);
8283 col += chars as u16;
8284 } else {
8285 s.push_str(&stripped);
8286 col += 1;
8287 }
8288 }
8289 s.trim_end().to_string()
8290 }
8291
8292 #[test]
8293 fn test_apply_osc8_to_cells_preserves_adjacent_cells() {
8294 use ratatui::buffer::Buffer;
8295 use ratatui::layout::Rect;
8296
8297 let text = "[Quick Install](#installation)";
8299 let area = Rect::new(0, 0, 40, 1);
8300 let mut buf = Buffer::empty(area);
8301 for (i, ch) in text.chars().enumerate() {
8302 if (i as u16) < 40 {
8303 buf[(i as u16, 0)].set_symbol(&ch.to_string());
8304 }
8305 }
8306
8307 let url = "https://example.com";
8309
8310 SplitRenderer::apply_osc8_to_cells(&mut buf, 1, 14, 0, url, Some((0, 0)));
8312
8313 let row = read_row(&buf, 0);
8314 assert_eq!(
8315 row, text,
8316 "After OSC 8 application, reading the row should reproduce the original text"
8317 );
8318
8319 let cell14 = strip_osc8(buf[(14, 0)].symbol());
8321 assert_eq!(cell14, "]", "Cell 14 (']') must not be modified by OSC 8");
8322
8323 let cell0 = strip_osc8(buf[(0, 0)].symbol());
8325 assert_eq!(cell0, "[", "Cell 0 ('[') must not be modified by OSC 8");
8326 }
8327
8328 #[test]
8329 fn test_apply_osc8_stable_across_reapply() {
8330 use ratatui::buffer::Buffer;
8331 use ratatui::layout::Rect;
8332
8333 let text = "[Quick Install](#installation)";
8334 let area = Rect::new(0, 0, 40, 1);
8335
8336 let mut buf1 = Buffer::empty(area);
8338 for (i, ch) in text.chars().enumerate() {
8339 if (i as u16) < 40 {
8340 buf1[(i as u16, 0)].set_symbol(&ch.to_string());
8341 }
8342 }
8343 SplitRenderer::apply_osc8_to_cells(
8344 &mut buf1,
8345 1,
8346 14,
8347 0,
8348 "https://example.com",
8349 Some((0, 0)),
8350 );
8351 let row1 = read_row(&buf1, 0);
8352
8353 let mut buf2 = Buffer::empty(area);
8355 for (i, ch) in text.chars().enumerate() {
8356 if (i as u16) < 40 {
8357 buf2[(i as u16, 0)].set_symbol(&ch.to_string());
8358 }
8359 }
8360 SplitRenderer::apply_osc8_to_cells(
8361 &mut buf2,
8362 1,
8363 14,
8364 0,
8365 "https://example.com",
8366 Some((5, 0)),
8367 );
8368 let row2 = read_row(&buf2, 0);
8369
8370 assert_eq!(row1, text);
8371 assert_eq!(row2, text);
8372 }
8373
8374 #[test]
8375 #[ignore = "OSC 8 hyperlinks disabled pending ratatui diff fix"]
8376 fn test_apply_osc8_diff_between_renders() {
8377 use ratatui::buffer::Buffer;
8378 use ratatui::layout::Rect;
8379
8380 let area = Rect::new(0, 0, 40, 1);
8383
8384 let concealed = "Quick Install";
8386 let mut frame1 = Buffer::empty(area);
8387 for (i, ch) in concealed.chars().enumerate() {
8388 frame1[(i as u16, 0)].set_symbol(&ch.to_string());
8389 }
8390 SplitRenderer::apply_osc8_to_cells(
8392 &mut frame1,
8393 0,
8394 13,
8395 0,
8396 "https://example.com",
8397 Some((0, 5)),
8398 );
8399
8400 let prev = Buffer::empty(area);
8402 let mut backend = Buffer::empty(area);
8403 let diff1 = prev.diff(&frame1);
8404 for (x, y, cell) in &diff1 {
8405 backend[(*x, *y)] = (*cell).clone();
8406 }
8407
8408 let full = "[Quick Install](#installation)";
8410 let mut frame2 = Buffer::empty(area);
8411 for (i, ch) in full.chars().enumerate() {
8412 if (i as u16) < 40 {
8413 frame2[(i as u16, 0)].set_symbol(&ch.to_string());
8414 }
8415 }
8416 SplitRenderer::apply_osc8_to_cells(
8418 &mut frame2,
8419 1,
8420 14,
8421 0,
8422 "https://example.com",
8423 Some((0, 0)),
8424 );
8425
8426 let diff2 = frame1.diff(&frame2);
8428 for (x, y, cell) in &diff2 {
8429 backend[(*x, *y)] = (*cell).clone();
8430 }
8431
8432 let row = read_row(&backend, 0);
8434 assert_eq!(
8435 row, full,
8436 "After diff-based update from concealed to unconcealed, \
8437 backend should show full text"
8438 );
8439
8440 let cell14 = strip_osc8(backend[(14, 0)].symbol());
8442 assert_eq!(cell14, "]", "Cell 14 must be ']' after unconcealed render");
8443 }
8444
8445 fn render_with_highlight_option(
8448 content: &str,
8449 cursor_pos: usize,
8450 highlight_current_line: bool,
8451 ) -> LineRenderOutput {
8452 let mut state = EditorState::new(20, 6, 1024, test_fs());
8453 state.buffer = Buffer::from_str(content, 1024, test_fs());
8454 let mut cursors = crate::model::cursor::Cursors::new();
8455 cursors.primary_mut().position = cursor_pos.min(state.buffer.len());
8456 let viewport = Viewport::new(20, 4);
8457 state.margins.left_config.enabled = false;
8458
8459 let render_area = Rect::new(0, 0, 20, 4);
8460 let visible_count = viewport.visible_line_count();
8461 let gutter_width = state.margins.left_total_width();
8462 let theme = Theme::load_builtin(theme::THEME_DARK).unwrap();
8463 let empty_folds = FoldManager::new();
8464
8465 let view_data = SplitRenderer::build_view_data(
8466 &mut state,
8467 &viewport,
8468 None,
8469 content.len().max(1),
8470 visible_count,
8471 false,
8472 render_area.width as usize,
8473 gutter_width,
8474 &ViewMode::Source,
8475 &empty_folds,
8476 &theme,
8477 );
8478 let view_anchor = SplitRenderer::calculate_view_anchor(&view_data.lines, 0);
8479
8480 let estimated_lines = (state.buffer.len() / state.buffer.estimated_line_length()).max(1);
8481 state.margins.update_width_for_buffer(estimated_lines, true);
8482 let gutter_width = state.margins.left_total_width();
8483
8484 let selection = SplitRenderer::selection_context(&state, &cursors);
8485 let _ = state
8486 .buffer
8487 .populate_line_cache(viewport.top_byte, visible_count);
8488 let viewport_start = viewport.top_byte;
8489 let viewport_end = SplitRenderer::calculate_viewport_end(
8490 &mut state,
8491 viewport_start,
8492 content.len().max(1),
8493 visible_count,
8494 );
8495 let decorations = SplitRenderer::decoration_context(
8496 &mut state,
8497 viewport_start,
8498 viewport_end,
8499 selection.primary_cursor_position,
8500 &empty_folds,
8501 &theme,
8502 100_000,
8503 &ViewMode::Source,
8504 false,
8505 );
8506
8507 SplitRenderer::render_view_lines(LineRenderInput {
8508 state: &state,
8509 theme: &theme,
8510 view_lines: &view_data.lines,
8511 view_anchor,
8512 render_area,
8513 gutter_width,
8514 selection: &selection,
8515 decorations: &decorations,
8516 visible_line_count: visible_count,
8517 lsp_waiting: false,
8518 is_active: true,
8519 line_wrap: viewport.line_wrap_enabled,
8520 estimated_lines,
8521 left_column: viewport.left_column,
8522 relative_line_numbers: false,
8523 session_mode: false,
8524 software_cursor_only: false,
8525 show_line_numbers: false,
8526 byte_offset_mode: false,
8527 show_tilde: true,
8528 highlight_current_line,
8529 cell_theme_map: &mut Vec::new(),
8530 screen_width: 0,
8531 })
8532 }
8533
8534 fn line_has_current_line_bg(output: &LineRenderOutput, line_idx: usize) -> bool {
8536 let current_line_bg = ratatui::style::Color::Rgb(40, 40, 40);
8537 if let Some(line) = output.lines.get(line_idx) {
8538 line.spans
8539 .iter()
8540 .any(|span| span.style.bg == Some(current_line_bg))
8541 } else {
8542 false
8543 }
8544 }
8545
8546 #[test]
8547 fn current_line_highlight_enabled_highlights_cursor_line() {
8548 let output = render_with_highlight_option("abc\ndef\nghi\n", 0, true);
8549 assert!(
8551 line_has_current_line_bg(&output, 0),
8552 "Cursor line (line 0) should have current_line_bg when highlighting is enabled"
8553 );
8554 assert!(
8556 !line_has_current_line_bg(&output, 1),
8557 "Non-cursor line (line 1) should NOT have current_line_bg"
8558 );
8559 }
8560
8561 #[test]
8562 fn current_line_highlight_disabled_no_highlight() {
8563 let output = render_with_highlight_option("abc\ndef\nghi\n", 0, false);
8564 assert!(
8566 !line_has_current_line_bg(&output, 0),
8567 "Cursor line should NOT have current_line_bg when highlighting is disabled"
8568 );
8569 assert!(
8570 !line_has_current_line_bg(&output, 1),
8571 "Non-cursor line should NOT have current_line_bg when highlighting is disabled"
8572 );
8573 }
8574
8575 #[test]
8576 fn current_line_highlight_follows_cursor_position() {
8577 let output = render_with_highlight_option("abc\ndef\nghi\n", 4, true);
8579 assert!(
8580 !line_has_current_line_bg(&output, 0),
8581 "Line 0 should NOT have current_line_bg when cursor is on line 1"
8582 );
8583 assert!(
8584 line_has_current_line_bg(&output, 1),
8585 "Line 1 should have current_line_bg when cursor is there"
8586 );
8587 assert!(
8588 !line_has_current_line_bg(&output, 2),
8589 "Line 2 should NOT have current_line_bg when cursor is on line 1"
8590 );
8591 }
8592}