From 12cc813377e7a564d6a832ac41a528656e82bcc9 Mon Sep 17 00:00:00 2001 From: "gel@microsoft.com" Date: Tue, 1 Aug 2023 22:54:59 -0700 Subject: [PATCH] Emulate suggestion mode in LSP completion by always soft-select --- ...tractLspCompletionResultCreationService.cs | 31 +++++++++------- .../Completion/CompletionFeaturesTests.cs | 36 +++++++++++++++++++ 2 files changed, 55 insertions(+), 12 deletions(-) diff --git a/src/Features/LanguageServer/Protocol/Handler/Completion/AbstractLspCompletionResultCreationService.cs b/src/Features/LanguageServer/Protocol/Handler/Completion/AbstractLspCompletionResultCreationService.cs index b6c2785ef5eb7..cedb68d72c278 100644 --- a/src/Features/LanguageServer/Protocol/Handler/Completion/AbstractLspCompletionResultCreationService.cs +++ b/src/Features/LanguageServer/Protocol/Handler/Completion/AbstractLspCompletionResultCreationService.cs @@ -36,6 +36,7 @@ internal abstract class AbstractLspCompletionResultCreationService : ILspComplet CompletionList list, bool isIncomplete, long resultId, CancellationToken cancellationToken) { + var isSuggestionMode = list.SuggestionModeItem is not null; if (list.ItemsList.Count == 0) { return new LSP.VSInternalCompletionList @@ -43,7 +44,7 @@ internal abstract class AbstractLspCompletionResultCreationService : ILspComplet Items = Array.Empty(), // If we have a suggestion mode item, we just need to keep the list in suggestion mode. // We don't need to return the fake suggestion mode item. - SuggestionMode = list.SuggestionModeItem is not null, + SuggestionMode = isSuggestionMode, IsIncomplete = isIncomplete, }; } @@ -132,18 +133,24 @@ internal abstract class AbstractLspCompletionResultCreationService : ILspComplet lspItem.Kind = GetCompletionKind(item.Tags, capabilityHelper.SupportedItemKinds); lspItem.Preselect = item.Rules.MatchPriority == MatchPriority.Preselect; - if (!lspItem.Preselect && - !lspVSClientCapability && - typedText.Length == 0 && - item.Rules.SelectionBehavior != CompletionItemSelectionBehavior.HardSelection) + if (lspVSClientCapability) { - // VSCode does not have the concept of soft selection, the list is always hard selected. - // In order to emulate soft selection behavior for things like argument completion, regex completion, - // datetime completion, etc. we create a completion item without any specific commit characters. - // This means only tab / enter will commit. VS supports soft selection, so we only do this for non-VS clients. - // - // Note this only applies when user hasn't actually typed anything and completion provider does not request the item - // to be hard-selected. Otherwise, we set its commit characters as normal. This also means we'd need to set IsIncomplete to true + lspItem.CommitCharacters = GetCommitCharacters(item, commitCharactersRuleCache); + return lspItem; + } + + // VSCode does not have the concept of soft selection, the list is always hard selected. + // In order to emulate soft selection behavior for things like suggestion mode, argument completion, regex completion, + // datetime completion, etc. we create a completion item without any specific commit characters. + // This means only tab / enter will commit. VS supports soft selection, so we only do this for non-VS clients. + if (isSuggestionMode) + { + lspItem.CommitCharacters = Array.Empty(); + } + else if (!lspItem.Preselect && typedText.Length == 0 && item.Rules.SelectionBehavior != CompletionItemSelectionBehavior.HardSelection) + { + // Note this also applies when user hasn't actually typed anything and completion provider does not request the item + // to be hard-selected. Otherwise, we set its commit characters as normal. This means we'd need to set IsIncomplete to true // to make sure the client will ask us again when user starts typing so we can provide items with proper commit characters. lspItem.CommitCharacters = Array.Empty(); isIncomplete = true; diff --git a/src/Features/LanguageServer/ProtocolUnitTests/Completion/CompletionFeaturesTests.cs b/src/Features/LanguageServer/ProtocolUnitTests/Completion/CompletionFeaturesTests.cs index 4646ba90a4e09..acf33930b0180 100644 --- a/src/Features/LanguageServer/ProtocolUnitTests/Completion/CompletionFeaturesTests.cs +++ b/src/Features/LanguageServer/ProtocolUnitTests/Completion/CompletionFeaturesTests.cs @@ -20,6 +20,7 @@ using Microsoft.CodeAnalysis.Text; using Microsoft.VisualStudio.LanguageServer.Protocol; using Roslyn.Test.Utilities; +using Roslyn.Utilities; using Xunit; using Xunit.Abstractions; using LSP = Microsoft.VisualStudio.LanguageServer.Protocol; @@ -555,4 +556,39 @@ void M() var expectedAdditionalEdit2 = new TextEdit() { NewText = "using Namespace2;\r\n\r\n", Range = new() { Start = new(1, 0), End = new(1, 0) } }; AssertJsonEquals(new[] { expectedAdditionalEdit2 }, resolvedItem2.AdditionalTextEdits); } + + [Theory, CombinatorialData, WorkItem("https://github.com/dotnet/vscode-csharp/issues/5732")] + public async Task TestEmptyCommitCharsInSuggestionMode(bool mutatingLspWorkspace) + { + var markup = +@" +using System.Collections.Generic; +using System.Linq; +public class C +{ + public Foo(List myList) + { + var foo = myList.Where(i{|caret:|}) + } +}"; + await using var testLspServer = await CreateTestLspServerAsync(markup, mutatingLspWorkspace, DefaultClientCapabilities); + var caret = testLspServer.GetLocations("caret").Single(); + var completionParams = new LSP.CompletionParams() + { + TextDocument = CreateTextDocumentIdentifier(caret.Uri), + Position = caret.Range.Start, + Context = new LSP.CompletionContext() + { + TriggerKind = LSP.CompletionTriggerKind.Invoked, + } + }; + + var document = testLspServer.GetCurrentSolution().Projects.First().Documents.First(); + + var results = await testLspServer.ExecuteRequestAsync(LSP.Methods.TextDocumentCompletionName, completionParams, CancellationToken.None); + AssertEx.NotNull(results); + Assert.NotEmpty(results.Items); + Assert.Empty(results.ItemDefaults.CommitCharacters); + Assert.True(results.Items.All(item => item.CommitCharacters is null)); + } }