Macro Programming
From Niki
Contents |
Basics
Record or write
The simplest way to create a macro is to use the recording feature on the macro menu. After recording you can name and save the macro (and give it a shortcut) using the macro dialog at Preferences->Default Settings->Customize Menus->Macro Menu. That way you are not limited to the one macro in the record buffer, they will reappear in later nedit sessions (if you save preferences afterwards), and will be visible in the macro menu.
You could also use the same macro dialog to write your macro from scratch. The built-in help contains comprehensive documentation of the macro language and API. Note that the API is divided into macro subroutines and action routines (at some point we hope to unify these two concepts). Be sure to scan both those pages if you can't find the feature you need.
Use Separate Files
The macro dialogs have several shortcomings:
- The text boxes are small,
- they don't have syntax highlighting,
- no subroutines are possible.
If you want to do more than a trivial macro, you should rather put them in subroutines in separate files somewhere in your NEdit home directory. Then add these files to your autoload.nm and use the macro dialog only to access them.
Example:
I have a dialoged grep that I use to find thing in the NEdit source tree. Instead of putting the ~150 lines in the tiny text box in the macro dialog, I put it in $NEDIT_HOME/macros/util.nm. Then I load this file by adding the following lines to autoload.nm:
$NEDIT_HOME = getenv("NEDIT_HOME")
load_macro_file($NEDIT_HOME "/macros/util.nm")
After that, all I have to put in the dialog is a little code to call the grep subroutine:
if ($selection_start == -1)
{
nGrep()
} else
{
nGrep(get_selection("any"))
}
For work on macro files the following macro comes handy for quick-testing your newest changes:
cur_file = $file_path $file_name
t = read_file(cur_file)
if ($read_status != 0) {
# file exists on disk, ie it is not untitled window
if ($modified == 1) save()
load_macro_file(cur_file)
}
Make this a menu item and assign a key to it so that you can easily reload the file you are working at.
Pitfalls
Namespace clashes
A user is likely to use more macro packages than just yours. Most sensible names simply expressing what a function or variable is about is likely to have been used by someone else too. NEdit does not currently provide any namespace abstraction to tackle this problem robustly.
Workaround: use a short prefix before all your identifiers in the same package. You still have no guarantee that no one else uses the same prefix, but it's a lot less likely, and if that happens at least it is easy to fix with a simple search/replace on the prefixes, without having to find out which function names actually clash. The verbosity might be a bit annoying, but robustness makes up for it.
load_macro_file() parsing order bug
In older versions, if a macro file loaded using this command contains both function definitions and literal code, the code chunks before/after/between function definitions are scanned in reverse order.
Workaround: Place all literal code below all the function definitions.
Fix: Upgrade to something newer than 5.3.
Performance issues
Some advice on avoiding performance traps in NEdit macro programming can be found on the page Macro performance and memory usage.
Style, tips and tricks
Naming your parameters
Function parameters aren't named in nedit macros; you just use the numbered parameters and hope they're there. This is not ideal for code quality, so to make the code more readable and less error-prone, I make it a rule to start every function by assigning the parameters to named local variables. So instead of:
define area{
return $1 * $2
}
I do:
define area{
width = $1
height = $2
return width * height
}
Actually, I might not bother with macros this trivial, but even for moderate-sized macro functions (and they always tend to grow) you will quickly experience that the little extra typing pays off double in debugging and general readability. It also lets you change the function signature with much less hassle. Exception: If you must pass large chunks of data as parameters (try to avoid this in any case), copying like this can hurt performance if the function is called very frequently. For most macros you will never notice it, so as with all optimisation: check that you actually have a problem before you spend time and make trade-offs to fix it --JoachimLous
Using "uninitialized" variables
The Problem When writing a macro you may want to retain some information from one call to the next. For this you can use a global variable, starting with a $-sign. Your macro can store the information there from one call to the next. But there's a catch: typically with this sort of thing, you will want to read from that variable before assigning to it. For example, imagine a macro that pastes the same string as last time unless a selection exists, in which case it inserts the selection text and also saves it for the next time. (A sort-of auto-copy-paste function). Now if invoked without a selection the first time, you will probably get a nasty macro failure mentioning the use of an uninitialized variable: the global one, supposedly containing the text to insert. Now to get around this, you initialize your global in the macro, but that would reset it at every call. You can't test the variable, because that would cause the same error... and there is no "this is an unassigned variable" function either.
A Solution One way out of this conundrum is to initialize the global in your autoload.nm macro initialization file. The snag is that the initialization is far from the only place the variable is ever used, and if some day you drop the macro, or change it, you have to make a corresponding change to autoload.nm too. So this solution requires non-local coupling between separate components - a bad thing.
A Better Solution You can store this sort of state in a global array, initialized in the macro. The trick is that here, you don't need to initialize the whole array, just one array entry which you won't need for anything else. At each call to your macro, that entry is reassigned - it won't hurt you, but it also won't destroy anything else you may have added in the array, like that saved information. The beauty is that you can test whether an entry already exists in an array, and act accordingly. So, with this technique, the pasting macro can keep its global close at hand; it could be implemented like this:
$MY_PASTE[""] = 0 # make sure global array $MY_PASTE exists
data = get_selection("any") # pick up any selected text
# if none found, use what was used last time - if at all
if (data == "" && ("buffer" in $MY_PASTE))
data = $MY_PASTE["buffer"]
# nothing to paste?
if (data == "") {
beep()
return
}
# insert the data to paste
pos = $cursor
replace_range(pos, pos, data)
set_cursor_pos(pos + length(data))
# save for next time
$MY_PASTE["buffer"] = data
So here, you set a dummy element every time, but its sole purpose is to create the global array $MY_PASTE the first time, and make sure it contains something. The test for the uninitialized entry, with index buffer, is the salient part: you use that entry as your store between calls, testing whether it exists, and emitting a subtle beep when it doesn't but should, instead of a noisy macro runtime error.
-- TonyBalinski
