diff --git a/Razor.sln b/Razor.sln index af9dbe9e74..27f498ee49 100644 --- a/Razor.sln +++ b/Razor.sln @@ -1,6 +1,6 @@ Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio 15 -VisualStudioVersion = 15.0.26428.3 +VisualStudioVersion = 15.0.26710.3002 MinimumVisualStudioVersion = 10.0.40219.1 Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{3C0D6505-79B3-49D0-B4C3-176F0F1836ED}" EndProject @@ -54,6 +54,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.Razor. EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.Razor.Test.MvcShim", "test\Microsoft.AspNetCore.Razor.Test.MvcShim\Microsoft.AspNetCore.Razor.Test.MvcShim.csproj", "{8F165A3F-A18C-4649-AA08-C0E1BA5F5C90}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.CodeAnalysis.Razor.Workspaces.Test", "test\Microsoft.CodeAnalysis.Razor.Workspaces.Test\Microsoft.CodeAnalysis.Razor.Workspaces.Test.csproj", "{C61AAE12-5007-4205-A220-68F354A7F235}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -212,6 +214,14 @@ Global {8F165A3F-A18C-4649-AA08-C0E1BA5F5C90}.Release|Any CPU.Build.0 = Release|Any CPU {8F165A3F-A18C-4649-AA08-C0E1BA5F5C90}.ReleaseNoVSIX|Any CPU.ActiveCfg = Release|Any CPU {8F165A3F-A18C-4649-AA08-C0E1BA5F5C90}.ReleaseNoVSIX|Any CPU.Build.0 = Release|Any CPU + {C61AAE12-5007-4205-A220-68F354A7F235}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {C61AAE12-5007-4205-A220-68F354A7F235}.Debug|Any CPU.Build.0 = Debug|Any CPU + {C61AAE12-5007-4205-A220-68F354A7F235}.DebugNoVSIX|Any CPU.ActiveCfg = Debug|Any CPU + {C61AAE12-5007-4205-A220-68F354A7F235}.DebugNoVSIX|Any CPU.Build.0 = Debug|Any CPU + {C61AAE12-5007-4205-A220-68F354A7F235}.Release|Any CPU.ActiveCfg = Release|Any CPU + {C61AAE12-5007-4205-A220-68F354A7F235}.Release|Any CPU.Build.0 = Release|Any CPU + {C61AAE12-5007-4205-A220-68F354A7F235}.ReleaseNoVSIX|Any CPU.ActiveCfg = Release|Any CPU + {C61AAE12-5007-4205-A220-68F354A7F235}.ReleaseNoVSIX|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -236,5 +246,6 @@ Global {078AEF36-F319-4CE2-BAA2-5B58A6536B46} = {92463391-81BE-462B-AC3C-78C6C760741F} {82C23CF8-95FF-40F7-BF50-3AD9EE21ED58} = {92463391-81BE-462B-AC3C-78C6C760741F} {8F165A3F-A18C-4649-AA08-C0E1BA5F5C90} = {92463391-81BE-462B-AC3C-78C6C760741F} + {C61AAE12-5007-4205-A220-68F354A7F235} = {92463391-81BE-462B-AC3C-78C6C760741F} EndGlobalSection EndGlobal diff --git a/src/Microsoft.VisualStudio.LanguageServices.Razor/AcceptedCharacters.cs b/src/Microsoft.CodeAnalysis.Razor.Workspaces/AcceptedCharacters.cs similarity index 89% rename from src/Microsoft.VisualStudio.LanguageServices.Razor/AcceptedCharacters.cs rename to src/Microsoft.CodeAnalysis.Razor.Workspaces/AcceptedCharacters.cs index a5355f9bae..108513d9bd 100644 --- a/src/Microsoft.VisualStudio.LanguageServices.Razor/AcceptedCharacters.cs +++ b/src/Microsoft.CodeAnalysis.Razor.Workspaces/AcceptedCharacters.cs @@ -3,7 +3,7 @@ using System; -namespace Microsoft.VisualStudio.LanguageServices.Razor +namespace Microsoft.CodeAnalysis.Razor { [Flags] public enum AcceptedCharacters diff --git a/src/Microsoft.VisualStudio.LanguageServices.Razor/AttributeCompletionContext.cs b/src/Microsoft.CodeAnalysis.Razor.Workspaces/AttributeCompletionContext.cs similarity index 97% rename from src/Microsoft.VisualStudio.LanguageServices.Razor/AttributeCompletionContext.cs rename to src/Microsoft.CodeAnalysis.Razor.Workspaces/AttributeCompletionContext.cs index bf815aa8c0..bbf5c60dac 100644 --- a/src/Microsoft.VisualStudio.LanguageServices.Razor/AttributeCompletionContext.cs +++ b/src/Microsoft.CodeAnalysis.Razor.Workspaces/AttributeCompletionContext.cs @@ -5,7 +5,7 @@ using System; using System.Collections.Generic; using Microsoft.AspNetCore.Razor.Language; -namespace Microsoft.VisualStudio.LanguageServices.Razor +namespace Microsoft.CodeAnalysis.Razor { public class AttributeCompletionContext { diff --git a/src/Microsoft.VisualStudio.LanguageServices.Razor/AttributeCompletionResult.cs b/src/Microsoft.CodeAnalysis.Razor.Workspaces/AttributeCompletionResult.cs similarity index 96% rename from src/Microsoft.VisualStudio.LanguageServices.Razor/AttributeCompletionResult.cs rename to src/Microsoft.CodeAnalysis.Razor.Workspaces/AttributeCompletionResult.cs index b5880d4c7f..1142b9ce6c 100644 --- a/src/Microsoft.VisualStudio.LanguageServices.Razor/AttributeCompletionResult.cs +++ b/src/Microsoft.CodeAnalysis.Razor.Workspaces/AttributeCompletionResult.cs @@ -5,7 +5,7 @@ using System.Collections.Generic; using System.Linq; using Microsoft.AspNetCore.Razor.Language; -namespace Microsoft.VisualStudio.LanguageServices.Razor +namespace Microsoft.CodeAnalysis.Razor { public abstract class AttributeCompletionResult { diff --git a/src/Microsoft.VisualStudio.LanguageServices.Razor/BlockKind.cs b/src/Microsoft.CodeAnalysis.Razor.Workspaces/BlockKind.cs similarity index 88% rename from src/Microsoft.VisualStudio.LanguageServices.Razor/BlockKind.cs rename to src/Microsoft.CodeAnalysis.Razor.Workspaces/BlockKind.cs index 472db60ca0..abb703173a 100644 --- a/src/Microsoft.VisualStudio.LanguageServices.Razor/BlockKind.cs +++ b/src/Microsoft.CodeAnalysis.Razor.Workspaces/BlockKind.cs @@ -1,7 +1,7 @@ // 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 +namespace Microsoft.CodeAnalysis.Razor { public enum BlockKind { diff --git a/src/Microsoft.VisualStudio.LanguageServices.Razor/ClassifiedSpan.cs b/src/Microsoft.CodeAnalysis.Razor.Workspaces/ClassifiedSpan.cs similarity index 93% rename from src/Microsoft.VisualStudio.LanguageServices.Razor/ClassifiedSpan.cs rename to src/Microsoft.CodeAnalysis.Razor.Workspaces/ClassifiedSpan.cs index bfa3321f0e..01684066e3 100644 --- a/src/Microsoft.VisualStudio.LanguageServices.Razor/ClassifiedSpan.cs +++ b/src/Microsoft.CodeAnalysis.Razor.Workspaces/ClassifiedSpan.cs @@ -3,7 +3,7 @@ using Microsoft.AspNetCore.Razor.Language; -namespace Microsoft.VisualStudio.LanguageServices.Razor +namespace Microsoft.CodeAnalysis.Razor { public struct ClassifiedSpan { diff --git a/src/Microsoft.CodeAnalysis.Razor.Workspaces/DefaultRazorSyntaxFactsService.cs b/src/Microsoft.CodeAnalysis.Razor.Workspaces/DefaultRazorSyntaxFactsService.cs new file mode 100644 index 0000000000..1f25923d7f --- /dev/null +++ b/src/Microsoft.CodeAnalysis.Razor.Workspaces/DefaultRazorSyntaxFactsService.cs @@ -0,0 +1,342 @@ +// 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.Linq; +using Microsoft.AspNetCore.Razor.Language; +using Microsoft.AspNetCore.Razor.Language.Legacy; +using Span = Microsoft.AspNetCore.Razor.Language.Legacy.Span; + +namespace Microsoft.CodeAnalysis.Razor +{ + internal class DefaultRazorSyntaxFactsService : RazorSyntaxFactsService + { + public override IReadOnlyList 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 Flatten(RazorSyntaxTree syntaxTree) + { + var result = new List(); + AppendFlattenedSpans(syntaxTree.Root, result); + return result; + + void AppendFlattenedSpans(SyntaxTreeNode node, List 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 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 GetTagHelperSpans(RazorSyntaxTree syntaxTree) + { + if (syntaxTree == null) + { + throw new ArgumentNullException(nameof(syntaxTree)); + } + + var results = new List(); + + List toProcess = new List(); + List blockChildren = new List(); + 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, int previousLineEndIndex, Func getLineContent, int indentSize, int tabSize) + { + if (syntaxTree == null) + { + throw new ArgumentNullException(nameof(syntaxTree)); + } + + if (previousLineEndIndex < 0) + { + throw new ArgumentOutOfRangeException(nameof(previousLineEndIndex)); + } + + if (getLineContent == null) + { + throw new ArgumentNullException(nameof(getLineContent)); + } + + if (indentSize < 0) + { + throw new ArgumentOutOfRangeException(nameof(indentSize)); + } + + if (tabSize < 0) + { + throw new ArgumentOutOfRangeException(nameof(tabSize)); + } + + var simulatedChange = new SourceChange(previousLineEndIndex, 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 children = new List(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) + { + 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; + } + } + + // We can't rely on the syntax trees representation of the source document because partial parses may have mutated + // the underlying SyntaxTree text buffer. Because of this, if we want to provide accurate indentations we need to + // operate on the current line representation as indicated by the provider. + var line = getLineContent(curSpan.Start.LineIndex); + desiredIndentation = GetIndentLevelOfLine(line, 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(string line, int tabSize) + { + var indentLevel = 0; + + foreach (var c in line) + { + if (!char.IsWhiteSpace(c)) + { + break; + } + else if (c == '\t') + { + indentLevel += tabSize; + } + else + { + indentLevel++; + } + } + + return indentLevel; + } + } +} diff --git a/src/Microsoft.CodeAnalysis.Razor.Workspaces/DefaultRazorSyntaxFactsServiceFactory.cs b/src/Microsoft.CodeAnalysis.Razor.Workspaces/DefaultRazorSyntaxFactsServiceFactory.cs new file mode 100644 index 0000000000..af38ae47e7 --- /dev/null +++ b/src/Microsoft.CodeAnalysis.Razor.Workspaces/DefaultRazorSyntaxFactsServiceFactory.cs @@ -0,0 +1,17 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using Microsoft.CodeAnalysis.Host; +using Microsoft.CodeAnalysis.Host.Mef; + +namespace Microsoft.CodeAnalysis.Razor +{ + [ExportLanguageServiceFactory(typeof(RazorSyntaxFactsService), RazorLanguage.Name, ServiceLayer.Default)] + internal class DefaultRazorSyntaxFactsServiceFactory : ILanguageServiceFactory + { + public ILanguageService CreateLanguageService(HostLanguageServices languageServices) + { + return new DefaultRazorSyntaxFactsService(); + } + } +} \ No newline at end of file diff --git a/src/Microsoft.VisualStudio.LanguageServices.Razor/DefaultTagHelperCompletionService.cs b/src/Microsoft.CodeAnalysis.Razor.Workspaces/DefaultTagHelperCompletionService.cs similarity index 98% rename from src/Microsoft.VisualStudio.LanguageServices.Razor/DefaultTagHelperCompletionService.cs rename to src/Microsoft.CodeAnalysis.Razor.Workspaces/DefaultTagHelperCompletionService.cs index de21ec6976..a59548233b 100644 --- a/src/Microsoft.VisualStudio.LanguageServices.Razor/DefaultTagHelperCompletionService.cs +++ b/src/Microsoft.CodeAnalysis.Razor.Workspaces/DefaultTagHelperCompletionService.cs @@ -3,20 +3,17 @@ 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 +namespace Microsoft.CodeAnalysis.Razor { - [Export(typeof(TagHelperCompletionService))] internal class DefaultTagHelperCompletionService : TagHelperCompletionService { private readonly TagHelperFactsService _tagHelperFactsService; private static readonly HashSet _emptyHashSet = new HashSet(); - [ImportingConstructor] public DefaultTagHelperCompletionService(TagHelperFactsService tagHelperFactsService) { _tagHelperFactsService = tagHelperFactsService; diff --git a/src/Microsoft.CodeAnalysis.Razor.Workspaces/DefaultTagHelperCompletionServiceFactory.cs b/src/Microsoft.CodeAnalysis.Razor.Workspaces/DefaultTagHelperCompletionServiceFactory.cs new file mode 100644 index 0000000000..3dd4ea4e4c --- /dev/null +++ b/src/Microsoft.CodeAnalysis.Razor.Workspaces/DefaultTagHelperCompletionServiceFactory.cs @@ -0,0 +1,18 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using Microsoft.CodeAnalysis.Host; +using Microsoft.CodeAnalysis.Host.Mef; + +namespace Microsoft.CodeAnalysis.Razor +{ + [ExportLanguageServiceFactory(typeof(TagHelperCompletionService), RazorLanguage.Name, ServiceLayer.Default)] + internal class DefaultTagHelperCompletionServiceFactory : ILanguageServiceFactory + { + public ILanguageService CreateLanguageService(HostLanguageServices languageServices) + { + var tagHelperFactsService = languageServices.GetRequiredService(); + return new DefaultTagHelperCompletionService(tagHelperFactsService); + } + } +} \ No newline at end of file diff --git a/src/Microsoft.VisualStudio.LanguageServices.Razor/DefaultTagHelperFactsService.cs b/src/Microsoft.CodeAnalysis.Razor.Workspaces/DefaultTagHelperFactsService.cs similarity index 96% rename from src/Microsoft.VisualStudio.LanguageServices.Razor/DefaultTagHelperFactsService.cs rename to src/Microsoft.CodeAnalysis.Razor.Workspaces/DefaultTagHelperFactsService.cs index 439bf92aed..5ea8295632 100644 --- a/src/Microsoft.VisualStudio.LanguageServices.Razor/DefaultTagHelperFactsService.cs +++ b/src/Microsoft.CodeAnalysis.Razor.Workspaces/DefaultTagHelperFactsService.cs @@ -3,14 +3,11 @@ using System; using System.Collections.Generic; -using System.ComponentModel.Composition; using System.Linq; using Microsoft.AspNetCore.Razor.Language; -using Microsoft.AspNetCore.Razor.Language.Legacy; -namespace Microsoft.VisualStudio.LanguageServices.Razor +namespace Microsoft.CodeAnalysis.Razor { - [Export(typeof(TagHelperFactsService))] internal class DefaultTagHelperFactsService : TagHelperFactsService { public override TagHelperBinding GetTagHelperBinding( diff --git a/src/Microsoft.CodeAnalysis.Razor.Workspaces/DefaultTagHelperFactsServiceFactory.cs b/src/Microsoft.CodeAnalysis.Razor.Workspaces/DefaultTagHelperFactsServiceFactory.cs new file mode 100644 index 0000000000..adfbeb2dc5 --- /dev/null +++ b/src/Microsoft.CodeAnalysis.Razor.Workspaces/DefaultTagHelperFactsServiceFactory.cs @@ -0,0 +1,17 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using Microsoft.CodeAnalysis.Host; +using Microsoft.CodeAnalysis.Host.Mef; + +namespace Microsoft.CodeAnalysis.Razor +{ + [ExportLanguageServiceFactory(typeof(TagHelperFactsService), RazorLanguage.Name, ServiceLayer.Default)] + internal class DefaultTagHelperFactsServiceFactory : ILanguageServiceFactory + { + public ILanguageService CreateLanguageService(HostLanguageServices languageServices) + { + return new DefaultTagHelperFactsService(); + } + } +} \ No newline at end of file diff --git a/src/Microsoft.VisualStudio.LanguageServices.Razor/ElementCompletionContext.cs b/src/Microsoft.CodeAnalysis.Razor.Workspaces/ElementCompletionContext.cs similarity index 96% rename from src/Microsoft.VisualStudio.LanguageServices.Razor/ElementCompletionContext.cs rename to src/Microsoft.CodeAnalysis.Razor.Workspaces/ElementCompletionContext.cs index f7a0e20016..c2d51e5d3e 100644 --- a/src/Microsoft.VisualStudio.LanguageServices.Razor/ElementCompletionContext.cs +++ b/src/Microsoft.CodeAnalysis.Razor.Workspaces/ElementCompletionContext.cs @@ -5,7 +5,7 @@ using System; using System.Collections.Generic; using Microsoft.AspNetCore.Razor.Language; -namespace Microsoft.VisualStudio.LanguageServices.Razor +namespace Microsoft.CodeAnalysis.Razor { public sealed class ElementCompletionContext { diff --git a/src/Microsoft.VisualStudio.LanguageServices.Razor/ElementCompletionResult.cs b/src/Microsoft.CodeAnalysis.Razor.Workspaces/ElementCompletionResult.cs similarity index 96% rename from src/Microsoft.VisualStudio.LanguageServices.Razor/ElementCompletionResult.cs rename to src/Microsoft.CodeAnalysis.Razor.Workspaces/ElementCompletionResult.cs index 4088662ffb..9bd1121380 100644 --- a/src/Microsoft.VisualStudio.LanguageServices.Razor/ElementCompletionResult.cs +++ b/src/Microsoft.CodeAnalysis.Razor.Workspaces/ElementCompletionResult.cs @@ -5,7 +5,7 @@ using System.Collections.Generic; using System.Linq; using Microsoft.AspNetCore.Razor.Language; -namespace Microsoft.VisualStudio.LanguageServices.Razor +namespace Microsoft.CodeAnalysis.Razor { public abstract class ElementCompletionResult { diff --git a/src/Microsoft.CodeAnalysis.Razor.Workspaces/Microsoft.CodeAnalysis.Razor.Workspaces.csproj b/src/Microsoft.CodeAnalysis.Razor.Workspaces/Microsoft.CodeAnalysis.Razor.Workspaces.csproj index 838f0fff2d..3cd637512e 100644 --- a/src/Microsoft.CodeAnalysis.Razor.Workspaces/Microsoft.CodeAnalysis.Razor.Workspaces.csproj +++ b/src/Microsoft.CodeAnalysis.Razor.Workspaces/Microsoft.CodeAnalysis.Razor.Workspaces.csproj @@ -12,7 +12,7 @@ - + diff --git a/src/Microsoft.VisualStudio.LanguageServices.Razor/RazorSyntaxFactsService.cs b/src/Microsoft.CodeAnalysis.Razor.Workspaces/RazorSyntaxFactsService.cs similarity index 67% rename from src/Microsoft.VisualStudio.LanguageServices.Razor/RazorSyntaxFactsService.cs rename to src/Microsoft.CodeAnalysis.Razor.Workspaces/RazorSyntaxFactsService.cs index d415ba9fa9..ded5a194b0 100644 --- a/src/Microsoft.VisualStudio.LanguageServices.Razor/RazorSyntaxFactsService.cs +++ b/src/Microsoft.CodeAnalysis.Razor.Workspaces/RazorSyntaxFactsService.cs @@ -1,18 +1,19 @@ // 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; -using Microsoft.VisualStudio.Text; +using Microsoft.CodeAnalysis.Host; -namespace Microsoft.VisualStudio.LanguageServices.Razor +namespace Microsoft.CodeAnalysis.Razor { - public abstract class RazorSyntaxFactsService + public abstract class RazorSyntaxFactsService : ILanguageService { public abstract IReadOnlyList GetClassifiedSpans(RazorSyntaxTree syntaxTree); public abstract IReadOnlyList GetTagHelperSpans(RazorSyntaxTree syntaxTree); - public abstract int? GetDesiredIndentation(RazorSyntaxTree syntaxTree, ITextSnapshot syntaxTreeSnapshot, ITextSnapshotLine line, int indentSize, int tabSize); + public abstract int? GetDesiredIndentation(RazorSyntaxTree syntaxTree, int previousLineEnd, Func lineProvider, int indentSize, int tabSize); } } diff --git a/src/Microsoft.VisualStudio.LanguageServices.Razor/RazorTemplateEngineFactoryService.cs b/src/Microsoft.CodeAnalysis.Razor.Workspaces/RazorTemplateEngineFactoryService.cs similarity index 69% rename from src/Microsoft.VisualStudio.LanguageServices.Razor/RazorTemplateEngineFactoryService.cs rename to src/Microsoft.CodeAnalysis.Razor.Workspaces/RazorTemplateEngineFactoryService.cs index 00cb256abc..7908a9f116 100644 --- a/src/Microsoft.VisualStudio.LanguageServices.Razor/RazorTemplateEngineFactoryService.cs +++ b/src/Microsoft.CodeAnalysis.Razor.Workspaces/RazorTemplateEngineFactoryService.cs @@ -3,11 +3,12 @@ using System; using Microsoft.AspNetCore.Razor.Language; +using Microsoft.CodeAnalysis.Host; -namespace Microsoft.VisualStudio.LanguageServices.Razor +namespace Microsoft.CodeAnalysis.Razor { - public abstract class RazorTemplateEngineFactoryService + public abstract class RazorTemplateEngineFactoryService : ILanguageService { public abstract RazorTemplateEngine Create(string projectPath, Action configure); } -} +} \ No newline at end of file diff --git a/src/Microsoft.VisualStudio.LanguageServices.Razor/SpanKind.cs b/src/Microsoft.CodeAnalysis.Razor.Workspaces/SpanKind.cs similarity index 83% rename from src/Microsoft.VisualStudio.LanguageServices.Razor/SpanKind.cs rename to src/Microsoft.CodeAnalysis.Razor.Workspaces/SpanKind.cs index 9febf5ecc3..e8a1239ffe 100644 --- a/src/Microsoft.VisualStudio.LanguageServices.Razor/SpanKind.cs +++ b/src/Microsoft.CodeAnalysis.Razor.Workspaces/SpanKind.cs @@ -1,7 +1,7 @@ // 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 +namespace Microsoft.CodeAnalysis.Razor { public enum SpanKind { diff --git a/src/Microsoft.VisualStudio.LanguageServices.Razor/TagHelperCompletionService.cs b/src/Microsoft.CodeAnalysis.Razor.Workspaces/TagHelperCompletionService.cs similarity index 74% rename from src/Microsoft.VisualStudio.LanguageServices.Razor/TagHelperCompletionService.cs rename to src/Microsoft.CodeAnalysis.Razor.Workspaces/TagHelperCompletionService.cs index f6412dc2c0..0e75536f40 100644 --- a/src/Microsoft.VisualStudio.LanguageServices.Razor/TagHelperCompletionService.cs +++ b/src/Microsoft.CodeAnalysis.Razor.Workspaces/TagHelperCompletionService.cs @@ -1,9 +1,11 @@ // 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 +using Microsoft.CodeAnalysis.Host; + +namespace Microsoft.CodeAnalysis.Razor { - public abstract class TagHelperCompletionService + public abstract class TagHelperCompletionService : ILanguageService { public abstract AttributeCompletionResult GetAttributeCompletions(AttributeCompletionContext completionContext); diff --git a/src/Microsoft.VisualStudio.LanguageServices.Razor/TagHelperFactsService.cs b/src/Microsoft.CodeAnalysis.Razor.Workspaces/TagHelperFactsService.cs similarity index 85% rename from src/Microsoft.VisualStudio.LanguageServices.Razor/TagHelperFactsService.cs rename to src/Microsoft.CodeAnalysis.Razor.Workspaces/TagHelperFactsService.cs index ff40ffbb29..0e569a419e 100644 --- a/src/Microsoft.VisualStudio.LanguageServices.Razor/TagHelperFactsService.cs +++ b/src/Microsoft.CodeAnalysis.Razor.Workspaces/TagHelperFactsService.cs @@ -1,13 +1,13 @@ // Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. -using Microsoft.AspNetCore.Razor.Language; -using Microsoft.AspNetCore.Razor.Language.Legacy; using System.Collections.Generic; +using Microsoft.AspNetCore.Razor.Language; +using Microsoft.CodeAnalysis.Host; -namespace Microsoft.VisualStudio.LanguageServices.Razor +namespace Microsoft.CodeAnalysis.Razor { - public abstract class TagHelperFactsService + public abstract class TagHelperFactsService : ILanguageService { public abstract TagHelperBinding GetTagHelperBinding(TagHelperDocumentContext documentContext, string tagName, IEnumerable> attributes, string parentTag); diff --git a/src/Microsoft.CodeAnalysis.Razor.Workspaces/TagHelperResolver.cs b/src/Microsoft.CodeAnalysis.Razor.Workspaces/TagHelperResolver.cs index a16f46400b..d246743f59 100644 --- a/src/Microsoft.CodeAnalysis.Razor.Workspaces/TagHelperResolver.cs +++ b/src/Microsoft.CodeAnalysis.Razor.Workspaces/TagHelperResolver.cs @@ -7,7 +7,7 @@ using Microsoft.CodeAnalysis.Host; namespace Microsoft.CodeAnalysis.Razor { - internal abstract class TagHelperResolver : ILanguageService + public abstract class TagHelperResolver : ILanguageService { public abstract TagHelperResolutionResult GetTagHelpers(Compilation compilation); diff --git a/src/Microsoft.VisualStudio.LanguageServices.Razor/TagHelperSpan.cs b/src/Microsoft.CodeAnalysis.Razor.Workspaces/TagHelperSpan.cs similarity index 87% rename from src/Microsoft.VisualStudio.LanguageServices.Razor/TagHelperSpan.cs rename to src/Microsoft.CodeAnalysis.Razor.Workspaces/TagHelperSpan.cs index 6cefb0a1c6..69654e3f06 100644 --- a/src/Microsoft.VisualStudio.LanguageServices.Razor/TagHelperSpan.cs +++ b/src/Microsoft.CodeAnalysis.Razor.Workspaces/TagHelperSpan.cs @@ -4,9 +4,8 @@ using System; using System.Collections.Generic; using Microsoft.AspNetCore.Razor.Language; -using Microsoft.AspNetCore.Razor.Language.Legacy; -namespace Microsoft.VisualStudio.LanguageServices.Razor +namespace Microsoft.CodeAnalysis.Razor { public struct TagHelperSpan { diff --git a/src/Microsoft.VisualStudio.LanguageServices.Razor/DefaultTagHelperResolver.cs b/src/Microsoft.VisualStudio.LanguageServices.Razor/DefaultTagHelperResolver.cs index 22804f9b90..e75134ad0f 100644 --- a/src/Microsoft.VisualStudio.LanguageServices.Razor/DefaultTagHelperResolver.cs +++ b/src/Microsoft.VisualStudio.LanguageServices.Razor/DefaultTagHelperResolver.cs @@ -17,14 +17,16 @@ using Newtonsoft.Json.Linq; namespace Microsoft.VisualStudio.LanguageServices.Razor { - [Export(typeof(ITagHelperResolver))] - internal class DefaultTagHelperResolver : ITagHelperResolver + internal class DefaultTagHelperResolver : TagHelperResolver { - [Import] - public VisualStudioWorkspace Workspace { get; set; } + private readonly Workspace _workspace; + private readonly IServiceProvider _services; - [Import] - public SVsServiceProvider Services { get; set; } + public DefaultTagHelperResolver(Workspace workspace, IServiceProvider services) + { + _workspace = workspace; + _services = services; + } public async Task GetTagHelpersAsync(Project project) { @@ -36,7 +38,7 @@ namespace Microsoft.VisualStudio.LanguageServices.Razor // when it's disconnected (user stops the process). // // This will change in the future to an easier to consume API but for VS RTM this is what we have. - var client = await RazorLanguageServiceClientFactory.CreateAsync(Workspace, CancellationToken.None); + var client = await RazorLanguageServiceClientFactory.CreateAsync(_workspace, CancellationToken.None); if (client != null) { using (var session = await client.CreateSessionAsync(project.Solution)) @@ -59,7 +61,7 @@ namespace Microsoft.VisualStudio.LanguageServices.Razor // The OOP host is turned off, so let's do this in process. var compilation = await project.GetCompilationAsync(CancellationToken.None).ConfigureAwait(false); - result = GetTagHelpers(compilation, designTime: true); + result = GetTagHelpers(compilation); return result; } catch (Exception exception) @@ -81,13 +83,13 @@ namespace Microsoft.VisualStudio.LanguageServices.Razor } } - private TagHelperResolutionResult GetTagHelpers(Compilation compilation, bool designTime) + public override TagHelperResolutionResult GetTagHelpers(Compilation compilation) { var descriptors = new List(); var providers = new ITagHelperDescriptorProvider[] { - new DefaultTagHelperDescriptorProvider() { DesignTime = designTime, }, + new DefaultTagHelperDescriptorProvider() { DesignTime = true, }, new ViewComponentTagHelperDescriptorProvider(), }; @@ -121,8 +123,7 @@ namespace Microsoft.VisualStudio.LanguageServices.Razor private IVsActivityLog GetActivityLog() { - var services = (IServiceProvider)Services; - return services.GetService(typeof(SVsActivityLog)) as IVsActivityLog; + return _services.GetService(typeof(SVsActivityLog)) as IVsActivityLog; } } } diff --git a/src/Microsoft.VisualStudio.LanguageServices.Razor/DefaultTagHelperResolverFactory.cs b/src/Microsoft.VisualStudio.LanguageServices.Razor/DefaultTagHelperResolverFactory.cs new file mode 100644 index 0000000000..126148e4f4 --- /dev/null +++ b/src/Microsoft.VisualStudio.LanguageServices.Razor/DefaultTagHelperResolverFactory.cs @@ -0,0 +1,26 @@ +// 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.Host; +using Microsoft.CodeAnalysis.Host.Mef; +using Microsoft.CodeAnalysis.Razor; +using Microsoft.VisualStudio.Shell; + +namespace Microsoft.VisualStudio.LanguageServices.Razor +{ + [ExportLanguageServiceFactory(typeof(TagHelperResolver), RazorLanguage.Name, ServiceLayer.Default)] + internal class DefaultTagHelperResolverFactory : ILanguageServiceFactory + { + [Import] + public VisualStudioWorkspace Workspace { get; set; } + + [Import] + public SVsServiceProvider Services { get; set; } + + public ILanguageService CreateLanguageService(HostLanguageServices languageServices) + { + return new DefaultTagHelperResolver(Workspace, Services); + } + } +} \ No newline at end of file diff --git a/src/Microsoft.VisualStudio.LanguageServices.Razor/DefaultRazorTemplateEngineFactoryService.cs b/src/Microsoft.VisualStudio.LanguageServices.Razor/DefaultTemplateEngineFactoryService.cs similarity index 80% rename from src/Microsoft.VisualStudio.LanguageServices.Razor/DefaultRazorTemplateEngineFactoryService.cs rename to src/Microsoft.VisualStudio.LanguageServices.Razor/DefaultTemplateEngineFactoryService.cs index 218eff0565..8b37cffc1e 100644 --- a/src/Microsoft.VisualStudio.LanguageServices.Razor/DefaultRazorTemplateEngineFactoryService.cs +++ b/src/Microsoft.VisualStudio.LanguageServices.Razor/DefaultTemplateEngineFactoryService.cs @@ -2,14 +2,12 @@ // 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.Mvc.Razor.Extensions; using Microsoft.AspNetCore.Razor.Language; -namespace Microsoft.VisualStudio.LanguageServices.Razor +namespace Microsoft.CodeAnalysis.Razor { - [Export(typeof(RazorTemplateEngineFactoryService))] - internal class DefaultRazorTemplateEngineFactoryService : RazorTemplateEngineFactoryService + internal class DefaultTemplateEngineFactoryService : RazorTemplateEngineFactoryService { public override RazorTemplateEngine Create(string projectPath, Action configure) { diff --git a/src/Microsoft.VisualStudio.LanguageServices.Razor/DefaultTemplateEngineFactoryServiceFactory.cs b/src/Microsoft.VisualStudio.LanguageServices.Razor/DefaultTemplateEngineFactoryServiceFactory.cs new file mode 100644 index 0000000000..21522b72ec --- /dev/null +++ b/src/Microsoft.VisualStudio.LanguageServices.Razor/DefaultTemplateEngineFactoryServiceFactory.cs @@ -0,0 +1,18 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using Microsoft.CodeAnalysis.Host; +using Microsoft.CodeAnalysis.Host.Mef; +using Microsoft.CodeAnalysis.Razor; + +namespace Microsoft.VisualStudio.LanguageServices.Razor +{ + [ExportLanguageServiceFactory(typeof(CodeAnalysis.Razor.RazorTemplateEngineFactoryService), RazorLanguage.Name, ServiceLayer.Default)] + internal class DefaultTemplateEngineFactoryServiceFactory : ILanguageServiceFactory + { + public ILanguageService CreateLanguageService(HostLanguageServices languageServices) + { + return new DefaultTemplateEngineFactoryService(); + } + } +} \ No newline at end of file diff --git a/src/Microsoft.VisualStudio.LanguageServices.Razor/Legacy/AcceptedCharacters.cs b/src/Microsoft.VisualStudio.LanguageServices.Razor/Legacy/AcceptedCharacters.cs new file mode 100644 index 0000000000..034dda9298 --- /dev/null +++ b/src/Microsoft.VisualStudio.LanguageServices.Razor/Legacy/AcceptedCharacters.cs @@ -0,0 +1,28 @@ +// 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 + } +} diff --git a/src/Microsoft.VisualStudio.LanguageServices.Razor/Legacy/AttributeCompletionContext.cs b/src/Microsoft.VisualStudio.LanguageServices.Razor/Legacy/AttributeCompletionContext.cs new file mode 100644 index 0000000000..f859c1d5c1 --- /dev/null +++ b/src/Microsoft.VisualStudio.LanguageServices.Razor/Legacy/AttributeCompletionContext.cs @@ -0,0 +1,69 @@ +// 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 existingCompletions, + string currentTagName, + IEnumerable> attributes, + string currentParentTagName, + Func 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 ExistingCompletions { get; } + + public string CurrentTagName { get; } + + public IEnumerable> Attributes { get; } + + public string CurrentParentTagName { get; } + + public Func InHTMLSchema { get; } + } +} diff --git a/src/Microsoft.VisualStudio.LanguageServices.Razor/Legacy/AttributeCompletionResult.cs b/src/Microsoft.VisualStudio.LanguageServices.Razor/Legacy/AttributeCompletionResult.cs new file mode 100644 index 0000000000..066d438054 --- /dev/null +++ b/src/Microsoft.VisualStudio.LanguageServices.Razor/Legacy/AttributeCompletionResult.cs @@ -0,0 +1,45 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.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> Completions { get; } + + internal static AttributeCompletionResult Create(Dictionary> completions) + { + var readonlyCompletions = completions.ToDictionary( + key => key.Key, + value => (IEnumerable)value.Value, + completions.Comparer); + var result = new DefaultAttributeCompletionResult(readonlyCompletions); + + return result; + } + + private class DefaultAttributeCompletionResult : AttributeCompletionResult + { + private readonly IReadOnlyDictionary> _completions; + + public DefaultAttributeCompletionResult(IReadOnlyDictionary> completions) + { + _completions = completions; + } + + public override IReadOnlyDictionary> Completions => _completions; + } + } +} diff --git a/src/Microsoft.VisualStudio.LanguageServices.Razor/Legacy/BlockKind.cs b/src/Microsoft.VisualStudio.LanguageServices.Razor/Legacy/BlockKind.cs new file mode 100644 index 0000000000..5591b31943 --- /dev/null +++ b/src/Microsoft.VisualStudio.LanguageServices.Razor/Legacy/BlockKind.cs @@ -0,0 +1,28 @@ +// 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 + } +} diff --git a/src/Microsoft.VisualStudio.LanguageServices.Razor/Legacy/ClassifiedSpan.cs b/src/Microsoft.VisualStudio.LanguageServices.Razor/Legacy/ClassifiedSpan.cs new file mode 100644 index 0000000000..a254d862dc --- /dev/null +++ b/src/Microsoft.VisualStudio.LanguageServices.Razor/Legacy/ClassifiedSpan.cs @@ -0,0 +1,33 @@ +// 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; } + } +} diff --git a/src/Microsoft.VisualStudio.LanguageServices.Razor/DefaultRazorSyntaxFactsService.cs b/src/Microsoft.VisualStudio.LanguageServices.Razor/Legacy/DefaultRazorSyntaxFactsService.cs similarity index 97% rename from src/Microsoft.VisualStudio.LanguageServices.Razor/DefaultRazorSyntaxFactsService.cs rename to src/Microsoft.VisualStudio.LanguageServices.Razor/Legacy/DefaultRazorSyntaxFactsService.cs index ddd0ab485f..944942048b 100644 --- a/src/Microsoft.VisualStudio.LanguageServices.Razor/DefaultRazorSyntaxFactsService.cs +++ b/src/Microsoft.VisualStudio.LanguageServices.Razor/Legacy/DefaultRazorSyntaxFactsService.cs @@ -9,10 +9,13 @@ using Microsoft.AspNetCore.Razor.Language; using Microsoft.AspNetCore.Razor.Language.Legacy; using Microsoft.VisualStudio.Text; using Span = Microsoft.AspNetCore.Razor.Language.Legacy.Span; -using ITextBuffer = Microsoft.AspNetCore.Razor.Language.Legacy.ITextBuffer; 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 { @@ -326,7 +329,7 @@ namespace Microsoft.VisualStudio.LanguageServices.Razor private int GetIndentLevelOfLine(ITextSnapshotLine line, int tabSize) { var indentLevel = 0; - + foreach (var c in line.GetText()) { if (!char.IsWhiteSpace(c)) diff --git a/src/Microsoft.VisualStudio.LanguageServices.Razor/Legacy/DefaultTagHelperCompletionService.cs b/src/Microsoft.VisualStudio.LanguageServices.Razor/Legacy/DefaultTagHelperCompletionService.cs new file mode 100644 index 0000000000..15b0aea6aa --- /dev/null +++ b/src/Microsoft.VisualStudio.LanguageServices.Razor/Legacy/DefaultTagHelperCompletionService.cs @@ -0,0 +1,280 @@ +// 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 _emptyHashSet = new HashSet(); + + [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(), + 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(); + 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(); + 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>(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(), + StringComparer.OrdinalIgnoreCase); + + var catchAllDescriptors = new HashSet(); + 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: | + // 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(); + elementCompletions[tagName] = existingRuleDescriptors; + } + + existingRuleDescriptors.Add(possibleDescriptor); + } + } + + private void AddAllowedChildrenCompletions( + ElementCompletionContext completionContext, + Dictionary> 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(); + elementCompletions[prefixedName] = existingRuleDescriptors; + } + + existingRuleDescriptors.UnionWith(descriptors); + } + } + } + } +} diff --git a/src/Microsoft.VisualStudio.LanguageServices.Razor/Legacy/DefaultTagHelperFactsService.cs b/src/Microsoft.VisualStudio.LanguageServices.Razor/Legacy/DefaultTagHelperFactsService.cs new file mode 100644 index 0000000000..8fa8a7a9b4 --- /dev/null +++ b/src/Microsoft.VisualStudio.LanguageServices.Razor/Legacy/DefaultTagHelperFactsService.cs @@ -0,0 +1,168 @@ +// 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> 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); + + return binding; + } + + public override IEnumerable 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(); + 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 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(); + 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 GetTagHelpersGivenParent(TagHelperDocumentContext documentContext, string parentTag) + { + if (documentContext == null) + { + throw new ArgumentNullException(nameof(documentContext)); + } + + var matchingDescriptors = new List(); + 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; + } + } +} diff --git a/src/Microsoft.VisualStudio.LanguageServices.Razor/Legacy/ElementCompletionContext.cs b/src/Microsoft.VisualStudio.LanguageServices.Razor/Legacy/ElementCompletionContext.cs new file mode 100644 index 0000000000..c38c8485c7 --- /dev/null +++ b/src/Microsoft.VisualStudio.LanguageServices.Razor/Legacy/ElementCompletionContext.cs @@ -0,0 +1,59 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.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 existingCompletions, + string containingTagName, + IEnumerable> attributes, + string containingParentTagName, + Func 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 ExistingCompletions { get; } + + public string ContainingTagName { get; } + + public IEnumerable> Attributes { get; } + + public string ContainingParentTagName { get; } + + public Func InHTMLSchema { get; } + } +} diff --git a/src/Microsoft.VisualStudio.LanguageServices.Razor/Legacy/ElementCompletionResult.cs b/src/Microsoft.VisualStudio.LanguageServices.Razor/Legacy/ElementCompletionResult.cs new file mode 100644 index 0000000000..88dc0c6a56 --- /dev/null +++ b/src/Microsoft.VisualStudio.LanguageServices.Razor/Legacy/ElementCompletionResult.cs @@ -0,0 +1,45 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.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> Completions { get; } + + internal static ElementCompletionResult Create(Dictionary> completions) + { + var readonlyCompletions = completions.ToDictionary( + key => key.Key, + value => (IEnumerable)value.Value, + completions.Comparer); + var result = new DefaultElementCompletionResult(readonlyCompletions); + + return result; + } + + private class DefaultElementCompletionResult : ElementCompletionResult + { + private readonly IReadOnlyDictionary> _completions; + + public DefaultElementCompletionResult(IReadOnlyDictionary> completions) + { + _completions = completions; + } + + public override IReadOnlyDictionary> Completions => _completions; + } + } +} diff --git a/src/Microsoft.VisualStudio.LanguageServices.Razor/ITagHelperResolver.cs b/src/Microsoft.VisualStudio.LanguageServices.Razor/Legacy/ITagHelperResolver.cs similarity index 52% rename from src/Microsoft.VisualStudio.LanguageServices.Razor/ITagHelperResolver.cs rename to src/Microsoft.VisualStudio.LanguageServices.Razor/Legacy/ITagHelperResolver.cs index 5a0446c89d..80c2502714 100644 --- a/src/Microsoft.VisualStudio.LanguageServices.Razor/ITagHelperResolver.cs +++ b/src/Microsoft.VisualStudio.LanguageServices.Razor/Legacy/ITagHelperResolver.cs @@ -1,13 +1,16 @@ // Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. -using System.Collections.Generic; using System.Threading.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 GetTagHelpersAsync(Project project); diff --git a/src/Microsoft.VisualStudio.LanguageServices.Razor/Legacy/LegacyTagHelperResolver.cs b/src/Microsoft.VisualStudio.LanguageServices.Razor/Legacy/LegacyTagHelperResolver.cs new file mode 100644 index 0000000000..7a073d48af --- /dev/null +++ b/src/Microsoft.VisualStudio.LanguageServices.Razor/Legacy/LegacyTagHelperResolver.cs @@ -0,0 +1,26 @@ +// 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.CodeAnalysis; +using Microsoft.VisualStudio.Shell; + +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, + [Import(typeof(SVsServiceProvider))] IServiceProvider services) : + base(workspace, services) + { + } + } +} diff --git a/src/Microsoft.VisualStudio.LanguageServices.Razor/Legacy/LegacyTemplateEngineFactoryService.cs b/src/Microsoft.VisualStudio.LanguageServices.Razor/Legacy/LegacyTemplateEngineFactoryService.cs new file mode 100644 index 0000000000..4747493a2f --- /dev/null +++ b/src/Microsoft.VisualStudio.LanguageServices.Razor/Legacy/LegacyTemplateEngineFactoryService.cs @@ -0,0 +1,38 @@ +// 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.Mvc.Razor.Extensions; +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 DefaultTemplateEngineFactoryService. + // ---------------------------------------------------------------------------------------------------- + [Export(typeof(RazorTemplateEngineFactoryService))] + internal class LegacyTemplateEngineFactoryService : RazorTemplateEngineFactoryService + { + public override RazorTemplateEngine Create(string projectPath, Action configure) + { + if (projectPath == null) + { + throw new ArgumentNullException(nameof(projectPath)); + } + + var engine = RazorEngine.CreateDesignTime(b => + { + configure?.Invoke(b); + + // For now we're hardcoded to use MVC's extensibility. + RazorExtensions.Register(b); + }); + + var templateEngine = new MvcRazorTemplateEngine(engine, RazorProject.Create(projectPath)); + templateEngine.Options.ImportsFileName = "_ViewImports.cshtml"; + return templateEngine; + } + } +} diff --git a/src/Microsoft.VisualStudio.LanguageServices.Razor/Legacy/RazorSyntaxFactsService.cs b/src/Microsoft.VisualStudio.LanguageServices.Razor/Legacy/RazorSyntaxFactsService.cs new file mode 100644 index 0000000000..6b187680e9 --- /dev/null +++ b/src/Microsoft.VisualStudio.LanguageServices.Razor/Legacy/RazorSyntaxFactsService.cs @@ -0,0 +1,22 @@ +// 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 GetClassifiedSpans(RazorSyntaxTree syntaxTree); + + public abstract IReadOnlyList GetTagHelperSpans(RazorSyntaxTree syntaxTree); + + public abstract int? GetDesiredIndentation(RazorSyntaxTree syntaxTree, ITextSnapshot syntaxTreeSnapshot, ITextSnapshotLine line, int indentSize, int tabSize); + } +} diff --git a/src/Microsoft.VisualStudio.LanguageServices.Razor/Legacy/RazorTemplateEngineFactoryService.cs b/src/Microsoft.VisualStudio.LanguageServices.Razor/Legacy/RazorTemplateEngineFactoryService.cs new file mode 100644 index 0000000000..784437df22 --- /dev/null +++ b/src/Microsoft.VisualStudio.LanguageServices.Razor/Legacy/RazorTemplateEngineFactoryService.cs @@ -0,0 +1,17 @@ +// 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 configure); + } +} diff --git a/src/Microsoft.VisualStudio.LanguageServices.Razor/Legacy/SpanKind.cs b/src/Microsoft.VisualStudio.LanguageServices.Razor/Legacy/SpanKind.cs new file mode 100644 index 0000000000..d557849f6d --- /dev/null +++ b/src/Microsoft.VisualStudio.LanguageServices.Razor/Legacy/SpanKind.cs @@ -0,0 +1,18 @@ +// 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 + } +} diff --git a/src/Microsoft.VisualStudio.LanguageServices.Razor/Legacy/TagHelperCompletionService.cs b/src/Microsoft.VisualStudio.LanguageServices.Razor/Legacy/TagHelperCompletionService.cs new file mode 100644 index 0000000000..78f3e4b821 --- /dev/null +++ b/src/Microsoft.VisualStudio.LanguageServices.Razor/Legacy/TagHelperCompletionService.cs @@ -0,0 +1,16 @@ +// 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); + } +} diff --git a/src/Microsoft.VisualStudio.LanguageServices.Razor/Legacy/TagHelperFactsService.cs b/src/Microsoft.VisualStudio.LanguageServices.Razor/Legacy/TagHelperFactsService.cs new file mode 100644 index 0000000000..5fca65c218 --- /dev/null +++ b/src/Microsoft.VisualStudio.LanguageServices.Razor/Legacy/TagHelperFactsService.cs @@ -0,0 +1,23 @@ +// 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> attributes, string parentTag); + + public abstract IEnumerable GetBoundTagHelperAttributes(TagHelperDocumentContext documentContext, string attributeName, TagHelperBinding binding); + + public abstract IReadOnlyList GetTagHelpersGivenTag(TagHelperDocumentContext documentContext, string tagName, string parentTag); + + public abstract IReadOnlyList GetTagHelpersGivenParent(TagHelperDocumentContext documentContext, string parentTag); + } +} diff --git a/src/Microsoft.VisualStudio.LanguageServices.Razor/Legacy/TagHelperSpan.cs b/src/Microsoft.VisualStudio.LanguageServices.Razor/Legacy/TagHelperSpan.cs new file mode 100644 index 0000000000..337234f3d0 --- /dev/null +++ b/src/Microsoft.VisualStudio.LanguageServices.Razor/Legacy/TagHelperSpan.cs @@ -0,0 +1,33 @@ +// 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 TagHelpers => Binding.Descriptors; + + public SourceSpan Span { get; } + } +} diff --git a/src/Microsoft.VisualStudio.LanguageServices.Razor/RazorSyntaxFactsServiceExtensions.cs b/src/Microsoft.VisualStudio.LanguageServices.Razor/RazorSyntaxFactsServiceExtensions.cs new file mode 100644 index 0000000000..bf180e8293 --- /dev/null +++ b/src/Microsoft.VisualStudio.LanguageServices.Razor/RazorSyntaxFactsServiceExtensions.cs @@ -0,0 +1,30 @@ +// 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.CodeAnalysis.Razor +{ + public static class RazorSyntaxFactsServiceExtensions + { + public static int? GetDesiredIndentation( + this RazorSyntaxFactsService service, + RazorSyntaxTree syntaxTree, + ITextSnapshot syntaxTreeSnapshot, + ITextSnapshotLine line, + int indentSize, + int 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); + + Func getLineContentDelegate = (lineIndex) => line.Snapshot.GetLineFromLineNumber(lineIndex).GetText(); + + return service.GetDesiredIndentation(syntaxTree, previousLineEnd, getLineContentDelegate, indentSize, tabSize); + } + } +} \ No newline at end of file diff --git a/test/Microsoft.CodeAnalysis.Razor.Workspaces.Test/DefaultTagHelperCompletionServiceTest.cs b/test/Microsoft.CodeAnalysis.Razor.Workspaces.Test/DefaultTagHelperCompletionServiceTest.cs new file mode 100644 index 0000000000..3961d46743 --- /dev/null +++ b/test/Microsoft.CodeAnalysis.Razor.Workspaces.Test/DefaultTagHelperCompletionServiceTest.cs @@ -0,0 +1,964 @@ +// 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.CodeAnalysis.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>() + { + ["onclick"] = new HashSet(), + ["visible"] = new HashSet() + { + documentDescriptors[0].BoundAttributes.Last() + } + }); + + var existingCompletions = new[] { "onclick" }; + var completionContext = BuildAttributeCompletionContext( + documentDescriptors, + existingCompletions, + attributes: new Dictionary() + { + ["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>() + { + ["class"] = new HashSet(), + ["onclick"] = new HashSet(), + ["repeat"] = new HashSet() + }); + + 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>() + { + ["class"] = new HashSet(documentDescriptors[1].BoundAttributes), + ["onclick"] = new HashSet(), + ["repeat"] = new HashSet() + { + 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>() + { + ["onclick"] = new HashSet(), + ["class"] = new HashSet(documentDescriptors[1].BoundAttributes), + ["repeat"] = new HashSet() + { + documentDescriptors[0].BoundAttributes.First() + }, + ["visible"] = new HashSet() + { + 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>() + { + ["class"] = new HashSet(), + ["repeat"] = new HashSet(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>() + { + ["repeat"] = new HashSet(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>() + { + ["class"] = new HashSet(), + ["repeat"] = new HashSet(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>() + { + ["class"] = new HashSet(), + }); + + var existingCompletions = new[] { "class" }; + var completionContext = BuildAttributeCompletionContext( + Enumerable.Empty(), + 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>() + { + ["class"] = new HashSet(), + }); + + 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>() + { + ["class"] = new HashSet(), + }); + + 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>() + { + ["my-table"] = new HashSet { documentDescriptors[0] }, + ["table"] = new HashSet(), + }); + + 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>() + { + ["th:li"] = new HashSet { documentDescriptors[1], documentDescriptors[0] }, + ["li"] = new HashSet(), + }); + + 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>() + { + ["th:superli"] = new HashSet { documentDescriptors[0] }, + ["th:li"] = new HashSet { documentDescriptors[1] }, + ["li"] = new HashSet(), + }); + + 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>() + { + ["superli"] = new HashSet() { documentDescriptors[0], documentDescriptors[1] }, + ["li"] = new HashSet(), + }); + + 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>() + { + ["th:superli"] = new HashSet() { documentDescriptors[0], documentDescriptors[1] }, + ["th:li"] = new HashSet() { 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>() + { + ["strong"] = new HashSet { documentDescriptors[0], documentDescriptors[1] }, + ["b"] = new HashSet { documentDescriptors[0] }, + ["bold"] = new HashSet { 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>() + { + ["li"] = new HashSet { 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>() + { + ["li"] = new HashSet { documentDescriptors[1] }, + ["superli"] = new HashSet { 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>() + { + ["div"] = new HashSet { documentDescriptors[0] }, + ["li"] = new HashSet { 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>() + { + ["li"] = new HashSet { 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>()); + + var existingCompletions = Enumerable.Empty(); + 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>() + { + ["b"] = new HashSet(), + ["bold"] = new HashSet(), + ["div"] = new HashSet { 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>() + { + ["b"] = new HashSet(), + ["bold"] = new HashSet(), + }); + + var completionContext = BuildElementCompletionContext(documentDescriptors, Enumerable.Empty(), 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>() + { + ["b"] = new HashSet(), + ["bold"] = new HashSet(), + ["div"] = new HashSet { documentDescriptors[0] } + }); + + var completionContext = BuildElementCompletionContext(documentDescriptors, Enumerable.Empty(), 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>() + { + ["strong"] = new HashSet { documentDescriptors[0] }, + ["b"] = new HashSet { documentDescriptors[0] }, + ["bold"] = new HashSet { documentDescriptors[0] }, + ["div"] = new HashSet { documentDescriptors[0], documentDescriptors[1] }, + }); + + var completionContext = BuildElementCompletionContext(documentDescriptors, Enumerable.Empty(), 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 descriptors, + IEnumerable existingCompletions, + string containingTagName, + string containingParentTagName = "body", + string tagHelperPrefix = "") + { + var documentContext = TagHelperDocumentContext.Create(tagHelperPrefix, descriptors); + var completionContext = new ElementCompletionContext( + documentContext, + existingCompletions, + containingTagName, + attributes: Enumerable.Empty>(), + containingParentTagName: containingParentTagName, + inHTMLSchema: (tag) => tag == "strong" || tag == "b" || tag == "bold" || tag == "li" || tag == "div"); + + return completionContext; + } + + private static AttributeCompletionContext BuildAttributeCompletionContext( + IEnumerable descriptors, + IEnumerable existingCompletions, + string currentTagName, + IEnumerable> attributes = null, + string tagHelperPrefix = "") + { + attributes = attributes ?? Enumerable.Empty>(); + 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; + } + } +} diff --git a/test/Microsoft.CodeAnalysis.Razor.Workspaces.Test/DefaultTagHelperFactsServiceTest.cs b/test/Microsoft.CodeAnalysis.Razor.Workspaces.Test/DefaultTagHelperFactsServiceTest.cs new file mode 100644 index 0000000000..712b815762 --- /dev/null +++ b/test/Microsoft.CodeAnalysis.Razor.Workspaces.Test/DefaultTagHelperFactsServiceTest.cs @@ -0,0 +1,388 @@ +// 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.CodeAnalysis.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>(), 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).Namespace + "IDictionary") + .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("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).Namespace + "IDictionary") + .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>(), 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>(), 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); + } + } +} diff --git a/test/Microsoft.CodeAnalysis.Razor.Workspaces.Test/Microsoft.CodeAnalysis.Razor.Workspaces.Test.csproj b/test/Microsoft.CodeAnalysis.Razor.Workspaces.Test/Microsoft.CodeAnalysis.Razor.Workspaces.Test.csproj new file mode 100644 index 0000000000..1616d8d02f --- /dev/null +++ b/test/Microsoft.CodeAnalysis.Razor.Workspaces.Test/Microsoft.CodeAnalysis.Razor.Workspaces.Test.csproj @@ -0,0 +1,28 @@ + + + + + + net461;netcoreapp2.0 + netcoreapp2.0 + true + + + + + + + + + + + + + + + + + + + + diff --git a/test/Microsoft.CodeAnalysis.Razor.Workspaces.Test/Test/TestBoundAttributeDescriptorBuilderExtensions.cs b/test/Microsoft.CodeAnalysis.Razor.Workspaces.Test/Test/TestBoundAttributeDescriptorBuilderExtensions.cs new file mode 100644 index 0000000000..5fa2ac8a2c --- /dev/null +++ b/test/Microsoft.CodeAnalysis.Razor.Workspaces.Test/Test/TestBoundAttributeDescriptorBuilderExtensions.cs @@ -0,0 +1,124 @@ +// 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.CodeAnalysis.Razor.Workspaces +{ + public static class TestBoundAttributeDescriptorBuilderExtensions + { + public static BoundAttributeDescriptorBuilder Name(this BoundAttributeDescriptorBuilder builder, string name) + { + if (builder == null) + { + throw new ArgumentNullException(nameof(builder)); + } + + builder.Name = name; + + return builder; + } + + public static BoundAttributeDescriptorBuilder TypeName(this BoundAttributeDescriptorBuilder builder, string typeName) + { + if (builder == null) + { + throw new ArgumentNullException(nameof(builder)); + } + + builder.TypeName = typeName; + + return builder; + } + + public static BoundAttributeDescriptorBuilder PropertyName(this BoundAttributeDescriptorBuilder builder, string propertyName) + { + if (builder == null) + { + throw new ArgumentNullException(nameof(builder)); + } + + builder.SetPropertyName(propertyName); + + return builder; + } + + public static BoundAttributeDescriptorBuilder DisplayName(this BoundAttributeDescriptorBuilder builder, string displayName) + { + if (builder == null) + { + throw new ArgumentNullException(nameof(builder)); + } + + builder.DisplayName = displayName; + + return builder; + } + + public static BoundAttributeDescriptorBuilder AsEnum(this BoundAttributeDescriptorBuilder builder) + { + if (builder == null) + { + throw new ArgumentNullException(nameof(builder)); + } + + builder.IsEnum = true; + + return builder; + } + + public static BoundAttributeDescriptorBuilder AsDictionaryAttribute( + this BoundAttributeDescriptorBuilder builder, + string attributeNamePrefix, + string valueTypeName) + { + if (builder == null) + { + throw new ArgumentNullException(nameof(builder)); + } + + builder.IsDictionary = true; + builder.IndexerAttributeNamePrefix = attributeNamePrefix; + builder.IndexerValueTypeName = valueTypeName; + + return builder; + } + + public static BoundAttributeDescriptorBuilder Documentation(this BoundAttributeDescriptorBuilder builder, string documentation) + { + if (builder == null) + { + throw new ArgumentNullException(nameof(builder)); + } + + builder.Documentation = documentation; + + return builder; + } + + public static BoundAttributeDescriptorBuilder AddMetadata(this BoundAttributeDescriptorBuilder builder, string key, string value) + { + if (builder == null) + { + throw new ArgumentNullException(nameof(builder)); + } + + builder.Metadata[key] = value; + + return builder; + } + + public static BoundAttributeDescriptorBuilder AddDiagnostic(this BoundAttributeDescriptorBuilder builder, RazorDiagnostic diagnostic) + { + if (builder == null) + { + throw new ArgumentNullException(nameof(builder)); + } + + builder.Diagnostics.Add(diagnostic); + + return builder; + } + } +} diff --git a/test/Microsoft.CodeAnalysis.Razor.Workspaces.Test/Test/TestRequiredAttributeDescriptorBuilderExtensions.cs b/test/Microsoft.CodeAnalysis.Razor.Workspaces.Test/Test/TestRequiredAttributeDescriptorBuilderExtensions.cs new file mode 100644 index 0000000000..0f5eeb0bf8 --- /dev/null +++ b/test/Microsoft.CodeAnalysis.Razor.Workspaces.Test/Test/TestRequiredAttributeDescriptorBuilderExtensions.cs @@ -0,0 +1,70 @@ +// 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.CodeAnalysis.Razor.Workspaces +{ + public static class TestRequiredAttributeDescriptorBuilderExtensions + { + public static RequiredAttributeDescriptorBuilder Name(this RequiredAttributeDescriptorBuilder builder, string name) + { + if (builder == null) + { + throw new ArgumentNullException(nameof(builder)); + } + + builder.Name = name; + + return builder; + } + + public static RequiredAttributeDescriptorBuilder NameComparisonMode( + this RequiredAttributeDescriptorBuilder builder, + RequiredAttributeDescriptor.NameComparisonMode nameComparison) + { + if (builder == null) + { + throw new ArgumentNullException(nameof(builder)); + } + + builder.NameComparisonMode = nameComparison; + + return builder; + } + + public static RequiredAttributeDescriptorBuilder Value(this RequiredAttributeDescriptorBuilder builder, string value) + { + if (builder == null) + { + throw new ArgumentNullException(nameof(builder)); + } + + builder.Value = value; + + return builder; + } + + public static RequiredAttributeDescriptorBuilder ValueComparisonMode( + this RequiredAttributeDescriptorBuilder builder, + RequiredAttributeDescriptor.ValueComparisonMode valueComparison) + { + if (builder == null) + { + throw new ArgumentNullException(nameof(builder)); + } + + builder.ValueComparisonMode = valueComparison; + + return builder; + } + + public static RequiredAttributeDescriptorBuilder AddDiagnostic(this RequiredAttributeDescriptorBuilder builder, RazorDiagnostic diagnostic) + { + builder.Diagnostics.Add(diagnostic); + + return builder; + } + } +} diff --git a/test/Microsoft.CodeAnalysis.Razor.Workspaces.Test/Test/TestTagHelperDescriptorBuilderExtensions.cs b/test/Microsoft.CodeAnalysis.Razor.Workspaces.Test/Test/TestTagHelperDescriptorBuilderExtensions.cs new file mode 100644 index 0000000000..67272261ba --- /dev/null +++ b/test/Microsoft.CodeAnalysis.Razor.Workspaces.Test/Test/TestTagHelperDescriptorBuilderExtensions.cs @@ -0,0 +1,123 @@ +// 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.CodeAnalysis.Razor.Workspaces +{ + public static class TestTagHelperDescriptorBuilderExtensions + { + public static TagHelperDescriptorBuilder TypeName(this TagHelperDescriptorBuilder builder, string typeName) + { + if (builder == null) + { + throw new ArgumentNullException(nameof(builder)); + } + + builder.SetTypeName(typeName); + + return builder; + } + + public static TagHelperDescriptorBuilder DisplayName(this TagHelperDescriptorBuilder builder, string displayName) + { + if (builder == null) + { + throw new ArgumentNullException(nameof(builder)); + } + + builder.DisplayName = displayName; + + return builder; + } + + public static TagHelperDescriptorBuilder AllowChildTag(this TagHelperDescriptorBuilder builder, string allowedChild) + { + if (builder == null) + { + throw new ArgumentNullException(nameof(builder)); + } + + builder.AllowChildTag(childTagBuilder => childTagBuilder.Name = allowedChild); + + return builder; + } + + public static TagHelperDescriptorBuilder TagOutputHint(this TagHelperDescriptorBuilder builder, string hint) + { + if (builder == null) + { + throw new ArgumentNullException(nameof(builder)); + } + + builder.TagOutputHint = hint; + + return builder; + } + + public static TagHelperDescriptorBuilder Documentation(this TagHelperDescriptorBuilder builder, string documentation) + { + if (builder == null) + { + throw new ArgumentNullException(nameof(builder)); + } + + builder.Documentation = documentation; + + return builder; + } + + public static TagHelperDescriptorBuilder AddMetadata(this TagHelperDescriptorBuilder builder, string key, string value) + { + if (builder == null) + { + throw new ArgumentNullException(nameof(builder)); + } + + builder.Metadata[key] = value; + + return builder; + } + + public static TagHelperDescriptorBuilder AddDiagnostic(this TagHelperDescriptorBuilder builder, RazorDiagnostic diagnostic) + { + if (builder == null) + { + throw new ArgumentNullException(nameof(builder)); + } + + builder.Diagnostics.Add(diagnostic); + + return builder; + } + + public static TagHelperDescriptorBuilder BoundAttributeDescriptor( + this TagHelperDescriptorBuilder builder, + Action configure) + { + if (builder == null) + { + throw new ArgumentNullException(nameof(builder)); + } + + builder.BindAttribute(configure); + + return builder; + } + + public static TagHelperDescriptorBuilder TagMatchingRuleDescriptor( + this TagHelperDescriptorBuilder builder, + Action configure) + { + if (builder == null) + { + throw new ArgumentNullException(nameof(builder)); + } + + builder.TagMatchingRule(configure); + + return builder; + } + } +} diff --git a/test/Microsoft.CodeAnalysis.Razor.Workspaces.Test/Test/TestTagMatchingRuleDescriptorBuilderExtensions.cs b/test/Microsoft.CodeAnalysis.Razor.Workspaces.Test/Test/TestTagMatchingRuleDescriptorBuilderExtensions.cs new file mode 100644 index 0000000000..b3950895d3 --- /dev/null +++ b/test/Microsoft.CodeAnalysis.Razor.Workspaces.Test/Test/TestTagMatchingRuleDescriptorBuilderExtensions.cs @@ -0,0 +1,73 @@ +// 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.CodeAnalysis.Razor.Workspaces +{ + public static class TestTagMatchingRuleDescriptorBuilderExtensions + { + public static TagMatchingRuleDescriptorBuilder RequireTagName(this TagMatchingRuleDescriptorBuilder builder, string tagName) + { + if (builder == null) + { + throw new ArgumentNullException(nameof(builder)); + } + + builder.TagName = tagName; + + return builder; + } + + public static TagMatchingRuleDescriptorBuilder RequireParentTag(this TagMatchingRuleDescriptorBuilder builder, string parentTag) + { + if (builder == null) + { + throw new ArgumentNullException(nameof(builder)); + } + + builder.ParentTag = parentTag; + + return builder; + } + + public static TagMatchingRuleDescriptorBuilder RequireTagStructure(this TagMatchingRuleDescriptorBuilder builder, TagStructure tagStructure) + { + if (builder == null) + { + throw new ArgumentNullException(nameof(builder)); + } + + builder.TagStructure = tagStructure; + + return builder; + } + + public static TagMatchingRuleDescriptorBuilder AddDiagnostic(this TagMatchingRuleDescriptorBuilder builder, RazorDiagnostic diagnostic) + { + if (builder == null) + { + throw new ArgumentNullException(nameof(builder)); + } + + builder.Diagnostics.Add(diagnostic); + + return builder; + } + + public static TagMatchingRuleDescriptorBuilder RequireAttributeDescriptor( + this TagMatchingRuleDescriptorBuilder builder, + Action configure) + { + if (builder == null) + { + throw new ArgumentNullException(nameof(builder)); + } + + builder.Attribute(configure); + + return builder; + } + } +} diff --git a/test/Microsoft.CodeAnalysis.Razor.Workspaces.Test/xunit.runner.json b/test/Microsoft.CodeAnalysis.Razor.Workspaces.Test/xunit.runner.json new file mode 100644 index 0000000000..c04bb61fe6 --- /dev/null +++ b/test/Microsoft.CodeAnalysis.Razor.Workspaces.Test/xunit.runner.json @@ -0,0 +1,4 @@ +{ + "methodDisplay": "method", + "shadowCopy": false +} \ No newline at end of file diff --git a/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/DefaultTagHelperCompletionServiceTest.cs b/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Legacy/DefaultTagHelperCompletionServiceTest.cs similarity index 99% rename from test/Microsoft.VisualStudio.LanguageServices.Razor.Test/DefaultTagHelperCompletionServiceTest.cs rename to test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Legacy/DefaultTagHelperCompletionServiceTest.cs index 2bc6d439a8..5d6ad363bc 100644 --- a/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/DefaultTagHelperCompletionServiceTest.cs +++ b/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Legacy/DefaultTagHelperCompletionServiceTest.cs @@ -961,4 +961,4 @@ namespace Microsoft.VisualStudio.LanguageServices.Razor return completionContext; } } -} +} \ No newline at end of file diff --git a/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/DefaultTagHelperFactsServiceTest.cs b/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Legacy/DefaultTagHelperFactsServiceTest.cs similarity index 99% rename from test/Microsoft.VisualStudio.LanguageServices.Razor.Test/DefaultTagHelperFactsServiceTest.cs rename to test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Legacy/DefaultTagHelperFactsServiceTest.cs index b6a8d32147..f62597b32f 100644 --- a/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/DefaultTagHelperFactsServiceTest.cs +++ b/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Legacy/DefaultTagHelperFactsServiceTest.cs @@ -385,4 +385,4 @@ namespace Microsoft.VisualStudio.LanguageServices.Razor Assert.Equal(expectedDescriptors, descriptors, TagHelperDescriptorComparer.CaseSensitive); } } -} +} \ No newline at end of file diff --git a/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Microsoft.VisualStudio.LanguageServices.Razor.Test.csproj b/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Microsoft.VisualStudio.LanguageServices.Razor.Test.csproj index e4936289ce..792e521085 100644 --- a/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Microsoft.VisualStudio.LanguageServices.Razor.Test.csproj +++ b/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Microsoft.VisualStudio.LanguageServices.Razor.Test.csproj @@ -4,20 +4,15 @@ net461 - $(DefaultItemExcludes);TestFiles\**\* true - - - - @@ -25,14 +20,11 @@ + - - - - diff --git a/tooling/Microsoft.VisualStudio.RazorExtension/DocumentInfo/RazorDocumentInfoViewModel.cs b/tooling/Microsoft.VisualStudio.RazorExtension/DocumentInfo/RazorDocumentInfoViewModel.cs index ed6790f278..7dd5e8e755 100644 --- a/tooling/Microsoft.VisualStudio.RazorExtension/DocumentInfo/RazorDocumentInfoViewModel.cs +++ b/tooling/Microsoft.VisualStudio.RazorExtension/DocumentInfo/RazorDocumentInfoViewModel.cs @@ -5,6 +5,7 @@ using System; using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Host; using Microsoft.CodeAnalysis.Razor; using Microsoft.CodeAnalysis.Text; @@ -44,6 +45,19 @@ namespace Microsoft.VisualStudio.RazorExtension.DocumentInfo public SourceTextContainer TextContainer => _documentTracker.TextContainer; public Workspace Workspace => _documentTracker.Workspace; + + public HostLanguageServices RazorLanguageServices => Workspace?.Services.GetLanguageServices(RazorLanguage.Name); + + public TagHelperResolver TagHelperResolver => RazorLanguageServices?.GetRequiredService(); + + public RazorSyntaxFactsService RazorSyntaxFactsService => RazorLanguageServices?.GetRequiredService(); + + public RazorTemplateEngineFactoryService RazorTemplateEngineFactoryService => RazorLanguageServices?.GetRequiredService(); + + public TagHelperCompletionService TagHelperCompletionService => RazorLanguageServices?.GetRequiredService(); + + public TagHelperFactsService TagHelperFactsService => RazorLanguageServices?.GetRequiredService(); + } } diff --git a/tooling/Microsoft.VisualStudio.RazorExtension/DocumentInfo/RazorDocumentInfoWindowControl.xaml b/tooling/Microsoft.VisualStudio.RazorExtension/DocumentInfo/RazorDocumentInfoWindowControl.xaml index 831f3cd015..c18ae0c7fb 100644 --- a/tooling/Microsoft.VisualStudio.RazorExtension/DocumentInfo/RazorDocumentInfoWindowControl.xaml +++ b/tooling/Microsoft.VisualStudio.RazorExtension/DocumentInfo/RazorDocumentInfoWindowControl.xaml @@ -20,6 +20,12 @@ + + + + + +