diff --git a/docs/dev/CodingGuidelines/CodingGuidelines.md b/docs/dev/CodingGuidelines/CodingGuidelines.md
index 12d183e8e..711452eeb 100644
--- a/docs/dev/CodingGuidelines/CodingGuidelines.md
+++ b/docs/dev/CodingGuidelines/CodingGuidelines.md
@@ -355,8 +355,10 @@ You should avoid the use of `Task.Result` and `Task.Wait()` because this can cau
For details, see the following articles:
+* [Most recent practices recorded in the AI agent skill](https://github.com/madskristensen/vs-agent-plugins/blob/master/skills/handling-async-threading/SKILL.md)
* [How to: Manage multiple threads in managed code](https://docs.microsoft.com/en-us/visualstudio/extensibility/managing-multiple-threads-in-managed-code)
* [Asynchronous and multithreaded programming within VS using the JoinableTaskFactory](https://blogs.msdn.microsoft.com/andrewarnottms/2014/05/07/asynchronous-and-multithreaded-programming-within-vs-using-the-joinabletaskfactory/)
+ - [Another link to the same article](https://docs.microsoft.com/en-us/archive/blogs/andrewarnott/asynchronous-and-multithreaded-programming-within-vs-using-the-joinabletaskfactory)
* [Cookbook for Visual Studio](https://github.com/Microsoft/vs-threading/blob/master/doc/cookbook_vs.md)
* [Three Threading Rules](https://github.com/Microsoft/vs-threading/blob/master/doc/threading_rules.md)
diff --git a/src/Acuminator/Acuminator.Analyzers/Acuminator.Analyzers.csproj b/src/Acuminator/Acuminator.Analyzers/Acuminator.Analyzers.csproj
index 8c54e8883..be5571bcd 100644
--- a/src/Acuminator/Acuminator.Analyzers/Acuminator.Analyzers.csproj
+++ b/src/Acuminator/Acuminator.Analyzers/Acuminator.Analyzers.csproj
@@ -3,7 +3,7 @@
Acuminator Analyzers
Acuminator.Analyzers
netstandard2.0
- 4.0.1
+ 4.1.0
13.0
False
Acumatica, Inc.
diff --git a/src/Acuminator/Acuminator.Runner.NetFramework/Acuminator.Runner.NetFramework.csproj b/src/Acuminator/Acuminator.Runner.NetFramework/Acuminator.Runner.NetFramework.csproj
index 534e969a4..97c345289 100644
--- a/src/Acuminator/Acuminator.Runner.NetFramework/Acuminator.Runner.NetFramework.csproj
+++ b/src/Acuminator/Acuminator.Runner.NetFramework/Acuminator.Runner.NetFramework.csproj
@@ -7,7 +7,7 @@
net48
True
13.0
- 4.0.1
+ 4.1.0
enable
9999
en
diff --git a/src/Acuminator/Acuminator.Tests/Acuminator.Tests.csproj b/src/Acuminator/Acuminator.Tests/Acuminator.Tests.csproj
index 1c1a52a8e..3e2c6dbe4 100644
--- a/src/Acuminator/Acuminator.Tests/Acuminator.Tests.csproj
+++ b/src/Acuminator/Acuminator.Tests/Acuminator.Tests.csproj
@@ -5,7 +5,7 @@
Acuminator.Tests
net48
- 4.0.1
+ 4.1.0
13.0
enable
9999
@@ -124,7 +124,7 @@
-
+
diff --git a/src/Acuminator/Acuminator.Utilities/Acuminator.Utilities.csproj b/src/Acuminator/Acuminator.Utilities/Acuminator.Utilities.csproj
index 347706ef8..6033f6318 100644
--- a/src/Acuminator/Acuminator.Utilities/Acuminator.Utilities.csproj
+++ b/src/Acuminator/Acuminator.Utilities/Acuminator.Utilities.csproj
@@ -3,7 +3,7 @@
Acuminator Utilities
Acuminator.Utilities
netstandard2.0
- 4.0.1
+ 4.1.0
en
Acuminator.Utilities library with shared analysis helpers
Acumatica, Inc.
diff --git a/src/Acuminator/Acuminator.Vsix/AcuminatorVSPackage.cs b/src/Acuminator/Acuminator.Vsix/AcuminatorVSPackage.cs
index 0a337c80c..16720ca6c 100644
--- a/src/Acuminator/Acuminator.Vsix/AcuminatorVSPackage.cs
+++ b/src/Acuminator/Acuminator.Vsix/AcuminatorVSPackage.cs
@@ -75,7 +75,7 @@ public sealed class AcuminatorVSPackage : AsyncPackage
private const string SettingsCategoryName = SharedConstants.PackageName;
public const string PackageName = SharedConstants.PackageName;
- public const string PackageVersion = "4.0.1";
+ public const string PackageVersion = "4.1.0";
///
/// AcuminatorVSPackage GUID string.
@@ -91,12 +91,38 @@ public sealed class AcuminatorVSPackage : AsyncPackage
private const int INSTANCE_UNINITIALIZED = 0;
private const int INSTANCE_INITIALIZED = 1;
- private static int _instanceInitialized;
+ private static int _instanceInitialized = INSTANCE_UNINITIALIZED;
+
+ private const int NOT_DISPOSED = 0;
+ private const int DISPOSED = 1;
+ private volatile int _isDisposed = NOT_DISPOSED;
private OutOfProcessSettingsUpdater? _outOfProcessSettingsUpdater;
public static AcuminatorVSPackage Instance { get; private set; } = null!;
+
+ ///
+ /// The instance initialized for the .
+ /// If the package is not yet initialized or already disposed, is returned instead.
+ ///
+ ///
+ /// According to VS cookbook and VS team's discussion, the should be preferred over :
+ ///
+ /// - https://github.com/VsixCommunity/Community.VisualStudio.Toolkit/issues/24
+ /// - https://microsoft.github.io/VSSDK-Analyzers/analyzers/VSSDK007.html
+ ///
+ /// Both factories are created from the same — the one bound to the VS main thread.
+ /// So, they have identical participation in the JTF dependency graph that prevents deadlocks on the UI thread. Swapping one for the other changes nothing about deadlock behavior.
+ /// The difference is the . has its own collection, and package disposal drains it.
+ /// The work you started can't still be running against torn-down state after the package unloads.
+ /// On the other hand, is ambient and tracks nothing on your behalf. That's the reason behind VSSDK007 diagnostic.
+ ///
+ public static JoinableTaskFactory JTF =>
+ Instance?._isDisposed == NOT_DISPOSED
+ ? Instance.JoinableTaskFactory
+ : ThreadHelper.JoinableTaskFactory;
+
private readonly Lazy _generalOptionsPage =
new(() => Instance.GetDialogPage(typeof(GeneralOptionsPage)) as GeneralOptionsPage, isThreadSafe: true);
@@ -345,6 +371,21 @@ private async System.Threading.Tasks.Task IsSolutionLoadedAsync()
protected override void Dispose(bool disposing)
{
base.Dispose(disposing);
+
+ // It is important to set flag after the base call to Dispose to avoid rare but possible VS hanging on package unload.
+ // The _isDisposed flag check on JTF property prevents returning JTF from a disposed package. But if the code flips it before base.Dispose call, there will be a problem.
+ // The base AsyncPackage.Dispose(bool) method does: disposeCancellationTokenSource.Cancel() -> ThreadHelper.JoinableTaskFactory.Run(JoinableTaskCollection.JoinTillEmptyAsync) — no token, no timeout —> Package.Dispose.
+ // So the main thread blocks until every JoinableTask in the package collection finishes.
+ // During that drain, AcuminatorVSPackage.JTF already returns ThreadHelper's factory. A FileAndForgetAcuminatorTask wrapper started before shutdown is a member of the package collection (so the drain waits on it)
+ // and awaits a foreign task. When that foreign task resumes and needs its main-thread hop via AcuminatorVSPackage.JTF.SwitchToMainThreadAsync(), RequestSwitchToMainThread (with a null ambient job) creates a transient
+ // on ThreadHelper's factory — which has no collection, so the transient is not in the drained graph.A main thread blocked in Run pumps only joined work,
+ // so that continuation never runs -> the foreign task never completes -> the wrapper never completes -> JoinTillEmptyAsync never returns -> indefinite hang on close.
+ //
+ // Had the flag been set after base.Dispose, the same hop would go through the package factory, land in the collection, and be pumped by the drain — no hang.
+ // The "removed redundant cancellation tokens" commit compounds it: those switches no longer observe DisposalToken, so in-flight work can't self-cancel to escape the wait either.
+ if (Interlocked.Exchange(ref _isDisposed, DISPOSED) == DISPOSED)
+ return;
+
AcuminatorLogger?.Dispose();
_outOfProcessSettingsUpdater?.Dispose();
diff --git a/src/Acuminator/Acuminator.Vsix/Coloriser/AsyncTagging/BackgroundTagging.cs b/src/Acuminator/Acuminator.Vsix/Coloriser/AsyncTagging/BackgroundTagging.cs
index 7ab7f900e..ac7cb8182 100644
--- a/src/Acuminator/Acuminator.Vsix/Coloriser/AsyncTagging/BackgroundTagging.cs
+++ b/src/Acuminator/Acuminator.Vsix/Coloriser/AsyncTagging/BackgroundTagging.cs
@@ -51,7 +51,7 @@ public static BackgroundTagging StartBackgroundTagging(PXRoslynColorizerTagger t
_vsTaskScheduler);
// ContinueWith schedules the lambda on the VS UI thread scheduler. The lambda runs on the UI thread and calls AfterTaggingActionAsync(...).
- // Inside AfterTaggingActionAsync, the important path calls ThreadHelper.JoinableTaskFactory.RunAsync(tagger.RaiseTagsChangedAsync).Task
+ // Inside AfterTaggingActionAsync, the important path calls AcuminatorVSPackage.JTF.RunAsync(tagger.RaiseTagsChangedAsync).Task
// this starts RaiseTagsChangedAsync and immediately returns the underlying Task representing it (still running).
// The lambda returns that inner Task immediately — it does not await it.
// The outer Task stored in TaggingTask is marked as Completed (RanToCompletion) at this point, because the lambda has returned.
@@ -124,7 +124,7 @@ private static Task AfterTaggingActionAsync(Task taggingTask, PXRoslynColorizerT
}
// We should be on UI thread here but the tagger.RaiseTagsChangedAsync switches to UI thread from non UI threads internally if needed
- return Shell.ThreadHelper.JoinableTaskFactory.RunAsync(tagger.RaiseTagsChangedAsync).Task;
+ return AcuminatorVSPackage.JTF.RunAsync(tagger.RaiseTagsChangedAsync).Task;
}
}
}
diff --git a/src/Acuminator/Acuminator.Vsix/Coloriser/Base/PXTaggerBase.cs b/src/Acuminator/Acuminator.Vsix/Coloriser/Base/PXTaggerBase.cs
index ad7f57dd4..96f5d4f37 100644
--- a/src/Acuminator/Acuminator.Vsix/Coloriser/Base/PXTaggerBase.cs
+++ b/src/Acuminator/Acuminator.Vsix/Coloriser/Base/PXTaggerBase.cs
@@ -3,12 +3,16 @@
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
+using System.Runtime.CompilerServices;
+using System.Threading;
using System.Threading.Tasks;
using Acuminator.Utilities.Common;
using Acuminator.Vsix.Settings;
+using Acuminator.Vsix.Utilities;
using Microsoft.VisualStudio.Text;
+using Microsoft.VisualStudio.Text.Tagging;
using ThreadHelper = Microsoft.VisualStudio.Shell.ThreadHelper;
@@ -55,6 +59,16 @@ protected PXTaggerBase(ITextBuffer buffer, ITextDocumentFactoryService textDocum
_disposedNotification.CurrentTextDocumentDisposed += CleanupOnTextDocumentDisposed;
}
+ protected static IEnumerable> GetIntersectionWithRequestedTags(
+ IReadOnlyCollection> tags,
+ NormalizedSnapshotSpanCollection requestedSpans)
+ where TTag : ITag
+ {
+ return tags?.Count > 0
+ ? tags.Where(tag => requestedSpans.IntersectsWith(tag.Span))
+ : [];
+ }
+
protected virtual void ColoringSettingChangedHandler(object sender, SettingChangedEventArgs e)
{
ColoringSettingsChanged = true;
@@ -64,11 +78,34 @@ protected virtual void ColoringSettingChangedHandler(object sender, SettingChang
RaiseTagsChanged();
}
+ ///
+ /// Raises the tags changed asynchronously and do not observe the raised task.
+ ///
+ ///
+ /// The method is intended to be called from void-returning event handlers.
+ ///
+ /// (Optional) The method raising the tag changed event.
+ protected void RaiseTagsChangedAsyncAndForget([CallerMemberName] string? calledFrom = null)
+ {
+ if (ThreadHelper.CheckAccess())
+ RaiseTagsChanged();
+ else
+ {
+ string taggerName = this.GetType().Name;
+ calledFrom = calledFrom.NullIfWhiteSpace() ?? nameof(RaiseTagsChangedAsyncAndForget);
+
+ // See the VS cookbook for file and forget methods
+ // https://github.com/microsoft/vs-threading/blob/main/docfx/docs/cookbook_vs.md#task-returning-fire-and-forget-methods
+ var raiseTaggerChanged = () => RaiseTagsChangedAsync();
+ raiseTaggerChanged.FileAndForgetAcuminatorTask($"vs/{AcuminatorVSPackage.PackageName}/{taggerName}/{calledFrom}");
+ }
+ }
+
internal async Task RaiseTagsChangedAsync()
{
if (!ThreadHelper.CheckAccess())
{
- await ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync();
+ await AcuminatorVSPackage.JTF.SwitchToMainThreadAsync();
}
RaiseTagsChangedImpl();
diff --git a/src/Acuminator/Acuminator.Vsix/Coloriser/Outlining/PXOutliningTagger.cs b/src/Acuminator/Acuminator.Vsix/Coloriser/Outlining/PXOutliningTagger.cs
index 215664bdf..25bae6c84 100644
--- a/src/Acuminator/Acuminator.Vsix/Coloriser/Outlining/PXOutliningTagger.cs
+++ b/src/Acuminator/Acuminator.Vsix/Coloriser/Outlining/PXOutliningTagger.cs
@@ -31,9 +31,9 @@ public PXOutliningTagger(ITextBuffer buffer, ITextDocumentFactoryService textDoc
{
}
- public IEnumerable> GetTags(NormalizedSnapshotSpanCollection spans)
+ public IEnumerable> GetTags(NormalizedSnapshotSpanCollection requestedSpans)
{
- if (spans == null || spans.Count == 0 || AcuminatorVSPackage.Instance?.UseBqlOutlining != true)
+ if (requestedSpans?.Count is null or 0 || AcuminatorVSPackage.Instance?.UseBqlOutlining != true)
return [];
if (ColorizerTagger == null)
@@ -48,7 +48,8 @@ public IEnumerable> GetTags(NormalizedSnapshotSpan
if (!HasReferenceToAcumaticaPlatform)
return [];
- return ColorizerTagger.OutliningsTagsCache.ProcessedTags;
+ var processedTags = ColorizerTagger.OutliningsTagsCache.ProcessedTags;
+ return GetIntersectionWithRequestedTags(processedTags, requestedSpans);
}
private static bool TryGetColorizingTaggerFromBuffer(ITextBuffer textBuffer, out PXRoslynColorizerTagger colorizingTagger)
diff --git a/src/Acuminator/Acuminator.Vsix/Coloriser/Outlining/PXOutliningTaggerProvider.cs b/src/Acuminator/Acuminator.Vsix/Coloriser/Outlining/PXOutliningTaggerProvider.cs
index a6a10b322..14f3f95fc 100644
--- a/src/Acuminator/Acuminator.Vsix/Coloriser/Outlining/PXOutliningTaggerProvider.cs
+++ b/src/Acuminator/Acuminator.Vsix/Coloriser/Outlining/PXOutliningTaggerProvider.cs
@@ -28,7 +28,7 @@ public PXOutliningTaggerProvider(ITextDocumentFactoryService textDocumentFactory
public ITagger? CreateTagger(ITextBuffer buffer) where T : ITag
{
- if (buffer == null || !ThreadHelper.CheckAccess())
+ if (buffer == null || !typeof(ITagger).IsAssignableFrom(typeof(PXOutliningTagger)) || !ThreadHelper.CheckAccess())
return null;
PXOutliningTagger outliningTagger = buffer.Properties.GetOrCreateSingletonProperty(typeof(PXOutliningTagger), () =>
diff --git a/src/Acuminator/Acuminator.Vsix/Coloriser/PXColorizerTaggerProvider.cs b/src/Acuminator/Acuminator.Vsix/Coloriser/PXColorizerTaggerProvider.cs
index d37c49927..f1110cf11 100644
--- a/src/Acuminator/Acuminator.Vsix/Coloriser/PXColorizerTaggerProvider.cs
+++ b/src/Acuminator/Acuminator.Vsix/Coloriser/PXColorizerTaggerProvider.cs
@@ -53,8 +53,11 @@ public PXColorizerTaggerProvider(IClassificationTypeRegistryService classificati
public virtual ITagger? CreateTagger(ITextView textView, ITextBuffer textBuffer)
where T : ITag
{
- if (textView == null || textBuffer == null || textView.TextBuffer != textBuffer || !ThreadHelper.CheckAccess())
+ if (textView == null || textBuffer == null || textView.TextBuffer != textBuffer ||
+ !typeof(ITagger).IsAssignableFrom(typeof(PXRoslynColorizerTagger)) || !ThreadHelper.CheckAccess())
+ {
return null;
+ }
var tagger = textBuffer.Properties.GetOrCreateSingletonProperty(typeof(PXRoslynColorizerTagger), () =>
{
diff --git a/src/Acuminator/Acuminator.Vsix/Coloriser/PXRoslynColorizerTagger.cs b/src/Acuminator/Acuminator.Vsix/Coloriser/PXRoslynColorizerTagger.cs
index 9adf80168..d9aec1aef 100644
--- a/src/Acuminator/Acuminator.Vsix/Coloriser/PXRoslynColorizerTagger.cs
+++ b/src/Acuminator/Acuminator.Vsix/Coloriser/PXRoslynColorizerTagger.cs
@@ -136,13 +136,15 @@ protected internal override void ResetCacheAndFlags(ITextSnapshot? newSnapshotTo
///
/// Gets the tags asynchronously from the specified snapshot with Roslyn.
///
- /// The spans for tagging. The current implementation doesn't take them into account and re-tags the entire document.
+ ///
+ /// The spans for tagging. The current implementation re-tags the entire document but returns the intersection with the requested spans.
+ ///
///
/// The current snapshot of the collected tags.
///
- public IEnumerable> GetTags(NormalizedSnapshotSpanCollection spans)
+ public IEnumerable> GetTags(NormalizedSnapshotSpanCollection requestedSpans)
{
- if (spans?.Count is null or 0 || AcuminatorVSPackage.Instance?.ColoringEnabled != true || !HasReferenceToAcumaticaPlatform)
+ if (requestedSpans?.Count is null or 0 || AcuminatorVSPackage.Instance?.ColoringEnabled != true || !HasReferenceToAcumaticaPlatform)
return [];
var workspace = _roslynWorkspaceProvider.Workspace;
@@ -150,11 +152,12 @@ public IEnumerable> GetTags(NormalizedSnapshotSpanC
if (workspace == null)
return [];
- ITextSnapshot newSnapshotToTag = spans[0].Snapshot;
+ ITextSnapshot newSnapshotToTag = requestedSpans[0].Snapshot;
if (CheckIfParsingAndRetaggingIsNotNecessary(newSnapshotToTag))
{
- return ClassificationTagsCache.ProcessedTags;
+ var cachedProcessedTags = ClassificationTagsCache.ProcessedTags;
+ return GetIntersectionWithRequestedTags(cachedProcessedTags, requestedSpans);
}
if (BackgroundTagging != null)
@@ -166,7 +169,8 @@ public IEnumerable> GetTags(NormalizedSnapshotSpanC
ResetCacheAndFlags(newSnapshotToTag);
BackgroundTagging = BackgroundTagging.StartBackgroundTagging(this);
- return ClassificationTagsCache.ProcessedTags;
+ var processedTags = ClassificationTagsCache.ProcessedTags;
+ return GetIntersectionWithRequestedTags(processedTags, requestedSpans);
}
protected virtual bool CheckIfParsingAndRetaggingIsNotNecessary(ITextSnapshot newSnapshotToTag) =>
@@ -278,11 +282,7 @@ private void WorkspaceAttachedToDocumentChanged(object sender, DocumentWorkspace
// We need to raise the tags changed event to trigger re-coloring on workspace change
ResetCacheAndFlags(newSnapshotToCache: null);
-
- if (ThreadHelper.CheckAccess())
- RaiseTagsChanged();
- else
- ThreadHelper.JoinableTaskFactory.Run(RaiseTagsChangedAsync);
+ RaiseTagsChangedAsyncAndForget();
}
private void OnWorkspaceChanged(object sender, WorkspaceChangeEventArgs e)
@@ -329,12 +329,8 @@ private void OnWorkspaceChanged(object sender, WorkspaceChangeEventArgs e)
if (oldHasReferenceToAcumaticaPlatform != _hasReferenceToAcumaticaPlatform)
{
ResetCacheAndFlags(newSnapshotToCache: null);
-
- if (ThreadHelper.CheckAccess())
- RaiseTagsChanged();
- else
- ThreadHelper.JoinableTaskFactory.Run(RaiseTagsChangedAsync);
- }
+ RaiseTagsChangedAsyncAndForget();
+ }
}
private bool GetAcumaticaReferenceOnProjectChange(WorkspaceChangeEventArgs e, bool oldHasReferenceToAcumaticaPlatform)
diff --git a/src/Acuminator/Acuminator.Vsix/Coloriser/Roslyn/PXRoslynColorizerTagger.PXColorizerSyntaxWalker.cs b/src/Acuminator/Acuminator.Vsix/Coloriser/Roslyn/PXRoslynColorizerTagger.PXColorizerSyntaxWalker.cs
index 80906c9bc..9f62ad5a8 100644
--- a/src/Acuminator/Acuminator.Vsix/Coloriser/Roslyn/PXRoslynColorizerTagger.PXColorizerSyntaxWalker.cs
+++ b/src/Acuminator/Acuminator.Vsix/Coloriser/Roslyn/PXRoslynColorizerTagger.PXColorizerSyntaxWalker.cs
@@ -466,7 +466,7 @@ private void UpdateCodeEditorIfNecessary()
var cancellationToken = _cancellationToken;
#pragma warning disable VSTHRD110 // Observe result of async calls
- Shell.ThreadHelper.JoinableTaskFactory.RunAsync(async () =>
+ AcuminatorVSPackage.JTF.RunAsync(async () =>
{
if (!cancellationToken.IsCancellationRequested)
{
diff --git a/src/Acuminator/Acuminator.Vsix/Commands/BQL Fixer/FixBqlCommand.cs b/src/Acuminator/Acuminator.Vsix/Commands/BQL Fixer/FixBqlCommand.cs
index c241f0fc1..6700b9d94 100644
--- a/src/Acuminator/Acuminator.Vsix/Commands/BQL Fixer/FixBqlCommand.cs
+++ b/src/Acuminator/Acuminator.Vsix/Commands/BQL Fixer/FixBqlCommand.cs
@@ -59,13 +59,15 @@ public static void Initialize(AsyncPackage package, OleMenuCommandService comman
}
}
- protected override void CommandCallback(object sender, EventArgs e) =>
- CommandCallbackAsync()
- .FileAndForget($"vs/{AcuminatorVSPackage.PackageName}/{nameof(FixBqlCommand)}");
+ protected override void CommandCallback(object sender, EventArgs e)
+ {
+ var commandExecutor = () => CommandCallbackAsync();
+ commandExecutor.FileAndForgetAcuminatorTask($"vs/{AcuminatorVSPackage.PackageName}/{nameof(FixBqlCommand)}");
+ }
private async System.Threading.Tasks.Task CommandCallbackAsync()
{
- await ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync();
+ await AcuminatorVSPackage.JTF.SwitchToMainThreadAsync();
IWpfTextView? textView = await ServiceProvider.GetWpfTextViewAsync();
if (textView == null)
@@ -130,13 +132,13 @@ private async System.Threading.Tasks.Task CommandCallbackAsync()
// have to format, because cannot save all original indention
BqlFormatter formatter = BqlFormatter.FromTextView(textView);
- var formatedRoot = formatter.Format(newSyntaxRoot, newSemanticModel);
+ var formattedRoot = formatter.Format(newSyntaxRoot, newSemanticModel);
- await ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync(); // Return to UI thread
+ await AcuminatorVSPackage.JTF.SwitchToMainThreadAsync(); // Return to UI thread
if (!textView.TextBuffer.EditInProgress)
{
- var formattedDocument = document.WithSyntaxRoot(formatedRoot);
+ var formattedDocument = document.WithSyntaxRoot(formattedRoot);
ApplyChanges(document, formattedDocument);
}
}
diff --git a/src/Acuminator/Acuminator.Vsix/Commands/DiagnosticSuppression/Base/SuppressDiagnosticCommandBase.cs b/src/Acuminator/Acuminator.Vsix/Commands/DiagnosticSuppression/Base/SuppressDiagnosticCommandBase.cs
index c154be94a..a4bb4a6af 100644
--- a/src/Acuminator/Acuminator.Vsix/Commands/DiagnosticSuppression/Base/SuppressDiagnosticCommandBase.cs
+++ b/src/Acuminator/Acuminator.Vsix/Commands/DiagnosticSuppression/Base/SuppressDiagnosticCommandBase.cs
@@ -32,9 +32,11 @@ protected SuppressDiagnosticCommandBase(Shell.AsyncPackage package, Shell.OleMen
{
}
- protected override void CommandCallback(object sender, EventArgs e) =>
- CommandCallbackAsync()
- .FileAndForget($"vs/{AcuminatorVSPackage.PackageName}/{this.GetType().Name}");
+ protected override void CommandCallback(object sender, EventArgs e)
+ {
+ var commandExecutor = () => CommandCallbackAsync();
+ commandExecutor.FileAndForgetAcuminatorTask($"vs/{AcuminatorVSPackage.PackageName}/{this.GetType().Name}");
+ }
protected virtual async Task CommandCallbackAsync()
{
@@ -104,7 +106,7 @@ protected bool IsPlatformReferenced(SemanticModel semanticModel)
protected async Task> GetDiagnosticsAsync(Document document, TextSpan caretSpan)
{
- await Shell.ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync();
+ await AcuminatorVSPackage.JTF.SwitchToMainThreadAsync();
IComponentModel? componentModel = await Package.GetServiceAsync(throwOnFailure: false);
if (componentModel == null)
@@ -141,14 +143,14 @@ protected virtual Task SuppressDiagnosticsAsync(List diagnosticD
case 1:
return SuppressSingleDiagnosticOnNodeAsync(diagnosticData[0], document, syntaxRoot, semanticModel, nodeWithDiagnostic);
default:
- return SupressMultipleDiagnosticOnNodeAsync(diagnosticData, document, syntaxRoot, semanticModel, nodeWithDiagnostic);
+ return SuppressMultipleDiagnosticOnNodeAsync(diagnosticData, document, syntaxRoot, semanticModel, nodeWithDiagnostic);
}
}
protected abstract Task SuppressSingleDiagnosticOnNodeAsync(DiagnosticData diagnostic, Document document, SyntaxNode syntaxRoot,
SemanticModel semanticModel, SyntaxNode nodeWithDiagnostic);
- protected abstract Task SupressMultipleDiagnosticOnNodeAsync(List diagnosticData, Document document, SyntaxNode syntaxRoot,
- SemanticModel semanticModel, SyntaxNode nodeWithDiagnostic);
+ protected abstract Task SuppressMultipleDiagnosticOnNodeAsync(List diagnosticData, Document document, SyntaxNode syntaxRoot,
+ SemanticModel semanticModel, SyntaxNode nodeWithDiagnostic);
}
}
diff --git a/src/Acuminator/Acuminator.Vsix/Commands/DiagnosticSuppression/BuildAction/VsixBuildActionSetterVS2019.cs b/src/Acuminator/Acuminator.Vsix/Commands/DiagnosticSuppression/BuildAction/VsixBuildActionSetterVS2019.cs
index ea6427198..9672d4ac6 100644
--- a/src/Acuminator/Acuminator.Vsix/Commands/DiagnosticSuppression/BuildAction/VsixBuildActionSetterVS2019.cs
+++ b/src/Acuminator/Acuminator.Vsix/Commands/DiagnosticSuppression/BuildAction/VsixBuildActionSetterVS2019.cs
@@ -14,7 +14,7 @@
namespace Acuminator.Vsix.DiagnosticSuppression
{
///
- /// A helper to set Build Action for newly added suppression file in VS 2019 or older that can use VS COM API directy.
+ /// A helper to set Build Action for newly added suppression file in VS 2019 or older that can use VS COM API directly.
///
public class VsixBuildActionSetterVS2019 : ICustomBuildActionSetter
{
@@ -30,8 +30,8 @@ public bool SetBuildAction(string roslynSuppressionFilePath, string buildActionT
{
#pragma warning disable VSTHRD104 // Offer async methods
// Justification: need to use sync API since consumer is code action operation which require synchronous execution
- // and located in the Utilities, so it can't use ThreadHelper.JoinableTaskFactory itself
- return ThreadHelper.JoinableTaskFactory.Run(() => SetBuildActionAsync(roslynSuppressionFilePath, buildActionToSet));
+ // and located in the Utilities, so the calling code can't use JoinableTaskFactory from AcuminatorVSPackage.JTF
+ return AcuminatorVSPackage.JTF.Run(() => SetBuildActionAsync(roslynSuppressionFilePath, buildActionToSet));
#pragma warning restore VSTHRD104
}
catch (Exception ex)
@@ -44,7 +44,7 @@ public bool SetBuildAction(string roslynSuppressionFilePath, string buildActionT
private async Task SetBuildActionAsync(string roslynSuppressionFilePath, string buildActionToSet)
{
var oldScheduler = TaskScheduler.Current;
- await ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync();
+ await AcuminatorVSPackage.JTF.SwitchToMainThreadAsync();
try
{
diff --git a/src/Acuminator/Acuminator.Vsix/Commands/DiagnosticSuppression/BuildAction/VsixBuildActionSetterVS2022.cs b/src/Acuminator/Acuminator.Vsix/Commands/DiagnosticSuppression/BuildAction/VsixBuildActionSetterVS2022.cs
index 17acba441..408d929a5 100644
--- a/src/Acuminator/Acuminator.Vsix/Commands/DiagnosticSuppression/BuildAction/VsixBuildActionSetterVS2022.cs
+++ b/src/Acuminator/Acuminator.Vsix/Commands/DiagnosticSuppression/BuildAction/VsixBuildActionSetterVS2022.cs
@@ -35,7 +35,7 @@ public bool SetBuildAction(string roslynSuppressionFilePath, string buildActionT
#pragma warning disable VSTHRD104 // Offer async methods
// Justification: need to use sync API since consumer is code action operation which require synchronous execution
// and located in the Utilities, so it can't use ThreadHelper.JoinableTaskFactory itself
- return ThreadHelper.JoinableTaskFactory.Run(() => SetBuildActionAsync(roslynSuppressionFilePath, buildActionToSet));
+ return AcuminatorVSPackage.JTF.Run(() => SetBuildActionAsync(roslynSuppressionFilePath, buildActionToSet));
#pragma warning restore VSTHRD104
}
catch (Exception ex)
@@ -51,7 +51,7 @@ private async Task SetBuildActionAsync(string roslynSuppressionFilePath, s
try
{
- await ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync();
+ await AcuminatorVSPackage.JTF.SwitchToMainThreadAsync();
dynamic? dte = GetDTE();
diff --git a/src/Acuminator/Acuminator.Vsix/Commands/DiagnosticSuppression/Suppression File/SuppressDiagnosticInSuppressionFileCommand.cs b/src/Acuminator/Acuminator.Vsix/Commands/DiagnosticSuppression/Suppression File/SuppressDiagnosticInSuppressionFileCommand.cs
index 92af9125c..8a36a73c5 100644
--- a/src/Acuminator/Acuminator.Vsix/Commands/DiagnosticSuppression/Suppression File/SuppressDiagnosticInSuppressionFileCommand.cs
+++ b/src/Acuminator/Acuminator.Vsix/Commands/DiagnosticSuppression/Suppression File/SuppressDiagnosticInSuppressionFileCommand.cs
@@ -92,7 +92,7 @@ protected override async Task SuppressSingleDiagnosticOnNodeAsync(DiagnosticData
private async Task<(TextDocument SuppressionFile, Project Project)> GetProjectAndSuppressionFileAsync(ProjectId projectId)
{
- await Shell.ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync();
+ await AcuminatorVSPackage.JTF.SwitchToMainThreadAsync();
var workspace = await Package.GetVSWorkspaceAsync();
Project? project = workspace?.CurrentSolution?.GetProject(projectId);
@@ -123,7 +123,7 @@ private void ShowErrorMessage(TextDocument? suppressionFile, Project project)
MessageBox.Show(errorMessage.ToString(), AcuminatorVSPackage.PackageName);
}
- protected override Task SupressMultipleDiagnosticOnNodeAsync(List diagnosticData, Document document, SyntaxNode syntaxRoot,
+ protected override Task SuppressMultipleDiagnosticOnNodeAsync(List diagnosticData, Document document, SyntaxNode syntaxRoot,
SemanticModel semanticModel, SyntaxNode nodeWithDiagnostic)
{
MessageBox.Show(VSIXResource.DiagnosticSuppression_MultipleDiagnosticFound, AcuminatorVSPackage.PackageName);
diff --git a/src/Acuminator/Acuminator.Vsix/Commands/Formatter/FormatBqlCommand.cs b/src/Acuminator/Acuminator.Vsix/Commands/Formatter/FormatBqlCommand.cs
index 39a715e9a..8ae95f50e 100644
--- a/src/Acuminator/Acuminator.Vsix/Commands/Formatter/FormatBqlCommand.cs
+++ b/src/Acuminator/Acuminator.Vsix/Commands/Formatter/FormatBqlCommand.cs
@@ -67,13 +67,15 @@ public static void Initialize(AsyncPackage package, OleMenuCommandService comman
}
#pragma warning restore CS8774
- protected override void CommandCallback(object sender, EventArgs e) =>
- CommandCallbackAsync()
- .FileAndForget($"vs/{AcuminatorVSPackage.PackageName}/{nameof(FormatBqlCommand)}");
+ protected override void CommandCallback(object sender, EventArgs e)
+ {
+ var commandExecutor = () => CommandCallbackAsync();
+ commandExecutor.FileAndForgetAcuminatorTask($"vs/{AcuminatorVSPackage.PackageName}/{nameof(FormatBqlCommand)}");
+ }
private async System.Threading.Tasks.Task CommandCallbackAsync()
{
- await ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync();
+ await AcuminatorVSPackage.JTF.SwitchToMainThreadAsync();
IWpfTextView? textView = await ServiceProvider.GetWpfTextViewAsync();
if (textView == null || Package.DisposalToken.IsCancellationRequested)
@@ -124,7 +126,7 @@ private async System.Threading.Tasks.Task CommandCallbackAsync()
formattedRoot = formatter.Format(syntaxRoot, semanticModel) ?? syntaxRoot;
}
- await ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync(); // Return to UI thread
+ await AcuminatorVSPackage.JTF.SwitchToMainThreadAsync(); // Return to UI thread
if (!textView.TextBuffer.EditInProgress && !syntaxRoot.Equals(formattedRoot))
{
diff --git a/src/Acuminator/Acuminator.Vsix/Commands/GoToDeclarationOrHandler/GoToDeclarationOrHandlerCommand.cs b/src/Acuminator/Acuminator.Vsix/Commands/GoToDeclarationOrHandler/GoToDeclarationOrHandlerCommand.cs
index 9d934e64b..7144098f4 100644
--- a/src/Acuminator/Acuminator.Vsix/Commands/GoToDeclarationOrHandler/GoToDeclarationOrHandlerCommand.cs
+++ b/src/Acuminator/Acuminator.Vsix/Commands/GoToDeclarationOrHandler/GoToDeclarationOrHandlerCommand.cs
@@ -80,10 +80,12 @@ internal static void Initialize(Shell.AsyncPackage package, Shell.OleMenuCommand
}
#pragma warning restore CS8774
- protected override void CommandCallback(object sender, EventArgs e) =>
- CommandCallbackAsync()
- .FileAndForget($"vs/{AcuminatorVSPackage.PackageName}/{nameof(GoToDeclarationOrHandlerCommand)}");
-
+ protected override void CommandCallback(object sender, EventArgs e)
+ {
+ var commandExecutor = () => CommandCallbackAsync();
+ commandExecutor.FileAndForgetAcuminatorTask($"vs/{AcuminatorVSPackage.PackageName}/{nameof(GoToDeclarationOrHandlerCommand)}");
+ }
+
private async Task CommandCallbackAsync()
{
IWpfTextView? textView = await ServiceProvider.GetWpfTextViewAsync();
diff --git a/src/Acuminator/Acuminator.Vsix/Properties/AssemblyInfo.cs b/src/Acuminator/Acuminator.Vsix/Properties/AssemblyInfo.cs
index 5d35b8c6c..aa0b1d82e 100644
--- a/src/Acuminator/Acuminator.Vsix/Properties/AssemblyInfo.cs
+++ b/src/Acuminator/Acuminator.Vsix/Properties/AssemblyInfo.cs
@@ -11,7 +11,7 @@
[assembly: AssemblyTrademark("")]
[assembly: AssemblyCulture("")]
[assembly: ComVisible(false)]
-[assembly: AssemblyVersion("4.0.1")]
-[assembly: AssemblyFileVersion("4.0.1")]
+[assembly: AssemblyVersion("4.1.0")]
+[assembly: AssemblyFileVersion("4.1.0")]
[assembly: InternalsVisibleTo("Acuminator.Tests")]
diff --git a/src/Acuminator/Acuminator.Vsix/Tool Windows/CodeMap/CodeMapWindow.cs b/src/Acuminator/Acuminator.Vsix/Tool Windows/CodeMap/CodeMapWindow.cs
index 51931b4bf..80a5de344 100644
--- a/src/Acuminator/Acuminator.Vsix/Tool Windows/CodeMap/CodeMapWindow.cs
+++ b/src/Acuminator/Acuminator.Vsix/Tool Windows/CodeMap/CodeMapWindow.cs
@@ -75,7 +75,7 @@ public override async void OnToolWindowCreated()
if (workspace == null)
return;
- IWpfTextView? textView = await ThreadHelper.JoinableTaskFactory.RunAsync(serviceProvider.GetWpfTextViewAsync);
+ IWpfTextView? textView = await AcuminatorVSPackage.JTF.RunAsync(serviceProvider.GetWpfTextViewAsync);
Document? document = textView?.TextSnapshot?.GetOpenDocumentInCurrentContextWithChanges();
if (CodeMapWPFControl.DataContext is CodeMapWindowViewModel codeMapViewModel)
diff --git a/src/Acuminator/Acuminator.Vsix/Tool Windows/CodeMap/UI/CodeMapTreeControl.xaml.cs b/src/Acuminator/Acuminator.Vsix/Tool Windows/CodeMap/UI/CodeMapTreeControl.xaml.cs
index d86d7133b..f498fe300 100644
--- a/src/Acuminator/Acuminator.Vsix/Tool Windows/CodeMap/UI/CodeMapTreeControl.xaml.cs
+++ b/src/Acuminator/Acuminator.Vsix/Tool Windows/CodeMap/UI/CodeMapTreeControl.xaml.cs
@@ -39,8 +39,9 @@ private void TreeNode_PreviewMouseLeftButtonDown(object sender, MouseButtonEvent
if (e.ClickCount >= 2)
{
- NavigateOnClickAsync(treeNodeVM)
- .FileAndForget($"vs/{AcuminatorVSPackage.PackageName}/{nameof(CodeMapWindowViewModel)}/{nameof(TreeNode_PreviewMouseLeftButtonDown)}");
+ var navigationHandler = () => NavigateOnClickAsync(treeNodeVM);
+ navigationHandler.FileAndForgetAcuminatorTask(
+ $"vs/{AcuminatorVSPackage.PackageName}/{nameof(CodeMapWindowViewModel)}/{nameof(TreeNode_PreviewMouseLeftButtonDown)}");
}
}
diff --git a/src/Acuminator/Acuminator.Vsix/Tool Windows/CodeMap/ViewModel/CodeMapWindowViewModel.CodeMapDteEventsObserver.cs b/src/Acuminator/Acuminator.Vsix/Tool Windows/CodeMap/ViewModel/CodeMapWindowViewModel.CodeMapDteEventsObserver.cs
index f3a299084..154fc9192 100644
--- a/src/Acuminator/Acuminator.Vsix/Tool Windows/CodeMap/ViewModel/CodeMapWindowViewModel.CodeMapDteEventsObserver.cs
+++ b/src/Acuminator/Acuminator.Vsix/Tool Windows/CodeMap/ViewModel/CodeMapWindowViewModel.CodeMapDteEventsObserver.cs
@@ -137,14 +137,16 @@ private void SetVisibilityForCodeMapWindow(EnvDTE.Window window, bool windowIsVi
if (!wasVisible && _codeMapViewModel.IsVisible) //Handle the case when WindowShowing event happens after WindowActivated event
{
- RefreshCodeMapAsync()
- .FileAndForget($"vs/{AcuminatorVSPackage.PackageName}/{nameof(CodeMapWindowViewModel)}/{nameof(SetVisibilityForCodeMapWindow)}");
+ var refreshCodeMapAction = () => RefreshCodeMapAsync();
+ refreshCodeMapAction
+ .FileAndForgetAcuminatorTask($"vs/{AcuminatorVSPackage.PackageName}/{nameof(CodeMapWindowViewModel)}/{nameof(SetVisibilityForCodeMapWindow)}");
}
}
else if (IsSwitchingToAnotherDocumentWhileCodeMapIsEmpty())
- {
- RefreshCodeMapAsync()
- .FileAndForget($"vs/{AcuminatorVSPackage.PackageName}/{nameof(CodeMapWindowViewModel)}/{nameof(SetVisibilityForCodeMapWindow)}");
+ {
+ var refreshCodeMapAction = () => RefreshCodeMapAsync();
+ refreshCodeMapAction
+ .FileAndForgetAcuminatorTask($"vs/{AcuminatorVSPackage.PackageName}/{nameof(CodeMapWindowViewModel)}/{nameof(SetVisibilityForCodeMapWindow)}");
}
//-------------------------------------------Local Function----------------------------------------------------------------------------------------
@@ -162,15 +164,18 @@ private void SolutionEvents_AfterClosing()
_codeMapViewModel.DocumentModel = null;
}
- private void WindowEvents_WindowActivated(EnvDTE.Window gotFocus, EnvDTE.Window lostFocus) =>
- WindowEventsWindowActivatedAsync(gotFocus, lostFocus)
- .FileAndForget($"vs/{AcuminatorVSPackage.PackageName}/{nameof(CodeMapWindowViewModel)}/{nameof(WindowEvents_WindowActivated)}");
+ private void WindowEvents_WindowActivated(EnvDTE.Window gotFocus, EnvDTE.Window lostFocus)
+ {
+ var windowActivatedHandler = () => WindowEventsWindowActivatedAsync(gotFocus, lostFocus);
+ windowActivatedHandler
+ .FileAndForgetAcuminatorTask($"vs/{AcuminatorVSPackage.PackageName}/{nameof(CodeMapWindowViewModel)}/{nameof(WindowEvents_WindowActivated)}");
+ }
private async Task WindowEventsWindowActivatedAsync(EnvDTE.Window gotFocus, EnvDTE.Window lostFocus)
{
if (!ThreadHelper.CheckAccess())
{
- await ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync();
+ await AcuminatorVSPackage.JTF.SwitchToMainThreadAsync();
}
if (!_codeMapViewModel.IsVisible || Equals(gotFocus, lostFocus) || gotFocus.Document == null)
@@ -197,7 +202,7 @@ private async Task RefreshCodeMapAsync(IWpfTextView? activeWpfTextView = null, D
if (!ThreadHelper.CheckAccess())
{
- await ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync();
+ await AcuminatorVSPackage.JTF.SwitchToMainThreadAsync();
}
var activeWpfTextViewTask = activeWpfTextView != null
diff --git a/src/Acuminator/Acuminator.Vsix/Tool Windows/CodeMap/ViewModel/CodeMapWindowViewModel.cs b/src/Acuminator/Acuminator.Vsix/Tool Windows/CodeMap/ViewModel/CodeMapWindowViewModel.cs
index fc34737b4..3f20d3db8 100644
--- a/src/Acuminator/Acuminator.Vsix/Tool Windows/CodeMap/ViewModel/CodeMapWindowViewModel.cs
+++ b/src/Acuminator/Acuminator.Vsix/Tool Windows/CodeMap/ViewModel/CodeMapWindowViewModel.cs
@@ -169,7 +169,13 @@ private CodeMapWindowViewModel(Workspace workspace)
FilterVM = new FilterViewModel();
FilterVM.FilterChanged += FilterVM_FilterChanged;
- RefreshCodeMapCommand = new Command(p => RefreshCodeMapAsync().Forget());
+ RefreshCodeMapCommand =
+ new Command(p =>
+ {
+ var refreshCodeMapAction = () => RefreshCodeMapAsync();
+ refreshCodeMapAction.FileAndForgetAcuminatorTask($"vs/{AcuminatorVSPackage.PackageName}/" +
+ $"{nameof(CodeMapWindowViewModel)}/{nameof(RefreshCodeMapAsync)}");
+ });
ExpandOrCollapseAllCommand = new Command(p => ExpandOrCollapseNodeDescendants(p as TreeNodeViewModel));
SortNodeChildrenByNameAscendingCommand =
@@ -209,7 +215,11 @@ public static CodeMapWindowViewModel InitCodeMap(Workspace workspace, IWpfTextVi
}
if (codeMapViewModel.DocumentModel != null)
- codeMapViewModel.BuildCodeMapAsync().Forget();
+ {
+ var buildCodeMapAction = () => codeMapViewModel.BuildCodeMapAsync();
+ buildCodeMapAction.FileAndForgetAcuminatorTask($"vs/{AcuminatorVSPackage.PackageName}/" +
+ $"{nameof(CodeMapWindowViewModel)}/{nameof(BuildCodeMapAsync)}");
+ }
return codeMapViewModel;
}
@@ -248,7 +258,7 @@ internal async Task RefreshCodeMapOnWindowOpeningAsync(IWpfTextView? activeWpfTe
{
if (!ThreadHelper.CheckAccess())
{
- await ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync();
+ await AcuminatorVSPackage.JTF.SwitchToMainThreadAsync();
}
IsCalculating = false;
@@ -263,7 +273,7 @@ private async Task RefreshCodeMapAsync(IWpfTextView? activeWpfTextView = null, D
if (!ThreadHelper.CheckAccess())
{
- await ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync();
+ await AcuminatorVSPackage.JTF.SwitchToMainThreadAsync();
}
var activeWpfTextViewTask = activeWpfTextView != null
@@ -307,7 +317,7 @@ private async void OnWorkspaceChanged(object sender, WorkspaceChangeEventArgs e)
if (!ThreadHelper.CheckAccess())
{
- await ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync();
+ await AcuminatorVSPackage.JTF.SwitchToMainThreadAsync();
}
if (!IsVisible || e.IsActiveDocumentCleared(Document))
@@ -373,7 +383,8 @@ private async Task HandleWorkspaceChangesAsync(Workspace newWorkspace, Microsoft
if (recalculateCodeMapMode == CodeMapRefreshMode.Recalculate && DocumentModel?.WpfTextView != null)
{
DocumentModel = new DocumentModel(DocumentModel.WpfTextView, changedDocument);
- BuildCodeMapAsync().Forget();
+ var buildCodeMapAction = () => BuildCodeMapAsync();
+ buildCodeMapAction.FileAndForgetAcuminatorTask($"vs/{AcuminatorVSPackage.PackageName}/{nameof(CodeMapWindowViewModel)}/{nameof(BuildCodeMapAsync)}");
}
}
@@ -397,7 +408,7 @@ private async Task BuildCodeMapAsync()
if (!ThreadHelper.CheckAccess())
{
- await ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync();
+ await AcuminatorVSPackage.JTF.SwitchToMainThreadAsync();
}
IsCalculating = true;
@@ -415,7 +426,7 @@ private async Task BuildCodeMapAsync()
if (newTreeVM == null)
return;
- await ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync();
+ await AcuminatorVSPackage.JTF.SwitchToMainThreadAsync();
Tree = newTreeVM;
AfterCodeMapTreeIsFiltered?.Invoke(this, new FilterEventArgs(filterOptions, oldFilterText: null));
diff --git a/src/Acuminator/Acuminator.Vsix/Tool Windows/OpenToolWindowCommandBase.cs b/src/Acuminator/Acuminator.Vsix/Tool Windows/OpenToolWindowCommandBase.cs
index 8a93e2f04..c3a215495 100644
--- a/src/Acuminator/Acuminator.Vsix/Tool Windows/OpenToolWindowCommandBase.cs
+++ b/src/Acuminator/Acuminator.Vsix/Tool Windows/OpenToolWindowCommandBase.cs
@@ -25,13 +25,15 @@ protected OpenToolWindowCommandBase(AsyncPackage package, OleMenuCommandService
///
/// The event sender.
/// The event args.
- protected override void CommandCallback(object sender, EventArgs e) =>
- OpenToolWindowAsync()
- .FileAndForget($"vs/{AcuminatorVSPackage.PackageName}/{nameof(OpenToolWindowAsync)}/{typeof(TWindow).Name}");
+ protected override void CommandCallback(object sender, EventArgs e)
+ {
+ var openToolWindowAction = () => OpenToolWindowAsync();
+ openToolWindowAction.FileAndForgetAcuminatorTask($"vs/{AcuminatorVSPackage.PackageName}/{nameof(OpenToolWindowAsync)}/{typeof(TWindow).Name}");
+ }
protected virtual async Task OpenToolWindowAsync()
{
- await ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync();
+ await AcuminatorVSPackage.JTF.SwitchToMainThreadAsync();
// Get the instance number 0 of this tool window. This window is single instance so this instance
// is actually the only one.
diff --git a/src/Acuminator/Acuminator.Vsix/Utils/Logger/AcuminatorLogger.cs b/src/Acuminator/Acuminator.Vsix/Utils/Logger/AcuminatorLogger.cs
index 27141b289..acc7613ce 100644
--- a/src/Acuminator/Acuminator.Vsix/Utils/Logger/AcuminatorLogger.cs
+++ b/src/Acuminator/Acuminator.Vsix/Utils/Logger/AcuminatorLogger.cs
@@ -143,7 +143,7 @@ public static void LogException(Exception? exception, LogMode logMode = LogMode.
{
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(timeoutSeconds));
- var joinableTask = ThreadHelper.JoinableTaskFactory.RunAsync(() => _package.GetWpfTextViewAsync());
+ var joinableTask = AcuminatorVSPackage.JTF.RunAsync(() => _package.GetWpfTextViewAsync());
var activeTextView = joinableTask.Join(cts.Token);
return activeTextView;
}
diff --git a/src/Acuminator/Acuminator.Vsix/Utils/Navigation/VSDocumentNavigation.cs b/src/Acuminator/Acuminator.Vsix/Utils/Navigation/VSDocumentNavigation.cs
index e9ff5b772..b6a84a52f 100644
--- a/src/Acuminator/Acuminator.Vsix/Utils/Navigation/VSDocumentNavigation.cs
+++ b/src/Acuminator/Acuminator.Vsix/Utils/Navigation/VSDocumentNavigation.cs
@@ -42,7 +42,7 @@ public static class VSDocumentNavigation
string filePath = location.SourceTree.FilePath;
TextSpan textSpanToNavigate = location.SourceSpan;
- await ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync();
+ await AcuminatorVSPackage.JTF.SwitchToMainThreadAsync();
cToken.ThrowIfCancellationRequested();
@@ -75,7 +75,7 @@ public static class VSDocumentNavigation
reference.ThrowOnNull();
var filePath = reference.SyntaxTree?.FilePath;
- await ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync();
+ await AcuminatorVSPackage.JTF.SwitchToMainThreadAsync();
var workspace = await AcuminatorVSPackage.Instance.GetVSWorkspaceAsync();
TextSpan textSpanToNavigate = await GetTextSpanToNavigateFromSymbolAsync(symbol, reference, cToken);
@@ -257,7 +257,7 @@ public static async Task ExpandAllRegionsContainingSpanAsync(this IAsyncServiceP
if (!File.Exists(filePath) )
return null;
- await ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync();
+ await AcuminatorVSPackage.JTF.SwitchToMainThreadAsync();
DTE? dte = await serviceProvider.GetServiceAsync();
if (dte == null)
diff --git a/src/Acuminator/Acuminator.Vsix/Utils/Tasks/VsTasksUtils.cs b/src/Acuminator/Acuminator.Vsix/Utils/Tasks/VsTasksUtils.cs
new file mode 100644
index 000000000..b5310e87b
--- /dev/null
+++ b/src/Acuminator/Acuminator.Vsix/Utils/Tasks/VsTasksUtils.cs
@@ -0,0 +1,83 @@
+#nullable enable
+
+using System;
+using System.Linq;
+
+using Acuminator.Utilities.Common;
+
+using Microsoft.VisualStudio.Shell;
+using Microsoft.VisualStudio.Threading;
+using Microsoft.VisualStudio.Telemetry;
+using Microsoft.Internal.VisualStudio.Shell;
+
+namespace Acuminator.Vsix.Utilities;
+
+///
+/// The threading and task related utilities that use VS threading mechanisms.
+///
+public static class VsTasksUtils
+{
+ ///
+ /// A extension method that runs async method
+ /// in a correct context of JTF, files exceptions and forgets.
+ ///
+ ///
+ /// The code is based on .FileAndForget method
+ /// which provides an example of how to handle fire-and-forget async action inside void-returning event handlers
+ /// with the use of JTF and VS telemetry mechanisms.
+ ///
+ /// The reason of having a separate method instead of using the .FileAndForget method is to
+ /// be able to use from the class instead of the default one from to run the async method.
+ /// This code also supports skipping of s.
+ ///
+ /// The async method to act on.
+ /// Name of the fault event. Use the name of the component for this with the following convention:
+ /// "vs/{AcuminatorVSPackage.PackageName}/{componentName}/{methodName}".
+ /// (Optional) Information describing the fault.
+ /// (Optional) True to log cancellation exceptions. False by default.
+ /// (Optional) The optional condition on exceptions to be logged. Takes precedence over the flag.
+ public static void FileAndForgetAcuminatorTask(this Func? asyncMethod, string faultEventName, string? faultDescription = null,
+ bool logCancellations = false, Func? fileOnlyIf = null)
+ {
+ asyncMethod.ThrowOnNull();
+ JoinableTask joinableTask = AcuminatorVSPackage.JTF.RunAsync(async delegate
+ {
+ try
+ {
+ await asyncMethod();
+ }
+ catch (Exception ex)
+ {
+ if (!ShouldLogException(ex, fileOnlyIf, logCancellations))
+ return;
+
+ await ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync();
+
+ faultEventName = faultEventName.NullIfWhiteSpace()?.Trim() ?? $"vs/{AcuminatorVSPackage.PackageName}/UnknownComponent/UnknownMethod";
+ var telemetryEvent = new FaultEvent(faultEventName, faultDescription, ex)
+ {
+ IsIncludedInWatsonSample = false
+ };
+
+ TelemetryHelper.DataModelTelemetrySession?.PostEvent(telemetryEvent);
+ faultDescription = faultDescription.NullIfWhiteSpace()?.Trim();
+ string text = faultDescription != null
+ ? faultDescription + Environment.NewLine
+ : string.Empty;
+ text += ex;
+
+ ActivityLog.TryLogError(faultEventName, text);
+ }
+ });
+ }
+
+ private static bool ShouldLogException(Exception exception, Func? fileOnlyIf, bool logCancellations)
+ {
+ if (fileOnlyIf != null)
+ return fileOnlyIf(exception);
+ else if (exception is OperationCanceledException)
+ return logCancellations;
+ else
+ return true;
+ }
+}
\ No newline at end of file
diff --git a/src/Acuminator/Acuminator.Vsix/Utils/VSServicesExtensions.cs b/src/Acuminator/Acuminator.Vsix/Utils/VSServicesExtensions.cs
index c96a510d7..6697115e5 100644
--- a/src/Acuminator/Acuminator.Vsix/Utils/VSServicesExtensions.cs
+++ b/src/Acuminator/Acuminator.Vsix/Utils/VSServicesExtensions.cs
@@ -38,7 +38,7 @@ internal static class VSServicesExtensions
return serviceProvider?.GetService(typeof(TService)) as TService;
}
- public static async Task GetServiceAsync(this IAsyncServiceProvider serviceProvider)
+ public static async Task GetServiceAsync(this IAsyncServiceProvider? serviceProvider)
where TService : class
{
if (serviceProvider == null)
@@ -48,7 +48,7 @@ internal static class VSServicesExtensions
return service as TService;
}
- internal static async Task GetVSWorkspaceAsync(this IAsyncServiceProvider serviceProvider)
+ internal static async Task GetVSWorkspaceAsync(this IAsyncServiceProvider? serviceProvider)
{
if (serviceProvider == null)
return null;
@@ -58,7 +58,7 @@ internal static class VSServicesExtensions
return componentModel?.GetService();
}
- internal static async Task GetSolutionPathAsync(this IAsyncServiceProvider serviceProvider)
+ internal static async Task GetSolutionPathAsync(this IAsyncServiceProvider? serviceProvider)
{
if (serviceProvider == null)
return null;
@@ -67,7 +67,7 @@ internal static class VSServicesExtensions
return workspace?.CurrentSolution?.FilePath ?? string.Empty;
}
- internal static async Task GetOutliningManagerAsync(this IAsyncServiceProvider serviceProvider, ITextView textView)
+ internal static async Task GetOutliningManagerAsync(this IAsyncServiceProvider? serviceProvider, ITextView? textView)
{
if (serviceProvider == null || textView == null)
return null;
@@ -81,7 +81,7 @@ internal static class VSServicesExtensions
return outliningManagerService.GetOutliningManager(textView);
}
- internal static async Task GetWpfTextViewAsync(this IAsyncServiceProvider serviceProvider)
+ internal static async Task GetWpfTextViewAsync(this IAsyncServiceProvider? serviceProvider)
{
if (serviceProvider == null)
return null;
@@ -109,7 +109,7 @@ internal static class VSServicesExtensions
if (!ThreadHelper.CheckAccess())
{
- await ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync();
+ await AcuminatorVSPackage.JTF.SwitchToMainThreadAsync();
}
DTE2? dte2 = await serviceProvider.GetServiceAsync(throwOnFailure: false);
@@ -149,7 +149,7 @@ internal static class VSServicesExtensions
if (serviceProvider == null)
return null;
- await ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync();
+ await AcuminatorVSPackage.JTF.SwitchToMainThreadAsync();
var errorService = await serviceProvider.GetServiceAsync(throwOnFailure: false);
if (errorService == null)
diff --git a/src/Acuminator/Acuminator.Vsix/Utils/Version/VSVersionProvider.cs b/src/Acuminator/Acuminator.Vsix/Utils/Version/VSVersionProvider.cs
index a949c885c..d1b1344db 100644
--- a/src/Acuminator/Acuminator.Vsix/Utils/Version/VSVersionProvider.cs
+++ b/src/Acuminator/Acuminator.Vsix/Utils/Version/VSVersionProvider.cs
@@ -27,7 +27,7 @@ public static async Task GetVersionAsync(IAsyncServiceProvider servic
if (!ThreadHelper.CheckAccess())
{
- await ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync();
+ await AcuminatorVSPackage.JTF.SwitchToMainThreadAsync();
}
Version? shellVersion = await VS.Shell.GetVsVersionAsync();
diff --git a/src/Acuminator/Acuminator.Vsix/source.extension.vsixmanifest b/src/Acuminator/Acuminator.Vsix/source.extension.vsixmanifest
index 8b7ca1d8b..36c00a1f5 100644
--- a/src/Acuminator/Acuminator.Vsix/source.extension.vsixmanifest
+++ b/src/Acuminator/Acuminator.Vsix/source.extension.vsixmanifest
@@ -1,7 +1,7 @@
-
+
Acuminator
Acuminator is a Visual Studio extension that simplifies development with Acumatica Framework. Acuminator provides the following functionality to boost developer productivity:
- Static code analysis diagnostics, code fixes, and refactorings