Loading core/java/android/widget/SpellChecker.java +193 −56 Original line number Original line Diff line number Diff line Loading @@ -26,6 +26,7 @@ import android.text.style.SpellCheckSpan; import android.text.style.SuggestionSpan; import android.text.style.SuggestionSpan; import android.util.Log; import android.util.Log; import android.util.LruCache; import android.util.LruCache; import android.util.Range; import android.view.textservice.SentenceSuggestionsInfo; import android.view.textservice.SentenceSuggestionsInfo; import android.view.textservice.SpellCheckerSession; import android.view.textservice.SpellCheckerSession; import android.view.textservice.SpellCheckerSession.SpellCheckerSessionListener; import android.view.textservice.SpellCheckerSession.SpellCheckerSessionListener; Loading Loading @@ -62,7 +63,8 @@ public class SpellChecker implements SpellCheckerSessionListener { // Pause between each spell check to keep the UI smooth // Pause between each spell check to keep the UI smooth private final static int SPELL_PAUSE_DURATION = 400; // milliseconds private final static int SPELL_PAUSE_DURATION = 400; // milliseconds private static final int MIN_SENTENCE_LENGTH = 50; // The maximum length of sentence. private static final int MAX_SENTENCE_LENGTH = WORD_ITERATOR_INTERVAL; private static final int USE_SPAN_RANGE = -1; private static final int USE_SPAN_RANGE = -1; Loading @@ -89,7 +91,7 @@ public class SpellChecker implements SpellCheckerSessionListener { // Shared by all SpellParsers. Cannot be shared with TextView since it may be used // Shared by all SpellParsers. Cannot be shared with TextView since it may be used // concurrently due to the asynchronous nature of onGetSuggestions. // concurrently due to the asynchronous nature of onGetSuggestions. private WordIterator mWordIterator; private SentenceIteratorWrapper mSentenceIterator; @Nullable @Nullable private TextServicesManager mTextServicesManager; private TextServicesManager mTextServicesManager; Loading Loading @@ -151,8 +153,9 @@ public class SpellChecker implements SpellCheckerSessionListener { resetSession(); resetSession(); if (locale != null) { if (locale != null) { // Change SpellParsers' wordIterator locale // Change SpellParsers' sentenceIterator locale mWordIterator = new WordIterator(locale); mSentenceIterator = new SentenceIteratorWrapper( BreakIterator.getSentenceInstance(locale)); } } // This class is the listener for locale change: warn other locale-aware objects // This class is the listener for locale change: warn other locale-aware objects Loading Loading @@ -306,22 +309,30 @@ public class SpellChecker implements SpellCheckerSessionListener { final int start = editable.getSpanStart(spellCheckSpan); final int start = editable.getSpanStart(spellCheckSpan); final int end = editable.getSpanEnd(spellCheckSpan); final int end = editable.getSpanEnd(spellCheckSpan); // Do not check this word if the user is currently editing it // Check the span if any of following conditions is met: final boolean isEditing; // - the user is not currently editing it // - or `forceCheckWhenEditingWord` is true. final boolean isNotEditing; // Defer spell check when typing a word ending with a punctuation like an apostrophe // Defer spell check when typing a word ending with a punctuation like an apostrophe // which could end up being a mid-word punctuation. // which could end up being a mid-word punctuation. if (selectionStart == end + 1 if (selectionStart == end + 1 && WordIterator.isMidWordPunctuation( && WordIterator.isMidWordPunctuation( mCurrentLocale, Character.codePointBefore(editable, end + 1))) { mCurrentLocale, Character.codePointBefore(editable, end + 1))) { isEditing = false; isNotEditing = false; } else { } else if (selectionEnd <= start || selectionStart > end) { // Allow the overlap of the cursor and the first boundary of the spell check span // Allow the overlap of the cursor and the first boundary of the spell check span // no to skip the spell check of the following word because the // no to skip the spell check of the following word because the // following word will never be spell-checked even if the user finishes composing // following word will never be spell-checked even if the user finishes composing isEditing = selectionEnd <= start || selectionStart > end; isNotEditing = true; } else { // When cursor is at the end of spell check span, allow spell check if the // character before cursor is a separator. isNotEditing = selectionStart == end && selectionStart > 0 && isSeparator(Character.codePointBefore(editable, selectionStart)); } } if (start >= 0 && end > start && (forceCheckWhenEditingWord || isEditing)) { if (start >= 0 && end > start && (forceCheckWhenEditingWord || isNotEditing)) { spellCheckSpan.setSpellCheckInProgress(true); spellCheckSpan.setSpellCheckInProgress(true); final TextInfo textInfo = new TextInfo(editable, start, end, mCookie, mIds[i]); final TextInfo textInfo = new TextInfo(editable, start, end, mCookie, mIds[i]); textInfos[textInfosCount++] = textInfo; textInfos[textInfosCount++] = textInfo; Loading @@ -346,6 +357,19 @@ public class SpellChecker implements SpellCheckerSessionListener { } } } } private static boolean isSeparator(int codepoint) { final int type = Character.getType(codepoint); return ((1 << type) & ((1 << Character.SPACE_SEPARATOR) | (1 << Character.LINE_SEPARATOR) | (1 << Character.PARAGRAPH_SEPARATOR) | (1 << Character.DASH_PUNCTUATION) | (1 << Character.END_PUNCTUATION) | (1 << Character.FINAL_QUOTE_PUNCTUATION) | (1 << Character.INITIAL_QUOTE_PUNCTUATION) | (1 << Character.START_PUNCTUATION) | (1 << Character.OTHER_PUNCTUATION))) != 0; } private SpellCheckSpan onGetSuggestionsInternal( private SpellCheckSpan onGetSuggestionsInternal( SuggestionsInfo suggestionsInfo, int offset, int length) { SuggestionsInfo suggestionsInfo, int offset, int length) { if (suggestionsInfo == null || suggestionsInfo.getCookie() != mCookie) { if (suggestionsInfo == null || suggestionsInfo.getCookie() != mCookie) { Loading Loading @@ -534,6 +558,60 @@ public class SpellChecker implements SpellCheckerSessionListener { mTextView.invalidateRegion(start, end, false /* No cursor involved */); mTextView.invalidateRegion(start, end, false /* No cursor involved */); } } /** * A wrapper of sentence iterator which only processes the specified window of the given text. */ private static class SentenceIteratorWrapper { private BreakIterator mSentenceIterator; private int mStartOffset; private int mEndOffset; SentenceIteratorWrapper(BreakIterator sentenceIterator) { mSentenceIterator = sentenceIterator; } /** * Set the char sequence and the text window to process. */ public void setCharSequence(CharSequence sequence, int start, int end) { mStartOffset = Math.max(0, start); mEndOffset = Math.min(end, sequence.length()); mSentenceIterator.setText(sequence.subSequence(mStartOffset, mEndOffset).toString()); } /** * See {@link BreakIterator#preceding(int)} */ public int preceding(int offset) { if (offset < mStartOffset) { return BreakIterator.DONE; } int result = mSentenceIterator.preceding(offset - mStartOffset); return result == BreakIterator.DONE ? BreakIterator.DONE : result + mStartOffset; } /** * See {@link BreakIterator#following(int)} */ public int following(int offset) { if (offset > mEndOffset) { return BreakIterator.DONE; } int result = mSentenceIterator.following(offset - mStartOffset); return result == BreakIterator.DONE ? BreakIterator.DONE : result + mStartOffset; } /** * See {@link BreakIterator#isBoundary(int)} */ public boolean isBoundary(int offset) { if (offset < mStartOffset || offset > mEndOffset) { return false; } return mSentenceIterator.isBoundary(offset - mStartOffset); } } private class SpellParser { private class SpellParser { private Object mRange = new Object(); private Object mRange = new Object(); Loading Loading @@ -582,27 +660,15 @@ public class SpellChecker implements SpellCheckerSessionListener { public void parse() { public void parse() { Editable editable = (Editable) mTextView.getText(); Editable editable = (Editable) mTextView.getText(); // Iterate over the newly added text and schedule new SpellCheckSpans final int textChangeStart = editable.getSpanStart(mRange); final int start = Math.max( final int textChangeEnd = editable.getSpanEnd(mRange); 0, editable.getSpanStart(mRange) - MIN_SENTENCE_LENGTH); final int end = editable.getSpanEnd(mRange); Range<Integer> sentenceBoundary = detectSentenceBoundary(editable, textChangeStart, textChangeEnd); int sentenceStart = sentenceBoundary.getLower(); int sentenceEnd = sentenceBoundary.getUpper(); int wordIteratorWindowEnd = Math.min(end, start + WORD_ITERATOR_INTERVAL); if (sentenceStart == sentenceEnd) { mWordIterator.setCharSequence(editable, start, wordIteratorWindowEnd); // Move back to the beginning of the current word, if any int wordStart = mWordIterator.preceding(start); int wordEnd; if (wordStart == BreakIterator.DONE) { wordEnd = mWordIterator.following(start); if (wordEnd != BreakIterator.DONE) { wordStart = mWordIterator.getBeginning(wordEnd); } } else { wordEnd = mWordIterator.getEnd(wordStart); } if (wordEnd == BreakIterator.DONE) { if (DBG) { if (DBG) { Log.i(TAG, "No more spell check."); Log.i(TAG, "No more spell check."); } } Loading @@ -612,29 +678,16 @@ public class SpellChecker implements SpellCheckerSessionListener { boolean scheduleOtherSpellCheck = false; boolean scheduleOtherSpellCheck = false; if (wordIteratorWindowEnd < end) { if (sentenceEnd < textChangeEnd) { if (DBG) { if (DBG) { Log.i(TAG, "schedule other spell check."); Log.i(TAG, "schedule other spell check."); } } // Several batches needed on that region. Cut after last previous word // Several batches needed on that region. Cut after last previous word scheduleOtherSpellCheck = true; scheduleOtherSpellCheck = true; } } int spellCheckEnd = mWordIterator.preceding(wordIteratorWindowEnd); int spellCheckEnd = sentenceEnd; boolean correct = spellCheckEnd != BreakIterator.DONE; if (correct) { spellCheckEnd = mWordIterator.getEnd(spellCheckEnd); correct = spellCheckEnd != BreakIterator.DONE; } if (!correct) { if (DBG) { Log.i(TAG, "Incorrect range span."); } stop(); return; } do { do { // TODO: Find the start position of the sentence. int spellCheckStart = sentenceStart; int spellCheckStart = wordStart; boolean createSpellCheckSpan = true; boolean createSpellCheckSpan = true; // Cancel or merge overlapped spell check spans // Cancel or merge overlapped spell check spans for (int i = 0; i < mLength; ++i) { for (int i = 0; i < mLength; ++i) { Loading Loading @@ -671,27 +724,23 @@ public class SpellChecker implements SpellCheckerSessionListener { } } // Stop spell checking when there are no characters in the range. // Stop spell checking when there are no characters in the range. if (spellCheckEnd < start) { break; } if (spellCheckEnd <= spellCheckStart) { if (spellCheckEnd <= spellCheckStart) { Log.w(TAG, "Trying to spellcheck invalid region, from " Log.w(TAG, "Trying to spellcheck invalid region, from " + start + " to " + end); + sentenceStart + " to " + spellCheckEnd); break; break; } } if (createSpellCheckSpan) { if (createSpellCheckSpan) { addSpellCheckSpan(editable, spellCheckStart, spellCheckEnd); addSpellCheckSpan(editable, spellCheckStart, spellCheckEnd); } } } while (false); } while (false); wordStart = spellCheckEnd; sentenceStart = spellCheckEnd; if (scheduleOtherSpellCheck && sentenceStart != BreakIterator.DONE if (scheduleOtherSpellCheck && wordStart != BreakIterator.DONE && wordStart <= end) { && sentenceStart <= textChangeEnd) { // Update range span: start new spell check from last wordStart // Update range span: start new spell check from last wordStart setRangeSpan(editable, wordStart, end); setRangeSpan(editable, sentenceStart, textChangeEnd); } else { } else { removeRangeSpan(editable); removeRangeSpan(editable); } } spellCheck(mForceCheckWhenEditingWord); spellCheck(mForceCheckWhenEditingWord); } } Loading @@ -708,6 +757,94 @@ public class SpellChecker implements SpellCheckerSessionListener { } } } } private Range<Integer> detectSentenceBoundary(CharSequence sequence, int textChangeStart, int textChangeEnd) { // Only process a substring of the full text due to performance concern. final int iteratorWindowStart = findSeparator(sequence, Math.max(0, textChangeStart - MAX_SENTENCE_LENGTH), Math.max(0, textChangeStart - 2 * MAX_SENTENCE_LENGTH)); final int iteratorWindowEnd = findSeparator(sequence, Math.min(textChangeStart + 2 * MAX_SENTENCE_LENGTH, textChangeEnd), Math.min(textChangeStart + 3 * MAX_SENTENCE_LENGTH, sequence.length())); if (DBG) { Log.d(TAG, "Set iterator window as [" + iteratorWindowStart + ", " + iteratorWindowEnd + ")."); } mSentenceIterator.setCharSequence(sequence, iteratorWindowStart, iteratorWindowEnd); // Detect the offset of sentence begin/end on the substring. int sentenceStart = mSentenceIterator.isBoundary(textChangeStart) ? textChangeStart : mSentenceIterator.preceding(textChangeStart); int sentenceEnd = mSentenceIterator.following(sentenceStart); if (sentenceEnd == BreakIterator.DONE) { sentenceEnd = iteratorWindowEnd; } if (DBG) { if (sentenceStart != sentenceEnd) { Log.d(TAG, "Sentence detected [" + sentenceStart + ", " + sentenceEnd + ")."); } } if (sentenceEnd - sentenceStart <= MAX_SENTENCE_LENGTH) { // Add more sentences until the MAX_SENTENCE_LENGTH limitation is reached. while (sentenceEnd < textChangeEnd) { int nextEnd = mSentenceIterator.following(sentenceEnd); if (nextEnd == BreakIterator.DONE || nextEnd - sentenceStart > MAX_SENTENCE_LENGTH) { break; } sentenceEnd = nextEnd; } } else { // If the sentence containing `textChangeStart` is longer than MAX_SENTENCE_LENGTH, // the sentence will be sliced into sub-sentences of about MAX_SENTENCE_LENGTH // characters each. This is done by processing the unchecked part of that sentence : // [textChangeStart, sentenceEnd) // // - If the `uncheckedLength` is bigger than MAX_SENTENCE_LENGTH, then check the // [textChangeStart, textChangeStart + MAX_SENTENCE_LENGTH), and leave the rest // part for the next check. // // - If the `uncheckedLength` is smaller than or equal to MAX_SENTENCE_LENGTH, // then check [sentenceEnd - MAX_SENTENCE_LENGTH, sentenceEnd). // // The offset should be rounded up to word boundary. int uncheckedLength = sentenceEnd - textChangeStart; if (uncheckedLength > MAX_SENTENCE_LENGTH) { sentenceEnd = findSeparator(sequence, sentenceStart + MAX_SENTENCE_LENGTH, sentenceEnd); sentenceStart = roundUpToWordStart(sequence, textChangeStart, sentenceStart); } else { sentenceStart = roundUpToWordStart(sequence, sentenceEnd - MAX_SENTENCE_LENGTH, sentenceStart); } } return new Range(sentenceStart, sentenceEnd); } private int roundUpToWordStart(CharSequence sequence, int position, int frontBoundary) { if (isSeparator(sequence.charAt(position))) { return position; } int separator = findSeparator(sequence, position, frontBoundary); return separator != frontBoundary ? separator + 1 : frontBoundary; } /** * Search the range [start, end) of sequence and returns the position of the first separator. * If end is smaller than start, do a reverse search. * Returns `end` if no separator is found. */ private static int findSeparator(CharSequence sequence, int start, int end) { final int step = start < end ? 1 : -1; for (int i = start; i != end; i += step) { if (isSeparator(sequence.charAt(i))) { return i; } } return end; } public static boolean haveWordBoundariesChanged(final Editable editable, final int start, public static boolean haveWordBoundariesChanged(final Editable editable, final int start, final int end, final int spanStart, final int spanEnd) { final int end, final int spanStart, final int spanEnd) { final boolean haveWordBoundariesChanged; final boolean haveWordBoundariesChanged; Loading Loading
core/java/android/widget/SpellChecker.java +193 −56 Original line number Original line Diff line number Diff line Loading @@ -26,6 +26,7 @@ import android.text.style.SpellCheckSpan; import android.text.style.SuggestionSpan; import android.text.style.SuggestionSpan; import android.util.Log; import android.util.Log; import android.util.LruCache; import android.util.LruCache; import android.util.Range; import android.view.textservice.SentenceSuggestionsInfo; import android.view.textservice.SentenceSuggestionsInfo; import android.view.textservice.SpellCheckerSession; import android.view.textservice.SpellCheckerSession; import android.view.textservice.SpellCheckerSession.SpellCheckerSessionListener; import android.view.textservice.SpellCheckerSession.SpellCheckerSessionListener; Loading Loading @@ -62,7 +63,8 @@ public class SpellChecker implements SpellCheckerSessionListener { // Pause between each spell check to keep the UI smooth // Pause between each spell check to keep the UI smooth private final static int SPELL_PAUSE_DURATION = 400; // milliseconds private final static int SPELL_PAUSE_DURATION = 400; // milliseconds private static final int MIN_SENTENCE_LENGTH = 50; // The maximum length of sentence. private static final int MAX_SENTENCE_LENGTH = WORD_ITERATOR_INTERVAL; private static final int USE_SPAN_RANGE = -1; private static final int USE_SPAN_RANGE = -1; Loading @@ -89,7 +91,7 @@ public class SpellChecker implements SpellCheckerSessionListener { // Shared by all SpellParsers. Cannot be shared with TextView since it may be used // Shared by all SpellParsers. Cannot be shared with TextView since it may be used // concurrently due to the asynchronous nature of onGetSuggestions. // concurrently due to the asynchronous nature of onGetSuggestions. private WordIterator mWordIterator; private SentenceIteratorWrapper mSentenceIterator; @Nullable @Nullable private TextServicesManager mTextServicesManager; private TextServicesManager mTextServicesManager; Loading Loading @@ -151,8 +153,9 @@ public class SpellChecker implements SpellCheckerSessionListener { resetSession(); resetSession(); if (locale != null) { if (locale != null) { // Change SpellParsers' wordIterator locale // Change SpellParsers' sentenceIterator locale mWordIterator = new WordIterator(locale); mSentenceIterator = new SentenceIteratorWrapper( BreakIterator.getSentenceInstance(locale)); } } // This class is the listener for locale change: warn other locale-aware objects // This class is the listener for locale change: warn other locale-aware objects Loading Loading @@ -306,22 +309,30 @@ public class SpellChecker implements SpellCheckerSessionListener { final int start = editable.getSpanStart(spellCheckSpan); final int start = editable.getSpanStart(spellCheckSpan); final int end = editable.getSpanEnd(spellCheckSpan); final int end = editable.getSpanEnd(spellCheckSpan); // Do not check this word if the user is currently editing it // Check the span if any of following conditions is met: final boolean isEditing; // - the user is not currently editing it // - or `forceCheckWhenEditingWord` is true. final boolean isNotEditing; // Defer spell check when typing a word ending with a punctuation like an apostrophe // Defer spell check when typing a word ending with a punctuation like an apostrophe // which could end up being a mid-word punctuation. // which could end up being a mid-word punctuation. if (selectionStart == end + 1 if (selectionStart == end + 1 && WordIterator.isMidWordPunctuation( && WordIterator.isMidWordPunctuation( mCurrentLocale, Character.codePointBefore(editable, end + 1))) { mCurrentLocale, Character.codePointBefore(editable, end + 1))) { isEditing = false; isNotEditing = false; } else { } else if (selectionEnd <= start || selectionStart > end) { // Allow the overlap of the cursor and the first boundary of the spell check span // Allow the overlap of the cursor and the first boundary of the spell check span // no to skip the spell check of the following word because the // no to skip the spell check of the following word because the // following word will never be spell-checked even if the user finishes composing // following word will never be spell-checked even if the user finishes composing isEditing = selectionEnd <= start || selectionStart > end; isNotEditing = true; } else { // When cursor is at the end of spell check span, allow spell check if the // character before cursor is a separator. isNotEditing = selectionStart == end && selectionStart > 0 && isSeparator(Character.codePointBefore(editable, selectionStart)); } } if (start >= 0 && end > start && (forceCheckWhenEditingWord || isEditing)) { if (start >= 0 && end > start && (forceCheckWhenEditingWord || isNotEditing)) { spellCheckSpan.setSpellCheckInProgress(true); spellCheckSpan.setSpellCheckInProgress(true); final TextInfo textInfo = new TextInfo(editable, start, end, mCookie, mIds[i]); final TextInfo textInfo = new TextInfo(editable, start, end, mCookie, mIds[i]); textInfos[textInfosCount++] = textInfo; textInfos[textInfosCount++] = textInfo; Loading @@ -346,6 +357,19 @@ public class SpellChecker implements SpellCheckerSessionListener { } } } } private static boolean isSeparator(int codepoint) { final int type = Character.getType(codepoint); return ((1 << type) & ((1 << Character.SPACE_SEPARATOR) | (1 << Character.LINE_SEPARATOR) | (1 << Character.PARAGRAPH_SEPARATOR) | (1 << Character.DASH_PUNCTUATION) | (1 << Character.END_PUNCTUATION) | (1 << Character.FINAL_QUOTE_PUNCTUATION) | (1 << Character.INITIAL_QUOTE_PUNCTUATION) | (1 << Character.START_PUNCTUATION) | (1 << Character.OTHER_PUNCTUATION))) != 0; } private SpellCheckSpan onGetSuggestionsInternal( private SpellCheckSpan onGetSuggestionsInternal( SuggestionsInfo suggestionsInfo, int offset, int length) { SuggestionsInfo suggestionsInfo, int offset, int length) { if (suggestionsInfo == null || suggestionsInfo.getCookie() != mCookie) { if (suggestionsInfo == null || suggestionsInfo.getCookie() != mCookie) { Loading Loading @@ -534,6 +558,60 @@ public class SpellChecker implements SpellCheckerSessionListener { mTextView.invalidateRegion(start, end, false /* No cursor involved */); mTextView.invalidateRegion(start, end, false /* No cursor involved */); } } /** * A wrapper of sentence iterator which only processes the specified window of the given text. */ private static class SentenceIteratorWrapper { private BreakIterator mSentenceIterator; private int mStartOffset; private int mEndOffset; SentenceIteratorWrapper(BreakIterator sentenceIterator) { mSentenceIterator = sentenceIterator; } /** * Set the char sequence and the text window to process. */ public void setCharSequence(CharSequence sequence, int start, int end) { mStartOffset = Math.max(0, start); mEndOffset = Math.min(end, sequence.length()); mSentenceIterator.setText(sequence.subSequence(mStartOffset, mEndOffset).toString()); } /** * See {@link BreakIterator#preceding(int)} */ public int preceding(int offset) { if (offset < mStartOffset) { return BreakIterator.DONE; } int result = mSentenceIterator.preceding(offset - mStartOffset); return result == BreakIterator.DONE ? BreakIterator.DONE : result + mStartOffset; } /** * See {@link BreakIterator#following(int)} */ public int following(int offset) { if (offset > mEndOffset) { return BreakIterator.DONE; } int result = mSentenceIterator.following(offset - mStartOffset); return result == BreakIterator.DONE ? BreakIterator.DONE : result + mStartOffset; } /** * See {@link BreakIterator#isBoundary(int)} */ public boolean isBoundary(int offset) { if (offset < mStartOffset || offset > mEndOffset) { return false; } return mSentenceIterator.isBoundary(offset - mStartOffset); } } private class SpellParser { private class SpellParser { private Object mRange = new Object(); private Object mRange = new Object(); Loading Loading @@ -582,27 +660,15 @@ public class SpellChecker implements SpellCheckerSessionListener { public void parse() { public void parse() { Editable editable = (Editable) mTextView.getText(); Editable editable = (Editable) mTextView.getText(); // Iterate over the newly added text and schedule new SpellCheckSpans final int textChangeStart = editable.getSpanStart(mRange); final int start = Math.max( final int textChangeEnd = editable.getSpanEnd(mRange); 0, editable.getSpanStart(mRange) - MIN_SENTENCE_LENGTH); final int end = editable.getSpanEnd(mRange); Range<Integer> sentenceBoundary = detectSentenceBoundary(editable, textChangeStart, textChangeEnd); int sentenceStart = sentenceBoundary.getLower(); int sentenceEnd = sentenceBoundary.getUpper(); int wordIteratorWindowEnd = Math.min(end, start + WORD_ITERATOR_INTERVAL); if (sentenceStart == sentenceEnd) { mWordIterator.setCharSequence(editable, start, wordIteratorWindowEnd); // Move back to the beginning of the current word, if any int wordStart = mWordIterator.preceding(start); int wordEnd; if (wordStart == BreakIterator.DONE) { wordEnd = mWordIterator.following(start); if (wordEnd != BreakIterator.DONE) { wordStart = mWordIterator.getBeginning(wordEnd); } } else { wordEnd = mWordIterator.getEnd(wordStart); } if (wordEnd == BreakIterator.DONE) { if (DBG) { if (DBG) { Log.i(TAG, "No more spell check."); Log.i(TAG, "No more spell check."); } } Loading @@ -612,29 +678,16 @@ public class SpellChecker implements SpellCheckerSessionListener { boolean scheduleOtherSpellCheck = false; boolean scheduleOtherSpellCheck = false; if (wordIteratorWindowEnd < end) { if (sentenceEnd < textChangeEnd) { if (DBG) { if (DBG) { Log.i(TAG, "schedule other spell check."); Log.i(TAG, "schedule other spell check."); } } // Several batches needed on that region. Cut after last previous word // Several batches needed on that region. Cut after last previous word scheduleOtherSpellCheck = true; scheduleOtherSpellCheck = true; } } int spellCheckEnd = mWordIterator.preceding(wordIteratorWindowEnd); int spellCheckEnd = sentenceEnd; boolean correct = spellCheckEnd != BreakIterator.DONE; if (correct) { spellCheckEnd = mWordIterator.getEnd(spellCheckEnd); correct = spellCheckEnd != BreakIterator.DONE; } if (!correct) { if (DBG) { Log.i(TAG, "Incorrect range span."); } stop(); return; } do { do { // TODO: Find the start position of the sentence. int spellCheckStart = sentenceStart; int spellCheckStart = wordStart; boolean createSpellCheckSpan = true; boolean createSpellCheckSpan = true; // Cancel or merge overlapped spell check spans // Cancel or merge overlapped spell check spans for (int i = 0; i < mLength; ++i) { for (int i = 0; i < mLength; ++i) { Loading Loading @@ -671,27 +724,23 @@ public class SpellChecker implements SpellCheckerSessionListener { } } // Stop spell checking when there are no characters in the range. // Stop spell checking when there are no characters in the range. if (spellCheckEnd < start) { break; } if (spellCheckEnd <= spellCheckStart) { if (spellCheckEnd <= spellCheckStart) { Log.w(TAG, "Trying to spellcheck invalid region, from " Log.w(TAG, "Trying to spellcheck invalid region, from " + start + " to " + end); + sentenceStart + " to " + spellCheckEnd); break; break; } } if (createSpellCheckSpan) { if (createSpellCheckSpan) { addSpellCheckSpan(editable, spellCheckStart, spellCheckEnd); addSpellCheckSpan(editable, spellCheckStart, spellCheckEnd); } } } while (false); } while (false); wordStart = spellCheckEnd; sentenceStart = spellCheckEnd; if (scheduleOtherSpellCheck && sentenceStart != BreakIterator.DONE if (scheduleOtherSpellCheck && wordStart != BreakIterator.DONE && wordStart <= end) { && sentenceStart <= textChangeEnd) { // Update range span: start new spell check from last wordStart // Update range span: start new spell check from last wordStart setRangeSpan(editable, wordStart, end); setRangeSpan(editable, sentenceStart, textChangeEnd); } else { } else { removeRangeSpan(editable); removeRangeSpan(editable); } } spellCheck(mForceCheckWhenEditingWord); spellCheck(mForceCheckWhenEditingWord); } } Loading @@ -708,6 +757,94 @@ public class SpellChecker implements SpellCheckerSessionListener { } } } } private Range<Integer> detectSentenceBoundary(CharSequence sequence, int textChangeStart, int textChangeEnd) { // Only process a substring of the full text due to performance concern. final int iteratorWindowStart = findSeparator(sequence, Math.max(0, textChangeStart - MAX_SENTENCE_LENGTH), Math.max(0, textChangeStart - 2 * MAX_SENTENCE_LENGTH)); final int iteratorWindowEnd = findSeparator(sequence, Math.min(textChangeStart + 2 * MAX_SENTENCE_LENGTH, textChangeEnd), Math.min(textChangeStart + 3 * MAX_SENTENCE_LENGTH, sequence.length())); if (DBG) { Log.d(TAG, "Set iterator window as [" + iteratorWindowStart + ", " + iteratorWindowEnd + ")."); } mSentenceIterator.setCharSequence(sequence, iteratorWindowStart, iteratorWindowEnd); // Detect the offset of sentence begin/end on the substring. int sentenceStart = mSentenceIterator.isBoundary(textChangeStart) ? textChangeStart : mSentenceIterator.preceding(textChangeStart); int sentenceEnd = mSentenceIterator.following(sentenceStart); if (sentenceEnd == BreakIterator.DONE) { sentenceEnd = iteratorWindowEnd; } if (DBG) { if (sentenceStart != sentenceEnd) { Log.d(TAG, "Sentence detected [" + sentenceStart + ", " + sentenceEnd + ")."); } } if (sentenceEnd - sentenceStart <= MAX_SENTENCE_LENGTH) { // Add more sentences until the MAX_SENTENCE_LENGTH limitation is reached. while (sentenceEnd < textChangeEnd) { int nextEnd = mSentenceIterator.following(sentenceEnd); if (nextEnd == BreakIterator.DONE || nextEnd - sentenceStart > MAX_SENTENCE_LENGTH) { break; } sentenceEnd = nextEnd; } } else { // If the sentence containing `textChangeStart` is longer than MAX_SENTENCE_LENGTH, // the sentence will be sliced into sub-sentences of about MAX_SENTENCE_LENGTH // characters each. This is done by processing the unchecked part of that sentence : // [textChangeStart, sentenceEnd) // // - If the `uncheckedLength` is bigger than MAX_SENTENCE_LENGTH, then check the // [textChangeStart, textChangeStart + MAX_SENTENCE_LENGTH), and leave the rest // part for the next check. // // - If the `uncheckedLength` is smaller than or equal to MAX_SENTENCE_LENGTH, // then check [sentenceEnd - MAX_SENTENCE_LENGTH, sentenceEnd). // // The offset should be rounded up to word boundary. int uncheckedLength = sentenceEnd - textChangeStart; if (uncheckedLength > MAX_SENTENCE_LENGTH) { sentenceEnd = findSeparator(sequence, sentenceStart + MAX_SENTENCE_LENGTH, sentenceEnd); sentenceStart = roundUpToWordStart(sequence, textChangeStart, sentenceStart); } else { sentenceStart = roundUpToWordStart(sequence, sentenceEnd - MAX_SENTENCE_LENGTH, sentenceStart); } } return new Range(sentenceStart, sentenceEnd); } private int roundUpToWordStart(CharSequence sequence, int position, int frontBoundary) { if (isSeparator(sequence.charAt(position))) { return position; } int separator = findSeparator(sequence, position, frontBoundary); return separator != frontBoundary ? separator + 1 : frontBoundary; } /** * Search the range [start, end) of sequence and returns the position of the first separator. * If end is smaller than start, do a reverse search. * Returns `end` if no separator is found. */ private static int findSeparator(CharSequence sequence, int start, int end) { final int step = start < end ? 1 : -1; for (int i = start; i != end; i += step) { if (isSeparator(sequence.charAt(i))) { return i; } } return end; } public static boolean haveWordBoundariesChanged(final Editable editable, final int start, public static boolean haveWordBoundariesChanged(final Editable editable, final int start, final int end, final int spanStart, final int spanEnd) { final int end, final int spanStart, final int spanEnd) { final boolean haveWordBoundariesChanged; final boolean haveWordBoundariesChanged; Loading