diff --git a/src/Microsoft.VisualStudio.LanguageServices.Razor/DefaultRazorSyntaxFactsService.cs b/src/Microsoft.VisualStudio.LanguageServices.Razor/DefaultRazorSyntaxFactsService.cs index 3deb20b240..c971e967e6 100644 --- a/src/Microsoft.VisualStudio.LanguageServices.Razor/DefaultRazorSyntaxFactsService.cs +++ b/src/Microsoft.VisualStudio.LanguageServices.Razor/DefaultRazorSyntaxFactsService.cs @@ -7,6 +7,9 @@ using System.ComponentModel.Composition; using System.Linq; using Microsoft.AspNetCore.Razor.Evolution; using Microsoft.AspNetCore.Razor.Evolution.Legacy; +using Microsoft.VisualStudio.Text; +using Span = Microsoft.AspNetCore.Razor.Evolution.Legacy.Span; +using ITextBuffer = Microsoft.AspNetCore.Razor.Evolution.Legacy.ITextBuffer; namespace Microsoft.VisualStudio.LanguageServices.Razor { @@ -135,5 +138,296 @@ namespace Microsoft.VisualStudio.LanguageServices.Razor return results; } + + public override int? GetDesiredIndentation(RazorSyntaxTree syntaxTree, ITextSnapshot syntaxTreeSnapshot, ITextSnapshotLine line, int indentSize, int tabSize) + { + if (syntaxTree == null) + { + throw new ArgumentNullException(nameof(syntaxTree)); + } + + if (syntaxTreeSnapshot == null) + { + throw new ArgumentNullException(nameof(syntaxTreeSnapshot)); + } + + if (line == null) + { + throw new ArgumentNullException(nameof(line)); + } + + if (indentSize < 0) + { + throw new ArgumentOutOfRangeException(nameof(indentSize)); + } + + if (tabSize < 0) + { + throw new ArgumentOutOfRangeException(nameof(tabSize)); + } + + // The tricky thing here is that line.Snapshot is very likely newer + var previousLine = line.Snapshot.GetLineFromLineNumber(line.LineNumber - 1); + var trackingPoint = line.Snapshot.CreateTrackingPoint(line.End, PointTrackingMode.Negative); + var previousLineEnd = trackingPoint.GetPosition(syntaxTreeSnapshot); + + var razorBuffer = new ShimTextBufferAdapter(syntaxTreeSnapshot); + var simulatedChange = new TextChange(previousLineEnd, 0, razorBuffer, previousLineEnd, 0, razorBuffer); + var owningSpan = LocateOwner(syntaxTree.Root, simulatedChange); + + int? desiredIndentation = null; + + if (owningSpan.Kind != SpanKind.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 == SpanKind.MetaCode) + { + // yay! We want to use the start of this span to determine the indent level. + var startLine = line.Snapshot.GetLineFromLineNumber(curSpan.Start.LineIndex); + var extraIndent = 0; + + // Dev11 337312: Only indent one level deeper if the item after the metacode is a markup block + if (i < children.Count - 1) + { + SyntaxTreeNode nextChild = children[i + 1]; + if (nextChild.IsBlock && ((nextChild as Block).Type == BlockKind.Markup)) + { + extraIndent = indentSize; + } + } + + desiredIndentation = GetIndentLevelOfLine(startLine, tabSize) + indentSize; + } + } + + if (curChild == owningChild) + { + break; + } + } + + owningChild = owningParent; + } + } + + return desiredIndentation; + } + + private Span LocateOwner(Block root, TextChange change) + { + // Ask each child recursively + Span owner = null; + foreach (SyntaxTreeNode element in root.Children) + { + if (element.Start.AbsoluteIndex > change.OldPosition) + { + // too far + break; + } + + int elementLen = element.Length; + if (element.Start.AbsoluteIndex + elementLen < change.OldPosition) + { + // not far enough + continue; + } + + if (element.IsBlock) + { + Block block = element as Block; + + if (element.Start.AbsoluteIndex + elementLen == change.OldPosition) + { + 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.OldPosition) && + (sourceStartTag.Start.AbsoluteIndex + sourceStartTag.Length >= change.OldPosition)) + { + // intersects the start tag + return LocateOwner(sourceStartTag, change); + } + else if ((sourceEndTag.Start.AbsoluteIndex <= change.OldPosition) && + (sourceEndTag.Start.AbsoluteIndex + sourceEndTag.Length >= change.OldPosition)) + { + // intersects the end tag + return LocateOwner(sourceEndTag, change); + } + } + } + + return owner; + } + + private int GetIndentLevelOfLine(ITextSnapshotLine line, int tabSize) + { + var indentLevel = 0; + + foreach (var c in line.GetText()) + { + if (!char.IsWhiteSpace(c)) + { + break; + } + else if (c == '\t') + { + indentLevel += tabSize; + } + else + { + indentLevel++; + } + } + + return indentLevel; + } + + private class ShimTextBufferAdapter : ITextBuffer + { + public ITextSnapshot Snapshot { get; private set; } + private int _position; + private string _cachedText; + private int _cachedPos; + + public ShimTextBufferAdapter(ITextSnapshot snapshot) + { + Snapshot = snapshot; + _cachedPos = -1; + } + + #region IRazorTextBuffer + + int ITextBuffer.Length + { + get { return Length; } + } + + int ITextBuffer.Position + { + get { return _position; } + set { _position = value; } + } + + int ITextBuffer.Read() + { + return Read(); + } + + int ITextBuffer.Peek() + { + return Peek(); + } + + #endregion + + #region private methods + + private int Length + { + get { return Snapshot.Length; } + } + + private int Read() + { + if (_position >= Snapshot.Length) + { + return -1; + } + + int readVal = ReadChar(); + _position = _position + 1; + + return readVal; + } + + private int Peek() + { + if (_position >= Snapshot.Length) + { + return -1; + } + + return ReadChar(); + } + + private int ReadChar() + { + if ((_cachedPos < 0) || (_position < _cachedPos) || (_position >= _cachedPos + _cachedText.Length)) + { + _cachedPos = _position; + int cachedLen = Math.Min(1024, Snapshot.Length - _cachedPos); + _cachedText = Snapshot.GetText(_cachedPos, cachedLen); + } + + return _cachedText[_position - _cachedPos]; + } + + #endregion + } } } diff --git a/src/Microsoft.VisualStudio.LanguageServices.Razor/RazorSyntaxFactsService.cs b/src/Microsoft.VisualStudio.LanguageServices.Razor/RazorSyntaxFactsService.cs index cdee67db91..3f539877f8 100644 --- a/src/Microsoft.VisualStudio.LanguageServices.Razor/RazorSyntaxFactsService.cs +++ b/src/Microsoft.VisualStudio.LanguageServices.Razor/RazorSyntaxFactsService.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using Microsoft.AspNetCore.Razor.Evolution; +using Microsoft.VisualStudio.Text; namespace Microsoft.VisualStudio.LanguageServices.Razor { @@ -11,5 +12,7 @@ namespace Microsoft.VisualStudio.LanguageServices.Razor 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); } }