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;
+ }
+ }
+ }
+}