From c89f6dee4f817fa83be42c9e4c3367a1f28a7e5b Mon Sep 17 00:00:00 2001 From: Sergey Nikomarov Date: Fri, 5 Jun 2026 13:46:03 +0200 Subject: [PATCH 01/15] ATR-972: reworked MEF imports to work through an importing constructor instead of an injection into an exposed field --- .../Coloriser/PXColorizerTaggerProvider.cs | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/src/Acuminator/Acuminator.Vsix/Coloriser/PXColorizerTaggerProvider.cs b/src/Acuminator/Acuminator.Vsix/Coloriser/PXColorizerTaggerProvider.cs index 2e25233b1..ed72581c1 100644 --- a/src/Acuminator/Acuminator.Vsix/Coloriser/PXColorizerTaggerProvider.cs +++ b/src/Acuminator/Acuminator.Vsix/Coloriser/PXColorizerTaggerProvider.cs @@ -28,8 +28,8 @@ namespace Acuminator.Vsix.Coloriser [Export(typeof(IViewTaggerProvider))] public class PXColorizerTaggerProvider : IViewTaggerProvider { - [Import] - internal IClassificationTypeRegistryService _classificationRegistry = null!; // Set via MEF + private readonly IClassificationTypeRegistryService _classificationRegistry; + private readonly IClassificationFormatMapService _classificationFormatMapService; [Import] internal IClassificationFormatMapService _classificationFormatMapService = null!; //Set via MEF @@ -69,6 +69,15 @@ public IClassificationType? this[int braceLevel] } } + [ImportingConstructor] + public PXColorizerTaggerProvider(IClassificationTypeRegistryService classificationRegistry, + IClassificationFormatMapService classificationFormatMapService, + ITextDocumentFactoryService textDocumentFactory) + { + _classificationRegistry = classificationRegistry; + _classificationFormatMapService = classificationFormatMapService; + } + public virtual ITagger? CreateTagger(ITextView textView, ITextBuffer textBuffer) where T : ITag { From 2ae17f1fc97c534f499ad5fed908a61d751490ad Mon Sep 17 00:00:00 2001 From: Sergey Nikomarov Date: Fri, 5 Jun 2026 14:19:47 +0200 Subject: [PATCH 02/15] ATR-972: improved synchronization in the constructor to use the same lock during the entire workspace provider creation event --- .../Coloriser/PXRoslynColorizerTagger.cs | 8 ++++-- .../Roslyn/RoslynWorkspaceProvider.cs | 25 ++++++++++++------- 2 files changed, 22 insertions(+), 11 deletions(-) diff --git a/src/Acuminator/Acuminator.Vsix/Coloriser/PXRoslynColorizerTagger.cs b/src/Acuminator/Acuminator.Vsix/Coloriser/PXRoslynColorizerTagger.cs index ff2d857cf..fd7d5e31b 100644 --- a/src/Acuminator/Acuminator.Vsix/Coloriser/PXRoslynColorizerTagger.cs +++ b/src/Acuminator/Acuminator.Vsix/Coloriser/PXRoslynColorizerTagger.cs @@ -56,10 +56,14 @@ public PXRoslynColorizerTagger(ITextBuffer buffer, PXColorizerTaggerProvider pro ClassificationTagsCache = new TagsCacheAsync(); OutliningsTagsCache = new TagsCacheAsync(); - _roslynWorkspaceProvider = new RoslynWorkspaceProvider(buffer); + // Roslyn workspace provider creation and subscription to workspace events should be done under sync lock to avoid + // unlikely race condition in the constructor. + // The lock is expected to be re-entrant, the Monitor synchronization should not be changed to another synchronization mechanism without a rework. + object workspaceLock = new object(); - lock (_roslynWorkspaceProvider.WorkspaceSubscriptionLocker) + lock (workspaceLock) { + _roslynWorkspaceProvider = RoslynWorkspaceProvider.Create(_cachedTextContainer, workspaceLock); _roslynWorkspaceProvider.WorkspaceChanged += WorkspaceAttachedToDocumentChanged; // Drive initial setup through the same code path as change events. diff --git a/src/Acuminator/Acuminator.Vsix/Coloriser/Roslyn/RoslynWorkspaceProvider.cs b/src/Acuminator/Acuminator.Vsix/Coloriser/Roslyn/RoslynWorkspaceProvider.cs index d75db6901..5210580f7 100644 --- a/src/Acuminator/Acuminator.Vsix/Coloriser/Roslyn/RoslynWorkspaceProvider.cs +++ b/src/Acuminator/Acuminator.Vsix/Coloriser/Roslyn/RoslynWorkspaceProvider.cs @@ -7,7 +7,6 @@ using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.Text; -using Microsoft.VisualStudio.Text; namespace Acuminator.Vsix.Coloriser; @@ -28,7 +27,7 @@ internal class RoslynWorkspaceProvider : IDisposable /// This locker has to be externally available because external subscribers like Roslyn colorizer subscribe on workspace changes and have unavoidable race condition
/// that has to use this locker to synchronize with change of the Roslyn workspace associated with a VS text buffer. /// - public object WorkspaceSubscriptionLocker { get; } = new object(); + public object WorkspaceSubscriptionLocker { get; } private Workspace? _workspace; @@ -45,16 +44,24 @@ public Workspace? Workspace public event EventHandler? WorkspaceChanged; - public RoslynWorkspaceProvider(ITextBuffer buffer) + private RoslynWorkspaceProvider(WorkspaceRegistration workspaceRegistration, object workspaceSubscriptionLocker) { - SourceTextContainer sourceTextContainer = buffer.CheckIfNull().AsTextContainer(); + WorkspaceSubscriptionLocker = workspaceSubscriptionLocker; + _workspaceRegistration = workspaceRegistration; + _workspaceRegistration.WorkspaceChanged += OnWorkspaceChanged; + _workspace = GetWorkspaceThatSupportsColoring(_workspaceRegistration); + } - lock (WorkspaceSubscriptionLocker) - { - _workspaceRegistration = Workspace.GetWorkspaceRegistration(sourceTextContainer); - _workspaceRegistration.WorkspaceChanged += OnWorkspaceChanged; + internal static RoslynWorkspaceProvider Create(SourceTextContainer sourceTextContainer, object syncLock) + { + sourceTextContainer.ThrowOnNull(); + syncLock.ThrowOnNull(); - _workspace = GetWorkspaceThatSupportsColoring(_workspaceRegistration); + lock (syncLock) + { + var workspaceRegistration = Workspace.GetWorkspaceRegistration(sourceTextContainer); + var workspaceProvider = new RoslynWorkspaceProvider(workspaceRegistration, syncLock); + return workspaceProvider; } } From 3e79a9617a798bcb16e12757b980d3b61fe2511d Mon Sep 17 00:00:00 2001 From: Sergey Nikomarov Date: Fri, 5 Jun 2026 14:45:25 +0200 Subject: [PATCH 03/15] ATR-972: added import of ITextDocumentFactoryService via MEF to the tagger provider --- .../Acuminator.Vsix/Coloriser/PXColorizerTaggerProvider.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Acuminator/Acuminator.Vsix/Coloriser/PXColorizerTaggerProvider.cs b/src/Acuminator/Acuminator.Vsix/Coloriser/PXColorizerTaggerProvider.cs index ed72581c1..aa9536347 100644 --- a/src/Acuminator/Acuminator.Vsix/Coloriser/PXColorizerTaggerProvider.cs +++ b/src/Acuminator/Acuminator.Vsix/Coloriser/PXColorizerTaggerProvider.cs @@ -31,8 +31,7 @@ public class PXColorizerTaggerProvider : IViewTaggerProvider private readonly IClassificationTypeRegistryService _classificationRegistry; private readonly IClassificationFormatMapService _classificationFormatMapService; - [Import] - internal IClassificationFormatMapService _classificationFormatMapService = null!; //Set via MEF + internal ITextDocumentFactoryService TextDocumentFactory { get; } private const string TextCategory = "text"; private static readonly object _syncRoot = new object(); @@ -76,6 +75,7 @@ public PXColorizerTaggerProvider(IClassificationTypeRegistryService classificati { _classificationRegistry = classificationRegistry; _classificationFormatMapService = classificationFormatMapService; + TextDocumentFactory = textDocumentFactory; } public virtual ITagger? CreateTagger(ITextView textView, ITextBuffer textBuffer) From 9a52bc2fceec1e1829f0603038d89f839448a63b Mon Sep 17 00:00:00 2001 From: Sergey Nikomarov Date: Fri, 5 Jun 2026 15:18:06 +0200 Subject: [PATCH 04/15] ATR-972: refactoring + optimization - reworked colorizer tagger provider, moved initialization to the tagger provider constructor and do it only once without any flag checking, simplified the code, made maps of classification types read only --- .../Coloriser/PXColorizerTaggerProvider.cs | 150 ++++++++---------- 1 file changed, 68 insertions(+), 82 deletions(-) diff --git a/src/Acuminator/Acuminator.Vsix/Coloriser/PXColorizerTaggerProvider.cs b/src/Acuminator/Acuminator.Vsix/Coloriser/PXColorizerTaggerProvider.cs index aa9536347..ee31571ae 100644 --- a/src/Acuminator/Acuminator.Vsix/Coloriser/PXColorizerTaggerProvider.cs +++ b/src/Acuminator/Acuminator.Vsix/Coloriser/PXColorizerTaggerProvider.cs @@ -2,7 +2,6 @@ using System; using System.Collections.Generic; using System.ComponentModel.Composition; -using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Runtime.CompilerServices; using System.Threading; @@ -34,48 +33,38 @@ public class PXColorizerTaggerProvider : IViewTaggerProvider internal ITextDocumentFactoryService TextDocumentFactory { get; } private const string TextCategory = "text"; - private static readonly object _syncRoot = new object(); - private static bool _isPriorityIncreased; - [MemberNotNullWhen(returnValue: true, nameof(_codeColoringClassificationTypes), nameof(_braceTypeByLevel))] - protected bool AreClassificationsInitialized - { - get; - private set; - } + private static readonly object _syncRoot = new object(); + private static volatile bool _isPriorityIncreased; - private Dictionary _codeColoringClassificationTypes = null!; + private readonly Dictionary _codeColoringClassificationTypes; - public IClassificationType? this[PXCodeType codeType] - { - [MethodImpl(MethodImplOptions.AggressiveInlining)] - get { - return _codeColoringClassificationTypes.TryGetValue(codeType, out IClassificationType type) + public IClassificationType? this[PXCodeType codeType] => + _codeColoringClassificationTypes.TryGetValue(codeType, out IClassificationType type) ? type : null; - } - } - private Dictionary _braceTypeByLevel = null!; + private readonly Dictionary _braceTypeByLevel; - public IClassificationType? this[int braceLevel] - { - [MethodImpl(MethodImplOptions.AggressiveInlining)] - get { - return _braceTypeByLevel.TryGetValue(braceLevel, out IClassificationType type) + public IClassificationType? this[int braceLevel] => + _braceTypeByLevel.TryGetValue(braceLevel, out IClassificationType type) ? type : null; - } - } [ImportingConstructor] public PXColorizerTaggerProvider(IClassificationTypeRegistryService classificationRegistry, IClassificationFormatMapService classificationFormatMapService, ITextDocumentFactoryService textDocumentFactory) { - _classificationRegistry = classificationRegistry; + _classificationRegistry = classificationRegistry; _classificationFormatMapService = classificationFormatMapService; - TextDocumentFactory = textDocumentFactory; + TextDocumentFactory = textDocumentFactory; + + _codeColoringClassificationTypes = GetClassificationTypesForAcumaticaCodeElements(_classificationRegistry); + _braceTypeByLevel = GetClassificationTypesForAngleBraces(_classificationRegistry); + + IncreaseCommentFormatTypesPriority(_classificationRegistry, _classificationFormatMapService, + _codeColoringClassificationTypes[PXCodeType.BqlParameter]); } public virtual ITagger? CreateTagger(ITextView textView, ITextBuffer textBuffer) @@ -84,8 +73,6 @@ public PXColorizerTaggerProvider(IClassificationTypeRegistryService classificati if (textView == null || textBuffer == null || textView.TextBuffer != textBuffer || !ThreadHelper.CheckAccess()) return null; - Initialize(textBuffer); - var tagger = textBuffer.Properties.GetOrCreateSingletonProperty(typeof(PXRoslynColorizerTagger), () => { return new PXRoslynColorizerTagger(textBuffer, this, subscribeToSettingsChanges: true, useCacheChecking: true); @@ -94,85 +81,84 @@ public PXColorizerTaggerProvider(IClassificationTypeRegistryService classificati return tagger as ITagger; } - [MemberNotNull(nameof(_codeColoringClassificationTypes), nameof(_braceTypeByLevel))] - protected void Initialize(ITextBuffer textBuffer) - { - if (AreClassificationsInitialized) - return; - - AreClassificationsInitialized = true; - InitializeClassificationTypes(); - IncreaseCommentFormatTypesPriority(_classificationRegistry, _classificationFormatMapService, - _codeColoringClassificationTypes[PXCodeType.BqlParameter]); - } - - [MemberNotNull(nameof(_codeColoringClassificationTypes), nameof(_braceTypeByLevel))] - protected void InitializeClassificationTypes() + private static Dictionary GetClassificationTypesForAcumaticaCodeElements( + IClassificationTypeRegistryService classificationRegistry) { - IClassificationType bqlClassificationType = _classificationRegistry.GetClassificationType(ColoringConstants.BQLOperatorFormat); - - _codeColoringClassificationTypes = new Dictionary + IClassificationType bqlClassificationType = classificationRegistry.GetClassificationType(ColoringConstants.BQLOperatorFormat); + var acumaticaCodeElementsClassificationTypes = new Dictionary { - [PXCodeType.Dac] = _classificationRegistry.GetClassificationType(ColoringConstants.DacFormat), - [PXCodeType.DacExtension] = _classificationRegistry.GetClassificationType(ColoringConstants.DacExtensionFormat), - [PXCodeType.DacField] = _classificationRegistry.GetClassificationType(ColoringConstants.DacFieldFormat), - [PXCodeType.BqlParameter] = _classificationRegistry.GetClassificationType(ColoringConstants.BQLParameterFormat), + [PXCodeType.Dac] = classificationRegistry.GetClassificationType(ColoringConstants.DacFormat), + [PXCodeType.DacExtension] = classificationRegistry.GetClassificationType(ColoringConstants.DacExtensionFormat), + [PXCodeType.DacField] = classificationRegistry.GetClassificationType(ColoringConstants.DacFieldFormat), + [PXCodeType.BqlParameter] = classificationRegistry.GetClassificationType(ColoringConstants.BQLParameterFormat), [PXCodeType.BqlOperator] = bqlClassificationType, [PXCodeType.BqlCommand] = bqlClassificationType, - [PXCodeType.BQLConstantPrefix] = _classificationRegistry.GetClassificationType(ColoringConstants.BQLConstantPrefixFormat), - [PXCodeType.BQLConstantEnding] = _classificationRegistry.GetClassificationType(ColoringConstants.BQLConstantEndingFormat), + [PXCodeType.BQLConstantPrefix] = classificationRegistry.GetClassificationType(ColoringConstants.BQLConstantPrefixFormat), + [PXCodeType.BQLConstantEnding] = classificationRegistry.GetClassificationType(ColoringConstants.BQLConstantEndingFormat), - [PXCodeType.PXGraph] = _classificationRegistry.GetClassificationType(ColoringConstants.PXGraphFormat), - [PXCodeType.PXAction] = _classificationRegistry.GetClassificationType(ColoringConstants.PXActionFormat), + [PXCodeType.PXGraph] = classificationRegistry.GetClassificationType(ColoringConstants.PXGraphFormat), + [PXCodeType.PXAction] = classificationRegistry.GetClassificationType(ColoringConstants.PXActionFormat), }; - _braceTypeByLevel = new Dictionary(capacity: ColoringConstants.MaxBraceLevel) + return acumaticaCodeElementsClassificationTypes; + } + + private static Dictionary GetClassificationTypesForAngleBraces(IClassificationTypeRegistryService classificationRegistry) + { + var braceTypeByLevel = new Dictionary(capacity: ColoringConstants.MaxBraceLevel) { - [0] = _classificationRegistry.GetClassificationType(ColoringConstants.BraceLevel_1_Format), - [1] = _classificationRegistry.GetClassificationType(ColoringConstants.BraceLevel_2_Format), - [2] = _classificationRegistry.GetClassificationType(ColoringConstants.BraceLevel_3_Format), + [0] = classificationRegistry.GetClassificationType(ColoringConstants.BraceLevel_1_Format), + [1] = classificationRegistry.GetClassificationType(ColoringConstants.BraceLevel_2_Format), + [2] = classificationRegistry.GetClassificationType(ColoringConstants.BraceLevel_3_Format), - [3] = _classificationRegistry.GetClassificationType(ColoringConstants.BraceLevel_4_Format), - [4] = _classificationRegistry.GetClassificationType(ColoringConstants.BraceLevel_5_Format), - [5] = _classificationRegistry.GetClassificationType(ColoringConstants.BraceLevel_6_Format), + [3] = classificationRegistry.GetClassificationType(ColoringConstants.BraceLevel_4_Format), + [4] = classificationRegistry.GetClassificationType(ColoringConstants.BraceLevel_5_Format), + [5] = classificationRegistry.GetClassificationType(ColoringConstants.BraceLevel_6_Format), - [6] = _classificationRegistry.GetClassificationType(ColoringConstants.BraceLevel_7_Format), - [7] = _classificationRegistry.GetClassificationType(ColoringConstants.BraceLevel_8_Format), - [8] = _classificationRegistry.GetClassificationType(ColoringConstants.BraceLevel_9_Format), + [6] = classificationRegistry.GetClassificationType(ColoringConstants.BraceLevel_7_Format), + [7] = classificationRegistry.GetClassificationType(ColoringConstants.BraceLevel_8_Format), + [8] = classificationRegistry.GetClassificationType(ColoringConstants.BraceLevel_9_Format), - [9] = _classificationRegistry.GetClassificationType(ColoringConstants.BraceLevel_10_Format), - [10] = _classificationRegistry.GetClassificationType(ColoringConstants.BraceLevel_11_Format), - [11] = _classificationRegistry.GetClassificationType(ColoringConstants.BraceLevel_12_Format), + [9] = classificationRegistry.GetClassificationType(ColoringConstants.BraceLevel_10_Format), + [10] = classificationRegistry.GetClassificationType(ColoringConstants.BraceLevel_11_Format), + [11] = classificationRegistry.GetClassificationType(ColoringConstants.BraceLevel_12_Format), - [12] = _classificationRegistry.GetClassificationType(ColoringConstants.BraceLevel_13_Format), - [13] = _classificationRegistry.GetClassificationType(ColoringConstants.BraceLevel_14_Format), + [12] = classificationRegistry.GetClassificationType(ColoringConstants.BraceLevel_13_Format), + [13] = classificationRegistry.GetClassificationType(ColoringConstants.BraceLevel_14_Format), }; + + return braceTypeByLevel; } private static void IncreaseCommentFormatTypesPriority(IClassificationTypeRegistryService registry, IClassificationFormatMapService formatMapService, IClassificationType highestPriorityType) { + if (_isPriorityIncreased) + return; + bool lockTaken = false; Monitor.TryEnter(_syncRoot, ref lockTaken); - if (lockTaken) + if (!lockTaken) + return; + + try { - try - { - if (!_isPriorityIncreased) - { - _isPriorityIncreased = true; - IClassificationFormatMap formatMap = formatMapService.GetClassificationFormatMap(category: TextCategory); - IncreaseServiceFormatPriority(formatMap, registry, PredefinedClassificationTypeNames.ExcludedCode, highestPriorityType); - IncreaseServiceFormatPriority(formatMap, registry, PredefinedClassificationTypeNames.Comment, highestPriorityType); - } - } - finally + if (_isPriorityIncreased) + return; + + if (formatMapService.GetClassificationFormatMap(category: TextCategory) is IClassificationFormatMap formatMap) { - Monitor.Exit(_syncRoot); + IncreaseServiceFormatPriority(formatMap, registry, PredefinedClassificationTypeNames.ExcludedCode, highestPriorityType); + IncreaseServiceFormatPriority(formatMap, registry, PredefinedClassificationTypeNames.Comment, highestPriorityType); + _isPriorityIncreased = true; } } + finally + { + Monitor.Exit(_syncRoot); + } } private static void IncreaseServiceFormatPriority(IClassificationFormatMap formatMap, IClassificationTypeRegistryService registry, string formatName, From 2f5ab6a5da12a55cdda033b5b4602bde3377421c Mon Sep 17 00:00:00 2001 From: Sergey Nikomarov Date: Fri, 5 Jun 2026 16:14:10 +0200 Subject: [PATCH 05/15] ATR-972: implemented notification for the disposal of the current document --- .../TextDocumentDisposedNotification.cs | 69 +++++++++++++++++++ 1 file changed, 69 insertions(+) create mode 100644 src/Acuminator/Acuminator.Vsix/Coloriser/TextDocumentDisposedNotification.cs diff --git a/src/Acuminator/Acuminator.Vsix/Coloriser/TextDocumentDisposedNotification.cs b/src/Acuminator/Acuminator.Vsix/Coloriser/TextDocumentDisposedNotification.cs new file mode 100644 index 000000000..2bbb7356a --- /dev/null +++ b/src/Acuminator/Acuminator.Vsix/Coloriser/TextDocumentDisposedNotification.cs @@ -0,0 +1,69 @@ +#nullable enable +using System; +using System.Collections.Generic; +using System.Linq; + +using Acuminator.Utilities.Common; + +using Microsoft.VisualStudio.Text; + +namespace Acuminator.Vsix.Coloriser; + +public class TextDocumentDisposedNotification +{ + private readonly ITextDocumentFactoryService _textDocumentFactory; + private readonly ITextBuffer _textBuffer; + private ITextDocument? _textDocument; + + public event EventHandler? OnCurrentTextDocumentDisposed; + + public TextDocumentDisposedNotification(ITextDocumentFactoryService textDocumentFactory, ITextBuffer textBuffer) + { + _textDocumentFactory = textDocumentFactory.CheckIfNull(); + _textBuffer = textBuffer.CheckIfNull(); + + SubscribeToDocumentLifetimeEvents(); + } + + private void SubscribeToDocumentLifetimeEvents() + { + // If a document already exists for this buffer just attach the disposed handler. + if (_textDocumentFactory.TryGetTextDocument(_textBuffer, out ITextDocument existingTextDocument)) + { + SubscribeOnTextDocumentDisposedEvent(existingTextDocument); + } + else + { + // If the document is created later, attach to the created event and listen for the right document to be created, then attach the disposed handler. + _textDocumentFactory.TextDocumentCreated += OnTextDocumentCreated; + } + } + + private void OnTextDocumentCreated(object sender, TextDocumentEventArgs e) + { + // Filter out doc creation events for other documents + if (e.TextDocument == null || !ReferenceEquals(_textBuffer, e.TextDocument.TextBuffer)) + return; + + //Dispose of the subscription immediately to always run the handler only once + _textDocumentFactory.TextDocumentCreated -= OnTextDocumentCreated; + SubscribeOnTextDocumentDisposedEvent(e.TextDocument); + } + + private void SubscribeOnTextDocumentDisposedEvent(ITextDocument textDocument) + { + _textDocument = textDocument; + _textDocumentFactory.TextDocumentDisposed += OnTextDocumentDisposed; + } + + private void OnTextDocumentDisposed(object sender, TextDocumentEventArgs e) + { + // Filter out dispose events for other documents + if (!ReferenceEquals(_textDocument, e.TextDocument)) + return; + + //Dispose of the subscription immediately to always run the handler only once + _textDocumentFactory.TextDocumentDisposed -= OnTextDocumentDisposed; + OnCurrentTextDocumentDisposed?.Invoke(this, EventArgs.Empty); + } +} From 1a22e428a7777b230463381f81d77e44c741a74d Mon Sep 17 00:00:00 2001 From: Sergey Nikomarov Date: Fri, 5 Jun 2026 16:24:25 +0200 Subject: [PATCH 06/15] ATR-972: replaced the DIspose method and IDisposable interface with the notification of the text document disposal in the base tagger --- .../Coloriser/Base/PXTaggerBase.cs | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/src/Acuminator/Acuminator.Vsix/Coloriser/Base/PXTaggerBase.cs b/src/Acuminator/Acuminator.Vsix/Coloriser/Base/PXTaggerBase.cs index f874b9201..afcd320aa 100644 --- a/src/Acuminator/Acuminator.Vsix/Coloriser/Base/PXTaggerBase.cs +++ b/src/Acuminator/Acuminator.Vsix/Coloriser/Base/PXTaggerBase.cs @@ -14,8 +14,10 @@ namespace Acuminator.Vsix.Coloriser { - public abstract class PXTaggerBase : IDisposable + public abstract class PXTaggerBase { + private readonly TextDocumentDisposedNotification _disposedNotification; + #pragma warning disable CS0067 public event EventHandler? TagsChanged; #pragma warning restore CS0067 @@ -32,7 +34,8 @@ public abstract class PXTaggerBase : IDisposable protected bool CacheCheckingEnabled { get; } - protected PXTaggerBase(ITextBuffer buffer, bool subscribeToSettingsChanges, bool useCacheChecking) + protected PXTaggerBase(ITextBuffer buffer, ITextDocumentFactoryService textDocumentFactory, + bool subscribeToSettingsChanges, bool useCacheChecking) { Buffer = buffer.CheckIfNull(); SubscribedToSettingsChanges = subscribeToSettingsChanges; @@ -47,6 +50,9 @@ protected PXTaggerBase(ITextBuffer buffer, bool subscribeToSettingsChanges, bool genOptionsPage.ColoringSettingChanged += ColoringSettingChangedHandler; } } + + _disposedNotification = new TextDocumentDisposedNotification(textDocumentFactory, Buffer); + _disposedNotification.OnCurrentTextDocumentDisposed += CleanupOnTextDocumentDisposed; } protected virtual void ColoringSettingChangedHandler(object sender, SettingChangedEventArgs e) @@ -94,8 +100,12 @@ protected internal virtual void ResetCacheAndFlags(ITextSnapshot? newSnapshotToC Snapshot = newSnapshotToCache; } - public virtual void Dispose() + protected virtual void CleanupOnTextDocumentDisposed(object sender, EventArgs e) { + Type taggerType = GetType(); + Buffer.Properties.RemoveProperty(taggerType); + _disposedNotification.OnCurrentTextDocumentDisposed -= CleanupOnTextDocumentDisposed; + if (!SubscribedToSettingsChanges) return; From 5d49238699d9b50213942b7c1f7dd7f038d040c0 Mon Sep 17 00:00:00 2001 From: Sergey Nikomarov Date: Fri, 5 Jun 2026 16:33:34 +0200 Subject: [PATCH 07/15] ATR-972: integrated new notification mechanism into the outlining tagger --- .../Coloriser/Outlining/PXOutliningTagger.cs | 11 ++--- .../Outlining/PXOutliningTaggerProvider.cs | 41 +++++++++++-------- 2 files changed, 29 insertions(+), 23 deletions(-) diff --git a/src/Acuminator/Acuminator.Vsix/Coloriser/Outlining/PXOutliningTagger.cs b/src/Acuminator/Acuminator.Vsix/Coloriser/Outlining/PXOutliningTagger.cs index c884d0f1d..215664bdf 100644 --- a/src/Acuminator/Acuminator.Vsix/Coloriser/Outlining/PXOutliningTagger.cs +++ b/src/Acuminator/Acuminator.Vsix/Coloriser/Outlining/PXOutliningTagger.cs @@ -25,8 +25,9 @@ internal class PXOutliningTagger : PXTaggerBase, ITagger [MemberNotNullWhen(returnValue: true, nameof(ColorizerTagger))] public override bool HasReferenceToAcumaticaPlatform => ColorizerTagger?.HasReferenceToAcumaticaPlatform ?? false; - public PXOutliningTagger(ITextBuffer buffer, bool subscribeToSettingsChanges, bool useCacheChecking) : - base(buffer, subscribeToSettingsChanges, useCacheChecking) + public PXOutliningTagger(ITextBuffer buffer, ITextDocumentFactoryService textDocumentFactory, bool subscribeToSettingsChanges, + bool useCacheChecking) : + base(buffer, textDocumentFactory, subscribeToSettingsChanges, useCacheChecking) { } @@ -69,15 +70,15 @@ private void OnColorizingTaggerTagsChanged(object sender, SnapshotSpanEventArgs RaiseTagsChanged(); } - public override void Dispose() + protected override void CleanupOnTextDocumentDisposed(object sender, EventArgs e) { + base.CleanupOnTextDocumentDisposed(sender, e); + if (Interlocked.Exchange(ref _isSubscribed, NOT_SUBSCRIBED) == SUBSCRIBED && ColorizerTagger != null) { ColorizerTagger.TagsChanged -= OnColorizingTaggerTagsChanged; ColorizerTagger = null; } - - base.Dispose(); } } } diff --git a/src/Acuminator/Acuminator.Vsix/Coloriser/Outlining/PXOutliningTaggerProvider.cs b/src/Acuminator/Acuminator.Vsix/Coloriser/Outlining/PXOutliningTaggerProvider.cs index 1a525d29b..a6a10b322 100644 --- a/src/Acuminator/Acuminator.Vsix/Coloriser/Outlining/PXOutliningTaggerProvider.cs +++ b/src/Acuminator/Acuminator.Vsix/Coloriser/Outlining/PXOutliningTaggerProvider.cs @@ -1,5 +1,4 @@ #nullable enable - using System; using System.Collections.Generic; using System.ComponentModel.Composition; @@ -7,30 +6,36 @@ using Microsoft.VisualStudio.Shell; using Microsoft.VisualStudio.Text; -using Microsoft.VisualStudio.Text.Classification; using Microsoft.VisualStudio.Text.Editor; using Microsoft.VisualStudio.Text.Tagging; using Microsoft.VisualStudio.Utilities; -namespace Acuminator.Vsix.Coloriser +namespace Acuminator.Vsix.Coloriser; + +[ContentType("CSharp")] +[TagType(typeof(IOutliningRegionTag))] +[TextViewRole(PredefinedTextViewRoles.Document)] +[Export(typeof(ITaggerProvider))] +public class PXOutliningTaggerProvider : ITaggerProvider { - [ContentType("CSharp")] - [TagType(typeof(IOutliningRegionTag))] - [TextViewRole(PredefinedTextViewRoles.Document)] - [Export(typeof(ITaggerProvider))] - public class PXOutliningTaggerProvider : ITaggerProvider + private readonly ITextDocumentFactoryService _textDocumentFactory; + + [ImportingConstructor] + public PXOutliningTaggerProvider(ITextDocumentFactoryService textDocumentFactory) { - public ITagger? CreateTagger(ITextBuffer buffer) where T : ITag - { - if (buffer == null || !ThreadHelper.CheckAccess()) - return null; + _textDocumentFactory = textDocumentFactory; + } + + public ITagger? CreateTagger(ITextBuffer buffer) where T : ITag + { + if (buffer == null || !ThreadHelper.CheckAccess()) + return null; - PXOutliningTagger outliningTagger = buffer.Properties.GetOrCreateSingletonProperty(() => - { - return new PXOutliningTagger(buffer, subscribeToSettingsChanges: true, useCacheChecking: true); - }); + PXOutliningTagger outliningTagger = buffer.Properties.GetOrCreateSingletonProperty(typeof(PXOutliningTagger), () => + { + return new PXOutliningTagger(buffer, _textDocumentFactory, subscribeToSettingsChanges: true, useCacheChecking: true); + }); - return outliningTagger as ITagger; - } + return outliningTagger as ITagger; } } From fc1639fef07de145f1967944c49876e8a0b26c9a Mon Sep 17 00:00:00 2001 From: Sergey Nikomarov Date: Fri, 5 Jun 2026 16:36:59 +0200 Subject: [PATCH 08/15] ATR-972: integrated new notification mechanism into the coloring tagger and remove the IDisposable interface --- .../Coloriser/PXColorizerTaggerProvider.cs | 235 +++++++++--------- .../Coloriser/PXRoslynColorizerTagger.cs | 16 +- 2 files changed, 124 insertions(+), 127 deletions(-) diff --git a/src/Acuminator/Acuminator.Vsix/Coloriser/PXColorizerTaggerProvider.cs b/src/Acuminator/Acuminator.Vsix/Coloriser/PXColorizerTaggerProvider.cs index ee31571ae..f52692dc5 100644 --- a/src/Acuminator/Acuminator.Vsix/Coloriser/PXColorizerTaggerProvider.cs +++ b/src/Acuminator/Acuminator.Vsix/Coloriser/PXColorizerTaggerProvider.cs @@ -3,7 +3,6 @@ using System.Collections.Generic; using System.ComponentModel.Composition; using System.Linq; -using System.Runtime.CompilerServices; using System.Threading; using Acuminator.Utilities.Roslyn; @@ -19,158 +18,156 @@ using ThreadHelper = Microsoft.VisualStudio.Shell.ThreadHelper; -namespace Acuminator.Vsix.Coloriser -{ - [ContentType(Constants.CSharp.LegacyLanguageName)] - [TagType(typeof(IClassificationTag))] - [TextViewRole(PredefinedTextViewRoles.Document)] - [Export(typeof(IViewTaggerProvider))] - public class PXColorizerTaggerProvider : IViewTaggerProvider - { - private readonly IClassificationTypeRegistryService _classificationRegistry; - private readonly IClassificationFormatMapService _classificationFormatMapService; +namespace Acuminator.Vsix.Coloriser; - internal ITextDocumentFactoryService TextDocumentFactory { get; } +[ContentType(Constants.CSharp.LegacyLanguageName)] +[TagType(typeof(IClassificationTag))] +[TextViewRole(PredefinedTextViewRoles.Document)] +[Export(typeof(IViewTaggerProvider))] +public class PXColorizerTaggerProvider : IViewTaggerProvider +{ + private const string TextCategory = "text"; + + private readonly IClassificationTypeRegistryService _classificationRegistry; + private readonly IClassificationFormatMapService _classificationFormatMapService; + private readonly ITextDocumentFactoryService _textDocumentFactory; - private const string TextCategory = "text"; + private static readonly object _syncRoot = new object(); + private static volatile bool _isPriorityIncreased; - private static readonly object _syncRoot = new object(); - private static volatile bool _isPriorityIncreased; + private readonly Dictionary _codeColoringClassificationTypes; - private readonly Dictionary _codeColoringClassificationTypes; + public IClassificationType? this[PXCodeType codeType] => + _codeColoringClassificationTypes.TryGetValue(codeType, out IClassificationType type) + ? type + : null; - public IClassificationType? this[PXCodeType codeType] => - _codeColoringClassificationTypes.TryGetValue(codeType, out IClassificationType type) - ? type - : null; + private readonly Dictionary _braceTypeByLevel; - private readonly Dictionary _braceTypeByLevel; + public IClassificationType? this[int braceLevel] => + _braceTypeByLevel.TryGetValue(braceLevel, out IClassificationType type) + ? type + : null; - public IClassificationType? this[int braceLevel] => - _braceTypeByLevel.TryGetValue(braceLevel, out IClassificationType type) - ? type - : null; + [ImportingConstructor] + public PXColorizerTaggerProvider(IClassificationTypeRegistryService classificationRegistry, + IClassificationFormatMapService classificationFormatMapService, + ITextDocumentFactoryService textDocumentFactory) + { + _classificationRegistry = classificationRegistry; + _classificationFormatMapService = classificationFormatMapService; + _textDocumentFactory = textDocumentFactory; - [ImportingConstructor] - public PXColorizerTaggerProvider(IClassificationTypeRegistryService classificationRegistry, - IClassificationFormatMapService classificationFormatMapService, - ITextDocumentFactoryService textDocumentFactory) - { - _classificationRegistry = classificationRegistry; - _classificationFormatMapService = classificationFormatMapService; - TextDocumentFactory = textDocumentFactory; + _codeColoringClassificationTypes = GetClassificationTypesForAcumaticaCodeElements(_classificationRegistry); + _braceTypeByLevel = GetClassificationTypesForAngleBraces(_classificationRegistry); - _codeColoringClassificationTypes = GetClassificationTypesForAcumaticaCodeElements(_classificationRegistry); - _braceTypeByLevel = GetClassificationTypesForAngleBraces(_classificationRegistry); + IncreaseCommentFormatTypesPriority(_classificationRegistry, _classificationFormatMapService, + _codeColoringClassificationTypes[PXCodeType.BqlParameter]); + } - IncreaseCommentFormatTypesPriority(_classificationRegistry, _classificationFormatMapService, - _codeColoringClassificationTypes[PXCodeType.BqlParameter]); - } + public virtual ITagger? CreateTagger(ITextView textView, ITextBuffer textBuffer) + where T : ITag + { + if (textView == null || textBuffer == null || textView.TextBuffer != textBuffer || !ThreadHelper.CheckAccess()) + return null; - public virtual ITagger? CreateTagger(ITextView textView, ITextBuffer textBuffer) - where T : ITag + var tagger = textBuffer.Properties.GetOrCreateSingletonProperty(typeof(PXRoslynColorizerTagger), () => { - if (textView == null || textBuffer == null || textView.TextBuffer != textBuffer || !ThreadHelper.CheckAccess()) - return null; - - var tagger = textBuffer.Properties.GetOrCreateSingletonProperty(typeof(PXRoslynColorizerTagger), () => - { - return new PXRoslynColorizerTagger(textBuffer, this, subscribeToSettingsChanges: true, useCacheChecking: true); - }); + return new PXRoslynColorizerTagger(textBuffer, _textDocumentFactory, this, subscribeToSettingsChanges: true, useCacheChecking: true); + }); - return tagger as ITagger; - } + return tagger as ITagger; + } - private static Dictionary GetClassificationTypesForAcumaticaCodeElements( - IClassificationTypeRegistryService classificationRegistry) + private static Dictionary GetClassificationTypesForAcumaticaCodeElements( + IClassificationTypeRegistryService classificationRegistry) + { + IClassificationType bqlClassificationType = classificationRegistry.GetClassificationType(ColoringConstants.BQLOperatorFormat); + var acumaticaCodeElementsClassificationTypes = new Dictionary { - IClassificationType bqlClassificationType = classificationRegistry.GetClassificationType(ColoringConstants.BQLOperatorFormat); - var acumaticaCodeElementsClassificationTypes = new Dictionary - { - [PXCodeType.Dac] = classificationRegistry.GetClassificationType(ColoringConstants.DacFormat), - [PXCodeType.DacExtension] = classificationRegistry.GetClassificationType(ColoringConstants.DacExtensionFormat), - [PXCodeType.DacField] = classificationRegistry.GetClassificationType(ColoringConstants.DacFieldFormat), - [PXCodeType.BqlParameter] = classificationRegistry.GetClassificationType(ColoringConstants.BQLParameterFormat), - [PXCodeType.BqlOperator] = bqlClassificationType, - [PXCodeType.BqlCommand] = bqlClassificationType, + [PXCodeType.Dac] = classificationRegistry.GetClassificationType(ColoringConstants.DacFormat), + [PXCodeType.DacExtension] = classificationRegistry.GetClassificationType(ColoringConstants.DacExtensionFormat), + [PXCodeType.DacField] = classificationRegistry.GetClassificationType(ColoringConstants.DacFieldFormat), + [PXCodeType.BqlParameter] = classificationRegistry.GetClassificationType(ColoringConstants.BQLParameterFormat), + [PXCodeType.BqlOperator] = bqlClassificationType, + [PXCodeType.BqlCommand] = bqlClassificationType, - [PXCodeType.BQLConstantPrefix] = classificationRegistry.GetClassificationType(ColoringConstants.BQLConstantPrefixFormat), - [PXCodeType.BQLConstantEnding] = classificationRegistry.GetClassificationType(ColoringConstants.BQLConstantEndingFormat), + [PXCodeType.BQLConstantPrefix] = classificationRegistry.GetClassificationType(ColoringConstants.BQLConstantPrefixFormat), + [PXCodeType.BQLConstantEnding] = classificationRegistry.GetClassificationType(ColoringConstants.BQLConstantEndingFormat), - [PXCodeType.PXGraph] = classificationRegistry.GetClassificationType(ColoringConstants.PXGraphFormat), - [PXCodeType.PXAction] = classificationRegistry.GetClassificationType(ColoringConstants.PXActionFormat), - }; + [PXCodeType.PXGraph] = classificationRegistry.GetClassificationType(ColoringConstants.PXGraphFormat), + [PXCodeType.PXAction] = classificationRegistry.GetClassificationType(ColoringConstants.PXActionFormat), + }; - return acumaticaCodeElementsClassificationTypes; - } + return acumaticaCodeElementsClassificationTypes; + } - private static Dictionary GetClassificationTypesForAngleBraces(IClassificationTypeRegistryService classificationRegistry) + private static Dictionary GetClassificationTypesForAngleBraces(IClassificationTypeRegistryService classificationRegistry) + { + var braceTypeByLevel = new Dictionary(capacity: ColoringConstants.MaxBraceLevel) { - var braceTypeByLevel = new Dictionary(capacity: ColoringConstants.MaxBraceLevel) - { - [0] = classificationRegistry.GetClassificationType(ColoringConstants.BraceLevel_1_Format), - [1] = classificationRegistry.GetClassificationType(ColoringConstants.BraceLevel_2_Format), - [2] = classificationRegistry.GetClassificationType(ColoringConstants.BraceLevel_3_Format), + [0] = classificationRegistry.GetClassificationType(ColoringConstants.BraceLevel_1_Format), + [1] = classificationRegistry.GetClassificationType(ColoringConstants.BraceLevel_2_Format), + [2] = classificationRegistry.GetClassificationType(ColoringConstants.BraceLevel_3_Format), - [3] = classificationRegistry.GetClassificationType(ColoringConstants.BraceLevel_4_Format), - [4] = classificationRegistry.GetClassificationType(ColoringConstants.BraceLevel_5_Format), - [5] = classificationRegistry.GetClassificationType(ColoringConstants.BraceLevel_6_Format), + [3] = classificationRegistry.GetClassificationType(ColoringConstants.BraceLevel_4_Format), + [4] = classificationRegistry.GetClassificationType(ColoringConstants.BraceLevel_5_Format), + [5] = classificationRegistry.GetClassificationType(ColoringConstants.BraceLevel_6_Format), - [6] = classificationRegistry.GetClassificationType(ColoringConstants.BraceLevel_7_Format), - [7] = classificationRegistry.GetClassificationType(ColoringConstants.BraceLevel_8_Format), - [8] = classificationRegistry.GetClassificationType(ColoringConstants.BraceLevel_9_Format), + [6] = classificationRegistry.GetClassificationType(ColoringConstants.BraceLevel_7_Format), + [7] = classificationRegistry.GetClassificationType(ColoringConstants.BraceLevel_8_Format), + [8] = classificationRegistry.GetClassificationType(ColoringConstants.BraceLevel_9_Format), - [9] = classificationRegistry.GetClassificationType(ColoringConstants.BraceLevel_10_Format), - [10] = classificationRegistry.GetClassificationType(ColoringConstants.BraceLevel_11_Format), - [11] = classificationRegistry.GetClassificationType(ColoringConstants.BraceLevel_12_Format), + [9] = classificationRegistry.GetClassificationType(ColoringConstants.BraceLevel_10_Format), + [10] = classificationRegistry.GetClassificationType(ColoringConstants.BraceLevel_11_Format), + [11] = classificationRegistry.GetClassificationType(ColoringConstants.BraceLevel_12_Format), - [12] = classificationRegistry.GetClassificationType(ColoringConstants.BraceLevel_13_Format), - [13] = classificationRegistry.GetClassificationType(ColoringConstants.BraceLevel_14_Format), - }; + [12] = classificationRegistry.GetClassificationType(ColoringConstants.BraceLevel_13_Format), + [13] = classificationRegistry.GetClassificationType(ColoringConstants.BraceLevel_14_Format), + }; - return braceTypeByLevel; - } + return braceTypeByLevel; + } - private static void IncreaseCommentFormatTypesPriority(IClassificationTypeRegistryService registry, IClassificationFormatMapService formatMapService, - IClassificationType highestPriorityType) - { - if (_isPriorityIncreased) - return; + private static void IncreaseCommentFormatTypesPriority(IClassificationTypeRegistryService registry, IClassificationFormatMapService formatMapService, + IClassificationType highestPriorityType) + { + if (_isPriorityIncreased) + return; + + bool lockTaken = false; + Monitor.TryEnter(_syncRoot, ref lockTaken); - bool lockTaken = false; - Monitor.TryEnter(_syncRoot, ref lockTaken); + if (!lockTaken) + return; - if (!lockTaken) + try + { + if (_isPriorityIncreased) return; - try + if (formatMapService.GetClassificationFormatMap(category: TextCategory) is IClassificationFormatMap formatMap) { - if (_isPriorityIncreased) - return; - - if (formatMapService.GetClassificationFormatMap(category: TextCategory) is IClassificationFormatMap formatMap) - { - IncreaseServiceFormatPriority(formatMap, registry, PredefinedClassificationTypeNames.ExcludedCode, highestPriorityType); - IncreaseServiceFormatPriority(formatMap, registry, PredefinedClassificationTypeNames.Comment, highestPriorityType); - _isPriorityIncreased = true; - } - } - finally - { - Monitor.Exit(_syncRoot); + IncreaseServiceFormatPriority(formatMap, registry, PredefinedClassificationTypeNames.ExcludedCode, highestPriorityType); + IncreaseServiceFormatPriority(formatMap, registry, PredefinedClassificationTypeNames.Comment, highestPriorityType); + _isPriorityIncreased = true; } } - - private static void IncreaseServiceFormatPriority(IClassificationFormatMap formatMap, IClassificationTypeRegistryService registry, string formatName, - IClassificationType highestPriorityType) + finally { - IClassificationType predefinedClassificationType = registry.GetClassificationType(formatName); - IClassificationType artificialClassType = registry.CreateTransientClassificationType(predefinedClassificationType); - TextFormattingRunProperties properties = formatMap.GetExplicitTextProperties(predefinedClassificationType); - - formatMap.AddExplicitTextProperties(artificialClassType, properties, highestPriorityType); - formatMap.SwapPriorities(artificialClassType, predefinedClassificationType); - formatMap.SwapPriorities(highestPriorityType, predefinedClassificationType); + Monitor.Exit(_syncRoot); } } + + private static void IncreaseServiceFormatPriority(IClassificationFormatMap formatMap, IClassificationTypeRegistryService registry, string formatName, + IClassificationType highestPriorityType) + { + IClassificationType predefinedClassificationType = registry.GetClassificationType(formatName); + IClassificationType artificialClassType = registry.CreateTransientClassificationType(predefinedClassificationType); + TextFormattingRunProperties properties = formatMap.GetExplicitTextProperties(predefinedClassificationType); + + formatMap.AddExplicitTextProperties(artificialClassType, properties, highestPriorityType); + formatMap.SwapPriorities(artificialClassType, predefinedClassificationType); + formatMap.SwapPriorities(highestPriorityType, predefinedClassificationType); + } } diff --git a/src/Acuminator/Acuminator.Vsix/Coloriser/PXRoslynColorizerTagger.cs b/src/Acuminator/Acuminator.Vsix/Coloriser/PXRoslynColorizerTagger.cs index fd7d5e31b..e56597e1b 100644 --- a/src/Acuminator/Acuminator.Vsix/Coloriser/PXRoslynColorizerTagger.cs +++ b/src/Acuminator/Acuminator.Vsix/Coloriser/PXRoslynColorizerTagger.cs @@ -14,6 +14,7 @@ using Microsoft.CodeAnalysis.Text; using Microsoft.VisualStudio.Text; using Microsoft.VisualStudio.Text.Tagging; +using Microsoft.VisualStudio.TextManager.Interop; using ThreadHelper = Microsoft.VisualStudio.Shell.ThreadHelper; @@ -22,7 +23,7 @@ namespace Acuminator.Vsix.Coloriser; /// /// A Roslyn-based colorizer tagger. /// -internal partial class PXRoslynColorizerTagger : PXTaggerBase, ITagger, IDisposable +internal partial class PXRoslynColorizerTagger : PXTaggerBase, ITagger { protected internal TagsCacheAsync ClassificationTagsCache { get; } @@ -47,9 +48,9 @@ internal bool LastTaggingWasSuccessful private readonly RoslynWorkspaceProvider _roslynWorkspaceProvider; private readonly SourceTextContainer _cachedTextContainer; - public PXRoslynColorizerTagger(ITextBuffer buffer, PXColorizerTaggerProvider provider, bool subscribeToSettingsChanges, - bool useCacheChecking) : - base(buffer, subscribeToSettingsChanges, useCacheChecking) + public PXRoslynColorizerTagger(ITextBuffer buffer, ITextDocumentFactoryService textDocumentFactory, PXColorizerTaggerProvider provider, + bool subscribeToSettingsChanges, bool useCacheChecking) : + base(buffer, textDocumentFactory, subscribeToSettingsChanges, useCacheChecking) { Provider = provider.CheckIfNull(); _cachedTextContainer = Buffer.AsTextContainer(); @@ -230,8 +231,10 @@ private void WalkDocumentSyntaxTreeForTags(ParsedDocument document, Cancellation OutliningsTagsCache.CompleteProcessing(); } - public override void Dispose() + protected override void CleanupOnTextDocumentDisposed(object sender, EventArgs e) { + base.CleanupOnTextDocumentDisposed(sender, e); + lock (_roslynWorkspaceProvider.WorkspaceSubscriptionLocker) { _roslynWorkspaceProvider.WorkspaceChanged -= WorkspaceAttachedToDocumentChanged; @@ -241,15 +244,12 @@ public override void Dispose() workspaceToUnsubscribe.WorkspaceChanged -= OnWorkspaceChanged; } - _roslynWorkspaceProvider.Dispose(); BackgroundTagging?.Dispose(); ClassificationTagsCache.Reset(); OutliningsTagsCache.Reset(); _hasReferenceToAcumaticaPlatform = false; - - base.Dispose(); } private void WorkspaceAttachedToDocumentChanged(object sender, DocumentWorkspaceChangedEventArgs e) From 45c7a21ead9b2d5bb67d7eed0b0d37e02a2e89f8 Mon Sep 17 00:00:00 2001 From: Sergey Nikomarov Date: Fri, 5 Jun 2026 16:59:20 +0200 Subject: [PATCH 09/15] ATR-972: removed the code that increases the priority of code comments over BQL operators and simplify the coloring tagger provider. This code was required for the regex coloring that was already removed. --- .../Coloriser/PXColorizerTaggerProvider.cs | 63 +------------------ 1 file changed, 3 insertions(+), 60 deletions(-) diff --git a/src/Acuminator/Acuminator.Vsix/Coloriser/PXColorizerTaggerProvider.cs b/src/Acuminator/Acuminator.Vsix/Coloriser/PXColorizerTaggerProvider.cs index f52692dc5..d37c49927 100644 --- a/src/Acuminator/Acuminator.Vsix/Coloriser/PXColorizerTaggerProvider.cs +++ b/src/Acuminator/Acuminator.Vsix/Coloriser/PXColorizerTaggerProvider.cs @@ -3,16 +3,13 @@ using System.Collections.Generic; using System.ComponentModel.Composition; using System.Linq; -using System.Threading; using Acuminator.Utilities.Roslyn; using Acuminator.Vsix.Utilities; -using Microsoft.VisualStudio.Language.StandardClassification; using Microsoft.VisualStudio.Text; using Microsoft.VisualStudio.Text.Classification; using Microsoft.VisualStudio.Text.Editor; -using Microsoft.VisualStudio.Text.Formatting; using Microsoft.VisualStudio.Text.Tagging; using Microsoft.VisualStudio.Utilities; @@ -26,15 +23,9 @@ namespace Acuminator.Vsix.Coloriser; [Export(typeof(IViewTaggerProvider))] public class PXColorizerTaggerProvider : IViewTaggerProvider { - private const string TextCategory = "text"; - private readonly IClassificationTypeRegistryService _classificationRegistry; - private readonly IClassificationFormatMapService _classificationFormatMapService; private readonly ITextDocumentFactoryService _textDocumentFactory; - private static readonly object _syncRoot = new object(); - private static volatile bool _isPriorityIncreased; - private readonly Dictionary _codeColoringClassificationTypes; public IClassificationType? this[PXCodeType codeType] => @@ -50,19 +41,13 @@ public class PXColorizerTaggerProvider : IViewTaggerProvider : null; [ImportingConstructor] - public PXColorizerTaggerProvider(IClassificationTypeRegistryService classificationRegistry, - IClassificationFormatMapService classificationFormatMapService, - ITextDocumentFactoryService textDocumentFactory) + public PXColorizerTaggerProvider(IClassificationTypeRegistryService classificationRegistry, ITextDocumentFactoryService textDocumentFactory) { - _classificationRegistry = classificationRegistry; - _classificationFormatMapService = classificationFormatMapService; - _textDocumentFactory = textDocumentFactory; + _classificationRegistry = classificationRegistry; + _textDocumentFactory = textDocumentFactory; _codeColoringClassificationTypes = GetClassificationTypesForAcumaticaCodeElements(_classificationRegistry); _braceTypeByLevel = GetClassificationTypesForAngleBraces(_classificationRegistry); - - IncreaseCommentFormatTypesPriority(_classificationRegistry, _classificationFormatMapService, - _codeColoringClassificationTypes[PXCodeType.BqlParameter]); } public virtual ITagger? CreateTagger(ITextView textView, ITextBuffer textBuffer) @@ -128,46 +113,4 @@ private static Dictionary GetClassificationTypesForAng return braceTypeByLevel; } - - private static void IncreaseCommentFormatTypesPriority(IClassificationTypeRegistryService registry, IClassificationFormatMapService formatMapService, - IClassificationType highestPriorityType) - { - if (_isPriorityIncreased) - return; - - bool lockTaken = false; - Monitor.TryEnter(_syncRoot, ref lockTaken); - - if (!lockTaken) - return; - - try - { - if (_isPriorityIncreased) - return; - - if (formatMapService.GetClassificationFormatMap(category: TextCategory) is IClassificationFormatMap formatMap) - { - IncreaseServiceFormatPriority(formatMap, registry, PredefinedClassificationTypeNames.ExcludedCode, highestPriorityType); - IncreaseServiceFormatPriority(formatMap, registry, PredefinedClassificationTypeNames.Comment, highestPriorityType); - _isPriorityIncreased = true; - } - } - finally - { - Monitor.Exit(_syncRoot); - } - } - - private static void IncreaseServiceFormatPriority(IClassificationFormatMap formatMap, IClassificationTypeRegistryService registry, string formatName, - IClassificationType highestPriorityType) - { - IClassificationType predefinedClassificationType = registry.GetClassificationType(formatName); - IClassificationType artificialClassType = registry.CreateTransientClassificationType(predefinedClassificationType); - TextFormattingRunProperties properties = formatMap.GetExplicitTextProperties(predefinedClassificationType); - - formatMap.AddExplicitTextProperties(artificialClassType, properties, highestPriorityType); - formatMap.SwapPriorities(artificialClassType, predefinedClassificationType); - formatMap.SwapPriorities(highestPriorityType, predefinedClassificationType); - } } From 86c2c78d05dc9b429f636c9f0e1f287eb9fa2dae Mon Sep 17 00:00:00 2001 From: Sergey Nikomarov Date: Fri, 5 Jun 2026 17:18:30 +0200 Subject: [PATCH 10/15] ATR-972: refactoring - renaming on Copilot suggestion --- src/Acuminator/Acuminator.Vsix/Coloriser/Base/PXTaggerBase.cs | 4 ++-- .../Coloriser/TextDocumentDisposedNotification.cs | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Acuminator/Acuminator.Vsix/Coloriser/Base/PXTaggerBase.cs b/src/Acuminator/Acuminator.Vsix/Coloriser/Base/PXTaggerBase.cs index afcd320aa..ad7f57dd4 100644 --- a/src/Acuminator/Acuminator.Vsix/Coloriser/Base/PXTaggerBase.cs +++ b/src/Acuminator/Acuminator.Vsix/Coloriser/Base/PXTaggerBase.cs @@ -52,7 +52,7 @@ protected PXTaggerBase(ITextBuffer buffer, ITextDocumentFactoryService textDocum } _disposedNotification = new TextDocumentDisposedNotification(textDocumentFactory, Buffer); - _disposedNotification.OnCurrentTextDocumentDisposed += CleanupOnTextDocumentDisposed; + _disposedNotification.CurrentTextDocumentDisposed += CleanupOnTextDocumentDisposed; } protected virtual void ColoringSettingChangedHandler(object sender, SettingChangedEventArgs e) @@ -104,7 +104,7 @@ protected virtual void CleanupOnTextDocumentDisposed(object sender, EventArgs e) { Type taggerType = GetType(); Buffer.Properties.RemoveProperty(taggerType); - _disposedNotification.OnCurrentTextDocumentDisposed -= CleanupOnTextDocumentDisposed; + _disposedNotification.CurrentTextDocumentDisposed -= CleanupOnTextDocumentDisposed; if (!SubscribedToSettingsChanges) return; diff --git a/src/Acuminator/Acuminator.Vsix/Coloriser/TextDocumentDisposedNotification.cs b/src/Acuminator/Acuminator.Vsix/Coloriser/TextDocumentDisposedNotification.cs index 2bbb7356a..e3649003d 100644 --- a/src/Acuminator/Acuminator.Vsix/Coloriser/TextDocumentDisposedNotification.cs +++ b/src/Acuminator/Acuminator.Vsix/Coloriser/TextDocumentDisposedNotification.cs @@ -15,7 +15,7 @@ public class TextDocumentDisposedNotification private readonly ITextBuffer _textBuffer; private ITextDocument? _textDocument; - public event EventHandler? OnCurrentTextDocumentDisposed; + public event EventHandler? CurrentTextDocumentDisposed; public TextDocumentDisposedNotification(ITextDocumentFactoryService textDocumentFactory, ITextBuffer textBuffer) { @@ -64,6 +64,6 @@ private void OnTextDocumentDisposed(object sender, TextDocumentEventArgs e) //Dispose of the subscription immediately to always run the handler only once _textDocumentFactory.TextDocumentDisposed -= OnTextDocumentDisposed; - OnCurrentTextDocumentDisposed?.Invoke(this, EventArgs.Empty); + CurrentTextDocumentDisposed?.Invoke(this, EventArgs.Empty); } } From 6b879214986b9286f316deecd38acdd4f67e10bf Mon Sep 17 00:00:00 2001 From: Sergey Nikomarov Date: Fri, 5 Jun 2026 17:35:57 +0200 Subject: [PATCH 11/15] ATR-972: minor cleanups based on AI remarks --- .../Acuminator.Vsix/Coloriser/PXRoslynColorizerTagger.cs | 1 - .../Coloriser/TextDocumentDisposedNotification.cs | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/src/Acuminator/Acuminator.Vsix/Coloriser/PXRoslynColorizerTagger.cs b/src/Acuminator/Acuminator.Vsix/Coloriser/PXRoslynColorizerTagger.cs index e56597e1b..8926e39e9 100644 --- a/src/Acuminator/Acuminator.Vsix/Coloriser/PXRoslynColorizerTagger.cs +++ b/src/Acuminator/Acuminator.Vsix/Coloriser/PXRoslynColorizerTagger.cs @@ -14,7 +14,6 @@ using Microsoft.CodeAnalysis.Text; using Microsoft.VisualStudio.Text; using Microsoft.VisualStudio.Text.Tagging; -using Microsoft.VisualStudio.TextManager.Interop; using ThreadHelper = Microsoft.VisualStudio.Shell.ThreadHelper; diff --git a/src/Acuminator/Acuminator.Vsix/Coloriser/TextDocumentDisposedNotification.cs b/src/Acuminator/Acuminator.Vsix/Coloriser/TextDocumentDisposedNotification.cs index e3649003d..6071dc4ec 100644 --- a/src/Acuminator/Acuminator.Vsix/Coloriser/TextDocumentDisposedNotification.cs +++ b/src/Acuminator/Acuminator.Vsix/Coloriser/TextDocumentDisposedNotification.cs @@ -9,7 +9,7 @@ namespace Acuminator.Vsix.Coloriser; -public class TextDocumentDisposedNotification +internal class TextDocumentDisposedNotification { private readonly ITextDocumentFactoryService _textDocumentFactory; private readonly ITextBuffer _textBuffer; From eae2ecf515ae366b12ffe4fdef9dead1c24a0940 Mon Sep 17 00:00:00 2001 From: Sergey Nikomarov Date: Fri, 5 Jun 2026 17:39:34 +0200 Subject: [PATCH 12/15] ATR-972: added the dispose call to the workspace provider under lock - Copilot remark --- .../Acuminator.Vsix/Coloriser/PXRoslynColorizerTagger.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Acuminator/Acuminator.Vsix/Coloriser/PXRoslynColorizerTagger.cs b/src/Acuminator/Acuminator.Vsix/Coloriser/PXRoslynColorizerTagger.cs index 8926e39e9..9adf80168 100644 --- a/src/Acuminator/Acuminator.Vsix/Coloriser/PXRoslynColorizerTagger.cs +++ b/src/Acuminator/Acuminator.Vsix/Coloriser/PXRoslynColorizerTagger.cs @@ -241,9 +241,10 @@ protected override void CleanupOnTextDocumentDisposed(object sender, EventArgs e if (workspaceToUnsubscribe != null) workspaceToUnsubscribe.WorkspaceChanged -= OnWorkspaceChanged; + + _roslynWorkspaceProvider.Dispose(); } - _roslynWorkspaceProvider.Dispose(); BackgroundTagging?.Dispose(); ClassificationTagsCache.Reset(); OutliningsTagsCache.Reset(); From f2382cdf1c9164b892a81faa2b532aedbb068ff8 Mon Sep 17 00:00:00 2001 From: Sergey Nikomarov Date: Fri, 5 Jun 2026 17:44:19 +0200 Subject: [PATCH 13/15] ATR-972: added comment about text document disposed notification component not being thread safe --- .../Coloriser/TextDocumentDisposedNotification.cs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/Acuminator/Acuminator.Vsix/Coloriser/TextDocumentDisposedNotification.cs b/src/Acuminator/Acuminator.Vsix/Coloriser/TextDocumentDisposedNotification.cs index 6071dc4ec..882f646c8 100644 --- a/src/Acuminator/Acuminator.Vsix/Coloriser/TextDocumentDisposedNotification.cs +++ b/src/Acuminator/Acuminator.Vsix/Coloriser/TextDocumentDisposedNotification.cs @@ -9,6 +9,12 @@ namespace Acuminator.Vsix.Coloriser; +/// +/// A text document disposed notification provider. +/// +/// +/// This component is not thread safe and is supposed to run only on the UI thread. +/// internal class TextDocumentDisposedNotification { private readonly ITextDocumentFactoryService _textDocumentFactory; From d7dbdec49ab0a49a703dd3c2c137358eff7614bb Mon Sep 17 00:00:00 2001 From: Sergey Nikomarov Date: Fri, 5 Jun 2026 17:49:45 +0200 Subject: [PATCH 14/15] ATR-972: Copilot suggestion - always cancel tagging on the call to the BackgoundTagging.Dispose call --- .../Acuminator.Vsix/Coloriser/AsyncTagging/BackgroundTagging.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Acuminator/Acuminator.Vsix/Coloriser/AsyncTagging/BackgroundTagging.cs b/src/Acuminator/Acuminator.Vsix/Coloriser/AsyncTagging/BackgroundTagging.cs index 72d583c5d..0e650fdf1 100644 --- a/src/Acuminator/Acuminator.Vsix/Coloriser/AsyncTagging/BackgroundTagging.cs +++ b/src/Acuminator/Acuminator.Vsix/Coloriser/AsyncTagging/BackgroundTagging.cs @@ -80,7 +80,7 @@ public void Dispose() return; _isDisposed = true; - CancelTagging(); + _cancellationTokenSource.Cancel(); _cancellationTokenSource.Dispose(); } From 1832f1b55b216fcd0b8191bf4d2bac75fc970043 Mon Sep 17 00:00:00 2001 From: Sergey Nikomarov Date: Fri, 5 Jun 2026 18:13:42 +0200 Subject: [PATCH 15/15] ATR-972: added safe CTS Cancel call in Dispose with logging of exceptions --- .../AsyncTagging/BackgroundTagging.cs | 29 +++++++++++++++++-- 1 file changed, 27 insertions(+), 2 deletions(-) diff --git a/src/Acuminator/Acuminator.Vsix/Coloriser/AsyncTagging/BackgroundTagging.cs b/src/Acuminator/Acuminator.Vsix/Coloriser/AsyncTagging/BackgroundTagging.cs index 0e650fdf1..7ab7f900e 100644 --- a/src/Acuminator/Acuminator.Vsix/Coloriser/AsyncTagging/BackgroundTagging.cs +++ b/src/Acuminator/Acuminator.Vsix/Coloriser/AsyncTagging/BackgroundTagging.cs @@ -6,6 +6,7 @@ using System.Threading.Tasks; using Acuminator.Utilities.Common; +using Acuminator.Vsix.Logger; using Shell = Microsoft.VisualStudio.Shell; @@ -80,8 +81,32 @@ public void Dispose() return; _isDisposed = true; - _cancellationTokenSource.Cancel(); - _cancellationTokenSource.Dispose(); + + try + { + _cancellationTokenSource.Cancel(); + } + catch (OperationCanceledException) + { + } + catch (AggregateException aggregateException) + { + var flattened = aggregateException.Flatten(); + var exceptionsToLog = flattened.InnerExceptions.Where(ex => ex is not OperationCanceledException); + + foreach (var exception in exceptionsToLog) + { + AcuminatorVSPackage.Instance?.AcuminatorLogger?.LogException(exception, logOnlyFromAcuminatorAssemblies: false, LogMode.Warning); + } + } + catch (Exception ex) + { + AcuminatorVSPackage.Instance?.AcuminatorLogger?.LogException(ex, logOnlyFromAcuminatorAssemblies: false, LogMode.Warning); + } + finally + { + _cancellationTokenSource.Dispose(); + } } private static Task AfterTaggingActionAsync(Task taggingTask, PXRoslynColorizerTagger tagger, CancellationToken cancellationToken)