aspnetcore/src/Microsoft.VisualStudio.Edit.../RazorDirectiveCompletionPro...

220 lines
8.3 KiB
C#

// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.ComponentModel.Composition;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Razor.Language;
using Microsoft.AspNetCore.Razor.Language.Legacy;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Completion;
using Microsoft.CodeAnalysis.Razor;
using Microsoft.CodeAnalysis.Tags;
using Microsoft.CodeAnalysis.Text;
using Microsoft.VisualStudio.Text;
using Microsoft.VisualStudio.Text.Projection;
namespace Microsoft.VisualStudio.Editor.Razor
{
[System.Composition.Shared]
[Export(typeof(CompletionProvider))]
[ExportMetadata("Language", LanguageNames.CSharp)]
internal class RazorDirectiveCompletionProvider : CompletionProvider
{
// Internal for testing
internal static readonly string DescriptionKey = "Razor.Description";
private static readonly IEnumerable<DirectiveDescriptor> DefaultDirectives = new[]
{
CSharpCodeParser.AddTagHelperDirectiveDescriptor,
CSharpCodeParser.RemoveTagHelperDirectiveDescriptor,
CSharpCodeParser.TagHelperPrefixDirectiveDescriptor,
};
private readonly Lazy<RazorCodeDocumentProvider> _codeDocumentProvider;
[ImportingConstructor]
public RazorDirectiveCompletionProvider([Import(typeof(RazorCodeDocumentProvider))] Lazy<RazorCodeDocumentProvider> codeDocumentProvider)
{
if (codeDocumentProvider == null)
{
throw new ArgumentNullException(nameof(codeDocumentProvider));
}
_codeDocumentProvider = codeDocumentProvider;
}
public override Task<CompletionDescription> GetDescriptionAsync(Document document, CompletionItem item, CancellationToken cancellationToken)
{
if (document == null)
{
throw new ArgumentNullException(nameof(document));
}
if (item == null)
{
throw new ArgumentNullException(nameof(item));
}
var descriptionContent = new List<TaggedText>();
if (item.Properties.TryGetValue(DescriptionKey, out var directiveDescription))
{
var descriptionText = new TaggedText(TextTags.Text, directiveDescription);
descriptionContent.Add(descriptionText);
}
var completionDescription = CompletionDescription.Create(descriptionContent.ToImmutableArray());
return Task.FromResult(completionDescription);
}
public override Task ProvideCompletionsAsync(CompletionContext context)
{
if (context == null)
{
throw new ArgumentNullException(nameof(context));
}
// FilePath will be null when the editor is open for cases where we don't have a file on disk (C# interactive window and others).
if (context.Document?.FilePath == null ||
!context.Document.FilePath.EndsWith(".cshtml", StringComparison.OrdinalIgnoreCase))
{
// Not a Razor file.
return Task.CompletedTask;
}
var result = AddCompletionItems(context);
return result;
}
// We do not want this inlined because the work done in this method requires Razor.Workspaces and Razor.Language assemblies.
// If those two assemblies were to load you'd have them load in every C# editor completion scenario.
[MethodImpl(MethodImplOptions.NoInlining)]
private Task AddCompletionItems(CompletionContext context)
{
if (!_codeDocumentProvider.Value.TryGetFromDocument(context.Document, out var codeDocument))
{
// A Razor code document has not yet been associated with the document.
return Task.CompletedTask;
}
var syntaxTree = codeDocument.GetSyntaxTree();
if (syntaxTree == null)
{
// No syntax tree has been computed for the current document.
return Task.CompletedTask;
}
if (!AtDirectiveCompletionPoint(syntaxTree, context))
{
// Can't have a valid directive at the current location.
return Task.CompletedTask;
}
var completionItems = GetCompletionItems(syntaxTree);
context.AddItems(completionItems);
return Task.CompletedTask;
}
// Internal virtual for testing
internal virtual IEnumerable<CompletionItem> GetCompletionItems(RazorSyntaxTree syntaxTree)
{
var directives = syntaxTree.Options.Directives.Concat(DefaultDirectives);
var completionItems = new List<CompletionItem>();
foreach (var directive in directives)
{
var propertyDictionary = new Dictionary<string, string>(StringComparer.Ordinal);
if (!string.IsNullOrEmpty(directive.Description))
{
propertyDictionary[DescriptionKey] = directive.Description;
}
var completionItem = CompletionItem.Create(
directive.Directive,
// This groups all Razor directives together
sortText: "_RazorDirective_",
rules: CompletionItemRules.Create(formatOnCommit: false),
tags: ImmutableArray.Create(WellKnownTags.Intrinsic),
properties: propertyDictionary.ToImmutableDictionary());
completionItems.Add(completionItem);
}
return completionItems;
}
// Internal for testing
internal bool AtDirectiveCompletionPoint(RazorSyntaxTree syntaxTree, CompletionContext context)
{
if (TryGetRazorSnapshotPoint(context, out var razorSnapshotPoint))
{
var change = new SourceChange(razorSnapshotPoint.Position, 0, string.Empty);
var owner = syntaxTree.Root.LocateOwner(change);
if (owner == null)
{
return false;
}
if (owner.ChunkGenerator is ExpressionChunkGenerator &&
owner.Symbols.All(IsDirectiveCompletableSymbol) &&
// Do not provide IntelliSense for explicit expressions. Explicit expressions will usually look like:
// [@] [(] [DateTime.Now] [)]
owner.Parent?.Children.Count > 1 &&
owner.Parent.Children[1] == owner)
{
return true;
}
}
return false;
}
protected virtual bool TryGetRazorSnapshotPoint(CompletionContext context, out SnapshotPoint snapshotPoint)
{
snapshotPoint = default(SnapshotPoint);
if (context.Document.TryGetText(out var sourceText))
{
var textSnapshot = sourceText.FindCorrespondingEditorTextSnapshot();
var projectionSnapshot = textSnapshot as IProjectionSnapshot;
if (projectionSnapshot == null)
{
return false;
}
var mappedPoints = projectionSnapshot.MapToSourceSnapshots(context.CompletionListSpan.Start);
var htmlSnapshotPoints = mappedPoints.Where(p => p.Snapshot.TextBuffer.IsRazorBuffer());
if (!htmlSnapshotPoints.Any())
{
return false;
}
snapshotPoint = htmlSnapshotPoints.First();
return true;
}
return false;
}
private static bool IsDirectiveCompletableSymbol(AspNetCore.Razor.Language.Legacy.ISymbol symbol)
{
if (!(symbol is CSharpSymbol csharpSymbol))
{
return false;
}
return csharpSymbol.Type == CSharpSymbolType.Identifier ||
// Marker symbol
csharpSymbol.Type == CSharpSymbolType.Unknown;
}
}
}