07.06.06
Ruby Tutorials for TextMate hackers, part 2
In the previous article from this series we learned some basic things about Ruby and how to use it to make TextMate do our bidding. In this post, we’ll see how to use some of TextMate’s Ruby libraries to do more stuff.
Snippeting Away
We’ll create a command similar to the one we talked about last time. This one will take as input the selected text or current line, and will wrap each line in html tags, with a different tab stop to fill each tag. In other words, each line should end up looking like <${1:p}>line text here</${1:p}>, with increasing snippet tab number on each line. Not a very useful command maybe, we’ll learn how to do things along the way.
The first thing to do is to set the output of our command to be “Insert as Snippet”. The command’s name might not say it, but this will in fact replace the text that was inputted to the command, just like the “Replace Selected Text” output option. The only difference is that the output is interpreted as if it were a snippet. So we could do what we need via a command like:
#!/usr/bin/env ruby
i = 1
for line in STDIN.read.split("\n") do
puts "<${#{i}:p}>"+line+"</${#{i}:p}>"
i += 1
end
We use split("\n") instead of readlines so that each line we process doesn’t have the trailing newline. Then the <${#{i}:p}> part becomes <${1:p}> the first time and so on, thus producing our desired snippet behavior.
All is not rosy with our command however. Try it on a line that contains something in backticks, or contains something like $1. What do you think just happened?
Correct, these things got interpreted by the snippet mechanism, as well they should! So, what do we do about it? We have to do what’s called “escaping”. This is often done by putting a backslash in front of the character we want to “escape”. This will cause the snippet mechanism to not interepret that character as something special. So if we manage to make all the backticks into a slash followed by a backtick, and all the dollar signs into \$, and all the backslashes into double backslashes (\\), we should be ok. This is where regular expressions come in. We can do this with this piece of code:
line.gsub(/[$`\\]/, "\\\\\\0")
Ok, so we’ve seen the first part before, we are doing a gsub. We’ve seen what the brackets do, they tell us to match any of the things in them. In our case, we want to match $, a backtick and a slash. But since slashes have special power in regexps (namely they are used for escaping characters), we need to escape them in the regexp, hence the two slashes. (Otherwise, if there was only one slash, the engine would want to interpret the \] part as desiring to match a right bracket).
Then we are telling the engine to replace each such occurrence with the proper thing. The replacement string looks very complicated, so let’s try to explain it. Remember that a slash followed by a number is replaced by the various grouped matches. What we did not see back then is that using 0 gives us back the entire match. Also backslashes are treated as special characters in strings, which means they need to be escaped. So what we are really seeing in the replacement string is two slashes followed by \0. This all happens before the gsub command gets to work with the replacing. The gsub command now takes this and replaces the two slashes with one, and the \0 with the match. So we end up with our match preceded with a slash.
Where is my Library?
If that last paragraph was too complicated, don’t worry because you don’t actually have to understand it, at least not until you decide to. This is where TextMate’s Ruby libraries come in handy. TextMate has a “Support” directory, which has a “lib” subdirectory. This contains some very useful pieces of code. We will be concerned with three files in there, escape.rb, exit_codes.rb and dialog.rb. Depending on whether you have checked this directory out via subversion or not, this directory has different location. Luckily for you, TextMate does the hard work of finding it for you and saving the information in the TM_SUPPORT_PATH variable. So this little program executed as a command will show you this path:
#!/usr/bin/env ruby
print ENV['TM_SUPPORT_PATH']
We haven’t talked about ENV yet, now is the time. When our program executes, there are a number of shell environment variables defined. Some are provided by the system, others, more of interest to us, by TextMate. ENV is Ruby’s way of accessing these variables. It is what is called a “Hash” in Ruby-speak. Think of it as a dictionary: You provide it with a key, like 'TM_SUPPORT_PATH', and it gives you back its value. See the manual section on environment variables for a list of all the various variables that TextMate provides for our enjoyment.
Ok, so now we know how to find the path. The next thing is to use ruby’s require command to load the escape.rb library. We want to load it because it has a command e_sn that will do the “snippet character escaping” we did manually above. In fact, the code we used above is exactly what the e_sn command does. Here is the command we want:
#!/usr/bin/env ruby
require ENV['TM_SUPPORT_PATH'] + "/lib/escape.rb"
i = 1
for line in STDIN.read.split("\n") do
puts "<${#{i}:p}>"+e_sn(line)+"</${#{i}:p}>"
i += 1
end
ENV['TM_SUPPORT_PATH'] returns to us the path to the support directory. We then append the path to the actual file we are after.
escape.rb contains a couple of other escaping functions, which you might want to look into if you are messing with other things, like passing parameters to shell commands, or producing links to be used with a URI scheme, like file://.... or txmt://...., but we won’t get into that in this tutorial.
Talk to me
It’s time to get our user to interact with us. for instance, instead of using p as the default tag, we’ll ask our user to provide us with a tag. The tools to do that are in the dialog.rb library. The library provides a number of methods, whose names basically tell us what they do:
Dialog.request_string, for requesting a string from the user.Dialog.request_secure_string, for requesting a password from the user.Dialog.request_item, for requesting the user to pick from a list of items.Dialog.request_confirmation, for requesting the user to confirm an action.
As an example of how things work, we will now use all but the secure string one. We will first offer the user a list of html tags to pick from. If the user cancels, we will then request then to type in the tag they want. In any case, we will ask the user to confirm their decision. Here is the script:
#!/usr/bin/env ruby
require ENV['TM_SUPPORT_PATH'] + "/lib/escape.rb"
require ENV['TM_SUPPORT_PATH'] + "/lib/dialog.rb"
choices = ["p","h1", "h2", "h3", "a", "li"]
item = Dialog.request_item(:title => "Selecting tag", :prompt => "Please select a tag to use, or Cancel to manually type a tag", :items => choices)
if item.nil? then
item = Dialog.request_string(:title => "Entering tag", :prompt => "Type in a tag:", :default => "p")
end
if item.nil? then
exit
else
confirmed = Dialog.request_confirmation(:title => "Confirming", :prompt => "You selected the tag \"#{item}\". Are you sure?")
end
unless confirmed then
exit
end
i = 1
for line in STDIN.read.split("\n") do
puts "<${#{i}:#{item}}>"+e_sn(line)+"</${#{i}:#{item}}>"
i += 1
end
Pretty long, eh? And it introduced a lot of new things, so let’s take them one at a time. You already know what the first three lines do.
The fourth line defines an array by listing its elements. That’s what the brackets there do. So this line sets the variable choices to hold a list of strings.
The next line calls the fourth library command. It needs to pass it a number of parameters, and the way it does it is through a hash (yes, that’s the same kind of hash as in the ENV case). The way to indentify a hash is through the “key-value” pairs key => value, separated by commas. Normally one needs to enclose the whole thing in braces ({), but because this is passed as an argument into the method, it doesn’t need to.
The thing that looks weird is probably the :title notation. It would perhaps make more sense to say "title" instead, right? What is the : for? This is one of the most misunderstood entities in Ruby. It is called a “symbol”, and you should think of it as a light-weight string. Symbols work well as keys to hashes, for reasons that have partly to do with memory management. But this will have to wait for another article. For now, just use them when you feel like it. Use them mainly for strings that might pop-up over and over, and that are “fixed”, like the words “title” and “prompt” in our example.
So we basically tell the Dialog object, to request an item from the user, providing the given title for the window, the given prompt, and the list of items provided by the choices array. The Dialog object then takes the responsibility of showing the appropriate window, interacting with the user, and finally giving us back an answer.
What is this answer? Well, if the user made a selection, then it is that selection. If the user did not make a selection, or pressed Cancel, then it is nil (Actually, all these commands have a particular behavior if we add a block to them, but we won’t get into that yet).
The next line starts an “if block”, which you are probably familiar with. An if block checks to see if the condition provided to it is true or not, and acts accordingly. Remember, that anything but nil and false is true. In our case, we ask item if it is nil, and act accordingly. Note the question mark at the end of the method name nil?. It is a convention in Ruby that all methods that return a true-false answer should end in a question mark. This line is essentially the same as:
if item == nil then
The next line tells us what happens when the item was indeed nil. We ask the user to type in a string. Since everything here is the same as the request_items case, we’ll just move on. Notice, that next we ask again if the item is nil, and if it is we exit the program. This is the case where the user pressed Cancel twice. The keyword exit is very important, and we’ll come back to it in the next section.
In the else part some interesting things happen. First, note the \" inside the string. Those of you who have been following along carefully will have figured out what it does. It places an actual double quote in the string. Since the double quotes were used to delimiter the string, we need to escape them if we want them to appear in the string. You might ask, why didn’t we use single quotes to delimit the string instead? Because inside single quotes, the #{} thingie does not do evaluation. Try it!
Let’s move to the next interesting line, the one with the unless. unless simply means “if not”. It just reads a bit better, that’s all. Ruby tries to go out of its way to read better, which is great for programmers using it, but a pain for anyone writing a parser for it. Anyway, the rest of the script should already be familiar to you.
Although the script is not that long, we’ll shorten it a bit using another Ruby idiom. Ruby allows you to replace:
if condition then
dothings
end
with
dothings if condition
So with these changes our code can be rewritten as:
#!/usr/bin/env ruby
require ENV['TM_SUPPORT_PATH'] + "/lib/escape.rb"
require ENV['TM_SUPPORT_PATH'] + "/lib/dialog.rb"
choices = ["p","h1", "h2", "h3", "a", "li"]
item = Dialog.request_item(:title => "Selecting tag", :prompt => "Please select a tag to use, or Cancel to manually type a tag", :items => choices)
item = Dialog.request_string(:title => "Entering tag", :prompt => "Type in a tag:", :default => "p") if item.nil?
exit if item.nil?
confirmed = Dialog.request_confirmation(:title => "Confirming", :prompt => "You selected the tag \"#{item}\". Are you sure?")
exit unless confirmed
i = 1
for line in STDIN.read.split("\n") do
puts "<${#{i}:#{item}}>"+e_sn(line)+"</${#{i}:#{item}}>"
i += 1
end
What you use is up to you. Ruby is all about empowering the programmer.
Leaving with Grace
If you tested what happens when we Cancel our command, you will probably have witnessed an unfortunate behavior. Whatever selection we had is gone. Why did that happen?
Well, because we told our program to exit, but had not given it any output. So it sent no output to TextMate. But earlier we had told TextMate to replace the selected text with the output, and so it did! So what we need is a way to tell TextMate that we don’t want it to do that any more.
Luckily for us, TextMate does offer us this power. Our exit command above is allowed to take a number as an argument, like: exit 200 or exit 1 or anything like that. exit 0 means that the program exits normally, and it is the default. Any other number means that the program did not exit normally, and whoever ran the program might want to take that into account. TextMate uses this number to determine what to do, so we can guide its behavior this way. We want to tell it to discard the output. This turns out to be exit code 200. So if you replace all the exit commands above with exit 200, things should work much better.
Well, obviously we don’t want to have to remember these numbers. That’s where another library file comes in, this time called exit_codes.rb. It offers us convenience methods to call instead. They are:
exit_discard, exit_replace_text, exit_replace_document
exit_insert_text, exit_insert_snippet, exit_show_html
exit_show_tool_tip, exit_create_new_document
All but the first one take an optional argument, which is the string to be printed out. They will also take into account anything that your program has already printed. You call these methods by having the object TextMate call them. Here is our program using this:
#!/usr/bin/env ruby
require ENV['TM_SUPPORT_PATH'] + "/lib/escape.rb"
require ENV['TM_SUPPORT_PATH'] + "/lib/dialog.rb"
require ENV['TM_SUPPORT_PATH'] + "/lib/exit_codes.rb"
choices = ["p","h1", "h2", "h3", "a", "li"]
item = Dialog.request_item(:title => "Selecting tag", :prompt => "Please select a tag to use, or Cancel to manually type a tag", :items => choices)
item = Dialog.request_string(:title => "Entering tag", :prompt => "Type in a tag:", :default => "p") if item.nil?
TextMate.exit_discard if item.nil?
confirmed = Dialog.request_confirmation(:title => "Confirming", :prompt => "You selected the tag \"#{item}\". Are you sure?")
TextMate.exit_discard unless confirmed
i = 1
for line in STDIN.read.split("\n") do
puts "<${#{i}:#{item}}>"+e_sn(line)+"</${#{i}:#{item}}>"
i += 1
end
Until Next Time
Well, that’s it for now! Here are a couple of exercises to keep you going until next time. We’ll be able to do a lot more once we learn more about scanning strings and matching regular expressions against them.
- Change the command to do the following. It looks to see whether the environment variable
'TM_MYLIST'is defined. If it is not defined then it continues normally. If it is defined, it’s supposed to be a space-separated list of the various tags. Your command should process this string and set the variable choices to this list of words instead of the default one. Then it should continue with the requests normally. - Create a command that takes as input the entire document, and then offers the user a list of all possible output options, and acts accordingly. Right now this will have to be a long if statement, but later on we might see a quick-and-dirty way to do it using a hash. (Basically, the value of the hash can actually be a method to be called at an appropriate time.)
Later
After thought » Ruby Tutorials for TextMate hackers, part 3 said,
July 7, 2006 at 4:04 pm
[…] In the previous tutorial we saw how to load some basic libraries and interact with the user using those. That post was not so much about Ruby as it was about TextMate. In this post we return to talking about Ruby, in particular some of its methods for manipulating strings. Along the way, we’ll see how to use the txmt: scheme. […]
Paul McCann said,
July 10, 2006 at 9:11 pm
Hi Haris,
thanks from a “Nuby” (my perl’s fine, but my Ruby is somewhat embryonic). You probably want to correct a few occurrences of, ah, “smart” quotes that appear in the code examples. I was getting no output at all from the commands until I finally twigged that each example had
puts ““+line+””
listed in the bundle editor. Them be curlies in there!! Once they were converted to plain double quotes everything ran nicely.
Cheers,
Paul
Haris said,
July 10, 2006 at 10:04 pm
Paul, thanks, I hadn’t realize it. I must be some problem with one of my plugins. It also had made a lot of code disappear.