21 August 2010

ReSharper’s Live Templates can do the repetitive work for you

I was inspired by Hadi Hariri's blog post about creating custom Live Templates to explore one of the features of ReSharper that I really regret not digging into before. Live Templates lets you create simple but intelligent code snippets that makes you more productive as a developer.

I’m not a fan of generating repetitive code, but I think generating repetitive structures is a great time saver that still keeps the code clean. One of the structures I write over and over all the time is new unit test methods, and I need a template that supports all relevant attributes for MSTest and Team Foundation Server integration. I also wanted to make it easier to write readable test method names by allowing me to use space instead of underscore to separate words.

Jimmy Bogard blogged about a similar solution that integrates with AutoHotKey to replace spaces with underscores while writing test method names. ReSharper macros are very powerful so I created a macro that ensures a given template variable is transformed to a valid C# identifier while typing (one limitation is that this macro doesn’t support Unicode escape characters):
using System.Text;
using JetBrains.ReSharper.Feature.Services.LiveTemplates.Macros;

namespace JoarOyen.Tools.ReSharper.Macros
{
    [Macro("JoarOyenLiveTemplateMacros.ValidIdentifier",
      ShortDescription = "Ensures that given variable is a valid C# identifier",
      LongDescription = 
        "Replaces invalid characters in a C# identifier with underscores")]
    public class ValidIdentifierMacro : QuickParameterlessMacro
    {
        public override string QuickEvaluate(string value)
        {
            return TransformToValidIdentifier(value);
        }

        private static string TransformToValidIdentifier(string value)
        {
            if (string.IsNullOrEmpty(value)) return value;

            var validIdentifier = new StringBuilder(value);

            PrefixWithUnderscoreIfNotStartingWithACharacter(validIdentifier);
            ReplaceInvalidCharactersWithUnderscore(validIdentifier);

            return validIdentifier.ToString();
        }

        private static void PrefixWithUnderscoreIfNotStartingWithACharacter(
            StringBuilder validIdentifier)
        {
            if (!char.IsLetter(validIdentifier[0]) && validIdentifier[0] != '_')
            {
                validIdentifier.Insert(0, '_');
            }
        }

        private static void ReplaceInvalidCharactersWithUnderscore(
            StringBuilder validIdentifier)
        {
            for (int i = 0; i < validIdentifier.Length; i++)
            {
                if (!char.IsLetterOrDigit(validIdentifier[i]))
                {
                    validIdentifier[i] = '_';
                }
            }
        }
    }
}

This is very simple, isn’t it? :)

ReSharper requires all macros to implement IMacro, and I’ve created an abstract class that implements a simplified version of macros. The only thing the concrete macro class needs to implement is the QuickEvaluate method that is called after a key is pressed. It’s responsibility is to transform the current value to the desired result that is then returned to ReSharper:
using System.Collections.Generic;
using JetBrains.ReSharper.Feature.Services.LiveTemplates.Hotspots;
using JetBrains.ReSharper.Feature.Services.LiveTemplates.Macros;

namespace JoarOyen.Tools.ReSharper.Macros
{
    public abstract class QuickParameterlessMacro : IMacro
    {
        public abstract string QuickEvaluate(string value);

        public ParameterInfo[] Parameters
        {
            get { return new ParameterInfo[] { }; }
        }

        public HotspotItems GetLookupItems(
            IHotspotContext context, IList<string> arguments)
        {
            return null;
        }

        public string GetPlaceholder()
        {
            return "a";
        }

        public string EvaluateQuickResult(
            IHotspotContext context, IList<string> arguments)
        {
            var currentHotspot = new CurrentHotspot(context.HotspotSession);
            return QuickEvaluate(currentHotspot.Value);
        }

        public bool HandleExpansion(
            IHotspotContext context, IList<string> arguments)
        {
            context.HotspotSession.HotspotUpdated += HotspotSessionHotspotUpdated;
            return false;
        }

        private static void HotspotSessionHotspotUpdated(
            object sender, System.EventArgs e)
        {
            var currentHotspot = new CurrentHotspot((HotspotSession)sender);
            currentHotspot.InvokeEvaluateQuickResult();
        }
    }
}

I discovered an issue (or maybe it’s by design?) with EvaluateQuickResult because it wasn’t called for each key press In the hotspot it was assigned to (key presses in other hotspots did trigger it though). As a workaround I attached an event handler to HotspotUpdated that invokes this method so that spaces can be replaced on the fly.

I’ve also created a class that has the responsibility of working with the current hotspot:
using JetBrains.ReSharper.Feature.Services.LiveTemplates.Hotspots;
using JetBrains.TextControl;
using JetBrains.TextControl.Coords;

namespace JoarOyen.Tools.ReSharper.Macros
{
    public class CurrentHotspot
    {
        private readonly HotspotSession _hotspotSession;
        private ITextControlPos _caretPosition;

        public CurrentHotspot(HotspotSession hotspotSession)
        {
            _hotspotSession = hotspotSession;
        }

        public void InvokeEvaluateQuickResult()
        {
            SaveCaretPosition();
            _hotspotSession.CurrentHotspot.QuickEvaluate();
            RestoreCaretPosition();
        }

        public string Value
        {
            get
            {
                return _hotspotSession.CurrentHotspot == null ?
                    null : _hotspotSession.CurrentHotspot.CurrentValue;
            }
        }

        private void SaveCaretPosition()
        {
            _caretPosition =
                _hotspotSession.Context.TextControl.Caret.Position.Value;
        }

        private void RestoreCaretPosition()
        {
            _hotspotSession.Context.TextControl.Caret.MoveTo(
                _caretPosition, CaretVisualPlacement.Generic);
        }
    }
}

A side issue of calling EvaluateQuickResult for each key press is that the caret is moved to the end of the current hotspot. This piece of code saves the current caret position before modifying the hotspot and restores it afterwards.

Using these classes, the macro for getting the current user name with domain is dead simple:
using System.Threading;
using JetBrains.ReSharper.Feature.Services.LiveTemplates.Macros;

namespace JoarOyen.Tools.ReSharper.Macros
{
    [Macro("JoarOyenLiveTemplateMacros.DomainAndUsername",
      ShortDescription = "Current username with domain",
      LongDescription = 
        "Current username with domain on the format <Domain>\\\\<Username>")]
    public class DomainAndUsernameMacro : QuickParameterlessMacro
    {
        public override string QuickEvaluate(string value)
        {
            return Thread.CurrentPrincipal.Identity.Name.Replace("\\", "\\\\");
        }
    }
}

To tie everything together, here is the Live Template that generates a new test method:
<TemplatesExport>
  <Template uid="0fb9eda3-89fa-4507-84c9-8d8b6847e66f" shortcut="tm"
    description="Creates an MS Test method" text="[TestMethod, TestCategory(&quot;$TestCategory$&quot;), Owner(&quot;$DomainAndUsername$&quot;)]&#xD;&#xA;public void $Test_method_name$()&#xD;&#xA;{&#xD;&#xA;    $END$&#xD;&#xA;}"
    reformat="True" shortenQualifiedReferences="True">
    <Context>
      <CSharpContext context="TypeMember" minimumLanguageVersion="2.0" />
    </Context>
    <Categories>
      <Category name="MSTest" />
    </Categories>
    <Variables>
      <Variable name="TestCategory"
        expression="list(&quot;,Unit,Integration&quot;)" initialRange="0" />
      <Variable name="DomainAndUsername"
        expression="JoarOyenLiveTemplateMacros.DomainAndUsername()"
        initialRange="-1" />
      <Variable name="Test_method_name"
        expression="JoarOyenLiveTemplateMacros.ValidIdentifier()" initialRange="0" />
    </Variables>
    <CustomProperties />
  </Template>
</TemplatesExport>
This template hardcodes my two preferred test categories, but that can be improved by implementing another macro.


Update: The complete code for the solution is pushed to GitHub http://github.com/joaroyen/ReSharperExtensions.

4 comments:

  1. I've been using this for a while now, and I really like it. I created templates for things like TestMethod (TM), TestClass (TC), etc. Then I tried to build a general-purpose template for just the valid identifier part, and I've run into a problem. When I'm inside a method, Auto-complete is still running, so when I hit the spacebar, it wants to guess what class/method/whatever I'm talking about, and fill it in. Do you know any way to get the valid identifier macro to turn off intellisense while it's in effect?

    ReplyDelete
  2. I just installed ReSharper 6.0 beta 2 and have been poking around the Live Template API but haven’t seen any way of controlling other parts of ReSharper while the macros are running. I wouldn’t be surprised if there exist an integration point in another part of the R# API though.

    In R# 6 Beta 2, JetBrains have removed an event I used to reevaluate the content of the hotspot and replace spaces while typing, and I’m working on finding another workaround to make the ValidIdentifierMacro work the way I want with R# 6.

    ReplyDelete
  3. Been playing around with a Live Template today and this little part really bugs me.

    Hope you find a solution and I will get a notification mail if you do.

    ReplyDelete
  4. @Syska If you're thinking about the HotspotUpdated event that were missing in R# 6 Beta 2, JetBrains has added it back to the IHotspotSession interface in the release candidate.

    I reported it to JetBrains as a breaking change, and I'm happy to say that they promptly responded to my request and fixed the next build of R#. The caret behavior has changed a bit in R# 6, so I was also able to remove some code that no longer is necessary. The repository on GitHub is updated with these changes.

    ReplyDelete