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 (each version you will target)
- ReSharper SDK Documentation ← read this
- 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
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+F5orDebug→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
Debugtab on the left. - Select
Start external program:and enter the path to yourdevenv.exe(Visual Studio). - Set the
Command line argumentsto:/ReSharper.Plugin <your-plugin>.dll, obviously substituting your plug-in DLL file. - Set the
Working directoryto:<the-folder-containing-your-plugin-dot-dll>\
- Right click your project then select
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.
[ShellComponent] // Tell ReSharper to manage the lifetime of this class.
public class MyStuff {
public MyStuff([NotNull] ISettingsStore someRequiredStuff){
// 'someRequiredStuff' will be injected by ReSharper.
}
public string ImportantStuff { get; private set; }
}
// ...
public class DoesStuff {
public void ProcessThings(IEnumerable things){
var myStuff = Shell.Instance.GetComponent<MyStuff>();
// do stuff...
}
}
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 aShellComponent.
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.
[ShellComponent] // Tell ReSharper to manage the lifetime of this class.
public class MyStuff {
private static WeakReference _mostRecentInstance;
public static MyStuff Instance {
get {
var instance = Shell.Instance.GetComponent<MyStuff>();
return (null == instance && null != _mostRecentInstance)
? _mostRecentInstance.Target as MyStuff // dirty hack
: instance;
}
}
public MyStuff([NotNull] ISettingsStore someRequiredStuff){
_mostRecentInstance = new WeakReference(this);
}
}
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.
[CanBeNull]
public static IDeclaration GetDeclaration(ITreeNode node) {
while (null != node) {
var declaration = node as IDeclaration;
if (null != declaration)
return declaration;
node = node.Parent;
}
return null;
}
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.
[CanBeNull]
public static IDeclaredElement GetDeclaredElement(ITreeNode node) {
var declaration = GetDeclaration(node); // see above
return null == declaration ? null : declaration.DeclaredElement;
}
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.
IFile. This example is for an ICSharpFile.
public static ICSharpFile GetCSharpFile(IPsiSourceFile sourceFile) {
#if RSHARP6
return sourceFile.GetPsiFile(CSharpLanguage.Instance) as ICSharpFile;
#else
return sourceFile.GetPsiFiles<CSharpLanguage>()
.OfType<ICSharpFile>().SingleOrDefault();
#endif
}
Then using that IFile instance you can get the correct document range.
var codeFile = GetCSharpFile(PsiSourceFile); var documentRange = codeFile.GetIntersectingRanges(range) .First(x => x.Document == PsiSourceFile.Document);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.Adding, Removing, and Replacing Nodes
A handy utility class for mutating the document tree is theModificationUtil.
It allows you to add, delete, and replace nodes in the document tree.
ModificationUtil.ReplaceChild(oldStringLiteralNode, newStringLiteralNode);
Creating C# Comment Nodes
var elementFactory = CSharpElementFactory.GetInstance(node.GetPsiModule()); var newComment = elementFactory.CreateComment(newText);
Creating C# String Literal Nodes
var elementFactory = CSharpElementFactory.GetInstance(node.GetPsiModule());
// NOTE: newText needs quotes "" or literal quotes @""
var newStringLiteral = elementFactory.CreateExpression("$0", newText);
Creating HTML Nodes
var elementFactory = HtmlElementFactory.GetInstance(node.Language);
// NOTE: may return multiple nodes, all of them should be used together:
var newElements = elementFactory.CompileText(someNewHtmlText, node).ToList();
// ...
// EXAMPLE: replace one node with the result of CompileText:
if(newElements.Count > 0){
var recentNode = ModificationUtil.ReplaceChild(oldNode, newElements[0]);
for(int i = 1; i < newElements.Count; i++) {
recentNode = ModificationUtil.AddChildAfter(
recentNode.Parent, recentNode, newElements[i]);
}
}
Creating JavaScript Nodes
[CanBeNull]
private static ITreeNode CreateNode(
[NotNull] IPsiSourceFile file,
[NotNull] IPsiModule module,
[NotNull] LanguageService languageService,
string javaScriptText
) {
// parse the JavaScript text into new node instances
var lexer = languageService.GetPrimaryLexerFactory()
.CreateLexer(new StringBuffer(javaScriptText))
var parser = languageService.CreateParser(
lexer,
module,
file
) as IJavaScriptParser;
if (null != parser) {
var newNodes = parser.ParseFile();
if (null != newNodes)
return newNodes.LastChild;
}
return null;
}
Identifier Names
You can use ReSharper's built in naming utilities. First you have to get access to the naming manager and provider.
public class MyDaemonStageProcess : IDaemonStageProcess{
public MyDaemonStageProcess(
[NotNull] IDaemonProcess process,
[NotNull] IContextBoundSettingsStore settingsStore,
[NotNull] PsiLanguageType languageType
) {
PsiSourceFile = process.SourceFile;
var psiServices = PsiSourceFile.PsiModule.GetPsiServices();
// get the naming manager
NamingManager = psiServices.Naming;
// get the naming policy provider
NamingPolicyProvider = psiServices.Naming.Policy
.GetPolicyProvider(languageType, process.SourceFile, settingsStore);
}
// ...
}
After you have an instance of the naming manager and provider you can then parse names using ReSharper.
public Name ParseName(string name){
return NamingManager.Parsing.Parse(
name, NamingRule.Default, NamingPolicyProvider);
}
public Name ParseName(IIdentifier identifier) {
var declared = GetDeclaredElement(identifier); // see above
var namingRule = null == declared
? NamingRule.Default
: NamingPolicyProvider.GetPolicy(declared).NamingRule
return NamingManager.Parsing.Parse(
identifier.Name, namingRule, NamingPolicyProvider);
}
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.
[SettingsKey(typeof(EnvironmentSettings), "My Settings")]
public class MySettings {
[SettingsIndexedEntryAttribute("Some Names")]
public IIndexedEntry<string, byte> SomeNames { get; set; }
}
You can then enumerate and modify the settings collection like the next example.
// read all var someWords = settings .EnumEntryIndices<SpellCheckSettings, string, byte>(x => x.SomeNames) // set an item settings.SetIndexedValue((MySettings x) => x.SomeNames, name, default(byte)); // remove an item settings.RemoveIndexedValue((MySettings x) => x.SomeNames, name);
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:
<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
ThemedIconPngaction will generate some new files for you. - Right click your resources folder and select "Add Existing Item..."
- Add the generated
csandxamlfiles. You may need to set the filter to All Files. - You should be good to go now.
[OptionsPage(
Pid,
"My Settings Form",
typeof(UnnamedThemedIcons.MyIcon),
ParentId = EnvironmentPage.Pid,
Sequence = 100
)]
public partial class MyOptionsControl : UserControl, IOptionsPage {
// ...
}
You can use the icon in a bulb item like this:
public class MyQuickFix : IQuickFix {
// ...
public void CreateBulbItems(BulbMenu menu, Severity severity)
{
var defaultGroup = menu.GetOrCreateGroup(Anchor.DefaultAnchor);
var subMenu = defaultGroup.GetOrCreateSubmenu(new BulbMenuItemViewDescription(
new Anchor("MyStuff", new AnchorRelation[0]),
UnnamedThemedIcons.MyIcon.Id,
"Stuff"
));
subMenu.Submenu.ArrangeQuickFixes(Items
.Select(x => new Pair<IBulbAction, Severity>(x,severity)));
}
// ...
}