File: Workspace\VisualStudioSymbolNavigationService.cs
Web Access
Project: src\src\VisualStudio\Core\Def\Microsoft.VisualStudio.LanguageServices_pxr0p0dn_wpftmp.csproj (Microsoft.VisualStudio.LanguageServices)
// 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.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Editor.Shared.Extensions;
using Microsoft.CodeAnalysis.Editor.Shared.Utilities;
using Microsoft.CodeAnalysis.FindUsages;
using Microsoft.CodeAnalysis.Host.Mef;
using Microsoft.CodeAnalysis.MetadataAsSource;
using Microsoft.CodeAnalysis.Navigation;
using Microsoft.CodeAnalysis.Options;
using Microsoft.CodeAnalysis.PooledObjects;
using Microsoft.CodeAnalysis.Shared.Extensions;
using Microsoft.CodeAnalysis.Text;
using Microsoft.VisualStudio.Editor;
using Microsoft.VisualStudio.LanguageServices.Implementation.Library;
using Microsoft.VisualStudio.LanguageServices.Implementation.ProjectSystem;
using Microsoft.VisualStudio.Shell;
using Microsoft.VisualStudio.Shell.Interop;
using Microsoft.VisualStudio.TextManager.Interop;
using Roslyn.Utilities;
 
namespace Microsoft.VisualStudio.LanguageServices.Implementation;
 
[ExportWorkspaceService(typeof(ISymbolNavigationService), ServiceLayer.Host), Shared]
[method: ImportingConstructor]
[method: Obsolete(MefConstruction.ImportingConstructorMessage, error: true)]
internal sealed partial class VisualStudioSymbolNavigationService(
    SVsServiceProvider serviceProvider,
    IGlobalOptionService globalOptions,
    IThreadingContext threadingContext,
    IVsEditorAdaptersFactoryService editorAdaptersFactory,
    IMetadataAsSourceFileService metadataAsSourceFileService,
    VisualStudioWorkspace workspace) : ISymbolNavigationService
{
    private readonly IServiceProvider _serviceProvider = serviceProvider;
    private readonly IGlobalOptionService _globalOptions = globalOptions;
    private readonly IThreadingContext _threadingContext = threadingContext;
    private readonly IVsEditorAdaptersFactoryService _editorAdaptersFactory = editorAdaptersFactory;
    private readonly IMetadataAsSourceFileService _metadataAsSourceFileService = metadataAsSourceFileService;
    private readonly VisualStudioWorkspace _workspace = workspace;
 
    public async Task<INavigableLocation?> GetNavigableLocationAsync(
        ISymbol symbol, Project project, CancellationToken cancellationToken)
    {
        if (project == null || symbol == null)
            return null;
 
        var solution = project.Solution;
        symbol = symbol.OriginalDefinition;
 
        // Prefer visible source locations if possible.
        var sourceLocations = symbol.Locations.Where(loc => loc.IsInSource);
        var visibleSourceLocations = sourceLocations.Where(loc => loc.IsVisibleSourceLocation());
        var sourceLocation = visibleSourceLocations.FirstOrDefault() ?? sourceLocations.FirstOrDefault();
 
        if (sourceLocation != null)
        {
            var targetDocument = solution.GetDocument(sourceLocation.SourceTree);
            if (targetDocument != null)
            {
                var navigationService = solution.Services.GetRequiredService<IDocumentNavigationService>();
                return await navigationService.GetLocationForPositionAsync(
                    solution.Workspace, targetDocument.Id, sourceLocation.SourceSpan.Start, cancellationToken).ConfigureAwait(false);
            }
        }
 
        // We don't have a source document, so show the Metadata as Source view in a preview tab.
 
        if (!_metadataAsSourceFileService.IsNavigableMetadataSymbol(symbol))
        {
            return null;
        }
 
        // See if there's another .Net language service that can handle navigating to this metadata symbol (for example, F#).
        var docCommentId = symbol.GetDocumentationCommentId();
        var assemblyName = symbol.ContainingAssembly.Identity.Name;
        if (docCommentId != null && assemblyName != null)
        {
            foreach (var lazyService in solution.Services.ExportProvider.GetExports<ICrossLanguageSymbolNavigationService>())
            {
                var crossLanguageService = lazyService.Value;
                var crossLanguageLocation = await crossLanguageService.TryGetNavigableLocationAsync(
                    assemblyName, docCommentId, cancellationToken).ConfigureAwait(false);
                if (crossLanguageLocation != null)
                    return crossLanguageLocation;
            }
        }
 
        // Should we prefer navigating to the Object Browser over metadata-as-source?
        if (_globalOptions.GetOption(VisualStudioNavigationOptionsStorage.NavigateToObjectBrowser, project.Language))
        {
            var libraryService = project.Services.GetService<ILibraryService>();
            if (libraryService == null)
            {
                return null;
            }
 
            var compilation = await project.GetCompilationAsync(cancellationToken).ConfigureAwait(false);
            var navInfo = libraryService.NavInfoFactory.CreateForSymbol(symbol, project, compilation);
            navInfo ??= libraryService.NavInfoFactory.CreateForProject(project);
 
            if (navInfo != null)
            {
                var navigationTool = _serviceProvider.GetServiceOnMainThread<SVsObjBrowser, IVsNavigationTool>();
                return new NavigableLocation(async (options, cancellationToken) =>
                {
                    await _threadingContext.JoinableTaskFactory.SwitchToMainThreadAsync(cancellationToken);
                    return navigationTool.NavigateToNavInfo(navInfo) == VSConstants.S_OK;
                });
            }
 
            // Note: we'll fallback to Metadata-As-Source if we fail to get IVsNavInfo, but that should never happen.
        }
 
        // Generate new source or retrieve existing source for the symbol in question
        return await GetNavigableLocationForMetadataAsync(project, symbol, cancellationToken).ConfigureAwait(false);
    }
 
    private async Task<INavigableLocation?> GetNavigableLocationForMetadataAsync(
        Project project, ISymbol symbol, CancellationToken cancellationToken)
    {
        var masOptions = _globalOptions.GetMetadataAsSourceOptions();
 
        var result = await _metadataAsSourceFileService.GetGeneratedFileAsync(_workspace, project, symbol, signaturesOnly: false, options: masOptions, cancellationToken: cancellationToken).ConfigureAwait(false);
 
        return new NavigableLocation(async (options, cancellationToken) =>
        {
            await _threadingContext.JoinableTaskFactory.SwitchToMainThreadAsync(cancellationToken);
 
            var vsRunningDocumentTable4 = _serviceProvider.GetServiceOnMainThread<SVsRunningDocumentTable, IVsRunningDocumentTable4>();
            var fileAlreadyOpen = vsRunningDocumentTable4.IsMonikerValid(result.FilePath);
 
            var openDocumentService = _serviceProvider.GetServiceOnMainThread<SVsUIShellOpenDocument, IVsUIShellOpenDocument>();
            openDocumentService.OpenDocumentViaProject(result.FilePath, VSConstants.LOGVIEWID.TextView_guid, out _, out _, out _, out var windowFrame);
 
            var documentCookie = vsRunningDocumentTable4.GetDocumentCookie(result.FilePath);
 
            var vsTextBuffer = (IVsTextBuffer)vsRunningDocumentTable4.GetDocumentData(documentCookie);
 
            var textBuffer = _editorAdaptersFactory.GetDataBuffer(vsTextBuffer);
 
            if (!fileAlreadyOpen)
            {
                ErrorHandler.ThrowOnFailure(windowFrame.SetProperty((int)__VSFPROPID5.VSFPROPID_IsProvisional, true));
                ErrorHandler.ThrowOnFailure(windowFrame.SetProperty((int)__VSFPROPID5.VSFPROPID_OverrideCaption, result.DocumentTitle));
                ErrorHandler.ThrowOnFailure(windowFrame.SetProperty((int)__VSFPROPID5.VSFPROPID_OverrideToolTip, result.DocumentTooltip));
            }
 
            windowFrame.Show();
 
            var openedDocument = textBuffer?.AsTextContainer().GetRelatedDocuments().FirstOrDefault();
            if (openedDocument != null)
            {
                var editorWorkspace = openedDocument.Project.Solution.Workspace;
                var navigationService = editorWorkspace.Services.GetRequiredService<IDocumentNavigationService>();
 
                await navigationService.TryNavigateToPositionAsync(
                    _threadingContext,
                    editorWorkspace,
                    openedDocument.Id,
                    result.IdentifierLocation.SourceSpan.Start,
                    options with { PreferProvisionalTab = true },
                    cancellationToken).ConfigureAwait(false);
            }
 
            return true;
        });
    }
 
    public async Task<bool> TrySymbolNavigationNotifyAsync(ISymbol symbol, Project project, CancellationToken cancellationToken)
    {
        await _threadingContext.JoinableTaskFactory.SwitchToMainThreadAsync(cancellationToken);
 
        var definitionItem = symbol.ToNonClassifiedDefinitionItem(project.Solution, includeHiddenLocations: true);
        definitionItem.Properties.TryGetValue(DefinitionItem.RQNameKey1, out var rqName);
 
        var result = await TryGetNavigationAPIRequiredArgumentsAsync(definitionItem, rqName, cancellationToken).ConfigureAwait(true);
        if (result is not var (hierarchy, itemID, navigationNotify))
            return false;
 
        var returnCode = navigationNotify.OnBeforeNavigateToSymbol(
            hierarchy,
            itemID,
            rqName,
            out var navigationHandled);
 
        return returnCode == VSConstants.S_OK && navigationHandled == 1;
    }
 
    public async Task<(string filePath, LinePosition linePosition)?> GetExternalNavigationSymbolLocationAsync(
        DefinitionItem definitionItem, CancellationToken cancellationToken)
    {
        definitionItem.Properties.TryGetValue(DefinitionItem.RQNameKey1, out var rqName1);
        definitionItem.Properties.TryGetValue(DefinitionItem.RQNameKey2, out var rqName2);
 
        return await GetExternalNavigationLocationForSpecificSymbolAsync(definitionItem, rqName1, cancellationToken).ConfigureAwait(false) ??
               await GetExternalNavigationLocationForSpecificSymbolAsync(definitionItem, rqName2, cancellationToken).ConfigureAwait(false);
    }
 
    public async Task<(string filePath, LinePosition linePosition)?> GetExternalNavigationLocationForSpecificSymbolAsync(
        DefinitionItem definitionItem, string? rqName, CancellationToken cancellationToken)
    {
        if (rqName == null)
            return null;
 
        var values = await TryGetNavigationAPIRequiredArgumentsAsync(
            definitionItem, rqName, cancellationToken).ConfigureAwait(false);
        if (values is not var (hierarchy, itemID, navigationNotify))
            return null;
 
        var navigateToTextSpan = new Microsoft.VisualStudio.TextManager.Interop.TextSpan[1];
 
        await _threadingContext.JoinableTaskFactory.SwitchToMainThreadAsync(cancellationToken);
 
        var queryNavigateStatusCode = navigationNotify.QueryNavigateToSymbol(
            hierarchy,
            itemID,
            rqName,
            out var navigateToHierarchy,
            out var navigateToItem,
            navigateToTextSpan,
            out var wouldNavigate);
 
        if (queryNavigateStatusCode != VSConstants.S_OK || wouldNavigate != 1)
            return null;
 
        navigateToHierarchy.GetCanonicalName(navigateToItem, out var filePath);
        var lineNumber = navigateToTextSpan[0].iStartLine;
        var charOffset = navigateToTextSpan[0].iStartIndex;
 
        return (filePath, new LinePosition(lineNumber, charOffset));
    }
 
    private async Task<(IVsHierarchy hierarchy, uint itemId, IVsSymbolicNavigationNotify navigationNotify)?> TryGetNavigationAPIRequiredArgumentsAsync(
        DefinitionItem definitionItem,
        string? rqName,
        CancellationToken cancellationToken)
    {
        if (rqName == null)
            return null;
 
        var sourceLocations = definitionItem.SourceSpans;
        if (!sourceLocations.Any())
            return null;
 
        using var _ = ArrayBuilder<Document>.GetInstance(out var documentsBuilder);
        foreach (var loc in sourceLocations)
            documentsBuilder.AddIfNotNull(loc.Document);
 
        var documents = documentsBuilder.ToImmutable();
 
        // We can only pass one itemid to IVsSymbolicNavigationNotify, so prefer itemids from
        // documents we consider to be "generated" to give external language services the best
        // chance of participating.
 
        var generatedDocuments = documents.WhereAsArray(d => d.IsGeneratedCode(cancellationToken));
 
        var documentToUse = generatedDocuments.FirstOrDefault() ?? documents.First();
 
        await _threadingContext.JoinableTaskFactory.SwitchToMainThreadAsync(cancellationToken);
 
        if (!TryGetVsHierarchyAndItemId(documentToUse, out var hierarchy, out var itemID))
            return null;
 
        var navigationNotify = hierarchy as IVsSymbolicNavigationNotify;
        if (navigationNotify == null)
            return null;
 
        return (hierarchy, itemID, navigationNotify);
    }
 
    private bool TryGetVsHierarchyAndItemId(Document document, [NotNullWhen(true)] out IVsHierarchy? hierarchy, out uint itemID)
    {
        _threadingContext.ThrowIfNotOnUIThread();
 
        if (document.Project.Solution.Workspace is VisualStudioWorkspace visualStudioWorkspace
            && document.FilePath is object)
        {
            hierarchy = visualStudioWorkspace.GetHierarchy(document.Project.Id);
            if (hierarchy is object)
            {
                itemID = hierarchy.TryGetItemId(document.FilePath);
                if (itemID != VSConstants.VSITEMID_NIL)
                {
                    return true;
                }
            }
        }
 
        hierarchy = null;
        itemID = (uint)VSConstants.VSITEMID.Nil;
        return false;
    }
}