Fix spacing bug: stop retyping finished word on flush char

The flush-char handling backspaced and re-typed the already-on-screen
word before/around the forwarded space. In the grabbed-device injection
path this raced against the separately-forwarded space, eating spaces
and merging finished words (e.g. "mất sự" -> "mấtsự",
"đầu ngã xuống" -> "đầungãxuống").

The composed word is already correct on screen, so a non-macro flush
now finalizes state without backspace+retype:
- engine: process_key returns None on flush (macros still Replace)
- daemon replay_and_inject: just types the flush char
- daemon did_flush branch: clears state without retyping

Add regression tests for flush behavior and multi-word spacing.

Co-Authored-By: vndangkhoa <vonguyendangkhoa@gmail.com>
This commit is contained in:
Devin AI 2026-06-26 10:38:54 +00:00
parent 0770cc59cc
commit bbd273bdd6
3 changed files with 60 additions and 22 deletions

View file

@ -282,14 +282,11 @@ 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
// 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.
if is_flush_char(ch) {
if !self.screen_output.is_empty() {
let backspaces = self.screen_output.chars().count();
commands.push(OutputCommand::Backspace(backspaces));
commands.push(OutputCommand::Type(self.screen_output.clone()));
}
// Type the flush character itself
commands.push(OutputCommand::Type(ch.to_string()));
self.keystroke_history.clear();
self.screen_output.clear();
@ -311,14 +308,9 @@ impl Daemon {
);
if did_flush {
// 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() {
let backspaces = self.screen_output.chars().count();
commands.push(OutputCommand::Backspace(backspaces));
commands.push(OutputCommand::Type(self.screen_output.clone()));
}
// 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).
self.keystroke_history.clear();
self.screen_output.clear();
return commands;

View file

@ -172,13 +172,11 @@ impl Engine {
}
self.reset();
if prev_len > 0 {
// Don't include flush char in insert — daemon forwards it separately
return Some(EngineEvent::Replace {
backspaces: prev_len,
insert: previous,
});
}
// 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.
return None;
}

View file

@ -455,4 +455,52 @@ mod tests {
e.set_method(InputMethod::Vni);
assert_eq!(get_display(&process_input(&mut e, "a1")), "á");
}
// ================================================================
// Spacing / flush behavior (regression)
// ================================================================
// A space after a finished word must NOT re-emit the word as a Replace
// (backspace + retype). Re-typing the already-on-screen word races with
// the separately-forwarded space in the daemon, eating spaces and merging
// words (e.g. "mất sự" -> "mấtsự"). The flush should produce no engine
// event so the space simply passes through.
#[test]
fn flush_after_word_emits_no_replace() {
let mut e = Engine::new(InputMethod::Telex);
// Compose "chào".
for ch in "chaof".chars() {
e.process_key(ch);
}
// Space finalizes the word — engine must return None.
assert_eq!(e.process_key(' '), None);
}
// Punctuation flush chars behave the same as space.
#[test]
fn flush_punctuation_emits_no_replace() {
let mut e = Engine::new(InputMethod::Telex);
for ch in "chaof".chars() {
e.process_key(ch);
}
assert_eq!(e.process_key('.'), None);
}
// Full multi-word sentence keeps every space and never concatenates words.
#[test]
fn multi_word_keeps_spacing() {
let mut e = Engine::new(InputMethod::Telex);
// "toio is" with telex: "tooi" -> "tôi"; "ddi" -> "đi"
let events = process_input(&mut e, "tooi ddi hocj ");
assert_eq!(get_display(&events), "tôi đi học ");
}
// A macro flush still expands (Replace) and keeps the trailing space.
#[test]
fn macro_flush_still_replaces() {
let mut e = Engine::new(InputMethod::Telex);
e.add_macro("vn".into(), "Việt Nam".into());
let events = process_input(&mut e, "vn ");
assert_eq!(get_display(&events), "Việt Nam ");
}
}