File: DocumentOutline\DocumentOutlineControl.xaml.cs
Web Access
Project: ..\..\..\src\VisualStudio\Core\Def\Microsoft.VisualStudio.LanguageServices_ckcrqypr_wpftmp.csproj (Microsoft.VisualStudio.LanguageServices)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
 
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Threading;
using System.Windows;
using System.Windows.Controls;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Editor.Shared.Extensions;
using Microsoft.CodeAnalysis.Editor.Shared.Tagging;
using Microsoft.CodeAnalysis.Editor.Shared.Utilities;
using Microsoft.CodeAnalysis.Editor.Tagging;
using Microsoft.CodeAnalysis.Shared.TestHooks;
using Microsoft.VisualStudio.Editor;
using Microsoft.VisualStudio.LanguageServer.Client;
using Microsoft.VisualStudio.LanguageServices.Implementation;
using Microsoft.VisualStudio.Text;
using Microsoft.VisualStudio.Text.Editor;
using Microsoft.VisualStudio.TextManager.Interop;
using Roslyn.Utilities;
 
namespace Microsoft.VisualStudio.LanguageServices.DocumentOutline
{
    /// <summary>
    /// Interaction logic for DocumentOutlineControl.xaml
    /// </summary>
    internal partial class DocumentOutlineControl : UserControl, IVsCodeWindowEvents, IDisposable
    {
        private readonly ILanguageServiceBroker2 _languageServiceBroker;
        private readonly IThreadingContext _threadingContext;
        private readonly IAsynchronousOperationListener _asyncListener;
        private readonly IVsEditorAdaptersFactoryService _editorAdaptersFactoryService;
        private readonly IVsCodeWindow _codeWindow;
        private readonly ComEventSink _codeWindowEventsSink;
        private readonly CompilationAvailableTaggerEventSource _textViewEventSource;
        private readonly CancellationTokenSource _cancellationTokenSource;
        private CancellationToken CancellationToken => _cancellationTokenSource.Token;
 
        /// <summary>
        /// The type of sorting to be applied to the data model in <see cref="FilterAndSortDataModelAsync"/>.
        /// </summary>
        /// <remarks>
        /// It is only safe to read/mutate SortOption from the UI thread.
        /// </remarks>
        private SortOption SortOption
        {
            get
            {
                _threadingContext.ThrowIfNotOnUIThread();
                return _sortOption;
            }
            set
            {
                _threadingContext.ThrowIfNotOnUIThread();
                _sortOption = value;
            }
        }
 
        private SortOption _sortOption;
 
        /// <summary>
        /// Queue to batch up work to do to compute the data model. Used so we can batch up a lot of events 
        /// and only fetch the model once for every batch. The bool parameter is unused.
        /// </summary>
        private readonly AsyncBatchingWorkQueue<bool, DocumentSymbolDataModel?> _computeDataModelQueue;
 
        /// <summary>
        /// Queue to batch up work to do to filter and sort the data model. The bool parameter is unused.
        /// </summary>
        private readonly AsyncBatchingWorkQueue<bool, DocumentSymbolDataModel?> _filterAndSortDataModelQueue;
 
        /// <summary>
        /// Queue to batch up work to do to highlight the currently selected symbol node, expand/collapse nodes,
        /// then update the UI.
        /// </summary>
        private readonly AsyncBatchingWorkQueue<ExpansionOption> _highlightExpandAndPresentItemsQueue;
 
        /// <summary>
        /// Keeps track of the current primary and secondary text views. Should only be accessed by the UI thread.
        /// </summary>
        private readonly Dictionary<IVsTextView, ITextView> _trackedTextViews = new();
 
        public DocumentOutlineControl(
            ILanguageServiceBroker2 languageServiceBroker,
            IThreadingContext threadingContext,
            IAsynchronousOperationListener asyncListener,
            IVsEditorAdaptersFactoryService editorAdaptersFactoryService,
            IVsCodeWindow codeWindow)
        {
            InitializeComponent();
 
            _languageServiceBroker = languageServiceBroker;
            _threadingContext = threadingContext;
            _asyncListener = asyncListener;
            _editorAdaptersFactoryService = editorAdaptersFactoryService;
            _codeWindow = codeWindow;
            _cancellationTokenSource = new CancellationTokenSource();
            SortOption = SortOption.Location;
 
            _computeDataModelQueue = new AsyncBatchingWorkQueue<bool, DocumentSymbolDataModel?>(
                DelayTimeSpan.Short,
                ComputeDataModelAsync,
                EqualityComparer<bool>.Default,
                asyncListener,
                CancellationToken);
 
            _filterAndSortDataModelQueue = new AsyncBatchingWorkQueue<bool, DocumentSymbolDataModel?>(
                DelayTimeSpan.NearImmediate,
                FilterAndSortDataModelAsync,
                EqualityComparer<bool>.Default,
                asyncListener,
                CancellationToken);
 
            _highlightExpandAndPresentItemsQueue = new AsyncBatchingWorkQueue<ExpansionOption>(
                DelayTimeSpan.NearImmediate,
                HighlightExpandAndPresentItemsAsync,
                asyncListener,
                CancellationToken);
 
            // We don't think the shell is initialized lazily, so we'll Debug.Fail(), but if it was we'd still
            // see the view created later so this will still function.
            if (ErrorHandler.Failed(codeWindow.GetPrimaryView(out var primaryTextView)))
                Debug.Fail("GetPrimaryView failed during DocumentOutlineControl initialization.");
 
            if (ErrorHandler.Failed(StartTrackingView(primaryTextView)))
                Debug.Fail("StartTrackingView failed during DocumentOutlineControl initialization.");
 
            if (ErrorHandler.Succeeded(codeWindow.GetSecondaryView(out var secondaryTextView)))
            {
                if (ErrorHandler.Failed(StartTrackingView(secondaryTextView)))
                    Debug.Fail("StartTrackingView failed during DocumentOutlineControl initialization.");
            }
 
            var subjectBuffer = _trackedTextViews[primaryTextView].TextBuffer;
            _textViewEventSource = new CompilationAvailableTaggerEventSource(
                subjectBuffer,
                _asyncListener,
                // Any time an edit happens, recompute as the document symbols may have changed.
                TaggerEventSources.OnTextChanged(subjectBuffer),
                // Switching what is the active context may change the document symbols.
                TaggerEventSources.OnDocumentActiveContextChanged(subjectBuffer),
                // Many workspace changes may need us to change the document symbols (like options changing, or project renaming).
                TaggerEventSources.OnWorkspaceChanged(subjectBuffer, _asyncListener),
                // Once we hook this buffer up to the workspace, then we can start computing the document symbols.
                TaggerEventSources.OnWorkspaceRegistrationChanged(subjectBuffer));
 
            _textViewEventSource.Changed += OnEventSourceChanged;
            _textViewEventSource.Connect();
            _codeWindowEventsSink = ComEventSink.Advise<IVsCodeWindowEvents>(codeWindow, this);
            EnqueueComputeDataModelTask();
        }
 
        public void Dispose()
        {
            _threadingContext.ThrowIfNotOnUIThread();
            _codeWindowEventsSink.Unadvise();
            _textViewEventSource.Changed -= OnEventSourceChanged;
            _textViewEventSource.Disconnect();
            _cancellationTokenSource.Cancel();
            _cancellationTokenSource.Dispose();
        }
 
        int IVsCodeWindowEvents.OnNewView(IVsTextView pView)
        {
            _threadingContext.ThrowIfNotOnUIThread();
            return StartTrackingView(pView);
        }
 
        private int StartTrackingView(IVsTextView textView)
        {
            _threadingContext.ThrowIfNotOnUIThread();
 
            var wpfTextView = _editorAdaptersFactoryService.GetWpfTextView(textView);
            if (wpfTextView is null)
                return VSConstants.E_FAIL;
 
            _trackedTextViews.Add(textView, wpfTextView);
 
            // In the split window case, there's two views (each with its own caret position) but only one text buffer.
            // Subscribe to caret position changes once per view.
            wpfTextView.Caret.PositionChanged += Caret_PositionChanged;
 
            return VSConstants.S_OK;
        }
 
        int IVsCodeWindowEvents.OnCloseView(IVsTextView pView)
        {
            _threadingContext.ThrowIfNotOnUIThread();
 
            if (_trackedTextViews.TryGetValue(pView, out var view))
            {
                // In the split window case, there's two views (each with its own caret position) but only one text buffer.
                // Unsubscribe to caret position changes once per view.
                view.Caret.PositionChanged -= Caret_PositionChanged;
 
                _trackedTextViews.Remove(pView);
            }
 
            return VSConstants.S_OK;
        }
 
        private void OnEventSourceChanged(object sender, TaggerEventArgs e)
            => EnqueueComputeDataModelTask();
 
        /// <summary>
        /// On caret position change, highlight the corresponding symbol node in the window and update the view.
        /// </summary>
        private void Caret_PositionChanged(object sender, CaretPositionChangedEventArgs e)
        {
            if (!e.NewPosition.Equals(e.OldPosition))
                EnqueueHighlightExpandAndPresentItemsTask(ExpansionOption.CurrentExpansion);
        }
 
        private void ExpandAll(object sender, RoutedEventArgs e)
            => EnqueueHighlightExpandAndPresentItemsTask(ExpansionOption.Expand);
 
        private void CollapseAll(object sender, RoutedEventArgs e)
            => EnqueueHighlightExpandAndPresentItemsTask(ExpansionOption.Collapse);
 
        private void SearchBox_TextChanged(object sender, EventArgs e)
            => EnqueueFilterAndSortDataModelTask();
 
        private void SortByName(object sender, EventArgs e)
            => SetSortOptionAndUpdateDataModel(SortOption.Name);
 
        private void SortByOrder(object sender, EventArgs e)
            => SetSortOptionAndUpdateDataModel(SortOption.Location);
 
        private void SortByType(object sender, EventArgs e)
            => SetSortOptionAndUpdateDataModel(SortOption.Type);
 
        private void SetSortOptionAndUpdateDataModel(SortOption sortOption)
        {
            SortOption = sortOption;
            EnqueueFilterAndSortDataModelTask();
        }
 
        /// <summary>
        /// When a symbol node in the window is clicked, move the caret to its position in the latest active text view.
        /// </summary>
        private void SymbolTree_MouseDown(object sender, EventArgs e)
        {
            _threadingContext.ThrowIfNotOnUIThread();
            if (sender is StackPanel panel && panel.DataContext is DocumentSymbolUIItem symbol)
            {
                var activeTextView = GetLastActiveIWpfTextView();
                if (activeTextView is null)
                    return;
 
                // When the user clicks on a symbol node in the window, we want to move the cursor to that line in the editor. If we
                // don't unsubscribe from Caret_PositionChanged first, we will call EnqueueHighlightExpandAndPresentItemsTask() once
                // we move the cursor ourselves. This is not ideal because we would be doing extra work to update the view with an
                // identical document symbol tree.
                activeTextView.Caret.PositionChanged -= Caret_PositionChanged;
 
                // Prevents us from being permanently unsubscribed if an exception is thrown while updating the text view selection.
                try
                {
                    // Map the symbol's selection range start SnapshotPoint to a SnapshotPoint in the current textview then set the
                    // active text view caret position to this SnapshotPoint.
                    activeTextView.TryMoveCaretToAndEnsureVisible(
                        symbol.SelectionRangeSpan.Start.TranslateTo(activeTextView.TextSnapshot, PointTrackingMode.Negative));
                }
                finally
                {
                    // Resubscribe to Caret_PositionChanged again so that when the user clicks somewhere else, we can highlight that node.
                    activeTextView.Caret.PositionChanged += Caret_PositionChanged;
                }
            }
        }
    }
}