Cleanup legacy and unintentionally public bits.

- Made `RazorTemplateEngineFactoryService`, `TagHelperResolver`, `RazorDiagnosticJsonConverter` and `RazorLanguageServiceException` internal.
- Deleted all items in the Legacy folder in VisualStudio.LanguageServices.Razor and `RazorLanguageServiceException`.

#1762
This commit is contained in:
N. Taylor Mullen 2017-11-13 15:15:47 -08:00
parent 44e5874029
commit 44b4bf6bc6
34 changed files with 15 additions and 4899 deletions

View File

@ -7,7 +7,7 @@ using Microsoft.CodeAnalysis.Host;
namespace Microsoft.CodeAnalysis.Razor
{
public abstract class RazorTemplateEngineFactoryService : ILanguageService
internal abstract class RazorTemplateEngineFactoryService : ILanguageService
{
public abstract RazorTemplateEngine Create(string projectPath, Action<IRazorEngineBuilder> configure);
}

View File

@ -7,7 +7,7 @@ using Microsoft.CodeAnalysis.Host;
namespace Microsoft.CodeAnalysis.Razor
{
public abstract class TagHelperResolver : ILanguageService
internal abstract class TagHelperResolver : ILanguageService
{
public abstract TagHelperResolutionResult GetTagHelpers(Compilation compilation);

View File

@ -29,9 +29,10 @@ namespace Microsoft.VisualStudio.LanguageServices.Razor
}
catch (Exception exception)
{
throw new RazorLanguageServiceException(
typeof(DefaultRazorEngineDirectiveResolver).FullName,
nameof(GetRazorEngineDirectivesAsync),
throw new InvalidOperationException(
Resources.FormatUnexpectedException(
typeof(DefaultRazorEngineDirectiveResolver).FullName,
nameof(GetRazorEngineDirectivesAsync)),
exception);
}
}

View File

@ -27,9 +27,10 @@ namespace Microsoft.VisualStudio.LanguageServices.Razor
}
catch (Exception exception)
{
throw new RazorLanguageServiceException(
typeof(DefaultRazorEngineDocumentGenerator).FullName,
nameof(GenerateDocumentAsync),
throw new InvalidOperationException(
Resources.FormatUnexpectedException(
typeof(DefaultRazorEngineDocumentGenerator).FullName,
nameof(GenerateDocumentAsync)),
exception);
}
}

View File

@ -74,9 +74,10 @@ namespace Microsoft.VisualStudio.LanguageServices.Razor
{
_errorReporter.ReportError(exception, project);
throw new RazorLanguageServiceException(
typeof(DefaultTagHelperResolver).FullName,
nameof(GetTagHelpersAsync),
throw new InvalidOperationException(
Resources.FormatUnexpectedException(
typeof(DefaultTagHelperResolver).FullName,
nameof(GetTagHelpersAsync)),
exception);
}
}

View File

@ -1,28 +0,0 @@
// 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 Microsoft.AspNetCore.Razor.Language;
namespace Microsoft.VisualStudio.LanguageServices.Razor
{
// ----------------------------------------------------------------------------------------------------
// NOTE: This is only here for VisualStudio binary compatibility. This type should not be used; instead
// use the Microsoft.CodeAnalysis.Razor variant from Microsoft.CodeAnalysis.Razor.Workspaces
// ----------------------------------------------------------------------------------------------------
[Flags]
public enum AcceptedCharacters
{
None = 0,
NewLine = 1,
WhiteSpace = 2,
NonWhiteSpace = 4,
AllWhiteSpace = NewLine | WhiteSpace,
Any = AllWhiteSpace | NonWhiteSpace,
AnyExceptNewline = NonWhiteSpace | WhiteSpace
}
}

View File

@ -1,69 +0,0 @@
// 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 Microsoft.AspNetCore.Razor.Language;
namespace Microsoft.VisualStudio.LanguageServices.Razor
{
// ----------------------------------------------------------------------------------------------------
// NOTE: This is only here for VisualStudio binary compatibility. This type should not be used; instead
// use the Microsoft.CodeAnalysis.Razor variant from Microsoft.CodeAnalysis.Razor.Workspaces
// ----------------------------------------------------------------------------------------------------
public class AttributeCompletionContext
{
public AttributeCompletionContext(
TagHelperDocumentContext documentContext,
IEnumerable<string> existingCompletions,
string currentTagName,
IEnumerable<KeyValuePair<string, string>> attributes,
string currentParentTagName,
Func<string, bool> inHTMLSchema)
{
if (documentContext == null)
{
throw new ArgumentNullException(nameof(documentContext));
}
if (existingCompletions == null)
{
throw new ArgumentNullException(nameof(existingCompletions));
}
if (currentTagName == null)
{
throw new ArgumentNullException(nameof(currentTagName));
}
if (attributes == null)
{
throw new ArgumentNullException(nameof(attributes));
}
if (inHTMLSchema == null)
{
throw new ArgumentNullException(nameof(inHTMLSchema));
}
DocumentContext = documentContext;
ExistingCompletions = existingCompletions;
CurrentTagName = currentTagName;
Attributes = attributes;
CurrentParentTagName = currentParentTagName;
InHTMLSchema = inHTMLSchema;
}
public TagHelperDocumentContext DocumentContext { get; }
public IEnumerable<string> ExistingCompletions { get; }
public string CurrentTagName { get; }
public IEnumerable<KeyValuePair<string, string>> Attributes { get; }
public string CurrentParentTagName { get; }
public Func<string, bool> InHTMLSchema { get; }
}
}

View File

@ -1,45 +0,0 @@
// 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.Linq;
using Microsoft.AspNetCore.Razor.Language;
namespace Microsoft.VisualStudio.LanguageServices.Razor
{
// ----------------------------------------------------------------------------------------------------
// NOTE: This is only here for VisualStudio binary compatibility. This type should not be used; instead
// use the Microsoft.CodeAnalysis.Razor variant from Microsoft.CodeAnalysis.Razor.Workspaces
// ----------------------------------------------------------------------------------------------------
public abstract class AttributeCompletionResult
{
private AttributeCompletionResult()
{
}
public abstract IReadOnlyDictionary<string, IEnumerable<BoundAttributeDescriptor>> Completions { get; }
internal static AttributeCompletionResult Create(Dictionary<string, HashSet<BoundAttributeDescriptor>> completions)
{
var readonlyCompletions = completions.ToDictionary(
key => key.Key,
value => (IEnumerable<BoundAttributeDescriptor>)value.Value,
completions.Comparer);
var result = new DefaultAttributeCompletionResult(readonlyCompletions);
return result;
}
private class DefaultAttributeCompletionResult : AttributeCompletionResult
{
private readonly IReadOnlyDictionary<string, IEnumerable<BoundAttributeDescriptor>> _completions;
public DefaultAttributeCompletionResult(IReadOnlyDictionary<string, IEnumerable<BoundAttributeDescriptor>> completions)
{
_completions = completions;
}
public override IReadOnlyDictionary<string, IEnumerable<BoundAttributeDescriptor>> Completions => _completions;
}
}
}

View File

@ -1,28 +0,0 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
namespace Microsoft.VisualStudio.LanguageServices.Razor
{
// ----------------------------------------------------------------------------------------------------
// NOTE: This is only here for VisualStudio binary compatibility. This type should not be used; instead
// use the Microsoft.CodeAnalysis.Razor variant from Microsoft.CodeAnalysis.Razor.Workspaces
// ----------------------------------------------------------------------------------------------------
public enum BlockKind
{
// Code
Statement,
Directive,
Functions,
Expression,
Helper,
// Markup
Markup,
Section,
Template,
// Special
Comment,
Tag
}
}

View File

@ -1,33 +0,0 @@
// 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.VisualStudio.LanguageServices.Razor
{
// ----------------------------------------------------------------------------------------------------
// NOTE: This is only here for VisualStudio binary compatibility. This type should not be used; instead
// use the Microsoft.CodeAnalysis.Razor variant from Microsoft.CodeAnalysis.Razor.Workspaces
// ----------------------------------------------------------------------------------------------------
public struct ClassifiedSpan
{
public ClassifiedSpan(SourceSpan span, SourceSpan blockSpan, SpanKind spanKind, BlockKind blockKind, AcceptedCharacters acceptedCharacters)
{
Span = span;
BlockSpan = blockSpan;
SpanKind = spanKind;
BlockKind = blockKind;
AcceptedCharacters = acceptedCharacters;
}
public AcceptedCharacters AcceptedCharacters { get; }
public BlockKind BlockKind { get; }
public SourceSpan BlockSpan { get; }
public SourceSpan Span { get; }
public SpanKind SpanKind { get; }
}
}

View File

@ -1,352 +0,0 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Collections.Generic;
using System.ComponentModel.Composition;
using System.Linq;
using Microsoft.AspNetCore.Razor.Language;
using Microsoft.AspNetCore.Razor.Language.Legacy;
using Microsoft.VisualStudio.Text;
using Span = Microsoft.AspNetCore.Razor.Language.Legacy.Span;
namespace Microsoft.VisualStudio.LanguageServices.Razor
{
// ----------------------------------------------------------------------------------------------------
// NOTE: This is only here for VisualStudio binary compatibility. This type should not be used; instead
// use the Microsoft.CodeAnalysis.Razor variant from Microsoft.CodeAnalysis.Razor.Workspaces
// ----------------------------------------------------------------------------------------------------
[Export(typeof(RazorSyntaxFactsService))]
internal class DefaultRazorSyntaxFactsService : RazorSyntaxFactsService
{
public override IReadOnlyList<ClassifiedSpan> GetClassifiedSpans(RazorSyntaxTree syntaxTree)
{
if (syntaxTree == null)
{
throw new ArgumentNullException(nameof(syntaxTree));
}
var spans = Flatten(syntaxTree);
var result = new ClassifiedSpan[spans.Count];
for (var i = 0; i < spans.Count; i++)
{
var span = spans[i];
result[i] = new ClassifiedSpan(
new SourceSpan(
span.Start.FilePath ?? syntaxTree.Source.FilePath,
span.Start.AbsoluteIndex,
span.Start.LineIndex,
span.Start.CharacterIndex,
span.Length),
new SourceSpan(
span.Parent.Start.FilePath ?? syntaxTree.Source.FilePath,
span.Parent.Start.AbsoluteIndex,
span.Parent.Start.LineIndex,
span.Parent.Start.CharacterIndex,
span.Parent.Length),
(SpanKind)span.Kind,
(BlockKind)span.Parent.Type,
(AcceptedCharacters)span.EditHandler.AcceptedCharacters);
}
return result;
}
private List<Span> Flatten(RazorSyntaxTree syntaxTree)
{
var result = new List<Span>();
AppendFlattenedSpans(syntaxTree.Root, result);
return result;
void AppendFlattenedSpans(SyntaxTreeNode node, List<Span> foundSpans)
{
Span spanNode = node as Span;
if (spanNode != null)
{
foundSpans.Add(spanNode);
}
else
{
TagHelperBlock tagHelperNode = node as TagHelperBlock;
if (tagHelperNode != null)
{
// These aren't in document order, sort them first and then dig in
List<SyntaxTreeNode> attributeNodes = tagHelperNode.Attributes.Select(kvp => kvp.Value).Where(att => att != null).ToList();
attributeNodes.Sort((x, y) => x.Start.AbsoluteIndex.CompareTo(y.Start.AbsoluteIndex));
foreach (SyntaxTreeNode curNode in attributeNodes)
{
AppendFlattenedSpans(curNode, foundSpans);
}
}
Block blockNode = node as Block;
if (blockNode != null)
{
foreach (SyntaxTreeNode curNode in blockNode.Children)
{
AppendFlattenedSpans(curNode, foundSpans);
}
}
}
}
}
public override IReadOnlyList<TagHelperSpan> GetTagHelperSpans(RazorSyntaxTree syntaxTree)
{
if (syntaxTree == null)
{
throw new ArgumentNullException(nameof(syntaxTree));
}
var results = new List<TagHelperSpan>();
List<Block> toProcess = new List<Block>();
List<Block> blockChildren = new List<Block>();
toProcess.Add(syntaxTree.Root);
for (var i = 0; i < toProcess.Count; i++)
{
var blockNode = toProcess[i];
TagHelperBlock tagHelperNode = blockNode as TagHelperBlock;
if (tagHelperNode != null)
{
results.Add(new TagHelperSpan(
new SourceSpan(
tagHelperNode.Start.FilePath ?? syntaxTree.Source.FilePath,
tagHelperNode.Start.AbsoluteIndex,
tagHelperNode.Start.LineIndex,
tagHelperNode.Start.CharacterIndex,
tagHelperNode.Length),
tagHelperNode.Binding));
}
// collect all child blocks and inject into toProcess as a single InsertRange
foreach (SyntaxTreeNode curNode in blockNode.Children)
{
Block curBlock = curNode as Block;
if (curBlock != null)
{
blockChildren.Add(curBlock);
}
}
if (blockChildren.Count > 0)
{
toProcess.InsertRange(i + 1, blockChildren);
blockChildren.Clear();
}
}
return results;
}
public override int? GetDesiredIndentation(RazorSyntaxTree syntaxTree, ITextSnapshot syntaxTreeSnapshot, ITextSnapshotLine line, int indentSize, int tabSize)
{
if (syntaxTree == null)
{
throw new ArgumentNullException(nameof(syntaxTree));
}
if (syntaxTreeSnapshot == null)
{
throw new ArgumentNullException(nameof(syntaxTreeSnapshot));
}
if (line == null)
{
throw new ArgumentNullException(nameof(line));
}
if (indentSize < 0)
{
throw new ArgumentOutOfRangeException(nameof(indentSize));
}
if (tabSize < 0)
{
throw new ArgumentOutOfRangeException(nameof(tabSize));
}
// The tricky thing here is that line.Snapshot is very likely newer
var previousLine = line.Snapshot.GetLineFromLineNumber(line.LineNumber - 1);
var trackingPoint = line.Snapshot.CreateTrackingPoint(line.End, PointTrackingMode.Negative);
var previousLineEnd = trackingPoint.GetPosition(syntaxTreeSnapshot);
var simulatedChange = new SourceChange(previousLineEnd, 0, string.Empty);
var owningSpan = LocateOwner(syntaxTree.Root, simulatedChange);
int? desiredIndentation = null;
if (owningSpan.Kind != SpanKindInternal.Code)
{
SyntaxTreeNode owningChild = owningSpan;
while ((owningChild.Parent != null) && !desiredIndentation.HasValue)
{
Block owningParent = owningChild.Parent;
List<SyntaxTreeNode> children = new List<SyntaxTreeNode>(owningParent.Children);
for (int i = 0; i < children.Count; i++)
{
SyntaxTreeNode curChild = children[i];
if (!curChild.IsBlock)
{
Span curSpan = curChild as Span;
if (curSpan.Kind == SpanKindInternal.MetaCode)
{
// yay! We want to use the start of this span to determine the indent level.
var startLine = line.Snapshot.GetLineFromLineNumber(curSpan.Start.LineIndex);
var extraIndent = 0;
// Dev11 337312: Only indent one level deeper if the item after the metacode is a markup block
if (i < children.Count - 1)
{
SyntaxTreeNode nextChild = children[i + 1];
if (nextChild.IsBlock && ((nextChild as Block).Type == BlockKindInternal.Markup))
{
extraIndent = indentSize;
}
}
desiredIndentation = GetIndentLevelOfLine(startLine, tabSize) + indentSize;
}
}
if (curChild == owningChild)
{
break;
}
}
owningChild = owningParent;
}
}
return desiredIndentation;
}
private Span LocateOwner(Block root, SourceChange change)
{
// Ask each child recursively
Span owner = null;
foreach (SyntaxTreeNode element in root.Children)
{
if (element.Start.AbsoluteIndex > change.Span.AbsoluteIndex)
{
// too far
break;
}
int elementLen = element.Length;
if (element.Start.AbsoluteIndex + elementLen < change.Span.AbsoluteIndex)
{
// not far enough
continue;
}
if (element.IsBlock)
{
Block block = element as Block;
if (element.Start.AbsoluteIndex + elementLen == change.Span.AbsoluteIndex)
{
Span lastDescendant = block.FindLastDescendentSpan();
if ((lastDescendant == null) && (block is TagHelperBlock))
{
TagHelperBlock tagHelperBlock = (TagHelperBlock)block;
if (tagHelperBlock.SourceEndTag != null)
{
lastDescendant = tagHelperBlock.SourceEndTag.FindLastDescendentSpan();
}
else if (tagHelperBlock.SourceStartTag != null)
{
lastDescendant = tagHelperBlock.SourceStartTag.FindLastDescendentSpan();
}
}
// Conceptually, lastDescendant should always be non-null, but runtime errs on some
// cases and makes empty blocks. Runtime will fix these issues as we find them, but make
// no guarantee that they catch them all.
if (lastDescendant == null)
{
owner = LocateOwner(block, change);
if (owner != null)
{
break;
}
}
else if (lastDescendant.EditHandler.OwnsChange(lastDescendant, change))
{
owner = lastDescendant;
break;
}
}
else
{
owner = LocateOwner(block, change);
if (owner != null)
{
break;
}
}
}
else
{
Span span = element as Span;
if (span.EditHandler.OwnsChange(span, change))
{
owner = span;
break;
}
}
}
if (owner == null)
{
TagHelperBlock tagHelperNode = root as TagHelperBlock;
if (tagHelperNode != null)
{
Block sourceStartTag = tagHelperNode.SourceStartTag;
Block sourceEndTag = tagHelperNode.SourceEndTag;
if ((sourceStartTag.Start.AbsoluteIndex <= change.Span.AbsoluteIndex) &&
(sourceStartTag.Start.AbsoluteIndex + sourceStartTag.Length >= change.Span.AbsoluteIndex))
{
// intersects the start tag
return LocateOwner(sourceStartTag, change);
}
else if ((sourceEndTag.Start.AbsoluteIndex <= change.Span.AbsoluteIndex) &&
(sourceEndTag.Start.AbsoluteIndex + sourceEndTag.Length >= change.Span.AbsoluteIndex))
{
// intersects the end tag
return LocateOwner(sourceEndTag, change);
}
}
}
return owner;
}
private int GetIndentLevelOfLine(ITextSnapshotLine line, int tabSize)
{
var indentLevel = 0;
foreach (var c in line.GetText())
{
if (!char.IsWhiteSpace(c))
{
break;
}
else if (c == '\t')
{
indentLevel += tabSize;
}
else
{
indentLevel++;
}
}
return indentLevel;
}
}
}

View File

@ -1,285 +0,0 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Collections.Generic;
using System.ComponentModel.Composition;
using System.Diagnostics;
using System.Linq;
using Microsoft.AspNetCore.Razor.Language;
namespace Microsoft.VisualStudio.LanguageServices.Razor
{
// ----------------------------------------------------------------------------------------------------
// NOTE: This is only here for VisualStudio binary compatibility. This type should not be used; instead
// use the Microsoft.CodeAnalysis.Razor variant from Microsoft.CodeAnalysis.Razor.Workspaces
// ----------------------------------------------------------------------------------------------------
[Export(typeof(TagHelperCompletionService))]
internal class DefaultTagHelperCompletionService : TagHelperCompletionService
{
private readonly TagHelperFactsService _tagHelperFactsService;
private static readonly HashSet<TagHelperDescriptor> _emptyHashSet = new HashSet<TagHelperDescriptor>();
[ImportingConstructor]
public DefaultTagHelperCompletionService(TagHelperFactsService tagHelperFactsService)
{
_tagHelperFactsService = tagHelperFactsService;
}
/*
* This API attempts to understand a users context as they're typing in a Razor file to provide TagHelper based attribute IntelliSense.
*
* Scenarios for TagHelper attribute IntelliSense follows:
* 1. TagHelperDescriptor's have matching required attribute names
* -> Provide IntelliSense for the required attributes of those descriptors to lead users towards a TagHelperified element.
* 2. TagHelperDescriptor entirely applies to current element. Tag name, attributes, everything is fulfilled.
* -> Provide IntelliSense for the bound attributes for the applied descriptors.
*
* Within each of the above scenarios if an attribute completion has a corresponding bound attribute we associate it with the corresponding
* BoundAttributeDescriptor. By doing this a user can see what C# type a TagHelper expects for the attribute.
*/
public override AttributeCompletionResult GetAttributeCompletions(AttributeCompletionContext completionContext)
{
if (completionContext == null)
{
throw new ArgumentNullException(nameof(completionContext));
}
var attributeCompletions = completionContext.ExistingCompletions.ToDictionary(
completion => completion,
_ => new HashSet<BoundAttributeDescriptor>(),
StringComparer.OrdinalIgnoreCase);
var documentContext = completionContext.DocumentContext;
var descriptorsForTag = _tagHelperFactsService.GetTagHelpersGivenTag(documentContext, completionContext.CurrentTagName, completionContext.CurrentParentTagName);
if (descriptorsForTag.Count == 0)
{
// If the current tag has no possible descriptors then we can't have any additional attributes.
var defaultResult = AttributeCompletionResult.Create(attributeCompletions);
return defaultResult;
}
var prefix = documentContext.Prefix ?? string.Empty;
Debug.Assert(completionContext.CurrentTagName.StartsWith(prefix, StringComparison.OrdinalIgnoreCase));
var applicableTagHelperBinding = _tagHelperFactsService.GetTagHelperBinding(
documentContext,
completionContext.CurrentTagName,
completionContext.Attributes,
completionContext.CurrentParentTagName);
var applicableDescriptors = applicableTagHelperBinding?.Descriptors ?? Enumerable.Empty<TagHelperDescriptor>();
var unprefixedTagName = completionContext.CurrentTagName.Substring(prefix.Length);
if (!completionContext.InHTMLSchema(unprefixedTagName) &&
applicableDescriptors.All(descriptor => descriptor.TagOutputHint == null))
{
// This isn't a known HTML tag and no descriptor has an output element hint. Remove all previous completions.
attributeCompletions.Clear();
}
for (var i = 0; i < descriptorsForTag.Count; i++)
{
var descriptor = descriptorsForTag[i];
if (applicableDescriptors.Contains(descriptor))
{
foreach (var attributeDescriptor in descriptor.BoundAttributes)
{
UpdateCompletions(attributeDescriptor.Name, attributeDescriptor);
}
}
else
{
var htmlNameToBoundAttribute = descriptor.BoundAttributes.ToDictionary(attribute => attribute.Name, StringComparer.OrdinalIgnoreCase);
foreach (var rule in descriptor.TagMatchingRules)
{
foreach (var requiredAttribute in rule.Attributes)
{
if (htmlNameToBoundAttribute.TryGetValue(requiredAttribute.Name, out var attributeDescriptor))
{
UpdateCompletions(requiredAttribute.Name, attributeDescriptor);
}
else
{
UpdateCompletions(requiredAttribute.Name, possibleDescriptor: null);
}
}
}
}
}
var completionResult = AttributeCompletionResult.Create(attributeCompletions);
return completionResult;
void UpdateCompletions(string attributeName, BoundAttributeDescriptor possibleDescriptor)
{
if (completionContext.Attributes.Any(attribute => string.Equals(attribute.Key, attributeName, StringComparison.OrdinalIgnoreCase)))
{
// Attribute is already present on this element it shouldn't exist in the completion list.
return;
}
if (!attributeCompletions.TryGetValue(attributeName, out var rules))
{
rules = new HashSet<BoundAttributeDescriptor>();
attributeCompletions[attributeName] = rules;
}
if (possibleDescriptor != null)
{
rules.Add(possibleDescriptor);
}
}
}
public override ElementCompletionResult GetElementCompletions(ElementCompletionContext completionContext)
{
if (completionContext == null)
{
throw new ArgumentNullException(nameof(completionContext));
}
var elementCompletions = new Dictionary<string, HashSet<TagHelperDescriptor>>(StringComparer.OrdinalIgnoreCase);
AddAllowedChildrenCompletions(completionContext, elementCompletions);
if (elementCompletions.Count > 0)
{
// If the containing element is already a TagHelper and only allows certain children.
var emptyResult = ElementCompletionResult.Create(elementCompletions);
return emptyResult;
}
elementCompletions = completionContext.ExistingCompletions.ToDictionary(
completion => completion,
_ => new HashSet<TagHelperDescriptor>(),
StringComparer.OrdinalIgnoreCase);
var catchAllDescriptors = new HashSet<TagHelperDescriptor>();
var prefix = completionContext.DocumentContext.Prefix ?? string.Empty;
var possibleChildDescriptors = _tagHelperFactsService.GetTagHelpersGivenParent(completionContext.DocumentContext, completionContext.ContainingTagName);
foreach (var possibleDescriptor in possibleChildDescriptors)
{
var addRuleCompletions = false;
var outputHint = possibleDescriptor.TagOutputHint;
foreach (var rule in possibleDescriptor.TagMatchingRules)
{
if (rule.TagName == TagHelperMatchingConventions.ElementCatchAllName)
{
catchAllDescriptors.Add(possibleDescriptor);
}
else if (elementCompletions.ContainsKey(rule.TagName))
{
addRuleCompletions = true;
}
else if (outputHint != null)
{
// If the current descriptor has an output hint we need to make sure it shows up only when its output hint would normally show up.
// Example: We have a MyTableTagHelper that has an output hint of "table" and a MyTrTagHelper that has an output hint of "tr".
// If we try typing in a situation like this: <body > | </body>
// We'd expect to only get "my-table" as a completion because the "body" tag doesn't allow "tr" tags.
addRuleCompletions = elementCompletions.ContainsKey(outputHint);
}
else if (!completionContext.InHTMLSchema(rule.TagName))
{
// If there is an unknown HTML schema tag that doesn't exist in the current completion we should add it. This happens for
// TagHelpers that target non-schema oriented tags.
addRuleCompletions = true;
}
if (addRuleCompletions)
{
UpdateCompletions(prefix + rule.TagName, possibleDescriptor);
}
}
}
// We needed to track all catch-alls and update their completions after all other completions have been completed.
// This way, any TagHelper added completions will also have catch-alls listed under their entries.
foreach (var catchAllDescriptor in catchAllDescriptors)
{
foreach (var completionTagName in elementCompletions.Keys)
{
if (elementCompletions[completionTagName].Count > 0 ||
!string.IsNullOrEmpty(prefix) && completionTagName.StartsWith(prefix, StringComparison.OrdinalIgnoreCase))
{
// The current completion either has other TagHelper's associated with it or is prefixed with a non-empty
// TagHelper prefix.
UpdateCompletions(completionTagName, catchAllDescriptor);
}
}
}
var result = ElementCompletionResult.Create(elementCompletions);
return result;
void UpdateCompletions(string tagName, TagHelperDescriptor possibleDescriptor)
{
if (!elementCompletions.TryGetValue(tagName, out var existingRuleDescriptors))
{
existingRuleDescriptors = new HashSet<TagHelperDescriptor>();
elementCompletions[tagName] = existingRuleDescriptors;
}
existingRuleDescriptors.Add(possibleDescriptor);
}
}
private void AddAllowedChildrenCompletions(
ElementCompletionContext completionContext,
Dictionary<string, HashSet<TagHelperDescriptor>> elementCompletions)
{
if (completionContext.ContainingTagName == null)
{
// If we're at the root then there's no containing TagHelper to specify allowed children.
return;
}
var prefix = completionContext.DocumentContext.Prefix ?? string.Empty;
var binding = _tagHelperFactsService.GetTagHelperBinding(
completionContext.DocumentContext,
completionContext.ContainingTagName,
completionContext.Attributes,
completionContext.ContainingParentTagName);
if (binding == null)
{
// Containing tag is not a TagHelper; therefore, it allows any children.
return;
}
foreach (var descriptor in binding.Descriptors)
{
foreach (var childTag in descriptor.AllowedChildTags)
{
var prefixedName = string.Concat(prefix, childTag.Name);
var descriptors = _tagHelperFactsService.GetTagHelpersGivenTag(
completionContext.DocumentContext,
prefixedName,
completionContext.ContainingTagName);
if (descriptors.Count == 0)
{
if (!elementCompletions.ContainsKey(prefixedName))
{
elementCompletions[prefixedName] = _emptyHashSet;
}
continue;
}
if (!elementCompletions.TryGetValue(prefixedName, out var existingRuleDescriptors))
{
existingRuleDescriptors = new HashSet<TagHelperDescriptor>();
elementCompletions[prefixedName] = existingRuleDescriptors;
}
existingRuleDescriptors.UnionWith(descriptors);
}
}
}
}
}

View File

@ -1,168 +0,0 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Collections.Generic;
using System.ComponentModel.Composition;
using System.Linq;
using Microsoft.AspNetCore.Razor.Language;
namespace Microsoft.VisualStudio.LanguageServices.Razor
{
// ----------------------------------------------------------------------------------------------------
// NOTE: This is only here for VisualStudio binary compatibility. This type should not be used; instead
// use the Microsoft.CodeAnalysis.Razor variant from Microsoft.CodeAnalysis.Razor.Workspaces
// ----------------------------------------------------------------------------------------------------
[Export(typeof(TagHelperFactsService))]
internal class DefaultTagHelperFactsService : TagHelperFactsService
{
public override TagHelperBinding GetTagHelperBinding(
TagHelperDocumentContext documentContext,
string tagName,
IEnumerable<KeyValuePair<string, string>> attributes,
string parentTag)
{
if (documentContext == null)
{
throw new ArgumentNullException(nameof(documentContext));
}
if (tagName == null)
{
throw new ArgumentNullException(nameof(tagName));
}
if (attributes == null)
{
throw new ArgumentNullException(nameof(attributes));
}
var descriptors = documentContext.TagHelpers;
if (descriptors == null || descriptors.Count == 0)
{
return null;
}
var prefix = documentContext.Prefix;
var tagHelperBinder = new TagHelperBinder(prefix, descriptors);
var binding = tagHelperBinder.GetBinding(tagName, attributes.ToList(), parentTag, parentIsTagHelper: false);
return binding;
}
public override IEnumerable<BoundAttributeDescriptor> GetBoundTagHelperAttributes(
TagHelperDocumentContext documentContext,
string attributeName,
TagHelperBinding binding)
{
if (documentContext == null)
{
throw new ArgumentNullException(nameof(documentContext));
}
if (attributeName == null)
{
throw new ArgumentNullException(nameof(attributeName));
}
if (binding == null)
{
throw new ArgumentNullException(nameof(binding));
}
var matchingBoundAttributes = new List<BoundAttributeDescriptor>();
foreach (var descriptor in binding.Descriptors)
{
foreach (var boundAttributeDescriptor in descriptor.BoundAttributes)
{
if (TagHelperMatchingConventions.CanSatisfyBoundAttribute(attributeName, boundAttributeDescriptor))
{
matchingBoundAttributes.Add(boundAttributeDescriptor);
// Only one bound attribute can match an attribute
break;
}
}
}
return matchingBoundAttributes;
}
public override IReadOnlyList<TagHelperDescriptor> GetTagHelpersGivenTag(
TagHelperDocumentContext documentContext,
string tagName,
string parentTag)
{
if (documentContext == null)
{
throw new ArgumentNullException(nameof(documentContext));
}
if (tagName == null)
{
throw new ArgumentNullException(nameof(tagName));
}
var matchingDescriptors = new List<TagHelperDescriptor>();
var descriptors = documentContext?.TagHelpers;
if (descriptors?.Count == 0)
{
return matchingDescriptors;
}
var prefix = documentContext.Prefix ?? string.Empty;
if (!tagName.StartsWith(prefix, StringComparison.OrdinalIgnoreCase))
{
// Can't possibly match TagHelpers, it doesn't start with the TagHelperPrefix.
return matchingDescriptors;
}
var tagNameWithoutPrefix = tagName.Substring(prefix.Length);
for (var i = 0; i < descriptors.Count; i++)
{
var descriptor = descriptors[i];
foreach (var rule in descriptor.TagMatchingRules)
{
if (TagHelperMatchingConventions.SatisfiesTagName(tagNameWithoutPrefix, rule) &&
TagHelperMatchingConventions.SatisfiesParentTag(parentTag, rule))
{
matchingDescriptors.Add(descriptor);
break;
}
}
}
return matchingDescriptors;
}
public override IReadOnlyList<TagHelperDescriptor> GetTagHelpersGivenParent(TagHelperDocumentContext documentContext, string parentTag)
{
if (documentContext == null)
{
throw new ArgumentNullException(nameof(documentContext));
}
var matchingDescriptors = new List<TagHelperDescriptor>();
var descriptors = documentContext?.TagHelpers;
if (descriptors?.Count == 0)
{
return matchingDescriptors;
}
for (var i = 0; i < descriptors.Count; i++)
{
var descriptor = descriptors[i];
foreach (var rule in descriptor.TagMatchingRules)
{
if (TagHelperMatchingConventions.SatisfiesParentTag(parentTag, rule))
{
matchingDescriptors.Add(descriptor);
break;
}
}
}
return matchingDescriptors;
}
}
}

View File

@ -1,47 +0,0 @@
// 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 Microsoft.AspNetCore.Razor.Language;
using Microsoft.VisualStudio.Text;
namespace Microsoft.VisualStudio.LanguageServices.Razor
{
/// <summary>
/// Arguments for the <see cref="RazorEditorParser.DocumentParseComplete"/> event in <see cref="RazorEditorParser"/>.
/// </summary>
public sealed class DocumentParseCompleteEventArgs : EventArgs
{
public DocumentParseCompleteEventArgs(
SourceChange change,
ITextSnapshot buffer,
bool treeStructureChanged,
RazorCodeDocument codeDocument)
{
SourceChange = change;
Buffer = buffer;
TreeStructureChanged = treeStructureChanged;
CodeDocument = codeDocument;
}
/// <summary>
/// The <see cref="AspNetCore.Razor.Language.SourceChange"/> which triggered the re-parse.
/// </summary>
public SourceChange SourceChange { get; }
/// <summary>
/// The text snapshot used in the re-parse.
/// </summary>
public ITextSnapshot Buffer { get; }
/// <summary>
/// Indicates if the tree structure has actually changed since the previous re-parse.
/// </summary>
public bool TreeStructureChanged { get; }
/// <summary>
/// The result of the parsing and code generation.
/// </summary>
public RazorCodeDocument CodeDocument { get; }
}
}

View File

@ -1,59 +0,0 @@
// 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 Microsoft.AspNetCore.Razor.Language;
namespace Microsoft.VisualStudio.LanguageServices.Razor
{
// ----------------------------------------------------------------------------------------------------
// NOTE: This is only here for VisualStudio binary compatibility. This type should not be used; instead
// use the Microsoft.CodeAnalysis.Razor variant from Microsoft.CodeAnalysis.Razor.Workspaces
// ----------------------------------------------------------------------------------------------------
public sealed class ElementCompletionContext
{
public ElementCompletionContext(
TagHelperDocumentContext documentContext,
IEnumerable<string> existingCompletions,
string containingTagName,
IEnumerable<KeyValuePair<string, string>> attributes,
string containingParentTagName,
Func<string, bool> inHTMLSchema)
{
if (documentContext == null)
{
throw new ArgumentNullException(nameof(documentContext));
}
if (existingCompletions == null)
{
throw new ArgumentNullException(nameof(existingCompletions));
}
if (inHTMLSchema == null)
{
throw new ArgumentNullException(nameof(inHTMLSchema));
}
DocumentContext = documentContext;
ExistingCompletions = existingCompletions;
ContainingTagName = containingTagName;
Attributes = attributes;
ContainingParentTagName = containingParentTagName;
InHTMLSchema = inHTMLSchema;
}
public TagHelperDocumentContext DocumentContext { get; }
public IEnumerable<string> ExistingCompletions { get; }
public string ContainingTagName { get; }
public IEnumerable<KeyValuePair<string, string>> Attributes { get; }
public string ContainingParentTagName { get; }
public Func<string, bool> InHTMLSchema { get; }
}
}

View File

@ -1,45 +0,0 @@
// 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.Linq;
using Microsoft.AspNetCore.Razor.Language;
namespace Microsoft.VisualStudio.LanguageServices.Razor
{
// ----------------------------------------------------------------------------------------------------
// NOTE: This is only here for VisualStudio binary compatibility. This type should not be used; instead
// use the Microsoft.CodeAnalysis.Razor variant from Microsoft.CodeAnalysis.Razor.Workspaces
// ----------------------------------------------------------------------------------------------------
public abstract class ElementCompletionResult
{
private ElementCompletionResult()
{
}
public abstract IReadOnlyDictionary<string, IEnumerable<TagHelperDescriptor>> Completions { get; }
internal static ElementCompletionResult Create(Dictionary<string, HashSet<TagHelperDescriptor>> completions)
{
var readonlyCompletions = completions.ToDictionary(
key => key.Key,
value => (IEnumerable<TagHelperDescriptor>)value.Value,
completions.Comparer);
var result = new DefaultElementCompletionResult(readonlyCompletions);
return result;
}
private class DefaultElementCompletionResult : ElementCompletionResult
{
private readonly IReadOnlyDictionary<string, IEnumerable<TagHelperDescriptor>> _completions;
public DefaultElementCompletionResult(IReadOnlyDictionary<string, IEnumerable<TagHelperDescriptor>> completions)
{
_completions = completions;
}
public override IReadOnlyDictionary<string, IEnumerable<TagHelperDescriptor>> Completions => _completions;
}
}
}

View File

@ -1,18 +0,0 @@
// 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.Threading.Tasks;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Razor;
namespace Microsoft.VisualStudio.LanguageServices.Razor
{
// ----------------------------------------------------------------------------------------------------
// NOTE: This is only here for VisualStudio binary compatibility. This type should not be used; instead
// use the Microsoft.CodeAnalysis.Razor variant from Microsoft.CodeAnalysis.Razor.Workspaces
// ----------------------------------------------------------------------------------------------------
public interface ITagHelperResolver
{
Task<TagHelperResolutionResult> GetTagHelpersAsync(Project project);
}
}

View File

@ -1,24 +0,0 @@
// 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.ComponentModel.Composition;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Razor;
namespace Microsoft.VisualStudio.LanguageServices.Razor
{
// ----------------------------------------------------------------------------------------------------
// NOTE: This is only here for VisualStudio binary compatibility. This type should not be used; instead
// use TagHelperResolver.
// ----------------------------------------------------------------------------------------------------
[Export(typeof(ITagHelperResolver))]
internal class LegacyTagHelperResolver : DefaultTagHelperResolver, ITagHelperResolver
{
[ImportingConstructor]
public LegacyTagHelperResolver(
[Import(typeof(VisualStudioWorkspace))] Workspace workspace)
: base(workspace.Services.GetRequiredService<ErrorReporter>(), workspace)
{
}
}
}

View File

@ -1,62 +0,0 @@
// 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;
using Inner = Microsoft.CodeAnalysis.Razor.RazorTemplateEngineFactoryService;
namespace Microsoft.VisualStudio.LanguageServices.Razor
{
// ----------------------------------------------------------------------------------------------------
// NOTE: This is only here for VisualStudio binary compatibility. This type should not be used; instead
// use DefaultTemplateEngineFactoryService.
// ----------------------------------------------------------------------------------------------------
[Export(typeof(RazorTemplateEngineFactoryService))]
internal class LegacyTemplateEngineFactoryService : RazorTemplateEngineFactoryService
{
private readonly Inner _inner;
private readonly Workspace _workspace;
[ImportingConstructor]
public LegacyTemplateEngineFactoryService([Import(typeof(VisualStudioWorkspace))] Workspace workspace)
{
if (workspace == null)
{
throw new ArgumentNullException(nameof(workspace));
}
_workspace = workspace;
_inner = workspace.Services.GetLanguageServices(RazorLanguage.Name).GetRequiredService<Inner>();
}
// internal for testing
internal LegacyTemplateEngineFactoryService(Workspace workspace, Inner inner)
{
if (workspace == null)
{
throw new ArgumentNullException(nameof(workspace));
}
if (inner == null)
{
throw new ArgumentNullException(nameof(inner));
}
_workspace = workspace;
_inner = inner;
}
public override RazorTemplateEngine Create(string projectPath, Action<IRazorEngineBuilder> configure)
{
if (projectPath == null)
{
throw new ArgumentNullException(nameof(projectPath));
}
return _inner.Create(projectPath, configure);
}
}
}

View File

@ -1,40 +0,0 @@
// 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.Editor.Razor;
using Microsoft.VisualStudio.Text;
namespace Microsoft.VisualStudio.LanguageServices.Razor.Editor
{
[System.Composition.Shared]
[Export(typeof(TextBufferCodeDocumentProvider))]
internal class LegacyTextBufferCodeDocumentProvider : TextBufferCodeDocumentProvider
{
public override bool TryGetFromBuffer(ITextBuffer textBuffer, out RazorCodeDocument codeDocument)
{
if (textBuffer == null)
{
throw new ArgumentNullException(nameof(textBuffer));
}
if (textBuffer.Properties.TryGetProperty(typeof(VisualStudioRazorParser), out VisualStudioRazorParser parser) && parser.CodeDocument != null)
{
codeDocument = parser.CodeDocument;
return true;
}
// Support the legacy parser for code document extraction.
if (textBuffer.Properties.TryGetProperty(typeof(RazorEditorParser), out RazorEditorParser legacyParser) && legacyParser.CodeDocument != null)
{
codeDocument = legacyParser.CodeDocument;
return true;
}
codeDocument = null;
return false;
}
}
}

View File

@ -1,21 +0,0 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
namespace Microsoft.VisualStudio.LanguageServices.Razor
{
[Flags]
public enum PartialParseResult
{
Rejected = 1,
Accepted = 2,
Provisional = 4,
SpanContextChanged = 8,
AutoCompleteBlock = 16
}
}

View File

@ -1,623 +0,0 @@
// 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.Diagnostics;
using System.IO;
using System.Linq;
using System.Threading;
using Microsoft.AspNetCore.Razor.Language;
using Microsoft.AspNetCore.Razor.Language.Legacy;
using Microsoft.VisualStudio.Editor.Razor;
using Microsoft.VisualStudio.Text;
namespace Microsoft.VisualStudio.LanguageServices.Razor
{
public class RazorEditorParser : IDisposable
{
private RazorTemplateEngine _templateEngine;
private AspNetCore.Razor.Language.Legacy.Span _lastChangeOwner;
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)
{
throw new ArgumentNullException(nameof(templateEngine));
}
if (string.IsNullOrEmpty(filePath))
{
throw new ArgumentException(
AspNetCore.Razor.Language.Resources.ArgumentCannotBeNullOrEmpty,
nameof(filePath));
}
TemplateEngine = templateEngine;
FilePath = filePath;
_parser = new BackgroundParser(this, filePath);
_parser.ResultsReady += (sender, args) => OnDocumentParseComplete(args);
_parser.Start();
}
/// <summary>
/// Event fired when a full reparse of the document completes.
/// </summary>
public event EventHandler<DocumentParseCompleteEventArgs> DocumentParseComplete;
public RazorTemplateEngine TemplateEngine
{
get => _templateEngine;
set
{
if (value == null)
{
throw new ArgumentNullException(nameof(value));
}
_templateEngine = value;
}
}
public string FilePath { get; }
// Internal for testing.
internal RazorSyntaxTree CurrentSyntaxTree { get; private set; }
internal RazorCodeDocument CodeDocument { get; private set; }
// Internal for testing.
internal bool LastResultProvisional { get; private set; }
public virtual string GetAutoCompleteString()
{
if (_lastAutoCompleteSpan?.EditHandler is AutoCompleteEditHandler editHandler)
{
return editHandler.AutoCompleteString;
}
return null;
}
public virtual PartialParseResult CheckForStructureChanges(SourceChange change, ITextSnapshot snapshot)
{
if (snapshot == null)
{
throw new ArgumentNullException(nameof(snapshot));
}
var result = PartialParseResultInternal.Rejected;
using (_parser.SynchronizeMainThreadState())
{
// Check if we can partial-parse
if (CurrentSyntaxTree != null && _parser.IsIdle)
{
result = TryPartialParse(change);
}
}
// If partial parsing failed or there were outstanding parser tasks, start a full reparse
if ((result & PartialParseResultInternal.Rejected) == PartialParseResultInternal.Rejected)
{
_parser.QueueChange(change, snapshot);
}
// Otherwise, remember if this was provisionally accepted for next partial parse
LastResultProvisional = (result & PartialParseResultInternal.Provisional) == PartialParseResultInternal.Provisional;
VerifyFlagsAreValid(result);
return (PartialParseResult)result;
}
/// <summary>
/// Disposes of this parser. Should be called when the editor window is closed and the document is unloaded.
/// </summary>
public void Dispose()
{
_parser.Dispose();
GC.SuppressFinalize(this);
}
private PartialParseResultInternal TryPartialParse(SourceChange change)
{
var result = PartialParseResultInternal.Rejected;
// Try the last change owner
if (_lastChangeOwner != null && _lastChangeOwner.EditHandler.OwnsChange(_lastChangeOwner, change))
{
var editResult = _lastChangeOwner.EditHandler.ApplyChange(_lastChangeOwner, change);
result = editResult.Result;
if ((editResult.Result & PartialParseResultInternal.Rejected) != PartialParseResultInternal.Rejected)
{
_lastChangeOwner.ReplaceWith(editResult.EditedSpan);
}
return result;
}
// Locate the span responsible for this change
_lastChangeOwner = CurrentSyntaxTree.Root.LocateOwner(change);
if (LastResultProvisional)
{
// Last change owner couldn't accept this, so we must do a full reparse
result = PartialParseResultInternal.Rejected;
}
else if (_lastChangeOwner != null)
{
var editResult = _lastChangeOwner.EditHandler.ApplyChange(_lastChangeOwner, change);
result = editResult.Result;
if ((editResult.Result & PartialParseResultInternal.Rejected) != PartialParseResultInternal.Rejected)
{
_lastChangeOwner.ReplaceWith(editResult.EditedSpan);
}
if ((result & PartialParseResultInternal.AutoCompleteBlock) == PartialParseResultInternal.AutoCompleteBlock)
{
_lastAutoCompleteSpan = _lastChangeOwner;
}
else
{
_lastAutoCompleteSpan = null;
}
}
return result;
}
private void OnDocumentParseComplete(DocumentParseCompleteEventArgs args)
{
using (_parser.SynchronizeMainThreadState())
{
CurrentSyntaxTree = args.CodeDocument.GetSyntaxTree();
CodeDocument = args.CodeDocument;
_lastChangeOwner = null;
}
Debug.Assert(args != null, "Event arguments cannot be null");
EventHandler<DocumentParseCompleteEventArgs> handler = DocumentParseComplete;
if (handler != null)
{
try
{
handler(this, args);
}
catch (Exception ex)
{
Debug.WriteLine("[RzEd] Document Parse Complete Handler Threw: " + ex.ToString());
}
}
}
[Conditional("DEBUG")]
private static void VerifyFlagsAreValid(PartialParseResultInternal result)
{
Debug.Assert(((result & PartialParseResultInternal.Accepted) == PartialParseResultInternal.Accepted) ||
((result & PartialParseResultInternal.Rejected) == PartialParseResultInternal.Rejected),
"Partial Parse result does not have either of Accepted or Rejected flags set");
Debug.Assert(((result & PartialParseResultInternal.Rejected) == PartialParseResultInternal.Rejected) ||
((result & PartialParseResultInternal.SpanContextChanged) != PartialParseResultInternal.SpanContextChanged),
"Partial Parse result was Accepted AND had SpanContextChanged flag set");
Debug.Assert(((result & PartialParseResultInternal.Rejected) == PartialParseResultInternal.Rejected) ||
((result & PartialParseResultInternal.AutoCompleteBlock) != PartialParseResultInternal.AutoCompleteBlock),
"Partial Parse result was Accepted AND had AutoCompleteBlock flag set");
Debug.Assert(((result & PartialParseResultInternal.Accepted) == PartialParseResultInternal.Accepted) ||
((result & PartialParseResultInternal.Provisional) != PartialParseResultInternal.Provisional),
"Partial Parse result was Rejected AND had Provisional flag set");
}
internal class BackgroundParser : IDisposable
{
private MainThreadState _main;
private BackgroundThread _bg;
public BackgroundParser(RazorEditorParser parser, string filePath)
{
_main = new MainThreadState(filePath);
_bg = new BackgroundThread(_main, parser, filePath);
_main.ResultsReady += (sender, args) => OnResultsReady(args);
}
/// <summary>
/// Fired on the main thread.
/// </summary>
public event EventHandler<DocumentParseCompleteEventArgs> ResultsReady;
public bool IsIdle
{
get { return _main.IsIdle; }
}
public void Start()
{
_bg.Start();
}
public void Cancel()
{
_main.Cancel();
}
public void QueueChange(SourceChange change, ITextSnapshot snapshot)
{
var edit = new Edit(change, snapshot);
_main.QueueChange(edit);
}
public void Dispose()
{
_main.Cancel();
}
public IDisposable SynchronizeMainThreadState()
{
return _main.Lock();
}
protected virtual void OnResultsReady(DocumentParseCompleteEventArgs args)
{
var handler = ResultsReady;
if (handler != null)
{
handler(this, args);
}
}
private static bool TreesAreDifferent(RazorSyntaxTree leftTree, RazorSyntaxTree rightTree, IEnumerable<Edit> edits, CancellationToken cancelToken)
{
return TreesAreDifferent(leftTree.Root, rightTree.Root, edits.Select(edit => edit.Change), cancelToken);
}
internal static bool TreesAreDifferent(Block leftTree, Block rightTree, IEnumerable<SourceChange> changes, CancellationToken cancelToken)
{
// Apply all the pending changes to the original tree
// PERF: If this becomes a bottleneck, we can probably do it the other way around,
// i.e. visit the tree and find applicable changes for each node.
foreach (var change in changes)
{
cancelToken.ThrowIfCancellationRequested();
var changeOwner = leftTree.LocateOwner(change);
// Apply the change to the tree
if (changeOwner == null)
{
return true;
}
var result = changeOwner.EditHandler.ApplyChange(changeOwner, change, force: true);
changeOwner.ReplaceWith(result.EditedSpan);
}
// Now compare the trees
var treesDifferent = !leftTree.EquivalentTo(rightTree);
return treesDifferent;
}
private abstract class ThreadStateBase
{
#if DEBUG
private int _id = -1;
#endif
protected ThreadStateBase()
{
}
[Conditional("DEBUG")]
protected void SetThreadId(int id)
{
#if DEBUG
_id = id;
#endif
}
[Conditional("DEBUG")]
protected void EnsureOnThread()
{
#if DEBUG
Debug.Assert(_id != -1, "SetThreadId was never called!");
Debug.Assert(Thread.CurrentThread.ManagedThreadId == _id, "Called from an unexpected thread!");
#endif
}
[Conditional("DEBUG")]
protected void EnsureNotOnThread()
{
#if DEBUG
Debug.Assert(_id != -1, "SetThreadId was never called!");
Debug.Assert(Thread.CurrentThread.ManagedThreadId != _id, "Called from an unexpected thread!");
#endif
}
}
private class MainThreadState : ThreadStateBase, IDisposable
{
private readonly CancellationTokenSource _cancelSource = new CancellationTokenSource();
private readonly ManualResetEventSlim _hasParcel = new ManualResetEventSlim(false);
private CancellationTokenSource _currentParcelCancelSource;
private string _fileName;
private readonly object _stateLock = new object();
private IList<Edit> _changes = new List<Edit>();
public MainThreadState(string fileName)
{
_fileName = fileName;
SetThreadId(Thread.CurrentThread.ManagedThreadId);
}
public event EventHandler<DocumentParseCompleteEventArgs> ResultsReady;
public CancellationToken CancelToken
{
get { return _cancelSource.Token; }
}
public bool IsIdle
{
get
{
lock (_stateLock)
{
return _currentParcelCancelSource == null;
}
}
}
public void Cancel()
{
EnsureOnThread();
_cancelSource.Cancel();
}
public IDisposable Lock()
{
Monitor.Enter(_stateLock);
return new DisposableAction(() => Monitor.Exit(_stateLock));
}
public void QueueChange(Edit edit)
{
EnsureOnThread();
lock (_stateLock)
{
// CurrentParcel token source is not null ==> There's a parse underway
if (_currentParcelCancelSource != null)
{
_currentParcelCancelSource.Cancel();
}
_changes.Add(edit);
_hasParcel.Set();
}
}
public WorkParcel GetParcel()
{
EnsureNotOnThread(); // Only the background thread can get a parcel
_hasParcel.Wait(_cancelSource.Token);
_hasParcel.Reset();
lock (_stateLock)
{
// Create a cancellation source for this parcel
_currentParcelCancelSource = new CancellationTokenSource();
var changes = _changes;
_changes = new List<Edit>();
return new WorkParcel(changes, _currentParcelCancelSource.Token);
}
}
public void ReturnParcel(DocumentParseCompleteEventArgs args)
{
lock (_stateLock)
{
// Clear the current parcel cancellation source
if (_currentParcelCancelSource != null)
{
_currentParcelCancelSource.Dispose();
_currentParcelCancelSource = null;
}
// If there are things waiting to be parsed, just don't fire the event because we're already out of date
if (_changes.Any())
{
return;
}
}
var handler = ResultsReady;
if (handler != null)
{
handler(this, args);
}
}
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
protected virtual void Dispose(bool disposing)
{
if (disposing)
{
if (_currentParcelCancelSource != null)
{
_currentParcelCancelSource.Dispose();
_currentParcelCancelSource = null;
}
_cancelSource.Dispose();
_hasParcel.Dispose();
}
}
}
private class BackgroundThread : ThreadStateBase
{
private MainThreadState _main;
private Thread _backgroundThread;
private CancellationToken _shutdownToken;
private RazorEditorParser _parser;
private string _filePath;
private RazorSyntaxTree _currentSyntaxTree;
private IList<Edit> _previouslyDiscarded = new List<Edit>();
public BackgroundThread(MainThreadState main, RazorEditorParser parser, string fileName)
{
// Run on MAIN thread!
_main = main;
_shutdownToken = _main.CancelToken;
_parser = parser;
_filePath = fileName;
_backgroundThread = new Thread(WorkerLoop);
SetThreadId(_backgroundThread.ManagedThreadId);
}
// **** ANY THREAD ****
public void Start()
{
_backgroundThread.Start();
}
// **** BACKGROUND THREAD ****
private void WorkerLoop()
{
var fileNameOnly = Path.GetFileName(_filePath);
try
{
EnsureOnThread();
while (!_shutdownToken.IsCancellationRequested)
{
// Grab the parcel of work to do
var parcel = _main.GetParcel();
if (parcel.Edits.Any())
{
try
{
DocumentParseCompleteEventArgs args = null;
using (var linkedCancel = CancellationTokenSource.CreateLinkedTokenSource(_shutdownToken, parcel.CancelToken))
{
if (!linkedCancel.IsCancellationRequested)
{
// Collect ALL changes
List<Edit> allEdits;
if (_previouslyDiscarded != null)
{
allEdits = Enumerable.Concat(_previouslyDiscarded, parcel.Edits).ToList();
}
else
{
allEdits = parcel.Edits.ToList();
}
var finalEdit = allEdits.Last();
var results = ParseChange(finalEdit.Snapshot, linkedCancel.Token);
if (results != null && !linkedCancel.IsCancellationRequested)
{
// Clear discarded changes list
_previouslyDiscarded = null;
var treeStructureChanged = _currentSyntaxTree == null || TreesAreDifferent(_currentSyntaxTree, results.GetSyntaxTree(), allEdits, parcel.CancelToken);
_currentSyntaxTree = results.GetSyntaxTree();
// Build Arguments
args = new DocumentParseCompleteEventArgs(
finalEdit.Change,
finalEdit.Snapshot,
treeStructureChanged,
results);
}
else
{
// Parse completed but we were cancelled in the mean time. Add these to the discarded changes set
_previouslyDiscarded = allEdits;
}
}
}
if (args != null)
{
_main.ReturnParcel(args);
}
}
catch (OperationCanceledException)
{
}
}
else
{
Thread.Yield();
}
}
}
catch (OperationCanceledException)
{
// Do nothing. Just shut down.
}
finally
{
// Clean up main thread resources
_main.Dispose();
}
}
private RazorCodeDocument ParseChange(ITextSnapshot snapshot, CancellationToken token)
{
EnsureOnThread();
var templateEngine = _parser.TemplateEngine;
var sourceDocument = new TextSnapshotSourceDocument(snapshot, _filePath);
var imports = templateEngine.GetImports(_filePath);
var codeDocument = RazorCodeDocument.Create(sourceDocument, imports);
templateEngine.GenerateCode(codeDocument);
return codeDocument;
}
}
private class WorkParcel
{
public WorkParcel(IList<Edit> changes, CancellationToken cancelToken)
{
Edits = changes;
CancelToken = cancelToken;
}
public CancellationToken CancelToken { get; }
public IList<Edit> Edits { get; }
}
private class Edit
{
public Edit(SourceChange change, ITextSnapshot snapshot)
{
Change = change;
Snapshot = snapshot;
}
public SourceChange Change { get; }
public ITextSnapshot Snapshot { get; set; }
}
}
}
}

View File

@ -1,22 +0,0 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System.Collections.Generic;
using Microsoft.AspNetCore.Razor.Language;
using Microsoft.VisualStudio.Text;
namespace Microsoft.VisualStudio.LanguageServices.Razor
{
// ----------------------------------------------------------------------------------------------------
// NOTE: This is only here for VisualStudio binary compatibility. This type should not be used; instead
// use the Microsoft.CodeAnalysis.Razor variant from Microsoft.CodeAnalysis.Razor.Workspaces
// ----------------------------------------------------------------------------------------------------
public abstract class RazorSyntaxFactsService
{
public abstract IReadOnlyList<ClassifiedSpan> GetClassifiedSpans(RazorSyntaxTree syntaxTree);
public abstract IReadOnlyList<TagHelperSpan> GetTagHelperSpans(RazorSyntaxTree syntaxTree);
public abstract int? GetDesiredIndentation(RazorSyntaxTree syntaxTree, ITextSnapshot syntaxTreeSnapshot, ITextSnapshotLine line, int indentSize, int tabSize);
}
}

View File

@ -1,17 +0,0 @@
// 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 Microsoft.AspNetCore.Razor.Language;
namespace Microsoft.VisualStudio.LanguageServices.Razor
{
// ----------------------------------------------------------------------------------------------------
// NOTE: This is only here for VisualStudio binary compatibility. This type should not be used; instead
// use the Microsoft.CodeAnalysis.Razor variant from Microsoft.CodeAnalysis.Razor.Workspaces
// ----------------------------------------------------------------------------------------------------
public abstract class RazorTemplateEngineFactoryService
{
public abstract RazorTemplateEngine Create(string projectPath, Action<IRazorEngineBuilder> configure);
}
}

View File

@ -1,18 +0,0 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
namespace Microsoft.VisualStudio.LanguageServices.Razor
{
// ----------------------------------------------------------------------------------------------------
// NOTE: This is only here for VisualStudio binary compatibility. This type should not be used; instead
// use the Microsoft.CodeAnalysis.Razor variant from Microsoft.CodeAnalysis.Razor.Workspaces
// ----------------------------------------------------------------------------------------------------
public enum SpanKind
{
Transition,
MetaCode,
Comment,
Code,
Markup
}
}

View File

@ -1,16 +0,0 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
namespace Microsoft.VisualStudio.LanguageServices.Razor
{
// ----------------------------------------------------------------------------------------------------
// NOTE: This is only here for VisualStudio binary compatibility. This type should not be used; instead
// use the Microsoft.CodeAnalysis.Razor variant from Microsoft.CodeAnalysis.Razor.Workspaces
// ----------------------------------------------------------------------------------------------------
public abstract class TagHelperCompletionService
{
public abstract AttributeCompletionResult GetAttributeCompletions(AttributeCompletionContext completionContext);
public abstract ElementCompletionResult GetElementCompletions(ElementCompletionContext completionContext);
}
}

View File

@ -1,23 +0,0 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System.Collections.Generic;
using Microsoft.AspNetCore.Razor.Language;
namespace Microsoft.VisualStudio.LanguageServices.Razor
{
// ----------------------------------------------------------------------------------------------------
// NOTE: This is only here for VisualStudio binary compatibility. This type should not be used; instead
// use the Microsoft.CodeAnalysis.Razor variant from Microsoft.CodeAnalysis.Razor.Workspaces
// ----------------------------------------------------------------------------------------------------
public abstract class TagHelperFactsService
{
public abstract TagHelperBinding GetTagHelperBinding(TagHelperDocumentContext documentContext, string tagName, IEnumerable<KeyValuePair<string, string>> attributes, string parentTag);
public abstract IEnumerable<BoundAttributeDescriptor> GetBoundTagHelperAttributes(TagHelperDocumentContext documentContext, string attributeName, TagHelperBinding binding);
public abstract IReadOnlyList<TagHelperDescriptor> GetTagHelpersGivenTag(TagHelperDocumentContext documentContext, string tagName, string parentTag);
public abstract IReadOnlyList<TagHelperDescriptor> GetTagHelpersGivenParent(TagHelperDocumentContext documentContext, string parentTag);
}
}

View File

@ -1,33 +0,0 @@
// 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 Microsoft.AspNetCore.Razor.Language;
namespace Microsoft.VisualStudio.LanguageServices.Razor
{
// ----------------------------------------------------------------------------------------------------
// NOTE: This is only here for VisualStudio binary compatibility. This type should not be used; instead
// use the Microsoft.CodeAnalysis.Razor variant from Microsoft.CodeAnalysis.Razor.Workspaces
// ----------------------------------------------------------------------------------------------------
public struct TagHelperSpan
{
public TagHelperSpan(SourceSpan span, TagHelperBinding binding)
{
if (binding == null)
{
throw new ArgumentNullException(nameof(binding));
}
Span = span;
Binding = binding;
}
public TagHelperBinding Binding { get; }
public IEnumerable<TagHelperDescriptor> TagHelpers => Binding.Descriptors;
public SourceSpan Span { get; }
}
}

View File

@ -9,7 +9,7 @@ using Newtonsoft.Json.Linq;
namespace Microsoft.VisualStudio.LanguageServices.Razor
{
public class RazorDiagnosticJsonConverter : JsonConverter
internal class RazorDiagnosticJsonConverter : JsonConverter
{
public static readonly RazorDiagnosticJsonConverter Instance = new RazorDiagnosticJsonConverter();
private const string RazorDiagnosticMessageKey = "Message";

View File

@ -1,15 +0,0 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
namespace Microsoft.VisualStudio.LanguageServices.Razor
{
public sealed class RazorLanguageServiceException : Exception
{
internal RazorLanguageServiceException(string callerClass, string callerMethod, Exception innerException)
: base(Resources.FormatUnexpectedException(callerClass, callerMethod), innerException)
{
}
}
}

View File

@ -1,99 +0,0 @@
// 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.Editor.Razor;
using Microsoft.VisualStudio.Text;
using Microsoft.VisualStudio.Utilities;
using Moq;
using Xunit;
namespace Microsoft.VisualStudio.LanguageServices.Razor.Editor
{
public class LegacyTextBufferCodeDocumentProviderTest
{
[Fact]
public void TryGetFromBuffer_UsesVisualStudioRazorParserIfAvailable()
{
// Arrange
var expectedCodeDocument = TestRazorCodeDocument.Create("Hello World");
var parser = new DefaultVisualStudioRazorParser(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 LegacyTextBufferCodeDocumentProvider();
// 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 LegacyTextBufferCodeDocumentProvider();
// 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 DefaultVisualStudioRazorParser(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 LegacyTextBufferCodeDocumentProvider();
// 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 LegacyTextBufferCodeDocumentProvider();
// Act
var result = provider.TryGetFromBuffer(textBuffer.Object, out var codeDocument);
// Assert
Assert.False(result);
Assert.Null(codeDocument);
}
}
}

View File

@ -1,964 +0,0 @@
// 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 System.Collections.Generic;
using System.Linq;
using Xunit;
namespace Microsoft.VisualStudio.LanguageServices.Razor
{
public class DefaultTagHelperCompletionServiceTest
{
[Fact]
public void GetAttributeCompletions_DoesNotReturnCompletionsForAlreadySuppliedAttributes()
{
// Arrange
var documentDescriptors = new[]
{
TagHelperDescriptorBuilder.Create("DivTagHelper", "TestAssembly")
.TagMatchingRuleDescriptor(rule => rule
.RequireTagName("div")
.RequireAttributeDescriptor(attribute => attribute.Name("repeat")))
.BoundAttributeDescriptor(attribute => attribute
.Name("visible")
.TypeName(typeof(bool).FullName)
.PropertyName("Visible"))
.Build(),
TagHelperDescriptorBuilder.Create("StyleTagHelper", "TestAssembly")
.TagMatchingRuleDescriptor(rule => rule.RequireTagName("*"))
.BoundAttributeDescriptor(attribute => attribute
.Name("class")
.TypeName(typeof(string).FullName)
.PropertyName("Class"))
.Build(),
};
var expectedCompletions = AttributeCompletionResult.Create(new Dictionary<string, HashSet<BoundAttributeDescriptor>>()
{
["onclick"] = new HashSet<BoundAttributeDescriptor>(),
["visible"] = new HashSet<BoundAttributeDescriptor>()
{
documentDescriptors[0].BoundAttributes.Last()
}
});
var existingCompletions = new[] { "onclick" };
var completionContext = BuildAttributeCompletionContext(
documentDescriptors,
existingCompletions,
attributes: new Dictionary<string, string>()
{
["class"] = "something",
["repeat"] = "4"
},
currentTagName: "div");
var service = CreateTagHelperCompletionFactsService();
// Act
var completions = service.GetAttributeCompletions(completionContext);
// Assert
AssertCompletionsAreEquivalent(expectedCompletions, completions);
}
[Fact]
public void GetAttributeCompletions_PossibleDescriptorsReturnUnboundRequiredAttributesWithExistingCompletions()
{
// Arrange
var documentDescriptors = new[]
{
TagHelperDescriptorBuilder.Create("DivTagHelper", "TestAssembly")
.TagMatchingRuleDescriptor(rule => rule
.RequireTagName("div")
.RequireAttributeDescriptor(attribute => attribute.Name("repeat")))
.Build(),
TagHelperDescriptorBuilder.Create("StyleTagHelper", "TestAssembly")
.TagMatchingRuleDescriptor(rule => rule
.RequireTagName("*")
.RequireAttributeDescriptor(attribute => attribute.Name("class")))
.Build(),
};
var expectedCompletions = AttributeCompletionResult.Create(new Dictionary<string, HashSet<BoundAttributeDescriptor>>()
{
["class"] = new HashSet<BoundAttributeDescriptor>(),
["onclick"] = new HashSet<BoundAttributeDescriptor>(),
["repeat"] = new HashSet<BoundAttributeDescriptor>()
});
var existingCompletions = new[] { "onclick", "class" };
var completionContext = BuildAttributeCompletionContext(
documentDescriptors,
existingCompletions,
currentTagName: "div");
var service = CreateTagHelperCompletionFactsService();
// Act
var completions = service.GetAttributeCompletions(completionContext);
// Assert
AssertCompletionsAreEquivalent(expectedCompletions, completions);
}
[Fact]
public void GetAttributeCompletions_PossibleDescriptorsReturnBoundRequiredAttributesWithExistingCompletions()
{
// Arrange
var documentDescriptors = new[]
{
TagHelperDescriptorBuilder.Create("DivTagHelper", "TestAssembly")
.TagMatchingRuleDescriptor(rule => rule
.RequireTagName("div")
.RequireAttributeDescriptor(attribute => attribute.Name("repeat")))
.BoundAttributeDescriptor(attribute => attribute
.Name("repeat")
.TypeName(typeof(bool).FullName)
.PropertyName("Repeat"))
.BoundAttributeDescriptor(attribute => attribute
.Name("visible")
.TypeName(typeof(bool).FullName)
.PropertyName("Visible"))
.Build(),
TagHelperDescriptorBuilder.Create("StyleTagHelper", "TestAssembly")
.TagMatchingRuleDescriptor(rule => rule
.RequireTagName("*")
.RequireAttributeDescriptor(attribute => attribute.Name("class")))
.BoundAttributeDescriptor(attribute => attribute
.Name("class")
.TypeName(typeof(string).FullName)
.PropertyName("Class"))
.Build(),
};
var expectedCompletions = AttributeCompletionResult.Create(new Dictionary<string, HashSet<BoundAttributeDescriptor>>()
{
["class"] = new HashSet<BoundAttributeDescriptor>(documentDescriptors[1].BoundAttributes),
["onclick"] = new HashSet<BoundAttributeDescriptor>(),
["repeat"] = new HashSet<BoundAttributeDescriptor>()
{
documentDescriptors[0].BoundAttributes.First()
}
});
var existingCompletions = new[] { "onclick" };
var completionContext = BuildAttributeCompletionContext(
documentDescriptors,
existingCompletions,
currentTagName: "div");
var service = CreateTagHelperCompletionFactsService();
// Act
var completions = service.GetAttributeCompletions(completionContext);
// Assert
AssertCompletionsAreEquivalent(expectedCompletions, completions);
}
[Fact]
public void GetAttributeCompletions_AppliedDescriptorsReturnAllBoundAttributesWithExistingCompletionsForSchemaTags()
{
// Arrange
var documentDescriptors = new[]
{
TagHelperDescriptorBuilder.Create("DivTagHelper", "TestAssembly")
.TagMatchingRuleDescriptor(rule => rule.RequireTagName("div"))
.BoundAttributeDescriptor(attribute => attribute
.Name("repeat")
.TypeName(typeof(bool).FullName)
.PropertyName("Repeat"))
.BoundAttributeDescriptor(attribute => attribute
.Name("visible")
.TypeName(typeof(bool).FullName)
.PropertyName("Visible"))
.Build(),
TagHelperDescriptorBuilder.Create("StyleTagHelper", "TestAssembly")
.TagMatchingRuleDescriptor(rule => rule
.RequireTagName("*")
.RequireAttributeDescriptor(attribute => attribute.Name("class")))
.BoundAttributeDescriptor(attribute => attribute
.Name("class")
.TypeName(typeof(string).FullName)
.PropertyName("Class"))
.Build(),
TagHelperDescriptorBuilder.Create("StyleTagHelper", "TestAssembly")
.TagMatchingRuleDescriptor(rule => rule.RequireTagName("*"))
.BoundAttributeDescriptor(attribute => attribute
.Name("visible")
.TypeName(typeof(bool).FullName)
.PropertyName("Visible"))
.Build(),
};
var expectedCompletions = AttributeCompletionResult.Create(new Dictionary<string, HashSet<BoundAttributeDescriptor>>()
{
["onclick"] = new HashSet<BoundAttributeDescriptor>(),
["class"] = new HashSet<BoundAttributeDescriptor>(documentDescriptors[1].BoundAttributes),
["repeat"] = new HashSet<BoundAttributeDescriptor>()
{
documentDescriptors[0].BoundAttributes.First()
},
["visible"] = new HashSet<BoundAttributeDescriptor>()
{
documentDescriptors[0].BoundAttributes.Last(),
documentDescriptors[2].BoundAttributes.First(),
}
});
var existingCompletions = new[] { "class", "onclick" };
var completionContext = BuildAttributeCompletionContext(
documentDescriptors,
existingCompletions,
currentTagName: "div");
var service = CreateTagHelperCompletionFactsService();
// Act
var completions = service.GetAttributeCompletions(completionContext);
// Assert
AssertCompletionsAreEquivalent(expectedCompletions, completions);
}
[Fact]
public void GetAttributeCompletions_AppliedTagOutputHintDescriptorsReturnBoundAttributesWithExistingCompletionsForNonSchemaTags()
{
// Arrange
var documentDescriptors = new[]
{
TagHelperDescriptorBuilder.Create("CustomTagHelper", "TestAssembly")
.TagMatchingRuleDescriptor(rule => rule.RequireTagName("custom"))
.BoundAttributeDescriptor(attribute => attribute
.Name("repeat")
.TypeName(typeof(bool).FullName)
.PropertyName("Repeat"))
.TagOutputHint("div")
.Build(),
};
var expectedCompletions = AttributeCompletionResult.Create(new Dictionary<string, HashSet<BoundAttributeDescriptor>>()
{
["class"] = new HashSet<BoundAttributeDescriptor>(),
["repeat"] = new HashSet<BoundAttributeDescriptor>(documentDescriptors[0].BoundAttributes)
});
var existingCompletions = new[] { "class" };
var completionContext = BuildAttributeCompletionContext(
documentDescriptors,
existingCompletions,
currentTagName: "custom");
var service = CreateTagHelperCompletionFactsService();
// Act
var completions = service.GetAttributeCompletions(completionContext);
// Assert
AssertCompletionsAreEquivalent(expectedCompletions, completions);
}
[Fact]
public void GetAttributeCompletions_AppliedDescriptorsReturnBoundAttributesCompletionsForNonSchemaTags()
{
// Arrange
var documentDescriptors = new[]
{
TagHelperDescriptorBuilder.Create("CustomTagHelper", "TestAssembly")
.TagMatchingRuleDescriptor(rule => rule.RequireTagName("custom"))
.BoundAttributeDescriptor(attribute => attribute
.Name("repeat")
.TypeName(typeof(bool).FullName)
.PropertyName("Repeat"))
.Build(),
};
var expectedCompletions = AttributeCompletionResult.Create(new Dictionary<string, HashSet<BoundAttributeDescriptor>>()
{
["repeat"] = new HashSet<BoundAttributeDescriptor>(documentDescriptors[0].BoundAttributes)
});
var existingCompletions = new[] { "class" };
var completionContext = BuildAttributeCompletionContext(
documentDescriptors,
existingCompletions,
currentTagName: "custom");
var service = CreateTagHelperCompletionFactsService();
// Act
var completions = service.GetAttributeCompletions(completionContext);
// Assert
AssertCompletionsAreEquivalent(expectedCompletions, completions);
}
[Fact]
public void GetAttributeCompletions_AppliedDescriptorsReturnBoundAttributesWithExistingCompletionsForSchemaTags()
{
// Arrange
var documentDescriptors = new[]
{
TagHelperDescriptorBuilder.Create("DivTagHelper", "TestAssembly")
.TagMatchingRuleDescriptor(rule => rule.RequireTagName("div"))
.BoundAttributeDescriptor(attribute => attribute
.Name("repeat")
.TypeName(typeof(bool).FullName)
.PropertyName("Repeat"))
.Build(),
};
var expectedCompletions = AttributeCompletionResult.Create(new Dictionary<string, HashSet<BoundAttributeDescriptor>>()
{
["class"] = new HashSet<BoundAttributeDescriptor>(),
["repeat"] = new HashSet<BoundAttributeDescriptor>(documentDescriptors[0].BoundAttributes)
});
var existingCompletions = new[] { "class" };
var completionContext = BuildAttributeCompletionContext(
documentDescriptors,
existingCompletions,
currentTagName: "div");
var service = CreateTagHelperCompletionFactsService();
// Act
var completions = service.GetAttributeCompletions(completionContext);
// Assert
AssertCompletionsAreEquivalent(expectedCompletions, completions);
}
[Fact]
public void GetAttributeCompletions_NoDescriptorsReturnsExistingCompletions()
{
// Arrange
var expectedCompletions = AttributeCompletionResult.Create(new Dictionary<string, HashSet<BoundAttributeDescriptor>>()
{
["class"] = new HashSet<BoundAttributeDescriptor>(),
});
var existingCompletions = new[] { "class" };
var completionContext = BuildAttributeCompletionContext(
Enumerable.Empty<TagHelperDescriptor>(),
existingCompletions,
currentTagName: "div");
var service = CreateTagHelperCompletionFactsService();
// Act
var completions = service.GetAttributeCompletions(completionContext);
// Assert
AssertCompletionsAreEquivalent(expectedCompletions, completions);
}
[Fact]
public void GetAttributeCompletions_NoDescriptorsForUnprefixedTagReturnsExistingCompletions()
{
// Arrange
var documentDescriptors = new[]
{
TagHelperDescriptorBuilder.Create("DivTagHelper", "TestAssembly")
.TagMatchingRuleDescriptor(rule => rule
.RequireTagName("div")
.RequireAttributeDescriptor(attribute => attribute.Name("special")))
.Build(),
};
var expectedCompletions = AttributeCompletionResult.Create(new Dictionary<string, HashSet<BoundAttributeDescriptor>>()
{
["class"] = new HashSet<BoundAttributeDescriptor>(),
});
var existingCompletions = new[] { "class" };
var completionContext = BuildAttributeCompletionContext(
documentDescriptors,
existingCompletions,
currentTagName: "div",
tagHelperPrefix: "th:");
var service = CreateTagHelperCompletionFactsService();
// Act
var completions = service.GetAttributeCompletions(completionContext);
// Assert
AssertCompletionsAreEquivalent(expectedCompletions, completions);
}
[Fact]
public void GetAttributeCompletions_NoDescriptorsForTagReturnsExistingCompletions()
{
// Arrange
var documentDescriptors = new[]
{
TagHelperDescriptorBuilder.Create("MyTableTagHelper", "TestAssembly")
.TagMatchingRuleDescriptor(rule => rule
.RequireTagName("table")
.RequireAttributeDescriptor(attribute => attribute.Name("special")))
.Build(),
};
var expectedCompletions = AttributeCompletionResult.Create(new Dictionary<string, HashSet<BoundAttributeDescriptor>>()
{
["class"] = new HashSet<BoundAttributeDescriptor>(),
});
var existingCompletions = new[] { "class" };
var completionContext = BuildAttributeCompletionContext(
documentDescriptors,
existingCompletions,
currentTagName: "div");
var service = CreateTagHelperCompletionFactsService();
// Act
var completions = service.GetAttributeCompletions(completionContext);
// Assert
AssertCompletionsAreEquivalent(expectedCompletions, completions);
}
[Fact]
public void GetElementCompletions_TagOutputHintDoesNotFallThroughToSchemaCheck()
{
// Arrange
var documentDescriptors = new[]
{
TagHelperDescriptorBuilder.Create("MyTableTagHelper", "TestAssembly")
.TagMatchingRuleDescriptor(rule => rule.RequireTagName("my-table"))
.TagOutputHint("table")
.Build(),
TagHelperDescriptorBuilder.Create("MyTrTagHelper", "TestAssembly")
.TagMatchingRuleDescriptor(rule => rule.RequireTagName("my-tr"))
.TagOutputHint("tr")
.Build(),
};
var expectedCompletions = ElementCompletionResult.Create(new Dictionary<string, HashSet<TagHelperDescriptor>>()
{
["my-table"] = new HashSet<TagHelperDescriptor> { documentDescriptors[0] },
["table"] = new HashSet<TagHelperDescriptor>(),
});
var existingCompletions = new[] { "table" };
var completionContext = BuildElementCompletionContext(
documentDescriptors,
existingCompletions,
containingTagName: "body",
containingParentTagName: null);
var service = CreateTagHelperCompletionFactsService();
// Act
var completions = service.GetElementCompletions(completionContext);
// Assert
AssertCompletionsAreEquivalent(expectedCompletions, completions);
}
[Fact]
public void GetElementCompletions_CatchAllsOnlyApplyToCompletionsStartingWithPrefix()
{
// Arrange
var documentDescriptors = new[]
{
TagHelperDescriptorBuilder.Create("CatchAllTagHelper", "TestAssembly")
.TagMatchingRuleDescriptor(rule => rule.RequireTagName("*"))
.Build(),
TagHelperDescriptorBuilder.Create("LiTagHelper", "TestAssembly")
.TagMatchingRuleDescriptor(rule => rule.RequireTagName("li"))
.Build(),
};
var expectedCompletions = ElementCompletionResult.Create(new Dictionary<string, HashSet<TagHelperDescriptor>>()
{
["th:li"] = new HashSet<TagHelperDescriptor> { documentDescriptors[1], documentDescriptors[0] },
["li"] = new HashSet<TagHelperDescriptor>(),
});
var existingCompletions = new[] { "li" };
var completionContext = BuildElementCompletionContext(
documentDescriptors,
existingCompletions,
containingTagName: "ul",
tagHelperPrefix: "th:");
var service = CreateTagHelperCompletionFactsService();
// Act
var completions = service.GetElementCompletions(completionContext);
// Assert
AssertCompletionsAreEquivalent(expectedCompletions, completions);
}
[Fact]
public void GetElementCompletions_TagHelperPrefixIsPrependedToTagHelperCompletions()
{
// Arrange
var documentDescriptors = new[]
{
TagHelperDescriptorBuilder.Create("SuperLiTagHelper", "TestAssembly")
.TagMatchingRuleDescriptor(rule => rule.RequireTagName("superli"))
.Build(),
TagHelperDescriptorBuilder.Create("LiTagHelper", "TestAssembly")
.TagMatchingRuleDescriptor(rule => rule.RequireTagName("li"))
.Build(),
};
var expectedCompletions = ElementCompletionResult.Create(new Dictionary<string, HashSet<TagHelperDescriptor>>()
{
["th:superli"] = new HashSet<TagHelperDescriptor> { documentDescriptors[0] },
["th:li"] = new HashSet<TagHelperDescriptor> { documentDescriptors[1] },
["li"] = new HashSet<TagHelperDescriptor>(),
});
var existingCompletions = new[] { "li" };
var completionContext = BuildElementCompletionContext(
documentDescriptors,
existingCompletions,
containingTagName: "ul",
tagHelperPrefix: "th:");
var service = CreateTagHelperCompletionFactsService();
// Act
var completions = service.GetElementCompletions(completionContext);
// Assert
AssertCompletionsAreEquivalent(expectedCompletions, completions);
}
[Fact]
public void GetElementCompletions_CatchAllsApplyToOnlyTagHelperCompletions()
{
// Arrange
var documentDescriptors = new[]
{
TagHelperDescriptorBuilder.Create("SuperLiTagHelper", "TestAssembly")
.TagMatchingRuleDescriptor(rule => rule.RequireTagName("superli"))
.Build(),
TagHelperDescriptorBuilder.Create("CatchAll", "TestAssembly")
.TagMatchingRuleDescriptor(rule => rule.RequireTagName("*"))
.Build(),
};
var expectedCompletions = ElementCompletionResult.Create(new Dictionary<string, HashSet<TagHelperDescriptor>>()
{
["superli"] = new HashSet<TagHelperDescriptor>() { documentDescriptors[0], documentDescriptors[1] },
["li"] = new HashSet<TagHelperDescriptor>(),
});
var existingCompletions = new[] { "li" };
var completionContext = BuildElementCompletionContext(
documentDescriptors,
existingCompletions,
containingTagName: "ul");
var service = CreateTagHelperCompletionFactsService();
// Act
var completions = service.GetElementCompletions(completionContext);
// Assert
AssertCompletionsAreEquivalent(expectedCompletions, completions);
}
[Fact]
public void GetElementCompletions_CatchAllsApplyToNonTagHelperCompletionsIfStartsWithTagHelperPrefix()
{
// Arrange
var documentDescriptors = new[]
{
TagHelperDescriptorBuilder.Create("SuperLiTagHelper", "TestAssembly")
.TagMatchingRuleDescriptor(rule => rule.RequireTagName("superli"))
.Build(),
TagHelperDescriptorBuilder.Create("CatchAll", "TestAssembly")
.TagMatchingRuleDescriptor(rule => rule.RequireTagName("*"))
.Build(),
};
var expectedCompletions = ElementCompletionResult.Create(new Dictionary<string, HashSet<TagHelperDescriptor>>()
{
["th:superli"] = new HashSet<TagHelperDescriptor>() { documentDescriptors[0], documentDescriptors[1] },
["th:li"] = new HashSet<TagHelperDescriptor>() { documentDescriptors[1] },
});
var existingCompletions = new[] { "th:li" };
var completionContext = BuildElementCompletionContext(
documentDescriptors,
existingCompletions,
containingTagName: "ul",
tagHelperPrefix: "th:");
var service = CreateTagHelperCompletionFactsService();
// Act
var completions = service.GetElementCompletions(completionContext);
// Assert
AssertCompletionsAreEquivalent(expectedCompletions, completions);
}
[Fact]
public void GetElementCompletions_AllowsMultiTargetingTagHelpers()
{
// Arrange
var documentDescriptors = new[]
{
TagHelperDescriptorBuilder.Create("BoldTagHelper1", "TestAssembly")
.TagMatchingRuleDescriptor(rule => rule.RequireTagName("strong"))
.TagMatchingRuleDescriptor(rule => rule.RequireTagName("b"))
.TagMatchingRuleDescriptor(rule => rule.RequireTagName("bold"))
.Build(),
TagHelperDescriptorBuilder.Create("BoldTagHelper2", "TestAssembly")
.TagMatchingRuleDescriptor(rule => rule.RequireTagName("strong"))
.Build(),
};
var expectedCompletions = ElementCompletionResult.Create(new Dictionary<string, HashSet<TagHelperDescriptor>>()
{
["strong"] = new HashSet<TagHelperDescriptor> { documentDescriptors[0], documentDescriptors[1] },
["b"] = new HashSet<TagHelperDescriptor> { documentDescriptors[0] },
["bold"] = new HashSet<TagHelperDescriptor> { documentDescriptors[0] },
});
var existingCompletions = new[] { "strong", "b", "bold" };
var completionContext = BuildElementCompletionContext(
documentDescriptors,
existingCompletions,
containingTagName: "ul");
var service = CreateTagHelperCompletionFactsService();
// Act
var completions = service.GetElementCompletions(completionContext);
// Assert
AssertCompletionsAreEquivalent(expectedCompletions, completions);
}
[Fact]
public void GetElementCompletions_CombinesDescriptorsOnExistingCompletions()
{
// Arrange
var documentDescriptors = new[]
{
TagHelperDescriptorBuilder.Create("LiTagHelper1", "TestAssembly")
.TagMatchingRuleDescriptor(rule => rule.RequireTagName("li"))
.Build(),
TagHelperDescriptorBuilder.Create("LiTagHelper2", "TestAssembly")
.TagMatchingRuleDescriptor(rule => rule.RequireTagName("li"))
.Build(),
};
var expectedCompletions = ElementCompletionResult.Create(new Dictionary<string, HashSet<TagHelperDescriptor>>()
{
["li"] = new HashSet<TagHelperDescriptor> { documentDescriptors[0], documentDescriptors[1] },
});
var existingCompletions = new[] { "li" };
var completionContext = BuildElementCompletionContext(documentDescriptors, existingCompletions, containingTagName: "ul");
var service = CreateTagHelperCompletionFactsService();
// Act
var completions = service.GetElementCompletions(completionContext);
// Assert
AssertCompletionsAreEquivalent(expectedCompletions, completions);
}
[Fact]
public void GetElementCompletions_NewCompletionsForSchemaTagsNotInExistingCompletionsAreIgnored()
{
// Arrange
var documentDescriptors = new[]
{
TagHelperDescriptorBuilder.Create("SuperLiTagHelper", "TestAssembly")
.TagMatchingRuleDescriptor(rule => rule.RequireTagName("superli"))
.Build(),
TagHelperDescriptorBuilder.Create("LiTagHelper", "TestAssembly")
.TagMatchingRuleDescriptor(rule => rule.RequireTagName("li"))
.TagOutputHint("strong")
.Build(),
TagHelperDescriptorBuilder.Create("DivTagHelper", "TestAssembly")
.TagMatchingRuleDescriptor(rule => rule.RequireTagName("div"))
.Build(),
};
var expectedCompletions = ElementCompletionResult.Create(new Dictionary<string, HashSet<TagHelperDescriptor>>()
{
["li"] = new HashSet<TagHelperDescriptor> { documentDescriptors[1] },
["superli"] = new HashSet<TagHelperDescriptor> { documentDescriptors[0] },
});
var existingCompletions = new[] { "li" };
var completionContext = BuildElementCompletionContext(documentDescriptors, existingCompletions, containingTagName: "ul");
var service = CreateTagHelperCompletionFactsService();
// Act
var completions = service.GetElementCompletions(completionContext);
// Assert
AssertCompletionsAreEquivalent(expectedCompletions, completions);
}
[Fact]
public void GetElementCompletions_OutputHintIsCrossReferencedWithExistingCompletions()
{
// Arrange
var documentDescriptors = new[]
{
TagHelperDescriptorBuilder.Create("DivTagHelper", "TestAssembly")
.TagMatchingRuleDescriptor(rule => rule.RequireTagName("div"))
.TagOutputHint("li")
.Build(),
TagHelperDescriptorBuilder.Create("LiTagHelper", "TestAssembly")
.TagMatchingRuleDescriptor(rule => rule.RequireTagName("li"))
.TagOutputHint("strong")
.Build(),
};
var expectedCompletions = ElementCompletionResult.Create(new Dictionary<string, HashSet<TagHelperDescriptor>>()
{
["div"] = new HashSet<TagHelperDescriptor> { documentDescriptors[0] },
["li"] = new HashSet<TagHelperDescriptor> { documentDescriptors[1] },
});
var existingCompletions = new[] { "li" };
var completionContext = BuildElementCompletionContext(documentDescriptors, existingCompletions, containingTagName: "ul");
var service = CreateTagHelperCompletionFactsService();
// Act
var completions = service.GetElementCompletions(completionContext);
// Assert
AssertCompletionsAreEquivalent(expectedCompletions, completions);
}
[Fact]
public void GetElementCompletions_EnsuresDescriptorsHaveSatisfiedParent()
{
// Arrange
var documentDescriptors = new[]
{
TagHelperDescriptorBuilder.Create("LiTagHelper1", "TestAssembly")
.TagMatchingRuleDescriptor(rule => rule.RequireTagName("li"))
.Build(),
TagHelperDescriptorBuilder.Create("LiTagHelper2", "TestAssembly")
.TagMatchingRuleDescriptor(rule => rule.RequireTagName("li").RequireParentTag("ol"))
.Build(),
};
var expectedCompletions = ElementCompletionResult.Create(new Dictionary<string, HashSet<TagHelperDescriptor>>()
{
["li"] = new HashSet<TagHelperDescriptor> { documentDescriptors[0] },
});
var existingCompletions = new[] { "li" };
var completionContext = BuildElementCompletionContext(documentDescriptors, existingCompletions, containingTagName: "ul");
var service = CreateTagHelperCompletionFactsService();
// Act
var completions = service.GetElementCompletions(completionContext);
// Assert
AssertCompletionsAreEquivalent(expectedCompletions, completions);
}
[Fact]
public void GetElementCompletions_AllowedChildrenAreIgnoredWhenAtRoot()
{
// Arrange
var documentDescriptors = new[]
{
TagHelperDescriptorBuilder.Create("CatchAll", "TestAssembly")
.TagMatchingRuleDescriptor(rule => rule.RequireTagName("*"))
.AllowChildTag("b")
.AllowChildTag("bold")
.AllowChildTag("div")
.Build(),
};
var expectedCompletions = ElementCompletionResult.Create(new Dictionary<string, HashSet<TagHelperDescriptor>>());
var existingCompletions = Enumerable.Empty<string>();
var completionContext = BuildElementCompletionContext(
documentDescriptors,
existingCompletions,
containingTagName: null,
containingParentTagName: null);
var service = CreateTagHelperCompletionFactsService();
// Act
var completions = service.GetElementCompletions(completionContext);
// Assert
AssertCompletionsAreEquivalent(expectedCompletions, completions);
}
[Fact]
public void GetElementCompletions_DoesNotReturnExistingCompletionsWhenAllowedChildren()
{
// Arrange
var documentDescriptors = new[]
{
TagHelperDescriptorBuilder.Create("BoldParent", "TestAssembly")
.TagMatchingRuleDescriptor(rule => rule.RequireTagName("div"))
.AllowChildTag("b")
.AllowChildTag("bold")
.AllowChildTag("div")
.Build(),
};
var expectedCompletions = ElementCompletionResult.Create(new Dictionary<string, HashSet<TagHelperDescriptor>>()
{
["b"] = new HashSet<TagHelperDescriptor>(),
["bold"] = new HashSet<TagHelperDescriptor>(),
["div"] = new HashSet<TagHelperDescriptor> { documentDescriptors[0] }
});
var existingCompletions = new[] { "p", "em" };
var completionContext = BuildElementCompletionContext(documentDescriptors, existingCompletions, containingTagName: "div");
var service = CreateTagHelperCompletionFactsService();
// Act
var completions = service.GetElementCompletions(completionContext);
// Assert
AssertCompletionsAreEquivalent(expectedCompletions, completions);
}
[Fact]
public void GetElementCompletions_CapturesAllAllowedChildTagsFromParentTagHelpers_NoneTagHelpers()
{
// Arrange
var documentDescriptors = new[]
{
TagHelperDescriptorBuilder.Create("BoldParent", "TestAssembly")
.TagMatchingRuleDescriptor(rule => rule.RequireTagName("div"))
.AllowChildTag("b")
.AllowChildTag("bold")
.Build(),
};
var expectedCompletions = ElementCompletionResult.Create(new Dictionary<string, HashSet<TagHelperDescriptor>>()
{
["b"] = new HashSet<TagHelperDescriptor>(),
["bold"] = new HashSet<TagHelperDescriptor>(),
});
var completionContext = BuildElementCompletionContext(documentDescriptors, Enumerable.Empty<string>(), containingTagName: "div");
var service = CreateTagHelperCompletionFactsService();
// Act
var completions = service.GetElementCompletions(completionContext);
// Assert
AssertCompletionsAreEquivalent(expectedCompletions, completions);
}
[Fact]
public void GetElementCompletions_CapturesAllAllowedChildTagsFromParentTagHelpers_SomeTagHelpers()
{
// Arrange
var documentDescriptors = new[]
{
TagHelperDescriptorBuilder.Create("BoldParent", "TestAssembly")
.TagMatchingRuleDescriptor(rule => rule.RequireTagName("div"))
.AllowChildTag("b")
.AllowChildTag("bold")
.AllowChildTag("div")
.Build(),
};
var expectedCompletions = ElementCompletionResult.Create(new Dictionary<string, HashSet<TagHelperDescriptor>>()
{
["b"] = new HashSet<TagHelperDescriptor>(),
["bold"] = new HashSet<TagHelperDescriptor>(),
["div"] = new HashSet<TagHelperDescriptor> { documentDescriptors[0] }
});
var completionContext = BuildElementCompletionContext(documentDescriptors, Enumerable.Empty<string>(), containingTagName: "div");
var service = CreateTagHelperCompletionFactsService();
// Act
var completions = service.GetElementCompletions(completionContext);
// Assert
AssertCompletionsAreEquivalent(expectedCompletions, completions);
}
[Fact]
public void GetElementCompletions_CapturesAllAllowedChildTagsFromParentTagHelpers_AllTagHelpers()
{
// Arrange
var documentDescriptors = new[]
{
TagHelperDescriptorBuilder.Create("BoldParentCatchAll", "TestAssembly")
.TagMatchingRuleDescriptor(rule => rule.RequireTagName("*"))
.AllowChildTag("strong")
.AllowChildTag("div")
.AllowChildTag("b")
.Build(),
TagHelperDescriptorBuilder.Create("BoldParent", "TestAssembly")
.TagMatchingRuleDescriptor(rule => rule.RequireTagName("div"))
.AllowChildTag("b")
.AllowChildTag("bold")
.Build(),
};
var expectedCompletions = ElementCompletionResult.Create(new Dictionary<string, HashSet<TagHelperDescriptor>>()
{
["strong"] = new HashSet<TagHelperDescriptor> { documentDescriptors[0] },
["b"] = new HashSet<TagHelperDescriptor> { documentDescriptors[0] },
["bold"] = new HashSet<TagHelperDescriptor> { documentDescriptors[0] },
["div"] = new HashSet<TagHelperDescriptor> { documentDescriptors[0], documentDescriptors[1] },
});
var completionContext = BuildElementCompletionContext(documentDescriptors, Enumerable.Empty<string>(), containingTagName: "div");
var service = CreateTagHelperCompletionFactsService();
// Act
var completions = service.GetElementCompletions(completionContext);
// Assert
AssertCompletionsAreEquivalent(expectedCompletions, completions);
}
private static DefaultTagHelperCompletionService CreateTagHelperCompletionFactsService()
{
var tagHelperFactService = new DefaultTagHelperFactsService();
var completionFactService = new DefaultTagHelperCompletionService(tagHelperFactService);
return completionFactService;
}
private static void AssertCompletionsAreEquivalent(ElementCompletionResult expected, ElementCompletionResult actual)
{
Assert.Equal(expected.Completions.Count, actual.Completions.Count);
foreach (var expectedCompletion in expected.Completions)
{
var actualValue = actual.Completions[expectedCompletion.Key];
Assert.NotNull(actualValue);
Assert.Equal(expectedCompletion.Value, actualValue, TagHelperDescriptorComparer.CaseSensitive);
}
}
private static void AssertCompletionsAreEquivalent(AttributeCompletionResult expected, AttributeCompletionResult actual)
{
Assert.Equal(expected.Completions.Count, actual.Completions.Count);
foreach (var expectedCompletion in expected.Completions)
{
var actualValue = actual.Completions[expectedCompletion.Key];
Assert.NotNull(actualValue);
Assert.Equal(expectedCompletion.Value, actualValue, BoundAttributeDescriptorComparer.CaseSensitive);
}
}
private static ElementCompletionContext BuildElementCompletionContext(
IEnumerable<TagHelperDescriptor> descriptors,
IEnumerable<string> existingCompletions,
string containingTagName,
string containingParentTagName = "body",
string tagHelperPrefix = "")
{
var documentContext = TagHelperDocumentContext.Create(tagHelperPrefix, descriptors);
var completionContext = new ElementCompletionContext(
documentContext,
existingCompletions,
containingTagName,
attributes: Enumerable.Empty<KeyValuePair<string, string>>(),
containingParentTagName: containingParentTagName,
inHTMLSchema: (tag) => tag == "strong" || tag == "b" || tag == "bold" || tag == "li" || tag == "div");
return completionContext;
}
private static AttributeCompletionContext BuildAttributeCompletionContext(
IEnumerable<TagHelperDescriptor> descriptors,
IEnumerable<string> existingCompletions,
string currentTagName,
IEnumerable<KeyValuePair<string, string>> attributes = null,
string tagHelperPrefix = "")
{
attributes = attributes ?? Enumerable.Empty<KeyValuePair<string, string>>();
var documentContext = TagHelperDocumentContext.Create(tagHelperPrefix, descriptors);
var completionContext = new AttributeCompletionContext(
documentContext,
existingCompletions,
currentTagName,
attributes,
currentParentTagName: "body",
inHTMLSchema: (tag) => tag == "strong" || tag == "b" || tag == "bold" || tag == "li" || tag == "div");
return completionContext;
}
}
}

View File

@ -1,388 +0,0 @@
// 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 System.Collections.Generic;
using System.Linq;
using Xunit;
namespace Microsoft.VisualStudio.LanguageServices.Razor
{
public class DefaultTagHelperFactsServiceTest
{
// Purposefully not thoroughly testing DefaultTagHelperFactsService.GetTagHelperBinding because it's a pass through
// into TagHelperDescriptorProvider.GetTagHelperBinding.
[Fact]
public void GetTagHelperBinding_DoesNotAllowOptOutCharacterPrefix()
{
// Arrange
var documentDescriptors = new[]
{
TagHelperDescriptorBuilder.Create("TestType", "TestAssembly")
.TagMatchingRuleDescriptor(rule => rule.RequireTagName("*"))
.Build()
};
var documentContext = TagHelperDocumentContext.Create(string.Empty, documentDescriptors);
var service = new DefaultTagHelperFactsService();
// Act
var binding = service.GetTagHelperBinding(documentContext, "!a", Enumerable.Empty<KeyValuePair<string, string>>(), parentTag: null);
// Assert
Assert.Null(binding);
}
[Fact]
public void GetTagHelperBinding_WorksAsExpected()
{
// Arrange
var documentDescriptors = new[]
{
TagHelperDescriptorBuilder.Create("TestType", "TestAssembly")
.TagMatchingRuleDescriptor(rule =>
rule
.RequireTagName("a")
.RequireAttributeDescriptor(attribute => attribute.Name("asp-for")))
.BoundAttributeDescriptor(attribute =>
attribute
.Name("asp-for")
.TypeName(typeof(string).FullName)
.PropertyName("AspFor"))
.BoundAttributeDescriptor(attribute =>
attribute
.Name("asp-route")
.TypeName(typeof(IDictionary<string, string>).Namespace + "IDictionary<string, string>")
.PropertyName("AspRoute")
.AsDictionaryAttribute("asp-route-", typeof(string).FullName))
.Build(),
TagHelperDescriptorBuilder.Create("TestType", "TestAssembly")
.TagMatchingRuleDescriptor(rule => rule.RequireTagName("input"))
.BoundAttributeDescriptor(attribute =>
attribute
.Name("asp-for")
.TypeName(typeof(string).FullName)
.PropertyName("AspFor"))
.Build(),
};
var documentContext = TagHelperDocumentContext.Create(string.Empty, documentDescriptors);
var service = new DefaultTagHelperFactsService();
var attributes = new[]
{
new KeyValuePair<string, string>("asp-for", "Name")
};
// Act
var binding = service.GetTagHelperBinding(documentContext, "a", attributes, parentTag: "p");
// Assert
var descriptor = Assert.Single(binding.Descriptors);
Assert.Equal(documentDescriptors[0], descriptor, TagHelperDescriptorComparer.CaseSensitive);
var boundRule = Assert.Single(binding.GetBoundRules(descriptor));
Assert.Equal(documentDescriptors[0].TagMatchingRules.First(), boundRule, TagMatchingRuleDescriptorComparer.CaseSensitive);
}
[Fact]
public void GetBoundTagHelperAttributes_MatchesPrefixedAttributeName()
{
// Arrange
var documentDescriptors = new[]
{
TagHelperDescriptorBuilder.Create("TestType", "TestAssembly")
.TagMatchingRuleDescriptor(rule => rule.RequireTagName("a"))
.BoundAttributeDescriptor(attribute =>
attribute
.Name("asp-for")
.TypeName(typeof(string).FullName)
.PropertyName("AspFor"))
.BoundAttributeDescriptor(attribute =>
attribute
.Name("asp-route")
.TypeName(typeof(IDictionary<string, string>).Namespace + "IDictionary<string, string>")
.PropertyName("AspRoute")
.AsDictionaryAttribute("asp-route-", typeof(string).FullName))
.Build()
};
var expectedAttributeDescriptors = new[]
{
documentDescriptors[0].BoundAttributes.Last()
};
var documentContext = TagHelperDocumentContext.Create(string.Empty, documentDescriptors);
var service = new DefaultTagHelperFactsService();
var binding = service.GetTagHelperBinding(documentContext, "a", Enumerable.Empty<KeyValuePair<string, string>>(), parentTag: null);
// Act
var descriptors = service.GetBoundTagHelperAttributes(documentContext, "asp-route-something", binding);
// Assert
Assert.Equal(expectedAttributeDescriptors, descriptors, BoundAttributeDescriptorComparer.CaseSensitive);
}
[Fact]
public void GetBoundTagHelperAttributes_MatchesAttributeName()
{
// Arrange
var documentDescriptors = new[]
{
TagHelperDescriptorBuilder.Create("TestType", "TestAssembly")
.TagMatchingRuleDescriptor(rule => rule.RequireTagName("input"))
.BoundAttributeDescriptor(attribute =>
attribute
.Name("asp-for")
.TypeName(typeof(string).FullName)
.PropertyName("AspFor"))
.BoundAttributeDescriptor(attribute =>
attribute
.Name("asp-extra")
.TypeName(typeof(string).FullName)
.PropertyName("AspExtra"))
.Build()
};
var expectedAttributeDescriptors = new[]
{
documentDescriptors[0].BoundAttributes.First()
};
var documentContext = TagHelperDocumentContext.Create(string.Empty, documentDescriptors);
var service = new DefaultTagHelperFactsService();
var binding = service.GetTagHelperBinding(documentContext, "input", Enumerable.Empty<KeyValuePair<string, string>>(), parentTag: null);
// Act
var descriptors = service.GetBoundTagHelperAttributes(documentContext, "asp-for", binding);
// Assert
Assert.Equal(expectedAttributeDescriptors, descriptors, BoundAttributeDescriptorComparer.CaseSensitive);
}
[Fact]
public void GetTagHelpersGivenTag_DoesNotAllowOptOutCharacterPrefix()
{
// Arrange
var documentDescriptors = new[]
{
TagHelperDescriptorBuilder.Create("TestType", "TestAssembly")
.TagMatchingRuleDescriptor(rule => rule.RequireTagName("*"))
.Build()
};
var documentContext = TagHelperDocumentContext.Create(string.Empty, documentDescriptors);
var service = new DefaultTagHelperFactsService();
// Act
var descriptors = service.GetTagHelpersGivenTag(documentContext, "!strong", parentTag: null);
// Assert
Assert.Empty(descriptors);
}
[Fact]
public void GetTagHelpersGivenTag_RequiresTagName()
{
// Arrange
var documentDescriptors = new[]
{
TagHelperDescriptorBuilder.Create("TestType", "TestAssembly")
.TagMatchingRuleDescriptor(rule => rule.RequireTagName("strong"))
.Build()
};
var documentContext = TagHelperDocumentContext.Create(string.Empty, documentDescriptors);
var service = new DefaultTagHelperFactsService();
// Act
var descriptors = service.GetTagHelpersGivenTag(documentContext, "strong", "p");
// Assert
Assert.Equal(documentDescriptors, descriptors, TagHelperDescriptorComparer.CaseSensitive);
}
[Fact]
public void GetTagHelpersGivenTag_RestrictsTagHelpersBasedOnTagName()
{
// Arrange
var expectedDescriptors = new[]
{
TagHelperDescriptorBuilder.Create("TestType", "TestAssembly")
.TagMatchingRuleDescriptor(
rule => rule
.RequireTagName("a")
.RequireParentTag("div"))
.Build()
};
var documentDescriptors = new[]
{
expectedDescriptors[0],
TagHelperDescriptorBuilder.Create("TestType2", "TestAssembly")
.TagMatchingRuleDescriptor(
rule => rule
.RequireTagName("strong")
.RequireParentTag("div"))
.Build()
};
var documentContext = TagHelperDocumentContext.Create(string.Empty, documentDescriptors);
var service = new DefaultTagHelperFactsService();
// Act
var descriptors = service.GetTagHelpersGivenTag(documentContext, "a", "div");
// Assert
Assert.Equal(expectedDescriptors, descriptors, TagHelperDescriptorComparer.CaseSensitive);
}
[Fact]
public void GetTagHelpersGivenTag_RestrictsTagHelpersBasedOnTagHelperPrefix()
{
// Arrange
var expectedDescriptors = new[]
{
TagHelperDescriptorBuilder.Create("TestType", "TestAssembly")
.TagMatchingRuleDescriptor(rule => rule.RequireTagName("strong"))
.Build()
};
var documentDescriptors = new[]
{
expectedDescriptors[0],
TagHelperDescriptorBuilder.Create("TestType2", "TestAssembly")
.TagMatchingRuleDescriptor(rule => rule.RequireTagName("thstrong"))
.Build()
};
var documentContext = TagHelperDocumentContext.Create("th", documentDescriptors);
var service = new DefaultTagHelperFactsService();
// Act
var descriptors = service.GetTagHelpersGivenTag(documentContext, "thstrong", "div");
// Assert
Assert.Equal(expectedDescriptors, descriptors, TagHelperDescriptorComparer.CaseSensitive);
}
[Fact]
public void GetTagHelpersGivenTag_RestrictsTagHelpersBasedOnParent()
{
// Arrange
var expectedDescriptors = new[]
{
TagHelperDescriptorBuilder.Create("TestType", "TestAssembly")
.TagMatchingRuleDescriptor(
rule => rule
.RequireTagName("strong")
.RequireParentTag("div"))
.Build()
};
var documentDescriptors = new[]
{
expectedDescriptors[0],
TagHelperDescriptorBuilder.Create("TestType2", "TestAssembly")
.TagMatchingRuleDescriptor(
rule => rule
.RequireTagName("strong")
.RequireParentTag("p"))
.Build()
};
var documentContext = TagHelperDocumentContext.Create(string.Empty, documentDescriptors);
var service = new DefaultTagHelperFactsService();
// Act
var descriptors = service.GetTagHelpersGivenTag(documentContext, "strong", "div");
// Assert
Assert.Equal(expectedDescriptors, descriptors, TagHelperDescriptorComparer.CaseSensitive);
}
[Fact]
public void GetTagHelpersGivenParent_AllowsRootParentTag()
{
// Arrange
var documentDescriptors = new[]
{
TagHelperDescriptorBuilder.Create("TestType", "TestAssembly")
.TagMatchingRuleDescriptor(rule => rule.RequireTagName("div"))
.Build()
};
var documentContext = TagHelperDocumentContext.Create(string.Empty, documentDescriptors);
var service = new DefaultTagHelperFactsService();
// Act
var descriptors = service.GetTagHelpersGivenParent(documentContext, parentTag: null /* root */);
// Assert
Assert.Equal(documentDescriptors, descriptors, TagHelperDescriptorComparer.CaseSensitive);
}
[Fact]
public void GetTagHelpersGivenParent_AllowsRootParentTagForParentRestrictedTagHelperDescriptors()
{
// Arrange
var documentDescriptors = new[]
{
TagHelperDescriptorBuilder.Create("DivTagHelper", "TestAssembly")
.TagMatchingRuleDescriptor(rule => rule.RequireTagName("div"))
.Build(),
TagHelperDescriptorBuilder.Create("PTagHelper", "TestAssembly")
.TagMatchingRuleDescriptor(rule => rule
.RequireTagName("p")
.RequireParentTag("body"))
.Build()
};
var documentContext = TagHelperDocumentContext.Create(string.Empty, documentDescriptors);
var service = new DefaultTagHelperFactsService();
// Act
var descriptors = service.GetTagHelpersGivenParent(documentContext, parentTag: null /* root */);
// Assert
var descriptor = Assert.Single(descriptors);
Assert.Equal(documentDescriptors[0], descriptor, TagHelperDescriptorComparer.CaseSensitive);
}
[Fact]
public void GetTagHelpersGivenParent_AllowsUnspecifiedParentTagHelpers()
{
// Arrange
var documentDescriptors = new[]
{
TagHelperDescriptorBuilder.Create("TestType", "TestAssembly")
.TagMatchingRuleDescriptor(rule => rule.RequireTagName("div"))
.Build()
};
var documentContext = TagHelperDocumentContext.Create(string.Empty, documentDescriptors);
var service = new DefaultTagHelperFactsService();
// Act
var descriptors = service.GetTagHelpersGivenParent(documentContext, "p");
// Assert
Assert.Equal(documentDescriptors, descriptors, TagHelperDescriptorComparer.CaseSensitive);
}
[Fact]
public void GetTagHelpersGivenParent_RestrictsTagHelpersBasedOnParent()
{
// Arrange
var expectedDescriptors = new[]
{
TagHelperDescriptorBuilder.Create("TestType", "TestAssembly")
.TagMatchingRuleDescriptor(
rule => rule
.RequireTagName("p")
.RequireParentTag("div"))
.Build()
};
var documentDescriptors = new[]
{
expectedDescriptors[0],
TagHelperDescriptorBuilder.Create("TestType2", "TestAssembly")
.TagMatchingRuleDescriptor(
rule => rule
.RequireTagName("strong")
.RequireParentTag("p"))
.Build()
};
var documentContext = TagHelperDocumentContext.Create(string.Empty, documentDescriptors);
var service = new DefaultTagHelperFactsService();
// Act
var descriptors = service.GetTagHelpersGivenParent(documentContext, "div");
// Assert
Assert.Equal(expectedDescriptors, descriptors, TagHelperDescriptorComparer.CaseSensitive);
}
}
}