File: HACK_ThemeColorFixer.cs
Web Access
Project: src\src\VisualStudio\Core\Def\Microsoft.VisualStudio.LanguageServices_pxr0p0dn_wpftmp.csproj (Microsoft.VisualStudio.LanguageServices)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
 
#nullable disable
 
using System;
using System.Collections.ObjectModel;
using System.ComponentModel.Composition;
using Microsoft.CodeAnalysis.Classification;
using Microsoft.CodeAnalysis.Editor;
using Microsoft.CodeAnalysis.Host.Mef;
using Microsoft.VisualStudio.Shell;
using Microsoft.VisualStudio.Text;
using Microsoft.VisualStudio.Text.Classification;
using Microsoft.VisualStudio.Text.Editor;
using Microsoft.VisualStudio.Utilities;
 
namespace Microsoft.VisualStudio.LanguageServices;
 
/// <summary>
/// This class works around the fact that shell theme changes are not fully propagated into an
/// editor classification format map unless a classification type is registered as a font and
/// color item in that format map's font and color category. So, for example, the "Keyword"
/// classification type in the "tooltip" classification format map is never is never updated
/// from its default blue. As a work around, we listen to <see cref="IClassificationFormatMap.ClassificationFormatMappingChanged"/>
/// and update the classification format maps that we care about.
/// </summary>
[Export(typeof(IWpfTextViewConnectionListener))]
[ContentType(ContentTypeNames.RoslynContentType)]
[TextViewRole(PredefinedTextViewRoles.Analyzable)]
internal sealed class HACK_ThemeColorFixer : IWpfTextViewConnectionListener
{
    private readonly IClassificationTypeRegistryService _classificationTypeRegistryService;
    private readonly IClassificationFormatMapService _classificationFormatMapService;
    private readonly IClassificationFormatMap _textFormatMap;
 
    private bool _done;
 
    [ImportingConstructor]
    [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)]
    public HACK_ThemeColorFixer(
        IClassificationTypeRegistryService classificationTypeRegistryService,
        IClassificationFormatMapService classificationFormatMapService)
    {
        _classificationTypeRegistryService = classificationTypeRegistryService;
        _classificationFormatMapService = classificationFormatMapService;
 
        // Note: We never unsubscribe from this event. This service lives for the lifetime of VS.
        _textFormatMap = _classificationFormatMapService.GetClassificationFormatMap("text");
        _textFormatMap.ClassificationFormatMappingChanged += TextFormatMap_ClassificationFormatMappingChanged;
    }
 
    private void TextFormatMap_ClassificationFormatMappingChanged(object sender, EventArgs e)
        => VsTaskLibraryHelper.CreateAndStartTask(VsTaskLibraryHelper.ServiceInstance, VsTaskRunContext.UIThreadIdlePriority, RefreshThemeColors);
 
    public void RefreshThemeColors()
    {
        // Unsubscribe while we're making any actual changes, to avoid reentrancy.
        _textFormatMap.ClassificationFormatMappingChanged -= TextFormatMap_ClassificationFormatMappingChanged;
        try
        {
            var textFormatMap = _classificationFormatMapService.GetClassificationFormatMap("text");
            var tooltipFormatMap = _classificationFormatMapService.GetClassificationFormatMap("tooltip");
 
            // We have features that would like to classify the contents of strings (for example, as regex/json, or even
            // as C# code itself).  To ensure that the classifications provided for the string show up over the string
            // literal, we reprioritize the 'string literal' classification to have the lowest priority of all
            // classifications.
            DeprioritizeStringClassification(textFormatMap, ClassificationTypeNames.StringLiteral);
            DeprioritizeStringClassification(tooltipFormatMap, ClassificationTypeNames.StringLiteral);
            DeprioritizeStringClassification(textFormatMap, ClassificationTypeNames.VerbatimStringLiteral);
            DeprioritizeStringClassification(tooltipFormatMap, ClassificationTypeNames.VerbatimStringLiteral);
 
            UpdateForegroundColors(textFormatMap, tooltipFormatMap);
        }
        finally
        {
            // resubscribe once done.
            _textFormatMap.ClassificationFormatMappingChanged += TextFormatMap_ClassificationFormatMappingChanged;
        }
    }
 
    private void DeprioritizeStringClassification(IClassificationFormatMap formatMap, string typeName)
    {
        // No better option (According to DPugh) than bubble sorting this classification backwards to the start of
        // the list.  Use a batch-update though to make this only do updates once.
        var classificationType = _classificationTypeRegistryService.GetClassificationType(typeName);
        if (classificationType is null)
            return;
 
        // We're only changing StringLiteral and VerbatimStringLiteral.  Once those are both at the start of the
        // list, we don't have to do anything else with them.
        var index = formatMap.CurrentPriorityOrder.IndexOf(classificationType);
        if (index <= 1)
            return;
 
        formatMap.BeginBatchUpdate();
        try
        {
            while (index - 1 >= 0)
            {
                index--;
                formatMap.SwapPriorities(classificationType, formatMap.CurrentPriorityOrder[index]);
            }
        }
        finally
        {
            formatMap.EndBatchUpdate();
        }
    }
 
    private void UpdateForegroundColors(
        IClassificationFormatMap sourceFormatMap,
        IClassificationFormatMap targetFormatMap)
    {
        UpdateForegroundColor(ClassificationTypeNames.Comment, sourceFormatMap, targetFormatMap);
        UpdateForegroundColor(ClassificationTypeNames.ExcludedCode, sourceFormatMap, targetFormatMap);
        UpdateForegroundColor(ClassificationTypeNames.Identifier, sourceFormatMap, targetFormatMap);
        UpdateForegroundColor(ClassificationTypeNames.Keyword, sourceFormatMap, targetFormatMap);
        UpdateForegroundColor(ClassificationTypeNames.ControlKeyword, sourceFormatMap, targetFormatMap);
        UpdateForegroundColor(ClassificationTypeNames.NumericLiteral, sourceFormatMap, targetFormatMap);
        UpdateForegroundColor(ClassificationTypeNames.StringLiteral, sourceFormatMap, targetFormatMap);
 
        UpdateForegroundColor(ClassificationTypeNames.VerbatimStringLiteral, sourceFormatMap, targetFormatMap);
        UpdateForegroundColor(ClassificationTypeNames.StringEscapeCharacter, sourceFormatMap, targetFormatMap);
 
        UpdateForegroundColor(ClassificationTypeNames.XmlDocCommentAttributeName, sourceFormatMap, targetFormatMap);
        UpdateForegroundColor(ClassificationTypeNames.XmlDocCommentAttributeQuotes, sourceFormatMap, targetFormatMap);
        UpdateForegroundColor(ClassificationTypeNames.XmlDocCommentAttributeValue, sourceFormatMap, targetFormatMap);
        UpdateForegroundColor(ClassificationTypeNames.XmlDocCommentText, sourceFormatMap, targetFormatMap);
        UpdateForegroundColor(ClassificationTypeNames.XmlDocCommentDelimiter, sourceFormatMap, targetFormatMap);
        UpdateForegroundColor(ClassificationTypeNames.XmlDocCommentComment, sourceFormatMap, targetFormatMap);
        UpdateForegroundColor(ClassificationTypeNames.XmlDocCommentCDataSection, sourceFormatMap, targetFormatMap);
 
        UpdateForegroundColor(ClassificationTypeNames.RegexComment, sourceFormatMap, targetFormatMap);
        UpdateForegroundColor(ClassificationTypeNames.RegexText, sourceFormatMap, targetFormatMap);
        UpdateForegroundColor(ClassificationTypeNames.RegexCharacterClass, sourceFormatMap, targetFormatMap);
        UpdateForegroundColor(ClassificationTypeNames.RegexQuantifier, sourceFormatMap, targetFormatMap);
        UpdateForegroundColor(ClassificationTypeNames.RegexAnchor, sourceFormatMap, targetFormatMap);
        UpdateForegroundColor(ClassificationTypeNames.RegexAlternation, sourceFormatMap, targetFormatMap);
        UpdateForegroundColor(ClassificationTypeNames.RegexGrouping, sourceFormatMap, targetFormatMap);
        UpdateForegroundColor(ClassificationTypeNames.RegexOtherEscape, sourceFormatMap, targetFormatMap);
        UpdateForegroundColor(ClassificationTypeNames.RegexSelfEscapedCharacter, sourceFormatMap, targetFormatMap);
 
        UpdateForegroundColor(ClassificationTypeNames.JsonComment, sourceFormatMap, targetFormatMap);
        UpdateForegroundColor(ClassificationTypeNames.JsonNumber, sourceFormatMap, targetFormatMap);
        UpdateForegroundColor(ClassificationTypeNames.JsonString, sourceFormatMap, targetFormatMap);
        UpdateForegroundColor(ClassificationTypeNames.JsonKeyword, sourceFormatMap, targetFormatMap);
        UpdateForegroundColor(ClassificationTypeNames.JsonText, sourceFormatMap, targetFormatMap);
        UpdateForegroundColor(ClassificationTypeNames.JsonOperator, sourceFormatMap, targetFormatMap);
        UpdateForegroundColor(ClassificationTypeNames.JsonArray, sourceFormatMap, targetFormatMap);
        UpdateForegroundColor(ClassificationTypeNames.JsonObject, sourceFormatMap, targetFormatMap);
        UpdateForegroundColor(ClassificationTypeNames.JsonPropertyName, sourceFormatMap, targetFormatMap);
        UpdateForegroundColor(ClassificationTypeNames.JsonConstructorName, sourceFormatMap, targetFormatMap);
 
        UpdateForegroundColor(ClassificationTypeNames.PreprocessorKeyword, sourceFormatMap, targetFormatMap);
        UpdateForegroundColor(ClassificationTypeNames.PreprocessorText, sourceFormatMap, targetFormatMap);
 
        UpdateForegroundColor(ClassificationTypeNames.Operator, sourceFormatMap, targetFormatMap);
        UpdateForegroundColor(ClassificationTypeNames.OperatorOverloaded, sourceFormatMap, targetFormatMap);
        UpdateForegroundColor(ClassificationTypeNames.Punctuation, sourceFormatMap, targetFormatMap);
 
        UpdateForegroundColor(ClassificationTypeNames.ClassName, sourceFormatMap, targetFormatMap);
        UpdateForegroundColor(ClassificationTypeNames.RecordClassName, sourceFormatMap, targetFormatMap);
        UpdateForegroundColor(ClassificationTypeNames.StructName, sourceFormatMap, targetFormatMap);
        UpdateForegroundColor(ClassificationTypeNames.InterfaceName, sourceFormatMap, targetFormatMap);
        UpdateForegroundColor(ClassificationTypeNames.DelegateName, sourceFormatMap, targetFormatMap);
        UpdateForegroundColor(ClassificationTypeNames.EnumName, sourceFormatMap, targetFormatMap);
        UpdateForegroundColor(ClassificationTypeNames.TypeParameterName, sourceFormatMap, targetFormatMap);
        UpdateForegroundColor(ClassificationTypeNames.ModuleName, sourceFormatMap, targetFormatMap);
 
        UpdateForegroundColor(ClassificationTypeNames.FieldName, sourceFormatMap, targetFormatMap);
        UpdateForegroundColor(ClassificationTypeNames.EnumMemberName, sourceFormatMap, targetFormatMap);
        UpdateForegroundColor(ClassificationTypeNames.ConstantName, sourceFormatMap, targetFormatMap);
        UpdateForegroundColor(ClassificationTypeNames.LocalName, sourceFormatMap, targetFormatMap);
        UpdateForegroundColor(ClassificationTypeNames.ParameterName, sourceFormatMap, targetFormatMap);
        UpdateForegroundColor(ClassificationTypeNames.MethodName, sourceFormatMap, targetFormatMap);
        UpdateForegroundColor(ClassificationTypeNames.ExtensionMethodName, sourceFormatMap, targetFormatMap);
        UpdateForegroundColor(ClassificationTypeNames.PropertyName, sourceFormatMap, targetFormatMap);
        UpdateForegroundColor(ClassificationTypeNames.EventName, sourceFormatMap, targetFormatMap);
        UpdateForegroundColor(ClassificationTypeNames.NamespaceName, sourceFormatMap, targetFormatMap);
        UpdateForegroundColor(ClassificationTypeNames.LabelName, sourceFormatMap, targetFormatMap);
 
        UpdateForegroundColor(ClassificationTypeNames.XmlLiteralText, sourceFormatMap, targetFormatMap);
        UpdateForegroundColor(ClassificationTypeNames.XmlLiteralProcessingInstruction, sourceFormatMap, targetFormatMap);
        UpdateForegroundColor(ClassificationTypeNames.XmlLiteralName, sourceFormatMap, targetFormatMap);
        UpdateForegroundColor(ClassificationTypeNames.XmlLiteralEmbeddedExpression, sourceFormatMap, targetFormatMap);
        UpdateForegroundColor(ClassificationTypeNames.XmlLiteralDelimiter, sourceFormatMap, targetFormatMap);
        UpdateForegroundColor(ClassificationTypeNames.XmlLiteralComment, sourceFormatMap, targetFormatMap);
        UpdateForegroundColor(ClassificationTypeNames.XmlLiteralCDataSection, sourceFormatMap, targetFormatMap);
        UpdateForegroundColor(ClassificationTypeNames.XmlLiteralAttributeValue, sourceFormatMap, targetFormatMap);
        UpdateForegroundColor(ClassificationTypeNames.XmlLiteralAttributeQuotes, sourceFormatMap, targetFormatMap);
        UpdateForegroundColor(ClassificationTypeNames.XmlLiteralAttributeName, sourceFormatMap, targetFormatMap);
        UpdateForegroundColor(ClassificationTypeNames.XmlLiteralEntityReference, sourceFormatMap, targetFormatMap);
 
        UpdateForegroundColor(ClassificationTypeNames.TestCode, sourceFormatMap, targetFormatMap);
        UpdateForegroundColor(ClassificationTypeNames.TestCodeMarkdown, sourceFormatMap, targetFormatMap);
    }
 
    private void UpdateForegroundColor(
        string classificationTypeName,
        IClassificationFormatMap sourceFormatMap,
        IClassificationFormatMap targetFormatMap)
    {
        var classificationType = _classificationTypeRegistryService.GetClassificationType(classificationTypeName);
        if (classificationType == null)
        {
            return;
        }
 
        var sourceProps = sourceFormatMap.GetTextProperties(classificationType);
        var targetProps = targetFormatMap.GetTextProperties(classificationType);
 
        targetProps = targetProps.SetForegroundBrush(sourceProps.ForegroundBrush);
 
        targetFormatMap.SetTextProperties(classificationType, targetProps);
    }
 
    public void SubjectBuffersConnected(IWpfTextView textView, ConnectionReason reason, Collection<ITextBuffer> subjectBuffers)
    {
        // DevDiv https://devdiv.visualstudio.com/DevDiv/_workitems/edit/130129:
        //
        // This needs to be scheduled after editor has been composed. Otherwise
        // it may cause UI delays by composing the editor before it is needed
        // by the rest of VS.
        if (!_done)
        {
            _done = true;
            VsTaskLibraryHelper.CreateAndStartTask(VsTaskLibraryHelper.ServiceInstance, VsTaskRunContext.UIThreadIdlePriority, RefreshThemeColors);
        }
    }
 
    public void SubjectBuffersDisconnected(IWpfTextView textView, ConnectionReason reason, Collection<ITextBuffer> subjectBuffers)
    {
    }
}