use crate::dict::PinyinDict;
use crate::engine::PinyinEngine;
use crate::fuzzy::FuzzyConfig;
pub struct Session<'e> {
engine: &'e PinyinEngine,
input: String,
cand_buf: Vec<String>,
}
impl<'e> Session<'e> {
pub fn new(engine: &'e PinyinEngine) -> Self {
Self {
engine,
input: String::new(),
cand_buf: Vec::with_capacity(16),
}
}
pub fn input_char(&mut self, c: char) {
if c.is_ascii_alphabetic() {
self.input.push(c.to_ascii_lowercase());
}
}
pub fn backspace(&mut self) -> bool {
self.input.pop().is_some()
}
pub fn reset(&mut self) {
self.input.clear();
}
pub fn input(&self) -> &str {
&self.input
}
pub fn candidates(&mut self) -> &[String] {
if self.input.is_empty() {
self.cand_buf.clear();
return &self.cand_buf;
}
let input = self.input.clone();
Self::lookup_with_fuzzy(self.engine, &input, &mut self.cand_buf);
&self.cand_buf
}
pub fn lookup_into(&self, out: &mut Vec<String>) {
if self.input.is_empty() {
out.clear();
return;
}
Self::lookup_with_fuzzy(self.engine, &self.input, out);
}
fn lookup_with_fuzzy(engine: &PinyinEngine, input: &str, out: &mut Vec<String>) {
out.clear();
let dict = engine.dict();
let fuzzy = engine.fuzzy();
let alternates = expand_full_input(fuzzy, input);
let mut local = Vec::with_capacity(8);
for variant in alternates {
dict.lookup_into(&variant, &mut local);
for w in local.drain(..) {
if !out.contains(&w) {
out.push(w);
}
}
}
}
pub fn commit(&mut self, idx: usize) -> Option<String> {
let cands = self.candidates();
let word = cands.get(idx).cloned()?;
self.engine.dict().record_pick(&self.input, &word);
self.input.clear();
self.cand_buf.clear();
Some(word)
}
}
fn expand_full_input(fuzzy: FuzzyConfig, input: &str) -> Vec<String> {
if matches!(
fuzzy,
FuzzyConfig {
z_zh: false,
c_ch: false,
s_sh: false,
n_l: false,
f_h: false,
r_l: false,
in_ing: false,
en_eng: false,
an_ang: false
}
) {
return vec![input.to_string()];
}
fuzzy.expand(input)
}
#[allow(dead_code)]
fn quick_lookup(dict: &PinyinDict, pinyin: &str) -> Vec<String> {
dict.lookup(pinyin)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn input_and_lookup_zhongguo() {
let engine = PinyinEngine::new();
let mut session = Session::new(&engine);
for c in "zhongguo".chars() {
session.input_char(c);
}
let cands = session.candidates();
assert_eq!(cands.first().map(String::as_str), Some("中国"));
}
#[test]
fn backspace_shrinks_input() {
let engine = PinyinEngine::new();
let mut session = Session::new(&engine);
for c in "abc".chars() {
session.input_char(c);
}
assert_eq!(session.input(), "abc");
assert!(session.backspace());
assert_eq!(session.input(), "ab");
assert!(session.backspace());
assert!(session.backspace());
assert!(!session.backspace()); }
#[test]
fn commit_returns_word_and_resets() {
let engine = PinyinEngine::new();
let mut session = Session::new(&engine);
for c in "wo".chars() {
session.input_char(c);
}
let committed = session.commit(0);
assert_eq!(committed.as_deref(), Some("我"));
assert_eq!(session.input(), "");
assert!(session.candidates().is_empty());
}
#[test]
fn commit_out_of_range_is_noop() {
let engine = PinyinEngine::new();
let mut session = Session::new(&engine);
for c in "wo".chars() {
session.input_char(c);
}
assert!(session.commit(999_999).is_none());
assert_eq!(session.input(), "wo");
}
#[test]
fn input_char_filters_non_ascii() {
let engine = PinyinEngine::new();
let mut session = Session::new(&engine);
session.input_char('z');
session.input_char('中'); session.input_char('h');
assert_eq!(session.input(), "zh");
}
#[test]
fn fuzzy_expands_lookup() {
let engine = PinyinEngine::with_fuzzy(FuzzyConfig {
z_zh: true,
..FuzzyConfig::default()
});
let mut session = Session::new(&engine);
for c in "zong".chars() {
session.input_char(c);
}
let cands = session.candidates();
assert!(
cands.iter().any(|w| w == "中"),
"expected fuzzy z→zh to find 中, got {cands:?}"
);
}
#[test]
fn empty_input_no_candidates() {
let engine = PinyinEngine::new();
let mut session = Session::new(&engine);
assert!(session.candidates().is_empty());
}
#[test]
fn reset_clears_input() {
let engine = PinyinEngine::new();
let mut session = Session::new(&engine);
for c in "abc".chars() {
session.input_char(c);
}
session.reset();
assert_eq!(session.input(), "");
}
#[cfg(not(feature = "bootstrap_only"))]
#[test]
fn commit_feeds_l0_pin_promotion() {
use crate::ranking::PROMOTE_THRESHOLD;
let engine = PinyinEngine::new();
let target = "时";
for _ in 0..PROMOTE_THRESHOLD {
let mut s = Session::new(&engine);
for c in "shi".chars() {
s.input_char(c);
}
let cands = s.candidates();
let idx = cands
.iter()
.position(|w| w == target)
.expect("时 should be a 'shi' candidate");
assert_eq!(s.commit(idx).as_deref(), Some(target));
}
let mut probe = Session::new(&engine);
for c in "shi".chars() {
probe.input_char(c);
}
assert_eq!(
probe.candidates().first().map(String::as_str),
Some(target),
"expected L0 to pin 时 after {PROMOTE_THRESHOLD} picks via Session::commit"
);
}
}