Refactor completion logic into standalone service.

- Migrated the completion item source provider and the legacy directive completion provider to use the new service.
- Cleaned up duplicate tests that were both verifying common completion functionality.
- Ensured that the legacy `RazorDirectiveCompletionProvider` did not result in additional Razor assembly loads when in C# scenarios.

#2530
This commit is contained in:
N. Taylor Mullen 2018-08-10 11:41:11 -07:00
parent aec88e3eba
commit 572b55690d
11 changed files with 501 additions and 518 deletions

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 System;
using System.Collections.Generic;
using System.ComponentModel.Composition;
using System.Linq;
using Microsoft.AspNetCore.Razor.Language;
using Microsoft.AspNetCore.Razor.Language.Legacy;
namespace Microsoft.VisualStudio.Editor.Razor
{
[System.Composition.Shared]
[Export(typeof(RazorCompletionFactsService))]
internal class DefaultRazorCompletionFactsService : RazorCompletionFactsService
{
private static readonly IEnumerable<DirectiveDescriptor> DefaultDirectives = new[]
{
CSharpCodeParser.AddTagHelperDirectiveDescriptor,
CSharpCodeParser.RemoveTagHelperDirectiveDescriptor,
CSharpCodeParser.TagHelperPrefixDirectiveDescriptor,
};
public override IReadOnlyList<RazorCompletionItem> GetCompletionItems(RazorSyntaxTree syntaxTree, SourceSpan location)
{
var completionItems = new List<RazorCompletionItem>();
if (AtDirectiveCompletionPoint(syntaxTree, location))
{
var directiveCompletions = GetDirectiveCompletionItems(syntaxTree);
completionItems.AddRange(directiveCompletions);
}
return completionItems;
}
// Internal for testing
internal static List<RazorCompletionItem> GetDirectiveCompletionItems(RazorSyntaxTree syntaxTree)
{
var directives = syntaxTree.Options.Directives.Concat(DefaultDirectives);
var completionItems = new List<RazorCompletionItem>();
foreach (var directive in directives)
{
var completionDisplayText = directive.DisplayName ?? directive.Directive;
var completionItem = new RazorCompletionItem(
completionDisplayText,
directive.Directive,
directive.Description,
RazorCompletionItemKind.Directive);
completionItems.Add(completionItem);
}
return completionItems;
}
// Internal for testing
internal static bool AtDirectiveCompletionPoint(RazorSyntaxTree syntaxTree, SourceSpan location)
{
if (syntaxTree == null)
{
return false;
}
var change = new SourceChange(location, string.Empty);
var owner = syntaxTree.Root.LocateOwner(change);
if (owner == null)
{
return false;
}
if (owner.ChunkGenerator is ExpressionChunkGenerator &&
owner.Tokens.All(IsDirectiveCompletableToken) &&
// 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;
}
// Internal for testing
internal static bool IsDirectiveCompletableToken(IToken token)
{
if (!(token is CSharpToken csharpToken))
{
return false;
}
return csharpToken.Type == CSharpTokenType.Identifier ||
// Marker symbol
csharpToken.Type == CSharpTokenType.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 System.Collections.Generic;
using Microsoft.AspNetCore.Razor.Language;
namespace Microsoft.VisualStudio.Editor.Razor
{
internal abstract class RazorCompletionFactsService
{
public abstract IReadOnlyList<RazorCompletionItem> GetCompletionItems(RazorSyntaxTree syntaxTree, SourceSpan location);
}
}

View File

@ -0,0 +1,45 @@
// 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;
namespace Microsoft.VisualStudio.Editor.Razor
{
internal sealed class RazorCompletionItem
{
public RazorCompletionItem(
string displayText,
string insertText,
string description,
RazorCompletionItemKind kind)
{
if (displayText == null)
{
throw new ArgumentNullException(nameof(displayText));
}
if (insertText == null)
{
throw new ArgumentNullException(nameof(insertText));
}
if (description == null)
{
throw new ArgumentNullException(nameof(description));
}
DisplayText = displayText;
InsertText = insertText;
Description = description;
Kind = kind;
}
public string DisplayText { get; }
public string InsertText { get; }
public string Description { get; }
public RazorCompletionItemKind Kind { get; }
}
}

View File

@ -0,0 +1,10 @@
// 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.
namespace Microsoft.VisualStudio.Editor.Razor
{
internal enum RazorCompletionItemKind
{
Directive
}
}

View File

@ -36,12 +36,14 @@ namespace Microsoft.VisualStudio.Editor.Razor
CSharpCodeParser.TagHelperPrefixDirectiveDescriptor,
};
private readonly Lazy<RazorCodeDocumentProvider> _codeDocumentProvider;
private readonly Lazy<RazorCompletionFactsService> _completionFactsService;
private readonly IAsyncCompletionBroker _asyncCompletionBroker;
private readonly RazorTextBufferProvider _textBufferProvider;
[ImportingConstructor]
public RazorDirectiveCompletionProvider(
[Import(typeof(RazorCodeDocumentProvider))] Lazy<RazorCodeDocumentProvider> codeDocumentProvider,
[Import(typeof(RazorCompletionFactsService))] Lazy<RazorCompletionFactsService> completionFactsService,
IAsyncCompletionBroker asyncCompletionBroker,
RazorTextBufferProvider textBufferProvider)
{
@ -50,6 +52,11 @@ namespace Microsoft.VisualStudio.Editor.Razor
throw new ArgumentNullException(nameof(codeDocumentProvider));
}
if (completionFactsService == null)
{
throw new ArgumentNullException(nameof(completionFactsService));
}
if (asyncCompletionBroker == null)
{
throw new ArgumentNullException(nameof(asyncCompletionBroker));
@ -61,6 +68,7 @@ namespace Microsoft.VisualStudio.Editor.Razor
}
_codeDocumentProvider = codeDocumentProvider;
_completionFactsService = completionFactsService;
_asyncCompletionBroker = asyncCompletionBroker;
_textBufferProvider = textBufferProvider;
}
@ -133,70 +141,41 @@ namespace Microsoft.VisualStudio.Editor.Razor
return Task.CompletedTask;
}
if (!AtDirectiveCompletionPoint(syntaxTree, context))
if (!TryGetRazorSnapshotPoint(context, out var razorSnapshotPoint))
{
// Can't have a valid directive at the current location.
// Could not find associated Razor location.
return Task.CompletedTask;
}
var completionItems = GetCompletionItems(syntaxTree);
context.AddItems(completionItems);
var location = new SourceSpan(razorSnapshotPoint.Position, 0);
var razorCompletionItems = _completionFactsService.Value.GetCompletionItems(syntaxTree, location);
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)
foreach (var razorCompletionItem in razorCompletionItems)
{
var propertyDictionary = new Dictionary<string, string>(StringComparer.Ordinal);
if (!string.IsNullOrEmpty(directive.Description))
if (razorCompletionItem.Kind != RazorCompletionItemKind.Directive)
{
propertyDictionary[DescriptionKey] = directive.Description;
// Don't support any other types of completion kinds other than directives.
continue;
}
var propertyDictionary = new Dictionary<string, string>(StringComparer.Ordinal);
if (!string.IsNullOrEmpty(razorCompletionItem.Description))
{
propertyDictionary[DescriptionKey] = razorCompletionItem.Description;
}
var completionItem = CompletionItem.Create(
directive.Directive,
razorCompletionItem.InsertText,
// This groups all Razor directives together
sortText: "_RazorDirective_",
rules: CompletionItemRules.Create(formatOnCommit: false),
tags: ImmutableArray.Create(WellKnownTags.Intrinsic),
properties: propertyDictionary.ToImmutableDictionary());
completionItems.Add(completionItem);
context.AddItem(completionItem);
}
return completionItems;
}
// Internal for testing
internal bool AtDirectiveCompletionPoint(RazorSyntaxTree syntaxTree, CompletionContext context)
{
if (TryGetRazorSnapshotPoint(context, out var razorSnapshotPoint))
{
var change = new SourceChange(razorSnapshotPoint.Position, 0, string.Empty);
var owner = syntaxTree.Root.LocateOwner(change);
if (owner == null)
{
return false;
}
if (owner.ChunkGenerator is ExpressionChunkGenerator &&
owner.Tokens.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;
return Task.CompletedTask;
}
protected virtual bool TryGetRazorSnapshotPoint(CompletionContext context, out SnapshotPoint snapshotPoint)

View File

@ -4,11 +4,9 @@
using System;
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.Legacy;
using Microsoft.CodeAnalysis.Razor;
using Microsoft.VisualStudio.Core.Imaging;
using Microsoft.VisualStudio.Imaging;
@ -29,33 +27,35 @@ namespace Microsoft.VisualStudio.Editor.Razor
internal static readonly ImmutableArray<CompletionFilter> DirectiveCompletionFilters = new[] {
new CompletionFilter("Razor Directive", "r", DirectiveImageGlyph)
}.ToImmutableArray();
private static readonly IEnumerable<DirectiveDescriptor> DefaultDirectives = new[]
{
CSharpCodeParser.AddTagHelperDirectiveDescriptor,
CSharpCodeParser.RemoveTagHelperDirectiveDescriptor,
CSharpCodeParser.TagHelperPrefixDirectiveDescriptor,
};
// Internal for testing
internal readonly VisualStudioRazorParser _parser;
private readonly RazorCompletionFactsService _completionFactsService;
private readonly ForegroundDispatcher _foregroundDispatcher;
public RazorDirectiveCompletionSource(
ForegroundDispatcher foregroundDispatcher,
VisualStudioRazorParser parser,
ForegroundDispatcher foregroundDispatcher)
RazorCompletionFactsService completionFactsService)
{
if (parser == null)
{
throw new ArgumentNullException(nameof(parser));
}
if (foregroundDispatcher == null)
{
throw new ArgumentNullException(nameof(foregroundDispatcher));
}
_parser = parser;
if (parser == null)
{
throw new ArgumentNullException(nameof(parser));
}
if (completionFactsService == null)
{
throw new ArgumentNullException(nameof(completionFactsService));
}
_foregroundDispatcher = foregroundDispatcher;
_parser = parser;
_completionFactsService = completionFactsService;
}
public Task<CompletionContext> GetCompletionContextAsync(
@ -67,12 +67,31 @@ namespace Microsoft.VisualStudio.Editor.Razor
_foregroundDispatcher.AssertBackgroundThread();
var syntaxTree = _parser.CodeDocument?.GetSyntaxTree();
if (!AtDirectiveCompletionPoint(syntaxTree, triggerLocation))
{
return Task.FromResult(CompletionContext.Empty);
}
var location = new SourceSpan(applicableSpan.Start.Position, applicableSpan.Length);
var razorCompletionItems = _completionFactsService.GetCompletionItems(syntaxTree, location);
var completionItems = GetCompletionItems(syntaxTree);
var completionItems = new List<CompletionItem>();
foreach (var razorCompletionItem in razorCompletionItems)
{
if (razorCompletionItem.Kind != RazorCompletionItemKind.Directive)
{
// Don't support any other types of completion kinds other than directives.
continue;
}
var completionItem = new CompletionItem(
displayText: razorCompletionItem.DisplayText,
filterText: razorCompletionItem.DisplayText,
insertText: razorCompletionItem.InsertText,
source: this,
icon: DirectiveImageGlyph,
filters: DirectiveCompletionFilters,
suffix: string.Empty,
sortText: razorCompletionItem.DisplayText,
attributeIcons: ImmutableArray<ImageElement>.Empty);
completionItem.Properties.AddProperty(DescriptionKey, razorCompletionItem.Description);
completionItems.Add(completionItem);
}
var context = new CompletionContext(completionItems.ToImmutableArray());
return Task.FromResult(context);
}
@ -96,72 +115,5 @@ namespace Microsoft.VisualStudio.Editor.Razor
applicableToSpan = default(SnapshotSpan);
return false;
}
// Internal for testing
internal List<CompletionItem> GetCompletionItems(RazorSyntaxTree syntaxTree)
{
var directives = syntaxTree.Options.Directives.Concat(DefaultDirectives);
var completionItems = new List<CompletionItem>();
foreach (var directive in directives)
{
var completionDisplayText = directive.DisplayName ?? directive.Directive;
var completionItem = new CompletionItem(
displayText: completionDisplayText,
filterText: completionDisplayText,
insertText: directive.Directive,
source: this,
icon: DirectiveImageGlyph,
filters: DirectiveCompletionFilters,
suffix: string.Empty,
sortText: completionDisplayText,
attributeIcons: ImmutableArray<ImageElement>.Empty);
completionItem.Properties.AddProperty(DescriptionKey, directive.Description);
completionItems.Add(completionItem);
}
return completionItems;
}
// Internal for testing
internal static bool AtDirectiveCompletionPoint(RazorSyntaxTree syntaxTree, SnapshotPoint location)
{
if (syntaxTree == null)
{
return false;
}
var change = new SourceChange(location.Position, 0, string.Empty);
var owner = syntaxTree.Root.LocateOwner(change);
if (owner == null)
{
return false;
}
if (owner.ChunkGenerator is ExpressionChunkGenerator &&
owner.Tokens.All(IsDirectiveCompletableToken) &&
// 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;
}
// Internal for testing
internal static bool IsDirectiveCompletableToken(IToken token)
{
if (!(token is CSharpToken csharpToken))
{
return false;
}
return csharpToken.Type == CSharpTokenType.Identifier ||
// Marker symbol
csharpToken.Type == CSharpTokenType.Unknown;
}
}
}

View File

@ -19,16 +19,25 @@ namespace Microsoft.VisualStudio.Editor.Razor
internal class RazorDirectiveCompletionSourceProvider : IAsyncCompletionSourceProvider
{
private readonly ForegroundDispatcher _foregroundDispatcher;
private readonly RazorCompletionFactsService _completionFactsService;
[ImportingConstructor]
public RazorDirectiveCompletionSourceProvider(ForegroundDispatcher foregroundDispatcher)
public RazorDirectiveCompletionSourceProvider(
ForegroundDispatcher foregroundDispatcher,
RazorCompletionFactsService completionFactsService)
{
if (foregroundDispatcher == null)
{
throw new ArgumentNullException(nameof(foregroundDispatcher));
}
if (completionFactsService == null)
{
throw new ArgumentNullException(nameof(completionFactsService));
}
_foregroundDispatcher = foregroundDispatcher;
_completionFactsService = completionFactsService;
}
public IAsyncCompletionSource GetOrCreate(ITextView textView)
@ -57,7 +66,7 @@ namespace Microsoft.VisualStudio.Editor.Razor
return null;
}
var completionSource = new RazorDirectiveCompletionSource(parser, _foregroundDispatcher);
var completionSource = new RazorDirectiveCompletionSource(_foregroundDispatcher, parser, _completionFactsService);
return completionSource;
}
}

View File

@ -0,0 +1,239 @@
// 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 Microsoft.AspNetCore.Razor.Language;
using Microsoft.AspNetCore.Razor.Language.Legacy;
using Moq;
using Xunit;
namespace Microsoft.VisualStudio.Editor.Razor
{
public class DefaultRazorCompletionFactsServiceTest
{
private static readonly IReadOnlyList<DirectiveDescriptor> DefaultDirectives = new[]
{
CSharpCodeParser.AddTagHelperDirectiveDescriptor,
CSharpCodeParser.RemoveTagHelperDirectiveDescriptor,
CSharpCodeParser.TagHelperPrefixDirectiveDescriptor,
};
[Fact]
public void GetDirectiveCompletionItems_ReturnsDefaultDirectivesAsCompletionItems()
{
// Arrange
var syntaxTree = CreateSyntaxTree("@addTag");
// Act
var completionItems = DefaultRazorCompletionFactsService.GetDirectiveCompletionItems(syntaxTree);
// Assert
Assert.Collection(
completionItems,
item => AssertRazorCompletionItem(DefaultDirectives[0], item),
item => AssertRazorCompletionItem(DefaultDirectives[1], item),
item => AssertRazorCompletionItem(DefaultDirectives[2], item));
}
[Fact]
public void GetDirectiveCompletionItems_ReturnsCustomDirectivesAsCompletionItems()
{
// Arrange
var customDirective = DirectiveDescriptor.CreateSingleLineDirective("custom", builder =>
{
builder.Description = "My Custom Directive.";
});
var syntaxTree = CreateSyntaxTree("@addTag", customDirective);
// Act
var completionItems = DefaultRazorCompletionFactsService.GetDirectiveCompletionItems(syntaxTree);
// Assert
Assert.Collection(
completionItems,
item => AssertRazorCompletionItem(customDirective, item),
item => AssertRazorCompletionItem(DefaultDirectives[0], item),
item => AssertRazorCompletionItem(DefaultDirectives[1], item),
item => AssertRazorCompletionItem(DefaultDirectives[2], item));
}
[Fact]
public void GetDirectiveCompletionItems_UsesDisplayNamesWhenNotNull()
{
// Arrange
var customDirective = DirectiveDescriptor.CreateSingleLineDirective("custom", builder =>
{
builder.DisplayName = "different";
builder.Description = "My Custom Directive.";
});
var syntaxTree = CreateSyntaxTree("@addTag", customDirective);
// Act
var completionItems = DefaultRazorCompletionFactsService.GetDirectiveCompletionItems(syntaxTree);
// Assert
Assert.Collection(
completionItems,
item => AssertRazorCompletionItem("different", customDirective, item),
item => AssertRazorCompletionItem(DefaultDirectives[0], item),
item => AssertRazorCompletionItem(DefaultDirectives[1], item),
item => AssertRazorCompletionItem(DefaultDirectives[2], item));
}
[Fact]
public void AtDirectiveCompletionPoint_ReturnsFalseIfSyntaxTreeNull()
{
// Act
var result = DefaultRazorCompletionFactsService.AtDirectiveCompletionPoint(syntaxTree: null, location: new SourceSpan(0, 0));
// Assert
Assert.False(result);
}
[Fact]
public void AtDirectiveCompletionPoint_ReturnsFalseIfNoOwner()
{
// Arrange
var syntaxTree = CreateSyntaxTree("@");
var location = new SourceSpan(2, 0);
// Act
var result = DefaultRazorCompletionFactsService.AtDirectiveCompletionPoint(syntaxTree, location);
// Assert
Assert.False(result);
}
[Fact]
public void AtDirectiveCompletionPoint_ReturnsFalseWhenOwnerIsNotExpression()
{
// Arrange
var syntaxTree = CreateSyntaxTree("@{");
var location = new SourceSpan(2, 0);
// Act
var result = DefaultRazorCompletionFactsService.AtDirectiveCompletionPoint(syntaxTree, location);
// Assert
Assert.False(result);
}
[Fact]
public void AtDirectiveCompletionPoint_ReturnsFalseWhenOwnerIsComplexExpression()
{
// Arrange
var syntaxTree = CreateSyntaxTree("@DateTime.Now");
var location = new SourceSpan(2, 0);
// Act
var result = DefaultRazorCompletionFactsService.AtDirectiveCompletionPoint(syntaxTree, location);
// Assert
Assert.False(result);
}
[Fact]
public void AtDirectiveCompletionPoint_ReturnsFalseWhenOwnerIsExplicitExpression()
{
// Arrange
var syntaxTree = CreateSyntaxTree("@(something)");
var location = new SourceSpan(4, 0);
// Act
var result = DefaultRazorCompletionFactsService.AtDirectiveCompletionPoint(syntaxTree, location);
// Assert
Assert.False(result);
}
[Fact]
public void AtDirectiveCompletionPoint_ReturnsTrueForSimpleImplicitExpressions()
{
// Arrange
var syntaxTree = CreateSyntaxTree("@mod");
var location = new SourceSpan(2, 0);
// Act
var result = DefaultRazorCompletionFactsService.AtDirectiveCompletionPoint(syntaxTree, location);
// Assert
Assert.True(result);
}
[Fact]
public void IsDirectiveCompletableToken_ReturnsTrueForCSharpIdentifiers()
{
// Arrange
var csharpToken = new CSharpToken("model", CSharpTokenType.Identifier);
// Act
var result = DefaultRazorCompletionFactsService.IsDirectiveCompletableToken(csharpToken);
// Assert
Assert.True(result);
}
[Fact]
public void IsDirectiveCompletableToken_ReturnsTrueForCSharpMarkerTokens()
{
// Arrange
var csharpToken = new CSharpToken(string.Empty, CSharpTokenType.Unknown);
// Act
var result = DefaultRazorCompletionFactsService.IsDirectiveCompletableToken(csharpToken);
// Assert
Assert.True(result);
}
[Fact]
public void IsDirectiveCompletableToken_ReturnsFalseForNonCSharpTokens()
{
// Arrange
var token = Mock.Of<IToken>();
// Act
var result = DefaultRazorCompletionFactsService.IsDirectiveCompletableToken(token);
// Assert
Assert.False(result);
}
[Fact]
public void IsDirectiveCompletableToken_ReturnsFalseForInvalidCSharpTokens()
{
// Arrange
var csharpToken = new CSharpToken("~", CSharpTokenType.Tilde);
// Act
var result = DefaultRazorCompletionFactsService.IsDirectiveCompletableToken(csharpToken);
// Assert
Assert.False(result);
}
private static void AssertRazorCompletionItem(string completionDisplayText, DirectiveDescriptor directive, RazorCompletionItem item)
{
Assert.Equal(item.DisplayText, completionDisplayText);
Assert.Equal(item.InsertText, directive.Directive);
Assert.Equal(directive.Description, item.Description);
}
private static void AssertRazorCompletionItem(DirectiveDescriptor directive, RazorCompletionItem item) =>
AssertRazorCompletionItem(directive.Directive, directive, item);
private static RazorSyntaxTree CreateSyntaxTree(string text, params DirectiveDescriptor[] directives)
{
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);
return syntaxTree;
}
}
}

View File

@ -8,12 +8,9 @@ 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.Tags;
using Microsoft.CodeAnalysis.Text;
using Microsoft.VisualStudio.Language.Intellisense.AsyncCompletion;
using Microsoft.VisualStudio.Text;
@ -26,41 +23,19 @@ namespace Microsoft.VisualStudio.Editor.Razor
{
public class RazorDirectiveCompletionProviderTest
{
private static readonly IReadOnlyList<DirectiveDescriptor> DefaultDirectives = new[]
{
CSharpCodeParser.AddTagHelperDirectiveDescriptor,
CSharpCodeParser.RemoveTagHelperDirectiveDescriptor,
CSharpCodeParser.TagHelperPrefixDirectiveDescriptor,
};
public RazorDirectiveCompletionProviderTest()
{
CompletionBroker = Mock.Of<IAsyncCompletionBroker>(broker => broker.IsCompletionSupported(It.IsAny<IContentType>()) == true);
var razorBuffer = Mock.Of<ITextBuffer>(buffer => buffer.ContentType == Mock.Of<IContentType>());
TextBufferProvider = Mock.Of<RazorTextBufferProvider>(provider => provider.TryGetFromDocument(It.IsAny<TextDocument>(), out razorBuffer) == true);
CompletionFactsService = new DefaultRazorCompletionFactsService();
}
private IAsyncCompletionBroker CompletionBroker { get; }
private RazorTextBufferProvider TextBufferProvider { get; }
[Fact]
public void AtDirectiveCompletionPoint_ReturnsFalseIfChangeHasNoOwner()
{
// Arrange
var codeDocumentProvider = CreateCodeDocumentProvider("@", Enumerable.Empty<DirectiveDescriptor>());
var completionProvider = new FailOnGetCompletionsProvider(codeDocumentProvider, CompletionBroker, TextBufferProvider);
var document = CreateDocument();
codeDocumentProvider.Value.TryGetFromDocument(document, out var codeDocument);
var syntaxTree = codeDocument.GetSyntaxTree();
var completionContext = CreateContext(2, completionProvider, document);
// Act
var result = completionProvider.AtDirectiveCompletionPoint(syntaxTree, completionContext);
// Assert
Assert.False(result);
}
private RazorCompletionFactsService CompletionFactsService { get; }
[Fact]
public async Task GetDescriptionAsync_AddsDirectiveDescriptionIfPropertyExists()
@ -76,6 +51,7 @@ namespace Microsoft.VisualStudio.Editor.Razor
var codeDocumentProvider = new Mock<RazorCodeDocumentProvider>();
var completionProvider = new RazorDirectiveCompletionProvider(
new Lazy<RazorCodeDocumentProvider>(() => codeDocumentProvider.Object),
new Lazy<RazorCompletionFactsService>(() => CompletionFactsService),
CompletionBroker,
TextBufferProvider);
@ -98,6 +74,7 @@ namespace Microsoft.VisualStudio.Editor.Razor
var codeDocumentProvider = new Mock<RazorCodeDocumentProvider>();
var completionProvider = new RazorDirectiveCompletionProvider(
new Lazy<RazorCodeDocumentProvider>(() => codeDocumentProvider.Object),
new Lazy<RazorCompletionFactsService>(() => CompletionFactsService),
CompletionBroker,
TextBufferProvider);
@ -126,7 +103,6 @@ namespace Microsoft.VisualStudio.Editor.Razor
await completionProvider.ProvideCompletionsAsync(context);
}
[Fact]
public async Task ProvideCompletionAsync_DoesNotProvideCompletionsForDocumentWithoutPath()
{
@ -189,145 +165,6 @@ namespace Microsoft.VisualStudio.Editor.Razor
await completionProvider.ProvideCompletionsAsync(context);
}
[Fact]
public async Task ProvideCompletionAsync_DoesNotProvideCompletionsWhenNotAtCompletionPoint()
{
// Arrange
var codeDocumentProvider = CreateCodeDocumentProvider("@", Enumerable.Empty<DirectiveDescriptor>());
var completionProvider = new FailOnGetCompletionsProvider(codeDocumentProvider, CompletionBroker, TextBufferProvider);
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, CompletionBroker, TextBufferProvider);
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, CompletionBroker, TextBufferProvider);
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(
new Lazy<RazorCodeDocumentProvider>(() => codeDocumentProvider.Object),
CompletionBroker,
TextBufferProvider);
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, CompletionBroker, TextBufferProvider);
var document = CreateDocument();
codeDocumentProvider.Value.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, CompletionBroker, TextBufferProvider);
var document = CreateDocument();
codeDocumentProvider.Value.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, CompletionBroker, TextBufferProvider);
var document = CreateDocument();
codeDocumentProvider.Value.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(WellKnownTags.Intrinsic, tag);
}
private static Lazy<RazorCodeDocumentProvider> CreateCodeDocumentProvider(string text, IEnumerable<DirectiveDescriptor> directives)
{
var codeDocumentProvider = new Mock<RazorCodeDocumentProvider>();
@ -388,17 +225,11 @@ namespace Microsoft.VisualStudio.Editor.Razor
IAsyncCompletionBroker asyncCompletionBroker,
RazorTextBufferProvider textBufferProvider,
bool canGetSnapshotPoint = true)
: base(codeDocumentProvider, asyncCompletionBroker, textBufferProvider)
: base(codeDocumentProvider, new Lazy<RazorCompletionFactsService>(() => new DefaultRazorCompletionFactsService()), asyncCompletionBroker, textBufferProvider)
{
_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)

View File

@ -19,6 +19,8 @@ namespace Microsoft.VisualStudio.Editor.Razor
private IContentType NonRazorContentType { get; } = Mock.Of<IContentType>(c => c.IsOfType(It.IsAny<string>()) == false);
private RazorCompletionFactsService CompletionFactsService { get; } = Mock.Of<RazorCompletionFactsService>();
[Fact]
public void CreateCompletionSource_ReturnsNullIfParserHasNotBeenAssocitedWithRazorBuffer()
{
@ -27,7 +29,7 @@ namespace Microsoft.VisualStudio.Editor.Razor
var properties = new PropertyCollection();
properties.AddProperty(typeof(VisualStudioRazorParser), expectedParser);
var razorBuffer = Mock.Of<ITextBuffer>(buffer => buffer.ContentType == RazorContentType && buffer.Properties == properties);
var completionSourceProvider = new RazorDirectiveCompletionSourceProvider(Dispatcher);
var completionSourceProvider = new RazorDirectiveCompletionSourceProvider(Dispatcher, CompletionFactsService);
// Act
var completionSource = completionSourceProvider.CreateCompletionSource(razorBuffer);
@ -42,7 +44,7 @@ namespace Microsoft.VisualStudio.Editor.Razor
{
// Arrange
var razorBuffer = Mock.Of<ITextBuffer>(buffer => buffer.ContentType == RazorContentType && buffer.Properties == new PropertyCollection());
var completionSourceProvider = new RazorDirectiveCompletionSourceProvider(Dispatcher);
var completionSourceProvider = new RazorDirectiveCompletionSourceProvider(Dispatcher, CompletionFactsService);
// Act
var completionSource = completionSourceProvider.CreateCompletionSource(razorBuffer);
@ -56,7 +58,7 @@ namespace Microsoft.VisualStudio.Editor.Razor
{
// Arrange
var textView = CreateTextView(NonRazorContentType, new PropertyCollection());
var completionSourceProvider = new RazorDirectiveCompletionSourceProvider(Dispatcher);
var completionSourceProvider = new RazorDirectiveCompletionSourceProvider(Dispatcher, CompletionFactsService);
// Act
var completionSource = completionSourceProvider.GetOrCreate(textView);
@ -73,7 +75,7 @@ namespace Microsoft.VisualStudio.Editor.Razor
var properties = new PropertyCollection();
properties.AddProperty(typeof(VisualStudioRazorParser), expectedParser);
var textView = CreateTextView(RazorContentType, properties);
var completionSourceProvider = new RazorDirectiveCompletionSourceProvider(Dispatcher);
var completionSourceProvider = new RazorDirectiveCompletionSourceProvider(Dispatcher, CompletionFactsService);
// Act
var completionSource1 = completionSourceProvider.GetOrCreate(textView);

View File

@ -25,16 +25,18 @@ namespace Microsoft.VisualStudio.Editor.Razor
CSharpCodeParser.TagHelperPrefixDirectiveDescriptor,
};
private RazorCompletionFactsService CompletionFactsService { get; } = new DefaultRazorCompletionFactsService();
[ForegroundFact]
public async Task GetCompletionContextAsync_DoesNotProvideCompletionsPriorToParseResults()
{
// Arrange
var text = "@validCompletion";
var parser = Mock.Of<VisualStudioRazorParser>(); // CodeDocument will be null faking a parser without a parse.
var completionSource = new RazorDirectiveCompletionSource(parser, Dispatcher);
var completionSource = new RazorDirectiveCompletionSource(Dispatcher, parser, CompletionFactsService);
var documentSnapshot = new StringTextSnapshot(text);
var triggerLocation = new SnapshotPoint(documentSnapshot, 4);
var applicableSpan = new SnapshotSpan(documentSnapshot, new Span(1, text.Length - 1 /* @ */));
var applicableSpan = new SnapshotSpan(documentSnapshot, new Span(1, text.Length - 1 /* validCompletion */));
// Act
var completionContext = await Task.Run(
@ -50,7 +52,7 @@ namespace Microsoft.VisualStudio.Editor.Razor
// Arrange
var text = "@(NotValidCompletionLocation)";
var parser = CreateParser(text);
var completionSource = new RazorDirectiveCompletionSource(parser, Dispatcher);
var completionSource = new RazorDirectiveCompletionSource(Dispatcher, parser, CompletionFactsService);
var documentSnapshot = new StringTextSnapshot(text);
var triggerLocation = new SnapshotPoint(documentSnapshot, 4);
var applicableSpan = new SnapshotSpan(documentSnapshot, new Span(2, text.Length - 3 /* @() */));
@ -70,10 +72,10 @@ namespace Microsoft.VisualStudio.Editor.Razor
// Arrange
var text = "@addTag";
var parser = CreateParser(text, SectionDirective.Directive);
var completionSource = new RazorDirectiveCompletionSource(parser, Dispatcher);
var completionSource = new RazorDirectiveCompletionSource(Dispatcher, parser, CompletionFactsService);
var documentSnapshot = new StringTextSnapshot(text);
var triggerLocation = new SnapshotPoint(documentSnapshot, 4);
var applicableSpan = new SnapshotSpan(documentSnapshot, new Span(1, text.Length - 1 /* @ */));
var applicableSpan = new SnapshotSpan(documentSnapshot, new Span(1, 6 /* addTag */));
// Act
var completionContext = await Task.Run(
@ -88,71 +90,6 @@ namespace Microsoft.VisualStudio.Editor.Razor
item => AssertRazorCompletionItem(DefaultDirectives[2], item, completionSource));
}
[Fact]
public void GetCompletionItems_ReturnsDefaultDirectivesAsCompletionItems()
{
// Arrange
var syntaxTree = CreateSyntaxTree("@addTag");
var completionSource = new RazorDirectiveCompletionSource(Mock.Of<VisualStudioRazorParser>(), Dispatcher);
// Act
var completionItems = completionSource.GetCompletionItems(syntaxTree);
// Assert
Assert.Collection(
completionItems,
item => AssertRazorCompletionItem(DefaultDirectives[0], item, completionSource),
item => AssertRazorCompletionItem(DefaultDirectives[1], item, completionSource),
item => AssertRazorCompletionItem(DefaultDirectives[2], item, completionSource));
}
[Fact]
public void GetCompletionItems_ReturnsCustomDirectivesAsCompletionItems()
{
// Arrange
var customDirective = DirectiveDescriptor.CreateSingleLineDirective("custom", builder =>
{
builder.Description = "My Custom Directive.";
});
var syntaxTree = CreateSyntaxTree("@addTag", customDirective);
var completionSource = new RazorDirectiveCompletionSource(Mock.Of<VisualStudioRazorParser>(), Dispatcher);
// Act
var completionItems = completionSource.GetCompletionItems(syntaxTree);
// Assert
Assert.Collection(
completionItems,
item => AssertRazorCompletionItem(customDirective, item, completionSource),
item => AssertRazorCompletionItem(DefaultDirectives[0], item, completionSource),
item => AssertRazorCompletionItem(DefaultDirectives[1], item, completionSource),
item => AssertRazorCompletionItem(DefaultDirectives[2], item, completionSource));
}
[Fact]
public void GetCompletionItems_UsesDisplayNamesWhenNotNull()
{
// Arrange
var customDirective = DirectiveDescriptor.CreateSingleLineDirective("custom", builder =>
{
builder.DisplayName = "different";
builder.Description = "My Custom Directive.";
});
var syntaxTree = CreateSyntaxTree("@addTag", customDirective);
var completionSource = new RazorDirectiveCompletionSource(Mock.Of<VisualStudioRazorParser>(), Dispatcher);
// Act
var completionItems = completionSource.GetCompletionItems(syntaxTree);
// Assert
Assert.Collection(
completionItems,
item => AssertRazorCompletionItem("different", customDirective, item, completionSource),
item => AssertRazorCompletionItem(DefaultDirectives[0], item, completionSource),
item => AssertRazorCompletionItem(DefaultDirectives[1], item, completionSource),
item => AssertRazorCompletionItem(DefaultDirectives[2], item, completionSource));
}
[Fact]
public async Task GetDescriptionAsync_AddsDirectiveDescriptionIfPropertyExists()
{
@ -160,7 +97,7 @@ namespace Microsoft.VisualStudio.Editor.Razor
var completionItem = new CompletionItem("TestDirective", Mock.Of<IAsyncCompletionSource>());
var expectedDescription = "The expected description";
completionItem.Properties.AddProperty(RazorDirectiveCompletionSource.DescriptionKey, expectedDescription);
var completionSource = new RazorDirectiveCompletionSource(Mock.Of<VisualStudioRazorParser>(), Dispatcher);
var completionSource = new RazorDirectiveCompletionSource(Dispatcher, Mock.Of<VisualStudioRazorParser>(), CompletionFactsService);
// Act
var descriptionObject = await completionSource.GetDescriptionAsync(completionItem, CancellationToken.None);
@ -175,7 +112,7 @@ namespace Microsoft.VisualStudio.Editor.Razor
{
// Arrange
var completionItem = new CompletionItem("TestDirective", Mock.Of<IAsyncCompletionSource>());
var completionSource = new RazorDirectiveCompletionSource(Mock.Of<VisualStudioRazorParser>(), Dispatcher);
var completionSource = new RazorDirectiveCompletionSource(Dispatcher, Mock.Of<VisualStudioRazorParser>(), CompletionFactsService);
// Act
var descriptionObject = await completionSource.GetDescriptionAsync(completionItem, CancellationToken.None);
@ -185,138 +122,6 @@ namespace Microsoft.VisualStudio.Editor.Razor
Assert.Equal(string.Empty, description);
}
[Fact]
public void AtDirectiveCompletionPoint_ReturnsFalseIfSyntaxTreeNull()
{
// Act
var result = RazorDirectiveCompletionSource.AtDirectiveCompletionPoint(syntaxTree: null, location: new SnapshotPoint());
// Assert
Assert.False(result);
}
[Fact]
public void AtDirectiveCompletionPoint_ReturnsFalseIfNoOwner()
{
// Arrange
var syntaxTree = CreateSyntaxTree("@");
var snapshotPoint = new SnapshotPoint(new StringTextSnapshot("@ text"), 2);
// Act
var result = RazorDirectiveCompletionSource.AtDirectiveCompletionPoint(syntaxTree, snapshotPoint);
// Assert
Assert.False(result);
}
[Fact]
public void AtDirectiveCompletionPoint_ReturnsFalseWhenOwnerIsNotExpression()
{
// Arrange
var syntaxTree = CreateSyntaxTree("@{");
var snapshotPoint = new SnapshotPoint(new StringTextSnapshot("@{"), 2);
// Act
var result = RazorDirectiveCompletionSource.AtDirectiveCompletionPoint(syntaxTree, snapshotPoint);
// Assert
Assert.False(result);
}
[Fact]
public void AtDirectiveCompletionPoint_ReturnsFalseWhenOwnerIsComplexExpression()
{
// Arrange
var syntaxTree = CreateSyntaxTree("@DateTime.Now");
var snapshotPoint = new SnapshotPoint(new StringTextSnapshot("@DateTime.Now"), 2);
// Act
var result = RazorDirectiveCompletionSource.AtDirectiveCompletionPoint(syntaxTree, snapshotPoint);
// Assert
Assert.False(result);
}
[Fact]
public void AtDirectiveCompletionPoint_ReturnsFalseWhenOwnerIsExplicitExpression()
{
// Arrange
var syntaxTree = CreateSyntaxTree("@(something)");
var snapshotPoint = new SnapshotPoint(new StringTextSnapshot("@(something)"), 4);
// Act
var result = RazorDirectiveCompletionSource.AtDirectiveCompletionPoint(syntaxTree, snapshotPoint);
// Assert
Assert.False(result);
}
[Fact]
public void AtDirectiveCompletionPoint_ReturnsTrueForSimpleImplicitExpressions()
{
// Arrange
var syntaxTree = CreateSyntaxTree("@mod");
var snapshotPoint = new SnapshotPoint(new StringTextSnapshot("@mod"), 2);
// Act
var result = RazorDirectiveCompletionSource.AtDirectiveCompletionPoint(syntaxTree, snapshotPoint);
// Assert
Assert.True(result);
}
[Fact]
public void IsDirectiveCompletableToken_ReturnsTrueForCSharpIdentifiers()
{
// Arrange
var csharpToken = new CSharpToken("model", CSharpTokenType.Identifier);
// Act
var result = RazorDirectiveCompletionSource.IsDirectiveCompletableToken(csharpToken);
// Assert
Assert.True(result);
}
[Fact]
public void IsDirectiveCompletableToken_ReturnsTrueForCSharpMarkerTokens()
{
// Arrange
var csharpToken = new CSharpToken(string.Empty, CSharpTokenType.Unknown);
// Act
var result = RazorDirectiveCompletionSource.IsDirectiveCompletableToken(csharpToken);
// Assert
Assert.True(result);
}
[Fact]
public void IsDirectiveCompletableToken_ReturnsFalseForNonCSharpTokens()
{
// Arrange
var token = Mock.Of<IToken>();
// Act
var result = RazorDirectiveCompletionSource.IsDirectiveCompletableToken(token);
// Assert
Assert.False(result);
}
[Fact]
public void IsDirectiveCompletableToken_ReturnsFalseForInvalidCSharpTokens()
{
// Arrange
var csharpToken = new CSharpToken("~", CSharpTokenType.Tilde);
// Act
var result = RazorDirectiveCompletionSource.IsDirectiveCompletableToken(csharpToken);
// Assert
Assert.False(result);
}
private static void AssertRazorCompletionItem(string completionDisplayText, DirectiveDescriptor directive, CompletionItem item, IAsyncCompletionSource source)
{
Assert.Equal(item.DisplayText, completionDisplayText);