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("$TestCategory$"), Owner("$DomainAndUsername$")]
public void $Test_method_name$()
{
 $END$
}"
reformat="True" shortenQualifiedReferences="True">
<Context>
<CSharpContext context="TypeMember" minimumLanguageVersion="2.0" />
</Context>
<Categories>
<Category name="MSTest" />
</Categories>
<Variables>
<Variable name="TestCategory"
expression="list(",Unit,Integration")" 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 hardcode 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.