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`,均有单元测试。
|
||||
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 {
|
||||
break;
|
||||
}
|
||||
// This segment overlaps [from, to): emit the columns inside range.
|
||||
let chars: Vec<char> = seg.text.chars().collect();
|
||||
for (i, ch) in chars.iter().enumerate() {
|
||||
// Distribute chars across the segment's columns 1:1 (clamped).
|
||||
let ch_col = col + i.min(seg.cols.saturating_sub(1));
|
||||
if ch_col >= from && ch_col < to {
|
||||
out.push(*ch);
|
||||
// Walk grapheme clusters and track actual grid-column width so
|
||||
// that wide chars (2 cols) and combining marks (0 cols) are
|
||||
// sliced correctly.
|
||||
use unicode_segmentation::UnicodeSegmentation;
|
||||
for grapheme in UnicodeSegmentation::graphemes(seg.text.as_str(), true) {
|
||||
let w = grapheme_width(grapheme, false);
|
||||
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;
|
||||
}
|
||||
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 {
|
||||
let start = byte_index_for_char_col(text, start_col);
|
||||
let end = byte_index_for_char_col(text, end_col);
|
||||
text.get(start..end).unwrap_or_default().to_owned()
|
||||
use unicode_segmentation::UnicodeSegmentation;
|
||||
let mut out = String::new();
|
||||
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 {
|
||||
|
||||
Reference in New Issue
Block a user