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
 
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;
}