| File: Rename\Renamer.RenameDocumentActionSet.cs | Web Access |
| Project: src\roslyn\src\Workspaces\Core\Portable\Microsoft.CodeAnalysis.Workspaces.csproj (Microsoft.CodeAnalysis.Workspaces) |
// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. using System; using System.Collections.Immutable; using System.Linq; using System.Threading; using System.Threading.Tasks; using Microsoft.CodeAnalysis.Shared.Extensions; namespace Microsoft.CodeAnalysis.Rename; public static partial class Renamer { /// <summary> /// Information about rename document calls that allows them to be applied as individual actions. Actions are individual units of work /// that can change the contents of one or more document in the solution. Even if the <see cref="ApplicableActions"/> is empty, the /// document metadata will still be updated by calling <see cref="UpdateSolutionAsync(Solution, ImmutableArray{RenameDocumentAction}, CancellationToken)"/> /// <para /> /// To apply all actions use <see cref="UpdateSolutionAsync(Solution, CancellationToken)"/>, or use a subset /// of the actions by calling <see cref="UpdateSolutionAsync(Solution, ImmutableArray{RenameDocumentAction}, CancellationToken)"/>. /// Actions can be applied in any order. /// Each action has a description of the changes that it will apply that can be presented to a user. /// </summary> public sealed class RenameDocumentActionSet { private readonly DocumentId _documentId; private readonly string _documentName; private readonly ImmutableArray<string> _documentFolders; private readonly DocumentRenameOptions _options; internal RenameDocumentActionSet( ImmutableArray<RenameDocumentAction> actions, DocumentId documentId, string documentName, ImmutableArray<string> documentFolders, DocumentRenameOptions options) { ApplicableActions = actions; _documentFolders = documentFolders; _documentId = documentId; _documentName = documentName; _options = options; } /// <summary> /// All applicable actions computed for the action. Action set may be empty, which represents updates to document /// contents rather than metadata. Document metadata will still not be updated unless <see cref="UpdateSolutionAsync(Solution, ImmutableArray{RenameDocumentAction}, CancellationToken)" /> /// is called. /// </summary> public ImmutableArray<RenameDocumentAction> ApplicableActions { get; } /// <summary> /// Same as calling <see cref="UpdateSolutionAsync(Solution, ImmutableArray{RenameDocumentAction}, CancellationToken)"/> with /// <see cref="ApplicableActions"/> as the argument /// </summary> public Task<Solution> UpdateSolutionAsync(Solution solution, CancellationToken cancellationToken) => UpdateSolutionAsync(solution, ApplicableActions, cancellationToken); /// <summary> /// Applies each <see cref="RenameDocumentAction"/> in order and returns the final solution. /// All actions must be contained in <see cref="ApplicableActions" /> /// </summary> /// <remarks> /// An empty action set is still allowed and will return a modified solution /// that will update the document properties as appropriate. This means we /// can still support when <see cref="ApplicableActions"/> is empty. It's desirable /// that consumers can call a rename API to produce a <see cref="RenameDocumentActionSet"/> and /// immediately call <see cref="UpdateSolutionAsync(Solution, ImmutableArray{RenameDocumentAction}, CancellationToken)"/> without /// having to inspect the returned <see cref="ApplicableActions"/>. /// </remarks> public async Task<Solution> UpdateSolutionAsync(Solution solution, ImmutableArray<RenameDocumentAction> actions, CancellationToken cancellationToken) { if (solution is null) { throw new ArgumentNullException(nameof(solution)); } if (actions.Any(static (a, self) => !self.ApplicableActions.Contains(a), this)) { throw new ArgumentException(string.Format(WorkspacesResources.Cannot_apply_action_that_is_not_in_0, nameof(ApplicableActions))); } // Prior to updating the solution it's possible the document id has changed between the time // the document action info was generated and when the solution update is applied. We // do a best effort to still locate the document if the id has changed. var document = GetDocument(solution); // If the document was found in the solution then the current id will be durable across actions // since we own the solution snapshot at this point. var documentId = document.Id; // Make sure that the document name and folders are updated to what we expect them to be solution = solution .WithDocumentName(documentId, _documentName) .WithDocumentFolders(documentId, _documentFolders); // Apply each action individually. Order should not matter foreach (var action in actions) { document = solution.GetRequiredDocument(documentId); solution = await action.GetModifiedSolutionAsync(document, _options, cancellationToken).ConfigureAwait(false); } return solution; } /// <summary> /// Attempts to find the document in the solution. Tries by documentId first, but /// that's not always reliable between analysis and application of the rename actions /// </summary> private Document GetDocument(Solution solution) { // DocumentId is the best bet for finding a document, // but it's possible the document was renamed or moved // before actions were applied. if (solution.ContainsDocument(_documentId)) { return solution.GetRequiredDocument(_documentId); } // There are cases where we expect work to be done between when the ActionSet is first generated // and when the solution can be worked on. This work can remove and add documents as part of the rename // and thus won't have the same DocumentId. // // 1. Right click solution explorer > rename // 2. Call Renamer.RenameDocument to generate dialog for user (near synchronous) // 3. CPS changes file on disk // 4. CPS updates project file if necessary // 5. In dotnet project system, a new design time build is started // 6. Re-evaluates what files need to be passed to Roslyn. Tell Roslyn of file changed. This is a remove then add. (Asynchronous on project-system side) // 7. We update the workspace snapshot in the VS Workspace. Synchronous and controlled by project system. // 8. RenameDocumentActionSet should be applied to the current Workspace Solution // // Since step 6 and 7 remove and add the document, step 8 can't depend on the DocumentId being available and the same document. // We are guaranateed that the project is the same and we know what the document name will be. // https://github.com/dotnet/roslyn/issues/43729 tracks designing a more elagent system that can help alleviate // this issue. var project = solution.GetRequiredProject(_documentId.ProjectId); return project.Documents.FirstOrDefault(d => d.Name == _documentName && d.Folders.SequenceEqual(_documentFolders)) ?? throw ISolutionExtensions.CreateDocumentNotFoundException(_documentName); } } }