├── src ├── version.json ├── Key.snk ├── Terrajobst.StackTraceExplorer │ ├── Resources │ │ ├── StackTraceExplorer.ico │ │ └── StackTraceExplorer.png │ ├── Properties │ │ └── AssemblyInfo.cs │ ├── VSPackage.cs │ ├── StackTraceExplorerPaneControl.xaml │ ├── StackTraceExplorerPane.cs │ ├── source.extension.vsixmanifest │ ├── ExploreStackTraceCommand.cs │ ├── app.config │ ├── StackTraceExplorerPaneControl.xaml.cs │ ├── VisualStudioStackTraceWriter.cs │ ├── VSPackage.vsct │ ├── VSPackage.resx │ └── Terrajobst.StackTraceExplorer.csproj ├── Terrajobst.StackTraces.Tests │ ├── App.config │ ├── Terrajobst.StackTraces.Tests.csproj │ ├── Helpers │ │ ├── RecordingCompilationStackTraceWriter.cs │ │ ├── AnnotatedText.cs │ │ └── AnnotatedText.Parser.cs │ └── StackTraceWriterTests.cs └── Terrajobst.StackTraces │ ├── Terrajobst.StackTraces.csproj │ ├── StackTraceWriter.cs │ └── CompilationStackTraceWriter.cs ├── docs └── screen.gif ├── README.md ├── LICENSE ├── StackTraceExplorer.sln ├── .gitattributes └── .gitignore /src/version.json: -------------------------------------------------------------------------------- 1 | { "version": "0.1.0-alpha" } -------------------------------------------------------------------------------- /src/Key.snk: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/terrajobst/stack-trace-explorer/HEAD/src/Key.snk -------------------------------------------------------------------------------- /docs/screen.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/terrajobst/stack-trace-explorer/HEAD/docs/screen.gif -------------------------------------------------------------------------------- /src/Terrajobst.StackTraceExplorer/Resources/StackTraceExplorer.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/terrajobst/stack-trace-explorer/HEAD/src/Terrajobst.StackTraceExplorer/Resources/StackTraceExplorer.ico -------------------------------------------------------------------------------- /src/Terrajobst.StackTraceExplorer/Resources/StackTraceExplorer.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/terrajobst/stack-trace-explorer/HEAD/src/Terrajobst.StackTraceExplorer/Resources/StackTraceExplorer.png -------------------------------------------------------------------------------- /src/Terrajobst.StackTraces.Tests/App.config: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /src/Terrajobst.StackTraceExplorer/Properties/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using System.Runtime.InteropServices; 3 | 4 | [assembly: AssemblyTitle("Terrajobst.StackExplorer.Vsix")] 5 | [assembly: AssemblyDescription("")] 6 | [assembly: AssemblyConfiguration("")] 7 | [assembly: AssemblyCompany("")] 8 | [assembly: AssemblyProduct("Terrajobst.StackExplorer.Vsix")] 9 | [assembly: AssemblyCopyright("")] 10 | [assembly: AssemblyTrademark("")] 11 | [assembly: AssemblyCulture("")] 12 | [assembly: ComVisible(false)] 13 | 14 | -------------------------------------------------------------------------------- /src/Terrajobst.StackTraces/Terrajobst.StackTraces.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net461 5 | true 6 | ..\Key.snk 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /src/Terrajobst.StackTraceExplorer/VSPackage.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Runtime.InteropServices; 3 | using Microsoft.VisualStudio.Shell; 4 | 5 | namespace Terrajobst.StackTraceExplorer 6 | { 7 | [PackageRegistration(UseManagedResourcesOnly = true)] 8 | [ProvideMenuResource("Menus.ctmenu", 1)] 9 | [ProvideToolWindow(typeof(StackTraceExplorerPane))] 10 | [Guid("cdc68824-5e11-4207-8caa-41f91a2716f4")] 11 | internal sealed class VSPackage : Package 12 | { 13 | protected override void Initialize() 14 | { 15 | ExploreStackTraceCommand.Initialize(this); 16 | base.Initialize(); 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/Terrajobst.StackTraceExplorer/StackTraceExplorerPaneControl.xaml: -------------------------------------------------------------------------------- 1 | 11 | 12 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Stack Trace Explorer 2 | 3 | [![Build status](https://ci.appveyor.com/api/projects/status/gnv1ddo8ge9d5ek7/branch/master?svg=true)](https://ci.appveyor.com/project/terrajobst/stack-trace-explorer/branch/master) 4 | 5 | Stack Trace Explorer is a Visual Studio plug-in that pretty prints a stack trace 6 | by turning types, methods, and paths into hyper links: 7 | 8 | ![](docs/screen.gif) 9 | 10 | ## Download 11 | 12 | You can find the latest version in the [Visual Studio Marketplace](https://marketplace.visualstudio.com/items?itemName=ImmoLandwerthMSFT.StackTraceExplorer). 13 | 14 | ## Usage 15 | 16 | Using it is super simple: 17 | 18 | 1. Copy a stack trace to the clipboard 19 | 2. Invoke Ctrl Shift E, S 20 | 21 | Enjoy navigating your stack trace! 22 | -------------------------------------------------------------------------------- /src/Terrajobst.StackTraces.Tests/Terrajobst.StackTraces.Tests.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net461 5 | false 6 | 7 | 8 | 9 | true 10 | true 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /src/Terrajobst.StackTraceExplorer/StackTraceExplorerPane.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Runtime.InteropServices; 3 | using Microsoft.VisualStudio.Shell; 4 | 5 | namespace Terrajobst.StackTraceExplorer 6 | { 7 | [Guid("0100a14e-3984-4732-a2b8-766393de4859")] 8 | public class StackTraceExplorerPane : ToolWindowPane 9 | { 10 | private string _stackTrace; 11 | 12 | public StackTraceExplorerPane() 13 | : base(null) 14 | { 15 | Caption = "Stack Trace Explorer"; 16 | } 17 | 18 | private StackTraceExplorerPaneControl Control => (StackTraceExplorerPaneControl)Content; 19 | 20 | public string StackTrace 21 | { 22 | get => _stackTrace; 23 | set 24 | { 25 | _stackTrace = value; 26 | Control.SetStackTrace(value); 27 | } 28 | } 29 | 30 | protected override void Initialize() 31 | { 32 | base.Initialize(); 33 | Content = new StackTraceExplorerPaneControl(this); 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2012 Immo Landwerth 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. -------------------------------------------------------------------------------- /src/Terrajobst.StackTraces.Tests/Helpers/RecordingCompilationStackTraceWriter.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Collections.Immutable; 3 | using System.Text; 4 | using Microsoft.CodeAnalysis; 5 | 6 | namespace Terrajobst.StackTraces.Tests.Helpers 7 | { 8 | internal sealed class RecordingCompilationStackTraceWriter : CompilationStackTraceWriter 9 | { 10 | private List _methods = new List(); 11 | private StringBuilder _stringBuilder = new StringBuilder(); 12 | 13 | public RecordingCompilationStackTraceWriter(Compilation compilation) 14 | : base(ImmutableArray.Create(compilation)) 15 | { 16 | } 17 | 18 | public string GetText() 19 | { 20 | return _stringBuilder.ToString(); 21 | } 22 | 23 | public ImmutableArray GetMethods() 24 | { 25 | return _methods.ToImmutableArray(); 26 | } 27 | 28 | protected override void WritePath(string path, int lineNumber) 29 | { 30 | _stringBuilder.Append(path); 31 | _stringBuilder.Append(':'); 32 | _stringBuilder.Append(lineNumber); 33 | } 34 | 35 | protected override void WriteSymbol(string text, ISymbol symbol) 36 | { 37 | if (symbol is IMethodSymbol m) 38 | _methods.Add(m); 39 | 40 | WriteText(text); 41 | } 42 | 43 | protected override void WriteText(string text) 44 | { 45 | _stringBuilder.Append(text); 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/Terrajobst.StackTraceExplorer/source.extension.vsixmanifest: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Stack Trace Explorer 6 | Exploring stack traces for the 21st century. 7 | https://github.com/terrajobst/stack-trace-explorer 8 | Resources\StackTraceExplorer.ico 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /src/Terrajobst.StackTraceExplorer/ExploreStackTraceCommand.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.ComponentModel.Design; 3 | using System.Windows; 4 | using Microsoft.VisualStudio; 5 | using Microsoft.VisualStudio.Shell; 6 | using Microsoft.VisualStudio.Shell.Interop; 7 | 8 | namespace Terrajobst.StackTraceExplorer 9 | { 10 | internal sealed class ExploreStackTraceCommand 11 | { 12 | public static readonly Guid CommandSet = new Guid("af9da84c-2742-4bc4-a2c1-2370f8bff5ee"); 13 | public const int CommandId = 0x0100; 14 | 15 | public static ExploreStackTraceCommand Instance { get; private set; } 16 | 17 | public static void Initialize(Package package) 18 | { 19 | Instance = new ExploreStackTraceCommand(package); 20 | } 21 | 22 | private readonly Package _package; 23 | 24 | private ExploreStackTraceCommand(Package package) 25 | { 26 | _package = package ?? throw new ArgumentNullException("package"); 27 | 28 | var serviceProvider = (IServiceProvider)_package; 29 | 30 | if (serviceProvider.GetService(typeof(IMenuCommandService)) is OleMenuCommandService commandService) 31 | { 32 | var menuCommandID = new CommandID(CommandSet, CommandId); 33 | var menuItem = new MenuCommand(ShowToolWindow, menuCommandID); 34 | commandService.AddCommand(menuItem); 35 | } 36 | } 37 | 38 | private void ShowToolWindow(object sender, EventArgs e) 39 | { 40 | var window = _package.FindToolWindow(typeof(StackTraceExplorerPane), 0, true) as StackTraceExplorerPane; 41 | if (window == null || window.Frame == null) 42 | throw new NotSupportedException("Cannot create tool window"); 43 | 44 | var stackTrace = Clipboard.GetText(); 45 | if (!string.IsNullOrEmpty(stackTrace)) 46 | window.StackTrace = stackTrace; 47 | 48 | var windowFrame = (IVsWindowFrame)window.Frame; 49 | ErrorHandler.ThrowOnFailure(windowFrame.Show()); 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/Terrajobst.StackTraces.Tests/Helpers/AnnotatedText.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Immutable; 3 | using System.IO; 4 | using System.Text; 5 | using Microsoft.CodeAnalysis.Text; 6 | 7 | namespace Terrajobst.StackTraces.Tests.Helpers 8 | { 9 | public sealed partial class AnnotatedText 10 | { 11 | private AnnotatedText(string text, ImmutableArray spans) 12 | { 13 | Text = text; 14 | Spans = spans; 15 | } 16 | 17 | public static AnnotatedText Parse(string text) 18 | { 19 | if (text == null) 20 | throw new ArgumentNullException(nameof(text)); 21 | 22 | var parser = new Parser(text); 23 | return parser.Parse(); 24 | } 25 | 26 | public static string NormalizeCode(string text) 27 | { 28 | return Unindent(text).Trim(); 29 | } 30 | 31 | private static string Unindent(string text) 32 | { 33 | var minIndent = int.MaxValue; 34 | 35 | using (var stringReader = new StringReader(text)) 36 | { 37 | string line; 38 | while ((line = stringReader.ReadLine()) != null) 39 | { 40 | if (string.IsNullOrWhiteSpace(line)) 41 | continue; 42 | 43 | var indent = line.Length - line.TrimStart().Length; 44 | minIndent = Math.Min(minIndent, indent); 45 | } 46 | } 47 | 48 | var sb = new StringBuilder(); 49 | using (var stringReader = new StringReader(text)) 50 | { 51 | string line; 52 | while ((line = stringReader.ReadLine()) != null) 53 | { 54 | var unindentedLine = line.Length < minIndent 55 | ? line 56 | : line.Substring(minIndent); 57 | sb.AppendLine(unindentedLine); 58 | } 59 | } 60 | 61 | return sb.ToString(); 62 | } 63 | 64 | public string Text { get; } 65 | 66 | public ImmutableArray Spans { get; } 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/Terrajobst.StackTraceExplorer/app.config: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /StackTraceExplorer.sln: -------------------------------------------------------------------------------- 1 | 2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio 15 4 | VisualStudioVersion = 15.0.27004.2008 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Terrajobst.StackTraceExplorer", "src\Terrajobst.StackTraceExplorer\Terrajobst.StackTraceExplorer.csproj", "{7BD4BA8A-B0E0-42DB-A035-C57C9430B447}" 7 | EndProject 8 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Terrajobst.StackTraces", "src\Terrajobst.StackTraces\Terrajobst.StackTraces.csproj", "{E82F0CB5-DF4C-4842-8307-0769537EB098}" 9 | EndProject 10 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Terrajobst.StackTraces.Tests", "src\Terrajobst.StackTraces.Tests\Terrajobst.StackTraces.Tests.csproj", "{BF9A27C2-0CEB-4505-8483-3C5E965C6F49}" 11 | EndProject 12 | Global 13 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 14 | Debug|Any CPU = Debug|Any CPU 15 | Release|Any CPU = Release|Any CPU 16 | EndGlobalSection 17 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 18 | {7BD4BA8A-B0E0-42DB-A035-C57C9430B447}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 19 | {7BD4BA8A-B0E0-42DB-A035-C57C9430B447}.Debug|Any CPU.Build.0 = Debug|Any CPU 20 | {7BD4BA8A-B0E0-42DB-A035-C57C9430B447}.Release|Any CPU.ActiveCfg = Release|Any CPU 21 | {7BD4BA8A-B0E0-42DB-A035-C57C9430B447}.Release|Any CPU.Build.0 = Release|Any CPU 22 | {E82F0CB5-DF4C-4842-8307-0769537EB098}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 23 | {E82F0CB5-DF4C-4842-8307-0769537EB098}.Debug|Any CPU.Build.0 = Debug|Any CPU 24 | {E82F0CB5-DF4C-4842-8307-0769537EB098}.Release|Any CPU.ActiveCfg = Release|Any CPU 25 | {E82F0CB5-DF4C-4842-8307-0769537EB098}.Release|Any CPU.Build.0 = Release|Any CPU 26 | {BF9A27C2-0CEB-4505-8483-3C5E965C6F49}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 27 | {BF9A27C2-0CEB-4505-8483-3C5E965C6F49}.Debug|Any CPU.Build.0 = Debug|Any CPU 28 | {BF9A27C2-0CEB-4505-8483-3C5E965C6F49}.Release|Any CPU.ActiveCfg = Release|Any CPU 29 | {BF9A27C2-0CEB-4505-8483-3C5E965C6F49}.Release|Any CPU.Build.0 = Release|Any CPU 30 | EndGlobalSection 31 | GlobalSection(SolutionProperties) = preSolution 32 | HideSolutionNode = FALSE 33 | EndGlobalSection 34 | GlobalSection(ExtensibilityGlobals) = postSolution 35 | SolutionGuid = {0E77C69C-33C2-4D33-8A50-1762875C3650} 36 | EndGlobalSection 37 | EndGlobal 38 | -------------------------------------------------------------------------------- /src/Terrajobst.StackTraceExplorer/StackTraceExplorerPaneControl.xaml.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Immutable; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | using System.Windows; 6 | using System.Windows.Controls; 7 | using System.Windows.Media; 8 | using Microsoft.CodeAnalysis; 9 | using Microsoft.VisualStudio.ComponentModelHost; 10 | using Microsoft.VisualStudio.LanguageServices; 11 | using Terrajobst.StackTraces; 12 | 13 | namespace Terrajobst.StackTraceExplorer 14 | { 15 | public partial class StackTraceExplorerPaneControl : UserControl 16 | { 17 | public StackTraceExplorerPaneControl(IServiceProvider serviceProvider) 18 | { 19 | InitializeComponent(); 20 | ServiceProvider = serviceProvider; 21 | } 22 | 23 | public IServiceProvider ServiceProvider { get; } 24 | 25 | public async void SetStackTrace(string text) 26 | { 27 | var componentModel = (IComponentModel)ServiceProvider.GetService(typeof(SComponentModel)); 28 | var workspace = componentModel.GetService(); 29 | 30 | var solution = workspace.CurrentSolution; 31 | var compilations = await GetCompilationsAsync(solution); 32 | 33 | var writer = new VisualStudioStackTraceWriter(compilations, ServiceProvider); 34 | StackTraceWriter.Write(text, writer); 35 | 36 | var textBlock = new TextBlock 37 | { 38 | Padding = new Thickness(16), 39 | FontFamily = new FontFamily("Consolas") 40 | }; 41 | textBlock.Inlines.AddRange(writer.GetInlines()); 42 | 43 | Content = new ScrollViewer 44 | { 45 | VerticalScrollBarVisibility = ScrollBarVisibility.Auto, 46 | HorizontalScrollBarVisibility = ScrollBarVisibility.Auto, 47 | Content = textBlock 48 | }; 49 | } 50 | 51 | public async Task> GetCompilationsAsync(Solution solution) 52 | { 53 | var compilationTasks = new Task[solution.ProjectIds.Count]; 54 | for (var i = 0; i < compilationTasks.Length; i++) 55 | { 56 | var project = solution.GetProject(solution.ProjectIds[i]); 57 | compilationTasks[i] = project.GetCompilationAsync(); 58 | } 59 | 60 | await Task.WhenAll(compilationTasks); 61 | 62 | return compilationTasks.Select(t => t.Result).ToImmutableArray(); 63 | } 64 | } 65 | } -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | ############################################################################### 2 | # Set default behavior to automatically normalize line endings. 3 | ############################################################################### 4 | * text=auto 5 | 6 | ############################################################################### 7 | # Set default behavior for command prompt diff. 8 | # 9 | # This is need for earlier builds of msysgit that does not have it on by 10 | # default for csharp files. 11 | # Note: This is only used by command line 12 | ############################################################################### 13 | #*.cs diff=csharp 14 | 15 | ############################################################################### 16 | # Set the merge driver for project and solution files 17 | # 18 | # Merging from the command prompt will add diff markers to the files if there 19 | # are conflicts (Merging from VS is not affected by the settings below, in VS 20 | # the diff markers are never inserted). Diff markers may cause the following 21 | # file extensions to fail to load in VS. An alternative would be to treat 22 | # these files as binary and thus will always conflict and require user 23 | # intervention with every merge. To do so, just uncomment the entries below 24 | ############################################################################### 25 | #*.sln merge=binary 26 | #*.csproj merge=binary 27 | #*.vbproj merge=binary 28 | #*.vcxproj merge=binary 29 | #*.vcproj merge=binary 30 | #*.dbproj merge=binary 31 | #*.fsproj merge=binary 32 | #*.lsproj merge=binary 33 | #*.wixproj merge=binary 34 | #*.modelproj merge=binary 35 | #*.sqlproj merge=binary 36 | #*.wwaproj merge=binary 37 | 38 | ############################################################################### 39 | # behavior for image files 40 | # 41 | # image files are treated as binary by default. 42 | ############################################################################### 43 | #*.jpg binary 44 | #*.png binary 45 | #*.gif binary 46 | 47 | ############################################################################### 48 | # diff behavior for common document formats 49 | # 50 | # Convert binary document formats to text before diffing them. This feature 51 | # is only available from the command line. Turn it on by uncommenting the 52 | # entries below. 53 | ############################################################################### 54 | #*.doc diff=astextplain 55 | #*.DOC diff=astextplain 56 | #*.docx diff=astextplain 57 | #*.DOCX diff=astextplain 58 | #*.dot diff=astextplain 59 | #*.DOT diff=astextplain 60 | #*.pdf diff=astextplain 61 | #*.PDF diff=astextplain 62 | #*.rtf diff=astextplain 63 | #*.RTF diff=astextplain 64 | -------------------------------------------------------------------------------- /src/Terrajobst.StackTraceExplorer/VisualStudioStackTraceWriter.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Collections.Immutable; 4 | using System.IO; 5 | using System.Linq; 6 | using System.Runtime.InteropServices; 7 | using System.Windows; 8 | using System.Windows.Documents; 9 | using System.Windows.Input; 10 | using Microsoft.CodeAnalysis; 11 | using Microsoft.VisualStudio; 12 | using Microsoft.VisualStudio.Shell; 13 | using Microsoft.VisualStudio.Shell.Interop; 14 | using Microsoft.VisualStudio.TextManager.Interop; 15 | using Terrajobst.StackTraces; 16 | 17 | namespace Terrajobst.StackTraceExplorer 18 | { 19 | internal sealed class VisualStudioStackTraceWriter : CompilationStackTraceWriter 20 | { 21 | private readonly IServiceProvider _serviceProvider; 22 | private readonly List _inlines = new List(); 23 | 24 | public VisualStudioStackTraceWriter(ImmutableArray compilations, IServiceProvider serviceProvider) 25 | : base(compilations) 26 | { 27 | _serviceProvider = serviceProvider; 28 | } 29 | 30 | public IReadOnlyList GetInlines() => _inlines.ToArray(); 31 | 32 | protected override void WriteText(string text) 33 | { 34 | _inlines.Add(new Run(text)); 35 | } 36 | 37 | protected override void WriteSymbol(string text, ISymbol symbol) 38 | { 39 | var location = symbol.Locations.First(); 40 | var isMethod = symbol is IMethodSymbol; 41 | 42 | var run = new Run(text) 43 | { 44 | ToolTip = symbol.ToString() 45 | }; 46 | 47 | if (location.IsInSource) 48 | { 49 | var path = location.SourceTree.FilePath; 50 | var position = location.GetLineSpan().StartLinePosition; 51 | var line = position.Line; 52 | var character = position.Character; 53 | 54 | RegisterNavigationCommand(run, path, line, character); 55 | } 56 | 57 | _inlines.Add(run); 58 | } 59 | 60 | protected override void WritePath(string path, int lineNumber) 61 | { 62 | var text = Path.GetFileName(path) + ":" + lineNumber; 63 | 64 | var run = new Run(text) 65 | { 66 | ToolTip = path 67 | }; 68 | 69 | RegisterNavigationCommand(run, path, lineNumber - 1, 0); 70 | 71 | _inlines.Add(run); 72 | } 73 | 74 | private void RegisterNavigationCommand(Run run, string path, int line, int character) 75 | { 76 | run.Cursor = Cursors.Hand; 77 | run.MouseEnter += (s, e) => run.TextDecorations = TextDecorations.Underline; 78 | run.MouseLeave += (s, e) => run.TextDecorations = null; 79 | run.MouseDown += (s, e) => NavigateTo(path, line, character); 80 | } 81 | 82 | private void NavigateTo(string fileName, int line, int character) 83 | { 84 | VsShellUtilities.OpenDocument(_serviceProvider, fileName); 85 | 86 | var docTable = (IVsRunningDocumentTable)_serviceProvider.GetService(typeof(SVsRunningDocumentTable)); 87 | if (docTable.FindAndLockDocument((uint)_VSRDTFLAGS.RDT_NoLock, fileName, out var hierarchy, 88 | out var itemid, out var bufferPtr, out var cookie) != VSConstants.S_OK) 89 | return; 90 | 91 | try 92 | { 93 | var lines = Marshal.GetObjectForIUnknown(bufferPtr) as IVsTextLines; 94 | if (lines == null) 95 | return; 96 | 97 | var textManager = (IVsTextManager)_serviceProvider.GetService(typeof(SVsTextManager)); 98 | if (textManager == null) 99 | return; 100 | 101 | textManager.NavigateToLineAndColumn(lines, VSConstants.LOGVIEWID.TextView_guid, line, character, line, character); 102 | } 103 | finally 104 | { 105 | if (bufferPtr != IntPtr.Zero) 106 | Marshal.Release(bufferPtr); 107 | } 108 | } 109 | } 110 | } -------------------------------------------------------------------------------- /src/Terrajobst.StackTraces.Tests/Helpers/AnnotatedText.Parser.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Collections.Immutable; 4 | using System.Text; 5 | using Microsoft.CodeAnalysis.Text; 6 | 7 | namespace Terrajobst.StackTraces.Tests.Helpers 8 | { 9 | public sealed partial class AnnotatedText 10 | { 11 | private sealed class Parser 12 | { 13 | private static readonly IComparer SpanComparer = Comparer.Create((x, y) => x.Start.CompareTo(y.Start)); 14 | 15 | private readonly string _text; 16 | private int _position; 17 | private readonly StringBuilder _textBuilder = new StringBuilder(); 18 | private readonly ImmutableArray.Builder _spanBuilder = ImmutableArray.CreateBuilder(); 19 | 20 | public Parser(string text) 21 | { 22 | _text = text; 23 | } 24 | 25 | private char Char 26 | { 27 | get { return _position < _text.Length ? _text[_position] : '\0'; } 28 | } 29 | 30 | private char Lookahead 31 | { 32 | get { return _position + 1 < _text.Length ? _text[_position + 1] : '\0'; } 33 | } 34 | 35 | private bool IsSpanStart() 36 | { 37 | return Char == '{' && Lookahead == '{'; 38 | } 39 | 40 | private bool IsSpanEnd() 41 | { 42 | return Char == '}' && Lookahead == '}'; 43 | } 44 | 45 | public AnnotatedText Parse() 46 | { 47 | ParseRoot(); 48 | 49 | var text = _textBuilder.ToString(); 50 | 51 | _spanBuilder.Sort(SpanComparer); 52 | var spans = _spanBuilder.ToImmutable(); 53 | 54 | return new AnnotatedText(text, spans); 55 | } 56 | 57 | private void ParseRoot() 58 | { 59 | while (_position < _text.Length) 60 | { 61 | if (IsSpanStart()) 62 | ParseSpan(); 63 | else 64 | ParseText(); 65 | } 66 | } 67 | 68 | private void ParseSpan() 69 | { 70 | // Skip intitial braces 71 | _position += 2; 72 | 73 | var spanStart = _textBuilder.Length; 74 | 75 | while (true) 76 | { 77 | if (IsSpanEnd()) 78 | { 79 | // Skip closing braces 80 | _position += 2; 81 | var spanEnd = _textBuilder.Length; 82 | var span = TextSpan.FromBounds(spanStart, spanEnd); 83 | _spanBuilder.Add(span); 84 | break; 85 | } 86 | else if (IsSpanStart()) 87 | { 88 | ParseSpan(); 89 | } 90 | else if (Char == '\0') 91 | { 92 | throw MissingClosingBrace(); 93 | } 94 | else 95 | { 96 | ParseText(); 97 | } 98 | } 99 | } 100 | 101 | private FormatException MissingClosingBrace() 102 | { 103 | var message = $"Missing '}}}}' at position {_position}."; 104 | return new FormatException(message); 105 | } 106 | 107 | private void ParseText() 108 | { 109 | var start = _position; 110 | while (_position < _text.Length && 111 | !IsSpanStart() && 112 | !IsSpanEnd()) 113 | { 114 | _position++; 115 | } 116 | 117 | var end = _position; 118 | var length = end - start; 119 | var text = _text.Substring(start, length); 120 | _textBuilder.Append(text); 121 | } 122 | } 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /src/Terrajobst.StackTraces/StackTraceWriter.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Text.RegularExpressions; 3 | 4 | namespace Terrajobst.StackTraces 5 | { 6 | public abstract class StackTraceWriter 7 | { 8 | public static void Write(string text, StackTraceWriter writer) 9 | { 10 | var lastPosition = 0; 11 | 12 | foreach (var lineExtent in GetLineExtents(text)) 13 | { 14 | var line = text.Substring(lineExtent.start, lineExtent.length); 15 | var frameMatch = Regex.Match(line, @"^.*?(?([^. ])+\..*\([^.]*\)).*?((?[a-zA-Z]:.*?):.*?(?[0-9]+))?$"); 16 | 17 | if (frameMatch.Success) 18 | { 19 | var methodNameGroup = frameMatch.Groups["method"]; 20 | var pathGroup = frameMatch.Groups["path"]; 21 | var lineNumberGroup = frameMatch.Groups["line"]; 22 | 23 | var methodNameStart = lineExtent.start + methodNameGroup.Index; 24 | var methodNameLength = methodNameGroup.Length; 25 | var methodName = text.Substring(methodNameStart, methodNameLength); 26 | 27 | var pathStart = lineExtent.start + pathGroup.Index; 28 | var pathLength = pathGroup.Length; 29 | var path = text.Substring(pathStart, pathLength); 30 | 31 | var lineNumberStart = lineExtent.start + lineNumberGroup.Index; 32 | var lineNumberLength = lineNumberGroup.Length; 33 | var hasLineNumber = lineNumberLength > 0; 34 | var lineNumberText = text.Substring(lineNumberStart, lineNumberLength); 35 | var lineNumber = hasLineNumber ? int.Parse(lineNumberText) : 0; 36 | 37 | writer.WriteText(text, ref lastPosition, methodNameStart); 38 | 39 | writer.WriteMethodText(methodName); 40 | 41 | lastPosition = methodNameStart + methodNameLength; 42 | 43 | writer.WriteText(text, ref lastPosition, pathStart); 44 | 45 | if (path.Length > 0) 46 | writer.WritePath(path, lineNumber); 47 | 48 | lastPosition = lineExtent.start + lineExtent.length; 49 | } 50 | } 51 | 52 | writer.WriteText(text, ref lastPosition, text.Length); 53 | } 54 | 55 | private static IEnumerable<(int start, int length)> GetLineExtents(string text) 56 | { 57 | var start = 0; 58 | var position = start; 59 | 60 | while (position < text.Length) 61 | { 62 | var c = text[position]; 63 | var l = position == text.Length - 1 ? '\0' : text[position + 1]; 64 | 65 | var end = position; 66 | var length = end - start; 67 | var returnLine = false; 68 | 69 | if (c == '\r') 70 | { 71 | if (l == '\n') 72 | position++; 73 | 74 | returnLine = true; 75 | } 76 | else if (c == '\n') 77 | { 78 | returnLine = true; 79 | } 80 | 81 | position++; 82 | 83 | if (returnLine) 84 | { 85 | yield return (start, length); 86 | start = position; 87 | } 88 | } 89 | 90 | if (start < position) 91 | yield return (start, text.Length - start); 92 | } 93 | 94 | private void WriteText(string text, ref int lastPosition, int position) 95 | { 96 | var start = lastPosition; 97 | var length = position - lastPosition; 98 | 99 | if (length > 0) 100 | { 101 | WriteText(text.Substring(start, length)); 102 | lastPosition = position; 103 | } 104 | } 105 | 106 | protected abstract void WriteText(string text); 107 | 108 | protected abstract void WriteMethodText(string methodName); 109 | 110 | protected abstract void WritePath(string path, int lineNumber); 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /src/Terrajobst.StackTraceExplorer/VSPackage.vsct: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 10 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 25 | 26 | 33 | 34 | 35 | 37 | 38 | 45 | 52 | 53 | 54 | 55 | 56 | 61 | 62 | 63 | 64 | 65 | 66 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | 4 | # User-specific files 5 | *.suo 6 | *.user 7 | *.userosscache 8 | *.sln.docstates 9 | 10 | # User-specific files (MonoDevelop/Xamarin Studio) 11 | *.userprefs 12 | 13 | # Build results 14 | [Dd]ebug/ 15 | [Dd]ebugPublic/ 16 | [Rr]elease/ 17 | [Rr]eleases/ 18 | x64/ 19 | x86/ 20 | bld/ 21 | [Bb]in/ 22 | [Oo]bj/ 23 | [Ll]og/ 24 | 25 | # Visual Studio 2015 cache/options directory 26 | .vs/ 27 | # Uncomment if you have tasks that create the project's static files in wwwroot 28 | #wwwroot/ 29 | 30 | # MSTest test Results 31 | [Tt]est[Rr]esult*/ 32 | [Bb]uild[Ll]og.* 33 | 34 | # NUNIT 35 | *.VisualState.xml 36 | TestResult.xml 37 | 38 | # Build Results of an ATL Project 39 | [Dd]ebugPS/ 40 | [Rr]eleasePS/ 41 | dlldata.c 42 | 43 | # DNX 44 | project.lock.json 45 | project.fragment.lock.json 46 | artifacts/ 47 | 48 | *_i.c 49 | *_p.c 50 | *_i.h 51 | *.ilk 52 | *.meta 53 | *.obj 54 | *.pch 55 | *.pdb 56 | *.pgc 57 | *.pgd 58 | *.rsp 59 | *.sbr 60 | *.tlb 61 | *.tli 62 | *.tlh 63 | *.tmp 64 | *.tmp_proj 65 | *.log 66 | *.vspscc 67 | *.vssscc 68 | .builds 69 | *.pidb 70 | *.svclog 71 | *.scc 72 | 73 | # Chutzpah Test files 74 | _Chutzpah* 75 | 76 | # Visual C++ cache files 77 | ipch/ 78 | *.aps 79 | *.ncb 80 | *.opendb 81 | *.opensdf 82 | *.sdf 83 | *.cachefile 84 | *.VC.db 85 | *.VC.VC.opendb 86 | 87 | # Visual Studio profiler 88 | *.psess 89 | *.vsp 90 | *.vspx 91 | *.sap 92 | 93 | # TFS 2012 Local Workspace 94 | $tf/ 95 | 96 | # Guidance Automation Toolkit 97 | *.gpState 98 | 99 | # ReSharper is a .NET coding add-in 100 | _ReSharper*/ 101 | *.[Rr]e[Ss]harper 102 | *.DotSettings.user 103 | 104 | # JustCode is a .NET coding add-in 105 | .JustCode 106 | 107 | # TeamCity is a build add-in 108 | _TeamCity* 109 | 110 | # DotCover is a Code Coverage Tool 111 | *.dotCover 112 | 113 | # NCrunch 114 | _NCrunch_* 115 | .*crunch*.local.xml 116 | nCrunchTemp_* 117 | 118 | # MightyMoose 119 | *.mm.* 120 | AutoTest.Net/ 121 | 122 | # Web workbench (sass) 123 | .sass-cache/ 124 | 125 | # Installshield output folder 126 | [Ee]xpress/ 127 | 128 | # DocProject is a documentation generator add-in 129 | DocProject/buildhelp/ 130 | DocProject/Help/*.HxT 131 | DocProject/Help/*.HxC 132 | DocProject/Help/*.hhc 133 | DocProject/Help/*.hhk 134 | DocProject/Help/*.hhp 135 | DocProject/Help/Html2 136 | DocProject/Help/html 137 | 138 | # Click-Once directory 139 | publish/ 140 | 141 | # Publish Web Output 142 | *.[Pp]ublish.xml 143 | *.azurePubxml 144 | # TODO: Comment the next line if you want to checkin your web deploy settings 145 | # but database connection strings (with potential passwords) will be unencrypted 146 | #*.pubxml 147 | *.publishproj 148 | 149 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 150 | # checkin your Azure Web App publish settings, but sensitive information contained 151 | # in these scripts will be unencrypted 152 | PublishScripts/ 153 | 154 | # NuGet Packages 155 | *.nupkg 156 | # The packages folder can be ignored because of Package Restore 157 | **/packages/* 158 | # except build/, which is used as an MSBuild target. 159 | !**/packages/build/ 160 | # Uncomment if necessary however generally it will be regenerated when needed 161 | #!**/packages/repositories.config 162 | # NuGet v3's project.json files produces more ignoreable files 163 | *.nuget.props 164 | *.nuget.targets 165 | 166 | # Microsoft Azure Build Output 167 | csx/ 168 | *.build.csdef 169 | 170 | # Microsoft Azure Emulator 171 | ecf/ 172 | rcf/ 173 | 174 | # Windows Store app package directories and files 175 | AppPackages/ 176 | BundleArtifacts/ 177 | Package.StoreAssociation.xml 178 | _pkginfo.txt 179 | 180 | # Visual Studio cache files 181 | # files ending in .cache can be ignored 182 | *.[Cc]ache 183 | # but keep track of directories ending in .cache 184 | !*.[Cc]ache/ 185 | 186 | # Others 187 | ClientBin/ 188 | ~$* 189 | *~ 190 | *.dbmdl 191 | *.dbproj.schemaview 192 | *.jfm 193 | *.pfx 194 | *.publishsettings 195 | node_modules/ 196 | orleans.codegen.cs 197 | 198 | # Since there are multiple workflows, uncomment next line to ignore bower_components 199 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 200 | #bower_components/ 201 | 202 | # RIA/Silverlight projects 203 | Generated_Code/ 204 | 205 | # Backup & report files from converting an old project file 206 | # to a newer Visual Studio version. Backup files are not needed, 207 | # because we have git ;-) 208 | _UpgradeReport_Files/ 209 | Backup*/ 210 | UpgradeLog*.XML 211 | UpgradeLog*.htm 212 | 213 | # SQL Server files 214 | *.mdf 215 | *.ldf 216 | 217 | # Business Intelligence projects 218 | *.rdl.data 219 | *.bim.layout 220 | *.bim_*.settings 221 | 222 | # Microsoft Fakes 223 | FakesAssemblies/ 224 | 225 | # GhostDoc plugin setting file 226 | *.GhostDoc.xml 227 | 228 | # Node.js Tools for Visual Studio 229 | .ntvs_analysis.dat 230 | 231 | # Visual Studio 6 build log 232 | *.plg 233 | 234 | # Visual Studio 6 workspace options file 235 | *.opt 236 | 237 | # Visual Studio LightSwitch build output 238 | **/*.HTMLClient/GeneratedArtifacts 239 | **/*.DesktopClient/GeneratedArtifacts 240 | **/*.DesktopClient/ModelManifest.xml 241 | **/*.Server/GeneratedArtifacts 242 | **/*.Server/ModelManifest.xml 243 | _Pvt_Extensions 244 | 245 | # Paket dependency manager 246 | .paket/paket.exe 247 | paket-files/ 248 | 249 | # FAKE - F# Make 250 | .fake/ 251 | 252 | # JetBrains Rider 253 | .idea/ 254 | *.sln.iml 255 | 256 | # CodeRush 257 | .cr/ 258 | 259 | # Python Tools for Visual Studio (PTVS) 260 | __pycache__/ 261 | *.pyc -------------------------------------------------------------------------------- /src/Terrajobst.StackTraceExplorer/VSPackage.resx: -------------------------------------------------------------------------------- 1 | 2 | 3 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | text/microsoft-resx 110 | 111 | 112 | 2.0 113 | 114 | 115 | System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 116 | 117 | 118 | System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 119 | 120 | -------------------------------------------------------------------------------- /src/Terrajobst.StackTraces/CompilationStackTraceWriter.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Collections.Immutable; 4 | using System.Linq; 5 | using System.Text; 6 | using Microsoft.CodeAnalysis; 7 | 8 | namespace Terrajobst.StackTraces 9 | { 10 | public abstract class CompilationStackTraceWriter : StackTraceWriter 11 | { 12 | private readonly ImmutableArray _compilations; 13 | 14 | public CompilationStackTraceWriter(ImmutableArray compilations) 15 | { 16 | _compilations = compilations; 17 | } 18 | 19 | protected override void WriteMethodText(string methodName) 20 | { 21 | var method = _compilations.Select(c => Resolve(c, methodName)) 22 | .Where(s => s != null) 23 | .FirstOrDefault(); 24 | 25 | if (method == null) 26 | { 27 | WriteText(methodName); 28 | } 29 | else 30 | { 31 | var displayParts = method.ToDisplayParts(SymbolDisplayFormat.CSharpShortErrorMessageFormat); 32 | 33 | foreach (var part in displayParts) 34 | { 35 | var partSymbol = part.Symbol; 36 | var partText = part.ToString(); 37 | 38 | if (partSymbol == null && method.AssociatedSymbol != null) 39 | { 40 | // For some reason, accessors don't have symbols 41 | 42 | if (method.AssociatedSymbol.Kind == SymbolKind.Property || 43 | method.AssociatedSymbol.Kind == SymbolKind.Event) 44 | { 45 | if (part.Kind == SymbolDisplayPartKind.Keyword && 46 | method.Name == partText + "_" + method.AssociatedSymbol.Name) 47 | { 48 | partSymbol = method; 49 | } 50 | } 51 | } 52 | 53 | if (partSymbol != null) 54 | WriteSymbol(partText, partSymbol); 55 | else 56 | WriteText(partText); 57 | } 58 | } 59 | } 60 | 61 | protected abstract void WriteSymbol(string text, ISymbol symbol); 62 | 63 | private static IMethodSymbol Resolve(Compilation compilation, string methodName) 64 | { 65 | var parts = methodName.ToString().Replace(".ctor", "#ctor").Split('.'); 66 | 67 | var currentContainer = (INamespaceOrTypeSymbol)compilation.Assembly.Modules.Single().GlobalNamespace; 68 | 69 | for (var i = 0; currentContainer != null && i < parts.Length - 1; i++) 70 | { 71 | ParseTypeName(parts[i], out var typeOrNamespaceName, out var typeArity); 72 | currentContainer = currentContainer.GetMembers(typeOrNamespaceName) 73 | .Where(n => typeArity == 0 || 74 | n is INamedTypeSymbol t && t.Arity == typeArity) 75 | .FirstOrDefault() as INamespaceOrTypeSymbol; 76 | } 77 | 78 | if (currentContainer == null) 79 | return null; 80 | 81 | var methodNameAndSignature = parts.Last(); 82 | var name = GetMethodName(methodNameAndSignature); 83 | var methodArity = GetMethodArity(methodNameAndSignature); 84 | var parameterTypes = GetMethodParameterTypes(methodNameAndSignature); 85 | 86 | var method = currentContainer.GetMembers(name) 87 | .OfType() 88 | .Where(m => m.Arity == methodArity) 89 | .Where(m => IsMatch(m, parameterTypes)) 90 | .FirstOrDefault(); 91 | return method; 92 | } 93 | 94 | private static void ParseTypeName(string typeName, out string name, out int arity) 95 | { 96 | var backtick = typeName.IndexOf('`'); 97 | 98 | if (backtick < 0) 99 | { 100 | name = typeName; 101 | arity = 0; 102 | } 103 | else 104 | { 105 | name = typeName.Substring(0, backtick); 106 | var arityText = typeName.Substring(backtick + 1); 107 | arity = int.Parse(arityText); 108 | } 109 | } 110 | 111 | private static string GetMethodName(string methodNameAndSignature) 112 | { 113 | var bracket = methodNameAndSignature.IndexOf('['); 114 | var parenthesis = methodNameAndSignature.IndexOf('('); 115 | var nameEnd = bracket >= 0 && bracket < parenthesis 116 | ? bracket 117 | : parenthesis; 118 | var result = methodNameAndSignature.Substring(0, nameEnd); 119 | if (result == "#ctor") 120 | return ".ctor"; 121 | return result; 122 | } 123 | 124 | private static int GetMethodArity(string methodNameAndSignature) 125 | { 126 | var parenthesis = methodNameAndSignature.IndexOf('('); 127 | 128 | var openBracket = methodNameAndSignature.IndexOf('[', 0, parenthesis); 129 | if (openBracket < 0) 130 | return 0; 131 | 132 | var closeBracket = methodNameAndSignature.IndexOf(']', 0, parenthesis); 133 | if (closeBracket < 0) 134 | return 0; 135 | 136 | var result = 1; 137 | for (var i = openBracket; i <= closeBracket; i++) 138 | { 139 | if (methodNameAndSignature[i] == ',') 140 | result++; 141 | } 142 | return result; 143 | } 144 | 145 | private static IReadOnlyList GetMethodParameterTypes(string methodNameAndSignature) 146 | { 147 | var openParenthesis = methodNameAndSignature.IndexOf('('); 148 | var closeParenthesis = methodNameAndSignature.IndexOf(')'); 149 | var signatureStart = openParenthesis + 1; 150 | var signatureLength = closeParenthesis - signatureStart; 151 | var signature = methodNameAndSignature.Substring(signatureStart, signatureLength); 152 | var parameters = signature.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries); 153 | 154 | for (var i = 0; i < parameters.Length; i++) 155 | parameters[i] = parameters[i].Trim(); 156 | 157 | var result = new List(parameters.Length); 158 | foreach (var parameter in parameters) 159 | { 160 | var space = parameter.IndexOf(' '); 161 | var typeName = parameter.Substring(0, space); 162 | result.Add(typeName); 163 | } 164 | 165 | return result; 166 | } 167 | 168 | private static bool IsMatch(IMethodSymbol method, IReadOnlyList parameterTypes) 169 | { 170 | if (method.Parameters.Length != parameterTypes.Count) 171 | return false; 172 | 173 | for (var i = 0; i < method.Parameters.Length; i++) 174 | { 175 | var symbolTypeName = GetTypeName(method.Parameters[i]); 176 | var frameTypename = parameterTypes[i]; 177 | 178 | if (symbolTypeName != frameTypename) 179 | return false; 180 | } 181 | 182 | return true; 183 | } 184 | 185 | private static string GetTypeName(IParameterSymbol symbol) 186 | { 187 | var sb = new StringBuilder(); 188 | if (symbol.Type is IArrayTypeSymbol array) 189 | { 190 | sb.Append(array.ElementType.MetadataName); 191 | sb.Append("[]"); 192 | } 193 | else if (symbol.Type is IPointerTypeSymbol pointer) 194 | { 195 | sb.Append(pointer.PointedAtType.MetadataName); 196 | sb.Append('*'); 197 | } 198 | else 199 | { 200 | sb.Append(symbol.Type.MetadataName); 201 | } 202 | 203 | if (symbol.RefKind != RefKind.None) 204 | sb.Append("&"); 205 | 206 | return sb.ToString(); 207 | } 208 | } 209 | } 210 | -------------------------------------------------------------------------------- /src/Terrajobst.StackTraceExplorer/Terrajobst.StackTraceExplorer.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 15.0 5 | $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) 6 | true 7 | true 8 | ..\Key.snk 9 | 10 | 11 | 12 | Debug 13 | AnyCPU 14 | 2.0 15 | {82b43b9b-a64c-4715-b499-d71e9ca2bd60};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC} 16 | {7BD4BA8A-B0E0-42DB-A035-C57C9430B447} 17 | Library 18 | Properties 19 | Terrajobst.StackTraceExplorer 20 | Terrajobst.StackTraceExplorer 21 | v4.6.1 22 | true 23 | true 24 | true 25 | true 26 | true 27 | false 28 | Program 29 | $(DevEnvDir)devenv.exe 30 | /rootsuffix Exp 31 | 32 | 33 | true 34 | full 35 | false 36 | bin\Debug\ 37 | DEBUG;TRACE 38 | prompt 39 | 4 40 | 41 | 42 | pdbonly 43 | true 44 | bin\Release\ 45 | TRACE 46 | prompt 47 | 4 48 | 49 | 50 | 51 | 52 | StackTraceExplorerPaneControl.xaml 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | Designer 63 | 64 | 65 | 66 | 67 | Always 68 | true 69 | 70 | 71 | Menus.ctmenu 72 | 73 | 74 | 75 | 76 | 77 | Designer 78 | MSBuild:Compile 79 | 80 | 81 | 82 | 83 | False 84 | 85 | 86 | False 87 | 88 | 89 | False 90 | 91 | 92 | False 93 | 94 | 95 | 96 | False 97 | 98 | 99 | 100 | 101 | False 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | true 116 | VSPackage 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | {e82f0cb5-df4c-4842-8307-0769537eb098} 147 | Terrajobst.StackTraces 148 | BuiltProjectOutputGroup%3bBuiltProjectOutputGroupDependencies%3bGetCopyToOutputDirectoryItems%3bSatelliteDllsProjectOutputGroup%3b 149 | DebugSymbolsProjectOutputGroup%3b 150 | 151 | 152 | 153 | 154 | -------------------------------------------------------------------------------- /src/Terrajobst.StackTraces.Tests/StackTraceWriterTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Immutable; 3 | using System.IO; 4 | using System.Linq; 5 | using System.Reflection; 6 | using System.Text; 7 | using Microsoft.CodeAnalysis; 8 | using Microsoft.CodeAnalysis.CSharp; 9 | using Terrajobst.StackTraces.Tests.Helpers; 10 | using Xunit; 11 | 12 | namespace Terrajobst.StackTraces.Tests 13 | { 14 | public class StackTraceWriterTests 15 | { 16 | private static void AssertIsMatch(string sourceWithMarkers, string expectedStackTrace) 17 | { 18 | var annotatedSource = AnnotatedText.Parse(sourceWithMarkers); 19 | 20 | var syntaxTree = CSharpSyntaxTree.ParseText(annotatedSource.Text); 21 | var mscorlib = MetadataReference.CreateFromFile(typeof(object).Assembly.Location); 22 | var options = new CSharpCompilationOptions(OutputKind.ConsoleApplication, allowUnsafe: true); 23 | var compilation = CSharpCompilation.Create("dummy", new[] { syntaxTree }, new[] { mscorlib }, options); 24 | 25 | var runtimeStackTrace = RunAndGetStackTrace(compilation); 26 | 27 | WriteStackTraceAndMethods(compilation, runtimeStackTrace, out var producedStackTrace, out var methods); 28 | 29 | Assert.Equal(AnnotatedText.NormalizeCode(expectedStackTrace), producedStackTrace); 30 | 31 | var root = syntaxTree.GetRoot(); 32 | var markedNodes = annotatedSource.Spans.Select(s => root.FindNode(s)).Reverse().ToArray(); 33 | var semanticModel = compilation.GetSemanticModel(syntaxTree); 34 | 35 | Assert.Equal(markedNodes.Length, methods.Length); 36 | 37 | for (var i = 0; i < markedNodes.Length; i++) 38 | { 39 | var expectedSymbol = semanticModel.GetDeclaredSymbol(markedNodes[i]); 40 | Assert.NotNull(expectedSymbol); 41 | 42 | var actualSymbol = methods[i]; 43 | Assert.Equal(expectedSymbol, methods[i]); 44 | } 45 | } 46 | 47 | private static string RunAndGetStackTrace(CSharpCompilation compilation) 48 | { 49 | var actualStackTrace = (string)null; 50 | 51 | using (var ms = new MemoryStream()) 52 | { 53 | var result = compilation.Emit(ms); 54 | 55 | if (!result.Success) 56 | { 57 | var sb = new StringBuilder(); 58 | foreach (var d in result.Diagnostics.Where(d => d.Severity == DiagnosticSeverity.Error)) 59 | sb.AppendLine(d.ToString()); 60 | 61 | throw new Exception("Failing to compile:" + sb); 62 | } 63 | 64 | Assert.True(result.Success); 65 | ms.Seek(0, SeekOrigin.Begin); 66 | var assembly = Assembly.Load(ms.ToArray()); 67 | 68 | try 69 | { 70 | assembly.EntryPoint.Invoke(null, null); 71 | } 72 | catch (TargetInvocationException ex) 73 | { 74 | actualStackTrace = ex.InnerException.ToString(); 75 | } 76 | 77 | Assert.NotNull(actualStackTrace); 78 | } 79 | 80 | return actualStackTrace; 81 | } 82 | 83 | private static void WriteStackTraceAndMethods(CSharpCompilation compilation, string actualStackTrace, out string stackTrace, out ImmutableArray methods) 84 | { 85 | var recorder = new RecordingCompilationStackTraceWriter(compilation); 86 | StackTraceWriter.Write(actualStackTrace, recorder); 87 | 88 | stackTrace = recorder.GetText(); 89 | methods = recorder.GetMethods(); 90 | } 91 | 92 | [Fact] 93 | public void StackTraceWriter_Constructor() 94 | { 95 | var source = @" 96 | using System; 97 | 98 | public static class Program 99 | { 100 | public static void {{Main}}() 101 | { 102 | new Customer(); 103 | } 104 | } 105 | 106 | public class Customer 107 | { 108 | public {{Customer}}() 109 | { 110 | throw new Exception(""Boom!""); 111 | } 112 | } 113 | "; 114 | 115 | var expected = @" 116 | System.Exception: Boom! 117 | at Customer.Customer() 118 | at Program.Main() 119 | "; 120 | 121 | AssertIsMatch(source, expected); 122 | } 123 | 124 | [Fact] 125 | public void StackTraceWriter_Method() 126 | { 127 | var source = @" 128 | using System; 129 | 130 | public static class Program 131 | { 132 | public static void {{Main}}() 133 | { 134 | Test(); 135 | } 136 | 137 | public static void {{Test}}() 138 | { 139 | throw new Exception(""Boom!""); 140 | } 141 | } 142 | "; 143 | 144 | var expected = @" 145 | System.Exception: Boom! 146 | at Program.Test() 147 | at Program.Main() 148 | "; 149 | 150 | AssertIsMatch(source, expected); 151 | } 152 | 153 | [Fact] 154 | public void StackTraceWriter_Method_GenericMethod() 155 | { 156 | var source = @" 157 | using System; 158 | 159 | public static class Program 160 | { 161 | public static void {{Main}}() 162 | { 163 | Test(1.0f); 164 | } 165 | 166 | public static void {{Test}}(T x) 167 | { 168 | throw new Exception(""Boom!""); 169 | } 170 | } 171 | "; 172 | 173 | var expected = @" 174 | System.Exception: Boom! 175 | at Program.Test(T) 176 | at Program.Main() 177 | "; 178 | 179 | AssertIsMatch(source, expected); 180 | } 181 | 182 | [Fact] 183 | public void StackTraceWriter_Method_GenericType() 184 | { 185 | var source = @" 186 | using System; 187 | 188 | public static class Program 189 | { 190 | public static void {{Main}}() 191 | { 192 | var g = new GenericType(); 193 | g.Test(2); 194 | } 195 | } 196 | 197 | class GenericType 198 | { 199 | public void {{Test}}(T value) 200 | { 201 | throw new Exception(""Boom!""); 202 | } 203 | } 204 | "; 205 | 206 | var expected = @" 207 | System.Exception: Boom! 208 | at GenericType.Test(T) 209 | at Program.Main() 210 | "; 211 | 212 | AssertIsMatch(source, expected); 213 | } 214 | 215 | [Fact] 216 | public void StackTraceWriter_Method_Overloaded() 217 | { 218 | var source = @" 219 | using System; 220 | 221 | public static class Program 222 | { 223 | public static void {{Main}}() 224 | { 225 | Test(2); 226 | } 227 | 228 | static void {{Test}}(int value) 229 | { 230 | throw new Exception(""Boom!""); 231 | } 232 | 233 | static void Test(float value) 234 | { 235 | } 236 | } 237 | "; 238 | 239 | var expected = @" 240 | System.Exception: Boom! 241 | at Program.Test(int) 242 | at Program.Main() 243 | "; 244 | 245 | AssertIsMatch(source, expected); 246 | } 247 | 248 | [Fact] 249 | public void StackTraceWriter_Method_Argument_Out() 250 | { 251 | var source = @" 252 | using System; 253 | 254 | public static class Program 255 | { 256 | public static void {{Main}}() 257 | { 258 | Test(out var x); 259 | } 260 | 261 | static void {{Test}}(out int value) 262 | { 263 | throw new Exception(""Boom!""); 264 | } 265 | } 266 | "; 267 | 268 | var expected = @" 269 | System.Exception: Boom! 270 | at Program.Test(out int) 271 | at Program.Main() 272 | "; 273 | 274 | AssertIsMatch(source, expected); 275 | } 276 | 277 | [Fact] 278 | public void StackTraceWriter_Method_Argument_Ref() 279 | { 280 | var source = @" 281 | using System; 282 | 283 | public static class Program 284 | { 285 | public static void {{Main}}() 286 | { 287 | var x = 0; 288 | Test(ref x); 289 | } 290 | 291 | static void {{Test}}(ref int value) 292 | { 293 | throw new Exception(""Boom!""); 294 | } 295 | } 296 | "; 297 | 298 | var expected = @" 299 | System.Exception: Boom! 300 | at Program.Test(ref int) 301 | at Program.Main() 302 | "; 303 | 304 | AssertIsMatch(source, expected); 305 | } 306 | 307 | [Fact] 308 | public void StackTraceWriter_Method_Argument_Array() 309 | { 310 | var source = @" 311 | using System; 312 | 313 | public static class Program 314 | { 315 | public static void {{Main}}() 316 | { 317 | Test(null); 318 | } 319 | 320 | static void {{Test}}(int[] value) 321 | { 322 | throw new Exception(""Boom!""); 323 | } 324 | } 325 | "; 326 | 327 | var expected = @" 328 | System.Exception: Boom! 329 | at Program.Test(int[]) 330 | at Program.Main() 331 | "; 332 | 333 | AssertIsMatch(source, expected); 334 | } 335 | 336 | [Fact] 337 | public void StackTraceWriter_Method_Argument_Params() 338 | { 339 | var source = @" 340 | using System; 341 | 342 | public static class Program 343 | { 344 | public static void {{Main}}() 345 | { 346 | Test(); 347 | } 348 | 349 | static void {{Test}}(params int[] value) 350 | { 351 | throw new Exception(""Boom!""); 352 | } 353 | } 354 | "; 355 | 356 | var expected = @" 357 | System.Exception: Boom! 358 | at Program.Test(params int[]) 359 | at Program.Main() 360 | "; 361 | 362 | AssertIsMatch(source, expected); 363 | } 364 | 365 | [Fact] 366 | public void StackTraceWriter_Method_Argument_GenericInstance() 367 | { 368 | var source = @" 369 | using System; 370 | using System.Collections.Generic; 371 | 372 | public static class Program 373 | { 374 | public static void {{Main}}() 375 | { 376 | Test(null); 377 | } 378 | 379 | static void {{Test}}(IEnumerable value) 380 | { 381 | throw new Exception(""Boom!""); 382 | } 383 | } 384 | "; 385 | 386 | var expected = @" 387 | System.Exception: Boom! 388 | at Program.Test(IEnumerable) 389 | at Program.Main() 390 | "; 391 | 392 | AssertIsMatch(source, expected); 393 | } 394 | 395 | [Fact] 396 | public void StackTraceWriter_Method_Argument_GenericInstance_Array() 397 | { 398 | var source = @" 399 | using System; 400 | using System.Collections.Generic; 401 | 402 | public static class Program 403 | { 404 | public static void {{Main}}() 405 | { 406 | Test(null); 407 | } 408 | 409 | static void {{Test}}(IEnumerable[] value) 410 | { 411 | throw new Exception(""Boom!""); 412 | } 413 | } 414 | "; 415 | 416 | var expected = @" 417 | System.Exception: Boom! 418 | at Program.Test(IEnumerable[]) 419 | at Program.Main() 420 | "; 421 | 422 | AssertIsMatch(source, expected); 423 | } 424 | 425 | [Fact] 426 | public void StackTraceWriter_Method_Argument_Pointer() 427 | { 428 | var source = @" 429 | using System; 430 | using System.Collections.Generic; 431 | 432 | unsafe static class Program 433 | { 434 | public static void {{Main}}() 435 | { 436 | Test(null); 437 | } 438 | 439 | unsafe static void {{Test}}(int* value) 440 | { 441 | throw new Exception(""Boom!""); 442 | } 443 | } 444 | "; 445 | 446 | var expected = @" 447 | System.Exception: Boom! 448 | at Program.Test(int*) 449 | at Program.Main() 450 | "; 451 | 452 | AssertIsMatch(source, expected); 453 | } 454 | 455 | [Fact] 456 | public void StackTraceWriter_Property_Getter() 457 | { 458 | var source = @" 459 | using System; 460 | 461 | public static class Program 462 | { 463 | public static void {{Main}}() 464 | { 465 | Console.WriteLine(X); 466 | } 467 | 468 | public static int X 469 | { 470 | {{get}} 471 | { 472 | throw new Exception(""Boom!""); 473 | } 474 | } 475 | } 476 | "; 477 | 478 | var expected = @" 479 | System.Exception: Boom! 480 | at Program.X.get 481 | at Program.Main() 482 | "; 483 | 484 | AssertIsMatch(source, expected); 485 | } 486 | 487 | [Fact] 488 | public void StackTraceWriter_Property_Setter() 489 | { 490 | var source = @" 491 | using System; 492 | 493 | public static class Program 494 | { 495 | public static void {{Main}}() 496 | { 497 | X = 10; 498 | } 499 | 500 | public static int X 501 | { 502 | get => 42; 503 | {{set}} 504 | { 505 | throw new Exception(""Boom!""); 506 | } 507 | } 508 | } 509 | "; 510 | 511 | var expected = @" 512 | System.Exception: Boom! 513 | at Program.X.set 514 | at Program.Main() 515 | "; 516 | 517 | AssertIsMatch(source, expected); 518 | } 519 | 520 | [Fact] 521 | public void StackTraceWriter_Event_Adder() 522 | { 523 | var source = @" 524 | using System; 525 | 526 | public static class Program 527 | { 528 | public static void {{Main}}() 529 | { 530 | Changed += null; 531 | } 532 | 533 | public static event EventHandler Changed 534 | { 535 | {{add}} 536 | { 537 | throw new Exception(""Boom!""); 538 | } 539 | remove { } 540 | } 541 | } 542 | "; 543 | 544 | var expected = @" 545 | System.Exception: Boom! 546 | at Program.Changed.add 547 | at Program.Main() 548 | "; 549 | 550 | AssertIsMatch(source, expected); 551 | } 552 | 553 | [Fact] 554 | public void StackTraceWriter_Event_Remover() 555 | { 556 | var source = @" 557 | using System; 558 | 559 | public static class Program 560 | { 561 | public static void {{Main}}() 562 | { 563 | Changed -= null; 564 | } 565 | 566 | public static event EventHandler Changed 567 | { 568 | add { } 569 | {{remove}} 570 | { 571 | throw new Exception(""Boom!""); 572 | } 573 | } 574 | } 575 | "; 576 | 577 | var expected = @" 578 | System.Exception: Boom! 579 | at Program.Changed.remove 580 | at Program.Main() 581 | "; 582 | 583 | AssertIsMatch(source, expected); 584 | } 585 | } 586 | } 587 | --------------------------------------------------------------------------------