From 4fe7d64143a709337ee3bf9123e1d1839acce432 Mon Sep 17 00:00:00 2001 From: max Date: Fri, 10 Jul 2026 16:10:55 +0200 Subject: [PATCH] Fix duplicated leading digit when picking a suggestion A word starting with a character that doesn't begin composition on its own (such as a digit) has its leading part committed immediately while only the rest of the word is composing. Picking a suggestion that includes the leading part then duplicated it, e.g. typing "2u2j" and picking "2u2j@addres.com" produced "22u2j@addres.com". Extend the composing region to cover the whole word before committing the picked suggestion, so it replaces the whole word instead of only the composing part. --- .../keyboard/latin/inputlogic/InputLogic.java | 26 +++++++++++++++++++ .../keyboard/latin/InputLogicTest.kt | 15 +++++++++++ 2 files changed, 41 insertions(+) diff --git a/app/src/main/java/helium314/keyboard/latin/inputlogic/InputLogic.java b/app/src/main/java/helium314/keyboard/latin/inputlogic/InputLogic.java index 9806384439..91f5ee3a26 100644 --- a/app/src/main/java/helium314/keyboard/latin/inputlogic/InputLogic.java +++ b/app/src/main/java/helium314/keyboard/latin/inputlogic/InputLogic.java @@ -344,6 +344,32 @@ public InputTransaction onPickSuggestionManually(SettingsValues settingsValues, return inputTransaction; } + // A word can extend before the composing region when it starts with characters that don't + // start composition on their own, such as digits (e.g. "2u2j"): the leading part is + // committed immediately, while only the rest ("u2j") is composing. Committing the picked + // suggestion, which includes the leading part, would then duplicate it. If the picked word + // starts with the whole word before the cursor, extend the composing region to cover it so + // that committing replaces the whole word rather than only the composing part. (#2611) + if (mWordComposer.isComposingWord() && !mWordComposer.isBatchMode()) { + final String typedWord = mWordComposer.getTypedWord(); + final int cursor = mConnection.getExpectedSelectionStart(); + final CharSequence before = mConnection.getTextBeforeCursor(Constants.EDITOR_CONTENTS_CACHE_SIZE, 0); + if (before != null && cursor >= 0 && before.length() >= typedWord.length()) { + // Non-separator characters committed right before the composing word still belong + // to the same word (e.g. leading digits), so include them in the word to replace. + int start = before.length() - typedWord.length(); + while (start > 0) { + final int cp = Character.codePointBefore(before, start); + if (settingsValues.isWordSeparator(cp) || Character.isWhitespace(cp)) break; + start -= Character.charCount(cp); + } + final String wordBeforeCursor = before.subSequence(start, before.length()).toString(); + if (wordBeforeCursor.length() > typedWord.length() && suggestion.startsWith(wordBeforeCursor)) { + mConnection.setComposingRegion(cursor - wordBeforeCursor.length(), cursor); + } + } + } + commitChosenWord(settingsValues, suggestion, LastComposedWord.COMMIT_TYPE_MANUAL_PICK, LastComposedWord.NOT_A_SEPARATOR); mConnection.endBatchEdit(); // Don't allow cancellation of manual pick diff --git a/app/src/test/java/helium314/keyboard/latin/InputLogicTest.kt b/app/src/test/java/helium314/keyboard/latin/InputLogicTest.kt index 5946d43148..c7b23a4dce 100644 --- a/app/src/test/java/helium314/keyboard/latin/InputLogicTest.kt +++ b/app/src/test/java/helium314/keyboard/latin/InputLogicTest.kt @@ -244,6 +244,21 @@ class InputLogicTest { assertEquals("example.", text) } + // #2611: a word starting with a digit has its leading digit committed outside the composing + // region (digits don't start composition), so picking a suggestion that includes it used to + // duplicate the digit ("2u2j" + pick "2u2j@addres.com" -> "22u2j@addres.com"). + @Test fun pickSuggestionForWordWithLeadingDigit() { + chainInput("2u2j") + pickSuggestion("2u2j@addres.com") + assertEquals("2u2j@addres.com", text) + } + + @Test fun pickSuggestionForWordWithLeadingDigitPartiallyTyped() { + chainInput("2u") + pickSuggestion("2u2j@addres.com") + assertEquals("2u2j@addres.com", text) + } + @Test fun noAutospaceForDetectedUrl() { // "light" version, should work without url detection latinIME.prefs().edit { putBoolean(Settings.PREF_AUTOSPACE_AFTER_PUNCTUATION, true) } chainInput("http://example.net")