export interface ComposerNativeEventSnapshot { readonly eventCount: number; readonly value: string; readonly selection: ComposerEditorSelection | null;} interface ComposerEditorSelection { readonly start: number; readonly end: number;} export function acknowledgeComposerNativeEvent( mostRecentEventCount: number, incomingEventCount: number,): number | null { if (!Number.isSafeInteger(incomingEventCount) || incomingEventCount < mostRecentEventCount) { return null; } return incomingEventCount;} export function resolveComposerControlledEventCount( value: string, selection: ComposerEditorSelection | null, mostRecentEventCount: number, snapshots: ReadonlyArray<ComposerNativeEventSnapshot>,): number { let newestValueEventCount: number | null = null; for (let index = snapshots.length - 1; index >= 0; index -= 1) { const snapshot = snapshots[index]; if (snapshot?.value !== value) continue; newestValueEventCount ??= snapshot.eventCount; if ( selection === null || (snapshot.selection?.start === selection.start && snapshot.selection.end === selection.end) ) { return snapshot.eventCount; } } // A value emitted by native paired with a different selection is an // intermediate React render. Keep it behind the native revision so it // cannot move the caret while newer keystrokes are being processed. if (newestValueEventCount !== null && mostRecentEventCount > 0) { return Math.min(newestValueEventCount, mostRecentEventCount - 1); } return mostRecentEventCount;} export function isComposerNativeEcho( value: string, selection: ComposerEditorSelection | null, eventCount: number, snapshots: ReadonlyArray<ComposerNativeEventSnapshot>,): boolean { for (let index = snapshots.length - 1; index >= 0; index -= 1) { const snapshot = snapshots[index]; if ( snapshot !== undefined && snapshot.eventCount === eventCount && snapshot.value === value && (selection === null || (snapshot.selection?.start === selection.start && snapshot.selection.end === selection.end)) ) { return true; } } return false;} export function pruneAcknowledgedComposerNativeEvents( snapshots: ReadonlyArray<ComposerNativeEventSnapshot>, acknowledgedEventCount: number,): ComposerNativeEventSnapshot[] { return snapshots.filter((snapshot) => snapshot.eventCount > acknowledgedEventCount);}
export interface ComposerNativeEventSnapshot {
readonly eventCount: number;
readonly value: string;
readonly selection: ComposerEditorSelection | null;
}
interface ComposerEditorSelection {
readonly start: number;
readonly end: number;
}
export function acknowledgeComposerNativeEvent(
mostRecentEventCount: number,
incomingEventCount: number,
): number | null {
if (!Number.isSafeInteger(incomingEventCount) || incomingEventCount < mostRecentEventCount) {
return null;
}
return incomingEventCount;
}
export function resolveComposerControlledEventCount(
value: string,
selection: ComposerEditorSelection | null,
mostRecentEventCount: number,
snapshots: ReadonlyArray<ComposerNativeEventSnapshot>,
): number {
let newestValueEventCount: number | null = null;
for (let index = snapshots.length - 1; index >= 0; index -= 1) {
const snapshot = snapshots[index];
if (snapshot?.value !== value) continue;
newestValueEventCount ??= snapshot.eventCount;
if (
selection === null ||
(snapshot.selection?.start === selection.start && snapshot.selection.end === selection.end)
) {
return snapshot.eventCount;
}
}
// A value emitted by native paired with a different selection is an
// intermediate React render. Keep it behind the native revision so it
// cannot move the caret while newer keystrokes are being processed.
if (newestValueEventCount !== null && mostRecentEventCount > 0) {
return Math.min(newestValueEventCount, mostRecentEventCount - 1);
}
return mostRecentEventCount;
}
export function isComposerNativeEcho(
value: string,
selection: ComposerEditorSelection | null,
eventCount: number,
snapshots: ReadonlyArray<ComposerNativeEventSnapshot>,
): boolean {
for (let index = snapshots.length - 1; index >= 0; index -= 1) {
const snapshot = snapshots[index];
if (
snapshot !== undefined &&
snapshot.eventCount === eventCount &&
snapshot.value === value &&
(selection === null ||
(snapshot.selection?.start === selection.start && snapshot.selection.end === selection.end))
) {
return true;
}
}
return false;
}
export function pruneAcknowledgedComposerNativeEvents(
snapshots: ReadonlyArray<ComposerNativeEventSnapshot>,
acknowledgedEventCount: number,
): ComposerNativeEventSnapshot[] {
return snapshots.filter((snapshot) => snapshot.eventCount > acknowledgedEventCount);
}