-
Notifications
You must be signed in to change notification settings - Fork 4.3k
Add willRenameFiles support, and an abstraction for extenders to implement #81549
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 4 commits
4425d18
0072e49
974d51c
eb48b33
c8bfdc7
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,15 @@ | ||
| // 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.Composition; | ||
|
|
||
| namespace Microsoft.CodeAnalysis.LanguageServer.Handler; | ||
|
|
||
| [MetadataAttribute] | ||
| [AttributeUsage(AttributeTargets.Class)] | ||
| internal class ExportLspWillRenameListenerAttribute(string glob) : ExportAttribute(typeof(ILspWillRenameListener)), ILspWillRenameListenerMetadata | ||
| { | ||
| public string Glob { get; } = glob; | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,23 @@ | ||
| // 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.Threading; | ||
| using System.Threading.Tasks; | ||
| using Roslyn.LanguageServer.Protocol; | ||
|
|
||
| namespace Microsoft.CodeAnalysis.LanguageServer.Handler; | ||
|
|
||
| /// <summary> | ||
| /// Allows listening for workspace/didRenameFiles notifications. | ||
| /// </summary> | ||
| /// <remarks> | ||
| /// Although the registration for didRename allows specifying a document selector, and that registration is passed | ||
| /// along to the client, the LSP server itself does not filter notifications based on that selector. It is up to the | ||
| /// the listener to determine if it cares about the rename notification or not. If any listener returns an edit, no | ||
| /// further listeners are called. | ||
| /// </remarks> | ||
| internal interface ILspWillRenameListener | ||
| { | ||
| Task<WorkspaceEdit?> HandleWillRenameAsync(RenameFilesParams renameParams, RequestContext context, CancellationToken cancellationToken); | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,10 @@ | ||
| // 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. | ||
|
|
||
| namespace Microsoft.CodeAnalysis.LanguageServer.Handler; | ||
|
|
||
| internal interface ILspWillRenameListenerMetadata | ||
| { | ||
| string Glob { get; } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,101 @@ | ||
| // 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.Composition; | ||
| using System.Threading; | ||
| using System.Threading.Tasks; | ||
| using Microsoft.CodeAnalysis.Host.Mef; | ||
| using Microsoft.CodeAnalysis.PooledObjects; | ||
| using Roslyn.LanguageServer.Protocol; | ||
| using LSP = Roslyn.LanguageServer.Protocol; | ||
|
|
||
| namespace Microsoft.CodeAnalysis.LanguageServer.Handler; | ||
|
|
||
| [ExportCSharpVisualBasicStatelessLspService(typeof(WillRenameHandler)), Shared] | ||
| [Method(LSP.Methods.WorkspaceWillRenameFilesName)] | ||
| [method: ImportingConstructor] | ||
| [method: Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] | ||
| internal sealed class WillRenameHandler( | ||
|
davidwengier marked this conversation as resolved.
|
||
| [ImportMany] IEnumerable<Lazy<ILspWillRenameListener, ILspWillRenameListenerMetadata>> renameListeners) : ILspServiceRequestHandler<LSP.RenameFilesParams, WorkspaceEdit?> | ||
| { | ||
| public bool MutatesSolutionState => true; | ||
| public bool RequiresLSPSolution => true; | ||
|
|
||
| public TextDocumentIdentifier GetTextDocumentIdentifier(RenameParams request) => request.TextDocument; | ||
|
|
||
| public async Task<WorkspaceEdit?> HandleRequestAsync(RenameFilesParams request, RequestContext requestContext, CancellationToken cancellationToken) | ||
| { | ||
| using var _1 = PooledDictionary<string, ArrayBuilder<TextEdit>>.GetInstance(out var changesBuilder); | ||
| using var _2 = ArrayBuilder<SumType<TextDocumentEdit, CreateFile, RenameFile, DeleteFile>>.GetInstance(out var documentChangesBuilder); | ||
|
|
||
| foreach (var listener in renameListeners) | ||
| { | ||
| var edit = await listener.Value.HandleWillRenameAsync(request, requestContext, cancellationToken).ConfigureAwait(false); | ||
|
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Rather than have Roslyn bother to handle globs, and deal with the complexity of one |
||
|
|
||
| if (edit is null) | ||
| { | ||
| continue; | ||
| } | ||
|
|
||
| if (edit.Changes is { } changes) | ||
| { | ||
| foreach (var (path, edits) in changes) | ||
| { | ||
| if (!changesBuilder.TryGetValue(path, out var existingEdits)) | ||
| { | ||
| existingEdits = ArrayBuilder<TextEdit>.GetInstance(); | ||
| changesBuilder.Add(path, existingEdits); | ||
| } | ||
|
|
||
| existingEdits.AddRange(edits); | ||
| } | ||
| } | ||
| else if (edit.DocumentChanges is { } documentChanges) | ||
| { | ||
| if (documentChanges.TryGetFirst(out var textDocumentEdits)) | ||
| { | ||
| foreach (var textDocumentEdit in textDocumentEdits) | ||
| { | ||
| documentChangesBuilder.Add(textDocumentEdit); | ||
| } | ||
| } | ||
| else if (documentChanges.TryGetSecond(out var sumTypes)) | ||
| { | ||
| foreach (var sumType in sumTypes) | ||
| { | ||
| documentChangesBuilder.Add(sumType); | ||
| } | ||
| } | ||
| } | ||
| } | ||
|
|
||
| if (changesBuilder.Count == 0 && documentChangesBuilder.Count == 0) | ||
| { | ||
| return null; | ||
| } | ||
|
|
||
| Contract.ThrowIfTrue(changesBuilder.Count > 0 && documentChangesBuilder.Count > 0, "Cannot have both changes and documentChanges in a WorkspaceEdit. Please honour the client capabilities."); | ||
|
|
||
| if (changesBuilder.Count > 0) | ||
| { | ||
| var changes = new Dictionary<string, TextEdit[]>(); | ||
| foreach (var (path, editsBuilder) in changesBuilder) | ||
| { | ||
| changes[path] = editsBuilder.ToArrayAndFree(); | ||
| } | ||
|
|
||
| return new WorkspaceEdit | ||
| { | ||
| Changes = changes | ||
| }; | ||
| } | ||
|
|
||
| return new WorkspaceEdit | ||
| { | ||
| DocumentChanges = documentChangesBuilder.ToArray() | ||
| }; | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,169 @@ | ||
| // 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. | ||
|
|
||
| #nullable disable | ||
|
|
||
| using System; | ||
| using System.Collections.Generic; | ||
| using System.Composition; | ||
| using System.Linq; | ||
| using System.Threading; | ||
| using System.Threading.Tasks; | ||
| using Microsoft.CodeAnalysis.Host.Mef; | ||
| using Microsoft.CodeAnalysis.LanguageServer.Handler; | ||
| using Microsoft.CodeAnalysis.Test.Utilities; | ||
| using Roslyn.LanguageServer.Protocol; | ||
| using Roslyn.Test.Utilities; | ||
| using Xunit; | ||
| using Xunit.Abstractions; | ||
| using LSP = Roslyn.LanguageServer.Protocol; | ||
|
|
||
| namespace Microsoft.CodeAnalysis.LanguageServer.UnitTests.Rename; | ||
|
|
||
| public sealed class WillRenameTests(ITestOutputHelper testOutputHelper) : AbstractLanguageServerProtocolTests(testOutputHelper) | ||
| { | ||
| protected override TestComposition Composition => base.Composition | ||
| .AddParts(typeof(TestWillRenameListener1)) | ||
| .AddParts(typeof(TestWillRenameListener2)); | ||
|
|
||
| [Theory, CombinatorialData] | ||
| public async Task SetsCapabilities(bool mutatingLspWorkspace) | ||
| { | ||
| var clientCapabilities = new ClientCapabilities | ||
| { | ||
| Workspace = new WorkspaceClientCapabilities | ||
| { | ||
| FileOperations = new FileOperationsWorkspaceClientCapabilities | ||
| { | ||
| WillRename = true | ||
| }, | ||
| }, | ||
| }; | ||
|
|
||
| await using var testLspServer = await CreateTestLspServerAsync("", mutatingLspWorkspace, clientCapabilities); | ||
|
|
||
| var capabilities = testLspServer.GetServerCapabilities(); | ||
|
|
||
| Assert.Collection(capabilities.Workspace.FileOperations.WillRename.Filters, | ||
| filter => Assert.Equal("*.cs", filter.Pattern.Glob), | ||
| filter => Assert.Equal("*.vb", filter.Pattern.Glob)); | ||
| } | ||
|
|
||
| [Theory, CombinatorialData] | ||
| public async Task CallsListener(bool mutatingLspWorkspace) | ||
| { | ||
| await using var testLspServer = await CreateTestLspServerAsync("", mutatingLspWorkspace); | ||
|
|
||
| var listeners = ((IMefHostExportProvider)testLspServer.TestWorkspace.Services.HostServices).GetExportedValues<ILspWillRenameListener>().OfType<TestWillRenameListener1>().ToArray(); | ||
|
|
||
| // Need at least one change, or the result will be dropped | ||
| listeners[0].Result = new WorkspaceEdit() { DocumentChanges = new TextDocumentEdit[] { new() { } } }; | ||
|
|
||
| var edit = await RunWillRenameAsync(testLspServer); | ||
| Assert.NotNull(edit); | ||
| } | ||
|
|
||
| [Theory, CombinatorialData] | ||
| public async Task CombinesDocumentChanges(bool mutatingLspWorkspace) | ||
| { | ||
| await using var testLspServer = await CreateTestLspServerAsync("", mutatingLspWorkspace); | ||
|
|
||
| var listeners = ((IMefHostExportProvider)testLspServer.TestWorkspace.Services.HostServices).GetExportedValues<ILspWillRenameListener>().OfType<TestWillRenameListener1>().ToArray(); | ||
|
|
||
| var expected = new WorkspaceEdit() | ||
| { | ||
| DocumentChanges = new TextDocumentEdit[] { | ||
| new() { TextDocument = new() { DocumentUri = new("file://file1.cs") } }, | ||
| new() { TextDocument = new() { DocumentUri = new("file://file2.cs") } } | ||
| } | ||
| }; | ||
|
|
||
| listeners[0].Result = new WorkspaceEdit() { DocumentChanges = new TextDocumentEdit[] { new() { TextDocument = new() { DocumentUri = new("file://file1.cs") } } } }; | ||
| listeners[1].Result = new WorkspaceEdit() { DocumentChanges = new TextDocumentEdit[] { new() { TextDocument = new() { DocumentUri = new("file://file2.cs") } } } }; | ||
|
|
||
| var edit = await RunWillRenameAsync(testLspServer); | ||
| Assert.NotNull(edit); | ||
|
|
||
| AssertJsonEquals(expected, edit); | ||
| } | ||
|
|
||
| [Theory, CombinatorialData] | ||
| public async Task CombinesDocumentChanges_SumType(bool mutatingLspWorkspace) | ||
| { | ||
| await using var testLspServer = await CreateTestLspServerAsync("", mutatingLspWorkspace); | ||
|
|
||
| var listeners = ((IMefHostExportProvider)testLspServer.TestWorkspace.Services.HostServices).GetExportedValues<ILspWillRenameListener>().OfType<TestWillRenameListener1>().ToArray(); | ||
|
|
||
| var expected = new WorkspaceEdit() | ||
| { | ||
| DocumentChanges = new SumType<TextDocumentEdit, CreateFile, RenameFile, DeleteFile>[] { | ||
| new TextDocumentEdit() { TextDocument = new() { DocumentUri = new("file://file1.cs") } }, | ||
| new RenameFile() { OldDocumentUri = new("file://file2.cs") } | ||
| } | ||
| }; | ||
|
|
||
| listeners[0].Result = new WorkspaceEdit() { DocumentChanges = new TextDocumentEdit[] { new() { TextDocument = new() { DocumentUri = new("file://file1.cs") } } } }; | ||
| listeners[1].Result = new WorkspaceEdit() { DocumentChanges = new SumType<TextDocumentEdit, CreateFile, RenameFile, DeleteFile>[] { new RenameFile() { OldDocumentUri = new("file://file2.cs") } } }; | ||
|
|
||
| var edit = await RunWillRenameAsync(testLspServer); | ||
| Assert.NotNull(edit); | ||
|
|
||
| AssertJsonEquals(expected, edit); | ||
| } | ||
|
|
||
| [Theory, CombinatorialData] | ||
| public async Task CombinesDocumentChanges_Changes(bool mutatingLspWorkspace) | ||
| { | ||
| await using var testLspServer = await CreateTestLspServerAsync("", mutatingLspWorkspace); | ||
|
|
||
| var listeners = ((IMefHostExportProvider)testLspServer.TestWorkspace.Services.HostServices).GetExportedValues<ILspWillRenameListener>().OfType<TestWillRenameListener1>().ToArray(); | ||
|
|
||
| var expected = new WorkspaceEdit() | ||
| { | ||
| Changes = new Dictionary<string, TextEdit[]> | ||
| { | ||
| { "file://file1.cs", []}, | ||
| { "file://file2.cs", [] } | ||
| } | ||
| }; | ||
|
|
||
| listeners[0].Result = new WorkspaceEdit() { Changes = new Dictionary<string, TextEdit[]> { { "file://file1.cs", [] } } }; | ||
| listeners[1].Result = new WorkspaceEdit() { Changes = new Dictionary<string, TextEdit[]> { { "file://file2.cs", [] } } }; | ||
|
|
||
| var edit = await RunWillRenameAsync(testLspServer); | ||
| Assert.NotNull(edit); | ||
|
|
||
| AssertJsonEquals(expected, edit); | ||
| } | ||
|
|
||
| private static async Task<WorkspaceEdit> RunWillRenameAsync(TestLspServer testLspServer) | ||
| { | ||
| var renameParams = new RenameFilesParams(); | ||
| return await testLspServer.ExecuteRequestAsync<LSP.RenameFilesParams, LSP.WorkspaceEdit>(LSP.Methods.WorkspaceWillRenameFilesName, renameParams, CancellationToken.None); | ||
| } | ||
|
|
||
| [ExportLspWillRenameListener("*.cs")] | ||
| [Shared] | ||
| [PartNotDiscoverable] | ||
| [method: ImportingConstructor] | ||
| [method: Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] | ||
| private class TestWillRenameListener1() : ILspWillRenameListener | ||
| { | ||
| public WorkspaceEdit Result { get; set; } | ||
|
|
||
| public Task<WorkspaceEdit> HandleWillRenameAsync(RenameFilesParams renameParams, RequestContext context, CancellationToken cancellationToken) | ||
| { | ||
| return Task.FromResult(Result); | ||
| } | ||
| } | ||
|
|
||
| [ExportLspWillRenameListener("*.vb")] | ||
| [Shared] | ||
| [PartNotDiscoverable] | ||
| [method: ImportingConstructor] | ||
| [method: Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] | ||
| private class TestWillRenameListener2() : TestWillRenameListener1 | ||
| { | ||
| } | ||
| } |
Uh oh!
There was an error while loading. Please reload this page.