|
// 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.Collections.Immutable;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
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.ErrorReporting;
using Microsoft.CodeAnalysis.Shared.Extensions;
using Microsoft.CodeAnalysis.Shared.TestHooks;
using Microsoft.CodeAnalysis.Text;
using Microsoft.CodeAnalysis.Workspaces;
using Microsoft.VisualStudio.Text;
using Roslyn.Utilities;
using IUIThreadOperationExecutor = Microsoft.VisualStudio.Utilities.IUIThreadOperationExecutor;
namespace Microsoft.CodeAnalysis.Editor.Implementation.NavigationBar
{
/// <summary>
/// The controller for navigation bars.
/// </summary>
/// <remarks>
/// The threading model for this class is simple: all non-static members are affinitized to the
/// UI thread.
/// </remarks>
internal partial class NavigationBarController : IDisposable
{
private readonly IThreadingContext _threadingContext;
private readonly INavigationBarPresenter _presenter;
private readonly ITextBuffer _subjectBuffer;
private readonly ITextBufferVisibilityTracker? _visibilityTracker;
private readonly IUIThreadOperationExecutor _uiThreadOperationExecutor;
private readonly IAsynchronousOperationListener _asyncListener;
private bool _disconnected = false;
/// <summary>
/// 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.
/// </summary>
private (ImmutableArray<NavigationBarProjectItem> projectItems, NavigationBarProjectItem? selectedProjectItem, NavigationBarModel? model, NavigationBarSelectedTypeAndMember selectedInfo) _lastPresentedInfo;
/// <summary>
/// Source of events that should cause us to update the nav bar model with new information.
/// </summary>
private readonly ITaggerEventSource _eventSource;
/// <summary>
/// Callback to us when the visibility of our <see cref="_subjectBuffer"/> changes.
/// </summary>
private readonly Action _onVisibilityChanged;
private readonly CancellationTokenSource _cancellationTokenSource = new();
/// <summary>
/// Queue to batch up work to do to compute the current model. Used so we can batch up a lot of events and only
/// compute the model once for every batch. The <c>bool</c> type parameter isn't used, but is provided as this
/// type is generic.
/// </summary>
private readonly AsyncBatchingWorkQueue<bool, NavigationBarModel?> _computeModelQueue;
/// <summary>
/// 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.
/// </summary>
private readonly AsyncBatchingWorkQueue _selectItemQueue;
/// <summary>
/// Whether or not the navbar is paused. We pause updates when documents become non-visible. See <see
/// cref="_visibilityTracker"/>.
/// </summary>
private bool _paused = false;
public NavigationBarController(
IThreadingContext threadingContext,
INavigationBarPresenter presenter,
ITextBuffer subjectBuffer,
ITextBufferVisibilityTracker? visibilityTracker,
IUIThreadOperationExecutor uiThreadOperationExecutor,
IAsynchronousOperationListener asyncListener)
{
_threadingContext = threadingContext;
_presenter = presenter;
_subjectBuffer = subjectBuffer;
_visibilityTracker = visibilityTracker;
_uiThreadOperationExecutor = uiThreadOperationExecutor;
_asyncListener = asyncListener;
_computeModelQueue = new AsyncBatchingWorkQueue<bool, NavigationBarModel?>(
DelayTimeSpan.Short,
ComputeModelAndSelectItemAsync,
EqualityComparer<bool>.Default,
asyncListener,
_cancellationTokenSource.Token);
_selectItemQueue = new AsyncBatchingWorkQueue(
DelayTimeSpan.NearImmediate,
SelectItemAsync,
asyncListener,
_cancellationTokenSource.Token);
presenter.CaretMovedOrActiveViewChanged += OnCaretMovedOrActiveViewChanged;
presenter.ItemSelected += OnItemSelected;
// Use 'compilation available' as that may produce different results from the initial 'frozen partial'
// snapshot we use.
_eventSource = new CompilationAvailableTaggerEventSource(
subjectBuffer,
asyncListener,
// Any time an edit happens, recompute as the nav bar items may have changed.
TaggerEventSources.OnTextChanged(subjectBuffer),
// Switching what is the active context may change the nav bar contents.
TaggerEventSources.OnDocumentActiveContextChanged(subjectBuffer),
// Many workspace changes may need us to change the items (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 nav bar items.
TaggerEventSources.OnWorkspaceRegistrationChanged(subjectBuffer));
_eventSource.Changed += OnEventSourceChanged;
_onVisibilityChanged = () =>
{
threadingContext.ThrowIfNotOnUIThread();
// any time visibility changes, resume tagging on all taggers. Any non-visible taggers will pause
// themselves immediately afterwards.
Resume();
};
// Register to hear about visibility changes so we can pause/resume this tagger.
_visibilityTracker?.RegisterForVisibilityChanges(subjectBuffer, _onVisibilityChanged);
_eventSource.Connect();
// Kick off initial work to populate the navbars
StartModelUpdateAndSelectedItemUpdateTasks();
}
void IDisposable.Dispose()
{
_threadingContext.ThrowIfNotOnUIThread();
_visibilityTracker?.UnregisterForVisibilityChanges(_subjectBuffer, _onVisibilityChanged);
_presenter.CaretMovedOrActiveViewChanged -= OnCaretMovedOrActiveViewChanged;
_presenter.ItemSelected -= OnItemSelected;
_presenter.Disconnect();
_eventSource.Changed -= OnEventSourceChanged;
_eventSource.Disconnect();
_disconnected = true;
// Cancel off any remaining background work
_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)
{
StartModelUpdateAndSelectedItemUpdateTasks();
}
private void StartModelUpdateAndSelectedItemUpdateTasks()
{
// If we disconnected already, just disregard
if (_disconnected)
return;
// 'true' value is unused. this just signals to the queue that we have work to do.
_computeModelQueue.AddWork(true);
}
private void OnCaretMovedOrActiveViewChanged(object? sender, EventArgs e)
{
_threadingContext.ThrowIfNotOnUIThread();
StartSelectedItemUpdateTask();
}
private void GetProjectItems(out ImmutableArray<NavigationBarProjectItem> projectItems, out NavigationBarProjectItem? selectedProjectItem)
{
var documents = _subjectBuffer.CurrentSnapshot.GetRelatedDocumentsWithChanges();
if (!documents.Any())
{
projectItems = ImmutableArray<NavigationBarProjectItem>.Empty;
selectedProjectItem = null;
return;
}
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).ToImmutableArray();
var document = _subjectBuffer.AsTextContainer().GetOpenDocumentInCurrentContext();
selectedProjectItem = document != null
? projectItems.FirstOrDefault(p => p.Text == document.Project.Name) ?? projectItems.First()
: projectItems.First();
}
private void OnItemSelected(object? sender, NavigationBarItemSelectedEventArgs e)
{
_threadingContext.ThrowIfNotOnUIThread();
var token = _asyncListener.BeginAsyncOperation(nameof(OnItemSelected));
var task = OnItemSelectedAsync(e.Item);
_ = task.CompletesAsyncOperation(token);
}
private async Task OnItemSelectedAsync(NavigationBarItem item)
{
_threadingContext.ThrowIfNotOnUIThread();
using var waitContext = _uiThreadOperationExecutor.BeginExecute(
EditorFeaturesResources.Navigation_Bars,
EditorFeaturesResources.Refreshing_navigation_bars,
allowCancellation: true,
showProgress: false);
try
{
await ProcessItemSelectionAsync(item, waitContext.UserCancellationToken).ConfigureAwait(false);
}
catch (OperationCanceledException)
{
}
catch (Exception e) when (FatalError.ReportAndCatch(e, ErrorSeverity.Critical))
{
}
}
private async Task ProcessItemSelectionAsync(NavigationBarItem item, CancellationToken cancellationToken)
{
_threadingContext.ThrowIfNotOnUIThread();
if (item is NavigationBarProjectItem projectItem)
{
projectItem.SwitchToContext();
}
else
{
// When navigating, just use the partial semantics workspace. Navigation doesn't need the fully bound
// compilations to be created, and it can save us a lot of costly time building skeleton assemblies.
var textSnapshot = _subjectBuffer.CurrentSnapshot;
var document = textSnapshot.AsText().GetDocumentWithFrozenPartialSemantics(cancellationToken);
if (document != null)
{
var navBarService = document.GetRequiredLanguageService<INavigationBarItemService>();
var view = _presenter.TryGetCurrentView();
// ConfigureAwait(true) as we have to come back to UI thread in order to kick of the refresh task
// below. Note that we only want to refresh if selecting the item had an effect (either navigating
// or generating). If nothing happened to don't want to refresh. This is important as some items
// exist in the type list that are only there to show a set a particular set of items in the member
// list. So selecting such an item should only update the member list, and we do not want a refresh
// to wipe that out.
if (!await navBarService.TryNavigateToItemAsync(
document, item, view, textSnapshot.Version, cancellationToken).ConfigureAwait(true))
{
return;
}
}
}
// Now that the edit has been done, refresh to make sure everything is up-to-date.
StartModelUpdateAndSelectedItemUpdateTasks();
}
public readonly struct TestAccessor
{
private readonly NavigationBarController _navigationBarController;
public TestAccessor(NavigationBarController navigationBarController)
{
_navigationBarController = navigationBarController;
}
public Task<NavigationBarModel?> GetModelAsync()
=> _navigationBarController._computeModelQueue.WaitUntilCurrentBatchCompletesAsync();
}
}
}
|