EditingUtils.java revision 0c8d5ca023d54b7c9ef6c20eb7988288132bacb5
1/*
2 * Copyright (C) 2009 Google Inc.
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License"); you may not
5 * use this file except in compliance with the License. You may obtain a copy of
6 * the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13 * License for the specific language governing permissions and limitations under
14 * the License.
15 */
16
17package com.android.inputmethod.latin;
18
19import com.android.inputmethod.compat.InputConnectionCompatUtils;
20
21import android.text.TextUtils;
22import android.view.inputmethod.ExtractedText;
23import android.view.inputmethod.ExtractedTextRequest;
24import android.view.inputmethod.InputConnection;
25
26import java.util.regex.Pattern;
27
28/**
29 * Utility methods to deal with editing text through an InputConnection.
30 */
31public class EditingUtils {
32    /**
33     * Number of characters we want to look back in order to identify the previous word
34     */
35    private static final int LOOKBACK_CHARACTER_NUM = 15;
36
37    private EditingUtils() {
38        // Unintentional empty constructor for singleton.
39    }
40
41    /**
42     * Append newText to the text field represented by connection.
43     * The new text becomes selected.
44     */
45    public static void appendText(InputConnection connection, String newText) {
46        if (connection == null) {
47            return;
48        }
49
50        // Commit the composing text
51        connection.finishComposingText();
52
53        // Add a space if the field already has text.
54        String text = newText;
55        CharSequence charBeforeCursor = connection.getTextBeforeCursor(1, 0);
56        if (charBeforeCursor != null
57                && !charBeforeCursor.equals(" ")
58                && (charBeforeCursor.length() > 0)) {
59            text = " " + text;
60        }
61
62        connection.setComposingText(text, 1);
63    }
64
65    private static int getCursorPosition(InputConnection connection) {
66        ExtractedText extracted = connection.getExtractedText(
67            new ExtractedTextRequest(), 0);
68        if (extracted == null) {
69          return -1;
70        }
71        return extracted.startOffset + extracted.selectionStart;
72    }
73
74    /**
75     * @param connection connection to the current text field.
76     * @param separators characters which may separate words
77     * @return the word that surrounds the cursor, including up to one trailing
78     *   separator. For example, if the field contains "he|llo world", where |
79     *   represents the cursor, then "hello " will be returned.
80     */
81    public static String getWordAtCursor(InputConnection connection, String separators) {
82        Range r = getWordRangeAtCursor(connection, separators);
83        return (r == null) ? null : r.mWord;
84    }
85
86    /**
87     * Removes the word surrounding the cursor. Parameters are identical to
88     * getWordAtCursor.
89     */
90    public static void deleteWordAtCursor(InputConnection connection, String separators) {
91        Range range = getWordRangeAtCursor(connection, separators);
92        if (range == null) return;
93
94        connection.finishComposingText();
95        // Move cursor to beginning of word, to avoid crash when cursor is outside
96        // of valid range after deleting text.
97        int newCursor = getCursorPosition(connection) - range.mCharsBefore;
98        connection.setSelection(newCursor, newCursor);
99        connection.deleteSurroundingText(0, range.mCharsBefore + range.mCharsAfter);
100    }
101
102    /**
103     * Represents a range of text, relative to the current cursor position.
104     */
105    public static class Range {
106        /** Characters before selection start */
107        public final int mCharsBefore;
108
109        /**
110         * Characters after selection start, including one trailing word
111         * separator.
112         */
113        public final int mCharsAfter;
114
115        /** The actual characters that make up a word */
116        public final String mWord;
117
118        public Range(int charsBefore, int charsAfter, String word) {
119            if (charsBefore < 0 || charsAfter < 0) {
120                throw new IndexOutOfBoundsException();
121            }
122            this.mCharsBefore = charsBefore;
123            this.mCharsAfter = charsAfter;
124            this.mWord = word;
125        }
126    }
127
128    private static Range getWordRangeAtCursor(InputConnection connection, String sep) {
129        if (connection == null || sep == null) {
130            return null;
131        }
132        CharSequence before = connection.getTextBeforeCursor(1000, 0);
133        CharSequence after = connection.getTextAfterCursor(1000, 0);
134        if (before == null || after == null) {
135            return null;
136        }
137
138        // Find first word separator before the cursor
139        int start = before.length();
140        while (start > 0 && !isWhitespace(before.charAt(start - 1), sep)) start--;
141
142        // Find last word separator after the cursor
143        int end = -1;
144        while (++end < after.length() && !isWhitespace(after.charAt(end), sep)) {
145            // Nothing to do here.
146        }
147
148        int cursor = getCursorPosition(connection);
149        if (start >= 0 && cursor + end <= after.length() + before.length()) {
150            String word = before.toString().substring(start, before.length())
151                    + after.toString().substring(0, end);
152            return new Range(before.length() - start, end, word);
153        }
154
155        return null;
156    }
157
158    private static boolean isWhitespace(int code, String whitespace) {
159        return whitespace.contains(String.valueOf((char) code));
160    }
161
162    private static final Pattern spaceRegex = Pattern.compile("\\s+");
163
164
165    public static CharSequence getPreviousWord(InputConnection connection,
166            String sentenceSeperators) {
167        //TODO: Should fix this. This could be slow!
168        CharSequence prev = connection.getTextBeforeCursor(LOOKBACK_CHARACTER_NUM, 0);
169        return getPreviousWord(prev, sentenceSeperators);
170    }
171
172    // Get the word before the whitespace preceding the non-whitespace preceding the cursor.
173    // Also, it won't return words that end in a separator.
174    // Example :
175    // "abc def|" -> abc
176    // "abc def |" -> abc
177    // "abc def. |" -> abc
178    // "abc def . |" -> def
179    // "abc|" -> null
180    // "abc |" -> null
181    // "abc. def|" -> null
182    public static CharSequence getPreviousWord(CharSequence prev, String sentenceSeperators) {
183        if (prev == null) return null;
184        String[] w = spaceRegex.split(prev);
185
186        // If we can't find two words, or we found an empty word, return null.
187        if (w.length < 2 || w[w.length - 2].length() <= 0) return null;
188
189        // If ends in a separator, return null
190        char lastChar = w[w.length - 2].charAt(w[w.length - 2].length() - 1);
191        if (sentenceSeperators.contains(String.valueOf(lastChar))) return null;
192
193        return w[w.length - 2];
194    }
195
196    public static CharSequence getThisWord(InputConnection connection, String sentenceSeperators) {
197        final CharSequence prev = connection.getTextBeforeCursor(LOOKBACK_CHARACTER_NUM, 0);
198        return getThisWord(prev, sentenceSeperators);
199    }
200
201    // Get the word immediately before the cursor, even if there is whitespace between it and
202    // the cursor - but not if there is punctuation.
203    // Example :
204    // "abc def|" -> def
205    // "abc def |" -> def
206    // "abc def. |" -> null
207    // "abc def . |" -> null
208    public static CharSequence getThisWord(CharSequence prev, String sentenceSeperators) {
209        if (prev == null) return null;
210        String[] w = spaceRegex.split(prev);
211
212        // No word : return null
213        if (w.length < 1 || w[w.length - 1].length() <= 0) return null;
214
215        // If ends in a separator, return null
216        char lastChar = w[w.length - 1].charAt(w[w.length - 1].length() - 1);
217        if (sentenceSeperators.contains(String.valueOf(lastChar))) return null;
218
219        return w[w.length - 1];
220    }
221
222    public static class SelectedWord {
223        public final int mStart;
224        public final int mEnd;
225        public final CharSequence mWord;
226
227        public SelectedWord(int start, int end, CharSequence word) {
228            mStart = start;
229            mEnd = end;
230            mWord = word;
231        }
232    }
233
234    /**
235     * Takes a character sequence with a single character and checks if the character occurs
236     * in a list of word separators or is empty.
237     * @param singleChar A CharSequence with null, zero or one character
238     * @param wordSeparators A String containing the word separators
239     * @return true if the character is at a word boundary, false otherwise
240     */
241    private static boolean isWordBoundary(CharSequence singleChar, String wordSeparators) {
242        return TextUtils.isEmpty(singleChar) || wordSeparators.contains(singleChar);
243    }
244
245    /**
246     * Checks if the cursor is inside a word or the current selection is a whole word.
247     * @param ic the InputConnection for accessing the text field
248     * @param selStart the start position of the selection within the text field
249     * @param selEnd the end position of the selection within the text field. This could be
250     *               the same as selStart, if there's no selection.
251     * @param wordSeparators the word separator characters for the current language
252     * @return an object containing the text and coordinates of the selected/touching word,
253     *         null if the selection/cursor is not marking a whole word.
254     */
255    public static SelectedWord getWordAtCursorOrSelection(final InputConnection ic,
256            int selStart, int selEnd, String wordSeparators) {
257        if (selStart == selEnd) {
258            // There is just a cursor, so get the word at the cursor
259            EditingUtils.Range range = getWordRangeAtCursor(ic, wordSeparators);
260            if (range != null && !TextUtils.isEmpty(range.mWord)) {
261                return new SelectedWord(selStart - range.mCharsBefore, selEnd + range.mCharsAfter,
262                        range.mWord);
263            }
264        } else {
265            // Is the previous character empty or a word separator? If not, return null.
266            CharSequence charsBefore = ic.getTextBeforeCursor(1, 0);
267            if (!isWordBoundary(charsBefore, wordSeparators)) {
268                return null;
269            }
270
271            // Is the next character empty or a word separator? If not, return null.
272            CharSequence charsAfter = ic.getTextAfterCursor(1, 0);
273            if (!isWordBoundary(charsAfter, wordSeparators)) {
274                return null;
275            }
276
277            // Extract the selection alone
278            CharSequence touching = InputConnectionCompatUtils.getSelectedText(
279                    ic, selStart, selEnd);
280            if (TextUtils.isEmpty(touching)) return null;
281            // Is any part of the selection a separator? If so, return null.
282            final int length = touching.length();
283            for (int i = 0; i < length; i++) {
284                if (wordSeparators.contains(touching.subSequence(i, i + 1))) {
285                    return null;
286                }
287            }
288            // Prepare the selected word
289            return new SelectedWord(selStart, selEnd, touching);
290        }
291        return null;
292    }
293}
294