예제 #1
0
        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));
        }
예제 #6
0
        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();
        }
예제 #7
0
        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);
        }
예제 #8
0
        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);
        }
예제 #9
0
        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);
        }
예제 #10
0
        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;
        }
예제 #11
0
        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();
            }
        }
예제 #12
0
        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);
        }
예제 #13
0
        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);
        }
예제 #15
0
        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);
        }
예제 #16
0
            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);
                    }
예제 #17
0
 public Task <(RazorCodeDocument output, VersionStamp inputVersion, VersionStamp outputCSharpVersion, VersionStamp outputHtmlVersion)> GetGeneratedOutputAndVersionAsync(DefaultProjectSnapshot project, DefaultDocumentSnapshot document)
 {
     return(ComputedState.GetGeneratedOutputAndVersionAsync(project, document));
 }
예제 #18
0
            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);
            }
예제 #19
0
            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);
            }
예제 #20
0
 public IReadOnlyList <DocumentSnapshot> GetImports(DefaultProjectSnapshot project)
 {
     return(GetImportsCore(project));
 }
예제 #21
0
            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);
            }