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."); } } }
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}."); } } } }
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); }
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); }
// 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); }
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); }
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); }