Skip to main content

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    /// 從 Terminal 建立 View(自動取得終端尺寸)
116    pub fn new(terminal: &Terminal) -> Self {
117        let (cols, rows) = terminal.size();
118        let screen_rows = rows.saturating_sub(1) as usize; // 減去狀態欄
119        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    /// 使用指定尺寸建立 View(不需要 Terminal)
133    ///
134    /// # Arguments
135    /// * `rows` - 螢幕行數(不含狀態欄)
136    /// * `cols` - 螢幕列數
137    ///
138    /// # Example
139    /// ```
140    /// use wedi_core::View;
141    /// let view = View::new_simple(24, 80);
142    /// ```
143    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    /// 調整視圖尺寸
158    ///
159    /// # Arguments
160    /// * `rows` - 新的螢幕行數
161    /// * `cols` - 新的螢幕列數
162    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    /// 完全清空緩存(用於大範圍變更或視窗調整)
169    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    /// 部分失效:僅清除指定邏輯行的緩存(用於單行編輯)
176    pub fn invalidate_line(&mut self, logical_row: usize) {
177        if logical_row < self.offset_row {
178            return; // 不在可見範圍內,無需清除
179        }
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    /// 部分失效:清除指定範圍的緩存(用於多行編輯)
188    #[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(); // 寬度或高度改變時使快取失效
205        }
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        // 計算選擇範圍(轉換為視覺列)
238        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            // 將start_col轉換為視覺列
243            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            // 將end_col轉換為視覺列
255            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                // 空行或超出範圍
300                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                // 渲染視覺行,支持 selection 高亮和語法高亮
327
328                // 檢查是否有語法高亮(無選擇時)
329                // 計算這個 visual_line 在邏輯行中的視覺起始位置
330                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                        // 這一行有選擇,需要逐字符渲染
349                        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                            // 單行模式:跳過 offset_col 之前的字符
356                            if !self.wrap_mode && current_visual_pos + ch_width <= self.offset_col {
357                                current_visual_pos += ch_width;
358                                continue;
359                            }
360
361                            // 單行模式:超出可見範圍則停止
362                            if !self.wrap_mode
363                                && current_visual_pos >= self.offset_col + available_width
364                            {
365                                break;
366                            }
367
368                            // 判斷這個字符是否在選擇範圍內
369                            let is_selected = if file_row == start_row && file_row == end_row {
370                                // 選擇在同一行
371                                current_visual_pos >= start_col && current_visual_pos < end_col
372                            } else if file_row == start_row {
373                                // 選擇起始行
374                                current_visual_pos >= start_col
375                            } else if file_row == end_row {
376                                // 選擇結束行
377                                current_visual_pos < end_col
378                            } else {
379                                // 選擇中間的行,全選
380                                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                        // 這一行沒有選擇,直接打印(單行模式需要截取)
395                        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                    // 沒有選擇
404                    if use_syntax_highlight {
405                        // 使用語法高亮
406                        #[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                                // 多行模式:截取當前視覺行對應的部分
411                                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                                // 單行模式:使用 ANSI 切割函數截取可見部分
419                                let sliced =
420                                    slice_ansi_text(highlighted, self.offset_col, available_width);
421                                queue!(stdout, style::Print(sliced))?;
422                            }
423                        } else {
424                            // 降級為純文字
425                            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                        // 純文字渲染
452                        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        // 畫底部的 ~ 行
472        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        // 移動終端光標到當前cursor位置
487        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        // 水平滾動(單行模式)
504        self.scroll_horizontal_if_needed(cursor, buffer);
505
506        // 向上滾動
507        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        // 大幅跳轉優化:如果跳轉距離超過 3 個螢幕高度,直接設置 offset_row
516        // 這避免了計算中間所有行的視覺高度,大幅提升大文件跳轉性能
517        let jump_threshold = effective_rows * 3;
518        let distance = cursor.row.saturating_sub(self.offset_row);
519
520        if distance > jump_threshold {
521            // 將 offset_row 設置為讓光標位於螢幕中間偏上的位置
522            // 這樣用戶可以看到光標上下文,體驗更好
523            self.offset_row = cursor.row.saturating_sub(effective_rows / 3);
524            self.invalidate_cache();
525            return;
526        }
527
528        // 計算目前 offset_row ~ cursor.row 的視覺高度
529        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        // 如果沒超出螢幕,就不用動
547        if visual_offset < effective_rows {
548            return;
549        }
550
551        // 向下推 offset_row,每次扣掉最上面那一行的視覺高度
552        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    /// 水平滾動(單行模式專用)
580    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        // 計算游標的視覺列
589        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        // 游標超出右邊界
597        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        // 游標超出左邊界
603        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        // 確保狀態欄填滿整行(使用視覺寬度)
648        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; // 連動切換換行模式
673        self.offset_col = 0; // 重置水平偏移
674        self.invalidate_cache();
675    }
676
677    /// 切換顯示模式(單行/多行),不影響行號顯示
678    pub fn toggle_display_mode(&mut self) {
679        self.wrap_mode = !self.wrap_mode;
680        self.offset_col = 0; // 重置水平偏移
681        self.invalidate_cache();
682    }
683
684    /// 獲取當前顯示模式名稱
685    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    /// 截取可見文字(處理中文寬度,用於單行模式)
694    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            // 跳過 offset 之前的字符
702            if current_col + ch_width <= start_col {
703                current_col += ch_width;
704                continue;
705            }
706
707            // 超出可見範圍則停止
708            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    /// 計算行號寬度(包含右側空格)
720    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    /// 獲取可用於顯示內容的寬度(扣除行號寬度)
729    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    /// 計算指定邏輯行的視覺行分割(給其他模組用,不依賴 cache 也可以)
737    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        // 如果 row 剛好在快取範圍內,優先使用快取
743        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    /// 將邏輯列轉換為視覺列(考慮 Tab 展開和字符寬度)
764    pub fn logical_col_to_visual_col(&self, line: &str, logical_col: usize) -> usize {
765        // 這個函式目前只拿到一行字串,不知道 row,無法用 cache。
766        // 保留原來的行為:直接掃一遍。
767        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    /// 從視覺行索引和視覺列轉換為邏輯列
782    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        // 優先使用快取(如果該行目前在視窗 cache 內)
790        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            // 計算前面視覺行的總視覺寬度
797            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            // 加上當前視覺行內的列位置
803            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            // 在 logical_to_visual 中尋找「視覺座標 >= visual_col_total」的最小 logical_col
808            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        // 若不在 cache 範圍,退回原本的計算方式(慢但安全)
819        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        // 計算前面視覺行的總視覺寬度
826        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    /// 實際可用於顯示文本的螢幕行數(扣除 debug 標尺)
864    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    /// 獲取cursor的視覺位置(螢幕座標)
873    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        // 計算cursor所在的螢幕行
881        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        // 添加cursor行內的視覺行偏移
913        screen_y += cursor.visual_line_index;
914
915        // 如果超出螢幕,返回最後一行
916        let screen_y = screen_y.min(self.screen_rows.saturating_sub(1));
917
918        // 計算cursor在視覺行內的x位置
919        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            // 計算前面視覺行的累計寬度
924            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            // cursor在整個邏輯行中的視覺col
930            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            // 在當前視覺行內的col
938            let visual_col_in_line = cursor_visual_col.saturating_sub(accumulated_width);
939
940            // 單行模式:減去水平偏移
941            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            // 加上行號寬度
948            screen_x += adjusted_col;
949        }
950
951        (screen_x, screen_y)
952    }
953
954    /// 渲染列標尺(顯示列位置個位數字)
955    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
979/// 將行按可用寬度切分成多個視覺行(共用)
980fn 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}