mirror of
https://github.com/keymanapp/keyman.git
synced 2026-11-03 09:37:47 +00:00
change(web): spin off method, add unit tests to verify handling of keep vs revert suggestions
Some checks are pending
Keyman Build Summary / Summarize build status checks (push) Waiting to run
Some checks are pending
Keyman Build Summary / Summarize build status checks (push) Waiting to run
This commit is contained in:
parent
e225b26f10
commit
f864bf293a
5 changed files with 148 additions and 17 deletions
|
|
@ -19,6 +19,15 @@ import Reversion = LexicalModelTypes.Reversion;
|
|||
import Suggestion = LexicalModelTypes.Suggestion;
|
||||
import Transform = LexicalModelTypes.Transform;
|
||||
|
||||
export interface TransitionReversionView extends Pick<ContextTransition, 'reversion'> {
|
||||
/**
|
||||
* Gets the context state resulting from the context transition event,
|
||||
* including any generated suggestions and data regarding potential
|
||||
* application thereof.
|
||||
*/
|
||||
final: Pick<ContextState, 'suggestions'>
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents the transition between two context states as triggered
|
||||
* by input keystrokes or applied suggestions.
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import * as models from '@keymanapp/models-templates';
|
|||
import { LexicalModelTypes } from '@keymanapp/common-types';
|
||||
|
||||
import { TransformUtils } from './transformUtils.js';
|
||||
import { applySuggestionCasing, correctAndEnumerate, createDefaultKeep, dedupeSuggestions, finalizeSuggestions, predictionAutoSelect, processSimilarity, toAnnotatedSuggestion, tupleDisplayOrderSort } from './predict-helpers.js';
|
||||
import { applySuggestionCasing, correctAndEnumerate, createDefaultKeep, dedupeSuggestions, finalizeSuggestions, predictionAutoSelect, prependReversion, processSimilarity, toAnnotatedSuggestion, tupleDisplayOrderSort } from './predict-helpers.js';
|
||||
import { detectCurrentCasing, determineModelTokenizer, determineModelWordbreaker, determinePunctuationFromModel } from './model-helpers.js';
|
||||
|
||||
import { ContextTracker } from './correction/context-tracker.js';
|
||||
|
|
@ -206,18 +206,8 @@ export class ModelCompositor {
|
|||
this.SUGGESTION_ID_SEED++;
|
||||
});
|
||||
|
||||
if(revertableTransitionId) {
|
||||
const reversion = this.contextTracker.peek(revertableTransitionId)?.reversion;
|
||||
if(reversion) {
|
||||
if(suggestions[0]?.tag == 'keep') {
|
||||
const keep = suggestions.shift();
|
||||
suggestions.unshift(reversion);
|
||||
suggestions.unshift(keep);
|
||||
} else {
|
||||
suggestions.unshift(reversion)
|
||||
}
|
||||
}
|
||||
}
|
||||
const transitionToRevert = this.contextTracker?.peek(revertableTransitionId);
|
||||
prependReversion(suggestions, transitionToRevert);
|
||||
|
||||
// Store the suggestions on the final token of the current context state (if it exists).
|
||||
// Or, once phrase-level suggestions are possible, on whichever token serves as each prediction's root.
|
||||
|
|
@ -239,8 +229,8 @@ export class ModelCompositor {
|
|||
// Step 1: re-use the original input Transform as the reversion's Transform.
|
||||
// The Web engine will restore the original state of the context before accepting
|
||||
// and before reverting; all we need to do is put the original keystroke back in place.
|
||||
let reversionTransform: Transform = originalInput
|
||||
? { ...originalInput }
|
||||
let reversionTransform: Transform = originalInput
|
||||
? { ...originalInput }
|
||||
: { insert: '', deleteLeft: 0, id: suggestion.transform.id };
|
||||
|
||||
// Step 2: building the proper 'displayAs' string for the Reversion
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ import { ContextTokenization } from './correction/context-tokenization.js';
|
|||
import { ContextTracker } from './correction/context-tracker.js';
|
||||
import { ContextToken } from './correction/context-token.js';
|
||||
import { ContextState, determineContextSlideTransform } from './correction/context-state.js';
|
||||
import { ContextTransition } from './correction/context-transition.js';
|
||||
import { ContextTransition, TransitionReversionView } from './correction/context-transition.js';
|
||||
import { ExecutionTimer } from './correction/execution-timer.js';
|
||||
import { ModelCompositor } from './model-compositor.js';
|
||||
import { getBestTokenMatches } from './correction/distance-modeler.js';
|
||||
|
|
@ -1196,4 +1196,35 @@ export function toAnnotatedSuggestion(
|
|||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* For applicable scenarios, this mutates the passed-in suggestion array by
|
||||
* prepending a predictive-text reversion that restores the context to a prior
|
||||
* state. Otherwise, it leaves the suggestion array unaltered.
|
||||
* @param suggestions
|
||||
* @param transitionToRevert
|
||||
* @returns
|
||||
*/
|
||||
export function prependReversion(suggestions: Suggestion[], transitionToRevert: TransitionReversionView) {
|
||||
if(transitionToRevert) {
|
||||
const reversion = transitionToRevert.reversion;
|
||||
if(reversion) {
|
||||
if(suggestions[0]?.tag == 'keep') {
|
||||
const appliedId = -reversion.id;
|
||||
const appliedSuggestion = transitionToRevert.final.suggestions.find((s) => s.id == appliedId);
|
||||
// If the selected suggestion was itself a `keep`, we don't need a
|
||||
// reversion. They'd do the same thing.
|
||||
if(appliedSuggestion.tag != 'keep') {
|
||||
const keep = suggestions.shift();
|
||||
suggestions.unshift(reversion);
|
||||
suggestions.unshift(keep);
|
||||
}
|
||||
} else {
|
||||
suggestions.unshift(reversion);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return suggestions;
|
||||
}
|
||||
|
|
@ -3,7 +3,7 @@ export * from './correction/context-state.js';
|
|||
export * from './correction/context-token.js';
|
||||
export * from './correction/context-tokenization.js';
|
||||
export { ContextTracker } from './correction/context-tracker.js';
|
||||
export { ContextTransition } from './correction/context-transition.js';
|
||||
export * from './correction/context-transition.js';
|
||||
export * from './correction/correction-searchable.js';
|
||||
export * from './correction/correction-result-mapping.js';
|
||||
export * from './correction/distance-modeler.js';
|
||||
|
|
|
|||
|
|
@ -0,0 +1,101 @@
|
|||
|
||||
/*
|
||||
* Keyman is copyright (C) SIL Global. MIT License.
|
||||
*
|
||||
* Created by jahorton on 2026-07-27
|
||||
*
|
||||
* This file contains tests designed to ensure predictive text does not
|
||||
* provide matching 'keep' and 'revert' suggestions in any context.
|
||||
*/
|
||||
|
||||
|
||||
import { assert } from 'chai';
|
||||
|
||||
import { LexicalModelTypes } from '@keymanapp/common-types';
|
||||
import { prependReversion, type TransitionReversionView } from "@keymanapp/lm-worker/test-index";
|
||||
|
||||
import Suggestion = LexicalModelTypes.Suggestion;
|
||||
|
||||
describe('prependReversion', () => {
|
||||
it(`prepends reversions when reverting a non-'keep' suggestion`, () => {
|
||||
// context: Original was appl+u, corrected to apply. Reached via bksp.
|
||||
const suggestions: Suggestion[] = [{
|
||||
tag: 'keep',
|
||||
transform: { insert: 'apply', deleteLeft: 6, id: 3 },
|
||||
displayAs: '"apply"',
|
||||
id: 5,
|
||||
matchesModel: false
|
||||
} as Suggestion];
|
||||
|
||||
const revertable: TransitionReversionView = {
|
||||
reversion: {
|
||||
tag: 'revert',
|
||||
transform: { insert: 'u', deleteLeft: 0, id: 3 },
|
||||
id: -3,
|
||||
displayAs: '"applu"'
|
||||
},
|
||||
final: {
|
||||
suggestions: [{
|
||||
tag: 'keep',
|
||||
transform: { insert: 'applu', deleteLeft: 4, id: 3 },
|
||||
displayAs: '"applu"',
|
||||
id: 2,
|
||||
matchesModel: false
|
||||
} as Suggestion, {
|
||||
transform: { insert: 'apply', deleteLeft: 4, id: 3 },
|
||||
displayAs: 'apply',
|
||||
id: 3
|
||||
}, {
|
||||
transform: { insert: 'applied', deleteLeft: 4, id: 3 },
|
||||
displayAs: 'applied',
|
||||
id: 4
|
||||
}]
|
||||
}
|
||||
};
|
||||
|
||||
prependReversion(suggestions, revertable);
|
||||
|
||||
assert.includeMembers(suggestions, [revertable.reversion]);
|
||||
});
|
||||
|
||||
it(`does not prepend reversions when reverting a 'keep' suggestion`, () => {
|
||||
// context: Original was appl+u, corrected to apply
|
||||
const suggestions: Suggestion[] = [{
|
||||
tag: 'keep',
|
||||
transform: { insert: 'applu', deleteLeft: 5, id: 3 },
|
||||
displayAs: '"applu"',
|
||||
id: 5,
|
||||
matchesModel: false
|
||||
} as Suggestion];
|
||||
|
||||
const revertable: TransitionReversionView = {
|
||||
reversion: {
|
||||
tag: 'revert',
|
||||
transform: { insert: 'u', deleteLeft: 0, id: 3 },
|
||||
id: -2,
|
||||
displayAs: '"applu"'
|
||||
},
|
||||
final: {
|
||||
suggestions: [{
|
||||
tag: 'keep',
|
||||
transform: { insert: 'applu', deleteLeft: 4, id: 3 },
|
||||
displayAs: '"applu"',
|
||||
id: 2,
|
||||
matchesModel: false
|
||||
} as Suggestion, {
|
||||
transform: { insert: 'apply', deleteLeft: 4, id: 3 },
|
||||
displayAs: 'apply',
|
||||
id: 3
|
||||
}, {
|
||||
transform: { insert: 'applied', deleteLeft: 4, id: 3 },
|
||||
displayAs: 'applied',
|
||||
id: 4
|
||||
}]
|
||||
}
|
||||
};
|
||||
|
||||
prependReversion(suggestions, revertable);
|
||||
|
||||
assert.notIncludeMembers(suggestions, [revertable.reversion]);
|
||||
});
|
||||
});
|
||||
Loading…
Add table
Reference in a new issue