A fair number of people in the Espresso forums ask questions like, “Is there a shortcut to delete the current line the way there is in Textmate?” Usually the answer is, “No, why don’t you create one?” Yet people remain leery of creating text actions by hand. I understand that; heck it’s extra work, and who wants that? What I think most people don’t realize is that, particularly for text manipulations like deleting the current line, the amount of work is minimal. All you need is a basic understanding of how Espresso works and the ability to author simple Javascript and XML.
Anatomy of an action
Espresso actions come in two parts:
- The XML definition, which tells Espresso the action’s title, what Objective-C class to execute when the action is invoked, and additional information like keyboard shortcuts or tab triggers that should invoke the action.
- The Objective-C class (or, if using a plugin like TEA or Spice, the script that defines the action’s logic)
Most often, XML action definitions are included with a Sugar (you can make a Sugar for custom text actions simply by adding a .sugar
extension to a folder that contains a TextActions folder with the action XML files in it). If you wish, you can also define action XML files outside of a Sugar by enabling TEA custom user actions in the Advanced pane of the Espresso Preferences (more info about TEA custom user actions).
As of Espresso 1.1 there isn’t an in-program GUI for editing actions yet, but hopefully that will come in time.
Spice up your actions
Spice is an Espresso Sugar that I’m currently developing that provides access to the Espresso API using Javascript (thanks to JSCocoa), and additionally provides a selection of utility classes that allow you to interact with the API without needing to know anything about Objective-C or the JSCocoa bridge.
The benefits of this should be fairly obvious, but there’s another reason creating actions with Spice is easier than using TEA or Objective-C; not only are the action scripts not compiled, but you don’t need to relaunch Espresso to see your changes. When I’m coding a Spice action, I usually open the Javascript action in Espresso, and then run the action right there on the Javascript when I need to test it.
Although Spice’s utility classes make creating custom Espresso actions easier, you still need a basic understanding of how the Espresso API works.
Contexts, ranges, recipes, and snippets
When a user invokes an action from the Actions menu, Espresso executes the action’s class and sends it an object representing the text or file context (depending on whether you’re using a text or file action; I’ll be focusing only on text actions because the file action API is currently underwhelming). The text context allows you to access the active file’s text, information about the selection (or selections, since you can select multiple things at once), line information, preferences (like what line ending or indentation is being used), and access to the syntax system.
When you are ready to change some text in the file, your action messages the text context and tells it what to change and how to do it.
As of Espresso 1.1, the easiest actions to create are ones that are invoked by the user, access and change something about the active file’s text, and then immediately exit. It’s possible to do things that are a bit more complicated or on-going, but at the moment Espresso doesn’t make it easy.
When you’re working with text, you’ll mainly be dealing with ranges. A range represents a section of text in the active file and is composed of a location and a length. Internally Espresso keeps track of text by counting all of the characters in the active file starting at zero, so the range location is the index of the starting character and the length is the number of characters contained within the range. Typically you’ll be dealing with selections, so you’ll have ranges like {0, 10} (the first ten characters in the file) or {100, 30} (characters 100 through 130). However, it’s also possible to have ranges like {10, 0}, which represents the cursor location at character index #10 in the file.
A typical text action will fetch the currently selected ranges from Espresso, manipulate the text within them somehow, and then tell Espresso to change the range(s) accordingly. In order to queue up changes like this, you’ll use text recipes.
Text recipes are something that is unique to Espresso, so far as I know. Basically, you create a recipe and tell it things like “delete the text in this range”, “replace the text in this second range”, and “add this text at a third range”. You can compose a multiple step recipe without worrying about tracking how ranges will change; for instance, if you add characters to an early range, you don’t have to adjust the location of later ranges. Instead, the recipe does that for you when you apply it to the document.
Text recipes allow you to make complex changes to multiple ranges in the active file, but you can instead insert text snippets, as well. A text snippet is a specially formatted string that allows Espresso to create tab stops, mirrored segments, and more after you’ve inserted it. Many of the custom actions bundled with Espresso are little more than text snippets; for instance Wrap Selection In Tag merely grabs the selected text and wraps it in a simple snippet that mirrors the HTML element from the opening tag to the closing tag. You can do some pretty magical-seeming things with very simple actions simply through tricky use of text snippets. The one downside to keep in mind for text snippets, however, is that you can only insert one at a time (unlike text recipes, you can’t insert multiple text snippets over discontiguous selections).
Bringing it all together
With an understanding of how the Espresso API works and a quick peek at the Spice docs, setting up an action to delete the current line (to take one example) becomes simple enough:
- First, we’ll need to define an action in XML, and create a Javascript file with the action logic
- In the Javascript, we’ll need to query the text context to get the range of the current line, and then create a text recipe to delete that range
The action XML is easy enough; simply create the file Actions.xml
and place it either in a Sugar’s TextAction folder or in your Espresso Support folder located here:
~/Library/Application Support/Espresso/Support/TextActions/
The contents of the action XML definition should be this:
<action-recipes>
<action id="com.onecrayon.DeleteLine" category="actions.text.generic">
<class>Spice</class>
<title>Delete Line</title>
<setup>
<script>delete_line</script>
<undo_name>Delete Line</undo_name>
</setup>
</action>
</action-recipes>
(If you wish to add a keyboard shortcut, you can do that with the key-equivalent
entity; see the Spice action XML docs.)
If you’re using the Support folder option, you’ll need to enable TEA custom user actions in the preferences and restart the program twice. Otherwise you’ll need to make sure the Sugar is installed and restart the program once (action XML definitions are loaded when Espresso boots up; unfortunately as of Espresso 1.1 there isn’t a way to refresh action XML definitions without a relaunch).
Now that you’ve got the action defined (and showing up in the Actions menu) you can create the Javascript file delete_line.js
(referenced in the script
element of the action XML). If you are using a Sugar, it should be located here:
MySugar.sugar/Support/Scripts/
If you’re using custom user actions in the Espresso Support folder, you’ll save it here:
~/Library/Application Support/Espresso/Support/Scripts/
The simplest way to delete the current line would be this:
// Deletes the current line
// require() allows you easy modular access to Spice's helper classes
var textContext = require('text_action_context').textContext;
var TextRecipe = require('text_recipe').TextRecipe;
// exports.main is your primary function, run automatically by Spice
exports.main = function() {
// Grab the range of the current line
var linerange = textContext.rangeForLine();
// Run the actual removal
return new TextRecipe().remove(linerange).apply();
}
First, the script uses the universal require()
function to include the Spice utility object textContext
(which contains methods for interacting with the Espresso text context) and utility class TextRecipe
(which allows access to Espresso text recipes).
Spice’s utility classes are provided in a modular system that allows you to only require what you need in order to run your action. Spice modules have the following naming conventions:
- The module is named for the primary class, converted to lowercase and with underscores between words (so primary class
TextActionContext
becomes module text_action_context
)
- Classes are all camel-case with the first letter capitalized (
TextActionContext
)
- Objects (instantiated versions of a class) are camel-case with the first letter lowercase (
textContext
is an instantiated version of TextActionContext
)
You can see what a given module exports in the Spice docs (along with full references to the object methods available). The text_action_context
module is one of the few that exports an object as well as the class, since you’ll never need more than one object for referencing the text context.
Once you’ve saved the Javascript file, you’ve officially created your first Espresso action! You can immediately run the action from the Actions menu, and it will delete the current line. If you test it out, though, you may notice that the action behaves differently when you delete the final line in the document; instead of removing the line completely, it only removes all the text. To fix this, you could modify the action like so:
// Deletes the current line
// require() allows you easy modular access to Spice's helper classes
var textContext = require('text_action_context').textContext;
var TextRecipe = require('text_recipe').TextRecipe;
var Range = require('range').Range;
// exports.main is your primary function, run automatically by Spice
exports.main = function() {
// Grab the range of the current line
var linerange = textContext.rangeForLine();
// If on the last line of the doc, remove the line break prior to the line
// This isn't strictly necessary, but it's nice to have
if (textContext.lineNumber() != 1 && textContext.rangeForLine(textContext.lineNumber() + 1) === false) {
linerange = new Range(linerange.location - 1, linerange.length + 1);
}
// Run the actual removal
return new TextRecipe().remove(linerange).apply();
}
The only addition is some logic to check if we’re on the last line of the document, and if so create a custom Range
object that includes the linebreak from the previous line.
Debugging
As you work on your own custom actions, you will of course run into problems and errors. The best way to debug is to keep Console.app open (located in your Applications/Utilities folder), since most errors will be output there. You can also use the globally available system.log('message')
to output directly to the console. Many Spice utility classes also include a log()
method to quickly log the contents of an object.
When using Spice, keep in mind that it’s still under development (at the time of this writing, it’s at version 1.0 beta 6). If you run into something that seems buggy or a limitation of the utility classes, please let me know.
Parting thoughts
Even if you don’t use Spice, the basic strategy for adding a custom action to Espresso remains the same. TEA offers several alternative methods to creating text actions (from Python scripts with full access to the API to scripts in arbitrary languages like Ruby or PHP that have more limited capabilities). Although the basic editor has a nice interface and some great features, I’ve found that the extensibility of Espresso is what keeps me coming back for more. A modicum of effort is often all that’s required to add a custom text action, so there’s little excuse for not giving it a try if you find yourself missing functionality you’ve grown used to in other editors.
If you do write any great custom actions, please think about sharing them either via a Sugar, GitHub, or the Espresso forums! I’d also love to hear how people are using Spice; I’m still planning out the additions I want to make now that I’ve got most of the basic Espresso API for text actions covered by the utility classes, so your input is extremely valuable.