创建Visual Studio主题特定语法突出显示

t8e9dugd  于 2023-01-17  发布在  其他
关注(0)|答案(5)|浏览(177)

我想在Visual Studio 2012(及更高版本)中创建一个支持不同主题(深、浅、蓝)的语法突出显示器。
Visual Studio的编辑器分类器项目模板解释了如何使用Microsoft.VisualStudio.Text.Classification.ClassificationFormatDefinition在环境中创建自己的颜色。
...直到您意识到Visual Studio 2012(及更高版本)中有不同的主题,而您实际上并不支持它们。您在浅色主题上漂亮的深蓝色标识符在深色主题环境中变得不可读。
据我所知,如果您在给定主题的工具/选项/字体和颜色中更改了 ClassificationFormatDefinition(例如:浅色),它不会影响不同主题中的相同 ClassificationFormatDefinition(例如:Dark).颜色在不同的主题中似乎是独立的。
这很好。但是**如何实现定义相同的 ClassificationFormatDefinition(例如:MyKeywords)在所有主题中具有相同的名称,但为它们提供不同的颜色?**就像Visual Studio自己的“标识符”一样,在Light主题中默认为黑色,在Black主题中默认为黑色。
我知道Microsoft.VisualStudio.PlatformUI.VSColorTheme.ThemeChanged事件,它允许我在颜色主题更改时得到通知。我是否必须使用此事件并以某种方式获取现有的 ClassificationFormatDefinition,然后根据新主题为它们分配新颜色?但这也会引发一个问题:这些修改过的颜色是否会保留到环境中,例如,如果我重新启动Visual Studio,下次我的更改是否会出现在所有不同的主题中。
我没有找到任何属性来说明 ClassificationFormatDefinition 支持哪个主题,也没有找到很多关于这个主题的有用文章。
任何帮助都感激不尽。

c0vxltue

c0vxltue1#

好吧,我找到了一个变通的办法。它远非完美,但它已经很好了。
诀窍是在定义自己的分类类型时使用另一个基本定义。这将为不同的主题使用它们的默认颜色。重要的是,您不能在MyKeywordsFormatDefinition中定义自己的颜色,因为这会禁用在主题之间切换时的默认行为。因此,请尝试找到与您的颜色匹配的基本定义。请在此处查找预定义的分类类型:Microsoft.VisualStudio.Language.StandardClassification.PredefinedClassificationTypeNames

internal static class Classifications
{
    // ...
    public const string MyKeyword = "MyKeyword";
    // ...
}

[Export(typeof(EditorFormatDefinition))]
[ClassificationType(ClassificationTypeNames = Classifications.MyKeyword)]
[Name("MyKeywords")]
[DisplayName("My Keywords")]
[UserVisible(true)]
internal sealed class MyKeywordsFormatDefinition: ClassificationFormatDefinition
{
    // Don't set the color here, as it will disable the default color supporting themes
}

[Export(typeof(ClassificationTypeDefinition))]
[Name(Classifications.MyKeyword)]
[BaseDefinition(PredefinedClassificationTypeNames.Keyword)]
internal static ClassificationTypeDefinition MyKeywordsTypeDefinition;

我希望它对你们中的一些人有用。甚至可能有助于完善一个适当的解决方案,当你可以实际设置自己的颜色而不重用现有的颜色定义时。

wfveoks0

wfveoks03#

使用VS SDK附带的VsixColorCompiler还有另一种更简洁的方法。
首先,像往常一样创建一个ClassificationTypeDefinitionClassificationFormatDefinition,这将定义所有主题的默认颜色:

public static class MyClassifications
{
    public const string CustomThing = "MyClassifications/CustomThing";

    [Export]
    [Name(CustomThing)]
    public static ClassificationTypeDefinition CustomThingType = null;

    [Export(typeof(EditorFormatDefinition))]
    [ClassificationType(ClassificationTypeNames = CustomThing)]
    [UserVisible(true)]  // Note: must be user-visible to be themed!
    [Name(CustomThing)]
    public sealed class CustomThingFormatDefinition : ClassificationFormatDefinition
    {
        public CustomThingFormatDefinition()
        {
            ForegroundColor = Color.FromRgb(0xFF, 0x22, 0x22);  // default colour in all themes
            DisplayName = "Custom Thing";  // appears in Fonts and Colors options
        }
    }
}

接下来,创建一个colours.xml文件,这将允许我们覆盖特定主题的颜色:

<!-- Syntax described here: https://learn.microsoft.com/en-us/visualstudio/extensibility/internals/vsix-color-compiler -->
<Themes>
  <Theme Name="Light" GUID="{de3dbbcd-f642-433c-8353-8f1df4370aba}">
  </Theme>
  <Theme Name="Dark" GUID="{1ded0138-47ce-435e-84ef-9ec1f439b749}">
    <!-- MEF colour overrides for dark theme -->
    <Category Name="MEFColours" GUID="{75A05685-00A8-4DED-BAE5-E7A50BFA929A}">
      <Color Name="MyClassifications/CustomThing">
        <Foreground Type="CT_RAW" Source="FF2222FF" />
      </Color>
    </Category>
  </Theme>
</Themes>

现在编辑您的.csproj以包含一个构建后命令,将XML编译为一个.pkgdef,紧挨着您的普通包的.pkgdef(VS2015 SDK如下所示):

<Target Name="AfterBuild">
  <Message Text="Compiling themed colours..." Importance="high" />
  <Exec Command="&quot;$(VSSDK140Install)\VisualStudioIntegration\Tools\Bin\VsixColorCompiler.exe&quot; /noLogo &quot;$(ProjectDir)colours.xml&quot; &quot;$(OutputPath)\MyPackage.Colours.pkgdef&quot;" />
</Target>

无论何时进行更改,请确保在两个版本之间执行clear the MEF cache以强制其更新。此外,可能还需要删除以下注册表项:

HKEY_CURRENT_USER\Software\Microsoft\VisualStudio\14.0\FontAndColors\Cache\{75A05685-00A8-4DED-BAE5-E7A50BFA929A}
HKEY_CURRENT_USER\Software\Microsoft\VisualStudio\14.0Exp\FontAndColors\Cache\{75A05685-00A8-4DED-BAE5-E7A50BFA929A}
xuo3flqw

xuo3flqw4#

我也遇到过类似的问题。我已经为DSL开发了一个语法高亮器。它有两组颜色--亮和暗主题。我需要一种方法,当VS主题改变时,在运行时在这两组颜色之间切换。
经过一番搜索,我在F#github中负责与VS集成的代码中找到了一个解决方案:www.example.comhttps://github.com/dotnet/fsharp/blob/main/vsintegration/src/FSharp.Editor/Classification/ClassificationDefinitions.fs#L121
F#repo中的代码与Omer Raviv的答案中的代码非常相似,我将其翻译成C#,得到如下内容:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Windows.Media;
using Microsoft.VisualStudio.Text.Classification;
using Microsoft.VisualStudio.Utilities;
using Microsoft.VisualStudio.PlatformUI;
using Microsoft.VisualStudio.Shell;
using Microsoft.VisualStudio.Shell.Interop;

using DefGuidList = Microsoft.VisualStudio.Editor.DefGuidList;
using VSConstants =  Microsoft.VisualStudio.VSConstants;

//...

internal abstract class EditorFormatBase : ClassificationFormatDefinition, IDisposable
{
    private const string textCategory = "text";
    private readonly string classificationTypeName; 
    
    protected EditorFormatBase()
    {          
        VSColorTheme.ThemeChanged += VSColorTheme_ThemeChanged;

        //Get string ID which has to be attached with NameAttribute for ClassificationFormatDefinition-derived classes
        Type type = this.GetType();
        classificationTypeName = type.GetCustomAttribute<NameAttribute>()?.Name;      

        if (classificationTypeName != null)
        {
                ForegroundColor = VSColors.GetThemedColor(classificationTypeName);   //Call to my class VSColors which returns correct color for the theme
        }              
    }
  
    private void VSColorTheme_ThemeChanged(ThemeChangedEventArgs e)
    {
   

     //Here MyPackage.Instance is a singleton of my extension's Package derived class, it contains references to
     // IClassificationFormatMapService and  
     // IClassificationTypeRegistryService objects
        if (MyPackage.Instance?.ClassificationFormatMapService == null || MyPackage.Instance.ClassificationRegistry == null || classificationTypeName == null)
        {
            return;
        }

        var fontAndColorStorage = 
            ServiceProvider.GlobalProvider.GetService(typeof(SVsFontAndColorStorage)) as IVsFontAndColorStorage;
        var fontAndColorCacheManager = 
            ServiceProvider.GlobalProvider.GetService(typeof(SVsFontAndColorCacheManager)) as IVsFontAndColorCacheManager;

        if (fontAndColorStorage == null || fontAndColorCacheManager == null)
            return;

        Guid guidTextEditorFontCategory = DefGuidList.guidTextEditorFontCategory;
        fontAndColorCacheManager.CheckCache(ref guidTextEditorFontCategory, out int _ );

        if (fontAndColorStorage.OpenCategory(ref guidTextEditorFontCategory, (uint) __FCSTORAGEFLAGS.FCSF_READONLY) != VSConstants.S_OK)
        {
            //Possibly log warning/error, in F# source it’s ignored           
        }

        Color? foregroundColorForTheme =  VSColors.GetThemedColor(classificationTypeName);  //VSColors is my class which stores colors, GetThemedColor returns color for the theme

        if (foregroundColorForTheme == null)
            return;
                
        IClassificationFormatMap formatMap = MyPackage.Instance.ClassificationFormatMapService
                              .GetClassificationFormatMap(category: textCategory);

        if (formatMap == null)
            return;

        try
        {
            formatMap.BeginBatchUpdate();
            ForegroundColor = foregroundColorForTheme;
            var myClasType = MyPackage.Instance.ClassificationRegistry
                                                                  .GetClassificationType(classificationTypeName);

            if (myClasType == null)
                return;

            ColorableItemInfo[] colorInfo = new ColorableItemInfo[1];

            if (fontAndColorStorage.GetItem(classificationTypeName, colorInfo) != VSConstants.S_OK)    //comment from F# repo: "we don't touch the changes made by the user"
            {
                var properties = formatMap.GetTextProperties(myClasType);
                var newProperties = properties.SetForeground(ForegroundColor.Value);

                formatMap.SetTextProperties(myClasType, newProperties);
            }                                                                           
        }
        catch (Exception)
        {
            //Log error here, in F# repo there are no catch blocks, only finally block       
        }
        finally
        {
            formatMap.EndBatchUpdate();
        }          
    }

    void IDisposable.Dispose()
    {
        VSColorTheme.ThemeChanged -= VSColorTheme_ThemeChanged;
    }
}

我已经使用上面的类作为所有ClassificationFormatDefinition类的基类。
编辑:升级到AsyncPackage以获得VS的新版本后,之前的代码停止工作。您需要在其他地方声明MEF导入,例如,直接在ClassificationFormatDefinition的继承者中声明。此外,正如@Alessandro所指出的,代码中存在一个微妙的bug,如果切换VS主题,然后立即转到VS设置"字体和颜色"你会看到默认颜色值没有改变。它们会在VS重启后改变,但这仍然不是理想的。幸运的是,有一个解决办法(再次感谢@Alessandro).您需要拨打IVsFontAndColorCacheManager 's具有正确guid 75A05685-00A8-4DED-BAE5-E7A50BFA929AClearCacheRefreshCache,该guid与注册表中Fonts and Colors缓存中的MefItems类别相对应。下面是对一篇文章的引用,该文章对此进行了一些描述:https://learn.microsoft.com/en-us/dotnet/api/microsoft.visualstudio.shell.interop.ivsfontandcolorcachemanager?view=visualstudiosdk-2019
不幸的是,我找不到guid常量的任何文档。
更新:经过更多的研究,调试和添加记录坏错误代码到VS活动日志,我发现以下几点:
1.为VS主题的单个更改多次调用主题更改处理程序

  1. ClearCache在前几次调用时返回0,但在此之后开始返回错误代码
  2. RefreshCache始终返回0(至少在我的情况下)
    因此,我将对ClearCache的调用替换为对RefreshCache的调用。
    下面是一个更新的示例:
internal abstract class EditorFormatBase : ClassificationFormatDefinition, IDisposable
{
    private const string TextCategory = "text";
    private readonly string _classificationTypeName;

    private const string MefItemsGuidString = "75A05685-00A8-4DED-BAE5-E7A50BFA929A";
    private Guid _mefItemsGuid = new Guid(MefItemsGuidString);

    [Import]
    internal IClassificationFormatMapService _classificationFormatMapService = null;  //Set via MEF

    [Import]
    internal IClassificationTypeRegistryService _classificationRegistry = null; // Set via MEF

    protected EditorFormatBase()
    {          
        VSColorTheme.ThemeChanged += VSColorTheme_ThemeChanged;

        Type type = this.GetType();
        _classificationTypeName = type.GetCustomAttribute<NameAttribute>()?.Name;
        
        if (_classificationTypeName != null)
        {
            ForegroundColor = VSColors.GetThemedColor(_classificationTypeName);
        }
    }

    private void VSColorTheme_ThemeChanged(ThemeChangedEventArgs e)
    {
        ThreadHelper.ThrowIfNotOnUIThread();

        if (_classificationFormatMapService == null || _classificationRegistry == null || _classificationTypeName == null)
            return;

        var fontAndColorStorage = ServiceProvider.GlobalProvider.GetService<SVsFontAndColorStorage, IVsFontAndColorStorage>();
        var fontAndColorCacheManager = ServiceProvider.GlobalProvider.GetService<SVsFontAndColorCacheManager, IVsFontAndColorCacheManager>();

        if (fontAndColorStorage == null || fontAndColorCacheManager == null)
            return;

        fontAndColorCacheManager.CheckCache(ref _mefItemsGuid, out int _);

        if (fontAndColorStorage.OpenCategory(ref _mefItemsGuid, (uint)__FCSTORAGEFLAGS.FCSF_READONLY) != VSConstants.S_OK)
        {
            //TODO Log error              
        }

        Color? foregroundColorForTheme = VSColors.GetThemedColor(_classificationTypeName);

        if (foregroundColorForTheme == null)
            return;

        IClassificationFormatMap formatMap = _classificationFormatMapService.GetClassificationFormatMap(category: TextCategory);

        if (formatMap == null)
            return;

        try
        {
            formatMap.BeginBatchUpdate();
            ForegroundColor = foregroundColorForTheme;
            var classificationType = _classificationRegistry.GetClassificationType(_classificationTypeName);

            if (classificationType == null)
                return;

            ColorableItemInfo[] colorInfo = new ColorableItemInfo[1];

            if (fontAndColorStorage.GetItem(_classificationTypeName, colorInfo) != VSConstants.S_OK)    //comment from F# repo: "we don't touch the changes made by the user"
            {
                var properties = formatMap.GetTextProperties(classificationType);
                var newProperties = properties.SetForeground(ForegroundColor.Value);

                formatMap.SetTextProperties(classificationType, newProperties);
            }      
        }
        catch (Exception)
        {
            //TODO Log error here               
        }
        finally
        {
            formatMap.EndBatchUpdate();
           
            if (fontAndColorCacheManager.RefreshCache(ref _mefItemsGuid) != VSConstants.S_OK)
            {
                //TODO Log error here
            }

            fontAndColorStorage.CloseCategory();
        }
    }

    void IDisposable.Dispose()
    {
        VSColorTheme.ThemeChanged -= VSColorTheme_ThemeChanged;
    }
}

通过检查代码编辑器的当前背景,可以确定是否需要使用适合浅色或深色主题的颜色。下面是我使用的代码的链接:www.example.comhttps://github.com/Acumatica/Acuminator/blob/dev/src/Acuminator/Acuminator.Vsix/Coloriser/Constants/VSColors.cs#L82
下面是@Alessandro的一个更简洁的片段(再次感谢!):

var colorBackground = VSColorTheme.GetThemedColor(EnvironmentColors.ToolWindowBackgroundColorKey);
Color color = (colorBackground != null && colorBackground.B < 64) ? lightcolor : darkcolor;

您也可以创建一个单独的共享ThemeUpdater类,该类将订阅ThemeChanged事件,并且所有ClassificationFormatDefinition派生类都将订阅它,以便在主题更改时进行特定更改。这具有性能优势,您可以批量更新所有格式定义,并且在主题更改时仅调用EndBatchUpdateRefreshCache/ClearCache一次。

pdtvr36n

pdtvr36n5#

对于Visual Studio 2022answer by SENya只能部分或“有时”工作:更改VS主题不会立即正确地更改颜色,大约有10%的情况是这样。此外,将主题从深色更改为浅色看起来很正常,但在重新启动Visual Studio后,经常使用深色而不是浅色(超过一半的情况)。所有这些都是不确定的。
经过一些调试,我理解的问题如下:调用IVsFontAndColorCacheManager.ClearCache()将删除注册表项"Software\Microsoft\VisualStudio\17.0_4d51a943Exp\FontAndColors\Cache\{75A05685-00A8-4DED-BAE5-E7A50BFA929A}\ItemAndFontInfo",该注册表项是字体和颜色该高速缓存。在自定义主题更改函数完成后,某些其他Visual Studio组件有时(但并不总是)立即更新字体和颜色缓存。也就是说,它调用类似fontAndColorStorage.OpenCategory(ref mFontAndColorCategoryGUID, (uint)__FCSTORAGEFLAGS.FCSF_READONLY | (uint)__FCSTORAGEFLAGS.FCSF_LOADDEFAULTS)的内容。请注意FCSF_LOADDEFAULTS。这会导致Visual Studio重新创建注册表项。但是,显然,它没有使用更新后的IClassificationFormatMap的颜色,而是使用了ClassificationFormatDefinition本身设置的颜色,这些颜色没有更新,因此,更改主题会立即更改显示的颜色(因为IClassificationFormatMap得到了更新),但是注册表缓存以错误的颜色结束。在重新启动VS之后,它使用缓存的值,因此最终使用错误的颜色。通过在ClassificationFormatDefinition示例上也更改颜色,该问题似乎得到了修复。

详情

在我的VSDoxyHighlighterGithub)中,我对answer by SENya进行了如下修改:
首先,一些helper类存储默认的文本格式:

public class TextProperties 
{
  public readonly Color? Foreground;
  public readonly Color? Background;
  public readonly bool IsBold;
  public readonly bool IsItalic;

  public TextProperties(Color? foreground, Color? background, bool isBold, bool isItalic)
  {
    Foreground = foreground;
    Background = background;
    IsBold = isBold;
    IsItalic = isItalic;
  }
}

然后是处理主题内容的主类DefaultColors

/// <summary>
/// Manages the default colors and formatting of our classifications, suitable for the current Visual Studio's color theme.
/// Thus, it provides access to the default formatting for the current theme, and also updates them if the theme 
/// of Visual Studio is changed by the user.
/// 
/// Note that the user settings are stored per theme in the registry.
/// 
/// An instance should be created via MEF.
/// </summary>
[Export]
public class DefaultColors : IDisposable
{
  DefaultColors() 
  {
    VSColorTheme.ThemeChanged += VSThemeChanged;
    mCurrentTheme = GetCurrentTheme();
  }

  public void Dispose()
  {
    if (mDisposed) {
      return;
    }
    mDisposed = true;
    VSColorTheme.ThemeChanged -= VSThemeChanged;
  }

  /// <summary>
  /// Returns the default colors for our extension's classifications, as suitable for the current color theme. 
  /// </summary>
  public Dictionary<string, TextProperties> GetDefaultFormattingForCurrentTheme()
  {
    return GetDefaultFormattingForTheme(mCurrentTheme);
  }

  public void RegisterFormatDefinition(IFormatDefinition f) 
  {
    mFormatDefinitions.Add(f);
  }

  private enum Theme
  {
    Light,
    Dark
  }

  static private Dictionary<string, TextProperties> GetDefaultFormattingForTheme(Theme theme)
  {
    switch (theme) {
      case Theme.Light:
        return cLightColors;
      case Theme.Dark:
        return cDarkColors;
      default:
        throw new System.Exception("Unknown Theme");
    }
  }

  // Event called by Visual Studio multiple times (!) when the user changes the color theme of Visual Studio.
  private void VSThemeChanged(ThemeChangedEventArgs e)
  {
    ThreadHelper.ThrowIfNotOnUIThread();

    var newTheme = GetCurrentTheme();
    if (newTheme != mCurrentTheme) {
      mCurrentTheme = newTheme; // Important: We indirectly access mCurrentTheme during the update, so set it before.
      ThemeChangedImpl();
    }
  }

  // Called when the Visual Studio theme changes. Responsible for switching out the default colors
  // of the classifications.
  //
  // Based on:
  // - https://stackoverflow.com/a/48993958/3740047
  // - https://github.com/dotnet/fsharp/blob/main/vsintegration/src/FSharp.Editor/Classification/ClassificationDefinitions.fs#L133
  // - https://github.com/fsprojects-archive/zzarchive-VisualFSharpPowerTools/blob/master/src/FSharpVSPowerTools/Commands/SymbolClassifiersProvider.cs
  private void ThemeChangedImpl()
  {
    ThreadHelper.ThrowIfNotOnUIThread();

    var fontAndColorStorage = ServiceProvider.GlobalProvider.GetService<SVsFontAndColorStorage, IVsFontAndColorStorage>();
    var fontAndColorCacheManager = ServiceProvider.GlobalProvider.GetService<SVsFontAndColorCacheManager, IVsFontAndColorCacheManager>();

    fontAndColorCacheManager.CheckCache(ref mFontAndColorCategoryGUID, out int _);

    if (fontAndColorStorage.OpenCategory(ref mFontAndColorCategoryGUID, (uint)__FCSTORAGEFLAGS.FCSF_READONLY) != VSConstants.S_OK) {
      throw new System.Exception("Failed to open font and color registry.");
    }

    IClassificationFormatMap formatMap = mClassificationFormatMapService.GetClassificationFormatMap(category: "text");

    try {
      formatMap.BeginBatchUpdate();

      ColorableItemInfo[] colorInfo = new ColorableItemInfo[1];
      foreach (var p in GetDefaultFormattingForTheme(mCurrentTheme)) {
        string classificationTypeId = p.Key;
        TextProperties newColor = p.Value;

        if (fontAndColorStorage.GetItem(classificationTypeId, colorInfo) != VSConstants.S_OK) { //comment from F# repo: "we don't touch the changes made by the user"
          IClassificationType classificationType = mClassificationTypeRegistryService.GetClassificationType(classificationTypeId);
          var oldProp = formatMap.GetTextProperties(classificationType);
          var oldTypeface = oldProp.Typeface;

          var foregroundBrush = newColor.Foreground == null ? null : new SolidColorBrush(newColor.Foreground.Value);
          var backgroundBrush = newColor.Background == null ? null : new SolidColorBrush(newColor.Background.Value);

          var newFontStyle = newColor.IsItalic ? FontStyles.Italic : FontStyles.Normal;
          var newWeight = newColor.IsBold ? FontWeights.Bold : FontWeights.Normal;
          var newTypeface = new Typeface(oldTypeface.FontFamily, newFontStyle, newWeight, oldTypeface.Stretch);

          var newProp = TextFormattingRunProperties.CreateTextFormattingRunProperties(
            foregroundBrush, backgroundBrush, newTypeface, null, null,
            oldProp.TextDecorations, oldProp.TextEffects, oldProp.CultureInfo);

          formatMap.SetTextProperties(classificationType, newProp);
        }
      }

      // Also update all of our ClassificationFormatDefinition values with the new values.
      // Without this, changing the theme does not reliably update the colors: Sometimes after restarting VS, we get
      // the wrong colors. For example, when switching from the dark to the light theme, we often end up with the colors
      // of the dark theme after a VS restart.
      // From what I could understand: The call fontAndColorCacheManager.ClearCache() below deletes the registry key
      //     "Software\Microsoft\VisualStudio\17.0_4d51a943Exp\FontAndColors\Cache\{75A05685-00A8-4DED-BAE5-E7A50BFA929A}\ItemAndFontInfo"
      // which is the cache of the font and colors. After our function here finishes, some Visual Studio component
      // sometimes (but not always) immediately updates the font and color cache. I.e. it calls something like
      //     fontAndColorStorage.OpenCategory(ref mFontAndColorCategoryGUID, (uint)__FCSTORAGEFLAGS.FCSF_READONLY | (uint)__FCSTORAGEFLAGS.FCSF_LOADDEFAULTS).
      // Note the "FCSF_LOADDEFAULTS". This causes Visual Studio to re-create the registry key. However, apparently
      // it does not use the colors from the updated formatMap, but instead the colors set on the ClassificationFormatDefinition,
      // which were not yet updated so far. Thus, changing the theme, changes the displayed colors immediately (because we update
      // the formatMap), but the registry cache ends up with the wrong colors. After a restart of VS, it uses the cached values
      // and therefore we get the wrong colors.
      // By changing the colors also on the ClassificationFormatDefinition, the issue appears to be fixed.
      foreach (IFormatDefinition f in mFormatDefinitions) {
        f.Reinitialize();
      }
    }
    finally {
      formatMap.EndBatchUpdate();
      fontAndColorStorage.CloseCategory();

      if (fontAndColorCacheManager.ClearCache(ref mFontAndColorCategoryGUID) != VSConstants.S_OK) {
        throw new System.Exception("Failed to clear cache of FontAndColorCacheManager.");
      }
    }
  }

  private Theme GetCurrentTheme()
  {
    // We need to figure out if our extension should choose the default colors suitable for light or dark themes.
    // In principle we could explicitly retrieve the color theme currently active in Visual Studio. However, that
    // approach is fundamentally flawed: We could check if the theme is one of the default ones (dark, light, blue,
    // etc.), but Visual Studio supports installing additional themes. It is impossible to know all themes existing
    // out there. So, what we really want is to check if the dark or the light defaults are more suitable given the
    // text editor's background color.
    // However, the EnvironmentColors does not seem to contain an element for the text editor's background. So we
    // simply use the tool windows' background, as suggested also here: https://stackoverflow.com/a/48993958/3740047
    // The simplistic heuristic of just checking the blue color seems to work reasonably well. The magic threshold
    // was chosen to (hopefully) select the better value for the themes shown at https://devblogs.microsoft.com/visualstudio/custom-themes/
    var referenceColor = VSColorTheme.GetThemedColor(EnvironmentColors.ToolWindowBackgroundColorKey);
    return (referenceColor != null && referenceColor.B < 100) ? Theme.Dark : Theme.Light;
  }

  // Default colors for light color themes.
  static readonly Dictionary<string, TextProperties> cLightColors = new Dictionary<string, TextProperties> {
    { IDs.ID_command,        new TextProperties(foreground: Color.FromRgb(0, 75, 0),    background: null, isBold: true,  isItalic: false) },
    { IDs.ID_parameter1,     new TextProperties(foreground: Color.FromRgb(0, 80, 218),  background: null, isBold: true,  isItalic: false) },
    // ... further custom classifications
  };

  // Default colors for dark color themes.
  static readonly Dictionary<string, TextProperties> cDarkColors = new Dictionary<string, TextProperties> {
    { IDs.ID_command,        new TextProperties(foreground: Color.FromRgb(140, 203, 128), background: null, isBold: true,  isItalic: false) },
    { IDs.ID_parameter1,     new TextProperties(foreground: Color.FromRgb(86, 156, 214),  background: null, isBold: true,  isItalic: false) },
    // ... further custom classifications
  };

  
  private Theme mCurrentTheme;

  // GUID of the category in which our classification items are placed (i.e. of the elements in the
  // fonts and colors settings of Visual Studio). Not just ours but all sorts of other items exist
  // in this category, too.
  // Can be found by installing our extension, modifying some of the colors of the classifications in
  // the Visual Studio's settings dialog, then exporting the settings and checking the resulting file.
  // The section about the modified entries contains the proper GUID.
  private const string cFontAndColorCategory = "75A05685-00A8-4DED-BAE5-E7A50BFA929A";
  Guid mFontAndColorCategoryGUID = new Guid(cFontAndColorCategory);

  [Import]
  private IClassificationFormatMapService mClassificationFormatMapService = null;

  [Import]
  private IClassificationTypeRegistryService mClassificationTypeRegistryService = null;

  private List<IFormatDefinition> mFormatDefinitions = new List<IFormatDefinition>();

  private bool mDisposed = false;
}

这里需要注意几点:

  • DefaultColors的一个示例不应该手工创建,而应该只由MEF创建一个示例(例如通过Import属性)。
  • 当前的VS主题是通过检查一些当前活动的背景颜色来识别的,如this answer。原则上可以检查亮、蓝、蓝(高对比度)、暗等VS主题是否活动。但是,由于用户可以安装其他主题,列表是无穷无尽的。因此,简单地检查背景颜色更通用和健壮。
  • ClassificationFormatDefinition定义(表示Visual Studio用于各种分类的文本格式)应通过RegisterFormatDefinition()DefaultColors示例上注册自身。
  • 为了获得有关Visual Studio主题更改的通知,我们订阅了VSColorTheme.ThemeChanged。还请注意,每次主题更改都会多次触发该事件。由于没有必要多次执行ThemeChangedImpl()中的所有更新代码,因此我们检查新旧主题是否不同。
  • 对主题变化的React在ThemeChangedImpl()中,这是主要基于answer by SENya的代码,但是添加了以前通过RegisterFormatDefinition()注册的ClassificationFormatDefinition获得对Reinitialize()的调用。

为了完整起见,ID_commandID_parameter1是用于标识ClassificationFormatDefinition的一些自定义标识符(参见下文):

/// <summary>
/// Identifiers for the classifications. E.g., Visual Studio will use these strings as keys
/// to store the classification's configuration in the registry.
/// </summary>
public static class IDs
{
  public const string ID_command = "VSDoxyHighlighter_Command";
  public const string ID_parameter1 = "VSDoxyHighlighter_Parameter1";
  // ... further IDs for further classifications
}

现在,实际的ClassificationFormatDefinition定义如下:它们继承自接口IFormatDefinition(可以传递给DefaultColors.RegisterFormatDefinition()函数)

public interface IFormatDefinition 
{
  void Reinitialize();
}

所有ClassificationFormatDefinition基本相同:它们设置文本属性(color、bold、italic等),在构造时适合当前的颜色主题,这是通过查询DefaultColors.GetDefaultFormattingForCurrentTheme()函数来完成的,并且它们在DefaultColors上注册自己,并实现Reinitialize()方法(由DefaultColors调用),因为总是一样的,所以我为它们定义了一个基类FormatDefinitionBase

internal abstract class FormatDefinitionBase : ClassificationFormatDefinition, IFormatDefinition
{
  protected FormatDefinitionBase(DefaultColors defaultColors, string ID, string displayName) 
  {
    if (defaultColors == null) {
      throw new System.ArgumentNullException("VSDoxyHighlighter: The 'DefaultColors' to a FormatDefinition is null");
    }

    mID = ID;

    mDefaultColors = defaultColors;
    mDefaultColors.RegisterFormatDefinition(this);

    DisplayName = displayName;

    Reinitialize();
  }

  public virtual void Reinitialize()
  {
    TextProperties color = mDefaultColors.GetDefaultFormattingForCurrentTheme()[mID];
    ForegroundColor = color.Foreground;
    BackgroundColor = color.Background;
    IsBold = color.IsBold;
    IsItalic = color.IsItalic;
  }

  protected readonly DefaultColors mDefaultColors;
  protected readonly string mID;
}

最后,实际的定义如下所示:

[Export(typeof(EditorFormatDefinition))]
[ClassificationType(ClassificationTypeNames = IDs.ID_command)]
[Name(IDs.ID_command)]
[UserVisible(true)]
[Order(After = /*Whatever is appropriate for your extension*/)]
internal sealed class CommandFormat : FormatDefinitionBase
{
  [ImportingConstructor]
  public CommandFormat(DefaultColors defaultColors)
    : base(defaultColors, IDs.ID_command, "VSDoxyHighlighter - Command")
  {
  }
}

[Export(typeof(EditorFormatDefinition))]
[ClassificationType(ClassificationTypeNames = IDs.ID_parameter1)]
[Name(IDs.ID_parameter1)]
[UserVisible(true)]
[Order(After = /*Whatever is appropriate for your extension*/)]
internal sealed class ParameterFormat1 : FormatDefinitionBase
{
  [ImportingConstructor]
  public ParameterFormat1(DefaultColors defaultColors)
    : base(defaultColors, IDs.ID_parameter1, "VSDoxyHighlighter - Parameter 1")
  {
  }
}

//... Further format definitions

请注意,构造函数被标记为ImportingConstructor,以便MEF自动创建DefaultColors类的单个示例并将其传递给构造函数。
所以,总结一下:

  • ClassificationFormatDefinition由MEF创建。同时,MEF还创建了DefaultColors的一个示例并将其传递给ClassificationFormatDefinitionClassificationFormatDefinition设置默认颜色并提供一个函数以允许其在主题更改时重新初始化。为了实现这一点,它还在DefaultColors示例上注册了自己。
  • DefaultColors计算出当前主题,并包含每个主题的默认颜色。
  • DefaultColors侦听VSColorTheme.ThemeChanged事件,如果触发,则清除Visual Studio的字体和颜色缓存,更新当前分类格式Map(以显示新颜色),并使用新颜色更新所有自定义ClassificationFormatDefinition示例(以便在VS重新创建字体和颜色缓存时将正确的颜色用于该高速缓存)。

相关问题