Loading java/src/com/android/inputmethod/latin/RichInputConnection.java +11 −0 Original line number Diff line number Diff line Loading @@ -814,6 +814,17 @@ public final class RichInputConnection { return StringUtils.lastPartLooksLikeURL(mCommittedTextBeforeComposingText); } /** * Looks at the text just before the cursor to find out if we are inside a double quote. * * As with #textBeforeCursorLooksLikeURL, this is dependent on how much text we have cached. * However this won't be a concrete problem in most situations, as the cache is almost always * long enough for this use. */ public boolean isInsideDoubleQuoteOrAfterDigit() { return StringUtils.isInsideDoubleQuoteOrAfterDigit(mCommittedTextBeforeComposingText); } /** * Try to get the text from the editor to expose lies the framework may have been * telling us. Concretely, when the device rotates, the frameworks tells us about where the Loading java/src/com/android/inputmethod/latin/inputlogic/InputLogic.java +21 −5 Original line number Diff line number Diff line Loading @@ -670,8 +670,21 @@ public final class InputLogic { final boolean swapWeakSpace = maybeStripSpace(settingsValues, codePoint, spaceState, isFromSuggestionStrip); if (SpaceState.PHANTOM == spaceState && settingsValues.isUsuallyPrecededBySpace(codePoint)) { final boolean isInsideDoubleQuoteOrAfterDigit = Constants.CODE_DOUBLE_QUOTE == codePoint && mConnection.isInsideDoubleQuoteOrAfterDigit(); final boolean needsPrecedingSpace; if (SpaceState.PHANTOM != spaceState) { needsPrecedingSpace = false; } else if (Constants.CODE_DOUBLE_QUOTE == codePoint) { // 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 { needsPrecedingSpace = settingsValues.isUsuallyPrecededBySpace(codePoint); } if (needsPrecedingSpace) { promotePhantomSpace(settingsValues); } if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) { Loading @@ -698,14 +711,17 @@ public final class InputLogic { if (swapWeakSpace) { swapSwapperAndSpace(keyboardSwitcher); mSpaceState = SpaceState.SWAP_PUNCTUATION; } else if (SpaceState.PHANTOM == spaceState && settingsValues.isUsuallyFollowedBySpace(codePoint)) { } else if ((SpaceState.PHANTOM == spaceState && 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 // stay in phantom space state so that the next keypress has a chance to add the // space. For example, if I type "Good dat", pick "day" from the suggestion strip // then insert a comma and go on to typing the next word, I want the space to be // inserted automatically before the next word, the same way it is when I don't // input the comma. // input the comma. A double quote behaves like it's usually followed by space if // we're inside a double quote. // The case is a little different if the separator is a space stripper. Such a // separator does not normally need a space on the right (that's the difference // between swappers and strippers), so we should not stay in phantom space state if Loading java/src/com/android/inputmethod/latin/utils/StringUtils.java +43 −1 Original line number Diff line number Diff line Loading @@ -387,6 +387,48 @@ public final class StringUtils { return false; } /** * Examines the string and returns whether we're inside a double quote. * * This is used to decide whether we should put an automatic space before or after a double * quote character. If we're inside a quotation, then we want to close it, so we want a space * after and not before. Otherwise, we want to open the quotation, so we want a space before * and not after. Exception: after a digit, we never want a space because the "inch" or * "minutes" use cases is dominant after digits. * In the practice, we determine whether we are in a quotation or not by finding the previous * double quote character, and looking at whether it's followed by whitespace. If so, that * was a closing quotation mark, so we're not inside a double quote. If it's not followed * by whitespace, then it was an opening quotation mark, and we're inside a quotation. * * @param text the text to examine. * @return whether we're inside a double quote. */ public static boolean isInsideDoubleQuoteOrAfterDigit(final CharSequence text) { int i = text.length(); if (0 == i) return false; int codePoint = Character.codePointBefore(text, i); if (Character.isDigit(codePoint)) return true; int prevCodePoint = 0; while (i > 0) { codePoint = Character.codePointBefore(text, i); if (Constants.CODE_DOUBLE_QUOTE == codePoint) { // If we see a double quote followed by whitespace, then that // was a closing quote. if (Character.isWhitespace(prevCodePoint)) return false; } if (Character.isWhitespace(codePoint) && Constants.CODE_DOUBLE_QUOTE == prevCodePoint) { // If we see a double quote preceded by whitespace, then that // was an opening quote. No need to continue seeking. return true; } i -= Character.charCount(codePoint); prevCodePoint = codePoint; } // We reached the start of text. If the first char is a double quote, then we're inside // a double quote. Otherwise we're not. return Constants.CODE_DOUBLE_QUOTE == codePoint; } public static boolean isEmptyStringOrWhiteSpaces(final String s) { final int N = codePointCount(s); for (int i = 0; i < N; ++i) { Loading tests/src/com/android/inputmethod/latin/PunctuationTests.java +28 −0 Original line number Diff line number Diff line Loading @@ -169,4 +169,32 @@ public class PunctuationTests extends InputTestsBase { + " ; Suggestions = " + mLatinIME.getSuggestedWords(), EXPECTED_RESULT, mEditText.getText().toString()); } public void testAutoSpaceWithDoubleQuotes() { final String STRING_TO_TYPE = "He said\"hello\"to me. I replied,\"hi\"." + "Then, 5\"passed. He said\"bye\"and left."; final String EXPECTED_RESULT = "He said \"hello\" to me. I replied, \"hi\". " + "Then, 5\" passed. He said \"bye\" and left. \""; // Split by double quote, so that we can type the double quotes individually. for (final String partToType : STRING_TO_TYPE.split("\"")) { // Split at word boundaries. This regexp means "anywhere that is preceded // by a word character but not followed by a word character, OR that is not // preceded by a word character but followed by a word character". // We need to input word by word because auto-spaces are only active when // manually picking or gesturing (which we can't simulate yet), but only words // can be picked. final String[] wordsToType = partToType.split("(?<=\\w)(?!\\w)|(?<!\\w)(?=\\w)"); for (final String wordToType : wordsToType) { type(wordToType); if (wordToType.matches("^\\w+$")) { // Only pick selection if that was a word, because if that was not a word, // then we don't have a composition. pickSuggestionManually(0, wordToType); } } type("\""); } assertEquals("auto-space with double quotes", EXPECTED_RESULT, mEditText.getText().toString()); } } Loading
java/src/com/android/inputmethod/latin/RichInputConnection.java +11 −0 Original line number Diff line number Diff line Loading @@ -814,6 +814,17 @@ public final class RichInputConnection { return StringUtils.lastPartLooksLikeURL(mCommittedTextBeforeComposingText); } /** * Looks at the text just before the cursor to find out if we are inside a double quote. * * As with #textBeforeCursorLooksLikeURL, this is dependent on how much text we have cached. * However this won't be a concrete problem in most situations, as the cache is almost always * long enough for this use. */ public boolean isInsideDoubleQuoteOrAfterDigit() { return StringUtils.isInsideDoubleQuoteOrAfterDigit(mCommittedTextBeforeComposingText); } /** * Try to get the text from the editor to expose lies the framework may have been * telling us. Concretely, when the device rotates, the frameworks tells us about where the Loading
java/src/com/android/inputmethod/latin/inputlogic/InputLogic.java +21 −5 Original line number Diff line number Diff line Loading @@ -670,8 +670,21 @@ public final class InputLogic { final boolean swapWeakSpace = maybeStripSpace(settingsValues, codePoint, spaceState, isFromSuggestionStrip); if (SpaceState.PHANTOM == spaceState && settingsValues.isUsuallyPrecededBySpace(codePoint)) { final boolean isInsideDoubleQuoteOrAfterDigit = Constants.CODE_DOUBLE_QUOTE == codePoint && mConnection.isInsideDoubleQuoteOrAfterDigit(); final boolean needsPrecedingSpace; if (SpaceState.PHANTOM != spaceState) { needsPrecedingSpace = false; } else if (Constants.CODE_DOUBLE_QUOTE == codePoint) { // 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 { needsPrecedingSpace = settingsValues.isUsuallyPrecededBySpace(codePoint); } if (needsPrecedingSpace) { promotePhantomSpace(settingsValues); } if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) { Loading @@ -698,14 +711,17 @@ public final class InputLogic { if (swapWeakSpace) { swapSwapperAndSpace(keyboardSwitcher); mSpaceState = SpaceState.SWAP_PUNCTUATION; } else if (SpaceState.PHANTOM == spaceState && settingsValues.isUsuallyFollowedBySpace(codePoint)) { } else if ((SpaceState.PHANTOM == spaceState && 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 // stay in phantom space state so that the next keypress has a chance to add the // space. For example, if I type "Good dat", pick "day" from the suggestion strip // then insert a comma and go on to typing the next word, I want the space to be // inserted automatically before the next word, the same way it is when I don't // input the comma. // input the comma. A double quote behaves like it's usually followed by space if // we're inside a double quote. // The case is a little different if the separator is a space stripper. Such a // separator does not normally need a space on the right (that's the difference // between swappers and strippers), so we should not stay in phantom space state if Loading
java/src/com/android/inputmethod/latin/utils/StringUtils.java +43 −1 Original line number Diff line number Diff line Loading @@ -387,6 +387,48 @@ public final class StringUtils { return false; } /** * Examines the string and returns whether we're inside a double quote. * * This is used to decide whether we should put an automatic space before or after a double * quote character. If we're inside a quotation, then we want to close it, so we want a space * after and not before. Otherwise, we want to open the quotation, so we want a space before * and not after. Exception: after a digit, we never want a space because the "inch" or * "minutes" use cases is dominant after digits. * In the practice, we determine whether we are in a quotation or not by finding the previous * double quote character, and looking at whether it's followed by whitespace. If so, that * was a closing quotation mark, so we're not inside a double quote. If it's not followed * by whitespace, then it was an opening quotation mark, and we're inside a quotation. * * @param text the text to examine. * @return whether we're inside a double quote. */ public static boolean isInsideDoubleQuoteOrAfterDigit(final CharSequence text) { int i = text.length(); if (0 == i) return false; int codePoint = Character.codePointBefore(text, i); if (Character.isDigit(codePoint)) return true; int prevCodePoint = 0; while (i > 0) { codePoint = Character.codePointBefore(text, i); if (Constants.CODE_DOUBLE_QUOTE == codePoint) { // If we see a double quote followed by whitespace, then that // was a closing quote. if (Character.isWhitespace(prevCodePoint)) return false; } if (Character.isWhitespace(codePoint) && Constants.CODE_DOUBLE_QUOTE == prevCodePoint) { // If we see a double quote preceded by whitespace, then that // was an opening quote. No need to continue seeking. return true; } i -= Character.charCount(codePoint); prevCodePoint = codePoint; } // We reached the start of text. If the first char is a double quote, then we're inside // a double quote. Otherwise we're not. return Constants.CODE_DOUBLE_QUOTE == codePoint; } public static boolean isEmptyStringOrWhiteSpaces(final String s) { final int N = codePointCount(s); for (int i = 0; i < N; ++i) { Loading
tests/src/com/android/inputmethod/latin/PunctuationTests.java +28 −0 Original line number Diff line number Diff line Loading @@ -169,4 +169,32 @@ public class PunctuationTests extends InputTestsBase { + " ; Suggestions = " + mLatinIME.getSuggestedWords(), EXPECTED_RESULT, mEditText.getText().toString()); } public void testAutoSpaceWithDoubleQuotes() { final String STRING_TO_TYPE = "He said\"hello\"to me. I replied,\"hi\"." + "Then, 5\"passed. He said\"bye\"and left."; final String EXPECTED_RESULT = "He said \"hello\" to me. I replied, \"hi\". " + "Then, 5\" passed. He said \"bye\" and left. \""; // Split by double quote, so that we can type the double quotes individually. for (final String partToType : STRING_TO_TYPE.split("\"")) { // Split at word boundaries. This regexp means "anywhere that is preceded // by a word character but not followed by a word character, OR that is not // preceded by a word character but followed by a word character". // We need to input word by word because auto-spaces are only active when // manually picking or gesturing (which we can't simulate yet), but only words // can be picked. final String[] wordsToType = partToType.split("(?<=\\w)(?!\\w)|(?<!\\w)(?=\\w)"); for (final String wordToType : wordsToType) { type(wordToType); if (wordToType.matches("^\\w+$")) { // Only pick selection if that was a word, because if that was not a word, // then we don't have a composition. pickSuggestionManually(0, wordToType); } } type("\""); } assertEquals("auto-space with double quotes", EXPECTED_RESULT, mEditText.getText().toString()); } }