Merge "Introduce clustering punctuation"
diff --git a/java/res/values-fr/donottranslate-config-spacing-and-punctuations.xml b/java/res/values-fr/donottranslate-config-spacing-and-punctuations.xml
index d72f72b..5a49142 100644
--- a/java/res/values-fr/donottranslate-config-spacing-and-punctuations.xml
+++ b/java/res/values-fr/donottranslate-config-spacing-and-punctuations.xml
@@ -22,6 +22,8 @@
     <string name="symbols_preceded_by_space">([{&amp;;:!?</string>
     <!-- Symbols that are normally followed by a space (used to add an auto-space after these) -->
     <string name="symbols_followed_by_space">.,;:!?)]}&amp;</string>
+    <!-- Symbols that behave like a single punctuation when typed next to each other -->
+    <string name="symbols_clustering_together">!?</string>
     <!-- Symbols that separate words -->
     <!-- Don't remove the enclosing double quotes, they protect whitespace (not just U+0020) -->
     <string name="symbols_word_separators">"&#x0009;&#x0020;&#x000A;&#x00A0;"()[]{}*&amp;&lt;&gt;+=|.,;:!?/_\"</string>
diff --git a/java/res/values/donottranslate-config-spacing-and-punctuations.xml b/java/res/values/donottranslate-config-spacing-and-punctuations.xml
index 1be5cf8..2faf578 100644
--- a/java/res/values/donottranslate-config-spacing-and-punctuations.xml
+++ b/java/res/values/donottranslate-config-spacing-and-punctuations.xml
@@ -26,6 +26,8 @@
     <string name="symbols_preceded_by_space">([{&amp;</string>
     <!-- Symbols that are normally followed by a space (used to add an auto-space after these) -->
     <string name="symbols_followed_by_space">.,;:!?)]}&amp;</string>
+    <!-- Symbols that behave like a single punctuation when typed next to each other -->
+    <string name="symbols_clustering_together"></string>
     <!-- Symbols that separate words -->
     <!-- Don't remove the enclosing double quotes, they protect whitespace (not just U+0020) -->
     <string name="symbols_word_separators">"&#x0009;&#x0020;&#x000A;&#x00A0;"()[]{}*&amp;&lt;&gt;+=|.,;:!?/_\"</string>
diff --git a/java/src/com/android/inputmethod/latin/inputlogic/InputLogic.java b/java/src/com/android/inputmethod/latin/inputlogic/InputLogic.java
index d2100d4..75432fb 100644
--- a/java/src/com/android/inputmethod/latin/inputlogic/InputLogic.java
+++ b/java/src/com/android/inputmethod/latin/inputlogic/InputLogic.java
@@ -784,11 +784,11 @@
             // TODO: remove this argument
             final LatinIME.UIHandler handler) {
         final int codePoint = inputTransaction.mEvent.mCodePoint;
+        final SettingsValues settingsValues = inputTransaction.mSettingsValues;
         boolean didAutoCorrect = false;
         // We avoid sending spaces in languages without spaces if we were composing.
         final boolean shouldAvoidSendingCode = Constants.CODE_SPACE == codePoint
-                && !inputTransaction.mSettingsValues.mSpacingAndPunctuations
-                        .mCurrentLanguageHasSpaces
+                && !settingsValues.mSpacingAndPunctuations.mCurrentLanguageHasSpaces
                 && mWordComposer.isComposingWord();
         if (mWordComposer.isCursorFrontOrMiddleOfComposingWord()) {
             // If we are in the middle of a recorrection, we need to commit the recorrection
@@ -798,13 +798,13 @@
         }
         // isComposingWord() may have changed since we stored wasComposing
         if (mWordComposer.isComposingWord()) {
-            if (inputTransaction.mSettingsValues.mCorrectionEnabled) {
+            if (settingsValues.mCorrectionEnabled) {
                 final String separator = shouldAvoidSendingCode ? LastComposedWord.NOT_A_SEPARATOR
                         : StringUtils.newSingleCodePointString(codePoint);
-                commitCurrentAutoCorrection(inputTransaction.mSettingsValues, separator, handler);
+                commitCurrentAutoCorrection(settingsValues, separator, handler);
                 didAutoCorrect = true;
             } else {
-                commitTyped(inputTransaction.mSettingsValues,
+                commitTyped(settingsValues,
                         StringUtils.newSingleCodePointString(codePoint));
             }
         }
@@ -821,20 +821,23 @@
             // Double quotes behave like they are usually preceded by space iff we are
             // not inside a double quote or after a digit.
             needsPrecedingSpace = !isInsideDoubleQuoteOrAfterDigit;
+        } else if (settingsValues.mSpacingAndPunctuations.isClusteringSymbol(codePoint)
+                && settingsValues.mSpacingAndPunctuations.isClusteringSymbol(
+                        mConnection.getCodePointBeforeCursor())) {
+            needsPrecedingSpace = false;
         } else {
-            needsPrecedingSpace = inputTransaction.mSettingsValues.isUsuallyPrecededBySpace(
-                    codePoint);
+            needsPrecedingSpace = settingsValues.isUsuallyPrecededBySpace(codePoint);
         }
 
         if (needsPrecedingSpace) {
-            promotePhantomSpace(inputTransaction.mSettingsValues);
+            promotePhantomSpace(settingsValues);
         }
         if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) {
             ResearchLogger.latinIME_handleSeparator(codePoint, mWordComposer.isComposingWord());
         }
 
         if (!shouldAvoidSendingCode) {
-            sendKeyCodePoint(inputTransaction.mSettingsValues, codePoint);
+            sendKeyCodePoint(settingsValues, codePoint);
         }
 
         if (Constants.CODE_SPACE == codePoint) {
@@ -852,7 +855,7 @@
                 swapSwapperAndSpace(inputTransaction);
                 mSpaceState = SpaceState.SWAP_PUNCTUATION;
             } else if ((SpaceState.PHANTOM == inputTransaction.mSpaceState
-                    && inputTransaction.mSettingsValues.isUsuallyFollowedBySpace(codePoint))
+                    && settingsValues.isUsuallyFollowedBySpace(codePoint))
                     || (Constants.CODE_DOUBLE_QUOTE == codePoint
                             && isInsideDoubleQuoteOrAfterDigit)) {
                 // If we are in phantom space state, and the user presses a separator, we want to
diff --git a/java/src/com/android/inputmethod/latin/settings/SpacingAndPunctuations.java b/java/src/com/android/inputmethod/latin/settings/SpacingAndPunctuations.java
index 796921f..b8d2a22 100644
--- a/java/src/com/android/inputmethod/latin/settings/SpacingAndPunctuations.java
+++ b/java/src/com/android/inputmethod/latin/settings/SpacingAndPunctuations.java
@@ -30,6 +30,7 @@
 public final class SpacingAndPunctuations {
     private final int[] mSortedSymbolsPrecededBySpace;
     private final int[] mSortedSymbolsFollowedBySpace;
+    private final int[] mSortedSymbolsClusteringTogether;
     private final int[] mSortedWordConnectors;
     public final int[] mSortedWordSeparators;
     public final PunctuationSuggestions mSuggestPuncList;
@@ -46,6 +47,8 @@
         // To be able to binary search the code point. See {@link #isUsuallyFollowedBySpace(int)}.
         mSortedSymbolsFollowedBySpace = StringUtils.toSortedCodePointArray(
                 res.getString(R.string.symbols_followed_by_space));
+        mSortedSymbolsClusteringTogether = StringUtils.toSortedCodePointArray(
+                res.getString(R.string.symbols_clustering_together));
         // To be able to binary search the code point. See {@link #isWordConnector(int)}.
         mSortedWordConnectors = StringUtils.toSortedCodePointArray(
                 res.getString(R.string.symbols_word_connectors));
@@ -85,6 +88,10 @@
         return Arrays.binarySearch(mSortedSymbolsFollowedBySpace, code) >= 0;
     }
 
+    public boolean isClusteringSymbol(final int code) {
+        return Arrays.binarySearch(mSortedSymbolsClusteringTogether, code) >= 0;
+    }
+
     public boolean isSentenceSeparator(final int code) {
         return code == mSentenceSeparator;
     }
diff --git a/tests/src/com/android/inputmethod/latin/InputLogicTests.java b/tests/src/com/android/inputmethod/latin/InputLogicTests.java
index d2dd292..29423e8 100644
--- a/tests/src/com/android/inputmethod/latin/InputLogicTests.java
+++ b/tests/src/com/android/inputmethod/latin/InputLogicTests.java
@@ -334,6 +334,18 @@
         assertEquals("manual pick then separator", EXPECTED_RESULT, mEditText.getText().toString());
     }
 
+    // This test matches the one in InputLogicTestsNonEnglish. In some non-English languages,
+    // ! and ? are clustering punctuation signs.
+    public void testClusteringPunctuation() {
+        final String WORD1_TO_TYPE = "test";
+        final String WORD2_TO_TYPE = "!!?!:!";
+        final String EXPECTED_RESULT = "test!!?!:!";
+        type(WORD1_TO_TYPE);
+        pickSuggestionManually(0, WORD1_TO_TYPE);
+        type(WORD2_TO_TYPE);
+        assertEquals("clustering punctuation", EXPECTED_RESULT, mEditText.getText().toString());
+    }
+
     public void testManualPickThenStripperThenPick() {
         final String WORD_TO_TYPE = "this";
         final String STRIPPER = "\n";
diff --git a/tests/src/com/android/inputmethod/latin/InputLogicTestsNonEnglish.java b/tests/src/com/android/inputmethod/latin/InputLogicTestsNonEnglish.java
index 1257ae2..68b6ee6 100644
--- a/tests/src/com/android/inputmethod/latin/InputLogicTestsNonEnglish.java
+++ b/tests/src/com/android/inputmethod/latin/InputLogicTestsNonEnglish.java
@@ -45,6 +45,19 @@
                 mEditText.getText().toString());
     }
 
+    public void testClusteringPunctuationForFrench() {
+        final String WORD1_TO_TYPE = "test";
+        final String WORD2_TO_TYPE = "!!?!:!";
+        // In English, the expected result would be "test!!?!:!"
+        final String EXPECTED_RESULT = "test !!?! : !";
+        changeLanguage("fr");
+        type(WORD1_TO_TYPE);
+        pickSuggestionManually(0, WORD1_TO_TYPE);
+        type(WORD2_TO_TYPE);
+        assertEquals("clustering punctuation for French", EXPECTED_RESULT,
+                mEditText.getText().toString());
+    }
+
     public void testWordThenSpaceThenPunctuationFromStripTwiceForFrench() {
         final String WORD_TO_TYPE = "test ";
         final String PUNCTUATION_FROM_STRIP = "!";