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