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.