Skip to content

Commit

Permalink
Merge pull request #74626 from dibarbet/vscode_source_link
Browse files Browse the repository at this point in the history
Add support in DevKit for source link go to definition
  • Loading branch information
dibarbet authored Aug 14, 2024
2 parents 9d57657 + c1e0b26 commit dee1a4f
Show file tree
Hide file tree
Showing 8 changed files with 234 additions and 79 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,8 @@
<InternalsVisibleTo Include="Microsoft.VisualStudio.LanguageServices.CodeLens" />
<InternalsVisibleTo Include="Microsoft.VisualStudio.LanguageServices.CSharp" />
<InternalsVisibleTo Include="Microsoft.VisualStudio.LanguageServices.CSharp.UnitTests" />
<!-- This assembly is shipped and versioned alongside Roslyn in the C# extension, fine to give it full IVTs -->
<InternalsVisibleTo Include="Microsoft.VisualStudio.LanguageServices.DevKit" />
<InternalsVisibleTo Include="Microsoft.VisualStudio.LanguageServices.ExternalDependencyServices" WorkItem="https://github.com/dotnet/roslyn/issues/35085" />
<InternalsVisibleTo Include="Microsoft.VisualStudio.LanguageServices.Implementation" />
<InternalsVisibleTo Include="Microsoft.VisualStudio.LanguageServices.LiveShare" />
Expand Down Expand Up @@ -113,11 +115,6 @@
<RestrictedInternalsVisibleTo Include="Microsoft.VisualStudio.LanguageServerClient.Razor.Test" Partner="Razor" Key="$(RazorKey)" />
<RestrictedInternalsVisibleTo Include="Microsoft.VisualStudio.LanguageServices.Razor" Partner="Razor" Key="$(RazorKey)" />
<RestrictedInternalsVisibleTo Include="Microsoft.VisualStudio.LanguageServices.Razor.Test" Partner="Razor" Key="$(RazorKey)" />
<!--
Only allow C# DevKit to use types from Contracts namespace. The contracts should not introduce breaking changes between versions,
because the versions of C# DevKit and C# Extension might not be aligned.
-->
<RestrictedInternalsVisibleTo Include="Microsoft.VisualStudio.LanguageServices.DevKit" Namespace="Microsoft.CodeAnalysis.Contracts" />
<RestrictedInternalsVisibleTo Include="Microsoft.VisualStudio.TestWindow.Core" Partner="UnitTesting" Key="$(UnitTestingKey)" />
</ItemGroup>
<ItemGroup>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,8 @@ internal class Descriptors
{ BrokeredServiceDescriptors.HotReloadOptionService.Moniker, new ServiceRegistration(ServiceAudience.Local, null, allowGuestClients: false) },
{ BrokeredServiceDescriptors.HotReloadLoggerService.Moniker, new ServiceRegistration(ServiceAudience.Local, null, allowGuestClients: false) },
{ BrokeredServiceDescriptors.MauiLaunchCustomizerService.Moniker, new ServiceRegistration(ServiceAudience.Local, null, allowGuestClients: false) },
{ BrokeredServiceDescriptors.DebuggerSymbolLocatorService.Moniker, new ServiceRegistration(ServiceAudience.Local, null, allowGuestClients: false) },
{ BrokeredServiceDescriptors.DebuggerSourceLinkService.Moniker, new ServiceRegistration(ServiceAudience.Local, null, allowGuestClients: false) },
}.ToImmutableDictionary();

public static ServiceJsonRpcDescriptor CreateDescriptor(ServiceMoniker serviceMoniker) => new(
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.

using System.IO;
using System.Reflection.PortableExecutable;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.PdbSourceDocument;
using Microsoft.CodeAnalysis.PooledObjects;
using Microsoft.VisualStudio.Debugger.Contracts.SourceLink;
using Microsoft.VisualStudio.Debugger.Contracts.SymbolLocator;

namespace Microsoft.VisualStudio.LanguageServices.PdbSourceDocument;

internal abstract class AbstractSourceLinkService : ISourceLinkService
{
public async Task<PdbFilePathResult?> GetPdbFilePathAsync(string dllPath, PEReader peReader, bool useDefaultSymbolServers, CancellationToken cancellationToken)
{
var hasCodeViewEntry = false;
uint timeStamp = 0;
CodeViewDebugDirectoryData codeViewEntry = default;
using var _ = ArrayBuilder<PdbChecksum>.GetInstance(out var checksums);
foreach (var entry in peReader.ReadDebugDirectory())
{
if (entry.Type == DebugDirectoryEntryType.PdbChecksum)
{
var checksum = peReader.ReadPdbChecksumDebugDirectoryData(entry);
checksums.Add(new PdbChecksum(checksum.AlgorithmName, checksum.Checksum));
}
else if (entry.Type == DebugDirectoryEntryType.CodeView && entry.IsPortableCodeView)
{
hasCodeViewEntry = true;
timeStamp = entry.Stamp;
codeViewEntry = peReader.ReadCodeViewDebugDirectoryData(entry);
}
}

if (!hasCodeViewEntry)
return null;

var pdbInfo = new SymbolLocatorPdbInfo(
Path.GetFileName(codeViewEntry.Path),
codeViewEntry.Guid,
(uint)codeViewEntry.Age,
timeStamp,
checksums.ToImmutable(),
dllPath,
codeViewEntry.Path);

var flags = useDefaultSymbolServers
? SymbolLocatorSearchFlags.ForceNuGetSymbolServer | SymbolLocatorSearchFlags.ForceMsftSymbolServer
: SymbolLocatorSearchFlags.None;
var result = await LocateSymbolFileAsync(pdbInfo, flags, cancellationToken).ConfigureAwait(false);
if (result is null)
{
Logger?.Log($"{nameof(LocateSymbolFileAsync)} returned null");
return null;
}

if (result.Value.Found && result.Value.SymbolFilePath is not null)
{
return new PdbFilePathResult(result.Value.SymbolFilePath);
}
else if (Logger is not null)
{
// We log specific info from the debugger if there is a failure, but the caller will log general failure
// information otherwise
Logger.Log(result.Value.Status);
Logger.Log(result.Value.Log);
}

return null;
}

public async Task<SourceFilePathResult?> GetSourceFilePathAsync(string url, string relativePath, CancellationToken cancellationToken)
{
var result = await GetSourceLinkAsync(url, relativePath, cancellationToken).ConfigureAwait(false);
if (result is null)
{
Logger?.Log($"{nameof(GetSourceLinkAsync)} returned null");
return null;
}

if (result.Value.Status == SourceLinkResultStatus.Succeeded && result.Value.Path is not null)
{
return new SourceFilePathResult(result.Value.Path);
}
else if (Logger is not null && result.Value.Log is not null)
{
// We log specific info from the debugger if there is a failure, but the caller will log general failure
// information otherwise.
Logger.Log(result.Value.Log);
}

return null;
}

protected abstract Task<SymbolLocatorResult?> LocateSymbolFileAsync(SymbolLocatorPdbInfo pdbInfo, SymbolLocatorSearchFlags flags, CancellationToken cancellationToken);

protected abstract Task<SourceLinkResult?> GetSourceLinkAsync(string url, string relativePath, CancellationToken cancellationToken);

protected abstract IPdbSourceDocumentLogger? Logger { get; }
}
76 changes: 7 additions & 69 deletions src/VisualStudio/Core/Def/PdbSourceDocument/SourceLinkService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,20 +4,17 @@

using System;
using System.Composition;
using System.IO;
using System.Reflection.PortableExecutable;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.Host.Mef;
using Microsoft.CodeAnalysis.PdbSourceDocument;
using Microsoft.CodeAnalysis.PooledObjects;
using Microsoft.VisualStudio.Debugger.Contracts.SourceLink;
using Microsoft.VisualStudio.Debugger.Contracts.SymbolLocator;

namespace Microsoft.VisualStudio.LanguageServices.PdbSourceDocument;

[Export(typeof(ISourceLinkService)), Shared]
internal class SourceLinkService : ISourceLinkService
internal class SourceLinkService : AbstractSourceLinkService
{
private readonly IDebuggerSymbolLocatorService _debuggerSymbolLocatorService;
private readonly IDebuggerSourceLinkService _debuggerSourceLinkService;
Expand All @@ -35,74 +32,15 @@ public SourceLinkService(
_logger = logger;
}

public async Task<PdbFilePathResult?> GetPdbFilePathAsync(string dllPath, PEReader peReader, bool useDefaultSymbolServers, CancellationToken cancellationToken)
protected override async Task<SymbolLocatorResult?> LocateSymbolFileAsync(SymbolLocatorPdbInfo pdbInfo, SymbolLocatorSearchFlags flags, CancellationToken cancellationToken)
{
var hasCodeViewEntry = false;
uint timeStamp = 0;
CodeViewDebugDirectoryData codeViewEntry = default;
using var _ = ArrayBuilder<PdbChecksum>.GetInstance(out var checksums);
foreach (var entry in peReader.ReadDebugDirectory())
{
if (entry.Type == DebugDirectoryEntryType.PdbChecksum)
{
var checksum = peReader.ReadPdbChecksumDebugDirectoryData(entry);
checksums.Add(new PdbChecksum(checksum.AlgorithmName, checksum.Checksum));
}
else if (entry.Type == DebugDirectoryEntryType.CodeView && entry.IsPortableCodeView)
{
hasCodeViewEntry = true;
timeStamp = entry.Stamp;
codeViewEntry = peReader.ReadCodeViewDebugDirectoryData(entry);
}
}

if (!hasCodeViewEntry)
return null;

var pdbInfo = new SymbolLocatorPdbInfo(
Path.GetFileName(codeViewEntry.Path),
codeViewEntry.Guid,
(uint)codeViewEntry.Age,
timeStamp,
checksums.ToImmutable(),
dllPath,
codeViewEntry.Path);

var flags = useDefaultSymbolServers
? SymbolLocatorSearchFlags.ForceNuGetSymbolServer | SymbolLocatorSearchFlags.ForceMsftSymbolServer
: SymbolLocatorSearchFlags.None;
var result = await _debuggerSymbolLocatorService.LocateSymbolFileAsync(pdbInfo, flags, progress: null, cancellationToken).ConfigureAwait(false);

if (result.Found && result.SymbolFilePath is not null)
{
return new PdbFilePathResult(result.SymbolFilePath);
}
else if (_logger is not null)
{
// We log specific info from the debugger if there is a failure, but the caller will log general failure
// information otherwise
_logger.Log(result.Status);
_logger.Log(result.Log);
}

return null;
return await _debuggerSymbolLocatorService.LocateSymbolFileAsync(pdbInfo, flags, progress: null, cancellationToken).ConfigureAwait(false);
}

public async Task<SourceFilePathResult?> GetSourceFilePathAsync(string url, string relativePath, CancellationToken cancellationToken)
protected override async Task<SourceLinkResult?> GetSourceLinkAsync(string url, string relativePath, CancellationToken cancellationToken)
{
var result = await _debuggerSourceLinkService.GetSourceLinkAsync(url, relativePath, allowInteractiveLogin: false, cancellationToken).ConfigureAwait(false);

if (result.Status == SourceLinkResultStatus.Succeeded && result.Path is not null)
{
return new SourceFilePathResult(result.Path);
}
else if (_logger is not null && result.Log is not null)
{
// We log specific info from the debugger if there is a failure, but the caller will log general failure
// information otherwise.
_logger.Log(result.Log);
}

return null;
return await _debuggerSourceLinkService.GetSourceLinkAsync(url, relativePath, allowInteractiveLogin: false, cancellationToken).ConfigureAwait(false);
}

protected override IPdbSourceDocumentLogger? Logger => _logger;
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
<Compile Include="..\..\..\EditorFeatures\Core\EditAndContinue\Contracts\ContractWrappers.cs" Link="EditAndContinue\ContractWrappers.cs" />
<Compile Include="..\..\..\EditorFeatures\Core\EditAndContinue\Contracts\ManagedHotReloadServiceBridge.cs" Link="EditAndContinue\ManagedHotReloadServiceBridge.cs" />
<Compile Include="..\..\..\VisualStudio\Core\Def\Telemetry\Shared\*.cs" LinkBase="Logging" />
<Compile Include="..\..\..\VisualStudio\Core\Def\PdbSourceDocument\AbstractSourceLinkService.cs" />
</ItemGroup>

<ItemGroup>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.

using System;
using System.Composition;
using Microsoft.CodeAnalysis.Host.Mef;
using Microsoft.CodeAnalysis.PdbSourceDocument;
using Microsoft.Extensions.Logging;

namespace Microsoft.CodeAnalysis.LanguageServer.Services.SourceLink;

[Export(typeof(IPdbSourceDocumentLogger)), Shared]
[method: ImportingConstructor]
[method: Obsolete(MefConstruction.ImportingConstructorMessage, error: true)]
internal class VSCodePdbSourceDocumentLogger(ILoggerFactory loggerFactory) : IPdbSourceDocumentLogger
{
private readonly ILogger _logger = loggerFactory.CreateLogger("SourceLink");

public void Clear()
{
// Do nothing, we just leave all the logs up.
return;
}

public void Log(string message)
{
_logger.LogTrace(message);
}
}
75 changes: 75 additions & 0 deletions src/VisualStudio/DevKit/Impl/SourceLink/VSCodeSourceLinkService.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.

using System;
using System.Composition;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.BrokeredServices;
using Microsoft.CodeAnalysis.Host.Mef;
using Microsoft.CodeAnalysis.PdbSourceDocument;
using Microsoft.ServiceHub.Framework;
using Microsoft.VisualStudio.Debugger.Contracts.SourceLink;
using Microsoft.VisualStudio.Debugger.Contracts.SymbolLocator;
using Microsoft.VisualStudio.LanguageServices.PdbSourceDocument;

namespace Microsoft.CodeAnalysis.LanguageServer.Services.SourceLink;

[Export(typeof(ISourceLinkService)), Shared]
[method: ImportingConstructor]
[method: Obsolete(MefConstruction.ImportingConstructorMessage, error: true)]
internal class VSCodeSourceLinkService(IServiceBrokerProvider serviceBrokerProvider, IPdbSourceDocumentLogger logger) : AbstractSourceLinkService
{
private readonly IServiceBroker _serviceBroker = serviceBrokerProvider.ServiceBroker;

protected override async Task<SymbolLocatorResult?> LocateSymbolFileAsync(SymbolLocatorPdbInfo pdbInfo, SymbolLocatorSearchFlags flags, CancellationToken cancellationToken)
{
var proxy = await _serviceBroker.GetProxyAsync<IDebuggerSymbolLocatorService>(BrokeredServiceDescriptors.DebuggerSymbolLocatorService, cancellationToken).ConfigureAwait(false);
using ((IDisposable?)proxy)
{
if (proxy is null)
{
return null;
}

try
{
var result = await proxy.LocateSymbolFileAsync(pdbInfo, flags, progress: null, cancellationToken).ConfigureAwait(false);
return result;
}
catch (StreamJsonRpc.RemoteMethodNotFoundException)
{
// Older versions of DevKit use an invalid service descriptor - calling it will throw a RemoteMethodNotFoundException.
// Just return null as there isn't a valid service available.
return null;
}
}
}

protected override async Task<SourceLinkResult?> GetSourceLinkAsync(string url, string relativePath, CancellationToken cancellationToken)
{
var proxy = await _serviceBroker.GetProxyAsync<IDebuggerSourceLinkService>(BrokeredServiceDescriptors.DebuggerSourceLinkService, cancellationToken).ConfigureAwait(false);
using ((IDisposable?)proxy)
{
if (proxy is null)
{
return null;
}

try
{
var result = await proxy.GetSourceLinkAsync(url, relativePath, allowInteractiveLogin: false, cancellationToken).ConfigureAwait(false);
return result;
}
catch (StreamJsonRpc.RemoteMethodNotFoundException)
{
// Older versions of DevKit use an invalid service descriptor - calling it will throw a RemoteMethodNotFoundException.
// Just return null as there isn't a valid service available.
return null;
}
}
}

protected override IPdbSourceDocumentLogger? Logger => logger;
}
18 changes: 13 additions & 5 deletions src/Workspaces/Remote/Core/BrokeredServiceDescriptors.cs
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,10 @@ protected override JsonRpcConnection CreateConnection(JsonRpc jsonRpc)
public static readonly ServiceRpcDescriptor GenericHotReloadAgentManagerService = CreateDebuggerServiceDescriptor("GenericHotReloadAgentManagerService", new Version(0, 1));
public static readonly ServiceRpcDescriptor HotReloadOptionService = CreateDebuggerClientServiceDescriptor("HotReloadOptionService", new Version(0, 1));
public static readonly ServiceRpcDescriptor MauiLaunchCustomizerService = CreateMauiServiceDescriptor("MauiLaunchCustomizerService", new Version(0, 1));
public static readonly ServiceRpcDescriptor DebuggerSymbolLocatorService =
CreateDebuggerServiceDescriptor("SymbolLocatorService", new Version(0, 1), new MultiplexingStream.Options { ProtocolMajorVersion = 3 });
public static readonly ServiceRpcDescriptor DebuggerSourceLinkService =
CreateDebuggerServiceDescriptor("SourceLinkService", new Version(0, 1), new MultiplexingStream.Options { ProtocolMajorVersion = 3 });

public static ServiceMoniker CreateMoniker(string namespaceName, string componentName, string serviceName, Version? version)
=> new(namespaceName + "." + componentName + "." + serviceName, version);
Expand All @@ -97,8 +101,8 @@ public static ServiceJsonRpcDescriptor CreateServerServiceDescriptor(string serv
/// <summary>
/// Descriptor for services proferred by the debugger server (implemented in C#).
/// </summary>
public static ServiceJsonRpcDescriptor CreateDebuggerServiceDescriptor(string serviceName, Version? version = null)
=> CreateDescriptor(CreateMoniker(VisualStudioComponentNamespace, DebuggerComponentName, serviceName, version));
public static ServiceJsonRpcDescriptor CreateDebuggerServiceDescriptor(string serviceName, Version? version = null, MultiplexingStream.Options? streamOptions = null)
=> CreateDescriptor(CreateMoniker(VisualStudioComponentNamespace, DebuggerComponentName, serviceName, version), streamOptions);

/// <summary>
/// Descriptor for services proferred by the debugger server (implemented in TypeScript).
Expand All @@ -116,7 +120,11 @@ public static ServiceJsonRpcDescriptor CreateMauiServiceDescriptor(string servic
new MultiplexingStream.Options { ProtocolMajorVersion = 3 })
.WithExceptionStrategy(ExceptionProcessing.ISerializable);

private static ServiceJsonRpcDescriptor CreateDescriptor(ServiceMoniker moniker)
=> new ServiceJsonRpcDescriptor(moniker, Formatters.MessagePack, MessageDelimiters.BigEndianInt32LengthHeader)
.WithExceptionStrategy(ExceptionProcessing.ISerializable);
private static ServiceJsonRpcDescriptor CreateDescriptor(ServiceMoniker moniker, MultiplexingStream.Options? streamOptions = null)
{
var descriptor = streamOptions is not null
? new ServiceJsonRpcDescriptor(moniker, clientInterface: null, Formatters.MessagePack, MessageDelimiters.BigEndianInt32LengthHeader, streamOptions)
: new ServiceJsonRpcDescriptor(moniker, Formatters.MessagePack, MessageDelimiters.BigEndianInt32LengthHeader);
return descriptor.WithExceptionStrategy(ExceptionProcessing.ISerializable);
}
}

0 comments on commit dee1a4f

Please sign in to comment.