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: * IBM Corporation - fixes and cleaning
011: *******************************************************************************/package org.eclipse.ui.texteditor;
012:
013: import java.util.ArrayList;
014: import java.util.List;
015: import java.util.ResourceBundle;
016:
017: import org.eclipse.jface.text.BadLocationException;
018: import org.eclipse.jface.text.IDocument;
019: import org.eclipse.jface.text.IRewriteTarget;
020: import org.eclipse.jface.text.ITextSelection;
021: import org.eclipse.jface.text.source.ISourceViewer;
022:
023: import org.eclipse.core.runtime.Assert;
024: import org.eclipse.core.runtime.IStatus;
025: import org.eclipse.core.runtime.Status;
026: import org.eclipse.ui.IEditorInput;
027: import org.eclipse.ui.IEditorPart;
028: import org.eclipse.ui.IEditorReference;
029: import org.eclipse.ui.IWorkbenchWindow;
030: import org.eclipse.ui.internal.texteditor.CompoundEditExitStrategy;
031: import org.eclipse.ui.internal.texteditor.HippieCompletionEngine;
032: import org.eclipse.ui.internal.texteditor.ICompoundEditListener;
033: import org.eclipse.ui.internal.texteditor.TextEditorPlugin;
034:
035: /**
036: * This class implements the emacs style completion action. Completion action is
037: * a stateful action, as the user may invoke it several times in a row in order
038: * to scroll the possible completions.
039: *
040: * TODO: Sort by editor type
041: * TODO: Provide history option
042: *
043: * @since 3.1
044: * @author Genady Beryozkin, me@genady.org
045: */
046: final class HippieCompleteAction extends TextEditorAction {
047:
048: /**
049: * This class represents the state of the last completion process. Each time
050: * the user moves to a new position and calls this action an instance of
051: * this inner class is created and saved in
052: * {@link HippieCompleteAction#fLastCompletion}.
053: */
054: private static class CompletionState {
055:
056: /** The length of the last suggestion string */
057: int length;
058:
059: /** The index of next suggestion (index into the suggestion array) */
060: int nextSuggestion;
061:
062: /** The caret position at which we insert the suggestions */
063: final int startOffset;
064:
065: /**
066: * The list of suggestions that was computed when the completion action
067: * was first invoked
068: */
069: final String[] suggestions;
070:
071: /**
072: * Create a new completion state object
073: *
074: * @param suggestions the array of possible completions
075: * @param startOffset the position in the parent document at which the
076: * completions will be inserted.
077: */
078: CompletionState(String[] suggestions, int startOffset) {
079: this .suggestions = suggestions;
080: this .startOffset = startOffset;
081: length = 0;
082: nextSuggestion = 0;
083: }
084:
085: /**
086: * Advances the completion state to represent the next completion.
087: */
088: public void advance() {
089: length = suggestions[nextSuggestion].length();
090: nextSuggestion = (nextSuggestion + 1) % suggestions.length;
091: }
092: }
093:
094: /**
095: * The document that will be manipulated (currently open in the editor)
096: */
097: private IDocument fDocument;
098:
099: /**
100: * The completion state that is used to continue the iteration over
101: * completion suggestions
102: */
103: private CompletionState fLastCompletion = null;
104:
105: /**
106: * The completion engine
107: */
108: private final HippieCompletionEngine fEngine = new HippieCompletionEngine();
109:
110: /** The compound edit exit strategy. */
111: private final CompoundEditExitStrategy fExitStrategy = new CompoundEditExitStrategy(
112: ITextEditorActionDefinitionIds.HIPPIE_COMPLETION);
113:
114: /**
115: * Creates a new action.
116: *
117: * @param bundle the resource bundle
118: * @param prefix a prefix to be prepended to the various resource keys
119: * (described in <code>ResourceAction</code> constructor), or
120: * <code>null</code> if none
121: * @param editor the text editor
122: */
123: HippieCompleteAction(ResourceBundle bundle, String prefix,
124: ITextEditor editor) {
125: super (bundle, prefix, editor);
126: fExitStrategy.addCompoundListener(new ICompoundEditListener() {
127: public void endCompoundEdit() {
128: clearState();
129: }
130: });
131: }
132:
133: /**
134: * Invalidates the cached completions, removes all registered listeners and
135: * sets the cached document to <code>null</code>.
136: */
137: private void clearState() {
138: fLastCompletion = null;
139:
140: ITextEditor editor = getTextEditor();
141:
142: if (editor != null) {
143: IRewriteTarget target = (IRewriteTarget) editor
144: .getAdapter(IRewriteTarget.class);
145: if (target != null) {
146: fExitStrategy.disarm();
147: target.endCompoundChange();
148: }
149: }
150:
151: fDocument = null;
152: }
153:
154: /**
155: * Perform the next completion.
156: */
157: private void completeNext() {
158: try {
159: fDocument
160: .replace(
161: fLastCompletion.startOffset,
162: fLastCompletion.length,
163: fLastCompletion.suggestions[fLastCompletion.nextSuggestion]);
164: } catch (BadLocationException e) {
165: // we should never get here. different from other places to notify the user.
166: log(e);
167: clearState();
168: return;
169: }
170:
171: // advance the suggestion state
172: fLastCompletion.advance();
173:
174: // move the caret to the insertion point
175: ISourceViewer sourceViewer = ((AbstractTextEditor) getTextEditor())
176: .getSourceViewer();
177: sourceViewer.setSelectedRange(fLastCompletion.startOffset
178: + fLastCompletion.length, 0);
179: sourceViewer.revealRange(fLastCompletion.startOffset,
180: fLastCompletion.length);
181:
182: fExitStrategy.arm(((AbstractTextEditor) getTextEditor())
183: .getSourceViewer());
184: }
185:
186: /**
187: * Return the list of suggestions from the current document. First the
188: * document is searched backwards from the caret position and then forwards.
189: *
190: * @param prefix the completion prefix
191: * @return all possible completions that were found in the current document
192: * @throws BadLocationException if accessing the document fails
193: */
194: private ArrayList createSuggestionsFromOpenDocument(String prefix)
195: throws BadLocationException {
196: int selectionOffset = getSelectionOffset();
197:
198: ArrayList completions = new ArrayList();
199: completions.addAll(fEngine.getCompletionsBackwards(fDocument,
200: prefix, selectionOffset));
201: completions.addAll(fEngine.getCompletionsForward(fDocument,
202: prefix, selectionOffset - prefix.length(), true));
203:
204: return completions;
205: }
206:
207: /**
208: * Returns the document currently displayed in the editor, or
209: * <code>null</code>
210: *
211: * @return the document currently displayed in the editor, or
212: * <code>null</code>
213: */
214: private IDocument getCurrentDocument() {
215: ITextEditor editor = getTextEditor();
216: if (editor == null)
217: return null;
218: IDocumentProvider provider = editor.getDocumentProvider();
219: if (provider == null)
220: return null;
221:
222: IDocument document = provider.getDocument(editor
223: .getEditorInput());
224: return document;
225: }
226:
227: /**
228: * Return the part of a word before the caret. If the caret is not at a
229: * middle/end of a word, returns null.
230: *
231: * @return the prefix at the current cursor position that will be used in
232: * the search for possible completions
233: * @throws BadLocationException if accessing the document fails
234: */
235: private String getCurrentPrefix() throws BadLocationException {
236: ITextSelection selection = (ITextSelection) getTextEditor()
237: .getSelectionProvider().getSelection();
238: if (selection.getLength() > 0) {
239: return null;
240: }
241: return fEngine
242: .getPrefixString(fDocument, selection.getOffset());
243: }
244:
245: /**
246: * Returns the current selection (or caret) offset.
247: *
248: * @return the current selection (or caret) offset
249: */
250: private int getSelectionOffset() {
251: return ((ITextSelection) getTextEditor().getSelectionProvider()
252: .getSelection()).getOffset();
253: }
254:
255: /**
256: * Create the array of suggestions. It scans all open text editors and
257: * prefers suggestions from the currently open editor. It also adds the
258: * empty suggestion at the end.
259: *
260: * @param prefix the prefix to search for
261: * @return the list of all possible suggestions in the currently open
262: * editors
263: * @throws BadLocationException if accessing the current document fails
264: */
265: private String[] getSuggestions(String prefix)
266: throws BadLocationException {
267:
268: ArrayList suggestions = createSuggestionsFromOpenDocument(prefix);
269:
270: IWorkbenchWindow window = getTextEditor().getSite()
271: .getWorkbenchWindow();
272: IEditorReference editorsArray[] = window.getActivePage()
273: .getEditorReferences();
274:
275: for (int i = 0; i < editorsArray.length; i++) {
276: IEditorPart realEditor = editorsArray[i].getEditor(false);
277: if (realEditor instanceof ITextEditor
278: && !realEditor.equals(getTextEditor())) { // realEditor != null
279: ITextEditor textEditor = (ITextEditor) realEditor;
280: IEditorInput input = textEditor.getEditorInput();
281: IDocument doc = textEditor.getDocumentProvider()
282: .getDocument(input);
283:
284: suggestions.addAll(fEngine.getCompletionsForward(doc,
285: prefix, 0, false));
286: }
287: }
288: // add the empty suggestion
289: suggestions.add(""); //$NON-NLS-1$
290:
291: List uniqueSuggestions = fEngine.makeUnique(suggestions);
292:
293: return (String[]) uniqueSuggestions.toArray(new String[0]);
294: }
295:
296: /**
297: * Returns <code>true</code> if the current completion state is still
298: * valid given the current document and selection.
299: *
300: * @return <code>true</code> if the cached state is valid,
301: * <code>false</code> otherwise
302: */
303: private boolean isStateValid() {
304: return fDocument != null
305: && fDocument.equals(getCurrentDocument())
306: && fLastCompletion != null
307: && fLastCompletion.startOffset + fLastCompletion.length == getSelectionOffset();
308: }
309:
310: /**
311: * Notifies the user that there are no suggestions.
312: */
313: private void notifyUser() {
314: // TODO notify via status line?
315: getTextEditor().getSite().getShell().getDisplay().beep();
316: }
317:
318: /*
319: * @see org.eclipse.jface.action.Action#run()
320: */
321: public void run() {
322: if (!validateEditorInputState())
323: return;
324:
325: if (!isStateValid())
326: updateState();
327:
328: if (isStateValid())
329: completeNext();
330: }
331:
332: /*
333: * @see org.eclipse.jface.action.IAction#isEnabled()
334: */
335: public boolean isEnabled() {
336: return canModifyEditor();
337: }
338:
339: /*
340: * @see org.eclipse.ui.texteditor.TextEditorAction#setEditor(org.eclipse.ui.texteditor.ITextEditor)
341: */
342: public void setEditor(ITextEditor editor) {
343: clearState(); // make sure to remove listers before the editor changes!
344: super .setEditor(editor);
345: }
346:
347: /**
348: * Update the completion state. The completion cache is updated with the
349: * completions based on the currently displayed document and the current
350: * selection. To track the validity of the cached state, listeners are
351: * registered with the editor and document, and the current document is
352: * cached.
353: */
354: private void updateState() {
355: Assert.isNotNull(getTextEditor());
356:
357: clearState();
358:
359: IDocument document = getCurrentDocument();
360: if (document != null) {
361: fDocument = document;
362:
363: String[] suggestions;
364: try {
365: String prefix = getCurrentPrefix();
366: if (prefix == null) {
367: notifyUser();
368: return;
369: }
370: suggestions = getSuggestions(prefix);
371: } catch (BadLocationException e) {
372: log(e);
373: return;
374: }
375:
376: // if it is single empty suggestion
377: if (suggestions.length == 1) {
378: notifyUser();
379: return;
380: }
381:
382: IRewriteTarget target = (IRewriteTarget) getTextEditor()
383: .getAdapter(IRewriteTarget.class);
384: if (target != null)
385: target.beginCompoundChange();
386:
387: fLastCompletion = new CompletionState(suggestions,
388: getSelectionOffset());
389: }
390: }
391:
392: /**
393: * Logs the exception.
394: *
395: * @param e the exception
396: */
397: private void log(BadLocationException e) {
398: String msg = e.getLocalizedMessage();
399: if (msg == null)
400: msg = "unable to access the document"; //$NON-NLS-1$
401: TextEditorPlugin.getDefault().getLog().log(
402: new Status(IStatus.ERROR, TextEditorPlugin.PLUGIN_ID,
403: IStatus.OK, msg, e));
404: }
405: }
|