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

Conversation

filipw
Copy link
Member

@filipw filipw commented Jun 2, 2017

This is an attempt to fix #876
I am not sure if this is the best way to do it, but I wanted to fit into the existing endpoint structure to avoid breaking changes.

The current flow of the client trying to go to metadata is the following

  1. Invoke /gotodefinition. The server code recognizes the symbol location is in metadata, and uses Roslyn's MetadataAsSourceService to generate a temporary document a provide basic metadata information to the caller - such as the position of the symbol in the metadata source. No metadata source is returned though.
  2. Invoke /metadata and pass in the meta information (i.e. TypeName, AssemblyName). The server will then once again call into MetadataAsSourceService, generate a temporary document again, and return the source text of that document, along with its temporary name.
  3. The client stitches these pieces of information together - displays the metadata source text, with its temporary name, and positions the user correctly based on the location obtained from the original /gotodefinition call.

It's impossible at the moment to continue the navigation from metadata to other metadata sources, because that would require going through /gotodefinition endpoint again, and it doesn't support starting points that are not a document inside the workspace.

This PR changes the above flow in the following, non-breaking way

  1. We still start by invoking /gotodefinition. The code recognizes the symbol location is in metadata, and uses Roslyn's MetadataAsSourceService to generate a temporary document a provide basic metadata information to the caller - such as the position of the symbol in the metadata source. No metadata source is returned though. The difference is that the temporary document document containing metadata source is stored in memory for future reuse (not added to current workspace, just stored on a side in a dictionary).
  2. Invoke /metadata and pass in the meta information (i.e. TypeName, AssemblyName). The server now doesn't call into MetadataAsSourceService again (so we have a perf boost), but rather it will use the cached document and return the source text of that document, along with its temporary name.
  3. The client stitches these pieces of information together - displays the metadata source text, with its temporary name, and positions the user correctly based on the location obtained from the original /gotodefinition call.
  4. The client can now call /gotodefinition and ask for anything based on the location in the metadata source document - that is because the /gotodefinition endpoint has also been modified to not only work with documents that are part of Workspace but also with metadata sources using the aformentioned metadata cache to look up the sources. From there the process goes back to point 1, and we can have a neverending navigation cycle between metadata documents.

Let me know what you think, and whether this approach makes sense - we can absolutely change it to something better.
I also specifically avoided storing the metadata source documents inside the workspace as that started impacting a lot of other endpoints in an unwelcome way (i.e. go to symbol).

Copy link
Member

@david-driscoll david-driscoll left a comment

Choose a reason for hiding this comment

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

Couple minor changes, otherwise 👍 .

Expanding to include perhaps find usages, and type info would be cool, but certainly not needed on the first pass.

var document = _workspace.GetDocument(request.FileName);
var document = request.IsMetadataFile() ?
_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

}

Document metadataDocument = null;
if (!_metadataDocumentCache.TryGetValue(fileName, out metadataDocument))
Copy link
Member

Choose a reason for hiding this comment

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

could use out var metadataDocument

Copy link
Contributor

Choose a reason for hiding this comment

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

Definitely. 😄

@@ -127,6 +127,8 @@ public IEnumerable<Document> GetDocuments(string filePath)

public Document GetDocument(string filePath)
{
if (filePath == null) return null;
Copy link
Member

Choose a reason for hiding this comment

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

Should probably use string.IsNullOrEmpty()

Copy link
Contributor

Choose a reason for hiding this comment

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

Or string.IsNullOrWhitespace(...)

Copy link
Contributor

@DustinCampbell DustinCampbell left a comment

Choose a reason for hiding this comment

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

Looks good! Just a few "clean up" suggestions. Also, the CI failure looks like a real test failure.

}

public Task<Document> GetDocumentFromMetadata(Project project, ISymbol symbol, CancellationToken cancellationToken = new CancellationToken())
public async Task<MetadataDocumentResult> GetAndAddDocumentFromMetadata(Project project, ISymbol symbol, CancellationToken cancellationToken = new CancellationToken())
Copy link
Contributor

Choose a reason for hiding this comment

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

What a beautiful place to use a C# 7 tuple!

}

Document metadataDocument = null;
if (!_metadataDocumentCache.TryGetValue(fileName, out metadataDocument))
Copy link
Contributor

Choose a reason for hiding this comment

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

Definitely. 😄

var topLevelSymbol = GetTopLevelContainingNamedType(symbol);
var fileName = GetFilePathForSymbol(project, symbol);

Project metadataProject = null;
Copy link
Contributor

Choose a reason for hiding this comment

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

Tiny, tiny, minuscule nit: I don't think you need = null here because `metadataProject is definitely assigned before it is used below.

@@ -127,6 +127,8 @@ public IEnumerable<Document> GetDocuments(string filePath)

public Document GetDocument(string filePath)
{
if (filePath == null) return null;
Copy link
Contributor

Choose a reason for hiding this comment

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

Or string.IsNullOrWhitespace(...)

@@ -6,6 +6,8 @@
using TestUtility;
using Xunit;
using Xunit.Abstractions;
using OmniSharp.Models.GotoDefinition;
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?

var metadataTree = CSharpSyntaxTree.ParseText(metadataResponse.Source);
var iComparable = metadataTree.GetCompilationUnitRoot().DescendantNodesAndSelf().OfType<BaseTypeDeclarationSyntax>().
First().BaseList.Types.FirstOrDefault(x => x.Type.ToString() == "IComparable");

Copy link
Contributor

Choose a reason for hiding this comment

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

Nit: extra blank line

[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!

FileName = testFile.FileName,
Line = point.Line,
Column = point.Offset,
Timeout = 60000,
Copy link
Contributor

Choose a reason for hiding this comment

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

Is it possible to call this endpoint without a timeout?

// 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");
Copy link
Contributor

Choose a reason for hiding this comment

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

Would love to see this broken down onto separate lines for readability.

.WithCompilationOptions(project.CompilationOptions)
.WithMetadataReferences(project.MetadataReferences)
: project;
.WithMetadataReferences(project.MetadataReferences);
Copy link
Contributor

Choose a reason for hiding this comment

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

Should this be indented?

@filipw
Copy link
Member Author

filipw commented Jun 6, 2017

thanks for the comments 👍 and sorry for the delay (long weekend) 🌴
I'll make the changes you mentioned

@filipw
Copy link
Member Author

filipw commented Jun 6, 2017

I addressed the comments, squashed the commits to remove the noise I managed to generate and rebased this on top of the latest dev.

@filipw filipw force-pushed the feature/go-to-metadata-2 branch 2 times, most recently from dd627bd to c317826 Compare June 6, 2017 14:03
Copy link
Contributor

@DustinCampbell DustinCampbell left a comment

Choose a reason for hiding this comment

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

LGTM

@DustinCampbell
Copy link
Contributor

Are you good here @filipw? If so, I think you can merge this once CI completes again.

@filipw
Copy link
Member Author

filipw commented Jun 10, 2017

I think so!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

3 participants