What I Learned About Writing ReSharper Plugins
Recently I wrote a plug-in for ReSharper, You Can’t Spell. It was just a simple spell check tool that I wanted to write as a weekend project. Yeah right! Nearly two months later my weekend project was finished and I released it. This post contains some tips and strategies I learned from my experience.
What You Need
- WiX Toolset
- Visual Studio Pro+ *
- ReSharper *
- ReSharper SDK (for each version you will target)
- ReSharper SDK Documentation ← read this
* Be aware that some of these things cost lots of money which many of us have little of. There are some solutions for you though!
- You are a college student.
- Check with your school to see if they offer MSDNAA. Probably the CS/IS/IT department.
- JetBrains offers cheaper academic pricing. If you blew the leftovers of your loan check on other stuff then get into open source for the experience and maybe a free ReSharper license.
- You own a small/tiny software “business.”
- Look into BizSpark
- You do open source stuff.
- Start a “business” then see above.
- E-mail JetBrains and ask for free stuff; you should reach a human. Also mention the plug-in you would like to write that may enhance their revenue.
Solution and Project Organization
Note: You may want to grab a copy of my project from BitBucket using Mercurial.
Projects and Code Organization (my way)
It helps to decide first what versions of Visual Studio and ReSharper you will be targeting. Each ReSharper version that you will target should have its own project as it will reference a different ReSharper SDK and there may be breaking code changes between each version.
For each project you should define Conditional compilation symbols that describe the version of ReSharper the project is targeting (example). I define two for each project; One for the major version and another or the major and minor version. For example, MyPlugin.ReSharper.v71 would have RSHARP7 and RSHARP71 defined. This lets me use the preprocessor to conditionally compile different sections of code depending on what version of the API I am compiling against (example).
Each project also shares the same root namespace but will have a different assembly name corresponding to the ReSharper version (example). This helps prevent any confusion over which assemblies reference which SDK version.
Each of my plug-in projects has very little code within their directories. Instead I put all of the code into a shared folder (not a common project or assembly) and each project references the same code using links (example). I then use the preprocessor to handle different versions of the API (example). A nice time saver with linked files is that you can make links that go to entire folders using wild-cards but eventually Visual Studio will flatten them out into individual items.
If you go with this project setup you will find that it is extremely important to keep as much code as is possible in a shared assembly to simplify the creation of new plug-in projects and ease maintenance. Maintaining the linked files can be error prone and annoying.
Assembly Versions
Assembly versions and installer versions are somewhat important.
For any given release of your plug-in you may want all of your files to share a common version number.
I am lazy and use the N.M.*
format but if you want to Do It Right use source control branching and a CI system to generate version numbers.
You can have a common file with
shared assembly information
and link it
within your projects. Then your
installer version can be derived from the assembly version
so everything matches up nicely.
Keep in mind that because of the way the MSI system works only the first 3 parts of a version are significant for upgrades.
Installer
I use the WiX Toolset to generate MSI packages. I don’t really know what I am doing but hopefully my example wxs can help you out. Something important to note is that I install to the user’s local “AppData” folder instead of the Program Files folder. This is because Visual Studio is sometimes run under a restricted account and may not have needed access to the Program Files directory to run the plug-in.
Visual Studio 2010 Solution Issues
There is an issue with Visual Studio 2010 where it will only properly debug a ReSharper plug-in that is targeting .NET 4.0. If you wish to release the plug-in against .NET 3.5 you will need to have two projects, one for debugging and another for release. I have an example on my project site of this structure.
Debugging
Debug Performance
Debugging ReSharper and your plug-in is brew-a-cup-of-coffee slow! If you want to ever get anything done, never start with the debugger attached unless you need to.
- Start with
Ctrl+F5
or “Debug → Start Without Debugging” - Open your ConsoleApplication42 that you want to debug your plug-in against
- Then right before you trigger an action that would reach your breakpoint or throw an exception attach the debugger: “Debug → Attach to Process…”
- Detach when done: “Debug → Detach all”
Project Issues
Your plug-in project should be configured to launch a new Visual Studio instance with the debugger attached and with your plug-in loaded. Normally it works but I ran into issues with this.
- If your plug-in is installed on the development system ReSharper will get confused (two of the same plug-in). Make sure to un-install your plug-in if you installed it before debugging it.
- If you set the project up wrong or didn’t copy the debug launch settings to another computer maybe, you will need to reconfigure that.
- Right click your project then select “Properties”
- Click the “Debug” tab on the left
- Select “Start external program:” and enter the path to your
devenv.exe
(Visual Studio) - Set the “Command line arguments” to:
/ReSharper.Plugin <your-plugin>.dll
, obviously substituting your plug-in DLL file. - Set the “Working directory” to:
<the-folder-containing-your-plugin-assembly>\
Visual Studio 2010 Debug Issue
If you are using Visual Studio 2010 and the debugger is behaving as if it is not attached see the above section: Visual Studio 2010 Solution Issues. You may need to target .NET 4.0 for debugging to work.
Testing
Unit Testing
If you are going to write tests for your plug-in I recommend you do as much unit testing as possible on your shared assemblies. The integration testing is in my opinion very difficult and tedious to get right.
Integration Testing
JetBrains provides a bit of framework to help you test your plug-in but I still found it difficult and very tedious when working with multiple versions of ReSharper. I highly recommend using the ReSharper Plug-In Tests project wizard to create your plug-in test projects instead of starting from scratch. The testing works by running your plug-in against input files and comparing the result against gold files which you can either write or rename after a test is first run.
Assembly Reference Issues
One thing I found out the hard way is that your test assemblies may not share the same output folder as your plug-in. The ReSharper Plug-In Tests project wizard adds a bunch of assemblies that are required to run the tests but mess with your plug-in during debug. Because of this I have a special output folder for my test projects.
Dependency injection and Inversion of Control
When you are working on a plug-in and need an instance of a ReSharper class you can probably get one from their IoC containers or have it injected into your classes. Most of your classes that implement ReSharper interfaces seem to receive their parameters through dependency injection. You can sometimes find ways to access needed instances from parameters or members you already have but may need to call directly to one of their IoC containers. You can even use their IoC containers for your own classes.
|
|
IoC Issues
I ran into some issues getting instances from the component system.
Specifically in my settings form I could not figure out how to access an instance of my own class marked as a ShellComponent
.
I had to resort to having a WeakReference
that pointed to the most recent instance.
I couldn’t just create a singleton as ReSharper is supposed to manage the lifetime of the object.
It feels dirty but that was the best I could come up with at the time.
|
|
Processing Nodes
ReSharper parses the code files into a tree for you, its pretty handy. ReSharper also allows you to manipulate that tree and have the changes reflected in the editor. This way you can edit and process code files in an object oriented way. It works out pretty well once you get the hang of their way of doing things.
Declarations
There is a class of node called a declaration that can be identified by the IDeclaration interface. I often ran into situations where the node I had was an ITreeNode when I needed an IDeclaration but the node I had did not implement that interface. It turns out that if the node you have does not implement the IDeclaration interface its parent or grandparent may. A variable identifier for example may be represented by both a parent and child node; one of which may be the IDeclaration you are looking for.
|
|
Declared Elements
After you get a declaration for a tree node you can get the declared element which can give you even more information or instances that you need.
|
|
Node Locations
A tree node has a location within a document. For simple documents like a C# code file this is pretty straight forward. For an MVC razor document however things get tricky. Consider that a cshtml file can contain HTML, JavaScript, and C#. ReSharper handles documents like this by treating them as multiple documents. Now not only do you need to worry about where in a document your node is but which document.
Nodes can offer what is called a TreeTextRange
that has information about the location of the node.
You can get an instance of this from the GetTreeTextRange()
method.
Additionally some more specific interfaces have different ranges they can offer.
For example the IComment
interface provides a GetCommentRange()
method.
Typically though you will need to work with a DocumentRange
instance.
As I understand it this offers more context regarding the text range.
Because of the complexity involved with multiple documents you have to go through some extra steps to make sure you get the right instance.
The first thing you need is an instance of IFile
. This example is for an ICSharpFile
.
|
|
Then using that IFile
instance you can get the correct document range.
|
|
This document range can be used to create a ReSharper highlighting in the correct spot within the code file you are processing.
Manipulating Nodes and Trees
Manipulating the node tree can be somewhat tricky. Each language seems to have a different way of doing things.
CRUD for Nodes
A handy utility class for mutating the document tree is the ModificationUtil
.
It allows you to add, delete, and replace nodes in the document tree.
|
|
Creating C# Comment Nodes
|
|
Creating C# String Literal Nodes
|
|
Creating HTML Nodes
|
|
Creating JavaScript Nodes
|
|
Identifier Names
You can use ReSharper’s built in naming utilities. First you have to get access to the naming manager and provider.
|
|
After you have an instance of the naming manager and provider you can then parse names using ReSharper.
|
|
ReSharper Settings
ReSharper lets you use their settings system for your own plug-in. There are plenty of advantages to this so consider it before you just go dropping XML files in some folder.
List
Saving primitive variables to the settings is simple enough but saving collections of strings for example is not so simple.
ReSharper does not offer any kind of simple collection setting type but they do offer an index entry type.
The index entry type works like a generic dictionary so if I need a list of strings I can use an index entry
of type IIndexEntry<string,byte>
where the key contains the string I wish to store and the byte will contain garbage.
You define an index entry like in the following example.
|
|
You can then enumerate and modify the settings collection like the next example.
|
|
This method ends up behaving more like a HashSet<string>
.
If you need something like a true list you can do that to too.
You still need to use key value pairs but you can use the list index as the key and the list item as the value: IIndexEntry<int,string>
.
Refactoring
Name Changes
If you dig around in the sample project you can find a utility class that triggers ReSharper renames for you. I don’t know of any documentation for it or what the magic values mean but so far it is the best way I know of to do it. You can find this magical beast within your ReSharper SDK folder at “\SDK\Samples\SamplePlugin\SamplePlugin\src\CallRename\CallRenameUtil.cs” .
Icons
In ReSharper 6 you could just give your options pane an embedded PNG file, that was nice. In ReSharper 7 it was changed to something a bit more complex. You have to jump through some code generation hoops the first time but it should be smooth after that. If for some reason you have trouble getting icons to work, try the following steps:
- Right click your project and select “Unload”
- Right click your project again and select “Edit …”
- Scroll all the way down to the bottom. You will want something that looks like this:
1 2 3
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" /> <Import Project="$(ReSharperSdkTargets)\Plugin.Targets" /> <Import Project="$(ReSharperSdkTools)\MSBuild\JetBrains.Build.Platform.ThemedIconsConverter.Targets" />
- Right click your project a 3rd time and select “Reload”
- You should have some folder in your project that you can put resources in, I called mine “resources”
- Make sure your PNG icon is in that folder
- Right click the PNG and select Properties
- In the properties pane make sure the BuildAction is “ThemedIconPng”
- Build your project again. When you build the project the “ThemedIconPng” action will generate some new files for you
- Right click your resources folder and select “Add Existing Item…”
- Add the generated cs and xaml files. You may need to set the filter to All Files
- You should be good to go now
You can attach the icon to a settings form like this:
|
|
You can use the icon in a bulb item like this:
|
|