File: Completion\DirectiveCompletionItemProvider.cs
Web Access
Project: src\src\Razor\src\Razor\src\Microsoft.CodeAnalysis.Razor.Workspaces\Microsoft.CodeAnalysis.Razor.Workspaces.csproj (Microsoft.CodeAnalysis.Razor.Workspaces)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
using System;
using System.Collections.Frozen;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using Microsoft.AspNetCore.Razor.Language;
using Microsoft.AspNetCore.Razor.Language.Legacy;
using Microsoft.AspNetCore.Razor.Language.Syntax;
using Microsoft.AspNetCore.Razor.PooledObjects;
 
namespace Microsoft.CodeAnalysis.Razor.Completion;
 
internal class DirectiveCompletionItemProvider : IRazorCompletionItemProvider
{
    internal static readonly ImmutableArray<RazorCommitCharacter> SingleLineDirectiveCommitCharacters = RazorCommitCharacter.CreateArray([" "]);
    internal static readonly ImmutableArray<RazorCommitCharacter> BlockDirectiveCommitCharacters = RazorCommitCharacter.CreateArray([" ", "{"]);
 
    // internal for testing
    internal static readonly ImmutableArray<DirectiveDescriptor> MvcDefaultDirectives = [
        CSharpCodeParser.AddTagHelperDirectiveDescriptor,
        CSharpCodeParser.RemoveTagHelperDirectiveDescriptor,
        CSharpCodeParser.TagHelperPrefixDirectiveDescriptor,
        CSharpCodeParser.UsingDirectiveDescriptor
    ];
 
    // internal for testing
    internal static readonly ImmutableArray<DirectiveDescriptor> ComponentDefaultDirectives = [
        CSharpCodeParser.UsingDirectiveDescriptor
    ];
 
    // internal for testing
    // Do not forget to update both insert and display text !important
    internal static readonly FrozenDictionary<string, (string InsertText, string DisplayText)> 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]"),
        ["implements"] = ("implements ${1:IDisposable}$0", "implements IDisposable"),
        ["inherits"] = ("inherits ${1:ComponentBase}$0", "inherits ComponentBase"),
        ["inject"] = ("inject ${1:IService} ${2:MyService}", "inject IService MyService"),
        ["layout"] = ("layout ${1:MainLayout}$0", "layout MainLayout"),
        ["model"] = ("model ${1:MyModelClass}$0", "model MyModelClass"),
        ["namespace"] = ("namespace ${1:MyNameSpace}$0", "namespace MyNameSpace"),
        ["page"] = ("page \"/${1:page}\"$0", "page \"/page\""),
        ["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"),
        ["using"] = ("using ${1:MyNamespace}$0", "using MyNamespace")
    }
    .ToFrozenDictionary();
 
    public ImmutableArray<RazorCompletionItem> GetCompletionItems(RazorCompletionContext context)
    {
        return ShouldProvideCompletions(context)
            ? GetDirectiveCompletionItems(context.SyntaxTree)
            : [];
    }
 
    // Internal for testing
    internal static bool ShouldProvideCompletions(RazorCompletionContext context)
    {
        if (context is null)
        {
            return false;
        }
 
        var owner = context.Owner;
        if (owner is null)
        {
            return false;
        }
 
        // Do not provide IntelliSense for explicit expressions. Explicit expressions will usually look like:
        // [@] [(] [DateTime.Now] [)]
        var implicitExpression = owner.FirstAncestorOrSelf<CSharpImplicitExpressionSyntax>();
        if (implicitExpression is null)
        {
            return false;
        }
 
        if (implicitExpression.Width > 2 && context.Reason != CompletionReason.Invoked)
        {
            // We only want to provide directive completions if the implicit expression is empty "@|" or at the beginning of a word "@i|", this ensures
            // we're consistent with how C# typically provides completion items.
            return false;
        }
 
        if (owner.ChildNodesAndTokens().Any(static x => !x.AsToken(out var token) || !IsDirectiveCompletableToken(token)))
        {
            // Implicit expression contains invalid directive tokens
            return false;
        }
 
        if (implicitExpression.FirstAncestorOrSelf<BaseRazorDirectiveSyntax>() != null)
        {
            // Implicit expression is nested in a directive
            return false;
        }
 
        if (implicitExpression.FirstAncestorOrSelf<CSharpStatementSyntax>() != null)
        {
            // Implicit expression is nested in a statement
            return false;
        }
 
        if (implicitExpression.FirstAncestorOrSelf<MarkupElementSyntax>() != null)
        {
            // Implicit expression is nested in an HTML element
            return false;
        }
 
        if (implicitExpression.FirstAncestorOrSelf<MarkupTagHelperElementSyntax>() != null)
        {
            // Implicit expression is nested in a TagHelper
            return false;
        }
 
        return true;
    }
 
    // Internal for testing
    internal static ImmutableArray<RazorCompletionItem> GetDirectiveCompletionItems(RazorSyntaxTree syntaxTree)
    {
        var defaultDirectives = syntaxTree.Options.FileKind.IsComponent()
            ? ComponentDefaultDirectives
            : MvcDefaultDirectives;
 
        ReadOnlySpan<DirectiveDescriptor> directives = [.. syntaxTree.Options.Directives, .. defaultDirectives];
 
        using var completionItems = new PooledArrayBuilder<RazorCompletionItem>(capacity: directives.Length + SingleLineDirectiveSnippets.Count);
 
        foreach (var directive in directives)
        {
            var completionDisplayText = directive.DisplayName ?? directive.Directive;
            var commitCharacters = GetDirectiveCommitCharacters(directive.Kind);
 
            var completionItem = RazorCompletionItem.CreateDirective(
                displayText: completionDisplayText,
                insertText: directive.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,
                descriptionInfo: new(directive.Description),
                commitCharacters,
                isSnippet: false);
 
            completionItems.Add(completionItem);
 
            if (SingleLineDirectiveSnippets.TryGetValue(directive.Directive, out var snippetTexts))
            {
                var snippetDescription = $"@{snippetTexts.DisplayText}{Environment.NewLine}{SR.DirectiveSnippetDescription}";
 
                var snippetCompletionItem = RazorCompletionItem.CreateDirective(
                    displayText: $"{completionDisplayText} {SR.Directive} ...",
                    insertText: snippetTexts.InsertText,
                    // Use the same sort text here as the directive completion item so both items are grouped together
                    sortText: completionDisplayText,
                    descriptionInfo: new(snippetDescription),
                    commitCharacters,
                    isSnippet: true);
 
                completionItems.Add(snippetCompletionItem);
            }
        }
 
        return completionItems.ToImmutableAndClear();
    }
 
    private static ImmutableArray<RazorCommitCharacter> GetDirectiveCommitCharacters(DirectiveKind directiveKind)
    {
        return directiveKind switch
        {
            DirectiveKind.CodeBlock or DirectiveKind.RazorBlock => BlockDirectiveCommitCharacters,
            _ => SingleLineDirectiveCommitCharacters,
        };
    }
 
    // Internal for testing
    internal static bool IsDirectiveCompletableToken(AspNetCore.Razor.Language.Syntax.SyntaxToken token)
    {
        return token is { Kind: SyntaxKind.Identifier or SyntaxKind.Marker or SyntaxKind.Keyword }
                     or { Kind: SyntaxKind.Transition, Parent.Kind: SyntaxKind.CSharpTransition };
    }
}