diff --git a/New_Extensibility_Model/Samples/ClassificationSample/ClassificationSample.csproj b/New_Extensibility_Model/Samples/ClassificationSample/ClassificationSample.csproj new file mode 100644 index 00000000..388495cb --- /dev/null +++ b/New_Extensibility_Model/Samples/ClassificationSample/ClassificationSample.csproj @@ -0,0 +1,13 @@ + + + net8.0-windows8.0 + enable + 12 + en-US + + + + + + + diff --git a/New_Extensibility_Model/Samples/ClassificationSample/CsvTagger.cs b/New_Extensibility_Model/Samples/ClassificationSample/CsvTagger.cs new file mode 100644 index 00000000..bc80fbb1 --- /dev/null +++ b/New_Extensibility_Model/Samples/ClassificationSample/CsvTagger.cs @@ -0,0 +1,147 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +namespace ClassificationSample; + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text.RegularExpressions; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.VisualStudio.Extensibility.Editor; + +#pragma warning disable VSEXTPREVIEW_TAGGERS // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. + +internal class CsvTagger : TextViewTagger +{ + private const string QuoteMatchName = "quote"; + private const string EscapedQuoteMatchName = "escapedQuote"; + private const string SeparatorMatchName = "separator"; + private const string FieldTextMatchName = "fieldText"; + + // Matches any sequence of characters, not containing '"' and ','. It also matches any + // sequence of characters enclosed in '"' as long as they don't contain other '"' + // characters, unless they are escaped (doubled). + // Examples of valid matches: xxx, "xxx", "xx""x" + private const string FieldRegex = $@"(((?<{QuoteMatchName}>"")((?<{FieldTextMatchName}>[^""]+)|(?<{EscapedQuoteMatchName}>""""))*(?<{QuoteMatchName}>""))|(?<{FieldTextMatchName}>([^,""])*))"; + + // Matches multiple fields separated by ',' + // This regex supports quoted fields, escaped quotes (inside fields). We are not supporting + // line-breaks inside a field. + // This regex is not terminated with '$' so that as much as possible of the beginning of + // the line is always matched, even in case of syntax error. + private static readonly Regex LineRegex = new($@"^{FieldRegex}((?<{SeparatorMatchName}>,){FieldRegex})*", RegexOptions.Compiled | RegexOptions.ExplicitCapture); + + private readonly CsvTaggerProvider provider; + private readonly Uri documentUri; + + public CsvTagger(CsvTaggerProvider provider, Uri documentUri) + { + this.provider = provider; + this.documentUri = documentUri; + } + + public override void Dispose() + { + this.provider.RemoveTagger(this.documentUri, this); + base.Dispose(); + } + + public async Task TextViewChangedAsync(ITextViewSnapshot textView, IReadOnlyList edits, CancellationToken cancellationToken) + { + if (edits.Count == 0) + { + return; + } + + var allRequestedRanges = await this.GetAllRequestedRangesAsync(textView.Document, cancellationToken); + await this.CreateTagsAsync( + textView.Document, + allRequestedRanges.Intersect(// Use Intersect to only create tags for ranges that VS has previously expressed interested in. + edits.Select(e => + EnsureNotEmpty(// Fix empty ranges to be at least 1 character long so that they are not ignored when intersected (empty ranges are the result of text deletion). + e.Range.TranslateTo(textView.Document, TextRangeTrackingMode.ExtendForwardAndBackward))))); // Translate the range to the new document version. + } + + protected override async Task RequestTagsAsync(NormalizedTextRangeCollection requestedRanges, bool recalculateAll, CancellationToken cancellationToken) + { + if (requestedRanges.Count == 0) + { + return; + } + + await this.CreateTagsAsync(requestedRanges.TextDocumentSnapshot!, requestedRanges); + } + + private static TextRange EnsureNotEmpty(TextRange range) + { + if (range.Length > 0) + { + return range; + } + + int start = Math.Max(0, range.Start - 1); + int end = Math.Min(range.Document.Length, range.Start + 1); + + return new(range.Document, start, end - start); + } + + private async Task CreateTagsAsync(ITextDocumentSnapshot document, IEnumerable requestedRanges) + { + List> tags = new(); + List ranges = new(); + foreach (var lineNumber in requestedRanges.SelectMany(r => + { + // Convert the requested range to line numbers. + var startLine = r.Document.GetLineNumberFromPosition(r.Start); + var endLine = r.Document.GetLineNumberFromPosition(r.End); + return Enumerable.Range(startLine, endLine - startLine + 1); + + // Use Distinct to avoid processing the same line multiple times. + }).Distinct()) + { + var line = document.Lines[lineNumber]; + var match = LineRegex.Match(line.Text.CopyToString()); + + if (match.Success) + { + // VisualStudio.Extensibility doesn't support defining text colors for + // new classification types yet, so we must use existing classification + // types. + foreach (Capture capture in match.Groups[FieldTextMatchName].Captures) + { + AddTag(capture, lineNumber == 0 ? ClassificationType.KnownValues.SymbolDefinition : ClassificationType.KnownValues.String); + } + + foreach (Capture capture in match.Groups[QuoteMatchName].Captures) + { + AddTag(capture, ClassificationType.KnownValues.BracePairLevelOne); + } + + foreach (Capture capture in match.Groups[EscapedQuoteMatchName].Captures) + { + AddTag(capture, ClassificationType.KnownValues.BracePairLevelTwo); + } + + foreach (Capture capture in match.Groups[SeparatorMatchName].Captures) + { + AddTag(capture, ClassificationType.KnownValues.Operator); + } + } + + void AddTag(Capture capture, ClassificationType classificationType) + { + tags.Add(new(new(document, line.Text.Start + capture.Index, capture.Length, TextRangeTrackingMode.ExtendNone), new(classificationType))); + } + + // Add the range to the list of ranges we have calculated tags for. We add the range even if no tags + // were created for it, this takes care of clearing any tags that were previously created for this + // range and are not valid anymore. + ranges.Add(new(document, line.TextIncludingLineBreak.Start, line.TextIncludingLineBreak.Length)); + } + + // Return the ranges we have calculated tags for and the tags themselves. + await this.UpdateTagsAsync(ranges, tags, CancellationToken.None); + } +} diff --git a/New_Extensibility_Model/Samples/ClassificationSample/CsvTaggerProvider.cs b/New_Extensibility_Model/Samples/ClassificationSample/CsvTaggerProvider.cs new file mode 100644 index 00000000..2c7bcf8b --- /dev/null +++ b/New_Extensibility_Model/Samples/ClassificationSample/CsvTaggerProvider.cs @@ -0,0 +1,81 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +namespace ClassificationSample; + +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.VisualStudio.Extensibility; +using Microsoft.VisualStudio.Extensibility.Editor; + +#pragma warning disable VSEXTPREVIEW_TAGGERS // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. + +[VisualStudioContribution] +internal class CsvTaggerProvider : ExtensionPart, ITextViewTaggerProvider, ITextViewChangedListener +{ + private readonly object lockObject = new(); + private readonly Dictionary> taggers = new(); + + [VisualStudioContribution] + public static DocumentTypeConfiguration CsvDocumentType => new("csv") + { + FileExtensions = new[] { ".csv" }, + BaseDocumentType = DocumentType.KnownValues.PlainText, + }; + + public TextViewExtensionConfiguration TextViewExtensionConfiguration => new() + { + AppliesTo = [DocumentFilter.FromDocumentType(CsvDocumentType)], + }; + + public async Task TextViewChangedAsync(TextViewChangedArgs args, CancellationToken cancellationToken) + { + List tasks = new(); + lock (this.lockObject) + { + if (this.taggers.TryGetValue(args.AfterTextView.Uri, out var taggers)) + { + foreach (var tagger in taggers) + { + tasks.Add(tagger.TextViewChangedAsync(args.AfterTextView, args.Edits, cancellationToken)); + } + } + } + + await Task.WhenAll(tasks); + } + + Task> ITextViewTaggerProvider.CreateTaggerAsync(ITextViewSnapshot textView, CancellationToken cancellationToken) + { + var tagger = new CsvTagger(this, textView.Document.Uri); + lock (this.lockObject) + { + if (!this.taggers.TryGetValue(textView.Document.Uri, out var taggers)) + { + taggers = new(); + this.taggers[textView.Document.Uri] = taggers; + } + + taggers.Add(tagger); + } + + return Task.FromResult>(tagger); + } + + internal void RemoveTagger(Uri documentUri, CsvTagger toBeRemoved) + { + lock (this.lockObject) + { + if (this.taggers.TryGetValue(documentUri, out var taggers)) + { + taggers.Remove(toBeRemoved); + if (taggers.Count == 0) + { + this.taggers.Remove(documentUri); + } + } + } + } +} diff --git a/New_Extensibility_Model/Samples/ClassificationSample/ExtensionEntrypoint.cs b/New_Extensibility_Model/Samples/ClassificationSample/ExtensionEntrypoint.cs new file mode 100644 index 00000000..1f6fb41e --- /dev/null +++ b/New_Extensibility_Model/Samples/ClassificationSample/ExtensionEntrypoint.cs @@ -0,0 +1,31 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +namespace ClassificationSample; + +using Microsoft.Extensions.DependencyInjection; +using Microsoft.VisualStudio.Extensibility; + +/// +/// Extension entry point for the ClassificationSample extension. +/// +[VisualStudioContribution] +public class ExtensionEntrypoint : Extension +{ + /// + public override ExtensionConfiguration ExtensionConfiguration => new() + { + Metadata = new( + id: "ClassificationSample.2e2edd6e-ccf8-4303-a159-068724c63ab0", + version: this.ExtensionAssemblyVersion, + publisherName: "Microsoft", + displayName: "Classification Sample Extension", + description: "Sample extension demonstrating contributing a classification tagger"), + }; + + /// + protected override void InitializeServices(IServiceCollection serviceCollection) + { + base.InitializeServices(serviceCollection); + } +} diff --git a/New_Extensibility_Model/Samples/ClassificationSample/README.md b/New_Extensibility_Model/Samples/ClassificationSample/README.md new file mode 100644 index 00000000..9b21bdbc --- /dev/null +++ b/New_Extensibility_Model/Samples/ClassificationSample/README.md @@ -0,0 +1,55 @@ +--- +title: Classification Extension Sample reference +description: A reference for Classification sample +date: 2025-04-22 +--- + +# Classification Extension Sample + +This extension creates a tagger that classifies the text of CSV files. + +A detailed walkthrough of how to create a tagger is available in the +[Taggers sample readme file](../TaggersSample/README.md). Please read +that first. + +This sample's `CsvTaggerProvider` and `CsvTagger` are equivalent to `MarkdownTextMarkerTaggerProvider` and `MarkdownTextMarkerTagger`. + +## Classification + +Classification can be performed by an extension by implementing an +`ITextViewTaggerProvider` and have the `TextViewTagger<>` +generate `ClassificationTag` values. + +```cs +tags.Add( + new TaggedTrackingTextRange( + new TrackingTextRange( + document, + tagStartPosition, + tagLength, + TextRangeTrackingMode.ExtendNone), + new ClassificationTag(ClassificationType.KnownValues.Operator))); + +``` + +At this time, VisualStudio.Extensibility doesn't support defining text colors for +new classification types yet, so we must use existing classification types. + +VSSDK-compatible extensions, can use [ClassificationTypeDefinition](https://learn.microsoft.com/dotnet/api/microsoft.visualstudio.text.classification.classificationtypedefinition) +to define new classification types. Their name can be referenced using +`ClassificationType.Custom`. + +## Performance considerations + +Since `CsvTagger` doesn't support CSV fields containing line breaks, the +tagger can perform parsing of any single line of the CSV file independently +from each other. This allows the tagger to only act on the modified lines. + +We further optimize the tag generation by intersecting the edited text ranges +with the ranges that have been previously requested (`GetAllRequestedRangesAsync`). +This is particularly useful if the user pastes a large amount of text into +the file resulting in lines being edited that don't fall into the currently +visible portion of the view. + + If these optimization were not possible, we would have to avoid generating tags + on each edit of the document (see the [Implementing "slow" taggers](../TaggersSample/README.md#implementing-slow-taggers) chapter of the Taggers sample readme file). diff --git a/New_Extensibility_Model/Samples/CodeLensSample/CodeLensSample.csproj b/New_Extensibility_Model/Samples/CodeLensSample/CodeLensSample.csproj index 2d21a8dd..e44a747c 100644 --- a/New_Extensibility_Model/Samples/CodeLensSample/CodeLensSample.csproj +++ b/New_Extensibility_Model/Samples/CodeLensSample/CodeLensSample.csproj @@ -7,8 +7,8 @@ - - + + diff --git a/New_Extensibility_Model/Samples/CommandParentingSample/CommandParentingSample.csproj b/New_Extensibility_Model/Samples/CommandParentingSample/CommandParentingSample.csproj index de6950d4..e5567038 100644 --- a/New_Extensibility_Model/Samples/CommandParentingSample/CommandParentingSample.csproj +++ b/New_Extensibility_Model/Samples/CommandParentingSample/CommandParentingSample.csproj @@ -5,13 +5,10 @@ enable enable 12 - - - https://pkgs.dev.azure.com/azure-public/vside/_packaging/msft_consumption/nuget/v3/index.json;$(RestoreAdditionalProjectSources) - + - - + + diff --git a/New_Extensibility_Model/Samples/CommentRemover/CommentRemover.csproj b/New_Extensibility_Model/Samples/CommentRemover/CommentRemover.csproj index 355ec26a..49776900 100644 --- a/New_Extensibility_Model/Samples/CommentRemover/CommentRemover.csproj +++ b/New_Extensibility_Model/Samples/CommentRemover/CommentRemover.csproj @@ -11,8 +11,8 @@ - - + + diff --git a/New_Extensibility_Model/Samples/CompositeExtension/CompositeExtension/CompositeExtension.csproj b/New_Extensibility_Model/Samples/CompositeExtension/CompositeExtension/CompositeExtension.csproj index b442cc82..8b911041 100644 --- a/New_Extensibility_Model/Samples/CompositeExtension/CompositeExtension/CompositeExtension.csproj +++ b/New_Extensibility_Model/Samples/CompositeExtension/CompositeExtension/CompositeExtension.csproj @@ -9,8 +9,8 @@ - - + + diff --git a/New_Extensibility_Model/Samples/CompositeExtension/OutOfProcComponent/OutOfProcComponent.csproj b/New_Extensibility_Model/Samples/CompositeExtension/OutOfProcComponent/OutOfProcComponent.csproj index 5fcc945b..47969e1e 100644 --- a/New_Extensibility_Model/Samples/CompositeExtension/OutOfProcComponent/OutOfProcComponent.csproj +++ b/New_Extensibility_Model/Samples/CompositeExtension/OutOfProcComponent/OutOfProcComponent.csproj @@ -17,8 +17,8 @@ - - + + diff --git a/New_Extensibility_Model/Samples/DialogSample/DialogSample.csproj b/New_Extensibility_Model/Samples/DialogSample/DialogSample.csproj index b2c63962..65f0705d 100644 --- a/New_Extensibility_Model/Samples/DialogSample/DialogSample.csproj +++ b/New_Extensibility_Model/Samples/DialogSample/DialogSample.csproj @@ -7,8 +7,8 @@ - - + + diff --git a/New_Extensibility_Model/Samples/DocumentSelectorSample/DocumentSelectorSample.csproj b/New_Extensibility_Model/Samples/DocumentSelectorSample/DocumentSelectorSample.csproj index 65fa78ba..be068225 100644 --- a/New_Extensibility_Model/Samples/DocumentSelectorSample/DocumentSelectorSample.csproj +++ b/New_Extensibility_Model/Samples/DocumentSelectorSample/DocumentSelectorSample.csproj @@ -7,8 +7,8 @@ - - + + diff --git a/New_Extensibility_Model/Samples/EncodeDecodeBase64/EncodeDecodeBase64.csproj b/New_Extensibility_Model/Samples/EncodeDecodeBase64/EncodeDecodeBase64.csproj index c8e240df..f4534705 100644 --- a/New_Extensibility_Model/Samples/EncodeDecodeBase64/EncodeDecodeBase64.csproj +++ b/New_Extensibility_Model/Samples/EncodeDecodeBase64/EncodeDecodeBase64.csproj @@ -7,7 +7,7 @@ - - + + diff --git a/New_Extensibility_Model/Samples/ExtensionPublisher/ExtensionPublisher.csproj b/New_Extensibility_Model/Samples/ExtensionPublisher/ExtensionPublisher.csproj index 2a877a05..c3f5c507 100644 --- a/New_Extensibility_Model/Samples/ExtensionPublisher/ExtensionPublisher.csproj +++ b/New_Extensibility_Model/Samples/ExtensionPublisher/ExtensionPublisher.csproj @@ -6,8 +6,8 @@ - - + + diff --git a/New_Extensibility_Model/Samples/ExtensionWithTraditionalComponents/Container/AssemblyInfo.cs b/New_Extensibility_Model/Samples/ExtensionWithTraditionalComponents/Container/AssemblyInfo.cs new file mode 100644 index 00000000..173c3199 --- /dev/null +++ b/New_Extensibility_Model/Samples/ExtensionWithTraditionalComponents/Container/AssemblyInfo.cs @@ -0,0 +1,9 @@ +using System.Runtime.InteropServices; +using Microsoft.VisualStudio.Shell; + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] + +[assembly: ProvideCodeBase] diff --git a/New_Extensibility_Model/Samples/ExtensionWithTraditionalComponents/Container/Container.csproj b/New_Extensibility_Model/Samples/ExtensionWithTraditionalComponents/Container/Container.csproj new file mode 100644 index 00000000..004f7086 --- /dev/null +++ b/New_Extensibility_Model/Samples/ExtensionWithTraditionalComponents/Container/Container.csproj @@ -0,0 +1,37 @@ + + + net472 + enable + 12 + en-US + true + + true + true + true + True + + sgKey.snk + + + + + + + + + + + false + true + true + ExtensionFilesOutputGroup + + + + + + + diff --git a/New_Extensibility_Model/Samples/ExtensionWithTraditionalComponents/Container/MyUserControl.xaml b/New_Extensibility_Model/Samples/ExtensionWithTraditionalComponents/Container/MyUserControl.xaml new file mode 100644 index 00000000..37533b42 --- /dev/null +++ b/New_Extensibility_Model/Samples/ExtensionWithTraditionalComponents/Container/MyUserControl.xaml @@ -0,0 +1,34 @@ + + + + + + + + + + + + + + +