From 2d9f0fcab759802de9c6104b8ff26411cd4aaf3a Mon Sep 17 00:00:00 2001 From: Sergey Nikomarov Date: Tue, 8 Sep 2026 02:06:16 +0200 Subject: [PATCH 01/17] ATR-975: added check of tagger type --- .../Coloriser/Outlining/PXOutliningTaggerProvider.cs | 2 +- .../Acuminator.Vsix/Coloriser/PXColorizerTaggerProvider.cs | 5 ++++- 2 files changed, 5 insertions(+), 2 deletions(-) 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), () => { From 3ee33ee77485fbaa28c4acfe0b98493184434691 Mon Sep 17 00:00:00 2001 From: Sergey Nikomarov Date: Tue, 8 Sep 2026 12:39:35 +0200 Subject: [PATCH 02/17] ATR-975: resolved deadlock that appeared due to the blocking of the main thread --- .../Coloriser/PXRoslynColorizerTagger.cs | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/src/Acuminator/Acuminator.Vsix/Coloriser/PXRoslynColorizerTagger.cs b/src/Acuminator/Acuminator.Vsix/Coloriser/PXRoslynColorizerTagger.cs index 9adf80168..505691f77 100644 --- a/src/Acuminator/Acuminator.Vsix/Coloriser/PXRoslynColorizerTagger.cs +++ b/src/Acuminator/Acuminator.Vsix/Coloriser/PXRoslynColorizerTagger.cs @@ -17,6 +17,8 @@ using ThreadHelper = Microsoft.VisualStudio.Shell.ThreadHelper; +using static Microsoft.VisualStudio.Shell.TaskExtensions; + namespace Acuminator.Vsix.Coloriser; /// @@ -282,7 +284,12 @@ private void WorkspaceAttachedToDocumentChanged(object sender, DocumentWorkspace if (ThreadHelper.CheckAccess()) RaiseTagsChanged(); else - ThreadHelper.JoinableTaskFactory.Run(RaiseTagsChangedAsync); + { + #pragma warning disable VSSDK007 // ThreadHelper.JoinableTaskFactory.RunAsync + ThreadHelper.JoinableTaskFactory.RunAsync(RaiseTagsChangedAsync) + .FireAndForget(); + #pragma warning restore VSSDK007 + } } private void OnWorkspaceChanged(object sender, WorkspaceChangeEventArgs e) @@ -333,8 +340,13 @@ private void OnWorkspaceChanged(object sender, WorkspaceChangeEventArgs e) if (ThreadHelper.CheckAccess()) RaiseTagsChanged(); else - ThreadHelper.JoinableTaskFactory.Run(RaiseTagsChangedAsync); - } + { +#pragma warning disable VSSDK007 // ThreadHelper.JoinableTaskFactory.RunAsync + ThreadHelper.JoinableTaskFactory.RunAsync(RaiseTagsChangedAsync) + .FireAndForget(); +#pragma warning restore VSSDK007 + } + } } private bool GetAcumaticaReferenceOnProjectChange(WorkspaceChangeEventArgs e, bool oldHasReferenceToAcumaticaPlatform) From b88dafa9e5e77fc3f0ce1c0b04df4b8f524462ee Mon Sep 17 00:00:00 2001 From: Sergey Nikomarov Date: Tue, 8 Sep 2026 13:16:41 +0200 Subject: [PATCH 03/17] ATR-975: replaced FireAndForget that writest to the debug output pane with FileAndForget that writes to the ActivityLog --- .../Acuminator.Vsix/Coloriser/PXRoslynColorizerTagger.cs | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/Acuminator/Acuminator.Vsix/Coloriser/PXRoslynColorizerTagger.cs b/src/Acuminator/Acuminator.Vsix/Coloriser/PXRoslynColorizerTagger.cs index 505691f77..3e0ab99be 100644 --- a/src/Acuminator/Acuminator.Vsix/Coloriser/PXRoslynColorizerTagger.cs +++ b/src/Acuminator/Acuminator.Vsix/Coloriser/PXRoslynColorizerTagger.cs @@ -15,10 +15,9 @@ using Microsoft.VisualStudio.Text; using Microsoft.VisualStudio.Text.Tagging; +using static Microsoft.VisualStudio.Shell.VsTaskLibraryHelper; using ThreadHelper = Microsoft.VisualStudio.Shell.ThreadHelper; -using static Microsoft.VisualStudio.Shell.TaskExtensions; - namespace Acuminator.Vsix.Coloriser; /// @@ -287,7 +286,7 @@ private void WorkspaceAttachedToDocumentChanged(object sender, DocumentWorkspace { #pragma warning disable VSSDK007 // ThreadHelper.JoinableTaskFactory.RunAsync ThreadHelper.JoinableTaskFactory.RunAsync(RaiseTagsChangedAsync) - .FireAndForget(); + .FileAndForget($"vs/{AcuminatorVSPackage.PackageName}/{nameof(PXRoslynColorizerTagger)}/{nameof(WorkspaceAttachedToDocumentChanged)}"); #pragma warning restore VSSDK007 } } @@ -343,7 +342,7 @@ private void OnWorkspaceChanged(object sender, WorkspaceChangeEventArgs e) { #pragma warning disable VSSDK007 // ThreadHelper.JoinableTaskFactory.RunAsync ThreadHelper.JoinableTaskFactory.RunAsync(RaiseTagsChangedAsync) - .FireAndForget(); + .FileAndForget($"vs/{AcuminatorVSPackage.PackageName}/{nameof(PXRoslynColorizerTagger)}/{nameof(OnWorkspaceChanged)}"); #pragma warning restore VSSDK007 } } From 5ce45340081d0715a98fe08ae7339528c28dadba Mon Sep 17 00:00:00 2001 From: Sergey Nikomarov Date: Tue, 8 Sep 2026 16:20:11 +0200 Subject: [PATCH 04/17] ATR-975: added the most recent guide on VS treading to references, it is recorded as an AI agent skill + extra link to the VS tasks libs description --- docs/dev/CodingGuidelines/CodingGuidelines.md | 2 ++ 1 file changed, 2 insertions(+) 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) From 383671fa220a31a257321ac00db804851cd76a77 Mon Sep 17 00:00:00 2001 From: Sergey Nikomarov Date: Tue, 8 Sep 2026 17:23:19 +0200 Subject: [PATCH 05/17] ATR-975: added property to expose JoinableTaskFactory from Acuminator package in a safe way + added FileAndForget helper methods --- .../Acuminator.Vsix/AcuminatorVSPackage.cs | 18 ++++ .../Utils/Tasks/VsTasksUtils.cs | 88 +++++++++++++++++++ 2 files changed, 106 insertions(+) create mode 100644 src/Acuminator/Acuminator.Vsix/Utils/Tasks/VsTasksUtils.cs diff --git a/src/Acuminator/Acuminator.Vsix/AcuminatorVSPackage.cs b/src/Acuminator/Acuminator.Vsix/AcuminatorVSPackage.cs index 0a337c80c..ef339913c 100644 --- a/src/Acuminator/Acuminator.Vsix/AcuminatorVSPackage.cs +++ b/src/Acuminator/Acuminator.Vsix/AcuminatorVSPackage.cs @@ -97,6 +97,24 @@ public sealed class AcuminatorVSPackage : AsyncPackage public static AcuminatorVSPackage Instance { get; private set; } = null!; + /// + /// The instance initialized for the .
+ /// If the package is not initialized yet, 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 + /// + /// According to Claude Code research, 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?.JoinableTaskFactory ?? ThreadHelper.JoinableTaskFactory; + private readonly Lazy _generalOptionsPage = new(() => Instance.GetDialogPage(typeof(GeneralOptionsPage)) as GeneralOptionsPage, isThreadSafe: true); 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..f39093548 --- /dev/null +++ b/src/Acuminator/Acuminator.Vsix/Utils/Tasks/VsTasksUtils.cs @@ -0,0 +1,88 @@ +#nullable enable + +using System; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; + +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 +{ + /// + /// The to act on. + public static void FileAndForget(this JoinableTask joinableTask, string? faultEventName, CancellationToken cancellation, + string? faultDescription = null, bool logCancellations = false, Func? fileOnlyIf = null) => + FileAndForget(joinableTask.CheckIfNull().Task, faultEventName, cancellation, faultDescription, logCancellations, fileOnlyIf); + + /// + /// A extension method that file and forget. + /// + /// + /// This code is written by example from .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 main 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 . + ///
+ /// The task 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}". + /// A token that allows processing to be cancelled. + /// (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 FileAndForget(this System.Threading.Tasks.Task task, string? faultEventName, CancellationToken cancellation, + string? faultDescription = null, bool logCancellations = false, Func? fileOnlyIf = null) + { + task.ThrowOnNull(); + JoinableTask joinableTask = AcuminatorVSPackage.JTF.RunAsync(async delegate + { + try + { +#pragma warning disable VSTHRD003 // Avoid awaiting foreign Tasks - This is already in JTF.RunAsync method, so we can safely await the task here. + await task.ConfigureAwait(continueOnCapturedContext: false); +#pragma warning restore VSTHRD003 + } + catch (Exception ex) when (FilterExceptions(ex, fileOnlyIf, logCancellations)) + { + await AcuminatorVSPackage.JTF.SwitchToMainThreadAsync(cancellation); + + FaultEvent 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 FilterExceptions(Exception exception, Func? fileOnlyIf, bool logCancellations) + { + if (fileOnlyIf?.Invoke(exception) == true) + return true; + else if (exception is OperationCanceledException) + return logCancellations; + else + return true; + } +} \ No newline at end of file From 3f11df4b02793b990a4aede1c4bea96d5f21f9a7 Mon Sep 17 00:00:00 2001 From: Sergey Nikomarov Date: Tue, 8 Sep 2026 17:39:32 +0200 Subject: [PATCH 06/17] ATR-975: reworked taggers to use new helper and integrated into them changes in raising tags changed event async --- .../AsyncTagging/BackgroundTagging.cs | 2 +- .../Coloriser/Base/PXTaggerBase.cs | 31 +++++++++++++++++-- .../Coloriser/PXRoslynColorizerTagger.cs | 23 +++----------- ...ColorizerTagger.PXColorizerSyntaxWalker.cs | 2 +- 4 files changed, 35 insertions(+), 23 deletions(-) diff --git a/src/Acuminator/Acuminator.Vsix/Coloriser/AsyncTagging/BackgroundTagging.cs b/src/Acuminator/Acuminator.Vsix/Coloriser/AsyncTagging/BackgroundTagging.cs index 7ab7f900e..891ea787a 100644 --- a/src/Acuminator/Acuminator.Vsix/Coloriser/AsyncTagging/BackgroundTagging.cs +++ b/src/Acuminator/Acuminator.Vsix/Coloriser/AsyncTagging/BackgroundTagging.cs @@ -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(async () => await tagger.RaiseTagsChangedAsync(cancellationToken)).Task; } } } diff --git a/src/Acuminator/Acuminator.Vsix/Coloriser/Base/PXTaggerBase.cs b/src/Acuminator/Acuminator.Vsix/Coloriser/Base/PXTaggerBase.cs index ad7f57dd4..bf25d73d1 100644 --- a/src/Acuminator/Acuminator.Vsix/Coloriser/Base/PXTaggerBase.cs +++ b/src/Acuminator/Acuminator.Vsix/Coloriser/Base/PXTaggerBase.cs @@ -3,10 +3,13 @@ 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; @@ -64,11 +67,35 @@ protected virtual void ColoringSettingChangedHandler(object sender, SettingChang RaiseTagsChanged(); } - internal async Task RaiseTagsChangedAsync() + /// + /// Raises the tags changed asynchronously and do not observe the raised task. + /// + /// + /// The method is intended to be called from void-returning event handlers. + /// + /// Cancellation. + /// (Optional) The method raising the tag changed event. + protected void RaiseTagsChangedAsyncAndForget(CancellationToken cancellation, [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 + RaiseTagsChangedAsync(cancellation) + .FileAndForget($"vs/{AcuminatorVSPackage.PackageName}/{taggerName}/{calledFrom}", cancellation); + } + } + + internal async Task RaiseTagsChangedAsync(CancellationToken cancellation) { if (!ThreadHelper.CheckAccess()) { - await ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync(); + await ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync(cancellation); } RaiseTagsChangedImpl(); diff --git a/src/Acuminator/Acuminator.Vsix/Coloriser/PXRoslynColorizerTagger.cs b/src/Acuminator/Acuminator.Vsix/Coloriser/PXRoslynColorizerTagger.cs index 3e0ab99be..1e0f82bc5 100644 --- a/src/Acuminator/Acuminator.Vsix/Coloriser/PXRoslynColorizerTagger.cs +++ b/src/Acuminator/Acuminator.Vsix/Coloriser/PXRoslynColorizerTagger.cs @@ -15,7 +15,6 @@ using Microsoft.VisualStudio.Text; using Microsoft.VisualStudio.Text.Tagging; -using static Microsoft.VisualStudio.Shell.VsTaskLibraryHelper; using ThreadHelper = Microsoft.VisualStudio.Shell.ThreadHelper; namespace Acuminator.Vsix.Coloriser; @@ -280,15 +279,8 @@ 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 - { - #pragma warning disable VSSDK007 // ThreadHelper.JoinableTaskFactory.RunAsync - ThreadHelper.JoinableTaskFactory.RunAsync(RaiseTagsChangedAsync) - .FileAndForget($"vs/{AcuminatorVSPackage.PackageName}/{nameof(PXRoslynColorizerTagger)}/{nameof(WorkspaceAttachedToDocumentChanged)}"); - #pragma warning restore VSSDK007 - } + var cancellation = BackgroundTagging?.CancellationToken ?? AcuminatorVSPackage.Instance?.DisposalToken ?? CancellationToken.None; + RaiseTagsChangedAsyncAndForget(cancellation); } private void OnWorkspaceChanged(object sender, WorkspaceChangeEventArgs e) @@ -336,15 +328,8 @@ private void OnWorkspaceChanged(object sender, WorkspaceChangeEventArgs e) { ResetCacheAndFlags(newSnapshotToCache: null); - if (ThreadHelper.CheckAccess()) - RaiseTagsChanged(); - else - { -#pragma warning disable VSSDK007 // ThreadHelper.JoinableTaskFactory.RunAsync - ThreadHelper.JoinableTaskFactory.RunAsync(RaiseTagsChangedAsync) - .FileAndForget($"vs/{AcuminatorVSPackage.PackageName}/{nameof(PXRoslynColorizerTagger)}/{nameof(OnWorkspaceChanged)}"); -#pragma warning restore VSSDK007 - } + var cancellation = BackgroundTagging?.CancellationToken ?? AcuminatorVSPackage.Instance?.DisposalToken ?? CancellationToken.None; + RaiseTagsChangedAsyncAndForget(cancellation); } } diff --git a/src/Acuminator/Acuminator.Vsix/Coloriser/Roslyn/PXRoslynColorizerTagger.PXColorizerSyntaxWalker.cs b/src/Acuminator/Acuminator.Vsix/Coloriser/Roslyn/PXRoslynColorizerTagger.PXColorizerSyntaxWalker.cs index 80906c9bc..b5005b10d 100644 --- a/src/Acuminator/Acuminator.Vsix/Coloriser/Roslyn/PXRoslynColorizerTagger.PXColorizerSyntaxWalker.cs +++ b/src/Acuminator/Acuminator.Vsix/Coloriser/Roslyn/PXRoslynColorizerTagger.PXColorizerSyntaxWalker.cs @@ -470,7 +470,7 @@ private void UpdateCodeEditorIfNecessary() { if (!cancellationToken.IsCancellationRequested) { - await _tagger.RaiseTagsChangedAsync(); + await _tagger.RaiseTagsChangedAsync(cancellationToken); } }); #pragma warning restore VSTHRD110 // Observe result of async calls From f147f8bdcd7b4b336e51598b5d3c8c00f705b0de Mon Sep 17 00:00:00 2001 From: Sergey Nikomarov Date: Tue, 8 Sep 2026 17:48:18 +0200 Subject: [PATCH 07/17] ATR-975: changed all usages of FileAndForget to use the new Acuminator's own FileAndForget helper --- .../Commands/BQL Fixer/FixBqlCommand.cs | 2 +- .../Base/SuppressDiagnosticCommandBase.cs | 2 +- .../Commands/Formatter/FormatBqlCommand.cs | 2 +- .../GoToDeclarationOrHandlerCommand.cs | 2 +- .../CodeMap/UI/CodeMapTreeControl.xaml.cs | 3 ++- ...CodeMapWindowViewModel.CodeMapDteEventsObserver.cs | 11 +++++++---- .../Tool Windows/OpenToolWindowCommandBase.cs | 3 ++- 7 files changed, 15 insertions(+), 10 deletions(-) diff --git a/src/Acuminator/Acuminator.Vsix/Commands/BQL Fixer/FixBqlCommand.cs b/src/Acuminator/Acuminator.Vsix/Commands/BQL Fixer/FixBqlCommand.cs index c241f0fc1..242474e0f 100644 --- a/src/Acuminator/Acuminator.Vsix/Commands/BQL Fixer/FixBqlCommand.cs +++ b/src/Acuminator/Acuminator.Vsix/Commands/BQL Fixer/FixBqlCommand.cs @@ -61,7 +61,7 @@ public static void Initialize(AsyncPackage package, OleMenuCommandService comman protected override void CommandCallback(object sender, EventArgs e) => CommandCallbackAsync() - .FileAndForget($"vs/{AcuminatorVSPackage.PackageName}/{nameof(FixBqlCommand)}"); + .FileAndForget($"vs/{AcuminatorVSPackage.PackageName}/{nameof(FixBqlCommand)}", cancellation: AcuminatorVSPackage.Instance?.DisposalToken ?? default); private async System.Threading.Tasks.Task CommandCallbackAsync() { diff --git a/src/Acuminator/Acuminator.Vsix/Commands/DiagnosticSuppression/Base/SuppressDiagnosticCommandBase.cs b/src/Acuminator/Acuminator.Vsix/Commands/DiagnosticSuppression/Base/SuppressDiagnosticCommandBase.cs index c154be94a..8c79ea1cb 100644 --- a/src/Acuminator/Acuminator.Vsix/Commands/DiagnosticSuppression/Base/SuppressDiagnosticCommandBase.cs +++ b/src/Acuminator/Acuminator.Vsix/Commands/DiagnosticSuppression/Base/SuppressDiagnosticCommandBase.cs @@ -34,7 +34,7 @@ protected SuppressDiagnosticCommandBase(Shell.AsyncPackage package, Shell.OleMen protected override void CommandCallback(object sender, EventArgs e) => CommandCallbackAsync() - .FileAndForget($"vs/{AcuminatorVSPackage.PackageName}/{this.GetType().Name}"); + .FileAndForget($"vs/{AcuminatorVSPackage.PackageName}/{this.GetType().Name}", cancellation: AcuminatorVSPackage.Instance?.DisposalToken ?? default); protected virtual async Task CommandCallbackAsync() { diff --git a/src/Acuminator/Acuminator.Vsix/Commands/Formatter/FormatBqlCommand.cs b/src/Acuminator/Acuminator.Vsix/Commands/Formatter/FormatBqlCommand.cs index 39a715e9a..83bfcb190 100644 --- a/src/Acuminator/Acuminator.Vsix/Commands/Formatter/FormatBqlCommand.cs +++ b/src/Acuminator/Acuminator.Vsix/Commands/Formatter/FormatBqlCommand.cs @@ -69,7 +69,7 @@ public static void Initialize(AsyncPackage package, OleMenuCommandService comman protected override void CommandCallback(object sender, EventArgs e) => CommandCallbackAsync() - .FileAndForget($"vs/{AcuminatorVSPackage.PackageName}/{nameof(FormatBqlCommand)}"); + .FileAndForget($"vs/{AcuminatorVSPackage.PackageName}/{nameof(FormatBqlCommand)}", cancellation: AcuminatorVSPackage.Instance?.DisposalToken ?? default); private async System.Threading.Tasks.Task CommandCallbackAsync() { diff --git a/src/Acuminator/Acuminator.Vsix/Commands/GoToDeclarationOrHandler/GoToDeclarationOrHandlerCommand.cs b/src/Acuminator/Acuminator.Vsix/Commands/GoToDeclarationOrHandler/GoToDeclarationOrHandlerCommand.cs index 9d934e64b..916e4e87f 100644 --- a/src/Acuminator/Acuminator.Vsix/Commands/GoToDeclarationOrHandler/GoToDeclarationOrHandlerCommand.cs +++ b/src/Acuminator/Acuminator.Vsix/Commands/GoToDeclarationOrHandler/GoToDeclarationOrHandlerCommand.cs @@ -82,7 +82,7 @@ internal static void Initialize(Shell.AsyncPackage package, Shell.OleMenuCommand protected override void CommandCallback(object sender, EventArgs e) => CommandCallbackAsync() - .FileAndForget($"vs/{AcuminatorVSPackage.PackageName}/{nameof(GoToDeclarationOrHandlerCommand)}"); + .FileAndForget($"vs/{AcuminatorVSPackage.PackageName}/{nameof(GoToDeclarationOrHandlerCommand)}", cancellation: AcuminatorVSPackage.Instance?.DisposalToken ?? default); private async Task CommandCallbackAsync() { 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..584840eb8 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) { + var cancellation = treeNodeVM.Tree.CodeMapViewModel.CancellationToken ?? AcuminatorVSPackage.Instance?.DisposalToken ?? default; NavigateOnClickAsync(treeNodeVM) - .FileAndForget($"vs/{AcuminatorVSPackage.PackageName}/{nameof(CodeMapWindowViewModel)}/{nameof(TreeNode_PreviewMouseLeftButtonDown)}"); + .FileAndForget($"vs/{AcuminatorVSPackage.PackageName}/{nameof(CodeMapWindowViewModel)}/{nameof(TreeNode_PreviewMouseLeftButtonDown)}", cancellation); } } 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..c39415b72 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 { + var cancellation = _codeMapViewModel.CancellationToken ?? AcuminatorVSPackage.Instance?.DisposalToken ?? default; RefreshCodeMapAsync() - .FileAndForget($"vs/{AcuminatorVSPackage.PackageName}/{nameof(CodeMapWindowViewModel)}/{nameof(SetVisibilityForCodeMapWindow)}"); + .FileAndForget($"vs/{AcuminatorVSPackage.PackageName}/{nameof(CodeMapWindowViewModel)}/{nameof(SetVisibilityForCodeMapWindow)}", cancellation); } } else if (IsSwitchingToAnotherDocumentWhileCodeMapIsEmpty()) - { + { + var cancellation = _codeMapViewModel.CancellationToken ?? AcuminatorVSPackage.Instance?.DisposalToken ?? default; RefreshCodeMapAsync() - .FileAndForget($"vs/{AcuminatorVSPackage.PackageName}/{nameof(CodeMapWindowViewModel)}/{nameof(SetVisibilityForCodeMapWindow)}"); + .FileAndForget($"vs/{AcuminatorVSPackage.PackageName}/{nameof(CodeMapWindowViewModel)}/{nameof(SetVisibilityForCodeMapWindow)}", cancellation); } //-------------------------------------------Local Function---------------------------------------------------------------------------------------- @@ -164,7 +166,8 @@ private void SolutionEvents_AfterClosing() private void WindowEvents_WindowActivated(EnvDTE.Window gotFocus, EnvDTE.Window lostFocus) => WindowEventsWindowActivatedAsync(gotFocus, lostFocus) - .FileAndForget($"vs/{AcuminatorVSPackage.PackageName}/{nameof(CodeMapWindowViewModel)}/{nameof(WindowEvents_WindowActivated)}"); + .FileAndForget($"vs/{AcuminatorVSPackage.PackageName}/{nameof(CodeMapWindowViewModel)}/{nameof(WindowEvents_WindowActivated)}", + cancellation: _codeMapViewModel.CancellationToken ?? AcuminatorVSPackage.Instance?.DisposalToken ?? default); private async Task WindowEventsWindowActivatedAsync(EnvDTE.Window gotFocus, EnvDTE.Window lostFocus) { diff --git a/src/Acuminator/Acuminator.Vsix/Tool Windows/OpenToolWindowCommandBase.cs b/src/Acuminator/Acuminator.Vsix/Tool Windows/OpenToolWindowCommandBase.cs index 8a93e2f04..0fe79e7e4 100644 --- a/src/Acuminator/Acuminator.Vsix/Tool Windows/OpenToolWindowCommandBase.cs +++ b/src/Acuminator/Acuminator.Vsix/Tool Windows/OpenToolWindowCommandBase.cs @@ -27,7 +27,8 @@ protected OpenToolWindowCommandBase(AsyncPackage package, OleMenuCommandService /// The event args. protected override void CommandCallback(object sender, EventArgs e) => OpenToolWindowAsync() - .FileAndForget($"vs/{AcuminatorVSPackage.PackageName}/{nameof(OpenToolWindowAsync)}/{typeof(TWindow).Name}"); + .FileAndForget($"vs/{AcuminatorVSPackage.PackageName}/{nameof(OpenToolWindowAsync)}/{typeof(TWindow).Name}", + cancellation: AcuminatorVSPackage.Instance?.DisposalToken ?? default); protected virtual async Task OpenToolWindowAsync() { From 777a86439388939503aa90bfcac2bd7581369ea0 Mon Sep 17 00:00:00 2001 From: Sergey Nikomarov Date: Tue, 8 Sep 2026 17:59:41 +0200 Subject: [PATCH 08/17] ATR-975: enhanced nullable annotation --- .../Acuminator.Vsix/Utils/VSServicesExtensions.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Acuminator/Acuminator.Vsix/Utils/VSServicesExtensions.cs b/src/Acuminator/Acuminator.Vsix/Utils/VSServicesExtensions.cs index c96a510d7..bc060c136 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; From 743730c08fd169d761cecd08a0ecab7e20ab542e Mon Sep 17 00:00:00 2001 From: Sergey Nikomarov Date: Tue, 8 Sep 2026 18:02:48 +0200 Subject: [PATCH 09/17] ATR-975: fixed mistype in method's name --- .../Base/SuppressDiagnosticCommandBase.cs | 4 ++-- .../SuppressDiagnosticInSuppressionFileCommand.cs | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Acuminator/Acuminator.Vsix/Commands/DiagnosticSuppression/Base/SuppressDiagnosticCommandBase.cs b/src/Acuminator/Acuminator.Vsix/Commands/DiagnosticSuppression/Base/SuppressDiagnosticCommandBase.cs index 8c79ea1cb..340d54be9 100644 --- a/src/Acuminator/Acuminator.Vsix/Commands/DiagnosticSuppression/Base/SuppressDiagnosticCommandBase.cs +++ b/src/Acuminator/Acuminator.Vsix/Commands/DiagnosticSuppression/Base/SuppressDiagnosticCommandBase.cs @@ -141,14 +141,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, + protected abstract Task SuppressMultipleDiagnosticOnNodeAsync(List diagnosticData, Document document, SyntaxNode syntaxRoot, SemanticModel semanticModel, SyntaxNode nodeWithDiagnostic); } } 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..4e633615e 100644 --- a/src/Acuminator/Acuminator.Vsix/Commands/DiagnosticSuppression/Suppression File/SuppressDiagnosticInSuppressionFileCommand.cs +++ b/src/Acuminator/Acuminator.Vsix/Commands/DiagnosticSuppression/Suppression File/SuppressDiagnosticInSuppressionFileCommand.cs @@ -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); From f3deab9830b3f04dcf3ab6e2ee5c652d3d48cd8c Mon Sep 17 00:00:00 2001 From: Sergey Nikomarov Date: Tue, 8 Sep 2026 18:15:21 +0200 Subject: [PATCH 10/17] ATR-975: replaced all usages of ThreadHelper.JTF with AcuminatorVSPackage.JTF --- .../Acuminator.Vsix/Coloriser/Base/PXTaggerBase.cs | 2 +- .../PXRoslynColorizerTagger.PXColorizerSyntaxWalker.cs | 2 +- .../Commands/BQL Fixer/FixBqlCommand.cs | 8 ++++---- .../Base/SuppressDiagnosticCommandBase.cs | 4 ++-- .../BuildAction/VsixBuildActionSetterVS2019.cs | 8 ++++---- .../BuildAction/VsixBuildActionSetterVS2022.cs | 4 ++-- .../SuppressDiagnosticInSuppressionFileCommand.cs | 2 +- .../Commands/Formatter/FormatBqlCommand.cs | 4 ++-- .../Tool Windows/CodeMap/CodeMapWindow.cs | 2 +- .../CodeMapWindowViewModel.CodeMapDteEventsObserver.cs | 4 ++-- .../CodeMap/ViewModel/CodeMapWindowViewModel.cs | 10 +++++----- .../Tool Windows/OpenToolWindowCommandBase.cs | 2 +- .../Acuminator.Vsix/Utils/Logger/AcuminatorLogger.cs | 2 +- .../Utils/Navigation/VSDocumentNavigation.cs | 6 +++--- .../Acuminator.Vsix/Utils/Tasks/VsTasksUtils.cs | 2 +- .../Acuminator.Vsix/Utils/VSServicesExtensions.cs | 6 +++--- .../Acuminator.Vsix/Utils/Version/VSVersionProvider.cs | 2 +- 17 files changed, 35 insertions(+), 35 deletions(-) diff --git a/src/Acuminator/Acuminator.Vsix/Coloriser/Base/PXTaggerBase.cs b/src/Acuminator/Acuminator.Vsix/Coloriser/Base/PXTaggerBase.cs index bf25d73d1..8a1a7c799 100644 --- a/src/Acuminator/Acuminator.Vsix/Coloriser/Base/PXTaggerBase.cs +++ b/src/Acuminator/Acuminator.Vsix/Coloriser/Base/PXTaggerBase.cs @@ -95,7 +95,7 @@ internal async Task RaiseTagsChangedAsync(CancellationToken cancellation) { if (!ThreadHelper.CheckAccess()) { - await ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync(cancellation); + await AcuminatorVSPackage.JTF.SwitchToMainThreadAsync(); } RaiseTagsChangedImpl(); diff --git a/src/Acuminator/Acuminator.Vsix/Coloriser/Roslyn/PXRoslynColorizerTagger.PXColorizerSyntaxWalker.cs b/src/Acuminator/Acuminator.Vsix/Coloriser/Roslyn/PXRoslynColorizerTagger.PXColorizerSyntaxWalker.cs index b5005b10d..21d674092 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 242474e0f..910f2921b 100644 --- a/src/Acuminator/Acuminator.Vsix/Commands/BQL Fixer/FixBqlCommand.cs +++ b/src/Acuminator/Acuminator.Vsix/Commands/BQL Fixer/FixBqlCommand.cs @@ -65,7 +65,7 @@ protected override void CommandCallback(object sender, EventArgs e) => 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 +130,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 340d54be9..6018fd984 100644 --- a/src/Acuminator/Acuminator.Vsix/Commands/DiagnosticSuppression/Base/SuppressDiagnosticCommandBase.cs +++ b/src/Acuminator/Acuminator.Vsix/Commands/DiagnosticSuppression/Base/SuppressDiagnosticCommandBase.cs @@ -104,7 +104,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) @@ -149,6 +149,6 @@ protected abstract Task SuppressSingleDiagnosticOnNodeAsync(DiagnosticData diagn SemanticModel semanticModel, SyntaxNode nodeWithDiagnostic); protected abstract Task SuppressMultipleDiagnosticOnNodeAsync(List diagnosticData, Document document, SyntaxNode syntaxRoot, - SemanticModel semanticModel, SyntaxNode nodeWithDiagnostic); + 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..dca121105 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 it 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 4e633615e..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); diff --git a/src/Acuminator/Acuminator.Vsix/Commands/Formatter/FormatBqlCommand.cs b/src/Acuminator/Acuminator.Vsix/Commands/Formatter/FormatBqlCommand.cs index 83bfcb190..4e8066cee 100644 --- a/src/Acuminator/Acuminator.Vsix/Commands/Formatter/FormatBqlCommand.cs +++ b/src/Acuminator/Acuminator.Vsix/Commands/Formatter/FormatBqlCommand.cs @@ -73,7 +73,7 @@ protected override void CommandCallback(object sender, EventArgs e) => 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 +124,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/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/ViewModel/CodeMapWindowViewModel.CodeMapDteEventsObserver.cs b/src/Acuminator/Acuminator.Vsix/Tool Windows/CodeMap/ViewModel/CodeMapWindowViewModel.CodeMapDteEventsObserver.cs index c39415b72..54270fbc4 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 @@ -173,7 +173,7 @@ private async Task WindowEventsWindowActivatedAsync(EnvDTE.Window gotFocus, EnvD { if (!ThreadHelper.CheckAccess()) { - await ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync(); + await AcuminatorVSPackage.JTF.SwitchToMainThreadAsync(); } if (!_codeMapViewModel.IsVisible || Equals(gotFocus, lostFocus) || gotFocus.Document == null) @@ -200,7 +200,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..06cd284cc 100644 --- a/src/Acuminator/Acuminator.Vsix/Tool Windows/CodeMap/ViewModel/CodeMapWindowViewModel.cs +++ b/src/Acuminator/Acuminator.Vsix/Tool Windows/CodeMap/ViewModel/CodeMapWindowViewModel.cs @@ -248,7 +248,7 @@ internal async Task RefreshCodeMapOnWindowOpeningAsync(IWpfTextView? activeWpfTe { if (!ThreadHelper.CheckAccess()) { - await ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync(); + await AcuminatorVSPackage.JTF.SwitchToMainThreadAsync(); } IsCalculating = false; @@ -263,7 +263,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 +307,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)) @@ -397,7 +397,7 @@ private async Task BuildCodeMapAsync() if (!ThreadHelper.CheckAccess()) { - await ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync(); + await AcuminatorVSPackage.JTF.SwitchToMainThreadAsync(); } IsCalculating = true; @@ -415,7 +415,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 0fe79e7e4..8aec2a3b5 100644 --- a/src/Acuminator/Acuminator.Vsix/Tool Windows/OpenToolWindowCommandBase.cs +++ b/src/Acuminator/Acuminator.Vsix/Tool Windows/OpenToolWindowCommandBase.cs @@ -32,7 +32,7 @@ protected override void CommandCallback(object sender, EventArgs e) => 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 index f39093548..bc2f1f7bf 100644 --- a/src/Acuminator/Acuminator.Vsix/Utils/Tasks/VsTasksUtils.cs +++ b/src/Acuminator/Acuminator.Vsix/Utils/Tasks/VsTasksUtils.cs @@ -57,7 +57,7 @@ public static void FileAndForget(this System.Threading.Tasks.Task task, string? } catch (Exception ex) when (FilterExceptions(ex, fileOnlyIf, logCancellations)) { - await AcuminatorVSPackage.JTF.SwitchToMainThreadAsync(cancellation); + await ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync(); FaultEvent telemetryEvent = new FaultEvent(faultEventName, faultDescription, ex) { diff --git a/src/Acuminator/Acuminator.Vsix/Utils/VSServicesExtensions.cs b/src/Acuminator/Acuminator.Vsix/Utils/VSServicesExtensions.cs index bc060c136..6697115e5 100644 --- a/src/Acuminator/Acuminator.Vsix/Utils/VSServicesExtensions.cs +++ b/src/Acuminator/Acuminator.Vsix/Utils/VSServicesExtensions.cs @@ -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(); From 8d941114c3ce67bdf4a5e7a190be93d576bafd3d Mon Sep 17 00:00:00 2001 From: Sergey Nikomarov Date: Wed, 9 Sep 2026 00:51:58 +0200 Subject: [PATCH 11/17] ATR-975: removed redundant cancellation tokens --- .../Coloriser/AsyncTagging/BackgroundTagging.cs | 2 +- .../Acuminator.Vsix/Coloriser/Base/PXTaggerBase.cs | 9 ++++----- .../Coloriser/PXRoslynColorizerTagger.cs | 8 ++------ ...RoslynColorizerTagger.PXColorizerSyntaxWalker.cs | 2 +- .../Commands/BQL Fixer/FixBqlCommand.cs | 2 +- .../Base/SuppressDiagnosticCommandBase.cs | 2 +- .../Commands/Formatter/FormatBqlCommand.cs | 2 +- .../GoToDeclarationOrHandlerCommand.cs | 2 +- .../CodeMap/UI/CodeMapTreeControl.xaml.cs | 2 +- ...deMapWindowViewModel.CodeMapDteEventsObserver.cs | 7 +++---- .../Tool Windows/OpenToolWindowCommandBase.cs | 3 +-- .../Acuminator.Vsix/Utils/Tasks/VsTasksUtils.cs | 13 ++++++------- 12 files changed, 23 insertions(+), 31 deletions(-) diff --git a/src/Acuminator/Acuminator.Vsix/Coloriser/AsyncTagging/BackgroundTagging.cs b/src/Acuminator/Acuminator.Vsix/Coloriser/AsyncTagging/BackgroundTagging.cs index 891ea787a..4db04d864 100644 --- a/src/Acuminator/Acuminator.Vsix/Coloriser/AsyncTagging/BackgroundTagging.cs +++ b/src/Acuminator/Acuminator.Vsix/Coloriser/AsyncTagging/BackgroundTagging.cs @@ -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 AcuminatorVSPackage.JTF.RunAsync(async () => await tagger.RaiseTagsChangedAsync(cancellationToken)).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 8a1a7c799..cd7cd1e2f 100644 --- a/src/Acuminator/Acuminator.Vsix/Coloriser/Base/PXTaggerBase.cs +++ b/src/Acuminator/Acuminator.Vsix/Coloriser/Base/PXTaggerBase.cs @@ -73,9 +73,8 @@ protected virtual void ColoringSettingChangedHandler(object sender, SettingChang /// /// The method is intended to be called from void-returning event handlers. /// - /// Cancellation. /// (Optional) The method raising the tag changed event. - protected void RaiseTagsChangedAsyncAndForget(CancellationToken cancellation, [CallerMemberName] string? calledFrom = null) + protected void RaiseTagsChangedAsyncAndForget([CallerMemberName] string? calledFrom = null) { if (ThreadHelper.CheckAccess()) RaiseTagsChanged(); @@ -86,12 +85,12 @@ protected void RaiseTagsChangedAsyncAndForget(CancellationToken cancellation, [C // 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 - RaiseTagsChangedAsync(cancellation) - .FileAndForget($"vs/{AcuminatorVSPackage.PackageName}/{taggerName}/{calledFrom}", cancellation); + RaiseTagsChangedAsync() + .FileAndForgetAcuminatorTask($"vs/{AcuminatorVSPackage.PackageName}/{taggerName}/{calledFrom}"); } } - internal async Task RaiseTagsChangedAsync(CancellationToken cancellation) + internal async Task RaiseTagsChangedAsync() { if (!ThreadHelper.CheckAccess()) { diff --git a/src/Acuminator/Acuminator.Vsix/Coloriser/PXRoslynColorizerTagger.cs b/src/Acuminator/Acuminator.Vsix/Coloriser/PXRoslynColorizerTagger.cs index 1e0f82bc5..80b5d885f 100644 --- a/src/Acuminator/Acuminator.Vsix/Coloriser/PXRoslynColorizerTagger.cs +++ b/src/Acuminator/Acuminator.Vsix/Coloriser/PXRoslynColorizerTagger.cs @@ -278,9 +278,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); - - var cancellation = BackgroundTagging?.CancellationToken ?? AcuminatorVSPackage.Instance?.DisposalToken ?? CancellationToken.None; - RaiseTagsChangedAsyncAndForget(cancellation); + RaiseTagsChangedAsyncAndForget(); } private void OnWorkspaceChanged(object sender, WorkspaceChangeEventArgs e) @@ -327,9 +325,7 @@ private void OnWorkspaceChanged(object sender, WorkspaceChangeEventArgs e) if (oldHasReferenceToAcumaticaPlatform != _hasReferenceToAcumaticaPlatform) { ResetCacheAndFlags(newSnapshotToCache: null); - - var cancellation = BackgroundTagging?.CancellationToken ?? AcuminatorVSPackage.Instance?.DisposalToken ?? CancellationToken.None; - RaiseTagsChangedAsyncAndForget(cancellation); + RaiseTagsChangedAsyncAndForget(); } } diff --git a/src/Acuminator/Acuminator.Vsix/Coloriser/Roslyn/PXRoslynColorizerTagger.PXColorizerSyntaxWalker.cs b/src/Acuminator/Acuminator.Vsix/Coloriser/Roslyn/PXRoslynColorizerTagger.PXColorizerSyntaxWalker.cs index 21d674092..9f62ad5a8 100644 --- a/src/Acuminator/Acuminator.Vsix/Coloriser/Roslyn/PXRoslynColorizerTagger.PXColorizerSyntaxWalker.cs +++ b/src/Acuminator/Acuminator.Vsix/Coloriser/Roslyn/PXRoslynColorizerTagger.PXColorizerSyntaxWalker.cs @@ -470,7 +470,7 @@ private void UpdateCodeEditorIfNecessary() { if (!cancellationToken.IsCancellationRequested) { - await _tagger.RaiseTagsChangedAsync(cancellationToken); + await _tagger.RaiseTagsChangedAsync(); } }); #pragma warning restore VSTHRD110 // Observe result of async calls diff --git a/src/Acuminator/Acuminator.Vsix/Commands/BQL Fixer/FixBqlCommand.cs b/src/Acuminator/Acuminator.Vsix/Commands/BQL Fixer/FixBqlCommand.cs index 910f2921b..b063154c4 100644 --- a/src/Acuminator/Acuminator.Vsix/Commands/BQL Fixer/FixBqlCommand.cs +++ b/src/Acuminator/Acuminator.Vsix/Commands/BQL Fixer/FixBqlCommand.cs @@ -61,7 +61,7 @@ public static void Initialize(AsyncPackage package, OleMenuCommandService comman protected override void CommandCallback(object sender, EventArgs e) => CommandCallbackAsync() - .FileAndForget($"vs/{AcuminatorVSPackage.PackageName}/{nameof(FixBqlCommand)}", cancellation: AcuminatorVSPackage.Instance?.DisposalToken ?? default); + .FileAndForgetAcuminatorTask($"vs/{AcuminatorVSPackage.PackageName}/{nameof(FixBqlCommand)}"); private async System.Threading.Tasks.Task CommandCallbackAsync() { diff --git a/src/Acuminator/Acuminator.Vsix/Commands/DiagnosticSuppression/Base/SuppressDiagnosticCommandBase.cs b/src/Acuminator/Acuminator.Vsix/Commands/DiagnosticSuppression/Base/SuppressDiagnosticCommandBase.cs index 6018fd984..331dffe57 100644 --- a/src/Acuminator/Acuminator.Vsix/Commands/DiagnosticSuppression/Base/SuppressDiagnosticCommandBase.cs +++ b/src/Acuminator/Acuminator.Vsix/Commands/DiagnosticSuppression/Base/SuppressDiagnosticCommandBase.cs @@ -34,7 +34,7 @@ protected SuppressDiagnosticCommandBase(Shell.AsyncPackage package, Shell.OleMen protected override void CommandCallback(object sender, EventArgs e) => CommandCallbackAsync() - .FileAndForget($"vs/{AcuminatorVSPackage.PackageName}/{this.GetType().Name}", cancellation: AcuminatorVSPackage.Instance?.DisposalToken ?? default); + .FileAndForgetAcuminatorTask($"vs/{AcuminatorVSPackage.PackageName}/{this.GetType().Name}"); protected virtual async Task CommandCallbackAsync() { diff --git a/src/Acuminator/Acuminator.Vsix/Commands/Formatter/FormatBqlCommand.cs b/src/Acuminator/Acuminator.Vsix/Commands/Formatter/FormatBqlCommand.cs index 4e8066cee..cede58b53 100644 --- a/src/Acuminator/Acuminator.Vsix/Commands/Formatter/FormatBqlCommand.cs +++ b/src/Acuminator/Acuminator.Vsix/Commands/Formatter/FormatBqlCommand.cs @@ -69,7 +69,7 @@ public static void Initialize(AsyncPackage package, OleMenuCommandService comman protected override void CommandCallback(object sender, EventArgs e) => CommandCallbackAsync() - .FileAndForget($"vs/{AcuminatorVSPackage.PackageName}/{nameof(FormatBqlCommand)}", cancellation: AcuminatorVSPackage.Instance?.DisposalToken ?? default); + .FileAndForgetAcuminatorTask($"vs/{AcuminatorVSPackage.PackageName}/{nameof(FormatBqlCommand)}"); private async System.Threading.Tasks.Task CommandCallbackAsync() { diff --git a/src/Acuminator/Acuminator.Vsix/Commands/GoToDeclarationOrHandler/GoToDeclarationOrHandlerCommand.cs b/src/Acuminator/Acuminator.Vsix/Commands/GoToDeclarationOrHandler/GoToDeclarationOrHandlerCommand.cs index 916e4e87f..8be01f68a 100644 --- a/src/Acuminator/Acuminator.Vsix/Commands/GoToDeclarationOrHandler/GoToDeclarationOrHandlerCommand.cs +++ b/src/Acuminator/Acuminator.Vsix/Commands/GoToDeclarationOrHandler/GoToDeclarationOrHandlerCommand.cs @@ -82,7 +82,7 @@ internal static void Initialize(Shell.AsyncPackage package, Shell.OleMenuCommand protected override void CommandCallback(object sender, EventArgs e) => CommandCallbackAsync() - .FileAndForget($"vs/{AcuminatorVSPackage.PackageName}/{nameof(GoToDeclarationOrHandlerCommand)}", cancellation: AcuminatorVSPackage.Instance?.DisposalToken ?? default); + .FileAndForgetAcuminatorTask($"vs/{AcuminatorVSPackage.PackageName}/{nameof(GoToDeclarationOrHandlerCommand)}"); private async Task CommandCallbackAsync() { 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 584840eb8..942b8b31e 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 @@ -41,7 +41,7 @@ private void TreeNode_PreviewMouseLeftButtonDown(object sender, MouseButtonEvent { var cancellation = treeNodeVM.Tree.CodeMapViewModel.CancellationToken ?? AcuminatorVSPackage.Instance?.DisposalToken ?? default; NavigateOnClickAsync(treeNodeVM) - .FileAndForget($"vs/{AcuminatorVSPackage.PackageName}/{nameof(CodeMapWindowViewModel)}/{nameof(TreeNode_PreviewMouseLeftButtonDown)}", cancellation); + .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 54270fbc4..d46187cd1 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 @@ -139,14 +139,14 @@ private void SetVisibilityForCodeMapWindow(EnvDTE.Window window, bool windowIsVi { var cancellation = _codeMapViewModel.CancellationToken ?? AcuminatorVSPackage.Instance?.DisposalToken ?? default; RefreshCodeMapAsync() - .FileAndForget($"vs/{AcuminatorVSPackage.PackageName}/{nameof(CodeMapWindowViewModel)}/{nameof(SetVisibilityForCodeMapWindow)}", cancellation); + .FileAndForgetAcuminatorTask($"vs/{AcuminatorVSPackage.PackageName}/{nameof(CodeMapWindowViewModel)}/{nameof(SetVisibilityForCodeMapWindow)}"); } } else if (IsSwitchingToAnotherDocumentWhileCodeMapIsEmpty()) { var cancellation = _codeMapViewModel.CancellationToken ?? AcuminatorVSPackage.Instance?.DisposalToken ?? default; RefreshCodeMapAsync() - .FileAndForget($"vs/{AcuminatorVSPackage.PackageName}/{nameof(CodeMapWindowViewModel)}/{nameof(SetVisibilityForCodeMapWindow)}", cancellation); + .FileAndForgetAcuminatorTask($"vs/{AcuminatorVSPackage.PackageName}/{nameof(CodeMapWindowViewModel)}/{nameof(SetVisibilityForCodeMapWindow)}"); } //-------------------------------------------Local Function---------------------------------------------------------------------------------------- @@ -166,8 +166,7 @@ private void SolutionEvents_AfterClosing() private void WindowEvents_WindowActivated(EnvDTE.Window gotFocus, EnvDTE.Window lostFocus) => WindowEventsWindowActivatedAsync(gotFocus, lostFocus) - .FileAndForget($"vs/{AcuminatorVSPackage.PackageName}/{nameof(CodeMapWindowViewModel)}/{nameof(WindowEvents_WindowActivated)}", - cancellation: _codeMapViewModel.CancellationToken ?? AcuminatorVSPackage.Instance?.DisposalToken ?? default); + .FileAndForgetAcuminatorTask($"vs/{AcuminatorVSPackage.PackageName}/{nameof(CodeMapWindowViewModel)}/{nameof(WindowEvents_WindowActivated)}"); private async Task WindowEventsWindowActivatedAsync(EnvDTE.Window gotFocus, EnvDTE.Window lostFocus) { diff --git a/src/Acuminator/Acuminator.Vsix/Tool Windows/OpenToolWindowCommandBase.cs b/src/Acuminator/Acuminator.Vsix/Tool Windows/OpenToolWindowCommandBase.cs index 8aec2a3b5..70dcb3c26 100644 --- a/src/Acuminator/Acuminator.Vsix/Tool Windows/OpenToolWindowCommandBase.cs +++ b/src/Acuminator/Acuminator.Vsix/Tool Windows/OpenToolWindowCommandBase.cs @@ -27,8 +27,7 @@ protected OpenToolWindowCommandBase(AsyncPackage package, OleMenuCommandService /// The event args. protected override void CommandCallback(object sender, EventArgs e) => OpenToolWindowAsync() - .FileAndForget($"vs/{AcuminatorVSPackage.PackageName}/{nameof(OpenToolWindowAsync)}/{typeof(TWindow).Name}", - cancellation: AcuminatorVSPackage.Instance?.DisposalToken ?? default); + .FileAndForgetAcuminatorTask($"vs/{AcuminatorVSPackage.PackageName}/{nameof(OpenToolWindowAsync)}/{typeof(TWindow).Name}"); protected virtual async Task OpenToolWindowAsync() { diff --git a/src/Acuminator/Acuminator.Vsix/Utils/Tasks/VsTasksUtils.cs b/src/Acuminator/Acuminator.Vsix/Utils/Tasks/VsTasksUtils.cs index bc2f1f7bf..97e6fb791 100644 --- a/src/Acuminator/Acuminator.Vsix/Utils/Tasks/VsTasksUtils.cs +++ b/src/Acuminator/Acuminator.Vsix/Utils/Tasks/VsTasksUtils.cs @@ -19,11 +19,11 @@ namespace Acuminator.Vsix.Utilities; ///
public static class VsTasksUtils { - /// + /// /// The to act on. - public static void FileAndForget(this JoinableTask joinableTask, string? faultEventName, CancellationToken cancellation, - string? faultDescription = null, bool logCancellations = false, Func? fileOnlyIf = null) => - FileAndForget(joinableTask.CheckIfNull().Task, faultEventName, cancellation, faultDescription, logCancellations, fileOnlyIf); + public static void FileAndForget(this JoinableTask joinableTask, string? faultEventName, string? faultDescription = null, + bool logCancellations = false, Func? fileOnlyIf = null) => + FileAndForgetAcuminatorTask(joinableTask.CheckIfNull().Task, faultEventName, faultDescription, logCancellations, fileOnlyIf); /// /// A extension method that file and forget. @@ -39,12 +39,11 @@ public static void FileAndForget(this JoinableTask joinableTask, string? faultEv /// The task 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}". - /// A token that allows processing to be cancelled. /// (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 FileAndForget(this System.Threading.Tasks.Task task, string? faultEventName, CancellationToken cancellation, - string? faultDescription = null, bool logCancellations = false, Func? fileOnlyIf = null) + public static void FileAndForgetAcuminatorTask(this System.Threading.Tasks.Task task, string? faultEventName, string? faultDescription = null, + bool logCancellations = false, Func? fileOnlyIf = null) { task.ThrowOnNull(); JoinableTask joinableTask = AcuminatorVSPackage.JTF.RunAsync(async delegate From a0e3063983c2f59e29fde32dbdb938f8aaafbeb0 Mon Sep 17 00:00:00 2001 From: Sergey Nikomarov Date: Wed, 9 Sep 2026 16:34:09 +0200 Subject: [PATCH 12/17] ATR-975: fixes from AI review remarks --- .../Acuminator.Vsix/AcuminatorVSPackage.cs | 31 ++++++++++++++++--- .../AsyncTagging/BackgroundTagging.cs | 2 +- .../Coloriser/Base/PXTaggerBase.cs | 2 +- .../VsixBuildActionSetterVS2019.cs | 2 +- .../CodeMap/UI/CodeMapTreeControl.xaml.cs | 1 - ...indowViewModel.CodeMapDteEventsObserver.cs | 2 -- .../ViewModel/CodeMapWindowViewModel.cs | 14 +++++++-- .../Utils/Tasks/VsTasksUtils.cs | 15 +++++---- 8 files changed, 50 insertions(+), 19 deletions(-) diff --git a/src/Acuminator/Acuminator.Vsix/AcuminatorVSPackage.cs b/src/Acuminator/Acuminator.Vsix/AcuminatorVSPackage.cs index ef339913c..aefbb58e3 100644 --- a/src/Acuminator/Acuminator.Vsix/AcuminatorVSPackage.cs +++ b/src/Acuminator/Acuminator.Vsix/AcuminatorVSPackage.cs @@ -91,15 +91,20 @@ 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 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 initialized yet, is returned instead. + /// 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 : @@ -107,13 +112,16 @@ public sealed class AcuminatorVSPackage : AsyncPackage /// https://github.com/VsixCommunity/Community.VisualStudio.Toolkit/issues/24 /// https://microsoft.github.io/VSSDK-Analyzers/analyzers/VSSDK007.html /// - /// According to Claude Code research, both factories are created from the same — the one bound to the VS main thread.
+ /// 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?.JoinableTaskFactory ?? ThreadHelper.JoinableTaskFactory; + 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); @@ -363,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 4db04d864..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. diff --git a/src/Acuminator/Acuminator.Vsix/Coloriser/Base/PXTaggerBase.cs b/src/Acuminator/Acuminator.Vsix/Coloriser/Base/PXTaggerBase.cs index cd7cd1e2f..2b628b59b 100644 --- a/src/Acuminator/Acuminator.Vsix/Coloriser/Base/PXTaggerBase.cs +++ b/src/Acuminator/Acuminator.Vsix/Coloriser/Base/PXTaggerBase.cs @@ -73,7 +73,7 @@ protected virtual void ColoringSettingChangedHandler(object sender, SettingChang /// /// The method is intended to be called from void-returning event handlers. /// - /// (Optional) The method raising the tag changed event. + /// (Optional) The method raising the tag changed event. protected void RaiseTagsChangedAsyncAndForget([CallerMemberName] string? calledFrom = null) { if (ThreadHelper.CheckAccess()) diff --git a/src/Acuminator/Acuminator.Vsix/Commands/DiagnosticSuppression/BuildAction/VsixBuildActionSetterVS2019.cs b/src/Acuminator/Acuminator.Vsix/Commands/DiagnosticSuppression/BuildAction/VsixBuildActionSetterVS2019.cs index dca121105..9672d4ac6 100644 --- a/src/Acuminator/Acuminator.Vsix/Commands/DiagnosticSuppression/BuildAction/VsixBuildActionSetterVS2019.cs +++ b/src/Acuminator/Acuminator.Vsix/Commands/DiagnosticSuppression/BuildAction/VsixBuildActionSetterVS2019.cs @@ -30,7 +30,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 JoinableTaskFactory from AcuminatorVSPackage.JTF + // 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 } 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 942b8b31e..8c04d1206 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,7 +39,6 @@ private void TreeNode_PreviewMouseLeftButtonDown(object sender, MouseButtonEvent if (e.ClickCount >= 2) { - var cancellation = treeNodeVM.Tree.CodeMapViewModel.CancellationToken ?? AcuminatorVSPackage.Instance?.DisposalToken ?? default; NavigateOnClickAsync(treeNodeVM) .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 d46187cd1..78c897427 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,12 @@ private void SetVisibilityForCodeMapWindow(EnvDTE.Window window, bool windowIsVi if (!wasVisible && _codeMapViewModel.IsVisible) //Handle the case when WindowShowing event happens after WindowActivated event { - var cancellation = _codeMapViewModel.CancellationToken ?? AcuminatorVSPackage.Instance?.DisposalToken ?? default; RefreshCodeMapAsync() .FileAndForgetAcuminatorTask($"vs/{AcuminatorVSPackage.PackageName}/{nameof(CodeMapWindowViewModel)}/{nameof(SetVisibilityForCodeMapWindow)}"); } } else if (IsSwitchingToAnotherDocumentWhileCodeMapIsEmpty()) { - var cancellation = _codeMapViewModel.CancellationToken ?? AcuminatorVSPackage.Instance?.DisposalToken ?? default; RefreshCodeMapAsync() .FileAndForgetAcuminatorTask($"vs/{AcuminatorVSPackage.PackageName}/{nameof(CodeMapWindowViewModel)}/{nameof(SetVisibilityForCodeMapWindow)}"); } 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 06cd284cc..894075374 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,10 @@ private CodeMapWindowViewModel(Workspace workspace) FilterVM = new FilterViewModel(); FilterVM.FilterChanged += FilterVM_FilterChanged; - RefreshCodeMapCommand = new Command(p => RefreshCodeMapAsync().Forget()); + RefreshCodeMapCommand = + new Command(p => RefreshCodeMapAsync() + .FileAndForgetAcuminatorTask($"vs/{AcuminatorVSPackage.PackageName}/" + + $"{nameof(CodeMapWindowViewModel)}/{nameof(RefreshCodeMapAsync)}")); ExpandOrCollapseAllCommand = new Command(p => ExpandOrCollapseNodeDescendants(p as TreeNodeViewModel)); SortNodeChildrenByNameAscendingCommand = @@ -209,7 +212,11 @@ public static CodeMapWindowViewModel InitCodeMap(Workspace workspace, IWpfTextVi } if (codeMapViewModel.DocumentModel != null) - codeMapViewModel.BuildCodeMapAsync().Forget(); + { + codeMapViewModel.BuildCodeMapAsync() + .FileAndForgetAcuminatorTask($"vs/{AcuminatorVSPackage.PackageName}/" + + $"{nameof(CodeMapWindowViewModel)}/{nameof(BuildCodeMapAsync)}"); + } return codeMapViewModel; } @@ -373,7 +380,8 @@ private async Task HandleWorkspaceChangesAsync(Workspace newWorkspace, Microsoft if (recalculateCodeMapMode == CodeMapRefreshMode.Recalculate && DocumentModel?.WpfTextView != null) { DocumentModel = new DocumentModel(DocumentModel.WpfTextView, changedDocument); - BuildCodeMapAsync().Forget(); + BuildCodeMapAsync() + .FileAndForgetAcuminatorTask($"vs/{AcuminatorVSPackage.PackageName}/{nameof(CodeMapWindowViewModel)}/{nameof(BuildCodeMapAsync)}"); } } diff --git a/src/Acuminator/Acuminator.Vsix/Utils/Tasks/VsTasksUtils.cs b/src/Acuminator/Acuminator.Vsix/Utils/Tasks/VsTasksUtils.cs index 97e6fb791..5ff044dc2 100644 --- a/src/Acuminator/Acuminator.Vsix/Utils/Tasks/VsTasksUtils.cs +++ b/src/Acuminator/Acuminator.Vsix/Utils/Tasks/VsTasksUtils.cs @@ -21,8 +21,8 @@ public static class VsTasksUtils { /// /// The to act on. - public static void FileAndForget(this JoinableTask joinableTask, string? faultEventName, string? faultDescription = null, - bool logCancellations = false, Func? fileOnlyIf = null) => + public static void FileAndForgetAcuminatorTask(this JoinableTask joinableTask, string? faultEventName, string? faultDescription = null, + bool logCancellations = false, Func? fileOnlyIf = null) => FileAndForgetAcuminatorTask(joinableTask.CheckIfNull().Task, faultEventName, faultDescription, logCancellations, fileOnlyIf); /// @@ -54,8 +54,11 @@ public static void FileAndForgetAcuminatorTask(this System.Threading.Tasks.Task await task.ConfigureAwait(continueOnCapturedContext: false); #pragma warning restore VSTHRD003 } - catch (Exception ex) when (FilterExceptions(ex, fileOnlyIf, logCancellations)) + catch (Exception ex) { + if (!ShouldLogException(ex, fileOnlyIf, logCancellations)) + return; + await ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync(); FaultEvent telemetryEvent = new FaultEvent(faultEventName, faultDescription, ex) @@ -75,10 +78,10 @@ public static void FileAndForgetAcuminatorTask(this System.Threading.Tasks.Task }); } - private static bool FilterExceptions(Exception exception, Func? fileOnlyIf, bool logCancellations) + private static bool ShouldLogException(Exception exception, Func? fileOnlyIf, bool logCancellations) { - if (fileOnlyIf?.Invoke(exception) == true) - return true; + if (fileOnlyIf != null) + return fileOnlyIf(exception); else if (exception is OperationCanceledException) return logCancellations; else From 6a33d3c51cd75c723a0049ed546b319c28c08bff Mon Sep 17 00:00:00 2001 From: Sergey Nikomarov Date: Thu, 10 Sep 2026 01:30:35 +0200 Subject: [PATCH 13/17] ATR-975: fixed VSTHRD003 warning by running async method inside the JTF RunAsync scope --- .../Coloriser/Base/PXTaggerBase.cs | 4 +-- .../Commands/BQL Fixer/FixBqlCommand.cs | 8 ++++-- .../Base/SuppressDiagnosticCommandBase.cs | 8 ++++-- .../Commands/Formatter/FormatBqlCommand.cs | 8 ++++-- .../GoToDeclarationOrHandlerCommand.cs | 10 ++++--- .../CodeMap/UI/CodeMapTreeControl.xaml.cs | 5 ++-- ...indowViewModel.CodeMapDteEventsObserver.cs | 13 ++++++--- .../ViewModel/CodeMapWindowViewModel.cs | 19 +++++++------ .../Tool Windows/OpenToolWindowCommandBase.cs | 8 ++++-- .../Utils/Tasks/VsTasksUtils.cs | 28 +++++++------------ 10 files changed, 61 insertions(+), 50 deletions(-) diff --git a/src/Acuminator/Acuminator.Vsix/Coloriser/Base/PXTaggerBase.cs b/src/Acuminator/Acuminator.Vsix/Coloriser/Base/PXTaggerBase.cs index 2b628b59b..ce21962e1 100644 --- a/src/Acuminator/Acuminator.Vsix/Coloriser/Base/PXTaggerBase.cs +++ b/src/Acuminator/Acuminator.Vsix/Coloriser/Base/PXTaggerBase.cs @@ -85,8 +85,8 @@ protected void RaiseTagsChangedAsyncAndForget([CallerMemberName] string? calledF // 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 - RaiseTagsChangedAsync() - .FileAndForgetAcuminatorTask($"vs/{AcuminatorVSPackage.PackageName}/{taggerName}/{calledFrom}"); + var raiseTaggerChanged = () => RaiseTagsChangedAsync(); + raiseTaggerChanged.FileAndForgetAcuminatorTask($"vs/{AcuminatorVSPackage.PackageName}/{taggerName}/{calledFrom}"); } } diff --git a/src/Acuminator/Acuminator.Vsix/Commands/BQL Fixer/FixBqlCommand.cs b/src/Acuminator/Acuminator.Vsix/Commands/BQL Fixer/FixBqlCommand.cs index b063154c4..6700b9d94 100644 --- a/src/Acuminator/Acuminator.Vsix/Commands/BQL Fixer/FixBqlCommand.cs +++ b/src/Acuminator/Acuminator.Vsix/Commands/BQL Fixer/FixBqlCommand.cs @@ -59,9 +59,11 @@ public static void Initialize(AsyncPackage package, OleMenuCommandService comman } } - protected override void CommandCallback(object sender, EventArgs e) => - CommandCallbackAsync() - .FileAndForgetAcuminatorTask($"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() { diff --git a/src/Acuminator/Acuminator.Vsix/Commands/DiagnosticSuppression/Base/SuppressDiagnosticCommandBase.cs b/src/Acuminator/Acuminator.Vsix/Commands/DiagnosticSuppression/Base/SuppressDiagnosticCommandBase.cs index 331dffe57..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() - .FileAndForgetAcuminatorTask($"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() { diff --git a/src/Acuminator/Acuminator.Vsix/Commands/Formatter/FormatBqlCommand.cs b/src/Acuminator/Acuminator.Vsix/Commands/Formatter/FormatBqlCommand.cs index cede58b53..8ae95f50e 100644 --- a/src/Acuminator/Acuminator.Vsix/Commands/Formatter/FormatBqlCommand.cs +++ b/src/Acuminator/Acuminator.Vsix/Commands/Formatter/FormatBqlCommand.cs @@ -67,9 +67,11 @@ public static void Initialize(AsyncPackage package, OleMenuCommandService comman } #pragma warning restore CS8774 - protected override void CommandCallback(object sender, EventArgs e) => - CommandCallbackAsync() - .FileAndForgetAcuminatorTask($"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() { diff --git a/src/Acuminator/Acuminator.Vsix/Commands/GoToDeclarationOrHandler/GoToDeclarationOrHandlerCommand.cs b/src/Acuminator/Acuminator.Vsix/Commands/GoToDeclarationOrHandler/GoToDeclarationOrHandlerCommand.cs index 8be01f68a..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() - .FileAndForgetAcuminatorTask($"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/Tool Windows/CodeMap/UI/CodeMapTreeControl.xaml.cs b/src/Acuminator/Acuminator.Vsix/Tool Windows/CodeMap/UI/CodeMapTreeControl.xaml.cs index 8c04d1206..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) - .FileAndForgetAcuminatorTask($"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 78c897427..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,13 +137,15 @@ private void SetVisibilityForCodeMapWindow(EnvDTE.Window window, bool windowIsVi if (!wasVisible && _codeMapViewModel.IsVisible) //Handle the case when WindowShowing event happens after WindowActivated event { - RefreshCodeMapAsync() + var refreshCodeMapAction = () => RefreshCodeMapAsync(); + refreshCodeMapAction .FileAndForgetAcuminatorTask($"vs/{AcuminatorVSPackage.PackageName}/{nameof(CodeMapWindowViewModel)}/{nameof(SetVisibilityForCodeMapWindow)}"); } } else if (IsSwitchingToAnotherDocumentWhileCodeMapIsEmpty()) { - RefreshCodeMapAsync() + var refreshCodeMapAction = () => RefreshCodeMapAsync(); + refreshCodeMapAction .FileAndForgetAcuminatorTask($"vs/{AcuminatorVSPackage.PackageName}/{nameof(CodeMapWindowViewModel)}/{nameof(SetVisibilityForCodeMapWindow)}"); } @@ -162,9 +164,12 @@ private void SolutionEvents_AfterClosing() _codeMapViewModel.DocumentModel = null; } - private void WindowEvents_WindowActivated(EnvDTE.Window gotFocus, EnvDTE.Window lostFocus) => - WindowEventsWindowActivatedAsync(gotFocus, lostFocus) + 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) { 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 894075374..3f20d3db8 100644 --- a/src/Acuminator/Acuminator.Vsix/Tool Windows/CodeMap/ViewModel/CodeMapWindowViewModel.cs +++ b/src/Acuminator/Acuminator.Vsix/Tool Windows/CodeMap/ViewModel/CodeMapWindowViewModel.cs @@ -170,9 +170,12 @@ private CodeMapWindowViewModel(Workspace workspace) FilterVM.FilterChanged += FilterVM_FilterChanged; RefreshCodeMapCommand = - new Command(p => RefreshCodeMapAsync() - .FileAndForgetAcuminatorTask($"vs/{AcuminatorVSPackage.PackageName}/" + - $"{nameof(CodeMapWindowViewModel)}/{nameof(RefreshCodeMapAsync)}")); + new Command(p => + { + var refreshCodeMapAction = () => RefreshCodeMapAsync(); + refreshCodeMapAction.FileAndForgetAcuminatorTask($"vs/{AcuminatorVSPackage.PackageName}/" + + $"{nameof(CodeMapWindowViewModel)}/{nameof(RefreshCodeMapAsync)}"); + }); ExpandOrCollapseAllCommand = new Command(p => ExpandOrCollapseNodeDescendants(p as TreeNodeViewModel)); SortNodeChildrenByNameAscendingCommand = @@ -213,9 +216,9 @@ public static CodeMapWindowViewModel InitCodeMap(Workspace workspace, IWpfTextVi if (codeMapViewModel.DocumentModel != null) { - codeMapViewModel.BuildCodeMapAsync() - .FileAndForgetAcuminatorTask($"vs/{AcuminatorVSPackage.PackageName}/" + - $"{nameof(CodeMapWindowViewModel)}/{nameof(BuildCodeMapAsync)}"); + var buildCodeMapAction = () => codeMapViewModel.BuildCodeMapAsync(); + buildCodeMapAction.FileAndForgetAcuminatorTask($"vs/{AcuminatorVSPackage.PackageName}/" + + $"{nameof(CodeMapWindowViewModel)}/{nameof(BuildCodeMapAsync)}"); } return codeMapViewModel; @@ -380,8 +383,8 @@ private async Task HandleWorkspaceChangesAsync(Workspace newWorkspace, Microsoft if (recalculateCodeMapMode == CodeMapRefreshMode.Recalculate && DocumentModel?.WpfTextView != null) { DocumentModel = new DocumentModel(DocumentModel.WpfTextView, changedDocument); - BuildCodeMapAsync() - .FileAndForgetAcuminatorTask($"vs/{AcuminatorVSPackage.PackageName}/{nameof(CodeMapWindowViewModel)}/{nameof(BuildCodeMapAsync)}"); + var buildCodeMapAction = () => BuildCodeMapAsync(); + buildCodeMapAction.FileAndForgetAcuminatorTask($"vs/{AcuminatorVSPackage.PackageName}/{nameof(CodeMapWindowViewModel)}/{nameof(BuildCodeMapAsync)}"); } } diff --git a/src/Acuminator/Acuminator.Vsix/Tool Windows/OpenToolWindowCommandBase.cs b/src/Acuminator/Acuminator.Vsix/Tool Windows/OpenToolWindowCommandBase.cs index 70dcb3c26..c3a215495 100644 --- a/src/Acuminator/Acuminator.Vsix/Tool Windows/OpenToolWindowCommandBase.cs +++ b/src/Acuminator/Acuminator.Vsix/Tool Windows/OpenToolWindowCommandBase.cs @@ -25,9 +25,11 @@ protected OpenToolWindowCommandBase(AsyncPackage package, OleMenuCommandService /// /// The event sender. /// The event args. - protected override void CommandCallback(object sender, EventArgs e) => - OpenToolWindowAsync() - .FileAndForgetAcuminatorTask($"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() { diff --git a/src/Acuminator/Acuminator.Vsix/Utils/Tasks/VsTasksUtils.cs b/src/Acuminator/Acuminator.Vsix/Utils/Tasks/VsTasksUtils.cs index 5ff044dc2..eaf6efff4 100644 --- a/src/Acuminator/Acuminator.Vsix/Utils/Tasks/VsTasksUtils.cs +++ b/src/Acuminator/Acuminator.Vsix/Utils/Tasks/VsTasksUtils.cs @@ -2,8 +2,6 @@ using System; using System.Linq; -using System.Threading; -using System.Threading.Tasks; using Acuminator.Utilities.Common; @@ -19,40 +17,34 @@ namespace Acuminator.Vsix.Utilities; ///
public static class VsTasksUtils { - /// - /// The to act on. - public static void FileAndForgetAcuminatorTask(this JoinableTask joinableTask, string? faultEventName, string? faultDescription = null, - bool logCancellations = false, Func? fileOnlyIf = null) => - FileAndForgetAcuminatorTask(joinableTask.CheckIfNull().Task, faultEventName, faultDescription, logCancellations, fileOnlyIf); - /// - /// A extension method that file and forget. + /// A extension method that runs async method + /// in a correct context of JTF, files exceptions and forgets. /// /// - /// This code is written by example from .FileAndForget method
+ /// 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 main 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 . + /// 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 task to act on. + /// 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 System.Threading.Tasks.Task task, string? faultEventName, string? faultDescription = null, + public static void FileAndForgetAcuminatorTask(this Func? asyncMethod, string? faultEventName, string? faultDescription = null, bool logCancellations = false, Func? fileOnlyIf = null) { - task.ThrowOnNull(); + asyncMethod.ThrowOnNull(); JoinableTask joinableTask = AcuminatorVSPackage.JTF.RunAsync(async delegate { try { -#pragma warning disable VSTHRD003 // Avoid awaiting foreign Tasks - This is already in JTF.RunAsync method, so we can safely await the task here. - await task.ConfigureAwait(continueOnCapturedContext: false); -#pragma warning restore VSTHRD003 + await asyncMethod(); } catch (Exception ex) { From e8888bd64e353f6bcb1d024e1d3c0aeade3aa2dc Mon Sep 17 00:00:00 2001 From: Sergey Nikomarov Date: Thu, 10 Sep 2026 02:37:35 +0200 Subject: [PATCH 14/17] ATR-975: updated version to 4.1.0 --- .../Acuminator.Analyzers/Acuminator.Analyzers.csproj | 2 +- .../Acuminator.Runner.NetFramework.csproj | 2 +- src/Acuminator/Acuminator.Tests/Acuminator.Tests.csproj | 4 ++-- .../Acuminator.Utilities/Acuminator.Utilities.csproj | 2 +- src/Acuminator/Acuminator.Vsix/AcuminatorVSPackage.cs | 2 +- src/Acuminator/Acuminator.Vsix/Properties/AssemblyInfo.cs | 4 ++-- src/Acuminator/Acuminator.Vsix/source.extension.vsixmanifest | 2 +- 7 files changed, 9 insertions(+), 9 deletions(-) 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 aefbb58e3..a6a61c2b9 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. 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/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 From 4d21e61ea1f76becd90c4f22457b8d54be604f0c Mon Sep 17 00:00:00 2001 From: Sergey Nikomarov Date: Thu, 10 Sep 2026 03:27:01 +0200 Subject: [PATCH 15/17] ATR-975: optimization - return only tags intersecting with the requested spans --- .../Coloriser/Base/PXTaggerBase.cs | 11 +++++++++++ .../Coloriser/Outlining/PXOutliningTagger.cs | 7 ++++--- .../Coloriser/PXRoslynColorizerTagger.cs | 16 ++++++++++------ 3 files changed, 25 insertions(+), 9 deletions(-) diff --git a/src/Acuminator/Acuminator.Vsix/Coloriser/Base/PXTaggerBase.cs b/src/Acuminator/Acuminator.Vsix/Coloriser/Base/PXTaggerBase.cs index ce21962e1..96f5d4f37 100644 --- a/src/Acuminator/Acuminator.Vsix/Coloriser/Base/PXTaggerBase.cs +++ b/src/Acuminator/Acuminator.Vsix/Coloriser/Base/PXTaggerBase.cs @@ -12,6 +12,7 @@ using Acuminator.Vsix.Utilities; using Microsoft.VisualStudio.Text; +using Microsoft.VisualStudio.Text.Tagging; using ThreadHelper = Microsoft.VisualStudio.Shell.ThreadHelper; @@ -58,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; 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/PXRoslynColorizerTagger.cs b/src/Acuminator/Acuminator.Vsix/Coloriser/PXRoslynColorizerTagger.cs index 80b5d885f..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) => From 44ce9841c677bee2066372f7c23071c4ecc27f2c Mon Sep 17 00:00:00 2001 From: Sergey Nikomarov Date: Thu, 10 Sep 2026 03:29:00 +0200 Subject: [PATCH 16/17] ATR-975: made __isDisposed volatile - AI remark about potential stale reads --- src/Acuminator/Acuminator.Vsix/AcuminatorVSPackage.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Acuminator/Acuminator.Vsix/AcuminatorVSPackage.cs b/src/Acuminator/Acuminator.Vsix/AcuminatorVSPackage.cs index a6a61c2b9..16720ca6c 100644 --- a/src/Acuminator/Acuminator.Vsix/AcuminatorVSPackage.cs +++ b/src/Acuminator/Acuminator.Vsix/AcuminatorVSPackage.cs @@ -95,7 +95,7 @@ public sealed class AcuminatorVSPackage : AsyncPackage private const int NOT_DISPOSED = 0; private const int DISPOSED = 1; - private int _isDisposed = NOT_DISPOSED; + private volatile int _isDisposed = NOT_DISPOSED; private OutOfProcessSettingsUpdater? _outOfProcessSettingsUpdater; From 998b457e4e32776e1a0faffa642294e12ec27d06 Mon Sep 17 00:00:00 2001 From: Sergey Nikomarov Date: Thu, 10 Sep 2026 03:37:25 +0200 Subject: [PATCH 17/17] ATR-975: added fallback for faultEventName --- src/Acuminator/Acuminator.Vsix/Utils/Tasks/VsTasksUtils.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/Acuminator/Acuminator.Vsix/Utils/Tasks/VsTasksUtils.cs b/src/Acuminator/Acuminator.Vsix/Utils/Tasks/VsTasksUtils.cs index eaf6efff4..b5310e87b 100644 --- a/src/Acuminator/Acuminator.Vsix/Utils/Tasks/VsTasksUtils.cs +++ b/src/Acuminator/Acuminator.Vsix/Utils/Tasks/VsTasksUtils.cs @@ -36,7 +36,7 @@ public static class VsTasksUtils /// (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, + public static void FileAndForgetAcuminatorTask(this Func? asyncMethod, string faultEventName, string? faultDescription = null, bool logCancellations = false, Func? fileOnlyIf = null) { asyncMethod.ThrowOnNull(); @@ -53,7 +53,8 @@ public static void FileAndForgetAcuminatorTask(this Func