Injection Grammars & Project Variables

I was recently dealing with issue #157 and wanted to store a reference in the source.

Of course this reference should be an underlined link and allow us to easily go to the online issue, yet I don’t want a 50 character long URL in the source, so how do we go about this?

Highlight

The first key to the solution is “injection grammars”. A new feature in 2.0 allows you to create a new grammar, and rather than apply it to the whole file, you tell TextMate which scope it should be applied to using a scope selector.

This means we can add small grammars for strings, comments, and similar, and we already have a few of these. Looking in the Hyperlink Helper bundle we see this grammar injected into text, string, comment (this is the version prior to my changes):

{   patterns = (
        {   match = '(?x)
                ( (https?|s?ftp|ftps|file|smb|afp|nfs|(x-)?man(-page)?|gopher|txmt)://|mailto:)
                [-:@a-zA-Z0-9_.,~%+/?=&#]+(?<![.,?:])
            ';
            name = 'markup.underline.link.hyperlink';
        },
    );
}

This means we get URLs highlighted inside strings, comments, and in text files (e.g. text.plain).

I decided on a reference syntax like <issue://157> so adding issue to the URL schemes in the above match pattern gives me highlight of these references in all comments, strings, and text files. That was easy!

Action

Next problem to solve is how to actually open them. We already have an “Open Current URL” command in the Text bundle bound to enter () and scoped to markup.underline.link so by default, pressing enter (fn return) on an issue will open it as a URL. Let’s inspect this command, you can find it by following these steps:

  1. Place caret on an underlined URL
  2. Choose Bundles → Select Bundle Item… (⌃⌘T)
  3. Switch to key equivalent search (⌘4)
  4. Press enter () to see what is bound to that key
  5. Press option return (⌥↩) or the arrow button to edit that item

This reveals that the command (written as a shell script) is doing:

open "$(cat)"

The command has its input set to “selection or current scope” (presently the fallback is not visible in 2.0’s preliminary bundle editor). This means that stdin for this command will be the entire link (the “or scope” part).

We could modify this command to support the special issue scheme, but that is not elegant. Instead we make a minor but important change to the injection grammar from above. We change the name key to markup.underline.link.$2.hyperlink. What we did was add $2 which is the second capture from the regular expression, namely the URL scheme (more about syntax allowed in format strings).

This means that when we are on a URL we’ll get the URL’s scheme in the scope. So placing caret on <issue://157> (placed in a comment) and pressing ⌃⇧P (to see current scope) shows us we now have:

markup.underline.link.issue.hyperlink

What’s great about this is that we can now create a new command, still using enter as key equivalent, but using a more specific scope selector so that it only targets issue links.

This command could simply be:

open "https://github.com/textmate/textmate/issues/$(cat)"

But we can do better!

Generalization

TextMate 2 makes it easy to deal with project specific variables, I won’t repeat all of it here but instead recommend you read about folder and file type specific settings.

The gist of it is that you should place a .tm_properties file in the root of your project and in this, you can specify, amongst others, (environment) variables for the project. So for TextMate’s .tm_properties I added:

TM_ISSUE_URL = 'https://github.com/textmate/textmate/issues/%s'

Our “Open Issue Link” command can read this variable and use it to construct the final URL. Since I figured this would be useful for others than me, I added the command to the hyperlink helper bundle and its source looks like this:

#!/System/Library/Frameworks/Ruby.framework/Versions/Current/usr/bin/ruby -wKU
require "#{ENV['TM_SUPPORT_PATH']}/lib/escape"

abort "TM_ISSUE_URL is unset for this project." unless ENV.has_key? 'TM_ISSUE_URL'

link = STDIN.read
if link =~ %r{issue://(.+)}
  url = ENV['TM_ISSUE_URL'] % $1
  %x{ /usr/bin/open #{e_sh url} }
else
  abort "Not an issue link: ‘#{link}’"
end