// Copyright (c) Microsoft. 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.Collections.Immutable;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Windows.Threading;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Diagnostics;
using Microsoft.CodeAnalysis.ErrorReporting;
using Microsoft.CodeAnalysis.Host;
using Microsoft.CodeAnalysis.Notification;
using Microsoft.VisualStudio.ComponentModelHost;
using Microsoft.VisualStudio.LanguageServices.Implementation.TaskList;
using Microsoft.VisualStudio.Shell;
using Microsoft.VisualStudio.Shell.Interop;
using Microsoft.VisualStudio.Text;
using Microsoft.VisualStudio.TextManager.Interop;
using Microsoft.VisualStudio.Utilities;
using Roslyn.Utilities;
using VSLangProj;
namespace Microsoft.VisualStudio.LanguageServices.Implementation.ProjectSystem
{
internal abstract partial class AbstractProject : IVisualStudioHostProject
{
internal static object RuleSetErrorId = new object();
private readonly ProjectId _id;
private readonly string _language;
private readonly IVsHierarchy _hierarchy;
private readonly VersionStamp _version;
private readonly string _projectSystemName;
///
/// The path to the project file itself. This is intentionally kept private, to avoid having to deal with people who
/// want the file path without realizing they need to deal with renames. If you need the folder of the project, just
/// use which is internal and doesn't change for a project.
///
private string _filePathOpt;
private readonly MiscellaneousFilesWorkspace _miscellaneousFilesWorkspaceOpt;
private readonly VisualStudioWorkspaceImpl _visualStudioWorkspaceOpt;
private readonly IContentTypeRegistryService _contentTypeRegistryService;
private readonly IVsReportExternalErrors _externalErrorReporter;
private readonly HostDiagnosticUpdateSource _hostDiagnosticUpdateSourceOpt;
internal readonly IServiceProvider ServiceProvider;
protected readonly VisualStudioProjectTracker ProjectTracker;
protected readonly IVsRunningDocumentTable4 RunningDocumentTable;
private string _objOutputPathOpt;
private string _binOutputPathOpt;
private string _assemblyName;
private CompilationOptions _compilationOptions;
private ParseOptions _parseOptions;
private readonly List _projectReferences = new List();
private readonly List _metadataReferences = new List();
private readonly Dictionary _documents = new Dictionary();
private readonly Dictionary _documentMonikers = new Dictionary(StringComparer.OrdinalIgnoreCase);
private readonly Dictionary _analyzers = new Dictionary(StringComparer.OrdinalIgnoreCase);
private readonly Dictionary _additionalDocuments = new Dictionary();
protected IRuleSetFile ruleSet = null;
///
/// The list of files which have been added to the project but we aren't tracking since they
/// aren't real source files. Sometimes we're asked to add silly things like HTML files or XAML
/// files, and if those are open in a strange editor we just bail.
///
private readonly ISet _untrackedDocuments = new HashSet(StringComparer.OrdinalIgnoreCase);
///
/// The path to a metadata reference that was converted to project references.
///
private readonly Dictionary _metadataFileNameToConvertedProjectReference = new Dictionary(StringComparer.OrdinalIgnoreCase);
private bool _pushingChangesToWorkspaceHosts;
///
/// Guid of the _hierarchy
///
/// it is not readonly since it can be changed while loading project
///
private Guid _guid;
// PERF: Create these event handlers once to be shared amongst all documents (the sender arg identifies which document and project)
private static readonly EventHandler s_documentOpenedEventHandler = OnDocumentOpened;
private static readonly EventHandler s_documentClosingEventHandler = OnDocumentClosing;
private static readonly EventHandler s_documentUpdatedOnDiskEventHandler = OnDocumentUpdatedOnDisk;
private static readonly EventHandler s_additionalDocumentOpenedEventHandler = OnAdditionalDocumentOpened;
private static readonly EventHandler s_additionalDocumentClosingEventHandler = OnAdditionalDocumentClosing;
private static readonly EventHandler s_additionalDocumentUpdatedOnDiskEventHandler = OnAdditionalDocumentUpdatedOnDisk;
public AbstractProject(
VisualStudioProjectTracker projectTracker,
Func reportExternalErrorCreatorOpt,
string projectSystemName,
IVsHierarchy hierarchy,
string language,
IServiceProvider serviceProvider,
MiscellaneousFilesWorkspace miscellaneousFilesWorkspaceOpt,
VisualStudioWorkspaceImpl visualStudioWorkspaceOpt,
HostDiagnosticUpdateSource hostDiagnosticUpdateSourceOpt)
{
Contract.ThrowIfNull(projectSystemName);
this.ServiceProvider = serviceProvider;
_language = language;
_hierarchy = hierarchy;
// get project id guid
_guid = GetProjectIDGuid(hierarchy);
var componentModel = (IComponentModel)serviceProvider.GetService(typeof(SComponentModel));
_contentTypeRegistryService = componentModel.GetService();
this.RunningDocumentTable = (IVsRunningDocumentTable4)serviceProvider.GetService(typeof(SVsRunningDocumentTable));
this.DisplayName = projectSystemName;
this.ProjectTracker = projectTracker;
_projectSystemName = projectSystemName;
_miscellaneousFilesWorkspaceOpt = miscellaneousFilesWorkspaceOpt;
_visualStudioWorkspaceOpt = visualStudioWorkspaceOpt;
_hostDiagnosticUpdateSourceOpt = hostDiagnosticUpdateSourceOpt;
UpdateProjectDisplayNameAndFilePath();
if (_filePathOpt != null)
{
_version = VersionStamp.Create(File.GetLastWriteTimeUtc(_filePathOpt));
}
else
{
_version = VersionStamp.Create();
}
_id = this.ProjectTracker.GetOrCreateProjectIdForPath(_filePathOpt ?? _projectSystemName, _projectSystemName);
if (reportExternalErrorCreatorOpt != null)
{
_externalErrorReporter = reportExternalErrorCreatorOpt(_id);
}
ConnectHierarchyEvents();
SetIsWebstite(hierarchy);
}
private Guid GetProjectIDGuid(IVsHierarchy hierarchy)
{
Guid guid;
if (hierarchy.TryGetGuidProperty(__VSHPROPID.VSHPROPID_ProjectIDGuid, out guid))
{
return guid;
}
return Guid.Empty;
}
private void SetIsWebstite(IVsHierarchy hierarchy)
{
EnvDTE.Project project;
try
{
if (hierarchy.TryGetProject(out project))
{
this.IsWebSite = project.Kind == VsWebSite.PrjKind.prjKindVenusProject;
}
}
catch (Exception)
{
this.IsWebSite = false;
}
}
///
/// Returns a display name for the given project.
///
private static bool TryGetProjectDisplayName(IVsHierarchy hierarchy, out string name)
{
name = null;
if (!hierarchy.TryGetName(out name))
{
return false;
}
return true;
}
///
/// Indicates whether this project is a website type.
///
public bool IsWebSite { get; private set; }
///
/// A full path to the project obj output binary, or null if the project doesn't have an obj output binary.
///
internal string TryGetObjOutputPath() => _objOutputPathOpt;
///
/// A full path to the project bin output binary, or null if the project doesn't have an bin output binary.
///
internal string TryGetBinOutputPath() => _binOutputPathOpt;
internal VisualStudioWorkspaceImpl VisualStudioWorkspace => _visualStudioWorkspaceOpt;
internal IRuleSetFile RuleSetFile => this.ruleSet;
internal HostDiagnosticUpdateSource HostDiagnosticUpdateSource => _hostDiagnosticUpdateSourceOpt;
public ProjectId Id => _id;
public string Language => _language;
public IVsHierarchy Hierarchy => _hierarchy;
public Guid Guid => _guid;
public Workspace Workspace => (Workspace)_visualStudioWorkspaceOpt ?? _miscellaneousFilesWorkspaceOpt;
public VersionStamp Version => _version;
///
/// The containing directory of the project. Null if none exists (consider Venus.)
///
protected string ContainingDirectoryPathOpt
{
get
{
if (_filePathOpt != null)
{
return Path.GetDirectoryName(_filePathOpt);
}
else
{
return null;
}
}
}
///
/// The public display name of the project. This name is not unique and may be shared
/// between multiple projects, especially in cases like Venus where the intellisense
/// projects will match the name of their logical parent project.
///
public string DisplayName { get; private set; }
///
/// The name of the project according to the project system. In "regular" projects this is
/// equivalent to , but in Venus cases these will differ. The
/// ProjectSystemName is the 2_Default.aspx project name, whereas the regular display name
/// matches the display name of the project the user actually sees in the solution explorer.
/// These can be assumed to be unique within the Visual Studio workspace.
///
public string ProjectSystemName
{
get { return _projectSystemName; }
}
protected DocumentProvider DocumentProvider
{
get { return this.ProjectTracker.DocumentProvider; }
}
protected VisualStudioMetadataReferenceManager MetadataReferenceProvider
{
get { return this.ProjectTracker.MetadataReferenceProvider; }
}
protected IContentTypeRegistryService ContentTypeRegistryService
{
get { return _contentTypeRegistryService; }
}
public ProjectInfo CreateProjectInfoForCurrentState()
{
ValidateReferences();
return ProjectInfo.Create(
this.Id,
this.Version,
this.DisplayName,
_assemblyName ?? this.ProjectSystemName,
this.Language,
filePath: _filePathOpt,
outputFilePath: this.TryGetObjOutputPath(),
compilationOptions: _compilationOptions,
parseOptions: _parseOptions,
documents: _documents.Values.Select(d => d.GetInitialState()),
metadataReferences: _metadataReferences.Select(r => r.CurrentSnapshot),
projectReferences: _projectReferences,
analyzerReferences: _analyzers.Values.Select(a => a.GetReference()),
additionalDocuments: _additionalDocuments.Values.Select(d => d.GetInitialState()));
}
protected ImmutableArray GetStrongNameKeyPaths()
{
var outputPath = this.TryGetObjOutputPath();
if (this.ContainingDirectoryPathOpt == null && outputPath == null)
{
return ImmutableArray.Empty;
}
var builder = ImmutableArray.CreateBuilder();
if (this.ContainingDirectoryPathOpt != null)
{
builder.Add(this.ContainingDirectoryPathOpt);
}
if (outputPath != null)
{
builder.Add(Path.GetDirectoryName(outputPath));
}
return builder.ToImmutable();
}
public ImmutableArray GetCurrentProjectReferences()
{
return ImmutableArray.CreateRange(_projectReferences);
}
public IVisualStudioHostDocument GetDocumentOrAdditionalDocument(DocumentId id)
{
IVisualStudioHostDocument doc;
_documents.TryGetValue(id, out doc);
if (doc == null)
{
_additionalDocuments.TryGetValue(id, out doc);
}
return doc;
}
public IEnumerable GetCurrentDocuments()
{
return _documents.Values.ToImmutableArrayOrEmpty();
}
public bool ContainsFile(string moniker)
{
return _documentMonikers.ContainsKey(moniker);
}
public IVisualStudioHostDocument GetCurrentDocumentFromPath(string filePath)
{
IVisualStudioHostDocument document;
_documentMonikers.TryGetValue(filePath, out document);
return document;
}
public bool HasMetadataReference(string filename)
{
return _metadataReferences.Any(r => StringComparer.OrdinalIgnoreCase.Equals(r.FilePath, filename));
}
public VisualStudioMetadataReference TryGetCurrentMetadataReference(string filename)
{
// We must normalize the file path, since the paths we're comparing to are always normalized
filename = FileUtilities.NormalizeAbsolutePath(filename);
return _metadataReferences.SingleOrDefault(r => StringComparer.OrdinalIgnoreCase.Equals(r.FilePath, filename));
}
public bool CurrentProjectReferencesContains(ProjectId projectId)
{
return _projectReferences.Any(r => r.ProjectId == projectId);
}
public bool CurrentProjectAnalyzersContains(string fullPath)
{
return _analyzers.ContainsKey(fullPath);
}
private static string GetAssemblyName(string outputPath)
{
Contract.Requires(outputPath != null);
// dev11 sometimes gives us output path w/o extension, so removing extension becomes problematic
if (outputPath.EndsWith(".exe", StringComparison.OrdinalIgnoreCase) ||
outputPath.EndsWith(".dll", StringComparison.OrdinalIgnoreCase) ||
outputPath.EndsWith(".netmodule", StringComparison.OrdinalIgnoreCase))
{
return Path.GetFileNameWithoutExtension(outputPath);
}
else
{
return Path.GetFileName(outputPath);
}
}
protected void SetOptions(CompilationOptions compilationOptions, ParseOptions parseOptions)
{
_compilationOptions = compilationOptions;
_parseOptions = parseOptions;
if (_pushingChangesToWorkspaceHosts)
{
this.ProjectTracker.NotifyWorkspaceHosts(host => host.OnOptionsChanged(_id, compilationOptions, parseOptions));
}
}
protected int AddMetadataReferenceAndTryConvertingToProjectReferenceIfPossible(string filePath, MetadataReferenceProperties properties, int hResultForMissingFile)
{
// If this file is coming from a project, then we should convert it to a project reference instead
AbstractProject project;
if (ProjectTracker.TryGetProjectByBinPath(filePath, out project))
{
var projectReference = new ProjectReference(project.Id, properties.Aliases, properties.EmbedInteropTypes);
AddProjectReference(projectReference);
_metadataFileNameToConvertedProjectReference.Add(filePath, projectReference);
return VSConstants.S_OK;
}
if (!File.Exists(filePath))
{
return hResultForMissingFile;
}
AddMetadataReferenceCore(this.MetadataReferenceProvider.CreateMetadataReference(this, filePath, properties));
return VSConstants.S_OK;
}
protected void RemoveMetadataReference(string filePath)
{
// Is this a reference we converted to a project reference?
ProjectReference projectReference;
if (_metadataFileNameToConvertedProjectReference.TryGetValue(filePath, out projectReference))
{
// We converted this, so remove the project reference instead
RemoveProjectReference(projectReference);
Contract.ThrowIfFalse(_metadataFileNameToConvertedProjectReference.Remove(filePath));
}
// Just a metadata reference, so remove all of those
var referenceToRemove = TryGetCurrentMetadataReference(filePath);
if (referenceToRemove != null)
{
RemoveMetadataReferenceCore(referenceToRemove, disposeReference: true);
}
}
private void AddMetadataReferenceCore(VisualStudioMetadataReference reference)
{
_metadataReferences.Add(reference);
if (_pushingChangesToWorkspaceHosts)
{
var snapshot = reference.CurrentSnapshot;
this.ProjectTracker.NotifyWorkspaceHosts(host => host.OnMetadataReferenceAdded(this.Id, snapshot));
}
reference.UpdatedOnDisk += OnImportChanged;
}
private void RemoveMetadataReferenceCore(VisualStudioMetadataReference reference, bool disposeReference)
{
_metadataReferences.Remove(reference);
if (_pushingChangesToWorkspaceHosts)
{
var snapshot = reference.CurrentSnapshot;
this.ProjectTracker.NotifyWorkspaceHosts(host => host.OnMetadataReferenceRemoved(this.Id, snapshot));
}
reference.UpdatedOnDisk -= OnImportChanged;
if (disposeReference)
{
reference.Dispose();
}
}
///
/// Called when a referenced metadata file changes on disk.
///
private void OnImportChanged(object sender, EventArgs e)
{
VisualStudioMetadataReference reference = (VisualStudioMetadataReference)sender;
// Ensure that we are still referencing this binary
if (_metadataReferences.Contains(reference))
{
// remove the old metadata reference
this.RemoveMetadataReferenceCore(reference, disposeReference: false);
// Signal to update the underlying reference snapshot
reference.UpdateSnapshot();
// add it back (it will now be based on the new file contents)
this.AddMetadataReferenceCore(reference);
}
}
private void OnAnalyzerChanged(object sender, EventArgs e)
{
VisualStudioAnalyzer analyzer = (VisualStudioAnalyzer)sender;
RemoveAnalyzerAssembly(analyzer.FullPath);
AddAnalyzerAssembly(analyzer.FullPath);
}
protected void AddProjectReference(ProjectReference projectReference)
{
// dev11 is sometimes calling us multiple times for the same data
if (_projectReferences.Contains(projectReference))
{
return;
}
// always manipulate current state after workspace is told so it will correctly observe the initial state
_projectReferences.Add(projectReference);
if (_pushingChangesToWorkspaceHosts)
{
// This project is already pushed to listening workspace hosts, but it's possible that our target
// project hasn't been yet. Get the dependent project into the workspace as well.
var targetProject = this.ProjectTracker.GetProject(projectReference.ProjectId);
this.ProjectTracker.StartPushingToWorkspaceAndNotifyOfOpenDocuments(SpecializedCollections.SingletonEnumerable(targetProject));
this.ProjectTracker.NotifyWorkspaceHosts(host => host.OnProjectReferenceAdded(this.Id, projectReference));
}
}
protected void RemoveProjectReference(ProjectReference projectReference)
{
Contract.ThrowIfFalse(_projectReferences.Remove(projectReference));
if (_pushingChangesToWorkspaceHosts)
{
this.ProjectTracker.NotifyWorkspaceHosts(host => host.OnProjectReferenceRemoved(this.Id, projectReference));
}
}
private static void OnDocumentOpened(object sender, bool isCurrentContext)
{
IVisualStudioHostDocument document = (IVisualStudioHostDocument)sender;
AbstractProject project = (AbstractProject)document.Project;
if (project._pushingChangesToWorkspaceHosts)
{
project.ProjectTracker.NotifyWorkspaceHosts(host => host.OnDocumentOpened(document.Id, document.GetOpenTextBuffer(), isCurrentContext));
}
else
{
StartPushingToWorkspaceAndNotifyOfOpenDocuments(project);
}
}
private static void OnDocumentClosing(object sender, bool updateActiveContext)
{
IVisualStudioHostDocument document = (IVisualStudioHostDocument)sender;
AbstractProject project = (AbstractProject)document.Project;
var projectTracker = project.ProjectTracker;
if (project._pushingChangesToWorkspaceHosts)
{
projectTracker.NotifyWorkspaceHosts(host => host.OnDocumentClosed(document.Id, document.GetOpenTextBuffer(), document.Loader, updateActiveContext));
}
}
private static void OnDocumentUpdatedOnDisk(object sender, EventArgs e)
{
IVisualStudioHostDocument document = (IVisualStudioHostDocument)sender;
AbstractProject project = (AbstractProject)document.Project;
if (project._pushingChangesToWorkspaceHosts)
{
project.ProjectTracker.NotifyWorkspaceHosts(host => host.OnDocumentTextUpdatedOnDisk(document.Id));
}
}
private static void OnAdditionalDocumentOpened(object sender, bool isCurrentContext)
{
IVisualStudioHostDocument document = (IVisualStudioHostDocument)sender;
AbstractProject project = (AbstractProject)document.Project;
if (project._pushingChangesToWorkspaceHosts)
{
project.ProjectTracker.NotifyWorkspaceHosts(host => host.OnAdditionalDocumentOpened(document.Id, document.GetOpenTextBuffer(), isCurrentContext));
}
else
{
StartPushingToWorkspaceAndNotifyOfOpenDocuments(project);
}
}
private static void OnAdditionalDocumentClosing(object sender, bool notUsed)
{
IVisualStudioHostDocument document = (IVisualStudioHostDocument)sender;
AbstractProject project = (AbstractProject)document.Project;
var projectTracker = project.ProjectTracker;
if (project._pushingChangesToWorkspaceHosts)
{
projectTracker.NotifyWorkspaceHosts(host => host.OnAdditionalDocumentClosed(document.Id, document.GetOpenTextBuffer(), document.Loader));
}
}
private static void OnAdditionalDocumentUpdatedOnDisk(object sender, EventArgs e)
{
IVisualStudioHostDocument document = (IVisualStudioHostDocument)sender;
AbstractProject project = (AbstractProject)document.Project;
if (project._pushingChangesToWorkspaceHosts)
{
project.ProjectTracker.NotifyWorkspaceHosts(host => host.OnAdditionalDocumentTextUpdatedOnDisk(document.Id));
}
}
protected void AddFile(string filename, SourceCodeKind sourceCodeKind, uint itemId, Func canUseTextBuffer)
{
var document = this.DocumentProvider.TryGetDocumentForFile(this, itemId, filePath: filename, sourceCodeKind: sourceCodeKind, canUseTextBuffer: canUseTextBuffer);
if (document == null)
{
// It's possible this file is open in some very strange editor. In that case, we'll just ignore it.
// This might happen if somebody decides to mark a non-source-file as something to compile.
// TODO: Venus does this for .aspx/.cshtml files which is completely unecessary for Roslyn. We should remove that code.
AddUntrackedFile(filename);
return;
}
AddDocument(
document,
isCurrentContext: document.Project.Hierarchy == LinkedFileUtilities.GetContextHierarchy(document, RunningDocumentTable));
}
protected void AddUntrackedFile(string filename)
{
_untrackedDocuments.Add(filename);
}
protected void RemoveFile(string filename)
{
// Remove this as an untracked file, if it is
if (_untrackedDocuments.Remove(filename))
{
return;
}
IVisualStudioHostDocument document = this.GetCurrentDocumentFromPath(filename);
if (document == null)
{
throw new InvalidOperationException("The document is not a part of the finalProject.");
}
RemoveDocument(document);
}
internal void AddDocument(IVisualStudioHostDocument document, bool isCurrentContext)
{
// We do not want to allow message pumping/reentrancy when processing project system changes.
using (Dispatcher.CurrentDispatcher.DisableProcessing())
{
if (_miscellaneousFilesWorkspaceOpt != null)
{
_miscellaneousFilesWorkspaceOpt.OnFileIncludedInProject(document);
}
_documents.Add(document.Id, document);
_documentMonikers.Add(document.Key.Moniker, document);
if (_pushingChangesToWorkspaceHosts)
{
this.ProjectTracker.NotifyWorkspaceHosts(host => host.OnDocumentAdded(document.GetInitialState()));
if (document.IsOpen)
{
this.ProjectTracker.NotifyWorkspaceHosts(host => host.OnDocumentOpened(document.Id, document.GetOpenTextBuffer(), isCurrentContext));
}
}
document.Opened += s_documentOpenedEventHandler;
document.Closing += s_documentClosingEventHandler;
document.UpdatedOnDisk += s_documentUpdatedOnDiskEventHandler;
DocumentProvider.NotifyDocumentRegisteredToProject(document);
if (!_pushingChangesToWorkspaceHosts && document.IsOpen)
{
StartPushingToWorkspaceAndNotifyOfOpenDocuments();
}
}
}
internal void RemoveDocument(IVisualStudioHostDocument document)
{
// We do not want to allow message pumping/reentrancy when processing project system changes.
using (Dispatcher.CurrentDispatcher.DisableProcessing())
{
_documents.Remove(document.Id);
_documentMonikers.Remove(document.Key.Moniker);
UninitializeDocument(document);
OnDocumentRemoved(document.Key.Moniker);
}
}
internal void AddAdditionalDocument(IVisualStudioHostDocument document, bool isCurrentContext)
{
if (_miscellaneousFilesWorkspaceOpt != null)
{
_miscellaneousFilesWorkspaceOpt.OnFileIncludedInProject(document);
}
_additionalDocuments.Add(document.Id, document);
_documentMonikers.Add(document.Key.Moniker, document);
if (_pushingChangesToWorkspaceHosts)
{
this.ProjectTracker.NotifyWorkspaceHosts(host => host.OnAdditionalDocumentAdded(document.GetInitialState()));
if (document.IsOpen)
{
this.ProjectTracker.NotifyWorkspaceHosts(host => host.OnAdditionalDocumentOpened(document.Id, document.GetOpenTextBuffer(), isCurrentContext));
}
}
document.Opened += s_additionalDocumentOpenedEventHandler;
document.Closing += s_additionalDocumentClosingEventHandler;
document.UpdatedOnDisk += s_additionalDocumentUpdatedOnDiskEventHandler;
DocumentProvider.NotifyDocumentRegisteredToProject(document);
if (!_pushingChangesToWorkspaceHosts && document.IsOpen)
{
StartPushingToWorkspaceAndNotifyOfOpenDocuments();
}
}
internal void RemoveAdditionalDocument(IVisualStudioHostDocument document)
{
_additionalDocuments.Remove(document.Id);
_documentMonikers.Remove(document.Key.Moniker);
UninitializeAdditionalDocument(document);
}
public virtual void Disconnect()
{
using (_visualStudioWorkspaceOpt?.Services.GetService()?.Start("Disconnect Project"))
{
// Unsubscribe IVsHierarchyEvents
DisconnectHierarchyEvents();
// The project is going away, so let's remove ourselves from the host. First, we
// close and dispose of any remaining documents
foreach (var document in this.GetCurrentDocuments())
{
UninitializeDocument(document);
}
// Dispose metadata references.
foreach (var reference in _metadataReferences)
{
reference.Dispose();
}
foreach (var analyzer in _analyzers.Values)
{
analyzer.Dispose();
}
// Make sure we clear out any external errors left when closing the project.
if (_externalErrorReporter != null)
{
_externalErrorReporter.ClearAllErrors();
}
// Make sure we clear out any host errors left when closing the project.
if (_hostDiagnosticUpdateSourceOpt != null)
{
_hostDiagnosticUpdateSourceOpt.ClearAllDiagnosticsForProject(this.Id);
}
ClearAnalyzerRuleSet();
this.ProjectTracker.RemoveProject(this);
}
}
internal void TryProjectConversionForIntroducedOutputPath(string binPath, AbstractProject projectToReference)
{
// We should not already have references for this, since we're only introducing the path for the first time
Contract.ThrowIfTrue(_metadataFileNameToConvertedProjectReference.ContainsKey(binPath));
var metadataReference = TryGetCurrentMetadataReference(binPath);
if (metadataReference != null)
{
var projectReference = new ProjectReference(
projectToReference.Id,
metadataReference.Properties.Aliases,
metadataReference.Properties.EmbedInteropTypes);
RemoveMetadataReferenceCore(metadataReference, disposeReference: true);
AddProjectReference(projectReference);
_metadataFileNameToConvertedProjectReference.Add(binPath, projectReference);
}
}
internal void UndoProjectReferenceConversionForDissappearingOutputPath(string binPath)
{
ProjectReference projectReference;
if (_metadataFileNameToConvertedProjectReference.TryGetValue(binPath, out projectReference))
{
// We converted this, so convert it back to a metadata reference
RemoveProjectReference(projectReference);
var metadataReferenceProperties = new MetadataReferenceProperties(
MetadataImageKind.Assembly,
projectReference.Aliases,
projectReference.EmbedInteropTypes);
AddMetadataReferenceCore(MetadataReferenceProvider.CreateMetadataReference(this, binPath, metadataReferenceProperties));
Contract.ThrowIfFalse(_metadataFileNameToConvertedProjectReference.Remove(binPath));
}
}
protected void UpdateMetadataReferenceAliases(string file, ImmutableArray aliases)
{
file = FileUtilities.NormalizeAbsolutePath(file);
// Have we converted these to project references?
ProjectReference convertedProjectReference;
if (_metadataFileNameToConvertedProjectReference.TryGetValue(file, out convertedProjectReference))
{
var project = ProjectTracker.GetProject(convertedProjectReference.ProjectId);
UpdateProjectReferenceAliases(project, aliases);
}
else
{
var existingReference = TryGetCurrentMetadataReference(file);
Contract.ThrowIfNull(existingReference);
var newProperties = existingReference.Properties.WithAliases(aliases);
RemoveMetadataReferenceCore(existingReference, disposeReference: true);
AddMetadataReferenceCore(this.MetadataReferenceProvider.CreateMetadataReference(this, file, newProperties));
}
}
protected void UpdateProjectReferenceAliases(AbstractProject referencedProject, ImmutableArray aliases)
{
var projectReference = GetCurrentProjectReferences().Single(r => r.ProjectId == referencedProject.Id);
var newProjectReference = new ProjectReference(referencedProject.Id, aliases, projectReference.EmbedInteropTypes);
// Is this a project with converted references? If so, make sure we track it
string referenceBinPath = referencedProject.TryGetBinOutputPath();
if (referenceBinPath != null && _metadataFileNameToConvertedProjectReference.ContainsKey(referenceBinPath))
{
_metadataFileNameToConvertedProjectReference[referenceBinPath] = newProjectReference;
}
// Remove the existing reference first
RemoveProjectReference(projectReference);
AddProjectReference(newProjectReference);
}
private void UninitializeDocument(IVisualStudioHostDocument document)
{
if (_pushingChangesToWorkspaceHosts)
{
if (document.IsOpen)
{
this.ProjectTracker.NotifyWorkspaceHosts(host => host.OnDocumentClosed(document.Id, document.GetOpenTextBuffer(), document.Loader, updateActiveContext: true));
}
this.ProjectTracker.NotifyWorkspaceHosts(host => host.OnDocumentRemoved(document.Id));
}
if (_miscellaneousFilesWorkspaceOpt != null)
{
_miscellaneousFilesWorkspaceOpt.OnFileRemovedFromProject(document);
}
document.Opened -= s_documentOpenedEventHandler;
document.Closing -= s_documentClosingEventHandler;
document.UpdatedOnDisk -= s_documentUpdatedOnDiskEventHandler;
document.Dispose();
}
private void UninitializeAdditionalDocument(IVisualStudioHostDocument document)
{
if (_pushingChangesToWorkspaceHosts)
{
if (document.IsOpen)
{
this.ProjectTracker.NotifyWorkspaceHosts(host => host.OnAdditionalDocumentClosed(document.Id, document.GetOpenTextBuffer(), document.Loader));
}
this.ProjectTracker.NotifyWorkspaceHosts(host => host.OnAdditionalDocumentRemoved(document.Id));
}
if (_miscellaneousFilesWorkspaceOpt != null)
{
_miscellaneousFilesWorkspaceOpt.OnFileRemovedFromProject(document);
}
document.Opened -= s_additionalDocumentOpenedEventHandler;
document.Closing -= s_additionalDocumentClosingEventHandler;
document.UpdatedOnDisk -= s_additionalDocumentUpdatedOnDiskEventHandler;
document.Dispose();
}
protected virtual void OnDocumentRemoved(string filePath)
{
}
protected virtual void UpdateAnalyzerRules()
{
}
private readonly Dictionary> _folderNameMap = new Dictionary>();
public IReadOnlyList GetFolderNames(uint documentItemID)
{
object parentObj;
if (documentItemID != (uint)VSConstants.VSITEMID.Nil && _hierarchy.GetProperty(documentItemID, (int)VsHierarchyPropID.Parent, out parentObj) == VSConstants.S_OK)
{
var parentID = this.UnboxVSItemId(parentObj);
if (parentID != (uint)VSConstants.VSITEMID.Nil && parentID != (uint)VSConstants.VSITEMID.Root)
{
return this.GetFolderNamesForFolder(parentID);
}
}
return SpecializedCollections.EmptyReadOnlyList();
}
private readonly List _tmpFolders = new List();
private IReadOnlyList GetFolderNamesForFolder(uint folderItemID)
{
// note: use of tmpFolders is assuming this API is called on UI thread only.
_tmpFolders.Clear();
IReadOnlyList names;
if (!_folderNameMap.TryGetValue(folderItemID, out names))
{
this.ComputeFolderNames(folderItemID, _tmpFolders);
names = _tmpFolders.ToImmutableArray();
_folderNameMap.Add(folderItemID, names);
}
else
{
// verify names, and change map if we get a different set.
// this is necessary because we only get document adds/removes from the project system
// when a document name or folder name changes.
this.ComputeFolderNames(folderItemID, _tmpFolders);
if (!Enumerable.SequenceEqual(names, _tmpFolders))
{
names = _tmpFolders.ToImmutableArray();
_folderNameMap[folderItemID] = names;
}
}
return names;
}
// Different hierarchies are inconsistent on whether they return ints or uints for VSItemIds.
// Technically it should be a uint. However, there's no enforcement of this, and marshalling
// from native to managed can end up resulting in boxed ints instead. Handle both here so
// we're resilient to however the IVsHierarchy was actually implemented.
private uint UnboxVSItemId(object id)
{
return id is uint ? (uint)id : unchecked((uint)(int)id);
}
private void ComputeFolderNames(uint folderItemID, List names)
{
object nameObj;
if (_hierarchy.GetProperty((uint)folderItemID, (int)VsHierarchyPropID.Name, out nameObj) == VSConstants.S_OK)
{
// For 'Shared' projects, IVSHierarchy returns a hierarcy item with < character in its name (i.e. )
// as a child of the root item. There is no such item in the 'visual' hierarcy in solution explorer and no such folder
// is present on disk either. Since this is not a real 'folder', we exclude it from the contents of Document.Folders.
// Note: The parent of the hierarchy item that contains < characher in its name is VSITEMID.Root. So we don't need to
// worry about accidental propogation out of the Shared project to any containing 'Solution' folders - the check for
// VSITEMID.Root below already takes care of that.
var name = (string)nameObj;
if (!name.StartsWith("<", StringComparison.OrdinalIgnoreCase))
{
names.Insert(0, name);
}
}
object parentObj;
if (_hierarchy.GetProperty((uint)folderItemID, (int)VsHierarchyPropID.Parent, out parentObj) == VSConstants.S_OK)
{
var parentID = this.UnboxVSItemId(parentObj);
if (parentID != (uint)VSConstants.VSITEMID.Nil && parentID != (uint)VSConstants.VSITEMID.Root)
{
ComputeFolderNames(parentID, names);
}
}
}
internal void StartPushingToWorkspaceHosts()
{
_pushingChangesToWorkspaceHosts = true;
}
internal void StopPushingToWorkspaceHosts()
{
_pushingChangesToWorkspaceHosts = false;
}
internal void StartPushingToWorkspaceAndNotifyOfOpenDocuments()
{
StartPushingToWorkspaceAndNotifyOfOpenDocuments(this);
}
internal bool PushingChangesToWorkspaceHosts
{
get
{
return _pushingChangesToWorkspaceHosts;
}
}
protected void UpdateRuleSetError(IRuleSetFile ruleSetFile)
{
if (this.HostDiagnosticUpdateSource == null)
{
return;
}
if (ruleSetFile == null ||
ruleSetFile.GetException() == null)
{
this.HostDiagnosticUpdateSource.ClearDiagnosticsForProject(this.Id, RuleSetErrorId);
}
else
{
string id = ServicesVSResources.ERR_CantReadRulesetFileId;
string category = ServicesVSResources.ErrorCategory;
string message = string.Format(ServicesVSResources.ERR_CantReadRulesetFileMessage, ruleSetFile.FilePath, ruleSetFile.GetException().Message);
DiagnosticData data = new DiagnosticData(id, category, message, ServicesVSResources.ERR_CantReadRulesetFileMessage, DiagnosticSeverity.Error, true, 0, this.Workspace, this.Id);
this.HostDiagnosticUpdateSource.UpdateDiagnosticsForProject(this.Id, RuleSetErrorId, SpecializedCollections.SingletonEnumerable(data));
}
}
protected void SetOutputPathAndRelatedData(string objOutputPath)
{
if (this.Workspace == null)
{
// can only happen in tests
return;
}
if (PathUtilities.IsAbsolute(objOutputPath) && !string.Equals(_objOutputPathOpt, objOutputPath, StringComparison.OrdinalIgnoreCase))
{
// set obj output path if changed
_objOutputPathOpt = objOutputPath;
var metadataService = this.Workspace.Services.GetService();
_compilationOptions = _compilationOptions.WithMetadataReferenceResolver(
new AssemblyReferenceResolver(
CreateMetadataReferenceResolver(projectDirectory: this.ContainingDirectoryPathOpt, outputDirectory: Path.GetDirectoryName(_objOutputPathOpt)),
metadataService.GetProvider()));
if (_pushingChangesToWorkspaceHosts)
{
this.ProjectTracker.NotifyWorkspaceHosts(host => host.OnOptionsChanged(this.Id, _compilationOptions, _parseOptions));
this.ProjectTracker.NotifyWorkspaceHosts(host => host.OnOutputFilePathChanged(this.Id, _objOutputPathOpt));
}
}
// set assembly name if changed
// we use designTimeOutputPath to get assembly name since it is more reliable way to get the assembly name.
// otherwise, friend assembly all get messed up.
var newAssemblyName = GetAssemblyName(_objOutputPathOpt ?? this.ProjectSystemName);
if (!string.Equals(_assemblyName, newAssemblyName, StringComparison.Ordinal))
{
_assemblyName = newAssemblyName;
if (_pushingChangesToWorkspaceHosts)
{
this.ProjectTracker.NotifyWorkspaceHosts(host => host.OnAssemblyNameChanged(this.Id, _assemblyName));
}
}
// refresh final output path
string newBinOutputPath;
if (TryGetOutputPathFromBuildManager(out newBinOutputPath) && newBinOutputPath != null)
{
if (!string.Equals(_binOutputPathOpt, newBinOutputPath, StringComparison.OrdinalIgnoreCase))
{
string oldBinOutputPath = _binOutputPathOpt;
// set obj output path if changed
_binOutputPathOpt = newBinOutputPath;
this.ProjectTracker.UpdateProjectBinPath(this, oldBinOutputPath, _binOutputPathOpt);
}
}
}
private void UpdateProjectDisplayNameAndFilePath()
{
bool updateMade = false;
string newDisplayName;
if (TryGetProjectDisplayName(_hierarchy, out newDisplayName) && this.DisplayName != newDisplayName)
{
this.DisplayName = newDisplayName;
updateMade = true;
}
string newPath;
if (ErrorHandler.Succeeded(((IVsProject3)_hierarchy).GetMkDocument((uint)VSConstants.VSITEMID.Root, out newPath)) &&
File.Exists(newPath) && _filePathOpt != newPath)
{
Debug.Assert(PathUtilities.IsAbsolute(newPath));
_filePathOpt = newPath;
updateMade = true;
}
if (updateMade && _pushingChangesToWorkspaceHosts)
{
this.ProjectTracker.NotifyWorkspaceHosts(host => host.OnProjectNameChanged(_id, this.DisplayName, _filePathOpt));
}
}
private static void StartPushingToWorkspaceAndNotifyOfOpenDocuments(AbstractProject project)
{
// If a document is opened in a project but we haven't started pushing yet, we want to stop doing lazy
// loading for this project and get it up to date so the user gets a fast experience there. If the file
// was presented as open to us right away, then we'll never do this in OnDocumentOpened, so we should do
// it here. It's important to do this after everything else happens in this method, so we don't get
// strange ordering issues. It's still possible that this won't actually push changes if the workspace
// host isn't ready to receive events yet.
project.ProjectTracker.StartPushingToWorkspaceAndNotifyOfOpenDocuments(SpecializedCollections.SingletonEnumerable(project));
}
private static MetadataFileReferenceResolver CreateMetadataReferenceResolver(string projectDirectory, string outputDirectory)
{
var assemblySearchPaths = ImmutableArray.Create();
if (projectDirectory != null && outputDirectory != null)
{
assemblySearchPaths = ImmutableArray.Create(projectDirectory, outputDirectory);
}
else if (projectDirectory != null)
{
assemblySearchPaths = ImmutableArray.Create(projectDirectory);
}
else if (outputDirectory != null)
{
assemblySearchPaths = ImmutableArray.Create(outputDirectory);
}
return new MetadataFileReferenceResolver(assemblySearchPaths, baseDirectory: projectDirectory);
}
private bool TryGetOutputPathFromBuildManager(out string binOutputPath)
{
binOutputPath = null;
string outputDirectory;
string targetFileName;
var storage = _hierarchy as IVsBuildPropertyStorage;
if (storage == null)
{
return false;
}
if (ErrorHandler.Failed(storage.GetPropertyValue("OutDir", null, (uint)_PersistStorageType.PST_PROJECT_FILE, out outputDirectory)) ||
ErrorHandler.Failed(storage.GetPropertyValue("TargetFileName", null, (uint)_PersistStorageType.PST_PROJECT_FILE, out targetFileName)))
{
return false;
}
// web app case
if (!PathUtilities.IsAbsolute(outputDirectory))
{
if (this.ContainingDirectoryPathOpt == null)
{
return false;
}
outputDirectory = FileUtilities.ResolveRelativePath(outputDirectory, this.ContainingDirectoryPathOpt);
}
binOutputPath = FileUtilities.NormalizeAbsolutePath(Path.Combine(outputDirectory, targetFileName));
return true;
}
#if DEBUG
public virtual bool Debug_VBEmbeddedCoreOptionOn
{
get
{
return false;
}
}
#endif
[Conditional("DEBUG")]
private void ValidateReferences()
{
// can happen when project is unloaded and reloaded or in venus (aspx) case
if (_filePathOpt == null || _binOutputPathOpt == null || _objOutputPathOpt == null)
{
return;
}
object property = null;
if (ErrorHandler.Failed(_hierarchy.GetProperty(VSConstants.VSITEMID_ROOT, (int)__VSHPROPID.VSHPROPID_ExtObject, out property)))
{
return;
}
var dteProject = property as EnvDTE.Project;
if (dteProject == null)
{
return;
}
var vsproject = dteProject.Object as VSProject;
if (vsproject == null)
{
return;
}
var noReferenceOutputAssemblies = new List();
var factory = this.ServiceProvider.GetService(typeof(SVsEnumHierarchyItemsFactory)) as IVsEnumHierarchyItemsFactory;
IEnumHierarchyItems items;
if (ErrorHandler.Failed(factory.EnumHierarchyItems(_hierarchy, (uint)__VSEHI.VSEHI_Leaf, (uint)VSConstants.VSITEMID.Root, out items)))
{
return;
}
uint fetched;
VSITEMSELECTION[] item = new VSITEMSELECTION[1];
while (ErrorHandler.Succeeded(items.Next(1, item, out fetched)) && fetched == 1)
{
// ignore ReferenceOutputAssembly=false references since those will not be added to us in design time.
var storage = _hierarchy as IVsBuildPropertyStorage;
string value;
storage.GetItemAttribute(item[0].itemid, "ReferenceOutputAssembly", out value);
object caption;
_hierarchy.GetProperty(item[0].itemid, (int)__VSHPROPID.VSHPROPID_Caption, out caption);
if (string.Equals(value, "false", StringComparison.OrdinalIgnoreCase) ||
string.Equals(value, "off", StringComparison.OrdinalIgnoreCase) ||
string.Equals(value, "0", StringComparison.OrdinalIgnoreCase))
{
noReferenceOutputAssemblies.Add((string)caption);
}
}
var set = new HashSet(vsproject.References.OfType().Select(r => PathUtilities.IsAbsolute(r.Name) ? Path.GetFileNameWithoutExtension(r.Name) : r.Name), StringComparer.OrdinalIgnoreCase);
var delta = set.Count - noReferenceOutputAssemblies.Count - (_projectReferences.Count + _metadataReferences.Count);
if (delta == 0)
{
return;
}
// okay, two has different set of dlls referenced. check special Microsoft.VisualBasic case.
if (delta != 1)
{
//// Contract.Requires(false, "different set of references!!!");
return;
}
set.ExceptWith(noReferenceOutputAssemblies);
set.ExceptWith(_projectReferences.Select(r => ProjectTracker.GetProject(r.ProjectId).DisplayName));
set.ExceptWith(_metadataReferences.Select(m => Path.GetFileNameWithoutExtension(m.FilePath)));
//// Contract.Requires(set.Count == 1);
var reference = set.First();
if (!string.Equals(reference, "Microsoft.VisualBasic", StringComparison.OrdinalIgnoreCase))
{
//// Contract.Requires(false, "unknown new reference " + reference);
return;
}
#if DEBUG
// when we are missing microsoft.visualbasic reference, make sure we have embedded vb core option on.
Contract.Requires(Debug_VBEmbeddedCoreOptionOn);
#endif
}
///
/// Used for unit testing: don't crash the process if something bad happens.
///
internal static bool CrashOnException = true;
protected static bool FilterException(Exception e)
{
if (CrashOnException)
{
FatalError.Report(e);
}
// Nothing fancy, so don't catch
return false;
}
}
}