1use crate::buffer::RopeBuffer;
2use crate::cursor::Cursor;
3use crate::terminal::Terminal;
4#[cfg(feature = "syntax-highlighting")]
5use crate::utils::slice_ansi_text;
6use crate::utils::visual_width;
7use anyhow::Result;
8use crossterm::{
9 cursor, execute, queue,
10 style::{self, Attribute, Color},
11};
12use std::io::{self, Write};
13use unicode_width::UnicodeWidthChar;
14
15const TAB_WIDTH: usize = 4; const CACHE_MULTIPLIER: usize = 3; const HORIZONTAL_SCROLL_MARGIN: usize = 5; #[derive(Clone, Debug)]
21pub struct LineLayout {
22 pub visual_lines: Vec<String>,
24 pub visual_height: usize,
26 pub logical_to_visual: Vec<usize>,
28}
29
30impl LineLayout {
31 pub fn new(
32 buffer: &RopeBuffer,
33 row: usize,
34 available_width: usize,
35 wrap: bool,
36 ) -> Option<Self> {
37 let line = buffer.line(row)?;
38 let mut line_str = line.to_string();
39 while matches!(line_str.chars().last(), Some('\n' | '\r')) {
41 line_str.pop();
42 }
43
44 let (displayed_line, logical_to_visual) = expand_tabs_and_build_map(&line_str);
45 let visual_lines = if wrap {
46 wrap_line(&displayed_line, available_width)
47 } else {
48 vec![displayed_line] };
50 let visual_height = visual_lines.len();
51
52 Some(LineLayout {
53 visual_lines,
54 visual_height,
55 logical_to_visual,
56 })
57 }
58}
59
60fn expand_tabs_and_build_map(line: &str) -> (String, Vec<usize>) {
61 let mut displayed = String::new();
62 let mut logical_to_visual = Vec::new();
63 let mut visual_col = 0;
64
65 for ch in line.chars() {
66 logical_to_visual.push(visual_col);
68
69 if ch == '\t' {
70 for _ in 0..TAB_WIDTH {
71 displayed.push(' ');
72 }
73 visual_col += TAB_WIDTH;
74 } else {
75 let w = UnicodeWidthChar::width(ch).unwrap_or(1);
76 displayed.push(ch);
77 visual_col += w;
78 }
79 }
80
81 logical_to_visual.push(visual_col);
83
84 (displayed, logical_to_visual)
85}
86
87#[allow(dead_code)]
88fn calculate_hash(line: &str) -> u64 {
89 use std::collections::hash_map::DefaultHasher;
90 use std::hash::{Hash, Hasher};
91
92 let mut hasher = DefaultHasher::new();
93 line.hash(&mut hasher);
94 hasher.finish()
95}
96
97#[derive(Debug, Clone, Copy)]
98pub struct Selection {
99 pub start: (usize, usize), pub end: (usize, usize), }
102
103pub struct View {
104 pub offset_row: usize, pub offset_col: usize, pub show_line_numbers: bool,
107 pub wrap_mode: bool, pub screen_rows: usize,
109 pub screen_cols: usize,
110 line_layout_cache: Vec<Option<LineLayout>>,
112}
113
114impl View {
115 pub fn new(terminal: &Terminal) -> Self {
117 let (cols, rows) = terminal.size();
118 let screen_rows = rows.saturating_sub(1) as usize; let cache_size = screen_rows.max(1) * CACHE_MULTIPLIER;
120
121 Self {
122 offset_row: 0,
123 offset_col: 0,
124 show_line_numbers: true,
125 wrap_mode: true,
126 screen_rows,
127 screen_cols: cols as usize,
128 line_layout_cache: vec![None; cache_size],
129 }
130 }
131
132 pub fn new_simple(rows: usize, cols: usize) -> Self {
144 let cache_size = rows.max(1) * CACHE_MULTIPLIER;
145
146 Self {
147 offset_row: 0,
148 offset_col: 0,
149 show_line_numbers: true,
150 wrap_mode: true,
151 screen_rows: rows,
152 screen_cols: cols,
153 line_layout_cache: vec![None; cache_size],
154 }
155 }
156
157 pub fn resize(&mut self, rows: usize, cols: usize) {
163 self.screen_rows = rows;
164 self.screen_cols = cols;
165 self.invalidate_cache();
166 }
167
168 pub fn invalidate_cache(&mut self) {
170 let cache_size = self.screen_rows.max(1) * CACHE_MULTIPLIER;
171 self.line_layout_cache.clear();
172 self.line_layout_cache.resize(cache_size, None);
173 }
174
175 pub fn invalidate_line(&mut self, logical_row: usize) {
177 if logical_row < self.offset_row {
178 return; }
180
181 let cache_index = logical_row.saturating_sub(self.offset_row);
182 if cache_index < self.line_layout_cache.len() {
183 self.line_layout_cache[cache_index] = None;
184 }
185 }
186
187 #[allow(dead_code)]
189 pub fn invalidate_lines(&mut self, start_row: usize, end_row: usize) {
190 for row in start_row..=end_row {
191 self.invalidate_line(row);
192 }
193 }
194
195 #[allow(dead_code)]
196 pub fn update_size(&mut self) {
197 let size = crossterm::terminal::size().unwrap_or((80, 24));
198 let new_screen_rows = size.1.saturating_sub(1) as usize;
199 let new_screen_cols = size.0 as usize;
200
201 if self.screen_rows != new_screen_rows || self.screen_cols != new_screen_cols {
202 self.screen_rows = new_screen_rows;
203 self.screen_cols = new_screen_cols;
204 self.invalidate_cache(); }
206 }
207
208 pub fn render(
209 &mut self,
210 buffer: &RopeBuffer,
211 cursor: &Cursor,
212 selection: Option<&Selection>,
213 message: Option<&str>,
214 #[cfg(feature = "syntax-highlighting")] highlighted_lines: Option<
215 &std::collections::HashMap<usize, String>,
216 >,
217 ) -> Result<()> {
218 let has_debug_ruler = message.is_some_and(|m| m.starts_with("DEBUG"));
219
220 self.scroll_if_needed(cursor, buffer, has_debug_ruler);
221
222 let mut stdout = io::stdout();
223
224 execute!(stdout, cursor::Hide)?;
225 execute!(stdout, cursor::MoveTo(0, 0))?;
226
227 let ruler_offset = if has_debug_ruler {
228 self.render_column_ruler(&mut stdout, buffer)?;
229 1
230 } else {
231 0
232 };
233
234 let line_num_width = self.calculate_line_number_width(buffer);
235 let available_width = self.get_available_width(buffer);
236
237 let sel_visual_range = selection.map(|sel| {
239 let (start_row, start_col) = sel.start.min(sel.end);
240 let (end_row, end_col) = sel.start.max(sel.end);
241
242 let start_visual_col = if start_row < buffer.line_count() {
244 let line = buffer
245 .line(start_row)
246 .map(|s| s.to_string())
247 .unwrap_or_default();
248 let line = line.trim_end_matches(['\n', '\r']);
249 self.logical_col_to_visual_col(line, start_col)
250 } else {
251 start_col
252 };
253
254 let end_visual_col = if end_row < buffer.line_count() {
256 let line = buffer
257 .line(end_row)
258 .map(|s| s.to_string())
259 .unwrap_or_default();
260 let line = line.trim_end_matches(['\n', '\r']);
261 self.logical_col_to_visual_col(line, end_col)
262 } else {
263 end_col
264 };
265
266 ((start_row, start_visual_col), (end_row, end_visual_col))
267 });
268
269 let mut screen_row = ruler_offset;
270 let mut file_row = self.offset_row;
271
272 while screen_row < self.screen_rows && file_row < buffer.line_count() {
273 queue!(stdout, cursor::MoveTo(0, screen_row as u16))?;
274
275 if self.show_line_numbers {
276 let line_num = format!("{:>width$} ", file_row + 1, width = line_num_width - 1);
277 queue!(stdout, style::SetForegroundColor(Color::DarkGrey))?;
278 queue!(stdout, style::Print(&line_num))?;
279 queue!(stdout, style::ResetColor)?;
280 }
281
282 let cache_index = file_row.saturating_sub(self.offset_row);
283 let layout_opt = self
284 .line_layout_cache
285 .get(cache_index)
286 .and_then(|l| l.as_ref())
287 .cloned();
288
289 let layout = if let Some(layout) = layout_opt {
290 layout
291 } else if let Some(new_layout) =
292 LineLayout::new(buffer, file_row, available_width, self.wrap_mode)
293 {
294 if cache_index < self.line_layout_cache.len() {
295 self.line_layout_cache[cache_index] = Some(new_layout.clone());
296 }
297 new_layout
298 } else {
299 LineLayout {
301 visual_lines: vec![String::new()],
302 visual_height: 1,
303 logical_to_visual: vec![0],
304 }
305 };
306
307 for (visual_idx, visual_line) in layout.visual_lines.iter().enumerate() {
308 if screen_row >= self.screen_rows {
309 break;
310 }
311
312 if visual_idx > 0 {
313 screen_row += 1;
314 if screen_row >= self.screen_rows {
315 break;
316 }
317 queue!(stdout, cursor::MoveTo(0, screen_row as u16))?;
318
319 if self.show_line_numbers {
320 for _ in 0..line_num_width {
321 queue!(stdout, style::Print(" "))?;
322 }
323 }
324 }
325
326 let visual_line_start_col: usize = layout
331 .visual_lines
332 .iter()
333 .take(visual_idx)
334 .map(|line| visual_width(line))
335 .sum();
336 #[cfg(feature = "syntax-highlighting")]
337 let visual_line_width = visual_width(visual_line);
338
339 #[cfg(feature = "syntax-highlighting")]
340 let use_syntax_highlight = selection.is_none()
341 && highlighted_lines.and_then(|h| h.get(&file_row)).is_some();
342
343 #[cfg(not(feature = "syntax-highlighting"))]
344 let use_syntax_highlight = false;
345
346 if let Some(((start_row, start_col), (end_row, end_col))) = sel_visual_range {
347 if file_row >= start_row && file_row <= end_row {
348 let chars: Vec<char> = visual_line.chars().collect();
350 let mut current_visual_pos = visual_line_start_col;
351
352 for &ch in chars.iter() {
353 let ch_width = UnicodeWidthChar::width(ch).unwrap_or(1);
354
355 if !self.wrap_mode && current_visual_pos + ch_width <= self.offset_col {
357 current_visual_pos += ch_width;
358 continue;
359 }
360
361 if !self.wrap_mode
363 && current_visual_pos >= self.offset_col + available_width
364 {
365 break;
366 }
367
368 let is_selected = if file_row == start_row && file_row == end_row {
370 current_visual_pos >= start_col && current_visual_pos < end_col
372 } else if file_row == start_row {
373 current_visual_pos >= start_col
375 } else if file_row == end_row {
376 current_visual_pos < end_col
378 } else {
379 true
381 };
382
383 if is_selected {
384 queue!(stdout, style::SetAttribute(Attribute::Reverse))?;
385 }
386 queue!(stdout, style::Print(ch))?;
387 if is_selected {
388 queue!(stdout, style::SetAttribute(Attribute::NoReverse))?;
389 }
390
391 current_visual_pos += ch_width;
392 }
393 } else {
394 let display_text = if self.wrap_mode {
396 visual_line.clone()
397 } else {
398 self.slice_visible_text(visual_line, self.offset_col, available_width)
399 };
400 queue!(stdout, style::Print(display_text))?;
401 }
402 } else {
403 if use_syntax_highlight {
405 #[cfg(feature = "syntax-highlighting")]
407 if let Some(highlighted) = highlighted_lines.and_then(|h| h.get(&file_row))
408 {
409 if self.wrap_mode {
410 let sliced = slice_ansi_text(
412 highlighted,
413 visual_line_start_col,
414 visual_line_width,
415 );
416 queue!(stdout, style::Print(sliced))?;
417 } else {
418 let sliced =
420 slice_ansi_text(highlighted, self.offset_col, available_width);
421 queue!(stdout, style::Print(sliced))?;
422 }
423 } else {
424 let display_text = if self.wrap_mode {
426 visual_line.to_string()
427 } else {
428 self.slice_visible_text(
429 visual_line,
430 self.offset_col,
431 available_width,
432 )
433 };
434 queue!(stdout, style::Print(display_text))?;
435 }
436
437 #[cfg(not(feature = "syntax-highlighting"))]
438 {
439 let display_text = if self.wrap_mode {
440 visual_line.to_string()
441 } else {
442 self.slice_visible_text(
443 visual_line,
444 self.offset_col,
445 available_width,
446 )
447 };
448 queue!(stdout, style::Print(display_text))?;
449 }
450 } else {
451 let display_text = if self.wrap_mode {
453 visual_line.to_string()
454 } else {
455 self.slice_visible_text(visual_line, self.offset_col, available_width)
456 };
457 queue!(stdout, style::Print(display_text))?;
458 }
459 }
460
461 queue!(
462 stdout,
463 crossterm::terminal::Clear(crossterm::terminal::ClearType::UntilNewLine)
464 )?;
465 }
466
467 screen_row += 1;
468 file_row += 1;
469 }
470
471 while screen_row < self.screen_rows {
473 queue!(stdout, cursor::MoveTo(0, screen_row as u16))?;
474 queue!(stdout, style::SetForegroundColor(Color::DarkGrey))?;
475 queue!(stdout, style::Print("~"))?;
476 queue!(stdout, style::ResetColor)?;
477 queue!(
478 stdout,
479 crossterm::terminal::Clear(crossterm::terminal::ClearType::UntilNewLine)
480 )?;
481 screen_row += 1;
482 }
483
484 self.render_status_bar(buffer, selection.is_some(), message, cursor)?;
485
486 let ruler_offset = if has_debug_ruler { 1 } else { 0 };
488 let (cursor_x, cursor_y) = self.get_cursor_visual_position(cursor, buffer);
489 let cursor_y = cursor_y + ruler_offset;
490 execute!(stdout, cursor::MoveTo(cursor_x as u16, cursor_y as u16))?;
491
492 execute!(stdout, cursor::Show)?;
493 stdout.flush()?;
494 Ok(())
495 }
496
497 pub fn scroll_if_needed(
498 &mut self,
499 cursor: &Cursor,
500 buffer: &RopeBuffer,
501 has_debug_ruler: bool,
502 ) {
503 self.scroll_horizontal_if_needed(cursor, buffer);
505
506 if cursor.row < self.offset_row {
508 self.offset_row = cursor.row;
509 self.invalidate_cache();
510 return;
511 }
512
513 let effective_rows = self.get_effective_screen_rows(has_debug_ruler);
514
515 let jump_threshold = effective_rows * 3;
518 let distance = cursor.row.saturating_sub(self.offset_row);
519
520 if distance > jump_threshold {
521 self.offset_row = cursor.row.saturating_sub(effective_rows / 3);
524 self.invalidate_cache();
525 return;
526 }
527
528 let mut visual_offset = 0;
530 let available_width = self.get_available_width(buffer);
531
532 for row in self.offset_row..=cursor.row {
533 let cache_index = row.saturating_sub(self.offset_row);
534 if let Some(Some(layout)) = self.line_layout_cache.get(cache_index) {
535 visual_offset += layout.visual_height;
536 } else if let Some(layout) =
537 LineLayout::new(buffer, row, available_width, self.wrap_mode)
538 {
539 visual_offset += layout.visual_height;
540 if cache_index < self.line_layout_cache.len() {
541 self.line_layout_cache[cache_index] = Some(layout);
542 }
543 }
544 }
545
546 if visual_offset < effective_rows {
548 return;
549 }
550
551 while self.offset_row < cursor.row && visual_offset >= effective_rows {
553 let top_layout_opt = self
554 .line_layout_cache
555 .first()
556 .and_then(|l| l.as_ref())
557 .cloned();
558
559 if let Some(layout) = top_layout_opt {
560 visual_offset = visual_offset.saturating_sub(layout.visual_height);
561 } else if let Some(layout) =
562 LineLayout::new(buffer, self.offset_row, available_width, self.wrap_mode)
563 {
564 visual_offset = visual_offset.saturating_sub(layout.visual_height);
565 if !self.line_layout_cache.is_empty() {
566 self.line_layout_cache[0] = Some(layout);
567 }
568 }
569
570 self.offset_row += 1;
571
572 if !self.line_layout_cache.is_empty() {
573 self.line_layout_cache.remove(0);
574 self.line_layout_cache.push(None);
575 }
576 }
577 }
578
579 pub fn scroll_horizontal_if_needed(&mut self, cursor: &Cursor, buffer: &RopeBuffer) {
581 if self.wrap_mode {
582 self.offset_col = 0;
583 return;
584 }
585
586 let available_width = self.get_available_width(buffer);
587
588 let line = buffer
590 .line(cursor.row)
591 .map(|s| s.to_string())
592 .unwrap_or_default();
593 let line = line.trim_end_matches(['\n', '\r']);
594 let cursor_visual_col = self.logical_col_to_visual_col(line, cursor.col);
595
596 if cursor_visual_col >= self.offset_col + available_width - HORIZONTAL_SCROLL_MARGIN {
598 self.offset_col =
599 cursor_visual_col.saturating_sub(available_width - HORIZONTAL_SCROLL_MARGIN - 1);
600 }
601
602 if cursor_visual_col < self.offset_col + HORIZONTAL_SCROLL_MARGIN {
604 self.offset_col = cursor_visual_col.saturating_sub(HORIZONTAL_SCROLL_MARGIN);
605 }
606 }
607
608 fn render_status_bar(
609 &self,
610 buffer: &RopeBuffer,
611 selection_mode: bool,
612 message: Option<&str>,
613 cursor: &Cursor,
614 ) -> Result<()> {
615 let mut stdout = io::stdout();
616 queue!(stdout, cursor::MoveTo(0, self.screen_rows as u16))?;
617
618 queue!(stdout, style::SetBackgroundColor(Color::DarkGrey))?;
619 queue!(stdout, style::SetForegroundColor(Color::White))?;
620
621 let modified = if buffer.is_modified() {
622 " [modified]"
623 } else {
624 ""
625 };
626 let filename = buffer.file_name();
627
628 let mode_indicator = if selection_mode {
629 " [Selection Mode]"
630 } else {
631 ""
632 };
633
634 let status = if let Some(msg) = message {
635 format!(" {}{}{} - {}", filename, modified, mode_indicator, msg)
636 } else {
637 format!(
638 " {}{}{} Line {}/{} Ctrl+W:Save Ctrl+Q:Quit",
639 filename,
640 modified,
641 mode_indicator,
642 cursor.row + 1,
643 buffer.line_count()
644 )
645 };
646
647 let status = if visual_width(&status) < self.screen_cols {
649 format!("{:width$}", status, width = self.screen_cols)
650 } else {
651 let mut result = String::new();
652 let mut current_width = 0;
653 for ch in status.chars() {
654 let ch_width = UnicodeWidthChar::width(ch).unwrap_or(1);
655 if current_width + ch_width > self.screen_cols {
656 break;
657 }
658 result.push(ch);
659 current_width += ch_width;
660 }
661 result
662 };
663
664 queue!(stdout, style::Print(status))?;
665 queue!(stdout, style::ResetColor)?;
666
667 Ok(())
668 }
669
670 pub fn toggle_line_numbers(&mut self) {
671 self.show_line_numbers = !self.show_line_numbers;
672 self.wrap_mode = self.show_line_numbers; self.offset_col = 0; self.invalidate_cache();
675 }
676
677 pub fn toggle_display_mode(&mut self) {
679 self.wrap_mode = !self.wrap_mode;
680 self.offset_col = 0; self.invalidate_cache();
682 }
683
684 pub fn get_display_mode_name(&self) -> &'static str {
686 if self.wrap_mode {
687 "Multi-line (Wrap)"
688 } else {
689 "Single-line (Scroll)"
690 }
691 }
692
693 fn slice_visible_text(&self, text: &str, start_col: usize, width: usize) -> String {
695 let mut result = String::new();
696 let mut current_col = 0;
697
698 for ch in text.chars() {
699 let ch_width = UnicodeWidthChar::width(ch).unwrap_or(1);
700
701 if current_col + ch_width <= start_col {
703 current_col += ch_width;
704 continue;
705 }
706
707 if current_col >= start_col + width {
709 break;
710 }
711
712 result.push(ch);
713 current_col += ch_width;
714 }
715
716 result
717 }
718
719 fn calculate_line_number_width(&self, buffer: &RopeBuffer) -> usize {
721 if self.show_line_numbers {
722 buffer.line_count().to_string().len() + 1
723 } else {
724 0
725 }
726 }
727
728 pub fn get_available_width(&self, buffer: &RopeBuffer) -> usize {
730 let line_num_width = self.calculate_line_number_width(buffer);
731 self.screen_cols
732 .saturating_sub(line_num_width)
733 .saturating_sub(1)
734 }
735
736 pub fn calculate_visual_lines_for_row(&self, buffer: &RopeBuffer, row: usize) -> Vec<String> {
738 if row >= buffer.line_count() {
739 return vec![String::new()];
740 }
741
742 let cache_index = row.saturating_sub(self.offset_row);
744 if let Some(Some(layout)) = self.line_layout_cache.get(cache_index) {
745 return layout.visual_lines.clone();
746 }
747
748 let available_width = self.get_available_width(buffer);
749 let line = buffer.line(row).map(|s| s.to_string()).unwrap_or_default();
750 let mut line = line;
751 while matches!(line.chars().last(), Some('\n' | '\r')) {
752 line.pop();
753 }
754
755 let (displayed_line, _) = expand_tabs_and_build_map(&line);
756 if self.wrap_mode {
757 wrap_line(&displayed_line, available_width)
758 } else {
759 vec![displayed_line]
760 }
761 }
762
763 pub fn logical_col_to_visual_col(&self, line: &str, logical_col: usize) -> usize {
765 let mut visual_col = 0;
768 for (idx, ch) in line.chars().enumerate() {
769 if idx >= logical_col {
770 break;
771 }
772 if ch == '\t' {
773 visual_col += TAB_WIDTH;
774 } else {
775 visual_col += UnicodeWidthChar::width(ch).unwrap_or(1);
776 }
777 }
778 visual_col
779 }
780
781 pub fn visual_to_logical_col(
783 &self,
784 buffer: &RopeBuffer,
785 row: usize,
786 visual_line_index: usize,
787 visual_col: usize,
788 ) -> usize {
789 let cache_index = row.saturating_sub(self.offset_row);
791 if let Some(Some(layout)) = self.line_layout_cache.get(cache_index) {
792 if visual_line_index >= layout.visual_lines.len() {
793 return 0;
794 }
795
796 let mut accumulated_width = 0;
798 for line in layout.visual_lines.iter().take(visual_line_index) {
799 accumulated_width += visual_width(line);
800 }
801
802 let col_in_visual =
804 visual_col.min(visual_width(&layout.visual_lines[visual_line_index]));
805 let visual_col_total = accumulated_width + col_in_visual;
806
807 let mut logical_col = 0;
809 for (idx, &vcol) in layout.logical_to_visual.iter().enumerate() {
810 if vcol > visual_col_total {
811 break;
812 }
813 logical_col = idx;
814 }
815 return logical_col;
816 }
817
818 let visual_lines = self.calculate_visual_lines_for_row(buffer, row);
820
821 if visual_line_index >= visual_lines.len() {
822 return 0;
823 }
824
825 let mut accumulated_width = 0;
827 for line in visual_lines.iter().take(visual_line_index) {
828 accumulated_width += visual_width(line);
829 }
830
831 let col_in_visual = visual_col.min(visual_width(&visual_lines[visual_line_index]));
832 let visual_col_total = accumulated_width + col_in_visual;
833
834 if let Some(line) = buffer.line(row) {
835 let mut line_str = line.to_string();
836 while matches!(line_str.chars().last(), Some('\n' | '\r')) {
837 line_str.pop();
838 }
839
840 let mut logical_col = 0;
841 let mut current_visual = 0;
842
843 for ch in line_str.chars() {
844 if current_visual >= visual_col_total {
845 break;
846 }
847
848 if ch == '\t' {
849 current_visual += TAB_WIDTH;
850 } else {
851 current_visual += UnicodeWidthChar::width(ch).unwrap_or(1);
852 }
853
854 logical_col += 1;
855 }
856
857 logical_col
858 } else {
859 0
860 }
861 }
862
863 pub fn get_effective_screen_rows(&self, has_debug_ruler: bool) -> usize {
865 if has_debug_ruler {
866 self.screen_rows.saturating_sub(1)
867 } else {
868 self.screen_rows
869 }
870 }
871
872 pub fn get_cursor_visual_position(
874 &self,
875 cursor: &Cursor,
876 buffer: &RopeBuffer,
877 ) -> (usize, usize) {
878 let line_num_width = self.calculate_line_number_width(buffer);
879
880 let mut screen_y = 0;
882 let mut file_row = self.offset_row;
883
884 while file_row < cursor.row && screen_y < self.screen_rows {
885 let cache_index = file_row.saturating_sub(self.offset_row);
886 let layout_opt = self
887 .line_layout_cache
888 .get(cache_index)
889 .and_then(|l| l.as_ref())
890 .cloned();
891
892 let layout = if let Some(layout) = layout_opt {
893 layout
894 } else {
895 LineLayout::new(
896 buffer,
897 file_row,
898 self.get_available_width(buffer),
899 self.wrap_mode,
900 )
901 .unwrap_or_else(|| LineLayout {
902 visual_lines: vec![String::new()],
903 visual_height: 1,
904 logical_to_visual: vec![0],
905 })
906 };
907
908 screen_y += layout.visual_height;
909 file_row += 1;
910 }
911
912 screen_y += cursor.visual_line_index;
914
915 let screen_y = screen_y.min(self.screen_rows.saturating_sub(1));
917
918 let visual_lines = self.calculate_visual_lines_for_row(buffer, cursor.row);
920 let mut screen_x = line_num_width;
921
922 if cursor.visual_line_index < visual_lines.len() {
923 let mut accumulated_width = 0;
925 for line in visual_lines.iter().take(cursor.visual_line_index) {
926 accumulated_width += visual_width(line);
927 }
928
929 let line_str = buffer
931 .line(cursor.row)
932 .map(|s| s.to_string())
933 .unwrap_or_default();
934 let line_str = line_str.trim_end_matches(['\n', '\r']);
935 let cursor_visual_col = self.logical_col_to_visual_col(line_str, cursor.col);
936
937 let visual_col_in_line = cursor_visual_col.saturating_sub(accumulated_width);
939
940 let adjusted_col = if self.wrap_mode {
942 visual_col_in_line
943 } else {
944 visual_col_in_line.saturating_sub(self.offset_col)
945 };
946
947 screen_x += adjusted_col;
949 }
950
951 (screen_x, screen_y)
952 }
953
954 fn render_column_ruler(&self, stdout: &mut io::Stdout, buffer: &RopeBuffer) -> Result<()> {
956 queue!(stdout, cursor::MoveTo(0, 0))?;
957 queue!(stdout, style::SetForegroundColor(Color::DarkGrey))?;
958
959 let line_num_width = self.calculate_line_number_width(buffer);
960
961 for _ in 0..line_num_width {
962 queue!(stdout, style::Print(" "))?;
963 }
964
965 let available_cols = self
966 .screen_cols
967 .saturating_sub(line_num_width)
968 .saturating_sub(1);
969 for col in 0..available_cols {
970 let digit = col % 10;
971 queue!(stdout, style::Print(digit))?;
972 }
973
974 queue!(stdout, style::ResetColor)?;
975 Ok(())
976 }
977}
978
979fn wrap_line(line: &str, max_width: usize) -> Vec<String> {
981 if max_width == 0 {
982 return vec![String::new()];
983 }
984
985 let mut result = Vec::new();
986 let mut current_line = String::new();
987 let mut current_width = 0;
988
989 for ch in line.chars() {
990 let char_width = UnicodeWidthChar::width(ch).unwrap_or(1);
991
992 if current_width + char_width > max_width && !current_line.is_empty() {
993 result.push(current_line);
994 current_line = String::new();
995 current_width = 0;
996 }
997
998 current_line.push(ch);
999 current_width += char_width;
1000 }
1001
1002 if !current_line.is_empty() {
1003 result.push(current_line);
1004 }
1005
1006 if result.is_empty() {
1007 result.push(String::new());
1008 }
1009
1010 result
1011}