File: ColorSchemes\ColorSchemeApplier.cs
Web Access
Project: src\src\VisualStudio\Core\Def\Microsoft.VisualStudio.LanguageServices_ozsccwvc_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.Immutable;
using System.ComponentModel;
using System.ComponentModel.Composition;
using System.Linq;
using System.Runtime.InteropServices;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.Editor.Shared.Utilities;
using Microsoft.CodeAnalysis.Host.Mef;
using Microsoft.CodeAnalysis.Options;
using Microsoft.CodeAnalysis.Shared.TestHooks;
using Microsoft.VisualStudio;
using Microsoft.VisualStudio.PlatformUI;
using Microsoft.VisualStudio.Settings;
using Microsoft.VisualStudio.Shell;
using Microsoft.VisualStudio.Shell.Interop;
using Microsoft.VisualStudio.Threading;
using Microsoft.Win32;
using Roslyn.Utilities;
using Task = System.Threading.Tasks.Task;
 
namespace Microsoft.CodeAnalysis.ColorSchemes;
 
[Export(typeof(ColorSchemeApplier))]
internal sealed partial class ColorSchemeApplier
{
    private const string ColorThemeValueName = "Microsoft.VisualStudio.ColorTheme";
    private const string ColorThemeNewValueName = "Microsoft.VisualStudio.ColorThemeNew";
 
    private readonly IThreadingContext _threadingContext;
    private readonly IServiceProvider _serviceProvider;
    private readonly IAsyncServiceProvider _asyncServiceProvider;
    private readonly ColorSchemeSettings _settings;
    private readonly ClassificationVerifier _classificationVerifier;
    private readonly ImmutableDictionary<ColorSchemeName, ColorScheme> _colorSchemes;
    private readonly AsyncBatchingWorkQueue _workQueue;
 
    private readonly object _gate = new();
 
    private ImmutableDictionary<ColorSchemeName, ImmutableArray<RegistryItem>>? _colorSchemeRegistryItems;
    private bool _isInitialized = false;
 
    [ImportingConstructor]
    [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)]
    public ColorSchemeApplier(
        IThreadingContext threadingContext,
        IVsService<SVsFontAndColorStorage, IVsFontAndColorStorage> fontAndColorStorage,
        IGlobalOptionService globalOptions,
        SVsServiceProvider serviceProvider,
        IAsynchronousOperationListenerProvider listenerProvider)
    {
        _threadingContext = threadingContext;
        _serviceProvider = serviceProvider;
        _asyncServiceProvider = (IAsyncServiceProvider)serviceProvider;
 
        _settings = new ColorSchemeSettings(threadingContext, _serviceProvider, globalOptions);
        _colorSchemes = ColorSchemeSettings.GetColorSchemes();
        _classificationVerifier = new ClassificationVerifier(threadingContext, fontAndColorStorage, _colorSchemes);
        _workQueue = new(
            DelayTimeSpan.Idle,
            QueueColorSchemeUpdateAsync,
            listenerProvider.GetListener(FeatureAttribute.ColorScheme),
            threadingContext.DisposalToken);
    }
 
    public async Task InitializeAsync(CancellationToken cancellationToken)
    {
        lock (_gate)
        {
            if (_isInitialized)
                return;
 
            _isInitialized = true;
        }
 
        // We need to update the theme whenever the Editor Color Scheme setting changes or the VS Theme changes.
        await TaskScheduler.Default;
        var settingsManager = await _asyncServiceProvider.GetServiceAsync<SVsSettingsPersistenceManager, ISettingsManager>(_threadingContext.JoinableTaskFactory).ConfigureAwait(false);
 
        await _threadingContext.JoinableTaskFactory.SwitchToMainThreadAsync(cancellationToken);
        settingsManager.GetSubset(ColorSchemeOptionsStorage.ColorSchemeSettingKey).SettingChangedAsync += ColorSchemeChangedAsync;
        VSColorTheme.ThemeChanged += VSColorTheme_ThemeChanged;
 
        await TaskScheduler.Default;
 
        // Try to migrate the `useEnhancedColorsSetting` to the new `ColorSchemeName` setting.
        _settings.MigrateToColorSchemeSetting();
 
        // Since the Roslyn colors are now defined in the Roslyn repo and no longer applied by the VS pkgdef built from EditorColors.xml,
        // We attempt to apply a color scheme when the Roslyn package is loaded. This is our chance to update the configuration registry
        // with the Roslyn colors before they are seen by the user. This is important because the MEF exported Roslyn classification
        // colors are only applicable to the Blue and Light VS themes.
 
        // If the color scheme has updated, apply the scheme.
        await UpdateColorSchemeAsync(cancellationToken).ConfigureAwait(false);
    }
 
    private async Task UpdateColorSchemeAsync(CancellationToken cancellationToken)
    {
        cancellationToken.ThrowIfCancellationRequested();
 
        var colorScheme = await TryGetUpdatedColorSchemeAsync(cancellationToken).ConfigureAwait(false);
        if (colorScheme == null)
            return;
 
        _colorSchemeRegistryItems ??= _colorSchemes.ToImmutableDictionary(
            kvp => kvp.Key, kvp => RegistryItemConverter.Convert(kvp.Value));
 
        await _settings.ApplyColorSchemeAsync(
            colorScheme.Value, _colorSchemeRegistryItems[colorScheme.Value], cancellationToken).ConfigureAwait(false);
    }
 
    private void VSColorTheme_ThemeChanged(ThemeChangedEventArgs e)
        => _workQueue.AddWork();
 
    private Task ColorSchemeChangedAsync(object sender, PropertyChangedEventArgs args)
    {
        _workQueue.AddWork();
        return Task.CompletedTask;
    }
 
    private async ValueTask QueueColorSchemeUpdateAsync(CancellationToken cancellationToken)
    {
        // Wait until things have settled down from the theme change, since we will potentially be changing theme colors.
        await VsTaskLibraryHelper.StartOnIdle(_threadingContext.JoinableTaskFactory, () => UpdateColorSchemeAsync(cancellationToken));
    }
 
    /// <summary>
    /// Returns true if the color scheme needs updating.
    /// </summary>
    private async Task<ColorSchemeName?> TryGetUpdatedColorSchemeAsync(CancellationToken cancellationToken)
    {
        // The color scheme that is currently applied to the registry
        var appliedColorScheme = await _settings.GetAppliedColorSchemeAsync(cancellationToken).ConfigureAwait(false);
 
        // If this is a supported theme then, use the users configured scheme, otherwise fallback to the VS 2017.
        // Custom themes would be based on the MEF exported color information for classifications which matches the VS 2017 theme.
        var configuredColorScheme = await IsSupportedThemeAsync(cancellationToken).ConfigureAwait(false)
            ? _settings.GetConfiguredColorScheme()
            : ColorSchemeName.VisualStudio2017;
 
        if (appliedColorScheme == configuredColorScheme)
            return null;
 
        return configuredColorScheme;
    }
 
    public async Task<bool> IsSupportedThemeAsync(CancellationToken cancellationToken)
        => IsSupportedTheme(await GetThemeIdAsync(cancellationToken).ConfigureAwait(false));
 
    private bool IsSupportedTheme(Guid themeId)
    {
        return _colorSchemes.Values.Any(
            scheme => scheme.Themes.Any(
                static (theme, themeId) => theme.Guid == themeId, themeId));
    }
 
    public async Task<bool> IsThemeCustomizedAsync(CancellationToken cancellationToken)
        => await _classificationVerifier.AreForegroundColorsCustomizedAsync(
            _settings.GetConfiguredColorScheme(),
            await GetThemeIdAsync(cancellationToken).ConfigureAwait(false),
            cancellationToken).ConfigureAwait(false);
 
    private async Task<Guid> GetThemeIdAsync(CancellationToken cancellationToken)
    {
        await TaskScheduler.Default;
        var settingsManager = await _asyncServiceProvider.GetServiceAsync<SVsSettingsPersistenceManager, ISettingsManager>(_threadingContext.JoinableTaskFactory).ConfigureAwait(false);
        await _threadingContext.JoinableTaskFactory.SwitchToMainThreadAsync(cancellationToken);
 
        //  Look up the value from the new roamed theme property first
        //  Fallback to the original roamed theme property if that fails
        var currentThemeString = settingsManager.GetValueOrDefault<string?>(ColorThemeNewValueName, defaultValue: null) ??
            settingsManager.GetValueOrDefault<string?>(ColorThemeValueName, defaultValue: null);
 
        if (currentThemeString is null)
        {
            // The ColorTheme setting is unpopulated when it has never been changed from its default.
            // The default VS ColorTheme is Blue
            return KnownColorThemes.Blue;
        }
 
        var themeId = Guid.Parse(currentThemeString);
 
        if (themeId == KnownColorThemes.System)
        {
            themeId = ShouldAppsUseLightTheme()
                ? KnownColorThemes.Light
                : KnownColorThemes.Dark;
        }
 
        return themeId;
    }
 
    private static bool ShouldAppsUseLightTheme()
    {
        const string PersonalizeKey = @"Software\Microsoft\Windows\CurrentVersion\Themes\Personalize";
        const string AppsUseLightThemeValue = "AppsUseLightTheme";
        const string appsUseDarkTheme = "0";
 
        using var personalizeKey = Registry.CurrentUser.OpenSubKey(PersonalizeKey, writable: false);
        var appsThemeValue = personalizeKey?.GetValue(AppsUseLightThemeValue)?.ToString();
        return appsThemeValue != appsUseDarkTheme;
    }
 
    // NOTE: This service is not public or intended for use by teams/individuals outside of Microsoft. Any data stored is subject to deletion without warning.
    [Guid("9B164E40-C3A2-4363-9BC5-EB4039DEF653")]
    private class SVsSettingsPersistenceManager { }
}