mirror of
https://github.com/keymanapp/keyman.git
synced 2026-08-27 18:57:42 +00:00
feat(web): add Web engine support for auto-reverting whitespace appended to Suggestions on punctuation input
Relates-to: #7163 Relates-to: #12013 This does not outright _fix_ them because we still need to add the ability to set language-specific punctuation mark sets within the model, and the model needs to use those to return an appropriate configuration. (This commit sets defaults that are English-centric and do not generalize to all languages.)
This commit is contained in:
parent
c5892ab843
commit
2c1c46e0dd
17 changed files with 351 additions and 143 deletions
|
|
@ -531,14 +531,28 @@ export interface Configuration {
|
|||
rightContextCodeUnits?: number,
|
||||
|
||||
/**
|
||||
* Whether or not the model appends characters to Suggestions for
|
||||
* wordbreaking purposes. (These characters need not be whitespace
|
||||
* or actual wordbreak characters.)
|
||||
* Specifies behaviors related to transforms that the active model appends
|
||||
* to Suggestions for wordbreaking purposes. (The Transforms need not apply
|
||||
* whitespace or actual wordbreak characters.)
|
||||
*
|
||||
* If not specified, this will be auto-detected based on the model's
|
||||
* punctuation properties (if they exist).
|
||||
* punctuation properties (if they exist). If left null/undefined, the model
|
||||
* does not append wordbreaking transforms to Suggestions.
|
||||
*/
|
||||
wordbreaksAfterSuggestions?: boolean
|
||||
appendsWordbreaks?: {
|
||||
/**
|
||||
* Specifies strings that, when input, always act as word-boundaries on the
|
||||
* input - both when typed after a manually-applied suggestion (replacing
|
||||
* appended whitespace) and when typed with an auto-selected suggestion
|
||||
* available (thus accepting it directly, as with whitespace).
|
||||
*
|
||||
* This is designed to allow language-appropriate punctuation marks to
|
||||
* automatically remove whitespace (or other wordbreak characters) as
|
||||
* appropriate, and to auto-accept for inputs that clearly signal intent
|
||||
* to end the current word, both in order to improve user UX with autocorrect.
|
||||
*/
|
||||
breakingMarks?: string[];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -166,6 +166,10 @@ builder_run_child_actions build:engine/keyboard
|
|||
builder_run_child_actions build:engine/js-processor
|
||||
builder_run_child_actions build:engine/element-wrappers
|
||||
builder_run_child_actions build:engine/events
|
||||
|
||||
# Builds the predictive-text components
|
||||
builder_run_child_actions build:engine/predictive-text
|
||||
|
||||
builder_run_child_actions build:engine/interfaces
|
||||
|
||||
# Uses engine/dom-utils and engine/interfaces
|
||||
|
|
@ -177,9 +181,6 @@ builder_run_child_actions build:engine/attachment
|
|||
# Uses engine/interfaces (due to resource-path config interface)
|
||||
builder_run_child_actions build:engine/keyboard-storage
|
||||
|
||||
# Builds the predictive-text components
|
||||
builder_run_child_actions build:engine/predictive-text
|
||||
|
||||
# Uses engine/interfaces, engine/keyboard-storage, engine/predictive-text, & engine/osk
|
||||
builder_run_child_actions build:engine/main
|
||||
|
||||
|
|
|
|||
|
|
@ -70,7 +70,7 @@ export interface LanguageProcessorSpec extends EventEmitter<LanguageProcessorEve
|
|||
|
||||
applyReversion(reversion: LexicalModelTypes.Reversion, outputTarget: OutputTarget): Promise<LexicalModelTypes.Suggestion[]>;
|
||||
|
||||
get wordbreaksAfterSuggestions(): boolean;
|
||||
get wordbreaksAfterSuggestions(): LexicalModelTypes.Configuration['appendsWordbreaks'];
|
||||
|
||||
get mayAutoCorrect(): boolean;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -23,11 +23,24 @@ export default class PredictionContext extends EventEmitter<PredictionContextEve
|
|||
private initNewContext: boolean = true;
|
||||
|
||||
private _currentSuggestions: Suggestion[] = [];
|
||||
private keepSuggestion: Keep;
|
||||
private revertSuggestion: Reversion;
|
||||
private _keepSuggestion: Keep;
|
||||
|
||||
public get keepSuggestion(): Keep {
|
||||
return this._keepSuggestion;
|
||||
}
|
||||
|
||||
private _revertSuggestion: Reversion;
|
||||
|
||||
public get revertSuggestion(): Reversion {
|
||||
return this._revertSuggestion;
|
||||
}
|
||||
|
||||
// Set to null/undefined if there was no recent acceptance.
|
||||
private recentAcceptCause: 'key' | 'banner';
|
||||
private _recentAcceptCause: 'key' | 'banner';
|
||||
|
||||
public get recentAcceptCause(): 'key' | 'banner' {
|
||||
return this._recentAcceptCause;
|
||||
}
|
||||
private revertAcceptancePromise: Promise<Reversion>;
|
||||
|
||||
private swallowPrediction: boolean = false;
|
||||
|
|
@ -161,8 +174,6 @@ export default class PredictionContext extends EventEmitter<PredictionContextEve
|
|||
* @returns if `suggestion` is a `Suggestion`, will return a `Promise<Reversion>`; else, `null`.
|
||||
*/
|
||||
public accept(suggestion: Suggestion): Promise<Reversion> | Promise<null> {
|
||||
const _this = this;
|
||||
|
||||
// Selecting a suggestion or a reversion should both clear selection
|
||||
// and clear the reversion-displaying state of the banner.
|
||||
this.selected = null;
|
||||
|
|
@ -173,23 +184,23 @@ export default class PredictionContext extends EventEmitter<PredictionContextEve
|
|||
// We get here either if suggestion acceptance fails or if it was a reversion.
|
||||
if(suggestion && suggestion.tag == 'revert') {
|
||||
// Reversion state management
|
||||
this.recentAcceptCause = null;
|
||||
this._recentAcceptCause = null;
|
||||
this.recentRevert = true;
|
||||
}
|
||||
|
||||
return Promise.resolve(null);
|
||||
}
|
||||
|
||||
this.revertAcceptancePromise.then(function(suggestion) {
|
||||
this.revertAcceptancePromise.then((suggestion) => {
|
||||
// Always null-check!
|
||||
if(suggestion) {
|
||||
_this.revertSuggestion = suggestion;
|
||||
this._revertSuggestion = suggestion;
|
||||
}
|
||||
});
|
||||
|
||||
// By default, we assume we were triggered by the banner.
|
||||
// Acceptance by keystroke will overwrite this later (in `tryAccept`)
|
||||
this.recentAcceptCause = 'banner';
|
||||
this._recentAcceptCause = 'banner';
|
||||
this.recentRevert = false;
|
||||
|
||||
this.swallowPrediction = true;
|
||||
|
|
@ -219,9 +230,9 @@ export default class PredictionContext extends EventEmitter<PredictionContextEve
|
|||
|
||||
// doTryAccept is the path for keystroke-based auto-acceptance.
|
||||
// Overwrite the cause to reflect this.
|
||||
this.recentAcceptCause = 'key';
|
||||
this._recentAcceptCause = 'key';
|
||||
} else if(recentAcceptCause && source == 'space') {
|
||||
this.recentAcceptCause = null;
|
||||
this._recentAcceptCause = null;
|
||||
if(recentAcceptCause == 'key') {
|
||||
// No need to swallow the keystroke's whitespace; we triggered the prior acceptance
|
||||
// FROM a space, so we've already aliased the suggestion's built-in space.
|
||||
|
|
@ -251,7 +262,7 @@ export default class PredictionContext extends EventEmitter<PredictionContextEve
|
|||
if(this.doRevert) {
|
||||
// If so, clear the 'revert' option and start doing normal predictions again.
|
||||
this.doRevert = false;
|
||||
this.recentAcceptCause = null;
|
||||
this._recentAcceptCause = null;
|
||||
// Otherwise, did we just accept something before the revert signal was received?
|
||||
} else if(this.recentAcceptCause) {
|
||||
this.showRevert();
|
||||
|
|
@ -274,7 +285,7 @@ export default class PredictionContext extends EventEmitter<PredictionContextEve
|
|||
this.selected = null;
|
||||
|
||||
if(!this.swallowPrediction || source == 'context') {
|
||||
this.recentAcceptCause = null;
|
||||
this._recentAcceptCause = null;
|
||||
this.doRevert = false;
|
||||
this.recentRevert = false;
|
||||
|
||||
|
|
@ -318,10 +329,10 @@ export default class PredictionContext extends EventEmitter<PredictionContextEve
|
|||
|
||||
// Do we have a keep suggestion? If so, remove it from the list so that we can control its display position
|
||||
// and prevent it from being hidden after reversion operations.
|
||||
this.keepSuggestion = null;
|
||||
this._keepSuggestion = null;
|
||||
for (const s of suggestions) {
|
||||
if(s.tag == 'keep') {
|
||||
this.keepSuggestion = s as Keep;
|
||||
this._keepSuggestion = s as Keep;
|
||||
}
|
||||
|
||||
if (this.langProcessor.mayAutoCorrect && s.autoAccept && !this.selected) {
|
||||
|
|
@ -335,7 +346,7 @@ export default class PredictionContext extends EventEmitter<PredictionContextEve
|
|||
|
||||
// If we've gotten an update request like this, it's almost always user-triggered and means the context has shifted.
|
||||
if(!this.swallowPrediction) {
|
||||
this.recentAcceptCause = null;
|
||||
this._recentAcceptCause = null;
|
||||
this.doRevert = false;
|
||||
this.recentRevert = false;
|
||||
} else { // This prediction was triggered by a recent 'accept.' Now that it's fulfilled, we clear the flag.
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
import ContextWindow from "./contextWindow.js";
|
||||
import { LanguageProcessor } from "./languageProcessor.js";
|
||||
import type { ModelSpec } from "keyman/engine/interfaces";
|
||||
import type { ModelSpec, PredictionContext } from "keyman/engine/interfaces";
|
||||
import { globalObject, DeviceSpec } from "@keymanapp/web-utils";
|
||||
|
||||
import { Codes, type Keyboard, type KeyEvent } from "keyman/engine/keyboard";
|
||||
|
|
@ -15,13 +15,24 @@ import {
|
|||
type OutputTarget,
|
||||
RuleBehavior,
|
||||
type ProcessorInitOptions,
|
||||
SystemStoreIDs
|
||||
SystemStoreIDs,
|
||||
TextTransform
|
||||
} from 'keyman/engine/js-processor';
|
||||
|
||||
import { TranscriptionCache } from "./transcriptionCache.js";
|
||||
import { LexicalModelTypes } from '@keymanapp/common-types';
|
||||
import { WorkerFactory } from "@keymanapp/lexical-model-layer";
|
||||
|
||||
// Only consider raw-insertion transforms. Delete-left and delete-right disqualify an
|
||||
// incoming transform from reverting post-suggestion whitespace (or similar).
|
||||
const transformMatchesPattern = (transform: TextTransform, breakingMarks: string[]) => {
|
||||
if(transform.deleteLeft || transform.deleteRight) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return breakingMarks.find((pattern) => transform.insert == pattern);
|
||||
}
|
||||
|
||||
export class InputProcessor {
|
||||
public static readonly DEFAULT_OPTIONS: ProcessorInitOptions = {
|
||||
baseLayout: 'us'
|
||||
|
|
@ -87,10 +98,12 @@ export class InputProcessor {
|
|||
*
|
||||
* @param {Object} keyEvent The abstracted KeyEvent to use for keystroke processing
|
||||
* @param {Object} outputTarget The OutputTarget receiving the KeyEvent
|
||||
* @param {Object} predictionContext Context for the state of all predictive-text
|
||||
* interactions associated with `outputTarget`
|
||||
* @returns {Object} A RuleBehavior object describing the cumulative effects of
|
||||
* all matched keyboard rules.
|
||||
*/
|
||||
processKeyEvent(keyEvent: KeyEvent, outputTarget: OutputTarget): RuleBehavior {
|
||||
processKeyEvent(keyEvent: KeyEvent, outputTarget: OutputTarget, predictionContext: PredictionContext): RuleBehavior {
|
||||
const kbdMismatch = keyEvent.srcKeyboard && this.activeKeyboard != keyEvent.srcKeyboard;
|
||||
const trueActiveKeyboard = this.activeKeyboard;
|
||||
|
||||
|
|
@ -127,7 +140,7 @@ export class InputProcessor {
|
|||
}
|
||||
}
|
||||
|
||||
return this._processKeyEvent(keyEvent, outputTarget);
|
||||
return this._processKeyEvent(keyEvent, outputTarget, predictionContext);
|
||||
} finally {
|
||||
if(kbdMismatch) {
|
||||
// Restore our "current" activeKeyboard to its setting before the mismatching KeyEvent.
|
||||
|
|
@ -143,7 +156,7 @@ export class InputProcessor {
|
|||
* @param outputTarget
|
||||
* @returns
|
||||
*/
|
||||
private _processKeyEvent(keyEvent: KeyEvent, outputTarget: OutputTarget): RuleBehavior {
|
||||
private _processKeyEvent(keyEvent: KeyEvent, outputTarget: OutputTarget, predictionContext: PredictionContext): RuleBehavior {
|
||||
const formFactor = keyEvent.device.formFactor;
|
||||
const fromOSK = keyEvent.isSynthetic;
|
||||
|
||||
|
|
@ -177,9 +190,11 @@ export class InputProcessor {
|
|||
if((keyEvent.kName == "K_BKSP" || keyEvent.Lcode == Codes.keyCodes["K_BKSP"]) && this.languageProcessor.tryRevertSuggestion()) {
|
||||
return new RuleBehavior();
|
||||
// Can the suggestion UI accept an existing suggestion? If so, do that and swallow the space character.
|
||||
// TODO: consider auto-application based on auto-reverting punctuation?
|
||||
} else if((keyEvent.kName == "K_SPACE" || keyEvent.Lcode == Codes.keyCodes["K_SPACE"]) && this.languageProcessor.tryAcceptSuggestion('space')) {
|
||||
return new RuleBehavior();
|
||||
}
|
||||
// checks to potentially cancel out previously-appended whitespace need to evaluate rules first.
|
||||
}
|
||||
|
||||
// // ...end I3363 (Build 301)
|
||||
|
|
@ -194,6 +209,11 @@ export class InputProcessor {
|
|||
// needed for some indexing operations when comparing two different output targets.
|
||||
let ruleBehavior = this.keyboardProcessor.processKeystroke(keyEvent, outputTarget);
|
||||
|
||||
// Check to see if the incoming keystroke should revert the appended component of an
|
||||
// immediately-preceding applied Suggestion.
|
||||
ruleBehavior = this.doPredictiveAutoBreaking(ruleBehavior, outputTarget, predictionContext) || ruleBehavior;
|
||||
// TODO: if it doesn't revert something, could it accept something?
|
||||
|
||||
// Swap layer as appropriate.
|
||||
if(keyEvent.kNextLayer) {
|
||||
this.keyboardProcessor.selectLayer(keyEvent);
|
||||
|
|
@ -219,9 +239,9 @@ export class InputProcessor {
|
|||
isOnlyLayerSwitchKey = true;
|
||||
}
|
||||
|
||||
const keepRuleBehavior = ruleBehavior != null;
|
||||
const haveValidKeyRuleBehavior = ruleBehavior != null;
|
||||
// Should we swallow any further processing of keystroke events for this keydown-keypress sequence?
|
||||
if(keepRuleBehavior) {
|
||||
if(haveValidKeyRuleBehavior) {
|
||||
// alternates are our fat-finger alternate outputs. We don't build these for keys we detect as
|
||||
// layer switch keys
|
||||
const alternates = isOnlyLayerSwitchKey ? null : this.buildAlternates(ruleBehavior, keyEvent, preInputMock);
|
||||
|
|
@ -272,7 +292,75 @@ export class InputProcessor {
|
|||
outputTarget.doInputEvent();
|
||||
}
|
||||
|
||||
return keepRuleBehavior ? ruleBehavior : null;
|
||||
return haveValidKeyRuleBehavior ? ruleBehavior : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements one predictive-text behavior:
|
||||
*
|
||||
* When a suggestion is manually applied and then followed by input of a
|
||||
* model-defined "wordbreaking mark", any usual appended (usually, whitespace)
|
||||
* transform will be reverted and replaced with the "wordbreaking mark".
|
||||
*
|
||||
* @param ruleBehavior The rule behavior from the keyboard for the incoming keystroke
|
||||
* @param outputTarget The context source affected by the incoming keystroke
|
||||
* @param predictionContext The "prediction context" corresponding to the context source.
|
||||
* @returns If an auto-correct behavior is triggered, returns a new `RuleBehavior` instance
|
||||
* for the keystroke (as the location of its application may change). If no autocorrect
|
||||
* behaviors are triggered, returns `null`.
|
||||
*/
|
||||
private doPredictiveAutoBreaking(
|
||||
ruleBehavior: RuleBehavior,
|
||||
outputTarget: OutputTarget,
|
||||
predictionContext: PredictionContext
|
||||
): RuleBehavior {
|
||||
// If there's no prediction-context instance or predictive-text available, this function is a no-op.
|
||||
if(!predictionContext || !this.languageProcessor.isActive) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Do we have an active model that appends wordbreaking transforms to suggestions...
|
||||
// and might it support reverting them or auto-applying them? Finally, does the
|
||||
// incoming Transform match a mark that would activate this behavior?
|
||||
const breakingMarks = this.languageProcessor.wordbreaksAfterSuggestions?.breakingMarks;
|
||||
const ruleTransform = ruleBehavior.transcription.transform;
|
||||
if(!breakingMarks || !transformMatchesPattern(ruleTransform, breakingMarks)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const keyEvent = ruleBehavior.transcription.keystroke;
|
||||
|
||||
// ...and is this immediately after a Suggestion with an appended Transform was applied?
|
||||
// (If not, don't consider reverting an appended transform.)
|
||||
if(predictionContext.revertSuggestion?.appendedTransform && predictionContext.recentAcceptCause == 'banner') {
|
||||
const reversion = predictionContext.revertSuggestion;
|
||||
// For reversions, the appended transform exists (if it did for the applied suggestion)
|
||||
// and has an ID set to the appended transform from the suggestion.
|
||||
const base = this.contextCache.get(reversion.appendedTransform.id);
|
||||
const postRevertBehavior = this.keyboardProcessor.processKeystroke(keyEvent, Mock.from(base.preInput));
|
||||
const postRevertTransform = postRevertBehavior.transcription.transform;
|
||||
|
||||
// Does the rule produce the same text & still match one of the breaking-mark patterns?
|
||||
// (with no deleteLeft, etc)
|
||||
if(postRevertTransform.insert == ruleTransform.insert && transformMatchesPattern(postRevertTransform, breakingMarks)) {
|
||||
// Then auto-revert the appended transform...
|
||||
const targetContext = Mock.from(base.preInput);
|
||||
targetContext.apply(postRevertBehavior.transcription.transform);
|
||||
|
||||
// Update the worker's context! (Which also wants to revert internally, so do it before
|
||||
// applying the final editing transform to the true output target!)
|
||||
this.languageProcessor.applyReversion(reversion, outputTarget, true);
|
||||
|
||||
// ... & use the new result as the final form of the context
|
||||
const transform = targetContext.buildTransformFrom(outputTarget);
|
||||
outputTarget.apply(transform);
|
||||
|
||||
// And overwrite the rule behavior transform with the form from the reverted context.
|
||||
return postRevertBehavior;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private buildAlternates(ruleBehavior: RuleBehavior, keyEvent: KeyEvent, preInputMock: Mock): Alternate[] {
|
||||
|
|
|
|||
|
|
@ -210,66 +210,76 @@ export class LanguageProcessor extends EventEmitter<LanguageProcessorEventMap> {
|
|||
if(!original) {
|
||||
console.warn("Could not apply the Suggestion!");
|
||||
return null;
|
||||
} else {
|
||||
this.recentTranscriptions.rewindTo(suggestion.transformId);
|
||||
// Apply the Suggestion!
|
||||
|
||||
// Step 1: determine the final output text
|
||||
const final = Mock.from(original.preInput, false);
|
||||
final.apply(suggestion.transform);
|
||||
if(suggestion.appendedTransform) {
|
||||
final.apply(suggestion.appendedTransform);
|
||||
}
|
||||
|
||||
// Step 2: build a final, master Transform that will produce the desired results from the CURRENT state.
|
||||
// In embedded mode, both Android and iOS are best served by calculating this transform and applying its
|
||||
// values as needed for use with their IME interfaces.
|
||||
const transform = final.buildTransformFrom(outputTarget);
|
||||
outputTarget.apply(transform);
|
||||
|
||||
// Tell the banner that a suggestion was applied, so it can call the
|
||||
// keyboard's PostKeystroke entry point as needed
|
||||
this.emit('suggestionapplied', outputTarget);
|
||||
|
||||
// Build a 'reversion' Transcription that can be used to undo this apply() if needed,
|
||||
// replacing the suggestion transform with the original input text.
|
||||
const preApply = Mock.from(original.preInput, false);
|
||||
preApply.apply(original.transform);
|
||||
|
||||
// Builds the reversion option according to the loaded lexical model's known
|
||||
// syntactic properties.
|
||||
const suggestionContext = new ContextWindow(original.preInput, this.configuration, getLayerId());
|
||||
|
||||
// We must accept the Suggestion from its original context, which was before
|
||||
// `original.transform` was applied.
|
||||
let reversionPromise: Promise<Reversion> = this.lmEngine.acceptSuggestion(suggestion, suggestionContext, original.transform);
|
||||
|
||||
// Also, request new prediction set based on the resulting context.
|
||||
reversionPromise = reversionPromise.then((reversion) => {
|
||||
const mappedReversion: Reversion = {
|
||||
// By mapping back to the original Transcription that generated the Suggestion,
|
||||
// the input will be automatically rewound to the preInput state.
|
||||
transform: original.transform,
|
||||
// The ID part is critical; the reversion can't be applied without it.
|
||||
transformId: -original.token, // reversions use the additive inverse.
|
||||
displayAs: reversion.displayAs, // The real reason we needed to call the LMLayer.
|
||||
id: reversion.id,
|
||||
tag: reversion.tag
|
||||
}
|
||||
// // If using the version from lm-layer:
|
||||
// let mappedReversion = reversion;
|
||||
// mappedReversion.transformId = reversionTranscription.token;
|
||||
this.predictFromTarget(outputTarget, getLayerId());
|
||||
return mappedReversion;
|
||||
});
|
||||
|
||||
return reversionPromise;
|
||||
}
|
||||
|
||||
this.recentTranscriptions.rewindTo(suggestion.transformId);
|
||||
|
||||
// Apply the Suggestion!
|
||||
|
||||
// Step 1: determine the final output text
|
||||
const intermediate = Mock.from(original.preInput, false);
|
||||
intermediate.apply(suggestion.transform);
|
||||
let final = intermediate;
|
||||
if(suggestion.appendedTransform) {
|
||||
final = Mock.from(intermediate);
|
||||
final.apply(suggestion.appendedTransform);
|
||||
|
||||
// Somewhere here, save-state the intermediate state!
|
||||
const appendedTranscription = final.buildTranscriptionFrom(intermediate, null, false);
|
||||
this.recordTranscription(appendedTranscription);
|
||||
// We set the appended transform with its own ID before passing it off to the predictive-text worker.
|
||||
suggestion.appendedTransform.id = appendedTranscription.token;
|
||||
}
|
||||
|
||||
// Step 2: build a final, master Transform that will produce the desired results from the CURRENT state.
|
||||
// In embedded mode, both Android and iOS are best served by calculating this transform and applying its
|
||||
// values as needed for use with their IME interfaces.
|
||||
const transform = final.buildTransformFrom(outputTarget);
|
||||
outputTarget.apply(transform);
|
||||
|
||||
// Tell the banner that a suggestion was applied, so it can call the
|
||||
// keyboard's PostKeystroke entry point as needed
|
||||
this.emit('suggestionapplied', outputTarget);
|
||||
|
||||
// Build a 'reversion' Transcription that can be used to undo this apply() if needed,
|
||||
// replacing the suggestion transform with the original input text.
|
||||
const preApply = Mock.from(original.preInput, false);
|
||||
preApply.apply(original.transform);
|
||||
|
||||
// Builds the reversion option according to the loaded lexical model's known
|
||||
// syntactic properties.
|
||||
const suggestionContext = new ContextWindow(original.preInput, this.configuration, getLayerId());
|
||||
|
||||
// We must accept the Suggestion from its original context, which was before
|
||||
// `original.transform` was applied.
|
||||
let reversionPromise: Promise<Reversion> = this.lmEngine.acceptSuggestion(suggestion, suggestionContext, original.transform);
|
||||
|
||||
// Also, request new prediction set based on the resulting context.
|
||||
reversionPromise = reversionPromise.then((reversion) => {
|
||||
const mappedReversion: Reversion = {
|
||||
// By mapping back to the original Transcription that generated the Suggestion,
|
||||
// the input will be automatically rewound to the preInput state.
|
||||
transform: original.transform,
|
||||
// The ID part is critical; the reversion can't be applied without it.
|
||||
transformId: -original.token, // reversions use the additive inverse.
|
||||
displayAs: reversion.displayAs, // The real reason we needed to call the LMLayer.
|
||||
id: reversion.id,
|
||||
tag: reversion.tag,
|
||||
appendedTransform: reversion.appendedTransform
|
||||
}
|
||||
// // If using the version from lm-layer:
|
||||
// let mappedReversion = reversion;
|
||||
// mappedReversion.transformId = reversionTranscription.token;
|
||||
this.predictFromTarget(outputTarget, getLayerId());
|
||||
return mappedReversion;
|
||||
});
|
||||
|
||||
return reversionPromise;
|
||||
}
|
||||
|
||||
public applyReversion(reversion: Reversion, outputTarget: OutputTarget) {
|
||||
public applyReversion(reversion: Reversion, outputTarget: OutputTarget, appendedOnly?: boolean) {
|
||||
if(!outputTarget) {
|
||||
throw "Accepting suggestions requires a destination OutputTarget instance."
|
||||
throw new Error("Accepting suggestions requires a destination OutputTarget instance.");
|
||||
}
|
||||
|
||||
if(!this.isActive) {
|
||||
|
|
@ -282,22 +292,22 @@ export class LanguageProcessor extends EventEmitter<LanguageProcessorEventMap> {
|
|||
//
|
||||
// Reversions use the additive inverse of the id token of the Transcription being
|
||||
// reverted to.
|
||||
const original = this.getPredictionState(-reversion.transformId);
|
||||
const reversionId = appendedOnly ? reversion.appendedTransform.id : -reversion.transformId;
|
||||
const original = this.getPredictionState(reversionId);
|
||||
if(!original) {
|
||||
console.warn("Could not apply the Suggestion!");
|
||||
return Promise.resolve([] as Suggestion[]);
|
||||
}
|
||||
|
||||
this.recentTranscriptions.rewindTo(-reversion.transformId);
|
||||
this.recentTranscriptions.rewindTo(reversionId);
|
||||
|
||||
// Apply the Reversion!
|
||||
|
||||
// Step 1: determine the final output text
|
||||
const final = Mock.from(original.preInput, false);
|
||||
final.apply(reversion.transform); // Should match original.transform, actually. (See applySuggestion)
|
||||
if(reversion.appendedTransform) {
|
||||
final.apply(reversion.appendedTransform);
|
||||
}
|
||||
if(!appendedOnly) {
|
||||
final.apply(reversion.transform); // Should match original.transform, actually. (See applySuggestion)
|
||||
} // else: the retrieved transcription matches the applied Suggestion's root, without the appended part.
|
||||
|
||||
// Step 2: build a final, master Transform that will produce the desired results from the CURRENT state.
|
||||
// In embedded mode, both Android and iOS are best served by calculating this transform and applying its
|
||||
|
|
@ -308,7 +318,8 @@ export class LanguageProcessor extends EventEmitter<LanguageProcessorEventMap> {
|
|||
// The reason we need to preserve the additive-inverse 'transformId' property on Reversions.
|
||||
const promise = this.currentPromise = this.lmEngine.revertSuggestion(
|
||||
reversion,
|
||||
new ContextWindow(final, this.configuration, null)
|
||||
new ContextWindow(final, this.configuration, null),
|
||||
appendedOnly
|
||||
);
|
||||
// If the "current Promise" is as set above, clear it.
|
||||
// If another one has been triggered since... don't.
|
||||
|
|
@ -455,7 +466,7 @@ export class LanguageProcessor extends EventEmitter<LanguageProcessorEventMap> {
|
|||
}
|
||||
|
||||
public get wordbreaksAfterSuggestions() {
|
||||
return this.configuration.wordbreaksAfterSuggestions;
|
||||
return this.configuration?.appendsWordbreaks;
|
||||
}
|
||||
|
||||
public tryAcceptSuggestion(source: string): boolean {
|
||||
|
|
|
|||
|
|
@ -55,6 +55,7 @@ export class KeymanEngineBase<
|
|||
|
||||
private keyEventListener: KeyEventFullHandler = (event, callback) => {
|
||||
const outputTarget = this.contextManager.activeTarget;
|
||||
const predictionContext = this.contextManager.predictionContext;
|
||||
|
||||
if(!this.contextManager.activeKeyboard || !outputTarget) {
|
||||
if(callback) {
|
||||
|
|
@ -88,7 +89,7 @@ export class KeymanEngineBase<
|
|||
this.core.keyboardProcessor.layerId = oskLayer;
|
||||
}
|
||||
}
|
||||
const result = this.core.processKeyEvent(event, outputTarget);
|
||||
const result = this.core.processKeyEvent(event, outputTarget, predictionContext);
|
||||
|
||||
if(result && result.transcription?.transform) {
|
||||
this.config.onRuleFinalization(result, this.contextManager.activeTarget);
|
||||
|
|
@ -249,6 +250,7 @@ export class KeymanEngineBase<
|
|||
|
||||
const keyboardProcessor = this.core.keyboardProcessor;
|
||||
const predictionContext = new PredictionContext(this.core.languageProcessor, () => keyboardProcessor.layerId);
|
||||
// Set the prediction context within languageProcessor (or InputProcessor / this.core)
|
||||
this.contextManager.configure({
|
||||
resetContext: (target) => {
|
||||
// Could reset the target's deadkeys here, but it's really more of a 'core' task.
|
||||
|
|
|
|||
|
|
@ -177,7 +177,7 @@ export default class LMLayer {
|
|||
});
|
||||
}
|
||||
|
||||
revertSuggestion(reversion: Reversion, context: Context): Promise<Suggestion[]> {
|
||||
revertSuggestion(reversion: Reversion, context: Context, appendedOnly?: boolean): Promise<Suggestion[]> {
|
||||
let token = this._nextToken++;
|
||||
return new Promise((resolve, reject) => {
|
||||
this._revertPromises.make(token, resolve, reject);
|
||||
|
|
@ -185,7 +185,8 @@ export default class LMLayer {
|
|||
message: 'revert',
|
||||
token: token,
|
||||
reversion: reversion,
|
||||
context: context
|
||||
context: context,
|
||||
appendedOnly: appendedOnly
|
||||
})
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -62,8 +62,10 @@ export class ContextState {
|
|||
|
||||
/**
|
||||
* The full set of Suggestions produced for the transition to this context state.
|
||||
*
|
||||
* May be undefined if no suggestions were generated for this state.
|
||||
*/
|
||||
suggestions: Suggestion[];
|
||||
suggestions?: Suggestion[];
|
||||
|
||||
/**
|
||||
* If set, denotes the suggestion ID for the suggestion (from .suggestions) that
|
||||
|
|
@ -71,6 +73,20 @@ export class ContextState {
|
|||
*/
|
||||
appliedSuggestionId?: number;
|
||||
|
||||
/**
|
||||
* If a suggestion was applied, returns the transition ID associated with the
|
||||
* applied suggestion.
|
||||
*
|
||||
* Otherwise, returns `undefined`.
|
||||
*/
|
||||
get appliedSuggestionTransitionId(): number | undefined {
|
||||
if(!this.appliedSuggestionId) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return this.suggestions[this.appliedSuggestionId]?.transformId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates whether or not the applied suggestion (if it exists) was applied
|
||||
* directly by the user.
|
||||
|
|
@ -145,6 +161,7 @@ export class ContextState {
|
|||
baseTokens.push(new ContextToken(this.model));
|
||||
}
|
||||
this.tokenization = new ContextTokenization(baseTokens);
|
||||
this.inputTransforms = new Map();
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -107,7 +107,7 @@ export class ContextTracker {
|
|||
if(transitionId !== undefined) {
|
||||
// Special case: if base and final match, we should use the old Transition instance.
|
||||
// This is currently used in some unit tests.
|
||||
if(result.final != result.base) {
|
||||
if(result.final.context != result.base.context) {
|
||||
this.cache.add(transitionId, result);
|
||||
} else {
|
||||
return this.cache.peek(priorMatchState.transitionId);
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ import { ContextState } from './context-state.js';
|
|||
import Distribution = LexicalModelTypes.Distribution;
|
||||
import Suggestion = LexicalModelTypes.Suggestion;
|
||||
import Transform = LexicalModelTypes.Transform;
|
||||
import { buildMergedTransform } from '@keymanapp/models-templates';
|
||||
|
||||
|
||||
// Mark affected tokens with the applied-suggestion transition ID
|
||||
// for easy future reference.
|
||||
|
|
@ -135,7 +135,10 @@ export class ContextTransition {
|
|||
* @param suggestion
|
||||
* @returns
|
||||
*/
|
||||
applySuggestion(suggestion: Suggestion) {
|
||||
applySuggestion(suggestion: Suggestion): {
|
||||
base: ContextTransition,
|
||||
appended?: ContextTransition
|
||||
} {
|
||||
const preAppliedState = this.final;
|
||||
if(!preAppliedState.suggestions?.find((s) => s.id == suggestion?.id)) {
|
||||
throw new Error("Could not find matching suggestion to apply");
|
||||
|
|
@ -162,22 +165,33 @@ export class ContextTransition {
|
|||
state.suggestions = preAppliedState.suggestions;
|
||||
}
|
||||
|
||||
const fullTransform = suggestion.appendedTransform
|
||||
? buildMergedTransform(suggestion.transform, suggestion.appendedTransform)
|
||||
: suggestion.transform;
|
||||
|
||||
// Start from a deep copy, then replace as needed to overwrite with the context
|
||||
// state resulting from the suggestion while preserving suggestion + primary
|
||||
// keystroke data.
|
||||
|
||||
const resultTransition = new ContextTransition(this);
|
||||
buildAppliedTransition(resultTransition, this.base, fullTransform);
|
||||
buildAppliedTransition(resultTransition, this.base, suggestion.transform);
|
||||
|
||||
// An applied suggestion should replace the original Transition's effects, though keeping
|
||||
// the original input around.
|
||||
resultTransition._transitionId = suggestion.transformId;
|
||||
resultTransition.final.appliedInput = preAppliedState.appliedInput;
|
||||
|
||||
return resultTransition;
|
||||
if(!suggestion.appendedTransform) {
|
||||
return { base: resultTransition };
|
||||
}
|
||||
|
||||
const finalTransition = new ContextTransition(resultTransition.final, suggestion.appendedTransform.id);
|
||||
buildAppliedTransition(finalTransition, resultTransition.final, suggestion.appendedTransform);
|
||||
|
||||
// The appended transform is applied with no intermediate input.
|
||||
finalTransition.final.appliedInput = { insert: '', deleteLeft: 0 };
|
||||
finalTransition.inputDistribution = [];
|
||||
|
||||
return {
|
||||
base: resultTransition,
|
||||
appended: finalTransition
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -239,8 +239,12 @@ export default class LMLayerWorker {
|
|||
let compositor = this.transitionToReadyState(model);
|
||||
// This test allows models to directly specify the property without it being auto-overridden by
|
||||
// this default.
|
||||
if(configuration.wordbreaksAfterSuggestions === undefined) {
|
||||
configuration.wordbreaksAfterSuggestions = (compositor.punctuation.insertAfterWord != '');
|
||||
if(configuration.appendsWordbreaks === undefined) {
|
||||
if(compositor.punctuation.insertAfterWord != '') {
|
||||
configuration.appendsWordbreaks = {
|
||||
breakingMarks: ['.', ',', ';', ':', '?', '!']
|
||||
};
|
||||
} // else leave undefined (falsy)
|
||||
}
|
||||
compositor.setConfiguration(configuration);
|
||||
this.cast('ready', { configuration });
|
||||
|
|
@ -368,9 +372,9 @@ export default class LMLayerWorker {
|
|||
});
|
||||
break;
|
||||
case 'revert':
|
||||
var {reversion, context} = payload;
|
||||
var {reversion, context, appendedOnly} = payload;
|
||||
|
||||
compositor.applyReversion(reversion, context).then((suggestions) => {
|
||||
compositor.applyReversion(reversion, context, appendedOnly).then((suggestions) => {
|
||||
this.cast('postrevert', {
|
||||
token: payload.token,
|
||||
suggestions: suggestions
|
||||
|
|
|
|||
|
|
@ -215,9 +215,9 @@ export class ModelCompositor {
|
|||
|
||||
acceptSuggestion(suggestion: Suggestion, context: Context, postTransform?: Transform): Reversion {
|
||||
// Step 1: generate and save the reversion's Transform.
|
||||
let sourceTransform = models.buildMergedTransform(suggestion.transform, suggestion.appendedTransform ?? { insert: '', deleteLeft: 0});
|
||||
let deletedLeftChars = KMWString.substr(context.left, -sourceTransform.deleteLeft, sourceTransform.deleteLeft);
|
||||
let insertedLength = KMWString.length(sourceTransform.insert);
|
||||
const sourceTransform = models.buildMergedTransform(suggestion.transform, suggestion.appendedTransform ?? { insert: '', deleteLeft: 0 });
|
||||
const deletedLeftChars = KMWString.substr(context.left, -sourceTransform.deleteLeft, sourceTransform.deleteLeft);
|
||||
const insertedLength = KMWString.length(sourceTransform.insert);
|
||||
|
||||
let reversionTransform: Transform = {
|
||||
insert: deletedLeftChars,
|
||||
|
|
@ -278,9 +278,18 @@ export class ModelCompositor {
|
|||
originalTransition = this.contextTracker.latest;
|
||||
}
|
||||
|
||||
const appliedTransition = originalTransition.applySuggestion(suggestion);
|
||||
this.contextTracker.latest = appliedTransition;
|
||||
const transitions = originalTransition.applySuggestion(suggestion);
|
||||
this.contextTracker.latest = transitions.base;
|
||||
this.contextTracker.saveLatest();
|
||||
if(transitions.appended) {
|
||||
this.contextTracker.latest = transitions.appended;
|
||||
this.contextTracker.saveLatest();
|
||||
reversion.appendedTransform = {
|
||||
insert: '',
|
||||
deleteLeft: suggestion.appendedTransform.insert.length,
|
||||
id: suggestion.appendedTransform.id
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return reversion;
|
||||
|
|
@ -293,7 +302,7 @@ export class ModelCompositor {
|
|||
* the original keystroke's effects.
|
||||
* @returns
|
||||
*/
|
||||
async applyReversion(reversion: Reversion, context: Context): Promise<Suggestion[]> {
|
||||
async applyReversion(reversion: Reversion, context: Context, appendedOnly?: boolean): Promise<Suggestion[]> {
|
||||
// If we are unable to track context (because the model does not support LexiconTraversal),
|
||||
// we need a "fallback" strategy.
|
||||
let compositor = this;
|
||||
|
|
@ -317,7 +326,10 @@ export class ModelCompositor {
|
|||
}
|
||||
|
||||
// When the context is tracked, we prefer the tracked information.
|
||||
let originalTransition = this.contextTracker.findAndRevert(-reversion.transformId);
|
||||
// Note that the base reversion's .transformId will predate the appendedTransform id
|
||||
// used to add whitespace (if one existed), so reverting to the base ID's associated
|
||||
// context also reverts the appendedTransform.
|
||||
let originalTransition = this.contextTracker.findAndRevert(appendedOnly ? reversion.appendedTransform.id : -reversion.transformId);
|
||||
|
||||
if(!originalTransition) {
|
||||
this.contextTracker.reset(context, -reversion.transformId);
|
||||
|
|
@ -325,7 +337,9 @@ export class ModelCompositor {
|
|||
|
||||
suggestions = fallbackSuggestions();
|
||||
} else {
|
||||
if(!suggestions) {
|
||||
if(suggestions || appendedOnly) {
|
||||
suggestions = Promise.resolve([]);
|
||||
} else {
|
||||
// Will need to be modified a bit if/when phrase-level suggestions are implemented.
|
||||
// Those will be tracked on the first token of the phrase, which won't be the tail
|
||||
// if they cover multiple tokens.
|
||||
|
|
|
|||
|
|
@ -207,6 +207,12 @@ export interface RevertMessage {
|
|||
* corresponding Suggestion.
|
||||
*/
|
||||
context: Context;
|
||||
|
||||
/**
|
||||
* Indicates if only the appended transform should be reverted, and not the base component
|
||||
* of the previously-applied suggestion.
|
||||
*/
|
||||
appendedOnly?: boolean;
|
||||
}
|
||||
|
||||
export interface ResetContextMessage {
|
||||
|
|
|
|||
|
|
@ -14,9 +14,6 @@ import { ModelCompositor, models } from '@keymanapp/lm-worker/test-index';
|
|||
|
||||
import TrieModel = models.TrieModel;
|
||||
|
||||
|
||||
var emptyInput = (id: number) => [{sample: {insert: '', deleteLeft: 0, id: id}, p: 1}];
|
||||
|
||||
describe('ContextTracker', function() {
|
||||
describe('suggestion acceptance tracking', function() {
|
||||
let englishPunctuation = {
|
||||
|
|
@ -34,7 +31,8 @@ describe('ContextTracker', function() {
|
|||
},
|
||||
appendedTransform: {
|
||||
insert: ' ',
|
||||
deleteLeft: 0
|
||||
deleteLeft: 0,
|
||||
id: 15
|
||||
},
|
||||
transformId: 2,
|
||||
id: 1,
|
||||
|
|
@ -62,7 +60,7 @@ describe('ContextTracker', function() {
|
|||
compositor.initContextTracker(baseContext, 0);
|
||||
const contextTracker = compositor.contextTracker;
|
||||
|
||||
let preAppliedTransition = contextTracker.analyzeState(model, baseContext, emptyInput(0));
|
||||
let preAppliedTransition = contextTracker.latest;
|
||||
// We'll ignore and overwrite the results. We do need the prediction round to occur, though.
|
||||
await compositor.predict([{sample: postTransform, p: 1}], baseContext);
|
||||
contextTracker.latest.final.suggestions = [baseSuggestion];
|
||||
|
|
@ -74,8 +72,7 @@ describe('ContextTracker', function() {
|
|||
contextTracker.unitTestEndPoints.cache().keys().forEach((key) => assert.isDefined(key));
|
||||
|
||||
// Next step - on the followup context, is the replacement still active?
|
||||
let postContext = models.applyTransform(baseSuggestion.appendedTransform, models.applyTransform(baseSuggestion.transform, baseContext));
|
||||
let postContextMatch = compositor.contextTracker.analyzeState(model, postContext, emptyInput(2));
|
||||
let postContextMatch = contextTracker.unitTestEndPoints.cache().peek(baseSuggestion.appendedTransform.id);
|
||||
assert.equal(postContextMatch.final.appliedSuggestionId, baseSuggestion.id);
|
||||
|
||||
// Penultimate token corresponds to whitespace, which does not have a 'raw' representation.
|
||||
|
|
|
|||
|
|
@ -12,9 +12,12 @@ import { assert } from 'chai';
|
|||
|
||||
import { default as defaultBreaker } from '@keymanapp/models-wordbreakers';
|
||||
import { jsonFixture } from '@keymanapp/common-test-resources/model-helpers.mjs';
|
||||
import { LexicalModelTypes } from '@keymanapp/common-types';
|
||||
|
||||
import { ContextState, ContextTransition, models } from '@keymanapp/lm-worker/test-index';
|
||||
|
||||
import Distribution = LexicalModelTypes.Distribution;
|
||||
import Transform = LexicalModelTypes.Transform;
|
||||
import TrieModel = models.TrieModel;
|
||||
|
||||
var plainModel = new TrieModel(jsonFixture('models/tries/english-1000'),
|
||||
|
|
@ -137,20 +140,43 @@ describe('ContextTransition', () => {
|
|||
}];
|
||||
|
||||
const appliedTransition = transition.applySuggestion(suggestions[0]);
|
||||
assert.notEqual(appliedTransition, transition);
|
||||
assert.sameOrderedMembers(appliedTransition.final.tokenization.exampleInput, [
|
||||
assert.notEqual(appliedTransition.base, transition);
|
||||
assert.isOk(appliedTransition.appended);
|
||||
assert.notEqual(appliedTransition.appended, transition);
|
||||
assert.sameOrderedMembers(appliedTransition.base.final.tokenization.exampleInput, [
|
||||
'hello', ' ', 'world'
|
||||
]);
|
||||
assert.sameOrderedMembers(appliedTransition.appended.final.tokenization.exampleInput, [
|
||||
'hello', ' ', 'world', ' ', ''
|
||||
]);
|
||||
assert.equal(appliedTransition.final.appliedSuggestionId, suggestions[0].id);
|
||||
appliedTransition.final.tokenization.tokens.forEach((token, index) => {
|
||||
assert.equal(appliedTransition.base.final.appliedSuggestionId, suggestions[0].id);
|
||||
assert.equal(appliedTransition.appended.final.appliedSuggestionId, suggestions[0].id);
|
||||
|
||||
// 3 long, only last token was edited.
|
||||
appliedTransition.base.final.tokenization.tokens.forEach((token, index) => {
|
||||
if(index >= 2) {
|
||||
assert.equal(token.appliedTransitionId, suggestions[0].transformId);
|
||||
} else {
|
||||
assert.isUndefined(token.appliedTransitionId);
|
||||
}
|
||||
});
|
||||
assert.deepEqual(appliedTransition.final.suggestions, transition.final.suggestions);
|
||||
assert.deepEqual(appliedTransition.final.inputTransforms, transition.final.inputTransforms);
|
||||
|
||||
appliedTransition.appended.final.tokenization.tokens.forEach((token, index) => {
|
||||
if(index >= 2) {
|
||||
assert.equal(token.appliedTransitionId, suggestions[0].transformId);
|
||||
} else {
|
||||
assert.isUndefined(token.appliedTransitionId);
|
||||
}
|
||||
});
|
||||
assert.deepEqual(appliedTransition.base.final.suggestions, transition.final.suggestions);
|
||||
assert.deepEqual(appliedTransition.appended.final.suggestions, transition.final.suggestions);
|
||||
assert.deepEqual(appliedTransition.base.final.inputTransforms, transition.final.inputTransforms);
|
||||
assert.deepEqual(appliedTransition.base.inputDistribution, transition.inputDistribution);
|
||||
|
||||
const emptyTransformMap = new Map<number, Distribution<Transform>>();
|
||||
emptyTransformMap.set(2, [{sample: {insert: '', deleteLeft: 0, id: 2}, p: 1}])
|
||||
assert.deepEqual(appliedTransition.appended.final.appliedInput, {insert: '', deleteLeft: 0});
|
||||
assert.isEmpty(appliedTransition.appended.inputDistribution);
|
||||
});
|
||||
|
||||
describe('reproduceOriginal', () => {
|
||||
|
|
@ -247,10 +273,10 @@ describe('ContextTransition', () => {
|
|||
displayAs: 'won'
|
||||
}];
|
||||
|
||||
const appliedTransition = transition.applySuggestion(suggestions[0]);
|
||||
const appliedTransitions = transition.applySuggestion(suggestions[0]);
|
||||
// To the point above, this matches the 'applySuggestion' test case.
|
||||
|
||||
const restoredTransition = appliedTransition.reproduceOriginal();
|
||||
const restoredTransition = appliedTransitions.base.reproduceOriginal();
|
||||
assertClonedTransitionMatch(restoredTransition, transition);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -19,8 +19,6 @@ import Suggestion = LexicalModelTypes.Suggestion;
|
|||
import Transform = LexicalModelTypes.Transform;
|
||||
import TrieModel = models.TrieModel;
|
||||
|
||||
var emptyInput = (id: number) => [{sample: {insert: '', deleteLeft: 0, id: id}, p: 1}];
|
||||
|
||||
describe('ModelCompositor', function() {
|
||||
describe('Prediction with 14.0+ models', function() {
|
||||
describe('Basic suggestion generation', function() {
|
||||
|
|
@ -851,6 +849,7 @@ describe('ModelCompositor', function() {
|
|||
assert.equal(keepSuggestion.tag, 'keep'); // corresponds to `postTransform`, but the transform isn't equal.
|
||||
|
||||
let baseSuggestion = initialSuggestions[1];
|
||||
baseSuggestion.appendedTransform.id = 15; // set an id for the applied transform.
|
||||
let reversion = compositor.acceptSuggestion(baseSuggestion, baseContext, postTransform);
|
||||
assert.equal(reversion.transformId, -baseSuggestion.transformId);
|
||||
assert.equal(reversion.id, -baseSuggestion.id);
|
||||
|
|
@ -892,18 +891,21 @@ describe('ModelCompositor', function() {
|
|||
assert.equal(compositor.contextTracker.unitTestEndPoints.cache().size, 0);
|
||||
|
||||
let baseSuggestion = initialSuggestions[1];
|
||||
baseSuggestion.appendedTransform.id = 15; // set an id for the applied transform.
|
||||
let reversion = compositor.acceptSuggestion(baseSuggestion, baseContext, postTransform);
|
||||
assert.equal(compositor.contextTracker.unitTestEndPoints.cache().size, 1);
|
||||
// Two rewind states: one for the suggestion's base word text, one for
|
||||
// appended post-suggestion whitespace.
|
||||
assert.equal(compositor.contextTracker.unitTestEndPoints.cache().size, 2);
|
||||
let contextIds = compositor.contextTracker.unitTestEndPoints.cache().keys();
|
||||
|
||||
assert.equal(reversion.transformId, -baseSuggestion.transformId);
|
||||
assert.equal(reversion.id, -baseSuggestion.id);
|
||||
|
||||
let postContext = models.applyTransform(baseSuggestion.appendedTransform, models.applyTransform(baseSuggestion.transform, baseContext));
|
||||
const appliedContextState = compositor.contextTracker.analyzeState(model, postContext, emptyInput(15));
|
||||
const appliedContextState = compositor.contextTracker.unitTestEndPoints.cache().peek(15);
|
||||
|
||||
// Accepting the suggestion rewrites the latest context transition.
|
||||
assert.equal(compositor.contextTracker.unitTestEndPoints.cache().size, 2);
|
||||
assert.sameMembers(compositor.contextTracker.unitTestEndPoints.cache().keys(), [15, baseSuggestion.transformId]);
|
||||
assert.sameMembers(compositor.contextTracker.unitTestEndPoints.cache().keys(), [...contextIds]);
|
||||
|
||||
// The replacement should be marked on the context-tracking token for the applied version of the results.
|
||||
assert.equal(suggestionContextState.final.appliedSuggestionId, undefined);
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue