【问题标题】:Create Visual Studio Theme Specific Syntax Highlighting创建 Visual Studio 主题特定语法突出显示
【发布时间】:2014-06-25 09:06:11
【问题描述】:

我想在 Visual Studio 2012 中创建一个语法荧光笔(及更高版本)支持不同的主题(深色、浅色、蓝色)。

Visual Studio 的编辑器分类器项目模板解释了如何使用Microsoft.VisualStudio.Text.Classification.ClassificationFormatDefinition 在环境中创建自己的颜色。它工作正常...

...直到您意识到 Visual Studio 2012(及更高版本)中有不同的主题并且您并不真正支持它们。浅色主题上漂亮的深蓝色标识符在深色主题环境中变得不可读。

据我了解,如果您在给定主题(例如:Light)的工具/选项/字体和颜色中更改 ClassificationFormatDefinition,它不会影响相同的 ClassificationFormatDefinition 在不同的主题中(例如:黑暗)。颜色似乎在不同主题中是独立的。

这很好。但是我如何实现定义相同的 ClassificationFormatDefinition(例如:MyKeywords)在所有主题中具有相同的名称,但为它们提供不同的颜色?就像 Visual Studio 自己的一样“标识符”,Light 主题默认为黑色,Black 主题默认为黑色。

我知道Microsoft.VisualStudio.PlatformUI.VSColorTheme.ThemeChanged 事件,它允许我在颜色主题更改时收到通知。我是否必须使用它并以某种方式获取我现有的 ClassificationFormatDefinition 并根据新主题为它们分配新颜色?但这也提出了一个问题:这些修改后的颜色是否会保留到环境中,即如果我重新启动 Visual Studio,我的更改是否会在下一次出现在所有不同的主题中。

我没有找到任何可以说明 ClassificationFormatDefinition 支持哪个主题的属性,也没有找到关于该主题的很有帮助的文章。

任何帮助表示赞赏。

【问题讨论】:

标签: visual-studio visual-studio-2012 syntax-highlighting visual-studio-extensions vspackage


【解决方案1】:

好的,这是我找到的解决方法。它远非完美,但已经做到了最好。

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

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

【讨论】:

  • 这仅在您想要重复使用现有的“基本”颜色时有效。如果您想支持 Visual Studio Shell(为非付费用户提供语言支持),您将无法引用诸如“用户类型(接口)”之类的项目,因为这些项目是由其他语言服务引入的,这些服务是不包含在 Shell 版本中。
【解决方案2】:

这可能会对您有所帮助,来自 F# Power Tools 的代码似乎正在监听 ThemeChanged 事件并更新分类器 - https://github.com/fsprojects/VisualFSharpPowerTools/blob/a7d7aa9dd3d2a90f21c6947867ac7d7163b9f99a/src/FSharpVSPowerTools/SyntaxConstructClassifierProvider.cs

【讨论】:

    【解决方案3】:

    使用 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://docs.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}
    

    【讨论】:

    • 我正在尝试听从您的建议。我已经能够为四个主题中的每一个创建一个替换“文本编辑器语言服务项目”类别中的现有颜色(“注释”)的颜色 pkgdef 文件。它工作正常。如果我转到工具/选项/字体和颜色,我会看到新的默认设置为我为四个主题中的每一个指定的颜色。但是,如果我将一些自定义颜色名称添加到与我的 ClassificationFormatDefinitions 中的名称匹配的颜色 pkgdef,则不会发生任何事情。在对话框中,我的自定义颜色的默认颜色没有改变。我还需要做什么?
    • (续) 通常,我在 ClassificationFormatDefinitions 中指定的颜色会自动添加到“字体和颜色”对话框中(作为默认设置)。所以我一直假设这些颜色被添加到相同的类别(“文本编辑器语言服务项目”)中,因为它们在“文本编辑器”下的字体和颜色中的周围条目。我不知道如何验证这一点。所以我想我可以将匹配的颜色元素添加到颜色 pkgdef 中的同一类别中,它们将显示为“每个主题”的默认值。但它不起作用。它仅适用于预定义的颜色。
    • 好吧,我至少可以告诉你它应该可以工作,因为我让它在本地工作。很多事情可能会出错。您的 ClassificationFormatDefinitions 是 [UserVisible(true)] 吗?所有自定义用户可见的[Export(typeof(EditorFormatDefinition))] 定义都应显示在“文本编辑器”字体和颜色设置中。
    • 确实如此。您在颜色 XML 文件中为自定义项目指定了什么类别(名称和 GUID)?
    • @SENya:嗯,我也找不到返回该 GUID 的任何(公共)接口。
    【解决方案4】:

    我遇到了类似的问题。我为工作中的 DSL 开发了一个语法荧光笔。它有两组颜色 - 用于浅色和深色主题。当 VS 主题发生变化时,我需要一种在运行时在这两组颜色之间切换的方法。

    经过一番搜索,我在 F# github 中负责与 VS 集成的代码中找到了解决方案: https://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 所指出的,代码中存在一个微妙的错误。如果您切换 VS 主题,然后立即转到 VS 设置“字体和颜色”部分,您将看到默认颜色值没有改变。它们会在 VS 重新启动后发生变化,但这仍然不理想。幸运的是,有一个解决方案(再次感谢@Alessandro)。您需要使用正确的 guid 75A05685-00A8-4DED-BAE5-E7A50BFA929A 调用 IVsFontAndColorCacheManagerClearCacheRefreshCache,它对应于注册表中字体和颜色缓存中的 MefItems 类别。 这是对一篇文章的参考,该文章对此进行了一些描述: https://docs.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 的更简洁的 sn-p(再次感谢!):

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

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

    【讨论】:

    • 如果你在切换到不同的主题后进入工具/选项/字体和颜色,你看到默认设置已经更新了吗?如果关闭并重新打开 Visual Studio,更改是否生效?我已经能够完成您在上面显示的内容,但只能在同一个 Visual Studio 会话中完成。如果我关闭并重新打开 VS,我会得到旧颜色的新主题。
    • 嗨@Alessandro。我尝试了您描述的操作。我在“字体和颜色”选项部分更改了我的自定义颜色,切换到另一个主题,更改了另一种自定义颜色以防万一并关闭了 VS。当我重新打开它时,第二种颜色没有重置。我改回来了,第一种颜色也没有重置。我再次重新启动VS并切换回第二个主题,第二个颜色也没有重置。所以,我无法重现您的问题。
    • 您实际上可以在这里看到我的扩展:github.com/Acumatica/Acuminator/blob/dev/src/Acuminator/… 我有两组预定义的颜色 - 用于浅色和深色背景,使用的颜色根据代码编辑器的当前背景推断。我尝试了浅色和深色 VS 主题,都允许自定义颜色,并且在重启或主题切换后更改仍然存在。
    • 非常感谢您的回复!我正在尝试你的扩展。你确定颜色总是更新吗?现在,我在实验会话中打开了您的扩展程序。主题是黑暗。我打开了字体和颜色对话框。它显示项目“Acuminator - BQL Angle Brackets (Level 1)”设置为黑色默认。我看不到实际的彩色项目(我不知道如何创建),但我在编辑器中打开了一个 C# 文件。
    • @Alessandro 不幸的是,您将无法做到这一点。它是为我公司的 ERP 框架提供高级支持的扩展。它需要在解决方案中存在公司框架 dll,因为它在 Roslyn 的帮助下为代码着色
    猜你喜欢
    • 2016-11-04
    • 1970-01-01
    • 2018-08-23
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2017-10-19
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多