diff --git a/plugins/pilot/lib/canon.js b/plugins/pilot/lib/canon.js new file mode 100644 index 00000000..bdb98558 --- /dev/null +++ b/plugins/pilot/lib/canon.js @@ -0,0 +1,262 @@ +/* ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is Mozilla Skywriter. + * + * The Initial Developer of the Original Code is + * Mozilla. + * Portions created by the Initial Developer are Copyright (C) 2009 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * Joe Walker (jwalker@mozilla.com) + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ + +define(function(require, exports, module) { + +var console = require('util/console'); +var Trace = require('util/stacktrace').Trace; +var Event = require('events').Event; +var history = require('canon/history'); + +exports.startup = function(data, reason) { + var settings = data.env.settings; + // TODO register these using new registration functionality + + // catalog.addExtensionPoint("command", { + // "description": + // "A command is a bit of functionality with optional typed arguments which can do something small like moving the cursor around the screen, or large like cloning a project from VCS.", + // "indexOn": "name" + // }); + // catalog.addExtensionPoint("addedRequestOutput", { + // "description": + // "An extension point to be called whenever a new command begins output." + // }); + // catalog.addExtensionPoint("dimensionsChanged", { + // "description": + // "A dimensionsChanged is a way to be notified of changes to the dimension of Skywriter" + // }); + settings.addSetting({ + "name": "historyLength", + "description": "How many typed commands do we recall for reference?", + "type": "number", + "defaultValue": 50 + }); +}; + +exports.shutdown = function(data, reason) { + var settings = data.env.settings; + settings.removeSetting('historyLength'); +}; + +exports.Canon = function() { + this._commands = {}; +}; + +exports.Canon.prototype = { + addCommand: function(options) { + if (!options.name) { + throw new Error("All registered commands must have a name"); + } + this._commands[name] = options; + }, + + removeCommand: function(name) { + delete this._commands[name]; + } +}; + +exports.addedRequestOutput = new Event(); + +/** + * Current requirements are around displaying the command line, and provision + * of a 'history' command and cursor up|down navigation of history. + *
Future requirements could include: + *
The execute() command doesn't really live here, except as part of that + * last future requirement, and because it doesn't really have anywhere else to + * live. + */ + +/** + * The array of requests that wish to announce their presence + */ +exports.requests = []; + +/** + * How many requests do we store? + */ +var maxRequestLength = 100; + +/** + * Called by Request instances when some output (or a cell to async() happens) + */ +exports.addRequestOutput = function(request) { + exports.requests.push(request); + // This could probably be optimized with some maths, but 99.99% of the + // time we will only be off by one, and I'm feeling lazy. + while (exports.requests.length > maxRequestLength) { + exports.requests.shiftObject(); + } + + exports.addedRequestOutput(request); +}; + +/** + * Execute a new command. + * This is basically an error trapping wrapper around request.command(...) + */ +exports.execute = function(args, request) { + // Check the function pointed to in the meta-data exists + if (!request.command) { + request.doneWithError('Command not found.'); + return; + } + + try { + request.command(args, request); + } catch (ex) { + var trace = new Trace(ex, true); + console.group('Error executing command \'' + request.typed + '\''); + console.log('command=', request.commandExt); + console.log('args=', args); + console.error(ex); + trace.log(3); + console.groupEnd(); + + request.doneWithError(ex); + } +}; + +/** + * To create an invocation, you need to do something like this (all the ctor + * args are optional): + *
+ * var request = new Request({
+ * command: command,
+ * commandExt: commandExt,
+ * args: args,
+ * typed: typed
+ * });
+ *
+ */
+exports.Request = function(options) {
+ options = options || {};
+
+ // Will be used in the keyboard case and the cli case
+ this.command = options.command;
+ this.commandExt = options.commandExt;
+
+ // Will be used only in the cli case
+ this.args = options.args;
+ this.typed = options.typed;
+
+ // Have we been initialized?
+ this._begunOutput = false;
+
+ this.start = new Date();
+ this.end = null;
+ this.completed = false;
+ this.error = false;
+
+ this.changed = new Event();
+};
+
+/**
+ * Lazy init to register with the history should only be done on output.
+ * init() is expensive, and won't be used in the majority of cases
+ */
+exports.Request.prototype._beginOutput = function() {
+ this._begunOutput = true;
+ this.outputs = [];
+
+ history.addRequestOutput(this);
+};
+
+/**
+ * Sugar for:
+ * request.error = true; request.done(output);+ */ +exports.Request.prototype.doneWithError = function(content) { + this.error = true; + this.done(content); +}; + +/** + * Declares that this function will not be automatically done when + * the command exits + */ +exports.Request.prototype.async = function() { + if (!this._begunOutput) { + this._beginOutput(); + } +}; + +/** + * Complete the currently executing command with successful output. + * @param output Either DOM node, an SproutCore element or something that + * can be used in the content of a DIV to create a DOM node. + */ +exports.Request.prototype.output = function(content) { + if (!this._begunOutput) { + this._beginOutput(); + } + + if (typeof content !== 'string' && !(content instanceof Node)) { + content = content.toString(); + } + + this.outputs.push(content); + this.changed(); + + return this; +}; + +/** + * All commands that do output must call this to indicate that the command + * has finished execution. + */ +exports.Request.prototype.done = function(content) { + this.completed = true; + this.end = new Date(); + this.duration = this.end.getTime() - this.start.getTime(); + + if (content) { + this.output(content); + } else { + this.changed(); + } +}; + + + +});