TextMate News

Anything vaguely related to TextMate and macOS.

Format Strings

TextMate 1 allowed you to use variable references in Snippets and even to supply defaults or simple replacements for them. TextMate 2 expands a lot on what you can do with variable references and where you can use them.

In this article we will discuss:

  • The syntax of TextMate 2’s Format Strings
  • Where you can use them
  • The kinds of tricks these tools can help you accomplish

Titling Your Work

We’ve already discussed .tm_properties files and how you can use them to customize TextMate 2’s behavior. What I didn’t tell you before though is that those settings are Format Strings.

One option you can customize is the title used for the editing window. By default, the window title shows the name and extension of the file you are editing. Let’s use some Format String magic to fancy that up.

The default window title

First, I can just add this option to the .tm_properties in my home directory to also show the directory of the file being edited:

windowTitle = "$TM_DISPLAYNAME – $TM_DIRECTORY"

A window title with a directory

This is useful to me if I am editing in a project where it’s common for files to have the same name, but be located in different places. Rails applications are like that.

However, the full paths can get pretty long. If you want something shorter, we can edit the variable as it is used with a Regular Expression:

windowTitle = "$TM_DISPLAYNAME – ${TM_DIRECTORY/([^\/])[^\/]+\//$1…\//g}"

That expression is a little difficult to read because it’s matching directory separators, which are the same characters TextMate uses to delimit a Regular Expression. The first chunk between the non-escaped / characters (([^\/])[^\/]+\/) is the expression which searches for two or more character directories with a trailing /. The replacement string ($1…\/), before the next unescaped /, just truncates the directory down to the first character, an ellipsis, and the trailing separator. The final g after the last / turns on the global option for the replacement, so we trim all the directories but the last (because it won’t have a trailing / to match).

A window title with a truncated directory

Obsessive Compulsive Window Titles

Note: the following example is real world and may get pretty complex. Lighter example follows.

I like the trimmed paths, but I’ll be honest and tell you that’s not my actual windowTitle setting. Here’s what I really use:

windowTitle = "$TM_DISPLAYNAME${TM_DIRECTORY/\A(?:\/Users\/james\/Documents\/(?:work\/)?(?!\b(?:communication|programming|reference)\b)\w+\/?(.*)|(.+))\z/${2:? – ${2/\/Users\/james/~/}:${1/\A(?=.)/ – /}}/}"

This leads us to two of the great features of Format Strings in TextMate 2: the replacement of a variable reference is a nested Format String and you can branch based on whether or not a variable is set. This allows your to run replacements on a replacement and choose between multiple replacements. Let me explain how I use those in the pattern above.

I keep my current project directories just inside of ~/Documents. I also move older projects into a ~/Documents/work subdirectory. I want to see paths inside of those project directories, but I don’t need to be reminded that they are in ~/Documents or ~/Documents/work. I also don’t need to see the project directory itself, since I’m seeing that in the File Browser. Other non-project directories should be handled normally. There are also a few directories inside ~/Documents that aren’t project directories and those should be treated like any other directory.

For example, here are some path transformations:

  • ~/Documents/tm2_documentation/ is stripped
  • ~/Documents/tm2_documentation/calendar/ becomes calendar
  • ~/Documents/work/scout/ is stripped
  • ~/Documents/reference/documentation/ is not changed

Given that, I just do a search for a leading (\A) followed by either ((?:…|…)) hardcoded path (\/Users\/james\/Documents\/), the optional work directory ((?:work\/)?), followed by a project directory name that isn’t one of the exceptions ((?!\b(?:communication|programming|reference)\b)\w+\/?), and anything that happens to be trailing that ((.*)) or any other directory listing ((.+)) anchored to the end of the path (\z). That’s the easy part, but the replacement is where we get tricky.

To understand the replacement, you have to understand the desired result. If I have a file open from just inside the root project directory, I only need to see that file’s name. For example, Gemfile. However, if I have a file open deeper in the project, I want to see the name and the path from the project’s root directory. An example of that would be show.html.erb – app/views/users. The gotcha is the separator (). I don’t want the separator if it isn’t going to be followed by a directory. Non-project directories can just show the full directory path, but it would be nice to at least shorten /Users/james to ~ for them.

To accomplish that, I first check to see if I’m in a project path or a normal (${2:?…:…}). When it’s a normal path, $2 will be set because that’s the part of the expression that matched them. If it is set, I prepend a separator and run a replacement to alias my home directory as discussed above ( – ${2/\/Users\/james/~/}). If this is a project path, I run another regex replacement on $1. If the beginning of it (\A) is followed by an unmatched non-newline character ((?=.)), I replace that beginning (no content, just an anchor) with a ` – ` to get my separator.

While the above was all about aesthetics, there are other variables, like TM_MAKE_TARGET, that can likewise benefit from a path-derived value. There’s an example of how this can be used in the article on .tm_properties files.

Replacing a Replacement

While the previous example is just a scary showing of how far I will go to keep my windows looking pretty, the features used have a lot of powerful and practical applications.

Nesting Format Strings allows for pretty clever uses of the Find and Replace dialog. Want to convert a single-quoted Ruby string literal into the double-quoted variety while adding the proper escaping? Just Find for the regex '(.*?)' and replace with "${1/"|#\{/\\$0/g}". Try that out on this chunk of Ruby:

'a "simple" string with no #{interpolation}'

and it will be replaced with:

"a \"simple\" string with no \#{interpolation}"

This works because the second search hunts inside of the original string content ($1) for things that need escaping (" and/or #{) and prepends the escape (\\) to them.

This or That

The other feature I made use of earlier was if/else variable replacements. Again, this is a handy transformation tool.

Let’s say we have some CSV content:

Product,Price
iPhone 4S,$199.00
MacBook Pro,"$1,199.00"
"Mac OS X ""Lion""",$29.99

We could convert that to tab-separated content and strip the extra quoting (we’re confident there are no tabs in the fields) as we go, with just one Find and Replace operation. The Regular Expression used in the Find is \G(?:"((?:""|[^"]*)*)"|([^,]*))([,\n]) and the replacement is ${1:?${1/""/"/g}:$2}${3/,/\t/}.

Let’s talk through how that works, because it’s pretty instructive.

The idea with the regex is that we need to walk through the data one field at a time. We do that by saying: pick up where the last match ended or at the beginning for the first match (\G) and look for either ((?:…|…)) a quoted field "(…)", which is zero or more escaped quotes and/or runs of non-quote characters ((?:""|[^"]*)*), or an unquoted field (([^,]*)) delimited by a comma or a newline (([,\n])). Each time that pattern matches either $1 will be set with the contents of an quoted field or $2 will be set with the contents of and unquoted field. No matter what $3 will be set with a comma or a newline. That sets us up for the replacement work.

The replacement string has two branches. If $1 is set (${1:?…:…}), then use the quoted content, but unescape all quotes first (${1/""/"/g}). Otherwise, use the unquoted content ($2). Either way, add the separator at the end, but switch commas to tabs (${3/,/\t/}).

Bundled Ifs

The ${VARIABLE:?…:…} form is useful when you need an if clause and an else clause. If you only need the if you can use ${VARIABLE:+…} and you can use ${VARIABLE:-…} for else-only insertions.

A good example where this shortcut style is used involves setting a shell variable inside the Themes bundle:

shellVariables = (
  { name = 'TM_THEME_PATH';
    value = '${TM_THEME_PATH:+$TM_THEME_PATH:}$TM_BUNDLE_SUPPORT/web-themes';
  },
);

This prepends an existing path, if it already exists before it is set here. This allows one command, like Web Preview in the Support folder, to read an environment variable (TM_THEME_PATH) and learn about the paths of third party bundles.

Bundle completionCommand settings are also Format Strings.

The Case for Replacements

Another form of Format Strings allows you to change the case of a variable reference. If you combine that with the fact that scope name elements in TextMate 2’s Language Grammars are now Format Strings, this allows for nice dynamic scoping tricks.

For example, say we rework Ruby’s “here-document” (heredoc) syntax rule to look something like this (simplified) example:

begin = '<<-?(\w+)';
name  = 'string.unquoted.${1:/downcase}.ruby';
…

That would mean that a heredoc definition like this:

<<-CSS
body {
  margin: 10px auto;
}
CSS

would be scoped as meta.here-doc.css.ruby. This allows the injection of the CSS Grammar and automations into the scope meta.here-doc.css. That would mean that you get the proper syntax highlighting of these heredocs and you could trigger CSS Bundle commands while editing them.

As I write this, the ruby grammar still has a dozen rules with a dozen different tokens that include a dozen different languages, so the above is a planned simplification that would allow us to avoid having to upgrade the ruby grammar for new potential here-doc tokens.

The supported case transformations are /upcase, /downcase, and /capitalize. There’s also a special /asciify transformation that will transliterate things like “æ” and “ø” into “ae” and “o” respectively as well as remove any remaining non-ASCII characters. The latter is particularly useful when deriving an identifier or label, that needs to be accepted by a language parser, from a heading or comment, which is free-form prose. These transformations can be combined.

A Tiny Snip of Snippets

Format Strings also have some Snippet-only enhancements, like the new pop-up choice syntax. We’ll cover those in an upcoming article on what’s new with Snippets.

In the meantime though, I will give you one fix TextMate 2 has over its predecessor. If you need to place a literal number right after a numeric variable reference you can now use this form: ${1}337. That works in Snippets and anywhere else Format Strings are used: .tm_properties files, Find replacements, Language Grammar name fields, and Bundle Settings.

categories General TextMate 2