📜 ⬆️ ⬇️

Extend ReSharper - Context Actions

In the comments to one of the previous posts, I promised to tell you how to write extensions to Resharper. I want to tell because I myself periodically write extensions that simplify the work in my particular area. Here I will briefly show my approach to writing extensions of the context action type.


So, context action is the menu that appears on the left with the arrow, which allows for “quick correction” in the code. If you want to prevent opening 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.

Creating plug-in & rsquo; a and debugging
Resharper extensions are the usual class libraries (DLLs) that are placed in the Plugins folder in the resharper bin folder. For debugging, there is no need to copy them there - you can simply specify the name and the path to the plugin as the arguments of 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 on F5, your plug-in will already appear in the list of resolver plugins. Of course it is better to add some content than we do now.

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

Context actions are made by inheriting from CSharpContextActionBase (in the case of C #), as well as implementing several other interfaces. Fortunately, part of the "plumbing" implemented the developers of other plug-ins. For context ContextActionBase , I add the ContextActionBase class to my project, which 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:



Interface of our SA
To make it work, you need to add only 4 methods to the resulting class:


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


So, let's try to make the implementation of this mini-refactoring step by step.

Frame
To begin with, we make our SA class, decorate it with a small set of metadata. The text field has been added so that we can directly prompt the user in the CA menu what will happen to his code after refactoring.



[ContextAction(Group = "C#" , Name = "Inline a power function" ,
Description = "Inlines a power statement; eg, changes Math.Pow(x, 3) to x*x*x." ,
Priority = 15)]
internal class InlinePowerAction : ContextActionBase
{
private string text;
public InlinePowerAction(ICSharpContextActionDataProvider provider) : base (provider)
{
//
}

}

The frame is ready. Now we need to learn to determine whether our CA is applicable.

IsAvailable ()
Our SA is applicable only if we are sitting in the body of Math.Pow() and this body has an integer degree - for example, 3.0. How to do it? First we find the place where the user has the cursor. Then, we get those nodes of the syntax tree, which stand in the same place as the cursor, and try to bring them to the expected types. Since Math.Pow() is a function call, we expect to see a IInvocationExpression with IInvocationExpression in its body. And so on, in a chain, and we always use the as operator in case that 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 - SA is applicable, return true . In all other cases, return false .

An example code is shown below. It is better not to read it and walk on it debugger. This refers to working with ReSharper entirely - the best way to learn more about the structure of the syntax tree is through the debugger.



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 ;
}

See how I set the value of the text variable before returning true ? This is to make the CA better read by the user. Yes, and as for ReadLockCookie in which the code is wrapped - this is an element of Resharper's internal semantics. I have no idea what he is doing - just copying him from the examples so, just in case. After all, there is no detailed, updated documentation on writing plugins for Resharper.

GetText ()
If we return true from IsAvailable() , Resharper will want to know which text to draw in the menu. In this case, we already know what to return - the content of the text variable.

protected override string GetText()
{
return text;
}

Oh, if everything was so simple ...

Execute ()
The user has the opportunity to use the CA for its intended purpose. If he clicked on it in the menu, the Execute() method is called. And here our replacement algorithm is just starting to work. Remember - we want to change, say, Math.Pow(x, 3.0) to x*x*x . How to do it?

We again need a tree node that contains Math.Pow() . We pull out both parameters (in the example above, x and 3), gently converting the values ​​even if it is written, for example, not 3.0 but 3.0f. Next, we determine how long the expression is to the left - because if we raise to the power of x , then we can write x*x*x , but if x+y we will have to write with brackets (x+y)*(x+y)*(x+y) To do this, we interrupt the type, and if it is ILiteralExpression or IReferenceExpression then hooray is the expression “short”.

After receiving the expression and the integer power, we use StringBuilder to make a string for replacement. But then interesting things happen.

First, we create an object of type ICSharpExpression , which allows us to create a node from our string that can replace the Math.Pow node. The following expression does just that - with LowLevelModificationUtil we replace one node with another.

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() });
}
}
}
}

That's all. Everything is working. Full SA can be downloaded here . I remind you that the base class ContextActionBase is here . This example was tested on version 4.5 of Resharper, I don’t know anything about version 5 :)

Conclusion
I know that the example is complicated. Traversing the syntax tree is not an easy task. I suffer with almost every SA that I write. Debagger in this regard helps a lot, of course, but if you write complex actions, I advise you to jot down a simple DSL on the same F #, for example, because searching the tree in C # looks untidy, with all these type conversions, null checks and so on. Good luck! ■

Source: https://habr.com/ru/post/74026/


All Articles