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}
+ };
+ }
+ }
+}