File: ColorSchemes\ColorSchemeApplier.ClassificationVerifier.cs
Web Access
Project: src\src\VisualStudio\Core\Def\Microsoft.VisualStudio.LanguageServices_rmjjt0xj_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.
 
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using System.Windows;
using Microsoft.CodeAnalysis.Classification;
using Microsoft.CodeAnalysis.Editor.Shared.Extensions;
using Microsoft.CodeAnalysis.Editor.Shared.Utilities;
using Microsoft.VisualStudio;
using Microsoft.VisualStudio.Shell;
using Microsoft.VisualStudio.Shell.Interop;
using Microsoft.VisualStudio.Threading;
using Roslyn.Utilities;
 
namespace Microsoft.CodeAnalysis.ColorSchemes;
 
internal partial class ColorSchemeApplier
{
    private sealed class ClassificationVerifier
    {
        private static readonly Guid TextEditorMEFItemsColorCategory = new("75a05685-00a8-4ded-bae5-e7a50bfa929a");
 
        // These classification colors (0x00BBGGRR) should match the VS\EditorColors.xml file.
        // They are not in the scheme files because they are core classifications.
        private const uint DarkThemePlainText = 0x00DCDCDCu;
        private const uint DarkThemeIdentifier = DarkThemePlainText;
        private const uint DarkThemeOperator = 0x00B4B4B4u;
        private const uint DarkThemeKeyword = 0x00D69C56u;
 
        private const uint LightThemePlainText = 0x00000000u;
        private const uint LightThemeIdentifier = LightThemePlainText;
        private const uint LightThemeOperator = LightThemePlainText;
        private const uint LightThemeKeyword = 0x00FF0000u;
 
        private const string PlainTextClassificationTypeName = "plain text";
 
        // Dark Theme Core Classifications
        private static ImmutableDictionary<string, uint> DarkThemeForeground
            => new Dictionary<string, uint>()
            {
                [PlainTextClassificationTypeName] = DarkThemePlainText,
                [ClassificationTypeNames.Identifier] = DarkThemeIdentifier,
                [ClassificationTypeNames.Keyword] = DarkThemeKeyword,
                [ClassificationTypeNames.Operator] = DarkThemeOperator,
            }.ToImmutableDictionary();
 
        // Light, Blue, or AdditionalContrast Theme Core Classifications
        private static ImmutableDictionary<string, uint> BlueLightThemeForeground
            => new Dictionary<string, uint>()
            {
                [PlainTextClassificationTypeName] = LightThemePlainText,
                [ClassificationTypeNames.Identifier] = LightThemeIdentifier,
                [ClassificationTypeNames.Keyword] = LightThemeKeyword,
                [ClassificationTypeNames.Operator] = LightThemeOperator,
            }.ToImmutableDictionary();
 
        private readonly IThreadingContext _threadingContext;
        private readonly IVsService<IVsFontAndColorStorage> _fontAndColorStorage;
        private readonly ImmutableArray<string> _classifications;
        private readonly ImmutableDictionary<ColorSchemeName, ImmutableDictionary<Guid, ImmutableDictionary<string, uint>>> _colorSchemes;
 
        // The High Contrast theme is not included because we do not want to make changes when the user is in High Contrast mode.
 
        public ClassificationVerifier(
            IThreadingContext threadingContext,
            IVsService<IVsFontAndColorStorage> fontAndColorStorage,
            ImmutableDictionary<ColorSchemeName, ColorScheme> colorSchemes)
        {
            _threadingContext = threadingContext;
            _fontAndColorStorage = fontAndColorStorage;
            _colorSchemes = colorSchemes.ToImmutableDictionary(
                nameAndScheme => nameAndScheme.Key,
                nameAndScheme => nameAndScheme.Value.Themes.ToImmutableDictionary(
                    theme => theme.Guid,
                    theme => theme.Category.Colors
                        .Where(color => color.Foreground.HasValue)
                        .ToImmutableDictionary(color => color.Name, color => color.Foreground!.Value)));
 
            // Gather all the classifications from the core and scheme dictionaries.
            var coreClassifications = DarkThemeForeground.Keys.Concat(BlueLightThemeForeground.Keys).Distinct();
            var colorSchemeClassifications = _colorSchemes.Values.SelectMany(scheme => scheme.Values.SelectMany(theme => theme.Keys)).Distinct();
            _classifications = coreClassifications.Concat(colorSchemeClassifications).ToImmutableArray();
        }
 
        /// <summary>
        /// Determines if any Classification foreground colors have been customized in Fonts and Colors.
        /// </summary>
        public async Task<bool> AreForegroundColorsCustomizedAsync(
            ColorSchemeName schemeName, Guid themeId, CancellationToken cancellationToken)
        {
            // Make no changes when in high contast mode or in unknown theme.
            if (SystemParameters.HighContrast ||
                !_colorSchemes.TryGetValue(schemeName, out var colorScheme) ||
                !colorScheme.TryGetValue(themeId, out var colorSchemeTheme))
            {
                return false;
            }
 
            // Ensure we are initialized
            var fontAndColorStorage = await _fontAndColorStorage.GetValueAsync(cancellationToken).ConfigureAwait(true);
 
            await _threadingContext.JoinableTaskFactory.SwitchToMainThreadAsync(cancellationToken);
            var fontAndColorUtilities = (IVsFontAndColorUtilities)fontAndColorStorage;
 
            var coreThemeColors = themeId == KnownColorThemes.Dark
                ? DarkThemeForeground
                : BlueLightThemeForeground;
 
            // Open Text Editor category for readonly access and do not load items if they are defaulted.
            if (fontAndColorStorage.OpenCategory(TextEditorMEFItemsColorCategory, (uint)__FCSTORAGEFLAGS.FCSF_READONLY) == VSConstants.S_OK)
            {
                try
                {
                    foreach (var classification in _classifications)
                    {
                        var colorItems = new ColorableItemInfo[1];
                        if (fontAndColorStorage.GetItem(classification, colorItems) != VSConstants.S_OK)
                        {
                            // Classifications that are still defaulted will not have entries.
                            continue;
                        }
 
                        var colorItem = colorItems[0];
 
                        if (IsClassificationCustomized(coreThemeColors, colorSchemeTheme, fontAndColorUtilities, colorItem, classification))
                        {
                            return true;
                        }
                    }
                }
                finally
                {
                    fontAndColorStorage.CloseCategory();
                }
            }
 
            return false;
        }
 
        /// <summary>
        /// Determines if the ColorableItemInfo's Foreground has been customized to a color that doesn't match the
        /// selected scheme.
        /// </summary>
        private bool IsClassificationCustomized(
            ImmutableDictionary<string, uint> coreThemeColors,
            ImmutableDictionary<string, uint> schemeThemeColors,
            IVsFontAndColorUtilities fontAndColorUtilities,
            ColorableItemInfo colorItem,
            string classification)
        {
            _threadingContext.ThrowIfNotOnUIThread();
 
            var foregroundColorRef = colorItem.crForeground;
 
            if (fontAndColorUtilities.GetColorType(foregroundColorRef, out var foregroundColorType) != VSConstants.S_OK)
            {
                // Without being able to check color type, we cannot make a determination.
                return false;
            }
 
            // If the color is defaulted then it isn't customized.
            if (foregroundColorType == (int)__VSCOLORTYPE.CT_AUTOMATIC)
            {
                return false;
            }
 
            // Since the color type isn't default then it has been customized, we will
            // perform an additional check for RGB colors to see if the customized color
            // matches the color scheme color.
            if (foregroundColorType != (int)__VSCOLORTYPE.CT_RAW)
            {
                return true;
            }
 
            if (coreThemeColors.TryGetValue(classification, out var coreColor))
            {
                return foregroundColorRef != coreColor;
            }
 
            if (schemeThemeColors.TryGetValue(classification, out var schemeColor))
            {
                return foregroundColorRef != schemeColor;
            }
 
            // Since Classification inheritance isn't represented in the scheme files,
            // this switch case will handle the 3 cases we expect.
            var fallbackColor = classification switch
            {
                ClassificationTypeNames.OperatorOverloaded => coreThemeColors[ClassificationTypeNames.Operator],
                ClassificationTypeNames.ControlKeyword => coreThemeColors[ClassificationTypeNames.Keyword],
                _ => coreThemeColors[ClassificationTypeNames.Identifier]
            };
 
            return foregroundColorRef != fallbackColor;
        }
    }
}