TextMate News

Anything vaguely related to TextMate and macOS.

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.

categories General

10 Comments

04 December 2007

by Ben Perry

not sure if anyone else had this problem, but i had to add:

#!/usr/bin/env ruby

to the top of the command. Otherwise I just keep getting errors.

In Python:

#!/usr/bin/env python
width=20
print ''.join('(?%s:: )' % i for i in range(1, width+1))

Bah - formatting didn’t work. This blog needs preview mode!

Thanks for cleaning up, whoever it was. ;-)

Ben: Yeah, my “script” was meant as a subset of a full snippet-generating script, so I didn’t put any shebang in it.

Simon: I fixed it — if you (or anyone else) know of a preview plug-in for WordPress which works with the Markdown plug-in and is a hassle-free install, let me know — for now there is Edit in TM and Preview from there :)

I’m confused as to why the example given doesn’t output the line above and below the the comment, this version I’ve done does. Bearing in mind I’ve never actually programmed Ruby before it might not be the most efficient way to do it.

#!/usr/bin/env ruby

width = 30

print "/"
width.times { print "*" }
print "\n"
print "${1/^" + "(.)?"*width + ".*$/"
print "*"
1.upto(width) { |i| print "(?#{i}:: )" }
print "/}${1:Header}" + "\n"
width.times { print "*" }
print "/\n"
#!/usr/bin/env ruby

width = 30

print "/", "*"*width, "\n"
print "${1/^", "(.)?"*width, ".*$/*"
1.upto(width) { |i| print "(?#{i}:: )" }
print "/}${1:Header}\n"
print "*"*width, "/\n"

Some alternatives for the ugly 1.upto(width):

print (1..width).map { |i| "(?#{i}:: )" }

width.times { |i| print "(?#{i+1}:: )" }

i = 0
width.times { print "(?#{i+=1}:: )" }

But the whole thing is just too complicated…I would prefer putting Ruby code into the ${…} expressions…like

${ruby:width=[30, $1.size].max}
# ${ruby:'-' * width}
# ${ruby:' ' * (width - $1.size)}${1:Header}
# ${ruby:'-' * width}

and

${ruby:width=[30, $1.size].max}
/*${ruby:'*' * width}*
* ${ruby:' ' * (width - $1.size)}${1:Header}*
**${ruby:'-' * width}*/

for C-style comments.

Anyone have a PHP or CSS translation of this?

As I didn’t understand very well the ruby+regexp+snippets solution, I decided to try to get the same result with a (hopefully simpler) Python-only command:

#!/usr/bin/env python
from sys import stdin

width = 20
s = stdin.read()

print "-" * width
print s.rjust(width)
print "-" * width