Skip to content

Commit

Permalink
Merge pull request #59759 from CyrusNajmabadi/backgroundWorkIndicator2
Browse files Browse the repository at this point in the history
Initial implementation of a new 'lightweight' background-work-indicator that we can use in-situ in the editor.
  • Loading branch information
CyrusNajmabadi authored Mar 17, 2022
2 parents 99ba809 + 32bb33f commit fb71ede
Show file tree
Hide file tree
Showing 52 changed files with 1,000 additions and 187 deletions.
24 changes: 12 additions & 12 deletions eng/Versions.props
Original file line number Diff line number Diff line change
Expand Up @@ -26,11 +26,11 @@
<!-- CodeStyleAnalyzerVersion should we updated together with version of dotnet-format in dotnet-tools.json -->
<CodeStyleAnalyzerVersion>4.1.0</CodeStyleAnalyzerVersion>
<VisualStudioEditorPackagesVersion>16.10.230</VisualStudioEditorPackagesVersion>
<VisualStudioEditorNewPackagesVersion>17.0.487</VisualStudioEditorNewPackagesVersion>
<VisualStudioEditorNewPackagesVersion>17.2.140-preview-ga1e1777dca</VisualStudioEditorNewPackagesVersion>
<ILAsmPackageVersion>5.0.0-alpha1.19409.1</ILAsmPackageVersion>
<ILDAsmPackageVersion>5.0.0-preview.1.20112.8</ILDAsmPackageVersion>
<MicrosoftVisualStudioLanguageServerProtocolPackagesVersion>17.2.3</MicrosoftVisualStudioLanguageServerProtocolPackagesVersion>
<MicrosoftVisualStudioShellPackagesVersion>17.0.31723.112</MicrosoftVisualStudioShellPackagesVersion>
<MicrosoftVisualStudioShellPackagesVersion>17.2.0-preview-1-32131-009</MicrosoftVisualStudioShellPackagesVersion>
<RefOnlyMicrosoftBuildPackagesVersion>16.5.0</RefOnlyMicrosoftBuildPackagesVersion>
<!-- The version of Roslyn we build Source Generators against that are built in this
repository. This must be lower than MicrosoftNetCompilersToolsetVersion,
Expand Down Expand Up @@ -115,11 +115,11 @@
<MicrosoftNetSdkVersion>2.0.0-alpha-20170405-2</MicrosoftNetSdkVersion>
<MicrosoftNuGetBuildTasksVersion>0.1.0</MicrosoftNuGetBuildTasksVersion>
<MicrosoftPortableTargetsVersion>0.1.2-dev</MicrosoftPortableTargetsVersion>
<MicrosoftServiceHubClientVersion>3.1.4</MicrosoftServiceHubClientVersion>
<MicrosoftServiceHubFrameworkVersion>3.1.4</MicrosoftServiceHubFrameworkVersion>
<MicrosoftServiceHubClientVersion>3.1.3058</MicrosoftServiceHubClientVersion>
<MicrosoftServiceHubFrameworkVersion>3.1.3058</MicrosoftServiceHubFrameworkVersion>
<MicrosoftSourceLinkToolsVersion>1.1.1-beta-21566-01</MicrosoftSourceLinkToolsVersion>
<MicrosoftVisualBasicVersion>10.1.0</MicrosoftVisualBasicVersion>
<MicrosoftVisualStudioCacheVersion>17.0.42</MicrosoftVisualStudioCacheVersion>
<MicrosoftVisualStudioCacheVersion>17.2.41-alpha</MicrosoftVisualStudioCacheVersion>
<MicrosoftVisualStudioCallHierarchyPackageDefinitionsVersion>15.8.27812-alpha</MicrosoftVisualStudioCallHierarchyPackageDefinitionsVersion>
<MicrosoftVisualStudioCodeAnalysisSdkUIVersion>15.8.27812-alpha</MicrosoftVisualStudioCodeAnalysisSdkUIVersion>
<MicrosoftVisualStudioComponentModelHostVersion>$(VisualStudioEditorNewPackagesVersion)</MicrosoftVisualStudioComponentModelHostVersion>
Expand Down Expand Up @@ -174,10 +174,10 @@
<MicrosoftVisualStudioTextUIVersion>$(VisualStudioEditorNewPackagesVersion)</MicrosoftVisualStudioTextUIVersion>
<MicrosoftVisualStudioTextUIWpfVersion>$(VisualStudioEditorNewPackagesVersion)</MicrosoftVisualStudioTextUIWpfVersion>
<MicrosoftVisualStudioTextUICocoaVersion>$(VisualStudioEditorPackagesVersion)</MicrosoftVisualStudioTextUICocoaVersion>
<MicrosoftVisualStudioThreadingAnalyzersVersion>17.0.64</MicrosoftVisualStudioThreadingAnalyzersVersion>
<MicrosoftVisualStudioThreadingVersion>17.0.64</MicrosoftVisualStudioThreadingVersion>
<MicrosoftVisualStudioThreadingAnalyzersVersion>17.2.10-alpha</MicrosoftVisualStudioThreadingAnalyzersVersion>
<MicrosoftVisualStudioThreadingVersion>17.2.10-alpha</MicrosoftVisualStudioThreadingVersion>
<MicrosoftVisualStudioUtilitiesVersion>$(MicrosoftVisualStudioShellPackagesVersion)</MicrosoftVisualStudioUtilitiesVersion>
<MicrosoftVisualStudioValidationVersion>17.0.28</MicrosoftVisualStudioValidationVersion>
<MicrosoftVisualStudioValidationVersion>17.0.46</MicrosoftVisualStudioValidationVersion>
<MicrosoftVisualStudioInteractiveWindowVersion>4.0.0-beta.21267.2</MicrosoftVisualStudioInteractiveWindowVersion>
<MicrosoftVisualStudioVsInteractiveWindowVersion>4.0.0-beta.21267.2</MicrosoftVisualStudioVsInteractiveWindowVersion>
<MicrosoftVisualStudioWinFormsInterfacesVersion>17.0.0-previews-4-31709-430</MicrosoftVisualStudioWinFormsInterfacesVersion>
Expand Down Expand Up @@ -212,11 +212,11 @@
<RoslynMicrosoftVisualStudioExtensionManagerVersion>0.0.4</RoslynMicrosoftVisualStudioExtensionManagerVersion>
<SourceBrowserVersion>1.0.21</SourceBrowserVersion>
<SystemBuffersVersion>4.5.1</SystemBuffersVersion>
<SystemCompositionVersion>1.0.31</SystemCompositionVersion>
<SystemCompositionVersion>6.0.0</SystemCompositionVersion>
<SystemCodeDomVersion>4.7.0</SystemCodeDomVersion>
<SystemCommandLineVersion>2.0.0-beta1.20574.7</SystemCommandLineVersion>
<SystemCommandLineExperimentalVersion>0.3.0-alpha.19577.1</SystemCommandLineExperimentalVersion>
<SystemComponentModelCompositionVersion>4.5.0</SystemComponentModelCompositionVersion>
<SystemComponentModelCompositionVersion>6.0.0</SystemComponentModelCompositionVersion>
<SystemDrawingCommonVersion>6.0.0</SystemDrawingCommonVersion>
<SystemIOFileSystemVersion>4.3.0</SystemIOFileSystemVersion>
<SystemIOFileSystemPrimitivesVersion>4.3.0</SystemIOFileSystemPrimitivesVersion>
Expand All @@ -236,7 +236,7 @@
<!-- We need System.ValueTuple assembly version at least 4.0.3.0 on net47 to make F5 work against Dev15 - see https://github.com/dotnet/roslyn/issues/29705 -->
<SystemValueTupleVersion>4.5.0</SystemValueTupleVersion>
<SystemThreadingTasksExtensionsVersion>4.5.4</SystemThreadingTasksExtensionsVersion>
<SQLitePCLRawbundle_greenVersion>2.0.4</SQLitePCLRawbundle_greenVersion>
<SQLitePCLRawbundle_greenVersion>2.0.7</SQLitePCLRawbundle_greenVersion>
<UIAComWrapperVersion>1.1.0.14</UIAComWrapperVersion>
<MicroBuildPluginsSwixBuildVersion>1.1.33</MicroBuildPluginsSwixBuildVersion>
<MicrosoftVSSDKBuildToolsVersion>17.0.1056-Dev17PIAs-g9dffd635</MicrosoftVSSDKBuildToolsVersion>
Expand Down Expand Up @@ -266,7 +266,7 @@
create a test insertion in Visual Studio to validate.
-->
<NewtonsoftJsonVersion>13.0.1</NewtonsoftJsonVersion>
<StreamJsonRpcVersion>2.11.14-alpha</StreamJsonRpcVersion>
<StreamJsonRpcVersion>2.11.16-alpha</StreamJsonRpcVersion>
<!--
When updating the S.C.I or S.R.M version please let the MSBuild team know in advance so they
can update to the same version. Version changes require a VS test insertion for validation.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,300 @@
// 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.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.Editor.Shared.Extensions;
using Microsoft.CodeAnalysis.Editor.Shared.Utilities;
using Microsoft.VisualStudio.Shell.Interop;
using Microsoft.VisualStudio.Text;
using Microsoft.VisualStudio.Text.Adornments;
using Microsoft.VisualStudio.Text.Editor;
using Microsoft.VisualStudio.Utilities;
using Roslyn.Utilities;

namespace Microsoft.CodeAnalysis.Editor.BackgroundWorkIndicator;

internal partial class WpfBackgroundWorkIndicatorFactory
{
/// <summary>
/// Implementation of an <see cref="IUIThreadOperationContext"/> for the background work indicator.
/// </summary>
private sealed class BackgroundWorkIndicatorContext : IBackgroundWorkIndicatorContext
{
/// <summary>
/// What sort of UI update request we've enqueued to <see cref="_uiUpdateQueue"/>. This effectively is just a
/// boolean, but with clearer names to make it obvious what is going on.
/// </summary>
private enum UIUpdateRequest
{
UpdateTooltip,
DismissTooltip,
}

/// <summary>
/// Cancellation token exposed to clients through <see cref="UserCancellationToken"/>.
/// </summary>
private readonly CancellationTokenSource _cancellationTokenSource = new();

/// <summary>
/// Lock controlling mutation of all data (except <see cref="_dismissed"/>) in this indicator, or in any
/// sub-scopes. Any read/write of mutable data must be protected by this.
/// </summary>
public readonly object Gate = new();

private readonly WpfBackgroundWorkIndicatorFactory _factory;
private readonly ITextView _textView;
private readonly ITextBuffer _subjectBuffer;
private readonly IToolTipPresenter _toolTipPresenter;
private readonly ITrackingSpan _trackingSpan;
private readonly string _firstDescription;

/// <summary>
/// Work queue used to batch up UI update and Dispose requests. A value of <see langword="true"/> means
/// just update the tool-tip. A value of <see langword="false"/> means we want to dismiss the tool-tip.
/// </summary>
private readonly AsyncBatchingWorkQueue<UIUpdateRequest> _uiUpdateQueue;

/// <summary>
/// Set of scopes we have. We always start with one (the one created by the initial call to create the work
/// indicator). However, the client of the background indicator can add more.
/// </summary>
private ImmutableArray<BackgroundWorkIndicatorScope> _scopes = ImmutableArray<BackgroundWorkIndicatorScope>.Empty;

/// <summary>
/// If we've been dismissed or not. Once dismissed, we will close the tool-tip showing information. This
/// field must only be accessed on the UI thread.
/// </summary>
private bool _dismissed = false;

private IThreadingContext ThreadingContext => _factory._threadingContext;

public PropertyCollection Properties { get; } = new();
public CancellationToken UserCancellationToken => _cancellationTokenSource.Token;
public IEnumerable<IUIThreadOperationScope> Scopes => _scopes;

private bool _cancelOnEdit_DoNotAccessDirectly;
private bool _cancelOnFocusLost_DoNotAccessDirectly;

public BackgroundWorkIndicatorContext(
WpfBackgroundWorkIndicatorFactory factory,
ITextView textView,
SnapshotSpan applicableToSpan,
string firstDescription,
bool cancelOnEdit,
bool cancelOnFocusLost)
{
_factory = factory;
_textView = textView;
_subjectBuffer = applicableToSpan.Snapshot.TextBuffer;

_cancelOnEdit_DoNotAccessDirectly = cancelOnEdit;
_cancelOnFocusLost_DoNotAccessDirectly = cancelOnFocusLost;

// Create a tool-tip at the requested position. Turn off all default behavior for it. We'll be
// controlling everything ourselves.
_toolTipPresenter = factory._toolTipPresenterFactory.Create(textView, new ToolTipParameters(
trackMouse: false,
ignoreBufferChange: true,
keepOpenFunc: null,
ignoreCaretPositionChange: true,
dismissWhenOffscreen: false));

_trackingSpan = applicableToSpan.CreateTrackingSpan(SpanTrackingMode.EdgeInclusive);

_firstDescription = firstDescription;

_uiUpdateQueue = new AsyncBatchingWorkQueue<UIUpdateRequest>(
DelayTimeSpan.Short,
UpdateUIAsync,
EqualityComparer<UIUpdateRequest>.Default,
factory._listener,
this.ThreadingContext.DisposalToken);

_toolTipPresenter.Dismissed += OnToolTipPresenterDismissed;
_subjectBuffer.Changed += OnTextBufferChanged;
textView.LostAggregateFocus += OnTextViewLostAggregateFocus;
}

public void Dispose()
=> _uiUpdateQueue.AddWork(UIUpdateRequest.DismissTooltip);

/// <summary>
/// Called after anyone consuming us makes a change that should be reflected in the UI.
/// </summary>
internal void EnqueueUIUpdate()
=> _uiUpdateQueue.AddWork(UIUpdateRequest.UpdateTooltip);

/// <summary>
/// The same as Dispose. Anyone taking ownership of this context wants to show their own UI, so we can just
/// close ours.
/// </summary>
public void TakeOwnership()
=> this.Dispose();

private void OnTextBufferChanged(object? sender, TextContentChangedEventArgs e)
{
if (CancelOnEdit)
CancelAndDispose();
}

private void OnTextViewLostAggregateFocus(object? sender, EventArgs e)
{
if (CancelOnFocusLost)
CancelAndDispose();
}

private void OnToolTipPresenterDismissed(object sender, EventArgs e)
=> CancelAndDispose();

public void CancelAndDispose()
{
_cancellationTokenSource.Cancel();
this.Dispose();
}

public bool CancelOnEdit
{
get
{
lock (Gate)
return _cancelOnEdit_DoNotAccessDirectly;
}

set
{
lock (Gate)
_cancelOnEdit_DoNotAccessDirectly = value;
}
}

public bool CancelOnFocusLost
{
get
{
lock (Gate)
return _cancelOnFocusLost_DoNotAccessDirectly;
}

set
{
lock (Gate)
_cancelOnFocusLost_DoNotAccessDirectly = value;
}
}

private ValueTask UpdateUIAsync(ImmutableArray<UIUpdateRequest> requests, CancellationToken cancellationToken)
{
Contract.ThrowIfTrue(requests.IsDefaultOrEmpty, "We must have gotten an actual request to process.");
Contract.ThrowIfTrue(requests.Length > 2, "At most we can have two requests in the queue (one to update, one to dismiss).");
Contract.ThrowIfFalse(
requests.Contains(UIUpdateRequest.DismissTooltip) || requests.Contains(UIUpdateRequest.UpdateTooltip),
"We didn't get an actual event we know about.");

return requests.Contains(UIUpdateRequest.DismissTooltip)
? DismissUIAsync()
: UpdateUIAsync();

async ValueTask DismissUIAsync()
{
await this.ThreadingContext.JoinableTaskFactory.SwitchToMainThreadAsync(cancellationToken);

// Ensure we only dismiss once.
if (_dismissed)
return;

_dismissed = true;

// Unhook any event handlers we've setup.
//
// note we have to disconnect from dismissal notifications so our own dismiss below doesn't cause us to
// re-enter and cancel ourselves.
_toolTipPresenter.Dismissed -= OnToolTipPresenterDismissed;
_subjectBuffer.Changed -= OnTextBufferChanged;
_textView.LostAggregateFocus -= OnTextViewLostAggregateFocus;

// Finally, dismiss the actual tool-tip.
_toolTipPresenter.Dismiss();

// Let our factory know that we were disposed so it can let go of us as well.
_factory.OnContextDisposed(this);
}

async ValueTask UpdateUIAsync()
{
// Build the current description in the background, then switch to the UI thread to actually update the
// tool-tip with it.
var data = this.BuildData();

await this.ThreadingContext.JoinableTaskFactory.SwitchToMainThreadAsync(cancellationToken);

// If we've been dismissed already, then no point in continuing.
if (_dismissed)
return;

// Todo: build a richer tool-tip that makes use of things like the progress reported, and perhaps has a
// close button.
_toolTipPresenter.StartOrUpdate(
_trackingSpan, new[] { string.Format(EditorFeaturesResources._0_Esc_to_cancel, data.description) });
}
}

public IUIThreadOperationScope AddScope(bool allowCancellation, string description)
{
var scope = new BackgroundWorkIndicatorScope(this, description);
lock (this.Gate)
{
_scopes = _scopes.Add(scope);
}

// We changed. Enqueue work to make sure the UI reflects this.
this.EnqueueUIUpdate();
return scope;
}

public void RemoveScope(BackgroundWorkIndicatorScope scope)
{
lock (this.Gate)
{
Contract.ThrowIfFalse(_scopes.Contains(scope));
_scopes = _scopes.Remove(scope);
}

// We changed. Enqueue work to make sure the UI reflects this.
this.EnqueueUIUpdate();
}

private (string description, ProgressInfo progressInfo) BuildData()
{
lock (Gate)
{
var description = _firstDescription;
var progressInfo = new ProgressInfo();

foreach (var scope in _scopes)
{
var scopeData = scope.ReadData_MustBeCalledUnderLock();

// use the description of the last scope if we have one. We don't have enough room to show all
// the descriptions at once.
description = scopeData.description;

var scopeProgressInfo = scopeData.progressInfo;
progressInfo = new ProgressInfo(
progressInfo.CompletedItems + scopeProgressInfo.CompletedItems,
progressInfo.TotalItems + scopeProgressInfo.TotalItems);
}

return (description, progressInfo);
}
}

string IUIThreadOperationContext.Description => BuildData().description;

bool IUIThreadOperationContext.AllowCancellation => true;
}
}
Loading

0 comments on commit fb71ede

Please sign in to comment.