CodeRush – Writing a Context Provider
There are two types of developers in the world – those who use CodeRush and those who use ReSharper. I happen to be in the CodeRush (CR) group for various reasons. One of the benefits I really like about CR is its flexibility and the ability to easily define my own custom templates. A template in CR is like a smart code snippet. When you type a certain key combination in the right (configurable) context then what you type can be replaced by something else. In this post I’m going to discuss a simple context provider that can be used in CR templates.
Background
Assume you have the following code.
using System; namespace ConsoleApplication1 { class Myclass { /// <summary>A property.</summary> /// <exception cref="ArgumentOutOfRangeException">When setting the property and the value is <see langword="null"/>.</exception> public string SomeValue { get { return m_value; } set { if (value == null) throw new ArgumentNullException("value"); m_value = value; } } /// <summary>A method </summary> /// <param name="value">Some argument.</param> /// <exception cref="ArgumentNullException"><paramref name="value"/> is <see langword="null"/>.</exception> public void Foo ( string value ) { if (m_value == null) throw new ArgumentNullException"value"); } private string m_value; } }
The exception documentation is boilerplate code. With CR you can easily define a template that says “when I enter /xout while inside an XML doc comment convert it to the desired text”. Whenever CR sees a template (/xout) it evaluates the context providers, if any, associated with the template. If the providers say the context is good then the template is replaced with whatever text is specified. A single template can have multiple contexts defined if desired. For an exception tag there are three contexts that seem reasonable.
- Documenting an exception for a property. The general format is “When setting the property and the value…” because only setters generally throw and the parameter name is always ‘value’.
- Documenting an exception for a non-property and the parameter name is known. The general format is “<paramref name=””/>…” where the parameter reference has a parameter name.
- Documenting a non-property and the parameter name is not know. The format is the same as above but put the caret inside the reference so the name can be typed in.
The following screenshot shows how this might be done in CR.
The contexts are:
- HasIdentifier – An identifier is on the clipboard.
- InXmlDocComment – The current element is inside an XML doc comment.
- InDocumentationForProperty – The current element is inside an XML doc comment for a property
The InDocumentationForProperty
is a custom context I created. There are several different variants for each of the element types of interest (method, class, etc). In order to understand how the provider works it is important to have a basic understanding of the CR model.
CodeRush Context Providers
With CR you can define a custom context provider that is called each time a template is evaluated. The sole purpose of the provider is to determine whether the context is valid or not. For the custom provider I wrote all I care about is whether the context precedes a specific type of element.
Caveat – the documentation for CR is almost non-existent. Most of the help you get comes from code samples or posting questions in the forums. Therefore everything I say is based upon what I understand and should not be taken as final.
CR breaks up parsed code into elements. An element could be a namespace declaration, type definition, comment, etc. An element can have child elements. For example a class will have a separate element for each member and each member will probably have its own child elements. Namespace declarations, import statements – they are all elements. Given an element you can get the next or previous sibling element or the parent of the current element. This comes in handy when you want to figure out where you are at in the code. Here’s a sample of what the above code looks like to CR.
As can be seen the exception tag for the property is a node nested within the larger XML doc comment node. The parent element has two children: summary and exception. Exception itself has 3 children: text, see tag and more text (the period). This information can be visualized using either the Source Tree or Expression Lab windows in CR. I prefer the Source Tree but they both appear to show the same information.
Writing the Addin
Writing a CR context provider is pretty straightforward.
- Create a new project.
- Select
DXCore\VSIX Standard Plugin
- In the
Settings
dialog enter a nice title. The remaining options can be left at their default settings.
At this point a valid plugin is defined. For CR providers runs inside a plugin. The plugin is just a container for the plugins. For simple providers it is possible to drag and drop the provider from the Toolbox onto the plugin, hook up a couple of provider events and have the plugin do all the work. But this isn’t very modular so I’m going to create my own context provider class and then add it to the plugin.
- Add a new class to the project to represent the context provider.
- Add a few namespaces required for CR to work (see the code).
- Add a reference to the
DevExpress.CodeRush.Extensions
assembly to simplify development. - Derive the provider from
ContextProvider
. - Add any properties that may be useful for configuring the provider.
- Implement the
IsContextSatisfied
virtual method to return true when the context is valid.
The key method for a context provider is the IsContextSatisfied
method. CR calls the method whenever the context needs to be verified. For this provider the context is satisfied if the current element is inside an XML doc comment and the comment applies to a specific element. Since I don’t want to write a separate provider for each element type (property, method, etc) I’ll expose a property to allow the element type to be set along with whether the XML doc comment is required or any comment will do.
public class CommentContextProvider : ContextProvider { public LanguageElementType ForElement { get; set; } public bool IsDocumentation { get; set; } public override bool IsContextSatisfied ( string parameters ) { throw new NotImplementedException(); } }
The provider is now operational but it doesn’t do anything useful. We need to implement the method to detect the cases we care about. Here’s the core code.
public override bool IsContextSatisfied ( string parameers ) { //Make sure we are seeing any changes CodeRush.Source.ParseIfTextChanged(); //No current element if (CurrentElement == null) return false; //Verify the current element is a comment of the appropriate type if ((IsDocumentation && !InDocumentationElement(CurrentElement)) || (!IsDocumentation && !InCommentElement(CurrentElement))) return false; return IsCommentFor(ForElement); } private LanguageElement CurrentElement { get { return (CodeRush.Source != null) ? CodeRush.Source.Active : null; } } private bool InCommentElement ( LanguageElement element ) { return (element.ElementType == LanguageElementType.Comment); } private bool InDocumentationElement ( LanguageElement element ) { switch (element.ElementType) { case LanguageElementType.XmlDocAttribute: case LanguageElementType.XmlDocComment: case LanguageElementType.XmlDocElement: case LanguageElementType.XmlDocText: return true; }; return false; } private bool IsCommentFor ( LanguageElementType elementType ) { //Step back until we get to the root comment element var current = CurrentElement; while (current != null) { //If this is a comment or doc comment element then we've found the root if ((current.ElementType == LanguageElementType.Comment) || (current.ElementType == LanguageElementType.XmlDocComment)) break; else //Somewhere else so keep going current = current.Parent; }; if (current == null) return false; //Get the next code element and verify its type var next = current.NextCodeSibling; return next.ElementType == elementType; }
The first thing the method does is call ParseIfTextChanged
to ensure that the current element has been updated to reflect any text the user has typed in. Without this call the current element is not going to be updated to point to the tag the user just typed in. Next the method does is confirm that the current element is either a comment or XML doc comment depending upon the IsDocumentation
property. If the current element is a comment (of either sort) then the method confirms that the next element (after the entire comment element) is of the appropriate type as determined by the ForElement
property. If all the above conditions are met then the context is valid otherwise it isn’t.
The final step is to associate the provider with the plugin. Since the provider is configurable we can add it multiple times to support the various contexts we want (documenting a property, documenting a method, etc). For now we’ll just create a single context for documenting a property.
- Ensure the code is built.
- Switch to the plugin designer.
- Open the
Toolbox
and drag and drop an instance of the custom context provider onto the designer. - Configure the provider.
ProviderName
is what shows up in the template editor in the context tree. Normally this is a path such asEditorCodeDocumentationInDocumentationForProperty
.Description
is the helpful description of what it does.DisplayName
doesn’t really seem to be used. It would seem that it should represent what the user would see in the context list but that doesn’t appear to happen. I just set it to the last entry inProviderName
.- Set the
ForElement
toProperty.
- Set
IsDocumentation
to true.
- Rebuild the updated plugin.
Debugging CodeRush Addins
Debugging CR addins in Visual Studio (VS) is trivial. The project template already has VS set as the program to be debugged. Since this is a VSIX project the project is already configured to build the addin locally, generate the VSIX file and deploy it to an experimental instance of VS. The experimental VS instance prevents cross-play between the stable version and the version running the addin. In my experience though it doesn’t actually install the VSIX so I have to do so manually. You can confirm it is installed by using Extension Manager
. I personally have had little luck getting this approach to work. In addition to the installation issue I also found that each time I made a code change I had to uninstall the VSIX otherwise the changes weren’t seen.
Because of the above issues I avoid debugging via the VSIX stuff. Instead I switch back to the approach that worked pre-VSIX. This approach has the plugin output to the standard directory (Documents\DevExpress\IDE Tools\Community\Plugins
) that CR will auto-load from. CR will then load the plugin the next time it starts. The only downside to this approach is that CR will load the plugin even in the initial (debugging) copy of VS. Since CR will lock the assemblies subsequent builds will fail. To work around this tell CR to load the raw assembly via the DevExpress
options. You should also disable the auto-installation of the VSIX via the project property settings.
This is something you’ll want to turn back off when you’re done but for testing plugins it works well. Now it is time to test the addin.
- Start the debugger.
- Create or open a project
- Set up a template that uses the new context provider. In my case I defined a new /xout template that uses the context rules mentioned at the beginning of the article to put an exception out of range documentation tag in the code.
- Open a source file, type in the correct code to get the context to be valid and then type the template to verify the provider is working.
- If the provider does not work properly then switch back to the debugger and put a breakpoint into the code so it can be stepped through.
Resources
For more information on CodeRush and extending it refer to the following links.
Hello! ddaeadg interesting ddaeadg site! I’m really like it! Very, very ddaeadg good!