I thought you’d be able to do this with Spice.sugar, but I actually hadn’t coded any line handling methods yet. That is now fixed as of 1.0b4, so here’s a quick tutorial/intro to Spice to allow you to add these move line actions yourself. I opted to write up an example for Spice rather than TEA because I’m hoping to make some converts; I’ve been surprised at how few people have tried it out, and it helps me a lot to know how to improve it if I can get people using it. Spice is a great way to get into Espresso actions because it allows you to code Javascript (which should be a lot easier for most people on the forums than Python) and it also provides numerous helper classes to make life easier so you don’t have to worry about knowing the Espresso API or Objective-C/JSCocoa.
First off, install Spice 1.0b4:
http://onecrayon.com/downloads/Spice.zip
To get this example working, you’ll need to enable custom user actions in the TEA preferences (Actions=>TEA=>Preferences), if you haven’t already. Then navigate here:
~/Library/Application Support/Espresso/
And create a folder called Support. Inside Support create two folders: TextActions and Scripts. Inside TextActions, create a file called Spice.xml with these contents:
<action-recipes>
<action id="com.onecrayon.MoveLineUp" category="actions.text.generic">
<class>Spice</class>
<title>Move Line Up</title>
<setup>
<script>move_line</script>
<undo_name>Move Line Up</undo_name>
<arguments>
<array>
<string>up</string>
</array>
</arguments>
</setup>
</action>
<action id="com.onecrayon.MoveLineDown" category="actions.text.generic">
<class>Spice</class>
<title>Move Line Down</title>
<setup>
<script>move_line</script>
<undo_name>Move Line Down</undo_name>
<arguments>
<array>
<string>down</string>
</array>
</arguments>
</setup>
</action>
</action-recipes>
This defines two actions, both of which use the same Javascript file. More info about XML action definitions for Spice. You’ll need to restart Espresso twice after creating this file to get your actions recognized (this is due to limitations in how Espresso loads actions from XML).
In the Scripts folder create a file called move_line.js with the following contents:
// move_line
// Moves the current line up or down
// require() allows you easy modular access to Spice's helper classes
// These particular modules are documented here:
// http://onecrayon.com/spice/docs/text_action_context-js/
// http://onecrayon.com/spice/docs/text_recipe-js/
var textContext = require('text_action_context').textContext;
var TextRecipe = require('text_recipe').TextRecipe;
// exports.main is your primary function
// You could do without a function, but you couldn't return false to have Espresso beep
exports.main = function(direction) {
// Make sure that there's a direction parameter passed
if (!$chk(direction)) {
log('move_line requires a single string parameter ("up" or "down" are valid)');
return false;
}
// Grab our current and target line numbers
var current = textContext.lineNumber();
var target;
if (direction.toLowerCase() == 'up')
target = current - 1;
else
target = current + 1;
// Grab the ranges for the current and target line numbers
current = textContext.rangeForLine(current);
next = textContext.rangeForLine(target);
// Make sure the target line is inside document bounds
if (next !== false) {
// Create a new text recipe
var recipe = new TextRecipe();
// Swap linebreaks to make sure that things work at the doc's end
var currentText = current.string();
var curBreak = currentText.replace(/^.*([\n\r]*)$/, '$1');
var nextText = next.string();
var nextBreak = nextText.replace(/^.*([\n\r]*)$/, '$1');
currentText = currentText.replace(/^(.*)[\n\r]*$/, '$1' + nextBreak);
nextText = nextText.replace(/^(.*)[\n\r]*$/, '$1' + curBreak);
// For some reason the order you specify the replacements matters; not sure why
if (direction.toLowerCase() == 'up')
recipe.replace(currentText, next).replace(nextText, current).apply();
else
recipe.replace(nextText, current).replace(currentText, next).apply();
// Now that the line's moved, select it
textContext.setSelection(textContext.rangeForLine(target));
return true;
} else {
return false;
}
}
After creating the Javascript file, you don’t have to relaunch Espresso at all. It will always execute the most recently saved version of the file, so you can experiment in real time. You only have to restart if you change the XML (and only have to restart twice if you add a new XML file).
I’ve commented things pretty heavily in the hopes that it’ll be useful for figuring out exactly what’s going on, and it should provide a good example of manipulating text using Spice. The functionality is pretty basic: it simply swaps the current line (line your cursor is on, or line at the start of your first selection) with the line immediately above or below it. You could, if you wanted, get a little fancier and code in handling for moving whole selections, etc. I leave it to you to add shortcuts to the actions or expand on the Javascript logic (see the Spice docs for help). If you get to tinkering, remember you can use log(message) to output things to Console.app (very useful for debugging). Enjoy!