Jak przez brak Intellisense w Xml napisałem własny plugin do Resharpera

31.05.2011

Ostatnio, podczas pracy z Unity 2.0 postanowiłem zaszaleć. Zamiast pisać klasę Bootstrapper’a i mapować implementacje na interfejsy w kodzie programu, zapragnąłem skonfigurować Unity przy pomocy pliku App.config. Dla niezorientowanych. Unity to kontener IoC od Microsoftu dostarczany razem z Enterprise Library. Proces konfiguracji Unity jest w miarę prosty. Ogranicza się do zdefiniowania aliasów, czyli skróconych nazw interfejsów, do których będą mapowane konkretne implementacje. Przykład:

<alias alias="IDoor" type="Door.Model.Controller.IDoor, Door.Model" />

Drugim i ostatnim krokiem jest przypisanie (mapowanie) implementacji do wcześniej stworzonego aliasu. Przykład:

<register type="IDoor" mapTo="Door.Model.Controller.Door, Door.Model"></register>

Pojawił się jednak "drobny" problem. Visual Studio + Resharper nie chciały pomóc mi przy wprowadzaniu pełnych nazw typów w pliku App.config. Jak się później okazało był to jakiś błąd w mojej instalacji VS/R#. W normalnych warunkach Visual Studio z Resharperem po wpisaniu atrybutu type w pliku Xml powinno wyświetlić okno Intellisense. Niestety, z jakiegoś nie znanego mi do dziś powodu, intellisense w plikach Xml przestało mi działać. Zawisło nade mną widmo ręcznego wpisywania czegoś takiego DoorController.Agent.Services.Synchronization. ISynchronization. Niefajnie. Na szczęście pod ręką miałem Live Templates nie straciłem więc całkowicie nadzieji. Patrząc na przykładowy wpis dla Unity stworzyłem dwa szablony:

<alias alias="$ALIAS$" type="$TYPE$, $MODULE$"/>$END$

Oraz:

<register type="$ALIAS$" mapTo="$TYPE$, $MODULE$"></register>

Gdzie:

ALIAS – jak sama nazwa wskazuje alias
TYPE – pełna nazwa typu Foor.IBar
MODULE – nazwa assembly, w którym znajduje się TYPE

Po przestudiowaniu listy dostępnych makr w R# byłem zawiedziony. Potrzebowałem dwóch makr. Jednego do wyświetlenia dialogu Intellisense z listą dostępnych typów oraz drugiego, do znajdowania nazwy assembly (modułu) w którym znajduje się dany typ. Postanowiłem sam napisać te dwa makra. Nigdy jednak nie pisałem dodatków do Resharpera więc czekało mnie trochę nauki. Naturalnym wyborem wydawała się strona JetBrains. Jednak po jej przeszukaniu doszedłem do wniosku, że wbrew pozorom nie jest to odpowiednie miejsce do szukania podstawowych informacji o tym w jaki sposób tworzy się dodatki do Resharpera. Dokumentacja z prawdziwego zdarzenia nie istnieje. Jest jeszcze forum, ale nie jest to miejsce gdzie można nauczyć się przysłowiowego „Hello world”. Na szczęście isteniej Google. Po krótkich poszukiwaniach trafiłem pod dwa ciekawe adresy – na blog Gutka i Hadi Haririego. Informacji nie za dużo, ale na początek musiało mi wystarczyć. Reszty dowiedziałem się dzięki Reflectorowi. Najłatwiejsze okazało się stworzenie makra do wyświetlania dialogu Intellisense i wstawiania pełnej nazwy typu. Na liście dostępnych makr Resharpera znalazłem te oznaczone "Execute basic completion" i "Execute SmartType completion", które odpowiadają obiektom BasicCompletionMacro i SmartCompletionMacro z pliku JetBrains.ReSharper.Feature.Services.dll. Okazało się, że wystarczy odziedziczyć BaseCompletionMacro i zmienić właściwość CompletionType na TypeNameCompletion żeby wyświetlić okno intellisense i po dokonaniu wyboru wstawić w tym miejscu pełną nazwę wybranego typu. Ciekawe dlaczego JetBrains sam nie zrobił makra wykorzystującego TypeNameCompletion? Kod:

 

using JetBrains.ReSharper.Feature.Services.CodeCompletion;
using JetBrains.ReSharper.Feature.Services.LiveTemplates.Macros;

namespace jdubrownik.Macros
{
    [Macro("jdubrownik.Macros.TypeCompletionMacro",
        ShortDescription = "Execute type completion",
        LongDescription = "Shows type auto completion form.")]
    public class TypeCompletionMacro : BaseCompletionMacro
    {
        protected override CodeCompletionType CompletionType
        {
            get { return CodeCompletionType.TypeNameCompletion; }
        }
    }
}

Trochę trudniejsze okazało się stworzenie makra, które będzie znajdować nazwę pliku, w którym znajduje się dany typ. Podobnie jak w przypadku pierwszego makra, znalazłem sobie coś podobnego pośród już istniejących makr Resharpera – ContextMacro. Po przeanalizowaniu jego kodu i kilku próbach doszedłem do czegoś takiego:

namespace jdubrownik.Macros
{
    [Macro("jdubrownik.Macros.ModuleMacro",
        ShortDescription = "Gets module of {#0:another variable}",
        LongDescription = "Gets module of variable.")]
    public class ModuleMacro : IMacro
    {
        #region IMacro Members

        public string GetPlaceholder()
        {
            return "a";
        }

        public bool HandleExpansion(IHotspotContext context, IList<string> arguments)
        {
            return false;
        }

        public HotspotItems GetLookupItems(IHotspotContext context, IList<string> arguments)
        {
            return null;
        }

        public string EvaluateQuickResult(IHotspotContext context, IList<string> arguments)
        {
            if (arguments.Count != 1)
            {
                return null;
            }

            ProjectFileType lang = ProjectFileLanguageServiceManager.Instance.GetLanguageTypeByName("CSHARP");
            PsiLanguageType languageType = ProjectFileLanguageServiceManager.Instance.GetPrimaryPsiLanguageType(lang);
            IMacroUtil macroUtil = MacroUtil.GetMacroUtil(languageType);
            if (macroUtil == null)
            {
                return null;
            }
            IType type = macroUtil.AsType(arguments[0], context);

            return type.Module == null ? null : type.Module.DisplayName;
        }

        public ParameterInfo[] Parameters
        {
            get { return new[] {new ParameterInfo(ParameterType.VariableReference)}; }
        }

        #endregion
    }
}

Działa. Źródła wraz z przykładem makra oraz binarkami dostępne są do ściągnięcia z bitbucket.