Some context
I'm implementing a self-closing pairs auto-completion feature in Rubberduck. One of the challenges is that the VBIDE API doesn't allow editing a line of code by "injecting" characters: we have to replace the entire line, and a side-effect of this is that it causes the VBE to parse/compile the line (since it's no longer "being edited"), which means a partial line of code like this:
msgbox(
Turns into this:
MsgBox (
And there's nothing we can do about it. Obviously this causes a serious problem with determining where the caret is supposed to be - ok maybe not in this particular case, but say you have this (where |
denotes caret position):
dosomething |, foo
If you type a "
here, what we want to do is to end up with this:
dosomething "|", foo
But we need to account for the VBE's "prettifying", and end up with this instead:
DoSomething "|", foo
So I needed a way to get the VBE to "prettify" the original code, while not losing track of where the caret needs to be - which is fundamental if I don't want Rubberduck's auto-completion to get in the way of typing code in the editor.
The Solution
So I wrote an ICodeStringPrettifier
interface, and implemented it like this:
public class CodeStringPrettifier : ICodeStringPrettifier
{
private readonly ICodeModule _module;
public CodeStringPrettifier(ICodeModule module)
{
_module = module;
}
public CodeString Prettify(CodeString original)
{
var originalCode = original.Code;
var originalPosition = original.CaretPosition.StartColumn;
var originalNonSpacePosition = 0;
for (var i = 0; i < originalPosition; i++)
{
if (originalCode[i] != ' ')
{
originalNonSpacePosition++;
}
}
_module.DeleteLines(original.SnippetPosition.StartLine);
_module.InsertLines(original.SnippetPosition.StartLine, originalCode);
var prettifiedCode = _module.GetLines(original.SnippetPosition);
var prettifiedNonSpacePosition = 0;
var index = 0;
for (var i = 0; i < prettifiedCode.Length; i++)
{
if (prettifiedCode[i] != ' ')
{
prettifiedNonSpacePosition++;
if (prettifiedNonSpacePosition == originalNonSpacePosition)
{
index = i;
break;
}
}
}
return new CodeString(prettifiedCode, new Selection(0, index + 1));
}
}
Where CodeString
is a struct
that is a little more than a formalized (string,Selection)
value tuple, that uses the pipe character exactly as shown at the top of this post, to denote the caret position in a code string - which is extremely handy for testing... but then, using this type with actual VBA code that includes an actual pipe character is problematic (#4373), but fixing that isn't in scope here.
I've written a few tests for the prettifier, and they all pass:
[TestFixture]
public class PrettifierTests
{
[Test][Category("AutoComplete")]
public void GivenSamePrettifiedCode_YieldsSameCodeString()
{
var original = "MsgBox (|".ToCodeString();
var module = new Mock<ICodeModule>();
module.Setup(m => m.GetLines(original.SnippetPosition)).Returns(original.Code);
var sut = new CodeStringPrettifier(module.Object);
var actual = sut.Prettify(original);
Assert.AreEqual(original, actual);
}
[Test][Category("AutoComplete")]
public void GivenTrailingWhitespace_PrettifiedCaretIsAtLastCharacter()
{
var original = "MsgBox |".ToCodeString();
var prettified = "MsgBox";
var expected = "MsgBox|".ToCodeString();
var module = new Mock<ICodeModule>();
module.Setup(m => m.GetLines(original.SnippetPosition)).Returns(prettified);
var sut = new CodeStringPrettifier(module.Object);
var actual = sut.Prettify(original);
Assert.AreEqual(expected, actual);
}
[Test]
[Category("AutoComplete")]
public void GivenExtraWhitespace_PrettifiedCaretStillAtSameToken()
{
var original = "MsgBox (\"test|\")".ToCodeString();
var prettified = "MsgBox (\"test\")";
var expected = "MsgBox (\"test|\")".ToCodeString();
var module = new Mock<ICodeModule>();
module.Setup(m => m.GetLines(original.SnippetPosition)).Returns(prettified);
var sut = new CodeStringPrettifier(module.Object);
var actual = sut.Prettify(original);
Assert.AreEqual(expected, actual);
}
}
Since this appears to work exactly as I need it, I'm going to start tweaking the self-closing pairs auto-completion feature to use it - now while it works, for a prettifier, I don't find the implementation particularly pretty. Ideas?