Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Document outline updates #69921

Merged
merged 3 commits into from
Sep 14, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
xmlns:vsui="clr-namespace:Microsoft.VisualStudio.PlatformUI;assembly=Microsoft.VisualStudio.Shell.15.0"
xmlns:platformimaging="clr-namespace:Microsoft.VisualStudio.PlatformUI;assembly=Microsoft.VisualStudio.Imaging"
xmlns:ComponentModel="clr-namespace:System.ComponentModel;assembly=WindowsBase"
d:DataContext="{d:DesignInstance Type=self:DocumentOutlineViewModel}"
mc:Ignorable="d"
d:DesignHeight="450" d:DesignWidth="800"
x:Name="DocumentOutline"
Expand Down Expand Up @@ -94,8 +95,9 @@
AutomationProperties.Name="{x:Static self:DocumentOutlineStrings.Document_Outline}"
VirtualizingStackPanel.IsVirtualizing="True"
VirtualizingStackPanel.VirtualizationMode="Recycling"
TreeViewItem.SourceUpdated="SymbolTreeItem_SourceUpdated"
SourceUpdated="SymbolTree_SourceUpdated"
TreeViewItem.Selected="SymbolTreeItem_Selected"
Visibility="{Binding Visibility, Mode=OneWayToSource}"
ItemsSource="{Binding Source={StaticResource DocumentSymbolItems}}"> <!-- Binding to our CollectionViewSource -->
<TreeView.Resources>
<HierarchicalDataTemplate DataType="{x:Type self:DocumentSymbolDataViewModel}">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
using Microsoft.VisualStudio.Shell;
using Microsoft.VisualStudio.Shell.Interop;
using Microsoft.VisualStudio.Text;
using Microsoft.VisualStudio.Text.Outlining;
using InternalUtilities = Microsoft.Internal.VisualStudio.PlatformUI.Utilities;
using IOleCommandTarget = Microsoft.VisualStudio.OLE.Interop.IOleCommandTarget;
using OLECMD = Microsoft.VisualStudio.OLE.Interop.OLECMD;
Expand All @@ -34,6 +35,7 @@ internal sealed partial class DocumentOutlineView : UserControl, IOleCommandTarg
{
private readonly IThreadingContext _threadingContext;
private readonly IGlobalOptionService _globalOptionService;
private readonly IOutliningManagerService _outliningManagerService;
private readonly VsCodeWindowViewTracker _viewTracker;
private readonly DocumentOutlineViewModel _viewModel;
private readonly IVsToolbarTrayHost _toolbarTrayHost;
Expand All @@ -44,11 +46,13 @@ public DocumentOutlineView(
IVsWindowSearchHostFactory windowSearchHostFactory,
IThreadingContext threadingContext,
IGlobalOptionService globalOptionService,
IOutliningManagerService outliningManagerService,
VsCodeWindowViewTracker viewTracker,
DocumentOutlineViewModel viewModel)
{
_threadingContext = threadingContext;
_globalOptionService = globalOptionService;
_outliningManagerService = outliningManagerService;
_viewTracker = viewTracker;
_viewModel = viewModel;

Expand Down Expand Up @@ -268,11 +272,18 @@ public static void UpdateSortDescription(SortDescriptionCollection sortDescripti
/// <summary>
/// When a symbol node in the window is selected via the keyboard, move the caret to its position in the latest active text view.
/// </summary>
private void SymbolTreeItem_SourceUpdated(object sender, DataTransferEventArgs e)
private void SymbolTree_SourceUpdated(object sender, DataTransferEventArgs e)
{
_threadingContext.ThrowIfNotOnUIThread();

if (!_viewModel.IsNavigating && e.OriginalSource is TreeViewItem { DataContext: DocumentSymbolDataViewModel symbolModel })
// 🐉 In practice, this event was firing in cases where the user did not manually select an item in the
// tree view, resulting in sporadic/unexpected navigation while editing. To filter out these cases, we
// include a final check that keyboard focus in currently within the selected tree view item, which implies
// that the keyboard focus is _not_ within the editor (and thus, we will not be interfering with a user who
// is editing source code). See https://github.com/dotnet/roslyn/issues/69292.
if (!_viewModel.IsNavigating
&& e.OriginalSource is TreeViewItem { DataContext: DocumentSymbolDataViewModel symbolModel } item
&& FocusHelper.IsKeyboardFocusWithin(item))
{
// This is a user-initiated navigation, and we need to prevent reentrancy. Specifically: when a user
// does click on an item, we do navigate, and that does move the caret. This part happens synchronously.
Expand All @@ -282,7 +293,8 @@ private void SymbolTreeItem_SourceUpdated(object sender, DataTransferEventArgs e
{
var textView = _viewTracker.GetActiveView();
textView.TryMoveCaretToAndEnsureVisible(
symbolModel.Data.SelectionRangeSpan.TranslateTo(textView.TextSnapshot, SpanTrackingMode.EdgeInclusive).Start);
symbolModel.Data.SelectionRangeSpan.TranslateTo(textView.TextSnapshot, SpanTrackingMode.EdgeInclusive).Start,
_outliningManagerService);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

how does this work, does this helper just know to expand in that case?

note: if so, it seems like a super weird helper since we'd always want htis behavior, in which case we should always require the outlining manager. but this is ok for now.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was exactly my conclusion

}
finally
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
using System.Runtime.CompilerServices;
using System.Threading;
using System.Threading.Tasks;
using System.Windows;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Collections;
using Microsoft.CodeAnalysis.Editor.Shared.Extensions;
Expand Down Expand Up @@ -63,6 +64,7 @@ internal sealed partial class DocumentOutlineViewModel : INotifyPropertyChanged,

// Mutable state. Should only update on UI thread.

private Visibility _visibility_doNotAccessDirectly = Visibility.Visible;
private SortOption _sortOption_doNotAccessDirectly = SortOption.Location;
private string _searchText_doNotAccessDirectly = "";
private ImmutableArray<DocumentSymbolDataViewModel> _documentSymbolViewModelItems_doNotAccessDirectly = ImmutableArray<DocumentSymbolDataViewModel>.Empty;
Expand Down Expand Up @@ -111,7 +113,7 @@ public DocumentOutlineViewModel(
_taggerEventSource.Connect();

// queue initial model update
_workQueue.AddWork(default(VoidResult));
_workQueue.AddWork();
}

public void Dispose()
Expand All @@ -129,7 +131,7 @@ private static DocumentOutlineViewState CreateEmptyViewState(ITextSnapshot curre
IntervalTree<DocumentSymbolDataViewModel>.Empty);

private void OnEventSourceChanged(object sender, TaggerEventArgs e)
=> _workQueue.AddWork(default(VoidResult), cancelExistingWork: true);
=> _workQueue.AddWork(cancelExistingWork: true);

/// <summary>
/// Keeps track if we're currently in the middle of navigating or not. For example, when the user clicks on an
Expand Down Expand Up @@ -173,6 +175,23 @@ private DocumentOutlineViewState LastPresentedViewState
}
}

/// <remarks>This property is bound to the UI. However, it is only read/written by the UI. We only act as
/// storage for the value. When this value is true, UI updates are deferred.</remarks>
public Visibility Visibility
{
get
{
_threadingContext.ThrowIfNotOnUIThread();
return _visibility_doNotAccessDirectly;
}

set
{
_threadingContext.ThrowIfNotOnUIThread();
_visibility_doNotAccessDirectly = value;
}
}

/// <remarks>This property is bound to the UI. However, it is only read/written by the UI. We only act as
/// storage for the value. When the value changes, the sorting is actually handled by
/// DocumentSymbolDataViewModelSorter.</remarks>
Expand Down Expand Up @@ -208,7 +227,7 @@ public string SearchText
_threadingContext.ThrowIfNotOnUIThread();
_searchText_doNotAccessDirectly = value;

_workQueue.AddWork(default(VoidResult), cancelExistingWork: true);
_workQueue.AddWork(cancelExistingWork: true);
}
}

Expand Down Expand Up @@ -250,6 +269,17 @@ static void ExpandOrCollapse(ImmutableArray<DocumentSymbolDataViewModel> models,

private async ValueTask ComputeViewStateAsync(CancellationToken cancellationToken)
{
await _threadingContext.JoinableTaskFactory.SwitchToMainThreadAsync(cancellationToken);
if (_isDisposed)
return;

if (Visibility != Visibility.Visible)
{
// Retry the update after a delay
_workQueue.AddWork(cancelExistingWork: true);
return;
}

// Do any expensive semantic/computation work in the background.
await TaskScheduler.Default;
cancellationToken.ThrowIfCancellationRequested();
Expand All @@ -262,6 +292,13 @@ private async ValueTask ComputeViewStateAsync(CancellationToken cancellationToke
if (_isDisposed)
return;

if (Visibility != Visibility.Visible)
{
// Retry the update after a delay
_workQueue.AddWork(cancelExistingWork: true);
return;
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

instead of all 3, it likely suffices to just have teh first case. worst that happens is that the user switches away, and we finish up the work here. but any future work requsts will bail out at the top.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure, that works for me

Copy link
Member Author

@sharwell sharwell Sep 12, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

On second thought, given the difficulty of proving the change works, I'm concerned about edge cases like switching back and forth between a Windows Forms designer and a C# source document (resulting in a visibility change for each switch). I'm more comfortable with the original change in terms of confidence that work is avoided when the UI is not visible. If you'd like, I could eliminate the middle of the three checks, though the explanation of why it's omitted might end up longer than the check itself.


var searchText = this.SearchText;
var sortOption = this.SortOption;
var lastPresentedViewState = this.LastPresentedViewState;
Expand Down Expand Up @@ -306,6 +343,13 @@ private async ValueTask ComputeViewStateAsync(CancellationToken cancellationToke
if (_isDisposed)
return;

if (Visibility != Visibility.Visible)
{
// Retry the update after a delay
_workQueue.AddWork(cancelExistingWork: true);
return;
}

this.LastPresentedViewState = newViewState;
this.DocumentSymbolViewModelItems = newViewModelItems;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
using Microsoft.VisualStudio.OLE.Interop;
using Microsoft.VisualStudio.Shell.Interop;
using Microsoft.VisualStudio.Text;
using Microsoft.VisualStudio.Text.Outlining;
using Microsoft.VisualStudio.TextManager.Interop;
using Roslyn.Utilities;

Expand Down Expand Up @@ -264,14 +265,15 @@ private void GetOutline(out IntPtr phwnd)
var asyncListenerProvider = _languageService.Package.ComponentModel.GetService<IAsynchronousOperationListenerProvider>();
var asyncListener = asyncListenerProvider.GetListener(FeatureAttribute.DocumentOutline);
var editorAdaptersFactoryService = _languageService.Package.ComponentModel.GetService<IVsEditorAdaptersFactoryService>();
var outliningManagerService = _languageService.Package.ComponentModel.GetService<IOutliningManagerService>();

// Assert that the previous Document Outline Control and host have been freed.
Contract.ThrowIfFalse(_documentOutlineView is null);
Contract.ThrowIfFalse(_documentOutlineViewHost is null);

var viewTracker = new VsCodeWindowViewTracker(_codeWindow, threadingContext, editorAdaptersFactoryService);
_documentOutlineView = new DocumentOutlineView(
uiShell, windowSearchHostFactory, threadingContext, _globalOptions, viewTracker,
uiShell, windowSearchHostFactory, threadingContext, _globalOptions, outliningManagerService, viewTracker,
new DocumentOutlineViewModel(threadingContext, viewTracker, languageServiceBroker, asyncListener));

_documentOutlineViewHost = new ElementHost
Expand Down
Loading