Implements versions for generated code
This change implements version tracking the inputs and outputs of generated code. Version tracking is still best-effort - meaning that in some cases a perfect system could avoid doing more work. However, since we base the versions off of all of the inputs, we now that the guarantee that code generation operations that happen 'out of order' will always result in the newer inputs generating the newer outputs. Fixes: https://github.com/aspnet/Razor/issues/2650
This commit is contained in:
parent
029304ae69
commit
357657fc45
|
|
@ -37,9 +37,11 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem
|
|||
|
||||
public override ProjectSnapshot Project => ProjectInternal;
|
||||
|
||||
public override bool SupportsOutput => true;
|
||||
|
||||
public override IReadOnlyList<DocumentSnapshot> GetImports()
|
||||
{
|
||||
return State.Imports.GetImports(Project, this);
|
||||
return State.GetImports(ProjectInternal);
|
||||
}
|
||||
|
||||
public override Task<SourceText> GetTextAsync()
|
||||
|
|
@ -52,10 +54,10 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem
|
|||
return State.GetTextVersionAsync();
|
||||
}
|
||||
|
||||
public override Task<RazorCodeDocument> GetGeneratedOutputAsync()
|
||||
public override async Task<RazorCodeDocument> GetGeneratedOutputAsync()
|
||||
{
|
||||
// IMPORTANT: Don't put more code here. We want this to return a cached task.
|
||||
return State.GeneratedOutput.GetGeneratedOutputInitializationTask(Project, this);
|
||||
var (output, _, _) = await State.GetGeneratedOutputAndVersionAsync(ProjectInternal, this).ConfigureAwait(false);
|
||||
return output;
|
||||
}
|
||||
|
||||
public override bool TryGetText(out SourceText result)
|
||||
|
|
@ -70,9 +72,9 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem
|
|||
|
||||
public override bool TryGetGeneratedOutput(out RazorCodeDocument result)
|
||||
{
|
||||
if (State.GeneratedOutput.IsResultAvailable)
|
||||
if (State.IsGeneratedOutputResultAvailable)
|
||||
{
|
||||
result = State.GeneratedOutput.GetGeneratedOutputInitializationTask(Project, this).Result;
|
||||
result = State.GetGeneratedOutputAndVersionAsync(ProjectInternal, this).Result.output;
|
||||
return true;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,85 @@
|
|||
// 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.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Razor.Language;
|
||||
using Microsoft.CodeAnalysis.Text;
|
||||
|
||||
namespace Microsoft.CodeAnalysis.Razor.ProjectSystem
|
||||
{
|
||||
internal class DefaultImportDocumentSnapshot : DocumentSnapshot
|
||||
{
|
||||
private ProjectSnapshot _project;
|
||||
private RazorProjectItem _importItem;
|
||||
private SourceText _sourceText;
|
||||
private VersionStamp _version;
|
||||
|
||||
public DefaultImportDocumentSnapshot(ProjectSnapshot project, RazorProjectItem item)
|
||||
{
|
||||
_project = project;
|
||||
_importItem = item;
|
||||
_version = VersionStamp.Default;
|
||||
}
|
||||
|
||||
public override string FilePath => null;
|
||||
|
||||
public override string TargetPath => null;
|
||||
|
||||
public override bool SupportsOutput => false;
|
||||
|
||||
public override ProjectSnapshot Project => _project;
|
||||
|
||||
public override Task<RazorCodeDocument> GetGeneratedOutputAsync()
|
||||
{
|
||||
throw new NotSupportedException();
|
||||
}
|
||||
|
||||
public override IReadOnlyList<DocumentSnapshot> GetImports()
|
||||
{
|
||||
return Array.Empty<DocumentSnapshot>();
|
||||
}
|
||||
|
||||
public async override Task<SourceText> GetTextAsync()
|
||||
{
|
||||
using (var stream = _importItem.Read())
|
||||
using (var reader = new StreamReader(stream))
|
||||
{
|
||||
var content = await reader.ReadToEndAsync();
|
||||
_sourceText = SourceText.From(content);
|
||||
}
|
||||
|
||||
return _sourceText;
|
||||
}
|
||||
|
||||
public override Task<VersionStamp> GetTextVersionAsync()
|
||||
{
|
||||
return Task.FromResult(_version);
|
||||
}
|
||||
|
||||
public override bool TryGetText(out SourceText result)
|
||||
{
|
||||
if (_sourceText != null)
|
||||
{
|
||||
result = _sourceText;
|
||||
return true;
|
||||
}
|
||||
|
||||
result = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
public override bool TryGetTextVersion(out VersionStamp result)
|
||||
{
|
||||
result = _version;
|
||||
return true;
|
||||
}
|
||||
|
||||
public override bool TryGetGeneratedOutput(out RazorCodeDocument result)
|
||||
{
|
||||
throw new NotSupportedException();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -89,20 +89,20 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem
|
|||
|
||||
public override RazorProjectEngine GetProjectEngine()
|
||||
{
|
||||
return State.ProjectEngine.GetProjectEngine(this.State);
|
||||
return State.ProjectEngine;
|
||||
}
|
||||
|
||||
public override Task<IReadOnlyList<TagHelperDescriptor>> GetTagHelpersAsync()
|
||||
{
|
||||
// IMPORTANT: Don't put more code here. We want this to return a cached task.
|
||||
return State.TagHelpers.GetTagHelperInitializationTask(this);
|
||||
return State.GetTagHelpersAsync(this);
|
||||
}
|
||||
|
||||
public override bool TryGetTagHelpers(out IReadOnlyList<TagHelperDescriptor> result)
|
||||
{
|
||||
if (State.TagHelpers.IsResultAvailable)
|
||||
if (State.IsTagHelperResultAvailable)
|
||||
{
|
||||
result = State.TagHelpers.GetTagHelperInitializationTask(this).Result;
|
||||
result = State.GetTagHelpersAsync(this).Result;
|
||||
return true;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,179 +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.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Razor.Language;
|
||||
using Microsoft.CodeAnalysis.Text;
|
||||
using Microsoft.Extensions.Internal;
|
||||
|
||||
namespace Microsoft.CodeAnalysis.Razor.ProjectSystem
|
||||
{
|
||||
internal class DocumentGeneratedOutputTracker
|
||||
{
|
||||
private readonly object _lock;
|
||||
|
||||
private DocumentGeneratedOutputTracker _older;
|
||||
private Task<RazorCodeDocument> _task;
|
||||
|
||||
private IReadOnlyList<TagHelperDescriptor> _tagHelpers;
|
||||
private IReadOnlyList<ImportItem> _imports;
|
||||
|
||||
public DocumentGeneratedOutputTracker(DocumentGeneratedOutputTracker older)
|
||||
{
|
||||
_older = older;
|
||||
|
||||
_lock = new object();
|
||||
}
|
||||
|
||||
public bool IsResultAvailable => _task?.IsCompleted == true;
|
||||
|
||||
public DocumentGeneratedOutputTracker Older => _older;
|
||||
|
||||
public Task<RazorCodeDocument> GetGeneratedOutputInitializationTask(ProjectSnapshot project, DocumentSnapshot document)
|
||||
{
|
||||
if (project == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(project));
|
||||
}
|
||||
|
||||
if (document == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(document));
|
||||
}
|
||||
|
||||
if (_task == null)
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
if (_task == null)
|
||||
{
|
||||
_task = GetGeneratedOutputInitializationTaskCore(project, document);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return _task;
|
||||
}
|
||||
|
||||
public DocumentGeneratedOutputTracker Fork()
|
||||
{
|
||||
return new DocumentGeneratedOutputTracker(this);
|
||||
}
|
||||
|
||||
private async Task<RazorCodeDocument> GetGeneratedOutputInitializationTaskCore(ProjectSnapshot project, DocumentSnapshot document)
|
||||
{
|
||||
var tagHelpers = await project.GetTagHelpersAsync().ConfigureAwait(false);
|
||||
var imports = await GetImportsAsync(project, document);
|
||||
|
||||
if (_older != null && _older.IsResultAvailable)
|
||||
{
|
||||
var tagHelperDifference = new HashSet<TagHelperDescriptor>(TagHelperDescriptorComparer.Default);
|
||||
tagHelperDifference.UnionWith(_older._tagHelpers);
|
||||
tagHelperDifference.SymmetricExceptWith(tagHelpers);
|
||||
|
||||
var importDifference = new HashSet<ImportItem>();
|
||||
importDifference.UnionWith(_older._imports);
|
||||
importDifference.SymmetricExceptWith(imports);
|
||||
|
||||
if (tagHelperDifference.Count == 0 && importDifference.Count == 0)
|
||||
{
|
||||
// We can use the cached result.
|
||||
var result = _older._task.Result;
|
||||
|
||||
// Drop reference so it can be GC'ed
|
||||
_older = null;
|
||||
|
||||
// Cache the tag helpers and imports so the next version can use them
|
||||
_tagHelpers = tagHelpers;
|
||||
_imports = imports;
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
// Drop reference so it can be GC'ed
|
||||
_older = null;
|
||||
|
||||
// Cache the tag helpers and imports so the next version can use them
|
||||
_tagHelpers = tagHelpers;
|
||||
_imports = imports;
|
||||
|
||||
var importSources = new List<RazorSourceDocument>();
|
||||
foreach (var item in imports)
|
||||
{
|
||||
var sourceDocument = await GetRazorSourceDocumentAsync(item.Import);
|
||||
importSources.Add(sourceDocument);
|
||||
}
|
||||
|
||||
var documentSource = await GetRazorSourceDocumentAsync(document);
|
||||
|
||||
var projectEngine = project.GetProjectEngine();
|
||||
|
||||
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)
|
||||
{
|
||||
var sourceText = await document.GetTextAsync();
|
||||
|
||||
return sourceText.GetRazorSourceDocument(document.FilePath);
|
||||
}
|
||||
|
||||
private async Task<IReadOnlyList<ImportItem>> GetImportsAsync(ProjectSnapshot project, DocumentSnapshot document)
|
||||
{
|
||||
var imports = new List<ImportItem>();
|
||||
foreach (var snapshot in document.GetImports())
|
||||
{
|
||||
var versionStamp = await snapshot.GetTextVersionAsync();
|
||||
imports.Add(new ImportItem(snapshot.FilePath, versionStamp, snapshot));
|
||||
}
|
||||
|
||||
return imports;
|
||||
}
|
||||
|
||||
private struct ImportItem : IEquatable<ImportItem>
|
||||
{
|
||||
public ImportItem(string filePath, VersionStamp versionStamp, DocumentSnapshot import)
|
||||
{
|
||||
FilePath = filePath;
|
||||
VersionStamp = versionStamp;
|
||||
Import = import;
|
||||
}
|
||||
|
||||
public string FilePath { get; }
|
||||
|
||||
public VersionStamp VersionStamp { get; }
|
||||
|
||||
public DocumentSnapshot Import { get; }
|
||||
|
||||
public bool Equals(ImportItem other)
|
||||
{
|
||||
return
|
||||
FilePathComparer.Instance.Equals(FilePath, other.FilePath) &&
|
||||
VersionStamp == other.VersionStamp;
|
||||
}
|
||||
|
||||
public override bool Equals(object obj)
|
||||
{
|
||||
return obj is ImportItem item ? Equals(item) : false;
|
||||
}
|
||||
|
||||
public override int GetHashCode()
|
||||
{
|
||||
var hash = new HashCodeCombiner();
|
||||
hash.Add(FilePath, FilePathComparer.Instance);
|
||||
hash.Add(VersionStamp);
|
||||
return hash;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,167 +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.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Razor.Language;
|
||||
using Microsoft.CodeAnalysis.Text;
|
||||
|
||||
namespace Microsoft.CodeAnalysis.Razor.ProjectSystem
|
||||
{
|
||||
internal class DocumentImportsTracker
|
||||
{
|
||||
private readonly object _lock;
|
||||
|
||||
private IReadOnlyList<DocumentSnapshot> _imports;
|
||||
|
||||
public DocumentImportsTracker()
|
||||
{
|
||||
_lock = new object();
|
||||
}
|
||||
|
||||
public IReadOnlyList<DocumentSnapshot> GetImports(ProjectSnapshot project, DocumentSnapshot document)
|
||||
{
|
||||
if (project == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(project));
|
||||
}
|
||||
|
||||
if (document == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(document));
|
||||
}
|
||||
|
||||
if (_imports == null)
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
if (_imports == null)
|
||||
{
|
||||
_imports = GetImportsCore(project, document);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return _imports;
|
||||
}
|
||||
|
||||
private IReadOnlyList<DocumentSnapshot> GetImportsCore(ProjectSnapshot project, DocumentSnapshot document)
|
||||
{
|
||||
var projectEngine = project.GetProjectEngine();
|
||||
var importFeature = projectEngine.ProjectFeatures.OfType<IImportProjectFeature>().FirstOrDefault();
|
||||
var projectItem = projectEngine.FileSystem.GetItem(document.FilePath);
|
||||
var importItems = importFeature?.GetImports(projectItem).Where(i => i.Exists);
|
||||
if (importItems == null)
|
||||
{
|
||||
return Array.Empty<DocumentSnapshot>();
|
||||
}
|
||||
|
||||
var imports = new List<DocumentSnapshot>();
|
||||
foreach (var item in importItems)
|
||||
{
|
||||
if (item.PhysicalPath == null)
|
||||
{
|
||||
// This is a default import.
|
||||
var defaultImport = new DefaultImportDocumentSnapshot(project, item);
|
||||
imports.Add(defaultImport);
|
||||
}
|
||||
else
|
||||
{
|
||||
var import = project.GetDocument(item.PhysicalPath);
|
||||
if (import == null)
|
||||
{
|
||||
// We are not tracking this document in this project. So do nothing.
|
||||
continue;
|
||||
}
|
||||
|
||||
imports.Add(import);
|
||||
}
|
||||
}
|
||||
|
||||
return imports;
|
||||
}
|
||||
|
||||
private class DefaultImportDocumentSnapshot : DocumentSnapshot
|
||||
{
|
||||
private ProjectSnapshot _project;
|
||||
private RazorProjectItem _importItem;
|
||||
private SourceText _sourceText;
|
||||
private VersionStamp _version;
|
||||
private DocumentGeneratedOutputTracker _generatedOutput;
|
||||
|
||||
public DefaultImportDocumentSnapshot(ProjectSnapshot project, RazorProjectItem item)
|
||||
{
|
||||
_project = project;
|
||||
_importItem = item;
|
||||
_version = VersionStamp.Default;
|
||||
_generatedOutput = new DocumentGeneratedOutputTracker(null);
|
||||
}
|
||||
|
||||
public override string FilePath => null;
|
||||
|
||||
public override string TargetPath => null;
|
||||
|
||||
public override ProjectSnapshot Project => _project;
|
||||
|
||||
public override Task<RazorCodeDocument> GetGeneratedOutputAsync()
|
||||
{
|
||||
return _generatedOutput.GetGeneratedOutputInitializationTask(_project, this);
|
||||
}
|
||||
|
||||
public override IReadOnlyList<DocumentSnapshot> GetImports()
|
||||
{
|
||||
return Array.Empty<DocumentSnapshot>();
|
||||
}
|
||||
|
||||
public async override Task<SourceText> GetTextAsync()
|
||||
{
|
||||
using (var stream = _importItem.Read())
|
||||
using (var reader = new StreamReader(stream))
|
||||
{
|
||||
var content = await reader.ReadToEndAsync();
|
||||
_sourceText = SourceText.From(content);
|
||||
}
|
||||
|
||||
return _sourceText;
|
||||
}
|
||||
|
||||
public override Task<VersionStamp> GetTextVersionAsync()
|
||||
{
|
||||
return Task.FromResult(_version);
|
||||
}
|
||||
|
||||
public override bool TryGetText(out SourceText result)
|
||||
{
|
||||
if (_sourceText != null)
|
||||
{
|
||||
result = _sourceText;
|
||||
return true;
|
||||
}
|
||||
|
||||
result = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
public override bool TryGetTextVersion(out VersionStamp result)
|
||||
{
|
||||
result = _version;
|
||||
return true;
|
||||
}
|
||||
|
||||
public override bool TryGetGeneratedOutput(out RazorCodeDocument result)
|
||||
{
|
||||
if (_generatedOutput.IsResultAvailable)
|
||||
{
|
||||
result = GetGeneratedOutputAsync().Result;
|
||||
return true;
|
||||
}
|
||||
|
||||
result = null;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -16,6 +16,8 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem
|
|||
|
||||
public abstract ProjectSnapshot Project { get; }
|
||||
|
||||
public abstract bool SupportsOutput { get; }
|
||||
|
||||
public abstract IReadOnlyList<DocumentSnapshot> GetImports();
|
||||
|
||||
public abstract Task<SourceText> GetTextAsync();
|
||||
|
|
|
|||
|
|
@ -2,7 +2,10 @@
|
|||
// 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.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Razor.Language;
|
||||
using Microsoft.CodeAnalysis.Host;
|
||||
using Microsoft.CodeAnalysis.Text;
|
||||
|
||||
|
|
@ -18,14 +21,13 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem
|
|||
|
||||
private readonly object _lock;
|
||||
|
||||
private ComputedStateTracker _computedState;
|
||||
|
||||
private Func<Task<TextAndVersion>> _loader;
|
||||
private Task<TextAndVersion> _loaderTask;
|
||||
private SourceText _sourceText;
|
||||
private VersionStamp? _version;
|
||||
|
||||
private DocumentGeneratedOutputTracker _generatedOutput;
|
||||
private DocumentImportsTracker _imports;
|
||||
|
||||
public static DocumentState Create(
|
||||
HostWorkspaceServices services,
|
||||
HostDocument hostDocument,
|
||||
|
|
@ -67,42 +69,35 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem
|
|||
|
||||
public GeneratedCodeContainer GeneratedCodeContainer => HostDocument.GeneratedCodeContainer;
|
||||
|
||||
public DocumentGeneratedOutputTracker GeneratedOutput
|
||||
public bool IsGeneratedOutputResultAvailable => ComputedState.IsResultAvailable == true;
|
||||
|
||||
private ComputedStateTracker ComputedState
|
||||
{
|
||||
get
|
||||
{
|
||||
if (_generatedOutput == null)
|
||||
if (_computedState == null)
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
if (_generatedOutput == null)
|
||||
if (_computedState == null)
|
||||
{
|
||||
_generatedOutput = new DocumentGeneratedOutputTracker(null);
|
||||
_computedState = new ComputedStateTracker(this);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return _generatedOutput;
|
||||
return _computedState;
|
||||
}
|
||||
}
|
||||
|
||||
public DocumentImportsTracker Imports
|
||||
public Task<(RazorCodeDocument output, VersionStamp inputVersion, VersionStamp outputVersion)> GetGeneratedOutputAndVersionAsync(DefaultProjectSnapshot project, DefaultDocumentSnapshot document)
|
||||
{
|
||||
get
|
||||
{
|
||||
if (_imports == null)
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
if (_imports == null)
|
||||
{
|
||||
_imports = new DocumentImportsTracker();
|
||||
}
|
||||
}
|
||||
}
|
||||
return ComputedState.GetGeneratedOutputAndVersionAsync(project, document);
|
||||
}
|
||||
|
||||
return _imports;
|
||||
}
|
||||
public IReadOnlyList<DocumentSnapshot> GetImports(DefaultProjectSnapshot project)
|
||||
{
|
||||
return GetImportsCore(project);
|
||||
}
|
||||
|
||||
public async Task<SourceText> GetTextAsync()
|
||||
|
|
@ -180,6 +175,8 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem
|
|||
state._version = _version;
|
||||
state._loaderTask = _loaderTask;
|
||||
|
||||
// Do not cache computed state
|
||||
|
||||
return state;
|
||||
}
|
||||
|
||||
|
|
@ -192,6 +189,9 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem
|
|||
state._version = _version;
|
||||
state._loaderTask = _loaderTask;
|
||||
|
||||
// Optimisically cache the computed state
|
||||
state._computedState = new ComputedStateTracker(state, _computedState);
|
||||
|
||||
return state;
|
||||
}
|
||||
|
||||
|
|
@ -204,8 +204,8 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem
|
|||
state._version = _version;
|
||||
state._loaderTask = _loaderTask;
|
||||
|
||||
// Opportunistically cache the generated code
|
||||
state._generatedOutput = _generatedOutput?.Fork();
|
||||
// Optimisically cache the computed state
|
||||
state._computedState = new ComputedStateTracker(state, _computedState);
|
||||
|
||||
return state;
|
||||
}
|
||||
|
|
@ -217,6 +217,8 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem
|
|||
throw new ArgumentNullException(nameof(sourceText));
|
||||
}
|
||||
|
||||
// Do not cache the computed state
|
||||
|
||||
return new DocumentState(Services, HostDocument, sourceText, version, null);
|
||||
}
|
||||
|
||||
|
|
@ -227,7 +229,239 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem
|
|||
throw new ArgumentNullException(nameof(loader));
|
||||
}
|
||||
|
||||
// Do not cache the computed state
|
||||
|
||||
return new DocumentState(Services, HostDocument, null, null, loader);
|
||||
}
|
||||
|
||||
private IReadOnlyList<DocumentSnapshot> GetImportsCore(DefaultProjectSnapshot project)
|
||||
{
|
||||
var projectEngine = project.GetProjectEngine();
|
||||
var importFeature = projectEngine.ProjectFeatures.OfType<IImportProjectFeature>().FirstOrDefault();
|
||||
var projectItem = projectEngine.FileSystem.GetItem(HostDocument.FilePath);
|
||||
var importItems = importFeature?.GetImports(projectItem);
|
||||
if (importItems == null)
|
||||
{
|
||||
return Array.Empty<DocumentSnapshot>();
|
||||
}
|
||||
|
||||
var imports = new List<DocumentSnapshot>();
|
||||
foreach (var item in importItems)
|
||||
{
|
||||
if (item.PhysicalPath == null)
|
||||
{
|
||||
// This is a default import.
|
||||
var defaultImport = new DefaultImportDocumentSnapshot(project, item);
|
||||
imports.Add(defaultImport);
|
||||
}
|
||||
else
|
||||
{
|
||||
var import = project.GetDocument(item.PhysicalPath);
|
||||
if (import == null)
|
||||
{
|
||||
// We are not tracking this document in this project. So do nothing.
|
||||
continue;
|
||||
}
|
||||
|
||||
imports.Add(import);
|
||||
}
|
||||
}
|
||||
|
||||
return imports;
|
||||
}
|
||||
|
||||
// See design notes on ProjectState.ComputedStateTracker.
|
||||
private class ComputedStateTracker
|
||||
{
|
||||
private readonly object _lock;
|
||||
|
||||
private ComputedStateTracker _older;
|
||||
public Task<(RazorCodeDocument, VersionStamp, VersionStamp)> TaskUnsafe;
|
||||
|
||||
public ComputedStateTracker(DocumentState state, ComputedStateTracker older = null)
|
||||
{
|
||||
_lock = state._lock;
|
||||
_older = older;
|
||||
}
|
||||
|
||||
public bool IsResultAvailable => TaskUnsafe?.IsCompleted == true;
|
||||
|
||||
public Task<(RazorCodeDocument, VersionStamp, VersionStamp)> GetGeneratedOutputAndVersionAsync(DefaultProjectSnapshot project, DocumentSnapshot document)
|
||||
{
|
||||
if (project == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(project));
|
||||
}
|
||||
|
||||
if (document == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(document));
|
||||
}
|
||||
|
||||
if (TaskUnsafe == null)
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
if (TaskUnsafe == null)
|
||||
{
|
||||
TaskUnsafe = GetGeneratedOutputAndVersionCoreAsync(project, document);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return TaskUnsafe;
|
||||
}
|
||||
|
||||
private async Task<(RazorCodeDocument, VersionStamp, VersionStamp)> GetGeneratedOutputAndVersionCoreAsync(DefaultProjectSnapshot project, DocumentSnapshot document)
|
||||
{
|
||||
// We only need to produce the generated code if any of our inputs is newer than the
|
||||
// previously cached output.
|
||||
//
|
||||
// First find the versions that are the inputs:
|
||||
// - The project + computed state
|
||||
// - The imports
|
||||
// - This document
|
||||
//
|
||||
// All of these things are cached, so no work is wasted if we do need to generate the code.
|
||||
var computedStateVersion = await project.State.GetComputedStateVersionAsync(project).ConfigureAwait(false);
|
||||
var documentCollectionVersion = project.State.DocumentCollectionVersion;
|
||||
var imports = await GetImportsAsync(project, document).ConfigureAwait(false);
|
||||
var documentVersion = await document.GetTextVersionAsync().ConfigureAwait(false);
|
||||
|
||||
// OK now that have the previous output and all of the versions, we can see if anything
|
||||
// has changed that would require regenerating the code.
|
||||
var inputVersion = documentVersion;
|
||||
if (inputVersion.GetNewerVersion(computedStateVersion) == computedStateVersion)
|
||||
{
|
||||
inputVersion = computedStateVersion;
|
||||
}
|
||||
|
||||
if (inputVersion.GetNewerVersion(documentCollectionVersion) == documentCollectionVersion)
|
||||
{
|
||||
inputVersion = documentCollectionVersion;
|
||||
}
|
||||
|
||||
for (var i = 0; i < imports.Count; i++)
|
||||
{
|
||||
var importVersion = imports[i].Version;
|
||||
if (inputVersion.GetNewerVersion(importVersion) == importVersion)
|
||||
{
|
||||
inputVersion = importVersion;
|
||||
}
|
||||
}
|
||||
|
||||
RazorCodeDocument olderOutput = null;
|
||||
var olderInputVersion = default(VersionStamp);
|
||||
var olderOutputVersion = default(VersionStamp);
|
||||
if (_older?.TaskUnsafe != null)
|
||||
{
|
||||
(olderOutput, olderInputVersion, olderOutputVersion) = await _older.TaskUnsafe.ConfigureAwait(false);
|
||||
if (inputVersion.GetNewerVersion(olderInputVersion) == olderInputVersion)
|
||||
{
|
||||
// Nothing has changed, we can use the cached result.
|
||||
lock (_lock)
|
||||
{
|
||||
TaskUnsafe = _older.TaskUnsafe;
|
||||
_older = null;
|
||||
return (olderOutput, olderInputVersion, olderOutputVersion);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// OK we have to generate the code.
|
||||
var tagHelpers = await project.GetTagHelpersAsync().ConfigureAwait(false);
|
||||
var importSources = new List<RazorSourceDocument>();
|
||||
foreach (var item in imports)
|
||||
{
|
||||
var sourceDocument = await GetRazorSourceDocumentAsync(item.Document).ConfigureAwait(false);
|
||||
importSources.Add(sourceDocument);
|
||||
}
|
||||
|
||||
var documentSource = await GetRazorSourceDocumentAsync(document).ConfigureAwait(false);
|
||||
|
||||
var projectEngine = project.GetProjectEngine();
|
||||
|
||||
var codeDocument = projectEngine.ProcessDesignTime(documentSource, importSources, tagHelpers);
|
||||
var csharpDocument = codeDocument.GetCSharpDocument();
|
||||
|
||||
// OK now we've generated the code. Let's check if the output is actually different. This is
|
||||
// a valuable optimization for our use cases because lots of changes you could make require
|
||||
// us to run code generation, but don't change the result.
|
||||
//
|
||||
// Note that we're talking about the effect on the generated C# code here (not the other artifacts).
|
||||
// This is the reason why we have two versions associated with the output.
|
||||
//
|
||||
// The INPUT version is related the .cshtml files and tag helpers
|
||||
// The OUTPUT version is related to the generated C#.
|
||||
//
|
||||
// Examples:
|
||||
//
|
||||
// A change to a tag helper not used by this document - updates the INPUT version, but not
|
||||
// the OUTPUT version.
|
||||
//
|
||||
// A change in the HTML - updates the INPUT version, but not the OUTPUT version.
|
||||
//
|
||||
//
|
||||
// Razor IDE features should always retrieve the output and party on it regardless. Depending
|
||||
// on the use cases we may or may not need to synchronize the output.
|
||||
|
||||
var outputVersion = inputVersion;
|
||||
if (olderOutput != null)
|
||||
{
|
||||
if (string.Equals(
|
||||
olderOutput.GetCSharpDocument().GeneratedCode,
|
||||
csharpDocument.GeneratedCode,
|
||||
StringComparison.Ordinal))
|
||||
{
|
||||
outputVersion = olderOutputVersion;
|
||||
}
|
||||
}
|
||||
|
||||
if (document is DefaultDocumentSnapshot defaultDocument)
|
||||
{
|
||||
defaultDocument.State.HostDocument.GeneratedCodeContainer.SetOutput(
|
||||
defaultDocument,
|
||||
csharpDocument,
|
||||
inputVersion,
|
||||
outputVersion);
|
||||
}
|
||||
|
||||
return (codeDocument, inputVersion, outputVersion);
|
||||
}
|
||||
|
||||
private async Task<RazorSourceDocument> GetRazorSourceDocumentAsync(DocumentSnapshot document)
|
||||
{
|
||||
var sourceText = await document.GetTextAsync();
|
||||
return sourceText.GetRazorSourceDocument(document.FilePath);
|
||||
}
|
||||
|
||||
private async Task<IReadOnlyList<ImportItem>> GetImportsAsync(ProjectSnapshot project, DocumentSnapshot document)
|
||||
{
|
||||
var imports = new List<ImportItem>();
|
||||
foreach (var snapshot in document.GetImports())
|
||||
{
|
||||
var versionStamp = await snapshot.GetTextVersionAsync();
|
||||
imports.Add(new ImportItem(snapshot.FilePath, versionStamp, snapshot));
|
||||
}
|
||||
|
||||
return imports;
|
||||
}
|
||||
|
||||
private readonly struct ImportItem
|
||||
{
|
||||
public ImportItem(string filePath, VersionStamp version, DocumentSnapshot document)
|
||||
{
|
||||
FilePath = filePath;
|
||||
Version = version;
|
||||
Document = document;
|
||||
}
|
||||
|
||||
public string FilePath { get; }
|
||||
|
||||
public VersionStamp Version { get; }
|
||||
|
||||
public DocumentSnapshot Document { get; }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,7 +13,8 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem
|
|||
public event EventHandler<TextChangeEventArgs> GeneratedCodeChanged;
|
||||
|
||||
private SourceText _source;
|
||||
private VersionStamp? _sourceVersion;
|
||||
private VersionStamp? _inputVersion;
|
||||
private VersionStamp? _outputVersion;
|
||||
private RazorCSharpDocument _output;
|
||||
private DocumentSnapshot _latestDocument;
|
||||
|
||||
|
|
@ -37,13 +38,24 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem
|
|||
}
|
||||
}
|
||||
|
||||
public VersionStamp SourceVersion
|
||||
public VersionStamp InputVersion
|
||||
{
|
||||
get
|
||||
{
|
||||
lock (_setOutputLock)
|
||||
{
|
||||
return _sourceVersion.Value;
|
||||
return _inputVersion.Value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public VersionStamp OutputVersion
|
||||
{
|
||||
get
|
||||
{
|
||||
lock (_setOutputLock)
|
||||
{
|
||||
return _outputVersion.Value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -81,19 +93,17 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem
|
|||
}
|
||||
}
|
||||
|
||||
public void SetOutput(RazorCSharpDocument csharpDocument, DefaultDocumentSnapshot document)
|
||||
public void SetOutput(
|
||||
DefaultDocumentSnapshot document,
|
||||
RazorCSharpDocument output,
|
||||
VersionStamp inputVersion,
|
||||
VersionStamp outputVersion)
|
||||
{
|
||||
lock (_setOutputLock)
|
||||
{
|
||||
if (!document.TryGetTextVersion(out var version))
|
||||
{
|
||||
Debug.Fail("The text version should have already been evaluated.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (_sourceVersion.HasValue &&
|
||||
_sourceVersion != version &&
|
||||
_sourceVersion == SourceVersion.GetNewerVersion(version))
|
||||
if (_inputVersion.HasValue &&
|
||||
_inputVersion != inputVersion &&
|
||||
_inputVersion == _inputVersion.Value.GetNewerVersion(inputVersion))
|
||||
{
|
||||
// Latest document is newer than the provided document.
|
||||
return;
|
||||
|
|
@ -106,10 +116,11 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem
|
|||
}
|
||||
|
||||
_source = source;
|
||||
_sourceVersion = version;
|
||||
_output = csharpDocument;
|
||||
_inputVersion = inputVersion;
|
||||
_outputVersion = outputVersion;
|
||||
_output = output;
|
||||
_latestDocument = document;
|
||||
_textContainer.SetText(SourceText.From(Output.GeneratedCode));
|
||||
_textContainer.SetText(SourceText.From(_output.GeneratedCode));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,88 +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.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using Microsoft.AspNetCore.Razor.Language;
|
||||
using Microsoft.CodeAnalysis.Host;
|
||||
|
||||
namespace Microsoft.CodeAnalysis.Razor.ProjectSystem
|
||||
{
|
||||
internal class ProjectEngineTracker
|
||||
{
|
||||
private const ProjectDifference Mask = ProjectDifference.ConfigurationChanged;
|
||||
|
||||
private readonly object _lock = new object();
|
||||
|
||||
private readonly HostWorkspaceServices _services;
|
||||
private RazorProjectEngine _projectEngine;
|
||||
|
||||
public ProjectEngineTracker(ProjectState state)
|
||||
{
|
||||
if (state == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(state));
|
||||
}
|
||||
|
||||
_services = state.Services;
|
||||
}
|
||||
|
||||
public ProjectEngineTracker ForkFor(ProjectState state, ProjectDifference difference)
|
||||
{
|
||||
if (state == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(state));
|
||||
}
|
||||
|
||||
if ((difference & Mask) != 0)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
public RazorProjectEngine GetProjectEngine(ProjectState state)
|
||||
{
|
||||
if (state == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(state));
|
||||
}
|
||||
|
||||
if (_projectEngine == null)
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
if (_projectEngine == null)
|
||||
{
|
||||
var factory = _services.GetRequiredService<ProjectSnapshotProjectEngineFactory>();
|
||||
_projectEngine = factory.Create(state.HostProject.Configuration, Path.GetDirectoryName(state.HostProject.FilePath), configure: null);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return _projectEngine;
|
||||
}
|
||||
|
||||
public List<string> GetImportDocumentTargetPaths(ProjectState state, string targetPath)
|
||||
{
|
||||
var projectEngine = GetProjectEngine(state);
|
||||
var importFeature = projectEngine.ProjectFeatures.OfType<IImportProjectFeature>().FirstOrDefault();
|
||||
var projectItem = projectEngine.FileSystem.GetItem(targetPath);
|
||||
var importItems = importFeature?.GetImports(projectItem).Where(i => i.FilePath != null);
|
||||
|
||||
// Target path looks like `Foo\\Bar.cshtml`
|
||||
var targetPaths = new List<string>();
|
||||
foreach (var importItem in importItems)
|
||||
{
|
||||
var itemTargetPath = importItem.FilePath.Replace('/', '\\').TrimStart('\\');
|
||||
targetPaths.Add(itemTargetPath);
|
||||
}
|
||||
|
||||
return targetPaths;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -4,7 +4,10 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Razor.Language;
|
||||
using Microsoft.CodeAnalysis.Host;
|
||||
using Microsoft.CodeAnalysis.Text;
|
||||
|
||||
|
|
@ -13,12 +16,24 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem
|
|||
// Internal tracker for DefaultProjectSnapshot
|
||||
internal class ProjectState
|
||||
{
|
||||
private const ProjectDifference ClearComputedStateMask = ProjectDifference.ConfigurationChanged;
|
||||
|
||||
private const ProjectDifference ClearCachedTagHelpersMask =
|
||||
ProjectDifference.ConfigurationChanged |
|
||||
ProjectDifference.WorkspaceProjectAdded |
|
||||
ProjectDifference.WorkspaceProjectChanged |
|
||||
ProjectDifference.WorkspaceProjectRemoved;
|
||||
|
||||
private const ProjectDifference ClearDocumentCollectionVersionMask =
|
||||
ProjectDifference.ConfigurationChanged |
|
||||
ProjectDifference.DocumentAdded |
|
||||
ProjectDifference.DocumentRemoved;
|
||||
|
||||
private static readonly ImmutableDictionary<string, DocumentState> EmptyDocuments = ImmutableDictionary.Create<string, DocumentState>(FilePathComparer.Instance);
|
||||
private static readonly ImmutableDictionary<string, ImmutableArray<string>> EmptyImportsToRelatedDocuments = ImmutableDictionary.Create<string, ImmutableArray<string>>(FilePathComparer.Instance);
|
||||
private readonly object _lock;
|
||||
|
||||
private ProjectEngineTracker _projectEngine;
|
||||
private ProjectTagHelperTracker _tagHelpers;
|
||||
|
||||
private ComputedStateTracker _computedState;
|
||||
|
||||
public static ProjectState Create(HostWorkspaceServices services, HostProject hostProject, Project workspaceProject = null)
|
||||
{
|
||||
|
|
@ -34,7 +49,7 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem
|
|||
|
||||
return new ProjectState(services, hostProject, workspaceProject);
|
||||
}
|
||||
|
||||
|
||||
private ProjectState(
|
||||
HostWorkspaceServices services,
|
||||
HostProject hostProject,
|
||||
|
|
@ -46,6 +61,7 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem
|
|||
Documents = EmptyDocuments;
|
||||
ImportsToRelatedDocuments = EmptyImportsToRelatedDocuments;
|
||||
Version = VersionStamp.Create();
|
||||
DocumentCollectionVersion = Version;
|
||||
|
||||
_lock = new object();
|
||||
}
|
||||
|
|
@ -88,8 +104,27 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem
|
|||
|
||||
_lock = new object();
|
||||
|
||||
_projectEngine = older._projectEngine?.ForkFor(this, difference);
|
||||
_tagHelpers = older._tagHelpers?.ForkFor(this, difference);
|
||||
if ((difference & ClearDocumentCollectionVersionMask) == 0)
|
||||
{
|
||||
// Document collection hasn't changed
|
||||
DocumentCollectionVersion = older.DocumentCollectionVersion;
|
||||
}
|
||||
else
|
||||
{
|
||||
DocumentCollectionVersion = Version;
|
||||
}
|
||||
|
||||
if ((difference & ClearComputedStateMask) == 0 && older._computedState != null)
|
||||
{
|
||||
// Optimistically cache the RazorProjectEngine.
|
||||
_computedState = new ComputedStateTracker(this, older._computedState);
|
||||
}
|
||||
|
||||
if ((difference & ClearCachedTagHelpersMask) == 0 && _computedState != null)
|
||||
{
|
||||
// It's OK to keep the computed Tag Helpers.
|
||||
_computedState.TaskUnsafe = older._computedState?.TaskUnsafe;
|
||||
}
|
||||
}
|
||||
|
||||
// Internal set for testing.
|
||||
|
|
@ -104,46 +139,68 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem
|
|||
|
||||
public Project WorkspaceProject { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the version of this project, INCLUDING content changes. The <see cref="Version"/> is
|
||||
/// incremented for each new <see cref="ProjectState"/> instance created.
|
||||
/// </summary>
|
||||
public VersionStamp Version { get; }
|
||||
|
||||
// Computed State
|
||||
public ProjectEngineTracker ProjectEngine
|
||||
/// <summary>
|
||||
/// Gets the version of this project, NOT INCLUDING computed or content changes. The
|
||||
/// <see cref="DocumentCollectionVersion"/> is incremented each time the configuration changes or
|
||||
/// a document is added or removed.
|
||||
/// </summary>
|
||||
public VersionStamp DocumentCollectionVersion { get; }
|
||||
|
||||
public RazorProjectEngine ProjectEngine => ComputedState.ProjectEngine;
|
||||
|
||||
public bool IsTagHelperResultAvailable => ComputedState.TaskUnsafe?.IsCompleted == true;
|
||||
|
||||
private ComputedStateTracker ComputedState
|
||||
{
|
||||
get
|
||||
{
|
||||
if (_projectEngine == null)
|
||||
if (_computedState == null)
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
if (_projectEngine == null)
|
||||
if (_computedState == null)
|
||||
{
|
||||
_projectEngine = new ProjectEngineTracker(this);
|
||||
_computedState = new ComputedStateTracker(this);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return _projectEngine;
|
||||
return _computedState;
|
||||
}
|
||||
}
|
||||
|
||||
// Computed State
|
||||
public ProjectTagHelperTracker TagHelpers
|
||||
/// <summary>
|
||||
/// Gets the version of this project based on the computed state, NOT INCLUDING content
|
||||
/// changes. The computed state is guaranteed to change when the configuration or tag helpers
|
||||
/// change.
|
||||
/// </summary>
|
||||
/// <returns>Asynchronously returns the computed version.</returns>
|
||||
public async Task<VersionStamp> GetComputedStateVersionAsync(ProjectSnapshot snapshot)
|
||||
{
|
||||
get
|
||||
if (snapshot == null)
|
||||
{
|
||||
if (_tagHelpers == null)
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
if (_tagHelpers == null)
|
||||
{
|
||||
_tagHelpers = new ProjectTagHelperTracker(this);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return _tagHelpers;
|
||||
throw new ArgumentNullException(nameof(snapshot));
|
||||
}
|
||||
|
||||
var (_, version) = await ComputedState.GetTagHelpersAndVersionAsync(snapshot).ConfigureAwait(false);
|
||||
return version;
|
||||
}
|
||||
|
||||
public async Task<IReadOnlyList<TagHelperDescriptor>> GetTagHelpersAsync(ProjectSnapshot snapshot)
|
||||
{
|
||||
if (snapshot == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(snapshot));
|
||||
}
|
||||
|
||||
var (tagHelpers, _) = await ComputedState.GetTagHelpersAndVersionAsync(snapshot).ConfigureAwait(false);
|
||||
return tagHelpers;
|
||||
}
|
||||
|
||||
public ProjectState WithAddedHostDocument(HostDocument hostDocument, Func<Task<TextAndVersion>> loader)
|
||||
|
|
@ -164,13 +221,13 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem
|
|||
{
|
||||
return this;
|
||||
}
|
||||
|
||||
|
||||
var documents = Documents.Add(hostDocument.FilePath, DocumentState.Create(Services, hostDocument, loader));
|
||||
|
||||
// Compute the effect on the import map
|
||||
var importTargetPaths = ProjectEngine.GetImportDocumentTargetPaths(this, hostDocument.TargetPath);
|
||||
var importTargetPaths = GetImportDocumentTargetPaths(hostDocument.TargetPath);
|
||||
var importsToRelatedDocuments = AddToImportsToRelatedDocuments(ImportsToRelatedDocuments, hostDocument, importTargetPaths);
|
||||
|
||||
|
||||
// Now check if the updated document is an import - it's important this this happens after
|
||||
// updating the imports map.
|
||||
if (importsToRelatedDocuments.TryGetValue(hostDocument.TargetPath, out var relatedDocuments))
|
||||
|
|
@ -196,7 +253,7 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem
|
|||
{
|
||||
return this;
|
||||
}
|
||||
|
||||
|
||||
var documents = Documents.Remove(hostDocument.FilePath);
|
||||
|
||||
// First check if the updated document is an import - it's important that this happens
|
||||
|
|
@ -210,7 +267,7 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem
|
|||
}
|
||||
|
||||
// Compute the effect on the import map
|
||||
var importTargetPaths = ProjectEngine.GetImportDocumentTargetPaths(this, hostDocument.TargetPath);
|
||||
var importTargetPaths = GetImportDocumentTargetPaths(hostDocument.TargetPath);
|
||||
var importsToRelatedDocuments = RemoveFromImportsToRelatedDocuments(ImportsToRelatedDocuments, hostDocument, importTargetPaths);
|
||||
|
||||
var state = new ProjectState(this, ProjectDifference.DocumentRemoved, HostProject, WorkspaceProject, documents, importsToRelatedDocuments);
|
||||
|
|
@ -280,7 +337,7 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem
|
|||
{
|
||||
return this;
|
||||
}
|
||||
|
||||
|
||||
var documents = Documents.ToImmutableDictionary(kvp => kvp.Key, kvp => kvp.Value.WithConfigurationChange(), FilePathComparer.Instance);
|
||||
|
||||
// If the host project has changed then we need to recompute the imports map
|
||||
|
|
@ -288,11 +345,10 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem
|
|||
|
||||
foreach (var document in documents)
|
||||
{
|
||||
var importTargetPaths = ProjectEngine.GetImportDocumentTargetPaths(this, document.Value.HostDocument.TargetPath);
|
||||
var importTargetPaths = GetImportDocumentTargetPaths(document.Value.HostDocument.TargetPath);
|
||||
importsToRelatedDocuments = AddToImportsToRelatedDocuments(ImportsToRelatedDocuments, document.Value.HostDocument, importTargetPaths);
|
||||
}
|
||||
|
||||
|
||||
var state = new ProjectState(this, ProjectDifference.ConfigurationChanged, hostProject, WorkspaceProject, documents, importsToRelatedDocuments);
|
||||
return state;
|
||||
}
|
||||
|
|
@ -367,5 +423,114 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem
|
|||
|
||||
return importsToRelatedDocuments;
|
||||
}
|
||||
|
||||
private RazorProjectEngine CreateProjectEngine()
|
||||
{
|
||||
var factory = Services.GetRequiredService<ProjectSnapshotProjectEngineFactory>();
|
||||
return factory.Create(HostProject.Configuration, Path.GetDirectoryName(HostProject.FilePath), configure: null);
|
||||
}
|
||||
|
||||
public List<string> GetImportDocumentTargetPaths(string targetPath)
|
||||
{
|
||||
var projectEngine = ComputedState.ProjectEngine;
|
||||
var importFeature = projectEngine.ProjectFeatures.OfType<IImportProjectFeature>().FirstOrDefault();
|
||||
var projectItem = projectEngine.FileSystem.GetItem(targetPath);
|
||||
var importItems = importFeature?.GetImports(projectItem).Where(i => i.FilePath != null);
|
||||
|
||||
// Target path looks like `Foo\\Bar.cshtml`
|
||||
var targetPaths = new List<string>();
|
||||
foreach (var importItem in importItems)
|
||||
{
|
||||
var itemTargetPath = importItem.FilePath.Replace('/', '\\').TrimStart('\\');
|
||||
targetPaths.Add(itemTargetPath);
|
||||
}
|
||||
|
||||
return targetPaths;
|
||||
}
|
||||
|
||||
// ComputedStateTracker is the 'holder' of all of the state that can be cached based on
|
||||
// the data in a ProjectState. It should not hold onto a ProjectState directly
|
||||
// as that could lead to things being in memory longer than we want them to.
|
||||
//
|
||||
// Rather, a ComputedStateTracker instance can hold on to a previous instance from an older
|
||||
// version of the same project.
|
||||
private class ComputedStateTracker
|
||||
{
|
||||
// ProjectState.Version
|
||||
private readonly VersionStamp _projectStateVersion;
|
||||
private readonly object _lock;
|
||||
|
||||
private ComputedStateTracker _older; // We be set to null when state is computed
|
||||
public Task<(IReadOnlyList<TagHelperDescriptor>, VersionStamp)> TaskUnsafe;
|
||||
|
||||
public ComputedStateTracker(ProjectState state, ComputedStateTracker older = null)
|
||||
{
|
||||
_projectStateVersion = state.Version;
|
||||
_lock = state._lock;
|
||||
_older = older;
|
||||
|
||||
ProjectEngine = _older?.ProjectEngine;
|
||||
if (ProjectEngine == null)
|
||||
{
|
||||
ProjectEngine = state.CreateProjectEngine();
|
||||
}
|
||||
}
|
||||
|
||||
public RazorProjectEngine ProjectEngine { get; }
|
||||
|
||||
public Task<(IReadOnlyList<TagHelperDescriptor>, VersionStamp)> GetTagHelpersAndVersionAsync(ProjectSnapshot snapshot)
|
||||
{
|
||||
if (TaskUnsafe == null)
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
if (TaskUnsafe == null)
|
||||
{
|
||||
TaskUnsafe = GetTagHelpersAndVersionCoreAsync(snapshot);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return TaskUnsafe;
|
||||
}
|
||||
|
||||
private async Task<(IReadOnlyList<TagHelperDescriptor>, VersionStamp)> GetTagHelpersAndVersionCoreAsync(ProjectSnapshot snapshot)
|
||||
{
|
||||
// Don't allow synchronous execution - we expect this to always be called with the lock.
|
||||
await Task.Yield();
|
||||
|
||||
var services = ((DefaultProjectSnapshot)snapshot).State.Services;
|
||||
var resolver = services.GetLanguageServices(RazorLanguage.Name).GetRequiredService<TagHelperResolver>();
|
||||
|
||||
var tagHelpers = (await resolver.GetTagHelpersAsync(snapshot).ConfigureAwait(false)).Descriptors;
|
||||
if (_older?.TaskUnsafe != null)
|
||||
{
|
||||
// We have something to diff against.
|
||||
var (olderTagHelpers, olderVersion) = await _older.TaskUnsafe.ConfigureAwait(false);
|
||||
|
||||
var difference = new HashSet<TagHelperDescriptor>(TagHelperDescriptorComparer.Default);
|
||||
difference.UnionWith(olderTagHelpers);
|
||||
difference.SymmetricExceptWith(tagHelpers);
|
||||
|
||||
if (difference.Count == 0)
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
|
||||
// Everything is the same. Return the cached version.
|
||||
TaskUnsafe = _older.TaskUnsafe;
|
||||
_older = null;
|
||||
return (olderTagHelpers, olderVersion);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
lock (_lock)
|
||||
{
|
||||
_older = null;
|
||||
return (tagHelpers, _projectStateVersion);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,79 +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.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Razor.Language;
|
||||
using Microsoft.CodeAnalysis.Host;
|
||||
|
||||
namespace Microsoft.CodeAnalysis.Razor.ProjectSystem
|
||||
{
|
||||
internal class ProjectTagHelperTracker
|
||||
{
|
||||
private const ProjectDifference Mask =
|
||||
ProjectDifference.ConfigurationChanged |
|
||||
ProjectDifference.WorkspaceProjectAdded |
|
||||
ProjectDifference.WorkspaceProjectChanged |
|
||||
ProjectDifference.WorkspaceProjectRemoved;
|
||||
|
||||
private readonly object _lock = new object();
|
||||
private readonly HostWorkspaceServices _services;
|
||||
|
||||
private Task<IReadOnlyList<TagHelperDescriptor>> _task;
|
||||
|
||||
public ProjectTagHelperTracker(ProjectState state)
|
||||
{
|
||||
if (state == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(state));
|
||||
}
|
||||
|
||||
_services = state.Services;
|
||||
}
|
||||
|
||||
public bool IsResultAvailable => _task?.IsCompleted == true;
|
||||
|
||||
public ProjectTagHelperTracker ForkFor(ProjectState state, ProjectDifference difference)
|
||||
{
|
||||
if (state == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(state));
|
||||
}
|
||||
|
||||
if ((difference & Mask) != 0)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
public Task<IReadOnlyList<TagHelperDescriptor>> GetTagHelperInitializationTask(ProjectSnapshot snapshot)
|
||||
{
|
||||
if (snapshot == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(snapshot));
|
||||
}
|
||||
|
||||
if (_task == null)
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
if (_task == null)
|
||||
{
|
||||
_task = GetTagHelperInitializationTaskCore(snapshot);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return _task;
|
||||
}
|
||||
|
||||
private async Task<IReadOnlyList<TagHelperDescriptor>> GetTagHelperInitializationTaskCore(ProjectSnapshot snapshot)
|
||||
{
|
||||
var resolver = _services.GetLanguageServices(RazorLanguage.Name).GetRequiredService<TagHelperResolver>();
|
||||
return (await resolver.GetTagHelpersAsync(snapshot)).Descriptors;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -94,30 +94,6 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem
|
|||
d => Assert.Same(d.Value, snapshot.GetDocument(d.Key)));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ProjectSnapshot_CachesTagHelperTask()
|
||||
{
|
||||
// Arrange
|
||||
TagHelperResolver.CompletionSource = new TaskCompletionSource<TagHelperResolutionResult>();
|
||||
|
||||
try
|
||||
{
|
||||
var state = ProjectState.Create(Workspace.Services, HostProject, WorkspaceProject);
|
||||
var snapshot = new DefaultProjectSnapshot(state);
|
||||
|
||||
// Act
|
||||
var task1 = snapshot.GetTagHelpersAsync();
|
||||
var task2 = snapshot.GetTagHelpersAsync();
|
||||
|
||||
// Assert
|
||||
Assert.Same(task1, task2);
|
||||
}
|
||||
finally
|
||||
{
|
||||
TagHelperResolver.CompletionSource.SetCanceled();
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IsImportDocument_NonImportDocument_ReturnsFalse()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -33,13 +33,13 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem
|
|||
SomeTagHelpers = new List<TagHelperDescriptor>();
|
||||
SomeTagHelpers.Add(TagHelperDescriptorBuilder.Create("Test1", "TestAssembly").Build());
|
||||
|
||||
Document = TestProjectData.SomeProjectFile1;
|
||||
HostDocument = TestProjectData.SomeProjectFile1;
|
||||
|
||||
Text = SourceText.From("Hello, world!");
|
||||
TextLoader = () => Task.FromResult(TextAndVersion.Create(Text, VersionStamp.Create()));
|
||||
}
|
||||
|
||||
private HostDocument Document { get; }
|
||||
private HostDocument HostDocument { get; }
|
||||
|
||||
private HostProject HostProject { get; }
|
||||
|
||||
|
|
@ -64,7 +64,7 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem
|
|||
public async Task DocumentState_CreatedNew_HasEmptyText()
|
||||
{
|
||||
// Arrange & Act
|
||||
var state = DocumentState.Create(Workspace.Services, Document, DocumentState.EmptyLoader);
|
||||
var state = DocumentState.Create(Workspace.Services, HostDocument, DocumentState.EmptyLoader);
|
||||
|
||||
// Assert
|
||||
var text = await state.GetTextAsync();
|
||||
|
|
@ -75,7 +75,7 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem
|
|||
public async Task DocumentState_WithText_CreatesNewState()
|
||||
{
|
||||
// Arrange
|
||||
var original = DocumentState.Create(Workspace.Services, Document, DocumentState.EmptyLoader);
|
||||
var original = DocumentState.Create(Workspace.Services, HostDocument, DocumentState.EmptyLoader);
|
||||
|
||||
// Act
|
||||
var state = original.WithText(Text, VersionStamp.Create());
|
||||
|
|
@ -89,7 +89,7 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem
|
|||
public async Task DocumentState_WithTextLoader_CreatesNewState()
|
||||
{
|
||||
// Arrange
|
||||
var original = DocumentState.Create(Workspace.Services, Document, DocumentState.EmptyLoader);
|
||||
var original = DocumentState.Create(Workspace.Services, HostDocument, DocumentState.EmptyLoader);
|
||||
|
||||
// Act
|
||||
var state = original.WithTextLoader(TextLoader);
|
||||
|
|
@ -103,7 +103,7 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem
|
|||
public void DocumentState_WithConfigurationChange_CachesSnapshotText()
|
||||
{
|
||||
// Arrange
|
||||
var original = DocumentState.Create(Workspace.Services, Document, DocumentState.EmptyLoader)
|
||||
var original = DocumentState.Create(Workspace.Services, HostDocument, DocumentState.EmptyLoader)
|
||||
.WithText(Text, VersionStamp.Create());
|
||||
|
||||
// Act
|
||||
|
|
@ -118,7 +118,7 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem
|
|||
public async Task DocumentState_WithConfigurationChange_CachesLoadedText()
|
||||
{
|
||||
// Arrange
|
||||
var original = DocumentState.Create(Workspace.Services, Document, DocumentState.EmptyLoader)
|
||||
var original = DocumentState.Create(Workspace.Services, HostDocument, DocumentState.EmptyLoader)
|
||||
.WithTextLoader(TextLoader);
|
||||
|
||||
await original.GetTextAsync();
|
||||
|
|
@ -135,7 +135,7 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem
|
|||
public void DocumentState_WithImportsChange_CachesSnapshotText()
|
||||
{
|
||||
// Arrange
|
||||
var original = DocumentState.Create(Workspace.Services, Document, DocumentState.EmptyLoader)
|
||||
var original = DocumentState.Create(Workspace.Services, HostDocument, DocumentState.EmptyLoader)
|
||||
.WithText(Text, VersionStamp.Create());
|
||||
|
||||
// Act
|
||||
|
|
@ -150,7 +150,7 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem
|
|||
public async Task DocumentState_WithImportsChange_CachesLoadedText()
|
||||
{
|
||||
// Arrange
|
||||
var original = DocumentState.Create(Workspace.Services, Document, DocumentState.EmptyLoader)
|
||||
var original = DocumentState.Create(Workspace.Services, HostDocument, DocumentState.EmptyLoader)
|
||||
.WithTextLoader(TextLoader);
|
||||
|
||||
await original.GetTextAsync();
|
||||
|
|
@ -167,7 +167,7 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem
|
|||
public void DocumentState_WithWorkspaceProjectChange_CachesSnapshotText()
|
||||
{
|
||||
// Arrange
|
||||
var original = DocumentState.Create(Workspace.Services, Document, DocumentState.EmptyLoader)
|
||||
var original = DocumentState.Create(Workspace.Services, HostDocument, DocumentState.EmptyLoader)
|
||||
.WithText(Text, VersionStamp.Create());
|
||||
|
||||
// Act
|
||||
|
|
@ -182,7 +182,7 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem
|
|||
public async Task DocumentState_WithWorkspaceProjectChange_CachesLoadedText()
|
||||
{
|
||||
// Arrange
|
||||
var original = DocumentState.Create(Workspace.Services, Document, DocumentState.EmptyLoader)
|
||||
var original = DocumentState.Create(Workspace.Services, HostDocument, DocumentState.EmptyLoader)
|
||||
.WithTextLoader(TextLoader);
|
||||
|
||||
await original.GetTextAsync();
|
||||
|
|
@ -194,20 +194,5 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem
|
|||
Assert.True(state.TryGetText(out _));
|
||||
Assert.True(state.TryGetTextVersion(out _));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DocumentState_WithWorkspaceProjectChange_TriesToCacheGeneratedOutput()
|
||||
{
|
||||
// Arrange
|
||||
var original = DocumentState.Create(Workspace.Services, Document, DocumentState.EmptyLoader);
|
||||
|
||||
GC.KeepAlive(original.GeneratedOutput);
|
||||
|
||||
// Act
|
||||
var state = original.WithWorkspaceProjectChange();
|
||||
|
||||
// Assert
|
||||
Assert.Same(state.GeneratedOutput.Older, original.GeneratedOutput);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,22 +15,26 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem
|
|||
public void SetOutput_AcceptsSameVersionedDocuments()
|
||||
{
|
||||
// Arrange
|
||||
var csharpDocument = RazorCSharpDocument.Create("...", RazorCodeGenerationOptions.CreateDefault(), Enumerable.Empty<RazorDiagnostic>());
|
||||
var hostProject = new HostProject("C:/project.csproj", RazorConfiguration.Default);
|
||||
var services = TestWorkspace.Create().Services;
|
||||
var hostProject = new HostProject("C:/project.csproj", RazorConfiguration.Default);
|
||||
var projectState = ProjectState.Create(services, hostProject);
|
||||
var project = new DefaultProjectSnapshot(projectState);
|
||||
var hostDocument = new HostDocument("C:/file.cshtml", "C:/file.cshtml");
|
||||
|
||||
var text = SourceText.From("...");
|
||||
var textAndVersion = TextAndVersion.Create(text, VersionStamp.Default);
|
||||
var hostDocument = new HostDocument("C:/file.cshtml", "C:/file.cshtml");
|
||||
var documentState = new DocumentState(services, hostDocument, text, VersionStamp.Default, () => Task.FromResult(textAndVersion));
|
||||
var document = new DefaultDocumentSnapshot(project, documentState);
|
||||
var newDocument = new DefaultDocumentSnapshot(project, documentState);
|
||||
|
||||
var csharpDocument = RazorCSharpDocument.Create("...", RazorCodeGenerationOptions.CreateDefault(), Enumerable.Empty<RazorDiagnostic>());
|
||||
|
||||
var version = VersionStamp.Create();
|
||||
var container = new GeneratedCodeContainer();
|
||||
container.SetOutput(csharpDocument, document);
|
||||
container.SetOutput(document, csharpDocument, version, version);
|
||||
|
||||
// Act
|
||||
container.SetOutput(csharpDocument, newDocument);
|
||||
container.SetOutput(newDocument, csharpDocument, version, version);
|
||||
|
||||
// Assert
|
||||
Assert.Same(newDocument, container.LatestDocument);
|
||||
|
|
@ -40,20 +44,23 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem
|
|||
public void SetOutput_AcceptsInitialOutput()
|
||||
{
|
||||
// Arrange
|
||||
var csharpDocument = RazorCSharpDocument.Create("...", RazorCodeGenerationOptions.CreateDefault(), Enumerable.Empty<RazorDiagnostic>());
|
||||
var hostProject = new HostProject("C:/project.csproj", RazorConfiguration.Default);
|
||||
var services = TestWorkspace.Create().Services;
|
||||
var hostProject = new HostProject("C:/project.csproj", RazorConfiguration.Default);
|
||||
var projectState = ProjectState.Create(services, hostProject);
|
||||
var project = new DefaultProjectSnapshot(projectState);
|
||||
var hostDocument = new HostDocument("C:/file.cshtml", "C:/file.cshtml");
|
||||
|
||||
var text = SourceText.From("...");
|
||||
var textAndVersion = TextAndVersion.Create(text, VersionStamp.Default);
|
||||
var hostDocument = new HostDocument("C:/file.cshtml", "C:/file.cshtml");
|
||||
var documentState = new DocumentState(services, hostDocument, text, VersionStamp.Default, () => Task.FromResult(textAndVersion));
|
||||
var document = new DefaultDocumentSnapshot(project, documentState);
|
||||
var csharpDocument = RazorCSharpDocument.Create("...", RazorCodeGenerationOptions.CreateDefault(), Enumerable.Empty<RazorDiagnostic>());
|
||||
|
||||
var version = VersionStamp.Create();
|
||||
var container = new GeneratedCodeContainer();
|
||||
|
||||
// Act
|
||||
container.SetOutput(csharpDocument, document);
|
||||
container.SetOutput(document, csharpDocument, version, version);
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(container.LatestDocument);
|
||||
|
|
|
|||
|
|
@ -0,0 +1,264 @@
|
|||
// 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.Linq;
|
||||
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 ProjectStateGeneratedOutputTest : WorkspaceTestBase
|
||||
{
|
||||
public ProjectStateGeneratedOutputTest()
|
||||
{
|
||||
HostProject = new HostProject(TestProjectData.SomeProject.FilePath, FallbackRazorConfiguration.MVC_2_0);
|
||||
HostProjectWithConfigurationChange = new HostProject(TestProjectData.SomeProject.FilePath, FallbackRazorConfiguration.MVC_1_0);
|
||||
|
||||
var projectId = ProjectId.CreateNewId("Test");
|
||||
var solution = Workspace.CurrentSolution.AddProject(ProjectInfo.Create(
|
||||
projectId,
|
||||
VersionStamp.Default,
|
||||
"Test",
|
||||
"Test",
|
||||
LanguageNames.CSharp,
|
||||
TestProjectData.SomeProject.FilePath));
|
||||
WorkspaceProject = solution.GetProject(projectId);
|
||||
|
||||
SomeTagHelpers = new List<TagHelperDescriptor>();
|
||||
SomeTagHelpers.Add(TagHelperDescriptorBuilder.Create("Test1", "TestAssembly").Build());
|
||||
|
||||
HostDocument = TestProjectData.SomeProjectFile1;
|
||||
|
||||
Text = SourceText.From("Hello, world!");
|
||||
TextLoader = () => Task.FromResult(TextAndVersion.Create(Text, VersionStamp.Create()));
|
||||
}
|
||||
|
||||
private HostDocument HostDocument { get; }
|
||||
|
||||
private HostProject HostProject { get; }
|
||||
|
||||
private HostProject HostProjectWithConfigurationChange { get; }
|
||||
|
||||
private Project WorkspaceProject { get; }
|
||||
|
||||
private TestTagHelperResolver TagHelperResolver { get; } = new TestTagHelperResolver();
|
||||
|
||||
private List<TagHelperDescriptor> SomeTagHelpers { get; }
|
||||
|
||||
private Func<Task<TextAndVersion>> TextLoader { get; }
|
||||
|
||||
private SourceText Text { get; }
|
||||
|
||||
protected override void ConfigureLanguageServices(List<ILanguageService> services)
|
||||
{
|
||||
services.Add(TagHelperResolver);
|
||||
}
|
||||
|
||||
protected override void ConfigureProjectEngine(RazorProjectEngineBuilder builder)
|
||||
{
|
||||
builder.Features.Remove(builder.Features.OfType<IImportProjectFeature>().Single());
|
||||
builder.Features.Add(new TestImportProjectFeature());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task HostDocumentAdded_CachesOutput()
|
||||
{
|
||||
// Arrange
|
||||
var original =
|
||||
ProjectState.Create(Workspace.Services, HostProject)
|
||||
.WithAddedHostDocument(HostDocument, DocumentState.EmptyLoader);
|
||||
|
||||
var (originalOutput, originalInputVersion, originalOutputVersion) = await GetOutputAsync(original, HostDocument);
|
||||
|
||||
// Act
|
||||
var state = original.WithAddedHostDocument(TestProjectData.AnotherProjectFile1, DocumentState.EmptyLoader);
|
||||
|
||||
// Assert
|
||||
var (actualOutput, actualInputVersion, actualOutputVersion) = await GetOutputAsync(state, HostDocument);
|
||||
Assert.Same(originalOutput, actualOutput);
|
||||
Assert.Equal(originalInputVersion, actualInputVersion);
|
||||
Assert.Equal(originalOutputVersion, actualOutputVersion);
|
||||
Assert.Equal(await state.GetComputedStateVersionAsync(new DefaultProjectSnapshot(state)), actualOutputVersion);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task HostDocumentAdded_Import_DoesNotCacheOutput()
|
||||
{
|
||||
// Arrange
|
||||
var original =
|
||||
ProjectState.Create(Workspace.Services, HostProject)
|
||||
.WithAddedHostDocument(HostDocument, DocumentState.EmptyLoader);
|
||||
|
||||
var (originalOutput, originalInputVersion, originalOutputVersion) = await GetOutputAsync(original, HostDocument);
|
||||
|
||||
// Act
|
||||
var state = original.WithAddedHostDocument(TestProjectData.SomeProjectImportFile, DocumentState.EmptyLoader);
|
||||
|
||||
// Assert
|
||||
var (actualOutput, actualInputVersion, actualOutputVersion) = await GetOutputAsync(state, HostDocument);
|
||||
Assert.NotSame(originalOutput, actualOutput);
|
||||
Assert.NotEqual(originalInputVersion, actualInputVersion);
|
||||
Assert.Equal(originalOutputVersion, actualOutputVersion);
|
||||
Assert.Equal(state.DocumentCollectionVersion, actualInputVersion);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task HostDocumentChanged_DoesNotCacheOutput()
|
||||
{
|
||||
// Arrange
|
||||
var original =
|
||||
ProjectState.Create(Workspace.Services, HostProject)
|
||||
.WithAddedHostDocument(HostDocument, DocumentState.EmptyLoader)
|
||||
.WithAddedHostDocument(TestProjectData.SomeProjectImportFile, DocumentState.EmptyLoader);
|
||||
|
||||
var (originalOutput, originalInputVersion, originalOutputVersion) = await GetOutputAsync(original, HostDocument);
|
||||
|
||||
// Act
|
||||
var version = VersionStamp.Create();
|
||||
var state = original.WithChangedHostDocument(HostDocument, () =>
|
||||
{
|
||||
return Task.FromResult(TextAndVersion.Create(SourceText.From("@using System"), version));
|
||||
});
|
||||
|
||||
// Assert
|
||||
var (actualOutput, actualInputVersion, actualOutputVersion) = await GetOutputAsync(state, HostDocument);
|
||||
Assert.NotSame(originalOutput, actualOutput);
|
||||
Assert.NotEqual(originalInputVersion, actualInputVersion);
|
||||
Assert.NotEqual(originalOutputVersion, actualOutputVersion);
|
||||
Assert.Equal(version, actualInputVersion);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task HostDocumentChanged_Import_DoesNotCacheOutput()
|
||||
{
|
||||
// Arrange
|
||||
var original =
|
||||
ProjectState.Create(Workspace.Services, HostProject)
|
||||
.WithAddedHostDocument(HostDocument, DocumentState.EmptyLoader)
|
||||
.WithAddedHostDocument(TestProjectData.SomeProjectImportFile, DocumentState.EmptyLoader);
|
||||
|
||||
var (originalOutput, originalInputVersion, originalOutputVersion) = await GetOutputAsync(original, HostDocument);
|
||||
|
||||
// Act
|
||||
var version = VersionStamp.Create();
|
||||
var state = original.WithChangedHostDocument(TestProjectData.SomeProjectImportFile, () =>
|
||||
{
|
||||
return Task.FromResult(TextAndVersion.Create(SourceText.From("@using System"), version));
|
||||
});
|
||||
|
||||
// Assert
|
||||
var (actualOutput, actualInputVersion, actualOutputVersion) = await GetOutputAsync(state, HostDocument);
|
||||
Assert.NotSame(originalOutput, actualOutput);
|
||||
Assert.NotEqual(originalInputVersion, actualInputVersion);
|
||||
Assert.NotEqual(originalOutputVersion, actualOutputVersion);
|
||||
Assert.Equal(version, actualInputVersion);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task HostDocumentRemoved_Import_DoesNotCacheOutput()
|
||||
{
|
||||
// Arrange
|
||||
var original =
|
||||
ProjectState.Create(Workspace.Services, HostProject)
|
||||
.WithAddedHostDocument(HostDocument, DocumentState.EmptyLoader)
|
||||
.WithAddedHostDocument(TestProjectData.SomeProjectImportFile, DocumentState.EmptyLoader);
|
||||
|
||||
var (originalOutput, originalInputVersion, originalOutputVersion) = await GetOutputAsync(original, HostDocument);
|
||||
|
||||
// Act
|
||||
var state = original.WithRemovedHostDocument(TestProjectData.SomeProjectImportFile);
|
||||
|
||||
// Assert
|
||||
var (actualOutput, actualInputVersion, actualOutputVersion) = await GetOutputAsync(state, HostDocument);
|
||||
Assert.NotSame(originalOutput, actualOutput);
|
||||
Assert.NotEqual(originalInputVersion, actualInputVersion);
|
||||
Assert.Equal(originalOutputVersion, actualOutputVersion);
|
||||
Assert.Equal(state.DocumentCollectionVersion, actualInputVersion);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task WorkspaceProjectChange_CachesOutput()
|
||||
{
|
||||
// Arrange
|
||||
var original =
|
||||
ProjectState.Create(Workspace.Services, HostProject)
|
||||
.WithAddedHostDocument(HostDocument, DocumentState.EmptyLoader);
|
||||
|
||||
var (originalOutput, originalInputVersion, originalOutputVersion) = await GetOutputAsync(original, HostDocument);
|
||||
|
||||
// Act
|
||||
var state = original.WithWorkspaceProject(WorkspaceProject.WithAssemblyName("Test2"));
|
||||
|
||||
// Assert
|
||||
var (actualOutput, actualInputVersion, actualOutputVersion) = await GetOutputAsync(state, HostDocument);
|
||||
Assert.Same(originalOutput, actualOutput);
|
||||
Assert.Equal(originalInputVersion, actualInputVersion);
|
||||
Assert.Equal(originalOutputVersion, actualOutputVersion);
|
||||
Assert.Equal(await state.GetComputedStateVersionAsync(new DefaultProjectSnapshot(state)), actualInputVersion);
|
||||
}
|
||||
|
||||
// The generated code's text doesn't change as a result, so the output version does not change
|
||||
[Fact]
|
||||
public async Task WorkspaceProjectChange_WithTagHelperChange_DoesNotCacheOutput()
|
||||
{
|
||||
// Arrange
|
||||
var original =
|
||||
ProjectState.Create(Workspace.Services, HostProject)
|
||||
.WithAddedHostDocument(HostDocument, DocumentState.EmptyLoader);
|
||||
|
||||
var (originalOutput, originalInputVersion, originalOutputVersion) = await GetOutputAsync(original, HostDocument);
|
||||
|
||||
TagHelperResolver.TagHelpers = SomeTagHelpers;
|
||||
|
||||
// Act
|
||||
var state = original.WithWorkspaceProject(WorkspaceProject.WithAssemblyName("Test2"));
|
||||
|
||||
// Assert
|
||||
var (actualOutput, actualInputVersion, actualOutputVersion) = await GetOutputAsync(state, HostDocument);
|
||||
Assert.NotSame(originalOutput, actualOutput);
|
||||
Assert.NotEqual(originalInputVersion, actualInputVersion);
|
||||
Assert.Equal(originalOutputVersion, actualOutputVersion);
|
||||
Assert.Equal(await state.GetComputedStateVersionAsync(new DefaultProjectSnapshot(state)), actualInputVersion);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ConfigurationChange_DoesNotCacheOutput()
|
||||
{
|
||||
// Arrange
|
||||
var original =
|
||||
ProjectState.Create(Workspace.Services, HostProject)
|
||||
.WithAddedHostDocument(HostDocument, DocumentState.EmptyLoader);
|
||||
|
||||
var (originalOutput, originalInputVersion, originalOutputVersion) = await GetOutputAsync(original, HostDocument);
|
||||
|
||||
// Act
|
||||
var state = original.WithHostProject(HostProjectWithConfigurationChange);
|
||||
|
||||
// Assert
|
||||
var (actualOutput, actualInputVersion, actualOutputVersion) = await GetOutputAsync(state, HostDocument);
|
||||
Assert.NotSame(originalOutput, actualOutput);
|
||||
Assert.NotEqual(originalInputVersion, actualInputVersion);
|
||||
Assert.NotEqual(originalOutputVersion, actualOutputVersion);
|
||||
Assert.Equal(await state.GetComputedStateVersionAsync(new DefaultProjectSnapshot(state)), actualInputVersion);
|
||||
}
|
||||
|
||||
private static Task<(RazorCodeDocument, VersionStamp, VersionStamp)> GetOutputAsync(ProjectState project, HostDocument hostDocument)
|
||||
{
|
||||
var document = project.Documents[hostDocument.FilePath];
|
||||
return GetOutputAsync(project, document);
|
||||
}
|
||||
|
||||
private static Task<(RazorCodeDocument, VersionStamp, VersionStamp)> GetOutputAsync(ProjectState project, DocumentState document)
|
||||
{
|
||||
|
||||
var projectSnapshot = new DefaultProjectSnapshot(project);
|
||||
var documentSnapshot = new DefaultDocumentSnapshot(projectSnapshot, document);
|
||||
return document.GetGeneratedOutputAndVersionAsync(projectSnapshot, documentSnapshot);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -17,8 +17,6 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem
|
|||
{
|
||||
public ProjectStateTest()
|
||||
{
|
||||
TagHelperResolver = new TestTagHelperResolver();
|
||||
|
||||
HostProject = new HostProject(TestProjectData.SomeProject.FilePath, FallbackRazorConfiguration.MVC_2_0);
|
||||
HostProjectWithConfigurationChange = new HostProject(TestProjectData.SomeProject.FilePath, FallbackRazorConfiguration.MVC_1_0);
|
||||
|
||||
|
|
@ -56,7 +54,7 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem
|
|||
|
||||
private Project WorkspaceProject { get; }
|
||||
|
||||
private TestTagHelperResolver TagHelperResolver { get; }
|
||||
private TestTagHelperResolver TagHelperResolver { get; set; }
|
||||
|
||||
private List<TagHelperDescriptor> SomeTagHelpers { get; }
|
||||
|
||||
|
|
@ -66,6 +64,7 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem
|
|||
|
||||
protected override void ConfigureLanguageServices(List<ILanguageService> services)
|
||||
{
|
||||
TagHelperResolver = new TestTagHelperResolver();
|
||||
services.Add(TagHelperResolver);
|
||||
}
|
||||
|
||||
|
|
@ -103,6 +102,7 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem
|
|||
Assert.Collection(
|
||||
state.Documents.OrderBy(kvp => kvp.Key),
|
||||
d => Assert.Same(Documents[0], d.Value.HostDocument));
|
||||
Assert.NotEqual(original.DocumentCollectionVersion, state.DocumentCollectionVersion);
|
||||
}
|
||||
|
||||
[Fact] // When we first add a document, we have no way to read the text, so it's empty.
|
||||
|
|
@ -138,6 +138,7 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem
|
|||
d => Assert.Same(Documents[2], d.Value.HostDocument),
|
||||
d => Assert.Same(Documents[0], d.Value.HostDocument),
|
||||
d => Assert.Same(Documents[1], d.Value.HostDocument));
|
||||
Assert.NotEqual(original.DocumentCollectionVersion, state.DocumentCollectionVersion);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
|
@ -225,7 +226,7 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem
|
|||
}
|
||||
|
||||
[Fact]
|
||||
public void ProjectState_AddHostDocument_RetainsComputedState()
|
||||
public async Task ProjectState_AddHostDocument_RetainsComputedState()
|
||||
{
|
||||
// Arrange
|
||||
var original = ProjectState.Create(Workspace.Services, HostProject, WorkspaceProject)
|
||||
|
|
@ -233,15 +234,19 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem
|
|||
.WithAddedHostDocument(Documents[1], DocumentState.EmptyLoader);
|
||||
|
||||
// Force init
|
||||
GC.KeepAlive(original.ProjectEngine);
|
||||
GC.KeepAlive(original.TagHelpers);
|
||||
var originalTagHelpers = await original.GetTagHelpersAsync(new DefaultProjectSnapshot(original));
|
||||
var originalComputedVersion = await original.GetComputedStateVersionAsync(new DefaultProjectSnapshot(original));
|
||||
|
||||
// Act
|
||||
var state = original.WithAddedHostDocument(Documents[0], DocumentState.EmptyLoader);
|
||||
|
||||
// Assert
|
||||
var actualTagHelpers = await state.GetTagHelpersAsync(new DefaultProjectSnapshot(state));
|
||||
var actualComputedVersion = await state.GetComputedStateVersionAsync(new DefaultProjectSnapshot(state));
|
||||
|
||||
Assert.Same(original.ProjectEngine, state.ProjectEngine);
|
||||
Assert.Same(original.TagHelpers, state.TagHelpers);
|
||||
Assert.Same(originalTagHelpers, actualTagHelpers);
|
||||
Assert.Equal(originalComputedVersion, actualComputedVersion);
|
||||
|
||||
Assert.Same(original.Documents[Documents[1].FilePath], state.Documents[Documents[1].FilePath]);
|
||||
Assert.Same(original.Documents[Documents[2].FilePath], state.Documents[Documents[2].FilePath]);
|
||||
|
|
@ -278,6 +283,8 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem
|
|||
|
||||
var text = await state.Documents[Documents[1].FilePath].GetTextAsync();
|
||||
Assert.Same(Text, text);
|
||||
|
||||
Assert.Equal(original.DocumentCollectionVersion, state.DocumentCollectionVersion);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
|
@ -296,10 +303,12 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem
|
|||
|
||||
var text = await state.Documents[Documents[1].FilePath].GetTextAsync();
|
||||
Assert.Same(Text, text);
|
||||
|
||||
Assert.Equal(original.DocumentCollectionVersion, state.DocumentCollectionVersion);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ProjectState_WithChangedHostDocument_Loader_RetainsComputedState()
|
||||
public async Task ProjectState_WithChangedHostDocument_Loader_RetainsComputedState()
|
||||
{
|
||||
// Arrange
|
||||
var original = ProjectState.Create(Workspace.Services, HostProject, WorkspaceProject)
|
||||
|
|
@ -307,21 +316,25 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem
|
|||
.WithAddedHostDocument(Documents[1], DocumentState.EmptyLoader);
|
||||
|
||||
// Force init
|
||||
GC.KeepAlive(original.ProjectEngine);
|
||||
GC.KeepAlive(original.TagHelpers);
|
||||
var originalTagHelpers = await original.GetTagHelpersAsync(new DefaultProjectSnapshot(original));
|
||||
var originalComputedVersion = await original.GetComputedStateVersionAsync(new DefaultProjectSnapshot(original));
|
||||
|
||||
// Act
|
||||
var state = original.WithChangedHostDocument(Documents[1], TextLoader);
|
||||
|
||||
// Assert
|
||||
var actualTagHelpers = await state.GetTagHelpersAsync(new DefaultProjectSnapshot(state));
|
||||
var actualComputedVersion = await state.GetComputedStateVersionAsync(new DefaultProjectSnapshot(state));
|
||||
|
||||
Assert.Same(original.ProjectEngine, state.ProjectEngine);
|
||||
Assert.Same(original.TagHelpers, state.TagHelpers);
|
||||
Assert.Same(originalTagHelpers, actualTagHelpers);
|
||||
Assert.Equal(originalComputedVersion, actualComputedVersion);
|
||||
|
||||
Assert.NotSame(original.Documents[Documents[1].FilePath], state.Documents[Documents[1].FilePath]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ProjectState_WithChangedHostDocument_Snapshot_RetainsComputedState()
|
||||
public async Task ProjectState_WithChangedHostDocument_Snapshot_RetainsComputedState()
|
||||
{
|
||||
// Arrange
|
||||
var original = ProjectState.Create(Workspace.Services, HostProject, WorkspaceProject)
|
||||
|
|
@ -329,15 +342,19 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem
|
|||
.WithAddedHostDocument(Documents[1], DocumentState.EmptyLoader);
|
||||
|
||||
// Force init
|
||||
GC.KeepAlive(original.ProjectEngine);
|
||||
GC.KeepAlive(original.TagHelpers);
|
||||
var originalTagHelpers = await original.GetTagHelpersAsync(new DefaultProjectSnapshot(original));
|
||||
var originalComputedVersion = await original.GetComputedStateVersionAsync(new DefaultProjectSnapshot(original));
|
||||
|
||||
// Act
|
||||
var state = original.WithChangedHostDocument(Documents[1], Text, VersionStamp.Create());
|
||||
|
||||
// Assert
|
||||
var actualTagHelpers = await state.GetTagHelpersAsync(new DefaultProjectSnapshot(state));
|
||||
var actualComputedVersion = await state.GetComputedStateVersionAsync(new DefaultProjectSnapshot(state));
|
||||
|
||||
Assert.Same(original.ProjectEngine, state.ProjectEngine);
|
||||
Assert.Same(original.TagHelpers, state.TagHelpers);
|
||||
Assert.Same(originalTagHelpers, actualTagHelpers);
|
||||
Assert.Equal(originalComputedVersion, actualComputedVersion);
|
||||
|
||||
Assert.NotSame(original.Documents[Documents[1].FilePath], state.Documents[Documents[1].FilePath]);
|
||||
}
|
||||
|
|
@ -389,6 +406,8 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem
|
|||
Assert.Collection(
|
||||
state.Documents.OrderBy(kvp => kvp.Key),
|
||||
d => Assert.Same(Documents[2], d.Value.HostDocument));
|
||||
|
||||
Assert.NotEqual(original.DocumentCollectionVersion, state.DocumentCollectionVersion);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
|
@ -454,7 +473,7 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem
|
|||
}
|
||||
|
||||
[Fact]
|
||||
public void ProjectState_RemoveHostDocument_RetainsComputedState()
|
||||
public async Task ProjectState_RemoveHostDocument_RetainsComputedState()
|
||||
{
|
||||
// Arrange
|
||||
var original = ProjectState.Create(Workspace.Services, HostProject, WorkspaceProject)
|
||||
|
|
@ -462,15 +481,19 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem
|
|||
.WithAddedHostDocument(Documents[1], DocumentState.EmptyLoader);
|
||||
|
||||
// Force init
|
||||
GC.KeepAlive(original.ProjectEngine);
|
||||
GC.KeepAlive(original.TagHelpers);
|
||||
var originalTagHelpers = await original.GetTagHelpersAsync(new DefaultProjectSnapshot(original));
|
||||
var originalComputedVersion = await original.GetComputedStateVersionAsync(new DefaultProjectSnapshot(original));
|
||||
|
||||
// Act
|
||||
var state = original.WithRemovedHostDocument(Documents[2]);
|
||||
|
||||
// Assert
|
||||
var actualTagHelpers = await state.GetTagHelpersAsync(new DefaultProjectSnapshot(state));
|
||||
var actualComputedVersion = await state.GetComputedStateVersionAsync(new DefaultProjectSnapshot(state));
|
||||
|
||||
Assert.Same(original.ProjectEngine, state.ProjectEngine);
|
||||
Assert.Same(original.TagHelpers, state.TagHelpers);
|
||||
Assert.Same(originalTagHelpers, actualTagHelpers);
|
||||
Assert.Equal(originalComputedVersion, actualComputedVersion);
|
||||
|
||||
Assert.Same(original.Documents[Documents[1].FilePath], state.Documents[Documents[1].FilePath]);
|
||||
}
|
||||
|
|
@ -491,7 +514,7 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem
|
|||
}
|
||||
|
||||
[Fact]
|
||||
public void ProjectState_WithHostProject_ConfigurationChange_UpdatesComputedState()
|
||||
public async Task ProjectState_WithHostProject_ConfigurationChange_UpdatesComputedState()
|
||||
{
|
||||
// Arrange
|
||||
var original = ProjectState.Create(Workspace.Services, HostProject, WorkspaceProject)
|
||||
|
|
@ -499,8 +522,10 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem
|
|||
.WithAddedHostDocument(Documents[1], DocumentState.EmptyLoader);
|
||||
|
||||
// Force init
|
||||
GC.KeepAlive(original.ProjectEngine);
|
||||
GC.KeepAlive(original.TagHelpers);
|
||||
var originalTagHelpers = await original.GetTagHelpersAsync(new DefaultProjectSnapshot(original));
|
||||
var originalComputedVersion = await original.GetComputedStateVersionAsync(new DefaultProjectSnapshot(original));
|
||||
|
||||
TagHelperResolver.TagHelpers = SomeTagHelpers;
|
||||
|
||||
// Act
|
||||
var state = original.WithHostProject(HostProjectWithConfigurationChange);
|
||||
|
|
@ -509,15 +534,21 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem
|
|||
Assert.NotEqual(original.Version, state.Version);
|
||||
Assert.Same(HostProjectWithConfigurationChange, state.HostProject);
|
||||
|
||||
var actualTagHelpers = await state.GetTagHelpersAsync(new DefaultProjectSnapshot(state));
|
||||
var actualComputedVersion = await state.GetComputedStateVersionAsync(new DefaultProjectSnapshot(state));
|
||||
|
||||
Assert.NotSame(original.ProjectEngine, state.ProjectEngine);
|
||||
Assert.NotSame(original.TagHelpers, state.TagHelpers);
|
||||
Assert.NotSame(originalTagHelpers, actualTagHelpers);
|
||||
Assert.NotEqual(originalComputedVersion, actualComputedVersion);
|
||||
|
||||
Assert.NotSame(original.Documents[Documents[1].FilePath], state.Documents[Documents[1].FilePath]);
|
||||
Assert.NotSame(original.Documents[Documents[2].FilePath], state.Documents[Documents[2].FilePath]);
|
||||
|
||||
Assert.NotEqual(original.DocumentCollectionVersion, state.DocumentCollectionVersion);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ProjectState_WithHostProject_NoConfigurationChange_Noops()
|
||||
public async Task ProjectState_WithHostProject_NoConfigurationChange_Noops()
|
||||
{
|
||||
// Arrange
|
||||
var original = ProjectState.Create(Workspace.Services, HostProject, WorkspaceProject)
|
||||
|
|
@ -525,8 +556,8 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem
|
|||
.WithAddedHostDocument(Documents[1], DocumentState.EmptyLoader);
|
||||
|
||||
// Force init
|
||||
GC.KeepAlive(original.ProjectEngine);
|
||||
GC.KeepAlive(original.TagHelpers);
|
||||
var originalTagHelpers = await original.GetTagHelpersAsync(new DefaultProjectSnapshot(original));
|
||||
var originalComputedVersion = await original.GetComputedStateVersionAsync(new DefaultProjectSnapshot(original));
|
||||
|
||||
// Act
|
||||
var state = original.WithHostProject(HostProject);
|
||||
|
|
@ -560,7 +591,7 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem
|
|||
}
|
||||
|
||||
[Fact]
|
||||
public void ProjectState_WithWorkspaceProject_Removed()
|
||||
public async Task ProjectState_WithWorkspaceProject_Removed()
|
||||
{
|
||||
// Arrange
|
||||
var original = ProjectState.Create(Workspace.Services, HostProject, WorkspaceProject)
|
||||
|
|
@ -568,8 +599,8 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem
|
|||
.WithAddedHostDocument(Documents[1], DocumentState.EmptyLoader);
|
||||
|
||||
// Force init
|
||||
GC.KeepAlive(original.ProjectEngine);
|
||||
GC.KeepAlive(original.TagHelpers);
|
||||
var originalTagHelpers = await original.GetTagHelpersAsync(new DefaultProjectSnapshot(original));
|
||||
var originalComputedVersion = await original.GetComputedStateVersionAsync(new DefaultProjectSnapshot(original));
|
||||
|
||||
// Act
|
||||
var state = original.WithWorkspaceProject(null);
|
||||
|
|
@ -578,15 +609,20 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem
|
|||
Assert.NotEqual(original.Version, state.Version);
|
||||
Assert.Null(state.WorkspaceProject);
|
||||
|
||||
var actualTagHelpers = await state.GetTagHelpersAsync(new DefaultProjectSnapshot(state));
|
||||
var actualComputedVersion = await state.GetComputedStateVersionAsync(new DefaultProjectSnapshot(state));
|
||||
|
||||
// The configuration didn't change, and the tag helpers didn't actually change
|
||||
Assert.Same(original.ProjectEngine, state.ProjectEngine);
|
||||
Assert.NotSame(original.TagHelpers, state.TagHelpers);
|
||||
Assert.Same(originalTagHelpers, actualTagHelpers);
|
||||
Assert.Equal(originalComputedVersion, actualComputedVersion);
|
||||
|
||||
Assert.NotSame(original.Documents[Documents[1].FilePath], state.Documents[Documents[1].FilePath]);
|
||||
Assert.NotSame(original.Documents[Documents[2].FilePath], state.Documents[Documents[2].FilePath]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ProjectState_WithWorkspaceProject_Added()
|
||||
public async Task ProjectState_WithWorkspaceProject_Added()
|
||||
{
|
||||
// Arrange
|
||||
var original = ProjectState.Create(Workspace.Services, HostProject, null)
|
||||
|
|
@ -594,8 +630,8 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem
|
|||
.WithAddedHostDocument(Documents[1], DocumentState.EmptyLoader);
|
||||
|
||||
// Force init
|
||||
GC.KeepAlive(original.ProjectEngine);
|
||||
GC.KeepAlive(original.TagHelpers);
|
||||
var originalTagHelpers = await original.GetTagHelpersAsync(new DefaultProjectSnapshot(original));
|
||||
var originalComputedVersion = await original.GetComputedStateVersionAsync(new DefaultProjectSnapshot(original));
|
||||
|
||||
// Act
|
||||
var state = original.WithWorkspaceProject(WorkspaceProject);
|
||||
|
|
@ -604,15 +640,20 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem
|
|||
Assert.NotEqual(original.Version, state.Version);
|
||||
Assert.Same(WorkspaceProject, state.WorkspaceProject);
|
||||
|
||||
var actualTagHelpers = await state.GetTagHelpersAsync(new DefaultProjectSnapshot(state));
|
||||
var actualComputedVersion = await state.GetComputedStateVersionAsync(new DefaultProjectSnapshot(state));
|
||||
|
||||
// The configuration didn't change, and the tag helpers didn't actually change
|
||||
Assert.Same(original.ProjectEngine, state.ProjectEngine);
|
||||
Assert.NotSame(original.TagHelpers, state.TagHelpers);
|
||||
Assert.Same(originalTagHelpers, actualTagHelpers);
|
||||
Assert.Equal(originalComputedVersion, actualComputedVersion);
|
||||
|
||||
Assert.NotSame(original.Documents[Documents[1].FilePath], state.Documents[Documents[1].FilePath]);
|
||||
Assert.NotSame(original.Documents[Documents[1].FilePath], state.Documents[Documents[1].FilePath]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ProjectState_WithWorkspaceProject_Changed()
|
||||
public async Task ProjectState_WithWorkspaceProject_Changed()
|
||||
{
|
||||
// Arrange
|
||||
var original = ProjectState.Create(Workspace.Services, HostProject, WorkspaceProject)
|
||||
|
|
@ -620,8 +661,8 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem
|
|||
.WithAddedHostDocument(Documents[1], DocumentState.EmptyLoader);
|
||||
|
||||
// Force init
|
||||
GC.KeepAlive(original.ProjectEngine);
|
||||
GC.KeepAlive(original.TagHelpers);
|
||||
var originalTagHelpers = await original.GetTagHelpersAsync(new DefaultProjectSnapshot(original));
|
||||
var originalComputedVersion = await original.GetComputedStateVersionAsync(new DefaultProjectSnapshot(original));
|
||||
|
||||
var changed = WorkspaceProject.WithAssemblyName("Test1");
|
||||
|
||||
|
|
@ -632,8 +673,50 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem
|
|||
Assert.NotEqual(original.Version, state.Version);
|
||||
Assert.Same(changed, state.WorkspaceProject);
|
||||
|
||||
var actualTagHelpers = await state.GetTagHelpersAsync(new DefaultProjectSnapshot(state));
|
||||
var actualComputedVersion = await state.GetComputedStateVersionAsync(new DefaultProjectSnapshot(state));
|
||||
|
||||
// The configuration didn't change, and the tag helpers didn't actually change
|
||||
Assert.Same(original.ProjectEngine, state.ProjectEngine);
|
||||
Assert.NotSame(original.TagHelpers, state.TagHelpers);
|
||||
Assert.Same(originalTagHelpers, actualTagHelpers);
|
||||
Assert.Equal(originalComputedVersion, actualComputedVersion);
|
||||
|
||||
Assert.NotSame(original.Documents[Documents[1].FilePath], state.Documents[Documents[1].FilePath]);
|
||||
Assert.NotSame(original.Documents[Documents[2].FilePath], state.Documents[Documents[2].FilePath]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ProjectState_WithWorkspaceProject_Changed_TagHelpersChanged()
|
||||
{
|
||||
// Arrange
|
||||
var original = ProjectState.Create(Workspace.Services, HostProject, WorkspaceProject)
|
||||
.WithAddedHostDocument(Documents[2], DocumentState.EmptyLoader)
|
||||
.WithAddedHostDocument(Documents[1], DocumentState.EmptyLoader);
|
||||
|
||||
// Force init
|
||||
var originalTagHelpers = await original.GetTagHelpersAsync(new DefaultProjectSnapshot(original));
|
||||
var originalComputedVersion = await original.GetComputedStateVersionAsync(new DefaultProjectSnapshot(original));
|
||||
|
||||
var changed = WorkspaceProject.WithAssemblyName("Test1");
|
||||
|
||||
// Now create some tag helpers
|
||||
TagHelperResolver.TagHelpers = SomeTagHelpers;
|
||||
|
||||
// Act
|
||||
var state = original.WithWorkspaceProject(changed);
|
||||
|
||||
// Assert
|
||||
Assert.NotEqual(original.Version, state.Version);
|
||||
Assert.Same(changed, state.WorkspaceProject);
|
||||
|
||||
var actualTagHelpers = await state.GetTagHelpersAsync(new DefaultProjectSnapshot(state));
|
||||
var actualComputedVersion = await state.GetComputedStateVersionAsync(new DefaultProjectSnapshot(state));
|
||||
|
||||
// The configuration didn't change, but the tag helpers did
|
||||
Assert.Same(original.ProjectEngine, state.ProjectEngine);
|
||||
Assert.NotEqual(originalTagHelpers, actualTagHelpers);
|
||||
Assert.NotEqual(originalComputedVersion, actualComputedVersion);
|
||||
Assert.Equal(state.Version, actualComputedVersion);
|
||||
|
||||
Assert.NotSame(original.Documents[Documents[1].FilePath], state.Documents[Documents[1].FilePath]);
|
||||
Assert.NotSame(original.Documents[Documents[2].FilePath], state.Documents[Documents[2].FilePath]);
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ namespace Microsoft.CodeAnalysis.Razor
|
|||
{
|
||||
public TaskCompletionSource<TagHelperResolutionResult> CompletionSource { get; set; }
|
||||
|
||||
public IList<TagHelperDescriptor> TagHelpers { get; } = new List<TagHelperDescriptor>();
|
||||
public IList<TagHelperDescriptor> TagHelpers { get; set; } = new List<TagHelperDescriptor>();
|
||||
|
||||
public override Task<TagHelperResolutionResult> GetTagHelpersAsync(ProjectSnapshot project, CancellationToken cancellationToken = default)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ using Xunit;
|
|||
|
||||
namespace Microsoft.VisualStudio.Editor.Razor
|
||||
{
|
||||
public class DefaultVisualStudioDocumentTrackerTest : ForegroundDispatcherTestBase
|
||||
public class DefaultVisualStudioDocumentTrackerTest : ForegroundDispatcherWorkspaceTestBase
|
||||
{
|
||||
public DefaultVisualStudioDocumentTrackerTest()
|
||||
{
|
||||
|
|
@ -32,27 +32,11 @@ namespace Microsoft.VisualStudio.Editor.Razor
|
|||
ImportDocumentManager = Mock.Of<ImportDocumentManager>();
|
||||
WorkspaceEditorSettings = new DefaultWorkspaceEditorSettings(Mock.Of<ForegroundDispatcher>(), Mock.Of<EditorSettingsManager>());
|
||||
|
||||
TagHelperResolver = new TestTagHelperResolver();
|
||||
SomeTagHelpers = new List<TagHelperDescriptor>()
|
||||
{
|
||||
TagHelperDescriptorBuilder.Create("test", "test").Build(),
|
||||
};
|
||||
|
||||
HostServices = TestServices.Create(
|
||||
new IWorkspaceService[] { },
|
||||
new ILanguageService[] { TagHelperResolver, });
|
||||
|
||||
Workspace = TestWorkspace.Create(HostServices, w =>
|
||||
{
|
||||
WorkspaceProject = w.AddProject(ProjectInfo.Create(
|
||||
ProjectId.CreateNewId(),
|
||||
new VersionStamp(),
|
||||
"Test1",
|
||||
"TestAssembly",
|
||||
LanguageNames.CSharp,
|
||||
filePath: ProjectPath));
|
||||
});
|
||||
|
||||
|
||||
ProjectManager = new TestProjectSnapshotManager(Dispatcher, Workspace) { AllowNotifyListeners = true };
|
||||
|
||||
HostProject = new HostProject(ProjectPath, FallbackRazorConfiguration.MVC_2_1);
|
||||
|
|
@ -92,16 +76,29 @@ namespace Microsoft.VisualStudio.Editor.Razor
|
|||
|
||||
private List<TagHelperDescriptor> SomeTagHelpers { get; }
|
||||
|
||||
private TestTagHelperResolver TagHelperResolver { get; }
|
||||
private TestTagHelperResolver TagHelperResolver { get; set; }
|
||||
|
||||
private ProjectSnapshotManagerBase ProjectManager { get; }
|
||||
|
||||
private HostServices HostServices { get; }
|
||||
|
||||
private Workspace Workspace { get; }
|
||||
|
||||
private DefaultVisualStudioDocumentTracker DocumentTracker { get; }
|
||||
|
||||
protected override void ConfigureLanguageServices(List<ILanguageService> services)
|
||||
{
|
||||
TagHelperResolver = new TestTagHelperResolver();
|
||||
services.Add(TagHelperResolver);
|
||||
}
|
||||
|
||||
protected override void ConfigureWorkspace(AdhocWorkspace workspace)
|
||||
{
|
||||
WorkspaceProject = workspace.AddProject(ProjectInfo.Create(
|
||||
ProjectId.CreateNewId(),
|
||||
new VersionStamp(),
|
||||
"Test1",
|
||||
"TestAssembly",
|
||||
LanguageNames.CSharp,
|
||||
filePath: TestProjectData.SomeProject.FilePath));
|
||||
}
|
||||
|
||||
[ForegroundFact]
|
||||
public void Subscribe_NoopsIfAlreadySubscribed()
|
||||
{
|
||||
|
|
@ -561,7 +558,7 @@ namespace Microsoft.VisualStudio.Editor.Razor
|
|||
await DocumentTracker.PendingTagHelperTask;
|
||||
|
||||
// Assert
|
||||
Assert.Same(DocumentTracker.TagHelpers, SomeTagHelpers);
|
||||
Assert.Same(SomeTagHelpers, DocumentTracker.TagHelpers);
|
||||
|
||||
Assert.Collection(
|
||||
args,
|
||||
|
|
|
|||
Loading…
Reference in New Issue