diff --git a/src/CodeNav.OutOfProc/ExtensionEntrypoint.cs b/src/CodeNav.OutOfProc/ExtensionEntrypoint.cs index 937d33c..e5714f0 100644 --- a/src/CodeNav.OutOfProc/ExtensionEntrypoint.cs +++ b/src/CodeNav.OutOfProc/ExtensionEntrypoint.cs @@ -1,4 +1,5 @@ -using CodeNav.OutOfProc.Services; +using CodeNav.OutOfProc.Helpers; +using CodeNav.OutOfProc.Services; using Microsoft.Extensions.DependencyInjection; using Microsoft.VisualStudio.Extensibility; @@ -36,5 +37,6 @@ protected override void InitializeServices(IServiceCollection serviceCollection) // As of now, any instance that ingests VisualStudioExtensibility is required to be added as a scoped // service. serviceCollection.AddScoped(); + serviceCollection.AddScoped(); } } diff --git a/src/CodeNav.OutOfProc/Helpers/OutliningHelper.cs b/src/CodeNav.OutOfProc/Helpers/OutliningHelper.cs index d74008b..69e5438 100644 --- a/src/CodeNav.OutOfProc/Helpers/OutliningHelper.cs +++ b/src/CodeNav.OutOfProc/Helpers/OutliningHelper.cs @@ -1,18 +1,122 @@ using CodeNav.OutOfProc.Extensions; using CodeNav.OutOfProc.Interfaces; using CodeNav.OutOfProc.ViewModels; +using CodeNav.Services; +using Microsoft; +using Microsoft.VisualStudio.Extensibility; +using Microsoft.VisualStudio.Extensibility.Helpers; namespace CodeNav.OutOfProc.Helpers; -public static class OutliningHelper +public class OutliningHelper : DisposableObject { + private readonly VisualStudioExtensibility _extensibility; + private readonly Task _initializationTask; + private IInProcService? _inProcService; + + public OutliningHelper(VisualStudioExtensibility extensibility) + { + _extensibility = extensibility; + _initializationTask = Task.Run(InitializeAsync); + } + + public async Task SubscribeToRegionEvents() + { + try + { + Assumes.NotNull(_inProcService); + await _inProcService.SubscribeToRegionEvents(); + } + catch (Exception e) + { + // TODO: Add logging + } + } + + public async Task CollapseOutlineRegion(int start, int length) + { + try + { + Assumes.NotNull(_inProcService); + await _inProcService.CollapseOutlineRegion(start, length); + } + catch (Exception e) + { + // TODO: Add logging + } + } + + public async Task ExpandOutlineRegion(int start, int length) + { + try + { + Assumes.NotNull(_inProcService); + await _inProcService.ExpandOutlineRegion(start, length); + } + catch (Exception e) + { + // TODO: Add logging + } + } + + public static async Task CollapseOutlineRegion(CodeItem codeItem) + { + if (codeItem.CodeDocumentViewModel?.CodeDocumentService?.OutliningHelper == null) + { + return; + } + + await codeItem.CodeDocumentViewModel.CodeDocumentService.OutliningHelper.CollapseOutlineRegion(codeItem.Span.Start, codeItem.Span.Length); + } + + public static async Task ExpandOutlineRegion(CodeItem codeItem) + { + if (codeItem.CodeDocumentViewModel?.CodeDocumentService?.OutliningHelper == null) + { + return; + } + + await codeItem.CodeDocumentViewModel.CodeDocumentService.OutliningHelper.ExpandOutlineRegion(codeItem.Span.Start, codeItem.Span.Length); + } + + /// + /// Set IsExpanded property to false on all code items + /// + /// Used in the main toolbar and in the code item context menu + /// The code document view model whose nodes will be collapsed. public static void CollapseAll(CodeDocumentViewModel? codeDocumentViewModel) - => SetIsExpanded(codeDocumentViewModel, isExpanded: false); + => SetAllIsExpanded(codeDocumentViewModel, isExpanded: false); + /// + /// Set IsExpanded property to true on all code items + /// + /// Used in the main toolbar and in the code item context menu + /// The code document view model whose nodes will be expanded. public static void ExpandAll(CodeDocumentViewModel? codeDocumentViewModel) - => SetIsExpanded(codeDocumentViewModel, isExpanded: true); + => SetAllIsExpanded(codeDocumentViewModel, isExpanded: true); - private static void SetIsExpanded(CodeDocumentViewModel? codeDocumentViewModel, bool isExpanded) + public static void SetIsExpanded(CodeDocumentViewModel? codeDocumentViewModel, int spanStart, int spanEnd, bool isExpanded) + { + codeDocumentViewModel? + .CodeItems + .Flatten() + .FilterNull() + .Where(item => item is IMembers) + .Where(codeItem => codeItem.Span.Start == spanStart || + codeItem.Span.End == spanEnd) + .Cast() + .ToList() + .ForEach(codeItem => codeItem.IsExpanded = isExpanded); + } + + /// + /// Sets the expanded state for all member items within the specified code document view model. + /// + /// Only items that implement the IMembers interface are affected. + /// The code document view model containing the code items to update. If null, no action is taken. + /// A value indicating whether the member items should be expanded () or collapsed (). + private static void SetAllIsExpanded(CodeDocumentViewModel? codeDocumentViewModel, bool isExpanded) { codeDocumentViewModel? .CodeItems @@ -23,4 +127,22 @@ private static void SetIsExpanded(CodeDocumentViewModel? codeDocumentViewModel, .ToList() .ForEach(item => item.IsExpanded = isExpanded); } + + private async Task InitializeAsync() + { + (_inProcService as IDisposable)?.Dispose(); + _inProcService = await _extensibility + .ServiceBroker + .GetProxyAsync(IInProcService.Configuration.ServiceDescriptor, cancellationToken: default); + } + + protected override void Dispose(bool isDisposing) + { + base.Dispose(isDisposing); + + if (isDisposing) + { + (_inProcService as IDisposable)?.Dispose(); + } + } } diff --git a/src/CodeNav.OutOfProc/Services/CodeDocumentService.cs b/src/CodeNav.OutOfProc/Services/CodeDocumentService.cs index 5ac731b..8592c1b 100644 --- a/src/CodeNav.OutOfProc/Services/CodeDocumentService.cs +++ b/src/CodeNav.OutOfProc/Services/CodeDocumentService.cs @@ -10,7 +10,9 @@ namespace CodeNav.OutOfProc.Services; -public class CodeDocumentService(OutputWindowService logService) +public class CodeDocumentService( + OutputWindowService logService, + OutliningHelper outliningHelper) { /// /// DataContext for the tool window. @@ -31,6 +33,8 @@ public class CodeDocumentService(OutputWindowService logService) public OutputWindowService LogService => logService; + public OutliningHelper OutliningHelper => outliningHelper; + public async Task UpdateCodeDocumentViewModel( VisualStudioExtensibility? extensibility, ITextViewSnapshot? textView, diff --git a/src/CodeNav.OutOfProc/Services/IOutOfProcService.cs b/src/CodeNav.OutOfProc/Services/IOutOfProcService.cs index 87bdfb0..103ebab 100644 --- a/src/CodeNav.OutOfProc/Services/IOutOfProcService.cs +++ b/src/CodeNav.OutOfProc/Services/IOutOfProcService.cs @@ -6,6 +6,8 @@ public interface IOutOfProcService { Task DoSomethingAsync(CancellationToken cancellationToken); + Task SetCodeItemIsExpanded(int spanStart, int spanEnd, bool isExpanded); + public static class Configuration { public const string ServiceName = "CodeNav.OutOfProcService"; diff --git a/src/CodeNav.OutOfProc/Services/OutOfProcService.cs b/src/CodeNav.OutOfProc/Services/OutOfProcService.cs index ff33349..b98c183 100644 --- a/src/CodeNav.OutOfProc/Services/OutOfProcService.cs +++ b/src/CodeNav.OutOfProc/Services/OutOfProcService.cs @@ -1,4 +1,5 @@ -using Microsoft.ServiceHub.Framework; +using CodeNav.OutOfProc.Helpers; +using Microsoft.ServiceHub.Framework; using Microsoft.VisualStudio.Extensibility; using Microsoft.VisualStudio.Extensibility.Shell; @@ -7,11 +8,15 @@ namespace CodeNav.OutOfProc.Services; [VisualStudioContribution] internal class OutOfProcService : IOutOfProcService, IBrokeredService { - private readonly VisualStudioExtensibility extensibility; + private readonly VisualStudioExtensibility _extensibility; + private readonly CodeDocumentService _codeDocumentService; - public OutOfProcService(VisualStudioExtensibility extensibility) + public OutOfProcService( + VisualStudioExtensibility extensibility, + CodeDocumentService codeDocumentService) { - this.extensibility = extensibility; + _extensibility = extensibility; + _codeDocumentService = codeDocumentService; } public static BrokeredServiceConfiguration BrokeredServiceConfiguration @@ -22,8 +27,13 @@ public static BrokeredServiceConfiguration BrokeredServiceConfiguration public static ServiceRpcDescriptor ServiceDescriptor => IOutOfProcService.Configuration.ServiceDescriptor; + public async Task SetCodeItemIsExpanded(int spanStart, int spanEnd, bool isExpanded) + { + OutliningHelper.SetIsExpanded(_codeDocumentService.CodeDocumentViewModel, spanStart, spanEnd, isExpanded); + } + public async Task DoSomethingAsync(CancellationToken cancellationToken) { - await this.extensibility.Shell().ShowPromptAsync("Hello from in-proc! (Showing this message from (out-of-proc)", PromptOptions.OK, cancellationToken); + await _extensibility.Shell().ShowPromptAsync("Hello from in-proc! (Showing this message from (out-of-proc)", PromptOptions.OK, cancellationToken); } } diff --git a/src/CodeNav.OutOfProc/ViewModels/CodeClassItem.cs b/src/CodeNav.OutOfProc/ViewModels/CodeClassItem.cs index 9a7a0cc..8bf82e9 100644 --- a/src/CodeNav.OutOfProc/ViewModels/CodeClassItem.cs +++ b/src/CodeNav.OutOfProc/ViewModels/CodeClassItem.cs @@ -1,4 +1,5 @@ -using CodeNav.OutOfProc.Interfaces; +using CodeNav.OutOfProc.Helpers; +using CodeNav.OutOfProc.Interfaces; using Microsoft.VisualStudio.Extensibility; using Microsoft.VisualStudio.Extensibility.UI; using System.Runtime.Serialization; @@ -35,8 +36,18 @@ public bool IsExpanded { if (_isExpanded != value) { - SetProperty(ref _isExpanded, value); + SetProperty(ref _isExpanded, value); + IsExpandedChanged?.Invoke(this, EventArgs.Empty); + + if (value) + { + _ = OutliningHelper.ExpandOutlineRegion(this); + } + else + { + _ = OutliningHelper.CollapseOutlineRegion(this); + } } } } @@ -51,6 +62,10 @@ public Visibility HasMembersVisibility ? Visibility.Visible : Visibility.Collapsed; + /// + /// Command use to collapse and expand class/region/namespace code items + /// when double-clicking on the expander header + /// [DataMember] public AsyncCommand ToggleExpandCollapseCommand { get; } public async Task ToggleExpandCollapse(object? commandParameter, IClientContext clientContext, CancellationToken cancellationToken) diff --git a/src/CodeNav.OutOfProc/ViewModels/CodeItem.cs b/src/CodeNav.OutOfProc/ViewModels/CodeItem.cs index c573e84..2ace970 100644 --- a/src/CodeNav.OutOfProc/ViewModels/CodeItem.cs +++ b/src/CodeNav.OutOfProc/ViewModels/CodeItem.cs @@ -75,8 +75,20 @@ public CodeItem() [DataMember] public string Tooltip { get; set; } = string.Empty; + /// + /// Path to the file containing the code item + /// + /// + /// Used for opening the file if it's different from the currently active one + /// public Uri? FilePath { get; set; } + /// + /// Full name of the code item + /// + /// + /// Used in constructing a unique id + /// internal string FullName = string.Empty; public CodeItemKindEnum Kind; diff --git a/src/CodeNav/Services/IInProcService.cs b/src/CodeNav/Services/IInProcService.cs index 8b233aa..a98fe1f 100644 --- a/src/CodeNav/Services/IInProcService.cs +++ b/src/CodeNav/Services/IInProcService.cs @@ -8,6 +8,12 @@ public interface IInProcService Task TextViewScrollToSpan(int start, int length); + Task ExpandOutlineRegion(int start, int length); + + Task CollapseOutlineRegion(int start, int length); + + Task SubscribeToRegionEvents(); + public static class Configuration { public const string ServiceName = "CodeNav.InProcService"; diff --git a/src/CodeNav/Services/InProcService.cs b/src/CodeNav/Services/InProcService.cs index 4f38484..b666816 100644 --- a/src/CodeNav/Services/InProcService.cs +++ b/src/CodeNav/Services/InProcService.cs @@ -1,4 +1,5 @@ -using Microsoft; +using CodeNav.OutOfProc.Services; +using Microsoft; using Microsoft.VisualStudio; using Microsoft.VisualStudio.Editor; using Microsoft.VisualStudio.Extensibility; @@ -7,6 +8,7 @@ using Microsoft.VisualStudio.Shell; using Microsoft.VisualStudio.Text; using Microsoft.VisualStudio.Text.Editor; +using Microsoft.VisualStudio.Text.Outlining; using Microsoft.VisualStudio.TextManager.Interop; namespace CodeNav.Services; @@ -16,15 +18,18 @@ internal class InProcService : IInProcService { private readonly VisualStudioExtensibility _extensibility; private readonly MefInjection _editorAdaptersFactoryService; + private readonly MefInjection _outliningManagerFactoryService; private readonly AsyncServiceProviderInjection _textManager; public InProcService( VisualStudioExtensibility extensibility, MefInjection editorAdaptersFactoryService, + MefInjection outliningManagerFactoryService, AsyncServiceProviderInjection textManager) { _extensibility = extensibility; _editorAdaptersFactoryService = editorAdaptersFactoryService; + _outliningManagerFactoryService = outliningManagerFactoryService; _textManager = textManager; } @@ -40,6 +45,139 @@ public async Task DoSomethingAsync(CancellationToken cancellationToken) await _extensibility.Shell().ShowPromptAsync("Hello from out-of-proc! (Showing this message from (in-proc)", PromptOptions.OK, cancellationToken); } + public async Task SubscribeToRegionEvents() + { + // Not using context.GetActiveTextViewAsync here because VisualStudio.Extensibility doesn't support outlining yet. + var textView = await GetCurrentTextViewAsync(); + + var outliningManager = await GetOutliningManager(textView); + + outliningManager.RegionsExpanded -= OutliningManager_RegionsExpanded; + outliningManager.RegionsExpanded += OutliningManager_RegionsExpanded; + + outliningManager.RegionsCollapsed -= OutliningManager_RegionsCollapsed; + outliningManager.RegionsCollapsed += OutliningManager_RegionsCollapsed; + } + + private async void OutliningManager_RegionsExpanded(object sender, RegionsExpandedEventArgs e) + { + var outOfProcService = await _extensibility.ServiceBroker + .GetProxyAsync(IOutOfProcService.Configuration.ServiceDescriptor, cancellationToken: default); + + try + { + Assumes.NotNull(outOfProcService); + + foreach (var region in e.ExpandedRegions) + { + var span = GetSpan(region); + await outOfProcService.SetCodeItemIsExpanded(span.Start, span.End, isExpanded: true); + } + } + finally + { + (outOfProcService as IDisposable)?.Dispose(); + } + } + + private async void OutliningManager_RegionsCollapsed(object sender, RegionsCollapsedEventArgs e) + { + var outOfProcService = await _extensibility.ServiceBroker + .GetProxyAsync(IOutOfProcService.Configuration.ServiceDescriptor, cancellationToken: default); + + try + { + Assumes.NotNull(outOfProcService); + + foreach (var region in e.CollapsedRegions) + { + var span = GetSpan(region); + await outOfProcService.SetCodeItemIsExpanded(span.Start, span.End, isExpanded: false); + } + } + finally + { + (outOfProcService as IDisposable)?.Dispose(); + } + } + + public async Task ExpandOutlineRegion(int start, int length) + { + try + { + // Not using context.GetActiveTextViewAsync here because VisualStudio.Extensibility doesn't support outlining yet. + var textView = await GetCurrentTextViewAsync(); + + var outliningManager = await GetOutliningManager(textView); + + // Switch to the UI thread to ensure we can interact with the outline regions. + await ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync(); + + var outlineRegion = await GetOutlineRegionForSpan(textView, outliningManager, start, length); + + // Check if the outline region is collapsed before expanding + if (outlineRegion?.IsCollapsed != true || + outlineRegion is not ICollapsed collapsedOutlineRegion) + { + return; + } + + outliningManager.Expand(collapsedOutlineRegion); + } + catch (Exception) + { + // TODO: Implement in-proc error logging + } + } + + public async Task CollapseOutlineRegion(int start, int length) + { + try + { + // Not using context.GetActiveTextViewAsync here because VisualStudio.Extensibility doesn't support outlining yet. + var textView = await GetCurrentTextViewAsync(); + + var outliningManager = await GetOutliningManager(textView); + + // Switch to the UI thread to ensure we can interact with the outline regions. + await ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync(); + + var outlineRegion = await GetOutlineRegionForSpan(textView, outliningManager, start, length); + + outliningManager.TryCollapse(outlineRegion); + } + catch (Exception) + { + // TODO: Implement in-proc error logging + } + } + + private async Task GetOutlineRegionForSpan( + IWpfTextView textView, IOutliningManager outliningManager, + int start, int length) + { + // Get all outline regions for the given span + var span = new SnapshotSpan(textView.TextSnapshot, start, length); + + var outlineRegions = outliningManager.GetAllRegions(span); + + // Get the first outline region that has the same span start or end + return outlineRegions.FirstOrDefault(outlineRegion + => GetSpan(outlineRegion).Start == start || + GetSpan(outlineRegion).End == start + length); + } + + private SnapshotSpan GetSpan(ICollapsible outlineRegion) + => outlineRegion.Extent.GetSpan(outlineRegion.Extent.TextBuffer.CurrentSnapshot); + + private async Task GetOutliningManager(IWpfTextView textView) + { + var outliningManagerService = await _outliningManagerFactoryService.GetServiceAsync(); + var outliningManager = outliningManagerService.GetOutliningManager(textView); + + return outliningManager; + } + public async Task TextViewScrollToSpan(int start, int length) { try @@ -53,7 +191,7 @@ public async Task TextViewScrollToSpan(int start, int length) return; } - var span = new SnapshotSpan(textView.TextSnapshot, new Span(start, length)); + var span = new SnapshotSpan(textView.TextSnapshot, start, length); // Switch to the UI thread to ensure we can interact with the view scroller. await ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync(); @@ -68,7 +206,7 @@ public async Task TextViewScrollToSpan(int start, int length) private async Task GetCurrentTextViewAsync() { - IVsEditorAdaptersFactoryService editorAdapter = await _editorAdaptersFactoryService.GetServiceAsync(); + var editorAdapter = await _editorAdaptersFactoryService.GetServiceAsync(); var view = editorAdapter.GetWpfTextView(await GetCurrentNativeTextViewAsync()); Assumes.Present(view); return view;