File: DocumentOutline\DocumentOutlineControl_ModelComputation.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.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.Collections;
using Microsoft.CodeAnalysis.Editor.Shared.Extensions;
using Microsoft.CodeAnalysis.Shared.Extensions;
using Microsoft.VisualStudio.LanguageServer.Protocol;
using Microsoft.VisualStudio.Shell.Interop;
using Microsoft.VisualStudio.Text.Editor;
using Microsoft.VisualStudio.Threading;
using Roslyn.Utilities;
 
namespace Microsoft.VisualStudio.LanguageServices.DocumentOutline
{
    /// The passing of the data model between the queues starts with _computeDataModelQueue which flows into _filterAndSortDataModelQueue which
    /// will then flow into _highlightExpandAndPresentItemsQueue. 
 
    /// Work is added to the _computeDataModelQueue when the user opens a new code window or makes changes to the text buffer.
    /// Work is added to the _filterAndSortDataModelQueue when the user performs a sort or search operation.
    /// Work is added to the _highlightExpandAndPresentItemsQueue when the user moves the caret around or expands/collapses all nodes.
 
    internal partial class DocumentOutlineControl
    {
        /// <summary>
        /// Enqueues a new task to compute the data model.
        /// </summary>
        private void EnqueueComputeDataModelTask()
        {
            // 'true' value is unused. This just signals to the queue that we have work to do.
            _computeDataModelQueue.AddWork(true);
        }
 
        /// <summary>
        /// Makes the LSP document symbol request and creates the data model.
        /// </summary>
        private async ValueTask<DocumentSymbolDataModel?> ComputeDataModelAsync(ImmutableSegmentedList<bool> _, CancellationToken cancellationToken)
        {
            // Jump to the UI thread to get the currently active text view. This cancellation token controls the entire DocumentOutlineControl
            // so if we are closed/cancelled on the UI thread, when this jumps back to the UI thread, it will auto-cancel and won't continue
            // further. We only get to the code below if the control is still in an active state.
            await _threadingContext.JoinableTaskFactory.SwitchToMainThreadAsync(cancellationToken);
 
            var activeTextView = GetLastActiveIWpfTextView();
            if (activeTextView is null)
                return null;
 
            var textBuffer = activeTextView.TextBuffer;
            var filePath = GetFilePath();
            if (filePath is null)
                return null;
 
            // Ensure we switch to the threadpool before calling ComputeModelAsync. It ensures that fetching and processing the document
            // symbol data model is not done on the UI thread.
            await TaskScheduler.Default;
 
            var model = await ComputeModelAsync().ConfigureAwait(false);
 
            // The model can be null if the LSP document symbol request returns a null response.
            if (model is not null)
                EnqueueFilterAndSortDataModelTask();
 
            return model;
 
            async Task<DocumentSymbolDataModel?> ComputeModelAsync()
            {
                cancellationToken.ThrowIfCancellationRequested();
 
                // Obtain the LSP response and text snapshot used.
                var response = await DocumentOutlineHelper.DocumentSymbolsRequestAsync(
                    textBuffer, _languageServiceBroker, filePath, cancellationToken).ConfigureAwait(false);
 
                // If there is no matching LSP server registered the client will return null here - e.g. wrong content type on the buffer, the
                // server totally failed to start, server doesn't support the right capabilities. For C# we might know it's a bug if we get a null
                // response here, but we don't know that in general for all languages.
                if (response is null)
                    return null;
 
                var responseBody = response.Value.response.ToObject<DocumentSymbol[]>();
                if (responseBody is null)
                    return null;
 
                return DocumentOutlineHelper.CreateDocumentSymbolDataModel(responseBody, response.Value.snapshot);
            }
 
            string? GetFilePath()
            {
                _threadingContext.ThrowIfNotOnUIThread();
                if (_editorAdaptersFactoryService.GetBufferAdapter(textBuffer) is IPersistFileFormat persistFileFormat &&
                    ErrorHandler.Succeeded(persistFileFormat.GetCurFile(out var filePath, out var _)))
                {
                    return filePath;
                }
 
                return null;
            }
        }
 
        /// <summary>
        /// Enqueues a new task to filter and sort the data model.
        /// </summary>
        private void EnqueueFilterAndSortDataModelTask()
        {
            // 'true' value is unused. This just signals to the queue that we have work to do.
            _filterAndSortDataModelQueue.AddWork(true);
        }
 
        /// <summary>
        /// Filters and sorts the data model.
        /// </summary>
        private async ValueTask<DocumentSymbolDataModel?> FilterAndSortDataModelAsync(ImmutableSegmentedList<bool> _, CancellationToken cancellationToken)
        {
            var model = await _computeDataModelQueue.WaitUntilCurrentBatchCompletesAsync().ConfigureAwait(false);
            if (model is null)
                return null;
 
            // Switch to the UI thread to get the current search query and sort option.
            await _threadingContext.JoinableTaskFactory.SwitchToMainThreadAsync(cancellationToken);
 
            var searchQuery = SearchBox.Text;
            var sortOption = SortOption;
 
            // Switch to the threadpool to filter and sort the data model.
            await TaskScheduler.Default;
 
            var updatedDocumentSymbolData = model.DocumentSymbolData;
 
            if (!string.IsNullOrWhiteSpace(searchQuery))
                updatedDocumentSymbolData = DocumentOutlineHelper.SearchDocumentSymbolData(updatedDocumentSymbolData, searchQuery, cancellationToken);
 
            updatedDocumentSymbolData = DocumentOutlineHelper.SortDocumentSymbolData(updatedDocumentSymbolData, sortOption, cancellationToken);
 
            EnqueueHighlightExpandAndPresentItemsTask(ExpansionOption.NoChange);
 
            return new DocumentSymbolDataModel(updatedDocumentSymbolData, model.OriginalSnapshot);
        }
 
        /// <summary>
        /// Enqueues a new task to highlight the symbol node corresponding to the current caret position in the editor, expand/collapse
        /// nodes, and update the UI.
        /// </summary>
        private void EnqueueHighlightExpandAndPresentItemsTask(ExpansionOption expansionOption)
        {
            _highlightExpandAndPresentItemsQueue.AddWork(expansionOption);
        }
 
        /// <summary>
        /// Highlights the symbol node corresponding to the current caret position in the editor, expands/collapses nodes, then updates the UI.
        /// </summary>
        private async ValueTask HighlightExpandAndPresentItemsAsync(ImmutableSegmentedList<ExpansionOption> expansionOption, CancellationToken cancellationToken)
        {
            var model = await _filterAndSortDataModelQueue.WaitUntilCurrentBatchCompletesAsync().ConfigureAwait(false);
            if (model is null)
                return;
 
            // Switch to the UI thread to get the current caret point and latest active text view then create the UI model.
            await _threadingContext.JoinableTaskFactory.SwitchToMainThreadAsync(cancellationToken);
 
            var activeTextView = GetLastActiveIWpfTextView();
            if (activeTextView is null)
                return;
 
            var caretPoint = activeTextView.GetCaretPoint(activeTextView.TextBuffer);
            if (!caretPoint.HasValue)
                return;
 
            var documentSymbolUIItems = DocumentOutlineHelper.GetDocumentSymbolUIItems(model.DocumentSymbolData, _threadingContext);
 
            // Switch to the threadpool to determine which node to select (if applicable).
            await TaskScheduler.Default;
 
            var symbolToSelect = DocumentOutlineHelper.GetDocumentNodeToSelect(documentSymbolUIItems, model.OriginalSnapshot, caretPoint.Value);
 
            // Switch to the UI thread to update the view.
            await _threadingContext.JoinableTaskFactory.SwitchToMainThreadAsync(cancellationToken);
 
            // Expand/collapse nodes based on the given Expansion Option.
            var expansion = expansionOption.Last();
            if (expansion is not ExpansionOption.NoChange && SymbolTree.ItemsSource is not null)
                DocumentOutlineHelper.SetIsExpanded(documentSymbolUIItems, (IEnumerable<DocumentSymbolUIItem>)SymbolTree.ItemsSource, expansion);
 
            // Hightlight the selected node if it exists, otherwise unselect all nodes (required so that the view does not select a node by default).
            if (symbolToSelect is not null)
            {
                // Expand all ancestors first to ensure the selected node will be visible.
                DocumentOutlineHelper.ExpandAncestors(documentSymbolUIItems, symbolToSelect.RangeSpan);
                symbolToSelect.IsSelected = true;
            }
            else
            {
                // On Document Outline Control initialization, SymbolTree.ItemsSource is null
                if (SymbolTree.ItemsSource is not null)
                    DocumentOutlineHelper.UnselectAll((IEnumerable<DocumentSymbolUIItem>)SymbolTree.ItemsSource);
            }
 
            SymbolTree.ItemsSource = documentSymbolUIItems;
        }
 
        private IWpfTextView? GetLastActiveIWpfTextView()
        {
            _threadingContext.ThrowIfNotOnUIThread();
 
            // If we return null, the calling queue returns and we stop processing.
            if (ErrorHandler.Failed(_codeWindow.GetLastActiveView(out var textView)))
                return null;
 
            return _editorAdaptersFactoryService.GetWpfTextView(textView);
        }
    }
}