Как стать автором
Обновить

Расширяем ReSharper — Context Actions

Время на прочтение 8 мин
Количество просмотров 1.7K
В комментариях к одному из предыдущих постов я обещал рассказать про то, как писать расширения к Решарперу. Рассказать хочу потому, что сам периодически пишу расширения которые упрощают работу в моей конкретной области. Тут же я покажу вкратце мой подход к написанию расширений типа context action.


Итак, context action – это то появляющееся слева меню со стрелочкой, которое дает возможности «быстрой коррекции» в коде. Если хотите забайндить открытие этого меню, кстати, комманда называется ReSharper.QuickFix. Я же пишу дополнительные опции для этого меню. Почему? Потому что это иногда экономит время. Давайте посмотрим на то, как писать context action для Решарпера.

Создание plug-in’а и настройка отладки
Расширения решарпера – это обычные библиотеки классов (DLL) которые кладутся в папку Plugins в папке bin решарпера. Для отладки же, копировать их туда не нужно – можно попросту указать название и путь к плагину как аргументы вызова самой студии (devenv.exe). Синтаксис примерно такой:

devenv.exe /ReSharper.Plugin c:\path\to\your.dll

Если ничего не писать, но запустить отладку по F5, ваш plug-in уже появится в списке плагинов решарпера. Конечно лучше добавить какой-нть контент, чем мы сейчас и займемся.

Начинаем делать context action
Первое что нужно сделать, это добавить ссылки на сборки решарпера, которые нам нужны. Я тупо добавляю ссылки на все сборки в которых фигурирует имя 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)
  {
    // тут пусто
  }
  ⋮
}

Каркас готов. Теперь нужно научиться определять, применим ли наш СА.

IsAvailable()
Наш СА применим только если мы сидим в теле 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 в который обернут код – это элемент внутренней семантики Решарпера. Я понятия не имею что он делает – просто копирую его из примеров так, на всякий случай. Ведь детальной, обновленной докуменации по написанию плагинов для Решарпера пока нет.

GetText()
Если мы вернули true из IsAvailable(), Решарпер захочет знать, какой текст нарисовать в меню. В данном случае мы уже знаем что возвращать – содержание переменной text.

protected override string GetText()
{
  return text;
}

Ах, если бы все было так просто…

Execute()
У пользователя появилась возможность использовать СА по назначению. Если он на нее кликнул в меню, вызывется метод 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 и так далее. Удачи! ■
Теги:
Хабы:
+13
Комментарии 13
Комментарии Комментарии 13

Публикации

Истории

Работа

.NET разработчик
66 вакансий

Ближайшие события

Московский туристический хакатон
Дата 23 марта – 7 апреля
Место
Москва Онлайн
Геймтон «DatsEdenSpace» от DatsTeam
Дата 5 – 6 апреля
Время 17:00 – 20:00
Место
Онлайн