EditingUtils.java revision 6e5ca890504f96844ba2585be4a3b50786213228
1/*
2 * Copyright (C) 2009 The Android Open Source Project
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 android.view.inputmethod.ExtractedText;
20import android.view.inputmethod.ExtractedTextRequest;
21import android.view.inputmethod.InputConnection;
22
23import java.util.regex.Pattern;
24
25/**
26 * Utility methods to deal with editing text through an InputConnection.
27 */
28public class EditingUtils {
29    /**
30     * Number of characters we want to look back in order to identify the previous word
31     */
32    // Provision for a long word pair and a separator
33    private static final int LOOKBACK_CHARACTER_NUM = BinaryDictionary.MAX_WORD_LENGTH * 2 + 1;
34    private static final int INVALID_CURSOR_POSITION = -1;
35
36    private EditingUtils() {
37        // Unintentional empty constructor for singleton.
38    }
39
40    private static int getCursorPosition(InputConnection connection) {
41        if (null == connection) return INVALID_CURSOR_POSITION;
42        final ExtractedText extracted = connection.getExtractedText(new ExtractedTextRequest(), 0);
43        if (extracted == null) {
44            return INVALID_CURSOR_POSITION;
45        }
46        return extracted.startOffset + extracted.selectionStart;
47    }
48
49    /**
50     * @param connection connection to the current text field.
51     * @param separators characters which may separate words
52     * @return the word that surrounds the cursor, including up to one trailing
53     *   separator. For example, if the field contains "he|llo world", where |
54     *   represents the cursor, then "hello " will be returned.
55     */
56    public static String getWordAtCursor(InputConnection connection, String separators) {
57        // getWordRangeAtCursor returns null if the connection is null
58        Range r = getWordRangeAtCursor(connection, separators);
59        return (r == null) ? null : r.mWord;
60    }
61
62    /**
63     * Represents a range of text, relative to the current cursor position.
64     */
65    public static class Range {
66        /** Characters before selection start */
67        public final int mCharsBefore;
68
69        /**
70         * Characters after selection start, including one trailing word
71         * separator.
72         */
73        public final int mCharsAfter;
74
75        /** The actual characters that make up a word */
76        public final String mWord;
77
78        public Range(int charsBefore, int charsAfter, String word) {
79            if (charsBefore < 0 || charsAfter < 0) {
80                throw new IndexOutOfBoundsException();
81            }
82            this.mCharsBefore = charsBefore;
83            this.mCharsAfter = charsAfter;
84            this.mWord = word;
85        }
86    }
87
88    private static Range getWordRangeAtCursor(InputConnection connection, String sep) {
89        if (connection == null || sep == null) {
90            return null;
91        }
92        CharSequence before = connection.getTextBeforeCursor(1000, 0);
93        CharSequence after = connection.getTextAfterCursor(1000, 0);
94        if (before == null || after == null) {
95            return null;
96        }
97
98        // Find first word separator before the cursor
99        int start = before.length();
100        while (start > 0 && !isWhitespace(before.charAt(start - 1), sep)) start--;
101
102        // Find last word separator after the cursor
103        int end = -1;
104        while (++end < after.length() && !isWhitespace(after.charAt(end), sep)) {
105            // Nothing to do here.
106        }
107
108        int cursor = getCursorPosition(connection);
109        if (start >= 0 && cursor + end <= after.length() + before.length()) {
110            String word = before.toString().substring(start, before.length())
111                    + after.toString().substring(0, end);
112            return new Range(before.length() - start, end, word);
113        }
114
115        return null;
116    }
117
118    private static boolean isWhitespace(int code, String whitespace) {
119        return whitespace.contains(String.valueOf((char) code));
120    }
121
122    private static final Pattern spaceRegex = Pattern.compile("\\s+");
123
124    public static CharSequence getPreviousWord(InputConnection connection,
125            String sentenceSeperators) {
126        //TODO: Should fix this. This could be slow!
127        if (null == connection) return null;
128        CharSequence prev = connection.getTextBeforeCursor(LOOKBACK_CHARACTER_NUM, 0);
129        return getPreviousWord(prev, sentenceSeperators);
130    }
131
132    // Get the word before the whitespace preceding the non-whitespace preceding the cursor.
133    // Also, it won't return words that end in a separator.
134    // Example :
135    // "abc def|" -> abc
136    // "abc def |" -> abc
137    // "abc def. |" -> abc
138    // "abc def . |" -> def
139    // "abc|" -> null
140    // "abc |" -> null
141    // "abc. def|" -> null
142    public static CharSequence getPreviousWord(CharSequence prev, String sentenceSeperators) {
143        if (prev == null) return null;
144        String[] w = spaceRegex.split(prev);
145
146        // If we can't find two words, or we found an empty word, return null.
147        if (w.length < 2 || w[w.length - 2].length() <= 0) return null;
148
149        // If ends in a separator, return null
150        char lastChar = w[w.length - 2].charAt(w[w.length - 2].length() - 1);
151        if (sentenceSeperators.contains(String.valueOf(lastChar))) return null;
152
153        return w[w.length - 2];
154    }
155
156    public static CharSequence getThisWord(InputConnection connection, String sentenceSeperators) {
157        if (null == connection) return null;
158        final CharSequence prev = connection.getTextBeforeCursor(LOOKBACK_CHARACTER_NUM, 0);
159        return getThisWord(prev, sentenceSeperators);
160    }
161
162    // Get the word immediately before the cursor, even if there is whitespace between it and
163    // the cursor - but not if there is punctuation.
164    // Example :
165    // "abc def|" -> def
166    // "abc def |" -> def
167    // "abc def. |" -> null
168    // "abc def . |" -> null
169    public static CharSequence getThisWord(CharSequence prev, String sentenceSeperators) {
170        if (prev == null) return null;
171        String[] w = spaceRegex.split(prev);
172
173        // No word : return null
174        if (w.length < 1 || w[w.length - 1].length() <= 0) return null;
175
176        // If ends in a separator, return null
177        char lastChar = w[w.length - 1].charAt(w[w.length - 1].length() - 1);
178        if (sentenceSeperators.contains(String.valueOf(lastChar))) return null;
179
180        return w[w.length - 1];
181    }
182}
183