Extending ReSharper - Context Actions

    In the comments on one of the previous posts, I promised to talk about how to write extensions to Resharper. I want to tell because I periodically write extensions that simplify work in my specific area. Immediately, I will show briefly my approach to writing extensions of type context action.


    So, context action is the menu with an arrow that appears on the left, which gives the possibility of "quick correction" in the code. If you want to bind the opening of this menu, by the way, the command is called ReSharper.QuickFix. I am writing additional options for this menu. Why? Because it sometimes saves time. Let's take a look at how to write context action for Resharper.

    Create a plug-in and configure debugging
    Resharper extensions are ordinary class libraries (DLLs) that are put in a folder Pluginsin a folderbinResharper. For debugging, you don’t need to copy them there - you can simply specify the name and path to the plugin as arguments for calling the studio itself ( devenv.exe). The syntax is something like this:

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

    If you do not write anything, but start debugging using F5, your plug-in will already appear in the list of plug-ins of the resolver. Of course, it’s better to add some kind of content than we are going to do now.

    We begin to do context action
    The first thing to do is add links to the resolver assemblies that we need. I stupidly add links to all assemblies in which the name appears ReSharper, because I have no idea what might be needed.

    Context actions are done by inheriting from CSharpContextActionBase(in the case of C #), as well as by implementing several other interfaces. Fortunately, part of the plumbing was implemented by developers of other plugins. For context actions, I add a class to my project ContextActionBasethat was written by the authors of the Agent Johnson plugin . Actually the file itself can be found here .

    Now, to create a CA, you need to do two things:

    • Inherit your CA from ContextActionBase
    • Decorate the resulting class with the ContextActionAttribute attribute


    The interface of our CA
    To make it work, you need to add only 4 methods to the resulting class:

    • The default constructor. There is basically nothing to do here.
    • Method GetText(). This method returns in the string what will be written for your command in the CA drop-down menu.
    • Method IsAvailable(IElement). Determines whether your CA is applicable at a given point in the code or not. IElement- this is your link to the point in the code where the cursor is. From this point in the code you can bypass at least the entire file tree.
    • Method Execute(IElement). If the user clicked on your CA, then you can apply it. We again have a link to IElement, i.e. we can walk around the code and choose what to change and where.

    Let's take a simple example. Imagine that you need to implement a function that quickly inlines calls Math.Pow()with integer values. This is necessary because

    • Math.Pow(x, 2.0) ← it's bad and slow
    • x*x ← much faster

    So, let's try to gradually implement this mini-refactoring.

    Frame
    To begin with, we make a class of our CA, decorate it with a small set of metadata. The field is textadded so that we can directly in the CA menu tell the user what will happen to his code after refactoring. The frame is ready. Now we need to learn to determine whether our CA is applicable. Our CA is applicable only if we are sitting in the body and this body has an integer degree - for example 3.0. How to do it? First, we find the place where the user has a cursor. Then, we get the nodes of the syntax tree that are in the same place as the cursor, and try to cast them to the expected types. Since - a function call, we expect to see in which in the body -



    [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()Math.Pow()IInvocationExpressionMath.Pow. And so on, along the chain, and we always use the operator asin case the expression is not what we expect.

    At the end of the whole chain, we find and check the value of the exponent. If it is integer and between 1 and 10 - CA is applicable, return true. In all other cases, return false.

    An example code is given below. It’s better not to read it, but to walk on it with a debugger. This applies to working with ReSharper as a whole - the best way to learn more about the structure of the syntax tree is through the debugger. See how I assign a value to a variable before returning ? This is to ensure that the CA is better read by the user. Yes, and as for



    protected override bool IsAvailable(JetBrains.ReSharper.Psi.Tree.IElement element)
    {
      using (ReadLockCookie.Create())
      {
        IInvocationExpression invEx = GetSelectedElement(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;
    }

    truetextReadLockCookiein which the code is wrapped is an element of the inner semantics of Resharper. I have no idea what he is doing - just copy it from the examples like this, just in case. After all, there is no detailed, updated documentation on writing plugins for Resharper.

    GetText ()
    If we returned truefrom IsAvailable(), Resharper wants to know what text to draw on the menu. In this case, we already know what to return - the content of the variable text. Ah, if everything was so simple ... The user now has the opportunity to use the CA for its intended purpose. If he clicked on it in the menu, the method is called. And here our replacement algorithm begins to work. Remember - we want to change, say, to . How to do it? We again need a tree node that contains

    protected override string GetText()
    {
      return text;
    }



    Execute ()
    Execute()Math.Pow(x, 3.0)x*x*x

    Math.Pow(). We pull out both parameters (in the example above - x 3), carefully converting the values ​​even if it is written, for example, not 3.0 but 3.0f. Further, we determine how long the expression is on the left - because if we raise to a power x, we can write x*x*x, but if x+ywe have to write with brackets (x+y)*(x+y)*(x+y). To do this, we interrupt the type, and if it ILiteralExpressionor IReferenceExpressionthen cheers - the expression "short".

    Having received the expression and the integer power, we use StringBuilderto make a string for replacement. But then interesting things happen.

    First, we create an object of the type ICSharpExpressionthat allows us to create a node from our line that can replace the node Math.Pow. The following expression does just that - withLowLevelModificationUtilwe replace one node with another. That's all. Everything works. Full CA can be downloaded here . I remind you that the base class is here . This example was tested on version 4.5 Resharper, I do n’t know anything about version 5 :) I know that the example is complicated. Walking the syntax tree is not an easy task. I suffer with almost every SA that I write. The debugger helps a lot in this regard, of course, but if you write complex action games, I advise you to sketch a simple DSL on the same F #, for example, because a tree search in C # looks untidy, with all these type casts, checks for and so on. Good luck ■

    protected override void Execute(JetBrains.ReSharper.Psi.Tree.IElement element)
    {
      IInvocationExpression expression = GetSelectedElement(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

    Conclusion
    null

    Also popular now: