In the end, I came up with a solution, at least for VS2010. Although I used this to color the #region
and #endregion
'tags, a similar solution should be applicable to any textual content in a Visual Studio window.
It seems that this problem can be solved by creating IViewTaggerProvider
, which will "tag" parts of the source code with "classification". Visual Studio will provide a style for the text marked with this classification, which can then be changed by the user in the desired style using Tools> Options ...> Environment> Fonts and Colors.
The Tagger provider looks like this:
[Export(typeof(IViewTaggerProvider))] [ContentType("any")] [TagType(typeof(ClassificationTag))] public sealed class RegionTaggerProvider : IViewTaggerProvider { [Import] public IClassificationTypeRegistryService Registry; [Import] internal ITextSearchService TextSearchService { get; set; } public ITagger<T> CreateTagger<T>(ITextView textView, ITextBuffer buffer) where T : ITag { if (buffer != textView.TextBuffer) return null; var classType = Registry.GetClassificationType("region-foreground"); return new RegionTagger(textView, TextSearchService, classType) as ITagger<T>; } }
This creates an ITagger
object, which, given the textual presentation of Visual Studio, will tag parts of the text with the specified classification type. Note that this will work for all text views (i.e. Source Code Editor, Find Results window, etc.). Perhaps this can be changed by editing the ContentType
attribute (only C#
?).
The type of classification (in this case, "region-foreground") is defined as:
public static class TypeExports { [Export(typeof(ClassificationTypeDefinition))] [Name("region-foreground")] public static ClassificationTypeDefinition OrdinaryClassificationType; } [Export(typeof(EditorFormatDefinition))] [ClassificationType(ClassificationTypeNames = "region-foreground")] [Name("region-foreground")] [UserVisible(true)] [Order(After = Priority.High)] public sealed class RegionForeground : ClassificationFormatDefinition { public RegionForeground() { DisplayName = "Region Foreground"; ForegroundColor = Colors.Gray; } }
The Order
attribute determines when the classification will be applied compared to other classifications, which can also apply to a range of text. DisplayName
will be used in the Tools> Options ... dialog box.
After determining the classification, the ITagger
class can search for text in the form and provide classifications for the relevant sections of the found text.
Simply put, its task is to listen to the ViewLayoutChanged
event of the text view, which is fired when the content of the provided text view changes (for example, because the user typed something).
Then he should look for the text for the area of ββinterest of the text (called "span"). Here it returns line spacing containing either #region
or #endregion
. I kept it simple, but the TextSearchService
used to search for matches can also do searches using regular expressions.
Finally, a visual method is provided for Visual Studio to retrieve the tags for the found text, called GetTags()
. For this collection of ranges, this will lead to the return of text spaces using classification tags, that is, the areas of those spaces that need to be classified in a certain way.
His code is:
public sealed class RegionTagger : ITagger<ClassificationTag> { private readonly ITextView m_View; private readonly ITextSearchService m_SearchService; private readonly IClassificationType m_Type; private NormalizedSnapshotSpanCollection m_CurrentSpans; public event EventHandler<SnapshotSpanEventArgs> TagsChanged = delegate { }; public RegionTagger(ITextView view, ITextSearchService searchService, IClassificationType type) { m_View = view; m_SearchService = searchService; m_Type = type; m_CurrentSpans = GetWordSpans(m_View.TextSnapshot); m_View.GotAggregateFocus += SetupSelectionChangedListener; } private void SetupSelectionChangedListener(object sender, EventArgs e) { if (m_View != null) { m_View.LayoutChanged += ViewLayoutChanged; m_View.GotAggregateFocus -= SetupSelectionChangedListener; } } private void ViewLayoutChanged(object sender, TextViewLayoutChangedEventArgs e) { if (e.OldSnapshot != e.NewSnapshot) { m_CurrentSpans = GetWordSpans(e.NewSnapshot); TagsChanged(this, new SnapshotSpanEventArgs(new SnapshotSpan(e.NewSnapshot, 0, e.NewSnapshot.Length))); } } private NormalizedSnapshotSpanCollection GetWordSpans(ITextSnapshot snapshot) { var wordSpans = new List<SnapshotSpan>(); wordSpans.AddRange(FindAll(@"#region", snapshot).Select(regionLine => regionLine.Start.GetContainingLine().Extent)); wordSpans.AddRange(FindAll(@"#endregion", snapshot).Select(regionLine => regionLine.Start.GetContainingLine().Extent)); return new NormalizedSnapshotSpanCollection(wordSpans); } private IEnumerable<SnapshotSpan> FindAll(String searchPattern, ITextSnapshot textSnapshot) { if (textSnapshot == null) return null; return m_SearchService.FindAll( new FindData(searchPattern, textSnapshot) { FindOptions = FindOptions.WholeWord | FindOptions.MatchCase }); } public IEnumerable<ITagSpan<ClassificationTag>> GetTags(NormalizedSnapshotSpanCollection spans) { if (spans == null || spans.Count == 0 || m_CurrentSpans.Count == 0) yield break; ITextSnapshot snapshot = m_CurrentSpans[0].Snapshot; spans = new NormalizedSnapshotSpanCollection(spans.Select(s => s.TranslateTo(snapshot, SpanTrackingMode.EdgeExclusive))); foreach (var span in NormalizedSnapshotSpanCollection.Intersection(m_CurrentSpans, spans)) { yield return new TagSpan<ClassificationTag>(span, new ClassificationTag(m_Type)); } } }
For brevity, I skipped namespaces and using statements, which are usually in the form Microsoft.VisualStudio.Text.*
. To make them available, you must first download the Visual Studio 2010 SDK .
I have been using this solution for the past few months without any problems.
The only restriction I noticed is colors that do not mix, so a color with an opacity of less than 100% will not "disappear" from existing colors in the range - which can be useful for maintaining syntax highlighting.
I also have little information about its effectiveness, as it looks like it will repeatedly search for a document at each key press. I have not done research to see how Visual Studio somehow optimizes this. I notice a slowdown in Visual Studio on large files (> ~ 1000 lines), but I also use Resharper, so I cannot attribute this to this plugin only.
Since this was mainly encoded using guesswork, I welcome any comments or code changes that may clarify or simplify things or improve code performance.