public void ProjectSnapshot_CachesDocumentSnapshots() { // Arrange var state = new ProjectState(Workspace.Services, HostProject, WorkspaceProject) .AddHostDocument(Documents[0]) .AddHostDocument(Documents[1]) .AddHostDocument(Documents[2]); var snapshot = new DefaultProjectSnapshot(state); // Act var documents = snapshot.DocumentFilePaths.ToDictionary(f => f, f => snapshot.GetDocument(f)); // Assert Assert.Collection( documents, d => Assert.Same(d.Value, snapshot.GetDocument(d.Key)), d => Assert.Same(d.Value, snapshot.GetDocument(d.Key)), d => Assert.Same(d.Value, snapshot.GetDocument(d.Key))); }
public DefaultDocumentSnapshotTest() { var services = TestServices.Create( new[] { new TestProjectSnapshotProjectEngineFactory() }, new[] { new TestTagHelperResolver() }); Workspace = TestWorkspace.Create(services); var hostProject = new HostProject("C:/some/path/project.csproj", RazorConfiguration.Default); var projectState = ProjectState.Create(Workspace.Services, hostProject); var project = new DefaultProjectSnapshot(projectState); HostDocument = new HostDocument("C:/some/path/file.cshtml", "C:/some/path/file.cshtml"); SourceText = Text.SourceText.From("<p>Hello World</p>"); Version = VersionStamp.Default.GetNewerVersion(); var textAndVersion = TextAndVersion.Create(SourceText, Version); var documentState = DocumentState.Create(Workspace.Services, HostDocument, () => Task.FromResult(textAndVersion)); Document = new DefaultDocumentSnapshot(project, documentState); }
public void ProjectSnapshot_CachesDocumentSnapshots() { // Arrange var state = ProjectState.Create(Workspace.Services, HostProject, ProjectWorkspaceState) .WithAddedHostDocument(Documents[0], DocumentState.EmptyLoader) .WithAddedHostDocument(Documents[1], DocumentState.EmptyLoader) .WithAddedHostDocument(Documents[2], DocumentState.EmptyLoader); var snapshot = new DefaultProjectSnapshot(state); // Act var documents = snapshot.DocumentFilePaths.ToDictionary(f => f, f => snapshot.GetDocument(f)); // Assert Assert.Collection( documents, d => Assert.Same(d.Value, snapshot.GetDocument(d.Key)), d => Assert.Same(d.Value, snapshot.GetDocument(d.Key)), d => Assert.Same(d.Value, snapshot.GetDocument(d.Key))); }
public void WithProjectChange_WithProject_CreatesSnapshot_UpdatesValues() { // Arrange var hostProject = new HostProject("Test.cshtml", FallbackRazorConfiguration.MVC_2_0); var workspaceProject = GetWorkspaceProject("Test1"); var original = new DefaultProjectSnapshot(hostProject, workspaceProject); var anotherProject = GetWorkspaceProject("Test1"); var update = new ProjectSnapshotUpdateContext(original.FilePath, hostProject, anotherProject, original.Version) { TagHelpers = Array.Empty <TagHelperDescriptor>(), }; // Act var snapshot = original.WithComputedUpdate(update); // Assert Assert.Same(original.WorkspaceProject, snapshot.WorkspaceProject); Assert.Same(update.TagHelpers, snapshot.TagHelpers); }
public void GetRelatedDocuments_ImportDocument_ReturnsRelated() { // Arrange var state = ProjectState.Create(Workspace.Services, HostProject, ProjectWorkspaceState) .WithAddedHostDocument(Documents[0], DocumentState.EmptyLoader) .WithAddedHostDocument(Documents[1], DocumentState.EmptyLoader) .WithAddedHostDocument(TestProjectData.SomeProjectImportFile, DocumentState.EmptyLoader); var snapshot = new DefaultProjectSnapshot(state); var document = snapshot.GetDocument(TestProjectData.SomeProjectImportFile.FilePath); // Act var documents = snapshot.GetRelatedDocuments(document); // Assert Assert.Collection( documents.OrderBy(d => d.FilePath), d => Assert.Equal(Documents[0].FilePath, d.FilePath), d => Assert.Equal(Documents[1].FilePath, d.FilePath)); }
private DefaultProjectSnapshot(Project workspaceProject, DefaultProjectSnapshot other) { if (workspaceProject == null) { throw new ArgumentNullException(nameof(workspaceProject)); } if (other == null) { throw new ArgumentNullException(nameof(other)); } ComputedVersion = other.ComputedVersion; FilePath = other.FilePath; TagHelpers = other.TagHelpers; HostProject = other.HostProject; WorkspaceProject = workspaceProject; Version = other.Version.GetNewerVersion(); }
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 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 documentState = new DocumentState(services, hostDocument, text, VersionStamp.Default, () => Task.FromResult(textAndVersion)); var document = new DefaultDocumentSnapshot(project, documentState); var container = new GeneratedCodeContainer(); // Act container.SetOutput(csharpDocument, document); // Assert Assert.NotNull(container.LatestDocument); }
public DefaultDocumentSnapshotTest() { SourceText = SourceText.From("<p>Hello World</p>"); Version = VersionStamp.Create(); // Create a new HostDocument to avoid mutating the code container ComponentHostDocument = new HostDocument(TestProjectData.SomeProjectComponentFile1); LegacyHostDocument = new HostDocument(TestProjectData.SomeProjectFile1); var projectState = ProjectState.Create(Workspace.Services, TestProjectData.SomeProject); var project = new DefaultProjectSnapshot(projectState); var textAndVersion = TextAndVersion.Create(SourceText, Version); var documentState = DocumentState.Create(Workspace.Services, LegacyHostDocument, () => Task.FromResult(textAndVersion)); LegacyDocument = new DefaultDocumentSnapshot(project, documentState); documentState = DocumentState.Create(Workspace.Services, ComponentHostDocument, () => Task.FromResult(textAndVersion)); ComponentDocument = new DefaultDocumentSnapshot(project, documentState); }
public void WithProjectChange_WithProject_CreatesSnapshot_UpdatesValues() { // Arrange var underlyingProject = GetProject("Test1"); var original = new DefaultProjectSnapshot(underlyingProject); var anotherProject = GetProject("Test1"); var update = new ProjectSnapshotUpdateContext(anotherProject) { Configuration = Mock.Of <ProjectExtensibilityConfiguration>(), TagHelpers = Array.Empty <TagHelperDescriptor>(), }; // Act var snapshot = original.WithProjectChange(update); // Assert Assert.Same(original.UnderlyingProject, snapshot.UnderlyingProject); Assert.Equal(update.UnderlyingProject.Version, snapshot.ComputedVersion); Assert.Same(update.Configuration, snapshot.Configuration); Assert.Same(update.TagHelpers, snapshot.TagHelpers); }
private DefaultProjectSnapshot(ProjectSnapshotUpdateContext update, DefaultProjectSnapshot other) { if (update == null) { throw new ArgumentNullException(nameof(update)); } if (other == null) { throw new ArgumentNullException(nameof(other)); } ComputedVersion = update.Version; FilePath = other.FilePath; HostProject = other.HostProject; TagHelpers = update.TagHelpers ?? Array.Empty <TagHelperDescriptor>(); WorkspaceProject = other.WorkspaceProject; // This doesn't represent a new version of the underlying data. Keep the same version. Version = other.Version; }
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(); } }
public void HaveTagHelpersChanged_TagHelpersUpdated_ReturnsTrue() { // Arrange var underlyingProject = GetProject("Test1"); var original = new DefaultProjectSnapshot(underlyingProject); var anotherProject = GetProject("Test1"); var update = new ProjectSnapshotUpdateContext(anotherProject) { TagHelpers = new[] { TagHelperDescriptorBuilder.Create("One", "TestAssembly").Build(), TagHelperDescriptorBuilder.Create("Two", "TestAssembly").Build(), }, }; var snapshot = original.WithProjectChange(update); // Act var result = snapshot.HaveTagHelpersChanged(original); // Assert Assert.True(result); }
private IReadOnlyList <DocumentSnapshot> GetImportsCore(DefaultProjectSnapshot project) { var projectEngine = project.GetProjectEngine(); var importFeatures = projectEngine.ProjectFeatures.OfType <IImportProjectFeature>(); var projectItem = projectEngine.FileSystem.GetItem(HostDocument.FilePath, HostDocument.FileKind); var importItems = importFeatures.SelectMany(f => f.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); }
public void HaveTagHelpersChanged_TagHelpersUpdated_ReturnsTrue() { // Arrange var hostProject = new HostProject("Test1.csproj", RazorConfiguration.Default); var workspaceProject = GetWorkspaceProject("Test1"); var original = new DefaultProjectSnapshot(hostProject, workspaceProject); var anotherProject = GetWorkspaceProject("Test1"); var update = new ProjectSnapshotUpdateContext("Test1.csproj", hostProject, anotherProject, VersionStamp.Default) { TagHelpers = new[] { TagHelperDescriptorBuilder.Create("One", "TestAssembly").Build(), TagHelperDescriptorBuilder.Create("Two", "TestAssembly").Build(), }, }; var snapshot = original.WithComputedUpdate(update); // Act var result = snapshot.HaveTagHelpersChanged(original); // Assert Assert.True(result); }
public void TrySetOutput_InvokesChangedEvent() { // Arrange using var workspace = TestWorkspace.Create(); var services = workspace.Services; var hostProject = new HostProject("C:/project.csproj", RazorConfiguration.Default, "project"); var projectState = ProjectState.Create(services, hostProject); var project = new DefaultProjectSnapshot(projectState); 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 htmlDocument = RazorHtmlDocument.Create("...", RazorCodeGenerationOptions.CreateDefault()); var codeDocument = CreateCodeDocument(csharpDocument, htmlDocument); var version = VersionStamp.Create(); var container = new GeneratedDocumentContainer(); var csharpChanged = false; var htmlChanged = false; container.GeneratedCSharpChanged += (o, a) => csharpChanged = true; container.GeneratedHtmlChanged += (o, a) => htmlChanged = true; // Act var result = container.TrySetOutput(document, codeDocument, version, version, version); // Assert Assert.NotNull(container.LatestDocument); Assert.True(csharpChanged); Assert.True(htmlChanged); Assert.True(result); }
public Task <(RazorCodeDocument, VersionStamp, VersionStamp, VersionStamp)> GetGeneratedOutputAndVersionAsync(DefaultProjectSnapshot project, DocumentSnapshot document) { if (project is null) { throw new ArgumentNullException(nameof(project)); } if (document is null) { throw new ArgumentNullException(nameof(document)); } if (_taskUnsafeReference is null || !_taskUnsafeReference.TryGetTarget(out var taskUnsafe)) { TaskCompletionSource <(RazorCodeDocument, VersionStamp, VersionStamp, VersionStamp)> tcs = null; lock (_lock) { if (_taskUnsafeReference is null || !_taskUnsafeReference.TryGetTarget(out taskUnsafe)) { // So this is a bit confusing. Instead of directly calling the Razor parser inside of this lock we create an indirect TaskCompletionSource // to represent when it completes. The reason behind this is that there are several scenarios in which the Razor parser will run synchronously // (mostly all in VS) resulting in this lock being held for significantly longer than expected. To avoid threads queuing up repeatedly on the // above lock and blocking we can allow those threads to await asynchronously for the completion of the original parse. tcs = new(TaskCreationOptions.RunContinuationsAsynchronously); taskUnsafe = tcs.Task; _taskUnsafeReference = new WeakReference <Task <(RazorCodeDocument, VersionStamp, VersionStamp, VersionStamp)> >(taskUnsafe); } } if (tcs is null) { // There's no task completion source created meaning a value was retrieved from cache, just return it. return(taskUnsafe); } // Typically in VS scenarios this will run synchronously because all resources are readily available. var outputTask = GetGeneratedOutputAndVersionCoreAsync(project, document); if (outputTask.IsCompleted) { // Compiling ran synchronously, lets just immediately propagate to the TCS PropagateToTaskCompletionSource(outputTask, tcs); } else { // Task didn't run synchronously (most likely outside of VS), lets allocate a bit more but utilize ContinueWith // to properly connect the output task and TCS _ = outputTask.ContinueWith( static (task, state) => { var tcs = (TaskCompletionSource <(RazorCodeDocument, VersionStamp, VersionStamp, VersionStamp)>)state; PropagateToTaskCompletionSource(task, tcs); }, tcs, CancellationToken.None, TaskContinuationOptions.ExecuteSynchronously, TaskScheduler.Default); }
public Task <(RazorCodeDocument output, VersionStamp inputVersion, VersionStamp outputCSharpVersion, VersionStamp outputHtmlVersion)> GetGeneratedOutputAndVersionAsync(DefaultProjectSnapshot project, DefaultDocumentSnapshot document) { return(ComputedState.GetGeneratedOutputAndVersionAsync(project, document)); }
private async Task <(RazorCodeDocument, VersionStamp, 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 configurationVersion = project.State.ConfigurationVersion; var projectWorkspaceStateVersion = project.State.ProjectWorkspaceStateVersion; 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(configurationVersion) == configurationVersion) { inputVersion = configurationVersion; } if (inputVersion.GetNewerVersion(projectWorkspaceStateVersion) == projectWorkspaceStateVersion) { inputVersion = projectWorkspaceStateVersion; } 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 olderCSharpOutputVersion = default(VersionStamp); var olderHtmlOutputVersion = default(VersionStamp); if (_older?.TaskUnsafeReference != null && _older.TaskUnsafeReference.TryGetTarget(out var taskUnsafe)) { (olderOutput, olderInputVersion, olderCSharpOutputVersion, olderHtmlOutputVersion) = await taskUnsafe.ConfigureAwait(false); if (inputVersion.GetNewerVersion(olderInputVersion) == olderInputVersion) { // Nothing has changed, we can use the cached result. lock (_lock) { TaskUnsafeReference = _older.TaskUnsafeReference; _older = null; return(olderOutput, olderInputVersion, olderCSharpOutputVersion, olderHtmlOutputVersion); } } } // OK we have to generate the code. var importSources = new List <RazorSourceDocument>(); var projectEngine = project.GetProjectEngine(); foreach (var item in imports) { var importProjectItem = item.FilePath == null ? null : projectEngine.FileSystem.GetItem(item.FilePath, item.FileKind); var sourceDocument = await GetRazorSourceDocumentAsync(item.Document, importProjectItem).ConfigureAwait(false); importSources.Add(sourceDocument); } var projectItem = document.FilePath == null ? null : projectEngine.FileSystem.GetItem(document.FilePath, document.FileKind); var documentSource = await GetRazorSourceDocumentAsync(document, projectItem).ConfigureAwait(false); var codeDocument = projectEngine.ProcessDesignTime(documentSource, fileKind: document.FileKind, importSources, project.TagHelpers); var csharpDocument = codeDocument.GetCSharpDocument(); var htmlDocument = codeDocument.GetHtmlDocument(); // 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#/HTML here (not the other artifacts). // This is the reason why we have three versions associated with the document. // // The INPUT version is related the .cshtml files and tag helpers // The CSHARPOUTPUT version is related to the generated C# // The HTMLOUTPUT version is related to the generated HTML // // Examples: // // A change to a tag helper not used by this document - 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 outputCSharpVersion = inputVersion; var outputHtmlVersion = inputVersion; if (olderOutput != null) { if (string.Equals( olderOutput.GetCSharpDocument().GeneratedCode, csharpDocument.GeneratedCode, StringComparison.Ordinal)) { outputCSharpVersion = olderCSharpOutputVersion; } if (string.Equals( olderOutput.GetHtmlDocument().GeneratedHtml, htmlDocument.GeneratedHtml, StringComparison.Ordinal)) { outputHtmlVersion = olderHtmlOutputVersion; } } if (document is DefaultDocumentSnapshot defaultDocument) { defaultDocument.State.HostDocument.GeneratedDocumentContainer.SetOutput( defaultDocument, csharpDocument, htmlDocument, inputVersion, outputCSharpVersion, outputHtmlVersion); } return(codeDocument, inputVersion, outputCSharpVersion, outputHtmlVersion); }
public Task <(RazorCodeDocument, VersionStamp, VersionStamp, VersionStamp)> GetGeneratedOutputAndVersionAsync(DefaultProjectSnapshot project, DocumentSnapshot document) { if (project == null) { throw new ArgumentNullException(nameof(project)); } if (document == null) { throw new ArgumentNullException(nameof(document)); } if (TaskUnsafeReference == null || !TaskUnsafeReference.TryGetTarget(out var taskUnsafe)) { lock (_lock) { if (TaskUnsafeReference == null || !TaskUnsafeReference.TryGetTarget(out taskUnsafe)) { taskUnsafe = GetGeneratedOutputAndVersionCoreAsync(project, document); TaskUnsafeReference = new WeakReference <Task <(RazorCodeDocument, VersionStamp, VersionStamp, VersionStamp)> >(taskUnsafe); } } } return(taskUnsafe); }
public IReadOnlyList <DocumentSnapshot> GetImports(DefaultProjectSnapshot project) { return(GetImportsCore(project)); }
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); }