Get Started Writing TextMate 2 Bundles
TextMate 2 is my favorite editor. In many ways it is the modern decendant of vim and emacs. Like those editors it is very flexible when it comes to extending it while following modern GUI conventions. One of the things I really like is the extension mechanisms. You can create macros and snippets easily. But what I want to talk about in this article is TextMate commands. Commands are external scripts you can invoke from TextMate. The cool thing about these scripts compared to vim and emacs is that you can write them in any language.
However information about how you write these scripts seem to be scattered, so this is my attempt to help you get started writing scripts to extend TextMate 2. This allows you to do all kinds of cool stuff like creating command completion like you find in modern IDEs.
The Unix philosophy of TextMate
TextMate is different from how you make plugins in a lot of IDEs. You don't create a dynamically loaded library which adheres to some binary interface. The downside of that is that you have to develop your plugin in a particular programming language.
TextMate bundles's on the otherhand are based on the unix philosophy of creating software by combining small programs which process text.
A TextMate command is a script which receives its input from TextMate through regular environment variables and standard input. TextMate will setup its own environment variables before running your script.
Variable Description TM_CURRENT_LINE Text found on the line the caret is on TM_CURRENT_WORD Word at location of caret TM_SELECTED_TEXT Contains text currently selected in document TM_LINE_INDEX Zero based position of caret on line TM_LINE_NUMBER Line number at caret. Counts from one.
There are a lot more environment variables. You can read about them in the Pragmatic Programmers TextMate book. Although it is for TextMate 1 it is very usefull for TextMate 2.
Environment variables are not suited for large amounts of data, so the bulk of data is sent in through STDIN.
For each command you can configure how information about current line, current selection etc is sent to your script.
TextMate store a configuration file with your command which it reads to figure out how to pass data to it what to do with the data received back through STDOUT. It configuration file might say to replace the current selection with the output from the script or insert the output at the caret. There are a lot of choices.
How to Debug and Develop a Command
Just as when debugging any other program without a dedicated debugger you can use printf style debugging. Anything you send to STDOUT can be shown by TextMate. E.g. you can configure your command to display output from STDOUT in a separate window, tooltip or just insert it right in your document at the caret.
Example of a TextMate Command is Implemented
This example is taken from the bundle which comes for the programming language Go. This bundle provides function and type completion through a command. It is implemented as a ruby script which calls a unix command named gocode which you need to install like any other Go command. gocode can be invoked at the command line:
gocode -f=json --in=foobar.go autocomplete 449
This will look at at the 449th character in the source code file foobar.go and produce a list of possible completions formated in JSON.
Our TextMate command consists of a Ruby script which invokes this command. First the script load a whole bunch of ruby scripts which come bundled with TextMate:
require ENV['TM_SUPPORT_PATH'] + '/lib/ui.rb'
This loads the ui.rb file. The TM_SUPPORT_PATH environment variable will always contain the path to scripts bundled with TextMate to help you make bundles.
Next step is to read in the whole file. This script does not pass the file in through STDIN.
document = [] File.open(ENV['TM_FILEPATH'], "r+") do |file| document = file.readlines end
Since gocode can't work with line and column number directly which is what TextMate sends our script through environment variables it has to calculate a byte offset like this:
cursor = document[ 0, ENV['TM_LINE_NUMBER'].to_i - 1].join().length + ENV['TM_LINE_INDEX'].to_i
Now we have enough information to execute the gocode command and get a list of completions. This script gets the result in CSV format and not JSON:
output = `$TM_GOCODE -f=csv -in=#{e_sh ENV['TM_FILEPATH']} autocomplete #{cursor}`
output now contains a string formated in CSV with our completion suggestions. The completions are sent to a ruby function provided by TextMate called TextMate::UI.complete. It pops up a list of our choices and let users choice a completion. However complete expects the completions in its own format:
They need to be formated as array of dictionaries with the following keys:
display The title to display in the suggestions list
insert Snippet to insert after selection
image An image name, see the :images option
match Typed text to filter on (defaults to display)
Trying to complete on fmt. I get the following CSV back (only first 4 lines):
func,,Errorf,,func(format string, a ...interface{}) error func,,Fprint,,func(w io.Writer, a ...interface{}) (n int, err error) func,,Fprintf,,func(w io.Writer, format string, a ...interface{}) (n int, err error) func,,Fprintln,,func(w io.Writer, a ...interface{}) (n int, err error)
To work with TextMate::UI.complete it needs to be changed to an array of dictionaries like this (just 4 first matches):
{"match"=>"Errorf", "display"=>" Errorf(format string, a ...interface{}) error", "insert"=>"(${1:format string}, ${2:a ...interface{\\}})$0", "image"=>"func"} {"match"=>"Fprint", "display"=>" Fprint(w io.Writer, a ...interface{}) (n int, err error)", "insert"=>"(${1:w io.Writer}, ${2:a ...interface{\\}})$0", "image"=>"func"} {"match"=>"Fprintf", "display"=>" Fprintf(w io.Writer, format string, a ...interface{}) (n int, err error)", "insert"=>"(${1:w io.Writer}, ${2: format string}, ${3:a ...interface{\\}})$0", "image"=>"func"} {"match"=>"Fprintln", "display"=>" Fprintln(w io.Writer, a ...interface{}) (n int, err error)", "insert"=>"(${1:w io.Writer}, ${2:a ...interface{\\}})$0", "image"=>"func"}
TextMate::UI.complete works by spawning a new executable bundled with TextMate called tm_dialog2 found under TextMate.app/Contents/PlugIns/Dialog2.tmplugin/Contents/Resources in the TextMate app. This is called with the following arguments:
tm_dialog2 popup --returnChoice --alreadyTyped '' --additionalWordCharacters _
The array of dictionaries are sent over to the tm_dialog2 process through its STDIN. Before it is sent it is formated as a OS X property list (plist). TextMate comes bundled with a function to_plist attached to array objects to do this.
To test this functionality you can write a simple script which executes this line:
tm_dialog2 popup --suggestions '( { display = law; }, { display = laws; insert = "(${1:hello}, ${2:again})"; } )'
And configure the command to output to caret position. Make sure the output of this command is send to STDOUT. When you run this script a list with two choices will popup at the caret:
If you select law then law will be inserted at caret. If you pick laws then you will get a snippet inserted:
Where you can tab between hello and again. Here is an example of a Julia script which does exactly this:
#!/usr/bin/env julia tm_dialog = ENV["DIALOG"] print(readall(`$tm_dialog popup --suggestions '( { display = law; }, { display = laws; insert = "(${1:hello}, ${2:again})"; } )'`))
Open TextMate from HTML document
If you have done iOS development you would be familiar with the concept of URL Schemes. This is not limited to iOS, but also supported on Mac OS X. TextMate registers in its Info.plist (contained in the Application bundle) the URL Scheme txmt. So any URL starting with txmt will cause a method in TextMate to be called which will be passed the URL. Thus any program which handles URLs in OS X can potentially open a TextMate document. E.g. the Safari Web browser or the Unix command open:
$ open "txmt://open?line=14&url=file://filename.txt"
This opens the textfile filename.txt at line number 14. This is the basis for how TextMate uses HTML pages with errors to let you jump to a line number.
Commands are regular Unix commands which you send and reveive data to through STDIN and STDOUT. Input can be provided by environment variables as well. For each command you provide a configuration file which you can setup with a GUI in TextMate. The configuration file says what data is transfered on STDIN to your command and what is doine with the text output from your script.