diff --git a/src/Features/LanguageServer/Protocol/Handler/Completion/AbstractLspCompletionResultCreationService.cs b/src/Features/LanguageServer/Protocol/Handler/Completion/AbstractLspCompletionResultCreationService.cs index 637c19c858518..dc9a02c83541d 100644 --- a/src/Features/LanguageServer/Protocol/Handler/Completion/AbstractLspCompletionResultCreationService.cs +++ b/src/Features/LanguageServer/Protocol/Handler/Completion/AbstractLspCompletionResultCreationService.cs @@ -65,17 +65,7 @@ internal abstract class AbstractLspCompletionResultCreationService : ILspComplet var creationService = document.Project.Solution.Services.GetRequiredService(); var completionService = document.GetRequiredLanguageService(); - // By default, Roslyn would treat continuous alphabetical text as a single word for completion purpose. - // e.g. when user triggers completion at the location of {$} in "pub{$}class", the span would cover "pubclass", - // which is used for subsequent matching and commit. - // This works fine for VS async-completion, where we have full control of entire completion process. - // However, the insert mode in VSCode (i.e. the mode our LSP server supports) expects us to return TextEdit that only - // covers the span ends at the cursor location, e.g. "pub" in the example above. Here we detect when that occurs and - // adjust the span accordingly. - var defaultSpan = !capabilityHelper.SupportVSInternalClientCapabilities && position < list.Span.End - ? new(list.Span.Start, length: position - list.Span.Start) - : list.Span; - + var defaultSpan = list.Span; var typedText = documentText.GetSubText(defaultSpan).ToString(); foreach (var item in list.ItemsList) { diff --git a/src/Features/LanguageServer/Protocol/Handler/Completion/CompletionHandler.cs b/src/Features/LanguageServer/Protocol/Handler/Completion/CompletionHandler.cs index 2741c24cd9d4f..ef3fce55c0e6b 100644 --- a/src/Features/LanguageServer/Protocol/Handler/Completion/CompletionHandler.cs +++ b/src/Features/LanguageServer/Protocol/Handler/Completion/CompletionHandler.cs @@ -64,7 +64,9 @@ public CompletionHandler( return null; } - var completionListResult = await GetFilteredCompletionListAsync(request, context, documentText, document, completionOptions, completionService, cancellationToken).ConfigureAwait(false); + var completionListResult = await GetFilteredCompletionListAsync( + request, context, documentText, document, completionOptions, completionService, capabilityHelper, cancellationToken).ConfigureAwait(false); + if (completionListResult == null) return null; @@ -82,6 +84,7 @@ public CompletionHandler( Document document, CompletionOptions completionOptions, CompletionService completionService, + CompletionCapabilityHelper capabilityHelper, CancellationToken cancellationToken) { var position = await document.GetPositionFromLinePositionAsync(ProtocolConversions.PositionToLinePosition(request.Position), cancellationToken).ConfigureAwait(false); @@ -108,12 +111,25 @@ public CompletionHandler( return null; } - var resultId = result.Value.ResultId; + var (completionList, resultId) = result.Value; + + // By default, Roslyn would treat continuous alphabetical text as a single word for completion purpose. + // e.g. when user triggers completion at the location of {$} in "pub{$}class", the span would cover "pubclass", + // which is used for subsequent matching and commit. + // This works fine for VS async-completion, where we have full control of entire completion process. + // However, the insert mode in VSCode (i.e. the mode our LSP server supports) expects us to return TextEdit that only + // covers the span ends at the cursor location, e.g. "pub" in the example above. Here we detect when that occurs and + // adjust the span accordingly. + if (!capabilityHelper.SupportVSInternalClientCapabilities && position < completionList.Span.End) + { + var defaultSpan = new TextSpan(completionList.Span.Start, length: position - completionList.Span.Start); + completionList = completionList.WithSpan(defaultSpan); + } var completionListMaxSize = _globalOptions.GetOption(LspOptionsStorage.MaxCompletionListSize); - var (completionList, isIncomplete) = FilterCompletionList(result.Value.List, completionListMaxSize, completionTrigger, sourceText); + var (filteredCompletionList, isIncomplete) = FilterCompletionList(completionList, completionListMaxSize, completionTrigger, sourceText); - return (completionList, isIncomplete, resultId); + return (filteredCompletionList, isIncomplete, resultId); } private static async Task<(CompletionList CompletionList, long ResultId)?> CalculateListAsync( diff --git a/src/Features/LanguageServer/ProtocolUnitTests/Completion/CompletionFeaturesTests.cs b/src/Features/LanguageServer/ProtocolUnitTests/Completion/CompletionFeaturesTests.cs index ec23623ed692e..7df53d6e343e9 100644 --- a/src/Features/LanguageServer/ProtocolUnitTests/Completion/CompletionFeaturesTests.cs +++ b/src/Features/LanguageServer/ProtocolUnitTests/Completion/CompletionFeaturesTests.cs @@ -920,4 +920,59 @@ public class MyClass : BaseClass Assert.Equal(false, resolvedItem.Command.Arguments[2]); Assert.Equal((long)268, resolvedItem.Command.Arguments[3]); } + + [Theory, CombinatorialData, WorkItem("https://github.com/dotnet/vscode-csharp/issues/6495")] + public async Task FilteringShouldBeDoneByTextBeforeCursorLocation(bool mutatingLspWorkspace) + { + var markup = +@" +public class Z +{ + public int M() + { + int ia, ib, ic, ifa, ifb, ifc; + i{|caret:|}Exception + } +}"; + await using var testLspServer = await CreateTestLspServerAsync(markup, mutatingLspWorkspace, DefaultClientCapabilities); + var caret = testLspServer.GetLocations("caret").Single(); + await testLspServer.OpenDocumentAsync(caret.Uri); + + var completionParams = new LSP.CompletionParams() + { + TextDocument = CreateTextDocumentIdentifier(caret.Uri), + Position = caret.Range.Start, + Context = new LSP.CompletionContext() + { + TriggerKind = LSP.CompletionTriggerKind.Invoked, + } + }; + + var globalOptions = testLspServer.TestWorkspace.GetService(); + var listMaxSize = 3; + + globalOptions.SetGlobalOption(LspOptionsStorage.MaxCompletionListSize, listMaxSize); + + // Because of the limit in list size, we should not have item "if" returned here + var results = await testLspServer.ExecuteRequestAsync(LSP.Methods.TextDocumentCompletionName, completionParams, CancellationToken.None); + AssertEx.NotNull(results); + Assert.True(results.IsIncomplete); + Assert.Equal(listMaxSize, results.Items.Length); + Assert.False(results.Items.Any(i => i.Label == "if")); + + await testLspServer.InsertTextAsync(caret.Uri, (caret.Range.End.Line, caret.Range.End.Character, "f")); + + completionParams = CreateCompletionParams( + GetLocationPlusOne(caret), + invokeKind: LSP.VSInternalCompletionInvokeKind.Typing, + triggerCharacter: "f", + triggerKind: LSP.CompletionTriggerKind.TriggerForIncompleteCompletions); + + // Now that user typed "Z", we should have item "Z" in the updated list since it's a perfect match + results = await testLspServer.ExecuteRequestAsync(LSP.Methods.TextDocumentCompletionName, completionParams, CancellationToken.None); + Assert.True(results.IsIncomplete); + Assert.Equal(listMaxSize, results.Items.Length); + Assert.True(results.Items.Any(i => i.Label == "if")); + + } }