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:
parent
32d5391ff0
commit
61260ddf1c
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
@ -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)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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[]
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue