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 {
116 let (cols, rows) = terminal.size();
117 let screen_rows = rows.saturating_sub(1) as usize; let cache_size = screen_rows.max(1) * CACHE_MULTIPLIER;
119
120 Self {
121 offset_row: 0,
122 offset_col: 0,
123 show_line_numbers: true,
124 wrap_mode: true,
125 screen_rows,
126 screen_cols: cols as usize,
127 line_layout_cache: vec![None; cache_size],
128 }
129 }
130
131 pub fn invalidate_cache(&mut self) {
133 let cache_size = self.screen_rows.max(1) * CACHE_MULTIPLIER;
134 self.line_layout_cache.clear();
135 self.line_layout_cache.resize(cache_size, None);
136 }
137
138 pub fn invalidate_line(&mut self, logical_row: usize) {
140 if logical_row < self.offset_row {
141 return; }
143
144 let cache_index = logical_row.saturating_sub(self.offset_row);
145 if cache_index < self.line_layout_cache.len() {
146 self.line_layout_cache[cache_index] = None;
147 }
148 }
149
150 #[allow(dead_code)]
152 pub fn invalidate_lines(&mut self, start_row: usize, end_row: usize) {
153 for row in start_row..=end_row {
154 self.invalidate_line(row);
155 }
156 }
157
158 #[allow(dead_code)]
159 pub fn update_size(&mut self) {
160 let size = crossterm::terminal::size().unwrap_or((80, 24));
161 let new_screen_rows = size.1.saturating_sub(1) as usize;
162 let new_screen_cols = size.0 as usize;
163
164 if self.screen_rows != new_screen_rows || self.screen_cols != new_screen_cols {
165 self.screen_rows = new_screen_rows;
166 self.screen_cols = new_screen_cols;
167 self.invalidate_cache(); }
169 }
170
171 pub fn render(
172 &mut self,
173 buffer: &RopeBuffer,
174 cursor: &Cursor,
175 selection: Option<&Selection>,
176 message: Option<&str>,
177 #[cfg(feature = "syntax-highlighting")] highlighted_lines: Option<
178 &std::collections::HashMap<usize, String>,
179 >,
180 ) -> Result<()> {
181 let has_debug_ruler = message.is_some_and(|m| m.starts_with("DEBUG"));
182
183 self.scroll_if_needed(cursor, buffer, has_debug_ruler);
184
185 let mut stdout = io::stdout();
186
187 execute!(stdout, cursor::Hide)?;
188 execute!(stdout, cursor::MoveTo(0, 0))?;
189
190 let ruler_offset = if has_debug_ruler {
191 self.render_column_ruler(&mut stdout, buffer)?;
192 1
193 } else {
194 0
195 };
196
197 let line_num_width = self.calculate_line_number_width(buffer);
198 let available_width = self.get_available_width(buffer);
199
200 let sel_visual_range = selection.map(|sel| {
202 let (start_row, start_col) = sel.start.min(sel.end);
203 let (end_row, end_col) = sel.start.max(sel.end);
204
205 let start_visual_col = if start_row < buffer.line_count() {
207 let line = buffer
208 .line(start_row)
209 .map(|s| s.to_string())
210 .unwrap_or_default();
211 let line = line.trim_end_matches(['\n', '\r']);
212 self.logical_col_to_visual_col(line, start_col)
213 } else {
214 start_col
215 };
216
217 let end_visual_col = if end_row < buffer.line_count() {
219 let line = buffer
220 .line(end_row)
221 .map(|s| s.to_string())
222 .unwrap_or_default();
223 let line = line.trim_end_matches(['\n', '\r']);
224 self.logical_col_to_visual_col(line, end_col)
225 } else {
226 end_col
227 };
228
229 ((start_row, start_visual_col), (end_row, end_visual_col))
230 });
231
232 let mut screen_row = ruler_offset;
233 let mut file_row = self.offset_row;
234
235 while screen_row < self.screen_rows && file_row < buffer.line_count() {
236 queue!(stdout, cursor::MoveTo(0, screen_row as u16))?;
237
238 if self.show_line_numbers {
239 let line_num = format!("{:>width$} ", file_row + 1, width = line_num_width - 1);
240 queue!(stdout, style::SetForegroundColor(Color::DarkGrey))?;
241 queue!(stdout, style::Print(&line_num))?;
242 queue!(stdout, style::ResetColor)?;
243 }
244
245 let cache_index = file_row.saturating_sub(self.offset_row);
246 let layout_opt = self
247 .line_layout_cache
248 .get(cache_index)
249 .and_then(|l| l.as_ref())
250 .cloned();
251
252 let layout = if let Some(layout) = layout_opt {
253 layout
254 } else if let Some(new_layout) =
255 LineLayout::new(buffer, file_row, available_width, self.wrap_mode)
256 {
257 if cache_index < self.line_layout_cache.len() {
258 self.line_layout_cache[cache_index] = Some(new_layout.clone());
259 }
260 new_layout
261 } else {
262 LineLayout {
264 visual_lines: vec![String::new()],
265 visual_height: 1,
266 logical_to_visual: vec![0],
267 }
268 };
269
270 for (visual_idx, visual_line) in layout.visual_lines.iter().enumerate() {
271 if screen_row >= self.screen_rows {
272 break;
273 }
274
275 if visual_idx > 0 {
276 screen_row += 1;
277 if screen_row >= self.screen_rows {
278 break;
279 }
280 queue!(stdout, cursor::MoveTo(0, screen_row as u16))?;
281
282 if self.show_line_numbers {
283 for _ in 0..line_num_width {
284 queue!(stdout, style::Print(" "))?;
285 }
286 }
287 }
288
289 let visual_line_start_col: usize = layout
294 .visual_lines
295 .iter()
296 .take(visual_idx)
297 .map(|line| visual_width(line))
298 .sum();
299 #[cfg(feature = "syntax-highlighting")]
300 let visual_line_width = visual_width(visual_line);
301
302 #[cfg(feature = "syntax-highlighting")]
303 let use_syntax_highlight = selection.is_none()
304 && highlighted_lines.and_then(|h| h.get(&file_row)).is_some();
305
306 #[cfg(not(feature = "syntax-highlighting"))]
307 let use_syntax_highlight = false;
308
309 if let Some(((start_row, start_col), (end_row, end_col))) = sel_visual_range {
310 if file_row >= start_row && file_row <= end_row {
311 let chars: Vec<char> = visual_line.chars().collect();
313 let mut current_visual_pos = visual_line_start_col;
314
315 for &ch in chars.iter() {
316 let ch_width = UnicodeWidthChar::width(ch).unwrap_or(1);
317
318 if !self.wrap_mode && current_visual_pos + ch_width <= self.offset_col {
320 current_visual_pos += ch_width;
321 continue;
322 }
323
324 if !self.wrap_mode
326 && current_visual_pos >= self.offset_col + available_width
327 {
328 break;
329 }
330
331 let is_selected = if file_row == start_row && file_row == end_row {
333 current_visual_pos >= start_col && current_visual_pos < end_col
335 } else if file_row == start_row {
336 current_visual_pos >= start_col
338 } else if file_row == end_row {
339 current_visual_pos < end_col
341 } else {
342 true
344 };
345
346 if is_selected {
347 queue!(stdout, style::SetAttribute(Attribute::Reverse))?;
348 }
349 queue!(stdout, style::Print(ch))?;
350 if is_selected {
351 queue!(stdout, style::SetAttribute(Attribute::NoReverse))?;
352 }
353
354 current_visual_pos += ch_width;
355 }
356 } else {
357 let display_text = if self.wrap_mode {
359 visual_line.clone()
360 } else {
361 self.slice_visible_text(visual_line, self.offset_col, available_width)
362 };
363 queue!(stdout, style::Print(display_text))?;
364 }
365 } else {
366 if use_syntax_highlight {
368 #[cfg(feature = "syntax-highlighting")]
370 if let Some(highlighted) = highlighted_lines.and_then(|h| h.get(&file_row))
371 {
372 if self.wrap_mode {
373 let sliced = slice_ansi_text(
375 highlighted,
376 visual_line_start_col,
377 visual_line_width,
378 );
379 queue!(stdout, style::Print(sliced))?;
380 } else {
381 let sliced =
383 slice_ansi_text(highlighted, self.offset_col, available_width);
384 queue!(stdout, style::Print(sliced))?;
385 }
386 } else {
387 let display_text = if self.wrap_mode {
389 visual_line.to_string()
390 } else {
391 self.slice_visible_text(
392 visual_line,
393 self.offset_col,
394 available_width,
395 )
396 };
397 queue!(stdout, style::Print(display_text))?;
398 }
399
400 #[cfg(not(feature = "syntax-highlighting"))]
401 {
402 let display_text = if self.wrap_mode {
403 visual_line.to_string()
404 } else {
405 self.slice_visible_text(
406 visual_line,
407 self.offset_col,
408 available_width,
409 )
410 };
411 queue!(stdout, style::Print(display_text))?;
412 }
413 } else {
414 let display_text = if self.wrap_mode {
416 visual_line.to_string()
417 } else {
418 self.slice_visible_text(visual_line, self.offset_col, available_width)
419 };
420 queue!(stdout, style::Print(display_text))?;
421 }
422 }
423
424 queue!(
425 stdout,
426 crossterm::terminal::Clear(crossterm::terminal::ClearType::UntilNewLine)
427 )?;
428 }
429
430 screen_row += 1;
431 file_row += 1;
432 }
433
434 while screen_row < self.screen_rows {
436 queue!(stdout, cursor::MoveTo(0, screen_row as u16))?;
437 queue!(stdout, style::SetForegroundColor(Color::DarkGrey))?;
438 queue!(stdout, style::Print("~"))?;
439 queue!(stdout, style::ResetColor)?;
440 queue!(
441 stdout,
442 crossterm::terminal::Clear(crossterm::terminal::ClearType::UntilNewLine)
443 )?;
444 screen_row += 1;
445 }
446
447 self.render_status_bar(buffer, selection.is_some(), message, cursor)?;
448
449 let ruler_offset = if has_debug_ruler { 1 } else { 0 };
451 let (cursor_x, cursor_y) = self.get_cursor_visual_position(cursor, buffer);
452 let cursor_y = cursor_y + ruler_offset;
453 execute!(stdout, cursor::MoveTo(cursor_x as u16, cursor_y as u16))?;
454
455 execute!(stdout, cursor::Show)?;
456 stdout.flush()?;
457 Ok(())
458 }
459
460 pub fn scroll_if_needed(
461 &mut self,
462 cursor: &Cursor,
463 buffer: &RopeBuffer,
464 has_debug_ruler: bool,
465 ) {
466 self.scroll_horizontal_if_needed(cursor, buffer);
468
469 if cursor.row < self.offset_row {
471 self.offset_row = cursor.row;
472 self.invalidate_cache();
473 return;
474 }
475
476 let effective_rows = self.get_effective_screen_rows(has_debug_ruler);
477
478 let jump_threshold = effective_rows * 3;
481 let distance = cursor.row.saturating_sub(self.offset_row);
482
483 if distance > jump_threshold {
484 self.offset_row = cursor.row.saturating_sub(effective_rows / 3);
487 self.invalidate_cache();
488 return;
489 }
490
491 let mut visual_offset = 0;
493 let available_width = self.get_available_width(buffer);
494
495 for row in self.offset_row..=cursor.row {
496 let cache_index = row.saturating_sub(self.offset_row);
497 if let Some(Some(layout)) = self.line_layout_cache.get(cache_index) {
498 visual_offset += layout.visual_height;
499 } else if let Some(layout) =
500 LineLayout::new(buffer, row, available_width, self.wrap_mode)
501 {
502 visual_offset += layout.visual_height;
503 if cache_index < self.line_layout_cache.len() {
504 self.line_layout_cache[cache_index] = Some(layout);
505 }
506 }
507 }
508
509 if visual_offset < effective_rows {
511 return;
512 }
513
514 while self.offset_row < cursor.row && visual_offset >= effective_rows {
516 let top_layout_opt = self
517 .line_layout_cache
518 .first()
519 .and_then(|l| l.as_ref())
520 .cloned();
521
522 if let Some(layout) = top_layout_opt {
523 visual_offset = visual_offset.saturating_sub(layout.visual_height);
524 } else if let Some(layout) =
525 LineLayout::new(buffer, self.offset_row, available_width, self.wrap_mode)
526 {
527 visual_offset = visual_offset.saturating_sub(layout.visual_height);
528 if !self.line_layout_cache.is_empty() {
529 self.line_layout_cache[0] = Some(layout);
530 }
531 }
532
533 self.offset_row += 1;
534
535 if !self.line_layout_cache.is_empty() {
536 self.line_layout_cache.remove(0);
537 self.line_layout_cache.push(None);
538 }
539 }
540 }
541
542 pub fn scroll_horizontal_if_needed(&mut self, cursor: &Cursor, buffer: &RopeBuffer) {
544 if self.wrap_mode {
545 self.offset_col = 0;
546 return;
547 }
548
549 let available_width = self.get_available_width(buffer);
550
551 let line = buffer
553 .line(cursor.row)
554 .map(|s| s.to_string())
555 .unwrap_or_default();
556 let line = line.trim_end_matches(['\n', '\r']);
557 let cursor_visual_col = self.logical_col_to_visual_col(line, cursor.col);
558
559 if cursor_visual_col >= self.offset_col + available_width - HORIZONTAL_SCROLL_MARGIN {
561 self.offset_col =
562 cursor_visual_col.saturating_sub(available_width - HORIZONTAL_SCROLL_MARGIN - 1);
563 }
564
565 if cursor_visual_col < self.offset_col + HORIZONTAL_SCROLL_MARGIN {
567 self.offset_col = cursor_visual_col.saturating_sub(HORIZONTAL_SCROLL_MARGIN);
568 }
569 }
570
571 fn render_status_bar(
572 &self,
573 buffer: &RopeBuffer,
574 selection_mode: bool,
575 message: Option<&str>,
576 cursor: &Cursor,
577 ) -> Result<()> {
578 let mut stdout = io::stdout();
579 queue!(stdout, cursor::MoveTo(0, self.screen_rows as u16))?;
580
581 queue!(stdout, style::SetBackgroundColor(Color::DarkGrey))?;
582 queue!(stdout, style::SetForegroundColor(Color::White))?;
583
584 let modified = if buffer.is_modified() {
585 " [modified]"
586 } else {
587 ""
588 };
589 let filename = buffer.file_name();
590
591 let mode_indicator = if selection_mode {
592 " [Selection Mode]"
593 } else {
594 ""
595 };
596
597 let status = if let Some(msg) = message {
598 format!(" {}{}{} - {}", filename, modified, mode_indicator, msg)
599 } else {
600 format!(
601 " {}{}{} Line {}/{} Ctrl+W:Save Ctrl+Q:Quit",
602 filename,
603 modified,
604 mode_indicator,
605 cursor.row + 1,
606 buffer.line_count()
607 )
608 };
609
610 let status = if visual_width(&status) < self.screen_cols {
612 format!("{:width$}", status, width = self.screen_cols)
613 } else {
614 let mut result = String::new();
615 let mut current_width = 0;
616 for ch in status.chars() {
617 let ch_width = UnicodeWidthChar::width(ch).unwrap_or(1);
618 if current_width + ch_width > self.screen_cols {
619 break;
620 }
621 result.push(ch);
622 current_width += ch_width;
623 }
624 result
625 };
626
627 queue!(stdout, style::Print(status))?;
628 queue!(stdout, style::ResetColor)?;
629
630 Ok(())
631 }
632
633 pub fn toggle_line_numbers(&mut self) {
634 self.show_line_numbers = !self.show_line_numbers;
635 self.wrap_mode = self.show_line_numbers; self.offset_col = 0; self.invalidate_cache();
638 }
639
640 pub fn toggle_display_mode(&mut self) {
642 self.wrap_mode = !self.wrap_mode;
643 self.offset_col = 0; self.invalidate_cache();
645 }
646
647 pub fn get_display_mode_name(&self) -> &'static str {
649 if self.wrap_mode {
650 "Multi-line (Wrap)"
651 } else {
652 "Single-line (Scroll)"
653 }
654 }
655
656 fn slice_visible_text(&self, text: &str, start_col: usize, width: usize) -> String {
658 let mut result = String::new();
659 let mut current_col = 0;
660
661 for ch in text.chars() {
662 let ch_width = UnicodeWidthChar::width(ch).unwrap_or(1);
663
664 if current_col + ch_width <= start_col {
666 current_col += ch_width;
667 continue;
668 }
669
670 if current_col >= start_col + width {
672 break;
673 }
674
675 result.push(ch);
676 current_col += ch_width;
677 }
678
679 result
680 }
681
682 fn calculate_line_number_width(&self, buffer: &RopeBuffer) -> usize {
684 if self.show_line_numbers {
685 buffer.line_count().to_string().len() + 1
686 } else {
687 0
688 }
689 }
690
691 pub fn get_available_width(&self, buffer: &RopeBuffer) -> usize {
693 let line_num_width = self.calculate_line_number_width(buffer);
694 self.screen_cols
695 .saturating_sub(line_num_width)
696 .saturating_sub(1)
697 }
698
699 pub fn calculate_visual_lines_for_row(&self, buffer: &RopeBuffer, row: usize) -> Vec<String> {
701 if row >= buffer.line_count() {
702 return vec![String::new()];
703 }
704
705 let cache_index = row.saturating_sub(self.offset_row);
707 if let Some(Some(layout)) = self.line_layout_cache.get(cache_index) {
708 return layout.visual_lines.clone();
709 }
710
711 let available_width = self.get_available_width(buffer);
712 let line = buffer.line(row).map(|s| s.to_string()).unwrap_or_default();
713 let mut line = line;
714 while matches!(line.chars().last(), Some('\n' | '\r')) {
715 line.pop();
716 }
717
718 let (displayed_line, _) = expand_tabs_and_build_map(&line);
719 if self.wrap_mode {
720 wrap_line(&displayed_line, available_width)
721 } else {
722 vec![displayed_line]
723 }
724 }
725
726 pub fn logical_col_to_visual_col(&self, line: &str, logical_col: usize) -> usize {
728 let mut visual_col = 0;
731 for (idx, ch) in line.chars().enumerate() {
732 if idx >= logical_col {
733 break;
734 }
735 if ch == '\t' {
736 visual_col += TAB_WIDTH;
737 } else {
738 visual_col += UnicodeWidthChar::width(ch).unwrap_or(1);
739 }
740 }
741 visual_col
742 }
743
744 pub fn visual_to_logical_col(
746 &self,
747 buffer: &RopeBuffer,
748 row: usize,
749 visual_line_index: usize,
750 visual_col: usize,
751 ) -> usize {
752 let cache_index = row.saturating_sub(self.offset_row);
754 if let Some(Some(layout)) = self.line_layout_cache.get(cache_index) {
755 if visual_line_index >= layout.visual_lines.len() {
756 return 0;
757 }
758
759 let mut accumulated_width = 0;
761 for line in layout.visual_lines.iter().take(visual_line_index) {
762 accumulated_width += visual_width(line);
763 }
764
765 let col_in_visual =
767 visual_col.min(visual_width(&layout.visual_lines[visual_line_index]));
768 let visual_col_total = accumulated_width + col_in_visual;
769
770 let mut logical_col = 0;
772 for (idx, &vcol) in layout.logical_to_visual.iter().enumerate() {
773 if vcol > visual_col_total {
774 break;
775 }
776 logical_col = idx;
777 }
778 return logical_col;
779 }
780
781 let visual_lines = self.calculate_visual_lines_for_row(buffer, row);
783
784 if visual_line_index >= visual_lines.len() {
785 return 0;
786 }
787
788 let mut accumulated_width = 0;
790 for line in visual_lines.iter().take(visual_line_index) {
791 accumulated_width += visual_width(line);
792 }
793
794 let col_in_visual = visual_col.min(visual_width(&visual_lines[visual_line_index]));
795 let visual_col_total = accumulated_width + col_in_visual;
796
797 if let Some(line) = buffer.line(row) {
798 let mut line_str = line.to_string();
799 while matches!(line_str.chars().last(), Some('\n' | '\r')) {
800 line_str.pop();
801 }
802
803 let mut logical_col = 0;
804 let mut current_visual = 0;
805
806 for ch in line_str.chars() {
807 if current_visual >= visual_col_total {
808 break;
809 }
810
811 if ch == '\t' {
812 current_visual += TAB_WIDTH;
813 } else {
814 current_visual += UnicodeWidthChar::width(ch).unwrap_or(1);
815 }
816
817 logical_col += 1;
818 }
819
820 logical_col
821 } else {
822 0
823 }
824 }
825
826 pub fn get_effective_screen_rows(&self, has_debug_ruler: bool) -> usize {
828 if has_debug_ruler {
829 self.screen_rows.saturating_sub(1)
830 } else {
831 self.screen_rows
832 }
833 }
834
835 pub fn get_cursor_visual_position(
837 &self,
838 cursor: &Cursor,
839 buffer: &RopeBuffer,
840 ) -> (usize, usize) {
841 let line_num_width = self.calculate_line_number_width(buffer);
842
843 let mut screen_y = 0;
845 let mut file_row = self.offset_row;
846
847 while file_row < cursor.row && screen_y < self.screen_rows {
848 let cache_index = file_row.saturating_sub(self.offset_row);
849 let layout_opt = self
850 .line_layout_cache
851 .get(cache_index)
852 .and_then(|l| l.as_ref())
853 .cloned();
854
855 let layout = if let Some(layout) = layout_opt {
856 layout
857 } else {
858 LineLayout::new(
859 buffer,
860 file_row,
861 self.get_available_width(buffer),
862 self.wrap_mode,
863 )
864 .unwrap_or_else(|| LineLayout {
865 visual_lines: vec![String::new()],
866 visual_height: 1,
867 logical_to_visual: vec![0],
868 })
869 };
870
871 screen_y += layout.visual_height;
872 file_row += 1;
873 }
874
875 screen_y += cursor.visual_line_index;
877
878 let screen_y = screen_y.min(self.screen_rows.saturating_sub(1));
880
881 let visual_lines = self.calculate_visual_lines_for_row(buffer, cursor.row);
883 let mut screen_x = line_num_width;
884
885 if cursor.visual_line_index < visual_lines.len() {
886 let mut accumulated_width = 0;
888 for line in visual_lines.iter().take(cursor.visual_line_index) {
889 accumulated_width += visual_width(line);
890 }
891
892 let line_str = buffer
894 .line(cursor.row)
895 .map(|s| s.to_string())
896 .unwrap_or_default();
897 let line_str = line_str.trim_end_matches(['\n', '\r']);
898 let cursor_visual_col = self.logical_col_to_visual_col(line_str, cursor.col);
899
900 let visual_col_in_line = cursor_visual_col.saturating_sub(accumulated_width);
902
903 let adjusted_col = if self.wrap_mode {
905 visual_col_in_line
906 } else {
907 visual_col_in_line.saturating_sub(self.offset_col)
908 };
909
910 screen_x += adjusted_col;
912 }
913
914 (screen_x, screen_y)
915 }
916
917 fn render_column_ruler(&self, stdout: &mut io::Stdout, buffer: &RopeBuffer) -> Result<()> {
919 queue!(stdout, cursor::MoveTo(0, 0))?;
920 queue!(stdout, style::SetForegroundColor(Color::DarkGrey))?;
921
922 let line_num_width = self.calculate_line_number_width(buffer);
923
924 for _ in 0..line_num_width {
925 queue!(stdout, style::Print(" "))?;
926 }
927
928 let available_cols = self
929 .screen_cols
930 .saturating_sub(line_num_width)
931 .saturating_sub(1);
932 for col in 0..available_cols {
933 let digit = col % 10;
934 queue!(stdout, style::Print(digit))?;
935 }
936
937 queue!(stdout, style::ResetColor)?;
938 Ok(())
939 }
940}
941
942fn wrap_line(line: &str, max_width: usize) -> Vec<String> {
944 if max_width == 0 {
945 return vec![String::new()];
946 }
947
948 let mut result = Vec::new();
949 let mut current_line = String::new();
950 let mut current_width = 0;
951
952 for ch in line.chars() {
953 let char_width = UnicodeWidthChar::width(ch).unwrap_or(1);
954
955 if current_width + char_width > max_width && !current_line.is_empty() {
956 result.push(current_line);
957 current_line = String::new();
958 current_width = 0;
959 }
960
961 current_line.push(ch);
962 current_width += char_width;
963 }
964
965 if !current_line.is_empty() {
966 result.push(current_line);
967 }
968
969 if result.is_empty() {
970 result.push(String::new());
971 }
972
973 result
974}