File: StackTraceExplorer\StackFrameViewModel.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.Collections.Generic;
using System.Collections.Immutable;
using System.Diagnostics;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.Windows.Documents;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Classification;
using Microsoft.CodeAnalysis.Editor.Shared.Utilities;
using Microsoft.CodeAnalysis.EmbeddedLanguages.Common;
using Microsoft.CodeAnalysis.EmbeddedLanguages.StackFrame;
using Microsoft.CodeAnalysis.ErrorReporting;
using Microsoft.CodeAnalysis.FindUsages;
using Microsoft.CodeAnalysis.Navigation;
using Microsoft.CodeAnalysis.PooledObjects;
using Microsoft.CodeAnalysis.StackTraceExplorer;
using Microsoft.VisualStudio.Text.Classification;
using Roslyn.Utilities;
 
namespace Microsoft.VisualStudio.LanguageServices.StackTraceExplorer;
 
using StackFrameToken = EmbeddedSyntaxToken<StackFrameKind>;
using StackFrameTrivia = EmbeddedSyntaxTrivia<StackFrameKind>;
 
internal class StackFrameViewModel : FrameViewModel
{
    private readonly ParsedStackFrame _frame;
    private readonly IThreadingContext _threadingContext;
    private readonly Workspace _workspace;
    private readonly IStackTraceExplorerService _stackExplorerService;
    private readonly Dictionary<StackFrameSymbolPart, DefinitionItem?> _definitionCache = [];
 
    private Document? _cachedDocument;
    private int _cachedLineNumber;
 
    public StackFrameViewModel(
        ParsedStackFrame frame,
        IThreadingContext threadingContext,
        Workspace workspace,
        IClassificationFormatMap formatMap,
        ClassificationTypeMap typeMap)
        : base(formatMap, typeMap)
    {
        _frame = frame;
        _threadingContext = threadingContext;
        _workspace = workspace;
        _stackExplorerService = workspace.Services.GetRequiredService<IStackTraceExplorerService>();
    }
 
    public override bool ShowMouseOver => true;
 
    public void NavigateToClass()
    {
        var cancellationToken = _threadingContext.DisposalToken;
        Task.Run(() => NavigateToClassAsync(cancellationToken), cancellationToken).ReportNonFatalErrorAsync();
    }
 
    public async Task NavigateToClassAsync(CancellationToken cancellationToken)
    {
        try
        {
            var definition = await GetDefinitionAsync(StackFrameSymbolPart.ContainingType, cancellationToken).ConfigureAwait(false);
            await NavigateToDefinitionAsync(definition, cancellationToken).ConfigureAwait(false);
        }
        catch (Exception ex) when (FatalError.ReportAndCatchUnlessCanceled(ex, cancellationToken))
        {
        }
    }
 
    private async Task NavigateToDefinitionAsync(DefinitionItem? definition, CancellationToken cancellationToken)
    {
        if (definition is null)
            return;
 
        var location = await definition.GetNavigableLocationAsync(
            _workspace, cancellationToken).ConfigureAwait(false);
        await location.TryNavigateToAsync(
            _threadingContext, new NavigationOptions(PreferProvisionalTab: true, ActivateTab: false), cancellationToken).ConfigureAwait(false);
    }
 
    public void NavigateToSymbol()
    {
        var cancellationToken = _threadingContext.DisposalToken;
        Task.Run(() => NavigateToMethodAsync(cancellationToken), cancellationToken).ReportNonFatalErrorAsync();
    }
 
    public async Task NavigateToMethodAsync(CancellationToken cancellationToken)
    {
        try
        {
            var definition = await GetDefinitionAsync(StackFrameSymbolPart.Method, cancellationToken).ConfigureAwait(false);
            await NavigateToDefinitionAsync(definition, cancellationToken).ConfigureAwait(false);
        }
        catch (Exception ex) when (FatalError.ReportAndCatchUnlessCanceled(ex, cancellationToken))
        {
        }
    }
 
    public void NavigateToFile()
    {
        var cancellationToken = _threadingContext.DisposalToken;
        Task.Run(() => NavigateToFileAsync(cancellationToken), cancellationToken).ReportNonFatalErrorAsync();
    }
 
    public async Task NavigateToFileAsync(CancellationToken cancellationToken)
    {
        try
        {
            var (document, lineNumber) = GetDocumentAndLine();
 
            if (document is not null)
            {
                // While navigating do not activate the tab, which will change focus from the tool window
                var options = new NavigationOptions(PreferProvisionalTab: true, ActivateTab: false);
 
                var sourceText = await document.GetValueTextAsync(cancellationToken).ConfigureAwait(false);
 
                // If the line number is larger than the total lines in the file
                // then just go to the end of the file (lines count). This can happen
                // if the file changed between the stack trace being looked at and the current
                // version of the file.
                lineNumber = Math.Min(sourceText.Lines.Count, lineNumber);
 
                var navigationService = _workspace.Services.GetService<IDocumentNavigationService>();
                if (navigationService is null)
                    return;
 
                var location = await navigationService.TryNavigateToLineAndOffsetAsync(
                    _threadingContext, _workspace, document.Id, lineNumber - 1, offset: 0, options, cancellationToken).ConfigureAwait(false);
            }
        }
        catch (Exception ex) when (FatalError.ReportAndCatchUnlessCanceled(ex, cancellationToken))
        {
        }
    }
 
    protected override IEnumerable<Inline> CreateInlines()
    {
        var methodDeclaration = _frame.Root.MethodDeclaration;
        var tree = _frame.Tree;
        var className = methodDeclaration.MemberAccessExpression.Left;
        var classLeadingTrivia = GetLeadingTrivia(className);
        yield return MakeClassifiedRun(ClassificationTypeNames.Text, CreateString(classLeadingTrivia));
 
        //
        // Build the link to the class
        //
 
        var classLink = new Hyperlink();
        var classLinkText = className.ToString();
        classLink.Inlines.Add(MakeClassifiedRun(ClassificationTypeNames.ClassName, classLinkText));
        classLink.Click += (s, a) => NavigateToClass();
        classLink.RequestNavigate += (s, a) => NavigateToClass();
        yield return classLink;
 
        // Since we're only using the left side of a qualified name, we expect 
        // there to be no trivia on the right (trailing).
        Debug.Assert(GetTrailingTrivia(className).IsEmpty);
 
        //
        // Build the link to the method
        //
        var methodLink = new Hyperlink();
        var methodTextBuilder = new StringBuilder();
        methodTextBuilder.Append(methodDeclaration.MemberAccessExpression.DotToken.ToFullString());
        methodTextBuilder.Append(methodDeclaration.MemberAccessExpression.Right.ToFullString());
 
        if (methodDeclaration.TypeArguments is not null)
        {
            methodTextBuilder.Append(methodDeclaration.TypeArguments.ToFullString());
        }
 
        methodTextBuilder.Append(methodDeclaration.ArgumentList.ToFullString());
        methodLink.Inlines.Add(MakeClassifiedRun(ClassificationTypeNames.MethodName, methodTextBuilder.ToString()));
        methodLink.Click += (s, a) => NavigateToSymbol();
        methodLink.RequestNavigate += (s, a) => NavigateToSymbol();
        yield return methodLink;
 
        //
        // If there is file information build a link to that
        //
        if (_frame.Root.FileInformationExpression is not null)
        {
            var fileInformation = _frame.Root.FileInformationExpression;
            var leadingTrivia = GetLeadingTrivia(fileInformation);
            yield return MakeClassifiedRun(ClassificationTypeNames.Text, CreateString(leadingTrivia));
 
            var fileLink = new Hyperlink();
            var fileLinkText = _frame.Root.FileInformationExpression.ToString();
            fileLink.Inlines.Add(MakeClassifiedRun(ClassificationTypeNames.Text, fileInformation.ToString()));
            fileLink.Click += (s, a) => NavigateToFile();
            fileLink.RequestNavigate += (s, a) => NavigateToFile();
            yield return fileLink;
 
            var trailingTrivia = GetTrailingTrivia(fileInformation);
            yield return MakeClassifiedRun(ClassificationTypeNames.Text, CreateString(trailingTrivia));
        }
 
        //
        // Don't lose the trailing trivia text
        //
        yield return MakeClassifiedRun(ClassificationTypeNames.Text, _frame.Root.EndOfLineToken.ToFullString());
    }
 
    private (Document? document, int lineNumber) GetDocumentAndLine()
    {
        if (_cachedDocument is not null)
        {
            return (_cachedDocument, _cachedLineNumber);
        }
 
        (_cachedDocument, _cachedLineNumber) = _stackExplorerService.GetDocumentAndLine(_workspace.CurrentSolution, _frame);
        return (_cachedDocument, _cachedLineNumber);
    }
 
    private async Task<DefinitionItem?> GetDefinitionAsync(StackFrameSymbolPart symbolPart, CancellationToken cancellationToken)
    {
        if (_definitionCache.TryGetValue(symbolPart, out var definition) && definition is not null)
        {
            return definition;
        }
 
        _definitionCache[symbolPart] = await _stackExplorerService.TryFindDefinitionAsync(_workspace.CurrentSolution, _frame, symbolPart, cancellationToken).ConfigureAwait(false);
        return _definitionCache[symbolPart];
    }
 
    private static ImmutableArray<StackFrameTrivia> GetLeadingTrivia(StackFrameNode node)
    {
        if (node.ChildCount == 0)
        {
            return ImmutableArray<StackFrameTrivia>.Empty;
        }
 
        var child = node[0];
        if (child.IsNode)
        {
            return GetLeadingTrivia(child.Node);
        }
 
        return child.Token.LeadingTrivia;
    }
 
    private static ImmutableArray<StackFrameTrivia> GetTrailingTrivia(StackFrameNode node)
    {
        if (node.ChildCount == 0)
        {
            return ImmutableArray<StackFrameTrivia>.Empty;
        }
 
        var child = node[^1];
        if (child.IsNode)
        {
            return GetTrailingTrivia(child.Node);
        }
 
        return child.Token.TrailingTrivia;
    }
 
    /// <summary>
    /// Depth first traversal of the descendents of a node to the tokens
    /// </summary>
    private static void GetLeafTokens(StackFrameNode node, ArrayBuilder<StackFrameToken> builder)
    {
        foreach (var child in node)
        {
            if (child.IsNode)
            {
                GetLeafTokens(child.Node, builder);
            }
            else
            {
                builder.Add(child.Token);
            }
        }
    }
 
    private static string CreateString(ImmutableArray<StackFrameTrivia> triviaList)
    {
        using var _ = PooledStringBuilder.GetInstance(out var sb);
        foreach (var trivia in triviaList)
        {
            sb.Append(trivia.ToString());
        }
 
        return sb.ToString();
    }
}