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:
- Place caret on an underlined URL
- Choose Bundles → Select Bundle Item… (⌃⌘T)
- Switch to key equivalent search (⌘4)
- Press enter (⌅) to see what is bound to that key
- 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