blob: 1dafdf5fd46d9755c8907f0185e9df34ca22ddeb [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 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.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
List<ICompletionProposal> computedProposals = Collections.synchronizedList(new ArrayList<>());
fFutures= buildCompletionFuturesOrJobs(fInvocationOffset);
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 == null ? 0 : computedProposals.size());
if (count == 0 && hideWhenNoProposals(autoActivated))
return null;
if (count == 1 && !autoActivated && canAutoInsert(computedProposals.get(0))) {
insertProposal(computedProposals.get(0), (char) 0, 0, fInvocationOffset);
hide();
} else {
createProposalSelector();
setProposals(computedProposals, false);
displayProposals();
}
} else { // processors took too much time, go asynchronous
createProposalSelector();
ComputingProposal computingProposal= new ComputingProposal(fInvocationOffset, fFutures.size());
computedProposals.add(0, computingProposal);
fComputedProposals = computedProposals;
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(() -> {
setProposals(newProposals, false);
displayProposals();
});
});
}
displayProposals();
}
} else {
fLastCompletionOffset= fFilterOffset;
handleRepeatedInvocation();
}
return getErrorMessage();
}
@Override
public void hide() {
super.hide();
if (fFutures != null) {
for (Future<?> future : fFutures) {
future.cancel(true);
}
}
}
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(() ->
Arrays.asList(processor.computeCompletionProposals(fViewer, invocationOffset))
));
}
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;
}
}