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 extensions are ordinary class libraries (DLLs) that are put in a folder
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.
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
Context actions are done by inheriting from
Now, to create a CA, you need to do two things:
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 function that quickly inlines calls
So, let's try to gradually implement this mini-refactoring.
To begin with, we make a class of our CA, decorate it with a small set of metadata. The field is
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
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
If we returned
Having received the expression and the integer power, we use
First, we create an object of the type
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. Resharper extensions are ordinary class libraries (DLLs) that are put in a folder
Plugins
in a folderbin
Resharper. 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.
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 ContextActionBase
that 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
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 toIElement
, 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 becauseMath.Pow(x, 2.0)
← it's bad and slowx*x
← much faster
So, let's try to gradually implement this mini-refactoring.
To begin with, we make a class of our CA, decorate it with a small set of metadata. The field is
text
added 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)
{
// тут пусто
}
⋮
}
Math.Pow()
Math.Pow()
IInvocationExpression
Math.Pow
. And so on, along the chain, and we always use the operator as
in 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;
}
true
text
ReadLockCookie
in 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. If we returned
true
from 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 containsprotected override string GetText()
{
return text;
}
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+y
we have to write with brackets (x+y)*(x+y)*(x+y)
. To do this, we interrupt the type, and if it ILiteralExpression
or IReferenceExpression
then cheers - the expression "short". Having received 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 the type
ICSharpExpression
that allows us to create a node from our line that can replace the node Math.Pow
. The following expression does just that - withLowLevelModificationUtil
we 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
null