| /******************************************************************************* |
| * 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 static org.eclipse.jface.util.Util.isValid; |
| |
| import java.util.ArrayList; |
| import java.util.Arrays; |
| import java.util.Collections; |
| import java.util.List; |
| import java.util.concurrent.atomic.AtomicBoolean; |
| |
| 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.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.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 { |
| if (fProposalTable.isDisposed()) { |
| return null; |
| } |
| 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 (!isValid(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 (!isValid(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 (!isValid(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.compareAndSet(true, false)) |
| return; |
| |
| if (!isValid(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) { |
| fDocumentEvents.clear(); |
| } |
| fFilterOffset= offset; |
| |
| if (proposals != null && !proposals.isEmpty()) |
| 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 final AtomicBoolean fIsFilterPending= new AtomicBoolean(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 (!isValid(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 (isValid(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= this::handleSetData; |
| 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() * 10; |
| // use 2 x ratio as default aspect ratio instead of (1 + Math.sqrt(5)) / 2 |
| double aspectRatio= 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.getInformationViewerForegroundColor(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(event -> unregister()); |
| |
| 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 (isValid(fProposalTable)) { |
| int items= fProposalTable.getItemHeight() * 10; |
| Rectangle trim= fProposalTable.computeTrim(0, 0, SWT.DEFAULT, items); |
| height= trim.height; |
| } |
| if (isValid(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 (isValid(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 (isValid(control)) { |
| if (fTraverseListener == null) { |
| fTraverseListener= 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 && isValid(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.get()) |
| fFilterRunnable.run(); |
| |
| // filter runnable may have hidden the proposals |
| if (!isValid(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 (isValid(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 (isValid(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 (isValid(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 (!isValid(fProposalShell) || !isValid(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() && isValid(fProposalShell)) |
| fProposalShell.setFocus(); // may run event loop on GTK ?? |
| |
| if (fAdditionalInfoController != null && isValid(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 (!isValid(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.compareAndSet(false, 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) { |
| fDocumentEvents.clear(); |
| |
| 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 (isValid(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 (isValid(fProposalShell) && fFilteredProposals != null) { |
| if (fLastCompletionOffset == fFilterOffset) { |
| handleRepeatedInvocation(); |
| } else { |
| fLastCompletionOffset= fFilterOffset; |
| completeCommonPrefix(); |
| } |
| } else { |
| final Control control= fContentAssistSubjectControlAdapter.getControl(); |
| |
| if (fKeyListener == null) |
| fKeyListener= new ProposalSelectionListener(); |
| |
| if (!isValid(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); |
| } |
| } |