mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-10-05 15:09:42 +02:00
fix(input): deliver a recovered keystroke before the Enter that submits it
Every message typed on an Android phone lost its last character. An Android soft keyboard commits the last typed character and sends the Enter key in ONE InputConnection transaction, so the committed-text `input` event and the Enter keydown are both processed before any zero-delay timer runs. The orphaned-input recovery from #388 resolved its candidate only on such a timer, and that lost the character twice over: * ORDER — xterm emits `\r` synchronously from the Enter keydown, and the local-echo composer submits `pendingText` right there. The recovered character arrived one macrotask too late to be part of the prompt. * LOSS — that same `\r` bumps the canonical counter, so by the time the candidate resolved, `canonicalCount > snapshot` read as "xterm spoke for this keystroke" and stood the recovery down. The character was not merely late, it was dropped. Drain pending candidates synchronously at the next keydown instead, from xterm's custom key handler, which runs before xterm processes that key. The counter then still holds the value it had while the candidate's own keystroke was current, so the stand-down decision is made against the right keystroke, and the recovered byte reaches the composer ahead of whatever the new key emits. The timer stays as the fallback for a keystroke with no key after it. Physical keyboards are unaffected: there the timer has already resolved the candidate long before the next key arrives. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
88e3faa456
commit
a1c35da0d8
@@ -66,6 +66,38 @@
|
||||
let composing = false;
|
||||
const pending = [];
|
||||
|
||||
/**
|
||||
* Resolve every candidate still pending, right now, instead of waiting for
|
||||
* its zero-delay timer.
|
||||
*
|
||||
* Android soft keyboards commit the last character and send the Enter key
|
||||
* in ONE InputConnection transaction: the `input` event and the Enter
|
||||
* keydown are both processed before any timer runs. Left on its timer the
|
||||
* candidate lost BOTH ways — xterm emits '\r' synchronously from the Enter
|
||||
* keydown (so the local-echo composer submitted the prompt without the
|
||||
* character), and that '\r' bumps `canonicalCount`, so the candidate then
|
||||
* read "xterm spoke for this keystroke" and stood down, dropping the
|
||||
* character outright. That is the "every message loses its last character"
|
||||
* report from phones.
|
||||
*
|
||||
* Draining at the next keydown is correct on both counts: the counter still
|
||||
* holds the value it had while this candidate's keystroke was current, and
|
||||
* the byte reaches the composer ahead of whatever the new key emits.
|
||||
*/
|
||||
function flushPending() {
|
||||
for (const candidate of pending.splice(0)) {
|
||||
if (candidate.timer !== null) {
|
||||
try {
|
||||
clearTimer(candidate.timer);
|
||||
} catch {
|
||||
// A broken timer host must not break input handling.
|
||||
}
|
||||
candidate.timer = null;
|
||||
}
|
||||
resolveCandidate(candidate);
|
||||
}
|
||||
}
|
||||
|
||||
function cancelPending() {
|
||||
for (const candidate of pending.splice(0)) {
|
||||
candidate.active = false;
|
||||
@@ -111,6 +143,11 @@
|
||||
*/
|
||||
function handleKeyEvent(event) {
|
||||
if (destroyed || event?.type !== 'keydown') return;
|
||||
// Settle the PREVIOUS keystroke before this one can move the counter or
|
||||
// reach the PTY — see flushPending(). This runs from xterm's custom key
|
||||
// handler, i.e. before xterm processes the key, so a recovered character
|
||||
// is always ordered ahead of the bytes this keydown produces.
|
||||
flushPending();
|
||||
keydownSnapshot = canonicalCount;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user