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


comments powered by Disqus