1 /*******************************************************************************
2 * Copyright (c) 2004, 2011 IBM Corporation and others.
3 * All rights reserved. This program and the accompanying materials
4 * are made available under the terms of the Eclipse Public License v1.0
5 * which accompanies this distribution, and is available at
6 * http://www.eclipse.org/legal/epl-v10.html
9 * IBM Rational Software - Initial API and implementation
10 * Jens Elmenthaler - http://bugs.eclipse.org/173458 (camel case completion)
11 *******************************************************************************/
13 package org.eclipse.cdt.internal.ui.text.contentassist;
16 import org.eclipse.core.runtime.Assert;
17 import org.eclipse.jface.preference.IPreferenceStore;
18 import org.eclipse.jface.preference.PreferenceConverter;
19 import org.eclipse.jface.text.BadLocationException;
20 import org.eclipse.jface.text.BadPositionCategoryException;
21 import org.eclipse.jface.text.DefaultPositionUpdater;
22 import org.eclipse.jface.text.DocumentEvent;
23 import org.eclipse.jface.text.IDocument;
24 import org.eclipse.jface.text.IInformationControlCreator;
25 import org.eclipse.jface.text.IPositionUpdater;
26 import org.eclipse.jface.text.IRegion;
27 import org.eclipse.jface.text.ITextViewer;
28 import org.eclipse.jface.text.ITextViewerExtension2;
29 import org.eclipse.jface.text.ITextViewerExtension5;
30 import org.eclipse.jface.text.Position;
31 import org.eclipse.jface.text.Region;
32 import org.eclipse.jface.text.contentassist.ICompletionProposalExtension;
33 import org.eclipse.jface.text.contentassist.ICompletionProposalExtension2;
34 import org.eclipse.jface.text.contentassist.ICompletionProposalExtension3;
35 import org.eclipse.jface.text.contentassist.IContextInformation;
36 import org.eclipse.jface.text.link.ILinkedModeListener;
37 import org.eclipse.jface.text.link.LinkedModeModel;
38 import org.eclipse.jface.text.link.LinkedModeUI;
39 import org.eclipse.jface.text.link.LinkedModeUI.ExitFlags;
40 import org.eclipse.jface.text.link.LinkedModeUI.IExitPolicy;
41 import org.eclipse.jface.text.link.LinkedPosition;
42 import org.eclipse.jface.text.link.LinkedPositionGroup;
43 import org.eclipse.swt.SWT;
44 import org.eclipse.swt.custom.StyleRange;
45 import org.eclipse.swt.custom.StyledText;
46 import org.eclipse.swt.events.VerifyEvent;
47 import org.eclipse.swt.graphics.Color;
48 import org.eclipse.swt.graphics.Image;
49 import org.eclipse.swt.graphics.Point;
50 import org.eclipse.swt.graphics.RGB;
51 import org.eclipse.ui.texteditor.link.EditorLinkedModeUI;
53 import org.eclipse.cdt.ui.CUIPlugin;
54 import org.eclipse.cdt.ui.text.ICCompletionProposal;
56 import org.eclipse.cdt.internal.core.parser.util.ContentAssistMatcherFactory;
58 import org.eclipse.cdt.internal.ui.text.CTextTools;
61 public class CCompletionProposal implements ICCompletionProposal, ICompletionProposalExtension, ICompletionProposalExtension2, ICompletionProposalExtension3 {
63 private String fDisplayString;
64 private String fIdString;
65 private String fReplacementString;
66 private int fReplacementOffset;
67 private int fReplacementLength;
68 private int fCursorPosition;
70 private IContextInformation fContextInformation;
71 private int fContextInformationPosition;
72 private String fProposalInfo;
73 private char[] fTriggerCharacters;
74 protected boolean fToggleEating;
75 protected ITextViewer fTextViewer;
77 private int fRelevance;
78 private StyleRange fRememberedStyleRange;
81 * Creates a new completion proposal. All fields are initialized based on the provided information.
83 * @param replacementString the actual string to be inserted into the document
84 * @param replacementOffset the offset of the text to be replaced
85 * @param replacementLength the length of the text to be replaced
86 * @param image the image to display for this proposal
87 * @param displayString the string to be displayed for the proposal
88 * If set to <code>null</code>, the replacement string will be taken as display string.
90 public CCompletionProposal(String replacementString, int replacementOffset, int replacementLength, Image image, String displayString, int relevance) {
91 this(replacementString, replacementOffset, replacementLength, image, displayString, null, relevance, null);
95 * Creates a new completion proposal. All fields are initialized based on the provided information.
97 * @param replacementString the actual string to be inserted into the document
98 * @param replacementOffset the offset of the text to be replaced
99 * @param replacementLength the length of the text to be replaced
100 * @param image the image to display for this proposal
101 * @param displayString the string to be displayed for the proposal
102 * @param viewer the text viewer for which this proposal is computed, may be <code>null</code>
103 * If set to <code>null</code>, the replacement string will be taken as display string.
105 public CCompletionProposal(String replacementString, int replacementOffset, int replacementLength, Image image, String displayString, int relevance, ITextViewer viewer) {
106 this(replacementString, replacementOffset, replacementLength, image, displayString, null, relevance, viewer);
110 * Creates a new completion proposal. All fields are initialized based on the provided information.
112 * @param replacementString the actual string to be inserted into the document
113 * @param replacementOffset the offset of the text to be replaced
114 * @param replacementLength the length of the text to be replaced
115 * @param image the image to display for this proposal
116 * @param displayString the string to be displayed for the proposal
117 * @param idString the string to be uniquely identify this proposal
118 * @param viewer the text viewer for which this proposal is computed, may be <code>null</code>
119 * If set to <code>null</code>, the replacement string will be taken as display string.
121 public CCompletionProposal(String replacementString, int replacementOffset, int replacementLength, Image image, String displayString, String idString, int relevance, ITextViewer viewer) {
122 Assert.isNotNull(replacementString);
123 Assert.isTrue(replacementOffset >= 0);
124 Assert.isTrue(replacementLength >= 0);
126 fReplacementString = replacementString;
127 fReplacementOffset = replacementOffset;
128 fReplacementLength = replacementLength;
130 fRelevance = relevance;
131 fTextViewer = viewer;
133 fDisplayString = displayString != null ? displayString : replacementString;
134 fIdString = idString != null ? idString : displayString;
136 fCursorPosition = replacementString.length();
138 fContextInformation = null;
139 fContextInformationPosition = -1;
140 fTriggerCharacters = null;
141 fProposalInfo = null;
145 * Sets the context information.
146 * @param contextInformation The context information associated with this proposal
148 public void setContextInformation(IContextInformation contextInformation) {
149 fContextInformation= contextInformation;
150 fContextInformationPosition= (fContextInformation != null ? fCursorPosition : -1);
154 * Sets the trigger characters.
155 * @param triggerCharacters The set of characters which can trigger the application of this completion proposal
157 public void setTriggerCharacters(char[] triggerCharacters) {
158 fTriggerCharacters= triggerCharacters;
162 * Sets the proposal info.
163 * @param proposalInfo The additional information associated with this proposal or <code>null</code>
165 public void setAdditionalProposalInfo(String proposalInfo) {
166 fProposalInfo= proposalInfo;
170 * Sets the cursor position relative to the insertion offset. By default this is the length of the completion string
171 * (Cursor positioned after the completion)
172 * @param cursorPosition The cursorPosition to set
174 public void setCursorPosition(int cursorPosition) {
175 Assert.isTrue(cursorPosition >= 0);
176 fCursorPosition= cursorPosition;
177 fContextInformationPosition= (fContextInformation != null ? fCursorPosition : -1);
181 * @see ICompletionProposalExtension#apply(IDocument, char, int)
183 public void apply(IDocument document, char trigger, int offset) {
185 // patch replacement length
186 int delta= offset - (fReplacementOffset + fReplacementLength);
188 fReplacementLength += delta;
191 if (trigger == (char) 0) {
192 string= fReplacementString;
194 StringBuffer buffer= new StringBuffer(fReplacementString);
196 // fix for PR #5533. Assumes that no eating takes place.
197 if ((fCursorPosition > 0 && fCursorPosition <= buffer.length() && buffer.charAt(fCursorPosition - 1) != trigger)) {
198 buffer.insert(fCursorPosition, trigger);
202 string= buffer.toString();
205 // reference position just at the end of the document change.
206 int referenceOffset= fReplacementOffset + fReplacementLength;
207 final ReferenceTracker referenceTracker= new ReferenceTracker();
208 referenceTracker.preReplace(document, referenceOffset);
210 replace(document, fReplacementOffset, fReplacementLength, string);
212 referenceOffset= referenceTracker.postReplace(document);
213 fReplacementOffset= referenceOffset - (string == null ? 0 : string.length());
215 if (fTextViewer != null && string != null) {
216 int index= string.indexOf("()"); //$NON-NLS-1$
217 if (index != -1 && index + 1 == fCursorPosition) {
218 int newOffset= fReplacementOffset + fCursorPosition;
220 LinkedPositionGroup group= new LinkedPositionGroup();
221 group.addPosition(new LinkedPosition(document, newOffset, 0, LinkedPositionGroup.NO_STOP));
223 LinkedModeModel model= new LinkedModeModel();
224 model.addGroup(group);
225 model.forceInstall();
227 LinkedModeUI ui= new EditorLinkedModeUI(model, fTextViewer);
228 ui.setSimpleMode(true);
229 ui.setExitPolicy(new ExitPolicy(')'));
230 ui.setExitPosition(fTextViewer, newOffset + 1, 0, Integer.MAX_VALUE);
231 ui.setCyclingMode(LinkedModeUI.CYCLE_NEVER);
236 } catch (BadLocationException x) {
242 * A class to simplify tracking a reference position in a document.
244 private static final class ReferenceTracker {
246 /** The reference position category name. */
247 private static final String CATEGORY= "reference_position"; //$NON-NLS-1$
248 /** The position updater of the reference position. */
249 private final IPositionUpdater fPositionUpdater= new DefaultPositionUpdater(CATEGORY);
250 /** The reference position. */
251 private final Position fPosition= new Position(0);
254 * Called before document changes occur. It must be followed by a call to postReplace().
256 * @param document the document on which to track the reference position.
259 public void preReplace(IDocument document, int offset) throws BadLocationException {
260 fPosition.setOffset(offset);
262 document.addPositionCategory(CATEGORY);
263 document.addPositionUpdater(fPositionUpdater);
264 document.addPosition(CATEGORY, fPosition);
266 } catch (BadPositionCategoryException e) {
273 * Called after the document changed occured. It must be preceded by a call to preReplace().
275 * @param document the document on which to track the reference position.
277 public int postReplace(IDocument document) {
279 document.removePosition(CATEGORY, fPosition);
280 document.removePositionUpdater(fPositionUpdater);
281 document.removePositionCategory(CATEGORY);
283 } catch (BadPositionCategoryException e) {
287 return fPosition.getOffset();
291 protected static class ExitPolicy implements IExitPolicy {
293 final char fExitCharacter;
295 public ExitPolicy(char exitCharacter) {
296 fExitCharacter= exitCharacter;
300 * @see org.eclipse.jdt.internal.ui.text.link.LinkedPositionUI.ExitPolicy#doExit(org.eclipse.jdt.internal.ui.text.link.LinkedPositionManager, org.eclipse.swt.events.VerifyEvent, int, int)
302 public ExitFlags doExit(LinkedModeModel environment, VerifyEvent event, int offset, int length) {
304 if (event.character == fExitCharacter) {
305 if (environment.anyPositionContains(offset))
306 return new ExitFlags(ILinkedModeListener.UPDATE_CARET, false);
308 return new ExitFlags(ILinkedModeListener.UPDATE_CARET, true);
311 switch (event.character) {
313 return new ExitFlags(ILinkedModeListener.NONE, true);
322 // #6410 - File unchanged but dirtied by code assist
323 private void replace(IDocument document, int offset, int length, String string) throws BadLocationException {
324 if (!document.get(offset, length).equals(string))
325 document.replace(offset, length, string);
329 * @see ICompletionProposal#apply
331 public void apply(IDocument document) {
332 apply(document, (char) 0, fReplacementOffset + fReplacementLength);
336 * @see ICompletionProposal#getSelection
338 public Point getSelection(IDocument document) {
339 return new Point(fReplacementOffset + fCursorPosition, 0);
343 * @see ICompletionProposal#getContextInformation()
345 public IContextInformation getContextInformation() {
346 return fContextInformation;
350 * @see ICompletionProposal#getImage()
352 public Image getImage() {
357 * @see ICompletionProposal#getDisplayString()
359 public String getDisplayString() {
360 return fDisplayString;
364 * This method is used by the comparator to compare proposals. It ignores the return type of a function.
366 * @return the string representing the display name without the return type (if any).
368 public String getIdString() {
373 * @see ICompletionProposal#getAdditionalProposalInfo()
375 public String getAdditionalProposalInfo() {
376 if (fProposalInfo != null) {
377 return fProposalInfo;
383 * @see ICompletionProposalExtension#getTriggerCharacters()
385 public char[] getTriggerCharacters() {
386 return fTriggerCharacters;
390 * @see ICompletionProposalExtension#getContextInformationPosition()
392 public int getContextInformationPosition() {
393 return fReplacementOffset + fContextInformationPosition;
397 * Gets the replacement offset.
398 * @return Returns a int
400 public int getReplacementOffset() {
401 return fReplacementOffset;
405 * @see org.eclipse.jface.text.contentassist.ICompletionProposalExtension3#getCompletionOffset()
407 public int getPrefixCompletionStart(IDocument document, int completionOffset) {
408 return getReplacementOffset();
412 * Sets the replacement offset.
413 * @param replacementOffset The replacement offset to set
415 public void setReplacementOffset(int replacementOffset) {
416 Assert.isTrue(replacementOffset >= 0);
417 fReplacementOffset= replacementOffset;
421 * Gets the replacement length.
422 * @return Returns a int
424 public int getReplacementLength() {
425 return fReplacementLength;
429 * Sets the replacement length.
430 * @param replacementLength The replacementLength to set
432 public void setReplacementLength(int replacementLength) {
433 Assert.isTrue(replacementLength >= 0);
434 fReplacementLength= replacementLength;
438 * Gets the replacement string.
439 * @return Returns a String
441 public String getReplacementString() {
442 return fReplacementString;
446 * @see org.eclipse.jface.text.contentassist.ICompletionProposalExtension3#getReplacementText()
448 public CharSequence getPrefixCompletionText(IDocument document, int completionOffset) {
449 String string= getReplacementString();
450 int pos= string.indexOf('(');
452 return string.subSequence(0, pos);
457 * Sets the replacement string.
458 * @param replacementString The replacement string to set
460 public void setReplacementString(String replacementString) {
461 fReplacementString= replacementString;
466 * @param image The image to set
468 public void setImage(Image image) {
473 * @see ICompletionProposalExtension#isValidFor(IDocument, int)
475 public boolean isValidFor(IDocument document, int offset) {
476 return validate(document, offset, null);
480 * @see org.eclipse.jface.text.contentassist.ICompletionProposalExtension2#validate(org.eclipse.jface.text.IDocument, int, org.eclipse.jface.text.DocumentEvent)
482 public boolean validate(IDocument document, int offset, DocumentEvent event) {
484 if (offset < fReplacementOffset)
487 boolean validated= match(document, offset, fReplacementString);
489 if (validated && event != null) {
490 // adapt replacement range to document change
491 int delta= (event.fText == null ? 0 : event.fText.length()) - event.fLength;
492 fReplacementLength += delta;
499 * Gets the proposal's relevance.
500 * @return Returns a int
502 public int getRelevance() {
507 * Sets the proposal's relevance.
508 * @param relevance The relevance to set
510 public void setRelevance(int relevance) {
511 fRelevance= relevance;
515 * Returns <code>true</code> if a words matches the code completion prefix in the document,
516 * <code>false</code> otherwise.
518 protected boolean match(IDocument document, int offset, String word) {
522 final int wordLength= word.length();
523 if (offset > fReplacementOffset + wordLength)
527 int length= offset - fReplacementOffset;
528 String pattern= document.get(fReplacementOffset, length);
529 return ContentAssistMatcherFactory.getInstance().match(pattern.toCharArray(), word.toCharArray());
530 } catch (BadLocationException x) {
536 private static boolean insertCompletion() {
537 IPreferenceStore preference= CUIPlugin.getDefault().getPreferenceStore();
538 return preference.getBoolean(ContentAssistPreference.AUTOINSERT);
542 * @see org.eclipse.jface.text.contentassist.ICompletionProposalExtension1#apply(org.eclipse.jface.text.ITextViewer, char, int, int)
544 public void apply(ITextViewer viewer, char trigger, int stateMask, int offset) {
546 IDocument document= viewer.getDocument();
548 // don't eat if not in preferences, XOR with modifier key 1 (Ctrl)
549 // but: if there is a selection, replace it!
550 Point selection= viewer.getSelectedRange();
551 fToggleEating= (stateMask & SWT.MOD1) != 0;
552 if (insertCompletion() ^ fToggleEating)
553 fReplacementLength= selection.x + selection.y - fReplacementOffset;
555 apply(document, trigger, offset);
556 fToggleEating= false;
559 private static Color getForegroundColor(StyledText text) {
561 IPreferenceStore preference= CUIPlugin.getDefault().getPreferenceStore();
562 RGB rgb= PreferenceConverter.getColor(preference, ContentAssistPreference.PROPOSALS_FOREGROUND);
563 CTextTools textTools= CUIPlugin.getDefault().getTextTools();
564 return textTools.getColorManager().getColor(rgb);
567 private static Color getBackgroundColor(StyledText text) {
569 IPreferenceStore preference= CUIPlugin.getDefault().getPreferenceStore();
570 RGB rgb= PreferenceConverter.getColor(preference, ContentAssistPreference.PROPOSALS_BACKGROUND);
571 CTextTools textTools= CUIPlugin.getDefault().getTextTools();
572 return textTools.getColorManager().getColor(rgb);
575 private void repairPresentation(ITextViewer viewer) {
576 if (fRememberedStyleRange != null) {
577 if (viewer instanceof ITextViewerExtension2) {
578 // attempts to reduce the redraw area
579 ITextViewerExtension2 viewer2= (ITextViewerExtension2) viewer;
581 if (viewer instanceof ITextViewerExtension5) {
583 ITextViewerExtension5 extension= (ITextViewerExtension5) viewer;
584 IRegion widgetRange= extension.modelRange2WidgetRange(new Region(fRememberedStyleRange.start, fRememberedStyleRange.length));
585 if (widgetRange != null)
586 viewer2.invalidateTextPresentation(widgetRange.getOffset(), widgetRange.getLength());
589 viewer2.invalidateTextPresentation(fRememberedStyleRange.start + viewer.getVisibleRegion().getOffset(), fRememberedStyleRange.length);
593 viewer.invalidateTextPresentation();
597 private void updateStyle(ITextViewer viewer) {
599 StyledText text= viewer.getTextWidget();
600 if (text == null || text.isDisposed())
603 int widgetCaret= text.getCaretOffset();
606 if (viewer instanceof ITextViewerExtension5) {
607 ITextViewerExtension5 extension= (ITextViewerExtension5) viewer;
608 modelCaret= extension.widgetOffset2ModelOffset(widgetCaret);
610 IRegion visibleRegion= viewer.getVisibleRegion();
611 modelCaret= widgetCaret + visibleRegion.getOffset();
614 if (modelCaret >= fReplacementOffset + fReplacementLength) {
615 repairPresentation(viewer);
619 int offset= widgetCaret;
620 int length= fReplacementOffset + fReplacementLength - modelCaret;
622 Color foreground= getForegroundColor(text);
623 Color background= getBackgroundColor(text);
625 StyleRange range= text.getStyleRangeAtOffset(offset);
626 int fontStyle= range != null ? range.fontStyle : SWT.NORMAL;
628 repairPresentation(viewer);
629 fRememberedStyleRange= new StyleRange(offset, length, foreground, background, fontStyle);
631 // http://dev.eclipse.org/bugs/show_bug.cgi?id=34754
633 text.setStyleRange(fRememberedStyleRange);
634 } catch (IllegalArgumentException x) {
635 // catching exception as offset + length might be outside of the text widget
636 fRememberedStyleRange= null;
641 * @see org.eclipse.jface.text.contentassist.ICompletionProposalExtension2#selected(ITextViewer, boolean)
643 public void selected(ITextViewer viewer, boolean smartToggle) {
644 if (!insertCompletion() ^ smartToggle)
647 repairPresentation(viewer);
648 fRememberedStyleRange= null;
653 * @see org.eclipse.jface.text.contentassist.ICompletionProposalExtension2#unselected(ITextViewer)
655 public void unselected(ITextViewer viewer) {
656 repairPresentation(viewer);
657 fRememberedStyleRange= null;
661 * @see org.eclipse.jface.text.contentassist.ICompletionProposalExtension3#getInformationControlCreator()
663 public IInformationControlCreator getInformationControlCreator() {
667 public void updateReplacementOffset(int newOffset) {
668 setReplacementOffset(newOffset);
671 public void updateReplacementLength(int length) {
672 setReplacementLength(length);
676 * @see java.lang.Object#hashCode()
679 public int hashCode() {
680 return fIdString.hashCode();
684 * @see java.lang.Object#equals(java.lang.Object)
687 public boolean equals(Object other) {
688 if(!(other instanceof ICCompletionProposal))
690 return fIdString.equalsIgnoreCase(((ICCompletionProposal)other).getIdString());