воскресенье, 2 сентября 2012 г.

DXCore и автоматизация Visual Studio

Итак, стала доступна Visual Studio 2012. Казалось бы впору радоваться, однако что-то радоваться совсем не хочется. Установив новую версию, я, естественно, в первую очередь полез посмотреть на любимые фичи, ну там изучить новые возможности и все такое. Учитывая, что возможности редактора кода в студии не безграничны(мягко говоря) и зачастую необходимо их расширить, то в связи с этим одной из моих любимых возможностей студии была возможность писать макросы. На самом деле инструмент просто незаменимый: большинство отсутствующих возможностей студии можно добавить, просто слегка отвлекшись на написание макроса. Написал и он тут же доступен, запускай и пользуйся. Можно, конечно писать надстройки, но вся прелесть макросов в том, что они доступны сразу после написания, да и в отличие от надстройки, макро-проект не нужно продумывать заранее, а достаточно писать макросы по мере возникновения необходимости в той или иной функции студии. Когда скопится достаточно, макро-проект можно и в надстройку преобразовать, благо со студией поставлялись примеры макросов, среди который есть и такой, который выполняет преобразование макро-проекта в проект надстройки. Сделать это совсем не сложно, учитывая, что и макросы и надстройки используют одну и ту же объектную модель автоматизации.

Расстроившись по поводу нововведения, я решил поискать решение среди расширения для студии. Макросы, конечно же, заменить трудно, но можно расширить возможности студии. Из инструментов, действительно заслуживающих внимания помимо всем хорошо известного Resharper’а, можно еще выделить Visual Assist X и CodeRush. Последний привлек мое внимание наличием бесплатной версии, поэтому с нее я и начал.

CodeRush Xpress включает в себя множество возможностей, помогающих удобно писать код. Помимо описания в документации на официальном сайте можно почитать еще здесь о возможностях этого плагина для студии. Кое-что из описанного не будет работать, например из-за версии, сейчас изменились клавиши быстрого доступа и кое-какие другие вещи. клавиши быстрого доступа есть в документации и в окне опций, но вот с ними-то как раз в бесплатной версии и проблема. При загрузке студии, когда интерфейс еще не доступен, можно увидеть меню “DevExpress”, которое исчезает во время загрузки. В окне настройки меню и это меню тоже присутствует, но активировать его не получится. Связана такая скрытность с какими-то лицензионными ограничениям, одним словом : майкрософт потребовала от DevExpress не включать меню  в бесплатную версии по крайней мере по умолчанию. Требование странное, но тем не менее это так и делается. Решение проблемы описано здесь. Версия платформы, для которой это решение описано устарела, но метод с реестром работает, то есть надо просто внести в нужную ветвь параметр. В той версии, которая установлена у меня ветвь называется
HKEY_LOCAL_MACHINE\SOFTWARE\Developer Express\CodeRush for VS\11.2
Последняя ветвь названа по номеру версии, так что в дальнейшем тоже будет меняться. Ну, а параметр, который надо установить такой же
"HideMenu"=dword:00000000.
После этого, если студия запущена, то ее надо перезапустить для того, чтобы изменения вступили в силу.

Помимо возможностей самого CodeRush Xpress меня заинтересовало другое. Данный продукт выполнен на ядре DXCore и производитель любезно предоставляет API для написания собственных плагинов, которые можно встраивать в ядро и пользоваться всеми его возможностями. Краткое описание, что есть DXCore, можно найти здесь.

Создание плагина.

Не смотря на то, что кое-какую документацию, статьи и записи в блогах по этому вопросу найти можно, тем не менее, документация крайне скудна. Вот например сайт, посвященный документации по DXCore
https://sites.google.com/site/dxcoredocs/contents
А вот описание работы с RefactoringProvider
https://sites.google.com/site/dxcoredocs/plugin-components/Creating-a-RefactoringProvider
Конечно, понять как начать работать можно и по этому документу, но, судя по всему, придется до всего доходить методом “научного тыка”.

Здесь я опишу пример создания плагина, добавляющий в оператор Select (switch в C#) Case’ы для каждого элемента какого-нибудь перечисления. Штуковина востребованная, да и в полной версии CodeRush она вроде бы есть.

Вначале все так, как описано по последней ссылке:
Создаем проект( в диалоге выбираем в ветке DXCore проект плагина).
На поверхность дизайнера бросаем RefactoringProvider, и устанавливаем значения для необходимого минимума свойств

  • Name: SwitchEnumExpander
  • DisplayName: Case для перечисления(это текст, который будет отображаться как пункт меню)
  • ProviderName: SwitchEnumExpander

Кроме того, будет совсем не лишним добавить описание того, что делает  эта функция. Для этого надо установить значение для свойства Description, в моем случае это был следующий текст.

Если оператор switch (Select в Visual Basic) проверяет значение выражения, возвращающего перечисляемый тип (Enum), команда добавляет в него операторы case для всех значений перечисления.

Он будет отображаться, когда соответствующее меню выделено.

Далее вставляем в код обработчики событий CheckAvailability и Apply. В обработчике первого события мы проверяем, доступен ли наш рефакторинг в данном контексте, а второй будет выполняться в том случае, если пользователь выберет соответствующий пункт меню. То есть основное действие выполняется именно в обработчике события Apply нашего провайдера.

Теперь о коде. Для начала объявим три поля для хранения объектов, которые нам помогут в построении кода.

VB.Net
  1. Dim _codeModServices As New CodeModServices
  2. Dim _sourceTreeResolver As New SourceTreeResolver()
  3. Dim _elementBuilder As New ElementBuilder

Результирующий код мы будем генерировать из дерева, которое построим. Можно было сразу сгенерировать текст, но в этом случае все это работало бы только для одного языка, а вот при генерации кода из дерева функция будет доступна для всех языков, при том, что запрограммировать ее придется только один раз.

Первый объект CodeModeServices нам понадобится именно для генерации кода.

SourceTreeResolver нужен для получения информации о типах. Когда мы найдем имя типа возвращаемого выражением , значение которого проверяет наш оператор Select, то по имени нам надо будет получить информацию о типе. Но тут есть трудности, в проекте могут быть определены собственные типы, кроме того, он может ссылаться как на другие проекты, так и на готовые сборки. В текущем файле могут быть импортированы пространства имен, кроме того, они обычно импортируются и на уровне проекта( в бейсике). Таким образом, среди всего этого многообразия нам потребуется отыскать нужный тип и проверить, является ли он перечислением, а если да – получить имена определенных в нем  констант. Для решения этих вопросов нам и понадобится экземпляр класса SourceTreeResolver.

ElementBuilder помогает строить дерево кода.

Для начала определим функцию, которая будет находить информацию о типе перечисления, если таковой имеется и доступен в текущем контексте.

VB.Net
  1. Function FindEnumForSwitch(sw As Switch) As IElement
  2.     Dim typename = sw.Expression.GetDeclaration.ToLanguageElement.GetTypeName
  3.     Dim typeel = _sourceTreeResolver.ResolveTypes(sw, typename, True)
  4.     Return Aggregate te As IElement In typeel Where te.ElementType = LanguageElementType.Enum Into FirstOrDefault()
  5. End Function

Когда какой-нибудь элемент Select будет доступен, мы будем передавать его этой функции, для того, чтобы определить, возвращает ли проверяемое им выражение объект перечисляемого типа (если нет – функция будет возвращать Nothing) и если да, то мы получим сведения об этом перечислении.

Далее, в обработчике события CheckAvailability нам нужно дать ответ на вопрос о доступности нашего рефакторинга в данном контексте. Доступным он будет при соблюдении двух условий: элементом контекста является оператор Select и проверяемое им выражение возвращает значение перечисляемого типа(последнее обстоятельство проверим с помощью только что написанной функции FindEnumFromSwitch).

VB.Net
  1. Private Sub SwitchEnumExpander_CheckAvailability(sender As System.Object, ea As DevExpress.CodeRush.Core.CheckContentAvailabilityEventArgs) Handles SwitchEnumExpander.CheckAvailability
  2.     If ea.Element.ElementType = LanguageElementType.Switch Then
  3.         ea.Available = FindEnumForSwitch(ea.Element) IsNot Nothing
  4.     End If
  5. End Sub

Теперь добавим в Select Case’ы. Для этого создадим отдельную функцию, причем она будет получать эелемент Select взятый из кода, а возвращать его копию, заполненную кейсами. Можно было бы добавлять кейсы непосредственно в этот же селект, но мы не будем этого делать, поскольку эту функцию мы будем использовать и для показа превью, а поскольку окно превью может появляться неоднократно – лучше все-таки исходный селект не трогать.

VB.Net
  1. Function CreateSwitchWithEnumCases(switch As Switch) As IElement
  2.     Dim langelcoll As New LanguageElementCollection
  3.  
  4.     Dim enumType = FindEnumForSwitch(switch)
  5.     Dim casestmt = Function(ie As IElement)
  6.                        Dim cs = _elementBuilder.BuildCase(_elementBuilder.BuildMemberAccessReference(enumType.FullName,
  7.                                                                           ie.Name, MemberAccesOperatorType.Default))
  8.                        cs.AddNode(_elementBuilder.BuildBreak)
  9.                        Return cs
  10.                    End Function
  11.  
  12.     langelcoll.AddRange((From ie As IElement In FindEnumForSwitch(switch).Children
  13.                         Where ie.ElementType = LanguageElementType.EnumElement
  14.                         Select casestmt(ie)).ToList)
  15.     Dim result As Switch = switch.Clone
  16.     result.RemoveNodesFromIndex(0)
  17.     result.AddCaseStatements(langelcoll)
  18.     Return result
  19. End Function

Здесь при генерации кейса мы добавляем оператор break, в бейсике это не нужно, но в других языках(а плагин будет работать не только в бейсике) это необходимо.

Теперь когда у нас появилась функция, генерирующая результат можно реализовать окошко preview, для того, чтобы пользователь мог видеть какой код будет сгенерирован еще до того, как примет изменения. Для этого нам потребуется обработать событие PreparePreview нашего провайдера. Код довольно прост.

VB.Net
  1. Private Sub SwitchEnumExpander_PreparePreview(sender As System.Object, ea As DevExpress.CodeRush.Core.PrepareContentPreviewEventArgs) Handles SwitchEnumExpander.PreparePreview
  2.     ea.AddCodePreview(ea.Element.View.GetSelectionRange.Start, _codeModServices.GenerateCode(CreateSwitchWithEnumCases(ea.Element)))
  3. End Sub

Ну и все что осталось – это только вставить код в том случае, если пользователь выбрал соотвествующий пункт меню. Для этого нужно обработать событие Apply провайдера.

VB.Net
  1. Private Sub SwitchEnumExpander_Apply(sender As System.Object, ea As DevExpress.CodeRush.Core.ApplyContentEventArgs) Handles SwitchEnumExpander.Apply
  2.     Dim sw = CType(ea.Element, Switch)
  3.     Dim typename = sw.Expression.GetDeclaration.ToLanguageElement.GetTypeName
  4.     Dim typeel = _sourceTreeResolver.ResolveTypes(sw, typename, True)
  5.  
  6.     sw.SelectCode()
  7.     ea.Selection.Text = _codeModServices.GenerateCode(CreateSwitchWithEnumCases(sw))
  8.     ea.Selection.Clear()
  9. End Sub

Здесь для вставки кода я воспользовался сгенерированным текстом, выделил блок Select и заменил выделенный текст новым, после чего свернул выделение. Скорей всего это не самый оптимальный способ замены кода, но как это делать правильно, я пока не знаю, а этот способ вполне себе сносно работает.

В заключение приведу полный код плагина.

VB.Net
  1. Imports System.ComponentModel
  2. Imports System.Drawing
  3. Imports System.Windows.Forms
  4. Imports DevExpress.CodeRush.Core
  5. Imports DevExpress.CodeRush.PlugInCore
  6. Imports DevExpress.CodeRush.StructuralParser
  7. Imports System.Linq
  8.  
  9. Public Class PlugIn1
  10.  
  11.     'DXCore-generated code...
  12. #Region " InitializePlugIn "
  13.     Public Overrides Sub InitializePlugIn()
  14.         MyBase.InitializePlugIn()
  15.  
  16.         'TODO: Add your initialization code here.
  17.     End Sub
  18. #End Region
  19. #Region " FinalizePlugIn "
  20.     Public Overrides Sub FinalizePlugIn()
  21.         'TODO: Add your finalization code here.
  22.  
  23.         MyBase.FinalizePlugIn()
  24.     End Sub
  25. #End Region
  26.  
  27.     Dim _codeModServices As New CodeModServices
  28.     Dim _sourceTreeResolver As New SourceTreeResolver()
  29.     Dim _elementBuilder As New ElementBuilder
  30.  
  31.     Private Sub SwitchEnumExpander_Apply(sender As System.Object, ea As DevExpress.CodeRush.Core.ApplyContentEventArgs) Handles SwitchEnumExpander.Apply
  32.         Dim sw = CType(ea.Element, Switch)
  33.         Dim typename = sw.Expression.GetDeclaration.ToLanguageElement.GetTypeName
  34.         Dim typeel = _sourceTreeResolver.ResolveTypes(sw, typename, True)
  35.  
  36.         sw.SelectCode()
  37.         ea.Selection.Text = _codeModServices.GenerateCode(CreateSwitchWithEnumCases(sw))
  38.         ea.Selection.Clear()
  39.     End Sub
  40.  
  41.  
  42.     Function CreateSwitchWithEnumCases(switch As Switch) As IElement
  43.         Dim langelcoll As New LanguageElementCollection
  44.  
  45.         Dim enumType = FindEnumForSwitch(switch)
  46.         Dim casestmt = Function(ie As IElement)
  47.                            Dim cs = _elementBuilder.BuildCase(_elementBuilder.BuildMemberAccessReference(enumType.FullName,
  48.                                                                               ie.Name, MemberAccesOperatorType.Default))
  49.                            cs.AddNode(_elementBuilder.BuildBreak)
  50.                            Return cs
  51.                        End Function
  52.  
  53.         langelcoll.AddRange((From ie As IElement In FindEnumForSwitch(switch).Children
  54.                             Where ie.ElementType = LanguageElementType.EnumElement
  55.                             Select casestmt(ie)).ToList)
  56.         Dim result As Switch = switch.Clone
  57.         result.RemoveNodesFromIndex(0)
  58.         result.AddCaseStatements(langelcoll)
  59.         Return result
  60.     End Function
  61.  
  62.     Private Sub SwitchEnumExpander_CheckAvailability(sender As System.Object, ea As DevExpress.CodeRush.Core.CheckContentAvailabilityEventArgs) Handles SwitchEnumExpander.CheckAvailability
  63.         If ea.Element.ElementType = LanguageElementType.Switch Then
  64.             ea.Available = FindEnumForSwitch(ea.Element) IsNot Nothing
  65.         End If
  66.     End Sub
  67.  
  68.     Function FindEnumForSwitch(sw As Switch) As IElement
  69.         Dim typename = sw.Expression.GetDeclaration.ToLanguageElement.GetTypeName
  70.         Dim typeel = _sourceTreeResolver.ResolveTypes(sw, typename, True)
  71.         Return Aggregate te As IElement In typeel Where te.ElementType = LanguageElementType.Enum Into FirstOrDefault()
  72.     End Function
  73.  
  74.     Private Sub SwitchEnumExpander_PreparePreview(sender As System.Object, ea As DevExpress.CodeRush.Core.PrepareContentPreviewEventArgs) Handles SwitchEnumExpander.PreparePreview
  75.         ea.AddCodePreview(ea.Element.View.GetSelectionRange.Start, _codeModServices.GenerateCode(CreateSwitchWithEnumCases(ea.Element)))
  76.     End Sub
  77.  
  78. End Class

Для запуска можно просто нажать F5, запустится новый экземпляр Visual Studio, в котором новая функция будет доступна. А вообще DXCore устанавливает плагины в специальную папку и если они там есть, то при запуске студии они подключаются автоматически, то есть никакая установка(как со стандартными надстройками студии) не требуется.