1111 *******************************************************************************/
1212package org .eclipse .lsp4e .jdt ;
1313
14+ import java .net .URI ;
15+ import java .util .ArrayList ;
1416import java .util .Arrays ;
1517import java .util .List ;
18+ import java .util .UUID ;
1619import java .util .concurrent .CompletableFuture ;
1720import java .util .concurrent .ExecutionException ;
1821import java .util .concurrent .TimeUnit ;
1922import java .util .concurrent .TimeoutException ;
23+ import java .util .regex .Matcher ;
24+ import java .util .regex .Pattern ;
2025
2126import org .eclipse .core .runtime .IProgressMonitor ;
2227import 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 ;
2337import org .eclipse .jdt .ui .text .java .ContentAssistInvocationContext ;
38+ import org .eclipse .jdt .ui .text .java .IJavaCompletionProposal ;
2439import 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 ;
2544import org .eclipse .jface .text .contentassist .ICompletionProposal ;
2645import org .eclipse .jface .text .contentassist .IContextInformation ;
2746import 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 ;
2851import org .eclipse .lsp4e .operations .completion .LSCompletionProposal ;
2952import 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" })
3261public 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