Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to write an add-in to change text color in Visual Studio editor?

After searching for a long time for a simple way of changing the text color of a #region directive in Visual Studio, I've concluded there is no easy way of doing so.

I know how to change the #region statement color, and how to change the collapsed region color, but I want to change the color of the text with the region description. So:

#region Some text   <--- all this text should be in a different color

public void Test()
{
}

#endregion          <--- this too

It seems a lot of people are looking for something like this - see How to change the color of expanded regions' titles in VS2008?.

So I've been looking at writing a simple Visual Studio add-in to change the color. However, it's more complicated than I thought it would be, with classes like Snapshot, Tagger, Classifier, WpfTextViewCreationListener, AdornmentLayer etc.

Simply put, I don't know where to start! I followed a couple of tutorials at the MSDN site, but they seem too complicated for what I'm trying to do.

Can someone point me to the most simple way of doing this? Ie. which classes/methods/events within the VS SDK I should use. I don't mind if the color is not customisable via the UI etc either. I'm using VS2010.

Edit: Just had the mztools website recommended to me; I'll take a look there too. Also noticed that StackOverflow's syntax highlighting of regions is pretty much exactly what I want!

like image 870
g t Avatar asked Mar 14 '12 07:03

g t


People also ask

How do I change the color of text in Visual Studio?

On the menu bar, choose Tools > Options. In the options list, choose Environment > Fonts and Colors. In the Show settings for list, choose Environment. If you want to change the font for tool windows only, in the Show settings for list, choose All Text Tool Windows.

Can you color code in Visual Studio?

Visual Studio provides an environment color service, also called the VSColor service or the shell color service. This service allows you to bind the color values of your UI elements to a name-value color set containing colors for each theme.

How do you change the font color in a script?

To change the font color of a HTML Element using JavaScript, get reference to this HTML element, and assign required color value to the element. style. color property. In the following example, we will change the font color of HTML Element with id "myElement" to the color "#FF0000" , in JavaScript, using element.

How do I change the color of my Visual Studio editor?

Set the color theme for the IDEOn the menu bar, which is the row of menus such as File and Edit, choose Tools > Options. On the Environment > General options page, change the Color theme selection to Dark, and then choose OK. The color theme for the entire Visual Studio development environment (IDE) changes to Dark.


1 Answers

I eventually came up with a solution, at least for VS2010. Whilst I have used this for coloring '#region' and '#endregion' tags, a similar solution ought to be applicable for any text content in a Visual Studio window.

It seems that this sort of problem can be resolved by creating a IViewTaggerProvider which will 'tag' parts of the source code with a 'classification'. Visual Studio will provide a style for text tagged with that classification which can then be changed by the user to the desired style via Tools > Options... > Environment > Fonts and Colors.


The Tagger provider looks like:

[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 a Visual Studio text view, will tag parts of the text with the given classification type. Note that this will work for all text views (i.e. source code editor, 'Find Results' windows etc.). It may be possible to change this by editing the ContentType attribute (to just C#?).


The classification type (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 may also apply to a span of text. The DisplayName will be used in the Tools > Options... dialog.


Once the classification is defined, an ITagger class can search a view's text and provide classifications for applicable sections of the text it finds.

Simply put, its job is to listen for the ViewLayoutChanged event of the text view, which is fired when the provided text view's content changes (e.g. because the the user has typed something).

It must then search the text for the area of text it is interested in (called a 'span'). Here, it returns spans of lines containing either #region or #endregion. I've kept this simple, but the TextSearchService used to find matches can also search using regular expressions.

Finally, a method is provided for Visual Studio to retrieve the tags of the text it has found, called GetTags(). For a given span collection, this will return text spans with classification tags, i.e. areas of those spans which should be classified in a certain way.

Its 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 have omitted namespaces and the using statements, which are typically of the form Microsoft.VisualStudio.Text.*. For these to be available, the Visual Studio 2010 SDK must first be downloaded.


I've been using this solution for the past few months without issue.

One limitation I noticed is colours are not 'blended', so a color with less than 100% opacity will not 'fade out' the existing colours in a span - which might be useful to preserve syntax highlighting.

I also have little idea of its efficiency, as it looks like it will repeatedly search a document on each keypress. I have not done the research to see if Visual Studio optimises this somehow. I do notice a slowdown of Visual Studio on large files (> ~1000 lines), but I also use Resharper, so I cannot attribute this to this plugin alone.

As this was coded mostly using guesswork, I welcome any comments or code changes which could clarify or simplify things or improve upon the code's performance.

like image 163
g t Avatar answered Sep 21 '22 19:09

g t