Create Visual Studio Theme Specific Syntax Highlighting

后端 未结 4 902
独厮守ぢ
独厮守ぢ 2021-01-04 15:56

I would like to create a Syntax Highlighter in Visual Studio 2012 (and above) that supports different themes (Dark, Light, Blue).

V

相关标签:
4条回答
  • 2021-01-04 16:31

    There's another, cleaner way using the VsixColorCompiler that ships with the VS SDK.

    First, create a ClassificationTypeDefinition and ClassificationFormatDefinition as usual. This will define the default colour in all themes:

    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
            }
        }
    }
    

    Next, create a colours.xml file. This will allow us to override the colour for specific themes:

    <!-- 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>
    

    Now edit your .csproj to include a post-build command to compile the XML to a .pkgdef next to your normal package's .pkgdef (VS2015 SDK shown here):

    <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>
    

    Whenever you make a change, be sure to clear the MEF cache between builds to force it to update. Additionally, the following registry keys may need to be deleted as well:

    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}
    
    0 讨论(0)
  • 2021-01-04 16:33

    Ok, here's a workaround I've found. It is far from perfect, but it is as good as it gets.

    The trick is to use another base definition when you define your own classification type. This will use their default color for the different themes. The important thing is that you must not define your own color in MyKeywordsFormatDefinition because that disables the default behavior when switching between themes. So try to find a base definition that matches your color. Look for predefined Classificatoin Types here: 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;
    

    I hope it will be useful for some of you. Even maybe help to refine a proper solution when you can actually set your own color without reusing existing color definitions.

    0 讨论(0)
  • 2021-01-04 16:38

    I had a similar problem. I've developed a syntax highlighter for the DSL at work. It has two sets of colors - for light and dark themes. I needed a way to switch between these two sets of colors at runtime when VS theme changes.

    After some search I found a solution in the F# github in the code responsible for the integration with VS: https://github.com/dotnet/fsharp/blob/main/vsintegration/src/FSharp.Editor/Classification/ClassificationDefinitions.fs#L121

    The code in F# repo is quite similar to the code from Omer Raviv’s answer. I translated it into C# and get something like this:

    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;
        }
    }
    

    I’ve used the class above as the base class for all my ClassificationFormatDefinition classes.

    EDIT: After upgrade to AsyncPackage for newer versions of VS the previous code stopped working. You need to declare MEF imports somewhere else, for example, directly in the inheritor of ClassificationFormatDefinition. Moreover, as was pointed out by @Alessandro there is a subtle bug in the code. If you switch the VS theme and then immediately go to the VS settings "Fonts and colors" section you will see that the default colors values didn't change. They will change after the restart of VS but that's still not ideal. Fortunately, there is a solution (thanks again @Alessandro). You need to call IVsFontAndColorCacheManager's either ClearCache or RefreshCache with a correct guid 75A05685-00A8-4DED-BAE5-E7A50BFA929A which corresponds to MefItems category in Fonts and Colors cache in the registry. Here is a reference to an article which describes this a bit: https://docs.microsoft.com/en-us/dotnet/api/microsoft.visualstudio.shell.interop.ivsfontandcolorcachemanager?view=visualstudiosdk-2019

    Unfortunately I can't find any documentation for the guid constant.

    UPDATE: After some more research, debugging and adding logging of bad error codes to VS Activity Log I found out the following:

    1. Theme changed handler is called multiple times for a single change of VS theme
    2. ClearCache returns 0 for the first few calls but after that starts returning bad error codes
    3. RefreshCache always return 0 (at least in my case)

    Therefore I replaced the call to ClearCache with the call to RefreshCache.
    So, here is an updated example:

    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;
        }
    }
    

    You can determine if you need to use a color suited for the light or dark theme by checking the current background of the code editor. Here is the link to the code I use: https://github.com/Acumatica/Acuminator/blob/dev/src/Acuminator/Acuminator.Vsix/Coloriser/Constants/VSColors.cs#L82

    And here is a more concise snippet from @Alessandro (thanks again!):

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

    You may also create a separate shared ThemeUpdater class which will subscribe to the ThemeChanged event and all ClassificationFormatDefinition derived classes will subscribe to it to make their specific changes on the theme change. This has a performance benefit that you can update all format definitions in a batch and call EndBatchUpdate and RefreshCache/ClearCache only once on theme change.

    0 讨论(0)
  • 2021-01-04 16:53

    This might help you, code from F# Power Tools, seems to be listening to the ThemeChanged event and updating the classifiers - https://github.com/fsprojects/VisualFSharpPowerTools/blob/a7d7aa9dd3d2a90f21c6947867ac7d7163b9f99a/src/FSharpVSPowerTools/SyntaxConstructClassifierProvider.cs

    0 讨论(0)
提交回复
热议问题