Skip to content

Commit f19e10b

Browse files
committed
feat: support sorting candidates by frequency
1 parent af5c48e commit f19e10b

File tree

5 files changed

+147
-7
lines changed

5 files changed

+147
-7
lines changed

NEWS

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,17 @@ What's New in libchewing (unreleased)
44
* Features
55
- cli: Now outputs and reads dictionary metadata in source file
66
- cli: Detects mismatched phrase word count and syllable count
7+
- editor: Support optional sorting candidates by frequency. Previously the
8+
candidates are always sorted by their order in the dictionary.
79

810
* Developer Features
911
- New API `chewing_handle_KeyboardEvent()` can be used to support any
1012
keyboard layout combination without updating libchewing. Other keyboard
1113
handling API will be deprecated in future release.
1214
- New API `chewing_new3()` and `chewing_get_defaultDictonaryNames()` can be
1315
used to initialize a chewing context with specific dictionaries enabled.
16+
- New boolean config option `chewing.sort_candidates_by_frequency` can be
17+
set to enable sorting candidates by frequency.
1418

1519
* Bug Fixes
1620
- Auto shift cursor after candidate selection now moves the cursor to the

capi/src/io.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -417,6 +417,7 @@ pub unsafe extern "C" fn chewing_config_has_option(
417417
| "chewing.space_is_select_key"
418418
| "chewing.conversion_engine"
419419
| "chewing.enable_fullwidth_toggle_key"
420+
| "chewing.sort_candidates_by_frequency"
420421
);
421422

422423
ret as c_int
@@ -468,6 +469,7 @@ pub unsafe extern "C" fn chewing_config_get_int(
468469
ConversionEngineKind::FuzzyChewingEngine => FUZZY_CHEWING_CONVERSION_ENGINE,
469470
},
470471
"chewing.enable_fullwidth_toggle_key" => option.enable_fullwidth_toggle_key as c_int,
472+
"chewing.sort_candidates_by_frequency" => option.sort_candidates_by_frequency as c_int,
471473
_ => ERROR,
472474
}
473475
}
@@ -588,6 +590,10 @@ pub unsafe extern "C" fn chewing_config_set_int(
588590
ensure_bool!(value);
589591
options.enable_fullwidth_toggle_key = value > 0;
590592
}
593+
"chewing.sort_candidates_by_frequency" => {
594+
ensure_bool!(value);
595+
options.sort_candidates_by_frequency = value > 0;
596+
}
591597
_ => return ERROR,
592598
};
593599

src/editor/mod.rs

Lines changed: 99 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,7 @@ pub struct EditorOptions {
7777
pub lookup_strategy: LookupStrategy,
7878
pub conversion_engine: ConversionEngineKind,
7979
pub enable_fullwidth_toggle_key: bool,
80+
pub sort_candidates_by_frequency: bool,
8081
}
8182

8283
impl Default for EditorOptions {
@@ -97,6 +98,7 @@ impl Default for EditorOptions {
9798
// FIXME may be out of sync with the engine used
9899
conversion_engine: ConversionEngineKind::ChewingEngine,
99100
enable_fullwidth_toggle_key: true,
101+
sort_candidates_by_frequency: false,
100102
}
101103
}
102104
}
@@ -1558,7 +1560,7 @@ mod tests {
15581560
use crate::{
15591561
conversion::ChewingEngine,
15601562
dictionary::{Layered, TrieBuf},
1561-
editor::{EditorKeyBehavior, SymbolSelector, abbrev::AbbrevTable, estimate},
1563+
editor::{EditorKeyBehavior, EditorOptions, SymbolSelector, abbrev::AbbrevTable, estimate},
15621564
input::{
15631565
KeyboardEvent, keycode,
15641566
keymap::{QWERTY_MAP, map_ascii},
@@ -1641,6 +1643,102 @@ mod tests {
16411643
assert_eq!("冊", editor.display());
16421644
}
16431645

1646+
#[test]
1647+
fn editing_mode_input_bopomofo_select() {
1648+
let dict = TrieBuf::from([(
1649+
vec![crate::syl![Bopomofo::C, Bopomofo::E, Bopomofo::TONE4]],
1650+
vec![("冊", 100), ("測", 200)],
1651+
)]);
1652+
let dict = Layered::new(vec![Box::new(dict)], Box::new(TrieBuf::new_in_memory()));
1653+
let conversion_engine = Box::new(ChewingEngine::new());
1654+
let estimate = LaxUserFreqEstimate::new(0);
1655+
let abbrev = AbbrevTable::new();
1656+
let sym_sel = SymbolSelector::default();
1657+
let mut editor = Editor::new(conversion_engine, dict, estimate, abbrev, sym_sel);
1658+
1659+
editor.set_editor_options(EditorOptions {
1660+
sort_candidates_by_frequency: false,
1661+
..Default::default()
1662+
});
1663+
1664+
editor.process_keyevent(
1665+
KeyboardEvent::builder()
1666+
.code(keycode::KEY_H)
1667+
.ksym(keysym::SYM_LOWER_H)
1668+
.build(),
1669+
);
1670+
editor.process_keyevent(
1671+
KeyboardEvent::builder()
1672+
.code(keycode::KEY_K)
1673+
.ksym(keysym::SYM_LOWER_H)
1674+
.build(),
1675+
);
1676+
editor.process_keyevent(
1677+
KeyboardEvent::builder()
1678+
.code(keycode::KEY_4)
1679+
.ksym(keysym::SYM_4)
1680+
.build(),
1681+
);
1682+
editor.process_keyevent(
1683+
KeyboardEvent::builder()
1684+
.code(keycode::KEY_DOWN)
1685+
.ksym(keysym::SYM_DOWN)
1686+
.build(),
1687+
);
1688+
let candidates = editor
1689+
.all_candidates()
1690+
.expect("should be in selection mode");
1691+
assert_eq!(vec!["冊", "測"], candidates);
1692+
}
1693+
1694+
#[test]
1695+
fn editing_mode_input_bopomofo_select_sorted() {
1696+
let dict = TrieBuf::from([(
1697+
vec![crate::syl![Bopomofo::C, Bopomofo::E, Bopomofo::TONE4]],
1698+
vec![("冊", 100), ("測", 200)],
1699+
)]);
1700+
let dict = Layered::new(vec![Box::new(dict)], Box::new(TrieBuf::new_in_memory()));
1701+
let conversion_engine = Box::new(ChewingEngine::new());
1702+
let estimate = LaxUserFreqEstimate::new(0);
1703+
let abbrev = AbbrevTable::new();
1704+
let sym_sel = SymbolSelector::default();
1705+
let mut editor = Editor::new(conversion_engine, dict, estimate, abbrev, sym_sel);
1706+
1707+
editor.set_editor_options(EditorOptions {
1708+
sort_candidates_by_frequency: true,
1709+
..Default::default()
1710+
});
1711+
1712+
editor.process_keyevent(
1713+
KeyboardEvent::builder()
1714+
.code(keycode::KEY_H)
1715+
.ksym(keysym::SYM_LOWER_H)
1716+
.build(),
1717+
);
1718+
editor.process_keyevent(
1719+
KeyboardEvent::builder()
1720+
.code(keycode::KEY_K)
1721+
.ksym(keysym::SYM_LOWER_H)
1722+
.build(),
1723+
);
1724+
editor.process_keyevent(
1725+
KeyboardEvent::builder()
1726+
.code(keycode::KEY_4)
1727+
.ksym(keysym::SYM_4)
1728+
.build(),
1729+
);
1730+
editor.process_keyevent(
1731+
KeyboardEvent::builder()
1732+
.code(keycode::KEY_DOWN)
1733+
.ksym(keysym::SYM_DOWN)
1734+
.build(),
1735+
);
1736+
let candidates = editor
1737+
.all_candidates()
1738+
.expect("should be in selection mode");
1739+
assert_eq!(vec!["測", "冊"], candidates);
1740+
}
1741+
16441742
#[test]
16451743
fn editing_mode_input_chinese_to_english_mode() {
16461744
let dict = TrieBuf::from([(

src/editor/selection/phrase.rs

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
use std::cmp::min;
1+
use std::cmp::{Reverse, min};
22

33
use crate::{
44
conversion::{Composition, Gap, Interval},
@@ -228,21 +228,22 @@ impl PhraseSelector {
228228
self.lookup_strategy,
229229
)
230230
.into_iter()
231-
.map(|phrase| phrase.into())
232-
.collect::<Vec<String>>();
231+
.collect::<Vec<_>>();
233232
if self.end - self.begin == 1 {
234233
let alt = editor
235234
.syl
236235
.alt_syllables(self.com.symbol(self.begin).unwrap().to_syllable().unwrap());
237236
for &syl in alt {
238237
candidates.extend(
239238
dict.lookup_all_phrases(&[syl], self.lookup_strategy)
240-
.into_iter()
241-
.map(|ph| ph.into()),
239+
.into_iter(),
242240
)
243241
}
244242
}
245-
candidates
243+
if editor.options.sort_candidates_by_frequency {
244+
candidates.sort_by_key(|ph| Reverse(ph.freq()));
245+
}
246+
candidates.into_iter().map(|ph| ph.into()).collect()
246247
}
247248

248249
pub(crate) fn interval(&self, phrase: impl Into<Box<str>>) -> Interval {

tests/test-bopomofo.c

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -539,6 +539,36 @@ void test_select_candidate_shift_cursor_rearword()
539539
chewing_delete(ctx);
540540
}
541541

542+
void test_select_candidate_sorted()
543+
{
544+
ChewingContext *ctx;
545+
546+
static const char *CAND1[] = {
547+
"妙", "廟", "繆", "玅", "謬", "庙", "庿"
548+
};
549+
550+
static const char *CAND2[] = {
551+
"廟", "妙", "繆", "玅", "謬", "庙", "庿"
552+
};
553+
554+
clean_userphrase();
555+
556+
ctx = chewing_new();
557+
start_testcase(ctx);
558+
559+
chewing_set_candPerPage(ctx, 2);
560+
chewing_set_spaceAsSelection(ctx, 1);
561+
chewing_set_phraseChoiceRearward(ctx, 1);
562+
ok(chewing_config_set_int(ctx, "chewing.sort_candidates_by_frequency", 1) == 0, "set config should return OK");
563+
type_keystroke_by_string(ctx, "aul4 ");
564+
ok_candidate(ctx, CAND1, ARRAY_SIZE(CAND1));
565+
type_keystroke_by_string(ctx, "2<E>");
566+
type_keystroke_by_string(ctx, "aul4 ");
567+
ok_candidate(ctx, CAND2, ARRAY_SIZE(CAND2));
568+
569+
chewing_delete(ctx);
570+
}
571+
542572
void test_select_candidate()
543573
{
544574
test_select_candidate_no_rearward();
@@ -555,6 +585,7 @@ void test_select_candidate()
555585
test_select_candidate_second_page_rewind();
556586
test_select_candidate_shift_cursor();
557587
test_select_candidate_shift_cursor_rearword();
588+
test_select_candidate_sorted();
558589
}
559590

560591
void test_Esc_not_entering_chewing()

0 commit comments

Comments
 (0)