docs: 逐模块细化中文逻辑注释并覆盖测试与示例
This commit is contained in:
@@ -12,13 +12,15 @@ use unicode_segmentation::UnicodeSegmentation;
|
||||
/// 行文本、锚点与选择端点,既供鼠标拖选也供键盘导航使用。
|
||||
pub(super) struct SourceSelection {
|
||||
lines: Vec<String>,
|
||||
// 端点是 (行号, UTF-8 字节偏移),字节位置始终由完整字素边界产生,不用字符数直接切片。
|
||||
anchor: (usize, usize),
|
||||
end: (usize, usize),
|
||||
pub dragging: bool,
|
||||
}
|
||||
impl SourceSelection {
|
||||
/// 替换可选择文本行,并把旧选择限制在新文本仍有效的位置。
|
||||
/// 同步展示文本;内容变化时重置选择和拖动,内容完全相同时保留原选区。
|
||||
pub fn set_lines(&mut self, lines: &[String]) {
|
||||
// 文本变化时重置两端与拖选状态;当前实现不是把旧选区钳到新文本,避免对应内容已经改变。
|
||||
if self.lines != lines {
|
||||
self.lines = lines.to_vec();
|
||||
self.anchor = (0, 0);
|
||||
@@ -28,6 +30,7 @@ impl SourceSelection {
|
||||
}
|
||||
/// 把工具指针位置换算成合法行号与文本字符边界。
|
||||
fn locate(p: Point, origin: Point, lines: &[String]) -> (usize, usize) {
|
||||
// 按固定行高求行号,向上下界钳制;空文本通过 saturating_sub 与后续 get 安全返回原点。
|
||||
let row = (((p.y - origin.y) / super::ROW).floor().max(0.0) as usize)
|
||||
.min(lines.len().saturating_sub(1));
|
||||
let Some(line) = lines.get(row) else {
|
||||
@@ -35,6 +38,7 @@ impl SourceSelection {
|
||||
};
|
||||
let x = (p.x - origin.x).max(0.0);
|
||||
let mut byte = line.len();
|
||||
// 逐个字素测量左右边界,以中点决定插入位置,点击超过行尾则保留 byte=line.len()。
|
||||
for (i, g) in line.grapheme_indices(true) {
|
||||
let left = text::measure(&line[..i], TextStyle::code()).width;
|
||||
let right = text::measure(&line[..i + g.len()], TextStyle::code()).width;
|
||||
@@ -47,6 +51,7 @@ impl SourceSelection {
|
||||
}
|
||||
/// 在指针位置建立文本选择锚点,开始一次拖动选择。
|
||||
pub fn begin(&mut self, p: Point, origin: Point, lines: &[String]) {
|
||||
// 调用方提供的 lines 与当前展示一致;开始拖动先折叠旧选择,后续 extend 只移动活动端。
|
||||
self.anchor = Self::locate(p, origin, lines);
|
||||
self.end = self.anchor;
|
||||
self.dragging = true;
|
||||
@@ -66,6 +71,7 @@ impl SourceSelection {
|
||||
let (mut row, mut byte) = self.end;
|
||||
let line = self.lines.get(row)?;
|
||||
match key {
|
||||
// 水平移动按字素跨越,组合音标和 emoji 视作整体;到行边界后跨到相邻行的末尾/开头。
|
||||
ArrowLeft => {
|
||||
if byte > 0 {
|
||||
byte = line[..byte]
|
||||
@@ -85,6 +91,7 @@ impl SourceSelection {
|
||||
byte = 0;
|
||||
}
|
||||
}
|
||||
// 垂直移动保持的是字素列数,不是像素 x;目标行较短时取尽其字素,落到行尾。
|
||||
ArrowUp | ArrowDown => {
|
||||
let col = line[..byte].graphemes(true).count();
|
||||
row = if *key == ArrowUp {
|
||||
@@ -98,6 +105,7 @@ impl SourceSelection {
|
||||
.map(str::len)
|
||||
.sum();
|
||||
}
|
||||
// 普通 Home/End 选本行边界,command 修饰时转到整个文本首/末行。
|
||||
Home => {
|
||||
if command {
|
||||
row = 0;
|
||||
@@ -113,6 +121,7 @@ impl SourceSelection {
|
||||
_ => return None,
|
||||
}
|
||||
self.end = (row, byte);
|
||||
// 未按 Shift 时同步锚点以折叠选区;按 Shift 则仅移动 end,保持原锚点扩展选择。
|
||||
if !shift {
|
||||
self.anchor = self.end;
|
||||
}
|
||||
@@ -129,6 +138,7 @@ impl SourceSelection {
|
||||
),
|
||||
);
|
||||
}
|
||||
// 元组字典序先比行号再比字节位置,统一处理从后向前拖选与从前向后拖选。
|
||||
(self.anchor.min(self.end), self.anchor.max(self.end))
|
||||
}
|
||||
/// 拼接当前选择范围内的实际文本,保持多行换行与 UTF-8 边界。
|
||||
@@ -139,6 +149,7 @@ impl SourceSelection {
|
||||
.enumerate()
|
||||
.filter(|(i, _)| *i >= a.0 && *i <= b.0)
|
||||
.map(|(i, line)| {
|
||||
// 首尾行只取选中片段,中间行取完整文本;用安全 get 防止不一致范围导致切片 panic。
|
||||
let start = if i == a.0 { a.1 } else { 0 };
|
||||
let end = if i == b.0 { b.1 } else { line.len() };
|
||||
line.get(start..end).unwrap_or("").to_owned()
|
||||
@@ -149,6 +160,7 @@ impl SourceSelection {
|
||||
/// 取得某行的可见选择片段,供高亮绘制复用。
|
||||
pub fn span(&self, row: usize, all: bool, lines: &[String]) -> Option<(f32, f32)> {
|
||||
let (a, b) = self.range(all);
|
||||
// 未选中的行或折叠选择不画高亮;有选择时返回两个真实字体测量的横向位置,不是宽度值。
|
||||
if row < a.0 || row > b.0 || a == b {
|
||||
return None;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user