From 918b1f77b4c1a96757a588a89f5a8c7b64c699ee Mon Sep 17 00:00:00 2001 From: Nils Andresen Date: Sun, 22 Aug 2021 21:51:17 +0200 Subject: [PATCH] (#2209) implemented /v2/gotodefinition for Cake --- .../Navigation/GotoDefinitionHandler.cs | 99 ++------ .../Navigation/GotoDefinitionHandlerHelper.cs | 113 +++++++++ .../Navigation/GotoDefinitionV2Handler.cs | 115 ++++++++++ .../test-projects/CakeProject/foo.cake | 7 +- .../GotoDefinitionFacts.cs | 6 +- .../GotoDefinitionV2Facts.cs | 217 ++++++++++++++++++ 6 files changed, 471 insertions(+), 86 deletions(-) create mode 100644 src/OmniSharp.Cake/Services/RequestHandlers/Navigation/GotoDefinitionHandlerHelper.cs create mode 100644 src/OmniSharp.Cake/Services/RequestHandlers/Navigation/GotoDefinitionV2Handler.cs create mode 100644 tests/OmniSharp.Cake.Tests/GotoDefinitionV2Facts.cs diff --git a/src/OmniSharp.Cake/Services/RequestHandlers/Navigation/GotoDefinitionHandler.cs b/src/OmniSharp.Cake/Services/RequestHandlers/Navigation/GotoDefinitionHandler.cs index a9fed750d0..fa4557daec 100644 --- a/src/OmniSharp.Cake/Services/RequestHandlers/Navigation/GotoDefinitionHandler.cs +++ b/src/OmniSharp.Cake/Services/RequestHandlers/Navigation/GotoDefinitionHandler.cs @@ -1,11 +1,7 @@ using System; using System.Composition; using System.Linq; -using System.Threading; using System.Threading.Tasks; -using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.FindSymbols; -using Microsoft.CodeAnalysis.Text; using OmniSharp.Extensions; using OmniSharp.Mef; using OmniSharp.Models.GotoDefinition; @@ -18,8 +14,6 @@ namespace OmniSharp.Cake.Services.RequestHandlers.Navigation [OmniSharpHandler(OmniSharpEndpoints.GotoDefinition, Constants.LanguageNames.Cake), Shared] public class GotoDefinitionHandler : CakeRequestHandler { - private const int MethodLineOffset = 3; - private const int PropertyLineOffset = 7; private readonly MetadataExternalSourceService _metadataExternalSourceService; [ImportingConstructor] @@ -48,90 +42,31 @@ protected override async Task TranslateResponse(GotoDefi return new GotoDefinitionResponse(); } - return await GetAliasFromMetadataAsync(new GotoDefinitionRequest - { - Line = response.Line, - Column = response.Column, - FileName = request.FileName, - Timeout = request.Timeout, - WantMetadata = true - }); - } - - private async Task GetAliasFromMetadataAsync(GotoDefinitionRequest request) - { - var document = Workspace.GetDocument(request.FileName); - var response = new GotoDefinitionResponse(); - var lineIndex = request.Line + MethodLineOffset; - var column = 0; + var alias = (await GotoDefinitionHandlerHelper.GetAliasFromMetadataAsync( + Workspace, + request.FileName, + response.Line, + request.Timeout, + _metadataExternalSourceService + )).FirstOrDefault(); - if (document == null) + if (alias == null) { - return response; + return new GotoDefinitionResponse(); } - var semanticModel = await document.GetSemanticModelAsync(); - var sourceText = await document.GetTextAsync(); - var sourceLine = sourceText.Lines[lineIndex].ToString(); - if (sourceLine.Contains("(Context")) - { - column = sourceLine.IndexOf("(Context", StringComparison.Ordinal); - } - else + return new GotoDefinitionResponse { - lineIndex = request.Line + PropertyLineOffset; - sourceLine = sourceText.Lines[lineIndex].ToString(); - if (sourceLine.Contains("(Context")) + FileName = alias.MetadataDocument.FilePath ?? alias.MetadataDocument.Name, + Line = alias.LineSpan.StartLinePosition.Line, + Column = alias.LineSpan.StartLinePosition.Character, + MetadataSource = new MetadataSource { - column = sourceLine.IndexOf("(Context", StringComparison.Ordinal); + AssemblyName = alias.Symbol.ContainingAssembly.Name, + ProjectName = alias.Document.Project.Name, + TypeName = alias.Symbol.GetSymbolName() } - else - { - return response; - } - } - var position = sourceText.Lines.GetPosition(new LinePosition(lineIndex, column)); - var symbol = await SymbolFinder.FindSymbolAtPositionAsync(semanticModel, position, Workspace); - - if (symbol == null || symbol is INamespaceSymbol) - { - return response; - } - if (symbol is IMethodSymbol method) - { - symbol = method.PartialImplementationPart ?? symbol; - } - - var location = symbol.Locations.First(); - - if (!location.IsInMetadata) - { - return response; - } - var cancellationSource = new CancellationTokenSource(TimeSpan.FromMilliseconds(request.Timeout)); - var (metadataDocument, _) = await _metadataExternalSourceService.GetAndAddExternalSymbolDocument(document.Project, symbol, cancellationSource.Token); - if (metadataDocument == null) - { - return response; - } - - cancellationSource = new CancellationTokenSource(TimeSpan.FromMilliseconds(request.Timeout)); - var metadataLocation = await _metadataExternalSourceService.GetExternalSymbolLocation(symbol, metadataDocument, cancellationSource.Token); - var lineSpan = metadataLocation.GetMappedLineSpan(); - - response = new GotoDefinitionResponse - { - Line = lineSpan.StartLinePosition.Line, - Column = lineSpan.StartLinePosition.Character, - MetadataSource = new MetadataSource() - { - AssemblyName = symbol.ContainingAssembly.Name, - ProjectName = document.Project.Name, - TypeName = symbol.GetSymbolName() - }, }; - - return response; } } } diff --git a/src/OmniSharp.Cake/Services/RequestHandlers/Navigation/GotoDefinitionHandlerHelper.cs b/src/OmniSharp.Cake/Services/RequestHandlers/Navigation/GotoDefinitionHandlerHelper.cs new file mode 100644 index 0000000000..b896ce8778 --- /dev/null +++ b/src/OmniSharp.Cake/Services/RequestHandlers/Navigation/GotoDefinitionHandlerHelper.cs @@ -0,0 +1,113 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.FindSymbols; +using Microsoft.CodeAnalysis.Text; +using OmniSharp.Roslyn; + +namespace OmniSharp.Cake.Services.RequestHandlers.Navigation +{ + public static class GotoDefinitionHandlerHelper + { + private const int MethodLineOffset = 3; + private const int PropertyLineOffset = 7; + + internal static async Task> GetAliasFromMetadataAsync( + OmniSharpWorkspace workspace, + string fileName, + int line, + int timeout, + MetadataExternalSourceService metadataExternalSourceService) + { + var document = workspace.GetDocument(fileName); + var lineIndex = line + MethodLineOffset; + int column; + + if (document == null) + { + return Enumerable.Empty(); + } + + var semanticModel = await document.GetSemanticModelAsync(); + var sourceText = await document.GetTextAsync(); + var sourceLine = sourceText.Lines[lineIndex].ToString(); + if (sourceLine.Contains("(Context")) + { + column = sourceLine.IndexOf("(Context", StringComparison.Ordinal); + } + else + { + lineIndex = line + PropertyLineOffset; + sourceLine = sourceText.Lines[lineIndex].ToString(); + if (sourceLine.Contains("(Context")) + { + column = sourceLine.IndexOf("(Context", StringComparison.Ordinal); + } + else + { + return Enumerable.Empty(); + } + } + + if (column > 0 && sourceLine[column - 1] == '>') + { + column = sourceLine.LastIndexOf("<", column, StringComparison.Ordinal); + } + + var position = sourceText.Lines.GetPosition(new LinePosition(lineIndex, column)); + var symbol = await SymbolFinder.FindSymbolAtPositionAsync(semanticModel, position, workspace); + + if (symbol == null || symbol is INamespaceSymbol) + { + return Enumerable.Empty(); + } + if (symbol is IMethodSymbol method) + { + symbol = method.PartialImplementationPart ?? symbol; + } + + var result = new List(); + foreach (var location in symbol.Locations) + { + if (!location.IsInMetadata) + { + continue; + } + + var cancellationSource = new CancellationTokenSource(TimeSpan.FromMilliseconds(timeout)); + var (metadataDocument, _) = await metadataExternalSourceService.GetAndAddExternalSymbolDocument(document.Project, symbol, cancellationSource.Token); + if (metadataDocument == null) + { + continue; + } + + cancellationSource = new CancellationTokenSource(TimeSpan.FromMilliseconds(timeout)); + var metadataLocation = await metadataExternalSourceService.GetExternalSymbolLocation(symbol, metadataDocument, cancellationSource.Token); + var lineSpan = metadataLocation.GetMappedLineSpan(); + + result.Add(new Alias + { + Document = document, + MetadataDocument = metadataDocument, + Symbol = symbol, + Location = location, + LineSpan = lineSpan + }); + } + + return result; + } + + internal class Alias + { + public Document Document { get; set; } + public ISymbol Symbol { get; set; } + public Location Location { get; set; } + public FileLinePositionSpan LineSpan { get; set; } + public Document MetadataDocument { get; set; } + } + } +} diff --git a/src/OmniSharp.Cake/Services/RequestHandlers/Navigation/GotoDefinitionV2Handler.cs b/src/OmniSharp.Cake/Services/RequestHandlers/Navigation/GotoDefinitionV2Handler.cs new file mode 100644 index 0000000000..5a8e70aff3 --- /dev/null +++ b/src/OmniSharp.Cake/Services/RequestHandlers/Navigation/GotoDefinitionV2Handler.cs @@ -0,0 +1,115 @@ +using System; +using System.Collections.Generic; +using System.Composition; +using System.Linq; +using System.Threading.Tasks; +using OmniSharp.Extensions; +using OmniSharp.Mef; +using OmniSharp.Models.V2.GotoDefinition; +using OmniSharp.Models.Metadata; +using OmniSharp.Models.v1.SourceGeneratedFile; +using OmniSharp.Models.V2; +using OmniSharp.Roslyn; +using OmniSharp.Utilities; +using Location = OmniSharp.Models.V2.Location; + +namespace OmniSharp.Cake.Services.RequestHandlers.Navigation +{ + [OmniSharpHandler(OmniSharpEndpoints.V2.GotoDefinition, Constants.LanguageNames.Cake), Shared] + public class GotoDefinitionV2Handler : CakeRequestHandler + { + private readonly MetadataExternalSourceService _metadataExternalSourceService; + + [ImportingConstructor] + public GotoDefinitionV2Handler( + OmniSharpWorkspace workspace, + MetadataExternalSourceService metadataExternalSourceService) + : base(workspace) + { + _metadataExternalSourceService = metadataExternalSourceService ?? throw new ArgumentNullException(nameof(metadataExternalSourceService)); + } + + protected override async Task TranslateResponse(GotoDefinitionResponse response, GotoDefinitionRequest request) + { + var definitions = new List(); + foreach (var definition in response.Definitions ?? Enumerable.Empty()) + { + var file = definition.Location.FileName; + + if (string.IsNullOrEmpty(file) || !file.Equals(Constants.Paths.Generated)) + { + if (PlatformHelper.IsWindows && !string.IsNullOrEmpty(file)) + { + file = file.Replace('/', '\\'); + } + + definitions.Add(new Definition + { + MetadataSource = definition.MetadataSource, + SourceGeneratedFileInfo = definition.SourceGeneratedFileInfo, + Location = new Location + { + FileName = file, + Range = definition.Location.Range + } + }); + + continue; + } + + if (!request.WantMetadata) + { + continue; + } + + var aliasLocations = await GotoDefinitionHandlerHelper.GetAliasFromMetadataAsync( + Workspace, + request.FileName, + definition.Location.Range.End.Line, + request.Timeout, + _metadataExternalSourceService + ); + + definitions.AddRange( + aliasLocations.Select(loc => + new Definition + { + Location = new Location + { + FileName = loc.MetadataDocument.FilePath ?? loc.MetadataDocument.Name, + Range = new Range + { + Start = new Point + { + Column = loc.LineSpan.StartLinePosition.Character, + Line = loc.LineSpan.StartLinePosition.Line + }, + End = new Point + { + Column = loc.LineSpan.EndLinePosition.Character, + Line = loc.LineSpan.EndLinePosition.Line + }, + } + }, + MetadataSource = new MetadataSource + { + AssemblyName = loc.Symbol.ContainingAssembly.Name, + ProjectName = loc.Document.Project.Name, + TypeName = loc.Symbol.GetSymbolName() + }, + SourceGeneratedFileInfo = new SourceGeneratedFileInfo + { + DocumentGuid = loc.Document.Id.Id, + ProjectGuid = loc.Document.Id.ProjectId.Id + } + }) + .ToList()); + } + + return new GotoDefinitionResponse + { + Definitions = definitions + }; + } + } +} diff --git a/test-assets/test-projects/CakeProject/foo.cake b/test-assets/test-projects/CakeProject/foo.cake index d8831bb312..8447e374fe 100644 --- a/test-assets/test-projects/CakeProject/foo.cake +++ b/test-assets/test-projects/CakeProject/foo.cake @@ -1,6 +1,6 @@ var HelloText = "Hello World!"; -public class Foo +public partial class Foo { public static Foo Create() { @@ -11,4 +11,9 @@ public class Foo { return; } +} + +public partial class Foo +{ + } \ No newline at end of file diff --git a/tests/OmniSharp.Cake.Tests/GotoDefinitionFacts.cs b/tests/OmniSharp.Cake.Tests/GotoDefinitionFacts.cs index 4ffaeeb194..4df525dcd1 100644 --- a/tests/OmniSharp.Cake.Tests/GotoDefinitionFacts.cs +++ b/tests/OmniSharp.Cake.Tests/GotoDefinitionFacts.cs @@ -83,7 +83,7 @@ public async Task ShouldNavigateIntoDslMetadataWithoutGenericParams() var requestHandler = GetRequestHandler(host); var response = await requestHandler.Handle(request); - Assert.Null(response.FileName); + Assert.StartsWith("$metadata$", response.FileName); Assert.Equal(198, response.Line); Assert.Equal(27, response.Column); var metadata = response.MetadataSource; @@ -112,7 +112,7 @@ public async Task ShouldNavigateIntoDslMetadataWithGenericParams() var requestHandler = GetRequestHandler(host); var response = await requestHandler.Handle(request); - Assert.Null(response.FileName); + Assert.StartsWith("$metadata$", response.FileName); // TODO: if https://github.com/OmniSharp/omnisharp-roslyn/issues/2211 is corrected // line should be 63, and col 24 Assert.Equal(0, response.Line); @@ -143,7 +143,7 @@ public async Task ShouldNavigateIntoDslMetadataProperty() var requestHandler = GetRequestHandler(host); var response = await requestHandler.Handle(request); - Assert.Null(response.FileName); + Assert.StartsWith("$metadata$", response.FileName); Assert.Equal(115, response.Line); Assert.Equal(34, response.Column); var metadata = response.MetadataSource; diff --git a/tests/OmniSharp.Cake.Tests/GotoDefinitionV2Facts.cs b/tests/OmniSharp.Cake.Tests/GotoDefinitionV2Facts.cs new file mode 100644 index 0000000000..84d891021b --- /dev/null +++ b/tests/OmniSharp.Cake.Tests/GotoDefinitionV2Facts.cs @@ -0,0 +1,217 @@ +using OmniSharp.Cake.Services.RequestHandlers.Navigation; +using OmniSharp.Models.V2.GotoDefinition; + +using System.IO; +using System.Linq; +using System.Threading.Tasks; + +using TestUtility; + +using Xunit; +using Xunit.Abstractions; + +namespace OmniSharp.Cake.Tests +{ + public sealed class GotoDefinitionV2Facts : CakeSingleRequestHandlerTestFixture + { + public GotoDefinitionV2Facts(ITestOutputHelper testOutput) : base(testOutput) + { + } + + protected override string EndpointName => OmniSharpEndpoints.V2.GotoDefinition; + + [Fact] + public async Task ShouldSupportLoadedFiles() + { + using (var testProject = await TestAssets.Instance.GetTestProjectAsync("CakeProject", shadowCopy: false)) + using (var host = CreateOmniSharpHost(testProject.Directory)) + { + var fileName = Path.Combine(testProject.Directory, "build.cake"); + + var request = new GotoDefinitionRequest + { + FileName = fileName, + Line = 8, + Column = 10 + }; + + var requestHandler = GetRequestHandler(host); + var response = await requestHandler.Handle(request); + + Assert.NotNull(response.Definitions); + Assert.Single(response.Definitions); + var definition = response.Definitions.Single(); + + Assert.Equal(Path.Combine(testProject.Directory, "foo.cake"), definition.Location.FileName); + Assert.Equal(4, definition.Location.Range.Start.Line); + Assert.Equal(22, definition.Location.Range.Start.Column); + } + } + + [Fact] + public async Task ShouldNavigateToAProperty() + { + using (var testProject = await TestAssets.Instance.GetTestProjectAsync("CakeProject", shadowCopy: false)) + using (var host = CreateOmniSharpHost(testProject.Directory)) + { + var fileName = Path.Combine(testProject.Directory, "build.cake"); + + var request = new GotoDefinitionRequest + { + FileName = fileName, + Line = 11, + Column = 20 + }; + + var requestHandler = GetRequestHandler(host); + var response = await requestHandler.Handle(request); + + Assert.NotNull(response.Definitions); + Assert.Single(response.Definitions); + var definition = response.Definitions.Single(); + + Assert.Equal(Path.Combine(testProject.Directory, "foo.cake"), definition.Location.FileName); + Assert.Equal(0, definition.Location.Range.Start.Line); + Assert.Equal(4, definition.Location.Range.Start.Column); + } + } + + [Fact] + public async Task ShouldNavigateIntoDslMetadataWithoutGenericParams() + { + using (var testProject = await TestAssets.Instance.GetTestProjectAsync("CakeProject", shadowCopy: false)) + using (var host = CreateOmniSharpHost(testProject.Directory)) + { + var fileName = Path.Combine(testProject.Directory, "build.cake"); + + var request = new GotoDefinitionRequest + { + FileName = fileName, + Line = 11, + Column = 10, + WantMetadata = true + }; + + var requestHandler = GetRequestHandler(host); + var response = await requestHandler.Handle(request); + + Assert.NotNull(response.Definitions); + Assert.Single(response.Definitions); + var definition = response.Definitions.Single(); + Assert.StartsWith("$metadata$", definition.Location.FileName); + Assert.Equal(198, definition.Location.Range.Start.Line); + Assert.Equal(27, definition.Location.Range.Start.Column); + + var metadata = definition.MetadataSource; + Assert.NotNull(metadata); + Assert.Equal("Cake.Common", metadata.AssemblyName); + Assert.Equal("Cake.Common.Diagnostics.LoggingAliases", metadata.TypeName); + } + } + + [Fact] + public async Task ShouldNavigateIntoDslMetadataWithGenericParams() + { + using (var testProject = await TestAssets.Instance.GetTestProjectAsync("CakeProject", shadowCopy: false)) + using (var host = CreateOmniSharpHost(testProject.Directory)) + { + var fileName = Path.Combine(testProject.Directory, "build.cake"); + + var request = new GotoDefinitionRequest + { + FileName = fileName, + Line = 0, + Column = 16, + WantMetadata = true + }; + + var requestHandler = GetRequestHandler(host); + var response = await requestHandler.Handle(request); + + Assert.NotNull(response.Definitions); + Assert.Single(response.Definitions); + var definition = response.Definitions.Single(); + Assert.StartsWith("$metadata$", definition.Location.FileName); + // TODO: if https://github.com/OmniSharp/omnisharp-roslyn/issues/2211 is corrected + // line should be 63, and col 24 + Assert.Equal(0, definition.Location.Range.Start.Line); + Assert.Equal(0, definition.Location.Range.Start.Column); + + var metadata = definition.MetadataSource; + Assert.NotNull(metadata); + Assert.Equal("Cake.Common", metadata.AssemblyName); + Assert.Equal("Cake.Common.ArgumentAliases", metadata.TypeName); + } + } + + [Fact] + public async Task ShouldNavigateIntoDslMetadataProperty() + { + using (var testProject = await TestAssets.Instance.GetTestProjectAsync("CakeProject", shadowCopy: false)) + using (var host = CreateOmniSharpHost(testProject.Directory)) + { + var fileName = Path.Combine(testProject.Directory, "build.cake"); + + var request = new GotoDefinitionRequest + { + FileName = fileName, + Line = 12, + Column = 37, + WantMetadata = true + }; + + var requestHandler = GetRequestHandler(host); + var response = await requestHandler.Handle(request); + + Assert.NotNull(response.Definitions); + Assert.Single(response.Definitions); + var definition = response.Definitions.Single(); + Assert.StartsWith("$metadata$", definition.Location.FileName); + Assert.Equal(115, definition.Location.Range.Start.Line); + Assert.Equal(34, definition.Location.Range.Start.Column); + + var metadata = definition.MetadataSource; + Assert.NotNull(metadata); + Assert.Equal("Cake.Common", metadata.AssemblyName); + Assert.Equal("Cake.Common.Build.BuildSystemAliases", metadata.TypeName); + } + } + + [Fact] + public async Task ShouldFindMultipleLocationsForPartial() + { + using (var testProject = await TestAssets.Instance.GetTestProjectAsync("CakeProject", shadowCopy: false)) + using (var host = CreateOmniSharpHost(testProject.Directory)) + { + var fileName = Path.Combine(testProject.Directory, "build.cake"); + + var request = new GotoDefinitionRequest + { + FileName = fileName, + Line = 7, + Column = 5 + }; + + var requestHandler = GetRequestHandler(host); + var response = await requestHandler.Handle(request); + + Assert.NotNull(response.Definitions); + var expectedFile = Path.Combine(testProject.Directory, "foo.cake"); + Assert.Collection( + response.Definitions, + d => + { + Assert.Equal(expectedFile, d.Location.FileName); + Assert.Equal(2, d.Location.Range.Start.Line); + Assert.Equal(21, d.Location.Range.Start.Column); + }, + d => + { + Assert.Equal(expectedFile, d.Location.FileName); + Assert.Equal(15, d.Location.Range.Start.Line); + Assert.Equal(21, d.Location.Range.Start.Column); + }); + } + } + } +}