Generated document output persists result to generated code container.

- Prior to this we had a `BackgroundDocumentGenerator` that would constantly be updating generated code containers. With this changes we've changed the details in how a `GeneratedCodeContainer` can be mutated. It can now be touched from any thread and is updated when an underlying `DocumentSnapshot` has available content. However, if a generated output comes through that's older then the last seen output we no-op.
- Removed `BackgroundDocumentGenerator` SetOutput logic.
- Updated tests to react to new behavior of `GetGeneratedOutputAsync`.
- Added new test to verify getting generated output results in the setting of the host documents output.
This commit is contained in:
N. Taylor Mullen 2018-09-06 18:12:24 -07:00
parent 96709c4d77
commit da935bfa95
5 changed files with 224 additions and 63 deletions

View File

@ -111,7 +111,14 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem
var projectEngine = project.GetProjectEngine(); var projectEngine = project.GetProjectEngine();
return projectEngine.ProcessDesignTime(documentSource, importSources, tagHelpers); var codeDocument = projectEngine.ProcessDesignTime(documentSource, importSources, tagHelpers);
var csharpDocument = codeDocument.GetCSharpDocument();
if (document is DefaultDocumentSnapshot defaultDocument)
{
defaultDocument.State.HostDocument.GeneratedCodeContainer.SetOutput(csharpDocument, defaultDocument);
}
return codeDocument;
} }
private async Task<RazorSourceDocument> GetRazorSourceDocumentAsync(DocumentSnapshot document) private async Task<RazorSourceDocument> GetRazorSourceDocumentAsync(DocumentSnapshot document)

View File

@ -2,32 +2,89 @@
// 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;
using Microsoft.CodeAnalysis.Text;
using Microsoft.CodeAnalysis.Experiment;
using Microsoft.AspNetCore.Razor.Language;
using System.Collections.Immutable;
using System.Threading.Tasks;
using System.Threading;
using System.Collections.Generic; 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 namespace Microsoft.CodeAnalysis.Razor.ProjectSystem
{ {
internal class GeneratedCodeContainer : IDocumentServiceFactory, ISpanMapper internal class GeneratedCodeContainer : IDocumentServiceFactory, ISpanMapper
{ {
public event EventHandler<TextChangeEventArgs> GeneratedCodeChanged;
private SourceText _source;
private VersionStamp _sourceVersion;
private RazorCSharpDocument _output;
private DocumentSnapshot _latestDocument;
private readonly object _setOutputLock = new object();
private readonly TextContainer _textContainer; private readonly TextContainer _textContainer;
public GeneratedCodeContainer() public GeneratedCodeContainer()
{ {
_textContainer = new TextContainer(); _textContainer = new TextContainer(_setOutputLock);
_textContainer.TextChanged += TextContainer_TextChanged;
} }
public SourceText Source { get; private set; } public SourceText Source
{
get
{
lock (_setOutputLock)
{
return _source;
}
}
}
public VersionStamp SourceVersion { get; private set; } public VersionStamp SourceVersion
{
get
{
lock (_setOutputLock)
{
return _sourceVersion;
}
}
}
public RazorCSharpDocument Output { get; private set; } public RazorCSharpDocument Output
{
get
{
lock (_setOutputLock)
{
return _output;
}
}
}
public SourceTextContainer SourceTextContainer => _textContainer; public DocumentSnapshot LatestDocument
{
get
{
lock (_setOutputLock)
{
return _latestDocument;
}
}
}
public SourceTextContainer SourceTextContainer
{
get
{
lock (_setOutputLock)
{
return _textContainer;
}
}
}
public TService GetService<TService>() public TService GetService<TService>()
{ {
@ -39,28 +96,59 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem
return default(TService); return default(TService);
} }
public void SetOutput(SourceText source, RazorCodeDocument codeDocument) public void SetOutput(RazorCSharpDocument csharpDocument, DefaultDocumentSnapshot document)
{ {
Source = source; lock (_setOutputLock)
Output = codeDocument.GetCSharpDocument(); {
if (!document.TryGetTextVersion(out var version))
{
Debug.Fail("The text version should have already been evaluated.");
return;
}
_textContainer.SetText(SourceText.From(Output.GeneratedCode)); var newerVersion = SourceVersion.GetNewerVersion(version);
if (newerVersion == SourceVersion)
{
// Latest document is newer than the provided document.
return;
}
if (!document.TryGetText(out var source))
{
Debug.Fail("The text should have already been evaluated.");
return;
}
_source = source;
_sourceVersion = version;
_output = csharpDocument;
_latestDocument = document;
_textContainer.SetText(SourceText.From(Output.GeneratedCode));
}
} }
public Task<ImmutableArray<SpanMapResult>> MapSpansAsync( public Task<ImmutableArray<SpanMapResult>> MapSpansAsync(
Document document, Document document,
IEnumerable<TextSpan> spans, IEnumerable<TextSpan> spans,
CancellationToken cancellationToken) CancellationToken cancellationToken)
{ {
if (Output == null) RazorCSharpDocument output;
SourceText source;
lock (_setOutputLock)
{ {
return Task.FromResult(ImmutableArray<SpanMapResult>.Empty); if (Output == null)
{
return Task.FromResult(ImmutableArray<SpanMapResult>.Empty);
}
output = Output;
source = Source;
} }
var results = ImmutableArray.CreateBuilder<SpanMapResult>(); var results = ImmutableArray.CreateBuilder<SpanMapResult>();
foreach (var span in spans) foreach (var span in spans)
{ {
if (TryGetLinePositionSpan(span, out var linePositionSpan)) if (TryGetLinePositionSpan(span, source, output, out var linePositionSpan))
{ {
results.Add(new SpanMapResult(document, linePositionSpan)); results.Add(new SpanMapResult(document, linePositionSpan));
} }
@ -70,11 +158,11 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem
} }
// Internal for testing. // Internal for testing.
internal bool TryGetLinePositionSpan(TextSpan span, out LinePositionSpan linePositionSpan) internal static bool TryGetLinePositionSpan(TextSpan span, SourceText source, RazorCSharpDocument output, out LinePositionSpan linePositionSpan)
{ {
for (var i = 0; i < Output.SourceMappings.Count; i++) for (var i = 0; i < output.SourceMappings.Count; i++)
{ {
var mapping = Output.SourceMappings[i]; var mapping = output.SourceMappings[i];
if (span.Length > mapping.GeneratedSpan.Length) if (span.Length > mapping.GeneratedSpan.Length)
{ {
// If the length of the generated span is smaller they can't match. A C# expression // If the length of the generated span is smaller they can't match. A C# expression
@ -94,7 +182,7 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem
{ {
// This span mapping contains the span. // This span mapping contains the span.
var adjusted = new TextSpan(original.Start + leftOffset, (original.End + rightOffset) - (original.Start + leftOffset)); var adjusted = new TextSpan(original.Start + leftOffset, (original.End + rightOffset) - (original.Start + leftOffset));
linePositionSpan = Source.Lines.GetLinePositionSpan(adjusted); linePositionSpan = source.Lines.GetLinePositionSpan(adjusted);
return true; return true;
} }
} }
@ -103,15 +191,22 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem
return false; return false;
} }
private void TextContainer_TextChanged(object sender, TextChangeEventArgs args)
{
GeneratedCodeChanged?.Invoke(this, args);
}
private class TextContainer : SourceTextContainer private class TextContainer : SourceTextContainer
{ {
public override event EventHandler<TextChangeEventArgs> TextChanged; public override event EventHandler<TextChangeEventArgs> TextChanged;
private readonly object _outerLock;
private SourceText _currentText; private SourceText _currentText;
public TextContainer() public TextContainer(object outerLock)
: this(SourceText.From(string.Empty)) : this(SourceText.From(string.Empty))
{ {
_outerLock = outerLock;
} }
public TextContainer(SourceText sourceText) public TextContainer(SourceText sourceText)
@ -124,7 +219,16 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem
_currentText = sourceText; _currentText = sourceText;
} }
public override SourceText CurrentText => _currentText; public override SourceText CurrentText
{
get
{
lock (_outerLock)
{
return _currentText;
}
}
}
public void SetText(SourceText sourceText) public void SetText(SourceText sourceText)
{ {
@ -133,10 +237,14 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem
throw new ArgumentNullException(nameof(sourceText)); throw new ArgumentNullException(nameof(sourceText));
} }
var e = new TextChangeEventArgs(_currentText, sourceText); lock (_outerLock)
_currentText = sourceText; {
TextChanged?.Invoke(this, e); var e = new TextChangeEventArgs(_currentText, sourceText);
_currentText = sourceText;
TextChanged?.Invoke(this, e);
}
} }
} }
} }

View File

@ -187,12 +187,6 @@ namespace Microsoft.CodeAnalysis.Razor
OnCompletingBackgroundWork(); OnCompletingBackgroundWork();
await Task.Factory.StartNew(
() => ReportUpdates(work),
CancellationToken.None,
TaskCreationOptions.None,
_foregroundDispatcher.ForegroundScheduler);
lock (_work) lock (_work)
{ {
// Resetting the timer allows another batch of work to start. // Resetting the timer allows another batch of work to start.
@ -219,22 +213,6 @@ namespace Microsoft.CodeAnalysis.Razor
} }
} }
private void ReportUpdates(KeyValuePair<DocumentKey, DocumentSnapshot>[] work)
{
for (var i = 0; i < work.Length; i++)
{
var key = work[i].Key;
var document = work[i].Value;
if (document.TryGetText(out var source) &&
document.TryGetGeneratedOutput(out var output))
{
var container = ((DefaultDocumentSnapshot)document).State.GeneratedCodeContainer;
container.SetOutput(source, output);
}
}
}
private void ReportError(DocumentSnapshot document, Exception ex) private void ReportError(DocumentSnapshot document, Exception ex)
{ {
GC.KeepAlive(Task.Factory.StartNew( GC.KeepAlive(Task.Factory.StartNew(

View File

@ -0,0 +1,72 @@
// 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.Tasks;
using Microsoft.AspNetCore.Razor.Language;
using Microsoft.CodeAnalysis.Host;
using Microsoft.CodeAnalysis.Text;
using Xunit;
namespace Microsoft.CodeAnalysis.Razor.ProjectSystem
{
public class DefaultDocumentSnapshotTest
{
public DefaultDocumentSnapshotTest()
{
var services = TestServices.Create(
new[] { new TestProjectSnapshotProjectEngineFactory() },
new[] { new TestTagHelperResolver() });
Workspace = TestWorkspace.Create(services);
var hostProject = new HostProject("C:/some/path/project.csproj", RazorConfiguration.Default);
var projectState = ProjectState.Create(Workspace.Services, hostProject);
var project = new DefaultProjectSnapshot(projectState);
HostDocument = new HostDocument("C:/some/path/file.cshtml", "C:/some/path/file.cshtml");
SourceText = Text.SourceText.From("<p>Hello World</p>");
Version = VersionStamp.Default.GetNewerVersion();
var textAndVersion = TextAndVersion.Create(SourceText, Version);
var documentState = DocumentState.Create(Workspace.Services, HostDocument, () => Task.FromResult(textAndVersion));
Document = new DefaultDocumentSnapshot(project, documentState);
}
private Workspace Workspace { get; }
private SourceText SourceText { get; }
private VersionStamp Version { get; }
private HostDocument HostDocument { get; }
private DefaultDocumentSnapshot Document { get; }
[Fact]
public async Task GetGeneratedOutputAsync_SetsHostDocumentOutput()
{
// Act
await Document.GetGeneratedOutputAsync();
// Assert
Assert.NotNull(HostDocument.GeneratedCodeContainer.Output);
Assert.Same(SourceText, 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);
// Force the output to be the new output
await newDocument.GetGeneratedOutputAsync();
// Act
await Document.GetGeneratedOutputAsync();
// Assert
Assert.NotNull(HostDocument.GeneratedCodeContainer.Output);
Assert.Same(newSourceText, HostDocument.GeneratedCodeContainer.Source);
}
}
}

View File

@ -20,10 +20,8 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem
"; ";
var sourceText = SourceText.From(content); var sourceText = SourceText.From(content);
var codeDocument = GetCodeDocument(content); var codeDocument = GetCodeDocument(content);
var generatedCode = codeDocument.GetCSharpDocument().GeneratedCode; var csharpDocument = codeDocument.GetCSharpDocument();
var generatedCode = csharpDocument.GeneratedCode;
var container = new GeneratedCodeContainer();
container.SetOutput(sourceText, codeDocument);
// TODO: Make writing these tests a little less manual. // TODO: Make writing these tests a little less manual.
// Position of `SomeProperty` in the generated code. // Position of `SomeProperty` in the generated code.
@ -34,7 +32,7 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem
var expectedLineSpan = new LinePositionSpan(new LinePosition(2, 22), new LinePosition(2, 34)); var expectedLineSpan = new LinePositionSpan(new LinePosition(2, 22), new LinePosition(2, 34));
// Act // Act
var result = container.TryGetLinePositionSpan(span, out var lineSpan); var result = GeneratedCodeContainer.TryGetLinePositionSpan(span, sourceText, csharpDocument, out var lineSpan);
// Assert // Assert
Assert.True(result); Assert.True(result);
@ -52,17 +50,15 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem
"; ";
var sourceText = SourceText.From(content); var sourceText = SourceText.From(content);
var codeDocument = GetCodeDocument(content); var codeDocument = GetCodeDocument(content);
var generatedCode = codeDocument.GetCSharpDocument().GeneratedCode; var csharpDocument = codeDocument.GetCSharpDocument();
var generatedCode = csharpDocument.GeneratedCode;
var container = new GeneratedCodeContainer();
container.SetOutput(sourceText, codeDocument);
// Position of `ExecuteAsync` in the generated code. // Position of `ExecuteAsync` in the generated code.
var symbol = "ExecuteAsync"; var symbol = "ExecuteAsync";
var span = new TextSpan(generatedCode.IndexOf(symbol), symbol.Length); var span = new TextSpan(generatedCode.IndexOf(symbol), symbol.Length);
// Act // Act
var result = container.TryGetLinePositionSpan(span, out var lineSpan); var result = GeneratedCodeContainer.TryGetLinePositionSpan(span, sourceText, csharpDocument, out var lineSpan);
// Assert // Assert
Assert.False(result); Assert.False(result);