From f9d4fba39d80830c931116552a8e5d7e89db703d Mon Sep 17 00:00:00 2001 From: Ajay Bhargav Baaskaran Date: Fri, 11 May 2018 11:18:09 -0700 Subject: [PATCH] Added a taghelpers and imports overload to Process and ProcessDesignTime We want to have a way to specify the taghelper descriptors and imports to use while processing a specific document. - Added an overload to Process and ProcessDesignTime to take in a list TagHelperDescriptors and a list of imports - Added the corresponding CreateCodeDocumentCore overload - Added GetTagHelpers and SetTagHelpers extension methods for CodeDocument - Added the necessary plumbing to use the taghelpers from the CodeDocument when available and fallback logic. - Added DocumentImportsTracker and updated background code generation logic to use the new overload - Added/updated tests --- .../DefaultRazorProjectEngine.cs | 30 +++- .../DefaultRazorTagHelperBinderPhase.cs | 15 +- .../RazorCodeDocumentExtensions.cs | 30 ++++ .../RazorProjectEngine.cs | 34 ++++ .../ProjectSystem/DefaultDocumentSnapshot.cs | 11 +- .../DocumentGeneratedOutputTracker.cs | 92 +++++++++- .../ProjectSystem/DocumentImportsTracker.cs | 165 ++++++++++++++++++ .../ProjectSystem/DocumentSnapshot.cs | 4 +- .../ProjectSystem/DocumentState.cs | 32 +++- .../ProjectSystem/ProjectState.cs | 5 +- .../SourceTextExtensions.cs | 23 +++ .../EditorDocumentManagerListener.cs | 17 +- ...efaultRazorProjectEngineIntegrationTest.cs | 144 +++++++++++++++ .../DefaultRazorTagHelperBinderPhaseTest.cs | 164 ++++++++++++++++- .../CodeGenerationIntegrationTest.cs | 18 +- .../RazorCodeDocumentExtensionsTest.cs | 16 ++ .../ProjectSystem/ProjectStateTest.cs | 117 ++++++++++++- .../EditorDocumentManagerListenerTest.cs | 103 +++++++++++ .../Documents/EditorDocumentTest.cs | 27 --- 19 files changed, 971 insertions(+), 76 deletions(-) create mode 100644 src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/DocumentImportsTracker.cs create mode 100644 src/Microsoft.CodeAnalysis.Razor.Workspaces/SourceTextExtensions.cs create mode 100644 test/Microsoft.VisualStudio.Editor.Razor.Test/Documents/EditorDocumentManagerListenerTest.cs diff --git a/src/Microsoft.AspNetCore.Razor.Language/DefaultRazorProjectEngine.cs b/src/Microsoft.AspNetCore.Razor.Language/DefaultRazorProjectEngine.cs index e5993ce1a3..a560a0030a 100644 --- a/src/Microsoft.AspNetCore.Razor.Language/DefaultRazorProjectEngine.cs +++ b/src/Microsoft.AspNetCore.Razor.Language/DefaultRazorProjectEngine.cs @@ -68,10 +68,23 @@ namespace Microsoft.AspNetCore.Razor.Language var importItems = importFeature.GetImports(projectItem); var importSourceDocuments = GetImportSourceDocuments(importItems); + return CreateCodeDocumentCore(sourceDocument, importSourceDocuments, tagHelpers: null); + } + + internal override RazorCodeDocument CreateCodeDocumentCore(RazorSourceDocument sourceDocument, IReadOnlyList importSourceDocuments, IReadOnlyList tagHelpers) + { + if (sourceDocument == null) + { + throw new ArgumentNullException(nameof(sourceDocument)); + } + var parserOptions = GetRequiredFeature().Create(ConfigureParserOptions); var codeGenerationOptions = GetRequiredFeature().Create(ConfigureCodeGenerationOptions); - return RazorCodeDocument.Create(sourceDocument, importSourceDocuments, parserOptions, codeGenerationOptions); + var codeDocument = RazorCodeDocument.Create(sourceDocument, importSourceDocuments, parserOptions, codeGenerationOptions); + codeDocument.SetTagHelpers(tagHelpers); + + return codeDocument; } protected override RazorCodeDocument CreateCodeDocumentDesignTimeCore(RazorProjectItem projectItem) @@ -87,10 +100,23 @@ namespace Microsoft.AspNetCore.Razor.Language var importItems = importFeature.GetImports(projectItem); var importSourceDocuments = GetImportSourceDocuments(importItems, suppressExceptions: true); + return CreateCodeDocumentDesignTimeCore(sourceDocument, importSourceDocuments, tagHelpers: null); + } + + internal override RazorCodeDocument CreateCodeDocumentDesignTimeCore(RazorSourceDocument sourceDocument, IReadOnlyList importSourceDocuments, IReadOnlyList tagHelpers) + { + if (sourceDocument == null) + { + throw new ArgumentNullException(nameof(sourceDocument)); + } + var parserOptions = GetRequiredFeature().Create(ConfigureDesignTimeParserOptions); var codeGenerationOptions = GetRequiredFeature().Create(ConfigureDesignTimeCodeGenerationOptions); - return RazorCodeDocument.Create(sourceDocument, importSourceDocuments, parserOptions, codeGenerationOptions); + var codeDocument = RazorCodeDocument.Create(sourceDocument, importSourceDocuments, parserOptions, codeGenerationOptions); + codeDocument.SetTagHelpers(tagHelpers); + + return codeDocument; } protected override void ProcessCore(RazorCodeDocument codeDocument) diff --git a/src/Microsoft.AspNetCore.Razor.Language/DefaultRazorTagHelperBinderPhase.cs b/src/Microsoft.AspNetCore.Razor.Language/DefaultRazorTagHelperBinderPhase.cs index 7d9dfb7c6e..aa0668a539 100644 --- a/src/Microsoft.AspNetCore.Razor.Language/DefaultRazorTagHelperBinderPhase.cs +++ b/src/Microsoft.AspNetCore.Razor.Language/DefaultRazorTagHelperBinderPhase.cs @@ -15,18 +15,23 @@ namespace Microsoft.AspNetCore.Razor.Language var syntaxTree = codeDocument.GetSyntaxTree(); ThrowForMissingDocumentDependency(syntaxTree); - var feature = Engine.Features.OfType().FirstOrDefault(); - if (feature == null) + var descriptors = codeDocument.GetTagHelpers(); + if (descriptors == null) { - // No feature, nothing to do. - return; + var feature = Engine.Features.OfType().FirstOrDefault(); + if (feature == null) + { + // No feature, nothing to do. + return; + } + + descriptors = feature.GetDescriptors(); } // We need to find directives in all of the *imports* as well as in the main razor file // // The imports come logically before the main razor file and are in the order they // should be processed. - var descriptors = feature.GetDescriptors(); var visitor = new DirectiveVisitor(descriptors); var imports = codeDocument.GetImportSyntaxTrees(); if (imports != null) diff --git a/src/Microsoft.AspNetCore.Razor.Language/RazorCodeDocumentExtensions.cs b/src/Microsoft.AspNetCore.Razor.Language/RazorCodeDocumentExtensions.cs index 85df33837a..dafc33983f 100644 --- a/src/Microsoft.AspNetCore.Razor.Language/RazorCodeDocumentExtensions.cs +++ b/src/Microsoft.AspNetCore.Razor.Language/RazorCodeDocumentExtensions.cs @@ -29,6 +29,26 @@ namespace Microsoft.AspNetCore.Razor.Language document.Items[typeof(TagHelperDocumentContext)] = context; } + internal static IReadOnlyList GetTagHelpers(this RazorCodeDocument document) + { + if (document == null) + { + throw new ArgumentNullException(nameof(document)); + } + + return (document.Items[typeof(TagHelpersHolder)] as TagHelpersHolder)?.TagHelpers; + } + + internal static void SetTagHelpers(this RazorCodeDocument document, IReadOnlyList tagHelpers) + { + if (document == null) + { + throw new ArgumentNullException(nameof(document)); + } + + document.Items[typeof(TagHelpersHolder)] = new TagHelpersHolder(tagHelpers); + } + public static RazorSyntaxTree GetSyntaxTree(this RazorCodeDocument document) { if (document == null) @@ -168,5 +188,15 @@ namespace Microsoft.AspNetCore.Razor.Language public IReadOnlyList SyntaxTrees { get; } } + + private class TagHelpersHolder + { + public TagHelpersHolder(IReadOnlyList tagHelpers) + { + TagHelpers = tagHelpers; + } + + public IReadOnlyList TagHelpers { get; } + } } } diff --git a/src/Microsoft.AspNetCore.Razor.Language/RazorProjectEngine.cs b/src/Microsoft.AspNetCore.Razor.Language/RazorProjectEngine.cs index 07ee624370..a36b80d4b0 100644 --- a/src/Microsoft.AspNetCore.Razor.Language/RazorProjectEngine.cs +++ b/src/Microsoft.AspNetCore.Razor.Language/RazorProjectEngine.cs @@ -35,6 +35,18 @@ namespace Microsoft.AspNetCore.Razor.Language return codeDocument; } + internal virtual RazorCodeDocument Process(RazorSourceDocument source, IReadOnlyList importSources, IReadOnlyList tagHelpers) + { + if (source == null) + { + throw new ArgumentNullException(nameof(source)); + } + + var codeDocument = CreateCodeDocumentCore(source, importSources, tagHelpers); + ProcessCore(codeDocument); + return codeDocument; + } + public virtual RazorCodeDocument ProcessDesignTime(RazorProjectItem projectItem) { if (projectItem == null) @@ -47,10 +59,32 @@ namespace Microsoft.AspNetCore.Razor.Language return codeDocument; } + internal virtual RazorCodeDocument ProcessDesignTime(RazorSourceDocument source, IReadOnlyList importSources, IReadOnlyList tagHelpers) + { + if (source == null) + { + throw new ArgumentNullException(nameof(source)); + } + + var codeDocument = CreateCodeDocumentDesignTimeCore(source, importSources, tagHelpers); + ProcessCore(codeDocument); + return codeDocument; + } + protected abstract RazorCodeDocument CreateCodeDocumentCore(RazorProjectItem projectItem); + internal virtual RazorCodeDocument CreateCodeDocumentCore(RazorSourceDocument source, IReadOnlyList importSources, IReadOnlyList tagHelpers) + { + return RazorCodeDocument.Create(source, importSources); + } + protected abstract RazorCodeDocument CreateCodeDocumentDesignTimeCore(RazorProjectItem projectItem); + internal virtual RazorCodeDocument CreateCodeDocumentDesignTimeCore(RazorSourceDocument source, IReadOnlyList importSources, IReadOnlyList tagHelpers) + { + return RazorCodeDocument.Create(source, importSources); + } + protected abstract void ProcessCore(RazorCodeDocument codeDocument); internal static RazorProjectEngine CreateEmpty(Action configure = null) diff --git a/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/DefaultDocumentSnapshot.cs b/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/DefaultDocumentSnapshot.cs index 0cc4512f2e..d25b38ddbe 100644 --- a/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/DefaultDocumentSnapshot.cs +++ b/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/DefaultDocumentSnapshot.cs @@ -2,7 +2,7 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; -using System.Threading; +using System.Collections.Generic; using System.Threading.Tasks; using Microsoft.AspNetCore.Razor.Language; using Microsoft.CodeAnalysis.Text; @@ -11,7 +11,7 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem { internal class DefaultDocumentSnapshot : DocumentSnapshot { - public DefaultDocumentSnapshot(ProjectSnapshot project, DocumentState state) + public DefaultDocumentSnapshot(DefaultProjectSnapshot project, DocumentState state) { if (project == null) { @@ -27,7 +27,7 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem State = state; } - public ProjectSnapshot Project { get; } + public DefaultProjectSnapshot Project { get; } public DocumentState State { get; } @@ -35,6 +35,11 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem public override string TargetPath => State.HostDocument.TargetPath; + public override IReadOnlyList GetImports() + { + return State.Imports.GetImports(Project, this); + } + public override Task GetTextAsync() { return State.GetTextAsync(); diff --git a/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/DocumentGeneratedOutputTracker.cs b/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/DocumentGeneratedOutputTracker.cs index 20329b954e..a3e11da04f 100644 --- a/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/DocumentGeneratedOutputTracker.cs +++ b/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/DocumentGeneratedOutputTracker.cs @@ -5,6 +5,8 @@ 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 { @@ -16,6 +18,7 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem private Task _task; private IReadOnlyList _tagHelpers; + private IReadOnlyList _imports; public DocumentGeneratedOutputTracker(DocumentGeneratedOutputTracker older) { @@ -62,13 +65,19 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem private async Task GetGeneratedOutputInitializationTaskCore(ProjectSnapshot project, DocumentSnapshot document) { var tagHelpers = await project.GetTagHelpersAsync().ConfigureAwait(false); + var imports = await GetImportsAsync(project, document); + if (_older != null && _older.IsResultAvailable) { - var difference = new HashSet(TagHelperDescriptorComparer.Default); - difference.UnionWith(_older._tagHelpers); - difference.SymmetricExceptWith(tagHelpers); + var tagHelperDifference = new HashSet(TagHelperDescriptorComparer.Default); + tagHelperDifference.UnionWith(_older._tagHelpers); + tagHelperDifference.SymmetricExceptWith(tagHelpers); - if (difference.Count == 0) + var importDifference = new HashSet(); + 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; @@ -76,8 +85,9 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem // Drop reference so it can be GC'ed _older = null; - // Cache the tag helpers so the next version can use them + // Cache the tag helpers and imports so the next version can use them _tagHelpers = tagHelpers; + _imports = imports; return result; } @@ -86,13 +96,77 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem // Drop reference so it can be GC'ed _older = null; - - // Cache the tag helpers so the next version can use them + // Cache the tag helpers and imports so the next version can use them _tagHelpers = tagHelpers; + _imports = imports; + + var importSources = new List(); + foreach (var item in imports) + { + var sourceDocument = await GetRazorSourceDocumentAsync(item.Import); + importSources.Add(sourceDocument); + } + + var documentSource = await GetRazorSourceDocumentAsync(document); var projectEngine = project.GetProjectEngine(); - var projectItem = projectEngine.FileSystem.GetItem(document.FilePath); - return projectItem == null ? null : projectEngine.ProcessDesignTime(projectItem); + + return projectEngine.ProcessDesignTime(documentSource, importSources, tagHelpers); + } + + private async Task GetRazorSourceDocumentAsync(DocumentSnapshot document) + { + var sourceText = await document.GetTextAsync(); + + return sourceText.GetRazorSourceDocument(document.FilePath); + } + + private async Task> GetImportsAsync(ProjectSnapshot project, DocumentSnapshot document) + { + var imports = new List(); + 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 + { + 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; + } } } } diff --git a/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/DocumentImportsTracker.cs b/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/DocumentImportsTracker.cs new file mode 100644 index 0000000000..96b927e42f --- /dev/null +++ b/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/DocumentImportsTracker.cs @@ -0,0 +1,165 @@ +// 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 _imports; + + public DocumentImportsTracker() + { + _lock = new object(); + } + + public IReadOnlyList 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 GetImportsCore(ProjectSnapshot project, DocumentSnapshot document) + { + var projectEngine = project.GetProjectEngine(); + var importFeature = projectEngine.ProjectFeatures.OfType().FirstOrDefault(); + var projectItem = projectEngine.FileSystem.GetItem(document.FilePath); + var importItems = importFeature?.GetImports(projectItem).Where(i => i.Exists); + if (importItems == null) + { + return Array.Empty(); + } + + var imports = new List(); + 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 Task GetGeneratedOutputAsync() + { + return _generatedOutput.GetGeneratedOutputInitializationTask(_project, this); + } + + public override IReadOnlyList GetImports() + { + return Array.Empty(); + } + + public async override Task 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 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; + } + } + } +} diff --git a/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/DocumentSnapshot.cs b/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/DocumentSnapshot.cs index ac94c6a1cd..0cc9da8f04 100644 --- a/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/DocumentSnapshot.cs +++ b/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/DocumentSnapshot.cs @@ -1,7 +1,7 @@ // Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. -using System.Threading; +using System.Collections.Generic; using System.Threading.Tasks; using Microsoft.AspNetCore.Razor.Language; using Microsoft.CodeAnalysis.Text; @@ -14,6 +14,8 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem public abstract string TargetPath { get; } + public abstract IReadOnlyList GetImports(); + public abstract Task GetTextAsync(); public abstract Task GetTextVersionAsync(); diff --git a/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/DocumentState.cs b/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/DocumentState.cs index 192a038f1d..d1fb05b67a 100644 --- a/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/DocumentState.cs +++ b/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/DocumentState.cs @@ -24,6 +24,7 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem private VersionStamp? _version; private DocumentGeneratedOutputTracker _generatedOutput; + private DocumentImportsTracker _imports; public static DocumentState Create( HostWorkspaceServices services, @@ -44,7 +45,8 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem return new DocumentState(services, hostDocument, null, null, loader); } - private DocumentState( + // Internal for testing + internal DocumentState( HostWorkspaceServices services, HostDocument hostDocument, SourceText text, @@ -82,6 +84,25 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem } } + public DocumentImportsTracker Imports + { + get + { + if (_imports == null) + { + lock (_lock) + { + if (_imports == null) + { + _imports = new DocumentImportsTracker(); + } + } + } + + return _imports; + } + } + public async Task GetTextAsync() { if (TryGetText(out var text)) @@ -148,7 +169,7 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem return false; } - public DocumentState WithConfigurationChange() + public virtual DocumentState WithConfigurationChange() { var state = new DocumentState(Services, HostDocument, _sourceText, _version, _loader); @@ -160,7 +181,7 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem return state; } - public DocumentState WithWorkspaceProjectChange() + public virtual DocumentState WithWorkspaceProjectChange() { var state = new DocumentState(Services, HostDocument, _sourceText, _version, _loader); @@ -175,7 +196,7 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem return state; } - public DocumentState WithText(SourceText sourceText, VersionStamp version) + public virtual DocumentState WithText(SourceText sourceText, VersionStamp version) { if (sourceText == null) { @@ -185,8 +206,7 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem return new DocumentState(Services, HostDocument, sourceText, version, null); } - - public DocumentState WithTextLoader(Func> loader) + public virtual DocumentState WithTextLoader(Func> loader) { if (loader == null) { diff --git a/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/ProjectState.cs b/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/ProjectState.cs index 5dece86c8f..6670ba55a2 100644 --- a/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/ProjectState.cs +++ b/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/ProjectState.cs @@ -83,7 +83,8 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem _tagHelpers = older._tagHelpers?.ForkFor(this, difference); } - public IReadOnlyDictionary Documents { get; } + // Internal set for testing. + public IReadOnlyDictionary Documents { get; internal set; } public HostProject HostProject { get; } @@ -293,7 +294,7 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem var documents = new Dictionary(FilePathComparer.Instance); foreach (var kvp in Documents) { - documents.Add(kvp.Key, kvp.Value.WithConfigurationChange()); + documents.Add(kvp.Key, kvp.Value.WithWorkspaceProjectChange()); } var state = new ProjectState(this, difference, HostProject, workspaceProject, documents); diff --git a/src/Microsoft.CodeAnalysis.Razor.Workspaces/SourceTextExtensions.cs b/src/Microsoft.CodeAnalysis.Razor.Workspaces/SourceTextExtensions.cs new file mode 100644 index 0000000000..efc74344d5 --- /dev/null +++ b/src/Microsoft.CodeAnalysis.Razor.Workspaces/SourceTextExtensions.cs @@ -0,0 +1,23 @@ +// 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 Microsoft.AspNetCore.Razor.Language; + +namespace Microsoft.CodeAnalysis.Text +{ + internal static class SourceTextExtensions + { + public static RazorSourceDocument GetRazorSourceDocument(this SourceText sourceText, string fileName) + { + if (sourceText == null) + { + throw new ArgumentNullException(nameof(sourceText)); + } + + var content = sourceText.ToString(); + + return RazorSourceDocument.Create(content, fileName); + } + } +} diff --git a/src/Microsoft.VisualStudio.Editor.Razor/Documents/EditorDocumentManagerListener.cs b/src/Microsoft.VisualStudio.Editor.Razor/Documents/EditorDocumentManagerListener.cs index 33abac5804..284df88590 100644 --- a/src/Microsoft.VisualStudio.Editor.Razor/Documents/EditorDocumentManagerListener.cs +++ b/src/Microsoft.VisualStudio.Editor.Razor/Documents/EditorDocumentManagerListener.cs @@ -32,6 +32,16 @@ namespace Microsoft.VisualStudio.Editor.Razor.Documents _onClosed = Document_Closed; } + // For testing purposes only. + internal EditorDocumentManagerListener(EditorDocumentManager documentManager, EventHandler onChangedOnDisk, EventHandler onChangedInEditor, EventHandler onOpened, EventHandler onClosed) + { + _documentManager = documentManager; + _onChangedOnDisk = onChangedOnDisk; + _onChangedInEditor = onChangedInEditor; + _onOpened = onOpened; + _onClosed = onClosed; + } + public override void Initialize(ProjectSnapshotManagerBase projectManager) { if (projectManager == null) @@ -45,17 +55,18 @@ namespace Microsoft.VisualStudio.Editor.Razor.Documents _projectManager.Changed += ProjectManager_Changed; } - private void ProjectManager_Changed(object sender, ProjectChangeEventArgs e) + // Internal for testing. + internal void ProjectManager_Changed(object sender, ProjectChangeEventArgs e) { switch (e.Kind) { case ProjectChangeKind.DocumentAdded: { var key = new DocumentKey(e.ProjectFilePath, e.DocumentFilePath); - var document = _documentManager.GetOrCreateDocument(key, _onChangedOnDisk, _onChangedOnDisk, _onOpened, _onClosed); + var document = _documentManager.GetOrCreateDocument(key, _onChangedOnDisk, _onChangedInEditor, _onOpened, _onClosed); if (document.IsOpenInEditor) { - Document_Opened(document, EventArgs.Empty); + _onOpened(document, EventArgs.Empty); } break; diff --git a/test/Microsoft.AspNetCore.Razor.Language.Test/DefaultRazorProjectEngineIntegrationTest.cs b/test/Microsoft.AspNetCore.Razor.Language.Test/DefaultRazorProjectEngineIntegrationTest.cs index 38b5082b2e..d9c58137bb 100644 --- a/test/Microsoft.AspNetCore.Razor.Language.Test/DefaultRazorProjectEngineIntegrationTest.cs +++ b/test/Microsoft.AspNetCore.Razor.Language.Test/DefaultRazorProjectEngineIntegrationTest.cs @@ -1,6 +1,8 @@ // 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 Moq; using Xunit; @@ -91,5 +93,147 @@ namespace Microsoft.AspNetCore.Razor.Language Assert.NotNull(csharpDocument); Assert.Empty(csharpDocument.Diagnostics); } + + [Fact] + public void Process_WithImportsAndTagHelpers_SetsOnCodeDocument() + { + // Arrange + var projectItem = new TestRazorProjectItem("Index.cshtml"); + var importItem = new TestRazorProjectItem("_import.cshtml"); + var expectedImports = new[] { RazorSourceDocument.ReadFrom(importItem) }; + var expectedTagHelpers = new[] + { + TagHelperDescriptorBuilder.Create("TestTagHelper", "TestAssembly").Build(), + TagHelperDescriptorBuilder.Create("Test2TagHelper", "TestAssembly").Build(), + }; + + var projectEngine = RazorProjectEngine.Create(RazorConfiguration.Default, TestRazorProjectFileSystem.Empty); + + // Act + var codeDocument = projectEngine.Process(RazorSourceDocument.ReadFrom(projectItem), expectedImports, expectedTagHelpers); + + // Assert + var tagHelpers = codeDocument.GetTagHelpers(); + Assert.Same(expectedTagHelpers, tagHelpers); + Assert.Equal(expectedImports, codeDocument.Imports); + } + + [Fact] + public void Process_WithNullTagHelpers_SetsOnCodeDocument() + { + // Arrange + var projectItem = new TestRazorProjectItem("Index.cshtml"); + + var projectEngine = RazorProjectEngine.Create(RazorConfiguration.Default, TestRazorProjectFileSystem.Empty); + + // Act + var codeDocument = projectEngine.Process(RazorSourceDocument.ReadFrom(projectItem), Array.Empty(), tagHelpers: null); + + // Assert + var tagHelpers = codeDocument.GetTagHelpers(); + Assert.Null(tagHelpers); + } + + [Fact] + public void Process_SetsNullTagHelpersOnCodeDocument() + { + // Arrange + var projectItem = new TestRazorProjectItem("Index.cshtml"); + + var projectEngine = RazorProjectEngine.Create(RazorConfiguration.Default, TestRazorProjectFileSystem.Empty); + + // Act + var codeDocument = projectEngine.Process(projectItem); + + // Assert + var tagHelpers = codeDocument.GetTagHelpers(); + Assert.Null(tagHelpers); + } + + [Fact] + public void Process_WithNullImports_SetsEmptyListOnCodeDocument() + { + // Arrange + var projectItem = new TestRazorProjectItem("Index.cshtml"); + + var projectEngine = RazorProjectEngine.Create(RazorConfiguration.Default, TestRazorProjectFileSystem.Empty); + + // Act + var codeDocument = projectEngine.Process(RazorSourceDocument.ReadFrom(projectItem), importSources: null, tagHelpers: null); + + // Assert + Assert.Empty(codeDocument.Imports); + } + + [Fact] + public void ProcessDesignTime_WithImportsAndTagHelpers_SetsOnCodeDocument() + { + // Arrange + var projectItem = new TestRazorProjectItem("Index.cshtml"); + var importItem = new TestRazorProjectItem("_import.cshtml"); + var expectedImports = new[] { RazorSourceDocument.ReadFrom(importItem) }; + var expectedTagHelpers = new[] + { + TagHelperDescriptorBuilder.Create("TestTagHelper", "TestAssembly").Build(), + TagHelperDescriptorBuilder.Create("Test2TagHelper", "TestAssembly").Build(), + }; + + var projectEngine = RazorProjectEngine.Create(RazorConfiguration.Default, TestRazorProjectFileSystem.Empty); + + // Act + var codeDocument = projectEngine.ProcessDesignTime(RazorSourceDocument.ReadFrom(projectItem), expectedImports, expectedTagHelpers); + + // Assert + var tagHelpers = codeDocument.GetTagHelpers(); + Assert.Same(expectedTagHelpers, tagHelpers); + Assert.Equal(expectedImports, codeDocument.Imports); + } + + [Fact] + public void ProcessDesignTime_WithNullTagHelpers_SetsOnCodeDocument() + { + // Arrange + var projectItem = new TestRazorProjectItem("Index.cshtml"); + + var projectEngine = RazorProjectEngine.Create(RazorConfiguration.Default, TestRazorProjectFileSystem.Empty); + + // Act + var codeDocument = projectEngine.ProcessDesignTime(RazorSourceDocument.ReadFrom(projectItem), Array.Empty(), tagHelpers: null); + + // Assert + var tagHelpers = codeDocument.GetTagHelpers(); + Assert.Null(tagHelpers); + } + + [Fact] + public void ProcessDesignTime_SetsNullTagHelpersOnCodeDocument() + { + // Arrange + var projectItem = new TestRazorProjectItem("Index.cshtml"); + + var projectEngine = RazorProjectEngine.Create(RazorConfiguration.Default, TestRazorProjectFileSystem.Empty); + + // Act + var codeDocument = projectEngine.ProcessDesignTime(projectItem); + + // Assert + var tagHelpers = codeDocument.GetTagHelpers(); + Assert.Null(tagHelpers); + } + + [Fact] + public void ProcessDesignTime_WithNullImports_SetsEmptyListOnCodeDocument() + { + // Arrange + var projectItem = new TestRazorProjectItem("Index.cshtml"); + + var projectEngine = RazorProjectEngine.Create(RazorConfiguration.Default, TestRazorProjectFileSystem.Empty); + + // Act + var codeDocument = projectEngine.ProcessDesignTime(RazorSourceDocument.ReadFrom(projectItem), importSources: null, tagHelpers: null); + + // Assert + Assert.Empty(codeDocument.Imports); + } } } diff --git a/test/Microsoft.AspNetCore.Razor.Language.Test/DefaultRazorTagHelperBinderPhaseTest.cs b/test/Microsoft.AspNetCore.Razor.Language.Test/DefaultRazorTagHelperBinderPhaseTest.cs index 0585198d3e..65b5b49fe9 100644 --- a/test/Microsoft.AspNetCore.Razor.Language.Test/DefaultRazorTagHelperBinderPhaseTest.cs +++ b/test/Microsoft.AspNetCore.Razor.Language.Test/DefaultRazorTagHelperBinderPhaseTest.cs @@ -171,6 +171,129 @@ namespace Microsoft.AspNetCore.Razor.Language Assert.Equal("input", inputTagHelper.TagName); } + [Fact] + public void Execute_WithTagHelperDescriptorsFromCodeDocument_RewritesTagHelpers() + { + // Arrange + var projectEngine = RazorProjectEngine.Create(); + var tagHelpers = new[] + { + CreateTagHelperDescriptor( + tagName: "form", + typeName: "TestFormTagHelper", + assemblyName: "TestAssembly"), + CreateTagHelperDescriptor( + tagName: "input", + typeName: "TestInputTagHelper", + assemblyName: "TestAssembly"), + }; + + var phase = new DefaultRazorTagHelperBinderPhase() + { + Engine = projectEngine.Engine, + }; + + var sourceDocument = CreateTestSourceDocument(); + var codeDocument = RazorCodeDocument.Create(sourceDocument); + var originalTree = RazorSyntaxTree.Parse(sourceDocument); + codeDocument.SetSyntaxTree(originalTree); + codeDocument.SetTagHelpers(tagHelpers); + + // Act + phase.Execute(codeDocument); + + // Assert + var rewrittenTree = codeDocument.GetSyntaxTree(); + Assert.Empty(rewrittenTree.Diagnostics); + Assert.Equal(3, rewrittenTree.Root.Children.Count); + var formTagHelper = Assert.IsType(rewrittenTree.Root.Children[2]); + Assert.Equal("form", formTagHelper.TagName); + Assert.Equal(3, formTagHelper.Children.Count); + var inputTagHelper = Assert.IsType(formTagHelper.Children[1]); + Assert.Equal("input", inputTagHelper.TagName); + } + + [Fact] + public void Execute_NullTagHelperDescriptorsFromCodeDocument_FallsBackToTagHelperFeature() + { + // Arrange + var tagHelpers = new[] + { + CreateTagHelperDescriptor( + tagName: "form", + typeName: "TestFormTagHelper", + assemblyName: "TestAssembly"), + CreateTagHelperDescriptor( + tagName: "input", + typeName: "TestInputTagHelper", + assemblyName: "TestAssembly"), + }; + var projectEngine = RazorProjectEngine.Create(builder => builder.AddTagHelpers(tagHelpers)); + + var phase = new DefaultRazorTagHelperBinderPhase() + { + Engine = projectEngine.Engine, + }; + + var sourceDocument = CreateTestSourceDocument(); + var codeDocument = RazorCodeDocument.Create(sourceDocument); + var originalTree = RazorSyntaxTree.Parse(sourceDocument); + codeDocument.SetSyntaxTree(originalTree); + codeDocument.SetTagHelpers(tagHelpers: null); + + // Act + phase.Execute(codeDocument); + + // Assert + var rewrittenTree = codeDocument.GetSyntaxTree(); + Assert.Empty(rewrittenTree.Diagnostics); + Assert.Equal(3, rewrittenTree.Root.Children.Count); + var formTagHelper = Assert.IsType(rewrittenTree.Root.Children[2]); + Assert.Equal("form", formTagHelper.TagName); + Assert.Equal(3, formTagHelper.Children.Count); + var inputTagHelper = Assert.IsType(formTagHelper.Children[1]); + Assert.Equal("input", inputTagHelper.TagName); + } + + [Fact] + public void Execute_EmptyTagHelperDescriptorsFromCodeDocument_DoesNotFallbackToTagHelperFeature() + { + // Arrange + var tagHelpers = new[] + { + CreateTagHelperDescriptor( + tagName: "form", + typeName: "TestFormTagHelper", + assemblyName: "TestAssembly"), + CreateTagHelperDescriptor( + tagName: "input", + typeName: "TestInputTagHelper", + assemblyName: "TestAssembly"), + }; + var projectEngine = RazorProjectEngine.Create(builder => builder.AddTagHelpers(tagHelpers)); + + var phase = new DefaultRazorTagHelperBinderPhase() + { + Engine = projectEngine.Engine, + }; + + var sourceDocument = CreateTestSourceDocument(); + var codeDocument = RazorCodeDocument.Create(sourceDocument); + var originalTree = RazorSyntaxTree.Parse(sourceDocument); + codeDocument.SetSyntaxTree(originalTree); + codeDocument.SetTagHelpers(tagHelpers: Array.Empty()); + + // Act + phase.Execute(codeDocument); + + // Assert + var rewrittenTree = codeDocument.GetSyntaxTree(); + Assert.Empty(rewrittenTree.Diagnostics); + Assert.Equal(7, rewrittenTree.Root.Children.Count); + var rewrittenNodes = rewrittenTree.Root.Children.OfType(); + Assert.Empty(rewrittenNodes); + } + [Fact] public void Execute_DirectiveWithoutQuotes_RewritesTagHelpers_TagHelperMatchesElementTwice() { @@ -278,35 +401,58 @@ namespace Microsoft.AspNetCore.Razor.Language } [Fact] - public void Execute_NoopsWhenNoTagHelperFeature() + public void Execute_TagHelpersFromCodeDocumentAndFeature_PrefersCodeDocument() { // Arrange - var projectEngine = RazorProjectEngine.Create(); + var featureTagHelpers = new[] + { + CreateTagHelperDescriptor( + tagName: "input", + typeName: "TestInputTagHelper", + assemblyName: "TestAssembly"), + }; + var projectEngine = RazorProjectEngine.Create(builder => builder.AddTagHelpers(featureTagHelpers)); + var phase = new DefaultRazorTagHelperBinderPhase() { Engine = projectEngine.Engine, }; + var sourceDocument = CreateTestSourceDocument(); var codeDocument = RazorCodeDocument.Create(sourceDocument); var originalTree = RazorSyntaxTree.Parse(sourceDocument); codeDocument.SetSyntaxTree(originalTree); + var codeDocumentTagHelpers = new[] + { + CreateTagHelperDescriptor( + tagName: "form", + typeName: "TestFormTagHelper", + assemblyName: "TestAssembly"), + }; + codeDocument.SetTagHelpers(codeDocumentTagHelpers); + // Act phase.Execute(codeDocument); // Assert - var outputTree = codeDocument.GetSyntaxTree(); - Assert.Empty(outputTree.Diagnostics); - Assert.Same(originalTree, outputTree); + var rewrittenTree = codeDocument.GetSyntaxTree(); + Assert.Empty(rewrittenTree.Diagnostics); + Assert.Equal(3, rewrittenTree.Root.Children.Count); + var formTagHelper = Assert.IsType(rewrittenTree.Root.Children[2]); + Assert.Equal("form", formTagHelper.TagName); + Assert.Collection( + formTagHelper.Children, + node => Assert.IsNotType(node), + node => Assert.IsNotType(node), + node => Assert.IsNotType(node)); } [Fact] - public void Execute_NoopsWhenNoFeature() + public void Execute_NoopsWhenNoTagHelpersFromCodeDocumentOrFeature() { // Arrange - var projectEngine = RazorProjectEngine.Create(builder => - { - }); + var projectEngine = RazorProjectEngine.Create(); var phase = new DefaultRazorTagHelperBinderPhase() { Engine = projectEngine.Engine, diff --git a/test/Microsoft.AspNetCore.Razor.Language.Test/IntegrationTests/CodeGenerationIntegrationTest.cs b/test/Microsoft.AspNetCore.Razor.Language.Test/IntegrationTests/CodeGenerationIntegrationTest.cs index e40989bbb3..34ddc3fb94 100644 --- a/test/Microsoft.AspNetCore.Razor.Language.Test/IntegrationTests/CodeGenerationIntegrationTest.cs +++ b/test/Microsoft.AspNetCore.Razor.Language.Test/IntegrationTests/CodeGenerationIntegrationTest.cs @@ -2,6 +2,7 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System.Collections.Generic; +using System.Linq; using Microsoft.AspNetCore.Razor.Language.Extensions; using Xunit; @@ -937,7 +938,6 @@ namespace Microsoft.AspNetCore.Razor.Language.IntegrationTests var projectEngine = CreateProjectEngine(builder => { builder.ConfigureDocumentClassifier(); - builder.AddTagHelpers(descriptors); // Some of these tests use templates builder.AddTargetExtension(new TemplateTargetExtension()); @@ -948,9 +948,10 @@ namespace Microsoft.AspNetCore.Razor.Language.IntegrationTests }); var projectItem = CreateProjectItem(); + var imports = GetImports(projectEngine, projectItem); // Act - var codeDocument = projectEngine.Process(projectItem); + var codeDocument = projectEngine.Process(RazorSourceDocument.ReadFrom(projectItem), imports, descriptors.ToList()); // Assert AssertDocumentNodeMatchesBaseline(codeDocument.GetDocumentIntermediateNode()); @@ -963,7 +964,6 @@ namespace Microsoft.AspNetCore.Razor.Language.IntegrationTests var projectEngine = CreateProjectEngine(builder => { builder.ConfigureDocumentClassifier(); - builder.AddTagHelpers(descriptors); // Some of these tests use templates builder.AddTargetExtension(new TemplateTargetExtension()); @@ -974,14 +974,24 @@ namespace Microsoft.AspNetCore.Razor.Language.IntegrationTests }); var projectItem = CreateProjectItem(); + var imports = GetImports(projectEngine, projectItem); // Act - var codeDocument = projectEngine.ProcessDesignTime(projectItem); + var codeDocument = projectEngine.ProcessDesignTime(RazorSourceDocument.ReadFrom(projectItem), imports, descriptors.ToList()); // Assert AssertDocumentNodeMatchesBaseline(codeDocument.GetDocumentIntermediateNode()); AssertCSharpDocumentMatchesBaseline(codeDocument.GetCSharpDocument()); AssertSourceMappingsMatchBaseline(codeDocument); } + + private static IReadOnlyList GetImports(RazorProjectEngine projectEngine, RazorProjectItem projectItem) + { + var importFeature = projectEngine.ProjectFeatures.OfType().FirstOrDefault(); + var importItems = importFeature.GetImports(projectItem); + var importSourceDocuments = importItems.Where(i => i.Exists).Select(i => RazorSourceDocument.ReadFrom(i)).ToList(); + + return importSourceDocuments; + } } } diff --git a/test/Microsoft.AspNetCore.Razor.Language.Test/RazorCodeDocumentExtensionsTest.cs b/test/Microsoft.AspNetCore.Razor.Language.Test/RazorCodeDocumentExtensionsTest.cs index 41f899512b..ceccb7f057 100644 --- a/test/Microsoft.AspNetCore.Razor.Language.Test/RazorCodeDocumentExtensionsTest.cs +++ b/test/Microsoft.AspNetCore.Razor.Language.Test/RazorCodeDocumentExtensionsTest.cs @@ -56,6 +56,22 @@ namespace Microsoft.AspNetCore.Razor.Language Assert.Same(expected, actual); } + [Fact] + public void GetAndSetTagHelpers_ReturnsTagHelpers() + { + // Arrange + var codeDocument = TestRazorCodeDocument.CreateEmpty(); + + var expected = new[] { TagHelperDescriptorBuilder.Create("TestTagHelper", "TestAssembly").Build() }; + codeDocument.SetTagHelpers(expected); + + // Act + var actual = codeDocument.GetTagHelpers(); + + // Assert + Assert.Same(expected, actual); + } + [Fact] public void GetIRDocument_ReturnsIRDocument() { diff --git a/test/Microsoft.CodeAnalysis.Razor.Workspaces.Test/ProjectSystem/ProjectStateTest.cs b/test/Microsoft.CodeAnalysis.Razor.Workspaces.Test/ProjectSystem/ProjectStateTest.cs index e2fbeec251..585d5af27e 100644 --- a/test/Microsoft.CodeAnalysis.Razor.Workspaces.Test/ProjectSystem/ProjectStateTest.cs +++ b/test/Microsoft.CodeAnalysis.Razor.Workspaces.Test/ProjectSystem/ProjectStateTest.cs @@ -4,12 +4,10 @@ using System; using System.Collections.Generic; using System.Linq; -using System.Text; using System.Threading.Tasks; using Microsoft.AspNetCore.Razor.Language; using Microsoft.CodeAnalysis.Host; using Microsoft.CodeAnalysis.Text; -using Moq; using Xunit; namespace Microsoft.CodeAnalysis.Razor.ProjectSystem @@ -373,7 +371,7 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem Assert.NotSame(original.TagHelpers, state.TagHelpers); Assert.NotSame(original.Documents[Documents[1].FilePath], state.Documents[Documents[1].FilePath]); - 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] @@ -395,6 +393,30 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem Assert.Same(original, state); } + [Fact] + public void ProjectState_WithHostProject_CallsConfigurationChangeOnDocumentState() + { + // Arrange + var callCount = 0; + + var documents = new Dictionary(); + documents[Documents[1].FilePath] = TestDocumentState.Create(Workspace.Services, Documents[1], onConfigurationChange: () => callCount++); + documents[Documents[2].FilePath] = TestDocumentState.Create(Workspace.Services, Documents[2], onConfigurationChange: () => callCount++); + + var original = ProjectState.Create(Workspace.Services, HostProject, WorkspaceProject); + original.Documents = documents; + + var changed = WorkspaceProject.WithAssemblyName("Test1"); + + // Act + var state = original.WithHostProject(HostProjectWithConfigurationChange); + + // Assert + Assert.NotEqual(original.Version, state.Version); + Assert.Same(HostProjectWithConfigurationChange, state.HostProject); + Assert.Equal(2, callCount); + } + [Fact] public void ProjectState_WithWorkspaceProject_Removed() { @@ -418,7 +440,7 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem Assert.NotSame(original.TagHelpers, state.TagHelpers); Assert.NotSame(original.Documents[Documents[1].FilePath], state.Documents[Documents[1].FilePath]); - 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] @@ -472,7 +494,92 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem Assert.NotSame(original.TagHelpers, state.TagHelpers); Assert.NotSame(original.Documents[Documents[1].FilePath], state.Documents[Documents[1].FilePath]); - 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_CallsWorkspaceProjectChangeOnDocumentState() + { + // Arrange + var callCount = 0; + + var documents = new Dictionary(); + documents[Documents[1].FilePath] = TestDocumentState.Create(Workspace.Services, Documents[1], onWorkspaceProjectChange: () => callCount++); + documents[Documents[2].FilePath] = TestDocumentState.Create(Workspace.Services, Documents[2], onWorkspaceProjectChange: () => callCount++); + + var original = ProjectState.Create(Workspace.Services, HostProject, WorkspaceProject); + original.Documents = documents; + + var changed = WorkspaceProject.WithAssemblyName("Test1"); + + // Act + var state = original.WithWorkspaceProject(changed); + + // Assert + Assert.NotEqual(original.Version, state.Version); + Assert.Equal(2, callCount); + } + + private class TestDocumentState : DocumentState + { + public static TestDocumentState Create( + HostWorkspaceServices services, + HostDocument hostDocument, + Func> loader = null, + Action onTextChange = null, + Action onTextLoaderChange = null, + Action onConfigurationChange = null, + Action onWorkspaceProjectChange = null) + { + return new TestDocumentState(services, hostDocument, null, null, loader, onTextChange, onTextLoaderChange, onConfigurationChange, onWorkspaceProjectChange); + } + + Action _onTextChange; + Action _onTextLoaderChange; + Action _onConfigurationChange; + Action _onWorkspaceProjectChange; + + private TestDocumentState( + HostWorkspaceServices services, + HostDocument hostDocument, + SourceText text, + VersionStamp? version, + Func> loader, + Action onTextChange, + Action onTextLoaderChange, + Action onConfigurationChange, + Action onWorkspaceProjectChange) + : base(services, hostDocument, text, version, loader) + { + _onTextChange = onTextChange; + _onTextLoaderChange = onTextLoaderChange; + _onConfigurationChange = onConfigurationChange; + _onWorkspaceProjectChange = onWorkspaceProjectChange; + } + + public override DocumentState WithText(SourceText sourceText, VersionStamp version) + { + _onTextChange?.Invoke(); + return base.WithText(sourceText, version); + } + + public override DocumentState WithTextLoader(Func> loader) + { + _onTextLoaderChange?.Invoke(); + return base.WithTextLoader(loader); + } + + public override DocumentState WithConfigurationChange() + { + _onConfigurationChange?.Invoke(); + return base.WithConfigurationChange(); + } + + public override DocumentState WithWorkspaceProjectChange() + { + _onWorkspaceProjectChange?.Invoke(); + return base.WithWorkspaceProjectChange(); + } } } } diff --git a/test/Microsoft.VisualStudio.Editor.Razor.Test/Documents/EditorDocumentManagerListenerTest.cs b/test/Microsoft.VisualStudio.Editor.Razor.Test/Documents/EditorDocumentManagerListenerTest.cs new file mode 100644 index 0000000000..6d1eddacee --- /dev/null +++ b/test/Microsoft.VisualStudio.Editor.Razor.Test/Documents/EditorDocumentManagerListenerTest.cs @@ -0,0 +1,103 @@ +// 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 Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Razor; +using Microsoft.CodeAnalysis.Razor.ProjectSystem; +using Microsoft.CodeAnalysis.Text; +using Microsoft.VisualStudio.Test; +using Microsoft.VisualStudio.Text; +using Moq; +using Xunit; + +namespace Microsoft.VisualStudio.Editor.Razor.Documents +{ + public class EditorDocumentManagerListenerTest + { + public EditorDocumentManagerListenerTest() + { + ProjectFilePath = "C:\\project1\\project.csproj"; + DocumentFilePath = "c:\\project1\\file1.cshtml"; + TextLoader = TextLoader.From(TextAndVersion.Create(SourceText.From("FILE"), VersionStamp.Default)); + FileChangeTracker = new DefaultFileChangeTracker(DocumentFilePath); + + TextBuffer = new TestTextBuffer(new StringTextSnapshot("Hello")); + } + + private string ProjectFilePath { get; } + + private string DocumentFilePath { get; } + + private TextLoader TextLoader { get; } + + private FileChangeTracker FileChangeTracker { get; } + + private TestTextBuffer TextBuffer { get; } + + [Fact] + public void ProjectManager_Changed_DocumentAdded_InvokesGetOrCreateDocument() + { + // Arrange + var changedOnDisk = new EventHandler((o, args) => { }); + var changedInEditor = new EventHandler((o, args) => { }); + var opened = new EventHandler((o, args) => { }); + var closed = new EventHandler((o, args) => { }); + + var editorDocumentManger = new Mock(MockBehavior.Strict); + editorDocumentManger + .Setup(e => e.GetOrCreateDocument(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) + .Returns(GetEditorDocument()) + .Callback((key, onChangedOnDisk, onChangedInEditor, onOpened, onClosed) => + { + Assert.Same(changedOnDisk, onChangedOnDisk); + Assert.Same(changedInEditor, onChangedInEditor); + Assert.Same(opened, onOpened); + Assert.Same(closed, onClosed); + }); + + var listener = new EditorDocumentManagerListener(editorDocumentManger.Object, changedOnDisk, changedInEditor, opened, closed); + + // Act & Assert + listener.ProjectManager_Changed(null, new ProjectChangeEventArgs("/Path/to/project.csproj", ProjectChangeKind.DocumentAdded)); + } + + [Fact] + public void ProjectManager_Changed_OpenDocumentAdded_InvokesOnOpened() + { + // Arrange + var called = false; + var opened = new EventHandler((o, args) => { called = true; }); + + var editorDocumentManger = new Mock(MockBehavior.Strict); + editorDocumentManger + .Setup(e => e.GetOrCreateDocument(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) + .Returns(GetEditorDocument(isOpen: true)); + + var listener = new EditorDocumentManagerListener(editorDocumentManger.Object, onChangedOnDisk: null, onChangedInEditor: null, onOpened: opened, onClosed: null); + + // Act + listener.ProjectManager_Changed(null, new ProjectChangeEventArgs("/Path/to/project.csproj", ProjectChangeKind.DocumentAdded)); + + // Assert + Assert.True(called); + } + + private EditorDocument GetEditorDocument(bool isOpen = false) + { + var document = new EditorDocument( + Mock.Of(), + ProjectFilePath, + DocumentFilePath, + TextLoader, + FileChangeTracker, + isOpen ? TextBuffer : null, + changedOnDisk: null, + changedInEditor: null, + opened: null, + closed: null); + + return document; + } + } +} diff --git a/test/Microsoft.VisualStudio.Editor.Razor.Test/Documents/EditorDocumentTest.cs b/test/Microsoft.VisualStudio.Editor.Razor.Test/Documents/EditorDocumentTest.cs index c6b2d508b6..7173be6d15 100644 --- a/test/Microsoft.VisualStudio.Editor.Razor.Test/Documents/EditorDocumentTest.cs +++ b/test/Microsoft.VisualStudio.Editor.Razor.Test/Documents/EditorDocumentTest.cs @@ -81,32 +81,5 @@ namespace Microsoft.VisualStudio.Editor.Razor.Documents Assert.Null(document.EditorTextContainer); } } - - private class TestSourceTextContainer : SourceTextContainer - { - public override event EventHandler TextChanged; - - private SourceText _currentText; - - public TestSourceTextContainer() - : this(SourceText.From(string.Empty)) - { - } - - public TestSourceTextContainer(SourceText text) - { - _currentText = text; - } - - public override SourceText CurrentText => _currentText; - - public void PushChange(SourceText text) - { - var args = new TextChangeEventArgs(_currentText, text); - _currentText = text; - - TextChanged?.Invoke(this, args); - } - } } }