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:
parent
96709c4d77
commit
da935bfa95
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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(
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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);
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue