blob: 26f260d4ac48f05b9c062e240da73eaea103eb61 [file] [log] [blame]
/*******************************************************************************
* Copyright (c) 2016 Red Hat Inc. and others.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* 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.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.function.Consumer;
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.swt.widgets.Display;
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
private List<CompletableFuture<List<ICompletionProposal>>> fFutures;
private static final class ComputingProposal implements ICompletionProposal, ICompletionProposalExtension {
private final int fOffset;
private final int fSize;
private int fRemaining;
public 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;
}
public 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
fFutures= buildCompletionFuturesOrJobs(fInvocationOffset);
runFutures(fInvocationOffset, null, true, autoActivated, true);
} else {
fLastCompletionOffset= fFilterOffset;
handleRepeatedInvocation();
}
return getErrorMessage();
}
@Override
void handleRepeatedInvocation() {
cancelFutures();
fFutures= buildCompletionFuturesOrJobs(fInvocationOffset);
runFutures(fInvocationOffset, null, false, false, false);
}
private List<ICompletionProposal> runFutures(int offset, Consumer<List<ICompletionProposal>> callback, boolean createSelector, boolean autoActivated, boolean autoInsert) {
List<ICompletionProposal> computedProposals= Collections.synchronizedList(new ArrayList<>());
List<CompletableFuture<Void>> populateFutures= new ArrayList<>(fFutures.size());
for (CompletableFuture<List<ICompletionProposal>> future : fFutures) {
populateFutures.add(future.thenAccept(proposals -> computedProposals.addAll(proposals)));
}
long requestBeginningTimestamp= System.currentTimeMillis();
long stillRemainingThreeshold= MAX_WAIT_IN_MS;
for (CompletableFuture<?> future : populateFutures) {
try {
future.get(stillRemainingThreeshold, TimeUnit.MILLISECONDS);
} catch (TimeoutException | ExecutionException | InterruptedException ex) {
// future failed or took more time than we want to wait
}
stillRemainingThreeshold= MAX_WAIT_IN_MS - (System.currentTimeMillis() - requestBeginningTimestamp);
if (stillRemainingThreeshold < 0) {
// we already spent too much time (more than MAX_WAIT_IN_MS), stop waiting.
break;
}
}
fComputedProposals= computedProposals;
if (stillRemainingThreeshold > 0) { // everything ready in time, go synchronous
int count= computedProposals.size();
if (count == 0 && hideWhenNoProposals(autoActivated))
return computedProposals;
if (autoInsert && count == 1 && !autoActivated && canAutoInsert(computedProposals.get(0))) {
insertProposal(computedProposals.get(0), (char) 0, 0, offset);
hide();
} else {
if (createSelector) {
createProposalSelector();
}
if (callback != null) {
callback.accept(computedProposals);
} else {
setProposals(computedProposals, false);
displayProposals();
}
}
} else { // processors took too much time, go asynchronous
if (createSelector) {
createProposalSelector();
}
ComputingProposal computingProposal= new ComputingProposal(offset, fFutures.size());
computedProposals.add(0, computingProposal);
setProposals(fComputedProposals, false);
Set<CompletableFuture<Void>> remaining= Collections.synchronizedSet(new HashSet<>(populateFutures));
for (CompletableFuture<Void> populateFuture : populateFutures) {
populateFuture.thenRun(() -> {
remaining.removeIf(CompletableFuture::isDone);
computingProposal.setRemaining(remaining.size());
if (remaining.isEmpty()) {
computedProposals.remove(computingProposal);
}
List<ICompletionProposal> newProposals= new ArrayList<>(computedProposals);
fComputedProposals= newProposals;
Display.getDefault().asyncExec(() -> {
if (autoInsert && !autoActivated && remaining.isEmpty() && newProposals.size() == 1 && canAutoInsert(newProposals.get(0))) {
if (Helper.okToUse(fProposalShell)) {
insertProposal(newProposals.get(0), (char) 0, 0, offset);
hide();
}
return;
}
if (remaining.isEmpty() && callback != null) {
callback.accept(newProposals);
} else {
setProposals(newProposals, false);
displayProposals();
}
});
});
}
displayProposals();
}
return computedProposals;
}
@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;
fFutures= buildCompletionFuturesOrJobs(fInvocationOffset);
fFilteredProposals= runFutures(fInvocationOffset, (List<ICompletionProposal> proposals) -> {
ensureDocumentListenerInstalled();
if (proposals.size() > 0 && completeCommonPrefix()) {
hide();
} else {
fFilteredProposals= proposals;
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() {
if (fFutures != null) {
for (Future<?> future : fFutures) {
future.cancel(true);
}
fFutures= null;
}
}
@Override
protected List<ICompletionProposal> computeFilteredProposals(int offset, DocumentEvent event) {
if(fComputedProposals.size() > 0 && fComputedProposals.get(0) instanceof ComputingProposal) {
Set<CompletableFuture<List<ICompletionProposal>>> remaining = Collections.synchronizedSet(new HashSet<>(fFutures));
for (CompletableFuture<List<ICompletionProposal>> future : fFutures) {
future.thenRun(() -> {
remaining.removeIf(CompletableFuture::isDone);
if (remaining.isEmpty()) {
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(() -> {
ICompletionProposal[] proposals= processor.computeCompletionProposals(fViewer, invocationOffset);
if (proposals == null) {
return Collections.emptyList();
}
return Arrays.asList(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;
}
}