Add generated documents to workspace

This commit is contained in:
Ryan Nowak 2018-05-19 19:11:38 -07:00
parent b486c5a233
commit 13e13d7632
20 changed files with 829 additions and 32 deletions

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -4,6 +4,7 @@
<Description>Razor is a markup syntax for adding server-side logic to web pages. This package contains the Razor design-time infrastructure.</Description>
<TargetFrameworks>net46;netstandard2.0</TargetFrameworks>
<EnableApiCheck>false</EnableApiCheck>
<DefineConstants>$(DefineConstants);DOCUMENT_SERVICE_FACTORY</DefineConstants>
</PropertyGroup>
<ItemGroup>

View File

@ -65,6 +65,8 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem
public HostWorkspaceServices Services { get; }
public GeneratedCodeContainer GeneratedCodeContainer => HostDocument.GeneratedCodeContainer;
public DocumentGeneratedOutputTracker GeneratedOutput
{
get

View File

@ -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<TService>()
{
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<ImmutableArray<SpanMapResult>> MapSpansAsync(
Document document,
IEnumerable<TextSpan> spans,
CancellationToken cancellationToken)
{
if (Output == null)
{
return Task.FromResult(ImmutableArray<SpanMapResult>.Empty);
}
var results = ImmutableArray.CreateBuilder<SpanMapResult>();
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<TextChangeEventArgs> 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);
}
}
}
}

View File

@ -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; }
}
}

View File

@ -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);
}
}
}

View File

@ -43,6 +43,9 @@ Copyright (c) .NET Foundation. All rights reserved.
<PropertyPageSchema Include="$(MSBuildThisFileDirectory)Rules\RazorGeneral.xaml">
<Context>Project</Context>
</PropertyPageSchema>
<PropertyPageSchema Include="$(MSBuildThisFileDirectory)Rules\RazorGenerateWithTargetPath.xaml">
<Context>Project</Context>
</PropertyPageSchema>
</ItemGroup>
<Target Name="RazorGenerateDesignTime" DependsOnTargets="ResolveRazorGenerateInputs;AssignRazorGenerateTargetPaths" Returns="@(RazorGenerateWithTargetPath)">

View File

@ -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<DocumentKey, DocumentSnapshot> _files;
private readonly Dictionary<DocumentKey, DocumentSnapshot> _work;
private Timer _timer;
[ImportingConstructor]
@ -30,17 +29,16 @@ namespace Microsoft.CodeAnalysis.Razor
}
_foregroundDispatcher = foregroundDispatcher;
_files = new Dictionary<DocumentKey, DocumentSnapshot>();
_work = new Dictionary<DocumentKey, DocumentSnapshot>();
}
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<DocumentKey, DocumentSnapshot>[] 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<DocumentKey, DocumentSnapshot>[] 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}");

View File

@ -5,6 +5,7 @@
<Description>Razor is a markup syntax for adding server-side logic to web pages. This package contains the Razor design-time infrastructure for Visual Studio.</Description>
<EnableApiCheck>false</EnableApiCheck>
<RulesDirectory>..\Microsoft.NET.Sdk.Razor\build\netstandard2.0\Rules\</RulesDirectory>
<DefineConstants>$(DefineConstants);WORKSPACE_PROJECT_CONTEXT_FACTORY</DefineConstants>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)' == 'Debug'">

View File

@ -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<string, System.Collections.Immutable.IImmutableDictionary<string, string>>;
#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<IWorkspaceProjectContextFactory> 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<string, IProjectRuleSnapshot> 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<string>();
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<string, IProjectRuleSnapshot> 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<string, IProjectRuleSnapshot> 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<string>();
}
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;
}
}
}

View File

@ -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<string> folderNames, SourceCodeKind sourceCodeKind)
{
}
public void AddSourceFile(string filePath, bool isInCurrentContext = true, IEnumerable<string> folderNames = null, SourceCodeKind sourceCodeKind = SourceCodeKind.Regular, IDocumentServiceFactory documentServiceFactory = null)
{
}
public void AddSourceFile(string filePath, SourceTextContainer container, bool isInCurrentContext = true, IEnumerable<string> 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

View File

@ -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)
{
}

View File

@ -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<string> folderNames, SourceCodeKind sourceCodeKind); // This overload just for binary compat with existing code
void AddSourceFile(string filePath, bool isInCurrentContext = true, IEnumerable<string> folderNames = null, SourceCodeKind sourceCodeKind = SourceCodeKind.Regular, IDocumentServiceFactory documentServiceFactory = null);
void AddSourceFile(string filePath, SourceTextContainer container, bool isInCurrentContext = true, IEnumerable<string> 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

View File

@ -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

View File

@ -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";

View File

@ -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

View File

@ -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<IWorkspaceProjectContextFactory> _projectContextFactory;
private readonly AsyncSemaphore _lock;
private ProjectSnapshotManagerBase _projectManager;
private HostProject _current;
private IWorkspaceProjectContext _projectContext;
private Dictionary<string, HostDocument> _currentDocuments;
private HashSet<string> _references;
private string _commandLineOptions;
public RazorProjectHostBase(
IUnconfiguredProjectCommonServices commonServices,
[Import(typeof(VisualStudioWorkspace))] Workspace workspace)
[Import(typeof(VisualStudioWorkspace))] Workspace workspace,
Lazy<IWorkspaceProjectContextFactory> 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<string, HostDocument>(FilePathComparer.Instance);
_references = new HashSet<string>(FilePathComparer.Instance);
}
// Internal for testing
@ -73,6 +88,7 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem
_lock = new AsyncSemaphore(initialCount: 1);
_currentDocuments = new Dictionary<string, HostDocument>(FilePathComparer.Instance);
_references = new HashSet<string>(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<Type>()).MakeGenericMethod(type);
var export = method.Invoke(CommonServices.UnconfiguredProject.Services.ExportProvider, Array.Empty<object>());
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<string>(references, FilePathComparer.Instance);
var older = new HashSet<string>(_references, FilePathComparer.Instance);
if (older.SetEquals(newer))
{
return;
}
var remove = new HashSet<string>(older, FilePathComparer.Instance);
remove.ExceptWith(newer);
var add = new HashSet<string>(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<string> 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);
}
}
}
}
}

View File

@ -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;
}
}
}