diff --git a/src/Microsoft.CodeAnalysis.Razor.Workspaces/RazorDocumentExcerptService.cs b/src/Microsoft.CodeAnalysis.Razor.Workspaces/RazorDocumentExcerptService.cs index 6fe7d9b979..93f836221f 100644 --- a/src/Microsoft.CodeAnalysis.Razor.Workspaces/RazorDocumentExcerptService.cs +++ b/src/Microsoft.CodeAnalysis.Razor.Workspaces/RazorDocumentExcerptService.cs @@ -37,6 +37,16 @@ namespace Microsoft.CodeAnalysis.Razor ExcerptMode mode, CancellationToken cancellationToken) { + var result = await TryGetExcerptInternalAsync(document, span, (ExcerptModeInternal)mode, cancellationToken).ConfigureAwait(false); + return result?.ToExcerptResult(); + } + + public async Task TryGetExcerptInternalAsync( + Document document, + TextSpan span, + ExcerptModeInternal mode, + CancellationToken cancellationToken) + { if (_document == null) { return null; @@ -78,7 +88,8 @@ namespace Microsoft.CodeAnalysis.Razor // Now translate everything to be relative to the excerpt var offset = 0 - excerptSpan.Start; var excerptText = primaryText.GetSubText(excerptSpan); - excerptSpan = new TextSpan(excerptSpan.Start + offset, excerptSpan.Length); + excerptSpan = new TextSpan(0, excerptSpan.Length); + primarySpan = new TextSpan(primarySpan.Start + offset, primarySpan.Length); for (var i = 0; i < classifiedSpans.Count; i++) { @@ -89,24 +100,26 @@ namespace Microsoft.CodeAnalysis.Razor classifiedSpans[i] = new ClassifiedSpan(classifiedSpan.ClassificationType, updated); } - return new ExcerptResult(excerptText, excerptSpan, classifiedSpans.ToImmutable(), document, span); + return new ExcerptResultInternal(excerptText, primarySpan, classifiedSpans.ToImmutable(), document, span); } - - private TextSpan ChooseExcerptSpan(SourceText primaryText, TextSpan primarySpan, ExcerptMode mode) + + private TextSpan ChooseExcerptSpan(SourceText primaryText, TextSpan primarySpan, ExcerptModeInternal mode) { var startLine = primaryText.Lines.GetLineFromPosition(primarySpan.Start); var endLine = primaryText.Lines.GetLineFromPosition(primarySpan.End); - // If we're showing a single line then this will do. Otherwise expand the range by 1 in + // If we're showing a single line then this will do. Otherwise expand the range by 3 in // each direction (if possible). - if (mode == ExcerptMode.Tooltip && startLine.LineNumber > 0) + if (mode == ExcerptModeInternal.Tooltip) { - startLine = primaryText.Lines[startLine.LineNumber - 1]; + var index = Math.Max(startLine.LineNumber - 3, 0); + startLine = primaryText.Lines[index]; } - if (mode == ExcerptMode.Tooltip && endLine.LineNumber < primaryText.Lines.Count - 1) + if (mode == ExcerptModeInternal.Tooltip) { - endLine = primaryText.Lines[endLine.LineNumber + 1]; + var index = Math.Min(endLine.LineNumber + 3, primaryText.Lines.Count - 1); + endLine = primaryText.Lines[index]; } return new TextSpan(startLine.Start, endLine.End - startLine.Start); @@ -188,5 +201,45 @@ namespace Microsoft.CodeAnalysis.Razor return builder; } + + // We have IVT access to the Roslyn APIs for product code, but not for testing. + public enum ExcerptModeInternal + { + SingleLine = ExcerptMode.SingleLine, + Tooltip = ExcerptMode.Tooltip, + } + + // We have IVT access to the Roslyn APIs for product code, but not for testing. + public readonly struct ExcerptResultInternal + { + public readonly SourceText Content; + + public readonly TextSpan MappedSpan; + + public readonly ImmutableArray ClassifiedSpans; + + public readonly Document Document; + + public readonly TextSpan Span; + + public ExcerptResultInternal( + SourceText content, + TextSpan mappedSpan, + ImmutableArray classifiedSpans, + Document document, + TextSpan span) + { + Content = content; + MappedSpan = mappedSpan; + ClassifiedSpans = classifiedSpans; + Document = document; + Span = span; + } + + public ExcerptResult ToExcerptResult() + { + return new ExcerptResult(Content, MappedSpan, ClassifiedSpans, Document, Span); + } + } } } \ No newline at end of file diff --git a/test/Microsoft.CodeAnalysis.Razor.Workspaces.Test/RazorExcerptServiceTest.cs b/test/Microsoft.CodeAnalysis.Razor.Workspaces.Test/RazorExcerptServiceTest.cs index 29f7191bb2..92cced1968 100644 --- a/test/Microsoft.CodeAnalysis.Razor.Workspaces.Test/RazorExcerptServiceTest.cs +++ b/test/Microsoft.CodeAnalysis.Razor.Workspaces.Test/RazorExcerptServiceTest.cs @@ -13,6 +13,7 @@ using Microsoft.CodeAnalysis.Host; using Microsoft.CodeAnalysis.Razor.ProjectSystem; using Microsoft.CodeAnalysis.Text; using Xunit; +using static Microsoft.CodeAnalysis.Razor.RazorDocumentExcerptService; namespace Microsoft.CodeAnalysis.Razor { @@ -33,7 +34,7 @@ namespace Microsoft.CodeAnalysis.Razor } [Fact] - public async Task TryExcerptAsync_SingleLine_CanClassifyCSharp() + public async Task TryGetExcerptInternalAsync_SingleLine_CanClassifyCSharp() { // Arrange var (sourceText, primarySpan) = CreateText( @@ -53,13 +54,19 @@ namespace Microsoft.CodeAnalysis.Razor var secondarySpan = await GetSecondarySpanAsync(primary, primarySpan, secondary); // Act - var result = await service.TryExcerptAsync(secondary, secondarySpan, ExcerptMode.SingleLine, CancellationToken.None); + var result = await service.TryGetExcerptInternalAsync(secondary, secondarySpan, ExcerptModeInternal.SingleLine, CancellationToken.None); // Assert Assert.NotNull(result); Assert.Equal(secondarySpan, result.Value.Span); Assert.Same(secondary, result.Value.Document); + // Verifies that the right part of the primary document will be highlighted. + Assert.Equal( + (await secondary.GetTextAsync()).GetSubText(secondarySpan).ToString(), + result.Value.Content.GetSubText(result.Value.MappedSpan).ToString(), + ignoreLineEndingDifferences: true); + Assert.Equal(@" var foo = ""Hello, World!"";", result.Value.Content.ToString(), ignoreLineEndingDifferences: true); Assert.Collection( result.Value.ClassifiedSpans, @@ -91,7 +98,7 @@ namespace Microsoft.CodeAnalysis.Razor } [Fact] - public async Task TryExcerptAsync_SingleLine_CanClassifyCSharp_ImplicitExpression() + public async Task TryGetExcerptInternalAsync_SingleLine_CanClassifyCSharp_ImplicitExpression() { // Arrange var (sourceText, primarySpan) = CreateText( @@ -111,13 +118,19 @@ namespace Microsoft.CodeAnalysis.Razor var secondarySpan = await GetSecondarySpanAsync(primary, primarySpan, secondary); // Act - var result = await service.TryExcerptAsync(secondary, secondarySpan, ExcerptMode.SingleLine, CancellationToken.None); + var result = await service.TryGetExcerptInternalAsync(secondary, secondarySpan, ExcerptModeInternal.SingleLine, CancellationToken.None); // Assert Assert.NotNull(result); Assert.Equal(secondarySpan, result.Value.Span); Assert.Same(secondary, result.Value.Document); + // Verifies that the right part of the primary document will be highlighted. + Assert.Equal( + (await secondary.GetTextAsync()).GetSubText(secondarySpan).ToString(), + result.Value.Content.GetSubText(result.Value.MappedSpan).ToString(), + ignoreLineEndingDifferences: true); + Assert.Equal(@" @foo", result.Value.Content.ToString(), ignoreLineEndingDifferences: true); Assert.Collection( result.Value.ClassifiedSpans, @@ -139,7 +152,7 @@ namespace Microsoft.CodeAnalysis.Razor } [Fact] - public async Task TryExcerptAsync_SingleLine_CanClassifyCSharp_ComplexLine() + public async Task TryGetExcerptInternalAsync_SingleLine_CanClassifyCSharp_ComplexLine() { // Arrange var (sourceText, primarySpan) = CreateText( @@ -159,13 +172,19 @@ namespace Microsoft.CodeAnalysis.Razor var secondarySpan = await GetSecondarySpanAsync(primary, primarySpan, secondary); // Act - var result = await service.TryExcerptAsync(secondary, secondarySpan, ExcerptMode.SingleLine, CancellationToken.None); + var result = await service.TryGetExcerptInternalAsync(secondary, secondarySpan, ExcerptModeInternal.SingleLine, CancellationToken.None); // Assert Assert.NotNull(result); Assert.Equal(secondarySpan, result.Value.Span); Assert.Same(secondary, result.Value.Document); + // Verifies that the right part of the primary document will be highlighted. + Assert.Equal( + (await secondary.GetTextAsync()).GetSubText(secondarySpan).ToString(), + result.Value.Content.GetSubText(result.Value.MappedSpan).ToString(), + ignoreLineEndingDifferences: true); + Assert.Equal(@"
@(3 + 4)
@(foo + foo)
", result.Value.Content.ToString(), ignoreLineEndingDifferences: true); Assert.Collection( result.Value.ClassifiedSpans, @@ -217,7 +236,7 @@ namespace Microsoft.CodeAnalysis.Razor } [Fact] - public async Task TryExcerptAsync_MultiLine_CanClassifyCSharp() + public async Task TryGetExcerptInternalAsync_MultiLine_CanClassifyCSharp() { // Arrange var (sourceText, primarySpan) = CreateText( @@ -226,8 +245,8 @@ namespace Microsoft.CodeAnalysis.Razor @{ var |foo| = ""Hello, World!""; } - @foo -
@(3 + 4)
@(foo + foo)
+ +
"); @@ -237,17 +256,27 @@ namespace Microsoft.CodeAnalysis.Razor var secondarySpan = await GetSecondarySpanAsync(primary, primarySpan, secondary); // Act - var result = await service.TryExcerptAsync(secondary, secondarySpan, ExcerptMode.Tooltip, CancellationToken.None); + var result = await service.TryGetExcerptInternalAsync(secondary, secondarySpan, ExcerptModeInternal.Tooltip, CancellationToken.None); // Assert Assert.NotNull(result); Assert.Equal(secondarySpan, result.Value.Span); Assert.Same(secondary, result.Value.Document); + // Verifies that the right part of the primary document will be highlighted. Assert.Equal( -@"@{ + (await secondary.GetTextAsync()).GetSubText(secondarySpan).ToString(), + result.Value.Content.GetSubText(result.Value.MappedSpan).ToString(), + ignoreLineEndingDifferences: true); + + Assert.Equal( +@" + +@{ var foo = ""Hello, World!""; -}", +} + +
", result.Value.Content.ToString(), ignoreLineEndingDifferences: true); Assert.Collection( @@ -255,7 +284,12 @@ namespace Microsoft.CodeAnalysis.Razor c => { Assert.Equal(ClassificationTypeNames.Text, c.ClassificationType); - Assert.Equal("@{", result.Value.Content.GetSubText(c.TextSpan).ToString()); + Assert.Equal( +@" + +@{", + result.Value.Content.GetSubText(c.TextSpan).ToString(), + ignoreLineEndingDifferences: true); }, c => { @@ -285,12 +319,17 @@ namespace Microsoft.CodeAnalysis.Razor c => { Assert.Equal(ClassificationTypeNames.Text, c.ClassificationType); - Assert.Equal("}", result.Value.Content.GetSubText(c.TextSpan).ToString()); + Assert.Equal( +@"} + +
", + result.Value.Content.GetSubText(c.TextSpan).ToString(), + ignoreLineEndingDifferences: true); }); } [Fact] - public async Task TryExcerptAsync_MultiLine_Boundaries_CanClassifyCSharp() + public async Task TryGetExcerptInternalAsync_MultiLine_Boundaries_CanClassifyCSharp() { // Arrange var (sourceText, primarySpan) = CreateText(@"@{ var |foo| = ""Hello, World!""; }"); @@ -301,13 +340,19 @@ namespace Microsoft.CodeAnalysis.Razor var secondarySpan = await GetSecondarySpanAsync(primary, primarySpan, secondary); // Act - var result = await service.TryExcerptAsync(secondary, secondarySpan, ExcerptMode.Tooltip, CancellationToken.None); + var result = await service.TryGetExcerptInternalAsync(secondary, secondarySpan, ExcerptModeInternal.Tooltip, CancellationToken.None); // Assert Assert.NotNull(result); Assert.Equal(secondarySpan, result.Value.Span); Assert.Same(secondary, result.Value.Document); + // Verifies that the right part of the primary document will be highlighted. + Assert.Equal( + (await secondary.GetTextAsync()).GetSubText(secondarySpan).ToString(), + result.Value.Content.GetSubText(result.Value.MappedSpan).ToString(), + ignoreLineEndingDifferences: true); + Assert.Equal( @"@{ var foo = ""Hello, World!""; }", result.Value.Content.ToString(), ignoreLineEndingDifferences: true);