From 38c9fc7a93ebf6eb186d37dbf4e6006132931d3e Mon Sep 17 00:00:00 2001 From: Joe Walker Date: Wed, 8 Dec 2010 13:13:33 +0000 Subject: [PATCH] a big lump of work to get the command line able to execute commands, and to hack in a trivial cli --- demo/boot.js | 7 +- demo/demo_startup.js | 2 +- editor.html | 15 +- plugins/{pilot => cockpit}/lib/cli.js | 531 +++++++++--------- plugins/cockpit/lib/index.js | 59 ++ plugins/{pilot => cockpit}/lib/test/assert.js | 0 .../{pilot => cockpit}/lib/test/testCli.js | 154 +++-- plugins/cockpit/lib/test/testNothing.js | 40 ++ plugins/cockpit/lib/ui/plain.css | 2 + plugins/cockpit/lib/ui/plain.js | 98 ++++ plugins/pilot/lib/canon.js | 2 +- plugins/pilot/lib/index.js | 5 +- plugins/pilot/lib/keyboard/index.js | 459 +++++++++++++++ plugins/pilot/lib/keyboard/keyutil.js | 273 +++++++++ .../pilot/lib/keyboard/tests/testKeyboard.js | 99 ++++ plugins/pilot/lib/rangeutils.js | 185 ++++++ plugins/pilot/lib/tests/testRangeutils.js | 163 ++++++ 17 files changed, 1715 insertions(+), 379 deletions(-) rename plugins/{pilot => cockpit}/lib/cli.js (89%) create mode 100644 plugins/cockpit/lib/index.js rename plugins/{pilot => cockpit}/lib/test/assert.js (100%) rename plugins/{pilot => cockpit}/lib/test/testCli.js (78%) create mode 100644 plugins/cockpit/lib/test/testNothing.js create mode 100644 plugins/cockpit/lib/ui/plain.css create mode 100644 plugins/cockpit/lib/ui/plain.js create mode 100644 plugins/pilot/lib/keyboard/index.js create mode 100644 plugins/pilot/lib/keyboard/keyutil.js create mode 100644 plugins/pilot/lib/keyboard/tests/testKeyboard.js create mode 100644 plugins/pilot/lib/rangeutils.js create mode 100644 plugins/pilot/lib/tests/testRangeutils.js diff --git a/demo/boot.js b/demo/boot.js index 369d216d..8cb3e6e3 100644 --- a/demo/boot.js +++ b/demo/boot.js @@ -45,7 +45,7 @@ var setupPlugins = function(config, callback) { // packages: ["ace"] // }; config.pluginDirs["../plugins"] = { - packages: ["pilot"] + packages: ["pilot", "cockpit"] }; var knownPlugins = []; @@ -109,7 +109,10 @@ var setupPlugins = function(config, callback) { } } } - +console.log(JSON.stringify({ + packagePaths: pluginPackageInfo, + paths: paths + })); require({ packagePaths: pluginPackageInfo, paths: paths diff --git a/demo/demo_startup.js b/demo/demo_startup.js index 8c2ba458..9ce6b585 100644 --- a/demo/demo_startup.js +++ b/demo/demo_startup.js @@ -150,7 +150,7 @@ exports.launch = function(env) { function onResize() { container.style.width = (document.documentElement.clientWidth - 4) + "px"; - container.style.height = (document.documentElement.clientHeight - 55 - 4) + "px"; + container.style.height = (document.documentElement.clientHeight - 55 - 4 - 23) + "px"; env.editor.resize(); }; diff --git a/editor.html b/editor.html index bedc4d8d..ba91335a 100644 --- a/editor.html +++ b/editor.html @@ -47,6 +47,11 @@ display: none; } + #cockpit { + position: absolute; + width: 100%; + bottom: 0; + } - + + + \ No newline at end of file diff --git a/plugins/pilot/lib/cli.js b/plugins/cockpit/lib/cli.js similarity index 89% rename from plugins/pilot/lib/cli.js rename to plugins/cockpit/lib/cli.js index b7bb1419..027ba9f8 100644 --- a/plugins/pilot/lib/cli.js +++ b/plugins/cockpit/lib/cli.js @@ -142,31 +142,221 @@ Argument.merge = function(argArray, start, end) { /** - * CLI / UI Interface - * The Cli interacts with the UI via an instance of CliUi. - * This implementation is designed as a template rather than to be used. - * It is expected that we will have a number of implementations of this: - * - A firebug/webkit inspector cli shim - * - A simple input[type=text] version - * - A possible Cloud9 UI version - * - A possible Skywriter UI version - * This class will probably need refactoring as time goes on. - * - * TODO: Who should own the Requisition? + * A link between a parameter and the data for that parameter. + * The data for the parameter is available as in the preferred type and as + * an Argument for the CLI. + *

We also record validity information where applicable. + *

For values, null and undefined have distinct definitions. null means + * that a value has been provided, undefined means that it has not. + * Thus, null is a valid default value, and common because it identifies an + * parameter that is optional. undefined means there is no value from + * the command line. + * @constructor */ -function CliUi() { -} -CliUi.prototype = { - getSelection: function() {}, - setHints: function() {}, - setRequisition: function() {} +function Assignment(param) { + this.param = param; + this.setValue(param.defaultValue); }; +Assignment.prototype = { + /** + * The parameter that we are assigning to + * @readonly + */ + param: undefined, + + /** + * The current value (i.e. not the string representation) + * Use setValue() to mutate + */ + value: undefined, + setValue: function(value) { + if (this.value === value) { + return; + } + if (value === undefined) { + value = this.param.defaultValue; + } + this.value = value; + + var text = (value == null) ? '' : this.param.type.stringify(value); + if (this.arg) { + this.arg.setText(text); + } + + this.conversion = undefined; + //this._dispatchEvent('change', { assignment: this }); + }, + + /** + * The textual representation of the current value + * Use setValue() to mutate + */ + arg: undefined, + setArgument: function(arg) { + if (this.arg === arg) { + return; + } + this.arg = arg; + this.conversion = this.param.type.parse(arg.text); + this.value = this.conversion.value; + //this._dispatchEvent('change', { assignment: this }); + }, + + /** + * Create a list of this hints associated with this parameter assignment + */ + getHints: function() { + var hints = []; + if (this.conversion != null && + (this.conversion.status !== Status.VALID || + this.conversion.message)) { + hints.push(new ConversionHint(this.conversion, this.arg)); + } + + var argProvided = this.arg != null && this.arg.text !== ''; + var dataProvided = this.value !== undefined || argProvided; + + if (this.param.defaultValue === undefined && !dataProvided) { + // If the there is no data provided, we have no start/end. Use -1 + hints.push(new Hint(Status.INVALID, + 'Argument for ' + param.name + ' is required' + -1, -1)); + } + return hints; + }, + + /** + * Report on the status of the last parse() conversion. + * @see types.Conversion + */ + conversion: undefined +}; +oop.implement(Assignment, EventEmitter); +exports.Assignment = Assignment; + + +/** + * A Requisition collects the information needed to execute a command. + * There is no point in a requisition for parameter-less commands because there + * is no information to collect. A Requisition is a collection of assignments + * of values to parameters, each handled by an instance of Assignment. + * CliRequisition adds functions for parsing input from a command line to this + * class + * @constructor + */ +function Requisition() { +} +Requisition.prototype = { + /** + * The command that we are about to execute. + * @readonly + */ + command: undefined, + + /** + * The count of assignments + * @readonly + */ + assignmentCount: undefined, + + /** + * Set a new command. We make no attempt to convert the args in the old + * command to args in the new command. The assignments need to be + * re-entered. + */ + setCommand: function(command) { + if (this.command === command) { + return; + } + + this.command = command; + this._assignments = {}; + + if (command) { + command.params.forEach(function(param) { + this._assignments[param.name] = new Assignment(param); + }, this); + } + + this.assignmentCount = Object.keys(this._assignments); + }, + + /** + * Assignments have an order, so we need to store them in an array. + * But we also need named access ... + */ + getAssignment: function(nameOrNumber) { + var name = (typeof nameOrNumber === 'string') ? + nameOrNumber : + Object.keys(this._assignments)[nameOrNumber]; + return this._assignments[name]; + }, + + /** + * Where parameter name == assignment names - they are the same. + */ + getParameterNames: function() { + return Object.keys(this._assignments); + }, + + /** + * A *shallow* clone of the assignments. + * This is useful for systems that wish to go over all the assignments + * finding values one way or another and wish to trim an array as they go. + */ + cloneAssignments: function() { + return Object.keys(this._assignments).map(function(name) { + return this._assignments[name]; + }, this); + }, + + /** + * Collect the statuses from the Assignments + */ + getHints: function() { + var hints = []; + Object.keys(this._assignments).map(function(name) { + // Append the assignments hints to our list + hints.push.apply(hints, this._assignments[name].getHints()); + }, this); + return hints; + }, + + /** + * Extract the names and values of all the assignments, and return as + * an object. + */ + getArgs: function() { + var args = {}; + Object.keys(this._assignments).forEach(function(name) { + args[name] = this.getAssignment(name).value; + }, this); + return args; + }, + + /** + * Reset all the assignments to their default values + */ + setDefaultValues: function() { + Object.keys(this._assignments).forEach(function(name) { + this._assignments[name].setValue(undefined); + }, this); + }, + + /** + * Helper to call canon.exec + */ + exec: function() { + canon.exec(this.command, this.getArgs()); + } +}; +exports.Requisition = Requisition; /** * An object used during command line parsing to hold the various intermediate * data steps. - *

The 'output' of the parse is held in 2 objects: input.hints which is an + *

The 'output' of the update is held in 2 objects: input.hints which is an * array of hints to display to the user. In the future this will become a * single value. *

The other output value is input.requisition which gives access to an @@ -187,99 +377,87 @@ CliUi.prototype = { * if not specified. * @constructor */ -function Cli(cliui, options) { - this.cliui = cliui; +function CliRequisition(options) { if (options && options.flags) { + /** + * TODO: We were using a default of keyboard.buildFlags({ }); + * This allowed us to have commands that only existed in certain contexts + * - i.e. Javascript specific commands. + */ this.flags = options.flags; } - - this.requisition = new Requisition(); - this.cliui.setRequisition(this.requisition); } -Cli.prototype = { - /** - * TODO: We were using a default of keyboard.buildFlags({ }); - * This allowed us to have commands that only existed in certain contexts - * - i.e. Javascript specific commands. - */ - flags: {}, - +oop.inherits(CliRequisition, Requisition); +(function() { /** * */ - parse: function(typed) { - if (util.none(typed)) { - this.requisition.setCommand(null); - this.cliui.setHints([]); + CliRequisition.prototype.update = function(input) { + this.hints = []; + + if (util.none(input.typed)) { + this.setCommand(null); return; } - this.typed = typed; - this.hints = []; - - var args = _tokenize(this.typed); + var args = _tokenize(input.typed); if (args.length === 0) { // We would like to put some initial help here, but for anyone but // a complete novice a 'type help' message is very annoying, so we // need to find a way to only display this message once, or for // until the user click a 'close' button or similar this._addHint(Status.INCOMPLETE, '', 0, 0); - this.requisition.setCommand(null); - this.cliui.setHints(this.hints); + this.setCommand(null); return; } var command = _split(args); - if (!command) { // No command found - bail helpfully. var commandType = types.getType('command'); - var conversion = commandType.parse(typed); + var conversion = commandType.parse(input.typed); var arg = Argument.merge(args); this._addHint(new ConversionHint(conversion, arg)); - this.requisition.setCommand(null); + this.setCommand(null); } else { // The user hasn't started to type any arguments if (args.length === 0) { var message = documentCommand(command); - this._addHint(Status.VALID, message, 0, typed.length); + this._addHint(Status.VALID, message, 0, input.typed.length); } - this.requisition.setCommand(command); + this.setCommand(command); this._assign(args); - this._addHint(this.requisition.getHints()); + this._addHint(CliRequisition.super_.getHints.call(this)); } - // TODO: This is the wrong place to filter this. - // It should be done by the CliUi because: - // - the cursor could move without notice - // - not all interfaces will have a notion of one cursor for the whole assignment - // Not knowing about cursor positioning, the requisition and assignments // can't know this, but anything they mark as INCOMPLETE is actually // INVALID unless the cursor is actually inside that argument. - var sel = this.cliui.getSelection(); + var c = input.cursor; this.hints.forEach(function(hint) { - var startInHint = sel.start >= hint.start && sel.start <= hint.end; - var endInHint = sel.end >= hint.start && sel.end <= hint.end; + var startInHint = c.start >= hint.start && c.start <= hint.end; + var endInHint = c.end >= hint.start && c.end <= hint.end; var inHint = startInHint || endInHint; if (!inHint && hint.status === Status.INCOMPLETE) { hint.status = Status.INVALID; } }, this); - this.cliui.setHints(this.hints); - return; - }, + }; + + CliRequisition.prototype.getHints = function() { + return this.hints; + }; /** * Some sugar around: 'this.hints.push(new Hint(...));', but you can also * pass in an array of Hints or the parameters to create a hint */ - _addHint: function(status, message, start, end) { + CliRequisition.prototype._addHint = function(status, message, start, end) { if (status == null) { return; } @@ -292,7 +470,7 @@ Cli.prototype = { else { this.hints.push(new Hint(status, message, start, end)); } - }, + }; /** * Work out which arguments are applicable to which parameters. @@ -304,36 +482,36 @@ Cli.prototype = { *

  • value - The matching input * */ - _assign: function(args) { + CliRequisition.prototype._assign = function(args) { if (args.length === 0) { - this.requisition.setDefaultValues(); + this.setDefaultValues(); return; } // Create an error if the command does not take parameters, but we have // been given them ... - if (this.requisition.assignmentCount === 0) { + if (this.assignmentCount === 0) { // TODO: previously we were doing some extra work to avoid this if // we determined that we had args that were all whitespace, but // probably given our tighter tokenize() this won't be an issue? this._addHint(Status.INVALID, - this.requisition.command.name + ' does not take any parameters', + this.command.name + ' does not take any parameters', Argument.merge(args)); return; } // Special case: if there is only 1 parameter, and that's of type // text we put all the params into the first param - if (this.requisition.assignmentCount == 1) { - var assignment = this.requisition.getAssignment(0); + if (this.assignmentCount == 1) { + var assignment = this.getAssignment(0); if (assignment.param.type.name === 'text') { assignment.setArgument(Argument.merge(args)); return; } } - var assignments = this.requisition.cloneAssignments(); - var names = this.requisition.getParameterNames(); + var assignments = this.cloneAssignments(); + var names = this.getParameterNames(); // Extract all the named parameters var used = []; @@ -376,7 +554,7 @@ Cli.prototype = { // What's left are positional parameters assign in order names.forEach(function(name) { - var assignment = this.requisition.getAssignment(name); + var assignment = this.getAssignment(name); if (args.length === 0) { // No more values assignment.setValue(undefined); // i.e. default @@ -394,9 +572,10 @@ Cli.prototype = { 'Input \'' + remaining.text + '\' makes no sense.', remaining); } - } -}; -exports.Cli = Cli; + }; + +})(); +exports.CliRequisition = CliRequisition; /** * Split up the input taking into account ' and " @@ -637,214 +816,6 @@ function documentCommand(command) { exports.documentCommand = documentCommand; -/** - * A Requisition collects the information needed to execute a command. - * There is no point in a requisition for parameter-less commands because there - * is no information to collect. A Requisition is a collection of assignments - * of values to parameters, each handled by an instance of Assignment. - * @constructor - */ -function Requisition() { -} -Requisition.prototype = { - /** - * The command that we are about to execute. - * @readonly - */ - command: undefined, - - /** - * The count of assignments - * @readonly - */ - assignmentCount: undefined, - - /** - * Set a new command. We make no attempt to convert the args in the old - * command to args in the new command. The assignments need to be - * re-entered. - */ - setCommand: function(command) { - if (this.command === command) { - return; - } - - this.command = command; - this._assignments = {}; - - if (command) { - command.params.forEach(function(param) { - this._assignments[param.name] = new Assignment(param); - }, this); - } - - this.assignmentCount = Object.keys(this._assignments); - }, - - /** - * Assignments have an order, so we need to store them in an array. - * But we also need named access ... - */ - getAssignment: function(nameOrNumber) { - var name = (typeof nameOrNumber === 'string') ? - nameOrNumber : - Object.keys(this._assignments)[nameOrNumber]; - return this._assignments[name]; - }, - - /** - * Where parameter name == assignment names - they are the same. - */ - getParameterNames: function() { - return Object.keys(this._assignments); - }, - - /** - * A *shallow* clone of the assignments. - * This is useful for systems that wish to go over all the assignments - * finding values one way or another and wish to trim an array as they go. - */ - cloneAssignments: function() { - return Object.keys(this._assignments).map(function(name) { - return this._assignments[name]; - }, this); - }, - - /** - * Collect the statuses from the Assignments - */ - getHints: function() { - var hints = []; - Object.keys(this._assignments).map(function(name) { - // Append the assignments hints to our list - hints.push.apply(hints, this._assignments[name].getHints()); - }, this); - return hints; - }, - - /** - * Extract the names and values of all the assignments, and return as - * an object. - */ - getArgs: function() { - var args = {}; - Object.keys(this._assignments).forEach(function(name) { - args[name] = getCommand(name); - }, this); - return args; - }, - - /** - * Reset all the assignments to their default values - */ - setDefaultValues: function() { - Object.keys(this._assignments).forEach(function(name) { - this._assignments[name].setValue(undefined); - }, this); - }, - - /** - * Helper to call canon.exec - */ - exec: function() { - exports.exec(this.command, this.getArgs()); - } -}; -exports.Requisition = Requisition; - - -/** - * A link between a parameter and the data for that parameter. - * The data for the parameter is available as in the preferred type and as - * an Argument for the CLI. - *

    We also record validity information where applicable. - *

    For values, null and undefined have distinct definitions. null means - * that a value has been provided, undefined means that it has not. - * Thus, null is a valid default value, and common because it identifies an - * parameter that is optional. undefined means there is no value from - * the command line. - * @constructor - */ -function Assignment(param) { - this.param = param; - this.setValue(param.defaultValue); -}; -Assignment.prototype = { - /** - * The parameter that we are assigning to - * @readonly - */ - param: undefined, - - /** - * The current value (i.e. not the string representation) - * Use setValue() to mutate - */ - value: undefined, - setValue: function(value) { - if (this.value === value) { - return; - } - if (value === undefined) { - value = this.param.defaultValue; - } - this.value = value; - - var text = (value == null) ? '' : this.param.type.stringify(value); - if (this.arg) { - this.arg.setText(text); - } - - this.conversion = undefined; - //this._dispatchEvent('change', { assignment: this }); - }, - - /** - * The textual representation of the current value - * Use setValue() to mutate - */ - arg: undefined, - setArgument: function(arg) { - if (this.arg === arg) { - return; - } - this.arg = arg; - this.conversion = this.param.type.parse(arg.text); - this.value = this.conversion.value; - //this._dispatchEvent('change', { assignment: this }); - }, - - /** - * Create a list of this hints associated with this parameter assignment - */ - getHints: function() { - var hints = []; - if (this.conversion != null && - (this.conversion.status !== Status.VALID || - this.conversion.message)) { - hints.push(new ConversionHint(this.conversion, this.arg)); - } - - var argProvided = this.arg != null && this.arg.text !== ''; - var dataProvided = this.value !== undefined || argProvided; - - if (this.param.defaultValue === undefined && !dataProvided) { - // If the there is no data provided, we have no start/end. Use -1 - hints.push(new Hint(Status.INVALID, - 'Argument for ' + param.name + ' is required' - -1, -1)); - } - return hints; - }, - - /** - * Report on the status of the last parse() conversion. - * @see types.Conversion - */ - conversion: undefined -}; -oop.implement(Assignment, EventEmitter); -exports.Assignment = Assignment; }); diff --git a/plugins/cockpit/lib/index.js b/plugins/cockpit/lib/index.js new file mode 100644 index 00000000..beacc28f --- /dev/null +++ b/plugins/cockpit/lib/index.js @@ -0,0 +1,59 @@ +/* ***** 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): + * Kevin Dangoor (kdangoor@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) { + +exports.startup = function(data, reason) { + + window.testCli = require('cockpit/test/testCli'); + + var plain = require('cockpit/ui/plain'); + plain.startup(data, reason); + +}; + +/* +exports.shutdown(data, reason) { + deps.forEach(function(dep) { + var module = require(dep); + if (typeof module.shutdown === "function") { + module.shutdown(data, reason); + } + }); +}; +*/ +}); diff --git a/plugins/pilot/lib/test/assert.js b/plugins/cockpit/lib/test/assert.js similarity index 100% rename from plugins/pilot/lib/test/assert.js rename to plugins/cockpit/lib/test/assert.js diff --git a/plugins/pilot/lib/test/testCli.js b/plugins/cockpit/lib/test/testCli.js similarity index 78% rename from plugins/pilot/lib/test/testCli.js rename to plugins/cockpit/lib/test/testCli.js index 1b69c78e..bace956e 100644 --- a/plugins/pilot/lib/test/testCli.js +++ b/plugins/cockpit/lib/test/testCli.js @@ -38,10 +38,13 @@ define(function(require, exports, module) { -var test = require('pilot/test/assert').test; -var cli = require('pilot/cli'); +var test = require('cockpit/test/assert').test; var Status = require('pilot/types').Status; var settings = require('pilot/settings').settings; +var tokenize = require('cockpit/cli')._tokenize; +var split = require('cockpit/cli')._split; +var CliRequisition = require('cockpit/cli').CliRequisition; + exports.testAll = function() { exports.testTokenize(); @@ -50,41 +53,18 @@ exports.testAll = function() { return "testAll Completed"; }; -exports.testSplit = function() { - var args = cli._tokenize('s'); - var command = cli._split(args); - test.verifyEqual(1, args.length); - test.verifyEqual('s', args[0].text); - test.verifyUndefined(command); - - var args = cli._tokenize('set'); - var command = cli._split(args); - test.verifyEqual([], args); - test.verifyEqual('set', command.name); - - var args = cli._tokenize('set a b'); - var command = cli._split(args); - test.verifyEqual('set', command.name); - test.verifyEqual(2, args.length); - test.verifyEqual('a', args[0].text); - test.verifyEqual('b', args[1].text); - - // TODO: add tests for sub commands - return "testSplit Completed"; -}; - exports.testTokenize = function() { - var args = cli._tokenize(''); + var args = tokenize(''); test.verifyEqual(0, args.length); - args = cli._tokenize('s'); + args = tokenize('s'); test.verifyEqual(1, args.length); test.verifyEqual('s', args[0].text); test.verifyEqual(0, args[0].start); test.verifyEqual(1, args[0].end); test.verifyEqual('', args[0].priorSpace); - args = cli._tokenize('s s'); + args = tokenize('s s'); test.verifyEqual(2, args.length); test.verifyEqual('s', args[0].text); test.verifyEqual(0, args[0].start); @@ -95,7 +75,7 @@ exports.testTokenize = function() { test.verifyEqual(3, args[1].end); test.verifyEqual(' ', args[1].priorSpace); - args = cli._tokenize(' 1234 \'12 34\''); + args = tokenize(' 1234 \'12 34\''); test.verifyEqual(2, args.length); test.verifyEqual('1234', args[0].text); test.verifyEqual(1, args[0].start); @@ -106,7 +86,7 @@ exports.testTokenize = function() { test.verifyEqual(13, args[1].end); test.verifyEqual(' ', args[1].priorSpace); - args = cli._tokenize('12\'34 "12 34" \\'); // 12'34 "12 34" \ + args = tokenize('12\'34 "12 34" \\'); // 12'34 "12 34" \ test.verifyEqual(3, args.length); test.verifyEqual('12\'34', args[0].text); test.verifyEqual(0, args[0].start); @@ -121,7 +101,7 @@ exports.testTokenize = function() { test.verifyEqual(15, args[2].end); test.verifyEqual(' ', args[2].priorSpace); - args = cli._tokenize('a\\ b \\t\\n\\r \\\'x\\\" \'d'); // a_b \t\n\r \'x\" 'd + args = tokenize('a\\ b \\t\\n\\r \\\'x\\\" \'d'); // a_b \t\n\r \'x\" 'd test.verifyEqual(4, args.length); test.verifyEqual('a b', args[0].text); test.verifyEqual(0, args[0].start); @@ -143,50 +123,60 @@ exports.testTokenize = function() { return "testTokenize Completed"; }; -var hints; -var hint0; -var requisition; -var sel = { start: -1, end: -1 }; -var settingAssignment; -var valueAssignment; -mockCliUi = { - getSelection: function() { - return sel; - }, +exports.testSplit = function() { + var args = tokenize('s'); + var command = split(args); + test.verifyEqual(1, args.length); + test.verifyEqual('s', args[0].text); + test.verifyUndefined(command); - setHints: function(h) { - hints = h; - hint0 = (h.length !== 0) ? h[0] : undefined; + var args = tokenize('set'); + var command = split(args); + test.verifyEqual([], args); + test.verifyEqual('set', command.name); - if (requisition && requisition.command && requisition.command.name === 'set') { - settingAssignment = requisition.getAssignment('setting'); - valueAssignment = requisition.getAssignment('value'); + var args = tokenize('set a b'); + var command = split(args); + test.verifyEqual('set', command.name); + test.verifyEqual(2, args.length); + test.verifyEqual('a', args[0].text); + test.verifyEqual('b', args[1].text); + + // TODO: add tests for sub commands + return "testSplit Completed"; +}; + +exports.testCli = function() { + var hints; + var hint0; + var settingAssignment; + var valueAssignment; + var cli = new CliRequisition(); + + function update(input) { + cli.update(input); + hints = cli.getHints(); + hint0 = (hints.length !== 0) ? hints[0] : undefined; + if (cli.command && cli.command.name === 'set') { + settingAssignment = cli.getAssignment('setting'); + valueAssignment = cli.getAssignment('value'); } else { settingAssignment = undefined; valueAssignment = undefined; } - }, - - setRequisition: function(r) { - requisition = r; } -}; -exports.testCli = function() { var historyLengthSetting = settings.getSetting('historyLength'); - var input = new cli.Cli(mockCliUi); - - input.parse(''); + update({ typed: '', cursor: { start: 0, end: 0 } }); test.verifyEqual(1, hints.length); test.verifyEqual(Status.INCOMPLETE, hint0.status); test.verifyEqual(0, hint0.start); test.verifyEqual(0, hint0.end); - test.verifyNull(requisition.command); + test.verifyNull(cli.command); - sel.start = sel.end = 1; - input.parse('s'); + update({ typed: 's', cursor: { start: 1, end: 1 } }); test.verifyEqual(1, hints.length); test.verifyEqual(Status.INCOMPLETE, hint0.status); test.verifyNotEqual(-1, hint0.message.indexOf('possibilities')); @@ -196,16 +186,16 @@ exports.testCli = function() { // This is slightly fragile because it depends on the configuration test.verifyTrue(hint0.predictions.length < 20); test.verifyNotEqual(-1, hint0.predictions.indexOf('set')); - test.verifyNull(requisition.command); + test.verifyNull(cli.command); - input.parse('set'); + update({ typed: 'set', cursor: { start: 3, end: 3 } }); test.verifyEqual(1, hints.length); test.verifyEqual(Status.VALID, hint0.status); test.verifyEqual(0, hint0.start); test.verifyEqual(3, hint0.end); - test.verifyEqual('set', requisition.command.name); + test.verifyEqual('set', cli.command.name); - input.parse('set '); + update({ typed: 'set ', cursor: { start: 4, end: 4 } }); test.verifyEqual(1, hints.length); test.verifyEqual(Status.VALID, hint0.status); test.verifyEqual(0, hint0.start); @@ -213,71 +203,67 @@ exports.testCli = function() { // This is caused by us using the whole input to determine the length. // Maybe one day we should fix this? //test.verifyEqual(3, hint0.end); - test.verifyEqual('set', requisition.command.name); + test.verifyEqual('set', cli.command.name); - sel.start = sel.end = 5; - input.parse('set h'); + update({ typed: 'set h', cursor: { start: 5, end: 5 } }); test.verifyEqual(1, hints.length); test.verifyEqual(Status.INCOMPLETE, hint0.status); test.verifyTrue(hint0.predictions.length > 0); test.verifyEqual(4, hint0.start); test.verifyEqual(5, hint0.end); test.verifyNotEqual(-1, hint0.predictions.indexOf('historyLength')); - test.verifyEqual('set', requisition.command.name); + test.verifyEqual('set', cli.command.name); test.verifyEqual('h', settingAssignment.arg.text); test.verifyEqual(undefined, settingAssignment.value); - sel.start = sel.end = 16; - input.parse('set historyLengt'); + update({ typed: 'set historyLengt', cursor: { start: 16, end: 16 } }); test.verifyEqual(1, hints.length); test.verifyEqual(Status.INCOMPLETE, hint0.status); test.verifyEqual(1, hint0.predictions.length); test.verifyEqual(4, hint0.start); test.verifyEqual(16, hint0.end); test.verifyEqual('historyLength', hint0.predictions[0]); - test.verifyEqual('set', requisition.command.name); + test.verifyEqual('set', cli.command.name); test.verifyEqual('historyLengt', settingAssignment.arg.text); test.verifyEqual(undefined, settingAssignment.value); - sel.start = sel.end = 1; - input.parse('set historyLengt'); + update({ typed: 'set historyLengt', cursor: { start: 1, end: 1 } }); test.verifyEqual(1, hints.length); test.verifyEqual(Status.INVALID, hint0.status); test.verifyEqual(4, hint0.start); test.verifyEqual(16, hint0.end); test.verifyEqual(1, hint0.predictions.length); test.verifyEqual('historyLength', hint0.predictions[0]); - test.verifyEqual('set', requisition.command.name); + test.verifyEqual('set', cli.command.name); test.verifyEqual('historyLengt', settingAssignment.arg.text); test.verifyEqual(undefined, settingAssignment.value); - sel.start = sel.end = 17; - input.parse('set historyLengt '); + update({ typed: 'set historyLengt ', cursor: { start: 17, end: 17 } }); test.verifyEqual(1, hints.length); test.verifyEqual(Status.INVALID, hint0.status); test.verifyEqual(4, hint0.start); test.verifyEqual(16, hint0.end); test.verifyEqual(1, hint0.predictions.length); test.verifyEqual('historyLength', hint0.predictions[0]); - test.verifyEqual('set', requisition.command.name); + test.verifyEqual('set', cli.command.name); test.verifyEqual('historyLengt', settingAssignment.arg.text); test.verifyEqual(undefined, settingAssignment.value); - input.parse('set historyLength'); + update({ typed: 'set historyLength', cursor: { start: 17, end: 17 } }); test.verifyEqual(0, hints.length); - test.verifyEqual('set', requisition.command.name); + test.verifyEqual('set', cli.command.name); test.verifyEqual('historyLength', settingAssignment.arg.text); test.verifyEqual(historyLengthSetting, settingAssignment.value); - input.parse('set historyLength '); + update({ typed: 'set historyLength ', cursor: { start: 18, end: 18 } }); test.verifyEqual(0, hints.length); - test.verifyEqual('set', requisition.command.name); + test.verifyEqual('set', cli.command.name); test.verifyEqual('historyLength', settingAssignment.arg.text); test.verifyEqual(historyLengthSetting, settingAssignment.value); - input.parse('set historyLength 6'); + update({ typed: 'set historyLength 6', cursor: { start: 19, end: 19 } }); test.verifyEqual(0, hints.length); - test.verifyEqual('set', requisition.command.name); + test.verifyEqual('set', cli.command.name); test.verifyEqual('historyLength', settingAssignment.arg.text); test.verifyEqual(historyLengthSetting, settingAssignment.value); test.verifyEqual('6', valueAssignment.arg.text); @@ -286,12 +272,8 @@ exports.testCli = function() { // TODO: Add test to see that a command without mandatory param causes INVALID - console.log(input); - return "testCli Completed"; }; -window.testCli = exports; - }); diff --git a/plugins/cockpit/lib/test/testNothing.js b/plugins/cockpit/lib/test/testNothing.js new file mode 100644 index 00000000..567b575a --- /dev/null +++ b/plugins/cockpit/lib/test/testNothing.js @@ -0,0 +1,40 @@ +/* ***** 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 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) { + +}); diff --git a/plugins/cockpit/lib/ui/plain.css b/plugins/cockpit/lib/ui/plain.css new file mode 100644 index 00000000..85c55ff1 --- /dev/null +++ b/plugins/cockpit/lib/ui/plain.css @@ -0,0 +1,2 @@ + +#cockpit { color: red; } diff --git a/plugins/cockpit/lib/ui/plain.js b/plugins/cockpit/lib/ui/plain.js new file mode 100644 index 00000000..445a7864 --- /dev/null +++ b/plugins/cockpit/lib/ui/plain.js @@ -0,0 +1,98 @@ +/* ***** 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 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 editorCss = require("text!cockpit/ui/plain.css"); +var dom = require("pilot/dom").dom; +dom.importCssString(editorCss); + +var CliRequisition = require('cockpit/cli').CliRequisition; +var keyutil = require('pilot/keyboard/keyutil'); + +exports.startup = function(data, reason) { + // TODO: we should have a better way to specify command lines??? + this.input = document.getElementById('cockpit'); + if (!this.input) { + console.log('No element with an id of cockpit. Bailing on plain cli'); + return; + } + + var cli = new CliRequisition(); + + /* + // All this does is to kill TABs normal use. I wonder if we can train + // people to use right arrow? Probably not? but ... + keyutil.addKeyDownListener(input, function(ev) { + // env.commandLine = this; + // var handled = keyboardManager.processKeyEvent(ev, this, { + // isCommandLine: true, isKeyUp: false + // }); + if (ev.keyCode === keyutil.KeyHelper.KEY.TAB) { + return true; + } + //return handled; + }.bind(this)); + */ + + this.input.addEventListener('keyup', function(ev) { + /* + var handled = keyboardManager.processKeyEvent(ev, this, { + isCommandLine: true, isKeyUp: true + }); + */ + + if (ev.keyCode === keyutil.KeyHelper.KEY.RETURN) { + cli.exec(); + this.input.value = ''; + } else { + cli.update({ + typed: this.input.value, + cursor: { + start: this.input.selectionStart, + end: this.input.selectionEnd + } + }); + console.log(JSON.stringify(cli.getHints())); + } + + // return handled; + }.bind(this), true); +}; + + +}); diff --git a/plugins/pilot/lib/canon.js b/plugins/pilot/lib/canon.js index 6bf03793..aa308f49 100644 --- a/plugins/pilot/lib/canon.js +++ b/plugins/pilot/lib/canon.js @@ -161,7 +161,7 @@ exports.getCommandNames = function() { * @param command Either a command, or the name of one */ exports.exec = function(command, args) { - if (typeof name === 'string') { + if (typeof command === 'string') { command = commands[command]; } if (!command) { diff --git a/plugins/pilot/lib/index.js b/plugins/pilot/lib/index.js index 553e36d5..73262d42 100644 --- a/plugins/pilot/lib/index.js +++ b/plugins/pilot/lib/index.js @@ -40,11 +40,9 @@ var deps = [ "pilot/types/basic", "pilot/types/command", "pilot/types/settings", - "pilot/canon", "pilot/commands/settings", "pilot/settings/canon", - "pilot/cli", - "pilot/test/testCli" + "pilot/canon" ]; var packages = deps.slice(); @@ -52,6 +50,7 @@ packages.unshift("require", "exports", "module"); define(packages, function(require, exports, module) { +console.log(packages); exports.startup = function(data, reason) { deps.forEach(function(dep) { console.log("test startup for " + dep); diff --git a/plugins/pilot/lib/keyboard/index.js b/plugins/pilot/lib/keyboard/index.js new file mode 100644 index 00000000..87e6281f --- /dev/null +++ b/plugins/pilot/lib/keyboard/index.js @@ -0,0 +1,459 @@ +/* ***** 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 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): + * Skywriter Team (skywriter@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(request, exports, module) { + +var console = require('pilot/console'); +var Trace = require('pilot/stacktrace').Trace; +var keyutil = require('pilot/keyboard/keyutil'); +var history = require('canon/history'); +var Request = require('canon/request').Request; +var env = require('environment').env; + +exports.keymappings = {}; + +exports.addKeymapping = function(mapping) { + exports.keymappings[mapping.name] = mapping; +}; + +exports.removeKeymapping = function(name) { + delete exports.keymapping[name]; +}; + +exports.startup = function(data, reason) { + var settings = data.env.settings; + // TODO register this + // catalog.addExtensionSpec("keymapping", { + // "description": "A keymapping defines how keystrokes are interpreted.", + // "params": [ + // { + // "name": "states", + // "required": true, + // "description": + // "Holds the states and all the informations about the keymapping. See docs: pluginguide/keymapping" + // } + // ] + // }); + settings.settingChange.add({ + match: "customKeymapping", + ref: exports.keyboardManager, + func: exports.keyboardManager._customKeymappingChanged + .bind(exports.keyboardManager) + }); +}; + +exports.shutdown = function(data, reason) { + var settings = data.env.settings; + settings.settingChange.remove(exports.keyboardManager); +}; + + +/* + * Things to do to sanitize this code: + * - 'no command' is a bizarre special value at the very least it should be a + * constant to make typos more obvious, but it would be better to refactor + * so that a natural value like null worked. + * - sender seems to be totally customized to the editor case, and the functions + * that we assume that it has make no sense for the commandLine case. We + * should either document and implement the same function set for both cases + * or admit that the cases are different enough to have separate + * implementations. + * - remove remaining sproutcore-isms + * - fold buildFlags into processKeyEvent or something better, preferably the + * latter. We don't want the environment to become a singleton + */ + +/** + * Every time we call processKeyEvent, we pass in some flags that require the + * same processing to set them up. This function can be called to do that + * setup. + * @param env Probably environment.env + * @param flags Probably {} (but check other places where this is called) + */ +exports.buildFlags = function(flags) { + flags.context = env.contexts[0]; + return flags; +}; + +/** + * The canon, or the repository of commands, contains functions to process + * events and dispatch command messages to targets. + * @class + */ +var KeyboardManager = function() { }; + +KeyboardManager.prototype = { + _customKeymappingCache: { states: {} }, + + /** + * Searches through the command canon for an event matching the given flags + * with a key equivalent matching the given SproutCore event, and, if the + * command is found, sends a message to the appropriate target. + * + * This will get a couple of upgrades in the not-too-distant future: + * 1. caching in the Canon for fast lookup based on key + * 2. there will be an extra layer in between to allow remapping via + * user preferences and keyboard mapping plugins + * + * @return True if a matching command was found, false otherwise. + */ + processKeyEvent: function(evt, sender, flags) { + // Use our modified commandCodes function to detect the meta key in + // more circumstances than SproutCore alone does. + var symbolicName = keyutil.commandCodes(evt, true)[0]; + if (util.none(symbolicName)) { + return false; + } + + // TODO: Maybe it should be the job of our caller to do this? + exports.buildFlags(flags); + + flags.isCommandKey = true; + return this._matchCommand(symbolicName, sender, flags); + }, + + _matchCommand: function(symbolicName, sender, flags) { + var match = this._findCommandExtension(symbolicName, sender, flags); + if (match && match.commandExt !== 'no command') { + if (flags.isTextView) { + sender.resetKeyBuffers(); + } + + var commandExt = match.commandExt; + commandExt.load(function(command) { + var request = new Request({ + command: command, + commandExt: commandExt + }); + history.execute(match.args, request); + }); + return true; + } + + // 'no command' is returned if a keyevent is handled but there is no + // command executed (for example when switchting the keyboard state). + if (match && match.commandExt === 'no command') { + return true; + } else { + return false; + } + }, + + _buildBindingsRegex: function(bindings) { + // Escape a given Regex string. + bindings.forEach(function(binding) { + if (!util.none(binding.key)) { + binding.key = new RegExp('^' + binding.key + '$'); + } else if (Array.isArray(binding.regex)) { + binding.key = new RegExp('^' + binding.regex[1] + '$'); + binding.regex = new RegExp(binding.regex.join('') + '$'); + } else { + binding.regex = new RegExp(binding.regex + '$'); + } + }); + }, + + /** + * Build the RegExp from the keymapping as RegExp can't stored directly + * in the metadata JSON and as the RegExp used to match the keys/buffer + * need to be adapted. + */ + _buildKeymappingRegex: function(keymapping) { + for (state in keymapping.states) { + this._buildBindingsRegex(keymapping.states[state]); + } + keymapping._convertedRegExp = true; + }, + + /** + * Loop through the commands in the canon, looking for something that + * matches according to #_commandMatches, and return that. + */ + _findCommandExtension: function(symbolicName, sender, flags) { + // If the flags indicate that we handle the textView's input then take + // a look at keymappings as well. + if (flags.isTextView) { + var currentState = sender._keyState; + + // Don't add the symbolic name to the key buffer if the alt_ key is + // part of the symbolic name. If it starts with alt_, this means + // that the user hit an alt keycombo and there will be a single, + // new character detected after this event, which then will be + // added to the buffer (e.g. alt_j will result in ∆). + if (!flags.isCommandKey || symbolicName.indexOf('alt_') === -1) { + sender._keyBuffer += + symbolicName.replace(/ctrl_meta|meta/,'ctrl'); + sender._keyMetaBuffer += symbolicName; + } + + // List of all the keymappings to look at. + var ak = [ this._customKeymappingCache ]; + + // Get keymapping extension points. + ak = ak.concat(catalog.getExtensions('keymapping')); + + for (var i = 0; i < ak.length; i++) { + // Check if the keymapping has the current state. + if (util.none(ak[i].states[currentState])) { + continue; + } + + if (util.none(ak[i]._convertedRegExp)) { + this._buildKeymappingRegex(ak[i]); + } + + // Try to match the current mapping. + var result = this._bindingsMatch( + symbolicName, + flags, + sender, + ak[i]); + + if (!util.none(result)) { + return result; + } + } + } + + var commandExts = catalog.getExtensions('command'); + var reply = null; + var args = {}; + + symbolicName = symbolicName.replace(/ctrl_meta|meta/,'ctrl'); + + commandExts.some(function(commandExt) { + if (this._commandMatches(commandExt, symbolicName, flags)) { + reply = commandExt; + return true; + } + return false; + }.bind(this)); + + return util.none(reply) ? null : { commandExt: reply, args: args }; + }, + + + /** + * Checks if the given parameters fit to one binding in the given bindings. + * Returns the command and arguments if a command was matched. + */ + _bindingsMatch: function(symbolicName, flags, sender, keymapping) { + var match; + var commandExt = null; + var args = {}; + var bufferToUse; + + if (!util.none(keymapping.hasMetaKey)) { + bufferToUse = sender._keyBuffer; + } else { + bufferToUse = sender._keyMetaBuffer; + } + + // Add the alt_key to the buffer as we don't want it to be in the buffer + // that is saved but for matching, it needs to be there. + if (symbolicName.indexOf('alt_') === 0 && flags.isCommandKey) { + bufferToUse += symbolicName; + } + + // Loop over all the bindings of the keymapp until a match is found. + keymapping.states[sender._keyState].some(function(binding) { + // Check if the key matches. + if (binding.key && !binding.key.test(symbolicName)) { + return false; + } + + // Check if the regex matches. + if (binding.regex && !(match = binding.regex.exec(bufferToUse))) { + return false; + } + + // Check for disallowed matches. + if (binding.disallowMatches) { + for (var i = 0; i < binding.disallowMatches.length; i++) { + if (!!match[binding.disallowMatches[i]]) { + return true; + } + } + } + + // Check predicates. + if (!exports.flagsMatch(binding.predicates, flags)) { + return false; + } + + // If there is a command to execute, then figure out the + // comand and the arguments. + if (binding.exec) { + // Get the command. + commandExt = catalog.getExtensionByKey('command', binding.exec); + if (util.none(commandExt)) { + throw new Error('Can\'t find command ' + binding.exec + + ' in state=' + sender._keyState + + ', symbolicName=' + symbolicName); + } + + // Bulid the arguments. + if (binding.params) { + var value; + binding.params.forEach(function(param) { + if (!util.none(param.match) && !util.none(match)) { + value = match[param.match] || param.defaultValue; + } else { + value = param.defaultValue; + } + + if (param.type === 'number') { + value = parseInt(value, 10); + } + + args[param.name] = value; + }); + } + sender.resetKeyBuffers(); + } + + // Handle the 'then' property. + if (binding.then) { + sender._keyState = binding.then; + sender.resetKeyBuffers(); + } + + // If there is no command matched now, then return a 'false' + // command to stop matching. + if (util.none(commandExt)) { + commandExt = 'no command'; + } + + return true; + }); + + if (util.none(commandExt)) { + return null; + } + + return { commandExt: commandExt, args: args }; + }, + + /** + * Check that the given command fits the given key name and flags. + */ + _commandMatches: function(commandExt, symbolicName, flags) { + var mappedKeys = commandExt.key; + if (!mappedKeys) { + return false; + } + + // Check predicates + if (!exports.flagsMatch(commandExt.predicates, flags)) { + return false; + } + + if (typeof(mappedKeys) === 'string') { + if (mappedKeys != symbolicName) { + return false; + } + return true; + } + + if (!Array.isArray(mappedKeys)) { + mappedKeys = [mappedKeys]; + commandExt.key = mappedKeys; + } + + for (var i = 0; i < mappedKeys.length; i++) { + var keymap = mappedKeys[i]; + if (typeof(keymap) === 'string') { + if (keymap == symbolicName) { + return true; + } + continue; + } + + if (keymap.key != symbolicName) { + continue; + } + + return exports.flagsMatch(keymap.predicates, flags); + } + return false; + }, + + /** + * Build a cache of custom keymappings whenever the associated setting + * changes. + */ + _customKeymappingChanged: function(settingName, value) { + var ckc = this._customKeymappingCache = + JSON.parse(value); + + ckc.states = ckc.states || {}; + + for (state in ckc.states) { + this._buildBindingsRegex(ckc.states[state]); + } + ckc._convertedRegExp = true; + } +}; + +/** + * + */ +exports.flagsMatch = function(predicates, flags) { + if (util.none(predicates)) { + return true; + } + + if (!flags) { + return false; + } + + for (var flagName in predicates) { + if (flags[flagName] !== predicates[flagName]) { + return false; + } + } + + return true; +}; + +/** + * The global exported KeyboardManager + */ +exports.keyboardManager = new KeyboardManager(); + + +}); diff --git a/plugins/pilot/lib/keyboard/keyutil.js b/plugins/pilot/lib/keyboard/keyutil.js new file mode 100644 index 00000000..bbc0ac5a --- /dev/null +++ b/plugins/pilot/lib/keyboard/keyutil.js @@ -0,0 +1,273 @@ +/*! @license +========================================================================== +SproutCore -- JavaScript Application Framework +copyright 2006-2009, Sprout Systems Inc., Apple Inc. and contributors. + +Permission is hereby granted, free of charge, to any person obtaining a +copy of this software and associated documentation files (the "Software"), +to deal in the Software without restriction, including without limitation +the rights to use, copy, modify, merge, publish, distribute, sublicense, +and/or sell copies of the Software, and to permit persons to whom the +Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. + +SproutCore and the SproutCore logo are trademarks of Sprout Systems, Inc. + +For more information about SproutCore, visit http://www.sproutcore.com + + +========================================================================== +@license */ + +// Most of the following code is taken from SproutCore with a few changes. + +define(function(require, exports, module) { + +var util = require('pilot/util'); + + +/** + * Helper functions and hashes for key handling. + */ +exports.KeyHelper = function() { + var ret = { + MODIFIER_KEYS: { + 16: 'shift', 17: 'ctrl', 18: 'alt', 224: 'meta' + }, + + FUNCTION_KEYS : { + 8: 'backspace', 9: 'tab', 13: 'return', 19: 'pause', + 27: 'escape', 33: 'pageup', 34: 'pagedown', 35: 'end', + 36: 'home', 37: 'left', 38: 'up', 39: 'right', + 40: 'down', 44: 'printscreen', 45: 'insert', 46: 'delete', + 112: 'f1', 113: 'f2', 114: 'f3', 115: 'f4', + 116: 'f5', 117: 'f7', 119: 'f8', 120: 'f9', + 121: 'f10', 122: 'f11', 123: 'f12', 144: 'numlock', + 145: 'scrolllock' + }, + + PRINTABLE_KEYS: { + 32: ' ', 48: '0', 49: '1', 50: '2', 51: '3', 52: '4', 53: '5', + 54: '6', 55: '7', 56: '8', 57: '9', 59: ';', 61: '=', 65: 'a', + 66: 'b', 67: 'c', 68: 'd', 69: 'e', 70: 'f', 71: 'g', 72: 'h', + 73: 'i', 74: 'j', 75: 'k', 76: 'l', 77: 'm', 78: 'n', 79: 'o', + 80: 'p', 81: 'q', 82: 'r', 83: 's', 84: 't', 85: 'u', 86: 'v', + 87: 'w', 88: 'x', 89: 'y', 90: 'z', 107: '+', 109: '-', 110: '.', + 188: ',', 190: '.', 191: '/', 192: '`', 219: '[', 220: '\\', + 221: ']', 222: '\"' + }, + + /** + * Create the lookup table for Firefox to convert charCodes to keyCodes + * in the keyPress event. + */ + PRINTABLE_KEYS_CHARCODE: {}, + + /** + * Allow us to lookup keyCodes by symbolic name rather than number + */ + KEY: {} + }; + + // Create the PRINTABLE_KEYS_CHARCODE hash. + for (var i in ret.PRINTABLE_KEYS) { + var k = ret.PRINTABLE_KEYS[i]; + ret.PRINTABLE_KEYS_CHARCODE[k.charCodeAt(0)] = i; + if (k.toUpperCase() != k) { + ret.PRINTABLE_KEYS_CHARCODE[k.toUpperCase().charCodeAt(0)] = i; + } + } + + // A reverse map of FUNCTION_KEYS + for (i in ret.FUNCTION_KEYS) { + var name = ret.FUNCTION_KEYS[i].toUpperCase(); + ret.KEY[name] = parseInt(i, 10); + } + + return ret; +}(); + +/** + * Determines if the keyDown event is a non-printable or function key. + * These kinds of events are processed as keyboard shortcuts. + * If no shortcut handles the event, then it will be sent as a regular + * keyDown event. + * @private + */ +var isFunctionOrNonPrintableKey = function(evt) { + return !!(evt.altKey || evt.ctrlKey || evt.metaKey || + ((evt.charCode !== evt.which) && + exports.KeyHelper.FUNCTION_KEYS[evt.which])); +}; + +/** + * Returns character codes for the event. + * The first value is the normalized code string, with any Shift or Ctrl + * characters added to the beginning. + * The second value is the char string by itself. + * @return {Array} + */ +exports.commandCodes = function(evt, dontIgnoreMeta) { + var code = evt._keyCode || evt.keyCode; + var charCode = (evt._charCode === undefined ? evt.charCode : evt._charCode); + var ret = null; + var key = null; + var modifiers = ''; + var lowercase; + var allowShift = true; + + // Absent a value for 'keyCode' or 'which', we can't compute the + // command codes. Bail out. + if (code === 0 && evt.which === 0) { + return false; + } + + // If the charCode is not zero, then we do not handle a command key + // here. Bail out. + if (charCode !== 0) { + return false; + } + + // Check for modifier keys. + if (exports.KeyHelper.MODIFIER_KEYS[charCode]) { + return [exports.KeyHelper.MODIFIER_KEYS[charCode], null]; + } + + // handle function keys. + if (code) { + ret = exports.KeyHelper.FUNCTION_KEYS[code]; + if (!ret && (evt.altKey || evt.ctrlKey || evt.metaKey)) { + ret = exports.KeyHelper.PRINTABLE_KEYS[code]; + // Don't handle the shift key if the combo is + // (meta_|ctrl_) + // This is necessary for the French keyboard. On that keyboard, + // you have to hold down the shift key to access the number + // characters. + if (code > 47 && code < 58) { + allowShift = evt.altKey; + } + } + + if (ret) { + if (evt.altKey) { + modifiers += 'alt_'; + } + if (evt.ctrlKey) { + modifiers += 'ctrl_'; + } + if (evt.metaKey) { + modifiers += 'meta_'; + } + } else if (evt.ctrlKey || evt.metaKey) { + return false; + } + } + + // otherwise just go get the right key. + if (!ret) { + code = evt.which; + key = ret = String.fromCharCode(code); + lowercase = ret.toLowerCase(); + + if (evt.metaKey) { + modifiers = 'meta_'; + ret = lowercase; + + } else ret = null; + } + + if (evt.shiftKey && ret && allowShift) { + modifiers += 'shift_'; + } + + if (ret) { + ret = modifiers + ret; + } + + if (!dontIgnoreMeta && ret) { + ret = ret.replace(/ctrl_meta|meta/,'ctrl'); + } + + return [ret, key]; +}; + +// Note: Most of the following code is taken from SproutCore with a few changes. + +/** + * Firefox sends a few key events twice: the first time to the keydown event + * and then later again to the keypress event. To handle them correct, they + * should be processed only once. Due to this, we will skip these events + * in keydown and handle them then in keypress. + */ +exports.addKeyDownListener = function(element, boundFunction) { + + var handleBoundFunction = function(ev) { + var handled = boundFunction(ev); + // If the boundFunction returned true, then stop the event. + if (handled) { + util.stopEvent(ev); + } + return handled; + }; + + element.addEventListener('keydown', function(ev) { + if (util.isMozilla) { + // Check for function keys (like DELETE, TAB, LEFT, RIGHT...) + if (exports.KeyHelper.FUNCTION_KEYS[ev.keyCode]) { + return true; + // Check for command keys (like ctrl_c, ctrl_z...) + } else if ((ev.ctrlKey || ev.metaKey) && + exports.KeyHelper.PRINTABLE_KEYS[ev.keyCode]) { + return true; + } + } + + if (isFunctionOrNonPrintableKey(ev)) { + return handleBoundFunction(ev); + } + + return true; + }, false); + + element.addEventListener('keypress', function(ev) { + if (util.isMozilla) { + // If this is a function key, we have to use the keyCode. + if (exports.KeyHelper.FUNCTION_KEYS[ev.keyCode]) { + return handleBoundFunction(ev); + } else if ((ev.ctrlKey || ev.metaKey) && + exports.KeyHelper.PRINTABLE_KEYS_CHARCODE[ev.charCode]){ + // Check for command keys (like ctrl_c, ctrl_z...). + // For command keys have to convert the charCode to a keyCode + // as it has been sent from the keydown event to be in line + // with the other browsers implementations. + + // FF does not allow let you change the keyCode or charCode + // property. Store to a custom keyCode/charCode variable. + // The getCommandCodes() function takes care of these + // special variables. + ev._keyCode = exports.KeyHelper.PRINTABLE_KEYS_CHARCODE[ev.charCode]; + ev._charCode = 0; + return handleBoundFunction(ev); + } + } + + // normal processing: send keyDown for printable keys. + if (ev.charCode !== undefined && ev.charCode === 0) { + return true; + } + + return handleBoundFunction(ev); + }, false); +}; + +}); diff --git a/plugins/pilot/lib/keyboard/tests/testKeyboard.js b/plugins/pilot/lib/keyboard/tests/testKeyboard.js new file mode 100644 index 00000000..17918935 --- /dev/null +++ b/plugins/pilot/lib/keyboard/tests/testKeyboard.js @@ -0,0 +1,99 @@ +require.def(['require', 'exports', 'module', + 'keyboard/keyboard', + 'keyboard/tests/plugindev' +], function(require, exports, module, + keyboard, + t +) { + +/* ***** 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 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): + * Skywriter Team (skywriter@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 ***** */ + + + + +exports.testKeyMatching = function() { + var km = keyboard.keyboardManager; + var command = {}; + t.equal(km._commandMatches(command, 'meta_z', {}), false, + 'no keymapping means false'); + + command = { + key: 'meta_z' + }; + t.equal(km._commandMatches(command, 'meta_z', {}), true, + 'matching keys, simple string'); + t.equal(km._commandMatches(command, 'meta_a', {}), false, + 'not matching key, simple string'); + + command = { + key: {key: 'meta_z', predicates: {isGreen: true}} + }; + t.equal(km._commandMatches(command, 'meta_z', {}), false, + 'object with not matching predicate'); + t.equal(km._commandMatches(command, 'meta_z', {isGreen: true}), true, + 'object with matching key and predicate'); + t.equal(km._commandMatches(command, 'meta_a', {isGreen: true}), false, + 'object with not matching key'); + t.equal(km._commandMatches(command, 'meta_a', {isGreen: false}), false, + 'object with neither matching'); + t.equal(km._commandMatches(command, 'meta_z', {isGreen: false}), false, + 'object with matching key and but different predicate'); + + command = { + key: ['meta_b', {key: 'meta_z', predicates: {isGreen: true}}, + {key: 'meta_c'}] + }; + t.equal(km._commandMatches(command, 'meta_z', {}), false, + 'list: object with not matching predicate'); + t.equal(km._commandMatches(command, 'meta_z', {isGreen: true}), true, + 'list: object with matching key and predicate'); + t.equal(km._commandMatches(command, 'meta_a', {isGreen: true}), false, + 'list: object with not matching key'); + t.equal(km._commandMatches(command, 'meta_a', {isGreen: false}), false, + 'list: object with neither matching'); + t.equal(km._commandMatches(command, 'meta_z', {isGreen: false}), false, + 'list: object with matching key and but different predicate'); + t.equal(km._commandMatches(command, 'meta_b'), true, + 'list: simple key match'); + t.equal(km._commandMatches(command, 'meta_c'), true, + 'list: object without predicate match'); + t.equal(km._commandMatches(command, 'meta_c', {isGreen: false}), true, + 'list: flags don\'t matter without predicates'); +}; + +}); diff --git a/plugins/pilot/lib/rangeutils.js b/plugins/pilot/lib/rangeutils.js new file mode 100644 index 00000000..b4714f7b --- /dev/null +++ b/plugins/pilot/lib/rangeutils.js @@ -0,0 +1,185 @@ +/* ***** 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): + * Patrick Walton (pwalton@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 util = require("util/util"); + +/** + * Returns the result of adding the two positions. + */ +exports.addPositions = function(a, b) { + return { row: a.row + b.row, col: a.col + b.col }; +}; + +/** Returns a copy of the given range. */ +exports.cloneRange = function(range) { + var oldStart = range.start, oldEnd = range.end; + var newStart = { row: oldStart.row, col: oldStart.col }; + var newEnd = { row: oldEnd.row, col: oldEnd.col }; + return { start: newStart, end: newEnd }; +}; + +/** + * Given two positions a and b, returns a negative number if a < b, 0 if a = b, + * or a positive number if a > b. + */ +exports.comparePositions = function(positionA, positionB) { + var rowDiff = positionA.row - positionB.row; + return rowDiff === 0 ? positionA.col - positionB.col : rowDiff; +}; + +/** + * Returns true if the two ranges are equal and false otherwise. + */ +exports.equal = function(rangeA, rangeB) { + return (exports.comparePositions(rangeA.start, rangeB.start) === 0 && + exports.comparePositions(rangeA.end, rangeB.end) === 0); +}; + +exports.extendRange = function(range, delta) { + var end = range.end; + return { + start: range.start, + end: { + row: end.row + delta.row, + col: end.col + delta.col + } + }; +}; + +/** + * Given two sets of ranges, returns the ranges of characters that exist in one + * of the sets but not both. + */ +exports.intersectRangeSets = function(setA, setB) { + var stackA = util.clone(setA), stackB = util.clone(setB); + var result = []; + while (stackA.length > 0 && stackB.length > 0) { + var rangeA = stackA.shift(), rangeB = stackB.shift(); + var startDiff = exports.comparePositions(rangeA.start, rangeB.start); + var endDiff = exports.comparePositions(rangeA.end, rangeB.end); + + if (exports.comparePositions(rangeA.end, rangeB.start) < 0) { + // A is completely before B + result.push(rangeA); + stackB.unshift(rangeB); + } else if (exports.comparePositions(rangeB.end, rangeA.start) < 0) { + // B is completely before A + result.push(rangeB); + stackA.unshift(rangeA); + } else if (startDiff < 0) { // A starts before B + result.push({ start: rangeA.start, end: rangeB.start }); + stackA.unshift({ start: rangeB.start, end: rangeA.end }); + stackB.unshift(rangeB); + } else if (startDiff === 0) { // A and B start at the same place + if (endDiff < 0) { // A ends before B + stackB.unshift({ start: rangeA.end, end: rangeB.end }); + } else if (endDiff > 0) { // A ends after B + stackA.unshift({ start: rangeB.end, end: rangeA.end }); + } + } else if (startDiff > 0) { // A starts after B + result.push({ start: rangeB.start, end: rangeA.start }); + stackA.unshift(rangeA); + stackB.unshift({ start: rangeA.start, end: rangeB.end }); + } + } + return result.concat(stackA, stackB); +}; + +exports.isZeroLength = function(range) { + return range.start.row === range.end.row && + range.start.col === range.end.col; +}; + +/** + * Returns the greater of the two positions. + */ +exports.maxPosition = function(a, b) { + return exports.comparePositions(a, b) > 0 ? a : b; +}; + +/** + * Converts a range with swapped 'end' and 'start' values into one with the + * values in the correct order. + * + * TODO: Unit test. + */ +exports.normalizeRange = function(range) { + return this.comparePositions(range.start, range.end) < 0 ? range : + { start: range.end, end: range.start }; +}; + +/** + * Returns a single range that spans the entire given set of ranges. + */ +exports.rangeSetBoundaries = function(rangeSet) { + return { + start: rangeSet[0].start, + end: rangeSet[rangeSet.length - 1].end + }; +}; + +exports.toString = function(range) { + var start = range.start, end = range.end; + return '[ ' + start.row + ', ' + start.col + ' ' + end.row + ',' + + end.col +' ]'; +}; + +/** + * Returns the union of the two ranges. + */ +exports.unionRanges = function(a, b) { + return { + start: a.start.row < b.start.row || + (a.start.row === b.start.row && a.start.col < b.start.col) ? + a.start : b.start, + end: a.end.row > b.end.row || + (a.end.row === b.end.row && a.end.col > b.end.col) ? + a.end : b.end + }; +}; + +exports.isPosition = function(pos) { + return !util.none(pos) && !util.none(pos.row) && !util.none(pos.col); +}; + +exports.isRange = function(range) { + return (!util.none(range) && exports.isPosition(range.start) && + exports.isPosition(range.end)); +}; + +}); \ No newline at end of file diff --git a/plugins/pilot/lib/tests/testRangeutils.js b/plugins/pilot/lib/tests/testRangeutils.js new file mode 100644 index 00000000..d96c43a4 --- /dev/null +++ b/plugins/pilot/lib/tests/testRangeutils.js @@ -0,0 +1,163 @@ +require.def(['require', 'exports', 'module', + 'rangeutils/tests/plugindev', + 'rangeutils/tests/utils/range' +], function(require, exports, module, + t, + Range +) { + +/* ***** 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 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): + * Skywriter Team (skywriter@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 ***** */ + + + + +exports.testAddPositions = function() { + t.deepEqual(Range.addPositions({ row: 0, col: 0 }, + { row: 0, col: 0 }), { row: 0, col: 0 }, '0,0 + 0,0 and 0,0'); + t.deepEqual(Range.addPositions({ row: 1, col: 0 }, + { row: 2, col: 0 }), { row: 3, col: 0 }, '1,0 + 2,0 and 3,0'); + t.deepEqual(Range.addPositions({ row: 0, col: 1 }, + { row: 0, col: 1 }), { row: 0, col: 2 }, '0,1 + 0,1 and 0,2'); + t.deepEqual(Range.addPositions({ row: 1, col: 2 }, + { row: -1, col: -2 }), { row: 0, col: 0 }, '1,2 + -1,-2 and 0,0'); +}; + +exports.testCloneRange = function() { + var oldRange = { start: { row: 1, col: 2 }, end: { row: 3, col: 4 } }; + var newRange = Range.cloneRange(oldRange); + t.deepEqual(oldRange, newRange, "the old range and the new range"); + t.ok(oldRange.start !== newRange.start, "the old range's start position " + + "is distinct from the new range's start position"); + t.ok(oldRange.end !== newRange.end, "the old range's end position is " + + "distinct from the new range's end position"); + t.ok(oldRange !== newRange, "the old range is distinct from the new " + + "range"); +}; + +exports.testComparePositions = function() { + t.equal(Range.comparePositions({ row: 0, col: 0 }, + { row: 0, col: 0 }), 0, '0,0 = 0,0'); + t.ok(Range.comparePositions({ row: 0, col: 0 }, + { row: 1, col: 0 }) < 0, '0,0 < 1,0'); + t.ok(Range.comparePositions({ row: 0, col: 0 }, + { row: 0, col: 1 }) < 0, '0,0 < 0,1'); + t.ok(Range.comparePositions({ row: 1, col: 0 }, + { row: 0, col: 0 }) > 0, '1,0 > 0,0'); + t.ok(Range.comparePositions({ row: 0, col: 1 }, + { row: 0, col: 0 }) > 0, '0,1 > 0,0'); +}; + +exports.testExtendRange = function() { + t.deepEqual(Range.extendRange({ + start: { row: 1, col: 2 }, + end: { row: 3, col: 4 } + }, { row: 5, col: 6 }), { + start: { row: 1, col: 2 }, + end: { row: 8, col: 10 } + }, '[ 1,2 3,4 ] extended by 5,6 = [ 1,2 8,10 ]'); + t.deepEqual(Range.extendRange({ + start: { row: 7, col: 8 }, + end: { row: 9, col: 10 } + }, { row: 0, col: 0 }), { + start: { row: 7, col: 8 }, + end: { row: 9, col: 10 } + }, '[ 7,8 9,10 ] extended by 0,0 remains the same'); +}; + +exports.testMaxPosition = function() { + t.deepEqual(Range.maxPosition({ row: 0, col: 0 }, + { row: 0, col: 0 }), { row: 0, col: 0 }, 'max(0,0 0,0) = 0,0'); + t.deepEqual(Range.maxPosition({ row: 0, col: 0 }, + { row: 1, col: 0 }), { row: 1, col: 0 }, 'max(0,0 1,0) = 1,0'); + t.deepEqual(Range.maxPosition({ row: 0, col: 0 }, + { row: 0, col: 1 }), { row: 0, col: 1 }, 'max(0,0 0,1) = 0,1'); + t.deepEqual(Range.maxPosition({ row: 1, col: 0 }, + { row: 0, col: 0 }), { row: 1, col: 0 }, 'max(1,0 0,0) = 1,0'); + t.deepEqual(Range.maxPosition({ row: 0, col: 1 }, + { row: 0, col: 0 }), { row: 0, col: 1 }, 'max(0,1 0,0) = 0,1'); +}; + +exports.testNormalizeRange = function() { + t.deepEqual(Range.normalizeRange({ + start: { row: 0, col: 0 }, + end: { row: 0, col: 0 } + }), { + start: { row: 0, col: 0 }, + end: { row: 0, col: 0 } + }, 'normalize(0,0 0,0) and (0,0 0,0)'); + t.deepEqual(Range.normalizeRange({ + start: { row: 1, col: 2 }, + end: { row: 3, col: 4 } + }), { + start: { row: 1, col: 2 }, + end: { row: 3, col: 4 } + }, 'normalize(1,2 3,4) and (1,2 3,4)'); + t.deepEqual(Range.normalizeRange({ + start: { row: 4, col: 3 }, + end: { row: 2, col: 1 } + }), { + start: { row: 2, col: 1 }, + end: { row: 4, col: 3 } + }, 'normalize(4,3 2,1) and (2,1 4,3)'); +}; + +exports.testUnionRanges = function() { + t.deepEqual(Range.unionRanges({ + start: { row: 1, col: 2 }, + end: { row: 3, col: 4 } + }, { + start: { row: 5, col: 6 }, + end: { row: 7, col: 8 } + }), { + start: { row: 1, col: 2 }, + end: { row: 7, col: 8 } + }, '[ 1,2 3,4 ] union [ 5,6 7,8 ] = [ 1,2 7,8 ]'); + t.deepEqual(Range.unionRanges({ + start: { row: 4, col: 4 }, + end: { row: 5, col: 5 } + }, { + start: { row: 3, col: 3 }, + end: { row: 4, col: 5 } + }), { + start: { row: 3, col: 3 }, + end: { row: 5, col: 5 } + }, '[ 4,4 5,5 ] union [ 3,3 4,5 ] = [ 3,3 5,5 ]'); +}; + + +});