diff --git a/common/core/web/input-processor/src/text/inputProcessor.ts b/common/core/web/input-processor/src/text/inputProcessor.ts index 65780232033..97c428d7d1b 100644 --- a/common/core/web/input-processor/src/text/inputProcessor.ts +++ b/common/core/web/input-processor/src/text/inputProcessor.ts @@ -171,6 +171,11 @@ namespace com.keyman.text { if(pair.p < KEYSTROKE_EPSILON) { break; } else if(timer && timer() >= TIMEOUT_THRESHOLD) { + // Note: it's always possible that the thread _executing_ our JS + // got paused by the OS, even if JS itself is single-threaded. + // + // The case where `alternates` is initialized (line 167) but empty + // (because of net-zero loop iterations) MUST be handled. break; } @@ -187,6 +192,8 @@ namespace com.keyman.text { // If alternateBehavior.beep == true, ignore it. It's a disallowed key sequence, // so we expect users to never intend their use. + // + // Also possible that this set of conditions fail for all evaluated alternates. if(alternateBehavior && !alternateBehavior.beep && pair.p > 0) { let transform: Transform = alternateBehavior.transcription.transform; @@ -215,7 +222,9 @@ namespace com.keyman.text { // -- All keystroke (and 'alternate') processing is now complete. Time to finalize everything! -- // Notify the ModelManager of new input - it's predictive text time! - ruleBehavior.transcription.alternates = alternates; + if(alternates && alternates.length > 0) { + ruleBehavior.transcription.alternates = alternates; + } // Yes, even for ruleBehavior.triggersDefaultCommand. Those tend to change the context. ruleBehavior.predictionPromise = this.languageProcessor.predict(ruleBehavior.transcription); diff --git a/common/core/web/input-processor/src/text/prediction/languageProcessor.ts b/common/core/web/input-processor/src/text/prediction/languageProcessor.ts index 17faf9e15de..d9a74b3a877 100644 --- a/common/core/web/input-processor/src/text/prediction/languageProcessor.ts +++ b/common/core/web/input-processor/src/text/prediction/languageProcessor.ts @@ -323,8 +323,16 @@ namespace com.keyman.text.prediction { this.lmEngine.resetContext(context); } + let alternates = transcription.alternates; + if(!alternates || alternates.length == 0) { + alternates = [{ + sample: transcription.transform, + p: 1.0 + }]; + } + let transform = transcription.transform; - var promise = this.currentPromise = this.lmEngine.predict(transcription.alternates || transcription.transform, context); + var promise = this.currentPromise = this.lmEngine.predict(alternates, context); let lp = this; return promise.then(function(suggestions: Suggestion[]) { diff --git a/common/predictive-text/worker/model-compositor.ts b/common/predictive-text/worker/model-compositor.ts index 5c41e919576..1cd69d9616a 100644 --- a/common/predictive-text/worker/model-compositor.ts +++ b/common/predictive-text/worker/model-compositor.ts @@ -73,6 +73,25 @@ class ModelCompositor { if(!(transformDistribution instanceof Array)) { transformDistribution = [ {sample: transformDistribution, p: 1.0} ]; + } else if(transformDistribution.length == 0) { + /* + Robust stop-gap: if our other filters somehow fail, this fixes the + zero-length array by making it match the form of the array that + would result if it were instead called with the other legal + parameter type - a single Transform. + + Unfortunately, the method will lack all data about even + the original keystroke that resulted in the call... but this way, + we can at least get some predictions rather than shortcutting + and producing none whatsoever. + */ + transformDistribution.push({ + sample: { + insert: '', + deleteLeft: 0 + }, + p: 1.0 + }) } let inputTransform = transformDistribution.sort(function(a, b) {