msime-engine 0.2.0

Input method engine for Chinese and other languages: quanpin, shuangpin, nine-key, wubi, zhuyin, Cantonese, Japanese, Korean and more, ranked over a word lattice with typo correction and learning from what the user picks. The engine of 水杉输入法 (Metasequoia IME).
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
//! The public `Session` (`include/metasequoia/session.h`, core-session.md §4): an input session plus the nine-key session, with the routing rules between them. One host serialises calls to its session; distinct sessions may run concurrently.
//!
//! The setters the product only sets at construction (`switch_scheme`, `set_helpcode_schema`, `set_helpcode_enabled`, `set_wubi_mixed_pinyin`, `set_personal_context_enabled`) and `candidate_key` stay public because the golden scenarios drive them. Learning undo (`set_learning_undo_enabled`, `forget_recent_commits`) is dropped.

mod candidates;
mod caret_edit;
mod chain;
mod clock;
mod commit;
mod composition;
mod conversion;
mod editing;
mod glide;
mod helpcode_entry;
mod input;
mod learning;
mod literal;
mod online;
pub mod options;
mod plugin_commands;
mod prediction;
mod raw_commit;
#[cfg(test)]
mod tests;

use std::path::PathBuf;
use std::sync::Arc;

use crate::diagnostics;
use crate::error::{EngineError, Result};
use crate::local::database::LocalDatabaseLease;
use crate::nine_key::{KeyGrid, NineKeySession};
use crate::pinyin::glide::{GlideKeyboard, GlidePoint};
use crate::touch::{KeyGeometry, KeyTouch, TouchTuning};
use crate::types::{
    CandidateEdge, CandidateSource, Command, CommandTableEntry, CommandTranslationQuery,
    FrequencyAdjustmentOptions, KeyResult, LocalInputMode, MentionEntry, OnlineQuery,
    PluginCommand, PluginCommandQuery, QuickPhraseEntry, SchemeType, WordItem,
};
use crate::user_dictionary::journal::{release_thread_journal, JournalUse};
use crate::user_dictionary::ngram_store::{flush_journal, note_settled};

use crate::types::FirstPageRows;
pub use clock::{Clock, LocalClock};
use input::InputSession;
pub use options::{SessionOptions, SessionSnapshot};

pub struct Session {
    input: InputSession,
    nine_key: NineKeySession,
    /// 触屏的组码网格:九键的数字从 `character` 进来,14 键的字母从 `grid_key` 进来;`None` 是不用网格。
    key_grid: Option<KeyGrid>,
    shuangpin_preedit_uses_raw: bool,
    /// `SessionOptions::learn_typed_spellings_on_enter`。
    learn_typed_spellings_on_enter: bool,
    prediction: prediction::Prediction,
    /// See [`Self::candidate_revision`].
    revision: u64,
    /// `msime_user.db` of this session's paths, written once more as the session ends.
    journal: PathBuf,
    journal_use: JournalUse,
    /// The generation and resource directories the local-mode queries read; dropped after the rest of the session, so the last session to go closes their cached connections.
    _local_databases: LocalDatabaseLease,
}

// A session may move to another thread between calls (a host's worker pool, the C ABI); calls into one session are still serialised by its owner, so it does not need to be `Sync`. The journal connections it opens are cached per thread, and its end closes the idle ones on every thread (`JournalUse`), so a session used on several threads leaves none open.
const _: fn() = || {
    fn moves_between_threads<T: Send>() {}
    moves_between_threads::<Session>();
};

impl Session {
    /// Validates the paths and every option (`INVALID_SESSION_OPTIONS`, `UNKNOWN_HELPCODE_SCHEMA`), loads the helpcode table and applies the options in the reference order (session.cpp:11-30, 57-66).
    pub fn new(options: SessionOptions) -> Result<Session> {
        options.paths.validate()?;
        crate::user_dictionary::generation::check_read_only_layout(&options.paths)?;
        let input = InputSession::new(&options)?;
        let mut nine_key = NineKeySession::new(
            &options.paths,
            options.learning,
            options.frequency,
            options.fuzzy_pinyin,
            options.english,
            options.enabled_schemes.contains(SchemeType::Quanpin),
            options.single_character_only,
        );
        nine_key.set_stroke_dictionary(options.stroke_dictionary.clone());
        nine_key.set_mixed_expressive(options.expressive);
        nine_key.set_mixed_candidates_enabled(options.mixed_candidates);
        nine_key.set_inline_date_time(
            options
                .local_modes
                .date_time_inline
                .then(|| Arc::clone(&input.clock.local)),
        );
        // 九键的整句与 26 键遵守同一组句子联想设置(#6059)。
        nine_key.set_sentence_options(
            options.sentence_association.clone(),
            options.sentence_alternatives,
        );
        nine_key.set_first_page_rows(FirstPageRows::new(options.first_page_rows));
        nine_key.set_rescoring_context(&options.rescoring_context);
        // 九键选中的词记进同一个个人上下文模型,开关与 26 键相同(#6185)。
        nine_key.set_personal_context_enabled(options.personal_context);
        let journal = options.paths.user(crate::assets::USER_JOURNAL);
        Ok(Session {
            input,
            nine_key,
            key_grid: None,
            shuangpin_preedit_uses_raw: options.shuangpin_preedit_uses_raw,
            learn_typed_spellings_on_enter: options.learn_typed_spellings_on_enter,
            prediction: prediction::Prediction::new(
                options.next_word_prediction,
                options.paths.dictionary(crate::assets::MAIN_DICTIONARY),
            ),
            revision: 0,
            journal_use: JournalUse::new(journal.clone()),
            journal,
            _local_databases: LocalDatabaseLease::new([
                options.paths.dictionaries.clone(),
                options.paths.resources.clone(),
            ]),
        })
    }

    /// Changes whenever a call may have changed the candidate list, so a host that read the candidates under one revision can tell that an index it holds no longer means the same row. Read-only calls never change it; neither do the setters that only affect later keys (clock, key touch and geometry, touch tuning, punctuation options, rescoring context). Starts at 0 and only grows.
    pub fn candidate_revision(&self) -> u64 {
        self.revision
    }

    fn candidates_may_change(&mut self) {
        self.revision = self.revision.wrapping_add(1);
    }

    /// 会话建立时定下的双拼方案(内置的表,或校验过的用户表)。
    pub(crate) fn shuangpin_layout(&self) -> &'static crate::shuangpin::ShuangpinProfile {
        self.input.profile
    }

    /// 会话现在过滤候选用的辅助码表(`SessionOptions::helpcode_table`、按 `helpcode_schema` 读的表,或之后 `set_helpcode_table` 换上的表);辅助码关着时照样留着。门面给自己合成的整句补注释时读它,不另读一份。
    pub(crate) fn helpcode_table(&self) -> Option<&crate::helpcode::HelpcodeKeymap> {
        self.input.helpcode_keymap.as_deref()
    }

    /// Replace the steady and local clocks, so tests can cross the 3 s / 8 s / 10 s personal-learning windows and the date/time mode can be pinned.
    pub fn set_clock(&mut self, clock: Clock) {
        if self.input.local_mode_options.date_time_inline {
            self.nine_key
                .set_inline_date_time(Some(Arc::clone(&clock.local)));
        }
        self.input.clock = clock;
    }

    /// One ASCII character; `shift_only` is a bare Shift+letter (local mode entry). 全拼下九键开启且没有别的组字时,数字 2-9 交给九宫格会话;九键双拼交的是方案用到的数字(可以有 1 和 0);注音九键的数字由注音编辑器自己处理。待用的触点([`Self::set_key_touch`])只交给 26 键组字,九宫格的数字用不到它,一律在这一键后作废。
    pub fn character(&mut self, value: u8, shift_only: bool) -> KeyResult {
        self.candidates_may_change();
        self.prediction.clear();
        let touch = self.input.pending_touch.take();
        // English is a mode rather than a scheme, so the grid stays available in it: the same digits spell words instead of syllables. A local mode still takes the keys, and the scheme underneath must be quanpin: 拼音九宫格只认得全拼音节,注音九键走注音编辑器。九键双拼没有英文九键,专用英文里数字照常是数字。
        let grid_digit = match self.key_grid {
            Some(KeyGrid::NineKey) => (b'2'..=b'9').contains(&value),
            Some(grid @ KeyGrid::NineKeyShuangpin(_)) => {
                !self.input.dedicated_english && grid.is_code(value)
            }
            _ => false,
        };
        if grid_digit
            && self.input.scheme() == SchemeType::Quanpin
            && self.input.local_mode == LocalInputMode::None
            && self.input.preedit().is_empty()
        {
            let result = self.nine_key.character(value);
            return self.after_nine_key(result);
        }
        if self.nine_key.active() {
            // `'` splits the grid's syllables where the user is typing; any other key is still the host's to handle.
            if value == b'\'' {
                let result = self.nine_key.character(value);
                return self.after_nine_key(result);
            }
            return KeyResult::unhandled();
        }
        self.input.pending_touch = touch;
        self.input.handle_character(value, shift_only)
    }

    /// 下一次 `character` 的按下点(触屏宿主经 `msime_client_touch_character` 送来)。只有那一键是 26 键双拼的小写字母或 `;` 时才交给方案;其余会改动组字或改道按键的入口(命令、选词、以词定字、结束组字、钉住和删除候选、移光标、九宫格、网格键、滑行、标点、换方案、换网格或几何、展开首字母候选、重置上下文)一开头就丢掉它,所以改道到别处的按键不会把触点留给后面的字母。只改配置的 setter(辅助码表、云候选开关、重排上下文等)不碰它。
    pub fn set_key_touch(&mut self, touch: Option<KeyTouch>) {
        self.input.pending_touch = touch;
    }

    /// 宿主注册或清除的键位几何,见 [`crate::touch::KeyGeometry`]。
    pub fn set_key_geometry(&mut self, geometry: Option<Arc<KeyGeometry>>) {
        // 待用触点是按旧几何量出来的,交给新几何解释没有意义。
        self.input.pending_touch = None;
        self.input.engine.set_key_geometry(geometry);
    }

    /// 按触点计价的参数;只有 Rust 侧(评测示例)调用,不经 FFI。
    pub fn set_touch_tuning(&mut self, tuning: TouchTuning) {
        self.input.engine.set_touch_tuning(tuning);
    }

    /// 滑过字母键的一笔:它最可能拼出的全拼字母写到光标处,与已有组字之间用 `'` 隔开。不是全拼、在本地模式或专用英文里、九键数字正在组字,以及没有串跟得上这一笔时,不处理。
    pub fn glide(&mut self, keyboard: &GlideKeyboard, points: &[GlidePoint]) -> KeyResult {
        self.candidates_may_change();
        self.prediction.clear();
        self.input.pending_touch = None;
        if self.nine_key.active() {
            return KeyResult::unhandled();
        }
        self.input.glide(keyboard, points)
    }

    /// 上一次 [`Session::set_key_grid`](或 [`Session::set_nine_key_enabled`])设下的组码网格,`None` 是不用网格;新会话是 `None`。网格开着时硬件键盘的字母照样走 26 键,要知道这次组字是不是在网格里打的,看 [`Session::grid_composing`]。
    pub fn key_grid(&self) -> Option<KeyGrid> {
        self.key_grid
    }

    /// 字母网格正在组字:九宫格会话(九键、14 键、18 键)在组字,快照、命令和上屏都来自它;或者五笔 14 键在组字,组码不是确定的字母。两种情况下的原样上屏都不是用户逐个打出的字母。
    pub fn grid_composing(&self) -> bool {
        self.nine_key.active() || self.input.wubi_grid_composing()
    }

    /// `set_key_grid` 的九键开关:开是九键,关是不用网格。
    pub fn set_nine_key_enabled(&mut self, enabled: bool) {
        self.set_key_grid(enabled.then_some(KeyGrid::NineKey));
    }

    /// 换触屏的组码网格,先丢掉网格里的组字,所以在换键盘布局、没有组字时调用。英文九键和注音九键只在九键下打开(注音组字会被丢掉):14 键的英文模式画全键盘,注音没有 14 键。
    pub fn set_key_grid(&mut self, grid: Option<KeyGrid>) {
        self.candidates_may_change();
        self.prediction.clear();
        self.input.pending_touch = None;
        self.input.helpcode_entry = None;
        self.nine_key.command(Command::Cancel);
        if let Some(grid) = grid {
            self.nine_key.set_grid(grid);
        }
        self.key_grid = grid;
        let nine_key = grid == Some(KeyGrid::NineKey);
        self.nine_key
            .set_english_only(nine_key && self.input.dedicated_english);
        self.input.set_zhuyin_nine_key(nine_key);
    }

    /// 字母网格的一键:小写字母由 `KeyGrid::code_of` 归成整组,十八键双拼另收大写 ASCII 标记精确字母。标记保留在组码里,不复用硬件键盘的 `character`。全拼和双拼分别使用自己的分组表,五笔在 14 键下按组展开码表(`wubi::grid`);本地模式、专用英文或已有 26 键组字时不处理,状态不变。
    pub fn grid_key(&mut self, letter: u8) -> KeyResult {
        self.candidates_may_change();
        self.prediction.clear();
        self.input.pending_touch = None;
        let Some(
            grid @ (KeyGrid::FourteenKey
            | KeyGrid::EighteenKey
            | KeyGrid::FourteenKeyShuangpin(_)
            | KeyGrid::EighteenKeyShuangpin(_)),
        ) = self.key_grid
        else {
            return KeyResult::unhandled();
        };
        let Some(code) = grid.code_of(letter) else {
            return KeyResult::unhandled();
        };
        // 五笔 14 键:键面与全拼 14 键相同,组码交给五笔方案按码表展开,不经九宫格会话。
        if grid == KeyGrid::FourteenKey && self.input.scheme() == SchemeType::Wubi {
            let Some(representative) = crate::wubi::grid::representative(code) else {
                return KeyResult::unhandled();
            };
            return self.input.handle_wubi_grid_key(representative);
        }
        let supported = match grid {
            KeyGrid::FourteenKey | KeyGrid::EighteenKey => {
                self.input.scheme() == SchemeType::Quanpin
            }
            KeyGrid::FourteenKeyShuangpin(_) | KeyGrid::EighteenKeyShuangpin(_) => {
                self.input.scheme() == SchemeType::Shuangpin
            }
            _ => false,
        };
        if !supported
            || self.input.local_mode != LocalInputMode::None
            || self.input.dedicated_english
            || !(self.input.preedit().is_empty() || self.nine_key.active())
        {
            return KeyResult::unhandled();
        }
        let result = self.nine_key.character(code);
        self.after_nine_key(result)
    }

    /// 九宫格组码已经到上限时,这一键能不能先选掉首选、再接着打(`NineKeySession::limit_pick_ready`)。`grid_key` 为真时 `value` 是 `grid_key` 收到的归组字母,否则是 `character` 收到的九键或九键双拼数字;两条路由各自只认自己的网格。
    pub fn grid_limit_pick_ready(&self, value: u8, grid_key: bool) -> bool {
        let code = match self.key_grid {
            Some(
                grid @ (KeyGrid::FourteenKey
                | KeyGrid::EighteenKey
                | KeyGrid::FourteenKeyShuangpin(_)
                | KeyGrid::EighteenKeyShuangpin(_)),
            ) if grid_key => grid.code_of(value),
            Some(KeyGrid::NineKey | KeyGrid::NineKeyShuangpin(_)) if !grid_key => Some(value),
            _ => None,
        };
        code.is_some_and(|code| self.nine_key.limit_pick_ready(code))
    }

    /// 九宫格会话活跃时选它的拼写;否则在注音里钉目标音节的读音。
    pub fn choose_nine_key_spelling(&mut self, index: usize) -> KeyResult {
        self.candidates_may_change();
        self.input.pending_touch = None;
        if !self.nine_key.active() && self.input.zhuyin_rules_apply() {
            return self.input.choose_zhuyin_spelling(index);
        }
        let result = self.nine_key.choose_spelling(index);
        self.after_nine_key(result)
    }

    /// 九宫格组字时按单字、按笔画筛选候选(`NineKeySession::set_filter`);没有九宫格组字时不处理。
    pub fn set_nine_key_filter(&mut self, single_character: bool, strokes: &str) -> KeyResult {
        self.candidates_may_change();
        self.input.pending_touch = None;
        if !self.nine_key.active() {
            return KeyResult::unhandled();
        }
        self.nine_key.set_filter(single_character, strokes)
    }

    /// 一条编辑命令。`CommitRaw` 原样上屏,专用英文里打的词学进英文词库(`SessionOptions::learn_english_on_enter`),`learn_typed_spellings_on_enter` 打开时还学临时模式与拼不成音节的拼音;`CommitRawWithoutLearning` 原样上屏、不学英文词。规则见 `session/raw_commit.rs`。
    pub fn command(&mut self, command: Command) -> KeyResult {
        match command {
            Command::CommitRawWithoutLearning => self.commit_raw_without_learning(),
            Command::CommitRaw if self.learn_typed_spellings_on_enter => {
                self.commit_raw_learning_typed_spellings()
            }
            _ => self.dispatch_command(command),
        }
    }

    /// 命令交给九宫格或组字,没有回车学词的策略。`CommitRawWithoutLearning` 不会到这里。
    fn dispatch_command(&mut self, command: Command) -> KeyResult {
        self.candidates_may_change();
        self.input.pending_touch = None;
        if !self.prediction.rows.is_empty() {
            self.prediction.clear();
            return if command == Command::Cancel {
                KeyResult::handled()
            } else {
                self.input.handle_command(command)
            };
        }
        let selected = (self.prediction.enabled && command == Command::CommitCandidate)
            .then(|| self.snapshot().candidates.first().cloned())
            .flatten();
        if self.nine_key.active() {
            // 九宫格没有整句改字,改字的光标键就是它的光标键。
            let command = match command {
                Command::ConversionLeft => Command::MoveLeft,
                Command::ConversionRight => Command::MoveRight,
                other => other,
            };
            let result = self.nine_key.command(command);
            let result = self.after_nine_key(result);
            self.predict_after_commit(selected, &result);
            return result;
        }
        let result = self.input.handle_command(command);
        self.predict_after_commit(selected, &result);
        result
    }

    /// 整句改字的左移,进入改字时从第 `index` 个候选(宿主高亮的那一行)开始;已在改字里时与 `command(ConversionLeft)` 相同。九宫格和进不了改字时按 `MoveLeft` 处理。
    pub fn conversion_left_from(&mut self, index: usize) -> KeyResult {
        self.candidates_may_change();
        self.prediction.clear();
        self.input.pending_touch = None;
        if self.nine_key.active() {
            return self.command(Command::MoveLeft);
        }
        self.input.conversion_left_from(index)
    }

    /// `1`..`9` 选择组字候选;空闲联想不接管数字键。
    pub fn candidate_key(&mut self, value: u8) -> KeyResult {
        self.candidates_may_change();
        self.prediction.clear();
        self.input.pending_touch = None;
        if self.nine_key.active() {
            if !(b'1'..=b'9').contains(&value) {
                return KeyResult::unhandled();
            }
            let result = self.nine_key.select(usize::from(value - b'1'));
            return self.after_nine_key(result);
        }
        self.input.handle_candidate_key(value)
    }

    /// Finishes the composition and appends the Chinese mark; unhandled when Chinese punctuation is off or locked to ASCII, or the key has no mark.
    pub fn punctuation(&mut self, value: u8) -> KeyResult {
        self.candidates_may_change();
        self.prediction.clear();
        self.input.pending_touch = None;
        if !self.nine_key.active() {
            return self.input.handle_punctuation(value);
        }
        let mut result = self.input.handle_punctuation(value);
        if !result.handled {
            return result;
        }
        // The grid's text goes before the mark the input session translated.
        let composition = self.nine_key.finish(0);
        if composition.commit.is_some() {
            self.input.reset_commit_context();
        }
        let mut text = composition.commit.unwrap_or_default();
        text.push_str(result.commit.as_deref().unwrap_or_default());
        result.commit = Some(text);
        result
    }

    /// Live; keeps the composition, caret and pairing state.
    pub fn set_chinese_punctuation_enabled(&mut self, enabled: bool) {
        self.input.chinese_punctuation_enabled = enabled;
    }

    /// `INVALID_PUNCTUATION_LOCK` outside 0..=2.
    pub fn set_punctuation_lock(&mut self, lock: i32) -> Result<()> {
        if !(0..=2).contains(&lock) {
            return Err(EngineError::invalid(diagnostics::INVALID_PUNCTUATION_LOCK));
        }
        self.input.punctuation_lock = lock;
        Ok(())
    }

    pub fn set_paired_punctuation_enabled(&mut self, enabled: bool) {
        self.input.punctuation.set_paired_enabled(enabled);
    }

    /// 换引号和方头括号的样式;组字、光标和交替状态都不动。
    pub fn set_punctuation_marks(&mut self, marks: crate::punctuation::PunctuationMarks) {
        self.input.punctuation.set_marks(marks);
    }

    /// The host emitted the closing half of a paired mark itself.
    pub fn balance_paired_punctuation_after_auto_close(&mut self, opening: u8) {
        self.input.punctuation.balance_after_auto_close(opening);
    }

    pub fn select(&mut self, index: usize) -> KeyResult {
        self.candidates_may_change();
        self.input.pending_touch = None;
        if !self.prediction.rows.is_empty() {
            let Some((row, word, pinyin)) = self.prediction.selected(index) else {
                return KeyResult::unhandled();
            };
            let diagnostic = self.input.record_personal_context(Some(&row), false, false);
            self.show_prediction(&word, &pinyin, &row.word);
            return KeyResult::committed(row.word.to_string()).with_diagnostic(diagnostic);
        }
        let selected = self
            .prediction
            .enabled
            .then(|| self.snapshot().candidates.get(index).cloned())
            .flatten();
        if self.nine_key.active() {
            let result = self.nine_key.select(index);
            let result = self.after_nine_key(result);
            self.predict_after_commit(selected, &result);
            return result;
        }
        let result = self.input.select_candidate(index);
        self.predict_after_commit(selected, &result);
        result
    }

    /// Commit the first or last Han character of a candidate and drop the rest of the composition.
    pub fn select_edge(&mut self, index: usize, edge: CandidateEdge) -> KeyResult {
        self.candidates_may_change();
        self.input.pending_touch = None;
        if !self.prediction.rows.is_empty() {
            return KeyResult::unhandled();
        }
        if self.nine_key.active() {
            return KeyResult::unhandled();
        }
        self.input.select_candidate_edge(index, edge)
    }

    /// Promote a dictionary candidate without committing; unsupported rows are unhandled, persistence failures carry a diagnostic.
    pub fn pin(&mut self, index: usize) -> KeyResult {
        self.candidates_may_change();
        self.input.pending_touch = None;
        if !self.prediction.rows.is_empty() {
            return KeyResult::unhandled();
        }
        if self.nine_key.active() {
            return self.nine_key.pin(index);
        }
        self.input.pin_candidate(index)
    }

    /// Remove a dictionary phrase without committing; single-character non-English rows are protected.
    pub fn remove(&mut self, index: usize) -> KeyResult {
        self.candidates_may_change();
        self.input.pending_touch = None;
        if !self.prediction.rows.is_empty() {
            return KeyResult::unhandled();
        }
        if self.nine_key.active() {
            return self.nine_key.remove(index);
        }
        self.input.remove_candidate(index)
    }

    /// Fix a dictionary candidate to slot 1..=5 in this input context; unhandled for any other slot.
    pub fn fix_position(&mut self, index: usize, position: i32) -> KeyResult {
        self.candidates_may_change();
        self.input.pending_touch = None;
        if !self.prediction.rows.is_empty() {
            return KeyResult::unhandled();
        }
        if !(1..=5).contains(&position) {
            return KeyResult::unhandled();
        }
        if self.nine_key.active() {
            return self.nine_key.set_position(index, position);
        }
        self.input.set_candidate_position(index, position)
    }

    pub fn clear_position(&mut self, index: usize) -> KeyResult {
        self.candidates_may_change();
        self.input.pending_touch = None;
        if !self.prediction.rows.is_empty() {
            return KeyResult::unhandled();
        }
        if self.nine_key.active() {
            return self.nine_key.set_position(index, 0);
        }
        self.input.set_candidate_position(index, 0)
    }

    /// 从宿主高亮的候选开始结束整段组字(没有高亮时为 0),剩余片段使用各自首选;索引无效时提交原文。普通拼音中的大写片段由字面候选保留。
    pub fn finish(&mut self, first_index: usize) -> KeyResult {
        self.candidates_may_change();
        self.prediction.clear();
        self.input.pending_touch = None;
        if self.nine_key.active() {
            let result = self.nine_key.finish(first_index);
            return self.after_nine_key(result);
        }
        self.input.finish_composition(first_index)
    }

    /// Discards the composition. Fails, staying in the current scheme with the composition untouched, when the new scheme's dictionary cannot be opened (Cantonese without a usable `msime-cantonese.db`, Stroke without a usable `msime-stroke.db`: `LANGUAGE_DICTIONARY_UNAVAILABLE`, `LANGUAGE_DICTIONARY_VERSION_UNSUPPORTED`).
    pub fn switch_scheme(&mut self, scheme: SchemeType) -> Result<()> {
        self.candidates_may_change();
        self.input.pending_touch = None;
        self.input.switch_scheme(scheme)?;
        self.prediction.clear();
        self.nine_key.command(Command::Cancel);
        Ok(())
    }

    pub fn is_supported_helpcode_schema(schema: &str) -> bool {
        crate::helpcode::is_supported_helpcode_schema(schema)
    }

    /// False for an unsupported schema or a table that cannot be loaded.
    pub fn set_helpcode_schema(&mut self, schema: &str) -> bool {
        self.candidates_may_change();
        self.input.set_helpcode_schema(schema)
    }

    /// 换上宿主给的辅助码表,替换当前的表;全拼和双拼都用它。
    pub fn set_helpcode_table(&mut self, table: crate::helpcode::SharedKeymap) {
        self.candidates_may_change();
        self.input.set_helpcode_table(table);
    }

    /// Quanpin and shuangpin together, as `SessionOptions::helpcode` does.
    pub fn set_helpcode_enabled(&mut self, enabled: bool) {
        self.candidates_may_change();
        self.input.set_helpcode_enabled(enabled);
    }

    /// 只有模式改变时才取消组字;重复设置保留当前输入。
    pub fn set_dedicated_english(&mut self, enabled: bool) {
        self.candidates_may_change();
        self.input.pending_touch = None;
        if self.input.dedicated_english == enabled {
            return;
        }
        self.prediction.clear();
        self.nine_key.command(Command::Cancel);
        self.input.set_dedicated_english_mode(enabled);
        // 九键在英文里用数字拼单词,在中文里拼音节,模式或网格改变时都要同步。
        self.nine_key
            .set_english_only(self.key_grid == Some(KeyGrid::NineKey) && enabled);
    }

    /// 混输候选的读音能继续时,运行时应保留组字,不提前顶字上屏。
    pub fn wubi_pinyin_continues(&self, value: u8, shift: bool) -> bool {
        !shift
            && !self.input.dedicated_english
            && self.input.local_mode == LocalInputMode::None
            && self.input.engine.wubi_pinyin_continues(value)
    }

    pub fn set_wubi_mixed_pinyin(&mut self, enabled: bool) {
        self.candidates_may_change();
        self.input.set_wubi_mixed_pinyin(enabled);
    }

    /// 五笔拼音混输是否打开。
    pub fn wubi_mixed_pinyin(&self) -> bool {
        self.input.engine.wubi_input_options().mixed_pinyin
    }

    /// 宿主的清缓存动作(维护快捷键、iOS 键盘扩展的内存告警):清掉本会话各方案的候选缓存,并放掉进程级缓存里的日文模型和没有会话在用的个人学习模型,让不再用它们的进程能交还内存;本会话若正持有模型,照常用到会话结束。
    pub fn reset_cache(&mut self) {
        self.candidates_may_change();
        self.prediction.clear();
        self.input.reset_cache();
        crate::japanese::decoder::JapaneseDictionary::release_shared();
        crate::release_idle_personal_models();
    }

    /// Replace the `/` mode's command table; rows it cannot use are dropped. A diagnostic only if the open command list could not be refreshed.
    pub fn set_command_table(&mut self, table: &[CommandTableEntry]) -> Option<String> {
        self.candidates_may_change();
        self.input.set_command_table(table)
    }

    /// 替换脚本插件的 `/` 指令;用不了的被丢弃(`local::command::usable_plugin_commands`),缓存的插件行清掉。只有打开的 `/` 列表刷新失败时才返回诊断。
    pub fn set_plugin_commands(&mut self, commands: &[PluginCommand]) -> Option<String> {
        self.candidates_may_change();
        self.input.set_plugin_commands(commands)
    }

    /// 生效的插件指令里 `trigger` 的标题,见 `CandidateQueries::plugin_command_title`。
    pub fn plugin_command_title(&self, trigger: &str) -> Option<&str> {
        self.input.queries.plugin_command_title(trigger)
    }

    /// 替换 K 模式的宿主短语表;用不了的行被丢弃。只有打开的 K 模式列表刷新失败时才返回诊断。
    pub fn set_quick_phrase_table(&mut self, table: &[QuickPhraseEntry]) -> Option<String> {
        self.candidates_may_change();
        self.input.set_quick_phrase_table(table)
    }

    /// Replace the `@` mode's list; entries it cannot use are dropped.
    pub fn set_mention_entries(&mut self, entries: &[MentionEntry]) -> Option<String> {
        self.candidates_may_change();
        self.input.set_mention_entries(entries)
    }

    /// Turn the embedded places of `@` mode on or off.
    pub fn set_mention_places(&mut self, enabled: bool) -> Option<String> {
        self.candidates_may_change();
        self.input.set_mention_places(enabled)
    }

    /// Forget the committed-word context (focus or application change, host-inserted text).
    pub fn reset_context(&mut self) {
        self.candidates_may_change();
        self.prediction.clear();
        self.input.pending_touch = None;
        self.input.reset_commit_context();
    }

    /// 宿主的即时隐私门禁;关闭时清掉建议,用户偏好仍保留。
    pub fn set_prediction_allowed(&mut self, allowed: bool) {
        self.candidates_may_change();
        self.prediction.set_allowed(allowed);
    }

    /// Disabling also ends the current context.
    pub fn set_personal_context_enabled(&mut self, enabled: bool) {
        self.candidates_may_change();
        self.prediction.clear();
        self.input.set_personal_context_enabled(enabled);
        self.nine_key.set_personal_context_enabled(enabled);
    }

    /// 当前组字是否预留云候选席位;运行时在修改前据此判断候选身份是否会变化。
    pub fn cloud_candidates_enabled(&self) -> bool {
        self.input.cloud_candidates_enabled
    }

    /// 让宿主把云候选的固定席位交给当前会话;关闭时立即去掉尚未回答的占位行。
    pub fn set_cloud_candidates_enabled(&mut self, enabled: bool) {
        self.candidates_may_change();
        self.input.set_cloud_candidates_enabled(enabled);
    }

    pub fn english_online_candidates_enabled(&self) -> bool {
        self.input.english_online_candidates_enabled
    }

    pub fn set_english_online_candidates_enabled(&mut self, enabled: bool) {
        self.candidates_may_change();
        self.input.set_english_online_candidates_enabled(enabled);
    }

    /// The committed text the neural sentence models condition on, updated without rebuilding the session. Only the last 64 characters matter.
    pub fn set_rescoring_context(&mut self, context: &str) {
        self.input.set_rescoring_context(context);
        self.nine_key.set_rescoring_context(context);
    }

    /// 首页行数只决定词库要不要并入补全词。`Runtime` 建立、换引擎和宿主改每页候选数(`Runtime::set_page_size`)时都调它,因此引擎的首屏门槛和分页始终同源,不必等下一次因别的偏好变化重建引擎。
    pub fn set_first_page_rows(&mut self, rows: FirstPageRows) {
        self.candidates_may_change();
        self.input.set_first_page_rows(rows);
        self.nine_key.set_first_page_rows(rows);
    }

    /// 调频方式(`SessionOptions::frequency`),不重建会话就换掉;26 键和九宫格一起换。从下一次选择起按新方式学,已经学下的排序照旧生效,眼前的候选不重排。`trigger_count` 或 `linear_step` 不在 1..=10 时是 `INVALID_SESSION_OPTIONS`(建会话时同样的检查),什么也不改。
    pub fn set_frequency_adjustment(&mut self, options: FrequencyAdjustmentOptions) -> Result<()> {
        if !input::frequency_options_valid(options) {
            return Err(EngineError::invalid(diagnostics::INVALID_SESSION_OPTIONS));
        }
        self.candidates_may_change();
        self.input.frequency = options;
        self.nine_key.set_frequency(options);
        Ok(())
    }

    /// 当前的调频方式:建会话时给的,或之后 [`Self::set_frequency_adjustment`] 换上的。
    pub fn frequency_adjustment(&self) -> FrequencyAdjustmentOptions {
        self.input.frequency
    }

    /// Hand over the candidates withheld from a single-letter query; whether the list grew.
    pub fn expand_initial_candidates(&mut self) -> bool {
        self.input.pending_touch = None;
        // Nine-key rows come from the spelling session rather than a dictionary query, so nothing is withheld there.
        if self.nine_key.active() {
            return false;
        }
        let grew = self.input.expand_initial_candidates();
        if grew {
            self.candidates_may_change();
        }
        grew
    }

    /// 设置前缀解码的组字光标(overlays.md §7.6);位置限制在可编辑范围,不能越过本地模式的入口标记。`None` 回到末尾,九键组码时无操作。
    pub fn set_caret(&mut self, caret: Option<usize>) {
        self.candidates_may_change();
        self.prediction.clear();
        self.input.pending_touch = None;
        // 五笔 14 键的组码不是逐个字母,没有可以落脚的光标位置。
        if self.nine_key.active() || self.input.wubi_grid_composing() {
            return;
        }
        self.input.set_caret(caret);
    }

    /// End of the complete pinyin units before the caret in `editing_text`; 0 while nine-key digits are active.
    pub fn prefix_end(&self) -> usize {
        if self.nine_key.active() {
            return 0;
        }
        self.input.prefix_end()
    }

    /// The raw text after `prefix_end`, case kept; empty while nine-key digits are active.
    pub fn pending_suffix(&self) -> String {
        if self.nine_key.active() {
            return String::new();
        }
        self.input.pending_suffix()
    }

    pub fn snapshot(&self) -> SessionSnapshot {
        self.snapshot_with_display_options(true, false)
    }

    /// 给宿主画在屏幕上的快照:全拼的 `preedit` 按音节分开(双拼照 `shuangpin_preedit_uses_raw`),辅助码注释照常给出。[`Session::snapshot`] 的 `preedit` 保留全拼的按键原文,参考回放按它比对;其余字段两者相同。C 接口的 `msime_engine_state` 读的是这一份。
    pub fn display_snapshot(&self) -> SessionSnapshot {
        self.snapshot_with_display_options(true, true)
    }

    pub(crate) fn snapshot_for_host(&self, show_helpcode: bool) -> SessionSnapshot {
        self.snapshot_with_display_options(show_helpcode, true)
    }

    fn snapshot_with_display_options(
        &self,
        show_helpcode: bool,
        segmented_quanpin: bool,
    ) -> SessionSnapshot {
        if !self.prediction.rows.is_empty() {
            return SessionSnapshot {
                prediction: true,
                scheme: self.input.scheme(),
                local_mode: self.input.local_mode,
                spelling_symbols: self.input.spelling_symbols(),
                shuangpin_profile: self.input.profile.kind.name().to_owned(),
                candidates: self.prediction.rows.clone(),
                candidate_sources: self.prediction.rows.iter().map(|row| row.source).collect(),
                candidate_annotations: vec![String::new(); self.prediction.rows.len()],
                candidate_answers_key: vec![false; self.prediction.rows.len()],
                ..SessionSnapshot::default()
            };
        }
        if self.nine_key.active() {
            return self.nine_key.snapshot();
        }
        let hide_helpcode = !show_helpcode
            && self.input.helpcode_enabled()
            && !self.input.local_mode.annotates_rows();
        let input = &self.input;
        let mut candidates = input.candidates().to_vec();
        for item in &mut candidates {
            input.apply_english_word_case(item.source, &mut item.word);
        }
        // 光标后编辑:候选仍是光标后那段的行,首选显示时前面接上固定的那段(由 runtime 在视图里接上),这样重排、释义和选择核对都按行本身的文字进行。
        let caret_edit_suffix = input.caret_edit.active && input.conversion.is_none();
        let caret_edit_prefix = input
            .caret_edit_prefix()
            .filter(|_| caret_edit_suffix && !candidates.is_empty())
            .unwrap_or_default();
        // 宿主显示全拼分词;参考回放的底层快照仍保留原文。
        let segmented_preedit = (segmented_quanpin && input.scheme() == SchemeType::Quanpin)
            || (!self.shuangpin_preedit_uses_raw && input.scheme() == SchemeType::Shuangpin);
        let preedit = if segmented_preedit
            && input.local_mode == LocalInputMode::None
            && !input.dedicated_english
        {
            input.pinyin_segmentation_with_cases().into_owned()
        } else {
            input.preedit()
        };
        SessionSnapshot {
            prediction: false,
            scheme: input.scheme(),
            local_mode: input.local_mode,
            spelling_symbols: input.spelling_symbols(),
            preedit,
            raw_segmentation: input.raw_segmentation(),
            normalized_segmentation: input.normalized_segmentation(),
            dedicated_english: input.dedicated_english,
            editing_text: input.editing_text(),
            caret_position: input.caret_position(),
            nine_key_spellings: input.zhuyin_spellings(),
            // 五笔 14 键的读音行显示首选的编码(就是预编辑),宿主 14 键的读音行照常读这一项。
            nine_key_reading: if input.wubi_grid_composing() {
                input.preedit()
            } else {
                String::new()
            },
            nine_key_single_character: false,
            nine_key_strokes: String::new(),
            answered_by_pinyin_fallback: input.answered_by_pinyin_fallback(),
            wubi_unique_four_code: input.wubi_unique_four_code(),
            shuangpin_profile: input.profile.kind.name().to_owned(),
            candidate_sources: candidates.iter().map(|item| item.source).collect(),
            multilingual_conversion: candidates
                .iter()
                .any(|item| item.source == CandidateSource::Multilingual)
                .then(|| input.multilingual_conversion.clone())
                .flatten(),
            candidate_annotations: input.candidate_annotations(hide_helpcode),
            // 纠错候选按原始按键长度消费;普通候选仍按 `pinyin`,两者都由组字推进判断。
            candidate_answers_key: candidates
                .iter()
                .map(|item| {
                    // 占位行、网址补全和联系人不交给重排模型;联系人保留本地优先顺序。
                    !matches!(
                        item.source,
                        CandidateSource::AiPending
                            | CandidateSource::CloudPending
                            | CandidateSource::Contact
                    ) && !input.is_url_completion(item)
                        && input.selection_completes_composition(item)
                })
                .collect(),
            candidate_list_open: input.candidate_list_open(),
            conversion: input
                .conversion
                .as_ref()
                .map(|edit| edit.text())
                .unwrap_or_default(),
            conversion_focus: input
                .conversion
                .as_ref()
                .map_or((0, 0), |edit| edit.focus_span()),
            caret_edit_suffix,
            caret_edit_prefix,
            candidates,
        }
    }

    /// 返回候选词的完整五笔编码,仅供宿主显示反查结果;九宫格会话不沿用旧的普通候选反查。
    pub fn candidate_wubi_code(&self, word: &str) -> Option<&str> {
        if self.nine_key.active() {
            return None;
        }
        self.input.engine.candidate_wubi_code(word)
    }

    /// 显示候选与原始候选同座且词面一致时复用已缓存的反查码;候选身份错位时返回 `None`,由宿主回退到按词入口。
    pub(crate) fn candidate_wubi_code_at_if_item(
        &self,
        index: usize,
        item: &WordItem,
    ) -> Option<&str> {
        if self.nine_key.active() {
            return None;
        }
        self.input
            .engine
            .candidate_wubi_code_at_if_item(index, item)
    }

    pub(crate) fn candidate_wubi_code_if_item(&self, item: &WordItem) -> Option<&str> {
        if self.nine_key.active() {
            return None;
        }
        self.input.engine.candidate_wubi_code_if_item(item)
    }

    /// Byte offsets into `editing_text` for pinyin-unit editing; empty when idle, in local modes, for non-pinyin schemes and during nine-key input. Read-only.
    pub fn segment_raw_boundaries(&self) -> Vec<usize> {
        if self.nine_key.active() {
            return Vec::new();
        }
        self.input.segment_raw_boundaries()
    }

    pub fn online_query(&self) -> Option<OnlineQuery> {
        if self.nine_key.active() {
            return None;
        }
        self.input.online_query()
    }

    /// Whether an apostrophe now separates a unit from its target or one `/fy` word from the next, so a host or runtime that would finish the composition on punctuation hands it over as a character instead.
    pub fn takes_local_separator(&self) -> bool {
        !self.nine_key.active() && self.input.takes_local_separator()
    }

    /// The `/fy` translation request; see `InputSession::command_translation_query`.
    pub fn command_translation_query(&self) -> Option<CommandTranslationQuery> {
        if self.nine_key.active() {
            return None;
        }
        self.input.command_translation_query()
    }

    /// Whether the translation now shows first; false (not an error) for a stale query.
    pub fn apply_command_translation(
        &mut self,
        query: &CommandTranslationQuery,
        translation: &str,
    ) -> bool {
        if self.nine_key.active() {
            return false;
        }
        let applied = self.input.apply_command_translation(query, translation);
        if applied {
            self.candidates_may_change();
        }
        applied
    }

    /// 插件指令要宿主去问的那一次;见 `InputSession::plugin_command_query`。
    pub fn plugin_command_query(&self) -> Option<PluginCommandQuery> {
        if self.nine_key.active() {
            return None;
        }
        self.input.plugin_command_query()
    }

    /// 插件的行是否写进了会话;过期的查询返回 false(不是错误)。
    pub fn apply_plugin_command_rows(
        &mut self,
        query: &PluginCommandQuery,
        rows: &[(String, String)],
    ) -> bool {
        if self.nine_key.active() {
            return false;
        }
        let applied = self.input.apply_plugin_command_rows(query, rows);
        if applied {
            self.candidates_may_change();
        }
        applied
    }

    /// Whether the rows now show; false (not an error) for a stale query.
    pub fn apply_online_candidates(
        &mut self,
        query: &OnlineQuery,
        words: &[String],
        source: CandidateSource,
    ) -> bool {
        if self.nine_key.active() {
            return false;
        }
        let applied = self.input.apply_online_candidates(query, words, source);
        if applied {
            self.candidates_may_change();
        }
        applied
    }

    /// Remove one online provider's rows from cached and visible candidates.
    pub fn clear_online_candidates(&mut self, source: CandidateSource) {
        self.candidates_may_change();
        self.input.clear_online_candidates(source);
    }

    pub fn apply_online_candidate(
        &mut self,
        query: &OnlineQuery,
        word: &str,
        source: CandidateSource,
    ) -> bool {
        if self.nine_key.active() {
            return false;
        }
        let applied = self.input.apply_online_candidate(query, word, source);
        if applied {
            self.candidates_may_change();
        }
        applied
    }

    /// Grid commits bypass the input session, so the word after one must not read the word before it as its context (session.cpp:37-45).
    fn after_nine_key(&mut self, result: KeyResult) -> KeyResult {
        if result.commit.is_some() {
            self.input.note_external_commit();
        }
        result
    }

    fn predict_after_commit(&mut self, selected: Option<WordItem>, result: &KeyResult) {
        if !self.prediction.enabled
            || self.nine_key.active()
            || self.input.has_composition()
            || self.input.dedicated_english
            || self.input.local_mode != LocalInputMode::None
            || !matches!(
                self.input.scheme(),
                SchemeType::Quanpin | SchemeType::Shuangpin
            )
        {
            return;
        }
        let Some(selected) = selected else { return };
        if result.commit.as_deref() != Some(selected.word.as_str()) {
            return;
        }
        let previous = selected
            .sentence_words
            .last()
            .map_or(selected.word.as_str(), String::as_str);
        self.show_prediction(&selected.word, &selected.canonical_pinyin, previous);
    }

    fn show_prediction(&mut self, word: &str, pinyin: &str, previous: &str) {
        let personal = self
            .input
            .personal_context_applies()
            .then_some(self.input.personal_context.as_ref());
        self.prediction.show(word, pinyin, previous, personal);
    }
}

/// The C++ registered `PersonalNgramStore::flush_all` with `atexit` (personal_ngram_store.cpp:255), so context learned in the last ~2 s reached the journal when the host quit. Rust runs no destructors for statics and `atexit` needs unsafe, so the session writes its journal's queue as it ends, which hosts do on deactivation and shutdown. A host that can exit without dropping its sessions (macOS `[NSApp terminate:]` runs `exit()`) calls [`crate::flush_personal_learning`] (host-api's `msime_client_flush_all`) or [`crate::shutdown`] from its will-terminate hook instead.
impl Drop for Session {
    fn drop(&mut self) {
        // Nobody is left to report to: a failed write stays queued and flagged in the store, and the next record reports it and schedules another try.
        let _ = flush_journal(&self.journal);
        // A host thread whose sessions are gone (a quiesced IME, a closed window) holds no journal handle.
        release_thread_journal();
        // The last session on this journal also closes the idle connections other threads keep to it, so a session created on one thread and ended on another leaves nothing open. Before the record below: closing the last connection to a WAL journal checkpoints it, which changes the files the record describes.
        self.journal_use.end();
        // Nothing of this session writes the journal after this point. A host-api session still holds its dictionary access here (its Runtime drops first), so no dictionary maintenance can change the file between the write above and this record.
        note_settled(&self.journal);
    }
}