创建 Visual Studio 主题特定的语法高亮显示

11
我想在Visual Studio 2012(及以上版本)中创建一个语法高亮器,支持不同的主题(深色、浅色、蓝色)。Visual Studio的编辑器分类器项目模板介绍了如何使用Microsoft.VisualStudio.Text.Classification.ClassificationFormatDefinition在环境中创建自己的颜色。它可以正常工作,但是你会意识到在Visual Studio 2012(及以上版本)中有不同的主题,而你并没有真正支持它们。在暗色主题环境中,漂亮的深蓝色标识符将变得难以辨认。
据我所知,如果你在Tools/Options/Fonts & Colors中更改给定主题(例如:浅色)下的ClassificationFormatDefinition,它不会影响另一个主题(例如:深色)下的相同ClassificationFormatDefinition。颜色似乎是独立的跨越不同的主题。
这很好,但是我应该如何定义相同名称(例如:MyKeywords)的ClassificationFormatDefinition,使其在所有主题中具有相同的名称,但是为它们提供不同的颜色?就像Visual Studio自己的“Identifier”一样,在浅色主题上默认为黑色,在黑色主题上默认为白色。
我知道Microsoft.VisualStudio.PlatformUI.VSColorTheme.ThemeChanged事件允许我在颜色主题发生更改时收到通知。我是否必须使用这个方法,并以某种方式获取我的现有ClassificationFormatDefinition,并根据新主题为它们分配新的颜色?但这也引出了一个问题:修改后的颜色是否会在环境中保留下来,即如果我重新启动Visual Studio,我的更改是否会在所有不同的主题中保存?
我没有找到任何可以说明ClassificationFormatDefinition支持哪个主题的属性,也没有找到关于此问题的有用文章。
任何帮助都将不胜感激。

可能是重复的问题:https://dev59.com/933aa4cB1Zd3GeqPgq8v - Matze
是的,https://dev59.com/933aa4cB1Zd3GeqPgq8v 基本上问的是同样的问题。我之前没有找到那篇帖子。 然而,它是3个月前发布的,而且仍然没有得到答案,这意味着对于这个主题,没有太多人拥有足够的知识。 :( - Shakaron
5个回答

2
我有一个类似的问题。我为工作中的DSL开发了一个语法高亮器。它有两套颜色 - 用于浅色和深色主题。我需要一种在运行时切换这两个颜色集的方法,当VS主题更改时。

经过一些搜索,我在F# github中找到了一个解决方案,该解决方案负责与VS的集成: https://github.com/dotnet/fsharp/blob/main/vsintegration/src/FSharp.Editor/Classification/ClassificationDefinitions.fs#L121

F#存储库中的代码与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类的基类。
编辑:升级到新版VS的AsyncPackage后,之前的代码停止工作了。你需要在其他地方声明MEF导入项,例如在ClassificationFormatDefinition的继承者中直接声明。 此外,正如@Alessandro所指出的那样,代码中存在一个微妙的错误。如果您切换了VS主题,然后立即转到VS设置的“字体和颜色”部分,您会发现默认颜色值未更改。它们将在重新启动VS后更改,但这仍然不是理想的情况。幸运的是,有一个解决方法(再次感谢@Alessandro)。 您需要使用正确的guid 75A05685-00A8-4DED-BAE5-E7A50BFA929A调用IVsFontAndColorCacheManagerClearCacheRefreshCache,该guid对应于字体和颜色缓存中的MefItems类别在注册表中。以下是描述此问题的文章的参考链接: https://learn.microsoft.com/en-us/dotnet/api/microsoft.visualstudio.shell.interop.ivsfontandcolorcachemanager?view=visualstudiosdk-2019 很遗憾,我找不到 guid 常量的任何文档。
更新:经过一些研究、调试和将错误代码记录到 VS 活动日志中,我发现了以下问题:
  1. 对于单个 VS 主题更改,主题更改处理程序会被调用多次
  2. ClearCache 在前几次调用时返回 0,但之后开始返回错误代码
  3. RefreshCache 总是返回 0(至少在我的情况下)
因此,我用 RefreshCache 替换了对 ClearCache 的调用。
因此,这里是一个更新的示例:
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;
    }
}

您可以通过检查代码编辑器的当前背景来确定是否需要使用适合于浅色或深色主题的颜色。这是我使用的代码链接: https://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派生类都将订阅它,在主题更改时进行特定更改。这具有性能优势,可以批量更新所有格式定义,并仅在主题更改时一次调用EndBatchUpdate和RefreshCache/ClearCache。

如果您在切换到不同的主题后进入“工具/选项/字体和颜色”页面,是否会发现默认设置已被更新? 如果您关闭并重新打开Visual Studio,更改是否生效?我能够完成您上面展示的操作,但仅在同一Visual Studio会话中。如果我关闭并重新打开VS,则会获得带有旧颜色的新主题。 - Alessandro
嗨@Alessandro。我尝试了你描述的操作。我在“字体和颜色”选项部分更改了我的自定义颜色,切换到另一个主题,再更改了另一个自定义颜色以防万一,并关闭了VS。当我重新打开它时,第二个颜色没有重置。我切换回去,第一个颜色也没有重置。我再次重启了VS并切换回第二个主题,第二个颜色也没有重置。所以,我无法复制你的问题。 - SENya
@Alessandro,很遗憾你不能这样做。这是一个扩展,为我公司的 ERP 框架提供高级支持。它需要在解决方案中存在公司框架 dlls,因为它使用 Roslyn 来给代码着色。 - SENya
BQL是商业查询语言的缩写,它允许声明DB提供程序独立的静态类型查询,就像Linq一样,在编译时进行。它利用静态类和泛型编写类似于以下内容的代码: PXSelect<Table, Where<Table.id, Equals<Required<Table.id>>>> curentInstance; - SENya
谢谢再次帮忙。我正在尝试找出我们做了什么不同的地方。这可能与实例化ClassificationFormatDefinitions的时间和使用标签器对项进行着色的时间有关。 - Alessandro
显示剩余5条评论

2

好的,我找到了一个解决方法。它并不完美,但已经尽可能地好了。

技巧是在定义自己的分类类型时使用另一个基本定义。这将使用其不同主题的默认颜色。重要的是,您不能在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;

我希望这对你们中的一些人有所帮助。甚至可能会在您可以设置自己的颜色而不重用现有颜色定义时,帮助您完善正确的解决方案。请注意,保留HTML标记。

3
仅当您希望重复使用现有的“基本”颜色时,此方法才有效。如果您想支持Visual Studio Shell(向非付费用户提供语言支持),您将无法引用诸如“用户类型(接口)”之类的项目,因为这些项目由其他语言服务引入,而这些服务未包含在Shell版本中。 - Sam Harwell

2

2
另一种更干净的方法是使用随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
        }
    }
}

接下来,创建一个colors.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>

每当您进行更改时,请确保在构建之间清除MEF缓存,以强制其更新。此外,可能还需要删除以下注册表键:
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}

好的,我至少可以告诉你它应该是能够工作的,因为我在本地已经让它工作了。很多事情都可能出错。你的ClassificationFormatDefinitions是否标记为[UserVisible(true)]?所有自定义用户可见的[Export(typeof(EditorFormatDefinition))]定义都应该显示在“文本编辑器”字体和颜色设置中。 - Cameron
MEFColours,就像我的答案中的示例一样。名称不重要,但 GUID 必须为 {75A05685-00A8-4DED-BAE5-E7A50BFA929A}。这是用于 VS2015 的,但我认为在其他版本中也有效。 - Cameron
嗨,@Cameron。非常感谢您的评论。在VS SDK中是否有此MEFColours类别的公共常量?我找不到任何包含诸如VSConstants常量的多个类中的内容。 - SENya
1
@SENya:嗯,我也找不到任何(公共的)接口返回那个GUID。 - Cameron
@Cameron 很遗憾。VS SDK文档没有涵盖很多领域,现在我们只需要将魔术GUID传递给他们的API。 - SENya
显示剩余4条评论

0
对于 Visual Studio 2022SENya的答案 只能部分地或“有时”起作用:更改 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)中,我按照SENya的答案进行了适应:

首先,创建一些辅助类来存储默认文本格式:

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://dev59.com/-2Af5IYBdhLWcg3wqELp#48993958
  // - 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://dev59.com/-2Af5IYBdhLWcg3wqELp#48993958
    // 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()中执行所有更新代码多次是不必要的,因此我们检查新旧主题是否不同。
  • 对主题更改的反应在ThemeChangedImpl()中。这是主要基于SENya的答案的代码,但添加了先前通过RegisterFormatDefinition()注册的ClassificationFormatDefinitionReinitialize()调用。

为了完整起见,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都基本相同:它们在构造时设置文本属性(颜色、加粗、斜体等),以适应当前的颜色主题。这是通过查询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的字体和颜色缓存,更新当前分类格式映射(以显示新颜色),并使用新颜色更新所有自定义ClassificationFormatDefinition实例(以便在通过VS重新创建字体和颜色缓存时使用正确的颜色)。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接