From a36dc12591428e0d5e7b357a93ab90abf943d90a Mon Sep 17 00:00:00 2001 From: Andrew Hall Date: Mon, 10 Mar 2025 18:49:08 -0700 Subject: [PATCH 1/4] Support additional documents when navigating to a file and line --- .../IDocumentNavigationServiceExtensions.cs | 2 +- .../IUnitTestingStackTraceServiceAccessor.cs | 2 +- .../UnitTestingStackTraceServiceAccessor.cs | 2 +- .../IStackTraceExplorerService.cs | 2 +- .../StackTraceExplorerService.cs | 24 +++++---- .../StackTraceExplorer/StackFrameViewModel.cs | 51 ++++++++----------- .../VisualStudioDocumentNavigationService.cs | 9 ++-- 7 files changed, 44 insertions(+), 48 deletions(-) diff --git a/src/EditorFeatures/Core/Navigation/IDocumentNavigationServiceExtensions.cs b/src/EditorFeatures/Core/Navigation/IDocumentNavigationServiceExtensions.cs index fbaa8a20e8f98..d2bc7ec0c15da 100644 --- a/src/EditorFeatures/Core/Navigation/IDocumentNavigationServiceExtensions.cs +++ b/src/EditorFeatures/Core/Navigation/IDocumentNavigationServiceExtensions.cs @@ -75,7 +75,7 @@ public static async Task TryNavigateToLineAndOffsetAsync( // Navigation should not change the context of linked files and Shared Projects. documentId = workspace.GetDocumentIdInCurrentContext(documentId); - var document = workspace.CurrentSolution.GetDocument(documentId); + var document = workspace.CurrentSolution.GetTextDocument(documentId); if (document is null) return false; diff --git a/src/Features/Core/Portable/ExternalAccess/UnitTesting/API/IUnitTestingStackTraceServiceAccessor.cs b/src/Features/Core/Portable/ExternalAccess/UnitTesting/API/IUnitTestingStackTraceServiceAccessor.cs index 3fe7fe959d471..5a8618b337f0f 100644 --- a/src/Features/Core/Portable/ExternalAccess/UnitTesting/API/IUnitTestingStackTraceServiceAccessor.cs +++ b/src/Features/Core/Portable/ExternalAccess/UnitTesting/API/IUnitTestingStackTraceServiceAccessor.cs @@ -13,6 +13,6 @@ internal interface IUnitTestingStackTraceServiceAccessor : IWorkspaceService { Task> TryParseAsync(string input, Workspace workspace, CancellationToken cancellationToken); Task TryFindMethodDefinitionAsync(Workspace workspace, UnitTestingParsedFrameWrapper parsedFrame, CancellationToken cancellationToken); - (Document? document, int lineNumber) GetDocumentAndLine(Workspace workspace, UnitTestingParsedFrameWrapper parsedFrame); + (TextDocument? document, int lineNumber) GetDocumentAndLine(Workspace workspace, UnitTestingParsedFrameWrapper parsedFrame); Task TryNavigateToAsync(Workspace workspace, UnitTestingDefinitionItemWrapper definitionItem, bool showInPreviewTab, bool activateTab, CancellationToken cancellationToken); } diff --git a/src/Features/Core/Portable/ExternalAccess/UnitTesting/UnitTestingStackTraceServiceAccessor.cs b/src/Features/Core/Portable/ExternalAccess/UnitTesting/UnitTestingStackTraceServiceAccessor.cs index aeb2a32036ddb..4a9785137ec1d 100644 --- a/src/Features/Core/Portable/ExternalAccess/UnitTesting/UnitTestingStackTraceServiceAccessor.cs +++ b/src/Features/Core/Portable/ExternalAccess/UnitTesting/UnitTestingStackTraceServiceAccessor.cs @@ -19,7 +19,7 @@ internal sealed class UnitTestingStackTraceServiceAccessor( { private readonly IStackTraceExplorerService _stackTraceExplorerService = stackTraceExplorerService; - public (Document? document, int lineNumber) GetDocumentAndLine(Workspace workspace, UnitTestingParsedFrameWrapper parsedFrame) + public (TextDocument? document, int lineNumber) GetDocumentAndLine(Workspace workspace, UnitTestingParsedFrameWrapper parsedFrame) => _stackTraceExplorerService.GetDocumentAndLine(workspace.CurrentSolution, parsedFrame.UnderlyingObject); public async Task TryFindMethodDefinitionAsync(Workspace workspace, UnitTestingParsedFrameWrapper parsedFrame, CancellationToken cancellationToken) diff --git a/src/Features/Core/Portable/StackTraceExplorer/IStackTraceExplorerService.cs b/src/Features/Core/Portable/StackTraceExplorer/IStackTraceExplorerService.cs index 68f42960dfc3b..d8aabcce75751 100644 --- a/src/Features/Core/Portable/StackTraceExplorer/IStackTraceExplorerService.cs +++ b/src/Features/Core/Portable/StackTraceExplorer/IStackTraceExplorerService.cs @@ -16,7 +16,7 @@ internal interface IStackTraceExplorerService : IWorkspaceService /// in a solution. Looks for an exact filepath match first, then defaults to /// a best guess. /// - (Document? document, int line) GetDocumentAndLine(Solution solution, ParsedFrame frame); + (TextDocument? document, int line) GetDocumentAndLine(Solution solution, ParsedFrame frame); Task TryFindDefinitionAsync(Solution solution, ParsedFrame frame, StackFrameSymbolPart symbolPart, CancellationToken cancellationToken); } diff --git a/src/Features/Core/Portable/StackTraceExplorer/StackTraceExplorerService.cs b/src/Features/Core/Portable/StackTraceExplorer/StackTraceExplorerService.cs index e809b29c3f232..239f39eb191cf 100644 --- a/src/Features/Core/Portable/StackTraceExplorer/StackTraceExplorerService.cs +++ b/src/Features/Core/Portable/StackTraceExplorer/StackTraceExplorerService.cs @@ -6,6 +6,7 @@ using System.Collections.Immutable; using System.Composition; using System.IO; +using System.Linq; using System.Threading; using System.Threading.Tasks; using Microsoft.CodeAnalysis.EmbeddedLanguages.StackFrame; @@ -17,15 +18,11 @@ namespace Microsoft.CodeAnalysis.StackTraceExplorer; [ExportWorkspaceService(typeof(IStackTraceExplorerService)), Shared] -internal sealed class StackTraceExplorerService : IStackTraceExplorerService +[method: ImportingConstructor] +[method: System.Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] +internal sealed class StackTraceExplorerService() : IStackTraceExplorerService { - [ImportingConstructor] - [System.Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] - public StackTraceExplorerService() - { - } - - public (Document? document, int line) GetDocumentAndLine(Solution solution, ParsedFrame frame) + public (TextDocument? document, int line) GetDocumentAndLine(Solution solution, ParsedFrame frame) { if (frame is ParsedStackFrame parsedFrame) { @@ -73,7 +70,7 @@ public StackTraceExplorerService() return await StackTraceExplorerUtilities.GetDefinitionAsync(solution, parsedFrame.Root, symbolPart, cancellationToken).ConfigureAwait(false); } - private static ImmutableArray GetFileMatches(Solution solution, StackFrameCompilationUnit root, out int lineNumber) + private static ImmutableArray GetFileMatches(Solution solution, StackFrameCompilationUnit root, out int lineNumber) { lineNumber = 0; if (root.FileInformationExpression is null) @@ -87,11 +84,16 @@ private static ImmutableArray GetFileMatches(Solution solution, StackF lineNumber = int.Parse(lineString); var documentName = Path.GetFileName(fileName); - var potentialMatches = new HashSet(); + var potentialMatches = new HashSet(); foreach (var project in solution.Projects) { - foreach (var document in project.Documents) + // As of writing there is no way to get all the documents for a specific project + // so we need to check both the main and additional documents. If more document types + // get added this likely will need to be updated. + var allDocuments = project.Documents.Concat(project.AdditionalDocuments); + + foreach (var document in allDocuments) { if (document.FilePath == fileName) { diff --git a/src/VisualStudio/Core/Def/StackTraceExplorer/StackFrameViewModel.cs b/src/VisualStudio/Core/Def/StackTraceExplorer/StackFrameViewModel.cs index b97644aa038cc..556e1eaaf469c 100644 --- a/src/VisualStudio/Core/Def/StackTraceExplorer/StackFrameViewModel.cs +++ b/src/VisualStudio/Core/Def/StackTraceExplorer/StackFrameViewModel.cs @@ -28,31 +28,22 @@ namespace Microsoft.VisualStudio.LanguageServices.StackTraceExplorer; using StackFrameToken = EmbeddedSyntaxToken; using StackFrameTrivia = EmbeddedSyntaxTrivia; -internal class StackFrameViewModel : FrameViewModel +internal class StackFrameViewModel( + ParsedStackFrame frame, + IThreadingContext threadingContext, + Workspace workspace, + IClassificationFormatMap formatMap, + ClassificationTypeMap typeMap) : FrameViewModel(formatMap, typeMap) { - private readonly ParsedStackFrame _frame; - private readonly IThreadingContext _threadingContext; - private readonly Workspace _workspace; - private readonly IStackTraceExplorerService _stackExplorerService; + private readonly ParsedStackFrame _frame = frame; + private readonly IThreadingContext _threadingContext = threadingContext; + private readonly Workspace _workspace = workspace; + private readonly IStackTraceExplorerService _stackExplorerService = workspace.Services.GetRequiredService(); private readonly Dictionary _definitionCache = []; - private Document? _cachedDocument; + private TextDocument? _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(); - } - public override bool ShowMouseOver => true; public void NavigateToClass() @@ -112,14 +103,11 @@ public async Task NavigateToFileAsync(CancellationToken cancellationToken) { try { - var (document, lineNumber) = GetDocumentAndLine(); + var (textDocument, lineNumber) = GetDocumentAndLine(); - if (document is not null) + if (textDocument 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); + var sourceText = await textDocument.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 @@ -131,8 +119,13 @@ public async Task NavigateToFileAsync(CancellationToken cancellationToken) if (navigationService is null) return; - var location = await navigationService.TryNavigateToLineAndOffsetAsync( - _threadingContext, _workspace, document.Id, lineNumber - 1, offset: 0, options, cancellationToken).ConfigureAwait(false); + // While navigating do not activate the tab, which will change focus from the tool window + var options = new NavigationOptions(PreferProvisionalTab: true, ActivateTab: false); + + await navigationService.TryNavigateToLineAndOffsetAsync( + _threadingContext, _workspace, textDocument.Id, lineNumber - 1, offset: 0, options, cancellationToken) + .ConfigureAwait(false); + } } catch (Exception ex) when (FatalError.ReportAndCatchUnlessCanceled(ex, cancellationToken)) @@ -208,7 +201,7 @@ protected override IEnumerable CreateInlines() yield return MakeClassifiedRun(ClassificationTypeNames.Text, _frame.Root.EndOfLineToken.ToFullString()); } - private (Document? document, int lineNumber) GetDocumentAndLine() + private (TextDocument? document, int lineNumber) GetDocumentAndLine() { if (_cachedDocument is not null) { diff --git a/src/VisualStudio/Core/Def/Workspace/VisualStudioDocumentNavigationService.cs b/src/VisualStudio/Core/Def/Workspace/VisualStudioDocumentNavigationService.cs index 97ec5a757a3ab..324cf6055ff35 100644 --- a/src/VisualStudio/Core/Def/Workspace/VisualStudioDocumentNavigationService.cs +++ b/src/VisualStudio/Core/Def/Workspace/VisualStudioDocumentNavigationService.cs @@ -189,8 +189,8 @@ static VsTextSpan GetVsTextSpanFromPosition(SourceText text, int position, int v documentId = workspace.GetDocumentIdInCurrentContext(documentId); var solution = workspace.CurrentSolution; - var document = solution.GetDocument(documentId); - if (document == null) + var textDocument = await solution.GetRequiredTextDocumentAsync(documentId, cancellationToken).ConfigureAwait(false); + if (textDocument is SourceGeneratedDocument) { var project = solution.GetProject(documentId.ProjectId); if (project is null) @@ -210,14 +210,15 @@ static VsTextSpan GetVsTextSpanFromPosition(SourceText text, int position, int v } // Before attempting to open the document, check if the location maps to a different file that should be opened instead. - var spanMappingService = document.DocumentServiceProvider.GetService(); - if (spanMappingService != null) + if (textDocument is Document document && + textDocument.DocumentServiceProvider.GetService() is ISpanMappingService spanMappingService) { var mappedSpanResult = await GetMappedSpanAsync( spanMappingService, document, await getTextSpanForMappingAsync(document).ConfigureAwait(false), cancellationToken).ConfigureAwait(false); + if (mappedSpanResult is { IsDefault: false } mappedSpan) { // Check if the mapped file matches one already in the workspace. From b0b2922ba4c27afb9e5fb077e74fa871b62f10dd Mon Sep 17 00:00:00 2001 From: Andrew Hall Date: Tue, 11 Mar 2025 16:38:15 -0700 Subject: [PATCH 2/4] PR feedback --- .../IUnitTestingStackTraceServiceAccessor.cs | 3 ++- .../UnitTestingStackTraceServiceAccessor.cs | 13 +++++++++++- .../StackTraceExplorerService.cs | 16 +++++++++------ .../VisualStudioDocumentNavigationService.cs | 20 +++++++------------ 4 files changed, 31 insertions(+), 21 deletions(-) diff --git a/src/Features/Core/Portable/ExternalAccess/UnitTesting/API/IUnitTestingStackTraceServiceAccessor.cs b/src/Features/Core/Portable/ExternalAccess/UnitTesting/API/IUnitTestingStackTraceServiceAccessor.cs index 5a8618b337f0f..b043ef3e96381 100644 --- a/src/Features/Core/Portable/ExternalAccess/UnitTesting/API/IUnitTestingStackTraceServiceAccessor.cs +++ b/src/Features/Core/Portable/ExternalAccess/UnitTesting/API/IUnitTestingStackTraceServiceAccessor.cs @@ -13,6 +13,7 @@ internal interface IUnitTestingStackTraceServiceAccessor : IWorkspaceService { Task> TryParseAsync(string input, Workspace workspace, CancellationToken cancellationToken); Task TryFindMethodDefinitionAsync(Workspace workspace, UnitTestingParsedFrameWrapper parsedFrame, CancellationToken cancellationToken); - (TextDocument? document, int lineNumber) GetDocumentAndLine(Workspace workspace, UnitTestingParsedFrameWrapper parsedFrame); + (Document? document, int lineNumber) GetDocumentAndLine(Workspace workspace, UnitTestingParsedFrameWrapper parsedFrame); + (TextDocument? textDocument, int lineNumber) GetTextDocumentAndLine(Workspace workspace, UnitTestingParsedFrameWrapper parsedFrame); Task TryNavigateToAsync(Workspace workspace, UnitTestingDefinitionItemWrapper definitionItem, bool showInPreviewTab, bool activateTab, CancellationToken cancellationToken); } diff --git a/src/Features/Core/Portable/ExternalAccess/UnitTesting/UnitTestingStackTraceServiceAccessor.cs b/src/Features/Core/Portable/ExternalAccess/UnitTesting/UnitTestingStackTraceServiceAccessor.cs index 4a9785137ec1d..394da78f82438 100644 --- a/src/Features/Core/Portable/ExternalAccess/UnitTesting/UnitTestingStackTraceServiceAccessor.cs +++ b/src/Features/Core/Portable/ExternalAccess/UnitTesting/UnitTestingStackTraceServiceAccessor.cs @@ -19,9 +19,20 @@ internal sealed class UnitTestingStackTraceServiceAccessor( { private readonly IStackTraceExplorerService _stackTraceExplorerService = stackTraceExplorerService; - public (TextDocument? document, int lineNumber) GetDocumentAndLine(Workspace workspace, UnitTestingParsedFrameWrapper parsedFrame) + public (TextDocument? textDocument, int lineNumber) GetTextDocumentAndLine(Workspace workspace, UnitTestingParsedFrameWrapper parsedFrame) => _stackTraceExplorerService.GetDocumentAndLine(workspace.CurrentSolution, parsedFrame.UnderlyingObject); + public (Document? document, int lineNumber) GetDocumentAndLine(Workspace workspace, UnitTestingParsedFrameWrapper parsedFrame) + { + var (textDocument, lineNumber) = GetTextDocumentAndLine(workspace, parsedFrame); + if (textDocument is Document document) + { + return (document, lineNumber); + } + + return (null, default); + } + public async Task TryFindMethodDefinitionAsync(Workspace workspace, UnitTestingParsedFrameWrapper parsedFrame, CancellationToken cancellationToken) { var definition = await _stackTraceExplorerService.TryFindDefinitionAsync(workspace.CurrentSolution, parsedFrame.UnderlyingObject, StackFrameSymbolPart.Method, cancellationToken).ConfigureAwait(false); diff --git a/src/Features/Core/Portable/StackTraceExplorer/StackTraceExplorerService.cs b/src/Features/Core/Portable/StackTraceExplorer/StackTraceExplorerService.cs index 239f39eb191cf..83316bcd64744 100644 --- a/src/Features/Core/Portable/StackTraceExplorer/StackTraceExplorerService.cs +++ b/src/Features/Core/Portable/StackTraceExplorer/StackTraceExplorerService.cs @@ -13,6 +13,7 @@ using Microsoft.CodeAnalysis.FindUsages; using Microsoft.CodeAnalysis.Host.Mef; using Microsoft.CodeAnalysis.Remote; +using Microsoft.CodeAnalysis.Shared.Extensions; using Roslyn.Utilities; namespace Microsoft.CodeAnalysis.StackTraceExplorer; @@ -83,6 +84,14 @@ private static ImmutableArray GetFileMatches(Solution solution, St RoslynDebug.AssertNotNull(lineString); lineNumber = int.Parse(lineString); + var documentId = solution.GetDocumentIdsWithFilePath(fileName).FirstOrDefault(); + + if (documentId is not null) + { + var document = solution.GetRequiredDocument(documentId); + return [document]; + } + var documentName = Path.GetFileName(fileName); var potentialMatches = new HashSet(); @@ -95,12 +104,7 @@ private static ImmutableArray GetFileMatches(Solution solution, St foreach (var document in allDocuments) { - if (document.FilePath == fileName) - { - return [document]; - } - - else if (document.Name == documentName) + if (document.Name == documentName) { potentialMatches.Add(document); } diff --git a/src/VisualStudio/Core/Def/Workspace/VisualStudioDocumentNavigationService.cs b/src/VisualStudio/Core/Def/Workspace/VisualStudioDocumentNavigationService.cs index 324cf6055ff35..ced22ef8af18a 100644 --- a/src/VisualStudio/Core/Def/Workspace/VisualStudioDocumentNavigationService.cs +++ b/src/VisualStudio/Core/Def/Workspace/VisualStudioDocumentNavigationService.cs @@ -189,21 +189,15 @@ static VsTextSpan GetVsTextSpanFromPosition(SourceText text, int position, int v documentId = workspace.GetDocumentIdInCurrentContext(documentId); var solution = workspace.CurrentSolution; - var textDocument = await solution.GetRequiredTextDocumentAsync(documentId, cancellationToken).ConfigureAwait(false); - if (textDocument is SourceGeneratedDocument) - { - var project = solution.GetProject(documentId.ProjectId); - if (project is null) - { - // This is a source generated document shown in Solution Explorer, but is no longer valid since - // the configuration and/or platform changed since the last generation completed. - return null; - } + var textDocument = await solution.GetTextDocumentAsync(documentId, cancellationToken).ConfigureAwait(false); - var generatedDocument = await project.GetSourceGeneratedDocumentAsync(documentId, cancellationToken).ConfigureAwait(false); - if (generatedDocument == null) - return null; + if (textDocument is null) + { + return null; + } + if (textDocument is SourceGeneratedDocument generatedDocument) + { return _sourceGeneratedFileManager.Value.GetNavigationCallback( generatedDocument, await getTextSpanForMappingAsync(generatedDocument).ConfigureAwait(false)); From 412475d10186ba739dc978b586c96a6ee1f4f613 Mon Sep 17 00:00:00 2001 From: Andrew Hall Date: Tue, 11 Mar 2025 16:44:33 -0700 Subject: [PATCH 3/4] Update src/VisualStudio/Core/Def/StackTraceExplorer/StackFrameViewModel.cs Co-authored-by: Joey Robichaud --- .../Core/Def/StackTraceExplorer/StackFrameViewModel.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/VisualStudio/Core/Def/StackTraceExplorer/StackFrameViewModel.cs b/src/VisualStudio/Core/Def/StackTraceExplorer/StackFrameViewModel.cs index 556e1eaaf469c..bc18e7a50097c 100644 --- a/src/VisualStudio/Core/Def/StackTraceExplorer/StackFrameViewModel.cs +++ b/src/VisualStudio/Core/Def/StackTraceExplorer/StackFrameViewModel.cs @@ -125,7 +125,6 @@ public async Task NavigateToFileAsync(CancellationToken cancellationToken) await navigationService.TryNavigateToLineAndOffsetAsync( _threadingContext, _workspace, textDocument.Id, lineNumber - 1, offset: 0, options, cancellationToken) .ConfigureAwait(false); - } } catch (Exception ex) when (FatalError.ReportAndCatchUnlessCanceled(ex, cancellationToken)) From 45c0e103f76f36bed6004f836d3dcfeae4bfae0d Mon Sep 17 00:00:00 2001 From: Andrew Hall Date: Tue, 11 Mar 2025 16:48:07 -0700 Subject: [PATCH 4/4] Ignore case for filename --- .../Portable/StackTraceExplorer/StackTraceExplorerService.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/Features/Core/Portable/StackTraceExplorer/StackTraceExplorerService.cs b/src/Features/Core/Portable/StackTraceExplorer/StackTraceExplorerService.cs index 83316bcd64744..ba9ccd34b6f6b 100644 --- a/src/Features/Core/Portable/StackTraceExplorer/StackTraceExplorerService.cs +++ b/src/Features/Core/Portable/StackTraceExplorer/StackTraceExplorerService.cs @@ -2,6 +2,7 @@ // 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.Composition; @@ -20,7 +21,7 @@ namespace Microsoft.CodeAnalysis.StackTraceExplorer; [ExportWorkspaceService(typeof(IStackTraceExplorerService)), Shared] [method: ImportingConstructor] -[method: System.Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] +[method: Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] internal sealed class StackTraceExplorerService() : IStackTraceExplorerService { public (TextDocument? document, int line) GetDocumentAndLine(Solution solution, ParsedFrame frame) @@ -104,7 +105,7 @@ private static ImmutableArray GetFileMatches(Solution solution, St foreach (var document in allDocuments) { - if (document.Name == documentName) + if (string.Equals(document.Name, documentName, StringComparison.OrdinalIgnoreCase)) { potentialMatches.Add(document); }