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 cbaabc8d7..127a12597 100644 --- a/app/src/main/java/helium314/keyboard/latin/inputlogic/InputLogic.kt +++ b/app/src/main/java/helium314/keyboard/latin/inputlogic/InputLogic.kt @@ -2269,7 +2269,8 @@ class InputLogic( val stringToCommit = autoCorrectionOrNull?.mWord ?: typedWord if (stringToCommit != null) { val isBatchMode = mWordComposer.isBatchMode() - commitChosenWord(settingsValues, stringToCommit, LastComposedWord.COMMIT_TYPE_DECIDED_WORD, separator) + val expanded = commitChosenWord(settingsValues, stringToCommit, LastComposedWord.COMMIT_TYPE_DECIDED_WORD, separator) + if (expanded) return if (typedWord != stringToCommit) { mConnection.commitCorrection( CorrectionInfo( @@ -2289,23 +2290,28 @@ class InputLogic( } } + /** Returns whether a shortcut expansion replaced the chosen word. */ private fun commitChosenWord( settingsValues: SettingsValues, chosenWord: String, commitType: Int, separatorString: String - ) { + ): Boolean { var startTimeMillis = 0L if (DebugFlags.DEBUG_ENABLED) { startTimeMillis = System.currentTimeMillis() Log.d(TAG, "commitChosenWord() : [$chosenWord]") } val isEnabled = TextExpanderUtils.isEnabled(mLatinIME) - if (isEnabled) { + val isDifferentManualPick = commitType == LastComposedWord.COMMIT_TYPE_MANUAL_PICK + && !chosenWord.equals(mWordComposer.getTypedWord(), ignoreCase = true) + if (isEnabled && !isDifferentManualPick) { val textBefore = mConnection.getTextBeforeCursor(50, 0) if (textBefore != null) { val textStr = textBefore.toString() - val result = TextExpanderUtils.getExpandedWordForTyped(chosenWord, textStr, mLatinIME) + val expansionWord = if (commitType == LastComposedWord.COMMIT_TYPE_DECIDED_WORD) + mWordComposer.getTypedWord() else chosenWord + val result = TextExpanderUtils.getExpandedWordForTyped(expansionWord, textStr, mLatinIME) if (result != null) { if (mJustRevertedExpandedShortcut != null && result.matchedString.equals(mJustRevertedExpandedShortcut, ignoreCase = true) @@ -2316,7 +2322,7 @@ class InputLogic( mConnection.deleteTextBeforeCursor(result.prefixLength + chosenWord.length) commitExpandedText(result.matchedString, result.expandedText) resetComposingState(true) - return + return true } } } @@ -2355,6 +2361,7 @@ class InputLogic( val runTimeMillis = System.currentTimeMillis() - startTimeMillis Log.d(TAG, "commitChosenWord() : $runTimeMillis ms to run WordComposer.commitWord()") } + return false } private fun wrapSelection(start: Int, end: Int) { diff --git a/app/src/main/java/helium314/keyboard/latin/utils/TextExpanderUtils.kt b/app/src/main/java/helium314/keyboard/latin/utils/TextExpanderUtils.kt index 796e6494a..27cb514e9 100644 --- a/app/src/main/java/helium314/keyboard/latin/utils/TextExpanderUtils.kt +++ b/app/src/main/java/helium314/keyboard/latin/utils/TextExpanderUtils.kt @@ -387,11 +387,8 @@ object TextExpanderUtils { if (textBeforeCursor == null || !isEnabled(context)) return null getShortcuts(context) val compiledList = cachedCompiledList ?: return null - val fullText = if (word != null && !textBeforeCursor.endsWith(word, ignoreCase = true)) { - textBeforeCursor + word - } else { - textBeforeCursor - } + // A pending correction is not text the user typed and must not trigger a macro. + val fullText = textBeforeCursor for (item in compiledList) { val entry = item.entry diff --git a/app/src/test/java/helium314/keyboard/latin/InputLogicTest.kt b/app/src/test/java/helium314/keyboard/latin/InputLogicTest.kt index d9b5b2324..ca0788522 100644 --- a/app/src/test/java/helium314/keyboard/latin/InputLogicTest.kt +++ b/app/src/test/java/helium314/keyboard/latin/InputLogicTest.kt @@ -30,6 +30,7 @@ import helium314.keyboard.latin.inputlogic.SpaceState import helium314.keyboard.latin.settings.Settings import helium314.keyboard.latin.utils.ScriptUtils import helium314.keyboard.latin.utils.SubtypeSettings +import helium314.keyboard.latin.utils.TextExpanderUtils import helium314.keyboard.latin.utils.getTimestampFormatter import helium314.keyboard.latin.utils.prefs import org.junit.runner.RunWith @@ -46,6 +47,7 @@ import kotlin.streams.asSequence import kotlin.test.BeforeTest import kotlin.test.Test import kotlin.test.assertEquals +import kotlin.test.assertNotNull @RunWith(RobolectricTestRunner::class) @Config(shadows = [ @@ -426,6 +428,184 @@ class InputLogicTest { assertEquals("user_mention", text) } + @Test fun correctionToLiteralShortcutDoesNotExpand() { + assertCorrectionDoesNotTriggerShortcut("cpp") + } + + @Test fun correctionToRegexShortcutDoesNotExpand() { + assertCorrectionDoesNotTriggerShortcut("${TextExpanderUtils.REGEX_PREFIX}cpp") + } + + private fun assertCorrectionDoesNotTriggerShortcut(shortcut: String) { + for (immediate in listOf(false, true)) { + configureExpansion(shortcut, immediate) + setInputType(InputType.TYPE_CLASS_TEXT or InputType.TYPE_TEXT_FLAG_AUTO_CORRECT) + typeNoAssert("cps") + val correction = SuggestedWordInfo("cpp", "", 0, 0, Mockito.mock(Dictionary::class.java), 0, 0) + composer.setAutoCorrection(correction) + assertEquals(true, settingsValues.mAutoCorrectEnabled) + typeNoAssert(" ") + assertEquals("cpp ", text, "immediate=$immediate") + checkConnectionConsistency() + } + } + + @Test fun literalShortcutStillExpandsOnlyFromActualTypedTrigger() { + assertTypedShortcutExpands("cpp") + } + + @Test fun regexShortcutStillExpandsOnlyFromActualTypedTrigger() { + assertTypedShortcutExpands("${TextExpanderUtils.REGEX_PREFIX}cpp") + } + + @Test fun prefixedRegexExpansionUsesTypedLengthNotCorrectionLength() { + for (correction in listOf("python", "p")) { + configureExpansion("${TextExpanderUtils.REGEX_PREFIX}\\.py", false) + setInputType(InputType.TYPE_CLASS_TEXT or InputType.TYPE_TEXT_FLAG_AUTO_CORRECT) + setText("keep .") + typeNoAssert("py") + assertEquals("py", composer.getTypedWord()) + composer.setAutoCorrection( + SuggestedWordInfo(correction, "", 0, 0, Mockito.mock(Dictionary::class.java), 0, 0) + ) + typeNoAssert(" ") + assertEquals("keep EXPANDED ", text, "correction=$correction") + assertEquals(text.length, cursor) + assertEquals(emptyList(), committedCorrections) + checkConnectionConsistency() + } + } + + @Test fun unexpandedAutoCorrectionStillNotifiesEditor() { + for (expansionEnabled in listOf(false, true)) { + configureExpansion("${TextExpanderUtils.REGEX_PREFIX}\\.zz", false) + latinIME.prefs().edit { + putBoolean(TextExpanderUtils.PREF_ENABLED, expansionEnabled) + } + setInputType(InputType.TYPE_CLASS_TEXT or InputType.TYPE_TEXT_FLAG_AUTO_CORRECT) + setText("keep .") + typeNoAssert("py") + composer.setAutoCorrection( + SuggestedWordInfo("python", "", 0, 0, Mockito.mock(Dictionary::class.java), 0, 0) + ) + typeNoAssert(" ") + assertEquals("keep .python ", text) + assertEquals(text.length, cursor) + val correction = assertNotNull(committedCorrections.single()) + assertEquals(6, correction.offset) + assertEquals("py", correction.oldText.toString()) + assertEquals("python", correction.newText.toString()) + checkConnectionConsistency() + } + } + + @Test fun manuallyPickedSuggestionDoesNotSynthesizeShortcutText() { + for (shortcut in listOf("cpp", "${TextExpanderUtils.REGEX_PREFIX}cpp")) { + configureExpansion(shortcut, false) + typeNoAssert("cps") + pickSuggestion("cpp") + assertEquals("cpp", text) + checkConnectionConsistency() + } + configureExpansion("cpp", false) + typeNoAssert("cpp") + pickSuggestion("chosen") + assertEquals("chosen", text) + checkConnectionConsistency() + } + + @Test fun manuallyPickedLongerSuggestionOverridesMatchingRegexShortcut() { + assertManualSuggestionOverridesMatchingRegexShortcut("python") + } + + @Test fun manuallyPickedShorterSuggestionOverridesMatchingRegexShortcut() { + assertManualSuggestionOverridesMatchingRegexShortcut("p") + } + + @Test fun manuallyPickedSuffixSuggestionOverridesMatchingRegexShortcut() { + assertManualSuggestionOverridesMatchingRegexShortcut("y") + } + + @Test fun manuallyPickedSameLengthSuggestionOverridesMatchingRegexShortcut() { + assertManualSuggestionOverridesMatchingRegexShortcut("by") + } + + private fun assertManualSuggestionOverridesMatchingRegexShortcut(suggestion: String) { + configureExpansion("${TextExpanderUtils.REGEX_PREFIX}\\.py", false) + setText("keep .") + typeNoAssert("py") + assertEquals("py", composer.getTypedWord()) + pickSuggestion(suggestion) + assertEquals("keep .$suggestion", text) + assertEquals(text.length, cursor) + checkConnectionConsistency() + } + + @Test fun manuallyPickedDifferentSuggestionOverridesWholeRegexMatch() { + configureExpansion("${TextExpanderUtils.REGEX_PREFIX}cpp", false) + typeNoAssert("cpp") + pickSuggestion("chosen") + assertEquals("chosen", text) + assertEquals(text.length, cursor) + checkConnectionConsistency() + } + + @Test fun manuallyPickedMatchingShortcutStillExpands() { + for (shortcut in listOf("cpp", "${TextExpanderUtils.REGEX_PREFIX}cpp")) { + for (suggestion in listOf("cpp", "CPP")) { + configureExpansion(shortcut, false) + typeNoAssert("cpp") + pickSuggestion(suggestion) + assertEquals("EXPANDED", text) + assertEquals(text.length, cursor) + functionalKeyPress(KeyCode.DELETE) + assertEquals("cpp", text) + checkConnectionConsistency() + } + } + } + + @Test fun manuallyPickedMatchingPrefixedRegexShortcutStillExpands() { + for (suggestion in listOf("py", "PY")) { + configureExpansion("${TextExpanderUtils.REGEX_PREFIX}\\.py", false) + setText("keep .") + typeNoAssert("py") + pickSuggestion(suggestion) + assertEquals("keep EXPANDED", text) + assertEquals(text.length, cursor) + functionalKeyPress(KeyCode.DELETE) + assertEquals("keep .py", text) + checkConnectionConsistency() + } + } + + private fun assertTypedShortcutExpands(shortcut: String) { + for (immediate in listOf(false, true)) { + configureExpansion(shortcut, immediate) + typeNoAssert("cps ") + assertEquals("cps ", text) + typeNoAssert("cpp") + if (!immediate) typeNoAssert(" ") + assertEquals("cps EXPANDED" + if (immediate) "" else " ", text) + if (!immediate) functionalKeyPress(KeyCode.DELETE) + functionalKeyPress(KeyCode.DELETE) + assertEquals("cps cpp", text) + checkConnectionConsistency() + } + } + + private fun configureExpansion(shortcut: String, immediate: Boolean) { + reset() + latinIME.prefs().edit { + putBoolean(TextExpanderUtils.PREF_ENABLED, true) + putBoolean(TextExpanderUtils.PREF_IMMEDIATE, immediate) + putBoolean(TextExpanderUtils.PREF_BACKSPACE_REVERTS, true) + } + TextExpanderUtils.saveShortcuts( + latinIME, mapOf(shortcut to TextExpanderUtils.ShortcutEntry("EXPANDED")) + ) + } + @Test fun numberShortcutWithAtPrefixExpandsOnSpace() { reset() latinIME.prefs().edit().apply { @@ -1269,6 +1449,7 @@ class InputLogicTest { lastNgramContext = "" addedWords.clear() ngramContexts.clear() + committedCorrections.clear() messages.clear() delayedMessages.clear() @@ -1516,6 +1697,7 @@ private var selectionStart = 0 private var selectionEnd = 0 private var composingStart = -1 private var composingEnd = -1 +private val committedCorrections = mutableListOf() // convenience for access private val textBeforeCursor get() = text.substring(0, selectionStart) private val textAfterCursor get() = text.substring(selectionEnd) @@ -1672,8 +1854,10 @@ private val ic = object : InputConnection { it.selectionEnd = selectionEnd } } - // only effect is flashing, so whatever... - override fun commitCorrection(p0: CorrectionInfo?): Boolean = true + override fun commitCorrection(p0: CorrectionInfo?): Boolean { + committedCorrections.add(p0) + return true + } // implement only when necessary override fun getCursorCapsMode(p0: Int): Int = TODO("Not yet implemented") override fun deleteSurroundingTextInCodePoints(p0: Int, p1: Int): Boolean = TODO("Not yet implemented")