diff --git a/src/Lsp/Capabilities/Client/Supports.cs b/src/Lsp/Capabilities/Client/Supports.cs index 0dba70d84..15308584b 100644 --- a/src/Lsp/Capabilities/Client/Supports.cs +++ b/src/Lsp/Capabilities/Client/Supports.cs @@ -1,4 +1,4 @@ -using System; +using System; using Newtonsoft.Json; using OmniSharp.Extensions.LanguageServer.Converters; @@ -38,8 +38,9 @@ public static implicit operator Supports(T value) public static class Supports { public static Supports OfValue(T value) + where T : class { - return new Supports(true, value); + return new Supports(value != null, value); } public static Supports OfBoolean(bool isSupported) diff --git a/src/Lsp/Capabilities/Client/TextDocumentClientCapabilities.cs b/src/Lsp/Capabilities/Client/TextDocumentClientCapabilities.cs index 762ede3ef..0d3f7e20b 100644 --- a/src/Lsp/Capabilities/Client/TextDocumentClientCapabilities.cs +++ b/src/Lsp/Capabilities/Client/TextDocumentClientCapabilities.cs @@ -1,4 +1,5 @@ -using Newtonsoft.Json; +using System.ComponentModel; +using Newtonsoft.Json; using Newtonsoft.Json.Serialization; namespace OmniSharp.Extensions.LanguageServer.Capabilities.Client @@ -8,74 +9,119 @@ public class TextDocumentClientCapabilities { public Supports Synchronization { get; set; } + [EditorBrowsable(EditorBrowsableState.Never)] + public bool ShouldSerializeSynchronization() => Synchronization.IsSupported; + /// /// Capabilities specific to the `textDocument/completion` /// public Supports Completion { get; set; } + [EditorBrowsable(EditorBrowsableState.Never)] + public bool ShouldSerializeCompletion() => Completion.IsSupported; + /// /// Capabilities specific to the `textDocument/hover` /// public Supports Hover { get; set; } + [EditorBrowsable(EditorBrowsableState.Never)] + public bool ShouldSerializeHover() => Hover.IsSupported; + /// /// Capabilities specific to the `textDocument/signatureHelp` /// public Supports SignatureHelp { get; set; } + [EditorBrowsable(EditorBrowsableState.Never)] + public bool ShouldSerializeSignatureHelp() => SignatureHelp.IsSupported; + /// /// Capabilities specific to the `textDocument/references` /// public Supports References { get; set; } + [EditorBrowsable(EditorBrowsableState.Never)] + public bool ShouldSerializeReferences() => References.IsSupported; + /// /// Capabilities specific to the `textDocument/documentHighlight` /// public Supports DocumentHighlight { get; set; } + [EditorBrowsable(EditorBrowsableState.Never)] + public bool ShouldSerializeDocumentHighlight() => DocumentHighlight.IsSupported; + /// /// Capabilities specific to the `textDocument/documentSymbol` /// public Supports DocumentSymbol { get; set; } + [EditorBrowsable(EditorBrowsableState.Never)] + public bool ShouldSerializeDocumentSymbol() => DocumentSymbol.IsSupported; + /// /// Capabilities specific to the `textDocument/formatting` /// public Supports Formatting { get; set; } + [EditorBrowsable(EditorBrowsableState.Never)] + public bool ShouldSerializeFormatting() => Formatting.IsSupported; + /// /// Capabilities specific to the `textDocument/rangeFormatting` /// public Supports RangeFormatting { get; set; } + [EditorBrowsable(EditorBrowsableState.Never)] + public bool ShouldSerializeRangeFormatting() => RangeFormatting.IsSupported; + /// /// Capabilities specific to the `textDocument/onTypeFormatting` /// public Supports OnTypeFormatting { get; set; } + [EditorBrowsable(EditorBrowsableState.Never)] + public bool ShouldSerializeOnTypeFormatting() => OnTypeFormatting.IsSupported; + /// /// Capabilities specific to the `textDocument/definition` /// public Supports Definition { get; set; } + [EditorBrowsable(EditorBrowsableState.Never)] + public bool ShouldSerializeDefinition() => Definition.IsSupported; + /// /// Capabilities specific to the `textDocument/codeAction` /// public Supports CodeAction { get; set; } + [EditorBrowsable(EditorBrowsableState.Never)] + public bool ShouldSerializeCodeAction() => CodeAction.IsSupported; + /// /// Capabilities specific to the `textDocument/codeLens` /// public Supports CodeLens { get; set; } + [EditorBrowsable(EditorBrowsableState.Never)] + public bool ShouldSerializeCodeLens() => CodeLens.IsSupported; + /// /// Capabilities specific to the `textDocument/documentLink` /// public Supports DocumentLink { get; set; } + [EditorBrowsable(EditorBrowsableState.Never)] + public bool ShouldSerializeDocumentLink() => DocumentLink.IsSupported; + /// /// Capabilities specific to the `textDocument/rename` /// public Supports Rename { get; set; } + + [EditorBrowsable(EditorBrowsableState.Never)] + public bool ShouldSerializeRename() => Rename.IsSupported; } } diff --git a/src/Lsp/Capabilities/Client/WorkspaceClientCapabilites.cs b/src/Lsp/Capabilities/Client/WorkspaceClientCapabilites.cs index 9137bc593..605530355 100644 --- a/src/Lsp/Capabilities/Client/WorkspaceClientCapabilites.cs +++ b/src/Lsp/Capabilities/Client/WorkspaceClientCapabilites.cs @@ -1,4 +1,5 @@ -using Newtonsoft.Json; +using System.ComponentModel; +using Newtonsoft.Json; using Newtonsoft.Json.Serialization; namespace OmniSharp.Extensions.LanguageServer.Capabilities.Client @@ -10,28 +11,46 @@ public class WorkspaceClientCapabilites /// The client supports applying batch edits /// to the workspace. /// - public bool ApplyEdit { get; set; } + public Supports ApplyEdit { get; set; } + + [EditorBrowsable(EditorBrowsableState.Never)] + public bool ShouldSerializeApplyEdit() => ApplyEdit.IsSupported; public Supports WorkspaceEdit { get; set; } + [EditorBrowsable(EditorBrowsableState.Never)] + public bool ShouldSerializeWorkspaceEdit() => WorkspaceEdit.IsSupported; + /// /// Capabilities specific to the `workspace/didChangeConfiguration` notification. /// public Supports DidChangeConfiguration { get; set; } + [EditorBrowsable(EditorBrowsableState.Never)] + public bool ShouldSerializeDidChangeConfiguration() => DidChangeConfiguration.IsSupported; + /// /// Capabilities specific to the `workspace/didChangeWatchedFiles` notification. /// public Supports DidChangeWatchedFiles { get; set; } + [EditorBrowsable(EditorBrowsableState.Never)] + public bool ShouldSerializeDidChangeWatchedFiles() => DidChangeWatchedFiles.IsSupported; + /// /// Capabilities specific to the `workspace/symbol` request. /// public Supports Symbol { get; set; } + [EditorBrowsable(EditorBrowsableState.Never)] + public bool ShouldSerializeSymbol() => Symbol.IsSupported; + /// /// Capabilities specific to the `workspace/executeCommand` request. /// public Supports ExecuteCommand { get; set; } + + [EditorBrowsable(EditorBrowsableState.Never)] + public bool ShouldSerializeExecuteCommand() => ExecuteCommand.IsSupported; } } diff --git a/src/Lsp/Converters/SupportsConverter.cs b/src/Lsp/Converters/SupportsConverter.cs index 7e9d2810f..eba142d0f 100644 --- a/src/Lsp/Converters/SupportsConverter.cs +++ b/src/Lsp/Converters/SupportsConverter.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Reflection; using Newtonsoft.Json; using OmniSharp.Extensions.LanguageServer.Capabilities.Client; @@ -35,7 +35,7 @@ public override void WriteJson(JsonWriter writer, object value, JsonSerializer s } else { - serializer.Serialize(writer, false); + serializer.Serialize(writer, null); } } @@ -44,6 +44,10 @@ public override object ReadJson(JsonReader reader, Type objectType, object exist var targetType = objectType.GetTypeInfo().GetGenericArguments()[0]; if (reader.TokenType == JsonToken.Boolean) { + if (targetType == typeof(bool)) + { + return new Supports(true, (bool)reader.Value); + } return OfBooleanMethod .MakeGenericMethod(targetType) .Invoke(null, new [] { reader.Value }); @@ -60,4 +64,4 @@ public override object ReadJson(JsonReader reader, Type objectType, object exist public override bool CanConvert(Type objectType) => objectType.GetGenericTypeDefinition() == typeof(Supports<>); } -} \ No newline at end of file +} diff --git a/src/Lsp/HandlerCollection.cs b/src/Lsp/HandlerCollection.cs index 386933e08..5203f58cd 100644 --- a/src/Lsp/HandlerCollection.cs +++ b/src/Lsp/HandlerCollection.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections; using System.Collections.Generic; using System.Linq; @@ -48,9 +48,11 @@ public IDisposable Add(params IJsonRpcHandler[] handlers) } var key = "default"; - if (handler is IRegistration textDocumentRegistration) + if (handler is IRegistration) { - var options = textDocumentRegistration.GetRegistrationOptions(); + var options = GetTextDocumentRegistrationOptionsMethod + .MakeGenericMethod(registration) + .Invoke(handler, new object[] { handler }) as TextDocumentRegistrationOptions; key = options.DocumentSelector; } @@ -76,6 +78,15 @@ public IDisposable Add(params IJsonRpcHandler[] handlers) return new ImmutableDisposable(descriptors); } + private static readonly MethodInfo GetTextDocumentRegistrationOptionsMethod = typeof(HandlerCollection).GetTypeInfo() + .GetMethod(nameof(GetTextDocumentRegistrationOptions), BindingFlags.Static | BindingFlags.NonPublic); + + private static TextDocumentRegistrationOptions GetTextDocumentRegistrationOptions(IRegistration instance) + where T : TextDocumentRegistrationOptions + { + return instance.GetRegistrationOptions(); + } + private Type UnwrapGenericType(Type genericType, Type type) { return type?.GetTypeInfo() diff --git a/src/Lsp/InitializeDelegate.cs b/src/Lsp/InitializeDelegate.cs new file mode 100644 index 000000000..0d2348bf0 --- /dev/null +++ b/src/Lsp/InitializeDelegate.cs @@ -0,0 +1,7 @@ +using System.Threading.Tasks; +using OmniSharp.Extensions.LanguageServer.Models; + +namespace OmniSharp.Extensions.LanguageServer +{ + public delegate Task InitializeDelegate(InitializeParams request); +} \ No newline at end of file diff --git a/src/Lsp/LanguageServer.cs b/src/Lsp/LanguageServer.cs index a9bdca31c..96723b0cd 100644 --- a/src/Lsp/LanguageServer.cs +++ b/src/Lsp/LanguageServer.cs @@ -18,8 +18,6 @@ namespace OmniSharp.Extensions.LanguageServer { - public delegate Task InitializeDelegate(InitializeParams request); - public class LanguageServer : ILanguageServer, IInitializeHandler, IInitializedHandler, IDisposable, IAwaitableTermination { private readonly Connection _connection; diff --git a/src/Lsp/LspRequestRouter.cs b/src/Lsp/LspRequestRouter.cs index 706c5af13..8f04385f2 100644 --- a/src/Lsp/LspRequestRouter.cs +++ b/src/Lsp/LspRequestRouter.cs @@ -1,5 +1,6 @@ -using System; +using System; using System.Collections.Concurrent; +using System.Collections.Generic; using System.Linq; using System.Reflection; using System.Threading; @@ -18,7 +19,7 @@ namespace OmniSharp.Extensions.LanguageServer class LspRequestRouter : IRequestRouter { private readonly IHandlerCollection _collection; - private ITextDocumentSyncHandler _textDocumentSyncHandler; + private ITextDocumentSyncHandler[] _textDocumentSyncHandlers; private readonly ConcurrentDictionary _requests = new ConcurrentDictionary(); public LspRequestRouter(IHandlerCollection collection) @@ -43,22 +44,23 @@ private string GetId(object id) private ILspHandlerDescriptor FindDescriptor(string method, JToken @params) { - var descriptor = _collection.FirstOrDefault(x => x.Method == method); + var descriptor = _collection.FirstOrDefault(x => x.Method.Equals(method, StringComparison.OrdinalIgnoreCase)); if (descriptor is null) return null; - if (_textDocumentSyncHandler == null) + if (_textDocumentSyncHandlers == null) { - _textDocumentSyncHandler = _collection + _textDocumentSyncHandlers = _collection .Select(x => x.Handler is ITextDocumentSyncHandler r ? r : null) - .FirstOrDefault(x => x != null); + .Where(x => x != null) + .ToArray(); } - if (_textDocumentSyncHandler is null) return descriptor; - if (typeof(ITextDocumentIdentifierParams).GetTypeInfo().IsAssignableFrom(descriptor.Params)) { var textDocumentIdentifierParams = @params.ToObject(descriptor.Params) as ITextDocumentIdentifierParams; - var attributes = _textDocumentSyncHandler.GetTextDocumentAttributes(textDocumentIdentifierParams.TextDocument.Uri); + var attributes = _textDocumentSyncHandlers + .Select(x => x.GetTextDocumentAttributes(textDocumentIdentifierParams.TextDocument.Uri)) + .Where(x => x != null); return GetHandler(method, attributes); } @@ -75,6 +77,13 @@ private ILspHandlerDescriptor FindDescriptor(string method, JToken @params) return descriptor; } + private ILspHandlerDescriptor GetHandler(string method, IEnumerable attributes) + { + return attributes + .Select(x => GetHandler(method, x)) + .FirstOrDefault(x => x != null); + } + private ILspHandlerDescriptor GetHandler(string method, TextDocumentAttributes attributes) { foreach (var handler in _collection.Where(x => x.Method == method)) diff --git a/src/Lsp/Models/CodeActionParams.cs b/src/Lsp/Models/CodeActionParams.cs index 7ae872d2a..dae6dba90 100644 --- a/src/Lsp/Models/CodeActionParams.cs +++ b/src/Lsp/Models/CodeActionParams.cs @@ -1,13 +1,8 @@ -using Newtonsoft.Json; +using Newtonsoft.Json; using Newtonsoft.Json.Serialization; namespace OmniSharp.Extensions.LanguageServer.Models { - public interface ITextDocumentIdentifierParams - { - TextDocumentIdentifier TextDocument { get; } - } - /// /// Params for the CodeActionRequest /// diff --git a/src/Lsp/Models/DocumentFilter.cs b/src/Lsp/Models/DocumentFilter.cs index e2fe0b89a..17771d2b3 100644 --- a/src/Lsp/Models/DocumentFilter.cs +++ b/src/Lsp/Models/DocumentFilter.cs @@ -1,4 +1,5 @@ -using System.Collections.Generic; +using System.Collections.Generic; +using System.Linq; using System.Text; using Minimatch; using Newtonsoft.Json; @@ -108,5 +109,20 @@ public bool IsMatch(TextDocumentAttributes attributes) return false; } + + public static DocumentFilter ForPattern(string wildcard) + { + return new DocumentFilter() { Pattern = wildcard }; + } + + public static DocumentFilter ForLanguage(string language) + { + return new DocumentFilter() { Language = language }; + } + + public static DocumentFilter ForScheme(string scheme) + { + return new DocumentFilter() { Scheme = scheme }; + } } } diff --git a/src/Lsp/Models/DocumentSelector.cs b/src/Lsp/Models/DocumentSelector.cs index 57191b19e..eeef9a0cb 100644 --- a/src/Lsp/Models/DocumentSelector.cs +++ b/src/Lsp/Models/DocumentSelector.cs @@ -1,4 +1,4 @@ -using System.Collections.Generic; +using System.Collections.Generic; using System.Collections.ObjectModel; using System.Linq; using OmniSharp.Extensions.LanguageServer.Protocol.Document; @@ -47,5 +47,20 @@ public bool IsMatch(TextDocumentAttributes attributes) { return this.Any(z => z.IsMatch(attributes)); } + + public static DocumentSelector ForPattern(params string[] wildcards) + { + return new DocumentSelector(wildcards.Select(DocumentFilter.ForPattern)); + } + + public static DocumentSelector ForLanguage(params string[] languages) + { + return new DocumentSelector(languages.Select(DocumentFilter.ForLanguage)); + } + + public static DocumentSelector ForScheme(params string[] schemes) + { + return new DocumentSelector(schemes.Select(DocumentFilter.ForScheme)); + } } } diff --git a/src/Lsp/Models/ITextDocumentIdentifierParams.cs b/src/Lsp/Models/ITextDocumentIdentifierParams.cs new file mode 100644 index 000000000..e3d902968 --- /dev/null +++ b/src/Lsp/Models/ITextDocumentIdentifierParams.cs @@ -0,0 +1,7 @@ +namespace OmniSharp.Extensions.LanguageServer.Models +{ + public interface ITextDocumentIdentifierParams + { + TextDocumentIdentifier TextDocument { get; } + } +} \ No newline at end of file diff --git a/src/Lsp/Protocol/Document/ITextDocumentSyncHandler.cs b/src/Lsp/Protocol/Document/ITextDocumentSyncHandler.cs index 53325fd96..b70e54a51 100644 --- a/src/Lsp/Protocol/Document/ITextDocumentSyncHandler.cs +++ b/src/Lsp/Protocol/Document/ITextDocumentSyncHandler.cs @@ -1,33 +1,18 @@ -using System; +using System; using OmniSharp.Extensions.LanguageServer.Protocol; using OmniSharp.Extensions.LanguageServer.Capabilities.Server; +using OmniSharp.Extensions.LanguageServer.Models; namespace OmniSharp.Extensions.LanguageServer.Protocol.Document { public interface ITextDocumentSyncHandler : IDidChangeTextDocumentHandler, IDidOpenTextDocumentHandler, IDidCloseTextDocumentHandler, IDidSaveTextDocumentHandler { TextDocumentSyncOptions Options { get; } + /// + /// Returns the attributes for the document at the given URI. This can return null. + /// + /// + /// TextDocumentAttributes GetTextDocumentAttributes(Uri uri); } - - public class TextDocumentAttributes - { - public TextDocumentAttributes(Uri uri, string languageId) - { - Uri = uri; - Scheme = uri.Scheme; - LanguageId = languageId; - } - - public TextDocumentAttributes(Uri uri, string scheme, string languageId) - { - Uri = uri; - Scheme = scheme; - LanguageId = languageId; - } - - public Uri Uri { get; } - public string Scheme { get; } - public string LanguageId { get; } - } } diff --git a/src/Lsp/Protocol/Document/TextDocumentAttributes.cs b/src/Lsp/Protocol/Document/TextDocumentAttributes.cs new file mode 100644 index 000000000..ffc7e0d3c --- /dev/null +++ b/src/Lsp/Protocol/Document/TextDocumentAttributes.cs @@ -0,0 +1,25 @@ +using System; + +namespace OmniSharp.Extensions.LanguageServer.Protocol.Document +{ + public class TextDocumentAttributes + { + public TextDocumentAttributes(Uri uri, string languageId) + { + Uri = uri; + Scheme = uri.Scheme; + LanguageId = languageId; + } + + public TextDocumentAttributes(Uri uri, string scheme, string languageId) + { + Uri = uri; + Scheme = scheme; + LanguageId = languageId; + } + + public Uri Uri { get; } + public string Scheme { get; } + public string LanguageId { get; } + } +} \ No newline at end of file diff --git a/test/Lsp.Tests/Capabilities/Client/TextDocumentClientCapabilitiesTests.cs b/test/Lsp.Tests/Capabilities/Client/TextDocumentClientCapabilitiesTests.cs index b3ed08806..5c329dbb7 100644 --- a/test/Lsp.Tests/Capabilities/Client/TextDocumentClientCapabilitiesTests.cs +++ b/test/Lsp.Tests/Capabilities/Client/TextDocumentClientCapabilitiesTests.cs @@ -1,4 +1,4 @@ -using System; +using System; using FluentAssertions; using Newtonsoft.Json; using OmniSharp.Extensions.LanguageServer.Capabilities.Client; @@ -45,5 +45,17 @@ public void SimpleTest(string expected) var deresult = JsonConvert.DeserializeObject(expected); deresult.ShouldBeEquivalentTo(model); } + + [Theory, JsonFixture] + public void EmptyTest(string expected) + { + var model = new TextDocumentClientCapabilities(); + var result = Fixture.SerializeObject(model); + + result.Should().Be(expected); + + var deresult = JsonConvert.DeserializeObject(expected); + deresult.ShouldBeEquivalentTo(model); + } } } diff --git a/test/Lsp.Tests/Capabilities/Client/TextDocumentClientCapabilitiesTests_$EmptyTest.json b/test/Lsp.Tests/Capabilities/Client/TextDocumentClientCapabilitiesTests_$EmptyTest.json new file mode 100644 index 000000000..0967ef424 --- /dev/null +++ b/test/Lsp.Tests/Capabilities/Client/TextDocumentClientCapabilitiesTests_$EmptyTest.json @@ -0,0 +1 @@ +{} diff --git a/test/Lsp.Tests/Capabilities/Client/WorkspaceClientCapabilitesTests.cs b/test/Lsp.Tests/Capabilities/Client/WorkspaceClientCapabilitesTests.cs index e26a8a29e..3a883fcb6 100644 --- a/test/Lsp.Tests/Capabilities/Client/WorkspaceClientCapabilitesTests.cs +++ b/test/Lsp.Tests/Capabilities/Client/WorkspaceClientCapabilitesTests.cs @@ -1,4 +1,4 @@ -using System; +using System; using FluentAssertions; using Newtonsoft.Json; using OmniSharp.Extensions.LanguageServer.Capabilities.Client; @@ -13,7 +13,7 @@ public void SimpleTest(string expected) { var model = new WorkspaceClientCapabilites() { ApplyEdit = true, - WorkspaceEdit = new WorkspaceEditCapability() { DocumentChanges = true}, + WorkspaceEdit = new WorkspaceEditCapability() { DocumentChanges = true }, DidChangeConfiguration = new DidChangeConfigurationCapability() { DynamicRegistration = true }, DidChangeWatchedFiles = new DidChangeWatchedFilesCapability() { DynamicRegistration = true }, ExecuteCommand = new ExecuteCommandCapability() { DynamicRegistration = true }, @@ -27,5 +27,18 @@ public void SimpleTest(string expected) var deresult = JsonConvert.DeserializeObject(expected); deresult.ShouldBeEquivalentTo(model); } + + [Theory, JsonFixture] + public void EmptyTest(string expected) + { + var model = new WorkspaceClientCapabilites(); + + var result = Fixture.SerializeObject(model); + + result.Should().Be(expected); + + var deresult = JsonConvert.DeserializeObject(expected); + deresult.ShouldBeEquivalentTo(model); + } } } diff --git a/test/Lsp.Tests/Capabilities/Client/WorkspaceClientCapabilitesTests_$EmptyTest.json b/test/Lsp.Tests/Capabilities/Client/WorkspaceClientCapabilitesTests_$EmptyTest.json new file mode 100644 index 000000000..0967ef424 --- /dev/null +++ b/test/Lsp.Tests/Capabilities/Client/WorkspaceClientCapabilitesTests_$EmptyTest.json @@ -0,0 +1 @@ +{} diff --git a/test/Lsp.Tests/HandlerResolverTests.cs b/test/Lsp.Tests/HandlerResolverTests.cs index 5a59c4c66..0ce63e351 100644 --- a/test/Lsp.Tests/HandlerResolverTests.cs +++ b/test/Lsp.Tests/HandlerResolverTests.cs @@ -21,10 +21,6 @@ public class HandlerResolverTests [Theory] [InlineData(typeof(IInitializeHandler), "initialize", 1)] [InlineData(typeof(IInitializedHandler), "initialized", 1)] - [InlineData(typeof(ITextDocumentSyncHandler), "textDocument/didOpen", 4)] - [InlineData(typeof(ITextDocumentSyncHandler), "textDocument/didChange", 4)] - [InlineData(typeof(ITextDocumentSyncHandler), "textDocument/didClose", 4)] - [InlineData(typeof(ITextDocumentSyncHandler), "textDocument/didSave", 4)] public void Should_Contain_AllDefinedMethods(Type requestHandler, string key, int count) { var handler = new HandlerCollection(); @@ -38,6 +34,21 @@ public void Should_Contain_AllDefinedMethods(Type requestHandler, string key, in handler._handlers.Count.Should().Be(count); } + [Theory] + [InlineData(typeof(ITextDocumentSyncHandler), "textDocument/didOpen", 4)] + [InlineData(typeof(ITextDocumentSyncHandler), "textDocument/didChange", 4)] + [InlineData(typeof(ITextDocumentSyncHandler), "textDocument/didClose", 4)] + [InlineData(typeof(ITextDocumentSyncHandler), "textDocument/didSave", 4)] + public void Should_Contain_AllDefinedTextDocumentSyncMethods(Type requestHandler, string key, int count) + { + var handler = new HandlerCollection(); + var sub = (IJsonRpcHandler)TextDocumentSyncHandlerExtensions.With(DocumentSelector.ForPattern("**/*.something")); + + handler.Add(sub); + handler._handlers.Should().Contain(x => x.Method == key); + handler._handlers.Count.Should().Be(count); + } + [Theory] [InlineData("textDocument/didOpen", 8)] [InlineData("textDocument/didChange", 8)] @@ -46,21 +57,9 @@ public void Should_Contain_AllDefinedMethods(Type requestHandler, string key, in public void Should_Contain_AllDefinedMethods_ForDifferentKeys(string key, int count) { var handler = new HandlerCollection(); - var sub = Substitute.For(); - if (sub is IRegistration reg) - reg.GetRegistrationOptions() - .Returns(new TextDocumentRegistrationOptions() { - DocumentSelector = new DocumentSelector(new DocumentFilter() { - Pattern = "**/*.cs" - }) - }); + var sub = TextDocumentSyncHandlerExtensions.With(DocumentSelector.ForPattern("**/*.cs")); - var sub2 = Substitute.For(); - if (sub2 is IRegistration reg2) - reg2.GetRegistrationOptions() - .Returns(new TextDocumentRegistrationOptions() { - DocumentSelector = new DocumentSelector(new DocumentFilter() { Pattern = "**/*.cake" }) - }); + var sub2 = TextDocumentSyncHandlerExtensions.With(DocumentSelector.ForPattern("**/*.cake")); handler.Add(sub); handler.Add(sub2); diff --git a/test/Lsp.Tests/LspRequestRouterTests.cs b/test/Lsp.Tests/LspRequestRouterTests.cs new file mode 100644 index 000000000..ecf4245f5 --- /dev/null +++ b/test/Lsp.Tests/LspRequestRouterTests.cs @@ -0,0 +1,130 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using FluentAssertions; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using NSubstitute; +using OmniSharp.Extensions.JsonRpc.Server; +using OmniSharp.Extensions.LanguageServer; +using OmniSharp.Extensions.LanguageServer.Messages; +using OmniSharp.Extensions.LanguageServer.Models; +using OmniSharp.Extensions.LanguageServer.Protocol; +using OmniSharp.Extensions.LanguageServer.Protocol.Document; +using Xunit; +using Xunit.Sdk; + +namespace Lsp.Tests +{ + public class LspRequestRouterTests + { + [Fact] + public void ShouldRouteToCorrect_Notification() + { + var textDocumentSyncHandler = TextDocumentSyncHandlerExtensions.With(DocumentSelector.ForPattern("**/*.cs")); + textDocumentSyncHandler.Handle(Arg.Any()).Returns(Task.CompletedTask); + + var collection = new HandlerCollection { textDocumentSyncHandler }; + var mediator = new LspRequestRouter(collection); + + var @params = new DidSaveTextDocumentParams() { + TextDocument = new TextDocumentIdentifier(new Uri("file:///c:/test/123.cs")) + }; + + var request = new Notification("textDocument/didSave", JObject.Parse(JsonConvert.SerializeObject(@params))); + + mediator.RouteNotification(request); + + textDocumentSyncHandler.Received(1).Handle(Arg.Any()); + } + + [Fact] + public void ShouldRouteToCorrect_Notification_WithManyHandlers() + { + var textDocumentSyncHandler = TextDocumentSyncHandlerExtensions.With(DocumentSelector.ForPattern("**/*.cs")); + var textDocumentSyncHandler2 = TextDocumentSyncHandlerExtensions.With(DocumentSelector.ForPattern("**/*.cake")); + textDocumentSyncHandler.Handle(Arg.Any()).Returns(Task.CompletedTask); + textDocumentSyncHandler2.Handle(Arg.Any()).Returns(Task.CompletedTask); + + var collection = new HandlerCollection { textDocumentSyncHandler, textDocumentSyncHandler2 }; + var mediator = new LspRequestRouter(collection); + + var @params = new DidSaveTextDocumentParams() { + TextDocument = new TextDocumentIdentifier(new Uri("file:///c:/test/123.cake")) + }; + + var request = new Notification("textDocument/didSave", JObject.Parse(JsonConvert.SerializeObject(@params))); + + mediator.RouteNotification(request); + + textDocumentSyncHandler.Received(0).Handle(Arg.Any()); + textDocumentSyncHandler2.Received(1).Handle(Arg.Any()); + } + + [Fact] + public async Task ShouldRouteToCorrect_Request() + { + var textDocumentSyncHandler = TextDocumentSyncHandlerExtensions.With(DocumentSelector.ForPattern("**/*.cs")); + textDocumentSyncHandler.Handle(Arg.Any()).Returns(Task.CompletedTask); + + var codeActionHandler = Substitute.For(); + codeActionHandler.GetRegistrationOptions().Returns(new TextDocumentRegistrationOptions() { DocumentSelector = DocumentSelector.ForPattern("**/*.cs") }); + codeActionHandler + .Handle(Arg.Any(), Arg.Any()) + .Returns(new CommandContainer()); + + var collection = new HandlerCollection { textDocumentSyncHandler, codeActionHandler }; + var mediator = new LspRequestRouter(collection); + + var id = Guid.NewGuid().ToString(); + var @params = new DidSaveTextDocumentParams() { + TextDocument = new TextDocumentIdentifier(new Uri("file:///c:/test/123.cs")) + }; + + var request = new Request(id, "textDocument/codeAction", JObject.Parse(JsonConvert.SerializeObject(@params))); + + await mediator.RouteRequest(request); + + await codeActionHandler.Received(1).Handle(Arg.Any(), Arg.Any()); + } + + [Fact] + public async Task ShouldRouteToCorrect_Request_WithManyHandlers() + { + var textDocumentSyncHandler = TextDocumentSyncHandlerExtensions.With(DocumentSelector.ForPattern("**/*.cs")); + var textDocumentSyncHandler2 = TextDocumentSyncHandlerExtensions.With(DocumentSelector.ForPattern("**/*.cake")); + textDocumentSyncHandler.Handle(Arg.Any()).Returns(Task.CompletedTask); + textDocumentSyncHandler2.Handle(Arg.Any()).Returns(Task.CompletedTask); + + var codeActionHandler = Substitute.For(); + codeActionHandler.GetRegistrationOptions().Returns(new TextDocumentRegistrationOptions() { DocumentSelector = DocumentSelector.ForPattern("**/*.cs") }); + codeActionHandler + .Handle(Arg.Any(), Arg.Any()) + .Returns(new CommandContainer()); + + var codeActionHandler2 = Substitute.For(); + codeActionHandler2.GetRegistrationOptions().Returns(new TextDocumentRegistrationOptions() { DocumentSelector = DocumentSelector.ForPattern("**/*.cake") }); + codeActionHandler2 + .Handle(Arg.Any(), Arg.Any()) + .Returns(new CommandContainer()); + + var collection = new HandlerCollection { textDocumentSyncHandler, textDocumentSyncHandler2, codeActionHandler , codeActionHandler2 }; + var mediator = new LspRequestRouter(collection); + + var id = Guid.NewGuid().ToString(); + var @params = new DidSaveTextDocumentParams() { + TextDocument = new TextDocumentIdentifier(new Uri("file:///c:/test/123.cake")) + }; + + var request = new Request(id, "textDocument/codeAction", JObject.Parse(JsonConvert.SerializeObject(@params))); + + await mediator.RouteRequest(request); + + await codeActionHandler.Received(0).Handle(Arg.Any(), Arg.Any()); + await codeActionHandler2.Received(1).Handle(Arg.Any(), Arg.Any()); + } + } +} diff --git a/test/Lsp.Tests/MediatorTestsRequestHandlerOfTRequestTResponse.cs b/test/Lsp.Tests/MediatorTestsRequestHandlerOfTRequestTResponse.cs index 79cf07694..b092eb131 100644 --- a/test/Lsp.Tests/MediatorTestsRequestHandlerOfTRequestTResponse.cs +++ b/test/Lsp.Tests/MediatorTestsRequestHandlerOfTRequestTResponse.cs @@ -12,21 +12,61 @@ using NSubstitute; using OmniSharp.Extensions.JsonRpc.Server; using OmniSharp.Extensions.LanguageServer; +using OmniSharp.Extensions.LanguageServer.Abstractions; using OmniSharp.Extensions.LanguageServer.Messages; using OmniSharp.Extensions.LanguageServer.Models; +using OmniSharp.Extensions.LanguageServer.Protocol.Document; using Xunit; using Xunit.Sdk; using HandlerCollection = OmniSharp.Extensions.LanguageServer.HandlerCollection; namespace Lsp.Tests { + static class TextDocumentSyncHandlerExtensions + { + public static ITextDocumentSyncHandler With(DocumentSelector documentSelector) + { + return Substitute.For().With(documentSelector); + } + + public static ITextDocumentSyncHandler With(this ITextDocumentSyncHandler handler, DocumentSelector documentSelector) + { + ((IDidChangeTextDocumentHandler)handler).GetRegistrationOptions().Returns(new TextDocumentChangeRegistrationOptions() { DocumentSelector = documentSelector }); + ((IDidOpenTextDocumentHandler)handler).GetRegistrationOptions().Returns(new TextDocumentRegistrationOptions() { DocumentSelector = documentSelector }); + ((IDidCloseTextDocumentHandler)handler).GetRegistrationOptions().Returns(new TextDocumentRegistrationOptions() { DocumentSelector = documentSelector }); + ((IDidSaveTextDocumentHandler)handler).GetRegistrationOptions().Returns(new TextDocumentSaveRegistrationOptions() { DocumentSelector = documentSelector }); + + handler + .GetTextDocumentAttributes(Arg.Is(x => documentSelector.IsMatch(new TextDocumentAttributes(x, "")))) + .Returns(c => new TextDocumentAttributes(c.Arg(), "")); + + return handler; + } + + private static void For(this ITextDocumentSyncHandler handler, DocumentSelector documentSelector) + where T : class, IRegistration + { + var me = handler as T; + me.GetRegistrationOptions().Returns(GetOptions(me, documentSelector)); + } + + private static TextDocumentRegistrationOptions GetOptions(IRegistration handler, DocumentSelector documentSelector) + where R : TextDocumentRegistrationOptions, new() + { + return new R { DocumentSelector = documentSelector }; + } + } + public class MediatorTestsRequestHandlerOfTRequestTResponse { [Fact] public async Task RequestsCancellation() { + var textDocumentSyncHandler = TextDocumentSyncHandlerExtensions.With(DocumentSelector.ForPattern("**/*.cs")); + textDocumentSyncHandler.Handle(Arg.Any()).Returns(Task.CompletedTask); + var codeActionHandler = Substitute.For(); - codeActionHandler.GetRegistrationOptions().Returns(new TextDocumentRegistrationOptions()); + codeActionHandler.GetRegistrationOptions().Returns(new TextDocumentRegistrationOptions() { DocumentSelector = DocumentSelector.ForPattern("**/*.cs") }); codeActionHandler .Handle(Arg.Any(), Arg.Any()) .Returns(async (c) => { @@ -35,7 +75,7 @@ public async Task RequestsCancellation() return new CommandContainer(); }); - var collection = new HandlerCollection { codeActionHandler }; + var collection = new HandlerCollection { textDocumentSyncHandler, codeActionHandler }; var mediator = new LspRequestRouter(collection); var id = Guid.NewGuid().ToString(); diff --git a/test/Lsp.Tests/Models/InitializeParamsTests.cs b/test/Lsp.Tests/Models/InitializeParamsTests.cs index 25cd6d953..4f973c37c 100644 --- a/test/Lsp.Tests/Models/InitializeParamsTests.cs +++ b/test/Lsp.Tests/Models/InitializeParamsTests.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using FluentAssertions; using Newtonsoft.Json; @@ -50,6 +50,7 @@ public void SimpleTest(string expected) DidChangeWatchedFiles = new DidChangeWatchedFilesCapability() { DynamicRegistration = true }, ExecuteCommand = new ExecuteCommandCapability() { DynamicRegistration = true }, Symbol = new WorkspaceSymbolCapability() { DynamicRegistration = true }, + } }, InitializationOptions = null, diff --git a/test/Lsp.Tests/Models/InitializeParamsTests_$SimpleTest.json b/test/Lsp.Tests/Models/InitializeParamsTests_$SimpleTest.json index 08e1bd52d..17e68df98 100644 --- a/test/Lsp.Tests/Models/InitializeParamsTests_$SimpleTest.json +++ b/test/Lsp.Tests/Models/InitializeParamsTests_$SimpleTest.json @@ -1,4 +1,4 @@ -{ +{ "processId": 1234, "rootPath": "/file/abc/12.cs", "rootUri": "file:///file/abc/12.cs", @@ -6,7 +6,6 @@ "capabilities": { "workspace": { "applyEdit": true, - "workspaceEdit": false, "didChangeConfiguration": { "dynamicRegistration": true }, @@ -78,4 +77,4 @@ } }, "trace": "verbose" -} \ No newline at end of file +} diff --git a/vscode-testextension/src/extension.ts b/vscode-testextension/src/extension.ts index 4e0fa44b3..f6c53ad7c 100644 --- a/vscode-testextension/src/extension.ts +++ b/vscode-testextension/src/extension.ts @@ -7,13 +7,14 @@ import * as path from 'path'; import { workspace, Disposable, ExtensionContext } from 'vscode'; -import { LanguageClient, LanguageClientOptions, SettingMonitor, ServerOptions, TransportKind } from 'vscode-languageclient'; +import { LanguageClient, LanguageClientOptions, SettingMonitor, ServerOptions, TransportKind, InitializeParams } from 'vscode-languageclient'; +import { Trace } from 'vscode-jsonrpc'; export function activate(context: ExtensionContext) { // The server is implemented in node - // let serverExe = 'D:/Development/Omnisharp/omnisharp-roslyn/bin/Debug/OmniSharp.Stdio/net46/OmniSharp.exe'; - let serverExe = 'D:/Development/Omnisharp/omnisharp-roslyn/artifacts/publish/OmniSharp.Stdio/win7-x64/OmniSharp.exe'; + let serverExe = 'C:/other/omnisharp-roslyn/bin/Debug/OmniSharp.Stdio/net46/OmniSharp.exe'; + // let serverExe = 'C:/other/omnisharp-roslyn/artifacts/publish/OmniSharp.Stdio/win7-x64/OmniSharp.exe'; // let serverExe = context.asAbsolutePath('D:/Development/Omnisharp/omnisharp-roslyn/artifacts/publish/OmniSharp.Stdio/win7-x64/OmniSharp.exe'); // The debug options for the server // let debugOptions = { execArgv: ['-lsp', '-d' };5 @@ -40,11 +41,12 @@ export function activate(context: ExtensionContext) { configurationSection: 'languageServerExample', // Notify the server about file changes to '.clientrc files contain in the workspace fileEvents: workspace.createFileSystemWatcher('**/.clientrc') - } + }, } // Create the language client and start the client. const client = new LanguageClient('languageServerExample', 'Language Server Example', serverOptions, clientOptions); + client.trace = Trace.Verbose; let disposable = client.start(); // Push the disposable to the context's subscriptions so that the