Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -322,7 +322,13 @@ export function determineContextSlideTransform(srcContext: Context, dstContext:
// Assertion: If the assumption above holds and both start-of-buffer flags
// are true, the contents must then match.
if(srcContext.startOfBuffer && dstContext.startOfBuffer) {
return { insert: '', deleteLeft: 0, deleteRight: 0 };
// Validate that they actually match.
// If not, the contexts shouldn't equal.
if(srcContext.left == dstContext.left) {
return { insert: '', deleteLeft: 0, deleteRight: 0 };
} else {
return null;
}
}

// Assumption: the right-hand side of the left-context strings WILL match.
Expand All @@ -343,11 +349,8 @@ export function determineContextSlideTransform(srcContext: Context, dstContext:
//
// Context operations are already code-point aligned; no need to use special
// non-BMP handling here.
const smallerIsSubstringOfOther = rawDelta > 0
? dst.slice(rawDelta) == src
: src.slice(-rawDelta) == dst;
if(!smallerIsSubstringOfOther) {
throw new Error(`Context-slide preconditions invalidated - neither before nor after context is a substring of the other`);
if(rawDelta > 0 ? dst.slice(rawDelta) != src : src.slice(-rawDelta) != dst) {
return null;
}

return {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,8 @@ import { defaultWordbreaker, WordBreakProperty } from '@keymanapp/models-wordbre

import TransformUtils from './transformUtils.js';
import { determineModelTokenizer, determineModelWordbreaker, determinePunctuationFromModel } from './model-helpers.js';
import { isSubstitutionAlignable } from './correction/alignment-helpers.js';
import { ContextTracker } from './correction/context-tracker.js';
import { ContextState } from './correction/context-state.js';
import { ContextState, determineContextSlideTransform } from './correction/context-state.js';
import { ExecutionTimer } from './correction/execution-timer.js';
import ModelCompositor from './model-compositor.js';

Expand Down Expand Up @@ -240,17 +239,6 @@ export function matchBaseContextState(
context: Context,
transitionId: number
): ContextState {
const contextsMatch = (a: Context, b: Context) => {
// If either context's window is equal to or greater than the threshold
// length, there's a good chance something slid into or out of range.
if(!a.startOfBuffer || !b.startOfBuffer) {
return isSubstitutionAlignable(a.left, b.left);
} else {
// If both are smaller than the threshold, the text should match exactly.
return a.left == b.left;
}
};

const lastTransition = contextTracker.latest;
let contextState: ContextState;

Expand All @@ -259,9 +247,9 @@ export function matchBaseContextState(
// delete (if lengthened). No substitutions possible, as no rules will have
// occurred - the ONLY change is the amount of known text vs the context
// window's range.
if(contextsMatch(lastTransition.final.context, context)) {
if(determineContextSlideTransform(lastTransition.final.context, context)) {
contextState = lastTransition.final;
} else if(contextsMatch(lastTransition.base.context, context)) {
} else if(determineContextSlideTransform(lastTransition.base.context, context)) {
// Multitap case: if we reverted back to the original underlying context,
// rather than using the previous output context.
//
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ describe('determineContextTransition', () => {
const transition = determineContextTransition(
tracker,
tracker.latest.base,
targetContext,
baseContext,
inputDistribution
);

Expand All @@ -105,6 +105,8 @@ describe('determineContextTransition', () => {
assert.isFalse(warningEmitterSpy.called);
assert.sameOrderedMembers(transition.final.tokenization.exampleInput, ['this', ' ', 'is', ' ', 'for', ' ', 'techn']);
assert.isOk(transition.final.tokenization.alignment);
assert.equal(transition.final.context.left, targetContext.left);
assert.equal(transition.final.context.right ?? "", targetContext.right ?? "");
assert.sameDeepOrderedMembers(transition.inputDistribution, inputDistribution);
assert.isNotOk(transition.preservationTransform);
assert.equal(transition.transitionId, 1);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,14 +41,16 @@ describe('ModelCompositor', function() {
assert.isEmpty(suggestions1.filter((entry) => entry.tag != 'keep'));
assert.isOk(suggestions1.find((entry) => entry.tag == 'keep'));

const context2 = {
left: "the apl", startOfBuffer: true, endOfBuffer: true
};
compositor.resetContext(context2, 2);
// Context + insert needs to match a word in the model; there is no matching word here.
// Invalid due to the existing context root.
const suggestions2 = await compositor.predict({
insert: "l",
deleteLeft: 0
}, {
// Context + insert needs to match a word in the model; there is no matching word here.
// Invalid due to the existing context root.
left: "the apl", startOfBuffer: true, endOfBuffer: true
});
}, context2);

assert.isEmpty(suggestions2.filter((entry) => entry.tag != 'keep'));
assert.isOk(suggestions2.find((entry) => entry.tag == 'keep'));
Expand All @@ -71,6 +73,41 @@ describe('ModelCompositor', function() {
assert.isOk(suggestions.find((entry) => entry.displayAs == 'applied'));
});

it('generates suggestions from an empty context', async function() {
let compositor = new ModelCompositor(plainModel, true);
let context = {
left: '', startOfBuffer: true, endOfBuffer: true,
};

let inputTransform = {
insert: '',
deleteLeft: 0
};

let suggestions = await compositor.predict(inputTransform, context);
suggestions.forEach(function(suggestion) {
// Suggstions are built based on the context state BEFORE the triggering
// input, replacing the prediction's root with the complete word.
//
// This is necessary, in part, for proper display-string construction.
assert.equal(suggestion.transform.deleteLeft, 0);
});

let keep = suggestions.find(function(suggestion) {
return suggestion.tag == 'keep';
});

assert.isUndefined(keep);

// Expect an appended space.
let expectedEntries = ['the', 'and', 'of', 'it'];
expectedEntries.forEach(function(entry) {
assert.isDefined(suggestions.find(function(suggestion) {
return suggestion.transform.insert == entry && suggestion.appendedTransform?.insert == ' ';
}));
});
});

it('generates suggestions with expected properties', async function() {
let compositor = new ModelCompositor(plainModel, true);
let context = {
Expand Down