Skip to content

Commit a584177

Browse files
committed
Completion using arbitrary langauge servers in Java text blocks
1 parent aa3effe commit a584177

7 files changed

Lines changed: 692 additions & 18 deletions

File tree

org.eclipse.lsp4e.jdt/META-INF/MANIFEST.MF

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ Manifest-Version: 1.0
22
Bundle-ManifestVersion: 2
33
Bundle-Name: JDT Integration for LSP4E
44
Bundle-SymbolicName: org.eclipse.lsp4e.jdt;singleton:=true
5-
Bundle-Version: 0.14.2.qualifier
5+
Bundle-Version: 0.14.3.qualifier
66
Export-Package: org.eclipse.lsp4e.jdt
77
Automatic-Module-Name: org.eclipse.lsp4e.jdt
88
Bundle-Activator: org.eclipse.lsp4e.jdt.LanguageServerJdtPlugin

org.eclipse.lsp4e.jdt/src/org/eclipse/lsp4e/jdt/LSJavaCompletionProposalComputer.java

Lines changed: 220 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -11,31 +11,65 @@
1111
*******************************************************************************/
1212
package org.eclipse.lsp4e.jdt;
1313

14+
import java.net.URI;
15+
import java.util.ArrayList;
1416
import java.util.Arrays;
1517
import java.util.List;
18+
import java.util.UUID;
1619
import java.util.concurrent.CompletableFuture;
1720
import java.util.concurrent.ExecutionException;
1821
import java.util.concurrent.TimeUnit;
1922
import java.util.concurrent.TimeoutException;
23+
import java.util.regex.Matcher;
24+
import java.util.regex.Pattern;
2025

2126
import org.eclipse.core.runtime.IProgressMonitor;
2227
import org.eclipse.jdt.annotation.Nullable;
28+
import org.eclipse.jdt.core.ICompilationUnit;
29+
import org.eclipse.jdt.core.ISourceRange;
30+
import org.eclipse.jdt.core.ISourceReference;
31+
import org.eclipse.jdt.core.JavaModelException;
32+
import org.eclipse.jdt.core.SourceRange;
33+
import org.eclipse.jdt.core.ToolFactory;
34+
import org.eclipse.jdt.core.compiler.IScanner;
35+
import org.eclipse.jdt.core.compiler.ITerminalSymbols;
36+
import org.eclipse.jdt.core.compiler.InvalidInputException;
2337
import org.eclipse.jdt.ui.text.java.ContentAssistInvocationContext;
38+
import org.eclipse.jdt.ui.text.java.IJavaCompletionProposal;
2439
import org.eclipse.jdt.ui.text.java.IJavaCompletionProposalComputer;
40+
import org.eclipse.jdt.ui.text.java.JavaContentAssistInvocationContext;
41+
import org.eclipse.jface.text.IDocument;
42+
import org.eclipse.jface.text.ITextViewer;
43+
import org.eclipse.jface.text.Region;
2544
import org.eclipse.jface.text.contentassist.ICompletionProposal;
2645
import org.eclipse.jface.text.contentassist.IContextInformation;
2746
import org.eclipse.lsp4e.LanguageServerPlugin;
47+
import org.eclipse.lsp4e.LanguageServers;
48+
import org.eclipse.lsp4e.LanguageServers.LanguageServerDocumentExecutor;
49+
import org.eclipse.lsp4e.internal.CancellationSupport;
50+
import org.eclipse.lsp4e.jdt.internal.TextBlockSourceContentMapper;
2851
import org.eclipse.lsp4e.operations.completion.LSCompletionProposal;
2952
import org.eclipse.lsp4e.operations.completion.LSContentAssistProcessor;
53+
import org.eclipse.lsp4j.DidChangeTextDocumentParams;
54+
import org.eclipse.lsp4j.DidCloseTextDocumentParams;
55+
import org.eclipse.lsp4j.TextDocumentContentChangeEvent;
56+
import org.eclipse.lsp4j.VersionedTextDocumentIdentifier;
57+
import org.eclipse.swt.graphics.Image;
58+
import org.eclipse.swt.graphics.Point;
3059

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

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

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

4074
@Override
4175
public void sessionStarted() {
@@ -45,13 +79,27 @@ public void sessionStarted() {
4579
public List<ICompletionProposal> computeCompletionProposals(ContentAssistInvocationContext context,
4680
IProgressMonitor monitor) {
4781
final var viewer = context.getViewer();
48-
if(viewer == null)
82+
if(viewer == null) {
4983
return List.of();
50-
CompletableFuture<ICompletionProposal[]> future = CompletableFuture.supplyAsync(() ->
51-
lsContentAssistProcessor.computeCompletionProposals(viewer, context.getInvocationOffset()));
84+
}
85+
CompletableFuture<ICompletionProposal[]> future = CompletableFuture.supplyAsync(() -> {
86+
ICompletionProposal[] proposals = lsContentAssistProcessor.computeCompletionProposals(viewer, context.getInvocationOffset());
87+
monitor.worked(1);
88+
List<ICompletionProposal> textBlockProposals = computeTextBlockProposals(context, viewer);
89+
if (!textBlockProposals.isEmpty()) {
90+
List<ICompletionProposal> merged = new ArrayList<>(Arrays.asList(proposals));
91+
for (ICompletionProposal proposal : textBlockProposals) {
92+
merged.add(proposal);
93+
}
94+
return merged.toArray(ICompletionProposal[]::new);
95+
96+
}
97+
return proposals;
98+
});
5299

53100
try {
54-
return List.of(asJavaProposals(future, context));
101+
ICompletionProposal[] asJavaProposals = asJavaProposals(future, context);
102+
return List.of(asJavaProposals);
55103
} catch (ExecutionException | TimeoutException e) {
56104
LanguageServerPlugin.logError(e);
57105
javaCompletionSpecificErrorMessage = createErrorMessage(e);
@@ -64,6 +112,163 @@ public List<ICompletionProposal> computeCompletionProposals(ContentAssistInvocat
64112
}
65113
}
66114

115+
private List<ICompletionProposal> computeTextBlockProposals(ContentAssistInvocationContext context,
116+
ITextViewer viewer) {
117+
IDocument fullDocument = viewer.getDocument();
118+
if (fullDocument == null || !(context instanceof JavaContentAssistInvocationContext ctx)) {
119+
return List.of();
120+
}
121+
try {
122+
ICompilationUnit compilationUnit = ctx.getCompilationUnit();
123+
if (compilationUnit == null
124+
|| !(compilationUnit.getElementAt(ctx.getInvocationOffset()) instanceof ISourceReference ref)) {
125+
return List.of();
126+
}
127+
ISourceRange sourceRange = ref.getSourceRange();
128+
if (sourceRange == null || !SourceRange.isAvailable(sourceRange)) {
129+
return List.of();
130+
}
131+
// TODO reuse scanner?
132+
IScanner scanner = ToolFactory.createScanner(true, false, false, false);
133+
scanner.setSource(fullDocument.get().toCharArray());// TODO only create scanner on source range?
134+
scanner.resetTo(sourceRange.getOffset(), sourceRange.getOffset() + sourceRange.getLength());
135+
int token;
136+
String language = null;
137+
do {
138+
token = scanner.getNextToken();
139+
switch (token) {
140+
case ITerminalSymbols.TokenNameSEMICOLON -> language = null;
141+
case ITerminalSymbols.TokenNameCOMMENT_LINE, ITerminalSymbols.TokenNameCOMMENT_BLOCK -> {
142+
Matcher matcher = TEXT_BLOCK_LANGUAGE_INDICATOR_COMMENT_PATTERN
143+
.matcher(String.valueOf(scanner.getCurrentTokenSource()));
144+
if (matcher.matches()) {
145+
language = matcher.group(1);// TODO ensure group present with custom regex
146+
} else {
147+
language = null;
148+
}
149+
}
150+
}
151+
} while (token != ITerminalSymbols.TokenNameEOF
152+
&& scanner.getCurrentTokenEndPosition() < ctx.getInvocationOffset());
153+
if (token == ITerminalSymbols.TokenNameTextBlock && language != null) {
154+
TextBlockDocument doc = createDocumentForTextBlockToken(fullDocument, scanner, language);
155+
LanguageServerDocumentExecutor documentExecutor = LanguageServers.forDocument(doc)
156+
.withFilter(capabilities -> capabilities.getCompletionProvider() != null);
157+
markTempDocumentOpened(documentExecutor, doc.getURI(), String.valueOf(scanner.getCurrentTokenSource()));
158+
159+
try {
160+
int invocationOffsetInContent = TextBlockSourceContentMapper.mapSourceToContent(
161+
scanner.getRawTokenSource(), scanner.getCurrentTokenSource(),
162+
context.getInvocationOffset() - scanner.getCurrentTokenStartPosition());
163+
if (invocationOffsetInContent != -1) {
164+
ICompletionProposal[] results = getTextBlockContentAssistProcessor()
165+
.computeCompletionProposals(doc, invocationOffsetInContent);
166+
List<ICompletionProposal> proposals = new ArrayList<>(results.length);
167+
for (ICompletionProposal proposal : results) {
168+
if (proposal instanceof LSCompletionProposal p) {
169+
proposals.add(new TextBlockProposal(p, doc));
170+
}
171+
}
172+
return proposals;
173+
}
174+
} finally {
175+
markTempDocumentClosed(documentExecutor, doc.getURI());
176+
}
177+
}
178+
} catch (InterruptedException e) {
179+
Thread.currentThread().interrupt();
180+
} catch (JavaModelException | InvalidInputException | ExecutionException e) {
181+
LanguageServerPlugin.logError(e);
182+
}
183+
return List.of();
184+
}
185+
186+
private LSContentAssistProcessor getTextBlockContentAssistProcessor() {
187+
LSContentAssistProcessor processor = textBlockContentAssistProcessor;
188+
if (processor == null) {
189+
processor = new LSContentAssistProcessor(false);
190+
textBlockContentAssistProcessor = processor;
191+
}
192+
return processor;
193+
}
194+
195+
private TextBlockDocument createDocumentForTextBlockToken(IDocument fullDocument, IScanner scanner,
196+
String language) {
197+
URI uri = URI.create("none:///" + UUID.randomUUID() + "." + language);
198+
return new TextBlockDocument(fullDocument,
199+
new Region(scanner.getCurrentTokenStartPosition(),
200+
scanner.getCurrentTokenEndPosition() - scanner.getCurrentTokenStartPosition()),
201+
scanner.getRawTokenSource(), scanner.getCurrentTokenSource(), uri);
202+
}
203+
204+
private void markTempDocumentOpened(LanguageServerDocumentExecutor executor, URI uri, String content)
205+
throws InterruptedException, ExecutionException {
206+
CompletableFuture<List<Object>> notifyOfDocument = executor
207+
.collectAll((w, ls) -> {
208+
VersionedTextDocumentIdentifier id = new VersionedTextDocumentIdentifier(uri.toString(), 0);
209+
w.sendNotification(l -> l.getTextDocumentService().didChange(
210+
new DidChangeTextDocumentParams(id, List.of(new TextDocumentContentChangeEvent(content)))));
211+
return CompletableFuture.completedFuture(null);
212+
});
213+
new CancellationSupport().execute(notifyOfDocument).get();
214+
}
215+
216+
private void markTempDocumentClosed(LanguageServerDocumentExecutor executor, URI uri) {
217+
CompletableFuture<List<Object>> notifyOfDocument = executor.collectAll((w, ls) -> {
218+
VersionedTextDocumentIdentifier id = new VersionedTextDocumentIdentifier(uri.toString(), 0);
219+
w.sendNotification(l -> l.getTextDocumentService().didClose(new DidCloseTextDocumentParams(id)));
220+
return CompletableFuture.completedFuture(null);
221+
});
222+
new CancellationSupport().execute(notifyOfDocument);
223+
}
224+
225+
static class TextBlockProposal implements IJavaCompletionProposal {
226+
227+
private final LSCompletionProposal proposalInTextBlock;
228+
private final IDocument doc;
229+
230+
public TextBlockProposal(LSCompletionProposal proposalInTextBlock, IDocument doc) {
231+
this.proposalInTextBlock = proposalInTextBlock;
232+
this.doc = doc;
233+
}
234+
235+
@Override
236+
public @Nullable Point getSelection(IDocument document) {
237+
return null;
238+
}
239+
240+
@Override
241+
public @Nullable Image getImage() {
242+
return proposalInTextBlock.getImage();
243+
}
244+
245+
@Override
246+
public String getDisplayString() {
247+
return proposalInTextBlock.getDisplayString();
248+
}
249+
250+
@Override
251+
public @Nullable IContextInformation getContextInformation() {
252+
return proposalInTextBlock.getContextInformation();
253+
}
254+
255+
@Override
256+
public @Nullable String getAdditionalProposalInfo() {
257+
return proposalInTextBlock.getAdditionalProposalInfo();
258+
}
259+
260+
@Override
261+
public void apply(IDocument document) {
262+
// TODO what if document doesn't match?
263+
proposalInTextBlock.apply(doc);
264+
}
265+
266+
@Override
267+
public int getRelevance() {
268+
return new LSJavaProposal(proposalInTextBlock).getRelevance();
269+
}
270+
}
271+
67272
private String createErrorMessage(Exception ex) {
68273
return Messages.javaSpecificCompletionError + " : " + (ex.getMessage() != null ? ex.getMessage() : ex.toString()); //$NON-NLS-1$
69274
}
@@ -77,20 +282,25 @@ private String createErrorMessage(Exception ex) {
77282
* This method wraps around the LSCompletionProposal with a IJavaCompletionProposal, and it sets the relevance
78283
* number that JDT uses to sort proposals in a desired order.
79284
*/
80-
private ICompletionProposal[] asJavaProposals(CompletableFuture<ICompletionProposal[]> future, ContentAssistInvocationContext context)
81-
throws InterruptedException, ExecutionException, TimeoutException {
285+
private ICompletionProposal[] asJavaProposals(CompletableFuture<ICompletionProposal[]> future,
286+
ContentAssistInvocationContext context) throws InterruptedException, ExecutionException, TimeoutException {
82287
ICompletionProposal[] originalProposals = future.get(TIMEOUT_LENGTH, TIMEOUT_UNIT);
83-
84-
return Arrays.stream(originalProposals).filter(LSCompletionProposal.class::isInstance).map(LSCompletionProposal.class::cast).map(LSJavaProposal::new).toArray(LSJavaProposal[]::new);
85-
288+
return Arrays.stream(originalProposals)
289+
.filter(p -> p instanceof LSCompletionProposal || p instanceof TextBlockProposal).map(proposal -> {
290+
if (proposal instanceof LSCompletionProposal p) {
291+
return new LSJavaProposal(p);
292+
}
293+
return proposal;
294+
}).toArray(ICompletionProposal[]::new);
86295
}
87296

88297
@Override
89298
public List<IContextInformation> computeContextInformation(ContentAssistInvocationContext context,
90299
IProgressMonitor monitor) {
91300
final var viewer = context.getViewer();
92-
if(viewer == null)
301+
if(viewer == null) {
93302
return List.of();
303+
}
94304
IContextInformation[] contextInformation = lsContentAssistProcessor.computeContextInformation(viewer, context.getInvocationOffset());
95305
return contextInformation == null ? List.of() : List.of(contextInformation);
96306
}

0 commit comments

Comments
 (0)