diff --git a/common/predictive-text/message.d.ts b/common/predictive-text/message.d.ts index 3e2e7f0833..e42b5c681a 100644 --- a/common/predictive-text/message.d.ts +++ b/common/predictive-text/message.d.ts @@ -216,6 +216,13 @@ interface Context { * A concrete suggestion */ interface Suggestion { + /** + * Indicates the externally-supplied id of the Transform that prompted + * the Suggestion. Automatically handled by the LMLayer; models should + * not handle this field. + */ + transformId?: number; + /** * The suggested update to the buffer. Note that this transform should * be applied AFTER the instigating transform, if any. diff --git a/common/predictive-text/worker/index.ts b/common/predictive-text/worker/index.ts index a842ddffd5..29fdec90c3 100644 --- a/common/predictive-text/worker/index.ts +++ b/common/predictive-text/worker/index.ts @@ -233,9 +233,17 @@ class LMLayerWorker { switch(payload.message) { case 'predict': let {transform, context} = payload; + + // Let's not rely on the model to copy transform IDs. + let suggestions = model.predict(transform, context); + suggestions.forEach(function(s: Suggestion) { + s.transformId = transform.id; + }); + + // Now that the suggestions are ready, send them out! this.cast('suggestions', { token: payload.token, - suggestions: model.predict(transform, context) + suggestions: suggestions }); break; case 'unload': diff --git a/web/source/dom/contentEditable.ts b/web/source/dom/contentEditable.ts index 0a858fd2be..d6713cadee 100644 --- a/web/source/dom/contentEditable.ts +++ b/web/source/dom/contentEditable.ts @@ -190,7 +190,7 @@ namespace com.keyman.dom { var n = start.node.ownerDocument.createTextNode(s); let range = this.root.ownerDocument.createRange(); - range.setStart(start.node, s.length); + range.setStart(start.node, start.offset); range.collapse(true); range.insertNode(n); } @@ -207,5 +207,35 @@ namespace com.keyman.dom { } Lsel.collapseToEnd(); } + + protected setTextAfterCaret(s: string) { + if(!this.hasSelection()) { + return; + } + + let caret = this.getCarets().end; + let delta = s._kmwLength(); + let Lsel = this.root.ownerDocument.getSelection(); + + if(delta == 0) { + return; + } + + // This is designed explicitly for use in direct-setting operations; deadkeys + // will be handled after this method. + + if(caret.node.nodeType == 3) { + let textStart = caret.node; + textStart.replaceData(caret.offset, textStart.length, s); + } else { + // Create a new text node - empty control + var n = caret.node.ownerDocument.createTextNode(s); + + let range = this.root.ownerDocument.createRange(); + range.setStart(caret.node, caret.offset); + range.collapse(true); + range.insertNode(n); + } + } } } \ No newline at end of file diff --git a/web/source/dom/designIFrame.ts b/web/source/dom/designIFrame.ts index 53e4ae95a9..b20b2cdbff 100644 --- a/web/source/dom/designIFrame.ts +++ b/web/source/dom/designIFrame.ts @@ -196,7 +196,7 @@ namespace com.keyman.dom { var n = this.doc.createTextNode(s); let range = this.doc.createRange(); - range.setStart(start.node, s.length); + range.setStart(start.node, start.offset); range.collapse(true); range.insertNode(n); } @@ -214,6 +214,36 @@ namespace com.keyman.dom { Lsel.collapseToEnd(); } + protected setTextAfterCaret(s: string) { + if(!this.hasSelection()) { + return; + } + + let caret = this.getCarets().end; + let delta = s._kmwLength(); + let Lsel = this.doc.getSelection(); + + if(delta == 0) { + return; + } + + // This is designed explicitly for use in direct-setting operations; deadkeys + // will be handled after this method. + + if(caret.node.nodeType == 3) { + let textStart = caret.node; + textStart.replaceData(caret.offset, textStart.length, s); + } else { + // Create a new text node - empty control + var n = caret.node.ownerDocument.createTextNode(s); + + let range = this.root.ownerDocument.createRange(); + range.setStart(caret.node, caret.offset); + range.collapse(true); + range.insertNode(n); + } + } + /** * Function saveProperties * Scope Private diff --git a/web/source/dom/input.ts b/web/source/dom/input.ts index 46cab77506..d432f5ce99 100644 --- a/web/source/dom/input.ts +++ b/web/source/dom/input.ts @@ -92,6 +92,13 @@ namespace com.keyman.dom { this.setCaret(newCaret); } + protected setTextAfterCaret(s: string) { + let c = this.getCaret(); + + this.root.value = this.getTextBeforeCaret() + s; + this.setCaret(c); + } + getTextAfterCaret(): string { this.getCaret(); return this.getText()._kmwSubstring(this.processedSelectionEnd); diff --git a/web/source/dom/textarea.ts b/web/source/dom/textarea.ts index aa9e539c31..de9d82027f 100644 --- a/web/source/dom/textarea.ts +++ b/web/source/dom/textarea.ts @@ -99,6 +99,13 @@ namespace com.keyman.dom { this.setCaret(newCaret); } + protected setTextAfterCaret(s: string) { + let c = this.getCaret(); + + this.root.value = this.getTextBeforeCaret() + s; + this.setCaret(c); + } + getTextAfterCaret(): string { this.getCaret(); return this.getText()._kmwSubstring(this.processedSelectionEnd); diff --git a/web/source/dom/touchAlias.ts b/web/source/dom/touchAlias.ts index ca7941a4a9..e64948235b 100644 --- a/web/source/dom/touchAlias.ts +++ b/web/source/dom/touchAlias.ts @@ -59,5 +59,9 @@ namespace com.keyman.dom { this.adjustDeadkeys(s._kmwLength()); this.root.setTextBeforeCaret(this.root.getTextBeforeCaret() + s); } + + protected setTextAfterCaret(s: string) { + this.root.setText(this.getTextBeforeCaret() + s, this.getTextBeforeCaret()._kmwLength()); + } } } \ No newline at end of file diff --git a/web/source/dom/touchAliasElement.ts b/web/source/dom/touchAliasElement.ts index a0919cc530..448368ba06 100644 --- a/web/source/dom/touchAliasElement.ts +++ b/web/source/dom/touchAliasElement.ts @@ -295,7 +295,7 @@ namespace com.keyman.dom { this.setText(textValue, null); } - private setText(t?: string, cp?: number): void { + setText(t?: string, cp?: number): void { var tLen=0; var t1: string, t2: string; diff --git a/web/source/text/outputTarget.ts b/web/source/text/outputTarget.ts index 19f85080e6..b4f99f8fcd 100644 --- a/web/source/text/outputTarget.ts +++ b/web/source/text/outputTarget.ts @@ -162,6 +162,37 @@ namespace com.keyman.text { return new Transcription(keyEvent, transform, Mock.from(original), removedDks, insertedDks); } + /** + * Restores the `OutputTarget` to the indicated state. Designed for use with `Transcription.preInput`. + * @param original An `OutputTarget` (usually a `Mock`). + */ + restoreTo(original: OutputTarget) { + // + this.setTextBeforeCaret(original.getTextBeforeCaret()); + this.setTextAfterCaret(original.getTextAfterCaret()); + + // Also, restore the deadkeys! + this._dks = original._dks.clone(); + } + + /** + * Helper to `restoreTo` - allows directly setting the 'before' context to that of another + * `OutputTarget`. + * @param s + */ + protected setTextBeforeCaret(s: string): void { + // This one's easy enough to provide a default implementation for. + this.deleteCharsBeforeCaret(this.getTextBeforeCaret()._kmwLength()); + this.insertTextBeforeCaret(s); + } + + /** + * Helper to `restoreTo` - allows directly setting the 'after' context to that of another + * `OutputTarget`. + * @param s + */ + protected abstract setTextAfterCaret(s: string): void; + /** * Returns the underlying element / document modeled by the wrapper. */ @@ -317,5 +348,9 @@ namespace com.keyman.text { this.text = this.getTextBeforeCaret() + s + this.getTextAfterCaret(); this.caretIndex += s.kmwLength(); } + + protected setTextAfterCaret(s: string): void { + this.text = this.getTextBeforeCaret() + s; + } } } \ No newline at end of file diff --git a/web/source/text/prediction/modelManager.ts b/web/source/text/prediction/modelManager.ts index a7c2dad1eb..fd385e78c5 100644 --- a/web/source/text/prediction/modelManager.ts +++ b/web/source/text/prediction/modelManager.ts @@ -251,6 +251,21 @@ namespace com.keyman.text.prediction { } } + /** + * Retrieves the context and output state of KMW immediately before the prediction with + * token `id` was generated. Must correspond to a 'recent' one, as only so many are stored + * in `ModelManager`'s history buffer. + * @param id A unique identifier corresponding to a recent `Transcription`. + * @returns The matching `Transcription`, or `null` none is found. + */ + public getPredictionState(id: number): Transcription { + let match = this.recentTranscriptions.filter(function(t: Transcription) { + return t.token == id; + }) + + return match.length == 0 ? null : match[0]; + } + public shutdown() { this.lmEngine.shutdown(); }