Browse files

changes to support array arguments

  • Loading branch information...
1 parent 8f32ed8 commit 3385812724b2529caa106a4d7fd56447d5f62d2a @joewalker joewalker committed Mar 16, 2011
Showing with 393 additions and 139 deletions.
  1. +290 −129 lib/pilot/argument.js
  2. +93 −6 lib/pilot/types.js
  3. +10 −4 lib/pilot/types/basic.js
View
419 lib/pilot/argument.js
@@ -42,8 +42,8 @@ var console = require('pilot/console');
var oop = require('pilot/oop');
/**
- * We record where in the input string an argument comes so we can report errors
- * against those string positions.
+ * We record where in the input string an argument comes so we can report
+ * errors against those string positions.
* We publish a 'change' event when-ever the text changes.
* @param text The string (trimmed) that contains the argument
* @param start The position of the text in the original input string
@@ -72,84 +72,127 @@ function Argument(text, prefix, suffix, start, end) {
this.end = end !== undefined ? end : Argument.AT_CURSOR;
}
}
-Argument.prototype = {
- /**
- * RTTI. TODO: Is there a better cross-browser way?
- */
- _type: Argument,
-
- /**
- * Return the result of merging these arguments.
- * TODO: What happens when we're merging arguments for the single string
- * case and some of the arguments are in quotation marks?
- */
- merge: function(following) {
- return new Argument(
- this.text + this.suffix + following.prefix + following.text,
- this.prefix, following.suffix,
- this.start, following.end);
- },
-
- /**
- * Returns a new Argument like this one but with the text set to
- * <tt>replText</tt> and the end adjusted to fit.
- * @param replText Text to replace the old text value
- */
- beget: function(replText, options) {
- var start = this.start;
- var prefix = this.prefix;
- var suffix = this.suffix;
-
- var quote = (replText.indexOf(' ') >= 0 || replText.length == 0) ?
- '\'' : '';
-
- if (options) {
- prefix = (options.prefixSpace ? ' ' : '') + quote;
- start = start - this.prefix.length + prefix.length;
- }
- var end = this.end - this.text.length + replText.length;
+/**
+ * Return the result of merging these arguments.
+ * TODO: What happens when we're merging arguments for the single string
+ * case and some of the arguments are in quotation marks?
+ */
+Argument.prototype.merge = function(following) {
+ return new Argument(
+ this.text + this.suffix + following.prefix + following.text,
+ this.prefix, following.suffix,
+ this.start, following.end);
+};
+
+/**
+ * Returns a new Argument like this one but with the text set to
+ * <tt>replText</tt> and the end adjusted to fit.
+ * @param replText Text to replace the old text value
+ */
+Argument.prototype.beget = function(replText, options) {
+ var start = this.start;
+ var prefix = this.prefix;
+ var suffix = this.suffix;
+
+ var quote = (replText.indexOf(' ') >= 0 || replText.length == 0) ?
+ '\'' : '';
+
+ if (options) {
+ prefix = (options.prefixSpace ? ' ' : '') + quote;
+ start = start - this.prefix.length + prefix.length;
+ }
+
+ var end = this.end - this.text.length + replText.length;
+
+ if (options) {
+ suffix = quote;
+ end = end - this.suffix.length + suffix.length;
+ }
+
+ return new Argument(replText, prefix, suffix, start, end);
+};
+
+/**
+ * Returns a new Argument like this one but slid along by <tt>distance</tt>.
+ * @param distance The amount to shift the prefix and suffix by (can be
+ * negative)
+ */
+Argument.prototype.begetShifted = function(distance) {
+ return new Argument(
+ this.text,
+ this.prefix, this.suffix,
+ this.start + distance, this.end + distance);
+};
- if (options) {
- suffix = quote;
- end = end - this.suffix.length + suffix.length;
+/**
+ * Is there any visible content to this argument?
+ */
+Argument.prototype.isBlank = function() {
+ return this.text === '' &&
+ this.prefix.trim() === '' &&
+ this.suffix.trim() === '';
+};
+
+/**
+ * We need to keep track of which assignment we've been assigned to
+ */
+Argument.prototype.assign = function(assignment) {
+ this.assignment = assignment;
+};
+
+/**
+ *
+ */
+Argument.prototype.updateCliArgs = function(args, oldArg) {
+ // If oldArg appears in our list of args then we need to update
+ var updated = false;
+ for (var i = 0; i < args.length; i++) {
+ if (args[i] === oldArg) {
+ args[i] = this;
+ updated = true;
}
+ }
- return new Argument(replText, prefix, suffix, start, end);
- },
-
- /**
- * Returns a new Argument like this one but slid along by <tt>distance</tt>.
- * @param distance The amount to shift the prefix and suffix by (can be
- * negative)
- */
- begetShifted: function(distance) {
- return new Argument(
- this.text,
- this.prefix, this.suffix,
- this.start + distance, this.end + distance);
- },
-
- /**
- * Is there any visible content to this argument?
- */
- isBlank: function() {
- return this.text === '' &&
- this.prefix.trim() === '' &&
- this.suffix.trim() === '';
- },
-
- /**
- * Helper when we're putting arguments back together
- */
- toString: function() {
- // TODO: There is a bug here - we should re-escape escaped characters
- // But can we do that reliably?
- return this.prefix + this.text + this.suffix;
+ // If we didn't find a slot for the argument earlier, then we add it on to
+ // the end of the command line
+ if (!updated) {
+ // TODO: Perhaps it would be nice to check that oldArg is blank?
+ args.push(this);
}
};
/**
+ * We define equals to mean all arg properties are strict equals
+ */
+Argument.prototype.equals = function(that) {
+ if (this === that) {
+ return true;
+ }
+ if (that == null) {
+ return false;
+ }
+
+ // TODO: can we check that it's not a subtype?
+ if (!(that instanceof Argument)) {
+ throw new Error('arg2 is not an Argument');
+ }
+
+ return this.text === that.text &&
+ this.prefix === that.prefix && this.suffix === that.suffix &&
+ this.start === that.start && this.end === that.end;
+};
+
+/**
+ * Helper when we're putting arguments back together
+ */
+Argument.prototype.toString = function() {
+ // TODO: There is a bug here - we should re-escape escaped characters
+ // But can we do that reliably?
+ return this.prefix + this.text + this.suffix;
+};
+
+/**
* Merge an array of arguments into a single argument.
* All Arguments in the array are expected to have the same emitter
*/
@@ -171,36 +214,6 @@ Argument.merge = function(argArray, start, end) {
};
/**
- * We define equals to mean either:
- * - both arg1 and arg2 are null or undefined or
- * - all arg properties are strict equals
- * <p>Is there a better way to define similarity in Javascript?
- */
-Argument.equals = function(arg1, arg2) {
- if (arg1 === arg2) {
- return true;
- }
- if (arg1 == null && arg2 == null) {
- return true;
- }
- if (arg1 == null || arg2 == null) {
- return false;
- }
-
- if (!(arg1 instanceof Argument)) {
- throw new Error('arg1 is not an Argument, it\'s a ' + arg1);
- }
-
- if (!(arg2 instanceof Argument)) {
- throw new Error('arg2 is not an Argument');
- }
-
- return arg1._type == arg2._type && arg1.text === arg2.text &&
- arg1.prefix === arg2.prefix && arg1.suffix === arg2.suffix &&
- arg1.start === arg2.start && arg1.end === arg2.end;
-};
-
-/**
* We sometimes need a way to say 'this error occurs where the cursor is',
* which causes it to be sorted towards the top.
*/
@@ -213,31 +226,33 @@ exports.Argument = Argument;
* special format like: 'echo a b c' effectively have a number of arguments
* merged together.
*/
-function MergedArgument(first, prefix, suffix, start, end) {
- if (typeof first === 'string') {
- this.text = first;
- this.prefix = prefix;
- this.suffix = suffix;
- this.start = start;
- this.end = end;
- }
- else if (Array.isArray(first)) {
- var arg = Argument.merge(first);
- this.text = arg.text;
- this.prefix = arg.prefix;
- this.suffix = arg.suffix;
- this.start = arg.start;
- this.end = arg.end;
- }
- else {
- throw new Error('Illegal 1st argument to MergedArgument');
+function MergedArgument(args, prefix, suffix, start, end) {
+ if (!Array.isArray(args)) {
+ throw new Error('args is not an array of Arguments');
}
+
+ this.args = args;
+ var arg = Argument.merge(args);
+ this.text = arg.text;
+ this.prefix = arg.prefix;
+ this.suffix = arg.suffix;
+ this.start = arg.start;
+ this.end = arg.end;
}
oop.inherits(MergedArgument, Argument);
-/** RTTI */
-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;
+};
/**
* The standard Argument.beget has a function to add quotes, however this is
@@ -264,6 +279,25 @@ MergedArgument.prototype.beget = function(replText, options) {
return new MergedArgument(replText, prefix, suffix, start, end);
};
+MergedArgument.prototype.equals = function(that) {
+ if (this === that) {
+ return true;
+ }
+ if (that == null) {
+ return false;
+ }
+
+ if (!(that instanceof MergedArgument)) {
+ throw new Error('arg2 is not a MergedArgument');
+ }
+
+ // TODO: do we need to check that args is the same?
+
+ return this.text === that.text &&
+ this.prefix === that.prefix && this.suffix === that.suffix &&
+ this.start === that.start && this.end === that.end;
+};
+
exports.MergedArgument = MergedArgument;
@@ -272,6 +306,8 @@ exports.MergedArgument = MergedArgument;
* has a boolean value, and thus the opposite of '--verbose' is ''.
*/
function BooleanNamedArgument(arg) {
+ this.arg = arg;
+
this.text = arg.text;
this.prefix = arg.prefix;
this.suffix = arg.suffix;
@@ -281,8 +317,29 @@ function BooleanNamedArgument(arg) {
oop.inherits(BooleanNamedArgument, Argument);
-/** RTTI */
-BooleanNamedArgument.prototype._type = BooleanNamedArgument;
+BooleanNamedArgument.prototype.assign = function(assignment) {
+ this.arg.assign(assignment);
+ this.assignment = assignment;
+};
+
+BooleanNamedArgument.prototype.equals = function(that) {
+ if (this === that) {
+ return true;
+ }
+ if (that == null) {
+ return false;
+ }
+
+ if (!(that instanceof BooleanNamedArgument)) {
+ throw new Error('arg2 is not a BooleanNamedArgument');
+ }
+
+ // TODO: do we need to check that arg is the same?
+
+ return this.text === that.text &&
+ this.prefix === that.prefix && this.suffix === that.suffix &&
+ this.start === that.start && this.end === that.end;
+};
exports.BooleanNamedArgument = BooleanNamedArgument;
@@ -302,6 +359,9 @@ exports.BooleanNamedArgument = BooleanNamedArgument;
* We model this as a normal argument but with a long prefix.
*/
function NamedArgument(nameArg, valueArg) {
+ this.nameArg = nameArg;
+ this.valueArg = valueArg;
+
this.text = valueArg.text;
this.start = valueArg.start;
this.end = valueArg.end;
@@ -311,8 +371,30 @@ function NamedArgument(nameArg, valueArg) {
oop.inherits(NamedArgument, Argument);
-/** RTTI */
-NamedArgument.prototype._type = NamedArgument;
+NamedArgument.prototype.assign = function(assignment) {
+ this.nameArg.assign(assignment);
+ this.valueArg.assign(assignment);
+ this.assignment = assignment;
+};
+
+NamedArgument.prototype.equals = function(that) {
+ if (this === that) {
+ return true;
+ }
+ if (that == null) {
+ return false;
+ }
+
+ if (!(that instanceof NamedArgument)) {
+ throw new Error('arg2 is not a NamedArgument');
+ }
+
+ // TODO: do we need to check that nameArg and valueArg are the same?
+
+ return this.text === that.text &&
+ this.prefix === that.prefix && this.suffix === that.suffix &&
+ this.start === that.start && this.end === that.end;
+};
exports.NamedArgument = NamedArgument;
@@ -321,13 +403,92 @@ exports.NamedArgument = NamedArgument;
*
*/
function ArrayArgument() {
- this.values = [];
+ this.args = [];
}
oop.inherits(ArrayArgument, Argument);
-/** RTTI */
-ArrayArgument.prototype._type = ArrayArgument;
+ArrayArgument.prototype.addArgument = function(arg) {
+ this.args.push(arg);
+};
+
+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;
+};
+
+ArrayArgument.prototype.updateCliArgs = function(cliArgs, oldArg) {
+ // Remove all oldArgs from CLIs list of args,
+ // remembering where we found the first match
+ var firstMatchingIdx = cliArgs.length;
+ var oldArgs = oldArg.args;
+
+ for (var cliIdx = 0; cliIdx < cliArgs.length; cliIdx++) {
+ var cliArg = cliArgs[cliIdx];
+
+ for (var oldIdx = 0; oldIdx < oldArgs.length; oldIdx++) {
+ var oldArg = oldArgs[oldIdx];
+
+ if (oldArg === cliArg) {
+ if (cliIdx < firstMatchingIdx) {
+ firstMatchingIdx = cliIdx;
+ }
+ cliArgs.splice(cliIdx, 1);
+ cliIdx--;
+ }
+ }
+ }
+
+ // Insert all new args (i.e. from 'this') at the position of the first
+ for (var i = 0; i < this.args.length; i++) {
+ cliArgs.splice(firstMatchingIdx + i, 0, this.args[i]);
+ }
+};
+
+ArrayArgument.prototype.equals = function(that) {
+ if (this === that) {
+ return true;
+ }
+ if (that == null) {
+ return false;
+ }
+
+ if (!(that instanceof ArrayArgument)) {
+ throw new Error('arg2 is not a ArrayArgument');
+ }
+
+ if (this.args.length !== that.args.length) {
+ return false;
+ }
+
+ for (var i = 0; i < this.args.length; i++) {
+ if (!this.args[i].equals(that.args[i])) {
+ return false;
+ }
+ }
+
+ return true;
+};
+
+/**
+ * Helper when we're putting arguments back together
+ */
+ArrayArgument.prototype.toString = function() {
+ return '{' + this.args.map(function(arg) {
+ return arg.toString();
+ }, this).join(',') + '}';
+};
exports.ArrayArgument = ArrayArgument;
View
99 lib/pilot/types.js
@@ -39,6 +39,7 @@ define(function(require, exports, module) {
var Argument = require('pilot/argument').Argument;
+var oop = require('pilot/oop');
/**
* Some types can detect validity, that is to say they can distinguish between
@@ -135,13 +136,101 @@ function Conversion(value, arg, status, message, predictions) {
// probably only the 4 very good matches should be presented.
this.predictions = predictions || [];
}
+
exports.Conversion = Conversion;
-Conversion.prototype = {
- getText: function() {
- return this.arg ? this.arg.text : '';
+
+Conversion.prototype.assign = function(assignment) {
+ this.arg.assign(assignment);
+};
+
+Conversion.prototype.dataProvided = function() {
+ var argProvided = this.arg.text !== '';
+ return this.value !== undefined || argProvided;
+};
+
+Conversion.prototype.updateCliArgs = function(args, oldConversion) {
+ this.arg.updateCliArgs(args, oldConversion.arg);
+};
+
+Conversion.prototype.valueEquals = function(that) {
+ // TODO: consider if this should be '=='
+ return this.value === that.value;
+};
+
+Conversion.prototype.argEquals = function(that) {
+ return this.arg.equals(that.arg);
+};
+
+Conversion.prototype.toString = function() {
+ return this.arg.toString();
+};
+
+/**
+ * ArrayConversion is a special Conversion, needed because arrays are converted
+ * member by member rather then as a whole, which means we can track the
+ * conversion if individual array elements. So an ArrayConversion acts like a
+ * normal Conversion (which is needed as Assignment requires a Conversion) but
+ * it can also be devolved into a set of Conversions for each array member.
+ */
+function ArrayConversion(conversions, arg) {
+ this.arg = arg;
+ this.conversions = conversions;
+ this.value = conversions.map(function(conversion) {
+ return conversion.value;
+ }, this);
+
+ // This message is just for reporting errors like "not enough values"
+ // rather that for problems with individual values.
+ this.message = '';
+
+ // Predictions are generally provided by individual values
+ this.predictions = [];
+}
+
+oop.inherits(ArrayConversion, Conversion);
+
+ArrayConversion.prototype.assign = function(assignment) {
+ this.conversions.forEach(function(conversion) {
+ conversion.assign(assignment);
+ }, this);
+ this.assignment = assignment;
+};
+
+ArrayConversion.prototype.dataProvided = function() {
+ return this.conversions.length > 0;
+};
+
+ArrayConversion.prototype.valueEquals = function(that) {
+ if (!(that instanceof ArrayConversion)) {
+ throw new Error('Can\'t compare values with non ArrayConversion');
+ }
+
+ if (this.value === that.value) {
+ return true;
+ }
+
+ if (this.value.length !== that.value.length) {
+ return false;
}
+
+ for (var i = 0; i < this.conversions.length; i++) {
+ if (!this.conversions[i].valueEquals(that.conversions[i])) {
+ return false;
+ }
+ }
+
+ return true;
};
+ArrayConversion.prototype.toString = function() {
+ return '[ ' + this.conversions.map(function(conversion) {
+ return conversion.toString();
+ }, this).join(', ') + ' ]';
+};
+
+exports.ArrayConversion = ArrayConversion;
+
+
/**
* Most of our types are 'static' e.g. there is only one type of 'text', however
* some types like 'selection' and 'deferred' are customizable. The basic
@@ -206,9 +295,7 @@ Type.prototype = {
* nothing, the output of this can sometimes be customized.
* @return Conversion
*/
- getDefault: function() {
- return new Conversion(null, new Argument());
- }
+ getDefault: undefined
};
exports.Type = Type;
View
14 lib/pilot/types/basic.js
@@ -44,6 +44,7 @@ var oop = require('pilot/oop');
var types = require("pilot/types");
var Type = types.Type;
var Conversion = types.Conversion;
+var ArrayConversion = types.ArrayConversion;
var Status = types.Status;
var ArrayArgument = require('pilot/argument').ArrayArgument;
@@ -357,16 +358,21 @@ ArrayType.prototype.stringify = function(values) {
ArrayType.prototype.parse = function(arg) {
if (arg instanceof ArrayArgument) {
- return arg.values.map(function(subvalue) {
- this.subtype.parse(subvalue);
+ var conversions = arg.getArguments().map(function(subvalue) {
+ return this.subtype.parse(subvalue);
}, this);
+ return new ArrayConversion(conversions, arg);
}
else {
- console.warn('Are we expecting ArrayType.parse to get non ArrayArguments?');
- return this.subtype.parse(arg);
+ console.error('non ArrayArgument to ArrayType.parse', arg);
+ throw new Error('non ArrayArgument to ArrayType.parse');
}
};
+ArrayType.prototype.getDefault = function() {
+ return new ArrayConversion([], new ArrayArgument());
+};
+
ArrayType.prototype.name = 'array';
exports.ArrayType = ArrayType;

0 comments on commit 3385812

Please sign in to comment.