// 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.Linq; using System.Threading; using System.Threading.Tasks; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.Diagnostics; using Microsoft.CodeAnalysis.Host; using Microsoft.CodeAnalysis.Internal.Log; using Microsoft.CodeAnalysis.Options; using Microsoft.CodeAnalysis.Text; using Roslyn.Utilities; namespace Microsoft.CodeAnalysis { /// /// A workspace provides access to a active set of source code projects and documents and their /// associated syntax trees, compilations and semantic models. A workspace has a current solution /// that is an immutable snapshot of the projects and documents. This property may change over time /// as the workspace is updated either from live interactions in the environment or via call to the /// workspace's method. /// public abstract partial class Workspace : IDisposable { private readonly string _workspaceKind; private readonly HostWorkspaceServices _services; private readonly BranchId _primaryBranchId; // forces serialization of mutation calls from host (OnXXX methods). Must take this lock before taking stateLock. private readonly SemaphoreSlim _serializationLock = new SemaphoreSlim(initialCount: 1); // this lock guards all the mutable fields (do not share lock with derived classes) private readonly NonReentrantLock _stateLock = new NonReentrantLock(useThisInstanceForSynchronization: true); // Current solution. private Solution _latestSolution; private readonly IWorkspaceTaskScheduler _taskQueue; // test hooks. internal static bool TestHookStandaloneProjectsDoNotHoldReferences = false; internal bool TestHookPartialSolutionsDisabled { get; set; } /// /// Constructs a new workspace instance. /// /// The this workspace uses /// A string that can be used to identify the kind of workspace. Usually this matches the name of the class. protected Workspace(HostServices host, string workspaceKind) { _primaryBranchId = BranchId.GetNextId(); _workspaceKind = workspaceKind; _services = host.CreateWorkspaceServices(this); // queue used for sending events var workspaceTaskSchedulerFactory = _services.GetRequiredService(); _taskQueue = workspaceTaskSchedulerFactory.CreateTaskQueue(); // initialize with empty solution _latestSolution = CreateSolution(SolutionId.CreateNewId()); } /// /// Services provider by the host for implementing workspace features. /// public HostWorkspaceServices Services { get { return _services; } } /// /// primary branch id that current solution has /// internal BranchId PrimaryBranchId { get { return _primaryBranchId; } } /// /// Override this property if the workspace supports partial semantics for documents. /// protected internal virtual bool PartialSemanticsEnabled { get { return false; } } /// /// The kind of the workspace. /// This is generally if originating from the host environment, but may be /// any other name used for a specific kind of workspace. /// public string Kind { get { return _workspaceKind; } } /// /// Create a new empty solution instance associated with this workspace. /// protected internal Solution CreateSolution(SolutionInfo solutionInfo) { return new Solution(this, solutionInfo); } /// /// Create a new empty solution instance associated with this workspace. /// protected internal Solution CreateSolution(SolutionId id) { return CreateSolution(SolutionInfo.Create(id, VersionStamp.Create())); } /// /// The current solution. /// /// The solution is an immutable model of the current set of projects and source documents. /// It provides access to source text, syntax trees and semantics. /// /// This property may change as the workspace reacts to changes in the environment or /// after is called. /// public Solution CurrentSolution { get { return Volatile.Read(ref _latestSolution); } } /// /// Sets the of this workspace. This method does not raise a event. /// protected Solution SetCurrentSolution(Solution solution) { var currentSolution = Volatile.Read(ref _latestSolution); if (solution == currentSolution) { // No change return solution; } while (true) { var newSolution = solution.WithNewWorkspace(this, currentSolution.WorkspaceVersion + 1); var replacedSolution = Interlocked.CompareExchange(ref _latestSolution, newSolution, currentSolution); if (replacedSolution == currentSolution) { return newSolution; } currentSolution = replacedSolution; } } /// /// Gets or sets the set of all global options. /// public OptionSet Options { get { return _services.GetService().GetOptions(); } set { _services.GetService().SetOptions(value); } } /// /// Executes an action as a background task, as part of a sequential queue of tasks. /// protected internal Task ScheduleTask(Action action, string taskName = "Workspace.Task") { return _taskQueue.ScheduleTask(action, taskName); } /// /// Execute a function as a background task, as part of a sequential queue of tasks. /// protected internal Task ScheduleTask(Func func, string taskName = "Workspace.Task") { return _taskQueue.ScheduleTask(func, taskName); } /// /// Override this method to act immediately when the text of a document has changed, as opposed /// to waiting for the corresponding workspace changed event to fire asynchronously. /// protected virtual void OnDocumentTextChanged(Document document) { } /// /// Override this method to act immediately when a document is closing, as opposed /// to waiting for the corresponding workspace changed event to fire asynchronously. /// protected virtual void OnDocumentClosing(DocumentId documentId) { } /// /// Clears all solution data and empties the current solution. /// protected void ClearSolution() { var oldSolution = this.CurrentSolution; this.ClearSolutionData(); this.RaiseWorkspaceChangedEventAsync(WorkspaceChangeKind.SolutionCleared, oldSolution, this.CurrentSolution); } /// /// This method is called when a solution is cleared. /// /// Override this method if you want to do additional work when a solution is cleared. /// Call the base method at the end of your method. /// protected virtual void ClearSolutionData() { // clear any open documents this.ClearOpenDocuments(); this.SetCurrentSolution(this.CreateSolution(this.CurrentSolution.Id)); } /// /// This method is called when an individual project is removed. /// /// Override this method if you want to do additional work when a project is removed. /// Call the base method at the end of your method. /// protected virtual void ClearProjectData(ProjectId projectId) { this.ClearOpenDocuments(projectId); } /// /// This method is called to clear an individual document is removed. /// /// Override this method if you want to do additional work when a document is removed. /// Call the base method at the end of your method. /// protected virtual void ClearDocumentData(DocumentId documentId) { this.ClearOpenDocument(documentId); } /// /// Disposes this workspace. The workspace can longer be used after it is disposed. /// public void Dispose() { this.Dispose(finalize: false); } /// /// Call this method when the workspace is disposed. /// /// Override this method to do addition work when the workspace is disposed. /// Call this method at the end of your method. /// protected virtual void Dispose(bool finalize) { if (!finalize) { this.ClearSolutionData(); } } #region Host API /// /// Call this method to respond to a solution being opened in the host environment. /// protected internal void OnSolutionAdded(SolutionInfo solutionInfo) { using (_serializationLock.DisposableWait()) { var oldSolution = this.CurrentSolution; var solutionId = solutionInfo.Id; CheckSolutionIsEmpty(); this.SetCurrentSolution(this.CreateSolution(solutionInfo)); solutionInfo.Projects.Do(p => OnProjectAdded_NoLock(p, silent: true)); var newSolution = this.CurrentSolution; this.RaiseWorkspaceChangedEventAsync(WorkspaceChangeKind.SolutionAdded, oldSolution, newSolution); } } /// /// Call this method to respond to a solution being reloaded in the host environment. /// protected internal void OnSolutionReloaded(SolutionInfo reloadedSolutionInfo) { using (_serializationLock.DisposableWait()) { var oldSolution = this.CurrentSolution; var newSolution = this.SetCurrentSolution(this.CreateSolution(reloadedSolutionInfo)); reloadedSolutionInfo.Projects.Do(pi => OnProjectAdded_NoLock(pi, silent: true)); newSolution = this.AdjustReloadedSolution(oldSolution, this.CurrentSolution); newSolution = this.SetCurrentSolution(newSolution); this.RaiseWorkspaceChangedEventAsync(WorkspaceChangeKind.SolutionReloaded, oldSolution, newSolution); } } /// /// This method is called when the solution is removed from the workspace. /// /// Override this method if you want to do additional work when the solution is removed. /// Call the base method at the end of your method. /// Call this method to respond to a solution being removed/cleared/closed in the host environment. /// protected internal void OnSolutionRemoved() { using (_serializationLock.DisposableWait()) { var oldSolution = this.CurrentSolution; this.ClearSolutionData(); // reset to new empty solution this.SetCurrentSolution(this.CreateSolution(SolutionId.CreateNewId())); this.RaiseWorkspaceChangedEventAsync(WorkspaceChangeKind.SolutionRemoved, oldSolution, this.CurrentSolution); } } /// /// Call this method to respond to a project being added/opened in the host environment. /// protected internal void OnProjectAdded(ProjectInfo projectInfo) { this.OnProjectAdded(projectInfo, silent: false); } private void OnProjectAdded(ProjectInfo projectInfo, bool silent) { using (_serializationLock.DisposableWait()) { this.OnProjectAdded_NoLock(projectInfo, silent); } } private void OnProjectAdded_NoLock(ProjectInfo projectInfo, bool silent) { var projectId = projectInfo.Id; CheckProjectIsNotInCurrentSolution(projectId); var oldSolution = this.CurrentSolution; var newSolution = this.SetCurrentSolution(oldSolution.AddProject(projectInfo)); if (!silent) { this.RaiseWorkspaceChangedEventAsync(WorkspaceChangeKind.ProjectAdded, oldSolution, newSolution, projectId); } } /// /// Call this method to respond to a project being reloaded in the host environment. /// protected internal virtual void OnProjectReloaded(ProjectInfo reloadedProjectInfo) { using (_serializationLock.DisposableWait()) { var projectId = reloadedProjectInfo.Id; CheckProjectIsInCurrentSolution(projectId); var oldSolution = this.CurrentSolution; var newSolution = oldSolution.RemoveProject(projectId).AddProject(reloadedProjectInfo); newSolution = this.AdjustReloadedProject(oldSolution.GetProject(projectId), newSolution.GetProject(projectId)).Solution; newSolution = this.SetCurrentSolution(newSolution); this.RaiseWorkspaceChangedEventAsync(WorkspaceChangeKind.ProjectReloaded, oldSolution, newSolution, projectId); } } /// /// Call this method to respond to a project being removed from the host environment. /// protected internal virtual void OnProjectRemoved(ProjectId projectId) { using (_serializationLock.DisposableWait()) { CheckProjectIsInCurrentSolution(projectId); this.CheckProjectCanBeRemoved(projectId); var oldSolution = this.CurrentSolution; this.ClearProjectData(projectId); var newSolution = this.SetCurrentSolution(oldSolution.RemoveProject(projectId)); this.RaiseWorkspaceChangedEventAsync(WorkspaceChangeKind.ProjectRemoved, oldSolution, newSolution, projectId); } } protected virtual void CheckProjectCanBeRemoved(ProjectId projectId) { CheckProjectDoesNotContainOpenDocuments(projectId); } /// /// Call this method when a project's assembly name is changed in the host environment. /// protected internal void OnAssemblyNameChanged(ProjectId projectId, string assemblyName) { using (_serializationLock.DisposableWait()) { CheckProjectIsInCurrentSolution(projectId); var oldSolution = this.CurrentSolution; var newSolution = this.SetCurrentSolution(oldSolution.WithProjectAssemblyName(projectId, assemblyName)); this.RaiseWorkspaceChangedEventAsync(WorkspaceChangeKind.ProjectChanged, oldSolution, newSolution, projectId); } } /// /// Call this method when a project's output file path is changed in the host environment. /// protected internal void OnOutputFilePathChanged(ProjectId projectId, string outputFilePath) { using (_serializationLock.DisposableWait()) { CheckProjectIsInCurrentSolution(projectId); var oldSolution = this.CurrentSolution; var newSolution = this.SetCurrentSolution(oldSolution.WithProjectOutputFilePath(projectId, outputFilePath)); this.RaiseWorkspaceChangedEventAsync(WorkspaceChangeKind.ProjectChanged, oldSolution, newSolution, projectId); } } /// /// Call this method when a project's name is changed in the host environment. /// protected internal void OnProjectNameChanged(ProjectId projectId, string name, string filePath) { using (_serializationLock.DisposableWait()) { CheckProjectIsInCurrentSolution(projectId); var oldSolution = this.CurrentSolution; var newSolution = this.SetCurrentSolution(oldSolution.WithProjectName(projectId, name).WithProjectFilePath(projectId, filePath)); this.RaiseWorkspaceChangedEventAsync(WorkspaceChangeKind.ProjectChanged, oldSolution, newSolution, projectId); } } /// /// Call this method when a project's compilation options are changed in the host environment. /// protected internal void OnCompilationOptionsChanged(ProjectId projectId, CompilationOptions options) { using (_serializationLock.DisposableWait()) { CheckProjectIsInCurrentSolution(projectId); var oldSolution = this.CurrentSolution; var newSolution = this.SetCurrentSolution(oldSolution.WithProjectCompilationOptions(projectId, options)); this.RaiseWorkspaceChangedEventAsync(WorkspaceChangeKind.ProjectChanged, oldSolution, newSolution, projectId); } } /// /// Call this method when a project's parse options are changed in the host environment. /// protected internal void OnParseOptionsChanged(ProjectId projectId, ParseOptions options) { using (_serializationLock.DisposableWait()) { CheckProjectIsInCurrentSolution(projectId); var oldSolution = this.CurrentSolution; var newSolution = this.SetCurrentSolution(oldSolution.WithProjectParseOptions(projectId, options)); this.RaiseWorkspaceChangedEventAsync(WorkspaceChangeKind.ProjectChanged, oldSolution, newSolution, projectId); } } /// /// Call this method when a project reference is added to a project in the host environment. /// protected internal void OnProjectReferenceAdded(ProjectId projectId, ProjectReference projectReference) { using (_serializationLock.DisposableWait()) { CheckProjectIsInCurrentSolution(projectId); CheckProjectIsInCurrentSolution(projectReference.ProjectId); CheckProjectDoesNotHaveProjectReference(projectId, projectReference); // Can only add this P2P reference if it would not cause a circularity. CheckProjectDoesNotHaveTransitiveProjectReference(projectId, projectReference.ProjectId); var oldSolution = this.CurrentSolution; var newSolution = this.SetCurrentSolution(oldSolution.AddProjectReference(projectId, projectReference)); this.RaiseWorkspaceChangedEventAsync(WorkspaceChangeKind.ProjectChanged, oldSolution, newSolution, projectId); } } /// /// Call this method when a project reference is removed from a project in the host environment. /// protected internal void OnProjectReferenceRemoved(ProjectId projectId, ProjectReference projectReference) { using (_serializationLock.DisposableWait()) { CheckProjectIsInCurrentSolution(projectId); CheckProjectIsInCurrentSolution(projectReference.ProjectId); CheckProjectHasProjectReference(projectId, projectReference); var oldSolution = this.CurrentSolution; var newSolution = this.SetCurrentSolution(oldSolution.RemoveProjectReference(projectId, projectReference)); this.RaiseWorkspaceChangedEventAsync(WorkspaceChangeKind.ProjectChanged, oldSolution, newSolution, projectId); } } /// /// Call this method when a metadata reference is added to a project in the host environment. /// protected internal void OnMetadataReferenceAdded(ProjectId projectId, MetadataReference metadataReference) { using (_serializationLock.DisposableWait()) { CheckProjectIsInCurrentSolution(projectId); CheckProjectDoesNotHaveMetadataReference(projectId, metadataReference); var oldSolution = this.CurrentSolution; var newSolution = this.SetCurrentSolution(oldSolution.AddMetadataReference(projectId, metadataReference)); this.RaiseWorkspaceChangedEventAsync(WorkspaceChangeKind.ProjectChanged, oldSolution, newSolution, projectId); } } /// /// Call this method when a metadata reference is removed from a project in the host environment. /// protected internal void OnMetadataReferenceRemoved(ProjectId projectId, MetadataReference metadataReference) { using (_serializationLock.DisposableWait()) { CheckProjectIsInCurrentSolution(projectId); CheckProjectHasMetadataReference(projectId, metadataReference); var oldSolution = this.CurrentSolution; var newSolution = this.SetCurrentSolution(oldSolution.RemoveMetadataReference(projectId, metadataReference)); this.RaiseWorkspaceChangedEventAsync(WorkspaceChangeKind.ProjectChanged, oldSolution, newSolution, projectId); } } /// /// Call this method when an analyzer reference is added to a project in the host environment. /// protected internal void OnAnalyzerReferenceAdded(ProjectId projectId, AnalyzerReference analyzerReference) { using (_serializationLock.DisposableWait()) { CheckProjectIsInCurrentSolution(projectId); CheckProjectDoesNotHaveAnalyzerReference(projectId, analyzerReference); var oldSolution = this.CurrentSolution; var newSolution = this.SetCurrentSolution(oldSolution.AddAnalyzerReference(projectId, analyzerReference)); this.RaiseWorkspaceChangedEventAsync(WorkspaceChangeKind.ProjectChanged, oldSolution, newSolution, projectId); } } /// /// Call this method when an analyzer reference is removed from a project in the host environment. /// protected internal void OnAnalyzerReferenceRemoved(ProjectId projectId, AnalyzerReference analyzerReference) { using (_serializationLock.DisposableWait()) { CheckProjectIsInCurrentSolution(projectId); CheckProjectHasAnalyzerReference(projectId, analyzerReference); var oldSolution = this.CurrentSolution; var newSolution = this.SetCurrentSolution(oldSolution.RemoveAnalyzerReference(projectId, analyzerReference)); this.RaiseWorkspaceChangedEventAsync(WorkspaceChangeKind.ProjectChanged, oldSolution, newSolution, projectId); } } /// /// Call this method when a document is added to a project in the host environment. /// protected internal void OnDocumentAdded(DocumentInfo documentInfo) { using (_serializationLock.DisposableWait()) { var documentId = documentInfo.Id; CheckProjectIsInCurrentSolution(documentId.ProjectId); CheckDocumentIsNotInCurrentSolution(documentId); var oldSolution = this.CurrentSolution; var newSolution = this.SetCurrentSolution(oldSolution.AddDocument(documentInfo)); this.RaiseWorkspaceChangedEventAsync(WorkspaceChangeKind.DocumentAdded, oldSolution, newSolution, documentId: documentId); } } /// /// Call this method when a document is reloaded in the host environment. /// protected internal void OnDocumentReloaded(DocumentInfo newDocumentInfo) { using (_serializationLock.DisposableWait()) { var documentId = newDocumentInfo.Id; CheckProjectIsInCurrentSolution(documentId.ProjectId); CheckDocumentIsInCurrentSolution(documentId); var oldSolution = this.CurrentSolution; var newSolution = this.SetCurrentSolution(oldSolution.RemoveDocument(documentId).AddDocument(newDocumentInfo)); this.RaiseWorkspaceChangedEventAsync(WorkspaceChangeKind.DocumentReloaded, oldSolution, newSolution, documentId: documentId); } } /// /// Call this method when a document is removed from a project in the host environment. /// protected internal void OnDocumentRemoved(DocumentId documentId) { using (_serializationLock.DisposableWait()) { CheckDocumentIsInCurrentSolution(documentId); this.CheckDocumentCanBeRemoved(documentId); var oldSolution = this.CurrentSolution; this.ClearDocumentData(documentId); var newSolution = this.SetCurrentSolution(oldSolution.RemoveDocument(documentId)); this.RaiseWorkspaceChangedEventAsync(WorkspaceChangeKind.DocumentRemoved, oldSolution, newSolution, documentId: documentId); } } protected virtual void CheckDocumentCanBeRemoved(DocumentId documentId) { CheckDocumentIsClosed(documentId); } /// /// Call this method when the text of a document is changed on disk. /// protected internal void OnDocumentTextLoaderChanged(DocumentId documentId, TextLoader loader) { using (_serializationLock.DisposableWait()) { CheckDocumentIsInCurrentSolution(documentId); var oldSolution = this.CurrentSolution; var newSolution = oldSolution.WithDocumentTextLoader(documentId, loader, PreservationMode.PreserveValue); newSolution = this.SetCurrentSolution(newSolution); var newDocument = newSolution.GetDocument(documentId); this.OnDocumentTextChanged(newDocument); this.RaiseWorkspaceChangedEventAsync(WorkspaceChangeKind.DocumentChanged, oldSolution, newSolution, documentId: documentId); } } /// /// Call this method when the text of a document is changed on disk. /// protected internal void OnAdditionalDocumentTextLoaderChanged(DocumentId documentId, TextLoader loader) { using (_serializationLock.DisposableWait()) { CheckAdditionalDocumentIsInCurrentSolution(documentId); var oldSolution = this.CurrentSolution; var newSolution = oldSolution.WithAdditionalDocumentTextLoader(documentId, loader, PreservationMode.PreserveValue); newSolution = this.SetCurrentSolution(newSolution); this.RaiseWorkspaceChangedEventAsync(WorkspaceChangeKind.AdditionalDocumentChanged, oldSolution, newSolution, documentId: documentId); } } /// /// Call this method when status of project has changed to incomplete. /// See for more information. /// // TODO: make it public internal void OnHasAllInformationChanged(ProjectId projectId, bool hasAllInformation) { using (_serializationLock.DisposableWait()) { CheckProjectIsInCurrentSolution(projectId); // if state is different than what we have var oldSolution = this.CurrentSolution; var newSolution = this.SetCurrentSolution(oldSolution.WithHasAllInformation(projectId, hasAllInformation)); this.RaiseWorkspaceChangedEventAsync(WorkspaceChangeKind.ProjectChanged, oldSolution, newSolution, projectId); } } /// /// Call this method when the text of a document is updated in the host environment. /// protected internal void OnDocumentTextChanged(DocumentId documentId, SourceText newText, PreservationMode mode) { using (_serializationLock.DisposableWait()) { CheckDocumentIsInCurrentSolution(documentId); var oldSolution = this.CurrentSolution; var newSolution = this.SetCurrentSolution(oldSolution.WithDocumentText(documentId, newText, mode)); var newDocument = newSolution.GetDocument(documentId); this.OnDocumentTextChanged(newDocument); this.RaiseWorkspaceChangedEventAsync(WorkspaceChangeKind.DocumentChanged, oldSolution, newSolution, documentId: documentId); } } /// /// Call this method when the text of a document is updated in the host environment. /// protected internal void OnAdditionalDocumentTextChanged(DocumentId documentId, SourceText newText, PreservationMode mode) { using (_serializationLock.DisposableWait()) { CheckAdditionalDocumentIsInCurrentSolution(documentId); var oldSolution = this.CurrentSolution; var newSolution = this.SetCurrentSolution(oldSolution.WithAdditionalDocumentText(documentId, newText, mode)); var newDocument = newSolution.GetAdditionalDocument(documentId); this.RaiseWorkspaceChangedEventAsync(WorkspaceChangeKind.AdditionalDocumentChanged, oldSolution, newSolution, documentId: documentId); } } /// /// Call this method when the SourceCodeKind of a document changes in the host environment. /// protected internal void OnDocumentSourceCodeKindChanged(DocumentId documentId, SourceCodeKind sourceCodeKind) { using (_serializationLock.DisposableWait()) { CheckDocumentIsInCurrentSolution(documentId); var oldSolution = this.CurrentSolution; var newSolution = this.SetCurrentSolution(oldSolution.WithDocumentSourceCodeKind(documentId, sourceCodeKind)); var newDocument = newSolution.GetDocument(documentId); this.OnDocumentTextChanged(newDocument); this.RaiseWorkspaceChangedEventAsync(WorkspaceChangeKind.DocumentChanged, oldSolution, newSolution, documentId: documentId); } } /// /// Call this method when an additional document is added to a project in the host environment. /// protected internal void OnAdditionalDocumentAdded(DocumentInfo documentInfo) { using (_serializationLock.DisposableWait()) { var documentId = documentInfo.Id; CheckProjectIsInCurrentSolution(documentId.ProjectId); CheckDocumentIsNotInCurrentSolution(documentId); var oldSolution = this.CurrentSolution; var newSolution = this.SetCurrentSolution(oldSolution.AddAdditionalDocument(documentInfo)); this.RaiseWorkspaceChangedEventAsync(WorkspaceChangeKind.AdditionalDocumentAdded, oldSolution, newSolution, documentId: documentId); } } /// /// Call this method when an additional document is removed from a project in the host environment. /// protected internal void OnAdditionalDocumentRemoved(DocumentId documentId) { using (_serializationLock.DisposableWait()) { CheckAdditionalDocumentIsInCurrentSolution(documentId); this.CheckDocumentCanBeRemoved(documentId); var oldSolution = this.CurrentSolution; this.ClearDocumentData(documentId); var newSolution = this.SetCurrentSolution(oldSolution.RemoveAdditionalDocument(documentId)); this.RaiseWorkspaceChangedEventAsync(WorkspaceChangeKind.AdditionalDocumentRemoved, oldSolution, newSolution, documentId: documentId); } } /// /// Updates all projects to properly reference other projects as project references instead of metadata references. /// protected void UpdateReferencesAfterAdd() { using (_serializationLock.DisposableWait()) { var oldSolution = this.CurrentSolution; var newSolution = this.UpdateReferencesAfterAdd(oldSolution); if (newSolution != oldSolution) { newSolution = this.SetCurrentSolution(newSolution); var ignore = this.RaiseWorkspaceChangedEventAsync(WorkspaceChangeKind.SolutionChanged, oldSolution, newSolution); } } } private Solution UpdateReferencesAfterAdd(Solution solution) { // Build map from output assembly path to ProjectId // Use explicit loop instead of ToDictionary so we don't throw if multiple projects have same output assembly path. var outputAssemblyToProjectIdMap = new Dictionary(); foreach (var p in solution.Projects) { if (!string.IsNullOrEmpty(p.OutputFilePath)) { outputAssemblyToProjectIdMap[p.OutputFilePath] = p.Id; } } // now fix each project if necessary foreach (var pid in solution.ProjectIds) { var project = solution.GetProject(pid); // convert metadata references to project references if the metadata reference matches some project's output assembly. foreach (var meta in project.MetadataReferences) { var pemeta = meta as PortableExecutableReference; if (pemeta != null) { ProjectId matchingProjectId; // check both Display and FilePath. FilePath points to the actually bits, but Display should match output path if // the metadata reference is shadow copied. if ((!string.IsNullOrEmpty(pemeta.Display) && outputAssemblyToProjectIdMap.TryGetValue(pemeta.Display, out matchingProjectId)) || (!string.IsNullOrEmpty(pemeta.FilePath) && outputAssemblyToProjectIdMap.TryGetValue(pemeta.FilePath, out matchingProjectId))) { var newProjRef = new ProjectReference(matchingProjectId, pemeta.Properties.Aliases, pemeta.Properties.EmbedInteropTypes); if (!project.ProjectReferences.Contains(newProjRef)) { project = project.WithProjectReferences(project.ProjectReferences.Concat(newProjRef)); } project = project.WithMetadataReferences(project.MetadataReferences.Where(mr => mr != meta)); } } } solution = project.Solution; } return solution; } #endregion #region Apply Changes /// /// Determines if the specific kind of change is supported by the method. /// public virtual bool CanApplyChange(ApplyChangesKind feature) { return false; } /// /// Returns true if a reference to referencedProject can be added to /// referencingProject. false otherwise. /// internal virtual bool CanAddProjectReference(ProjectId referencingProject, ProjectId referencedProject) { return false; } /// /// Apply changes made to a solution back to the workspace. /// /// The specified solution must be one that originated from this workspace. If it is not, or the workspace /// has been updated since the solution was obtained from the workspace, then this method returns false. This method /// will still throw if the solution contains changes that are not supported according to the /// method. /// /// Thrown if the solution contains changes not supported according to the /// method. public virtual bool TryApplyChanges(Solution newSolution) { using (Logger.LogBlock(FunctionId.Workspace_ApplyChanges, CancellationToken.None)) { // If solution did not originate from this workspace then fail if (newSolution.Workspace != this) { return false; } Solution oldSolution = this.CurrentSolution; // If the workspace has already accepted an update, then fail if (newSolution.WorkspaceVersion != oldSolution.WorkspaceVersion) { return false; } // make sure that newSolution is a branch of the current solution // the given solution must be a branched one. // otherwise, there should be no change to apply. if (oldSolution.BranchId == newSolution.BranchId) { CheckNoChanges(oldSolution, newSolution); return true; } var solutionChanges = newSolution.GetChanges(oldSolution); this.CheckAllowedSolutionChanges(solutionChanges); var solutionWithLinkedFileChangesMerged = newSolution.WithMergedLinkedFileChangesAsync(oldSolution, solutionChanges, cancellationToken: CancellationToken.None).Result; solutionChanges = solutionWithLinkedFileChangesMerged.GetChanges(oldSolution); // added projects foreach (var proj in solutionChanges.GetAddedProjects()) { this.ApplyProjectAdded(this.CreateProjectInfo(proj)); } // changed projects foreach (var projectChanges in solutionChanges.GetProjectChanges()) { this.ApplyProjectChanges(projectChanges); } // removed projects foreach (var proj in solutionChanges.GetRemovedProjects()) { this.ApplyProjectRemoved(proj.Id); } return true; } } private void CheckAllowedSolutionChanges(SolutionChanges solutionChanges) { if (solutionChanges.GetRemovedProjects().Any() && !this.CanApplyChange(ApplyChangesKind.RemoveProject)) { throw new NotSupportedException(WorkspacesResources.RemovingProjectsNotSupported); } if (solutionChanges.GetAddedProjects().Any() && !this.CanApplyChange(ApplyChangesKind.AddProject)) { throw new NotSupportedException(WorkspacesResources.AddingProjectsNotSupported); } foreach (var projectChanges in solutionChanges.GetProjectChanges()) { this.CheckAllowedProjectChanges(projectChanges); } } private void CheckAllowedProjectChanges(ProjectChanges projectChanges) { if (projectChanges.OldProject.CompilationOptions != projectChanges.NewProject.CompilationOptions && !this.CanApplyChange(ApplyChangesKind.ChangeCompilationOptions)) { throw new NotSupportedException(WorkspacesResources.ChangingCompilationOptionsNotSupported); } if (projectChanges.OldProject.ParseOptions != projectChanges.NewProject.ParseOptions && !this.CanApplyChange(ApplyChangesKind.ChangeParseOptions)) { throw new NotSupportedException(WorkspacesResources.ChangingParseOptionsNotSupported); } if (projectChanges.GetAddedDocuments().Any() && !this.CanApplyChange(ApplyChangesKind.AddDocument)) { throw new NotSupportedException(WorkspacesResources.AddingDocumentsNotSupported); } if (projectChanges.GetRemovedDocuments().Any() && !this.CanApplyChange(ApplyChangesKind.RemoveDocument)) { throw new NotSupportedException(WorkspacesResources.RemovingDocumentsNotSupported); } if (projectChanges.GetChangedDocuments().Any() && !this.CanApplyChange(ApplyChangesKind.ChangeDocument)) { throw new NotSupportedException(WorkspacesResources.ChangingDocumentsNotSupported); } if (projectChanges.GetAddedAdditionalDocuments().Any() && !this.CanApplyChange(ApplyChangesKind.AddAdditionalDocument)) { throw new NotSupportedException(WorkspacesResources.AddingAdditionalDocumentsNotSupported); } if (projectChanges.GetRemovedAdditionalDocuments().Any() && !this.CanApplyChange(ApplyChangesKind.RemoveAdditionalDocument)) { throw new NotSupportedException(WorkspacesResources.RemovingAdditionalDocumentsIsNotSupported); } if (projectChanges.GetChangedAdditionalDocuments().Any() && !this.CanApplyChange(ApplyChangesKind.ChangeAdditionalDocument)) { throw new NotSupportedException(WorkspacesResources.ChangingAdditionalDocumentsIsNotSupported); } if (projectChanges.GetAddedProjectReferences().Any() && !this.CanApplyChange(ApplyChangesKind.AddProjectReference)) { throw new NotSupportedException(WorkspacesResources.AddingProjectReferencesNotSupported); } if (projectChanges.GetRemovedProjectReferences().Any() && !this.CanApplyChange(ApplyChangesKind.RemoveProjectReference)) { throw new NotSupportedException(WorkspacesResources.RemovingProjectReferencesNotSupported); } if (projectChanges.GetAddedMetadataReferences().Any() && !this.CanApplyChange(ApplyChangesKind.AddMetadataReference)) { throw new NotSupportedException(WorkspacesResources.AddingProjectReferencesNotSupported); } if (projectChanges.GetRemovedMetadataReferences().Any() && !this.CanApplyChange(ApplyChangesKind.RemoveMetadataReference)) { throw new NotSupportedException(WorkspacesResources.RemovingProjectReferencesNotSupported); } if (projectChanges.GetAddedAnalyzerReferences().Any() && !this.CanApplyChange(ApplyChangesKind.AddAnalyzerReference)) { throw new NotSupportedException(WorkspacesResources.AddingAnalyzerReferencesNotSupported); } if (projectChanges.GetRemovedAnalyzerReferences().Any() && !this.CanApplyChange(ApplyChangesKind.RemoveAnalyzerReference)) { throw new NotSupportedException(WorkspacesResources.RemovingAnalyzerReferencesNotSupported); } } /// /// This method is called during for each project that has been added, removed or changed. /// /// Override this method if you want to modify how project changes are applied. /// protected virtual void ApplyProjectChanges(ProjectChanges projectChanges) { // changed compilation options if (projectChanges.OldProject.CompilationOptions != projectChanges.NewProject.CompilationOptions) { this.ApplyCompilationOptionsChanged(projectChanges.ProjectId, projectChanges.NewProject.CompilationOptions); } // changed parse options if (projectChanges.OldProject.ParseOptions != projectChanges.NewProject.ParseOptions) { this.ApplyParseOptionsChanged(projectChanges.ProjectId, projectChanges.NewProject.ParseOptions); } // removed project references foreach (var removedProjectReference in projectChanges.GetRemovedProjectReferences()) { this.ApplyProjectReferenceRemoved(projectChanges.ProjectId, removedProjectReference); } // added project references foreach (var addedProjectReference in projectChanges.GetAddedProjectReferences()) { this.ApplyProjectReferenceAdded(projectChanges.ProjectId, addedProjectReference); } // removed metadata references foreach (var metadata in projectChanges.GetRemovedMetadataReferences()) { this.ApplyMetadataReferenceRemoved(projectChanges.ProjectId, metadata); } // added metadata references foreach (var metadata in projectChanges.GetAddedMetadataReferences()) { this.ApplyMetadataReferenceAdded(projectChanges.ProjectId, metadata); } // removed analyzer references foreach (var analyzerReference in projectChanges.GetRemovedAnalyzerReferences()) { this.ApplyAnalyzerReferenceRemoved(projectChanges.ProjectId, analyzerReference); } // added analyzer references foreach (var analyzerReference in projectChanges.GetAddedAnalyzerReferences()) { this.ApplyAnalyzerReferenceAdded(projectChanges.ProjectId, analyzerReference); } // removed documents foreach (var documentId in projectChanges.GetRemovedDocuments()) { this.ApplyDocumentRemoved(documentId); } // removed additional documents foreach (var documentId in projectChanges.GetRemovedAdditionalDocuments()) { this.ApplyAdditionalDocumentRemoved(documentId); } // added documents foreach (var documentId in projectChanges.GetAddedDocuments()) { var doc = projectChanges.NewProject.GetDocument(documentId); var text = this.GetTextForced(doc); var info = this.CreateDocumentInfoWithoutText(doc); this.ApplyDocumentAdded(info, text); } // added additional documents foreach (var documentId in projectChanges.GetAddedAdditionalDocuments()) { var doc = projectChanges.NewProject.GetAdditionalDocument(documentId); var text = this.GetTextForced(doc); var info = this.CreateDocumentInfoWithoutText(doc); this.ApplyAdditionalDocumentAdded(info, text); } // changed documents foreach (var documentId in projectChanges.GetChangedDocuments()) { var oldDoc = projectChanges.OldProject.GetDocument(documentId); var newDoc = projectChanges.NewProject.GetDocument(documentId); // see whether we can get oldText SourceText oldText; if (!oldDoc.TryGetText(out oldText)) { // we can't get old text, there is not much we can do except replacing whole text. var currentText = newDoc.GetTextAsync(CancellationToken.None).WaitAndGetResult_CanCallOnBackground(CancellationToken.None); // needs wait this.ApplyDocumentTextChanged(documentId, currentText); continue; } // see whether we can get new text SourceText newText; if (!newDoc.TryGetText(out newText)) { // okay, we have old text, but no new text. let document determine text changes var textChanges = newDoc.GetTextChangesAsync(oldDoc, CancellationToken.None).WaitAndGetResult_CanCallOnBackground(CancellationToken.None); // needs wait this.ApplyDocumentTextChanged(documentId, oldText.WithChanges(textChanges)); continue; } // we have both old and new text, just update using the new text. this.ApplyDocumentTextChanged(documentId, newText); } // changed additional documents foreach (var documentId in projectChanges.GetChangedAdditionalDocuments()) { var oldDoc = projectChanges.OldProject.GetAdditionalDocument(documentId); var newDoc = projectChanges.NewProject.GetAdditionalDocument(documentId); // We don't understand the text of additional documents and so we just replace the entire text. var currentText = newDoc.GetTextAsync(CancellationToken.None).WaitAndGetResult_CanCallOnBackground(CancellationToken.None); // needs wait this.ApplyAdditionalDocumentTextChanged(documentId, currentText); } } [Conditional("DEBUG")] private void CheckNoChanges(Solution oldSolution, Solution newSolution) { var changes = newSolution.GetChanges(oldSolution); Contract.ThrowIfTrue(changes.GetAddedProjects().Any()); Contract.ThrowIfTrue(changes.GetRemovedProjects().Any()); Contract.ThrowIfTrue(changes.GetProjectChanges().Any()); } private ProjectInfo CreateProjectInfo(Project project) { return ProjectInfo.Create( project.Id, VersionStamp.Create(), project.Name, project.AssemblyName, project.Language, project.FilePath, project.OutputFilePath, project.CompilationOptions, project.ParseOptions, project.Documents.Select(d => CreateDocumentInfoWithText(d)), project.ProjectReferences, project.MetadataReferences, project.AnalyzerReferences, project.AdditionalDocuments.Select(d => CreateDocumentInfoWithText(d))); } private SourceText GetTextForced(TextDocument doc) { return doc.GetTextAsync(CancellationToken.None).WaitAndGetResult_CanCallOnBackground(CancellationToken.None); // needs wait (called during TryApplyChanges) } private DocumentInfo CreateDocumentInfoWithText(TextDocument doc) { return CreateDocumentInfoWithoutText(doc).WithTextLoader(TextLoader.From(TextAndVersion.Create(GetTextForced(doc), VersionStamp.Create(), doc.FilePath))); } private DocumentInfo CreateDocumentInfoWithoutText(TextDocument doc) { var sourceDoc = doc as Document; return DocumentInfo.Create( doc.Id, doc.Name, doc.Folders, sourceDoc != null ? sourceDoc.SourceCodeKind : SourceCodeKind.Regular, filePath: doc.FilePath); } /// /// This method is called during to add a project to the current solution. /// /// Override this method to implement the capability of adding projects. /// protected virtual void ApplyProjectAdded(ProjectInfo project) { Debug.Assert(CanApplyChange(ApplyChangesKind.AddProject)); this.OnProjectAdded(project); } /// /// This method is called during to remove a project from the current solution. /// /// Override this method to implement the capability of removing projects. /// protected virtual void ApplyProjectRemoved(ProjectId projectId) { Debug.Assert(CanApplyChange(ApplyChangesKind.RemoveProject)); this.OnProjectRemoved(projectId); } /// /// This method is called during to change the compilation options. /// /// Override this method to implement the capability of changing compilation options. /// protected virtual void ApplyCompilationOptionsChanged(ProjectId projectId, CompilationOptions options) { Debug.Assert(CanApplyChange(ApplyChangesKind.ChangeCompilationOptions)); this.OnCompilationOptionsChanged(projectId, options); } /// /// This method is called during to change the parse options. /// /// Override this method to implement the capability of changing parse options. /// protected virtual void ApplyParseOptionsChanged(ProjectId projectId, ParseOptions options) { Debug.Assert(CanApplyChange(ApplyChangesKind.ChangeParseOptions)); this.OnParseOptionsChanged(projectId, options); } /// /// This method is called during to add a project reference to a project. /// /// Override this method to implement the capability of adding project references. /// protected virtual void ApplyProjectReferenceAdded(ProjectId projectId, ProjectReference projectReference) { Debug.Assert(CanApplyChange(ApplyChangesKind.AddProjectReference)); this.OnProjectReferenceAdded(projectId, projectReference); } /// /// This method is called during to remove a project reference from a project. /// /// Override this method to implement the capability of removing project references. /// protected virtual void ApplyProjectReferenceRemoved(ProjectId projectId, ProjectReference projectReference) { Debug.Assert(CanApplyChange(ApplyChangesKind.RemoveProjectReference)); this.OnProjectReferenceRemoved(projectId, projectReference); } /// /// This method is called during to add a metadata reference to a project. /// /// Override this method to implement the capability of adding metadata references. /// protected virtual void ApplyMetadataReferenceAdded(ProjectId projectId, MetadataReference metadataReference) { Debug.Assert(CanApplyChange(ApplyChangesKind.AddMetadataReference)); this.OnMetadataReferenceAdded(projectId, metadataReference); } /// /// This method is called during to remove a metadata reference from a project. /// /// Override this method to implement the capability of removing metadata references. /// protected virtual void ApplyMetadataReferenceRemoved(ProjectId projectId, MetadataReference metadataReference) { Debug.Assert(CanApplyChange(ApplyChangesKind.RemoveMetadataReference)); this.OnMetadataReferenceRemoved(projectId, metadataReference); } /// /// This method is called during to add an analyzer reference to a project. /// /// Override this method to implement the capability of adding analyzer references. /// protected virtual void ApplyAnalyzerReferenceAdded(ProjectId projectId, AnalyzerReference analyzerReference) { Debug.Assert(CanApplyChange(ApplyChangesKind.AddAnalyzerReference)); this.OnAnalyzerReferenceAdded(projectId, analyzerReference); } /// /// This method is called during to remove an analyzer reference from a project. /// /// Override this method to implement the capability of removing analyzer references. /// protected virtual void ApplyAnalyzerReferenceRemoved(ProjectId projectId, AnalyzerReference analyzerReference) { Debug.Assert(CanApplyChange(ApplyChangesKind.RemoveAnalyzerReference)); this.OnAnalyzerReferenceRemoved(projectId, analyzerReference); } /// /// This method is called during to add a new document to a project. /// /// Override this method to implement the capability of adding documents. /// protected virtual void ApplyDocumentAdded(DocumentInfo info, SourceText text) { Debug.Assert(CanApplyChange(ApplyChangesKind.AddDocument)); this.OnDocumentAdded(info.WithTextLoader(TextLoader.From(TextAndVersion.Create(text, VersionStamp.Create())))); } /// /// This method is called during to remove a document from a project. /// /// Override this method to implement the capability of removing documents. /// protected virtual void ApplyDocumentRemoved(DocumentId documentId) { Debug.Assert(CanApplyChange(ApplyChangesKind.RemoveDocument)); this.OnDocumentRemoved(documentId); } /// /// This method is called to change the text of a document. /// /// Override this method to implement the capability of changing document text. /// protected virtual void ApplyDocumentTextChanged(DocumentId id, SourceText text) { Debug.Assert(CanApplyChange(ApplyChangesKind.ChangeDocument)); this.OnDocumentTextChanged(id, text, PreservationMode.PreserveValue); } /// /// This method is called during to add a new additional document to a project. /// /// Override this method to implement the capability of adding additional documents. /// protected virtual void ApplyAdditionalDocumentAdded(DocumentInfo info, SourceText text) { Debug.Assert(CanApplyChange(ApplyChangesKind.AddAdditionalDocument)); this.OnAdditionalDocumentAdded(info.WithTextLoader(TextLoader.From(TextAndVersion.Create(text, VersionStamp.Create())))); } /// /// This method is called during to remove an additional document from a project. /// /// Override this method to implement the capability of removing additional documents. /// protected virtual void ApplyAdditionalDocumentRemoved(DocumentId documentId) { Debug.Assert(CanApplyChange(ApplyChangesKind.RemoveAdditionalDocument)); this.OnAdditionalDocumentRemoved(documentId); } /// /// This method is called to change the text of an additional document. /// /// Override this method to implement the capability of changing additional document text. /// protected virtual void ApplyAdditionalDocumentTextChanged(DocumentId id, SourceText text) { Debug.Assert(CanApplyChange(ApplyChangesKind.ChangeAdditionalDocument)); this.OnAdditionalDocumentTextChanged(id, text, PreservationMode.PreserveValue); } #endregion #region Checks and Asserts /// /// Throws an exception is the solution is not empty. /// protected void CheckSolutionIsEmpty() { if (this.CurrentSolution.ProjectIds.Any()) { throw new ArgumentException(WorkspacesResources.WorkspaceIsNotEmpty); } } /// /// Throws an exception if the project is not part of the current solution. /// protected void CheckProjectIsInCurrentSolution(ProjectId projectId) { if (!this.CurrentSolution.ContainsProject(projectId)) { throw new ArgumentException(string.Format( WorkspacesResources.ProjectOrDocumentNotInWorkspace, this.GetProjectName(projectId))); } } /// /// Throws an exception is the project is part of the current solution. /// protected void CheckProjectIsNotInCurrentSolution(ProjectId projectId) { if (this.CurrentSolution.ContainsProject(projectId)) { throw new ArgumentException(string.Format( WorkspacesResources.ProjectOrDocumentAlreadyInWorkspace, this.GetProjectName(projectId))); } } /// /// Throws an exception if a project does not have a specific project reference. /// protected void CheckProjectHasProjectReference(ProjectId fromProjectId, ProjectReference projectReference) { if (!this.CurrentSolution.GetProject(fromProjectId).ProjectReferences.Contains(projectReference)) { throw new ArgumentException(string.Format( WorkspacesResources.ProjectNotReferenced, this.GetProjectName(projectReference.ProjectId))); } } /// /// Throws an exception if a project already has a specific project reference. /// protected void CheckProjectDoesNotHaveProjectReference(ProjectId fromProjectId, ProjectReference projectReference) { if (this.CurrentSolution.GetProject(fromProjectId).ProjectReferences.Contains(projectReference)) { throw new ArgumentException(string.Format( WorkspacesResources.ProjectAlreadyReferenced, this.GetProjectName(projectReference.ProjectId))); } } /// /// Throws an exception if project has a transitive reference to another project. /// protected void CheckProjectDoesNotHaveTransitiveProjectReference(ProjectId fromProjectId, ProjectId toProjectId) { var transitiveReferences = GetTransitiveProjectReferences(toProjectId); if (transitiveReferences.Contains(fromProjectId)) { throw new ArgumentException(string.Format( WorkspacesResources.CausesCircularProjectReference, this.GetProjectName(fromProjectId), this.GetProjectName(toProjectId))); } } private ISet GetTransitiveProjectReferences(ProjectId project, ISet projects = null) { projects = projects ?? new HashSet(); if (projects.Add(project)) { this.CurrentSolution.GetProject(project).ProjectReferences.Do(p => GetTransitiveProjectReferences(p.ProjectId, projects)); } return projects; } /// /// Throws an exception if a project does not have a specific metadata reference. /// protected void CheckProjectHasMetadataReference(ProjectId projectId, MetadataReference metadataReference) { if (!this.CurrentSolution.GetProject(projectId).MetadataReferences.Contains(metadataReference)) { throw new ArgumentException(WorkspacesResources.MetadataIsNotReferenced); } } /// /// Throws an exception if a project already has a specific metadata reference. /// protected void CheckProjectDoesNotHaveMetadataReference(ProjectId projectId, MetadataReference metadataReference) { if (this.CurrentSolution.GetProject(projectId).MetadataReferences.Contains(metadataReference)) { throw new ArgumentException(WorkspacesResources.MetadataIsAlreadyReferenced); } } /// /// Throws an exception if a project does not have a specific analyzer reference. /// protected void CheckProjectHasAnalyzerReference(ProjectId projectId, AnalyzerReference analyzerReference) { if (!this.CurrentSolution.GetProject(projectId).AnalyzerReferences.Contains(analyzerReference)) { throw new ArgumentException(string.Format(WorkspacesResources.AnalyzerIsNotPresent, analyzerReference)); } } /// /// Throws an exception if a project already has a specific analyzer reference. /// protected void CheckProjectDoesNotHaveAnalyzerReference(ProjectId projectId, AnalyzerReference analyzerReference) { if (this.CurrentSolution.GetProject(projectId).AnalyzerReferences.Contains(analyzerReference)) { throw new ArgumentException(string.Format(WorkspacesResources.AnalyzerIsAlreadyPresent, analyzerReference)); } } /// /// Throws an exception if a document is not part of the current solution. /// protected void CheckDocumentIsInCurrentSolution(DocumentId documentId) { if (this.CurrentSolution.GetDocument(documentId) == null) { throw new ArgumentException(string.Format( WorkspacesResources.ProjectOrDocumentNotInWorkspace, this.GetDocumentName(documentId))); } } /// /// Throws an exception if an additional document is not part of the current solution. /// protected void CheckAdditionalDocumentIsInCurrentSolution(DocumentId documentId) { if (this.CurrentSolution.GetAdditionalDocument(documentId) == null) { throw new ArgumentException(string.Format( WorkspacesResources.ProjectOrDocumentNotInWorkspace, this.GetDocumentName(documentId))); } } /// /// Throws an exception if a document is already part of the current solution. /// protected void CheckDocumentIsNotInCurrentSolution(DocumentId documentId) { if (this.CurrentSolution.ContainsDocument(documentId)) { throw new ArgumentException(string.Format( WorkspacesResources.ProjectOrDocumentAlreadyInWorkspace, this.GetDocumentName(documentId))); } } /// /// Throws an exception if an additional document is already part of the current solution. /// protected void CheckAdditionalDocumentIsNotInCurrentSolution(DocumentId documentId) { if (this.CurrentSolution.ContainsAdditionalDocument(documentId)) { throw new ArgumentException(string.Format( WorkspacesResources.ProjectOrDocumentAlreadyInWorkspace, this.GetAdditionalDocumentName(documentId))); } } /// /// Gets the name to use for a project in an error message. /// protected virtual string GetProjectName(ProjectId projectId) { var project = this.CurrentSolution.GetProject(projectId); var name = project != null ? project.Name : ""; return name; } /// /// Gets the name to use for a document in an error message. /// protected virtual string GetDocumentName(DocumentId documentId) { var document = this.CurrentSolution.GetDocument(documentId); var name = document != null ? document.Name : ""; return name; } /// /// Gets the name to use for an additional document in an error message. /// protected virtual string GetAdditionalDocumentName(DocumentId documentId) { var document = this.CurrentSolution.GetAdditionalDocument(documentId); var name = document != null ? document.Name : ""; return name; } #endregion } }