Add definitions for Document Services

This change adds mock ups of the interfaces that we've been designing as
part of Razor FAR as well as the implementations. This isn't wired up to
anything yet in this PR, but the basic functionality here is stable
enough for us to stabilize and review.

For now we have the interface definitions in the Razor codebase until a
build of Roslyn is available with these definitions + IVT for us to use
them.
This commit is contained in:
Ryan Nowak 2018-10-20 16:35:41 -07:00
parent baa71375d0
commit 81904f579a
26 changed files with 1317 additions and 253 deletions

View File

@ -0,0 +1,29 @@
// 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 System.Composition;
using Microsoft.CodeAnalysis.Host;
using Microsoft.CodeAnalysis.Razor.ProjectSystem;
namespace Microsoft.CodeAnalysis.Razor
{
[Export(typeof(DocumentServiceProviderFactory))]
internal class DefaultDocumentServiceProviderFactory : DocumentServiceProviderFactory
{
public override IDocumentServiceProvider Create(DocumentSnapshot document)
{
if (document == null)
{
throw new ArgumentNullException(nameof(document));
}
return new RazorDocumentServiceProvider(document);
}
public override IDocumentServiceProvider CreateEmpty()
{
return new RazorDocumentServiceProvider();
}
}
}

View File

@ -0,0 +1,15 @@
// 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 Microsoft.CodeAnalysis.Host;
using Microsoft.CodeAnalysis.Razor.ProjectSystem;
namespace Microsoft.CodeAnalysis.Razor
{
internal abstract class DocumentServiceProviderFactory
{
public abstract IDocumentServiceProvider CreateEmpty();
public abstract IDocumentServiceProvider Create(DocumentSnapshot document);
}
}

View File

@ -1,12 +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.
// Temporary code until this gets merged into Roslyn
#if DOCUMENT_SERVICE_FACTORY
namespace Microsoft.CodeAnalysis.Experiment
{
public interface IDocumentServiceFactory
{
}
}
#endif

View File

@ -1,12 +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.
// Temporary code until this gets merged into Roslyn
#if DOCUMENT_SERVICE_FACTORY
namespace Microsoft.CodeAnalysis.Experiment
{
public interface ISpanMapper
{
}
}
#endif

View File

@ -1,18 +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.
// Temporary code until this gets merged into Roslyn
#if DOCUMENT_SERVICE_FACTORY
using Microsoft.CodeAnalysis.Text;
namespace Microsoft.CodeAnalysis.Experiment
{
public class SpanMapResult
{
public SpanMapResult(Document document, LinePositionSpan linePositionSpan)
{
}
}
}
#endif

View File

@ -0,0 +1,79 @@
// Copyright (c) Microsoft. All Rights Reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
#if DOCUMENT_SERVICE_FACTORY
using System.Collections.Immutable;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.Classification;
using Microsoft.CodeAnalysis.Text;
namespace Microsoft.CodeAnalysis.Host
{
/// <summary>
/// excerpt some part of <see cref="Document"/>
/// </summary>
internal interface IDocumentExcerptService : IDocumentService
{
/// <summary>
/// return <see cref="ExcerptResult"/> of given <see cref="Document"/> and <see cref="TextSpan"/>
///
/// the result might not be an exact copy of the given source or contains more then given span
/// </summary>
Task<ExcerptResult?> TryExcerptAsync(Document document, TextSpan span, ExcerptMode mode, CancellationToken cancellationToken);
}
/// <summary>
/// this mode shows intention not actual behavior. it is up to implementation how to interpret the intention.
/// </summary>
internal enum ExcerptMode
{
SingleLine,
Tooltip
}
/// <summary>
/// Result of excerpt
/// </summary>
internal struct ExcerptResult
{
/// <summary>
/// excerpt content
/// </summary>
public readonly SourceText Content;
/// <summary>
/// span on <see cref="Content"/> that given <see cref="Span"/> got mapped to
/// </summary>
public readonly TextSpan MappedSpan;
/// <summary>
/// classification information on the <see cref="Content"/>
/// </summary>
public readonly ImmutableArray<ClassifiedSpan> ClassifiedSpans;
/// <summary>
/// <see cref="Document"/> this excerpt is from
/// </summary>
public readonly Document Document;
/// <summary>
/// span on <see cref="Document"/> this excerpt is from
/// </summary>
public readonly TextSpan Span;
public ExcerptResult(SourceText content, TextSpan mappedSpan, ImmutableArray<ClassifiedSpan> classifiedSpans, Document document, TextSpan span)
{
Content = content;
MappedSpan = mappedSpan;
ClassifiedSpans = classifiedSpans;
// these 2 might not actually needed
Document = document;
Span = span;
}
}
}
#endif

View File

@ -0,0 +1,27 @@
// Copyright (c) Microsoft. All Rights Reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
#if DOCUMENT_SERVICE_FACTORY
namespace Microsoft.CodeAnalysis.Host
{
/// <summary>
/// provide various operations for this document
///
/// I followed name from EditorOperation for now.
/// </summary>
internal interface IDocumentOperationService : IDocumentService
{
/// <summary>
/// document version of <see cref="Workspace.CanApplyChange(ApplyChangesKind)"/>
/// </summary>
bool CanApplyChange { get; }
/// <summary>
/// indicates whether this document supports diagnostics or not
/// </summary>
bool SupportDiagnostics { get; }
}
}
#endif

View File

@ -0,0 +1,16 @@
// Copyright (c) Microsoft. All Rights Reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
#if DOCUMENT_SERVICE_FACTORY
namespace Microsoft.CodeAnalysis.Host
{
/// <summary>
/// Empty interface just to mark document services.
/// </summary>
internal interface IDocumentService
{
}
}
#endif

View File

@ -0,0 +1,18 @@
// Copyright (c) Microsoft. All Rights Reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
#if DOCUMENT_SERVICE_FACTORY
namespace Microsoft.CodeAnalysis.Host
{
internal interface IDocumentServiceProvider
{
/// <summary>
/// Gets a document specific service provided by the host identified by the service type.
/// If the host does not provide the service, this method returns null.
/// </summary>
TService GetService<TService>() where TService : class, IDocumentService;
}
}
#endif

View File

@ -0,0 +1,70 @@
// Copyright (c) Microsoft. All Rights Reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
#if DOCUMENT_SERVICE_FACTORY
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.Text;
namespace Microsoft.CodeAnalysis.Host
{
/// <summary>
/// Map spans in a document to other spans even in other document
///
/// this will be used by various features if provided to convert span in one document to other spans.
///
/// for example, it is used to show spans users expect in a razor file rather than spans in
/// auto generated file that is implementation detail or navigate to the right place rather
/// than the generated file and etc.
/// </summary>
internal interface ISpanMappingService : IDocumentService
{
/// <summary>
/// Map spans in the document to more appropriate locations
///
/// in current design, this can NOT map a span to a span that is not backed by a file.
/// for example, roslyn supports someone to have a document that is not backed by a file. and current design doesn't allow
/// such document to be returned from this API
/// for example, span on razor secondary buffer document in roslyn solution mapped to a span on razor cshtml file is possible but
/// a span on razor cshtml file to a span on secondary buffer document is not possible since secondary buffer document is not backed by a file
/// </summary>
/// <param name="document">Document given spans belong to</param>
/// <param name="spans">Spans in the document</param>
/// <param name="cancellationToken">Cancellation token</param>
/// <returns>Return mapped span. order of result should be same as the given span</returns>
Task<ImmutableArray<MappedSpanResult>> MapSpansAsync(Document document, IEnumerable<TextSpan> spans, CancellationToken cancellationToken);
}
/// <summary>
/// Result of span mapping
/// </summary>
internal struct MappedSpanResult
{
/// <summary>
/// Path to mapped file
/// </summary>
public readonly string FilePath;
/// <summary>
/// LinePosition representation of the Span
/// </summary>
public readonly LinePositionSpan LinePositionSpan;
/// <summary>
/// Mapped span
/// </summary>
public readonly TextSpan Span;
public MappedSpanResult(string filePath, LinePositionSpan linePositionSpan, TextSpan span)
{
FilePath = filePath;
LinePositionSpan = linePositionSpan;
Span = span;
}
}
}
#endif

View File

@ -23,11 +23,11 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem
throw new ArgumentNullException(nameof(state));
}
Project = project;
ProjectInternal = project;
State = state;
}
public DefaultProjectSnapshot Project { get; }
public DefaultProjectSnapshot ProjectInternal { get; }
public DocumentState State { get; }
@ -35,6 +35,8 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem
public override string TargetPath => State.HostDocument.TargetPath;
public override ProjectSnapshot Project => ProjectInternal;
public override IReadOnlyList<DocumentSnapshot> GetImports()
{
return State.Imports.GetImports(Project, this);

View File

@ -104,6 +104,8 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem
public override string TargetPath => null;
public override ProjectSnapshot Project => _project;
public override Task<RazorCodeDocument> GetGeneratedOutputAsync()
{
return _generatedOutput.GetGeneratedOutputInitializationTask(_project, this);

View File

@ -14,6 +14,8 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem
public abstract string TargetPath { get; }
public abstract ProjectSnapshot Project { get; }
public abstract IReadOnlyList<DocumentSnapshot> GetImports();
public abstract Task<SourceText> GetTextAsync();

View File

@ -0,0 +1,26 @@
// 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.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.Text;
namespace Microsoft.CodeAnalysis.Razor.ProjectSystem
{
internal class EmptyTextLoader : TextLoader
{
private readonly string _filePath;
private readonly VersionStamp _version;
public EmptyTextLoader(string filePath)
{
_filePath = filePath;
_version = VersionStamp.Create(); // Version will never change so this can be reused.
}
public override Task<TextAndVersion> LoadTextAndVersionAsync(Workspace workspace, DocumentId documentId, CancellationToken cancellationToken)
{
return Task.FromResult(TextAndVersion.Create(SourceText.From(""), _version, _filePath));
}
}
}

View File

@ -2,18 +2,13 @@
// 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.Immutable;
using System.Diagnostics;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Razor.Language;
using Microsoft.CodeAnalysis.Experiment;
using Microsoft.CodeAnalysis.Text;
namespace Microsoft.CodeAnalysis.Razor.ProjectSystem
{
internal class GeneratedCodeContainer : IDocumentServiceFactory, ISpanMapper
internal class GeneratedCodeContainer
{
public event EventHandler<TextChangeEventArgs> GeneratedCodeChanged;
@ -86,16 +81,6 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem
}
}
public TService GetService<TService>()
{
if (this is TService service)
{
return service;
}
return default(TService);
}
public void SetOutput(RazorCSharpDocument csharpDocument, DefaultDocumentSnapshot document)
{
lock (_setOutputLock)
@ -128,73 +113,6 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem
}
}
public Task<ImmutableArray<SpanMapResult>> MapSpansAsync(
Document document,
IEnumerable<TextSpan> spans,
CancellationToken cancellationToken)
{
RazorCSharpDocument output;
SourceText source;
lock (_setOutputLock)
{
if (Output == null)
{
return Task.FromResult(ImmutableArray<SpanMapResult>.Empty);
}
output = Output;
source = Source;
}
var results = ImmutableArray.CreateBuilder<SpanMapResult>();
foreach (var span in spans)
{
if (TryGetLinePositionSpan(span, source, output, out var linePositionSpan))
{
results.Add(new SpanMapResult(document, linePositionSpan));
}
else
{
results.Add(null);
}
}
return Task.FromResult(results.ToImmutable());
}
// Internal for testing.
internal static bool TryGetLinePositionSpan(TextSpan span, SourceText source, RazorCSharpDocument output, out LinePositionSpan linePositionSpan)
{
var mappings = output.SourceMappings;
for (var i = 0; i < mappings.Count; i++)
{
var mapping = mappings[i];
var original = mapping.OriginalSpan.AsTextSpan();
var generated = mapping.GeneratedSpan.AsTextSpan();
if (!generated.Contains(span))
{
// If the search span isn't contained within the generated span, it is not a match.
// A C# identifier won't cover multiple generated spans.
continue;
}
var leftOffset = span.Start - generated.Start;
var rightOffset = span.End - generated.End;
Debug.Assert(leftOffset >= 0);
Debug.Assert(rightOffset <= 0);
// Note: we don't handle imports here - the assumption is that for all of the scenarios we
// support, the span is in the original source document.
var adjusted = new TextSpan(original.Start + leftOffset, (original.End + rightOffset) - (original.Start + leftOffset));
linePositionSpan = source.Lines.GetLinePositionSpan(adjusted);
return true;
}
linePositionSpan = default;
return false;
}
private void TextContainer_TextChanged(object sender, TextChangeEventArgs args)
{
GeneratedCodeChanged?.Invoke(this, args);

View File

@ -0,0 +1,36 @@
// 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 System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Razor.Language;
using Microsoft.CodeAnalysis.Text;
namespace Microsoft.CodeAnalysis.Razor.ProjectSystem
{
internal class GeneratedOutputTextLoader : TextLoader
{
private readonly DocumentSnapshot _document;
private readonly string _filePath;
private readonly VersionStamp _version;
public GeneratedOutputTextLoader(DocumentSnapshot document, string filePath)
{
if (document == null)
{
throw new ArgumentNullException(nameof(document));
}
_document = document;
_filePath = filePath;
_version = VersionStamp.Create(); // Version will never change so this can be reused.
}
public override async Task<TextAndVersion> LoadTextAndVersionAsync(Workspace workspace, DocumentId documentId, CancellationToken cancellationToken)
{
var output = await _document.GetGeneratedOutputAsync().ConfigureAwait(false);
return TextAndVersion.Create(SourceText.From(output.GetCSharpDocument().GeneratedCode), _version, _filePath);
}
}
}

View File

@ -0,0 +1,192 @@
// 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 System.Collections.Generic;
using System.Collections.Immutable;
using System.Diagnostics;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Razor.Language;
using Microsoft.CodeAnalysis.Classification;
using Microsoft.CodeAnalysis.Host;
using Microsoft.CodeAnalysis.Razor.ProjectSystem;
using Microsoft.CodeAnalysis.Text;
namespace Microsoft.CodeAnalysis.Razor
{
internal class RazorDocumentExcerptService : IDocumentExcerptService
{
private readonly DocumentSnapshot _document;
private readonly ISpanMappingService _mapper;
public RazorDocumentExcerptService(DocumentSnapshot document, ISpanMappingService mapper)
{
if (mapper == null)
{
throw new ArgumentNullException(nameof(mapper));
}
_document = document;
_mapper = mapper;
}
public async Task<ExcerptResult?> TryExcerptAsync(
Document document,
TextSpan span,
ExcerptMode mode,
CancellationToken cancellationToken)
{
if (_document == null)
{
return null;
}
var mapped = await _mapper.MapSpansAsync(document, new[] { span }, cancellationToken).ConfigureAwait(false);
if (mapped.Length == 0 || mapped[0].Equals(default(MappedSpanResult)))
{
return null;
}
var project = _document.Project;
var primaryDocument = project.GetDocument(mapped[0].FilePath);
if (primaryDocument == null)
{
return null;
}
var primaryText = await primaryDocument.GetTextAsync().ConfigureAwait(false);
var primarySpan = primaryText.Lines.GetTextSpan(mapped[0].LinePositionSpan);
var secondaryDocument = document;
var secondarySpan = span;
// First compute the range of text we want to we to display relative to the primary document.
var excerptSpan = ChooseExcerptSpan(primaryText, primarySpan, mode);
// Then we'll classify the spans based on the primary document, since that's the coordinate
// space that our output mappings use.
var output = await _document.GetGeneratedOutputAsync().ConfigureAwait(false);
var mappings = output.GetCSharpDocument().SourceMappings;
var classifiedSpans = await ClassifyPreviewAsync(
primaryText,
excerptSpan,
secondaryDocument,
mappings,
cancellationToken).ConfigureAwait(false);
// 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);
for (var i = 0; i < classifiedSpans.Count; i++)
{
var classifiedSpan = classifiedSpans[i];
var updated = new TextSpan(classifiedSpan.TextSpan.Start + offset, classifiedSpan.TextSpan.Length);
Debug.Assert(excerptSpan.Contains(updated));
classifiedSpans[i] = new ClassifiedSpan(classifiedSpan.ClassificationType, updated);
}
return new ExcerptResult(excerptText, excerptSpan, classifiedSpans.ToImmutable(), document, span);
}
private TextSpan ChooseExcerptSpan(SourceText primaryText, TextSpan primarySpan, ExcerptMode 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
// each direction (if possible).
if (mode == ExcerptMode.Tooltip && startLine.LineNumber > 0)
{
startLine = primaryText.Lines[startLine.LineNumber - 1];
}
if (mode == ExcerptMode.Tooltip && endLine.LineNumber < primaryText.Lines.Count - 1)
{
endLine = primaryText.Lines[endLine.LineNumber + 1];
}
return new TextSpan(startLine.Start, endLine.End - startLine.Start);
}
private async Task<ImmutableArray<ClassifiedSpan>.Builder> ClassifyPreviewAsync(
SourceText primaryText,
TextSpan excerptSpan,
Document secondaryDocument,
IReadOnlyList<SourceMapping> mappings,
CancellationToken cancellationToken)
{
var builder = ImmutableArray.CreateBuilder<ClassifiedSpan>();
var sorted = new List<SourceMapping>(mappings);
sorted.Sort((x, y) => x.OriginalSpan.AbsoluteIndex.CompareTo(y.OriginalSpan.AbsoluteIndex));
// The algorithm here is to iterate through the source mappings (sorted) and use the C# classifier
// on the spans that are known to the C#. For the spans that are not known to be C# then
// we just treat them as text since we'd don't currently have our own classifications.
var remainingSpan = excerptSpan;
for (var i = 0; i < sorted.Count && excerptSpan.Length > 0; i++)
{
var primarySpan = sorted[i].OriginalSpan.AsTextSpan();
var intersection = primarySpan.Intersection(remainingSpan);
if (intersection == null)
{
// This span is outside the area we're interested in.
continue;
}
// OK this span intersects with the excerpt span, so we will process it. Let's compute
// the secondary span that matches the intersection.
var secondarySpan = sorted[i].GeneratedSpan.AsTextSpan();
secondarySpan = new TextSpan(secondarySpan.Start + intersection.Value.Start - primarySpan.Start, intersection.Value.Length);
primarySpan = intersection.Value;
if (remainingSpan.Start < primarySpan.Start)
{
// The position is before the next C# span. Classify everything up to the C# start
// as text.
builder.Add(new ClassifiedSpan(ClassificationTypeNames.Text, new TextSpan(remainingSpan.Start, primarySpan.Start - remainingSpan.Start)));
// Advance to the start of the C# span.
remainingSpan = new TextSpan(primarySpan.Start, remainingSpan.Length - (primarySpan.Start - remainingSpan.Start));
}
// We should be able to process this whole span as C#, so classify it.
//
// However, we'll have to translate it to the the secondary document's coordinates to do that.
Debug.Assert(remainingSpan.Contains(primarySpan) && remainingSpan.Start == primarySpan.Start);
var classifiedSecondarySpans = await Classifier.GetClassifiedSpansAsync(
secondaryDocument,
secondarySpan,
cancellationToken);
// Now we have to translate back to the primary document's coordinates.
var offset = primarySpan.Start - secondarySpan.Start;
foreach (var classifiedSecondarySpan in classifiedSecondarySpans)
{
Debug.Assert(secondarySpan.Contains(classifiedSecondarySpan.TextSpan));
var updated = new TextSpan(classifiedSecondarySpan.TextSpan.Start + offset, classifiedSecondarySpan.TextSpan.Length);
Debug.Assert(primarySpan.Contains(updated));
builder.Add(new ClassifiedSpan(classifiedSecondarySpan.ClassificationType, updated));
}
remainingSpan = new TextSpan(primarySpan.End, remainingSpan.Length - primarySpan.Length);
}
// Deal with residue
if (remainingSpan.Length > 0)
{
// Trailing Razor/markup text.
builder.Add(new ClassifiedSpan(ClassificationTypeNames.Text, remainingSpan));
}
return builder;
}
}
}

View File

@ -0,0 +1,71 @@
// 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.CodeAnalysis.Razor;
using Microsoft.CodeAnalysis.Razor.ProjectSystem;
namespace Microsoft.CodeAnalysis.Host
{
internal class RazorDocumentServiceProvider : IDocumentServiceProvider, IDocumentOperationService
{
private readonly DocumentSnapshot _document;
private readonly object _lock;
private RazorSpanMappingService _spanMappingService;
private RazorDocumentExcerptService _excerptService;
public RazorDocumentServiceProvider()
: this(null)
{
}
public RazorDocumentServiceProvider(DocumentSnapshot document)
{
_document = document;
_lock = new object();
}
public bool CanApplyChange => false;
public bool SupportDiagnostics => false;
public TService GetService<TService>() where TService : class, IDocumentService
{
if (typeof(TService) == typeof(ISpanMappingService))
{
if (_spanMappingService == null)
{
lock (_lock)
{
if (_spanMappingService == null)
{
_spanMappingService = new RazorSpanMappingService(_document);
}
}
}
return (TService)(object)_spanMappingService;
}
if (typeof(TService) == typeof(IDocumentExcerptService))
{
if (_excerptService == null)
{
lock (_lock)
{
if (_excerptService == null)
{
_excerptService = new RazorDocumentExcerptService(_document, GetService<ISpanMappingService>());
}
}
}
return (TService)(object)_excerptService;
}
return this as TService;
}
}
}

View File

@ -0,0 +1,102 @@
// 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 System.Collections.Generic;
using System.Collections.Immutable;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Razor.Language;
using Microsoft.CodeAnalysis.Host;
using Microsoft.CodeAnalysis.Razor.ProjectSystem;
using Microsoft.CodeAnalysis.Text;
namespace Microsoft.CodeAnalysis.Razor
{
internal class RazorSpanMappingService: ISpanMappingService
{
private readonly DocumentSnapshot _document;
public RazorSpanMappingService(DocumentSnapshot document)
{
if (document == null)
{
throw new ArgumentNullException(nameof(document));
}
_document = document;
}
public async Task<ImmutableArray<MappedSpanResult>> MapSpansAsync(
Document document,
IEnumerable<TextSpan> spans,
CancellationToken cancellationToken)
{
if (document == null)
{
throw new ArgumentNullException(nameof(document));
}
if (spans == null)
{
throw new ArgumentNullException(nameof(spans));
}
// Called on an uninitialized document.
if (_document == null)
{
return ImmutableArray.Create<MappedSpanResult>();
}
var source = await _document.GetTextAsync().ConfigureAwait(false);
var output = await _document.GetGeneratedOutputAsync().ConfigureAwait(false);
var results = ImmutableArray.CreateBuilder<MappedSpanResult>();
foreach (var span in spans)
{
if (TryGetLinePositionSpan(span, source, output.GetCSharpDocument(), out var linePositionSpan))
{
results.Add(new MappedSpanResult(output.Source.FilePath, linePositionSpan, span));
}
else
{
results.Add(default);
}
}
return results.ToImmutable();
}
// Internal for testing.
internal static bool TryGetLinePositionSpan(TextSpan span, SourceText source, RazorCSharpDocument output, out LinePositionSpan linePositionSpan)
{
var mappings = output.SourceMappings;
for (var i = 0; i < mappings.Count; i++)
{
var mapping = mappings[i];
var original = mapping.OriginalSpan.AsTextSpan();
var generated = mapping.GeneratedSpan.AsTextSpan();
if (!generated.Contains(span))
{
// If the search span isn't contained within the generated span, it is not a match.
// A C# identifier won't cover multiple generated spans.
continue;
}
var leftOffset = span.Start - generated.Start;
var rightOffset = span.End - generated.End;
if (leftOffset >= 0 && rightOffset <= 0)
{
// This span mapping contains the span.
var adjusted = new TextSpan(original.Start + leftOffset, (original.End + rightOffset) - (original.Start + leftOffset));
linePositionSpan = source.Lines.GetLinePositionSpan(adjusted);
return true;
}
}
linePositionSpan = default;
return false;
}
}
}

View File

@ -8,8 +8,6 @@ using System;
using System.Collections.Generic;
using System.ComponentModel.Composition;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Experiment;
using Microsoft.CodeAnalysis.Text;
using Microsoft.VisualStudio.LanguageServices.Implementation.TaskList;
using IWorkspaceProjectContextFactory = Microsoft.VisualStudio.LanguageServices.ProjectSystem.IWorkspaceProjectContextFactory2;
@ -56,11 +54,7 @@ namespace Microsoft.VisualStudio.LanguageServices.ProjectSystem
{
}
public void AddSourceFile(string filePath, bool isInCurrentContext = true, IEnumerable<string> folderNames = null, SourceCodeKind sourceCodeKind = SourceCodeKind.Regular, IDocumentServiceFactory documentServiceFactory = null)
{
}
public void AddSourceFile(string filePath, SourceTextContainer container, bool isInCurrentContext = true, IEnumerable<string> folderNames = null, SourceCodeKind sourceCodeKind = SourceCodeKind.Regular, IDocumentServiceFactory documentServiceFactory = null)
public void AddDynamicSourceFile(string filePath, IEnumerable<string> folderNames = null)
{
}
@ -88,6 +82,11 @@ namespace Microsoft.VisualStudio.LanguageServices.ProjectSystem
{
}
public void RemoveDynamicSourceFile(string filePath)
{
}
public void SetOptions(string commandLineForOptions)
{
}

View File

@ -7,7 +7,6 @@
using System;
using System.Collections.Generic;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Experiment;
using Microsoft.CodeAnalysis.Text;
namespace Microsoft.VisualStudio.LanguageServices.ProjectSystem
@ -33,11 +32,10 @@ namespace Microsoft.VisualStudio.LanguageServices.ProjectSystem
void RemoveAnalyzerReference(string referencePath);
// Files.
void AddSourceFile(string filePath, bool isInCurrentContext, IEnumerable<string> folderNames, SourceCodeKind sourceCodeKind); // This overload just for binary compat with existing code
void AddSourceFile(string filePath, bool isInCurrentContext = true, IEnumerable<string> folderNames = null, SourceCodeKind sourceCodeKind = SourceCodeKind.Regular, IDocumentServiceFactory documentServiceFactory = null);
void AddSourceFile(string filePath, SourceTextContainer container, bool isInCurrentContext = true, IEnumerable<string> folderNames = null, SourceCodeKind sourceCodeKind = SourceCodeKind.Regular, IDocumentServiceFactory documentServiceFactory = null);
void AddSourceFile(string filePath, bool isInCurrentContext, IEnumerable<string> folderNames, SourceCodeKind sourceCodeKind);
void AddDynamicSourceFile(string filePath, IEnumerable<string> folderNames = null);
void RemoveSourceFile(string filePath);
void RemoveDynamicSourceFile(string filePath);
void AddAdditionalFile(string filePath, bool isInCurrentContext = true);
void RemoveAdditionalFile(string filePath);
void SetRuleSetFile(string filePath);

View File

@ -302,7 +302,7 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem
}
projectManager.DocumentAdded(_current, document, new FileTextLoader(document.FilePath, null));
_projectContext?.AddSourceFile(document.FilePath, document.GeneratedCodeContainer.SourceTextContainer, true, GetFolders(document), SourceCodeKind.Regular, document.GeneratedCodeContainer);
_projectContext?.AddDynamicSourceFile(document.FilePath, GetFolders(document));
_currentDocuments.Add(document.FilePath, document);
}
@ -310,7 +310,7 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem
{
var projectManager = GetProjectManager();
_projectContext?.RemoveSourceFile(document.FilePath);
_projectContext?.RemoveDynamicSourceFile(document.FilePath);
projectManager.DocumentRemoved(_current, document);
_currentDocuments.Remove(document.FilePath);
}

View File

@ -1,6 +1,7 @@
// 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 System.Collections.Generic;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.Host;
@ -15,9 +16,9 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem
{
var projectState = ProjectState.Create(Workspace.Services, TestProjectData.SomeProject);
var project = new DefaultProjectSnapshot(projectState);
HostDocument = TestProjectData.SomeProjectFile1;
HostDocument = new HostDocument(TestProjectData.SomeProjectFile1.FilePath, TestProjectData.SomeProjectFile1.TargetPath);
SourceText = SourceText.From("<p>Hello World</p>");
Version = VersionStamp.Default.GetNewerVersion();
Version = VersionStamp.Create();
var textAndVersion = TextAndVersion.Create(SourceText, Version);
var documentState = DocumentState.Create(Workspace.Services, HostDocument, () => Task.FromResult(textAndVersion));
Document = new DefaultDocumentSnapshot(project, documentState);
@ -47,13 +48,32 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem
Assert.Same(SourceText, HostDocument.GeneratedCodeContainer.Source);
}
[Fact]
public async Task GetGeneratedOutputAsync_SetsOutputWhenDocumentIsNewer()
{
// Arrange
var newSourceText = SourceText.From("NEW!");
var newDocumentState = Document.State.WithText(newSourceText, Version.GetNewerVersion());
var newDocument = new DefaultDocumentSnapshot(Document.ProjectInternal, newDocumentState);
// Force the output to be the new output
await Document.GetGeneratedOutputAsync();
// Act
await newDocument.GetGeneratedOutputAsync();
// Assert
Assert.NotNull(HostDocument.GeneratedCodeContainer.Output);
Assert.Same(newSourceText, HostDocument.GeneratedCodeContainer.Source);
}
[Fact]
public async Task GetGeneratedOutputAsync_OnlySetsOutputIfDocumentNewer()
{
// Arrange
var newSourceText = SourceText.From("NEW!");
var newDocumentState = Document.State.WithText(newSourceText, Version.GetNewerVersion());
var newDocument = new DefaultDocumentSnapshot(Document.Project, newDocumentState);
var newDocument = new DefaultDocumentSnapshot(Document.ProjectInternal, newDocumentState);
// Force the output to be the new output
await newDocument.GetGeneratedOutputAsync();

View File

@ -59,115 +59,6 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem
Assert.NotNull(container.LatestDocument);
}
[Fact]
public void TryGetLinePositionSpan_SpanMatchesSourceMapping_ReturnsTrue()
{
// Arrange
var content = @"
@SomeProperty
";
var sourceText = SourceText.From(content);
var codeDocument = GetCodeDocument(content);
var csharpDocument = codeDocument.GetCSharpDocument();
var generatedCode = csharpDocument.GeneratedCode;
var symbol = "SomeProperty";
var span = new TextSpan(generatedCode.IndexOf(symbol), symbol.Length);
// Position of `SomeProperty` in the source code.
var expectedLineSpan = new LinePositionSpan(new LinePosition(1, 1), new LinePosition(1, 13));
// Act
var result = GeneratedCodeContainer.TryGetLinePositionSpan(span, sourceText, csharpDocument, out var lineSpan);
// Assert
Assert.True(result);
Assert.Equal(expectedLineSpan, lineSpan);
}
[Fact]
public void TryGetLinePositionSpan_SpanMatchesSourceMapping_MatchingOnPosition_ReturnsTrue()
{
// Arrange
var content = @"
@SomeProperty
@SomeProperty
@SomeProperty
";
var sourceText = SourceText.From(content);
var codeDocument = GetCodeDocument(content);
var csharpDocument = codeDocument.GetCSharpDocument();
var generatedCode = csharpDocument.GeneratedCode;
var symbol = "SomeProperty";
// Second occurrence
var span = new TextSpan(generatedCode.IndexOf(symbol, generatedCode.IndexOf(symbol) + symbol.Length), symbol.Length);
// Position of `SomeProperty` in the source code.
var expectedLineSpan = new LinePositionSpan(new LinePosition(2, 1), new LinePosition(2, 13));
// Act
var result = GeneratedCodeContainer.TryGetLinePositionSpan(span, sourceText, csharpDocument, out var lineSpan);
// Assert
Assert.True(result);
Assert.Equal(expectedLineSpan, lineSpan);
}
[Fact]
public void TryGetLinePositionSpan_SpanWithinSourceMapping_ReturnsTrue()
{
// Arrange
var content = @"
@{
var x = SomeClass.SomeProperty;
}
";
var sourceText = SourceText.From(content);
var codeDocument = GetCodeDocument(content);
var csharpDocument = codeDocument.GetCSharpDocument();
var generatedCode = csharpDocument.GeneratedCode;
var symbol = "SomeProperty";
var span = new TextSpan(generatedCode.IndexOf(symbol), symbol.Length);
// Position of `SomeProperty` in the source code.
var expectedLineSpan = new LinePositionSpan(new LinePosition(2, 22), new LinePosition(2, 34));
// Act
var result = GeneratedCodeContainer.TryGetLinePositionSpan(span, sourceText, csharpDocument, out var lineSpan);
// Assert
Assert.True(result);
Assert.Equal(expectedLineSpan, lineSpan);
}
[Fact]
public void TryGetLinePositionSpan_SpanOutsideSourceMapping_ReturnsFalse()
{
// Arrange
var content = @"
@{
var x = SomeClass.SomeProperty;
}
";
var sourceText = SourceText.From(content);
var codeDocument = GetCodeDocument(content);
var csharpDocument = codeDocument.GetCSharpDocument();
var generatedCode = csharpDocument.GeneratedCode;
// Position of `ExecuteAsync` in the generated code.
var symbol = "ExecuteAsync";
var span = new TextSpan(generatedCode.IndexOf(symbol), symbol.Length);
// Act
var result = GeneratedCodeContainer.TryGetLinePositionSpan(span, sourceText, csharpDocument, out var lineSpan);
// Assert
Assert.False(result);
}
private static RazorCodeDocument GetCodeDocument(string content)
{
var sourceProjectItem = new TestRazorProjectItem("test.cshtml")

View File

@ -0,0 +1,429 @@
// 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 System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Razor.Language;
using Microsoft.CodeAnalysis.Classification;
using Microsoft.CodeAnalysis.Host;
using Microsoft.CodeAnalysis.Razor.ProjectSystem;
using Microsoft.CodeAnalysis.Text;
using Xunit;
namespace Microsoft.CodeAnalysis.Razor
{
public class RazorExcerptServiceTest : WorkspaceTestBase
{
public RazorExcerptServiceTest()
{
HostProject = TestProjectData.SomeProject;
HostDocument = TestProjectData.SomeProjectFile1;
}
private HostProject HostProject { get; }
private HostDocument HostDocument { get; }
protected override void ConfigureLanguageServices(List<ILanguageService> services)
{
services.Add(new TestTagHelperResolver());
}
[Fact]
public async Task TryExcerptAsync_SingleLine_CanClassifyCSharp()
{
// Arrange
var (sourceText, primarySpan) = CreateText(
@"
<html>
@{
var |foo| = ""Hello, World!"";
}
<body>@foo</body>
<div>@(3 + 4)</div><div>@(foo + foo)</div>
</html>
");
var (primary, secondary) = Initialize(sourceText);
var service = CreateExcerptService(primary);
var secondarySpan = await GetSecondarySpanAsync(primary, primarySpan, secondary);
// Act
var result = await service.TryExcerptAsync(secondary, secondarySpan, ExcerptMode.SingleLine, CancellationToken.None);
// Assert
Assert.NotNull(result);
Assert.Equal(secondarySpan, result.Value.Span);
Assert.Same(secondary, result.Value.Document);
Assert.Equal(@" var foo = ""Hello, World!"";", result.Value.Content.ToString(), ignoreLineEndingDifferences: true);
Assert.Collection(
result.Value.ClassifiedSpans,
c =>
{
Assert.Equal(ClassificationTypeNames.Keyword, c.ClassificationType);
Assert.Equal("var", result.Value.Content.GetSubText(c.TextSpan).ToString());
},
c =>
{
Assert.Equal(ClassificationTypeNames.LocalName, c.ClassificationType);
Assert.Equal("foo", result.Value.Content.GetSubText(c.TextSpan).ToString());
},
c =>
{
Assert.Equal(ClassificationTypeNames.Operator, c.ClassificationType);
Assert.Equal("=", result.Value.Content.GetSubText(c.TextSpan).ToString());
},
c =>
{
Assert.Equal(ClassificationTypeNames.StringLiteral, c.ClassificationType);
Assert.Equal("\"Hello, World!\"", result.Value.Content.GetSubText(c.TextSpan).ToString());
},
c =>
{
Assert.Equal(ClassificationTypeNames.Punctuation, c.ClassificationType);
Assert.Equal(";", result.Value.Content.GetSubText(c.TextSpan).ToString());
});
}
[Fact]
public async Task TryExcerptAsync_SingleLine_CanClassifyCSharp_ImplicitExpression()
{
// Arrange
var (sourceText, primarySpan) = CreateText(
@"
<html>
@{
var foo = ""Hello, World!"";
}
<body>@|foo|</body>
<div>@(3 + 4)</div><div>@(foo + foo)</div>
</html>
");
var (primary, secondary) = Initialize(sourceText);
var service = CreateExcerptService(primary);
var secondarySpan = await GetSecondarySpanAsync(primary, primarySpan, secondary);
// Act
var result = await service.TryExcerptAsync(secondary, secondarySpan, ExcerptMode.SingleLine, CancellationToken.None);
// Assert
Assert.NotNull(result);
Assert.Equal(secondarySpan, result.Value.Span);
Assert.Same(secondary, result.Value.Document);
Assert.Equal(@" <body>@foo</body>", result.Value.Content.ToString(), ignoreLineEndingDifferences: true);
Assert.Collection(
result.Value.ClassifiedSpans,
c =>
{
Assert.Equal(ClassificationTypeNames.Text, c.ClassificationType);
Assert.Equal(" <body>@", result.Value.Content.GetSubText(c.TextSpan).ToString());
},
c =>
{
Assert.Equal(ClassificationTypeNames.LocalName, c.ClassificationType);
Assert.Equal("foo", result.Value.Content.GetSubText(c.TextSpan).ToString());
},
c =>
{
Assert.Equal(ClassificationTypeNames.Text, c.ClassificationType);
Assert.Equal("</body>", result.Value.Content.GetSubText(c.TextSpan).ToString());
});
}
[Fact]
public async Task TryExcerptAsync_SingleLine_CanClassifyCSharp_ComplexLine()
{
// Arrange
var (sourceText, primarySpan) = CreateText(
@"
<html>
@{
var foo = ""Hello, World!"";
}
<body>@foo</body>
<div>@(3 + 4)</div><div>@(foo + |foo|)</div>
</html>
");
var (primary, secondary) = Initialize(sourceText);
var service = CreateExcerptService(primary);
var secondarySpan = await GetSecondarySpanAsync(primary, primarySpan, secondary);
// Act
var result = await service.TryExcerptAsync(secondary, secondarySpan, ExcerptMode.SingleLine, CancellationToken.None);
// Assert
Assert.NotNull(result);
Assert.Equal(secondarySpan, result.Value.Span);
Assert.Same(secondary, result.Value.Document);
Assert.Equal(@" <div>@(3 + 4)</div><div>@(foo + foo)</div>", result.Value.Content.ToString(), ignoreLineEndingDifferences: true);
Assert.Collection(
result.Value.ClassifiedSpans,
c =>
{
Assert.Equal(ClassificationTypeNames.Text, c.ClassificationType);
Assert.Equal(" <div>@(", result.Value.Content.GetSubText(c.TextSpan).ToString());
},
c =>
{
Assert.Equal(ClassificationTypeNames.NumericLiteral, c.ClassificationType);
Assert.Equal("3", result.Value.Content.GetSubText(c.TextSpan).ToString());
},
c =>
{
Assert.Equal(ClassificationTypeNames.Operator, c.ClassificationType);
Assert.Equal("+", result.Value.Content.GetSubText(c.TextSpan).ToString());
},
c =>
{
Assert.Equal(ClassificationTypeNames.NumericLiteral, c.ClassificationType);
Assert.Equal("4", result.Value.Content.GetSubText(c.TextSpan).ToString());
},
c =>
{
Assert.Equal(ClassificationTypeNames.Text, c.ClassificationType);
Assert.Equal(")</div><div>@(", result.Value.Content.GetSubText(c.TextSpan).ToString());
},
c =>
{
Assert.Equal(ClassificationTypeNames.LocalName, c.ClassificationType);
Assert.Equal("foo", result.Value.Content.GetSubText(c.TextSpan).ToString());
},
c =>
{
Assert.Equal(ClassificationTypeNames.Operator, c.ClassificationType);
Assert.Equal("+", result.Value.Content.GetSubText(c.TextSpan).ToString());
},
c =>
{
Assert.Equal(ClassificationTypeNames.LocalName, c.ClassificationType);
Assert.Equal("foo", result.Value.Content.GetSubText(c.TextSpan).ToString());
},
c =>
{
Assert.Equal(ClassificationTypeNames.Text, c.ClassificationType);
Assert.Equal(")</div>", result.Value.Content.GetSubText(c.TextSpan).ToString());
});
}
[Fact]
public async Task TryExcerptAsync_MultiLine_CanClassifyCSharp()
{
// Arrange
var (sourceText, primarySpan) = CreateText(
@"
<html>
@{
var |foo| = ""Hello, World!"";
}
<body>@foo</body>
<div>@(3 + 4)</div><div>@(foo + foo)</div>
</html>
");
var (primary, secondary) = Initialize(sourceText);
var service = CreateExcerptService(primary);
var secondarySpan = await GetSecondarySpanAsync(primary, primarySpan, secondary);
// Act
var result = await service.TryExcerptAsync(secondary, secondarySpan, ExcerptMode.Tooltip, CancellationToken.None);
// Assert
Assert.NotNull(result);
Assert.Equal(secondarySpan, result.Value.Span);
Assert.Same(secondary, result.Value.Document);
Assert.Equal(
@"@{
var foo = ""Hello, World!"";
}",
result.Value.Content.ToString(), ignoreLineEndingDifferences: true);
Assert.Collection(
result.Value.ClassifiedSpans,
c =>
{
Assert.Equal(ClassificationTypeNames.Text, c.ClassificationType);
Assert.Equal("@{", result.Value.Content.GetSubText(c.TextSpan).ToString());
},
c =>
{
Assert.Equal(ClassificationTypeNames.Keyword, c.ClassificationType);
Assert.Equal("var", result.Value.Content.GetSubText(c.TextSpan).ToString());
},
c =>
{
Assert.Equal(ClassificationTypeNames.LocalName, c.ClassificationType);
Assert.Equal("foo", result.Value.Content.GetSubText(c.TextSpan).ToString());
},
c =>
{
Assert.Equal(ClassificationTypeNames.Operator, c.ClassificationType);
Assert.Equal("=", result.Value.Content.GetSubText(c.TextSpan).ToString());
},
c =>
{
Assert.Equal(ClassificationTypeNames.StringLiteral, c.ClassificationType);
Assert.Equal("\"Hello, World!\"", result.Value.Content.GetSubText(c.TextSpan).ToString());
},
c =>
{
Assert.Equal(ClassificationTypeNames.Punctuation, c.ClassificationType);
Assert.Equal(";", result.Value.Content.GetSubText(c.TextSpan).ToString());
},
c =>
{
Assert.Equal(ClassificationTypeNames.Text, c.ClassificationType);
Assert.Equal("}", result.Value.Content.GetSubText(c.TextSpan).ToString());
});
}
[Fact]
public async Task TryExcerptAsync_MultiLine_Boundaries_CanClassifyCSharp()
{
// Arrange
var (sourceText, primarySpan) = CreateText(@"@{ var |foo| = ""Hello, World!""; }");
var (primary, secondary) = Initialize(sourceText);
var service = CreateExcerptService(primary);
var secondarySpan = await GetSecondarySpanAsync(primary, primarySpan, secondary);
// Act
var result = await service.TryExcerptAsync(secondary, secondarySpan, ExcerptMode.Tooltip, CancellationToken.None);
// Assert
Assert.NotNull(result);
Assert.Equal(secondarySpan, result.Value.Span);
Assert.Same(secondary, result.Value.Document);
Assert.Equal(
@"@{ var foo = ""Hello, World!""; }",
result.Value.Content.ToString(), ignoreLineEndingDifferences: true);
Assert.Collection(
result.Value.ClassifiedSpans,
c =>
{
Assert.Equal(ClassificationTypeNames.Text, c.ClassificationType);
Assert.Equal("@{", result.Value.Content.GetSubText(c.TextSpan).ToString());
},
c =>
{
Assert.Equal(ClassificationTypeNames.Keyword, c.ClassificationType);
Assert.Equal("var", result.Value.Content.GetSubText(c.TextSpan).ToString());
},
c =>
{
Assert.Equal(ClassificationTypeNames.LocalName, c.ClassificationType);
Assert.Equal("foo", result.Value.Content.GetSubText(c.TextSpan).ToString());
},
c =>
{
Assert.Equal(ClassificationTypeNames.Operator, c.ClassificationType);
Assert.Equal("=", result.Value.Content.GetSubText(c.TextSpan).ToString());
},
c =>
{
Assert.Equal(ClassificationTypeNames.StringLiteral, c.ClassificationType);
Assert.Equal("\"Hello, World!\"", result.Value.Content.GetSubText(c.TextSpan).ToString());
},
c =>
{
Assert.Equal(ClassificationTypeNames.Punctuation, c.ClassificationType);
Assert.Equal(";", result.Value.Content.GetSubText(c.TextSpan).ToString());
},
c =>
{
Assert.Equal(ClassificationTypeNames.Text, c.ClassificationType);
Assert.Equal("}", result.Value.Content.GetSubText(c.TextSpan).ToString());
});
}
public (SourceText sourceText, TextSpan span) CreateText(string text)
{
// Since we're using positions, normalize to Windows style
text = text.Replace("\r", "").Replace("\n", "\r\n");
var start = text.IndexOf('|');
var length = text.IndexOf('|', start + 1) - start - 1;
text = text.Replace("|", "");
if (start < 0 || length < 0)
{
throw new InvalidOperationException("Could not find delimited text.");
}
return (SourceText.From(text), new TextSpan(start, length));
}
// Adds the text to a ProjectSnapshot, generates code, and updates the workspace.
private (DocumentSnapshot primary, Document secondary) Initialize(SourceText sourceText)
{
var project = new DefaultProjectSnapshot(
ProjectState.Create(Workspace.Services, HostProject)
.WithAddedHostDocument(HostDocument, () =>
{
return Task.FromResult(TextAndVersion.Create(sourceText, VersionStamp.Create()));
}));
var primary = project.GetDocument(HostDocument.FilePath);
var solution = Workspace.CurrentSolution.AddProject(ProjectInfo.Create(
ProjectId.CreateNewId(Path.GetFileNameWithoutExtension(HostDocument.FilePath)),
VersionStamp.Create(),
Path.GetFileNameWithoutExtension(HostDocument.FilePath),
Path.GetFileNameWithoutExtension(HostDocument.FilePath),
LanguageNames.CSharp,
HostDocument.FilePath));
solution = solution.AddDocument(
DocumentId.CreateNewId(solution.ProjectIds.Single(), HostDocument.FilePath),
HostDocument.FilePath,
new GeneratedOutputTextLoader(primary, HostDocument.FilePath));
var secondary = solution.Projects.Single().Documents.Single();
return (primary, secondary);
}
// Maps a span in the primary buffer to the secondary buffer. This is only valid for C# code
// that appears in the primary buffer.
private async Task<TextSpan> GetSecondarySpanAsync(DocumentSnapshot primary, TextSpan primarySpan, Document secondary)
{
var output = await primary.GetGeneratedOutputAsync();
var mappings = output.GetCSharpDocument().SourceMappings;
for (var i = 0; i < mappings.Count; i++)
{
var mapping = mappings[i];
if (mapping.OriginalSpan.AsTextSpan().Contains(primarySpan))
{
var offset = mapping.GeneratedSpan.AbsoluteIndex - mapping.OriginalSpan.AbsoluteIndex;
var secondarySpan = new TextSpan(primarySpan.Start + offset, primarySpan.Length);
Assert.Equal(
(await primary.GetTextAsync()).GetSubText(primarySpan).ToString(),
(await secondary.GetTextAsync()).GetSubText(secondarySpan).ToString());
return secondarySpan;
}
}
throw new InvalidOperationException("Could not map the primary span to the generated code.");
}
private RazorDocumentExcerptService CreateExcerptService(DocumentSnapshot document)
{
return new RazorDocumentExcerptService(document, new RazorSpanMappingService(document));
}
}
}

View File

@ -0,0 +1,164 @@
// 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.Collections.Generic;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Razor.Language;
using Microsoft.CodeAnalysis.Host;
using Microsoft.CodeAnalysis.Razor.ProjectSystem;
using Microsoft.CodeAnalysis.Text;
using Xunit;
namespace Microsoft.CodeAnalysis.Razor
{
public class RazorSpanMappingServiceTest : WorkspaceTestBase
{
public RazorSpanMappingServiceTest()
{
HostProject = TestProjectData.SomeProject;
HostDocument = TestProjectData.SomeProjectFile1;
}
private HostProject HostProject { get; }
private HostDocument HostDocument { get; }
protected override void ConfigureLanguageServices(List<ILanguageService> services)
{
services.Add(new TestTagHelperResolver());
}
[Fact]
public async Task TryGetLinePositionSpan_SpanMatchesSourceMapping_ReturnsTrue()
{
// Arrange
var sourceText = SourceText.From(@"
@SomeProperty
");
var project = new DefaultProjectSnapshot(
ProjectState.Create(Workspace.Services, HostProject)
.WithAddedHostDocument(HostDocument, () =>
{
return Task.FromResult(TextAndVersion.Create(sourceText, VersionStamp.Create()));
}));
var document = project.GetDocument(HostDocument.FilePath);
var service = new RazorSpanMappingService(document);
var output = await document.GetGeneratedOutputAsync();
var generated = output.GetCSharpDocument();
var symbol = "SomeProperty";
var span = new TextSpan(generated.GeneratedCode.IndexOf(symbol), symbol.Length);
// Act
var result = RazorSpanMappingService.TryGetLinePositionSpan(span, await document.GetTextAsync(), generated, out var mapped);
// Assert
Assert.True(result);
Assert.Equal(new LinePositionSpan(new LinePosition(1, 1), new LinePosition(1, 13)), mapped);
}
[Fact]
public async Task TryGetLinePositionSpan_SpanMatchesSourceMappingAndPosition_ReturnsTrue()
{
// Arrange
var sourceText = SourceText.From(@"
@SomeProperty
@SomeProperty
@SomeProperty
");
var project = new DefaultProjectSnapshot(
ProjectState.Create(Workspace.Services, HostProject)
.WithAddedHostDocument(HostDocument, () =>
{
return Task.FromResult(TextAndVersion.Create(sourceText, VersionStamp.Create()));
}));
var document = project.GetDocument(HostDocument.FilePath);
var service = new RazorSpanMappingService(document);
var output = await document.GetGeneratedOutputAsync();
var generated = output.GetCSharpDocument();
var symbol = "SomeProperty";
// Second occurrence
var span = new TextSpan(generated.GeneratedCode.IndexOf(symbol, generated.GeneratedCode.IndexOf(symbol) + symbol.Length), symbol.Length);
// Act
var result = RazorSpanMappingService.TryGetLinePositionSpan(span, await document.GetTextAsync(), generated, out var mapped);
// Assert
Assert.True(result);
Assert.Equal(new LinePositionSpan(new LinePosition(2, 1), new LinePosition(2, 13)), mapped);
}
[Fact]
public async Task TryGetLinePositionSpan_SpanWithinSourceMapping_ReturnsTrue()
{
// Arrange
var sourceText = SourceText.From(@"
@{
var x = SomeClass.SomeProperty;
}
");
var project = new DefaultProjectSnapshot(
ProjectState.Create(Workspace.Services, HostProject)
.WithAddedHostDocument(HostDocument, () =>
{
return Task.FromResult(TextAndVersion.Create(sourceText, VersionStamp.Create()));
}));
var document = project.GetDocument(HostDocument.FilePath);
var service = new RazorSpanMappingService(document);
var output = await document.GetGeneratedOutputAsync();
var generated = output.GetCSharpDocument();
var symbol = "SomeProperty";
var span = new TextSpan(generated.GeneratedCode.IndexOf(symbol), symbol.Length);
// Act
var result = RazorSpanMappingService.TryGetLinePositionSpan(span, await document.GetTextAsync(), generated, out var mapped);
// Assert
Assert.True(result);
Assert.Equal(new LinePositionSpan(new LinePosition(2, 22), new LinePosition(2, 34)), mapped);
}
[Fact]
public async Task TryGetLinePositionSpan_SpanOutsideSourceMapping_ReturnsFalse()
{
// Arrange
var sourceText = SourceText.From(@"
@{
var x = SomeClass.SomeProperty;
}
");
var project = new DefaultProjectSnapshot(
ProjectState.Create(Workspace.Services, HostProject)
.WithAddedHostDocument(HostDocument, () =>
{
return Task.FromResult(TextAndVersion.Create(sourceText, VersionStamp.Create()));
}));
var document = project.GetDocument(HostDocument.FilePath);
var service = new RazorSpanMappingService(document);
var output = await document.GetGeneratedOutputAsync();
var generated = output.GetCSharpDocument();
var symbol = "ExecuteAsync";
var span = new TextSpan(generated.GeneratedCode.IndexOf(symbol), symbol.Length);
// Act
var result = RazorSpanMappingService.TryGetLinePositionSpan(span, await document.GetTextAsync(), generated, out var mapped);
// Assert
Assert.False(result);
}
}
}