P3.NET

Writing a Context Provider for CodeRush for Roslyn

Several years ago I wrote an article about creating a custom context provider for CodeRush. In that time CodeRush Classic, as it is called, has been replaced by CodeRush for Roslyn which relies on Roslyn. Now seems like a good time to update the provider. Rather than having to read both articles I’m going to repost the old article with updated changes for Roslyn. The code is semantically similar but had to be rewritten to use Roslyn.

Early Attempts

Unfortunately early attempts to upgrade were problematic because:

  • CodeRush for Roslyn was/is still in active development and does not yet support addins.
  • DevExpress does not yet provide NuGet packages for CodeRush assemblies that are needed.
  • Changes in VS 2017 have caused CodeRush issues when loading addins.

Fortunately DevExpress helped convert the code for me to get me started. However CodeRush still does not technically support end user plugins yet.

Background

One of the benefits of CodeRush is the ability to define your own code templates. Templates make it easier to press a couple of characters and auto-generate a lot of code. CodeRush ships with many templates but I am specifically going to focus on generating doc comments for elements. Doc comments tend to be very specific to the element they are applied to. For example an argument exception on a property almost always says something about “when setting the property”. But on a method this does not make sense. Hence the need to know what type of element the comment is being applied to. This is where my custom context provider will come in. It will allow a template to determine if it is being applied to a doc comment for a specific kind of element (e.g. method or property). This will allow me to create a separate template for each of the different kinds of comments to be applied.

Context Providers

A context provider is a CodeRush addin that provides contextual information. It is heavily used in templates to set up the rules under which the template will apply. As mentioned earlier we can set up a context provider to determine that we are in a doc comment and that the doc comment is being applied to a property. Then we can create a template that generates different content based upon the context.

Since CodeRush is using Roslyn all the information is basically obtained using Roslyn rules. At the basic level Roslyn breaks up the syntactical parts of code into nodes, tokens and trivia. A SyntaxNode is the core element and represents a syntactical element along with everything that makes it up. Class declarations and method bodies are nodes. Nodes generally have child elements. A SyntaxToken is a terminal element in a node. Keywords and literals are examples of these. Tokens do not have any children. SyntaxTrivia is basically the stuff that can be ignored in most cases. This includes whitespace and comments. Since we will be working with comments the trivia is what we need.

Creating the Addin

To get started we need to create a new class library for the provider.

  1. Create a class library called P3Net.CodeRush.ContextProviders. Ensure you are targeting .NET 4.6 or higher.
  2. Rename the created class to CommentContextProvider. Make it abstract.
  3. The class will derive from DefaultContextProvider.
public abstract class CommentContextProvider : DefaultContextProvider
{       
    protected SyntaxKind ForElement { get; set; }

    protected bool IsDocumentation { get; set; }

    public override string Language
    {
        get { return KnownLanguageNames.CSharp; }
    }

    public override string Category
    {
        get { return @"P3Net\Code"; }
    }
}

This class is simply a base implementation of the real provider to be discussed later. It derives from DefaultContextProvider which is provided by CodeRush. There are several virtual properties that need to be set to play nice with CodeRush. For this provider we only support C#. Besides the public properties there are a couple of protected properties that derived types will set to determine what type of element the provider will be applied to. Additionally the provider can be configured to work with doc comments or regular comments.

The IsSatisfiedAsync method is where the bulk of the action takes place. This method is called to determine if the context is met by the given element. We need to confirm we are in a doc comment (or comment) and that the comment is associated with an element that matches the kind defined by ForElement. In Roslyn the comment is structured trivia. If we had the node then we could get to the trivia but going the other way is harder. Ultimately we have to resort to a dirty hack that gets the trivia associated with the current node and then find the parent.

public override async Task<bool> IsSatisfiedAsync ( IProviderContext context, ParameterCollection parameters )
{
    try
    {
        var currentDocument = context.ActiveDocument.GetCodeAnalysisDocument();
        var root = currentDocument != null ? await currentDocument.GetSyntaxRootAsync(context.CancellationToken).ConfigureAwait(false) : null;
        CurrentNode = root?.FindNode(context.SelectionSpan, true);
        if (CurrentNode == null)
            return false;

        if (IsDocumentation && !InDocumentationElement(CurrentNode)
            || !IsDocumentation && !InCommentElement(CurrentNode))
            return false;

        return IsCommentFor(ForElement);
    } finally
    {
        CurrentNode = null;
    };
}

private bool InDocumentationElement ( SyntaxNode node )
{
    switch (node.Kind())
    {
        case SyntaxKind.XmlText:
        case SyntaxKind.XmlElement:
        case SyntaxKind.SingleLineDocumentationCommentTrivia:
        case SyntaxKind.MultiLineDocumentationCommentTrivia:
        return true;
    };

    return false;
}

private bool InCommentElement ( SyntaxNode node )
{
    var kind = node.Kind();
    return kind == SyntaxKind.SingleLineCommentTrivia || kind == SyntaxKind.MultiLineCommentTrivia;
}

private bool IsCommentFor ( SyntaxKind forElement )
{
    try
    {
        //Look for the comment associated with element
        var current = CurrentNode;
        while (current != null)
        {
            //If this is a comment or doc comment element then we've found the root
            if (InDocumentationElement(current))
                break;

            current = current.Parent;
        };

        if (current == null)
            return false;

        //Get the parent element
        var trivia = GetRootTrivia(current);

        //Get the parent associated with the trivia
        var parent = GetParentOfTrivia(trivia);

        return parent?.IsKind(ForElement) ?? false;
    } catch
    {
        return false;
    };
}

private SyntaxNode GetParentOfTrivia ( SyntaxNode node )
{
    var parent = node.Parent;
    if (parent == null && (node is IStructuredTriviaSyntax structuredTrivia))
    {
        parent = structuredTrivia.ParentTrivia.Token.Parent;
    };

    return parent;
}

private SyntaxNode GetRootTrivia ( SyntaxNode node )
{
    var root = node;
    while (root.Parent != null && (InDocumentationElement(root.Parent) || InCommentElement(root.Parent)))
        root = node.Parent;

    return root;
}

private SyntaxNode CurrentNode { get; set; }

At this point we have the base implementation of the provider in place. We just need to create the actual providers that will use it.

Adding Dependencies

The addin will require both Roslyn and CodeRush to compile. Roslyn ships as NuGet packages so we just need to add them.

  • Microsoft.CodeAnalysis.Common
  • Microsoft.CodeAnalysis.CSharp
  • Microsoft.CodeAnalysis.Workspaces.Common

CodeRush is not yet NuGet’ed so we have to add binary references to the required assemblies. Unfortunately the path to CodeRush is going to be specific to the user that installed it. Fortunately in the CodeRush menu under Support is a link to the current directory for CodeRush. Go there and add references to the following assemblies.

  • DevExpress.CodeAnalysis
  • DevExpress.CodeAnalysis.Workspaces
  • DevExpress.CodeRush.Foundation
  • DevExpress.CodeRush.Platform
  • DevExpress.CodeRush.TextEditor

Implementing the InDocumentForElement Provider

The first provider we will create is the general purpose provider for determining if a comment is applied to the element kind configured in the template. For this we will create a new class called DocumentationForElementContextProvider that derives from our CommentContextProvider. It also needs to be attributed with ExportContextProvider so CodeRush will find it. Additionally we need to give it the display name that will show up in the UI.

[ExportContextProvider]
public class DocumentationForElementContextProvider : CommentContextProvider
{      
    public DocumentationForElementContextProvider ( )
    {
        IsDocumentation = true;
    }

    public override string Name
    {
        get { return "InDocumentationForElement"; }
    }
}

For this particular provider we need to know what element to check the comments for so we need to receive parameters. Parameters are specified when the template is set up. Normally in a template the context is simply surrounded by brackets. But if a context needs parameters then they are passed like arguments to a function (e.g. InDocumentationForElement(Method)).

The parameters are passed to the provider as part of the IsSatisfiedAsync method. Unfortunately, as of now, the parameters are passed as a single string rather than as a collection of parameters. So we need to parse the parameter string to know what kind of element to look for.

public override async Task<bool> IsSatisfiedAsync ( IProviderContext context, ParameterCollection parameters )
{
    ForElement = GetElementParameter(parameters);

    return await base.IsSatisfiedAsync(context, parameters);
}

#region Private Members

private SyntaxKind GetElementParameter ( ParameterCollection parameters )
{
    //Split by commas
    var tokens = parameters.All?.Split(',');
    if (tokens.Length > 0)
    {
        //First parameter is the kind of element
        var converter = TypeDescriptor.GetConverter(typeof(SyntaxKind));

        return (SyntaxKind)converter.ConvertFromString(tokens[0]);
    };

    return SyntaxKind.None;
}

Installing the Addin

The addin will need to be installed just like any other VS extension so we need to create a VSIX package. Add a new VSIX project to the solution. At a minimum ensure that you open the manifest in the designer and provide reasonable values for the product name, ID and version.

To add the addin to the package go to the Assets tab, select New and specify the following.

Type
Microsoft.VisualStudio.MefComponent
Source
A project in current solution
Project
the name of the project

The addin requires CodeRush to be installed so go to the Dependencies tab and add it.

Source
Installed extension
Name
CodeRush for Roslyn
Version Range
[16.2.0, 17.0)

Compile the solution and resolve any errors.

Testing the Addin

We’ll want to use the experimental instance of VS to avoid conflicts with our running instance. VSIX projects are configured to auto-install the project to the experimental instance. But our package requires CodeRush so it needs to be installed first.

  1. Start up the experimental instance of VS (add the /RootSuffix Exp options to the command).
  2. Open the extensions dialog and find CodeRush for Roslyn.
  3. Install the extension.
  4. Restart VS to install the extension.
  5. Rebuild the VSIX project so it installs properly.

Set a breakpoint in the IsSatisfiedAsync method, set the VSIX as the startup project and debug. Loading VS the first time will be slow as it loads symbols and don’t be surprised if there are several errors that occur. I find that keeping most tool windows closed in the experimental instance speeds things up. You’ll want to do that before starting it in the debugger though.

Now that everything is loaded go to the templates for CodeRush (ensure you are under C#) and add a new one called /xtest. In the Context field click the browse button to view the available contexts. Our context should appear. Unlike previous versions of CodeRush, the current version is able to find our context without any registration.

Our context requires the type of the element to check for so we need to pass it a parameter. To pass it parameters you need to pass the parameters in a comma separated list in parenthesis: [InDocumentationForElement(MethodDeclaration)]. For the template text put something like “In method”. Now create an alternative expansion but use PropertyDeclaration instead. Save the template.

Now open or create a new project. Go to the comments above a method and type /xtest to trigger the expansion. It should hit the breakpoint set earlier. Do the same thing for a property. The expansion should be working correctly.

Adding Providers

Having to pass parameters for common kinds like properties and methods is tedious. You can create derived classes to provide these common overrides. The sample code has several overloads. To reduce code the core provider logic was moved to a base class and the DocumentationFor… providers simply change which element to target. The DocumentationForElementProvider is still provided for the kinds that are not commonly used.

Final Thoughts

The downloadable code has a few additional versions of the provider for the common elements. Other than parsing parameters they behave like the base provider.

This is what I like about CodeRush. Hopefully the documentation will get better, DevExpress will move the dependencies to NuGet and we’ll get some sample templates but as you can see it really isn’t that hard to set up.

You can download the code on Github. There is a built version of the VSIX there as well.