Skip to content
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

Support go to definition from metadata source #883

Merged
merged 7 commits into from
Jun 12, 2017
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/OmniSharp.Abstractions/Models/Request.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
using System.Collections.Generic;
using System.Collections.Generic;
using System.IO;
using Newtonsoft.Json;

Expand Down
1 change: 1 addition & 0 deletions src/OmniSharp.Roslyn.CSharp/Helpers/LocationExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ public static QuickFix GetQuickFix(this Location location, OmniSharpWorkspace wo
var lineSpan = location.GetLineSpan();
var path = lineSpan.Path;
var documents = workspace.GetDocuments(path);

var line = lineSpan.StartLinePosition.Line;
var text = location.SourceTree.GetText().Lines[line].ToString();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,11 @@ public async Task<GotoDefinitionResponse> Handle(GotoDefinitionRequest request)
{
var quickFixes = new List<QuickFix>();

var document = _workspace.GetDocument(request.FileName);
var document = _metadataHelper.FindDocumentInMetadataCache(request.FileName) ??
_workspace.GetDocument(request.FileName);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This could technically work for other endpoints as well right? Thinking of /findusages, /typelookup and maybe even /highlight.

Would be great if we could simplify this.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We could probably just make it _metadataHelper.FindDocumentInMetadataCache(request.FileName) ?? _workspace.GetDocument(request.FileName); to avoid the first if there.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes this approach should work for other endpoints too


var response = new GotoDefinitionResponse();

if (document != null)
{
var semanticModel = await document.GetSemanticModelAsync();
Expand Down Expand Up @@ -64,10 +67,11 @@ public async Task<GotoDefinitionResponse> Handle(GotoDefinitionRequest request)
else if (location.IsInMetadata && request.WantMetadata)
{
var cancellationSource = new CancellationTokenSource(TimeSpan.FromMilliseconds(request.Timeout));
var metadataDocument = await _metadataHelper.GetDocumentFromMetadata(document.Project, symbol, cancellationSource.Token);
var (metadataDocument, _) = await _metadataHelper.GetAndAddDocumentFromMetadata(document.Project, symbol, cancellationSource.Token);
if (metadataDocument != null)
{
cancellationSource = new CancellationTokenSource(TimeSpan.FromMilliseconds(request.Timeout));

var metadataLocation = await _metadataHelper.GetSymbolLocationFromMetadata(symbol, metadataDocument, cancellationSource.Token);
var lineSpan = metadataLocation.GetMappedLineSpan();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,12 +31,12 @@ public async Task<MetadataResponse> Handle(MetadataRequest request)
if (symbol != null && symbol.ContainingAssembly.Name == request.AssemblyName)
{
var cancellationSource = new CancellationTokenSource(TimeSpan.FromMilliseconds(request.Timeout));
var document = await _metadataHelper.GetDocumentFromMetadata(project, symbol, cancellationSource.Token);
if (document != null)
var (metadataDocument, documentPath) = await _metadataHelper.GetAndAddDocumentFromMetadata(project, symbol, cancellationSource.Token);
if (metadataDocument != null)
{
var source = await document.GetTextAsync();
response.SourceName = _metadataHelper.GetFilePathForSymbol(project, symbol);
var source = await metadataDocument.GetTextAsync();
response.Source = source.ToString();
response.SourceName = documentPath;

return response;
}
Expand Down
13 changes: 13 additions & 0 deletions src/OmniSharp.Roslyn/Extensions/SymbolExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -26,5 +26,18 @@ public static string GetKind(this ISymbol symbol)

return Enum.GetName(symbol.Kind.GetType(), symbol.Kind);
}

internal static INamedTypeSymbol GetTopLevelContainingNamedType(this ISymbol symbol)
{
// Traverse up until we find a named type that is parented by the namespace
var topLevelNamedType = symbol;
while (topLevelNamedType.ContainingSymbol != symbol.ContainingNamespace ||
topLevelNamedType.Kind != SymbolKind.NamedType)
{
topLevelNamedType = topLevelNamedType.ContainingSymbol;
}

return (INamedTypeSymbol)topLevelNamedType;
}
}
}
91 changes: 55 additions & 36 deletions src/OmniSharp.Roslyn/MetadataHelper.cs
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis;
using OmniSharp.Extensions;
using OmniSharp.Services;
using OmniSharp.Utilities;

Expand All @@ -21,13 +23,15 @@ public class MetadataHelper
private readonly Lazy<Type> _symbolKey;
private readonly Lazy<Type> _metadataAsSourceHelper;
private readonly Lazy<MethodInfo> _getLocationInGeneratedSourceAsync;
private Dictionary<string, Document> _metadataDocumentCache = new Dictionary<string, Document>();

private const string CSharpMetadataAsSourceService = "Microsoft.CodeAnalysis.CSharp.MetadataAsSource.CSharpMetadataAsSourceService";
private const string SymbolKey = "Microsoft.CodeAnalysis.SymbolKey";
private const string MetadataAsSourceHelpers = "Microsoft.CodeAnalysis.MetadataAsSource.MetadataAsSourceHelpers";
private const string GetLocationInGeneratedSourceAsync = "GetLocationInGeneratedSourceAsync";
private const string AddSourceToAsync = "AddSourceToAsync";
private const string Create = "Create";
private const string MetadataKey = "$Metadata$";

public MetadataHelper(IAssemblyLoader loader)
{
Expand All @@ -43,36 +47,61 @@ public MetadataHelper(IAssemblyLoader loader)
_getLocationInGeneratedSourceAsync = _metadataAsSourceHelper.LazyGetMethod(GetLocationInGeneratedSourceAsync);
}

public string GetSymbolName(ISymbol symbol)
public Document FindDocumentInMetadataCache(string fileName)
{
var topLevelSymbol = GetTopLevelContainingNamedType(symbol);
return GetTypeDisplayString(topLevelSymbol);
if (_metadataDocumentCache.TryGetValue(fileName, out var metadataDocument))
{
return metadataDocument;
}

return null;
}

public string GetFilePathForSymbol(Project project, ISymbol symbol)
public string GetSymbolName(ISymbol symbol)
{
var topLevelSymbol = GetTopLevelContainingNamedType(symbol);
return $"metadata/Project/{Folderize(project.Name)}/Assembly/{Folderize(topLevelSymbol.ContainingAssembly.Name)}/Symbol/{Folderize(GetTypeDisplayString(topLevelSymbol))}.cs".Replace(Path.AltDirectorySeparatorChar, Path.DirectorySeparatorChar);
var topLevelSymbol = symbol.GetTopLevelContainingNamedType();
return GetTypeDisplayString(topLevelSymbol);
}

public Task<Document> GetDocumentFromMetadata(Project project, ISymbol symbol, CancellationToken cancellationToken = new CancellationToken())
public async Task<(Document metadataDocument, string documentPath)> GetAndAddDocumentFromMetadata(Project project, ISymbol symbol, CancellationToken cancellationToken = new CancellationToken())
{
var filePath = GetFilePathForSymbol(project, symbol);
var topLevelSymbol = GetTopLevelContainingNamedType(symbol);
var fileName = GetFilePathForSymbol(project, symbol);

Project metadataProject;

// since submission projects cannot have new documents added to it
// we will use temporary project to hold metadata documents
var metadataProject = project.IsSubmission
? project.Solution.AddProject("metadataTemp", "metadataTemp.dll", LanguageNames.CSharp)
.WithCompilationOptions(project.CompilationOptions)
.WithMetadataReferences(project.MetadataReferences)
: project;

var temporaryDocument = metadataProject.AddDocument(filePath, string.Empty);
var service = _csharpMetadataAsSourceService.CreateInstance(temporaryDocument.Project.LanguageServices);
var method = _csharpMetadataAsSourceService.GetMethod(AddSourceToAsync);

return method.Invoke<Task<Document>>(service, new object[] { temporaryDocument, topLevelSymbol, cancellationToken });
// we will use a separate project to hold metadata documents
if (project.IsSubmission)
{
metadataProject = project.Solution.Projects.FirstOrDefault(x => x.Name == MetadataKey);
if (metadataProject == null)
{
metadataProject = project.Solution.AddProject(MetadataKey, $"{MetadataKey}.dll", LanguageNames.CSharp)
.WithCompilationOptions(project.CompilationOptions)
.WithMetadataReferences(project.MetadataReferences);
}
}
else
{
// for regular projects we will use current project to store metadata
metadataProject = project;
}

if (!_metadataDocumentCache.TryGetValue(fileName, out var metadataDocument))
{
var topLevelSymbol = symbol.GetTopLevelContainingNamedType();

var temporaryDocument = metadataProject.AddDocument(fileName, string.Empty);
var service = _csharpMetadataAsSourceService.CreateInstance(temporaryDocument.Project.LanguageServices);
var method = _csharpMetadataAsSourceService.GetMethod(AddSourceToAsync);

var documentTask = method.Invoke<Task<Document>>(service, new object[] { temporaryDocument, topLevelSymbol, default(CancellationToken) });
metadataDocument = await documentTask;

_metadataDocumentCache[fileName] = metadataDocument;
}

return (metadataDocument, fileName);
}

public async Task<Location> GetSymbolLocationFromMetadata(ISymbol symbol, Document metadataDocument, CancellationToken cancellationToken = new CancellationToken())
Expand All @@ -83,7 +112,7 @@ public string GetFilePathForSymbol(Project project, ISymbol symbol)
return await _getLocationInGeneratedSourceAsync.InvokeStatic<Task<Location>>(new object[] { symboldId, metadataDocument, cancellationToken });
}

private string GetTypeDisplayString(INamedTypeSymbol symbol)
private static string GetTypeDisplayString(INamedTypeSymbol symbol)
{
if (symbol.SpecialType != SpecialType.None)
{
Expand Down Expand Up @@ -116,22 +145,12 @@ private string GetTypeDisplayString(INamedTypeSymbol symbol)
return symbol.ToDisplayString();
}

private string Folderize(string path)
private static string GetFilePathForSymbol(Project project, ISymbol symbol)
{
return string.Join("/", path.Split('.'));
var topLevelSymbol = symbol.GetTopLevelContainingNamedType();
return $"$metadata$/Project/{Folderize(project.Name)}/Assembly/{Folderize(topLevelSymbol.ContainingAssembly.Name)}/Symbol/{Folderize(GetTypeDisplayString(topLevelSymbol))}.cs".Replace(Path.AltDirectorySeparatorChar, Path.DirectorySeparatorChar);
}

private INamedTypeSymbol GetTopLevelContainingNamedType(ISymbol symbol)
{
// Traverse up until we find a named type that is parented by the namespace
var topLevelNamedType = symbol;
while (topLevelNamedType.ContainingSymbol != symbol.ContainingNamespace ||
topLevelNamedType.Kind != SymbolKind.NamedType)
{
topLevelNamedType = topLevelNamedType.ContainingSymbol;
}

return (INamedTypeSymbol)topLevelNamedType;
}
private static string Folderize(string path) => string.Join("/", path.Split('.'));
}
}
2 changes: 2 additions & 0 deletions src/OmniSharp.Roslyn/OmniSharpWorkspace.cs
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,8 @@ public IEnumerable<Document> GetDocuments(string filePath)

public Document GetDocument(string filePath)
{
if (string.IsNullOrWhiteSpace(filePath)) return null;

var documentId = GetDocumentId(filePath);
if (documentId == null)
{
Expand Down
73 changes: 73 additions & 0 deletions tests/OmniSharp.Roslyn.CSharp.Tests/GoToDefinitionFacts.cs
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
using System.Linq;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using OmniSharp.Models.GotoDefinition;
using OmniSharp.Roslyn.CSharp.Services.Navigation;
using OmniSharp.Models.Metadata;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sort usings?

using TestUtility;
using Xunit;
using Xunit.Abstractions;
Expand Down Expand Up @@ -159,6 +162,76 @@ await TestGoToMetadataAsync(testFile,
expectedTypeName: "System.String");
}

[Theory]
[InlineData("bar.cs")]
[InlineData("bar.csx")]
public async Task ReturnsDefinitionInMetadata_FromMetadata_WhenSymbolIsType(string filename)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Again, unifying the pattern for calling each handler would be helpful for a minor readability improvement.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the comments though! Very helpful!

{
var testFile = new TestFile(filename, @"
using System;
class Bar {
public void Baz() {
var number = in$$t.MaxValue;
}
}");

using (var host = CreateOmniSharpHost(testFile))
{
var point = testFile.Content.GetPointFromPosition();

// 1. start by asking for definition of "int"
var gotoDefinitionRequest = new GotoDefinitionRequest
{
FileName = testFile.FileName,
Line = point.Line,
Column = point.Offset,
WantMetadata = true
};
var gotoDefinitionRequestHandler = GetRequestHandler(host);
var gotoDefinitionResponse = await gotoDefinitionRequestHandler.Handle(gotoDefinitionRequest);

// 2. now, based on the response information
// go to the metadata endpoint, and ask for "int" specific metadata
var metadataRequest = new MetadataRequest
{
AssemblyName = gotoDefinitionResponse.MetadataSource.AssemblyName,
TypeName = gotoDefinitionResponse.MetadataSource.TypeName,
ProjectName = gotoDefinitionResponse.MetadataSource.ProjectName,
Language = gotoDefinitionResponse.MetadataSource.Language
};
var metadataRequestHandler = host.GetRequestHandler<MetadataService>(OmniSharpEndpoints.Metadata);
var metadataResponse = await metadataRequestHandler.Handle(metadataRequest);

// 3. the metadata response contains SourceName (metadata "file") and SourceText (syntax tree)
// use the source to locate "IComparable" which is an interface implemented by Int32 struct
var metadataTree = CSharpSyntaxTree.ParseText(metadataResponse.Source);
var iComparable = metadataTree.GetCompilationUnitRoot().
DescendantNodesAndSelf().
OfType<BaseTypeDeclarationSyntax>().First().
BaseList.Types.FirstOrDefault(x => x.Type.ToString() == "IComparable");
var relevantLineSpan = iComparable.GetLocation().GetLineSpan();

// 4. now ask for the definition of "IComparable"
// pass in the SourceName (metadata "file") as FileName - since it's not a regular file in our workspace
var metadataNavigationRequest = new GotoDefinitionRequest
{
FileName = metadataResponse.SourceName,
Line = relevantLineSpan.StartLinePosition.Line,
Column = relevantLineSpan.StartLinePosition.Character,
WantMetadata = true
};
var metadataNavigationResponse = await gotoDefinitionRequestHandler.Handle(metadataNavigationRequest);

// 5. validate the response to be matching the expected IComparable meta info
Assert.NotNull(metadataNavigationResponse.MetadataSource);
Assert.Equal(AssemblyHelpers.CorLibName, metadataNavigationResponse.MetadataSource.AssemblyName);
Assert.Equal("System.IComparable", metadataNavigationResponse.MetadataSource.TypeName);

Assert.NotEqual(0, metadataNavigationResponse.Line);
Assert.NotEqual(0, metadataNavigationResponse.Column);
}
}

private async Task TestGoToSourceAsync(params TestFile[] testFiles)
{
var response = await GetResponseAsync(testFiles, wantMetadata: false);
Expand Down