Nie poruszam podstaw tworzenia rozszerzeń zaś opisuje jedynie w jaki sposób wykorzystać dostępne mechanizmy do pobrania informacji która nas interesuje. Wszystkich zainteresowanych pisaniem rozszerzeń pod VS 2010 zapraszam na strony MSDN, które naprawdę dobrze tłumaczą o co w tym wszystkim biega.

Nowy system rozszerzeń VS 2010 jest naprawdę przyjemny nie tylko pod względem możliwości takich jak chociażby zmiany sposobu wyświetlania komentarzy w kodzie ale także pod względem jego oprogramowania. Wszystko opiera się na MEF, więc jeżeli coś potrzebujemy to mówimy Import i to mamy. I do póki chcemy stworzyć proste rozszerzenia jesteśmy wstanie trzymać się tego co udostępnia nam najnowsze SDK. Jeżeli jednak w ciągu pierwszych kilkunastu minut czegoś nie znajdziemy oznacza to, że jesteśmy w głębokiej jaskini platońskiej. Problem polega na tym iż zaczynamy mieszać dostęp do obiektów COM z pięknym i zgrabnym .NETowym API. Z funkcji na funkcje musimy zmieniać sposób myślenia – dobra, wykonałem akcje X, teraz muszę sprawdzić czy operacja się udała poprzez porównanie rezultatu do wartości NON_ZERO.

Na taki właśnie problem natrafiłem podczas pisania jednego z rozszerzeń. To co chciałem osiągnąć to w elegancki sposób z wykorzystaniem nowego API pobrać informacje o czcionce i kolorze czcionki ustawionej w opcjach VS 2010. Po 2h prób i błędów okazało się, że niestety tego akurat feature w nowym VS z nowym API nie da się osiągnąć. Trzeba odwołać się do interfejsów operujących na COM, które zaś w dokumentacji za dużo nie mówią poza tym iż wartość zwracana to mieszanka 2-3 różnym enumeratorów – jak za pomocą takiego wyniku stworzyć czcionkę?

Przypadkiem szukając odpowiedzi na powyższe i inne pytania natrafiłem na forum MSDN w którym osoba dzieli się swoim fragmentem kodu do pobrania koloru czcionki. Wciąż jednak miałem problem z pobraniem czcionki – jednak głównie on leżał w mojej głupocie, gdyż sądziłem iż dana grupa ustawień fontów i kolorów w VS może mieć więcej niż jeden font, a metody takiej która by mi pozwalała zwrócić czcionkę w zależności od wartości – na przykład Text Editor > Comment nie znalazłem a próby podania w jakiś sposób nazwy kończyły się fiaskiem – więc zamiast sprawdzić co da się ustawić a co nie kombinowałem jak koń pod górę :) No ale w końcu się udało :)

A więc do rzeczy, by móc pobrać czcionkę i jej kolor z ustawień, do naszego projektu należy dodać następujące 4 referencje:

  • Microsoft.VisualStudio.Shell – zawiera klasę VSConstants, posiadającą ogólnodostępne, zdefiniowane wartości jak chociażby wartość S_OK, która mówi czy operacja się powiodła;
  • Microsoft.VisualStudio.Shell.Immutable.10.0 – posiada interfejs SVsServiceProvider umożliwiający nam pobierania usług COM;
  • Microsoft.VisualStudio.Shell.Interop – zawiera interfejs IVsFontAndColorStorage umożliwiający nam odwołanie się do ustawień;
  • System.Drawing – zawiera klasę Font umożliwiającą nam konwersje z dziwnych numerków na odpowiednią czcionkę.

Zanim jednak przejdziemy do kodu, kilka informacji, o których warto wiedzieć. Konfiguracja fontów i czcionek przechowywana jest w rejestrze pod kluczami (ludzie piszą iż powinny ustawienia być pod HKLM jednak u mnie ich tam napewno nie było):

  • HKCU/Software/Microsoft/VisualStudio/10.0/FontsAndColors/GUID
  • HKCU/Software/Microsoft/VisualStudio/10.0/FontsAndColors/Cache/GUID

Gdzie GUID określa daną kategorię ustawień. Nazwa kategorii to wartości z combobox Show settings for w oknie Tools -> Options -> Fonts and Colors.

vss_ex_fc_002

Zaś GUID danej kategorii to jej unikatowy identyfikator, niestety stworzenie pary z Nazwy i GUIDa proste nie jest i wymaga regedita. Dzięki Show settings for wiemy jak się nazywa nasza kategoria, więc następnie w odpowiedniej ścieżce rejestru przeglądamy wszystkie klucze w poszukiwaniu wartości string CategoryName, która odpowiada nazwie z combobox.

vss_ex_fc_003

Na szczęście mamy jeszcze drugą opcję możliwą do wykonania. Jeżeli wyeksportujemy ustawienia kolorów z VS to w pliku vssettings powinniśmy znaleźć element Category którego atrybut @Name posiada wartość Environment_FontsAndColors:

vss_ex_fc_004

Element ten zawiera dzieci Category, które posiadają atrybut @GUID – atrybut ten odpowiada naszemu poszukiwanemu GUIDowi, zaś atrybut @Name od elementu Item określa ustawienia dla danego Display Item (bo wyjdzie masło maślane jak napiszę: dla elementu danej kategorii) z okna Fonts and Colors:

vss_ex_fc_005

GUID kategorii jak i nazwa elementu Display Item są dwoma wymaganymi parametrami jeżeli chcemy pobrać wartość i muszą być znane odgórnie – nie ma lub ja nie znalazłem sposobu, pobrania ich za pomocą VS SDK, te parametry musimy znać i przeważnie ręcznie je wprowadzać, lub za pomocą jakiejś konfiguracji. Na szczęście GUID nie ulega zmianie przy kolejnych wersjach VS (przeważnie!, są takie które uległy zmianie i można je zobaczyć na przykład w pliku vsshelluuids.h), jednak nazwa elementu może nie tylko ulec zmianie jak i może całkowicie zniknąć w następnych wersjach – choć w to wątpię.

vss_ex_fc_006

Tak przygotowani, możemy zaczynać zabawę. Pierwszym krokiem po tym jak dodaliśmy referencje i zebraliśmy nasze GUIDy i nazwy jest pobranie interfejsu, umożliwiającego nam pobieranie usług COM, w tym celu w klasie, która jest exportowana (bierze udział w kompozycji, jest to przeważnie XxxNameFactory jeżeli tworzymy projekty z szablonu VS) w VS Extension, dodajemy import:

// if we are creating new extensions that use MEF - not VSPackage
// we can import SVsServiceProvider, null is just given to
// distinguish  property from field.
[Import]
private SVsServiceProvider _svsServiceProvider = null;

Teraz możemy za pomocą metody GetService pobrać usługę która nas interesuje:

var fontAndColorStorage = _svsServiceProvider.GetService(
                                typeof(IVsFontAndColorStorage)
                            ) as IVsFontAndColorStorage;

Następnie musimy otworzyć storage dla danej kategorii – to dzięki niemu będziemy mieć dostęp do ustawień:

int openResult = fontAndColorStorage.OpenCategory(ref textEditorGuid, (uint)
                                                    (__FCSTORAGEFLAGS.FCSF_LOADDEFAULTS |
                                                    __FCSTORAGEFLAGS.FCSF_NOAUTOCOLORS));

if(openResult != VSConstants.S_OK) return null; // or throw exception

Teraz już pozostaje nam tylko i wyłącznie wywoływanie metod GetItem i GetFont z odpowiednimi PUSTYMI (przeważnie deklaracja nowej tablicy o rozmiarze 1) parametrami w celu pobrania wartości, które nas interesują. Po każdej operacji jednak musimy sprawdzić czy ona się udała, jeżeli nie to otwarty storage należy zamknąć:

// we will be calling this few times in code, therefore
// local function
Func<FontAndColorInfo, FontAndColorInfo> closeCategory = (fontAndColor) =>
{
    int closeResult = fontAndColorStorage.CloseCategory();

    if(closeResult == VSConstants.S_OK)
    {
        return fontAndColor;
    }

    throw new InvalidOperationException();
};

A tak wygląda cała funkcja, która przyjmując GUID kategorii i nazwę elementu, zwróci nam NULL lub obiekt z informacją o czcionce i jej kolorze:

// if we are creating new extensions that use MEF - not VSPackage
// we can import SVsServiceProvider, null is just given to
// distinguish  property from field.
[Import]
private SVsServiceProvider _svsServiceProvider = null;

public void DummyCall()
{
    // text editor GUID taken from: 
    // HKCU/Software/Microsoft/VisualStudio/10.0/FontsAndColors/GUID
    // OR
    // HKCU/Software/Microsoft/VisualStudio/10.0/FontsAndColors/Cache/GUID
    // OR
    // .vssettings file in section: Environment_FontsAndColors
    Guid textEditorGuid = Guid.Parse("{A27B4E24-A735-4D1D-B8E7-9716E1E3D8E0}");
    try
    {
        var result = GetFontAndColorInfo("Line Numbers", textEditorGuid);
        if(result == null)
        {
            // load defaults
        }
    }
    catch(Exception ex)
    {
        // handle, try by more specific than Exception
    }
}

/// <remarks>
/// Colors solution taken and changed from: 
/// <see cref="http://social.msdn.microsoft.com/Forums/en-US/vsx/thread/17da3d9c-7e5d-413f-b9c6-eeedc1ec991f/"/>
/// </remarks>
public FontAndColorInfo GetFontAndColorInfo(string textEditorItemName, Guid textEditorGuid)
{
    var fontAndColorStorage = _svsServiceProvider.GetService(
                                    typeof(IVsFontAndColorStorage)
                                ) as IVsFontAndColorStorage;

    if(fontAndColorStorage == null) return null; // or throw exception

    // we will be calling this few times in code, therefore
    // local function
    Func<FontAndColorInfo, FontAndColorInfo> closeCategory = (fontAndColor) =>
    {
        int closeResult = fontAndColorStorage.CloseCategory();

        if(closeResult == VSConstants.S_OK)
        {
            return fontAndColor;
        }

        throw new InvalidOperationException();
    };

    int openResult = fontAndColorStorage.OpenCategory(ref textEditorGuid, (uint)
                                                        (__FCSTORAGEFLAGS.FCSF_LOADDEFAULTS |
                                                        __FCSTORAGEFLAGS.FCSF_NOAUTOCOLORS));

    if(openResult != VSConstants.S_OK) return null; // or throw exception

    var colorableItemInfo = new ColorableItemInfo[1];
    int getItemResult = fontAndColorStorage.GetItem(textEditorItemName, colorableItemInfo);

    if(getItemResult != VSConstants.S_OK)
    {
        return closeCategory(null);
    }

    // Valid - NON_ZERO value
    if(colorableItemInfo[0].bForegroundValid == 0)
    {
        return closeCategory(null);
    }

    var lineNumbersForeground = ConvertFromWin32Color((int)colorableItemInfo[0].crForeground);

    // there is only one font and font size per category
    // you can check that by playing with Tools -> Options -> Fonts And Colors
    var fontInfos = new FontInfo[1];
    var logFontW = new LOGFONTW[1];
    int getFontResult = fontAndColorStorage.GetFont(logFontW, fontInfos);

    if(getFontResult != VSConstants.S_OK)
    {
        return closeCategory(null);
    }

    // Font - System.Drawning.Font
    // this can throw exception
    var lineNumberFont = Font.FromLogFont(logFontW[0]);
    // we can do this in this way too:
    //var lineNumberFont = new Font(fontInfos[0].bstrFaceName, (float)fontInfos[0].wPointSize);

    return closeCategory(new FontAndColorInfo
    {
        Foreground = lineNumbersForeground,
        TypeFace = new FontFamily(lineNumberFont.FontFamily.Name),
        FontSizeInPoints = lineNumberFont.SizeInPoints,
        FontSize = lineNumberFont.Size
    });
}

/// <see cref="http://www.alteridem.net/2007/08/23/win32-colorref-vs-net-color/"/>
public static Color ConvertFromWin32Color(int color)
{
    int r = color & 0x000000FF;
    int g = (color & 0x0000FF00) >> 8;
    int b = (color & 0x00FF0000) >> 16;
    return Color.FromArgb(255, (byte)r, (byte)g, (byte)b);
}

public class FontAndColorInfo
{
    // System.Windows.Media.Color
    public Color Foreground { get; set; }
    // System.Windows.Media.FontFamily
    public FontFamily TypeFace { get; set; }

    public float FontSize { get; set; }
    public float FontSizeInPoints { get; set; }
}

Poniżej zaś rezultat po połączeniu z rozszerzeniem nad którym pracuje:

Colors

Mówiąc szczerze, po tym jak się trochę pobawiłem rozszerzeniami dla VS, naprawdę zacząłem doceniać to co chłopaki z JetBrains i DevExpress robią. To wcale nie jest prosta sprawa operować na kodzie, UI, fontach itp. itd. To z początku może wydawać się proste, jednak im człowiek bardziej się zagłębi w bebechy tym bardziej widzi, że aby coś zmienić musi się naprawdę nieźle napracować a i także dobrze przemyśleć sprawę bo tutaj łatwo o pomyłkę, a zawieszający się VS jest jedną z najgorszych rzeczy jakie można załatwić użytkownikowi końcowemu dostarczając mu wadliwe oprogramowanie.

Tak czy siak, ja tam się cieszę bo czegoś się nauczyłem, zobaczymy czy będę wstanie to wykorzystać :)