Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
Browse files

Merge branch 'asynctypes-685526'

  • Loading branch information...
commit cf87afb7b7cca8dc0e1db780a0a045519d643c51 2 parents e8b2526 + 6317fed
@joewalker joewalker authored
Showing with 7,742 additions and 4,141 deletions.
  1. +1 −1  .coverignore
  2. +2 −0  .travis.yml
  3. +1 −2  docs/developing-gcli.md
  4. +10 −9 docs/writing-commands.md
  5. +4 −6 docs/writing-types.md
  6. +11 −4 gcli.js
  7. +2 −1  index.html
  8. +2 −1  lib/demo/commands/basic.js
  9. +2 −1  lib/demo/commands/bugs.js
  10. +2 −1  lib/demo/commands/demo.js
  11. +2 −0  lib/demo/index.js
  12. +1 −0  lib/gcli/argument.js
  13. +12 −14 lib/gcli/canon.js
  14. +249 −154 lib/gcli/cli.js
  15. +5 −5 lib/gcli/commands/help.js
  16. +3 −1 lib/gcli/commands/intro.js
  17. +2 −1  lib/gcli/commands/pref.js
  18. +5 −4 lib/gcli/commands/pref_list.js
  19. +2 −0  lib/gcli/history.js
  20. +3 −1 lib/gcli/index.js
  21. +1 −1  lib/gcli/nls/strings.js
  22. +0 −211 lib/gcli/promise.js
  23. +2 −1  lib/gcli/settings.js
  24. +36 −21 lib/gcli/types.js
  25. +69 −56 lib/gcli/types/basic.js
  26. +39 −28 lib/gcli/types/command.js
  27. +26 −23 lib/gcli/types/javascript.js
  28. +19 −17 lib/gcli/types/node.js
  29. +5 −3 lib/gcli/types/resource.js
  30. +163 −99 lib/gcli/types/selection.js
  31. +12 −9 lib/gcli/types/setting.js
  32. +24 −17 lib/gcli/types/spell.js
  33. +8 −8 lib/gcli/ui/arg_fetch.js
  34. +155 −116 lib/gcli/ui/completer.js
  35. +8 −6 lib/gcli/ui/display.js
  36. +12 −9 lib/gcli/ui/fields.js
  37. +26 −24 lib/gcli/ui/fields/basic.js
  38. +31 −23 lib/gcli/ui/fields/javascript.js
  39. +4 −4 lib/gcli/ui/fields/menu.js
  40. +29 −19 lib/gcli/ui/fields/selection.js
  41. +3 −2 lib/gcli/ui/focus.js
  42. +49 −22 lib/gcli/ui/inputter.js
  43. +4 −2 lib/gcli/ui/intro.js
  44. +5 −3 lib/gcli/ui/output_terminal.js
  45. +1 −0  lib/gcli/ui/prompt.js
  46. +10 −7 lib/gcli/ui/tooltip.js
  47. +3 −2 lib/gcli/ui/view.js
  48. +798 −246 lib/gclitest/helpers.js
  49. +125 −104 lib/gclitest/index.js
  50. +38 −22 lib/gclitest/mockCommands.js
  51. +1 −0  lib/gclitest/mockSettings.js
  52. +2 −0  lib/gclitest/recurse.js
  53. +2 −0  lib/gclitest/requirable.js
  54. +3 −2 lib/gclitest/suite.js
  55. +152 −0 lib/gclitest/testAsync.js
  56. +163 −107 lib/gclitest/testCanon.js
  57. +1,249 −450 lib/gclitest/testCli.js
  58. +464 −379 lib/gclitest/testCompletion.js
  59. +571 −124 lib/gclitest/testExec.js
  60. +38 −32 lib/gclitest/testFocus.js
  61. +120 −102 lib/gclitest/testHelp.js
  62. +5 −9 lib/gclitest/testHistory.js
  63. +342 −296 lib/gclitest/testIncomplete.js
  64. +23 −25 lib/gclitest/testInputter.js
  65. +44 −48 lib/gclitest/testIntro.js
  66. +404 −133 lib/gclitest/testJs.js
  67. +454 −175 lib/gclitest/testKeyboard.js
  68. +20 −18 lib/gclitest/testMenu.js
  69. +288 −244 lib/gclitest/testNode.js
  70. +236 −175 lib/gclitest/testPref.js
  71. +26 −25 lib/gclitest/testRequire.js
  72. +82 −46 lib/gclitest/testResource.js
  73. +6 −14 lib/gclitest/testScratchpad.js
  74. +48 −51 lib/gclitest/testSettings.js
  75. +2 −6 lib/gclitest/testSpell.js
  76. +9 −7 lib/gclitest/testSplit.js
  77. +9 −8 lib/gclitest/testTokenize.js
  78. +104 −62 lib/gclitest/testTooltip.js
  79. +14 −16 lib/gclitest/testTypes.js
  80. +3 −1 lib/gclitest/testUtil.js
  81. +2 −0  lib/gclitest/unrequirable.js
  82. +40 −0 lib/server/commands/exit.js
  83. +6 −28 lib/server/commands/firefox.js
  84. +6 −3 lib/server/commands/git.js
  85. +1 −1  lib/server/commands/make.js
  86. +13 −3 lib/server/commands/test.js
  87. +34 −29 lib/server/index.js
  88. +12 −33 lib/test/assert.js
  89. +8 −14 lib/test/commands/test.js
  90. +141 −92 lib/test/examiner.js
  91. +4 −3 lib/test/status.js
  92. +110 −56 lib/{gcli/ui → util}/domtemplate.js
  93. +1 −0  lib/{gcli → util}/host.js
  94. +2 −0  lib/{gcli → util}/l10n.js
  95. +28 −0 lib/{gcli → util}/legacy.js
  96. +227 −0 lib/util/promise.js
  97. +133 −1 lib/{gcli → util}/util.js
  98. +2 −1  localtest.html
  99. +2 −0  mozilla/gcli/gclichrome.js
  100. +5 −0 mozilla/gcli/index.js
  101. +3 −1 mozilla/gcli/settings.js
  102. +3 −3 mozilla/gcli/ui/ffdisplay.js
  103. +2 −0  mozilla/{gcli/ui → util}/domtemplate.js
  104. +1 −0  mozilla/{gcli → util}/host.js
  105. +5 −3 mozilla/{gcli → util}/l10n.js
  106. +2 −0  mozilla/{gcli → util}/promise.js
  107. +4 −4 package.json
  108. +35 −0 phantom-test.js
View
2  .coverignore
@@ -10,6 +10,6 @@ node_modules/jsdom
node_modules/socket.io
node_modules/test
-node_modules/gcli/promise.js
+node_modules/util/promise.js
node_modules/gcli/index.js
View
2  .travis.yml
@@ -2,4 +2,6 @@
language: node_js
node_js:
- "0.8"
+script:
+ - "node gcli.js test && phantomjs ./phantom-test.js"
View
3  docs/developing-gcli.md
@@ -114,7 +114,7 @@ browser-like as possible.
## Testing
-GCLI contains 3 test suites:
+GCLI contains 2 test suites:
- JS level testing is run with the ``test`` command. The tests are located in
``lib/gclitest`` and they use the test runner in ``lib/test``. This is fairly
@@ -125,7 +125,6 @@ GCLI contains 3 test suites:
- Browser integration tests are included in ``browser_webconsole_gcli_*.js``,
in ``toolkit/components/console/hudservice/tests/browser``. These are
run with the rest of the Mozilla test suite.
-- Selenium tests for testing UI interaction are included in ``selenium-tests``.
## Coding Conventions
View
19 docs/writing-commands.md
@@ -203,7 +203,7 @@ Initially the available types are:
- number
- array
- selection
-- deferred
+- delegate
This list can be extended. See [Writing Types](writing-types.md) on types for
more information.
@@ -286,9 +286,9 @@ properties are used by the command line when up and down are pressed and in
the input type of a dialog generated from this command.
-## Deferred types
+## Delegate types
-Deferred types are needed when the type of some parameter depends on the type
+Delegate types are needed when the type of some parameter depends on the type
of another parameter. For example:
» set height 100
@@ -306,16 +306,17 @@ We can achieve this as follows:
{
name: 'value',
type: {
- name: 'deferred',
- defer: function() { ... }
+ name: 'delegate',
+ delegateType: function() { ... }
}
}
],
...
});
-Several details are left out of this example, like how the defer function knows
-what the current setting is. See the ``pref`` command in Ace for an example.
+Several details are left out of this example, like how the delegateType()
+function knows what the current setting is. See the ``pref`` command for an
+example.
## Array types
@@ -569,7 +570,7 @@ GCLI will interpret this as HTML, and parse it for display.
and other XML documents. In an HTML document it's functionally equivalent to
``context.document.createElement('div')``. If your command is likely to be used
in Firefox or another XML environment, you should use it. You can import it
-with ``var util = require('gcli/util');``.
+with ``var util = require('util/util');``.
GCLI will use the returned HTML element as returned. See notes on ``context``
above.
@@ -643,7 +644,7 @@ types this is enough detail. There are a number of exceptions:
]
}
-* Deferred type. It is generally best to inherit from Deferred in order to
+* Delegate type. It is generally best to inherit from Delegate in order to
provide a customization of this type. See settingValue for an example.
See below for more information.
View
10 docs/writing-types.md
@@ -9,18 +9,18 @@ number of built in types:
* number. A JavaScript number
* boolean. A Javascript boolean
* selection. This is an selection from a number of alternatives
-* deferred. This type could change depending on other factors, but is well
+* delegate. This type could change depending on other factors, but is well
defined when one of the conversion routines is called.
There are a number of additional types defined by Pilot and GCLI as
-extensions to the ``selection`` and ``deferred`` types
+extensions to the ``selection`` and ``delegate`` types
* setting. One of the defined settings
* settingValue. A value that can be applied to an associated setting.
* command. One of the defined commands
Most of our types are 'static' e.g. there is only one type of 'string', however
-some types like 'selection' and 'deferred' are customizable.
+some types like 'selection' and 'delegate' are customizable.
All types must inherit from Type and have the following methods:
@@ -42,7 +42,7 @@ All types must inherit from Type and have the following methods:
/**
* The plug-in system, and other things need to know what this type is
* called. The name alone is not enough to fully specify a type. Types like
- * 'selection' and 'deferred' need extra data, however this function returns
+ * 'selection' and 'delegate' need extra data, however this function returns
* only the name, not the extra data.
* <p>In old bespin, equality was based on the name. This may turn out to be
* important in Ace too.
@@ -104,5 +104,3 @@ This is an example of a very simple new password field type:
PasswordField.claim = function(type) {
return type.name === 'password' ? Field.claim.MATCH : Field.claim.NO_MATCH;
};
-
-
View
15 gcli.js
@@ -54,7 +54,8 @@ else {
{ name: 'test', main: 'index', lib: '.' },
{ name: 'gclitest', main: 'index', lib: '.' },
{ name: 'demo', main: 'index', lib: '.' },
- { name: 'server', main: 'index', lib: '.' }
+ { name: 'server', main: 'index', lib: '.' },
+ { name: 'util', main: 'index', lib: '.' }
]
}
});
@@ -65,12 +66,13 @@ else {
exports.require('gcli/index');
// Load the commands defined in Node modules
-require('./lib/server/commands/unamd').startup();
+require('./lib/server/commands/exit').startup();
require('./lib/server/commands/firefox').startup();
// require('./lib/server/commands/git').startup();
require('./lib/server/commands/make').startup();
require('./lib/server/commands/standard').startup();
require('./lib/server/commands/test').startup();
+require('./lib/server/commands/unamd').startup();
// Load the commands defined in CommonJS modules
var help = exports.require('gcli/commands/help');
@@ -88,6 +90,11 @@ if (process.argv.length < 3) {
}
else {
var command = process.argv.slice(2).join(' ');
- var reply = server.exec(command);
- console.log(reply);
+
+ server.exec(command, function(message, isError) {
+ console.log(message);
+ if (isError) {
+ process.exit(1);
+ }
+ });
}
View
3  index.html
@@ -34,7 +34,8 @@
{ name: "gcli", main: "index", lib: "." },
{ name: "test", main: "index", lib: "." },
{ name: "gclitest", main: "index", lib: "." },
- { name: "demo", main: "index", lib: "." }
+ { name: "demo", main: "index", lib: "." },
+ { name: "util", main: "index", lib: "." }
]
}
});
View
3  lib/demo/commands/basic.js
@@ -16,6 +16,7 @@
define(function(require, exports, module) {
+'use strict';
var gcli = require('gcli/index');
@@ -62,7 +63,7 @@ var echo = {
name: 'echo',
description: {
root: 'Show a message',
- fr_fr: 'Afficher un message',
+ fr_fr: 'Afficher un message'
},
params: [
{
View
3  lib/demo/commands/bugs.js
@@ -16,9 +16,10 @@
define(function(require, exports, module) {
+'use strict';
var gcli = require('gcli/index');
-var util = require('gcli/util');
+var util = require('util/util');
var bugsHtml = require('text!demo/commands/bugs.html');
View
3  lib/demo/commands/demo.js
@@ -16,6 +16,7 @@
define(function(require, exports, module) {
+'use strict';
var gcli = require('gcli/index');
@@ -135,7 +136,7 @@ var gcliTwonums = {
defaultValue: 3
}
]
- },
+ }
],
returnType: 'html',
exec: function(args, context) {
View
2  lib/demo/index.js
@@ -16,6 +16,8 @@
define(function(require, exports, module) {
+ 'use strict';
+
require('gcli/index');
require('gcli/commands/help').startup();
View
1  lib/gcli/argument.js
@@ -16,6 +16,7 @@
define(function(require, exports, module) {
+'use strict';
/**
* Thinking out loud here:
View
26 lib/gcli/canon.js
@@ -15,11 +15,13 @@
*/
define(function(require, exports, module) {
-var canon = exports;
+'use strict';
+var canon = exports;
-var util = require('gcli/util');
-var l10n = require('gcli/l10n');
+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;
@@ -190,16 +192,16 @@ function Parameter(paramSpec, command, groupName) {
if (this._defaultValue != null) {
try {
var defaultText = this.type.stringify(this.paramSpec.defaultValue);
- var defaultConversion = this.type.parseString(defaultText);
- if (defaultConversion.getStatus() !== Status.VALID) {
- throw new Error('In ' + this.command.name + '/' + this.name +
+ this.type.parseString(defaultText).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), console.error);
}
catch (ex) {
- throw new Error('In ' + this.command.name + '/' + this.name +
- ': ' + ex);
+ throw new Error('In ' + this.command.name + '/' + this.name + ': ' + ex);
}
}
@@ -246,11 +248,7 @@ Parameter.prototype.isKnownAs = function(name) {
* parseString on an empty string
*/
Parameter.prototype.getBlank = function() {
- if (this.type.getBlank) {
- return this.type.getBlank();
- }
-
- return this.type.parseString('');
+ return this.type.getBlank();
};
/**
View
403 lib/gcli/cli.js
@@ -16,13 +16,14 @@
define(function(require, exports, module) {
+'use strict';
-var util = require('gcli/util');
-var view = require('gcli/ui/view');
-var l10n = require('gcli/l10n');
+var Promise = require('util/promise');
+var util = require('util/util');
+var l10n = require('util/l10n');
+var view = require('gcli/ui/view');
var canon = require('gcli/canon');
-var Q = require('gcli/promise');
var CommandOutputManager = require('gcli/canon').CommandOutputManager;
var Status = require('gcli/types').Status;
@@ -142,19 +143,20 @@ Assignment.prototype.getPredictionAt = function(index) {
}
if (this.isInName()) {
- return undefined;
+ return Promise.resolve(undefined);
}
- var predictions = this.getPredictions();
- if (predictions.length === 0) {
- return undefined;
- }
+ return this.getPredictions().then(function(predictions) {
+ if (predictions.length === 0) {
+ return undefined;
+ }
- index = index % predictions.length;
- if (index < 0) {
- index = predictions.length + index;
- }
- return predictions[index];
+ index = index % predictions.length;
+ if (index < 0) {
+ index = predictions.length + index;
+ }
+ return predictions[index];
+ }.bind(this), console.error);
};
/**
@@ -187,7 +189,8 @@ Assignment.prototype.ensureVisibleArgument = function() {
text: '',
prefixSpace: this.param instanceof CommandAssignment
});
- this.conversion = this.param.type.parse(arg);
+ // For trivial input like { test: '' }, parse() should be synchronous ...
+ this.conversion = util.synchronize(this.param.type.parse(arg));
this.conversion.assign(this);
return true;
@@ -229,6 +232,10 @@ Assignment.prototype.toString = function() {
*/
Object.defineProperty(Assignment.prototype, '_summaryJson', {
get: function() {
+ var predictionCount = '<async>';
+ this.getPredictions().then(function(predictions) {
+ predictionCount = predictions.length;
+ }, console.log);
return {
param: this.param.name + '/' + this.param.type.name,
defaultValue: this.param.defaultValue,
@@ -236,7 +243,7 @@ Object.defineProperty(Assignment.prototype, '_summaryJson', {
value: this.value,
message: this.getMessage(),
status: this.getStatus().toString(),
- predictionCount: this.getPredictions().length
+ predictionCount: predictionCount
};
},
enumerable: true
@@ -346,7 +353,8 @@ function UnassignedAssignment(requisition, arg) {
this.paramIndex = -1;
this.onAssignmentChange = util.createEvent('UnassignedAssignment.onAssignmentChange');
- this.conversion = this.param.type.parse(arg);
+ // synchronize is ok because we can be sure that param type is synchronous
+ this.conversion = util.synchronize(this.param.type.parse(arg));
this.conversion.assign(this);
}
@@ -402,7 +410,9 @@ function Requisition(environment, doc, commandOutputManager) {
// The command that we are about to execute.
// @see setCommandConversion()
this.commandAssignment = new CommandAssignment();
- this.setAssignment(this.commandAssignment, null);
+ var promise = this.setAssignment(this.commandAssignment, null,
+ { skipArgUpdate: true });
+ util.synchronize(promise);
// The object that stores of Assignment objects that we are filling out.
// The Assignment objects are stored under their param.name for named
@@ -487,7 +497,9 @@ Requisition.prototype._commandAssignmentChanged = function(ev) {
for (var i = 0; i < command.params.length; i++) {
var param = command.params[i];
var assignment = new Assignment(param, i);
- this.setAssignment(assignment, null);
+ var promise = this.setAssignment(assignment, null,
+ { skipArgUpdate: true });
+ util.synchronize(promise);
assignment.onAssignmentChange.add(this._assignmentChanged, this);
this._assignments[param.name] = assignment;
}
@@ -616,16 +628,17 @@ Requisition.prototype.getAssignments = function(includeCommand) {
* 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:
- * - argUpdate: (default:false) Adjusts the args in this requisition to keep
+ * - 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) If argUpdate=true, and matchPadding=true
- * then further take the step of altering the whitespace on the prefix and
- * suffix of the new argument to match that of the old argument.
+ * - matchPadding: (default:false) Altering 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
*/
Requisition.prototype.setAssignment = function(assignment, arg, options) {
options = options || {};
- if (options.argUpdate) {
+ if (options.skipArgUpdate !== true) {
var originalArgs = assignment.arg.getArgs();
// Update the args array
@@ -666,31 +679,36 @@ Requisition.prototype.setAssignment = function(assignment, arg, options) {
}
}
- var conversion;
+ 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
+ });
+ }
+
if (arg == null) {
- conversion = assignment.param.type.getBlank();
+ setAssignmentInternal(assignment.param.type.getBlank());
}
else if (typeof arg.getStatus === 'function') {
- conversion = arg;
+ setAssignmentInternal(arg);
}
else {
- conversion = assignment.param.type.parse(arg);
+ return assignment.param.type.parse(arg).then(function(conversion) {
+ setAssignmentInternal(conversion);
+ }.bind(this), console.error);
}
- 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
- });
+ return Promise.resolve(undefined);
};
/**
@@ -698,7 +716,8 @@ Requisition.prototype.setAssignment = function(assignment, arg, options) {
*/
Requisition.prototype.setBlankArguments = function() {
this.getAssignments().forEach(function(assignment) {
- this.setAssignment(assignment, null);
+ var promise = this.setAssignment(assignment, null, { skipArgUpdate: true });
+ util.synchronize(promise);
}, this);
};
@@ -709,59 +728,89 @@ Requisition.prototype.setBlankArguments = function() {
* assignment.value = assignment.conversion.predictions[0];
* Except it's done safely, and with particular care to where we place the
* space, which is complex, and annoying if we get it wrong.
+ *
+ * WARNING: complete() can happen asynchronously.
+ *
* @param cursor The cursor configuration. Should have start and end properties
* which should be set to start and end of the selection.
* @param predictionChoice The index of the prediction that we should choose.
* This number is not bounded by the size of the prediction array, we take the
* modulus to get it within bounds
+ * @return A promise which completes (with undefined) when any outstanding
+ * completion tasks are done.
*/
Requisition.prototype.complete = function(cursor, predictionChoice) {
var assignment = this.getAssignmentAt(cursor.start);
- this.onTextChange.holdFire();
-
- var prediction = assignment.getPredictionAt(predictionChoice);
- if (prediction == null) {
- // No predictions generally means we shouldn't change anything on TAB, but
- // TAB has the connotation of 'next thing' and when we're at the end of
- // a thing that implies that we should add a space. i.e.
- // 'help<TAB>' -> 'help '
- // But we should only do this if the thing that we're 'completing' is valid
- // and doesn't already end in a space.
- if (assignment.arg.suffix.slice(-1) !== ' ' &&
- assignment.getStatus() === Status.VALID) {
- this._addSpace(assignment);
+ var predictionPromise = assignment.getPredictionAt(predictionChoice);
+ return predictionPromise.then(function(prediction) {
+ var outstanding = [];
+ this.onTextChange.holdFire();
+
+ // Note: Since complete is asynchronous we should perhaps have a system to
+ // bail out of making changes if the command line has changed since TAB
+ // was pressed. It's not yet clear if this will be a problem.
+
+ if (prediction == null) {
+ // No predictions generally means we shouldn't change anything on TAB,
+ // but TAB has the connotation of 'next thing' and when we're at the end
+ // of a thing that implies that we should add a space. i.e.
+ // 'help<TAB>' -> 'help '
+ // But we should only do this if the thing that we're 'completing' is
+ // valid and doesn't already end in a space.
+ if (assignment.arg.suffix.slice(-1) !== ' ' &&
+ assignment.getStatus() === Status.VALID) {
+ outstanding.push(this._addSpace(assignment));
+ }
+
+ // Also add a space if we are in the name part of an assignment, however
+ // this time we don't want the 'push the space to the next assignment'
+ // logic, so we don't use addSpace
+ if (assignment.isInName()) {
+ var newArg = assignment.conversion.arg.beget({ prefixPostSpace: true });
+ var p = this.setAssignment(assignment, newArg);
+ outstanding.push(p);
+ }
}
+ else {
+ // Mutate this argument to hold the completion
+ var arg = assignment.arg.beget({
+ text: prediction.name,
+ dontQuote: (assignment === this.commandAssignment)
+ });
+ var promise = this.setAssignment(assignment, arg);
+
+ if (!prediction.incomplete) {
+ promise = promise.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));
+ }
- // Also add a space if we are in the name part of an assignment, however
- // this time we don't want the 'push the space to the next assignment'
- // logic, so we don't use addSpace
- if (assignment.isInName()) {
- var newArg = assignment.conversion.arg.beget({ prefixPostSpace: true });
- this.setAssignment(assignment, newArg, { argUpdate: true });
+ outstanding.push(promise);
}
- }
- else {
- // Mutate this argument to hold the completion
- var arg = assignment.arg.beget({
- text: prediction.name,
- dontQuote: (assignment === this.commandAssignment)
- });
- this.setAssignment(assignment, arg, { argUpdate: true });
- if (!prediction.incomplete) {
- // The prediction is complete, add a space to let the user move-on
- this._addSpace(assignment);
+ return util.all(outstanding).then(function() {
+ this.onTextChange();
+ this.onTextChange.resumeFire();
+ }.bind(this));
+ }.bind(this));
+};
- // Bug 779443 - Remove or explain the reparse
- if (assignment instanceof UnassignedAssignment) {
- this.update(this.toString());
- }
+/**
+ * A test method to check that all args are assigned in some way
+ */
+Requisition.prototype._assertArgsAssigned = function() {
+ this._args.forEach(function(arg) {
+ if (arg.assignment == null) {
+ console.log('No assignment for ' + arg);
}
- }
-
- this.onTextChange();
- this.onTextChange.resumeFire();
+ }, this);
};
/**
@@ -772,7 +821,10 @@ Requisition.prototype.complete = function(cursor, predictionChoice) {
Requisition.prototype._addSpace = function(assignment) {
var arg = assignment.conversion.arg.beget({ suffixSpace: true });
if (arg !== assignment.conversion.arg) {
- this.setAssignment(assignment, arg, { argUpdate: true });
+ return this.setAssignment(assignment, arg);
+ }
+ else {
+ return Promise.resolve(undefined);
}
};
@@ -784,7 +836,8 @@ Requisition.prototype.decrement = function(assignment) {
if (replacement != null) {
var str = assignment.param.type.stringify(replacement);
var arg = assignment.conversion.arg.beget({ text: str });
- this.setAssignment(assignment, arg, { argUpdate: true });
+ var promise = this.setAssignment(assignment, arg);
+ util.synchronize(promise);
}
};
@@ -796,7 +849,8 @@ Requisition.prototype.increment = function(assignment) {
if (replacement != null) {
var str = assignment.param.type.stringify(replacement);
var arg = assignment.conversion.arg.beget({ text: str });
- this.setAssignment(assignment, arg, { argUpdate: true });
+ var promise = this.setAssignment(assignment, arg);
+ util.synchronize(promise);
}
};
@@ -1044,44 +1098,41 @@ Requisition.prototype.getAssignmentAt = function(cursor) {
/**
* Entry point for keyboard accelerators or anything else that wants to execute
- * a command. There are 3 ways to call <tt>exec()</tt>:
- * 1. Without any parameters. This assumes that the command to be executed has
- * already been parsed by the requisition using <tt>update()</tt>.
- * 2. With a string parameter, or an object with a 'typed' property. This is
- * effectively a shortcut for calling <tt>update(typed); exec();</tt>
- * 3. With input having a 'command' property which is either a command object
- * (i.e. from canon.getCommand) or a string which can be passed to
- * canon.getCommand() plus and optional 'args' property which contains the
- * argument values as passed to command.exec. This method is significantly
- * faster, and designed for use from keyboard shortcuts.
- * In addition to these properties, the input parameter can contain a 'hidden'
- * property which can be set to true to hide the output from the
- * CommandOutputManager.
- * @param input (optional) The command to execute. See above.
+ * a command.
+ * @param options Object describing how the execution should be handled.
+ * (optional). Contains some of the following properties:
+ * - hidden (boolean, default=false) Should the output be hidden from the
+ * commandOutputManager for this requisition
+ * - command/args A fast shortcut to executing a known command with a known
+ * set of parsed arguments.
+ * - typed (string, deprecated) Don't use this. Also don't set the options
+ * object itself to be a string.
*/
-Requisition.prototype.exec = function(input) {
+Requisition.prototype.exec = function(options) {
var command = null;
var args = null;
var hidden = false;
- if (input && input.hidden) {
+ if (options && options.hidden) {
hidden = true;
}
- if (input) {
+ if (options) {
if (typeof input === 'string') {
- this.update(input);
+ // Deprecated - does not handle async properly
+ this.update(options);
}
- else if (typeof input.typed === 'string') {
- this.update(input.typed);
+ else if (typeof options.typed === 'string') {
+ // Deprecated - does not handle async properly
+ this.update(options.typed);
}
- else if (input.command != null) {
+ else if (options.command != null) {
// Fast track by looking up the command directly since passed args
// means there is no command line to parse.
- command = canon.getCommand(input.command);
+ command = canon.getCommand(options.command);
if (!command) {
- console.error('Command not found: ' + input.command);
+ console.error('Command not found: ' + options.command);
}
- args = input.args;
+ args = options.args;
}
}
@@ -1112,15 +1163,8 @@ Requisition.prototype.exec = function(input) {
this.commandOutputManager.onOutput({ output: output });
- var onDone = function(data) {
- output.complete(data);
- };
-
- var onError = function(error) {
- console.error(error);
- output.error = true;
- output.complete(error);
- };
+ var onDone = function(data) { output.complete(data, false); };
+ var onError = function(error) { output.complete(error, true); };
try {
var context = exports.createExecutionContext(this);
@@ -1129,14 +1173,45 @@ Requisition.prototype.exec = function(input) {
this._then(reply, onDone, onError);
}
catch (ex) {
+ console.error(ex);
onError(ex);
}
- this.update('');
+ this.clear();
return output;
};
/**
+ * A shortcut for calling update, resolving the promise and then exec.
+ * @param input The string to execute
+ * @param options Passed to exec
+ * @return A promise of an output object
+ */
+Requisition.prototype.updateExec = function(input, options) {
+ return this.update(input).then(function() {
+ return this.exec(options);
+ }.bind(this));
+};
+
+/**
+ * Similar to update('') except that it's guaranteed to execute synchronously
+ */
+Requisition.prototype.clear = function() {
+ this._structuralChangeInProgress = true;
+
+ var arg = new Argument('', '', '');
+ this._args = [ arg ];
+
+ var commandType = this.commandAssignment.param.type;
+ var conversion = util.synchronize(commandType.parse(arg));
+ this.setAssignment(this.commandAssignment, conversion,
+ { skipArgUpdate: true });
+
+ this._structuralChangeInProgress = false;
+ this.onTextChange();
+};
+
+/**
* Different types of promise have different ways of doing 'then'. This is a
* catch-all so we can ignore the differences. It also handles concrete values
* and calls onDone directly if thing is not a promise.
@@ -1147,12 +1222,12 @@ Requisition.prototype.exec = function(input) {
Requisition.prototype._then = function(thing, onDone, onError) {
var then = null;
if (thing != null && typeof thing.then === 'function') {
- // Old GCLI style / simple promises with a then function
+ // Simple promises with a then function
then = thing.then;
}
else if (thing != null && thing.promise != null &&
typeof thing.promise.then === 'function') {
- // Q / Mozilla add-ons style
+ // Deprecated: When we're passed a deferred rather than a promise
then = thing.promise.then;
}
@@ -1173,11 +1248,13 @@ Requisition.prototype.update = function(typed) {
this._args = this._tokenize(typed);
var args = this._args.slice(0); // i.e. clone
- this._split(args);
- this._assign(args);
- this._structuralChangeInProgress = false;
- this.onTextChange();
+ return this._split(args).then(function() {
+ return this._assign(args).then(function() {
+ this._structuralChangeInProgress = false;
+ this.onTextChange();
+ }.bind(this));
+ }.bind(this));
};
/**
@@ -1446,25 +1523,31 @@ function isSimple(typed) {
* 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 };
+
// 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;
+ 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());
- this.setAssignment(this.commandAssignment, conversion);
- return;
+ return this.setAssignment(this.commandAssignment, conversion, noArgUp);
}
var argsUsed = 1;
+ var commandType = this.commandAssignment.param.type;
while (argsUsed <= args.length) {
var arg = (argsUsed === 1) ?
args[0] :
new MergedArgument(args, 0, argsUsed);
- conversion = this.commandAssignment.param.type.parse(arg);
+ // Making this promise synchronous is OK because we know that commandType
+ // is a synchronous type.
+ conversion = util.synchronize(commandType.parse(arg));
// We only want to carry on if this command is a parent command,
// which means that there is a commandAssignment, but not one with
@@ -1481,12 +1564,12 @@ Requisition.prototype._split = function(args) {
argsUsed++;
}
- this.setAssignment(this.commandAssignment, conversion);
-
for (var i = 0; i < argsUsed; i++) {
args.shift();
}
+ return this.setAssignment(this.commandAssignment, conversion, noArgUp);
+
// This could probably be re-written to consume args as we go
};
@@ -1503,23 +1586,27 @@ Requisition.prototype._addUnassignedArgs = function(args) {
* Work out which arguments are applicable to which parameters.
*/
Requisition.prototype._assign = function(args) {
+ // See comment in _split. Avoid multiple updates
+ var noArgUp = { skipArgUpdate: true };
+
this._unassigned = [];
+ var outstanding = [];
if (!this.commandAssignment.value) {
this._addUnassignedArgs(args);
- return;
+ return util.all(outstanding);
}
if (args.length === 0) {
this.setBlankArguments();
- return;
+ return util.all(outstanding);
}
// Create an error if the command does not take parameters, but we have
// been given them ...
if (this.assignmentCount === 0) {
this._addUnassignedArgs(args);
- return;
+ return util.all(outstanding);
}
// Special case: if there is only 1 parameter, and that's of type
@@ -1528,8 +1615,8 @@ Requisition.prototype._assign = function(args) {
var assignment = this.getAssignment(0);
if (assignment.param.type instanceof StringType) {
var arg = (args.length === 1) ? args[0] : new MergedArgument(args);
- this.setAssignment(assignment, arg);
- return;
+ outstanding.push(this.setAssignment(assignment, arg, noArgUp));
+ return util.all(outstanding);
}
}
@@ -1573,7 +1660,7 @@ Requisition.prototype._assign = function(args) {
arrayArg.addArgument(arg);
}
else {
- this.setAssignment(assignment, arg);
+ outstanding.push(this.setAssignment(assignment, arg, noArgUp));
}
}
else {
@@ -1590,7 +1677,7 @@ Requisition.prototype._assign = function(args) {
// If not set positionally, and we can't set it non-positionally,
// we have to default it to prevent previous values surviving
if (!assignment.param.isPositionalAllowed) {
- this.setAssignment(assignment, null);
+ outstanding.push(this.setAssignment(assignment, null, noArgUp));
return;
}
@@ -1607,7 +1694,7 @@ Requisition.prototype._assign = function(args) {
}
else {
if (args.length === 0) {
- this.setAssignment(assignment, null);
+ outstanding.push(this.setAssignment(assignment, null, noArgUp));
}
else {
var arg = args.splice(0, 1)[0];
@@ -1621,7 +1708,7 @@ Requisition.prototype._assign = function(args) {
this._unassigned.push(new UnassignedAssignment(this, arg));
}
else {
- this.setAssignment(assignment, arg);
+ outstanding.push(this.setAssignment(assignment, arg, noArgUp));
}
}
}
@@ -1630,11 +1717,13 @@ Requisition.prototype._assign = function(args) {
// Now we need to assign the array argument (if any)
Object.keys(arrayArgs).forEach(function(name) {
var assignment = this.getAssignment(name);
- this.setAssignment(assignment, arrayArgs[name]);
+ outstanding.push(this.setAssignment(assignment, arrayArgs[name], noArgUp));
}, this);
// What's left is can't be assigned, but we need to extract
this._addUnassignedArgs(args);
+
+ return util.all(outstanding);
};
exports.Requisition = Requisition;
@@ -1655,6 +1744,9 @@ function Output(options) {
this.error = false;
this.start = new Date();
+ this.deferred = Promise.defer();
+ this.then = this.deferred.promise.then;
+
this.onClose = util.createEvent('Output.onClose');
this.onChange = util.createEvent('Output.onChange');
}
@@ -1679,12 +1771,20 @@ Output.prototype.changed = function(data, ev) {
* Called when there is data to display, and the command has finished executing
* See changed() for details on parameters.
*/
-Output.prototype.complete = function(data, ev) {
+Output.prototype.complete = function(data, error, ev) {
this.end = new Date();
this.duration = this.end.getTime() - this.start.getTime();
this.completed = true;
+ this.error = error;
this.changed(data, ev);
+
+ if (error) {
+ this.deferred.reject();
+ }
+ else {
+ this.deferred.resolve();
+ }
};
/**
@@ -1752,20 +1852,14 @@ Output.prototype.toDom = function(element) {
* based terminals.
*/
Output.prototype.toString = function(document) {
- var output = this.data;
- if (output == null) {
- return '';
+ if (this.data.isView) {
+ return this.data.toDom(document).textContent;
}
- if (typeof HTMLElement !== 'undefined' && output instanceof HTMLElement) {
- return output.textContent;
+ if (typeof HTMLElement !== 'undefined' && this.data instanceof HTMLElement) {
+ return this.data.textContent;
}
-
- if (output.isView) {
- return output.toDom(document).textContent;
- }
-
- return output.toString();
+ return this.data == null ? '' : this.data.toString();
};
exports.Output = Output;
@@ -1777,18 +1871,19 @@ exports.createExecutionContext = function(requisition) {
return {
exec: requisition.exec.bind(requisition),
update: requisition.update.bind(requisition),
+ updateExec: requisition.updateExec.bind(requisition),
document: requisition.document,
environment: requisition.environment,
createView: view.createView,
defer: function() {
- return Q.defer();
+ return Promise.defer();
},
/**
* @deprecated Use defer() instead, which does the same thing, but is not
* confusingly named
*/
createPromise: function() {
- return Q.defer();
+ return Promise.defer();
}
};
};
View
10 lib/gcli/commands/help.js
@@ -15,12 +15,12 @@
*/
define(function(require, exports, module) {
-var help = exports;
+'use strict';
+var util = require('util/util');
+var l10n = require('util/l10n');
var canon = require('gcli/canon');
-var l10n = require('gcli/l10n');
-var util = require('gcli/util');
var view = require('gcli/ui/view');
// Storing the HTML on exports allows other builds to alter the help template
@@ -72,11 +72,11 @@ var helpCommandSpec = {
/**
* Registration and de-registration.
*/
-help.startup = function() {
+exports.startup = function() {
canon.addCommand(helpCommandSpec);
};
-help.shutdown = function() {
+exports.shutdown = function() {
canon.removeCommand(helpCommandSpec);
};
View
4 lib/gcli/commands/intro.js
@@ -16,8 +16,10 @@
define(function(require, exports, module) {
+ 'use strict';
+
+ var l10n = require('util/l10n');
var canon = require('gcli/canon');
- var l10n = require('gcli/l10n');
var intro = require('gcli/ui/intro');
/**
View
3  lib/gcli/commands/pref.js
@@ -16,9 +16,10 @@
define(function(require, exports, module) {
+'use strict';
+var l10n = require('util/l10n');
var canon = require('gcli/canon');
-var l10n = require('gcli/l10n');
var settings = require('gcli/settings');
/**
View
9 lib/gcli/commands/pref_list.js
@@ -16,12 +16,13 @@
define(function(require, exports, module) {
+'use strict';
+var Promise = require('util/promise');
+var util = require('util/util');
+var l10n = require('util/l10n');
var canon = require('gcli/canon');
-var l10n = require('gcli/l10n');
-var util = require('gcli/util');
var settings = require('gcli/settings');
-var Q = require('gcli/promise');
/**
* 'pref list' command
@@ -121,7 +122,7 @@ Object.defineProperty(PrefList.prototype, 'preferences', {
*/
Object.defineProperty(PrefList.prototype, 'promisePreferences', {
get: function() {
- var deferred = Q.defer();
+ var deferred = Promise.defer();
setTimeout(function() {
deferred.resolve(settings.getAll(this.search));
}.bind(this), 10);
View
2  lib/gcli/history.js
@@ -16,6 +16,8 @@
define(function(require, exports, module) {
+'use strict';
+
/**
* A History object remembers commands that have been entered in the past and
* provides an API for accessing them again.
View
4 lib/gcli/index.js
@@ -16,8 +16,10 @@
define(function(require, exports, module) {
+ 'use strict';
+
// Patch-up IE9
- require('gcli/legacy');
+ require('util/legacy');
// The API for use by command authors
exports.addCommand = require('gcli/canon').addCommand;
View
2  lib/gcli/nls/strings.js
@@ -83,7 +83,7 @@ var i18n = {
// When the command line is passed a number, but the number has a decimal
// part and floats are not allowed.
- typesNumberNotInt: '\'%S\' must be an integer.',
+ typesNumberNotInt2: 'Can\'t convert "%S" to an integer.',
// 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
View
211 lib/gcli/promise.js
@@ -1,211 +0,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(function(require, exports, module) {
-
-'use strict';
-
-/**
- * Internal utility: Wraps given `value` into simplified promise, successfully
- * fulfilled to a given `value`. Note the result is not a complete promise
- * implementation, as its method `then` does not returns anything.
- */
-function fulfilled(value) {
- return { then: function then(fulfill) { fulfill(value); } };
-}
-
-/**
- * Internal utility: Wraps given input into simplified promise, pre-rejected
- * with a given `reason`. Note the result is not a complete promise
- * implementation, as its method `then` does not returns anything.
- */
-function rejected(reason) {
- return { then: function then(fulfill, reject) { reject(reason); } };
-}
-
-/**
- * Internal utility: Decorates given `f` function, so that on exception promise
- * rejected with thrown error is returned.
- */
-function attempt(f) {
- return function effort(input) {
- try {
- return f(input);
- }
- catch(error) {
- return rejected(error);
- }
- };
-}
-
-/**
- * Internal utility: Returns `true` if given `value` is a promise. Value is
- * assumed to be a promise if it implements method `then`.
- */
-function isPromise(value) {
- return value && typeof(value.then) === 'function';
-}
-
-/**
- * Creates deferred object containing fresh promise & methods to either resolve
- * or reject it. The result is an object with the following properties:
- * - `promise` Eventual value representation implementing CommonJS [Promises/A]
- * (http://wiki.commonjs.org/wiki/Promises/A) API.
- * - `resolve` Single shot function that resolves enclosed `promise` with a
- * given `value`.
- * - `reject` Single shot function that rejects enclosed `promise` with a given
- * `reason`.
- *
- * ## Example
- *
- * function fetchURI(uri, type) {
- * var deferred = defer();
- * var request = new XMLHttpRequest();
- * request.open("GET", uri, true);
- * request.responseType = type;
- * request.onload = function onload() {
- * deferred.resolve(request.response);
- * }
- * request.onerror = function(event) {
- * deferred.reject(event);
- * }
- * request.send();
- *
- * return deferred.promise;
- * }
- */
-function defer() {
- // Define FIFO queue of observer pairs. Once promise is resolved & all queued
- // observers are forwarded to `result` and variable is set to `null`.
- var observers = [];
-
- // Promise `result`, which will be assigned a resolution value once promise
- // is resolved. Note that result will always be assigned promise (or alike)
- // object to take care of propagation through promise chains. If result is
- // `null` promise is not resolved yet.
- var result = null;
-
- var deferred = {
- promise: {
- then: function then(onFulfill, onError) {
- var deferred = defer();
-
- // Decorate `onFulfill` / `onError` handlers with `attempt`, that
- // way if wrapped handler throws exception decorator will catch and
- // return promise rejected with it, which will cause rejection of
- // `deferred.promise`. If handler is missing, substitute it with an
- // utility function that takes one argument and returns promise
- // fulfilled / rejected with it. This takes care of propagation
- // through the rest of the promise chain.
- onFulfill = onFulfill ? attempt(onFulfill) : fulfilled;
- onError = onError ? attempt(onError) : rejected;
-
- // Create a pair of observers that invoke given handlers & propagate
- // results to `deferred.promise`.
- function resolveDeferred(value) { deferred.resolve(onFulfill(value)); }
- function rejectDeferred(reason) { deferred.resolve(onError(reason)); }
-
- // If enclosed promise (`this.promise`) observers queue is still alive
- // enqueue a new observer pair into it. Note that this does not
- // necessary means that promise is pending, it may already be resolved,
- // but we still have to queue observers to guarantee an order of
- // propagation.
- if (observers) {
- observers.push({ resolve: resolveDeferred, reject: rejectDeferred });
- }
- // Otherwise just forward observer pair right to a `result` promise.
- else {
- result.then(resolveDeferred, rejectDeferred);
- }
-
- return deferred.promise;
- }
- },
- /**
- * Resolves associated `promise` to a given `value`, unless it's already
- * resolved or rejected. Note that resolved promise is not necessary a
- * successfully fulfilled. Promise may be resolved with a promise `value`
- * in which case `value` promise's fulfillment / rejection will propagate
- * up to a promise resolved with `value`.
- */
- resolve: function resolve(value) {
- if (!result) {
- // Store resolution `value` in a `result` as a promise, so that all
- // the subsequent handlers can be simply forwarded to it. Since
- // `result` will be a promise all the value / error propagation will
- // be uniformly taken care of.
- result = isPromise(value) ? value : fulfilled(value);
-
- // Forward already registered observers to a `result` promise in the
- // order they were registered. Note that we intentionally dequeue
- // observer at a time until queue is exhausted. This makes sure that
- // handlers registered as side effect of observer forwarding are
- // queued instead of being invoked immediately, guaranteeing FIFO
- // order.
- while (observers.length) {
- var observer = observers.shift();
- result.then(observer.resolve, observer.reject);
- }
-
- // Once `observers` queue is exhausted we `null`-ify it, so that
- // new handlers are forwarded straight to the `result`.
- observers = null;
- }
- },
- /**
- * Rejects associated `promise` with a given `reason`, unless it's already
- * resolved / rejected. This is just a (better performing) convenience
- * shortcut for `deferred.resolve(reject(reason))`.
- */
- reject: function reject(reason) {
- // Note that if promise is resolved that does not necessary means that it
- // is successfully fulfilled. Resolution value may be a promise in which
- // case its result propagates. In other words if promise `a` is resolved
- // with promise `b`, `a` is either fulfilled or rejected depending
- // on weather `b` is fulfilled or rejected. Here `deferred.promise` is
- // resolved with a promise pre-rejected with a given `reason`, there for
- // `deferred.promise` is rejected with a given `reason`. This may feel
- // little awkward first, but doing it this way greatly simplifies
- // propagation through promise chains.
- deferred.resolve(rejected(reason));
- }
- };
-
- return deferred;
-}
-exports.defer = defer;
-
-/**
- * Returns a promise resolved to a given `value`.
- */
-function resolve(value) {
- var deferred = defer();
- deferred.resolve(value);
- return deferred.promise;
-}
-exports.resolve = resolve;
-
-/**
- * Returns a promise rejected with a given `reason`.
- */
-function reject(reason) {
- var deferred = defer();
- deferred.reject(reason);
- return deferred.promise;
-}
-exports.reject = reject;
-
-});
View
3  lib/gcli/settings.js
@@ -16,8 +16,9 @@
define(function(require, exports, module) {
+'use strict';
-var util = require('gcli/util');
+var util = require('util/util');
var types = require('gcli/types');
View
57 lib/gcli/types.js
@@ -16,7 +16,9 @@
define(function(require, exports, module) {
+'use strict';
+var Promise = require('util/promise');
var Argument = require('gcli/argument').Argument;
var BlankArgument = require('gcli/argument').BlankArgument;
@@ -123,6 +125,18 @@ function Conversion(value, arg, status, message, predictions) {
throw new Error('Missing arg');
}
+ if (predictions != null) {
+ var toCheck = typeof predictions === 'function' ? predictions() : predictions;
+ if (typeof toCheck.then !== 'function') {
+ throw new Error('predictions is not a promise');
+ }
+ toCheck.then(function(value) {
+ if (!Array.isArray(value)) {
+ throw new Error('prediction resolves to non array');
+ }
+ }, console.error);
+ }
+
this._status = status || Status.VALID;
this.message = message;
this.predictions = predictions;
@@ -195,8 +209,8 @@ Conversion.prototype.toString = function() {
/**
* If status === INCOMPLETE, then we may be able to provide predictions as to
* how the argument can be completed.
- * @return An array of items, where each item is an object with the following
- * properties:
+ * @return An array of items, or a promise of an array of items, where each
+ * item is an object with the following properties:
* - name (mandatory): Displayed to the user, and typed in. No whitespace
* - description (optional): Short string for display in a tool-tip
* - manual (optional): Longer description which details usage
@@ -210,28 +224,29 @@ Conversion.prototype.getPredictions = function() {
if (typeof this.predictions === 'function') {
return this.predictions();
}
- return this.predictions || [];
+ return Promise.resolve(this.predictions || []);
};
/**
- * Return an index constrained by the available predictions. Basically
- * (index % predicitons.length)
+ * Return a promise of an index constrained by the available predictions.
+ * i.e. (index % predicitons.length)
*/
Conversion.prototype.constrainPredictionIndex = function(index) {
if (index == null) {
- return undefined;
+ return Promise.resolve();
}
- var predictions = this.getPredictions();
- if (predictions.length === 0) {
- return undefined;
- }
+ return this.getPredictions().then(function(value) {
+ if (value.length === 0) {
+ return undefined;
+ }
- index = index % predictions.length;
- if (index < 0) {
- index = predictions.length + index;
- }
- return index;
+ index = index % value.length;
+ if (index < 0) {
+ index = value.length + index;
+ }
+ return index;
+ }.bind(this));
};
/**
@@ -322,7 +337,7 @@ exports.ArrayConversion = ArrayConversion;
/**
* Most of our types are 'static' e.g. there is only one type of 'string',
- * however some types like 'selection' and 'deferred' are customizable.
+ * however some types like 'selection' and 'delegate' are customizable.
* The basic Type type isn't useful, but does provide documentation about what
* types do.
*/
@@ -356,12 +371,12 @@ Type.prototype.parse = function(arg) {
*/
Type.prototype.parseString = function(str) {
return this.parse(new Argument(str));
-},
+};
/**
* The plug-in system, and other things need to know what this type is
* called. The name alone is not enough to fully specify a type. Types like
- * 'selection' and 'deferred' need extra data, however this function returns
+ * 'selection' and 'delegate' need extra data, however this function returns
* only the name, not the extra data.
*/
Type.prototype.name = undefined;
@@ -389,13 +404,13 @@ Type.prototype.decrement = function(value) {
* 2 known examples of this are boolean -> false and array -> []
*/
Type.prototype.getBlank = function() {
- return this.parse(new BlankArgument());
+ return new Conversion(undefined, new BlankArgument(), Status.INCOMPLETE, '');
};
/**
- * This is something of a hack for the benefit of DeferredType which needs to
+ * This is something of a hack for the benefit of DelegateType which needs to
* 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 DeferredType.
+ * Sub-types can ignore this unless they're DelegateType.
*/
Type.prototype.getType = function() {
return this;
View
125 lib/gcli/types/basic.js
@@ -16,8 +16,11 @@
define(function(require, exports, module) {
+'use strict';
-var l10n = require('gcli/l10n');
+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;
@@ -37,7 +40,7 @@ exports.startup = function() {
types.registerType(NumberType);
types.registerType(BooleanType);
types.registerType(BlankType);
- types.registerType(DeferredType);
+ types.registerType(DelegateType);
types.registerType(ArrayType);
};
@@ -46,7 +49,7 @@ exports.shutdown = function() {
types.unregisterType(NumberType);
types.unregisterType(BooleanType);
types.unregisterType(BlankType);
- types.unregisterType(DeferredType);
+ types.unregisterType(DelegateType);
types.unregisterType(ArrayType);
};
@@ -68,9 +71,9 @@ StringType.prototype.stringify = function(value) {
StringType.prototype.parse = function(arg) {
if (arg.text == null || arg.text === '') {
- return new Conversion(undefined, arg, Status.INCOMPLETE, '');
+ return Promise.resolve(new Conversion(undefined, arg, Status.INCOMPLETE, ''));
}
- return new Conversion(arg.text, arg);
+ return Promise.resolve(new Conversion(arg.text, arg));
};
StringType.prototype.name = 'string';
@@ -137,12 +140,12 @@ NumberType.prototype.getMax = function() {
NumberType.prototype.parse = function(arg) {
if (arg.text.replace(/^\s*-?/, '').length === 0) {
- return new Conversion(undefined, arg, Status.INCOMPLETE, '');
+ return Promise.resolve(new Conversion(undefined, arg, Status.INCOMPLETE, ''));
}
if (!this._allowFloat && (arg.text.indexOf('.') !== -1)) {
- return new Conversion(undefined, arg, Status.ERROR,
- l10n.lookupFormat('typesNumberNotInt', [ arg.text ]));
+ var message = l10n.lookupFormat('typesNumberNotInt2', [ arg.text ]);
+ return Promise.resolve(new Conversion(undefined, arg, Status.ERROR, message));
}
var value;
@@ -154,23 +157,23 @@ NumberType.prototype.parse = function(arg) {
}
if (isNaN(value)) {
- return new Conversion(undefined, arg, Status.ERROR,
- l10n.lookupFormat('typesNumberNan', [ arg.text ]));
+ var message = l10n.lookupFormat('typesNumberNan', [ arg.text ]);
+ return Promise.resolve(new Conversion(undefined, arg, Status.ERROR, message));
}
var max = this.getMax();
if (max != null && value > max) {
- return new Conversion(undefined, arg, Status.ERROR,
- l10n.lookupFormat('typesNumberMax', [ value, max ]));
+ var message = l10n.lookupFormat('typesNumberMax', [ value, max ]);
+ return Promise.resolve(new Conversion(undefined, arg, Status.ERROR, message));
}
var min = this.getMin();
if (min != null && value < min) {
- return new Conversion(undefined, arg, Status.ERROR,
- l10n.lookupFormat('typesNumberMin', [ value, min ]));
+ var message = l10n.lookupFormat('typesNumberMin', [ value, min ]);
+ return Promise.resolve(new Conversion(undefined, arg, Status.ERROR, message));
}
- return new Conversion(value, arg);
+ return Promise.resolve(new Conversion(value, arg));
};
NumberType.prototype.decrement = function(value) {
@@ -242,10 +245,10 @@ BooleanType.prototype.lookup = [
BooleanType.prototype.parse = function(arg) {
if (arg.type === 'TrueNamedArgument') {
- return new Conversion(true, arg);
+ return Promise.resolve(new Conversion(true, arg));
}
if (arg.type === 'FalseNamedArgument') {
- return new Conversion(false, arg);
+ return Promise.resolve(new Conversion(false, arg));
}
return SelectionType.prototype.parse.call(this, arg);
};
@@ -258,7 +261,8 @@ BooleanType.prototype.stringify = function(value) {
};
BooleanType.prototype.getBlank = function() {
- return new Conversion(false, new BlankArgument(), Status.VALID, '', this.lookup);
+ return new Conversion(false, new BlankArgument(), Status.VALID, '',
+ Promise.resolve(this.lookup));
};
BooleanType.prototype.name = 'boolean';
@@ -269,58 +273,62 @@ exports.BooleanType = BooleanType;
/**
* A type for "we don't know right now, but hope to soon".
*/
-function DeferredType(typeSpec) {
- if (typeof typeSpec.defer !== 'function') {
- throw new Error('Instances of DeferredType need typeSpec.defer to be a function that returns a type');
+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);
}
-DeferredType.prototype = Object.create(Type.prototype);
-
-DeferredType.prototype.stringify = function(value) {
- return this.defer().stringify(value);
+/**
+ * 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.prototype.delegateType = function() {
+ throw new Error('Not implemented');
};
-DeferredType.prototype.parse = function(arg) {
- return this.defer().parse(arg);
+DelegateType.prototype = Object.create(Type.prototype);
+
+DelegateType.prototype.stringify = function(value) {
+ return this.delegateType().stringify(value);
};
-DeferredType.prototype.decrement = function(value) {
- var deferred = this.defer();
- return (deferred.decrement ? deferred.decrement(value) : undefined);
+DelegateType.prototype.parse = function(arg) {
+ return this.delegateType().parse(arg);
};
-DeferredType.prototype.increment = function(value) {
- var deferred = this.defer();
- return (deferred.increment ? deferred.increment(value) : undefined);
+DelegateType.prototype.decrement = function(value) {
+ var delegated = this.delegateType();
+ return (delegated.decrement ? delegated.decrement(value) : undefined);
};
-DeferredType.prototype.increment = function(value) {
- var deferred = this.defer();
- return (deferred.increment ? deferred.increment(value) : undefined);
+DelegateType.prototype.increment = function(value) {
+ var delegated = this.delegateType();
+ return (delegated.increment ? delegated.increment(value) : undefined);
};
-DeferredType.prototype.getType = function() {
- return this.defer();
+DelegateType.prototype.getType = function() {
+ return this.delegateType();
};
-Object.defineProperty(DeferredType.prototype, 'isImportant', {
+Object.defineProperty(DelegateType.prototype, 'isImportant', {
get: function() {
- return this.defer().isImportant;
+ return this.delegateType().isImportant;
},
enumerable: true
});
-DeferredType.prototype.name = 'deferred';
+DelegateType.prototype.name = 'delegate';
-exports.DeferredType = DeferredType;
+exports.DelegateType = DelegateType;
/**
- * 'blank' is a type for use with DeferredType when we don't know yet.
+ * 'blank' is a type for use with DelegateType when we don't know yet.
* It should not be used anywhere else.
*/
function BlankType(typeSpec) {
@@ -333,7 +341,7 @@ BlankType.prototype.stringify = function(value) {
};
BlankType.prototype.parse = function(arg) {
- return new Conversion(undefined, arg);
+ return Promise.resolve(new Conversion(undefined, arg));
};
BlankType.prototype.name = 'blank';
@@ -368,21 +376,26 @@ ArrayType.prototype.stringify = function(values) {
};
ArrayType.prototype.parse = function(arg) {
- if (arg.type === 'ArrayArgument') {
- var conversions = arg.getArguments().map(function(subArg) {
- var conversion = this.subtype.parse(subArg);
- // Hack alert. ArrayConversion needs to be able to answer questions
- // about the status of individual conversions in addition to the
- // overall state. This allows us to do that easily.
- subArg.conversion = conversion;
- return conversion;
- }, this);
- return new ArrayConversion(conversions, arg);
- }
- else {
+ 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).then(function(conversion) {
+ subArg.conversion = conversion;
+ return conversion;
+ }.bind(this), console.error);
+ }.bind(this);
+
+ var conversionPromises = arg.getArguments().map(subArgParse);
+ return util.all(conversionPromises).then(function(conversions) {
+ return new ArrayConversion(conversions, arg);
+ });
};
ArrayType.prototype.getBlank = function(values) {
View
67 lib/gcli/types/command.js
@@ -16,9 +16,11 @@
define(function(require, exports, module) {
+'use strict';
+var Promise = require('util/promise');
+var l10n = require('util/l10n');
var canon = require('gcli/canon');
-var l10n = require('gcli/l10n');
var types = require('gcli/types');
var SelectionType = require('gcli/types/selection').SelectionType;
var Status = require('gcli/types').Status;
@@ -50,6 +52,7 @@ function ParamType(typeSpec) {
this.requisition = typeSpec.requisition;
this.isIncompleteName = typeSpec.isIncompleteName;
this.stringifyProperty = 'name';
+ this.neverForceAsync = true;
}
ParamType.prototype = Object.create(SelectionType.prototype);
@@ -59,19 +62,25 @@ ParamType.prototype.name = 'param';
ParamType.prototype.lookup = function() {
var displayedParams = [];
var command = this.requisition.commandAssignment.value;
- 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);
+ 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) {
- return this.isIncompleteName ?
- SelectionType.prototype.parse.call(this, arg) :
- new Conversion(undefined, arg, Status.ERROR, l10n.lookup('cliUnusedArg'));
+ if (this.isIncompleteName) {
+ return SelectionType.prototype.parse.call(this, arg);
+ }
+ else {
+ var message = l10n.lookup('cliUnusedArg');
+ return Promise.resolve(new Conversion(undefined, arg, Status.ERROR, message));
+ }
};
@@ -84,6 +93,7 @@ ParamType.prototype.parse = function(arg) {
*/
function CommandType() {
this.stringifyProperty = 'name';
+ this.neverForceAsync = true;
}
CommandType.prototype = Object.create(SelectionType.prototype);
@@ -120,30 +130,31 @@ CommandType.prototype.parse = function(arg) {
return this._findPredictions(arg);
}.bind(this);
- var predictions = this._findPredictions(arg);
+ 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);
+ }
- 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;
- 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, '');
+ }
- 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);
}
- 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);
- }
+ // 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);
+ return new Conversion(undefined, arg, Status.INCOMPLETE, '', predictFunc);
+ }.bind(this));
};