diff --git a/org.eclipse.lsp4e.test/src/org/eclipse/lsp4e/test/edit/LSPEclipseUtilsTest.java b/org.eclipse.lsp4e.test/src/org/eclipse/lsp4e/test/edit/LSPEclipseUtilsTest.java index ef9393e54..87fa9e1d0 100644 --- a/org.eclipse.lsp4e.test/src/org/eclipse/lsp4e/test/edit/LSPEclipseUtilsTest.java +++ b/org.eclipse.lsp4e.test/src/org/eclipse/lsp4e/test/edit/LSPEclipseUtilsTest.java @@ -44,6 +44,7 @@ import org.eclipse.core.runtime.CoreException; import org.eclipse.core.runtime.IPath; import org.eclipse.core.runtime.NullProgressMonitor; +import org.eclipse.jface.text.BadLocationException; import org.eclipse.jface.text.IDocument; import org.eclipse.jface.text.ITextViewer; import org.eclipse.lsp4e.LSPEclipseUtils; @@ -51,6 +52,7 @@ import org.eclipse.lsp4e.test.utils.NoErrorLoggedRule; import org.eclipse.lsp4e.test.utils.TestUtils; import org.eclipse.lsp4e.ui.UI; +import org.eclipse.lsp4j.CompletionContext; import org.eclipse.lsp4j.CompletionTriggerKind; import org.eclipse.lsp4j.CreateFile; import org.eclipse.lsp4j.Location; @@ -504,15 +506,22 @@ public void testGetOpenEditorExternalFile(@TempDir Path tempDir) throws Exceptio assertNotEquals(Collections.emptySet(), LSPEclipseUtils.findOpenEditorsFor(file.toUri())); } + private static CompletionContext toCompletionContext(int offset, IDocument document, char[] completionTriggerChars) + throws BadLocationException { + int positionCharacterOffset = offset > 0 ? offset - 1 : offset; + String positionCharacter = document.get(positionCharacterOffset, 1); + return LSPEclipseUtils.toCompletionContext(positionCharacter, completionTriggerChars); + } + @Test public void testToCompletionParams_EmptyDocument() throws Exception { // Given an empty file/document var file = TestUtils.createFile(project, "dummy" + new Random().nextInt(), ""); var triggerChars = new char[] {':', '>'}; - // When toCompletionParams get called with offset == 0 and document.getLength() == 0: - var param = LSPEclipseUtils.toCompletionParams(file.getLocationURI(), 0, LSPEclipseUtils.getDocument(file), triggerChars); - // Then no context has been added to param: - assertNull(param.getContext()); + // When toCompletionContext get called with offset == 0 and document.getLength() == 0: + var context = toCompletionContext(0, LSPEclipseUtils.getDocument(file), triggerChars); + // Then the trigger kind is Invoked: + assertEquals(context.getTriggerKind(), CompletionTriggerKind.Invoked); } @Test @@ -520,10 +529,10 @@ public void testToCompletionParams_ZeroOffset() throws Exception { // Given a non empty file/document containing a non trigger character at position 3: var file = TestUtils.createFile(project, "dummy" + new Random().nextInt(), "std"); var triggerChars = new char[] {':', '>'}; - // When toCompletionParams get called with offset == 0 and document.getLength() > 0: - var param = LSPEclipseUtils.toCompletionParams(file.getLocationURI(), 0, LSPEclipseUtils.getDocument(file), triggerChars); + // When toCompletionContext get called with offset == 0 and document.getLength() > 0: + var context = toCompletionContext( 0, LSPEclipseUtils.getDocument(file), triggerChars); // Then the trigger kind is Invoked: - assertEquals(param.getContext().getTriggerKind(), CompletionTriggerKind.Invoked); + assertEquals(context.getTriggerKind(), CompletionTriggerKind.Invoked); } @Test @@ -531,12 +540,12 @@ public void testToCompletionParams_MatchingTriggerCharacter() throws Exception { // Given a non empty file/document containing a trigger character at position 4: var file = TestUtils.createFile(project, "dummy" + new Random().nextInt(), "std:"); var triggerChars = new char[] {':', '>'}; - // When toCompletionParams get called with offset > 0 and document.getLength() > 0: - var param = LSPEclipseUtils.toCompletionParams(file.getLocationURI(), 4, LSPEclipseUtils.getDocument(file), triggerChars); + // When toCompletionContext get called with offset > 0 and document.getLength() > 0: + var context = toCompletionContext(4, LSPEclipseUtils.getDocument(file), triggerChars); // Then the context has been added with a colon as trigger character: - assertEquals(param.getContext().getTriggerCharacter(), ":"); + assertEquals(context.getTriggerCharacter(), ":"); // And the trigger kind is TriggerCharacter: - assertEquals(param.getContext().getTriggerKind(), CompletionTriggerKind.TriggerCharacter); + assertEquals(context.getTriggerKind(), CompletionTriggerKind.TriggerCharacter); } @Test @@ -544,10 +553,10 @@ public void testToCompletionParams_NonMatchingTriggerCharacter() throws Exceptio // Given a non empty file/document containing a non trigger character at position 3: var file = TestUtils.createFile(project, "dummy" + new Random().nextInt(), "std"); var triggerChars = new char[] {':', '>'}; - // When toCompletionParams get called with offset > 0 and document.getLength() > 0: - var param = LSPEclipseUtils.toCompletionParams(file.getLocationURI(), 3, LSPEclipseUtils.getDocument(file), triggerChars); + // When toCompletionContext get called with offset > 0 and document.getLength() > 0: + var context = toCompletionContext(3, LSPEclipseUtils.getDocument(file), triggerChars); // Then the trigger kind is Invoked: - assertEquals(param.getContext().getTriggerKind(), CompletionTriggerKind.Invoked); + assertEquals(context.getTriggerKind(), CompletionTriggerKind.Invoked); } @Test diff --git a/org.eclipse.lsp4e/src/org/eclipse/lsp4e/LSPEclipseUtils.java b/org.eclipse.lsp4e/src/org/eclipse/lsp4e/LSPEclipseUtils.java index 7270449ba..65223b6cd 100644 --- a/org.eclipse.lsp4e/src/org/eclipse/lsp4e/LSPEclipseUtils.java +++ b/org.eclipse.lsp4e/src/org/eclipse/lsp4e/LSPEclipseUtils.java @@ -240,30 +240,43 @@ public static boolean isOffsetInRange(int offset, Range range, IDocument documen } } + /** + * use {@link #toCompletionParams(URI, int, IDocument)} and {@link #toCompletionContext(String, char[])} instead. + */ + @Deprecated(forRemoval = true) public static CompletionParams toCompletionParams(URI fileUri, int offset, IDocument document, char[] completionTriggerChars) throws BadLocationException { Position start = toPosition(offset, document); final var param = new CompletionParams(); if (document.getLength() > 0) { - try { - int positionCharacterOffset = offset > 0 ? offset-1 : offset; - String positionCharacter = document.get(positionCharacterOffset, 1); - if (Chars.contains(completionTriggerChars, positionCharacter.charAt(0))) { - param.setContext(new CompletionContext(CompletionTriggerKind.TriggerCharacter, positionCharacter)); - } else { - // According to LSP 3.17 specification: the triggerCharacter in CompletionContext is undefined if - // triggerKind != CompletionTriggerKind.TriggerCharacter - param.setContext(new CompletionContext(CompletionTriggerKind.Invoked)); - } - } catch (BadLocationException e) { - LanguageServerPlugin.logError(e); - } + int positionCharacterOffset = offset > 0 ? offset - 1 : offset; + String positionCharacter = document.get(positionCharacterOffset, 1); + param.setContext(toCompletionContext(positionCharacter, completionTriggerChars)); } param.setPosition(start); param.setTextDocument(toTextDocumentIdentifier(fileUri)); return param; } + public static CompletionParams toCompletionParams(URI fileUri, int offset, IDocument document) + throws BadLocationException { + final var param = new CompletionParams(); + param.setPosition(toPosition(offset, document)); + param.setTextDocument(toTextDocumentIdentifier(fileUri)); + return param; + } + + public static CompletionContext toCompletionContext(String positionCharacter, char[] completionTriggerChars) { + if (Chars.contains(completionTriggerChars, positionCharacter.charAt(0))) { + return new CompletionContext(CompletionTriggerKind.TriggerCharacter, positionCharacter); + } else { + // According to LSP 3.17 specification: the triggerCharacter in + // CompletionContext is undefined if + // triggerKind != CompletionTriggerKind.TriggerCharacter + return new CompletionContext(CompletionTriggerKind.Invoked); + } + } + public static @Nullable ITextSelection toSelection(Range range, IDocument document) { try { int offset = toOffset(range.getStart(), document); diff --git a/org.eclipse.lsp4e/src/org/eclipse/lsp4e/operations/completion/LSCompletionProposal.java b/org.eclipse.lsp4e/src/org/eclipse/lsp4e/operations/completion/LSCompletionProposal.java index 442eef939..641156893 100644 --- a/org.eclipse.lsp4e/src/org/eclipse/lsp4e/operations/completion/LSCompletionProposal.java +++ b/org.eclipse.lsp4e/src/org/eclipse/lsp4e/operations/completion/LSCompletionProposal.java @@ -234,7 +234,9 @@ public int getRankScore() { rankScore = CompletionProposalTools.getScoreOfFilterMatch(getDocumentFilter(), getFilterString()); } catch (BadLocationException e) { - LanguageServerPlugin.logError(e); + // Document was changed while we computed completion proposals, which made the + // offset invalid. We can stop any further computation as the result will be + // discarded anyway. rankScore = -1; } this.rankScore = rankScore; @@ -257,7 +259,9 @@ public int getRankCategory() { rankCategory = CompletionProposalTools.getCategoryOfFilterMatch(getDocumentFilter(), getFilterString()); } catch (BadLocationException e) { - LanguageServerPlugin.logError(e); + // Document was changed while we computed completion proposals, which made the + // offset invalid. We can stop any further computation as the result will be + // discarded anyway. rankCategory = CompletionProposalTools.CATEGORY_NO_MATCH; } this.rankCategory = rankCategory; @@ -442,19 +446,15 @@ private void updateCompletionItem(@Nullable CompletionItem resolvedItem) { @Override public int getPrefixCompletionStart(IDocument document, int completionOffset) { - Either textEdit = item.getTextEdit(); - if (textEdit != null) { - try { + try { + Either textEdit = item.getTextEdit(); + if (textEdit != null) { return LSPEclipseUtils.toOffset(getTextEditRange().getStart(), document); - } catch (BadLocationException e) { - LanguageServerPlugin.logError(e); } - } - final String insertText = getInsertText(); - final int insertTextLength = insertText.length(); - try { - String subDoc = document.get( - Math.max(0, completionOffset - insertTextLength), + + final String insertText = getInsertText(); + final int insertTextLength = insertText.length(); + String subDoc = document.get(Math.max(0, completionOffset - insertTextLength), Math.min(insertTextLength, completionOffset)); for (int i = 0; i < insertTextLength && i < completionOffset; i++) { if (insertText.regionMatches(true, 0, subDoc, i, subDoc.length() - i)) { @@ -462,7 +462,9 @@ public int getPrefixCompletionStart(IDocument document, int completionOffset) { } } } catch (BadLocationException e) { - LanguageServerPlugin.logError(e); + // Document was changed while we computed completion proposals, which made the + // offset invalid. We can stop any further computation as the result will be + // discarded anyway. } return completionOffset; } diff --git a/org.eclipse.lsp4e/src/org/eclipse/lsp4e/operations/completion/LSContentAssistProcessor.java b/org.eclipse.lsp4e/src/org/eclipse/lsp4e/operations/completion/LSContentAssistProcessor.java index 4b5a75cca..181882f42 100644 --- a/org.eclipse.lsp4e/src/org/eclipse/lsp4e/operations/completion/LSContentAssistProcessor.java +++ b/org.eclipse.lsp4e/src/org/eclipse/lsp4e/operations/completion/LSContentAssistProcessor.java @@ -87,8 +87,8 @@ public class LSContentAssistProcessor implements IContentAssistProcessor { */ private CancellationSupport completionCancellationSupport; /** - * The cancellation support used to cancel previous LSP requests - * for fetching the trigger characters + * The cancellation support used to cancel previous LSP requests for fetching + * the trigger characters */ private CancellationSupport triggerCharsCancellationSupport; @@ -116,22 +116,31 @@ public ICompletionProposal[] computeCompletionProposals(ITextViewer viewer, int return NO_COMPLETION_PROPOSALS; } + String positionCharacter = getPositionCharacter(document, offset); + + if (positionCharacter == null) { + return NO_COMPLETION_PROPOSALS; + } + URI uri = LSPEclipseUtils.toUri(document); if (uri == null) { return NO_COMPLETION_PROPOSALS; } - initiateLanguageServers(document); - CompletionParams param; - + final CompletionParams param; try { - param = LSPEclipseUtils.toCompletionParams(uri, offset, document, this.completionTriggerChars); + param = LSPEclipseUtils.toCompletionParams(uri, offset, document); } catch (BadLocationException e) { - LanguageServerPlugin.logError(e); - this.errorMessage = createErrorMessage(offset, e); - return createErrorProposal(offset, e); + // Document was changed while we computed completion proposals, which made the + // offset invalid. We can stop any further computation as the result will be + // discarded anyway. + return NO_COMPLETION_PROPOSALS; } + initiateLanguageServers(document); + + param.setContext(LSPEclipseUtils.toCompletionContext(positionCharacter, completionTriggerChars)); + final var proposals = Collections.synchronizedList(new ArrayList()); final var anyIncomplete = new AtomicBoolean(false); try { @@ -143,8 +152,8 @@ public ICompletionProposal[] computeCompletionProposals(ITextViewer viewer, int // - LSP requests 'textDocument/completions' // - completionLanguageServersFuture final var cancellationSupport = new CancellationSupport(); - final var completionLanguageServersFuture = cancellationSupport.execute( - LanguageServers.forDocument(document).withFilter(capabilities -> capabilities.getCompletionProvider() != null) // + final var completionLanguageServersFuture = cancellationSupport.execute(LanguageServers + .forDocument(document).withFilter(capabilities -> capabilities.getCompletionProvider() != null) // .collectAll((w, ls) -> cancellationSupport.execute(ls.getTextDocumentService().completion(param)) // .thenAccept(completion -> { boolean isIncomplete = completion != null && completion.isRight() @@ -191,7 +200,8 @@ public ICompletionProposal[] computeCompletionProposals(ITextViewer viewer, int final ICompletionProposal incompleteProposal = createIncompleteProposal(offset, anyIncomplete.get()); if (incompleteProposal != null && !completeProposals.isEmpty()) { // Only add the incompleteProposal if the list is not empty. - // Otherwise we might get a completion popup which contains only the incompleteProposal. + // Otherwise we might get a completion popup which contains only the + // incompleteProposal. @SuppressWarnings("unchecked") final var incompleteProposals = (List) (List) completeProposals; @@ -201,6 +211,18 @@ public ICompletionProposal[] computeCompletionProposals(ITextViewer viewer, int return completeProposals.toArray(ICompletionProposal[]::new); } + private @Nullable String getPositionCharacter(IDocument document, int offset) { + int positionCharacterOffset = offset > 0 ? offset - 1 : offset; + try { + return document.get(positionCharacterOffset, 1); + } catch (BadLocationException e) { + // Document was changed while we computed completion proposals, which made the + // offset invalid. We can stop any further computation as the result will be + // discarded anyway. + } + return null; + } + private ICompletionProposal[] createErrorProposal(int offset, Exception ex) { if (errorAsCompletionItem) { return new ICompletionProposal[] { @@ -231,22 +253,25 @@ private void initiateLanguageServers(IDocument document) { contextTriggerChars = NO_CHARS; completionTriggerCharsFuture = LanguageServers.forDocument(document) - .withFilter(capabilities -> capabilities.getCompletionProvider() != null) // - .collectAll((w, ls) -> { - List triggerChars = castNonNull(w.getServerCapabilities()).getCompletionProvider().getTriggerCharacters(); - completionTriggerChars = mergeTriggers(completionTriggerChars,triggerChars); - return CompletableFuture.completedFuture(null); - }); + .withFilter(capabilities -> capabilities.getCompletionProvider() != null) // + .collectAll((w, ls) -> { + List triggerChars = castNonNull(w.getServerCapabilities()).getCompletionProvider() + .getTriggerCharacters(); + completionTriggerChars = mergeTriggers(completionTriggerChars, triggerChars); + return CompletableFuture.completedFuture(null); + }); triggerCharsCancellationSupport.execute(completionTriggerCharsFuture); contextInformationTriggerCharsFuture = LanguageServers.forDocument(document) - .withFilter(capabilities -> capabilities.getSignatureHelpProvider() != null) // - .collectAll((w, ls) -> { - List triggerChars = castNonNull(w.getServerCapabilities()).getSignatureHelpProvider().getTriggerCharacters(); - contextTriggerChars = mergeTriggers(contextTriggerChars, triggerChars); - return CompletableFuture.completedFuture(null); - }); - contextInformationLanguageServersFuture = triggerCharsCancellationSupport.execute(contextInformationTriggerCharsFuture); + .withFilter(capabilities -> capabilities.getSignatureHelpProvider() != null) // + .collectAll((w, ls) -> { + List triggerChars = castNonNull(w.getServerCapabilities()).getSignatureHelpProvider() + .getTriggerCharacters(); + contextTriggerChars = mergeTriggers(contextTriggerChars, triggerChars); + return CompletableFuture.completedFuture(null); + }); + contextInformationLanguageServersFuture = triggerCharsCancellationSupport + .execute(contextInformationTriggerCharsFuture); } } @@ -270,15 +295,15 @@ private static List toProposals(IDocument document, int off // Stop the compute of ICompletionProposal if the completion has been cancelled cancelChecker.checkCanceled(); CompletionItemDefaults defaults = completionList.map(o -> null, CompletionList::getItemDefaults); - return completionList.map( Functions.identity(), CompletionList::getItems).stream() // + return completionList.map(Functions.identity(), CompletionList::getItems).stream() // .filter(Objects::nonNull) // - .map(item -> new LSCompletionProposal(document, offset, item, defaults, languageServerWrapper, isIncomplete)) + .map(item -> new LSCompletionProposal(document, offset, item, defaults, languageServerWrapper, + isIncomplete)) .filter(proposal -> { // Stop the compute of ICompletionProposal if the completion has been cancelled cancelChecker.checkCanceled(); return proposal.validate(document, offset, null); - }).map(ICompletionProposal.class::cast) - .toList(); + }).map(ICompletionProposal.class::cast).toList(); } @Override