Saving Keystrokes with Macro-Like Snippets

Snippets and macros have been a part of Komodo since the beginning. Snippets are for inserting frequently-used pieces of boilerplate text into documents. They have some pre-canned functionality in their shortcuts, but can't be extended. Macros are for running arbitrary code, in JavaScript or Python, but it's much harder to just insert text into a document than with a snippet.

Adding Smarts to Snippets

My personal issue with snippets harkens back to the 7±2 problem: the more snippets I need to learn to speed up a set of tasks, the more likely I am to fall back on my reasonable typing skills. Where I really want a boost is for tedious, repetitive operations. That word, "repetitive", implies being able to add a dynamic component to snippets. But the old snippets didn't support that, so I usually ignored them. I wasn't going to create one snippet to insert two li tags, another to insert three of them. I can do without frivolously overloading my cognitive abilities that way. What I really wanted was to make snippets more "macro-like", expecting there wouldn't be too much floor-wax-dessert-topping confusion.

Additionally, many people have requested alternative ways to invoke abbreviations besides the Ctrl-T (Cmd-T on OSX) key-sequence built into Komodo. In particular, people have requested that a simple tab key expand abbreviations. While addressing this problem, we decided it was time to add some features we and our customers have been talking about for a long time. This post is about what we did. All the features described here are available in Komodo 8, so you can download it and try them out right now.

Auto-Abbreviations

The first step was to free up abbreviations from the tyranny of the cmd_expandAbbrev keybinding, namely the above-mentioned Ctrl-T. Now, by default, when you type the name of an active abbreviation snippet, followed by a valid trigger character, the text will be replaced with the snippet contents.

The Two-Minute Abbreviation Refresher Course

If you haven't used abbreviations before, here's how they work. When you're editing a document in some language, say Python, any snippets that are associated with Python abbreviations are candidates. There are three ways a snippet can be a candidate. In all three ways, the snippet must live somewhere inside a toolbox folder named "Abbreviations" (and there can be more than one such folder in the toolbox). Here are the specifics for the three ways:

  1. The snippet is inside a folder named "Python", which is itself below a folder named "Abbreviations".
  2. The snippet is next to a folder named "Python". This is to accommodate similar languages, such as Python and Python3, or JavaScript and Node.js.
  3. The snippet is found somewhere inside a folder named "*", which sits next to a folder named "Python". This is a continuation of the previous step, but allows you to create a richer hierarchy of common snippets. Many of the sample snippets that ship with Komodo have now been moved into a folder called "keywords", and for "Python-common", we put that under a "*" folder. This scheme assumes that no one is going to one day create a language called "*". As long as programmers use the Unix command-line to run programs, it won't. So this scheme should be good for basically forever. And as long as we're mentioning the command-line, the folder corresponding to the toolbox folder "*" is actually called "_".

When an abbreviation is triggered, it is replaced with the contents of the snippet it names.

Did I mention that abbreviations can only contain name-characters (ASCII letters, digits, and a smattering of characters like "-", "_", "=", and ".")? You can use any characters in a snippet name, but then those snippets can only be inserted by double-clicking or dragging them.

Triggering Abbreviations

When the global Auto-Abbreviation preference is on (now the default), and you press one of the trigger characters after an abbreviation name, Komodo will replace the text with the snippet contents. You can set which keys trigger an abbreviation under Preferences|Editor|Smart Editing|Auto-Abbreviations. We expect the two most commonly used characters are Space and Tab (which is represented by the standard \t, as there is no good way to represent a tab in a Mozilla textbox), but just for fun we added many other punctuation characters. Note that both the abbreviation and the trigger will be replaced by the snippet. You can use \r to specify that pressing Return triggers abbreviation. You can also use the standard \xHH and \uHHHH forms to indicate byte and Unicode values respectively (where the "H" refers to any hexadecimal character).

By default, each created snippet is not an auto-abbreviation. To make it one, simply check the "Auto-Abbreviation" checkbox in the snippet properties. Note that the samples shipped with Komodo have this property turned on. And while we're on the subject, this would be a good time to delete any old sample folders in the toolbox, and keep only the version 8 ones. If you have two snippets with the same name in two different folders, Komodo might choose the newer one when it's trying to match an Auto-Abbreviation, but choose the older one when you press Ctrl-T (the old way). The newer versions are more useful, but won't be selected if older ones are present.

Preventing Auto-Abbreviations

Except for HTML, Komodo won't bother matching abbreviations in comments or strings (in HTML, they also trigger in default mode). But there might be times where you need to type an abbreviation name, and not have it expand. It's cumbersome to temporarily turn off the auto-abbreviation property; this feature is supposed to save work, not create more. A quicker way is to simply press Shift+Space. Also, auto-abbreviation expansion only happens when the cursor is at the end of the line. So if you really need to type something like "def:" in a Ruby file outside string/comment context, you could type def[shift+space][back-arrow]:[delete] (please let me know the use-case for that).

EJS: Making Snippets Smarter

Adding JavaScript Code to Snippets

No doubt there are plenty of web pages that are still generated from CGI Perl scripts containing long runs of print statements with embedded angle-brackets, but not many new sites are built that way now. Most people use a template language to reverse the files, where the document to output contains a bit of markup that lets the programming language decide what to insert at runtime. Komodo supports many of these out of the box, like RHTML, Template Toolkit, PHP, Smarty, Django, Mason, Mojolicious. We recently added support for EJS (Embedded JavaScript), a simple language that lets you embed (normally) server-side JavaScript into your documents.

Well, Komodo has a powerful built-in JavaScript engine. We asked ourselves if it would be a good idea if we ran snippets through an EJS preprocessor before inserting them into the document. After spending a couple of hours hooking EJS up to the snippet processor and looking at the results, we realized there was no going back. Templates rock for snippets as well.

EJS in a Nutshell

In an EJS file, everything between <% and %> is JavaScript control code. This is where your if statements go. You can define functions inside <% and %> tags. You can even use while and for loops in them, and a later section here describes a good use for them.

Everything between <%= and %> is run through the JavaScript evaluator, and its results are written to the snippet's replacement text. I call this emitted JavaScript.

Everything else is treated like current snippet contents.

A Sample: Flipping Coins in Snippets

This code sometimes prints the current month, and other times prints the current date:

   1    <% var m = new Date();
   2    var isHeads = Math.random() < 0.5; /* Treat under as "Heads" */ %>
   3    Current date part: <% if (isHeads) { %>
   4    Month: <%= m.getMonth() %> <% } else { %> Date: <%= m.getDate() %>
   5    <% } %>

If you have even a passing familiarity with JavaScript, the above snippet should be straightforward. If you associate it with an abbreviation called, say, rdate, and repeatedly expand it and undo it, you should see the month half the time, the date the other half.

The snippet also illustrates the main gotcha of taking this template-approach to text-creation. When you're generating HTML, you normally aren't concerned too much about the creation of spaces and newlines. But when you're inserting text into your documents, everyone cares about every single space. If you're questioning that, search the Komodo bug database for bugs against the editor, with keywords like "indent", "tab", and "space". Here are the whitespace rules for snippets.

First, if an EJS close-tag is followed by a newline, that newline is always thrown away before the EJS is evaluated. This is why that comment in line 2 above uses the older /*...*/ and not //.... If we used the //... comment, and tried to expand the abbreviation, nothing would happen, and Komodo would put a "Snippet Insertion Deliberately Suppressed" message in the status bar. This is because of the way Komodo removes newlines before evaluation. So while some JavaScript pundits deprecate the old-style form of comments, they're safer in snippets.

Beyond this, whitespace is added during snippet insertion as it was in earlier versions. Each leading tab in a snippet line is replaced by one indentWidth's worth of spaces. And then if tabs are on, each tabWidth's worth of leading spaces is converted back to a tab. Even if you never use tabs in your final documents, you always want to use leading tabs in your snippets, not spaces.

I'm finding that the lines in some of the sample snippets were getting very long. But keep in mind that inside control EJS tags, newlines are ignored. So there's nothing wrong with putting %> tags at the start of the subsequent line.

Case Study: Handling Ruby Keywords

Giving You More Control

Ruby was the first keyword-based language Komodo supported. All the other core languages used either braces, indentation, or angle brackets to denote structure. Making Ruby a first-class member of the Komodo family meant carrying over features like auto-indent and auto-dedent to a keyword-based language. We added a special-case language service to handle indentation and "end" insertion for Ruby. If you've coded with Ruby in Komodo, you've seen this when you typed a keyword like "class" or "if" or "def", and Komodo quietly inserted an "end" one line below at the same level of indentation. It was cool, and useful, and it bugged most of the Komodo team members that nothing else in Komodo behaved like this. We're all about coding to the universal, and handling special-cases via data-driven tables, not code. It seemed wrong to dedicate all that code just to make writing Ruby code easier.

Earlier in 2012 I was taking one of those online university courses, which used Matlab, another keyword-structure language with "end" statements. During an ActiveState hackathon I added a language service for keyword-based languages (including the various Basics, Pascal, Lua, and Ruby). Unlike those other keyword-based languages, in Ruby some keywords like if start a block in most situations

   1    if starts_at_line
   2      puts "a regular block"
   3    end

But they qualify a statement when occurring in other positions, like here:

   1    puts "qualifier if 'if' guards this action" if near_end_of_line

So we would want to insert the end in the first situation, but not the second. This was impossible with the old static snippets, but straightforward with the current ones. Here's the new snippet for Ruby/keywords/if:

   1    <% if (ko.snippets.rightOfFirstRubyKeyword()) {%>
   2    if [%tabstop:test]
   3        [%tabstop:#code]
   4    end
   5    <% } else {
   6      throw new ko.snippets.RejectedSnippet("not at start of line");
   7    } %>

This version of Komodo adds a new library file, snippets.js, where we've started to build an API that makes writing snippets easier. You can easily add to the ko.snippets namespace with a startup trigger macro, or even easier by posting a feature request at The Komodo Bug Page. The two members of that namespace shown here should be straightforward. The rightOfFirstRubyKeyword method is used to tell whether keywords like if, unless, while, and until start a block. The ko.snippets.RejectedSnippet needs a bit more explanation. If snippet evaluation throws a JavaScript exception, the snippet's contents aren't inserted into the document. But if the exception is a ko.snippets.RejectedSnippet object, Komodo won't emit a statusbar notification if the attempted insertion was due to auto-abbreviation matching. In other words, Komodo does the right thing. In our Ruby if example, Komodo will expand an if at the start of a line, and silently do nothing if you type a space after a qualifying if. However, if you tried to explicitly invoke an abbreviation by pressing Ctrl-T after a qualifying if, you would get a notification that the insertion was suppressed. Full details will be in the log file.

Repetition a la Emmet

Emmet (possibly better known by its previous name, "Zen Coding") is a very cool extension (available for other editors as well, not just Komodo) that lets you enter a CSS-like selector, press a button, and have it converted into a long bit of HTML. In Komodo it's implemented with an extension, and is only available for HTML. But we can easily bring this type of feature over to other languages, using dynamic snippets.

Case Study: Creating Quick Constructors

For example, a common task in much object-oriented programming is to write a constructor that takes n parameters, and then assign each parameter's value to an attribute with its same name. For example, in Python, you might have an initializer like this, taken from the standard Python library's formatter.py:

   1    class DumbWriter(NullWriter):
   2        def __init__(self, file=None, maxcol=72):
   3            self.file = file or sys.stdout
   4            self.maxcol = maxcol
   5            NullWriter.__init__(self)
   6            self.reset()

As a first cut, let's write a snippet that generates an initializer, and inserts n parameters, tying each parameter to an assignment statement using tabstops. Let's say the macro will be invoked by typing n:ninit followed by a trigger character (remember that everything from the number through the trigger character will be consumed):


   1    <%
   2    var i, numReps = ko.snippets.consumeLeadingNumericFactor(":");
   3    %>
   4    def __init__(self<%
   5    if (numReps === 0) {
   6            // Allow for args
   7            %>[%tabstop:, args]<%
   8    } else {
   9            for (i = 1; i <= numReps; i++) {
  10                    %>[%tabstop<%= i %>:arg<%= i %>]<%
  11            }
  12    } %>):
  13    <% for (i = 1; i <= numReps; i++) { %>
  14            self.[%tabstop<%= i %>:arg<%= i %>] = [%tabstop<%= i %>:arg<%= i %>]
  15    <% } %>
  16            [[%tabstop:]]

OK, this is a bit of a jump in complexity from the previous snippet. I'll walk through it line-by-line and hopefully all will be clear by the end.

Lines 1-3 are a simple JS block that declares two variables, and uses yet another convenient helper that checks for a leading number, and removes it if present. We specify the delimiter, which by default is ":". In particular, you can't use ":" as a delimiter in Ruby, because Komodo will interpret the ":" and all characters that follow it as a symbol name, and then you won't get any expansion at all.

In line 4, we emit some Python code that starts the constructor definition. But notice that the line ends with the start of a second JS block. Why did I put it there, and not at the start of line 5? I want to keep the parameter list on one line. Recall that newlines are ignored inside JavaScript control blocks. So it's a good place to put one.

Things are more complex in lines 5-12, because I'm interleaving JavaScript control code, JavaScript emitted values, verbatim Python text, and, just to keep things more interesting, Komodo snippet tabstops. Hang on.

Lines 5-6 are pure control JavaScript, handling the case where there are no parameters.

The leading white space in line 7 is ignored (because it's still in control JS). We then emit a tabstop that lets us specify whatever parameters we want, and then we're back into JS control mode.

The JS control mode continues in lines 8-10, where we bring out the useful gun of a JS for loop. If you've looped over database rows in PHP or RHTML, this is the same concept. Only you're doing it to generate code in Komodo. Mind should be getting blown now.

Line 10 continues with intermixing of Komodo Snippet tabstops with emitted JavaScript. By default we're calling the first parameter "arg1", the second "arg2", etc. You'll probably have better names in mind for your actual code. Because all instances of tabstop i are linked, when you change the name of "arg1", all other uses of it will change as well.

Lines 10-12 carry on with a range of control JS, which just ends the current for and else blocks.

Line 12 ends with some verbatim text, namely ): followed by a newline, which in this case we want. A good exercise at this point is to understand why this is the first newline to end up in the final snippet.

Line 13 starts another for loop, which we'll use to generate n initializers.

Line 14 should look a lot like line 10: again, we're intermixing verbatim Python text, tabstops, and emitted JS.

Line 15 ends the for loop.

Finally, line 16 uses an empty tabstop where we can add more code.

Using the Snippet

As you tab through the snippet, you get to set each parameter name, and all the boilerplate code is set automatically. Alpha 2 ships with a variety of different n-abbreviations, in particular nclass for JavaScript, nnew for Perl, ninit for PHP, Python, and Ruby. For example, you would invoke the Python ninit snippet for 3 arguments by typing the following, followed by a space (or any other abbreviation trigger character):

3:ninit

Why not fire up the alpha and try it out?

Another Helper: Getting the Class Name

This is just the start. Many times, your class will be subclassing another class, and you'll want to call its constructor before filling in the details. Using ko.snippets.getTextLine(scimoz=null, currentLine=i), it's not hard to find the superclass name and emit an optional tabstop that would call the superclass's constructor with the same parameters to the current one. Recall that the DumbWriter class in the Python example a few pages back was subclassed off the NullWriter. When you generate the __init__ snippet, it would be nice to grab the superclass name and set up a call to its constructor. In the interest of keeping this post from growing even longer, we'll leave coding this as an exercise. Keep in mind that Python supports multiple inheritance (for most cases, this might not be that useful a tabstop).

Soft Shortcuts: Skipping Through Inserted Code

This version of Komodo also lets you generate "soft" characters in snippets with the [%soft:text] shortcut (recall that soft characters are those highlighted characters you can type over, rather than right-arrow or mouse to get over, and are usually closers like "]", ")", or a close-quote). As an example, here's a useful Perl abbreviation for print:

   1    print [[%tabstop1:LIST]][[%soft:;]][[%tabstop]]

The soft ; means we can type over it. And finally, by putting an empty tabstop at the end of the line, we can tab to get over it and hop over the ; when we're done typing the guts of the print expression. Common pet peeve solved.

Editing Snippets

With all these new features, editing snippets inside the Snippet Properties dialog has become less fun. So we've added an Edit Snippet menuitem when you right-click on a snippet. This loads the snippet contents into a regular editor view, with a suffix of .snippet, and a language name of Komodo Snippet. This is more of a pseudo-language: it colors anything inside EJS tags as JavaScript, but treats the rest as plain-text (we plan to color tabstops as well in the near future). One huge advantage is that the JavaScript is subject to regular JS syntax-checking. This finds the most common errors in Komodo Snippets (well, the ones we anticipate to be the most common, seeing how this author is the only one to have used this feature): missing or mismatched parentheses around JS conditions, and errant semi-colons inside emitted JS.

The only caveat is when you edit a snippet inside the editor, you lose any cursor position information that the snippet editor manages for you. So you might need to close the snippet in the editor, and revisit its properties to set items like a selection. This information isn't used when you have tabstops though, and we see less of a need to change the contents of simpler snippets in the editor. Also, if you leave the insertion-point inside a conditional block of code, it might not appear in the expanded snippet, and in a tabstop-free snippet, the cursor will appear at the start of the inserted text.

Blurring the Distinction: Should Anyone Care?

Some people might wonder why we didn't just fold snippets into macros. After all, the Emmet (Zen Coding) add-on was done as an extension, which is far more macro-like. But it's a matter of ease of use. Snippets have shortcuts and take care of the details of getting text into your document. Macros don't. But they both now have access to the full Komodo JavaScript API, including the behemoth that is Mozilla's XPCOM. Floor wax or topping, you choose. It doesn't really matter. Just please use the great power responsibly.