/******************************************************************************* * Copyright (c) 2000, 2010 IBM Corporation and others. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at * http://www.eclipse.org/legal/epl-v10.html * * Contributors: * Genady Beryozkin, me@genady.org - initial API and implementation * Fabio Zadrozny <fabiofz at gmail dot com> - [typing] HippieCompleteAction is slow ( Alt+/ ) - https://bugs.eclipse.org/bugs/show_bug.cgi?id=270385 *******************************************************************************/ package org.eclipse.ui.internal.texteditor; import java.util.ArrayList; import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.NoSuchElementException; import java.util.regex.Matcher; import java.util.regex.Pattern; import org.eclipse.core.runtime.Assert; import org.eclipse.core.runtime.IStatus; import org.eclipse.core.runtime.Status; import org.eclipse.jface.text.BadLocationException; import org.eclipse.jface.text.FindReplaceDocumentAdapter; import org.eclipse.jface.text.IDocument; import org.eclipse.jface.text.IRegion; import org.eclipse.ui.IEditorPart; import org.eclipse.ui.IEditorReference; import org.eclipse.ui.IWorkbenchWindow; import org.eclipse.ui.texteditor.IDocumentProvider; import org.eclipse.ui.texteditor.ITextEditor; /** * This class contains the hippie completion engine methods that actually * compute the possible completions. * <p> * This engine is used by the <code>org.eclipse.ui.texteditor.HippieCompleteAction</code>. * </p> * * TODO: Sort by editor type * TODO: Provide history option * * @since 3.1 * @author Genady Beryozkin, me@genady.org */ public final class HippieCompletionEngine { /** * Regular expression that is used to find words. */ // unicode identifier part // private static final String COMPLETION_WORD_REGEX= "[\\p{L}[\\p{Mn}[\\p{Pc}[\\p{Nd}[\\p{Nl}]]]]]+"; //$NON-NLS-1$ // java identifier part (unicode id part + currency symbols) private static final String COMPLETION_WORD_REGEX= "[\\p{L}[\\p{Mn}[\\p{Pc}[\\p{Nd}[\\p{Nl}[\\p{Sc}]]]]]]+"; //$NON-NLS-1$ /** * The pre-compiled word pattern. * * @since 3.2 */ private static final Pattern COMPLETION_WORD_PATTERN= Pattern.compile(COMPLETION_WORD_REGEX); /** * Word boundary pattern that does not allow searching at the beginning of the document. * * @since 3.2 */ private static final String NON_EMPTY_COMPLETION_BOUNDARY= "[\\s\\p{Z}[\\p{P}&&[\\P{Pc}]][\\p{S}&&[\\P{Sc}]]]+"; //$NON-NLS-1$ /** * The word boundary pattern string. * * @since 3.2 */ private static final String COMPLETION_BOUNDARY= "(^|" + NON_EMPTY_COMPLETION_BOUNDARY + ")"; //$NON-NLS-1$ //$NON-NLS-2$ // with a 1.5 JRE, you can do this: // private static final String COMPLETION_WORD_REGEX= "\\p{javaUnicodeIdentifierPart}+"; //$NON-NLS-1$ // private static final String COMPLETION_WORD_REGEX= "\\p{javaJavaIdentifierPart}+"; //$NON-NLS-1$ /** * Is completion case sensitive? Even if set to <code>false</code>, the * case of the prefix won't be changed. */ private static final boolean CASE_SENSITIVE= true; /** * Creates a new engine. */ public HippieCompletionEngine() { } /* * Copied from {@link FindReplaceDocumentAdapter#asRegPattern(java.lang.String)}. */ /** * Converts a non-regex string to a pattern that can be used with the regex * search engine. * * @param string the non-regex pattern * @return the string converted to a regex pattern */ private String asRegPattern(CharSequence string) { StringBuffer out= new StringBuffer(string.length()); boolean quoting= false; for (int i= 0, length= string.length(); i < length; i++) { char ch= string.charAt(i); if (ch == '\\') { if (quoting) { out.append("\\E"); //$NON-NLS-1$ quoting= false; } out.append("\\\\"); //$NON-NLS-1$ continue; } if (!quoting) { out.append("\\Q"); //$NON-NLS-1$ quoting= true; } out.append(ch); } if (quoting) out.append("\\E"); //$NON-NLS-1$ return out.toString(); } /** * Return the list of completion suggestions that correspond to the * provided prefix. * * @param document the document to be scanned * @param prefix the prefix to search for * @param firstPosition the initial position in the document that * the search will start from. In order to search from the * beginning of the document use <code>firstPosition=0</code>. * @param currentWordLast if <code>true</code> the word at caret position * should be that last completion. <code>true</code> is good * for searching in the currently open document and <code>false</code> * is good for searching in other documents. * @return a {@link List} of possible completions (as {@link String}s), * excluding the common prefix * @throws BadLocationException if there is some error scanning the * document. */ public List getCompletionsForward(IDocument document, CharSequence prefix, int firstPosition, boolean currentWordLast) throws BadLocationException { ArrayList res= new ArrayList(); for (Iterator it= getForwardIterator(document, prefix, firstPosition, currentWordLast); it.hasNext();) { res.add(it.next()); } return res; } /** * Search for possible completions in the backward direction. If there * is a possible completion that begins before <code>firstPosition</code> * but ends after that position, it will not be included in the results. * * @param document the document to be scanned * @param prefix the completion prefix * @param firstPosition the caret position * @return a {@link List} of possible completions ({@link String}s) * from the caret position to the beginning of the document. * The empty suggestion is not included in the results. * @throws BadLocationException if any error occurs */ public List getCompletionsBackwards(IDocument document, CharSequence prefix, int firstPosition) throws BadLocationException { ArrayList res= new ArrayList(); for (Iterator it= getBackwardIterator(document, prefix, firstPosition); it.hasNext();) { res.add(it.next()); } return res; } /** * Returns the text between the provided position and the preceding word boundary. * * @param doc the document that will be scanned. * @param pos the caret position. * @return the text if found, or null. * @throws BadLocationException if an error occurs. * @since 3.2 */ public String getPrefixString(IDocument doc, int pos) throws BadLocationException { Matcher m= COMPLETION_WORD_PATTERN.matcher(""); //$NON-NLS-1$ int prevNonAlpha= pos; while (prevNonAlpha > 0) { m.reset(doc.get(prevNonAlpha-1, pos - prevNonAlpha + 1)); if (!m.matches()) { break; } prevNonAlpha--; } if (prevNonAlpha != pos) { return doc.get(prevNonAlpha, pos - prevNonAlpha); } return null; } /** * Remove duplicate suggestions (excluding the prefix), leaving the closest * to list head. * * @param suggestions a list of suggestions ({@link String}). * @return a list of unique completion suggestions. */ public List makeUnique(List suggestions) { HashSet seenAlready= new HashSet(); ArrayList uniqueSuggestions= new ArrayList(); for (Iterator i= suggestions.iterator(); i.hasNext();) { String suggestion= (String) i.next(); if (!seenAlready.contains(suggestion)) { seenAlready.add(suggestion); uniqueSuggestions.add(suggestion); } } return uniqueSuggestions; } /** * Calculates the documents to be searched. Note that the first returned document is always from * the current editor and if we have no current editor, an empty list is returned even if there * are other documents available. * * @param currentTextEditor this is the currently opened text editor. * @return A List of IDocument with the opened documents so that the first document in that list * is always the current document. * @since 3.6 */ public static List computeDocuments(ITextEditor currentTextEditor) { ArrayList documentsForSearch= new ArrayList(); if (currentTextEditor == null) { return documentsForSearch; } IDocumentProvider provider= currentTextEditor.getDocumentProvider(); if (provider == null) { return documentsForSearch; } IDocument currentDocument= provider.getDocument(currentTextEditor.getEditorInput()); if(currentDocument == null){ return documentsForSearch; } List computedDocuments= new ArrayList(); IWorkbenchWindow window= currentTextEditor.getSite().getWorkbenchWindow(); IEditorReference editorsArray[]= window.getActivePage().getEditorReferences(); for (int i= 0; i < editorsArray.length; i++) { IEditorPart realEditor= editorsArray[i].getEditor(false); if (realEditor instanceof ITextEditor && !realEditor.equals(currentTextEditor)) { ITextEditor textEditor= (ITextEditor)realEditor; provider= textEditor.getDocumentProvider(); if (provider == null) { continue; } IDocument doc= provider.getDocument(textEditor.getEditorInput()); if (doc == null) { continue; } computedDocuments.add(doc); } } //The first is always the one related to the passed currentTextEditor. computedDocuments.add(0, currentDocument); return computedDocuments; } /** * Provides an iterator that will get the completions that start with the passed prefix after * the passed position (forward until the end of the document). * * @param document the document to be scanned * @param prefix the prefix to search for * @param firstPosition the initial position in the document that the search will start from. In * order to search from the beginning of the document use * <code>firstPosition=0</code>. * @param currentWordLast if <code>true</code> the word at caret position should be that last * completion. <code>true</code> is good for searching in the currently open document * and <code>false</code> is good for searching in other documents. * @return Iterator (for Strings) that will get the completions forward from the passed * position. * * @since 3.6 */ public Iterator getForwardIterator(IDocument document, CharSequence prefix, int firstPosition, boolean currentWordLast) { return new HippieCompletionForwardIterator(document, prefix, firstPosition, currentWordLast); } /** * Provides an iterator that will get the completions that start with the passed prefix before * the passed position (backwards until the start of the document). * * @param document the document to be scanned * @param prefix the prefix to search for * @param firstPosition the initial position in the document that the search will start from. In * order to search from the end of the document use * <code>firstPosition=document.getLength()</code>. * @return Iterator that will get the completions backward from the passed position. * * @since 3.6 */ public Iterator getBackwardIterator(IDocument document, CharSequence prefix, int firstPosition) { return new HippieCompletionBackwardIterator(document, prefix, firstPosition); } /** * Provides an iterator that will get the completions for all the documents received, starting * at the "document" passed (first going backward and then forward from the position passed) and * later going forward through each of the "otherDocuments". * * @param document the document to be scanned * @param otherDocuments the additional documents to be scanned * @param prefix the prefix to search for * @param firstPosition the initial position in the document that the search will start from. * @return Iterator that will first get the completions backward from the document passed, then * forward in that same document and when that is finished it will get it forward for * the other documents (in the same sequence the documents are available). * * @since 3.6 */ public Iterator getMultipleDocumentsIterator(IDocument document, List otherDocuments, CharSequence prefix, int firstPosition) { return new MultipleDocumentsIterator(document, otherDocuments, prefix, firstPosition); } /** * Class that keeps the state while iterating the suggestions * * @since 3.6 */ private final class MultipleDocumentsIterator implements Iterator { /** * This is the next token to be returned (when null, no more tokens should be returned) */ private String fNext; /** * -1 means that we still haven't checked the current do completions Any other number means * that we'll get the completions for some other editor. */ private int fCurrLocation= -1; /** These are the suggestions which we already loaded. */ private final List fSuggestions; /** This marks the current suggestion to be returned */ private int fCurrSuggestion= 0; /** This is the prefix that should be searched */ private final CharSequence fPrefix; /** The list of IDocuments that we should search */ private final List fOtherDocuments; /** * The document that's currently opened (that's the 1st we should look and we should 1st * search backwards from the current offset and later forwards) */ private final IDocument fOpenDocument; /** The current offset in the opened document */ private final int fSelectionOffset; /** Indicates whether we already added the empty completion. */ private boolean fAddedEmpty= false; /** The 'current' forward iterator. */ private Iterator fCompletionsForwardIterator; /** The 'current' backward iterator. */ private Iterator fCompletionsBackwardIterator; /* * (non-Javadoc) * @see HippieCompletionEngine#getMultipleDocumentsIterator(IDocument, List, CharSequence, int) */ private MultipleDocumentsIterator(IDocument openDocument, List otherDocuments, CharSequence prefix, int selectionOffset) { this.fPrefix= prefix; this.fSuggestions= new ArrayList(); this.fOtherDocuments= otherDocuments; this.fSelectionOffset= selectionOffset; this.fOpenDocument= openDocument; calculateNext(); } /** * This method calculates the next token to be returned (so, after creating the class or * after calling next(), this function must be called). * * It'll check which document should be used and will get the completions on that document * until some completion is found. * * An empty completion is always added at the end. * * After the empty completion, the next is set to null. */ private void calculateNext() { if (fCurrLocation == -1) { fCompletionsBackwardIterator= getBackwardIterator( fOpenDocument, fPrefix, fSelectionOffset); fCompletionsForwardIterator= getForwardIterator( fOpenDocument, fPrefix, (fSelectionOffset - fPrefix.length()), true); fCurrLocation++; } if (checkNext()) { return; } while (fCurrLocation < this.fOtherDocuments.size()) { fCompletionsForwardIterator= getForwardIterator( ((IDocument)this.fOtherDocuments.get(fCurrLocation)), fPrefix, 0, false); fCurrLocation++; if (checkNext()) { return; } } // add the empty suggestion (last one) if (!fAddedEmpty) { fSuggestions.add(""); //$NON-NLS-1$ fAddedEmpty= true; } checkNext(); } /** * @return true if a completion was found and false if it couldn't be found -- in which case * the next is set to null. */ private boolean checkNext() { if (fCompletionsBackwardIterator != null) { if (fCompletionsBackwardIterator.hasNext()) { fSuggestions.add(fCompletionsBackwardIterator.next()); } else { fCompletionsBackwardIterator= null; } } if (fCompletionsBackwardIterator == null) { //only get if backward completions are consumed if (fCompletionsForwardIterator != null && fCompletionsForwardIterator.hasNext()) { fSuggestions.add(fCompletionsForwardIterator.next()); } } if (fSuggestions.size() > fCurrSuggestion) { fNext= (String)fSuggestions.get(fCurrSuggestion); fCurrSuggestion++; return true; } fNext= null; return false; } /** * We always calculate the next to see if it's available... * * @return true if the next token to be returned is not null (we always pre-calculate * things) */ public boolean hasNext() { return fNext != null; } /** * @return the next suggestion */ public Object next() { if (fNext == null) { throw new NoSuchElementException("No more elements to iterate"); //$NON-NLS-1$ } Object ret= fNext; calculateNext(); return ret; } /** * Not supported! * * @throws UnsupportedOperationException always. */ public void remove() { throw new UnsupportedOperationException("Not supported"); //$NON-NLS-1$ } } /** * Base class for Iterator that gets the word completions in a document, and returns them one by * one (lazily gotten). * * @since 3.6 */ private abstract class HippieCompletionIterator implements Iterator { /** The document to be scanned */ protected IDocument fDocument; /** The prefix to search for */ protected CharSequence fPrefix; /** * The initial position in the document that the search will start from. In order to search * from the beginning of the document use <code>firstPosition=0</code>. */ protected int fFirstPosition; /** Determines if we have a next element to be returned. */ protected boolean fHasNext; /** The next element to be returned */ protected String fNext; /** The current state for the iterator */ protected int fCurrentState= 0; /** The class that'll do the search */ protected FindReplaceDocumentAdapter fSearcher; /** Pattern to be used -- search only at word boundaries */ protected String fSearchPattern; /** The next place to search for */ protected int fNextPos; /** * Constructor * * @param document the document to be scanned * @param prefix the prefix to search for * @param firstPosition the initial position in the document that the search will start * from. In order to search from the beginning of the document use * <code>firstPosition=0</code>. */ public HippieCompletionIterator(IDocument document, CharSequence prefix, int firstPosition) { this.fDocument= document; this.fPrefix= prefix; this.fFirstPosition= firstPosition; } /** * Must be called to calculate the first completion (subclasses must explicitly call it when * properly initialized). */ protected void calculateFirst() { try { calculateNext(); } catch (BadLocationException e) { log(e); fHasNext= false; fNext= null; } } /* * (non-Javadoc) * @see java.util.Iterator#hasNext() */ public boolean hasNext() { return fHasNext; } /* * (non-Javadoc) * @see java.util.Iterator#next() */ public Object next() { if (!fHasNext) { throw new NoSuchElementException(); } String ret= fNext; try { calculateNext(); } catch (BadLocationException e) { log(e); fHasNext= false; fNext= null; } return ret; } /* * (non-Javadoc) * @see java.util.Iterator#remove() */ public void remove() { throw new UnsupportedOperationException(); } /** * Subclasses must override to calculates whether we have a next element to be returned and * which element it is (set fHasNext and fNext). * * @throws BadLocationException if we're at an invalid position in the document. */ protected abstract void calculateNext() throws BadLocationException; } /** * Iterator that gets the word completions in a document, and returns them one by one (lazily * gotten) from the current position. * * @since 3.6 */ private class HippieCompletionForwardIterator extends HippieCompletionIterator { /** * If <code>true</code> the word at caret position should be that last completion. * <code>true</code> is good for searching in the currently open document and * <code>false</code> is good for searching in other documents. */ private boolean fCurrentWordLast; /** The completion for the current word -- fix bug 132533 */ private String fCurrentWordCompletion= null; /* * (non-Javadoc) * @see HippieCompletionEngine#getForwardIterator(IDocument, CharSequence, int, boolean) */ private HippieCompletionForwardIterator(IDocument document, CharSequence prefix, int firstPosition, boolean currentWordLast) { super(document, prefix, firstPosition); this.fCurrentWordLast= currentWordLast; calculateFirst(); } /* * (non-Javadoc) * @see HippieCompletionIterator#calculateNext() */ protected void calculateNext() throws BadLocationException { if (fCurrentState == 0) { if (fFirstPosition == fDocument.getLength()) { this.fHasNext= false; return; } fSearcher= new FindReplaceDocumentAdapter(fDocument); // unless we are at the beginning of the document, the completion boundary // matches one character. It is enough to move just one character backwards // because the boundary pattern has the (....)+ form. // see HippieCompletionTest#testForwardSearch(). if (fFirstPosition > 0) { fFirstPosition--; // empty spacing is not permitted now. fSearchPattern= NON_EMPTY_COMPLETION_BOUNDARY + asRegPattern(fPrefix); } else { fSearchPattern= COMPLETION_BOUNDARY + asRegPattern(fPrefix); } fNextPos= fFirstPosition; fCurrentState= 1; } if (fCurrentState == 1) { fHasNext= false; IRegion reg= fSearcher.find(fNextPos, fSearchPattern, true, CASE_SENSITIVE, false, true); while (reg != null) { IRegion word= checkRegion(reg); fNextPos= word.getOffset() + word.getLength(); if (fNextPos >= fDocument.getLength()) { fCurrentState= 2; if (fHasNext) { return; } break; } else { if (fHasNext) { return; } reg= fSearcher.find(fNextPos, fSearchPattern, true, CASE_SENSITIVE, false, true); } } fCurrentState= 2; } if (fCurrentState == 2) { fCurrentState= 3; // the word at caret position goes last (bug 132533). if (fCurrentWordCompletion != null) { fNext= fCurrentWordCompletion; fHasNext= true; return; } } fNext= null; fHasNext= false; return; } /** * Checks the given region for a word to be returned in this iterator. * * @param reg the region to check * @return the word region. * @throws BadLocationException if we're at an invalid position in the document. */ private IRegion checkRegion(IRegion reg) throws BadLocationException { // since the boundary may be of nonzero length int wordSearchPos= reg.getOffset() + reg.getLength() - fPrefix.length(); // try to complete to a word. case is irrelevant here. IRegion word= fSearcher.find(wordSearchPos, COMPLETION_WORD_REGEX, true, true, false, true); if (word.getLength() > fPrefix.length()) { // empty suggestion will be added later String wholeWord= fDocument.get(word.getOffset(), word.getLength()); String completion= wholeWord.substring(fPrefix.length()); if (fCurrentWordLast && reg.getOffset() == fFirstPosition) { // we got the word at caret as completion if (fCurrentWordCompletion == null) { fCurrentWordCompletion= completion; // add it as the last word. } } else { fNext= completion; fHasNext= true; } } return word; } } /** * Iterator that gets the word completions in a document, and returns them one by one (lazily * gotten) backward from the current position. * * @since 3.6 */ private class HippieCompletionBackwardIterator extends HippieCompletionIterator { /** Last position searched **/ private int fLastSearchPos= -1; /* * (non-Javadoc) * @see HippieCompletionEngine#getBackwardIterator(IDocument, CharSequence, int) */ private HippieCompletionBackwardIterator(IDocument document, CharSequence prefix, int firstPosition) { super(document, prefix, firstPosition); calculateFirst(); } /* * (non-Javadoc) * @see HippieCompletionIterator#calculateNext() */ protected void calculateNext() throws BadLocationException { if (fCurrentState == 0) { fCurrentState= 1; // FindReplaceDocumentAdapter expects the start offset to be before the // actual caret position, probably for compatibility with forward search. if (fFirstPosition <= 1) { this.fNext= null; this.fHasNext= false; return; } fSearcher= new FindReplaceDocumentAdapter(fDocument); // search only at word boundaries fSearchPattern= COMPLETION_BOUNDARY + asRegPattern(fPrefix); int length= fDocument.getLength(); fNextPos= fFirstPosition; if (fNextPos >= length) { fNextPos= length - 1; } } while (true) { if (fNextPos <= 0) { this.fNext= null; this.fHasNext= false; return; } Assert.isTrue(fLastSearchPos != fNextPos, "Position did not change in loop (this would lead to recursion -- and should never happen)."); //$NON-NLS-1$ fLastSearchPos= fNextPos; IRegion reg= fSearcher.find(fNextPos, fSearchPattern, false, CASE_SENSITIVE, false, true); if (reg == null) { this.fNext= null; this.fHasNext= false; return; } // since the boundary may be of nonzero length int wordSearchPos= reg.getOffset() + reg.getLength() - fPrefix.length(); // try to complete to a word. case is of no matter here IRegion word= fSearcher.find(wordSearchPos, COMPLETION_WORD_REGEX, true, true, false, true); fNextPos= word.getOffset() - 1; if (word.getOffset() + word.getLength() > fFirstPosition) { continue; } if (word.getLength() > fPrefix.length()) { // empty suggestion will be added later String found= fDocument.get(word.getOffset(), word.getLength()); this.fHasNext= true; this.fNext= found.substring(fPrefix.length()); return; } } //Note: unreachable section } } /** * Logs the exception. * * @param e the exception * * @since 3.6 */ private void log(BadLocationException e) { String msg= e.getLocalizedMessage(); if (msg == null) msg= "unable to access the document"; //$NON-NLS-1$ TextEditorPlugin.getDefault().getLog().log(new Status(IStatus.ERROR, TextEditorPlugin.PLUGIN_ID, IStatus.OK, msg, e)); } }