mirror of
https://github.com/qingjian-team/qingjian.git
synced 2026-11-06 08:13:51 +00:00
组句中 ⌥← / ⌥→ 按音节跳光标
光标在末尾发现前面某个音节错了:⌥← 跳到它后面、⌥⌫ 删掉重敲、⌘→ 回末尾,不用一个字母一个字母挪。 边界规则与 ⌥⌫ 相同(全拼按切分、双拼两键、直输段按字母段),往左 ` 跟着前面的音节,往右先越过 `。
This commit is contained in:
@@ -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());
|
||||
}
|
||||
|
||||
@@ -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 清掉。表达式 / 问字模式优先。中文模式下就能打带标点的英文句子;代价是没有了「拼音 + 标点 = 上屏候选 + 全角标点」,要先空格再打标点 |
|
||||
|
||||
@@ -32,6 +32,7 @@ description: 中英切换、选词、翻页、整句输入、选错了怎么退
|
||||
| `Esc` | 清空 |
|
||||
| `上` / `下` | 移动高亮,到页边自动翻页 |
|
||||
| `左` / `右` | 移动拼音光标;光标停在中间时候选只按光标前的拼音算 |
|
||||
| `⌥ + 左` / `⌥ + 右` | 按音节跳光标;`⌘ + 左` / `⌘ + 右` 到开头 / 末尾。前面某个音节敲错了:⌥← 跳到它后面,⌥⌫ 删掉重敲,⌘→ 回末尾 |
|
||||
| `[` `]` | 翻页(可改成 `,` `.`);`PageUp` / `PageDown`、`Shift + Tab` 也翻 |
|
||||
| `Tab` | 有云端整句补全时接受它,没有则翻页 |
|
||||
| 半角标点(`,` `.` `?` `-` 等,翻页键除外) | 进入英文直输段:`hello,` `no-way` 整段原样显示,之后的字符原样追加,空格 / 回车原样上屏(空格本身也打出)。中文模式下也能打 `hello, world?` 这样带标点的英文 |
|
||||
|
||||
Reference in New Issue
Block a user