В комментариях к одному из предыдущих постов я обещал рассказать про то, как писать расширения к Решарперу. Рассказать хочу потому, что сам периодически пишу расширения которые упрощают работу в моей конкретной области. Тут же я покажу вкратце мой подход к написанию расширений типа context action.
Итак, context action – это то появляющееся слева меню со стрелочкой, которое дает возможности «быстрой коррекции» в коде. Если хотите забайндить открытие этого меню, кстати, комманда называется
Расширения решарпера – это обычные библиотеки классов (DLL) которые кладутся в папку
Если ничего не писать, но запустить отладку по F5, ваш plug-in уже появится в списке плагинов решарпера. Конечно лучше добавить какой-нть контент, чем мы сейчас и займемся.
Первое что нужно сделать, это добавить ссылки на сборки решарпера, которые нам нужны. Я тупо добавляю ссылки на все сборки в которых фигурирует имя
Context action’ы делаются путем наследования от
Теперь, для создания CA нужно сделать две вещи:
Чтобы все заработало, нужно добавить в получившийся класс всего 4 метода:
Давайте возьмем простой пример. Представьте, что вам нужно реализовать фунционал который быстро инлайнит вызовы
Итак, попробуем поэтапно сделать реализацию этого мини-рефакторинга.
Для начала, делаем класс нашей СА, декорируем его небольшим набором метаданных. Поле
Каркас готов. Теперь нужно научиться определять, применим ли наш СА.
Наш СА применим только если мы сидим в теле
В конце всей цепочки, мы находим и проверяем значение показателя степени. Если оно целочисленно и между 1 и 10 – СА применим, возвращаем
Пример кода приведен ниже. Его лучше не читать а ходить по нему дебаггером. Это относится к работе с ReSharper целиком – лучший способ узнать больше про структуру синтаксического дерева – это дебаггер.
Видите как я перед возвратом
Если мы вернули
Ах, если бы все было так просто…
У пользователя появилась возможность использовать СА по назначению. Если он на нее кликнул в меню, вызывется метод
Нам снова нужен узел дерева который содержит
Получив выражение и целочисленную степень, мы используем
Во-первых, мы создаем объект типа
Вот и все. Все работает. Полный СА можно скачать тут. Напоминаю, что базовый класс
Я знаю что пример сложноват. Обход синтаксического дерева – это дело непростое. Я мучаюсь практически с каждым СА, который пишу. Дебаггер в этом плане очень помогает, конечно, но если будете писать сложные экшны, советую набросать простенькую DSL на том же F#, например, потому что поиск по дереву в C# выглядит неопрятно, со всеми этими приведениями типов, проверками на
Итак, context action – это то появляющееся слева меню со стрелочкой, которое дает возможности «быстрой коррекции» в коде. Если хотите забайндить открытие этого меню, кстати, комманда называется
ReSharper.QuickFix
. Я же пишу дополнительные опции для этого меню. Почему? Потому что это иногда экономит время. Давайте посмотрим на то, как писать context action для Решарпера.Расширения решарпера – это обычные библиотеки классов (DLL) которые кладутся в папку
Plugins
в папке bin
решарпера. Для отладки же, копировать их туда не нужно – можно попросту указать название и путь к плагину как аргументы вызова самой студии (devenv.exe
). Синтаксис примерно такой:devenv.exe /ReSharper.Plugin c:\path\to\your.dll
Если ничего не писать, но запустить отладку по F5, ваш plug-in уже появится в списке плагинов решарпера. Конечно лучше добавить какой-нть контент, чем мы сейчас и займемся.
Первое что нужно сделать, это добавить ссылки на сборки решарпера, которые нам нужны. Я тупо добавляю ссылки на все сборки в которых фигурирует имя
ReSharper
, т.к. понятия не имею что может понадобиться.Context action’ы делаются путем наследования от
CSharpContextActionBase
(в случае с C#), а также реализации нескольких других интерфейсов. К счастью, часть «сантехники» реализовали разработчики других плагинов. Я для context action’ов добавляю в свой проект класс ContextActionBase
, который написали авторы плагина Agent Johnson. Собственно сам файл можно найти тут.Теперь, для создания CA нужно сделать две вещи:
- Отнаследовать ваш CA от
ContextActionBase
- Декорировать получившийся класс аттрибутом ContextActionAttribute
Чтобы все заработало, нужно добавить в получившийся класс всего 4 метода:
- Конструктор по умолчанию. Тут делать в принципе ничего не надо.
- Метод
GetText()
. Этот метод возвращает в строке то, что будет написано для вашей комманды в выпадающем меню CA.
- Метод
IsAvailable(IElement)
. Определяет, применим ли ваш СА в данной точке кода или нет.IElement
– это ваша ссылка на ту точку кода, где курсор. От этой точки кода можно обойти хоть все дерево файла.
- Метод
Execute(IElement)
. Если пользователь кликнул на ваш СА, то можно его применять. У нас снова ссылка наIElement
, т.е. мы можем походить по коду и выбрать, что менять и где.
Давайте возьмем простой пример. Представьте, что вам нужно реализовать фунционал который быстро инлайнит вызовы
Math.Pow()
с целочисленными значениями. Это нужно потому, чтоMath.Pow(x, 2.0)
← это плохо и медленно
x*x
← намного быстрее
Итак, попробуем поэтапно сделать реализацию этого мини-рефакторинга.
Для начала, делаем класс нашей СА, декорируем его небольшим набором метаданных. Поле
text
добавлено для того, чтобы мы могли прямо в СА-меню подсказать пользователю, что произойдет с его кодом после рефакторинга.[ContextAction(Group = "C#", Name = "Inline a power function",
Description = "Inlines a power statement; e.g., changes Math.Pow(x, 3) to x*x*x.",
Priority = 15)]
internal class InlinePowerAction : ContextActionBase
{
private string text;
public InlinePowerAction(ICSharpContextActionDataProvider provider) : base(provider)
{
// тут пусто
}
⋮
}
Каркас готов. Теперь нужно научиться определять, применим ли наш СА.
Наш СА применим только если мы сидим в теле
Math.Pow()
и это тело имеет целочисленную степень – например 3.0. Как это сделать? Сначала мы находим то место, где у пользователя курсор. Потом, мы получаем те узлы синтаксического дерева, которые стоят там же где и курсор, и пытаемся привести их к ожидаемым типам. Поскольку Math.Pow()
– вызов функции, мы ожидаем увидеть IInvocationExpression
у которого в теле – Math.Pow
. И так далее, по цепочке, причем мы везде используем оператор as
на тот случай если в выражении не то, что мы ожидаем.В конце всей цепочки, мы находим и проверяем значение показателя степени. Если оно целочисленно и между 1 и 10 – СА применим, возвращаем
true
. Во всех других случаях возвращаем false
.Пример кода приведен ниже. Его лучше не читать а ходить по нему дебаггером. Это относится к работе с ReSharper целиком – лучший способ узнать больше про структуру синтаксического дерева – это дебаггер.
protected override bool IsAvailable(JetBrains.ReSharper.Psi.Tree.IElement element)
{
using (ReadLockCookie.Create())
{
IInvocationExpression invEx = GetSelectedElement<IInvocationExpression>(false);
if (invEx != null && invEx.InvokedExpression.GetText() == "Math.Pow")
{
IArgumentListNode node = invEx.ToTreeNode().ArgumentList;
if (node != null && node.Arguments.Count == 2)
{
ILiteralExpression value = node.Arguments[1].Value as ILiteralExpression;
if (value != null)
{
float n;
if (float.TryParse(value.GetText().Replace("f", string.Empty), out n) &&
(n - Math.Floor(n) == 0 && n >= 1 && n <= 10))
{
text = "Replace with " + (n-1) + " multiplications";
return true;
}
}
}
}
}
return false;
}
Видите как я перед возвратом
true
присваиваю значение переменной text
? Это для того чтобы СА лучше читался пользователем. Да, а что касается ReadLockCookie
в который обернут код – это элемент внутренней семантики Решарпера. Я понятия не имею что он делает – просто копирую его из примеров так, на всякий случай. Ведь детальной, обновленной докуменации по написанию плагинов для Решарпера пока нет.Если мы вернули
true
из IsAvailable()
, Решарпер захочет знать, какой текст нарисовать в меню. В данном случае мы уже знаем что возвращать – содержание переменной text
.protected override string GetText()
{
return text;
}
Ах, если бы все было так просто…
У пользователя появилась возможность использовать СА по назначению. Если он на нее кликнул в меню, вызывется метод
Execute()
. А вот тут как раз начинает работать наш алгоритм замены. Помните – мы хотим менять, скажем, Math.Pow(x, 3.0)
на x*x*x
. Как это сделать?Нам снова нужен узел дерева который содержит
Math.Pow()
. Мы вытаскиваем оба параметра (в примере выше – x
и 3), аккуратно конвертируя значения даже если написано, например, не 3.0 а 3.0f. Далее, мы определяем насколько длинное выражение стоит слева – ведь если мы возводим в степень x
, то можно написать x*x*x
, а вот если x+y
то придется писать со скобками (x+y)*(x+y)*(x+y)
. Для этого мы преряем тип, и если он ILiteralExpression
или IReferenceExpression
то ура – выражение «короткое».Получив выражение и целочисленную степень, мы используем
StringBuilder
чтобы сделать строку для замены. А вот дальше происходят интересные вещи.Во-первых, мы создаем объект типа
ICSharpExpression
, который позволяет из нашей строки создать узел, который сможет подменить узел Math.Pow
. Последующее выражение делает именно это – с помощью LowLevelModificationUtil
мы заменяем один узел на другой.protected override void Execute(JetBrains.ReSharper.Psi.Tree.IElement element)
{
IInvocationExpression expression = GetSelectedElement<IInvocationExpression>(false);
if (expression != null)
{
IInvocationExpressionNode node = expression.ToTreeNode();
if (node != null)
{
IArgumentListNode args = node.ArgumentList;
int count = (int)double.Parse(args.Arguments[1].Value.GetText().Replace("f", string.Empty));
bool isShort = node.Arguments[0].Value is ILiteralExpression ||
node.Arguments[0].Value is IReferenceExpression;
var sb = new StringBuilder();
sb.Append("(");
for (int i = 0; i < count; ++i)
{
if (!isShort) sb.Append("(");
sb.Append(args.Arguments[0].GetText());
if (!isShort) sb.Append(")");
if (i + 1 != count)
sb.Append("*");
}
sb.Append(")");
// now replace everything
ICSharpExpression newExp = Provider.ElementFactory.CreateExpression(
sb.ToString(), new object[] { });
if (newExp != null)
{
LowLevelModificationUtil.ReplaceChildRange(
expression.ToTreeNode(),
expression.ToTreeNode(),
new[] { newExp.ToTreeNode() });
}
}
}
}
Вот и все. Все работает. Полный СА можно скачать тут. Напоминаю, что базовый класс
ContextActionBase
находится тут. Этот пример был протестирован на версии 4.5 Решарпера, про версию 5 ничего не знаю :)Я знаю что пример сложноват. Обход синтаксического дерева – это дело непростое. Я мучаюсь практически с каждым СА, который пишу. Дебаггер в этом плане очень помогает, конечно, но если будете писать сложные экшны, советую набросать простенькую DSL на том же F#, например, потому что поиск по дереву в C# выглядит неопрятно, со всеми этими приведениями типов, проверками на
null
и так далее. Удачи! ■