diff --git a/common/web/input-processor/src/text/inputProcessor.ts b/common/web/input-processor/src/text/inputProcessor.ts index 80821bc76d..8dd45bc774 100644 --- a/common/web/input-processor/src/text/inputProcessor.ts +++ b/common/web/input-processor/src/text/inputProcessor.ts @@ -22,6 +22,7 @@ import { SystemStoreIDs, type TextTransform } from "@keymanapp/keyboard-processor"; +import { TranscriptionCache } from "../transcriptionCache.js"; export default class InputProcessor { public static readonly DEFAULT_OPTIONS: ProcessorInitOptions = { @@ -37,6 +38,8 @@ export default class InputProcessor { private kbdProcessor: KeyboardProcessor; private lngProcessor: LanguageProcessor; + private readonly contextCache = new TranscriptionCache(); + constructor(device: DeviceSpec, predictiveTextWorker: Worker, options?: ProcessorInitOptions) { if(!device) { throw new Error('device must be defined'); @@ -48,7 +51,7 @@ export default class InputProcessor { this.contextDevice = device; this.kbdProcessor = new KeyboardProcessor(device, options); - this.lngProcessor = new LanguageProcessor(predictiveTextWorker); + this.lngProcessor = new LanguageProcessor(predictiveTextWorker, this.contextCache); } public get languageProcessor(): LanguageProcessor { @@ -215,6 +218,10 @@ export default class InputProcessor { ruleBehavior.triggersDefaultCommand = true; } + // Multitaps operate in part by referencing 'committed' Transcriptions to rewind + // the context as necessary. + this.contextCache.save(ruleBehavior.transcription); + // The keyboard may want to take an action after all other keystroke processing is // finished, for example to switch layers. This action may not have any output // but may change system store or variable store values. Given this, we don't need to diff --git a/common/web/input-processor/src/text/prediction/languageProcessor.ts b/common/web/input-processor/src/text/prediction/languageProcessor.ts index 58c6e42993..eecf82dc05 100644 --- a/common/web/input-processor/src/text/prediction/languageProcessor.ts +++ b/common/web/input-processor/src/text/prediction/languageProcessor.ts @@ -3,6 +3,7 @@ import { LMLayer } from "@keymanapp/lexical-model-layer/web"; import { OutputTarget, Transcription, Mock } from "@keymanapp/keyboard-processor"; import ContextWindow from "../contextWindow.js"; import ModelSpec from "./modelSpec.js" +import { TranscriptionCache } from "../../transcriptionCache.js"; /** * Corresponds to the 'suggestionsready' LanguageProcessor event. @@ -60,18 +61,18 @@ export default class LanguageProcessor extends EventEmitter; - private recentTranscriptions: Transcription[] = []; + private readonly recentTranscriptions: TranscriptionCache; private _mayPredict: boolean = true; private _mayCorrect: boolean = true; private _state: StateChangeEnum = 'inactive'; - private static readonly TRANSCRIPTION_BUFFER: 10 = 10; - - public constructor(predictiveTextWorker: Worker, supportsRightDeletions: boolean = false) { + public constructor(predictiveTextWorker: Worker, transcriptionCache: TranscriptionCache, supportsRightDeletions: boolean = false) { super(); + this.recentTranscriptions = transcriptionCache; + // Establishes KMW's platform 'capabilities', which limit the range of context a LMLayer // model may expect. let capabilities: Capabilities = { @@ -357,11 +358,7 @@ export default class LanguageProcessor extends EventEmitter LanguageProcessor.TRANSCRIPTION_BUFFER) { - this.recentTranscriptions.splice(0, 1); - } + this.recentTranscriptions.save(transcription); } /** @@ -372,11 +369,7 @@ export default class LanguageProcessor extends EventEmitter { - return t.token == id; - }) - - return match.length == 0 ? null : match[0]; + return this.recentTranscriptions.get(id); } public shutdown() { diff --git a/common/web/input-processor/src/transcriptionCache.ts b/common/web/input-processor/src/transcriptionCache.ts new file mode 100644 index 0000000000..c173009171 --- /dev/null +++ b/common/web/input-processor/src/transcriptionCache.ts @@ -0,0 +1,37 @@ +import { Transcription } from "@keymanapp/keyboard-processor"; + +const TRANSCRIPTION_BUFFER_SIZE = 10; + +export class TranscriptionCache { + private readonly map = new Map(); + + public get(key: number) { + const value = this.map.get(key); + + // Update the entry's 'age' / position in the keys() ordering. + if(value) { + this.save(value); + } + + return value; + } + + public save(value: Transcription) { + const key = value.token >= 0 ? value.token : -value.token; + + // Resets the key's ordering in Map.keys. + this.map.delete(key); + this.map.set(key, value); + + if(this.map.size > TRANSCRIPTION_BUFFER_SIZE) { + /* Deletes the oldest entry. As per the specification of `Map.keys()`, the keys are in + * insertion order. The earlier `map.delete` call resets a key's position in the list, + * ensuring index 0 corresponds to the entry least-recently referenced. + * + * See also: + * https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map/keys + */ + this.map.delete(this.map.keys().next().value); + } + } +} \ No newline at end of file