| /******************************************************************************* |
| * Copyright (c) 2016 Red Hat Inc. 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: |
| * Mickael Istria (Red Hat Inc.) - [251156] async content assist |
| *******************************************************************************/ |
| package org.eclipse.jface.text.contentassist; |
| |
| import java.util.ArrayList; |
| import java.util.Arrays; |
| import java.util.Collection; |
| import java.util.Collections; |
| import java.util.LinkedList; |
| import java.util.List; |
| import java.util.Set; |
| import java.util.concurrent.CompletableFuture; |
| import java.util.concurrent.ExecutionException; |
| import java.util.concurrent.TimeUnit; |
| import java.util.concurrent.TimeoutException; |
| import java.util.concurrent.atomic.AtomicInteger; |
| import java.util.concurrent.atomic.AtomicReference; |
| import java.util.function.Consumer; |
| import java.util.stream.Collectors; |
| |
| import org.eclipse.osgi.util.NLS; |
| |
| import org.eclipse.swt.graphics.Image; |
| import org.eclipse.swt.graphics.Point; |
| import org.eclipse.swt.widgets.Control; |
| |
| import org.eclipse.core.runtime.SafeRunner; |
| |
| import org.eclipse.jface.contentassist.IContentAssistSubjectControl; |
| |
| import org.eclipse.jface.text.BadLocationException; |
| import org.eclipse.jface.text.DocumentEvent; |
| import org.eclipse.jface.text.IDocument; |
| import org.eclipse.jface.text.ITextViewer; |
| import org.eclipse.jface.text.TextUtilities; |
| |
| /** |
| * This class is used to present proposals asynchronously 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. |
| * |
| * @since 3.12 |
| */ |
| class AsyncCompletionProposalPopup extends CompletionProposalPopup { |
| |
| private static final int MAX_WAIT_IN_MS= 50; // TODO make it a preference |
| |
| /** |
| * This is only used and set when populating the dialog is async (ie computation takes more than |
| * MAX_WAIT_IN_MS |
| */ |
| private CompletableFuture<?> fAggregatedPopulateFuture; |
| |
| private Collection<CompletableFuture<?>> toCancelFutures= new LinkedList<>(); |
| |
| private static final class ComputingProposal implements ICompletionProposal, ICompletionProposalExtension { |
| |
| private final int fOffset; |
| private final int fSize; |
| private int fRemaining; |
| |
| ComputingProposal(int offset, int size) { |
| fSize= size; |
| fRemaining = size; |
| fOffset = offset; |
| } |
| |
| @Override |
| public void apply(IDocument document) { |
| // Nothing to do, maybe show some progress report? |
| } |
| |
| @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 NLS.bind(JFaceTextMessages.getString("AsyncCompletionProposalPopup.computing"), Long.valueOf(Math.round(100. * (fSize - fRemaining)/fSize))); //$NON-NLS-1$ |
| } |
| |
| @Override |
| public String getAdditionalProposalInfo() { |
| return NLS.bind(JFaceTextMessages.getString("AsyncCompletionProposalPopup.computingDetails"), new Object[] { //$NON-NLS-1$; |
| Integer.valueOf(fSize), |
| Integer.valueOf(fSize - fRemaining), |
| Integer.valueOf(fRemaining) }); |
| } |
| |
| @Override |
| public void apply(IDocument document, char trigger, int offset) { |
| // Nothing to do |
| } |
| |
| @Override |
| public boolean isValidFor(IDocument document, int offset) { |
| return false; |
| } |
| |
| @Override |
| public char[] getTriggerCharacters() { |
| return null; |
| } |
| |
| @Override |
| public int getContextInformationPosition() { |
| return -1; |
| } |
| |
| void setRemaining(int size) { |
| this.fRemaining = size; |
| } |
| } |
| |
| public AsyncCompletionProposalPopup(ContentAssistant contentAssistant, IContentAssistSubjectControl contentAssistSubjectControl, AdditionalInfoController infoController) { |
| super(contentAssistant, contentAssistSubjectControl, infoController); |
| } |
| |
| public AsyncCompletionProposalPopup(ContentAssistant contentAssistant, ITextViewer viewer, AdditionalInfoController infoController) { |
| super(contentAssistant, viewer, infoController); |
| } |
| |
| /** |
| * This methods differs from its super as it will show the list of proposals that |
| * gets augmented as the {@link IContentAssistProcessor#computeCompletionProposals(ITextViewer, int)} |
| * complete. All computations operation happen in a non-UI Thread so they're not blocking UI. |
| */ |
| @Override |
| public String showProposals(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); |
| |
| fInvocationOffset= fContentAssistSubjectControlAdapter.getSelectedRange().x; |
| fFilterOffset= fInvocationOffset; |
| fLastCompletionOffset= fFilterOffset; |
| // start invocation of processors as Futures, and make them populate the proposals upon completion |
| computeAndPopulateProposals(fInvocationOffset, null, true, autoActivated, true); |
| } else { |
| fLastCompletionOffset= fFilterOffset; |
| handleRepeatedInvocation(); |
| } |
| |
| return getErrorMessage(); |
| } |
| |
| @Override |
| void handleRepeatedInvocation() { |
| cancelFutures(); |
| computeAndPopulateProposals(fInvocationOffset, null, false, false, false); |
| } |
| |
| private void computeAndPopulateProposals(int offset, Consumer<List<ICompletionProposal>> callback, boolean createSelector, boolean autoActivated, boolean autoInsert) { |
| List<CompletableFuture<List<ICompletionProposal>>> computationFutures= buildCompletionFuturesOrJobs(offset); |
| toCancelFutures.addAll(computationFutures); |
| fComputedProposals= Collections.synchronizedList(new ArrayList<>()); |
| List<CompletableFuture<Void>> populateFutures= computationFutures.stream().map(future -> future.thenAccept(fComputedProposals::addAll)).collect(Collectors.toList()); |
| toCancelFutures.addAll(populateFutures); |
| CompletableFuture<?> aggregatedPopulateFuture= CompletableFuture.allOf(populateFutures.toArray(new CompletableFuture[populateFutures.size()])); |
| toCancelFutures.add(aggregatedPopulateFuture); |
| |
| boolean useAsyncMode= false; |
| try { |
| aggregatedPopulateFuture.get(MAX_WAIT_IN_MS, TimeUnit.MILLISECONDS); |
| } catch (TimeoutException e) { |
| useAsyncMode= true; |
| } catch (ExecutionException | InterruptedException ex) { |
| // nothing to do |
| } |
| if (!useAsyncMode) { |
| int count= fComputedProposals.size(); |
| if (count == 0 && hideWhenNoProposals(autoActivated)) { |
| return; |
| } |
| |
| if (autoInsert && count == 1 && !autoActivated && canAutoInsert(fComputedProposals.get(0))) { |
| insertProposal(fComputedProposals.get(0), (char) 0, 0, offset); |
| hide(); |
| } else { |
| if (createSelector) { |
| createProposalSelector(); |
| } |
| if (callback != null) { |
| callback.accept(fComputedProposals); |
| } else { |
| setProposals(fComputedProposals, false); |
| displayProposals(); |
| } |
| } |
| } else { |
| if (createSelector) { |
| createProposalSelector(); |
| } |
| ComputingProposal computingProposal= new ComputingProposal(offset, populateFutures.size()); |
| fComputedProposals.add(0, computingProposal); |
| setProposals(fComputedProposals, false); |
| AtomicInteger remaining= new AtomicInteger(populateFutures.size()); |
| final List<ICompletionProposal> requestSpecificProposals= fComputedProposals; //fComputedProposals can be changed/reset later |
| populateFutures= populateFutures.stream().map(future -> future.thenRun(() -> { |
| computingProposal.setRemaining(remaining.decrementAndGet()); |
| if (remaining.get() == 0) { |
| requestSpecificProposals.remove(computingProposal); |
| } |
| Control control= fContentAssistSubjectControlAdapter.getControl(); |
| if (!control.isDisposed() && offset == fInvocationOffset) { |
| control.getDisplay().asyncExec(() -> { |
| // Skip if offset has changed while runnable was scheduled |
| // nor when completion "session" was modified or canceled. |
| if (offset != fInvocationOffset || fComputedProposals != requestSpecificProposals) { |
| return; |
| } |
| if (autoInsert |
| && !autoActivated |
| && !fComputedProposals.contains(computingProposal) |
| && fComputedProposals.size() == 1 |
| && remaining.get() == 0 |
| && canAutoInsert(fComputedProposals.get(0))) { |
| if (Helper.okToUse(fProposalShell)) { |
| insertProposal(fComputedProposals.get(0), (char) 0, 0, offset); |
| hide(); |
| } |
| return; |
| } |
| if (!fComputedProposals.contains(computingProposal) && callback != null) { |
| callback.accept(fComputedProposals); |
| } else { |
| setProposals(fComputedProposals, false); |
| displayProposals(); |
| } |
| }); |
| } |
| })).collect(Collectors.toList()); |
| toCancelFutures.addAll(populateFutures); |
| fAggregatedPopulateFuture= CompletableFuture.allOf(populateFutures.toArray(new CompletableFuture[populateFutures.size()])); |
| toCancelFutures.add(fAggregatedPopulateFuture); |
| } |
| displayProposals(); |
| } |
| |
| @Override |
| public String incrementalComplete() { |
| cancelFutures(); |
| if (Helper.okToUse(fProposalShell) && fFilteredProposals != null) { |
| return super.incrementalComplete(); |
| } |
| final Control control= fContentAssistSubjectControlAdapter.getControl(); |
| |
| if (fKeyListener == null) |
| fKeyListener= new ProposalSelectionListener(); |
| |
| if (!Helper.okToUse(fProposalShell) && !control.isDisposed()) |
| fContentAssistSubjectControlAdapter.addKeyListener(fKeyListener); |
| |
| fInvocationOffset= fContentAssistSubjectControlAdapter.getSelectedRange().x; |
| fFilterOffset= fInvocationOffset; |
| fLastCompletionOffset= fFilterOffset; |
| |
| computeAndPopulateProposals(fInvocationOffset, (List<ICompletionProposal> proposals) -> { |
| ensureDocumentListenerInstalled(); |
| fFilteredProposals= proposals; |
| if (!proposals.isEmpty() && completeCommonPrefix()) { |
| hide(); |
| } else { |
| setProposals(proposals, false); |
| displayProposals(); |
| } |
| }, true, false, true); |
| return getErrorMessage(); |
| } |
| |
| @Override |
| List<ICompletionProposal> computeProposals(int offset) { |
| if (fProposalShell != null) { |
| fProposalShell.dispose(); |
| } |
| showProposals(true); |
| return fComputedProposals; |
| } |
| |
| @Override |
| void createProposalSelector() { |
| super.createProposalSelector(); |
| fProposalShell.addDisposeListener(e -> cancelFutures()); |
| } |
| |
| void cancelFutures() { |
| toCancelFutures.forEach(future -> future.cancel(true)); |
| toCancelFutures.clear(); |
| } |
| |
| @Override |
| protected List<ICompletionProposal> computeFilteredProposals(int offset, DocumentEvent event) { |
| if (fAggregatedPopulateFuture != null && !fAggregatedPopulateFuture.isDone()) { |
| // user typed a char & computation still pending -> let all futures complete then invoke "filterProposals" upon completion |
| fAggregatedPopulateFuture.thenRun(this::filterProposals); |
| return fComputedProposals; |
| } |
| return super.computeFilteredProposals(offset, event); |
| } |
| |
| @Override |
| public void hide() { |
| super.hide(); |
| cancelFutures(); |
| } |
| |
| protected List<CompletableFuture<List<ICompletionProposal>>> buildCompletionFuturesOrJobs(int invocationOffset) { |
| Set<IContentAssistProcessor> processors = null; |
| try { |
| processors= fContentAssistant.getContentAssistProcessors(getTokenContentType(invocationOffset)); |
| } catch (BadLocationException e) { |
| // ignore |
| } |
| if (processors == null) { |
| return Collections.emptyList(); |
| } |
| List<CompletableFuture<List<ICompletionProposal>>> futures = new ArrayList<>(processors.size()); |
| for (IContentAssistProcessor processor : processors) { |
| futures.add(CompletableFuture.supplyAsync(() -> { |
| AtomicReference<List<ICompletionProposal>> result= new AtomicReference<>(); |
| SafeRunner.run(() -> { |
| ICompletionProposal[] proposals= processor.computeCompletionProposals(fViewer, invocationOffset); |
| if (proposals == null) { |
| result.set(Collections.emptyList()); |
| } else { |
| result.set(Arrays.asList(proposals)); |
| } |
| }); |
| List<ICompletionProposal> proposals= result.get(); |
| if (proposals == null) { // an error occurred during computeCompletionProposal, |
| // possible improvement: give user feedback by returning an error "proposal" shown |
| // in completion popup and providing details |
| return Collections.emptyList(); |
| } |
| return proposals; |
| })); |
| } |
| return futures; |
| } |
| |
| private String getTokenContentType(int invocationOffset) throws BadLocationException { |
| if (fContentAssistSubjectControl != null) { |
| IDocument document= fContentAssistSubjectControl.getDocument(); |
| if (document != null) { |
| return TextUtilities.getContentType(document, fContentAssistant.getDocumentPartitioning(), invocationOffset, true); |
| } |
| } else { |
| return TextUtilities.getContentType(fViewer.getDocument(), fContentAssistant.getDocumentPartitioning(), invocationOffset, true); |
| } |
| return IDocument.DEFAULT_CONTENT_TYPE; |
| } |
| |
| } |