Skip to content
Open
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
Original file line number Diff line number Diff line change
Expand Up @@ -44,13 +44,15 @@
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;
import org.eclipse.lsp4e.test.utils.AbstractTestWithProject;
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;
Expand Down Expand Up @@ -504,50 +506,57 @@ 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
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
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
public void testToCompletionParams_NonMatchingTriggerCharacter() 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(), 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
Expand Down
39 changes: 26 additions & 13 deletions org.eclipse.lsp4e/src/org/eclipse/lsp4e/LSPEclipseUtils.java
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -442,27 +446,25 @@ private void updateCompletionItem(@Nullable CompletionItem resolvedItem) {

@Override
public int getPrefixCompletionStart(IDocument document, int completionOffset) {
Either<TextEdit, InsertReplaceEdit> textEdit = item.getTextEdit();
if (textEdit != null) {
try {
try {
Either<TextEdit, InsertReplaceEdit> 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)) {
return completionOffset - subDoc.substring(i).length();
}
}
} 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;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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<ICompletionProposal>());
final var anyIncomplete = new AtomicBoolean(false);
try {
Expand All @@ -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()
Expand Down Expand Up @@ -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<ICompletionProposal>) (List<?>) completeProposals;
Expand All @@ -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[] {
Expand Down Expand Up @@ -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<String> triggerChars = castNonNull(w.getServerCapabilities()).getCompletionProvider().getTriggerCharacters();
completionTriggerChars = mergeTriggers(completionTriggerChars,triggerChars);
return CompletableFuture.completedFuture(null);
});
.withFilter(capabilities -> capabilities.getCompletionProvider() != null) //
.collectAll((w, ls) -> {
List<String> 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<String> 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<String> triggerChars = castNonNull(w.getServerCapabilities()).getSignatureHelpProvider()
.getTriggerCharacters();
contextTriggerChars = mergeTriggers(contextTriggerChars, triggerChars);
return CompletableFuture.completedFuture(null);
});
contextInformationLanguageServersFuture = triggerCharsCancellationSupport
.execute(contextInformationTriggerCharsFuture);
}

}
Expand All @@ -270,15 +295,15 @@ private static List<ICompletionProposal> 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
Expand Down