Skip to content

Commit

Permalink
Merge pull request #2170 from 333fred/source-generator-gotodef
Browse files Browse the repository at this point in the history
Add support for GoToDefinition on source-generated files
  • Loading branch information
filipw committed Jun 2, 2021
2 parents 8a23df3 + 791bd5f commit 65bb630
Show file tree
Hide file tree
Showing 19 changed files with 448 additions and 15 deletions.
@@ -1,16 +1,20 @@
#nullable enable

using Newtonsoft.Json;
using OmniSharp.Models.Metadata;
using OmniSharp.Models.v1.SourceGeneratedFile;

namespace OmniSharp.Models.GotoDefinition
{
public class GotoDefinitionResponse : ICanBeEmptyResponse
{
public string FileName { get; set; }
public string? FileName { get; set; }
[JsonConverter(typeof(ZeroBasedIndexConverter))]
public int Line { get; set; }
[JsonConverter(typeof(ZeroBasedIndexConverter))]
public int Column { get; set; }
public MetadataSource MetadataSource { get; set; }
public bool IsEmpty => string.IsNullOrWhiteSpace(FileName) && MetadataSource == null;
public MetadataSource? MetadataSource { get; set; }
public SourceGeneratedFileInfo? SourceGeneratedInfo { get; set; }
public bool IsEmpty => string.IsNullOrWhiteSpace(FileName) && MetadataSource == null && SourceGeneratedInfo == null;
}
}
@@ -0,0 +1,11 @@
#nullable enable

using OmniSharp.Mef;

namespace OmniSharp.Models.v1.SourceGeneratedFile
{
[OmniSharpEndpoint(OmniSharpEndpoints.SourceGeneratedFileClosed, typeof(SourceGeneratedFileClosedRequest), typeof(SourceGeneratedFileClosedResponse))]
public sealed record SourceGeneratedFileClosedRequest : SourceGeneratedFileInfo, IRequest
{
}
}
@@ -0,0 +1,10 @@
#nullable enable
using System.Threading.Tasks;

namespace OmniSharp.Models.v1.SourceGeneratedFile
{
public sealed class SourceGeneratedFileClosedResponse
{
public static readonly Task<SourceGeneratedFileClosedResponse> Instance = Task.FromResult(new SourceGeneratedFileClosedResponse());
}
}
@@ -0,0 +1,12 @@
#nullable enable

using System;

namespace OmniSharp.Models.v1.SourceGeneratedFile
{
public record SourceGeneratedFileInfo
{
public Guid ProjectGuid { get; init; }
public Guid DocumentGuid { get; init; }
}
}
@@ -0,0 +1,12 @@
#nullable enable

using OmniSharp.Mef;
using System;

namespace OmniSharp.Models.v1.SourceGeneratedFile
{
[OmniSharpEndpoint(OmniSharpEndpoints.SourceGeneratedFile, typeof(SourceGeneratedFileRequest), typeof(SourceGeneratedFileResponse))]
public sealed record SourceGeneratedFileRequest : SourceGeneratedFileInfo, IRequest
{
}
}
@@ -0,0 +1,10 @@
#nullable enable

namespace OmniSharp.Models.v1.SourceGeneratedFile
{
public sealed record SourceGeneratedFileResponse
{
public string? SourceName { get; init; }
public string? Source { get; init; }
}
}
@@ -0,0 +1,10 @@
#nullable enable
using OmniSharp.Mef;

namespace OmniSharp.Models.v1.SourceGeneratedFile
{
[OmniSharpEndpoint(OmniSharpEndpoints.UpdateSourceGeneratedFile, typeof(UpdateSourceGeneratedFileRequest), typeof(UpdateSourceGeneratedFileResponse))]
public sealed record UpdateSourceGeneratedFileRequest : SourceGeneratedFileInfo, IRequest
{
}
}
@@ -0,0 +1,17 @@
#nullable enable

namespace OmniSharp.Models.v1.SourceGeneratedFile
{
public record UpdateSourceGeneratedFileResponse
{
public UpdateType UpdateType { get; init; }
public string? Source { get; init; }
}

public enum UpdateType
{
Unchanged,
Deleted,
Modified
}
}
@@ -1,6 +1,7 @@
#nullable enable

using OmniSharp.Models.Metadata;
using OmniSharp.Models.v1.SourceGeneratedFile;
using System.Collections.Generic;

namespace OmniSharp.Models.V2.GotoDefinition
Expand All @@ -14,5 +15,6 @@ public record Definition
{
public Location Location { get; init; } = null!;
public MetadataSource? MetadataSource { get; init; }
public SourceGeneratedFileInfo? SourceGeneratedFileInfo { get; init; }
}
}
4 changes: 4 additions & 0 deletions src/OmniSharp.Abstractions/OmniSharpEndpoints.cs
Expand Up @@ -51,6 +51,10 @@ public static class OmniSharpEndpoints
public const string CompletionResolve = "/completion/resolve";
public const string CompletionAfterInsert = "/completion/afterinsert";

public const string SourceGeneratedFile = "/sourcegeneratedfile";
public const string UpdateSourceGeneratedFile = "/updatesourcegeneratedfile";
public const string SourceGeneratedFileClosed = "/sourcegeneratedfileclosed";

public static class V2
{
public const string GetCodeActions = "/v2/getcodeactions";
Expand Down
Expand Up @@ -3,6 +3,8 @@
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.FindSymbols;
using OmniSharp.Extensions;
using OmniSharp.Models.v1.SourceGeneratedFile;
using System.Diagnostics;
using System.Threading;
using System.Threading.Tasks;

Expand Down Expand Up @@ -42,5 +44,21 @@ internal static class GoToDefinitionHelpers

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
};
}
}
}
Expand Up @@ -50,7 +50,8 @@ public async Task<GotoDefinitionResponse> Handle(GotoDefinitionRequest request)
{
FileName = lineSpan.Path,
Line = lineSpan.StartLinePosition.Line,
Column = lineSpan.StartLinePosition.Character
Column = lineSpan.StartLinePosition.Character,
SourceGeneratedInfo = GoToDefinitionHelpers.GetSourceGeneratedFileInfo(_workspace, location)
};
}
else if (location.IsInMetadata && request.WantMetadata)
Expand Down
Expand Up @@ -49,7 +49,11 @@ public async Task<GotoDefinitionResponse> Handle(GotoDefinitionRequest request)
return new GotoDefinitionResponse()
{
Definitions = symbol.Locations
.Select(location => new Definition { Location = location.GetMappedLineSpan().GetLocationFromFileLinePositionSpan() })
.Select(location => new Definition
{
Location = location.GetMappedLineSpan().GetLocationFromFileLinePositionSpan(),
SourceGeneratedFileInfo = GoToDefinitionHelpers.GetSourceGeneratedFileInfo(_workspace, location)
})
.ToList()
};
}
Expand Down
@@ -0,0 +1,105 @@
#nullable enable

using Microsoft.CodeAnalysis;
using Microsoft.Extensions.Logging;
using OmniSharp.Mef;
using OmniSharp.Models.v1.SourceGeneratedFile;
using System.Collections.Generic;
using System.Composition;
using System.Threading;
using System.Threading.Tasks;

namespace OmniSharp.Roslyn.CSharp.Services.Navigation
{
[Shared]
[OmniSharpHandler(OmniSharpEndpoints.SourceGeneratedFile, LanguageNames.CSharp)]
[OmniSharpHandler(OmniSharpEndpoints.UpdateSourceGeneratedFile, LanguageNames.CSharp)]
[OmniSharpHandler(OmniSharpEndpoints.SourceGeneratedFileClosed, LanguageNames.CSharp)]
public class SourceGeneratedFileService :
IRequestHandler<SourceGeneratedFileRequest, SourceGeneratedFileResponse>,
IRequestHandler<UpdateSourceGeneratedFileRequest, UpdateSourceGeneratedFileResponse>,
IRequestHandler<SourceGeneratedFileClosedRequest, SourceGeneratedFileClosedResponse>
{
private readonly OmniSharpWorkspace _workspace;
private readonly ILogger _logger;
private readonly Dictionary<DocumentId, VersionStamp> _lastSentVerisons = new();
private readonly object _lock = new();

[ImportingConstructor]
public SourceGeneratedFileService(OmniSharpWorkspace workspace, ILoggerFactory loggerFactory)
{
_workspace = workspace;
_logger = loggerFactory.CreateLogger<SourceGeneratedFileService>();
}

public async Task<SourceGeneratedFileResponse> Handle(SourceGeneratedFileRequest request)
{
var documentId = GetId(request);

var document = await _workspace.CurrentSolution.GetSourceGeneratedDocumentAsync(documentId, CancellationToken.None);

if (document is null)
{
_logger.LogError("Document with ID {0}:{1} was not found or not a source generated file", request.ProjectGuid, request.DocumentGuid);
return new SourceGeneratedFileResponse();
}

var text = await document.GetTextAsync();

var documentVerison = await document.GetTextVersionAsync();
lock (_lock)
{
_lastSentVerisons[documentId] = documentVerison;
}

return new SourceGeneratedFileResponse
{
Source = text.ToString(),
SourceName = document.FilePath
};
}

public async Task<UpdateSourceGeneratedFileResponse> Handle(UpdateSourceGeneratedFileRequest request)
{
var documentId = GetId(request);
var document = await _workspace.CurrentSolution.GetSourceGeneratedDocumentAsync(documentId, CancellationToken.None);
if (document == null)
{
lock (_lock)
{
_ = _lastSentVerisons.Remove(documentId);
}
return new UpdateSourceGeneratedFileResponse() { UpdateType = UpdateType.Deleted };
}

var docVersion = await document.GetTextVersionAsync();
lock (_lock)
{
if (_lastSentVerisons.TryGetValue(documentId, out var lastVersion) && lastVersion == docVersion)
{
return new UpdateSourceGeneratedFileResponse() { UpdateType = UpdateType.Unchanged };
}

_lastSentVerisons[documentId] = docVersion;
}

return new UpdateSourceGeneratedFileResponse()
{
UpdateType = UpdateType.Modified,
Source = (await document.GetTextAsync()).ToString()
};
}

public Task<SourceGeneratedFileClosedResponse> Handle(SourceGeneratedFileClosedRequest request)
{
lock (_lock)
{
_ = _lastSentVerisons.Remove(GetId(request));
}

return SourceGeneratedFileClosedResponse.Instance;
}

private DocumentId GetId(SourceGeneratedFileInfo info) => DocumentId.CreateFromSerialized(ProjectId.CreateFromSerialized(info.ProjectGuid), info.DocumentGuid);
}
}
@@ -1,9 +1,12 @@
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Diagnostics;
using OmniSharp.Mef;
using OmniSharp.Models.Metadata;
using OmniSharp.Models.v1.SourceGeneratedFile;
using OmniSharp.Roslyn.CSharp.Services.Navigation;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using System.Threading.Tasks;
using TestUtility;
Expand Down Expand Up @@ -506,6 +509,62 @@ public async Task ReturnsNoResultsButDoesNotThrowForNamespaces()
Assert.Empty(GetInfo(response));
}

[Fact]
public async Task ReturnsResultsForSourceGenerators()
{
const string Source = @"
public class {|generatedClassName:Generated|}
{
public int {|propertyName:Property|} { get; set; }
}
";
const string FileName = "real.cs";
TestFile generatedTestFile = new("GeneratedFile.cs", Source);
var testFile = new TestFile(FileName, @"
class C
{
public void M(Generated g)
{
_ = g.P$$roperty;
}
}
");

TestHelpers.AddProjectToWorkspace(SharedOmniSharpTestHost.Workspace,
"project.csproj",
new[] { "netcoreapp3.1" },
new[] { testFile },
analyzerRefs: ImmutableArray.Create<AnalyzerReference>(new TestGeneratorReference(
context => context.AddSource("GeneratedFile", generatedTestFile.Content.Code))));

var point = testFile.Content.GetPointFromPosition();

var gotoDefRequest = CreateRequest(FileName, point.Line, point.Offset, wantMetadata: true);
var gotoDefHandler = GetRequestHandler(SharedOmniSharpTestHost);
var response = await gotoDefHandler.Handle(gotoDefRequest);
var info = GetInfo(response).Single();

Assert.NotNull(info.SourceGeneratorInfo);

var expectedSpan = generatedTestFile.Content.GetSpans("propertyName").Single();
var expectedRange = generatedTestFile.Content.GetRangeFromSpan(expectedSpan);

Assert.Equal(expectedRange.Start.Line, info.Line);
Assert.Equal(expectedRange.Start.Offset, info.Column);

var sourceGeneratedFileHandler = SharedOmniSharpTestHost.GetRequestHandler<SourceGeneratedFileService>(OmniSharpEndpoints.SourceGeneratedFile);
var sourceGeneratedRequest = new SourceGeneratedFileRequest
{
DocumentGuid = info.SourceGeneratorInfo.DocumentGuid,
ProjectGuid = info.SourceGeneratorInfo.ProjectGuid
};

var sourceGeneratedFileResponse = await sourceGeneratedFileHandler.Handle(sourceGeneratedRequest);
Assert.NotNull(sourceGeneratedFileResponse);
Assert.Equal(generatedTestFile.Content.Code, sourceGeneratedFileResponse.Source);
Assert.Equal(@"OmniSharp.Roslyn.CSharp.Tests\OmniSharp.Roslyn.CSharp.Tests.TestSourceGenerator\GeneratedFile.cs", sourceGeneratedFileResponse.SourceName.Replace("/", @"\"));
}

protected async Task TestGoToSourceAsync(params TestFile[] testFiles)
{
var response = await GetResponseAsync(testFiles, wantMetadata: false);
Expand Down Expand Up @@ -581,6 +640,6 @@ protected async Task<TGotoDefinitionResponse> GetResponseAsync(TestFile[] testFi

protected abstract TGotoDefinitionRequest CreateRequest(string fileName, int line, int column, bool wantMetadata, int timeout = 60000);
protected abstract MetadataSource GetMetadataSource(TGotoDefinitionResponse response);
protected abstract IEnumerable<(int Line, int Column, string FileName)> GetInfo(TGotoDefinitionResponse response);
protected abstract IEnumerable<(int Line, int Column, string FileName, SourceGeneratedFileInfo SourceGeneratorInfo)> GetInfo(TGotoDefinitionResponse response);
}
}

0 comments on commit 65bb630

Please sign in to comment.