diff --git a/src/EditorFeatures/Core/NavigationBar/NavigationBarController.cs b/src/EditorFeatures/Core/NavigationBar/NavigationBarController.cs
index 042f5b12b8f8e..74386145ba77e 100644
--- a/src/EditorFeatures/Core/NavigationBar/NavigationBarController.cs
+++ b/src/EditorFeatures/Core/NavigationBar/NavigationBarController.cs
@@ -42,8 +42,9 @@ internal partial class NavigationBarController : IDisposable
private bool _disconnected = false;
///
- /// The last full information we have presented. If we end up wanting to present the same thing again, we can
- /// just skip doing that as the UI will already know about this.
+ /// The last full information we have presented. If we end up wanting to present the same thing again, we can just
+ /// skip doing that as the UI will already know about this. This is only ever read or written from . So we don't need to worry about any synchronization over it.
///
private (ImmutableArray projectItems, NavigationBarProjectItem? selectedProjectItem, NavigationBarModel? model, NavigationBarSelectedTypeAndMember selectedInfo) _lastPresentedInfo;
@@ -66,10 +67,10 @@ internal partial class NavigationBarController : IDisposable
private readonly AsyncBatchingWorkQueue _computeModelQueue;
///
- /// Queue to batch up work to do to determine the selected item. Used so we can batch up a lot of events and
- /// only compute the selected item once for every batch.
+ /// Queue to batch up work to do to determine the selected item. Used so we can batch up a lot of events and only
+ /// compute the selected item once for every batch. The value passed in is the last recorded caret position.
///
- private readonly AsyncBatchingWorkQueue _selectItemQueue;
+ private readonly AsyncBatchingWorkQueue _selectItemQueue;
///
/// Whether or not the navbar is paused. We pause updates when documents become non-visible. See (
DelayTimeSpan.Short,
SelectItemAsync,
asyncListener,
@@ -126,9 +127,10 @@ public NavigationBarController(
{
threadingContext.ThrowIfNotOnUIThread();
- // any time visibility changes, resume tagging on all taggers. Any non-visible taggers will pause
- // themselves immediately afterwards.
- Resume();
+ if (_visibilityTracker?.IsVisible(_subjectBuffer) is false)
+ Pause();
+ else
+ Resume();
};
// Register to hear about visibility changes so we can pause/resume this tagger.
@@ -138,6 +140,26 @@ public NavigationBarController(
// Kick off initial work to populate the navbars
StartModelUpdateAndSelectedItemUpdateTasks();
+
+ return;
+
+ void Pause()
+ {
+ _paused = true;
+ _eventSource.Pause();
+ }
+
+ void Resume()
+ {
+ // if we're not actually paused, no need to do anything.
+ if (_paused)
+ {
+ // Set us back to running, and kick off work to compute tags now that we're visible again.
+ _paused = false;
+ _eventSource.Resume();
+ StartModelUpdateAndSelectedItemUpdateTasks();
+ }
+ }
}
void IDisposable.Dispose()
@@ -161,26 +183,6 @@ void IDisposable.Dispose()
_cancellationTokenSource.Cancel();
}
- private void Pause()
- {
- _threadingContext.ThrowIfNotOnUIThread();
- _paused = true;
- _eventSource.Pause();
- }
-
- private void Resume()
- {
- _threadingContext.ThrowIfNotOnUIThread();
- // if we're not actually paused, no need to do anything.
- if (_paused)
- {
- // Set us back to running, and kick off work to compute tags now that we're visible again.
- _paused = false;
- _eventSource.Resume();
- StartModelUpdateAndSelectedItemUpdateTasks();
- }
- }
-
public TestAccessor GetTestAccessor() => new TestAccessor(this);
private void OnEventSourceChanged(object? sender, TaggerEventArgs e)
@@ -200,31 +202,46 @@ private void StartModelUpdateAndSelectedItemUpdateTasks()
private void OnCaretMovedOrActiveViewChanged(object? sender, EventArgs e)
{
_threadingContext.ThrowIfNotOnUIThread();
- StartSelectedItemUpdateTask();
+
+ var caretPoint = GetCaretPoint();
+ if (caretPoint == null)
+ return;
+
+ // Cancel any in flight work. We're on the UI thread, so we know this is the latest position of the user, and that
+ // this should supersede any other selection work items.
+ _selectItemQueue.AddWork(caretPoint.Value, cancelExistingWork: true);
}
- private void GetProjectItems(out ImmutableArray projectItems, out NavigationBarProjectItem? selectedProjectItem)
+ private int? GetCaretPoint()
{
- var documents = _subjectBuffer.CurrentSnapshot.GetRelatedDocumentsWithChanges();
- if (!documents.Any())
- {
- projectItems = [];
- selectedProjectItem = null;
- return;
- }
+ var currentView = _presenter.TryGetCurrentView();
+ return currentView?.GetCaretPoint(_subjectBuffer)?.Position;
+ }
- projectItems = [.. documents.Select(d =>
- new NavigationBarProjectItem(
+ private (ImmutableArray projectItems, NavigationBarProjectItem? selectedProjectItem) GetProjectItems()
+ {
+ var textContainer = _subjectBuffer.AsTextContainer();
+
+ var documents = textContainer.GetRelatedDocuments();
+ if (documents.IsEmpty)
+ return ([], null);
+
+ var projectItems = documents
+ .Select(d => new NavigationBarProjectItem(
d.Project.Name,
d.Project.GetGlyph(),
workspace: d.Project.Solution.Workspace,
documentId: d.Id,
- language: d.Project.Language)).OrderBy(projectItem => projectItem.Text)];
+ language: d.Project.Language))
+ .OrderBy(projectItem => projectItem.Text)
+ .ToImmutableArray();
- var document = _subjectBuffer.AsTextContainer().GetOpenDocumentInCurrentContext();
- selectedProjectItem = document != null
+ var document = textContainer.GetOpenDocumentInCurrentContext();
+ var selectedProjectItem = document != null
? projectItems.FirstOrDefault(p => p.Text == document.Project.Name) ?? projectItems.First()
: projectItems.First();
+
+ return (projectItems, selectedProjectItem);
}
private void OnItemSelected(object? sender, NavigationBarItemSelectedEventArgs e)
diff --git a/src/EditorFeatures/Core/NavigationBar/NavigationBarController_ModelComputation.cs b/src/EditorFeatures/Core/NavigationBar/NavigationBarController_ModelComputation.cs
index d1cc86325ea39..b9dd43d278ced 100644
--- a/src/EditorFeatures/Core/NavigationBar/NavigationBarController_ModelComputation.cs
+++ b/src/EditorFeatures/Core/NavigationBar/NavigationBarController_ModelComputation.cs
@@ -13,6 +13,7 @@
using Microsoft.CodeAnalysis.Shared.Extensions;
using Microsoft.CodeAnalysis.Text;
using Microsoft.CodeAnalysis.Workspaces;
+using Microsoft.VisualStudio.Text;
using Microsoft.VisualStudio.Threading;
using Roslyn.Utilities;
@@ -34,16 +35,19 @@ internal partial class NavigationBarController
return null;
var textSnapshot = _subjectBuffer.CurrentSnapshot;
+ var caretPoint = GetCaretPoint();
- // Ensure we switch to the threadpool before calling GetDocumentWithFrozenPartialSemantics. It ensures
- // that any IO that performs is not potentially on the UI thread.
+ // Ensure we switch to the threadpool before calling GetDocumentWithFrozenPartialSemantics. It ensures that any
+ // IO that performs is not potentially on the UI thread.
await TaskScheduler.Default;
var model = await ComputeModelAsync().ConfigureAwait(false);
- // Now, enqueue work to select the right item in this new model.
- if (model != null)
- StartSelectedItemUpdateTask();
+ // Now, enqueue work to select the right item in this new model. Note: we don't want to cancel existing items in
+ // the queue as it may be the case that the user moved between us capturing the initial caret point and now, and
+ // we'd want the selection work we enqueued for that to take precedence over us.
+ if (model != null && caretPoint != null)
+ _selectItemQueue.AddWork(caretPoint.Value, cancelExistingWork: false);
return model;
@@ -94,55 +98,27 @@ await _visibilityTracker.DelayWhileNonVisibleAsync(
}
}
- ///
- /// Starts a new task to compute what item should be selected.
- ///
- private void StartSelectedItemUpdateTask()
- {
- // Cancel any in flight work. This way we don't update until a short lull after the last user event we received.
- _selectItemQueue.AddWork(cancelExistingWork: true);
- }
-
- private async ValueTask SelectItemAsync(CancellationToken cancellationToken)
+ private async ValueTask SelectItemAsync(ImmutableSegmentedList positions, CancellationToken cancellationToken)
{
- // Switch to the UI so we can determine where the user is and determine the state the last time we updated
- // the UI.
- await _threadingContext.JoinableTaskFactory.SwitchToMainThreadAsync(cancellationToken).NoThrowAwaitable();
-
- // Cancellation exceptions are ignored in AsyncBatchingWorkQueue, so return without throwing if cancellation
- // occurred while switching to the main thread.
- if (cancellationToken.IsCancellationRequested)
- return;
+ var lastCaretPosition = positions.Last();
- await SelectItemWorkerAsync(cancellationToken).ConfigureAwait(true);
+ // Can grab this directly here as only this queue ever reads or writes to it.
+ var lastPresentedInfo = _lastPresentedInfo;
- // Once we've computed and selected the latest navbar items, pause ourselves if we're no longer visible.
- // That way we don't consume any machine resources that the user won't even notice.
- if (_visibilityTracker?.IsVisible(_subjectBuffer) is false)
- Pause();
- }
+ // Make a task that waits indefinitely, or until the cancellation token is signaled.
+ var cancellationTriggeredTask = Task.Delay(-1, cancellationToken);
- private async ValueTask SelectItemWorkerAsync(CancellationToken cancellationToken)
- {
- _threadingContext.ThrowIfNotOnUIThread();
+ // Get the task representing the computation of the model.
+ var modelTask = _computeModelQueue.WaitUntilCurrentBatchCompletesAsync();
- var currentView = _presenter.TryGetCurrentView();
- var caretPosition = currentView?.GetCaretPoint(_subjectBuffer);
- if (!caretPosition.HasValue)
+ var completedTask = await Task.WhenAny(cancellationTriggeredTask, modelTask).ConfigureAwait(false);
+ if (completedTask == cancellationTriggeredTask)
return;
- var position = caretPosition.Value.Position;
- var lastPresentedInfo = _lastPresentedInfo;
-
- // Jump back to the BG to do any expensive work walking the entire model
- await TaskScheduler.Default;
-
- // Ensure the latest model is computed.
- var model = await _computeModelQueue.WaitUntilCurrentBatchCompletesAsync().ConfigureAwait(true);
-
- var currentSelectedItem = ComputeSelectedTypeAndMember(model, position, cancellationToken);
+ var model = await modelTask.ConfigureAwait(false);
+ var currentSelectedItem = ComputeSelectedTypeAndMember(model, lastCaretPosition, cancellationToken);
- GetProjectItems(out var projectItems, out var selectedProjectItem);
+ var (projectItems, selectedProjectItem) = GetProjectItems();
if (Equals(model, lastPresentedInfo.model) &&
Equals(currentSelectedItem, lastPresentedInfo.selectedInfo) &&
Equals(selectedProjectItem, lastPresentedInfo.selectedProjectItem) &&
diff --git a/src/EditorFeatures/Core/Tagging/ITaggerEventSource.cs b/src/EditorFeatures/Core/Tagging/ITaggerEventSource.cs
index bba97133761dc..43cc5947723c3 100644
--- a/src/EditorFeatures/Core/Tagging/ITaggerEventSource.cs
+++ b/src/EditorFeatures/Core/Tagging/ITaggerEventSource.cs
@@ -30,14 +30,14 @@ internal interface ITaggerEventSource
void Disconnect();
///
- /// Pauses this event source and prevents it from firing the event. Can be called many
- /// times (but subsequence calls have no impact if already paused). Must be called on the UI thread.
+ /// Pauses this event source and prevents it from firing the event. Can be called many times
+ /// (but subsequent calls have no impact if already paused). Must be called on the UI thread.
///
void Pause();
///
/// Resumes this event source and allows firing the event. Can be called many times (but
- /// subsequence calls have no impact if already resumed). Must be called on the UI thread.
+ /// subsequent calls have no impact if already resumed). Must be called on the UI thread.
///
void Resume();