Right-Aligned Snippet Placeholders
The other day Abhi Beckert asked (on IRC) how to ensure right-aligned text in a snippet. That is, after the snippet has been first inserted, it reads:
# --------------------
# Header
# --------------------
Here Header
is a placeholder which we can overtype. When we fill in the actual header name (for example Configuration
) the text should be formatted like:
# --------------------
# Configuration
# --------------------
The trick to solving this problem is by using conditional insertions in the replacement string.
First let’s briefly summarize snippet placeholders and mirrors.
If we do a snippet like this:
# --------------------
# ${1:Header}
# --------------------
We use just a placeholder, that is, we can overtype the Header
string after having inserted the snippet.
A mirror is using $1
some other place in the snippet. This will then mirror what we type. We can run a replacement on this mirror to make it more interesting, for example we can replace each character with a dash, and use that technique to have the dashed lines above/below the text line match in length, for example by doing:
# ${1/./-/g}
# ${1:Header}
# ${1/./-/g}
Getting back to right-alignment. Here what we want to do is similar to the above. I.e. we want to insert spaces based on what is entered in the placeholder.
But with the construction of the dashed lines, we want one dash for each letter entered, and so we can use a simple replacement. In the case of right-alignment, we actually want one space for each letter not typed.
So to get started, here is the outline of what we want:
# --------------------
# ${1/…/…/}${1:Header}
# --------------------
The ${1/…/…/}
is the interesting bit. If the user types zero characters, we want it to output 20 spaces. If the user types 1 character, we want 19 spaces, etc.
So while the result of the transformation is dependent on text entered, there isn’t the simple 1:1 relationship which we could exploit for the dashed lines.
This is where conditional insertions are useful. What they allow you to do is optionally match something, and based on whether or not something was actually matched, insert something in the replacement.
This works by testing capture registers, that is, if our search is for (foo)?
then capture register 1 can be tested for whether or not we matched foo
. Testing this is done by using (?1:bar)
in the replacement string. Here we insert bar
only when capture register 1 captured something. We can also have text inserted if it did not match, this is done using (?1:bar:baz)
, now bar
is inserted if we matched foo
, otherwise baz
is inserted.
We concluded above that for each character not matched, we want to insert a space. So conceptually we try to match 20 characters, using a capture register for each of them, and for those not matched, we insert a space.
In this example I will only try to match 5 letters, as the pattern should be clear from that:
^(.)?(.)?(.)?(.)?(.)?.*$
If the string is foo
we have capture register 1-3 filled and 4-5 empty, and we want to insert a space for those last two.
So our replacement becomes:
(?1:: )(?2:: )(?3:: )(?4:: )(?5:: )
So for each capture register (1-5) insert nothing if there is a match, otherwise insert a space.
Writing this out for a 20 character wide field might be tedious, so we can instead do a command with output set to “Insert as Snippet” which would use code similar to the following to generate the mirror and placeholder:
width = 20
print "${1/^" + "(.)?"*width + ".*$/"
1.upto(width) { |i| print "(?#{i}:: )" }
print "/}${1:Header}" + "\n"
You can also pre-generate the snippet, but a command makes it easy to later tweak the field lengths.