diff --git a/src/Microsoft.AspNetCore.Mvc.Razor.Extensions/InjectDirective.cs b/src/Microsoft.AspNetCore.Mvc.Razor.Extensions/InjectDirective.cs index fc6fabbac5..9e6c681a21 100644 --- a/src/Microsoft.AspNetCore.Mvc.Razor.Extensions/InjectDirective.cs +++ b/src/Microsoft.AspNetCore.Mvc.Razor.Extensions/InjectDirective.cs @@ -18,6 +18,7 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Extensions { builder.AddTypeToken().AddMemberToken(); builder.Usage = DirectiveUsage.FileScopedMultipleOccurring; + builder.Description = Resources.InjectDirective_Description; }); public static IRazorEngineBuilder Register(IRazorEngineBuilder builder) diff --git a/src/Microsoft.AspNetCore.Mvc.Razor.Extensions/ModelDirective.cs b/src/Microsoft.AspNetCore.Mvc.Razor.Extensions/ModelDirective.cs index db37b74ca6..5ce9ff3b5f 100644 --- a/src/Microsoft.AspNetCore.Mvc.Razor.Extensions/ModelDirective.cs +++ b/src/Microsoft.AspNetCore.Mvc.Razor.Extensions/ModelDirective.cs @@ -18,6 +18,7 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Extensions { builder.AddTypeToken(); builder.Usage = DirectiveUsage.FileScopedSinglyOccurring; + builder.Description = Resources.ModelDirective_Description; }); public static IRazorEngineBuilder Register(IRazorEngineBuilder builder) diff --git a/src/Microsoft.AspNetCore.Mvc.Razor.Extensions/NamespaceDirective.cs b/src/Microsoft.AspNetCore.Mvc.Razor.Extensions/NamespaceDirective.cs index 98ec0ea336..8a2ae03bd4 100644 --- a/src/Microsoft.AspNetCore.Mvc.Razor.Extensions/NamespaceDirective.cs +++ b/src/Microsoft.AspNetCore.Mvc.Razor.Extensions/NamespaceDirective.cs @@ -21,6 +21,7 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Extensions { builder.AddNamespaceToken(); builder.Usage = DirectiveUsage.FileScopedSinglyOccurring; + builder.Description = Resources.NamespaceDirective_Description; }); public static void Register(IRazorEngineBuilder builder) diff --git a/src/Microsoft.AspNetCore.Mvc.Razor.Extensions/PageDirective.cs b/src/Microsoft.AspNetCore.Mvc.Razor.Extensions/PageDirective.cs index 55f502b80d..6de2627600 100644 --- a/src/Microsoft.AspNetCore.Mvc.Razor.Extensions/PageDirective.cs +++ b/src/Microsoft.AspNetCore.Mvc.Razor.Extensions/PageDirective.cs @@ -19,6 +19,7 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Extensions { builder.AddOptionalStringToken(); builder.Usage = DirectiveUsage.FileScopedSinglyOccurring; + builder.Description = Resources.PageDirective_Description; }); private PageDirective(string routeTemplate, IntermediateNode directiveNode) diff --git a/src/Microsoft.AspNetCore.Mvc.Razor.Extensions/Properties/Resources.Designer.cs b/src/Microsoft.AspNetCore.Mvc.Razor.Extensions/Properties/Resources.Designer.cs index ccbf5f33b0..df83b83197 100644 --- a/src/Microsoft.AspNetCore.Mvc.Razor.Extensions/Properties/Resources.Designer.cs +++ b/src/Microsoft.AspNetCore.Mvc.Razor.Extensions/Properties/Resources.Designer.cs @@ -24,6 +24,34 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Extensions internal static string FormatArgumentCannotBeNullOrEmpy() => GetString("ArgumentCannotBeNullOrEmpy"); + /// + /// Declare a property and inject a service from the application's service container into it. + /// + internal static string InjectDirective_Description + { + get => GetString("InjectDirective_Description"); + } + + /// + /// Declare a property and inject a service from the application's service container into it. + /// + internal static string FormatInjectDirective_Description() + => GetString("InjectDirective_Description"); + + /// + /// Specify the view or page model for the current document. + /// + internal static string ModelDirective_Description + { + get => GetString("ModelDirective_Description"); + } + + /// + /// Specify the view or page model for the current document. + /// + internal static string FormatModelDirective_Description() + => GetString("ModelDirective_Description"); + /// /// The 'inherits' keyword is not allowed when a '{0}' keyword is used. /// @@ -94,6 +122,20 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Extensions internal static string FormatMvcRazorParser_InvalidPropertyType(object p0, object p1, object p2) => string.Format(CultureInfo.CurrentCulture, GetString("MvcRazorParser_InvalidPropertyType"), p0, p1, p2); + /// + /// Specify the base namespace for the current document. + /// + internal static string NamespaceDirective_Description + { + get => GetString("NamespaceDirective_Description"); + } + + /// + /// Specify the base namespace for the current document. + /// + internal static string FormatNamespaceDirective_Description() + => GetString("NamespaceDirective_Description"); + /// /// The '@{0}' directive specified in {1} file will not be imported. The directive must appear at the top of each Razor cshtml file. /// @@ -108,6 +150,20 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Extensions internal static string FormatPageDirectiveCannotBeImported(object p0, object p1) => string.Format(CultureInfo.CurrentCulture, GetString("PageDirectiveCannotBeImported"), p0, p1); + /// + /// Declare the current document as a Razor Page. + /// + internal static string PageDirective_Description + { + get => GetString("PageDirective_Description"); + } + + /// + /// Declare the current document as a Razor Page. + /// + internal static string FormatPageDirective_Description() + => GetString("PageDirective_Description"); + private static string GetString(string name, params string[] formatterNames) { var value = _resourceManager.GetString(name); diff --git a/src/Microsoft.AspNetCore.Mvc.Razor.Extensions/Resources.resx b/src/Microsoft.AspNetCore.Mvc.Razor.Extensions/Resources.resx index 6133d1dde0..dd2e475194 100644 --- a/src/Microsoft.AspNetCore.Mvc.Razor.Extensions/Resources.resx +++ b/src/Microsoft.AspNetCore.Mvc.Razor.Extensions/Resources.resx @@ -120,6 +120,12 @@ Value cannot be null or empty. + + Declare a property and inject a service from the application's service container into it. + + + Specify the view or page model for the current document. + The 'inherits' keyword is not allowed when a '{0}' keyword is used. @@ -135,7 +141,13 @@ Invalid tag helper property '{0}.{1}'. Dictionary values must not be of type '{2}'. + + Specify the base namespace for the current document. + The '@{0}' directive specified in {1} file will not be imported. The directive must appear at the top of each Razor cshtml file. + + Declare the current document as a Razor Page. + \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.Razor.Language/Extensions/FunctionsDirective.cs b/src/Microsoft.AspNetCore.Razor.Language/Extensions/FunctionsDirective.cs index c752f5b53f..f9033b8a83 100644 --- a/src/Microsoft.AspNetCore.Razor.Language/Extensions/FunctionsDirective.cs +++ b/src/Microsoft.AspNetCore.Razor.Language/Extensions/FunctionsDirective.cs @@ -9,7 +9,11 @@ namespace Microsoft.AspNetCore.Razor.Language.Extensions { public static readonly DirectiveDescriptor Directive = DirectiveDescriptor.CreateDirective( SyntaxConstants.CSharp.FunctionsKeyword, - DirectiveKind.CodeBlock); + DirectiveKind.CodeBlock, + builder => + { + builder.Description = Resources.FunctionsDirective_Description; + }); public static void Register(IRazorEngineBuilder builder) { diff --git a/src/Microsoft.AspNetCore.Razor.Language/Extensions/InheritsDirective.cs b/src/Microsoft.AspNetCore.Razor.Language/Extensions/InheritsDirective.cs index afc103f08e..337450feb5 100644 --- a/src/Microsoft.AspNetCore.Razor.Language/Extensions/InheritsDirective.cs +++ b/src/Microsoft.AspNetCore.Razor.Language/Extensions/InheritsDirective.cs @@ -14,6 +14,7 @@ namespace Microsoft.AspNetCore.Razor.Language.Extensions { builder.AddTypeToken(); builder.Usage = DirectiveUsage.FileScopedSinglyOccurring; + builder.Description = Resources.FunctionsDirective_Description; }); public static void Register(IRazorEngineBuilder builder) diff --git a/src/Microsoft.AspNetCore.Razor.Language/Extensions/SectionDirective.cs b/src/Microsoft.AspNetCore.Razor.Language/Extensions/SectionDirective.cs index cd3d2c9783..1ccd04aa31 100644 --- a/src/Microsoft.AspNetCore.Razor.Language/Extensions/SectionDirective.cs +++ b/src/Microsoft.AspNetCore.Razor.Language/Extensions/SectionDirective.cs @@ -10,7 +10,11 @@ namespace Microsoft.AspNetCore.Razor.Language.Extensions public static readonly DirectiveDescriptor Directive = DirectiveDescriptor.CreateDirective( SyntaxConstants.CSharp.SectionKeyword, DirectiveKind.RazorBlock, - builder => builder.AddMemberToken()); + builder => + { + builder.AddMemberToken(); + builder.Description = Resources.SectionDirective_Description; + }); public static void Register(IRazorEngineBuilder builder) { diff --git a/src/Microsoft.AspNetCore.Razor.Language/Legacy/CSharpCodeParser.cs b/src/Microsoft.AspNetCore.Razor.Language/Legacy/CSharpCodeParser.cs index 1f0c3aaad0..be60ad7081 100644 --- a/src/Microsoft.AspNetCore.Razor.Language/Legacy/CSharpCodeParser.cs +++ b/src/Microsoft.AspNetCore.Razor.Language/Legacy/CSharpCodeParser.cs @@ -21,17 +21,29 @@ namespace Microsoft.AspNetCore.Razor.Language.Legacy internal static readonly DirectiveDescriptor AddTagHelperDirectiveDescriptor = DirectiveDescriptor.CreateDirective( SyntaxConstants.CSharp.AddTagHelperKeyword, DirectiveKind.SingleLine, - builder => builder.AddStringToken()); + builder => + { + builder.AddStringToken(); + builder.Description = Resources.AddTagHelperDirective_Description; + }); internal static readonly DirectiveDescriptor RemoveTagHelperDirectiveDescriptor = DirectiveDescriptor.CreateDirective( SyntaxConstants.CSharp.RemoveTagHelperKeyword, DirectiveKind.SingleLine, - builder => builder.AddStringToken()); + builder => + { + builder.AddStringToken(); + builder.Description = Resources.RemoveTagHelperDirective_Description; + }); internal static readonly DirectiveDescriptor TagHelperPrefixDirectiveDescriptor = DirectiveDescriptor.CreateDirective( SyntaxConstants.CSharp.TagHelperPrefixKeyword, DirectiveKind.SingleLine, - builder => builder.AddStringToken()); + builder => + { + builder.AddStringToken(); + builder.Description = Resources.TagHelperPrefixDirective_Description; + }); internal static readonly IEnumerable DefaultDirectiveDescriptors = new DirectiveDescriptor[] { diff --git a/src/Microsoft.AspNetCore.Razor.Language/Properties/Resources.Designer.cs b/src/Microsoft.AspNetCore.Razor.Language/Properties/Resources.Designer.cs index 6d77589965..f8bd37e267 100644 --- a/src/Microsoft.AspNetCore.Razor.Language/Properties/Resources.Designer.cs +++ b/src/Microsoft.AspNetCore.Razor.Language/Properties/Resources.Designer.cs @@ -598,6 +598,90 @@ namespace Microsoft.AspNetCore.Razor.Language internal static string FormatCodeWriter_InvalidNewLine(object p0) => string.Format(CultureInfo.CurrentCulture, GetString("CodeWriter_InvalidNewLine"), p0); + /// + /// Register Tag Helpers for use in the current document. + /// + internal static string AddTagHelperDirective_Description + { + get => GetString("AddTagHelperDirective_Description"); + } + + /// + /// Register Tag Helpers for use in the current document. + /// + internal static string FormatAddTagHelperDirective_Description() + => GetString("AddTagHelperDirective_Description"); + + /// + /// Specify a C# code block. + /// + internal static string FunctionsDirective_Description + { + get => GetString("FunctionsDirective_Description"); + } + + /// + /// Specify a C# code block. + /// + internal static string FormatFunctionsDirective_Description() + => GetString("FunctionsDirective_Description"); + + /// + /// Specify the base class for the current document. + /// + internal static string InheritsDirective_Description + { + get => GetString("InheritsDirective_Description"); + } + + /// + /// Specify the base class for the current document. + /// + internal static string FormatInheritsDirective_Description() + => GetString("InheritsDirective_Description"); + + /// + /// Remove Tag Helpers for use in the current document. + /// + internal static string RemoveTagHelperDirective_Description + { + get => GetString("RemoveTagHelperDirective_Description"); + } + + /// + /// Remove Tag Helpers for use in the current document. + /// + internal static string FormatRemoveTagHelperDirective_Description() + => GetString("RemoveTagHelperDirective_Description"); + + /// + /// Define a section to be rendered in the configured layout page. + /// + internal static string SectionDirective_Description + { + get => GetString("SectionDirective_Description"); + } + + /// + /// Define a section to be rendered in the configured layout page. + /// + internal static string FormatSectionDirective_Description() + => GetString("SectionDirective_Description"); + + /// + /// Specify a prefix that is required in an element name for it to be included in Tag Helper processing. + /// + internal static string TagHelperPrefixDirective_Description + { + get => GetString("TagHelperPrefixDirective_Description"); + } + + /// + /// Specify a prefix that is required in an element name for it to be included in Tag Helper processing. + /// + internal static string FormatTagHelperPrefixDirective_Description() + => GetString("TagHelperPrefixDirective_Description"); + private static string GetString(string name, params string[] formatterNames) { var value = _resourceManager.GetString(name); diff --git a/src/Microsoft.AspNetCore.Razor.Language/Resources.resx b/src/Microsoft.AspNetCore.Razor.Language/Resources.resx index d611f9b458..f7c8033553 100644 --- a/src/Microsoft.AspNetCore.Razor.Language/Resources.resx +++ b/src/Microsoft.AspNetCore.Razor.Language/Resources.resx @@ -243,4 +243,22 @@ Invalid newline sequence '{0}'. Support newline sequences are '\r\n' and '\n'. + + Register Tag Helpers for use in the current document. + + + Specify a C# code block. + + + Specify the base class for the current document. + + + Remove Tag Helpers for use in the current document. + + + Define a section to be rendered in the configured layout page. + + + Specify a prefix that is required in an element name for it to be included in Tag Helper processing. + \ No newline at end of file diff --git a/src/Microsoft.CodeAnalysis.Razor.Workspaces/RazorCodeDocumentProvider.cs b/src/Microsoft.CodeAnalysis.Razor.Workspaces/RazorCodeDocumentProvider.cs new file mode 100644 index 0000000000..9eac4f8907 --- /dev/null +++ b/src/Microsoft.CodeAnalysis.Razor.Workspaces/RazorCodeDocumentProvider.cs @@ -0,0 +1,12 @@ +// 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 Microsoft.AspNetCore.Razor.Language; + +namespace Microsoft.CodeAnalysis.Razor +{ + public abstract class RazorCodeDocumentProvider + { + public abstract bool TryGetFromDocument(TextDocument document, out RazorCodeDocument codeDocument); + } +} diff --git a/src/Microsoft.VisualStudio.LanguageServices.Razor/Editor/DefaultCodeDocumentProvider.cs b/src/Microsoft.VisualStudio.LanguageServices.Razor/Editor/DefaultCodeDocumentProvider.cs new file mode 100644 index 0000000000..dd9e2f0781 --- /dev/null +++ b/src/Microsoft.VisualStudio.LanguageServices.Razor/Editor/DefaultCodeDocumentProvider.cs @@ -0,0 +1,59 @@ +// 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.ComponentModel.Composition; +using Microsoft.AspNetCore.Razor.Language; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Razor; + +namespace Microsoft.VisualStudio.LanguageServices.Razor.Editor +{ + [System.Composition.Shared] + [Export(typeof(RazorCodeDocumentProvider))] + internal class DefaultCodeDocumentProvider : RazorCodeDocumentProvider + { + private readonly RazorTextBufferProvider _bufferProvider; + private readonly VisualStudioCodeDocumentProvider _codeDocumentProvider; + + [ImportingConstructor] + public DefaultCodeDocumentProvider(RazorTextBufferProvider bufferProvider, VisualStudioCodeDocumentProvider codeDocumentProvider) + { + if (bufferProvider == null) + { + throw new ArgumentNullException(nameof(bufferProvider)); + } + + if (codeDocumentProvider == null) + { + throw new ArgumentNullException(nameof(codeDocumentProvider)); + } + + _bufferProvider = bufferProvider; + _codeDocumentProvider = codeDocumentProvider; + } + + public override bool TryGetFromDocument(TextDocument document, out RazorCodeDocument codeDocument) + { + if (document == null) + { + throw new ArgumentNullException(nameof(document)); + } + + if (!_bufferProvider.TryGetFromDocument(document, out var textBuffer)) + { + // Could not find a Razor buffer associated with the document. + codeDocument = null; + return false; + } + + if (!_codeDocumentProvider.TryGetFromBuffer(textBuffer, out codeDocument)) + { + // A Razor code document has not yet been associated with the buffer. + return false; + } + + return true; + } + } +} diff --git a/src/Microsoft.VisualStudio.LanguageServices.Razor/Editor/DefaultTextBufferProvider.cs b/src/Microsoft.VisualStudio.LanguageServices.Razor/Editor/DefaultTextBufferProvider.cs new file mode 100644 index 0000000000..6c3f86ad89 --- /dev/null +++ b/src/Microsoft.VisualStudio.LanguageServices.Razor/Editor/DefaultTextBufferProvider.cs @@ -0,0 +1,71 @@ +// 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.ComponentModel.Composition; +using System.Linq; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Text; +using Microsoft.VisualStudio.Text; +using Microsoft.VisualStudio.Text.Projection; + +namespace Microsoft.VisualStudio.LanguageServices.Razor.Editor +{ + [System.Composition.Shared] + [Export(typeof(RazorTextBufferProvider))] + internal class DefaultTextBufferProvider : RazorTextBufferProvider + { + private readonly IBufferGraphFactoryService _bufferGraphService; + + [ImportingConstructor] + public DefaultTextBufferProvider(IBufferGraphFactoryService bufferGraphService) + { + if (bufferGraphService == null) + { + throw new ArgumentNullException(nameof(bufferGraphService)); + } + + _bufferGraphService = bufferGraphService; + } + + public override bool TryGetFromDocument(TextDocument document, out ITextBuffer textBuffer) + { + if (document == null) + { + throw new ArgumentNullException(nameof(document)); + } + + textBuffer = null; + + if (!document.TryGetText(out var sourceText)) + { + // Could not retrieve source text from the document. We have no way have locating an ITextBuffer. + return false; + } + + var container = sourceText.Container; + ITextBuffer buffer; + try + { + buffer = container.GetTextBuffer(); + } + catch (ArgumentException) + { + // The source text container was not built from an ITextBuffer. + return false; + } + + var bufferGraph = _bufferGraphService.CreateBufferGraph(buffer); + var razorBuffer = bufferGraph.GetRazorBuffers().FirstOrDefault(); + + if (razorBuffer == null) + { + // Could not find a text buffer associated with the text document. + return false; + } + + textBuffer = razorBuffer; + return true; + } + } +} diff --git a/src/Microsoft.VisualStudio.LanguageServices.Razor/Editor/DefaultVisualStudioCodeDocumentProvider.cs b/src/Microsoft.VisualStudio.LanguageServices.Razor/Editor/DefaultVisualStudioCodeDocumentProvider.cs new file mode 100644 index 0000000000..ec282cf4b9 --- /dev/null +++ b/src/Microsoft.VisualStudio.LanguageServices.Razor/Editor/DefaultVisualStudioCodeDocumentProvider.cs @@ -0,0 +1,39 @@ +// 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.ComponentModel.Composition; +using Microsoft.AspNetCore.Razor.Language; +using Microsoft.VisualStudio.Text; + +namespace Microsoft.VisualStudio.LanguageServices.Razor.Editor +{ + [System.Composition.Shared] + [Export(typeof(VisualStudioCodeDocumentProvider))] + internal class DefaultVisualStudioCodeDocumentProvider : VisualStudioCodeDocumentProvider + { + public override bool TryGetFromBuffer(ITextBuffer textBuffer, out RazorCodeDocument codeDocument) + { + if (textBuffer == null) + { + throw new ArgumentNullException(nameof(textBuffer)); + } + + if (textBuffer.Properties.TryGetProperty(typeof(VisualStudioRazorParser), out VisualStudioRazorParser parser) && parser.CodeDocument != null) + { + codeDocument = parser.CodeDocument; + return true; + } + + // Support the legacy parser for code document extraction. + if (textBuffer.Properties.TryGetProperty(typeof(RazorEditorParser), out RazorEditorParser legacyParser) && legacyParser.CodeDocument != null) + { + codeDocument = legacyParser.CodeDocument; + return true; + } + + codeDocument = null; + return false; + } + } +} diff --git a/src/Microsoft.VisualStudio.LanguageServices.Razor/Editor/RazorDirectiveCompletionProvider.cs b/src/Microsoft.VisualStudio.LanguageServices.Razor/Editor/RazorDirectiveCompletionProvider.cs new file mode 100644 index 0000000000..963cfbf127 --- /dev/null +++ b/src/Microsoft.VisualStudio.LanguageServices.Razor/Editor/RazorDirectiveCompletionProvider.cs @@ -0,0 +1,198 @@ +// 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.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.Text; +using Microsoft.VisualStudio.Text; +using Microsoft.VisualStudio.Text.Projection; + +namespace Microsoft.VisualStudio.LanguageServices.Razor.Editor +{ + [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 DefaultDirectives = new[] + { + CSharpCodeParser.AddTagHelperDirectiveDescriptor, + CSharpCodeParser.RemoveTagHelperDirectiveDescriptor, + CSharpCodeParser.TagHelperPrefixDirectiveDescriptor, + }; + private readonly RazorCodeDocumentProvider _codeDocumentProvider; + + [ImportingConstructor] + public RazorDirectiveCompletionProvider(RazorCodeDocumentProvider codeDocumentProvider) + { + if (codeDocumentProvider == null) + { + throw new ArgumentNullException(nameof(codeDocumentProvider)); + } + + _codeDocumentProvider = codeDocumentProvider; + } + + public override Task 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(); + 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)); + } + + if (!context.Document.FilePath.EndsWith(".cshtml", StringComparison.OrdinalIgnoreCase)) + { + // Not a Razor file. + return Task.CompletedTask; + } + + if (!_codeDocumentProvider.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 GetCompletionItems(RazorSyntaxTree syntaxTree) + { + var directives = syntaxTree.Options.Directives.Concat(DefaultDirectives); + var completionItems = new List(); + foreach (var directive in directives) + { + var propertyDictionary = new Dictionary(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(CompletionTags.Intrinsic), + properties: propertyDictionary.ToImmutableDictionary()); + completionItems.Add(completionItem); + } + + return completionItems; + } + + private 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.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.ContentType.IsOfType(RazorLanguage.ContentType)); + + 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; + } + } +} diff --git a/src/Microsoft.VisualStudio.LanguageServices.Razor/Editor/RazorTextBufferProvider.cs b/src/Microsoft.VisualStudio.LanguageServices.Razor/Editor/RazorTextBufferProvider.cs new file mode 100644 index 0000000000..8c6dfbe8c5 --- /dev/null +++ b/src/Microsoft.VisualStudio.LanguageServices.Razor/Editor/RazorTextBufferProvider.cs @@ -0,0 +1,13 @@ +// 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 Microsoft.CodeAnalysis; +using Microsoft.VisualStudio.Text; + +namespace Microsoft.VisualStudio.LanguageServices.Razor.Editor +{ + internal abstract class RazorTextBufferProvider + { + public abstract bool TryGetFromDocument(TextDocument document, out ITextBuffer textBuffer); + } +} diff --git a/src/Microsoft.VisualStudio.LanguageServices.Razor/Editor/VisualStudioCodeDocumentProvider.cs b/src/Microsoft.VisualStudio.LanguageServices.Razor/Editor/VisualStudioCodeDocumentProvider.cs new file mode 100644 index 0000000000..1bfde5d00e --- /dev/null +++ b/src/Microsoft.VisualStudio.LanguageServices.Razor/Editor/VisualStudioCodeDocumentProvider.cs @@ -0,0 +1,13 @@ +// 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 Microsoft.AspNetCore.Razor.Language; +using Microsoft.VisualStudio.Text; + +namespace Microsoft.VisualStudio.LanguageServices.Razor.Editor +{ + internal abstract class VisualStudioCodeDocumentProvider + { + public abstract bool TryGetFromBuffer(ITextBuffer textBuffer, out RazorCodeDocument codeDocument); + } +} diff --git a/src/Microsoft.VisualStudio.LanguageServices.Razor/Editor/VisualStudioRazorParser.cs b/src/Microsoft.VisualStudio.LanguageServices.Razor/Editor/VisualStudioRazorParser.cs index 6bbd900efd..5e0d66d1ea 100644 --- a/src/Microsoft.VisualStudio.LanguageServices.Razor/Editor/VisualStudioRazorParser.cs +++ b/src/Microsoft.VisualStudio.LanguageServices.Razor/Editor/VisualStudioRazorParser.cs @@ -26,6 +26,12 @@ namespace Microsoft.VisualStudio.LanguageServices.Razor.Editor private readonly ForegroundDispatcher _dispatcher; private RazorSyntaxTreePartialParser _partialParser; + // For testing only + internal VisualStudioRazorParser(RazorCodeDocument codeDocument) + { + CodeDocument = codeDocument; + } + public VisualStudioRazorParser(ForegroundDispatcher dispatcher, ITextBuffer buffer, RazorTemplateEngine templateEngine, string filePath, ICompletionBroker completionBroker) { if (dispatcher == null) diff --git a/src/Microsoft.VisualStudio.LanguageServices.Razor/Legacy/RazorEditorParser.cs b/src/Microsoft.VisualStudio.LanguageServices.Razor/Legacy/RazorEditorParser.cs index 58757df9ef..0caebb1f68 100644 --- a/src/Microsoft.VisualStudio.LanguageServices.Razor/Legacy/RazorEditorParser.cs +++ b/src/Microsoft.VisualStudio.LanguageServices.Razor/Legacy/RazorEditorParser.cs @@ -19,6 +19,12 @@ namespace Microsoft.VisualStudio.LanguageServices.Razor private AspNetCore.Razor.Language.Legacy.Span _lastAutoCompleteSpan; private BackgroundParser _parser; + // For testing only. + internal RazorEditorParser(RazorCodeDocument codeDocument) + { + CodeDocument = codeDocument; + } + public RazorEditorParser(RazorTemplateEngine templateEngine, string filePath) { if (templateEngine == null) @@ -52,6 +58,8 @@ namespace Microsoft.VisualStudio.LanguageServices.Razor // Internal for testing. internal RazorSyntaxTree CurrentSyntaxTree { get; private set; } + internal RazorCodeDocument CodeDocument { get; private set; } + // Internal for testing. internal bool LastResultProvisional { get; private set; } @@ -156,6 +164,7 @@ namespace Microsoft.VisualStudio.LanguageServices.Razor using (_parser.SynchronizeMainThreadState()) { CurrentSyntaxTree = args.CodeDocument.GetSyntaxTree(); + CodeDocument = args.CodeDocument; _lastChangeOwner = null; } diff --git a/src/Microsoft.VisualStudio.LanguageServices.Razor/Microsoft.VisualStudio.LanguageServices.Razor.csproj b/src/Microsoft.VisualStudio.LanguageServices.Razor/Microsoft.VisualStudio.LanguageServices.Razor.csproj index 96f57ba222..e1080f6608 100644 --- a/src/Microsoft.VisualStudio.LanguageServices.Razor/Microsoft.VisualStudio.LanguageServices.Razor.csproj +++ b/src/Microsoft.VisualStudio.LanguageServices.Razor/Microsoft.VisualStudio.LanguageServices.Razor.csproj @@ -12,7 +12,7 @@ - + @@ -28,6 +28,7 @@ + KRB4002 diff --git a/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Editor/DefaultCodeDocumentProviderTest.cs b/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Editor/DefaultCodeDocumentProviderTest.cs new file mode 100644 index 0000000000..44630aaa8e --- /dev/null +++ b/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Editor/DefaultCodeDocumentProviderTest.cs @@ -0,0 +1,83 @@ +// 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 Microsoft.AspNetCore.Razor.Language; +using Microsoft.CodeAnalysis; +using Microsoft.VisualStudio.Text; +using Moq; +using Xunit; + +namespace Microsoft.VisualStudio.LanguageServices.Razor.Editor +{ + public class DefaultCodeDocumentProviderTest + { + [Fact] + public void TryGetFromDocument_ReturnsFalseIfBufferProviderCanNotGetAssociatedBuffer() + { + // Arrange + ITextBuffer textBuffer; + RazorCodeDocument codeDocument; + var bufferProvider = new Mock(); + bufferProvider.Setup(provider => provider.TryGetFromDocument(It.IsAny(), out textBuffer)) + .Returns(false); + var vsCodeDocumentProvider = new Mock(); + vsCodeDocumentProvider.Setup(provider => provider.TryGetFromBuffer(It.IsAny(), out codeDocument)) + .Returns(true); + var codeDocumentProvider = new DefaultCodeDocumentProvider(bufferProvider.Object, vsCodeDocumentProvider.Object); + var document = new Mock(); + + // Act + var result = codeDocumentProvider.TryGetFromDocument(document.Object, out codeDocument); + + // Assert + Assert.False(result); + Assert.Null(codeDocument); + } + + [Fact] + public void TryGetFromDocument_ReturnsFalseIfVSProviderCanNotGetCodeDocument() + { + // Arrange + var textBuffer = new Mock().Object; + RazorCodeDocument codeDocument; + var bufferProvider = new Mock(); + bufferProvider.Setup(provider => provider.TryGetFromDocument(It.IsAny(), out textBuffer)) + .Returns(true); + var vsCodeDocumentProvider = new Mock(); + vsCodeDocumentProvider.Setup(provider => provider.TryGetFromBuffer(It.Is(val => val == textBuffer), out codeDocument)) + .Returns(false); + var codeDocumentProvider = new DefaultCodeDocumentProvider(bufferProvider.Object, vsCodeDocumentProvider.Object); + var document = new Mock(); + + // Act + var result = codeDocumentProvider.TryGetFromDocument(document.Object, out codeDocument); + + // Assert + Assert.False(result); + Assert.Null(codeDocument); + } + + [Fact] + public void TryGetFromDocument_ReturnsTrueIfBothBufferAndVSProviderReturnTrue() + { + // Arrange + var textBuffer = new Mock().Object; + var expectedCodeDocument = new Mock().Object; + var bufferProvider = new Mock(); + bufferProvider.Setup(provider => provider.TryGetFromDocument(It.IsAny(), out textBuffer)) + .Returns(true); + var vsCodeDocumentProvider = new Mock(); + vsCodeDocumentProvider.Setup(provider => provider.TryGetFromBuffer(It.Is(val => val == textBuffer), out expectedCodeDocument)) + .Returns(true); + var codeDocumentProvider = new DefaultCodeDocumentProvider(bufferProvider.Object, vsCodeDocumentProvider.Object); + var document = new Mock(); + + // Act + var result = codeDocumentProvider.TryGetFromDocument(document.Object, out var codeDocument); + + // Assert + Assert.True(result); + Assert.Same(expectedCodeDocument, codeDocument); + } + } +} diff --git a/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Editor/DefaultTextBufferProviderTest.cs b/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Editor/DefaultTextBufferProviderTest.cs new file mode 100644 index 0000000000..ac9ed038d8 --- /dev/null +++ b/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Editor/DefaultTextBufferProviderTest.cs @@ -0,0 +1,156 @@ +// 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.ObjectModel; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Razor; +using Microsoft.CodeAnalysis.Text; +using Microsoft.VisualStudio.Text; +using Microsoft.VisualStudio.Text.Projection; +using Microsoft.VisualStudio.Utilities; +using Moq; +using Xunit; + +namespace Microsoft.VisualStudio.LanguageServices.Razor.Editor +{ + public class DefaultTextBufferProviderTest + { + [Fact] + public void TryGetFromDocument_ReturnsFalseIfCannotExtractSourceText() + { + // Arrange + var textBuffer = CreateTextBuffer(); + var bufferGraphService = CreateBufferGraphService(textBuffer); + var document = CreateDocumentWithoutText(); + var bufferProvider = new DefaultTextBufferProvider(bufferGraphService); + + // Act + var result = bufferProvider.TryGetFromDocument(document, out var buffer); + + // Assert + Assert.False(result); + Assert.Null(buffer); + } + + [Fact] + public void TryGetFromDocument_ReturnsFalseIfSourceContainerNotConstructedFromTextBuffer() + { + // Arrange + var bufferGraphService = CreateBufferGraphService(null); + var text = SourceText.From("Hello World"); + var document = CreateDocumentWithoutText(); + document = document.WithText(text); + var bufferProvider = new DefaultTextBufferProvider(bufferGraphService); + + // Act + var result = bufferProvider.TryGetFromDocument(document, out var buffer); + + // Assert + Assert.False(result); + Assert.Null(buffer); + } + + [Fact] + public void TryGetFromDocument_ReturnsFalseIfBufferGraphCanNotFindRazorBuffer() + { + // Arrange + var textBuffer = CreateTextBuffer(); + var bufferGraph = new Mock(); + bufferGraph.Setup(graph => graph.GetTextBuffers(It.IsAny>())) + .Returns(new Collection()); + var bufferGraphService = new Mock(); + bufferGraphService.Setup(service => service.CreateBufferGraph(textBuffer)) + .Returns(bufferGraph.Object); + var document = CreateDocument(textBuffer); + var bufferProvider = new DefaultTextBufferProvider(bufferGraphService.Object); + + // Act + var result = bufferProvider.TryGetFromDocument(document, out var buffer); + + // Assert + Assert.False(result); + Assert.Null(buffer); + } + + [Fact] + public void TryGetFromDocument_ReturnsTrueForValidDocuments() + { + // Arrange + var textBuffer = CreateTextBuffer(); + var bufferGraphService = CreateBufferGraphService(textBuffer); + var document = CreateDocument(textBuffer); + var bufferProvider = new DefaultTextBufferProvider(bufferGraphService); + + // Act + var result = bufferProvider.TryGetFromDocument(document, out var buffer); + + // Assert + Assert.True(result); + Assert.Same(textBuffer, buffer); + } + + private static Document CreateDocumentWithoutText() + { + var project = ProjectInfo + .Create(ProjectId.CreateNewId(), VersionStamp.Default, "TestProject", "TestAssembly", LanguageNames.CSharp) + .WithFilePath("/TestProject.csproj"); + var workspace = new AdhocWorkspace(); + workspace.AddProject(project); + var documentInfo = DocumentInfo.Create(DocumentId.CreateNewId(project.Id), "Test.cshtml"); + var document = workspace.AddDocument(documentInfo); + + return document; + } + + private static Document CreateDocument(ITextBuffer buffer) + { + var document = CreateDocumentWithoutText(); + var container = buffer.AsTextContainer(); + document = document.WithText(container.CurrentText); + return document; + } + + private static ITextBuffer CreateTextBuffer() + { + var textBuffer = new Mock(); + textBuffer.Setup(buffer => buffer.Properties) + .Returns(new PropertyCollection()); + + var textImage = new Mock(); + var textVersion = new Mock(); + var textBufferSnapshot = new Mock(); + textBufferSnapshot.Setup(snapshot => snapshot.TextImage) + .Returns(textImage.Object); + textBufferSnapshot.Setup(snapshot => snapshot.Length) + .Returns(0); + textBufferSnapshot.Setup(snapshot => snapshot.Version) + .Returns(textVersion.Object); + textBufferSnapshot.Setup(snapshot => snapshot.TextBuffer) + .Returns(() => textBuffer.Object); + + textBuffer.Setup(buffer => buffer.CurrentSnapshot) + .Returns(() => textBufferSnapshot.Object); + + var contentType = new Mock(); + contentType.Setup(type => type.IsOfType(It.IsAny())) + .Returns(val => val == RazorLanguage.ContentType); + textBuffer.Setup(buffer => buffer.ContentType) + .Returns(contentType.Object); + + return textBuffer.Object; + } + + private static IBufferGraphFactoryService CreateBufferGraphService(ITextBuffer buffer) + { + var bufferGraph = new Mock(); + bufferGraph.Setup(graph => graph.GetTextBuffers(It.IsAny>())) + .Returns>(predicate => predicate(buffer) ? new Collection() { buffer } : new Collection()); + var bufferGraphService = new Mock(); + bufferGraphService.Setup(service => service.CreateBufferGraph(buffer)) + .Returns(bufferGraph.Object); + + return bufferGraphService.Object; + } + } +} diff --git a/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Editor/DefaultVisualStudioCodeDocumentProviderTest.cs b/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Editor/DefaultVisualStudioCodeDocumentProviderTest.cs new file mode 100644 index 0000000000..3eebcde7b0 --- /dev/null +++ b/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Editor/DefaultVisualStudioCodeDocumentProviderTest.cs @@ -0,0 +1,98 @@ +// 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 Microsoft.AspNetCore.Razor.Language; +using Microsoft.VisualStudio.Text; +using Microsoft.VisualStudio.Utilities; +using Moq; +using Xunit; + +namespace Microsoft.VisualStudio.LanguageServices.Razor.Editor +{ + public class DefaultVisualStudioCodeDocumentProviderTest + { + [Fact] + public void TryGetFromBuffer_UsesVisualStudioRazorParserIfAvailable() + { + // Arrange + var expectedCodeDocument = TestRazorCodeDocument.Create("Hello World"); + var parser = new VisualStudioRazorParser(expectedCodeDocument); + var properties = new PropertyCollection(); + properties.AddProperty(typeof(VisualStudioRazorParser), parser); + var textBuffer = new Mock(); + textBuffer.Setup(buffer => buffer.Properties) + .Returns(properties); + var provider = new DefaultVisualStudioCodeDocumentProvider(); + + // Act + var result = provider.TryGetFromBuffer(textBuffer.Object, out var codeDocument); + + // Assert + Assert.True(result); + Assert.Same(expectedCodeDocument, codeDocument); + } + + [Fact] + public void TryGetFromBuffer_UsesRazorEditorParserIfAvailable() + { + // Arrange + var expectedCodeDocument = TestRazorCodeDocument.Create("Hello World"); + var parser = new RazorEditorParser(expectedCodeDocument); + var properties = new PropertyCollection(); + properties.AddProperty(typeof(RazorEditorParser), parser); + var textBuffer = new Mock(); + textBuffer.Setup(buffer => buffer.Properties) + .Returns(properties); + var provider = new DefaultVisualStudioCodeDocumentProvider(); + + // Act + var result = provider.TryGetFromBuffer(textBuffer.Object, out var codeDocument); + + // Assert + Assert.True(result); + Assert.Same(expectedCodeDocument, codeDocument); + } + + [Fact] + public void TryGetFromBuffer_PrefersVisualStudioRazorParserIfRazorEditorParserIsAvailable() + { + // Arrange + var properties = new PropertyCollection(); + var expectedCodeDocument = TestRazorCodeDocument.Create("Hello World"); + var parser = new VisualStudioRazorParser(expectedCodeDocument); + properties.AddProperty(typeof(VisualStudioRazorParser), parser); + var unexpectedCodeDocument = TestRazorCodeDocument.Create("Unexpected"); + var legacyParser = new RazorEditorParser(unexpectedCodeDocument); + properties.AddProperty(typeof(RazorEditorParser), legacyParser); + var textBuffer = new Mock(); + textBuffer.Setup(buffer => buffer.Properties) + .Returns(properties); + var provider = new DefaultVisualStudioCodeDocumentProvider(); + + // Act + var result = provider.TryGetFromBuffer(textBuffer.Object, out var codeDocument); + + // Assert + Assert.True(result); + Assert.Same(expectedCodeDocument, codeDocument); + } + + [Fact] + public void TryGetFromBuffer_FailsIfNoParserIsAvailable() + { + // Arrange + var properties = new PropertyCollection(); + var textBuffer = new Mock(); + textBuffer.Setup(buffer => buffer.Properties) + .Returns(properties); + var provider = new DefaultVisualStudioCodeDocumentProvider(); + + // Act + var result = provider.TryGetFromBuffer(textBuffer.Object, out var codeDocument); + + // Assert + Assert.False(result); + Assert.Null(codeDocument); + } + } +} diff --git a/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Editor/RazorDirectiveCompletionProviderTest.cs b/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Editor/RazorDirectiveCompletionProviderTest.cs new file mode 100644 index 0000000000..0e73407610 --- /dev/null +++ b/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Editor/RazorDirectiveCompletionProviderTest.cs @@ -0,0 +1,332 @@ +// 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.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Razor.Language; +using Microsoft.AspNetCore.Razor.Language.Extensions; +using Microsoft.AspNetCore.Razor.Language.Legacy; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Completion; +using Microsoft.CodeAnalysis.Options; +using Microsoft.CodeAnalysis.Razor; +using Microsoft.CodeAnalysis.Text; +using Microsoft.VisualStudio.Text; +using Moq; +using Xunit; + +namespace Microsoft.VisualStudio.LanguageServices.Razor.Editor +{ + public class RazorDirectiveCompletionProviderTest + { + private static readonly IReadOnlyList DefaultDirectives = new[] + { + CSharpCodeParser.AddTagHelperDirectiveDescriptor, + CSharpCodeParser.RemoveTagHelperDirectiveDescriptor, + CSharpCodeParser.TagHelperPrefixDirectiveDescriptor, + }; + + [Fact] + public async Task GetDescriptionAsync_AddsDirectiveDescriptionIfPropertyExists() + { + // Arrange + var document = CreateDocument(); + var expectedDescription = "The expected description"; + var item = CompletionItem.Create("TestDirective") + .WithProperties((new Dictionary() + { + [RazorDirectiveCompletionProvider.DescriptionKey] = expectedDescription, + }).ToImmutableDictionary()); + var codeDocumentProvider = new Mock(); + var completionProvider = new RazorDirectiveCompletionProvider(codeDocumentProvider.Object); + + // Act + var description = await completionProvider.GetDescriptionAsync(document, item, CancellationToken.None); + + // Assert + var part = Assert.Single(description.TaggedParts); + Assert.Equal(TextTags.Text, part.Tag); + Assert.Equal(expectedDescription, part.Text); + Assert.Equal(expectedDescription, description.Text); + } + + [Fact] + public async Task GetDescriptionAsync_DoesNotAddDescriptionWhenPropertyAbsent() + { + // Arrange + var document = CreateDocument(); + var item = CompletionItem.Create("TestDirective"); + var codeDocumentProvider = new Mock(); + var completionProvider = new RazorDirectiveCompletionProvider(codeDocumentProvider.Object); + + // Act + var description = await completionProvider.GetDescriptionAsync(document, item, CancellationToken.None); + + // Assert + Assert.Empty(description.TaggedParts); + Assert.Equal(string.Empty, description.Text); + } + + [Fact] + public async Task ProvideCompletionAsync_DoesNotProvideCompletionsForNonRazorFiles() + { + // Arrange + var codeDocumentProvider = new Mock(MockBehavior.Strict); + var completionProvider = new FailOnGetCompletionsProvider(codeDocumentProvider.Object); + var document = CreateDocument(); + document = document.WithFilePath("NotRazor.cs"); + var context = CreateContext(1, completionProvider, document); + + // Act & Assert + await completionProvider.ProvideCompletionsAsync(context); + } + + [Fact] + public async Task ProvideCompletionAsync_DoesNotProvideCompletionsWhenDocumentProviderCanNotGetDocument() + { + // Arrange + RazorCodeDocument codeDocument; + var codeDocumentProvider = new Mock(); + codeDocumentProvider.Setup(provider => provider.TryGetFromDocument(It.IsAny(), out codeDocument)) + .Returns(false); + var completionProvider = new FailOnGetCompletionsProvider(codeDocumentProvider.Object); + var document = CreateDocument(); + var context = CreateContext(1, completionProvider, document); + + // Act & Assert + await completionProvider.ProvideCompletionsAsync(context); + } + + [Fact] + public async Task ProvideCompletionAsync_DoesNotProvideCompletionsCanNotFindSnapshotPoint() + { + // Arrange + var codeDocumentProvider = CreateCodeDocumentProvider("@", Enumerable.Empty()); + var completionProvider = new FailOnGetCompletionsProvider(codeDocumentProvider, false); + var document = CreateDocument(); + var context = CreateContext(0, completionProvider, document); + + // Act & Assert + await completionProvider.ProvideCompletionsAsync(context); + } + + [Fact] + public async Task ProvideCompletionAsync_DoesNotProvideCompletionsWhenNotAtCompletionPoint() + { + // Arrange + var codeDocumentProvider = CreateCodeDocumentProvider("@", Enumerable.Empty()); + var completionProvider = new FailOnGetCompletionsProvider(codeDocumentProvider); + var document = CreateDocument(); + var context = CreateContext(0, completionProvider, document); + + // Act & Assert + await completionProvider.ProvideCompletionsAsync(context); + } + + [Theory] + [InlineData("DateTime.Now")] + [InlineData("SomeMethod()")] + public async Task ProvideCompletionAsync_DoesNotProvideCompletionsWhenAtComplexExpressions(string content) + { + // Arrange + var codeDocumentProvider = CreateCodeDocumentProvider("@" + content, Enumerable.Empty()); + var completionProvider = new FailOnGetCompletionsProvider(codeDocumentProvider); + var document = CreateDocument(); + var context = CreateContext(1, completionProvider, document); + + // Act & Assert + await completionProvider.ProvideCompletionsAsync(context); + } + + [Fact] + public async Task ProvideCompletionAsync_DoesNotProvideCompletionsForExplicitExpressions() + { + // Arrange + var codeDocumentProvider = CreateCodeDocumentProvider("@()", Enumerable.Empty()); + var completionProvider = new FailOnGetCompletionsProvider(codeDocumentProvider); + var document = CreateDocument(); + var context = CreateContext(2, completionProvider, document); + + // Act & Assert + await completionProvider.ProvideCompletionsAsync(context); + } + + [Fact] + public async Task ProvideCompletionAsync_DoesNotProvideCompletionsForCodeDocumentWithoutSyntaxTree() + { + // Arrange + var codeDocumentProvider = new Mock(); + var codeDocument = TestRazorCodeDocument.CreateEmpty(); + codeDocumentProvider.Setup(provider => provider.TryGetFromDocument(It.IsAny(), out codeDocument)) + .Returns(true); + var completionProvider = new FailOnGetCompletionsProvider(codeDocumentProvider.Object); + var document = CreateDocument(); + var context = CreateContext(2, completionProvider, document); + + // Act & Assert + await completionProvider.ProvideCompletionsAsync(context); + } + + [Fact] + public void GetCompletionItems_ProvidesCompletionsForDefaultDirectives() + { + // Arrange + var codeDocumentProvider = CreateCodeDocumentProvider("@", Enumerable.Empty()); + var completionProvider = new RazorDirectiveCompletionProvider(codeDocumentProvider); + var document = CreateDocument(); + codeDocumentProvider.TryGetFromDocument(document, out var codeDocument); + var syntaxTree = codeDocument.GetSyntaxTree(); + + // Act + var completionItems = completionProvider.GetCompletionItems(syntaxTree); + + // Assert + Assert.Collection( + completionItems, + item => AssertRazorCompletionItem(DefaultDirectives[0].Description, item), + item => AssertRazorCompletionItem(DefaultDirectives[1].Description, item), + item => AssertRazorCompletionItem(DefaultDirectives[2].Description, item)); + } + + [Fact] + public void GetCompletionItems_ProvidesCompletionsForDefaultAndExtensibleDirectives() + { + // Arrange + var codeDocumentProvider = CreateCodeDocumentProvider("@", new[] { SectionDirective.Directive }); + var completionProvider = new RazorDirectiveCompletionProvider(codeDocumentProvider); + var document = CreateDocument(); + codeDocumentProvider.TryGetFromDocument(document, out var codeDocument); + var syntaxTree = codeDocument.GetSyntaxTree(); + + // Act + var completionItems = completionProvider.GetCompletionItems(syntaxTree); + + // Assert + Assert.Collection( + completionItems, + item => AssertRazorCompletionItem(SectionDirective.Directive.Description, item), + item => AssertRazorCompletionItem(DefaultDirectives[0].Description, item), + item => AssertRazorCompletionItem(DefaultDirectives[1].Description, item), + item => AssertRazorCompletionItem(DefaultDirectives[2].Description, item)); + } + + [Fact] + public void GetCompletionItems_ProvidesCompletionsForDirectivesWithoutDescription() + { + // Arrange + var customDirective = DirectiveDescriptor.CreateSingleLineDirective("custom"); + var codeDocumentProvider = CreateCodeDocumentProvider("@", new[] { customDirective }); + var completionProvider = new RazorDirectiveCompletionProvider(codeDocumentProvider); + var document = CreateDocument(); + codeDocumentProvider.TryGetFromDocument(document, out var codeDocument); + var syntaxTree = codeDocument.GetSyntaxTree(); + + // Act + var completionItems = completionProvider.GetCompletionItems(syntaxTree); + + // Assert + var customDirectiveCompletion = Assert.Single(completionItems, item => item.DisplayText == customDirective.Directive); + AssertRazorCompletionItemDefaults(customDirectiveCompletion); + Assert.DoesNotContain(customDirectiveCompletion.Properties, property => property.Key == RazorDirectiveCompletionProvider.DescriptionKey); + } + + private static void AssertRazorCompletionItem(string expectedDescription, CompletionItem item) + { + Assert.True(item.Properties.TryGetValue(RazorDirectiveCompletionProvider.DescriptionKey, out var actualDescription)); + Assert.Equal(expectedDescription, actualDescription); + + AssertRazorCompletionItemDefaults(item); + } + + private static void AssertRazorCompletionItemDefaults(CompletionItem item) + { + Assert.Equal("_RazorDirective_", item.SortText); + Assert.False(item.Rules.FormatOnCommit); + var tag = Assert.Single(item.Tags); + Assert.Equal(CompletionTags.Intrinsic, tag); + } + + private static RazorCodeDocumentProvider CreateCodeDocumentProvider(string text, IEnumerable directives) + { + var codeDocumentProvider = new Mock(); + var codeDocument = TestRazorCodeDocument.CreateEmpty(); + var sourceDocument = TestRazorSourceDocument.Create(text); + var options = RazorParserOptions.Create(builder => + { + foreach (var directive in directives) + { + builder.Directives.Add(directive); + } + }); + var syntaxTree = RazorSyntaxTree.Parse(sourceDocument, options); + codeDocument.SetSyntaxTree(syntaxTree); + codeDocumentProvider.Setup(provider => provider.TryGetFromDocument(It.IsAny(), out codeDocument)) + .Returns(true); + + return codeDocumentProvider.Object; + } + + private static CompletionContext CreateContext(int position, RazorDirectiveCompletionProvider completionProvider, Document document) + { + var context = new CompletionContext( + completionProvider, + document, + position, + TextSpan.FromBounds(position, position), + CompletionTrigger.Invoke, + new Mock().Object, + CancellationToken.None); + + return context; + } + + private static Document CreateDocument() + { + var project = ProjectInfo + .Create(ProjectId.CreateNewId(), VersionStamp.Default, "TestProject", "TestAssembly", LanguageNames.CSharp) + .WithFilePath("/TestProject.csproj"); + var workspace = new AdhocWorkspace(); + workspace.AddProject(project); + var documentInfo = DocumentInfo.Create(DocumentId.CreateNewId(project.Id), "Test.cshtml"); + var document = workspace.AddDocument(documentInfo); + document = document.WithFilePath("Test.cshtml"); + + return document; + } + + private class FailOnGetCompletionsProvider : RazorDirectiveCompletionProvider + { + private readonly bool _canGetSnapshotPoint; + + public FailOnGetCompletionsProvider(RazorCodeDocumentProvider codeDocumentProvider, bool canGetSnapshotPoint = true) + : base(codeDocumentProvider) + { + _canGetSnapshotPoint = canGetSnapshotPoint; + } + + internal override IEnumerable GetCompletionItems(RazorSyntaxTree syntaxTree) + { + Assert.False(true, "Completions should not have been attempted."); + return null; + } + + protected override bool TryGetRazorSnapshotPoint(CompletionContext context, out SnapshotPoint snapshotPoint) + { + if (!_canGetSnapshotPoint) + { + snapshotPoint = default(SnapshotPoint); + return false; + } + + var snapshot = new Mock(MockBehavior.Strict); + snapshot.Setup(s => s.Length) + .Returns(context.CompletionListSpan.End); + snapshotPoint = new SnapshotPoint(snapshot.Object, context.CompletionListSpan.Start); + return true; + } + } + } +}