Java tutorial
/** * Aptana Studio * Copyright (c) 2005-2011 by Appcelerator, Inc. All Rights Reserved. * Licensed under the terms of the Eclipse Public License (EPL). * Please see the license-epl.html included with this distribution for details. * Any modifications to this file must keep this entire header intact. */ package com.aptana.editor.common.contentassist; /*********************************************************************************************************************** * Copyright (c) 2000, 2005 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: IBM Corporation - initial API and implementation **********************************************************************************************************************/ import java.text.MessageFormat; import java.util.ArrayList; import java.util.List; import org.eclipse.core.resources.IProject; import org.eclipse.core.resources.IResource; import org.eclipse.core.resources.ProjectScope; import org.eclipse.core.runtime.IAdaptable; import org.eclipse.core.runtime.Platform; import org.eclipse.core.runtime.preferences.IEclipsePreferences; import org.eclipse.core.runtime.preferences.IEclipsePreferences.IPreferenceChangeListener; import org.eclipse.core.runtime.preferences.IEclipsePreferences.PreferenceChangeEvent; import org.eclipse.jface.contentassist.IContentAssistSubjectControl; import org.eclipse.jface.layout.GridDataFactory; import org.eclipse.jface.preference.JFacePreferences; import org.eclipse.jface.resource.JFaceResources; import org.eclipse.jface.text.BadLocationException; import org.eclipse.jface.text.DocumentEvent; import org.eclipse.jface.text.IDocument; import org.eclipse.jface.text.IDocumentListener; import org.eclipse.jface.text.IEditingSupport; import org.eclipse.jface.text.IEditingSupportRegistry; import org.eclipse.jface.text.IRegion; import org.eclipse.jface.text.IRewriteTarget; import org.eclipse.jface.text.ITextViewer; import org.eclipse.jface.text.ITextViewerExtension; import org.eclipse.jface.text.TextUtilities; import org.eclipse.jface.text.contentassist.ICompletionProposal; import org.eclipse.jface.text.contentassist.ICompletionProposalExtension; import org.eclipse.jface.text.contentassist.ICompletionProposalExtension2; import org.eclipse.jface.text.contentassist.ICompletionProposalExtension3; import org.eclipse.jface.text.contentassist.ICompletionProposalExtension4; import org.eclipse.jface.text.contentassist.IContextInformation; import org.eclipse.swt.SWT; import org.eclipse.swt.custom.BusyIndicator; import org.eclipse.swt.events.ControlAdapter; import org.eclipse.swt.events.ControlEvent; import org.eclipse.swt.events.ControlListener; import org.eclipse.swt.events.DisposeEvent; import org.eclipse.swt.events.DisposeListener; import org.eclipse.swt.events.KeyEvent; import org.eclipse.swt.events.KeyListener; import org.eclipse.swt.events.SelectionEvent; import org.eclipse.swt.events.SelectionListener; import org.eclipse.swt.events.VerifyEvent; import org.eclipse.swt.graphics.Color; import org.eclipse.swt.graphics.GC; import org.eclipse.swt.graphics.Image; import org.eclipse.swt.graphics.Point; import org.eclipse.swt.graphics.Rectangle; import org.eclipse.swt.layout.GridData; import org.eclipse.swt.layout.GridLayout; import org.eclipse.swt.widgets.Control; import org.eclipse.swt.widgets.Event; import org.eclipse.swt.widgets.Listener; import org.eclipse.swt.widgets.Shell; import org.eclipse.swt.widgets.Table; import org.eclipse.swt.widgets.TableColumn; import org.eclipse.swt.widgets.TableItem; import org.eclipse.ui.IEditorInput; import org.eclipse.ui.texteditor.ITextEditor; import com.aptana.core.logging.IdeLog; import com.aptana.core.util.EclipseUtil; import com.aptana.core.util.StringUtil; import com.aptana.ui.epl.UIEplPlugin; /** * This class is used to present proposals to the user. If additional information exists for a proposal, then selecting * that proposal will result in the information being displayed in a secondary window. * * @see org.eclipse.jface.text.contentassist.ICompletionProposal */ public class CompletionProposalPopup implements IContentAssistListener { private static final String COM_APTANA_EDITOR_COMMON = "com.aptana.editor.common"; //$NON-NLS-1$ /** * Set to <code>true</code> to use a Table with SWT.VIRTUAL. XXX: This is a workaround for: * https://bugs.eclipse.org/bugs/show_bug.cgi?id=90321 More details see also: * https://bugs.eclipse.org/bugs/show_bug.cgi?id=98585#c36 * * @since 3.1 */ private static final boolean USE_VIRTUAL = !"motif".equals(SWT.getPlatform()); //$NON-NLS-1$ /** * PROPOSAL_ITEM_HEIGHT */ public static final int PROPOSAL_ITEMS_VISIBLE = 7; /** * MAX_PROPOSAL_COLUMN_WIDTH */ public static final int MAX_PROPOSAL_COLUMN_WIDTH = 500; /** * MAX_LOCATION_COLUMN_WIDTH */ public static final int MAX_LOCATION_COLUMN_WIDTH = 200; /** * MIN_PROPOSAL_COLUMN_WIDTH */ public static final int MIN_PROPOSAL_COLUMN_WIDTH = 100; /** * ProposalSelectionListener * * @author Ingo Muschenetz */ private final class ProposalSelectionListener implements KeyListener { /** * @see org.eclipse.swt.events.KeyListener#keyPressed(org.eclipse.swt.events.KeyEvent) */ public void keyPressed(KeyEvent e) { if (!Helper.okToUse(fProposalShell)) { return; } if (e.character == 0 && e.keyCode == SWT.MOD1) { // http://dev.eclipse.org/bugs/show_bug.cgi?id=34754 int index = fProposalTable.getSelectionIndex(); if (index >= 0) { selectProposal(index, true, true); } // else // { // fProposalTable.setTopIndex(0); // } } } /** * @see org.eclipse.swt.events.KeyListener#keyReleased(org.eclipse.swt.events.KeyEvent) */ public void keyReleased(KeyEvent e) { if (!Helper.okToUse(fProposalShell)) { return; } if (e.character == 0 && e.keyCode == SWT.MOD1) { // http://dev.eclipse.org/bugs/show_bug.cgi?id=34754 int index = fProposalTable.getSelectionIndex(); if (index >= 0) { selectProposal(index, false, true); } // else // { // fProposalTable.setTopIndex(0); // } } } } /** The associated text viewer. */ private ITextViewer fViewer; /** The associated code assistant. */ private ContentAssistant fContentAssistant; /** The used additional info controller. */ private AdditionalInfoController fAdditionalInfoController; /** The closing strategy for this completion proposal popup. */ private PopupCloser fPopupCloser = new PopupCloser(); /** The popup shell. */ private Shell fProposalShell; /** The proposal table. */ private Table fProposalTable; /** Indicates whether a completion proposal is being inserted. */ private boolean fInserting = false; /** The key listener to control navigation. */ private ProposalSelectionListener fKeyListener; /** List of document events used for filtering proposals. */ private List<DocumentEvent> fDocumentEvents = new ArrayList<DocumentEvent>(); /** Listener filling the document event queue. */ private IDocumentListener fDocumentListener; /** Reentrance count for filtered proposals. */ private long fInvocationCounter = 0; /** The filter list of proposals. */ private ICompletionProposal[] fFilteredProposals; /** The computed list of proposals. */ private ICompletionProposal[] fComputedProposals; /** The offset for which the proposals have been computed. */ private int fInvocationOffset; /** The offset for which the computed proposals have been filtered. */ private int fFilterOffset; /** The key last pressed to trigger activation * */ private char fActivationKey; /** Do we insert the selected proposal on tab? * */ private boolean _insertOnTab; /** * The most recently selected proposal. * * @since 3.0 */ private ICompletionProposal fLastProposal; /** * The code assist subject control. This replaces <code>fViewer</code> * * @since 3.0 */ private IContentAssistSubjectControl fContentAssistSubjectControl; /** * The code assist subject control adapter. This replaces <code>fViewer</code> * * @since 3.0 */ private ContentAssistSubjectControlAdapter fContentAssistSubjectControlAdapter; /** * Remembers the size for this completion proposal popup. * * @since 3.0 */ private Point fSize; /** * Editor helper that communicates that the completion proposal popup may have focus while the 'logical focus' is * still with the editor. * * @since 3.1 */ private IEditingSupport fFocusHelper; /** * Set to true by {@link #computeFilteredProposals(int, DocumentEvent)} if the returned proposals are a subset of * {@link #fFilteredProposals}, <code>false</code> if not. * * @since 3.1 */ private boolean fIsFilteredSubset; private IEclipsePreferences projectScopeNode; private IEclipsePreferences instanceScopeNode; private IPreferenceChangeListener prefListener; /** * Creates a new completion proposal popup for the given elements. * * @param contentAssistant * the code assistant feeding this popup * @param viewer * the viewer on top of which this popup appears * @param infoController * the information control collaborating with this popup * @since 2.0 */ public CompletionProposalPopup(ContentAssistant contentAssistant, ITextViewer viewer, AdditionalInfoController infoController) { fContentAssistant = contentAssistant; fViewer = viewer; fAdditionalInfoController = infoController; fContentAssistSubjectControlAdapter = new ContentAssistSubjectControlAdapter(fViewer); } /** * Creates a new completion proposal popup for the given elements. * * @param contentAssistant * the code assistant feeding this popup * @param contentAssistSubjectControl * the code assist subject control on top of which this popup appears * @param infoController * the information control collaborating with this popup * @since 3.0 */ public CompletionProposalPopup(ContentAssistant contentAssistant, IContentAssistSubjectControl contentAssistSubjectControl, AdditionalInfoController infoController) { fContentAssistant = contentAssistant; fContentAssistSubjectControl = contentAssistSubjectControl; fAdditionalInfoController = infoController; fContentAssistSubjectControlAdapter = new ContentAssistSubjectControlAdapter(fContentAssistSubjectControl); } /** * Computes and presents completion proposals. The flag indicates whether this call has be made out of an auto * activation context. * * @param autoActivated * <code>true</code> if auto activation context * @return an error message or <code>null</code> in case of no error */ public String showProposals(final boolean autoActivated) { if (fKeyListener == null) { fKeyListener = new ProposalSelectionListener(); } final Control control = fContentAssistSubjectControlAdapter.getControl(); if (control != null && !control.isDisposed()) { // add the listener before computing the proposals so we don't move the caret // when the user types fast. fContentAssistSubjectControlAdapter.addKeyListener(fKeyListener); BusyIndicator.showWhile(control.getDisplay(), new Runnable() { public void run() { fInvocationOffset = fContentAssistSubjectControlAdapter.getSelectedRange().x; fFilterOffset = fInvocationOffset; fComputedProposals = computeProposals(fInvocationOffset, autoActivated); IDocument doc = fContentAssistSubjectControlAdapter.getDocument(); DocumentEvent initial = new DocumentEvent(doc, fInvocationOffset, 0, StringUtil.EMPTY); fComputedProposals = filterProposals(fComputedProposals, doc, fInvocationOffset, initial); int count = (fComputedProposals == null ? 0 : fComputedProposals.length); // If we don't have any proposals, and we've manually asked for proposals, show "no proposals" if (!autoActivated && count == 0) { fComputedProposals = createNoProposal(); count = fComputedProposals.length; } if (count == 0) { hide(); } else if (count == 1 && !autoActivated && canAutoInsert(fComputedProposals[0])) { insertProposal(fComputedProposals[0], (char) 0, 0, fInvocationOffset); hide(); } else { createPopup(); } } }); } return getErrorMessage(); } /** * Create the "no proposals" proposal * * @return */ private ICompletionProposal[] createNoProposal() { fEmptyProposal.fOffset = fFilterOffset; fEmptyProposal.fDisplayString = fEmptyMessage != null ? fEmptyMessage : JFaceTextMessages.getString("CompletionProposalPopup.no_proposals"); //$NON-NLS-1$ modifySelection(-1, -1); // deselect everything return new ICompletionProposal[] { fEmptyProposal }; } /** * Returns the completion proposal available at the given offset of the viewer's document. Delegates the work to the * code assistant. * * @param offset * the offset * @param autoActivated * @return the completion proposals available at this offset */ private ICompletionProposal[] computeProposals(int offset, boolean autoActivated) { if (fContentAssistSubjectControl != null) { return fContentAssistant.computeCompletionProposals(fContentAssistSubjectControl, offset, fActivationKey); } return fContentAssistant.computeCompletionProposals(fViewer, offset, fActivationKey, autoActivated); } /** * Returns the error message. * * @return the error message */ private String getErrorMessage() { return fContentAssistant.getErrorMessage(); } /** * Creates the proposal selector. */ private void createProposalSelector() { Control control = fContentAssistSubjectControlAdapter.getControl(); if (Helper.okToUse(fProposalShell)) { // Custom code to force colors again in case theme changed... // Not sure why we don't set background for all WS here if (!"carbon".equals(SWT.getPlatform())) //$NON-NLS-1$ { fProposalShell.setBackground(getForegroundColor(control)); } Color c = getBackgroundColor(control); fProposalTable.setBackground(c); c = getForegroundColor(control); fProposalTable.setForeground(c); return; } fProposalShell = new Shell(control.getShell(), SWT.ON_TOP | SWT.RESIZE); fProposalShell.setFont(JFaceResources.getDefaultFont()); if (USE_VIRTUAL) { fProposalTable = new Table(fProposalShell, SWT.H_SCROLL | SWT.V_SCROLL | SWT.VIRTUAL); Listener listener = new Listener() { public void handleEvent(Event event) { handleSetData(event); } }; fProposalTable.addListener(SWT.SetData, listener); } else { fProposalTable = new Table(fProposalShell, SWT.H_SCROLL | SWT.V_SCROLL); } fProposalShell.addControlListener(new ControlAdapter() { public void controlResized(ControlEvent e) { TableColumn[] columns = fProposalTable.getColumns(); int currentSize = 0; for (int i = 1; i < columns.length; i++) { currentSize += columns[i].getWidth(); } Rectangle area = fProposalShell.getClientArea(); int width = getTableWidth(); TableColumn column1 = fProposalTable.getColumn(0); // take up any remaining space for first column fProposalTable.setSize(area.width, area.height); int col1Width = width - currentSize; // 1st column can't be smaller than a default size; col1Width = Math.max(col1Width, MIN_PROPOSAL_COLUMN_WIDTH); column1.setWidth(col1Width); } }); _insertOnTab = true; // store.getBoolean(IPreferenceConstants.INSERT_ON_TAB); // Here we add custom columns new TableColumn(fProposalTable, SWT.LEFT); for (int i = 0; i < fContentAssistant.getUserAgentColumnCount(); i++) { TableColumn tc = new TableColumn(fProposalTable, SWT.LEFT); tc.setWidth(20); } new TableColumn(fProposalTable, SWT.LEFT); // end custom columns fProposalTable.setLocation(0, 0); if (fAdditionalInfoController != null) { fAdditionalInfoController.setSizeConstraints(40, 20, true, false); } // Custom code: We set margins to 1 so we get a border GridLayout layout = new GridLayout(); layout.marginWidth = 1; layout.marginHeight = 1; fProposalShell.setLayout(layout); GridData data = new GridData(GridData.FILL_BOTH); Point size = fContentAssistant.restoreCompletionProposalPopupSize(); if (size != null) { fProposalTable.setLayoutData(data); fProposalShell.setSize(size); } else { int height = fProposalTable.getItemHeight() * CompletionProposalPopup.PROPOSAL_ITEMS_VISIBLE; // use golden ratio as default aspect ratio final double aspectRatio = (1 + Math.sqrt(5)) / 2; int width = (int) (height * aspectRatio); Rectangle trim = fProposalTable.computeTrim(0, 0, width, height); data.heightHint = trim.height; data.widthHint = trim.width; fProposalTable.setLayoutData(data); fProposalShell.pack(); } fProposalShell.addControlListener(new ControlListener() { public void controlMoved(ControlEvent e) { } public void controlResized(ControlEvent e) { if (fAdditionalInfoController != null) { // reset the cached resize constraints fAdditionalInfoController.setSizeConstraints(40, 20, true, false); fAdditionalInfoController.hideInformationControl(); fAdditionalInfoController.handleTableSelectionChanged(); } fSize = fProposalShell.getSize(); } }); // Custom code: not sure why we don't set background for all WS here if (!"carbon".equals(SWT.getPlatform())) //$NON-NLS-1$ { fProposalShell.setBackground(getForegroundColor(control)); } Color c = getBackgroundColor(control); fProposalTable.setBackground(c); c = getForegroundColor(control); fProposalTable.setForeground(c); // Custom code for overriding selection color Listener selectionOverride = new Listener() { public void handleEvent(Event event) { if ((event.detail & SWT.SELECTED) != 0) { GC gc = event.gc; Color oldBackground = gc.getBackground(); Color sc = fContentAssistant.getProposalSelectorSelectionColor(); if (sc == null) return; gc.setBackground(sc); gc.fillRectangle(event.x, event.y, event.width, event.height); gc.setBackground(oldBackground); event.detail &= ~SWT.SELECTED; event.detail &= ~SWT.BACKGROUND; gc.setForeground(getForegroundColor(fContentAssistSubjectControlAdapter.getControl())); } } }; fProposalTable.addListener(SWT.EraseItem, selectionOverride); fProposalTable.addSelectionListener(new SelectionListener() { public void widgetSelected(SelectionEvent e) { } public void widgetDefaultSelected(SelectionEvent e) { selectProposalWithMask(e.stateMask); // This disposal on windows it because of focus issue when a select is manually select and the next time // the popup is show it has focus and so editor typing focus is lost if (Platform.OS_WIN32.equals(Platform.getOS())) { disposePopup(); } } }); fPopupCloser.install(fContentAssistant, fProposalTable, fAdditionalInfoController); // TISTUD-913: changed to the line above from 'fPopupCloser.install(fContentAssistant, fProposalTable);' installPreferenceListener(); fProposalShell.addDisposeListener(new DisposeListener() { public void widgetDisposed(DisposeEvent e) { unregister(); // but don't dispose the shell, since we're being called from its disposal event! } }); fProposalTable.setHeaderVisible(false); // addCommandSupport(fProposalTable); } /** * Set up the preference listener */ private void installPreferenceListener() { prefListener = new IPreferenceChangeListener() { public void preferenceChange(PreferenceChangeEvent event) { if (event.getKey().equals(IPreferenceConstants.USER_AGENT_PREFERENCE)) { if (Helper.okToUse(fProposalShell)) { fProposalShell.dispose(); } else { if (projectScopeNode != null) { projectScopeNode.removePreferenceChangeListener(this); } if (instanceScopeNode != null) { instanceScopeNode.removePreferenceChangeListener(this); } } } } }; projectScopeNode = getProjectScopeNode(); if (projectScopeNode != null) { projectScopeNode.addPreferenceChangeListener(prefListener); } instanceScopeNode = EclipseUtil.instanceScope().getNode(COM_APTANA_EDITOR_COMMON); if (instanceScopeNode != null) { instanceScopeNode.addPreferenceChangeListener(prefListener); } } /** * Returns a project scope node, or null. */ private IEclipsePreferences getProjectScopeNode() { // Locate the project. What a joy!... if (fViewer instanceof IAdaptable) { ITextEditor editor = (ITextEditor) ((IAdaptable) fViewer).getAdapter(ITextEditor.class); if (editor != null) { IEditorInput editorInput = editor.getEditorInput(); if (editorInput != null) { IResource resource = (IResource) editorInput.getAdapter(IResource.class); if (resource != null) { IProject project = resource.getProject(); return new ProjectScope(project).getNode(COM_APTANA_EDITOR_COMMON); } } } } return null; } /** * Returns the background color to use. * * @param control * the control to get the display from * @return the background color * @since 3.2 */ private Color getBackgroundColor(Control control) { Color c = fContentAssistant.getProposalSelectorBackground(); if (c == null) c = JFaceResources.getColorRegistry().get(JFacePreferences.CONTENT_ASSIST_BACKGROUND_COLOR); return c; } /** * Returns the foreground color to use. * * @param control * the control to get the display from * @return the foreground color * @since 3.2 */ private Color getForegroundColor(Control control) { Color c = fContentAssistant.getProposalSelectorForeground(); if (c == null) c = JFaceResources.getColorRegistry().get(JFacePreferences.CONTENT_ASSIST_FOREGROUND_COLOR); return c; } /** * @since 3.1 */ int defaultIndex = -1; private char fLastKeyPressed; /** * The (reusable) empty proposal. * * @since 3.2 */ private final EmptyProposal fEmptyProposal = new EmptyProposal(); /** * The text for the empty proposal, or <code>null</code> to use the default text. * * @since 3.2 */ private String fEmptyMessage = null; private void handleSetData(Event event) { TableItem item = (TableItem) event.item; int index = fProposalTable.indexOf(item); boolean outputRelevance = IdeLog.isInfoEnabled(UIEplPlugin.getDefault(), IUiEplScopes.RELEVANCE); if (0 <= index && index < fFilteredProposals.length) { ICompletionProposal current = fFilteredProposals[index]; String entry = current.getDisplayString().trim(); if (outputRelevance) { int relevance = 0; if (current instanceof ICommonCompletionProposal) { relevance = ((ICommonCompletionProposal) current).getRelevance(); } entry += StringUtil.format( JFaceTextMessages.getString("CompletionProposalPopup.RelevancePercentage"), relevance); //$NON-NLS-1$ } item.setImage(current.getImage()); item.setText(0, entry); item.setData(current); if (current instanceof ICommonCompletionProposal) { ICommonCompletionProposal proposal = (ICommonCompletionProposal) current; String location = proposal.getFileLocation(); Image[] images = proposal.getUserAgentImages(); int userAgentCount = fContentAssistant.getUserAgentColumnCount(); if (images != null) { for (int j = 0; j < images.length; j++) { Image image = images[j]; item.setImage(j + 1, image); } } else { for (int j = 0; j < userAgentCount; j++) { item.setImage(j + 1, null); } } if (userAgentCount > 0) { item.setText(userAgentCount + 1, " " + location); //$NON-NLS-1$ } else { item.setText(userAgentCount + 1, location); } } } else { // this should not happen, but does on win32 } } /** * Resizes the table to match the internal items */ private void resizeTable(int objectColumn, int locationColumn) { // Try/catch is fix for LH where we are strangely getting an ArrayIndexOutOfBounds exception // Not entirely sure how it's happening: https://aptana.lighthouseapp.com/projects/35272/tickets/2017 try { fProposalTable.setRedraw(false); int height = (fProposalTable.getItemHeight() * Math.min(fFilteredProposals.length, PROPOSAL_ITEMS_VISIBLE)); if (fProposalTable.getHorizontalBar() != null) { height += fProposalTable.getHorizontalBar().getSize().y; } fProposalTable.setLayoutData( GridDataFactory.fillDefaults().hint(SWT.DEFAULT, height).grab(true, true).create()); for (int j = 1; j < fProposalTable.getColumnCount() - 1; j++) { // User agent images are 16px. Adding a few px for padding fProposalTable.getColumn(j).setWidth(22); } TableColumn lastColumn = fProposalTable.getColumn(fProposalTable.getColumnCount() - 1); lastColumn.setWidth(locationColumn); fProposalTable.getColumn(0).setWidth(objectColumn); fProposalTable.setRedraw(true); fProposalShell.pack(true); } catch (ArrayIndexOutOfBoundsException e) { IdeLog.logError(UIEplPlugin.getDefault(), JFaceTextMessages.getString("CompletionProposalPopup.Error_Resizing_Popup"), e); //$NON-NLS-1$ } } /** * Gets the interior width of the CA proposal table * * @return */ private int getTableWidth() { Rectangle area = fProposalShell.getClientArea(); Point preferredSize = fProposalTable.computeSize(SWT.DEFAULT, SWT.DEFAULT); int width = area.width - 2 * fProposalTable.getBorderWidth(); if (preferredSize.y > area.height + fProposalTable.getHeaderHeight()) { // Subtract the scrollbar width from the total column width // if a vertical scrollbar will be required if (fProposalTable.getVerticalBar() != null) { Point vBarSize = fProposalTable.getVerticalBar().getSize(); width -= vBarSize.x; } } // We subtract an extra 2 pixels because it seems this calculation overestimates // the table width a tiny bit. This might be a calculatable value. width -= 2; return width; } /** * Returns the proposal selected in the proposal selector. * * @return the selected proposal * @since 2.0 */ private ICompletionProposal getSelectedProposal() { if (fProposalTable == null || fProposalTable.isDisposed()) { return null; } int i = fProposalTable.getSelectionIndex(); if (fFilteredProposals == null || i < 0 || i >= fFilteredProposals.length) { return null; } return fFilteredProposals[i]; } /** * Takes the selected proposal and applies it. * * @param stateMask * the state mask * @since 2.1 */ private boolean selectProposalWithMask(int stateMask) { ICompletionProposal p = getSelectedProposal(); hide(); if (p == null) { return false; } insertProposal(p, (char) 0, stateMask, fContentAssistSubjectControlAdapter.getSelectedRange().x); return true; } /** * Applies the given proposal at the given offset. The given character is the one that triggered the insertion of * this proposal. * * @param p * the completion proposal * @param trigger * the trigger character * @param stateMask * the state mask * @param offset * the offset * @since 2.1 */ private void insertProposal(ICompletionProposal p, char trigger, int stateMask, final int offset) { fInserting = true; IRewriteTarget target = null; IEditingSupport helper = new IEditingSupport() { public boolean isOriginator(DocumentEvent event, IRegion focus) { return focus.getOffset() <= offset && focus.getOffset() + focus.getLength() >= offset; } public boolean ownsFocusShell() { return false; } }; try { IDocument document = fContentAssistSubjectControlAdapter.getDocument(); if (fViewer instanceof ITextViewerExtension) { ITextViewerExtension extension = (ITextViewerExtension) fViewer; target = extension.getRewriteTarget(); } if (target != null) { target.beginCompoundChange(); } if (fViewer instanceof IEditingSupportRegistry) { IEditingSupportRegistry registry = (IEditingSupportRegistry) fViewer; registry.register(helper); } if (p instanceof ICompletionProposalExtension2 && fViewer != null) { ICompletionProposalExtension2 e = (ICompletionProposalExtension2) p; e.apply(fViewer, trigger, stateMask, offset); } else if (p instanceof ICompletionProposalExtension) { ICompletionProposalExtension e = (ICompletionProposalExtension) p; e.apply(document, trigger, offset); } else { p.apply(document); } Point selection = p.getSelection(document); if (selection != null) { fContentAssistSubjectControlAdapter.setSelectedRange(selection.x, selection.y); fContentAssistSubjectControlAdapter.revealRange(selection.x, selection.y); } IContextInformation info = p.getContextInformation(); if (info != null) { int contextInformationOffset; if (p instanceof ICompletionProposalExtension) { ICompletionProposalExtension e = (ICompletionProposalExtension) p; contextInformationOffset = e.getContextInformationPosition(); } else { if (selection == null) { selection = fContentAssistSubjectControlAdapter.getSelectedRange(); } contextInformationOffset = selection.x + selection.y; } fContentAssistant.showContextInformation(info, contextInformationOffset); } else { fContentAssistant.showContextInformation(null, -1); } } finally { if (target != null) { target.endCompoundChange(); } if (fViewer instanceof IEditingSupportRegistry) { IEditingSupportRegistry registry = (IEditingSupportRegistry) fViewer; registry.unregister(helper); } fInserting = false; } } /** * Returns whether this popup has the focus. * * @return <code>true</code> if the popup has the focus */ public boolean hasFocus() { if (fPopupCloser != null && fPopupCloser.isAdditionalInfoInFocus()) { // TISTUD-913 return true; } if (Helper.okToUse(fProposalShell)) { return (fProposalShell.isFocusControl() || fProposalTable.isFocusControl()); } return false; } /** * Hides this popup. */ public void hide() { fLastKeyPressed = '\0'; unregister(); if (fViewer instanceof IEditingSupportRegistry) { IEditingSupportRegistry registry = (IEditingSupportRegistry) fViewer; registry.unregister(fFocusHelper); } if (Helper.okToUse(fProposalShell)) { fContentAssistant.removeContentAssistListener(this, ContentAssistant.PROPOSAL_SELECTOR); // TISTUD-913: moved the 'fPopupCloser.uninstall();' to disposePopup() if (fAdditionalInfoController != null) { fAdditionalInfoController.disposeInformationControl(); } // TISTUD-1550: Call to dispose, instead of fProposalShell.setVisible(false); fProposalShell.dispose(); } } /** * Disposes the popup */ public void disposePopup() { if (fProposalShell != null && !fProposalShell.isDisposed()) { fProposalShell.dispose(); } try { if (projectScopeNode != null) { projectScopeNode.removePreferenceChangeListener(prefListener); projectScopeNode = null; } if (instanceScopeNode != null) { instanceScopeNode.removePreferenceChangeListener(prefListener); instanceScopeNode = null; } } catch (IllegalStateException e) { // ignores } if (fPopupCloser != null) { // TISTUD-913 fPopupCloser.uninstall(); } prefListener = null; } /** * Unregister this completion proposal popup. * * @since 3.0 */ private void unregister() { if (fDocumentListener != null) { IDocument document = fContentAssistSubjectControlAdapter.getDocument(); if (document != null) { document.removeDocumentListener(fDocumentListener); } fDocumentListener = null; } fDocumentEvents.clear(); if (fKeyListener != null && Helper.okToUse(fContentAssistSubjectControlAdapter.getControl())) { fContentAssistSubjectControlAdapter.removeKeyListener(fKeyListener); fKeyListener = null; } if (fLastProposal != null) { if (fLastProposal instanceof ICompletionProposalExtension2 && fViewer != null) { ICompletionProposalExtension2 extension = (ICompletionProposalExtension2) fLastProposal; extension.unselected(fViewer); } fLastProposal = null; } fFilteredProposals = null; fComputedProposals = null; fContentAssistant.possibleCompletionsClosed(); } /** * Returns whether this popup is active. It is active if the proposal selector is visible. * * @return <code>true</code> if this popup is active */ public boolean isActive() { return Helper.okToUse(fProposalShell) && fProposalShell.isVisible(); } /** * Initializes the proposal selector with these given proposals. * * @param proposals * the proposals * @param isFilteredSubset * if <code>true</code>, the proposal table is not cleared, but the proposals that are not in the passed * array are removed from the displayed set */ private void setProposals(ICompletionProposal[] proposals, boolean isFilteredSubset) { ICompletionProposal[] oldProposals = fFilteredProposals; ICompletionProposal oldProposal = getSelectedProposal(); // may trigger filtering and a reentrant call to // setProposals() if (oldProposals != fFilteredProposals) // reentrant call was first - abort return; if (Helper.okToUse(fProposalTable)) { if (oldProposal instanceof ICompletionProposalExtension2 && fViewer != null) ((ICompletionProposalExtension2) oldProposal).unselected(fViewer); if (proposals == null) { proposals = new ICompletionProposal[] {}; } fFilteredProposals = proposals; final int newLen = proposals.length; if (USE_VIRTUAL) { fProposalTable.setItemCount(newLen); fProposalTable.clearAll(); } else { fProposalTable.setRedraw(false); fProposalTable.setItemCount(newLen); TableItem[] items = fProposalTable.getItems(); for (int i = 0; i < items.length; i++) { TableItem item = items[i]; ICompletionProposal proposal = proposals[i]; item.setText(proposal.getDisplayString()); item.setImage(proposal.getImage()); item.setData(proposal); } fProposalTable.setRedraw(true); } // Custom code for modifying selection/size int defaultIndex = -1; int suggestedIndex = -1; // select the first proposal if (proposals.length > 0) { defaultIndex = 0; suggestedIndex = 0; } String longestString = StringUtil.EMPTY; String longestLoc = StringUtil.EMPTY; for (int i = 0; i < proposals.length; i++) { ICompletionProposal proposal = proposals[i]; String entry = proposal.getDisplayString().trim(); if (entry.length() > longestString.length()) { longestString = entry; } if (proposal instanceof ICommonCompletionProposal) { ICommonCompletionProposal prop = (ICommonCompletionProposal) proposal; String loc = prop.getFileLocation(); if (loc.length() > longestLoc.length()) { longestLoc = loc; } } } int objWidth = getStringWidth(longestString); int locWidth = getStringWidth(longestLoc); objWidth = Math.min(objWidth, MAX_PROPOSAL_COLUMN_WIDTH); locWidth = Math.min(locWidth, MAX_LOCATION_COLUMN_WIDTH); if (!isFilteredSubset) { resizeTable(objWidth, locWidth); } modifySelection(defaultIndex, suggestedIndex); } } /** * Returns the width of the string in pixels * * @param string * @return */ protected int getStringWidth(String string) { String measureString = "M" + string + "MM"; //$NON-NLS-1$ //$NON-NLS-2$ GC gc = new GC(fProposalTable.getShell()); Point extent = gc.stringExtent(measureString); int width = extent.x; gc.dispose(); return width; } protected void modifySelection(int defaultIndex, int suggestedIndex) { // IM changed this to deselect the table if there is no default selection if (defaultIndex != -1) { this.selectProposal(defaultIndex, false, true); } else if (suggestedIndex != -1) { this.fProposalTable.deselectAll(); this.setScroll(suggestedIndex); } else if (Helper.okToUse(fProposalTable)) { if (fLastKeyPressed == '\b' && defaultIndex == -1) { hide(); } else { this.fProposalTable.setTopIndex(0); this.fProposalTable.deselectAll(); } } } /** * Returns the graphical location at which this popup should be made visible. * * @param width * @param height * @return the location of this popup */ /* * private Point getLocation(int width, int height) { // get character index into the source for the current caret * position int caret = fContentAssistSubjectControlAdapter.getCaretOffset(); // get coordinate of caret position * Point clientPoint = fContentAssistSubjectControlAdapter.getLocationAtOffset(caret); // make sure clientPoint is * within the visible client area if (clientPoint.x < 0) clientPoint.x = 0; if (clientPoint.y < 0) clientPoint.y = * 0; // convert coordinate to screen coordinates Point screenPoint = * fContentAssistSubjectControlAdapter.getControl().toDisplay(clientPoint); Rectangle screenRect = * Display.getCurrent().getClientArea(); int lineHeight = fContentAssistSubjectControlAdapter.getLineHeight(); int x * = screenPoint.x; int y = screenPoint.y + lineHeight + 2; if (x + width > screenRect.width) { // hangs over the * right side of the screen if (screenPoint.x - width > 0) { x = screenPoint.x - width; } else { // doesn't fit on * the left either // This should only happen on a really small screen or with a large popup } } if (y + height > * screenRect.height) { // doesn't fit below current line // TODO: '7' is a magic number. We really need a reliable * way to get the true height/width of this widget int yOffset = height + lineHeight + 7; if (screenPoint.y - * yOffset > 0) { y = screenPoint.y - yOffset; } else { // doesn't fit above current line either // This should only * happen on a really small screen or with a large popup } } return new Point(x, y); } */ /** * Returns the graphical location at which this popup should be made visible. * * @param proposalBox * @param displayBounds * @param caretLocation * @param lineHeight * @return the location of this popup */ public static Point computeLocation(Rectangle proposalBox, Rectangle displayBounds, Point caretLocation, int lineHeight) { if (caretLocation.y + proposalBox.height > displayBounds.height + displayBounds.y) { return new Point(caretLocation.x, caretLocation.y - (lineHeight + proposalBox.height)); } else { return new Point(caretLocation.x, caretLocation.y + lineHeight); } } /** * Returns the size of this completion proposal popup. * * @return a Point containing the size * @since 3.0 */ Point getSize() { return fSize; } /** * Displays this popup and install the additional info controller, so that additional info is displayed when a * proposal is selected and additional info is available. */ private void displayProposals() { if (!Helper.okToUse(fProposalShell) || !Helper.okToUse(fProposalTable)) { return; } if (fContentAssistant.addContentAssistListener(this, ContentAssistant.PROPOSAL_SELECTOR)) { if (fDocumentListener == null) { fDocumentListener = new IDocumentListener() { public void documentAboutToBeChanged(DocumentEvent event) { if (!fInserting) { fDocumentEvents.add(event); } } public void documentChanged(DocumentEvent event) { if (!fInserting) { filterProposals(); } } }; } IDocument document = fContentAssistSubjectControlAdapter.getDocument(); if (document != null) { document.addDocumentListener(fDocumentListener); } if (fFocusHelper == null) { fFocusHelper = new IEditingSupport() { public boolean isOriginator(DocumentEvent event, IRegion focus) { return false; // this helper just covers the focus change to the proposal // shell, no remote // editions } public boolean ownsFocusShell() { return true; } }; } if (fViewer instanceof IEditingSupportRegistry) { IEditingSupportRegistry registry = (IEditingSupportRegistry) fViewer; registry.register(fFocusHelper); } /* * https://bugs.eclipse.org/bugs/show_bug.cgi?id=52646 on GTK, setVisible and such may run the event loop * (see also https://bugs.eclipse.org/bugs/show_bug.cgi?id=47511) Since the user may have already canceled * the popup or selected an entry (ESC or RETURN), we have to double check whether the table is still * okToUse. See comments below */ fProposalShell.setVisible(true); // may run event loop on GTK // transfer focus since no verify key listener can be attached if (!fContentAssistSubjectControlAdapter.supportsVerifyKeyListener() && Helper.okToUse(fProposalShell)) { fProposalShell.setFocus(); // may run event loop on GTK ?? } if (fAdditionalInfoController != null && Helper.okToUse(fProposalTable)) { fAdditionalInfoController.install(fProposalTable); fAdditionalInfoController.handleTableSelectionChanged(); } } else { hide(); } } /** * @see IContentAssistListener#verifyKey(VerifyEvent) */ public boolean verifyKey(VerifyEvent e) { if (!Helper.okToUse(fProposalShell)) { return true; } char key = e.character; fLastKeyPressed = e.character; if (key == 0) { int newSelection = fProposalTable.getSelectionIndex(); int visibleRows = (fProposalTable.getSize().y / fProposalTable.getItemHeight()) - 1; boolean smartToggle = false; switch (e.keyCode) { case SWT.ARROW_LEFT: case SWT.ARROW_RIGHT: // filterProposals(); hide(); return true; case SWT.ARROW_UP: newSelection -= 1; if (newSelection < 0) { newSelection = fProposalTable.getItemCount() - 1; } break; case SWT.ARROW_DOWN: newSelection += 1; if (newSelection > fProposalTable.getItemCount() - 1) { newSelection = 0; } break; case SWT.PAGE_DOWN: newSelection += visibleRows; if (newSelection >= fProposalTable.getItemCount()) { newSelection = fProposalTable.getItemCount() - 1; } break; case SWT.PAGE_UP: newSelection -= visibleRows; if (newSelection < 0) { newSelection = 0; } break; case SWT.HOME: hide(); return true; // newSelection= 0; // break; // case SWT.END: hide(); return true; // newSelection= fProposalTable.getItemCount() - 1; // break; default: if (e.keyCode != SWT.CAPS_LOCK && e.keyCode != SWT.MOD1 && e.keyCode != SWT.MOD2 && e.keyCode != SWT.MOD3 && e.keyCode != SWT.MOD4) { hide(); } return true; } selectProposal(newSelection, smartToggle, false); e.doit = false; return false; } // key != 0 switch (key) { case '\t': if (!_insertOnTab) { e.doit = true; hide(); break; } else { if (selectProposalWithMask(e.stateMask)) { e.doit = false; break; } else { return true; } } case 0x1B: // Esc e.doit = false; hide(); break; case '\n': // Ctrl-Enter on w2k case '\r': // Enter if (selectProposalWithMask(e.stateMask)) { e.doit = false; break; } else { return true; } default: ICompletionProposal p = getSelectedProposal(); if (p instanceof ICompletionProposalExtension) { ICompletionProposalExtension t = (ICompletionProposalExtension) p; char[] triggers = t.getTriggerCharacters(); if (contains(triggers, key)) { // we do this inside the 'if' for performance reasons boolean triggerEnabled = true; if (p instanceof ICommonCompletionProposal) { triggerEnabled = ((ICommonCompletionProposal) p).validateTrigger( fContentAssistSubjectControlAdapter.getDocument(), fFilterOffset, e); } if (triggerEnabled) { e.doit = false; hide(); insertProposal(p, key, e.stateMask, fContentAssistSubjectControlAdapter.getSelectedRange().x); } } } } return true; } /** * Selects the entry with the given index in the proposal selector and feeds the selection to the additional info * controller. * * @param index * the index in the list * @param smartToggle * <code>true</code> if the smart toggle key has been pressed * @param autoScroll * Do we scroll the item into view at the middle of the list * @since 2.1 */ private void selectProposal(int index, boolean smartToggle, boolean autoScroll) { if (fFilteredProposals == null) { return; } ICompletionProposal oldProposal = getSelectedProposal(); if (oldProposal instanceof ICompletionProposalExtension2 && fViewer != null) { ((ICompletionProposalExtension2) oldProposal).unselected(fViewer); } ICompletionProposal proposal = fFilteredProposals[index]; if (proposal instanceof ICompletionProposalExtension2 && fViewer != null) { ((ICompletionProposalExtension2) proposal).selected(fViewer, smartToggle); } fLastProposal = proposal; fProposalTable.setSelection(index); if (autoScroll) { setScroll(index); } fProposalTable.showSelection(); if (fAdditionalInfoController != null) { fAdditionalInfoController.handleTableSelectionChanged(); } } /** * Sets the postition of the table * * @param index */ private void setScroll(int index) { int topIndex = index - 4 > 0 ? index - 4 : 0; fProposalTable.setTopIndex(topIndex); } /** * Returns whether the given character is contained in the given array of characters. * * @param characters * the list of characters * @param c * the character to look for in the list * @return <code>true</code> if character belongs to the list * @since 2.0 */ private boolean contains(char[] characters, char c) { if (characters == null) { return false; } for (int i = 0; i < characters.length; i++) { if (c == characters[i]) { return true; } } return false; } /** * @see org.eclipse.jface.text.IEventConsumer#processEvent(org.eclipse.swt.events.VerifyEvent) */ public void processEvent(VerifyEvent e) { } /** * Filters the displayed proposal based on the given cursor position and the offset of the original invocation of * the code assistant. */ private void filterProposals() { ++fInvocationCounter; final Control control = fContentAssistSubjectControlAdapter.getControl(); control.getDisplay().asyncExec(new Runnable() { final long fCounter = fInvocationCounter; public void run() { if (fCounter != fInvocationCounter) { return; } if (control.isDisposed()) { return; } int offset = fContentAssistSubjectControlAdapter.getSelectedRange().x; ICompletionProposal[] proposals = null; try { if (offset > -1) { DocumentEvent event = TextUtilities.mergeProcessedDocumentEvents(fDocumentEvents); proposals = computeFilteredProposals(offset, event); } } catch (BadLocationException x) { } finally { fDocumentEvents.clear(); } fFilterOffset = offset; if (proposals != null && proposals.length > 0) { fContentAssistant.promoteKeyListener(); setProposals(proposals, fIsFilteredSubset); } else { hide(); } } }); } /** * Computes the subset of already computed proposals that are still valid for the given offset. * * @param offset * the offset * @param event * the merged document event * @return the set of filtered proposals * @since 3.0 */ private ICompletionProposal[] computeFilteredProposals(int offset, DocumentEvent event) { if (offset == fInvocationOffset && event == null) { fIsFilteredSubset = false; return fComputedProposals; } if (offset < fInvocationOffset) { fIsFilteredSubset = false; fInvocationOffset = offset; fComputedProposals = computeProposals(fInvocationOffset, false); return fComputedProposals; } ICompletionProposal[] proposals; if (offset < fFilterOffset) { proposals = fComputedProposals; fIsFilteredSubset = false; } else { proposals = fFilteredProposals; fIsFilteredSubset = true; } if (proposals == null) { fIsFilteredSubset = false; return null; } IDocument document = fContentAssistSubjectControlAdapter.getDocument(); // this does go through the array twice (once to figure out if it's okay to use the else case, and the second // time to actual filter the proposals, but it is what the original logic suggests for (int i = 0; i < proposals.length; i++) { ICompletionProposal proposal = proposals[i]; if (!(proposal instanceof ICompletionProposalExtension2) && !(proposal instanceof ICompletionProposalExtension)) { // restore original behavior fIsFilteredSubset = false; fInvocationOffset = offset; fComputedProposals = computeProposals(fInvocationOffset, false); return fComputedProposals; } } ICompletionProposal[] filtered = filterProposals(proposals, document, offset, event); return filtered; } /** * Filters the list of proposals to only those that are valid in the current context of the document event * * @param proposals * @param document * @param offset * @param event * @return */ private ICompletionProposal[] filterProposals(ICompletionProposal[] proposals, IDocument document, int offset, DocumentEvent event) { int length = proposals == null ? 0 : proposals.length; List<ICompletionProposal> filtered = new ArrayList<ICompletionProposal>(length); for (int i = 0; i < length; i++) { ICompletionProposal proposal = proposals[i]; if (proposal instanceof ICompletionProposalExtension2) { ICompletionProposalExtension2 p = (ICompletionProposalExtension2) proposal; if (p.validate(document, offset, event)) { filtered.add(proposal); } } else if (proposal instanceof ICompletionProposalExtension) { ICompletionProposalExtension p = (ICompletionProposalExtension) proposal; if (p.isValidFor(document, offset)) { filtered.add(proposal); } } } IdeLog.logInfo(UIEplPlugin.getDefault(), MessageFormat.format("Filtered list to {0} proposals", filtered.size()), //$NON-NLS-1$ IUiEplScopes.CONTENT_ASSIST); return filtered.toArray(new ICompletionProposal[filtered.size()]); } /** * Requests the proposal shell to take focus. * * @since 3.0 */ public void setFocus() { if (Helper.okToUse(fProposalShell)) { fProposalShell.setFocus(); } } /** * Returns <code>true</code> if <code>proposal</code> should be auto-inserted, <code>false</code> otherwise. * * @param proposal * the single proposal that might be automatically inserted * @return <code>true</code> if <code>proposal</code> can be inserted automatically, <code>false</code> otherwise * @since 3.1 */ private boolean canAutoInsert(ICompletionProposal proposal) { if (fContentAssistant.isAutoInserting()) { if (proposal instanceof ICompletionProposalExtension4) { ICompletionProposalExtension4 ext = (ICompletionProposalExtension4) proposal; return ext.isAutoInsertable(); } return true; // default behavior before ICompletionProposalExtension4 was introduced } return false; } /** * Completes the common prefix of all proposals directly in the code. If no common prefix can be found, the proposal * popup is shown. * * @return an error message if completion failed. * @since 3.0 */ public String incrementalComplete() { if (Helper.okToUse(fProposalShell) && fFilteredProposals != null) { completeCommonPrefix(); } else { final Control control = fContentAssistSubjectControlAdapter.getControl(); if (fKeyListener == null) { fKeyListener = new ProposalSelectionListener(); } if (!Helper.okToUse(fProposalShell) && !control.isDisposed()) { fContentAssistSubjectControlAdapter.addKeyListener(fKeyListener); } BusyIndicator.showWhile(control.getDisplay(), new Runnable() { public void run() { fInvocationOffset = fContentAssistSubjectControlAdapter.getSelectedRange().x; fFilterOffset = fInvocationOffset; fFilteredProposals = computeProposals(fInvocationOffset, false); int count = (fFilteredProposals == null ? 0 : fFilteredProposals.length); if (count == 0) { // IM turned off for the moment, as it's annoying more than helpful to beep. // control.getDisplay().beep(); hide(); } else if (count == 1 && canAutoInsert(fFilteredProposals[0])) { insertProposal(fFilteredProposals[0], (char) 0, 0, fInvocationOffset); hide(); } else { if (completeCommonPrefix()) { hide(); } else { fComputedProposals = fFilteredProposals; createPopup(); } } } }); } return getErrorMessage(); } /** * Calls the necessary methods to popup the content assist method. This was moved to one single method to unify the * path to show the window and to provide an easy way to time the operations needed to populate and create the * widget. */ private void createPopup() { createProposalSelector(); setProposals(fComputedProposals, false); fContentAssistant.addToLayout(this, fProposalShell, ContentAssistant.LayoutManager.LAYOUT_PROPOSAL_SELECTOR, fContentAssistant.getSelectionOffset()); displayProposals(); } /** * Acts upon <code>fFilteredProposals</code>: if there is just one valid proposal, it is inserted, otherwise, the * common prefix of all proposals is inserted into the document. If there is no common prefix, nothing happens and * <code>false</code> is returned. * * @return <code>true</code> if a single proposal was inserted and the selector can be closed, <code>false</code> if * more than once choice remain * @since 3.0 */ private boolean completeCommonPrefix() { // 0: insert single proposals if (fFilteredProposals.length == 1) { if (canAutoInsert(fFilteredProposals[0])) { insertProposal(fFilteredProposals[0], (char) 0, 0, fFilterOffset); hide(); return true; } return false; } // 1: extract pre- and postfix from all remaining proposals IDocument document = fContentAssistSubjectControlAdapter.getDocument(); // contains the common postfix in the case that there are any proposals matching our LHS StringBuffer rightCasePostfix = null; List<ICompletionProposal> rightCase = new ArrayList<ICompletionProposal>(); // whether to check for non-case compatible matches. This is initially true, and stays so // as long as there are i) no case-sensitive matches and ii) all proposals share the same // (although not corresponding with the document contents) common prefix. boolean checkWrongCase = true; // the prefix of all case insensitive matches. This differs from the document // contents and will be replaced. CharSequence wrongCasePrefix = null; int wrongCasePrefixStart = 0; // contains the common postfix of all case-insensitive matches StringBuffer wrongCasePostfix = null; List<ICompletionProposal> wrongCase = new ArrayList<ICompletionProposal>(); for (int i = 0; i < fFilteredProposals.length; i++) { ICompletionProposal proposal = fFilteredProposals[i]; CharSequence insertion = getPrefixCompletion(proposal); int start = getPrefixCompletionOffset(proposal); try { int prefixLength = fFilterOffset - start; int relativeCompletionOffset = Math.min(insertion.length(), prefixLength); String prefix = document.get(start, prefixLength); if (insertion.toString().startsWith(prefix)) { checkWrongCase = false; rightCase.add(proposal); CharSequence newPostfix = insertion.subSequence(relativeCompletionOffset, insertion.length()); if (rightCasePostfix == null) { rightCasePostfix = new StringBuffer(newPostfix.toString()); } else { truncatePostfix(rightCasePostfix, newPostfix); } } else if (checkWrongCase) { CharSequence newPrefix = insertion.subSequence(0, relativeCompletionOffset); if (isPrefixCompatible(wrongCasePrefix, wrongCasePrefixStart, newPrefix, start, document)) { wrongCasePrefix = newPrefix; wrongCasePrefixStart = start; CharSequence newPostfix = insertion.subSequence(relativeCompletionOffset, insertion.length()); if (wrongCasePostfix == null) { wrongCasePostfix = new StringBuffer(newPostfix.toString()); } else { truncatePostfix(wrongCasePostfix, newPostfix); } wrongCase.add(proposal); } else { checkWrongCase = false; } } } catch (BadLocationException e2) { // bail out silently return false; } if (rightCasePostfix != null && rightCasePostfix.length() == 0 && rightCase.size() > 1) { return false; } } // 2: replace single proposals if (rightCase.size() == 1) { ICompletionProposal proposal = rightCase.get(0); if (canAutoInsert(proposal)) { insertProposal(proposal, (char) 0, 0, fInvocationOffset); hide(); return true; } return false; } else if (checkWrongCase && wrongCase.size() == 1) { ICompletionProposal proposal = wrongCase.get(0); if (canAutoInsert(proposal)) { insertProposal(proposal, (char) 0, 0, fInvocationOffset); hide(); return true; } return false; } // 3: replace post- / prefixes CharSequence prefix; if (checkWrongCase) { prefix = wrongCasePrefix; } else { prefix = ""; //$NON-NLS-1$ } CharSequence postfix; if (checkWrongCase) { postfix = wrongCasePostfix; } else { postfix = rightCasePostfix; } if (prefix == null || postfix == null) { return false; } try { // 4: check if parts of the postfix are already in the document int to = Math.min(document.getLength(), fFilterOffset + postfix.length()); StringBuffer inDocument = new StringBuffer(document.get(fFilterOffset, to - fFilterOffset)); truncatePostfix(inDocument, postfix); // 5: replace and reveal document.replace(fFilterOffset - prefix.length(), prefix.length() + inDocument.length(), prefix.toString() + postfix.toString()); fContentAssistSubjectControlAdapter.setSelectedRange(fFilterOffset + postfix.length(), 0); fContentAssistSubjectControlAdapter.revealRange(fFilterOffset + postfix.length(), 0); return false; } catch (BadLocationException e) { // ignore and return false return false; } } /** * @since 3.1 */ private boolean isPrefixCompatible(CharSequence oneSequence, int oneOffset, CharSequence twoSequence, int twoOffset, IDocument document) throws BadLocationException { if (oneSequence == null || twoSequence == null) { return true; } int min = Math.min(oneOffset, twoOffset); int oneEnd = oneOffset + oneSequence.length(); int twoEnd = twoOffset + twoSequence.length(); String one = document.get(oneOffset, min - oneOffset) + oneSequence + document.get(oneEnd, Math.min(fFilterOffset, fFilterOffset - oneEnd)); String two = document.get(twoOffset, min - twoOffset) + twoSequence + document.get(twoEnd, Math.min(fFilterOffset, fFilterOffset - twoEnd)); return one.equals(two); } /** * Truncates <code>buffer</code> to the common prefix of <code>buffer</code> and <code>sequence</code>. * * @param buffer * the common postfix to truncate * @param sequence * the characters to truncate with */ private void truncatePostfix(StringBuffer buffer, CharSequence sequence) { // find common prefix int min = Math.min(buffer.length(), sequence.length()); for (int c = 0; c < min; c++) { if (sequence.charAt(c) != buffer.charAt(c)) { buffer.delete(c, buffer.length()); return; } } // all equal up to minimum buffer.delete(min, buffer.length()); } /** * Extracts the completion offset of an <code>ICompletionProposal</code>. If <code>proposal</code> is a * <code>ICompletionProposalExtension3</code>, its <code>getCompletionOffset</code> method is called, otherwise, the * invocation offset of this popup is shown. * * @param proposal * the proposal to extract the offset from * @return the proposals completion offset, or <code>fInvocationOffset</code> * @since 3.1 */ private int getPrefixCompletionOffset(ICompletionProposal proposal) { if (proposal instanceof ICompletionProposalExtension3) { return ((ICompletionProposalExtension3) proposal) .getPrefixCompletionStart(fContentAssistSubjectControlAdapter.getDocument(), fFilterOffset); } return fInvocationOffset; } /** * Extracts the replacement string from an <code>ICompletionProposal</code>. If <code>proposal</code> is a * <code>ICompletionProposalExtension3</code>, its <code>getCompletionText</code> method is called, otherwise, the * display string is used. * * @param proposal * the proposal to extract the text from * @return the proposals completion text * @since 3.1 */ private CharSequence getPrefixCompletion(ICompletionProposal proposal) { CharSequence insertion = null; if (proposal instanceof ICompletionProposalExtension3) { insertion = ((ICompletionProposalExtension3) proposal) .getPrefixCompletionText(fContentAssistSubjectControlAdapter.getDocument(), fFilterOffset); } if (insertion == null) { insertion = proposal.getDisplayString(); } return insertion; } /** * Gets the activation key * * @return char */ public char getActivationKey() { return fActivationKey; } /** * Sets the activation key * * @param activationKey */ public void setActivationKey(char activationKey) { fActivationKey = activationKey; } /** * The empty proposal displayed if there is nothing else to show. * * @since 3.2 */ private static final class EmptyProposal implements ICompletionProposal, ICompletionProposalExtension, ICompletionProposalExtension4 { String fDisplayString; int fOffset; /* * @see ICompletionProposal#apply(IDocument) */ public void apply(IDocument document) { } /* * @see ICompletionProposal#getSelection(IDocument) */ public Point getSelection(IDocument document) { return new Point(fOffset, 0); } /* * @see ICompletionProposal#getContextInformation() */ public IContextInformation getContextInformation() { return null; } /* * @see ICompletionProposal#getImage() */ public Image getImage() { return null; } /* * @see ICompletionProposal#getDisplayString() */ public String getDisplayString() { return fDisplayString; } /* * @see ICompletionProposal#getAdditionalProposalInfo() */ public String getAdditionalProposalInfo() { return null; } /* * @see * org.eclipse.jface.text.contentassist.ICompletionProposalExtension#apply(org.eclipse.jface.text.IDocument, * char, int) */ public void apply(IDocument document, char trigger, int offset) { } /* * @see * org.eclipse.jface.text.contentassist.ICompletionProposalExtension#isValidFor(org.eclipse.jface.text.IDocument * , int) */ public boolean isValidFor(IDocument document, int offset) { return false; } /* * @see org.eclipse.jface.text.contentassist.ICompletionProposalExtension#getTriggerCharacters() */ public char[] getTriggerCharacters() { return null; } /* * @see org.eclipse.jface.text.contentassist.ICompletionProposalExtension#getContextInformationPosition() */ public int getContextInformationPosition() { return -1; } /* * @see org.eclipse.jface.text.contentassist.ICompletionProposalExtension#isAutoInsertable() */ public boolean isAutoInsertable() { return false; } } }