macdevcenter.com
oreilly.comSafari Books Online.Conferences.

advertisement

AddThis Social Bookmark Button

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

Pages: 1, 2

Next Pagearrow