From 7bc3fc35265d47cc1725d05d597b18d435991aef Mon Sep 17 00:00:00 2001 From: Kan-Ru Chen Date: Sat, 7 Feb 2026 18:44:16 +0900 Subject: [PATCH] feat(editor): introduce chewing.auto_snapshot_selections option --- NEWS | 3 +++ capi/src/io.rs | 6 +++++ src/conversion/mod.rs | 9 ++++++++ src/editor/mod.rs | 19 +++++++++++---- src/editor/selection/phrase.rs | 35 +--------------------------- tests/genkeystroke.c | 2 ++ tests/test-bopomofo.c | 42 +++++++++++++++++++++++++++++++--- tests/testchewing.c | 2 +- 8 files changed, 75 insertions(+), 43 deletions(-) diff --git a/NEWS b/NEWS index c806e27ee..ebc6cae90 100644 --- a/NEWS +++ b/NEWS @@ -7,6 +7,9 @@ What's New in libchewing (unreleased) - dict: deleted phrases now can be recorded in a separate chewing-deleted.dat exclusion dictionary. This allows excluding phrases from even built-in dictionaries. Deleted phrases will not be auto learned again. + - editor: new config option "chewing.auto_snapshot_selections" (default false) + can be used to control whether phrase selections are automatically locked + after any cursor movement. * Bug Fixes - dict: fixed parsing trie dictionary file with extension fields. diff --git a/capi/src/io.rs b/capi/src/io.rs index 6ea33a34c..38e3888bd 100644 --- a/capi/src/io.rs +++ b/capi/src/io.rs @@ -378,6 +378,7 @@ pub unsafe extern "C" fn chewing_config_has_option( | "chewing.conversion_engine" | "chewing.enable_fullwidth_toggle_key" | "chewing.sort_candidates_by_frequency" + | "chewing.auto_snapshot_selections" ); ret as c_int @@ -431,6 +432,7 @@ pub unsafe extern "C" fn chewing_config_get_int( }, "chewing.enable_fullwidth_toggle_key" => option.enable_fullwidth_toggle_key as c_int, "chewing.sort_candidates_by_frequency" => option.sort_candidates_by_frequency as c_int, + "chewing.auto_snapshot_selections" => option.auto_snapshot_selections as c_int, _ => ERROR, } } @@ -556,6 +558,10 @@ pub unsafe extern "C" fn chewing_config_set_int( ensure_bool!(value); options.sort_candidates_by_frequency = value > 0; } + "chewing.auto_snapshot_selections" => { + ensure_bool!(value); + options.auto_snapshot_selections = value > 0; + } _ => return ERROR, }; diff --git a/src/conversion/mod.rs b/src/conversion/mod.rs index b6937deb2..684c9f85d 100644 --- a/src/conversion/mod.rs +++ b/src/conversion/mod.rs @@ -81,6 +81,15 @@ impl Interval { pub fn is_empty(&self) -> bool { self.len() == 0 } + /// Return the texts in the interval + pub fn sub_intervals(&self) -> impl Iterator { + self.text.chars().enumerate().map(|(offset, ch)| Interval { + start: self.start + offset, + end: self.start + offset + 1, + is_phrase: self.is_phrase, + text: ch.to_string().into_boxed_str(), + }) + } } /// Represents the gap between symbols. diff --git a/src/editor/mod.rs b/src/editor/mod.rs index 8086d92a5..ba38b65c0 100644 --- a/src/editor/mod.rs +++ b/src/editor/mod.rs @@ -78,6 +78,7 @@ pub struct EditorOptions { pub conversion_engine: ConversionEngineKind, pub enable_fullwidth_toggle_key: bool, pub sort_candidates_by_frequency: bool, + pub auto_snapshot_selections: bool, } impl Default for EditorOptions { @@ -99,6 +100,7 @@ impl Default for EditorOptions { conversion_engine: ConversionEngineKind::ChewingEngine, enable_fullwidth_toggle_key: true, sort_candidates_by_frequency: false, + auto_snapshot_selections: false, } } } @@ -600,14 +602,21 @@ impl SharedState { } paths[self.nth_conversion % paths.len()].intervals.clone() } - fn intervals(&self) -> impl Iterator { + fn intervals(&self) -> impl Iterator + use<> { self.conversion().into_iter() } fn snapshot(&mut self) { - // for interval in self.intervals() { - // self.com.select(interval); - // } - // self.nth_conversion = 0; + if !self.options.auto_snapshot_selections { + return; + } + for interval in self.intervals() { + if interval.is_phrase { + for i in interval.sub_intervals() { + self.com.select(i); + } + } + } + self.nth_conversion = 0; } fn cursor(&self) -> usize { self.com.cursor() diff --git a/src/editor/selection/phrase.rs b/src/editor/selection/phrase.rs index 96d56e47f..20d5cffd2 100644 --- a/src/editor/selection/phrase.rs +++ b/src/editor/selection/phrase.rs @@ -205,14 +205,10 @@ impl PhraseSelector { } fn after_previous_break_point(&self, mut cursor: usize) -> usize { - let selection_ends: Vec<_> = self.com.selections().iter().map(|sel| sel.end).collect(); loop { if cursor == 0 { return 0; } - if selection_ends.contains(&cursor) { - break; - } if let Some(Gap::Break) = self.com.gap(cursor) { break; } @@ -263,7 +259,7 @@ impl PhraseSelector { mod tests { use super::PhraseSelector; use crate::{ - conversion::{Composition, Interval, Symbol}, + conversion::{Composition, Symbol}, dictionary::{LookupStrategy, TrieBuf}, syl, zhuyin::Bopomofo::*, @@ -383,33 +379,4 @@ mod tests { assert_eq!(1, sel.after_previous_break_point(1)); assert_eq!(1, sel.after_previous_break_point(2)); } - - #[test] - fn should_stop_after_first_selection() { - let mut com = Composition::new(); - for sym in [ - Symbol::from(syl![C, E, TONE4]), - Symbol::from(syl![C, E, TONE4]), - ] { - com.push(sym); - } - com.push_selection(Interval { - start: 0, - end: 1, - is_phrase: true, - text: "冊".into(), - }); - let sel = PhraseSelector { - begin: 0, - end: 2, - forward_select: false, - orig: 0, - lookup_strategy: LookupStrategy::Standard, - com, - }; - - assert_eq!(0, sel.after_previous_break_point(0)); - assert_eq!(1, sel.after_previous_break_point(1)); - assert_eq!(1, sel.after_previous_break_point(2)); - } } diff --git a/tests/genkeystroke.c b/tests/genkeystroke.c index 4f9a36358..22f87280c 100644 --- a/tests/genkeystroke.c +++ b/tests/genkeystroke.c @@ -343,6 +343,8 @@ int main(int argc, char *argv[]) chewing_set_selKey(ctx, selKey_define, 10); chewing_set_spaceAsSelection(ctx, 1); chewing_set_phraseChoiceRearward(ctx, 1); + chewing_set_autoShiftCur(ctx, 1); + chewing_config_set_int(ctx, "chewing.auto_snapshot_selections", 0); clear(); diff --git a/tests/test-bopomofo.c b/tests/test-bopomofo.c index 02f7ea03c..526316c8b 100644 --- a/tests/test-bopomofo.c +++ b/tests/test-bopomofo.c @@ -373,7 +373,7 @@ void test_select_candidate_4_bytes_utf8() type_keystroke_by_string(ctx, "8"); ok_preedit_buffer(ctx, "\xF0\xA2\x94\xA8\xE5\xBE\x97" /* 𢔨得 */ ); - type_keystroke_by_string(ctx, "8"); + type_keystroke_by_string(ctx, "8"); ok_preedit_buffer(ctx, "\xF0\xA2\x94\xA8\xF0\xA2\x94\xA8" /* 𢔨𢔨 */ ); @@ -532,7 +532,7 @@ void test_select_candidate_shift_cursor_rearword() ok_preedit_buffer(ctx, "七上八下納裡"); - type_keystroke_by_string(ctx, "2"); + type_keystroke_by_string(ctx, "2"); ok_preedit_buffer(ctx, "七上八下納里"); @@ -569,6 +569,40 @@ void test_select_candidate_sorted() chewing_delete(ctx); } +void test_select_without_auto_snapshot() +{ + ChewingContext *ctx; + + clean_userphrase(); + + ctx = chewing_new(); + start_testcase(ctx); + chewing_set_phraseChoiceRearward(ctx, 1); + chewing_config_set_int(ctx, "chewing.auto_snapshot_selections", 0); + + type_keystroke_by_string(ctx, "hk4g45"); + ok_preedit_buffer(ctx, "策士"); + + chewing_delete(ctx); +} + +void test_select_with_auto_snapshot() +{ + ChewingContext *ctx; + + clean_userphrase(); + + ctx = chewing_new(); + start_testcase(ctx); + chewing_set_phraseChoiceRearward(ctx, 1); + chewing_config_set_int(ctx, "chewing.auto_snapshot_selections", 1); + + type_keystroke_by_string(ctx, "hk4g45"); + ok_preedit_buffer(ctx, "測士"); + + chewing_delete(ctx); +} + void test_select_candidate() { test_select_candidate_no_rearward(); @@ -586,6 +620,8 @@ void test_select_candidate() test_select_candidate_shift_cursor(); test_select_candidate_shift_cursor_rearword(); test_select_candidate_sorted(); + test_select_without_auto_snapshot(); + test_select_with_auto_snapshot(); } void test_Esc_not_entering_chewing() @@ -1921,7 +1957,7 @@ void test_KB_HSU_example() ok_preedit_buffer(ctx, "一隻隻可愛的小花貓"); chewing_clean_preedit_buf(ctx); - type_keystroke_by_string(ctx, "sm sxajdwj1xfsxajdgscewfhidxfdwj1cd1rnd"); + type_keystroke_by_string(ctx, "sm sxajdwj1xfsxajdgscewfhidxfdwj1cd1rnd"); ok_preedit_buffer(ctx, "三歲到五歲的小孩五到十人"); chewing_clean_preedit_buf(ctx); diff --git a/tests/testchewing.c b/tests/testchewing.c index 7cc8f56b6..66947286e 100644 --- a/tests/testchewing.c +++ b/tests/testchewing.c @@ -13,7 +13,6 @@ #include #include -#include static int selKey_define[11] = { '1', '2', '3', '4', '5', '6', '7', '8', '9', '0', 0 }; /* Default */ @@ -59,6 +58,7 @@ int main(int argc, char *argv[]) chewing_set_addPhraseDirection(ctx, 1); chewing_set_selKey(ctx, selKey_define, 10); chewing_set_spaceAsSelection(ctx, 1); + chewing_config_set_int(ctx, "chewing.auto_snapshot_selections", 0); while (1) { i = get_keystroke(get_char_from_fp, fp);