Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Filter completion list only with text before cursor #70448

Merged
merged 2 commits into from
Oct 19, 2023
Merged
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 @@ -65,17 +65,7 @@ internal abstract class AbstractLspCompletionResultCreationService : ILspComplet
var creationService = document.Project.Solution.Services.GetRequiredService<ILspCompletionResultCreationService>();
var completionService = document.GetRequiredLanguageService<CompletionService>();

// 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)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -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);
Expand All @@ -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(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<IGlobalOptionService>();
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.CompletionParams, LSP.CompletionList>(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.CompletionParams, LSP.CompletionList>(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"));

}
}