blob: 063882b2faaa81a6a5c90afe4357e085ce8e8426 [file] [log] [blame]
/*******************************************************************************
* Copyright (c) 2016, 2020 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 is available at https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*
* Contributors:
* Mickael Istria (Red Hat Inc.) - initial implementation
* Lucas Bullen (Red Hat Inc.) - Bug 520700 - TextEditors within FormEditors are not supported * Lucas Bullen (Red Hat Inc.) - Refactored for incomplete completion lists
* - Refactored for incomplete completion lists
*******************************************************************************/
package org.eclipse.lsp4e.operations.completion;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.concurrent.CancellationException;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import org.eclipse.core.runtime.OperationCanceledException;
import org.eclipse.jdt.annotation.NonNull;
import org.eclipse.jface.text.BadLocationException;
import org.eclipse.jface.text.IDocument;
import org.eclipse.jface.text.ITextViewer;
import org.eclipse.jface.text.contentassist.CompletionProposal;
import org.eclipse.jface.text.contentassist.ContextInformation;
import org.eclipse.jface.text.contentassist.ContextInformationValidator;
import org.eclipse.jface.text.contentassist.ICompletionProposal;
import org.eclipse.jface.text.contentassist.IContentAssistProcessor;
import org.eclipse.jface.text.contentassist.IContextInformation;
import org.eclipse.jface.text.contentassist.IContextInformationValidator;
import org.eclipse.lsp4e.LSPEclipseUtils;
import org.eclipse.lsp4e.LanguageServerPlugin;
import org.eclipse.lsp4e.LanguageServiceAccessor;
import org.eclipse.lsp4e.ui.Messages;
import org.eclipse.lsp4j.CompletionItem;
import org.eclipse.lsp4j.CompletionList;
import org.eclipse.lsp4j.CompletionOptions;
import org.eclipse.lsp4j.CompletionParams;
import org.eclipse.lsp4j.SignatureHelpOptions;
import org.eclipse.lsp4j.SignatureHelpParams;
import org.eclipse.lsp4j.SignatureInformation;
import org.eclipse.lsp4j.jsonrpc.messages.Either;
import org.eclipse.lsp4j.services.LanguageServer;
import org.eclipse.ui.texteditor.ITextEditor;
import com.google.common.base.Strings;
public class LSContentAssistProcessor implements IContentAssistProcessor {
private static final long TRIGGERS_TIMEOUT = 50;
private static final long CONTEXT_INFORMATION_TIMEOUT = 1000;
private IDocument currentDocument;
private String errorMessage;
private boolean errorAsCompletionItem;
private CompletableFuture<List<@NonNull LanguageServer>> completionLanguageServersFuture;
private final Object completionTriggerCharsSemaphore = new Object();
private char[] completionTriggerChars = new char[0];
private CompletableFuture<List<@NonNull LanguageServer>> contextInformationLanguageServersFuture;
private final Object contextTriggerCharsSemaphore = new Object();
private char[] contextTriggerChars = new char[0];
public LSContentAssistProcessor() {
this(true);
}
public LSContentAssistProcessor(boolean errorAsCompletionItem) {
this.errorAsCompletionItem = errorAsCompletionItem;
}
private Comparator<LSCompletionProposal> proposalComparator = new LSCompletionProposalComparator();
@Override
public ICompletionProposal[] computeCompletionProposals(ITextViewer viewer, int offset) {
IDocument document = viewer.getDocument();
initiateLanguageServers(document);
CompletionParams param;
try {
param = LSPEclipseUtils.toCompletionParams(LSPEclipseUtils.toUri(document), offset, document);
} catch (BadLocationException e) {
LanguageServerPlugin.logError(e);
this.errorMessage = createErrorMessage(offset, e);
return createErrorProposal(offset, e);
}
List<ICompletionProposal> proposals = Collections.synchronizedList(new ArrayList<>());
try {
this.completionLanguageServersFuture
.thenComposeAsync(languageServers -> CompletableFuture.allOf(languageServers.stream()
.map(languageServer -> languageServer.getTextDocumentService().completion(param)
.thenAcceptAsync(completion -> proposals
.addAll(toProposals(document, offset, completion, languageServer))))
.toArray(CompletableFuture[]::new)))
.get();
} catch (ExecutionException e) {
LanguageServerPlugin.logError(e);
this.errorMessage = createErrorMessage(offset, e);
return createErrorProposal(offset, e);
} catch (InterruptedException e) {
LanguageServerPlugin.logError(e);
this.errorMessage = createErrorMessage(offset, e);
Thread.currentThread().interrupt();
return createErrorProposal(offset, e);
}
LSCompletionProposal[] completeProposals = new LSCompletionProposal[proposals.size()];
int i = 0;
for (ICompletionProposal proposal : proposals) {
if (proposal instanceof LSCompletionProposal) {
completeProposals[i] = (LSCompletionProposal) proposal;
i++;
} else {
return proposals.toArray(new ICompletionProposal[proposals.size()]);
}
}
Arrays.sort(completeProposals, proposalComparator);
return completeProposals;
}
private ICompletionProposal[] createErrorProposal(int offset, Exception ex) {
if (errorAsCompletionItem) {
return new ICompletionProposal[] {new CompletionProposal("", offset, 0, 0, null, Messages.completionError, null, ex.getMessage())}; //$NON-NLS-1$
}
else {
return new ICompletionProposal[0];
}
}
private String createErrorMessage(int offset, Exception ex) {
return Messages.completionError + " : " + ex.getMessage(); //$NON-NLS-1$
}
private void initiateLanguageServers(@NonNull IDocument document) {
if (currentDocument != document) {
this.currentDocument = document;
if (this.completionLanguageServersFuture != null) {
try {
this.completionLanguageServersFuture.cancel(true);
} catch (CancellationException ex) {
// nothing
}
}
if (this.contextInformationLanguageServersFuture != null) {
try {
this.contextInformationLanguageServersFuture.cancel(true);
} catch (CancellationException ex) {
// nothing
}
}
this.completionTriggerChars = new char[0];
this.contextTriggerChars = new char[0];
this.completionLanguageServersFuture = LanguageServiceAccessor.getLanguageServers(document,
capabilities -> {
CompletionOptions provider = capabilities.getCompletionProvider();
if (provider != null) {
synchronized (this.completionTriggerCharsSemaphore) {
this.completionTriggerChars = mergeTriggers(this.completionTriggerChars,
provider.getTriggerCharacters());
}
return true;
}
return false;
});
this.contextInformationLanguageServersFuture = LanguageServiceAccessor.getLanguageServers(document,
capabilities -> {
SignatureHelpOptions provider = capabilities.getSignatureHelpProvider();
if (provider != null) {
synchronized (this.contextTriggerCharsSemaphore) {
this.contextTriggerChars = mergeTriggers(this.contextTriggerChars,
provider.getTriggerCharacters());
}
return true;
}
return false;
});
}
}
private static List<ICompletionProposal> toProposals(IDocument document,
int offset, Either<List<CompletionItem>, CompletionList> completionList, LanguageServer languageServer) {
if (completionList == null) {
return Collections.emptyList();
}
List<CompletionItem> items = Collections.emptyList();
boolean isIncomplete = false;
if (completionList.isLeft()) {
items = completionList.getLeft();
} else if (completionList.isRight()) {
isIncomplete = completionList.getRight().isIncomplete();
items = completionList.getRight().getItems();
}
List<ICompletionProposal> proposals = new ArrayList<>();
for (CompletionItem item : items) {
if (item != null) {
if (isIncomplete) {
ICompletionProposal proposal = new LSIncompleteCompletionProposal(document, offset, item,
languageServer);
proposals.add(proposal);
} else {
ICompletionProposal proposal = new LSCompletionProposal(document, offset, item, languageServer);
if (((LSCompletionProposal) proposal).validate(document, offset, null)) {
proposals.add(proposal);
}
}
}
}
return proposals;
}
@Override
public IContextInformation[] computeContextInformation(ITextViewer viewer, int offset) {
initiateLanguageServers(viewer.getDocument());
SignatureHelpParams param;
try {
param = LSPEclipseUtils.toSignatureHelpParams(offset, viewer.getDocument());
} catch (BadLocationException e) {
LanguageServerPlugin.logError(e);
return new IContextInformation[] { /* TODO? show error in context information */ };
}
List<IContextInformation> contextInformations = Collections.synchronizedList(new ArrayList<>());
try {
contextInformationLanguageServersFuture
.thenComposeAsync(languageServers -> CompletableFuture.allOf(languageServers.stream()
.map(languageServer -> languageServer.getTextDocumentService().signatureHelp(param))
.map(signatureHelpFuture -> signatureHelpFuture.thenAcceptAsync(signatureHelp -> {
if (signatureHelp != null) {
signatureHelp.getSignatures().stream().map(LSContentAssistProcessor::toContextInformation)
.forEach(contextInformations::add);
}
})).toArray(CompletableFuture[]::new)))
.get(CONTEXT_INFORMATION_TIMEOUT, TimeUnit.MILLISECONDS);
} catch (ExecutionException | TimeoutException e) {
LanguageServerPlugin.logError(e);
return new IContextInformation[] { /* TODO? show error in context information */ };
} catch (InterruptedException e) {
LanguageServerPlugin.logError(e);
Thread.currentThread().interrupt();
return new IContextInformation[] { /* TODO? show error in context information */ };
}
return contextInformations.toArray(new IContextInformation[0]);
}
private static IContextInformation toContextInformation(SignatureInformation information) {
StringBuilder signature = new StringBuilder(information.getLabel());
String docString = LSPEclipseUtils.getDocString(information.getDocumentation());
if (docString!=null && !docString.isEmpty()) {
signature.append('\n').append(docString);
}
IContextInformation contextInformation = new ContextInformation(
information.getLabel(), signature.toString());
return contextInformation;
}
@Override
public char[] getCompletionProposalAutoActivationCharacters() {
ITextEditor textEditor = LSPEclipseUtils.getActiveTextEditor();
if(textEditor != null) {
initiateLanguageServers(LSPEclipseUtils.getDocument(textEditor));
try {
this.completionLanguageServersFuture.get(TRIGGERS_TIMEOUT, TimeUnit.MILLISECONDS);
} catch (OperationCanceledException | TimeoutException | ExecutionException e) {
LanguageServerPlugin.logError(e);
} catch (InterruptedException e) {
LanguageServerPlugin.logError(e);
Thread.currentThread().interrupt();
}
}
return completionTriggerChars;
}
private static char[] mergeTriggers(char[] initialArray, Collection<String> additionalTriggers) {
if (initialArray == null) {
initialArray = new char[0];
}
if (additionalTriggers == null) {
additionalTriggers = Collections.emptySet();
}
Set<Character> triggers = new HashSet<>();
for (char c : initialArray) {
triggers.add(Character.valueOf(c));
}
additionalTriggers.stream().filter(s -> !Strings.isNullOrEmpty(s))
.map(triggerChar -> Character.valueOf(triggerChar.charAt(0))).forEach(triggers::add);
char[] res = new char[triggers.size()];
int i = 0;
for (Character c : triggers) {
res[i] = c.charValue();
i++;
}
return res;
}
@Override
public char[] getContextInformationAutoActivationCharacters() {
ITextEditor textEditor = LSPEclipseUtils.getActiveTextEditor();
if(textEditor != null) {
initiateLanguageServers(LSPEclipseUtils.getDocument(textEditor));
try {
this.contextInformationLanguageServersFuture.get(TRIGGERS_TIMEOUT, TimeUnit.MILLISECONDS);
} catch (OperationCanceledException | TimeoutException | ExecutionException e) {
LanguageServerPlugin.logError(e);
} catch (InterruptedException e) {
LanguageServerPlugin.logError(e);
Thread.currentThread().interrupt();
}
}
return contextTriggerChars;
}
@Override
public String getErrorMessage() {
return this.errorMessage;
}
@Override
public IContextInformationValidator getContextInformationValidator() {
return new ContextInformationValidator(this);
}
}