Dotnet logo

.NET Tools

Essential productivity kit for .NET and game developers

How-To's

Writing plug-ins for ReSharper: Part 2 of N

Finally I’ve managed to get the second part of the post on plug-ins. Sorry for the delay to everyone that was waiting. Appreciate your patience.  And now we’ll resume my holidays!

In the previous part of this series, we saw the basics of how to create a plug-in for ReSharper, install it and run it. We created a context action that would allow us to mark a public method as virtual (where applicable). However, this was done as an explicit action by the user, as such, you didn’t get any kind of hint or suggestion to do this. What we want to do now is make this a hint, so that highlighting appears under methods that could be made virtual. In this part we are going to expand on the same plug-in and convert it into a QuickFix.

What is a QuickFix?

Have you seen the little squiggly lines that appear in Visual Studio?

image

They usually indicate a Suggestion (field can be made read-only), Warning (possible null reference) or Error. ReSharper analyzes and can detect potential issues in the code (similar to what static checker of Code Contracts does). These are known as Highlights and they are related to QuickFixes in that usually a highlight has an QuickFix associated to it, which invokes a context action. This is usually done by placing the cursor on top of the highlighting and press Alt+Enter

image

 

Highlighting Daemons

In the gutter of the Visual Studio editor (right-side), ReSharper displays a series of warnings, errors and hints, which indicate potential issues on a specific file. These issues are detected by background processes known as Daemons. Since what we are looking for is for ReSharper to warn us of existing methods that could be made virtual, what we need to do is somehow hook into these daemons.

image

 

Step by Step Guide

The Daemons in ReSharper use the Visitor pattern to use act on elements, be it code, files, etc. The first step is to implement an IDaemonStage interface, which hold metadata about our daemon stage at at the same time acts as a factory for the actual process we are implementing.

[DaemonStage(StagesBefore = new[]  { typeof(LanguageSpecificDaemonStage) })]
 public class MakeMethodVirtualDaemonStage: IDaemonStage
 {
     public IDaemonStageProcess CreateProcess(IDaemonProcess process, DaemonProcessKind processKind)
     {
         return new MakeMethodVirtualDaemonStageProcess(process);
     }

     public ErrorStripeRequest NeedsErrorStripe(IProjectFile projectFile)
     {
         return ErrorStripeRequest.STRIPE_AND_ERRORS;
     }
 }

There are two main methods to implement. The CreateProcess is what creates the actual process for us and the NeedsErrorStrip which indicates whether this daemon uses the gutter to display strips. The DaemonProcessKind parameter passed into the first method helps us discriminate on when this process should be executed, i.e. only during checking of visible (current) document, during solution wide analysis, etc.

The next step is to implement the process via the IDaemonStageProcess interface:

  public class MakeMethodVirtualDaemonStageProcess : IDaemonStageProcess
  {
      readonly IDaemonProcess _process;

      public MakeMethodVirtualDaemonStageProcess(IDaemonProcess process)
      {
          _process = process;
          
      }

      public void Execute(Action<DaemonStageResult> commiter)
      {
          if (_process.InterruptFlag)
          {
              return;
          }

          var file = _process.ProjectFile.GetPsiFile(CSharpLanguageService.CSHARP) as ICSharpFile;

          if (file != null)
          {
              var highlights = new List<HighlightingInfo>();

              var processor = new RecursiveElementProcessor<IMethodDeclaration>(declaration =>
              {

                  var accessRights = declaration.GetAccessRights();

                  if (accessRights == AccessRights.PUBLIC && !declaration.IsStatic && !declaration.IsVirtual &&
                      !declaration.IsOverride)
                  {
                      var docRange = declaration.GetNameDocumentRange();

                      highlights.Add(new HighlightingInfo(docRange, new MakeMethodVirtualSuggestion(declaration)));
                  }
              });

              file.ProcessDescendants(processor);
              
              commiter(new DaemonStageResult(highlights));
          }

      }

      
  }

The main meat of this class is in the Execute method. We first check to make sure that we’ve not received an interruption (Interrupt Flag raised) due to some external action. Next step is to get access to the current file (remember that we are visiting the entire visible document, not just a specific method). Having the file, we can now create a RecusiveElementProcessor* to perform a tree walk of the AST and perform the specific action on each element. The action to perform is declared as the lambda expression. Since we’re interested in the method declaration, the type is IMethodDeclaration (there are many others). If we look at the expression, we can see that it’s pretty much the same as that of Part 1, the only difference is that we add the results to the highlighting variable.

The HighlightingInfo class has a parameter which can be a Suggestion, Warning or Error, as explained previously. Since in our case we need a suggestion, we pass in the MakeMethodVirtualSuggestion:

[StaticSeverityHighlighting(Severity.SUGGESTION)]
 public class MakeMethodVirtualSuggestion : CSharpHighlightingBase, IHighlighting
 {
     public ICSharpTypeMemberDeclaration Declaration { get; private set; }

     public MakeMethodVirtualSuggestion(ICSharpTypeMemberDeclaration memberDeclaration)
     {
         Declaration = memberDeclaration;
     }

     public string ToolTip
     {
         get { return “Method could be marked as virtual”; }
     }

     public string ErrorStripeToolTip
     {
         get { return ToolTip; }

     }

     public override bool IsValid()
     {
         return Declaration.IsValid();
     }

     public int NavigationOffsetPatch
     {
         get { return 0; }
     }
 }

This class is pretty simple. The main property to define is the ToolTip, which is the text that will show when we hover of the highlighting. The ErrorStripeToolTip is what’s displayed in the right-hand side gutter. Finally the Attribute StaticSeverityHighlighting is to indicate what type of tip it is (Warning, Error, etc.).

 

[*Note: In this case, the operation we want to perform is very simple. If we want a more complex scenario where we need to do some processing before and after each element is visited or have a more fine-grained control, we can implement the IRecurisveElementProcessor. I’ll cover this in another post]. 

 

To recap, right now we would have everything place to display highlighting when a method that could be made virtual is encountered. The only remaining part is to now be able to apply a QuickFix. This is in many ways similar to the ContextAction we saw in Part 1:

[QuickFix]
public class MakeMethodVirtualQuickFix : BulbItemImpl, IQuickFix
{
    readonly MakeMethodVirtualSuggestion _highlighter;

    // Takes as parameter the Highlighter the quickfix refers to
    public MakeMethodVirtualQuickFix(MakeMethodVirtualSuggestion highlighter)
    {
        _highlighter = highlighter;
    }

    // In the transaction we make the necessary changes to the code
    protected override Action<ITextControl> ExecuteTransaction(ISolution solution, IProgressIndicator progress)
    {
        _highlighter.Declaration.SetVirtual(true);

        return null;
    }

    // Text that appears in the context menu
    public override string Text
    {
        get { return “Make Method Virtual”; }
    }

    // Indicates when the option is available
    public bool IsAvailable(IUserDataHolder cache)
    {
        return _highlighter.IsValid();
    }
}

The MakeMethodVirtualQuickFix needs to implement the IBulbItem and IQuickFix interfaces. For ease of implementation we can inherit from BulbItemImpl. The constructor should take as parameter always the actual highlighting that has given way to invoking the QuickFix, in our case the MakeMethodVirtualSuggestion. Similar to the ContextAction we implemented in Part 1, the actual fix itself is pretty trivial. All we need to do is make the method virtual. How do we get access to the method? The easiest way is via the Declaration property of the highlighting passed in (this is a property we added before). The only thing left is to call the SetVirtual method on it. Since we are in the ExecuteTransaction method, ReSharper makes sure that any change made is executed as a whole.

The rest of the properties are trivial. Text returns the text of the QuickFix (what appears in the menu), and IsAvailable indicates when the QuickFix is available, which in our case is whenever the highlighting is valid.

 

The End Result

Once we compile the plug-in and place it in the corresponding Plugins folder under ReSharperBin, we’re done. Here’s the end result:

image

and invoking Alt+Enter on the highlighting gives us:

 

image

 

Summary

Extending ReSharper to create highlightings and quick fixes is pretty simple once you understand how all the pieces fall into place. Most of the code will usually be the same and what will vary will be the actual element processing to be performed and the corresponding QuickFix. As mentioned previously (in the Note), for complex scenarios, we can have more control over the tree walk and that’s something we’ll examine in a future post.

I’ve placed the code up on my github account so feel free to download it, play with it and ping me if you have any comments or questions. The code is updated to work with ReSharper 5.1

[Thanks to Howard for his valuable input]

image description