a big lump of work to get the command line able to execute commands, and to hack in a trivial cli

This commit is contained in:
Joe Walker 2010-12-08 13:13:33 +00:00
commit 38c9fc7a93
17 changed files with 1715 additions and 379 deletions

View file

@ -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

View file

@ -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();
};

View file

@ -47,6 +47,11 @@
display: none;
}
#cockpit {
position: absolute;
width: 100%;
bottom: 0;
}
</style>
<script>
require = {
@ -63,11 +68,7 @@
}
}
}, function(plugin_manager, settings) {
var data = {
env: {
settings: settings
}
};
var data = { env: { settings: settings } };
plugin_manager.catalog.startupPlugins(data, plugin_manager.REASONS.APP_STARTUP).then(function() {
var demo_startup = require("demo_startup");
demo_startup.launch(data.env);
@ -155,6 +156,8 @@
</body>
</html>
</script>
<input id=cockpit type=text/>
</body>
</html>

View file

@ -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.
* <p>We also record validity information where applicable.
* <p>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.
* <p>The 'output' of the parse is held in 2 objects: input.hints which is an
* <p>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.
* <p>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 = {
* <li>value - The matching input
* </ul>
*/
_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.
* <p>We also record validity information where applicable.
* <p>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;
});

View file

@ -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);
}
});
};
*/
});

View file

@ -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;
});

View file

@ -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) {
});

View file

@ -0,0 +1,2 @@
#cockpit { color: red; }

View file

@ -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);
};
});

View file

@ -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) {

View file

@ -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);

View file

@ -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();
});

View file

@ -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_)<number>
// 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);
};
});

View file

@ -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');
};
});

View file

@ -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));
};
});

View file

@ -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 ]');
};
});