Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion org.eclipse.lsp4e.jdt/META-INF/MANIFEST.MF
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ Manifest-Version: 1.0
Bundle-ManifestVersion: 2
Bundle-Name: JDT Integration for LSP4E
Bundle-SymbolicName: org.eclipse.lsp4e.jdt;singleton:=true
Bundle-Version: 0.14.2.qualifier
Bundle-Version: 0.14.3.qualifier
Export-Package: org.eclipse.lsp4e.jdt
Automatic-Module-Name: org.eclipse.lsp4e.jdt
Bundle-Activator: org.eclipse.lsp4e.jdt.LanguageServerJdtPlugin
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,31 +11,65 @@
*******************************************************************************/
package org.eclipse.lsp4e.jdt;

import java.net.URI;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.UUID;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.jdt.annotation.Nullable;
import org.eclipse.jdt.core.ICompilationUnit;
import org.eclipse.jdt.core.ISourceRange;
import org.eclipse.jdt.core.ISourceReference;
import org.eclipse.jdt.core.JavaModelException;
import org.eclipse.jdt.core.SourceRange;
import org.eclipse.jdt.core.ToolFactory;
import org.eclipse.jdt.core.compiler.IScanner;
import org.eclipse.jdt.core.compiler.ITerminalSymbols;
import org.eclipse.jdt.core.compiler.InvalidInputException;
import org.eclipse.jdt.ui.text.java.ContentAssistInvocationContext;
import org.eclipse.jdt.ui.text.java.IJavaCompletionProposal;
import org.eclipse.jdt.ui.text.java.IJavaCompletionProposalComputer;
import org.eclipse.jdt.ui.text.java.JavaContentAssistInvocationContext;
import org.eclipse.jface.text.IDocument;
import org.eclipse.jface.text.ITextViewer;
import org.eclipse.jface.text.Region;
import org.eclipse.jface.text.contentassist.ICompletionProposal;
import org.eclipse.jface.text.contentassist.IContextInformation;
import org.eclipse.lsp4e.LanguageServerPlugin;
import org.eclipse.lsp4e.LanguageServers;
import org.eclipse.lsp4e.LanguageServers.LanguageServerDocumentExecutor;
import org.eclipse.lsp4e.internal.CancellationSupport;
import org.eclipse.lsp4e.jdt.internal.TextBlockSourceContentMapper;
import org.eclipse.lsp4e.operations.completion.LSCompletionProposal;
import org.eclipse.lsp4e.operations.completion.LSContentAssistProcessor;
import org.eclipse.lsp4j.DidChangeTextDocumentParams;
import org.eclipse.lsp4j.DidCloseTextDocumentParams;
import org.eclipse.lsp4j.TextDocumentContentChangeEvent;
import org.eclipse.lsp4j.VersionedTextDocumentIdentifier;
import org.eclipse.swt.graphics.Image;
import org.eclipse.swt.graphics.Point;

@SuppressWarnings({ "restriction" })
public class LSJavaCompletionProposalComputer implements IJavaCompletionProposalComputer {

private static final TimeUnit TIMEOUT_UNIT = TimeUnit.MILLISECONDS;
private static final long TIMEOUT_LENGTH = 300;

// TODO this should be customizable
private static final Pattern TEXT_BLOCK_LANGUAGE_INDICATOR_COMMENT_PATTERN = Pattern
.compile("\\W*\\s*language=(\\w+)\\W*",
Pattern.CASE_INSENSITIVE);
private final LSContentAssistProcessor lsContentAssistProcessor = new LSContentAssistProcessor(false);
private @Nullable String javaCompletionSpecificErrorMessage;
private @Nullable LSContentAssistProcessor textBlockContentAssistProcessor;

@Override
public void sessionStarted() {
Expand All @@ -45,13 +79,27 @@ public void sessionStarted() {
public List<ICompletionProposal> computeCompletionProposals(ContentAssistInvocationContext context,
IProgressMonitor monitor) {
final var viewer = context.getViewer();
if(viewer == null)
if(viewer == null) {
return List.of();
CompletableFuture<ICompletionProposal[]> future = CompletableFuture.supplyAsync(() ->
lsContentAssistProcessor.computeCompletionProposals(viewer, context.getInvocationOffset()));
}
CompletableFuture<ICompletionProposal[]> future = CompletableFuture.supplyAsync(() -> {
ICompletionProposal[] proposals = lsContentAssistProcessor.computeCompletionProposals(viewer, context.getInvocationOffset());
monitor.worked(1);
List<ICompletionProposal> textBlockProposals = computeTextBlockProposals(context, viewer);
if (!textBlockProposals.isEmpty()) {
List<ICompletionProposal> merged = new ArrayList<>(Arrays.asList(proposals));
for (ICompletionProposal proposal : textBlockProposals) {
merged.add(proposal);
}
return merged.toArray(ICompletionProposal[]::new);

}
return proposals;
});

try {
return List.of(asJavaProposals(future, context));
ICompletionProposal[] asJavaProposals = asJavaProposals(future, context);
return List.of(asJavaProposals);
} catch (ExecutionException | TimeoutException e) {
LanguageServerPlugin.logError(e);
javaCompletionSpecificErrorMessage = createErrorMessage(e);
Expand All @@ -64,6 +112,163 @@ public List<ICompletionProposal> computeCompletionProposals(ContentAssistInvocat
}
}

private List<ICompletionProposal> computeTextBlockProposals(ContentAssistInvocationContext context,
ITextViewer viewer) {
IDocument fullDocument = viewer.getDocument();
if (fullDocument == null || !(context instanceof JavaContentAssistInvocationContext ctx)) {
return List.of();
}
try {
ICompilationUnit compilationUnit = ctx.getCompilationUnit();
if (compilationUnit == null
|| !(compilationUnit.getElementAt(ctx.getInvocationOffset()) instanceof ISourceReference ref)) {
return List.of();
}
ISourceRange sourceRange = ref.getSourceRange();
if (sourceRange == null || !SourceRange.isAvailable(sourceRange)) {
return List.of();
}
// TODO reuse scanner?
IScanner scanner = ToolFactory.createScanner(true, false, false, false);
scanner.setSource(fullDocument.get().toCharArray());// TODO only create scanner on source range?
scanner.resetTo(sourceRange.getOffset(), sourceRange.getOffset() + sourceRange.getLength());
int token;
String language = null;
do {
token = scanner.getNextToken();
switch (token) {
case ITerminalSymbols.TokenNameSEMICOLON -> language = null;
case ITerminalSymbols.TokenNameCOMMENT_LINE, ITerminalSymbols.TokenNameCOMMENT_BLOCK -> {
Matcher matcher = TEXT_BLOCK_LANGUAGE_INDICATOR_COMMENT_PATTERN
.matcher(String.valueOf(scanner.getCurrentTokenSource()));
if (matcher.matches()) {
language = matcher.group(1);// TODO ensure group present with custom regex
} else {
language = null;
}
}
}
} while (token != ITerminalSymbols.TokenNameEOF
&& scanner.getCurrentTokenEndPosition() < ctx.getInvocationOffset());
if (token == ITerminalSymbols.TokenNameTextBlock && language != null) {
TextBlockDocument doc = createDocumentForTextBlockToken(fullDocument, scanner, language);
LanguageServerDocumentExecutor documentExecutor = LanguageServers.forDocument(doc)
.withFilter(capabilities -> capabilities.getCompletionProvider() != null);
markTempDocumentOpened(documentExecutor, doc.getURI(), String.valueOf(scanner.getCurrentTokenSource()));

try {
int invocationOffsetInContent = TextBlockSourceContentMapper.mapSourceToContent(
scanner.getRawTokenSource(), scanner.getCurrentTokenSource(),
context.getInvocationOffset() - scanner.getCurrentTokenStartPosition());
if (invocationOffsetInContent != -1) {
ICompletionProposal[] results = getTextBlockContentAssistProcessor()
.computeCompletionProposals(doc, invocationOffsetInContent);
List<ICompletionProposal> proposals = new ArrayList<>(results.length);
for (ICompletionProposal proposal : results) {
if (proposal instanceof LSCompletionProposal p) {
proposals.add(new TextBlockProposal(p, doc));
}
}
return proposals;
}
} finally {
markTempDocumentClosed(documentExecutor, doc.getURI());
}
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} catch (JavaModelException | InvalidInputException | ExecutionException e) {
LanguageServerPlugin.logError(e);
}
return List.of();
}

private LSContentAssistProcessor getTextBlockContentAssistProcessor() {
LSContentAssistProcessor processor = textBlockContentAssistProcessor;
if (processor == null) {
processor = new LSContentAssistProcessor(false);
textBlockContentAssistProcessor = processor;
}
return processor;
}

private TextBlockDocument createDocumentForTextBlockToken(IDocument fullDocument, IScanner scanner,
String language) {
URI uri = URI.create("none:///" + UUID.randomUUID() + "." + language);
return new TextBlockDocument(fullDocument,
new Region(scanner.getCurrentTokenStartPosition(),
scanner.getCurrentTokenEndPosition() - scanner.getCurrentTokenStartPosition()),
scanner.getRawTokenSource(), scanner.getCurrentTokenSource(), uri);
}

private void markTempDocumentOpened(LanguageServerDocumentExecutor executor, URI uri, String content)
throws InterruptedException, ExecutionException {
CompletableFuture<List<Object>> notifyOfDocument = executor
.collectAll((w, ls) -> {
VersionedTextDocumentIdentifier id = new VersionedTextDocumentIdentifier(uri.toString(), 0);
w.sendNotification(l -> l.getTextDocumentService().didChange(
new DidChangeTextDocumentParams(id, List.of(new TextDocumentContentChangeEvent(content)))));
return CompletableFuture.completedFuture(null);
});
new CancellationSupport().execute(notifyOfDocument).get();
}

private void markTempDocumentClosed(LanguageServerDocumentExecutor executor, URI uri) {
CompletableFuture<List<Object>> notifyOfDocument = executor.collectAll((w, ls) -> {
VersionedTextDocumentIdentifier id = new VersionedTextDocumentIdentifier(uri.toString(), 0);
w.sendNotification(l -> l.getTextDocumentService().didClose(new DidCloseTextDocumentParams(id)));
return CompletableFuture.completedFuture(null);
});
new CancellationSupport().execute(notifyOfDocument);
}

static class TextBlockProposal implements IJavaCompletionProposal {

private final LSCompletionProposal proposalInTextBlock;
private final IDocument doc;

public TextBlockProposal(LSCompletionProposal proposalInTextBlock, IDocument doc) {
this.proposalInTextBlock = proposalInTextBlock;
this.doc = doc;
}

@Override
public @Nullable Point getSelection(IDocument document) {
return null;
}

@Override
public @Nullable Image getImage() {
return proposalInTextBlock.getImage();
}

@Override
public String getDisplayString() {
return proposalInTextBlock.getDisplayString();
}

@Override
public @Nullable IContextInformation getContextInformation() {
return proposalInTextBlock.getContextInformation();
}

@Override
public @Nullable String getAdditionalProposalInfo() {
return proposalInTextBlock.getAdditionalProposalInfo();
}

@Override
public void apply(IDocument document) {
// TODO what if document doesn't match?
proposalInTextBlock.apply(doc);
}

@Override
public int getRelevance() {
return new LSJavaProposal(proposalInTextBlock).getRelevance();
}
}

private String createErrorMessage(Exception ex) {
return Messages.javaSpecificCompletionError + " : " + (ex.getMessage() != null ? ex.getMessage() : ex.toString()); //$NON-NLS-1$
}
Expand All @@ -77,20 +282,25 @@ private String createErrorMessage(Exception ex) {
* This method wraps around the LSCompletionProposal with a IJavaCompletionProposal, and it sets the relevance
* number that JDT uses to sort proposals in a desired order.
*/
private ICompletionProposal[] asJavaProposals(CompletableFuture<ICompletionProposal[]> future, ContentAssistInvocationContext context)
throws InterruptedException, ExecutionException, TimeoutException {
private ICompletionProposal[] asJavaProposals(CompletableFuture<ICompletionProposal[]> future,
ContentAssistInvocationContext context) throws InterruptedException, ExecutionException, TimeoutException {
ICompletionProposal[] originalProposals = future.get(TIMEOUT_LENGTH, TIMEOUT_UNIT);

return Arrays.stream(originalProposals).filter(LSCompletionProposal.class::isInstance).map(LSCompletionProposal.class::cast).map(LSJavaProposal::new).toArray(LSJavaProposal[]::new);

return Arrays.stream(originalProposals)
.filter(p -> p instanceof LSCompletionProposal || p instanceof TextBlockProposal).map(proposal -> {
if (proposal instanceof LSCompletionProposal p) {
return new LSJavaProposal(p);
}
return proposal;
}).toArray(ICompletionProposal[]::new);
}

@Override
public List<IContextInformation> computeContextInformation(ContentAssistInvocationContext context,
IProgressMonitor monitor) {
final var viewer = context.getViewer();
if(viewer == null)
if(viewer == null) {
return List.of();
}
IContextInformation[] contextInformation = lsContentAssistProcessor.computeContextInformation(viewer, context.getInvocationOffset());
return contextInformation == null ? List.of() : List.of(contextInformation);
}
Expand Down
Loading
Loading