ActiveBlog

Want Something? Pull a Trigger
by

, February 11, 2009

Although there's been a disconcerting spike in gang-related shootings here in Vancouver, this article is talking about Komodo's triggers, macros that fire automatically under certain conditions. By taking advantage of these macros, you can get Komodo to automatically work in...

Although there's been a disconcerting spike in gang-related shootings here in Vancouver, this article is talking about Komodo's triggers, macros that fire automatically under certain conditions. By taking advantage of these macros, you can get Komodo to automatically work in ways the designers never anticipated. As an example, in this past week, I've seen two requests for useful features, both of which wanted to change Komodo's behavior under certain conditions.

I would have asked the reporters to log an enhancement request at http://bugs.activestate.com/enter_bug.cgi?product=Komodo, but that always isn't the best route to take. If your request is honored, you might need to wait at least a full minor-rev cycle (for example, 5.1 is currently in alpha, and is feature-complete, so there won't be any new features released until at least 5.2). And the spirit of full disclosure forces me to admit that those people who get their request made in one minor-rev cycle, well..., they're the lucky ones.

Enter Komodo's agile, powerful macro-system to the rescue. You can write macros in either JavaScript or Python. Most of Komodo's front-end code is in JavaScript, and the back-end in Python. If you're considering writing macros in Python, they tend to be shorter, but interacting with some parts of the front-end are much harder, and sometimes impossible. However you can call any of the back-end Python code from JavaScript, and that's my preference.

Macro triggers are documented at http://docs.activestate.com/komodo/5.1/macros.html#macros_trigger_subject.
This post covers much of that information, but gives a couple of concrete examples. I'll also be showing a couple of strategies I use to figure out what things are called in Komodo. If Komodo documented every single preference name and configuration value, it would entail chopping down a minor forest, either to make way for the paper or for building the dam to power the server to deliver the on-line docs. Instead it pays to learn how to use the tools to examine and understand the source.

So the first query came from a fan of the feature that strips extra whitespace out of a buffer when you save it ([Preferences|Editor |Save Options]). However, he didn't want this to happen in YAML, where an empty line is different from a line containing only spaces.

Much of this kind of behavior is governed by preferences, so let's set out in search of the pref that covers white-space stripping on save, find the pref that governs its behavior, see how that pref is used, and write a macro to override it.

Fire up another browser window, point it at http://grok.openkomodo.com/source/, put "/openkomodo/trunk" in the "File Path" field, and follow along.

Our first step is to bring up the pref window we just mentioned. We can't copy and paste preference labels, so let's move to the search window, type "Clean trailing whitespace" (with the quotes, or you'll get many more hits), check the "only in /openkomodo/trunk" box, and search away. You should get two hits. Choose the one in the pref.dtd localization file. We want to get the name of the string reference, which is used in the code: "cleanTrailingWhitespaceAndEOLMarkers.label". Copy it, move the search back one, paste it into the search field, and you should see it's defined in the file we just looked at, and used in one other place, pref-save.xul. We should find the name of the actual pref at this point.

We find our string reference is the label of a checkbox widget, which makes sense. There's a bit of Mozilla magic going on here -- when you see an attribute called "pref" set to "true", it means the id attribute of the widget is the name of the pref the widget is wired to. So we can now do a search for "cleanLineEnds" -- hopefully everything is making sense so far.

This search leads to three files. We just saw pref-save.xul; prefs.p.xml contains a list of all the preferences Komodo knows about (in fact, next time we'll hunt for prefs using that file); so let's see how the pref is referenced in the third file, koDocument.py.

While the name "cleanLineEnds" shows up multiple times, the key line is 1153, which gets the boolean pref of the same name. Note how the pref is coming off "self._globalPrefs". We try to use consistent naming in the Komodo source, and this name suggests that there's one global pref that governs this choice.

Komodo has a short hierarchy of prefs. Some prefs live on a view or document, others are language-specific, then project-specific, and finally global. For example, when you turn visible whitespace on for one document, you're setting a document-specific pref. But since most documents don't have that pref explicitly set, they get the value from the global default, when they're created.

So we now know what we need to do. Before the document is saved, we want to turn the "cleanLineEnds" pref off (and why not the "ensureFinalEOL" for good measure)? After the document is saved, we want to restore the prefs, but we only want to do this for YAML files.

This is where we take advantage of Komodo's macro triggers, available at the third tab of a macro's properties dialog. We'll create two macros, with the following plan: Both of them will quickly disappear if they aren't dealing with a YAML file. Otherwise, the pre-save one will set the two prefs to false, if needed, and populate a persistent variable that will list the prefs' original values. And the post-save trigger will restore those prefs.

Here's the pre-save macro:

try {
     var view = (typeof("subject") != "undefined" ? subject
                 : ko.views.manager.currentView);
     if (view.languageObj.name != "YAML") {
          return;
     }
     if (!('extensions' in ko)) ko.extensions = {};
     ko.extensions.yamlPrefsToRestore = {};
     var gprefs = Components.classes["@activestate.com/koPrefService;1"].getService(Components.interfaces.koIPrefService).prefs
     var prefNames = {"cleanLineEnds":null, "ensureFinalEOL":null};
     for (var prefName in prefNames) {
          if (gprefs.getBooleanPref(prefName)) {
              gprefs.setBooleanPref(prefName, false);
              ko.extensions.yamlPrefsToRestore[prefName] = true;
          }
     }
} catch(ex) {
     ko.dialogs.alert("The pre-save macro failed", ex);
}

If you're new to writing Komodo macros, much of the above code could use a bit of explanation, so here goes.

First, I wrap all my macros in a try/except block. The final alert statement is friendlier than the standard alert, since the text is selectable with a mouse. Useful to make it easier for other users to report the exact text of an exception.

Second, since version 5.0, every trigger macro is invoked with an argument called "subject". For most of the trigger types, "subject" references the view that was associated with the trigger -- in this case the view containing the document that is about to be saved. (The main exception is the post-close macro: since the view's document has disappeared, "subject" gives the URI of the document. The startup and shutdown triggers don't define "subject".)

The check to make sure "subject" is defined means the macro will work with earlier versions. However, this won't work during a SaveAll command, because while the pre-save trigger will fire for each file that needs saving, the current view won't change. With 5.0 we now associate the main object with each trigger, not whichever buffer happens to be current when the macro run.

Third, we need to allow the two macros to communicate via persistent data. I use a namespace called "ko.extensions" for this. "ko" is Komodo's main global variable, so be careful where you stomp around. It's JavaScript, so you could use any variable without declaring it, but there's the risk that you could be stomping on a global used elsewhere.

The line that gets the global prefs object is a typical example of an XPCOM call. XPCOM is a powerful, if somewhat wordy, technology. Komodo's code-completion can help reduce the amount of typing you need to do here.

Finally, the rest of the code is garden-variety JavaScript, using calls on the prefs object.

I should mention that I still find the JavaScript shell invaluable for interactively building and testing macros and extensions. Get it from http://community.activestate.com/node/1824. ToddW even added a Python shell to it.

Now the post-file-open trigger practically writes itself:

try {
    if ((typeof("subject") != "undefined"
         ? subject
         : ko.views.manager.currentView).languageObj.name != "YAML") {
         return;
    }
    var gprefs = Components.classes["@activestate.com/koPrefService;1"].getService(Components.interfaces.koIPrefService).prefs
    for (prefName in ko.extensions.yamlPrefsToRestore) {
       gprefs.setBooleanPref(prefName, ko.extensions.yamlPrefsToRestore[prefName]);
    }
    delete ko.extensions.yamlPrefsToRestore;
} catch(ex) {
    ko.dialogs.alert("The post-save macro failed", ex);
}

There shouldn't be anything surprising here. I've compressed the check for "subject" into one statement. The only new part to point out is how the macro cleans up after itself when it's done with the global data.

You might be thinking this code isn't very thread-safe. It isn't, but it doesn't matter. All this code runs on the single UI thread, one save at a time. So we don't have to worry that we'll be mixing up file types. You might also be concerned that if the post-save macro doesn't fire for some reason, the state won't be correctly preserved. True, but this should only happen under exceptional circumstances, such as a meteorite smashing through a window into your computer right while you're saving a file. In which case you'll have bigger problems than mismatched actions.

So the second question, one business day later, was asking how to turn off HTML syntax-checking for certain types of documents, such as Django, Mason, and other templating languages that define a superset of HTML, and tend to toss Tidy, the HTML syntax checker Komodo uses, into a tizzy.

This time, rather than searching the codebase, let's dive into the prefs.p.xml file. If you were following along, it was the third hit in http://grok.openkomodo.com/source/search for "cleanLineEnds". Here it helps to know that the internal name for syntax checking is "linting", so let's use the browser's find command to find instances of "lint". The first hit is for "editUseLinting", with a default value of 1. Sounds good, but let's check for others. "lintDelay": nope. "lintEOLs": obviously not. A few other options mention specific languages, none of which are HTML, and we wrap around. So let's go back to the search field, and look for other hits of "editUseLinting".

The first set of hits are in files in chrome/komodo/content/pref -- In Mozilla apps, front-end files typically go in a folder called chrome, and Komodo stores all its pref-related widgets in content/pref/, so those are the hits for managing the pref. The key hits are in lint.js and views-editor.js, which show that Komodo is looking at the view to determine if it should "lint" a buffer.

Recall that in Komodo's prefs there are prefs that control whether to do syntax checking globally, and for each document. When you create a new document, or open one that Komodo doesn't have a set of prefs for in its cache, the document inherits various prefs from the global set. You can then change the handling of the document by setting prefs lower in the hierarchy, without affecting how other documents are handled.

So now our plan of action should become apparent -- we want to decide whether to do syntax-checking or not on HTML-like languages when we open a file. Fortunately Komodo supports post-file-open triggers -- here's the code:

try {
    var view = (typeof(subject) == "undefined"
                ? ko.views.manager.currentView
                : subject);
    var prefs = view.prefs;
    if (prefs.getBooleanPref("editUseLinting")) {
        var noLintLanguages = ["Django", "Mason"];
        if (noLintLanguages.indexOf(view.document.language) >= 0) {
            prefs.setBooleanPref("editUseLinting", false);
        } 
    }
} catch(ex) {
    alert("Trigger macro post-file-opened on file "
           + view.document.displayPath + " failed: " + ex);
}

Here we get the prefs object off the view, rather than the global prefs from last time, because we want to manipulate each document's pref settings. The other parts of the code build on what I've already presented.

If I decide that I want to have syntax-checking for a particular document, I can either right-click on the tab to get at the properties dialog, but that's clumsy. I'd rather create a toolbar button with this short macro:

try {
    var prefs = ko.views.manager.currentView.prefs;
    var p = "editUseLinting";
    prefs.setBooleanPref(p, !prefs.getBooleanPref(p));
} catch(ex) {
    alert("flipLint on "
       + view.document.displayPath + " failed: " + ex);
}

Hopefully, this article has given you some ideas on how to use triggers to implement your own custom behavior. Be sure to visit http://community.activestate.com/forums/komodo-extensions if you've got questions, want to browse other people's macros and extensions for ideas (also a good idea to ramp up on the API by seeing how features were implemented), or just hang out.

Subscribe to ActiveState Blogs by Email

Share this post: