Merge pull request #4 from vndangkhoa/devin/1782470733-fix-flush-spacing-v2

The auto-restore merge reintroduced the spacing bug: on every flush char
the engine re-emitted a Replace and the daemon backspaced+retyped the
already-on-screen composed word, racing against the separately-forwarded
flush char and eating spaces (mất sự->mấtsự, đầu ngã xuống->đầungãxuống).

Now flush only backspaces+retypes when auto-restore actually changes the
word (English/invalid Vietnamese -> raw keystrokes). For a normal composed
word the engine returns None and the daemon types only the flush char,
leaving the correct word untouched on screen.

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-authored-by: vndangkhoa <vonguyendangkhoa@gmail.com>
This commit is contained in:
vndangkhoa 2026-06-26 17:48:39 +07:00 committed by GitHub
commit 575de7a5a5
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 32 additions and 31 deletions

View file

@ -285,15 +285,17 @@ impl Daemon {
fn replay_and_inject(&mut self, ch: char) -> Vec<OutputCommand> {
let mut commands = Vec::new();
// Flush characters: commit current word, type the character, clear state.
// The composed word is already correctly on screen, so we must NOT
// backspace and retype it — doing so eats the spacing and shifts the
// finished word left. Just type the flush char and clear state.
// Flush characters: commit the current word and type the flush char.
// Only backspace + retype when auto-restore actually CHANGES the word
// (English / invalid Vietnamese). For a normal composed word it is
// already correctly on screen, so retyping it would eat the spacing and
// shift the finished word left.
if is_flush_char(ch) {
if !self.screen_output.is_empty() {
let to_commit = self.word_to_commit();
if !self.screen_output.is_empty() && to_commit != self.screen_output {
let backspaces = self.screen_output.chars().count();
commands.push(OutputCommand::Backspace(backspaces));
commands.push(OutputCommand::Type(self.word_to_commit()));
commands.push(OutputCommand::Type(to_commit));
}
// Type the flush character itself
commands.push(OutputCommand::Type(ch.to_string()));
@ -317,16 +319,15 @@ impl Daemon {
);
if did_flush {
// Engine flushed a word — it is already correctly on screen, so
// just clear state without backspacing/retyping it (retyping eats
// spacing and shifts the finished word left).
// Engine flushed a word — commit it and clear state
// The flush char (space/period/etc) was NOT in history, so we need to
// type whatever was on screen + the flush char
if !self.screen_output.is_empty() {
// Engine flushed a word. Only backspace + retype when auto-restore
// actually CHANGES the word; otherwise the composed word is already
// correct on screen and retyping it eats spacing and shifts the
// finished word left.
let to_commit = self.word_to_commit();
if !self.screen_output.is_empty() && to_commit != self.screen_output {
let backspaces = self.screen_output.chars().count();
commands.push(OutputCommand::Backspace(backspaces));
commands.push(OutputCommand::Type(self.word_to_commit()));
commands.push(OutputCommand::Type(to_commit));
}
self.keystroke_history.clear();
self.screen_output.clear();

View file

@ -213,25 +213,21 @@ impl Engine {
let raw = self.raw_buffer.clone();
self.reset();
// The composed word is already correctly on screen — re-typing it
// here would trigger a redundant backspace + clipboard-paste cycle
// that races against the separately-forwarded flush char, eating
// spaces and merging words. Just finalize and let the flush char
// through untouched.
if prev_len > 0 {
// Auto-restore: if the committed word is English / not valid
// Vietnamese, revert to the raw keystrokes the user typed.
// Vietnamese, revert to the raw keystrokes the user typed. This
// genuinely changes the on-screen word, so a Replace is needed.
if self.auto_restore && Engine::should_restore_word(&previous, &raw) {
return Some(EngineEvent::Replace {
backspaces: prev_len,
insert: raw,
});
}
// Don't include flush char in insert — daemon forwards it separately
return Some(EngineEvent::Replace {
backspaces: prev_len,
insert: previous,
});
// Normal case: the composed word is already correctly on screen.
// Re-typing it would trigger a redundant backspace + retype that
// races against the separately-forwarded flush char, eating
// spaces and merging words. Finalize and let the flush char
// through untouched.
}
return None;
}

View file

@ -91,11 +91,15 @@ fn auto_restore_can_be_disabled() {
for ch in "cargo".chars() {
engine.process_key(ch);
}
// With auto-restore off the Vietnamese composition is kept on screen
// (no restore back to the raw English keystrokes).
assert_eq!(engine.buffer(), "cảgo");
// The composed word is already correct on screen, so flushing emits no
// Replace — re-typing it would race with the forwarded flush char and eat
// the spacing. (Contrast with auto-restore on, which emits Replace→"cargo".)
let event = engine.process_key(' ');
match event {
Some(vietc_engine::EngineEvent::Replace { insert, .. }) => {
assert_eq!(insert, "cảgo", "with auto-restore off the VN form is kept");
}
other => panic!("expected Replace to 'cảgo', got {other:?}"),
}
assert!(
event.is_none(),
"with auto-restore off the composed VN word stays untouched on flush, got {event:?}"
);
}