Tuesday, March 14, 2006

Writing a DXCore Plugin

As part of evaluating the CodeRush and Refactor! Pro products from Developer Express, I decided that I wanted to write my own plugin. Both of these products are based on the DXCore API, which is the layer that allows access to Visual Studio and the source code that you are editing. DXCore is freely available, and any plugin that you write will always work (i.e., no timeout/expiration periods).

I use copy/paste A LOT when writing code. Often, I will create something outside of Visual Studio, like a XML document or a piece of JavaScript code, that I then need to use within my code as a string. So, I would find myself pasting the text into my code, and then manually wrapping each line with some sort of string-building syntax:

<xml>
<element1 />
<element1 />
</xml>
would become:
string xml = "<xml> \n" 
+ "<element1 /> \n"
+ "<element1 /> \n"
+ "</xml>";

What I wanted to do for my first DXCore plugin was to be able to highlight a range of text in Visual Studio, hit some key sequence, and then have the string-building code automatically added for me. Instead of using string concatenation, I wanted to wrap everything in StringBuilder code. Sounded simple enough.

Following Mark Miller's example on dnrTV, I came up with the following action:
StringBuilder code = new StringBuilder();

TextSelection selection = CodeRush.Selection.Active;

code.AppendLine("System.Text.StringBuilder ___sb = new System.Text.StringBuilder();");

foreach (string s in selection.Lines)
{
code.AppendLine("___sb.AppendLine(\"" + s.Replace("\"", "\\\"") + "\");");
}

selection.Replace(code.ToString(), true);
This worked nicely, except the output was restricted to C#, and it didn't have all of the nice touchy-feely stuff that CodeRush and Refactor! gives you.

Trying to figure out the DXCore API was a little overwhelming, so, I approached Mark via email for some pointers of how to use DXCore to generate output in whatever language you're using (VB.NET or C#). A couple of emails later, and he finally understood what this plugin was trying to do. Note to self: always provide clear examples when talking to Miller.

Much to my surprise, instead of just pointing me to a couple of classes or methods, he returned a fully functional piece of source code to me that I could then study, complete with Undo functionality, language-independent code generation, and the cool caret and linking stuff that you come to expect when working with DXCore products:
private string CreateTextCommand(string command)
{
return CodeRush.Constants.TextCommandBegin + command + CodeRush.Constants.TextCommandEnd;
}

private string CreateLink(string linkName)
{
return CreateTextCommand(String.Format("Link({0})", linkName));
}

private string Caret
{
get
{
return CreateTextCommand("Caret");
}
}

private string BlockAnchor
{
get
{
return CreateTextCommand("BlockAnchor");
}
}



private void actWrapText_SB_Execute(ExecuteEventArgs ea)
{
WrapSelectionInStringBuilder();
}

public string GenerateStringBuilderWrapper(string text)
{
ElementBuilder eb = new ElementBuilder();

string variableName = CreateLink("__sb");

eb.AddInitializedVariable(null, "StringBuilder", Caret + variableName + BlockAnchor,
new ObjectCreationExpression(new TypeReferenceExpression("StringBuilder")));

string[] lines = text.Split('\r');

foreach (string lineToAdd in lines)
{
string trimmedLine = lineToAdd.Trim();

ExpressionCollection expressionCollection = new ExpressionCollection();

expressionCollection.Add(new PrimitiveExpression("\"" + trimmedLine + "\""));

eb.TopLevelElements.Add(eb.BuildMethodCall("Append", expressionCollection, variableName));
}
return eb.GenerateCode();
}

private void WrapSelectionInStringBuilder()
{
TextDocument activeDoc = CodeRush.Documents.ActiveTextDocument;

if (activeDoc == null)
return;

TextView activeView = activeDoc.ActiveView;

if (activeView == null)
return;

TextViewSelection activeSelection = activeView.Selection;

if (activeSelection == null)
return;

string replaceCode = GenerateStringBuilderWrapper(activeSelection.Text);

DevExpress.CodeRush.Interop.OLE.Helpers.ParentUndoUnit lParentUnit =
CodeRush.UndoStack.OpenParentUnit("Text to StringBuilder", true);

try
{
activeDoc.Selection.Delete();

SourceRange rangeExpanded = activeDoc.ExpandText(CodeRush.Caret.SourcePoint, replaceCode);

activeDoc.Format(rangeExpanded);

CodeRush.Source.DeclareReference("System.Text");
}

finally
{
CodeRush.UndoStack.CommitParentUnit(lParentUnit);
}
}