1use 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
17pub const LIST_OPEN_SYMBOLS: &str = "0,./;-";
19const SMALL_READING_DEDUP: usize = 32;
20
21#[derive(Debug, Clone, Copy, PartialEq, Eq)]
23pub enum ZhuyinKey {
24 Char(u8),
26 OpenList,
28 Enter,
29 Escape,
30 Backspace,
31}
32
33#[derive(Debug, Clone, PartialEq, Eq)]
35pub struct ListCandidate {
36 pub text: String,
37 pub start: usize,
38 pub key: String,
40}
41
42#[derive(Debug, Clone, PartialEq, Eq)]
44struct Syllable {
45 keys: String,
47 readings: Arc<[String]>,
49 locked: Option<usize>,
51}
52
53impl Syllable {
54 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 pins: Vec<Span>,
81 pending: PendingSyllable,
82 conversion: Vec<Span>,
84 list_open: bool,
85 list: Vec<ListCandidate>,
87 committed: String,
89 best: BestCache,
91 nine_key: bool,
93 nine_key_index: Option<NineKeyIndex>,
95 pending_digits: Vec<u8>,
97 spellings: Vec<String>,
99 spelling_target: Option<usize>,
101}
102
103impl ZhuyinScheme {
104 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 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 pub fn into_dictionary(self) -> LanguageDictionary {
136 self.dictionary
137 }
138
139 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 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 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 pub fn take_text(&mut self) -> String {
218 let text = self.converted_text();
219 self.clear_composition();
220 text
221 }
222
223 pub fn take_committed(&mut self) -> String {
225 std::mem::take(&mut self.committed)
226 }
227
228 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 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 pub fn preedit(&self) -> String {
275 self.reading()
276 }
277
278 pub fn build_request(&self) -> QueryRequest {
280 let mut request = QueryRequest::default();
281 self.build_request_into(&mut request);
282 request
283 }
284
285 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 pub fn candidates(&self) -> &[ListCandidate] {
308 &self.list
309 }
310
311 pub fn spellings(&self) -> &[String] {
313 if self.list_open {
314 return &[];
315 }
316 &self.spellings
317 }
318
319 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 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 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 if byte == b' ' {
389 self.open_list()?;
390 }
391 return Ok(true);
392 };
393 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 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 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 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 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 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 let dictionary = &self.dictionary;
547 let best = &mut self.best;
548 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 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
744const AMBIGUOUS_WORD_FLOOR: i64 = 1000;
748
749const SMALL_SPELLING_RANK: usize = 64;
751
752fn 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
770fn 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
786fn 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
807fn 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 #[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 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 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 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 assert_eq!(type_keys(&mut scheme, "4"), [true]);
1311 assert_eq!(scheme.reading(), "你");
1312 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 assert!(scheme.handle_key(ZhuyinKey::OpenList).unwrap());
1344 assert!(!scheme.list_open());
1345 assert!(scheme.candidates().is_empty());
1346 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 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 type_keys(&mut scheme, "cl3");
1407 assert_eq!(scheme.converted_text(), "妳好");
1408 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 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 assert_eq!(type_keys(&mut scheme, "1 3"), [false, false, false]);
1471 assert!(scheme.list_open());
1472 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 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 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 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 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 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 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 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 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 assert_eq!(scheme.converted_text(), "今天去");
1904 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 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 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 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 assert_eq!(type_keys(&mut scheme, "28v"), [true, true, true]);
2038 assert_eq!(scheme.reading(), "28");
2039 assert_eq!(type_keys(&mut scheme, "2"), [true]);
2041 assert_eq!(scheme.reading(), "28");
2042 scheme.reset();
2043 type_keys(&mut scheme, "3871");
2045 assert_eq!(scheme.reading(), "387");
2046 type_keys(&mut scheme, " ");
2047 assert_eq!(scheme.reading(), "花");
2048 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 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 type_keys(&mut scheme, "28c");
2063 assert_eq!(type_keys(&mut scheme, "sau,"), [false; 4]);
2064 assert_eq!(scheme.reading(), "李");
2065 assert_eq!(type_keys(&mut scheme, "x"), [true]);
2067 assert_eq!(scheme.reading(), "李");
2068 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 assert!(scheme.spellings().is_empty());
2085 assert!(!scheme.choose_spelling(0).unwrap());
2086 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 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 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 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 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 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 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 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 type_keys(&mut scheme, "28c39c");
2218 assert_eq!(scheme.converted_text(), "你好");
2219 assert_eq!(spellings(&scheme), ["ㄋㄧˇ", "ㄌㄧˇ", "ㄉㄧˇ"]);
2220 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 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 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}