Skip to content

Commit

Permalink
Pass Deprecated tag through to LSP completion
Browse files Browse the repository at this point in the history
  • Loading branch information
sharwell committed Feb 16, 2024
1 parent 75f8d0f commit da4a4b8
Show file tree
Hide file tree
Showing 8 changed files with 143 additions and 0 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
using Microsoft.CodeAnalysis.PooledObjects;
using Microsoft.CodeAnalysis.Shared.Extensions;
using Microsoft.CodeAnalysis.Shared.Utilities;
using Microsoft.CodeAnalysis.Tags;

namespace Microsoft.CodeAnalysis.Completion.Providers
{
Expand Down Expand Up @@ -56,6 +57,10 @@ private static CompletionItem CreateWorker(
AddSupportedPlatforms(builder, supportedPlatforms);
symbolEncoder(symbols, builder);

tags = tags.NullToEmpty();
if (symbols.All(symbol => symbol.IsObsolete()) && !tags.Contains(WellKnownTags.Deprecated))
tags = tags.Add(WellKnownTags.Deprecated);

var firstSymbol = symbols[0];
var item = CommonCompletionItem.Create(
displayText: displayText,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,17 @@ internal static partial class ProtocolConversions
{ WellKnownTags.NuGet, ImmutableArray.Create(LSP.CompletionItemKind.Text) }
}.ToImmutableDictionary();

/// <summary>
/// Mapping from tags to LSP completion item tags. The value lists the potential LSP tags from
/// least-preferred to most preferred. More preferred kinds will be chosen if the client states they support
/// it. This mapping allows values including extensions to the kinds defined by VS (but not in the core LSP
/// spec).
/// </summary>
public static readonly ImmutableDictionary<string, ImmutableArray<LSP.CompletionItemTag>> RoslynTagToCompletionItemTags = new Dictionary<string, ImmutableArray<LSP.CompletionItemTag>>()
{
{ WellKnownTags.Deprecated, ImmutableArray.Create(LSP.CompletionItemTag.Deprecated) },
}.ToImmutableDictionary();

// TO-DO: More LSP.CompletionTriggerKind mappings are required to properly map to Roslyn CompletionTriggerKinds.
// https://dev.azure.com/devdiv/DevDiv/_workitems/edit/1178726
public static async Task<Completion.CompletionTrigger> LSPToRoslynCompletionTriggerAsync(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
using Microsoft.CodeAnalysis.ErrorReporting;
using Microsoft.CodeAnalysis.LanguageService;
using Microsoft.CodeAnalysis.PooledObjects;
using Microsoft.CodeAnalysis.Shared.Collections;
using Microsoft.CodeAnalysis.Shared.Extensions;
using Microsoft.CodeAnalysis.Text;
using Roslyn.Utilities;
Expand Down Expand Up @@ -121,6 +122,7 @@ internal abstract class AbstractLspCompletionResultCreationService : ILspComplet
lspItem.FilterText = item.FilterText;

lspItem.Kind = GetCompletionKind(item.Tags, capabilityHelper.SupportedItemKinds);
lspItem.Tags = GetCompletionTags(item.Tags, capabilityHelper.SupportedItemTags);
lspItem.Preselect = item.Rules.MatchPriority == MatchPriority.Preselect;

if (lspVSClientCapability)
Expand Down Expand Up @@ -179,6 +181,37 @@ static LSP.CompletionItemKind GetCompletionKind(
return LSP.CompletionItemKind.Text;
}

static LSP.CompletionItemTag[]? GetCompletionTags(
ImmutableArray<string> tags,
ISet<LSP.CompletionItemTag> supportedClientTags)
{
using var result = TemporaryArray<LSP.CompletionItemTag>.Empty;

foreach (var tag in tags)
{
if (ProtocolConversions.RoslynTagToCompletionItemTags.TryGetValue(tag, out var completionItemTags))
{
// Always at least pick the core tag provided.
var lspTag = completionItemTags[0];

// If better kinds are preferred, return them if the client supports them.
for (var i = 1; i < completionItemTags.Length; i++)
{
var preferredTag = completionItemTags[i];
if (supportedClientTags.Contains(preferredTag))
lspTag = preferredTag;
}

result.Add(lspTag);
}
}

if (result.Count == 0)
return null;

return [.. result.ToImmutableAndClear()];
}

static string[] GetCommitCharacters(
CompletionItem item,
Dictionary<ImmutableArray<CharacterSetModificationRule>, string[]> currentRuleCache)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ internal sealed class CompletionCapabilityHelper
public bool SupportSnippets { get; }
public bool SupportsMarkdownDocumentation { get; }
public ISet<CompletionItemKind> SupportedItemKinds { get; }
public ISet<CompletionItemTag> SupportedItemTags { get; }

public CompletionCapabilityHelper(ClientCapabilities clientCapabilities)
{
Expand All @@ -42,6 +43,7 @@ public CompletionCapabilityHelper(ClientCapabilities clientCapabilities)
SupportCompletionListData = _completionSetting?.CompletionListSetting?.ItemDefaults?.Contains(DataPropertyName) == true;
SupportDefaultCommitCharacters = _completionSetting?.CompletionListSetting?.ItemDefaults?.Contains(CommitCharactersPropertyName) == true;
SupportedItemKinds = _completionSetting?.CompletionItemKind?.ValueSet?.ToSet() ?? SpecializedCollections.EmptySet<CompletionItemKind>();
SupportedItemTags = _completionSetting?.CompletionItem?.TagSupport?.ValueSet?.ToSet() ?? SpecializedCollections.EmptySet<CompletionItemTag>();

// internal VS LSP
if (clientCapabilities.HasVisualStudioLspCapability())
Expand Down
11 changes: 11 additions & 0 deletions src/Features/LanguageServer/Protocol/Protocol/CompletionItem.cs
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,17 @@ public CompletionItemKind Kind
set;
} = CompletionItemKind.None;

/// <summary>
/// Tags for this completion item.
/// </summary>
[DataMember(Name = "tags")]
[JsonProperty(DefaultValueHandling = DefaultValueHandling.Ignore)]
public CompletionItemTag[]? Tags
{
get;
set;
}

/// <summary>
/// Gets or sets the completion detail.
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,12 @@ private static void WriteCompletionItem(JsonWriter writer, CompletionItem comple
writer.WritePropertyName("kind");
writer.WriteValue(completionItem.Kind);

if (completionItem.Tags != null)
{
writer.WritePropertyName("tags");
serializer.Serialize(writer, completionItem.Tags);
}

if (completionItem.Detail != null)
{
writer.WritePropertyName("detail");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -496,6 +496,79 @@ public async Task TestUsingServerDefaultCommitCharacters(bool mutatingLspWorkspa
}
}

[Theory]
[CombinatorialData]
[WorkItem("https://github.com/dotnet/roslyn/issues/26488")]
public async Task TestCompletionForObsoleteSymbol(bool mutatingLspWorkspace)
{
var markup =
"""
using System;

[Obsolete]
class ObsoleteType;

class A
{
void M()
{
ObsoleteType{|caret:|}
}
}
""";

await using var testLspServer = await CreateTestLspServerAsync(markup, mutatingLspWorkspace, DefaultClientCapabilities);
var completionParams = CreateCompletionParams(
testLspServer.GetLocations("caret").Single(),
invokeKind: LSP.VSInternalCompletionInvokeKind.Explicit,
triggerCharacter: "\0",
triggerKind: LSP.CompletionTriggerKind.Invoked);

var document = testLspServer.GetCurrentSolution().Projects.First().Documents.First();

var completionResult = await testLspServer.ExecuteRequestAsync<LSP.CompletionParams, LSP.CompletionList>(LSP.Methods.TextDocumentCompletionName, completionParams, CancellationToken.None).ConfigureAwait(false);
Assert.NotNull(completionResult.ItemDefaults.EditRange);
Assert.NotNull(completionResult.ItemDefaults.Data);
Assert.NotNull(completionResult.ItemDefaults.CommitCharacters);

var actualItem = completionResult.Items.First(i => i.Label == "ObsoleteType");
Assert.Null(actualItem.LabelDetails);
Assert.Null(actualItem.SortText);
Assert.Equal(CompletionItemKind.Class, actualItem.Kind);
Assert.Equal([CompletionItemTag.Deprecated], actualItem.Tags);
Assert.Null(actualItem.FilterText);
Assert.Null(actualItem.TextEdit);
Assert.Null(actualItem.TextEditText);
Assert.Null(actualItem.AdditionalTextEdits);
Assert.Null(actualItem.Command);
Assert.Null(actualItem.CommitCharacters);
Assert.Null(actualItem.Data);
Assert.Null(actualItem.Detail);
Assert.Null(actualItem.Documentation);

actualItem.Data = completionResult.ItemDefaults.Data;

var resolvedItem = await testLspServer.ExecuteRequestAsync<LSP.CompletionItem, LSP.CompletionItem>(LSP.Methods.TextDocumentCompletionResolveName, actualItem, CancellationToken.None).ConfigureAwait(false);
Assert.Null(resolvedItem.LabelDetails);
Assert.Null(resolvedItem.SortText);
Assert.Equal(CompletionItemKind.Class, resolvedItem.Kind);
Assert.Equal([CompletionItemTag.Deprecated], resolvedItem.Tags);

Assert.Null(resolvedItem.AdditionalTextEdits);
Assert.Null(resolvedItem.FilterText);
Assert.Null(resolvedItem.TextEdit);
Assert.Null(resolvedItem.TextEditText);
Assert.Null(resolvedItem.Command);
Assert.Null(resolvedItem.Detail);

var expectedDocumentation = new MarkupContent()
{
Kind = LSP.MarkupKind.PlainText,
Value = "[deprecated] class ObsoleteType"
};
AssertJsonEquals(resolvedItem.Documentation, expectedDocumentation);
}

private sealed class CSharpLspMockCompletionService : CompletionService
{
private CSharpLspMockCompletionService(SolutionServices services, IAsynchronousOperationListenerProvider listenerProvider) : base(services, listenerProvider)
Expand Down
2 changes: 2 additions & 0 deletions src/Workspaces/Core/Portable/Tags/WellKnownTags.cs
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,8 @@ public static class WellKnownTags
public const string Error = nameof(Error);
public const string Warning = nameof(Warning);

internal const string Deprecated = nameof(Deprecated);

internal const string StatusInformation = nameof(StatusInformation);

internal const string AddReference = nameof(AddReference);
Expand Down

0 comments on commit da4a4b8

Please sign in to comment.