Change Razor directive completions to use new completions API.

- Kept the same behavior as we previously had with Razor directive completions.
- Attempted adding additional functionalities such as lighting up Razor directive completion when completion was invoked on top of Razor directives (non-C#) but ran into issues involving the core HTML editor not consuming the new completion APIs yet. That's something we'll have to re-visit once they move to the new completion APIs.
- Added tests to validate all aspects of new completion APIs.
- Made completion provider turn on and off based off of feature flag.

#1743
#1813
This commit is contained in:
N. Taylor Mullen 2018-06-06 14:30:42 -07:00
parent dd62753312
commit aec88e3eba
8 changed files with 786 additions and 21 deletions

View File

@ -25,6 +25,7 @@
<MicrosoftVisualStudioComponentModelHostPackageVersion>15.0.26606</MicrosoftVisualStudioComponentModelHostPackageVersion>
<MicrosoftVisualStudioEditorPackageVersion>15.6.161-preview</MicrosoftVisualStudioEditorPackageVersion>
<MicrosoftVisualStudioLanguageIntellisensePackageVersion>15.6.161-preview</MicrosoftVisualStudioLanguageIntellisensePackageVersion>
<MicrosoftVisualStudioLanguagePackageVersion>15.8.519</MicrosoftVisualStudioLanguagePackageVersion>
<MicrosoftVisualStudioOLEInteropPackageVersion>7.10.6070</MicrosoftVisualStudioOLEInteropPackageVersion>
<MicrosoftVisualStudioProjectSystemAnalyzersPackageVersion>15.3.224</MicrosoftVisualStudioProjectSystemAnalyzersPackageVersion>
<MicrosoftVisualStudioProjectSystemManagedVSPackageVersion>2.0.6142705</MicrosoftVisualStudioProjectSystemManagedVSPackageVersion>
@ -42,7 +43,7 @@
<MoqPackageVersion>4.7.49</MoqPackageVersion>
<NETStandardLibrary20PackageVersion>2.0.3</NETStandardLibrary20PackageVersion>
<NewtonsoftJsonPackageVersion>11.0.2</NewtonsoftJsonPackageVersion>
<StreamJsonRpcPackageVersion>1.1.92</StreamJsonRpcPackageVersion>
<StreamJsonRpcPackageVersion>1.3.23</StreamJsonRpcPackageVersion>
<SystemDiagnosticsDiagnosticSourcePackageVersion>4.6.0-preview1-26727-04</SystemDiagnosticsDiagnosticSourcePackageVersion>
<SystemRuntimeInteropServicesRuntimeInformationPackageVersion>4.3.0</SystemRuntimeInteropServicesRuntimeInformationPackageVersion>
<SystemValueTuplePackageVersion>4.6.0-preview1-26727-04</SystemValueTuplePackageVersion>

View File

@ -9,7 +9,9 @@
<ItemGroup>
<PackageReference Include="Microsoft.CodeAnalysis.CSharp.Features" Version="$(VSIX_MicrosoftCodeAnalysisCSharpFeaturesPackageVersion)" />
<PackageReference Include="Microsoft.CodeAnalysis.EditorFeatures.Text" Version="$(VSIX_MicrosoftCodeAnalysisEditorFeaturesTextPackageVersion)" />
<PackageReference Include="Microsoft.VisualStudio.Text.UI" Version="$(MicrosoftVisualStudioTextUIPackageVersion)" />
<PackageReference Include="Microsoft.VisualStudio.Language.Intellisense" Version="$(MicrosoftVisualStudioLanguageIntellisensePackageVersion)" />
<PackageReference Include="Microsoft.VisualStudio.Language" Version="$(MicrosoftVisualStudioLanguagePackageVersion)" />
<PackageReference Include="StreamJsonRpc" Version="$(StreamJsonRpcPackageVersion)" />
</ItemGroup>
<ItemGroup>

View File

@ -13,9 +13,9 @@ using Microsoft.AspNetCore.Razor.Language;
using Microsoft.AspNetCore.Razor.Language.Legacy;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Completion;
using Microsoft.CodeAnalysis.Razor;
using Microsoft.CodeAnalysis.Tags;
using Microsoft.CodeAnalysis.Text;
using Microsoft.VisualStudio.Language.Intellisense.AsyncCompletion;
using Microsoft.VisualStudio.Text;
using Microsoft.VisualStudio.Text.Projection;
@ -36,16 +36,33 @@ namespace Microsoft.VisualStudio.Editor.Razor
CSharpCodeParser.TagHelperPrefixDirectiveDescriptor,
};
private readonly Lazy<RazorCodeDocumentProvider> _codeDocumentProvider;
private readonly IAsyncCompletionBroker _asyncCompletionBroker;
private readonly RazorTextBufferProvider _textBufferProvider;
[ImportingConstructor]
public RazorDirectiveCompletionProvider([Import(typeof(RazorCodeDocumentProvider))] Lazy<RazorCodeDocumentProvider> codeDocumentProvider)
public RazorDirectiveCompletionProvider(
[Import(typeof(RazorCodeDocumentProvider))] Lazy<RazorCodeDocumentProvider> codeDocumentProvider,
IAsyncCompletionBroker asyncCompletionBroker,
RazorTextBufferProvider textBufferProvider)
{
if (codeDocumentProvider == null)
{
throw new ArgumentNullException(nameof(codeDocumentProvider));
}
if (asyncCompletionBroker == null)
{
throw new ArgumentNullException(nameof(asyncCompletionBroker));
}
if (textBufferProvider == null)
{
throw new ArgumentNullException(nameof(textBufferProvider));
}
_codeDocumentProvider = codeDocumentProvider;
_asyncCompletionBroker = asyncCompletionBroker;
_textBufferProvider = textBufferProvider;
}
public override Task<CompletionDescription> GetDescriptionAsync(Document document, CompletionItem item, CancellationToken cancellationToken)
@ -86,6 +103,13 @@ namespace Microsoft.VisualStudio.Editor.Razor
return Task.CompletedTask;
}
if (!_textBufferProvider.TryGetFromDocument(context.Document, out var textBuffer) ||
!_asyncCompletionBroker.IsCompletionSupported(textBuffer.ContentType))
{
// Completion is not supported.
return Task.CompletedTask;
}
var result = AddCompletionItems(context);
return result;

View File

@ -0,0 +1,167 @@
// 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.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;
using Microsoft.VisualStudio.Language.Intellisense.AsyncCompletion;
using Microsoft.VisualStudio.Language.Intellisense.AsyncCompletion.Data;
using Microsoft.VisualStudio.Text;
using Microsoft.VisualStudio.Text.Adornments;
namespace Microsoft.VisualStudio.Editor.Razor
{
internal class RazorDirectiveCompletionSource : IAsyncCompletionSource
{
// Internal for testing
internal static readonly object DescriptionKey = new object();
internal static readonly ImageElement DirectiveImageGlyph = new ImageElement(
new ImageId(KnownImageIds.ImageCatalogGuid, KnownImageIds.Type),
"Razor Directive.");
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 ForegroundDispatcher _foregroundDispatcher;
public RazorDirectiveCompletionSource(
VisualStudioRazorParser parser,
ForegroundDispatcher foregroundDispatcher)
{
if (parser == null)
{
throw new ArgumentNullException(nameof(parser));
}
if (foregroundDispatcher == null)
{
throw new ArgumentNullException(nameof(foregroundDispatcher));
}
_parser = parser;
_foregroundDispatcher = foregroundDispatcher;
}
public Task<CompletionContext> GetCompletionContextAsync(
InitialTrigger trigger,
SnapshotPoint triggerLocation,
SnapshotSpan applicableSpan,
CancellationToken token)
{
_foregroundDispatcher.AssertBackgroundThread();
var syntaxTree = _parser.CodeDocument?.GetSyntaxTree();
if (!AtDirectiveCompletionPoint(syntaxTree, triggerLocation))
{
return Task.FromResult(CompletionContext.Empty);
}
var completionItems = GetCompletionItems(syntaxTree);
var context = new CompletionContext(completionItems.ToImmutableArray());
return Task.FromResult(context);
}
public Task<object> GetDescriptionAsync(CompletionItem item, CancellationToken token)
{
if (!item.Properties.TryGetProperty<string>(DescriptionKey, out var directiveDescription))
{
directiveDescription = string.Empty;
}
return Task.FromResult<object>(directiveDescription);
}
public bool TryGetApplicableToSpan(char typeChar, SnapshotPoint triggerLocation, out SnapshotSpan applicableToSpan, CancellationToken token)
{
// The applicable span for completion is the piece of text a completion is for. For example:
// @Date|Time.Now
// If you trigger completion at the | then the applicable span is the region of 'DateTime'; however, Razor
// doesn't know this information so we rely on Roslyn to define what the applicable span for a completion is.
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

@ -0,0 +1,64 @@
// 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.Razor;
using Microsoft.VisualStudio.Language.Intellisense.AsyncCompletion;
using Microsoft.VisualStudio.Text;
using Microsoft.VisualStudio.Text.Editor;
using Microsoft.VisualStudio.Utilities;
namespace Microsoft.VisualStudio.Editor.Razor
{
[System.Composition.Shared]
[Export(typeof(IAsyncCompletionSourceProvider))]
[Name("Razor directive completion provider.")]
[ContentType(RazorLanguage.CoreContentType)]
internal class RazorDirectiveCompletionSourceProvider : IAsyncCompletionSourceProvider
{
private readonly ForegroundDispatcher _foregroundDispatcher;
[ImportingConstructor]
public RazorDirectiveCompletionSourceProvider(ForegroundDispatcher foregroundDispatcher)
{
if (foregroundDispatcher == null)
{
throw new ArgumentNullException(nameof(foregroundDispatcher));
}
_foregroundDispatcher = foregroundDispatcher;
}
public IAsyncCompletionSource GetOrCreate(ITextView textView)
{
if (textView == null)
{
throw new ArgumentNullException(nameof(textView));
}
var razorBuffer = textView.BufferGraph.GetRazorBuffers().FirstOrDefault();
if (!razorBuffer.Properties.TryGetProperty(typeof(RazorDirectiveCompletionSource), out IAsyncCompletionSource completionSource))
{
completionSource = CreateCompletionSource(razorBuffer);
razorBuffer.Properties.AddProperty(typeof(RazorDirectiveCompletionSource), completionSource);
}
return completionSource;
}
// Internal for testing
internal IAsyncCompletionSource CreateCompletionSource(ITextBuffer razorBuffer)
{
if (!razorBuffer.Properties.TryGetProperty(typeof(VisualStudioRazorParser), out VisualStudioRazorParser parser))
{
// Parser hasn't been associated with the text buffer yet.
return null;
}
var completionSource = new RazorDirectiveCompletionSource(parser, _foregroundDispatcher);
return completionSource;
}
}
}

View File

@ -4,7 +4,6 @@
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.ComponentModel.Composition;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
@ -16,9 +15,12 @@ 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;
using Microsoft.VisualStudio.Utilities;
using Moq;
using Xunit;
using ITextBuffer = Microsoft.VisualStudio.Text.ITextBuffer;
namespace Microsoft.VisualStudio.Editor.Razor
{
@ -31,12 +33,23 @@ namespace Microsoft.VisualStudio.Editor.Razor
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);
}
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);
var completionProvider = new FailOnGetCompletionsProvider(codeDocumentProvider, CompletionBroker, TextBufferProvider);
var document = CreateDocument();
codeDocumentProvider.Value.TryGetFromDocument(document, out var codeDocument);
var syntaxTree = codeDocument.GetSyntaxTree();
@ -61,7 +74,10 @@ namespace Microsoft.VisualStudio.Editor.Razor
[RazorDirectiveCompletionProvider.DescriptionKey] = expectedDescription,
}).ToImmutableDictionary());
var codeDocumentProvider = new Mock<RazorCodeDocumentProvider>();
var completionProvider = new RazorDirectiveCompletionProvider(new Lazy<RazorCodeDocumentProvider>(() => codeDocumentProvider.Object));
var completionProvider = new RazorDirectiveCompletionProvider(
new Lazy<RazorCodeDocumentProvider>(() => codeDocumentProvider.Object),
CompletionBroker,
TextBufferProvider);
// Act
var description = await completionProvider.GetDescriptionAsync(document, item, CancellationToken.None);
@ -80,7 +96,10 @@ namespace Microsoft.VisualStudio.Editor.Razor
var document = CreateDocument();
var item = CompletionItem.Create("TestDirective");
var codeDocumentProvider = new Mock<RazorCodeDocumentProvider>();
var completionProvider = new RazorDirectiveCompletionProvider(new Lazy<RazorCodeDocumentProvider>(() => codeDocumentProvider.Object));
var completionProvider = new RazorDirectiveCompletionProvider(
new Lazy<RazorCodeDocumentProvider>(() => codeDocumentProvider.Object),
CompletionBroker,
TextBufferProvider);
// Act
var description = await completionProvider.GetDescriptionAsync(document, item, CancellationToken.None);
@ -95,7 +114,10 @@ namespace Microsoft.VisualStudio.Editor.Razor
{
// Arrange
var codeDocumentProvider = new Mock<RazorCodeDocumentProvider>(MockBehavior.Strict);
var completionProvider = new FailOnGetCompletionsProvider(new Lazy<RazorCodeDocumentProvider>(() => codeDocumentProvider.Object));
var completionProvider = new FailOnGetCompletionsProvider(
new Lazy<RazorCodeDocumentProvider>(() => codeDocumentProvider.Object),
CompletionBroker,
TextBufferProvider);
var document = CreateDocument();
document = document.WithFilePath("NotRazor.cs");
var context = CreateContext(1, completionProvider, document);
@ -121,7 +143,10 @@ namespace Microsoft.VisualStudio.Editor.Razor
});
var codeDocumentProvider = new Mock<RazorCodeDocumentProvider>(MockBehavior.Strict);
var completionProvider = new FailOnGetCompletionsProvider(new Lazy<RazorCodeDocumentProvider>(() => codeDocumentProvider.Object));
var completionProvider = new FailOnGetCompletionsProvider(
new Lazy<RazorCodeDocumentProvider>(() => codeDocumentProvider.Object),
CompletionBroker,
TextBufferProvider);
var context = CreateContext(1, completionProvider, document);
// Act & Assert
@ -136,7 +161,10 @@ namespace Microsoft.VisualStudio.Editor.Razor
var codeDocumentProvider = new Mock<RazorCodeDocumentProvider>();
codeDocumentProvider.Setup(provider => provider.TryGetFromDocument(It.IsAny<TextDocument>(), out codeDocument))
.Returns(false);
var completionProvider = new FailOnGetCompletionsProvider(new Lazy<RazorCodeDocumentProvider>(() => codeDocumentProvider.Object));
var completionProvider = new FailOnGetCompletionsProvider(
new Lazy<RazorCodeDocumentProvider>(() => codeDocumentProvider.Object),
CompletionBroker,
TextBufferProvider);
var document = CreateDocument();
var context = CreateContext(1, completionProvider, document);
@ -149,7 +177,11 @@ namespace Microsoft.VisualStudio.Editor.Razor
{
// Arrange
var codeDocumentProvider = CreateCodeDocumentProvider("@", Enumerable.Empty<DirectiveDescriptor>());
var completionProvider = new FailOnGetCompletionsProvider(codeDocumentProvider, false);
var completionProvider = new FailOnGetCompletionsProvider(
codeDocumentProvider,
CompletionBroker,
TextBufferProvider,
canGetSnapshotPoint: false);
var document = CreateDocument();
var context = CreateContext(0, completionProvider, document);
@ -162,7 +194,7 @@ namespace Microsoft.VisualStudio.Editor.Razor
{
// Arrange
var codeDocumentProvider = CreateCodeDocumentProvider("@", Enumerable.Empty<DirectiveDescriptor>());
var completionProvider = new FailOnGetCompletionsProvider(codeDocumentProvider);
var completionProvider = new FailOnGetCompletionsProvider(codeDocumentProvider, CompletionBroker, TextBufferProvider);
var document = CreateDocument();
var context = CreateContext(0, completionProvider, document);
@ -177,7 +209,7 @@ namespace Microsoft.VisualStudio.Editor.Razor
{
// Arrange
var codeDocumentProvider = CreateCodeDocumentProvider("@" + content, Enumerable.Empty<DirectiveDescriptor>());
var completionProvider = new FailOnGetCompletionsProvider(codeDocumentProvider);
var completionProvider = new FailOnGetCompletionsProvider(codeDocumentProvider, CompletionBroker, TextBufferProvider);
var document = CreateDocument();
var context = CreateContext(1, completionProvider, document);
@ -190,7 +222,7 @@ namespace Microsoft.VisualStudio.Editor.Razor
{
// Arrange
var codeDocumentProvider = CreateCodeDocumentProvider("@()", Enumerable.Empty<DirectiveDescriptor>());
var completionProvider = new FailOnGetCompletionsProvider(codeDocumentProvider);
var completionProvider = new FailOnGetCompletionsProvider(codeDocumentProvider, CompletionBroker, TextBufferProvider);
var document = CreateDocument();
var context = CreateContext(2, completionProvider, document);
@ -206,7 +238,10 @@ namespace Microsoft.VisualStudio.Editor.Razor
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));
var completionProvider = new FailOnGetCompletionsProvider(
new Lazy<RazorCodeDocumentProvider>(() => codeDocumentProvider.Object),
CompletionBroker,
TextBufferProvider);
var document = CreateDocument();
var context = CreateContext(2, completionProvider, document);
@ -219,7 +254,7 @@ namespace Microsoft.VisualStudio.Editor.Razor
{
// Arrange
var codeDocumentProvider = CreateCodeDocumentProvider("@", Enumerable.Empty<DirectiveDescriptor>());
var completionProvider = new RazorDirectiveCompletionProvider(codeDocumentProvider);
var completionProvider = new RazorDirectiveCompletionProvider(codeDocumentProvider, CompletionBroker, TextBufferProvider);
var document = CreateDocument();
codeDocumentProvider.Value.TryGetFromDocument(document, out var codeDocument);
var syntaxTree = codeDocument.GetSyntaxTree();
@ -240,7 +275,7 @@ namespace Microsoft.VisualStudio.Editor.Razor
{
// Arrange
var codeDocumentProvider = CreateCodeDocumentProvider("@", new[] { SectionDirective.Directive });
var completionProvider = new RazorDirectiveCompletionProvider(codeDocumentProvider);
var completionProvider = new RazorDirectiveCompletionProvider(codeDocumentProvider, CompletionBroker, TextBufferProvider);
var document = CreateDocument();
codeDocumentProvider.Value.TryGetFromDocument(document, out var codeDocument);
var syntaxTree = codeDocument.GetSyntaxTree();
@ -263,7 +298,7 @@ namespace Microsoft.VisualStudio.Editor.Razor
// Arrange
var customDirective = DirectiveDescriptor.CreateSingleLineDirective("custom");
var codeDocumentProvider = CreateCodeDocumentProvider("@", new[] { customDirective });
var completionProvider = new RazorDirectiveCompletionProvider(codeDocumentProvider);
var completionProvider = new RazorDirectiveCompletionProvider(codeDocumentProvider, CompletionBroker, TextBufferProvider);
var document = CreateDocument();
codeDocumentProvider.Value.TryGetFromDocument(document, out var codeDocument);
var syntaxTree = codeDocument.GetSyntaxTree();
@ -348,8 +383,12 @@ namespace Microsoft.VisualStudio.Editor.Razor
{
private readonly bool _canGetSnapshotPoint;
public FailOnGetCompletionsProvider(Lazy<RazorCodeDocumentProvider> codeDocumentProvider, bool canGetSnapshotPoint = true)
: base(codeDocumentProvider)
public FailOnGetCompletionsProvider(
Lazy<RazorCodeDocumentProvider> codeDocumentProvider,
IAsyncCompletionBroker asyncCompletionBroker,
RazorTextBufferProvider textBufferProvider,
bool canGetSnapshotPoint = true)
: base(codeDocumentProvider, asyncCompletionBroker, textBufferProvider)
{
_canGetSnapshotPoint = canGetSnapshotPoint;
}

View File

@ -0,0 +1,99 @@
// 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.Razor;
using Microsoft.VisualStudio.Text;
using Microsoft.VisualStudio.Text.Editor;
using Microsoft.VisualStudio.Text.Projection;
using Microsoft.VisualStudio.Utilities;
using Moq;
using Xunit;
namespace Microsoft.VisualStudio.Editor.Razor
{
public class RazorDirectiveCompletionSourceProviderTest : ForegroundDispatcherTestBase
{
private IContentType RazorContentType { get; } = Mock.Of<IContentType>(c => c.IsOfType(RazorLanguage.ContentType) == true);
private IContentType NonRazorContentType { get; } = Mock.Of<IContentType>(c => c.IsOfType(It.IsAny<string>()) == false);
[Fact]
public void CreateCompletionSource_ReturnsNullIfParserHasNotBeenAssocitedWithRazorBuffer()
{
// Arrange
var expectedParser = Mock.Of<VisualStudioRazorParser>();
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);
// Act
var completionSource = completionSourceProvider.CreateCompletionSource(razorBuffer);
// Assert
var completionSourceImpl = Assert.IsType<RazorDirectiveCompletionSource>(completionSource);
Assert.Same(expectedParser, completionSourceImpl._parser);
}
[Fact]
public void CreateCompletionSource_CreatesACompletionSourceWithTextBuffersParser()
{
// Arrange
var razorBuffer = Mock.Of<ITextBuffer>(buffer => buffer.ContentType == RazorContentType && buffer.Properties == new PropertyCollection());
var completionSourceProvider = new RazorDirectiveCompletionSourceProvider(Dispatcher);
// Act
var completionSource = completionSourceProvider.CreateCompletionSource(razorBuffer);
// Assert
Assert.Null(completionSource);
}
[Fact]
public void GetOrCreate_ReturnsNullIfRazorBufferHasNotBeenAssociatedWithTextView()
{
// Arrange
var textView = CreateTextView(NonRazorContentType, new PropertyCollection());
var completionSourceProvider = new RazorDirectiveCompletionSourceProvider(Dispatcher);
// Act
var completionSource = completionSourceProvider.GetOrCreate(textView);
// Assert
Assert.Null(completionSource);
}
[Fact]
public void GetOrCreate_CachesCompletionSource()
{
// Arrange
var expectedParser = Mock.Of<VisualStudioRazorParser>();
var properties = new PropertyCollection();
properties.AddProperty(typeof(VisualStudioRazorParser), expectedParser);
var textView = CreateTextView(RazorContentType, properties);
var completionSourceProvider = new RazorDirectiveCompletionSourceProvider(Dispatcher);
// Act
var completionSource1 = completionSourceProvider.GetOrCreate(textView);
var completionSource2 = completionSourceProvider.GetOrCreate(textView);
// Assert
Assert.Same(completionSource1, completionSource2);
}
private static ITextView CreateTextView(IContentType contentType, PropertyCollection properties)
{
var bufferGraph = new Mock<IBufferGraph>();
bufferGraph.Setup(graph => graph.GetTextBuffers(It.IsAny<Predicate<ITextBuffer>>()))
.Returns(new Collection<ITextBuffer>()
{
Mock.Of<ITextBuffer>(buffer => buffer.ContentType == contentType && buffer.Properties == properties)
});
var textView = Mock.Of<ITextView>(view => view.BufferGraph == bufferGraph.Object);
return textView;
}
}
}

View File

@ -0,0 +1,369 @@
// 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.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Razor.Language;
using Microsoft.AspNetCore.Razor.Language.Extensions;
using Microsoft.AspNetCore.Razor.Language.Legacy;
using Microsoft.VisualStudio.Language.Intellisense.AsyncCompletion;
using Microsoft.VisualStudio.Language.Intellisense.AsyncCompletion.Data;
using Microsoft.VisualStudio.Text;
using Moq;
using Xunit;
using Span = Microsoft.VisualStudio.Text.Span;
namespace Microsoft.VisualStudio.Editor.Razor
{
public class RazorDirectiveCompletionSourceTest : ForegroundDispatcherTestBase
{
private static readonly IReadOnlyList<DirectiveDescriptor> DefaultDirectives = new[]
{
CSharpCodeParser.AddTagHelperDirectiveDescriptor,
CSharpCodeParser.RemoveTagHelperDirectiveDescriptor,
CSharpCodeParser.TagHelperPrefixDirectiveDescriptor,
};
[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 documentSnapshot = new StringTextSnapshot(text);
var triggerLocation = new SnapshotPoint(documentSnapshot, 4);
var applicableSpan = new SnapshotSpan(documentSnapshot, new Span(1, text.Length - 1 /* @ */));
// Act
var completionContext = await Task.Run(
async () => await completionSource.GetCompletionContextAsync(new InitialTrigger(), triggerLocation, applicableSpan, CancellationToken.None));
// Assert
Assert.Empty(completionContext.Items);
}
[ForegroundFact]
public async Task GetCompletionContextAsync_DoesNotProvideCompletionsWhenNotAtCompletionPoint()
{
// Arrange
var text = "@(NotValidCompletionLocation)";
var parser = CreateParser(text);
var completionSource = new RazorDirectiveCompletionSource(parser, Dispatcher);
var documentSnapshot = new StringTextSnapshot(text);
var triggerLocation = new SnapshotPoint(documentSnapshot, 4);
var applicableSpan = new SnapshotSpan(documentSnapshot, new Span(2, text.Length - 3 /* @() */));
// Act
var completionContext = await Task.Run(
async () => await completionSource.GetCompletionContextAsync(new InitialTrigger(), triggerLocation, applicableSpan, CancellationToken.None));
// Assert
Assert.Empty(completionContext.Items);
}
// This is more of an integration level test validating the end-to-end completion flow.
[ForegroundFact]
public async Task GetCompletionContextAsync_ProvidesCompletionsWhenAtCompletionPoint()
{
// Arrange
var text = "@addTag";
var parser = CreateParser(text, SectionDirective.Directive);
var completionSource = new RazorDirectiveCompletionSource(parser, Dispatcher);
var documentSnapshot = new StringTextSnapshot(text);
var triggerLocation = new SnapshotPoint(documentSnapshot, 4);
var applicableSpan = new SnapshotSpan(documentSnapshot, new Span(1, text.Length - 1 /* @ */));
// Act
var completionContext = await Task.Run(
async () => await completionSource.GetCompletionContextAsync(new InitialTrigger(), triggerLocation, applicableSpan, CancellationToken.None));
// Assert
Assert.Collection(
completionContext.Items,
item => AssertRazorCompletionItem(SectionDirective.Directive, item, completionSource),
item => AssertRazorCompletionItem(DefaultDirectives[0], item, completionSource),
item => AssertRazorCompletionItem(DefaultDirectives[1], item, completionSource),
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()
{
// Arrange
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);
// Act
var descriptionObject = await completionSource.GetDescriptionAsync(completionItem, CancellationToken.None);
// Assert
var description = Assert.IsType<string>(descriptionObject);
Assert.Equal(expectedDescription, descriptionObject);
}
[Fact]
public async Task GetDescriptionAsync_DoesNotAddDescriptionWhenPropertyAbsent()
{
// Arrange
var completionItem = new CompletionItem("TestDirective", Mock.Of<IAsyncCompletionSource>());
var completionSource = new RazorDirectiveCompletionSource(Mock.Of<VisualStudioRazorParser>(), Dispatcher);
// Act
var descriptionObject = await completionSource.GetDescriptionAsync(completionItem, CancellationToken.None);
// Assert
var description = Assert.IsType<string>(descriptionObject);
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);
Assert.Equal(item.FilterText, completionDisplayText);
Assert.Equal(item.InsertText, directive.Directive);
Assert.Same(item.Source, source);
Assert.True(item.Properties.TryGetProperty<string>(RazorDirectiveCompletionSource.DescriptionKey, out var actualDescription));
Assert.Equal(directive.Description, actualDescription);
AssertRazorCompletionItemDefaults(item);
}
private static void AssertRazorCompletionItem(DirectiveDescriptor directive, CompletionItem item, IAsyncCompletionSource source) =>
AssertRazorCompletionItem(directive.Directive, directive, item, source);
private static void AssertRazorCompletionItemDefaults(CompletionItem item)
{
Assert.Equal(item.Icon.ImageId.Guid, RazorDirectiveCompletionSource.DirectiveImageGlyph.ImageId.Guid);
var filter = Assert.Single(item.Filters);
Assert.Same(RazorDirectiveCompletionSource.DirectiveCompletionFilters[0], filter);
Assert.Equal(string.Empty, item.Suffix);
Assert.Equal(item.DisplayText, item.SortText);
Assert.Empty(item.AttributeIcons);
}
private static VisualStudioRazorParser CreateParser(string text, params DirectiveDescriptor[] directives)
{
var syntaxTree = CreateSyntaxTree(text, directives);
var codeDocument = TestRazorCodeDocument.Create(text);
codeDocument.SetSyntaxTree(syntaxTree);
var parser = Mock.Of<VisualStudioRazorParser>(p => p.CodeDocument == codeDocument);
return parser;
}
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;
}
}
}