Skip to main content

bamboo_core/
engine.rs

1//! The core engine that processes keypresses and maintains the IME state.
2
3use crate::config::Config;
4use crate::input_method::{InputMethod, Rule};
5use crate::mode::{Mode, OutputOptions};
6use crate::utils::{is_upper, lower};
7
8/// Maximum number of active transformations in a single syllable.
9pub const MAX_ACTIVE_TRANS: usize = 16;
10
11/// A lightweight snapshot of the engine state before a keystroke, used for O(1) backspace.
12#[derive(Clone, Copy)]
13struct Snapshot {
14    active_buffer: [Transformation; MAX_ACTIVE_TRANS],
15    active_len: usize,
16    current_state_id: u32,
17}
18
19/// Represents a single keypress or a transformation derived from it (e.g., adding a mark or tone).
20#[derive(Clone, Copy, Debug, PartialEq, Eq, Default, Hash)]
21pub struct Transformation {
22    /// The rule that was applied to create this transformation.
23    pub rule: Rule,
24    /// The index of the transformation in the composition that this transformation targets (if any).
25    /// For example, a tone mark transformation targets an earlier vowel.
26    /// Uses u8 since MAX_ACTIVE_TRANS = 16, saving 14 bytes vs `Option<usize>`.
27    pub target: Option<u8>,
28    /// Whether the resulting character should be rendered as uppercase.
29    pub is_upper_case: bool,
30}
31
32/// A stack-allocated buffer for transformations to avoid heap allocations in the hot path.
33///
34/// This structure uses a fixed-size array and is extremely fast for frequent updates.
35#[derive(Clone, Copy, Debug, PartialEq, Eq, Default, Hash)]
36pub struct TransformationStack {
37    data: [Transformation; MAX_ACTIVE_TRANS],
38    len: usize,
39}
40
41impl TransformationStack {
42    /// Creates a new, empty transformation stack.
43    pub fn new() -> Self {
44        Self { data: [Transformation::default(); MAX_ACTIVE_TRANS], len: 0 }
45    }
46
47    /// Pushes a new transformation onto the stack.
48    /// Does nothing if the stack is full.
49    pub fn push(&mut self, t: Transformation) {
50        debug_assert!(
51            self.len < MAX_ACTIVE_TRANS,
52            "TransformationStack overflow: max {MAX_ACTIVE_TRANS} reached"
53        );
54        if self.len < MAX_ACTIVE_TRANS {
55            self.data[self.len] = t;
56            self.len += 1;
57        }
58    }
59
60    /// Removes and returns the last transformation from the stack.
61    #[allow(dead_code)]
62    pub fn pop(&mut self) -> Option<Transformation> {
63        if self.len > 0 {
64            self.len -= 1;
65            Some(self.data[self.len])
66        } else {
67            None
68        }
69    }
70
71    /// Clears all transformations from the stack.
72    pub fn clear(&mut self) {
73        self.len = 0;
74    }
75
76    /// Returns the number of transformations currently in the stack.
77    pub fn len(&self) -> usize {
78        self.len
79    }
80
81    /// Returns true if the stack contains no transformations.
82    pub fn is_empty(&self) -> bool {
83        self.len == 0
84    }
85
86    /// Returns a slice containing all transformations in the stack.
87    pub fn as_slice(&self) -> &[Transformation] {
88        &self.data[..self.len]
89    }
90
91    /// Returns a mutable slice containing all transformations in the stack.
92    pub fn as_mut_slice(&mut self) -> &mut [Transformation] {
93        &mut self.data[..self.len]
94    }
95
96    /// Appends a slice of transformations to the stack.
97    pub fn extend_from_slice(&mut self, other: &[Transformation]) {
98        let to_copy = other.len().min(MAX_ACTIVE_TRANS - self.len);
99        if to_copy > 0 {
100            self.data[self.len..self.len + to_copy].copy_from_slice(&other[..to_copy]);
101            self.len += to_copy;
102        }
103    }
104
105    /// Drains transformations from a starting index into another stack.
106    pub fn drain_to(&mut self, start: usize, target: &mut TransformationStack) {
107        target.clear();
108        if start < self.len {
109            target.extend_from_slice(&self.data[start..self.len]);
110            self.len = start;
111        }
112    }
113}
114
115#[inline]
116fn uoh_tail_match(s: &str) -> bool {
117    for pat in ["uơ", "ưo"] {
118        if let Some(idx) = s.find(pat) {
119            let after = &s[idx + pat.len()..];
120            if after.chars().next().is_some_and(|c| c.is_alphabetic()) {
121                return true;
122            }
123        }
124    }
125    false
126}
127
128/// The main stateful processor of the Vietnamese Input Method Engine.
129///
130/// It maintains an internal buffer of transformations and produces the correctly marked Vietnamese text.
131/// The engine uses a hybrid approach combining a Rule Engine with a Lazy JIT DFA for peak performance.
132pub struct Engine {
133    committed_text: String,
134    /// Stack-allocated buffer for the active composition to avoid heap allocations.
135    active_buffer: [Transformation; MAX_ACTIVE_TRANS],
136    active_len: usize,
137
138    input_method: InputMethod,
139    all_rules: Box<[Rule]>,
140    ascii_rule_indices: [(u16, u16); 128],
141    non_ascii_rule_indices: Box<[(char, (u16, u16))]>,
142    ascii_effect_keys: [bool; 128],
143    non_ascii_effect_keys: Vec<char>,
144    config: Config,
145
146    // Stack buffers to avoid per-keystroke heap allocations.
147    work_comp: TransformationStack,
148    scratch_comp: TransformationStack,
149
150    prev_preedit: String,
151    delta_buf: String,
152
153    dfa: crate::dfa::Dfa,
154    current_state_id: u32,
155
156    // Snapshot stack for O(1) backspace — all stack-allocated, zero heap.
157    snapshots: [Snapshot; MAX_ACTIVE_TRANS],
158    snapshot_len: usize,
159}
160
161impl Engine {
162    /// Creates a new engine with the specified input method and default configuration.
163    pub fn new(input_method: InputMethod) -> Self {
164        Self::with_config(input_method, Config::default())
165    }
166
167    /// Creates a new engine with a specific input method and configuration.
168    pub fn with_config(input_method: InputMethod, config: Config) -> Self {
169        let mut rules_by_key: std::collections::BTreeMap<char, Vec<Rule>> =
170            std::collections::BTreeMap::new();
171        for rule in &input_method.rules {
172            let key = lower(rule.key);
173            rules_by_key.entry(key).or_default().push(*rule);
174        }
175
176        let total_rules: usize = rules_by_key.values().map(|v| v.len()).sum();
177        let mut all_rules_vec = Vec::with_capacity(total_rules);
178        let mut ascii_rule_indices = [(0u16, 0u16); 128];
179        let mut non_ascii_indices_vec = Vec::new();
180
181        for (key, rules) in rules_by_key {
182            let start = all_rules_vec.len() as u16;
183            all_rules_vec.extend(rules);
184            let end = all_rules_vec.len() as u16;
185            if key.is_ascii() {
186                ascii_rule_indices[key as usize] = (start, end);
187            } else {
188                non_ascii_indices_vec.push((key, (start, end)));
189            }
190        }
191
192        let mut ascii_effect_keys = [false; 128];
193        let mut non_ascii_effect_keys: Vec<char> = Vec::new();
194        for key in &input_method.keys {
195            if key.is_ascii() {
196                ascii_effect_keys[*key as usize] = true;
197            } else {
198                non_ascii_effect_keys.push(*key);
199            }
200        }
201        non_ascii_effect_keys.sort_unstable();
202        non_ascii_effect_keys.dedup();
203
204        Self {
205            committed_text: String::with_capacity(256),
206            active_buffer: [Transformation::default(); MAX_ACTIVE_TRANS],
207            active_len: 0,
208            input_method,
209            all_rules: all_rules_vec.into_boxed_slice(),
210            ascii_rule_indices,
211            non_ascii_rule_indices: non_ascii_indices_vec.into_boxed_slice(),
212            ascii_effect_keys,
213            non_ascii_effect_keys,
214            config,
215
216            work_comp: TransformationStack::new(),
217            scratch_comp: TransformationStack::new(),
218
219            prev_preedit: String::with_capacity(64),
220            delta_buf: String::with_capacity(64),
221            dfa: crate::dfa::Dfa::new(),
222            current_state_id: 0,
223
224            snapshots: [Snapshot {
225                active_buffer: [Transformation::default(); MAX_ACTIVE_TRANS],
226                active_len: 0,
227                current_state_id: 0,
228            }; MAX_ACTIVE_TRANS],
229            snapshot_len: 0,
230        }
231    }
232
233    #[inline]
234    pub(crate) fn active_slice(&self) -> &[Transformation] {
235        &self.active_buffer[..self.active_len]
236    }
237
238    fn take_active_into(&mut self, out: &mut TransformationStack) {
239        out.clear();
240        out.extend_from_slice(self.active_slice());
241        self.active_len = 0;
242    }
243
244    fn set_active_from_stack(&mut self, src: &mut TransformationStack) {
245        self.active_len = src.len().min(MAX_ACTIVE_TRANS);
246        self.active_buffer[..self.active_len].copy_from_slice(src.as_slice());
247        src.clear();
248    }
249
250    #[inline]
251    fn push_snapshot(&mut self) {
252        if self.snapshot_len < MAX_ACTIVE_TRANS {
253            let snap = &mut self.snapshots[self.snapshot_len];
254            snap.active_buffer[..self.active_len].copy_from_slice(&self.active_buffer[..self.active_len]);
255            snap.active_len = self.active_len;
256            snap.current_state_id = self.current_state_id;
257            self.snapshot_len += 1;
258        }
259    }
260
261    #[inline]
262    fn pop_snapshot(&mut self) -> Option<()> {
263        if self.snapshot_len == 0 {
264            return None;
265        }
266        self.snapshot_len -= 1;
267        let snap = &self.snapshots[self.snapshot_len];
268        self.active_len = snap.active_len;
269        self.active_buffer[..self.active_len].copy_from_slice(&snap.active_buffer[..self.active_len]);
270        self.current_state_id = snap.current_state_id;
271        Some(())
272    }
273
274    /// Returns the current configuration of the engine.
275    pub fn config(&self) -> Config {
276        self.config
277    }
278
279    /// Updates the engine configuration.
280    pub fn set_config(&mut self, config: Config) {
281        self.config = config;
282    }
283
284    /// Returns a copy of the current input method.
285    pub fn input_method(&self) -> InputMethod {
286        self.input_method.clone()
287    }
288
289    /// Warms up the DFA by pre-compiling common Vietnamese syllables.
290    ///
291    /// This API is intentionally unstable and currently uses a Telex-biased
292    /// heuristic corpus. It can help long-lived Telex sessions, but may hurt
293    /// cold-start latency or non-Telex/custom input methods.
294    ///
295    /// Prefer relying on the default lazy JIT behavior unless you have benchmark
296    /// data for your production workload.
297    #[deprecated(
298        since = "0.3.4",
299        note = "Engine::warm_up() is unstable and may be removed. It uses a Telex-biased heuristic and may regress cold-start or non-Telex workloads."
300    )]
301    pub fn warm_up(&mut self) {
302        let mut compiler = crate::dfa::DfaCompiler::new(&self.input_method, self.config.to_flags());
303        compiler.compile_common();
304        self.dfa = compiler.dfa;
305        self.current_state_id = 0;
306    }
307
308    fn get_applicable_rules(&self, key: char) -> &[Rule] {
309        let key = lower(key);
310        if key.is_ascii() {
311            let (start, end) = self.ascii_rule_indices[key as usize];
312            &self.all_rules[start as usize..end as usize]
313        } else {
314            self.non_ascii_rule_indices
315                .binary_search_by_key(&key, |(k, _)| *k)
316                .map(|idx| {
317                    let (start, end) = self.non_ascii_rule_indices[idx].1;
318                    &self.all_rules[start as usize..end as usize]
319                })
320                .unwrap_or(&[])
321        }
322    }
323
324    fn can_process_key_raw(&self, lower_key: char) -> bool {
325        if crate::utils::is_alpha(lower_key)
326            || (lower_key.is_ascii() && self.ascii_effect_keys[lower_key as usize])
327            || self.non_ascii_effect_keys.binary_search(&lower_key).is_ok()
328        {
329            return true;
330        }
331        if crate::utils::is_word_break_symbol(lower_key) {
332            return false;
333        }
334        crate::utils::is_vietnamese_rune(lower_key)
335    }
336
337    fn generate_transformations(
338        &self,
339        composition: &mut TransformationStack,
340        key: char,
341        is_upper_case: bool,
342    ) {
343        let lower_key = lower(key);
344        let mut trans_buf = TransformationStack::new();
345
346        crate::bamboo_util::generate_transformations(
347            composition.as_slice(),
348            self.get_applicable_rules(lower_key),
349            self.config.to_flags(),
350            lower_key,
351            is_upper_case,
352            &mut trans_buf,
353        );
354
355        if trans_buf.is_empty() {
356            crate::bamboo_util::generate_fallback_transformations(
357                self.get_applicable_rules(lower_key),
358                lower_key,
359                is_upper_case,
360                &mut trans_buf,
361            );
362
363            // Temporary combined data to avoid full struct copy
364            let combined_len = composition.len() + trans_buf.len();
365            if combined_len <= MAX_ACTIVE_TRANS {
366                let mut tmp_data = [Transformation::default(); MAX_ACTIVE_TRANS];
367                tmp_data[..composition.len()].copy_from_slice(composition.as_slice());
368                tmp_data[composition.len()..combined_len].copy_from_slice(trans_buf.as_slice());
369
370                if !self.input_method.super_keys.is_empty() {
371                    let current_str = crate::flattener::flatten_slice(
372                        &tmp_data[..combined_len],
373                        OutputOptions::TONE_LESS | OutputOptions::LOWER_CASE,
374                    );
375                    if uoh_tail_match(&current_str) {
376                        let (target, rule) = crate::bamboo_util::find_target(
377                            &tmp_data[..combined_len],
378                            self.get_applicable_rules(self.input_method.super_keys[0]),
379                            self.config.to_flags(),
380                        );
381                        if let (Some(target), Some(mut rule)) = (target, rule) {
382                            rule.key = '\0';
383                            trans_buf.push(Transformation {
384                                rule,
385                                target: Some(target),
386                                is_upper_case: false,
387                            });
388                        }
389                    }
390                }
391            }
392        }
393        composition.extend_from_slice(trans_buf.as_slice());
394        if self.config.to_flags() & crate::bamboo_util::EFREE_TONE_MARKING != 0
395            && self.is_valid_internal(composition.as_slice(), false)
396        {
397            let mut extra = TransformationStack::new();
398            crate::bamboo_util::refresh_last_tone_target_into(
399                composition.as_mut_slice(),
400                self.config.to_flags() & crate::bamboo_util::ESTD_TONE_STYLE != 0,
401                &mut extra,
402            );
403            composition.extend_from_slice(extra.as_slice());
404        }
405    }
406
407    fn last_syllable_start(composition: &[Transformation]) -> usize {
408        let mut idx = composition.len();
409        let mut last_is_vowel = false;
410        let mut found_vowel = false;
411
412        while idx > 0 {
413            let tmp = &composition[idx - 1];
414            if tmp.target.is_none() {
415                let is_v = crate::utils::is_vowel(tmp.rule.result);
416                if found_vowel && !is_v && !last_is_vowel {
417                    break;
418                }
419                if is_v {
420                    found_vowel = true;
421                }
422                last_is_vowel = is_v;
423            }
424            idx -= 1;
425        }
426
427        idx
428    }
429
430    fn new_composition_in_place(
431        &self,
432        composition: &mut TransformationStack,
433        scratch: &mut TransformationStack,
434        key: char,
435        is_upper_case: bool,
436    ) {
437        let syllable_abs_start = Self::last_syllable_start(composition.as_slice());
438
439        composition.drain_to(syllable_abs_start, scratch);
440
441        let offset = syllable_abs_start;
442        if offset != 0 {
443            for t in scratch.as_mut_slice().iter_mut() {
444                if let Some(target) = t.target {
445                    t.target = Some(target.saturating_sub(offset as u8));
446                }
447            }
448        }
449
450        self.generate_transformations(scratch, key, is_upper_case);
451
452        if offset != 0 {
453            for t in scratch.as_mut_slice().iter_mut() {
454                if let Some(target) = t.target {
455                    t.target = Some(target + offset as u8);
456                }
457            }
458        }
459
460        composition.extend_from_slice(scratch.as_slice());
461    }
462
463    /// Processes a string of characters and returns the resulting active word.
464    ///
465    /// This is a convenience wrapper around [`Self::process_str`] and [`Self::output`].
466    pub fn process(&mut self, s: &str, mode: Mode) -> String {
467        self.process_str(s, mode).output()
468    }
469
470    /// Processes a string of characters and returns a reference to the engine.
471    pub fn process_str(&mut self, s: &str, mode: Mode) -> &Self {
472        for key in s.chars() {
473            self.process_key(key, mode);
474        }
475        self
476    }
477
478    fn lcp_chars_and_bytes(a: &str, b: &str) -> (usize, usize) {
479        let mut lcp_chars = 0usize;
480        let mut lcp_bytes = 0usize;
481        for (ac, bc) in a.chars().zip(b.chars()) {
482            if ac == bc {
483                lcp_chars += 1;
484                lcp_bytes += ac.len_utf8();
485            } else {
486                break;
487            }
488        }
489        (lcp_chars, lcp_bytes)
490    }
491
492    /// Processes a single key and returns the "delta" change required for a text editor.
493    ///
494    /// This is useful for IMEs to update the preedit text efficiently without rewriting the entire word.
495    ///
496    /// # Returns
497    /// A tuple containing:
498    /// 1. `backspaces_chars`: Number of characters to delete from the end of the previous preedit.
499    /// 2. `backspaces_bytes`: Number of UTF-8 bytes to delete.
500    /// 3. `inserted`: The new string to append after deletion.
501    pub fn process_key_delta(&mut self, key: char, mode: Mode) -> (usize, usize, &str) {
502        self.process_key(key, mode);
503
504        let active_len = self.active_len;
505        let active = &self.active_buffer[..active_len];
506        crate::flattener::flatten_slice_into(active, OutputOptions::NONE, &mut self.delta_buf);
507
508        let (_lcp_chars, lcp_bytes) =
509            Self::lcp_chars_and_bytes(&self.prev_preedit, &self.delta_buf);
510
511        let prev_bytes = self.prev_preedit.len();
512
513        // Count only the suffix chars after the common prefix — O(suffix_len) instead of O(total).
514        let backspaces_chars = self.prev_preedit[lcp_bytes..].chars().count();
515        let backspaces_bytes = prev_bytes.saturating_sub(lcp_bytes);
516
517        std::mem::swap(&mut self.prev_preedit, &mut self.delta_buf);
518        let inserted = &self.prev_preedit[lcp_bytes..];
519        (backspaces_chars, backspaces_bytes, inserted)
520    }
521
522    /// Similar to [`Self::process_key_delta`], but writes the inserted string into a provided buffer.
523    ///
524    /// # Returns
525    /// The number of backspaces (characters) to perform.
526    pub fn process_key_delta_into(
527        &mut self,
528        key: char,
529        mode: Mode,
530        inserted: &mut String,
531    ) -> usize {
532        let (backspaces_chars, _backspaces_bytes, ins) = self.process_key_delta(key, mode);
533        inserted.clear();
534        inserted.push_str(ins);
535        backspaces_chars
536    }
537
538    /// Processes a single character.
539    ///
540    /// The `mode` determines whether to apply Vietnamese transformation rules.
541    pub fn process_key(&mut self, key: char, mode: Mode) {
542        let lower_key = lower(key);
543        let is_upper_case = is_upper(key);
544
545        // English mode: skip all Vietnamese processing.
546        // Direct buffer append — no DFA lookup, no snapshot.
547        if mode == Mode::English {
548            if crate::utils::is_word_break_symbol(lower_key) && self.active_len > 0 {
549                self.commit();
550            }
551            if self.active_len >= MAX_ACTIVE_TRANS {
552                self.commit();
553            }
554            self.active_buffer[self.active_len] =
555                crate::bamboo_util::new_appending_trans(lower_key, is_upper_case);
556            self.active_len += 1;
557            if crate::utils::is_word_break_symbol(lower_key) {
558                self.commit();
559            }
560            self.current_state_id = 0;
561            return;
562        }
563
564        // DFA Fast Path: if DFA has a cached transition, key is valid.
565        // Skip can_process_key_raw entirely.
566        if lower_key.is_ascii() && !is_upper_case {
567            let next_state_id =
568                self.dfa.get_state(self.current_state_id).transitions[lower_key as usize];
569            if next_state_id != 0 {
570                // Snapshot before overwriting active buffer (skip if empty — nothing to restore).
571                if self.active_len > 0 {
572                    self.push_snapshot();
573                }
574                self.current_state_id = next_state_id;
575                let comp = self.dfa.get_composition(next_state_id);
576                self.active_len = comp.len().min(MAX_ACTIVE_TRANS);
577                self.active_buffer[..self.active_len].copy_from_slice(comp);
578                return;
579            }
580        }
581
582        // Slow path: validate key and handle word breaks
583        if !self.can_process_key_raw(lower_key) {
584            if crate::utils::is_word_break_symbol(lower_key) {
585                self.commit();
586            }
587            // Snapshot before push_active so backspace can restore previous state.
588            // Word breaks trigger commit() which clears snapshots — that's correct
589            // (committed text can't be undone via backspace).
590            if self.active_len > 0 {
591                self.push_snapshot();
592            }
593            let trans = crate::bamboo_util::new_appending_trans(lower_key, is_upper_case);
594            self.push_active(trans);
595            if crate::utils::is_word_break_symbol(lower_key) {
596                self.commit();
597            }
598            self.current_state_id = 0;
599            return;
600        }
601
602        // Snapshot only needed before slow-path mutations (new_composition_in_place).
603        self.push_snapshot();
604
605        let mut work = self.work_comp;
606        let mut scratch = self.scratch_comp;
607
608        self.take_active_into(&mut work);
609        self.new_composition_in_place(&mut work, &mut scratch, lower_key, is_upper_case);
610
611        // Try to update DFA (Lazy JIT)
612        if lower_key.is_ascii() && !is_upper_case && work.len() <= MAX_ACTIVE_TRANS {
613            let next_id = self.dfa.add_state(work.as_slice());
614            self.dfa.states[self.current_state_id as usize].transitions[lower_key as usize] =
615                next_id;
616            self.current_state_id = next_id;
617        } else {
618            self.current_state_id = self.dfa.find_state(work.as_slice()).unwrap_or(0);
619        }
620
621        self.set_active_from_stack(&mut work);
622
623        self.work_comp = work;
624        self.scratch_comp = scratch;
625    }
626
627    fn push_active(&mut self, trans: Transformation) {
628        if self.active_len >= MAX_ACTIVE_TRANS {
629            // Buffer full, auto-commit to make room
630            self.commit();
631        }
632        self.active_buffer[self.active_len] = trans;
633        self.active_len += 1;
634        self.current_state_id = self.dfa.find_state(self.active_slice()).unwrap_or(0);
635    }
636
637    /// Clears the active syllable buffer and appends it to the committed text.
638    pub fn commit(&mut self) {
639        if self.active_len == 0 {
640            return;
641        }
642        let word = self.output();
643        self.committed_text.push_str(&word);
644        self.active_len = 0;
645        self.current_state_id = 0;
646        self.snapshot_len = 0;
647    }
648
649    /// Returns the currently active syllable as a string.
650    pub fn output(&self) -> String {
651        crate::flattener::flatten_slice(self.active_slice(), OutputOptions::NONE)
652    }
653
654    /// Returns the processed string according to the specified options.
655    ///
656    /// This can be used to get the full text (committed + active) or variations like toneless text.
657    pub fn get_processed_str(&self, options: OutputOptions) -> String {
658        let active = self.active_slice();
659        if options.contains(OutputOptions::FULL_TEXT) {
660            let mut result = self.committed_text.clone();
661            result.push_str(&crate::flattener::flatten_slice(active, options));
662            return result;
663        }
664        if options.contains(OutputOptions::PUNCTUATION_MODE) {
665            if active.is_empty() {
666                return String::new();
667            }
668            let (_, tail) = crate::bamboo_util::extract_last_word_with_punctuation_marks(
669                active,
670                &self.input_method.keys,
671            );
672            return crate::flattener::flatten_slice(tail, OutputOptions::NONE);
673        }
674        crate::flattener::flatten_slice(active, options)
675    }
676
677    /// Checks if the current composition forms a valid Vietnamese syllable.
678    pub fn is_valid(&self, input_is_full_complete: bool) -> bool {
679        self.is_valid_internal(self.active_slice(), input_is_full_complete)
680    }
681
682    fn is_valid_internal(
683        &self,
684        composition: &[Transformation],
685        input_is_full_complete: bool,
686    ) -> bool {
687        crate::bamboo_util::is_valid(composition, input_is_full_complete)
688    }
689
690    /// Restores the last word in the composition to its un-transformed state.
691    ///
692    /// If `to_vietnamese` is true, it attempts to re-apply Vietnamese transformations.
693    pub fn restore_last_word(&mut self, to_vietnamese: bool) {
694        let mut work = self.work_comp;
695
696        self.take_active_into(&mut work);
697        if work.is_empty() {
698            self.set_active_from_stack(&mut work);
699            self.current_state_id = 0;
700            return;
701        }
702
703        let (prev_slice, last) =
704            crate::bamboo_util::extract_last_word(work.as_slice(), Some(&self.input_method.keys));
705
706        let mut previous = TransformationStack::new();
707        previous.extend_from_slice(prev_slice);
708
709        if last.is_empty() {
710            self.set_active_from_stack(&mut work);
711            self.current_state_id = 0;
712            return;
713        }
714        if !to_vietnamese {
715            previous.extend_from_slice(&crate::bamboo_util::break_composition_slice(last));
716            self.set_active_from_stack(&mut previous);
717            self.current_state_id = 0;
718            return;
719        }
720
721        let mut new_comp = TransformationStack::new();
722        let mut temp_engine = Self::with_config(self.input_method.clone(), self.config);
723
724        for t in last {
725            if t.rule.key == '\0' {
726                continue;
727            }
728            temp_engine.process_key(t.rule.key, Mode::Vietnamese);
729        }
730        new_comp.extend_from_slice(temp_engine.active_slice());
731
732        previous.extend_from_slice(new_comp.as_slice());
733
734        self.set_active_from_stack(&mut previous);
735        self.current_state_id = 0;
736    }
737
738    /// Removes the last character from the active composition.
739    pub fn remove_last_char(&mut self, refresh_last_tone_target: bool) {
740        if self.pop_snapshot().is_none() {
741            return;
742        }
743
744        if refresh_last_tone_target && self.active_len > 0 {
745            let mut extra = TransformationStack::new();
746            crate::bamboo_util::refresh_last_tone_target_into(
747                &mut self.active_buffer[..self.active_len],
748                self.config.to_flags() & crate::bamboo_util::ESTD_TONE_STYLE != 0,
749                &mut extra,
750            );
751            for t in extra.as_slice() {
752                if self.active_len < MAX_ACTIVE_TRANS {
753                    self.active_buffer[self.active_len] = *t;
754                    self.active_len += 1;
755                }
756            }
757        }
758    }
759
760    /// Resets the engine state, clearing committed and active text.
761    pub fn reset(&mut self) {
762        self.committed_text.clear();
763        self.active_len = 0;
764        self.prev_preedit.clear();
765        self.delta_buf.clear();
766        self.current_state_id = 0;
767        self.snapshot_len = 0;
768    }
769}
770
771#[cfg(test)]
772mod tests {
773    use super::*;
774
775    #[test]
776    fn delta_backspaces_and_inserted() {
777        let telex = InputMethod::telex();
778        let mut e = Engine::new(telex);
779
780        let (bs1, _bb1, ins1) = e.process_key_delta('a', Mode::Vietnamese);
781        assert_eq!(bs1, 0, "First 'a' should have 0 backspaces");
782        assert_eq!(ins1, "a");
783
784        let (bs2, _bb2, ins2) = e.process_key_delta('s', Mode::Vietnamese);
785        assert_eq!(bs2, 1, "Adding 's' to 'a' should have 1 backspace for 'á'");
786        assert_eq!(ins2, "á");
787
788        let (bs3, _bb3, ins3) = e.process_key_delta(' ', Mode::Vietnamese);
789        assert_eq!(bs3, 1, "Space should clear the preedit 'á'");
790        assert_eq!(ins3, "");
791    }
792}