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

Add using directive support. #9982

Merged
merged 8 commits into from
Mar 1, 2024
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 @@ -30,6 +30,14 @@ internal class CSharpCodeParser : TokenizerBackedParser<CSharpTokenizer>
builder.Description = Resources.AddTagHelperDirective_Description;
});

internal static readonly DirectiveDescriptor UsingDirectiveDescriptor = DirectiveDescriptor.CreateDirective(
alexgav marked this conversation as resolved.
Show resolved Hide resolved
SyntaxConstants.CSharp.UsingKeyword,
DirectiveKind.SingleLine,
builder =>
{
builder.Description = Resources.UsingDirective_Description;
});

internal static readonly DirectiveDescriptor RemoveTagHelperDirectiveDescriptor = DirectiveDescriptor.CreateDirective(
SyntaxConstants.CSharp.RemoveTagHelperKeyword,
DirectiveKind.SingleLine,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ internal static class SyntaxConstants
public static class CSharp
{
public const int UsingKeywordLength = 5;
public const string UsingKeyword = "using";
public const string TagHelperPrefixKeyword = "tagHelperPrefix";
public const string AddTagHelperKeyword = "addTagHelper";
public const string RemoveTagHelperKeyword = "removeTagHelper";
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
Expand Down Expand Up @@ -595,4 +595,7 @@
<data name="DirectiveExpectsIdentifierOrExpression" xml:space="preserve">
<value>The '{0}' directive expects an identifier or explicit razor expression ("@()").</value>
</data>
</root>
<data name="UsingDirective_Description" xml:space="preserve">
<value>Adds the C# using directive to the generated view.</value>
</data>
</root>
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the MIT license. See License.txt in the project root for license information.

using System.Collections.Frozen;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Razor.LanguageServer.Protocol;
using Microsoft.VisualStudio.LanguageServer.Protocol;

namespace Microsoft.AspNetCore.Razor.LanguageServer.Completion.Delegation;

/// <summary>
/// Modifies delegated snippet completion items
/// </summary>
/// <remarks>
/// At the moment primarily used to modify C# "using" snippet to "using statement" snippet
/// in order to disambiguate it from Razor "using directive" snippet
/// </remarks>
internal class SnippetResponseRewriter : DelegatedCompletionResponseRewriter
{
private static readonly FrozenDictionary<string, (string Label, string SortText)> s_snippetToCompletionData = new Dictionary<string, (string Label, string SortText)>()
{
// Modifying label of the C# using snippet to "using statement" to disambiguate from
// Razor @using directive, and also appending a space to sort text to make sure it's sorted
// after Razor "using" keyword and "using directive ..." entries (which use "using" as sort text)
["using"] = (Label:$"using {SR.Statement}", SortText:"using ")
}
.ToFrozenDictionary();

public override int Order => ExecutionBehaviorOrder.ChangesCompletionItems;

public override Task<VSInternalCompletionList> RewriteAsync(VSInternalCompletionList completionList, int hostDocumentIndex, DocumentContext hostDocumentContext, DelegatedCompletionParams delegatedParameters, CancellationToken cancellationToken)
{
foreach (var item in completionList.Items)
{
if (item.Kind == CompletionItemKind.Snippet)
{
if (item.Label is null)
{
continue;
}

if (s_snippetToCompletionData.TryGetValue(item.Label, out var completionData))
{
item.Label = completionData.Label;
item.SortText = completionData.SortText;
}
}
}

return Task.FromResult(completionList);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,29 @@ public RazorCompletionItemResolver(
return null;
}

var associatedRazorCompletion = razorCompletionResolveContext.CompletionItems.FirstOrDefault(completion => string.Equals(completion.DisplayText, completionItem.Label, StringComparison.Ordinal));
var associatedRazorCompletion = razorCompletionResolveContext.CompletionItems.FirstOrDefault(completion =>
{
if (completion.DisplayText != completionItem.Label)
{
return false;
}

// We may have items of different types with the same label (e.g. snippet and keyword)
if (clientCapabilities is not null)
{
// CompletionItem.Kind and RazorCompletionItem.Kind are not compatible/comparable, so we need to convert
// Razor completion item to VS completion item (as logic to convert just the kind is not easy to separate from
// the rest of the conversion logic) prior to comparing them
if (RazorCompletionListProvider.TryConvert(completion, clientCapabilities, out var convertedRazorCompletionItem))
{
return completionItem.Kind == convertedRazorCompletionItem.Kind;
}
}

// If display text matches but we couldn't convert razor completion item to VS completion item for some reason,
// do what previous version of the code did and return true.
return true;
});
if (associatedRazorCompletion is null)
{
return null;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the MIT license. See License.txt in the project root for license information.

using System;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
Expand Down Expand Up @@ -50,7 +49,9 @@ public async Task<VSInternalCompletionItem> HandleRequestAsync(VSInternalComplet

// See if this is the right completion list for this corresponding completion item. We cross-check this based on label only given that
// is what users interact with.
if (cacheEntry.CompletionList.Items.Any(completion => string.Equals(completionItem.Label, completion.Label, StringComparison.Ordinal)))
if (cacheEntry.CompletionList.Items.Any(completion => completionItem.Label == completion.Label &&
// Check the Kind as well, e.g. we may have a Razor snippet and a C# keyword with the same label, etc.
completionItem.Kind == completion.Kind))
{
originalRequestContext = cacheEntry.Context;
containingCompletionList = cacheEntry.CompletionList;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@ public static void AddCompletionServices(this IServiceCollection services, Langu
services.AddSingleton<DelegatedCompletionResponseRewriter, TextEditResponseRewriter>();
services.AddSingleton<DelegatedCompletionResponseRewriter, DesignTimeHelperResponseRewriter>();
services.AddSingleton<DelegatedCompletionResponseRewriter, HtmlCommitCharacterResponseRewriter>();
services.AddSingleton<DelegatedCompletionResponseRewriter, SnippetResponseRewriter>();

services.AddSingleton<AggregateCompletionItemResolver>();
services.AddSingleton<CompletionItemResolver, RazorCompletionItemResolver>();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -195,4 +195,7 @@
<data name="Version_Should_Not_Be_Null" xml:space="preserve">
<value>Provided version should not be null.</value>
</data>
<data name="Statement" xml:space="preserve">
<value>statement</value>
</data>
</root>

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
#nullable disable

using System;
using System.Collections.Frozen;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Composition;
Expand All @@ -27,11 +28,15 @@ internal class DirectiveCompletionItemProvider : IRazorCompletionItemProvider
CSharpCodeParser.AddTagHelperDirectiveDescriptor,
CSharpCodeParser.RemoveTagHelperDirectiveDescriptor,
CSharpCodeParser.TagHelperPrefixDirectiveDescriptor,
CSharpCodeParser.UsingDirectiveDescriptor
};

// Test accessor
internal static IEnumerable<DirectiveDescriptor> DefaultDirectives => s_defaultDirectives;

// internal for testing
// Do not forget to update both insert and display text !important
internal static readonly IReadOnlyDictionary<string, (string InsertText, string DisplayText)> s_singleLineDirectiveSnippets = new Dictionary<string, (string InsertText, string DisplayText)>(StringComparer.Ordinal)
internal static readonly FrozenDictionary<string, (string InsertText, string DisplayText)> s_singleLineDirectiveSnippets = new Dictionary<string, (string InsertText, string DisplayText)>(StringComparer.Ordinal)
{
["addTagHelper"] = ("addTagHelper ${1:*}, ${2:Microsoft.AspNetCore.Mvc.TagHelpers}", "addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers"),
["attribute"] = ("attribute [${1:Authorize}]$0", "attribute [Authorize]"),
Expand All @@ -45,8 +50,10 @@ internal class DirectiveCompletionItemProvider : IRazorCompletionItemProvider
["preservewhitespace"] = ("preservewhitespace ${1:true}$0", "preservewhitespace true"),
["removeTagHelper"] = ("removeTagHelper ${1:*}, ${2:Microsoft.AspNetCore.Mvc.TagHelpers}", "removeTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers"),
["tagHelperPrefix"] = ("tagHelperPrefix ${1:prefix}$0", "tagHelperPrefix prefix"),
["typeparam"] = ("typeparam ${1:T}$0", "typeparam T")
};
["typeparam"] = ("typeparam ${1:T}$0", "typeparam T"),
["using"] = ("using ${1:MyNamespace}$0", "using MyNamespace")
}
.ToFrozenDictionary();

public ImmutableArray<RazorCompletionItem> GetCompletionItems(RazorCompletionContext context)
{
Expand Down Expand Up @@ -145,6 +152,10 @@ internal static ImmutableArray<RazorCompletionItem> GetDirectiveCompletionItems(
completionDisplayText,
directive.Directive,
RazorCompletionItemKind.Directive,
// Make sort text one less than display text so if there are any delegated completion items
// with the same display text in the combined completion list, they will be sorted below
// our items.
sortText: completionDisplayText,
commitCharacters: commitCharacters,
isSnippet: false);
var completionDescription = new DirectiveCompletionDescription(directive.Description);
Expand All @@ -154,9 +165,11 @@ internal static ImmutableArray<RazorCompletionItem> GetDirectiveCompletionItems(
if (s_singleLineDirectiveSnippets.TryGetValue(directive.Directive, out var snippetTexts))
{
var snippetCompletionItem = new RazorCompletionItem(
$"{completionDisplayText} ...",
$"{completionDisplayText} {SR.Directive} ...",
snippetTexts.InsertText,
RazorCompletionItemKind.Directive,
// Use the same sort text here as the directive completion item so both items are grouped together
sortText: completionDisplayText,
commitCharacters: commitCharacters,
isSnippet: true);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,9 @@
<data name="ArgumentCannotBeNullOrEmpty" xml:space="preserve">
<value>Value cannot be null or an empty string.</value>
</data>
<data name="Directive" xml:space="preserve">
<value>directive</value>
</data>
<data name="DirectiveSnippetDescription" xml:space="preserve">
<value>Insert a directive code snippet
[Tab] to navigate between elements, [Enter] to complete</value>
Expand Down
Loading