From b3b4f2ebbba758949894c63385dc50d82e61cc1e Mon Sep 17 00:00:00 2001 From: Arjen Witteveen Date: Fri, 2 Oct 2020 18:49:56 +0200 Subject: [PATCH] Add support for textDocument/implementation in LSP mode --- .../OmniSharpImplementationHandler.cs | 54 +++++ .../LanguageServerHost.cs | 1 + .../Navigation/FindImplementationsService.cs | 5 + .../OmniSharpImplementationHandlerFacts.cs | 227 ++++++++++++++++++ 4 files changed, 287 insertions(+) create mode 100644 src/OmniSharp.LanguageServerProtocol/Handlers/OmniSharpImplementationHandler.cs create mode 100644 tests/OmniSharp.Lsp.Tests/OmniSharpImplementationHandlerFacts.cs diff --git a/src/OmniSharp.LanguageServerProtocol/Handlers/OmniSharpImplementationHandler.cs b/src/OmniSharp.LanguageServerProtocol/Handlers/OmniSharpImplementationHandler.cs new file mode 100644 index 0000000000..592c92f3e3 --- /dev/null +++ b/src/OmniSharp.LanguageServerProtocol/Handlers/OmniSharpImplementationHandler.cs @@ -0,0 +1,54 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using OmniSharp.Extensions.JsonRpc; +using OmniSharp.Extensions.LanguageServer.Protocol.Document; +using OmniSharp.Extensions.LanguageServer.Protocol.Models; +using OmniSharp.Models; +using OmniSharp.Models.FindImplementations; +using static OmniSharp.LanguageServerProtocol.Helpers; + +namespace OmniSharp.LanguageServerProtocol.Handlers +{ + internal sealed class OmniSharpImplementationHandler : ImplementationHandler + { + public static IEnumerable Enumerate(RequestHandlers handlers) + { + foreach (var (selector, handler) in handlers + .OfType>()) + if (handler != null) + yield return new OmniSharpImplementationHandler(handler, selector); + } + + private readonly Mef.IRequestHandler _findImplementationsHandler; + + public OmniSharpImplementationHandler(Mef.IRequestHandler findImplementationsHandler, DocumentSelector documentSelector) + : base(new ImplementationRegistrationOptions() + { + DocumentSelector = documentSelector + }) + { + _findImplementationsHandler = findImplementationsHandler; + } + + public async override Task Handle(ImplementationParams request, CancellationToken token) + { + var omnisharpRequest = new FindImplementationsRequest() + { + FileName = FromUri(request.TextDocument.Uri), + Column = Convert.ToInt32(request.Position.Character), + Line = Convert.ToInt32(request.Position.Line) + }; + + var omnisharpResponse = await _findImplementationsHandler.Handle(omnisharpRequest); + + return omnisharpResponse?.QuickFixes?.Select(x => new LocationOrLocationLink(new Location + { + Uri = ToUri(x.FileName), + Range = ToRange((x.Column, x.Line)) + })).ToArray() ?? Array.Empty(); + } + } +} diff --git a/src/OmniSharp.LanguageServerProtocol/LanguageServerHost.cs b/src/OmniSharp.LanguageServerProtocol/LanguageServerHost.cs index 985b8be342..3dd22b5c5b 100644 --- a/src/OmniSharp.LanguageServerProtocol/LanguageServerHost.cs +++ b/src/OmniSharp.LanguageServerProtocol/LanguageServerHost.cs @@ -342,6 +342,7 @@ internal static void RegisterHandlers(ILanguageServer server, CompositionHost co .Concat(OmniSharpWorkspaceSymbolsHandler.Enumerate(handlers)) .Concat(OmniSharpDocumentSymbolHandler.Enumerate(handlers)) .Concat(OmniSharpReferencesHandler.Enumerate(handlers)) + .Concat(OmniSharpImplementationHandler.Enumerate(handlers)) .Concat(OmniSharpCodeLensHandler.Enumerate(handlers)) .Concat(OmniSharpCodeActionHandler.Enumerate(handlers, serializer, server, documentVersions)) .Concat(OmniSharpDocumentFormattingHandler.Enumerate(handlers)) diff --git a/src/OmniSharp.Roslyn.CSharp/Services/Navigation/FindImplementationsService.cs b/src/OmniSharp.Roslyn.CSharp/Services/Navigation/FindImplementationsService.cs index d2f40a73cd..f55c1f21ff 100644 --- a/src/OmniSharp.Roslyn.CSharp/Services/Navigation/FindImplementationsService.cs +++ b/src/OmniSharp.Roslyn.CSharp/Services/Navigation/FindImplementationsService.cs @@ -39,6 +39,11 @@ public async Task Handle(FindImplementationsRequest request) var quickFixes = new List(); var symbol = await SymbolFinder.FindSymbolAtPositionAsync(semanticModel, position, _workspace); + if (symbol == null) + { + return response; + } + if (symbol.IsInterfaceType() || symbol.IsImplementableMember()) { // SymbolFinder.FindImplementationsAsync will not include the method overrides diff --git a/tests/OmniSharp.Lsp.Tests/OmniSharpImplementationHandlerFacts.cs b/tests/OmniSharp.Lsp.Tests/OmniSharpImplementationHandlerFacts.cs new file mode 100644 index 0000000000..fcfdde01d3 --- /dev/null +++ b/tests/OmniSharp.Lsp.Tests/OmniSharpImplementationHandlerFacts.cs @@ -0,0 +1,227 @@ +using System.IO; +using System.Linq; +using System.Threading.Tasks; +using OmniSharp.Extensions.LanguageServer.Protocol; +using OmniSharp.Extensions.LanguageServer.Protocol.Document; +using OmniSharp.Extensions.LanguageServer.Protocol.Models; +using TestUtility; +using Xunit; +using Xunit.Abstractions; + +namespace OmniSharp.Lsp.Tests +{ + public class OmniSharpImplementationHandlerFacts : AbstractLanguageServerTestBase + { + public OmniSharpImplementationHandlerFacts(ITestOutputHelper output) + : base(output) + { + } + + [Fact] + public async Task CanFindImplementationsOfClass() + { + const string code = @" + public class Foo$$Base + { + } + + public class FooDerivedA : FooBase + { + } + + public class FooDerivedB : FooBase + { + }"; + + var implementations = await FindImplementationsAsync(code); + Assert.Equal(3, implementations.Count()); + } + + [Fact] + public async Task CanFindImplementationsOfInterface() + { + const string code = @" + public interface IF$$oo + { + } + + public class FooA : IFoo + { + } + + public class FooB : IFoo + { + }"; + + var implementations = await FindImplementationsAsync(code); + Assert.Equal(2, implementations.Count()); + } + + [Fact] + public async Task CanFindImplementationsOfVirtualFunction() + { + const string code = @" + public class FooBase + { + public virtual int B$$ar() { return 1; } + } + + public class FooDerivedA : FooBase + { + public override int Bar() { return 2; } + } + + public class FooDerivedB : FooBase + { + public override int Bar() { return 3; } + }"; + + var implementations = await FindImplementationsAsync(code); + Assert.Equal(3, implementations.Count()); + } + + [Fact] + public async Task CanFindImplementationsOfAbstractFunction() + { + const string code = @" + public abstract class FooBase + { + public abstract int B$$ar(); + } + + public class FooDerivedA : FooBase + { + public override int Bar() { return 2; } + } + + public class FooDerivedB : FooBase + { + public override int Bar() { return 3; } + }"; + + var implementations = await FindImplementationsAsync(code); + Assert.Equal(2, implementations.Count()); + } + + [Fact] + public async Task CanFindImplementationsOfVirtualProperty() + { + const string code = @" + public class FooBase + { + public virtual int B$$ar => 1; + } + + public class FooDerivedA : FooBase + { + public override int Bar => 2; + } + + public class FooDerivedB : FooBase + { + public override int Bar => 3; + }"; + + var implementations = await FindImplementationsAsync(code); + Assert.Equal(3, implementations.Count()); + } + + [Fact] + public async Task CanFindImplementationsOfAbstractProperty() + { + const string code = @" + public abstract class FooBase + { + public abstract int B$$ar { get; } + } + + public class FooDerivedA : FooBase + { + public override int Bar => 2; + } + + public class FooDerivedB : FooBase + { + public override int Bar => 3; + }"; + + var implementations = await FindImplementationsAsync(code); + Assert.Equal(2, implementations.Count()); + } + + [Fact] + public async Task CannotFindImplementationsWithoutSymbol() + { + const string code = @" + public class Foo + { + $$ + }"; + + var implementations = await FindImplementationsAsync(code); + Assert.Empty(implementations); + } + + [Fact] + public async Task CannotFindImplementationsForUnsupportedSymbol() + { + const string code = @" + pub$$lic class Foo + { + }"; + + var implementations = await FindImplementationsAsync(code); + Assert.Empty(implementations); + } + + [Fact] + public async Task CannotFindImplementationsForEmptyFiles() + { + var response = await Client.TextDocument.RequestImplementation(new ImplementationParams + { + Position = (0, 0), + TextDocument = "notfound.cs" + }, CancellationToken); + + Assert.Empty(response); + } + + private Task FindImplementationsAsync(string code) + { + return FindImplementationsAsync(new[] { new TestFile("dummy.cs", code) }); + } + + private async Task FindImplementationsAsync(TestFile[] testFiles) + { + OmniSharpTestHost.AddFilesToWorkspace(testFiles + .Select(f => + new TestFile( + ((f.FileName.StartsWith("/") || f.FileName.StartsWith("\\")) ? f.FileName : ("/" + f.FileName)) + .Replace(Path.AltDirectorySeparatorChar, Path.DirectorySeparatorChar), f.Content)) + .ToArray() + ); + + var file = testFiles.Single(tf => tf.Content.HasPosition); + var point = file.Content.GetPointFromPosition(); + + Client.TextDocument.DidChangeTextDocument(new DidChangeTextDocumentParams + { + ContentChanges = new Container(new TextDocumentContentChangeEvent + { + Text = file.Content.Code + }), + TextDocument = new VersionedTextDocumentIdentifier + { + Uri = DocumentUri.From(file.FileName), + Version = 1 + } + }); + + return await Client.TextDocument.RequestImplementation(new ImplementationParams + { + Position = new Position(point.Line, point.Offset), + TextDocument = new TextDocumentIdentifier(DocumentUri.From(file.FileName)) + }, CancellationToken); + } + } +}