diff --git a/.github/workflows/workflow.yml b/.github/workflows/workflow.yml index a8b3087..43d5481 100644 --- a/.github/workflows/workflow.yml +++ b/.github/workflows/workflow.yml @@ -10,7 +10,7 @@ permissions: contents: read env: - version: '10.13.${{ github.run_number }}' + version: '10.14.${{ github.run_number }}' dotnetVersion: '8' repoUrl: ${{ github.server_url }}/${{ github.repository }} vsixPath: src/CodeNav/bin/Release/net472/CodeNav.vsix diff --git a/README.md b/README.md index 999782e..90e5192 100644 --- a/README.md +++ b/README.md @@ -25,6 +25,7 @@ Visual Studio extension to show the code structure of your current document - Option to hide the tool window for unsupported files and files without code items - Compact mode with less spacing optimizing screen real estate - Pin an active document to keep the CodeNav window open for that document even when switching to other documents +- Open a second, independent CodeNav window # 💻 Supported Visual Studio versions @@ -44,7 +45,7 @@ Visual Studio extension to show the code structure of your current document [Open VSIX Gallery](https://www.vsixgallery.com/extension/CodeNav.dcdbcca4-3a88-432f-ba04-eb4a4cb64437) # 🤲 Usage -The CodeNav tool window can be opened via the entry in the `Extensions` menu. +The CodeNav tool window can be opened via the entries in the `Extensions` menu. # 🖼️ Screenshots ![Preview](https://raw.githubusercontent.com/sboulema/CodeNav/main/art/Screenshot-light.png) ![Preview-Dark](https://raw.githubusercontent.com/sboulema/CodeNav/main/art/Screenshot-dark.png) diff --git a/src/CodeNav.OutOfProc/ExtensionEntrypoint.cs b/src/CodeNav.OutOfProc/ExtensionEntrypoint.cs index 364b428..4235c9b 100644 --- a/src/CodeNav.OutOfProc/ExtensionEntrypoint.cs +++ b/src/CodeNav.OutOfProc/ExtensionEntrypoint.cs @@ -18,7 +18,10 @@ internal class ExtensionEntrypoint : Extension version: ExtensionAssemblyVersion, publisherName: "Samir Boulema", displayName: "CodeNav", - description: "Show the code structure of your current document."), + description: "Show the code structure of your current document.") + { + DotnetTargetVersions = [DotnetTarget.Net8, DotnetTarget.Custom("10.0")], + }, LoadedWhen = ActivationConstraint.ClientContext(ClientContextKey.Shell.ActiveEditorContentType, "CSharp|Basic|TypeScript"), }; diff --git a/src/CodeNav.OutOfProc/Helpers/SortHelper.cs b/src/CodeNav.OutOfProc/Helpers/SortHelper.cs index 28b92af..804080b 100644 --- a/src/CodeNav.OutOfProc/Helpers/SortHelper.cs +++ b/src/CodeNav.OutOfProc/Helpers/SortHelper.cs @@ -16,22 +16,21 @@ public static class SortHelper /// Used in the main toolbar sort buttons /// ClientContext /// CodeDocumentService + /// The view model of the tool window the sort was changed on /// CancellationToken /// Awaitable Task public static async Task ChangeSort( IClientContext clientContext, CodeDocumentService? codeDocumentService, + CodeDocumentViewModel? codeDocumentViewModel, SortOrderEnum sortOrder, CancellationToken cancellationToken) { var textViewSnapshot = await clientContext.GetActiveTextViewAsync(cancellationToken); - if (textViewSnapshot == null) - { - return; - } - - if (codeDocumentService == null) + if (textViewSnapshot == null || + codeDocumentService == null || + codeDocumentViewModel == null) { return; } @@ -40,12 +39,13 @@ public static async Task ChangeSort( codeDocumentService.GlobalSettings!.SortOrder = sortOrder; await SettingsHelper.SaveGlobalSettings(codeDocumentService); - ApplySort(codeDocumentService.CodeDocumentViewModel, sortOrder); + ApplySort(codeDocumentViewModel, sortOrder); await codeDocumentService.UpdateCodeDocumentViewModel( clientContext.Extensibility, textViewSnapshot.FilePath, textViewSnapshot.Document.Text.CopyToString(), + codeDocumentViewModel, cancellationToken); } diff --git a/src/CodeNav.OutOfProc/Services/CodeDocumentService.cs b/src/CodeNav.OutOfProc/Services/CodeDocumentService.cs index a1b6565..f2611e4 100644 --- a/src/CodeNav.OutOfProc/Services/CodeDocumentService.cs +++ b/src/CodeNav.OutOfProc/Services/CodeDocumentService.cs @@ -8,9 +8,10 @@ using Microsoft.VisualStudio.Extensibility.ToolWindows; using Microsoft.VisualStudio.Extensibility.UI; using System.Windows; +using System.Windows.Controls; using CSharpDocumentMapper = CodeNav.OutOfProc.Languages.CSharp.Mappers.DocumentMapper; -using VisualBasicDocumentMapper = CodeNav.OutOfProc.Languages.VisualBasic.Mappers.DocumentMapper; using TypeScriptDocumentMapper = CodeNav.OutOfProc.Languages.TypeScript.Mappers.DocumentMapper; +using VisualBasicDocumentMapper = CodeNav.OutOfProc.Languages.VisualBasic.Mappers.DocumentMapper; namespace CodeNav.OutOfProc.Services; @@ -27,6 +28,12 @@ public class CodeDocumentService new TypeScriptDocumentMapper(), ]; + // The last active document seen, used to immediately populate a newly opened tool + // window (see issue #186) instead of leaving it empty until the next document change. + private VisualStudioExtensibility? lastExtensibility; + private string? lastFilePath; + private string? lastText; + public CodeDocumentService( OutputWindowService logService, OutliningService outliningService, @@ -36,16 +43,23 @@ public CodeDocumentService( this.outliningService = outliningService; this.windowFrameService = windowFrameService; - CodeDocumentViewModel = new CodeDocumentViewModel + CodeDocumentViewModels.Add(new CodeDocumentViewModel { CodeDocumentService = this, - }; + }); } /// - /// DataContext for the tool window. + /// DataContext for every open CodeNav tool window. There is always at least one entry, + /// the primary tool window. A second, independent window (see issue #186) is added + /// through when it's opened via the duplicate toolbar button. + /// + public List CodeDocumentViewModels { get; } = []; + + /// + /// DataContext for the primary tool window. /// - public CodeDocumentViewModel CodeDocumentViewModel { get; set; } + public CodeDocumentViewModel CodeDocumentViewModel => CodeDocumentViewModels[0]; /// /// DataContext for the settings dialog. @@ -63,7 +77,63 @@ public CodeDocumentService( public OutliningService OutliningService => outliningService; - public ToolWindow? ToolWindow { get; set; } + /// + /// Registers the second CodeNav tool window (see issue #186), opened via the duplicate + /// toolbar button, and immediately populates it with the last known active document. + /// + /// The newly opened tool window + /// A new, independent view model for the tool window + public CodeDocumentViewModel RegisterWindow(ToolWindow toolWindow) + { + var viewModel = new CodeDocumentViewModel + { + CodeDocumentService = this, + ToolWindow = toolWindow, + }; + + CodeDocumentViewModels.Add(viewModel); + + return viewModel; + } + + /// + /// Populate a newly registered tool window with the last known active document, + /// so it isn't empty until the next document change. + /// + public async Task RefreshWindow(CodeDocumentViewModel viewModel, CancellationToken cancellationToken) + { + if (lastExtensibility == null || lastFilePath == null || lastText == null) + { + return; + } + + await UpdateCodeDocumentViewModel(lastExtensibility, lastFilePath, lastText, viewModel, cancellationToken); + } + + /// + /// Update ViewModels with the given, newly active document. + /// + /// + /// - ViewModel is not pinned: Update the ViewModel with the new document
+ /// - ViewModel is pinned and the document path is the same: Update the ViewModel with the new document + ///
+ public async Task UpdateCodeDocumentViewModels( + VisualStudioExtensibility? extensibility, + string? filePath, + string? text, + CancellationToken cancellationToken) + { + lastFilePath = filePath; + lastText = text; + + var activeCodeDocumentViewModels = CodeDocumentViewModels + .Where(model => !model.IsPinned || model.FilePath == filePath); + + foreach (var codeDocumentViewModel in activeCodeDocumentViewModels) + { + await UpdateCodeDocumentViewModel(extensibility, filePath, text, codeDocumentViewModel, cancellationToken); + } + } /// /// Update the view model @@ -71,21 +141,28 @@ public CodeDocumentService( /// Visual Studio extensibility /// The path to the file /// The text of the file + /// The view model of the tool window to update /// The cancellation token /// The updated code document view model public async Task UpdateCodeDocumentViewModel( VisualStudioExtensibility? extensibility, string? filePath, string? text, + CodeDocumentViewModel codeDocumentViewModel, CancellationToken cancellationToken) { try { + if (extensibility != null) + { + lastExtensibility = extensibility; + } + if (extensibility == null || string.IsNullOrEmpty(filePath) || string.IsNullOrEmpty(text)) { - return CodeDocumentViewModel; + return codeDocumentViewModel; } await LoadGlobalSettings(); @@ -96,13 +173,13 @@ public async Task UpdateCodeDocumentViewModel( if (documentMapper == null) { - CodeDocumentViewModel.CodeItems = + codeDocumentViewModel.CodeItems = PlaceholderHelper.CreateNoCodeItemsFound(); // No code items found, hide the tool window after showing the "No code items found" message - await HideToolWindow(cancellationToken); + await HideToolWindow(codeDocumentViewModel, cancellationToken); - return CodeDocumentViewModel; + return codeDocumentViewModel; } // Show loading item while we process the document @@ -110,7 +187,7 @@ public async Task UpdateCodeDocumentViewModel( var loadingCancellationToken = loadingCancellationTokenSource.Token; _ = PlaceholderHelper.CreateLoadingItem( - CodeDocumentViewModel, + codeDocumentViewModel, 500, loadingCancellationToken); @@ -118,7 +195,7 @@ public async Task UpdateCodeDocumentViewModel( var codeItems = await documentMapper.MapDocument( text, filePath, - CodeDocumentViewModel, + codeDocumentViewModel, extensibility, cancellationToken); @@ -128,72 +205,72 @@ public async Task UpdateCodeDocumentViewModel( await loadingCancellationTokenSource.CancelAsync(); // Set properties on the CodeDocumentViewModel that are needed for other features - CodeDocumentViewModel.FilePath = filePath ?? string.Empty; + codeDocumentViewModel.FilePath = filePath ?? string.Empty; if (!codeItems.Any()) { - CodeDocumentViewModel.CodeItems = PlaceholderHelper.CreateNoCodeItemsFound(); + codeDocumentViewModel.CodeItems = PlaceholderHelper.CreateNoCodeItemsFound(); // No code items found, hide the tool window after showing the "No code items found" message - await HideToolWindow(cancellationToken); + await HideToolWindow(codeDocumentViewModel, cancellationToken); - return CodeDocumentViewModel; + return codeDocumentViewModel; } // Code items were found, make sure the tool window is visible - await ShowToolWindow(cancellationToken); + await ShowToolWindow(codeDocumentViewModel, cancellationToken); // Sort the list of code items, // And update the DataContext for the tool window - CodeDocumentViewModel.CodeItems = SortHelper.Sort(codeItems, CodeDocumentViewModel.SortOrder); + codeDocumentViewModel.CodeItems = SortHelper.Sort(codeItems, codeDocumentViewModel.SortOrder); - await logService.WriteInfo(filePath, $"Sorted code items on '{CodeDocumentViewModel.SortOrder}'"); + await logService.WriteInfo(filePath, $"Sorted code items on '{codeDocumentViewModel.SortOrder}'"); // Apply highlights - HighlightHelper.UnHighlight(CodeDocumentViewModel); + HighlightHelper.UnHighlight(codeDocumentViewModel); await logService.WriteInfo(filePath, $"Remove highlight from all code items"); // Apply current visibility settings to the document VisibilityHelper.SetCodeItemVisibility( - CodeDocumentViewModel, - CodeDocumentViewModel.CodeItems, - CodeDocumentViewModel.FilterRules, - CodeDocumentViewModel.FilterText, - CodeDocumentViewModel.BookmarkIds); + codeDocumentViewModel, + codeDocumentViewModel.CodeItems, + codeDocumentViewModel.FilterRules, + codeDocumentViewModel.FilterText, + codeDocumentViewModel.BookmarkIds); await logService.WriteInfo(filePath, $"Set code item visibility"); // Apply filter rules - FilterRuleHelper.ApplyFilterRules(CodeDocumentViewModel, CodeDocumentViewModel.CodeItems, CodeDocumentViewModel.FilterRules); + FilterRuleHelper.ApplyFilterRules(codeDocumentViewModel, codeDocumentViewModel.CodeItems, codeDocumentViewModel.FilterRules); await logService.WriteInfo(filePath, $"Set code item filter rules"); // Apply history items - HistoryHelper.ApplyHistoryIndicator(CodeDocumentViewModel); + HistoryHelper.ApplyHistoryIndicator(codeDocumentViewModel); await logService.WriteInfo(filePath, $"Apply history indicators"); // Apply bookmarks - BookmarkHelper.ApplyBookmarkIndicator(CodeDocumentViewModel); + BookmarkHelper.ApplyBookmarkIndicator(codeDocumentViewModel); await logService.WriteInfo(filePath, $"Apply bookmark indicators"); // Apply outlining - await OutliningService.SubscribeToRegionEvents(CodeDocumentViewModel); + await OutliningService.SubscribeToRegionEvents(codeDocumentViewModel); await logService.WriteInfo(filePath, $"Apply outlining"); await windowFrameService.SubscribeToWindowFrameEvents(); - return CodeDocumentViewModel; + return codeDocumentViewModel; } catch (Exception e) { await LogHelper.LogException(this, "Error updating CodeDocumentViewModel", e); } - return CodeDocumentViewModel; + return codeDocumentViewModel; } public async Task LoadGlobalSettings(bool readFromDisk = false) @@ -241,36 +318,40 @@ public async Task LoadGlobalSettings(bool readFromDisk = false) FilterRules = new ObservableList(filterRules) ?? [], }; - CodeDocumentViewModel.FilterRules = [.. FilterDialogData.FilterRules]; + // Apply settings to every open CodeNav tool window (see issue #186) + foreach (var window in CodeDocumentViewModels) + { + window.FilterRules = [.. FilterDialogData.FilterRules]; - // Update the filter toolbar visibility - CodeDocumentViewModel.ShowFilterToolbarVisibility = SettingsDialogData.ShowFilterToolbar - ? Visibility.Visible - : Visibility.Collapsed; + // Update the filter toolbar visibility + window.ShowFilterToolbarVisibility = SettingsDialogData.ShowFilterToolbar + ? Visibility.Visible + : Visibility.Collapsed; - // Update the usage of compact mode - CodeDocumentViewModel.UseCompactMode = SettingsDialogData.UseCompactMode; + // Update the usage of compact mode + window.UseCompactMode = SettingsDialogData.UseCompactMode; - // Clear any history indicators if the setting was turned off - if (SettingsDialogData.ShowHistoryIndicators == false) - { - HistoryHelper.ClearHistory(CodeDocumentViewModel); - } + // Clear any history indicators if the setting was turned off + if (SettingsDialogData.ShowHistoryIndicators == false) + { + HistoryHelper.ClearHistory(window); + } - // Clear any highlights if the setting was turned off - if (SettingsDialogData.AutoHighlight == false) - { - HighlightHelper.UnHighlight(CodeDocumentViewModel); - } + // Clear any highlights if the setting was turned off + if (SettingsDialogData.AutoHighlight == false) + { + HighlightHelper.UnHighlight(window); + } - // Update the view model with the filter rules - VisibilityHelper.SetCodeItemVisibility(CodeDocumentViewModel, CodeDocumentViewModel.CodeItems, CodeDocumentViewModel.FilterRules); + // Update the view model with the filter rules + VisibilityHelper.SetCodeItemVisibility(window, window.CodeItems, window.FilterRules); - // Apply filter rules - FilterRuleHelper.ApplyFilterRules(CodeDocumentViewModel, CodeDocumentViewModel.CodeItems, CodeDocumentViewModel.FilterRules); + // Apply filter rules + FilterRuleHelper.ApplyFilterRules(window, window.CodeItems, window.FilterRules); - // Update the view model with the sort order - SortHelper.ApplySort(CodeDocumentViewModel, GlobalSettings.SortOrder); + // Update the view model with the sort order + SortHelper.ApplySort(window, GlobalSettings.SortOrder); + } } catch (Exception e) { @@ -278,9 +359,9 @@ public async Task LoadGlobalSettings(bool readFromDisk = false) } } - public async Task HideToolWindow(CancellationToken cancellationToken) + public async Task HideToolWindow(CodeDocumentViewModel viewModel, CancellationToken cancellationToken) { - if (ToolWindow == null) + if (viewModel.ToolWindow == null) { return; } @@ -290,12 +371,12 @@ public async Task HideToolWindow(CancellationToken cancellationToken) return; } - await ToolWindow.HideAsync(cancellationToken); + await viewModel.ToolWindow.HideAsync(cancellationToken); } - private async Task ShowToolWindow(CancellationToken cancellationToken) + private async Task ShowToolWindow(CodeDocumentViewModel viewModel, CancellationToken cancellationToken) { - if (ToolWindow == null) + if (viewModel.ToolWindow == null) { return; } @@ -305,6 +386,6 @@ private async Task ShowToolWindow(CancellationToken cancellationToken) return; } - await ToolWindow.ShowAsync(activate: false, cancellationToken); + await viewModel.ToolWindow.ShowAsync(activate: false, cancellationToken); } } diff --git a/src/CodeNav.OutOfProc/Services/OutOfProcService.cs b/src/CodeNav.OutOfProc/Services/OutOfProcService.cs index 4c6a536..95f8a8f 100644 --- a/src/CodeNav.OutOfProc/Services/OutOfProcService.cs +++ b/src/CodeNav.OutOfProc/Services/OutOfProcService.cs @@ -2,6 +2,7 @@ using CodeNav.OutOfProc.Models; using Microsoft.ServiceHub.Framework; using Microsoft.VisualStudio.Extensibility; +using Microsoft.VisualStudio.Extensibility.Editor; using Microsoft.VisualStudio.Extensibility.Shell; using System.Text.Json; @@ -39,13 +40,6 @@ public async Task ProcessActiveFrameChanged(string documentViewJsonString) return; } - // If the document is pinned, skip processing if the file path has changed to avoid losing the pinned state - if (codeDocumentService.CodeDocumentViewModel.IsPinned && - documentView.FilePath != codeDocumentService.CodeDocumentViewModel.FilePath) - { - return; - } - // Conditions: // - Frame is not a document frame // Actions: @@ -55,7 +49,7 @@ public async Task ProcessActiveFrameChanged(string documentViewJsonString) { codeDocumentService.CodeDocumentViewModel.CodeItems = PlaceholderHelper.CreateSelectDocumentItem(); - await codeDocumentService.HideToolWindow(default); + await codeDocumentService.HideToolWindow(codeDocumentService.CodeDocumentViewModel, default); return; } @@ -63,7 +57,7 @@ public async Task ProcessActiveFrameChanged(string documentViewJsonString) // Frame has changed and has a text document, so we need to update the list of code items await codeDocumentService.LoadGlobalSettings(); - await codeDocumentService.UpdateCodeDocumentViewModel( + await codeDocumentService.UpdateCodeDocumentViewModels( extensibility, documentView.FilePath, documentView.Text, diff --git a/src/CodeNav.OutOfProc/TextViewEventListener.cs b/src/CodeNav.OutOfProc/TextViewEventListener.cs index a1adf41..41ed812 100644 --- a/src/CodeNav.OutOfProc/TextViewEventListener.cs +++ b/src/CodeNav.OutOfProc/TextViewEventListener.cs @@ -32,7 +32,7 @@ internal class TextViewEventListener( { AppliesTo = [ - DocumentFilter.FromGlobPattern("**/*.{cs,vb}", true), + DocumentFilter.FromGlobPattern("**/*.{cs,vb,ts,tsx}", true), ], }; @@ -43,35 +43,32 @@ public async Task TextViewChangedAsync(TextViewChangedArgs args, CancellationTok { await codeDocumentService.LoadGlobalSettings(); - // If the document is pinned, skip processing if the file path has changed to avoid losing the pinned state - if (codeDocumentService.CodeDocumentViewModel.IsPinned && - args.AfterTextView.FilePath != codeDocumentService.CodeDocumentViewModel.FilePath) - { - return; - } + // Windows pinned to a different document (see issue #186) don't follow the active document + var activeCodeDocumentViewModels = codeDocumentService.CodeDocumentViewModels + .Where(model => !(model.IsPinned && model.FilePath != args.AfterTextView.FilePath)) + .ToList(); // if the document is too large, skip processing to avoid performance issues if (args.AfterTextView.Document.Lines.Count >= codeDocumentService.SettingsDialogData.AutoLoadLineThreshold && codeDocumentService.SettingsDialogData.AutoLoadLineThreshold > 0) { // Show the "line threshold passed" placeholder if the document exceeds the line threshold for auto-loading - codeDocumentService.CodeDocumentViewModel.CodeItems = PlaceholderHelper.CreateLineThresholdPassedItem(); + foreach (var window in activeCodeDocumentViewModels) + { + window.CodeItems = PlaceholderHelper.CreateLineThresholdPassedItem(); + } return; } - // Document changed: - // - File path changed - // - Edits made in the document - // Action: - // - Update code items list - if ((args.Edits.Any() && codeDocumentService.SettingsDialogData.UpdateWhileTyping) || - args.AfterTextView.FilePath != codeDocumentService.CodeDocumentViewModel.FilePath) + // Document changed - Update code items list + if ((args.Edits.Any() && + codeDocumentService.SettingsDialogData.UpdateWhileTyping)) { #pragma warning disable VSTHRD101 // Avoid unsupported async delegates await debounceDispatcher.DebounceAsync(async () => { - await codeDocumentService.UpdateCodeDocumentViewModel( + await codeDocumentService.UpdateCodeDocumentViewModels( Extensibility, args.AfterTextView.FilePath, args.AfterTextView.Document.Text.CopyToString(), @@ -85,7 +82,10 @@ await codeDocumentService.UpdateCodeDocumentViewModel( if (args.Edits.Any() && codeDocumentService.SettingsDialogData.ShowHistoryIndicators) { - await HistoryHelper.AddItemToHistory(codeDocumentService.CodeDocumentViewModel, args.Edits); + foreach (var window in activeCodeDocumentViewModels) + { + await HistoryHelper.AddItemToHistory(window, args.Edits); + } } // Selection changed - Update highlights @@ -93,9 +93,12 @@ await codeDocumentService.UpdateCodeDocumentViewModel( args.AfterTextView.Selection.ActivePosition.GetContainingLine().LineNumber && codeDocumentService.SettingsDialogData.AutoHighlight) { - await HighlightHelper.HighlightCurrentItem( - codeDocumentService.CodeDocumentViewModel, - args.AfterTextView.Selection.ActivePosition.Offset); + foreach (var window in activeCodeDocumentViewModels) + { + await HighlightHelper.HighlightCurrentItem( + window, + args.AfterTextView.Selection.ActivePosition.Offset); + } } } catch (Exception e) @@ -109,19 +112,21 @@ public async Task TextViewClosedAsync(ITextViewSnapshot textViewSnapshot, Cancel { try { - // If the document is pinned, skip processing if the file path has changed to avoid losing the pinned state - if (codeDocumentService.CodeDocumentViewModel.IsPinned && - textViewSnapshot.FilePath != codeDocumentService.CodeDocumentViewModel.FilePath) + foreach (var window in codeDocumentService.CodeDocumentViewModels) { - return; - } + // If this window is pinned to a different document, leave it untouched + if (window.IsPinned && textViewSnapshot.FilePath != window.FilePath) + { + continue; + } - // The pinned document itself was closed, unpin so CodeNav can follow the active document again - codeDocumentService.CodeDocumentViewModel.IsPinned = false; + // The pinned document itself was closed, unpin so this window can follow the active document again + window.IsPinned = false; - codeDocumentService.CodeDocumentViewModel.CodeItems = PlaceholderHelper.CreateSelectDocumentItem(); + window.CodeItems = PlaceholderHelper.CreateSelectDocumentItem(); - await codeDocumentService.HideToolWindow(cancellationToken); + await codeDocumentService.HideToolWindow(window, cancellationToken); + } } catch (Exception e) { @@ -136,23 +141,24 @@ public async Task TextViewOpenedAsync(ITextViewSnapshot textViewSnapshot, Cancel { await codeDocumentService.LoadGlobalSettings(); - // If the document is pinned, skip processing if the file path has changed to avoid losing the pinned state - if (codeDocumentService.CodeDocumentViewModel.IsPinned && - textViewSnapshot.FilePath != codeDocumentService.CodeDocumentViewModel.FilePath) - { - return; - } + // Windows pinned to a different document (see issue #186) don't follow the active document + var activeWindows = codeDocumentService.CodeDocumentViewModels + .Where(window => !(window.IsPinned && window.FilePath != textViewSnapshot.FilePath)) + .ToList(); if (textViewSnapshot.Document.Lines.Count >= codeDocumentService.SettingsDialogData.AutoLoadLineThreshold && codeDocumentService.SettingsDialogData.AutoLoadLineThreshold > 0) { // Show the "line threshold passed" placeholder if the document exceeds the line threshold for auto-loading - codeDocumentService.CodeDocumentViewModel.CodeItems = PlaceholderHelper.CreateLineThresholdPassedItem(); + foreach (var window in activeWindows) + { + window.CodeItems = PlaceholderHelper.CreateLineThresholdPassedItem(); + } return; } - await codeDocumentService.UpdateCodeDocumentViewModel( + await codeDocumentService.UpdateCodeDocumentViewModels( Extensibility, textViewSnapshot.FilePath, textViewSnapshot.Document.Text.CopyToString(), diff --git a/src/CodeNav.OutOfProc/ToolWindows/CodeNavToolWindow.cs b/src/CodeNav.OutOfProc/ToolWindows/CodeNavToolWindow.cs index 7d7af86..eb5c3f2 100644 --- a/src/CodeNav.OutOfProc/ToolWindows/CodeNavToolWindow.cs +++ b/src/CodeNav.OutOfProc/ToolWindows/CodeNavToolWindow.cs @@ -20,7 +20,7 @@ public override Task InitializeAsync(CancellationToken cancellationToken) { Title = "CodeNav"; - codeDocumentService.ToolWindow = this; + codeDocumentService.CodeDocumentViewModel.ToolWindow = this; return Task.CompletedTask; } diff --git a/src/CodeNav.OutOfProc/ToolWindows/CodeNavToolWindow2.cs b/src/CodeNav.OutOfProc/ToolWindows/CodeNavToolWindow2.cs new file mode 100644 index 0000000..b8221b1 --- /dev/null +++ b/src/CodeNav.OutOfProc/ToolWindows/CodeNavToolWindow2.cs @@ -0,0 +1,32 @@ +using CodeNav.OutOfProc.Services; +using CodeNav.OutOfProc.ViewModels; +using Microsoft.VisualStudio.Extensibility; +using Microsoft.VisualStudio.Extensibility.ToolWindows; +using Microsoft.VisualStudio.RpcContracts.RemoteUI; + +namespace CodeNav.OutOfProc.ToolWindows; + +[VisualStudioContribution] +internal class CodeNavToolWindow2(CodeDocumentService codeDocumentService) : ToolWindow +{ + private CodeDocumentViewModel? codeDocumentViewModel; + + public override ToolWindowConfiguration ToolWindowConfiguration => new() + { + Placement = ToolWindowPlacement.Floating, + }; + + public override async Task InitializeAsync(CancellationToken cancellationToken) + { + Title = "CodeNav 2"; + + codeDocumentViewModel = codeDocumentService.RegisterWindow(this); + + // Immediately show whatever document is currently active, + // instead of leaving the window empty because we are opening a duplicated window. + await codeDocumentService.RefreshWindow(codeDocumentViewModel, cancellationToken); + } + + public override Task GetContentAsync(CancellationToken cancellationToken) + => Task.FromResult(new CodeNavToolWindowControl(codeDocumentViewModel)); +} diff --git a/src/CodeNav.OutOfProc/ToolWindows/CodeNavToolWindowCommand2.cs b/src/CodeNav.OutOfProc/ToolWindows/CodeNavToolWindowCommand2.cs new file mode 100644 index 0000000..4c4126a --- /dev/null +++ b/src/CodeNav.OutOfProc/ToolWindows/CodeNavToolWindowCommand2.cs @@ -0,0 +1,22 @@ +using Microsoft.VisualStudio.Extensibility; +using Microsoft.VisualStudio.Extensibility.Commands; + +namespace CodeNav.OutOfProc.ToolWindows; + +[VisualStudioContribution] +public class CodeNavToolWindowCommand2 : Command +{ + public override CommandConfiguration CommandConfiguration => new("%CodeNav.CodeNavToolWindowCommand2.DisplayName%") + { + Placements = [CommandPlacement.KnownPlacements.ExtensionsMenu], + Icon = new(ImageMoniker.KnownValues.DocumentOutline, IconSettings.IconAndText), + }; + + public override Task InitializeAsync(CancellationToken cancellationToken) + { + return base.InitializeAsync(cancellationToken); + } + + public override async Task ExecuteCommandAsync(IClientContext context, CancellationToken cancellationToken) + => await Extensibility.Shell().ShowToolWindowAsync(activate: true, cancellationToken); +} diff --git a/src/CodeNav.OutOfProc/ViewModels/CodeDocumentViewModel.cs b/src/CodeNav.OutOfProc/ViewModels/CodeDocumentViewModel.cs index 2bead0d..6ce2691 100644 --- a/src/CodeNav.OutOfProc/ViewModels/CodeDocumentViewModel.cs +++ b/src/CodeNav.OutOfProc/ViewModels/CodeDocumentViewModel.cs @@ -6,9 +6,9 @@ using CodeNav.OutOfProc.Services; using Microsoft.VisualStudio.Extensibility; using Microsoft.VisualStudio.Extensibility.Editor; +using Microsoft.VisualStudio.Extensibility.ToolWindows; using Microsoft.VisualStudio.Extensibility.UI; using Microsoft.VisualStudio.RpcContracts.Notifications; -using Newtonsoft.Json.Linq; using System.Runtime.Serialization; using System.Windows; @@ -37,6 +37,8 @@ public CodeDocumentViewModel() public CodeDocumentService? CodeDocumentService { get; set; } + public ToolWindow? ToolWindow { get; set; } + public SortOrderEnum SortOrder = SortOrderEnum.SortByFile; public List HistoryItemIds = []; @@ -207,6 +209,7 @@ await CodeDocumentService.UpdateCodeDocumentViewModel( clientContext.Extensibility, textViewSnapshot.FilePath, textViewSnapshot.Document.Text.CopyToString(), + this, cancellationToken); } @@ -214,21 +217,21 @@ await CodeDocumentService.UpdateCodeDocumentViewModel( public AsyncCommand SortByNameCommand { get; } private async Task SortByName(object? commandParameter, IClientContext clientContext, CancellationToken cancellationToken) { - await SortHelper.ChangeSort(clientContext, CodeDocumentService, SortOrderEnum.SortByName, cancellationToken); + await SortHelper.ChangeSort(clientContext, CodeDocumentService, this, SortOrderEnum.SortByName, cancellationToken); } [DataMember] public AsyncCommand SortByFileCommand { get; } private async Task SortByFile(object? commandParameter, IClientContext clientContext, CancellationToken cancellationToken) { - await SortHelper.ChangeSort(clientContext, CodeDocumentService, SortOrderEnum.SortByFile, cancellationToken); + await SortHelper.ChangeSort(clientContext, CodeDocumentService, this, SortOrderEnum.SortByFile, cancellationToken); } [DataMember] public AsyncCommand SortByTypeCommand { get; } private async Task SortByType(object? commandParameter, IClientContext clientContext, CancellationToken cancellationToken) { - await SortHelper.ChangeSort(clientContext, CodeDocumentService, SortOrderEnum.SortByType, cancellationToken); + await SortHelper.ChangeSort(clientContext, CodeDocumentService, this, SortOrderEnum.SortByType, cancellationToken); } [DataMember] diff --git a/src/CodeNav.OutOfProc/ViewModels/CodeItem.cs b/src/CodeNav.OutOfProc/ViewModels/CodeItem.cs index e747b6a..deb3115 100644 --- a/src/CodeNav.OutOfProc/ViewModels/CodeItem.cs +++ b/src/CodeNav.OutOfProc/ViewModels/CodeItem.cs @@ -369,10 +369,11 @@ public async Task Refresh(object? commandParameter, IClientContext clientContext await CodeDocumentViewModel! .CodeDocumentService! .UpdateCodeDocumentViewModel( - clientContext.Extensibility, - textViewSnapshot.FilePath, - textViewSnapshot.Document.Text.CopyToString(), - cancellationToken); + clientContext.Extensibility, + textViewSnapshot.FilePath, + textViewSnapshot.Document.Text.CopyToString(), + CodeDocumentViewModel, + cancellationToken); } [DataMember] diff --git a/src/CodeNav/.vsextension/string-resources.json b/src/CodeNav/.vsextension/string-resources.json index 63868f6..ec257b0 100644 --- a/src/CodeNav/.vsextension/string-resources.json +++ b/src/CodeNav/.vsextension/string-resources.json @@ -1,3 +1,4 @@ { - "CodeNav.CodeNavToolWindowCommand.DisplayName": "CodeNav" + "CodeNav.CodeNavToolWindowCommand.DisplayName": "CodeNav", + "CodeNav.CodeNavToolWindowCommand2.DisplayName": "CodeNav 2" } diff --git a/src/CodeNav/Services/InProcService.cs b/src/CodeNav/Services/InProcService.cs index 87cd2e2..2892ec5 100644 --- a/src/CodeNav/Services/InProcService.cs +++ b/src/CodeNav/Services/InProcService.cs @@ -168,6 +168,11 @@ public async Task ExpandOutlineRegion(int spanStart, int spanLength) var outliningManager = await GetOutliningManager(textView); + if (outliningManager == null) + { + return; + } + // Switch to the UI thread to ensure we can interact with the outline regions. await ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync(); @@ -216,6 +221,11 @@ public async Task CollapseOutlineRegion(int spanStart, int spanLength) var outliningManager = await GetOutliningManager(textView); + if (outliningManager == null) + { + return; + } + // Switch to the UI thread to ensure we can interact with the outline regions. await ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync(); @@ -380,7 +390,7 @@ public async void OnActiveFrameChanged(IVsWindowFrame oldFrame, IVsWindowFrame n // Check if the new frame is the CodeNav tool window, // if so ignore it since we don't want to trigger updates when CodeNav is focused - if (windowCaption == "CodeNav") + if (windowCaption.StartsWith("CodeNav")) { return; }