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();
|
||||
|
||||
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)
|
||||
|
|
|
|||
|
|
@ -2,32 +2,89 @@
|
|||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
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.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
|
||||
{
|
||||
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;
|
||||
|
||||
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>()
|
||||
{
|
||||
|
|
@ -39,28 +96,59 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem
|
|||
return default(TService);
|
||||
}
|
||||
|
||||
public void SetOutput(SourceText source, RazorCodeDocument codeDocument)
|
||||
public void SetOutput(RazorCSharpDocument csharpDocument, DefaultDocumentSnapshot document)
|
||||
{
|
||||
Source = source;
|
||||
Output = codeDocument.GetCSharpDocument();
|
||||
lock (_setOutputLock)
|
||||
{
|
||||
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(
|
||||
Document document,
|
||||
IEnumerable<TextSpan> spans,
|
||||
CancellationToken cancellationToken)
|
||||
Document document,
|
||||
IEnumerable<TextSpan> spans,
|
||||
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>();
|
||||
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));
|
||||
}
|
||||
|
|
@ -70,11 +158,11 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem
|
|||
}
|
||||
|
||||
// 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 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.
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
|
@ -103,15 +191,22 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem
|
|||
return false;
|
||||
}
|
||||
|
||||
private void TextContainer_TextChanged(object sender, TextChangeEventArgs args)
|
||||
{
|
||||
GeneratedCodeChanged?.Invoke(this, args);
|
||||
}
|
||||
|
||||
private class TextContainer : SourceTextContainer
|
||||
{
|
||||
public override event EventHandler<TextChangeEventArgs> TextChanged;
|
||||
|
||||
private readonly object _outerLock;
|
||||
private SourceText _currentText;
|
||||
|
||||
public TextContainer()
|
||||
public TextContainer(object outerLock)
|
||||
: this(SourceText.From(string.Empty))
|
||||
{
|
||||
_outerLock = outerLock;
|
||||
}
|
||||
|
||||
public TextContainer(SourceText sourceText)
|
||||
|
|
@ -124,7 +219,16 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem
|
|||
_currentText = sourceText;
|
||||
}
|
||||
|
||||
public override SourceText CurrentText => _currentText;
|
||||
public override SourceText CurrentText
|
||||
{
|
||||
get
|
||||
{
|
||||
lock (_outerLock)
|
||||
{
|
||||
return _currentText;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void SetText(SourceText sourceText)
|
||||
{
|
||||
|
|
@ -133,10 +237,14 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem
|
|||
throw new ArgumentNullException(nameof(sourceText));
|
||||
}
|
||||
|
||||
var e = new TextChangeEventArgs(_currentText, sourceText);
|
||||
_currentText = sourceText;
|
||||
lock (_outerLock)
|
||||
{
|
||||
|
||||
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();
|
||||
|
||||
await Task.Factory.StartNew(
|
||||
() => ReportUpdates(work),
|
||||
CancellationToken.None,
|
||||
TaskCreationOptions.None,
|
||||
_foregroundDispatcher.ForegroundScheduler);
|
||||
|
||||
lock (_work)
|
||||
{
|
||||
// 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)
|
||||
{
|
||||
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 codeDocument = GetCodeDocument(content);
|
||||
var generatedCode = codeDocument.GetCSharpDocument().GeneratedCode;
|
||||
|
||||
var container = new GeneratedCodeContainer();
|
||||
container.SetOutput(sourceText, codeDocument);
|
||||
var csharpDocument = codeDocument.GetCSharpDocument();
|
||||
var generatedCode = csharpDocument.GeneratedCode;
|
||||
|
||||
// TODO: Make writing these tests a little less manual.
|
||||
// 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));
|
||||
|
||||
// Act
|
||||
var result = container.TryGetLinePositionSpan(span, out var lineSpan);
|
||||
var result = GeneratedCodeContainer.TryGetLinePositionSpan(span, sourceText, csharpDocument, out var lineSpan);
|
||||
|
||||
// Assert
|
||||
Assert.True(result);
|
||||
|
|
@ -52,17 +50,15 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem
|
|||
";
|
||||
var sourceText = SourceText.From(content);
|
||||
var codeDocument = GetCodeDocument(content);
|
||||
var generatedCode = codeDocument.GetCSharpDocument().GeneratedCode;
|
||||
|
||||
var container = new GeneratedCodeContainer();
|
||||
container.SetOutput(sourceText, codeDocument);
|
||||
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 = container.TryGetLinePositionSpan(span, out var lineSpan);
|
||||
var result = GeneratedCodeContainer.TryGetLinePositionSpan(span, sourceText, csharpDocument, out var lineSpan);
|
||||
|
||||
// Assert
|
||||
Assert.False(result);
|
||||
|
|
|
|||
Loading…
Reference in New Issue