Skip to main content

msime_engine/zhuyin/
scheme.rs

1//! The Dachen bopomofo editor with libchewing's semantics: keys fill the pending syllable, a tone key completes it against the syllable inventory, and the completed syllables are reconverted after every change. Candidates come from a list the user opens; choosing one pins that span's text and never commits. Text leaves the editor only through Enter, Shift punctuation, auto-shift past `MAX_SYLLABLES` and `take_text`.
2//!
3//! 九键模式(`set_nine_key`)下不读大千键:数字串按 `nine_key::KEYPAD` 记下符号位置,声调键结束音节,这个位置的读音是数字串加声调对应的全部合法音节;转换在每个位置的读音里一起挑,用户可以经 `choose_spelling` 逐个钉住目标音节的读音。
4
5use std::cmp::Reverse;
6use std::collections::{HashMap, HashSet};
7use std::sync::Arc;
8
9use super::conversion::{self, Span, MAX_SYLLABLES};
10use super::layout::{self, DACHEN_SYMBOLS, IDLE_SYMBOLS, SHIFT_PUNCTUATION};
11use super::nine_key::{self, NineKeyIndex};
12use super::syllable::PendingSyllable;
13use crate::error::Result;
14use crate::language_dictionary::{LanguageDictionary, LanguageEntry};
15use crate::types::{QueryRequest, SchemeType};
16
17/// The non-letter keys the editor still claims while the list is open: the phonetic keys that are not selection digits. Digits 1–9 and Space go to selection.
18pub const LIST_OPEN_SYMBOLS: &str = "0,./;-";
19const SMALL_READING_DEDUP: usize = 32;
20
21/// A key as the editor sees it. The session maps host keys and command 16 (`Command::ConvertHanja`, "open the candidate list") onto these.
22#[derive(Debug, Clone, Copy, PartialEq, Eq)]
23pub enum ZhuyinKey {
24    /// A printable ASCII key: a Dachen key, a tone key, or a Shift punctuation key.
25    Char(u8),
26    /// Open the list, or close it when it is open.
27    OpenList,
28    Enter,
29    Escape,
30    Backspace,
31}
32
33/// One row of the candidate list: `text` for the syllables from `start` to the end of the composition.
34#[derive(Debug, Clone, PartialEq, Eq)]
35pub struct ListCandidate {
36    pub text: String,
37    pub start: usize,
38    /// 这一行在词库里的键(带调音节以空格连接),选中后随 pin 一起保存,说明被覆盖的音节用的是哪个读音。
39    pub key: String,
40}
41
42/// 一个已完成的音节和打出它的键。
43#[derive(Debug, Clone, PartialEq, Eq)]
44struct Syllable {
45    /// 大千下是大千键加声调键,九键下是数字串加声调键。
46    keys: String,
47    /// 这个位置合法的带调音节。大千下只有一个;九键下是这串数字和声调对应的全部音节。
48    readings: Arc<[String]>,
49    /// 九键下用户钉住的读音在 `readings` 里的下标;大千下恒为 `None`。
50    locked: Option<usize>,
51}
52
53impl Syllable {
54    /// 转换时这个位置允许的读音:钉住的那一个,否则全部。
55    fn allowed(&self) -> &[String] {
56        match self.locked {
57            Some(index) => std::slice::from_ref(&self.readings[index]),
58            None => &self.readings,
59        }
60    }
61}
62
63#[derive(Default)]
64struct BestCache {
65    entries: HashMap<String, Option<(String, LanguageEntry)>>,
66    description: String,
67}
68
69impl BestCache {
70    fn clear(&mut self) {
71        self.entries.clear();
72        self.description.clear();
73    }
74}
75
76pub struct ZhuyinScheme {
77    dictionary: LanguageDictionary,
78    syllables: Vec<Syllable>,
79    /// Spans whose text the user chose from the list; non-overlapping.
80    pins: Vec<Span>,
81    pending: PendingSyllable,
82    /// The current conversion of `syllables`, recomputed after every change to them or to `pins`.
83    conversion: Vec<Span>,
84    list_open: bool,
85    /// The list rows while `list_open`, empty otherwise.
86    list: Vec<ListCandidate>,
87    /// Text the last key committed, waiting for the session to hand it to the host.
88    committed: String,
89    /// 组字过程中每段位置描述(各位置允许的读音以 `|` 连接,位置之间用空格)的最重词条及其键。词库只读,所以条目一直有效;九键下每次钉读音都会产生新的描述,缓存在组字结束和每次 auto-shift 时清空。
90    best: BestCache,
91    /// 注音九键模式:数字键拼音节,`zxcvb` 和空格是声调键。
92    nine_key: bool,
93    /// 九键模式第一次处理按键时从音节表建出的索引,之后一直保留。
94    nine_key_index: Option<NineKeyIndex>,
95    /// 九键下还没按声调键的数字串。
96    pending_digits: Vec<u8>,
97    /// 九键下供用户钉读音的目标音节的候选读音,当前转换用的排在最前;最近一次转换后算出,列表打开时不对外提供。
98    spellings: Vec<String>,
99    /// `spellings` 对应的音节下标。
100    spelling_target: Option<usize>,
101}
102
103impl ZhuyinScheme {
104    /// An idle editor reading `dictionary` (`msime-zhuyin.db`).
105    pub fn new(dictionary: LanguageDictionary) -> Self {
106        Self {
107            dictionary,
108            syllables: Vec::new(),
109            pins: Vec::new(),
110            pending: PendingSyllable::default(),
111            conversion: Vec::new(),
112            list_open: false,
113            list: Vec::new(),
114            committed: String::new(),
115            best: BestCache::default(),
116            nine_key: false,
117            nine_key_index: None,
118            pending_digits: Vec::new(),
119            spellings: Vec::new(),
120            spelling_target: None,
121        }
122    }
123
124    /// 切换注音九键模式,丢掉正在进行的组字。`reset` 不改变模式。
125    pub fn set_nine_key(&mut self, enabled: bool) {
126        self.clear_composition();
127        self.nine_key = enabled;
128    }
129
130    pub fn nine_key(&self) -> bool {
131        self.nine_key
132    }
133
134    /// The `msime-zhuyin.db` connection, given back when the editor is replaced so the next one reuses it.
135    pub fn into_dictionary(self) -> LanguageDictionary {
136        self.dictionary
137    }
138
139    /// Drops the composition and any text not yet taken.
140    pub fn reset(&mut self) {
141        self.clear_composition();
142        self.committed.clear();
143    }
144
145    pub fn is_composing(&self) -> bool {
146        !self.syllables.is_empty() || !self.pending.is_empty() || !self.pending_digits.is_empty()
147    }
148
149    /// Handles one key and returns whether the editor claimed it. An unclaimed key is left to the session and host (idle tone digits and Space type themselves; selection digits and Space while the list is open select through the session). Text the key committed is in `take_committed`.
150    pub fn handle_key(&mut self, key: ZhuyinKey) -> Result<bool> {
151        match key {
152            ZhuyinKey::Char(byte) => self.handle_char(byte),
153            _ if !self.is_composing() => Ok(false),
154            ZhuyinKey::OpenList => {
155                if self.list_open {
156                    self.close_list();
157                } else {
158                    self.open_list()?;
159                }
160                Ok(true)
161            }
162            ZhuyinKey::Enter => {
163                let text = self.take_text();
164                self.committed.push_str(&text);
165                Ok(true)
166            }
167            ZhuyinKey::Escape => {
168                if self.list_open {
169                    self.close_list();
170                } else {
171                    self.clear_composition();
172                }
173                Ok(true)
174            }
175            ZhuyinKey::Backspace => {
176                if self.list_open {
177                    self.close_list();
178                } else if self.pending_digits.pop().is_none() && !self.pending.pop() {
179                    let end = self.syllables.len();
180                    self.syllables.pop();
181                    self.pins.retain(|pin| pin.end != end);
182                    self.reconvert()?;
183                    if !self.is_composing() {
184                        self.clear_composition();
185                    }
186                }
187                Ok(true)
188            }
189        }
190    }
191
192    /// Pins the text of list row `index` over its span and closes the list. Nothing is committed. Returns false when the list is closed or has no such row.
193    pub fn select(&mut self, index: usize) -> Result<bool> {
194        if !self.list_open {
195            return Ok(false);
196        }
197        if index >= self.list.len() {
198            return Ok(false);
199        }
200        let candidate = self.list.swap_remove(index);
201        let pin = Span {
202            start: candidate.start,
203            end: self.syllables.len(),
204            key: candidate.key,
205            text: candidate.text,
206        };
207        self.pins
208            .retain(|other| !other.overlaps(pin.start, pin.end));
209        self.pins.push(pin);
210        self.pins.sort_by_key(|pin| pin.start);
211        self.close_list();
212        self.reconvert()?;
213        Ok(true)
214    }
215
216    /// The converted text, ending the composition; the pending syllable is dropped. This is what blur, a scheme switch and Enter commit.
217    pub fn take_text(&mut self) -> String {
218        let text = self.converted_text();
219        self.clear_composition();
220        text
221    }
222
223    /// The text the last key committed; empty when it committed none.
224    pub fn take_committed(&mut self) -> String {
225        std::mem::take(&mut self.committed)
226    }
227
228    /// 打出组字的键,按顺序,供光标锁定的编辑文本使用;九键下是数字串加声调键,例如 `28c39c4`。
229    pub fn editing_text(&self) -> String {
230        let mut keys =
231            build_editing_keys(&self.syllables, &self.pending, self.pending_digits.len());
232        keys.extend(self.pending_digits.iter().map(|digit| char::from(*digit)));
233        keys
234    }
235
236    fn editing_text_into(&self, keys: &mut String) {
237        append_editing_keys(
238            &self.syllables,
239            &self.pending,
240            self.pending_digits.len(),
241            keys,
242        );
243        keys.extend(self.pending_digits.iter().map(|digit| char::from(*digit)));
244    }
245
246    /// 转换后的文字加上还在拼的部分:大千是待定的注音符号,例如 `你好ㄇㄚ`;九键是还没按声调的数字,例如 `你好28`。
247    pub fn reading(&self) -> String {
248        let mut reading = String::new();
249        self.reading_into(&mut reading);
250        reading
251    }
252
253    fn reading_into(&self, reading: &mut String) {
254        let pending_capacity = [self.pending.initial, self.pending.medial, self.pending.rime]
255            .into_iter()
256            .flatten()
257            .map(char::len_utf8)
258            .sum::<usize>()
259            + self.pending_digits.len();
260        append_converted_text(&self.conversion, pending_capacity, reading);
261        if let Some(symbol) = self.pending.initial {
262            reading.push(symbol);
263        }
264        if let Some(symbol) = self.pending.medial {
265            reading.push(symbol);
266        }
267        if let Some(symbol) = self.pending.rime {
268            reading.push(symbol);
269        }
270        reading.extend(self.pending_digits.iter().map(|digit| char::from(*digit)));
271    }
272
273    /// What the session shows and tests for a composition: the reading, which is empty exactly when nothing is composing.
274    pub fn preedit(&self) -> String {
275        self.reading()
276    }
277
278    /// The request the session keeps for the composition. Nothing is queried with it, since the list rows come from the editor itself; `raw_input` is the typed keys the caret-locked editing text shows and `normalized_segmentation` the reading the snapshot draws.
279    pub fn build_request(&self) -> QueryRequest {
280        let mut request = QueryRequest::default();
281        self.build_request_into(&mut request);
282        request
283    }
284
285    /// 将注音请求写入已有存储,避免刷新时重复复制按键和读音字符串。
286    pub fn build_request_into(&self, request: &mut QueryRequest) {
287        request.scheme = SchemeType::Zhuyin;
288        self.editing_text_into(&mut request.raw_input);
289        request.raw_input_with_cases.clone_from(&request.raw_input);
290        request.normalized_input.clone_from(&request.raw_input);
291        request.raw_segmentation.clone_from(&request.raw_input);
292        self.reading_into(&mut request.normalized_segmentation);
293        request.segmentation.clear();
294        request.korean_hanja = false;
295        request.valid = self.is_composing();
296    }
297
298    pub fn converted_text(&self) -> String {
299        build_converted_text(&self.conversion)
300    }
301
302    pub fn list_open(&self) -> bool {
303        self.list_open
304    }
305
306    /// The list rows; empty unless the list is open.
307    pub fn candidates(&self) -> &[ListCandidate] {
308        &self.list
309    }
310
311    /// 九键下供用户钉读音的候选读音(带调注音,一声不带符号),当前转换用的排在最前;列表打开、没有歧义音节或在大千模式时为空。
312    pub fn spellings(&self) -> &[String] {
313        if self.list_open {
314            return &[];
315        }
316        &self.spellings
317    }
318
319    /// 把 `spellings()[index]` 钉为目标音节的读音并重新转换,不提交任何文字。读音即使与当前转换相同也照样钉住,目标随之移到下一个歧义音节,用户可以逐个确认。列表打开、没有目标或下标越界时返回 false。
320    pub fn choose_spelling(&mut self, index: usize) -> Result<bool> {
321        if self.list_open {
322            return Ok(false);
323        }
324        let (Some(target), Some(reading)) = (self.spelling_target, self.spellings.get(index))
325        else {
326            return Ok(false);
327        };
328        let Some(syllable) = self.syllables.get_mut(target) else {
329            return Ok(false);
330        };
331        let Some(position) = syllable.readings.iter().position(|other| other == reading) else {
332            return Ok(false);
333        };
334        syllable.locked = Some(position);
335        self.reconvert()?;
336        Ok(true)
337    }
338
339    /// The non-letter keys the editor claims in its current state, in `DACHEN_SYMBOLS` order; 九键下见 `nine_key` 的三个常量。
340    pub fn spelling_symbols(&self) -> &'static str {
341        if self.nine_key {
342            return if !self.is_composing() {
343                nine_key::IDLE_SYMBOLS
344            } else if self.list_open {
345                nine_key::LIST_OPEN_SYMBOLS
346            } else {
347                nine_key::SYMBOLS
348            };
349        }
350        if !self.is_composing() {
351            IDLE_SYMBOLS
352        } else if self.list_open {
353            LIST_OPEN_SYMBOLS
354        } else {
355            DACHEN_SYMBOLS
356        }
357    }
358
359    fn handle_char(&mut self, byte: u8) -> Result<bool> {
360        if let Some((_, mark)) = SHIFT_PUNCTUATION.iter().find(|(key, _)| *key == byte) {
361            let text = self.take_text();
362            self.committed.push_str(&text);
363            self.committed.push(*mark);
364            return Ok(true);
365        }
366        if self.nine_key {
367            return self.handle_nine_key_char(byte);
368        }
369        if self.list_open {
370            if !byte.is_ascii_lowercase() && !LIST_OPEN_SYMBOLS.as_bytes().contains(&byte) {
371                return Ok(false);
372            }
373            self.close_list();
374        }
375        // Every phonetic key is a lowercase letter or one of IDLE_SYMBOLS, so each starts a composition from idle.
376        if let Some((symbol, kind)) = layout::symbol(byte) {
377            self.pending.insert(symbol, kind);
378            return Ok(true);
379        }
380        let Some(mark) = layout::tone_mark(byte) else {
381            return Ok(false);
382        };
383        if !self.is_composing() {
384            return Ok(false);
385        }
386        let Some(toned) = self.pending.toned(mark) else {
387            // A tone with nothing pending: Space opens the list, the tone digits are swallowed.
388            if byte == b' ' {
389                self.open_list()?;
390            }
391            return Ok(true);
392        };
393        // A syllable the inventory does not know stays pending; libchewing beeps here.
394        if !self.dictionary.has_syllable(&toned)? {
395            return Ok(true);
396        }
397        if self.syllables.len() == MAX_SYLLABLES {
398            self.shift_leftmost_word();
399        }
400        let mut keys = self.pending.keys();
401        keys.push(char::from(byte));
402        self.syllables.push(Syllable {
403            keys,
404            readings: Arc::from([toned]),
405            locked: None,
406        });
407        self.pending.clear();
408        self.reconvert()?;
409        Ok(true)
410    }
411
412    /// 九键模式的字符键。列表打开时,数字和声调字母关闭列表后照常处理,其余键(包括空格)不认领,交给运行时选行。数字总是认领,只有还能拼成某个音节时才追加,否则吞掉(libchewing 在这里会响铃)。声调键结束一个音节:空闲时声调字母和空格都不认领(实体键盘上的这几个字母照常上屏,软键盘空闲时不发声调键);有组字但没有数字时空格打开列表、声调字母被吞掉;数字串加这个声调没有合法音节时数字留着。其他键(包括全部字母)不认领,九键模式不读大千键。
413    fn handle_nine_key_char(&mut self, byte: u8) -> Result<bool> {
414        let mark = nine_key::tone_mark(byte);
415        if self.list_open {
416            if !byte.is_ascii_digit() && (mark.is_none() || byte == b' ') {
417                return Ok(false);
418            }
419            self.close_list();
420        }
421        if byte.is_ascii_digit() {
422            if self.pending_digits.len() == nine_key::MAX_DIGITS {
423                return Ok(true);
424            }
425            let mut next = std::mem::take(&mut self.pending_digits);
426            next.push(byte);
427            let accepted = self.nine_key_index().map(|index| index.accepts(&next));
428            if !matches!(accepted, Ok(true)) {
429                next.pop();
430            }
431            self.pending_digits = next;
432            accepted?;
433            return Ok(true);
434        }
435        let Some(mark) = mark else {
436            return Ok(false);
437        };
438        if !self.is_composing() {
439            return Ok(false);
440        }
441        if self.pending_digits.is_empty() {
442            if byte == b' ' {
443                self.open_list()?;
444            }
445            return Ok(true);
446        }
447        let mut digits = std::mem::take(&mut self.pending_digits);
448        let readings = match self.nine_key_index() {
449            Ok(index) => index.readings(&digits, mark),
450            Err(error) => {
451                self.pending_digits = digits;
452                return Err(error);
453            }
454        };
455        let Some(readings) = readings else {
456            self.pending_digits = digits;
457            return Ok(true);
458        };
459        if self.syllables.len() == MAX_SYLLABLES {
460            self.shift_leftmost_word();
461        }
462        let mut keys: String = digits.iter().map(|digit| char::from(*digit)).collect();
463        keys.push(char::from(byte));
464        digits.clear();
465        self.pending_digits = digits;
466        self.syllables.push(Syllable {
467            keys,
468            readings,
469            locked: None,
470        });
471        self.reconvert()?;
472        Ok(true)
473    }
474
475    /// 九键索引,第一次用到时从 `msime-zhuyin.db` 的音节表建立,大千用户不付出这份代价。
476    fn nine_key_index(&mut self) -> Result<&NineKeyIndex> {
477        let index = match self.nine_key_index.take() {
478            Some(index) => index,
479            None => NineKeyIndex::new(self.dictionary.syllables()?),
480        };
481        Ok(self.nine_key_index.insert(index))
482    }
483
484    /// Commits the first converted word and drops its syllables, making room for one more.
485    fn shift_leftmost_word(&mut self) {
486        let Some(word) = self.conversion.first() else {
487            return;
488        };
489        let end = word.end;
490        self.committed.push_str(&word.text);
491        self.syllables.drain(..end);
492        self.best.clear();
493        self.pins.retain(|pin| pin.start >= end);
494        for pin in &mut self.pins {
495            pin.start -= end;
496            pin.end -= end;
497        }
498    }
499
500    /// Lists every suffix span of the syllables, longest first, each span's entries by weight. Every entry is listed, since the list is the only way to choose a character; common syllables such as ㄧˋ have over 200. The list stays closed when it would be empty.
501    fn open_list(&mut self) -> Result<()> {
502        let count = self.syllables.len();
503        self.list.clear();
504        let positions = list_positions(&self.syllables);
505        for start in 0..count {
506            let mut entries = self
507                .dictionary
508                .lookup_readings(&positions[start..count], usize::MAX)?;
509            // 九键下同一个字可能在同一位置的两个读音下各有一条,只留较重的那条。
510            deduplicate_reading_entries(&mut entries);
511            self.list.reserve(entries.len());
512            self.list
513                .extend(entries.into_iter().map(|(key, entry)| ListCandidate {
514                    text: entry.text,
515                    start,
516                    key,
517                }));
518        }
519        self.list_open = !self.list.is_empty();
520        Ok(())
521    }
522
523    fn close_list(&mut self) {
524        self.list_open = false;
525        self.list.clear();
526    }
527
528    fn clear_composition(&mut self) {
529        self.syllables.clear();
530        self.pins.clear();
531        self.pending.clear();
532        self.pending_digits.clear();
533        self.spellings.clear();
534        self.spelling_target = None;
535        self.conversion.clear();
536        self.close_list();
537        self.best.clear();
538    }
539
540    fn reconvert(&mut self) -> Result<()> {
541        let count = self.syllables.len();
542        let (positions, ambiguous) = reconversion_buffers(&self.syllables);
543        let positions = &positions[..count];
544        let ambiguous = &ambiguous[..count];
545        // 歧义按打字时的读音算,不按钉住后剩下的:钉住当前用的读音不该放出被下限挡住的冷僻词(是之 → 適之)。
546        let dictionary = &self.dictionary;
547        let best = &mut self.best;
548        // 每个位置单字最重词条的权重,只在九键下有位置打出来不止一个读音时才用得到。
549        let mut singles = [0; MAX_SYLLABLES];
550        if ambiguous.iter().any(|&ambiguous| ambiguous) {
551            for index in 0..positions.len() {
552                singles[index] =
553                    cached_best_weight(dictionary, best, &positions[index..=index])?.unwrap_or(0);
554            }
555        }
556        let singles = &singles[..count];
557        self.conversion = conversion::convert_filtered(
558            positions.len(),
559            &self.pins,
560            |start, end, can_improve| {
561                cached_best(
562                    dictionary,
563                    best,
564                    &positions[start..end],
565                    |entry| match entry.filter(|(_, entry)| {
566                        clears_ambiguous_word_floor(&ambiguous[start..end], singles, start, entry)
567                    }) {
568                        Some((key, entry)) if can_improve(entry.weight) => {
569                            conversion::Lookup::Found(key.clone(), entry.clone())
570                        }
571                        Some(_) => conversion::Lookup::Rejected,
572                        None => conversion::Lookup::Missing,
573                    },
574                )
575            },
576            |index| positions[index][0].clone(),
577        )?;
578        self.refresh_spellings()
579    }
580
581    /// 重算钉读音的目标和它的候选读音。目标是第一个不被任何 pin 覆盖、没有钉住、且有不止一个读音的音节;读音按当前转换用的那个、单字最重词条的权重(从重到轻)、字典序排列。没有目标时为空,大千模式下永远为空。
582    fn refresh_spellings(&mut self) -> Result<()> {
583        let result = self.refresh_spelling_choices();
584        if result.is_err() {
585            self.spellings.clear();
586        }
587        result
588    }
589
590    fn refresh_spelling_choices(&mut self) -> Result<()> {
591        self.spelling_target = None;
592        if !self.nine_key {
593            self.spellings.clear();
594            return Ok(());
595        }
596        let Some(target) = self
597            .syllables
598            .iter()
599            .enumerate()
600            .position(|(index, syllable)| {
601                syllable.locked.is_none()
602                    && syllable.readings.len() > 1
603                    && !self.pins.iter().any(|pin| pin.overlaps(index, index + 1))
604            })
605        else {
606            self.spellings.clear();
607            return Ok(());
608        };
609        let current = current_spelling(&self.conversion, target);
610        let readings = Arc::clone(&self.syllables[target].readings);
611        if readings.len() <= SMALL_SPELLING_RANK {
612            let mut ranked = [(false, Reverse(0), 0usize); SMALL_SPELLING_RANK];
613            for (index, reading) in readings.iter().enumerate() {
614                let weight = cached_best_weight(
615                    &self.dictionary,
616                    &mut self.best,
617                    &[std::slice::from_ref(reading)],
618                )?
619                .unwrap_or(i64::MIN);
620                ranked[index] = (current != Some(reading.as_str()), Reverse(weight), index);
621            }
622            ranked[..readings.len()].sort_by(|left, right| {
623                left.0
624                    .cmp(&right.0)
625                    .then_with(|| left.1.cmp(&right.1))
626                    .then_with(|| readings[left.2].cmp(&readings[right.2]))
627            });
628            replace_string_buffer(
629                &mut self.spellings,
630                ranked[..readings.len()]
631                    .iter()
632                    .map(|(_, _, index)| readings[*index].as_str()),
633            );
634        } else {
635            let mut ranked = Vec::with_capacity(readings.len());
636            for reading in readings.iter() {
637                let weight = cached_best_weight(
638                    &self.dictionary,
639                    &mut self.best,
640                    &[std::slice::from_ref(reading)],
641                )?
642                .unwrap_or(i64::MIN);
643                ranked.push((current != Some(reading.as_str()), Reverse(weight), reading));
644            }
645            ranked.sort();
646            replace_string_buffer(
647                &mut self.spellings,
648                ranked.into_iter().map(|(_, _, reading)| reading.as_str()),
649            );
650        }
651        self.spelling_target = Some(target);
652        Ok(())
653    }
654}
655
656fn replace_string_buffer<'a>(
657    destination: &mut Vec<String>,
658    mut source: impl Iterator<Item = &'a str>,
659) {
660    let mut kept = 0;
661    loop {
662        let Some(value) = source.next() else {
663            destination.truncate(kept);
664            return;
665        };
666        if let Some(target) = destination.get_mut(kept) {
667            target.clear();
668            target.push_str(value);
669            kept += 1;
670        } else {
671            destination.extend(std::iter::once(value).chain(source).map(str::to_owned));
672            return;
673        }
674    }
675}
676
677fn deduplicate_reading_entries(entries: &mut Vec<(String, LanguageEntry)>) {
678    if entries.len() <= SMALL_READING_DEDUP {
679        let mut write = 0;
680        for read in 0..entries.len() {
681            if entries[..write]
682                .iter()
683                .any(|(_, entry)| entry.text == entries[read].1.text)
684            {
685                continue;
686            }
687            if write != read {
688                entries.swap(write, read);
689            }
690            write += 1;
691        }
692        entries.truncate(write);
693        return;
694    }
695    let mut seen = HashSet::with_capacity(entries.len());
696    let duplicates = entries
697        .iter()
698        .enumerate()
699        .filter_map(|(index, (_, entry))| (!seen.insert(entry.text.as_str())).then_some(index))
700        .collect::<Vec<_>>();
701    drop(seen);
702    let mut duplicates = duplicates.into_iter().peekable();
703    let mut write = 0;
704    for read in 0..entries.len() {
705        if duplicates.peek() == Some(&read) {
706            duplicates.next();
707            continue;
708        }
709        if write != read {
710            entries.swap(write, read);
711        }
712        write += 1;
713    }
714    entries.truncate(write);
715}
716
717fn list_positions<'a>(syllables: &'a [Syllable]) -> [&'a [String]; MAX_SYLLABLES] {
718    let mut positions: [&'a [String]; MAX_SYLLABLES] = [&[]; MAX_SYLLABLES];
719    for (index, syllable) in syllables.iter().enumerate() {
720        positions[index] = syllable.allowed();
721    }
722    positions
723}
724
725fn reconversion_buffers<'a>(
726    syllables: &'a [Syllable],
727) -> ([&'a [String]; MAX_SYLLABLES], [bool; MAX_SYLLABLES]) {
728    let mut positions: [&'a [String]; MAX_SYLLABLES] = [&[]; MAX_SYLLABLES];
729    let mut ambiguous = [false; MAX_SYLLABLES];
730    for (index, syllable) in syllables.iter().enumerate() {
731        positions[index] = syllable.allowed();
732        ambiguous[index] = syllable.readings.len() > 1;
733    }
734    (positions, ambiguous)
735}
736
737fn current_spelling(conversion: &[Span], target: usize) -> Option<&str> {
738    conversion
739        .iter()
740        .find(|span| span.overlaps(target, target + 1))
741        .and_then(|span| span.key.split(' ').nth(target - span.start))
742}
743
744/// 多音节词经过一个有多种允许读法的位置时,词频最多可以比它覆盖的最弱单字轻多少倍。
745///
746/// Conversion ranks paths by word length first (the sum of squared span lengths in `conversion.rs`), which suits Dachen, where each position has exactly one reading. A nine-key position allows 4 to 23 readings, so the combined reading sets of two or three positions match some obscure word almost everywhere, and length-first alone lets 監聽器 (weight 9) beat 今天 (25469) + 去 (28394). Such a word therefore takes part only when its weight times this factor reaches the smallest single-character weight over its positions. 1000 was chosen against a rebuild of the libchewing-derived dictionary: it drops 監聽器, 趕明兒 and 禮教 from 我們今天去學校, 這個東西很便宜 and 請問你叫什麼名字, and of the sampled counted 2 to 4 syllable words that convert to themselves without the floor all but one still do, while a factor of 300 already loses about one in eight of them.
747const AMBIGUOUS_WORD_FLOOR: i64 = 1000;
748
749/// 九键一个音节的读音通常很短;短列表用栈上的排序状态,避免为临时排名向量分配堆内存。
750const SMALL_SPELLING_RANK: usize = 64;
751
752/// Whether `entry`, the heaviest entry for the span starting at syllable `start` whose positions are flagged in `ambiguous`, may take part in conversion. Single syllables and spans where no position was typed with more than one reading (all of Dachen) always may, so Dachen conversion is unchanged; otherwise see `AMBIGUOUS_WORD_FLOOR`. Ambiguity is how the syllable was typed, not what is left after the user pinned a reading: pinning the reading the conversion already uses must not let a word the floor held back win (是之 turning into 適之). `singles` holds every position's single-character weight over its allowed readings and is empty when no position is ambiguous.
753fn clears_ambiguous_word_floor(
754    ambiguous: &[bool],
755    singles: &[i64],
756    start: usize,
757    entry: &LanguageEntry,
758) -> bool {
759    if ambiguous.len() < 2 || !ambiguous.iter().any(|&ambiguous| ambiguous) {
760        return true;
761    }
762    let weakest = singles[start..start + ambiguous.len()]
763        .iter()
764        .copied()
765        .min()
766        .unwrap_or(0);
767    entry.weight.saturating_mul(AMBIGUOUS_WORD_FLOOR) >= weakest
768}
769
770/// 只取词频时,单位置单读音的缓存键直接借用读音;未命中或需要组合缓存键时复用完整词条的查询路径。
771fn cached_best_weight(
772    dictionary: &LanguageDictionary,
773    best: &mut BestCache,
774    positions: &[&[String]],
775) -> Result<Option<i64>> {
776    if let [[reading]] = positions {
777        if let Some(entry) = best.entries.get(reading.as_str()) {
778            return Ok(entry.as_ref().map(|(_, entry)| entry.weight));
779        }
780    }
781    cached_best(dictionary, best, positions, |entry| {
782        entry.map(|(_, entry)| entry.weight)
783    })
784}
785
786/// 在借用期间消费 `positions` 的缓存词条;只在成功插入新键时复制描述,查询缓冲始终保留。
787fn cached_best<T>(
788    dictionary: &LanguageDictionary,
789    best: &mut BestCache,
790    positions: &[&[String]],
791    consume: impl FnOnce(Option<&(String, LanguageEntry)>) -> T,
792) -> Result<T> {
793    describe(positions, &mut best.description);
794    if let Some(entry) = best.entries.get(best.description.as_str()) {
795        return Ok(consume(entry.as_ref()));
796    }
797    let candidate = dictionary.lookup_readings(positions, 1)?.into_iter().next();
798    let description = best.description.clone();
799    Ok(consume(
800        best.entries
801            .entry(description)
802            .or_insert(candidate)
803            .as_ref(),
804    ))
805}
806
807/// `cached_best` 的缓存键:各位置的读音以 `|` 连接,位置之间用空格。
808fn describe(positions: &[&[String]], description: &mut String) {
809    let capacity = positions
810        .iter()
811        .enumerate()
812        .map(|(position, readings)| {
813            readings.iter().map(String::len).sum::<usize>()
814                + readings.len().saturating_sub(1)
815                + usize::from(position > 0)
816        })
817        .sum();
818    description.clear();
819    description.reserve(capacity);
820    for (position, readings) in positions.iter().enumerate() {
821        if position > 0 {
822            description.push(' ');
823        }
824        for (reading, value) in readings.iter().enumerate() {
825            if reading > 0 {
826                description.push('|');
827            }
828            description.push_str(value);
829        }
830    }
831}
832
833fn build_editing_keys(
834    syllables: &[Syllable],
835    pending: &PendingSyllable,
836    extra_capacity: usize,
837) -> String {
838    let mut keys = String::new();
839    append_editing_keys(syllables, pending, extra_capacity, &mut keys);
840    keys
841}
842
843fn build_converted_text(spans: &[Span]) -> String {
844    let mut text = String::new();
845    append_converted_text(spans, 0, &mut text);
846    text
847}
848
849fn append_editing_keys(
850    syllables: &[Syllable],
851    pending: &PendingSyllable,
852    extra_capacity: usize,
853    keys: &mut String,
854) {
855    let pending_capacity = usize::from(pending.initial.is_some())
856        + usize::from(pending.medial.is_some())
857        + usize::from(pending.rime.is_some());
858    let capacity = syllables
859        .iter()
860        .map(|syllable| syllable.keys.len())
861        .sum::<usize>()
862        + pending_capacity
863        + extra_capacity;
864    keys.clear();
865    keys.reserve(capacity);
866    for syllable in syllables {
867        keys.push_str(&syllable.keys);
868    }
869    pending.append_keys(keys);
870}
871
872fn append_converted_text(spans: &[Span], extra_capacity: usize, text: &mut String) {
873    let capacity = spans.iter().map(|span| span.text.len()).sum::<usize>() + extra_capacity;
874    text.clear();
875    text.reserve(capacity);
876    for span in spans {
877        text.push_str(&span.text);
878    }
879}
880
881#[cfg(test)]
882mod tests {
883    use rusqlite::Connection;
884
885    use super::*;
886    use crate::language_dictionary::{
887        open_read_only, FORMAT_VERSION, METADATA_FORMAT_VERSION, SCHEMA,
888    };
889
890    const ENTRIES: [(&str, &str, i64); 15] = [
891        ("ㄋㄧˇ", "你", 1000),
892        ("ㄋㄧˇ", "妳", 300),
893        ("ㄋㄧˇ", "擬", 50),
894        ("ㄏㄠˇ", "好", 2000),
895        ("ㄏㄠˇ", "郝", 10),
896        ("ㄋㄧˇ ㄏㄠˇ", "你好", 500),
897        ("ㄊㄞˊ", "台", 900),
898        ("ㄊㄞˊ", "臺", 400),
899        ("ㄨㄢ", "彎", 500),
900        ("ㄨㄢ", "灣", 300),
901        ("ㄊㄞˊ ㄨㄢ", "臺灣", 800),
902        ("ㄊㄞˊ ㄨㄢ", "台灣", 600),
903        ("ㄇㄚ˙", "嗎", 800),
904        ("ㄇㄚ", "媽", 700),
905        ("ㄢ", "安", 100),
906    ];
907
908    fn syllable(keys: &str, readings: &[&str]) -> Syllable {
909        Syllable {
910            keys: keys.to_owned(),
911            readings: readings
912                .iter()
913                .map(|reading| (*reading).to_owned())
914                .collect(),
915            locked: None,
916        }
917    }
918
919    #[test]
920    fn editing_keys_append_syllables_and_pending_keys_in_order() {
921        let syllables = vec![syllable("su3", &["ㄋㄧˇ"]), syllable("lc3", &["ㄏㄠˇ"])];
922        let pending = PendingSyllable {
923            initial: Some('ㄇ'),
924            medial: Some('ㄚ'),
925            rime: None,
926        };
927
928        assert_eq!(build_editing_keys(&syllables, &pending, 0), "su3lc3a8");
929    }
930
931    #[test]
932    fn list_positions_keep_allowed_readings_on_the_stack() {
933        let syllables = [
934            syllable("su3", &["ㄋㄧˇ"]),
935            syllable("28c", &["ㄋㄧˇ", "ㄌㄧˇ"]),
936        ];
937
938        let positions = list_positions(&syllables);
939
940        assert_eq!(positions[0], &["ㄋㄧˇ".to_owned()]);
941        assert_eq!(positions[1], &["ㄋㄧˇ".to_owned(), "ㄌㄧˇ".to_owned()]);
942    }
943
944    #[test]
945    fn short_reading_dedup_keeps_first_rows_without_temporary_heap_state() {
946        let mut entries = (0..32)
947            .map(|index| {
948                (
949                    format!("key-{index}"),
950                    LanguageEntry {
951                        text: format!("字{}", index % 18),
952                        weight: index as i64,
953                    },
954                )
955            })
956            .collect::<Vec<_>>();
957        let ((), allocations) = crate::ime::personal_rerank::allocations::count(|| {
958            deduplicate_reading_entries(&mut entries);
959        });
960        assert_eq!(allocations, 0);
961        assert_eq!(entries.len(), 18);
962        assert_eq!(entries[0].1.text, "字0");
963        assert_eq!(entries[17].1.text, "字17");
964
965        let mut large = (0..=SMALL_READING_DEDUP)
966            .map(|index| {
967                (
968                    format!("key-{index}"),
969                    LanguageEntry {
970                        text: format!("大字{index}"),
971                        weight: index as i64,
972                    },
973                )
974            })
975            .collect::<Vec<_>>();
976        large.push((
977            "duplicate".to_owned(),
978            LanguageEntry {
979                text: "大字0".to_owned(),
980                weight: 0,
981            },
982        ));
983        deduplicate_reading_entries(&mut large);
984        assert_eq!(large.len(), SMALL_READING_DEDUP + 1);
985        assert_eq!(large[0].1.text, "大字0");
986    }
987
988    #[test]
989    fn opening_a_list_reserves_only_unique_reading_rows() {
990        let entries = [
991            ("r0", "字", 0),
992            ("r1", "字", 0),
993            ("r2", "字", 0),
994            ("r3", "字", 0),
995            ("r4", "字", 0),
996            ("r5", "字", 0),
997            ("r6", "字", 0),
998            ("r7", "字", 0),
999        ];
1000        let (_dir, mut scheme) = scheme_with(&entries);
1001        scheme.syllables = vec![syllable(
1002            "keys",
1003            &["r0", "r1", "r2", "r3", "r4", "r5", "r6", "r7"],
1004        )];
1005
1006        scheme.open_list().unwrap();
1007
1008        assert_eq!(scheme.list.len(), 1);
1009        assert!(scheme.list.capacity() < entries.len());
1010    }
1011
1012    #[test]
1013    fn reconversion_buffers_keep_allowed_readings_and_ambiguity_on_the_stack() {
1014        let syllables = [
1015            syllable("su3", &["ㄋㄧˇ"]),
1016            syllable("28c", &["ㄋㄧˇ", "ㄌㄧˇ"]),
1017        ];
1018
1019        let (positions, ambiguous) = reconversion_buffers(&syllables);
1020
1021        assert_eq!(positions[0], &["ㄋㄧˇ".to_owned()]);
1022        assert_eq!(positions[1], &["ㄋㄧˇ".to_owned(), "ㄌㄧˇ".to_owned()]);
1023        assert_eq!(&ambiguous[..2], &[false, true]);
1024    }
1025
1026    // 大千下每个位置只有一个读音,缓存键就是词库键;九键的多读音位置以 `|` 连接,钉住后只剩钉住的那个。
1027    #[test]
1028    fn cache_keys_describe_the_allowed_readings_in_order() {
1029        let mut syllables = [
1030            syllable("su3", &["ㄋㄧˇ"]),
1031            syllable("lc3", &["ㄏㄠˇ"]),
1032            syllable("28c", &["ㄋㄧˇ", "ㄌㄧˇ"]),
1033        ];
1034        let positions: Vec<&[String]> = syllables.iter().map(Syllable::allowed).collect();
1035        let mut description = String::new();
1036        describe(&positions[..2], &mut description);
1037        assert_eq!(description, "ㄋㄧˇ ㄏㄠˇ");
1038        describe(&positions, &mut description);
1039        assert_eq!(description, "ㄋㄧˇ ㄏㄠˇ ㄋㄧˇ|ㄌㄧˇ");
1040        syllables[2].locked = Some(1);
1041        let positions: Vec<&[String]> = syllables.iter().map(Syllable::allowed).collect();
1042        describe(&positions[1..], &mut description);
1043        assert_eq!(description, "ㄏㄠˇ ㄌㄧˇ");
1044    }
1045
1046    #[test]
1047    fn cache_key_description_needs_only_the_output_allocation() {
1048        let syllables = [
1049            syllable("su3", &["ㄋㄧˇ"]),
1050            syllable("lc3", &["ㄏㄠˇ"]),
1051            syllable("28c", &["ㄋㄧˇ", "ㄌㄧˇ"]),
1052        ];
1053        let positions: Vec<&[String]> = syllables.iter().map(Syllable::allowed).collect();
1054
1055        let (description, allocations) = crate::ime::personal_rerank::allocations::count(|| {
1056            let mut description = String::new();
1057            describe(&positions, &mut description);
1058            description
1059        });
1060
1061        assert_eq!(description, "ㄋㄧˇ ㄏㄠˇ ㄋㄧˇ|ㄌㄧˇ");
1062        assert_eq!(allocations, 1);
1063    }
1064
1065    #[test]
1066    fn reading_pending_suffix_buffer_allocates_once() {
1067        let (_dir, mut scheme) = scheme();
1068        scheme.conversion = (0..MAX_SYLLABLES)
1069            .map(|start| Span {
1070                start,
1071                end: start + 1,
1072                key: "ㄋㄧˇ".to_owned(),
1073                text: "合".to_owned(),
1074            })
1075            .collect();
1076        for digits in [false, true] {
1077            scheme.pending = if digits {
1078                PendingSyllable::default()
1079            } else {
1080                PendingSyllable {
1081                    initial: Some('ㄇ'),
1082                    medial: Some('ㄧ'),
1083                    rime: Some('ㄢ'),
1084                }
1085            };
1086            scheme.pending_digits = if digits { b"28".to_vec() } else { Vec::new() };
1087            let expected = format!(
1088                "{}{}",
1089                "合".repeat(MAX_SYLLABLES),
1090                if digits { "28" } else { "ㄇㄧㄢ" }
1091            );
1092            let mut reading = String::with_capacity(16);
1093            let ((), allocations) = crate::ime::personal_rerank::allocations::count(|| {
1094                scheme.reading_into(&mut reading);
1095            });
1096            assert_eq!(reading, expected);
1097            assert_eq!(allocations, 1, "待定符号和数字应纳入一次容量预留");
1098            let (reading, allocations) =
1099                crate::ime::personal_rerank::allocations::count(|| scheme.reading());
1100            assert_eq!(reading, expected);
1101            assert_eq!(allocations, 1);
1102        }
1103    }
1104
1105    #[test]
1106    fn editing_pending_digits_buffer_allocates_once() {
1107        let (_dir, mut scheme) = scheme();
1108        scheme.syllables = (0..MAX_SYLLABLES)
1109            .map(|_| syllable("28c", &["ㄋㄧˇ"]))
1110            .collect();
1111        scheme.pending_digits.extend_from_slice(b"28");
1112        let expected = format!("{}28", "28c".repeat(MAX_SYLLABLES));
1113        let mut keys = String::with_capacity(16);
1114        let ((), allocations) = crate::ime::personal_rerank::allocations::count(|| {
1115            scheme.editing_text_into(&mut keys);
1116        });
1117        assert_eq!(keys, expected);
1118        assert_eq!(allocations, 1, "待定数字应纳入一次按键容量预留");
1119        let (keys, allocations) =
1120            crate::ime::personal_rerank::allocations::count(|| scheme.editing_text());
1121        assert_eq!(keys, expected);
1122        assert_eq!(allocations, 1);
1123    }
1124
1125    #[test]
1126    fn editing_keys_growing_buffer_allocates_once() {
1127        let syllables = (0..MAX_SYLLABLES)
1128            .map(|_| syllable("su3", &["ㄋㄧˇ"]))
1129            .collect::<Vec<_>>();
1130        let pending = PendingSyllable {
1131            initial: Some('ㄇ'),
1132            medial: None,
1133            rime: Some('ㄚ'),
1134        };
1135        let expected = format!("{}a8", "su3".repeat(MAX_SYLLABLES));
1136        for capacity in [16, 0, 128] {
1137            let mut keys = String::with_capacity(capacity);
1138            if capacity > 0 {
1139                keys.push_str("舊值");
1140            }
1141            let old_capacity = keys.capacity();
1142            let ((), allocations) = crate::ime::personal_rerank::allocations::count(|| {
1143                append_editing_keys(&syllables, &pending, 0, &mut keys);
1144            });
1145            assert_eq!(keys, expected);
1146            assert_eq!(allocations, usize::from(old_capacity < expected.len()));
1147            let capacity = keys.capacity();
1148            let ((), allocations) = crate::ime::personal_rerank::allocations::count(|| {
1149                append_editing_keys(&[], &PendingSyllable::default(), 0, &mut keys);
1150            });
1151            assert!(keys.is_empty());
1152            assert_eq!(allocations, 0);
1153            assert_eq!(keys.capacity(), capacity);
1154        }
1155    }
1156
1157    #[test]
1158    fn converted_text_growing_buffer_allocates_once() {
1159        let spans = (0..MAX_SYLLABLES)
1160            .map(|start| Span {
1161                start,
1162                end: start + 1,
1163                key: "ㄋㄧˇ".to_owned(),
1164                text: "合".to_owned(),
1165            })
1166            .collect::<Vec<_>>();
1167        let expected = "合".repeat(MAX_SYLLABLES);
1168        for capacity in [16, 0, 128] {
1169            let mut text = String::with_capacity(capacity);
1170            if capacity > 0 {
1171                text.push_str("舊值");
1172            }
1173            let old_capacity = text.capacity();
1174            let ((), allocations) = crate::ime::personal_rerank::allocations::count(|| {
1175                append_converted_text(&spans, 0, &mut text);
1176            });
1177            assert_eq!(text, expected);
1178            assert_eq!(allocations, usize::from(old_capacity < expected.len()));
1179            let capacity = text.capacity();
1180            let ((), allocations) = crate::ime::personal_rerank::allocations::count(|| {
1181                append_converted_text(&[], 0, &mut text);
1182            });
1183            assert!(text.is_empty());
1184            assert_eq!(allocations, 0);
1185            assert_eq!(text.capacity(), capacity);
1186        }
1187    }
1188
1189    #[test]
1190    fn converted_text_appends_spans_in_order() {
1191        let spans = vec![
1192            Span {
1193                start: 0,
1194                end: 2,
1195                key: "ㄋㄧˇ ㄏㄠˇ".to_owned(),
1196                text: "你好".to_owned(),
1197            },
1198            Span {
1199                start: 2,
1200                end: 3,
1201                key: "ㄇㄚ˙".to_owned(),
1202                text: "嗎".to_owned(),
1203            },
1204        ];
1205
1206        assert_eq!(build_converted_text(&spans), "你好嗎");
1207    }
1208
1209    fn scheme() -> (tempfile::TempDir, ZhuyinScheme) {
1210        scheme_with(&ENTRIES)
1211    }
1212
1213    fn scheme_with(entries: &[(&str, &str, i64)]) -> (tempfile::TempDir, ZhuyinScheme) {
1214        let dir = tempfile::tempdir().unwrap();
1215        let path = dir.path().join("msime-zhuyin.db");
1216        let connection = Connection::open(&path).unwrap();
1217        connection.execute_batch(SCHEMA).unwrap();
1218        connection
1219            .execute(
1220                "INSERT INTO metadata VALUES (?1, ?2)",
1221                (METADATA_FORMAT_VERSION, FORMAT_VERSION.to_string()),
1222            )
1223            .unwrap();
1224        for &(key, text, weight) in entries {
1225            connection
1226                .execute(
1227                    "INSERT INTO entries VALUES (?1, ?2, ?3)",
1228                    (key, text, weight),
1229                )
1230                .unwrap();
1231            for syllable in key.split(' ') {
1232                connection
1233                    .execute("INSERT OR IGNORE INTO syllables VALUES (?1)", (syllable,))
1234                    .unwrap();
1235            }
1236        }
1237        drop(connection);
1238        let scheme = ZhuyinScheme::new(open_read_only(&path).unwrap());
1239        (dir, scheme)
1240    }
1241
1242    /// Types `keys` as characters and returns what each claimed.
1243    fn type_keys(scheme: &mut ZhuyinScheme, keys: &str) -> Vec<bool> {
1244        keys.bytes()
1245            .map(|byte| scheme.handle_key(ZhuyinKey::Char(byte)).unwrap())
1246            .collect()
1247    }
1248
1249    fn texts(scheme: &ZhuyinScheme) -> Vec<(&str, usize)> {
1250        scheme
1251            .candidates()
1252            .iter()
1253            .map(|candidate| (candidate.text.as_str(), candidate.start))
1254            .collect()
1255    }
1256
1257    #[test]
1258    fn idle_claims_phonetic_keys_but_not_tone_keys() {
1259        let (_dir, mut scheme) = scheme();
1260        assert_eq!(scheme.spelling_symbols(), IDLE_SYMBOLS);
1261        for byte in *b"3467 " {
1262            assert!(!scheme.handle_key(ZhuyinKey::Char(byte)).unwrap());
1263            assert!(!scheme.is_composing());
1264        }
1265        for key in [
1266            ZhuyinKey::Enter,
1267            ZhuyinKey::Escape,
1268            ZhuyinKey::Backspace,
1269            ZhuyinKey::OpenList,
1270        ] {
1271            assert!(!scheme.handle_key(key).unwrap());
1272        }
1273        // Uppercase letters and other ASCII are not Dachen keys.
1274        assert_eq!(type_keys(&mut scheme, "A!'"), [false, false, false]);
1275        for start in ["1", ",", "-", "a"] {
1276            scheme.reset();
1277            assert_eq!(type_keys(&mut scheme, start), [true]);
1278            assert!(scheme.is_composing());
1279            assert_eq!(scheme.spelling_symbols(), DACHEN_SYMBOLS);
1280        }
1281        assert_eq!(scheme.reading(), "ㄇ");
1282        assert_eq!(scheme.editing_text(), "a");
1283    }
1284
1285    #[test]
1286    fn slots_are_replaced_by_keys_of_the_same_kind() {
1287        let (_dir, mut scheme) = scheme();
1288        type_keys(&mut scheme, "sxuj");
1289        assert_eq!(scheme.reading(), "ㄌㄨ");
1290        assert_eq!(scheme.editing_text(), "xj");
1291        type_keys(&mut scheme, "sul3");
1292        assert_eq!(scheme.converted_text(), "");
1293        assert_eq!(scheme.reading(), "ㄋㄧㄠ");
1294        type_keys(&mut scheme, "c");
1295        type_keys(&mut scheme, "j");
1296        assert_eq!(scheme.reading(), "ㄏㄨㄠ");
1297    }
1298
1299    #[test]
1300    fn tone_keys_complete_only_known_syllables() {
1301        let (_dir, mut scheme) = scheme();
1302        // ㄋˇ is not in the inventory: the key is consumed and ㄋ stays pending.
1303        assert_eq!(type_keys(&mut scheme, "s3"), [true, true]);
1304        assert_eq!(scheme.reading(), "ㄋ");
1305        assert_eq!(scheme.editing_text(), "s");
1306        type_keys(&mut scheme, "u3");
1307        assert_eq!(scheme.reading(), "你");
1308        assert_eq!(scheme.editing_text(), "su3");
1309        // A tone digit with nothing pending is swallowed.
1310        assert_eq!(type_keys(&mut scheme, "4"), [true]);
1311        assert_eq!(scheme.reading(), "你");
1312        // Space is tone 1 and 7 the neutral tone.
1313        type_keys(&mut scheme, "a8 a87");
1314        assert_eq!(scheme.reading(), "你媽嗎");
1315        assert_eq!(scheme.editing_text(), "su3a8 a87");
1316        assert_eq!(scheme.take_committed(), "");
1317    }
1318
1319    #[test]
1320    fn conversion_prefers_words() {
1321        let (_dir, mut scheme) = scheme();
1322        type_keys(&mut scheme, "su3cl3");
1323        assert_eq!(scheme.converted_text(), "你好");
1324        type_keys(&mut scheme, "a8");
1325        assert_eq!(scheme.reading(), "你好ㄇㄚ");
1326        scheme.reset();
1327        type_keys(&mut scheme, "w96j0 ");
1328        assert_eq!(scheme.converted_text(), "臺灣");
1329    }
1330
1331    #[test]
1332    fn the_list_holds_suffix_spans_longest_first() {
1333        let (_dir, mut scheme) = scheme();
1334        type_keys(&mut scheme, "w96j0 ");
1335        assert!(scheme.handle_key(ZhuyinKey::OpenList).unwrap());
1336        assert!(scheme.list_open());
1337        assert_eq!(scheme.spelling_symbols(), LIST_OPEN_SYMBOLS);
1338        assert_eq!(
1339            texts(&scheme),
1340            [("臺灣", 0), ("台灣", 0), ("彎", 1), ("灣", 1)]
1341        );
1342        // Command 16 again closes it.
1343        assert!(scheme.handle_key(ZhuyinKey::OpenList).unwrap());
1344        assert!(!scheme.list_open());
1345        assert!(scheme.candidates().is_empty());
1346        // Space with nothing pending opens it too.
1347        assert!(scheme.handle_key(ZhuyinKey::Char(b' ')).unwrap());
1348        assert!(scheme.list_open());
1349    }
1350
1351    #[test]
1352    fn the_list_holds_every_entry_of_a_span() {
1353        // ㄧˋ has 215 single characters in the real data; the list must not cut off the light ones.
1354        let texts: Vec<String> = (0..215u32)
1355            .map(|index| char::from_u32(0x4E00 + index).unwrap().to_string())
1356            .collect();
1357        let entries: Vec<(&str, &str, i64)> = texts
1358            .iter()
1359            .zip((1..=215i64).rev())
1360            .map(|(text, weight)| ("ㄧˋ", text.as_str(), weight))
1361            .collect();
1362        let (_dir, mut scheme) = scheme_with(&entries);
1363        type_keys(&mut scheme, "u4");
1364        assert!(scheme.handle_key(ZhuyinKey::OpenList).unwrap());
1365        assert_eq!(scheme.candidates().len(), 215);
1366        assert_eq!(scheme.list.capacity(), scheme.list.len());
1367        assert_eq!(scheme.candidates()[214].text, texts[214]);
1368    }
1369
1370    #[test]
1371    fn reopening_the_list_reuses_its_row_storage() {
1372        let texts: Vec<String> = (0..215u32)
1373            .map(|index| char::from_u32(0x4E00 + index).unwrap().to_string())
1374            .collect();
1375        let mut entries: Vec<(&str, &str, i64)> = texts
1376            .iter()
1377            .zip((1..=215i64).rev())
1378            .map(|(text, weight)| ("ㄧˋ", text.as_str(), weight))
1379            .collect();
1380        entries.push(("ㄋㄧˇ", "你", 1));
1381        let (_dir, mut scheme) = scheme_with(&entries);
1382
1383        type_keys(&mut scheme, "u4");
1384        assert!(scheme.handle_key(ZhuyinKey::OpenList).unwrap());
1385        let capacity = scheme.list.capacity();
1386        assert_eq!(scheme.list.len(), 215);
1387        assert!(scheme.handle_key(ZhuyinKey::OpenList).unwrap());
1388
1389        scheme.reset();
1390        type_keys(&mut scheme, "su3");
1391        assert!(scheme.handle_key(ZhuyinKey::OpenList).unwrap());
1392        assert_eq!(scheme.list.len(), 1);
1393        assert!(scheme.list.capacity() >= capacity);
1394    }
1395
1396    #[test]
1397    fn select_pins_without_committing_and_pins_survive_reconversion() {
1398        let (_dir, mut scheme) = scheme();
1399        type_keys(&mut scheme, "su3 ");
1400        assert_eq!(texts(&scheme), [("你", 0), ("妳", 0), ("擬", 0)]);
1401        assert!(scheme.select(1).unwrap());
1402        assert!(!scheme.list_open());
1403        assert_eq!(scheme.take_committed(), "");
1404        assert_eq!(scheme.reading(), "妳");
1405        // The pinned 妳 holds even though 你好 is the better word.
1406        type_keys(&mut scheme, "cl3");
1407        assert_eq!(scheme.converted_text(), "妳好");
1408        // Choosing the longer span replaces the pin it overlaps.
1409        type_keys(&mut scheme, " ");
1410        assert_eq!(texts(&scheme), [("你好", 0), ("好", 1), ("郝", 1)]);
1411        assert!(scheme.select(2).unwrap());
1412        assert_eq!(scheme.converted_text(), "妳郝");
1413        type_keys(&mut scheme, " ");
1414        assert!(scheme.select(0).unwrap());
1415        assert_eq!(scheme.converted_text(), "你好");
1416        type_keys(&mut scheme, "w96");
1417        assert_eq!(scheme.converted_text(), "你好台");
1418        assert!(!scheme.select(0).unwrap());
1419        assert!(!scheme.list_open());
1420        // Backspace drops the last syllable; the pin on the others stays.
1421        scheme.handle_key(ZhuyinKey::Backspace).unwrap();
1422        assert_eq!(scheme.converted_text(), "你好");
1423        assert_eq!(scheme.take_committed(), "");
1424    }
1425
1426    #[test]
1427    fn selecting_a_list_row_moves_its_owned_fields_into_the_pin() {
1428        let (_dir, mut scheme) = scheme();
1429        type_keys(&mut scheme, "su3 ");
1430        let candidate = &scheme.list[1];
1431        let text = candidate.text.as_ptr();
1432        let key = candidate.key.as_ptr();
1433
1434        assert!(scheme.select(1).unwrap());
1435
1436        assert!(!scheme.list_open());
1437        assert_eq!(scheme.pins.len(), 1);
1438        assert_eq!(scheme.pins[0].text.as_ptr(), text);
1439        assert_eq!(scheme.pins[0].key.as_ptr(), key);
1440        assert_eq!(scheme.take_committed(), "");
1441    }
1442
1443    #[test]
1444    fn selecting_a_missing_list_row_keeps_the_list_open() {
1445        let (_dir, mut scheme) = scheme();
1446        type_keys(&mut scheme, "su3 ");
1447        let before = texts(&scheme)
1448            .into_iter()
1449            .map(|(text, start)| (text.to_owned(), start))
1450            .collect::<Vec<_>>();
1451
1452        assert!(!scheme.select(before.len()).unwrap());
1453
1454        assert!(scheme.list_open());
1455        assert_eq!(
1456            texts(&scheme)
1457                .into_iter()
1458                .map(|(text, start)| (text.to_owned(), start))
1459                .collect::<Vec<_>>(),
1460            before
1461        );
1462    }
1463
1464    #[test]
1465    fn list_keys_close_or_fall_through() {
1466        let (_dir, mut scheme) = scheme();
1467        type_keys(&mut scheme, "su3 ");
1468        assert!(scheme.list_open());
1469        // Selection digits and Space belong to the session while the list is open.
1470        assert_eq!(type_keys(&mut scheme, "1 3"), [false, false, false]);
1471        assert!(scheme.list_open());
1472        // 0 closes the list and types ㄢ.
1473        assert_eq!(type_keys(&mut scheme, "0"), [true]);
1474        assert!(!scheme.list_open());
1475        assert_eq!(scheme.reading(), "你ㄢ");
1476        type_keys(&mut scheme, " ");
1477        assert_eq!(scheme.reading(), "你安");
1478        // Esc and Backspace only close an open list.
1479        type_keys(&mut scheme, " ");
1480        assert!(scheme.handle_key(ZhuyinKey::Escape).unwrap());
1481        assert!(!scheme.list_open());
1482        assert_eq!(scheme.reading(), "你安");
1483        type_keys(&mut scheme, " ");
1484        assert!(scheme.handle_key(ZhuyinKey::Backspace).unwrap());
1485        assert_eq!(scheme.reading(), "你安");
1486        // A letter closes it and composes.
1487        type_keys(&mut scheme, " ");
1488        assert_eq!(type_keys(&mut scheme, "c"), [true]);
1489        assert!(!scheme.list_open());
1490        assert_eq!(scheme.reading(), "你安ㄏ");
1491    }
1492
1493    #[test]
1494    fn enter_commits_the_conversion_and_esc_clears() {
1495        let (_dir, mut scheme) = scheme();
1496        type_keys(&mut scheme, "su3cl3a");
1497        assert!(scheme.handle_key(ZhuyinKey::Enter).unwrap());
1498        assert_eq!(scheme.take_committed(), "你好");
1499        assert!(!scheme.is_composing());
1500
1501        type_keys(&mut scheme, "su3cl3");
1502        assert!(scheme.handle_key(ZhuyinKey::Escape).unwrap());
1503        assert!(!scheme.is_composing());
1504        assert_eq!(scheme.take_committed(), "");
1505
1506        type_keys(&mut scheme, "su3");
1507        assert_eq!(scheme.take_text(), "你");
1508        assert!(!scheme.is_composing());
1509    }
1510
1511    #[test]
1512    fn backspace_drops_a_symbol_then_a_syllable() {
1513        let (_dir, mut scheme) = scheme();
1514        type_keys(&mut scheme, "su3cl3ju");
1515        let mut shown = vec![scheme.reading()];
1516        while scheme.is_composing() {
1517            assert!(scheme.handle_key(ZhuyinKey::Backspace).unwrap());
1518            shown.push(scheme.reading());
1519        }
1520        assert_eq!(shown, ["你好ㄧ", "你好", "你", ""]);
1521        assert_eq!(scheme.spelling_symbols(), IDLE_SYMBOLS);
1522    }
1523
1524    #[test]
1525    fn shift_punctuation_commits_then_inserts_the_mark() {
1526        let (_dir, mut scheme) = scheme();
1527        type_keys(&mut scheme, "su3cl3a");
1528        assert_eq!(type_keys(&mut scheme, "<"), [true]);
1529        assert_eq!(scheme.take_committed(), "你好,");
1530        assert!(!scheme.is_composing());
1531        assert_eq!(type_keys(&mut scheme, "["), [true]);
1532        assert_eq!(scheme.take_committed(), "「");
1533        type_keys(&mut scheme, "su3 ");
1534        assert_eq!(type_keys(&mut scheme, "?"), [true]);
1535        assert_eq!(scheme.take_committed(), "你?");
1536        assert!(!scheme.list_open());
1537    }
1538
1539    #[test]
1540    fn the_21st_syllable_commits_the_leftmost_word() {
1541        let (_dir, mut scheme) = scheme();
1542        type_keys(&mut scheme, &"su3cl3".repeat(10));
1543        assert_eq!(scheme.converted_text(), "你好".repeat(10));
1544        assert_eq!(scheme.take_committed(), "");
1545        type_keys(&mut scheme, "su");
1546        assert_eq!(scheme.take_committed(), "");
1547        type_keys(&mut scheme, "3");
1548        assert_eq!(scheme.take_committed(), "你好");
1549        assert_eq!(scheme.converted_text(), format!("{}你", "你好".repeat(9)));
1550        assert_eq!(scheme.editing_text(), format!("{}su3", "su3cl3".repeat(9)));
1551        // The lookup cache keeps no key longer than the syllables that are left.
1552        let longest = scheme
1553            .best
1554            .entries
1555            .keys()
1556            .map(|key| key.split(' ').count())
1557            .max();
1558        assert_eq!(longest, Some(scheme.syllables.len()));
1559
1560        // A pin to the right of the shifted word moves with its syllables.
1561        scheme.reset();
1562        type_keys(&mut scheme, &"su3cl3".repeat(10));
1563        type_keys(&mut scheme, " ");
1564        assert!(scheme.select(2).unwrap());
1565        assert_eq!(scheme.converted_text(), format!("{}你郝", "你好".repeat(9)));
1566        type_keys(&mut scheme, "su3");
1567        assert_eq!(scheme.take_committed(), "你好");
1568        assert_eq!(
1569            scheme.converted_text(),
1570            format!("{}你郝你", "你好".repeat(8))
1571        );
1572    }
1573
1574    // ---- 注音九键 ----
1575
1576    /// 大千夹具加上与 ㄋㄧˇ 同为 `28` + ˇ 的 ㄌㄧˇ、ㄉㄧˇ,以及三个符号的 ㄏㄨㄚ。
1577    fn nine_key_scheme() -> (tempfile::TempDir, ZhuyinScheme) {
1578        let mut entries = ENTRIES.to_vec();
1579        entries.extend([
1580            ("ㄌㄧˇ", "李", 1200),
1581            ("ㄉㄧˇ", "底", 600),
1582            ("ㄏㄨㄚ", "花", 500),
1583        ]);
1584        let (dir, mut scheme) = scheme_with(&entries);
1585        scheme.set_nine_key(true);
1586        (dir, scheme)
1587    }
1588
1589    fn spellings(scheme: &ZhuyinScheme) -> Vec<&str> {
1590        scheme.spellings().iter().map(String::as_str).collect()
1591    }
1592
1593    #[test]
1594    fn nine_key_tone_keys_end_a_syllable_against_every_matching_reading() {
1595        let (_dir, mut scheme) = nine_key_scheme();
1596        assert_eq!(scheme.spelling_symbols(), nine_key::IDLE_SYMBOLS);
1597        assert_eq!(type_keys(&mut scheme, "28"), [true, true]);
1598        assert_eq!(scheme.reading(), "28");
1599        assert_eq!(scheme.spelling_symbols(), nine_key::SYMBOLS);
1600        assert!(scheme.spellings().is_empty());
1601        // ˇ 结束音节:ㄋㄧˇ、ㄌㄧˇ、ㄉㄧˇ 都在 28 上,单字里 李 最重。
1602        assert_eq!(type_keys(&mut scheme, "c"), [true]);
1603        assert_eq!(scheme.reading(), "李");
1604        assert_eq!(scheme.editing_text(), "28c");
1605        assert_eq!(spellings(&scheme), ["ㄌㄧˇ", "ㄋㄧˇ", "ㄉㄧˇ"]);
1606        // 你好 够重,过得了九键的权重下限,双音节词按长度平方胜出;当前用的读音排到最前,其余按单字权重。
1607        type_keys(&mut scheme, "39c");
1608        assert_eq!(scheme.converted_text(), "你好");
1609        assert_eq!(scheme.editing_text(), "28c39c");
1610        assert_eq!(spellings(&scheme), ["ㄋㄧˇ", "ㄌㄧˇ", "ㄉㄧˇ"]);
1611        assert_eq!(scheme.conversion[0].key, "ㄋㄧˇ ㄏㄠˇ");
1612        assert_eq!(scheme.take_committed(), "");
1613    }
1614
1615    #[test]
1616    fn spelling_refresh_failure_clears_previous_choices() {
1617        for count in [3, SMALL_SPELLING_RANK + 1] {
1618            let (dir, mut scheme) = nine_key_scheme();
1619            type_keys(&mut scheme, "28c");
1620            assert!(!scheme.spellings().is_empty());
1621            if count > SMALL_SPELLING_RANK {
1622                scheme.syllables[0].readings = (0..count)
1623                    .map(|index| format!("測試讀音{index:03}"))
1624                    .collect::<Vec<_>>()
1625                    .into();
1626            }
1627            scheme.best.clear();
1628            Connection::open(dir.path().join("msime-zhuyin.db"))
1629                .unwrap()
1630                .execute_batch("DROP TABLE entries")
1631                .unwrap();
1632
1633            assert!(scheme.refresh_spellings().is_err());
1634            assert!(scheme.spellings().is_empty());
1635            assert_eq!(scheme.spelling_target, None);
1636        }
1637    }
1638
1639    #[test]
1640    fn spelling_refresh_handles_changing_choice_counts() {
1641        let (_dir, mut scheme) = nine_key_scheme();
1642        type_keys(&mut scheme, "28c");
1643        for count in [SMALL_SPELLING_RANK + 1, 2, 4] {
1644            let readings = (0..count)
1645                .map(|index| format!("測試讀音{index:03}"))
1646                .collect::<Vec<_>>();
1647            scheme.syllables[0].readings = readings.clone().into();
1648            scheme.refresh_spellings().unwrap();
1649            assert_eq!(scheme.spellings(), readings);
1650            assert_eq!(scheme.spelling_target, Some(0));
1651        }
1652        scheme.syllables[0].locked = Some(0);
1653        scheme.refresh_spellings().unwrap();
1654        assert!(scheme.spellings().is_empty());
1655        assert_eq!(scheme.spelling_target, None);
1656        scheme.syllables[0].locked = None;
1657        scheme.refresh_spellings().unwrap();
1658        assert_eq!(scheme.spellings(), &*scheme.syllables[0].readings);
1659    }
1660
1661    #[test]
1662    fn cached_best_preserves_rows_misses_and_query_errors() {
1663        let (dir, scheme) = nine_key_scheme();
1664        let mut best = BestCache::default();
1665        let readings = ["ㄋㄧˇ".to_owned(), "ㄌㄧˇ".to_owned()];
1666        let missing = ["測試缺失讀音".to_owned()];
1667        let uncached = ["ㄉㄧˇ".to_owned()];
1668        let check = |entry: Option<&(String, LanguageEntry)>| {
1669            let (key, entry) = entry.unwrap();
1670            assert_eq!(key, "ㄌㄧˇ");
1671            assert_eq!(entry.text, "李");
1672            assert_eq!(entry.weight, 1200);
1673            (key.as_ptr(), entry.text.as_ptr())
1674        };
1675        let pointers = cached_best(&scheme.dictionary, &mut best, &[&readings], check).unwrap();
1676        assert!(
1677            cached_best(&scheme.dictionary, &mut best, &[&missing], |entry| entry
1678                .is_none())
1679            .unwrap()
1680        );
1681        assert_eq!(best.entries.len(), 2);
1682        Connection::open(dir.path().join("msime-zhuyin.db"))
1683            .unwrap()
1684            .execute_batch("DROP TABLE entries")
1685            .unwrap();
1686
1687        let cached_pointers =
1688            cached_best(&scheme.dictionary, &mut best, &[&readings], check).unwrap();
1689        assert_eq!(cached_pointers, pointers);
1690        assert!(
1691            cached_best(&scheme.dictionary, &mut best, &[&missing], |entry| entry
1692                .is_none())
1693            .unwrap()
1694        );
1695        for _ in 0..2 {
1696            assert!(
1697                cached_best(&scheme.dictionary, &mut best, &[&uncached], |_| {
1698                    panic!("词库查询失败不应消费词条")
1699                })
1700                .is_err()
1701            );
1702        }
1703        assert_eq!(best.entries.len(), 2);
1704        let capacity = best.description.capacity();
1705        best.clear();
1706        assert!(best.entries.is_empty());
1707        assert!(best.description.is_empty());
1708        assert_eq!(best.description.capacity(), capacity);
1709        assert!(cached_best(&scheme.dictionary, &mut best, &[&readings], |_| ()).is_err());
1710    }
1711
1712    #[test]
1713    fn rejected_cached_single_rows_do_not_become_fallbacks() {
1714        let (_dir, mut scheme) = scheme_with(&[
1715            ("ㄋㄧˇ", "甲", 0),
1716            ("ㄏㄠˇ", "乙", 0),
1717            ("ㄇㄚ", "丙", 0),
1718            ("ㄊㄞˊ", "丁", -20),
1719            ("ㄏㄠˇ ㄇㄚ", "乙丙", 0),
1720            ("ㄇㄚ ㄊㄞˊ", "丙丁", -10),
1721        ]);
1722        scheme.syllables = ["ㄋㄧˇ", "ㄏㄠˇ", "ㄇㄚ", "ㄊㄞˊ"]
1723            .into_iter()
1724            .map(|reading| syllable("合成按鍵", &[reading]))
1725            .collect();
1726
1727        scheme.reconvert().unwrap();
1728
1729        assert_eq!(
1730            scheme
1731                .conversion
1732                .iter()
1733                .map(|span| span.text.as_str())
1734                .collect::<Vec<_>>(),
1735            ["甲", "乙", "丙丁"]
1736        );
1737        assert_eq!(scheme.conversion[2].key, "ㄇㄚ ㄊㄞˊ");
1738    }
1739
1740    #[test]
1741    fn warmed_dense_conversion_only_clones_improving_paths() {
1742        let entries = (1..=MAX_SYLLABLES)
1743            .map(|len| (vec!["ㄋㄧˇ"; len].join(" "), "合".repeat(len), len as i64))
1744            .collect::<Vec<_>>();
1745        let rows = entries
1746            .iter()
1747            .map(|(key, text, weight)| (key.as_str(), text.as_str(), *weight))
1748            .collect::<Vec<_>>();
1749        let (dir, mut scheme) = scheme_with(&rows);
1750        scheme.syllables = (0..MAX_SYLLABLES)
1751            .map(|_| syllable("su3", &["ㄋㄧˇ"]))
1752            .collect();
1753        scheme.reconvert().unwrap();
1754        Connection::open(dir.path().join("msime-zhuyin.db"))
1755            .unwrap()
1756            .execute_batch("DROP TABLE entries")
1757            .unwrap();
1758
1759        let ((), allocations) = crate::ime::personal_rerank::allocations::count(|| {
1760            scheme.reconvert().unwrap();
1761        });
1762
1763        assert_eq!(scheme.conversion.len(), 1);
1764        assert_eq!(scheme.conversion[0].key, entries.last().unwrap().0);
1765        assert_eq!(scheme.conversion[0].text, entries.last().unwrap().1);
1766        assert_eq!(
1767            allocations,
1768            MAX_SYLLABLES * 2 + 1,
1769            "只应复制首次改善路径的二十组词条字符串,并分配返回向量"
1770        );
1771    }
1772
1773    #[test]
1774    fn warmed_conversion_cache_queries_do_not_allocate_descriptions() {
1775        for readings in [
1776            vec!["ㄋㄧˇ".to_owned()],
1777            vec!["ㄋㄧˇ".to_owned(), "ㄌㄧˇ".to_owned()],
1778        ] {
1779            let (dir, scheme) = nine_key_scheme();
1780            let expected = if readings.len() == 1 {
1781                ("ㄋㄧˇ", "你", 1000)
1782            } else {
1783                ("ㄌㄧˇ", "李", 1200)
1784            };
1785            let positions = [readings.as_slice(); MAX_SYLLABLES];
1786            let mut best = BestCache::default();
1787            for _ in 0..2 {
1788                for start in 0..positions.len() {
1789                    for end in start + 1..=positions.len() {
1790                        cached_best(
1791                            &scheme.dictionary,
1792                            &mut best,
1793                            &positions[start..end],
1794                            |_| (),
1795                        )
1796                        .unwrap();
1797                    }
1798                }
1799            }
1800            Connection::open(dir.path().join("msime-zhuyin.db"))
1801                .unwrap()
1802                .execute_batch("DROP TABLE entries")
1803                .unwrap();
1804
1805            let ((), allocations) = crate::ime::personal_rerank::allocations::count(|| {
1806                for start in 0..positions.len() {
1807                    for end in start + 1..=positions.len() {
1808                        cached_best(
1809                            &scheme.dictionary,
1810                            &mut best,
1811                            &positions[start..end],
1812                            |entry| {
1813                                if end == start + 1 {
1814                                    let (key, entry) = entry.unwrap();
1815                                    assert_eq!(key, expected.0);
1816                                    assert_eq!(entry.text, expected.1);
1817                                    assert_eq!(entry.weight, expected.2);
1818                                } else {
1819                                    assert!(entry.is_none());
1820                                }
1821                            },
1822                        )
1823                        .unwrap();
1824                    }
1825                }
1826            });
1827            assert_eq!(allocations, 0, "热缓存查询不应分配临时描述键");
1828        }
1829    }
1830
1831    #[test]
1832    fn cached_single_reading_weights_keep_hits_misses_and_errors() {
1833        let (dir, scheme) = nine_key_scheme();
1834        let mut best = BestCache::default();
1835        let reading = ["ㄌㄧˇ".to_owned()];
1836        let missing = ["測試缺失讀音".to_owned()];
1837        assert_eq!(
1838            cached_best_weight(&scheme.dictionary, &mut best, &[&reading]).unwrap(),
1839            Some(1200)
1840        );
1841        assert_eq!(
1842            cached_best_weight(&scheme.dictionary, &mut best, &[&missing]).unwrap(),
1843            None
1844        );
1845        Connection::open(dir.path().join("msime-zhuyin.db"))
1846            .unwrap()
1847            .execute_batch("DROP TABLE entries")
1848            .unwrap();
1849
1850        let ((), allocations) = crate::ime::personal_rerank::allocations::count(|| {
1851            assert_eq!(
1852                cached_best_weight(&scheme.dictionary, &mut best, &[&reading]).unwrap(),
1853                Some(1200)
1854            );
1855            assert_eq!(
1856                cached_best_weight(&scheme.dictionary, &mut best, &[&missing]).unwrap(),
1857                None
1858            );
1859        });
1860        assert_eq!(allocations, 0);
1861        let readings = ["ㄌㄧˇ".to_owned(), "ㄋㄧˇ".to_owned()];
1862        assert!(cached_best_weight(&scheme.dictionary, &mut best, &[&readings]).is_err());
1863        assert!(cached_best_weight(&scheme.dictionary, &mut best, &[&reading, &reading]).is_err());
1864        assert_eq!(best.entries.len(), 2);
1865    }
1866
1867    #[test]
1868    fn refreshing_short_spelling_choices_does_not_allocate_ranking_state() {
1869        let (_dir, mut scheme) = nine_key_scheme();
1870        type_keys(&mut scheme, "28c");
1871        assert_eq!(scheme.spellings.len(), 3);
1872        scheme.spellings.reserve(scheme.syllables[0].readings.len());
1873
1874        let ((), allocations) = crate::ime::personal_rerank::allocations::count(|| {
1875            scheme.refresh_spellings().unwrap();
1876        });
1877
1878        assert_eq!(
1879            allocations, 0,
1880            "读取已有单读音词频仍分配了 {allocations} 次"
1881        );
1882        assert_eq!(spellings(&scheme), ["ㄌㄧˇ", "ㄋㄧˇ", "ㄉㄧˇ"]);
1883    }
1884
1885    /// 今天 + 去 的读音和冷僻的 監聽器 落在同样的数字串上:ㄐㄧㄣ/ㄐㄧㄢ 都是 480,ㄊㄧㄢ/ㄊㄧㄥ 都是 280,ㄑㄩˋ/ㄑㄧˋ 都是 48ˋ。
1886    const AMBIGUOUS_WORD_ENTRIES: [(&str, &str, i64); 8] = [
1887        ("ㄐㄧㄣ", "今", 33812),
1888        ("ㄊㄧㄢ", "天", 31487),
1889        ("ㄑㄩˋ", "去", 28394),
1890        ("ㄐㄧㄣ ㄊㄧㄢ", "今天", 25469),
1891        ("ㄐㄧㄢ", "監", 100),
1892        ("ㄊㄧㄥ", "聽", 3000),
1893        ("ㄑㄧˋ", "器", 900),
1894        ("ㄐㄧㄢ ㄊㄧㄥ ㄑㄧˋ", "監聽器", 9),
1895    ];
1896
1897    #[test]
1898    fn nine_key_light_long_words_do_not_beat_heavy_shorter_ones() {
1899        let (_dir, mut scheme) = scheme_with(&AMBIGUOUS_WORD_ENTRIES);
1900        scheme.set_nine_key(true);
1901        type_keys(&mut scheme, "480 280 48v");
1902        // 只比长度的话三音节的 監聽器(9)会压过 今天(25469)+ 去(28394)。
1903        assert_eq!(scheme.converted_text(), "今天去");
1904        // 钉住 ㄐㄧㄢ 之后今天不再匹配,这个位置的单字只剩 監(100),監聽器 过得了下限。
1905        let jian = scheme
1906            .spellings()
1907            .iter()
1908            .position(|reading| reading == "ㄐㄧㄢ")
1909            .unwrap();
1910        assert!(scheme.choose_spelling(jian).unwrap());
1911        assert_eq!(scheme.converted_text(), "監聽器");
1912
1913        // 大千下每个位置只有一个读音,照旧按长度:打出 監聽器 的读音就得到 監聽器。
1914        let (_dir, mut scheme) = scheme_with(&AMBIGUOUS_WORD_ENTRIES);
1915        type_keys(&mut scheme, "ru0 wu/ fu4");
1916        assert_eq!(scheme.converted_text(), "監聽器");
1917    }
1918
1919    #[test]
1920    fn nine_key_confirming_the_readings_in_use_keeps_the_text() {
1921        // 適之 和 是 + 之 用的是同一组读音,权重却低得过不了下限;5 上 ㄓˋ/ㄕˋ、ㄓ/ㄔ 两两同键,两个位置打出来都有歧义。
1922        let (_dir, mut scheme) = scheme_with(&[
1923            ("ㄕˋ", "是", 160_000),
1924            ("ㄓ", "之", 120_000),
1925            ("ㄕˋ ㄓ", "適之", 1),
1926            ("ㄓˋ", "至", 500),
1927            ("ㄔ", "吃", 800),
1928        ]);
1929        scheme.set_nine_key(true);
1930        type_keys(&mut scheme, "5v5 ");
1931        assert_eq!(scheme.converted_text(), "是之");
1932        // 读音条第一个就是当前用的读音;逐个确认,歧义仍按打字时算,適之 不会因为两个位置都钉住而冒出来。
1933        let mut confirmed = 0;
1934        while !scheme.spellings().is_empty() {
1935            assert!(scheme.choose_spelling(0).unwrap());
1936            assert_eq!(scheme.converted_text(), "是之");
1937            confirmed += 1;
1938        }
1939        assert_eq!(confirmed, 2);
1940    }
1941
1942    #[test]
1943    fn nine_key_space_and_z_are_the_first_tone() {
1944        let (_dir, mut scheme) = nine_key_scheme();
1945        type_keys(&mut scheme, "29x80 ");
1946        assert_eq!(scheme.converted_text(), "臺灣");
1947        assert_eq!(scheme.editing_text(), "29x80 ");
1948        scheme.reset();
1949        assert_eq!(type_keys(&mut scheme, "29x80z"), [true; 6]);
1950        assert_eq!(scheme.converted_text(), "臺灣");
1951        scheme.reset();
1952        type_keys(&mut scheme, "17b17 ");
1953        assert_eq!(scheme.converted_text(), "嗎媽");
1954        assert!(scheme.nine_key());
1955    }
1956
1957    #[test]
1958    fn nine_key_digit_edits_reuse_pending_storage() {
1959        let (_dir, mut scheme) = nine_key_scheme();
1960        scheme.nine_key_index().unwrap();
1961        scheme.pending_digits.reserve(nine_key::MAX_DIGITS);
1962        let pointer = scheme.pending_digits.as_ptr();
1963
1964        let ((), allocations) = crate::ime::personal_rerank::allocations::count(|| {
1965            assert!(scheme.handle_key(ZhuyinKey::Char(b'2')).unwrap());
1966            assert!(scheme.handle_key(ZhuyinKey::Char(b'8')).unwrap());
1967            assert!(scheme.handle_key(ZhuyinKey::Char(b'2')).unwrap());
1968            assert_eq!(scheme.pending_digits, b"28");
1969            assert!(scheme.handle_key(ZhuyinKey::Backspace).unwrap());
1970            assert!(scheme.handle_key(ZhuyinKey::Char(b'9')).unwrap());
1971        });
1972
1973        assert_eq!(allocations, 0);
1974        assert_eq!(scheme.pending_digits.as_ptr(), pointer);
1975        assert_eq!(scheme.pending_digits, b"29");
1976        assert_eq!(scheme.reading(), "29");
1977    }
1978
1979    #[test]
1980    fn nine_key_completed_syllables_reuse_pending_digit_storage() {
1981        let (_dir, mut scheme) = nine_key_scheme();
1982        type_keys(&mut scheme, "28");
1983        let pointer = scheme.pending_digits.as_ptr();
1984        let capacity = scheme.pending_digits.capacity();
1985        assert!(scheme.handle_key(ZhuyinKey::Char(b'c')).unwrap());
1986        assert!(scheme.pending_digits.is_empty());
1987
1988        let (claimed, allocations) = crate::ime::personal_rerank::allocations::count(|| {
1989            scheme.handle_key(ZhuyinKey::Char(b'2')).unwrap()
1990        });
1991        assert!(claimed);
1992        assert_eq!(allocations, 0);
1993        assert_eq!(scheme.pending_digits.as_ptr(), pointer);
1994        assert_eq!(scheme.pending_digits.capacity(), capacity);
1995        assert_eq!(scheme.pending_digits, b"2");
1996        assert_eq!(scheme.reading(), "李2");
1997    }
1998
1999    #[test]
2000    fn nine_key_tone_index_failure_restores_pending_digits() {
2001        let (dir, mut scheme) = nine_key_scheme();
2002        scheme.pending_digits.extend_from_slice(b"28");
2003        let pointer = scheme.pending_digits.as_ptr();
2004        Connection::open(dir.path().join("msime-zhuyin.db"))
2005            .unwrap()
2006            .execute_batch("DROP TABLE syllables")
2007            .unwrap();
2008
2009        assert!(scheme.handle_key(ZhuyinKey::Char(b'c')).is_err());
2010        assert_eq!(scheme.pending_digits, b"28");
2011        assert_eq!(scheme.pending_digits.as_ptr(), pointer);
2012        assert!(scheme.syllables.is_empty());
2013        assert!(scheme.is_composing());
2014        assert_eq!(scheme.reading(), "28");
2015    }
2016
2017    #[test]
2018    fn nine_key_index_failure_restores_pending_digits() {
2019        let (dir, mut scheme) = nine_key_scheme();
2020        scheme.pending_digits.extend_from_slice(b"2");
2021        let pointer = scheme.pending_digits.as_ptr();
2022        Connection::open(dir.path().join("msime-zhuyin.db"))
2023            .unwrap()
2024            .execute_batch("DROP TABLE syllables")
2025            .unwrap();
2026
2027        assert!(scheme.handle_key(ZhuyinKey::Char(b'8')).is_err());
2028
2029        assert_eq!(scheme.pending_digits, b"2");
2030        assert_eq!(scheme.pending_digits.as_ptr(), pointer);
2031    }
2032
2033    #[test]
2034    fn nine_key_unknown_codes_stay_pending_and_extra_digits_are_swallowed() {
2035        let (_dir, mut scheme) = nine_key_scheme();
2036        // 28 加 ˋ 没有合法音节:声调键被认领,数字留着。
2037        assert_eq!(type_keys(&mut scheme, "28v"), [true, true, true]);
2038        assert_eq!(scheme.reading(), "28");
2039        // 282 不是任何音节的前缀,第三个数字被吞掉。
2040        assert_eq!(type_keys(&mut scheme, "2"), [true]);
2041        assert_eq!(scheme.reading(), "28");
2042        scheme.reset();
2043        // 三个符号已满,第四个数字被吞掉。
2044        type_keys(&mut scheme, "3871");
2045        assert_eq!(scheme.reading(), "387");
2046        type_keys(&mut scheme, " ");
2047        assert_eq!(scheme.reading(), "花");
2048        // 数字从空闲开始组字;夹具里没有 ㄐㄑㄒ 的音节,4 打不出任何音节,只被吞掉。
2049        scheme.reset();
2050        assert_eq!(type_keys(&mut scheme, "4"), [true]);
2051        assert!(!scheme.is_composing());
2052    }
2053
2054    #[test]
2055    fn nine_key_claims_tone_letters_and_digits_but_no_other_letters() {
2056        let (_dir, mut scheme) = nine_key_scheme();
2057        // 空闲时声调字母、空格和其他字母都不认领:实体键盘上打 z 要上屏 z。
2058        assert_eq!(type_keys(&mut scheme, "zxcvb"), [false; 5]);
2059        assert!(!scheme.is_composing());
2060        assert_eq!(type_keys(&mut scheme, " sa"), [false, false, false]);
2061        // 组字中:大千键和其他字母都不认领。
2062        type_keys(&mut scheme, "28c");
2063        assert_eq!(type_keys(&mut scheme, "sau,"), [false; 4]);
2064        assert_eq!(scheme.reading(), "李");
2065        // 没有数字时声调字母被吞掉。
2066        assert_eq!(type_keys(&mut scheme, "x"), [true]);
2067        assert_eq!(scheme.reading(), "李");
2068        // Shift 标点照常提交转换文字再加标点。
2069        type_keys(&mut scheme, "39c2");
2070        assert_eq!(type_keys(&mut scheme, "?"), [true]);
2071        assert_eq!(scheme.take_committed(), "你好?");
2072        assert!(!scheme.is_composing());
2073    }
2074
2075    #[test]
2076    fn nine_key_choosing_a_spelling_pins_the_reading() {
2077        let (_dir, mut scheme) = nine_key_scheme();
2078        type_keys(&mut scheme, "28c39c");
2079        assert!(!scheme.choose_spelling(3).unwrap());
2080        assert!(scheme.choose_spelling(1).unwrap());
2081        assert_eq!(scheme.converted_text(), "李好");
2082        assert_eq!(scheme.take_committed(), "");
2083        // 39c 只有一个读音,没有下一个目标。
2084        assert!(scheme.spellings().is_empty());
2085        assert!(!scheme.choose_spelling(0).unwrap());
2086        // 确认与当前转换相同的读音也会钉住,目标右移。
2087        type_keys(&mut scheme, "28c");
2088        assert_eq!(spellings(&scheme), ["ㄌㄧˇ", "ㄋㄧˇ", "ㄉㄧˇ"]);
2089        assert!(scheme.choose_spelling(0).unwrap());
2090        assert_eq!(scheme.converted_text(), "李好李");
2091        assert!(scheme.spellings().is_empty());
2092        assert_eq!(scheme.syllables[2].allowed(), ["ㄌㄧˇ"]);
2093        // Enter 提交转换文字,丢掉还没结束的数字。
2094        type_keys(&mut scheme, "2");
2095        assert_eq!(scheme.reading(), "李好李2");
2096        assert!(scheme.handle_key(ZhuyinKey::Enter).unwrap());
2097        assert_eq!(scheme.take_committed(), "李好李");
2098        assert!(!scheme.is_composing());
2099    }
2100
2101    #[test]
2102    fn nine_key_list_rows_carry_keys_and_hide_the_spellings() {
2103        let (_dir, mut scheme) = nine_key_scheme();
2104        type_keys(&mut scheme, "28c39c");
2105        // 没有数字时空格打开列表。
2106        assert_eq!(type_keys(&mut scheme, " "), [true]);
2107        assert!(scheme.list_open());
2108        assert_eq!(scheme.spelling_symbols(), nine_key::LIST_OPEN_SYMBOLS);
2109        assert!(scheme.spellings().is_empty());
2110        assert!(!scheme.choose_spelling(0).unwrap());
2111        let rows: Vec<(&str, usize, &str)> = scheme
2112            .candidates()
2113            .iter()
2114            .map(|row| (row.text.as_str(), row.start, row.key.as_str()))
2115            .collect();
2116        assert_eq!(
2117            rows,
2118            [
2119                ("你好", 0, "ㄋㄧˇ ㄏㄠˇ"),
2120                ("好", 1, "ㄏㄠˇ"),
2121                ("郝", 1, "ㄏㄠˇ")
2122            ]
2123        );
2124        // 列表打开时空格留给运行时选行;数字关闭列表继续拼写。
2125        assert_eq!(type_keys(&mut scheme, " "), [false]);
2126        assert_eq!(type_keys(&mut scheme, "2"), [true]);
2127        assert!(!scheme.list_open());
2128        assert_eq!(scheme.reading(), "你好2");
2129        // 声调字母同样关闭列表。
2130        scheme.handle_key(ZhuyinKey::Backspace).unwrap();
2131        type_keys(&mut scheme, " ");
2132        assert!(scheme.list_open());
2133        assert_eq!(type_keys(&mut scheme, "c"), [true]);
2134        assert!(!scheme.list_open());
2135        assert_eq!(spellings(&scheme), ["ㄋㄧˇ", "ㄌㄧˇ", "ㄉㄧˇ"]);
2136    }
2137
2138    #[test]
2139    fn nine_key_list_selection_resolves_the_syllables_it_covers() {
2140        let (_dir, mut scheme) = nine_key_scheme();
2141        type_keys(&mut scheme, "28c ");
2142        let rows: Vec<(&str, &str)> = scheme
2143            .candidates()
2144            .iter()
2145            .map(|row| (row.text.as_str(), row.key.as_str()))
2146            .collect();
2147        assert_eq!(
2148            rows,
2149            [
2150                ("李", "ㄌㄧˇ"),
2151                ("你", "ㄋㄧˇ"),
2152                ("底", "ㄉㄧˇ"),
2153                ("妳", "ㄋㄧˇ"),
2154                ("擬", "ㄋㄧˇ")
2155            ]
2156        );
2157        assert!(scheme.select(1).unwrap());
2158        assert_eq!(scheme.converted_text(), "你");
2159        assert_eq!(scheme.pins[0].key, "ㄋㄧˇ");
2160        // 被 pin 覆盖的音节算已解析,不再是钉读音的目标,也不写 `locked`。
2161        assert!(scheme.spellings().is_empty());
2162        assert_eq!(scheme.syllables[0].locked, None);
2163        type_keys(&mut scheme, "39c");
2164        assert_eq!(scheme.converted_text(), "你好");
2165        assert!(scheme.spellings().is_empty());
2166        // 删掉 pin 结尾的音节时 pin 一起删掉,音节恢复歧义。
2167        scheme.handle_key(ZhuyinKey::Backspace).unwrap();
2168        scheme.handle_key(ZhuyinKey::Backspace).unwrap();
2169        assert!(!scheme.is_composing());
2170        type_keys(&mut scheme, "28c ");
2171        assert!(scheme.select(1).unwrap());
2172        type_keys(&mut scheme, "39c");
2173        scheme.handle_key(ZhuyinKey::Backspace).unwrap();
2174        assert_eq!(scheme.converted_text(), "你");
2175        assert!(scheme.spellings().is_empty());
2176        // 再删 39c 之后的那一步删掉的是 28c 和以它结尾的 pin。
2177        scheme.handle_key(ZhuyinKey::Backspace).unwrap();
2178        type_keys(&mut scheme, "28c");
2179        assert_eq!(scheme.converted_text(), "李");
2180        assert_eq!(spellings(&scheme), ["ㄌㄧˇ", "ㄋㄧˇ", "ㄉㄧˇ"]);
2181    }
2182
2183    #[test]
2184    fn current_spelling_lookup_does_not_allocate() {
2185        let (_dir, mut scheme) = nine_key_scheme();
2186        type_keys(&mut scheme, "28c");
2187        let conversion = &scheme.conversion;
2188        let (current, allocations) =
2189            crate::ime::personal_rerank::allocations::count(|| current_spelling(conversion, 0));
2190
2191        assert_eq!(allocations, 0);
2192        assert_eq!(current, Some("ㄌㄧˇ"));
2193    }
2194
2195    #[test]
2196    fn nine_key_backspace_drops_a_digit_then_a_syllable_with_its_lock() {
2197        let (_dir, mut scheme) = nine_key_scheme();
2198        type_keys(&mut scheme, "28c39c");
2199        assert!(scheme.choose_spelling(1).unwrap());
2200        type_keys(&mut scheme, "28");
2201        let mut shown = vec![(scheme.reading(), scheme.spelling_symbols())];
2202        while scheme.is_composing() {
2203            assert!(scheme.handle_key(ZhuyinKey::Backspace).unwrap());
2204            shown.push((scheme.reading(), scheme.spelling_symbols()));
2205        }
2206        assert_eq!(
2207            shown,
2208            [
2209                ("李好28".to_owned(), nine_key::SYMBOLS),
2210                ("李好2".to_owned(), nine_key::SYMBOLS),
2211                ("李好".to_owned(), nine_key::SYMBOLS),
2212                ("李".to_owned(), nine_key::SYMBOLS),
2213                (String::new(), nine_key::IDLE_SYMBOLS),
2214            ]
2215        );
2216        // 钉住的读音随音节一起删掉。
2217        type_keys(&mut scheme, "28c39c");
2218        assert_eq!(scheme.converted_text(), "你好");
2219        assert_eq!(spellings(&scheme), ["ㄋㄧˇ", "ㄌㄧˇ", "ㄉㄧˇ"]);
2220        // 列表打开时 Backspace 只关闭列表。
2221        type_keys(&mut scheme, " ");
2222        assert!(scheme.handle_key(ZhuyinKey::Backspace).unwrap());
2223        assert!(!scheme.list_open());
2224        assert_eq!(scheme.converted_text(), "你好");
2225    }
2226
2227    #[test]
2228    fn nine_key_locks_stay_aligned_across_an_auto_shift() {
2229        let (_dir, mut scheme) = nine_key_scheme();
2230        type_keys(&mut scheme, &"28c39c".repeat(10));
2231        assert_eq!(scheme.converted_text(), "你好".repeat(10));
2232        // 先确认第一个音节的读音,目标移到第三个音节,再把它钉成 ㄌㄧˇ。
2233        assert!(scheme.choose_spelling(0).unwrap());
2234        assert_eq!(scheme.spelling_target, Some(2));
2235        assert!(scheme.choose_spelling(1).unwrap());
2236        assert_eq!(scheme.spelling_target, Some(4));
2237        assert_eq!(
2238            scheme.converted_text(),
2239            format!("你好李好{}", "你好".repeat(8))
2240        );
2241        type_keys(&mut scheme, "28c");
2242        assert_eq!(scheme.take_committed(), "你好");
2243        assert_eq!(scheme.syllables.len(), MAX_SYLLABLES - 1);
2244        assert_eq!(scheme.syllables[0].allowed(), ["ㄌㄧˇ"]);
2245        assert_eq!(
2246            scheme.converted_text(),
2247            format!("李好{}李", "你好".repeat(8))
2248        );
2249        assert_eq!(scheme.spelling_target, Some(2));
2250    }
2251
2252    #[test]
2253    fn nine_key_mode_switches_clear_the_composition_and_reset_keeps_the_mode() {
2254        let (_dir, mut scheme) = nine_key_scheme();
2255        type_keys(&mut scheme, "28c3");
2256        scheme.reset();
2257        assert!(scheme.nine_key());
2258        assert!(!scheme.is_composing());
2259        type_keys(&mut scheme, "28c3");
2260        scheme.set_nine_key(false);
2261        assert!(!scheme.is_composing());
2262        assert!(!scheme.nine_key());
2263        // 回到大千:数字按大千键处理,没有候选读音。
2264        assert_eq!(scheme.spelling_symbols(), IDLE_SYMBOLS);
2265        type_keys(&mut scheme, "su3");
2266        assert_eq!(scheme.reading(), "你");
2267        assert!(scheme.spellings().is_empty());
2268        assert!(!scheme.choose_spelling(0).unwrap());
2269        scheme.set_nine_key(true);
2270        assert!(!scheme.is_composing());
2271        assert_eq!(scheme.spelling_symbols(), nine_key::IDLE_SYMBOLS);
2272    }
2273}