/
githubmirror
/
roslyn
Обзор
Документация
Войти
/
githubmirror
/
roslyn
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
main
src/LanguageServer/Protocol/Handler/CodeActions/CodeActionResolveHelper.cs
330 строк
17 KB
Cyrus Najmabadi
Reapply "Update methods to be `async`." (#81808)
31 дек 2025, 21:23
31 дек 2025, 21:23
da211b4
Код
Авторство
О чём код?
// 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.Generic; using System.Collections.Immutable; using System.Linq; using System.Threading; using System.Threading.Tasks; using Microsoft.CodeAnalysis.CodeActions; using Microsoft.CodeAnalysis.Collections; using Microsoft.CodeAnalysis.PooledObjects; using Microsoft.CodeAnalysis.Shared.Extensions; using Microsoft.CodeAnalysis.Text; using Roslyn.LanguageServer.Protocol; using Roslyn.Utilities; using LSP = Roslyn.LanguageServer.Protocol; namespace Microsoft.CodeAnalysis.LanguageServer.Handler.CodeActions; internal sealed class CodeActionResolveHelper { public static Task<LSP.WorkspaceEdit> GetCodeActionResolveEditsAsync(RequestContext context, CodeActionResolveData data, ImmutableArray<CodeActionOperation> operations, CancellationToken cancellationToken) { var solution = context.Solution; Contract.ThrowIfNull(solution); return GetCodeActionResolveEditsAsync( solution, data, operations, context.GetRequiredClientCapabilities().Workspace?.WorkspaceEdit?.ResourceOperations ?? [], context.TraceDebug, cancellationToken); } public static async Task<LSP.WorkspaceEdit> GetCodeActionResolveEditsAsync(Solution solution, CodeActionResolveData data, ImmutableArray<CodeActionOperation> operations, ResourceOperationKind[] resourceOperations, Action<string> logFunction, CancellationToken cancellationToken) { // TO-DO: We currently must execute code actions which add new documents on the server as commands, // since there is no LSP support for adding documents yet. In the future, we should move these actions // to execute on the client. // https://devdiv.visualstudio.com/DevDiv/_workitems/edit/1147293/ var textDiffService = solution.Services.GetService<IDocumentTextDifferencingService>(); using var _1 = ArrayBuilder<SumType<TextDocumentEdit, CreateFile, RenameFile, DeleteFile>>.GetInstance(out var textDocumentEdits); using var _2 = PooledHashSet<DocumentId>.GetInstance(out var modifiedDocumentIds); foreach (var option in operations) { // We only support making solution-updating operations in LSP. And only ones that modify documents. 1st // class code actions that do more than this are supposed to add the CodeAction.MakesNonDocumentChange // in their Tags so we can filter them out before returning them to the client. // // However, we cannot enforce this as 3rd party fixers can still run. So we filter their results to // only apply the portions of their work that updates documents, and nothing else. if (option is not ApplyChangesOperation applyChangesOperation) { logFunction($"Skipping code action operation for '{data.UniqueIdentifier}'. It was a '{option.GetType().FullName}'"); continue; } var changes = applyChangesOperation.ChangedSolution.GetChanges(solution); var newSolution = await applyChangesOperation.ChangedSolution.WithMergedLinkedFileChangesAsync(solution, changes, cancellationToken: cancellationToken).ConfigureAwait(false); changes = newSolution.GetChanges(solution); var projectChanges = changes.GetProjectChanges(); // Don't apply changes in the presence of any non-document changes for now. Note though that LSP does // support additional functionality (like create/rename/delete file). Once VS updates their LSP client // impl to support this, we should add that support here. // // https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#workspaceEdit // // Tracked with: https://github.com/dotnet/roslyn/issues/65303 foreach (var projectChange in projectChanges) { if (projectChange.GetAddedProjectReferences().Any() || projectChange.GetRemovedProjectReferences().Any() || projectChange.GetAddedMetadataReferences().Any() || projectChange.GetRemovedMetadataReferences().Any() || projectChange.GetAddedAnalyzerReferences().Any() || projectChange.GetRemovedAnalyzerReferences().Any()) { // Changes to references are not currently supported return new LSP.WorkspaceEdit { DocumentChanges = Array.Empty<TextDocumentEdit>() }; } if (projectChange.GetRemovedDocuments().Any() || projectChange.GetRemovedAdditionalDocuments().Any() || projectChange.GetRemovedAnalyzerConfigDocuments().Any()) { if (!resourceOperations.Contains(ResourceOperationKind.Delete)) { // Removing documents is not supported by this workspace return new LSP.WorkspaceEdit { DocumentChanges = Array.Empty<TextDocumentEdit>() }; } } if (projectChange.GetAddedDocuments().Any() || projectChange.GetAddedAdditionalDocuments().Any() || projectChange.GetAddedAnalyzerConfigDocuments().Any()) { if (!resourceOperations.Contains(ResourceOperationKind.Create)) { // Adding documents is not supported by this workspace return new LSP.WorkspaceEdit { DocumentChanges = Array.Empty<TextDocumentEdit>() }; } } if (projectChange.GetChangedDocuments().Any(docId => HasDocumentNameChange(docId, newSolution, solution)) || projectChange.GetChangedAdditionalDocuments().Any(docId => HasDocumentNameChange(docId, newSolution, solution) || projectChange.GetChangedAnalyzerConfigDocuments().Any(docId => HasDocumentNameChange(docId, newSolution, solution)))) { if (!resourceOperations.Contains(ResourceOperationKind.Rename)) { // Rename documents is not supported by this workspace return new LSP.WorkspaceEdit { DocumentChanges = Array.Empty<TextDocumentEdit>() }; } } } #if false // TO-DO: If the change involves adding or removing a document, execute via command instead of WorkspaceEdit // until adding/removing documents is supported in LSP: https://devdiv.visualstudio.com/DevDiv/_workitems/edit/1147293/ // After support is added, remove the below if-statement and add code to support adding/removing documents. var addedDocuments = projectChanges.SelectMany( pc => pc.GetAddedDocuments().Concat(pc.GetAddedAdditionalDocuments().Concat(pc.GetAddedAnalyzerConfigDocuments()))); var removedDocuments = projectChanges.SelectMany( pc => pc.GetRemovedDocuments().Concat(pc.GetRemovedAdditionalDocuments().Concat(pc.GetRemovedAnalyzerConfigDocuments()))); if (addedDocuments.Any() || removedDocuments.Any()) { codeAction.Command = SetCommand(codeAction.Title, data); return codeAction; } // TO-DO: If the change involves adding or removing a project reference, execute via command instead of // WorkspaceEdit until adding/removing project references is supported in LSP: // https://devdiv.visualstudio.com/DevDiv/_workitems/edit/1166040 var projectReferences = projectChanges.SelectMany( pc => pc.GetAddedProjectReferences().Concat(pc.GetRemovedProjectReferences())); if (projectReferences.Any()) { codeAction.Command = SetCommand(codeAction.Title, data); return codeAction; } #endif // Removed documents await AddTextDocumentDeletionsAsync( projectChanges.SelectMany(pc => pc.GetRemovedDocuments()), solution.GetDocument).ConfigureAwait(false); // Removed analyzer config documents await AddTextDocumentDeletionsAsync( projectChanges.SelectMany(pc => pc.GetRemovedAnalyzerConfigDocuments()), solution.GetAnalyzerConfigDocument).ConfigureAwait(false); // Removed additional documents await AddTextDocumentDeletionsAsync( projectChanges.SelectMany(pc => pc.GetRemovedAdditionalDocuments()), solution.GetAdditionalDocument).ConfigureAwait(false); // Added documents await AddTextDocumentAdditionsAsync( projectChanges.SelectMany(pc => pc.GetAddedDocuments()), newSolution.GetDocument).ConfigureAwait(false); // Added analyzer config documents await AddTextDocumentAdditionsAsync( projectChanges.SelectMany(pc => pc.GetAddedAnalyzerConfigDocuments()), newSolution.GetAnalyzerConfigDocument).ConfigureAwait(false); // Added additional documents await AddTextDocumentAdditionsAsync( projectChanges.SelectMany(pc => pc.GetAddedAdditionalDocuments()), newSolution.GetAdditionalDocument).ConfigureAwait(false); // Changed documents await AddTextDocumentEditsAsync( projectChanges.SelectMany(pc => pc.GetChangedDocuments()), newSolution.GetDocument, solution.GetDocument).ConfigureAwait(false); // Razor calls through our code action handlers with documents that come from the Razor source generator // Those changes are not visible in project changes, because they happen in the compilation state, so we // make sure to pull changes out from that too. Changed source generated documents are "frozen" because // their content no longer comes from the source generator, so thats our cue to know when to handle. // Changes to non-frozen documents don't need to be included, because changes to the origin document would // cause the generator to re-generate the same changed content. await AddTextDocumentEditsAsync( changes.GetExplicitlyChangedSourceGeneratedDocuments(), newSolution.GetRequiredSourceGeneratedDocumentForAlreadyGeneratedId, solution.GetRequiredSourceGeneratedDocumentForAlreadyGeneratedId).ConfigureAwait(false); // Changed analyzer config documents await AddTextDocumentEditsAsync( projectChanges.SelectMany(pc => pc.GetChangedAnalyzerConfigDocuments()), newSolution.GetAnalyzerConfigDocument, solution.GetAnalyzerConfigDocument).ConfigureAwait(false); // Changed additional documents await AddTextDocumentEditsAsync( projectChanges.SelectMany(pc => pc.GetChangedAdditionalDocuments()), newSolution.GetAdditionalDocument, solution.GetAdditionalDocument).ConfigureAwait(false); } return new LSP.WorkspaceEdit { DocumentChanges = textDocumentEdits.ToArray() }; async Task AddTextDocumentDeletionsAsync<TTextDocument>( IEnumerable<DocumentId> removedDocuments, Func<DocumentId, TTextDocument?> getOldDocument) where TTextDocument : TextDocument { foreach (var docId in removedDocuments) { var oldTextDoc = getOldDocument(docId); Contract.ThrowIfNull(oldTextDoc); textDocumentEdits.Add(new DeleteFile { DocumentUri = oldTextDoc.GetURI() }); } } async Task AddTextDocumentAdditionsAsync<TTextDocument>( IEnumerable<DocumentId> addedDocuments, Func<DocumentId, TTextDocument?> getNewDocument) where TTextDocument : TextDocument { foreach (var docId in addedDocuments) { var newTextDoc = getNewDocument(docId); Contract.ThrowIfNull(newTextDoc); DocumentUri? uri = null; if (newTextDoc.FilePath != null) { uri = newTextDoc.GetURI(); } else if (newTextDoc.Project.FilePath != null) { // If there is no file path with the document, try to find its path by using its project file. uri = newTextDoc.CreateUriForDocumentWithoutFilePath(); } else { // No document file path, and no project path. We don't know how to add this document. Throw. Contract.Fail($"Can't find uri for document: {newTextDoc.Name}."); } textDocumentEdits.Add(new CreateFile { DocumentUri = uri }); // And then give it content var newText = await newTextDoc.GetValueTextAsync(cancellationToken).ConfigureAwait(false); var emptyDocumentRange = new LSP.Range { Start = new Position { Line = 0, Character = 0 }, End = new Position { Line = 0, Character = 0 } }; var edit = new TextEdit { Range = emptyDocumentRange, NewText = newText.ToString() }; var documentIdentifier = new OptionalVersionedTextDocumentIdentifier { DocumentUri = uri }; textDocumentEdits.Add(new TextDocumentEdit { TextDocument = documentIdentifier, Edits = [edit] }); } } async Task AddTextDocumentEditsAsync<TTextDocument>( IEnumerable<DocumentId> changedDocuments, Func<DocumentId, TTextDocument?> getNewDocument, Func<DocumentId, TTextDocument?> getOldDocument) where TTextDocument : TextDocument { foreach (var docId in changedDocuments) { var newTextDoc = getNewDocument(docId); var oldTextDoc = getOldDocument(docId); Contract.ThrowIfNull(oldTextDoc); Contract.ThrowIfNull(newTextDoc); // For linked documents, only generated the document edit once. if (modifiedDocumentIds.Add(docId)) { var oldText = await oldTextDoc.GetValueTextAsync(cancellationToken).ConfigureAwait(false); IEnumerable<TextChange> textChanges; // Normal documents have a unique service for calculating minimal text edits. If we used the standard 'GetTextChanges' // method instead, we would get a change that spans the entire document, which we ideally want to avoid. if (newTextDoc is Document newDoc && oldTextDoc is Document oldDoc) { Contract.ThrowIfNull(textDiffService); textChanges = await textDiffService.GetTextChangesAsync(oldDoc, newDoc, cancellationToken).ConfigureAwait(false); } else { var newText = await newTextDoc.GetValueTextAsync(cancellationToken).ConfigureAwait(false); textChanges = newText.GetTextChanges(oldText); } var edits = textChanges.Select(tc => new LSP.SumType<LSP.TextEdit, LSP.AnnotatedTextEdit>(ProtocolConversions.TextChangeToTextEdit(tc, oldText))).ToArray(); if (edits.Length > 0) { var documentIdentifier = new OptionalVersionedTextDocumentIdentifier { DocumentUri = newTextDoc.GetURI() }; textDocumentEdits.Add(new TextDocumentEdit { TextDocument = documentIdentifier, Edits = edits }); } // Add Rename edit. // Note: // Client is expected to do the change in the order in which they are provided. // https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#workspaceEdit // So we would like to first edit the old document, then rename it. if (oldTextDoc.Name != newTextDoc.Name) { textDocumentEdits.Add(new RenameFile() { OldDocumentUri = oldTextDoc.GetURI(), NewDocumentUri = newTextDoc.GetUriForRenamedDocument() }); } var linkedDocuments = solution.GetRelatedDocumentIds(docId); modifiedDocumentIds.AddRange(linkedDocuments); } } } } private static bool HasDocumentNameChange(DocumentId documentId, Solution newSolution, Solution oldSolution) { var newDocument = newSolution.GetRequiredTextDocument(documentId); var oldDocument = oldSolution.GetRequiredTextDocument(documentId); return newDocument.Name != oldDocument.Name; } }