mirror of
https://github.com/keymanapp/keyman.git
synced 2026-08-08 18:05:32 +00:00
Merge branch 'epic/autocorrect' into chore/merge-master-into-autocorrect
This commit is contained in:
commit
29f0a20898
9 changed files with 135 additions and 41 deletions
|
|
@ -108,11 +108,11 @@ public final class LanguageSettingsActivity extends AppCompatActivity {
|
|||
RadioGroup radioGroup = (RadioGroup) findViewById(R.id.suggestion_radio_group);
|
||||
radioGroup.clearCheck();
|
||||
|
||||
// Auto-correct disabled for Keyman 18.0 #12767
|
||||
int[] RadioButtonArray = {
|
||||
R.id.suggestion_radio_0,
|
||||
R.id.suggestion_radio_1,
|
||||
R.id.suggestion_radio_2};
|
||||
R.id.suggestion_radio_2,
|
||||
R.id.suggestion_radio_3};
|
||||
RadioButton radioButton = (RadioButton)radioGroup.findViewById(RadioButtonArray[maySuggest]);
|
||||
radioButton.setChecked(true);
|
||||
|
||||
|
|
|
|||
|
|
@ -90,13 +90,12 @@
|
|||
android:layout_gravity="center_vertical"
|
||||
android:text="@string/suggestions_radio_2" />
|
||||
|
||||
<!-- Auto-correct disabled for Keyman 18.0 #12767 -->
|
||||
<!--com.google.android.material.radiobutton.MaterialRadioButton
|
||||
<com.google.android.material.radiobutton.MaterialRadioButton
|
||||
android:id="@+id/suggestion_radio_3"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center_vertical"
|
||||
android:text="@string/suggestions_radio_3" /-->
|
||||
android:text="@string/suggestions_radio_3" />
|
||||
|
||||
</RadioGroup>
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
# Keyman Engine for Web
|
||||
The Original Code is (C) SIL International
|
||||
The Original Code is (C) SIL Global
|
||||
|
||||
## Prerequisites
|
||||
See [build configuration](../docs/build/index.md) for details on how to
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ export class LanguageProcessor extends EventEmitter<LanguageProcessorEventMap> {
|
|||
|
||||
private _mayPredict: boolean = true;
|
||||
private _mayCorrect: boolean = true;
|
||||
private _mayAutoCorrect: boolean = false; // initialized to false - #12767
|
||||
private _mayAutoCorrect: boolean = true;
|
||||
|
||||
private _state: StateChangeEnum = 'inactive';
|
||||
|
||||
|
|
|
|||
|
|
@ -38,7 +38,7 @@ export interface DefaultWordBreakerOptions {
|
|||
* @see http://unicode.org/reports/tr29/#Word_Boundaries
|
||||
* @see https://github.com/eddieantonio/unicode-default-word-boundary/tree/v12.0.0
|
||||
*/
|
||||
export default function default_(text: string, options?: DefaultWordBreakerOptions): LexicalModelTypes.Span[] {
|
||||
function default_(text: string, options?: DefaultWordBreakerOptions): LexicalModelTypes.Span[] {
|
||||
let boundaries = findBoundaries(text, options);
|
||||
if (boundaries.length == 0) {
|
||||
return [];
|
||||
|
|
@ -64,6 +64,16 @@ export default function default_(text: string, options?: DefaultWordBreakerOptio
|
|||
return spans;
|
||||
}
|
||||
|
||||
// Exposes `searchForProperty` for external use while associating it with this wordbreaker.
|
||||
const def = Object.assign(default_, {
|
||||
/**
|
||||
* This method returns enum values corresponding to the character type as perceived by the wordbreaking algorithm.
|
||||
*/
|
||||
searchForProperty: searchForProperty
|
||||
});
|
||||
|
||||
export default def;
|
||||
|
||||
/**
|
||||
* A span that does not cut out the substring until it absolutely has to!
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import placeholder from "./placeholder.js";
|
||||
import ascii from "./ascii.js";
|
||||
import default_ from "./default/index.js";
|
||||
import { WordBreakProperty } from "./default/data.inc.js";
|
||||
|
||||
export { placeholder, ascii, default_ as default, default_ as defaultWordbreaker };
|
||||
export { placeholder, ascii, default_ as default, default_ as defaultWordbreaker, WordBreakProperty };
|
||||
|
|
@ -6,6 +6,9 @@ import { ContextTracker, TrackedContextState } from './correction/context-tracke
|
|||
import { ExecutionTimer } from './correction/execution-timer.js';
|
||||
import ModelCompositor from './model-compositor.js';
|
||||
import { LexicalModelTypes } from '@keymanapp/common-types';
|
||||
import { defaultWordbreaker, WordBreakProperty } from '@keymanapp/models-wordbreakers';
|
||||
const searchForProperty = defaultWordbreaker.searchForProperty;
|
||||
|
||||
import Context = LexicalModelTypes.Context;
|
||||
import Distribution = LexicalModelTypes.Distribution;
|
||||
import Keep = LexicalModelTypes.Keep;
|
||||
|
|
@ -615,6 +618,35 @@ export function processSimilarity(
|
|||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* This function may be used to prevent auto-selection/auto-correct from applying in
|
||||
* unexpected ways. For example, when typing numbers in English, we don't expect
|
||||
* '5' to auto-correct to '5th' just because there are no pure-number entries in
|
||||
* the lexicon rooted on '5'.
|
||||
* @param correction
|
||||
* @returns
|
||||
*/
|
||||
export function correctionValidForAutoSelect(correction: string) {
|
||||
let chars = [...correction];
|
||||
|
||||
// If the _correction_ - the actual, existing text - does not include any letters,
|
||||
// then predictions built upon it should not be considered valid for auto-correction.
|
||||
for(let c of chars) {
|
||||
// Found even one letter? We'll consider it valid.
|
||||
switch(searchForProperty(c.codePointAt(0))) {
|
||||
case WordBreakProperty.ALetter:
|
||||
case WordBreakProperty.Hebrew_Letter:
|
||||
case WordBreakProperty.Katakana:
|
||||
return true;
|
||||
default:
|
||||
}
|
||||
}
|
||||
|
||||
// Only reached when the correction has nothing that passes as a letter in-context.
|
||||
// (MidLet and MidNumLet only count when there are adjacent letters.)
|
||||
return false;
|
||||
}
|
||||
|
||||
export function predictionAutoSelect(suggestionDistribution: CorrectionPredictionTuple[]) {
|
||||
if(suggestionDistribution.length == 0) {
|
||||
return;
|
||||
|
|
@ -633,6 +665,11 @@ export function predictionAutoSelect(suggestionDistribution: CorrectionPredictio
|
|||
suggestionDistribution = suggestionDistribution.slice(1);
|
||||
|
||||
if(suggestionDistribution.length == 1) {
|
||||
// Prevent auto-acceptance when the root doesn't meet validation criteria.
|
||||
if(!correctionValidForAutoSelect(suggestionDistribution[0].correction.sample)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Mark for auto-acceptance; there are no alternatives.
|
||||
suggestionDistribution[0].prediction.sample.autoAccept = true;
|
||||
return;
|
||||
|
|
@ -676,6 +713,10 @@ export function predictionAutoSelect(suggestionDistribution: CorrectionPredictio
|
|||
return;
|
||||
}
|
||||
|
||||
if(!correctionValidForAutoSelect(bestSuggestion.correction.sample)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// compare correction-cost aspects? We disable if the base correction is lower than best,
|
||||
// but should we do other comparisons too?
|
||||
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@ describe('predictionAutoSelect', () => {
|
|||
const predictions = [
|
||||
{
|
||||
correction: {
|
||||
sample: 'apple', // can be null / "mocked out"
|
||||
sample: 'apple',
|
||||
p: 1
|
||||
},
|
||||
prediction: {
|
||||
|
|
@ -52,6 +52,56 @@ describe('predictionAutoSelect', () => {
|
|||
assert.isOk(autoselected);
|
||||
});
|
||||
|
||||
it(`does not select suggestions if the root correction has no letters`, () => {
|
||||
/**
|
||||
* @type {import('#./predict-helpers.js').CorrectionPredictionTuple[]}
|
||||
*/
|
||||
const predictions = [
|
||||
{
|
||||
correction: {
|
||||
sample: '5',
|
||||
p: 1
|
||||
},
|
||||
prediction: {
|
||||
sample: {
|
||||
tag: 'keep',
|
||||
transform: {
|
||||
insert: '5',
|
||||
deleteLeft: 0
|
||||
},
|
||||
matchesModel: false
|
||||
},
|
||||
p: 0.01
|
||||
},
|
||||
totalProb: 0.01
|
||||
},
|
||||
{
|
||||
correction: {
|
||||
sample: '5',
|
||||
p: 1
|
||||
},
|
||||
prediction: {
|
||||
sample: {
|
||||
transform: {
|
||||
insert: '5th',
|
||||
deleteLeft: 0
|
||||
},
|
||||
matchesModel: true
|
||||
},
|
||||
p: 0.8
|
||||
},
|
||||
totalProb: 0.8
|
||||
}
|
||||
];
|
||||
|
||||
const originalPredictions = [].concat(predictions);
|
||||
assert.doesNotThrow(() => predictionAutoSelect(predictions));
|
||||
assert.sameDeepOrderedMembers(predictions, originalPredictions);
|
||||
|
||||
const autoselected = predictions.find((entry) => entry.prediction.sample.autoAccept);
|
||||
assert.isNotOk(autoselected);
|
||||
});
|
||||
|
||||
it(`does not select solitary 'keep' suggestion that doesn't match the model`, () => {
|
||||
/**
|
||||
* @type {import('#./predict-helpers.js').CorrectionPredictionTuple[]}
|
||||
|
|
@ -59,7 +109,7 @@ describe('predictionAutoSelect', () => {
|
|||
const predictions = [
|
||||
{
|
||||
correction: {
|
||||
sample: 'appl', // can be null / "mocked out"
|
||||
sample: 'appl',
|
||||
p: 1
|
||||
},
|
||||
prediction: {
|
||||
|
|
@ -91,7 +141,7 @@ describe('predictionAutoSelect', () => {
|
|||
*/
|
||||
const keepSuggestion = {
|
||||
correction: {
|
||||
sample: 'thin', // can be null / "mocked out"
|
||||
sample: 'thin',
|
||||
p: .8
|
||||
},
|
||||
prediction: {
|
||||
|
|
@ -110,7 +160,7 @@ describe('predictionAutoSelect', () => {
|
|||
|
||||
const highestNonKeepSuggestion = {
|
||||
correction: {
|
||||
sample: 'thin', // can be null / "mocked out"
|
||||
sample: 'thin',
|
||||
p: .8
|
||||
},
|
||||
prediction: {
|
||||
|
|
@ -133,7 +183,7 @@ describe('predictionAutoSelect', () => {
|
|||
highestNonKeepSuggestion,
|
||||
{
|
||||
correction: {
|
||||
sample: 'thin', // can be null / "mocked out"
|
||||
sample: 'thin',
|
||||
p: .8
|
||||
},
|
||||
prediction: {
|
||||
|
|
@ -149,7 +199,7 @@ describe('predictionAutoSelect', () => {
|
|||
},
|
||||
{
|
||||
correction: {
|
||||
sample: 'thic', // can be null / "mocked out"
|
||||
sample: 'thic',
|
||||
p: .2
|
||||
},
|
||||
prediction: {
|
||||
|
|
@ -181,7 +231,7 @@ describe('predictionAutoSelect', () => {
|
|||
*/
|
||||
const keepSuggestion = {
|
||||
correction: {
|
||||
sample: 'thin', // can be null / "mocked out"
|
||||
sample: 'thin',
|
||||
p: .8
|
||||
},
|
||||
prediction: {
|
||||
|
|
@ -204,7 +254,7 @@ describe('predictionAutoSelect', () => {
|
|||
// Refer to AUTOSELECT_PROPORTION_THRESHOLD in predict-helpers.ts.
|
||||
const onlyNonKeepSuggestion = {
|
||||
correction: {
|
||||
sample: 'thin', // can be null / "mocked out"
|
||||
sample: 'thin',
|
||||
p: .8
|
||||
},
|
||||
prediction: {
|
||||
|
|
@ -246,7 +296,7 @@ describe('predictionAutoSelect', () => {
|
|||
*/
|
||||
const keepSuggestion = {
|
||||
correction: {
|
||||
sample: 'thin', // can be null / "mocked out"
|
||||
sample: 'thin',
|
||||
p: .8
|
||||
},
|
||||
prediction: {
|
||||
|
|
@ -269,7 +319,7 @@ describe('predictionAutoSelect', () => {
|
|||
// Refer to AUTOSELECT_PROPORTION_THRESHOLD in predict-helpers.ts.
|
||||
const highestNonKeepSuggestion = {
|
||||
correction: {
|
||||
sample: 'thin', // can be null / "mocked out"
|
||||
sample: 'thin',
|
||||
p: .8
|
||||
},
|
||||
prediction: {
|
||||
|
|
@ -292,7 +342,7 @@ describe('predictionAutoSelect', () => {
|
|||
highestNonKeepSuggestion,
|
||||
{
|
||||
correction: {
|
||||
sample: 'thin', // can be null / "mocked out"
|
||||
sample: 'thin',
|
||||
p: .8
|
||||
},
|
||||
prediction: {
|
||||
|
|
@ -308,7 +358,7 @@ describe('predictionAutoSelect', () => {
|
|||
},
|
||||
{
|
||||
correction: {
|
||||
sample: 'thic', // can be null / "mocked out"
|
||||
sample: 'thic',
|
||||
p: .2
|
||||
},
|
||||
prediction: {
|
||||
|
|
@ -343,7 +393,7 @@ describe('predictionAutoSelect', () => {
|
|||
*/
|
||||
const keepSuggestion = {
|
||||
correction: {
|
||||
sample: 'thin', // can be null / "mocked out"
|
||||
sample: 'thin',
|
||||
p: .8
|
||||
},
|
||||
prediction: {
|
||||
|
|
@ -362,7 +412,7 @@ describe('predictionAutoSelect', () => {
|
|||
|
||||
const highestNonKeepSuggestion = {
|
||||
correction: {
|
||||
sample: 'thin', // can be null / "mocked out"
|
||||
sample: 'thin',
|
||||
p: .9
|
||||
},
|
||||
prediction: {
|
||||
|
|
@ -385,7 +435,7 @@ describe('predictionAutoSelect', () => {
|
|||
highestNonKeepSuggestion,
|
||||
{
|
||||
correction: {
|
||||
sample: 'thin', // can be null / "mocked out"
|
||||
sample: 'thin',
|
||||
p: .9
|
||||
},
|
||||
prediction: {
|
||||
|
|
@ -401,7 +451,7 @@ describe('predictionAutoSelect', () => {
|
|||
},
|
||||
{
|
||||
correction: {
|
||||
sample: 'thic', // can be null / "mocked out"
|
||||
sample: 'thic',
|
||||
p: .1
|
||||
},
|
||||
prediction: {
|
||||
|
|
@ -436,7 +486,7 @@ describe('predictionAutoSelect', () => {
|
|||
*/
|
||||
const keepSuggestion = {
|
||||
correction: {
|
||||
sample: 'cant', // can be null / "mocked out"
|
||||
sample: 'cant',
|
||||
p: 1
|
||||
},
|
||||
prediction: {
|
||||
|
|
@ -456,7 +506,7 @@ describe('predictionAutoSelect', () => {
|
|||
|
||||
const expectedSuggestion = {
|
||||
correction: {
|
||||
sample: 'cant', // can be null / "mocked out"
|
||||
sample: 'cant',
|
||||
p: 1
|
||||
},
|
||||
prediction: {
|
||||
|
|
@ -480,7 +530,7 @@ describe('predictionAutoSelect', () => {
|
|||
expectedSuggestion,
|
||||
{
|
||||
correction: {
|
||||
sample: 'cant', // can be null / "mocked out"
|
||||
sample: 'cant',
|
||||
p: 1
|
||||
},
|
||||
prediction: {
|
||||
|
|
@ -515,7 +565,7 @@ describe('predictionAutoSelect', () => {
|
|||
*/
|
||||
const keepSuggestion = {
|
||||
correction: {
|
||||
sample: 'thi', // can be null / "mocked out"
|
||||
sample: 'thi',
|
||||
p: .7
|
||||
},
|
||||
prediction: {
|
||||
|
|
@ -534,7 +584,7 @@ describe('predictionAutoSelect', () => {
|
|||
|
||||
const highestCorrectionSuggestion = {
|
||||
correction: {
|
||||
sample: 'thi', // can be null / "mocked out"
|
||||
sample: 'thi',
|
||||
p: .7
|
||||
},
|
||||
prediction: {
|
||||
|
|
@ -551,7 +601,7 @@ describe('predictionAutoSelect', () => {
|
|||
|
||||
const highestNonKeepSuggestion = {
|
||||
correction: {
|
||||
sample: 'the', // can be null / "mocked out"
|
||||
sample: 'the',
|
||||
p: .3
|
||||
},
|
||||
prediction: {
|
||||
|
|
|
|||
|
|
@ -159,21 +159,14 @@ describe("PredictionContext", () => {
|
|||
assert.equal(updateFake.callCount, 3);
|
||||
suggestions = updateFake.thirdCall.args[0];
|
||||
|
||||
// Note: this unit test was originally written with auto-correct on!
|
||||
// #11941 was written 2024-07-25 (added unit test for auto-correction method)
|
||||
// #12169 was written 2024-08-14, which is what added THIS unit test.
|
||||
|
||||
// This does re-use the apply-revert oriented mocking.
|
||||
// Should skip the (second) "apple", "apply", "apps" round, as it became outdated
|
||||
// by its following request before its response could be received.
|
||||
assert.deepEqual(suggestions.map((obj) => obj.displayAs), ['applied']); // '“apple”' included with auto-correct enabled.
|
||||
// Is not displayed; we only display it if auto-correct is on, as 'applied' would be automatic then.
|
||||
assert.equal(predictiveContext.keepSuggestion.displayAs, '“apple”');
|
||||
// assert.equal(suggestions.find((obj) => obj.tag == 'keep').displayAs, '“apple”'); // with auto-correct enabled.
|
||||
assert.equal(suggestions.find((obj) => obj.transform.deleteLeft != 0).displayAs, 'applied');
|
||||
assert.deepEqual(suggestions.map((obj) => obj.displayAs), ['“apple”', 'applied']);
|
||||
assert.equal(suggestions.find((obj) => obj.tag == 'keep').displayAs, '“apple”');
|
||||
// Our reused mocking doesn't directly provide the 'keep' suggestion; we
|
||||
// need to remove it before testing for set equality.
|
||||
assert.deepEqual(suggestions /*.splice(1)*/, expected);
|
||||
assert.deepEqual(suggestions.splice(1), expected);
|
||||
});
|
||||
|
||||
it('sendUpdateState retrieves the most recent suggestion set', async function() {
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue