fix(terminal): use grapheme_width for accurate column slicing
text_in_cols and text_slice_cols now walk grapheme clusters with grapheme_width() instead of assuming 1 char = 1 column. This fixes column slicing for: - Wide characters (CJK, emoji) that occupy 2 grid columns - Combining marks that occupy 0 columns The prior 1:1 char∶col mapping caused selection-copy and serialize output to misalign column ranges when wide/combining characters were present in a row. - Update roadmap: mark both P0 follow-up items as resolved
This commit is contained in:
@@ -62,7 +62,7 @@
|
|||||||
- 实现于 `component.rs`(`Selection`/`selection_rects`/`point_to_cell`/鼠标处理)+ `core.rs::selection_text` + `core.rs::TerminalRow::text_in_cols`,均有单元测试。
|
- 实现于 `component.rs`(`Selection`/`selection_rects`/`point_to_cell`/鼠标处理)+ `core.rs::selection_text` + `core.rs::TerminalRow::text_in_cols`,均有单元测试。
|
||||||
3. **快捷键**:Ctrl-Shift-C 复制 / Ctrl-V 粘贴。
|
3. **快捷键**:Ctrl-Shift-C 复制 / Ctrl-V 粘贴。
|
||||||
|
|
||||||
**已知后续项(非阻塞)**:拖拽到视窗边缘时**没有自动滚动延伸选区**(需先滚到位再选);行内 wide+组合字符混排的列切片为近似。
|
**已知后续项(非阻塞)**:拖拽到视窗边缘时**没有自动滚动延伸选区**(✅ 已通过上一轮 auto-scroll 实现);行内 wide+组合字符混排的列切片(✅ 已通过 grapheme_width 修正 text_in_cols / text_slice_cols)。
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -821,15 +821,22 @@ impl TerminalRow {
|
|||||||
if col >= to {
|
if col >= to {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
// This segment overlaps [from, to): emit the columns inside range.
|
// Walk grapheme clusters and track actual grid-column width so
|
||||||
let chars: Vec<char> = seg.text.chars().collect();
|
// that wide chars (2 cols) and combining marks (0 cols) are
|
||||||
for (i, ch) in chars.iter().enumerate() {
|
// sliced correctly.
|
||||||
// Distribute chars across the segment's columns 1:1 (clamped).
|
use unicode_segmentation::UnicodeSegmentation;
|
||||||
let ch_col = col + i.min(seg.cols.saturating_sub(1));
|
for grapheme in UnicodeSegmentation::graphemes(seg.text.as_str(), true) {
|
||||||
if ch_col >= from && ch_col < to {
|
let w = grapheme_width(grapheme, false);
|
||||||
out.push(*ch);
|
let ch_end = col + w;
|
||||||
|
// If this grapheme overlaps [from, to), emit it.
|
||||||
|
if ch_end > from && col < to {
|
||||||
|
out.push_str(grapheme);
|
||||||
}
|
}
|
||||||
|
col = ch_end;
|
||||||
}
|
}
|
||||||
|
// If our tracked `col` differs from `seg_end` (e.g. because
|
||||||
|
// `grapheme_width` disagrees with the vt100 grid), trust the
|
||||||
|
// grid column count for the next segment's baseline.
|
||||||
col = seg_end;
|
col = seg_end;
|
||||||
}
|
}
|
||||||
out.trim_end().to_owned()
|
out.trim_end().to_owned()
|
||||||
@@ -947,9 +954,21 @@ fn rfind_literal_before_col(text: &str, query: &str, before_col: usize) -> Optio
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn text_slice_cols(text: &str, start_col: usize, end_col: usize) -> String {
|
fn text_slice_cols(text: &str, start_col: usize, end_col: usize) -> String {
|
||||||
let start = byte_index_for_char_col(text, start_col);
|
use unicode_segmentation::UnicodeSegmentation;
|
||||||
let end = byte_index_for_char_col(text, end_col);
|
let mut out = String::new();
|
||||||
text.get(start..end).unwrap_or_default().to_owned()
|
let mut col = 0usize;
|
||||||
|
for grapheme in UnicodeSegmentation::graphemes(text, true) {
|
||||||
|
let w = grapheme_width(grapheme, false);
|
||||||
|
let ch_end = col + w;
|
||||||
|
if ch_end > start_col && col < end_col {
|
||||||
|
out.push_str(grapheme);
|
||||||
|
}
|
||||||
|
col = ch_end;
|
||||||
|
if col >= end_col {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
out
|
||||||
}
|
}
|
||||||
|
|
||||||
fn byte_index_for_char_col(text: &str, col: usize) -> usize {
|
fn byte_index_for_char_col(text: &str, col: usize) -> usize {
|
||||||
|
|||||||
Reference in New Issue
Block a user