wedi_core/
view.rs

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
15// 視圖配置常量
16const TAB_WIDTH: usize = 4; // Tab 寬度(空格數)
17const CACHE_MULTIPLIER: usize = 3; // 緩存大小倍數(螢幕行數 × 倍數)
18const HORIZONTAL_SCROLL_MARGIN: usize = 5; // 水平滾動邊界預留
19
20#[derive(Clone, Debug)]
21pub struct LineLayout {
22    /// 視覺行(已處理 Tab 並依螢幕寬度換行)
23    pub visual_lines: Vec<String>,
24    /// 視覺行高度(visual_lines.len())
25    pub visual_height: usize,
26    /// logical_col -> visual_col(整行累計視覺座標)
27    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        // 去掉結尾換行符
40        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] // 單行模式:不切分
49        };
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_col 對應的視覺座標」
67        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    // 尾端一個 mapping,讓「行尾」也有對應視覺座標
82    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), // (row, col)
100    pub end: (usize, usize),   // (row, col)
101}
102
103pub struct View {
104    pub offset_row: usize, // 視窗頂部顯示的行號(邏輯行)
105    pub offset_col: usize, // 水平偏移(單行模式用)
106    pub show_line_numbers: bool,
107    pub wrap_mode: bool, // 換行模式(true=多行換行, false=單行水平滾動)
108    pub screen_rows: usize,
109    pub screen_cols: usize,
110    // 行快取:從 offset_row 起往下的數行
111    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; // 減去狀態欄
118        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    /// 完全清空緩存(用於大範圍變更或視窗調整)
132    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    /// 部分失效:僅清除指定邏輯行的緩存(用於單行編輯)
139    pub fn invalidate_line(&mut self, logical_row: usize) {
140        if logical_row < self.offset_row {
141            return; // 不在可見範圍內,無需清除
142        }
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    /// 部分失效:清除指定範圍的緩存(用於多行編輯)
151    #[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(); // 寬度或高度改變時使快取失效
168        }
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        // 計算選擇範圍(轉換為視覺列)
201        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            // 將start_col轉換為視覺列
206            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            // 將end_col轉換為視覺列
218            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                // 空行或超出範圍
263                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                // 渲染視覺行,支持 selection 高亮和語法高亮
290
291                // 檢查是否有語法高亮(無選擇時)
292                // 計算這個 visual_line 在邏輯行中的視覺起始位置
293                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                        // 這一行有選擇,需要逐字符渲染
312                        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                            // 單行模式:跳過 offset_col 之前的字符
319                            if !self.wrap_mode && current_visual_pos + ch_width <= self.offset_col {
320                                current_visual_pos += ch_width;
321                                continue;
322                            }
323
324                            // 單行模式:超出可見範圍則停止
325                            if !self.wrap_mode
326                                && current_visual_pos >= self.offset_col + available_width
327                            {
328                                break;
329                            }
330
331                            // 判斷這個字符是否在選擇範圍內
332                            let is_selected = if file_row == start_row && file_row == end_row {
333                                // 選擇在同一行
334                                current_visual_pos >= start_col && current_visual_pos < end_col
335                            } else if file_row == start_row {
336                                // 選擇起始行
337                                current_visual_pos >= start_col
338                            } else if file_row == end_row {
339                                // 選擇結束行
340                                current_visual_pos < end_col
341                            } else {
342                                // 選擇中間的行,全選
343                                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                        // 這一行沒有選擇,直接打印(單行模式需要截取)
358                        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                    // 沒有選擇
367                    if use_syntax_highlight {
368                        // 使用語法高亮
369                        #[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                                // 多行模式:截取當前視覺行對應的部分
374                                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                                // 單行模式:使用 ANSI 切割函數截取可見部分
382                                let sliced =
383                                    slice_ansi_text(highlighted, self.offset_col, available_width);
384                                queue!(stdout, style::Print(sliced))?;
385                            }
386                        } else {
387                            // 降級為純文字
388                            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                        // 純文字渲染
415                        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        // 畫底部的 ~ 行
435        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        // 移動終端光標到當前cursor位置
450        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        // 水平滾動(單行模式)
467        self.scroll_horizontal_if_needed(cursor, buffer);
468
469        // 向上滾動
470        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        // 大幅跳轉優化:如果跳轉距離超過 3 個螢幕高度,直接設置 offset_row
479        // 這避免了計算中間所有行的視覺高度,大幅提升大文件跳轉性能
480        let jump_threshold = effective_rows * 3;
481        let distance = cursor.row.saturating_sub(self.offset_row);
482
483        if distance > jump_threshold {
484            // 將 offset_row 設置為讓光標位於螢幕中間偏上的位置
485            // 這樣用戶可以看到光標上下文,體驗更好
486            self.offset_row = cursor.row.saturating_sub(effective_rows / 3);
487            self.invalidate_cache();
488            return;
489        }
490
491        // 計算目前 offset_row ~ cursor.row 的視覺高度
492        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        // 如果沒超出螢幕,就不用動
510        if visual_offset < effective_rows {
511            return;
512        }
513
514        // 向下推 offset_row,每次扣掉最上面那一行的視覺高度
515        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    /// 水平滾動(單行模式專用)
543    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        // 計算游標的視覺列
552        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        // 游標超出右邊界
560        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        // 游標超出左邊界
566        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        // 確保狀態欄填滿整行(使用視覺寬度)
611        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; // 連動切換換行模式
636        self.offset_col = 0; // 重置水平偏移
637        self.invalidate_cache();
638    }
639
640    /// 切換顯示模式(單行/多行),不影響行號顯示
641    pub fn toggle_display_mode(&mut self) {
642        self.wrap_mode = !self.wrap_mode;
643        self.offset_col = 0; // 重置水平偏移
644        self.invalidate_cache();
645    }
646
647    /// 獲取當前顯示模式名稱
648    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    /// 截取可見文字(處理中文寬度,用於單行模式)
657    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            // 跳過 offset 之前的字符
665            if current_col + ch_width <= start_col {
666                current_col += ch_width;
667                continue;
668            }
669
670            // 超出可見範圍則停止
671            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    /// 計算行號寬度(包含右側空格)
683    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    /// 獲取可用於顯示內容的寬度(扣除行號寬度)
692    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    /// 計算指定邏輯行的視覺行分割(給其他模組用,不依賴 cache 也可以)
700    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        // 如果 row 剛好在快取範圍內,優先使用快取
706        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    /// 將邏輯列轉換為視覺列(考慮 Tab 展開和字符寬度)
727    pub fn logical_col_to_visual_col(&self, line: &str, logical_col: usize) -> usize {
728        // 這個函式目前只拿到一行字串,不知道 row,無法用 cache。
729        // 保留原來的行為:直接掃一遍。
730        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    /// 從視覺行索引和視覺列轉換為邏輯列
745    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        // 優先使用快取(如果該行目前在視窗 cache 內)
753        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            // 計算前面視覺行的總視覺寬度
760            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            // 加上當前視覺行內的列位置
766            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            // 在 logical_to_visual 中尋找「視覺座標 >= visual_col_total」的最小 logical_col
771            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        // 若不在 cache 範圍,退回原本的計算方式(慢但安全)
782        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        // 計算前面視覺行的總視覺寬度
789        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    /// 實際可用於顯示文本的螢幕行數(扣除 debug 標尺)
827    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    /// 獲取cursor的視覺位置(螢幕座標)
836    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        // 計算cursor所在的螢幕行
844        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        // 添加cursor行內的視覺行偏移
876        screen_y += cursor.visual_line_index;
877
878        // 如果超出螢幕,返回最後一行
879        let screen_y = screen_y.min(self.screen_rows.saturating_sub(1));
880
881        // 計算cursor在視覺行內的x位置
882        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            // 計算前面視覺行的累計寬度
887            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            // cursor在整個邏輯行中的視覺col
893            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            // 在當前視覺行內的col
901            let visual_col_in_line = cursor_visual_col.saturating_sub(accumulated_width);
902
903            // 單行模式:減去水平偏移
904            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            // 加上行號寬度
911            screen_x += adjusted_col;
912        }
913
914        (screen_x, screen_y)
915    }
916
917    /// 渲染列標尺(顯示列位置個位數字)
918    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
942/// 將行按可用寬度切分成多個視覺行(共用)
943fn 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}