blob: 94ce3d8b8b4f36f045685fc374de3acca4e86ff9 [file] [log] [blame]
/*******************************************************************************
* Copyright (c) 2000, 2018 IBM Corporation and others.
*
* This program and the accompanying materials
* are made available under the terms of the Eclipse Public License 2.0
* which accompanies this distribution, and is available at
* https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*
* Contributors:
* IBM Corporation - initial API and implementation
* Sean Montgomery, sean_montgomery@comcast.net - https://bugs.eclipse.org/bugs/show_bug.cgi?id=116454
* Marcel Bruch, bruch@cs.tu-darmstadt.de - [content assist] Allow to re-sort proposals - https://bugs.eclipse.org/bugs/show_bug.cgi?id=350991
* Terry Parker, tparker@google.com - Protect against poorly behaved completion proposers - http://bugs.eclipse.org/429925
* Lars Vogel <Lars.Vogel@vogella.com> - Bug 493649
* Mickael Istria (Red Hat Inc.) - [251156] Allow multiple contentAssitProviders internally & inheritance
*******************************************************************************/
package org.eclipse.jface.text.contentassist;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import org.eclipse.osgi.util.TextProcessor;
import org.eclipse.swt.SWT;
import org.eclipse.swt.custom.BusyIndicator;
import org.eclipse.swt.custom.StyleRange;
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.FocusEvent;
import org.eclipse.swt.events.FocusListener;
import org.eclipse.swt.events.KeyAdapter;
import org.eclipse.swt.events.KeyEvent;
import org.eclipse.swt.events.KeyListener;
import org.eclipse.swt.events.MouseAdapter;
import org.eclipse.swt.events.MouseEvent;
import org.eclipse.swt.events.SelectionEvent;
import org.eclipse.swt.events.SelectionListener;
import org.eclipse.swt.events.TraverseEvent;
import org.eclipse.swt.events.TraverseListener;
import org.eclipse.swt.events.VerifyEvent;
import org.eclipse.swt.graphics.Color;
import org.eclipse.swt.graphics.Font;
import org.eclipse.swt.graphics.FontData;
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.Display;
import org.eclipse.swt.widgets.Event;
import org.eclipse.swt.widgets.Label;
import org.eclipse.swt.widgets.Listener;
import org.eclipse.swt.widgets.Monitor;
import org.eclipse.swt.widgets.Shell;
import org.eclipse.swt.widgets.Table;
import org.eclipse.swt.widgets.TableItem;
import org.eclipse.core.commands.AbstractHandler;
import org.eclipse.core.commands.ExecutionEvent;
import org.eclipse.core.commands.ExecutionException;
import org.eclipse.core.commands.IHandler;
import org.eclipse.core.runtime.Assert;
import org.eclipse.core.runtime.ILog;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.Platform;
import org.eclipse.core.runtime.Status;
import org.eclipse.jface.bindings.keys.KeySequence;
import org.eclipse.jface.bindings.keys.SWTKeySupport;
import org.eclipse.jface.contentassist.IContentAssistSubjectControl;
import org.eclipse.jface.internal.text.InformationControlReplacer;
import org.eclipse.jface.internal.text.TableOwnerDrawSupport;
import org.eclipse.jface.preference.JFacePreferences;
import org.eclipse.jface.resource.JFaceColors;
import org.eclipse.jface.resource.JFaceResources;
import org.eclipse.jface.util.Geometry;
import org.eclipse.jface.util.Util;
import org.eclipse.jface.viewers.StyledString;
import org.eclipse.jface.text.AbstractInformationControlManager;
import org.eclipse.jface.text.AbstractInformationControlManager.Anchor;
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.IInformationControl;
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;
/**
* 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
* @see org.eclipse.jface.text.contentassist.AdditionalInfoController
*/
class CompletionProposalPopup implements IContentAssistListener {
/**
* Completion proposal selection handler.
*
* @since 3.4
*/
final class ProposalSelectionHandler extends AbstractHandler {
/**
* Selection operation codes.
*/
static final int SELECT_NEXT= 1;
static final int SELECT_PREVIOUS= 2;
private final int fOperationCode;
/**
* Creates a new selection handler.
*
* @param operationCode the operation code
* @since 3.4
*/
public ProposalSelectionHandler(int operationCode) {
Assert.isLegal(operationCode == SELECT_NEXT || operationCode == SELECT_PREVIOUS);
fOperationCode= operationCode;
}
@Override
public Object execute(ExecutionEvent event) throws ExecutionException {
int itemCount= fProposalTable.getItemCount();
int selectionIndex= fProposalTable.getSelectionIndex();
switch (fOperationCode) {
case SELECT_NEXT:
selectionIndex+= 1;
if (selectionIndex > itemCount - 1)
selectionIndex= 0;
break;
case SELECT_PREVIOUS:
selectionIndex-= 1;
if (selectionIndex < 0)
selectionIndex= itemCount - 1;
break;
default:
break;
}
selectProposal(selectionIndex, false);
return null;
}
}
/**
* The empty proposal displayed if there is nothing else to show.
*
* @since 3.2
*/
private static final class EmptyProposal implements ICompletionProposal, ICompletionProposalExtension {
String fDisplayString;
int fOffset;
@Override
public void apply(IDocument document) {
}
@Override
public Point getSelection(IDocument document) {
return new Point(fOffset, 0);
}
@Override
public IContextInformation getContextInformation() {
return null;
}
@Override
public Image getImage() {
return null;
}
@Override
public String getDisplayString() {
return fDisplayString;
}
@Override
public String getAdditionalProposalInfo() {
return null;
}
@Override
public void apply(IDocument document, char trigger, int offset) {
}
@Override
public boolean isValidFor(IDocument document, int offset) {
return false;
}
@Override
public char[] getTriggerCharacters() {
return null;
}
@Override
public int getContextInformationPosition() {
return -1;
}
}
final class ProposalSelectionListener implements KeyListener {
@Override
public void keyPressed(KeyEvent e) {
if (!Helper.okToUse(fProposalShell))
return;
if (e.character == 0 && e.keyCode == SWT.CTRL) {
// http://dev.eclipse.org/bugs/show_bug.cgi?id=34754
int index= fProposalTable.getSelectionIndex();
if (index >= 0)
selectProposal(index, true);
}
}
@Override
public void keyReleased(KeyEvent e) {
if (!Helper.okToUse(fProposalShell))
return;
if (e.character == 0 && e.keyCode == SWT.CTRL) {
// http://dev.eclipse.org/bugs/show_bug.cgi?id=34754
int index= fProposalTable.getSelectionIndex();
if (index >= 0)
selectProposal(index, false);
}
}
}
private final class CommandKeyListener extends KeyAdapter {
private final KeySequence fCommandSequence;
private CommandKeyListener(KeySequence keySequence) {
fCommandSequence= keySequence;
}
@Override
public void keyPressed(KeyEvent e) {
if (!Helper.okToUse(fProposalShell))
return;
int accelerator= SWTKeySupport.convertEventToUnmodifiedAccelerator(e);
KeySequence sequence= KeySequence.getInstance(SWTKeySupport.convertAcceleratorToKeyStroke(accelerator));
if (sequence.equals(fCommandSequence))
if (fContentAssistant.isPrefixCompletionEnabled())
incrementalComplete();
else
showProposals(false);
}
}
/** The associated text viewer. */
ITextViewer fViewer;
/** The associated content assistant. */
final ContentAssistant fContentAssistant;
/** The used additional info controller, or <code>null</code> if none. */
private final AdditionalInfoController fAdditionalInfoController;
/** The closing strategy for this completion proposal popup. */
private final PopupCloser fPopupCloser= new PopupCloser();
/** The popup shell. */
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. */
ProposalSelectionListener fKeyListener;
/** List of document events used for filtering proposals. */
private final List<DocumentEvent> fDocumentEvents= new ArrayList<>();
/** Listener filling the document event queue. */
private IDocumentListener fDocumentListener;
/** The filter list of proposals. */
List<ICompletionProposal> fFilteredProposals;
/** The computed list of proposals. */
List<ICompletionProposal> fComputedProposals;
/** The offset for which the proposals have been computed. */
int fInvocationOffset;
/** The offset for which the computed proposals have been filtered. */
int fFilterOffset;
/**
* The most recently selected proposal.
* @since 3.0
*/
private ICompletionProposal fLastProposal;
/**
* The content assist subject control.
* This replaces <code>fViewer</code>
*
* @since 3.0
*/
IContentAssistSubjectControl fContentAssistSubjectControl;
/**
* The content assist subject control adapter.
* This replaces <code>fViewer</code>
*
* @since 3.0
*/
final 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;
/**
* The filter runnable.
*
* @since 3.1.1
*/
private final Runnable fFilterRunnable= new Runnable() {
@Override
public void run() {
if (!fIsFilterPending)
return;
fIsFilterPending= false;
if (!Helper.okToUse(fContentAssistSubjectControlAdapter.getControl()))
return;
int offset= fContentAssistSubjectControlAdapter.getSelectedRange().x;
List<ICompletionProposal> proposals= null;
DocumentEvent event= null;
try {
if (offset > -1) {
event= TextUtilities.mergeProcessedDocumentEvents(fDocumentEvents);
proposals= computeFilteredProposals(offset, event);
}
} catch (BadLocationException x) {
} finally {
fDocumentEvents.clear();
}
fFilterOffset= offset;
if (proposals != null && proposals.size() > 0)
setProposals(proposals, fIsFilteredSubset);
else {
hide();
if (fContentAssistant.isAutoActivation() && offset > 0 && event != null) {
try {
char charBeforeOffset= event.getDocument().getChar(offset - 1);
if (fContentAssistant.isAutoActivationTriggerChar(charBeforeOffset)) {
fContentAssistant.fireSessionBeginEvent(true);
showProposals(true);
}
} catch (BadLocationException e) {
}
}
}
}
};
/**
* <code>true</code> if <code>fFilterRunnable</code> has been
* posted, <code>false</code> if not.
*
* @since 3.1.1
*/
private boolean fIsFilterPending= false;
/**
* The info message at the bottom of the popup, or <code>null</code> for no popup (if
* ContentAssistant does not provide one).
*
* @since 3.2
*/
private Label fMessageText;
/**
* The font used for <code>fMessageText</code> or null; dispose when done.
*
* @since 3.2
*/
private Font fMessageTextFont;
/**
* The most recent completion offset (used to determine repeated invocation)
*
* @since 3.2
*/
int fLastCompletionOffset;
/**
* 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;
/**
* Tells whether colored labels support is enabled.
* Only valid while the popup is active.
*
* @since 3.4
*/
private boolean fIsColoredLabelsSupportEnabled= false;
/**
* The sorter to be used for sorting the proposals or <code>null</code> if no sorting is
* requested.
*
* @since 3.8
*/
ICompletionProposalSorter fSorter;
/**
* Set to true by {@link #computeProposals(int)} when initial sorting is performed on the
* computed proposals using {@link #fSorter}.
*
* @since 3.11
*/
boolean fIsInitialSort;
/**
* Creates a new completion proposal popup for the given elements.
*
* @param contentAssistant the content assistant feeding this popup
* @param viewer the viewer on top of which this popup appears
* @param infoController the information control collaborating with this popup, or <code>null</code>
* @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 content assistant feeding this popup
* @param contentAssistSubjectControl the content assist subject control on top of which this popup appears
* @param infoController the information control collaborating with this popup, or <code>null</code>
* @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 (!Helper.okToUse(fProposalShell) && 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(), () -> {
fInvocationOffset= fContentAssistSubjectControlAdapter.getSelectedRange().x;
fFilterOffset= fInvocationOffset;
fLastCompletionOffset= fFilterOffset;
fComputedProposals= computeProposals(fInvocationOffset);
int count= (fComputedProposals == null ? 0 : fComputedProposals.size());
if (count == 0 && hideWhenNoProposals(autoActivated))
return;
if (count == 1 && !autoActivated && canAutoInsert(fComputedProposals.get(0))) {
insertProposal(fComputedProposals.get(0), (char) 0, 0, fInvocationOffset);
hide();
} else {
createProposalSelector();
setProposals(fComputedProposals, false);
displayProposals();
}
});
} else {
fLastCompletionOffset= fFilterOffset;
handleRepeatedInvocation();
}
return getErrorMessage();
}
/**
* Hides the popup and returns <code>true</code> if the popup is configured
* to never display an empty list. Returns <code>false</code> otherwise.
*
* @param autoActivated whether the invocation was auto-activated
* @return <code>false</code> if an empty list should be displayed, <code>true</code> otherwise
* @since 3.2
*/
boolean hideWhenNoProposals(boolean autoActivated) {
if (autoActivated || !fContentAssistant.isShowEmptyList()) {
if (!autoActivated) {
Control control= fContentAssistSubjectControlAdapter.getControl();
if (control != null && !control.isDisposed())
control.getDisplay().beep();
}
hide();
return true;
}
return false;
}
/**
* If content assist is set up to handle cycling, then the proposals are recomputed. Otherwise,
* nothing happens.
*
* @since 3.2
*/
void handleRepeatedInvocation() {
if (fContentAssistant.isRepeatedInvocationMode()) {
fComputedProposals= computeProposals(fFilterOffset);
setProposals(fComputedProposals, false);
}
}
/**
* Returns the completion proposals available at the given offset of the viewer's document.
* Delegates the work to the content assistant. Sorts the computed proposals if sorting is
* requested with {@link #fSorter}.
*
* @param offset the offset
* @return the completion proposals available at this offset, never null
*/
List<ICompletionProposal> computeProposals(int offset) {
ICompletionProposal[] completionProposals;
if (fContentAssistSubjectControl != null) {
completionProposals= fContentAssistant.computeCompletionProposals(fContentAssistSubjectControl, offset);
} else {
completionProposals= fContentAssistant.computeCompletionProposals(fViewer, offset);
}
if (completionProposals == null) {
return Collections.emptyList();
}
List<ICompletionProposal> proposals= Arrays.asList(completionProposals);
if (fSorter != null) {
sortProposals(proposals);
fIsInitialSort= true;
}
return proposals;
}
/**
* Returns the error message.
*
* @return the error message
*/
String getErrorMessage() {
return fContentAssistant.getErrorMessage();
}
/**
* Creates the proposal selector.
*/
void createProposalSelector() {
if (Helper.okToUse(fProposalShell))
return;
Control control= fContentAssistSubjectControlAdapter.getControl();
fProposalShell= new Shell(control.getShell(), SWT.ON_TOP | SWT.RESIZE );
fProposalShell.setFont(JFaceResources.getDefaultFont());
fProposalTable= new Table(fProposalShell, SWT.H_SCROLL | SWT.V_SCROLL | SWT.VIRTUAL);
Listener listener= new Listener() {
@Override
public void handleEvent(Event event) {
handleSetData(event);
}
};
fProposalTable.addListener(SWT.SetData, listener);
fIsColoredLabelsSupportEnabled= fContentAssistant.isColoredLabelsSupportEnabled();
if (fIsColoredLabelsSupportEnabled)
TableOwnerDrawSupport.install(fProposalTable);
fProposalTable.setLocation(0, 0);
if (fAdditionalInfoController != null)
fAdditionalInfoController.setSizeConstraints(50, 10, true, true);
GridLayout layout= new GridLayout();
layout.marginWidth= 0;
layout.marginHeight= 0;
layout.verticalSpacing= 1;
fProposalShell.setLayout(layout);
if (fContentAssistant.isStatusLineVisible()) {
createMessageText();
}
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() * 15;
// use golden ratio as default aspect ratio
final double aspectRatio= (1 + Math.sqrt(5)) / 2;
int width= (int) (height * aspectRatio);
// Make sure our bounds still fit to the screen
Monitor monitor= Util.getClosestMonitor(fProposalShell.getDisplay(), getLocation());
Rectangle bounds= monitor.getClientArea();
width= Math.min(width, bounds.width / 4);
height= Math.min(height, bounds.height / 4);
Rectangle trim= fProposalTable.computeTrim(0, 0, width, height);
data.heightHint= trim.height;
data.widthHint= trim.width;
fProposalTable.setLayoutData(data);
fProposalShell.pack();
}
fContentAssistant.addToLayout(this, fProposalShell, ContentAssistant.LayoutManager.LAYOUT_PROPOSAL_SELECTOR, fContentAssistant.getSelectionOffset());
fProposalShell.addControlListener(new ControlListener() {
@Override
public void controlMoved(ControlEvent e) {}
@Override
public void controlResized(ControlEvent e) {
if (fAdditionalInfoController != null) {
// reset the cached resize constraints
fAdditionalInfoController.setSizeConstraints(50, 10, true, false);
fAdditionalInfoController.hideInformationControl();
fAdditionalInfoController.handleTableSelectionChanged();
}
fSize= fProposalShell.getSize();
}
});
Color background= getBackgroundColor(control);
if (background == null) {
background= JFaceColors.getInformationViewerBackgroundColor(Display.getDefault());
}
Color foreground= getForegroundColor(control);
if (foreground == null) {
foreground= JFaceColors.getInformationViewerBackgroundColor(Display.getDefault());
}
fProposalShell.setBackground(background);
fProposalTable.setBackground(background);
fProposalTable.setForeground(foreground);
fProposalTable.addSelectionListener(new SelectionListener() {
@Override
public void widgetSelected(SelectionEvent e) {}
@Override
public void widgetDefaultSelected(SelectionEvent e) {
insertSelectedProposalWithMask(e.stateMask);
}
});
fPopupCloser.install(fContentAssistant, fProposalTable, fAdditionalInfoController);
fProposalShell.addDisposeListener(new DisposeListener() {
@Override
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);
}
/**
* Returns the minimal required height for the proposal, may return 0 if the popup has not been
* created yet.
*
* @return the minimal height
* @since 3.3
*/
int getMinimalHeight() {
int height= 0;
if (Helper.okToUse(fProposalTable)) {
int items= fProposalTable.getItemHeight() * 10;
Rectangle trim= fProposalTable.computeTrim(0, 0, SWT.DEFAULT, items);
height= trim.height;
}
if (Helper.okToUse(fMessageText))
height+= fMessageText.getSize().y + 1;
return height;
}
/**
* Adds command support to the given control.
*
* @param control the control to watch for focus
* @since 3.2
*/
private void addCommandSupport(final Control control) {
final KeySequence commandSequence= fContentAssistant.getRepeatedInvocationKeySequence();
if (commandSequence != null && !commandSequence.isEmpty() && fContentAssistant.isRepeatedInvocationMode()) {
control.addFocusListener(new FocusListener() {
private CommandKeyListener fCommandKeyListener;
@Override
public void focusGained(FocusEvent e) {
if (Helper.okToUse(control)) {
if (fCommandKeyListener == null) {
fCommandKeyListener= new CommandKeyListener(commandSequence);
fProposalTable.addKeyListener(fCommandKeyListener);
}
}
}
@Override
public void focusLost(FocusEvent e) {
if (fCommandKeyListener != null) {
control.removeKeyListener(fCommandKeyListener);
fCommandKeyListener= null;
}
}
});
}
if (fAdditionalInfoController != null) {
control.addFocusListener(new FocusListener() {
private TraverseListener fTraverseListener;
@Override
public void focusGained(FocusEvent e) {
if (Helper.okToUse(control)) {
if (fTraverseListener == null) {
fTraverseListener= new TraverseListener() {
@Override
public void keyTraversed(TraverseEvent event) {
if (event.detail == SWT.TRAVERSE_TAB_NEXT) {
IInformationControl iControl= fAdditionalInfoController.getCurrentInformationControl2();
if (fAdditionalInfoController.getInternalAccessor().canReplace(iControl)) {
fAdditionalInfoController.getInternalAccessor().replaceInformationControl(true);
event.doit= false;
}
}
}
};
fProposalTable.addTraverseListener(fTraverseListener);
}
}
}
@Override
public void focusLost(FocusEvent e) {
if (fTraverseListener != null) {
control.removeTraverseListener(fTraverseListener);
fTraverseListener= 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;
}
/**
* Creates the caption line under the proposal table.
*
* @since 3.2
*/
private void createMessageText() {
if (fMessageText == null) {
fMessageText= new Label(fProposalShell, SWT.RIGHT);
GridData textData= new GridData(SWT.FILL, SWT.BOTTOM, true, false);
fMessageText.setLayoutData(textData);
fMessageText.setText(fContentAssistant.getStatusMessage() + " "); //$NON-NLS-1$
if (fMessageTextFont == null) {
Font font= fMessageText.getFont();
Display display= fProposalShell.getDisplay();
FontData[] fontDatas= font.getFontData();
for (FontData fontData : fontDatas)
fontData.setHeight(fontData.getHeight() * 9 / 10);
fMessageTextFont= new Font(display, fontDatas);
}
fMessageText.setFont(fMessageTextFont);
fMessageText.setBackground(getBackgroundColor(fProposalShell));
fMessageText.setForeground(getForegroundColor(fProposalShell));
if (fContentAssistant.isRepeatedInvocationMode()) {
fMessageText.setCursor(fProposalShell.getDisplay().getSystemCursor(SWT.CURSOR_HAND));
fMessageText.addMouseListener(new MouseAdapter() {
@Override
public void mouseUp(MouseEvent e) {
fLastCompletionOffset= fFilterOffset;
fProposalTable.setFocus();
handleRepeatedInvocation();
}
@Override
public void mouseDown(MouseEvent e) {
}
});
}
}
}
/*
* @since 3.1
*/
private void handleSetData(Event event) {
TableItem item= (TableItem) event.item;
int index= fProposalTable.indexOf(item);
if (0 <= index && index < fFilteredProposals.size()) {
ICompletionProposal current= fFilteredProposals.get(index);
String displayString;
StyleRange[] styleRanges= null;
Image image= null;
try {
if (fIsColoredLabelsSupportEnabled && current instanceof ICompletionProposalExtension7 && Helper.okToUse(fProposalShell)) {
BoldStylerProvider boldStylerProvider= fContentAssistant.getBoldStylerProvider();
if (boldStylerProvider == null) {
boldStylerProvider= new BoldStylerProvider(fProposalShell.getFont());
fContentAssistant.setBoldStylerProvider(boldStylerProvider);
}
StyledString styledString= ((ICompletionProposalExtension7) current).getStyledDisplayString(fContentAssistSubjectControlAdapter.getDocument(), fFilterOffset,
boldStylerProvider);
displayString= styledString.getString();
styleRanges= styledString.getStyleRanges();
} else if (fIsColoredLabelsSupportEnabled && current instanceof ICompletionProposalExtension6) {
StyledString styledString= ((ICompletionProposalExtension6) current).getStyledDisplayString();
displayString= styledString.getString();
styleRanges= styledString.getStyleRanges();
} else {
displayString= current.getDisplayString();
}
} catch (RuntimeException e) {
// On failures to retrieve the proposal's text, insert a dummy entry and log the error.
displayString= JFaceTextMessages.getString("CompletionProposalPopup.error_retrieving_proposal"); //$NON-NLS-1$
String PLUGIN_ID= "org.eclipse.jface.text"; //$NON-NLS-1$
ILog log= Platform.getLog(Platform.getBundle(PLUGIN_ID));
log.log(new Status(IStatus.ERROR, PLUGIN_ID, IStatus.OK, JFaceTextMessages.getString("CompletionProposalPopup.unexpected_error"), e)); //$NON-NLS-1$
}
try {
image= current.getImage();
} catch (RuntimeException e) {
// If we are unable to retrieve the proposal's image, leave it blank.
}
item.setText(displayString);
if (fIsColoredLabelsSupportEnabled)
TableOwnerDrawSupport.storeStyleRanges(item, 0, styleRanges);
item.setImage(image);
item.setData(current);
} else {
// this should not happen, but does on win32
}
}
/**
* Returns the proposal selected in the proposal selector.
*
* @return the selected proposal
* @since 2.0
*/
private ICompletionProposal getSelectedProposal() {
/* Make sure that there is no filter runnable pending.
* See https://bugs.eclipse.org/bugs/show_bug.cgi?id=31427
*/
if (fIsFilterPending)
fFilterRunnable.run();
// filter runnable may have hidden the proposals
if (!Helper.okToUse(fProposalTable))
return null;
int i= fProposalTable.getSelectionIndex();
if (fFilteredProposals == null || i < 0 || i >= fFilteredProposals.size())
return null;
return fFilteredProposals.get(i);
}
/**
* Takes the selected proposal and applies it.
*
* @param stateMask the state mask
* @since 3.2
*/
private void insertSelectedProposalWithMask(int stateMask) {
ICompletionProposal p= getSelectedProposal();
hide();
if (p != null)
insertProposal(p, (char) 0, stateMask, fContentAssistSubjectControlAdapter.getSelectedRange().x);
}
/**
* 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
*/
void insertProposal(ICompletionProposal p, char trigger, int stateMask, final int offset) {
fInserting= true;
IRewriteTarget target= null;
IEditingSupport helper= new IEditingSupport() {
@Override
public boolean isOriginator(DocumentEvent event, IRegion focus) {
return focus.getOffset() <= offset && focus.getOffset() + focus.getLength() >= offset;
}
@Override
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);
}
fireAppliedEvent(p);
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 (Helper.okToUse(fProposalShell)) {
if ((fProposalShell.getDisplay().getActiveShell() == fProposalShell))
return true;
/*
* We have to delegate this query to the additional info controller
* as well, since the content assistant is the widget token owner
* and its closer does not know that the additional info control can
* now also take focus.
*/
if (fAdditionalInfoController != null) {
IInformationControl informationControl= fAdditionalInfoController.getCurrentInformationControl2();
if (informationControl != null && informationControl.isFocusControl())
return true;
InformationControlReplacer replacer= fAdditionalInfoController.getInternalAccessor().getInformationControlReplacer();
if (replacer != null) {
informationControl= replacer.getCurrentInformationControl2();
if (informationControl != null && informationControl.isFocusControl())
return true;
}
}
}
return false;
}
/**
* Hides this popup.
*/
public void hide() {
unregister();
if (fViewer instanceof IEditingSupportRegistry) {
IEditingSupportRegistry registry= (IEditingSupportRegistry) fViewer;
registry.unregister(fFocusHelper);
}
if (Helper.okToUse(fProposalShell)) {
fContentAssistant.removeContentAssistListener(this, ContentAssistant.PROPOSAL_SELECTOR);
fPopupCloser.uninstall();
fProposalShell.setVisible(false);
fProposalShell.dispose();
fProposalShell= null;
}
if (fMessageTextFont != null) {
fMessageTextFont.dispose();
fMessageTextFont= null;
}
if (fMessageText != null) {
fMessageText= null;
}
fEmptyMessage= null;
fLastCompletionOffset= -1;
fContentAssistant.fireSessionEndEvent();
}
/**
* 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 && fContentAssistSubjectControlAdapter.getControl() != null && !fContentAssistSubjectControlAdapter.getControl().isDisposed()) {
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 fProposalShell != null && !fProposalShell.isDisposed();
}
/**
* Initializes the proposal selector with these given proposals. If a proposal sorter is
* configured, the given proposals are sorted before.
*
* @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
*/
void setProposals(List<ICompletionProposal> proposals, boolean isFilteredSubset) {
List<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.isEmpty()) {
fEmptyProposal.fOffset= fFilterOffset;
fEmptyProposal.fDisplayString= fEmptyMessage != null ? fEmptyMessage : JFaceTextMessages.getString("CompletionProposalPopup.no_proposals"); //$NON-NLS-1$
proposals= Collections.singletonList(fEmptyProposal);
}
if (fSorter != null && !fIsInitialSort) {
sortProposals(proposals);
}
fIsInitialSort= false;
fFilteredProposals= proposals;
final int newLen= proposals.size();
fProposalTable.clearAll();
fProposalTable.setItemCount(newLen);
Point currentLocation= fProposalShell.getLocation();
Point newLocation= getLocation();
if ((newLocation.x < currentLocation.x && newLocation.y == currentLocation.y) || newLocation.y < currentLocation.y)
fProposalShell.setLocation(newLocation);
selectProposal(0, false);
}
}
/**
* Returns the graphical location at which this popup should be made visible.
*
* @return the location of this popup
*/
private Point getLocation() {
int caret= fContentAssistSubjectControlAdapter.getCaretOffset();
Rectangle location= fContentAssistant.getLayoutManager().computeBoundsBelowAbove(fProposalShell, fSize == null ? fProposalShell.getSize() : fSize, caret, this);
return Geometry.getLocation(location);
}
/**
* 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.
*/
void displayProposals() {
if (!Helper.okToUse(fProposalShell) || !Helper.okToUse(fProposalTable))
return;
if (fContentAssistant.addContentAssistListener(this, ContentAssistant.PROPOSAL_SELECTOR)) {
ensureDocumentListenerInstalled();
if (fFocusHelper == null) {
fFocusHelper= new IEditingSupport() {
@Override
public boolean isOriginator(DocumentEvent event, IRegion focus) {
return false; // this helper just covers the focus change to the proposal shell, no remote editions
}
@Override
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();
}
/**
* Installs the document listener if not already done.
*
* @since 3.2
*/
void ensureDocumentListenerInstalled() {
if (fDocumentListener == null) {
fDocumentListener= new IDocumentListener() {
@Override
public void documentAboutToBeChanged(DocumentEvent event) {
if (!fInserting)
fDocumentEvents.add(event);
}
@Override
public void documentChanged(DocumentEvent event) {
if (!fInserting)
filterProposals();
}
};
IDocument document= fContentAssistSubjectControlAdapter.getDocument();
if (document != null)
document.addDocumentListener(fDocumentListener);
}
}
@Override
public boolean verifyKey(VerifyEvent e) {
if (!Helper.okToUse(fProposalShell))
return true;
char key= e.character;
if (key == 0) {
int newSelection= fProposalTable.getSelectionIndex();
int visibleRows= (fProposalTable.getSize().y / fProposalTable.getItemHeight()) - 1;
int itemCount= fProposalTable.getItemCount();
switch (e.keyCode) {
case SWT.ARROW_LEFT :
case SWT.ARROW_RIGHT :
filterProposals();
return true;
case SWT.ARROW_UP :
newSelection -= 1;
if (newSelection < 0)
newSelection= itemCount - 1;
break;
case SWT.ARROW_DOWN :
newSelection += 1;
if (newSelection > itemCount - 1)
newSelection= 0;
break;
case SWT.PAGE_DOWN :
newSelection += visibleRows;
if (newSelection >= itemCount)
newSelection= itemCount - 1;
break;
case SWT.PAGE_UP :
newSelection -= visibleRows;
if (newSelection < 0)
newSelection= 0;
break;
case SWT.HOME :
newSelection= 0;
break;
case SWT.END :
newSelection= itemCount - 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, (e.stateMask & SWT.CTRL) != 0);
e.doit= false;
return false;
}
// key != 0
switch (key) {
case 0x1B: // Esc
e.doit= false;
hide();
break;
case '\n': // Ctrl-Enter on w2k
case '\r': // Enter
e.doit= false;
insertSelectedProposalWithMask(e.stateMask);
break;
case '\t':
e.doit= false;
fProposalShell.setFocus();
return false;
default:
if (fContentAssistant.isCompletionProposalTriggerCharsEnabled()) {
ICompletionProposal p= getSelectedProposal();
if (p instanceof ICompletionProposalExtension) {
ICompletionProposalExtension t= (ICompletionProposalExtension) p;
char[] triggers= t.getTriggerCharacters();
if (contains(triggers, key)) {
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
* @since 2.1
*/
private void selectProposal(int index, boolean smartToggle) {
ICompletionProposal oldProposal= getSelectedProposal();
if (oldProposal instanceof ICompletionProposalExtension2 && fViewer != null)
((ICompletionProposalExtension2) oldProposal).unselected(fViewer);
if (fFilteredProposals == null) {
fireSelectionEvent(null, smartToggle);
return;
}
ICompletionProposal proposal= fFilteredProposals.get(index);
if (proposal instanceof ICompletionProposalExtension2 && fViewer != null)
((ICompletionProposalExtension2) proposal).selected(fViewer, smartToggle);
fireSelectionEvent(proposal, smartToggle);
fLastProposal= proposal;
fProposalTable.setSelection(index);
fProposalTable.showSelection();
if (fAdditionalInfoController != null)
fAdditionalInfoController.handleTableSelectionChanged();
}
/**
* Fires a selection event, see {@link ICompletionListener}.
*
* @param proposal the selected proposal, possibly <code>null</code>
* @param smartToggle true if the smart toggle is on
* @since 3.2
*/
private void fireSelectionEvent(ICompletionProposal proposal, boolean smartToggle) {
fContentAssistant.fireSelectionEvent(proposal, smartToggle);
}
/**
* Fires an event after applying the given proposal, see {@link ICompletionListenerExtension2}.
*
* @param proposal the applied proposal
* @since 3.8
*/
private void fireAppliedEvent(ICompletionProposal proposal) {
fContentAssistant.fireAppliedEvent(proposal);
}
/**
* 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 (char character : characters) {
if (c == character)
return true;
}
return false;
}
@Override
public void processEvent(VerifyEvent e) {
}
/**
* Filters the displayed proposal based on the given cursor position and the
* offset of the original invocation of the content assistant.
*/
void filterProposals() {
if (!fIsFilterPending) {
fIsFilterPending= true;
Control control= fContentAssistSubjectControlAdapter.getControl();
control.getDisplay().asyncExec(fFilterRunnable);
}
}
/**
* 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
*/
List<ICompletionProposal> computeFilteredProposals(int offset, DocumentEvent event) {
if (offset == fInvocationOffset && event == null) {
fIsFilteredSubset= false;
return fComputedProposals;
}
if (offset < fInvocationOffset) {
fIsFilteredSubset= false;
fInvocationOffset= offset;
fContentAssistant.fireSessionRestartEvent();
fComputedProposals= computeProposals(fInvocationOffset);
return fComputedProposals;
}
List<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();
int length= proposals.size();
List<ICompletionProposal> filtered= new ArrayList<>(length);
for (ICompletionProposal proposal : proposals) {
if (proposal instanceof ICompletionProposalExtension2) {
ICompletionProposalExtension2 p= (ICompletionProposalExtension2) proposal;
try {
if (p.validate(document, offset, event))
filtered.add(proposal);
} catch (RuntimeException e) {
// Make sure that poorly behaved completion proposers do not break filtering.
}
} else if (proposal instanceof ICompletionProposalExtension) {
ICompletionProposalExtension p= (ICompletionProposalExtension) proposal;
try {
if (p.isValidFor(document, offset))
filtered.add(proposal);
} catch (RuntimeException e) {
// Make sure that poorly behaved completion proposers do not break filtering.
}
} else {
// restore original behavior
fIsFilteredSubset= false;
fInvocationOffset= offset;
fContentAssistant.fireSessionRestartEvent();
fComputedProposals= computeProposals(fInvocationOffset);
return fComputedProposals;
}
}
return filtered;
}
/**
* 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
*/
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) {
if (fLastCompletionOffset == fFilterOffset) {
handleRepeatedInvocation();
} else {
fLastCompletionOffset= fFilterOffset;
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(), () -> {
fInvocationOffset= fContentAssistSubjectControlAdapter.getSelectedRange().x;
fFilterOffset= fInvocationOffset;
fLastCompletionOffset= fFilterOffset;
fFilteredProposals= computeProposals(fInvocationOffset);
int count= (fFilteredProposals == null ? 0 : fFilteredProposals.size());
if (count == 0 && hideWhenNoProposals(false))
return;
if (count == 1 && canAutoInsert(fFilteredProposals.get(0))) {
insertProposal(fFilteredProposals.get(0), (char) 0, 0, fInvocationOffset);
hide();
} else {
ensureDocumentListenerInstalled();
if (count > 0 && completeCommonPrefix())
hide();
else {
fComputedProposals= fFilteredProposals;
createProposalSelector();
setProposals(fComputedProposals, false);
displayProposals();
}
}
});
}
return getErrorMessage();
}
/**
* 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> otherwise
* @since 3.0
*/
boolean completeCommonPrefix() {
// 0: insert single proposals
if (fFilteredProposals.size() == 1) {
if (canAutoInsert(fFilteredProposals.get(0))) {
insertProposal(fFilteredProposals.get(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
StringBuilder rightCasePostfix= null;
List<ICompletionProposal> rightCase= new ArrayList<>();
boolean isWrongCaseMatch= false;
// 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
StringBuilder wrongCasePostfix= null;
List<ICompletionProposal> wrongCase= new ArrayList<>();
boolean hasMixedProposals= hasMixedProposals();
for (int i= 0; i < fFilteredProposals.size(); i++) {
ICompletionProposal proposal= fFilteredProposals.get(i);
if (!(proposal instanceof ICompletionProposalExtension3))
return false;
int start= ((ICompletionProposalExtension3)proposal).getPrefixCompletionStart(document, fFilterOffset);
CharSequence insertion= ((ICompletionProposalExtension3)proposal).getPrefixCompletionText(document, fFilterOffset);
if (insertion == null)
insertion= TextProcessor.deprocess(proposal.getDisplayString());
try {
int prefixLength= fFilterOffset - start;
int relativeCompletionOffset= Math.min(insertion.length(), prefixLength);
String prefix= document.get(start, prefixLength);
if (!isWrongCaseMatch && insertion.toString().startsWith(prefix) && !hasMixedProposals) {
isWrongCaseMatch= false;
rightCase.add(proposal);
CharSequence newPostfix= insertion.subSequence(relativeCompletionOffset, insertion.length());
if (rightCasePostfix == null)
rightCasePostfix= new StringBuilder(newPostfix.toString());
else
truncatePostfix(rightCasePostfix, newPostfix, false);
} else if (i == 0 || isWrongCaseMatch) {
String insertionStrLowerCase= insertion.toString().toLowerCase();
String prefixLowerCase= prefix.toLowerCase();
boolean isSubstringMatch= !insertionStrLowerCase.startsWith(prefixLowerCase) && insertionStrLowerCase.contains(prefixLowerCase);
CharSequence newPrefix;
if (isSubstringMatch) {
int subStrStart= insertionStrLowerCase.indexOf(prefixLowerCase);
newPrefix= insertion.subSequence(subStrStart, subStrStart + relativeCompletionOffset);
} else {
newPrefix= insertion.subSequence(0, relativeCompletionOffset);
}
if (isPrefixCompatible(wrongCasePrefix, wrongCasePrefixStart, newPrefix, start, document, hasMixedProposals)) {
isWrongCaseMatch= true;
if (insertionStrLowerCase.isEmpty()) {
newPrefix= prefix;
}
if (wrongCasePrefix == null || !hasMixedProposals || !wrongCasePrefix.toString().equalsIgnoreCase(newPrefix.toString())) {
wrongCasePrefix= newPrefix; // ignore casing when there are mixed proposals - don't update if newPrefix differs only in case
}
wrongCasePrefixStart= start;
CharSequence newPostfix;
if (isSubstringMatch) {
int subStrStart= insertionStrLowerCase.indexOf(prefixLowerCase);
newPostfix= insertion.subSequence(subStrStart + prefixLength, insertion.length());
} else {
newPostfix= insertion.subSequence(relativeCompletionOffset, insertion.length());
}
if (wrongCasePostfix == null)
wrongCasePostfix= new StringBuilder(newPostfix.toString());
else
truncatePostfix(wrongCasePostfix, newPostfix, hasMixedProposals);
wrongCase.add(proposal);
} else {
return false;
}
} else
return 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) && rightCasePostfix.length() > 0) {
insertProposal(proposal, (char) 0, 0, fInvocationOffset);
hide();
return true;
}
return false;
} else if (isWrongCaseMatch && 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 (isWrongCaseMatch)
prefix= wrongCasePrefix;
else
prefix= ""; //$NON-NLS-1$
CharSequence postfix;
if (isWrongCaseMatch)
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());
StringBuilder inDocument= new StringBuilder(document.get(fFilterOffset, to - fFilterOffset));
truncatePostfix(inDocument, postfix, hasMixedProposals);
// 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);
fFilterOffset+= postfix.length();
fLastCompletionOffset= fFilterOffset;
return false;
} catch (BadLocationException e) {
// ignore and return false
return false;
}
}
/**
* Checks if {@link #fFilteredProposals} list contains proposals based on different rules
* (prefix and substring match rules). While extracting the common prefix, if substring
* proposals are also present along with prefix proposals (i.e. <code>fFilteredProposals</code>
* list has mixed proposals) then casing of substring matches is ignored for the computation of
* common prefix.
*
* @return <code>true</code> if <code>fFilteredProposals</code> list contains proposals based on
* different rules
*/
private boolean hasMixedProposals() {
IDocument document= fContentAssistSubjectControlAdapter.getDocument();
boolean hasSubstringMatch= false;
boolean hasPrefixMatch= false;
for (ICompletionProposal proposal : fFilteredProposals) {
if (!(proposal instanceof ICompletionProposalExtension3))
return false;
int start= ((ICompletionProposalExtension3) proposal).getPrefixCompletionStart(document, fFilterOffset);
CharSequence insertion= ((ICompletionProposalExtension3) proposal).getPrefixCompletionText(document, fFilterOffset);
if (insertion == null) {
insertion= TextProcessor.deprocess(proposal.getDisplayString());
}
int prefixLength= fFilterOffset - start;
try {
String prefix= document.get(start, prefixLength);
String insertionString= insertion.toString();
if (insertionString.isEmpty() || insertionString.toLowerCase().startsWith(prefix.toLowerCase())) {
hasPrefixMatch= true;
} else if (insertionString.toLowerCase().contains(prefix.toLowerCase())) {
hasSubstringMatch= true;
}
} catch (BadLocationException e) {
return false;
}
if (hasPrefixMatch && hasSubstringMatch) {
return true;
}
}
return false;
}
/*
* @since 3.1
*/
private boolean isPrefixCompatible(CharSequence oneSequence, int oneOffset, CharSequence twoSequence, int twoOffset, IDocument document, boolean ignoreCase) 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 ignoreCase ? one.equalsIgnoreCase(two) : 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
* @param ignoreCase <code>true</code> to ignore case while comparing
*/
private void truncatePostfix(StringBuilder buffer, CharSequence sequence, boolean ignoreCase) {
// find common prefix
int min= Math.min(buffer.length(), sequence.length());
for (int c= 0; c < min; c++) {
boolean matches;
if (ignoreCase) {
matches= Character.toUpperCase(sequence.charAt(c)) == Character.toUpperCase(buffer.charAt(c));
} else {
matches= sequence.charAt(c) == buffer.charAt(c);
}
if (!matches) {
buffer.delete(c, buffer.length());
return;
}
}
// all equal up to minimum
buffer.delete(min, buffer.length());
}
/**
* Sets the message for the repetition affordance text at the bottom of the proposal. Only has
* an effect if {@link ContentAssistant#isRepeatedInvocationMode()} returns <code>true</code>.
*
* @param message the new caption
* @since 3.2
*/
void setMessage(String message) {
Assert.isNotNull(message);
if (isActive() && fMessageText != null && !fMessageText.isDisposed())
fMessageText.setText(message + " "); //$NON-NLS-1$
}
/**
* Sets the text to be displayed if no proposals are available. Only has an effect if
* {@link ContentAssistant#isShowEmptyList()} returns <code>true</code>.
*
* @param message the empty message
* @since 3.2
*/
void setEmptyMessage(String message) {
Assert.isNotNull(message);
fEmptyMessage= message;
}
/**
* Enables or disables showing of the caption line. See also {@link #setMessage(String)}.
*
* @param show <code>true</code> if the status line is visible
* @since 3.2
*/
public void setStatusLineVisible(boolean show) {
if (!isActive() || show == (fMessageText != null && !fMessageText.isDisposed()))
return; // nothing to do
if (show) {
createMessageText();
} else {
fMessageText.dispose();
fMessageText= null;
}
fProposalShell.layout();
}
/**
* Informs the popup that it is being placed above the caret line instead of below.
*
* @param above <code>true</code> if the location of the popup is above the caret line, <code>false</code> if it is below
* @since 3.3
*/
void switchedPositionToAbove(boolean above) {
if (fAdditionalInfoController != null) {
fAdditionalInfoController.setFallbackAnchors(new Anchor[] {
AbstractInformationControlManager.ANCHOR_RIGHT,
AbstractInformationControlManager.ANCHOR_LEFT,
above ? AbstractInformationControlManager.ANCHOR_TOP : AbstractInformationControlManager.ANCHOR_BOTTOM
});
}
}
/**
* Returns a new proposal selection handler.
*
* @param operationCode the operation code
* @return the handler
* @since 3.4
*/
IHandler createProposalSelectionHandler(int operationCode) {
return new ProposalSelectionHandler(operationCode);
}
/**
* Sets the proposal sorter.
*
* @param sorter the sorter to be used, or <code>null</code> if no sorting is requested
* @since 3.8
* @see ContentAssistant#setSorter(ICompletionProposalSorter)
*/
public void setSorter(ICompletionProposalSorter sorter) {
fSorter= sorter;
}
/**
* Sorts the given proposal array.
*
* @param proposals the new proposals to display in the popup window
* @throws NullPointerException if no sorter has been set
* @since 3.8
*/
void sortProposals(final List<ICompletionProposal> proposals) {
proposals.sort(fSorter::compare);
}
}