From 13e13d763285e49983d7a8e6413013370ad2749b Mon Sep 17 00:00:00 2001 From: Ryan Nowak Date: Sat, 19 May 2018 19:11:38 -0700 Subject: [PATCH] Add generated documents to workspace --- .../Experiment/IDocumentServiceFactory.cs | 12 ++ .../Experiment/ISpanMapper.cs | 12 ++ .../Experiment/SpanMapResult.cs | 19 +++ ...osoft.CodeAnalysis.Razor.Workspaces.csproj | 1 + .../ProjectSystem/DocumentState.cs | 2 + .../ProjectSystem/GeneratedCodeContainer.cs | 143 +++++++++++++++++ .../ProjectSystem/HostDocument.cs | 3 + .../SourceSpanExtensions.cs | 16 ++ ...Microsoft.NET.Sdk.Razor.DesignTime.targets | 3 + .../BackgroundDocumentGenerator.cs | 88 ++++++---- ...VisualStudio.LanguageServices.Razor.csproj | 1 + .../ProjectSystem/DefaultRazorProjectHost.cs | 126 ++++++++++++++- .../DefaultWorkspaceProjectContextFactory.cs | 102 ++++++++++++ .../ProjectSystem/FallbackRazorProjectHost.cs | 2 +- .../ProjectSystem/IWorkspaceProjectContext.cs | 47 ++++++ .../IWorkspaceProjectContextFactory.cs | 23 +++ .../ManagedProjectSystemSchema.cs | 14 ++ .../ProjectExternalErrorReporter.cs | 14 ++ .../ProjectSystem/RazorProjectHostBase.cs | 150 +++++++++++++++++- .../GeneratedCodeContainerTest.cs | 83 ++++++++++ 20 files changed, 829 insertions(+), 32 deletions(-) create mode 100644 src/Microsoft.CodeAnalysis.Razor.Workspaces/Experiment/IDocumentServiceFactory.cs create mode 100644 src/Microsoft.CodeAnalysis.Razor.Workspaces/Experiment/ISpanMapper.cs create mode 100644 src/Microsoft.CodeAnalysis.Razor.Workspaces/Experiment/SpanMapResult.cs create mode 100644 src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/GeneratedCodeContainer.cs create mode 100644 src/Microsoft.CodeAnalysis.Razor/SourceSpanExtensions.cs rename src/{Microsoft.CodeAnalysis.Razor.Workspaces/DocumentGenerator => Microsoft.VisualStudio.LanguageServices.Razor}/BackgroundDocumentGenerator.cs (76%) create mode 100644 src/Microsoft.VisualStudio.LanguageServices.Razor/ProjectSystem/DefaultWorkspaceProjectContextFactory.cs create mode 100644 src/Microsoft.VisualStudio.LanguageServices.Razor/ProjectSystem/IWorkspaceProjectContext.cs create mode 100644 src/Microsoft.VisualStudio.LanguageServices.Razor/ProjectSystem/IWorkspaceProjectContextFactory.cs create mode 100644 src/Microsoft.VisualStudio.LanguageServices.Razor/ProjectSystem/ProjectExternalErrorReporter.cs create mode 100644 test/Microsoft.CodeAnalysis.Razor.Workspaces.Test/ProjectSystem/GeneratedCodeContainerTest.cs diff --git a/src/Microsoft.CodeAnalysis.Razor.Workspaces/Experiment/IDocumentServiceFactory.cs b/src/Microsoft.CodeAnalysis.Razor.Workspaces/Experiment/IDocumentServiceFactory.cs new file mode 100644 index 0000000000..fd8253234c --- /dev/null +++ b/src/Microsoft.CodeAnalysis.Razor.Workspaces/Experiment/IDocumentServiceFactory.cs @@ -0,0 +1,12 @@ +// 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. + +// Temporary code until this gets merged into Roslyn +#if DOCUMENT_SERVICE_FACTORY +namespace Microsoft.CodeAnalysis.Experiment +{ + public interface IDocumentServiceFactory + { + } +} +#endif diff --git a/src/Microsoft.CodeAnalysis.Razor.Workspaces/Experiment/ISpanMapper.cs b/src/Microsoft.CodeAnalysis.Razor.Workspaces/Experiment/ISpanMapper.cs new file mode 100644 index 0000000000..6d3c276116 --- /dev/null +++ b/src/Microsoft.CodeAnalysis.Razor.Workspaces/Experiment/ISpanMapper.cs @@ -0,0 +1,12 @@ +// 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. + +// Temporary code until this gets merged into Roslyn +#if DOCUMENT_SERVICE_FACTORY +namespace Microsoft.CodeAnalysis.Experiment +{ + public interface ISpanMapper + { + } +} +#endif diff --git a/src/Microsoft.CodeAnalysis.Razor.Workspaces/Experiment/SpanMapResult.cs b/src/Microsoft.CodeAnalysis.Razor.Workspaces/Experiment/SpanMapResult.cs new file mode 100644 index 0000000000..531f6adead --- /dev/null +++ b/src/Microsoft.CodeAnalysis.Razor.Workspaces/Experiment/SpanMapResult.cs @@ -0,0 +1,19 @@ +// 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. + +// Temporary code until this gets merged into Roslyn +#if DOCUMENT_SERVICE_FACTORY +using Microsoft.CodeAnalysis.Text; + +namespace Microsoft.CodeAnalysis.Experiment +{ + public class SpanMapResult + { + public SpanMapResult(Document document, LinePositionSpan linePositionSpan) + { + + } + + } +} +#endif diff --git a/src/Microsoft.CodeAnalysis.Razor.Workspaces/Microsoft.CodeAnalysis.Razor.Workspaces.csproj b/src/Microsoft.CodeAnalysis.Razor.Workspaces/Microsoft.CodeAnalysis.Razor.Workspaces.csproj index e7f29a2fb0..03e0c91ef3 100644 --- a/src/Microsoft.CodeAnalysis.Razor.Workspaces/Microsoft.CodeAnalysis.Razor.Workspaces.csproj +++ b/src/Microsoft.CodeAnalysis.Razor.Workspaces/Microsoft.CodeAnalysis.Razor.Workspaces.csproj @@ -4,6 +4,7 @@ Razor is a markup syntax for adding server-side logic to web pages. This package contains the Razor design-time infrastructure. net46;netstandard2.0 false + $(DefineConstants);DOCUMENT_SERVICE_FACTORY diff --git a/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/DocumentState.cs b/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/DocumentState.cs index d1fb05b67a..681ce5fd9a 100644 --- a/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/DocumentState.cs +++ b/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/DocumentState.cs @@ -65,6 +65,8 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem public HostWorkspaceServices Services { get; } + public GeneratedCodeContainer GeneratedCodeContainer => HostDocument.GeneratedCodeContainer; + public DocumentGeneratedOutputTracker GeneratedOutput { get diff --git a/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/GeneratedCodeContainer.cs b/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/GeneratedCodeContainer.cs new file mode 100644 index 0000000000..342da76b09 --- /dev/null +++ b/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/GeneratedCodeContainer.cs @@ -0,0 +1,143 @@ +// 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.Text; +using Microsoft.CodeAnalysis.Experiment; +using Microsoft.AspNetCore.Razor.Language; +using System.Collections.Immutable; +using System.Threading.Tasks; +using System.Threading; +using System.Collections.Generic; + +namespace Microsoft.CodeAnalysis.Razor.ProjectSystem +{ + internal class GeneratedCodeContainer : IDocumentServiceFactory, ISpanMapper + { + private readonly TextContainer _textContainer; + + public GeneratedCodeContainer() + { + _textContainer = new TextContainer(); + } + + public SourceText Source { get; private set; } + + public VersionStamp SourceVersion { get; private set; } + + public RazorCSharpDocument Output { get; private set; } + + public SourceTextContainer SourceTextContainer => _textContainer; + + public TService GetService() + { + if (this is TService service) + { + return service; + } + + return default(TService); + } + + public void SetOutput(SourceText source, RazorCodeDocument codeDocument) + { + Source = source; + Output = codeDocument.GetCSharpDocument(); + + _textContainer.SetText(SourceText.From(Output.GeneratedCode)); + } + + public Task> MapSpansAsync( + Document document, + IEnumerable spans, + CancellationToken cancellationToken) + { + if (Output == null) + { + return Task.FromResult(ImmutableArray.Empty); + } + + var results = ImmutableArray.CreateBuilder(); + foreach (var span in spans) + { + if (TryGetLinePositionSpan(span, out var linePositionSpan)) + { + results.Add(new SpanMapResult(document, linePositionSpan)); + } + } + + return Task.FromResult(results.ToImmutable()); + } + + // Internal for testing. + internal bool TryGetLinePositionSpan(TextSpan span, out LinePositionSpan linePositionSpan) + { + for (var i = 0; i < Output.SourceMappings.Count; i++) + { + var mapping = Output.SourceMappings[i]; + if (span.Length > mapping.GeneratedSpan.Length) + { + // If the length of the generated span is smaller they can't match. A C# expression + // won't cover multiple generated spans. + // + // This heuristic is useful in the Razor context to filter out zero-length + // spans. + continue; + } + + var original = mapping.OriginalSpan.AsTextSpan(); + var generated = mapping.GeneratedSpan.AsTextSpan(); + + var leftOffset = span.Start - generated.Start; + var rightOffset = span.End - generated.End; + if (leftOffset >= 0 && rightOffset <= 0) + { + // This span mapping contains the span. + var adjusted = new TextSpan(original.Start + leftOffset, (original.End + rightOffset) - (original.Start + leftOffset)); + linePositionSpan = Source.Lines.GetLinePositionSpan(adjusted); + return true; + } + } + + linePositionSpan = default; + return false; + } + + private class TextContainer : SourceTextContainer + { + public override event EventHandler TextChanged; + + private SourceText _currentText; + + public TextContainer() + : this(SourceText.From(string.Empty)) + { + } + + public TextContainer(SourceText sourceText) + { + if (sourceText == null) + { + throw new ArgumentNullException(nameof(sourceText)); + } + + _currentText = sourceText; + } + + public override SourceText CurrentText => _currentText; + + public void SetText(SourceText sourceText) + { + if (sourceText == null) + { + throw new ArgumentNullException(nameof(sourceText)); + } + + var e = new TextChangeEventArgs(_currentText, sourceText); + _currentText = sourceText; + + TextChanged?.Invoke(this, e); + } + } + } +} diff --git a/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/HostDocument.cs b/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/HostDocument.cs index 859cc0df32..77d62eb854 100644 --- a/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/HostDocument.cs +++ b/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/HostDocument.cs @@ -21,10 +21,13 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem FilePath = filePath; TargetPath = targetPath; + GeneratedCodeContainer = new GeneratedCodeContainer(); } public string FilePath { get; } public string TargetPath { get; } + + public GeneratedCodeContainer GeneratedCodeContainer { get; } } } diff --git a/src/Microsoft.CodeAnalysis.Razor/SourceSpanExtensions.cs b/src/Microsoft.CodeAnalysis.Razor/SourceSpanExtensions.cs new file mode 100644 index 0000000000..51fd517024 --- /dev/null +++ b/src/Microsoft.CodeAnalysis.Razor/SourceSpanExtensions.cs @@ -0,0 +1,16 @@ +// 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 Microsoft.AspNetCore.Razor.Language; +using Microsoft.CodeAnalysis.Text; + +namespace Microsoft.CodeAnalysis.Razor +{ + internal static class SourceSpanExtensions + { + public static TextSpan AsTextSpan(this SourceSpan sourceSpan) + { + return new TextSpan(sourceSpan.AbsoluteIndex, sourceSpan.Length); + } + } +} diff --git a/src/Microsoft.NET.Sdk.Razor/build/netstandard2.0/Microsoft.NET.Sdk.Razor.DesignTime.targets b/src/Microsoft.NET.Sdk.Razor/build/netstandard2.0/Microsoft.NET.Sdk.Razor.DesignTime.targets index 64c5d8f61a..9aaafbd17c 100644 --- a/src/Microsoft.NET.Sdk.Razor/build/netstandard2.0/Microsoft.NET.Sdk.Razor.DesignTime.targets +++ b/src/Microsoft.NET.Sdk.Razor/build/netstandard2.0/Microsoft.NET.Sdk.Razor.DesignTime.targets @@ -43,6 +43,9 @@ Copyright (c) .NET Foundation. All rights reserved. Project + + Project + diff --git a/src/Microsoft.CodeAnalysis.Razor.Workspaces/DocumentGenerator/BackgroundDocumentGenerator.cs b/src/Microsoft.VisualStudio.LanguageServices.Razor/BackgroundDocumentGenerator.cs similarity index 76% rename from src/Microsoft.CodeAnalysis.Razor.Workspaces/DocumentGenerator/BackgroundDocumentGenerator.cs rename to src/Microsoft.VisualStudio.LanguageServices.Razor/BackgroundDocumentGenerator.cs index 122e577abe..b0a99e8e24 100644 --- a/src/Microsoft.CodeAnalysis.Razor.Workspaces/DocumentGenerator/BackgroundDocumentGenerator.cs +++ b/src/Microsoft.VisualStudio.LanguageServices.Razor/BackgroundDocumentGenerator.cs @@ -11,14 +11,13 @@ using Microsoft.CodeAnalysis.Razor.ProjectSystem; namespace Microsoft.CodeAnalysis.Razor { - // Deliberately not exported for now, until this feature is working end to end. - // [Export(typeof(ProjectSnapshotChangeTrigger))] + [Export(typeof(ProjectSnapshotChangeTrigger))] internal class BackgroundDocumentGenerator : ProjectSnapshotChangeTrigger { - private ForegroundDispatcher _foregroundDispatcher; + private readonly ForegroundDispatcher _foregroundDispatcher; private ProjectSnapshotManagerBase _projectManager; - private readonly Dictionary _files; + private readonly Dictionary _work; private Timer _timer; [ImportingConstructor] @@ -30,17 +29,16 @@ namespace Microsoft.CodeAnalysis.Razor } _foregroundDispatcher = foregroundDispatcher; - - _files = new Dictionary(); + _work = new Dictionary(); } public bool HasPendingNotifications { get { - lock (_files) + lock (_work) { - return _files.Count > 0; + return _work.Count > 0; } } } @@ -134,11 +132,11 @@ namespace Microsoft.CodeAnalysis.Razor _foregroundDispatcher.AssertForegroundThread(); - lock (_files) + lock (_work) { // We only want to store the last 'seen' version of any given document. That way when we pick one to process // it's always the best version to use. - _files[new DocumentKey(project.FilePath, document.FilePath)] = document; + _work[new DocumentKey(project.FilePath, document.FilePath)] = document; StartWorker(); } @@ -165,18 +163,18 @@ namespace Microsoft.CodeAnalysis.Razor OnStartingBackgroundWork(); - DocumentSnapshot[] work; - lock (_files) + KeyValuePair[] work; + lock (_work) { - work = _files.Values.ToArray(); - _files.Clear(); + work = _work.ToArray(); + _work.Clear(); } OnBackgroundCapturedWorkload(); for (var i = 0; i < work.Length; i++) { - var document = work[i]; + var document = work[i].Value; try { await ProcessDocument(document); @@ -189,14 +187,20 @@ namespace Microsoft.CodeAnalysis.Razor OnCompletingBackgroundWork(); - lock (_files) + await Task.Factory.StartNew( + () => ReportUpdates(work), + CancellationToken.None, + TaskCreationOptions.None, + _foregroundDispatcher.ForegroundScheduler); + + lock (_work) { // Resetting the timer allows another batch of work to start. _timer.Dispose(); _timer = null; // If more work came in while we were running start the worker again. - if (_files.Count > 0) + if (_work.Count > 0) { StartWorker(); } @@ -215,6 +219,22 @@ namespace Microsoft.CodeAnalysis.Razor } } + private void ReportUpdates(KeyValuePair[] work) + { + for (var i = 0; i < work.Length; i++) + { + var key = work[i].Key; + var document = work[i].Value; + + if (document.TryGetText(out var source) && + document.TryGetGeneratedOutput(out var output)) + { + var container = ((DefaultDocumentSnapshot)document).State.GeneratedCodeContainer; + container.SetOutput(source, output); + } + } + } + private void ReportError(DocumentSnapshot document, Exception ex) { GC.KeepAlive(Task.Factory.StartNew( @@ -229,22 +249,34 @@ namespace Microsoft.CodeAnalysis.Razor switch (e.Kind) { case ProjectChangeKind.ProjectAdded: + { + var projectSnapshot = _projectManager.GetLoadedProject(e.ProjectFilePath); + foreach (var documentFilePath in projectSnapshot.DocumentFilePaths) + { + Enqueue(projectSnapshot, projectSnapshot.GetDocument(documentFilePath)); + } + + break; + } case ProjectChangeKind.ProjectChanged: { - var project = _projectManager.GetLoadedProject(e.ProjectFilePath); - foreach (var documentFilePath in project.DocumentFilePaths) + var projectSnapshot = _projectManager.GetLoadedProject(e.ProjectFilePath); + foreach (var documentFilePath in projectSnapshot.DocumentFilePaths) { - Enqueue(project, project.GetDocument(documentFilePath)); + Enqueue(projectSnapshot, projectSnapshot.GetDocument(documentFilePath)); } break; } - case ProjectChangeKind.ProjectRemoved: - // ignore - break; - case ProjectChangeKind.DocumentAdded: + { + var project = _projectManager.GetLoadedProject(e.ProjectFilePath); + Enqueue(project, project.GetDocument(e.DocumentFilePath)); + + break; + } + case ProjectChangeKind.DocumentChanged: { var project = _projectManager.GetLoadedProject(e.ProjectFilePath); @@ -253,10 +285,12 @@ namespace Microsoft.CodeAnalysis.Razor break; } - + case ProjectChangeKind.ProjectRemoved: case ProjectChangeKind.DocumentRemoved: - // ignore - break; + { + // ignore + break; + } default: throw new InvalidOperationException($"Unknown ProjectChangeKind {e.Kind}"); diff --git a/src/Microsoft.VisualStudio.LanguageServices.Razor/Microsoft.VisualStudio.LanguageServices.Razor.csproj b/src/Microsoft.VisualStudio.LanguageServices.Razor/Microsoft.VisualStudio.LanguageServices.Razor.csproj index 32296b51c6..64d66873f8 100644 --- a/src/Microsoft.VisualStudio.LanguageServices.Razor/Microsoft.VisualStudio.LanguageServices.Razor.csproj +++ b/src/Microsoft.VisualStudio.LanguageServices.Razor/Microsoft.VisualStudio.LanguageServices.Razor.csproj @@ -5,6 +5,7 @@ Razor is a markup syntax for adding server-side logic to web pages. This package contains the Razor design-time infrastructure for Visual Studio. false ..\Microsoft.NET.Sdk.Razor\build\netstandard2.0\Rules\ + $(DefineConstants);WORKSPACE_PROJECT_CONTEXT_FACTORY diff --git a/src/Microsoft.VisualStudio.LanguageServices.Razor/ProjectSystem/DefaultRazorProjectHost.cs b/src/Microsoft.VisualStudio.LanguageServices.Razor/ProjectSystem/DefaultRazorProjectHost.cs index 68dfc4312a..97978c18cd 100644 --- a/src/Microsoft.VisualStudio.LanguageServices.Razor/ProjectSystem/DefaultRazorProjectHost.cs +++ b/src/Microsoft.VisualStudio.LanguageServices.Razor/ProjectSystem/DefaultRazorProjectHost.cs @@ -7,15 +7,22 @@ using System.Collections.Immutable; using System.ComponentModel.Composition; using System.IO; using System.Linq; +using System.Runtime.InteropServices; using System.Threading; using System.Threading.Tasks; using System.Threading.Tasks.Dataflow; using Microsoft.AspNetCore.Razor.Language; +using Microsoft.VisualStudio; using Microsoft.VisualStudio.LanguageServices; using Microsoft.VisualStudio.ProjectSystem; using Microsoft.VisualStudio.ProjectSystem.Properties; +using Microsoft.VisualStudio.TextManager.Interop; using Item = System.Collections.Generic.KeyValuePair>; +#if WORKSPACE_PROJECT_CONTEXT_FACTORY +using IWorkspaceProjectContextFactory = Microsoft.VisualStudio.LanguageServices.ProjectSystem.IWorkspaceProjectContextFactory2; +#endif + namespace Microsoft.CodeAnalysis.Razor.ProjectSystem { // Somewhat similar to https://github.com/dotnet/project-system/blob/fa074d228dcff6dae9e48ce43dd4a3a5aa22e8f0/src/Microsoft.VisualStudio.ProjectSystem.Managed/ProjectSystem/LanguageServices/LanguageServiceHost.cs @@ -23,16 +30,18 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem // This class is responsible for intializing the Razor ProjectSnapshotManager for cases where // MSBuild provides configuration support (>= 2.1). [AppliesTo("DotNetCoreRazor & DotNetCoreRazorConfiguration")] + [ExportVsProfferedProjectService(typeof(IVsContainedLanguageProjectNameProvider))] [Export(ExportContractNames.Scopes.UnconfiguredProject, typeof(IProjectDynamicLoadComponent))] - internal class DefaultRazorProjectHost : RazorProjectHostBase + internal class DefaultRazorProjectHost : RazorProjectHostBase, IVsContainedLanguageProjectNameProvider { private IDisposable _subscription; [ImportingConstructor] public DefaultRazorProjectHost( IUnconfiguredProjectCommonServices commonServices, - [Import(typeof(VisualStudioWorkspace))] Workspace workspace) - : base(commonServices, workspace) + [Import(typeof(VisualStudioWorkspace))] Workspace workspace, + Lazy projectContextFactory) + : base(commonServices, workspace, projectContextFactory) { } @@ -68,6 +77,9 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem Rules.RazorConfiguration.SchemaName, Rules.RazorExtension.SchemaName, Rules.RazorGenerateWithTargetPath.SchemaName, + ManagedProjectSystemSchema.CompilerCommandLineArgs.SchemaName, + ManagedProjectSystemSchema.ConfigurationGeneral.SchemaName, + ManagedProjectSystemSchema.ResolvedCompilationReference.SchemaName, }); } @@ -107,9 +119,14 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem var documents = GetCurrentDocuments(update.Value); var changedDocuments = GetChangedAndRemovedDocuments(update.Value); + var references = GetReferences(update.Value); + TryGetCommandLineOptions(update.Value.CurrentState, out var commandLineOptions); + await UpdateAsync(() => { UpdateProjectUnsafe(hostProject); + UpdateWorkspaceProjectOptionsUnsafe(commandLineOptions); + UpdateWorkspaceProjectReferencesUnsafe(references); for (var i = 0; i < changedDocuments.Length; i++) { @@ -302,6 +319,77 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem return true; } + + // This is temporary code for initializing the companion project. We expect + // this to be provided by the Managed Project System in the near future. + internal static bool TryGetReferences( + IImmutableDictionary state, + out string[] references) + { + if (!state.TryGetValue(ManagedProjectSystemSchema.ResolvedCompilationReference.ItemName, out var rule)) + { + references = null; + return false; + } + + var items = rule.Items; + var referencesList = new List(); + foreach (var item in items) + { + var reference = item.Key; + if (!referencesList.Contains(reference, FilePathComparer.Instance)) + { + referencesList.Add(reference); + } + } + + references = referencesList.ToArray(); + return true; + } + + // This is temporary code for initializing the companion project. We expect + // this to be provided by the Managed Project System in the near future. + internal static bool TryGetCommandLineOptions( + IImmutableDictionary state, + out string commandLineOptions) + { + if (!state.TryGetValue(ManagedProjectSystemSchema.CompilerCommandLineArgs.ItemName, out var rule)) + { + commandLineOptions = null; + return false; + } + + commandLineOptions = string.Join(" ", rule.Items.Select(kvp => kvp.Key)); + return true; + } + + // This is temporary code for initializing the companion project. We expect + // this to be provided by the Managed Project System in the near future. + internal static bool TryGetTargetPath( + IImmutableDictionary state, + out string targetPath) + { + if (!state.TryGetValue(ManagedProjectSystemSchema.ConfigurationGeneral.SchemaName, out var rule)) + { + targetPath = null; + return false; + } + + if (!rule.Properties.TryGetValue(ManagedProjectSystemSchema.ConfigurationGeneral.TargetPathPropertyName, out targetPath)) + { + targetPath = null; + return false; + } + + if (string.IsNullOrEmpty(targetPath)) + { + targetPath = null; + return false; + } + + return true; + } + private HostDocument[] GetCurrentDocuments(IProjectSubscriptionUpdate update) { if (!update.CurrentState.TryGetValue(Rules.RazorGenerateWithTargetPath.SchemaName, out var rule)) @@ -348,5 +436,37 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem return documents.ToArray(); } + + // This is temporary code for initializing the companion project. We expect + // this to be provided by the Managed Project System in the near future. + private string[] GetReferences(IProjectSubscriptionUpdate update) + { + if (!TryGetReferences(update.CurrentState, out var references)) + { + return Array.Empty(); + } + + if (TryGetTargetPath(update.CurrentState, out var targetPath)) + { + references = references.Concat(new[] { targetPath, }).ToArray(); + } + + return references; + } + + // This is temporary code for initializing the companion project. We expect + // this to be provided by the Managed Project System in the near future. + public int GetProjectName([In] uint itemid, [MarshalAs(UnmanagedType.BStr)] out string pbstrProjectName) + { + if (Current == null) + { + pbstrProjectName = null; + + return VSConstants.E_INVALIDARG; + } + + pbstrProjectName = Path.GetFileNameWithoutExtension(Current.FilePath) + " (Razor)"; + return VSConstants.S_OK; + } } } \ No newline at end of file diff --git a/src/Microsoft.VisualStudio.LanguageServices.Razor/ProjectSystem/DefaultWorkspaceProjectContextFactory.cs b/src/Microsoft.VisualStudio.LanguageServices.Razor/ProjectSystem/DefaultWorkspaceProjectContextFactory.cs new file mode 100644 index 0000000000..b562bd74c1 --- /dev/null +++ b/src/Microsoft.VisualStudio.LanguageServices.Razor/ProjectSystem/DefaultWorkspaceProjectContextFactory.cs @@ -0,0 +1,102 @@ +// 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. + +// Temporary code until we get access to these APIs +#if WORKSPACE_PROJECT_CONTEXT_FACTORY + +using System; +using System.Collections.Generic; +using System.ComponentModel.Composition; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Experiment; +using Microsoft.CodeAnalysis.Text; +using Microsoft.VisualStudio.LanguageServices.Implementation.TaskList; +using IWorkspaceProjectContextFactory = Microsoft.VisualStudio.LanguageServices.ProjectSystem.IWorkspaceProjectContextFactory2; + +namespace Microsoft.VisualStudio.LanguageServices.ProjectSystem +{ + [Export(typeof(IWorkspaceProjectContextFactory))] + internal class DefaultWorkspaceProjectContextFactory : IWorkspaceProjectContextFactory + { + public IWorkspaceProjectContext CreateProjectContext(string languageName, string projectDisplayName, string projectFilePath, Guid projectGuid, object hierarchy, string binOutputPath) + { + return new WorkspaceProjectContext(); + } + + public IWorkspaceProjectContext CreateProjectContext(string languageName, string projectDisplayName, string projectFilePath, Guid projectGuid, object hierarchy, string binOutputPath, ProjectExternalErrorReporter errorReporter) + { + return new WorkspaceProjectContext(); + } + + private class WorkspaceProjectContext : IWorkspaceProjectContext + { + public string DisplayName { get; set; } + public string ProjectFilePath { get; set; } + public Guid Guid { get; set; } + public bool LastDesignTimeBuildSucceeded { get; set; } + public string BinOutputPath { get; set; } + + public void AddAdditionalFile(string filePath, bool isInCurrentContext = true) + { + } + + public void AddAnalyzerReference(string referencePath) + { + } + + public void AddMetadataReference(string referencePath, MetadataReferenceProperties properties) + { + } + + public void AddProjectReference(IWorkspaceProjectContext project, MetadataReferenceProperties properties) + { + } + + public void AddSourceFile(string filePath, bool isInCurrentContext, IEnumerable folderNames, SourceCodeKind sourceCodeKind) + { + } + + public void AddSourceFile(string filePath, bool isInCurrentContext = true, IEnumerable folderNames = null, SourceCodeKind sourceCodeKind = SourceCodeKind.Regular, IDocumentServiceFactory documentServiceFactory = null) + { + } + + public void AddSourceFile(string filePath, SourceTextContainer container, bool isInCurrentContext = true, IEnumerable folderNames = null, SourceCodeKind sourceCodeKind = SourceCodeKind.Regular, IDocumentServiceFactory documentServiceFactory = null) + { + } + + public void Dispose() + { + } + + public void RemoveAdditionalFile(string filePath) + { + } + + public void RemoveAnalyzerReference(string referencePath) + { + } + + public void RemoveMetadataReference(string referencePath) + { + } + + public void RemoveProjectReference(IWorkspaceProjectContext project) + { + } + + public void RemoveSourceFile(string filePath) + { + } + + public void SetOptions(string commandLineForOptions) + { + } + + public void SetRuleSetFile(string filePath) + { + } + } + } +} + +#endif diff --git a/src/Microsoft.VisualStudio.LanguageServices.Razor/ProjectSystem/FallbackRazorProjectHost.cs b/src/Microsoft.VisualStudio.LanguageServices.Razor/ProjectSystem/FallbackRazorProjectHost.cs index 5dedf67af9..ef33b79723 100644 --- a/src/Microsoft.VisualStudio.LanguageServices.Razor/ProjectSystem/FallbackRazorProjectHost.cs +++ b/src/Microsoft.VisualStudio.LanguageServices.Razor/ProjectSystem/FallbackRazorProjectHost.cs @@ -38,7 +38,7 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem public FallbackRazorProjectHost( IUnconfiguredProjectCommonServices commonServices, [Import(typeof(VisualStudioWorkspace))] Workspace workspace) - : base(commonServices, workspace) + : base(commonServices, workspace, projectContextFactory: null) { } diff --git a/src/Microsoft.VisualStudio.LanguageServices.Razor/ProjectSystem/IWorkspaceProjectContext.cs b/src/Microsoft.VisualStudio.LanguageServices.Razor/ProjectSystem/IWorkspaceProjectContext.cs new file mode 100644 index 0000000000..8b34558d85 --- /dev/null +++ b/src/Microsoft.VisualStudio.LanguageServices.Razor/ProjectSystem/IWorkspaceProjectContext.cs @@ -0,0 +1,47 @@ +// 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. + +// Temporary code until we get access to these APIs +#if WORKSPACE_PROJECT_CONTEXT_FACTORY + +using System; +using System.Collections.Generic; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Experiment; +using Microsoft.CodeAnalysis.Text; + +namespace Microsoft.VisualStudio.LanguageServices.ProjectSystem +{ + internal interface IWorkspaceProjectContext : IDisposable + { + // Project properties. + string DisplayName { get; set; } + string ProjectFilePath { get; set; } + Guid Guid { get; set; } + bool LastDesignTimeBuildSucceeded { get; set; } + string BinOutputPath { get; set; } + + // Options. + void SetOptions(string commandLineForOptions); + + // References. + void AddMetadataReference(string referencePath, MetadataReferenceProperties properties); + void RemoveMetadataReference(string referencePath); + void AddProjectReference(IWorkspaceProjectContext project, MetadataReferenceProperties properties); + void RemoveProjectReference(IWorkspaceProjectContext project); + void AddAnalyzerReference(string referencePath); + void RemoveAnalyzerReference(string referencePath); + + // Files. + void AddSourceFile(string filePath, bool isInCurrentContext, IEnumerable folderNames, SourceCodeKind sourceCodeKind); // This overload just for binary compat with existing code + void AddSourceFile(string filePath, bool isInCurrentContext = true, IEnumerable folderNames = null, SourceCodeKind sourceCodeKind = SourceCodeKind.Regular, IDocumentServiceFactory documentServiceFactory = null); + void AddSourceFile(string filePath, SourceTextContainer container, bool isInCurrentContext = true, IEnumerable folderNames = null, SourceCodeKind sourceCodeKind = SourceCodeKind.Regular, IDocumentServiceFactory documentServiceFactory = null); + + void RemoveSourceFile(string filePath); + void AddAdditionalFile(string filePath, bool isInCurrentContext = true); + void RemoveAdditionalFile(string filePath); + void SetRuleSetFile(string filePath); + } +} + +#endif diff --git a/src/Microsoft.VisualStudio.LanguageServices.Razor/ProjectSystem/IWorkspaceProjectContextFactory.cs b/src/Microsoft.VisualStudio.LanguageServices.Razor/ProjectSystem/IWorkspaceProjectContextFactory.cs new file mode 100644 index 0000000000..0cb912b627 --- /dev/null +++ b/src/Microsoft.VisualStudio.LanguageServices.Razor/ProjectSystem/IWorkspaceProjectContextFactory.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. + +// Temporary code until we get access to these APIs +#if WORKSPACE_PROJECT_CONTEXT_FACTORY + +using System; +using Microsoft.VisualStudio.LanguageServices.Implementation.TaskList; + +namespace Microsoft.VisualStudio.LanguageServices.ProjectSystem +{ + // The 2 is needed here to prevent clashes with the real interface. MEF is based on the FullName + // as a string. + internal interface IWorkspaceProjectContextFactory2 + { + IWorkspaceProjectContext CreateProjectContext(string languageName, string projectDisplayName, string projectFilePath, Guid projectGuid, object hierarchy, string binOutputPath); + + + IWorkspaceProjectContext CreateProjectContext(string languageName, string projectDisplayName, string projectFilePath, Guid projectGuid, object hierarchy, string binOutputPath, ProjectExternalErrorReporter errorReporter); + } +} + +#endif \ No newline at end of file diff --git a/src/Microsoft.VisualStudio.LanguageServices.Razor/ProjectSystem/ManagedProjectSystemSchema.cs b/src/Microsoft.VisualStudio.LanguageServices.Razor/ProjectSystem/ManagedProjectSystemSchema.cs index a56464e495..9f67217eb6 100644 --- a/src/Microsoft.VisualStudio.LanguageServices.Razor/ProjectSystem/ManagedProjectSystemSchema.cs +++ b/src/Microsoft.VisualStudio.LanguageServices.Razor/ProjectSystem/ManagedProjectSystemSchema.cs @@ -6,6 +6,20 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem // Well-Known Schema and property names defined by the ManagedProjectSystem internal static class ManagedProjectSystemSchema { + public static class CompilerCommandLineArgs + { + public static readonly string SchemaName = "CompilerCommandLineArgs"; + + public static readonly string ItemName = "CompilerCommandLineArgs"; + } + + public static class ConfigurationGeneral + { + public static readonly string SchemaName = "ConfigurationGeneral"; + + public static readonly string TargetPathPropertyName = "TargetPath"; + } + public static class ResolvedCompilationReference { public static readonly string SchemaName = "ResolvedCompilationReference"; diff --git a/src/Microsoft.VisualStudio.LanguageServices.Razor/ProjectSystem/ProjectExternalErrorReporter.cs b/src/Microsoft.VisualStudio.LanguageServices.Razor/ProjectSystem/ProjectExternalErrorReporter.cs new file mode 100644 index 0000000000..d9c230f65d --- /dev/null +++ b/src/Microsoft.VisualStudio.LanguageServices.Razor/ProjectSystem/ProjectExternalErrorReporter.cs @@ -0,0 +1,14 @@ +// 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. + +// Temporary code until we get access to these APIs +#if WORKSPACE_PROJECT_CONTEXT_FACTORY + +namespace Microsoft.VisualStudio.LanguageServices.Implementation.TaskList +{ + internal class ProjectExternalErrorReporter + { + } +} + +#endif \ No newline at end of file diff --git a/src/Microsoft.VisualStudio.LanguageServices.Razor/ProjectSystem/RazorProjectHostBase.cs b/src/Microsoft.VisualStudio.LanguageServices.Razor/ProjectSystem/RazorProjectHostBase.cs index e39a866f05..930262b57a 100644 --- a/src/Microsoft.VisualStudio.LanguageServices.Razor/ProjectSystem/RazorProjectHostBase.cs +++ b/src/Microsoft.VisualStudio.LanguageServices.Razor/ProjectSystem/RazorProjectHostBase.cs @@ -5,27 +5,40 @@ using System; using System.Collections.Generic; using System.ComponentModel.Composition; using System.Diagnostics; +using System.IO; using System.Linq; +using System.Reflection; using System.Threading; using System.Threading.Tasks; +using Microsoft.VisualStudio.Composition; using Microsoft.VisualStudio.LanguageServices; +using Microsoft.VisualStudio.LanguageServices.ProjectSystem; using Microsoft.VisualStudio.ProjectSystem; using Microsoft.VisualStudio.Threading; +#if WORKSPACE_PROJECT_CONTEXT_FACTORY +using IWorkspaceProjectContextFactory = Microsoft.VisualStudio.LanguageServices.ProjectSystem.IWorkspaceProjectContextFactory2; +#endif + namespace Microsoft.CodeAnalysis.Razor.ProjectSystem { internal abstract class RazorProjectHostBase : OnceInitializedOnceDisposedAsync, IProjectDynamicLoadComponent { private readonly Workspace _workspace; + private readonly Lazy _projectContextFactory; private readonly AsyncSemaphore _lock; private ProjectSnapshotManagerBase _projectManager; private HostProject _current; + private IWorkspaceProjectContext _projectContext; private Dictionary _currentDocuments; + private HashSet _references; + private string _commandLineOptions; public RazorProjectHostBase( IUnconfiguredProjectCommonServices commonServices, - [Import(typeof(VisualStudioWorkspace))] Workspace workspace) + [Import(typeof(VisualStudioWorkspace))] Workspace workspace, + Lazy projectContextFactory) : base(commonServices.ThreadingService.JoinableTaskContext) { if (commonServices == null) @@ -40,9 +53,11 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem CommonServices = commonServices; _workspace = workspace; + _projectContextFactory = projectContextFactory; _lock = new AsyncSemaphore(initialCount: 1); _currentDocuments = new Dictionary(FilePathComparer.Instance); + _references = new HashSet(FilePathComparer.Instance); } // Internal for testing @@ -73,6 +88,7 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem _lock = new AsyncSemaphore(initialCount: 1); _currentDocuments = new Dictionary(FilePathComparer.Instance); + _references = new HashSet(FilePathComparer.Instance); } protected HostProject Current => _current; @@ -129,6 +145,8 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem { var filePath = CommonServices.UnconfiguredProject.FullPath; UpdateProjectUnsafe(new HostProject(filePath, old.Configuration)); + UpdateWorkspaceProjectOptionsUnsafe(_commandLineOptions); + UpdateWorkspaceProjectReferencesUnsafe(_references.ToArray()); // This should no-op in the common case, just putting it here for insurance. for (var i = 0; i < oldDocuments.Length; i++) @@ -153,6 +171,12 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem return _projectManager; } + private IWorkspaceProjectContextFactory GetProjectContextFactory() + { + CommonServices.ThreadingService.VerifyOnUIThread(); + return _projectContextFactory?.Value; + } + protected async Task UpdateAsync(Action action) { await CommonServices.ThreadingService.SwitchToUIThread(); @@ -174,12 +198,40 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem } else if (_current == null && project != null) { + // This is temporary code for initializing the companion project. We expect + // this to be provided by the Managed Project System in the near future. + var projectContextFactory = GetProjectContextFactory(); + if (projectContextFactory != null) + { + var assembly = Assembly.Load("Microsoft.VisualStudio.ProjectSystem.Managed, Version=2.7.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35"); + var type = assembly.GetType("Microsoft.VisualStudio.ProjectSystem.LanguageServices.IProjectHostProvider"); + + var exportProviderType = CommonServices.UnconfiguredProject.Services.ExportProvider.GetType(); + var method = exportProviderType.GetMethod(nameof(ExportProvider.GetExportedValue), Array.Empty()).MakeGenericMethod(type); + var export = method.Invoke(CommonServices.UnconfiguredProject.Services.ExportProvider, Array.Empty()); + var host = new IProjectHostProvider(export); + + var displayName = Path.GetFileNameWithoutExtension(CommonServices.UnconfiguredProject.FullPath) + " (Razor)"; + _projectContext = projectContextFactory.CreateProjectContext( + LanguageNames.CSharp, + displayName, + CommonServices.UnconfiguredProject.FullPath, + Guid.NewGuid(), + host.UnconfiguredProjectHostObject.ActiveIntellisenseProjectHostObject, + null, + null); + } + + // END temporary code + projectManager.HostProjectAdded(project); } else if (_current != null && project == null) { Debug.Assert(_currentDocuments.Count == 0); projectManager.HostProjectRemoved(_current); + _projectContext?.Dispose(); + _projectContext = null; } else { @@ -189,6 +241,56 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem _current = project; } + protected void UpdateWorkspaceProjectOptionsUnsafe(string commandLineOptions) + { + if (_projectContext == null) + { + _commandLineOptions = null; + return; + } + + if (!string.Equals(_commandLineOptions, commandLineOptions)) + { + _projectContext.SetOptions(commandLineOptions); + _commandLineOptions = commandLineOptions; + } + } + + protected void UpdateWorkspaceProjectReferencesUnsafe(string[] references) + { + if (_projectContext == null) + { + _references.Clear(); + return; + } + + var newer = new HashSet(references, FilePathComparer.Instance); + var older = new HashSet(_references, FilePathComparer.Instance); + + if (older.SetEquals(newer)) + { + return; + } + + var remove = new HashSet(older, FilePathComparer.Instance); + remove.ExceptWith(newer); + + var add = new HashSet(newer, FilePathComparer.Instance); + add.ExceptWith(older); + + foreach (var reference in remove) + { + _references.Remove(reference); + _projectContext.RemoveMetadataReference(reference); + } + + foreach (var reference in add) + { + _references.Add(reference); + _projectContext.AddMetadataReference(reference, new MetadataReferenceProperties()); + } + } + protected void AddDocumentUnsafe(HostDocument document) { var projectManager = GetProjectManager(); @@ -200,6 +302,7 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem } projectManager.DocumentAdded(_current, document, new FileTextLoader(document.FilePath, null)); + _projectContext?.AddSourceFile(document.FilePath, document.GeneratedCodeContainer.SourceTextContainer, true, GetFolders(document), SourceCodeKind.Regular, document.GeneratedCodeContainer); _currentDocuments.Add(document.FilePath, document); } @@ -207,6 +310,7 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem { var projectManager = GetProjectManager(); + _projectContext?.RemoveSourceFile(document.FilePath); projectManager.DocumentRemoved(_current, document); _currentDocuments.Remove(document.FilePath); } @@ -217,6 +321,7 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem foreach (var kvp in _currentDocuments) { + _projectContext?.RemoveSourceFile(kvp.Value.FilePath); _projectManager.DocumentRemoved(_current, kvp.Value); } @@ -249,5 +354,48 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem { await OnProjectRenamingAsync().ConfigureAwait(false); } + + private static IEnumerable GetFolders(HostDocument document) + { + var split = document.TargetPath.Split('/'); + return split.Take(split.Length - 1); + } + + private class IUnconfiguredProjectHostObject + { + private readonly object _inner; + + public IUnconfiguredProjectHostObject(object inner) + { + _inner = inner; + } + + public object ActiveIntellisenseProjectHostObject + { + get + { + return _inner.GetType().GetProperty(nameof(ActiveIntellisenseProjectHostObject)).GetValue(_inner); + } + } + } + + private class IProjectHostProvider + { + private readonly object _inner; + + public IProjectHostProvider(object inner) + { + _inner = inner; + } + + public IUnconfiguredProjectHostObject UnconfiguredProjectHostObject + { + get + { + var inner = _inner.GetType().GetProperty(nameof(UnconfiguredProjectHostObject)).GetValue(_inner); + return new IUnconfiguredProjectHostObject(inner); + } + } + } } } \ No newline at end of file diff --git a/test/Microsoft.CodeAnalysis.Razor.Workspaces.Test/ProjectSystem/GeneratedCodeContainerTest.cs b/test/Microsoft.CodeAnalysis.Razor.Workspaces.Test/ProjectSystem/GeneratedCodeContainerTest.cs new file mode 100644 index 0000000000..0f2c03c02e --- /dev/null +++ b/test/Microsoft.CodeAnalysis.Razor.Workspaces.Test/ProjectSystem/GeneratedCodeContainerTest.cs @@ -0,0 +1,83 @@ +// 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 Microsoft.AspNetCore.Razor.Language; +using Microsoft.CodeAnalysis.Text; +using Xunit; + +namespace Microsoft.CodeAnalysis.Razor.ProjectSystem +{ + public class GeneratedCodeContainerTest + { + [Fact] + public void TryGetLinePositionSpan_SpanWithinSourceMapping_ReturnsTrue() + { + // Arrange + var content = @" +@{ + var x = SomeClass.SomeProperty; +} +"; + var sourceText = SourceText.From(content); + var codeDocument = GetCodeDocument(content); + var generatedCode = codeDocument.GetCSharpDocument().GeneratedCode; + + var container = new GeneratedCodeContainer(); + container.SetOutput(sourceText, codeDocument); + + // TODO: Make writing these tests a little less manual. + // Position of `SomeProperty` in the generated code. + var symbol = "SomeProperty"; + var span = new TextSpan(generatedCode.IndexOf(symbol), symbol.Length); + + // Position of `SomeProperty` in the source code. + var expectedLineSpan = new LinePositionSpan(new LinePosition(2, 22), new LinePosition(2, 34)); + + // Act + var result = container.TryGetLinePositionSpan(span, out var lineSpan); + + // Assert + Assert.True(result); + Assert.Equal(expectedLineSpan, lineSpan); + } + + [Fact] + public void TryGetLinePositionSpan_SpanOutsideSourceMapping_ReturnsFalse() + { + // Arrange + var content = @" +@{ + var x = SomeClass.SomeProperty; +} +"; + var sourceText = SourceText.From(content); + var codeDocument = GetCodeDocument(content); + var generatedCode = codeDocument.GetCSharpDocument().GeneratedCode; + + var container = new GeneratedCodeContainer(); + container.SetOutput(sourceText, codeDocument); + + // Position of `ExecuteAsync` in the generated code. + var symbol = "ExecuteAsync"; + var span = new TextSpan(generatedCode.IndexOf(symbol), symbol.Length); + + // Act + var result = container.TryGetLinePositionSpan(span, out var lineSpan); + + // Assert + Assert.False(result); + } + + private static RazorCodeDocument GetCodeDocument(string content) + { + var sourceProjectItem = new TestRazorProjectItem("test.cshtml") + { + Content = content, + }; + + var engine = RazorProjectEngine.Create(); + var codeDocument = engine.ProcessDesignTime(sourceProjectItem); + return codeDocument; + } + } +}