组句中 ⌥← / ⌥→ 按音节跳光标

光标在末尾发现前面某个音节错了:⌥← 跳到它后面、⌥⌫ 删掉重敲、⌘→ 回末尾,不用一个字母一个字母挪。
边界规则与 ⌥⌫ 相同(全拼按切分、双拼两键、直输段按字母段),往左 ` 跟着前面的音节,往右先越过 `。
This commit is contained in:
yenharvey
2026-09-08 10:49:00 +08:00
parent e63e9cfbed
commit dc3b561e78
5 changed files with 101 additions and 0 deletions
+10
View File
@@ -235,6 +235,8 @@ impl QingjianInputController {
125 => Some(sel!(moveDown:)),
123 if command => Some(sel!(moveToLeftEndOfLine:)),
124 if command => Some(sel!(moveToRightEndOfLine:)),
123 if option => Some(sel!(moveWordLeft:)),
124 if option => Some(sel!(moveWordRight:)),
123 => Some(sel!(moveLeft:)),
124 => Some(sel!(moveRight:)),
116 => Some(sel!(pageUp:)),
@@ -607,6 +609,14 @@ impl QingjianInputController {
} else if selector == sel!(moveRight:) {
host::with(|h| h.engine.move_cursor_right());
self.refresh(client);
} else if selector == sel!(moveWordLeft:) {
// ⌥←:光标往左跳一个音节
host::with(|h| h.engine.move_cursor_syllable_left());
self.refresh(client);
} else if selector == sel!(moveWordRight:) {
// ⌥→:光标往右跳一个音节
host::with(|h| h.engine.move_cursor_syllable_right());
self.refresh(client);
} else if selector == sel!(moveToBeginningOfLine:) || selector == sel!(moveToLeftEndOfLine:)
{
host::with(|h| h.engine.move_cursor_home());
@@ -95,6 +95,24 @@ impl Engine {
self.composition.delete_before_cursor(len)
}
/// 光标往左跳一个音节(壳里 ⌥←),边界与 [`Self::delete_syllable_backward`] 相同。已在开头返回 `false`。
pub fn move_cursor_syllable_left(&mut self) -> bool {
let cursor = self.composition.cursor();
let before = &self.composition.text()[..cursor];
let plain = self.raw_mode() || self.expression_mode() || self.question_mode();
let len = unit_len_before(before, self.shuangpin.is_some(), plain);
len > 0 && (0..len).all(|_| self.composition.move_left())
}
/// 光标往右跳一个音节(壳里 ⌥→):跳过紧跟的 `'`,再跳过一个音节。已在末尾返回 `false`。
pub fn move_cursor_syllable_right(&mut self) -> bool {
let cursor = self.composition.cursor();
let after = &self.composition.text()[cursor..];
let plain = self.raw_mode() || self.expression_mode() || self.question_mode();
let len = unit_len_after(after, self.shuangpin.is_some(), plain);
len > 0 && (0..len).all(|_| self.composition.move_right())
}
/// 删掉光标前的全部拼音(壳里 ⌘⌫),光标后的留着。光标在开头时返回 `false`。
pub fn delete_to_start(&mut self) -> bool {
let cursor = self.composition.cursor();
@@ -182,6 +200,42 @@ impl Engine {
}
}
/// 光标后的第一个「单位」占几个字节:先跳过紧跟的 `'`,再算一个音节;规则同 [`unit_len_before`]。
fn unit_len_after(after: &str, shuangpin: bool, plain: bool) -> usize {
let trimmed = after.trim_start_matches('\'');
let separators = after.len() - trimmed.len();
let Some(first) = trimmed.chars().next() else {
return separators;
};
if plain || !first.is_ascii_lowercase() {
let run = if first.is_ascii_alphanumeric() {
trimmed
.chars()
.take_while(char::is_ascii_alphanumeric)
.map(char::len_utf8)
.sum()
} else {
first.len_utf8()
};
return separators + run;
}
if shuangpin {
let run = trimmed
.chars()
.take_while(|c| c.is_ascii_lowercase() || *c == ';')
.count();
return separators + run.min(2);
}
let syllable = match segment_longest_prefix(trimmed) {
Ok((segmentations, _)) => segmentations
.first()
.and_then(|s| s.syllables.first())
.map_or(1, |s| s.text.len()),
Err(_) => 1,
};
separators + syllable
}
/// 光标前的最后一个「单位」占几个字节:拼音里是一个音节(连同它后面的 `'`),见 [`Engine::delete_syllable_backward`]。
fn unit_len_before(before: &str, shuangpin: bool, plain: bool) -> usize {
let trimmed = before.trim_end_matches('\'');
@@ -335,3 +335,38 @@ fn option_backspace_deletes_a_syllable_and_command_backspace_deletes_to_the_star
assert!(engine.delete_syllable_backward());
assert_eq!(engine.composition().text(), "kd");
}
#[test]
fn option_arrows_move_the_cursor_by_syllable() {
let mut engine = engine();
// 光标在末尾发现前面错了:⌥← 两下跳到第二个音节后面,⌥⌫ 删掉它,重敲,⌘→ 回末尾
engine.set_input("kaifa'xian'xia");
assert!(engine.move_cursor_syllable_left());
assert_eq!(engine.composition().cursor(), "kaifa'xian'".len());
assert!(engine.move_cursor_syllable_left());
assert_eq!(engine.composition().cursor(), "kaifa'".len());
assert!(engine.move_cursor_syllable_left());
assert_eq!(engine.composition().cursor(), "kai".len());
assert!(engine.delete_syllable_backward());
assert_eq!(engine.composition().text(), "fa'xian'xia");
engine.push('x');
engine.push('i');
assert_eq!(engine.composition().text(), "xifa'xian'xia");
// ⌥→:从 xi| 起跳过一个音节到 xifa|,再跳先越过 `'` 再过一个音节
assert!(engine.move_cursor_syllable_right());
assert_eq!(engine.composition().cursor(), "xifa".len());
assert!(engine.move_cursor_syllable_right());
assert_eq!(engine.composition().cursor(), "xifa'xian".len());
assert!(engine.move_cursor_syllable_right());
assert_eq!(engine.composition().cursor(), "xifa'xian'xia".len());
assert!(!engine.move_cursor_syllable_right());
engine.move_cursor_home();
assert!(!engine.move_cursor_syllable_left());
// 直输段按字母段跳
engine.set_input("hello,world");
assert!(engine.move_cursor_syllable_left());
assert_eq!(engine.composition().cursor(), "hello,".len());
engine.move_cursor_home();
assert!(engine.move_cursor_syllable_right());
assert_eq!(engine.composition().cursor(), "hello".len());
}
+1
View File
@@ -90,6 +90,7 @@ collection behavior 是 CanJoinAllSpaces + FullScreenAuxiliary + Stationary。
| ⌥⌫ / ⌘⌫ | 删光标前一个音节(全拼按最优切分的最后一个音节连同 `'`,切不动的尾巴整个删;双拼两键一音节;直输段按字母段)/ 删光标前全部拼音(`Engine::delete_syllable_backward` / `delete_to_start`)。不在组句时交给应用,撤销的账不再记(`break_chain`) |
| Esc | 清空 |
| 上 / 下 | 移动高亮,到页边自动翻页;第一项再按上不动 |
| ⌥← / ⌥→ | 按音节跳光标(边界规则同 ⌥⌫;`'` 跟着前面的音节,`Engine::move_cursor_syllable_left` / `right`)。光标在末尾发现前面的音节错了:⌥← 跳过去、⌥⌫ 删掉重敲、⌘→ 回末尾 |
| 左 / 右、Cmd+左 / 右 | 移动拼音光标(不切模式)。光标停在中间时候选只按光标前的拼音算(`ni|hao` 出 你),上屏后剩余拼音留着、光标落到末尾;光标在开头或末尾按整段算 |
| PageUp / PageDown、Shift+Tab、翻页键对(配置 `[general] page_keys`,缺省 `[` `]`,可选 `,` `.`) | 翻页。每页几个由 `page_size` 定(1–9) |
| 半角标点(`is_ascii_punctuation`,翻页键除外;⇧+数字已被删候选 / 译词键截走) | 组句中敲它进入**英文直输段**:`no-way` `hello,` `dui'ma?` 整段原样显示,之后的可见字符(含数字、大写)都追加,回车原样上屏,空格原样上屏后空格本身也交给应用(`hello, world` 的空格要在),Esc 清掉。表达式 / 问字模式优先。中文模式下就能打带标点的英文句子;代价是没有了「拼音 + 标点 = 上屏候选 + 全角标点」,要先空格再打标点 |
+1
View File
@@ -32,6 +32,7 @@ description: 中英切换、选词、翻页、整句输入、选错了怎么退
| `Esc` | 清空 |
| `上` / `下` | 移动高亮,到页边自动翻页 |
| `左` / `右` | 移动拼音光标;光标停在中间时候选只按光标前的拼音算 |
| `⌥ + 左` / `⌥ + 右` | 按音节跳光标;`⌘ + 左` / `⌘ + 右` 到开头 / 末尾。前面某个音节敲错了:⌥← 跳到它后面,⌥⌫ 删掉重敲,⌘→ 回末尾 |
| `[` `]` | 翻页(可改成 `,` `.`);`PageUp` / `PageDown`、`Shift + Tab` 也翻 |
| `Tab` | 有云端整句补全时接受它,没有则翻页 |
| 半角标点(`,` `.` `?` `-` 等,翻页键除外) | 进入英文直输段:`hello,` `no-way` 整段原样显示,之后的字符原样追加,空格 / 回车原样上屏(空格本身也打出)。中文模式下也能打 `hello, world?` 这样带标点的英文 |