diff --git a/CHANGELOG.md b/CHANGELOG.md index 9d4be60502..775f9bc74d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ All changes to the project will be documented in this file. * Fixed return type in LSP completion handler ([#1864](https://github.com/OmniSharp/omnisharp-roslyn/issues/1864), PR: [#1869](https://github.com/OmniSharp/omnisharp-roslyn/pull/1869)) * Upgraded to the latest version of the csharp-language-server-protocol [#1815](https://github.com/OmniSharp/omnisharp-roslyn/pull/1815) * Added support for Roslyn `EmbeddedLanguageCompletionProvider` which enables completions for string literals for `DateTime` and `Regex` ([#1871](https://github.com/OmniSharp/omnisharp-roslyn/pull/1871)) +* Improve performance of the `textDocument/codeAction` request. (PR: [#1814](https://github.com/OmniSharp/omnisharp-roslyn/pull/1814)) ## [1.35.4] - 2020-07-22 * Update to Roslyn `3.8.0-1.20357.3` (PR: [#1849](https://github.com/OmniSharp/omnisharp-roslyn/pull/1849)) @@ -23,7 +24,6 @@ All changes to the project will be documented in this file. * Expose a custom LSP `omnisharp/client/findReferences` command via code lens (meant to be handled by LSP client). (PR: [#1807](https://github.com/OmniSharp/omnisharp-roslyn/pull/1807)) * Added `DirectoryDelete` option to `FileChangeType` allowing clients to report deleted directories that need to be removed (along all the files) from the workspace (PR: [#1821](https://github.com/OmniSharp/omnisharp-roslyn/pull/1821)) * Do not crash when plugin assembly cannot be loaded ([#1307](https://github.com/OmniSharp/omnisharp-roslyn/issues/1307), PR: [#1827](https://github.com/OmniSharp/omnisharp-roslyn/pull/1827)) -* Update to Roslyn `3.7.0-4.20311.4` (PR: [#1832](https://github.com/OmniSharp/omnisharp-roslyn/pull/1832)) ## [1.35.2] - 2020-05-20 * Added support for `WarningsAsErrors` in csproj files (PR: [#1779](https://github.com/OmniSharp/omnisharp-roslyn/pull/1779)) diff --git a/build/Packages.props b/build/Packages.props index 4309668fdd..eac4bf43e8 100644 --- a/build/Packages.props +++ b/build/Packages.props @@ -62,8 +62,8 @@ - - + + diff --git a/src/OmniSharp.Host/CompositionHostBuilder.cs b/src/OmniSharp.Host/CompositionHostBuilder.cs index 449c1475b6..b1c0e158d4 100644 --- a/src/OmniSharp.Host/CompositionHostBuilder.cs +++ b/src/OmniSharp.Host/CompositionHostBuilder.cs @@ -75,6 +75,7 @@ public CompositionHost Build() .WithProvider(MefValueProvider.From(loggerFactory)) .WithProvider(MefValueProvider.From(environment)) .WithProvider(MefValueProvider.From(options.CurrentValue)) + .WithProvider(MefValueProvider.From(options)) .WithProvider(MefValueProvider.From(options.CurrentValue.FormattingOptions)) .WithProvider(MefValueProvider.From(assemblyLoader)) .WithProvider(MefValueProvider.From(analyzerAssemblyLoader)) diff --git a/src/OmniSharp.Host/WorkspaceInitializer.cs b/src/OmniSharp.Host/WorkspaceInitializer.cs index b0af06b60b..b2c869cb46 100644 --- a/src/OmniSharp.Host/WorkspaceInitializer.cs +++ b/src/OmniSharp.Host/WorkspaceInitializer.cs @@ -30,6 +30,7 @@ public static void Initialize(IServiceProvider serviceProvider, CompositionHost var projectSystems = compositionHost.GetExports(); workspace.EditorConfigEnabled = options.CurrentValue.FormattingOptions.EnableEditorConfigSupport; + options.OnChange(x => workspace.EditorConfigEnabled = x.FormattingOptions.EnableEditorConfigSupport); foreach (var projectSystem in projectSystems) { diff --git a/src/OmniSharp.LanguageServerProtocol/Eventing/LanguageServerEventEmitter.cs b/src/OmniSharp.LanguageServerProtocol/Eventing/LanguageServerEventEmitter.cs index 453b624aa1..456e8abf72 100644 --- a/src/OmniSharp.LanguageServerProtocol/Eventing/LanguageServerEventEmitter.cs +++ b/src/OmniSharp.LanguageServerProtocol/Eventing/LanguageServerEventEmitter.cs @@ -1,10 +1,12 @@ using System.Linq; +using Microsoft.Extensions.DependencyInjection; using Newtonsoft.Json; using Newtonsoft.Json.Linq; using OmniSharp.Eventing; using OmniSharp.Extensions.LanguageServer.Protocol.Document; using OmniSharp.Extensions.LanguageServer.Protocol.Models; using OmniSharp.Extensions.LanguageServer.Protocol.Server; +using OmniSharp.LanguageServerProtocol.Handlers; using OmniSharp.Models.Diagnostics; using OmniSharp.Models.Events; @@ -13,10 +15,12 @@ namespace OmniSharp.LanguageServerProtocol.Eventing public class LanguageServerEventEmitter : IEventEmitter { private readonly ILanguageServer _server; + private readonly DocumentVersions _documentVersions; public LanguageServerEventEmitter(ILanguageServer server) { _server = server; + _documentVersions = server.Services.GetRequiredService(); } public void Emit(string kind, object args) @@ -34,6 +38,7 @@ public void Emit(string kind, object args) _server.TextDocument.PublishDiagnostics(new PublishDiagnosticsParams() { Uri = group.Key, + Version = _documentVersions.GetVersion(group.Key), Diagnostics = group .SelectMany(z => z.Select(v => v.ToDiagnostic())) .ToArray() diff --git a/src/OmniSharp.LanguageServerProtocol/Handlers/DocumentVersions.cs b/src/OmniSharp.LanguageServerProtocol/Handlers/DocumentVersions.cs new file mode 100644 index 0000000000..1faf7115f1 --- /dev/null +++ b/src/OmniSharp.LanguageServerProtocol/Handlers/DocumentVersions.cs @@ -0,0 +1,36 @@ +using System.Collections.Concurrent; +using OmniSharp.Extensions.LanguageServer.Protocol; +using OmniSharp.Extensions.LanguageServer.Protocol.Models; + +namespace OmniSharp.LanguageServerProtocol.Handlers +{ + public class DocumentVersions + { + private readonly ConcurrentDictionary _documentVersions = new ConcurrentDictionary(); + + public int? GetVersion(DocumentUri documentUri) + { + if (_documentVersions.TryGetValue(documentUri, out var version)) + { + return version; + } + + return null; + } + + public void Update(VersionedTextDocumentIdentifier identifier) + { + _documentVersions.AddOrUpdate(identifier.Uri, identifier.Version ?? 0, (uri, i) => identifier.Version ?? 0); + } + + public void Reset(TextDocumentIdentifier identifier) + { + _documentVersions.AddOrUpdate(identifier.Uri, 0, (uri, i) => 0); + } + + public void Remove(TextDocumentIdentifier identifier) + { + _documentVersions.TryRemove(identifier.Uri, out _); + } + } +} diff --git a/src/OmniSharp.LanguageServerProtocol/Handlers/OmniSharpCodeActionHandler.cs b/src/OmniSharp.LanguageServerProtocol/Handlers/OmniSharpCodeActionHandler.cs index cc903ac04c..793c32aa61 100644 --- a/src/OmniSharp.LanguageServerProtocol/Handlers/OmniSharpCodeActionHandler.cs +++ b/src/OmniSharp.LanguageServerProtocol/Handlers/OmniSharpCodeActionHandler.cs @@ -1,36 +1,55 @@ -using System; using System.Linq; using System.Collections.Generic; +using System.Diagnostics; using System.Threading; using System.Threading.Tasks; +using MediatR; +using Microsoft.CodeAnalysis; +using Newtonsoft.Json.Linq; using OmniSharp.Extensions.JsonRpc; +using OmniSharp.Extensions.LanguageServer.Protocol; +using OmniSharp.Extensions.LanguageServer.Protocol.Client.Capabilities; using OmniSharp.Extensions.LanguageServer.Protocol.Models; using OmniSharp.Extensions.LanguageServer.Protocol.Server; -using OmniSharp.Models; using OmniSharp.Models.V2.CodeActions; using OmniSharp.Extensions.LanguageServer.Protocol.Document; +using OmniSharp.Extensions.LanguageServer.Protocol.Workspace; +using OmniSharp.Models; +using Diagnostic = OmniSharp.Extensions.LanguageServer.Protocol.Models.Diagnostic; namespace OmniSharp.LanguageServerProtocol.Handlers { - internal sealed class OmniSharpCodeActionHandler : CodeActionHandler + internal sealed class OmniSharpCodeActionHandler : CodeActionHandler, IExecuteCommandHandler { - public static IEnumerable Enumerate(RequestHandlers handlers) + public static IEnumerable Enumerate( + RequestHandlers handlers, + ISerializer serializer, + ILanguageServer mediator, + DocumentVersions versions) { foreach (var (selector, getActionsHandler, runActionHandler) in handlers .OfType, Mef.IRequestHandler>()) { - yield return new OmniSharpCodeActionHandler(getActionsHandler, runActionHandler, selector); + yield return new OmniSharpCodeActionHandler(getActionsHandler, runActionHandler, selector, serializer, mediator, versions); } } private readonly Mef.IRequestHandler _getActionsHandler; - private readonly Mef.IRequestHandler _runActionHandler; + private readonly ExecuteCommandRegistrationOptions _executeCommandRegistrationOptions; + private ExecuteCommandCapability _executeCommandCapability; + private Mef.IRequestHandler _runActionHandler; + private readonly ISerializer _serializer; + private readonly ILanguageServer _server; + private readonly DocumentVersions _documentVersions; public OmniSharpCodeActionHandler( Mef.IRequestHandler getActionsHandler, Mef.IRequestHandler runActionHandler, - DocumentSelector documentSelector) + DocumentSelector documentSelector, + ISerializer serializer, + ILanguageServer server, + DocumentVersions documentVersions) : base(new CodeActionRegistrationOptions() { DocumentSelector = documentSelector, @@ -42,15 +61,22 @@ public OmniSharpCodeActionHandler( { _getActionsHandler = getActionsHandler; _runActionHandler = runActionHandler; + _serializer = serializer; + _server = server; + _documentVersions = documentVersions; + _executeCommandRegistrationOptions = new ExecuteCommandRegistrationOptions() + { + Commands = new Container("omnisharp/executeCodeAction"), + }; } - public async override Task Handle(CodeActionParams request, CancellationToken cancellationToken) + public override async Task Handle(CodeActionParams request, CancellationToken cancellationToken) { var omnisharpRequest = new GetCodeActionsRequest { FileName = Helpers.FromUri(request.TextDocument.Uri), - Column = (int)request.Range.Start.Character, - Line = (int)request.Range.Start.Line, + Column = request.Range.Start.Character, + Line = request.Range.Start.Line, Selection = Helpers.FromRange(request.Range), }; @@ -60,27 +86,6 @@ public async override Task Handle(CodeActionParams foreach (var ca in omnisharpResponse.CodeActions) { - var omnisharpCaRequest = new RunCodeActionRequest - { - Identifier = ca.Identifier, - FileName = Helpers.FromUri(request.TextDocument.Uri), - Column = Convert.ToInt32(request.Range.Start.Character), - Line = Convert.ToInt32(request.Range.Start.Line), - Selection = Helpers.FromRange(request.Range), - ApplyTextChanges = false, - WantsTextChanges = true, - }; - - var omnisharpCaResponse = await _runActionHandler.Handle(omnisharpCaRequest); - - var changes = omnisharpCaResponse.Changes.ToDictionary( - x => Helpers.ToUri(x.FileName), - x => ((ModifiedFileResponse)x).Changes.Select(edit => new TextEdit - { - NewText = edit.NewText, - Range = Helpers.ToRange((edit.StartColumn, edit.StartLine), (edit.EndColumn, edit.EndLine)) - })); - CodeActionKind kind; if (ca.Identifier.StartsWith("using ")) { kind = CodeActionKind.SourceOrganizeImports; } else if (ca.Identifier.StartsWith("Inline ")) { kind = CodeActionKind.RefactorInline; } @@ -94,12 +99,71 @@ public async override Task Handle(CodeActionParams Title = ca.Name, Kind = kind, Diagnostics = new Container(), - Edit = new WorkspaceEdit { Changes = changes, } + Edit = new WorkspaceEdit(), + Command = Command.Create("omnisharp/executeCodeAction") + .WithTitle(ca.Name) + .WithArguments(new CommandData() + { + Uri = request.TextDocument.Uri, + Identifier = ca.Identifier, + Name = ca.Name, + Range = request.Range, + }) }); } return new CommandOrCodeActionContainer( codeActions.Select(ca => new CommandOrCodeAction(ca))); } + + public async Task Handle(ExecuteCommandParams request, CancellationToken cancellationToken) + { + Debug.Assert(request.Command == "omnisharp/executeCodeAction"); + var data = request.Arguments[0].ToObject(_serializer.JsonSerializer); + + var omnisharpCaRequest = new RunCodeActionRequest { + Identifier = data.Identifier, + FileName = data.Uri.GetFileSystemPath(), + Column = data.Range.Start.Character, + Line = data.Range.Start.Line, + Selection = Helpers.FromRange(data.Range), + ApplyTextChanges = false, + WantsTextChanges = true, + WantsAllCodeActionOperations = true + }; + + var omnisharpCaResponse = await _runActionHandler.Handle(omnisharpCaRequest); + if (omnisharpCaResponse.Changes != null) + { + var edit = Helpers.ToWorkspaceEdit( + omnisharpCaResponse.Changes, + _server.ClientSettings.Capabilities.Workspace.WorkspaceEdit.Value, + _documentVersions + ); + ; + + await _server.Workspace.ApplyWorkspaceEdit(new ApplyWorkspaceEditParams() + { + Label = data.Name, + Edit = edit + }, cancellationToken); + + // Do something with response? + //if (response.Applied) + } + + return Unit.Value; + } + + class CommandData + { + public DocumentUri Uri { get; set;} + public string Identifier { get; set;} + public string Name { get; set;} + public Range Range { get; set;} + } + + ExecuteCommandRegistrationOptions IRegistration .GetRegistrationOptions() => _executeCommandRegistrationOptions; + void ICapability.SetCapability(ExecuteCommandCapability capability) => _executeCommandCapability = capability; } } diff --git a/src/OmniSharp.LanguageServerProtocol/Handlers/OmniSharpCodeLensHandler.cs b/src/OmniSharp.LanguageServerProtocol/Handlers/OmniSharpCodeLensHandler.cs index 86200f309b..44ecc8474d 100644 --- a/src/OmniSharp.LanguageServerProtocol/Handlers/OmniSharpCodeLensHandler.cs +++ b/src/OmniSharp.LanguageServerProtocol/Handlers/OmniSharpCodeLensHandler.cs @@ -127,14 +127,5 @@ private static void ToCodeLens(TextDocumentIdentifier textDocument, FileMemberEl } } } - - public override bool CanResolve(CodeLens value) - { - var textDocumentUri = value.Data.ToObject(); - - return textDocumentUri != null && - GetRegistrationOptions().DocumentSelector - .IsMatch(new TextDocumentAttributes(textDocumentUri, string.Empty)); - } } } diff --git a/src/OmniSharp.LanguageServerProtocol/Handlers/OmniSharpCompletionHandler.cs b/src/OmniSharp.LanguageServerProtocol/Handlers/OmniSharpCompletionHandler.cs index e731b2b682..1759499372 100644 --- a/src/OmniSharp.LanguageServerProtocol/Handlers/OmniSharpCompletionHandler.cs +++ b/src/OmniSharp.LanguageServerProtocol/Handlers/OmniSharpCompletionHandler.cs @@ -137,12 +137,7 @@ public async override Task Handle(CompletionParams request, Canc public override Task Handle(CompletionItem request, CancellationToken cancellationToken) { - throw new NotImplementedException(); - } - - public override bool CanResolve(CompletionItem value) - { - return false; + return Task.FromResult(request); } } } diff --git a/src/OmniSharp.LanguageServerProtocol/Handlers/OmniSharpExecuteCommandHandler.cs b/src/OmniSharp.LanguageServerProtocol/Handlers/OmniSharpExecuteCommandHandler.cs deleted file mode 100644 index 7fb876457f..0000000000 --- a/src/OmniSharp.LanguageServerProtocol/Handlers/OmniSharpExecuteCommandHandler.cs +++ /dev/null @@ -1,33 +0,0 @@ -using System.Collections.Generic; -using System.Threading; -using System.Threading.Tasks; -using MediatR; -using OmniSharp.Extensions.JsonRpc; -using OmniSharp.Extensions.LanguageServer.Protocol.Models; -using OmniSharp.Extensions.LanguageServer.Protocol.Server; -using OmniSharp.Extensions.LanguageServer.Protocol.Workspace; - -namespace OmniSharp.LanguageServerProtocol.Handlers -{ - class OmniSharpExecuteCommandHandler : ExecuteCommandHandler - { - public static IEnumerable Enumerate(RequestHandlers handlers) - { - yield return new OmniSharpExecuteCommandHandler(); - } - - public OmniSharpExecuteCommandHandler() - : base(new ExecuteCommandRegistrationOptions() - { - Commands = new Container(), - }) - { - } - - public override Task - Handle(ExecuteCommandParams request, CancellationToken cancellationToken) - { - return Task.FromResult(Unit.Value); - } - } -} diff --git a/src/OmniSharp.LanguageServerProtocol/Handlers/OmniSharpTextDocumentSyncHandler.cs b/src/OmniSharp.LanguageServerProtocol/Handlers/OmniSharpTextDocumentSyncHandler.cs index 8d98bc2da5..498066d4dc 100644 --- a/src/OmniSharp.LanguageServerProtocol/Handlers/OmniSharpTextDocumentSyncHandler.cs +++ b/src/OmniSharp.LanguageServerProtocol/Handlers/OmniSharpTextDocumentSyncHandler.cs @@ -23,7 +23,8 @@ class OmniSharpTextDocumentSyncHandler : TextDocumentSyncHandler { public static IEnumerable Enumerate( RequestHandlers handlers, - OmniSharpWorkspace workspace) + OmniSharpWorkspace workspace, + DocumentVersions documentVersions) { foreach (var (selector, openHandler, closeHandler, bufferHandler) in handlers .OfType< @@ -34,7 +35,7 @@ public static IEnumerable Enumerate( // TODO: Fix once cake has working support for incremental var documentSyncKind = TextDocumentSyncKind.Incremental; if (selector.ToString().IndexOf(".cake") > -1) documentSyncKind = TextDocumentSyncKind.Full; - yield return new OmniSharpTextDocumentSyncHandler(openHandler, closeHandler, bufferHandler, selector, documentSyncKind, workspace); + yield return new OmniSharpTextDocumentSyncHandler(openHandler, closeHandler, bufferHandler, selector, documentSyncKind, workspace, documentVersions); } } @@ -43,6 +44,7 @@ public static IEnumerable Enumerate( private readonly Mef.IRequestHandler _closeHandler; private readonly Mef.IRequestHandler _bufferHandler; private readonly OmniSharpWorkspace _workspace; + private readonly DocumentVersions _documentVersions; public OmniSharpTextDocumentSyncHandler( Mef.IRequestHandler openHandler, @@ -50,7 +52,8 @@ public OmniSharpTextDocumentSyncHandler( Mef.IRequestHandler bufferHandler, DocumentSelector documentSelector, TextDocumentSyncKind documentSyncKind, - OmniSharpWorkspace workspace) + OmniSharpWorkspace workspace, + DocumentVersions documentVersions) : base(documentSyncKind, new TextDocumentSaveRegistrationOptions() { DocumentSelector = documentSelector, @@ -61,6 +64,7 @@ public OmniSharpTextDocumentSyncHandler( _closeHandler = closeHandler; _bufferHandler = bufferHandler; _workspace = workspace; + _documentVersions = documentVersions; } public override TextDocumentAttributes GetTextDocumentAttributes(DocumentUri uri) @@ -107,6 +111,8 @@ await _bufferHandler.Handle(new UpdateBufferRequest() Changes = changes }); + _documentVersions.Update(notification.TextDocument); + return Unit.Value; } @@ -119,6 +125,8 @@ await _openHandler.Handle(new FileOpenRequest() Buffer = notification.TextDocument.Text, FileName = Helpers.FromUri(notification.TextDocument.Uri) }); + + _documentVersions.Reset(notification.TextDocument); } return Unit.Value; @@ -132,6 +140,8 @@ await _closeHandler.Handle(new FileCloseRequest() { FileName = Helpers.FromUri(notification.TextDocument.Uri) }); + + _documentVersions.Remove(notification.TextDocument); } return Unit.Value; @@ -146,6 +156,8 @@ await _bufferHandler.Handle(new UpdateBufferRequest() FileName = Helpers.FromUri(notification.TextDocument.Uri), Buffer = notification.Text }); + + _documentVersions.Reset(notification.TextDocument); } return Unit.Value; } diff --git a/src/OmniSharp.LanguageServerProtocol/Helpers.cs b/src/OmniSharp.LanguageServerProtocol/Helpers.cs index c7744b3e71..27c43862a6 100644 --- a/src/OmniSharp.LanguageServerProtocol/Helpers.cs +++ b/src/OmniSharp.LanguageServerProtocol/Helpers.cs @@ -1,8 +1,12 @@ using System; using System.Collections.Generic; using System.IO; +using System.Linq; +using System.Text.RegularExpressions; using OmniSharp.Extensions.LanguageServer.Protocol; +using OmniSharp.Extensions.LanguageServer.Protocol.Client.Capabilities; using OmniSharp.Extensions.LanguageServer.Protocol.Models; +using OmniSharp.LanguageServerProtocol.Handlers; using OmniSharp.Models; using OmniSharp.Models.Diagnostics; @@ -145,5 +149,97 @@ public static SymbolKind ToSymbolKind(string omnisharpKind) { return Kinds.TryGetValue(omnisharpKind.ToLowerInvariant(), out var symbolKind) ? symbolKind : SymbolKind.Class; } + + public static WorkspaceEdit ToWorkspaceEdit(IEnumerable responses, WorkspaceEditCapability workspaceEditCapability, DocumentVersions documentVersions) + { + workspaceEditCapability ??= new WorkspaceEditCapability(); + workspaceEditCapability.ResourceOperations ??= Array.Empty(); + + + if (workspaceEditCapability.DocumentChanges) + { + var documentChanges = new List(); + foreach (var response in responses) + { + documentChanges.Add(ToWorkspaceEditDocumentChange(response, workspaceEditCapability, + documentVersions)); + + } + + return new WorkspaceEdit() + { + DocumentChanges = documentChanges + }; + } + else + { + var changes = new Dictionary>(); + foreach (var response in responses) + { + changes.Add(DocumentUri.FromFileSystemPath(response.FileName), ToTextEdits(response)); + } + + return new WorkspaceEdit() + { + Changes = changes + }; + } + } + + public static WorkspaceEditDocumentChange ToWorkspaceEditDocumentChange(FileOperationResponse response, WorkspaceEditCapability workspaceEditCapability, DocumentVersions documentVersions) + { + workspaceEditCapability ??= new WorkspaceEditCapability(); + workspaceEditCapability.ResourceOperations ??= Array.Empty(); + + if (response is ModifiedFileResponse modified) + { + return new TextDocumentEdit() + { + Edits = new TextEditContainer(modified.Changes.Select(ToTextEdit)), + TextDocument = new VersionedTextDocumentIdentifier() + { + Version = documentVersions.GetVersion(DocumentUri.FromFileSystemPath(response.FileName)), + Uri = DocumentUri.FromFileSystemPath(response.FileName) + }, + }; + } + + if (response is RenamedFileResponse rename && workspaceEditCapability.ResourceOperations.Contains(ResourceOperationKind.Rename)) + { + return new RenameFile() + { + // Options = new RenameFileOptions() + // { + // Overwrite = true, + // IgnoreIfExists = false + // }, + NewUri = DocumentUri.FromFileSystemPath(rename.NewFileName).ToString(), + OldUri = DocumentUri.FromFileSystemPath(rename.FileName).ToString(), + }; + } + + return default; + } + + public static IEnumerable ToTextEdits(FileOperationResponse response) + { + if (!(response is ModifiedFileResponse modified)) yield break; + foreach (var change in modified.Changes) + { + yield return ToTextEdit(change); + } + } + + public static TextEdit ToTextEdit(LinePositionSpanTextChange textChange) + { + return new TextEdit() + { + NewText = textChange.NewText, + Range = ToRange( + (textChange.StartColumn, textChange.StartLine), + (textChange.EndColumn, textChange.EndLine) + ) + }; + } } } diff --git a/src/OmniSharp.LanguageServerProtocol/LanguageServerHost.cs b/src/OmniSharp.LanguageServerProtocol/LanguageServerHost.cs index 7db761b4ed..7ceb6a060b 100644 --- a/src/OmniSharp.LanguageServerProtocol/LanguageServerHost.cs +++ b/src/OmniSharp.LanguageServerProtocol/LanguageServerHost.cs @@ -99,6 +99,7 @@ private void ConfigureServices(IServiceCollection services) { Section = "omnisharp" }); + services.AddSingleton(new DocumentVersions()); } public void Dispose() @@ -184,6 +185,7 @@ IServiceCollection services var eventEmitter = new LanguageServerEventEmitter(server); services.AddSingleton(server); + var serviceProvider = CompositionHostBuilder.CreateDefaultServiceProvider(environment, configurationRoot, eventEmitter, services); @@ -326,9 +328,11 @@ internal static void RegisterHandlers(ILanguageServer server, CompositionHost co // TODO: Make it easier to resolve handlers from MEF (without having to add more attributes to the services if we can help it) var workspace = compositionHost.GetExport(); compositionHost.GetExport().IsEnabled = true; + var documentVersions = server.Services.GetRequiredService(); + var serializer = server.Services.GetRequiredService(); server.Register(s => { - foreach (var handler in OmniSharpTextDocumentSyncHandler.Enumerate(handlers, workspace) + foreach (var handler in OmniSharpTextDocumentSyncHandler.Enumerate(handlers, workspace, documentVersions) .Concat(OmniSharpDefinitionHandler.Enumerate(handlers)) .Concat(OmniSharpHoverHandler.Enumerate(handlers)) .Concat(OmniSharpCompletionHandler.Enumerate(handlers)) @@ -338,10 +342,9 @@ internal static void RegisterHandlers(ILanguageServer server, CompositionHost co .Concat(OmniSharpDocumentSymbolHandler.Enumerate(handlers)) .Concat(OmniSharpReferencesHandler.Enumerate(handlers)) .Concat(OmniSharpCodeLensHandler.Enumerate(handlers)) - .Concat(OmniSharpCodeActionHandler.Enumerate(handlers)) + .Concat(OmniSharpCodeActionHandler.Enumerate(handlers, serializer, server, documentVersions)) .Concat(OmniSharpDocumentFormattingHandler.Enumerate(handlers)) .Concat(OmniSharpDocumentFormatRangeHandler.Enumerate(handlers)) - .Concat(OmniSharpExecuteCommandHandler.Enumerate(handlers)) .Concat(OmniSharpDocumentOnTypeFormattingHandler.Enumerate(handlers))) { s.AddHandlers(handler); diff --git a/src/OmniSharp.Roslyn.CSharp/Workers/Diagnostics/CSharpDiagnosticWorker.cs b/src/OmniSharp.Roslyn.CSharp/Workers/Diagnostics/CSharpDiagnosticWorker.cs index 06f7816e73..496e31937f 100644 --- a/src/OmniSharp.Roslyn.CSharp/Workers/Diagnostics/CSharpDiagnosticWorker.cs +++ b/src/OmniSharp.Roslyn.CSharp/Workers/Diagnostics/CSharpDiagnosticWorker.cs @@ -16,12 +16,13 @@ namespace OmniSharp.Roslyn.CSharp.Workers.Diagnostics { - public class CSharpDiagnosticWorker: ICsDiagnosticWorker + public class CSharpDiagnosticWorker: ICsDiagnosticWorker, IDisposable { private readonly ILogger _logger; private readonly OmniSharpWorkspace _workspace; private readonly DiagnosticEventForwarder _forwarder; private readonly IObserver _openDocuments; + private readonly IDisposable _disposable; public CSharpDiagnosticWorker(OmniSharpWorkspace workspace, DiagnosticEventForwarder forwarder, ILoggerFactory loggerFactory) { @@ -36,7 +37,7 @@ public CSharpDiagnosticWorker(OmniSharpWorkspace workspace, DiagnosticEventForwa _workspace.DocumentOpened += OnDocumentOpened; _workspace.DocumentClosed += OnDocumentOpened; - openDocumentsSubject + _disposable = openDocumentsSubject .GroupByUntil(x => true, group => Observable.Amb( group.Throttle(TimeSpan.FromMilliseconds(200)), group.Distinct().Skip(99)) @@ -194,5 +195,13 @@ public Task> GetAllDiagnosticsAsync() var documents = _workspace.CurrentSolution.Projects.SelectMany(x => x.Documents).Select(x => x.FilePath).ToImmutableArray(); return GetDiagnostics(documents); } + + public void Dispose() + { + _workspace.WorkspaceChanged -= OnWorkspaceChanged; + _workspace.DocumentOpened -= OnDocumentOpened; + _workspace.DocumentClosed -= OnDocumentOpened; + _disposable.Dispose(); + } } } diff --git a/src/OmniSharp.Roslyn.CSharp/Workers/Diagnostics/CSharpDiagnosticWorkerWithAnalyzers.cs b/src/OmniSharp.Roslyn.CSharp/Workers/Diagnostics/CSharpDiagnosticWorkerWithAnalyzers.cs index a7bf6c94b6..8b5e5b102d 100644 --- a/src/OmniSharp.Roslyn.CSharp/Workers/Diagnostics/CSharpDiagnosticWorkerWithAnalyzers.cs +++ b/src/OmniSharp.Roslyn.CSharp/Workers/Diagnostics/CSharpDiagnosticWorkerWithAnalyzers.cs @@ -20,7 +20,7 @@ namespace OmniSharp.Roslyn.CSharp.Services.Diagnostics { - public class CSharpDiagnosticWorkerWithAnalyzers : ICsDiagnosticWorker + public class CSharpDiagnosticWorkerWithAnalyzers : ICsDiagnosticWorker, IDisposable { private readonly AnalyzerWorkQueue _workQueue; private readonly ILogger _logger; @@ -59,12 +59,11 @@ public CSharpDiagnosticWorkerWithAnalyzers( ?? throw new InvalidOperationException("Could not resolve 'Microsoft.CodeAnalysis.Diagnostics.WorkspaceAnalyzerOptions' for IDE analyzers."); _workspace.WorkspaceChanged += OnWorkspaceChanged; + _workspace.OnInitialized += OnWorkspaceInitialized; Task.Factory.StartNew(() => Worker(AnalyzerWorkType.Foreground), TaskCreationOptions.LongRunning); Task.Factory.StartNew(() => Worker(AnalyzerWorkType.Background), TaskCreationOptions.LongRunning); - _workspace.OnInitialized += (isInitialized) => OnWorkspaceInitialized(isInitialized); - OnWorkspaceInitialized(_workspace.Initialized); } @@ -321,5 +320,11 @@ public ImmutableArray QueueDocumentsForDiagnostics(ImmutableArray _providers; + private readonly ILoggerFactory _loggerFactory; + private readonly DiagnosticEventForwarder _forwarder; + private ICsDiagnosticWorker _implementation; + private readonly IDisposable _onChange; [ImportingConstructor] public CsharpDiagnosticWorkerComposer( @@ -23,15 +31,39 @@ public CsharpDiagnosticWorkerComposer( [ImportMany] IEnumerable providers, ILoggerFactory loggerFactory, DiagnosticEventForwarder forwarder, - OmniSharpOptions options) + IOptionsMonitor options) { - if(options.RoslynExtensionsOptions.EnableAnalyzersSupport) + _workspace = workspace; + _providers = providers; + _loggerFactory = loggerFactory; + _forwarder = forwarder; + _onChange = options.OnChange(UpdateImplementation); + UpdateImplementation(options.CurrentValue); + } + + private void UpdateImplementation(OmniSharpOptions options) + { + var firstRun = _implementation is null; + if (options.RoslynExtensionsOptions.EnableAnalyzersSupport && (firstRun || _implementation is CSharpDiagnosticWorker)) { - _implementation = new CSharpDiagnosticWorkerWithAnalyzers(workspace, providers, loggerFactory, forwarder, options); + var old = Interlocked.Exchange(ref _implementation, new CSharpDiagnosticWorkerWithAnalyzers(_workspace, _providers, _loggerFactory, _forwarder, options)); + if (old is IDisposable disposable) + { + disposable.Dispose(); + } } - else + else if (!options.RoslynExtensionsOptions.EnableAnalyzersSupport && (firstRun || _implementation is CSharpDiagnosticWorkerWithAnalyzers)) { - _implementation = new CSharpDiagnosticWorker(workspace, forwarder, loggerFactory); + var old = Interlocked.Exchange(ref _implementation, new CSharpDiagnosticWorker(_workspace, _forwarder, _loggerFactory)); + if (old is IDisposable disposable) + { + disposable.Dispose(); + } + + if (!firstRun) + { + _implementation.QueueDocumentsForDiagnostics(); + } } } @@ -54,5 +86,11 @@ public ImmutableArray QueueDocumentsForDiagnostics(ImmutableArray options .ConfigureLogging(x => x.AddLanguageProtocolLogging()) - .WithServices(services => - { - services.AddSingleton(_loggerFactory); - }) + .WithServices(services => { services.AddSingleton(_loggerFactory); }) .OnInitialize((server, request, token) => { - var config = new Microsoft.Extensions.Configuration.ConfigurationBuilder() + var configBuilder = new Microsoft.Extensions.Configuration.ConfigurationBuilder() .AddConfiguration(server.Configuration.GetSection("csharp")) - .AddConfiguration(server.Configuration.GetSection("omnisharp")) - .Build(); + .AddConfiguration(server.Configuration.GetSection("omnisharp")); + if (_setupConfiguration != null) configBuilder.AddConfiguration(_setupConfiguration); + var config = configBuilder.Build(); OmniSharpTestHost = CreateOmniSharpHost(config); - var handlers = LanguageServerHost.ConfigureCompositionHost(server, OmniSharpTestHost.CompositionHost); + var handlers = + LanguageServerHost.ConfigureCompositionHost(server, OmniSharpTestHost.CompositionHost); _host.UnderTest(OmniSharpTestHost.ServiceProvider, OmniSharpTestHost.CompositionHost); LanguageServerHost.RegisterHandlers(server, OmniSharpTestHost.CompositionHost, handlers); return Task.CompletedTask; @@ -79,10 +91,133 @@ protected override (Stream clientOutput, Stream serverInput) SetupServer() return (serverPipe.Reader.AsStream(), clientPipe.Writer.AsStream()); } + public async Task Restart(IConfiguration configuration = null, IDictionary configurationData = null) + { + _host.Dispose(); + Disposable.Remove(Client); + Client.Dispose(); + OmniSharpTestHost.Dispose(); + + _setupConfiguration ??= new Microsoft.Extensions.Configuration.ConfigurationBuilder() + .AddInMemoryCollection(configurationData ?? new Dictionary()) + .Build(); + await InitializeAsync(); + } + public async Task InitializeAsync() { - Client = await InitializeClient(x => { }); + Client = await InitializeClient(x => + { + x.WithCapability(new WorkspaceEditCapability() + { + DocumentChanges = true, + FailureHandling = FailureHandlingKind.Undo, + ResourceOperations = new[] + { + ResourceOperationKind.Create, ResourceOperationKind.Delete, ResourceOperationKind.Rename + } + }); + }); + Client.Register(c => c.OnApplyWorkspaceEdit(async @params => + { + if (@params.Edit?.Changes != null) + { + foreach (var change in @params.Edit.Changes) + { + var changes = change.Value + .Select(change => new LinePositionSpanTextChange() + { + NewText = change.NewText, + StartColumn = Convert.ToInt32(change.Range.Start.Character), + StartLine = Convert.ToInt32(change.Range.Start.Line), + EndColumn = Convert.ToInt32(change.Range.End.Character), + EndLine = Convert.ToInt32(change.Range.End.Line), + }) + .ToArray(); + + await OmniSharpTestHost.Workspace.BufferManager.UpdateBufferAsync(new UpdateBufferRequest() + { + FileName = LanguageServerProtocol.Helpers.FromUri(change.Key), + Changes = changes + }); + } + } + else if (@params.Edit?.DocumentChanges != null) + { + foreach (var change in @params.Edit.DocumentChanges) + { + if (change.IsTextDocumentEdit) + { + var contentChanges = change.TextDocumentEdit.Edits.ToArray(); + if (contentChanges.Length == 1 && contentChanges[0].Range == null) + { + var c = contentChanges[0]; + await OmniSharpTestHost.Workspace.BufferManager.UpdateBufferAsync( + new UpdateBufferRequest() + { + FileName = LanguageServerProtocol.Helpers.FromUri(change.TextDocumentEdit + .TextDocument.Uri), + Buffer = c.NewText + }); + } + else + { + var changes = contentChanges + .Select(change => new LinePositionSpanTextChange() + { + NewText = change.NewText, + StartColumn = Convert.ToInt32(change.Range.Start.Character), + StartLine = Convert.ToInt32(change.Range.Start.Line), + EndColumn = Convert.ToInt32(change.Range.End.Character), + EndLine = Convert.ToInt32(change.Range.End.Line), + }) + .ToArray(); + + await OmniSharpTestHost.Workspace.BufferManager.UpdateBufferAsync( + new UpdateBufferRequest() + { + FileName = LanguageServerProtocol.Helpers.FromUri(change.TextDocumentEdit + .TextDocument.Uri), + Changes = changes + }); + } + } + + if (change.IsRenameFile) + { + var documents = + OmniSharpTestHost.Workspace.GetDocuments(change.RenameFile.OldUri.GetFileSystemPath()); + foreach (var oldDocument in documents) + { + var text = await oldDocument.GetTextAsync(); + var newFilePath = change.RenameFile.NewUri.GetFileSystemPath(); + var newFileName = Path.GetFileName(newFilePath); + OmniSharpTestHost.Workspace.TryApplyChanges( + OmniSharpTestHost.Workspace.CurrentSolution + .RemoveDocument(oldDocument.Id) + .AddDocument( + DocumentId.CreateNewId(oldDocument.Project.Id, newFileName), + newFileName, + text, + oldDocument.Folders, + newFilePath + ) + ); + } + } + } + } + + await ClientEvents.SettleNext(); + + return new ApplyWorkspaceEditResponse() + { + Applied = true + }; + })); await startUpTask; + Configuration = new ConfigurationProvider(Server, Client, CancellationToken); + Client.Register(x => x.AddHandler(Configuration)); } public Task DisposeAsync() @@ -92,7 +227,26 @@ public Task DisposeAsync() return Task.CompletedTask; } + protected async Task AddProjectToWorkspace(ITestProject testProject) + { + var projectInfo = ProjectInfo.Create( + ProjectId.CreateNewId(), + VersionStamp.Create(), + testProject.Name, + testProject.Name, + LanguageNames.CSharp, + Directory.EnumerateFiles(testProject.Directory, "*.csproj", SearchOption.TopDirectoryOnly).Single() + ); + OmniSharpTestHost.Workspace.AddProject(projectInfo); + await OmniSharpTestHost.RestoreProject(testProject); + await OmniSharpTestHost.GetFilesChangedService().Handle(Directory.GetFiles(testProject.Directory) + .Select(file => new FilesChangedRequest() {FileName = file, ChangeType = FileWatching.FileChangeType.Create})); + var project = OmniSharpTestHost.Workspace.CurrentSolution.GetProject(projectInfo.Id); + return project; + } + protected OmniSharpTestHost OmniSharpTestHost { get; private set; } + protected ConfigurationProvider Configuration { get; private set; } protected ILanguageClient Client { get; private set; } protected ILanguageServer Server => _host.Server; diff --git a/tests/OmniSharp.Lsp.Tests/ConfigurationProvider.cs b/tests/OmniSharp.Lsp.Tests/ConfigurationProvider.cs new file mode 100644 index 0000000000..687c448234 --- /dev/null +++ b/tests/OmniSharp.Lsp.Tests/ConfigurationProvider.cs @@ -0,0 +1,192 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using MediatR; +using Microsoft.Extensions.Configuration; +using Newtonsoft.Json.Linq; +using OmniSharp.Extensions.LanguageServer.Protocol; +using OmniSharp.Extensions.LanguageServer.Protocol.Client; +using OmniSharp.Extensions.LanguageServer.Protocol.Models; +using OmniSharp.Extensions.LanguageServer.Protocol.Server; +using OmniSharp.Extensions.LanguageServer.Protocol.Workspace; +using TestUtility; + +namespace OmniSharp.Lsp.Tests +{ + public class ConfigurationProvider: IConfigurationHandler + { + private readonly ILanguageServer _server; + private readonly ILanguageClient _client; + private readonly CancellationToken _cancellationToken; + + private readonly ConcurrentDictionary<(string section, DocumentUri scope), IConfiguration> _scopedConfigurations = + new ConcurrentDictionary<(string section, DocumentUri scope), IConfiguration>(); + + public ConfigurationProvider(ILanguageServer server, ILanguageClient client, + CancellationToken cancellationToken) + { + _server = server; + _client = client; + _cancellationToken = cancellationToken; + } + + public Task Update(string section, IDictionary configuration) + { + if (configuration == null) return Task.CompletedTask; + return Update(section, new Microsoft.Extensions.Configuration.ConfigurationBuilder().AddInMemoryCollection(configuration).Build()); + } + + public Task Update(string section, IConfiguration configuration) + { + if (configuration == null) return Task.CompletedTask; + return Update(section, null, configuration); + } + + public Task Update(string section, DocumentUri documentUri, IDictionary configuration) + { + if (configuration == null) return Task.CompletedTask; + return Update(section, documentUri, new Microsoft.Extensions.Configuration.ConfigurationBuilder().AddInMemoryCollection(configuration).Build()); + } + + public Task Update(string section, DocumentUri documentUri, IConfiguration configuration) + { + if (configuration == null) return Task.CompletedTask; + _scopedConfigurations.AddOrUpdate((section, documentUri), configuration, (a, _) => configuration); + return TriggerChange(); + } + + public Task Reset(string section) + { + return Reset(section, null); + } + + public Task Reset(string section, DocumentUri documentUri) + { + _scopedConfigurations.TryRemove((section, documentUri), out _); + _client.Workspace.DidChangeConfiguration(new DidChangeConfigurationParams()); + return TriggerChange(); + } + + private IConfiguration Get(ConfigurationItem configurationItem) + { + if (_scopedConfigurations.TryGetValue( + (configurationItem.Section, configurationItem.ScopeUri), + out var configuration) + ) + { + return new Microsoft.Extensions.Configuration.ConfigurationBuilder() + .AddConfiguration(configuration, false) + .Build(); + } + + return new Microsoft.Extensions.Configuration.ConfigurationBuilder().Build(); + } + + private async Task TriggerChange() + { + _client.Workspace.DidChangeConfiguration(new DidChangeConfigurationParams()); + await _server.Configuration.WaitForChange(_cancellationToken); + } + + Task> IRequestHandler>. Handle(ConfigurationParams request, CancellationToken cancellationToken) + { + var results = new List(); + foreach (var item in request.Items) + { + var config = Get(item); + results.Add(Parse(config.AsEnumerable(true).Where(x => x.Value != null))); + } + + return Task.FromResult>(results); + } + + private JObject Parse(IEnumerable> values) + { + if (values == null) + { + throw new ArgumentNullException(nameof(values)); + } + + var result = new JObject(); + foreach (var item in values) + { + var keys = item.Key.Split(new [] { ":" }, StringSplitOptions.RemoveEmptyEntries); + var prop = keys.Last(); + JToken root = result; + + // This produces a simple look ahead + var zippedKeys = keys + .Zip(keys.Skip(1), (prev, current) => (prev, current)); + + foreach (var (key, next) in zippedKeys) + { + if (int.TryParse(next, out var value)) + { + root = SetValueToToken(root, key, new JArray()); + } + else + { + root = SetValueToToken(root, key, new JObject()); + } + } + + SetValueToToken(root, prop, new JValue(item.Value)); + } + return result; + } + private T SetValueToToken(JToken root, string key, T value) + where T : JToken + { + var currentValue = GetValueFromToken(root, key); + if (currentValue == null || currentValue.Type == JTokenType.Null) + { + if (root is JArray arr) + { + if (int.TryParse(key, out var index)) + { + if (arr.Count <= index) + { + while (arr.Count < index) + arr.Add(null!); + arr.Add(value); + } + else + { + arr[index] = value; + } + + return value; + } + } + else + { + root[key] = value; + return value; + } + } + + if (root is JArray arr2 && int.TryParse(key, out var i)) + { + return (T)arr2[i]; + } + return root[key] as T; + } + + private static JToken GetValueFromToken(JToken root, string key) + { + if (root is JArray arr) + { + if (int.TryParse(key, out var index)) + { + if (arr.Count <= index) return null; + return arr[index]; + } + throw new IndexOutOfRangeException(key); + } + return root[key]; + } + } +} diff --git a/tests/OmniSharp.Lsp.Tests/LanguageServerFoundationFacts.cs b/tests/OmniSharp.Lsp.Tests/LanguageServerFoundationFacts.cs index 2509f1fe2d..2e115b6a19 100644 --- a/tests/OmniSharp.Lsp.Tests/LanguageServerFoundationFacts.cs +++ b/tests/OmniSharp.Lsp.Tests/LanguageServerFoundationFacts.cs @@ -1,4 +1,5 @@ -using System.Linq; +using System.Collections.Generic; +using System.Linq; using System.Threading.Tasks; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; @@ -27,37 +28,23 @@ public async Task Language_server_contributes_configuration_from_client() { var options = OmniSharpTestHost.ServiceProvider.GetRequiredService>(); - - using (Client.Register(x => x.OnConfiguration(request => - { - return Task.FromResult(new Container(Enumerable.Select(request.Items, item => - item.Section == "csharp" ? new JObject() - { - ["FormattingOptions"] = new JObject() {["IndentationSize"] = 12,} - } : - item.Section == "omnisharp" ? new JObject() - { - ["RenameOptions"] = new JObject() {["RenameOverloads"] = true,} - } : new JObject()))); - }))) - { - ChangeToken.OnChange(Server.Configuration.GetReloadToken, - () => { Logger?.LogCritical("Server Reloaded!"); }); - - ChangeToken.OnChange( - OmniSharpTestHost.ServiceProvider.GetRequiredService().GetReloadToken, - () => { Logger?.LogCritical("Host Reloaded!"); }); - - var originalIndentationSize = options.CurrentValue.FormattingOptions.IndentationSize; - - Client.Workspace.DidChangeConfiguration(new DidChangeConfigurationParams() {Settings = null}); - - await options.WaitForChange(CancellationToken); - - Assert.NotEqual(originalIndentationSize, options.CurrentValue.FormattingOptions.IndentationSize); - Assert.Equal(12, options.CurrentValue.FormattingOptions.IndentationSize); - Assert.True(options.CurrentValue.RenameOptions.RenameOverloads); - } + var originalIndentationSize = options.CurrentValue.FormattingOptions.IndentationSize; + + await Task.WhenAll( + Configuration.Update("csharp", new Dictionary() + { + ["FormattingOptions:IndentationSize"] = "12", + }), + Configuration.Update("omnisharp", new Dictionary() + { + ["RenameOptions:RenameOverloads"] = "true", + }), + options.WaitForChange(CancellationToken) + ); + + Assert.NotEqual(originalIndentationSize, options.CurrentValue.FormattingOptions.IndentationSize); + Assert.Equal(12, options.CurrentValue.FormattingOptions.IndentationSize); + Assert.True(options.CurrentValue.RenameOptions.RenameOverloads); } } } diff --git a/tests/OmniSharp.Lsp.Tests/OmniSharpCodeActionHandlerFacts.cs b/tests/OmniSharp.Lsp.Tests/OmniSharpCodeActionHandlerFacts.cs new file mode 100644 index 0000000000..5d9055e3f7 --- /dev/null +++ b/tests/OmniSharp.Lsp.Tests/OmniSharpCodeActionHandlerFacts.cs @@ -0,0 +1,331 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.Extensions.Configuration; +using Newtonsoft.Json; +using OmniSharp.Extensions.LanguageServer.Protocol; +using OmniSharp.Extensions.LanguageServer.Protocol.Document; +using OmniSharp.Extensions.LanguageServer.Protocol.Models; +using OmniSharp.Extensions.LanguageServer.Protocol.Workspace; +using OmniSharp.Models; +using OmniSharp.Models.V2; +using OmniSharp.Models.V2.CodeActions; +using OmniSharp.Roslyn.CSharp.Services.Refactoring.V2; +using TestUtility; +using Xunit; +using Xunit.Abstractions; +using Xunit.Sdk; +using Range = OmniSharp.Extensions.LanguageServer.Protocol.Models.Range; + +namespace OmniSharp.Lsp.Tests +{ + public class OmniSharpCodeActionHandlerFacts : AbstractLanguageServerTestBase + { + public OmniSharpCodeActionHandlerFacts(ITestOutputHelper output) + : base(output) { } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public async Task Can_get_code_actions_from_roslyn(bool roslynAnalyzersEnabled) + { + const string code = + @"public class Class1 + { + public void Whatever() + { + Gu[||]id.NewGuid(); + } + }"; + + var refactorings = await FindRefactoringNamesAsync(code, roslynAnalyzersEnabled); + Assert.Contains("using System;", refactorings); + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public async Task Can_get_code_actions_from_external_source(bool roslynAnalyzersEnabled) + { + await Restart(configurationData: new Dictionary + { + {"RoslynExtensionsOptions:LocationPaths:0", TestAssets.Instance.TestBinariesFolder}, + }); + const string code = + @" + using System.Threading.Tasks; + public class Class1 + { + public async Task Whatever() + { + awa[||]it FooAsync(); + } + + public Task FooAsync() => return Task.FromResult(0); + }"; + + var refactorings = await FindRefactoringsAsync(code, + TestHelpers.GetConfigurationDataWithAnalyzerConfig(roslynAnalyzersEnabled)); + + Assert.NotEmpty(refactorings); + Assert.Contains("Add ConfigureAwait(false)", refactorings.Select(x => x.Title)); + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public async Task Can_remove_unnecessary_usings(bool roslynAnalyzersEnabled) + { + const string code = + @"using MyNamespace3; + using MyNamespace4; + using MyNamespace2; + using System; + u[||]sing MyNamespace1; + + public class c {public c() {Guid.NewGuid();}}"; + + const string expected = + @"using System; + + public class c {public c() {Guid.NewGuid();}}"; + + var response = + (await RunRefactoringAsync(code, "Remove Unnecessary Usings", + isAnalyzersEnabled: roslynAnalyzersEnabled)).Single(); + var updatedText = await OmniSharpTestHost.Workspace.GetDocument(response.FileName).GetTextAsync(); + AssertUtils.AssertIgnoringIndent(expected, updatedText.ToString()); + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public async Task Can_get_ranged_code_action(bool roslynAnalyzersEnabled) + { + const string code = + @"public class Class1 + { + public void Whatever() + { + [|Console.Write(""should be using System;"");|] + } + }"; + + var refactorings = await FindRefactoringNamesAsync(code, roslynAnalyzersEnabled); + Assert.Contains("Extract method", refactorings); + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public async Task Returns_ordered_code_actions(bool roslynAnalyzersEnabled) + { + const string code = + @"public class Class1 + { + public void Whatever() + { + [|Console.Write(""should be using System;"");|] + } + }"; + + var refactorings = await FindRefactoringNamesAsync(code, roslynAnalyzersEnabled); + + List expected = roslynAnalyzersEnabled + ? new List + { + "Fix formatting", + "using System;", + "System.Console", + "Generate variable 'Console' -> Generate property 'Class1.Console'", + "Generate variable 'Console' -> Generate field 'Class1.Console'", + "Generate variable 'Console' -> Generate read-only field 'Class1.Console'", + "Generate variable 'Console' -> Generate local 'Console'", + "Generate variable 'Console' -> Generate parameter 'Console'", + "Generate type 'Console' -> Generate class 'Console' in new file", + "Generate type 'Console' -> Generate class 'Console'", + "Generate type 'Console' -> Generate nested class 'Console'", + "Extract local function", + "Extract method", + "Introduce local for 'Console.Write(\"should be using System;\")'" + } + : new List + { + "using System;", + "System.Console", + "Generate variable 'Console' -> Generate property 'Class1.Console'", + "Generate variable 'Console' -> Generate field 'Class1.Console'", + "Generate variable 'Console' -> Generate read-only field 'Class1.Console'", + "Generate variable 'Console' -> Generate local 'Console'", + "Generate variable 'Console' -> Generate parameter 'Console'", + "Generate type 'Console' -> Generate class 'Console' in new file", + "Generate type 'Console' -> Generate class 'Console'", + "Generate type 'Console' -> Generate nested class 'Console'", + "Extract local function", + "Extract method", + "Introduce local for 'Console.Write(\"should be using System;\")'" + }; + Assert.Equal(expected.OrderBy(x => x), refactorings.OrderBy(x => x)); + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public async Task Can_extract_method(bool roslynAnalyzersEnabled) + { + const string code = + @"public class Class1 + { + public void Whatever() + { + [|Console.Write(""should be using System;"");|] + } + }"; + const string expected = + @"public class Class1 + { + public void Whatever() + { + NewMethod(); + } + + private static void NewMethod() + { + Console.Write(""should be using System;""); + } + }"; + var response = + (await RunRefactoringAsync(code, "Extract Method", isAnalyzersEnabled: roslynAnalyzersEnabled)) + .Single(); + var updatedText = await OmniSharpTestHost.Workspace.GetDocument(response.FileName).GetTextAsync(); + AssertUtils.AssertIgnoringIndent(expected, updatedText.ToString()); + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public async Task Can_generate_type_and_return_name_of_new_file(bool roslynAnalyzersEnabled) + { + await Configuration.Update("omnisharp", + TestHelpers.GetConfigurationDataWithAnalyzerConfig(roslynAnalyzersEnabled)); + using var testProject = await TestAssets.Instance.GetTestProjectAsync("ProjectWithMissingType"); + var project = await AddProjectToWorkspace(testProject); + var document = project.Documents.First(); + + await Client.ExecuteCommand(Command.Create("omnisharp/executeCodeAction") + .WithArguments(new + { + Uri = DocumentUri.FromFileSystemPath(document.FilePath), + Identifier = "Generate class 'Z' in new file", + Name = "N/A", + Range = new Range((8, 12), (8, 12)), + }), CancellationToken); + + var updatedDocument = OmniSharpTestHost.Workspace.GetDocument(Path.Combine(Path.GetDirectoryName(document.FilePath), "Z.cs")); + var updateDocumentText = await updatedDocument.GetTextAsync(); + + Assert.Equal(@"namespace ConsoleApplication +{ + internal class Z + { + } +}".Replace("\r\n", "\n"), updateDocumentText.ToString()); + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public async Task Can_send_rename_and_fileOpen_responses_when_codeAction_renames_file(bool roslynAnalyzersEnabled) + { + await Restart(TestHelpers.GetConfigurationDataWithAnalyzerConfig(roslynAnalyzersEnabled)); + using var testProject = await TestAssets.Instance.GetTestProjectAsync("ProjectWithMismatchedFileName"); + var project = await AddProjectToWorkspace(testProject); + var document = project.Documents.First(); + + await Client.ExecuteCommand(Command.Create("omnisharp/executeCodeAction") + .WithArguments(new + { + Uri = DocumentUri.FromFileSystemPath(document.FilePath), + Identifier = "Rename file to Class1.cs", + Name = "N/A", + Range = new Range((4, 10), (4, 10)), + }), CancellationToken); + + Assert.Empty(OmniSharpTestHost.Workspace.GetDocuments(document.FilePath)); + + Assert.NotEmpty(OmniSharpTestHost.Workspace.GetDocuments( + Path.Combine(Path.GetDirectoryName(document.FilePath), "Class1.cs") + )); + } + + private async Task> RunRefactoringAsync(string code, string refactoringName, + bool isAnalyzersEnabled = true) + { + var refactorings = await FindRefactoringsAsync(code, + configurationData: TestHelpers.GetConfigurationDataWithAnalyzerConfig(isAnalyzersEnabled)); + Assert.Contains(refactoringName, refactorings.Select(x => x.Title), StringComparer.OrdinalIgnoreCase); + + var command = refactorings + .First(action => action.Title.Equals(refactoringName, StringComparison.OrdinalIgnoreCase)).Command; + return await RunRefactoringsAsync(code, command); + } + + private async Task> FindRefactoringNamesAsync(string code, bool isAnalyzersEnabled = true) + { + var codeActions = await FindRefactoringsAsync(code, + TestHelpers.GetConfigurationDataWithAnalyzerConfig(isAnalyzersEnabled)); + + return codeActions.Select(a => a.Title); + } + + private async Task> FindRefactoringsAsync(string code, + IConfiguration configurationData = null) + { + var bufferPath = + $"{Directory.GetCurrentDirectory()}{Path.DirectorySeparatorChar}somepath{Path.DirectorySeparatorChar}buffer.cs"; + var testFile = new TestFile(bufferPath, code); + OmniSharpTestHost.AddFilesToWorkspace(testFile); + await Configuration.Update("csharp", configurationData); + + var span = testFile.Content.GetSpans().Single(); + var range = GetSelection(testFile.Content.GetRangeFromSpan(span)); + var response = await Client.RequestCodeAction(new CodeActionParams() + { + Context = new CodeActionContext() { }, + Range = LanguageServerProtocol.Helpers.ToRange(range), + TextDocument = new TextDocumentIdentifier(bufferPath) + }); + + return response.Where(z => z.IsCodeAction).Select(z => z.CodeAction); + } + + private async Task> RunRefactoringsAsync(string code, Command command) + { + var bufferPath = + $"{Directory.GetCurrentDirectory()}{Path.DirectorySeparatorChar}somepath{Path.DirectorySeparatorChar}buffer.cs"; + var testFile = new TestFile(bufferPath, code); + + OmniSharpTestHost.AddFilesToWorkspace(testFile); + + await Client.Workspace.ExecuteCommand(command); + + return new[] {testFile}; + } + + private static Models.V2.Range GetSelection(TextRange range) + { + if (range.IsEmpty) + { + return null; + } + + return new Models.V2.Range + { + Start = new Point {Line = range.Start.Line, Column = range.Start.Offset}, + End = new Point {Line = range.End.Line, Column = range.End.Offset} + }; + } + } +}