Merge pull request #15096 from keymanapp/refactor/web/textStoreInterface

refactor(web): extract minimal TextStore interface that LanguageProcessor can use 🎼
This commit is contained in:
Eberhard Beilharz 2025-11-06 08:07:29 +01:00 • committed by GitHub
commit e519d12032
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 44 additions and 21 deletions

View file

@ -1,6 +1,6 @@
import { LexicalModelTypes } from '@keymanapp/common-types';
import { EventEmitter } from "eventemitter3";
import { TextStore } from "keyman/engine/keyboard";
import { TextStoreLanguageProcessorInterface } from "keyman/engine/keyboard";
export class ReadySuggestions {
suggestions: LexicalModelTypes.Suggestion[];
@ -48,7 +48,7 @@ export interface LanguageProcessorEventMap {
* @param textStore The `TextStore` representation of the context the suggestion was applied to.
* @returns
*/
'suggestionapplied': (textStore: TextStore) => boolean
'suggestionapplied': (textStore: TextStoreLanguageProcessorInterface) => boolean
}
@ -56,7 +56,7 @@ export interface LanguageProcessorSpec extends EventEmitter<LanguageProcessorEve
get state(): StateChangeEnum;
invalidateContext(textStore: TextStore, layerId: string): Promise<LexicalModelTypes.Suggestion[]>;
invalidateContext(textStore: TextStoreLanguageProcessorInterface, layerId: string): Promise<LexicalModelTypes.Suggestion[]>;
/**
*
@ -66,9 +66,9 @@ export interface LanguageProcessorSpec extends EventEmitter<LanguageProcessorEve
* required because layerid can be changed by PostKeystroke
* @returns
*/
applySuggestion(suggestion: LexicalModelTypes.Suggestion, textStore: TextStore, getLayerId: () => string): Promise<LexicalModelTypes.Reversion>;
applySuggestion(suggestion: LexicalModelTypes.Suggestion, textStore: TextStoreLanguageProcessorInterface, getLayerId: () => string): Promise<LexicalModelTypes.Reversion>;
applyReversion(reversion: LexicalModelTypes.Reversion, textStore: TextStore): Promise<LexicalModelTypes.Suggestion[]>;
applyReversion(reversion: LexicalModelTypes.Reversion, textStore: TextStoreLanguageProcessorInterface): Promise<LexicalModelTypes.Suggestion[]>;
get wordbreaksAfterSuggestions(): boolean;

View file

@ -33,6 +33,7 @@ export { type VariableStore, VariableStoreSerializer, VariableStoreDictionary }
export { SyntheticTextStore } from "./syntheticTextStore.js";
export { TextStore } from "./textStore.js";
export { TextStoreLanguageProcessorInterface } from "./textStoreLanguageProcessorInterface.js";
export { findCommonSubstringEndIndex } from "./stringDivergence.js";
export { Deadkey } from "./deadkeys.js";

View file

@ -1,4 +1,5 @@
import { TextStore } from './textStore.js';
import { TextStoreLanguageProcessorInterface } from './textStoreLanguageProcessorInterface.js';
import { KMWString } from '@keymanapp/web-utils';
export class SyntheticTextStore extends TextStore {
@ -25,17 +26,11 @@ export class SyntheticTextStore extends TextStore {
this.selForward = this.selEnd >= this.selStart;
}
static assertIsOutputTargetBase(textStore: TextStore): asserts textStore is TextStore {
if (!(textStore instanceof TextStore)) {
throw new TypeError("textStore is not a TextStore");
}
}
// Clones the state of an existing EditableElement, creating a SyntheticTextStore version of its state.
static from(textStore: TextStore, readonly?: boolean): SyntheticTextStore {
static from(textStore: TextStoreLanguageProcessorInterface, readonly?: boolean): SyntheticTextStore {
let clone: SyntheticTextStore;
this.assertIsOutputTargetBase(textStore);
this.assertIsTextStore(textStore);
if (textStore instanceof SyntheticTextStore) {
// Avoids the need to run expensive kmwstring.ts `length()`

View file

@ -5,7 +5,8 @@ import { findCommonSubstringEndIndex } from "./stringDivergence.js";
import { SyntheticTextStore } from "./syntheticTextStore.js";
// Defines deadkey management in a manner attachable to each element interface.
import { type KeyEvent } from 'keyman/engine/keyboard';
import { type KeyEvent } from './keyEvent.js';
import { TextStoreLanguageProcessorInterface } from './textStoreLanguageProcessorInterface.js';
import { Deadkey, DeadkeyTracker } from "./deadkeys.js";
import { LexicalModelTypes } from '@keymanapp/common-types';
@ -59,6 +60,13 @@ export abstract class TextStore {
this._dks = dks.clone();
}
static assertIsTextStore(textStore: TextStoreLanguageProcessorInterface): asserts textStore is TextStore {
if (!(textStore instanceof TextStore)) {
throw new TypeError("textStore is not a TextStore");
}
}
/**
* Determines the basic operations needed to reconstruct the current TextStore's text from the prior state specified
* by another TextStore based on their text and caret positions.
@ -67,7 +75,9 @@ export abstract class TextStore {
* As such, it assumes that the caret is immediately after any inserted text.
* @param from An output target (preferably a SyntheticTextStore) representing the prior state of the input/output system.
*/
buildTransformFrom(original: TextStore): TextTransform {
buildTransformFrom(original: TextStoreLanguageProcessorInterface): TextTransform {
TextStore.assertIsTextStore(original);
const toLeft = this.getTextBeforeCaret();
const fromLeft = original.getTextBeforeCaret();

View file

@ -0,0 +1,17 @@
/*
* Keyman is copyright (C) SIL Global. MIT License.
*/
import { LexicalModelTypes } from '@keymanapp/common-types';
import { KeyEvent } from './keyEvent.js';
import { Transcription } from './keyboards/transcription.js';
import { Alternate } from './keyboards/textTransform.js';
/**
* Interface with the methods LanguageProcessor needs from TextStore
* for transcription building and applying transforms.
*/
export interface TextStoreLanguageProcessorInterface {
buildTranscriptionFrom(original: TextStoreLanguageProcessorInterface, keyEvent: KeyEvent, readonly: boolean, alternates?: Alternate[]): Transcription;
apply(transform: LexicalModelTypes.Transform): void;
}

View file

@ -1,6 +1,6 @@
import { EventEmitter } from "eventemitter3";
import { LMLayer, WorkerFactory } from "@keymanapp/lexical-model-layer/web";
import { Transcription, TextStore, SyntheticTextStore } from 'keyman/engine/keyboard';
import { Transcription, TextStoreLanguageProcessorInterface, SyntheticTextStore } from 'keyman/engine/keyboard';
import { LanguageProcessorEventMap, ModelSpec, StateChangeEnum, ReadySuggestions } from 'keyman/engine/interfaces';
import ContextWindow from "./contextWindow.js";
import { TranscriptionCache } from "./transcriptionCache.js";
@ -125,7 +125,7 @@ export class LanguageProcessor extends EventEmitter<LanguageProcessorEventMap> {
});
}
public invalidateContext(textStore: TextStore, layerId: string): Promise<Suggestion[]> {
public invalidateContext(textStore: TextStoreLanguageProcessorInterface, layerId: string): Promise<Suggestion[]> {
// If there's no active model, there can be no predictions.
// We'll also be missing important data needed to even properly REQUEST the predictions.
if(!this.currentModel || !this.configuration) {
@ -153,7 +153,7 @@ export class LanguageProcessor extends EventEmitter<LanguageProcessorEventMap> {
}
}
public wordbreak(textStore: TextStore, layerId: string): Promise<string> {
public wordbreak(textStore: TextStoreLanguageProcessorInterface, layerId: string): Promise<string> {
if(!this.isActive) {
return null;
}
@ -188,7 +188,7 @@ export class LanguageProcessor extends EventEmitter<LanguageProcessorEventMap> {
* required because layerid can be changed by PostKeystroke
* @returns
*/
public applySuggestion(suggestion: Suggestion, textStore: TextStore, getLayerId: ()=>string): Promise<Reversion> {
public applySuggestion(suggestion: Suggestion, textStore: TextStoreLanguageProcessorInterface, getLayerId: ()=>string): Promise<Reversion> {
if(!textStore) {
throw new Error("Accepting suggestions requires a destination TextStore instance.");
}
@ -263,7 +263,7 @@ export class LanguageProcessor extends EventEmitter<LanguageProcessorEventMap> {
}
}
public applyReversion(reversion: Reversion, textStore: TextStore) {
public applyReversion(reversion: Reversion, textStore: TextStoreLanguageProcessorInterface) {
if(!textStore) {
throw new Error("Accepting suggestions requires a destination TextStore instance.");
}
@ -305,7 +305,7 @@ export class LanguageProcessor extends EventEmitter<LanguageProcessorEventMap> {
return promise;
}
public predictFromTarget(textStore: TextStore, layerId: string): Promise<Suggestion[]> {
public predictFromTarget(textStore: TextStoreLanguageProcessorInterface, layerId: string): Promise<Suggestion[]> {
if(!this.isActive || !textStore) {
return null;
}