Move indentation service to VS.Editor.Razor and export it.

- Removed indentation service extensions and made `GetDesiredIndentation` understand `ITextSnapshot`s and `ITextLineSnapshot`.
- Updated our test `StringTextSnapshot` implementation to have the required features our indentation tests require.
- Updated indentation service tests to use new API.

#1762
This commit is contained in:
N. Taylor Mullen 2017-11-13 12:19:28 -08:00
parent 44a47182b2
commit de23788019
6 changed files with 96 additions and 127 deletions

View File

@ -1,19 +0,0 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System.Composition;
using Microsoft.CodeAnalysis.Host;
using Microsoft.CodeAnalysis.Host.Mef;
namespace Microsoft.CodeAnalysis.Razor
{
[Shared]
[ExportLanguageServiceFactory(typeof(RazorIndentationFactsService), RazorLanguage.Name, ServiceLayer.Default)]
internal class DefaultRazorIndentationFactsServiceFactory : ILanguageServiceFactory
{
public ILanguageService CreateLanguageService(HostLanguageServices languageServices)
{
return new DefaultRazorIndentationFactsService();
}
}
}

View File

@ -3,28 +3,38 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.ComponentModel.Composition;
using Microsoft.AspNetCore.Razor.Language; using Microsoft.AspNetCore.Razor.Language;
using Microsoft.AspNetCore.Razor.Language.Legacy; using Microsoft.AspNetCore.Razor.Language.Legacy;
using Microsoft.VisualStudio.Text;
using Span = Microsoft.AspNetCore.Razor.Language.Legacy.Span;
namespace Microsoft.CodeAnalysis.Razor namespace Microsoft.VisualStudio.Editor.Razor
{ {
[System.Composition.Shared]
[Export(typeof(RazorIndentationFactsService))]
internal class DefaultRazorIndentationFactsService : RazorIndentationFactsService internal class DefaultRazorIndentationFactsService : RazorIndentationFactsService
{ {
public override int? GetDesiredIndentation(RazorSyntaxTree syntaxTree, int previousLineEndIndex, Func<int, string> getLineContent, int indentSize, int tabSize) public override int? GetDesiredIndentation(
RazorSyntaxTree syntaxTree,
ITextSnapshot syntaxTreeSnapshot,
ITextSnapshotLine line,
int indentSize,
int tabSize)
{ {
if (syntaxTree == null) if (syntaxTree == null)
{ {
throw new ArgumentNullException(nameof(syntaxTree)); throw new ArgumentNullException(nameof(syntaxTree));
} }
if (previousLineEndIndex < 0) if (syntaxTreeSnapshot == null)
{ {
throw new ArgumentOutOfRangeException(nameof(previousLineEndIndex)); throw new ArgumentNullException(nameof(syntaxTreeSnapshot));
} }
if (getLineContent == null) if (line == null)
{ {
throw new ArgumentNullException(nameof(getLineContent)); throw new ArgumentNullException(nameof(line));
} }
if (indentSize < 0) if (indentSize < 0)
@ -37,6 +47,10 @@ namespace Microsoft.CodeAnalysis.Razor
throw new ArgumentOutOfRangeException(nameof(tabSize)); throw new ArgumentOutOfRangeException(nameof(tabSize));
} }
var previousLine = line.Snapshot.GetLineFromLineNumber(line.LineNumber - 1);
var trackingPoint = previousLine.Snapshot.CreateTrackingPoint(previousLine.End, PointTrackingMode.Negative);
var previousLineEndIndex = trackingPoint.GetPosition(syntaxTreeSnapshot);
var simulatedChange = new SourceChange(previousLineEndIndex, 0, string.Empty); var simulatedChange = new SourceChange(previousLineEndIndex, 0, string.Empty);
var owningSpan = LocateOwner(syntaxTree.Root, simulatedChange); var owningSpan = LocateOwner(syntaxTree.Root, simulatedChange);
if (owningSpan.Kind == SpanKindInternal.Code) if (owningSpan.Kind == SpanKindInternal.Code)
@ -80,8 +94,8 @@ namespace Microsoft.CodeAnalysis.Razor
// We can't rely on the syntax trees representation of the source document because partial parses may have mutated // 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 // 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. // operate on the current line representation as indicated by the provider.
var line = getLineContent(currentSpan.Start.LineIndex); var lineText = line.Snapshot.GetLineFromLineNumber(currentSpan.Start.LineIndex).GetText();
desiredIndentation = GetIndentLevelOfLine(line, tabSize) + indentSize; desiredIndentation = GetIndentLevelOfLine(lineText, tabSize) + indentSize;
} }
} }

View File

@ -1,14 +1,14 @@
// Copyright (c) .NET Foundation. All rights reserved. // 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. // 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.AspNetCore.Razor.Language;
using Microsoft.CodeAnalysis.Host; using Microsoft.CodeAnalysis.Host;
using Microsoft.VisualStudio.Text;
namespace Microsoft.CodeAnalysis.Razor namespace Microsoft.VisualStudio.Editor.Razor
{ {
public abstract class RazorIndentationFactsService : ILanguageService public abstract class RazorIndentationFactsService : ILanguageService
{ {
public abstract int? GetDesiredIndentation(RazorSyntaxTree syntaxTree, int previousLineEnd, Func<int, string> lineProvider, int indentSize, int tabSize); public abstract int? GetDesiredIndentation(RazorSyntaxTree syntaxTree, ITextSnapshot syntaxTreeSnapshot, ITextSnapshotLine line, int indentSize, int tabSize);
} }
} }

View File

@ -1,30 +0,0 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using Microsoft.AspNetCore.Razor.Language;
using Microsoft.VisualStudio.Text;
namespace Microsoft.CodeAnalysis.Razor
{
public static class RazorIndentationFactsServiceExtensions
{
public static int? GetDesiredIndentation(
this RazorIndentationFactsService 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<int, string> getLineContentDelegate = (lineIndex) => line.Snapshot.GetLineFromLineNumber(lineIndex).GetText();
return service.GetDesiredIndentation(syntaxTree, previousLineEnd, getLineContentDelegate, indentSize, tabSize);
}
}
}

View File

@ -77,7 +77,20 @@ namespace Microsoft.VisualStudio.Text
return matchingLine; return matchingLine;
} }
public ITrackingPoint CreateTrackingPoint(int position, PointTrackingMode trackingMode) => throw new NotImplementedException(); public ITextSnapshotLine GetLineFromLineNumber(int lineNumber)
{
if (lineNumber < 0 || lineNumber >= _lines.Count)
{
throw new ArgumentOutOfRangeException(nameof(lineNumber));
}
return _lines[lineNumber];
}
public ITrackingPoint CreateTrackingPoint(int position, PointTrackingMode trackingMode)
{
return new SnapshotTrackingPoint(position);
}
public ITrackingPoint CreateTrackingPoint(int position, PointTrackingMode trackingMode, TrackingFidelityMode trackingFidelity) => throw new NotImplementedException(); public ITrackingPoint CreateTrackingPoint(int position, PointTrackingMode trackingMode, TrackingFidelityMode trackingFidelity) => throw new NotImplementedException();
@ -89,8 +102,6 @@ namespace Microsoft.VisualStudio.Text
public ITrackingSpan CreateTrackingSpan(int start, int length, SpanTrackingMode trackingMode, TrackingFidelityMode trackingFidelity) => throw new NotImplementedException(); public ITrackingSpan CreateTrackingSpan(int start, int length, SpanTrackingMode trackingMode, TrackingFidelityMode trackingFidelity) => throw new NotImplementedException();
public ITextSnapshotLine GetLineFromLineNumber(int lineNumber) => throw new NotImplementedException();
public int GetLineNumberFromPosition(int position) => throw new NotImplementedException(); public int GetLineNumberFromPosition(int position) => throw new NotImplementedException();
public string GetText(VisualStudio.Text.Span span) => throw new NotImplementedException(); public string GetText(VisualStudio.Text.Span span) => throw new NotImplementedException();
@ -133,6 +144,30 @@ namespace Microsoft.VisualStudio.Text
} }
} }
private class SnapshotTrackingPoint : ITrackingPoint
{
private readonly int _position;
public SnapshotTrackingPoint(int position)
{
_position = position;
}
public ITextBuffer TextBuffer => throw new NotImplementedException();
public PointTrackingMode TrackingMode => throw new NotImplementedException();
public TrackingFidelityMode TrackingFidelity => throw new NotImplementedException();
public char GetCharacter(ITextSnapshot snapshot) => throw new NotImplementedException();
public SnapshotPoint GetPoint(ITextSnapshot snapshot) => throw new NotImplementedException();
public int GetPosition(ITextSnapshot snapshot) => _position;
public int GetPosition(ITextVersion version) => throw new NotImplementedException();
}
private class SnapshotLine : ITextSnapshotLine private class SnapshotLine : ITextSnapshotLine
{ {
private readonly string _contentWithLineBreak; private readonly string _contentWithLineBreak;
@ -153,7 +188,9 @@ namespace Microsoft.VisualStudio.Text
} }
Start = new SnapshotPoint(owner, start); Start = new SnapshotPoint(owner, start);
End = new SnapshotPoint(owner, start + _content.Length);
Snapshot = owner; Snapshot = owner;
LineNumber = (owner as StringTextSnapshot)._lines.Count;
} }
public ITextSnapshot Snapshot { get; } public ITextSnapshot Snapshot { get; }
@ -172,14 +209,14 @@ namespace Microsoft.VisualStudio.Text
public string GetTextIncludingLineBreak() => _contentWithLineBreak; public string GetTextIncludingLineBreak() => _contentWithLineBreak;
public int LineNumber => throw new NotImplementedException(); public int LineNumber { get; }
public SnapshotPoint End { get; }
public SnapshotSpan Extent => throw new NotImplementedException(); public SnapshotSpan Extent => throw new NotImplementedException();
public SnapshotSpan ExtentIncludingLineBreak => throw new NotImplementedException(); public SnapshotSpan ExtentIncludingLineBreak => throw new NotImplementedException();
public SnapshotPoint End => throw new NotImplementedException();
public SnapshotPoint EndIncludingLineBreak => throw new NotImplementedException(); public SnapshotPoint EndIncludingLineBreak => throw new NotImplementedException();
} }
} }

View File

@ -1,13 +1,13 @@
// Copyright (c) .NET Foundation. All rights reserved. // 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. // 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.Collections.Generic;
using System.Linq; using System.Linq;
using Microsoft.AspNetCore.Razor.Language; using Microsoft.AspNetCore.Razor.Language;
using Microsoft.VisualStudio.Text;
using Xunit; using Xunit;
namespace Microsoft.CodeAnalysis.Razor namespace Microsoft.VisualStudio.Editor.Razor
{ {
public class DefaultRazorIndentationFactsServiceTest public class DefaultRazorIndentationFactsServiceTest
{ {
@ -15,17 +15,17 @@ namespace Microsoft.CodeAnalysis.Razor
public void GetDesiredIndentation_ReturnsNull_IfOwningSpanIsCode() public void GetDesiredIndentation_ReturnsNull_IfOwningSpanIsCode()
{ {
// Arrange // Arrange
var source = $@" var source = new StringTextSnapshot($@"
@{{ @{{
"; ");
var syntaxTree = GetSyntaxTree(source); var syntaxTree = GetSyntaxTree(source);
var service = new DefaultRazorIndentationFactsService(); var service = new DefaultRazorIndentationFactsService();
// Act // Act
var indentation = service.GetDesiredIndentation( var indentation = service.GetDesiredIndentation(
syntaxTree, syntaxTree,
previousLineEndIndex: GetLineEndIndexForLine(source, 1), source,
getLineContent: line => GetLineContent(source, line), source.GetLineFromLineNumber(2),
indentSize: 4, indentSize: 4,
tabSize: 1); tabSize: 1);
@ -38,17 +38,17 @@ namespace Microsoft.CodeAnalysis.Razor
{ {
// Arrange // Arrange
var customDirective = DirectiveDescriptor.CreateSingleLineDirective("custom"); var customDirective = DirectiveDescriptor.CreateSingleLineDirective("custom");
var source = $@" var source = new StringTextSnapshot($@"
@custom @custom
"; ");
var syntaxTree = GetSyntaxTree(source, new[] { customDirective }); var syntaxTree = GetSyntaxTree(source, new[] { customDirective });
var service = new DefaultRazorIndentationFactsService(); var service = new DefaultRazorIndentationFactsService();
// Act // Act
var indentation = service.GetDesiredIndentation( var indentation = service.GetDesiredIndentation(
syntaxTree, syntaxTree,
previousLineEndIndex: GetLineEndIndexForLine(source, 1), source,
getLineContent: line => GetLineContent(source, line), source.GetLineFromLineNumber(2),
indentSize: 4, indentSize: 4,
tabSize: 1); tabSize: 1);
@ -60,17 +60,17 @@ namespace Microsoft.CodeAnalysis.Razor
public void GetDesiredIndentation_ReturnsCorrectIndentation_ForMarkupWithinCodeBlock() public void GetDesiredIndentation_ReturnsCorrectIndentation_ForMarkupWithinCodeBlock()
{ {
// Arrange // Arrange
var source = $@"@{{ var source = new StringTextSnapshot($@"@{{
<div> <div>
"; ");
var syntaxTree = GetSyntaxTree(source); var syntaxTree = GetSyntaxTree(source);
var service = new DefaultRazorIndentationFactsService(); var service = new DefaultRazorIndentationFactsService();
// Act // Act
var indentation = service.GetDesiredIndentation( var indentation = service.GetDesiredIndentation(
syntaxTree, syntaxTree,
previousLineEndIndex: GetLineEndIndexForLine(source, 1), source,
getLineContent: line => GetLineContent(source, line), source.GetLineFromLineNumber(2),
indentSize: 4, indentSize: 4,
tabSize: 4); tabSize: 4);
@ -83,18 +83,18 @@ namespace Microsoft.CodeAnalysis.Razor
{ {
// Arrange // Arrange
var customDirective = DirectiveDescriptor.CreateRazorBlockDirective("custom"); var customDirective = DirectiveDescriptor.CreateRazorBlockDirective("custom");
var source = $@"@custom var source = new StringTextSnapshot($@"@custom
{{ {{
<div> <div>
}}"; }}");
var syntaxTree = GetSyntaxTree(source, new[] { customDirective }); var syntaxTree = GetSyntaxTree(source, new[] { customDirective });
var service = new DefaultRazorIndentationFactsService(); var service = new DefaultRazorIndentationFactsService();
// Act // Act
var indentation = service.GetDesiredIndentation( var indentation = service.GetDesiredIndentation(
syntaxTree, syntaxTree,
previousLineEndIndex: GetLineEndIndexForLine(source, 2), source,
getLineContent: line => GetLineContent(source, line), source.GetLineFromLineNumber(3),
indentSize: 4, indentSize: 4,
tabSize: 4); tabSize: 4);
@ -106,21 +106,21 @@ namespace Microsoft.CodeAnalysis.Razor
public void GetDesiredIndentation_ReturnsCorrectIndentation_ForNestedMarkupWithinCodeBlock() public void GetDesiredIndentation_ReturnsCorrectIndentation_ForNestedMarkupWithinCodeBlock()
{ {
// Arrange // Arrange
var source = $@" var source = new StringTextSnapshot($@"
<div> <div>
@{{ @{{
<span> <span>
}} }}
</div> </div>
"; ");
var syntaxTree = GetSyntaxTree(source); var syntaxTree = GetSyntaxTree(source);
var service = new DefaultRazorIndentationFactsService(); var service = new DefaultRazorIndentationFactsService();
// Act // Act
var indentation = service.GetDesiredIndentation( var indentation = service.GetDesiredIndentation(
syntaxTree, syntaxTree,
previousLineEndIndex: GetLineEndIndexForLine(source, 3), source,
getLineContent: line => GetLineContent(source, line), source.GetLineFromLineNumber(4),
indentSize: 4, indentSize: 4,
tabSize: 4); tabSize: 4);
@ -133,20 +133,20 @@ namespace Microsoft.CodeAnalysis.Razor
{ {
// Arrange // Arrange
var customDirective = DirectiveDescriptor.CreateRazorBlockDirective("custom"); var customDirective = DirectiveDescriptor.CreateRazorBlockDirective("custom");
var source = $@"@custom var source = new StringTextSnapshot($@"@custom
{{ {{
@{{ @{{
<div> <div>
}} }}
}}"; }}");
var syntaxTree = GetSyntaxTree(source, new[] { customDirective }); var syntaxTree = GetSyntaxTree(source, new[] { customDirective });
var service = new DefaultRazorIndentationFactsService(); var service = new DefaultRazorIndentationFactsService();
// Act // Act
var indentation = service.GetDesiredIndentation( var indentation = service.GetDesiredIndentation(
syntaxTree, syntaxTree,
previousLineEndIndex: GetLineEndIndexForLine(source, 3), source,
getLineContent: line => GetLineContent(source, line), source.GetLineFromLineNumber(4),
indentSize: 4, indentSize: 4,
tabSize: 4); tabSize: 4);
@ -154,7 +154,7 @@ namespace Microsoft.CodeAnalysis.Razor
Assert.Equal(8, indentation); Assert.Equal(8, indentation);
} }
private static RazorSyntaxTree GetSyntaxTree(string source, IEnumerable<DirectiveDescriptor> directives = null) private static RazorSyntaxTree GetSyntaxTree(StringTextSnapshot source, IEnumerable<DirectiveDescriptor> directives = null)
{ {
directives = directives ?? Enumerable.Empty<DirectiveDescriptor>(); directives = directives ?? Enumerable.Empty<DirectiveDescriptor>();
var engine = RazorEngine.CreateDesignTime(builder => var engine = RazorEngine.CreateDesignTime(builder =>
@ -165,45 +165,12 @@ namespace Microsoft.CodeAnalysis.Razor
} }
}); });
var sourceDocument = RazorSourceDocument.Create(source, "test.cshtml"); var sourceDocument = RazorSourceDocument.Create(source.GetText(), "test.cshtml");
var codeDocument = RazorCodeDocument.Create(sourceDocument); var codeDocument = RazorCodeDocument.Create(sourceDocument);
engine.Process(codeDocument); engine.Process(codeDocument);
return codeDocument.GetSyntaxTree(); return codeDocument.GetSyntaxTree();
} }
private static string GetLineContent(string source, int lineIndex)
{
if (string.IsNullOrEmpty(source))
{
return string.Empty;
}
var lines = source.Split(new[] { Environment.NewLine }, StringSplitOptions.None);
return lines[lineIndex];
}
private static int GetLineEndIndexForLine(string source, int lineIndex)
{
var absoluteIndex = 0;
if (string.IsNullOrEmpty(source))
{
return absoluteIndex;
}
var lines = source.Split(new[] { Environment.NewLine }, StringSplitOptions.None);
for (var i = 0; i <= lineIndex; i++)
{
absoluteIndex += lines[i].Length;
if (i < lineIndex)
{
absoluteIndex += Environment.NewLine.Length;
}
}
return absoluteIndex;
}
} }
} }