Skip to content

Commit

Permalink
Add using directive support. (#9982)
Browse files Browse the repository at this point in the history
* Add "ussing" keyword and "using" snippet, fixup tests

* Add snippet completion item re-writer so we can change C# "using"  snippet to say "using statement"

* CR feedback

* Removing unneeded using

* Update src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/Resources.resx

CR feedback

Co-authored-by: Dustin Campbell <[email protected]>

* Update src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Completion/RazorCompletionItemResolver.cs

CR feedback

Co-authored-by: Dustin Campbell <[email protected]>

* Restore ... suffix in snippet directive completion item labels

* Adjusting sort text

Adding space after "using" in the C# snippet sort text so that it's sorted after Razor "using" keyword and snippet instead of taking a substring with (length - 1) of directive name as Razor directive sort text.

---------

Co-authored-by: Dustin Campbell <[email protected]>
  • Loading branch information
alexgav and DustinCampbell authored Mar 1, 2024
1 parent c5c96a2 commit d4c396a
Show file tree
Hide file tree
Showing 42 changed files with 508 additions and 106 deletions.
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(
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

0 comments on commit d4c396a

Please sign in to comment.