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