-
Notifications
You must be signed in to change notification settings - Fork 420
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Create a new GoToTypeDefinition endpoint #2315
Changes from 10 commits
75bd929
7cf4938
37c0c3e
3ba51bf
a60e227
3464747
8936863
c79f647
afd79e1
96c5ab6
070884e
e8bcc74
7d3b562
14129b0
ffb8dee
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,11 @@ | ||
using OmniSharp.Mef; | ||
|
||
namespace OmniSharp.Models.V2.GotoTypeDefinition | ||
{ | ||
[OmniSharpEndpoint(OmniSharpEndpoints.V2.GotoTypeDefinition, typeof(GotoTypeDefinitionRequest), typeof(GotoTypeDefinitionResponse))] | ||
public class GotoTypeDefinitionRequest : Request | ||
{ | ||
public int Timeout { get; init; } = 10000; | ||
public bool WantMetadata { get; init; } | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,20 @@ | ||
#nullable enable | ||
|
||
using OmniSharp.Models.Metadata; | ||
using OmniSharp.Models.v1.SourceGeneratedFile; | ||
using System.Collections.Generic; | ||
|
||
namespace OmniSharp.Models.V2.GotoTypeDefinition | ||
{ | ||
public record GotoTypeDefinitionResponse | ||
{ | ||
public List<TypeDefinition>? Definitions { get; init; } | ||
} | ||
|
||
public record TypeDefinition | ||
{ | ||
public Location Location { get; init; } = null!; | ||
public MetadataSource? MetadataSource { get; init; } | ||
public SourceGeneratedFileInfo? SourceGeneratedFileInfo { get; init; } | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,116 @@ | ||
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.GotoTypeDefinition; | ||
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; | ||
using Range = OmniSharp.Models.V2.Range; | ||
|
||
namespace OmniSharp.Cake.Services.RequestHandlers.Navigation | ||
{ | ||
[OmniSharpHandler(OmniSharpEndpoints.V2.GotoTypeDefinition, Constants.LanguageNames.Cake), Shared] | ||
public class GotoTypeDefinitionV2Handler : CakeRequestHandler<GotoTypeDefinitionRequest, GotoTypeDefinitionResponse> | ||
{ | ||
private readonly MetadataExternalSourceService _metadataExternalSourceService; | ||
|
||
[ImportingConstructor] | ||
public GotoTypeDefinitionV2Handler( | ||
OmniSharpWorkspace workspace, | ||
MetadataExternalSourceService metadataExternalSourceService) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think this should use There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This file seems to be mostly a cut and paste of the GotoDefinitionV2Handler. Perhaps we can update all the Navigation handlers as a follow up. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Yeah, it was a copy and paste. I've updated to use the |
||
: base(workspace) | ||
{ | ||
_metadataExternalSourceService = metadataExternalSourceService ?? throw new ArgumentNullException(nameof(metadataExternalSourceService)); | ||
} | ||
|
||
protected override async Task<GotoTypeDefinitionResponse> TranslateResponse(GotoTypeDefinitionResponse response, GotoTypeDefinitionRequest request) | ||
{ | ||
var definitions = new List<TypeDefinition>(); | ||
foreach (var definition in response.Definitions ?? Enumerable.Empty<TypeDefinition>()) | ||
{ | ||
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 TypeDefinition | ||
{ | ||
MetadataSource = definition.MetadataSource, | ||
SourceGeneratedFileInfo = definition.SourceGeneratedFileInfo, | ||
Location = new Location | ||
{ | ||
FileName = file, | ||
Range = definition.Location.Range | ||
} | ||
}); | ||
|
||
continue; | ||
} | ||
|
||
if (!request.WantMetadata) | ||
{ | ||
continue; | ||
} | ||
|
||
var aliasLocations = await GotoTypeDefinitionHandlerHelper.GetAliasFromMetadataAsync( | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I am not sure you can assume that metadata will be used, because the user might have enabled decompilation There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Thanks for the tip, I've updated to use the |
||
Workspace, | ||
request.FileName, | ||
definition.Location.Range.End.Line, | ||
request.Timeout, | ||
_metadataExternalSourceService | ||
); | ||
|
||
definitions.AddRange( | ||
aliasLocations.Select(loc => | ||
new TypeDefinition | ||
{ | ||
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 GotoTypeDefinitionResponse | ||
{ | ||
Definitions = definitions | ||
}; | ||
} | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,122 @@ | ||
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 GotoTypeDefinitionHandlerHelper | ||
{ | ||
private const int MethodLineOffset = 3; | ||
private const int PropertyLineOffset = 7; | ||
|
||
internal static async Task<IEnumerable<Alias>> 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<Alias>(); | ||
} | ||
|
||
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<Alias>(); | ||
} | ||
} | ||
|
||
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<Alias>(); | ||
} | ||
if (symbol is IMethodSymbol method) | ||
{ | ||
symbol = method.PartialImplementationPart ?? symbol; | ||
} | ||
|
||
var typeSymbol = symbol switch | ||
{ | ||
ILocalSymbol localSymbol => localSymbol.Type, | ||
IFieldSymbol fieldSymbol => fieldSymbol.Type, | ||
IPropertySymbol propertySymbol => propertySymbol.Type, | ||
IParameterSymbol parameterSymbol => parameterSymbol.Type, | ||
_ => null | ||
}; | ||
|
||
var result = new List<Alias>(); | ||
foreach (var location in typeSymbol.Locations) | ||
JoeRobich marked this conversation as resolved.
Show resolved
Hide resolved
|
||
{ | ||
if (!location.IsInMetadata) | ||
{ | ||
continue; | ||
} | ||
|
||
var cancellationSource = new CancellationTokenSource(TimeSpan.FromMilliseconds(timeout)); | ||
var (metadataDocument, _) = await metadataExternalSourceService.GetAndAddExternalSymbolDocument(document.Project, typeSymbol, cancellationSource.Token); | ||
if (metadataDocument == null) | ||
{ | ||
continue; | ||
} | ||
|
||
cancellationSource = new CancellationTokenSource(TimeSpan.FromMilliseconds(timeout)); | ||
var metadataLocation = await metadataExternalSourceService.GetExternalSymbolLocation(typeSymbol, metadataDocument, cancellationSource.Token); | ||
var lineSpan = metadataLocation.GetMappedLineSpan(); | ||
|
||
result.Add(new Alias | ||
{ | ||
Document = document, | ||
MetadataDocument = metadataDocument, | ||
Symbol = typeSymbol, | ||
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; } | ||
} | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,64 @@ | ||
#nullable enable | ||
|
||
using Microsoft.CodeAnalysis; | ||
using Microsoft.CodeAnalysis.FindSymbols; | ||
using Microsoft.CodeAnalysis.Operations; | ||
using OmniSharp.Extensions; | ||
using OmniSharp.Models.v1.SourceGeneratedFile; | ||
using System.Diagnostics; | ||
using System.Threading; | ||
using System.Threading.Tasks; | ||
|
||
namespace OmniSharp.Roslyn.CSharp.Services.Navigation | ||
{ | ||
internal static class GotoTypeDefinitionHelpers | ||
{ | ||
internal static async Task<ITypeSymbol?> GetTypeOfSymbol(Document document, int line, int column, CancellationToken cancellationToken) | ||
{ | ||
var sourceText = await document.GetTextAsync(cancellationToken); | ||
var position = sourceText.GetPositionFromLineAndOffset(line, column); | ||
var symbol = await SymbolFinder.FindSymbolAtPositionAsync(document, position, cancellationToken); | ||
|
||
return symbol switch | ||
{ | ||
ILocalSymbol localSymbol => localSymbol.Type, | ||
IFieldSymbol fieldSymbol => fieldSymbol.Type, | ||
IPropertySymbol propertySymbol => propertySymbol.Type, | ||
IParameterSymbol parameterSymbol => parameterSymbol.Type, | ||
_ => null | ||
}; | ||
} | ||
|
||
internal static async Task<FileLinePositionSpan?> GetMetadataMappedSpan( | ||
Document document, | ||
ISymbol symbol, | ||
IExternalSourceService externalSourceService, | ||
CancellationToken cancellationToken) | ||
{ | ||
var (metadataDocument, _) = await externalSourceService.GetAndAddExternalSymbolDocument(document.Project, symbol, cancellationToken); | ||
if (metadataDocument != null) | ||
{ | ||
var metadataLocation = await externalSourceService.GetExternalSymbolLocation(symbol, metadataDocument, cancellationToken); | ||
return metadataLocation.GetMappedLineSpan(); | ||
} | ||
|
||
return null; | ||
} | ||
|
||
internal static SourceGeneratedFileInfo? GetSourceGeneratedFileInfo(OmniSharpWorkspace workspace, Location location) | ||
{ | ||
Debug.Assert(location.IsInSource); | ||
var document = workspace.CurrentSolution.GetDocument(location.SourceTree); | ||
if (document is not SourceGeneratedDocument) | ||
{ | ||
return null; | ||
} | ||
|
||
return new SourceGeneratedFileInfo | ||
{ | ||
ProjectGuid = document.Project.Id.Id, | ||
DocumentGuid = document.Id.Id | ||
}; | ||
} | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@filipw Since this is the first iteration of this endpoint, is it ok to be under V2?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think it should just be
/gototypedefinition
, we do not version at API level but on endpoint level.for example
/completion
is the modern version of intellisense and it was not versioned asv2
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Okay, it's updated accordingly