author | Tim Taubert <ttaubert@mozilla.com> |
Sun, 21 Jul 2013 11:12:51 +0200 | |
changeset 151612 | 2268ff80683a5fbdfbda4219b55af2a112c9f71b |
parent 151604 | 9af6265e9884684838c143ca1efbf297db62855c (current diff) |
parent 151611 | 541487a04a9c065060401d98cbbcd4e7e8042943 (diff) |
child 151684 | f80683d8c3e7323cca2c51186d89023d0a1b4d1b |
child 151785 | 05ff12c00114de0b595cab891fb42336753b8462 |
child 170144 | e4177c0221d0730145f081924a23a75731c59b98 |
push id | 2859 |
push user | akeybl@mozilla.com |
push date | Mon, 16 Sep 2013 19:14:59 +0000 |
treeherder | mozilla-beta@87d3c51cd2bf [default view] [failures only] |
perfherder | [talos] [build metrics] [platform microbench] (compared to previous push) |
milestone | 25.0a1 |
first release with | nightly linux32
2268ff80683a
/
25.0a1
/
20130722030226
/
files
nightly linux64
2268ff80683a
/
25.0a1
/
20130722030226
/
files
nightly mac
2268ff80683a
/
25.0a1
/
20130722030226
/
files
nightly win32
2268ff80683a
/
25.0a1
/
20130722030226
/
files
nightly win64
2268ff80683a
/
25.0a1
/
20130722030226
/
files
|
last release without | nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
|
releases | nightly linux32
25.0a1
/
20130722030226
/
pushlog to previous
nightly linux64
25.0a1
/
20130722030226
/
pushlog to previous
nightly mac
25.0a1
/
20130722030226
/
pushlog to previous
nightly win32
25.0a1
/
20130722030226
/
pushlog to previous
nightly win64
25.0a1
/
20130722030226
/
pushlog to previous
|
--- a/browser/devtools/commandline/BuiltinCommands.jsm +++ b/browser/devtools/commandline/BuiltinCommands.jsm @@ -595,16 +595,21 @@ XPCOMUtils.defineLazyModuleGetter(this, (function(module) { let prefSvc = "@mozilla.org/preferences-service;1"; XPCOMUtils.defineLazyGetter(this, "prefBranch", function() { let prefService = Cc[prefSvc].getService(Ci.nsIPrefService); return prefService.getBranch(null).QueryInterface(Ci.nsIPrefBranch2); }); + XPCOMUtils.defineLazyGetter(this, 'supportsString', function() { + return Cc["@mozilla.org/supports-string;1"] + .createInstance(Ci.nsISupportsString); + }); + XPCOMUtils.defineLazyModuleGetter(this, "NetUtil", "resource://gre/modules/NetUtil.jsm"); XPCOMUtils.defineLazyModuleGetter(this, "console", "resource://gre/modules/devtools/Console.jsm"); const PREF_DIR = "devtools.commands.dir"; /** @@ -714,44 +719,84 @@ XPCOMUtils.defineLazyModuleGetter(this, console.error("Command file '" + aFileEntry.name + "' does not have top level array."); return; } data.forEach(function(commandSpec) { gcli.addCommand(commandSpec); commands.push(commandSpec.name); }); - }, function onError(reason) { console.error("OS.File.read(" + aFileEntry.path + ") failed."); throw reason; } ); } /** * 'cmd' command */ gcli.addCommand({ name: "cmd", - get hidden() { return !prefBranch.prefHasUserValue(PREF_DIR); }, + get hidden() { + return !prefBranch.prefHasUserValue(PREF_DIR); + }, description: gcli.lookup("cmdDesc") }); /** * 'cmd refresh' command */ gcli.addCommand({ name: "cmd refresh", description: gcli.lookup("cmdRefreshDesc"), - get hidden() { return !prefBranch.prefHasUserValue(PREF_DIR); }, - exec: function Command_cmdRefresh(args, context) { + get hidden() { + return !prefBranch.prefHasUserValue(PREF_DIR); + }, + exec: function(args, context) { let chromeWindow = context.environment.chromeDocument.defaultView; CmdCommands.refreshAutoCommands(chromeWindow); + + let dirName = prefBranch.getComplexValue(PREF_DIR, + Ci.nsISupportsString).data.trim(); + return gcli.lookupFormat("cmdStatus", [ commands.length, dirName ]); + } + }); + + /** + * 'cmd setdir' command + */ + gcli.addCommand({ + name: "cmd setdir", + description: gcli.lookup("cmdSetdirDesc"), + params: [ + { + name: "directory", + description: gcli.lookup("cmdSetdirDirectoryDesc"), + type: { + name: "file", + filetype: "directory", + existing: "yes" + }, + defaultValue: null + } + ], + returnType: "string", + get hidden() { + return true; // !prefBranch.prefHasUserValue(PREF_DIR); + }, + exec: function(args, context) { + supportsString.data = args.directory; + prefBranch.setComplexValue(PREF_DIR, Ci.nsISupportsString, supportsString); + + let chromeWindow = context.environment.chromeDocument.defaultView; + CmdCommands.refreshAutoCommands(chromeWindow); + + return gcli.lookupFormat("cmdStatus", [ commands.length, args.directory ]); } }); }(this)); /* CmdConsole -------------------------------------------------------------- */ (function(module) { XPCOMUtils.defineLazyModuleGetter(this, "HUDService", @@ -1488,17 +1533,21 @@ XPCOMUtils.defineLazyModuleGetter(this, gcli.addCommand({ name: "tools srcdir", description: gcli.lookup("toolsSrcdirDesc"), manual: gcli.lookupFormat("toolsSrcdirManual2", [BRAND_SHORT_NAME]), get hidden() gcli.hiddenByChromePref(), params: [ { name: "srcdir", - type: "string", + type: "string" /* { + name: "file", + filetype: "directory", + existing: "yes" + } */, description: gcli.lookup("toolsSrcdirDir") } ], returnType: "string", exec: function(args, context) { let clobber = OS.Path.join(args.srcdir, "CLOBBER"); return OS.File.exists(clobber).then(function(exists) { if (exists) {
--- a/browser/devtools/commandline/test/Makefile.in +++ b/browser/devtools/commandline/test/Makefile.in @@ -39,20 +39,24 @@ MOCHITEST_BROWSER_FILES = \ browser_cmd_media.js \ browser_cmd_pagemod_export.html \ browser_cmd_pagemod_export.js \ browser_cmd_pref.js \ browser_cmd_restart.js \ browser_cmd_screenshot.html \ browser_cmd_screenshot.js \ browser_cmd_settings.js \ + browser_gcli_async.js \ browser_gcli_canon.js \ browser_gcli_cli.js \ browser_gcli_completion.js \ + browser_gcli_date.js \ browser_gcli_exec.js \ + browser_gcli_fail.js \ + browser_gcli_file.js \ browser_gcli_focus.js \ browser_gcli_history.js \ browser_gcli_incomplete.js \ browser_gcli_inputter.js \ browser_gcli_intro.js \ browser_gcli_js.js \ browser_gcli_keyboard1.js \ browser_gcli_keyboard2.js \
--- a/browser/devtools/commandline/test/browser_gcli_async.js +++ b/browser/devtools/commandline/test/browser_gcli_async.js @@ -1,12 +1,22 @@ /* - * Copyright 2009-2011 Mozilla Foundation and contributors - * Licensed under the New BSD license. See LICENSE.txt or: - * http://opensource.org/licenses/BSD-3-Clause + * Copyright 2012, Mozilla Foundation and contributors + * + * Licensed under the Apache License, Version 2.0 (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.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. */ // define(function(require, exports, module) { // <INJECTED SOURCE:START> // THIS FILE IS GENERATED FROM SOURCE IN THE GCLI PROJECT // DO NOT EDIT IT DIRECTLY
--- a/browser/devtools/commandline/test/browser_gcli_canon.js +++ b/browser/devtools/commandline/test/browser_gcli_canon.js @@ -212,20 +212,21 @@ exports.testAltCanon = function(options) return context.commandName + ':' + args.str + ':' + args.num + ':' + args.opt; } }; altCanon.addCommand(tss); var commandSpecs = altCanon.getCommandSpecs(); assert.is(JSON.stringify(commandSpecs), - '{"tss":{"name":"tss","params":[' + - '{"name":"str","type":"string"},' + - '{"name":"num","type":"number"},' + - '{"name":"opt","type":{"name":"selection","data":["1","2","3"]}}]}}', + '{"tss":{"name":"tss","description":"(No description)","params":[' + + '{"name":"str","type":"string","description":"(No description)"},' + + '{"name":"num","type":"number","description":"(No description)"},' + + '{"name":"opt","type":{"name":"selection","data":["1","2","3"]},"description":"(No description)"}'+ + '],"isParent":false}}', 'JSON.stringify(commandSpecs)'); var remoter = function(args, context) { assert.is(context.commandName, 'tss', 'commandName is tss'); var cmd = altCanon.getCommand(context.commandName); return cmd.exec(args, context); };
--- a/browser/devtools/commandline/test/browser_gcli_context.js +++ b/browser/devtools/commandline/test/browser_gcli_context.js @@ -32,36 +32,27 @@ function test() { } // <INJECTED SOURCE:END> 'use strict'; // var helpers = require('gclitest/helpers'); // var mockCommands = require('gclitest/mockCommands'); -var cli = require('gcli/cli'); - -var origLogErrors = undefined; exports.setup = function(options) { mockCommands.setup(); - - origLogErrors = cli.logErrors; - cli.logErrors = false; }; exports.shutdown = function(options) { mockCommands.shutdown(); - - cli.logErrors = origLogErrors; - origLogErrors = undefined; }; exports.testBaseline = function(options) { - helpers.audit(options, [ + return helpers.audit(options, [ // These 3 establish a baseline for comparison when we have used the // context command { setup: 'ext', check: { input: 'ext', hints: ' -> context', markup: 'III', @@ -95,17 +86,17 @@ exports.testBaseline = function(options) command: { name: 'tsn' }, } } } ]); }; exports.testContext = function(options) { - helpers.audit(options, [ + return helpers.audit(options, [ // Use the 'tsn' context { setup: 'context tsn', check: { input: 'context tsn', hints: '', markup: 'VVVVVVVVVVV', message: '',
--- a/browser/devtools/commandline/test/browser_gcli_date.js +++ b/browser/devtools/commandline/test/browser_gcli_date.js @@ -1,12 +1,22 @@ /* - * Copyright 2009-2011 Mozilla Foundation and contributors - * Licensed under the New BSD license. See LICENSE.txt or: - * http://opensource.org/licenses/BSD-3-Clause + * Copyright 2012, Mozilla Foundation and contributors + * + * Licensed under the Apache License, Version 2.0 (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.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. */ // define(function(require, exports, module) { // <INJECTED SOURCE:START> // THIS FILE IS GENERATED FROM SOURCE IN THE GCLI PROJECT // DO NOT EDIT IT DIRECTLY @@ -75,18 +85,20 @@ exports.testIncrement = function(options // See comments in testParse var gap = new Date().getTime() - minusOne.getTime(); assert.ok(gap < 60000, 'now is less than a minute away'); }); }; exports.testInput = function(options) { - helpers.audit(options, [ + return helpers.audit(options, [ { + // See bug 892901 + skipRemainingIf: options.isFirefox, setup: 'tsdate 2001-01-01 1980-01-03', check: { input: 'tsdate 2001-01-01 1980-01-03', hints: '', markup: 'VVVVVVVVVVVVVVVVVVVVVVVVVVVV', status: 'VALID', message: '', args: { @@ -127,18 +139,20 @@ exports.testInput = function(options) { type: 'string', error: false } } ]); }; exports.testIncrDecr = function(options) { - helpers.audit(options, [ + return helpers.audit(options, [ { + // See bug 892901 + skipRemainingIf: options.isFirefox, setup: 'tsdate 2001-01-01<UP>', check: { input: 'tsdate 2001-01-02', hints: ' <d2>', markup: 'VVVVVVVVVVVVVVVVV', status: 'ERROR', message: '', args: { @@ -219,24 +233,24 @@ exports.testIncrDecr = function(options) assert.is(d1.getSeconds(), 0, 'd1 seconds'); assert.is(d1.getMilliseconds(), 0, 'd1 millis'); }, arg: ' 2001-02-01', status: 'VALID', message: '' }, d2: { - value: function(d1) { - assert.is(d1.getFullYear(), 2000, 'd1 year'); - assert.is(d1.getMonth(), 1, 'd1 month'); - assert.is(d1.getDate(), 28, 'd1 date'); - assert.is(d1.getHours(), 0, 'd1 hours'); - assert.is(d1.getMinutes(), 0, 'd1 minutes'); - assert.is(d1.getSeconds(), 0, 'd1 seconds'); - assert.is(d1.getMilliseconds(), 0, 'd1 millis'); + value: function(d2) { + assert.is(d2.getFullYear(), 2000, 'd2 year'); + assert.is(d2.getMonth(), 1, 'd2 month'); + assert.is(d2.getDate(), 28, 'd2 date'); + assert.is(d2.getHours(), 0, 'd2 hours'); + assert.is(d2.getMinutes(), 0, 'd2 minutes'); + assert.is(d2.getSeconds(), 0, 'd2 seconds'); + assert.is(d2.getMilliseconds(), 0, 'd2 millis'); }, arg: ' "2000-02-28"', status: 'VALID', message: '' }, } } }
--- a/browser/devtools/commandline/test/browser_gcli_fail.js +++ b/browser/devtools/commandline/test/browser_gcli_fail.js @@ -1,12 +1,22 @@ /* - * Copyright 2009-2011 Mozilla Foundation and contributors - * Licensed under the New BSD license. See LICENSE.txt or: - * http://opensource.org/licenses/BSD-3-Clause + * Copyright 2012, Mozilla Foundation and contributors + * + * Licensed under the Apache License, Version 2.0 (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.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. */ // define(function(require, exports, module) { // <INJECTED SOURCE:START> // THIS FILE IS GENERATED FROM SOURCE IN THE GCLI PROJECT // DO NOT EDIT IT DIRECTLY @@ -22,32 +32,23 @@ function test() { } // <INJECTED SOURCE:END> 'use strict'; // var helpers = require('gclitest/helpers'); // var mockCommands = require('gclitest/mockCommands'); -var cli = require('gcli/cli'); - -var origLogErrors = undefined; exports.setup = function(options) { mockCommands.setup(); - - origLogErrors = cli.logErrors; - cli.logErrors = false; }; exports.shutdown = function(options) { mockCommands.shutdown(); - - cli.logErrors = origLogErrors; - origLogErrors = undefined; }; exports.testBasic = function(options) { return helpers.audit(options, [ { setup: 'tsfail reject', exec: { completed: false,
new file mode 100644 --- /dev/null +++ b/browser/devtools/commandline/test/browser_gcli_file.js @@ -0,0 +1,848 @@ +/* + * Copyright 2012, Mozilla Foundation and contributors + * + * Licensed under the Apache License, Version 2.0 (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.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// define(function(require, exports, module) { + +// <INJECTED SOURCE:START> + +// THIS FILE IS GENERATED FROM SOURCE IN THE GCLI PROJECT +// DO NOT EDIT IT DIRECTLY + +var exports = {}; + +const TEST_URI = "data:text/html;charset=utf-8,<p id='gcli-input'>gcli-testFile.js</p>"; + +function test() { + helpers.addTabWithToolbar(TEST_URI, function(options) { + return helpers.runTests(options, exports); + }).then(finish); +} + +// <INJECTED SOURCE:END> + +'use strict'; + +// var helpers = require('gclitest/helpers'); +// var mockCommands = require('gclitest/mockCommands'); + +exports.setup = function(options) { + mockCommands.setup(); +}; + +exports.shutdown = function(options) { + mockCommands.shutdown(); +}; + +var local = false; + +exports.testBasic = function(options) { + var isPhantomjsFromFilesystem = (!options.isHttp && options.isPhantomjs); + return helpers.audit(options, [ + { + // These tests require us to be using node directly or to be in + // phantomjs connected to an allowexec enabled node server or to be in + // firefox. In short they only don't work when in phantomjs reading + // from the filesystem, but they do work in Firefox + skipRemainingIf: isPhantomjsFromFilesystem || options.isFirefox, + setup: 'tsfile open /', + check: { + input: 'tsfile open /', + hints: '', + markup: 'VVVVVVVVVVVVI', + cursor: 13, + current: 'p1', + status: 'ERROR', + message: '\'/\' is not a file', + args: { + command: { name: 'tsfile open' }, + p1: { + value: undefined, + arg: ' /', + status: 'INCOMPLETE', + message: '\'/\' is not a file' + } + } + } + }, + { + setup: 'tsfile open /zxcv', + check: { + input: 'tsfile open /zxcv', + // hints: ' -> /etc/', + markup: 'VVVVVVVVVVVVIIIII', + cursor: 17, + current: 'p1', + status: 'ERROR', + message: '\'/zxcv\' doesn\'t exist', + args: { + command: { name: 'tsfile open' }, + p1: { + value: undefined, + arg: ' /zxcv', + status: 'INCOMPLETE', + message: '\'/zxcv\' doesn\'t exist' + } + } + } + }, + { + skipIf: !local, + setup: 'tsfile open /mach_kernel', + check: { + input: 'tsfile open /mach_kernel', + hints: '', + markup: 'VVVVVVVVVVVVVVVVVVVVVVVV', + cursor: 24, + current: 'p1', + status: 'VALID', + message: '', + args: { + command: { name: 'tsfile open' }, + p1: { + value: '/mach_kernel', + arg: ' /mach_kernel', + status: 'VALID', + message: '' + } + } + } + }, + { + setup: 'tsfile saveas /', + check: { + input: 'tsfile saveas /', + hints: '', + markup: 'VVVVVVVVVVVVVVI', + cursor: 15, + current: 'p1', + status: 'ERROR', + message: '\'/\' already exists', + args: { + command: { name: 'tsfile saveas' }, + p1: { + value: undefined, + arg: ' /', + status: 'INCOMPLETE', + message: '\'/\' already exists' + } + } + } + }, + { + setup: 'tsfile saveas /zxcv', + check: { + input: 'tsfile saveas /zxcv', + // hints: ' -> /etc/', + markup: 'VVVVVVVVVVVVVVVVVVV', + cursor: 19, + current: 'p1', + status: 'VALID', + message: '', + args: { + command: { name: 'tsfile saveas' }, + p1: { + value: '/zxcv', + arg: ' /zxcv', + status: 'VALID', + message: '' + } + } + } + }, + { + skipIf: !local, + setup: 'tsfile saveas /mach_kernel', + check: { + input: 'tsfile saveas /mach_kernel', + hints: '', + markup: 'VVVVVVVVVVVVVVIIIIIIIIIIII', + cursor: 26, + current: 'p1', + status: 'ERROR', + message: '\'/mach_kernel\' already exists', + args: { + command: { name: 'tsfile saveas' }, + p1: { + value: undefined, + arg: ' /mach_kernel', + status: 'INCOMPLETE', + message: '\'/mach_kernel\' already exists' + } + } + } + }, + { + setup: 'tsfile save /', + check: { + input: 'tsfile save /', + hints: '', + markup: 'VVVVVVVVVVVVI', + cursor: 13, + current: 'p1', + status: 'ERROR', + message: '\'/\' is not a file', + args: { + command: { name: 'tsfile save' }, + p1: { + value: undefined, + arg: ' /', + status: 'INCOMPLETE', + message: '\'/\' is not a file' + } + } + } + }, + { + setup: 'tsfile save /zxcv', + check: { + input: 'tsfile save /zxcv', + // hints: ' -> /etc/', + markup: 'VVVVVVVVVVVVVVVVV', + cursor: 17, + current: 'p1', + status: 'VALID', + message: '', + args: { + command: { name: 'tsfile save' }, + p1: { + value: '/zxcv', + arg: ' /zxcv', + status: 'VALID', + message: '' + } + } + } + }, + { + skipIf: !local, + setup: 'tsfile save /mach_kernel', + check: { + input: 'tsfile save /mach_kernel', + hints: '', + markup: 'VVVVVVVVVVVVVVVVVVVVVVVV', + cursor: 24, + current: 'p1', + status: 'VALID', + message: '', + args: { + command: { name: 'tsfile save' }, + p1: { + value: '/mach_kernel', + arg: ' /mach_kernel', + status: 'VALID', + message: '' + } + } + } + }, + { + setup: 'tsfile cd /', + check: { + input: 'tsfile cd /', + hints: '', + markup: 'VVVVVVVVVVV', + cursor: 11, + current: 'p1', + status: 'VALID', + message: '', + args: { + command: { name: 'tsfile cd' }, + p1: { + value: '/', + arg: ' /', + status: 'VALID', + message: '' + } + } + } + }, + { + setup: 'tsfile cd /zxcv', + check: { + input: 'tsfile cd /zxcv', + // hints: ' -> /dev/', + markup: 'VVVVVVVVVVIIIII', + cursor: 15, + current: 'p1', + status: 'ERROR', + message: '\'/zxcv\' doesn\'t exist', + args: { + command: { name: 'tsfile cd' }, + p1: { + value: undefined, + arg: ' /zxcv', + status: 'INCOMPLETE', + message: '\'/zxcv\' doesn\'t exist' + } + } + } + }, + { + skipIf: true || !local, + setup: 'tsfile cd /etc/passwd', + check: { + input: 'tsfile cd /etc/passwd', + hints: ' -> /etc/pam.d/', + markup: 'VVVVVVVVVVIIIIIIIIIII', + cursor: 21, + current: 'p1', + status: 'ERROR', + message: '\'/etc/passwd\' is not a directory', + args: { + command: { name: 'tsfile cd' }, + p1: { + value: undefined, + arg: ' /etc/passwd', + status: 'INCOMPLETE', + message: '\'/etc/passwd\' is not a directory' + } + } + } + }, + { + setup: 'tsfile mkdir /', + check: { + input: 'tsfile mkdir /', + hints: '', + markup: 'VVVVVVVVVVVVVI', + cursor: 14, + current: 'p1', + status: 'ERROR', + message: ''/' already exists', + args: { + command: { name: 'tsfile mkdir' }, + p1: { + value: undefined, + arg: ' /', + status: 'INCOMPLETE', + message: '\'/\' already exists' + } + } + } + }, + { + setup: 'tsfile mkdir /zxcv', + check: { + input: 'tsfile mkdir /zxcv', + // hints: ' -> /dev/', + markup: 'VVVVVVVVVVVVVVVVVV', + cursor: 18, + current: 'p1', + status: 'VALID', + message: '', + args: { + command: { name: 'tsfile mkdir' }, + p1: { + value: '/zxcv', + arg: ' /zxcv', + status: 'VALID', + message: '' + } + } + } + }, + { + skipIf: !local, + setup: 'tsfile mkdir /mach_kernel', + check: { + input: 'tsfile mkdir /mach_kernel', + hints: '', + markup: 'VVVVVVVVVVVVVIIIIIIIIIIII', + cursor: 25, + current: 'p1', + status: 'ERROR', + message: '\'/mach_kernel\' already exists', + args: { + command: { name: 'tsfile mkdir' }, + p1: { + value: undefined, + arg: ' /mach_kernel', + status: 'INCOMPLETE', + message: '\'/mach_kernel\' already exists' + } + } + } + }, + { + setup: 'tsfile rm /', + check: { + input: 'tsfile rm /', + hints: '', + markup: 'VVVVVVVVVVV', + cursor: 11, + current: 'p1', + status: 'VALID', + message: '', + args: { + command: { name: 'tsfile rm' }, + p1: { + value: '/', + arg: ' /', + status: 'VALID', + message: '' + } + } + } + }, + { + setup: 'tsfile rm /zxcv', + check: { + input: 'tsfile rm /zxcv', + // hints: ' -> /etc/', + markup: 'VVVVVVVVVVIIIII', + cursor: 15, + current: 'p1', + status: 'ERROR', + message: '\'/zxcv\' doesn\'t exist', + args: { + command: { name: 'tsfile rm' }, + p1: { + value: undefined, + arg: ' /zxcv', + status: 'INCOMPLETE', + message: '\'/zxcv\' doesn\'t exist' + } + } + } + }, + { + skipIf: !local, + setup: 'tsfile rm /mach_kernel', + check: { + input: 'tsfile rm /mach_kernel', + hints: '', + markup: 'VVVVVVVVVVVVVVVVVVVVVV', + cursor: 22, + current: 'p1', + status: 'VALID', + message: '', + args: { + command: { name: 'tsfile rm' }, + p1: { + value: '/mach_kernel', + arg: ' /mach_kernel', + status: 'VALID', + message: '' + } + } + } + } + ]); +}; + +exports.testFirefoxBasic = function(options) { + return helpers.audit(options, [ + { + // These tests are just like the ones above tailored for running in + // Firefox + skipRemainingIf: true, + // skipRemainingIf: !options.isFirefox, + skipIf: true, + setup: 'tsfile open /', + check: { + input: 'tsfile open /', + hints: '', + markup: 'VVVVVVVVVVVVI', + cursor: 13, + current: 'p1', + status: 'ERROR', + message: '\'/\' is not a file', + args: { + command: { name: 'tsfile open' }, + p1: { + value: undefined, + arg: ' /', + status: 'INCOMPLETE', + message: '\'/\' is not a file' + } + } + } + }, + { + skipIf: true, + setup: 'tsfile open /zxcv', + check: { + input: 'tsfile open /zxcv', + // hints: ' -> /etc/', + markup: 'VVVVVVVVVVVVIIIII', + cursor: 17, + current: 'p1', + status: 'ERROR', + message: '\'/zxcv\' doesn\'t exist', + args: { + command: { name: 'tsfile open' }, + p1: { + value: undefined, + arg: ' /zxcv', + status: 'INCOMPLETE', + message: '\'/zxcv\' doesn\'t exist' + } + } + } + }, + { + skipIf: !local, + setup: 'tsfile open /mach_kernel', + check: { + input: 'tsfile open /mach_kernel', + hints: '', + markup: 'VVVVVVVVVVVVVVVVVVVVVVVV', + cursor: 24, + current: 'p1', + status: 'VALID', + message: '', + args: { + command: { name: 'tsfile open' }, + p1: { + value: '/mach_kernel', + arg: ' /mach_kernel', + status: 'VALID', + message: '' + } + } + } + }, + { + skipIf: true, + setup: 'tsfile saveas /', + check: { + input: 'tsfile saveas /', + hints: '', + markup: 'VVVVVVVVVVVVVVI', + cursor: 15, + current: 'p1', + status: 'ERROR', + message: '\'/\' already exists', + args: { + command: { name: 'tsfile saveas' }, + p1: { + value: undefined, + arg: ' /', + status: 'INCOMPLETE', + message: '\'/\' already exists' + } + } + } + }, + { + skipIf: true, + setup: 'tsfile saveas /zxcv', + check: { + input: 'tsfile saveas /zxcv', + // hints: ' -> /etc/', + markup: 'VVVVVVVVVVVVVVVVVVV', + cursor: 19, + current: 'p1', + status: 'VALID', + message: '', + args: { + command: { name: 'tsfile saveas' }, + p1: { + value: '/zxcv', + arg: ' /zxcv', + status: 'VALID', + message: '' + } + } + } + }, + { + skipIf: !local, + setup: 'tsfile saveas /mach_kernel', + check: { + input: 'tsfile saveas /mach_kernel', + hints: '', + markup: 'VVVVVVVVVVVVVVIIIIIIIIIIII', + cursor: 26, + current: 'p1', + status: 'ERROR', + message: '\'/mach_kernel\' already exists', + args: { + command: { name: 'tsfile saveas' }, + p1: { + value: undefined, + arg: ' /mach_kernel', + status: 'INCOMPLETE', + message: '\'/mach_kernel\' already exists' + } + } + } + }, + { + skipIf: true, + setup: 'tsfile save /', + check: { + input: 'tsfile save /', + hints: '', + markup: 'VVVVVVVVVVVVI', + cursor: 13, + current: 'p1', + status: 'ERROR', + message: '\'/\' is not a file', + args: { + command: { name: 'tsfile save' }, + p1: { + value: undefined, + arg: ' /', + status: 'INCOMPLETE', + message: '\'/\' is not a file' + } + } + } + }, + { + skipIf: true, + setup: 'tsfile save /zxcv', + check: { + input: 'tsfile save /zxcv', + // hints: ' -> /etc/', + markup: 'VVVVVVVVVVVVVVVVV', + cursor: 17, + current: 'p1', + status: 'VALID', + message: '', + args: { + command: { name: 'tsfile save' }, + p1: { + value: '/zxcv', + arg: ' /zxcv', + status: 'VALID', + message: '' + } + } + } + }, + { + skipIf: !local, + setup: 'tsfile save /mach_kernel', + check: { + input: 'tsfile save /mach_kernel', + hints: '', + markup: 'VVVVVVVVVVVVVVVVVVVVVVVV', + cursor: 24, + current: 'p1', + status: 'VALID', + message: '', + args: { + command: { name: 'tsfile save' }, + p1: { + value: '/mach_kernel', + arg: ' /mach_kernel', + status: 'VALID', + message: '' + } + } + } + }, + { + setup: 'tsfile cd /', + check: { + input: 'tsfile cd /', + hints: '', + markup: 'VVVVVVVVVVV', + cursor: 11, + current: 'p1', + status: 'VALID', + message: '', + args: { + command: { name: 'tsfile cd' }, + p1: { + value: '/', + arg: ' /', + status: 'VALID', + message: '' + } + } + } + }, + { + setup: 'tsfile cd /zxcv', + check: { + input: 'tsfile cd /zxcv', + // hints: ' -> /dev/', + // markup: 'VVVVVVVVVVIIIII', + cursor: 15, + current: 'p1', + // status: 'ERROR', + message: '\'/zxcv\' doesn\'t exist', + args: { + command: { name: 'tsfile cd' }, + p1: { + value: undefined, + arg: ' /zxcv', + // status: 'INCOMPLETE', + message: '\'/zxcv\' doesn\'t exist' + } + } + } + }, + { + skipIf: true || !local, + setup: 'tsfile cd /etc/passwd', + check: { + input: 'tsfile cd /etc/passwd', + hints: ' -> /etc/pam.d/', + markup: 'VVVVVVVVVVIIIIIIIIIII', + cursor: 21, + current: 'p1', + status: 'ERROR', + message: '\'/etc/passwd\' is not a directory', + args: { + command: { name: 'tsfile cd' }, + p1: { + value: undefined, + arg: ' /etc/passwd', + status: 'INCOMPLETE', + message: '\'/etc/passwd\' is not a directory' + } + } + } + }, + { + setup: 'tsfile mkdir /', + check: { + input: 'tsfile mkdir /', + hints: '', + markup: 'VVVVVVVVVVVVVI', + cursor: 14, + current: 'p1', + status: 'ERROR', + message: ''/' already exists', + args: { + command: { name: 'tsfile mkdir' }, + p1: { + value: undefined, + arg: ' /', + status: 'INCOMPLETE', + message: '\'/\' already exists' + } + } + } + }, + { + setup: 'tsfile mkdir /zxcv', + check: { + input: 'tsfile mkdir /zxcv', + // hints: ' -> /dev/', + markup: 'VVVVVVVVVVVVVVVVVV', + cursor: 18, + current: 'p1', + status: 'VALID', + message: '', + args: { + command: { name: 'tsfile mkdir' }, + p1: { + value: '/zxcv', + arg: ' /zxcv', + status: 'VALID', + message: '' + } + } + } + }, + { + skipIf: !local, + setup: 'tsfile mkdir /mach_kernel', + check: { + input: 'tsfile mkdir /mach_kernel', + hints: '', + markup: 'VVVVVVVVVVVVVIIIIIIIIIIII', + cursor: 25, + current: 'p1', + status: 'ERROR', + message: '\'/mach_kernel\' already exists', + args: { + command: { name: 'tsfile mkdir' }, + p1: { + value: undefined, + arg: ' /mach_kernel', + status: 'INCOMPLETE', + message: '\'/mach_kernel\' already exists' + } + } + } + }, + { + skipIf: true, + setup: 'tsfile rm /', + check: { + input: 'tsfile rm /', + hints: '', + markup: 'VVVVVVVVVVV', + cursor: 11, + current: 'p1', + status: 'VALID', + message: '', + args: { + command: { name: 'tsfile rm' }, + p1: { + value: '/', + arg: ' /', + status: 'VALID', + message: '' + } + } + } + }, + { + skipIf: true, + setup: 'tsfile rm /zxcv', + check: { + input: 'tsfile rm /zxcv', + // hints: ' -> /etc/', + markup: 'VVVVVVVVVVIIIII', + cursor: 15, + current: 'p1', + status: 'ERROR', + message: '\'/zxcv\' doesn\'t exist', + args: { + command: { name: 'tsfile rm' }, + p1: { + value: undefined, + arg: ' /zxcv', + status: 'INCOMPLETE', + message: '\'/zxcv\' doesn\'t exist' + } + } + } + }, + { + skipIf: !local, + setup: 'tsfile rm /mach_kernel', + check: { + input: 'tsfile rm /mach_kernel', + hints: '', + markup: 'VVVVVVVVVVVVVVVVVVVVVV', + cursor: 22, + current: 'p1', + status: 'VALID', + message: '', + args: { + command: { name: 'tsfile rm' }, + p1: { + value: '/mach_kernel', + arg: ' /mach_kernel', + status: 'VALID', + message: '' + } + } + } + } + ]); +}; + + +// });
new file mode 100644 --- /dev/null +++ b/browser/devtools/commandline/test/browser_gcli_fileparser.js @@ -0,0 +1,57 @@ +/* + * Copyright 2012, Mozilla Foundation and contributors + * + * Licensed under the Apache License, Version 2.0 (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.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// define(function(require, exports, module) { + +// <INJECTED SOURCE:START> + +// THIS FILE IS GENERATED FROM SOURCE IN THE GCLI PROJECT +// DO NOT EDIT IT DIRECTLY + +var exports = {}; + +const TEST_URI = "data:text/html;charset=utf-8,<p id='gcli-input'>gcli-testFileparser.js</p>"; + +function test() { + helpers.addTabWithToolbar(TEST_URI, function(options) { + return helpers.runTests(options, exports); + }).then(finish); +} + +// <INJECTED SOURCE:END> + +'use strict'; + +// var assert = require('test/assert'); +var fileparser = require('util/fileparser'); + +var local = false; + +exports.testGetPredictor = function(options) { + if (!options.isNode || !local) { + return; + } + + var options = { filetype: 'file', existing: 'yes' }; + var predictor = fileparser.getPredictor('/usr/locl/bin/nmp', options); + return predictor().then(function(replies) { + assert.is(replies[0].name, + '/usr/local/bin/npm', + 'predict npm'); + }); +}; + +// });
new file mode 100644 --- /dev/null +++ b/browser/devtools/commandline/test/browser_gcli_filesystem.js @@ -0,0 +1,78 @@ +/* + * Copyright 2012, Mozilla Foundation and contributors + * + * Licensed under the Apache License, Version 2.0 (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.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// define(function(require, exports, module) { + +// <INJECTED SOURCE:START> + +// THIS FILE IS GENERATED FROM SOURCE IN THE GCLI PROJECT +// DO NOT EDIT IT DIRECTLY + +var exports = {}; + +const TEST_URI = "data:text/html;charset=utf-8,<p id='gcli-input'>gcli-testFilesystem.js</p>"; + +function test() { + helpers.addTabWithToolbar(TEST_URI, function(options) { + return helpers.runTests(options, exports); + }).then(finish); +} + +// <INJECTED SOURCE:END> + +'use strict'; + +// var assert = require('test/assert'); +// var helpers = require('gclitest/helpers'); +var filesystem = require('util/filesystem'); + +exports.testSplit = function(options) { + if (!options.isNode) { + return; + } + + helpers.arrayIs(filesystem.split('', '/'), + [ '.' ], + 'split <blank>'); + + helpers.arrayIs(filesystem.split('a', '/'), + [ 'a' ], + 'split a'); + + helpers.arrayIs(filesystem.split('a/b/c', '/'), + [ 'a', 'b', 'c' ], + 'split a/b/c'); + + helpers.arrayIs(filesystem.split('/a/b/c/', '/'), + [ 'a', 'b', 'c' ], + 'split a/b/c'); + + helpers.arrayIs(filesystem.split('/a/b///c/', '/'), + [ 'a', 'b', 'c' ], + 'split a/b/c'); +}; + +exports.testJoin = function(options) { + if (!options.isNode) { + return; + } + + assert.is(filesystem.join('usr', 'local', 'bin'), + 'usr/local/bin', + 'join to usr/local/bin'); +}; + +// });
--- a/browser/devtools/commandline/test/browser_gcli_focus.js +++ b/browser/devtools/commandline/test/browser_gcli_focus.js @@ -1,12 +1,22 @@ /* - * Copyright 2009-2011 Mozilla Foundation and contributors - * Licensed under the New BSD license. See LICENSE.txt or: - * http://opensource.org/licenses/BSD-3-Clause + * Copyright 2012, Mozilla Foundation and contributors + * + * Licensed under the Apache License, Version 2.0 (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.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. */ // define(function(require, exports, module) { // <INJECTED SOURCE:START> // THIS FILE IS GENERATED FROM SOURCE IN THE GCLI PROJECT // DO NOT EDIT IT DIRECTLY
--- a/browser/devtools/commandline/test/browser_gcli_incomplete.js +++ b/browser/devtools/commandline/test/browser_gcli_incomplete.js @@ -1,12 +1,22 @@ /* - * Copyright 2009-2011 Mozilla Foundation and contributors - * Licensed under the New BSD license. See LICENSE.txt or: - * http://opensource.org/licenses/BSD-3-Clause + * Copyright 2012, Mozilla Foundation and contributors + * + * Licensed under the Apache License, Version 2.0 (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.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. */ // define(function(require, exports, module) { // <INJECTED SOURCE:START> // THIS FILE IS GENERATED FROM SOURCE IN THE GCLI PROJECT // DO NOT EDIT IT DIRECTLY
--- a/browser/devtools/commandline/test/browser_gcli_menu.js +++ b/browser/devtools/commandline/test/browser_gcli_menu.js @@ -1,12 +1,22 @@ /* - * Copyright 2009-2011 Mozilla Foundation and contributors - * Licensed under the New BSD license. See LICENSE.txt or: - * http://opensource.org/licenses/BSD-3-Clause + * Copyright 2012, Mozilla Foundation and contributors + * + * Licensed under the Apache License, Version 2.0 (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.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. */ // define(function(require, exports, module) { // <INJECTED SOURCE:START> // THIS FILE IS GENERATED FROM SOURCE IN THE GCLI PROJECT // DO NOT EDIT IT DIRECTLY
--- a/browser/devtools/commandline/test/browser_gcli_node.js +++ b/browser/devtools/commandline/test/browser_gcli_node.js @@ -1,12 +1,22 @@ /* - * Copyright 2009-2011 Mozilla Foundation and contributors - * Licensed under the New BSD license. See LICENSE.txt or: - * http://opensource.org/licenses/BSD-3-Clause + * Copyright 2012, Mozilla Foundation and contributors + * + * Licensed under the Apache License, Version 2.0 (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.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. */ // define(function(require, exports, module) { // <INJECTED SOURCE:START> // THIS FILE IS GENERATED FROM SOURCE IN THE GCLI PROJECT // DO NOT EDIT IT DIRECTLY
--- a/browser/devtools/commandline/test/browser_gcli_remote.js +++ b/browser/devtools/commandline/test/browser_gcli_remote.js @@ -43,16 +43,17 @@ exports.setup = function(options) { mockCommands.setup(); }; exports.shutdown = function(options) { mockCommands.shutdown(); }; exports.testRemote = function(options) { + var connected = false; return helpers.audit(options, [ { skipRemainingIf: !options.isHttp, setup: 'remote ', check: { input: 'remote ', hints: '', markup: 'EEEEEEV', @@ -81,16 +82,82 @@ exports.testRemote = function(options) { args: { command: { name: 'connect' }, prefix: { value: 'remote', arg: ' remote', status: 'VALID', message: '' }, host: { value: undefined, arg: '', status: 'VALID', message: '' }, port: { value: undefined, arg: '', status: 'VALID', message: '' }, } }, exec: { + completed: false, + error: false + }, + post: function(output, data) { + connected = !output.error; + if (!connected) { + console.log('Failure from "connect remote". Run server with "node gcli server start --websocket --allowexec" to allow remote command testing'); + } + } + }, + { + // We do a connect-disconnect dance for 2 reasons, partly re-establishing + // a connection is a good test, and secondly it lets us have minimal + // testing on the first connection so we don't need to turn websockets + // on all the time + setup: 'disconnect remote --force', + skipRemainingIf: !connected, + check: { + input: 'disconnect remote --force', + hints: '', + markup: 'VVVVVVVVVVVVVVVVVVVVVVVVV', + cursor: 25, + current: 'force', + status: 'VALID', + message: '', + unassigned: [ ], + args: { + command: { name: 'disconnect' }, + prefix: { + value: function(connection) { + assert.is(connection.prefix, 'remote', 'disconnecting remote'); + }, + arg: ' remote', + status: 'VALID', + message: '' + } + } + }, + exec: { + output: /^Removed [0-9]* commands.$/, + completed: true, + type: 'string', + error: false + } + }, + { + setup: 'connect remote', + check: { + input: 'connect remote', + hints: ' [options]', + markup: 'VVVVVVVVVVVVVV', + cursor: 14, + current: 'prefix', + status: 'VALID', + options: [ ], + message: '', + predictions: [ ], + unassigned: [ ], + args: { + command: { name: 'connect' }, + prefix: { value: 'remote', arg: ' remote', status: 'VALID', message: '' }, + host: { value: undefined, arg: '', status: 'VALID', message: '' }, + port: { value: undefined, arg: '', status: 'VALID', message: '' }, + } + }, + exec: { output: /^Added [0-9]* commands.$/, completed: false, type: 'string', error: false } }, { setup: 'remote ',
--- a/browser/devtools/commandline/test/browser_gcli_spell.js +++ b/browser/devtools/commandline/test/browser_gcli_spell.js @@ -30,17 +30,17 @@ function test() { }).then(finish); } // <INJECTED SOURCE:END> 'use strict'; // var assert = require('test/assert'); -var spell = require('gcli/types/spell'); +var spell = require('util/spell'); exports.testSpellerSimple = function(options) { var alternatives = Object.keys(options.window); assert.is(spell.correct('document', alternatives), 'document'); assert.is(spell.correct('documen', alternatives), 'document'); if (options.isJsdom) { @@ -49,10 +49,39 @@ exports.testSpellerSimple = function(opt else { assert.is(spell.correct('ocument', alternatives), 'document'); } assert.is(spell.correct('odcument', alternatives), 'document'); assert.is(spell.correct('=========', alternatives), undefined); }; +exports.testRank = function(options) { + var distances = spell.rank('fred', [ 'banana', 'fred', 'ed', 'red', 'FRED' ]); + + assert.is(distances.length, 5, 'rank length'); + + assert.is(distances[0].name, 'fred', 'fred name #0'); + assert.is(distances[1].name, 'FRED', 'FRED name #1'); + assert.is(distances[2].name, 'red', 'red name #2'); + assert.is(distances[3].name, 'ed', 'ed name #3'); + assert.is(distances[4].name, 'banana', 'banana name #4'); + + assert.is(distances[0].dist, 0, 'fred dist 0'); + assert.is(distances[1].dist, 4, 'FRED dist 4'); + assert.is(distances[2].dist, 10, 'red dist 10'); + assert.is(distances[3].dist, 20, 'ed dist 20'); + assert.is(distances[4].dist, 100, 'banana dist 100'); +}; + +exports.testRank2 = function(options) { + var distances = spell.rank('caps', [ 'CAPS', 'false' ]); + assert.is(JSON.stringify(distances), + '[{"name":"CAPS","dist":4},{"name":"false","dist":50}]', + 'spell.rank("caps", [ "CAPS", "false" ]'); +}; + +exports.testDistancePrefix = function(options) { + assert.is(spell.distancePrefix('fred', 'freddy'), 0, 'distancePrefix fred'); + assert.is(spell.distancePrefix('FRED', 'freddy'), 4, 'distancePrefix FRED'); +}; // });
--- a/browser/devtools/commandline/test/browser_gcli_string.js +++ b/browser/devtools/commandline/test/browser_gcli_string.js @@ -42,17 +42,17 @@ exports.setup = function(options) { mockCommands.setup(); }; exports.shutdown = function(options) { mockCommands.shutdown(); }; exports.testNewLine = function(options) { - helpers.audit(options, [ + return helpers.audit(options, [ { setup: 'echo a\\nb', check: { input: 'echo a\\nb', hints: '', markup: 'VVVVVVVVV', cursor: 9, current: 'message', @@ -67,17 +67,17 @@ exports.testNewLine = function(options) } } } } ]); }; exports.testTab = function(options) { - helpers.audit(options, [ + return helpers.audit(options, [ { setup: 'echo a\\tb', check: { input: 'echo a\\tb', hints: '', markup: 'VVVVVVVVV', cursor: 9, current: 'message', @@ -92,17 +92,17 @@ exports.testTab = function(options) { } } } } ]); }; exports.testEscape = function(options) { - helpers.audit(options, [ + return helpers.audit(options, [ { // What's typed is actually: // tsrsrsr a\\ b c setup: 'tsrsrsr a\\\\ b c', check: { input: 'tsrsrsr a\\\\ b c', hints: '', markup: 'VVVVVVVVVVVVVVV', @@ -138,17 +138,17 @@ exports.testEscape = function(options) { p3: { value: 'asd', arg: ' asd', status: 'VALID', message: '' }, } } } ]); }; exports.testBlank = function(options) { - helpers.audit(options, [ + return helpers.audit(options, [ { setup: 'tsrsrsr a "" c', check: { input: 'tsrsrsr a "" c', hints: '', markup: 'VVVVVVVVVVVVVV', cursor: 14, current: 'p3', @@ -208,17 +208,17 @@ exports.testBlank = function(options) { } } } } ]); }; exports.testBlankWithParam = function(options) { - helpers.audit(options, [ + return helpers.audit(options, [ { setup: 'tsrsrsr a --p3', check: { input: 'tsrsrsr a --p3', hints: ' <string> <p2>', markup: 'VVVVVVVVVVVVVVV', cursor: 15, current: 'p3',
--- a/browser/devtools/commandline/test/helpers.js +++ b/browser/devtools/commandline/test/helpers.js @@ -25,16 +25,17 @@ Components.utils.import("resource://gre/ let console = (Cu.import("resource://gre/modules/devtools/Console.jsm", {})).console; let TargetFactory = (Cu.import("resource://gre/modules/devtools/Loader.jsm", {})).devtools.TargetFactory; let promise = (Cu.import("resource://gre/modules/commonjs/sdk/core/promise.js", {})).Promise; let assert = { ok: ok, is: is, log: info }; var util = require('util/util'); +var cli = require('gcli/cli'); var converters = require('gcli/converters'); /** * Warning: For use with Firefox Mochitests only. * * Open a new tab at a URL and call a callback on load, and then tidy up when * the callback finishes. @@ -561,17 +562,17 @@ helpers._check = function(options, name, var hintCheck = function(actualHints) { assert.is(actualHints, checks.hints, 'hints' + suffix); }; outstanding.push(helpers._actual.hints(options).then(hintCheck)); } if ('predictions' in checks) { var predictionsCheck = function(actualPredictions) { - helpers._arrayIs(actualPredictions, + helpers.arrayIs(actualPredictions, checks.predictions, 'predictions' + suffix); }; outstanding.push(helpers._actual.predictions(options).then(predictionsCheck)); } if ('predictionsContains' in checks) { var containsCheck = function(actualPredictions) { @@ -580,17 +581,17 @@ helpers._check = function(options, name, assert.ok(index !== -1, 'predictionsContains:' + prediction + suffix); }); }; outstanding.push(helpers._actual.predictions(options).then(containsCheck)); } if ('unassigned' in checks) { - helpers._arrayIs(helpers._actual.unassigned(options), + helpers.arrayIs(helpers._actual.unassigned(options), checks.unassigned, 'unassigned' + suffix); } if ('tooltipState' in checks) { if (options.isJsdom) { assert.log('Skipped ' + name + '/tooltipState due to jsdom'); } @@ -608,17 +609,17 @@ helpers._check = function(options, name, else { assert.is(helpers._actual.outputState(options), checks.outputState, 'outputState' + suffix); } } if ('options' in checks) { - helpers._arrayIs(helpers._actual.options(options), + helpers.arrayIs(helpers._actual.options(options), checks.options, 'options' + suffix); } if ('error' in checks) { assert.is(helpers._actual.message(options), checks.error, 'error' + suffix); } @@ -713,39 +714,54 @@ helpers._check = function(options, name, * @param expected See helpers.audit for a list of available exec checks * @return A promise which resolves to undefined when the checks are complete */ helpers._exec = function(options, name, expected) { if (expected == null) { return promise.resolve({}); } + var origLogErrors = cli.logErrors; + if (expected.error) { + cli.logErrors = false; + } + var output; try { output = options.display.requisition.exec({ hidden: true }); } catch (ex) { assert.ok(false, 'Failure executing \'' + name + '\': ' + ex); util.errorHandler(ex); + if (expected.error) { + cli.logErrors = origLogErrors; + } return promise.resolve({}); } if ('completed' in expected) { assert.is(output.completed, expected.completed, 'output.completed false for: ' + name); } if (!options.window.document.createElement) { assert.log('skipping output tests (missing doc.createElement) for ' + name); + + if (expected.error) { + cli.logErrors = origLogErrors; + } return promise.resolve({ output: output }); } if (!('output' in expected)) { + if (expected.error) { + cli.logErrors = origLogErrors; + } return promise.resolve({ output: output }); } var checkOutput = function() { if ('type' in expected) { assert.is(output.type, expected.type, 'output.type for: ' + name); @@ -783,16 +799,19 @@ helpers._exec = function(options, name, expected.output.forEach(function(match) { doTest(match, actualOutput); }); } else { doTest(expected.output, actualOutput); } + if (expected.error) { + cli.logErrors = origLogErrors; + } return { output: output, text: actualOutput }; }); }; return output.promise.then(checkOutput, checkOutput); }; /** @@ -935,40 +954,40 @@ helpers.audit = function(options, audits if (name == null && typeof audit.setup === 'string') { name = audit.setup; } if (assert.testLogging) { log('- START \'' + name + '\' in ' + assert.currentTest); } - if (audit.skipIf) { - var skip = (typeof audit.skipIf === 'function') ? - audit.skipIf(options) : - !!audit.skipIf; - if (skip) { - var reason = audit.skipIf.name ? 'due to ' + audit.skipIf.name : ''; - assert.log('Skipped ' + name + ' ' + reason); - return promise.resolve(undefined); - } - } - if (audit.skipRemainingIf) { var skipRemainingIf = (typeof audit.skipRemainingIf === 'function') ? audit.skipRemainingIf(options) : !!audit.skipRemainingIf; if (skipRemainingIf) { skipReason = audit.skipRemainingIf.name ? 'due to ' + audit.skipRemainingIf.name : ''; assert.log('Skipped ' + name + ' ' + skipReason); return promise.resolve(undefined); } } + if (audit.skipIf) { + var skip = (typeof audit.skipIf === 'function') ? + audit.skipIf(options) : + !!audit.skipIf; + if (skip) { + var reason = audit.skipIf.name ? 'due to ' + audit.skipIf.name : ''; + assert.log('Skipped ' + name + ' ' + reason); + return promise.resolve(undefined); + } + } + if (skipReason != null) { assert.log('Skipped ' + name + ' ' + skipReason); return promise.resolve(undefined); } var start = new Date().getTime(); var setupDone = helpers._setup(options, name, audit.setup); @@ -1003,17 +1022,17 @@ helpers.audit = function(options, audits }).then(function() { return options.display.inputter.setInput(''); }); }; /** * Compare 2 arrays. */ -helpers._arrayIs = function(actual, expected, message) { +helpers.arrayIs = function(actual, expected, message) { assert.ok(Array.isArray(actual), 'actual is not an array: ' + message); assert.ok(Array.isArray(expected), 'expected is not an array: ' + message); if (!Array.isArray(actual) || !Array.isArray(expected)) { return; } assert.is(actual.length, expected.length, 'array length: ' + message);
--- a/browser/devtools/commandline/test/mockCommands.js +++ b/browser/devtools/commandline/test/mockCommands.js @@ -477,16 +477,126 @@ var tsfail = { if (args.method === 'throwstring') { throw 'thrown string'; } return 'no error'; } }; +var tsfile = { + item: 'command', + name: 'tsfile', + description: 'test file params', +}; + +var tsfileOpen = { + item: 'command', + name: 'tsfile open', + description: 'a file param in open mode', + params: [ + { + name: 'p1', + type: { + name: 'file', + filetype: 'file', + existing: 'yes' + } + } + ], + exec: createExec('tsfile open') +}; + +var tsfileSaveas = { + item: 'command', + name: 'tsfile saveas', + description: 'a file param in saveas mode', + params: [ + { + name: 'p1', + type: { + name: 'file', + filetype: 'file', + existing: 'no' + } + } + ], + exec: createExec('tsfile saveas') +}; + +var tsfileSave = { + item: 'command', + name: 'tsfile save', + description: 'a file param in save mode', + params: [ + { + name: 'p1', + type: { + name: 'file', + filetype: 'file', + existing: 'maybe' + } + } + ], + exec: createExec('tsfile save') +}; + +var tsfileCd = { + item: 'command', + name: 'tsfile cd', + description: 'a file param in cd mode', + params: [ + { + name: 'p1', + type: { + name: 'file', + filetype: 'directory', + existing: 'yes' + } + } + ], + exec: createExec('tsfile cd') +}; + +var tsfileMkdir = { + item: 'command', + name: 'tsfile mkdir', + description: 'a file param in mkdir mode', + params: [ + { + name: 'p1', + type: { + name: 'file', + filetype: 'directory', + existing: 'no' + } + } + ], + exec: createExec('tsfile mkdir') +}; + +var tsfileRm = { + item: 'command', + name: 'tsfile rm', + description: 'a file param in rm mode', + params: [ + { + name: 'p1', + type: { + name: 'file', + filetype: 'any', + existing: 'yes' + } + } + ], + exec: createExec('tsfile rm') +}; + + + mockCommands.commands = {}; /** * Registration and de-registration. */ mockCommands.setup = function(opts) { // setup/shutdown needs to register/unregister types, however that means we // need to re-initialize mockCommands.option1 and mockCommands.option2 with @@ -528,16 +638,23 @@ mockCommands.setup = function(opts) { mockCommands.commands.tselarr = canon.addCommand(tselarr); mockCommands.commands.tsm = canon.addCommand(tsm); mockCommands.commands.tsg = canon.addCommand(tsg); mockCommands.commands.tshidden = canon.addCommand(tshidden); mockCommands.commands.tscook = canon.addCommand(tscook); mockCommands.commands.tslong = canon.addCommand(tslong); mockCommands.commands.tsdate = canon.addCommand(tsdate); mockCommands.commands.tsfail = canon.addCommand(tsfail); + mockCommands.commands.tsfile = canon.addCommand(tsfile); + mockCommands.commands.tsfileOpen = canon.addCommand(tsfileOpen); + mockCommands.commands.tsfileSaveas = canon.addCommand(tsfileSaveas); + mockCommands.commands.tsfileSave = canon.addCommand(tsfileSave); + mockCommands.commands.tsfileCd = canon.addCommand(tsfileCd); + mockCommands.commands.tsfileMkdir = canon.addCommand(tsfileMkdir); + mockCommands.commands.tsfileRm = canon.addCommand(tsfileRm); }; mockCommands.shutdown = function(opts) { canon.removeCommand(tsv); canon.removeCommand(tsr); canon.removeCommand(tsrsrsr); canon.removeCommand(tso); canon.removeCommand(tse); @@ -559,16 +676,23 @@ mockCommands.shutdown = function(opts) { canon.removeCommand(tselarr); canon.removeCommand(tsm); canon.removeCommand(tsg); canon.removeCommand(tshidden); canon.removeCommand(tscook); canon.removeCommand(tslong); canon.removeCommand(tsdate); canon.removeCommand(tsfail); + canon.removeCommand(tsfile); + canon.removeCommand(tsfileOpen); + canon.removeCommand(tsfileSaveas); + canon.removeCommand(tsfileSave); + canon.removeCommand(tsfileCd); + canon.removeCommand(tsfileMkdir); + canon.removeCommand(tsfileRm); types.removeType(mockCommands.optionType); types.removeType(mockCommands.optionValue); mockCommands.commands = {}; };
--- a/browser/devtools/framework/test/Makefile.in +++ b/browser/devtools/framework/test/Makefile.in @@ -17,16 +17,17 @@ MOCHITEST_BROWSER_FILES = \ browser_toolbox_dynamic_registration.js \ browser_toolbox_hosts.js \ browser_toolbox_ready.js \ browser_toolbox_select_event.js \ browser_target_events.js \ browser_toolbox_tool_ready.js \ browser_toolbox_sidebar.js \ browser_toolbox_window_shortcuts.js \ + browser_toolbox_tabsswitch_shortcuts.js \ browser_toolbox_window_title_changes.js \ browser_toolbox_options.js \ browser_toolbox_options_disablejs.js \ browser_toolbox_options_disablejs.html \ browser_toolbox_options_disablejs_iframe.html \ browser_toolbox_highlight.js \ browser_toolbox_raise.js \ $(NULL)
new file mode 100644 --- /dev/null +++ b/browser/devtools/framework/test/browser_toolbox_tabsswitch_shortcuts.js @@ -0,0 +1,88 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +let Toolbox = devtools.Toolbox; + +let toolbox, toolIDs, idIndex, secondTime = false, + reverse = false, nextKey = null, prevKey = null; + +function test() { + waitForExplicitFinish(); + + addTab("about:blank", function() { + toolIDs = [tool.id for (tool of gDevTools.getToolDefinitionArray())]; + let target = TargetFactory.forTab(gBrowser.selectedTab); + idIndex = 0; + gDevTools.showToolbox(target, toolIDs[0], Toolbox.HostType.BOTTOM) + .then(testShortcuts); + }); +} + +function testShortcuts(aToolbox, aIndex) { + if (aIndex == toolIDs.length) { + aIndex = 0; + if (secondTime) { + secondTime = false; + reverse = true; + aIndex = toolIDs.length - 2; + } + else { + secondTime = true; + } + } + else if (aIndex == -1) { + aIndex = toolIDs.length - 1; + if (secondTime) { + tidyUp(); + return; + } + secondTime = true; + } + + toolbox = aToolbox; + if (!nextKey) { + nextKey = toolbox.doc.getElementById("toolbox-next-tool-key") + .getAttribute("key"); + prevKey = toolbox.doc.getElementById("toolbox-previous-tool-key") + .getAttribute("key"); + } + info("Toolbox fired a `ready` event"); + + toolbox.once("select", onSelect); + + if (aIndex != null) { + // This if block is to allow the call of onSelect without shortcut press for + // the first time. That happens because on opening of toolbox, one tool gets + // selected atleast. + + let key = (reverse ? prevKey: nextKey); + let modifiers = { + accelKey: true + }; + idIndex = aIndex; + info("Testing shortcut to switch to tool " + aIndex + ":" + toolIDs[aIndex] + + " using key " + key); + EventUtils.synthesizeKey(key, modifiers, toolbox.doc.defaultView); + } +} + +function onSelect(event, id) { + info("toolbox-select event from " + id); + + is(toolIDs.indexOf(id), idIndex, + "Correct tool is selected on pressing the shortcut for " + id); + // Execute soon to reset the stack trace. + executeSoon(() => { + testShortcuts(toolbox, idIndex + (reverse ? -1: 1)); + }); +} + +function tidyUp() { + toolbox.destroy().then(function() { + gBrowser.removeCurrentTab(); + + toolbox = toolIDs = idIndex = Toolbox = secondTime = reverse = nextKey = + prevKey = null; + finish(); + }); +}
--- a/browser/devtools/framework/toolbox.js +++ b/browser/devtools/framework/toolbox.js @@ -202,16 +202,17 @@ Toolbox.prototype = { let closeButton = this.doc.getElementById("toolbox-close"); closeButton.addEventListener("command", this.destroy, true); this._buildDockButtons(); this._buildOptions(); this._buildTabs(); this._buildButtons(); this._addKeysToWindow(); + this._addToolSwitchingKeys(); this._telemetry.toolOpened("toolbox"); this.selectTool(this._defaultToolId).then(function(panel) { this.emit("ready"); deferred.resolve(); }.bind(this)); }; @@ -225,16 +226,23 @@ Toolbox.prototype = { _buildOptions: function TBOX__buildOptions() { let key = this.doc.getElementById("toolbox-options-key"); key.addEventListener("command", function(toolId) { this.selectTool(toolId); }.bind(this, "options"), true); }, + _addToolSwitchingKeys: function TBOX__addToolSwitchingKeys() { + let nextKey = this.doc.getElementById("toolbox-next-tool-key"); + nextKey.addEventListener("command", this.selectNextTool.bind(this), true); + let prevKey = this.doc.getElementById("toolbox-previous-tool-key"); + prevKey.addEventListener("command", this.selectPreviousTool.bind(this), true); + }, + /** * Adds the keys and commands to the Toolbox Window in window mode. */ _addKeysToWindow: function TBOX__addKeysToWindow() { if (this.hostType != Toolbox.HostType.WINDOW) { return; } let doc = this.doc.defaultView.parent.document; @@ -535,16 +543,36 @@ Toolbox.prototype = { return this.loadTool(id).then((panel) => { this.emit("select", id); this.emit(id + "-selected", panel); return panel; }); }, /** + * Loads the tool next to the currently selected tool. + */ + selectNextTool: function TBOX_selectNextTool() { + let selected = this.doc.querySelector(".devtools-tab[selected]"); + let next = selected.nextSibling || selected.parentNode.firstChild; + let tool = next.getAttribute("toolid"); + return this.selectTool(tool); + }, + + /** + * Loads the tool just left to the currently selected tool. + */ + selectPreviousTool: function TBOX_selectPreviousTool() { + let selected = this.doc.querySelector(".devtools-tab[selected]"); + let previous = selected.previousSibling || selected.parentNode.lastChild; + let tool = previous.getAttribute("toolid"); + return this.selectTool(tool); + }, + + /** * Highlights the tool's tab if it is not the currently selected tool. * * @param {string} id * The id of the tool to highlight */ highlightTool: function TBOX_highlightTool(id) { let tab = this.doc.getElementById("toolbox-tab-" + id); tab && tab.classList.add("highlighted");
--- a/browser/devtools/framework/toolbox.xul +++ b/browser/devtools/framework/toolbox.xul @@ -18,16 +18,24 @@ <commandset id="editMenuCommands"/> <keyset id="editMenuKeys"/> <keyset id="toolbox-keyset"> <key id="toolbox-options-key" key="&toolboxOptionsButton.key;" oncommand="void(0);" modifiers="shift, accel"/> + <key id="toolbox-next-tool-key" + key="&toolboxNextTool.key;" + oncommand="void(0);" + modifiers="accel"/> + <key id="toolbox-previous-tool-key" + key="&toolboxPreviousTool.key;" + oncommand="void(0);" + modifiers="accel"/> </keyset> <notificationbox id="toolbox-notificationbox" flex="1"> <toolbar class="devtools-tabbar"> #ifdef XP_MACOSX <hbox id="toolbox-controls"> <toolbarbutton id="toolbox-close" class="devtools-closebutton"
--- a/browser/devtools/inspector/inspector-panel.js +++ b/browser/devtools/inspector/inspector-panel.js @@ -219,28 +219,29 @@ InspectorPanel.prototype = { */ setupSearchBox: function InspectorPanel_setupSearchBox() { let searchDoc; if (this.target.isLocalTab) { searchDoc = this.browser.contentDocument; } else if (this.target.window) { searchDoc = this.target.window.document; } else { - return; + searchDoc = null; } // Initiate the selectors search object. - let setNodeFunction = function(node) { - this.selection.setNode(node, "selectorsearch"); + let setNodeFunction = function(eventName, node) { + this.selection.setNodeFront(node, "selectorsearch"); }.bind(this); if (this.searchSuggestions) { this.searchSuggestions.destroy(); this.searchSuggestions = null; } this.searchBox = this.panelDoc.getElementById("inspector-searchbox"); - this.searchSuggestions = new SelectorSearch(searchDoc, this.searchBox, setNodeFunction); + this.searchSuggestions = new SelectorSearch(this, searchDoc, this.searchBox); + this.searchSuggestions.on("node-selected", setNodeFunction); }, /** * Build the sidebar. */ setupSidebar: function InspectorPanel_setupSidebar() { let tabbox = this.panelDoc.querySelector("#inspector-sidebar"); this.sidebar = new ToolSidebar(tabbox, this, "inspector"); @@ -630,34 +631,38 @@ InspectorPanel.prototype = { /** * Copy the innerHTML of the selected Node to the clipboard. */ copyInnerHTML: function InspectorPanel_copyInnerHTML() { if (!this.selection.isNode()) { return; } - let toCopy = this.selection.node.innerHTML; - if (toCopy) { - clipboardHelper.copyString(toCopy); - } + this._copyLongStr(this.walker.innerHTML(this.selection.nodeFront)); }, /** * Copy the outerHTML of the selected Node to the clipboard. */ copyOuterHTML: function InspectorPanel_copyOuterHTML() { if (!this.selection.isNode()) { return; } - let toCopy = this.selection.node.outerHTML; - if (toCopy) { - clipboardHelper.copyString(toCopy); - } + + this._copyLongStr(this.walker.outerHTML(this.selection.nodeFront)); + }, + + _copyLongStr: function(promise) { + return promise.then(longstr => { + return longstr.string().then(toCopy => { + longstr.release().then(null, console.error); + clipboardHelper.copyString(toCopy); + }); + }).then(null, console.error); }, /** * Copy a unique selector of the selected Node to the clipboard. */ copyUniqueSelector: function InspectorPanel_copyUniqueSelector() { if (!this.selection.isNode()) { @@ -680,18 +685,17 @@ InspectorPanel.prototype = { } // If the markup panel is active, use the markup panel to delete // the node, making this an undoable action. if (this.markup) { this.markup.deleteNode(this.selection.nodeFront); } else { // remove the node from content - let parent = this.selection.node.parentNode; - parent.removeChild(this.selection.node); + this.walker.removeNode(this.selection.nodeFront); } }, /** * Schedule a low-priority change event for things like paint * and resize. */ scheduleLayoutChange: function Inspector_scheduleLayoutChange()
--- a/browser/devtools/inspector/selector-search.js +++ b/browser/devtools/inspector/selector-search.js @@ -1,39 +1,42 @@ /* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ "use strict"; const {Cu} = require("chrome"); +const EventEmitter = require("devtools/shared/event-emitter"); +const promise = require("sdk/core/promise"); loader.lazyGetter(this, "AutocompletePopup", () => { return Cu.import("resource:///modules/devtools/AutocompletePopup.jsm", {}).AutocompletePopup; }); // Maximum number of selector suggestions shown in the panel. const MAX_SUGGESTIONS = 15; /** * Converts any input box on a page to a CSS selector search and suggestion box. * * @constructor + * @param InspectorPanel aInspector + * The InspectorPanel whose `walker` attribute should be used for + * document traversal. * @param nsIDOMDocument aContentDocument - * The content document which inspector is attached to. + * The content document which inspector is attached to, or null if + * a remote document. * @param nsiInputElement aInputNode * The input element to which the panel will be attached and from where * search input will be taken. - * @param Function aCallback - * The method to callback when a search is available. - * This method is called with the matched node as the first argument. */ -function SelectorSearch(aContentDocument, aInputNode, aCallback) { +function SelectorSearch(aInspector, aContentDocument, aInputNode) { + this.inspector = aInspector; this.doc = aContentDocument; - this.callback = aCallback; this.searchBox = aInputNode; this.panelDoc = this.searchBox.ownerDocument; // initialize variables. this._lastSearched = null; this._lastValidSearch = ""; this._lastToLastValidSearch = null; this._searchResults = null; @@ -57,22 +60,30 @@ function SelectorSearch(aContentDocument onClick: this._onListBoxKeypress, onKeypress: this._onListBoxKeypress, }; this.searchPopup = new AutocompletePopup(this.panelDoc, options); // event listeners. this.searchBox.addEventListener("command", this._onHTMLSearch, true); this.searchBox.addEventListener("keypress", this._onSearchKeypress, true); + + // For testing, we need to be able to wait for the most recent node request + // to finish. Tests can watch this promise for that. + this._lastQuery = promise.resolve(null); + + EventEmitter.decorate(this); } exports.SelectorSearch = SelectorSearch; SelectorSearch.prototype = { + get walker() this.inspector.walker, + // The possible states of the query. States: { CLASS: "class", ID: "id", TAG: "tag", }, // The current state of the query. @@ -163,101 +174,122 @@ SelectorSearch.prototype = { this.searchBox.removeEventListener("keypress", this._onSearchKeypress, true); this.searchPopup.destroy(); this.searchPopup = null; this.searchBox = null; this.doc = null; this.panelDoc = null; this._searchResults = null; this._searchSuggestions = null; - this.callback = null; + EventEmitter.decorate(this); + }, + + _selectResult: function(index) { + return this._searchResults.item(index).then(node => { + this.emit("node-selected", node); + }); }, /** * The command callback for the input box. This function is automatically * invoked as the user is typing if the input box type is search. */ _onHTMLSearch: function SelectorSearch__onHTMLSearch() { let query = this.searchBox.value; if (query == this._lastSearched) { return; } this._lastSearched = query; + this._searchResults = null; this._searchIndex = 0; if (query.length == 0) { this._lastValidSearch = ""; this.searchBox.removeAttribute("filled"); this.searchBox.classList.remove("devtools-no-search-result"); if (this.searchPopup.isOpen) { this.searchPopup.hidePopup(); } return; } this.searchBox.setAttribute("filled", true); - try { - this._searchResults = this.doc.querySelectorAll(query); - } - catch (ex) { - this._searchResults = []; - } - if (this._searchResults.length > 0) { - this._lastValidSearch = query; - // Even though the selector matched atleast one node, there is still - // possibility of suggestions. - if (query.match(/[\s>+]$/)) { - // If the query has a space or '>' at the end, create a selector to match - // the children of the selector inside the search box by adding a '*'. - this._lastValidSearch += "*"; - } - else if (query.match(/[\s>+][\.#a-zA-Z][\.#>\s+]*$/)) { - // If the query is a partial descendant selector which does not matches - // any node, remove the last incomplete part and add a '*' to match - // everything. For ex, convert 'foo > b' to 'foo > *' . - let lastPart = query.match(/[\s>+][\.#a-zA-Z][^>\s+]*$/)[0]; - this._lastValidSearch = query.slice(0, -1 * lastPart.length + 1) + "*"; + let queryList = null; + + this._lastQuery = this.walker.querySelectorAll(this.walker.rootNode, query).then(list => { + return list; + }, (err) => { + // Failures are ok here, just use a null item list; + return null; + }).then(queryList => { + // Value has changed since we started this request, we're done. + if (query != this.searchBox.value) { + if (queryList) { + queryList.release(); + } + return promise.reject(null); } - if (!query.slice(-1).match(/[\.#\s>+]/)) { - // Hide the popup if we have some matching nodes and the query is not - // ending with [.# >] which means that the selector is not at the - // beginning of a new class, tag or id. - if (this.searchPopup.isOpen) { - this.searchPopup.hidePopup(); + this._searchResults = queryList; + if (this._searchResults && this._searchResults.length > 0) { + this._lastValidSearch = query; + // Even though the selector matched atleast one node, there is still + // possibility of suggestions. + if (query.match(/[\s>+]$/)) { + // If the query has a space or '>' at the end, create a selector to match + // the children of the selector inside the search box by adding a '*'. + this._lastValidSearch += "*"; } + else if (query.match(/[\s>+][\.#a-zA-Z][\.#>\s+]*$/)) { + // If the query is a partial descendant selector which does not matches + // any node, remove the last incomplete part and add a '*' to match + // everything. For ex, convert 'foo > b' to 'foo > *' . + let lastPart = query.match(/[\s>+][\.#a-zA-Z][^>\s+]*$/)[0]; + this._lastValidSearch = query.slice(0, -1 * lastPart.length + 1) + "*"; + } + + if (!query.slice(-1).match(/[\.#\s>+]/)) { + // Hide the popup if we have some matching nodes and the query is not + // ending with [.# >] which means that the selector is not at the + // beginning of a new class, tag or id. + if (this.searchPopup.isOpen) { + this.searchPopup.hidePopup(); + } + } + else { + this.showSuggestions(); + } + this.searchBox.classList.remove("devtools-no-search-result"); + + return this._selectResult(0); } else { + if (query.match(/[\s>+]$/)) { + this._lastValidSearch = query + "*"; + } + else if (query.match(/[\s>+][\.#a-zA-Z][\.#>\s+]*$/)) { + let lastPart = query.match(/[\s+>][\.#a-zA-Z][^>\s+]*$/)[0]; + this._lastValidSearch = query.slice(0, -1 * lastPart.length + 1) + "*"; + } + this.searchBox.classList.add("devtools-no-search-result"); this.showSuggestions(); } - this.searchBox.classList.remove("devtools-no-search-result"); - this.callback(this._searchResults[0]); - } - else { - if (query.match(/[\s>+]$/)) { - this._lastValidSearch = query + "*"; - } - else if (query.match(/[\s>+][\.#a-zA-Z][\.#>\s+]*$/)) { - let lastPart = query.match(/[\s+>][\.#a-zA-Z][^>\s+]*$/)[0]; - this._lastValidSearch = query.slice(0, -1 * lastPart.length + 1) + "*"; - } - this.searchBox.classList.add("devtools-no-search-result"); - this.showSuggestions(); - } + return undefined; + }); }, /** * Handles keypresses inside the input box. */ _onSearchKeypress: function SelectorSearch__onSearchKeypress(aEvent) { let query = this.searchBox.value; switch(aEvent.keyCode) { case aEvent.DOM_VK_ENTER: case aEvent.DOM_VK_RETURN: - if (query == this._lastSearched) { + if (query == this._lastSearched && this._searchResults) { this._searchIndex = (this._searchIndex + 1) % this._searchResults.length; } else { this._onHTMLSearch(); return; } break; @@ -310,18 +342,18 @@ SelectorSearch.prototype = { return; default: return; } aEvent.preventDefault(); aEvent.stopPropagation(); - if (this._searchResults.length > 0) { - this.callback(this._searchResults[this._searchIndex]); + if (this._searchResults && this._searchResults.length > 0) { + this._lastQuery = this._selectResult(this._searchIndex); } }, /** * Handles keypress and mouse click on the suggestions richlistbox. */ _onListBoxKeypress: function SelectorSearch__onListBoxKeypress(aEvent) { switch(aEvent.keyCode || aEvent.button) { @@ -437,16 +469,19 @@ SelectorSearch.prototype = { } }, /** * Suggests classes,ids and tags based on the user input as user types in the * searchbox. */ showSuggestions: function SelectorSearch_showSuggestions() { + if (!this.walker.isLocal()) { + return; + } let query = this.searchBox.value; if (this._lastValidSearch != "" && this._lastToLastValidSearch != this._lastValidSearch) { this._searchSuggestions = { ids: new Map(), classes: new Map(), tags: new Map(), };
--- a/browser/devtools/inspector/test/browser_inspector_bug_650804_search.js +++ b/browser/devtools/inspector/test/browser_inspector_bug_650804_search.js @@ -63,16 +63,17 @@ function test() { openInspector(startTest); } function startTest(aInspector) { inspector = aInspector; inspector.selection.setNode($("b1")); + searchBox = inspector.panelWin.document.getElementById("inspector-searchbox"); focusSearchBoxUsingShortcut(inspector.panelWin, function() { searchBox.addEventListener("command", checkState, true); searchBox.addEventListener("keypress", checkState, true); checkStateAndMoveOn(0); }); @@ -90,25 +91,28 @@ function test() info("pressing key " + key + " to get id " + id); EventUtils.synthesizeKey(key, {}, inspector.panelWin); } function checkState(event) { if (event.type == "keypress" && keypressStates.indexOf(state) == -1) { return; } - executeSoon(function() { - let [key, id, isValid] = keyStates[state]; - info(inspector.selection.node.id + " is selected with text " + - inspector.searchBox.value); - is(inspector.selection.node, $(id), - "Correct node is selected for state " + state); - is(!searchBox.classList.contains("devtools-no-search-result"), isValid, - "Correct searchbox result state for state " + state); - checkStateAndMoveOn(state + 1); + + inspector.searchSuggestions._lastQuery.then(() => { + executeSoon(() => { + let [key, id, isValid] = keyStates[state]; + info(inspector.selection.node.id + " is selected with text " + + inspector.searchBox.value); + is(inspector.selection.node, $(id), + "Correct node is selected for state " + state); + is(!searchBox.classList.contains("devtools-no-search-result"), isValid, + "Correct searchbox result state for state " + state); + checkStateAndMoveOn(state + 1); + }); }); } function finishUp() { searchBox = null; gBrowser.removeCurrentTab(); finish(); }
--- a/browser/devtools/inspector/test/browser_inspector_bug_831693_combinator_suggestions.js +++ b/browser/devtools/inspector/test/browser_inspector_bug_831693_combinator_suggestions.js @@ -55,16 +55,17 @@ function test() function setupTest() { openInspector(startTest); } function startTest(aInspector) { inspector = aInspector; + searchBox = inspector.panelWin.document.getElementById("inspector-searchbox"); popup = inspector.searchSuggestions.searchPopup; focusSearchBoxUsingShortcut(inspector.panelWin, function() { searchBox.addEventListener("command", checkState, true); checkStateAndMoveOn(0); }); @@ -80,17 +81,17 @@ function test() state = index; info("pressing key " + key + " to get suggestions " + JSON.stringify(suggestions)); EventUtils.synthesizeKey(key, {}, inspector.panelWin); } function checkState(event) { - executeSoon(function() { + inspector.searchSuggestions._lastQuery.then(() => { let [key, suggestions] = keyStates[state]; let actualSuggestions = popup.getItems(); is(popup._panel.state == "open" || popup._panel.state == "showing" ? actualSuggestions.length: 0, suggestions.length, "There are expected number of suggestions at " + state + "th step."); actualSuggestions = actualSuggestions.reverse(); for (let i = 0; i < suggestions.length; i++) { is(suggestions[i][0], actualSuggestions[i].label,
--- a/browser/devtools/inspector/test/browser_inspector_bug_831693_input_suggestion.js +++ b/browser/devtools/inspector/test/browser_inspector_bug_831693_input_suggestion.js @@ -82,17 +82,17 @@ function test() state = index; info("pressing key " + key + " to get suggestions " + JSON.stringify(suggestions)); EventUtils.synthesizeKey(key, {}, inspector.panelWin); } function checkState(event) { - executeSoon(function() { + inspector.searchSuggestions._lastQuery.then(() => { let [key, suggestions] = keyStates[state]; let actualSuggestions = popup.getItems(); is(popup._panel.state == "open" || popup._panel.state == "showing" ? actualSuggestions.length: 0, suggestions.length, "There are expected number of suggestions at " + state + "th step."); actualSuggestions = actualSuggestions.reverse(); for (let i = 0; i < suggestions.length; i++) { is(suggestions[i][0], actualSuggestions[i].label,
--- a/browser/devtools/inspector/test/browser_inspector_menu.js +++ b/browser/devtools/inspector/test/browser_inspector_menu.js @@ -98,17 +98,17 @@ function test() { function() { copyUniqueSelector.doCommand(); }, testDeleteNode, testDeleteNode); } function testDeleteNode() { let deleteNode = inspector.panelDoc.getElementById("node-menu-delete"); ok(deleteNode, "the popup menu has a delete menu item"); - inspector.selection.once("detached", deleteTest); + inspector.once("markupmutation", deleteTest); let commandEvent = document.createEvent("XULCommandEvent"); commandEvent.initCommandEvent("command", true, true, window, 0, false, false, false, false, null); deleteNode.dispatchEvent(commandEvent); } function deleteTest() {
--- a/browser/devtools/markupview/markup-view.js +++ b/browser/devtools/markupview/markup-view.js @@ -225,38 +225,38 @@ MarkupView.prototype = { }, /** * Delete a node from the DOM. * This is an undoable action. */ deleteNode: function MC__deleteNode(aNode) { - aNode = aNode.rawNode(); - if (!aNode) { - return; - } - let doc = nodeDocument(aNode); - if (aNode === doc || - aNode === doc.documentElement || + if (aNode.isDocumentElement || aNode.nodeType == Ci.nsIDOMNode.DOCUMENT_TYPE_NODE) { return; } - let parentNode = aNode.parentNode; - let sibling = aNode.nextSibling; + let container = this._containers.get(aNode); - this.undo.do(function() { - if (aNode.selected) { - this.navigate(this._containers.get(parentNode)); - } - parentNode.removeChild(aNode); - }.bind(this), function() { - parentNode.insertBefore(aNode, sibling); - }); + // Retain the node so we can undo this... + this.walker.retainNode(aNode).then(() => { + let parent = aNode.parentNode(); + let sibling = null; + this.undo.do(() => { + if (container.selected) { + this.navigate(this._containers.get(parent)); + } + this.walker.removeNode(aNode).then(nextSibling => { + sibling = nextSibling; + }); + }, () => { + this.walker.insertBefore(aNode, parent, sibling); + }); + }).then(null, console.error); }, /** * If an editable item is focused, select its container. */ _onFocus: function MC__onFocus(aEvent) { let parent = aEvent.target; while (!parent.container) { @@ -588,16 +588,19 @@ MarkupView.prototype = { // centered node. let centered = this._checkSelectionVisible(aContainer); // Children aren't updated yet, but clear the childrenDirty flag anyway. // If the dirty flag is re-set while we're fetching we'll need to fetch // again. aContainer.childrenDirty = false; let updatePromise = this._getVisibleChildren(aContainer, centered).then(children => { + if (!this._containers) { + return promise.reject("markup view destroyed"); + } this._queuedChildUpdates.delete(aContainer); // If children are dirty, we got a change notification for this node // while the request was in progress, we need to do it again. if (aContainer.childrenDirty) { return this._updateChildren(aContainer, centered); }
--- a/browser/locales/en-US/chrome/browser/devtools/gcli.properties +++ b/browser/locales/en-US/chrome/browser/devtools/gcli.properties @@ -6,514 +6,325 @@ # command line which is available from the Web Developer sub-menu # -> 'Web Console'. # The correct localization of this file might be to keep it in # English, or another language commonly spoken among web developers. # You want to make that choice consistent across the developer tools. # A good criteria is the language in which you'd find the best # documentation on web development on the web. -# LOCALIZATION NOTE (canonDescNone): Short string used to describe any command -# or command parameter when no description has been provided. +# For each command there are in general two strings. As an example consider +# the 'pref' command. +# commandDesc (e.g. prefDesc for the command 'pref'): this string contains a +# very short description of the command. It's designed to be shown in a menu +# alongside the command name, which is why it should be as short as possible. +# commandManual (e.g. prefManual for the command 'pref'): this string will +# contain a fuller description of the command. It's diplayed when the user +# asks for help about a specific command (e.g. 'help pref'). + +# LOCALIZATION NOTE: This message is used to describe any command or command +# parameter when no description has been provided. canonDescNone=(No description) -# LOCALIZATION NOTE (canonDefaultGroupName): The default name for a group of -# parameters. +# LOCALIZATION NOTE: The default name for a group of parameters. canonDefaultGroupName=Options -# LOCALIZATION NOTE (canonProxyDesc): A very short description of a set of -# remote commands. This string is designed to be shown in a menu alongside the -# command name, which is why it should be as short as possible. See -# canonProxyManual for a fuller description of what it does. +# LOCALIZATION NOTE (canonProxyDesc, canonProxyManual): These commands are +# used to execute commands on a remote system (using a proxy). Parameters: %S +# is the name of the remote system. canonProxyDesc=Execute a command on %S - -# LOCALIZATION NOTE (canonProxyManual): A fuller description of a set of -# remote commands. Displayed when the user asks for help on what it does. canonProxyManual=A set of commands that are executed on a remote system. The remote system is reached via %S -# LOCALIZATION NOTE (canonProxyExists): An error message displayed when we try -# to add new command (via a proxy) where one already exists in that name. +# LOCALIZATION NOTE: This error message is displayed when we try to add a new +# command (using a proxy) where one already exists with the same name. canonProxyExists=There is already a command called '%S' -# LOCALIZATION NOTE (cliEvalJavascript): The special '{' command allows entry -# of JavaScript like traditional developer tool command lines. This describes -# the '{' command. +# LOCALIZATION NOTE: This message describes the '{' command, which allows +# entry of JavaScript like traditional developer tool command lines. cliEvalJavascript=Enter JavaScript directly -# LOCALIZATION NOTE (cliUnusedArg): When the command line has more arguments -# than the current command can understand this is the error message shown to -# the user. +# LOCALIZATION NOTE: This message is displayed when the command line has more +# arguments than the current command can understand. cliUnusedArg=Too many arguments -# LOCALIZATION NOTE (cliOptions): The title of the dialog which displays the -# options that are available to the current command. +# LOCALIZATION NOTE: The title of the dialog which displays the options that +# are available to the current command. cliOptions=Available Options -# LOCALIZATION NOTE (fieldSelectionSelect): When a command has a parameter -# that has a number of pre-defined options the user interface presents these -# in a drop-down menu, where the first 'option' is an indicator that a -# selection should be made. This string describes that first option. +# LOCALIZATION NOTE: Error message given when a file argument points to a file +# that does not exist, but should (e.g. for use with File->Open) %1$S is a +# filename +fileErrNotExists='%1$S' doesn't exist + +# LOCALIZATION NOTE: Error message given when a file argument points to a file +# that exists, but should not (e.g. for use with File->Save As) %1$S is a +# filename +fileErrExists='%1$S' already exists + +# LOCALIZATION NOTE: Error message given when a file argument points to a +# non-file, when a file is needed. %1$S is a filename +fileErrIsNotFile='%1$S' is not a file + +# LOCALIZATION NOTE: Error message given when a file argument points to a +# non-directory, when a directory is needed (e.g. for use with 'cd') %1$S is a +# filename +fileErrIsNotDirectory='%1$S' is not a directory + +# LOCALIZATION NOTE: Error message given when a file argument does not match +# the specified regular expression %1$S is a filename %2$S is a regular +# expression +fileErrDoesntMatch='%1$S' does not match '%2$S' + +# LOCALIZATION NOTE: When a command has a parameter that has a number of +# pre-defined options the user interface presents these in a drop-down menu, +# where the first 'option' is an indicator that a selection should be made. +# This string describes that first option. fieldSelectionSelect=Select a %S… -# LOCALIZATION NOTE (fieldArrayAdd): When a command has a parameter that can -# be repeated a number of times (e.g. like the 'cat a.txt b.txt' command) the -# user interface presents buttons to add and remove arguments. This string is -# used to add arguments. +# LOCALIZATION NOTE (fieldArrayAdd, fieldArrayDel): When a command has a +# parameter that can be repeated multiple times (e.g. like the 'cat a.txt +# b.txt' command) the user interface presents buttons to add and remove +# arguments. This string is used to add arguments. fieldArrayAdd=Add - -# LOCALIZATION NOTE (fieldArrayDel): When a command has a parameter that can -# be repeated a number of times (e.g. like the 'cat a.txt b.txt' command) the -# user interface presents buttons to add and remove arguments. This string is -# used to remove arguments. fieldArrayDel=Delete -# LOCALIZATION NOTE (fieldMenuMore): When the menu has displayed all the -# matches that it should (i.e. about 10 items) then we display this to alert -# the user that more matches are available. +# LOCALIZATION NOTE: When the menu has displayed all the matches that it +# should (i.e. about 10 items) then we display this to alert the user that +# more matches are available. fieldMenuMore=More matches, keep typing -# LOCALIZATION NOTE (jstypeParseScope): The command line provides completion -# for JavaScript commands, however there are times when the scope of what -# we're completing against can't be used. This error message is displayed when -# this happens. +# LOCALIZATION NOTE: The command line provides completion for JavaScript +# commands, however there are times when the scope of what we're completing +# against can't be used. This error message is displayed when this happens. jstypeParseScope=Scope lost -# LOCALIZATION NOTE (jstypeParseMissing): When the command line is doing -# JavaScript completion, sometimes the property to be completed does not -# exist. This error message is displayed when this happens. +# LOCALIZATION NOTE (jstypeParseMissing, jstypeBeginSyntax, +# jstypeBeginUnterm): These error messages are displayed when the command line +# is doing JavaScript completion and encounters errors. jstypeParseMissing=Can't find property '%S' - -# LOCALIZATION NOTE (jstypeBeginSyntax): When the command line is doing -# JavaScript completion using invalid JavaScript, this error message is -# displayed. jstypeBeginSyntax=Syntax error - -# LOCALIZATION NOTE (jstypeBeginUnterm): When the command line is doing -# JavaScript completion using a string that is not properly terminated, this -# error message is displayed. jstypeBeginUnterm=Unterminated string literal -# LOCALIZATION NOTE (jstypeParseError): If the system for providing JavaScript -# completions encounters and error it displays this. +# LOCALIZATION NOTE: This message is displayed if the system for providing +# JavaScript completions encounters and error it displays this. jstypeParseError=Error -# LOCALIZATION NOTE (typesNumberNan): When the command line is passed a -# number, however the input string is not a valid number, this error message -# is displayed. +# LOCALIZATION NOTE (typesNumberNan, typesNumberNotInt2, typesDateNan): These +# error messages are displayed when the command line is passed a variable +# which has the wrong format and can't be converted. Parameters: %S is the +# passed variable. typesNumberNan=Can't convert "%S" to a number. - -# LOCALIZATION NOTE (typesNumberMax): When the command line is passed a -# number, but the number is bigger than the largest allowed number, this error -# message is displayed. -typesNumberMax=%1$S is greater than maximum allowed: %2$S. - -# LOCALIZATION NOTE (typesNumberMin): When the command line is passed a -# number, but the number is lower than the smallest allowed number, this error -# message is displayed. -typesNumberMin=%1$S is smaller than minimum allowed: %2$S. - -# LOCALIZATION NOTE (typesNumberNotInt2): When the command line is passed a -# number, but the number has a decimal part and floats are not allowed. typesNumberNotInt2=Can't convert "%S" to an integer. - -# LOCALIZATION NOTE (typesDateNan): When the command line is passed a date, -# however the input string is not a valid date, this error message is -# displayed. typesDateNan=Can't convert "%S" to a date. -# LOCALIZATION NOTE (typesDateMax): When the command line is passed a date, -# but the number is later than the latest allowed date, this error message is -# displayed. +# LOCALIZATION NOTE (typesNumberMax, typesNumberMin, typesDateMax, +# typesDateMin): These error messages are displayed when the command line is +# passed a variable which has a value out of range (number or date). +# Parameters: %1$S is the passed variable, %2$S is the limit value. +typesNumberMax=%1$S is greater than maximum allowed: %2$S. +typesNumberMin=%1$S is smaller than minimum allowed: %2$S. typesDateMax=%1$S is later than maximum allowed: %2$S. - -# LOCALIZATION NOTE (typesDateMin): When the command line is passed a date, -# but the date is earlier than the earliest allowed number, this error message -# is displayed. typesDateMin=%1$S is earlier than minimum allowed: %2$S. -# LOCALIZATION NOTE (typesSelectionNomatch): When the command line is passed -# an option with a limited number of correct values, but the passed value is -# not one of them, this error message is displayed. +# LOCALIZATION NOTE: This error message is displayed when the command line is +# passed an option with a limited number of correct values, but the passed +# value is not one of them. typesSelectionNomatch=Can't use '%S'. -# LOCALIZATION NOTE (nodeParseSyntax): When the command line is expecting a -# CSS query string, however the passed string is not valid, this error message -# is displayed. +# LOCALIZATION NOTE: This error message is displayed when the command line is +# expecting a CSS query string, however the passed string is not valid. nodeParseSyntax=Syntax error in CSS query -# LOCALIZATION NOTE (nodeParseMultiple): When the command line is expecting a -# CSS string that matches a single node, but more than one node matches, this -# error message is displayed. +# LOCALIZATION NOTE (nodeParseMultiple, nodeParseNone): These error messages +# are displayed when the command line is expecting a CSS string that matches a +# single node, but more nodes (or none) match. nodeParseMultiple=Too many matches (%S) - -# LOCALIZATION NOTE (nodeParseNone): When the command line is expecting a CSS -# string that matches a single node, but no nodes match, this error message is -# displayed. nodeParseNone=No matches -# LOCALIZATION NOTE (helpDesc): A very short description of the 'help' -# command. This string is designed to be shown in a menu alongside the command -# name, which is why it should be as short as possible. See helpManual for a -# fuller description of what it does. +# LOCALIZATION NOTE (helpDesc, helpManual, helpSearchDesc, helpSearchManual3): +# These strings describe the "help" command, used to display a description of +# a command (e.g. "help pref"), and its parameter 'search'. helpDesc=Get help on the available commands - -# LOCALIZATION NOTE (helpManual): A fuller description of the 'help' command. -# Displayed when the user asks for help on what it does. helpManual=Provide help either on a specific command (if a search string is provided and an exact match is found) or on the available commands (if a search string is not provided, or if no exact match is found). - -# LOCALIZATION NOTE (helpSearchDesc): A very short description of the 'search' -# parameter to the 'help' command. See helpSearchManual3 for a fuller -# description of what it does. This string is designed to be shown in a dialog -# with restricted space, which is why it should be as short as possible. helpSearchDesc=Search string - -# LOCALIZATION NOTE (helpSearchManual3): A fuller description of the 'search' -# parameter to the 'help' command. Displayed when the user asks for help on -# what it does. helpSearchManual3=search string to use in narrowing down the displayed commands. Regular expressions not supported. -# LOCALIZATION NOTE (helpManSynopsis): A heading shown at the top of a help -# page for a command in the console It labels a summary of the parameters to -# the command +# LOCALIZATION NOTE (helpManSynopsis, helpManDescription, helpManParameters): +# These strings are displayed in the help page for a command in the console. helpManSynopsis=Synopsis - -# LOCALIZATION NOTE (helpManDescription): A heading shown in a help page for a -# command in the console. This heading precedes the top level description. helpManDescription=Description - -# LOCALIZATION NOTE (helpManParameters): A heading shown above the parameters -# in a help page for a command in the console. helpManParameters=Parameters -# LOCALIZATION NOTE (helpManNone): Some text shown under the parameters -# heading in a help page for a command which has no parameters. +# LOCALIZATION NOTE: This message is displayed in the help page if the command +# has no parameters. helpManNone=None -# LOCALIZATION NOTE (helpListAll): The heading shown in response to the 'help' +# LOCALIZATION NOTE: This message is displayed in response to the 'help' # command when used without a filter, just above the list of known commands. helpListAll=Available Commands: -# LOCALIZATION NOTE (helpListPrefix): The heading shown in response to the -# 'help <search>' command (i.e. with a search string), just above the list of -# matching commands. -helpListPrefix=Commands starting with '%1$S': - -# LOCALIZATION NOTE (helpListNone): The heading shown in response to the 'help -# <search>' command (i.e. with a search string), when there are no matching -# commands. -helpListNone=No commands starting with '%1$S' +# LOCALIZATION NOTE (helpListPrefix, helpListNone): These messages are +# displayed in response to the 'help <search>' command (i.e. with a search +# string), just above the list of matching commands. Parameters: %S is the +# search string. +helpListPrefix=Commands starting with '%S': +helpListNone=No commands starting with '%S' -# LOCALIZATION NOTE (helpManRequired): When the 'help x' command wants to show -# the manual for the 'x' command it needs to be able to describe the -# parameters as either required or optional, or if they have a default value. -# See also 'helpManOptional' and 'helpManDefault'. +# LOCALIZATION NOTE (helpManRequired, helpManOptional, helpManDefault): When +# the 'help x' command wants to show the manual for the 'x' command, it needs +# to be able to describe the parameters as either required or optional, or if +# they have a default value. helpManRequired=required - -# LOCALIZATION NOTE (helpManOptional): See description of 'helpManRequired' helpManOptional=optional +helpManDefault=optional, default=%S -# LOCALIZATION NOTE (helpManDefault): See description of 'helpManRequired'. %1$ -# S is the default value -helpManDefault=optional, default=%1$S - -# LOCALIZATION NOTE (subCommands): Text shown as part of the output of the -# 'help' command when the command in question has sub-commands, before a list -# of the matching sub-commands +# LOCALIZATION NOTE: Text shown as part of the output of the 'help' command +# when the command in question has sub-commands, before a list of the matching +# sub-commands. subCommands=Sub-Commands -# LOCALIZATION NOTE (subCommandsNone): Text shown as part of the output of the -# 'help' command when the command in question should have sub-commands but in -# fact has none +# LOCALIZATION NOTE: Text shown as part of the output of the 'help' command +# when the command in question should have sub-commands but in fact has none. subCommandsNone=None -# LOCALIZATION NOTE (contextDesc): A very short description of the 'context' -# command. This string is designed to be shown in a menu alongside the command -# name, which is why it should be as short as possible. See contextManual for -# a fuller description of what it does. +# LOCALIZATION NOTE (contextDesc, contextManual, contextPrefixDesc): These +# strings are used to describe the 'context' command and its 'prefix' +# parameter. See localization comment for 'connect' for an explanation about +# 'prefix'. contextDesc=Concentrate on a group of commands - -# LOCALIZATION NOTE (contextManual): A fuller description of the 'context' -# command. Displayed when the user asks for help on what it does. contextManual=Setup a default prefix to future commands. For example 'context git' would allow you to type 'commit' rather than 'git commit'. - -# LOCALIZATION NOTE (contextPrefixDesc): A short description of the 'prefix' -# parameter to the 'context' command. This string is designed to be shown in a -# dialog with restricted space, which is why it should be as short as -# possible. contextPrefixDesc=The command prefix -# LOCALIZATION NOTE (contextNotParentError): An error message displayed during -# the processing of the 'context' command, when the found command is not a -# parent command. -contextNotParentError=Can't use '%1$S' as a prefix because it is not a parent command. +# LOCALIZATION NOTE: This message message displayed during the processing of +# the 'context' command, when the found command is not a parent command. +contextNotParentError=Can't use '%S' as a prefix because it is not a parent command. -# LOCALIZATION NOTE (contextReply): A message displayed during the processing -# of the 'context' command, to indicate success. -contextReply=Using %1$S as a command prefix - -# LOCALIZATION NOTE (contextEmptyReply): A message displayed during the -# processing of the 'context' command, to indicate that there is no command -# prefix +# LOCALIZATION NOTE (contextReply, contextEmptyReply): These messages are +# displayed during the processing of the 'context' command, to indicate +# success or that there is no command prefix. +contextReply=Using %S as a command prefix contextEmptyReply=Command prefix is unset -# LOCALIZATION NOTE (connectDesc): A very short description of the 'connect' -# command. This string is designed to be shown in a menu alongside the command -# name, which is why it should be as short as possible. See connectManual for -# a fuller description of what it does. +# LOCALIZATION NOTE (connectDesc, connectManual, connectPrefixDesc, +# connectPortDesc, connectHostDesc, connectDupReply): These strings describe +# the 'connect' command and all its available parameters. A 'prefix' is an +# alias for the remote server (think of it as a "connection name"), and it +# allows to identify a specific server when connected to multiple remote +# servers. connectDesc=Proxy commands to server - -# LOCALIZATION NOTE (connectManual): A fuller description of the 'connect' -# command. Displayed when the user asks for help on what it does. connectManual=Connect to the server, creating local versions of the commands on the server. Remote commands initially have a prefix to distinguish them from local commands (but see the context command to get past this) - -# LOCALIZATION NOTE (connectPrefixDesc): A short description of the 'prefix' -# parameter to the 'connect' command. This string is designed to be shown in a -# dialog with restricted space, which is why it should be as short as -# possible. connectPrefixDesc=Parent prefix for imported commands - -# LOCALIZATION NOTE (connectPortDesc): A short description of the 'port' -# parameter to the 'connect' command. This string is designed to be shown in a -# dialog with restricted space, which is why it should be as short as -# possible. connectPortDesc=The TCP port to listen on - -# LOCALIZATION NOTE (connectHostDesc): A short description of the 'host' -# parameter to the 'connect' command. This string is designed to be shown in a -# dialog with restricted space, which is why it should be as short as -# possible. connectHostDesc=The hostname to bind to - -# LOCALIZATION NOTE (connectDupReply): An error condition from executing the -# 'connect' command connectDupReply=Connection called %S already exists. -# LOCALIZATION NOTE (connectReply): The output of the 'connect' command, -# telling the user what it has done. +# LOCALIZATION NOTE: The output of the 'connect' command, telling the user +# what it has done. Parameters: %S is the prefix command. See localization +# comment for 'connect' for an explanation about 'prefix'. connectReply=Added %S commands. -# LOCALIZATION NOTE (disconnectDesc): A very short description of the -# 'disconnect' command. This string is designed to be shown in a menu -# alongside the command name, which is why it should be as short as possible. -# See connectManual for a fuller description of what it does. -disconnectDesc=Proxy commands to server - -# LOCALIZATION NOTE (disconnectManual): A fuller description of the -# 'disconnect' command. Displayed when the user asks for help on what it does. -disconnectManual=Connect to the server, creating local versions of the commands on the server. Remote commands initially have a prefix to distinguish them from local commands (but see the context command to get past this) - -# LOCALIZATION NOTE (disconnectPrefixDesc): A short description of the -# 'prefix' parameter to the 'disconnect' command. This string is designed to -# be shown in a dialog with restricted space, which is why it should be as -# short as possible. +# LOCALIZATION NOTE (disconnectDesc2, disconnectManual2, disconnectPrefixDesc, +# disconnectForceDesc): These strings describe the 'disconnect' command and +# all its available parameters. See localization comment for 'connect' for an +# explanation about 'prefix'. +disconnectDesc2=Disconnect from server +disconnectManual2=Disconnect from a server currently connected for remote commands execution disconnectPrefixDesc=Parent prefix for imported commands - -# LOCALIZATION NOTE (disconnectForceDesc): A short description of the 'force' -# parameter to the 'disconnect' command. This string is designed to be shown -# in a dialog with restricted space, which is why it should be as short as -# possible. disconnectForceDesc=Ignore outstanding requests -# LOCALIZATION NOTE (disconnectReply): The output of the 'disconnect' command, -# telling the user what it's done. +# LOCALIZATION NOTE: This is the output of the 'disconnect' command, +# explaining the user what has been done. Parameters: %S is the number of +# commands removed. disconnectReply=Removed %S commands. -# LOCALIZATION NOTE (disconnectOutstanding): An error message displayed when -# the user attempts to disconnect before all requests have completed. %1$S is -# a list of commands which are incomplete -disconnectOutstanding=Outstanding requests (%1$S) - -# LOCALIZATION NOTE (prefDesc): A very short description of the 'pref' -# command. This string is designed to be shown in a menu alongside the command -# name, which is why it should be as short as possible. See prefManual for a -# fuller description of what it does. -prefDesc=Commands to control settings - -# LOCALIZATION NOTE (prefManual): A fuller description of the 'pref' command. -# Displayed when the user asks for help on what it does. -prefManual=Commands to display and alter preferences both for GCLI and the surrounding environment - -# LOCALIZATION NOTE (prefListDesc): A very short description of the 'pref -# list' command. This string is designed to be shown in a menu alongside the -# command name, which is why it should be as short as possible. See -# prefListManual for a fuller description of what it does. -prefListDesc=Display available settings - -# LOCALIZATION NOTE (prefListManual): A fuller description of the 'pref list' -# command. Displayed when the user asks for help on what it does. -prefListManual=Display a list of preferences, optionally filtered when using the 'search' parameter +# LOCALIZATION NOTE: This error message is displayed when the user attempts to +# disconnect before all requests have completed. Parameters: %S is a list of +# incomplete requests. +disconnectOutstanding=Outstanding requests (%S) -# LOCALIZATION NOTE (prefListSearchDesc): A short description of the 'search' -# parameter to the 'pref list' command. See prefListSearchManual for a fuller -# description of what it does. This string is designed to be shown in a dialog -# with restricted space, which is why it should be as short as possible. +# LOCALIZATION NOTE (prefDesc, prefManual, prefListDesc, prefListManual, +# prefListSearchDesc, prefListSearchManual, prefShowDesc, prefShowManual, +# prefShowSettingDesc, prefShowSettingManual): These strings describe the +# 'pref' command and all its available sub-commands and parameters. +prefDesc=Commands to control settings +prefManual=Commands to display and alter preferences both for GCLI and the surrounding environment +prefListDesc=Display available settings +prefListManual=Display a list of preferences, optionally filtered when using the 'search' parameter prefListSearchDesc=Filter the list of settings displayed - -# LOCALIZATION NOTE (prefListSearchManual): A fuller description of the -# 'search' parameter to the 'pref list' command. Displayed when the user asks -# for help on what it does. prefListSearchManual=Search for the given string in the list of available preferences - -# LOCALIZATION NOTE (prefShowDesc): A very short description of the 'pref -# show' command. This string is designed to be shown in a menu alongside the -# command name, which is why it should be as short as possible. See -# prefShowManual for a fuller description of what it does. prefShowDesc=Display setting value - -# LOCALIZATION NOTE (prefShowManual): A fuller description of the 'pref show' -# command. Displayed when the user asks for help on what it does. prefShowManual=Display the value of a given preference - -# LOCALIZATION NOTE (prefShowSettingDesc): A short description of the -# 'setting' parameter to the 'pref show' command. See prefShowSettingManual -# for a fuller description of what it does. This string is designed to be -# shown in a dialog with restricted space, which is why it should be as short -# as possible. prefShowSettingDesc=Setting to display - -# LOCALIZATION NOTE (prefShowSettingManual): A fuller description of the -# 'setting' parameter to the 'pref show' command. Displayed when the user asks -# for help on what it does. prefShowSettingManual=The name of the setting to display -# LOCALIZATION NOTE (prefShowSettingValue): This is used to show the -# preference name and the associated preference value. %1$S is replaced with -# the preference name and %2$S is replaced with the preference value. +# LOCALIZATION NOTE: This message is used to show the preference name and the +# associated preference value. Parameters: %1$S is the preference name, %2$S +# is the preference value. prefShowSettingValue=%1$S: %2$S -# LOCALIZATION NOTE (prefSetDesc): A very short description of the 'pref set' -# command. This string is designed to be shown in a menu alongside the command -# name, which is why it should be as short as possible. See prefSetManual for -# a fuller description of what it does. +# LOCALIZATION NOTE (prefSetDesc, prefSetManual, prefSetSettingDesc, +# prefSetSettingManual, prefSetValueDesc, prefSetValueManual): These strings +# describe the 'pref set' command and all its parameters. prefSetDesc=Alter a setting - -# LOCALIZATION NOTE (prefSetManual): A fuller description of the 'pref set' -# command. Displayed when the user asks for help on what it does. prefSetManual=Alter preferences defined by the environment - -# LOCALIZATION NOTE (prefSetSettingDesc): A short description of the 'setting' -# parameter to the 'pref set' command. See prefSetSettingManual for a fuller -# description of what it does. This string is designed to be shown in a dialog -# with restricted space, which is why it should be as short as possible. prefSetSettingDesc=Setting to alter - -# LOCALIZATION NOTE (prefSetSettingManual): A fuller description of the -# 'setting' parameter to the 'pref set' command. Displayed when the user asks -# for help on what it does. prefSetSettingManual=The name of the setting to alter. - -# LOCALIZATION NOTE (prefSetValueDesc): A short description of the 'value' -# parameter to the 'pref set' command. See prefSetValueManual for a fuller -# description of what it does. This string is designed to be shown in a dialog -# with restricted space, which is why it should be as short as possible. prefSetValueDesc=New value for setting - -# LOCALIZATION NOTE (prefSetValueManual): A fuller description of the 'value' -# parameter to the 'pref set' command. Displayed when the user asks for help -# on what it does. prefSetValueManual=The new value for the specified setting -# LOCALIZATION NOTE (prefSetCheckHeading): Title displayed to the user the -# first time they try to alter a setting This is displayed directly above -# prefSetCheckBody and prefSetCheckGo. +# LOCALIZATION NOTE (prefSetCheckHeading, prefSetCheckBody, prefSetCheckGo): +# These strings are displayed to the user the first time they try to alter a +# setting. prefSetCheckHeading=This might void your warranty! - -# LOCALIZATION NOTE (prefSetCheckBody): The main text of the warning displayed -# to the user the first time they try to alter a setting. See also -# prefSetCheckHeading and prefSetCheckGo. prefSetCheckBody=Changing these advanced settings can be harmful to the stability, security, and performance of this application. You should only continue if you are sure of what you are doing. - -# LOCALIZATION NOTE (prefSetCheckGo): The text to enable preference editing. -# Displayed in a button directly under prefSetCheckHeading and -# prefSetCheckBody prefSetCheckGo=I'll be careful, I promise! -# LOCALIZATION NOTE (prefResetDesc): A very short description of the 'pref -# reset' command. This string is designed to be shown in a menu alongside the -# command name, which is why it should be as short as possible. See -# prefResetManual for a fuller description of what it does. +# LOCALIZATION NOTE (prefResetDesc, prefResetManual, prefResetSettingDesc, +# prefResetSettingManual): These strings describe the 'pref reset' command and +# all its parameters. prefResetDesc=Reset a setting - -# LOCALIZATION NOTE (prefResetManual): A fuller description of the 'pref -# reset' command. Displayed when the user asks for help on what it does. prefResetManual=Reset the value of a setting to the system defaults - -# LOCALIZATION NOTE (prefResetSettingDesc): A short description of the -# 'setting' parameter to the 'pref reset' command. See prefResetSettingManual -# for a fuller description of what it does. This string is designed to be -# shown in a dialog with restricted space, which is why it should be as short -# as possible. prefResetSettingDesc=Setting to reset - -# LOCALIZATION NOTE (prefResetSettingManual): A fuller description of the -# 'setting' parameter to the 'pref reset' command. Displayed when the user -# asks for help on what it does. prefResetSettingManual=The name of the setting to reset to the system default value -# LOCALIZATION NOTE (prefOutputFilter): Displayed in the output from the 'pref +# LOCALIZATION NOTE: This string is displayed in the output from the 'pref # list' command as a label to an input element that allows the user to filter -# the results +# the results. prefOutputFilter=Filter -# LOCALIZATION NOTE (prefOutputName): Displayed in the output from the 'pref -# list' command as a heading to a table. The column contains the names of the -# available preferences +# LOCALIZATION NOTE (prefOutputName, prefOutputValue): These strings are +# displayed in the output from the 'pref list' command as table headings. prefOutputName=Name - -# LOCALIZATION NOTE (prefOutputValue): Displayed in the output from the 'pref -# list' command as a heading to a table. The column contains the values of the -# available preferences prefOutputValue=Value -# LOCALIZATION NOTE (introDesc): A very short description of the 'intro' -# command. This string is designed to be shown in a menu alongside the command -# name, which is why it should be as short as possible. See introManual for a -# fuller description of what it does. +# LOCALIZATION NOTE (introDesc, introManual): These strings describe the +# 'intro' command. The localization of 'Got it!' should be the same used in +# introTextGo. introDesc=Show the opening message - -# LOCALIZATION NOTE (introManual): A fuller description of the 'intro' -# command. Displayed when the user asks for help on what it does. introManual=Redisplay the message that is shown to new users until they click the 'Got it!' button -# LOCALIZATION NOTE (introTextOpening2): The 'intro text' opens when the user +# LOCALIZATION NOTE (introTextOpening2, introTextCommands, introTextKeys2, +# introTextF1Escape, introTextGo): These strings are displayed when the user # first opens the developer toolbar to explain the command line, and is shown -# each time it is opened until the user clicks the 'Got it!' button. This -# string is the opening paragraph of the intro text. +# each time it is opened until the user clicks the 'Got it!' button. introTextOpening2=This command line is designed for developers. It focuses on speed of input over JavaScript syntax and a rich display over monospace output. - -# LOCALIZATION NOTE (introTextCommands): For information about the 'intro -# text' see introTextOpening2. The second paragraph is in 2 sections, the -# first section points the user to the 'help' command. introTextCommands=For a list of commands type - -# LOCALIZATION NOTE (introTextKeys2): For information about the 'intro text' -# see introTextOpening2. The second section in the second paragraph points the -# user to the F1/Escape keys which show and hide hints. introTextKeys2=, or to show/hide command hints press - -# LOCALIZATION NOTE (introTextF1Escape): For information about the 'intro -# text' see introTextOpening2. This string is used with introTextKeys2, and -# contains the keys that are pressed to open and close hints. introTextF1Escape=F1/Escape - -# LOCALIZATION NOTE (introTextGo): For information about the 'intro text' see -# introTextOpening2. The text on the button that dismisses the intro text. introTextGo=Got it! -# LOCALIZATION NOTE (hideIntroDesc): Short description of the 'hideIntro' -# setting. Displayed when the user asks for help on the settings. +# LOCALIZATION NOTE: This is a short description of the 'hideIntro' setting. hideIntroDesc=Show the initial welcome message -# LOCALIZATION NOTE (eagerHelperDesc): Short description of the 'eagerHelper' -# setting. Displayed when the user asks for help on the settings. eagerHelper -# allows users to select between showing no tooltips, permanent tooltips, and -# only important tooltips +# LOCALIZATION NOTE: This is a description of the 'eagerHelper' setting. It's +# displayed when the user asks for help on the settings. eagerHelper allows +# users to select between showing no tooltips, permanent tooltips, and only +# important tooltips. eagerHelperDesc=How eager are the tooltips -# LOCALIZATION NOTE (allowSetDesc): Short description of the 'allowSetDesc' -# setting. Displayed when the user asks for help on the settings. +# LOCALIZATION NOTE: This is a short description of the 'allowSetDesc' +# setting. allowSetDesc=Has the user enabled the 'pref set' command? -# LOCALIZATION NOTE (introBody): The text displayed at the top of the output -# for the help command, just before the list of commands. This text is wrapped -# inside a link to a localized MDN article +# LOCALIZATION NOTE: This text is displayed at the top of the output for the +# help command, just before the list of commands. This text is wrapped inside +# a link to a localized MDN article. introBody=For more information see MDN. -
--- a/browser/locales/en-US/chrome/browser/devtools/gclicommands.properties +++ b/browser/locales/en-US/chrome/browser/devtools/gclicommands.properties @@ -499,16 +499,31 @@ resizeModeManual2=Responsive websites re # name, which is why it should be as short as possible. cmdDesc=Manipulate the commands # LOCALIZATION NOTE (cmdRefreshDesc) A very short description of the 'cmd refresh' # command. This string is designed to be shown in a menu alongside the command # name, which is why it should be as short as possible. cmdRefreshDesc=Re-read mozcmd directory +# LOCALIZATION NOTE (cmdStatus) When the we load new commands from mozcmd +# directory, we report on how many we loaded. %1$S is a count of the number +# of loaded commands, and %2$S is the directory we loaded from. +cmdStatus=Read %1$S commands from '%2$S' + +# LOCALIZATION NOTE (cmdSetdirDesc) +cmdSetdirDesc=Setup a mozcmd directory + +# LOCALIZATION NOTE (cmdSetdirManual) +cmdSetdirManual=A 'mozcmd' directory is an easy way to create new custom commands for the Firefox command line. For more information see the <a href="https://developer.mozilla.org/en-US/docs/Tools/GCLI/Customization">MDN documentation</a>. + +# LOCALIZATION NOTE (cmdSetdirDirectoryDesc) The description of the directory +# parameter to the 'cmd setdir' command. +cmdSetdirDirectoryDesc=Directory containing .mozcmd files + # LOCALIZATION NOTE (addonDesc) A very short description of the 'addon' # command. This string is designed to be shown in a menu alongside the command # name, which is why it should be as short as possible. addonDesc=Manipulate add-ons # LOCALIZATION NOTE (addonListDesc) A very short description of the 'addon list' # command. This string is designed to be shown in a menu alongside the command # name, which is why it should be as short as possible.
--- a/browser/locales/en-US/chrome/browser/devtools/toolbox.dtd +++ b/browser/locales/en-US/chrome/browser/devtools/toolbox.dtd @@ -4,16 +4,18 @@ <!-- LOCALIZATION NOTE : FILE This file contains the Toolbox strings --> <!-- LOCALIZATION NOTE : FILE Do not translate key --> <!ENTITY closeCmd.key "W"> <!ENTITY toolboxCloseButton.tooltip "Close Developer Tools"> <!ENTITY toolboxOptionsButton.key "O"> +<!ENTITY toolboxNextTool.key "]"> +<!ENTITY toolboxPreviousTool.key "["> <!-- LOCALIZATION NOTE (options.context.advancedSettings): This is the label for - the heading of the advanced settings group in the options panel. --> <!ENTITY options.context.advancedSettings "Advanced settings"> <!-- LOCALIZATION NOTE (options.context.requiresRestart2): This is the requires - restart label at right of settings that require a browser restart to be - effective. -->
--- a/toolkit/devtools/gcli/gcli.jsm +++ b/toolkit/devtools/gcli/gcli.jsm @@ -46,601 +46,129 @@ var Event = Components.interfaces.nsIDOM * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ -var mozl10n = {}; - -(function(aMozl10n) { - - 'use strict'; - - var temp = {}; - Components.utils.import("resource://gre/modules/Services.jsm", temp); - - var stringBundle; - try { - stringBundle = temp.Services.strings.createBundle( - "chrome://browser/locale/devtools/gclicommands.properties"); - } - catch (ex) { - console.error("Using string fallbacks"); - stringBundle = { - GetStringFromName: function(name) { return name; }, - formatStringFromName: function(name) { return name; } - }; - } +define('gcli/index', ['require', 'exports', 'module' , 'gcli/settings', 'gcli/api', 'gcli/types/selection', 'gcli/types/delegate', 'gcli/types/array', 'gcli/types/boolean', 'gcli/types/command', 'gcli/types/date', 'gcli/types/file', 'gcli/types/javascript', 'gcli/types/node', 'gcli/types/number', 'gcli/types/resource', 'gcli/types/setting', 'gcli/types/string', 'gcli/converters', 'gcli/converters/basic', 'gcli/converters/terminal', 'gcli/ui/intro', 'gcli/ui/focus', 'gcli/ui/fields/basic', 'gcli/ui/fields/javascript', 'gcli/ui/fields/selection', 'gcli/commands/connect', 'gcli/commands/context', 'gcli/commands/help', 'gcli/commands/pref', 'gcli/ui/ffdisplay'], function(require, exports, module) { + +'use strict'; + +require('gcli/settings').startup(); + +var api = require('gcli/api'); +api.populateApi(exports); + +exports.addItems(require('gcli/types/selection').items); +exports.addItems(require('gcli/types/delegate').items); + +exports.addItems(require('gcli/types/array').items); +exports.addItems(require('gcli/types/boolean').items); +exports.addItems(require('gcli/types/command').items); +exports.addItems(require('gcli/types/date').items); +exports.addItems(require('gcli/types/file').items); +exports.addItems(require('gcli/types/javascript').items); +exports.addItems(require('gcli/types/node').items); +exports.addItems(require('gcli/types/number').items); +exports.addItems(require('gcli/types/resource').items); +exports.addItems(require('gcli/types/setting').items); +exports.addItems(require('gcli/types/string').items); + +exports.addItems(require('gcli/converters').items); +exports.addItems(require('gcli/converters/basic').items); +// Don't export the 'html' type to avoid use of innerHTML +// exports.addItems(require('gcli/converters/html').items); +exports.addItems(require('gcli/converters/terminal').items); + +exports.addItems(require('gcli/ui/intro').items); +exports.addItems(require('gcli/ui/focus').items); + +exports.addItems(require('gcli/ui/fields/basic').items); +exports.addItems(require('gcli/ui/fields/javascript').items); +exports.addItems(require('gcli/ui/fields/selection').items); + +// Don't export the '{' command +// exports.addItems(require('gcli/cli').items); + +exports.addItems(require('gcli/commands/connect').items); +exports.addItems(require('gcli/commands/context').items); +exports.addItems(require('gcli/commands/help').items); +exports.addItems(require('gcli/commands/pref').items); + +/** + * This code is internal and subject to change without notice. + * createDisplay() for Firefox requires an options object with the following + * members: + * - contentDocument: From the window of the attached tab + * - chromeDocument: GCLITerm.document + * - environment.hudId: GCLITerm.hudId + * - jsEnvironment.globalObject: 'window' + * - jsEnvironment.evalFunction: 'eval' in a sandbox + * - inputElement: GCLITerm.inputNode + * - completeElement: GCLITerm.completeNode + * - hintElement: GCLITerm.hintNode + * - inputBackgroundElement: GCLITerm.inputStack + */ +exports.createDisplay = function(opts) { + var FFDisplay = require('gcli/ui/ffdisplay').FFDisplay; + return new FFDisplay(opts); +}; + +var prefSvc = Components.classes['@mozilla.org/preferences-service;1'] + .getService(Components.interfaces.nsIPrefService); +var prefBranch = prefSvc.getBranch(null) + .QueryInterface(Components.interfaces.nsIPrefBranch2); + +exports.hiddenByChromePref = function() { + return !prefBranch.prefHasUserValue('devtools.chrome.enabled'); +}; + + +try { + var Services = Components.utils.import("resource://gre/modules/Services.jsm", {}).Services; + var stringBundle = Services.strings.createBundle( + 'chrome://browser/locale/devtools/gclicommands.properties'); /** * Lookup a string in the GCLI string bundle - * @param name The name to lookup - * @return The looked up name */ - aMozl10n.lookup = function(name) { + exports.lookup = function(name) { try { return stringBundle.GetStringFromName(name); } catch (ex) { - throw new Error("Failure in lookup('" + name + "')"); + throw new Error('Failure in lookup(\'' + name + '\')'); } }; /** * Lookup a string in the GCLI string bundle - * @param name The name to lookup - * @param swaps An array of swaps. See stringBundle.formatStringFromName - * @return The looked up name */ - aMozl10n.lookupFormat = function(name, swaps) { + exports.lookupFormat = function(name, swaps) { try { return stringBundle.formatStringFromName(name, swaps, swaps.length); } catch (ex) { - throw new Error("Failure in lookupFormat('" + name + "')"); + throw new Error('Failure in lookupFormat(\'' + name + '\')'); } }; - -})(mozl10n); - -define('gcli/index', ['require', 'exports', 'module' , 'gcli/types/basic', 'gcli/types/selection', 'gcli/types/command', 'gcli/types/date', 'gcli/types/javascript', 'gcli/types/node', 'gcli/types/resource', 'gcli/types/setting', 'gcli/settings', 'gcli/ui/intro', 'gcli/ui/focus', 'gcli/ui/fields/basic', 'gcli/ui/fields/javascript', 'gcli/ui/fields/selection', 'gcli/commands/connect', 'gcli/commands/context', 'gcli/commands/help', 'gcli/commands/pref', 'gcli/canon', 'gcli/converters', 'gcli/types', 'gcli/ui/ffdisplay'], function(require, exports, module) { - - 'use strict'; - - // Internal startup process. Not exported - // The basic/selection are depended on by others so they must come first - require('gcli/types/basic').startup(); - require('gcli/types/selection').startup(); - - require('gcli/types/command').startup(); - require('gcli/types/date').startup(); - require('gcli/types/javascript').startup(); - require('gcli/types/node').startup(); - require('gcli/types/resource').startup(); - require('gcli/types/setting').startup(); - - require('gcli/settings').startup(); - require('gcli/ui/intro').startup(); - require('gcli/ui/focus').startup(); - require('gcli/ui/fields/basic').startup(); - require('gcli/ui/fields/javascript').startup(); - require('gcli/ui/fields/selection').startup(); - - require('gcli/commands/connect').startup(); - require('gcli/commands/context').startup(); - require('gcli/commands/help').startup(); - require('gcli/commands/pref').startup(); - - var Cc = Components.classes; - var Ci = Components.interfaces; - var prefSvc = "@mozilla.org/preferences-service;1"; - var prefService = Cc[prefSvc].getService(Ci.nsIPrefService); - var prefBranch = prefService.getBranch(null).QueryInterface(Ci.nsIPrefBranch2); - - // The API for use by command authors - exports.addCommand = require('gcli/canon').addCommand; - exports.removeCommand = require('gcli/canon').removeCommand; - exports.addConverter = require('gcli/converters').addConverter; - exports.removeConverter = require('gcli/converters').removeConverter; - exports.addType = require('gcli/types').addType; - exports.removeType = require('gcli/types').removeType; - - exports.lookup = mozl10n.lookup; - exports.lookupFormat = mozl10n.lookupFormat; - - /** - * This code is internal and subject to change without notice. - * createView() for Firefox requires an options object with the following - * members: - * - contentDocument: From the window of the attached tab - * - chromeDocument: GCLITerm.document - * - environment.hudId: GCLITerm.hudId - * - jsEnvironment.globalObject: 'window' - * - jsEnvironment.evalFunction: 'eval' in a sandbox - * - inputElement: GCLITerm.inputNode - * - completeElement: GCLITerm.completeNode - * - hintElement: GCLITerm.hintNode - * - inputBackgroundElement: GCLITerm.inputStack - */ - exports.createDisplay = function(opts) { - var FFDisplay = require('gcli/ui/ffdisplay').FFDisplay; - return new FFDisplay(opts); - }; - - exports.hiddenByChromePref = function() { - return !prefBranch.prefHasUserValue("devtools.chrome.enabled"); +} +catch (ex) { + console.error('Using string fallbacks', ex); + + exports.lookup = function(name) { + return name; }; - -}); -/* - * Copyright 2012, Mozilla Foundation and contributors - * - * Licensed under the Apache License, Version 2.0 (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.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -define('gcli/types/basic', ['require', 'exports', 'module' , 'util/promise', 'util/util', 'util/l10n', 'gcli/types', 'gcli/types/selection', 'gcli/argument'], function(require, exports, module) { - -'use strict'; - -var promise = require('util/promise'); -var util = require('util/util'); -var l10n = require('util/l10n'); -var types = require('gcli/types'); -var Type = require('gcli/types').Type; -var Status = require('gcli/types').Status; -var Conversion = require('gcli/types').Conversion; -var ArrayConversion = require('gcli/types').ArrayConversion; -var SelectionType = require('gcli/types/selection').SelectionType; - -var BlankArgument = require('gcli/argument').BlankArgument; -var ArrayArgument = require('gcli/argument').ArrayArgument; - - -/** - * Registration and de-registration. - */ -exports.startup = function() { - types.addType(StringType); - types.addType(NumberType); - types.addType(BooleanType); - types.addType(BlankType); - types.addType(DelegateType); - types.addType(ArrayType); -}; - -exports.shutdown = function() { - types.removeType(StringType); - types.removeType(NumberType); - types.removeType(BooleanType); - types.removeType(BlankType); - types.removeType(DelegateType); - types.removeType(ArrayType); -}; - - -/** - * 'string' the most basic string type where all we need to do is to take care - * of converting escaped characters like \t, \n, etc. For the full list see - * https://developer.mozilla.org/en-US/docs/JavaScript/Guide/Values,_variables,_and_literals - * - * The exception is that we ignore \b because replacing '\b' characters in - * stringify() with their escaped version injects '\\b' all over the place and - * the need to support \b seems low) - * - * @param typeSpec Options object, currently obeys only one parameter: - * - allowBlank: Allow a blank string to be counted as valid - */ -function StringType(typeSpec) { - this._allowBlank = !!typeSpec.allowBlank; -} - -StringType.prototype = Object.create(Type.prototype); - -StringType.prototype.stringify = function(value, context) { - if (value == null) { - return ''; - } - - return value - .replace(/\\/g, '\\\\') - .replace(/\f/g, '\\f') - .replace(/\n/g, '\\n') - .replace(/\r/g, '\\r') - .replace(/\t/g, '\\t') - .replace(/\v/g, '\\v') - .replace(/\n/g, '\\n') - .replace(/\r/g, '\\r') - .replace(/ /g, '\\ ') - .replace(/'/g, '\\\'') - .replace(/"/g, '\\"') - .replace(/{/g, '\\{') - .replace(/}/g, '\\}'); -}; - -StringType.prototype.parse = function(arg, context) { - if (!this._allowBlank && (arg.text == null || arg.text === '')) { - return promise.resolve(new Conversion(undefined, arg, Status.INCOMPLETE, '')); - } - - // The string '\\' (i.e. an escaped \ (represented here as '\\\\' because it - // is double escaped)) is first converted to a private unicode character and - // then at the end from \uF000 to a single '\' to avoid the string \\n being - // converted first to \n and then to a <LF> - - var value = arg.text - .replace(/\\\\/g, '\uF000') - .replace(/\\f/g, '\f') - .replace(/\\n/g, '\n') - .replace(/\\r/g, '\r') - .replace(/\\t/g, '\t') - .replace(/\\v/g, '\v') - .replace(/\\n/g, '\n') - .replace(/\\r/g, '\r') - .replace(/\\ /g, ' ') - .replace(/\\'/g, '\'') - .replace(/\\"/g, '"') - .replace(/\\{/g, '{') - .replace(/\\}/g, '}') - .replace(/\uF000/g, '\\'); - - return promise.resolve(new Conversion(value, arg)); -}; - -StringType.prototype.name = 'string'; - -exports.StringType = StringType; - - -/** - * We distinguish between integers and floats with the _allowFloat flag. - */ -function NumberType(typeSpec) { - // Default to integer values - this._allowFloat = !!typeSpec.allowFloat; - - if (typeSpec) { - this._min = typeSpec.min; - this._max = typeSpec.max; - this._step = typeSpec.step || 1; - - if (!this._allowFloat && - (this._isFloat(this._min) || - this._isFloat(this._max) || - this._isFloat(this._step))) { - throw new Error('allowFloat is false, but non-integer values given in type spec'); - } - } - else { - this._step = 1; - } -} - -NumberType.prototype = Object.create(Type.prototype); - -NumberType.prototype.stringify = function(value, context) { - if (value == null) { - return ''; - } - return '' + value; -}; - -NumberType.prototype.getMin = function(context) { - if (this._min) { - if (typeof this._min === 'function') { - return this._min(context); - } - if (typeof this._min === 'number') { - return this._min; - } - } - return undefined; -}; - -NumberType.prototype.getMax = function(context) { - if (this._max) { - if (typeof this._max === 'function') { - return this._max(context); - } - if (typeof this._max === 'number') { - return this._max; - } - } - return undefined; -}; - -NumberType.prototype.parse = function(arg, context) { - if (arg.text.replace(/^\s*-?/, '').length === 0) { - return promise.resolve(new Conversion(undefined, arg, Status.INCOMPLETE, '')); - } - - if (!this._allowFloat && (arg.text.indexOf('.') !== -1)) { - var message = l10n.lookupFormat('typesNumberNotInt2', [ arg.text ]); - return promise.resolve(new Conversion(undefined, arg, Status.ERROR, message)); - } - - var value; - if (this._allowFloat) { - value = parseFloat(arg.text); - } - else { - value = parseInt(arg.text, 10); - } - - if (isNaN(value)) { - var message = l10n.lookupFormat('typesNumberNan', [ arg.text ]); - return promise.resolve(new Conversion(undefined, arg, Status.ERROR, message)); - } - - var max = this.getMax(context); - if (max != null && value > max) { - var message = l10n.lookupFormat('typesNumberMax', [ value, max ]); - return promise.resolve(new Conversion(undefined, arg, Status.ERROR, message)); - } - - var min = this.getMin(context); - if (min != null && value < min) { - var message = l10n.lookupFormat('typesNumberMin', [ value, min ]); - return promise.resolve(new Conversion(undefined, arg, Status.ERROR, message)); - } - - return promise.resolve(new Conversion(value, arg)); -}; - -NumberType.prototype.decrement = function(value, context) { - if (typeof value !== 'number' || isNaN(value)) { - return this.getMax(context) || 1; - } - var newValue = value - this._step; - // Snap to the nearest incremental of the step - newValue = Math.ceil(newValue / this._step) * this._step; - return this._boundsCheck(newValue, context); -}; - -NumberType.prototype.increment = function(value, context) { - if (typeof value !== 'number' || isNaN(value)) { - var min = this.getMin(context); - return min != null ? min : 0; - } - var newValue = value + this._step; - // Snap to the nearest incremental of the step - newValue = Math.floor(newValue / this._step) * this._step; - if (this.getMax(context) == null) { - return newValue; - } - return this._boundsCheck(newValue, context); -}; - -/** - * Return the input value so long as it is within the max/min bounds. If it is - * lower than the minimum, return the minimum. If it is bigger than the maximum - * then return the maximum. - */ -NumberType.prototype._boundsCheck = function(value, context) { - var min = this.getMin(context); - if (min != null && value < min) { - return min; - } - var max = this.getMax(context); - if (max != null && value > max) { - return max; - } - return value; -}; - -/** - * Return true if the given value is a finite number and not an integer, else - * return false. - */ -NumberType.prototype._isFloat = function(value) { - return ((typeof value === 'number') && isFinite(value) && (value % 1 !== 0)); -}; - -NumberType.prototype.name = 'number'; - -exports.NumberType = NumberType; - - -/** - * true/false values - */ -function BooleanType(typeSpec) { -} - -BooleanType.prototype = Object.create(SelectionType.prototype); - -BooleanType.prototype.lookup = [ - { name: 'false', value: false }, - { name: 'true', value: true } -]; - -BooleanType.prototype.parse = function(arg, context) { - if (arg.type === 'TrueNamedArgument') { - return promise.resolve(new Conversion(true, arg)); - } - if (arg.type === 'FalseNamedArgument') { - return promise.resolve(new Conversion(false, arg)); - } - return SelectionType.prototype.parse.call(this, arg, context); -}; - -BooleanType.prototype.stringify = function(value, context) { - if (value == null) { - return ''; - } - return '' + value; -}; - -BooleanType.prototype.getBlank = function(context) { - return new Conversion(false, new BlankArgument(), Status.VALID, '', - promise.resolve(this.lookup)); -}; - -BooleanType.prototype.name = 'boolean'; - -exports.BooleanType = BooleanType; - - -/** - * A type for "we don't know right now, but hope to soon". - */ -function DelegateType(typeSpec) { - if (typeof typeSpec.delegateType !== 'function') { - throw new Error('Instances of DelegateType need typeSpec.delegateType to be a function that returns a type'); - } - Object.keys(typeSpec).forEach(function(key) { - this[key] = typeSpec[key]; - }, this); -} - -/** - * Child types should implement this method to return an instance of the type - * that should be used. If no type is available, or some sort of temporary - * placeholder is required, BlankType can be used. - * @param context An ExecutionContext to allow access to information about the - * current state of the command line, or null if one is not available. A - * context is available for stringify, parse*, [in|de]crement and getType - * functions but not for isImportant - */ -DelegateType.prototype.delegateType = function(context) { - throw new Error('Not implemented'); -}; - -DelegateType.prototype = Object.create(Type.prototype); - -DelegateType.prototype.stringify = function(value, context) { - return this.delegateType(context).stringify(value, context); -}; - -DelegateType.prototype.parse = function(arg, context) { - return this.delegateType(context).parse(arg, context); -}; - -DelegateType.prototype.decrement = function(value, context) { - var delegated = this.delegateType(context); - return (delegated.decrement ? delegated.decrement(value, context) : undefined); -}; - -DelegateType.prototype.increment = function(value, context) { - var delegated = this.delegateType(context); - return (delegated.increment ? delegated.increment(value, context) : undefined); -}; - -DelegateType.prototype.getType = function(context) { - return this.delegateType(context); -}; - -Object.defineProperty(DelegateType.prototype, 'isImportant', { - get: function() { - return this.delegateType().isImportant; - }, - enumerable: true -}); - -/** - * DelegateType is designed to be inherited from, so DelegateField needs a way - * to check if something works like a delegate without using 'name' - */ -DelegateType.prototype.isDelegate = true; - -DelegateType.prototype.name = 'delegate'; - -exports.DelegateType = DelegateType; - - -/** - * 'blank' is a type for use with DelegateType when we don't know yet. - * It should not be used anywhere else. - */ -function BlankType(typeSpec) { -} - -BlankType.prototype = Object.create(Type.prototype); - -BlankType.prototype.stringify = function(value, context) { - return ''; -}; - -BlankType.prototype.parse = function(arg, context) { - return promise.resolve(new Conversion(undefined, arg)); -}; - -BlankType.prototype.name = 'blank'; - -exports.BlankType = BlankType; - - -/** - * A set of objects of the same type - */ -function ArrayType(typeSpec) { - if (!typeSpec.subtype) { - console.error('Array.typeSpec is missing subtype. Assuming string.' + - JSON.stringify(typeSpec)); - typeSpec.subtype = 'string'; - } - - Object.keys(typeSpec).forEach(function(key) { - this[key] = typeSpec[key]; - }, this); - this.subtype = types.createType(this.subtype); -} - -ArrayType.prototype = Object.create(Type.prototype); - -ArrayType.prototype.stringify = function(values, context) { - if (values == null) { - return ''; - } - // BUG 664204: Check for strings with spaces and add quotes - return values.join(' '); -}; - -ArrayType.prototype.parse = function(arg, context) { - if (arg.type !== 'ArrayArgument') { - console.error('non ArrayArgument to ArrayType.parse', arg); - throw new Error('non ArrayArgument to ArrayType.parse'); - } - - // Parse an argument to a conversion - // Hack alert. ArrayConversion needs to be able to answer questions about - // the status of individual conversions in addition to the overall state. - // |subArg.conversion| allows us to do that easily. - var subArgParse = function(subArg) { - return this.subtype.parse(subArg, context).then(function(conversion) { - subArg.conversion = conversion; - return conversion; - }.bind(this)); - }.bind(this); - - var conversionPromises = arg.getArguments().map(subArgParse); - return promise.all(conversionPromises).then(function(conversions) { - return new ArrayConversion(conversions, arg); - }); -}; - -ArrayType.prototype.getBlank = function(context) { - return new ArrayConversion([], new ArrayArgument()); -}; - -ArrayType.prototype.name = 'array'; - -exports.ArrayType = ArrayType; + exports.lookupFormat = function(name, swaps) { + return name; + }; +} }); /* * Copyright 2012, Mozilla Foundation and contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -650,29 +178,299 @@ exports.ArrayType = ArrayType; * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ -define('util/promise', ['require', 'exports', 'module' ], function(require, exports, module) { +define('gcli/settings', ['require', 'exports', 'module' , 'util/util', 'gcli/types'], function(require, exports, module) { 'use strict'; -var imported = {}; -Components.utils.import("resource://gre/modules/commonjs/sdk/core/promise.js", - imported); - -exports.defer = imported.Promise.defer; -exports.resolve = imported.Promise.resolve; -exports.reject = imported.Promise.reject; -exports.promised = imported.Promise.promised; -exports.all = imported.Promise.all; +var imports = {}; + +Components.utils.import('resource://gre/modules/XPCOMUtils.jsm', imports); + +imports.XPCOMUtils.defineLazyGetter(imports, 'prefBranch', function() { + var prefService = Components.classes['@mozilla.org/preferences-service;1'] + .getService(Components.interfaces.nsIPrefService); + return prefService.getBranch(null) + .QueryInterface(Components.interfaces.nsIPrefBranch2); +}); + +imports.XPCOMUtils.defineLazyGetter(imports, 'supportsString', function() { + return Components.classes["@mozilla.org/supports-string;1"] + .createInstance(Components.interfaces.nsISupportsString); +}); + + +var util = require('util/util'); +var types = require('gcli/types'); + +/** + * All local settings have this prefix when used in Firefox + */ +var DEVTOOLS_PREFIX = 'devtools.gcli.'; + +/** + * A class to wrap up the properties of a preference. + * @see toolkit/components/viewconfig/content/config.js + */ +function Setting(prefSpec) { + if (typeof prefSpec === 'string') { + // We're coming from getAll() i.e. a full listing of prefs + this.name = prefSpec; + this.description = ''; + } + else { + // A specific addition by GCLI + this.name = DEVTOOLS_PREFIX + prefSpec.name; + + if (prefSpec.ignoreTypeDifference !== true && prefSpec.type) { + if (this.type.name !== prefSpec.type) { + throw new Error('Locally declared type (' + prefSpec.type + ') != ' + + 'Mozilla declared type (' + this.type.name + ') for ' + this.name); + } + } + + this.description = prefSpec.description; + } + + this.onChange = util.createEvent('Setting.onChange'); +} + +/** + * What type is this property: boolean/integer/string? + */ +Object.defineProperty(Setting.prototype, 'type', { + get: function() { + switch (imports.prefBranch.getPrefType(this.name)) { + case imports.prefBranch.PREF_BOOL: + return types.createType('boolean'); + + case imports.prefBranch.PREF_INT: + return types.createType('number'); + + case imports.prefBranch.PREF_STRING: + return types.createType('string'); + + default: + throw new Error('Unknown type for ' + this.name); + } + }, + enumerable: true +}); + +/** + * What type is this property: boolean/integer/string? + */ +Object.defineProperty(Setting.prototype, 'value', { + get: function() { + switch (imports.prefBranch.getPrefType(this.name)) { + case imports.prefBranch.PREF_BOOL: + return imports.prefBranch.getBoolPref(this.name); + + case imports.prefBranch.PREF_INT: + return imports.prefBranch.getIntPref(this.name); + + case imports.prefBranch.PREF_STRING: + var value = imports.prefBranch.getComplexValue(this.name, + Components.interfaces.nsISupportsString).data; + // In case of a localized string + if (/^chrome:\/\/.+\/locale\/.+\.properties/.test(value)) { + value = imports.prefBranch.getComplexValue(this.name, + Components.interfaces.nsIPrefLocalizedString).data; + } + return value; + + default: + throw new Error('Invalid value for ' + this.name); + } + }, + + set: function(value) { + if (imports.prefBranch.prefIsLocked(this.name)) { + throw new Error('Locked preference ' + this.name); + } + + switch (imports.prefBranch.getPrefType(this.name)) { + case imports.prefBranch.PREF_BOOL: + imports.prefBranch.setBoolPref(this.name, value); + break; + + case imports.prefBranch.PREF_INT: + imports.prefBranch.setIntPref(this.name, value); + break; + + case imports.prefBranch.PREF_STRING: + imports.supportsString.data = value; + imports.prefBranch.setComplexValue(this.name, + Components.interfaces.nsISupportsString, + imports.supportsString); + break; + + default: + throw new Error('Invalid value for ' + this.name); + } + + Services.prefs.savePrefFile(null); + }, + + enumerable: true +}); + +/** + * Reset this setting to it's initial default value + */ +Setting.prototype.setDefault = function() { + imports.prefBranch.clearUserPref(this.name); + Services.prefs.savePrefFile(null); +}; + + +/** + * Collection of preferences for sorted access + */ +var settingsAll = []; + +/** + * Collection of preferences for fast indexed access + */ +var settingsMap = new Map(); + +/** + * Flag so we know if we've read the system preferences + */ +var hasReadSystem = false; + +/** + * Clear out all preferences and return to initial state + */ +function reset() { + settingsMap = new Map(); + settingsAll = []; + hasReadSystem = false; +} + +/** + * Reset everything on startup and shutdown because we're doing lazy loading + */ +exports.startup = function() { + reset(); +}; + +exports.shutdown = function() { + reset(); +}; + +/** + * Load system prefs if they've not been loaded already + * @return true + */ +function readSystem() { + if (hasReadSystem) { + return; + } + + imports.prefBranch.getChildList('').forEach(function(name) { + var setting = new Setting(name); + settingsAll.push(setting); + settingsMap.set(name, setting); + }); + + settingsAll.sort(function(s1, s2) { + return s1.name.localeCompare(s2.name); + }); + + hasReadSystem = true; +} + +/** + * Get an array containing all known Settings filtered to match the given + * filter (string) at any point in the name of the setting + */ +exports.getAll = function(filter) { + readSystem(); + + if (filter == null) { + return settingsAll; + } + + return settingsAll.filter(function(setting) { + return setting.name.indexOf(filter) !== -1; + }); +}; + +/** + * Add a new setting. + */ +exports.addSetting = function(prefSpec) { + var setting = new Setting(prefSpec); + + if (settingsMap.has(setting.name)) { + // Once exists already, we're going to need to replace it in the array + for (var i = 0; i < settingsAll.length; i++) { + if (settingsAll[i].name === setting.name) { + settingsAll[i] = setting; + } + } + } + + settingsMap.set(setting.name, setting); + exports.onChange({ added: setting.name }); + + return setting; +}; + +/** + * Getter for an existing setting. Generally use of this function should be + * avoided. Systems that define a setting should export it if they wish it to + * be available to the outside, or not otherwise. Use of this function breaks + * that boundary and also hides dependencies. Acceptable uses include testing + * and embedded uses of GCLI that pre-define all settings (e.g. Firefox) + * @param name The name of the setting to fetch + * @return The found Setting object, or undefined if the setting was not found + */ +exports.getSetting = function(name) { + // We might be able to give the answer without needing to read all system + // settings if this is an internal setting + var found = settingsMap.get(name); + if (!found) { + found = settingsMap.get(DEVTOOLS_PREFIX + name); + } + + if (found) { + return found; + } + + if (hasReadSystem) { + return undefined; + } + else { + readSystem(); + var found = settingsMap.get(name); + if (!found) { + found = settingsMap.get(DEVTOOLS_PREFIX + name); + } + return found; + } +}; + +/** + * Event for use to detect when the list of settings changes + */ +exports.onChange = util.createEvent('Settings.onChange'); + +/** + * Remove a setting. A no-op in this case + */ +exports.removeSetting = function() { }; + }); /* * Copyright 2012, Mozilla Foundation and contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at @@ -908,31 +706,31 @@ exports.createEvent = function(name) { var promise = require('util/promise'); /** * Utility to convert a resolved promise to a concrete value. * Warning: This is something of an experiment. The alternative of mixing * concrete/promise return values could be better. */ -exports.synchronize = function(promise) { - if (promise == null || typeof promise.then !== 'function') { - return promise; +exports.synchronize = function(p) { + if (p == null || typeof p.then !== 'function') { + return p; } var failure = undefined; var reply = undefined; var onDone = function(value) { failure = false; reply = value; }; var onError = function (value) { failure = true; reply = value; }; - promise.then(onDone, onError); + p.then(onDone, onError); if (failure === undefined) { throw new Error('non synchronizable promise'); } if (failure) { throw reply; } return reply; }; @@ -1478,90 +1276,29 @@ else { * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ -define('util/l10n', ['require', 'exports', 'module' ], function(require, exports, module) { +define('util/promise', ['require', 'exports', 'module' ], function(require, exports, module) { 'use strict'; -Components.utils.import('resource://gre/modules/XPCOMUtils.jsm'); -Components.utils.import('resource://gre/modules/Services.jsm'); - -var imports = {}; -XPCOMUtils.defineLazyGetter(imports, 'stringBundle', function () { - return Services.strings.createBundle('chrome://browser/locale/devtools/gcli.properties'); -}); - -/* - * Not supported when embedded - we're doing things the Mozilla way not the - * require.js way. - */ -exports.registerStringsSource = function(modulePath) { - throw new Error('registerStringsSource is not available in mozilla'); -}; - -exports.unregisterStringsSource = function(modulePath) { - throw new Error('unregisterStringsSource is not available in mozilla'); -}; - -exports.lookupSwap = function(key, swaps) { - throw new Error('lookupSwap is not available in mozilla'); -}; - -exports.lookupPlural = function(key, ord, swaps) { - throw new Error('lookupPlural is not available in mozilla'); -}; - -exports.getPreferredLocales = function() { - return [ 'root' ]; -}; - -/** @see lookup() in lib/util/l10n.js */ -exports.lookup = function(key) { - try { - // Our memory leak hunter walks reachable objects trying to work out what - // type of thing they are using object.constructor.name. If that causes - // problems then we can avoid the unknown-key-exception with the following: - /* - if (key === 'constructor') { - return { name: 'l10n-mem-leak-defeat' }; - } - */ - - return imports.stringBundle.GetStringFromName(key); - } - catch (ex) { - console.error('Failed to lookup ', key, ex); - return key; - } -}; - -/** @see propertyLookup in lib/util/l10n.js */ -exports.propertyLookup = Proxy.create({ - get: function(rcvr, name) { - return exports.lookup(name); - } -}); - -/** @see lookupFormat in lib/util/l10n.js */ -exports.lookupFormat = function(key, swaps) { - try { - return imports.stringBundle.formatStringFromName(key, swaps, swaps.length); - } - catch (ex) { - console.error('Failed to format ', key, ex); - return key; - } -}; - +var imported = {}; +Components.utils.import("resource://gre/modules/commonjs/sdk/core/promise.js", + imported); + +exports.defer = imported.Promise.defer; +exports.resolve = imported.Promise.resolve; +exports.reject = imported.Promise.reject; +exports.promised = imported.Promise.promised; +exports.all = imported.Promise.all; }); /* * Copyright 2012, Mozilla Foundation and contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at @@ -1634,16 +1371,29 @@ var Status = { if (Array.isArray(status)) { status = Status.combine.apply(null, status); } if (status > combined) { combined = status; } } return combined; + }, + + fromString: function(str) { + switch (str) { + case Status.VALID.toString(): + return Status.VALID; + case Status.INCOMPLETE.toString(): + return Status.INCOMPLETE; + case Status.ERROR.toString(): + return Status.ERROR; + default: + throw new Error('\'' + str + '\' is not a status'); + } } }; exports.Status = Status; /** * The type.parse() method converts an Argument into a value, Conversion is @@ -1705,19 +1455,21 @@ function Conversion(value, arg, status, } /** * Ensure that all arguments that are part of this conversion know what they * are assigned to. * @param assignment The Assignment (param/conversion link) to inform the * argument about. */ -Conversion.prototype.assign = function(assignment) { - this.arg.assign(assignment); -}; +Object.defineProperty(Conversion.prototype, 'assignment', { + get: function() { return this.arg.assignment; }, + set: function(assignment) { this.arg.assignment = assignment; }, + enumerable: true +}); /** * Work out if there is information provided in the contained argument. */ Conversion.prototype.isDataProvided = function() { return this.arg.type !== 'BlankArgument'; }; @@ -1843,22 +1595,27 @@ function ArrayConversion(conversions, ar this.message = ''; // Predictions are generally provided by individual values this.predictions = []; } ArrayConversion.prototype = Object.create(Conversion.prototype); -ArrayConversion.prototype.assign = function(assignment) { - this.conversions.forEach(function(conversion) { - conversion.assign(assignment); - }, this); - this.assignment = assignment; -}; +Object.defineProperty(ArrayConversion.prototype, 'assignment', { + get: function() { return this._assignment; }, + set: function(assignment) { + this._assignment = assignment; + + this.conversions.forEach(function(conversion) { + conversion.assignment = assignment; + }, this); + }, + enumerable: true +}); ArrayConversion.prototype.getStatus = function(arg) { if (arg && arg.conversion) { return arg.conversion.getStatus(); } return this._status; }; @@ -1977,16 +1734,22 @@ Type.prototype.getBlank = function(conte * be able to lie about it's type for fields to accept it as one of their own. * Sub-types can ignore this unless they're DelegateType. * @param context An ExecutionContext to allow basic Requisition access */ Type.prototype.getType = function(context) { return this; }; +/** + * addItems allows registrations of a number of things. This allows it to know + * what type of item, and how it should be registered. + */ +Type.prototype.item = 'type'; + exports.Type = Type; /** * Private registry of types * Invariant: types[name] = type.name */ var registeredTypes = {}; @@ -2008,19 +1771,16 @@ exports.addType = function(type) { if (!type.name) { throw new Error('All registered types must have a name'); } if (type instanceof Type) { registeredTypes[type.name] = type; } else { - if (!type.parent) { - throw new Error('\'parent\' property required for object declarations'); - } var name = type.name; var parent = type.parent; type.name = parent; delete type.parent; registeredTypes[name] = exports.createType(type); type.name = name; @@ -2052,51 +1812,81 @@ exports.createType = function(typeSpec) if (typeof typeSpec === 'string') { typeSpec = { name: typeSpec }; } if (typeof typeSpec !== 'object') { throw new Error('Can\'t extract type from ' + typeSpec); } - if (!typeSpec.name) { - throw new Error('Missing \'name\' member to typeSpec'); - } - - var newType; - var type = registeredTypes[typeSpec.name]; + var type, newType; + if (typeSpec.name == null || typeSpec.name == 'type') { + type = Type; + } + else { + type = registeredTypes[typeSpec.name]; + } if (!type) { console.error('Known types: ' + Object.keys(registeredTypes).join(', ')); throw new Error('Unknown type: \'' + typeSpec.name + '\''); } if (typeof type === 'function') { newType = new type(typeSpec); } else { - // Shallow clone 'type' + // clone 'type' newType = {}; - for (var key in type) { - newType[key] = type[key]; - } - - // Copy the properties of typeSpec onto the new type - for (var key in typeSpec) { - newType[key] = typeSpec[key]; - } - + copyProperties(type, newType); + } + + // Copy the properties of typeSpec onto the new type + copyProperties(typeSpec, newType); + + if (typeof type !== 'function') { if (typeof newType.constructor === 'function') { newType.constructor(); } } return newType; }; +function copyProperties(src, dest) { + for (var key in src) { + var descriptor; + var obj = src; + while (true) { + descriptor = Object.getOwnPropertyDescriptor(obj, key); + if (descriptor != null) { + break; + } + obj = Object.getPrototypeOf(obj); + if (obj == null) { + throw new Error('Can\'t find descriptor of ' + key); + } + } + + if ('value' in descriptor) { + dest[key] = src[key]; + } + else if ('get' in descriptor) { + Object.defineProperty(dest, key, { + get: descriptor.get, + set: descriptor.set, + enumerable: descriptor.enumerable + }); + } + else { + throw new Error('Don\'t know how to copy ' + key + ' property.'); + } + } +} + }); /* * Copyright 2012, Mozilla Foundation and contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at @@ -2216,19 +2006,21 @@ Argument.prototype.beget = function(opti var type = options.type || Argument; return new type(text, prefix, suffix); }; /** * We need to keep track of which assignment we've been assigned to */ -Argument.prototype.assign = function(assignment) { - this.assignment = assignment; -}; +Object.defineProperty(Argument.prototype, 'assignment', { + get: function() { return this._assignment; }, + set: function(assignment) { this._assignment = assignment; }, + enumerable: true +}); /** * Sub-classes of Argument are collections of arguments, getArgs() gets access * to the members of the collection in order to do things like re-create input * command lines. For the simple Argument case it's just an array containing * only this. */ Argument.prototype.getArgs = function() { @@ -2396,23 +2188,27 @@ function MergedArgument(args, start, end MergedArgument.prototype = Object.create(Argument.prototype); MergedArgument.prototype.type = 'MergedArgument'; /** * Keep track of which assignment we've been assigned to, and allow the * original args to do the same. */ -MergedArgument.prototype.assign = function(assignment) { - this.args.forEach(function(arg) { - arg.assign(assignment); - }, this); - - this.assignment = assignment; -}; +Object.defineProperty(MergedArgument.prototype, 'assignment', { + get: function() { return this._assignment; }, + set: function(assignment) { + this._assignment = assignment; + + this.args.forEach(function(arg) { + arg.assignment = assignment; + }, this); + }, + enumerable: true +}); MergedArgument.prototype.getArgs = function() { return this.args; }; MergedArgument.prototype.equals = function(that) { if (this === that) { return true; @@ -2440,22 +2236,27 @@ function TrueNamedArgument(arg) { this.prefix = arg.prefix; this.suffix = arg.suffix; } TrueNamedArgument.prototype = Object.create(Argument.prototype); TrueNamedArgument.prototype.type = 'TrueNamedArgument'; -TrueNamedArgument.prototype.assign = function(assignment) { - if (this.arg) { - this.arg.assign(assignment); - } - this.assignment = assignment; -}; +Object.defineProperty(TrueNamedArgument.prototype, 'assignment', { + get: function() { return this._assignment; }, + set: function(assignment) { + this._assignment = assignment; + + if (this.arg) { + this.arg.assignment = assignment; + } + }, + enumerable: true +}); TrueNamedArgument.prototype.getArgs = function() { return [ this.arg ]; }; TrueNamedArgument.prototype.equals = function(that) { if (this === that) { return true; @@ -2557,23 +2358,28 @@ function NamedArgument() { this.suffix = this.valueArg.suffix; } } NamedArgument.prototype = Object.create(Argument.prototype); NamedArgument.prototype.type = 'NamedArgument'; -NamedArgument.prototype.assign = function(assignment) { - this.nameArg.assign(assignment); - if (this.valueArg != null) { - this.valueArg.assign(assignment); - } - this.assignment = assignment; -}; +Object.defineProperty(NamedArgument.prototype, 'assignment', { + get: function() { return this._assignment; }, + set: function(assignment) { + this._assignment = assignment; + + this.nameArg.assignment = assignment; + if (this.valueArg != null) { + this.valueArg.assignment = assignment; + } + }, + enumerable: true +}); NamedArgument.prototype.getArgs = function() { return this.valueArg ? [ this.nameArg, this.valueArg ] : [ this.nameArg ]; }; NamedArgument.prototype.equals = function(that) { if (this === that) { return true; @@ -2637,23 +2443,27 @@ ArrayArgument.prototype.addArgument = fu ArrayArgument.prototype.addArguments = function(args) { Array.prototype.push.apply(this.args, args); }; ArrayArgument.prototype.getArguments = function() { return this.args; }; -ArrayArgument.prototype.assign = function(assignment) { - this.args.forEach(function(arg) { - arg.assign(assignment); - }, this); - - this.assignment = assignment; -}; +Object.defineProperty(ArrayArgument.prototype, 'assignment', { + get: function() { return this._assignment; }, + set: function(assignment) { + this._assignment = assignment; + + this.args.forEach(function(arg) { + arg.assignment = assignment; + }, this); + }, + enumerable: true +}); ArrayArgument.prototype.getArgs = function() { return this.args; }; ArrayArgument.prototype.equals = function(that) { if (this === that) { return true; @@ -2703,44 +2513,1247 @@ exports.ArrayArgument = ArrayArgument; * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ -define('gcli/types/selection', ['require', 'exports', 'module' , 'util/promise', 'util/util', 'util/l10n', 'gcli/types', 'gcli/types/spell', 'gcli/argument'], function(require, exports, module) { +define('gcli/api', ['require', 'exports', 'module' , 'gcli/canon', 'gcli/converters', 'gcli/types', 'gcli/settings', 'gcli/ui/fields'], function(require, exports, module) { + +'use strict'; + +var canon = require('gcli/canon'); +var converters = require('gcli/converters'); +var types = require('gcli/types'); +var settings = require('gcli/settings'); +var fields = require('gcli/ui/fields'); + +/** + * This is the heart of the API that we expose to the outside + */ +exports.getApi = function() { + return { + addCommand: canon.addCommand, + removeCommand: canon.removeCommand, + addConverter: converters.addConverter, + removeConverter: converters.removeConverter, + addType: types.addType, + removeType: types.removeType, + + addItems: function(items) { + items.forEach(function(item) { + // Some items are registered using the constructor so we need to check + // the prototype for the the type of the item + var type = item.item; + if (type == null && item.prototype) { + type = item.prototype.item; + } + if (type === 'command') { + canon.addCommand(item); + } + else if (type === 'type') { + types.addType(item); + } + else if (type === 'converter') { + converters.addConverter(item); + } + else if (type === 'setting') { + settings.addSetting(item); + } + else if (type === 'field') { + fields.addField(item); + } + else { + console.error('Error for: ', item); + throw new Error('item property not found'); + } + }); + }, + + removeItems: function(items) { + items.forEach(function(item) { + if (item.item === 'command') { + canon.removeCommand(item); + } + else if (item.item === 'type') { + types.removeType(item); + } + else if (item.item === 'converter') { + converters.removeConverter(item); + } + else if (item.item === 'settings') { + settings.removeSetting(item); + } + else if (item.item === 'field') { + fields.removeField(item); + } + else { + throw new Error('item property not found'); + } + }); + } + }; +}; + +/** + * api.getApi() is clean, but generally we want to add the functions to the + * 'exports' object. So this is a quick helper. + */ +exports.populateApi = function(obj) { + var exportable = exports.getApi(); + Object.keys(exportable).forEach(function(key) { + obj[key] = exportable[key]; + }); +}; + +}); +/* + * Copyright 2012, Mozilla Foundation and contributors + * + * Licensed under the Apache License, Version 2.0 (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.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +define('gcli/canon', ['require', 'exports', 'module' , 'util/util', 'util/l10n', 'gcli/types'], function(require, exports, module) { + +'use strict'; + +var util = require('util/util'); +var l10n = require('util/l10n'); + +var types = require('gcli/types'); +var Status = require('gcli/types').Status; + +/** + * Implement the localization algorithm for any documentation objects (i.e. + * description and manual) in a command. + * @param data The data assigned to a description or manual property + * @param onUndefined If data == null, should we return the data untouched or + * lookup a 'we don't know' key in it's place. + */ +function lookup(data, onUndefined) { + if (data == null) { + if (onUndefined) { + return l10n.lookup(onUndefined); + } + + return data; + } + + if (typeof data === 'string') { + return data; + } + + if (typeof data === 'object') { + if (data.key) { + return l10n.lookup(data.key); + } + + var locales = l10n.getPreferredLocales(); + var translated; + locales.some(function(locale) { + translated = data[locale]; + return translated != null; + }); + if (translated != null) { + return translated; + } + + console.error('Can\'t find locale in descriptions: ' + + 'locales=' + JSON.stringify(locales) + ', ' + + 'description=' + JSON.stringify(data)); + return '(No description)'; + } + + return l10n.lookup(onUndefined); +} + + +/** + * The command object is mostly just setup around a commandSpec (as passed to + * #addCommand()). + */ +function Command(commandSpec) { + Object.keys(commandSpec).forEach(function(key) { + this[key] = commandSpec[key]; + }, this); + + if (!this.name) { + throw new Error('All registered commands must have a name'); + } + + if (this.params == null) { + this.params = []; + } + if (!Array.isArray(this.params)) { + throw new Error('command.params must be an array in ' + this.name); + } + + this.hasNamedParameters = false; + this.description = 'description' in this ? this.description : undefined; + this.description = lookup(this.description, 'canonDescNone'); + this.manual = 'manual' in this ? this.manual : undefined; + this.manual = lookup(this.manual); + + // At this point this.params has nested param groups. We want to flatten it + // out and replace the param object literals with Parameter objects + var paramSpecs = this.params; + this.params = []; + + // Track if the user is trying to mix default params and param groups. + // All the non-grouped parameters must come before all the param groups + // because non-grouped parameters can be assigned positionally, so their + // index is important. We don't want 'holes' in the order caused by + // parameter groups. + var usingGroups = false; + + // In theory this could easily be made recursive, so param groups could + // contain nested param groups. Current thinking is that the added + // complexity for the UI probably isn't worth it, so this implementation + // prevents nesting. + paramSpecs.forEach(function(spec) { + if (!spec.group) { + var param = new Parameter(spec, this, null); + this.params.push(param); + + if (!param.isPositionalAllowed) { + this.hasNamedParameters = true; + } + + if (usingGroups && param.groupName == null) { + throw new Error('Parameters can\'t come after param groups.' + + ' Ignoring ' + this.name + '/' + spec.name); + } + + if (param.groupName != null) { + usingGroups = true; + } + } + else { + spec.params.forEach(function(ispec) { + var param = new Parameter(ispec, this, spec.group); + this.params.push(param); + + if (!param.isPositionalAllowed) { + this.hasNamedParameters = true; + } + }, this); + + usingGroups = true; + } + }, this); +} + +/** + * JSON serializer that avoids non-serializable data + */ +Object.defineProperty(Command.prototype, 'json', { + get: function() { + return { + name: this.name, + description: this.description, + manual: this.manual, + params: this.params.map(function(param) { return param.json; }), + returnType: this.returnType, + isParent: (this.exec == null) + }; + }, + enumerable: true +}); + +exports.Command = Command; + + +/** + * A wrapper for a paramSpec so we can sort out shortened versions names for + * option switches + */ +function Parameter(paramSpec, command, groupName) { + this.command = command || { name: 'unnamed' }; + this.paramSpec = paramSpec; + this.name = this.paramSpec.name; + this.type = this.paramSpec.type; + + this.groupName = groupName; + if (this.groupName != null) { + if (this.paramSpec.option != null) { + throw new Error('Can\'t have a "option" property in a nested parameter'); + } + } + else { + if (this.paramSpec.option != null) { + this.groupName = this.paramSpec.option === true ? + l10n.lookup('canonDefaultGroupName') : + '' + this.paramSpec.option; + } + } + + if (!this.name) { + throw new Error('In ' + this.command.name + + ': all params must have a name'); + } + + var typeSpec = this.type; + this.type = types.createType(typeSpec); + if (this.type == null) { + console.error('Known types: ' + types.getTypeNames().join(', ')); + throw new Error('In ' + this.command.name + '/' + this.name + + ': can\'t find type for: ' + JSON.stringify(typeSpec)); + } + + // boolean parameters have an implicit defaultValue:false, which should + // not be changed. See the docs. + if (this.type.name === 'boolean' && + this.paramSpec.defaultValue !== undefined) { + throw new Error('In ' + this.command.name + '/' + this.name + + ': boolean parameters can not have a defaultValue.' + + ' Ignoring'); + } + + // Check the defaultValue for validity. + // Both undefined and null get a pass on this test. undefined is used when + // there is no defaultValue, and null is used when the parameter is + // optional, neither are required to parse and stringify. + if (this._defaultValue != null) { + try { + // Passing null in for a context is bound to get us into trouble some day + // in which case we'll need to mock one up in some way + var context = null; + var defaultText = this.type.stringify(this.paramSpec.defaultValue, context); + var parsed = this.type.parseString(defaultText, context); + parsed.then(function(defaultConversion) { + if (defaultConversion.getStatus() !== Status.VALID) { + console.error('In ' + this.command.name + '/' + this.name + + ': Error round tripping defaultValue. status = ' + + defaultConversion.getStatus()); + } + }.bind(this), util.errorHandler); + } + catch (ex) { + throw new Error('In ' + this.command.name + '/' + this.name + ': ' + ex); + } + } + + // All parameters that can only be set via a named parameter must have a + // non-undefined default value + if (!this.isPositionalAllowed && this.paramSpec.defaultValue === undefined && + this.type.getBlank == null && !(this.type.name === 'boolean')) { + throw new Error('In ' + this.command.name + '/' + this.name + + ': Missing defaultValue for optional parameter.'); + } +} + +/** + * type.getBlank can be expensive, so we delay execution where we can + */ +Object.defineProperty(Parameter.prototype, 'defaultValue', { + get: function() { + if (!('_defaultValue' in this)) { + this._defaultValue = (this.paramSpec.defaultValue !== undefined) ? + this.paramSpec.defaultValue : + this.type.getBlank().value; + } + + return this._defaultValue; + }, + enumerable : true +}); + +/** + * Does the given name uniquely identify this param (among the other params + * in this command) + * @param name The name to check + */ +Parameter.prototype.isKnownAs = function(name) { + if (name === '--' + this.name) { + return true; + } + return false; +}; + +/** + * Resolve the manual for this parameter, by looking in the paramSpec + * and doing a l10n lookup + */ +Object.defineProperty(Parameter.prototype, 'manual', { + get: function() { + return lookup(this.paramSpec.manual || undefined); + }, + enumerable: true +}); + +/** + * Resolve the description for this parameter, by looking in the paramSpec + * and doing a l10n lookup + */ +Object.defineProperty(Parameter.prototype, 'description', { + get: function() { + return lookup(this.paramSpec.description || undefined, 'canonDescNone'); + }, + enumerable: true +}); + +/** + * Is the user required to enter data for this parameter? (i.e. has + * defaultValue been set to something other than undefined) + */ +Object.defineProperty(Parameter.prototype, 'isDataRequired', { + get: function() { + return this.defaultValue === undefined; + }, + enumerable: true +}); + +/** + * Reflect the paramSpec 'hidden' property (dynamically so it can change) + */ +Object.defineProperty(Parameter.prototype, 'hidden', { + get: function() { + return this.paramSpec.hidden; + }, + enumerable: true +}); + +/** + * Are we allowed to assign data to this parameter using positional + * parameters? + */ +Object.defineProperty(Parameter.prototype, 'isPositionalAllowed', { + get: function() { + return this.groupName == null; + }, + enumerable: true +}); + +/** + * JSON serializer that avoids non-serializable data + */ +Object.defineProperty(Parameter.prototype, 'json', { + get: function() { + var json = { + name: this.name, + type: this.paramSpec.type, + description: this.description + }; + if (this.defaultValue !== undefined && json.type !== 'boolean') { + json.defaultValue = this.defaultValue; + } + if (this.option !== undefined) { + json.option = this.option; + } + return json; + }, + enumerable: true +}); + +exports.Parameter = Parameter; + + +/** + * A canon is a store for a list of commands + */ +function Canon() { + // A lookup hash of our registered commands + this._commands = {}; + // A sorted list of command names, we regularly want them in order, so pre-sort + this._commandNames = []; + // A lookup of the original commandSpecs by command name + this._commandSpecs = {}; + + // Enable people to be notified of changes to the list of commands + this.onCanonChange = util.createEvent('canon.onCanonChange'); +} + +/** + * Add a command to the canon of known commands. + * This function is exposed to the outside world (via gcli/index). It is + * documented in docs/index.md for all the world to see. + * @param commandSpec The command and its metadata. + * @return The new command + */ +Canon.prototype.addCommand = function(commandSpec) { + if (this._commands[commandSpec.name] != null) { + // Roughly canon.removeCommand() without the event call, which we do later + delete this._commands[commandSpec.name]; + this._commandNames = this._commandNames.filter(function(test) { + return test !== commandSpec.name; + }); + } + + var command = new Command(commandSpec); + this._commands[commandSpec.name] = command; + this._commandNames.push(commandSpec.name); + this._commandNames.sort(); + + this._commandSpecs[commandSpec.name] = commandSpec; + + this.onCanonChange(); + return command; +}; + +/** + * Remove an individual command. The opposite of #addCommand(). + * Removing a non-existent command is a no-op. + * @param commandOrName Either a command name or the command itself. + * @return true if a command was removed, false otherwise. + */ +Canon.prototype.removeCommand = function(commandOrName) { + var name = typeof commandOrName === 'string' ? + commandOrName : + commandOrName.name; + + if (!this._commands[name]) { + return false; + } + + // See start of canon.addCommand if changing this code + delete this._commands[name]; + delete this._commandSpecs[name]; + this._commandNames = this._commandNames.filter(function(test) { + return test !== name; + }); + + this.onCanonChange(); + return true; +}; + +/** + * Retrieve a command by name + * @param name The name of the command to retrieve + */ +Canon.prototype.getCommand = function(name) { + // '|| undefined' is to silence 'reference to undefined property' warnings + return this._commands[name] || undefined; +}; + +/** + * Get an array of all the registered commands. + */ +Canon.prototype.getCommands = function() { + return Object.keys(this._commands).map(function(name) { + return this._commands[name]; + }, this); +}; + +/** + * Get an array containing the names of the registered commands. + */ +Canon.prototype.getCommandNames = function() { + return this._commandNames.slice(0); +}; + +/** + * Get access to the stored commandMetaDatas (i.e. before they were made into + * instances of Command/Parameters) so we can remote them. + */ +Canon.prototype.getCommandSpecs = function() { + var specs = {}; + + Object.keys(this._commands).forEach(function(name) { + var command = this._commands[name]; + if (!command.noRemote) { + specs[name] = command.json; + } + }.bind(this)); + + return specs; +}; + +/** + * Add a set of commands that are executed somewhere else. + * @param prefix The name prefix that we assign to all command names + * @param commandSpecs Presumably as obtained from getCommandSpecs on remote + * @param remoter Function to call on exec of a new remote command. This is + * defined just like an exec function (i.e. that takes args/context as params + * and returns a promise) with one extra feature, that the context includes a + * 'commandName' property that contains the original command name. + * @param to URL-like string that describes where the commands are executed. + * This is to complete the parent command description. + */ +Canon.prototype.addProxyCommands = function(prefix, commandSpecs, remoter, to) { + var names = Object.keys(commandSpecs); + + if (this._commands[prefix] != null) { + throw new Error(l10n.lookupFormat('canonProxyExists', [ prefix ])); + } + + // We need to add the parent command so all the commands from the other + // system have a parent + this.addCommand({ + name: prefix, + isProxy: true, + description: l10n.lookupFormat('canonProxyDesc', [ to ]), + manual: l10n.lookupFormat('canonProxyManual', [ to ]) + }); + + names.forEach(function(name) { + var commandSpec = commandSpecs[name]; + + if (commandSpec.noRemote) { + return; + } + + if (!commandSpec.isParent) { + commandSpec.exec = function(args, context) { + context.commandName = name; + return remoter(args, context); + }.bind(this); + } + + commandSpec.name = prefix + ' ' + commandSpec.name; + commandSpec.isProxy = true; + this.addCommand(commandSpec); + }.bind(this)); +}; + +/** + * Add a set of commands that are executed somewhere else. + * @param prefix The name prefix that we assign to all command names + * @param commandSpecs Presumably as obtained from getCommandSpecs on remote + * @param remoter Function to call on exec of a new remote command. This is + * defined just like an exec function (i.e. that takes args/context as params + * and returns a promise) with one extra feature, that the context includes a + * 'commandName' property that contains the original command name. + * @param to URL-like string that describes where the commands are executed. + * This is to complete the parent command description. + */ +Canon.prototype.removeProxyCommands = function(prefix) { + var toRemove = []; + Object.keys(this._commandSpecs).forEach(function(name) { + if (name.indexOf(prefix) === 0) { + toRemove.push(name); + } + }.bind(this)); + + var removed = []; + toRemove.forEach(function(name) { + var command = this.getCommand(name); + if (command.isProxy) { + this.removeCommand(name); + removed.push(name); + } + else { + console.error('Skipping removal of \'' + name + + '\' because it is not a proxy command.'); + } + }.bind(this)); + + return removed; +}; + +var canon = new Canon(); + +exports.Canon = Canon; +exports.addCommand = canon.addCommand.bind(canon); +exports.removeCommand = canon.removeCommand.bind(canon); +exports.onCanonChange = canon.onCanonChange; +exports.getCommands = canon.getCommands.bind(canon); +exports.getCommand = canon.getCommand.bind(canon); +exports.getCommandNames = canon.getCommandNames.bind(canon); +exports.getCommandSpecs = canon.getCommandSpecs.bind(canon); +exports.addProxyCommands = canon.addProxyCommands.bind(canon); +exports.removeProxyCommands = canon.removeProxyCommands.bind(canon); + +/** + * CommandOutputManager stores the output objects generated by executed + * commands. + * + * CommandOutputManager is exposed to the the outside world and could (but + * shouldn't) be used before gcli.startup() has been called. + * This could should be defensive to that where possible, and we should + * certainly document if the use of it or similar will fail if used too soon. + */ +function CommandOutputManager() { + this.onOutput = util.createEvent('CommandOutputManager.onOutput'); +} + +exports.CommandOutputManager = CommandOutputManager; + + +}); +/* + * Copyright 2012, Mozilla Foundation and contributors + * + * Licensed under the Apache License, Version 2.0 (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.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +define('util/l10n', ['require', 'exports', 'module' ], function(require, exports, module) { + +'use strict'; + +Components.utils.import('resource://gre/modules/XPCOMUtils.jsm'); +Components.utils.import('resource://gre/modules/Services.jsm'); + +var imports = {}; +XPCOMUtils.defineLazyGetter(imports, 'stringBundle', function () { + return Services.strings.createBundle('chrome://browser/locale/devtools/gcli.properties'); +}); + +/* + * Not supported when embedded - we're doing things the Mozilla way not the + * require.js way. + */ +exports.registerStringsSource = function(modulePath) { + throw new Error('registerStringsSource is not available in mozilla'); +}; + +exports.unregisterStringsSource = function(modulePath) { + throw new Error('unregisterStringsSource is not available in mozilla'); +}; + +exports.lookupSwap = function(key, swaps) { + throw new Error('lookupSwap is not available in mozilla'); +}; + +exports.lookupPlural = function(key, ord, swaps) { + throw new Error('lookupPlural is not available in mozilla'); +}; + +exports.getPreferredLocales = function() { + return [ 'root' ]; +}; + +/** @see lookup() in lib/util/l10n.js */ +exports.lookup = function(key) { + try { + // Our memory leak hunter walks reachable objects trying to work out what + // type of thing they are using object.constructor.name. If that causes + // problems then we can avoid the unknown-key-exception with the following: + /* + if (key === 'constructor') { + return { name: 'l10n-mem-leak-defeat' }; + } + */ + + return imports.stringBundle.GetStringFromName(key); + } + catch (ex) { + console.error('Failed to lookup ', key, ex); + return key; + } +}; + +/** @see propertyLookup in lib/util/l10n.js */ +exports.propertyLookup = Proxy.create({ + get: function(rcvr, name) { + return exports.lookup(name); + } +}); + +/** @see lookupFormat in lib/util/l10n.js */ +exports.lookupFormat = function(key, swaps) { + try { + return imports.stringBundle.formatStringFromName(key, swaps, swaps.length); + } + catch (ex) { + console.error('Failed to format ', key, ex); + return key; + } +}; + + +}); +/* + * Copyright 2012, Mozilla Foundation and contributors + * + * Licensed under the Apache License, Version 2.0 (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.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +define('gcli/converters', ['require', 'exports', 'module' , 'util/promise'], function(require, exports, module) { + +'use strict'; + +var promise = require('util/promise'); + +// It's probably easiest to read this bottom to top + +/** + * Best guess at creating a DOM element from random data + */ +var fallbackDomConverter = { + from: '*', + to: 'dom', + exec: function(data, conversionContext) { + return conversionContext.document.createTextNode(data || ''); + } +}; + +/** + * Best guess at creating a string from random data + */ +var fallbackStringConverter = { + from: '*', + to: 'string', + exec: function(data, conversionContext) { + return data == null ? '' : data.toString(); + } +}; + +/** + * Convert a view object to a DOM element + */ +var viewDomConverter = { + item: 'converter', + from: 'view', + to: 'dom', + exec: function(view, conversionContext) { + return view.toDom(conversionContext.document); + } +}; + +/** + * Convert a view object to a string + */ +var viewStringConverter = { + item: 'converter', + from: 'view', + to: 'string', + exec: function(view, conversionContext) { + return view.toDom(conversionContext.document).textContent; + } +}; + +/** + * Create a new converter by using 2 converters, one after the other + */ +function getChainConverter(first, second) { + if (first.to !== second.from) { + throw new Error('Chain convert impossible: ' + first.to + '!=' + second.from); + } + return { + from: first.from, + to: second.to, + exec: function(data, conversionContext) { + var intermediate = first.exec(data, conversionContext); + return second.exec(intermediate, conversionContext); + } + }; +} + +/** + * This is where we cache the converters that we know about + */ +var converters = { + from: {} +}; + +/** + * Add a new converter to the cache + */ +exports.addConverter = function(converter) { + var fromMatch = converters.from[converter.from]; + if (fromMatch == null) { + fromMatch = {}; + converters.from[converter.from] = fromMatch; + } + + fromMatch[converter.to] = converter; +}; + +/** + * Remove an existing converter from the cache + */ +exports.removeConverter = function(converter) { + var fromMatch = converters.from[converter.from]; + if (fromMatch == null) { + return; + } + + if (fromMatch[converter.to] === converter) { + fromMatch[converter.to] = null; + } +}; + +/** + * Work out the best converter that we've got, for a given conversion. + */ +function getConverter(from, to) { + var fromMatch = converters.from[from]; + if (fromMatch == null) { + return getFallbackConverter(from, to); + } + + var converter = fromMatch[to]; + if (converter == null) { + // Someone is going to love writing a graph search algorithm to work out + // the smallest number of conversions, or perhaps the least 'lossy' + // conversion but for now the only 2 step conversion is foo->view->dom, + // which we are going to special case. + if (to === 'dom') { + converter = fromMatch['view']; + if (converter != null) { + return getChainConverter(converter, viewDomConverter); + } + } + if (to === 'string') { + converter = fromMatch['view']; + if (converter != null) { + return getChainConverter(converter, viewStringConverter); + } + } + return getFallbackConverter(from, to); + } + return converter; +} + +/** + * Helper for getConverter to pick the best fallback converter + */ +function getFallbackConverter(from, to) { + console.error('No converter from ' + from + ' to ' + to + '. Using fallback'); + + if (to === 'dom') { + return fallbackDomConverter; + } + + if (to === 'string') { + return fallbackStringConverter; + } + + throw new Error('No conversion possible from ' + from + ' to ' + to + '.'); +} + +/** + * Convert some data from one type to another + * @param data The object to convert + * @param from The type of the data right now + * @param to The type that we would like the data in + * @param conversionContext An execution context (i.e. simplified requisition) which is + * often required for access to a document, or createView function + */ +exports.convert = function(data, from, to, conversionContext) { + if (from === to) { + return promise.resolve(data); + } + return promise.resolve(getConverter(from, to).exec(data, conversionContext)); +}; + +/** + * Items for export + */ +exports.items = [ viewDomConverter, viewStringConverter ]; + + +}); +/* + * Copyright 2012, Mozilla Foundation and contributors + * + * Licensed under the Apache License, Version 2.0 (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.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +define('gcli/ui/fields', ['require', 'exports', 'module' , 'util/promise', 'util/util'], function(require, exports, module) { + +'use strict'; + +var promise = require('util/promise'); +var util = require('util/util'); +var KeyEvent = require('util/util').KeyEvent; + +/** + * A Field is a way to get input for a single parameter. + * This class is designed to be inherited from. It's important that all + * subclasses have a similar constructor signature because they are created + * via getField(...) + * @param type The type to use in conversions + * @param options A set of properties to help fields configure themselves: + * - document: The document we use in calling createElement + * - named: Is this parameter named? That is to say, are positional + * arguments disallowed, if true, then we need to provide updates to + * the command line that explicitly name the parameter in use + * (e.g. --verbose, or --name Fred rather than just true or Fred) + * - name: If this parameter is named, what name should we use + * - requisition: The requisition that we're attached to + * - required: Boolean to indicate if this is a mandatory field + */ +function Field(type, options) { + this.type = type; + this.document = options.document; + this.requisition = options.requisition; +} + +/** + * Enable registration of fields using addItems + */ +Field.prototype.item = 'field'; + +/** + * Subclasses should assign their element with the DOM node that gets added + * to the 'form'. It doesn't have to be an input node, just something that + * contains it. + */ +Field.prototype.element = undefined; + +/** + * Indicates that this field should drop any resources that it has created + */ +Field.prototype.destroy = function() { + delete this.messageElement; +}; + +// Note: We could/should probably change Fields from working with Conversions +// to working with Arguments (Tokens), which makes for less calls to parse() + +/** + * Update this field display with the value from this conversion. + * Subclasses should provide an implementation of this function. + */ +Field.prototype.setConversion = function(conversion) { + throw new Error('Field should not be used directly'); +}; + +/** + * Extract a conversion from the values in this field. + * Subclasses should provide an implementation of this function. + */ +Field.prototype.getConversion = function() { + throw new Error('Field should not be used directly'); +}; + +/** + * Set the element where messages and validation errors will be displayed + * @see setMessage() + */ +Field.prototype.setMessageElement = function(element) { + this.messageElement = element; +}; + +/** + * Display a validation message in the UI + */ +Field.prototype.setMessage = function(message) { + if (this.messageElement) { + util.setTextContent(this.messageElement, message || ''); + } +}; + +/** + * Method to be called by subclasses when their input changes, which allows us + * to properly pass on the onFieldChange event. + */ +Field.prototype.onInputChange = function(ev) { + promise.resolve(this.getConversion()).then(function(conversion) { + this.onFieldChange({ conversion: conversion }); + this.setMessage(conversion.message); + + if (ev.keyCode === KeyEvent.DOM_VK_RETURN) { + this.requisition.exec(); + } + }.bind(this), util.errorHandler); +}; + +/** + * Some fields contain information that is more important to the user, for + * example error messages and completion menus. + */ +Field.prototype.isImportant = false; + +/** + * 'static/abstract' method to allow implementations of Field to lay a claim + * to a type. This allows claims of various strength to be weighted up. + * See the Field.*MATCH values. + */ +Field.claim = function(type, context) { + throw new Error('Field should not be used directly'); +}; + +/** + * About minimalism - If we're producing a dialog, we want a field for every + * parameter. If we're providing a quick tooltip, we only want a field when + * it's really going to help. + * The getField() function takes an option of 'tooltip: true'. Fields are + * expected to reply with a TOOLTIP_* constant if they should be shown in the + * tooltip case. + */ +Field.TOOLTIP_MATCH = 5; // A best match, that works for a tooltip +Field.TOOLTIP_DEFAULT = 4; // A default match that should show in a tooltip +Field.MATCH = 3; // Match, but ignorable if we're being minimalist +Field.DEFAULT = 2; // This is a default (non-minimalist) match +Field.BASIC = 1; // OK in an emergency. i.e. assume Strings +Field.NO_MATCH = 0; // This field can't help with the given type + +exports.Field = Field; + + +/** + * Internal array of known fields + */ +var fieldCtors = []; + +/** + * Add a field definition by field constructor + * @param fieldCtor Constructor function of new Field + */ +exports.addField = function(fieldCtor) { + if (typeof fieldCtor !== 'function') { + console.error('addField erroring on ', fieldCtor); + throw new Error('addField requires a Field constructor'); + } + fieldCtors.push(fieldCtor); +}; + +/** + * Remove a Field definition + * @param field A previously registered field, specified either with a field + * name or from the field name + */ +exports.removeField = function(field) { + if (typeof field !== 'string') { + fields = fields.filter(function(test) { + return test !== field; + }); + delete fields[field]; + } + else if (field instanceof Field) { + removeField(field.name); + } + else { + console.error('removeField erroring on ', field); + throw new Error('removeField requires an instance of Field'); + } +}; + +/** + * Find the best possible matching field from the specification of the type + * of field required. + * @param type An instance of Type that we will represent + * @param options A set of properties that we should attempt to match, and use + * in the construction of the new field object: + * - document: The document to use in creating new elements + * - name: The parameter name, (i.e. assignment.param.name) + * - requisition: The requisition we're monitoring, + * - required: Is this a required parameter (i.e. param.isDataRequired) + * - named: Is this a named parameters (i.e. !param.isPositionalAllowed) + * @return A newly constructed field that best matches the input options + */ +exports.getField = function(type, options) { + var ctor; + var highestClaim = -1; + fieldCtors.forEach(function(fieldCtor) { + var claim = fieldCtor.claim(type, options.requisition.executionContext); + if (claim > highestClaim) { + highestClaim = claim; + ctor = fieldCtor; + } + }); + + if (!ctor) { + console.error('Unknown field type ', type, ' in ', fieldCtors); + throw new Error('Can\'t find field for ' + type); + } + + if (options.tooltip && highestClaim < Field.TOOLTIP_DEFAULT) { + return new BlankField(type, options); + } + + return new ctor(type, options); +}; + + +/** + * For use with delegate types that do not yet have anything to resolve to. + * BlankFields are not for general use. + */ +function BlankField(type, options) { + Field.call(this, type, options); + + this.element = util.createElement(this.document, 'div'); + + this.onFieldChange = util.createEvent('BlankField.onFieldChange'); +} + +BlankField.prototype = Object.create(Field.prototype); + +BlankField.claim = function(type, context) { + return type.name === 'blank' ? Field.MATCH : Field.NO_MATCH; +}; + +BlankField.prototype.setConversion = function(conversion) { + this.setMessage(conversion.message); +}; + +BlankField.prototype.getConversion = function() { + return this.type.parse(new Argument(), this.requisition.executionContext); +}; + +exports.addField(BlankField); + + +}); +/* + * Copyright 2012, Mozilla Foundation and contributors + * + * Licensed under the Apache License, Version 2.0 (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.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +define('gcli/types/selection', ['require', 'exports', 'module' , 'util/promise', 'util/util', 'util/l10n', 'util/spell', 'gcli/types', 'gcli/argument'], function(require, exports, module) { 'use strict'; var promise = require('util/promise'); var util = require('util/util'); var l10n = require('util/l10n'); -var types = require('gcli/types'); +var spell = require('util/spell'); var Type = require('gcli/types').Type; var Status = require('gcli/types').Status; var Conversion = require('gcli/types').Conversion; -var spell = require('gcli/types/spell'); var BlankArgument = require('gcli/argument').BlankArgument; /** - * Registration and de-registration. - */ -exports.startup = function() { - types.addType(SelectionType); -}; - -exports.shutdown = function() { - types.removeType(SelectionType); -}; - - -/** * A selection allows the user to pick a value from known set of options. * An option is made up of a name (which is what the user types) and a value * (which is passed to exec) * @param typeSpec Object containing properties that describe how this * selection functions. Properties include: * - lookup: An array of objects, one for each option, which contain name and * value properties. lookup can be a function which returns this array * - data: An array of strings - alternative to 'lookup' where the valid values @@ -3070,16 +4083,17 @@ SelectionType.prototype._findValue = fun * SelectionType is designed to be inherited from, so SelectionField needs a way * to check if something works like a selection without using 'name' */ SelectionType.prototype.isSelection = true; SelectionType.prototype.name = 'selection'; exports.SelectionType = SelectionType; +exports.items = [ SelectionType ]; }); /* * Copyright 2012, Mozilla Foundation and contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -3089,35 +4103,37 @@ exports.SelectionType = SelectionType; * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ -define('gcli/types/spell', ['require', 'exports', 'module' ], function(require, exports, module) { +define('util/spell', ['require', 'exports', 'module' ], function(require, exports, module) { 'use strict'; /* * A spell-checker based on Damerau-Levenshtein distance. */ -var INSERTION_COST = 1; -var DELETION_COST = 1; -var SWAP_COST = 1; -var SUBSTITUTION_COST = 2; -var MAX_EDIT_DISTANCE = 4; - -/** - * Compute Damerau-Levenshtein Distance +var CASE_CHANGE_COST = 1; +var INSERTION_COST = 10; +var DELETION_COST = 10; +var SWAP_COST = 10; +var SUBSTITUTION_COST = 20; +var MAX_EDIT_DISTANCE = 40; + +/** + * Compute Damerau-Levenshtein Distance, with a modification to allow a low + * case-change cost (1/10th of a swap-cost) * @see http://en.wikipedia.org/wiki/Damerau%E2%80%93Levenshtein_distance */ -function damerauLevenshteinDistance(wordi, wordj) { +var distance = exports.distance = function(wordi, wordj) { var wordiLen = wordi.length; var wordjLen = wordj.length; // We only need to store three rows of our dynamic programming matrix. // (Without swap, it would have been two.) var row0 = new Array(wordiLen+1); var row1 = new Array(wordiLen+1); var row2 = new Array(wordiLen+1); @@ -3128,239 +4144,157 @@ function damerauLevenshteinDistance(word // The distance between the empty string and a string of size i is the cost // of i insertions. for (i = 0; i <= wordiLen; i++) { row1[i] = i * INSERTION_COST; } // Row-by-row, we're computing the edit distance between substrings wordi[0..i] // and wordj[0..j]. - for (j = 1; j <= wordjLen; j++) - { + for (j = 1; j <= wordjLen; j++) { // Edit distance between wordi[0..0] and wordj[0..j] is the cost of j // insertions. row0[0] = j * INSERTION_COST; for (i = 1; i <= wordiLen; i++) { // Handle deletion, insertion and substitution: we can reach each cell // from three other cells corresponding to those three operations. We // want the minimum cost. - row0[i] = Math.min( - row0[i-1] + DELETION_COST, - row1[i] + INSERTION_COST, - row1[i-1] + (wordi[i-1] === wordj[j-1] ? 0 : SUBSTITUTION_COST)); + var dc = row0[i - 1] + DELETION_COST; + var ic = row1[i] + INSERTION_COST; + var sc0; + if (wordi[i-1] === wordj[j-1]) { + sc0 = 0; + } + else { + if (wordi[i-1].toLowerCase() === wordj[j-1].toLowerCase()) { + sc0 = CASE_CHANGE_COST; + } + else { + sc0 = SUBSTITUTION_COST; + } + } + var sc = row1[i-1] + sc0; + + row0[i] = Math.min(dc, ic, sc); + // We handle swap too, eg. distance between help and hlep should be 1. If // we find such a swap, there's a chance to update row0[1] to be lower. if (i > 1 && j > 1 && wordi[i-1] === wordj[j-2] && wordj[j-1] === wordi[i-2]) { row0[i] = Math.min(row0[i], row2[i-2] + SWAP_COST); } } tmp = row2; row2 = row1; row1 = row0; row0 = tmp; } return row1[wordiLen]; -} +}; + +/** + * As distance() except that we say that if word is a prefix of name then we + * only count the case changes. This allows us to use words that can be + * completed by typing as more likely than short words + */ +var distancePrefix = exports.distancePrefix = function(word, name) { + var dist = 0; + + for (var i = 0; i < word.length; i++) { + if (name[i] !== word[i]) { + if (name[i].toLowerCase() === word[i].toLowerCase()) { + dist++; + } + else { + // name does not start with word, even ignoring case, use + // Damerau-Levenshtein + return exports.distance(word, name); + } + } + } + + return dist; +}; /** * A function that returns the correction for the specified word. */ exports.correct = function(word, names) { if (names.length === 0) { return undefined; } - var distance = {}; + var distances = {}; var sortedCandidates; names.forEach(function(candidate) { - distance[candidate] = damerauLevenshteinDistance(word, candidate); + distances[candidate] = exports.distance(word, candidate); }); sortedCandidates = names.sort(function(worda, wordb) { - if (distance[worda] !== distance[wordb]) { - return distance[worda] - distance[wordb]; + if (distances[worda] !== distances[wordb]) { + return distances[worda] - distances[wordb]; } else { // if the score is the same, always return the first string // in the lexicographical order return worda < wordb; } }); - if (distance[sortedCandidates[0]] <= MAX_EDIT_DISTANCE) { + if (distances[sortedCandidates[0]] <= MAX_EDIT_DISTANCE) { return sortedCandidates[0]; } else { return undefined; } }; - -}); -/* - * Copyright 2012, Mozilla Foundation and contributors - * - * Licensed under the Apache License, Version 2.0 (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.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -define('gcli/types/command', ['require', 'exports', 'module' , 'util/promise', 'util/l10n', 'gcli/canon', 'gcli/types', 'gcli/types/selection'], function(require, exports, module) { - -'use strict'; - -var promise = require('util/promise'); -var l10n = require('util/l10n'); -var canon = require('gcli/canon'); -var types = require('gcli/types'); -var SelectionType = require('gcli/types/selection').SelectionType; -var Status = require('gcli/types').Status; -var Conversion = require('gcli/types').Conversion; - - -/** - * Registration and de-registration. - */ -exports.startup = function() { - types.addType(CommandType); - types.addType(ParamType); -}; - -exports.shutdown = function() { - types.removeType(CommandType); - types.removeType(ParamType); -}; - - -/** - * Select from the available commands. - * This is very similar to a SelectionType, however the level of hackery in - * SelectionType to make it handle Commands correctly was to high, so we - * simplified. - * If you are making changes to this code, you should check there too. - */ -function ParamType(typeSpec) { - this.requisition = typeSpec.requisition; - this.isIncompleteName = typeSpec.isIncompleteName; - this.stringifyProperty = 'name'; - this.neverForceAsync = true; -} - -ParamType.prototype = Object.create(SelectionType.prototype); - -ParamType.prototype.name = 'param'; - -ParamType.prototype.lookup = function() { - var displayedParams = []; - var command = this.requisition.commandAssignment.value; - if (command != null) { - command.params.forEach(function(param) { - var arg = this.requisition.getAssignment(param.name).arg; - if (!param.isPositionalAllowed && arg.type === "BlankArgument") { - displayedParams.push({ name: '--' + param.name, value: param }); - } - }, this); - } - return displayedParams; -}; - -ParamType.prototype.parse = function(arg, context) { - if (this.isIncompleteName) { - return SelectionType.prototype.parse.call(this, arg, context); - } - else { - var message = l10n.lookup('cliUnusedArg'); - return promise.resolve(new Conversion(undefined, arg, Status.ERROR, message)); - } -}; - - -/** - * Select from the available commands. - * This is very similar to a SelectionType, however the level of hackery in - * SelectionType to make it handle Commands correctly was to high, so we - * simplified. - * If you are making changes to this code, you should check there too. - */ -function CommandType() { - this.stringifyProperty = 'name'; - this.neverForceAsync = true; -} - -CommandType.prototype = Object.create(SelectionType.prototype); - -CommandType.prototype.name = 'command'; - -CommandType.prototype.lookup = function() { - var commands = canon.getCommands(); - commands.sort(function(c1, c2) { - return c1.name.localeCompare(c2.name); +/** + * Return a ranked list of matches: + * + * spell.rank('fred', [ 'banana', 'fred', 'ed', 'red' ]); + * ↓ + * [ + * { name: 'fred', dist: 0 }, + * { name: 'red', dist: 1 }, + * { name: 'ed', dist: 2 }, + * { name: 'banana', dist: 10 }, + * ] + * + * @param word The string that we're comparing names against + * @param names An array of strings to compare word against + * @param options Comparison options: + * - noSort: Do not sort the output by distance + * - prefixZero: Count prefix matches as edit distance 0 (i.e. word='bana' and + * names=['banana'], would return { name:'banana': dist: 0 }) This is useful + * if someone is typing the matches and may not have finished yet + */ +exports.rank = function(word, names, options) { + options = options || {}; + + var reply = names.map(function(name) { + // If any name starts with the word then the distance is based on the + // number of case changes rather than Damerau-Levenshtein + var algo = options.prefixZero ? distancePrefix : distance; + return { + name: name, + dist: algo(word, name) + }; }); - return commands.map(function(command) { - return { name: command.name, value: command }; - }, this); -}; - -/** - * Add an option to our list of predicted options - */ -CommandType.prototype._addToPredictions = function(predictions, option, arg) { - // The command type needs to exclude sub-commands when the CLI - // is blank, but include them when we're filtering. This hack - // excludes matches when the filter text is '' and when the - // name includes a space. - if (arg.text.length !== 0 || option.name.indexOf(' ') === -1) { - predictions.push(option); - } -}; - -CommandType.prototype.parse = function(arg, context) { - // Especially at startup, predictions live over the time that things change - // so we provide a completion function rather than completion values - var predictFunc = function() { - return this._findPredictions(arg); - }.bind(this); - - return this._findPredictions(arg).then(function(predictions) { - if (predictions.length === 0) { - var msg = l10n.lookupFormat('typesSelectionNomatch', [ arg.text ]); - return new Conversion(undefined, arg, Status.ERROR, msg, predictFunc); - } - - var command = predictions[0].value; - - if (predictions.length === 1) { - // Is it an exact match of an executable command, - // or just the only possibility? - if (command.name === arg.text && typeof command.exec === 'function') { - return new Conversion(command, arg, Status.VALID, ''); - } - - return new Conversion(undefined, arg, Status.INCOMPLETE, '', predictFunc); - } - - // It's valid if the text matches, even if there are several options - if (predictions[0].name === arg.text) { - return new Conversion(command, arg, Status.VALID, '', predictFunc); - } - - return new Conversion(undefined, arg, Status.INCOMPLETE, '', predictFunc); - }.bind(this)); + + if (!options.noSort) { + reply = reply.sort(function(d1, d2) { + return d1.dist - d2.dist; + }); + } + + return reply; }; }); /* * Copyright 2012, Mozilla Foundation and contributors * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -3371,551 +4305,401 @@ CommandType.prototype.parse = function(a * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ -define('gcli/canon', ['require', 'exports', 'module' , 'util/promise', 'util/util', 'util/l10n', 'gcli/types'], function(require, exports, module) { +define('gcli/types/delegate', ['require', 'exports', 'module' , 'util/promise', 'gcli/types'], function(require, exports, module) { + +'use strict'; + +var promise = require('util/promise'); +var Conversion = require('gcli/types').Conversion; + +/** + * A type for "we don't know right now, but hope to soon" + */ +var delegate = { + item: 'type', + name: 'delegate', + + constructor: function() { + if (typeof this.delegateType !== 'function') { + throw new Error('Instances of DelegateType need typeSpec.delegateType' + + ' to be a function that returns a type'); + } + }, + + // Child types should implement this method to return an instance of the type + // that should be used. If no type is available, or some sort of temporary + // placeholder is required, BlankType can be used. + delegateType: function(context) { + throw new Error('Not implemented'); + }, + + stringify: function(value, context) { + return this.delegateType(context).stringify(value, context); + }, + + parse: function(arg, context) { + return this.delegateType(context).parse(arg, context); + }, + + decrement: function(value, context) { + var delegated = this.delegateType(context); + return (delegated.decrement ? delegated.decrement(value, context) : undefined); + }, + + increment: function(value, context) { + var delegated = this.delegateType(context); + return (delegated.increment ? delegated.increment(value, context) : undefined); + }, + + getType: function(context) { + return this.delegateType(context); + }, + + // DelegateType is designed to be inherited from, so DelegateField needs a way + // to check if something works like a delegate without using 'name' + isDelegate: true, +}; + +Object.defineProperty(delegate, 'isImportant', { + get: function() { + return this.delegateType().isImportant; + }, + enumerable: true +}); + +/** + * 'blank' is a type for use with DelegateType when we don't know yet. + * It should not be used anywhere else. + */ +var blank = { + item: 'type', + name: 'blank', + + stringify: function(value, context) { + return ''; + }, + + parse: function(arg, context) { + return promise.resolve(new Conversion(undefined, arg)); + } +}; + +/** + * The types we expose for registration + */ +exports.items = [ delegate, blank ]; + + +}); +/* + * Copyright 2012, Mozilla Foundation and contributors + * + * Licensed under the Apache License, Version 2.0 (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.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +define('gcli/types/array', ['require', 'exports', 'module' , 'util/promise', 'gcli/types', 'gcli/argument'], function(require, exports, module) { + +'use strict'; + +var promise = require('util/promise'); +var types = require('gcli/types'); +var ArrayConversion = require('gcli/types').ArrayConversion; +var ArrayArgument = require('gcli/argument').ArrayArgument; + +exports.items = [ + { + // A set of objects of the same type + item: 'type', + name: 'array', + subtype: undefined, + + constructor: function() { + if (!this.subtype) { + console.error('Array.typeSpec is missing subtype. Assuming string.' + + this.name); + this.subtype = 'string'; + } + this.subtype = types.createType(this.subtype); + }, + + stringify: function(values, context) { + if (values == null) { + return ''; + } + // BUG 664204: Check for strings with spaces and add quotes + return values.join(' '); + }, + + parse: function(arg, context) { + if (arg.type !== 'ArrayArgument') { + console.error('non ArrayArgument to ArrayType.parse', arg); + throw new Error('non ArrayArgument to ArrayType.parse'); + } + + // Parse an argument to a conversion + // Hack alert. ArrayConversion needs to be able to answer questions about + // the status of individual conversions in addition to the overall state. + // |subArg.conversion| allows us to do that easily. + var subArgParse = function(subArg) { + return this.subtype.parse(subArg, context).then(function(conversion) { + subArg.conversion = conversion; + return conversion; + }.bind(this)); + }.bind(this); + + var conversionPromises = arg.getArguments().map(subArgParse); + return promise.all(conversionPromises).then(function(conversions) { + return new ArrayConversion(conversions, arg); + }); + }, + + getBlank: function() { + return new ArrayConversion([], new ArrayArgument()); + } + }, +]; + + +}); +/* + * Copyright 2012, Mozilla Foundation and contributors + * + * Licensed under the Apache License, Version 2.0 (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.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +define('gcli/types/boolean', ['require', 'exports', 'module' , 'util/promise', 'gcli/types', 'gcli/types/selection', 'gcli/argument'], function(require, exports, module) { 'use strict'; var promise = require('util/promise'); -var util = require('util/util'); -var l10n = require('util/l10n'); - -var types = require('gcli/types'); var Status = require('gcli/types').Status; - -/** - * Implement the localization algorithm for any documentation objects (i.e. - * description and manual) in a command. - * @param data The data assigned to a description or manual property - * @param onUndefined If data == null, should we return the data untouched or - * lookup a 'we don't know' key in it's place. - */ -function lookup(data, onUndefined) { - if (data == null) { - if (onUndefined) { - return l10n.lookup(onUndefined); - } - - return data; - } - - if (typeof data === 'string') { - return data; - } - - if (typeof data === 'object') { - if (data.key) { - return l10n.lookup(data.key); - } - - var locales = l10n.getPreferredLocales(); - var translated; - locales.some(function(locale) { - translated = data[locale]; - return translated != null; - }); - if (translated != null) { - return translated; - } - - console.error('Can\'t find locale in descriptions: ' + - 'locales=' + JSON.stringify(locales) + ', ' + - 'description=' + JSON.stringify(data)); - return '(No description)'; - } - - return l10n.lookup(onUndefined); -} - - -/** - * The command object is mostly just setup around a commandSpec (as passed to - * #addCommand()). - */ -function Command(commandSpec) { - Object.keys(commandSpec).forEach(function(key) { - this[key] = commandSpec[key]; - }, this); - - if (!this.name) { - throw new Error('All registered commands must have a name'); - } - - if (this.params == null) { - this.params = []; - } - if (!Array.isArray(this.params)) { - throw new Error('command.params must be an array in ' + this.name); - } - - this.hasNamedParameters = false; - this.description = 'description' in this ? this.description : undefined; - this.description = lookup(this.description, 'canonDescNone'); - this.manual = 'manual' in this ? this.manual : undefined; - this.manual = lookup(this.manual); - - // At this point this.params has nested param groups. We want to flatten it - // out and replace the param object literals with Parameter objects - var paramSpecs = this.params; - this.params = []; - - // Track if the user is trying to mix default params and param groups. - // All the non-grouped parameters must come before all the param groups - // because non-grouped parameters can be assigned positionally, so their - // index is important. We don't want 'holes' in the order caused by - // parameter groups. - var usingGroups = false; - - // In theory this could easily be made recursive, so param groups could - // contain nested param groups. Current thinking is that the added - // complexity for the UI probably isn't worth it, so this implementation - // prevents nesting. - paramSpecs.forEach(function(spec) { - if (!spec.group) { - var param = new Parameter(spec, this, null); - this.params.push(param); - - if (!param.isPositionalAllowed) { - this.hasNamedParameters = true; - } - - if (usingGroups && param.groupName == null) { - throw new Error('Parameters can\'t come after param groups.' + - ' Ignoring ' + this.name + '/' + spec.name); - } - - if (param.groupName != null) { - usingGroups = true; - } - } - else { - spec.params.forEach(function(ispec) { - var param = new Parameter(ispec, this, spec.group); - this.params.push(param); - - if (!param.isPositionalAllowed) { - this.hasNamedParameters = true; - } - }, this); - - usingGroups = true; - } - }, this); -} - -exports.Command = Command; - - -/** - * A wrapper for a paramSpec so we can sort out shortened versions names for - * option switches - */ -function Parameter(paramSpec, command, groupName) { - this.command = command || { name: 'unnamed' }; - this.paramSpec = paramSpec; - this.name = this.paramSpec.name; - this.type = this.paramSpec.type; - - this.groupName = groupName; - if (this.groupName != null) { - if (this.paramSpec.option != null) { - throw new Error('Can\'t have a "option" property in a nested parameter'); - } - } - else { - if (this.paramSpec.option != null) { - this.groupName = this.paramSpec.option === true ? - l10n.lookup('canonDefaultGroupName') : - '' + this.paramSpec.option; - } - } - - if (!this.name) { - throw new Error('In ' + this.command.name + - ': all params must have a name'); - } - - var typeSpec = this.type; - this.type = types.createType(typeSpec); - if (this.type == null) { - console.error('Known types: ' + types.getTypeNames().join(', ')); - throw new Error('In ' + this.command.name + '/' + this.name + - ': can\'t find type for: ' + JSON.stringify(typeSpec)); - } - - // boolean parameters have an implicit defaultValue:false, which should - // not be changed. See the docs. - if (this.type.name === 'boolean' && - this.paramSpec.defaultValue !== undefined) { - throw new Error('In ' + this.command.name + '/' + this.name + - ': boolean parameters can not have a defaultValue.' + - ' Ignoring'); - } - - // Check the defaultValue for validity. - // Both undefined and null get a pass on this test. undefined is used when - // there is no defaultValue, and null is used when the parameter is - // optional, neither are required to parse and stringify. - if (this._defaultValue != null) { - try { - // Passing null in for a context is bound to get us into trouble some day - // in which case we'll need to mock one up in some way - var context = null; - var defaultText = this.type.stringify(this.paramSpec.defaultValue, context); - var parsed = this.type.parseString(defaultText, context); - parsed.then(function(defaultConversion) { - if (defaultConversion.getStatus() !== Status.VALID) { - console.error('In ' + this.command.name + '/' + this.name + - ': Error round tripping defaultValue. status = ' + - defaultConversion.getStatus()); - } - }.bind(this), util.errorHandler); - } - catch (ex) { - throw new Error('In ' + this.command.name + '/' + this.name + ': ' + ex); - } - } - - // All parameters that can only be set via a named parameter must have a - // non-undefined default value - if (!this.isPositionalAllowed && this.paramSpec.defaultValue === undefined && - this.type.getBlank == null && !(this.type.name === 'boolean')) { - throw new Error('In ' + this.command.name + '/' + this.name + - ': Missing defaultValue for optional parameter.'); - } -} - -/** - * type.getBlank can be expensive, so we delay execution where we can - */ -Object.defineProperty(Parameter.prototype, 'defaultValue', { - get: function() { - if (!('_defaultValue' in this)) { - this._defaultValue = (this.paramSpec.defaultValue !== undefined) ? - this.paramSpec.defaultValue : - this.type.getBlank().value; - } - - return this._defaultValue; - }, - enumerable : true -}); - -/** - * Does the given name uniquely identify this param (among the other params - * in this command) - * @param name The name to check - */ -Parameter.prototype.isKnownAs = function(name) { - if (name === '--' + this.name) { - return true; - } - return false; -}; - -/** - * Resolve the manual for this parameter, by looking in the paramSpec - * and doing a l10n lookup - */ -Object.defineProperty(Parameter.prototype, 'manual', { - get: function() { - return lookup(this.paramSpec.manual || undefined); - }, - enumerable: true -}); - -/** - * Resolve the description for this parameter, by looking in the paramSpec - * and doing a l10n lookup - */ -Object.defineProperty(Parameter.prototype, 'description', { - get: function() { - return lookup(this.paramSpec.description || undefined, 'canonDescNone'); - }, - enumerable: true -}); - -/** - * Is the user required to enter data for this parameter? (i.e. has - * defaultValue been set to something other than undefined) - */ -Object.defineProperty(Parameter.prototype, 'isDataRequired', { - get: function() { - return this.defaultValue === undefined; - }, - enumerable: true -}); - -/** - * Reflect the paramSpec 'hidden' property (dynamically so it can change) - */ -Object.defineProperty(Parameter.prototype, 'hidden', { - get: function() { - return this.paramSpec.hidden; - }, - enumerable: true -}); - -/** - * Are we allowed to assign data to this parameter using positional - * parameters? - */ -Object.defineProperty(Parameter.prototype, 'isPositionalAllowed', { - get: function() { - return this.groupName == null; - }, - enumerable: true -}); - -exports.Parameter = Parameter; - - -/** - * A canon is a store for a list of commands - */ -function Canon() { - // A lookup hash of our registered commands - this._commands = {}; - // A sorted list of command names, we regularly want them in order, so pre-sort - this._commandNames = []; - // A lookup of the original commandSpecs by command name - this._commandSpecs = {}; - - // Enable people to be notified of changes to the list of commands - this.onCanonChange = util.createEvent('canon.onCanonChange'); -} - -/** - * Add a command to the canon of known commands. - * This function is exposed to the outside world (via gcli/index). It is - * documented in docs/index.md for all the world to see. - * @param commandSpec The command and its metadata. - * @return The new command - */ -Canon.prototype.addCommand = function(commandSpec) { - if (this._commands[commandSpec.name] != null) { - // Roughly canon.removeCommand() without the event call, which we do later - delete this._commands[commandSpec.name]; - this._commandNames = this._commandNames.filter(function(test) { - return test !== commandSpec.name; - }); - } - - var command = new Command(commandSpec); - this._commands[commandSpec.name] = command; - this._commandNames.push(commandSpec.name); - this._commandNames.sort(); - - this._commandSpecs[commandSpec.name] = commandSpec; - - this.onCanonChange(); - return command; -}; - -/** - * Remove an individual command. The opposite of #addCommand(). - * Removing a non-existent command is a no-op. - * @param commandOrName Either a command name or the command itself. - * @return true if a command was removed, false otherwise. - */ -Canon.prototype.removeCommand = function(commandOrName) { - var name = typeof commandOrName === 'string' ? - commandOrName : - commandOrName.name; - - if (!this._commands[name]) { - return false; - } - - // See start of canon.addCommand if changing this code - delete this._commands[name]; - delete this._commandSpecs[name]; - this._commandNames = this._commandNames.filter(function(test) { - return test !== name; - }); - - this.onCanonChange(); - return true; -}; - -/** - * Retrieve a command by name - * @param name The name of the command to retrieve - */ -Canon.prototype.getCommand = function(name) { - // '|| undefined' is to silence 'reference to undefined property' warnings - return this._commands[name] || undefined; -}; - -/** - * Get an array of all the registered commands. - */ -Canon.prototype.getCommands = function() { - return Object.keys(this._commands).map(function(name) { - return this._commands[name]; - }, this); -}; - -/** - * Get an array containing the names of the registered commands. - */ -Canon.prototype.getCommandNames = function() { - return this._commandNames.slice(0); -}; - -/** - * Get access to the stored commandMetaDatas (i.e. before they were made into - * instances of Command/Parameters) so we can remote them. - */ -Canon.prototype.getCommandSpecs = function() { - var specs = {}; - - Object.keys(this._commandSpecs).forEach(function(name) { - var spec = this._commandSpecs[name]; - if (spec.exec == null) { - spec.isParent = true; - } - specs[name] = spec; - }.bind(this)); - - return specs; -}; - -/** - * Add a set of commands that are executed somewhere else. - * @param prefix The name prefix that we assign to all command names - * @param commandSpecs Presumably as obtained from getCommandSpecs on remote - * @param remoter Function to call on exec of a new remote command. This is - * defined just like an exec function (i.e. that takes args/context as params - * and returns a promise) with one extra feature, that the context includes a - * 'commandName' property that contains the original command name. - * @param to URL-like string that describes where the commands are executed. - * This is to complete the parent command description. - */ -Canon.prototype.addProxyCommands = function(prefix, commandSpecs, remoter, to) { - var names = Object.keys(commandSpecs); - - if (this._commands[prefix] != null) { - throw new Error(l10n.lookupFormat('canonProxyExists', [ prefix ])); - } - - // We need to add the parent command so all the commands from the other - // system have a parent - this.addCommand({ - name: prefix, - isProxy: true, - description: l10n.lookupFormat('canonProxyDesc', [ to ]), - manual: l10n.lookupFormat('canonProxyManual', [ to ]) - }); - - names.forEach(function(name) { - var commandSpec = commandSpecs[name]; - - if (commandSpec.noRemote) { - return; - } - - if (!commandSpec.isParent) { - commandSpec.exec = function(args, context) { - context.commandName = name; - return remoter(args, context); - }.bind(this); - } - - commandSpec.name = prefix + ' ' + commandSpec.name; - commandSpec.isProxy = true; - this.addCommand(commandSpec); - }.bind(this)); -}; - -/** - * Add a set of commands that are executed somewhere else. - * @param prefix The name prefix that we assign to all command names - * @param commandSpecs Presumably as obtained from getCommandSpecs on remote - * @param remoter Function to call on exec of a new remote command. This is - * defined just like an exec function (i.e. that takes args/context as params - * and returns a promise) with one extra feature, that the context includes a - * 'commandName' property that contains the original command name. - * @param to URL-like string that describes where the commands are executed. - * This is to complete the parent command description. - */ -Canon.prototype.removeProxyCommands = function(prefix) { - var toRemove = []; - Object.keys(this._commandSpecs).forEach(function(name) { - if (name.indexOf(prefix) === 0) { - toRemove.push(name); - } - }.bind(this)); - - var removed = []; - toRemove.forEach(function(name) { - var command = this.getCommand(name); - if (command.isProxy) { - this.removeCommand(name); - removed.push(name); - } - else { - console.error('Skipping removal of \'' + name + - '\' because it is not a proxy command.'); - } - }.bind(this)); - - return removed; -}; - -var canon = new Canon(); - -exports.Canon = Canon; -exports.addCommand = canon.addCommand.bind(canon); -exports.removeCommand = canon.removeCommand.bind(canon); -exports.onCanonChange = canon.onCanonChange; -exports.getCommands = canon.getCommands.bind(canon); -exports.getCommand = canon.getCommand.bind(canon); -exports.getCommandNames = canon.getCommandNames.bind(canon); -exports.getCommandSpecs = canon.getCommandSpecs.bind(canon); -exports.addProxyCommands = canon.addProxyCommands.bind(canon); -exports.removeProxyCommands = canon.removeProxyCommands.bind(canon); - -/** - * CommandOutputManager stores the output objects generated by executed - * commands. - * - * CommandOutputManager is exposed to the the outside world and could (but - * shouldn't) be used before gcli.startup() has been called. - * This could should be defensive to that where possible, and we should - * certainly document if the use of it or similar will fail if used too soon. - */ -function CommandOutputManager() { - this.onOutput = util.createEvent('CommandOutputManager.onOutput'); -} - -exports.CommandOutputManager = CommandOutputManager; +var Conversion = require('gcli/types').Conversion; +var SelectionType = require('gcli/types/selection').SelectionType; + +var BlankArgument = require('gcli/argument').BlankArgument; + +exports.items = [ + { + // 'boolean' type + item: 'type', + name: 'boolean', + parent: 'selection', + + lookup: [ + { name: 'false', value: false }, + { name: 'true', value: true } + ], + + parse: function(arg, context) { + if (arg.type === 'TrueNamedArgument') { + return promise.resolve(new Conversion(true, arg)); + } + if (arg.type === 'FalseNamedArgument') { + return promise.resolve(new Conversion(false, arg)); + } + return SelectionType.prototype.parse.call(this, arg, context); + }, + + stringify: function(value, context) { + if (value == null) { + return ''; + } + return '' + value; + }, + + getBlank: function(context) { + return new Conversion(false, new BlankArgument(), Status.VALID, '', + promise.resolve(this.lookup)); + } + } +]; }); /* - * Copyright 2009-2011 Mozilla Foundation and contributors - * Licensed under the New BSD license. See LICENSE.txt or: - * http://opensource.org/licenses/BSD-3-Clause + * Copyright 2012, Mozilla Foundation and contributors + * + * Licensed under the Apache License, Version 2.0 (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.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +define('gcli/types/command', ['require', 'exports', 'module' , 'util/promise', 'util/l10n', 'gcli/canon', 'gcli/types/selection', 'gcli/types'], function(require, exports, module) { + +'use strict'; + +var promise = require('util/promise'); +var l10n = require('util/l10n'); +var canon = require('gcli/canon'); +var SelectionType = require('gcli/types/selection').SelectionType; +var Status = require('gcli/types').Status; +var Conversion = require('gcli/types').Conversion; + +exports.items = [ + { + // Select from the available parameters to a command + item: 'type', + name: 'param', + parent: 'selection', + stringifyProperty: 'name', + neverForceAsync: true, + requisition: undefined, + isIncompleteName: undefined, + + lookup: function() { + var displayedParams = []; + var command = this.requisition.commandAssignment.value; + if (command != null) { + command.params.forEach(function(param) { + var arg = this.requisition.getAssignment(param.name).arg; + if (!param.isPositionalAllowed && arg.type === "BlankArgument") { + displayedParams.push({ name: '--' + param.name, value: param }); + } + }, this); + } + return displayedParams; + }, + + parse: function(arg, context) { + if (this.isIncompleteName) { + return SelectionType.prototype.parse.call(this, arg, context); + } + else { + var message = l10n.lookup('cliUnusedArg'); + return promise.resolve(new Conversion(undefined, arg, Status.ERROR, message)); + } + } + }, + { + // Select from the available commands + // This is very similar to a SelectionType, however the level of hackery in + // SelectionType to make it handle Commands correctly was to high, so we + // simplified. + // If you are making changes to this code, you should check there too. + item: 'type', + name: 'command', + parent: 'selection', + stringifyProperty: 'name', + neverForceAsync: true, + + lookup: function() { + var commands = canon.getCommands(); + commands.sort(function(c1, c2) { + return c1.name.localeCompare(c2.name); + }); + return commands.map(function(command) { + return { name: command.name, value: command }; + }, this); + }, + + // Add an option to our list of predicted options + _addToPredictions: function(predictions, option, arg) { + // The command type needs to exclude sub-commands when the CLI + // is blank, but include them when we're filtering. This hack + // excludes matches when the filter text is '' and when the + // name includes a space. + if (arg.text.length !== 0 || option.name.indexOf(' ') === -1) { + predictions.push(option); + } + }, + + parse: function(arg, context) { + // Especially at startup, predictions live over the time that things change + // so we provide a completion function rather than completion values + var predictFunc = function() { + return this._findPredictions(arg); + }.bind(this); + + return this._findPredictions(arg).then(function(predictions) { + if (predictions.length === 0) { + var msg = l10n.lookupFormat('typesSelectionNomatch', [ arg.text ]); + return new Conversion(undefined, arg, Status.ERROR, msg, predictFunc); + } + + var command = predictions[0].value; + + if (predictions.length === 1) { + // Is it an exact match of an executable command, + // or just the only possibility? + if (command.name === arg.text && typeof command.exec === 'function') { + return new Conversion(command, arg, Status.VALID, ''); + } + + return new Conversion(undefined, arg, Status.INCOMPLETE, '', predictFunc); + } + + // It's valid if the text matches, even if there are several options + if (predictions[0].name === arg.text) { + return new Conversion(command, arg, Status.VALID, '', predictFunc); + } + + return new Conversion(undefined, arg, Status.INCOMPLETE, '', predictFunc); + }.bind(this)); + } + } +]; + +}); +/* + * Copyright 2012, Mozilla Foundation and contributors + * + * Licensed under the Apache License, Version 2.0 (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.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. */ define('gcli/types/date', ['require', 'exports', 'module' , 'util/promise', 'util/l10n', 'gcli/types'], function(require, exports, module) { 'use strict'; var promise = require('util/promise'); var l10n = require('util/l10n'); - -var types = require('gcli/types'); var Type = require('gcli/types').Type; var Status = require('gcli/types').Status; var Conversion = require('gcli/types').Conversion; function DateType(typeSpec) { // ECMA 5.1 §15.9.1.1 // @see http://stackoverflow.com/questions/11526504/minimum-and-maximum-date @@ -4086,16 +4870,18 @@ DateType.prototype.increment = function( } else { return this.getMax(); } }; DateType.prototype.name = 'date'; +exports.items = [ DateType ]; + /** * Utility to convert a string to a date, throwing if the date can't be * parsed rather than having an invalid date */ function toDate(str) { var millis = Date.parse(str); if (isNaN(millis)) { @@ -4109,27 +4895,551 @@ function toDate(str) { * @see http://stackoverflow.com/questions/1353684/detecting-an-invalid-date-date-instance-in-javascript */ function isDate(thing) { return Object.prototype.toString.call(thing) === '[object Date]' && !isNaN(thing.getTime()); }; -/** - * Registration and de-registration. - */ -exports.startup = function() { - types.addType(DateType); -}; - -exports.shutdown = function() { - types.removeType(DateType); -}; - +}); +/* + * Copyright 2012, Mozilla Foundation and contributors + * + * Licensed under the Apache License, Version 2.0 (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.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +define('gcli/types/file', ['require', 'exports', 'module' , 'gcli/types/fileparser', 'gcli/types'], function(require, exports, module) { + +'use strict'; + +/* + * The file type is a bit of a spiders-web, but there isn't a nice solution + * yet. The core of the problem is that the modules used by Firefox and NodeJS + * intersect with the modules used by the web, but not each other. Except here. + * So we have to do something fancy to get the sharing but not mess up the web. + * + * This file requires 'gcli/types/fileparser', and there are 4 implementations + * of this: + * - '/lib/gcli/types/fileparser.js', the default web version that uses XHR to + * talk to the node server + * - '/lib/server/gcli/types/fileparser.js', an NodeJS stub, and ... + * - '/mozilla/gcli/types/fileparser.js', the Firefox implementation both of + * these are shims which import + * - 'util/fileparser', which does the real work, except the actual file access + * + * The file access comes from the 'util/filesystem' module, and there are 2 + * implementations of this: + * - '/lib/server/util/filesystem.js', which uses NodeJS APIs + * - '/mozilla/util/filesystem.js', which uses OS.File APIs + */ + +var fileparser = require('gcli/types/fileparser'); +var Conversion = require('gcli/types').Conversion; + +exports.items = [ + { + item: 'type', + name: 'file', + + filetype: 'any', // One of 'file', 'directory', 'any' + existing: 'maybe', // Should be one of 'yes', 'no', 'maybe' + matches: undefined, // RegExp to match the file part of the path + + isSelection: true, // It's not really a selection, but acts like one + + constructor: function() { + if (this.filetype !== 'any' && this.filetype !== 'file' && + this.filetype !== 'directory') { + throw new Error('filetype must be one of [any|file|directory]'); + } + + if (this.existing !== 'yes' && this.existing !== 'no' && + this.existing !== 'maybe') { + throw new Error('existing must be one of [yes|no|maybe]'); + } + }, + + stringify: function(file) { + if (file == null) { + return ''; + } + + return file.toString(); + }, + + parse: function(arg, context) { + var options = { + filetype: this.filetype, + existing: this.existing, + matches: this.matches + }; + var promise = fileparser.parse(arg.text, options); + + return promise.then(function(reply) { + return new Conversion(reply.value, arg, reply.status, + reply.message, reply.predictor); + }); + } + } +]; + +}); +/* + * Copyright 2012, Mozilla Foundation and contributors + * + * Licensed under the Apache License, Version 2.0 (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.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +define('gcli/types/fileparser', ['require', 'exports', 'module' , 'util/fileparser'], function(require, exports, module) { + +'use strict'; + +var fileparser = require('util/fileparser'); + +fileparser.supportsPredictions = false; +exports.parse = fileparser.parse; + +}); +/* + * Copyright 2012, Mozilla Foundation and contributors + * + * Licensed under the Apache License, Version 2.0 (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.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +define('util/fileparser', ['require', 'exports', 'module' , 'util/util', 'util/l10n', 'util/spell', 'util/filesystem', 'gcli/types'], function(require, exports, module) { + +'use strict'; + +var util = require('util/util'); +var l10n = require('util/l10n'); +var spell = require('util/spell'); +var filesystem = require('util/filesystem'); +var Status = require('gcli/types').Status; + +/* + * An implementation of the functions that call the filesystem, designed to + * support the file type. + * See: lib/gcli/util/filesystem.js + */ + +/** + * Helper for the parse() function from the file type. + * See util/filesystem.js for details + */ +exports.parse = function(typed, options) { + return filesystem.stat(typed).then(function(stats) { + // The 'save-as' case - the path should not exist but does + if (options.existing === 'no' && stats.exists) { + return { + value: undefined, + status: Status.INCOMPLETE, + message: l10n.lookupFormat('fileErrExists', [ typed ]), + predictor: undefined // No predictions that we can give here + }; + } + + if (stats.exists) { + // The path exists - check it's the correct file type ... + if (options.filetype === 'file' && !stats.isFile) { + return { + value: undefined, + status: Status.INCOMPLETE, + message: l10n.lookupFormat('fileErrIsNotFile', [ typed ]), + predictor: getPredictor(typed, options) + }; + } + + if (options.filetype === 'directory' && !stats.isDir) { + return { + value: undefined, + status: Status.INCOMPLETE, + message: l10n.lookupFormat('fileErrIsNotDirectory', [ typed ]), + predictor: getPredictor(typed, options) + }; + } + + // ... and that it matches any 'match' RegExp + if (options.matches != null && !options.matches.test(typed)) { + return { + value: undefined, + status: Status.INCOMPLETE, + message: l10n.lookupFormat('fileErrDoesntMatch', + [ typed, options.source ]), + predictor: getPredictor(typed, options) + }; + } + } + else { + if (options.existing === 'yes') { + // We wanted something that exists, but it doesn't. But we don't know + // if the path so far is an ERROR or just INCOMPLETE + var parentName = filesystem.dirname(typed); + return filesystem.stat(parentName).then(function(stats) { + return { + value: undefined, + status: stats.isDir ? Status.INCOMPLETE : Status.ERROR, + message: l10n.lookupFormat('fileErrNotExists', [ typed ]), + predictor: getPredictor(typed, options) + }; + }); + } + } + + // We found no problems + return { + value: typed, + status: Status.VALID, + message: undefined, + predictor: getPredictor(typed, options) + }; + }); +}; + +var RANK_OPTIONS = { noSort: true, prefixZero: true }; + +/** + * We want to be able to turn predictions off in Firefox + */ +exports.supportsPredictions = true; + +/** + * Get a function which creates predictions of files that match the given + * path + */ +function getPredictor(typed, options) { + if (!exports.supportsPredictions) { + return undefined; + } + + return function() { + var allowFile = (options.filetype !== 'directory'); + var parts = filesystem.split(typed); + + var absolute = (typed.indexOf('/') === 0); + var roots; + if (absolute) { + roots = [ { name: '/', dist: 0, original: '/' } ]; + } + else { + roots = history.getCommonDirectories().map(function(root) { + return { name: root, dist: 0, original: root }; + }); + } + + // Add each part of the typed pathname onto each of the roots in turn, + // Finding options from each of those paths, and using these options as + // our roots for the next part + var partsAdded = util.promiseEach(parts, function(part, index) { + + var partsSoFar = filesystem.join.apply(filesystem, parts.slice(0, index + 1)); + + // We allow this file matches in this pass if we're allowed files at all + // (i.e this isn't 'cd') and if this is the last part of the path + var allowFileForPart = (allowFile && index >= parts.length - 1); + + var rootsPromise = util.promiseEach(roots, function(root) { + + // Extend each roots to a list of all the files in each of the roots + var matchFile = allowFileForPart ? options.matches : null; + var promise = filesystem.ls(root.name, matchFile); + + var onSuccess = function(entries) { + // Unless this is the final part filter out the non-directories + if (!allowFileForPart) { + entries = entries.filter(function(entry) { + return entry.isDir; + }); + } + var entryMap = {}; + entries.forEach(function(entry) { + entryMap[entry.pathname] = entry; + }); + return entryMap; + }; + + var onError = function(err) { + // We expect errors due to the path not being a directory, not being + // accessible, or removed since the call to 'readdir', but other + // errors should be reported + var noComplainCodes = [ 'ENOTDIR', 'EACCES', 'EBADF', 'ENOENT' ]; + if (noComplainCodes.indexOf(err.code) === -1) { + console.error('Error looing up', root.name, err); + } + return {}; + }; + + promise = promise.then(onSuccess, onError); + + // We want to compare all the directory entries with the original root + // plus the partsSoFar + var compare = filesystem.join(root.original, partsSoFar); + + return promise.then(function(entryMap) { + + var ranks = spell.rank(compare, Object.keys(entryMap), RANK_OPTIONS); + // penalize each path by the distance of it's parent + ranks.forEach(function(rank) { + rank.original = root.original; + rank.stats = entryMap[rank.name]; + }); + return ranks; + }); + }); + + return rootsPromise.then(function(data) { + // data is an array of arrays of ranking objects. Squash down. + data = data.reduce(function(prev, curr) { + return prev.concat(curr); + }, []); + + data.sort(function(r1, r2) { + return r1.dist - r2.dist; + }); + + // Trim, but by how many? + // If this is the last run through, we want to present the user with + // a sensible set of predictions. Otherwise we want to trim the tree + // to a reasonable set of matches, so we're happy with 1 + // We look through x +/- 3 roots, and find the one with the biggest + // distance delta, and cut below that + // x=5 for the last time through, and x=8 otherwise + var isLast = index >= parts.length - 1; + var start = isLast ? 1 : 5; + var end = isLast ? 7 : 10; + + var maxDeltaAt = start; + var maxDelta = data[start].dist - data[start - 1].dist; + + for (var i = start + 1; i < end; i++) { + var delta = data[i].dist - data[i - 1].dist; + if (delta >= maxDelta) { + maxDelta = delta; + maxDeltaAt = i; + } + } + + // Update the list of roots for the next time round + roots = data.slice(0, maxDeltaAt); + }); + }); + + return partsAdded.then(function() { + var predictions = roots.map(function(root) { + var isFile = root.stats && root.stats.isFile; + var isDir = root.stats && root.stats.isDir; + + var name = root.name; + if (isDir && name.charAt(name.length) !== filesystem.sep) { + name += filesystem.sep; + } + + return { + name: name, + incomplete: !(allowFile && isFile), + isFile: isFile, // Added for describe, below + dist: root.dist, // TODO: Remove - added for debug in describe + }; + }); + + return util.promiseEach(predictions, function(prediction) { + if (!prediction.isFile) { + prediction.description = '(' + prediction.dist + ')'; + prediction.dist = undefined; + prediction.isFile = undefined; + return prediction; + } + + return filesystem.describe(prediction.name).then(function(description) { + prediction.description = description; + prediction.dist = undefined; + prediction.isFile = undefined; + return prediction; + }); + }); + }); + }; +} + +// ============================================================================= + +/* + * The idea is that we maintain a list of 'directories that the user is + * interested in'. We store directories in a most-frequently-used cache + * of some description. + * But for now we're just using / and ~/ + */ +var history = { + getCommonDirectories: function() { + return [ + filesystem.sep, // i.e. the root directory + filesystem.home // i.e. the users home directory + ]; + }, + addCommonDirectory: function(ignore) { + // Not implemented yet + } +}; + + +}); +/* + * Copyright 2012, Mozilla Foundation and contributors + * + * Licensed under the Apache License, Version 2.0 (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.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +define('util/filesystem', ['require', 'exports', 'module' , 'util/promise'], function(require, exports, module) { + +'use strict'; + +var OS = Components.utils.import("resource://gre/modules/osfile.jsm", {}).OS; +var promise = require('util/promise'); + +/** + * A set of functions that don't really belong in 'fs' (because they're not + * really universal in scope) but also kind of do (because they're not specific + * to GCLI + */ + +exports.join = OS.Path.join; +exports.sep = OS.Path.sep; +exports.dirname = OS.Path.dirname; + +var dirService = Components.classes["@mozilla.org/file/directory_service;1"] + .getService(Components.interfaces.nsIProperties); +exports.home = dirService.get("Home", Components.interfaces.nsIFile).path; + +if ("winGetDrive" in OS.Path) { + exports.sep = '\\'; +} +else { + exports.sep = '/'; +} + +/** + * Split a path into its components. + * @param pathname (string) The part to cut up + * @return An array of path components + */ +exports.split = function(pathname) { + return OS.Path.split(pathname).components; +}; + +/** + * @param pathname string, path of an existing directory + * @param matches optional regular expression - filter output to include only + * the files that match the regular expression. The regexp is applied to the + * filename only not to the full path + * @return A promise of an array of stat objects for each member of the + * directory pointed to by ``pathname``, each containing 2 extra properties: + * - pathname: The full pathname of the file + * - filename: The final filename part of the pathname + */ +exports.ls = function(pathname, matches) { + var iterator = new OS.File.DirectoryIterator(pathname); + var entries = []; + + var iteratePromise = iterator.forEach(function(entry) { + entries.push({ + exists: true, + isDir: entry.isDir, + isFile: !entry.isFile, + filename: entry.name, + pathname: entry.path + }); + }); + + return iteratePromise.then(function onSuccess() { + iterator.close(); + return entries; + }, + function onFailure(reason) { + iterator.close(); + throw reason; + } + ); +}; + +/** + * stat() is annoying because it considers stat('/doesnt/exist') to be an + * error, when the point of stat() is to *find* *out*. So this wrapper just + * converts 'ENOENT' i.e. doesn't exist to { exists:false } and adds + * exists:true to stat blocks from existing paths + */ +exports.stat = function(pathname) { + var onResolve = function(stats) { + return { + exists: true, + isDir: stats.isDir, + isFile: !stats.isFile + }; + }; + + var onReject = function(err) { + if (err instanceof OS.File.Error && err.becauseNoSuchFile) { + return { + exists: false, + isDir: false, + isFile: false + }; + } + throw err; + }; + + return OS.File.stat(pathname).then(onResolve, onReject); +}; + +/** + * We may read the first line of a file to describe it? + * Right now, however, we do nothing. + */ +exports.describe = function(pathname) { + return promise.resolve(''); +}; }); /* * Copyright 2012, Mozilla Foundation and contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -4151,28 +5461,16 @@ define('gcli/types/javascript', ['requir var promise = require('util/promise'); var l10n = require('util/l10n'); var types = require('gcli/types'); var Conversion = types.Conversion; var Type = types.Type; var Status = types.Status; - -/** - * Registration and de-registration. - */ -exports.startup = function() { - types.addType(JavascriptType); -}; - -exports.shutdown = function() { - types.removeType(JavascriptType); -}; - /** * The object against which we complete, which is usually 'window' if it exists * but could be something else in non-web-content environments. */ var globalObject = undefined; if (typeof window !== 'undefined') { globalObject = window; } @@ -4683,17 +5981,17 @@ JavascriptType.prototype._isSafeProperty // The property is safe if 'get' isn't a function or if the function has a // prototype (in which case it's native) return typeof propDesc.get !== 'function' || 'prototype' in propDesc.get; }; JavascriptType.prototype.name = 'javascript'; -exports.JavascriptType = JavascriptType; +exports.items = [ JavascriptType ]; }); /* * Copyright 2012, Mozilla Foundation and contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -4710,37 +6008,22 @@ exports.JavascriptType = JavascriptType; define('gcli/types/node', ['require', 'exports', 'module' , 'util/promise', 'util/host', 'util/l10n', 'gcli/types', 'gcli/argument'], function(require, exports, module) { 'use strict'; var promise = require('util/promise'); var host = require('util/host'); var l10n = require('util/l10n'); -var types = require('gcli/types'); -var Type = require('gcli/types').Type; var Status = require('gcli/types').Status; var Conversion = require('gcli/types').Conversion; var BlankArgument = require('gcli/argument').BlankArgument; /** - * Registration and de-registration. - */ -exports.startup = function() { - types.addType(NodeType); - types.addType(NodeListType); -}; - -exports.shutdown = function() { - types.removeType(NodeType); - types.removeType(NodeListType); -}; - -/** * The object against which we complete, which is usually 'window' if it exists * but could be something else in non-web-content environments. */ var doc = undefined; if (typeof document !== 'undefined') { doc = document; } @@ -4773,128 +6056,123 @@ exports.unsetDocument = function() { /** * Getter for the document that contains the nodes we're matching * Most for changing things back to how they were for unit testing */ exports.getDocument = function() { return doc; }; - -/** - * A CSS expression that refers to a single node - */ -function NodeType(typeSpec) { -} - -NodeType.prototype = Object.create(Type.prototype); - -NodeType.prototype.stringify = function(value, context) { - if (value == null) { - return ''; - } - return value.__gcliQuery || 'Error'; -}; - -NodeType.prototype.parse = function(arg, context) { - if (arg.text === '') { - return promise.resolve(new Conversion(undefined, arg, Status.INCOMPLETE)); - } - - var nodes; - try { - nodes = doc.querySelectorAll(arg.text); - } - catch (ex) { - return promise.resolve(new Conversion(undefined, arg, Status.ERROR, - l10n.lookup('nodeParseSyntax'))); - } - - if (nodes.length === 0) { - return promise.resolve(new Conversion(undefined, arg, Status.INCOMPLETE, - l10n.lookup('nodeParseNone'))); - } - - if (nodes.length === 1) { - var node = nodes.item(0); - node.__gcliQuery = arg.text; - - host.flashNodes(node, true); - - return promise.resolve(new Conversion(node, arg, Status.VALID, '')); - } - - host.flashNodes(nodes, false); - - var message = l10n.lookupFormat('nodeParseMultiple', [ nodes.length ]); - return promise.resolve(new Conversion(undefined, arg, Status.ERROR, message)); -}; - -NodeType.prototype.name = 'node'; - - - -/** - * A CSS expression that refers to a node list. - * - * The 'allowEmpty' option ensures that we do not complain if the entered CSS - * selector is valid, but does not match any nodes. There is some overlap - * between this option and 'defaultValue'. What the user wants, in most cases, - * would be to use 'defaultText' (i.e. what is typed rather than the value that - * it represents). However this isn't a concept that exists yet and should - * probably be a part of GCLI if/when it does. - * All NodeListTypes have an automatic defaultValue of an empty NodeList so - * they can easily be used in named parameters. - */ -function NodeListType(typeSpec) { - if ('allowEmpty' in typeSpec && typeof typeSpec.allowEmpty !== 'boolean') { - throw new Error('Legal values for allowEmpty are [true|false]'); - } - - this.allowEmpty = typeSpec.allowEmpty; -} - -NodeListType.prototype = Object.create(Type.prototype); - -NodeListType.prototype.getBlank = function(context) { - return new Conversion(exports._empty, new BlankArgument(), Status.VALID); -}; - -NodeListType.prototype.stringify = function(value, context) { - if (value == null) { - return ''; - } - return value.__gcliQuery || 'Error'; -}; - -NodeListType.prototype.parse = function(arg, context) { - if (arg.text === '') { - return promise.resolve(new Conversion(undefined, arg, Status.INCOMPLETE)); - } - - var nodes; - try { - nodes = doc.querySelectorAll(arg.text); - } - catch (ex) { - return promise.resolve(new Conversion(undefined, arg, Status.ERROR, - l10n.lookup('nodeParseSyntax'))); - } - - if (nodes.length === 0 && !this.allowEmpty) { - return promise.resolve(new Conversion(undefined, arg, Status.INCOMPLETE, - l10n.lookup('nodeParseNone'))); - } - - host.flashNodes(nodes, false); - return promise.resolve(new Conversion(nodes, arg, Status.VALID, '')); -}; - -NodeListType.prototype.name = 'nodelist'; - +/** + * The exported 'node' and 'nodelist' types + */ +exports.items = [ + { + // The 'node' type is a CSS expression that refers to a single node + item: 'type', + name: 'node', + + stringify: function(value, context) { + if (value == null) { + return ''; + } + return value.__gcliQuery || 'Error'; + }, + + parse: function(arg, context) { + if (arg.text === '') { + return promise.resolve(new Conversion(undefined, arg, + Status.INCOMPLETE)); + } + + var nodes; + try { + nodes = doc.querySelectorAll(arg.text); + } + catch (ex) { + return promise.resolve(new Conversion(undefined, arg, Status.ERROR, + l10n.lookup('nodeParseSyntax'))); + } + + if (nodes.length === 0) { + return promise.resolve(new Conversion(undefined, arg, Status.INCOMPLETE, + l10n.lookup('nodeParseNone'))); + } + + if (nodes.length === 1) { + var node = nodes.item(0); + node.__gcliQuery = arg.text; + + host.flashNodes(node, true); + + return promise.resolve(new Conversion(node, arg, Status.VALID, '')); + } + + host.flashNodes(nodes, false); + + var msg = l10n.lookupFormat('nodeParseMultiple', [ nodes.length ]); + return promise.resolve(new Conversion(undefined, arg, Status.ERROR, msg)); + } + }, + { + // The 'nodelist' type is a CSS expression that refers to a node list + item: 'type', + name: 'nodelist', + + // The 'allowEmpty' option ensures that we do not complain if the entered + // CSS selector is valid, but does not match any nodes. There is some + // overlap between this option and 'defaultValue'. What the user wants, in + // most cases, would be to use 'defaultText' (i.e. what is typed rather than + // the value that it represents). However this isn't a concept that exists + // yet and should probably be a part of GCLI if/when it does. + // All NodeListTypes have an automatic defaultValue of an empty NodeList so + // they can easily be used in named parameters. + allowEmpty: false, + + constructor: function() { + if (typeof this.allowEmpty !== 'boolean') { + throw new Error('Legal values for allowEmpty are [true|false]'); + } + }, + + getBlank: function(context) { + return new Conversion(exports._empty, new BlankArgument(), Status.VALID); + }, + + stringify: function(value, context) { + if (value == null) { + return ''; + } + return value.__gcliQuery || 'Error'; + }, + + parse: function(arg, context) { + if (arg.text === '') { + return promise.resolve(new Conversion(undefined, arg, Status.INCOMPLETE)); + } + + var nodes; + try { + nodes = doc.querySelectorAll(arg.text); + } + catch (ex) { + return promise.resolve(new Conversion(undefined, arg, Status.ERROR, + l10n.lookup('nodeParseSyntax'))); + } + + if (nodes.length === 0 && !this.allowEmpty) { + return promise.resolve(new Conversion(undefined, arg, Status.INCOMPLETE, + l10n.lookup('nodeParseNone'))); + } + + host.flashNodes(nodes, false); + return promise.resolve(new Conversion(nodes, arg, Status.VALID, '')); + } + } +]; }); /* * Copyright 2012, Mozilla Foundation and contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at @@ -4956,37 +6234,188 @@ exports.exec = function(execSpec) { * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ -define('gcli/types/resource', ['require', 'exports', 'module' , 'util/promise', 'gcli/types', 'gcli/types/selection'], function(require, exports, module) { +define('gcli/types/number', ['require', 'exports', 'module' , 'util/promise', 'util/l10n', 'gcli/types'], function(require, exports, module) { 'use strict'; var promise = require('util/promise'); -var types = require('gcli/types'); +var l10n = require('util/l10n'); +var Status = require('gcli/types').Status; +var Conversion = require('gcli/types').Conversion; + +exports.items = [ + { + // 'number' type + // Has custom max / min / step values to control increment and decrement + // and a boolean allowFloat property to clamp values to integers + item: 'type', + name: 'number', + allowFloat: false, + max: undefined, + min: undefined, + step: 1, + + constructor: function() { + if (!this.allowFloat && + (this._isFloat(this.min) || + this._isFloat(this.max) || + this._isFloat(this.step))) { + throw new Error('allowFloat is false, but non-integer values given in type spec'); + } + }, + + stringify: function(value, context) { + if (value == null) { + return ''; + } + return '' + value; + }, + + getMin: function(context) { + if (this.min) { + if (typeof this.min === 'function') { + return this.min(context); + } + if (typeof this.min === 'number') { + return this.min; + } + } + return undefined; + }, + + getMax: function(context) { + if (this.max) { + if (typeof this.max === 'function') { + return this.max(context); + } + if (typeof this.max === 'number') { + return this.max; + } + } + return undefined; + }, + + parse: function(arg, context) { + if (arg.text.replace(/^\s*-?/, '').length === 0) { + return promise.resolve(new Conversion(undefined, arg, Status.INCOMPLETE, '')); + } + + if (!this.allowFloat && (arg.text.indexOf('.') !== -1)) { + var message = l10n.lookupFormat('typesNumberNotInt2', [ arg.text ]); + return promise.resolve(new Conversion(undefined, arg, Status.ERROR, message)); + } + + var value; + if (this.allowFloat) { + value = parseFloat(arg.text); + } + else { + value = parseInt(arg.text, 10); + } + + if (isNaN(value)) { + var message = l10n.lookupFormat('typesNumberNan', [ arg.text ]); + return promise.resolve(new Conversion(undefined, arg, Status.ERROR, message)); + } + + var max = this.getMax(context); + if (max != null && value > max) { + var message = l10n.lookupFormat('typesNumberMax', [ value, max ]); + return promise.resolve(new Conversion(undefined, arg, Status.ERROR, message)); + } + + var min = this.getMin(context); + if (min != null && value < min) { + var message = l10n.lookupFormat('typesNumberMin', [ value, min ]); + return promise.resolve(new Conversion(undefined, arg, Status.ERROR, message)); + } + + return promise.resolve(new Conversion(value, arg)); + }, + + decrement: function(value, context) { + if (typeof value !== 'number' || isNaN(value)) { + return this.getMax(context) || 1; + } + var newValue = value - this.step; + // Snap to the nearest incremental of the step + newValue = Math.ceil(newValue / this.step) * this.step; + return this._boundsCheck(newValue, context); + }, + + increment: function(value, context) { + if (typeof value !== 'number' || isNaN(value)) { + var min = this.getMin(context); + return min != null ? min : 0; + } + var newValue = value + this.step; + // Snap to the nearest incremental of the step + newValue = Math.floor(newValue / this.step) * this.step; + if (this.getMax(context) == null) { + return newValue; + } + return this._boundsCheck(newValue, context); + }, + + // Return the input value so long as it is within the max/min bounds. + // If it is lower than the minimum, return the minimum. If it is bigger + // than the maximum then return the maximum. + _boundsCheck: function(value, context) { + var min = this.getMin(context); + if (min != null && value < min) { + return min; + } + var max = this.getMax(context); + if (max != null && value > max) { + return max; + } + return value; + }, + + // Return true if the given value is a finite number and not an integer, + // else return false. + _isFloat: function(value) { + return ((typeof value === 'number') && isFinite(value) && (value % 1 !== 0)); + } + } +]; + + +}); +/* + * Copyright 2012, Mozilla Foundation and contributors + * + * Licensed under the Apache License, Version 2.0 (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.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +define('gcli/types/resource', ['require', 'exports', 'module' , 'util/promise', 'gcli/types/selection'], function(require, exports, module) { + +'use strict'; + +var promise = require('util/promise'); var SelectionType = require('gcli/types/selection').SelectionType; -/** - * Registration and de-registration. - */ -exports.startup = function() { - types.addType(ResourceType); -}; - -exports.shutdown = function() { - types.removeType(ResourceType); - exports.clearResourceCache(); -}; - exports.clearResourceCache = function() { ResourceCache.clear(); }; /** * The object against which we complete, which is usually 'window' if it exists * but could be something else in non-web-content environments. */ @@ -5184,52 +6613,16 @@ function dedupe(resources, onDupe) { var clones = names[name]; if (clones.length > 1) { onDupe(clones); } }); } /** - * Use the Resource implementations to create a type based on SelectionType - */ -function ResourceType(typeSpec) { - this.include = typeSpec.include; - if (this.include !== Resource.TYPE_SCRIPT && - this.include !== Resource.TYPE_CSS && - this.include != null) { - throw new Error('invalid include property: ' + this.include); - } -} - -ResourceType.prototype = Object.create(SelectionType.prototype); - -/** - * There are several ways to get selection data. This unifies them into one - * single function. - * @return A map of names to values. - */ -ResourceType.prototype.getLookup = function() { - var resources = []; - if (this.include !== Resource.TYPE_SCRIPT) { - Array.prototype.push.apply(resources, CssResource._getAllStyles()); - } - if (this.include !== Resource.TYPE_CSS) { - Array.prototype.push.apply(resources, ScriptResource._getAllScripts()); - } - - return promise.resolve(resources.map(function(resource) { - return { name: resource.name, value: resource }; - })); -}; - -ResourceType.prototype.name = 'resource'; - - -/** * A quick cache of resources against nodes * TODO: Potential memory leak when the target document has css or script * resources repeatedly added and removed. Solution might be to use a weak * hash map or some such. */ var ResourceCache = { _cached: [], @@ -5255,16 +6648,48 @@ var ResourceCache = { /** * Drop all cache entries. Helpful to prevent memory leaks */ clear: function() { ResourceCache._cached = []; } }; +/** + * The resource type itself + */ +exports.items = [ + { + item: 'type', + constructor: function() { + if (this.include !== Resource.TYPE_SCRIPT && + this.include !== Resource.TYPE_CSS && + this.include != null) { + throw new Error('invalid include property: ' + this.include); + } + }, + name: 'resource', + parent: 'selection', + include: null, + cacheable: false, + lookup: function() { + var resources = []; + if (this.include !== Resource.TYPE_SCRIPT) { + Array.prototype.push.apply(resources, CssResource._getAllStyles()); + } + if (this.include !== Resource.TYPE_CSS) { + Array.prototype.push.apply(resources, ScriptResource._getAllScripts()); + } + + return promise.resolve(resources.map(function(resource) { + return { name: resource.name, value: resource }; + })); + } + } +]; }); /* * Copyright 2012, Mozilla Foundation and contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at @@ -5280,69 +6705,146 @@ var ResourceCache = { define('gcli/types/setting', ['require', 'exports', 'module' , 'gcli/settings', 'gcli/types'], function(require, exports, module) { 'use strict'; var settings = require('gcli/settings'); var types = require('gcli/types'); -/** - * A type for selecting a known setting - */ -var settingType = { - constructor: function() { - settings.onChange.add(function(ev) { - this.clearCache(); - }, this); +exports.items = [ + { + // A type for selecting a known setting + item: 'type', + name: 'setting', + parent: 'selection', + cacheable: true, + constructor: function() { + settings.onChange.add(function(ev) { + this.clearCache(); + }, this); + }, + lookup: function() { + return settings.getAll().map(function(setting) { + return { name: setting.name, value: setting }; + }); + } }, - name: 'setting', - parent: 'selection', - cacheable: true, - lookup: function() { - return settings.getAll().map(function(setting) { - return { name: setting.name, value: setting }; - }); - } -}; - -/** - * A type for entering the value of a known setting - * Customizations: - * - settingParamName The name of the setting parameter so we can customize the - * type that we are expecting to read - */ -var settingValueType = { - name: 'settingValue', - parent: 'delegate', - settingParamName: 'setting', - delegateType: function(context) { - if (context != null) { - var setting = context.getArgsObject()[this.settingParamName]; - if (setting != null) { - return setting.type; - } - } - - return types.createType('blank'); - } -}; - -/** - * Registration and de-registration. - */ -exports.startup = function() { - types.addType(settingType); - types.addType(settingValueType); -}; - -exports.shutdown = function() { - types.removeType(settingType); - types.removeType(settingValueType); -}; + { + // A type for entering the value of a known setting + // Customizations: + // - settingParamName The name of the setting parameter so we can customize the + // type that we are expecting to read + item: 'type', + name: 'settingValue', + parent: 'delegate', + settingParamName: 'setting', + delegateType: function(context) { + if (context != null) { + var setting = context.getArgsObject()[this.settingParamName]; + if (setting != null) { + return setting.type; + } + } + + return types.createType('blank'); + } + } +]; + +}); +/* + * Copyright 2012, Mozilla Foundation and contributors + * + * Licensed under the Apache License, Version 2.0 (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.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +define('gcli/types/string', ['require', 'exports', 'module' , 'util/promise', 'gcli/types'], function(require, exports, module) { + +'use strict'; + +var promise = require('util/promise'); +var Status = require('gcli/types').Status; +var Conversion = require('gcli/types').Conversion; + +exports.items = [ + { + // 'string' the most basic string type where all we need to do is to take + // care of converting escaped characters like \t, \n, etc. + // For the full list see + // https://developer.mozilla.org/en-US/docs/JavaScript/Guide/Values,_variables,_and_literals + // The exception is that we ignore \b because replacing '\b' characters in + // stringify() with their escaped version injects '\\b' all over the place + // and the need to support \b seems low) + // Customizations: + // allowBlank: Allow a blank string to be counted as valid + item: 'type', + name: 'string', + allowBlank: false, + + stringify: function(value, context) { + if (value == null) { + return ''; + } + + return value + .replace(/\\/g, '\\\\') + .replace(/\f/g, '\\f') + .replace(/\n/g, '\\n') + .replace(/\r/g, '\\r') + .replace(/\t/g, '\\t') + .replace(/\v/g, '\\v') + .replace(/\n/g, '\\n') + .replace(/\r/g, '\\r') + .replace(/ /g, '\\ ') + .replace(/'/g, '\\\'') + .replace(/"/g, '\\"') + .replace(/{/g, '\\{') + .replace(/}/g, '\\}'); + }, + + parse:function(arg, context) { + if (!this.allowBlank && (arg.text == null || arg.text === '')) { + return promise.resolve(new Conversion(undefined, arg, Status.INCOMPLETE, '')); + } + + // The string '\\' (i.e. an escaped \ (represented here as '\\\\' because it + // is double escaped)) is first converted to a private unicode character and + // then at the end from \uF000 to a single '\' to avoid the string \\n being + // converted first to \n and then to a <LF> + var value = arg.text + .replace(/\\\\/g, '\uF000') + .replace(/\\f/g, '\f') + .replace(/\\n/g, '\n') + .replace(/\\r/g, '\r') + .replace(/\\t/g, '\t') + .replace(/\\v/g, '\v') + .replace(/\\n/g, '\n') + .replace(/\\r/g, '\r') + .replace(/\\ /g, ' ') + .replace(/\\'/g, '\'') + .replace(/\\"/g, '"') + .replace(/\\{/g, '{') + .replace(/\\}/g, '}') + .replace(/\uF000/g, '\\'); + + return promise.resolve(new Conversion(value, arg)); + } + } +]; }); /* * Copyright 2012, Mozilla Foundation and contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -5352,290 +6854,130 @@ exports.shutdown = function() { * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ -define('gcli/settings', ['require', 'exports', 'module' , 'util/util', 'gcli/types'], function(require, exports, module) { +define('gcli/converters/basic', ['require', 'exports', 'module' , 'util/util'], function(require, exports, module) { 'use strict'; -var imports = {}; - -Components.utils.import('resource://gre/modules/XPCOMUtils.jsm', imports); - -imports.XPCOMUtils.defineLazyGetter(imports, 'prefBranch', function() { - var prefService = Components.classes['@mozilla.org/preferences-service;1'] - .getService(Components.interfaces.nsIPrefService); - return prefService.getBranch(null) - .QueryInterface(Components.interfaces.nsIPrefBranch2); -}); - -imports.XPCOMUtils.defineLazyGetter(imports, 'supportsString', function() { - return Components.classes["@mozilla.org/supports-string;1"] - .createInstance(Components.interfaces.nsISupportsString); -}); - - var util = require('util/util'); -var types = require('gcli/types'); - -/** - * All local settings have this prefix when used in Firefox - */ -var DEVTOOLS_PREFIX = 'devtools.gcli.'; - -/** - * A class to wrap up the properties of a preference. - * @see toolkit/components/viewconfig/content/config.js - */ -function Setting(prefSpec) { - if (typeof prefSpec === 'string') { - // We're coming from getAll() i.e. a full listing of prefs - this.name = prefSpec; - this.description = ''; - } - else { - // A specific addition by GCLI - this.name = DEVTOOLS_PREFIX + prefSpec.name; - - if (prefSpec.ignoreTypeDifference !== true && prefSpec.type) { - if (this.type.name !== prefSpec.type) { - throw new Error('Locally declared type (' + prefSpec.type + ') != ' + - 'Mozilla declared type (' + this.type.name + ') for ' + this.name); - } - } - - this.description = prefSpec.description; - } - - this.onChange = util.createEvent('Setting.onChange'); -} - -/** - * What type is this property: boolean/integer/string? - */ -Object.defineProperty(Setting.prototype, 'type', { - get: function() { - switch (imports.prefBranch.getPrefType(this.name)) { - case imports.prefBranch.PREF_BOOL: - return types.createType('boolean'); - - case imports.prefBranch.PREF_INT: - return types.createType('number'); - - case imports.prefBranch.PREF_STRING: - return types.createType('string'); - - default: - throw new Error('Unknown type for ' + this.name); - } + +/** + * Several converters are just data.toString inside a 'p' element + */ +function nodeFromDataToString(data, conversionContext) { + var node = util.createElement(conversionContext.document, 'p'); + node.textContent = data.toString(); + return node; +} + +exports.items = [ + { + item: 'converter', + from: 'string', + to: 'dom', + exec: nodeFromDataToString }, - enumerable: true -}); - -/** - * What type is this property: boolean/integer/string? - */ -Object.defineProperty(Setting.prototype, 'value', { - get: function() { - switch (imports.prefBranch.getPrefType(this.name)) { - case imports.prefBranch.PREF_BOOL: - return imports.prefBranch.getBoolPref(this.name); - - case imports.prefBranch.PREF_INT: - return imports.prefBranch.getIntPref(this.name); - - case imports.prefBranch.PREF_STRING: - var value = imports.prefBranch.getComplexValue(this.name, - Components.interfaces.nsISupportsString).data; - // In case of a localized string - if (/^chrome:\/\/.+\/locale\/.+\.properties/.test(value)) { - value = imports.prefBranch.getComplexValue(this.name, - Components.interfaces.nsIPrefLocalizedString).data; - } - return value; - - default: - throw new Error('Invalid value for ' + this.name); + { + item: 'converter', + from: 'number', + to: 'dom', + exec: nodeFromDataToString + }, + { + item: 'converter', + from: 'boolean', + to: 'dom', + exec: nodeFromDataToString + }, + { + item: 'converter', + from: 'undefined', + to: 'dom', + exec: function(data, conversionContext) { + return util.createElement(conversionContext.document, 'span'); } }, - - set: function(value) { - if (imports.prefBranch.prefIsLocked(this.name)) { - throw new Error('Locked preference ' + this.name); - } - - switch (imports.prefBranch.getPrefType(this.name)) { - case imports.prefBranch.PREF_BOOL: - imports.prefBranch.setBoolPref(this.name, value); - break; - - case imports.prefBranch.PREF_INT: - imports.prefBranch.setIntPref(this.name, value); - break; - - case imports.prefBranch.PREF_STRING: - imports.supportsString.data = value; - imports.prefBranch.setComplexValue(this.name, - Components.interfaces.nsISupportsString, - imports.supportsString); - break; - - default: - throw new Error('Invalid value for ' + this.name); - } - - Services.prefs.savePrefFile(null); + { + item: 'converter', + from: 'error', + to: 'dom', + exec: function(ex, conversionContext) { + var node = util.createElement(conversionContext.document, 'p'); + node.className = "gcli-error"; + node.textContent = ex; + return node; + } + } +]; + +}); +/* + * Copyright 2012, Mozilla Foundation and contributors + * + * Licensed under the Apache License, Version 2.0 (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.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +define('gcli/converters/terminal', ['require', 'exports', 'module' , 'util/util'], function(require, exports, module) { + +'use strict'; + +var util = require('util/util'); + +/** + * A 'terminal' object is a string or an array of strings, which are typically + * the output from a shell command + */ +exports.items = [ + { + item: 'converter', + from: 'terminal', + to: 'dom', + createTextArea: function(text, conversionContext) { + var node = util.createElement(conversionContext.document, 'textarea'); + node.classList.add('gcli-row-subterminal'); + node.readOnly = true; + node.textContent = text; + return node; + }, + exec: function(data, conversionContext) { + if (Array.isArray(data)) { + var node = util.createElement(conversionContext.document, 'div'); + data.forEach(function(member) { + node.appendChild(this.createTextArea(member, conversionContext)); + }); + return node; + } + return this.createTextArea(data); + } }, - - enumerable: true -}); - -/** - * Reset this setting to it's initial default value - */ -Setting.prototype.setDefault = function() { - imports.prefBranch.clearUserPref(this.name); - Services.prefs.savePrefFile(null); -}; - - -/** - * Collection of preferences for sorted access - */ -var settingsAll = []; - -/** - * Collection of preferences for fast indexed access - */ -var settingsMap = new Map(); - -/** - * Flag so we know if we've read the system preferences - */ -var hasReadSystem = false; - -/** - * Clear out all preferences and return to initial state - */ -function reset() { - settingsMap = new Map(); - settingsAll = []; - hasReadSystem = false; -} - -/** - * Reset everything on startup and shutdown because we're doing lazy loading - */ -exports.startup = function() { - reset(); -}; - -exports.shutdown = function() { - reset(); -}; - -/** - * Load system prefs if they've not been loaded already - * @return true - */ -function readSystem() { - if (hasReadSystem) { - return; - } - - imports.prefBranch.getChildList('').forEach(function(name) { - var setting = new Setting(name); - settingsAll.push(setting); - settingsMap.set(name, setting); - }); - - settingsAll.sort(function(s1, s2) { - return s1.name.localeCompare(s2.name); - }); - - hasReadSystem = true; -} - -/** - * Get an array containing all known Settings filtered to match the given - * filter (string) at any point in the name of the setting - */ -exports.getAll = function(filter) { - readSystem(); - - if (filter == null) { - return settingsAll; - } - - return settingsAll.filter(function(setting) { - return setting.name.indexOf(filter) !== -1; - }); -}; - -/** - * Add a new setting. - */ -exports.addSetting = function(prefSpec) { - var setting = new Setting(prefSpec); - - if (settingsMap.has(setting.name)) { - // Once exists already, we're going to need to replace it in the array - for (var i = 0; i < settingsAll.length; i++) { - if (settingsAll[i].name === setting.name) { - settingsAll[i] = setting; - } - } - } - - settingsMap.set(setting.name, setting); - exports.onChange({ added: setting.name }); - - return setting; -}; - -/** - * Getter for an existing setting. Generally use of this function should be - * avoided. Systems that define a setting should export it if they wish it to - * be available to the outside, or not otherwise. Use of this function breaks - * that boundary and also hides dependencies. Acceptable uses include testing - * and embedded uses of GCLI that pre-define all settings (e.g. Firefox) - * @param name The name of the setting to fetch - * @return The found Setting object, or undefined if the setting was not found - */ -exports.getSetting = function(name) { - // We might be able to give the answer without needing to read all system - // settings if this is an internal setting - var found = settingsMap.get(name); - if (found) { - return found; - } - - if (hasReadSystem) { - return undefined; - } - else { - readSystem(); - return settingsMap.get(name); - } -}; - -/** - * Event for use to detect when the list of settings changes - */ -exports.onChange = util.createEvent('Settings.onChange'); - -/** - * Remove a setting. A no-op in this case - */ -exports.removeSetting = function() { }; + { + item: 'converter', + from: 'terminal', + to: 'string', + exec: function(data, conversionContext) { + return Array.isArray(data) ? data.join('') : '' + data; + } + } +]; }); /* * Copyright 2012, Mozilla Foundation and contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -5657,70 +6999,58 @@ define('gcli/ui/intro', ['require', 'exp var l10n = require('util/l10n'); var settings = require('gcli/settings'); var view = require('gcli/ui/view'); var Output = require('gcli/cli').Output; /** * Record if the user has clicked on 'Got It!' */ -var hideIntroSettingSpec = { - name: 'hideIntro', - type: 'boolean', - description: l10n.lookup('hideIntroDesc'), - defaultValue: false -}; -var hideIntro; - -/** - * Register (and unregister) the hide-intro setting - */ -exports.startup = function() { - hideIntro = settings.addSetting(hideIntroSettingSpec); -}; - -exports.shutdown = function() { - settings.removeSetting(hideIntroSettingSpec); - hideIntro = undefined; -}; +exports.items = [ + { + item: 'setting', + name: 'hideIntro', + type: 'boolean', + description: l10n.lookup('hideIntroDesc'), + defaultValue: false + } +]; /** * Called when the UI is ready to add a welcome message to the output */ exports.maybeShowIntro = function(commandOutputManager, conversionContext) { + var hideIntro = settings.getSetting('hideIntro'); if (hideIntro.value) { return; } var output = new Output(); output.type = 'view'; commandOutputManager.onOutput({ output: output }); - var viewData = this.createView(conversionContext, output); + var viewData = this.createView(null, conversionContext, output); output.complete({ isTypedData: true, type: 'view', data: viewData }); }; /** * Called when the UI is ready to add a welcome message to the output */ -exports.createView = function(conversionContext, output) { +exports.createView = function(ignore, conversionContext, output) { return view.createView({ html: require('text!gcli/ui/intro.html'), options: { stack: 'intro.html' }, data: { l10n: l10n.propertyLookup, - onclick: function(ev) { - conversionContext.update(ev.currentTarget); - }, - ondblclick: function(ev) { - conversionContext.updateExec(ev.currentTarget); - }, + onclick: conversionContext.update, + ondblclick: conversionContext.updateExec, showHideButton: (output != null), onGotIt: function(ev) { + var hideIntro = settings.getSetting('hideIntro'); hideIntro.value = true; output.onClose(); } } }); }; }); @@ -5873,30 +7203,25 @@ var Conversion = require('gcli/types').C var Argument = require('gcli/argument').Argument; var ArrayArgument = require('gcli/argument').ArrayArgument; var NamedArgument = require('gcli/argument').NamedArgument; var TrueNamedArgument = require('gcli/argument').TrueNamedArgument; var MergedArgument = require('gcli/argument').MergedArgument; var ScriptArgument = require('gcli/argument').ScriptArgument; -var evalCommand; - -/** - * Registration and de-registration. - */ -exports.startup = function() { - evalCommand = canon.addCommand(evalCommandSpec); -}; - -exports.shutdown = function() { - canon.removeCommand(evalCommandSpec.name); - evalCommand = undefined; -}; - +/** + * Some manual intervention is needed in parsing the { command. + */ +function getEvalCommand() { + if (getEvalCommand._cmd == null) { + getEvalCommand._cmd = canon.getCommand(evalCmd.name); + } + return getEvalCommand._cmd; +} /** * Assignment is 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. @@ -6090,34 +7415,36 @@ exports.setEvalFunction = function(newCu */ exports.unsetEvalFunction = function() { customEval = undefined; }; /** * 'eval' command */ -var evalCommandSpec = { +var evalCmd = { + item: 'command', name: '{', params: [ { name: 'javascript', type: 'javascript', description: '' } ], hidden: true, returnType: 'object', description: { key: 'cliEvalJavascript' }, exec: function(args, context) { return customEval(args.javascript); }, - evalRegexp: /^\s*{\s*/ -}; - + isCommandRegexp: /^\s*{\s*/ +}; + +exports.items = [ evalCmd ]; /** * This is a special assignment to reflect the command itself. */ function CommandAssignment() { var commandParamMetadata = { name: '__command', type: 'command' }; // This is a hack so that rather than reply with a generic description of the // command assignment, we reply with the description of the assigned command, @@ -6164,17 +7491,17 @@ function UnassignedAssignment(requisitio } }); this.paramIndex = -1; this.onAssignmentChange = util.createEvent('UnassignedAssignment.onAssignmentChange'); // synchronize is ok because we can be sure that param type is synchronous var parsed = this.param.type.parse(arg, requisition.executionContext); this.conversion = util.synchronize(parsed); - this.conversion.assign(this); + this.conversion.assignment = this; } UnassignedAssignment.prototype = Object.create(Assignment.prototype); UnassignedAssignment.prototype.getStatus = function(arg) { return this.conversion.getStatus(); }; @@ -6225,19 +7552,19 @@ function Requisition(environment, doc, c this.shell = { cwd: '/', // Where we store the current working directory env: {} // Where we store the current environment }; // The command that we are about to execute. // @see setCommandConversion() this.commandAssignment = new CommandAssignment(); - var promise = this.setAssignment(this.commandAssignment, null, - { skipArgUpdate: true }); - util.synchronize(promise); + var assignPromise = this.setAssignment(this.commandAssignment, null, + { internal: true }); + util.synchronize(assignPromise); // The object that stores of Assignment objects that we are filling out. // The Assignment objects are stored under their param.name for named // lookup. Note: We make use of the property of Javascript objects that // they are not just hashmaps, but linked-list hashmaps which iterate in // insertion order. // _assignments excludes the commandAssignment. this._assignments = {}; @@ -6250,16 +7577,20 @@ function Requisition(environment, doc, c // Used to store cli arguments that were not assigned to parameters this._unassigned = []; // Temporarily set this to true to prevent _assignmentChanged resetting // argument positions this._structuralChangeInProgress = false; + // Changes can be asynchronous, when one update starts before another + // finishes we abandon the former change + this._nextUpdateId = 0; + // We can set a prefix to typed commands to make it easier to focus on // Allowing us to type "add -a; commit" in place of "git add -a; git commit" this.prefix = ''; this.commandAssignment.onAssignmentChange.add(this._commandAssignmentChanged, this); this.commandAssignment.onAssignmentChange.add(this._assignmentChanged, this); this.onAssignmentChange = util.createEvent('Requisition.onAssignmentChange'); @@ -6272,16 +7603,55 @@ function Requisition(environment, doc, c Requisition.prototype.destroy = function() { this.commandAssignment.onAssignmentChange.remove(this._commandAssignmentChanged, this); this.commandAssignment.onAssignmentChange.remove(this._assignmentChanged, this); delete this.document; delete this.environment; }; +/** + * If we're about to make an asynchronous change when other async changes could + * overtake this one, then we want to be able to bail out if overtaken. The + * value passed back from beginChange should be passed to endChangeCheckOrder + * on completion of calculation, before the results are applied in order to + * check that the calculation has not been overtaken + */ +Requisition.prototype._beginChange = function() { + this._structuralChangeInProgress = true; + var updateId = this._nextUpdateId; + this._nextUpdateId++; + return updateId; +}; + +/** + * Check to see if another change has started since updateId started. + * This allows us to bail out of an update. + * It's hard to make updates atomic because until you've responded to a parse + * of the command argument, you don't know how to parse the arguments to that + * command. + */ +Requisition.prototype._isChangeCurrent = function(updateId) { + return updateId + 1 === this._nextUpdateId; +}; + +/** + * See notes on beginChange + */ +Requisition.prototype._endChangeCheckOrder = function(updateId) { + if (updateId + 1 !== this._nextUpdateId) { + // An update that started after we did has already finished, so our + // changes are out of date. Abandon further work. + return false; + } + + this._structuralChangeInProgress = false; + return true; +}; + var legacy = false; /** * Functions and data related to the execution of a command */ Object.defineProperty(Requisition.prototype, 'executionContext', { get: function() { if (this._executionContext == null) { @@ -6424,19 +7794,18 @@ Requisition.prototype._commandAssignment this._assignments = {}; var command = this.commandAssignment.value; if (command) { for (var i = 0; i < command.params.length; i++) { var param = command.params[i]; var assignment = new Assignment(param, i); - var promise = this.setAssignment(assignment, null, - { skipArgUpdate: true }); - util.synchronize(promise); + var assignPromise = this.setAssignment(assignment, null, { internal: true }); + util.synchronize(assignPromise); assignment.onAssignmentChange.add(this._assignmentChanged, this); this._assignments[param.name] = assignment; } } this.assignmentCount = Object.keys(this._assignments).length; }; /** @@ -6518,16 +7887,38 @@ Requisition.prototype.getStatus = functi }, this); if (status === Status.INCOMPLETE) { status = Status.ERROR; } return status; }; /** + * If ``requisition.getStatus() != VALID`` message then return a string which + * best describes what is wrong. Generally error messages are delivered by + * looking at the error associated with the argument at the cursor, but there + * are times when you just want to say 'tell me the worst'. + * If ``requisition.getStatus() != VALID`` then return ``null``. + */ +Requisition.prototype.getStatusMessage = function() { + var message = null; + this.getAssignments(true).forEach(function(assignment) { + if (assignment.getStatus() !== Status.VALID) { + message = assignment.getMessage(); + } + }, this); + + if (message == null && this._unassigned.length !== 0) { + message = l10n.lookup('cliUnusedArg'); + } + + return message; +}; + +/** * Extract the names and values of all the assignments, and return as * an object. */ Requisition.prototype.getArgsObject = function() { var args = {}; this.getAssignments().forEach(function(assignment) { args[assignment.param.name] = assignment.conversion.isDataProvided() ? assignment.value : @@ -6555,27 +7946,29 @@ Requisition.prototype.getAssignments = f /** * Internal function to alter the given assignment using the given arg. * @param assignment The assignment to alter * @param arg The new value for the assignment. An instance of Argument, or an * instance of Conversion, or null to set the blank value. * @param options There are a number of ways to customize how the assignment * is made, including: - * - skipArgUpdate: (default:false) Adjusts the args in this requisition to keep - * things up to date. Args should only be skipped when setAssignment is being - * called as part of the update process. - * - matchPadding: (default:false) Altering the whitespace on the prefix and + * - internal: (default:false) External updates are required to do more work, + * including adjusting the args in this requisition to stay in sync. + * On the other hand non internal changes use beginChange to back out of + * changes when overtaken asynchronously. + * Setting internal:true effectively means this is being called as part of + * the update process. + * - matchPadding: (default:false) Alter the whitespace on the prefix and * suffix of the new argument to match that of the old argument. This only - * makes sense with skipArgUpdate=false - * then further take the step of + * makes sense with internal=false */ Requisition.prototype.setAssignment = function(assignment, arg, options) { options = options || {}; - if (options.skipArgUpdate !== true) { + if (!options.internal) { var originalArgs = assignment.arg.getArgs(); // Update the args array var replacementArgs = arg.getArgs(); var maxLen = Math.max(originalArgs.length, replacementArgs.length); for (var i = 0; i < maxLen; i++) { // If there are no more original args, or if the original arg was blank // (i.e. not typed by the user), we'll just need to add at the end @@ -6606,57 +7999,58 @@ Requisition.prototype.setAssignment = fu replacementArgs[i].suffix = this._args[index].suffix; } } this._args[index] = replacementArgs[i]; } } } - function setAssignmentInternal(conversion) { - var oldConversion = assignment.conversion; - - assignment.conversion = conversion; - assignment.conversion.assign(assignment); - - if (assignment.conversion.equals(oldConversion)) { - return; - } - - assignment.onAssignmentChange({ - assignment: assignment, - conversion: assignment.conversion, - oldConversion: oldConversion - }); - } + var updateId = options.internal ? null : this._beginChange(); + + var setAssignmentInternal = function(conversion) { + if (options.internal || this._endChangeCheckOrder(updateId)) { + var oldConversion = assignment.conversion; + + assignment.conversion = conversion; + assignment.conversion.assignment = assignment; + + if (!assignment.conversion.equals(oldConversion)) { + assignment.onAssignmentChange({ + assignment: assignment, + conversion: assignment.conversion, + oldConversion: oldConversion + }); + } + } + + return promise.resolve(undefined); + }.bind(this); if (arg == null) { var blank = assignment.param.type.getBlank(this.executionContext); - setAssignmentInternal(blank); - } - else if (typeof arg.getStatus === 'function') { - setAssignmentInternal(arg); - } - else { - var parsed = assignment.param.type.parse(arg, this.executionContext); - return parsed.then(function(conversion) { - setAssignmentInternal(conversion); - }.bind(this)); - } - - return promise.resolve(undefined); + return setAssignmentInternal(blank); + } + + if (typeof arg.getStatus === 'function') { + // It's not really an arg, it's a conversion already + return setAssignmentInternal(arg); + } + + var parsed = assignment.param.type.parse(arg, this.executionContext); + return parsed.then(setAssignmentInternal); }; /** * Reset all the assignments to their default values */ Requisition.prototype.setBlankArguments = function() { this.getAssignments().forEach(function(assignment) { - var promise = this.setAssignment(assignment, null, { skipArgUpdate: true }); - util.synchronize(promise); + var assignPromise = this.setAssignment(assignment, null, { internal: true }); + util.synchronize(assignPromise); }, this); }; /** * Complete the argument at <tt>cursor</tt>. * Basically the same as: * assignment = getAssignmentAt(cursor); * assignment.value = assignment.conversion.predictions[0]; @@ -6707,36 +8101,37 @@ Requisition.prototype.complete = functio } } else { // Mutate this argument to hold the completion var arg = assignment.arg.beget({ text: prediction.name, dontQuote: (assignment === this.commandAssignment) }); - var assignmentPromise = this.setAssignment(assignment, arg); + var assignPromise = this.setAssignment(assignment, arg); if (!prediction.incomplete) { - assignmentPromise = assignmentPromise.then(function() { + assignPromise = assignPromise.then(function() { // The prediction is complete, add a space to let the user move-on return this._addSpace(assignment).then(function() { // Bug 779443 - Remove or explain the re-parse if (assignment instanceof UnassignedAssignment) { return this.update(this.toString()); } }.bind(this)); }.bind(this)); } - outstanding.push(assignmentPromise); + outstanding.push(assignPromise); } return promise.all(outstanding).then(function() { this.onTextChange(); this.onTextChange.resumeFire(); + return true; }.bind(this)); }.bind(this)); }; /** * A test method to check that all args are assigned in some way */ Requisition.prototype._assertArgsAssigned = function() { @@ -6767,33 +8162,33 @@ Requisition.prototype._addSpace = functi */ Requisition.prototype.decrement = function(assignment) { var replacement = assignment.param.type.decrement(assignment.conversion.value, this.executionContext); if (replacement != null) { var str = assignment.param.type.stringify(replacement, this.executionContext); var arg = assignment.conversion.arg.beget({ text: str }); - var promise = this.setAssignment(assignment, arg); - util.synchronize(promise); + var assignPromise = this.setAssignment(assignment, arg); + util.synchronize(assignPromise); } }; /** * Replace the current value with the higher value if such a concept exists. */ Requisition.prototype.increment = function(assignment) { var replacement = assignment.param.type.increment(assignment.conversion.value, this.executionContext); if (replacement != null) { var str = assignment.param.type.stringify(replacement, this.executionContext); var arg = assignment.conversion.arg.beget({ text: str }); - var promise = this.setAssignment(assignment, arg); - util.synchronize(promise); + var assignPromise = this.setAssignment(assignment, arg); + util.synchronize(assignPromise); } }; /** * Extract a canonical version of the input */ Requisition.prototype.toCanonicalString = function() { var line = []; @@ -6831,24 +8226,24 @@ Requisition.prototype.toCanonicalString * to display this typed input. It's a bit like toString on steroids. * <p> * The returned object has the following members:<ul> * <li>character: The character to which this arg trace refers. * <li>arg: The Argument to which this character is assigned. * <li>part: One of ['prefix'|'text'|suffix'] - how was this char understood * </ul> * <p> - * The Argument objects are as output from #tokenize() rather than as applied - * to Assignments by #_assign() (i.e. they are not instances of NamedArgument, + * The Argument objects are as output from tokenize() rather than as applied + * to Assignments by _assign() (i.e. they are not instances of NamedArgument, * ArrayArgument, etc). * <p> * To get at the arguments applied to the assignments simply call * <tt>arg.assignment.arg</tt>. If <tt>arg.assignment.arg !== arg</tt> then * the arg applied to the assignment will contain the original arg. - * See #_assign() for details. + * See _assign() for details. */ Requisition.prototype.createInputArgTrace = function() { if (!this._args) { throw new Error('createInputMap requires a command line. See source.'); // If this is a problem then we can fake command line input using // something like the code in #toCanonicalString(). } @@ -7080,18 +8475,18 @@ Requisition.prototype.exec = function(op } if (!command) { throw new Error('Unknown command'); } // Display JavaScript input without the initial { or closing } var typed = this.toString(); - if (evalCommandSpec.evalRegexp.test(typed)) { - typed = typed.replace(evalCommandSpec.evalRegexp, ''); + if (evalCmd.isCommandRegexp.test(typed)) { + typed = typed.replace(evalCmd.isCommandRegexp, ''); // Bug 717763: What if the JavaScript naturally ends with a }? typed = typed.replace(/\s*}\s*$/, ''); } var output = new Output({ command: command, args: args, typed: typed, @@ -7147,20 +8542,20 @@ Requisition.prototype.updateExec = funct */ Requisition.prototype.clear = function() { this._structuralChangeInProgress = true; var arg = new Argument('', '', ''); this._args = [ arg ]; var commandType = this.commandAssignment.param.type; - var promise = commandType.parse(arg, this.executionContext); + var parsePromise = commandType.parse(arg, this.executionContext); this.setAssignment(this.commandAssignment, - util.synchronize(promise), - { skipArgUpdate: true }); + util.synchronize(parsePromise), + { internal: true }); this._structuralChangeInProgress = false; this.onTextChange(); }; /** * Helper to find the 'data-command' attribute, used by |update()| */ @@ -7180,25 +8575,33 @@ function getDataCommandAttribute(element Requisition.prototype.update = function(typed) { if (typeof HTMLElement !== 'undefined' && typed instanceof HTMLElement) { typed = getDataCommandAttribute(typed); } if (typeof Event !== 'undefined' && typed instanceof Event) { typed = getDataCommandAttribute(typed.currentTarget); } - this._structuralChangeInProgress = true; + var updateId = this._beginChange(); this._args = exports.tokenize(typed); var args = this._args.slice(0); // i.e. clone return this._split(args).then(function() { + if (!this._isChangeCurrent(updateId)) { + return false; + } + return this._assign(args).then(function() { - this._structuralChangeInProgress = false; - this.onTextChange(); + if (this._endChangeCheckOrder(updateId)) { + this.onTextChange(); + return true; + } + + return false; }.bind(this)); }.bind(this)); }; /** * For test/debug use only. The output from this function is subject to wanton * random change without notice, and should not be relied upon to even exist * at some later date. @@ -7451,42 +8854,42 @@ function isSimple(typed) { /** * Looks in the canon for a command extension that matches what has been * typed at the command line. */ Requisition.prototype._split = function(args) { // We're processing args, so we don't want the assignments that we make to // try to adjust other args assuming this is an external update - var noArgUp = { skipArgUpdate: true }; + var noArgUp = { internal: true }; // Handle the special case of the user typing { javascript(); } // We use the hidden 'eval' command directly rather than shift()ing one of // the parameters, and parse()ing it. var conversion = undefined; if (args[0].type === 'ScriptArgument') { // Special case: if the user enters { console.log('foo'); } then we need to // use the hidden 'eval' command - conversion = new Conversion(evalCommand, new ScriptArgument()); + conversion = new Conversion(getEvalCommand(), new ScriptArgument()); return this.setAssignment(this.commandAssignment, conversion, noArgUp); } var argsUsed = 1; var parsePromise; var commandType = this.commandAssignment.param.type; while (argsUsed <= args.length) { var arg = (argsUsed === 1) ? args[0] : new MergedArgument(args, 0, argsUsed); // Making the commandType.parse() promise as synchronous is OK because we // know that commandType is a synchronous type. - if (this.prefix != nul