001: /*******************************************************************************
002: * Copyright (c) 2000, 2006 IBM Corporation and others.
003: * All rights reserved. This program and the accompanying materials
004: * are made available under the terms of the Eclipse Public License v1.0
005: * which accompanies this distribution, and is available at
006: * http://www.eclipse.org/legal/epl-v10.html
007: *
008: * Contributors:
009: * Genady Beryozkin, me@genady.org - initial API and implementation
010: *******************************************************************************/package org.eclipse.ui.internal.texteditor;
011:
012: import java.util.ArrayList;
013: import java.util.Collections;
014: import java.util.HashSet;
015: import java.util.Iterator;
016: import java.util.List;
017: import java.util.regex.Matcher;
018: import java.util.regex.Pattern;
019:
020: import org.eclipse.jface.text.BadLocationException;
021: import org.eclipse.jface.text.FindReplaceDocumentAdapter;
022: import org.eclipse.jface.text.IDocument;
023: import org.eclipse.jface.text.IRegion;
024:
025: /**
026: * This class contains the hippie completion engine methods that actually
027: * compute the possible completions.
028: * <p>
029: * This engine is used by the <code>org.eclipse.ui.texteditor.HippieCompleteAction</code>.
030: * </p>
031: *
032: * TODO: Sort by editor type
033: * TODO: Provide history option
034: *
035: * @since 3.1
036: * @author Genady Beryozkin, me@genady.org
037: */
038: public final class HippieCompletionEngine {
039:
040: /**
041: * Regular expression that is used to find words.
042: */
043: // unicode identifier part
044: // private static final String COMPLETION_WORD_REGEX= "[\\p{L}[\\p{Mn}[\\p{Pc}[\\p{Nd}[\\p{Nl}]]]]]+"; //$NON-NLS-1$
045: // java identifier part (unicode id part + currency symbols)
046: private static final String COMPLETION_WORD_REGEX = "[\\p{L}[\\p{Mn}[\\p{Pc}[\\p{Nd}[\\p{Nl}[\\p{Sc}]]]]]]+"; //$NON-NLS-1$
047: /**
048: * The pre-compiled word pattern.
049: *
050: * @since 3.2
051: */
052: private static final Pattern COMPLETION_WORD_PATTERN = Pattern
053: .compile(COMPLETION_WORD_REGEX);
054:
055: /**
056: * Word boundary pattern that does not allow searching at the beginning of the document.
057: *
058: * @since 3.2
059: */
060: private static final String NON_EMPTY_COMPLETION_BOUNDARY = "[\\s\\p{Z}[\\p{P}&&[\\P{Pc}]][\\p{S}&&[\\P{Sc}]]]+"; //$NON-NLS-1$
061:
062: /**
063: * The word boundary pattern string.
064: *
065: * @since 3.2
066: */
067: private static final String COMPLETION_BOUNDARY = "(^|" + NON_EMPTY_COMPLETION_BOUNDARY + ")"; //$NON-NLS-1$ //$NON-NLS-2$
068: // with a 1.5 JRE, you can do this:
069: // private static final String COMPLETION_WORD_REGEX= "\\p{javaUnicodeIdentifierPart}+"; //$NON-NLS-1$
070: // private static final String COMPLETION_WORD_REGEX= "\\p{javaJavaIdentifierPart}+"; //$NON-NLS-1$
071:
072: /**
073: * Is completion case sensitive? Even if set to <code>false</code>, the
074: * case of the prefix won't be changed.
075: */
076: private static final boolean CASE_SENSITIVE = true;
077:
078: /**
079: * Creates a new engine.
080: */
081: public HippieCompletionEngine() {
082: }
083:
084: /*
085: * Copied from {@link FindReplaceDocumentAdapter#asRegPattern(java.lang.String)}.
086: */
087: /**
088: * Converts a non-regex string to a pattern that can be used with the regex
089: * search engine.
090: *
091: * @param string the non-regex pattern
092: * @return the string converted to a regex pattern
093: */
094: private String asRegPattern(CharSequence string) {
095: StringBuffer out = new StringBuffer(string.length());
096: boolean quoting = false;
097:
098: for (int i = 0, length = string.length(); i < length; i++) {
099: char ch = string.charAt(i);
100: if (ch == '\\') {
101: if (quoting) {
102: out.append("\\E"); //$NON-NLS-1$
103: quoting = false;
104: }
105: out.append("\\\\"); //$NON-NLS-1$
106: continue;
107: }
108: if (!quoting) {
109: out.append("\\Q"); //$NON-NLS-1$
110: quoting = true;
111: }
112: out.append(ch);
113: }
114: if (quoting)
115: out.append("\\E"); //$NON-NLS-1$
116:
117: return out.toString();
118: }
119:
120: /**
121: * Return the list of completion suggestions that correspond to the
122: * provided prefix.
123: *
124: * @param document the document to be scanned
125: * @param prefix the prefix to search for
126: * @param firstPosition the initial position in the document that
127: * the search will start from. In order to search from the
128: * beginning of the document use <code>firstPosition=0</code>.
129: * @param currentWordLast if <code>true</code> the word at caret position
130: * should be that last completion. <code>true</code> is good
131: * for searching in the currently open document and <code>false</code>
132: * is good for searching in other documents.
133: * @return a {@link List} of possible completions (as {@link String}s),
134: * excluding the common prefix
135: * @throws BadLocationException if there is some error scanning the
136: * document.
137: */
138: public List getCompletionsForward(IDocument document,
139: CharSequence prefix, int firstPosition,
140: boolean currentWordLast) throws BadLocationException {
141: ArrayList res = new ArrayList();
142: String currentWordCompletion = null; // fix bug 132533
143:
144: if (firstPosition == document.getLength()) {
145: return res;
146: }
147:
148: FindReplaceDocumentAdapter searcher = new FindReplaceDocumentAdapter(
149: document);
150:
151: // search only at word boundaries
152: String searchPattern;
153:
154: // unless we are at the beginning of the document, the completion boundary
155: // matches one character. It is enough to move just one character backwards
156: // because the boundary pattern has the (....)+ form.
157: // see HippieCompletionTest#testForwardSearch().
158: if (firstPosition > 0) {
159: firstPosition--;
160: // empty spacing is not permitted now.
161: searchPattern = NON_EMPTY_COMPLETION_BOUNDARY
162: + asRegPattern(prefix);
163: } else {
164: searchPattern = COMPLETION_BOUNDARY + asRegPattern(prefix);
165: }
166:
167: IRegion reg = searcher.find(firstPosition, searchPattern, true,
168: CASE_SENSITIVE, false, true);
169: while (reg != null) {
170: // since the boundary may be of nonzero length
171: int wordSearchPos = reg.getOffset() + reg.getLength()
172: - prefix.length();
173: // try to complete to a word. case is irrelevant here.
174: IRegion word = searcher.find(wordSearchPos,
175: COMPLETION_WORD_REGEX, true, true, false, true);
176: if (word.getLength() > prefix.length()) { // empty suggestion will be added later
177: String wholeWord = document.get(word.getOffset(), word
178: .getLength());
179: String completion = wholeWord
180: .substring(prefix.length());
181: if (currentWordLast && reg.getOffset() == firstPosition) { // we got the word at caret as completion
182: currentWordCompletion = completion; // add it as the last word.
183: } else {
184: res.add(completion);
185: }
186: }
187: int nextPos = word.getOffset() + word.getLength();
188: if (nextPos >= document.getLength()) {
189: break;
190: }
191: reg = searcher.find(nextPos, searchPattern, true,
192: CASE_SENSITIVE, false, true);
193: }
194:
195: // the word at caret position goes last (bug 132533).
196: if (currentWordCompletion != null) {
197: res.add(currentWordCompletion);
198: }
199:
200: return res;
201: }
202:
203: /**
204: * Search for possible completions in the backward direction. If there
205: * is a possible completion that begins before <code>firstPosition</code>
206: * but ends after that position, it will not be included in the results.
207: *
208: * @param document the document to be scanned
209: * @param prefix the completion prefix
210: * @param firstPosition the caret position
211: * @return a {@link List} of possible completions ({@link String}s)
212: * from the caret position to the beginning of the document.
213: * The empty suggestion is not included in the results.
214: * @throws BadLocationException if any error occurs
215: */
216: public List getCompletionsBackwards(IDocument document,
217: CharSequence prefix, int firstPosition)
218: throws BadLocationException {
219: ArrayList res = new ArrayList();
220:
221: // FindReplaceDocumentAdapter expects the start offset to be before the
222: // actual caret position, probably for compatibility with forward search.
223: if (firstPosition == 0) {
224: return res;
225: }
226:
227: FindReplaceDocumentAdapter searcher = new FindReplaceDocumentAdapter(
228: document);
229:
230: // search only at word boundaries
231: String searchPattern = COMPLETION_BOUNDARY
232: + asRegPattern(prefix);
233:
234: IRegion reg = searcher.find(0, searchPattern, true,
235: CASE_SENSITIVE, false, true);
236: while (reg != null) {
237: // since the boundary may be of nonzero length
238: int wordSearchPos = reg.getOffset() + reg.getLength()
239: - prefix.length();
240: // try to complete to a word. case is of no matter here
241: IRegion word = searcher.find(wordSearchPos,
242: COMPLETION_WORD_REGEX, true, true, false, true);
243: if (word.getOffset() + word.getLength() > firstPosition) {
244: break;
245: }
246: if (word.getLength() > prefix.length()) { // empty suggestion will be added later
247: String found = document.get(word.getOffset(), word
248: .getLength());
249: res.add(found.substring(prefix.length()));
250: }
251: int nextPos = word.getOffset() + word.getLength();
252: if (nextPos >= firstPosition) { // for efficiency only
253: break;
254: }
255: reg = searcher.find(nextPos, searchPattern, true,
256: CASE_SENSITIVE, false, true);
257: }
258: Collections.reverse(res);
259:
260: return res;
261: }
262:
263: /**
264: * Returns the text between the provided position and the preceding word boundary.
265: *
266: * @param doc the document that will be scanned.
267: * @param pos the caret position.
268: * @return the text if found, or null.
269: * @throws BadLocationException if an error occurs.
270: * @since 3.2
271: */
272: public String getPrefixString(IDocument doc, int pos)
273: throws BadLocationException {
274: Matcher m = COMPLETION_WORD_PATTERN.matcher(""); //$NON-NLS-1$
275: int prevNonAlpha = pos;
276: while (prevNonAlpha > 0) {
277: m.reset(doc.get(prevNonAlpha - 1, pos - prevNonAlpha + 1));
278: if (!m.matches()) {
279: break;
280: }
281: prevNonAlpha--;
282: }
283: if (prevNonAlpha != pos) {
284: return doc.get(prevNonAlpha, pos - prevNonAlpha);
285: }
286: return null;
287: }
288:
289: /**
290: * Remove duplicate suggestions (excluding the prefix), leaving the closest
291: * to list head.
292: *
293: * @param suggestions a list of suggestions ({@link String}).
294: * @return a list of unique completion suggestions.
295: */
296: public List makeUnique(List suggestions) {
297: HashSet seenAlready = new HashSet();
298: ArrayList uniqueSuggestions = new ArrayList();
299:
300: for (Iterator i = suggestions.iterator(); i.hasNext();) {
301: String suggestion = (String) i.next();
302: if (!seenAlready.contains(suggestion)) {
303: seenAlready.add(suggestion);
304: uniqueSuggestions.add(suggestion);
305: }
306: }
307: return uniqueSuggestions;
308: }
309: }
|