From 946b04d97de4c51844c7fa397e90f7dbe57f5e18 Mon Sep 17 00:00:00 2001 From: Tamas Balog Date: Wed, 30 Nov 2022 07:07:57 +0100 Subject: [PATCH 1/9] Add text tagging and colored underlining of Rosie violations --- src/Extension/Extension.csproj | 17 +- src/Extension/ExtensionPackage.cs | 37 +-- .../Annotation/RosieViolationSquiggleTag.cs | 21 ++ .../RosieViolationSquiggleTagger.cs | 210 +++++++++++++++ .../RosieViolationSquiggleTaggerProvider.cs | 54 ++++ .../Rosie/Annotation/RosieViolationTag.cs | 26 ++ .../Rosie/Annotation/RosieViolationTagger.cs | 243 ++++++++++++++++++ .../RosieViolationTaggerProvider.cs | 52 ++++ src/Extension/Rosie/Annotation/StringUtils.cs | 18 ++ src/Extension/Rosie/IRosieClient.cs | 13 +- src/Extension/Rosie/Model/RosiePosition.cs | 25 +- src/Extension/Rosie/RosieClient.cs | 105 ++++++-- src/Extension/Rosie/RosieRulesCache.cs | 27 +- src/Extension/Rosie/RosieSeverities.cs | 14 + src/Extension/SnippetFormats/LanguageUtils.cs | 28 +- 15 files changed, 807 insertions(+), 83 deletions(-) create mode 100644 src/Extension/Rosie/Annotation/RosieViolationSquiggleTag.cs create mode 100644 src/Extension/Rosie/Annotation/RosieViolationSquiggleTagger.cs create mode 100644 src/Extension/Rosie/Annotation/RosieViolationSquiggleTaggerProvider.cs create mode 100644 src/Extension/Rosie/Annotation/RosieViolationTag.cs create mode 100644 src/Extension/Rosie/Annotation/RosieViolationTagger.cs create mode 100644 src/Extension/Rosie/Annotation/RosieViolationTaggerProvider.cs create mode 100644 src/Extension/Rosie/Annotation/StringUtils.cs create mode 100644 src/Extension/Rosie/RosieSeverities.cs diff --git a/src/Extension/Extension.csproj b/src/Extension/Extension.csproj index 2e856fe..fcd26c5 100644 --- a/src/Extension/Extension.csproj +++ b/src/Extension/Extension.csproj @@ -67,11 +67,21 @@ + + + + + + + + + + + - @@ -82,8 +92,10 @@ + + @@ -157,6 +169,9 @@ 17.3.198 + + 17.3.32803.143 + 17.3.198 diff --git a/src/Extension/ExtensionPackage.cs b/src/Extension/ExtensionPackage.cs index 75eaf4b..cb2de2a 100644 --- a/src/Extension/ExtensionPackage.cs +++ b/src/Extension/ExtensionPackage.cs @@ -7,9 +7,8 @@ using Extension.Rosie; using Task = System.Threading.Tasks.Task; using Extension.SnippetSearch; -using Microsoft.VisualStudio.Shell.Events; using Microsoft.VisualStudio; -using System.Threading.Tasks; +using SolutionEvents = Microsoft.VisualStudio.Shell.Events.SolutionEvents; namespace Extension { @@ -29,8 +28,6 @@ public sealed class ExtensionPackage : AsyncPackage /// public const string PackageGuidString = "e8d2d8f8-96dc-4c92-bb81-346b4d2318e4"; - private CancellationToken _cancellationToken; - /// /// Initializes a new instance of the class. /// @@ -49,15 +46,7 @@ public ExtensionPackage() /// A task representing the async work of package initialization, or an already completed task if there is none. Do not return null from this method. protected override async Task InitializeAsync(CancellationToken cancellationToken, IProgress progress) { - _cancellationToken = cancellationToken; - var isSolutionLoaded = await IsSolutionLoadedAsync(); - - if (isSolutionLoaded) - InitializeRulesCache(); - - //Inits the cache only after a solution is loaded completely - SolutionEvents.OnAfterBackgroundSolutionLoadComplete += InitializeRulesCache; - SolutionEvents.OnAfterCloseSolution += DisposeRulesCache; + SolutionEvents.OnAfterCloseSolution += CleanupCachesAndServices; // When initialized asynchronously, the current thread may be a background thread at this point. // Do any initialization that requires the UI thread after switching to the UI thread. @@ -65,18 +54,7 @@ protected override async Task InitializeAsync(CancellationToken cancellationToke await SnippetSearchMenuCommand.InitializeAsync(this); } - //See https://github.com/madskristensen/SolutionLoadSample - private async Task IsSolutionLoadedAsync() - { - await JoinableTaskFactory.SwitchToMainThreadAsync(); - var vsSolution = await GetServiceAsync(typeof(SVsSolution)) as IVsSolution; - - ErrorHandler.ThrowOnFailure(vsSolution.GetProperty((int)__VSPROPID.VSPROPID_IsSolutionOpen, out object value)); - - return value is bool isSolutionOpen && isSolutionOpen; - } - - public override IVsAsyncToolWindowFactory GetAsyncToolWindowFactory(Guid toolWindowType) + public override IVsAsyncToolWindowFactory GetAsyncToolWindowFactory(Guid toolWindowType) { ThreadHelper.ThrowIfNotOnUIThread(); if (toolWindowType == typeof(SnippetSearch.SearchWindow).GUID) @@ -97,14 +75,7 @@ protected override string GetToolWindowTitle(Type toolWindowType, int id) return base.GetToolWindowTitle(toolWindowType, id); } - private async void InitializeRulesCache(object sender = null, EventArgs e = null) - { - //Switching back to main thread due to RosieRulesCache.StartPolling() - await JoinableTaskFactory.SwitchToMainThreadAsync(_cancellationToken); - RosieRulesCache.Initialize(); - } - - private static void DisposeRulesCache(object sender, EventArgs e) + private static void CleanupCachesAndServices(object sender, EventArgs e) { RosieRulesCache.Dispose(); } diff --git a/src/Extension/Rosie/Annotation/RosieViolationSquiggleTag.cs b/src/Extension/Rosie/Annotation/RosieViolationSquiggleTag.cs new file mode 100644 index 0000000..83b3338 --- /dev/null +++ b/src/Extension/Rosie/Annotation/RosieViolationSquiggleTag.cs @@ -0,0 +1,21 @@ +using Microsoft.VisualStudio.Text.Tagging; + +namespace Extension.Rosie.Annotation +{ + /// + /// Custom tag implementation for squiggle tagging. + ///
+ /// This tag is not responsible for carrying information of a violation + /// returned from the Rosie server, only for providing squiggle information. + /// For information on violations, is used. + ///
+ /// Instances of this class are created by via + ///
+ public class RosieViolationSquiggleTag : ErrorTag + { + public RosieViolationSquiggleTag(string squiggleType, object toolTipContent) : base(squiggleType, + toolTipContent) + { + } + } +} \ No newline at end of file diff --git a/src/Extension/Rosie/Annotation/RosieViolationSquiggleTagger.cs b/src/Extension/Rosie/Annotation/RosieViolationSquiggleTagger.cs new file mode 100644 index 0000000..5bb17d9 --- /dev/null +++ b/src/Extension/Rosie/Annotation/RosieViolationSquiggleTagger.cs @@ -0,0 +1,210 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel.Composition; +using System.Windows.Media; +using Microsoft.VisualStudio.Text; +using Microsoft.VisualStudio.Text.Adornments; +using Microsoft.VisualStudio.Text.Classification; +using Microsoft.VisualStudio.Text.Tagging; +using Microsoft.VisualStudio.Utilities; + +namespace Extension.Rosie.Annotation +{ + /// + /// Provides tagging information for an , so that a span of text can be + /// marked/tagged with squiggles for an already available Rosie violation and . + ///
+ /// In order for the coloring and visual formatting to take effect, both an + /// and an instance must be exported for the same name for each color. + /// There are four pairs of such classes defined here for the four severity levels available in Rosie. + ///
+ /// Instances of this class are created by . + ///
+ internal class RosieViolationSquiggleTagger : ITagger, IDisposable + { + #region Severity based squiggle color and format definitions + + private const string RosieViolationCritical = "Rosie Violation Critical"; + private const string RosieViolationError = "Rosie Violation Error"; + private const string RosieViolationWarning = "Rosie Violation Warning"; + private const string RosieViolationInfo = "Rosie Violation Informational"; + + /// + /// Defines the squiggle color for violations with 'critical' Rosie severity. + /// + [Export(typeof(EditorFormatDefinition))] + [Name(RosieViolationCritical)] + [UserVisible(true)] + internal class RosieViolationCriticalFormatDefinition : EditorFormatDefinition + { + public RosieViolationCriticalFormatDefinition() + { + ForegroundColor = Colors.Red; + BackgroundCustomizable = false; + DisplayName = RosieViolationCritical; + } + } + + /// + /// Defines the squiggle color for violations with 'error' Rosie severity. + /// + [Export(typeof(EditorFormatDefinition))] + [Name(RosieViolationError)] + [UserVisible(true)] + internal class RosieViolationErrorFormatDefinition : EditorFormatDefinition + { + public RosieViolationErrorFormatDefinition() + { + ForegroundColor = Colors.Magenta; + BackgroundCustomizable = false; + DisplayName = RosieViolationError; + } + } + + /// + /// Defines the squiggle color for violations with 'warning' Rosie severity. + /// + [Export(typeof(EditorFormatDefinition))] + [Name(RosieViolationWarning)] + [UserVisible(true)] + internal class RosieViolationWarningFormatDefinition : EditorFormatDefinition + { + public RosieViolationWarningFormatDefinition() + { + ForegroundColor = Colors.Orange; + BackgroundCustomizable = false; + DisplayName = RosieViolationWarning; + } + } + + /// + /// Defines the squiggle color for violations with 'informational' and 'unknown' Rosie severities. + /// + [Export(typeof(EditorFormatDefinition))] + [Name(RosieViolationInfo)] + [UserVisible(true)] + internal class RosieViolationInfoFormatDefinition : EditorFormatDefinition + { + public RosieViolationInfoFormatDefinition() + { + ForegroundColor = Colors.White; + BackgroundCustomizable = false; + DisplayName = RosieViolationInfo; + } + } + + [Export(typeof(ErrorTypeDefinition))] [Name(RosieViolationCritical)] + private readonly ErrorTypeDefinition RosieViolationCriticalTypeDefinition = null; + + [Export(typeof(ErrorTypeDefinition))] [Name(RosieViolationError)] + private readonly ErrorTypeDefinition RosieViolationErrorTypeDefinition = null; + + [Export(typeof(ErrorTypeDefinition))] [Name(RosieViolationWarning)] + private readonly ErrorTypeDefinition RosieViolationWarningTypeDefinition = null; + + [Export(typeof(ErrorTypeDefinition))] [Name(RosieViolationInfo)] + private readonly ErrorTypeDefinition RosieViolationInformatioalTypeDefinition = null; + + #endregion + + /// + /// Called when there was a change in the edited document, to signal that tagging must be updated. + ///
+ /// Instantiated automatically by the VS platform. + ///
+ public event EventHandler TagsChanged; + + /// + /// It aggregates all s in the inspected text buffer. + ///
+ /// Based on the returned tags, this tagger can create the s, + /// so that the colored squiggles can be applied in the text view. + ///
+ private readonly ITagAggregator _rosieViolationAggregator; + + /// + /// The text buffer in which the squiggles are displayed. + /// + private readonly ITextBuffer _sourceBuffer; + + private bool _isDisposed; + + public RosieViolationSquiggleTagger(ITextBuffer sourceBuffer, + ITagAggregator rosieViolationAggregator) + { + _sourceBuffer = sourceBuffer; + _rosieViolationAggregator = rosieViolationAggregator; + _rosieViolationAggregator.BatchedTagsChanged += OnTagsChanged; + } + + /// + /// Tt signals this tagger, thus all associated s to update the tags. + /// + /// the sender of the event + /// the event arguments + private void OnTagsChanged(object sender, EventArgs e) + { + if (!_isDisposed) + TagsChanged?.Invoke(this, + new SnapshotSpanEventArgs(new SnapshotSpan(_sourceBuffer.CurrentSnapshot, + new Span(0, _sourceBuffer.CurrentSnapshot.Length - 1)))); + } + + /// + /// Creates new s for each + /// whose spans intersect with the provided span collection. + ///
+ /// The tag is created with the appropriate severity and message from the retrieved . + ///
+ public IEnumerable> GetTags(NormalizedSnapshotSpanCollection spans) + { + if (spans.Count == 0 || _isDisposed) + yield break; + + var snapshot = spans[0].Snapshot; + foreach (var violationTagSpan in _rosieViolationAggregator.GetTags(spans)) + { + var spanCollection = violationTagSpan.Span.GetSpans(snapshot); + if (spanCollection.Count == 1) + { + var errorSpan = spanCollection[0]; + yield return new TagSpan( + errorSpan, + new RosieViolationSquiggleTag( + GetSquiggleTypeForRosieSeverity(violationTagSpan.Tag.Annotation.Severity), + violationTagSpan.Tag.Annotation.Message)); + } + } + } + + /// + /// Maps the provided Rosie severity (see ) to its respective + /// editor format definition name. + ///
+ /// For informational and unknown severities, it returns . + ///
+ /// The Rosie severity + /// The respective format definition name. + private static string GetSquiggleTypeForRosieSeverity(string severity) + { + if (StringUtils.AreEqualIgnoreCase(RosieSeverities.Critical, severity)) + return RosieViolationCritical; + if (StringUtils.AreEqualIgnoreCase(RosieSeverities.Error, severity)) + return RosieViolationError; + if (StringUtils.AreEqualIgnoreCase(RosieSeverities.Warning, severity)) + return RosieViolationWarning; + + return RosieViolationInfo; + } + + public void Dispose() + { + if (!_isDisposed) + { + _isDisposed = true; + _rosieViolationAggregator.BatchedTagsChanged -= OnTagsChanged; + _rosieViolationAggregator.Dispose(); + } + } + } +} \ No newline at end of file diff --git a/src/Extension/Rosie/Annotation/RosieViolationSquiggleTaggerProvider.cs b/src/Extension/Rosie/Annotation/RosieViolationSquiggleTaggerProvider.cs new file mode 100644 index 0000000..d3b2efa --- /dev/null +++ b/src/Extension/Rosie/Annotation/RosieViolationSquiggleTaggerProvider.cs @@ -0,0 +1,54 @@ +using System.ComponentModel.Composition; +using Microsoft.VisualStudio.Text; +using Microsoft.VisualStudio.Text.Editor; +using Microsoft.VisualStudio.Text.Tagging; +using Microsoft.VisualStudio.Utilities; + +namespace Extension.Rosie.Annotation +{ + /// + /// Creates instances for and + /// pairs, that in turn can create s. + /// + [Export(typeof(IViewTaggerProvider))] + [ContentType("any")] + [TagType(typeof(RosieViolationSquiggleTag))] + //Restricts the creation of this provider to certain text view roles + [TextViewRole(PredefinedTextViewRoles.Document)] + [TextViewRole(PredefinedTextViewRoles.Editable)] + [TextViewRole(PredefinedTextViewRoles.PrimaryDocument)] + internal class RosieViolationSquiggleTaggerProvider : IViewTaggerProvider + { + [Import] internal IViewTagAggregatorFactoryService TagAggregatorFactory { get; set; } + + /// + /// Creates a for the specified view and buffer. + ///
+ /// The tagger instance is saved in the textView's Properties, thus, if there is + /// such tagger instance already created, we don't create another one, but return the cached tagger instance. + ///
+ /// A RosieViolationSquiggleTagger is created only when the file being tagged is actually supported by Rosie. + ///
+ public ITagger CreateTagger(ITextView textView, ITextBuffer buffer) where T : ITag + { + // This provider is only interested in creating tagging for the top buffer, + // and not for IErrorTags, so that we can filter out, among others, ones for RosieViolationTags. + if (buffer != textView.TextBuffer || typeof(T) != typeof(IErrorTag)) + return null; + + if (!textView.Properties.TryGetProperty(typeof(RosieViolationSquiggleTagger), + out RosieViolationSquiggleTagger squiggleTagger)) + { + //Create a tagger only when the language of the current file is supported by Rosie + if (RosieClient.IsLanguageOfFileSupported(buffer.GetFileName())) + { + squiggleTagger = new RosieViolationSquiggleTagger(buffer, + TagAggregatorFactory.CreateTagAggregator(textView)); + textView.Properties[typeof(RosieViolationSquiggleTagger)] = squiggleTagger; + } + } + + return squiggleTagger as ITagger; + } + } +} diff --git a/src/Extension/Rosie/Annotation/RosieViolationTag.cs b/src/Extension/Rosie/Annotation/RosieViolationTag.cs new file mode 100644 index 0000000..675038c --- /dev/null +++ b/src/Extension/Rosie/Annotation/RosieViolationTag.cs @@ -0,0 +1,26 @@ +using Extension.Rosie.Model; +using Microsoft.VisualStudio.Text.Tagging; + +namespace Extension.Rosie.Annotation +{ + /// + /// Custom tag implementation for text tagging, that also stores a instance + /// to be used in code analysis and related lightbulb actions. + ///
+ /// This tag is not user-visible, it tags a span of text in a text buffer behind the scenes. + ///
+ /// It is not responsible for providing squiggle information, only for carrying information of a violation + /// returned from the Rosie server. The squiggle information is provided by . + ///
+ /// Instances of this class are created by via + ///
+ public class RosieViolationTag : ITag + { + public RosieAnnotation Annotation { get; } + + public RosieViolationTag(RosieAnnotation annotation) + { + Annotation = annotation; + } + } +} \ No newline at end of file diff --git a/src/Extension/Rosie/Annotation/RosieViolationTagger.cs b/src/Extension/Rosie/Annotation/RosieViolationTagger.cs new file mode 100644 index 0000000..92efb97 --- /dev/null +++ b/src/Extension/Rosie/Annotation/RosieViolationTagger.cs @@ -0,0 +1,243 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Extension.Rosie.Model; +using Microsoft.VisualStudio; +using Microsoft.VisualStudio.Shell; +using Microsoft.VisualStudio.Shell.Interop; +using Microsoft.VisualStudio.Text; +using Microsoft.VisualStudio.Text.Editor; +using Microsoft.VisualStudio.Text.Tagging; + +namespace Extension.Rosie.Annotation +{ + /// + /// Provides tagging information for an , so that a span of text can be + /// marked/tagged with information about a code analysis violation returned from the Rosie server. + ///
+ /// Instances of this classes are created by . + ///
+ public class RosieViolationTagger : ITagger, IDisposable + { + /// + /// Called when there was a change in the edited document, to signal that tagging must be updated. + ///
+ /// Instantiated automatically by the VS platform. + ///
+ public event EventHandler TagsChanged; + + /// + /// We wait for this amount of time after each edit in a text buffer, and if no edit happened during that + /// interval, only then we send a request to Rosie. + /// + private const double DocumentModificationWaitIntervalInMillis = 500; + + /// + /// The text buffer on whose changes requests to Rosie, and the persistence of Rosie annotations are performed. + /// + private readonly ITextBuffer _sourceBuffer; + + /// + /// Stores the timestamp (in milliseconds) of the text buffer's last modification. + ///
+ /// We use this timestamp instead of the file's last write time, because the last write time requires the file to be saved, + /// not just edited, to change. + ///
+ private long _fileLastModificationTime; + + /// + /// Stores the Rosie annotations, and violation details, returned from Rosie. + ///
+ /// Since there is a conceptual difference between how Rosie works and how tagging works, we have to do some caching. + ///
+ /// While Rosie is called for an entire file, ITagger.GetTags() is called for a span/range within a file, + /// that can be be a span within a file, or the span of the whole file. + /// So, during the creation of this tagger, and after each edit in a document (in an ITextBuffer, and if the user hasn't typed for + /// at least 500ms), we send a Rosie request for the whole file, cache it in this field, and if there is no reason to re-analyze + /// the file, we provide the information for tags from this field. + ///
+ private IList _annotations = RosieClient.NoAnnotation; + + private bool _isDisposed; + + public RosieViolationTagger(ITextView textView, ITextBuffer sourceBuffer) + { + _sourceBuffer = sourceBuffer; + + //Preparing this tagger for code analysis only when all the Changed* event handlers have finished execution + _sourceBuffer.PostChanged += RequestCodeAnalysis; + + //Remove event handler when the text view gets closed, so that code analysis doesn't happen on a closed view + textView.Closed += (sender, args) => _sourceBuffer.PostChanged -= RequestCodeAnalysis; + + //Sends the first request to Rosie for this file, + //so that we can display potential issues right after a file is opened/displayed. + ThreadHelper.JoinableTaskFactory.Run(async () => + { + if (RosieClientProvider.TryGetClient(out var client)) + _annotations = await client.GetAnnotations(_sourceBuffer); + }); + } + + /// + /// It requests a code analysis, sends a request to Rosie for the current file, and caches its results in + /// if the user hasn't typed anything in the last . + ///
+ /// When the violations are received from the server, it signals this tagger, thus all associated s + /// to update the tags. + ///
+ /// Notes on asynchronicity: + ///
+ /// Although async void methods are advised for only certain types of usage, due to its error handling mechanism, + /// there doesn't seem to be a different way to handle this event synchronously while still using await. + ///
+ /// See details about async void methods. + ///
+ /// The wait logic was adopted from the Codiga VSCode plugin. + ///
+ /// the sender of the event + /// the event arguments + private async void RequestCodeAnalysis(object sender, EventArgs e) + { + // Save the timestamp, so that other threads and analysis requests can see it. + var fileLastModificationTime = DateTimeOffset.Now.ToUnixTimeMilliseconds(); + _fileLastModificationTime = fileLastModificationTime; + + // Wait for some time. During that time, the user might type another key that triggers further buffer changed events, + // that also update the saved timestamp. + var delay = Task.Delay(TimeSpan.FromMilliseconds(DocumentModificationWaitIntervalInMillis), + new CancellationToken()); + try + { + await delay; + } + catch (TaskCanceledException) + { + return; + } + + // Get the actual saved timestamp, and check if it is the one we called the function with. + // If yes, and it hasn't changed (the user finished typing for at least the wait time), request code analysis. + if (_fileLastModificationTime == fileLastModificationTime) + { + if (RosieClientProvider.TryGetClient(out var client)) + { + var textBuffer = sender as ITextBuffer; + _annotations = await client.GetAnnotations(textBuffer); + + //Signals a tag update for the whole file's range + TagsChanged.Invoke(this, + new SnapshotSpanEventArgs(new SnapshotSpan(textBuffer.CurrentSnapshot, + new Span(0, textBuffer.CurrentSnapshot.Length - 1)))); + } + } + } + + public /*override*/ IEnumerable> GetTags(NormalizedSnapshotSpanCollection spans) + { + return ThreadHelper.JoinableTaskFactory.Run(async () => await GetTagsAsync(spans)); + } + + /// + /// Returns the tag spans for the requested span. It creates new s for each + ///
+ /// It calculates the positions in the text buffer, based on the line and column coordinates in each Rosie violation, + /// so that later, the error squiggles can be display in their correct positions and ranges. + ///
+ private async Task>> GetTagsAsync(NormalizedSnapshotSpanCollection spans) + { + if (spans.Count == 0 || _isDisposed) + return Enumerable.Empty>(); + + var span = spans[0]; + var snapshot = span.Snapshot; + + //Temporarily switching back to main thread due to RosieRulesCache.StartPolling() + var fileName = await Task.Run(async () => + { + await ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync(); + return snapshot.TextBuffer.GetFileName(); + }); + + //If there is no file to create tags in, return no tags + if (fileName == null) + return Enumerable.Empty>(); + + var tagSpans = new List>(); + + //Iterate over all Rosie annotations/violations and create the proper tags for each of them. + foreach (var annotation in _annotations) + { + int annotationStart; + int annotationEnd; + try + { + annotationStart = annotation.Start.GetOffset(snapshot.TextBuffer); + annotationEnd = annotation.End.GetOffset(snapshot.TextBuffer); + } + catch (ArgumentOutOfRangeException) + { + //For example, when deleting an entire line in the document, and the not-yet-updated violation is on the last line of the document, + //the Line of RosiePosition can be greater than the updated line count of the text buffer. + continue; + } + + //If the spans don't intersect, don't create a tag + //span: |--------| + //anno: |--------| + //anno: |--------| + if (annotationStart > span.End.Position || annotationEnd < span.Start.Position) + continue; + + if (annotationStart <= span.Start.Position) + { + //span: |--------------| + //anno: |--------------| + //anno: |------------------| + //anno: |----------------| + //anno: |------------------| + if (annotationEnd >= span.End.Position) + tagSpans.Add(new TagSpan( + new SnapshotSpan(snapshot, span.Start.Position, span.Length), + new RosieViolationTag(annotation))); + //span: |--------------| + //anno: |--------------| + //anno: |------------| + else + tagSpans.Add(new TagSpan(new SnapshotSpan(snapshot, span.Start.Position, + annotationEnd - span.Start.Position), new RosieViolationTag(annotation))); + } + else + { + //span: |--------------| + //anno: |-----------| + //anno: |----------------| + if (annotationEnd >= span.End.Position) + tagSpans.Add(new TagSpan(new SnapshotSpan(snapshot, + annotationStart, + span.End.Position - annotationStart), new RosieViolationTag(annotation))); + //span: |--------------| + //anno: |--------| + else + tagSpans.Add(new TagSpan(new SnapshotSpan(snapshot, + annotationStart, + annotationEnd - annotationStart), + new RosieViolationTag(annotation))); + } + } + + return tagSpans; + } + + public void Dispose() + { + if (!_isDisposed) + { + _sourceBuffer.PostChanged -= RequestCodeAnalysis; + _isDisposed = true; + } + } + } +} \ No newline at end of file diff --git a/src/Extension/Rosie/Annotation/RosieViolationTaggerProvider.cs b/src/Extension/Rosie/Annotation/RosieViolationTaggerProvider.cs new file mode 100644 index 0000000..4181db4 --- /dev/null +++ b/src/Extension/Rosie/Annotation/RosieViolationTaggerProvider.cs @@ -0,0 +1,52 @@ +using System.ComponentModel.Composition; +using Microsoft.VisualStudio.Text; +using Microsoft.VisualStudio.Text.Editor; +using Microsoft.VisualStudio.Text.Tagging; +using Microsoft.VisualStudio.Utilities; + +namespace Extension.Rosie.Annotation +{ + /// + /// Creates instances for and + /// pairs, that in turn can create s. + /// + [Export(typeof(IViewTaggerProvider))] + [ContentType("any")] + [TagType(typeof(RosieViolationTag))] + //Restricts the creation of this provider to certain text view roles + [TextViewRole(PredefinedTextViewRoles.Document)] + [TextViewRole(PredefinedTextViewRoles.Editable)] + // [TextViewRole(PredefinedTextViewRoles.Analyzable)] + [TextViewRole(PredefinedTextViewRoles.PrimaryDocument)] + internal class RosieViolationTaggerProvider : IViewTaggerProvider + { + /// + /// Creates a for the specified view and buffer. + ///
+ /// The tagger instance is saved in the textView's Properties, thus, if there is + /// such tagger instance already created, we don't create another one, but return the cached tagger instance. + ///
+ /// A RosieViolationTagger is created only when the file being tagged is actually supported by Rosie. + ///
+ public ITagger CreateTagger(ITextView textView, ITextBuffer buffer) where T : ITag + { + // This provider is only interested in creating tagging for the top buffer, + // and for IErrorTags, so that we can filter out, among others, ones for RosieViolationSquiggleTags. + if (textView.TextBuffer != buffer || typeof(T) == typeof(IErrorTag)) + return null; + + if (!textView.Properties.TryGetProperty(typeof(RosieViolationTagger), + out RosieViolationTagger violationTagger)) + { + //Create a tagger only when the language of the current file is supported by Rosie + if (RosieClient.IsLanguageOfFileSupported(buffer.GetFileName())) + { + violationTagger = new RosieViolationTagger(textView, buffer); + textView.Properties[typeof(RosieViolationTagger)] = violationTagger; + } + } + + return violationTagger as ITagger; + } + } +} diff --git a/src/Extension/Rosie/Annotation/StringUtils.cs b/src/Extension/Rosie/Annotation/StringUtils.cs new file mode 100644 index 0000000..a7ccc29 --- /dev/null +++ b/src/Extension/Rosie/Annotation/StringUtils.cs @@ -0,0 +1,18 @@ +using System; + +namespace Extension.Rosie.Annotation +{ + internal static class StringUtils + { + /// + /// Returns whether the two strings are equal ignoring their cases. + /// + /// The first string to compare. + /// The second string to compare. + /// True if the strings are equal ignoring cases, false otherwise. + internal static bool AreEqualIgnoreCase(string s1, string s2) + { + return s1.Equals(s2, StringComparison.OrdinalIgnoreCase); + } + } +} diff --git a/src/Extension/Rosie/IRosieClient.cs b/src/Extension/Rosie/IRosieClient.cs index c5be939..d765e5a 100644 --- a/src/Extension/Rosie/IRosieClient.cs +++ b/src/Extension/Rosie/IRosieClient.cs @@ -1,20 +1,21 @@ using System.Collections.Generic; using System.Threading.Tasks; using Extension.Rosie.Model; +using Microsoft.VisualStudio.Text; namespace Extension.Rosie { /// - /// Service for retrieving Rosie specific information from the Codiga API. + /// API for retrieving Rosie specific information from the Codiga API. /// public interface IRosieClient { /// - /// Returns the annotations from the Codiga API based on the argument file, based on which code annotation - /// will be applied in the currently selected and active editor. + /// Returns the annotations from the Codiga API based on the argument textBuffer's content, + /// based on which code annotation is applied in the provider text buffer. /// - /// the file to query Rosie information for - /// - Task> GetAnnotations(string file); + /// contains the file content to query Rosie information for + /// The list of violation information from Rosie + Task> GetAnnotations(ITextBuffer textBuffer); } } diff --git a/src/Extension/Rosie/Model/RosiePosition.cs b/src/Extension/Rosie/Model/RosiePosition.cs index 6e84fd4..976d979 100644 --- a/src/Extension/Rosie/Model/RosiePosition.cs +++ b/src/Extension/Rosie/Model/RosiePosition.cs @@ -7,6 +7,9 @@ namespace Extension.Rosie.Model /// public class RosiePosition { + /// + /// The line returned from Codiga is 1-based, while the VS ITextBuffer is 0-based. + /// public int Line { get; set; } public int Col { get; set; } @@ -14,24 +17,22 @@ public class RosiePosition /// Returns the position offset within the Document of the argument Editor. /// /// the editor in which the offset is calculated - /// public int GetOffset(ITextBuffer textBuffer) { - return textBuffer.CurrentSnapshot.GetLineFromLineNumber(Line - 1).Start.Position + AdjustColumnOffset(Col); + return textBuffer.CurrentSnapshot.GetLineFromLineNumber(AdjustOffset(Line)).Start.Position + AdjustOffset(Col); } - /// - /// Adjusts the column offset by -1 since the column index returned by Codiga is 1-based, while the IDE editor is 0-based. - /// - /// It doesn't adjust the offset if it is 0, so at the beginning of a line. - /// - /// the offset to adjust - /// - private int AdjustColumnOffset(int columnOffset) + /// + /// Adjusts the column offset by -1 since the column index returned by Codiga is 1-based, while the IDE editor is 0-based. + ///
+ /// It doesn't adjust the offset if it is 0, so at the beginning of a line. + ///
+ /// the offset to adjust + private int AdjustOffset(int offset) { - return columnOffset != 0 ? (columnOffset - 1) : columnOffset; + return offset != 0 ? offset - 1 : offset; } - + public override bool Equals(object obj) { return obj is RosiePosition position && diff --git a/src/Extension/Rosie/RosieClient.cs b/src/Extension/Rosie/RosieClient.cs index fdbb295..9806f7f 100644 --- a/src/Extension/Rosie/RosieClient.cs +++ b/src/Extension/Rosie/RosieClient.cs @@ -6,12 +6,15 @@ using System.Text; using System.Text.Json; using System.Text.RegularExpressions; +using System.Threading; using System.Threading.Tasks; using Community.VisualStudio.Toolkit; using Extension.Rosie.Model; using Extension.SnippetFormats; using Microsoft.VisualStudio.Shell; using Microsoft.VisualStudio.Shell.Interop; +using Microsoft.VisualStudio.Text; +using Microsoft.VisualStudio.Threading; namespace Extension.Rosie { @@ -20,10 +23,11 @@ namespace Extension.Rosie /// public class RosieClient : IRosieClient { - private const string RosiePostUrl = "https://analysis.codiga.io/analyze"; - private static readonly Regex AppVersionRegex = new Regex(@"(\d+)\.(\d+)\.\d+.*"); - private static readonly IList NoAnnotation = new List(); - + /// + /// An empty list of RosieAnnotations, so that when we need an empty list of them, we don't need to create a new list each time. + /// + public static readonly IList NoAnnotation = new List(); + /// /// Languages currently supported by Rosie. ///
@@ -32,6 +36,11 @@ public class RosieClient : IRosieClient private static readonly IList SupportedLanguages = new List { LanguageUtils.LanguageEnumeration.Python }; + /// + /// Matches for example '17.4.33103.184 D17.4' where majorVersion is 17, minorVersion is 4. + /// + private static readonly Regex AppVersionRegex = new Regex(@"(?\d+)\.(?\d+)\.\d+.*"); + /// /// In order to create the JSON property names as e.g. 'filename' as the server requires it, /// and not 'Filename' as they are required to be named in the model classes. @@ -41,40 +50,50 @@ public class RosieClient : IRosieClient PropertyNamingPolicy = JsonNamingPolicy.CamelCase }; + /// + /// Essentially, to handle the opposite of SerializerOptions, to be able to deserialize + /// into the fields of using the uppercase field names. + /// private static readonly JsonSerializerOptions DeserializerOptions = new JsonSerializerOptions { IncludeFields = true, PropertyNameCaseInsensitive = true }; - public async Task> GetAnnotations(string file) + private const string RosiePostUrl = "https://analysis.codiga.io/analyze"; + + public /*override*/ async Task> GetAnnotations(ITextBuffer textBuffer) { - if (!File.Exists(file)) + if (textBuffer.GetFileName() == null || !File.Exists(textBuffer.GetFileName())) return NoAnnotation; - var language = LanguageUtils.Parse(Path.GetExtension(file)); + var language = LanguageUtils.ParseFromFileName(textBuffer.GetFileName()); if (!SupportedLanguages.Contains(language)) return NoAnnotation; try { - var fileText = Encoding.UTF8.GetBytes(File.ReadAllText(file)); + //The ITextBuffer's text contains \r\n new line symbols, but sending the file content having the \r characters + // included, can result in incorrect start/end line/column offsets to be returned from Rosie. + var fileText = Encoding.UTF8.GetBytes(textBuffer.CurrentSnapshot.GetText().Replace("\r", "")); var codeBase64 = Convert.ToBase64String(fileText); - // Prepare the request + await InitializeRulesCacheIfNeeded(); + + //Prepare the request var rosieRules = RosieRulesCache.Instance?.GetRosieRulesForLanguage(language); - //If there is no rule for the target language, then Rosie is not called, and no annotation is performed + //If there is no rule for the target language, then Rosie is not called, and no tagging is performed if (rosieRules == null || rosieRules.Count == 0) return NoAnnotation; using (var httpClient = new HttpClient()) { //Prepare the request and send it to the Rosie server - var rosieRequest = new RosieRequest(Path.GetFileName(file), RosieUtils.GetRosieLanguage(language), + var rosieRequest = new RosieRequest(Path.GetFileName(textBuffer.GetFileName()), RosieUtils.GetRosieLanguage(language), "utf8", codeBase64, rosieRules, true); - var userAgent = await GetUserAgent(); + var userAgent = await GetUserAgentAsync(); httpClient.DefaultRequestHeaders.Add("User-Agent", userAgent); var requestBody = JsonSerializer.Serialize(rosieRequest, SerializerOptions); var requestContent = new StringContent(requestBody, Encoding.UTF8, "application/json"); @@ -96,7 +115,7 @@ public async Task> GetAnnotations(string file) foreach (var vi in res.Violations) if (!distinct.Any(v => v.Equals(vi))) distinct.Add(vi); - + return distinct .Select(violation => { @@ -117,6 +136,48 @@ public async Task> GetAnnotations(string file) } } + /// + /// Initializes the rules cache if it hasn't been done. + ///
+ /// We are doing this at the first request for annotations (see above), instead of on/after Solution open, + /// since a document might be open (thus has an ITextView and ITextBuffer, and a is created) + /// before the Solution would open or complete opening. + ///
+ private static async Task InitializeRulesCacheIfNeeded() + { + if (RosieRulesCache.Instance == null) + { + //Temporarily switching back to main thread due to RosieRulesCache.StartPolling() + await Task.Run(async () => + { + await ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync(); + RosieRulesCache.Initialize(); + }); + + //Wait a little for the RosieRulesCache to be initialized with rules. + await Task.Run(async () => + { + while (true) + { + if (!RosieRulesCache.IsInitializedWithRules) + { + var delay = Task.Delay(TimeSpan.FromMilliseconds(100), new CancellationToken()); + try + { + await delay; + } + catch (TaskCanceledException) + { + return; + } + } + else + return; + } + }).WithTimeout(TimeSpan.FromSeconds(2)); + } + } + /// /// Builds a user agent string from the current Visual Studio brand name and its major and minor version numbers. ///
@@ -124,7 +185,7 @@ public async Task> GetAnnotations(string file) ///
/// See also: https://learn.microsoft.com/en-us/nuget/visual-studio-extensibility/nuget-api-in-visual-studio ///
- private static async Task GetUserAgent() + private static async Task GetUserAgentAsync() { await ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync(); var vssp = VS.GetMefService(); @@ -143,7 +204,21 @@ private static async Task GetUserAgent() var match = AppVersionRegex.Match((string)version); //e.g. Microsoft Visual Studio Community 2022 17 4 - return $"{brandName ?? ""} {match.Groups[1].Value} {match.Groups[2].Value}"; + return $"{brandName ?? ""} {match.Groups["majorVersion"].Value} {match.Groups["minorVersion"].Value}"; + } + + /// + /// Returns whether the language of the provided filename is supported by Rosie. + /// + /// The file name to validate the language of. + /// True if the file language is supported, false otherwise. + public static bool IsLanguageOfFileSupported(string? fileName) + { + if (fileName == null) + return false; + + var languageOfCurrentFile = LanguageUtils.ParseFromFileName(fileName); + return SupportedLanguages.Contains(languageOfCurrentFile); } } } \ No newline at end of file diff --git a/src/Extension/Rosie/RosieRulesCache.cs b/src/Extension/Rosie/RosieRulesCache.cs index 178de44..ad8777f 100644 --- a/src/Extension/Rosie/RosieRulesCache.cs +++ b/src/Extension/Rosie/RosieRulesCache.cs @@ -23,7 +23,7 @@ namespace Extension.Rosie ///
public class RosieRulesCache { - private const int PollIntervalInSeconds = 10; + private const int PollIntervalInMillis = 10000; private readonly IReadOnlyList NoRule = new List(); private ICodigaClientProvider _clientProvider; @@ -57,6 +57,12 @@ public class RosieRulesCache ///
public IList RulesetNames { get; set; } + /// + /// The cache is considered initialized with rules right after the response is received from , + /// or when there is no to use. + /// + public static bool IsInitializedWithRules; + public static RosieRulesCache? Instance { get; set; } private Solution? _solution; @@ -114,19 +120,23 @@ private async Task PollRulesetsAsync(CancellationToken cancellationToken) { switch (HandleCacheUpdate()) { - case UpdateResult.NoConfigFile: - continue; case UpdateResult.NoCodigaClient: + { + IsInitializedWithRules = true; return; + } + + //If there is no config file, or there is one, and the rule update was successful, + //Wait for 'PollIntervalInSeconds' before starting a new round of polling. + case UpdateResult.NoConfigFile: case UpdateResult.Success: default: { - //Wait for 'PollIntervalInSeconds' before starting a new round of polling. //The combination of 'while(true)' and 'Task.Delay()' forms the periodic polling of rulesets. - var task = Task.Delay(TimeSpan.FromSeconds(PollIntervalInSeconds), cancellationToken); + var delay = Task.Delay(TimeSpan.FromMilliseconds(PollIntervalInMillis), cancellationToken); try { - await task; + await delay; } catch (TaskCanceledException) { @@ -157,6 +167,7 @@ public UpdateResult HandleCacheUpdate() ClearCache(); //Since the config file no longer exists, its last write time is reset too _configFileLastWriteTime = DateTime.MinValue; + IsInitializedWithRules = true; return UpdateResult.NoConfigFile; } @@ -191,6 +202,7 @@ private async void UpdateCacheFromModifiedCodigaConfigFile(string? codigaConfigF try { var rulesetsForClient = await client.GetRulesetsForClientAsync(rulesetNames); + IsInitializedWithRules = true; if (rulesetsForClient == null) return; @@ -254,7 +266,7 @@ private void UpdateCacheFrom(IReadOnlyCollection rulesetsFrom } /// - /// handles the case when the codiga.yml file is unchanged, but there might be change on the server. + /// Handles the case when the codiga.yml file is unchanged, but there might be change on the server. /// private async void UpdateCacheFromChangesOnServer(ICodigaClient client) { @@ -265,6 +277,7 @@ private async void UpdateCacheFromChangesOnServer(ICodigaClient client) { //Retrieve the last updated timestamp for the rulesets var timestampFromServer = await client.GetRulesetsLastUpdatedTimestampAsync(RulesetNames.ToImmutableList()); + IsInitializedWithRules = true; //If there was a change on the server, we can get and cache the rulesets if (_lastUpdatedTimeStamp != timestampFromServer) { diff --git a/src/Extension/Rosie/RosieSeverities.cs b/src/Extension/Rosie/RosieSeverities.cs new file mode 100644 index 0000000..46effa2 --- /dev/null +++ b/src/Extension/Rosie/RosieSeverities.cs @@ -0,0 +1,14 @@ +namespace Extension.Rosie +{ + /// + /// Severities of Rosie violations. + ///
+ /// See ruleResponses.violations.severity property on https://doc.codiga.io/docs/rosie/ide-specification/#getting-the-results. + ///
+ internal static class RosieSeverities + { + public const string Critical = "critical"; + public const string Error = "error"; + public const string Warning = "warning"; + } +} \ No newline at end of file diff --git a/src/Extension/SnippetFormats/LanguageUtils.cs b/src/Extension/SnippetFormats/LanguageUtils.cs index 3a4ab9f..ac11d8b 100644 --- a/src/Extension/SnippetFormats/LanguageUtils.cs +++ b/src/Extension/SnippetFormats/LanguageUtils.cs @@ -1,8 +1,5 @@ using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; +using System.IO; namespace Extension.SnippetFormats { @@ -49,10 +46,22 @@ public static string GetName(this LanguageEnumeration language) } /// - /// Parses the given file extension to LanguageEnumeration + /// Parses the given file name's extension to LanguageEnumeration, or + /// if the file extension is not supported. + ///
+ /// This is a convenience method for LanguageEnumeration.Parse(Path.GetExtension(fileName)). ///
- /// - /// + /// the file name whose extension is parsed + public static LanguageEnumeration ParseFromFileName(string fileName) + { + return Parse(Path.GetExtension(fileName)); + } + + /// + /// Parses the given file extension to LanguageEnumeration, or + /// if the file extension is not supported. + /// + /// the file extension public static LanguageEnumeration Parse(string extension) { return extension switch @@ -108,8 +117,6 @@ public static string GetCommentSign(LanguageEnumeration language) case LanguageEnumeration.Swift: case LanguageEnumeration.Solidity: case LanguageEnumeration.Rust: - - default: return "//"; case LanguageEnumeration.Python: @@ -127,6 +134,9 @@ public static string GetCommentSign(LanguageEnumeration language) case LanguageEnumeration.Css: return "/*"; + + default: + return "//"; } } } From 95a903b978573d3399cf7d0ec494034a23aacfc1 Mon Sep 17 00:00:00 2001 From: Tamas Balog Date: Wed, 30 Nov 2022 07:08:15 +0100 Subject: [PATCH 2/9] Delete an unused class --- .../Rosie/Model/RosieAnnotationJetBrains.cs | 35 ------------------- 1 file changed, 35 deletions(-) delete mode 100644 src/Extension/Rosie/Model/RosieAnnotationJetBrains.cs diff --git a/src/Extension/Rosie/Model/RosieAnnotationJetBrains.cs b/src/Extension/Rosie/Model/RosieAnnotationJetBrains.cs deleted file mode 100644 index b60ee60..0000000 --- a/src/Extension/Rosie/Model/RosieAnnotationJetBrains.cs +++ /dev/null @@ -1,35 +0,0 @@ -using System.Collections.Generic; -using Microsoft.VisualStudio.Text; -using Microsoft.VisualStudio.Text.Editor; - -namespace Extension.Rosie.Model -{ - /// - /// An updated version of . It stores the start and end offsets based - /// on the current Editor. This is used in } - /// to provide the annotation information. - /// - /// - public class RosieAnnotationJetBrains - { - public string RulesetName { get; } - public string RuleName { get; } - public string Message { get; } - public string Severity { get; } - public string Category { get; } - public int Start { get; } - public int End { get; } - public IReadOnlyList Fixes { get; } - - public RosieAnnotationJetBrains(RosieAnnotation annotation, ITextBuffer textBuffer) { - RulesetName = annotation.RulesetName; - RuleName = annotation.RuleName; - Message = annotation.Message; - Severity = annotation.Severity; - Category = annotation.Category; - Start = annotation.Start.GetOffset(textBuffer); - End = annotation.End.GetOffset(textBuffer); - Fixes = new List(annotation.Fixes); - } - } -} \ No newline at end of file From aa32aa28e610c1c62aa97f2607e56068ebc9faa7 Mon Sep 17 00:00:00 2001 From: Tamas Balog Date: Wed, 30 Nov 2022 07:08:40 +0100 Subject: [PATCH 3/9] Add lightbulb actions for violation fixes --- .../ApplyRosieFixSuggestedAction.cs | 144 ++++++++++++++++ .../DisableRosieAnalysisSuggestedAction.cs | 133 +++++++++++++++ .../OpenOnCodigaHubSuggestedAction.cs | 89 ++++++++++ .../RosieHighlightActionsSourceProvider.cs | 156 ++++++++++++++++++ src/Extension/Rosie/RosieEditTypes.cs | 14 ++ 5 files changed, 536 insertions(+) create mode 100644 src/Extension/Rosie/Annotation/ApplyRosieFixSuggestedAction.cs create mode 100644 src/Extension/Rosie/Annotation/DisableRosieAnalysisSuggestedAction.cs create mode 100644 src/Extension/Rosie/Annotation/OpenOnCodigaHubSuggestedAction.cs create mode 100644 src/Extension/Rosie/Annotation/RosieHighlightActionsSourceProvider.cs create mode 100644 src/Extension/Rosie/RosieEditTypes.cs diff --git a/src/Extension/Rosie/Annotation/ApplyRosieFixSuggestedAction.cs b/src/Extension/Rosie/Annotation/ApplyRosieFixSuggestedAction.cs new file mode 100644 index 0000000..64a21c3 --- /dev/null +++ b/src/Extension/Rosie/Annotation/ApplyRosieFixSuggestedAction.cs @@ -0,0 +1,144 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.VisualStudio.Imaging.Interop; +using Microsoft.VisualStudio.Language.Intellisense; +using Extension.Rosie.Model; +using Microsoft.VisualStudio.Text; +using Span = Microsoft.VisualStudio.Text.Span; + +namespace Extension.Rosie.Annotation +{ + /// + /// Applies a fix with a series of edits on the code. + /// + public class ApplyRosieFixSuggestedAction : ISuggestedAction + { + private readonly ITextBuffer _textBuffer; + private readonly IList _edits; + private readonly string _displayText; + + public ApplyRosieFixSuggestedAction(ITextBuffer textBuffer, RosieViolationFix fix) + { + _textBuffer = textBuffer; + _edits = fix.Edits; + _displayText = $"Fix: {fix.Description}"; + } + + public void Invoke(CancellationToken cancellationToken) + { + if (HasInvalidEditOffset()) + return; + + foreach (var edit in _edits) + { + //Apply code insertion/addition + if (StringUtils.AreEqualIgnoreCase(edit.EditType, RosieEditTypes.Add)) + { + _textBuffer.Insert(edit.Start.GetOffset(_textBuffer), edit.Content); + } + + //Apply code replacement/update + if (StringUtils.AreEqualIgnoreCase(edit.EditType, RosieEditTypes.Update)) + { + var replacementSpan = + Span.FromBounds(edit.Start.GetOffset(_textBuffer), edit.End.GetOffset(_textBuffer)); + _textBuffer.Replace(replacementSpan, edit.Content); + } + + //Apply code removal + if (StringUtils.AreEqualIgnoreCase(edit.EditType, RosieEditTypes.Remove)) + { + var removalSpan = + Span.FromBounds(edit.Start.GetOffset(_textBuffer), edit.End.GetOffset(_textBuffer)); + _textBuffer.Delete(removalSpan); + } + } + } + + /// + /// If the start offset for additions, or the start/end offset for removals and updates, received from the rule configuration, + /// is either null or is outside the current file's range, we don't apply the fix. + /// + internal bool HasInvalidEditOffset() + { + var hasInvalidOffset = true; + try + { + var documentLastPosition = _textBuffer.CurrentSnapshot.Length - 1; + hasInvalidOffset = _edits + .Any(edit => + { + int? startPosition; + if (StringUtils.AreEqualIgnoreCase(edit.EditType, RosieEditTypes.Add)) + { + startPosition = edit.Start?.GetOffset(_textBuffer); + return startPosition == null || startPosition < 0 || startPosition > documentLastPosition; + } + + startPosition = edit.Start?.GetOffset(_textBuffer); + int? endPosition = edit.End?.GetOffset(_textBuffer); + + return startPosition == null || + endPosition == null || + startPosition < 0 || + endPosition < 0 || + startPosition > documentLastPosition || + endPosition > documentLastPosition; + }); + } + catch (IndexOutOfRangeException) + { + //Let it through, no edit will happen. + } + + return hasInvalidOffset; + } + + #region Action sets and preview + + public Task> GetActionSetsAsync(CancellationToken cancellationToken) + { + return Task.FromResult>(null); + } + + public Task GetPreviewAsync(CancellationToken cancellationToken) + { + return Task.FromResult(null); + } + + #endregion + + #region Disposal + + public void Dispose() + { + } + + #endregion + + #region Properties + + public bool TryGetTelemetryId(out Guid telemetryId) + { + telemetryId = Guid.Empty; + return false; + } + + public bool HasActionSets => false; + + public string DisplayText => _displayText; + + public ImageMoniker IconMoniker => default; + + public string IconAutomationText => null; + + public string InputGestureText => null; + + public bool HasPreview => false; + + #endregion + } +} diff --git a/src/Extension/Rosie/Annotation/DisableRosieAnalysisSuggestedAction.cs b/src/Extension/Rosie/Annotation/DisableRosieAnalysisSuggestedAction.cs new file mode 100644 index 0000000..6eac9e1 --- /dev/null +++ b/src/Extension/Rosie/Annotation/DisableRosieAnalysisSuggestedAction.cs @@ -0,0 +1,133 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Extension.Rosie.Model; +using Extension.SnippetFormats; +using Microsoft.VisualStudio.Imaging.Interop; +using Microsoft.VisualStudio.Language.Intellisense; +using Microsoft.VisualStudio.Text; + +namespace Extension.Rosie.Annotation +{ + /// + /// Adds the codiga-disable string as a comment above the line on which this action is invoked. + ///
+ /// This will make the Rosie service ignore that line during analysis. + ///
+ public class DisableRosieAnalysisSuggestedAction : ISuggestedAction + { + private const string CodigaDisable = "codiga-disable"; + private readonly ITextBuffer _textBuffer; + private readonly RosieAnnotation _annotation; + private readonly string _displayText; + + public DisableRosieAnalysisSuggestedAction(ITextBuffer textBuffer, RosieAnnotation annotation) + { + _textBuffer = textBuffer; + _annotation = annotation; + _displayText = $"Remove error '{annotation.RuleName}'"; + } + + public void Invoke(CancellationToken cancellationToken) + { + var violationStartPosition = _annotation.Start.GetOffset(_textBuffer); + var lineAtViolationStart = _textBuffer.CurrentSnapshot.GetLineFromPosition(violationStartPosition); + var lineText = lineAtViolationStart.GetText(); + + //Calculate the indentation length by counting the whitespace characters at the beginning of the violation's line. + var indentationLength = 0; + while (char.IsWhiteSpace(lineText[indentationLength])) + indentationLength++; + + //If the violation is in the first line of the document + if (lineAtViolationStart.LineNumber == 0) + { + //Insert the first line's new line text at the beginning of the first line, to add a new line + _textBuffer.Insert(lineAtViolationStart.Start.Position, + GetNewLineText(new SnapshotPoint(_textBuffer.CurrentSnapshot, + lineAtViolationStart.Start.Position))); + } + //If the violation is NOT in the first line + else + { + //Find the previous line's end position + var previousLineEndPosition = + _textBuffer.CurrentSnapshot.GetLineFromLineNumber(lineAtViolationStart.LineNumber - 1).End.Position; + + //Insert the previous line's new line text at the end of the previous line, to add a new line + _textBuffer.Insert(previousLineEndPosition, + GetNewLineText(new SnapshotPoint(_textBuffer.CurrentSnapshot, previousLineEndPosition))); + } + + //Get the comment sign for the current file + var language = LanguageUtils.ParseFromFileName(_textBuffer.GetFileName()); + var commentSign = LanguageUtils.GetCommentSign(language); + + //Insert the "codiga-disable" comment at the new line's start position. + //It uses 'lineAtViolationStart" because after inserting the new line character, the original violation's line number becomes + //the new empty line's number. + _textBuffer.Insert(lineAtViolationStart.Start.Position, + $"{string.Concat(Enumerable.Repeat(" ", indentationLength))}{commentSign} {CodigaDisable}"); + } + + /// + /// Returns the new line text for the line at the provided point/position. + ///
+ /// This is based on https://blog.paranoidcoding.com/2014/09/02/vsix-insert-newline.html, it is a modified version of it, + /// because we are adding a new line before the current one, and not after it, and we do the line number check + /// in Invoke(). + ///
+ /// the position whose line's new line text we want to use + private static string GetNewLineText(SnapshotPoint point) + { + var line = point.GetContainingLine(); + return line.LineBreakLength > 0 ? line.GetLineBreakText() : Environment.NewLine; + } + + #region Action sets and preview + + public Task> GetActionSetsAsync(CancellationToken cancellationToken) + { + return Task.FromResult>(null); + } + + public Task GetPreviewAsync(CancellationToken cancellationToken) + { + return Task.FromResult(null); + } + + #endregion + + #region Disposal + + public void Dispose() + { + } + + #endregion + + #region Properties + + public bool TryGetTelemetryId(out Guid telemetryId) + { + telemetryId = Guid.Empty; + return false; + } + + public bool HasActionSets => false; + + public string DisplayText => _displayText; + + public ImageMoniker IconMoniker => default; + + public string IconAutomationText => null; + + public string InputGestureText => null; + + public bool HasPreview => false; + + #endregion + } +} \ No newline at end of file diff --git a/src/Extension/Rosie/Annotation/OpenOnCodigaHubSuggestedAction.cs b/src/Extension/Rosie/Annotation/OpenOnCodigaHubSuggestedAction.cs new file mode 100644 index 0000000..f8652f3 --- /dev/null +++ b/src/Extension/Rosie/Annotation/OpenOnCodigaHubSuggestedAction.cs @@ -0,0 +1,89 @@ +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Extension.Rosie.Model; +using Microsoft.VisualStudio.Imaging.Interop; +using Microsoft.VisualStudio.Language.Intellisense; +using Process = System.Diagnostics.Process; + +namespace Extension.Rosie.Annotation +{ + /// + /// Action to open an associated rule on Codiga Hub, in the browser, to learn more about a violation and a rule. + /// + public class OpenOnCodigaHubSuggestedAction : ISuggestedAction + { + private readonly RosieAnnotation _annotation; + private readonly string _displayText; + + public OpenOnCodigaHubSuggestedAction(RosieAnnotation annotation) + { + _annotation = annotation; + _displayText = $"See rule '{annotation.RuleName}' on the Codiga Hub"; + } + + /// + /// Opens the rule's page on Codiga Hub for this particular violation. + ///
+ /// Related VS extension: https://github.com/tunnelvisionlabs/OpenInExternalBrowser/blob/master/OpenInExternalBrowser/ + ///
+ public void Invoke(CancellationToken cancellationToken) + { + try + { + Process.Start($"https://app.codiga.io/hub/ruleset/{_annotation.RulesetName}/{_annotation.RuleName}"); + } + catch + { + // ignored + } + } + + #region Action sets and preview + + public Task> GetActionSetsAsync(CancellationToken cancellationToken) + { + //There is no subset/submenu of actions for this action + return Task.FromResult>(null); + } + + public Task GetPreviewAsync(CancellationToken cancellationToken) + { + //No preview provided + return Task.FromResult(null); + } + + #endregion + + #region Disposal + + public void Dispose() + { + } + + #endregion + + #region Properties + + public bool TryGetTelemetryId(out Guid telemetryId) + { + telemetryId = Guid.Empty; + return false; + } + + public bool HasActionSets => false; + + public string DisplayText => _displayText; + + public ImageMoniker IconMoniker => default; + + public string IconAutomationText => null; + + public string InputGestureText => null; + + public bool HasPreview => false; + + #endregion + } +} \ No newline at end of file diff --git a/src/Extension/Rosie/Annotation/RosieHighlightActionsSourceProvider.cs b/src/Extension/Rosie/Annotation/RosieHighlightActionsSourceProvider.cs new file mode 100644 index 0000000..2900eb4 --- /dev/null +++ b/src/Extension/Rosie/Annotation/RosieHighlightActionsSourceProvider.cs @@ -0,0 +1,156 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel.Composition; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Extension.Rosie.Model; +using Microsoft.VisualStudio.Language.Intellisense; +using Microsoft.VisualStudio.Text; +using Microsoft.VisualStudio.Text.Editor; +using Microsoft.VisualStudio.Text.Projection; +using Microsoft.VisualStudio.Text.Tagging; +using Microsoft.VisualStudio.Utilities; + +namespace Extension.Rosie.Annotation +{ + /// + /// Provides the following lightbulb actions for Rosie violations: + /// - apply fix + /// - disable the analysis on a violation by adding the codiga-disable comment + /// - view the related rule on Codiga Hub + ///
+ /// See documentation at https://learn.microsoft.com/en-us/visualstudio/extensibility/walkthrough-displaying-light-bulb-suggestions?view=vs-2022. + ///
+ [Export(typeof(ISuggestedActionsSourceProvider)), ContentType("any"), Name("Rosie Code Analysis Actions")] + internal class RosieHighlightActionsSourceProvider : ISuggestedActionsSourceProvider + { + [Import] private readonly IViewTagAggregatorFactoryService tagAggregatorFactory = null; + + public ISuggestedActionsSource CreateSuggestedActionsSource(ITextView textView, ITextBuffer textBuffer) + { + return textBuffer == null || textView == null + || !textView.Roles.Contains(PredefinedTextViewRoles.Document) + || !textView.Roles.Contains(PredefinedTextViewRoles.Editable) + || !textView.Roles.Contains(PredefinedTextViewRoles.PrimaryDocument) + ? null + : new RosieHighlightActionsSource( + tagAggregatorFactory.CreateTagAggregator(textView)); + } + } + + /// + /// Provides the lightbulb actions (apply fix, disable analysis, open rule on Codiga Hub) for requested ranges + /// in a document. + /// + internal class RosieHighlightActionsSource : ISuggestedActionsSource + { + /// + /// Called to signal that the list of actions has to be updated. + /// + public event EventHandler? SuggestedActionsChanged; + + /// + /// Collects the RosieViolationTags in a given range of the it is created for. + /// See its creation in the constructor of . + /// + private ITagAggregator _violationTagAggregator; + + private bool _isDisposed; + + public RosieHighlightActionsSource(ITagAggregator tagAggregator) + { + _violationTagAggregator = tagAggregator; + _violationTagAggregator.BatchedTagsChanged += OnTagsChanged; + } + + /// + /// Signals to update the suggested actions if the tags have changed. + /// + private void OnTagsChanged(object sender, EventArgs e) + { + if (!_isDisposed) + SuggestedActionsChanged?.Invoke(this, EventArgs.Empty); + } + + /// + /// Lightbulb actions are available only when the inspected range is not empty and + /// there is at least one present in that range. + /// + public Task HasSuggestedActionsAsync(ISuggestedActionCategorySet requestedActionCategories, + SnapshotSpan range, CancellationToken cancellationToken) + { + return Task.Run( + () => !_isDisposed && _violationTagAggregator.GetTags(range).Any(), + cancellationToken); + } + + /// + /// Collects the lightbulb actions for the given range. + ///
+ /// The tags for a given range are retrieved via _violationTagAggregator. + ///
+ /// This method maps the available actions to their respective violations. + /// In more technical terms, maps s to their respective s. + ///
+ /// The 'disable-codiga' and 'open on Codiga Hub' actions are always available, while fixes are available only when there + /// is at least one fix present for a violation. + ///
+ public IEnumerable GetSuggestedActions( + ISuggestedActionCategorySet requestedActionCategories, + SnapshotSpan range, + CancellationToken cancellationToken) + { + if (range.IsEmpty || _isDisposed) + return Enumerable.Empty(); + + var violationsInRange = _violationTagAggregator.GetTags(range); + + var suggestedActions = new List(); + foreach (var violation in violationsInRange) + { + var rosieAnnotation = violation.Tag.Annotation; + foreach (var fix in rosieAnnotation.Fixes) + suggestedActions.Add(new ApplyRosieFixSuggestedAction(range.Snapshot.TextBuffer, fix)); + + suggestedActions.Add(new DisableRosieAnalysisSuggestedAction(range.Snapshot.TextBuffer, + rosieAnnotation)); + suggestedActions.Add(new OpenOnCodigaHubSuggestedAction(rosieAnnotation)); + } + + return new[] + { + new SuggestedActionSet(null, + suggestedActions.ToArray(), + null, + SuggestedActionSetPriority.Medium) + }; + } + + public bool TryGetTelemetryId(out Guid telemetryId) + { + telemetryId = Guid.Empty; + return false; + } + + public void Dispose() + { + Dispose(true); + } + + private void Dispose(bool disposing) + { + if (!_isDisposed) + { + if (disposing) + { + _violationTagAggregator.BatchedTagsChanged -= OnTagsChanged; + _violationTagAggregator.Dispose(); + _violationTagAggregator = null; + } + + _isDisposed = true; + } + } + } +} \ No newline at end of file diff --git a/src/Extension/Rosie/RosieEditTypes.cs b/src/Extension/Rosie/RosieEditTypes.cs new file mode 100644 index 0000000..d813103 --- /dev/null +++ b/src/Extension/Rosie/RosieEditTypes.cs @@ -0,0 +1,14 @@ +namespace Extension.Rosie +{ + /// + /// Edit types for Rosie violations. + ///
+ /// See ruleResponses.violations.fixes.edits.editType property on https://doc.codiga.io/docs/rosie/ide-specification/#getting-the-results. + ///
+ internal static class RosieEditTypes + { + public const string Add = "add"; + public const string Update = "update"; + public const string Remove = "remove"; + } +} \ No newline at end of file From 43532c998f4eb9a7800aafddef7b6e5a2c04b8a3 Mon Sep 17 00:00:00 2001 From: Tamas Balog Date: Wed, 30 Nov 2022 11:09:00 +0100 Subject: [PATCH 4/9] Add developer documentation for the rules caching, text tagging and lightbulb actions. Update project structure docs. --- DEVELOPMENT.md | 122 ++++++++++++++++-- images/project-structure.png | Bin 10572 -> 10305 bytes images/tagging-flow-during-editing.drawio.xml | 2 + images/tagging-flow-during-editing.png | Bin 0 -> 48773 bytes images/tagging-flow-initial.drawio.xml | 2 + images/tagging-flow-initial.png | Bin 0 -> 42308 bytes 6 files changed, 116 insertions(+), 10 deletions(-) create mode 100644 images/tagging-flow-during-editing.drawio.xml create mode 100644 images/tagging-flow-during-editing.png create mode 100644 images/tagging-flow-initial.drawio.xml create mode 100644 images/tagging-flow-initial.png diff --git a/DEVELOPMENT.md b/DEVELOPMENT.md index 57a9060..a458762 100644 --- a/DEVELOPMENT.md +++ b/DEVELOPMENT.md @@ -56,6 +56,7 @@ Visual Studio Extensions still need to target full .NET 4.8 Framework as Visual * `Caching` - everything related to Snippet caching * `InlineCompletion` - support for inline snippet completion * `Logging` - Helper classes for Rollbar logging +* `Rosie` - Static code analysis and its lightbulb actions * `Settings` - Handling current VS setting including the Codiga settings dialog * `SnippetFormats` - The different snippet models and the parsing * `SnippetSearch` - The menu entry and the tool window for the snippet search @@ -99,7 +100,7 @@ During the expansion session, we use [`IOleCommandTarget`](https://learn.microso For a detailed explanation on the async completion API, there is a great GitHub issue at [microsoft/vs-editor-api#Async Completion API discussion](https://github.com/microsoft/vs-editor-api/issues/9). - ## Inline completion +## Inline completion The inline completion is triggered by starting a line comment on a new line. ### Triggering To be able to trigger the inline completion we make use of another `IOleCommandTarget` in [`InlineCompletion/InlineCompletionClient.cs`](src/Extension/InlineCompletion/InlineCompletionClient.cs). Where we check if a session should be started based on the typed text of the current line. @@ -124,6 +125,106 @@ To be able to bring up the tool window via the menu, two parts are needed: 1. Define the menu item command in a [VS command table](https://learn.microsoft.com/en-us/visualstudio/extensibility/internals/visual-studio-command-table-dot-vsct-files?view=vs-2022) ([`SnippetSearchPackage.vsct`](src/Extension/SnippetSearch/SnippetSearchPackage.vsct)) 2. Implement the command that gets fired when clicking the menu item (done in [`SearchWindowMenuCommand.cs`](/src/Extension/SnippetSearch/SearchWindowMenuCommand.cs)) +## Rosie code analysis + +Rosie is the static code analysis tool and service of Codiga. Its implementation requirements are available +at [Implementing an IDE plugin for analyzing custom rules](https://doc.codiga.io/docs/rosie/ide-specification/). + +### Rules cache + +In order to perform code analysis, users must have a `codiga.yml` file in their Solution's root directory, +with at least one valid ruleset name. If there is no ruleset name, or no valid ruleset name defined, then code analysis +will not be performed. + +[`CodigaConfigFileUtil`]() is responsible for finding this config file in the Solution root, and for parsing this config file into a +[`CodigaCodeAnalysisConfig`]() containing the list of ruleset names. + +Here comes in [`RosieRulesCache`]() which provides a periodic background thread for polling the contents of this config file, +and looking for rule changes on Codiga Hub, as well as the caches the received rules per language. + +The rules are retrieved via [`RosieClient`](), and this is where `RosieRulesCache` is initialized before sending the first +request to the Rosie server. This way, it is initialized only when code analysis is actually needed. + +For response/request (de)serialization, you can find the model classes in the `Extension.Rosie.Model` namespace. + +### Tagging + +#### Tagging in general in Visual Studio extensions + +Tagging and lightbulb related classes are in the `Extension.Rosie.Annotation` namespace. + +Tagging text in Visual Studio extensions means that you can associate information and data to specified spans/ranges of text in an editor. +This information can be user-visible or not, can hold arbitrary information or can provide error highlighting. + +To get a proper understanding of tagging, it is necessary to know a bit about the following editor related classes: + +| Class | Functionality | +|------------------------------------------------------------------------------------------------------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| [`ITextView`](https://learn.microsoft.com/en-us/dotnet/api/microsoft.visualstudio.text.editor.itextview?view=visualstudiosdk-2022) | It is a higher level view of a document being edited. This view may be associated with the editor itself, small code peek windows, or color highlighting on the scrollbar. | +| [`ITextBuffer`](https://learn.microsoft.com/en-us/dotnet/api/microsoft.visualstudio.text.itextbuffer?view=visualstudiosdk-2022) | A lower level view of a document via which you can also perform certain types of edits on the document. An `ITextView` instance holds a reference to an `ITextBuffer`. | + +The tagging functionality is provided by the VS platform via the following set of classes: + +| Class | Functionality | +|-----------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `ITag` | A type of marker to provide arbitrary information for a span of text. | +| `IErrorTag` | An implementation of `ITag` that provides so-called squiggles for a span of text.
You can configure the type of the squiggle, which can be a custom type defined by the extension developer, and the tooltip to show on mouse-hover of the associated span of text. | +| `ITagger` | Provides the logic based on which `ITag` markers are created and associated with a span of text. | +| `IViewTaggerProvider` | Provides `ITagger` instances for an `ITextView`-`ITextBuffer` pair. | +| `ITagAggregator` | Aggregates the list of tags of the specified `ITag` type from an editor. | + +**External resources:** +- MSDN: [Highlighting text](https://learn.microsoft.com/en-us/visualstudio/extensibility/walkthrough-highlighting-text?view=vs-2022&tabs=csharp) +- Michael's Coding Spot: [Highlighting code in Editor](https://michaelscodingspot.com/visual-studio-2017-extension-development-tutorial-highlight-code-in-editor/) +- Stackoverflow: [VSIX: IErrorTag tooltip content not displaying](https://stackoverflow.com/questions/64458987/vsix-ierrortag-tooltip-content-not-displaying/64497016#64497016) +- MSDN forum: [How to get the ErrorTag ToolTipContent to work...](https://social.msdn.microsoft.com/Forums/sqlserver/en-US/157b3f6d-eadd-4693-b8f2-458f837b4394/mef-errortag-how-to-get-the-errortag-tooltipcontent-to-work-in-vs2010-extensibility-component?forum=vsx) + +#### Tagging in the Codiga extension + +The tagging logic is separated into two branches of classes to properly be able to provide Rosie violation and error squiggles related +information. Their functionality is detailed in their code documentation. + +| Classification | Rosie violations | Squiggles | +|-----------------------|---------------------------------------------------------------|---------------------------------------------------------------------------------| +| | Stores information about the violation returned from Rosie. | Stores the color definition and tooltip of the violation to render it to users. | +| `ITag`/`IErrorTag` | [`RosieViolationTag`]() | [`RosieViolationSquiggleTag`]() | +| `ITagger` | [`RosieViolationTagger`]() | [`RosieViolationSquiggleTagger`]() | +| `IViewTaggerProvider` | [`RosieViolationTaggerProvider`]() | [`RosieViolationSquiggleTaggerProvider`]() | + +#### Tagging flow on file open + +When a user opens a file, or a file is already open when a Solution is being opened, the following flow of actions are performed +to have tagging in the editor, including the extension initialization steps: + +![Tagging flow initials](images\tagging-flow-initial.png) + +#### Tagging flow during document editing + +When a user makes a modification in a file (regardless of the file also being saved), the following event handling chain is performed, +so that every affected component is notified that they should call an update on tagging and suggested actions: + +![Tagging flow during editing](images\tagging-flow-during-editing.png) + +The last step will trigger a call on `RosieViolationSquiggleTagger.GetTags()` and will perform the same tag generation and collection steps as on the flow diagram above. + +### Lightbulb actions + +Lightbulb actions are actions that are provided in a context of texts or language elements. +They are available and created when there is at least one violation (a `RosieViolationTag`) available +for a span of text in the editor. Lightbulb actions are provided by [`RosieHighlightActionsSourceProvider`]() and [`RosieHighlightActionsSource`](). + +MSDN documentation: [Displaying lightbulb suggestions](https://learn.microsoft.com/en-us/visualstudio/extensibility/walkthrough-displaying-light-bulb-suggestions?view=vs-2022) + +There are three lightbulb actions (quick fixes) available for each violation: + +| Action | Behaviour | Implementation classes | Availability | +|------------------------|----------------------------------------------------------------------------------------------------------------------|--------------------------------------------|-----------------------------------------------------------------------| +| **Apply fix** | It applies the fix, a series of code edits. | [`ApplyRosieFixSuggestedAction`]() | Available only when the violation returned from Rosie contains a fix. | +| **Disable analysis** | It adds the `codiga-disable` comment above the violation's line, thus tells Rosie to disable analysis for that line. | [`DisableRosieAnalysisSuggestedAction`]() | Always available. | +| **Open on Codiga Hub** | It opens the rule's page on Codiga Hub in a web browser. | [`OpenOnCodigaHubSuggestedAction`]() | Always available. | + +
+ ## Settings The settings dialog is also divided into the settings model and the options dialog that shows up in the VS settings. The definition and registration of the Codiga settings are done in [`Settings/ExtensionOptions.cs`](src/Extension/Settings/ExtensionOptions.cs). These settings are stored in the Windows registry and can be accessed via a singleton instance `CodigaOptions.Instance`. The UI for the settings is defined in [`OptionsPage.xaml`](src/Extension/Settings/OptionsPage.xaml). For the simple settings dialog, the minimal logic is done in the [code-behind](https://learn.microsoft.com/en-us/dotnet/desktop/wpf/advanced/code-behind-and-xaml-in-wpf?view=netframeworkdesktop-4.8) file [`OptionsPage.xaml.cs`](src/Extension/Settings/OptionsPage.xaml.cs). @@ -131,19 +232,20 @@ The definition and registration of the Codiga settings are done in [`Settings/Ex # Frameworks/Packages List of used third-party frameworks and packages: -| Library | Purpose| -|-|-| -| [NUnit](https://nunit.org/) | Unit test framework | -| [Moq](https://github.com/Moq) | For mocking in unit tests | -| [GraphQL .NET](https://github.com/graphql-dotnet/graphql-dotnet) | For consuming the Codiga API | +| Library | Purpose | +|----------------------------------------------------------------------------------------------------|-------------------------------------------| +| [NUnit](https://nunit.org/) | Unit test framework | +| [Moq](https://github.com/Moq) | For mocking in unit tests | +| [GraphQL .NET](https://github.com/graphql-dotnet/graphql-dotnet) | For consuming the Codiga API | | [Visual Studio Community Toolkit](https://github.com/VsixCommunity/Community.VisualStudio.Toolkit) | For easier development against the VS SDK | # Testing Some general overview of features and edge cases beyond the defined extension main features that should be tested with Visual Studio: -| Scenario | Expected| -|-|-| -| Drag a file tab out of the main VS window and create a new one | All extension features should work in both windows | -| Changing the color theme under Tools -> Theme | The search window should adapt to the new theme. | + +| Scenario | Expected | +|--------------------------------------------------------------------------------------|----------------------------------------------------------------------| +| Drag a file tab out of the main VS window and create a new one | All extension features should work in both windows | +| Changing the color theme under Tools -> Theme | The search window should adapt to the new theme. | | Changing the font settings under Tools -> Options -> Environment -> Fonts and Colors | Should also affect the inline completion and snippet search preview. | # Links/Help diff --git a/images/project-structure.png b/images/project-structure.png index ec5326d0b5897a867ef5af56a7302efff891630c..6feb089a842a47a8236b4586ae7a575345e03221 100644 GIT binary patch literal 10305 zcmaKSc|278`~I&pSWwL`llg(KmY(ZZD^op1^^V` z-xT_%Xu&FwZ);LG7w-q74A(WQL!&X~1pz`v%s&0KoL_*AGRn zci9~P5SujA(>4#X-_G5f;@WKm#yn$u4`{g1P! zI&L}HOi{kNBt^pkIY*he{efd-gTZr;#nD#ad0#N3WLDYJa`8url6Syr!@*Y8;sVCo zcQ_BTnzKq?!sIDpWHBQbzms#+(`MR!z7JXW&bL?JfISTWEm}|@62?pe2tf2GfLMh( z7!$vSR1&}D0>i!K4F*8Yw1l08<~Uv2CRp3kHjD!3d9MOT*Pb)YQ9)qNrQ7;5rg!zn zS)f}6EZA>N)mY^g+Vmri=e&pF!^Vg|4dzrglVOeMRj1@*9q~( z>f;@?-ht6TiJ1|GwBqNij{XlbFdmiE@V%Yv)^i%9oRm>_%iIiyaBJFd{v@7@OIt?g z+Jr@)+zYuFY*vJH)F}{libsv0|K!7+-#u{FUgQWFF(SRKY3mhUQ`|MYHQVY6?>{j3 zz`Yr`-6M%4lzOH_shsSritNu?;71E|9hZ9&vD0q@t1+_lM=0uW>|7D;%xFg+)hqn} zTvG=_e^4!i%{SD0N>U%jz$Df#Wnkp=XHLZ-b-Y)ZyL)ASMEw{@){vG7KDaR43fF#i4_|&dg19 z!f0$p_HrpoI&eigMSU+nBF!y3*te}DPx-D-8H1_FBF+dxH|@!{;xTk9n$Bovi{$35!@Csm#tk5m;- z%@|6cH>U7dwnV0scB>7gmG|x$tKrFcX?Csp`eb{j-A|jiu?1YT3V<$vEMr06?#A(kyRC#tX6Bvhe^Cp+juXQ8CD~lE^>gM4LDnt zCWU;=I_AT3G_a|$?%Sq6#mVNmm;hdXdF$;*`=bptvMKKqe2BquICLctwRx!d7A>C% zTWCFo?iFj2#STgJxe*`7laZ+A*4j2VXD8CF89L8U#Lt1xV)`+An@$mNp=xyzQ}qlE zyt7x2Hrhhl!V(^rd%D_ai@A`S)*no`wjQ`0N!*xPJ3e@KP@42;tPsDV7~k6V%*QHB zY1cOm-;n(AL%7(460i5`#X^CzqX|n4mKKW0GwMng@?R%mf_8 zzn-n;bl$HRBf8faZEN0FsI|fuf~oXgrRhsGqnh;~+Jc<0w|4Xhy?e0DkWz^s^ZBM6 zh7qVWU+RDqQREJy$Q!kZ5cJ{Z6f4il;Xdz0GGZNmE^!PipuDeOU)w0fZ}PX9&XK=i9q~pX-4^*xdOMmw-bVK< z7U&gl>@%K&0N{Cg+NnWbpIdfqR=4d)iIGai-|nE@yN4E=tvzN!OXe2BC)vg6vJK&a zQzE0sBbzK#7^K80uuc9S;f9n=kjF-d58T=MUhQGt)}Y*RV;x&7^e{($z0NUC1PH4OE0eJ%A72RcQD&Yc8Qm6K64O{-UR zv970u=Hv?s8OVJ7PDaCN0&btCoiWP*S}&}F5|H0FjP(7KNVTNC>gj$*Yd7n$s5P|8 z!BO{|i(H^yLGiV;iuQ3`9Funx_kNC z>~kKjh@$U)6!jUNc^0VnT^-4XIz@X|>Sfa%Hy&o1%oQ9}R=8sAv-fsgF+IQz3kCRoyCNL>lTARy_8_E>bxG6uPF7%c# zqYTo#CY};VNgRu%OukPgXMb4;0<;JH9|-!rYw{Uv;pF%rel;(|^yi9l+gVNW%t+Vd zYnxmG0El+$X}e#xB|mZOP2ycjR9;~2P3F%)P1iA8y-1mIMIOPU9_&|A6LfsIS}bh9 zo?#zcj;Qy?{#(jN07>XHQbVD#}9`7Y* zTlXv{YwmcIo{H1K+FKV)7sGZ0#C88sQ`pCSG1v!xrTpEE2_^N*cXJUnGwX zY_M$y0S2Xwuvl!!!4>OWBg*XYuNz9jabaX)TrXdU@j};^8UZ&YwDSPFt4$17!b&mS z^Sww*oQn*D%yS7Vf1D{7!;j+U%~vf?D)Z3W>(S|z%0_DxnF&j%ms!+?Q7-HOyi9R| zA{0zdgnbsn4&r1Z4hNRDFeB^(0|O64wL4V;wlag3;>1{vxIEh|cej)$3J}hjUoH)Y(sEbP=%?+8u-~}Q+yPvduhA*zhKH(3ynhQIa zA8x+gOtB-qDFX2~97T45ixJzgKqMB7zW07H!vv_q{yJ3;9exyyjyO5K%#wWRTi@eU zfvr!B&&QM)E(--&SbrbjlW@OJz~hL7F`b(BK}usCc|;3w*8P2|fEgY!of@=1rr%wv zd9-ed_1pTC@XU~6Z9PJ{>@d-Ad1~Ix@^j0+TiQvFx67n|f?von7absoj6)?0leCm& z_=DF3vrhe}a5#F^xLNf!hv0J6Km|z1c0_SfzA$O|u@t}5$sh}LAkR*9 zQVz;8h(MO=abBz=8sy5Kn-a2gAgyJ@u!6!)^KW%Wi2wl9-+~Xs?!KW_8uwJhRfEP2 zN&td%>@yFNrxl=WaCX|ilh*yUly3d1M9QG<+7lFJ$ z><-X7b&C$2ANSC4D9Ie?f2e-1nnW!9ptvb5+aReu^mQrEFL1au<&QJ_zfkwgJV<<+ z^8WMdmnE6DF)@ygw5>N6h6aq|%7A(z-R9oQGgIzKp0>wj?aJNSK_patPY57j>Vp_H zMqQ=MX8!fC)LnaVofDP}3@V2Vuc^$v730o#c+lM42`-So8yYr2e(OlleiecE@{Ia+ z{8IZPD%$Jus9B{Ny~C(+@yG;1D*W??(WCL8muKMcr`K7iY1DfyG+x67J%e-Qw)uJf4}}J@({~;H2~OIF2bI^!w)0BXzug zJq7R>ymf)u=cmx)!zOj!@T28wzb^%XO^u|QrzdHRp+&QHe0mzDp6XTVii12*jLFDq zkg@vZu@OikOca8JNi|5>o)b96H!L#Co_q=~)hnBu$e_q=sgQJZ_Ag@%ii+I$v{q=3 zzg@}lnkYTuG2zxv?%!MJNC6}K`k+sm0TP_Ss8bZHQ(epB)%}$jbxkp@#^}8u68^Fb zw&v|+%_szcG?Mhv6&W=Al}iZ{FgtRP-1JxTNw3WT?uX@0+wP?GqHf=PL08tNY$!s4 zZi9QV8NX|}bg4paZl=vl-z=PsWA=P#3Cx^roA)9+~_Y@JJKdqw*4OstAvv@+X!_1LOu!IG{t zijJ* z2KLXO#k9n;Ur?E_Kd;1O*g*F;C)ClKGk{5U;y^TsI$(QQ42`knHQGu0=p!Q3zQiL}rZ0v4L3|Vm$&RSe_|69dog$Quugrr8H>nOfU$uVvEG!rC55PT!pS6B9 z&kgb)KOC+d;X82dE$oX2%I@BtY8tFFMNUD<^;P^1%hda;_GOf|2jUqyZ@)>76KaE03A5HoS0cd|`V5nSyfFM?eCxX7 z<=ZP6WmJ>6q=EJYBDTI>?OV+zWl9DL)hA64-r4a0#wd8rj(w1s#)=tdSp|Iodo3_)?k)TM=X9Q&-LXmO~lRHEZ=<{`BN`7R9i#{_w|_ z9(mxo6B7R|2H(tsSYtb_7t(?~<9r4Gs8;3_$BTUrp)D*fgSlb#y{xZ|zP-cj7z(C6 zJ^lkR3qVAsqfjr~nw;e;VH@rDOG=lumxFRsK2$w~+U&&ly?5&cvpczXwosuCYUdf) z!(%dFImRXSd~+L;6(>0k=J%p2N|iz>QdS1(+aQ~E1fos`;<0%r>av(Wr)$>6E}(Y-Ct%^wTlmBFX?TE)19 zIJrTHYnJb>3G-cnvMEGviDOF>78~UHDUawHCN`fxCth}yEUESRAXPBWRAyPZf|z6s5kyzZ&8RiEpH=IG@A7}*F{-t{sINOySv&n9dHN=G>{oVRDQc|4s8>^}bb+!1F_q@k`dNC4p zZd?<{`iqSc6xj0QachY5D@hu_scXhiclxZz$;Q%@8-MMgKrluT+ETdNhy^v~5B4dv zRR3GGH8X}bu}-?BK+Z(|1dDEHap&@g)EfeC*wd@mmJseB3H|R{&X|4XdMF53asPvu z_F3Sr+>za-|0;eUgCKVO*?J&m;nt2CF0Y9;VO*Z_Mik4%pu*WJ?R2MVH8kn%|Ks9r z2(vJ>P^JSu#+LGwl{NA#!WMcytuL$HWt1xhn}b`ZlB=<`V3Chr+=-`;U5llO^Z{iT zxPYMOx|dyJm=))K302l$K@{&Teo`n6IDrWY!j)KfmEX{7NUI-fr;s%JKoert%=}R_Oljy#Eu=Amh#j zcK&hN>@!75z9T9#-#mzIxEC#^r%~^z!NSx&E&jh^L)0|JRd)YXea6hjs0?v7W#{Hu zU?mYmAc3#RSz8;QsvY|hwv})L?D^jWfs1=IBpttnN_n(n&E!>iTx9*^6|Rcb4qboO zAI7176x@WZl}~y{a^*|xn)1UQpGanO?sNp_>coW9~-GFnuFA<{Lh`%hHc+soi% zc16~=_sl7Cnt3PQrTWR~BIBc6ST3?oGYTPJkT~pVtL3xH)(1IbGhvsU$J>0~J3Y7P z)&0ONC;1mOXu}bL5i^FT6s+t{C|OvdvAi&bIMjk`$qdykmA?+zLRsB(=_r@co;6~e zsty+Tp@u^%j1)hq#O?GBQvRZJFhehqzoBF>vYHzTSKe$cnzF%ZLc-rQX1ymapHNX? zUV;@og&O^)e77Y-6<`FDIx@V^+OT@{BG5;tCx3I$Rpfpg#g+e5YX1*Xkn)qbbO?10 zwCjd=0Uow*8FA(=maW8?SJP7y=&0(*_;!FAHm*U z{qJIX2cS+*Q~Nl*_p;nUwocMSjgaHa*0W*$VxkPaw(M_Nc$0Jc?M%~+8*b~(J5xM# z6BCCrr!=ZuE=Y|fbSyma3yaqSIsSV#dypT9b3G) zN%rSwyl}a$jjMXOvEkfN~qLNf10|n{L`5s|BPPqX+5=DcG^x;*W3Db zG_0~NaHpuaGS6!W*I>Ekj+cpZRdCcO`L!zHMRC|~PaQ{i!SLkAdO zd!Khh=#2>`?wOGY?FYipFAh5?-__}u2z%|t^GajZVd|Q73}e?0Iy*aVI4iLl>s=Yv zfFoZJyJ6XNMX~TrkF#xFrXu$*lIdnn73j%;$TIhJC;&aW@`y@a0f^j*7Bm{vC5LqX z!4>nzFH7I`N8wGN5JCdvBb=__Lpp^O;s z!CQi$wxTTGW%;INepC@zOEd}vkDmG(zPx?T%C2V=ViG{M#n0@ZIgS2HP6_^8X^w3fcS-LQ1>EC)>nc*;(!6pQJaqq}V8K+CYS-i$ zFXq5YflpN41Z#z^H#o552Y8pxVBoC-%26B0x0x~TD~yN;blNs8T~Fe z2G6@mAC(NWb0QADvjlIyYVhx+-TTCS28qkZ+O=$c!*tc3@qprSM;g|b9aTX4lDD(Q z$`)a)d?VIT8etCISGHik2@S@HA#L)S;GqFuF-t{hrVr9rvk+EBZ?cPV<{2j2ge#^_ zY+??V?FpO4#l1IzZm3v#-jvkcW-H%=hRu5hA}^g&;tQUy-y^>3X=!fpNa{Z1nrZC$ zJWKcBx-C@iibTsm9x3`6)e~??Ks?zDM{kC!J<%DsgL1p7r#QM^cRa93jz*gpS{_tT z#g#rQoVxsyrGs9KX^{LB8V5RpG z-W^&$ngfN&kgyrcbPDGJ`u1CjZOmraUZN~A9^cf6$?pqa7mOZdVyS2)49GqetDl#} zRpBzVZ)f;kjQOy+=NUZI>qsnEOzw=3TEM@O4YE7pG^<%mu5xgqYxCyu$5mNrnSwN7 z!J_vHM&y|l+9CD6RV80|D5HK4vabd|XsYWlb#I?3Ph~R`lY5 zx8_Rb+YCPRNS|_2IuY|ABl=ry^%zQC4JNwJ%ng2Qt5vvV#g8^Rt_Sf5MpiWYa(3L5 zf8c^|Ij(<1VDwCg!{_WQwuO04?-=)XVn8W`B3tvcw8m_!`<3n56X; zL4w{8XuItng)Jp}hZT}SA~0uAfWY8f?70hU_QueN(9KQID52kL^A5+(&>;Y3mmR%= z1~E1eC(q15Ojr=|P&oGk$g}#y*y3Rh5A23d$%0JC`ar zIt*vMG&jP>f~#t3I2)DhhZuEd|DvEW?_3qiuoJ*zA^U8kz`9u`!}7bxTB{S4oVu?N zghltt(jSoE)UG5tdlr7xAS>JBFA;GVjevZ?KZn#bBCSX?~09)&q7ksBj#^nV;r4*dr-40hP>MGySb2Ic}hJFMkOsc4E`A znf^py22!`gf&z-I4G`y3!H)ALJI>oKJ#{+#okN^7hpmK6$WP58sZ_Y20|rp0zLnlB zs~)6=vG>Jy*o@2%hj{=cu7Y?3XX>q*AAO5Q+0(~y(L);2@qP&NzE2j)r`0+#b2bti zFjV0dm6g|CRx~HtZuv~(3cwc?@fAXv!`4n~>j_vthRX}~cnGlB(6TCwp(%DTD{yT_ zK}a**nC65HAHq1m1NbSG1M7cH=zF^1X@(FO;^9pHj~*q1`0r^_=m~l8;M+Vt+%ohN z761Ku8o&<(wUJCySCb~)6DD33Ze_21ov(}Jg#!TwfCc>&eK##vH=^7c7|Y;279t`@{?#_pX)o}Vi@Pa z0oa&K*@t-6NSR&#d{!QDzpSbvxtj{bnRi#iDYlayoNko#gE-@RY6)Fg9$PW<={+9i z7;czwuTwSGbjkhDw)%9-ly|}sW1L&k@c?4nJ3)}}OP5-e-`G>VzSGom54$+Mx@7dV zm*kBzKaUsm^H?Oi_*(^Q->XPp3hWh}dOuFi9uZdwP^Tr~>OK2&^U+84gZ0}!y}F=e z3OU$$UZb*{pqQYVGw!^icLOnMICZ!*&VkDr{dVVN8a!itidcVdD(}uD{lYB*B2f^_ zv|Y+-c0w3GQ|~G!iH(>~KETK==AZcP?GK=n#)N+us@CJfEPE%GQhJ^9o4k2)Tf>|h zHrJ<;wqNC5^4iBHB>%F;{>`!XIsMcEN|22@TlOIT;e$G^3LUX{i-kVJKq6?+biK%bA)Z^UiVTA zhduM=hNQ z2l0DrsK1*ONp^80nfIRe)G`k89V6HK<=9r|=%(|ymw)jKlg8-5h^_I1?U@pMe`d#S zYgpJ1`%TX&*SV!|+2z7an0?50-5o<_@dKDl<0s0!e#z=s_o}9nnS(*ak=Sgn=5q0q zud)&119eiVI|y@!`l#H4>)~4h?gwhJ466R=r}z3CDpyMK zNdDgBE0<^&1H#!008&}9oyi*HR$ws{?s1LuF+ub%Fy_&NT zswo88gd~G=&-sscpExeIh7Bp%1@Nc^3^p1zyN>Yr{MfyHad}Ox5V-XkY>RcZBX>PS)K$k&;F_i8pU_~!zOQzC|! zrqw>;DWuMa;CxF{XMpc+9$&A}TlQ~0j^UxpdvjJHL<6*Zz@%ww+b$=NnZ5veh)txv zb0RVG0;v1cV;>+6a_6N#gKnQ(vRMAA_notD4h&V+WCY$O9p|H6oQ9EoQ)Vm-xmo&D zB7Phia1)x{;Ki=2@u0BrVE3;f@Du!xH**oYfpm#&K8P|0C!x_ARtCR}XKuF5iPd2q zTVQV_&H689h{%^a10Qjd+S*kJzV2xyk&ip^7K3bmpsy_6@mTP7ue z{Dg&ugo5taL=dCk+TI(V;9dZ5as)>u=S21x!4cf&1orMn%1p7Ie-DQZp}7uUwXU7L zoCidxzc^bqK>0SolgFSb!$hX70yYqT_NF zcXtW+QOabXr{|rHlgseWjFDQAoE==v$?bUlX=e`{_f``gQN*JtXNuP4t6DNI8ob`&wG=G#99WY^YNW2cyM@a9 zL#?l*i2h4L7^R#X7A_B64tuPIfdQGimxYjfzxwe1=Fef_V`8^W_`{FC=f^MiCLctB zwlpR11w=gP)9XKKa~?>y|Ml_0!>pv$eQ; z7R7e@2WQ%sz)uWfbINx^J*MAuh|{CmU{ZIbCd!v`X3CoV(Tv=c7NYF`BdE<-rsfWp zaveuBx8$u3i2e`Zw^#28zpbd|VN~YU2cb)nv7)UPHv9$vE4|=&$1?MvQqNb^d!4yU z<0hp@T$}IPpjBcZ^5a#_dGS+2`^t)&MrVZV?A4a#%(@HUMw%DbA}%@ z&i+>(*HV)J literal 10572 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 \ No newline at end of file diff --git a/images/tagging-flow-during-editing.png b/images/tagging-flow-during-editing.png new file mode 100644 index 0000000000000000000000000000000000000000..6b1c67cf5824a250266dbb617b1f01b3285e5bf2 GIT binary patch literal 48773 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 WEFRBy1y4!s(ABzhKJVQ1;Qs?)l(+r> literal 0 HcmV?d00001 diff --git a/images/tagging-flow-initial.drawio.xml b/images/tagging-flow-initial.drawio.xml new file mode 100644 index 0000000..e4c8ea5 --- /dev/null +++ b/images/tagging-flow-initial.drawio.xml @@ -0,0 +1,2 @@ + +7Vpbd9o4EP41nNM+hINtMPAYSNL2bNuTs8lmk0dhC6NWSK4sbv31HdnyDdvESQBDdp9sje4z+r6RRmpZ4/n6k0D+7Bt3MW2ZHXfdsq5apmn0Bn34KMkmkvS7RiTwBHF1oVRwR35jLexo6YK4OMgVlJxTSfy80OGMYUfmZEgIvsoXm3Ka79VHHi4I7hxEi9J/iStnkdS0rGGa8RkTb6a7tqyOHvkcxaW1IJghl68yIuu6ZY0F5zL6m6/HmCrtxYqJ6t1U5CYjE5jJOhX+efxrc/F0697P1vTHZoSv7r9/vzC1fQK5iaeMXdCATnIhZ9zjDNHrVDoSfMFcrJrtQCot85VzH4QGCH9gKTfanGghOYhmck51Ll4T+Zj5f1JNtXs6dbXWLYeJTZxgUmwes4lMLZVMq4WpuN6UM3mD5oQqwdeFQ1wE8x1zFvBwMtH81aQr9apFAV8IB+9QZrxAkfCw3KV0OzE/AAfzOYYBQ0WBKZJkmR8I0ivYS8qlNoYfbeYXmFy3u0R0oXtqmTaF8Y58+PFkqJNIoHSnUIP0tO1fC7VaRwU1JjlxC3/zgOAHwtWEOLtHnofFreBLQLOIW5+I7f5gPlGXkdgRGElAPyihus5+x1gxEmV9wjyKJWftTJmMxrZQlGJELfLVjEh850djXAFV5vEwJZSOOeUirGtNp9h2HJAHUvCfOJPj9oeTTifpb4mFxOvd67a4zHSFga2JaRNTrk6vMkTX1bJZhuN6nUOtTPPsyOii0+7YLyOkMHWLBQGlYXF8ljJrstSwSZIyK0lK4z1dIzGsJ8j56YWr4cKJEHOpemFEEkQj2CcE8Ha6uPu1IB4QwiupLRYHPmJvnk09QvyM6RJL4qBi1VpEWzGDSnscUt/1xvQSFcAXzRUts0mgPqV0X9YXiCMj7sURPEMDp+Eo+iWOYnhMR2GfnZ843U2rVdMdVKyR47iDszmlnKDhzCYNZz3nx1/C0Q93qQsEoyl/VUHOXx4IXm275s4buv52fRNGGeY+Z2CpoNIpvDv2t/tb7G8V2d/olbC/fSj2N3sli+r49HBEqA/rcrTVJNaHBbO028nuKd1P+jCWKRfz0GCL6bRgO1irMq9qgLvH4N8Bzakj00itaMApvdQZc+K6kVVxQH6jSdiUsqvPiUIrtNsbtXpXqi0wZBDZ1ChghwG6t4AWi2pZ+01IM6zBFtR6JVAzS6DWPdiJvNGdVupsn7K+9lx3WkbtrVa3SRgb1T7bz3tiwKI+NvJl2dlXIQfylOudc6H+Q2iDquDAFBTLExadQONiE2CH3FnvnMNtRs/aQne3V0S3dcxjVHIzcQLoNmqiOwX0UzbvBNDdrYvufqPo7j6P7jGiNLu53leIPR/DaX/CEv6CDx+fj7CcNfDtLbfeLXr15NbwOLg3Cmr8H/evxn2/Lu4HjeK+XwP3aRiYueFxl1LsyBJPfYDLtwoSUKNZEhSOaS+U9AX6uvQ8gT0k1dZEh31pcVAyHFSU/UwkPBc6niirwuGEEuYdgEX/4+xpGDXps38w+mz0mvK90eegLn02eh8ZD7MefR5w47SDJicowIq2+X6I8gUM/T6Zxix5EFHKNAcLv5hWo0yT4Zn6F115pjFOiGnOI4pqFMOoBaa5IoFP0UaBXs5UXCXQBKEkH6LNW/JMUhPCTMVoPr5brNqNY3XQJFZfdynd7sCx4LweL9k1Qdzs6yW7BoYXIjofEBZIxCQJ3Wz4zGf7tOXoeEz8+mdv3n1MCVhAHSUuGeMyHMH7P1EkVyi7Hj6WXbO8IiADyfSFd5iXeShvXf8B \ No newline at end of file diff --git a/images/tagging-flow-initial.png b/images/tagging-flow-initial.png new file mode 100644 index 0000000000000000000000000000000000000000..fc77fd6d4cb5e36fafecac724ee45cec59313485 GIT binary patch literal 42308 zcmagF2UJsA*Def*0-_W_5J5l@L8XO20!Z&IK&SyJhTcLTfdHW^QiY(X^sa!7BE5;y zrT1P0lu$)_$A9OX^WN{f$M=q(kr9%;SJs+qu34Wscch+<+Qsu%&y$gnUDQxl)+Zx7 z%SA>;PDOnVxWY#Zk_7&dWA)V($qKqTmdMCx&U>mLJW)ROPOf%jykMojKkKFoVAQ9j)SP}RH+<=Jv{m};^ zb{n{&;O_2fXJTiq(tccp4gu!<3;)cn5EQQqmZl z9R}-!cKgp}f$6}c8(|K?NN1=O7>YDi)5n=YO^iKN{+`DT3DEQx zSM>mf2!j5e1ZwPVZ7YcuQ_&Lxx#_@oMHL-2bUa~t3Z@1++M?DvUWQl+SCos0t)mh| z$HYKO3~P-8gVb%^e3fl+Dh6;xWm|Vith1dM9{4E+qOXr~HxmPP=>~BT0Zvd6s^|%Z znTVRHBkXOl9^y!Ml$(g68(2&W=7Ms;89?=&480ZARY7QDeQ$lRxU;sM4;o^sC8i5= zFm~1SF*LCEl@QTYgP9uHqs&wsG;mln0tMDWLM4sW)t$sNppHH&PLi6yicSWeZZ5V4 zA{aF_q=deUp)~{v_4M%7RmZv5_#jO{8k!D{t`G@d4KFW1XI9?lq0*+Eu9i@cv z^fC3&gm{9C#Fb2)Rm7b&bsZ4^^>+3?7#m=Sst809st3Xus+*w=a8QgSa1Do70@=g0 z#GR0KFodm=f}xv+qnRY$Sy@LNW2}reu!pJIU^FqFa3!>-gt#$YT@0?Kq%P^8WoVCd zQ*&_CHFZSUxQK(aUBv(yR5WuIw+CtJcp|V6TWf@gwyB+_p_z|^5z5;RE9MH*HZz8K zh?%&1=|GHNV3e{r8t&n3Yo`had8Zs2C5p(o<03$j;&8YA_+jX;{tu0BeD zXsY5JbQRRSJxx?}wLR6{(DterJ4J1{wyuVWI52|-)Lz^{&s7cKV(Xx36vL||Gt%`m)YHTI>N+_Ail+!Q)%MkOfvKBlE5g-WO%bXR zuFjemtfzsuftiM{mWZpC7#?J+fmg$7N;r!{OieIiHd>l0>bmZBC{+~|7r3*il8uUl zIuhcg=LOPG6oY#^dAV!ro2u)fG;wY&B9d^NFASousV(NA=mau$S24k>DI2(9ZH!2E z2IPZ-8>reSAn^J|o>&o-gPw=62tv{n;iLp}hGLEM^;BTW?f{)CXyDDsMAy{K5vDJy zX76SuAqF-Q#TeVdY>l;i^~Io0%2+(g)lEy;5wB#bV}=L2`2f1Fsw4uJ1be`-DrjF< z;H$Bbx-P;;Oap-ug^FXvti5pJqW1baYHD_-zyfBXO73VK4R@R}PLxERr!`bf!dTQ6 z_=tA325g|Jy@9i+4jAhSSU5$Dn54R*iiZMl8DoPnG}hEXsw-OSiik?Md1={#^nfAG zN+w$R_IMFBC2feczJo5v2?O(x^l;F|sf+1IDB_VW&LSdCo=BL2JI)qv2I!!o7R1Ha z*axl-aW=4ZGQxV`^qh<|FrGSCYfpqb!ox|#+e}f}45Z}f;OMOe@f1@qaT0Y0Jb1F)@)kF&A52Hpm)jn+YVIO%#LM2zs(I7J<0TZ}2r z%ico|4Rwa-L7ml{bhN$1!LD{1diuZ&%1FQzsVb@9AWF(Uz_RwvP$e53du2~;YkNCw z1O_CoW9_Z0?ErXCj08ZJsvXY94e%VQ+FoMn&UU2TA-wg#NPQ<$FFREqSCBT=S5Z^X z-c(uJ(;IDIq@`n~;%K0YGynz~Nh%;9UO2dfgQ18E$QHN(2D#f9EBLCb8W;gn8hN|u zDcV5XoSZP;UTP{TUZ%DVDkSm%o3H5OuBMD|$ATrifiEb$2nweqDPm{mpzS6iYUe0n zrVNGI+3SmYi$M*PHJxnG-WnbNnTiOAhJ=F=;CfUIA!a^KKH@5>>RLJqHrhCGM-#M& zuc4Tk8$=ZZ5_fl2)pb&H)721j_fUoDq7{)yaSgPpo`?}r5rGvqLMS2Nidrfz>JV)m z6JR<`n31ENk|E3(t%nh}#^`wHn1T^-O;s~{TVFR@C|1cwOxxQ|Thavtx5Z;2z9vvb zv?)Tv8EvnkXr|$#1lI(c;KlH2_E1k<1lmpoPqKh0TO8O81XD70R8TiC@=~*d`KrUz z0q3CNV(jY#RdO$@Q$&Ee|GqM?qLavr&e#qm>p|3w|u=CVco zu^x1v*lw<}W8-B~NDPXZjjJ;@8G3JJ{zTD2;nBG)M8Grv<0zSF(< z?w&#XZa}`uUdo%dYt5z>!TSgd&-q6RWR&v%dVK>^)L35A2`{gc&-3C`hD~f|hjmFA z)EO+Fp(m~VJw|>ia^7?Go3Ye(AAY>Xi%ooOFk{?e6OkJbLGWciFGfkhP4@5CPioiu zzPv+)GpXZgSSY2MHv-N?9V|~lA)x#dPb8NL^q2Em-n(0PD}q21oX!7VHp%>u%ujmw z+O*PIPnu!GLfqU4Rl^&{l~s(E4eyZ1H&S;!{0yXWtt(tyUB5riM0 z)Wx%ZY*PL;!)WCza<6*)HN}FvcAxun8TjE89QeAW*V7DKTAs%bAb5rqWPE%7{hjZl;*#T$zTfJk9Y$d zquw1#ZrPXZ>R)vJi|_(tBuQheNn=KXznK5GF+`pPfJ5?c7yy~O!*llDf0H>Z1whp0 z>`OlO2hpC5i?@>+M?N4F>x5R>5dH&4_npRTn@in?WlA^d1Px18&zgw@2pEaGF_)Qg zP*Jdve#`c6F$SbMvx!>XU!a)$Zd{ds^svBvjUItyjSspm3VypkbFSyy8h8$>pgbSP zz(p(ez;0Zisa5nE!jtw7g~;@$0j8NL_8J1qWqF)%L2RYjT$3(+<3RTNfa%18d%Q_Ycm5h~6UGD1JgU0v|!xrWSvr zu?qS1>uHXd{jz`c_jkm;xR4Q_hx2Rk#C{%ds~<}ua>U?g?c^S&=?V5L1kQTvEsp{%c$cmWcW zsp#w<{UO7KLgW#I7x#AIew+&jZ9Bg}msGog_N0%ZOFcJS-|e@tcl#FwuwLW@J%!XB zv0epvaznBrrLJ5Kg_ z-|F1zn_Nq7EP$e?e!gg>qI?LPUeob^F`6NlcmT4S&b^mkI9$WYcaV9>(yM$9JU#`q)X!F%YhtVyMgLd(x zY6jdI=6QRK5TMyIpBDc#;_@_?^54GA)|ESX-V%FFEB+QtN+UFMAMxFCwrcN{wtL8douHnq-OyN5VO!5s$!+=4yjAM7Th zX1*Q*L5=sO%lJumT!pwM@+!Jnji=(bgL0@pglYW6g$+jRzsx`Im772*RDX;ytHGf- z?+eW2UKMkcKXdLvW|GNoiaDDDIxzkmg`EmnI>Amnb@9T#7iQ)nr(-X8_wKH2W7Omg zA6b?IvMNgwYgM?vzq#a%Uq$!Rc2!CKm_Su0Xg6r(cTtEcEOq|L zmnUa+d+V@4t4Ww4JJx&GRwPc8*t{uy3bsGtu6(#(9NH~c5$-*`5$-Kn=sd{}S7P8| znpHynsjMP^9yd~_9s6Cav2cOM-sb>$W-=kK?@cqVjnxpso+N84QSIbhKU4VC@XL(c z;b4!`Bua^imeYI$P&mi&O2((ntrB)~+g1r<+o#4?rLAwEeD;Ks3Q3|Bt$oCQ1N2~);Xm&KNVgLMX@NLC3PyXd9+xc4*M{~(P_%<|@q$|44$|+2}=%w4} z+hL11{F9>}0oCaYQ0&Z*%Lt?A(is{W8gB5w)!1K0ZVj)cQ)N+eCiw5vK8!SX?Ri)4 zcUOz-$9lErvOHjKlkSP-D05r~=_K6@Td1kKS)TmR$#A1jMbZRUlOIl8oK$nM_KgpV zEPtFxFYxN4+PjR_k89FhFL;_-%_C=h{$yM#i4+=>SC8VO2$R&bbQbr#J0l3WP}Xp( zC~8MM%(+MUT9nHc8Cd4%mF4N_?pN&6)1-Ns9%U9~;z@p(>a=HCYp@JY^Zo8^O0l|T z*R?|$$#F|JwPi>>ai-M45+Qe@_)KNnZW#I`U2!}`P}{YC^^+IMiqNsResQ;@nM3ZR zf>SufJBxGWQ-bC9p}X^$6SntzBc(weRI;Dw6*eSW2Pr`EX78~ zN8U;*&u>J4(W3U;4tmnQchY9#xhV!A<(HRn#;L;oRl4R zS_gag@@YeYY42OpUB#DtIUg)H5?%C%Mmz5EO(&QKc2&!zKA-4g9&>$2!_~-@#@eqd zHda259(>ctHocKMV#1u&JgmEB)V|}4F8*`tCaP_r9O3mBe25QkiQvQkJ!ReSB?a<nTqfJnh~w#fdTLOMa=MwX;bb&t7DxWU9>OY5kz*nnRjr02K^3Ko9V$;hZAen075 zD(myd>Jw-IMXE8IUGOrS^SChavCoZ9+wEwXEYqNEf5KhaC^G((lV3Atoe-JCAG~B- ztxrbh_5I?b0^kcZr5`3D+O=XI?I+#^anS$ai{qGp%}UNjdAgf#JXH~9M0tK{71@A4 zdFF1B5;w~yo9uhdi%R9yyxxjKhLzpa=cd>Dj$5||t?x5~T2}M!Zal4W>`TB}Et~tl z=9WDdBfcl>*myaKfWPo8iV(OP8W|kOm(^vUH_|+3GwM&@{$dIGyR$a>FtzBlu82%r zmE4V%8myv7iu7GwZxUqouy8-1ZUb*`U1FbP_<4?zh zSCbp>^BRt-D%Q3hV}4d$sDUWqQOJ>07g7E^tJETwwWa*WmgV6p&A*AM!g$B+O83ed zd%lLk@2dk@3lQX2-PMxE;vMJiOy>TqAIp7`0ju4j?SnMi8~B%Wsie^w?is}X@Q;>O zRj5BZ#&kIC{z%4*lvb$;F3TrLv=?JW52R?Mi^+AL3! zJLbSY4sy|j+-=I3zMNz<|D%<^?_k1 z37!Lf*ap00_R|q+o2Ohev6z#U#m)4an3BzjGGwZiDsM`qK4?W(3^hrOp68OG?}XU7 z9xq=^xeH&N?LA!>>T=u}AE~?cT7}_9SGYuN((ijd`NK?eo|WYX_j@PjXBwm#Ohd~e z35S!6ocJ?|BIg3PLRX@m-3casZ%aAX7g6Kjn>BLgXyXkWn$Z zHhv<0Rs7)}QQh~aWKC*&Me`AapC9C74sL!+2m-yis9+f^R6G4_oK@w?OPhw;ee((Z zCp$^EnX^aR?3`bI*v7=PFUXsBuOIPyE!%uM`2Ue`k)~Gl#P(%wlewHFac8_YVByc@ z%YbOf2e;dX4pbin-VOcm*l}RH^ujpvoi^EP) zBUz>ff>jVRyqgMeC~R!!f$Wbt0?1$}LextBebjolfncMW%L&+KQno4(e0Y*hKK!op zpR?|#k~m^OK9}Q{o6iUYKi2?HKQJ3*_Gc`{Et?CW0^Y({E(7th1Q5RWf%FWi{?v6C z5F7n;pakaChVuZaj}$;nZsX;$KhMKohioqA=uMG_#+l{79jUk8ngM$+asZ0S8QhgK ze>QoHZ|kGqngD;qd>z9xe=J2tVM^L*21825y=bZ3RF#-yZ>*K1GbM?8YLu}*gEGp| ziIf|CjJp5j*AK3f)V_3dXW8A^NAq&oGCt?1okTIPyBohY7BYH^nZmEjO3g(&|J!(z z0n0Af(Q!^2q19;(Xn=Uik^*C8W1W5V*k=l}wn$oA(Uyzo@^VDO=C}ReDv#lB2HOYu?4N~*9Nr~>#zh(r!#$6?+ zzWOFhrJ4KNbFyxuJP9~klUnB=281lcp8?npeY@is%4bZUtCCZ%I~Oqp6M5{i|FXl4&hOK! zy2StO;C_L6pD+Wz7qBtS07_)gYlhENX196Leg`W0cgkT)5w?`LI;m7TlwXL>igZMB z`I5@T6t7%--k`APT_hc~AxlU#Q|r>`mk*wcIXk7^E27ThZ{*5qz84Q(|Avo~vKG@L zr#$aqF z5dMv4qUAsHd{%5J@aD{%Z{0J<3#`|SIbT3U;?-qsZYv`CERX7lLgr0s`Ck4=-`YhUb_*J*|`;Us2N7)Co%L7 zDklnQ)MLoErMNefuCnviEN_lHaz%l+M_^lqi?UxjXbUMf4ji*O36u~Gk~9GJlwxA-&$Ay?%lfqXU-jC20+y>i;XTW%Tc@QR9vg) zRJIVdpd+YhUtr*8q*zrb`v=EMT?lU4$yB~Y)+yjb;gnX-d+}_`Bl)u=*g@e@OuaDTYI!339kV@4n1vQSlt7E()TvyCfflaXxD?h(|PE(X%&}i^B-)My0 z!0op0&OZsIk``-E?$0SHzQSKySJO>TZw&oUlFDc%%oPN{1=AifJ*U=R)@a19P$}PF zu;cv2@!oWoLNHPG<=d;!C_}UvRY2_lpXX{5GH;sj+EXT?!>eaHK+JZ~)~sx|?ERhL zzmcybgR*b}$7_%QaiCY>1S{m?!vFPnE-)rtEIc=co$=_fa_QuQ7;4(uOk#!eaB*H& z^fz6#P&eWirPtcz)36J*Vb4Z0nD#{_Bx1HIE)-?ba_cby`!jA5$%*IH46fq>zz9z% z{I+8*Rm@fKYuTa+o4G4DXkrxBntewJ2Dh9uy5>W_kaAX|r(up)M|3Q7XlphD zIN)uM1q+(5k3gYhg*B%J!r31|!!&HMez+c@0a~r`E`b#nJET zLsgaKjvJu%=qnKIXu{JA%?g|$V%=7mfHw!E2tY<{QXkm;xYRpM_=SHz=^JO?b6qx9 zgVl>wFeFCns@~qa5lNaV2$#93I)29+eYsQXNuB%t7lfbKruY5wwO_TK&jRP9S6gq# z59$b6<>RnrX~fQj4-?>r9$CaVry!6o&SP;QHvw-zKnC?(E?M6YDx~Uuf-!#z^Z$Np z+uRQT{!BRwM1@$~`BWCh0_*6k|ad-7hR%DNm_S%_LZn zX^($@O0ZBKf~DBjT|u(j26iP&B;V??R6pMQK3yfZOFaoaJxZECsLritt*Nx|E&jag zNUwWac*JR-o72JTD@PCjXAEq6@GNGv1}*z4d}MIi+7Gm8tYH)WmB-qhDU>EKB1`+lqe-`QfCoMF+T(Pg%ZCKe$B{xO<$b*?l=^&`bzHMMiBdnKTmi*Yp}Qwl zw-JdS%=yi(BcDhgo2zGoSKVxU8xg#K<_V+gjCgsNmB1C%^~-UfQJ3>0>PF+k)J1u2um4wsj2f_krGP>du+#7x0-F6Ko z#DFy08OWFI&3*j+tnPI-4(;_v*Z!6h#rog+K2Rt(cf0up4=sN+%)oz;VpFT|c$n#y zWK(?OG;AG)qMx|YKrqdthTXs{G^7!ScP(nb7alHfhJW5nbU)#kd)Lgv$H&2l@}H$x z_Rvaux7_w)=W`M~_Uwob!n{(ZQWK|c6wFn^XG zr0@!un0!zXsJ^hN6T~0Gp(? zfyCJv54?rDpk-w(r^H%*W4n0-0TY`zmqYC@UAXOU_6gIa!C5!nXj1JEMd*)}4JzBu zFSJn@VL4&1_~F23rIr2Ne~|bF=Fi*%`}+7zp&;wF^h5Z*7aD`%t;jmE1D^EQKC3bcj7r}7TKWP(7g%8)JupQnG)f_mfe zE`4yj(iTnJwB1Jw2rOXZm&gJaR zfdGi5X07JY;uz7y)cc{=ZQ108FehwrB*9)sWk3lOxt|Z+?w!AMOLknkq4wax_eXn5_2;4i2zPAvh^yRz`!NPt}sGGUnSBm1F4R?E|5j}<(6NS z^Pi_Gb!g;p?pq4d-&v~5hewyZQJ61R-qHC=Aa?}9>&%bTgK3%dTw^e`-8wBQIdk2k z@)IuSsVz>alZRaCP2_VncCd@vyI2aL=5q#)1L-je$Po^F2eS5K&08yktKMi?)w{CW z+8(j;^{IYUVUk*onXm_&!iX(XqdixJmt6*Z^rejo?!;jtKtU2zY2C=y z^h^QO&VbUemqO`jU-RBzd2$71&poz?&bza>^Eu0`hZ($`UcWW}vsFHEbb%89`0JX+ zo7AusE@`UrM8_w0Wm~!q9GSR$<5?LOcgpnhSVfxE0s=SNZZdN5{&@KP{ge1D#yJ3p zovTy)EXtss0a$oXHm<27p*8b+p;yJ%v{BvFFRmJvX;ml;(vh+j7K$Lb+Gtt)x`!0r z@oriDDx0(C_*ha7x2$!$;mr`SW;NfogmA<9B3JAH9Va!mk@>V6%{N%InOpRLuGDHY zgHA2BX0~(xDYXCe43$$yc^aFp&O|{mvWjZVUe3?8AtkVq_z|wyBq@OX4twO-G*u#f zO2R8vjqlcbREkFv1U@eZN=vK>9iH3|YTpzPZ>-7*vuNG$XWr0*e4ee}(PGHa>dF1y#-#TybN(-&#e|iC?Bk@2nrxxbY(9o!Yuoc2rxSZa{b=x0}#UD3| zuhhAa`+044q?voylt0c(5!R3LW~nN^kWO%u$Yx>SCnaSA>>w7-iVw99&Aiz)^SnOE z)`jVTNm-fl%_8p=Y$@^N>hj?F2pjpOIU8jkiTAbCmCRfn(HHPGm#EMC?J|WafGj8g z@i(XMzb0HTwJoAkuTrY_p3*#{zIj}C^z7f29UDB@x+5r6O>lhg@vlTq>p>vhWOzhI z8+U{&$QAm1_|oL+oXv#@e*HsAZpib~E4f2w1`NEKdPCR>)duE;9dj~ESC#61Vq0eM zaoz7lp+Os3`NYX-TIJ6z1i#9hWWzkLp$1P<1GLF7pXlAzxso{!R#9*@6bW%;5!6<(oi=c`M67 zxTD7G_s$YHf8xj8xcNRA%Ry=i?r*_l#9;s5FI<*rp~n~Ja?YgZXH(M1yYl~4l=~$~ zJ~DTs=TiNiHr2BK#yoWujsVe*2~^}E*4op5GG$U5cr`}5 z6dyq4v?Ofe$N7|)G4ZHHJ54v*<)t6WQCI_P0{J~W|6b^yoI$*L`D3)q)2$qWT-Fz! zTm50)zn>ix>^KRErW|QZ=?bSRD4ny!>RY+4ZC4lZZS2whM!oSqF1c$Q-;FBVn#Rf= z1E4;srpAx}mIJd$E9`Zjco+lt@j*XYu18I1wN;~%HlBAYulGrK`me;OuFm|uvPNn4 zPx=? znDv+XMw2SD{OuH=_Nsyv;@RVWie#QZJz_fhf;D$d4a!kraK(@qRwMG2lrJS zdoMHay8y~JMZey{yYr@1ypVC|UUR^Ye9&{ULPmt6jY4m^+f#A%RvbzuD^|{5I%pp) zB!j@8N3eM{<1stIqqVKsk_=%+W1Jpv}Y*LGv?f5Kcu zO;=AH#hk6?nyilqlt3E3MAhOpU5H&3Q2fI*7L8`yRIF^bxl-|z^&`tX8KB|^#$kM? z(56RAMJv8?Z;LUaBk2C=wv>X7_ZvPkw^wA4OCBZE#aWM@oS9_Ezo)=%ReNPFhdnv} z^(giKD%2Sda6_0UBC1OGqPS-Z#0L$jDXOAaD><31pJ$gPDf>SeZjg(OVtNLkq$cgN ztwj83*hmBqpeLFkBp@>di5-TE%-Aox+%1QM<{BG@opIT2=GSF}@dZ+S1dD$5@|>%b}~6SaN0_xb(E%GKwi_K)Gfq+2_X! z8mceX2Y!CBJt-XYg68YNo?7kz;q-*a*3+8F@T&4_&z4r5r&-6&v0z&I7k$9waFgVr zhhDm;(ji$Z1}?+J+pZW+yMnWg$;pV>_cm2Zn#&1g(z+%kJvYOvz zXS9~w(>~z~Q@b(~y=7Rn^Ov|J-;aMEd~HDWuHwzX!!fjAa>xE0qJo5V{D>Tnium8P zcm`oy%M-SEUd1_ki_tG1tG@Tt?CA(wsl5rk#Z6h_j^#Jmpcr{{9P^@TKfd{O72<3R4kwXu@clDEjlFw+~~cVBnCQTq*z>YQsA z;p^nBH&kQ-Fc7y-EsUh&>hXE}m^<=vxgMw7MqdwRU<)K*$nV^(2By5-enU>qvm@t) z1QG4K%~3}d(oRZOPWF}jJ$uAPuAbm(fHn!Pi>o8VSI7NQ#2g=@U~&Lqv(4JIzh@K@amY|pp)Rx@fRe=IV6g(bOkt95BWzCBG% zux1T+j(+fU(v!|ZHcYPVAaP|C=O_L5#-jdD$HE-T|J|-&oxE}9m!Fnn)U#&JWFS7C z-ml?rh87A>CD-0Er+)?(bLovX2#8Ir;lNZKwFG4xOLIwj_#qbKX6Pa(3t*EME^+0A zaaZvf`-)df1wEd$OR!KatliZ75u!{`_^ZRnQ|M3O6CMTcIWJ_`2UIH5MyzE?Z+7>#qb)`q!|gQ?b*(R0>}s7Qx;%ZkAI| z3hI{o;~{HX6GKu%zNZ9;ze0x3SJKgzPg>wKofe6*+=Nm3da z%f5%mr=F#iPTLd_0VPv!`3t=0rHmke&dtD|N~9Z9a!Nep(vQgi@(;~0U)f~~o)bkU z^GL!Z@0vJGPpcyFh zd5=Mw9K73$?hFZp@tsuy;4L5YFJA{Wxt>=3EG3{=gS@UTIrn*%g`z6G9?5{U;0hfI zp8=@pzaESok}>(Cnga`wFQ8>vbxUdBmH>#r3)fgg6M3K}omn*F;EwB84KD)&Nb56Z zN=YlPa|mV$-H_Pqe#~(LJK;^$7ws}U@IEpJ{B_i;TMZI^RrmqqINyM6wVD|BD` zYAibT%JulblwYssMt!(5vIV_hSg`V_=Fs*z@5XKi}*& z?>syUT`>%&eep>CEs(|mj^mY-c|Hf_M^b!aQAjUh{?};&5PkaHVd4K%9YZv?P8Ck+ zWVqs=nWam{C6}$iV$A$QbUvW>IJH^@2F=b>%E|-=$NoIJdu_5VR`%e5WZ7yUxU6pf z>7cfE-2-5cWm$^N+;LKW2XSxc*{+g>Tp%AI7Ow1&hn@ect|nnJg>61;{tv@K1U5E& z*67AR9kLG=eN7tN)ylapk=s}+;9_?jvzNizUp!8qZ!<%D6Btj|eYAxC@-U@r>$>7& z?l|b*aq}j`(~+98ZRcPG9}s*UKn2vp0rY>*yg1OKM=W2p+ezkc#}DyZA09KE?-2-k z3RE*+Ik0rS;+Onk$JBjs{j|AccIh$LvF)=<>R7z7TpC+q(Ty~)^!fGBuB?3`<;SMh z4?VwW#Zg}5Wd{u+WE3F?O4-AeOH$h#?1KmF^#mi?{J52~v~I)Q;nbHO%1={QvXBht zL~qu0KxIhXk(V_;@0fj31lfDBuOzIRK8V5q_;5BhOJun=9tOTw?xJPTGWqOw%S_J^ z{9;V)Qnk9G4Z@EUhLKujH2j|!-0Gy8SYK$#I!siIg{6MV)W%S-Z9G-_ASYAw2ng3f zz8~_lvT0WR{UB*(Q(t_k%l{(ivJ9;6;1Zu+>|D-DMQ3eMt@lqt-P47s*C_OWMTP0y zz}v;HnG)}eiZlBFzKp@Qx%Yu~+Y^r*WA+$YGe~hH0EyX*%^n_m)ycYlq4fe1f>dpw z5OZW2LQINpZgtvX8E7!w^J(Cmsj&n?snQ3Bh_}v@Z6{=*gKaAUmV_noD?t@@6S0Ur z-TPRN&?X7HXASe>o&UsoI<=}W?k!fDBj^l6tfok=* zM@2W(zg$+x(8sfIdYQSv^)&bF zO+l%#VY6HY+|2&E-*oJzdE@VwUPu1{P?^4MgI~akqBcA2?qePOd`p`+7y6FI<8#b` zZrHgo)?wX}+UHC^bLyd^v|0}2-OOiBhFy>=a$OI81%Dd+r~*byCls2`(?m8LQ0)n4 zbLm`p@dNg@HAC`Al2pb%+gqTuh;73IKt4ZGTDuYC7R)-CpY3=67-gEu0;$o*PXVZf z$P}j!MeVI>x+U?+{tUFiSWJDQ&tFS$)3xMrCL_NwenM2sw~PiaZ5MXBw>0iNKb33fgZ#5~?_dZ|wywPd1l&&jjLa7kv286OF5P`8Gg~=^Z=YpB1Jj z#8XiH0r)V zl8eh0T2SBP;6KeQn@wUW`kZIC9>1D78MWX04^Vn8exf#DbMn2o9>NNBB33`JgSjVA zX|er6*stLm(D)?%b$$R{*D|w(D0X;TRZv6|xG8EFO$aO*E``@#GvyvkCsg|nvVJ*~ zkZFNulxZGmM?2K<$+{g7XjgRPR*aqd508DyGMTBbP&)6`YH)qps~ngl6tD_7O}^iG zJh_{DH-o8pS8q|ORh`5=HrC1PX?63`A8+Xd)7$SGo2oBW-~^MQInCl7b@nQ?edq`E zK$qJ(G?3jFXrB;8pX^`E3!LCb{VL`duX0{&0+?LXvgYe!`#j+K;aCgU!dNP1?QTT~)@SW9jqR zrC$Tq`rn-SV7eFnK2;q9`zEcyp_tR?+A#g?$X6%Sm$@w{cgLeoZrTJ5>x z7NuC)&F^tXm;9*QuYBTU8sNm%W(#DZ=(Ul=>XogLokQ-u^@k3j<$|d-m2EAz9g{5vgnmZ z30=)E%Bt=ZR*S0OWGMZ8S=y7&@6d@0?4qA~3;ca$V%Sd81o|SWzScgVnyTG7Q+P1k zwr=kYWV7jrY~fLJLg*EsTRLH{McZw%YLsc)U9kKxk`M^JCzQt~G6L2?tL#mCy(223 zqpowDbJy7`U}HUzn2V7$?PM35=*b8!|1{5yr1ZV@0g*@*fLaxiwJITdTV%by&Vmmrd$` z>+)UH_ZPET9v{|dzA2mB^KQHxXqO`-S;=acuKX$nIszAETF-Vd)t5bW%3C_%PdSzM z->%R0nVZkx%B$u)nc>e{p*z&4q>MV5y+N{2^2u&<3I$tci?iR`0=Xt)8fs0K>Z>dR zz~479eJ>z$(w&;76Bi{`XQeu~K(R76xW_pQnvLt(Bk8nyZS2FGBd%P(UT}D>2~|{d zCp%?K?WDqo!5QB8(sNu_ug_n?Dw5NR?dTuqrY`lC8UK&^vi&w3_?{^u33vGTmZQSM zqns6LCa}3hvbmMj-`?hsK-7Nq+s6ZSXD%m+FQQf3Z*hs0Zo!XT?du_jNMY71d$^L|K%c2Iqw ze>6-x&SD@@CWH2{;27_^7Z&|3&kB;#)BkbUX{5YtCrK@OP?`$Czd+=}3g)6f1X`^8 zooVo``j;=B>@OzFe}>T9O#Lc6dH3zr55g39j2aE-yr16JbrIIwGV7p9^?Ca>;@Fi1 zslqyn6F1Gx;Jd7*Y(@iELOoQ;bFAYBsnD`P*3bGwg1$syE=O9vL+8P9#ZQCLMpq8M zhkqSmINm_-63$+T0hPd3?HYBiDgvtDkXH_qRzC2#|IwDypbGMz`ig4eW=YzLXuQsZ z@+~l9iDfN_Zy-gRGF7Zlx}Z=PUO?5n4Y6nui%M`RNGAmD-RJD#My3%Ta9nR^!F^bn z#WhNwKc2=-6c8{ncS85C{CI8Hdk{ao17H3teZ!T!EE4dM&Go&kG{vpwOF|X6w)j_A z_dc+oW6Be<&9pcDp1P`vdC;T!>;qli0ysOG<-_ZBctO&M~{mmapiTeAFy4?kV(QHlugX=Dt=Ft6KM0G>2C+PPT%`HmC(8|2z^wrc`FHbHx;pO>KV`d|lJJTinG98+#{-uJ!J7;KV z?@9^iH3cWz-POe;#6ALdijTbqYd?szQF`;jLy)fWFXQ;q_r?~y!L+eS&9`@;A#E+p zAtfA%E>Ko~@MRUv73kl8KlQKEu^Daj8m(e%O|P8GS?kKosIV%S%c0xqCAuMgpLAmx zHE)h&R2ihOaA7z62OF|7(sCJG@Uq6)`)#$f0Cd6=y&u=9_jli%@rd&cTse{ss^`S%*jMV z8ZVdAu$I?eJG?I48GkEQ#-mwnfJHF%W0#pSqlfbJ+oXp0wGVGmtaJHuIldg&AB3bW zc({ZkBezbsnwyg4sN)wn;=;AT9<8{pfP}P#{WY7{Q3MR|_`T}&>D(05__J5U<%72Q zH51|AEG))3Gvz6}`;i0%)MiJL2!Y~$3fk3YAz&6TK3kS%-iX{=uNmYDhUr-`2jfz}V=sFZql`nE5WeE>a_MxtCw`0qZpw&GG= zIulFkBuoQpSd&SQU#QRf-g*xM0)~Sd4+_xA_ns!SAACHsR(V}d6j1srjs&uk%uFur zV{^B&K%4j>YBZDUEl`o0v$z>K0RtIw_PQQqjk27`XFk5b!l10(e?7*$<)vRkm!Z3r zR~(DX}YDIz0!=_2FwoU|_*kE^UofJDD8)WYMOXfT-}W2dDkn z#0;&vKpzNKf!)vOOinNFp-TD@>vuCLqB6JsX%!~*$>TW({C{d9J|5rlWrmIT{cQcB zRQ8Fzd;do#ZL+_88)f*FUwi_qW?ZepVx?;ez;h*8ZQAA6N+-%P_7mT=`V2A^|I+}v z3|d`>M~8AIwCr$~!a0CP8Wlg|=)dxATX}V#&oiCk9p658lr&p6)pvg=q}#9PXuxN3Vo{ zpXcXm(<~-9O{$#CK@x=WJWdsR+w3Ua1*n;0Noo1&by=X;fAudoM^Aq)hvgz$_;=Li z#I@1&kO>x?;8(cUs5SG(Q$im*WFIyB0DsPjUI%0v)SuUU1`=@#J%@i*|as}vk>(MG*NT-f%kmu8Lxc<5GDUnrYS<_$4`+3aV zf#ZVO{HIV~Lye%En}Ocaa+i!uR_Azx4{^tM2Cq8Ls)61W3 zuQ|QQhCFuwsbvkQD{|c{{q~Zq{+Cqg+F7X&Tinj$xvb)@!)z|_iU?XWcB_@xYm=Jc zvSzw|6+$_=X@~a$kZ|q z^|}-7V@`iwwpJvwljfDzDNyd&iZrmFnke7Yd}~!NS$2=;MjdMhDKGz7#8CCWT5nVL zfGP*}m7Ph}v|hikwb+PG^zMxmxa#foiamwrACaw1T}s!C(OH3nYK1piM>i=DyW+Fk zVJNe*&T&UkCqG_3*>>gi{x>pX7@_4DA=jEf(ISaCo^=jC00!k-!wJq{>uQV7`9YG; z@UnL=T9nv*vn{#!6?wbHLd&PDs*$guU@D3rO^2YoRrds_!@bhwUTdz|CL4LQ-FivT zkTayT#|n7R5t^r2*6Q=s=q*pe`WX+2nzLe zIO6oNo6Lt&roeOp(y6Y0l>ezp>2LFsJ7unYt2gV|vZo!sR=G8~GV0|4Dn0`H(!al) zL@$OjsiuiR4pl-WYCYKMFD5rib8!VM6FtA_34X9Vk|I5>W_qK_ExLIt;E2Az)PqM| zjSD*iRD082@ULrSK5tT6w~lU_N$ld*GpF24ESg%C0OBR_%MV~Qm)hnJXg7)}iE3w) zpTu0$}66IskPC!oh+6^UFO(FYW$j(5yjT>iA2bY?~i{H^ZH3B38ex*Lj%}` zCy)}QkG61Ki|we7^!dsdCeXeiOTSv|G;TBe4jRNPtj(fPImwn+k?pr${bDp;>vzjq zw8&OpYew%kMkX1gN>Hgueug`b^*ZUHu)`APwL+eEf&b|RaA7WGd(9F)#$(i^HjDg; z%7Ru7u|G(*j2}1d8d`ukVdC&rI(3plTos!?cEs%Wy>+bfP{@fcDHLZJ1m1)GnKo8@ zEe3#CZb})ps2m#j?Ncwcnf!lzy$3ke|NsAAQV1a;8OMkyqq4WmWJlSQy$;FdAemXm z-XmnM>`nG|tZYv9-p+A2#{YGy_viEee1F&V?{anNx^%o=&+&MU$NheLl#AGhncrb} zo^NA3*6qi%}VsSY9tbnb`MTl>{p7AeyaQbb=?07 z0Wv*iEUKzO*u4LW;T88h!zYNMUe}&Bk~5(0t8s^aT)b%AD4Lv+3#OAJBs%Jp*Y+xn z6RW%Om~MK1*RkV{+VpUOvTrCyCv`Z*DOiekMqZ{aGaAnz5zB&w~X6y zOG{cqzno6tn6#s&h6b@z=0=UDenw=EJT~J zF-!?x0C^V0VTClKWGbli)5}19#?L9V)(b3Mc4?08tFBbztH3p3A+2ehqKA@w+Nn{2 z2D0GYdpQF&7lpIqrXkXVWuqC~3E^h2`^U-Y)xFQFsRoos56fJS65$C93EzG$oo0FA zTiMTqHLK>_F*DXY!*g4OP%s0#rSz;|v$SQ(b>|D-4ogubYu+yAey8SI5^2bC*|cOF ztQpW((u)xJ6d&zF9Go^}rI~@7{diI!82GYh#~y`&oD6qg zQ7vwm4L;L*D-7z8d@ac(vc1fQgw%gso6<@$p7edL7BWF=4=Arr8%0lwo6fDYutEfW z7~0lX6)8_c-g5CZ3i(Wv9c%!i(L4R57Nnp7J5%>8@WHv!K`h$jXuSMcWu?(=w->FN z;e79-iOuafri%Hrw>>vv;HIw0iG@DX1V>}XNqf&Zec@YP=TUEbxo5^b76fC@t#8hW zZtHL#r9Ud9pUBQFS<4hS>7e2c0%U2K{UYiWC?NMSwL)`mMFP-5a8v;6Ja9pb`4XoF z_8?0tk_iyfm-ZTlTnev)88D)sL#!yz8L8`+irCnVs@b?)g3Ic(n$o&wf90~NvP|x+ z>nbnr$Wa5AQVOVVtRG*#%tCgK5p{bm6qveXMpwJef0@jZe!@jG?Lj1}_%Uo90)4-jIr(TUdq)>YKRTLMx{8p=2zx@5;<3J40T++EH}PV#bF)P%NAH3L zC`9XMNNh5Lppcz%aHRForZ=ehr zeLqo0==x)INQ*?_VaPb-DW9a8qf08exZ{1!0FNL-YYH)QA~dq{ksr$@v7YA#S{}Qw zHEHA6xP4s`X|0zL!X}F(`F%;Qy}E;n6M%-JLpF(}%fn26m3q{5F21^!w2W3q2gQ3d zQM>KZ^d)@YD(<^g3cf1Tr=Kgq`HW8|kbg~KY_a0Fk8^vPxxW_IyUJDE(tEf?t_6n7X|qp&W%^fs;*akk%1awgAgmTx+|^3$Y%%c}jQLJzW-W9+y&9R&rkqwX;*Tru z2~O1Z0Vmgjlhzo$KyUSf(W|cOIgB;aA3BrWtxLwUDkjSID+e}Ms0ITya@c3*OB87s z2K9dp6ePLUiy!q7?cf!zxffFn?mNmm)$^jMmznt#RbzBQAsDe`OK?-JC$M2FCqqI zMz-4hCsKC20wP0c7{T@k_6QlMmd2HL5l5HYO=_$>^s_xFjW$BC22;(n7=i;XMnZxw#i70a(oRy@IhJ_Hq z#M4_LoD!5$gRP|+fIX?>2_OQ=Jd4sL;mi>ssfoGYJ`hL_+GOZ9IHsmNh}7tQ@P%rVylC_#H~z`<}DDb8Z8 zv>vT)7+w`2<9TY$|AzHF4-e&+Q+Ho&>^fGW_z=YTMbH{= zHN8-KeJ+1TVkDO3X|Na5 zgII8G8=FbB`uRpzf$5<*hePC=IDVcrsH<824&ISW)|F|gy6tfB4j>){6IHiJJoIu| z+@IkkvAIC)pF~o%ISi1SzL*6IXgU-J)Li(Y;+Gwywf=D=|CJ90l$|n!SH1k!Hm{)k z!rngN>+C+T0`Am8kC--zMQA)9rN#;JeMLfyWeLG?p1h;;LFea@fKbE^YhjJLvHm+d zk@i4Y%zSygFLh#)cTFlN>#=Cv>L&~+;W1R4;XpN5tJb>Brcb(M{gfT|QA>1D8%XQ0 zkdL7u6cq)#jN}jb3aIBqNw;I0<12aU2vSawQj1Gkvr!O`Zh)r_xd{cwe5=0KH+}nH zCy$Uc_F2zxwDaVk!mNJ-YsF4(d-Kfv-9*D&*AVt*vuMBtR@i`#INTaUs7-u9vp@xu%>>_Xseiep+r8NDVs{t-qJQZ_vYh`kGs5s3ti29ToafD z+ALGJtJ`LRAu%zP85WL~0b?vniY@}h!a%oo zE2fWkyNa3yOVPKw&s|SkF-hGOYw3y~jeqO~zbL3qO00P|)86%UE*)@_Tc@#Si?VP^ zt8j!oTx1N6n^qemXCT&qgp9)I4i4n2>x5{utE}bRPj($O=q-V)`yb~Xiz~3|%X{KJ z$+&S!40?iP{KXj%zJ4L1>&LlH9H>ZFin?E=S7VSG_H{05x%ydKfXhPnleZV0eR3{4 zZ{kb^ElDr>KU~v^FZaC`U;omg+RI+vLDXriU0}RA5Rit<%IRp8S@v>tBIdi&Yz zy77`tPwj$=$Al}|WR4^dY9i7Q0i2Sb zJ%3CM$Td_Us@T?LWuy!PXVq71V7t{((rud!2bqx@eT3cW;R1 zpZ1~0w2%4s$CK)SnN1j#zXcri6L1O%UtTK7jmB0mH4j~UbwhCM)LQqFc8v2QrSd4c z$T+X>urXUanVhb$AI+A2s18vmSdf^YB1869WtB3yrK6MgkiX+f#2$&1`#*8z9Ed9{ zm(M=?Ndaa}{P<5>YniVTG_<3Q?i4#!hH({~^Xe|{VGeg%dGm{7Fq77bTRM#|!sP9C z_Mfa(!-sU_Fhvz>G?QBGAM6JH0IZJ4jG{J8Hteu>>l;ss~4_~4B4HJ>UbJ~w$a z?(L4;PN$djSBc}$GjI8PQ~k}Gm{XDU>c#trk@5_6sf-au9c}V@PtnIQu34Gni0n#0 z#;O*6eJmo@P5BGP;dJc%(EnaNDiwI1~@Ez`W zCZ2?Aoz(o^k-Tm19+N2fjr~_;Ex)&8G2HG$8h`sIQ0Cg!jX>zI>8tn9$M+&~Riaz5 zKRvCFj3p|7|J*B1gT6A+PhcJGXOSEwc`1aKpdTf8Guerw%(ZS~ZoriwOW(c(_rJ&z zzA;G|#cT{8>Jprt92`kIz^s`t@4xuMZ1;@AUh$04VtsLvakk!R_hrM4#sMqX-t4nl z`5@scnDNuU?2-Okjc&TA>R9$f6A8b8vezX~sV*8@^vNEdXjE@F-?;@gWizop5^s&C zm|%nDPbbR)AyO-6{>)mOAlEg?|H)N`ljJWkt@KBWhAU7SZLo#8*I? zYU+TFkvdnX&x91brspMdDx^wueEFlIHhg`=k=uYNr{oGH`QElV0rD%pCA- zjx7)n`3aa#ABB0D#nQ}FB2WqX?rv$f1>Pd(hS2KA`~|^}&JNcSq`4YH12mc%QacH* zlzweccCwq(?rZwSoRFTO+*4(@1+3hTZaqW2LvuvBnjcw)Fl0Xuf9n0*V4^~I+8Y&* z&rY30dVPWq(3=S?bhZ35sYtN<5iNb3%?ci9WEO_2RADi?UNvJ={?UBAkdj8=qwj9R zk& z-}mJ6%MIfcMt@GO5x%BJ3^vo8N7A;*PwbU@HwEr@igfo49lsl6;B-%VrF~sQY~=xr zHjWoIn5!QO*xu_?G8bPz1P1QygHkfH>(;maEH>T_c$4Ee$!q(yYpI#ciis`^Lt}Yr z%6I8O?%U7mmcoQ`i;KgQlKEd$!##zhx*Y=Y3$46R3jr3bzIgMxivj|s{qbxhBV=Es z8jS3eTdyYV%>h`G2q(kkepoi!Y-l2XSD#6z{=03;_Ic^#YJGPyxp_ejGn?FW=qMr~ z)!1+pV-^fBr>DqUj6H+9(phy6;GP2X(zBI#ynxjYAUO3oo4rd&5_W=#HuoL{a(5VF z4#Te#CEms3#ws5SQ{P|@>~W>mfJ7hlH(W~Ho9n0g0>EO*m$#%Qg>jFhDqP`-t`rDe zyu0rZmD9%xKPPN!2hnr<^rEqdNUW5;m`__tPXNG^?6t1Hpg8eU((gq?^R8<&f>-B9 z8RQHa2l9``glHnIz zQu$TobV?$T2t1shM9b9v-9*XTl=sOv?vr&|j=#VCN&EiLVqld|-+F)}`_n{3645$u zMBvsauPww@{+(Edd-62)JK?(q}6<*3{)@%w<{8!lyk0a3SH zWW;}*tw{#t7t)KKD+9M0(x$V1*Wb5k$Z9UgMgK9Eig?l(c4u7&+rGQ%5id)-+2IDu&QR3TY0dS>kGiqR+PB0$?1bU z16@TUoRlLAUV%Ck-_BnqG|SGt6=5#VU?X2qlxre%_OMsffe1ICd8Vs3$POrTl~raoCIgZqm@HM?SPtz(gN*uKG(>Sjb0r)pdAMZ0Xm zjkCJ+Q@;U58d2~^XxT!@u{~f%Yd9uc)Ee;Mrd;PHv#vJC$GwPb6W-`Y%N9mGhx=_R z4Rum*X1jTLCT|`lT+_*;u-Y5RPq56LfyIGgmFvmj#Eda9chfgxIr7xf6q; zRN+8=>i;Bf5`fI2m{u0xRtJ_7DRsaCNh4GfbH}416=|a^RJa6c3)|rk($4L}xZuY+SJGqa33Z8NY!^jQaNtc52(xXU|&T$B>W_ zv7`X42a8MYFo&;$6k2Cdrw7>3KDt)<9F#-@wcqq}w6eT&?<|Cw1Uq zdvd{N_-ARww}mrf`A#ZjhDYF_4bj;QSFT9Do99W}1X)pR7;2t~&?%`&@ELb@!WN&q zg~%lM-_Y1DZe}cu-Lsqc9sS1%A)=v!C3K!zvw^n)CCSSz+BY(Rhtu}}c3=kM8pPO^ zk6p0rS>#c~P1%VI#(?=dl+S(N7HK1;6b1jNSdGv>SxyT$-=VjTwksRXtf$h{C#6AVMjlA*?i?2*{s7u#Sz%*57;i<5(RrLl!)&ZmFJ+tO@NC zcgK|v)2by;(Er&XN=Wp_b;g>ELKWHSN@MN~zFe*8^#Trp*S0v%CV<^J$CJ%-jFo5k z8m>ID-=<+YG?sMb7SoeE_0Q}*Ov?9WaK)F+IBty>i44QYXgWq9 zJ5B>w)2jnX_IE)(z2CnJ0H_IAa5->p?6@)C6-miusxncmXHS01=gbwg|K3(IE?qK! zJal~%uwo{mSg2~cC5sGj9}`K3-;jEJFkI2*@cro)v<$xjH`}_tcBWFP*2LzV4(8CH z#$o%KaRt#yZ;HxIa95({ZC_|3pjvq;G8HZ*W_Hllj5IH+jB=Vp{524&pzY@xXIajI zZxdj=cmfz)$qT?82(%=>$N7oH<}Aovg3$}bKEBh|B)zof&LR&v?%U|@5jF2nyvJBP>kIT2LNU6c zy^xc7zHbAeB6NILrgV-qhLj)0vk8Ti5t5L8()GZB50l4*TIEWBG3i z0xLU(`YTa27Ng9{F1j@z=u3ZSZ>QB@LUpj}rxY`_If$14+~aKkj^;_dR4 zP=`0pDk8u+2jd>UJUgZw+c~_4+c?;DErSp+Sta9r=tiJRbma|d7Hs_h6S9!|9irV) zw8V5780QO$*|tL0Uk}Z+5|X$rpa7|0dcu3;=gVX?4lEWmW$SJFiQ4_gq@Yv9G;&^D zd>K`?l+hT6Nf^Goc9Ojx-tF$iJfr%m1mg}}n%`iN)UQSG%TDlP z>j`$9M!VkjN@(8~@myG%L>5ipG*s8=376XQb>5&iKgRHqr{)dI%T*Q`%TX>z%IlL~ z+V(v|+{(yK?TYtQrRVkHyH1i}BE-#zBHLkL(`W%~OSlc&akam{yIx9>46z3G3s%$* z22D`*Ki_DOfKG;GK_(uYjvrng{M0Z=x!23yET=`zFyQ9MQWp4W+s}2c@x*j`+0Ap` z2jrq34RioSk$z*M=RvT+P%OKSa8fI_5*N4jGQ8w&&XDyORJ`ml~ zYBw(O-Aq}#&cQ;}{WFkLFYc464TLHn+W0)h0XHtrJhX$S;~gRHrypzBz-HXDz<&K0 z-&0fbAVP0KqwtJyN9@DU%uhfAl1(q#iGYx)Vq}#t*_6UbGJMBD(m^Ie`M3QN5tK@F zfKHkIB_#T}UtQ@(G%zAgo(sK|*@`8bZ$Uz$2PTVG76Zx`AV0mgQYnQwBW%HN`i7v(u>AF6S5>x{q{ zhmLQNq4_5suK5@bhe^wlUk!>~K@$0FX-kK?K&46O=ayqEZku^I>p5ZBYOf@ilY! z;LouY1Nh-KJ8+-2*wV#7y~d7_EXKio#DE*8D&wwM@<%wUNQ!}`0BZpdkoVDzODN=L zTg!wiS)48C3U5z}7IOxzEn&bUM+`AVyAemjp87U_v_u?lyB))c`_p${EcCA03SKWl zYF1_uKNW$u7r?4cNB)ug_aX8cVb*xIh&|6dd~1n}syNnQ5!1Vdl-GN*NXM1M+DmzHUU}uci8qe|kij}RZroOB6$s(K889MZBExR&OLSOmD7({6&^W~Ew*cwglcgdrGR5*YazRxJ7&rC zVgdTLZh3r&`^Xdi4%O^#z>&sQcD=_(fOjF(kN0wYz~tOlisVLz`cBq;Szr&-mYA5x z29pOv({Q^l-od&+m-_(~c1tqa<)(z8RD6R+s|2K=DW{Eqfh{%9>J(z0=!kO_u-JrmU7P&{5ojJB!JX~aU}%|7yl==RjwQ)n zKe2I@_TFLt;&rk001xYW?V~o+A)5){%uoeac-xPZQbaB<1YP~31tY)Rn%43;;wiq{wcN~t;?4~B)(@-OO?$^RH|EY=wtzb8B zK0{Vy@<=@8WONlU7|mPjC-c@<@1Uq0H$V7yb(5hQal&pYyR=58on#fIy+D4jQS;Qo zKv}l&OdFQys#`cY_$zeN`m$frrum zLmxv&KGzEH9iK0{`yh$e+`yb#)<+_NFCslV>%zbNqNkm>6D<#535`DbXT9}FLz&+O z^U_B&yCZ&Ta7m=I62wNLSeBUIkFO5S??cW*h=oD&I|RyI^MJ;vD)we~idBFLpi0Uc z(5hy@F&2~z)CM$361X42x}fv!$zin9uXZ@ynHhyYmNvMH#*Ui%x$4BI8QAXx|6UO4 z9|r%RgUKZQdcXT_P$(r}MeNtEPS;7`VHO$!~CBzD+mkIFq)5gp)jD_=F$FqukYSmL+V?LZw zND~j1$Uwe4#;+e={I)9}`Q0iB$M7ypmPT~9=nXHJVX%+irUOtI0*BSH{Ydgud zNv_fT1nKlhb`sE2P#`EG#YK0~$u~a2`~}7`Mz6k^=A2}7vJYn83Z1$8Qz$E3Pj&`~ zYAdh4?qXkDblV7)?db`k7Tl(*cdk^nQcqk2Xa|VeyRf4Gu7Q{^aIZB%4E>2 zh`L(kIHWyV;WQl~ZAU=3yLz%vxz1J9DJH;Sr1ncJtJ#E&mg6!{%)OXTc=f^*7XJa2!Z^|jc8vGN}czl(>W7BSI?Xn6PfGD`Bw$AgrJ(K9i zni8Zn!R>leTmCx{adSBrtaKe2?dBU)HCP?{vivK`}Jg$|_r3YiF^6JRV+!`|w#EvW!3eJn`U=}?TT>fhz zTgggcRWC;88u_9<)nD@$hhu-wW_@^ZXj4C*{itB8&f_A^!SZ7BaF^9Bjt?u1R!n@a zS@=_v%c!RrxULD8SqnQ6pHr>>5WkQ2$n`n3j*?QdpPr^G*V4u7t5|5{f^ zF6~C7Bj;Gc&5W=64pjpsY43rPf3hR=(XY)zT~M&QplqS3)vCcts3zu^-ww>5!3;+C z&IGDNl&ip&aMWMjZNJOHhFh)3Ty}`!7@=8z?Ol^L$~!(F*HO9$b|-AMsb6sWsD5?Z zvQ?F#xuV2)k4SFG0WBsG5D;Fo98{7F zjM4)HF*yJb5j9j=mZ0mYM4}6{+&L2d(p6xIaBP{#SQiXv?mw3<_nV&BtH_fRhIjj z=iGEIms;yaVRbQy6{5}O?!mOHe&Rc+d>d^iv^j%tRAV_(*8uyd7T4j{^?L%0#LSQm z3zjbw?~;*`u{})N0v0+fwvRpqgdZkQ0$b%3$y*|T0Q()*sjjnSDR7e$wmxK-%pTcN zMNc&YmMNLkKkr>lb1N4NJ0^ijDXC(h37lt)Y}?bZB}w+qTip~F&r@b1W=GAOtk)V} z-Wwk6uN4LCxO+DRy!hh~0MJKjx?*gFpwk1+q&OBse!b9Qgdx~%Yv*5%LmWcl4-)TA z(m?5M?1SG%o1eGIdFX!G3VtU_;XO$AmB9=fu&}SLa=68TN9|#p)bm<75|5vYs5**% z`wBlaF<`3l0bYmFapENRklrU1&?b>-LzXLpTu!9*u23{)x0C8@k^&YnS|SnHsUfq- zt9VrIP36)Bz=^ubCW_=QkRLbo5&Z1e(f@sR+G0R^_GA=zM!mZ~5<7v1&cYiLDswiP z8(-u6YGZJv?5l5N^N@L8s~d5ZF$j0@c~H5XS|ghG10hYkm|33wLK+Ct&+vZHKV+_9 zU|T(-edDX=i>bnZhr4V)XK~vX$d~NDF8ts|BBnYVJKcpG8C!jmSGxWItkatac%O1C z?dU}}9fYkP<)qhQ2h=Fl%ZRp9j9ydqePrX5Xis&DEaH&LH zs7WZs1{hIp9tS2f~X0w6x}Yr*$SbWLR_BzMTr4&KyI=s?PlM%GWY| zjrwF9N0des?IZimXHTMrB&Q%GA1-E7IO~U%4oLUgWqIwlj;8EMIKQgGq;|iuVA?A| z9b)_f%6G6t9a_5#F;@L#+QuehrW9c9rQIsG+VEz&V%@#;p;}{4n8H z(EiBX%l!OY$lYFM;53&nkIVk$s{Q9*O=-&0=1(|)F}N5hS^E@lSN~sQI>~1mnLEv9 zxF2#6yJDQ*j4DAYT}?2x3uc<+Q9BQk{xyan0RjNkLZ?EGy-)m_ylvgF6(N!j>Hhi% z=wXF-OOdz6E;b#3o6obn8($bj6NR+k$hjBCi-`yTU;a!xY^ix*4uGyFc8H8_g*V<8 z_;v<3n^wOBz1DXZORU$k4?G6y!9bk04=FbMht(S9>SX7+yGgkL&ka|!?&g!|H6x%X zQw2~bXv}C5u9kMg90N}Jk;n{?v%T(bwwq9%ELkn25(BLdu5_ zwnmO0i?0Cig7k-fm3vtPz)g7NtL4Tk0QabLV#@JJZ20o+e`JYTaQ0%YRDrM~>Vp7# zB197sKcIZU`G1aJT+R+VwRV4DH<+Q&}`2Og?U z)ITZ1<^Rh*GW+$HeMGt!o$IV|4kSMld2OGu11qd^@Eel29Hzo4z7ED1C~=}rocm2`tU8E6kRsu zWc~#`@vh%!9RQ%*_x)ZW$pJsLU~Y4@P9|g-jbR$awd>x^IkE|YBpV2Dz*>z5$|!_Y zRfC~a@R>!tuw<8%MjFSLNZ?$~%P1=2mpI}`0;wX*mtP;#fx;Rv(FG)YC!!Sb6Bd#JiM~968JH@ zJ=*Su>&l&F^%z z9Lax@3z-h60)li#eknk`Xp;(Kcw6wjtEsjJHba~)Yh&UZ9pmmnR%Yf+P>Uy58R52FPyd&ZuFnwL*01t^OgPIf1ULO}1bG9a zE!FdE!{K;+I7HyT(Bh7#;wFYSfbw0GFCCVQ0h)N>wpO~mz$^U@ob1^bVrM65QFpp{ zp;SQ89rnZvEe=@{P?9=NHb7Q(hbO$BI8PZo?XIP_0ty|pe#fX{#wzW*6auT+*D}dL z13>e~o>2R*0TrP*-Ttut_bIQF^V&3>nz|kKJfb&#_xh7~#2@%D+sKQ@cICkTKwWEv z2wew0mqKOU;g$gCiG6ImkmA-{aV2WKnz8aSwJkRX3VH$LYad%hQ8M>2EXjhrB-pfV zFfI%F2~$nJnRotrIF)Sz4-Z%k0Oe4hv-L37Z|R1&7x6D10b9$Kq_=dwq+B!@ub@1L*TLF0NYu=dB9;K7AxX3@c-Q<$&cXA z=cUE%u>cf$F;_!8?Q~~zf1%AoQt11P<4pxD05%>36i4V{0MW(&dGYZ0YZ3yaj8Zo- z2$q)-;e2EFrik0Sf~cJw9xVQ;@|KRl3LfKr_4xCw+<5OugSj_MCp^q&UY!{)EveUU zy*WExPd(=e!NC2*f6#rf%p4#4Hpetp)>SlCq1N1A0*IF0JdOl@^Y0JF4hjOk&3JI< z{-5#(zv4e3-J>3sa z-ZpyAV5CL)lCxblPDNE?gP6tX;NWi^y3Id)?_jR)|5VHaNHG{z4#Q6&qZU$!Js?xk zrSCY^aonGgFm_f!9?|Ph6a7}KZ8{W<=IS%}2Ka_ko`#odRw8*|$pBpozMO>o!?O2q{ObJ;K+6#T;8qpq>Kz9w zU~t>1D#o&*`^G3>ANvo5kRcdq*Za_~3w-BKExR~{Hvlqm!S}`Mne*N?hMQ*67>j5rcdIX;yg%%r|<3U6tp6nmVRQKKY!5)!o45FJ&?d%#aX|;v#DNh4T zJP~CBBxD(eIa^*_P2DTVTbnZI58QkX10-KLzs1F-$0xT&F`VXTGnm|*e*>8dz#^v{ zek-NTu5MMLk3<4j0I@)Ze2%nq)UEUytV?20s^WM4jxHQins9D3>`*v!Ldn{Di-ozk zJbFCSF5^c=h;H$)YIeD=l`NiES2T-9&&jjmE-4}0jkQ3?$&)zb?S=1c$OGYpHQKm{ zP|pj0RD55^k5Ut9y50f7!nW1WSmoK`6d}`g*-=UGTo6-2_-mTp{+(`HQ5p#ZfESRNv)8~B(xdvNYyDw6!p{Pw87@dja*+EZoaZ_c zVFR9t_)P4VxXz{nsX2)+;!);7uhB@*v2QPcl{EblYbxT_U@Z}S>G6k$Q{KzoICSLBOTN|?K&x!tcJ#CS^b8KMcU?99Uz~*SII}pL zMLj$X3zvp=OGb+?_yi5Bz+Hpi;14S(++FGCE6r?Q%hkKgEAlAf!Q5!+A^q;LD|@(f zpRQr;NPpUY=Bd2uk&4G|t1dH1%mq0X)0u?H9o#I}QP81%?BpGAMOoJEI9{e4;Z$CA zAZ8FvVm#p<=5*Bn^K;U<;tIdeVZ>5s?TQys*1@fK?%hDK#gJoVV7`JUd4nhcxh>{B z*(^UJoy{4buf9wuIocbLbW`VSPt0~^c$?bzilmLhnmpb&JLgQf|15*kN^u%-BB2~K zl8XJM?$$SIHBX0>UIyiF>08HMW-~u+%eiL{p?O{>1}x{IWpLb8Kd2xr#Ot*2ih0mq z^Fdj@$#iviIv-feqeu-sI=>B`yQJXzekBw+TD6vQ&tE&e;C;p|E8f{CkDa>A^k+p& znm6j8=S%KUrK~OQ%_!~QWCPHlA&Y7Q-#y~&nQnl)Iy(ySbABXh2 zg=}T;<3~W7Vgu?o%E? zNC@RqirS|8GwP(G(@Q_=45kIpEvC8Rl45a?_0K1Da$FR=Pl_X)M)L*AY1Meyl%7aM zbQO~8qD~xMCebs`ShUw^E8d|e)Y z<#?6TE$nja4PLCM>r}_CPBn)Uyz#WoRIOO@#VZz-qZ=XuZ?(hMOFS$}@?!U&9sAlE zpgXnK$ap)Zqf`Fp$RV6F8B=e87CMCZ2m zM~ml5RK_kIKCEbQ*8Y@}UJi!unF)XhT~@Awu{Q<792Av`7-HZv78BzNuotmJ;g(v2 zAIn>1>&Xio%Z<9cxGc~zt+F1ZFHxGyQ{iA@@oU$yiMz>x1{ae!6ovWD!)D5H-yo+v zw&PaI(%37$8)(jOI$to{x_IrmJGM0kP-Y{UoE*L+^K7MwE--((bT_#wmm1$@X|^0` zvK>sixmsP2W+ov`={$b{vRM|>i|jYdH+53i&U&}bWiY)~P*Dlzd02(^3yb6BQl2v* zk4lF2E0w2h9Jb1C(V;i28Cb(g(@3nnEDt^xkEZc(l}t}*Ki5ABqUC;fA_mRbi9??D zk}s^UIS7#sdO>_eGp%OQXm}W06;S1k*6;mAWiDC?Hgr4`rY^FG9LOiaozs49ua=Lz zWr6f3(vv58yj^Cl^@Sc7+%PW<#a(XHq^KomX6P$EiN7a0U9W(C13}hUEM!_oIpJ7) zWJJ9B-rP{N=DTAU0QLN|WZ*3C4_!2tG&ptE!3UL}XcXBdznPY+yi(QLVeT->g<}DBr8AzAKuoXJo#?i1y^SbyA;p7Z=fAPdta8UR8M)>cYzI z5!Go$J>stKG0%-}^GdV1LGSJ}q9Qh>rEs~;UK`ucMA*m&OGGgG-9KdVIy_xOl}Yki zSG5#_#!n|q%)Cwfpbw@^5OVD0l@%qD^oIHdW`40Fsr?L5O-CRhEv^M?z?HdsAv&1f zq>yxP6>68HxbAZLYv@R=?VZkPJkQ&Q+;;|E%lADxZomZviW>R)S5jwTuwev zg)ngrnHDLYR;%y#Ko!K0Iju8B)Af-7673C5<3?!PGKtYX#7zlg=}(`togB|m^OT=T zO)2CQUO`nmT^S3R%_+Lo=O`;t|JJUd>Q9%`;g^Zfv(O4p^n)`~&-3_*9>F4HXk_8( z%N3&tvlmq_Ff&H{Yy7nZqh~pmRlR6owTeU40usqB_TJOUtgvkYwv-k_xPjl}BZuse zCU|UqUBzSZD{AYZ7(SuiwgVXE5Ehn7OwA9x`FKBm|8<);B3U<4aUPE0ERs1Mdlg zTJSwZm}O6O`Fri*pj|t6DIO$UBBt$$=Hy#S`YWNVdZvBNC!e<&<;$jiJ>B8b9Vd9^ znw;hFw$JU70aCa+l>(lL`H^knwKj-ecAHp;4&yP1yTabZ5wI}Xx!fyYZHbE-n1TDV zAa!`A_;#oiuD?)S-p~8*O4Op zCBeyJ!FQ&`Zy$*sD2Kh-4Jmq)d$`D9Mc1oMeyLY33AdcKkMStE=#4iXki-LZz?HoA zp?N->;K!2s201%s139{Arjs)&bc&J~{5U7>iPh0MT*6i`t7N(P0_RTl{Do*(ZxS8g5prW%JeI*v8&uRrBm?!78F6$qpu`5peu zpI`wI!Tm8Ox< zmziwrXdEUq0dWmM_rO3x3rF6bqU95M19s;yiS!_KF#@Zeebo|;!S_=%T#-TJRb-<6 zSnq^n%42)V%70jcP#3DkkQoWGW|wxLDorSiFR3vOQLEbdwYlB9u}Vc6nDvDXy|S%c zDA-!XWY|spvyJ+>PwDo0IirIXT^c=ECV0k;E%~w>R*KOX_dX-2n-e{b2oC2b)$I!> zXUOyNLp}QAcW7KMq^rS8+|J0gE3$H}X^B6h_swl$(aycthW%$1*_kO?nfKnDF;yg| zi*;yD1dTTLMC%q6M#6jIcC@TSd(n&8F$Hr(O=v~uz8$ebY-bCu=JS*6bgj1*<(b>+ zCL(YKJ~)BEX=9c_YEIDv$bQ<dUkB(6@f}kB>C4%DkUmW3<}EX$Qj4cF1E3 zA*+Ed)`>9Xd`qzVA!=ku&DQj>0eQC@stPtT4jJ<>u8K3rdpx1umk0%VGYw`@N`t?x z>0Bv-#48j&Z|sumrqs} z=>kaJ&Gme6!R0f;=K56ANeax2zvj(LFOIt3oCK^?0vTdS-Axn&+E&W&`b}#VuWVQ$ zD{16o@dT2ra!Uy=`R#-4kkmqp30{@qCCj0_h1k?I5BN@V7hxk*5m+OIdh8^eabu@c z-Hc>rGA&|kT)BR5KSY@QL~;2kZ4J*|81AUWFr^=w7MUpK{H%}aD@E9U0@e79&l4!t z#=(S~n03v`;~%_C>r-G(&@(j~$wGXg;rux`9%&_yPjYl72o|#{o)B0_vX8Pev}M_% zGDkg9%Pgv;5t}vulz?c|_ohSlydNIM1Eg~MC462HRp(vNH13y#?-8qM&!6o1Q5XP= zd*w)KqrMk2<$}@qn!UDl7gm6i&4-yQs}Lb{X2E8n+g#X}8g6W27CG^3-R9$h;o>W? z5(m9Aef1GlbMik;HqMA?GR`2NN1(5QagFi6-7H1aR8~%D1HHOwXWoY{TQ$||c;zJ|{JIY&;2*;h;7)W5jUok*9r&8H3iTuSqMcbG(h*&X@;pQvb2>glby zV5E~+6vIPW>^Y1a##%5!ugex^-0?0d5L*>=5*|Rf!}#((6}Cm_k%-e7h(D%vK8Nk> zt`ASl{fL0B?foGCy@SWCAiWOEG#WY7eRwoEiTGD);GZ9LEh#e6;(`=))QAbEE-C<<4!#0Tjdy(MAd}zh* zMjicdB~GN})T78rN^ZQ%>Ij<>c51<_=3Gj-jf0+` z^W(KoltsIjTcUisbsILof^0LpuKcDd8qQgJWoC28v$Q43X|Z)?QC*u5^@!{a$iT%e zIH~qso(}g(w)u6-2R;vktDM^Kk|2vC!5RKFgnPfD)<5||AE}zz=l%1hzu#G09v6s; z8jm0$w)qQ7@;X&a_k6Wm_|DrXxbd%g1wPii!IPZ0IGZxPo_oGq=zTHAL~*AGt*-5k zGUt5iw9dNY@xE2P`ruSds^qe;-e_9uhA(udP_mZ#fBNTZB_1aBl3*Iu6UB0s>ao_i zyZz$EiF#AY)3qj-(8vpkpV7RyQQ!)_(~F55 zm@_<$iz!bbTHK+9C~3CR;8b@cR$a#O%~z4sbH%pNEX9J?P^#OY3roqA*o)%p7m*ut z)2R&W2x?)6n$;r`Av})`?QEZb?9VxV&?dBVHl*nJ#VjAHbp_iM4+ky9fcc3?>13{u^EomUnspb=bOpWx7zPlfJ ziXb&wOh&nSDdV%1@@o$tj&Alyw=!9l z(m16!U9nYY*{a>z)o|o`9H-121GU`JvHE^r+v<7AYqbKZiw{$~U1t$1!l}U2bIfx5 zpRT>;{jaGRVpu=r?NN{VR@(V_>90_ILw~5} z%MAl(E1;{cP5LJbDhjhhDmv3uY5>i|$EsH=OFdJ2x;+2$nnO=9fn^L6SimTjdoJ3P zkFGekuvQi8V_8SsG_0qbrh;(h3VTQs=OP5b)0ML*jS7pyH_s>N<-S<~; z18W;6!P;hN=O4oABgpdDKBjjRlCeU#HCxT)O1Z^+#zOFks3owMA=)Ekg(?9t(f}ZC zwQ!bN0{5ML$ReO7;2-5mO+lh4-F^DHg)%tK9d(XFftO|8exueQtdXQr_xC`j_3QnM zJU8d1i!gM~Szq#|ZMvHs} zGYBH+Pm*Jjjg>Zjw7Ow`npYdrr5>lEbzv7=H(03iZIC#-3$)mT^w=G@G@qjMHPsXL z0gQ;z=R0&ut2;IK?W`0XQNQX<)sIgrpv{@K+s~vP;*go@=p0nJ)|G zl9O@5a@JX5xKn}_im=_F@Zg-0$FHD;=#RdU`x(@!>$Azszw~T@et**{el}T0M5bGE zgLuLzjiBukQ+r0LotnNn9J8qm>+1SBy1#!)RYMeS`P06Sx%Nv5e%O{t6c;o6DUY%|p3TTH3K~p+ z4LL-#e862-N3c{(#D%X_AfQz}?jCz_WQn1$Q3#EK_M=s!%xHC9$a?T7%-HZ2X@y|3 zBn69g2Hq@*oY>qcc}t9`GAe{#V21yEolPZ{xl>;rasF)xJKu5kLTO%jdA6R%-E0_m z7>?5`25kPDJ?8;sGG`SBNKEa#Gexa$TH{Q&C99-`eiJe^b`itTFIQmSz3Qd~^dH^u zzx3A-C#@uxQ#Fe8-UCvgdq5;7K4-J?Nv_ zwm7pU&qN)t%KBqJ^xsrd!g{yu<)Lg8VPXA&fXm-9*z%ni&S{e?q^*NZ-2w00+7>Cq zLcuF1#cko^a(0v#C75p;zQX6-4~3BgofmrK;UrUfC)~g}ccZdUIjbiNz`s)sFjp{T zDOo3iUyTSv5!3yh1--=@j8Fe21s0R^y?>%8La7E^$<;c`f6w+|31vL^GyByCF~T# zIGE6ruM+FxFgff=pbjtoR0>lYMS+2-V+GmzmfPzabX48Seo2%RD!|2Ju`z0h4$MzI|w-L-}`p@-lL%J@AM~{tnw6IsKYe9$5?=7D0zstl+ zprHn5AAYNK>Rnu=VjNj&D z#ABB4v&?axi7BMROw@K$`d{ZrP)(<01qTC$PUbufq*`+;`I50*#W!+`lUgaa`Ok~@ zsLJ}eP=Z5Y*iT<9;g%o)=F^uZ4dDDOOQWW>Db~+pkCGWZC3Y{@ceN&2()lyPe&x0e zXJj)fr|vc_Q)^{YcScSQHyvepP0wdU9lB3k?cOvKl5*`xut}FO?fApsi^VIJSoYG01RG@nPa`mK=1 z7O-Q6;gvJcZtX{T8`f6rE1uhLG-sf2x;uit@N^YyJRgb>K+Ipm4*I>$ zy1%;x6Ayu84DxGT+u7>cl!kYq*09UAA;{;)4Tv(LpH-mRXBAr3oSlwzd;DBA=uf+tI3=-UQvl zcJK~hQxzd`RoUSe0^#99qty5c-U<`{Vb9h#M8yNvot|C38TuTj7HW6HE-ePKlPc}P zeeFe!JFdNqp&Qop=I9$j5@6r99=JzCAR{ zo2u64w-&?$hrT4PFiKo8ei^{&(}Q1K!=WsghFMcD%B7hcKWaD#bHg zLNhN=LVYSJwU-bhQ8NL)ZDnpyPke6#Co3sXJkt6XSWdN#YY4r{+pkpqRg(!5A9LpX zW7XbFu370L&#%>bOE?L7C#dr+MHRpav*${?r#_nW25@$}M|nQl?7j%S9h+p}U&F*Z z*Fw^3o|$FV_(UzNR#fID`qbB1&S9e3t5+?Lt+pS-zytSVqBE75czbiYPRXdri4y~P zQ5DK*Nrmua#5>D_(iYXy32VgeV;nWuI(LBMEsH;*USL;|y}l55KGP`Ty`69S#}Nk; zdiUP9EQwV)*bcbC!sOj(k)cAVo3zQvhJ~VMB^7@Csw#Dx9qDkx)nBqBXpb!)kXOr( zO5Jc>BhPmlz2b6$!e`Z>kzpkh=Ph{d(dEkc;?t}mML6Bl&?t7ZrH-Z|moVfwkvBeu zgwx^a{=Zg%hv;IjCeK!%16pr&maAEb$VV=_)21p^u?sZaI^2_-3|sCxy9ui-wZbC2 zog0ng(Yri4W0Fi2UWmdkNWh;EUQ&u{6qi!F=w+}e*KZ59ab$O1ntv^=$UU`xu~j87 zhmQ@jU~Sfk9J%`?aW@dwSCm%xCa2<2nb)2vnFX5W0uAf!^oU$TC?-giG4X>1rsbc0 zu6XA#la-|V0!h|3`HU3t8I zI;5ksS9((jy)L(X%8Eiln=Q^j4>jNWUcU}qlD^TCGRH5Rs_Fi?>Oq6L(!qaLmzk({ z9Q&fp5uMn#c9lHuxN+F={ur^hWlv|$SmgYNpk|64o7=WNHw@lmY}GIj(zot0VgLN4 zG;?*5B{(+0fc0}4<#e2;qUypz75De0bK)x}JKh_=eDi0nQD+16(UiUshmss~>xp5` zH+s{n2U#9ygD%QI2;Hz}i+LZ>i+^NnJveOA{PD-c20Frarj77QJVsNo&gmY-Wl!YK zS51T`6|l7M#idIsqHdKkii+u^x@SF2=0SBFJwbPc)J24#{_HuA~xv5c^ zeckd~H0Wmay4=R`lvn-Ls-;t{jd>|X*oD$=YVcwDmrT^TgJ4t^j!8k9B=PNRb=qtI z6l??#fYogNAxJ~ik{<-yO6wvBSf9oKQlpY;v{@KF#Kd<9*1A9R>=u_y)dcvcshuDK zn@Td5KtK6cx+Go<#*6e*<}F~8;0T0uH9ABq)>^RDefNx*Bqm-Bcrxlt&ljSaKz0Hm zETb0(xGxzf#a1g4HW_#Ts)gY7JNPBWiiy{4lY#g7Qu9Q5pCs6WFj2Q323Z55{~aKX zoAp9P=r*E2b<=wP8!AMBG$@!XRIQ>>0-H0%&7@}nvuQ~|^5AcDL$_;*dQ-+-PJBRt zAI~0QuMs9PGwKs|jUU>97%AHQdFzsw`nbzf#Dhdf+{Oz?%Admo@txPmb8vjHw$1w@ ztdINnwQQLl#R7(cktxYLb8F@*>Y$FfBZn4ZBb}MDS{25@>g- g$i5rR?4kT9y5{*3C+_iKG4ON7)bb?Z#N`|R0rAQCivR!s literal 0 HcmV?d00001 From 280ff9b0dd4a27b1dc21afc71d74e8f0ff60a671 Mon Sep 17 00:00:00 2001 From: Tamas Balog Date: Wed, 30 Nov 2022 11:59:17 +0100 Subject: [PATCH 5/9] Add class and web references to the dev docs --- DEVELOPMENT.md | 54 +++++++++++++++++++++++++------------------------- 1 file changed, 27 insertions(+), 27 deletions(-) diff --git a/DEVELOPMENT.md b/DEVELOPMENT.md index a458762..536f5a8 100644 --- a/DEVELOPMENT.md +++ b/DEVELOPMENT.md @@ -136,13 +136,13 @@ In order to perform code analysis, users must have a `codiga.yml` file in their with at least one valid ruleset name. If there is no ruleset name, or no valid ruleset name defined, then code analysis will not be performed. -[`CodigaConfigFileUtil`]() is responsible for finding this config file in the Solution root, and for parsing this config file into a -[`CodigaCodeAnalysisConfig`]() containing the list of ruleset names. +[`CodigaConfigFileUtil`](/src/Extension/Rosie/CodigaConfigFileUtil.cs) is responsible for finding this config file in the Solution root, and for parsing this config file into a +[`CodigaCodeAnalysisConfig`](/src/Extension/Rosie/CodigaCodeAnalysisConfig.cs) containing the list of ruleset names. -Here comes in [`RosieRulesCache`]() which provides a periodic background thread for polling the contents of this config file, +Here comes in [`RosieRulesCache`](/src/Extension/Rosie/RosieRulesCache.cs) which provides a periodic background thread for polling the contents of this config file, and looking for rule changes on Codiga Hub, as well as the caches the received rules per language. -The rules are retrieved via [`RosieClient`](), and this is where `RosieRulesCache` is initialized before sending the first +The rules are retrieved via [`RosieClient`](/src/Extension/Rosie/RosieClient.cs), and this is where `RosieRulesCache` is initialized before sending the first request to the Rosie server. This way, it is initialized only when code analysis is actually needed. For response/request (de)serialization, you can find the model classes in the `Extension.Rosie.Model` namespace. @@ -158,20 +158,20 @@ This information can be user-visible or not, can hold arbitrary information or c To get a proper understanding of tagging, it is necessary to know a bit about the following editor related classes: -| Class | Functionality | -|------------------------------------------------------------------------------------------------------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| [`ITextView`](https://learn.microsoft.com/en-us/dotnet/api/microsoft.visualstudio.text.editor.itextview?view=visualstudiosdk-2022) | It is a higher level view of a document being edited. This view may be associated with the editor itself, small code peek windows, or color highlighting on the scrollbar. | -| [`ITextBuffer`](https://learn.microsoft.com/en-us/dotnet/api/microsoft.visualstudio.text.itextbuffer?view=visualstudiosdk-2022) | A lower level view of a document via which you can also perform certain types of edits on the document. An `ITextView` instance holds a reference to an `ITextBuffer`. | +| Class | Functionality | +|----------------------------------------------------------------------------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| [`ITextView`](https://learn.microsoft.com/en-us/dotnet/api/microsoft.visualstudio.text.editor.itextview) | It is a higher level view of a document being edited. This view may be associated with the editor itself, small code peek windows, or color highlighting on the scrollbar. | +| [`ITextBuffer`](https://learn.microsoft.com/en-us/dotnet/api/microsoft.visualstudio.text.itextbuffer) | A lower level view of a document via which you can also perform certain types of edits on the document. An `ITextView` instance holds a reference to an `ITextBuffer`. | The tagging functionality is provided by the VS platform via the following set of classes: -| Class | Functionality | -|-----------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| `ITag` | A type of marker to provide arbitrary information for a span of text. | -| `IErrorTag` | An implementation of `ITag` that provides so-called squiggles for a span of text.
You can configure the type of the squiggle, which can be a custom type defined by the extension developer, and the tooltip to show on mouse-hover of the associated span of text. | -| `ITagger` | Provides the logic based on which `ITag` markers are created and associated with a span of text. | -| `IViewTaggerProvider` | Provides `ITagger` instances for an `ITextView`-`ITextBuffer` pair. | -| `ITagAggregator` | Aggregates the list of tags of the specified `ITag` type from an editor. | +| Class | Functionality | +|-------------------------------------------------------------------------------------------------------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| [`ITag`](https://learn.microsoft.com/en-us/dotnet/api/microsoft.visualstudio.text.tagging.itag) | A type of marker to provide arbitrary information for a span of text. | +| [`IErrorTag`](https://learn.microsoft.com/en-us/dotnet/api/microsoft.visualstudio.text.tagging.ierrortag) | An implementation of `ITag` that provides so-called squiggles for a span of text.
You can configure the type of the squiggle, which can be a custom type defined by the extension developer, and the tooltip to show on mouse-hover of the associated span of text. | +| [`ITagger`](https://learn.microsoft.com/en-us/dotnet/api/microsoft.visualstudio.text.tagging.itagger-1) | Provides the logic based on which `ITag` markers are created and associated with a span of text. | +| [`IViewTaggerProvider`](https://learn.microsoft.com/en-us/dotnet/api/microsoft.visualstudio.text.tagging.iviewtaggerprovider) | Provides `ITagger` instances for an `ITextView`-`ITextBuffer` pair. | +| [`ITagAggregator`](https://learn.microsoft.com/en-us/dotnet/api/microsoft.visualstudio.text.tagging.itagaggregator-1) | Aggregates the list of tags of the specified `ITag` type from an editor. | **External resources:** - MSDN: [Highlighting text](https://learn.microsoft.com/en-us/visualstudio/extensibility/walkthrough-highlighting-text?view=vs-2022&tabs=csharp) @@ -184,12 +184,12 @@ The tagging functionality is provided by the VS platform via the following set o The tagging logic is separated into two branches of classes to properly be able to provide Rosie violation and error squiggles related information. Their functionality is detailed in their code documentation. -| Classification | Rosie violations | Squiggles | -|-----------------------|---------------------------------------------------------------|---------------------------------------------------------------------------------| -| | Stores information about the violation returned from Rosie. | Stores the color definition and tooltip of the violation to render it to users. | -| `ITag`/`IErrorTag` | [`RosieViolationTag`]() | [`RosieViolationSquiggleTag`]() | -| `ITagger` | [`RosieViolationTagger`]() | [`RosieViolationSquiggleTagger`]() | -| `IViewTaggerProvider` | [`RosieViolationTaggerProvider`]() | [`RosieViolationSquiggleTaggerProvider`]() | +| Classification | Rosie violations | Squiggles | +|-----------------------|---------------------------------------------------------------------------------------------------|-------------------------------------------------------------------------------------------------------------------| +| | Stores information about the violation returned from Rosie. | Stores the color definition and tooltip of the violation to render it to users. | +| `ITag`/`IErrorTag` | [`RosieViolationTag`](/src/Extension/Rosie/Annotation/RosieViolationTag.cs) | [`RosieViolationSquiggleTag`](/src/Extension/Rosie/Annotation/RosieViolationSquiggleTag.cs) | +| `ITagger` | [`RosieViolationTagger`](/src/Extension/Rosie/Annotation/RosieViolationTagger.cs) | [`RosieViolationSquiggleTagger`](/src/Extension/Rosie/Annotation/RosieViolationSquiggleTagger.cs) | +| `IViewTaggerProvider` | [`RosieViolationTaggerProvider`](/src/Extension/Rosie/Annotation/RosieViolationTaggerProvider.cs) | [`RosieViolationSquiggleTaggerProvider`](/src/Extension/Rosie/Annotation/RosieViolationSquiggleTaggerProvider.cs) | #### Tagging flow on file open @@ -211,17 +211,17 @@ The last step will trigger a call on `RosieViolationSquiggleTagger.GetTags()` an Lightbulb actions are actions that are provided in a context of texts or language elements. They are available and created when there is at least one violation (a `RosieViolationTag`) available -for a span of text in the editor. Lightbulb actions are provided by [`RosieHighlightActionsSourceProvider`]() and [`RosieHighlightActionsSource`](). +for a span of text in the editor. Lightbulb actions are provided by [`RosieHighlightActionsSourceProvider`](/src/Extension/Rosie/Annotation/RosieHighlightActionsSourceProvider.cs) and [`RosieHighlightActionsSource`](/src/Extension/Rosie/Annotation/RosieHighlightActionsSource.cs). MSDN documentation: [Displaying lightbulb suggestions](https://learn.microsoft.com/en-us/visualstudio/extensibility/walkthrough-displaying-light-bulb-suggestions?view=vs-2022) There are three lightbulb actions (quick fixes) available for each violation: -| Action | Behaviour | Implementation classes | Availability | -|------------------------|----------------------------------------------------------------------------------------------------------------------|--------------------------------------------|-----------------------------------------------------------------------| -| **Apply fix** | It applies the fix, a series of code edits. | [`ApplyRosieFixSuggestedAction`]() | Available only when the violation returned from Rosie contains a fix. | -| **Disable analysis** | It adds the `codiga-disable` comment above the violation's line, thus tells Rosie to disable analysis for that line. | [`DisableRosieAnalysisSuggestedAction`]() | Always available. | -| **Open on Codiga Hub** | It opens the rule's page on Codiga Hub in a web browser. | [`OpenOnCodigaHubSuggestedAction`]() | Always available. | +| Action | Behaviour | Implementation classes | Availability | +|------------------------|----------------------------------------------------------------------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------|-----------------------------------------------------------------------| +| **Apply fix** | It applies the fix, a series of code edits. | [`ApplyRosieFixSuggestedAction`](/src/Extension/Rosie/Annotation/ApplyRosieFixSuggestedAction.cs) | Available only when the violation returned from Rosie contains a fix. | +| **Disable analysis** | It adds the `codiga-disable` comment above the violation's line, thus tells Rosie to disable analysis for that line. | [`DisableRosieAnalysisSuggestedAction`](/src/Extension/Rosie/Annotation/DisableRosieAnalysisSuggestedAction.cs) | Always available. | +| **Open on Codiga Hub** | It opens the rule's page on Codiga Hub in a web browser. | [`OpenOnCodigaHubSuggestedAction`](/src/Extension/Rosie/Annotation/OpenOnCodigaHubSuggestedAction.cs) | Always available. |
From 99c813b01bb3b6d464dea680656231ac83e289de Mon Sep 17 00:00:00 2001 From: Tamas Balog Date: Wed, 30 Nov 2022 12:20:52 +0100 Subject: [PATCH 6/9] Revert NUnit3TestAdapter to v4.2.1 --- src/Tests/Tests.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Tests/Tests.csproj b/src/Tests/Tests.csproj index f9b47d6..3ebed6b 100644 --- a/src/Tests/Tests.csproj +++ b/src/Tests/Tests.csproj @@ -22,7 +22,7 @@ runtime; build; native; contentfiles; analyzers; buildtransitive - + From 973030acd64a13925c45f1dab7ca574481eed4ee Mon Sep 17 00:00:00 2001 From: Tamas Balog Date: Wed, 30 Nov 2022 13:03:23 +0100 Subject: [PATCH 7/9] Remove Microsoft.VisualStudio.Imaging.Interop.14.0.DesignTime package and move NUnit3TestAdapter back to 4.3.0 --- src/Extension/Extension.csproj | 3 --- src/Tests/Tests.csproj | 2 +- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/src/Extension/Extension.csproj b/src/Extension/Extension.csproj index fcd26c5..64738dc 100644 --- a/src/Extension/Extension.csproj +++ b/src/Extension/Extension.csproj @@ -169,9 +169,6 @@ 17.3.198 - - 17.3.32803.143 - 17.3.198 diff --git a/src/Tests/Tests.csproj b/src/Tests/Tests.csproj index 3ebed6b..f9b47d6 100644 --- a/src/Tests/Tests.csproj +++ b/src/Tests/Tests.csproj @@ -22,7 +22,7 @@ runtime; build; native; contentfiles; analyzers; buildtransitive - + From 247dca4b991bf504ad694056fb1545f13711abd4 Mon Sep 17 00:00:00 2001 From: Tamas Balog Date: Wed, 30 Nov 2022 15:49:13 +0100 Subject: [PATCH 8/9] Upgrade NUnit.ConsoleRunner to 3.16.0 --- .github/workflows/build.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 50e3611..07091e6 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -25,10 +25,10 @@ jobs: run: msbuild Extension.sln /restore /v:m /property:Configuration=${{ inputs.config }} - name: Install NUnit.ConsoleRunner - run: nuget install NUnit.ConsoleRunner -Version 3.13.0 -DirectDownload -OutputDirectory . + run: nuget install NUnit.ConsoleRunner -Version 3.16.0 -DirectDownload -OutputDirectory . - name: Run UnitTests - run: ./NUnit.ConsoleRunner.3.13.0/tools/nunit3-console.exe src\Tests\bin\${{ inputs.config }}\net48\Tests.dll + run: ./NUnit.ConsoleRunner.3.16.0/tools/nunit3-console.exe src\Tests\bin\${{ inputs.config }}\net48\Tests.dll - name: Publish test results uses: EnricoMi/publish-unit-test-result-action/composite@v2 From 5fc45fd9a21cbafa1e7118f436bdaa2ca576657e Mon Sep 17 00:00:00 2001 From: dastrong-codiga <100973772+dastrong-codiga@users.noreply.github.com> Date: Wed, 30 Nov 2022 08:49:00 -0800 Subject: [PATCH 9/9] fix: image links --- DEVELOPMENT.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/DEVELOPMENT.md b/DEVELOPMENT.md index 536f5a8..40551eb 100644 --- a/DEVELOPMENT.md +++ b/DEVELOPMENT.md @@ -196,14 +196,14 @@ information. Their functionality is detailed in their code documentation. When a user opens a file, or a file is already open when a Solution is being opened, the following flow of actions are performed to have tagging in the editor, including the extension initialization steps: -![Tagging flow initials](images\tagging-flow-initial.png) +![Tagging flow initials](images/tagging-flow-initial.png) #### Tagging flow during document editing When a user makes a modification in a file (regardless of the file also being saved), the following event handling chain is performed, so that every affected component is notified that they should call an update on tagging and suggested actions: -![Tagging flow during editing](images\tagging-flow-during-editing.png) +![Tagging flow during editing](images/tagging-flow-during-editing.png) The last step will trigger a call on `RosieViolationSquiggleTagger.GetTags()` and will perform the same tag generation and collection steps as on the flow diagram above.