-
Notifications
You must be signed in to change notification settings - Fork 4.1k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
9 changed files
with
434 additions
and
78 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
96 changes: 96 additions & 0 deletions
96
...erver/Microsoft.CodeAnalysis.LanguageServer.UnitTests/Services/ExtractRefactoringTests.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,96 @@ | ||
// 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.Diagnostics.CodeAnalysis; | ||
using Microsoft.CodeAnalysis; | ||
using Roslyn.Test.Utilities; | ||
using Xunit.Abstractions; | ||
using LSP = Roslyn.LanguageServer.Protocol; | ||
|
||
namespace Microsoft.CodeAnalysis.LanguageServer.UnitTests.Services; | ||
|
||
public class ExtractRefactoringTests(ITestOutputHelper testOutputHelper) : AbstractLanguageServerHostTests(testOutputHelper) | ||
{ | ||
[Theory] | ||
[CombinatorialData] | ||
public async Task TestExtractBaseClass(bool includeDevKitComponents) | ||
{ | ||
var markup = | ||
""" | ||
class {|caret:A|} | ||
{ | ||
public void M() | ||
{ | ||
} | ||
} | ||
"""; | ||
var expected = | ||
""" | ||
internal class NewBaseType | ||
{ | ||
public void M() | ||
{ | ||
} | ||
} | ||
class A : NewBaseType | ||
{ | ||
} | ||
"""; | ||
|
||
await using var testLspServer = await CreateCSharpLanguageServerAsync(markup, includeDevKitComponents); | ||
var caretLocation = testLspServer.GetLocations("caret").Single(); | ||
|
||
await TestCodeActionAsync(testLspServer, caretLocation, "Extract base class...", expected); | ||
} | ||
|
||
[Theory] | ||
[CombinatorialData] | ||
public async Task TestExtractInterface(bool includeDevKitComponents) | ||
{ | ||
var markup = | ||
""" | ||
class {|caret:A|} | ||
{ | ||
public void M() | ||
{ | ||
} | ||
} | ||
"""; | ||
var expected = | ||
""" | ||
interface IA | ||
{ | ||
void M(); | ||
} | ||
class A : IA | ||
{ | ||
public void M() | ||
{ | ||
} | ||
} | ||
"""; | ||
|
||
await using var testLspServer = await CreateCSharpLanguageServerAsync(markup, includeDevKitComponents); | ||
var caretLocation = testLspServer.GetLocations("caret").Single(); | ||
|
||
await TestCodeActionAsync(testLspServer, caretLocation, "Extract interface...", expected); | ||
} | ||
|
||
private static async Task TestCodeActionAsync(TestLspServer testLspServer, LSP.Location caretLocation, string codeActionTitle, [StringSyntax("c#-test")] string expected) | ||
{ | ||
var codeActionResults = await testLspServer.RunGetCodeActionsAsync(CreateCodeActionParams(caretLocation)); | ||
|
||
var unresolvedCodeAction = Assert.Single(codeActionResults, codeAction => codeAction.Title == codeActionTitle); | ||
|
||
var resolvedCodeAction = await testLspServer.RunGetCodeActionResolveAsync(unresolvedCodeAction); | ||
|
||
Assert.True(testLspServer.TryApplyWorkspaceEdit(resolvedCodeAction.Edit)); | ||
|
||
var updatedCode = testLspServer.GetDocumentText(caretLocation.Uri); | ||
|
||
AssertEx.Equal(expected, updatedCode); | ||
} | ||
} |
156 changes: 156 additions & 0 deletions
156
...lysis.LanguageServer.UnitTests/Utilities/AbstractLanguageServerHostTests.TestLspServer.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,156 @@ | ||
// 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.Collections.Immutable; | ||
using Microsoft.CodeAnalysis.LanguageServer.HostWorkspace; | ||
using Microsoft.CodeAnalysis.LanguageServer.LanguageServer; | ||
using Microsoft.CodeAnalysis.Text; | ||
using Microsoft.VisualStudio.Composition; | ||
using Nerdbank.Streams; | ||
using Roslyn.LanguageServer.Protocol; | ||
using Roslyn.Utilities; | ||
using StreamJsonRpc; | ||
using LSP = Roslyn.LanguageServer.Protocol; | ||
|
||
namespace Microsoft.CodeAnalysis.LanguageServer.UnitTests; | ||
|
||
public partial class AbstractLanguageServerHostTests | ||
{ | ||
internal sealed class TestLspServer : IAsyncDisposable | ||
{ | ||
private readonly Dictionary<Uri, SourceText> _files; | ||
private readonly Dictionary<string, IList<LSP.Location>> _locations; | ||
private readonly Task _languageServerHostCompletionTask; | ||
private readonly JsonRpc _clientRpc; | ||
|
||
private ServerCapabilities? _serverCapabilities; | ||
|
||
internal static async Task<TestLspServer> CreateAsync( | ||
ClientCapabilities clientCapabilities, | ||
TestOutputLogger logger, | ||
string cacheDirectory, | ||
bool includeDevKitComponents = true, | ||
string[]? extensionPaths = null, | ||
Dictionary<Uri, SourceText>? files = null, | ||
Dictionary<string, IList<LSP.Location>>? locations = null) | ||
{ | ||
var exportProvider = await LanguageServerTestComposition.CreateExportProviderAsync( | ||
logger.Factory, includeDevKitComponents, cacheDirectory, extensionPaths, out var serverConfiguration, out var assemblyLoader); | ||
exportProvider.GetExportedValue<ServerConfigurationFactory>().InitializeConfiguration(serverConfiguration); | ||
|
||
var testLspServer = new TestLspServer(exportProvider, logger, assemblyLoader, files ?? [], locations ?? []); | ||
var initializeResponse = await testLspServer.Initialize(clientCapabilities); | ||
Assert.NotNull(initializeResponse?.Capabilities); | ||
testLspServer._serverCapabilities = initializeResponse!.Capabilities; | ||
|
||
await testLspServer.Initialized(); | ||
|
||
return testLspServer; | ||
} | ||
|
||
internal LanguageServerHost LanguageServerHost { get; } | ||
public ExportProvider ExportProvider { get; } | ||
|
||
internal ServerCapabilities ServerCapabilities => _serverCapabilities ?? throw new InvalidOperationException("Initialize has not been called"); | ||
|
||
private TestLspServer(ExportProvider exportProvider, TestOutputLogger logger, IAssemblyLoader assemblyLoader, Dictionary<Uri, SourceText> files, Dictionary<string, IList<LSP.Location>> locations) | ||
{ | ||
_files = files; | ||
_locations = locations; | ||
|
||
var typeRefResolver = new ExtensionTypeRefResolver(assemblyLoader, logger.Factory); | ||
|
||
var (clientStream, serverStream) = FullDuplexStream.CreatePair(); | ||
LanguageServerHost = new LanguageServerHost(serverStream, serverStream, exportProvider, logger, typeRefResolver); | ||
|
||
var messageFormatter = RoslynLanguageServer.CreateJsonMessageFormatter(); | ||
_clientRpc = new JsonRpc(new HeaderDelimitedMessageHandler(clientStream, clientStream, messageFormatter)) | ||
{ | ||
AllowModificationWhileListening = true, | ||
ExceptionStrategy = ExceptionProcessing.ISerializable, | ||
}; | ||
|
||
_clientRpc.StartListening(); | ||
|
||
// This task completes when the server shuts down. We store it so that we can wait for completion | ||
// when we dispose of the test server. | ||
LanguageServerHost.Start(); | ||
|
||
_languageServerHostCompletionTask = LanguageServerHost.WaitForExitAsync(); | ||
ExportProvider = exportProvider; | ||
} | ||
|
||
public async Task<TResponseType?> ExecuteRequestAsync<TRequestType, TResponseType>(string methodName, TRequestType request, CancellationToken cancellationToken) where TRequestType : class | ||
{ | ||
var result = await _clientRpc.InvokeWithParameterObjectAsync<TResponseType>(methodName, request, cancellationToken: cancellationToken); | ||
return result; | ||
} | ||
|
||
public Task ExecuteNotificationAsync<RequestType>(string methodName, RequestType request) where RequestType : class | ||
{ | ||
return _clientRpc.NotifyWithParameterObjectAsync(methodName, request); | ||
} | ||
|
||
public Task ExecuteNotification0Async(string methodName) | ||
{ | ||
return _clientRpc.NotifyWithParameterObjectAsync(methodName); | ||
} | ||
|
||
public void AddClientLocalRpcTarget(object target) | ||
{ | ||
_clientRpc.AddLocalRpcTarget(target); | ||
} | ||
|
||
public void AddClientLocalRpcTarget(string methodName, Delegate handler) | ||
{ | ||
_clientRpc.AddLocalRpcMethod(methodName, handler); | ||
} | ||
|
||
public bool TryApplyWorkspaceEdit(WorkspaceEdit? workspaceEdit) | ||
{ | ||
Assert.NotNull(workspaceEdit); | ||
|
||
// We do not support applying the following edits | ||
Assert.Null(workspaceEdit.Changes); | ||
Assert.Null(workspaceEdit.ChangeAnnotations); | ||
|
||
// Currently we only support applying TextDocumentEdits | ||
var textDocumentEdits = (TextDocumentEdit[]?)workspaceEdit.DocumentChanges?.Value; | ||
Assert.NotNull(textDocumentEdits); | ||
|
||
foreach (var documentEdit in textDocumentEdits) | ||
{ | ||
var uri = documentEdit.TextDocument.Uri; | ||
var document = _files[uri]; | ||
|
||
var changes = documentEdit.Edits | ||
.Select(edit => edit.Value) | ||
.Cast<TextEdit>() | ||
.SelectAsArray(edit => ProtocolConversions.TextEditToTextChange(edit, document)); | ||
|
||
var updatedDocument = document.WithChanges(changes); | ||
_files[uri] = updatedDocument; | ||
} | ||
|
||
return true; | ||
} | ||
|
||
public string GetDocumentText(Uri uri) => _files[uri].ToString(); | ||
|
||
public IList<LSP.Location> GetLocations(string locationName) => _locations[locationName]; | ||
|
||
public async ValueTask DisposeAsync() | ||
{ | ||
await _clientRpc.InvokeAsync(Methods.ShutdownName); | ||
await _clientRpc.NotifyAsync(Methods.ExitName); | ||
|
||
// The language server host task should complete once shutdown and exit are called. | ||
#pragma warning disable VSTHRD003 // Avoid awaiting foreign Tasks | ||
await _languageServerHostCompletionTask; | ||
#pragma warning restore VSTHRD003 // Avoid awaiting foreign Tasks | ||
|
||
_clientRpc.Dispose(); | ||
} | ||
} | ||
} |
Oops, something went wrong.