예제 #1
0
        protected void AssertSourceMappingsMatchBaseline(RazorCodeDocument codeDocument)
        {
            if (FileName == null)
            {
                var message = $"{nameof(AssertSourceMappingsMatchBaseline)} should only be called from an integration test ({nameof(FileName)} is null).";
                throw new InvalidOperationException(message);
            }

            var csharpDocument = codeDocument.GetCSharpDocument();

            Assert.NotNull(csharpDocument);

            var baselineFileName   = Path.ChangeExtension(FileName, ".mappings.txt");
            var serializedMappings = SourceMappingsSerializer.Serialize(csharpDocument, codeDocument.Source);

            if (GenerateBaselines)
            {
                var baselineFullPath = Path.Combine(TestProjectRoot, baselineFileName);
                File.WriteAllText(baselineFullPath, serializedMappings);
                return;
            }

            var testFile = TestFile.Create(baselineFileName, GetType().GetTypeInfo().Assembly);

            if (!testFile.Exists())
            {
                throw new XunitException($"The resource {baselineFileName} was not found.");
            }

            var baseline = testFile.ReadAllText();

            // Normalize newlines to match those in the baseline.
            var actualBaseline = serializedMappings.Replace("\r", "").Replace("\n", "\r\n");

            Assert.Equal(baseline, actualBaseline);

            var syntaxTree = codeDocument.GetSyntaxTree();
            var visitor    = new CodeSpanVisitor();

            visitor.Visit(syntaxTree.Root);

            var charBuffer = new char[codeDocument.Source.Length];

            codeDocument.Source.CopyTo(0, charBuffer, 0, codeDocument.Source.Length);
            var sourceContent = new string(charBuffer);

            var spans = visitor.CodeSpans;

            for (var i = 0; i < spans.Count; i++)
            {
                var span       = spans[i];
                var sourceSpan = span.GetSourceSpan(codeDocument.Source);
                if (sourceSpan == null)
                {
                    // Not in the main file, skip.
                    continue;
                }

                var expectedSpan = sourceContent.Substring(sourceSpan.AbsoluteIndex, sourceSpan.Length);

                // See #2593
                if (string.IsNullOrWhiteSpace(expectedSpan))
                {
                    // For now we don't verify whitespace inside of a directive. We know that directives cheat
                    // with how they bound whitespace/C#/markup to make completion work.
                    if (span.FirstAncestorOrSelf <RazorDirectiveSyntax>() != null)
                    {
                        continue;
                    }
                }

                // See #2594
                if (string.Equals("@", expectedSpan, StringComparison.Ordinal))
                {
                    // For now we don't verify an escaped transition. In some cases one of the @ tokens in @@foo
                    // will be mapped as C# but will not be present in the output buffer because it's not actually C#.
                    continue;
                }

                var found = false;
                for (var j = 0; j < csharpDocument.SourceMappings.Count; j++)
                {
                    var mapping = csharpDocument.SourceMappings[j];
                    if (mapping.OriginalSpan == sourceSpan)
                    {
                        var actualSpan = csharpDocument.GeneratedCode.Substring(
                            mapping.GeneratedSpan.AbsoluteIndex,
                            mapping.GeneratedSpan.Length);

                        if (!string.Equals(expectedSpan, actualSpan, StringComparison.Ordinal))
                        {
                            throw new XunitException(
                                      $"Found the span {sourceSpan} in the output mappings but it contains " +
                                      $"'{EscapeWhitespace(actualSpan)}' instead of '{EscapeWhitespace(expectedSpan)}'.");
                        }

                        found = true;
                        break;
                    }
                }

                if (!found)
                {
                    throw new XunitException(
                              $"Could not find the span {sourceSpan} - containing '{EscapeWhitespace(expectedSpan)}' " +
                              $"in the output.");
                }
            }
        }
예제 #2
0
        protected void AssertLinePragmas(RazorCodeDocument codeDocument, bool designTime)
        {
            if (FileName == null)
            {
                var message = $"{nameof(AssertSourceMappingsMatchBaseline)} should only be called from an integration test. ({nameof(FileName)} is null).";
                throw new InvalidOperationException(message);
            }

            var csharpDocument = codeDocument.GetCSharpDocument();

            Assert.NotNull(csharpDocument);
            var linePragmas = csharpDocument.LinePragmas;

            designTime = false;
            if (designTime)
            {
                var sourceMappings = csharpDocument.SourceMappings;
                foreach (var sourceMapping in sourceMappings)
                {
                    var foundMatchingPragma = false;
                    foreach (var linePragma in linePragmas)
                    {
                        if (sourceMapping.OriginalSpan.LineIndex >= linePragma.StartLineIndex &&
                            sourceMapping.OriginalSpan.LineIndex <= linePragma.EndLineIndex)
                        {
                            // Found a match.
                            foundMatchingPragma = true;
                            break;
                        }
                    }

                    Assert.True(foundMatchingPragma, $"No line pragma found for code at line {sourceMapping.OriginalSpan.LineIndex + 1}.");
                }
            }
            else
            {
                var syntaxTree   = codeDocument.GetSyntaxTree();
                var sourceBuffer = new char[syntaxTree.Source.Length];
                syntaxTree.Source.CopyTo(0, sourceBuffer, 0, syntaxTree.Source.Length);
                var sourceContent   = new string(sourceBuffer);
                var classifiedSpans = syntaxTree.GetClassifiedSpans();
                foreach (var classifiedSpan in classifiedSpans)
                {
                    var content = sourceContent.Substring(classifiedSpan.Span.AbsoluteIndex, classifiedSpan.Span.Length);
                    if (!string.IsNullOrWhiteSpace(content) &&
                        classifiedSpan.BlockKind != BlockKindInternal.Directive &&
                        classifiedSpan.SpanKind == SpanKindInternal.Code)
                    {
                        var foundMatchingPragma = false;
                        foreach (var linePragma in linePragmas)
                        {
                            if (classifiedSpan.Span.LineIndex >= linePragma.StartLineIndex &&
                                classifiedSpan.Span.LineIndex <= linePragma.EndLineIndex)
                            {
                                // Found a match.
                                foundMatchingPragma = true;
                                break;
                            }
                        }

                        Assert.True(foundMatchingPragma, $"No line pragma found for code '{content}' at line {classifiedSpan.Span.LineIndex + 1}.");
                    }
                }
            }
        }
예제 #3
0
        public static FormattingContext Create(
            DocumentUri uri,
            DocumentSnapshot originalSnapshot,
            RazorCodeDocument codeDocument,
            FormattingOptions options,
            Range range         = null,
            bool isFormatOnType = false)
        {
            if (uri is null)
            {
                throw new ArgumentNullException(nameof(uri));
            }

            if (originalSnapshot is null)
            {
                throw new ArgumentNullException(nameof(originalSnapshot));
            }

            if (codeDocument is null)
            {
                throw new ArgumentNullException(nameof(codeDocument));
            }

            if (options is null)
            {
                throw new ArgumentNullException(nameof(options));
            }

            var text = codeDocument.GetSourceText();

            range ??= TextSpan.FromBounds(0, text.Length).AsRange(text);

            var syntaxTree      = codeDocument.GetSyntaxTree();
            var formattingSpans = syntaxTree.GetFormattingSpans();

            var result = new FormattingContext()
            {
                Uri = uri,
                OriginalSnapshot = originalSnapshot,
                CodeDocument     = codeDocument,
                Range            = range,
                Options          = options,
                IsFormatOnType   = isFormatOnType,
                FormattingSpans  = formattingSpans
            };

            var sourceText   = codeDocument.GetSourceText();
            var indentations = new Dictionary <int, IndentationContext>();

            var previousIndentationLevel = 0;

            for (var i = 0; i < sourceText.Lines.Count; i++)
            {
                // Get first non-whitespace character position
                var nonWsPos              = sourceText.Lines[i].GetFirstNonWhitespacePosition();
                var existingIndentation   = (nonWsPos ?? sourceText.Lines[i].End) - sourceText.Lines[i].Start;
                var emptyOrWhitespaceLine = false;
                if (nonWsPos == null)
                {
                    emptyOrWhitespaceLine = true;
                    nonWsPos = sourceText.Lines[i].Start;
                }

                // position now contains the first non-whitespace character or 0. Get the corresponding FormattingSpan.
                if (result.TryGetFormattingSpan(nonWsPos.Value, out var span))
                {
                    indentations[i] = new IndentationContext
                    {
                        Line = i,
                        RazorIndentationLevel    = span.RazorIndentationLevel,
                        HtmlIndentationLevel     = span.HtmlIndentationLevel,
                        RelativeIndentationLevel = span.IndentationLevel - previousIndentationLevel,
                        ExistingIndentation      = existingIndentation,
                        FirstSpan             = span,
                        EmptyOrWhitespaceLine = emptyOrWhitespaceLine,
                    };
                    previousIndentationLevel = span.IndentationLevel;
                }
                else
                {
                    // Couldn't find a corresponding FormattingSpan. Happens if it is a 0 length line.
                    // Let's create a 0 length span to represent this and default it to HTML.
                    var placeholderSpan = new FormattingSpan(
                        new Language.Syntax.TextSpan(nonWsPos.Value, 0),
                        new Language.Syntax.TextSpan(nonWsPos.Value, 0),
                        FormattingSpanKind.Markup,
                        FormattingBlockKind.Markup,
                        razorIndentationLevel: 0,
                        htmlIndentationLevel: 0,
                        isInClassBody: false,
                        componentLambdaNestingLevel: 0);

                    indentations[i] = new IndentationContext
                    {
                        Line = i,
                        RazorIndentationLevel    = 0,
                        HtmlIndentationLevel     = 0,
                        RelativeIndentationLevel = previousIndentationLevel,
                        ExistingIndentation      = existingIndentation,
                        FirstSpan             = placeholderSpan,
                        EmptyOrWhitespaceLine = emptyOrWhitespaceLine,
                    };
                }
            }

            result.Indentations = indentations;

            return(result);
        }
예제 #4
0
        public override HoverModel GetHoverInfo(RazorCodeDocument codeDocument, SourceLocation location, ClientCapabilities clientCapabilities)
        {
            if (codeDocument is null)
            {
                throw new ArgumentNullException(nameof(codeDocument));
            }

            var syntaxTree = codeDocument.GetSyntaxTree();

            var change = new SourceChange(location.AbsoluteIndex, length: 0, newText: "");
            var owner  = syntaxTree.Root.LocateOwner(change);

            if (owner == null)
            {
                Debug.Fail("Owner should never be null.");
                return(null);
            }

            var parent   = owner.Parent;
            var position = new Position(location.LineIndex, location.CharacterIndex);
            var tagHelperDocumentContext = codeDocument.GetTagHelperContext();

            var ancestors = owner.Ancestors();

            var(parentTag, parentIsTagHelper) = _tagHelperFactsService.GetNearestAncestorTagInfo(ancestors);

            if (_htmlFactsService.TryGetElementInfo(parent, out var containingTagNameToken, out var attributes) &&
                containingTagNameToken.Span.IntersectsWith(location.AbsoluteIndex))
            {
                // Hovering over HTML tag name
                var stringifiedAttributes = _tagHelperFactsService.StringifyAttributes(attributes);
                var binding = _tagHelperFactsService.GetTagHelperBinding(
                    tagHelperDocumentContext,
                    containingTagNameToken.Content,
                    stringifiedAttributes,
                    parentTag: parentTag,
                    parentIsTagHelper: parentIsTagHelper);

                if (binding is null)
                {
                    // No matching tagHelpers, it's just HTML
                    return(null);
                }
                else
                {
                    Debug.Assert(binding.Descriptors.Any());

                    var range = containingTagNameToken.GetRange(codeDocument.Source);

                    var result = ElementInfoToHover(binding.Descriptors, range, clientCapabilities);
                    return(result);
                }
            }

            if (_htmlFactsService.TryGetAttributeInfo(parent, out containingTagNameToken, out _, out var selectedAttributeName, out var selectedAttributeNameLocation, out attributes) &&
                selectedAttributeNameLocation?.IntersectsWith(location.AbsoluteIndex) == true)
            {
                // Hovering over HTML attribute name
                var stringifiedAttributes = _tagHelperFactsService.StringifyAttributes(attributes);

                var binding = _tagHelperFactsService.GetTagHelperBinding(
                    tagHelperDocumentContext,
                    containingTagNameToken.Content,
                    stringifiedAttributes,
                    parentTag: parentTag,
                    parentIsTagHelper: parentIsTagHelper);

                if (binding is null)
                {
                    // No matching TagHelpers, it's just HTML
                    return(null);
                }
                else
                {
                    Debug.Assert(binding.Descriptors.Any());
                    var tagHelperAttributes = _tagHelperFactsService.GetBoundTagHelperAttributes(tagHelperDocumentContext, selectedAttributeName, binding);

                    // Grab the first attribute that we find that intersects with this location. That way if there are multiple attributes side-by-side aka hovering over:
                    //      <input checked| minimized />
                    // Then we take the left most attribute (attributes are returned in source order).
                    var attribute = attributes.First(a => a.Span.IntersectsWith(location.AbsoluteIndex));
                    if (attribute is MarkupTagHelperAttributeSyntax thAttributeSyntax)
                    {
                        attribute = thAttributeSyntax.Name;
                    }
                    else if (attribute is MarkupMinimizedTagHelperAttributeSyntax thMinimizedAttribute)
                    {
                        attribute = thMinimizedAttribute.Name;
                    }
                    else if (attribute is MarkupTagHelperDirectiveAttributeSyntax directiveAttribute)
                    {
                        attribute = directiveAttribute.Name;
                    }
                    else if (attribute is MarkupMinimizedTagHelperDirectiveAttributeSyntax miniDirectiveAttribute)
                    {
                        attribute = miniDirectiveAttribute;
                    }

                    var attributeName = attribute.GetContent();
                    var range         = attribute.GetRange(codeDocument.Source);

                    // Include the @ in the range
                    switch (attribute.Parent.Kind)
                    {
                    case SyntaxKind.MarkupTagHelperDirectiveAttribute:
                        var directiveAttribute = attribute.Parent as MarkupTagHelperDirectiveAttributeSyntax;
                        range.Start.Character -= directiveAttribute.Transition.FullWidth;
                        attributeName          = "@" + attributeName;
                        break;

                    case SyntaxKind.MarkupMinimizedTagHelperDirectiveAttribute:
                        var minimizedAttribute = containingTagNameToken.Parent as MarkupMinimizedTagHelperDirectiveAttributeSyntax;
                        range.Start.Character -= minimizedAttribute.Transition.FullWidth;
                        attributeName          = "@" + attributeName;
                        break;
                    }

                    var attributeHoverModel = AttributeInfoToHover(tagHelperAttributes, range, attributeName, clientCapabilities);

                    return(attributeHoverModel);
                }
            }

            return(null);
        }
        private async Task <IReadOnlyList <TagHelperDescriptor> > GetOriginTagHelpersAsync(DocumentSnapshot documentSnapshot, RazorCodeDocument codeDocument, Position position)
        {
            var sourceText = await documentSnapshot.GetTextAsync().ConfigureAwait(false);

            var linePosition      = new LinePosition((int)position.Line, (int)position.Character);
            var hostDocumentIndex = sourceText.Lines.GetPosition(linePosition);
            var location          = new SourceLocation(hostDocumentIndex, (int)position.Line, (int)position.Character);

            var change     = new SourceChange(location.AbsoluteIndex, length: 0, newText: string.Empty);
            var syntaxTree = codeDocument.GetSyntaxTree();

            if (syntaxTree?.Root is null)
            {
                return(null);
            }

            var owner = syntaxTree.Root.LocateOwner(change);

            if (owner == null)
            {
                Debug.Fail("Owner should never be null.");
                return(null);
            }

            var node = owner.Ancestors().FirstOrDefault(n => n.Kind == SyntaxKind.MarkupTagHelperStartTag);

            if (node == null || !(node is MarkupTagHelperStartTagSyntax tagHelperStartTag))
            {
                return(null);
            }

            // Ensure the rename action was invoked on the component name
            // instead of a component parameter. This serves as an issue
            // mitigation till `textDocument/prepareRename` is supported
            // and we can ensure renames aren't triggered in unsupported
            // contexts. (https://github.com/dotnet/aspnetcore/issues/26407)
            if (!tagHelperStartTag.Name.FullSpan.IntersectsWith(hostDocumentIndex))
            {
                return(null);
            }

            if (!(tagHelperStartTag?.Parent is MarkupTagHelperElementSyntax tagHelperElement))
            {
                return(null);
            }

            // Can only have 1 component TagHelper belonging to an element at a time
            var primaryTagHelper = tagHelperElement.TagHelperInfo.BindingResult.Descriptors.FirstOrDefault(descriptor => descriptor.IsComponentTagHelper());

            if (primaryTagHelper == null)
            {
                return(null);
            }

            var originTagHelpers = new List <TagHelperDescriptor>()
            {
                primaryTagHelper
            };
            var associatedTagHelper = FindAssociatedTagHelper(primaryTagHelper, documentSnapshot.Project.TagHelpers);

            if (associatedTagHelper == null)
            {
                Debug.Fail("Components should always have an associated TagHelper.");
                return(null);
            }

            originTagHelpers.Add(associatedTagHelper);

            return(originTagHelpers);
        }
        internal static async Task <TagHelperBinding?> GetOriginTagHelperBindingAsync(
            DocumentSnapshot documentSnapshot,
            RazorCodeDocument codeDocument,
            Position position,
            ILogger logger)
        {
            var sourceText = await documentSnapshot.GetTextAsync().ConfigureAwait(false);

            var linePosition      = new LinePosition(position.Line, position.Character);
            var hostDocumentIndex = sourceText.Lines.GetPosition(linePosition);
            var location          = new SourceLocation(hostDocumentIndex, position.Line, position.Character);

            var change     = new SourceChange(location.AbsoluteIndex, length: 0, newText: string.Empty);
            var syntaxTree = codeDocument.GetSyntaxTree();

            if (syntaxTree?.Root is null)
            {
                logger.LogInformation("Could not retrieve syntax tree.");
                return(null);
            }

            var owner = syntaxTree.Root.LocateOwner(change);

            if (owner is null)
            {
                logger.LogInformation("Could not locate owner.");
                return(null);
            }

            var node = owner.Ancestors().FirstOrDefault(n =>
                                                        n.Kind == SyntaxKind.MarkupTagHelperStartTag ||
                                                        n.Kind == SyntaxKind.MarkupTagHelperEndTag);

            if (node is null)
            {
                logger.LogInformation("Could not locate ancestor of type MarkupTagHelperStartTag or MarkupTagHelperEndTag.");
                return(null);
            }

            var name = GetStartOrEndTagName(node);

            if (name is null)
            {
                logger.LogInformation("Could not retrieve name of start or end tag.");
                return(null);
            }

            if (!name.Span.Contains(location.AbsoluteIndex))
            {
                logger.LogInformation($"Tag name's span does not contain location's absolute index ({location.AbsoluteIndex}).");
                return(null);
            }

            if (node.Parent is not MarkupTagHelperElementSyntax tagHelperElement)
            {
                logger.LogInformation("Parent of start or end tag is not a MarkupTagHelperElement.");
                return(null);
            }

            return(tagHelperElement.TagHelperInfo.BindingResult);
        }
예제 #7
0
    // In general documents will have a relative path (relative to the project root).
    // We can only really compute a nice namespace when we know a relative path.
    //
    // However all kinds of thing are possible in tools. We shouldn't barf here if the document isn't
    // set up correctly.
    public static bool TryComputeNamespace(this RazorCodeDocument document, bool fallbackToRootNamespace, out string @namespace)
    {
        if (document == null)
        {
            throw new ArgumentNullException(nameof(document));
        }

        var filePath = document.Source.FilePath;

        if (filePath == null || document.Source.RelativePath == null || filePath.Length < document.Source.RelativePath.Length)
        {
            @namespace = null;
            return(false);
        }

        // If the document or it's imports contains a @namespace directive, we want to use that over the root namespace.
        var baseNamespace         = string.Empty;
        var appendSuffix          = true;
        var lastNamespaceContent  = string.Empty;
        var lastNamespaceLocation = SourceSpan.Undefined;
        var importSyntaxTrees     = document.GetImportSyntaxTrees();

        if (importSyntaxTrees != null)
        {
            // ImportSyntaxTrees is usually set. Just being defensive.
            foreach (var importSyntaxTree in importSyntaxTrees)
            {
                if (importSyntaxTree != null && NamespaceVisitor.TryGetLastNamespaceDirective(importSyntaxTree, out var importNamespaceContent, out var importNamespaceLocation))
                {
                    lastNamespaceContent  = importNamespaceContent;
                    lastNamespaceLocation = importNamespaceLocation;
                }
            }
        }

        var syntaxTree = document.GetSyntaxTree();

        if (syntaxTree != null && NamespaceVisitor.TryGetLastNamespaceDirective(syntaxTree, out var namespaceContent, out var namespaceLocation))
        {
            lastNamespaceContent  = namespaceContent;
            lastNamespaceLocation = namespaceLocation;
        }

        StringSegment relativePath = document.Source.RelativePath;

        // If there are multiple @namespace directives in the heirarchy,
        // we want to pick the closest one to the current document.
        if (!string.IsNullOrEmpty(lastNamespaceContent))
        {
            baseNamespace = lastNamespaceContent;
            var directiveLocationDirectory = NormalizeDirectory(lastNamespaceLocation.FilePath);

            var sourceFilePath = new StringSegment(document.Source.FilePath);
            // We're specifically using OrdinalIgnoreCase here because Razor treats all paths as case-insensitive.
            if (!sourceFilePath.StartsWith(directiveLocationDirectory, StringComparison.OrdinalIgnoreCase) ||
                sourceFilePath.Length <= directiveLocationDirectory.Length)
            {
                // The most relevant directive is not from the directory hierarchy, can't compute a suffix.
                appendSuffix = false;
            }
            else
            {
                // We know that the document containing the namespace directive is in the current document's heirarchy.
                // Let's compute the actual relative path that we'll use to compute the namespace suffix.
                relativePath = sourceFilePath.Subsegment(directiveLocationDirectory.Length);
            }
        }
        else if (fallbackToRootNamespace)
        {
            var options = document.GetCodeGenerationOptions() ?? document.GetDocumentIntermediateNode()?.Options;
            baseNamespace = options?.RootNamespace;
            appendSuffix  = true;
        }

        if (string.IsNullOrEmpty(baseNamespace))
        {
            // There was no valid @namespace directive and we couldn't compute the RootNamespace.
            @namespace = null;
            return(false);
        }

        var builder = new StringBuilder();

        // Sanitize the base namespace, but leave the dots.
        var segments = new StringTokenizer(baseNamespace, NamespaceSeparators);
        var first    = true;

        foreach (var token in segments)
        {
            if (token.IsEmpty)
            {
                continue;
            }

            if (first)
            {
                first = false;
            }
            else
            {
                builder.Append('.');
            }

            CSharpIdentifier.AppendSanitized(builder, token);
        }

        if (appendSuffix)
        {
            // If we get here, we already have a base namespace and the relative path that should be used as the namespace suffix.
            segments = new StringTokenizer(relativePath, PathSeparators);
            var previousLength = builder.Length;
            foreach (var token in segments)
            {
                if (token.IsEmpty)
                {
                    continue;
                }

                previousLength = builder.Length;

                builder.Append('.');
                CSharpIdentifier.AppendSanitized(builder, token);
            }

            // Trim the last segment because it's the FileName.
            builder.Length = previousLength;
        }

        @namespace = builder.ToString();

        return(true);

        // We want to normalize the path of the file containing the '@namespace' directive to just the containing
        // directory with a trailing separator.
        //
        // Not using Path.GetDirectoryName here because it doesn't meet these requirements, and we want to handle
        // both 'view engine' style paths and absolute paths.
        //
        // We also don't normalize the separators here. We expect that all documents are using a consistent style of path.
        //
        // If we can't normalize the path, we just return null so it will be ignored.
        StringSegment NormalizeDirectory(string path)
        {
            if (string.IsNullOrEmpty(path))
            {
                return(default);
        internal static async Task <(TagHelperDescriptor?, BoundAttributeDescriptor?)> GetOriginTagHelperBindingAsync(
            DocumentSnapshot documentSnapshot,
            RazorCodeDocument codeDocument,
            Position position,
            ILogger logger)
        {
            var sourceText = await documentSnapshot.GetTextAsync().ConfigureAwait(false);

            var linePosition      = new LinePosition(position.Line, position.Character);
            var hostDocumentIndex = sourceText.Lines.GetPosition(linePosition);
            var location          = new SourceLocation(hostDocumentIndex, position.Line, position.Character);

            var change     = new SourceChange(location.AbsoluteIndex, length: 0, newText: string.Empty);
            var syntaxTree = codeDocument.GetSyntaxTree();

            if (syntaxTree?.Root is null)
            {
                logger.LogInformation("Could not retrieve syntax tree.");
                return(null, null);
            }

            var owner = syntaxTree.Root.LocateOwner(change);

            if (owner is null)
            {
                logger.LogInformation("Could not locate owner.");
                return(null, null);
            }

            var node = owner.Ancestors().FirstOrDefault(n =>
                                                        n.Kind == SyntaxKind.MarkupTagHelperStartTag ||
                                                        n.Kind == SyntaxKind.MarkupTagHelperEndTag);

            if (node is null)
            {
                logger.LogInformation("Could not locate ancestor of type MarkupTagHelperStartTag or MarkupTagHelperEndTag.");
                return(null, null);
            }

            var name = GetStartOrEndTagName(node);

            if (name is null)
            {
                logger.LogInformation("Could not retrieve name of start or end tag.");
                return(null, null);
            }

            string?propertyName = null;

            // If we're on an attribute then just validate against the attribute name
            if (owner.Parent is MarkupTagHelperAttributeSyntax attribute)
            {
                // Normal attribute, ie <Component attribute=value />
                name         = attribute.Name;
                propertyName = attribute.TagHelperAttributeInfo.Name;
            }
            else if (owner.Parent is MarkupMinimizedTagHelperAttributeSyntax minimizedAttribute)
            {
                // Minimized attribute, ie <Component attribute />
                name         = minimizedAttribute.Name;
                propertyName = minimizedAttribute.TagHelperAttributeInfo.Name;
            }

            if (!name.Span.Contains(location.AbsoluteIndex))
            {
                logger.LogInformation($"Tag name or attributes's span does not contain location's absolute index ({location.AbsoluteIndex}).");
                return(null, null);
            }

            if (node.Parent is not MarkupTagHelperElementSyntax tagHelperElement)
            {
                logger.LogInformation("Parent of start or end tag is not a MarkupTagHelperElement.");
                return(null, null);
            }

            var originTagDescriptor = tagHelperElement.TagHelperInfo.BindingResult.Descriptors.FirstOrDefault(d => !d.IsAttributeDescriptor());

            if (originTagDescriptor is null)
            {
                logger.LogInformation("Origin TagHelper descriptor is null.");
                return(null, null);
            }

            var attributeDescriptor = (propertyName is not null)
                ? originTagDescriptor.BoundAttributes.FirstOrDefault(a => a.Name?.Equals(propertyName, StringComparison.Ordinal) == true)
                : null;

            return(originTagDescriptor, attributeDescriptor);
        }
        private static FormattingContext CreateFormattingContext(Uri uri, RazorCodeDocument codedocument, Range range, FormattingOptions options)
        {
            var result = new FormattingContext()
            {
                Uri          = uri,
                CodeDocument = codedocument,
                Range        = range,
                Options      = options
            };

            var source          = codedocument.Source;
            var syntaxTree      = codedocument.GetSyntaxTree();
            var formattingSpans = syntaxTree.GetFormattingSpans();

            var total = 0;
            var previousIndentationLevel = 0;

            for (var i = 0; i < source.Lines.Count; i++)
            {
                // Get first non-whitespace character position
                var lineLength = source.Lines.GetLineLength(i);
                var nonWsChar  = 0;
                for (var j = 0; j < lineLength; j++)
                {
                    var ch = source[total + j];
                    if (!char.IsWhiteSpace(ch) && !ParserHelpers.IsNewLine(ch))
                    {
                        nonWsChar = j;
                        break;
                    }
                }

                // position now contains the first non-whitespace character or 0. Get the corresponding FormattingSpan.
                if (TryGetFormattingSpan(total + nonWsChar, formattingSpans, out var span))
                {
                    result.Indentations[i] = new IndentationContext
                    {
                        Line                     = i,
                        IndentationLevel         = span.IndentationLevel,
                        RelativeIndentationLevel = span.IndentationLevel - previousIndentationLevel,
                        ExistingIndentation      = nonWsChar,
                        FirstSpan                = span,
                    };
                    previousIndentationLevel = span.IndentationLevel;
                }
                else
                {
                    // Couldn't find a corresponding FormattingSpan.
                    result.Indentations[i] = new IndentationContext
                    {
                        Line                     = i,
                        IndentationLevel         = -1,
                        RelativeIndentationLevel = previousIndentationLevel,
                        ExistingIndentation      = nonWsChar,
                    };
                }

                total += lineLength;
            }

            return(result);
        }
예제 #10
0
        public override HoverModel GetHoverInfo(RazorCodeDocument codeDocument, SourceSpan location)
        {
            if (codeDocument is null)
            {
                throw new ArgumentNullException(nameof(codeDocument));
            }

            var syntaxTree = codeDocument.GetSyntaxTree();
            var change     = new SourceChange(location, "");
            var owner      = syntaxTree.Root.LocateOwner(change);

            if (owner == null)
            {
                Debug.Fail("Owner should never be null.");
                return(null);
            }

            var parent   = owner.Parent;
            var position = new Position(location.LineIndex, location.CharacterIndex);
            var tagHelperDocumentContext = codeDocument.GetTagHelperContext();

            var ancestors = owner.Ancestors();

            var(parentTag, parentIsTagHelper) = _tagHelperFactsService.GetNearestAncestorTagInfo(ancestors);

            if (_htmlFactsService.TryGetElementInfo(parent, out var containingTagNameToken, out var attributes) &&
                containingTagNameToken.Span.IntersectsWith(location.AbsoluteIndex))
            {
                // Hovering over HTML tag name
                var stringifiedAttributes = _tagHelperFactsService.StringifyAttributes(attributes);
                var binding = _tagHelperFactsService.GetTagHelperBinding(
                    tagHelperDocumentContext,
                    containingTagNameToken.Content,
                    stringifiedAttributes,
                    parentTag: parentTag,
                    parentIsTagHelper: parentIsTagHelper);

                if (binding is null)
                {
                    // No matching tagHelpers, it's just HTML
                    return(null);
                }
                else
                {
                    Debug.Assert(binding.Descriptors.Count() > 0);

                    var range = GetRangeFromSyntaxNode(containingTagNameToken, codeDocument);

                    var result = ElementInfoToHover(binding.Descriptors, range);
                    return(result);
                }
            }

            if (_htmlFactsService.TryGetAttributeInfo(parent, out containingTagNameToken, out var selectedAttributeName, out attributes) &&
                attributes.Span.IntersectsWith(location.AbsoluteIndex))
            {
                // Hovering over HTML attribute name
                var stringifiedAttributes = _tagHelperFactsService.StringifyAttributes(attributes);


                var binding = _tagHelperFactsService.GetTagHelperBinding(
                    tagHelperDocumentContext,
                    containingTagNameToken.Content,
                    stringifiedAttributes,
                    parentTag: parentTag,
                    parentIsTagHelper: parentIsTagHelper);

                if (binding is null)
                {
                    // No matching TagHelpers, it's just HTML
                    return(null);
                }
                else
                {
                    Debug.Assert(binding.Descriptors.Count() > 0);
                    var tagHelperAttributes = _tagHelperFactsService.GetBoundTagHelperAttributes(tagHelperDocumentContext, selectedAttributeName, binding);

                    var attribute           = attributes.Single(a => a.Span.IntersectsWith(location.AbsoluteIndex));
                    var range               = GetRangeFromSyntaxNode(attribute, codeDocument);
                    var attributeHoverModel = AttributeInfoToHover(tagHelperAttributes, range);

                    return(attributeHoverModel);
                }
            }

            return(null);
        }
예제 #11
0
        public static FormattingContext Create(
            DocumentUri uri,
            DocumentSnapshot originalSnapshot,
            RazorCodeDocument codeDocument,
            FormattingOptions options,
            Range range         = null,
            bool isFormatOnType = false)
        {
            if (uri is null)
            {
                throw new ArgumentNullException(nameof(uri));
            }

            if (originalSnapshot is null)
            {
                throw new ArgumentNullException(nameof(originalSnapshot));
            }

            if (codeDocument is null)
            {
                throw new ArgumentNullException(nameof(codeDocument));
            }

            if (options is null)
            {
                throw new ArgumentNullException(nameof(options));
            }

            var text = codeDocument.GetSourceText();

            range ??= TextSpan.FromBounds(0, text.Length).AsRange(text);

            var result = new FormattingContext()
            {
                Uri = uri,
                OriginalSnapshot = originalSnapshot,
                CodeDocument     = codeDocument,
                Range            = range,
                Options          = options,
                IsFormatOnType   = isFormatOnType
            };

            var source          = codeDocument.Source;
            var syntaxTree      = codeDocument.GetSyntaxTree();
            var formattingSpans = syntaxTree.GetFormattingSpans();
            var indentations    = new Dictionary <int, IndentationContext>();

            var total = 0;
            var previousIndentationLevel = 0;

            for (var i = 0; i < source.Lines.Count; i++)
            {
                // Get first non-whitespace character position
                var lineLength = source.Lines.GetLineLength(i);
                var nonWsChar  = 0;
                for (var j = 0; j < lineLength; j++)
                {
                    var ch = source[total + j];
                    if (!char.IsWhiteSpace(ch) && !ParserHelpers.IsNewLine(ch))
                    {
                        nonWsChar = j;
                        break;
                    }
                }

                // position now contains the first non-whitespace character or 0. Get the corresponding FormattingSpan.
                if (TryGetFormattingSpan(total + nonWsChar, formattingSpans, out var span))
                {
                    indentations[i] = new IndentationContext
                    {
                        Line                     = i,
                        IndentationLevel         = span.IndentationLevel,
                        RelativeIndentationLevel = span.IndentationLevel - previousIndentationLevel,
                        ExistingIndentation      = nonWsChar,
                        FirstSpan                = span,
                    };
                    previousIndentationLevel = span.IndentationLevel;
                }
                else
                {
                    // Couldn't find a corresponding FormattingSpan. Happens if it is a 0 length line.
                    // Let's create a 0 length span to represent this and default it to HTML.
                    var placeholderSpan = new FormattingSpan(
                        new Language.Syntax.TextSpan(total + nonWsChar, 0),
                        new Language.Syntax.TextSpan(total + nonWsChar, 0),
                        FormattingSpanKind.Markup,
                        FormattingBlockKind.Markup,
                        indentationLevel: 0,
                        isInClassBody: false);

                    indentations[i] = new IndentationContext
                    {
                        Line                     = i,
                        IndentationLevel         = 0,
                        RelativeIndentationLevel = previousIndentationLevel,
                        ExistingIndentation      = nonWsChar,
                        FirstSpan                = placeholderSpan,
                    };
                }

                total += lineLength;
            }

            result.Indentations = indentations;

            return(result);
        }