diff --git a/.github/workflows/workflow.yml b/.github/workflows/workflow.yml index 607e519..034dc3d 100644 --- a/.github/workflows/workflow.yml +++ b/.github/workflows/workflow.yml @@ -7,7 +7,7 @@ on: - 'feature/**' env: - version: '10.5.${{ github.run_number }}' + version: '10.6.${{ github.run_number }}' dotnetVersion: '8' repoUrl: ${{ github.server_url }}/${{ github.repository }} vsixPath: src/CodeNav/bin/Release/net472/CodeNav.vsix diff --git a/docs/links.md b/docs/links.md index 778bf54..4c8ed7b 100644 --- a/docs/links.md +++ b/docs/links.md @@ -25,6 +25,10 @@ Here is a list of links to helpful pages I needed when developing this extension - [CompositeExtension Sample](https://github.com/microsoft/VSExtensibility/tree/main/New_Extensibility_Model/Samples/CompositeExtension) - [The CheckBox control](https://wpf-tutorial.com/basic-controls/the-checkbox-control/) - [Sorting enum by a custom attribute](https://stackoverflow.com/questions/61091166/sorting-enum-by-a-custom-attribute) +- [TextView methods](https://github.com/llvm-mirror/clang/blob/master/tools%2Fclang-format-vs%2FClangFormat%2FVsix.cs#L34) +- [TextView methods 2](https://github.com/msomeone/PeasyMotion/blob/466eaad0f0e731928c645b10f5f43f3519631ea0/Shared/VsMethodExtensions.cs#L115) +- [TextView methods 3](https://github.com/VsixCommunity/Community.VisualStudio.Toolkit/blob/8624a25f504d7fc0bab5fa3434ce95be2466f438/src/toolkit/Community.VisualStudio.Toolkit.Shared/Windows/WindowFrame.cs#L232) +- [Get Path of the document from IWpfTextView for non cs files](https://stackoverflow.com/questions/48068134/get-path-of-the-document-from-iwpftextview-for-non-cs-files) ## VSExtensibility Issues - [#545 - Feature request: Text Editor: Collapse/Expand ranges](https://github.com/microsoft/VSExtensibility/issues/545) diff --git a/src/CodeNav.OutOfProc/ExtensionEntrypoint.cs b/src/CodeNav.OutOfProc/ExtensionEntrypoint.cs index e5714f0..072d1f5 100644 --- a/src/CodeNav.OutOfProc/ExtensionEntrypoint.cs +++ b/src/CodeNav.OutOfProc/ExtensionEntrypoint.cs @@ -1,5 +1,4 @@ -using CodeNav.OutOfProc.Helpers; -using CodeNav.OutOfProc.Services; +using CodeNav.OutOfProc.Services; using Microsoft.Extensions.DependencyInjection; using Microsoft.VisualStudio.Extensibility; @@ -37,6 +36,7 @@ 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(); + serviceCollection.AddScoped(); + serviceCollection.AddScoped(); } } diff --git a/src/CodeNav.OutOfProc/Helpers/SortHelper.cs b/src/CodeNav.OutOfProc/Helpers/SortHelper.cs index 9c5b104..28b92af 100644 --- a/src/CodeNav.OutOfProc/Helpers/SortHelper.cs +++ b/src/CodeNav.OutOfProc/Helpers/SortHelper.cs @@ -4,6 +4,7 @@ using CodeNav.OutOfProc.Services; using CodeNav.OutOfProc.ViewModels; using Microsoft.VisualStudio.Extensibility; +using Microsoft.VisualStudio.Extensibility.Editor; namespace CodeNav.OutOfProc.Helpers; @@ -41,7 +42,11 @@ public static async Task ChangeSort( ApplySort(codeDocumentService.CodeDocumentViewModel, sortOrder); - await codeDocumentService.UpdateCodeDocumentViewModel(clientContext.Extensibility, textViewSnapshot, cancellationToken); + await codeDocumentService.UpdateCodeDocumentViewModel( + clientContext.Extensibility, + textViewSnapshot.FilePath, + textViewSnapshot.Document.Text.CopyToString(), + cancellationToken); } /// diff --git a/src/CodeNav.OutOfProc/Languages/CSharp/Mappers/DocumentMapper.cs b/src/CodeNav.OutOfProc/Languages/CSharp/Mappers/DocumentMapper.cs index 7cf7d3e..f593627 100644 --- a/src/CodeNav.OutOfProc/Languages/CSharp/Mappers/DocumentMapper.cs +++ b/src/CodeNav.OutOfProc/Languages/CSharp/Mappers/DocumentMapper.cs @@ -3,30 +3,12 @@ using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.CSharp.Syntax; using Microsoft.VisualStudio.Extensibility; -using Microsoft.VisualStudio.Extensibility.Editor; using Microsoft.VisualStudio.ProjectSystem.Query; namespace CodeNav.OutOfProc.Languages.CSharp.Mappers; public class DocumentMapper { - /// - /// Map text document to list of code items. - /// - /// Document snapshot with latest version of text - /// File path of the document snaphot, used to exclude the saved version of the text - /// Current view model connected to the CodeNav tool window - /// Visual Studio extensibility used to retrieve all solution files for compilation - /// Cancellation token - /// List of code items - public static async Task> MapDocument( - ITextDocumentSnapshot documentSnapshot, - string? excludeFilePath, - CodeDocumentViewModel codeDocumentViewModel, - VisualStudioExtensibility extensibility, - CancellationToken cancellationToken) - => await MapDocument(documentSnapshot.Text.CopyToString(), excludeFilePath, codeDocumentViewModel, extensibility, cancellationToken); - /// /// Map text document to list of code items. /// diff --git a/src/CodeNav.OutOfProc/Models/DocumentView.cs b/src/CodeNav.OutOfProc/Models/DocumentView.cs new file mode 100644 index 0000000..cd12cda --- /dev/null +++ b/src/CodeNav.OutOfProc/Models/DocumentView.cs @@ -0,0 +1,10 @@ +namespace CodeNav.OutOfProc.Models; + +public sealed class DocumentView +{ + public string FilePath { get; set; } = string.Empty; + + public string Text { get; set; } = string.Empty; + + public bool IsDocumentFrame { get; set; } +} diff --git a/src/CodeNav.OutOfProc/Services/CodeDocumentService.cs b/src/CodeNav.OutOfProc/Services/CodeDocumentService.cs index b5bdb65..67cfcb9 100644 --- a/src/CodeNav.OutOfProc/Services/CodeDocumentService.cs +++ b/src/CodeNav.OutOfProc/Services/CodeDocumentService.cs @@ -5,7 +5,6 @@ using CodeNav.OutOfProc.Models; using CodeNav.OutOfProc.ViewModels; using Microsoft.VisualStudio.Extensibility; -using Microsoft.VisualStudio.Extensibility.Editor; using Microsoft.VisualStudio.Extensibility.UI; using System.Windows; @@ -13,7 +12,8 @@ namespace CodeNav.OutOfProc.Services; public class CodeDocumentService( OutputWindowService logService, - OutliningHelper outliningHelper) + OutliningService outliningService, + WindowFrameService windowFrameService) { /// /// DataContext for the tool window. @@ -34,17 +34,19 @@ public class CodeDocumentService( public OutputWindowService LogService => logService; - public OutliningHelper OutliningHelper => outliningHelper; + public OutliningService OutliningService => outliningService; public async Task UpdateCodeDocumentViewModel( VisualStudioExtensibility? extensibility, - ITextViewSnapshot? textView, + string? filePath, + string? text, CancellationToken cancellationToken) { try { if (extensibility == null || - textView == null) + string.IsNullOrEmpty(filePath) || + string.IsNullOrEmpty(text)) { return CodeDocumentViewModel; } @@ -60,20 +62,20 @@ public async Task UpdateCodeDocumentViewModel( // Get the new list of code items var codeItems = await DocumentMapper.MapDocument( - textView.Document, - textView.FilePath, + text, + filePath, CodeDocumentViewModel, extensibility, cancellationToken); - await logService.WriteInfo(textView, $"Found '{codeItems.Count}' code items"); + await logService.WriteInfo(filePath, $"Found '{codeItems.Count}' code items"); // Getting the new code items is done, cancel creating a loading placeholder await loadingCancellationTokenSource.CancelAsync(); // Set properties on the CodeDocumentViewModel that are needed for other features CodeDocumentViewModel.CodeDocumentService = this; - CodeDocumentViewModel.FilePath = textView.FilePath ?? string.Empty; + CodeDocumentViewModel.FilePath = filePath ?? string.Empty; if (!codeItems.Any()) { @@ -85,37 +87,39 @@ public async Task UpdateCodeDocumentViewModel( // And update the DataContext for the tool window CodeDocumentViewModel.CodeItems = SortHelper.Sort(codeItems, CodeDocumentViewModel.SortOrder); - await logService.WriteInfo(textView, $"Sorted code items on '{CodeDocumentViewModel.SortOrder}'"); + await logService.WriteInfo(filePath, $"Sorted code items on '{CodeDocumentViewModel.SortOrder}'"); // Apply highlights HighlightHelper.UnHighlight(CodeDocumentViewModel); - await logService.WriteInfo(textView, $"Remove highlight from all code items"); + await logService.WriteInfo(filePath, $"Remove highlight from all code items"); // Apply current visibility settings to the document VisibilityHelper.SetCodeItemVisibility(CodeDocumentViewModel, CodeDocumentViewModel.CodeItems, CodeDocumentViewModel.FilterRules); - await logService.WriteInfo(textView, $"Set code item visibility"); + await logService.WriteInfo(filePath, $"Set code item visibility"); // Apply filter rules FilterRuleHelper.ApplyFilterRules(CodeDocumentViewModel, CodeDocumentViewModel.CodeItems, CodeDocumentViewModel.FilterRules); - await logService.WriteInfo(textView, $"Set code item filter rules"); + await logService.WriteInfo(filePath, $"Set code item filter rules"); // Apply history items HistoryHelper.ApplyHistoryIndicator(CodeDocumentViewModel); - await logService.WriteInfo(textView, $"Apply history indicators"); + await logService.WriteInfo(filePath, $"Apply history indicators"); // Apply bookmarks BookmarkHelper.ApplyBookmarkIndicator(CodeDocumentViewModel); - await logService.WriteInfo(textView, $"Apply bookmark indicators"); + await logService.WriteInfo(filePath, $"Apply bookmark indicators"); // Apply outlining - await OutliningHelper.SubscribeToRegionEvents(CodeDocumentViewModel); + await OutliningService.SubscribeToRegionEvents(CodeDocumentViewModel); - await logService.WriteInfo(textView, $"Apply outlining"); + await logService.WriteInfo(filePath, $"Apply outlining"); + + await windowFrameService.SubscribeToWindowFrameEvents(); return CodeDocumentViewModel; } diff --git a/src/CodeNav.OutOfProc/Services/IOutOfProcService.cs b/src/CodeNav.OutOfProc/Services/IOutOfProcService.cs index 103ebab..3ea8a04 100644 --- a/src/CodeNav.OutOfProc/Services/IOutOfProcService.cs +++ b/src/CodeNav.OutOfProc/Services/IOutOfProcService.cs @@ -8,6 +8,8 @@ public interface IOutOfProcService Task SetCodeItemIsExpanded(int spanStart, int spanEnd, bool isExpanded); + Task ProcessActiveFrameChanged(string documentViewJsonString); + 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 b98c183..8b5e39d 100644 --- a/src/CodeNav.OutOfProc/Services/OutOfProcService.cs +++ b/src/CodeNav.OutOfProc/Services/OutOfProcService.cs @@ -1,24 +1,16 @@ -using CodeNav.OutOfProc.Helpers; +using CodeNav.OutOfProc.Models; using Microsoft.ServiceHub.Framework; using Microsoft.VisualStudio.Extensibility; using Microsoft.VisualStudio.Extensibility.Shell; +using System.Text.Json; namespace CodeNav.OutOfProc.Services; [VisualStudioContribution] -internal class OutOfProcService : IOutOfProcService, IBrokeredService +internal class OutOfProcService( + VisualStudioExtensibility extensibility, + CodeDocumentService codeDocumentService) : IOutOfProcService, IBrokeredService { - private readonly VisualStudioExtensibility _extensibility; - private readonly CodeDocumentService _codeDocumentService; - - public OutOfProcService( - VisualStudioExtensibility extensibility, - CodeDocumentService codeDocumentService) - { - _extensibility = extensibility; - _codeDocumentService = codeDocumentService; - } - public static BrokeredServiceConfiguration BrokeredServiceConfiguration => new(IOutOfProcService.Configuration.ServiceName, IOutOfProcService.Configuration.ServiceVersion, typeof(OutOfProcService)) { @@ -29,11 +21,34 @@ public static BrokeredServiceConfiguration BrokeredServiceConfiguration public async Task SetCodeItemIsExpanded(int spanStart, int spanEnd, bool isExpanded) { - OutliningHelper.SetIsExpanded(_codeDocumentService.CodeDocumentViewModel, spanStart, spanEnd, isExpanded); + OutliningService.SetIsExpanded(codeDocumentService.CodeDocumentViewModel, spanStart, spanEnd, isExpanded); + } + + public async Task ProcessActiveFrameChanged(string documentViewJsonString) + { + var documentView = JsonSerializer.Deserialize(documentViewJsonString); + + // Conditions: + // - Frame is null + // - Frame did not change + // - Frame is not a document frame + // Actions: + // - Do nothing + if (documentView?.IsDocumentFrame != true) + { + return; + } + + // Frame has changed and has a text document, so we need to update the list of code items + await codeDocumentService.UpdateCodeDocumentViewModel( + extensibility, + documentView.FilePath, + documentView.Text, + default); } public async Task DoSomethingAsync(CancellationToken cancellationToken) { - await _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/Helpers/OutliningHelper.cs b/src/CodeNav.OutOfProc/Services/OutliningService.cs similarity index 95% rename from src/CodeNav.OutOfProc/Helpers/OutliningHelper.cs rename to src/CodeNav.OutOfProc/Services/OutliningService.cs index 84b6907..c52b0e1 100644 --- a/src/CodeNav.OutOfProc/Helpers/OutliningHelper.cs +++ b/src/CodeNav.OutOfProc/Services/OutliningService.cs @@ -8,15 +8,15 @@ using Microsoft.VisualStudio.Extensibility.Helpers; using System.Text.Json; -namespace CodeNav.OutOfProc.Helpers; +namespace CodeNav.OutOfProc.Services; -public class OutliningHelper : DisposableObject +public class OutliningService : DisposableObject { private readonly VisualStudioExtensibility _extensibility; private readonly Task _initializationTask; private IInProcService? _inProcService; - public OutliningHelper(VisualStudioExtensibility extensibility) + public OutliningService(VisualStudioExtensibility extensibility) { _extensibility = extensibility; _initializationTask = Task.Run(InitializeAsync); @@ -83,7 +83,7 @@ public async Task ExpandOutlineRegion(int start, int length) public static async Task CollapseOutlineRegion(CodeItem codeItem) { - if (codeItem.CodeDocumentViewModel?.CodeDocumentService?.OutliningHelper == null) + if (codeItem.CodeDocumentViewModel?.CodeDocumentService?.OutliningService == null) { return; } @@ -91,13 +91,13 @@ public static async Task CollapseOutlineRegion(CodeItem codeItem) await codeItem .CodeDocumentViewModel .CodeDocumentService - .OutliningHelper + .OutliningService .CollapseOutlineRegion(codeItem.OutlineSpan.Start, codeItem.OutlineSpan.Length); } public static async Task ExpandOutlineRegion(CodeItem codeItem) { - if (codeItem.CodeDocumentViewModel?.CodeDocumentService?.OutliningHelper == null) + if (codeItem.CodeDocumentViewModel?.CodeDocumentService?.OutliningService == null) { return; } @@ -105,7 +105,7 @@ public static async Task ExpandOutlineRegion(CodeItem codeItem) await codeItem .CodeDocumentViewModel .CodeDocumentService - .OutliningHelper + .OutliningService .ExpandOutlineRegion(codeItem.OutlineSpan.Start, codeItem.OutlineSpan.Length); } diff --git a/src/CodeNav.OutOfProc/Services/OutputWindowService.cs b/src/CodeNav.OutOfProc/Services/OutputWindowService.cs index df78095..904e333 100644 --- a/src/CodeNav.OutOfProc/Services/OutputWindowService.cs +++ b/src/CodeNav.OutOfProc/Services/OutputWindowService.cs @@ -1,6 +1,5 @@ using Microsoft.VisualStudio.Extensibility; using Microsoft.VisualStudio.Extensibility.Documents; -using Microsoft.VisualStudio.Extensibility.Editor; using Microsoft.VisualStudio.Extensibility.Helpers; namespace CodeNav.OutOfProc.Services; @@ -19,14 +18,11 @@ public OutputWindowService(VisualStudioExtensibility extensibility) _initializationTask = Task.Run(InitializeAsync); } - public async Task WriteInfo(ITextViewSnapshot textView, string text) - => await WriteInfo(Path.GetFileName(textView.FilePath), text); - public async Task WriteInfo(Uri? FilePath, string text) => await WriteInfo(Path.GetFileName(FilePath?.AbsolutePath), text); - public async Task WriteInfo(string? fileName, string text) - => await WriteLine($"[Info] [{fileName}] {text}"); + public async Task WriteInfo(string? filePath, string text) + => await WriteLine($"[Info] [{Path.GetFileName(filePath)}] {text}"); public async Task WriteLine(string text) { diff --git a/src/CodeNav.OutOfProc/Services/WindowFrameService.cs b/src/CodeNav.OutOfProc/Services/WindowFrameService.cs new file mode 100644 index 0000000..8b3125f --- /dev/null +++ b/src/CodeNav.OutOfProc/Services/WindowFrameService.cs @@ -0,0 +1,68 @@ +using CodeNav.Services; +using Microsoft; +using Microsoft.VisualStudio.Extensibility; +using Microsoft.VisualStudio.Extensibility.Helpers; + +namespace CodeNav.OutOfProc.Services; + +public class WindowFrameService : DisposableObject +{ + private readonly VisualStudioExtensibility _extensibility; + private readonly OutputWindowService _outputWindowService; + private readonly Task _initializationTask; + private IInProcService? _inProcService; + + private bool _isSubscribed; + + public WindowFrameService( + VisualStudioExtensibility extensibility, + OutputWindowService outputWindowService) + { + _extensibility = extensibility; + _initializationTask = Task.Run(InitializeAsync); + _outputWindowService = outputWindowService; + } + + public async Task SubscribeToWindowFrameEvents() + { + if (_isSubscribed) + { + await _outputWindowService.WriteLine("[Info] Already subscribed to window frame events."); + return; + } + + try + { + Assumes.NotNull(_inProcService); + + // Subscribe to window frame events + await _inProcService.SubscribeToWindowFrameEvents(); + + await _outputWindowService.WriteLine("[Info] Subscribed to window frame events."); + + _isSubscribed = true; + } + catch (Exception e) + { + await _outputWindowService.WriteException("Exception subscribing to window frame events", e); + } + } + + 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/TextViewEventListener.cs b/src/CodeNav.OutOfProc/TextViewEventListener.cs index 263449e..f73aef8 100644 --- a/src/CodeNav.OutOfProc/TextViewEventListener.cs +++ b/src/CodeNav.OutOfProc/TextViewEventListener.cs @@ -59,7 +59,11 @@ public async Task TextViewChangedAsync(TextViewChangedArgs args, CancellationTok if ((args.Edits.Any() && codeDocumentService.SettingsDialogData.UpdateWhileTyping) || args.AfterTextView.FilePath != codeDocumentService.CodeDocumentViewModel.FilePath) { - await codeDocumentService.UpdateCodeDocumentViewModel(Extensibility, args.AfterTextView, cancellationToken); + await codeDocumentService.UpdateCodeDocumentViewModel( + Extensibility, + args.AfterTextView.FilePath, + args.AfterTextView.Document.Text.CopyToString(), + cancellationToken); } // Document changed - Update history indicators @@ -114,7 +118,11 @@ public async Task TextViewOpenedAsync(ITextViewSnapshot textViewSnapshot, Cancel return; } - await codeDocumentService.UpdateCodeDocumentViewModel(Extensibility, textViewSnapshot, cancellationToken); + await codeDocumentService.UpdateCodeDocumentViewModel( + Extensibility, + textViewSnapshot.FilePath, + textViewSnapshot.Document.Text.CopyToString(), + cancellationToken); } catch (Exception e) { diff --git a/src/CodeNav.OutOfProc/ViewModels/CodeClassItem.cs b/src/CodeNav.OutOfProc/ViewModels/CodeClassItem.cs index eadd66b..91e5d65 100644 --- a/src/CodeNav.OutOfProc/ViewModels/CodeClassItem.cs +++ b/src/CodeNav.OutOfProc/ViewModels/CodeClassItem.cs @@ -1,5 +1,5 @@ -using CodeNav.OutOfProc.Helpers; -using CodeNav.OutOfProc.Interfaces; +using CodeNav.OutOfProc.Interfaces; +using CodeNav.OutOfProc.Services; using Microsoft.VisualStudio.Extensibility; using Microsoft.VisualStudio.Extensibility.UI; using System.Runtime.Serialization; @@ -45,11 +45,11 @@ public bool IsExpanded if (value) { - _ = OutliningHelper.ExpandOutlineRegion(this); + _ = OutliningService.ExpandOutlineRegion(this); } else { - _ = OutliningHelper.CollapseOutlineRegion(this); + _ = OutliningService.CollapseOutlineRegion(this); } } } diff --git a/src/CodeNav.OutOfProc/ViewModels/CodeDocumentViewModel.cs b/src/CodeNav.OutOfProc/ViewModels/CodeDocumentViewModel.cs index eb882b4..f2d813b 100644 --- a/src/CodeNav.OutOfProc/ViewModels/CodeDocumentViewModel.cs +++ b/src/CodeNav.OutOfProc/ViewModels/CodeDocumentViewModel.cs @@ -5,6 +5,7 @@ using CodeNav.OutOfProc.Models; using CodeNav.OutOfProc.Services; using Microsoft.VisualStudio.Extensibility; +using Microsoft.VisualStudio.Extensibility.Editor; using Microsoft.VisualStudio.Extensibility.UI; using Microsoft.VisualStudio.RpcContracts.Notifications; using System.Runtime.Serialization; @@ -170,7 +171,11 @@ private async Task Refresh(object? commandParameter, IClientContext clientContex return; } - await CodeDocumentService.UpdateCodeDocumentViewModel(clientContext.Extensibility, textViewSnapshot, cancellationToken); + await CodeDocumentService.UpdateCodeDocumentViewModel( + clientContext.Extensibility, + textViewSnapshot.FilePath, + textViewSnapshot.Document.Text.CopyToString(), + cancellationToken); } [DataMember] @@ -198,14 +203,14 @@ private async Task SortByType(object? commandParameter, IClientContext clientCon public AsyncCommand ExpandAllCommand { get; } private async Task ExpandAll(object? commandParameter, IClientContext clientContext, CancellationToken cancellationToken) { - OutliningHelper.ExpandAll(CodeDocumentService?.CodeDocumentViewModel); + OutliningService.ExpandAll(CodeDocumentService?.CodeDocumentViewModel); } [DataMember] public AsyncCommand CollapseAllCommand { get; } private async Task CollapseAll(object? commandParameter, IClientContext clientContext, CancellationToken cancellationToken) { - OutliningHelper.CollapseAll(CodeDocumentService?.CodeDocumentViewModel); + OutliningService.CollapseAll(CodeDocumentService?.CodeDocumentViewModel); } [DataMember] diff --git a/src/CodeNav.OutOfProc/ViewModels/CodeItem.cs b/src/CodeNav.OutOfProc/ViewModels/CodeItem.cs index bbb5de0..5f47839 100644 --- a/src/CodeNav.OutOfProc/ViewModels/CodeItem.cs +++ b/src/CodeNav.OutOfProc/ViewModels/CodeItem.cs @@ -1,5 +1,6 @@ using CodeNav.OutOfProc.Constants; using CodeNav.OutOfProc.Helpers; +using CodeNav.OutOfProc.Services; using CodeNav.Services; using Microsoft; using Microsoft.CodeAnalysis.Text; @@ -336,21 +337,31 @@ public async Task CopyName(object? commandParameter, IClientContext clientContex public AsyncCommand RefreshCommand { get; } public async Task Refresh(object? commandParameter, IClientContext clientContext, CancellationToken cancellationToken) { - var textView = await clientContext.GetActiveTextViewAsync(cancellationToken); + var textViewSnapshot = await clientContext.GetActiveTextViewAsync(cancellationToken); + + if (textViewSnapshot == null) + { + return; + } + await CodeDocumentViewModel! .CodeDocumentService! - .UpdateCodeDocumentViewModel(clientContext.Extensibility, textView, cancellationToken); + .UpdateCodeDocumentViewModel( + clientContext.Extensibility, + textViewSnapshot.FilePath, + textViewSnapshot.Document.Text.CopyToString(), + cancellationToken); } [DataMember] public AsyncCommand ExpandAllCommand { get; } public async Task ExpandAll(object? commandParameter, IClientContext clientContext, CancellationToken cancellationToken) - => OutliningHelper.ExpandAll(CodeDocumentViewModel!); + => OutliningService.ExpandAll(CodeDocumentViewModel!); [DataMember] public AsyncCommand CollapseAllCommand { get; } public async Task CollapseAll(object? commandParameter, IClientContext clientContext, CancellationToken cancellationToken) - => OutliningHelper.CollapseAll(CodeDocumentViewModel!); + => OutliningService.CollapseAll(CodeDocumentViewModel!); [DataMember] public AsyncCommand AddBookmarkCommand { get; } diff --git a/src/CodeNav/ExtensionEntrypoint.cs b/src/CodeNav/ExtensionEntrypoint.cs index 88b3e6f..c9d285c 100644 --- a/src/CodeNav/ExtensionEntrypoint.cs +++ b/src/CodeNav/ExtensionEntrypoint.cs @@ -24,7 +24,9 @@ protected override void InitializeServices(IServiceCollection serviceCollection) base.InitializeServices(serviceCollection); - // You can configure dependency injection here by adding services to the serviceCollection. + // As of now, any instance that ingests VisualStudioExtensibility is required to be added as a scoped + // service. + serviceCollection.AddScoped(); } } } diff --git a/src/CodeNav/Models/DocumentView.cs b/src/CodeNav/Models/DocumentView.cs new file mode 100644 index 0000000..fb52ac1 --- /dev/null +++ b/src/CodeNav/Models/DocumentView.cs @@ -0,0 +1,10 @@ +namespace CodeNav.Models; + +public sealed class DocumentView +{ + public string FilePath { get; set; } = string.Empty; + + public string Text { get; set; } = string.Empty; + + public bool IsDocumentFrame { get; set; } +} diff --git a/src/CodeNav/Services/IInProcService.cs b/src/CodeNav/Services/IInProcService.cs index b354619..5ccd3ca 100644 --- a/src/CodeNav/Services/IInProcService.cs +++ b/src/CodeNav/Services/IInProcService.cs @@ -16,6 +16,8 @@ public interface IInProcService Task SubscribeToRegionEvents(); + Task SubscribeToWindowFrameEvents(); + 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 403ed78..dbabc4d 100644 --- a/src/CodeNav/Services/InProcService.cs +++ b/src/CodeNav/Services/InProcService.cs @@ -1,38 +1,36 @@ using CodeNav.Models; using CodeNav.OutOfProc.Services; using Microsoft; -using Microsoft.VisualStudio; -using Microsoft.VisualStudio.Editor; using Microsoft.VisualStudio.Extensibility; using Microsoft.VisualStudio.Extensibility.Shell; using Microsoft.VisualStudio.Extensibility.VSSdkCompatibility; using Microsoft.VisualStudio.Shell; +using Microsoft.VisualStudio.Shell.Interop; using Microsoft.VisualStudio.Text; using Microsoft.VisualStudio.Text.Editor; using Microsoft.VisualStudio.Text.Outlining; -using Microsoft.VisualStudio.TextManager.Interop; using System.Text.Json; namespace CodeNav.Services; [VisualStudioContribution] -internal class InProcService : IInProcService +internal class InProcService : IInProcService, IVsWindowFrameEvents { private readonly VisualStudioExtensibility _extensibility; - private readonly MefInjection _editorAdaptersFactoryService; + private readonly TextViewService _textViewService; private readonly MefInjection _outliningManagerFactoryService; - private readonly AsyncServiceProviderInjection _textManager; + private readonly AsyncServiceProviderInjection _vsUIShell; public InProcService( VisualStudioExtensibility extensibility, - MefInjection editorAdaptersFactoryService, + TextViewService textViewService, MefInjection outliningManagerFactoryService, - AsyncServiceProviderInjection textManager) + AsyncServiceProviderInjection vsUIShell) { _extensibility = extensibility; - _editorAdaptersFactoryService = editorAdaptersFactoryService; + _textViewService = textViewService; _outliningManagerFactoryService = outliningManagerFactoryService; - _textManager = textManager; + _vsUIShell = vsUIShell; } [VisualStudioContribution] @@ -58,7 +56,7 @@ public async Task DoSomethingAsync(CancellationToken cancellationToken) public async Task SubscribeToRegionEvents() { // Not using context.GetActiveTextViewAsync here because VisualStudio.Extensibility doesn't support outlining yet. - var textView = await GetCurrentTextViewAsync(); + var textView = await _textViewService.GetCurrentTextViewAsync(); var outliningManager = await GetOutliningManager(textView); @@ -158,7 +156,7 @@ public async Task ExpandOutlineRegion(int spanStart, int spanLength) try { // Not using context.GetActiveTextViewAsync here because VisualStudio.Extensibility doesn't support outlining yet. - var textView = await GetCurrentTextViewAsync(); + var textView = await _textViewService.GetCurrentTextViewAsync(); var outliningManager = await GetOutliningManager(textView); @@ -194,7 +192,7 @@ public async Task CollapseOutlineRegion(int spanStart, int spanLength) try { // Not using context.GetActiveTextViewAsync here because VisualStudio.Extensibility doesn't support outlining yet. - var textView = await GetCurrentTextViewAsync(); + var textView = await _textViewService.GetCurrentTextViewAsync(); var outliningManager = await GetOutliningManager(textView); @@ -267,30 +265,7 @@ private async Task GetOutliningManager(IWpfTextView textView) /// Length of the span /// public async Task TextViewScrollToSpan(int start, int length) - { - try - { - // Not using context.GetActiveTextViewAsync here because VisualStudio.Extensibility doesn't support viewscroller yet. - var textView = await GetCurrentTextViewAsync(); - - if (!textView.TextBuffer.ContentType.TypeName.Equals("CSharp")) - { - // TODO: Log that the cursor and thus active text view is not in a csharp editor, for example the output window. - return; - } - - 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(); - - textView.ViewScroller.EnsureSpanVisible(span, EnsureSpanVisibleOptions.ShowStart); - } - catch (Exception) - { - // TODO: Implement in-proc error logging - } - } + => await _textViewService.ScrollToSpan(start, length); /// /// Move the caret in the text view to the given position and keep the keyboard focus on the text view @@ -298,46 +273,76 @@ public async Task TextViewScrollToSpan(int start, int length) /// Position in the text view /// Awaitable Task public async Task TextViewMoveCaretToPosition(int position) + => await _textViewService.MoveCaretToPosition(position); + + #endregion + + #region Window Frame Events + + public async Task SubscribeToWindowFrameEvents() + { + var vsUIShell = await _vsUIShell.GetServiceAsync(); + + // Switch to the UI thread to ensure we can interact with the window frames. + await ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync(); + + vsUIShell.AdviseWindowFrameEvents(this); + } + + public void OnFrameCreated(IVsWindowFrame frame) + { + // Ignore + } + + public void OnFrameDestroyed(IVsWindowFrame frame) { + // Ignore + } + + public void OnFrameIsVisibleChanged(IVsWindowFrame frame, bool newIsVisible) + { + // Ignore + } + + public void OnFrameIsOnScreenChanged(IVsWindowFrame frame, bool newIsOnScreen) + { + // Ignore + } + +#pragma warning disable VSTHRD100 // Avoid async void methods + public async void OnActiveFrameChanged(IVsWindowFrame oldFrame, IVsWindowFrame newFrame) +#pragma warning restore VSTHRD100 // Avoid async void methods + { + var outOfProcService = await _extensibility.ServiceBroker + .GetProxyAsync(IOutOfProcService.Configuration.ServiceDescriptor, cancellationToken: default); + try { - // Not using context.GetActiveTextViewAsync here because VisualStudio.Extensibility doesn't support viewscroller yet. - var textView = await GetCurrentTextViewAsync(); + Assumes.NotNull(outOfProcService); - if (!textView.TextBuffer.ContentType.TypeName.Equals("CSharp")) + // Check if the new frame is different from the old frame + if (oldFrame == newFrame) { - // TODO: Log that the cursor and thus active text view is not in a csharp editor, for example the output window. return; } - var snapShotPoint = new SnapshotPoint(textView.TextSnapshot, position); + var documentView = await _textViewService.GetDocumentView(newFrame); - // Switch to the UI thread to ensure we can interact with the view scroller. - await ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync(); + // Check if the new frame has a document view + if (documentView == null) + { + await outOfProcService.ProcessActiveFrameChanged(JsonSerializer.Serialize(new DocumentView { IsDocumentFrame = false })); + return; + } - textView.Caret.MoveTo(snapShotPoint); - textView.VisualElement.Focus(); + // Notify about new document frame with text, filepath + await outOfProcService.ProcessActiveFrameChanged(JsonSerializer.Serialize(documentView)); } - catch (Exception) + finally { - // TODO: Implement in-proc error logging + (outOfProcService as IDisposable)?.Dispose(); } } - private async Task GetCurrentTextViewAsync() - { - var editorAdapter = await _editorAdaptersFactoryService.GetServiceAsync(); - var view = editorAdapter.GetWpfTextView(await GetCurrentNativeTextViewAsync()); - Assumes.Present(view); - return view; - } - - private async Task GetCurrentNativeTextViewAsync() - { - var textManager = await _textManager.GetServiceAsync(); - ErrorHandler.ThrowOnFailure(textManager.GetActiveView(1, null, out IVsTextView activeView)); - return activeView; - } - #endregion } diff --git a/src/CodeNav/Services/TextViewService.cs b/src/CodeNav/Services/TextViewService.cs new file mode 100644 index 0000000..e7dca15 --- /dev/null +++ b/src/CodeNav/Services/TextViewService.cs @@ -0,0 +1,183 @@ +using CodeNav.Models; +using Microsoft; +using Microsoft.VisualStudio; +using Microsoft.VisualStudio.Editor; +using Microsoft.VisualStudio.Extensibility.VSSdkCompatibility; +using Microsoft.VisualStudio.Shell; +using Microsoft.VisualStudio.Shell.Interop; +using Microsoft.VisualStudio.Text; +using Microsoft.VisualStudio.Text.Editor; +using Microsoft.VisualStudio.TextManager.Interop; + +namespace CodeNav.Services; + +public class TextViewService +{ + private readonly MefInjection _editorAdaptersFactoryService; + private readonly AsyncServiceProviderInjection _textManager; + +#pragma warning disable IDE0290 // Use primary constructor + public TextViewService( +#pragma warning restore IDE0290 // Use primary constructor + MefInjection editorAdaptersFactoryService, + AsyncServiceProviderInjection textManager) + { + _editorAdaptersFactoryService = editorAdaptersFactoryService; + _textManager = textManager; + } + + /// + /// Scroll to a given span in the text view + /// + /// Caret will remain at its original position + /// Start position of the span + /// Length of the span + /// + public async Task ScrollToSpan(int start, int length) + { + try + { + // Not using context.GetActiveTextViewAsync here because VisualStudio.Extensibility doesn't support viewscroller yet. + var textView = await GetCurrentTextViewAsync(); + + if (!textView.TextBuffer.ContentType.TypeName.Equals("CSharp")) + { + // TODO: Log that the cursor and thus active text view is not in a csharp editor, for example the output window. + return; + } + + 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(); + + textView.ViewScroller.EnsureSpanVisible(span, EnsureSpanVisibleOptions.AlwaysCenter); + } + catch (Exception) + { + // TODO: Implement in-proc error logging + } + } + + /// + /// Move the caret in the text view to the given position and keep the keyboard focus on the text view + /// + /// Position in the text view + /// Awaitable Task + public async Task MoveCaretToPosition(int position) + { + try + { + // Not using context.GetActiveTextViewAsync here because VisualStudio.Extensibility doesn't support viewscroller yet. + var textView = await GetCurrentTextViewAsync(); + + if (!textView.TextBuffer.ContentType.TypeName.Equals("CSharp")) + { + // TODO: Log that the cursor and thus active text view is not in a csharp editor, for example the output window. + return; + } + + var snapShotPoint = new SnapshotPoint(textView.TextSnapshot, position); + + // Switch to the UI thread to ensure we can interact with the view scroller. + await ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync(); + + textView.Caret.MoveTo(snapShotPoint); + textView.VisualElement.Focus(); + } + catch (Exception) + { + // TODO: Implement in-proc error logging + } + } + + public async Task GetCurrentTextViewAsync() + { + var editorAdapter = await _editorAdaptersFactoryService.GetServiceAsync(); + var view = editorAdapter.GetWpfTextView(await GetCurrentNativeTextViewAsync()); + Assumes.Present(view); + return view; + } + + private async Task GetCurrentNativeTextViewAsync() + { + var textManager = await _textManager.GetServiceAsync(); + ErrorHandler.ThrowOnFailure(textManager.GetActiveView(1, null, out IVsTextView activeView)); + return activeView; + } + + /// + /// Get the document view from the window frame + /// + /// Document view is a combination of the file path and the text of a text document for a given frame + /// + /// + public async Task GetDocumentView(IVsWindowFrame windowFrame) + { + var textView = await GetTextView(windowFrame); + + if (textView == null) + { + return null; + } + + var textDocument = GetTextDocument(textView); + + if (textDocument == null) + { + return null; + } + + return new() + { + FilePath = textDocument?.FilePath ?? string.Empty, + Text = textView.TextBuffer.CurrentSnapshot.GetText(), + IsDocumentFrame = true, + }; + } + + /// + /// Gets the text view from the window frame. + /// + /// if the window isn't a document window. + private async Task GetTextView(IVsWindowFrame windowFrame) + { + try + { + await ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync(); + + // Force the loading of a document that may be pending initialization. + // See https://docs.microsoft.com/en-us/visualstudio/extensibility/internals/delayed-document-loading + windowFrame.GetProperty((int)__VSFPROPID.VSFPROPID_DocView, out _); + + var nativeView = VsShellUtilities.GetTextView(windowFrame); + + if (nativeView == null) + { + return null; + } + + var editorAdapter = await _editorAdaptersFactoryService.GetServiceAsync(); + + var view = editorAdapter.GetWpfTextView(nativeView); + + return view; + } + catch (Exception) + { + // TODO: Implement in-proc error logging + } + + return null; + } + + /// + /// Get the text document associated with the given text view + /// + /// WPF Text View + /// + private static ITextDocument? GetTextDocument(IWpfTextView textView) + => textView != null && textView.TextBuffer.Properties.TryGetProperty(typeof(ITextDocument), out ITextDocument document) + ? document + : null; +}