Add basic Razor directive completion.

- Added APIs to retrieve an `ITextBuffer` from a `Document` and to retrieve a `RazorCodeDocument` from an `ITextBuffer`.
- The `RazorCodeDocument` from `ITextBuffer` API supports both the new and old Razor parsers so we can transition seamlessly between the two.
- Added logic in the `RazorDirectiveCompletionProvider` to consume descriptions from `DirectiveDescriptor`s. This is then surfaced via tooltips.
- Retrieved currently active `RazorCodeDocument` given a Roslyn buffer and harvested all directives to display in the completion list.
- Added unit tests to validate each new services functionality.

#291
This commit is contained in:
N. Taylor Mullen 2017-08-29 15:53:20 -07:00
parent 32d5391ff0
commit 61260ddf1c
26 changed files with 1291 additions and 6 deletions

View File

@ -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)

View File

@ -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)

View File

@ -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)

View File

@ -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)

View File

@ -24,6 +24,34 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Extensions
internal static string FormatArgumentCannotBeNullOrEmpy()
=> GetString("ArgumentCannotBeNullOrEmpy");
/// <summary>
/// Declare a property and inject a service from the application's service container into it.
/// </summary>
internal static string InjectDirective_Description
{
get => GetString("InjectDirective_Description");
}
/// <summary>
/// Declare a property and inject a service from the application's service container into it.
/// </summary>
internal static string FormatInjectDirective_Description()
=> GetString("InjectDirective_Description");
/// <summary>
/// Specify the view or page model for the current document.
/// </summary>
internal static string ModelDirective_Description
{
get => GetString("ModelDirective_Description");
}
/// <summary>
/// Specify the view or page model for the current document.
/// </summary>
internal static string FormatModelDirective_Description()
=> GetString("ModelDirective_Description");
/// <summary>
/// The 'inherits' keyword is not allowed when a '{0}' keyword is used.
/// </summary>
@ -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);
/// <summary>
/// Specify the base namespace for the current document.
/// </summary>
internal static string NamespaceDirective_Description
{
get => GetString("NamespaceDirective_Description");
}
/// <summary>
/// Specify the base namespace for the current document.
/// </summary>
internal static string FormatNamespaceDirective_Description()
=> GetString("NamespaceDirective_Description");
/// <summary>
/// The '@{0}' directive specified in {1} file will not be imported. The directive must appear at the top of each Razor cshtml file.
/// </summary>
@ -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);
/// <summary>
/// Declare the current document as a Razor Page.
/// </summary>
internal static string PageDirective_Description
{
get => GetString("PageDirective_Description");
}
/// <summary>
/// Declare the current document as a Razor Page.
/// </summary>
internal static string FormatPageDirective_Description()
=> GetString("PageDirective_Description");
private static string GetString(string name, params string[] formatterNames)
{
var value = _resourceManager.GetString(name);

View File

@ -120,6 +120,12 @@
<data name="ArgumentCannotBeNullOrEmpy" xml:space="preserve">
<value>Value cannot be null or empty.</value>
</data>
<data name="InjectDirective_Description" xml:space="preserve">
<value>Declare a property and inject a service from the application's service container into it.</value>
</data>
<data name="ModelDirective_Description" xml:space="preserve">
<value>Specify the view or page model for the current document.</value>
</data>
<data name="MvcRazorCodeParser_CannotHaveModelAndInheritsKeyword" xml:space="preserve">
<value>The 'inherits' keyword is not allowed when a '{0}' keyword is used.</value>
</data>
@ -135,7 +141,13 @@
<data name="MvcRazorParser_InvalidPropertyType" xml:space="preserve">
<value>Invalid tag helper property '{0}.{1}'. Dictionary values must not be of type '{2}'.</value>
</data>
<data name="NamespaceDirective_Description" xml:space="preserve">
<value>Specify the base namespace for the current document.</value>
</data>
<data name="PageDirectiveCannotBeImported" xml:space="preserve">
<value>The '@{0}' directive specified in {1} file will not be imported. The directive must appear at the top of each Razor cshtml file.</value>
</data>
<data name="PageDirective_Description" xml:space="preserve">
<value>Declare the current document as a Razor Page.</value>
</data>
</root>

View File

@ -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)
{

View File

@ -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)

View File

@ -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)
{

View File

@ -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<DirectiveDescriptor> DefaultDirectiveDescriptors = new DirectiveDescriptor[]
{

View File

@ -598,6 +598,90 @@ namespace Microsoft.AspNetCore.Razor.Language
internal static string FormatCodeWriter_InvalidNewLine(object p0)
=> string.Format(CultureInfo.CurrentCulture, GetString("CodeWriter_InvalidNewLine"), p0);
/// <summary>
/// Register Tag Helpers for use in the current document.
/// </summary>
internal static string AddTagHelperDirective_Description
{
get => GetString("AddTagHelperDirective_Description");
}
/// <summary>
/// Register Tag Helpers for use in the current document.
/// </summary>
internal static string FormatAddTagHelperDirective_Description()
=> GetString("AddTagHelperDirective_Description");
/// <summary>
/// Specify a C# code block.
/// </summary>
internal static string FunctionsDirective_Description
{
get => GetString("FunctionsDirective_Description");
}
/// <summary>
/// Specify a C# code block.
/// </summary>
internal static string FormatFunctionsDirective_Description()
=> GetString("FunctionsDirective_Description");
/// <summary>
/// Specify the base class for the current document.
/// </summary>
internal static string InheritsDirective_Description
{
get => GetString("InheritsDirective_Description");
}
/// <summary>
/// Specify the base class for the current document.
/// </summary>
internal static string FormatInheritsDirective_Description()
=> GetString("InheritsDirective_Description");
/// <summary>
/// Remove Tag Helpers for use in the current document.
/// </summary>
internal static string RemoveTagHelperDirective_Description
{
get => GetString("RemoveTagHelperDirective_Description");
}
/// <summary>
/// Remove Tag Helpers for use in the current document.
/// </summary>
internal static string FormatRemoveTagHelperDirective_Description()
=> GetString("RemoveTagHelperDirective_Description");
/// <summary>
/// Define a section to be rendered in the configured layout page.
/// </summary>
internal static string SectionDirective_Description
{
get => GetString("SectionDirective_Description");
}
/// <summary>
/// Define a section to be rendered in the configured layout page.
/// </summary>
internal static string FormatSectionDirective_Description()
=> GetString("SectionDirective_Description");
/// <summary>
/// Specify a prefix that is required in an element name for it to be included in Tag Helper processing.
/// </summary>
internal static string TagHelperPrefixDirective_Description
{
get => GetString("TagHelperPrefixDirective_Description");
}
/// <summary>
/// Specify a prefix that is required in an element name for it to be included in Tag Helper processing.
/// </summary>
internal static string FormatTagHelperPrefixDirective_Description()
=> GetString("TagHelperPrefixDirective_Description");
private static string GetString(string name, params string[] formatterNames)
{
var value = _resourceManager.GetString(name);

View File

@ -243,4 +243,22 @@
<data name="CodeWriter_InvalidNewLine" xml:space="preserve">
<value>Invalid newline sequence '{0}'. Support newline sequences are '\r\n' and '\n'.</value>
</data>
<data name="AddTagHelperDirective_Description" xml:space="preserve">
<value>Register Tag Helpers for use in the current document.</value>
</data>
<data name="FunctionsDirective_Description" xml:space="preserve">
<value>Specify a C# code block.</value>
</data>
<data name="InheritsDirective_Description" xml:space="preserve">
<value>Specify the base class for the current document.</value>
</data>
<data name="RemoveTagHelperDirective_Description" xml:space="preserve">
<value>Remove Tag Helpers for use in the current document.</value>
</data>
<data name="SectionDirective_Description" xml:space="preserve">
<value>Define a section to be rendered in the configured layout page.</value>
</data>
<data name="TagHelperPrefixDirective_Description" xml:space="preserve">
<value>Specify a prefix that is required in an element name for it to be included in Tag Helper processing.</value>
</data>
</root>

View File

@ -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);
}
}

View File

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

View File

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

View File

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

View File

@ -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<DirectiveDescriptor> 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<CompletionDescription> GetDescriptionAsync(Document document, CompletionItem item, CancellationToken cancellationToken)
{
if (document == null)
{
throw new ArgumentNullException(nameof(document));
}
if (item == null)
{
throw new ArgumentNullException(nameof(item));
}
var descriptionContent = new List<TaggedText>();
if (item.Properties.TryGetValue(DescriptionKey, out var directiveDescription))
{
var descriptionText = new TaggedText(TextTags.Text, directiveDescription);
descriptionContent.Add(descriptionText);
}
var completionDescription = CompletionDescription.Create(descriptionContent.ToImmutableArray());
return Task.FromResult(completionDescription);
}
public override Task ProvideCompletionsAsync(CompletionContext context)
{
if (context == null)
{
throw new ArgumentNullException(nameof(context));
}
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<CompletionItem> GetCompletionItems(RazorSyntaxTree syntaxTree)
{
var directives = syntaxTree.Options.Directives.Concat(DefaultDirectives);
var completionItems = new List<CompletionItem>();
foreach (var directive in directives)
{
var propertyDictionary = new Dictionary<string, string>(StringComparer.Ordinal);
if (!string.IsNullOrEmpty(directive.Description))
{
propertyDictionary[DescriptionKey] = directive.Description;
}
var completionItem = CompletionItem.Create(
directive.Directive,
// This groups all Razor directives together
sortText: "_RazorDirective_",
rules: CompletionItemRules.Create(formatOnCommit: false),
tags: ImmutableArray.Create(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;
}
}
}

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -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)

View File

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

View File

@ -12,7 +12,7 @@
<ItemGroup>
<PackageReference Include="Microsoft.CodeAnalysis.Common" Version="$(RoslynDevVersion)" NoWarn="KRB4002" />
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="$(RoslynDevVersion)" NoWarn="KRB4002" />
<PackageReference Include="Microsoft.CodeAnalysis.CSharp.Features" Version="$(RoslynDevVersion)" NoWarn="KRB4002" />
<PackageReference Include="Microsoft.CodeAnalysis.EditorFeatures.Text" Version="$(RoslynDevVersion)" NoWarn="KRB4002" />
<PackageReference Include="Microsoft.CodeAnalysis.Workspaces.Common" Version="$(RoslynDevVersion)" NoWarn="KRB4002" />
<PackageReference Include="Microsoft.VisualStudio.ComponentModelHost" />
@ -28,6 +28,7 @@
<PackageReference Include="Microsoft.VisualStudio.Shell.Interop.8.0" />
<PackageReference Include="Microsoft.VisualStudio.Shell.Interop.9.0" />
<PackageReference Include="Microsoft.VisualStudio.Shell.Interop" />
<PackageReference Include="Newtonsoft.Json" Version="9.0.1">
<!-- We need to use this version of Json.Net to maintain consistency with Visual Studio. -->
<NoWarn>KRB4002</NoWarn>

View File

@ -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<RazorTextBufferProvider>();
bufferProvider.Setup(provider => provider.TryGetFromDocument(It.IsAny<TextDocument>(), out textBuffer))
.Returns(false);
var vsCodeDocumentProvider = new Mock<VisualStudioCodeDocumentProvider>();
vsCodeDocumentProvider.Setup(provider => provider.TryGetFromBuffer(It.IsAny<ITextBuffer>(), out codeDocument))
.Returns(true);
var codeDocumentProvider = new DefaultCodeDocumentProvider(bufferProvider.Object, vsCodeDocumentProvider.Object);
var document = new Mock<TextDocument>();
// 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<ITextBuffer>().Object;
RazorCodeDocument codeDocument;
var bufferProvider = new Mock<RazorTextBufferProvider>();
bufferProvider.Setup(provider => provider.TryGetFromDocument(It.IsAny<TextDocument>(), out textBuffer))
.Returns(true);
var vsCodeDocumentProvider = new Mock<VisualStudioCodeDocumentProvider>();
vsCodeDocumentProvider.Setup(provider => provider.TryGetFromBuffer(It.Is<ITextBuffer>(val => val == textBuffer), out codeDocument))
.Returns(false);
var codeDocumentProvider = new DefaultCodeDocumentProvider(bufferProvider.Object, vsCodeDocumentProvider.Object);
var document = new Mock<TextDocument>();
// 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<ITextBuffer>().Object;
var expectedCodeDocument = new Mock<RazorCodeDocument>().Object;
var bufferProvider = new Mock<RazorTextBufferProvider>();
bufferProvider.Setup(provider => provider.TryGetFromDocument(It.IsAny<TextDocument>(), out textBuffer))
.Returns(true);
var vsCodeDocumentProvider = new Mock<VisualStudioCodeDocumentProvider>();
vsCodeDocumentProvider.Setup(provider => provider.TryGetFromBuffer(It.Is<ITextBuffer>(val => val == textBuffer), out expectedCodeDocument))
.Returns(true);
var codeDocumentProvider = new DefaultCodeDocumentProvider(bufferProvider.Object, vsCodeDocumentProvider.Object);
var document = new Mock<TextDocument>();
// Act
var result = codeDocumentProvider.TryGetFromDocument(document.Object, out var codeDocument);
// Assert
Assert.True(result);
Assert.Same(expectedCodeDocument, codeDocument);
}
}
}

View File

@ -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<IBufferGraph>();
bufferGraph.Setup(graph => graph.GetTextBuffers(It.IsAny<Predicate<ITextBuffer>>()))
.Returns(new Collection<ITextBuffer>());
var bufferGraphService = new Mock<IBufferGraphFactoryService>();
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<ITextBuffer>();
textBuffer.Setup(buffer => buffer.Properties)
.Returns(new PropertyCollection());
var textImage = new Mock<ITextImage>();
var textVersion = new Mock<ITextVersion>();
var textBufferSnapshot = new Mock<ITextSnapshot2>();
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<IContentType>();
contentType.Setup(type => type.IsOfType(It.IsAny<string>()))
.Returns<string>(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<IBufferGraph>();
bufferGraph.Setup(graph => graph.GetTextBuffers(It.IsAny<Predicate<ITextBuffer>>()))
.Returns<Predicate<ITextBuffer>>(predicate => predicate(buffer) ? new Collection<ITextBuffer>() { buffer } : new Collection<ITextBuffer>());
var bufferGraphService = new Mock<IBufferGraphFactoryService>();
bufferGraphService.Setup(service => service.CreateBufferGraph(buffer))
.Returns(bufferGraph.Object);
return bufferGraphService.Object;
}
}
}

View File

@ -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<ITextBuffer>();
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<ITextBuffer>();
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<ITextBuffer>();
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<ITextBuffer>();
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);
}
}
}

View File

@ -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<DirectiveDescriptor> 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<string, string>()
{
[RazorDirectiveCompletionProvider.DescriptionKey] = expectedDescription,
}).ToImmutableDictionary());
var codeDocumentProvider = new Mock<RazorCodeDocumentProvider>();
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<RazorCodeDocumentProvider>();
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<RazorCodeDocumentProvider>(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<RazorCodeDocumentProvider>();
codeDocumentProvider.Setup(provider => provider.TryGetFromDocument(It.IsAny<TextDocument>(), 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<DirectiveDescriptor>());
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<DirectiveDescriptor>());
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<DirectiveDescriptor>());
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<DirectiveDescriptor>());
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<RazorCodeDocumentProvider>();
var codeDocument = TestRazorCodeDocument.CreateEmpty();
codeDocumentProvider.Setup(provider => provider.TryGetFromDocument(It.IsAny<TextDocument>(), 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<DirectiveDescriptor>());
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<DirectiveDescriptor> directives)
{
var codeDocumentProvider = new Mock<RazorCodeDocumentProvider>();
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<TextDocument>(), 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<OptionSet>().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<CompletionItem> 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<ITextSnapshot>(MockBehavior.Strict);
snapshot.Setup(s => s.Length)
.Returns(context.CompletionListSpan.End);
snapshotPoint = new SnapshotPoint(snapshot.Object, context.CompletionListSpan.Start);
return true;
}
}
}
}