From 06247ab8f2cd5e121898dfa15eac08cc4ff8ebcc Mon Sep 17 00:00:00 2001 From: 4di-hello <4di-hello@users.noreply.github.com> Date: Tue, 13 Oct 2026 16:58:02 +0200 Subject: [PATCH] Remember capitalization changed with shift When the user changes the case of a word with shift the same way twice (e.g. du -> Du), it is written like this automatically, as a correction that can be reverted with backspace (which also forgets it). Learned forms can be viewed and removed in settings. Requires shift_recapitalizes_word, off by default, nothing learned in incognito mode or password fields. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01VuiaMhLXwhErf9shhrr1uD --- .../helium314/keyboard/latin/LearnedCasing.kt | 106 ++++++++++++++++++ .../keyboard/latin/inputlogic/InputLogic.kt | 48 +++++++- .../keyboard/latin/settings/Defaults.kt | 1 + .../keyboard/latin/settings/Settings.kt | 2 + .../keyboard/latin/settings/SettingsValues.kt | 2 + .../preferences/LearnedCasingPreference.kt | 82 ++++++++++++++ .../settings/screens/TextCorrectionScreen.kt | 11 ++ app/src/main/res/values-de/strings.xml | 7 ++ app/src/main/res/values/strings.xml | 7 ++ .../keyboard/latin/InputLogicTest.kt | 28 +++++ 10 files changed, 289 insertions(+), 5 deletions(-) create mode 100644 app/src/main/java/helium314/keyboard/latin/LearnedCasing.kt create mode 100644 app/src/main/java/helium314/keyboard/settings/preferences/LearnedCasingPreference.kt diff --git a/app/src/main/java/helium314/keyboard/latin/LearnedCasing.kt b/app/src/main/java/helium314/keyboard/latin/LearnedCasing.kt new file mode 100644 index 000000000..0a73f7363 --- /dev/null +++ b/app/src/main/java/helium314/keyboard/latin/LearnedCasing.kt @@ -0,0 +1,106 @@ +// SPDX-License-Identifier: GPL-3.0-only +package helium314.keyboard.latin + +import android.content.SharedPreferences +import androidx.core.content.edit +import helium314.keyboard.latin.settings.Settings +import helium314.keyboard.latin.utils.Log +import org.json.JSONObject +import java.util.Locale + +/** + * Remembers how the user writes words, learned from changing the case of a word with shift + * (see [Settings.PREF_SHIFT_RECAPITALIZES_WORD]). After the same word was changed to the same + * form [TIMES_TO_LEARN] times, it is written like this automatically (as a revertible correction). + * Only forms containing upper case letters are learned, e.g. "Du" or "HZD". + */ +object LearnedCasing { + private const val TAG = "LearnedCasing" + const val TIMES_TO_LEARN = 2 + private const val MAX_ENTRIES = 300 + private const val MAX_WORD_LENGTH = 48 + + class Entry(val form: String, val count: Int) + + private var cachedJson: String? = null + private var cache: LinkedHashMap = LinkedHashMap() + + private fun key(word: String) = word.lowercase(Locale.ROOT) + + @Synchronized + private fun read(prefs: SharedPreferences): LinkedHashMap { + val json = prefs.getString(Settings.PREF_LEARNED_CASING, "") ?: "" + if (json == cachedJson) return cache + val map = LinkedHashMap() + if (json.isNotEmpty()) { + try { + val o = JSONObject(json) + for (k in o.keys()) { + val a = o.optJSONArray(k) ?: continue + val form = a.optString(0, "") + val count = a.optInt(1, 0) + if (isLearnableForm(form) && key(form) == k && count > 0) map[k] = Entry(form, count) + if (map.size >= MAX_ENTRIES) break + } + } catch (t: Throwable) { + Log.w(TAG, "could not read learned casing: ${t.javaClass.simpleName}") + } + } + cachedJson = json + cache = map + return map + } + + @Synchronized + private fun write(prefs: SharedPreferences, map: Map) { + val o = JSONObject() + map.forEach { (k, e) -> o.put(k, org.json.JSONArray().put(e.form).put(e.count)) } + val json = o.toString() + prefs.edit { putString(Settings.PREF_LEARNED_CASING, json) } + cachedJson = json + cache = LinkedHashMap(map) + } + + /** words with upper case letters, nothing else than letters, apostrophes and hyphens */ + fun isLearnableForm(form: String): Boolean = + form.isNotEmpty() && form.length <= MAX_WORD_LENGTH + && form.any { Character.isUpperCase(it) } + && form.all { Character.isLetter(it) || it == '\'' || it == '-' } + + /** The user changed the case of [form] with shift and used it */ + @Synchronized + fun onRecapitalizedWordUsed(prefs: SharedPreferences, form: String) { + if (!isLearnableForm(form)) return + val map = LinkedHashMap(read(prefs)) + val k = key(form) + val old = map.remove(k) + val count = if (old != null && old.form == form) old.count + 1 else 1 + map[k] = Entry(form, count) // re-insert: most recent last + while (map.size > MAX_ENTRIES) map.remove(map.keys.first()) + write(prefs, map) + } + + /** Learned form for [word] if it differs from [word], null otherwise */ + @Synchronized + fun learnedFormFor(prefs: SharedPreferences, word: String): String? { + if (word.isEmpty() || word.length > MAX_WORD_LENGTH) return null + val entry = read(prefs)[key(word)] ?: return null + if (entry.count < TIMES_TO_LEARN || entry.form == word) return null + return entry.form + } + + /** All learned forms (also those not yet used automatically) */ + @Synchronized + fun getAll(prefs: SharedPreferences): List = read(prefs).values.sortedBy { it.form.lowercase(Locale.ROOT) } + + @Synchronized + fun forget(prefs: SharedPreferences, word: String) { + val map = LinkedHashMap(read(prefs)) + if (map.remove(key(word)) != null) write(prefs, map) + } + + @Synchronized + fun clear(prefs: SharedPreferences) { + write(prefs, emptyMap()) + } +} diff --git a/app/src/main/java/helium314/keyboard/latin/inputlogic/InputLogic.kt b/app/src/main/java/helium314/keyboard/latin/inputlogic/InputLogic.kt index 2c3efc08c..1848fd200 100644 --- a/app/src/main/java/helium314/keyboard/latin/inputlogic/InputLogic.kt +++ b/app/src/main/java/helium314/keyboard/latin/inputlogic/InputLogic.kt @@ -19,6 +19,8 @@ import android.view.inputmethod.CorrectionInfo import android.view.inputmethod.EditorInfo import helium314.keyboard.compat.AppQuirksManager import helium314.keyboard.event.Event +import helium314.keyboard.latin.LearnedCasing +import helium314.keyboard.latin.utils.prefs import helium314.keyboard.event.InputTransaction import helium314.keyboard.keyboard.Keyboard import helium314.keyboard.keyboard.KeyboardActionListenerImpl @@ -87,6 +89,8 @@ class InputLogic( // the composing word at the time the shift key was pressed down, null if there was none // used to tell a tap on shift from typing a letter while holding shift private var mTypedWordOnShiftPress: String? = null + /** word as changed by [recapitalizeComposingWord] (not lower case), for [LearnedCasing] */ + private var mRecapitalizedWord: String? = null private var mDeleteCount = 0 private var mLastKeyTime = 0L @@ -123,6 +127,7 @@ class InputLogic( get() = mSuggestedWords fun startInput(combiningSpec: String?, settingsValues: SettingsValues) { + mRecapitalizedWord = null mEnteredText = null mWordBeingCorrectedByCursor = null mConnection.onStartInput() @@ -1245,7 +1250,9 @@ class InputLogic( commitCurrentAutoCorrection(settingsValues, separator, handler) inputTransaction.setDidAutoCorrect() } else { - commitTyped(settingsValues, StringUtils.newSingleCodePointString(codePoint)) + // like an auto-correction, a learned capitalization can be reverted with backspace + if (commitTyped(settingsValues, StringUtils.newSingleCodePointString(codePoint), useLearnedCasing = true)) + inputTransaction.setDidAutoCorrect() } } else if (TextExpanderUtils.isEnabled(mLatinIME)) { val textBefore = mConnection.getTextBeforeCursor(50, 0) @@ -1833,6 +1840,7 @@ class InputLogic( mConnection.endBatchEdit() // typing should continue in the same case: caps lock for an upper case word, lower case otherwise KeyboardSwitcher.getInstance().setCapsLock(mRecapitalizeStatus.getCurrentMode() == RecapitalizeMode.ALL_UPPER) + mRecapitalizedWord = if (mRecapitalizeStatus.getCurrentMode() == RecapitalizeMode.ALL_LOWER) null else replacement return true } @@ -2081,6 +2089,11 @@ class InputLogic( val originallyTypedWord = mLastComposedWord.mTypedWord val committedWord = mLastComposedWord.mCommittedWord val committedWordString = committedWord.toString() + // the user doesn't want the learned case here: forget it + if (inputTransaction.settingsValues.mLearnCasing + && LearnedCasing.learnedFormFor(mLatinIME.prefs(), originallyTypedWord) == committedWordString) { + LearnedCasing.forget(mLatinIME.prefs(), originallyTypedWord) + } val cancelLength = committedWord.length val separatorString = mLastComposedWord.mSeparatorString val usePhantomSpace = separatorString == Constants.STRING_SPACE @@ -2373,14 +2386,34 @@ class InputLogic( } } - fun commitTyped(settingsValues: SettingsValues, separatorString: String) { - if (!mWordComposer.isComposingWord()) return + /** @return whether the word was changed to a learned capitalization (like an auto-correction) */ + fun commitTyped(settingsValues: SettingsValues, separatorString: String, useLearnedCasing: Boolean = false): Boolean { + if (!mWordComposer.isComposingWord()) return false val typedWord = mWordComposer.getTypedWord() + var changed = false if (typedWord.isNotEmpty()) { val isBatchMode = mWordComposer.isBatchMode() - commitChosenWord(settingsValues, typedWord, LastComposedWord.COMMIT_TYPE_USER_TYPED_WORD, separatorString) + val learned = if (useLearnedCasing) applyLearnedCasing(settingsValues, typedWord) else typedWord + if (learned != typedWord) { + // like an auto-correction, so it can be reverted with backspace + commitChosenWord(settingsValues, learned, LastComposedWord.COMMIT_TYPE_DECIDED_WORD, separatorString) + mConnection.commitCorrection( + CorrectionInfo(mConnection.expectedSelectionEnd - learned.length, typedWord, learned) + ) + changed = true + } else { + commitChosenWord(settingsValues, typedWord, LastComposedWord.COMMIT_TYPE_USER_TYPED_WORD, separatorString) + } StatsUtils.onWordCommitUserTyped(typedWord, isBatchMode) } + return changed + } + + /** [word] in the form learned by [LearnedCasing], if enabled and the user did not just change its case */ + private fun applyLearnedCasing(settingsValues: SettingsValues, word: String): String { + if (!settingsValues.mLearnCasing || settingsValues.mInputAttributes.mIsPasswordField) return word + if (isRecapitalizingComposingWord()) return word + return LearnedCasing.learnedFormFor(mLatinIME.prefs(), word) ?: word } private fun commitCurrentAutoCorrection( @@ -2394,7 +2427,7 @@ class InputLogic( } val autoCorrectionOrNull = mWordComposer.getAutoCorrectionOrNull() val typedWord = mWordComposer.getTypedWord() - val stringToCommit = autoCorrectionOrNull?.mWord ?: typedWord + val stringToCommit = applyLearnedCasing(settingsValues, autoCorrectionOrNull?.mWord ?: typedWord) if (stringToCommit != null) { val isBatchMode = mWordComposer.isBatchMode() commitChosenWord(settingsValues, stringToCommit, LastComposedWord.COMMIT_TYPE_DECIDED_WORD, separator) @@ -2486,6 +2519,11 @@ class InputLogic( Log.d(TAG, "commitChosenWord() : $runTimeMillis ms to run performAdditionToUserHistoryDictionary()") startTimeMillis = System.currentTimeMillis() } + // the user changed the case of this word with shift: remember it + if (settingsValues.mLearnCasing && !settingsValues.mIncognitoModeEnabled && chosenWord == mRecapitalizedWord) { + LearnedCasing.onRecapitalizedWordUsed(mLatinIME.prefs(), chosenWord) + } + mRecapitalizedWord = null mLastComposedWord = mWordComposer.commitWord(commitType, chosenWord, separatorString, ngramContext) if (DebugFlags.DEBUG_ENABLED) { val runTimeMillis = System.currentTimeMillis() - startTimeMillis diff --git a/app/src/main/java/helium314/keyboard/latin/settings/Defaults.kt b/app/src/main/java/helium314/keyboard/latin/settings/Defaults.kt index e21cd68ac..fb8745fdc 100644 --- a/app/src/main/java/helium314/keyboard/latin/settings/Defaults.kt +++ b/app/src/main/java/helium314/keyboard/latin/settings/Defaults.kt @@ -215,6 +215,7 @@ object Defaults { const val PREF_TOUCHPAD_FULLSCREEN = false const val PREF_FORCE_AUTO_CAPS = false const val PREF_SHIFT_RECAPITALIZES_WORD = false + const val PREF_LEARN_CASING = false const val PREF_OFFLINE_TEMP = 0.1f // Lower for faster, more deterministic proofreading const val PREF_OFFLINE_TOP_P = 0.5f // Lower for faster token sampling const val PREF_OFFLINE_TOP_K = 40 diff --git a/app/src/main/java/helium314/keyboard/latin/settings/Settings.kt b/app/src/main/java/helium314/keyboard/latin/settings/Settings.kt index b28526dcb..cede34b18 100644 --- a/app/src/main/java/helium314/keyboard/latin/settings/Settings.kt +++ b/app/src/main/java/helium314/keyboard/latin/settings/Settings.kt @@ -484,6 +484,8 @@ class Settings private constructor() : SharedPreferences.OnSharedPreferenceChang const val PREF_PERSIST_TEXT_EDIT_MODE = "persist_text_edit_mode" const val PREF_FORCE_AUTO_CAPS = "force_auto_caps" const val PREF_SHIFT_RECAPITALIZES_WORD = "shift_recapitalizes_word" + const val PREF_LEARN_CASING = "learn_casing" + const val PREF_LEARNED_CASING = "learned_casing" const val PREF_OFFLINE_TEMP = "offline_temp" const val PREF_OFFLINE_TOP_P = "offline_top_p" const val PREF_OFFLINE_TOP_K = "offline_top_k" diff --git a/app/src/main/java/helium314/keyboard/latin/settings/SettingsValues.kt b/app/src/main/java/helium314/keyboard/latin/settings/SettingsValues.kt index decdbbdb0..6156c214c 100644 --- a/app/src/main/java/helium314/keyboard/latin/settings/SettingsValues.kt +++ b/app/src/main/java/helium314/keyboard/latin/settings/SettingsValues.kt @@ -105,6 +105,7 @@ open class SettingsValues( val mTouchpadFullscreen: Boolean val mForceAutoCaps: Boolean val mShiftRecapitalizesWord: Boolean + val mLearnCasing: Boolean val mDeleteSwipeEnabled: Boolean val mDeleteSwipeWordByWord: Boolean val mAutospaceAfterPunctuation: Boolean @@ -446,6 +447,7 @@ open class SettingsValues( mTouchpadFullscreen = prefs.getBoolean(Settings.PREF_TOUCHPAD_FULLSCREEN, Defaults.PREF_TOUCHPAD_FULLSCREEN) mForceAutoCaps = prefs.getBoolean(Settings.PREF_FORCE_AUTO_CAPS, Defaults.PREF_FORCE_AUTO_CAPS) mShiftRecapitalizesWord = prefs.getBoolean(Settings.PREF_SHIFT_RECAPITALIZES_WORD, Defaults.PREF_SHIFT_RECAPITALIZES_WORD) + mLearnCasing = mShiftRecapitalizesWord && prefs.getBoolean(Settings.PREF_LEARN_CASING, Defaults.PREF_LEARN_CASING) mDeleteSwipeEnabled = prefs.getBoolean(Settings.PREF_DELETE_SWIPE, Defaults.PREF_DELETE_SWIPE) mDeleteSwipeWordByWord = prefs.getBoolean(Settings.PREF_DELETE_SWIPE_WORD_BY_WORD, Defaults.PREF_DELETE_SWIPE_WORD_BY_WORD) mAutospaceAfterPunctuation = prefs.getBoolean(Settings.PREF_AUTOSPACE_AFTER_PUNCTUATION, Defaults.PREF_AUTOSPACE_AFTER_PUNCTUATION) diff --git a/app/src/main/java/helium314/keyboard/settings/preferences/LearnedCasingPreference.kt b/app/src/main/java/helium314/keyboard/settings/preferences/LearnedCasingPreference.kt new file mode 100644 index 000000000..d77d4a377 --- /dev/null +++ b/app/src/main/java/helium314/keyboard/settings/preferences/LearnedCasingPreference.kt @@ -0,0 +1,82 @@ +// SPDX-License-Identifier: GPL-3.0-only +package helium314.keyboard.settings.preferences + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.heightIn +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableIntStateOf +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import helium314.keyboard.latin.LearnedCasing +import helium314.keyboard.latin.R +import helium314.keyboard.latin.utils.prefs +import helium314.keyboard.settings.Setting +import helium314.keyboard.settings.dialogs.ThreeButtonAlertDialog + +/** Shows the forms learned by [LearnedCasing], each can be removed */ +@Composable +fun LearnedCasingPreference(setting: Setting) { + val prefs = LocalContext.current.prefs() + var showDialog by remember { mutableStateOf(false) } + var refresh by remember { mutableIntStateOf(0) } + val entries = remember(refresh, showDialog) { LearnedCasing.getAll(prefs) } + val active = entries.count { it.count >= LearnedCasing.TIMES_TO_LEARN } + Preference( + name = setting.title, + onClick = { showDialog = true }, + description = stringResource(R.string.learned_casing_summary, active, entries.size - active) + ) + if (showDialog) { + ThreeButtonAlertDialog( + onDismissRequest = { showDialog = false }, + onConfirmed = { }, + title = { Text(setting.title) }, + confirmButtonText = null, + cancelButtonText = stringResource(R.string.dialog_close), + neutralButtonText = if (entries.isEmpty()) null else stringResource(R.string.learned_casing_forget_all), + onNeutral = { LearnedCasing.clear(prefs); refresh++ }, + content = { + if (entries.isEmpty()) { + Text(stringResource(R.string.learned_casing_empty)) + } else { + Column(Modifier.heightIn(max = 400.dp).verticalScroll(rememberScrollState())) { + entries.forEach { entry -> + Row( + Modifier.fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceBetween + ) { + Column(Modifier.weight(1f)) { + Text(entry.form, style = MaterialTheme.typography.bodyLarge) + if (entry.count < LearnedCasing.TIMES_TO_LEARN) + Text(stringResource(R.string.learned_casing_not_yet), style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant) + } + IconButton(onClick = { LearnedCasing.forget(prefs, entry.form); refresh++ }) { + Icon(painterResource(R.drawable.ic_bin), stringResource(R.string.delete)) + } + } + } + } + } + } + ) + } +} diff --git a/app/src/main/java/helium314/keyboard/settings/screens/TextCorrectionScreen.kt b/app/src/main/java/helium314/keyboard/settings/screens/TextCorrectionScreen.kt index 877a7a385..6ff9515cf 100644 --- a/app/src/main/java/helium314/keyboard/settings/screens/TextCorrectionScreen.kt +++ b/app/src/main/java/helium314/keyboard/settings/screens/TextCorrectionScreen.kt @@ -31,6 +31,7 @@ import helium314.keyboard.settings.Setting import helium314.keyboard.settings.SettingsActivity import helium314.keyboard.settings.Theme import helium314.keyboard.settings.initPreview +import helium314.keyboard.settings.preferences.LearnedCasingPreference import helium314.keyboard.settings.preferences.ListPreference import helium314.keyboard.settings.preferences.Preference import helium314.keyboard.settings.preferences.SliderPreference @@ -69,6 +70,8 @@ fun TextCorrectionScreen( Settings.PREF_AUTO_CAP, Settings.PREF_FORCE_AUTO_CAPS, Settings.PREF_SHIFT_RECAPITALIZES_WORD, + Settings.PREF_LEARN_CASING, + Settings.PREF_LEARNED_CASING, // Space R.string.settings_category_space, @@ -228,6 +231,14 @@ fun createCorrectionSettings(context: Context) = listOf( ) { SwitchPreference(it, Defaults.PREF_SHIFT_RECAPITALIZES_WORD) }, + Setting(context, Settings.PREF_LEARN_CASING, R.string.learn_casing, R.string.learn_casing_summary) { + val prefs = LocalContext.current.prefs() + SwitchPreference(it, Defaults.PREF_LEARN_CASING, + enabled = prefs.getBoolean(Settings.PREF_SHIFT_RECAPITALIZES_WORD, Defaults.PREF_SHIFT_RECAPITALIZES_WORD)) + }, + Setting(context, Settings.PREF_LEARNED_CASING, R.string.learned_casing) { + LearnedCasingPreference(it) + }, Setting(context, Settings.PREF_KEY_USE_DOUBLE_SPACE_PERIOD, R.string.use_double_space_period, R.string.use_double_space_period_summary ) { diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index aedc47bcc..b34acfa53 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -1734,4 +1734,11 @@ Ermöglicht lokale GGUF / llama.cpp-Inferenz für Korrekturlesen und Umschreiben API-Schlüssel einschließen Holen Sie sich den API-Schlüssel Deaktiviert + Schreibweisen merken + Wenn Du ein Wort zweimal gleich per Shift änderst (z. B. du → Du, hzd → HZD), wird es künftig automatisch so geschrieben. Die Rücktaste direkt danach macht es rückgängig und vergisst es + Gemerkte Schreibweisen + %1$d aktiv, %2$d im Lernen + Noch nichts gemerkt. + Im Lernen: wird nach einem weiteren Mal automatisch verwendet + Alle vergessen diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 7921e25d2..98ca26625 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -2418,4 +2418,11 @@ New dictionary: No recent items Manage Local Media Get API key + Remember capitalization + When you change a word with shift the same way twice (e.g. du → Du, hzd → HZD), it is written like this automatically. Backspace right after it reverts and forgets it + Remembered capitalization + %1$d in use, %2$d learning + Nothing remembered yet. + Learning: used automatically after one more time + Forget all diff --git a/app/src/test/java/helium314/keyboard/latin/InputLogicTest.kt b/app/src/test/java/helium314/keyboard/latin/InputLogicTest.kt index 23f26c2fe..4d40b5495 100644 --- a/app/src/test/java/helium314/keyboard/latin/InputLogicTest.kt +++ b/app/src/test/java/helium314/keyboard/latin/InputLogicTest.kt @@ -386,6 +386,34 @@ class InputLogicTest { assertEquals("hello ", text) } + @Test fun `capitalization changed with shift is remembered`() { + reset() + latinIME.prefs().edit { + putBoolean(Settings.PREF_SHIFT_RECAPITALIZES_WORD, true) + putBoolean(Settings.PREF_LEARN_CASING, true) + } + fun rawInput(c: Char) { + latinIME.onEvent(Event.createEventForCodePointFromUnknownSource(c.code)) + handleMessages() + } + repeat(2) { + chainInput("hzd") + shiftTap() + shiftTap() + input(' ') + } + assertEquals("HZD HZD ", text) + assertEquals("HZD", LearnedCasing.learnedFormFor(latinIME.prefs(), "hzd")) + // now typed normally, it is written in the learned form + chainInput("hzd") + rawInput(' ') + assertEquals("HZD HZD HZD ", text) + // backspace reverts it and forgets it + functionalKeyPress(KeyCode.DELETE) + assertEquals("HZD HZD hzd", text) + assertEquals(null, LearnedCasing.learnedFormFor(latinIME.prefs(), "hzd")) + } + @Test fun `shift at end of word does not change the word by default`() { reset() chainInput("hello")