MacDevCenter    
 Published on MacDevCenter (http://www.macdevcenter.com/)
 See this if you're having trouble printing code examples


Customizing TextMate

by James Edward Gray II
04/18/2007

Editor's Note: James Edward Gray II is the author of TextMate: Power Editing for the Mac (Pragmatic Programmers). We know there is a lot of interest in TextMate from MacDev Center readers, so we asked James to write up an example of how to customize TextMate. If you want to know more about this powerful programmer-friendly editor, I highly recommend getting your hands on a copy of TextMate: Power Editing for the Mac.

I'm going to assume a few things about you as the reader, since you are reading this article on this site. First, as a Mac developer, I'll assume you know what TextMate is. Beyond that, I'm going to assume you have seen the screencasts and read the blog posts, so you've got the basics and you've seen plenty of cute tricks. Finally, I'm going to assume you are comfortable with reading things like regular expressions and Ruby code.

Given all of that, let's dive right into some dramatic customization of TextMate. We will stretch the rules a bit just so we can get an idea of what is possible. Something like this exercise should also be fun though, so let's turn TextMate into an RPN calculator.

Some TextMate automations interact with the user via GUI dialog boxes they display. Others show the user a web page with links that can trigger actions. We will take the road less traveled, though, and ask TextMate to respond to normal typing operations, turning it into a semi-interactive environment. This wouldn't be ideal for all commands, but it could be a useful technique for creating tools like a TextMate-savvy IRb (Interactive Ruby) clone or the shell worksheets some other editors have. It also makes fun calculators.

What Is an RPN Calculator?

If you keep an HP calculator on your desk, you can safely skip ahead to the next section. The rest of you should know that RPN stands for Reverse Polish Notation, which is a unique approach to solving math problems.

Very briefly, RPN is a way of doing math problems with the help of a stack. You can think of a stack as some slots you can push numbers onto and pop numbers off of. The first number off will always be the last one pushed on. For example, an empty stack might look like this:

3:
2:
1:

Now we can push a number onto this stack and it will look like this:

3:
2:
1:  100

If we push another number onto the stack, it becomes:

3:
2:  100
1:   25

Math operations pop an operand or two off the stack, calculate an answer, and push that answer back onto the stack. For example, if we now trigger a subtraction operation on our stack, it changes to:

3:
2:
1:  75

Some people prefer this method of doing math because you never need to use parentheses, and, as your skill grows with the system, you learn some great shortcuts for shaving off steps.

A Simple Grammar

The first step to transforming TextMate into an environment where we can play with an RPN stack is to build a Language Grammar for RPN documents. There are a couple of good reasons for us to do this.

The most important is that we are going to modify the behavior of some normal keys on your keyboard like "plus" and Enter. You don't want those changes to bother you next time you sit down to write some HTML or whatever else, so we need to restrict these changes to only affect the calculator mode we are about to build.

The other reason is that we can syntax highlight our RPN stack. This isn't strictly needed for the simple cases we will be working with, but it's easy enough that we might as well cover that, too.

Now I often hear people discuss TextMate's Language Grammars as if they are heavy voodoo. It's true that they can grow into pretty complex structures--and some that ship with TextMate certainly have--but you might be surprised what you can do with just a simple grammar. Here's a quick something I threw together for our RPN calculator mode:

{   scopeName = 'source.rpn';
    fileTypes = ( 'rpn' );
    patterns = (
        {   name = 'invalid.illegal.error.rpn';
            match = '^!.+!';
        },
        {   name = 'variable.language.rpn';
            match = '^\d+(?=:\s*)';
        },
        {   name = 'constant.numeric.rpn';
            match = '-?(?:0|[1-9]\d*)(?:\.\d+(?:[eE][+-]?\d+)?)?';
        },
    );
}

TextMate's Language Grammars are written in Apple's plist file format, which is a declarative syntax we use to assign values to names TextMate cares about. For example, the first line tells the editor we need a new type of document and these documents will live in a source.rpn scope.

Scopes are a central concept to TextMate. They allow the editor to recognize things it reads and assign names to those things. Then, when we make changes to TextMate's behaviors, we can limit our changes to affect only those areas matching names we describe. That's how we'll make sure we don't break the "plus" key globally.

The second line of the grammar gives TextMate one way to recognize RPN files: those files ending with an .rpn extension. This isn't a must, since you can always change a document's grammar manually, but it allows you to save an RPN scratch pad and have TextMate recognize it when you reopen it.

The final bit of the grammar is a collection of patterns. These apply scopes to pieces of an RPN document. Here I'm using the most basic form these patterns can take: a regular expression that matches something and a name to call the content it matches. I used traditional TextMate names for the three constructs I wanted to match so I could get free syntax highlighting from the themes TextMate ships with. Those themes look for and color these scopes.

I told you what all that code does, but not where to put it. Let's fix that. Here's how to start your very own RPN bundle, a group of extensions for TextMate:

  1. Open the Bundle Editor by choosing Show Bundle Editor in the Bundle Editor submenu of the Bundles menu.
  2. Create a New Bundle by selecting that option from the Plus menu button in the lower-left corner of the window.
  3. Type a name for the bundle. I used "RPN."
  4. Go back into the Plus menu button and select New Language. Name the new grammar RPN and give it the traditional Key Equivalent of Control-Option-Shift-R, so you can switch to it by keyboard.
  5. Paste the grammar code above into the body of the window and then close it.

You should now be able to create a new TextMate document and switch to the RPN grammar using the language menu at the bottom of every editing window. You may also want to verify that you are now in the source.rpn scope by selecting Show Scope from the TextMate submenu of the Bundles menu. We're now ready to give TextMate some new behaviors.

Wrapping Some Code

Now that we can create RPN documents, we'd like for them to be able to actually do calculations. The first step in making that happen is to create a little script that can read our current RPN stack, make changes to it, and write it back out. I'm going to put one together in Ruby because that's my favorite language, but you could use any language you are comfortable with. Here's some basic code for reading and writing the stack:

#!/usr/bin/env ruby -w

require "#{ENV['TM_SUPPORT_PATH']}/lib/escape"

class Stack
  def initialize(io)
    @stack = Array.new
    @raw   = io.read
    @error = nil

    push
  end

  def push
    @raw.each do |line|
      line.sub!(/\A\s*\d+:\s*(.+?)\s*\Z/, "\\1")
      case line
      when /\A-?\d+\Z/
        @stack << Integer(line)
      when /\A-?\d*\.\d*\Z/
        @stack << Float(line.sub(/\A(-?)\./, "\\10.").sub(/\.\Z/, ".0"))
      end
    end
  end

  def print(io)
    if @error
      io.puts e_sn("!#{@error} Error!")
      io.puts
      io.puts e_sn(@raw.sub(/\A(?:!.+!\n)*(?:[\t ]*\n)*/, ""))
    else
      high_level  = [@stack.size, 9].max
      label_width = high_level.to_s.size
      fields      = @stack.map { |f| f.inspect }.reverse
      field_width = fields.map { |f| f.size }.max

      high_level.downto(1) do |i|
        io.printf "%#{label_width}d: %#{field_width}s\n",
                  i, e_sn(fields[i - 1])
      end
    end
    io.print "${0}"
  end
end

This just creates a Stack object that we can do our manipulations on. The actual implementation is just an Array, since Ruby's Arrays can easily behave as a stack. The object also stores the raw input and error messages, in case it runs into problems.

The push() method reads our stack back in from the input TextMate passes us when it triggers our script. It just pulls off stack-level markers and turns the numbers into actual Ruby objects.

The print() method does the opposite, writing our new stack back out to TextMate. When there is an error, the error message and input we received are sent back. Otherwise, the code calculates some widths so it can print the stack, then does the writing.

Most output is snippet escaped by a helper library that ships with TextMate. This is important because we want to have the command write out a snippet instead of just normal text. Snippets have a couple of uses, but in this case we just want to be able to insert a ${0} tab stop variable, so TextMate will move the caret to the bottom of the stack.

Again, we need to save this code now that we know what it does. Where you save it is important. We need it to be inside our RPN bundle so TextMate can find it when we need it. TextMate will have placed your bundle in ~/Library/Application Support/TextMate/Bundles, in a folder called RPN.tmbundle. Create a Support folder inside the bundle and a lib folder inside that. You should be able to do this by feeding the Terminal the following line:

mkdir -p ~/Library/Application\ Support/TextMate/Bundles/RPN.tmbundle/Support/lib

With the directory created, save this code into it and name it "stack.rb" so we can find it when we wrap it in a TextMate command. In fact, let's do that wrapping now:

  1. Go back into the Bundle Editor and click once on the name of your RPN bundle to highlight it.
  2. Select New Command from the Plus menu button and name it Push.
  3. Set a Key Equivalent of enter and set the Scope Selector to source.rpn so we don't affect any other documents.
  4. Set the command Input to Selected Text or Document and Output to Insert as Snippet. This asks TextMate to hand us the entire document on STDIN and replace it with the snippet we will print to STDOUT after our command is run.

When you get that far, you will be ready for the code of the command itself. All we need to do here is load and print the stack, so this little chunk of code will do:

#!/usr/bin/env ruby -w

require "#{ENV['TM_BUNDLE_SUPPORT']}/lib/stack"

stack = Stack.new($stdin)
stack.print($stdout)

At this point, you should now be able to push values onto the stack. Close the Bundle Editor and go back to your RPN document. Try entering any number and pushing the Return key. The stack should appear and show what you entered. Try pushing another number.

Adding Math Operations

The good news is that we've done all the hard work already just by setting everything up. The rest is pretty trivial. First, let's go back into our Ruby Stack class and add some methods for stack and math operations:

class Stack
  def dup;  unary_operation { |stack, f| stack << f << f }     end
  def pop;  unary_operation { }                                end
  def swap; binary_operation { |stack, l, r| stack << r << l } end

  def add;      binary_operation { |stack, l, r| stack << l + r } end
  def subtract; binary_operation { |stack, l, r| stack << l - r } end
  def multiply; binary_operation { |stack, l, r| stack << l * r } end
  def divide
    binary_operation do |stack, l, r|
      if r.zero?
        @error = "Division by Zero"
      elsif (l % r).nonzero?
        stack << l / r.to_f
      else
        stack << l / r
      end
    end
  end

  private

  def unary_operation
    if @stack.empty?
      @error = "Stack Underflow"
    else
      yield(@stack, @stack.pop)
    end
  end

  def binary_operation
    if @stack.size < 2
      @error = "Stack Underflow"
    else
      right, left = @stack.pop, @stack.pop
      yield(@stack, left, right)
    end
  end
end

These should be pretty straightforward. A couple of helper methods are used to pull the needed number of operands off the stack. Each method then uses those numbers and places results on the stack when needed. The divide() method is the only complicated one, and that's because I need to detect division by zero errors and force Ruby into floating-point arithmetic as needed.

When you have this code in place, make a small edit to your Push command, adding the following line between the code that creates the Stack and the line that prints it:

stack.dup if ENV["TM_CURRENT_WORD"].to_s.empty?

This will allow you to duplicate the first entry on the stack just by pressing Enter on a blank line. That's a super handy trick when entering expressions. The code works by checking an environment variable TextMate sets before running your command to see whether you have entered any content on that line yet. TextMate provides a good deal of information to the automations you build through variables like this.

Finally, we need to wrap the other methods in TextMate commands. These new commands are almost identical, so I'm only going to show one of them. Here's the code for a Subtract command:

#!/usr/bin/env ruby -w

require "#{ENV['TM_BUNDLE_SUPPORT']}/lib/stack"

stack = Stack.new($stdin)
stack.subtract  # change this call for the other commands
stack.print($stdout)

This command should have the same Input, Output, and Scope Selector of the Push command and TextMate has a shortcut to help with that. With the old command selected in the Bundle Editor, use the double Plus button to duplicate it. Then just change the name, code, and Key Equivalent. I used - for the Key Equivalent on this one, though that does make entering negative numbers a hassle.

Here's a list of the other commands I created, the Key Equivalent I set for them, and the code I used to call into my library:

Ready to try out your new calculator? Great. Try to find the answer to this expression using the stack:

(4 * 6 - 5) / (1 + (4 * 6 - 5))

I did it with the following keystrokes:

4 enter 6 * 5 - enter 1 + /

Summary

Feel free to download the RPN bundle we created in this article and play around. You can install this bundle by double-clicking it or dragging it onto the TextMate application icon. Try adding in some of your own ideas to get practice customizing TextMate.

While this exercise isn't very sophisticated, my hope is that it gives you ideas about how you might customize TextMate for your own unique purposes. As you can see, it doesn't take very much effort to make some pretty drastic changes.

TextMate has many, many more options for customization than I could begin to cover in the space of this small article. That's why I wrote a book on the subject. The book goes into greater detail on everything I've touched on in this article, and also shows how you can get a lot of value out of TextMate without writing any code. I recommend picking up a copy if you want to learn more about how you can make TextMate exactly what you need it to be.

James Edward Gray II is a contract programmer in Oklahoma and the author of TextMate: Power Editing for the Mac.


Return to Mac DevCenter.

Copyright © 2009 O'Reilly Media, Inc.