From 6599458ba48194224afb56e44cd9facc17ce7bfd Mon Sep 17 00:00:00 2001 From: Asaf Mahlev Date: Tue, 29 Sep 2026 18:19:00 +0300 Subject: [PATCH] fix: select the actual missing-apostrophe contraction candidate Bind contraction auto-correction to the candidate being evaluated and promote that existing transformed entry only when correction is allowed. Prefer available canonical spellings while preserving typed alternatives, capitalization, whitelist and shortcut intent. Cover ranked distractors, canonical casing, disabled and resumed input, known words, multilingual input, dictionary shortcuts, and the space-commit path with deterministic Robolectric regressions. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../java/helium314/keyboard/latin/Suggest.kt | 42 ++- .../keyboard/latin/InputLogicTest.kt | 43 +++ .../helium314/keyboard/latin/SuggestTest.kt | 276 +++++++++++++++++- docs/FEATURES.md | 1 + 4 files changed, 351 insertions(+), 11 deletions(-) diff --git a/app/src/main/java/helium314/keyboard/latin/Suggest.kt b/app/src/main/java/helium314/keyboard/latin/Suggest.kt index 902d1e598..da9021433 100644 --- a/app/src/main/java/helium314/keyboard/latin/Suggest.kt +++ b/app/src/main/java/helium314/keyboard/latin/Suggest.kt @@ -118,12 +118,23 @@ class Suggest(private val mDictionaryFacilitator: DictionaryFacilitator) { val firstOccurrenceOfTypedWordInSuggestions = SuggestedWordInfo.removeDupsAndTypedWord(capitalizedTypedWord, suggestionsContainer) makeFirstTwoSuggestionsNonEmoji(suggestionsContainer) + val firstSuggestion = suggestionsContainer.firstOrNull() + val expectedContraction = getExpectedContraction( + capitalizedTypedWord.dropLast(trailingSingleQuotesCount), trailingSingleQuotesCount) + val correctionCandidate = if (typedWordFirstOccurrenceWordInfo == null && expectedContraction != null + && firstSuggestion?.isKindOf(SuggestedWordInfo.KIND_WHITELIST) != true + && firstSuggestion?.isKindOf(SuggestedWordInfo.KIND_SHORTCUT) != true) { + // Prefer the existing canonical spelling, then a Shift/CapsLock or dictionary variant. + suggestionsContainer.firstOrNull { it.mWord == expectedContraction } + ?: suggestionsContainer.firstOrNull { it.mWord.equals(expectedContraction, ignoreCase = true) } + ?: firstSuggestion + } else firstSuggestion val (allowsToBeAutoCorrected, hasAutoCorrection) = shouldBeAutoCorrected( trailingSingleQuotesCount, capitalizedTypedWord, - suggestionsContainer.firstOrNull(), + correctionCandidate, { - val first = suggestionsContainer.firstOrNull() ?: suggestionResults.first() + val first = correctionCandidate ?: suggestionResults.first() val suggestions = getNextWordSuggestions(ngramContext, keyboard, inputStyleIfNotPrediction, settingsValuesForSuggestion) val suggestionForFirstInContainer = suggestions.firstOrNull { it.mWord == first.word } val suggestionForTypedWord = suggestions.firstOrNull { it.mWord == capitalizedTypedWord } @@ -135,6 +146,10 @@ class Suggest(private val mDictionaryFacilitator: DictionaryFacilitator) { firstOccurrenceOfTypedWordInSuggestions, typedWordFirstOccurrenceWordInfo ) + if (hasAutoCorrection && correctionCandidate != null && correctionCandidate !== firstSuggestion) { + suggestionsContainer.remove(correctionCandidate) + suggestionsContainer.add(0, correctionCandidate) + } val typedWordInfo = SuggestedWordInfo(capitalizedTypedWord, "", SuggestedWordInfo.MAX_SCORE, SuggestedWordInfo.KIND_TYPED, typedWordFirstOccurrenceWordInfo?.mSourceDict ?: Dictionary.DICTIONARY_USER_TYPED, SuggestedWordInfo.NOT_AN_INDEX , SuggestedWordInfo.NOT_A_CONFIDENCE) @@ -272,19 +287,20 @@ class Suggest(private val mDictionaryFacilitator: DictionaryFacilitator) { hasAutoCorrection = false } else { val firstSuggestion = firstSuggestionInContainer ?: suggestionResults.first() + val expectedContraction = getExpectedContraction(consideredWord, trailingSingleQuotesCount) + if (expectedContraction != null && typedWordInfo == null + && firstSuggestion.isKindOf(SuggestedWordInfo.KIND_SHORTCUT) + && !Settings.getValues().mAutoCorrectShortcuts) { + return true to false + } if (suggestionResults.mFirstSuggestionExceedsConfidenceThreshold && firstOccurrenceOfTypedWordInSuggestions != 0) { // mFirstSuggestionExceedsConfidenceThreshold is always set to false, so currently this branch is useless return true to true } - val lowerConsidered = consideredWord.lowercase(Locale.ROOT) - val expectedContraction = COMMON_CONTRACTIONS[lowerConsidered] - if (expectedContraction != null && typedWordInfo == null) { - // If typed word matches a missing-apostrophe contraction (e.g. dont -> don't), promote it - val contractionMatch = suggestionResults.firstOrNull { it.mWord.equals(expectedContraction, ignoreCase = true) } - if (contractionMatch != null || firstSuggestion.mWord.equals(expectedContraction, ignoreCase = true)) { - return true to true - } + if (expectedContraction != null && typedWordInfo == null + && firstSuggestion.mWord.equals(expectedContraction, ignoreCase = true)) { + return true to true } // For short words (<= 3 chars) not in the dictionary (e.g. Ab, yt, tg, db), @@ -585,6 +601,12 @@ class Suggest(private val mDictionaryFacilitator: DictionaryFacilitator) { "lets" to "let's", "itll" to "it'll", "youll" to "you'll", "theyll" to "they'll" ) + private fun getExpectedContraction(consideredWord: String, trailingSingleQuotesCount: Int): String? { + val contraction = COMMON_CONTRACTIONS[consideredWord.lowercase(Locale.ROOT)] ?: return null + // Contractions already contain one apostrophe; match the normal trailing-quote transform. + return contraction + "'".repeat((trailingSingleQuotesCount - 1).coerceAtLeast(0)) + } + private fun isDeveloperTokenOrSpecialSyntax(word: String): Boolean { if (word.isEmpty()) return false if (word.startsWith('#') || word.startsWith('@') || word.startsWith('/') || word.startsWith('.')) return true diff --git a/app/src/test/java/helium314/keyboard/latin/InputLogicTest.kt b/app/src/test/java/helium314/keyboard/latin/InputLogicTest.kt index d9b5b2324..74212a835 100644 --- a/app/src/test/java/helium314/keyboard/latin/InputLogicTest.kt +++ b/app/src/test/java/helium314/keyboard/latin/InputLogicTest.kt @@ -10,11 +10,16 @@ import android.view.KeyEvent import android.view.inputmethod.* import androidx.core.content.edit import org.junit.Ignore +import helium314.keyboard.ShadowBinaryDictionaryUtils import helium314.keyboard.ShadowInputMethodManager2 import helium314.keyboard.ShadowLocaleManagerCompat import helium314.keyboard.event.Event +import helium314.keyboard.keyboard.Keyboard +import helium314.keyboard.keyboard.KeyboardId +import helium314.keyboard.keyboard.KeyboardLayoutSet import helium314.keyboard.keyboard.KeyboardSwitcher import helium314.keyboard.keyboard.MainKeyboardView +import helium314.keyboard.keyboard.internal.KeyboardParams import helium314.keyboard.keyboard.internal.keyboard_parser.floris.KeyCode import helium314.keyboard.latin.ShadowFacilitator2.Companion.addedWords import helium314.keyboard.latin.ShadowFacilitator2.Companion.lastAddedWord @@ -28,7 +33,9 @@ import helium314.keyboard.latin.dictionary.Dictionary import helium314.keyboard.latin.inputlogic.InputLogic import helium314.keyboard.latin.inputlogic.SpaceState import helium314.keyboard.latin.settings.Settings +import helium314.keyboard.latin.settings.SettingsValuesForSuggestion import helium314.keyboard.latin.utils.ScriptUtils +import helium314.keyboard.latin.utils.SuggestionResults import helium314.keyboard.latin.utils.SubtypeSettings import helium314.keyboard.latin.utils.getTimestampFormatter import helium314.keyboard.latin.utils.prefs @@ -46,6 +53,7 @@ import kotlin.streams.asSequence import kotlin.test.BeforeTest import kotlin.test.Test import kotlin.test.assertEquals +import kotlin.test.assertTrue @RunWith(RobolectricTestRunner::class) @Config(shadows = [ @@ -98,6 +106,41 @@ class InputLogicTest { assertEquals("", composingText) } + @Config(shadows = [ShadowFacilitator::class, ShadowBinaryDictionaryUtils::class]) + @Test fun `space commits selected contraction rather than higher ranked distractors`() { + for (distractor in listOf("Youre", "yours", "You're")) { + typingSuggestionResults = SuggestionResults(0, false, false) + reset() + latinIME.prefs().edit { putBoolean(Settings.PREF_MORE_AUTO_CORRECTION, true) } + chainInput("I think youre") + assertEquals("youre", composer.typedWord) + assertTrue(settingsValues.mAutoCorrectEnabled) + typingSuggestionResults = SuggestionResults(2, false, false).apply { + add(suggestion(distractor, 900000, Locale.ENGLISH)) + add(suggestion("you're", 100000, Locale.ENGLISH)) + } + val keyboard = Keyboard(KeyboardParams().apply { + mId = KeyboardLayoutSet.getFakeKeyboardId(KeyboardId.ELEMENT_ALPHABET) + GRID_WIDTH = 1 + GRID_HEIGHT = 1 + }) + val result = inputLogic.suggest.getSuggestedWords( + composer, NgramContext(NgramContext.WordInfo("think")), keyboard, + SettingsValuesForSuggestion(false, false, ""), true, SuggestedWords.INPUT_STYLE_TYPING, 42 + ) + inputLogic.setSuggestedWords(result) + assertTrue(result.mWillAutoCorrect) + assertEquals("you're", composer.getAutoCorrectionOrNull()?.mWord) + latinIME.mHandler.cancelUpdateSuggestionStrip() + latinIME.onEvent(Event.createEventForCodePointFromUnknownSource(Constants.CODE_SPACE)) + handleMessages() + + assertEquals("I think you're ", text) + assertEquals("", composingText) + checkConnectionConsistency() + } + } + @Test fun delete() { reset() setText("hello there ") diff --git a/app/src/test/java/helium314/keyboard/latin/SuggestTest.kt b/app/src/test/java/helium314/keyboard/latin/SuggestTest.kt index da51f83ae..5953ecea6 100644 --- a/app/src/test/java/helium314/keyboard/latin/SuggestTest.kt +++ b/app/src/test/java/helium314/keyboard/latin/SuggestTest.kt @@ -5,6 +5,11 @@ import androidx.core.content.edit import helium314.keyboard.ShadowBinaryDictionaryUtils import helium314.keyboard.ShadowInputMethodManager2 import helium314.keyboard.ShadowLocaleManagerCompat +import helium314.keyboard.event.Event +import helium314.keyboard.keyboard.Keyboard +import helium314.keyboard.keyboard.KeyboardId +import helium314.keyboard.keyboard.KeyboardLayoutSet +import helium314.keyboard.keyboard.internal.KeyboardParams import helium314.keyboard.latin.SuggestedWords.SuggestedWordInfo import helium314.keyboard.latin.common.ComposedData import helium314.keyboard.latin.common.StringUtils @@ -25,6 +30,9 @@ import java.util.* import kotlin.test.BeforeTest import kotlin.test.Test import kotlin.test.assertEquals +import kotlin.test.assertFalse +import kotlin.test.assertSame +import kotlin.test.assertTrue @Suppress("NonAsciiCharacters") @RunWith(RobolectricTestRunner::class) @@ -44,6 +52,9 @@ class SuggestTest { private val thresholdVeryAggressive = -1f @BeforeTest fun setUp() { + currentTypingLocale = Locale.ENGLISH + typingSuggestionResults = SuggestionResults(0, false, false) + mainDictionaryInitialized = true latinIME = Robolectric.setupService(LatinIME::class.java) // start logging only after latinIME is created, avoids showing the stack traces if library is not found ShadowLog.setupLogging() @@ -291,6 +302,253 @@ class SuggestTest { assert(result.last()) // should be corrected } + @Test fun `missing apostrophe promotes the actual contraction past a case-only distractor`() { + val contraction = suggestion("you're", 100000, Locale.ENGLISH) + val candidates = listOf( + suggestion("Youre", 900000, Locale.ENGLISH), + suggestion("your", 800000, Locale.ENGLISH), + suggestion("yours", 700000, Locale.ENGLISH), + suggestion("yore", 600000, Locale.ENGLISH), + contraction + ) + val result = getSuggestedWords("youre", candidates) + + assertTrue(result.mWillAutoCorrect) + assertSame(contraction, result.getInfo(SuggestedWords.INDEX_OF_AUTO_CORRECTION)) + assertEquals("youre", result.getWord(SuggestedWords.INDEX_OF_TYPED_WORD)) + assertEquals("youre", result.getWord(2)) + assertEquals(listOf("youre", "you're", "youre", "Youre", "your", "yours", "yore"), + (0 until result.size()).map(result::getWord)) + assertEquals(SuggestedWords.INPUT_STYLE_TYPING, result.mInputStyle) + assertEquals(42, result.mSequenceNumber) + assertFalse(result.mTypedWordValid) + } + + @Test fun `missing apostrophe does not authorize an unrelated first candidate`() { + val contraction = suggestion("you're", 100000, Locale.ENGLISH) + val result = getSuggestedWords("youre", listOf( + suggestion("yours", 900000, Locale.ENGLISH), contraction)) + + assertTrue(result.mWillAutoCorrect) + assertSame(contraction, result.getInfo(SuggestedWords.INDEX_OF_AUTO_CORRECTION)) + assertEquals("youre", result.getWord(2)) + assertEquals("yours", result.getWord(3)) + } + + @Test fun `unshifted contraction prefers existing canonical spelling over a higher capitalized variant`() { + val contraction = suggestion("you're", 100000, Locale.ENGLISH) + val result = getSuggestedWords("youre", listOf( + suggestion("Youre", 950000, Locale.ENGLISH), + suggestion("You're", 900000, Locale.ENGLISH), + contraction + )) + assertTrue(result.mWillAutoCorrect) + assertSame(contraction, result.getInfo(SuggestedWords.INDEX_OF_AUTO_CORRECTION)) + assertEquals(listOf("youre", "you're", "youre", "Youre", "You're"), + (0 until result.size()).map(result::getWord)) + } + + @Test fun `canonical contraction spelling retains English I capitalization`() { + val contraction = suggestion("I'm", 100000, Locale.ENGLISH) + val result = getSuggestedWords("im", listOf( + suggestion("Im", 950000, Locale.ENGLISH), + suggestion("i'm", 900000, Locale.ENGLISH), + contraction + )) + assertTrue(result.mWillAutoCorrect) + assertSame(contraction, result.getInfo(SuggestedWords.INDEX_OF_AUTO_CORRECTION)) + } + + @Test fun `contraction shortcut only authorizes the candidate being evaluated`() { + val result = shouldBeAutoCorrected( + "youre", + listOf(suggestion("Youre", 900000, Locale.ENGLISH), + suggestion("you're", 100000, Locale.ENGLISH)), + null, null, Locale.ENGLISH, thresholdModest + ) + assertFalse(result.last()) + } + + @Test fun `promoted contraction retains sentence-start and manual capitalization`() { + val candidates = listOf( + suggestion("yours", 900000, Locale.ENGLISH), + suggestion("you're", 100000, Locale.ENGLISH), + suggestion("You're", 90000, Locale.ENGLISH) + ) + val cases = listOf( + Triple("Youre", KeyboardId.ELEMENT_ALPHABET, WordComposer.CAPS_MODE_AUTO_SHIFTED), + Triple("Youre", KeyboardId.ELEMENT_ALPHABET, WordComposer.CAPS_MODE_MANUAL_SHIFTED), + Triple("youre", KeyboardId.ELEMENT_ALPHABET_MANUAL_SHIFTED, WordComposer.CAPS_MODE_OFF), + Triple("YOURE", KeyboardId.ELEMENT_ALPHABET, WordComposer.CAPS_MODE_MANUAL_SHIFT_LOCKED), + Triple("youre", KeyboardId.ELEMENT_ALPHABET_SHIFT_LOCKED, WordComposer.CAPS_MODE_OFF) + ) + for ((word, element, capsMode) in cases) { + val result = getSuggestedWords(word, candidates, keyboardElement = element, capsMode = capsMode) + val expected = if (word == "YOURE" || element == KeyboardId.ELEMENT_ALPHABET_SHIFT_LOCKED) + "YOU'RE" else "You're" + assertTrue(result.mWillAutoCorrect, "$word / $element / $capsMode") + assertEquals(expected, result.getWord(SuggestedWords.INDEX_OF_AUTO_CORRECTION)) + assertEquals(1, (0 until result.size()).count { result.getWord(it) == expected }) + val promoted = result.getInfo(SuggestedWords.INDEX_OF_AUTO_CORRECTION) + assertEquals(candidates[1].mScore, promoted.mScore) + assertEquals(candidates[1].mKindAndFlags, promoted.mKindAndFlags) + assertSame(candidates[1].mSourceDict, promoted.mSourceDict) + assertEquals(result.getWord(SuggestedWords.INDEX_OF_TYPED_WORD), result.getWord(2)) + } + } + + @Test fun `promoted contraction retains trailing quote transformation`() { + for ((word, expected) in listOf("youre'" to "you're", "youre''" to "you're'")) { + val result = getSuggestedWords(word, listOf( + suggestion("yours", 900000, Locale.ENGLISH), + suggestion("you're", 100000, Locale.ENGLISH) + )) + assertTrue(result.mWillAutoCorrect) + assertEquals(expected, result.getWord(SuggestedWords.INDEX_OF_AUTO_CORRECTION)) + assertEquals(word, result.getWord(2)) + } + } + + @Test fun `disabled correction resumed input and missing main dictionary do not promote contractions`() { + val candidates = listOf( + suggestion("Youre", 900000, Locale.ENGLISH), + suggestion("you're", 100000, Locale.ENGLISH) + ) + val disabled = getSuggestedWords("youre", candidates, correctionEnabled = false) + val resumed = getSuggestedWords("youre", candidates, resumed = true) + mainDictionaryInitialized = false + val noDictionary = getSuggestedWords("youre", candidates) + for (result in listOf(disabled, resumed, noDictionary)) { + assertFalse(result.mWillAutoCorrect) + assertEquals("Youre", result.getWord(1)) + assertEquals(listOf("Youre", "you're"), + (0 until result.size()).map(result::getWord).filter { it != "youre" }) + } + } + + @Test fun `known typed words are not replaced by contractions including in another language`() { + for ((word, contraction, locale) in listOf( + Triple("cant", "can't", Locale.ENGLISH), + Triple("dont", "don't", Locale.FRENCH), + Triple("you're", "Youre", Locale.ENGLISH) + )) { + val result = getSuggestedWords(word, listOf( + suggestion(contraction, 1900000, Locale.ENGLISH), + suggestion(word, 1600000, locale) + ), typingLocale = locale) + assertTrue(result.mTypedWordValid) + assertFalse(result.mWillAutoCorrect) + assertEquals(word, result.mTypedWordInfo?.mWord) + assertTrue((0 until result.size()).any { result.getWord(it) == word }) + } + } + + @Test fun `existing dictionary whitelist takes priority over contraction promotion`() { + val whitelist = suggestion("your", Int.MAX_VALUE, Locale.ENGLISH) + val result = getSuggestedWords("youre", listOf( + whitelist, suggestion("you're", 100000, Locale.ENGLISH))) + assertTrue(result.mWillAutoCorrect) + assertSame(whitelist, result.getInfo(SuggestedWords.INDEX_OF_AUTO_CORRECTION)) + } + + @Test fun `explicit dictionary shortcut keeps priority and respects its correction setting`() { + val shortcut = suggestion("your", 1200000, Locale.ENGLISH, shortcut = true) + val candidates = listOf(shortcut, suggestion("you're", 100000, Locale.ENGLISH)) + for (enabled in listOf(true, false)) { + latinIME.prefs().edit { putBoolean(Settings.PREF_AUTOCORRECT_SHORTCUTS, enabled) } + val result = getSuggestedWords("youre", candidates) + assertEquals(enabled, result.mWillAutoCorrect) + assertSame(shortcut, result.getInfo(1)) + } + } + + @Test fun `contraction shortcut does not bypass disabled dictionary shortcuts`() { + val contraction = suggestion("you're", 100000, Locale.ENGLISH, shortcut = true) + val candidates = listOf(suggestion("Youre", 900000, Locale.ENGLISH), contraction) + for (enabled in listOf(true, false)) { + latinIME.prefs().edit { putBoolean(Settings.PREF_AUTOCORRECT_SHORTCUTS, enabled) } + val result = getSuggestedWords("youre", candidates) + assertEquals(enabled, result.mWillAutoCorrect) + assertSame(if (enabled) contraction else candidates[0], result.getInfo(1)) + } + } + + @Test fun `absent contractions are not synthesized and case-only protection remains`() { + val result = getSuggestedWords("youre", listOf(suggestion("Youre", 900000, Locale.ENGLISH))) + assertFalse(result.mWillAutoCorrect) + assertEquals("Youre", result.getWord(1)) + assertFalse((0 until result.size()).any { result.getWord(it) == "you're" }) + } + + @Test fun `contraction promotion does not rewrite dictionary casing`() { + val contraction = suggestion("You're", 100000, Locale.ENGLISH) + val result = getSuggestedWords("youre", listOf( + suggestion("yours", 900000, Locale.ENGLISH), contraction)) + assertTrue(result.mWillAutoCorrect) + assertSame(contraction, result.getInfo(SuggestedWords.INDEX_OF_AUTO_CORRECTION)) + } + + @Test fun `existing contraction matching still works with English as a secondary language`() { + val contraction = suggestion("you're", 100000, Locale.ENGLISH) + val result = getSuggestedWords("youre", listOf( + suggestion("yourte", 900000, Locale.FRENCH), contraction), typingLocale = Locale.FRENCH) + assertTrue(result.mWillAutoCorrect) + assertSame(contraction, result.getInfo(SuggestedWords.INDEX_OF_AUTO_CORRECTION)) + } + + @Test fun `intentional mixed case is not replaced or reordered`() { + val result = getSuggestedWords("yOUre", listOf( + suggestion("yours", 900000, Locale.ENGLISH), + suggestion("you're", 100000, Locale.ENGLISH))) + assertFalse(result.mWillAutoCorrect) + assertEquals("yours", result.getWord(1)) + } + + private fun getSuggestedWords( + word: String, + candidates: List, + correctionEnabled: Boolean = true, + keyboardElement: Int = KeyboardId.ELEMENT_ALPHABET, + capsMode: Int = WordComposer.CAPS_MODE_OFF, + resumed: Boolean = false, + typingLocale: Locale = Locale.ENGLISH + ): SuggestedWords { + currentTypingLocale = typingLocale + val dictionaryResults = SuggestionResults(candidates.size, false, false).apply { + addAll(candidates) + mRawSuggestions?.addAll(candidates) + } + val originalOrder = dictionaryResults.toList() + typingSuggestionResults = dictionaryResults + suggest.clearNextWordSuggestionsCache() + suggest.setAutoCorrectionThreshold(thresholdModest) + val composer = WordComposer() + if (resumed) { + val codePoints = StringUtils.toCodePointArray(word) + composer.setComposingWord(codePoints, IntArray(codePoints.size * 2)) + } else { + word.codePoints().forEach { + composer.applyProcessedEvent(composer.processEvent( + Event.createEventForCodePointFromUnknownSource(it))) + } + } + composer.setCapitalizedModeAtStartComposingTime(capsMode) + val keyboard = Keyboard(KeyboardParams().apply { + mId = KeyboardLayoutSet.getFakeKeyboardId(keyboardElement) + GRID_WIDTH = 1 + GRID_HEIGHT = 1 + }) + val result = suggest.getSuggestedWords( + composer, NgramContext(NgramContext.WordInfo("think")), keyboard, + SettingsValuesForSuggestion(false, false, ""), + correctionEnabled, SuggestedWords.INPUT_STYLE_TYPING, 42 + ) + assertEquals(originalOrder, dictionaryResults.toList()) + assertSame(dictionaryResults.mRawSuggestions, result.mRawSuggestions) + result.mRawSuggestions?.let { assertEquals(candidates, it) } + return result + } + private fun shouldBeAutoCorrected(word: String, // typed word suggestions: List, // suggestions ordered by score, including suggestion for typed word if in dictionary @@ -329,6 +587,8 @@ class SuggestTest { } private var currentTypingLocale = Locale.ENGLISH +internal var typingSuggestionResults = SuggestionResults(0, false, false) +private var mainDictionaryInitialized = true fun suggestion(word: String, score: Int, locale: Locale, shortcut: Boolean = false) = SuggestedWordInfo( @@ -352,7 +612,21 @@ class ShadowFacilitator { @Implementation fun getCurrentLocale(): Locale = currentTypingLocale @Implementation - fun hasAtLeastOneInitializedMainDictionary() = true // otherwise no autocorrect + fun getMainLocale(): Locale = currentTypingLocale + @Implementation + fun hasAtLeastOneInitializedMainDictionary() = mainDictionaryInitialized + @Implementation + fun isMainDictionaryLoadPending() = false + @Implementation + fun getSuggestionResults( + composedData: ComposedData, + ngramContext: NgramContext, + keyboard: Keyboard, + settingsValuesForSuggestion: SettingsValuesForSuggestion, + sessionId: Int, + inputStyle: Int + ): SuggestionResults = if (composedData.mTypedWord.isEmpty()) SuggestionResults(0, false, false) + else typingSuggestionResults } private class TestDict(locale: Locale) : Dictionary("testDict", locale) { diff --git a/docs/FEATURES.md b/docs/FEATURES.md index 797ce075f..d938ac730 100644 --- a/docs/FEATURES.md +++ b/docs/FEATURES.md @@ -435,6 +435,7 @@ Prevent offensive, sensitive, or unwanted words from ever appearing in the sugge LeanType learns your vocabulary organically as you type while providing deep granular control over the suggestion scoring pipeline: - **Adjustable Learning Threshold**: Choose how many times a new word must be typed (1 to 5 times) before it is automatically added to your personal dictionary. - **Session Word Boost**: Temporarily boosts recently typed, verified words for immediate next-word ranking during active typing sessions. +- **Missing-Apostrophe Corrections**: With auto-correction enabled, an unrecognized word such as `youre` can select an existing `you're` candidate even when another suggestion ranks first. The conventional spelling is preferred when available, while Shift/CapsLock transformations and explicit dictionary replacements are preserved. Missing dictionary candidates are not created. - **Suggestion Balance Master Sliders**: Fine-tune the exact scoring weights between unigram frequency, bigram/ngram context, and dictionary matches (**Settings → Suggestions → Suggestion Balance**). - **SuggestTrace & ScoreAudit Telemetry**: Built-in developer/power-user instrumentation to audit why specific words are being predicted or auto-corrected, backed by early beam pruning (`BEAM_DELTA = 60`) for optimal typing latency. - **Google Dictionary Import**: Import existing user dictionaries exported from Gboard.