| /******************************************************************************* |
| * 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; |
| } |
| } |