Skip to main content

msime_engine/session/
mod.rs

1//! 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.
2//!
3//! 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.
4
5mod candidates;
6mod caret_edit;
7mod chain;
8mod clock;
9mod commit;
10mod composition;
11mod conversion;
12mod editing;
13mod glide;
14mod helpcode_entry;
15mod input;
16mod learning;
17mod literal;
18mod online;
19pub mod options;
20mod plugin_commands;
21mod prediction;
22#[cfg(test)]
23mod tests;
24
25use std::path::PathBuf;
26use std::sync::Arc;
27
28use crate::diagnostics;
29use crate::error::{EngineError, Result};
30use crate::local::database::LocalDatabaseLease;
31use crate::nine_key::{KeyGrid, NineKeySession};
32use crate::pinyin::glide::{GlideKeyboard, GlidePoint};
33use crate::touch::{KeyGeometry, KeyTouch, TouchTuning};
34use crate::types::{
35    CandidateEdge, CandidateSource, Command, CommandTableEntry, CommandTranslationQuery, KeyResult,
36    LocalInputMode, MentionEntry, OnlineQuery, PluginCommand, PluginCommandQuery, QuickPhraseEntry,
37    SchemeType, WordItem,
38};
39use crate::user_dictionary::journal::{release_thread_journal, JournalUse};
40use crate::user_dictionary::ngram_store::{flush_journal, note_settled};
41
42use crate::types::FirstPageRows;
43pub use clock::{Clock, LocalClock};
44use input::InputSession;
45pub use options::{SessionOptions, SessionSnapshot};
46
47pub struct Session {
48    input: InputSession,
49    nine_key: NineKeySession,
50    /// 触屏的组码网格:九键的数字从 `character` 进来,14 键的字母从 `grid_key` 进来;`None` 是不用网格。
51    key_grid: Option<KeyGrid>,
52    shuangpin_preedit_uses_raw: bool,
53    prediction: prediction::Prediction,
54    /// See [`Self::candidate_revision`].
55    revision: u64,
56    /// `msime_user.db` of this session's paths, written once more as the session ends.
57    journal: PathBuf,
58    journal_use: JournalUse,
59    /// 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.
60    _local_databases: LocalDatabaseLease,
61}
62
63// 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.
64const _: fn() = || {
65    fn moves_between_threads<T: Send>() {}
66    moves_between_threads::<Session>();
67};
68
69impl Session {
70    /// 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).
71    pub fn new(options: SessionOptions) -> Result<Session> {
72        options.paths.validate()?;
73        crate::user_dictionary::generation::check_read_only_layout(&options.paths)?;
74        let input = InputSession::new(&options)?;
75        let mut nine_key = NineKeySession::new(
76            &options.paths,
77            options.learning,
78            options.frequency,
79            options.fuzzy_pinyin,
80            options.english,
81            options.enabled_schemes.contains(SchemeType::Quanpin),
82            options.single_character_only,
83        );
84        nine_key.set_stroke_dictionary(options.stroke_dictionary.clone());
85        nine_key.set_mixed_expressive(options.expressive);
86        nine_key.set_mixed_candidates_enabled(options.mixed_candidates);
87        nine_key.set_inline_date_time(
88            options
89                .local_modes
90                .date_time_inline
91                .then(|| Arc::clone(&input.clock.local)),
92        );
93        // 九键的整句与 26 键遵守同一组句子联想设置(#6059)。
94        nine_key.set_sentence_options(
95            options.sentence_association.clone(),
96            options.sentence_alternatives,
97        );
98        nine_key.set_first_page_rows(FirstPageRows::new(options.first_page_rows));
99        nine_key.set_rescoring_context(&options.rescoring_context);
100        // 九键选中的词记进同一个个人上下文模型,开关与 26 键相同(#6185)。
101        nine_key.set_personal_context_enabled(options.personal_context);
102        let journal = options.paths.user(crate::assets::USER_JOURNAL);
103        Ok(Session {
104            input,
105            nine_key,
106            key_grid: None,
107            shuangpin_preedit_uses_raw: options.shuangpin_preedit_uses_raw,
108            prediction: prediction::Prediction::new(
109                options.next_word_prediction,
110                options.paths.dictionary(crate::assets::MAIN_DICTIONARY),
111            ),
112            revision: 0,
113            journal_use: JournalUse::new(journal.clone()),
114            journal,
115            _local_databases: LocalDatabaseLease::new([
116                options.paths.dictionaries.clone(),
117                options.paths.resources.clone(),
118            ]),
119        })
120    }
121
122    /// 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.
123    pub fn candidate_revision(&self) -> u64 {
124        self.revision
125    }
126
127    fn candidates_may_change(&mut self) {
128        self.revision = self.revision.wrapping_add(1);
129    }
130
131    /// 会话建立时定下的双拼方案(内置的表,或校验过的用户表)。
132    pub(crate) fn shuangpin_layout(&self) -> &'static crate::shuangpin::ShuangpinProfile {
133        self.input.profile
134    }
135
136    /// 会话现在过滤候选用的辅助码表(`SessionOptions::helpcode_table`、按 `helpcode_schema` 读的表,或之后 `set_helpcode_table` 换上的表);辅助码关着时照样留着。门面给自己合成的整句补注释时读它,不另读一份。
137    pub(crate) fn helpcode_table(&self) -> Option<&crate::helpcode::HelpcodeKeymap> {
138        self.input.helpcode_keymap.as_deref()
139    }
140
141    /// 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.
142    pub fn set_clock(&mut self, clock: Clock) {
143        if self.input.local_mode_options.date_time_inline {
144            self.nine_key
145                .set_inline_date_time(Some(Arc::clone(&clock.local)));
146        }
147        self.input.clock = clock;
148    }
149
150    /// One ASCII character; `shift_only` is a bare Shift+letter (local mode entry). 全拼下九键开启且没有别的组字时,数字 2-9 交给九宫格会话;九键双拼交的是方案用到的数字(可以有 1 和 0);注音九键的数字由注音编辑器自己处理。待用的触点([`Self::set_key_touch`])只交给 26 键组字,九宫格的数字用不到它,一律在这一键后作废。
151    pub fn character(&mut self, value: u8, shift_only: bool) -> KeyResult {
152        self.candidates_may_change();
153        self.prediction.clear();
154        let touch = self.input.pending_touch.take();
155        // 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: 拼音九宫格只认得全拼音节,注音九键走注音编辑器。九键双拼没有英文九键,专用英文里数字照常是数字。
156        let grid_digit = match self.key_grid {
157            Some(KeyGrid::NineKey) => (b'2'..=b'9').contains(&value),
158            Some(grid @ KeyGrid::NineKeyShuangpin(_)) => {
159                !self.input.dedicated_english && grid.is_code(value)
160            }
161            _ => false,
162        };
163        if grid_digit
164            && self.input.scheme() == SchemeType::Quanpin
165            && self.input.local_mode == LocalInputMode::None
166            && self.input.preedit().is_empty()
167        {
168            let result = self.nine_key.character(value);
169            return self.after_nine_key(result);
170        }
171        if self.nine_key.active() {
172            // `'` splits the grid's syllables where the user is typing; any other key is still the host's to handle.
173            if value == b'\'' {
174                let result = self.nine_key.character(value);
175                return self.after_nine_key(result);
176            }
177            return KeyResult::unhandled();
178        }
179        self.input.pending_touch = touch;
180        self.input.handle_character(value, shift_only)
181    }
182
183    /// 下一次 `character` 的按下点(触屏宿主经 `msime_client_touch_character` 送来)。只有那一键是 26 键双拼的小写字母或 `;` 时才交给方案;其余会改动组字或改道按键的入口(命令、选词、以词定字、结束组字、钉住和删除候选、移光标、九宫格、网格键、滑行、标点、换方案、换网格或几何、展开首字母候选、重置上下文)一开头就丢掉它,所以改道到别处的按键不会把触点留给后面的字母。只改配置的 setter(辅助码表、云候选开关、重排上下文等)不碰它。
184    pub fn set_key_touch(&mut self, touch: Option<KeyTouch>) {
185        self.input.pending_touch = touch;
186    }
187
188    /// 宿主注册或清除的键位几何,见 [`crate::touch::KeyGeometry`]。
189    pub fn set_key_geometry(&mut self, geometry: Option<Arc<KeyGeometry>>) {
190        // 待用触点是按旧几何量出来的,交给新几何解释没有意义。
191        self.input.pending_touch = None;
192        self.input.engine.set_key_geometry(geometry);
193    }
194
195    /// 按触点计价的参数;只有 Rust 侧(评测示例)调用,不经 FFI。
196    pub fn set_touch_tuning(&mut self, tuning: TouchTuning) {
197        self.input.engine.set_touch_tuning(tuning);
198    }
199
200    /// 滑过字母键的一笔:它最可能拼出的全拼字母写到光标处,与已有组字之间用 `'` 隔开。不是全拼、在本地模式或专用英文里、九键数字正在组字,以及没有串跟得上这一笔时,不处理。
201    pub fn glide(&mut self, keyboard: &GlideKeyboard, points: &[GlidePoint]) -> KeyResult {
202        self.candidates_may_change();
203        self.prediction.clear();
204        self.input.pending_touch = None;
205        if self.nine_key.active() {
206            return KeyResult::unhandled();
207        }
208        self.input.glide(keyboard, points)
209    }
210
211    /// 上一次 [`Session::set_key_grid`](或 [`Session::set_nine_key_enabled`])设下的组码网格,`None` 是不用网格;新会话是 `None`。网格开着时硬件键盘的字母照样走 26 键,要知道这次组字是不是在网格里打的,看 [`Session::grid_composing`]。
212    pub fn key_grid(&self) -> Option<KeyGrid> {
213        self.key_grid
214    }
215
216    /// 字母网格正在组字:九宫格会话(九键、14 键、18 键)在组字,快照、命令和上屏都来自它;或者五笔 14 键在组字,组码不是确定的字母。两种情况下的原样上屏都不是用户逐个打出的字母。
217    pub fn grid_composing(&self) -> bool {
218        self.nine_key.active() || self.input.wubi_grid_composing()
219    }
220
221    /// `set_key_grid` 的九键开关:开是九键,关是不用网格。
222    pub fn set_nine_key_enabled(&mut self, enabled: bool) {
223        self.set_key_grid(enabled.then_some(KeyGrid::NineKey));
224    }
225
226    /// 换触屏的组码网格,先丢掉网格里的组字,所以在换键盘布局、没有组字时调用。英文九键和注音九键只在九键下打开(注音组字会被丢掉):14 键的英文模式画全键盘,注音没有 14 键。
227    pub fn set_key_grid(&mut self, grid: Option<KeyGrid>) {
228        self.candidates_may_change();
229        self.prediction.clear();
230        self.input.pending_touch = None;
231        self.input.helpcode_entry = None;
232        self.nine_key.command(Command::Cancel);
233        if let Some(grid) = grid {
234            self.nine_key.set_grid(grid);
235        }
236        self.key_grid = grid;
237        let nine_key = grid == Some(KeyGrid::NineKey);
238        self.nine_key
239            .set_english_only(nine_key && self.input.dedicated_english);
240        self.input.set_zhuyin_nine_key(nine_key);
241    }
242
243    /// 字母网格的一键:小写字母由 `KeyGrid::code_of` 归成整组,十八键双拼另收大写 ASCII 标记精确字母。标记保留在组码里,不复用硬件键盘的 `character`。全拼和双拼分别使用自己的分组表,五笔在 14 键下按组展开码表(`wubi::grid`);本地模式、专用英文或已有 26 键组字时不处理,状态不变。
244    pub fn grid_key(&mut self, letter: u8) -> KeyResult {
245        self.candidates_may_change();
246        self.prediction.clear();
247        self.input.pending_touch = None;
248        let Some(
249            grid @ (KeyGrid::FourteenKey
250            | KeyGrid::EighteenKey
251            | KeyGrid::FourteenKeyShuangpin(_)
252            | KeyGrid::EighteenKeyShuangpin(_)),
253        ) = self.key_grid
254        else {
255            return KeyResult::unhandled();
256        };
257        let Some(code) = grid.code_of(letter) else {
258            return KeyResult::unhandled();
259        };
260        // 五笔 14 键:键面与全拼 14 键相同,组码交给五笔方案按码表展开,不经九宫格会话。
261        if grid == KeyGrid::FourteenKey && self.input.scheme() == SchemeType::Wubi {
262            let Some(representative) = crate::wubi::grid::representative(code) else {
263                return KeyResult::unhandled();
264            };
265            return self.input.handle_wubi_grid_key(representative);
266        }
267        let supported = match grid {
268            KeyGrid::FourteenKey | KeyGrid::EighteenKey => {
269                self.input.scheme() == SchemeType::Quanpin
270            }
271            KeyGrid::FourteenKeyShuangpin(_) | KeyGrid::EighteenKeyShuangpin(_) => {
272                self.input.scheme() == SchemeType::Shuangpin
273            }
274            _ => false,
275        };
276        if !supported
277            || self.input.local_mode != LocalInputMode::None
278            || self.input.dedicated_english
279            || !(self.input.preedit().is_empty() || self.nine_key.active())
280        {
281            return KeyResult::unhandled();
282        }
283        let result = self.nine_key.character(code);
284        self.after_nine_key(result)
285    }
286
287    /// 九宫格组码已经到上限时,这一键能不能先选掉首选、再接着打(`NineKeySession::limit_pick_ready`)。`grid_key` 为真时 `value` 是 `grid_key` 收到的归组字母,否则是 `character` 收到的九键或九键双拼数字;两条路由各自只认自己的网格。
288    pub fn grid_limit_pick_ready(&self, value: u8, grid_key: bool) -> bool {
289        let code = match self.key_grid {
290            Some(
291                grid @ (KeyGrid::FourteenKey
292                | KeyGrid::EighteenKey
293                | KeyGrid::FourteenKeyShuangpin(_)
294                | KeyGrid::EighteenKeyShuangpin(_)),
295            ) if grid_key => grid.code_of(value),
296            Some(KeyGrid::NineKey | KeyGrid::NineKeyShuangpin(_)) if !grid_key => Some(value),
297            _ => None,
298        };
299        code.is_some_and(|code| self.nine_key.limit_pick_ready(code))
300    }
301
302    /// 九宫格会话活跃时选它的拼写;否则在注音里钉目标音节的读音。
303    pub fn choose_nine_key_spelling(&mut self, index: usize) -> KeyResult {
304        self.candidates_may_change();
305        self.input.pending_touch = None;
306        if !self.nine_key.active() && self.input.zhuyin_rules_apply() {
307            return self.input.choose_zhuyin_spelling(index);
308        }
309        let result = self.nine_key.choose_spelling(index);
310        self.after_nine_key(result)
311    }
312
313    /// 九宫格组字时按单字、按笔画筛选候选(`NineKeySession::set_filter`);没有九宫格组字时不处理。
314    pub fn set_nine_key_filter(&mut self, single_character: bool, strokes: &str) -> KeyResult {
315        self.candidates_may_change();
316        self.input.pending_touch = None;
317        if !self.nine_key.active() {
318            return KeyResult::unhandled();
319        }
320        self.nine_key.set_filter(single_character, strokes)
321    }
322
323    pub fn command(&mut self, command: Command) -> KeyResult {
324        self.candidates_may_change();
325        self.input.pending_touch = None;
326        if !self.prediction.rows.is_empty() {
327            self.prediction.clear();
328            return if command == Command::Cancel {
329                KeyResult::handled()
330            } else {
331                self.input.handle_command(command)
332            };
333        }
334        let selected = (self.prediction.enabled && command == Command::CommitCandidate)
335            .then(|| self.snapshot().candidates.first().cloned())
336            .flatten();
337        if self.nine_key.active() {
338            // 九宫格没有整句改字,改字的光标键就是它的光标键。
339            let command = match command {
340                Command::ConversionLeft => Command::MoveLeft,
341                Command::ConversionRight => Command::MoveRight,
342                other => other,
343            };
344            let result = self.nine_key.command(command);
345            let result = self.after_nine_key(result);
346            self.predict_after_commit(selected, &result);
347            return result;
348        }
349        let result = self.input.handle_command(command);
350        self.predict_after_commit(selected, &result);
351        result
352    }
353
354    /// 整句改字的左移,进入改字时从第 `index` 个候选(宿主高亮的那一行)开始;已在改字里时与 `command(ConversionLeft)` 相同。九宫格和进不了改字时按 `MoveLeft` 处理。
355    pub fn conversion_left_from(&mut self, index: usize) -> KeyResult {
356        self.candidates_may_change();
357        self.prediction.clear();
358        self.input.pending_touch = None;
359        if self.nine_key.active() {
360            return self.command(Command::MoveLeft);
361        }
362        self.input.conversion_left_from(index)
363    }
364
365    /// `1`..`9` 选择组字候选;空闲联想不接管数字键。
366    pub fn candidate_key(&mut self, value: u8) -> KeyResult {
367        self.candidates_may_change();
368        self.prediction.clear();
369        self.input.pending_touch = None;
370        if self.nine_key.active() {
371            if !(b'1'..=b'9').contains(&value) {
372                return KeyResult::unhandled();
373            }
374            let result = self.nine_key.select(usize::from(value - b'1'));
375            return self.after_nine_key(result);
376        }
377        self.input.handle_candidate_key(value)
378    }
379
380    /// Finishes the composition and appends the Chinese mark; unhandled when Chinese punctuation is off or locked to ASCII, or the key has no mark.
381    pub fn punctuation(&mut self, value: u8) -> KeyResult {
382        self.candidates_may_change();
383        self.prediction.clear();
384        self.input.pending_touch = None;
385        if !self.nine_key.active() {
386            return self.input.handle_punctuation(value);
387        }
388        let mut result = self.input.handle_punctuation(value);
389        if !result.handled {
390            return result;
391        }
392        // The grid's text goes before the mark the input session translated.
393        let composition = self.nine_key.finish(0);
394        if composition.commit.is_some() {
395            self.input.reset_commit_context();
396        }
397        let mut text = composition.commit.unwrap_or_default();
398        text.push_str(result.commit.as_deref().unwrap_or_default());
399        result.commit = Some(text);
400        result
401    }
402
403    /// Live; keeps the composition, caret and pairing state.
404    pub fn set_chinese_punctuation_enabled(&mut self, enabled: bool) {
405        self.input.chinese_punctuation_enabled = enabled;
406    }
407
408    /// `INVALID_PUNCTUATION_LOCK` outside 0..=2.
409    pub fn set_punctuation_lock(&mut self, lock: i32) -> Result<()> {
410        if !(0..=2).contains(&lock) {
411            return Err(EngineError::invalid(diagnostics::INVALID_PUNCTUATION_LOCK));
412        }
413        self.input.punctuation_lock = lock;
414        Ok(())
415    }
416
417    pub fn set_paired_punctuation_enabled(&mut self, enabled: bool) {
418        self.input.punctuation.set_paired_enabled(enabled);
419    }
420
421    /// 换引号和方头括号的样式;组字、光标和交替状态都不动。
422    pub fn set_punctuation_marks(&mut self, marks: crate::punctuation::PunctuationMarks) {
423        self.input.punctuation.set_marks(marks);
424    }
425
426    /// The host emitted the closing half of a paired mark itself.
427    pub fn balance_paired_punctuation_after_auto_close(&mut self, opening: u8) {
428        self.input.punctuation.balance_after_auto_close(opening);
429    }
430
431    pub fn select(&mut self, index: usize) -> KeyResult {
432        self.candidates_may_change();
433        self.input.pending_touch = None;
434        if !self.prediction.rows.is_empty() {
435            let Some((row, word, pinyin)) = self.prediction.selected(index) else {
436                return KeyResult::unhandled();
437            };
438            let diagnostic = self.input.record_personal_context(Some(&row), false, false);
439            self.show_prediction(&word, &pinyin, &row.word);
440            return KeyResult::committed(row.word.to_string()).with_diagnostic(diagnostic);
441        }
442        let selected = self
443            .prediction
444            .enabled
445            .then(|| self.snapshot().candidates.get(index).cloned())
446            .flatten();
447        if self.nine_key.active() {
448            let result = self.nine_key.select(index);
449            let result = self.after_nine_key(result);
450            self.predict_after_commit(selected, &result);
451            return result;
452        }
453        let result = self.input.select_candidate(index);
454        self.predict_after_commit(selected, &result);
455        result
456    }
457
458    /// Commit the first or last Han character of a candidate and drop the rest of the composition.
459    pub fn select_edge(&mut self, index: usize, edge: CandidateEdge) -> KeyResult {
460        self.candidates_may_change();
461        self.input.pending_touch = None;
462        if !self.prediction.rows.is_empty() {
463            return KeyResult::unhandled();
464        }
465        if self.nine_key.active() {
466            return KeyResult::unhandled();
467        }
468        self.input.select_candidate_edge(index, edge)
469    }
470
471    /// Promote a dictionary candidate without committing; unsupported rows are unhandled, persistence failures carry a diagnostic.
472    pub fn pin(&mut self, index: usize) -> KeyResult {
473        self.candidates_may_change();
474        self.input.pending_touch = None;
475        if !self.prediction.rows.is_empty() {
476            return KeyResult::unhandled();
477        }
478        if self.nine_key.active() {
479            return self.nine_key.pin(index);
480        }
481        self.input.pin_candidate(index)
482    }
483
484    /// Remove a dictionary phrase without committing; single-character non-English rows are protected.
485    pub fn remove(&mut self, index: usize) -> KeyResult {
486        self.candidates_may_change();
487        self.input.pending_touch = None;
488        if !self.prediction.rows.is_empty() {
489            return KeyResult::unhandled();
490        }
491        if self.nine_key.active() {
492            return self.nine_key.remove(index);
493        }
494        self.input.remove_candidate(index)
495    }
496
497    /// Fix a dictionary candidate to slot 1..=5 in this input context; unhandled for any other slot.
498    pub fn fix_position(&mut self, index: usize, position: i32) -> KeyResult {
499        self.candidates_may_change();
500        self.input.pending_touch = None;
501        if !self.prediction.rows.is_empty() {
502            return KeyResult::unhandled();
503        }
504        if !(1..=5).contains(&position) {
505            return KeyResult::unhandled();
506        }
507        if self.nine_key.active() {
508            return self.nine_key.set_position(index, position);
509        }
510        self.input.set_candidate_position(index, position)
511    }
512
513    pub fn clear_position(&mut self, index: usize) -> KeyResult {
514        self.candidates_may_change();
515        self.input.pending_touch = None;
516        if !self.prediction.rows.is_empty() {
517            return KeyResult::unhandled();
518        }
519        if self.nine_key.active() {
520            return self.nine_key.set_position(index, 0);
521        }
522        self.input.set_candidate_position(index, 0)
523    }
524
525    /// 从宿主高亮的候选开始结束整段组字(没有高亮时为 0),剩余片段使用各自首选;索引无效时提交原文。普通拼音中的大写片段由字面候选保留。
526    pub fn finish(&mut self, first_index: usize) -> KeyResult {
527        self.candidates_may_change();
528        self.prediction.clear();
529        self.input.pending_touch = None;
530        if self.nine_key.active() {
531            let result = self.nine_key.finish(first_index);
532            return self.after_nine_key(result);
533        }
534        self.input.finish_composition(first_index)
535    }
536
537    /// 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`).
538    pub fn switch_scheme(&mut self, scheme: SchemeType) -> Result<()> {
539        self.candidates_may_change();
540        self.input.pending_touch = None;
541        self.input.switch_scheme(scheme)?;
542        self.prediction.clear();
543        self.nine_key.command(Command::Cancel);
544        Ok(())
545    }
546
547    pub fn is_supported_helpcode_schema(schema: &str) -> bool {
548        crate::helpcode::is_supported_helpcode_schema(schema)
549    }
550
551    /// False for an unsupported schema or a table that cannot be loaded.
552    pub fn set_helpcode_schema(&mut self, schema: &str) -> bool {
553        self.candidates_may_change();
554        self.input.set_helpcode_schema(schema)
555    }
556
557    /// 换上宿主给的辅助码表,替换当前的表;全拼和双拼都用它。
558    pub fn set_helpcode_table(&mut self, table: crate::helpcode::SharedKeymap) {
559        self.candidates_may_change();
560        self.input.set_helpcode_table(table);
561    }
562
563    /// Quanpin and shuangpin together, as `SessionOptions::helpcode` does.
564    pub fn set_helpcode_enabled(&mut self, enabled: bool) {
565        self.candidates_may_change();
566        self.input.set_helpcode_enabled(enabled);
567    }
568
569    /// 只有模式改变时才取消组字;重复设置保留当前输入。
570    pub fn set_dedicated_english(&mut self, enabled: bool) {
571        self.candidates_may_change();
572        self.input.pending_touch = None;
573        if self.input.dedicated_english == enabled {
574            return;
575        }
576        self.prediction.clear();
577        self.nine_key.command(Command::Cancel);
578        self.input.set_dedicated_english_mode(enabled);
579        // 九键在英文里用数字拼单词,在中文里拼音节,模式或网格改变时都要同步。
580        self.nine_key
581            .set_english_only(self.key_grid == Some(KeyGrid::NineKey) && enabled);
582    }
583
584    /// 混输候选的读音能继续时,运行时应保留组字,不提前顶字上屏。
585    pub fn wubi_pinyin_continues(&self, value: u8, shift: bool) -> bool {
586        !shift
587            && !self.input.dedicated_english
588            && self.input.local_mode == LocalInputMode::None
589            && self.input.engine.wubi_pinyin_continues(value)
590    }
591
592    pub fn set_wubi_mixed_pinyin(&mut self, enabled: bool) {
593        self.candidates_may_change();
594        self.input.set_wubi_mixed_pinyin(enabled);
595    }
596
597    /// 五笔拼音混输是否打开。
598    pub fn wubi_mixed_pinyin(&self) -> bool {
599        self.input.engine.wubi_input_options().mixed_pinyin
600    }
601
602    /// 宿主的清缓存动作(维护快捷键、iOS 键盘扩展的内存告警):清掉本会话各方案的候选缓存,并放掉进程级缓存里的日文模型和没有会话在用的个人学习模型,让不再用它们的进程能交还内存;本会话若正持有模型,照常用到会话结束。
603    pub fn reset_cache(&mut self) {
604        self.candidates_may_change();
605        self.prediction.clear();
606        self.input.reset_cache();
607        crate::japanese::decoder::JapaneseDictionary::release_shared();
608        crate::release_idle_personal_models();
609    }
610
611    /// Replace the `/` mode's command table; rows it cannot use are dropped. A diagnostic only if the open command list could not be refreshed.
612    pub fn set_command_table(&mut self, table: &[CommandTableEntry]) -> Option<String> {
613        self.candidates_may_change();
614        self.input.set_command_table(table)
615    }
616
617    /// 替换脚本插件的 `/` 指令;用不了的被丢弃(`local::command::usable_plugin_commands`),缓存的插件行清掉。只有打开的 `/` 列表刷新失败时才返回诊断。
618    pub fn set_plugin_commands(&mut self, commands: &[PluginCommand]) -> Option<String> {
619        self.candidates_may_change();
620        self.input.set_plugin_commands(commands)
621    }
622
623    /// 生效的插件指令里 `trigger` 的标题,见 `CandidateQueries::plugin_command_title`。
624    pub fn plugin_command_title(&self, trigger: &str) -> Option<&str> {
625        self.input.queries.plugin_command_title(trigger)
626    }
627
628    /// 替换 K 模式的宿主短语表;用不了的行被丢弃。只有打开的 K 模式列表刷新失败时才返回诊断。
629    pub fn set_quick_phrase_table(&mut self, table: &[QuickPhraseEntry]) -> Option<String> {
630        self.candidates_may_change();
631        self.input.set_quick_phrase_table(table)
632    }
633
634    /// Replace the `@` mode's list; entries it cannot use are dropped.
635    pub fn set_mention_entries(&mut self, entries: &[MentionEntry]) -> Option<String> {
636        self.candidates_may_change();
637        self.input.set_mention_entries(entries)
638    }
639
640    /// Turn the embedded places of `@` mode on or off.
641    pub fn set_mention_places(&mut self, enabled: bool) -> Option<String> {
642        self.candidates_may_change();
643        self.input.set_mention_places(enabled)
644    }
645
646    /// Forget the committed-word context (focus or application change, host-inserted text).
647    pub fn reset_context(&mut self) {
648        self.candidates_may_change();
649        self.prediction.clear();
650        self.input.pending_touch = None;
651        self.input.reset_commit_context();
652    }
653
654    /// 宿主的即时隐私门禁;关闭时清掉建议,用户偏好仍保留。
655    pub fn set_prediction_allowed(&mut self, allowed: bool) {
656        self.candidates_may_change();
657        self.prediction.set_allowed(allowed);
658    }
659
660    /// Disabling also ends the current context.
661    pub fn set_personal_context_enabled(&mut self, enabled: bool) {
662        self.candidates_may_change();
663        self.prediction.clear();
664        self.input.set_personal_context_enabled(enabled);
665        self.nine_key.set_personal_context_enabled(enabled);
666    }
667
668    /// 当前组字是否预留云候选席位;运行时在修改前据此判断候选身份是否会变化。
669    pub fn cloud_candidates_enabled(&self) -> bool {
670        self.input.cloud_candidates_enabled
671    }
672
673    /// 让宿主把云候选的固定席位交给当前会话;关闭时立即去掉尚未回答的占位行。
674    pub fn set_cloud_candidates_enabled(&mut self, enabled: bool) {
675        self.candidates_may_change();
676        self.input.set_cloud_candidates_enabled(enabled);
677    }
678
679    pub fn english_online_candidates_enabled(&self) -> bool {
680        self.input.english_online_candidates_enabled
681    }
682
683    pub fn set_english_online_candidates_enabled(&mut self, enabled: bool) {
684        self.candidates_may_change();
685        self.input.set_english_online_candidates_enabled(enabled);
686    }
687
688    /// The committed text the neural sentence models condition on, updated without rebuilding the session. Only the last 64 characters matter.
689    pub fn set_rescoring_context(&mut self, context: &str) {
690        self.input.set_rescoring_context(context);
691        self.nine_key.set_rescoring_context(context);
692    }
693
694    /// 首页行数只决定词库要不要并入补全词。`Runtime::set_page_size` 在宿主改每页候选数时调它,
695    /// 因此引擎的首屏门槛和分页始终同源,不必等下一次因别的偏好变化重建引擎。
696    pub fn set_first_page_rows(&mut self, rows: FirstPageRows) {
697        self.candidates_may_change();
698        self.input.set_first_page_rows(rows);
699        self.nine_key.set_first_page_rows(rows);
700    }
701
702    /// Hand over the candidates withheld from a single-letter query; whether the list grew.
703    pub fn expand_initial_candidates(&mut self) -> bool {
704        self.input.pending_touch = None;
705        // Nine-key rows come from the spelling session rather than a dictionary query, so nothing is withheld there.
706        if self.nine_key.active() {
707            return false;
708        }
709        let grew = self.input.expand_initial_candidates();
710        if grew {
711            self.candidates_may_change();
712        }
713        grew
714    }
715
716    /// 设置前缀解码的组字光标(overlays.md §7.6);位置限制在可编辑范围,不能越过本地模式的入口标记。`None` 回到末尾,九键组码时无操作。
717    pub fn set_caret(&mut self, caret: Option<usize>) {
718        self.candidates_may_change();
719        self.prediction.clear();
720        self.input.pending_touch = None;
721        // 五笔 14 键的组码不是逐个字母,没有可以落脚的光标位置。
722        if self.nine_key.active() || self.input.wubi_grid_composing() {
723            return;
724        }
725        self.input.set_caret(caret);
726    }
727
728    /// End of the complete pinyin units before the caret in `editing_text`; 0 while nine-key digits are active.
729    pub fn prefix_end(&self) -> usize {
730        if self.nine_key.active() {
731            return 0;
732        }
733        self.input.prefix_end()
734    }
735
736    /// The raw text after `prefix_end`, case kept; empty while nine-key digits are active.
737    pub fn pending_suffix(&self) -> String {
738        if self.nine_key.active() {
739            return String::new();
740        }
741        self.input.pending_suffix()
742    }
743
744    pub fn snapshot(&self) -> SessionSnapshot {
745        self.snapshot_with_display_options(true, false)
746    }
747
748    /// 给宿主画在屏幕上的快照:全拼的 `preedit` 按音节分开(双拼照 `shuangpin_preedit_uses_raw`),辅助码注释照常给出。[`Session::snapshot`] 的 `preedit` 保留全拼的按键原文,参考回放按它比对;其余字段两者相同。C 接口的 `msime_engine_state` 读的是这一份。
749    pub fn display_snapshot(&self) -> SessionSnapshot {
750        self.snapshot_with_display_options(true, true)
751    }
752
753    pub(crate) fn snapshot_for_host(&self, show_helpcode: bool) -> SessionSnapshot {
754        self.snapshot_with_display_options(show_helpcode, true)
755    }
756
757    fn snapshot_with_display_options(
758        &self,
759        show_helpcode: bool,
760        segmented_quanpin: bool,
761    ) -> SessionSnapshot {
762        if !self.prediction.rows.is_empty() {
763            return SessionSnapshot {
764                prediction: true,
765                scheme: self.input.scheme(),
766                local_mode: self.input.local_mode,
767                spelling_symbols: self.input.spelling_symbols(),
768                shuangpin_profile: self.input.profile.kind.name().to_owned(),
769                candidates: self.prediction.rows.clone(),
770                candidate_sources: self.prediction.rows.iter().map(|row| row.source).collect(),
771                candidate_annotations: vec![String::new(); self.prediction.rows.len()],
772                candidate_answers_key: vec![false; self.prediction.rows.len()],
773                ..SessionSnapshot::default()
774            };
775        }
776        if self.nine_key.active() {
777            return self.nine_key.snapshot();
778        }
779        let hide_helpcode = !show_helpcode
780            && self.input.helpcode_enabled()
781            && !self.input.local_mode.annotates_rows();
782        let input = &self.input;
783        let mut candidates = input.candidates().to_vec();
784        for item in &mut candidates {
785            input.apply_english_word_case(item.source, &mut item.word);
786        }
787        // 光标后编辑:候选仍是光标后那段的行,首选显示时前面接上固定的那段(由 runtime 在视图里接上),这样重排、释义和选择核对都按行本身的文字进行。
788        let caret_edit_suffix = input.caret_edit.active && input.conversion.is_none();
789        let caret_edit_prefix = input
790            .caret_edit_prefix()
791            .filter(|_| caret_edit_suffix && !candidates.is_empty())
792            .unwrap_or_default();
793        // 宿主显示全拼分词;参考回放的底层快照仍保留原文。
794        let segmented_preedit = (segmented_quanpin && input.scheme() == SchemeType::Quanpin)
795            || (!self.shuangpin_preedit_uses_raw && input.scheme() == SchemeType::Shuangpin);
796        let preedit = if segmented_preedit
797            && input.local_mode == LocalInputMode::None
798            && !input.dedicated_english
799        {
800            input.pinyin_segmentation_with_cases().into_owned()
801        } else {
802            input.preedit()
803        };
804        SessionSnapshot {
805            prediction: false,
806            scheme: input.scheme(),
807            local_mode: input.local_mode,
808            spelling_symbols: input.spelling_symbols(),
809            preedit,
810            raw_segmentation: input.raw_segmentation(),
811            normalized_segmentation: input.normalized_segmentation(),
812            dedicated_english: input.dedicated_english,
813            editing_text: input.editing_text(),
814            caret_position: input.caret_position(),
815            nine_key_spellings: input.zhuyin_spellings(),
816            // 五笔 14 键的读音行显示首选的编码(就是预编辑),宿主 14 键的读音行照常读这一项。
817            nine_key_reading: if input.wubi_grid_composing() {
818                input.preedit()
819            } else {
820                String::new()
821            },
822            nine_key_single_character: false,
823            nine_key_strokes: String::new(),
824            answered_by_pinyin_fallback: input.answered_by_pinyin_fallback(),
825            wubi_unique_four_code: input.wubi_unique_four_code(),
826            shuangpin_profile: input.profile.kind.name().to_owned(),
827            candidate_sources: candidates.iter().map(|item| item.source).collect(),
828            multilingual_conversion: candidates
829                .iter()
830                .any(|item| item.source == CandidateSource::Multilingual)
831                .then(|| input.multilingual_conversion.clone())
832                .flatten(),
833            candidate_annotations: input.candidate_annotations(hide_helpcode),
834            // 纠错候选按原始按键长度消费;普通候选仍按 `pinyin`,两者都由组字推进判断。
835            candidate_answers_key: candidates
836                .iter()
837                .map(|item| {
838                    // 占位行、网址补全和联系人不交给重排模型;联系人保留本地优先顺序。
839                    !matches!(
840                        item.source,
841                        CandidateSource::AiPending
842                            | CandidateSource::CloudPending
843                            | CandidateSource::Contact
844                    ) && !input.is_url_completion(item)
845                        && input.selection_completes_composition(item)
846                })
847                .collect(),
848            candidate_list_open: input.candidate_list_open(),
849            conversion: input
850                .conversion
851                .as_ref()
852                .map(|edit| edit.text())
853                .unwrap_or_default(),
854            conversion_focus: input
855                .conversion
856                .as_ref()
857                .map_or((0, 0), |edit| edit.focus_span()),
858            caret_edit_suffix,
859            caret_edit_prefix,
860            candidates,
861        }
862    }
863
864    /// 返回候选词的完整五笔编码,仅供宿主显示反查结果;九宫格会话不沿用旧的普通候选反查。
865    pub fn candidate_wubi_code(&self, word: &str) -> Option<&str> {
866        if self.nine_key.active() {
867            return None;
868        }
869        self.input.engine.candidate_wubi_code(word)
870    }
871
872    /// 显示候选与原始候选同座且词面一致时复用已缓存的反查码;候选身份错位时返回 `None`,由宿主回退到按词入口。
873    pub(crate) fn candidate_wubi_code_at_if_item(
874        &self,
875        index: usize,
876        item: &WordItem,
877    ) -> Option<&str> {
878        if self.nine_key.active() {
879            return None;
880        }
881        self.input
882            .engine
883            .candidate_wubi_code_at_if_item(index, item)
884    }
885
886    pub(crate) fn candidate_wubi_code_if_item(&self, item: &WordItem) -> Option<&str> {
887        if self.nine_key.active() {
888            return None;
889        }
890        self.input.engine.candidate_wubi_code_if_item(item)
891    }
892
893    /// 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.
894    pub fn segment_raw_boundaries(&self) -> Vec<usize> {
895        if self.nine_key.active() {
896            return Vec::new();
897        }
898        self.input.segment_raw_boundaries()
899    }
900
901    pub fn online_query(&self) -> Option<OnlineQuery> {
902        if self.nine_key.active() {
903            return None;
904        }
905        self.input.online_query()
906    }
907
908    /// 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.
909    pub fn takes_local_separator(&self) -> bool {
910        !self.nine_key.active() && self.input.takes_local_separator()
911    }
912
913    /// The `/fy` translation request; see `InputSession::command_translation_query`.
914    pub fn command_translation_query(&self) -> Option<CommandTranslationQuery> {
915        if self.nine_key.active() {
916            return None;
917        }
918        self.input.command_translation_query()
919    }
920
921    /// Whether the translation now shows first; false (not an error) for a stale query.
922    pub fn apply_command_translation(
923        &mut self,
924        query: &CommandTranslationQuery,
925        translation: &str,
926    ) -> bool {
927        if self.nine_key.active() {
928            return false;
929        }
930        let applied = self.input.apply_command_translation(query, translation);
931        if applied {
932            self.candidates_may_change();
933        }
934        applied
935    }
936
937    /// 插件指令要宿主去问的那一次;见 `InputSession::plugin_command_query`。
938    pub fn plugin_command_query(&self) -> Option<PluginCommandQuery> {
939        if self.nine_key.active() {
940            return None;
941        }
942        self.input.plugin_command_query()
943    }
944
945    /// 插件的行是否写进了会话;过期的查询返回 false(不是错误)。
946    pub fn apply_plugin_command_rows(
947        &mut self,
948        query: &PluginCommandQuery,
949        rows: &[(String, String)],
950    ) -> bool {
951        if self.nine_key.active() {
952            return false;
953        }
954        let applied = self.input.apply_plugin_command_rows(query, rows);
955        if applied {
956            self.candidates_may_change();
957        }
958        applied
959    }
960
961    /// Whether the rows now show; false (not an error) for a stale query.
962    pub fn apply_online_candidates(
963        &mut self,
964        query: &OnlineQuery,
965        words: &[String],
966        source: CandidateSource,
967    ) -> bool {
968        if self.nine_key.active() {
969            return false;
970        }
971        let applied = self.input.apply_online_candidates(query, words, source);
972        if applied {
973            self.candidates_may_change();
974        }
975        applied
976    }
977
978    /// Remove one online provider's rows from cached and visible candidates.
979    pub fn clear_online_candidates(&mut self, source: CandidateSource) {
980        self.candidates_may_change();
981        self.input.clear_online_candidates(source);
982    }
983
984    pub fn apply_online_candidate(
985        &mut self,
986        query: &OnlineQuery,
987        word: &str,
988        source: CandidateSource,
989    ) -> bool {
990        if self.nine_key.active() {
991            return false;
992        }
993        let applied = self.input.apply_online_candidate(query, word, source);
994        if applied {
995            self.candidates_may_change();
996        }
997        applied
998    }
999
1000    /// 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).
1001    fn after_nine_key(&mut self, result: KeyResult) -> KeyResult {
1002        if result.commit.is_some() {
1003            self.input.note_external_commit();
1004        }
1005        result
1006    }
1007
1008    fn predict_after_commit(&mut self, selected: Option<WordItem>, result: &KeyResult) {
1009        if !self.prediction.enabled
1010            || self.nine_key.active()
1011            || self.input.has_composition()
1012            || self.input.dedicated_english
1013            || self.input.local_mode != LocalInputMode::None
1014            || !matches!(
1015                self.input.scheme(),
1016                SchemeType::Quanpin | SchemeType::Shuangpin
1017            )
1018        {
1019            return;
1020        }
1021        let Some(selected) = selected else { return };
1022        if result.commit.as_deref() != Some(selected.word.as_str()) {
1023            return;
1024        }
1025        let previous = selected
1026            .sentence_words
1027            .last()
1028            .map_or(selected.word.as_str(), String::as_str);
1029        self.show_prediction(&selected.word, &selected.canonical_pinyin, previous);
1030    }
1031
1032    fn show_prediction(&mut self, word: &str, pinyin: &str, previous: &str) {
1033        let personal = self
1034            .input
1035            .personal_context_applies()
1036            .then_some(self.input.personal_context.as_ref());
1037        self.prediction.show(word, pinyin, previous, personal);
1038    }
1039}
1040
1041/// 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.
1042impl Drop for Session {
1043    fn drop(&mut self) {
1044        // 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.
1045        let _ = flush_journal(&self.journal);
1046        // A host thread whose sessions are gone (a quiesced IME, a closed window) holds no journal handle.
1047        release_thread_journal();
1048        // 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.
1049        self.journal_use.end();
1050        // 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.
1051        note_settled(&self.journal);
1052    }
1053}