Bug 773271 - GCLI needs a date type; r=jwalker,harth
authorQuentin Pradet <quentin.pradet@gmail.com>
Tue, 21 May 2013 10:18:55 +0100
changeset 132422 3b90dbff5809
parent 132421 231636fc3231
child 132423 99a40b3cf2ec
push id24702
push userryanvm@gmail.com
push date2013-05-21 12:15 +0000
treeherdermozilla-central@957f5f047a94 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersjwalker, harth
bugs773271
milestone24.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 773271 - GCLI needs a date type; r=jwalker,harth
browser/devtools/commandline/test/browser_gcli_cli.js
browser/devtools/commandline/test/browser_gcli_date.js
browser/devtools/commandline/test/mockCommands.js
browser/locales/en-US/chrome/browser/devtools/gcli.properties
toolkit/devtools/gcli/gcli.jsm
--- a/browser/devtools/commandline/test/browser_gcli_cli.js
+++ b/browser/devtools/commandline/test/browser_gcli_cli.js
@@ -305,17 +305,17 @@ exports.testTsv = function(options) {
         input:  'tsv option ',
         hints:             '<optionValue>',
         markup: 'VVVVEEEEEEV',
         cursor: 11,
         current: 'optionValue',
         status: 'ERROR',
         predictions: [ ],
         unassigned: [ ],
-        tooltipState: 'true:isError',
+        tooltipState: 'false:default',
         args: {
           command: { name: 'tsv' },
           optionType: {
             value: undefined,
             arg: ' option ',
             status: 'ERROR',
             message: 'Can\'t use \'option\'.'
           },
new file mode 100644
--- /dev/null
+++ b/browser/devtools/commandline/test/browser_gcli_date.js
@@ -0,0 +1,247 @@
+/*
+ * Copyright 2009-2011 Mozilla Foundation and contributors
+ * Licensed under the New BSD license. See LICENSE.txt or:
+ * http://opensource.org/licenses/BSD-3-Clause
+ */
+
+// 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-testDate.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 types = require('gcli/types');
+var Argument = require('gcli/argument').Argument;
+var Status = require('gcli/types').Status;
+
+// var helpers = require('gclitest/helpers');
+// var mockCommands = require('gclitest/mockCommands');
+
+exports.setup = function(options) {
+  mockCommands.setup();
+};
+
+exports.shutdown = function(options) {
+  mockCommands.shutdown();
+};
+
+
+exports.testParse = function(options) {
+  var date = types.createType('date');
+  return date.parse(new Argument('now')).then(function(conversion) {
+    // Date comparison - these 2 dates may not be the same, but how close is
+    // close enough? If this test takes more than 30secs to run the it will
+    // probably time out, so we'll assume that these 2 values must be within
+    // 1 min of each other
+    var gap = new Date().getTime() - conversion.value.getTime();
+    assert.ok(gap < 60000, 'now is less than a minute away');
+
+    assert.is(conversion.getStatus(), Status.VALID, 'now parse');
+  });
+};
+
+exports.testMaxMin = function(options) {
+  var max = new Date();
+  var min = new Date();
+  var date = types.createType({ name: 'date', max: max, min: min });
+  assert.is(date.getMax(), max, 'max setup');
+
+  var incremented = date.increment(min);
+  assert.is(incremented, max, 'incremented');
+};
+
+exports.testIncrement = function(options) {
+  var date = types.createType('date');
+  return date.parse(new Argument('now')).then(function(conversion) {
+    var plusOne = date.increment(conversion.value);
+    var minusOne = date.decrement(plusOne);
+
+    // 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, [
+    {
+      setup:    'tsdate 2001-01-01 1980-01-03',
+      check: {
+        input:  'tsdate 2001-01-01 1980-01-03',
+        hints:                              '',
+        markup: 'VVVVVVVVVVVVVVVVVVVVVVVVVVVV',
+        status: 'VALID',
+        message: '',
+        args: {
+          command: { name: 'tsdate' },
+          d1: {
+            value: function(d1) {
+              assert.is(d1.getFullYear(), 2001, 'd1 year');
+              assert.is(d1.getMonth(), 0, 'd1 month');
+              assert.is(d1.getDate(), 1, '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');
+            },
+            arg: ' 2001-01-01',
+            status: 'VALID',
+            message: ''
+          },
+          d2: {
+            value: function(d2) {
+              assert.is(d2.getFullYear(), 1980, 'd1 year');
+              assert.is(d2.getMonth(), 0, 'd1 month');
+              assert.is(d2.getDate(), 3, 'd1 date');
+              assert.is(d2.getHours(), 0, 'd1 hours');
+              assert.is(d2.getMinutes(), 0, 'd1 minutes');
+              assert.is(d2.getSeconds(), 0, 'd1 seconds');
+              assert.is(d2.getMilliseconds(), 0, 'd1 millis');
+            },
+            arg: ' 1980-01-03',
+            status: 'VALID',
+            message: ''
+          },
+        }
+      },
+      exec: {
+        output: [ /^Exec: tsdate/, /2001/, /1980/ ],
+        completed: true,
+        type: 'string',
+        error: false
+      }
+    }
+  ]);
+};
+
+exports.testIncrDecr = function(options) {
+  helpers.audit(options, [
+    {
+      setup:    'tsdate 2001-01-01<UP>',
+      check: {
+        input:  'tsdate 2001-01-02',
+        hints:                    ' <d2>',
+        markup: 'VVVVVVVVVVVVVVVVV',
+        status: 'ERROR',
+        message: '',
+        args: {
+          command: { name: 'tsdate' },
+          d1: {
+            value: function(d1) {
+              assert.is(d1.getFullYear(), 2001, 'd1 year');
+              assert.is(d1.getMonth(), 0, 'd1 month');
+              assert.is(d1.getDate(), 2, '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');
+            },
+            arg: ' 2001-01-02',
+            status: 'VALID',
+            message: ''
+          },
+          d2: {
+            value: undefined,
+            status: 'INCOMPLETE',
+            message: ''
+          },
+        }
+      }
+    },
+    {
+      // Check wrapping on decrement
+      setup:    'tsdate 2001-02-01<DOWN>',
+      check: {
+        input:  'tsdate 2001-01-31',
+        hints:                    ' <d2>',
+        markup: 'VVVVVVVVVVVVVVVVV',
+        status: 'ERROR',
+        message: '',
+        args: {
+          command: { name: 'tsdate' },
+          d1: {
+            value: function(d1) {
+              assert.is(d1.getFullYear(), 2001, 'd1 year');
+              assert.is(d1.getMonth(), 0, 'd1 month');
+              assert.is(d1.getDate(), 31, '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');
+            },
+            arg: ' 2001-01-31',
+            status: 'VALID',
+            message: ''
+          },
+          d2: {
+            value: undefined,
+            status: 'INCOMPLETE',
+            message: ''
+          },
+        }
+      }
+    },
+    {
+      // Check 'max' value capping on increment
+      setup:    'tsdate 2001-02-01 "27 feb 2000"<UP>',
+      check: {
+        input:  'tsdate 2001-02-01 "2000-02-28"',
+        hints:                                '',
+        markup: 'VVVVVVVVVVVVVVVVVVVVVVVVVVVVVV',
+        status: 'VALID',
+        message: '',
+        args: {
+          command: { name: 'tsdate' },
+          d1: {
+            value: function(d1) {
+              assert.is(d1.getFullYear(), 2001, 'd1 year');
+              assert.is(d1.getMonth(), 1, 'd1 month');
+              assert.is(d1.getDate(), 1, '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');
+            },
+            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');
+            },
+            arg: ' "2000-02-28"',
+            status: 'VALID',
+            message: ''
+          },
+        }
+      }
+    }
+  ]);
+};
+
+
+// });
--- a/browser/devtools/commandline/test/mockCommands.js
+++ b/browser/devtools/commandline/test/mockCommands.js
@@ -400,16 +400,37 @@ var tslong = {
           defaultValue: "collapse"
         }
       ]
     }
   ],
   exec: createExec('tslong')
 };
 
+var tsdate = {
+  name: 'tsdate',
+  description: 'long param tests to catch problems with the jsb command',
+  params: [
+    {
+      name: 'd1',
+      type: 'date',
+    },
+    {
+      name: 'd2',
+      type: {
+        name: 'date',
+        min: '1 jan 2000',
+        max: '28 feb 2000',
+        step: 2
+      }
+    },
+  ],
+  exec: createExec('tsdate')
+};
+
 var tsfail = {
   name: 'tsfail',
   description: 'test errors',
   params: [
     {
       name: 'method',
       type: {
         name: 'selection',
@@ -505,16 +526,17 @@ mockCommands.setup = function(opts) {
   mockCommands.commands.tsnDeepDownNested = canon.addCommand(tsnDeepDownNested);
   mockCommands.commands.tsnDeepDownNestedCmd = canon.addCommand(tsnDeepDownNestedCmd);
   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.shutdown = function(opts) {
   canon.removeCommand(tsv);
   canon.removeCommand(tsr);
   canon.removeCommand(tsrsrsr);
   canon.removeCommand(tso);
@@ -535,16 +557,17 @@ mockCommands.shutdown = function(opts) {
   canon.removeCommand(tsnDeepDownNested);
   canon.removeCommand(tsnDeepDownNestedCmd);
   canon.removeCommand(tselarr);
   canon.removeCommand(tsm);
   canon.removeCommand(tsg);
   canon.removeCommand(tshidden);
   canon.removeCommand(tscook);
   canon.removeCommand(tslong);
+  canon.removeCommand(tsdate);
   canon.removeCommand(tsfail);
 
   types.removeType(mockCommands.optionType);
   types.removeType(mockCommands.optionValue);
 
   mockCommands.commands = {};
 };
 
--- a/browser/locales/en-US/chrome/browser/devtools/gcli.properties
+++ b/browser/locales/en-US/chrome/browser/devtools/gcli.properties
@@ -109,16 +109,31 @@ typesNumberMax=%1$S is greater than maxi
 # 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.
+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.
 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.
--- a/toolkit/devtools/gcli/gcli.jsm
+++ b/toolkit/devtools/gcli/gcli.jsm
@@ -99,26 +99,27 @@ var mozl10n = {};
     }
     catch (ex) {
       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/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/ui/ffdisplay'], function(require, exports, module) {
+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/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();
@@ -3937,16 +3938,247 @@ function CommandOutputManager() {
   this.onOutput = util.createEvent('CommandOutputManager.onOutput');
 }
 
 exports.CommandOutputManager = CommandOutputManager;
 
 
 });
 /*
+ * Copyright 2009-2011 Mozilla Foundation and contributors
+ * Licensed under the New BSD license. See LICENSE.txt or:
+ * http://opensource.org/licenses/BSD-3-Clause
+ */
+
+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
+  typeSpec = typeSpec || {};
+
+  this._step = typeSpec.step || 1;
+  this._min = new Date(-8640000000000000);
+  this._max = new Date(8640000000000000);
+
+  if (typeSpec.min != null) {
+    if (typeof typeSpec.min === 'string') {
+      this._min = toDate(typeSpec.min);
+    }
+    else if (isDate(typeSpec.min) || typeof typeSpec.min === 'function') {
+      this._min = typeSpec.min;
+    }
+    else {
+      throw new Error('date min value must be a string a date or a function');
+    }
+  }
+
+  if (typeSpec.max != null) {
+    if (typeof typeSpec.max === 'string') {
+      this._max = toDate(typeSpec.max);
+    }
+    else if (isDate(typeSpec.max) || typeof typeSpec.max === 'function') {
+      this._max = typeSpec.max;
+    }
+    else {
+      throw new Error('date max value must be a string a date or a function');
+    }
+  }
+}
+
+DateType.prototype = Object.create(Type.prototype);
+
+/**
+ * Helper for stringify() to left pad a single digit number with a single '0'
+ * so 1 -> '01', 42 -> '42', etc.
+ */
+function pad(number) {
+  var r = String(number);
+  return r.length === 1 ? '0' + r : r;
+}
+
+DateType.prototype.stringify = function(value) {
+  if (!isDate(value)) {
+    return '';
+  }
+
+  var str = pad(value.getFullYear()) + '-' +
+            pad(value.getMonth() + 1) + '-' +
+            pad(value.getDate());
+
+  // Only add in the time if it's not midnight
+  if (value.getHours() !== 0 || value.getMinutes() !== 0 ||
+      value.getSeconds() !== 0 || value.getMilliseconds() !== 0) {
+
+    // What string should we use to separate the date from the time?
+    // There are 3 options:
+    // 'T': This is the standard from ISO8601. i.e. 2013-05-20T11:05
+    //      The good news - it's a standard. The bad news - it's weird and
+    //      alien to many if not most users
+    // ' ': This looks nicest, but needs escaping (which GCLI will do
+    //      automatically) so it would look like: '2013-05-20 11:05'
+    //      Good news: looks best, bad news: on completion we place the cursor
+    //      after the final ', so repeated increment/decrement doesn't work
+    // '\ ': It's possible that we could find a way to use a \ to escape the
+    //      space, so the output would look like: 2013-05-20\ 11:05
+    //      This would involve changes to a number of parts, and is probably
+    //      too complex a solution for this problem for now
+    // In the short term I'm going for ' ', and raising the priority of cursor
+    // positioning on actions like increment/decrement/tab.
+
+    str += ' ' + pad(value.getHours());
+    str += ':' + pad(value.getMinutes());
+
+    // Only add in seconds/milliseconds if there is anything to report
+    if (value.getSeconds() !== 0 || value.getMilliseconds() !== 0) {
+      str += ':' + pad(value.getSeconds());
+      if (value.getMilliseconds() !== 0) {
+        str += '.' + String((value.getUTCMilliseconds()/1000).toFixed(3)).slice(2, 5);
+      }
+    }
+  }
+
+  return str;
+};
+
+DateType.prototype.getMin = function(context) {
+  if (typeof this._min === 'function') {
+    return this._min(context);
+  }
+  if (isDate(this._min)) {
+    return this._min;
+  }
+  return undefined;
+};
+
+DateType.prototype.getMax = function(context) {
+  if (typeof this._max === 'function') {
+    return this._max(context);
+  }
+  if (isDate(this._max)) {
+    return this._max;
+  }
+  return undefined;
+};
+
+DateType.prototype.parse = function(arg, context) {
+  var value;
+
+  if (arg.text.replace(/\s/g, '').length === 0) {
+    return Promise.resolve(new Conversion(undefined, arg, Status.INCOMPLETE, ''));
+  }
+
+  // Lots of room for improvement here: 1h ago, in two days, etc.
+  // Should "1h ago" dynamically update the step?
+  if (arg.text === 'now') {
+    value = new Date();
+  }
+  else if (arg.text === 'yesterday') {
+    value = new Date().setDate(new Date().getDate() - 1);
+  }
+  else if (arg.text === 'tomorrow') {
+    value = new Date().setDate(new Date().getDate() + 1);
+  }
+  else {
+    var millis = Date.parse(arg.text);
+
+    if (isNaN(millis)) {
+      var msg = l10n.lookupFormat('typesDateNan', [ arg.text ]);
+      return Promise.resolve(new Conversion(undefined, arg, Status.ERROR, msg));
+    }
+
+    value = new Date(millis);
+  }
+
+  return Promise.resolve(new Conversion(value, arg));
+};
+
+DateType.prototype.decrement = function(value, context) {
+  if (!isDate(value)) {
+    return new Date();
+  }
+
+  var newValue = new Date(value);
+  newValue.setDate(value.getDate() - this._step);
+
+  if (newValue >= this.getMin(context)) {
+    return newValue;
+  }
+  else {
+    return this.getMin(context);
+  }
+};
+
+DateType.prototype.increment = function(value, context) {
+  if (!isDate(value)) {
+    return new Date();
+  }
+
+  var newValue = new Date(value);
+  newValue.setDate(value.getDate() + this._step);
+
+  if (newValue <= this.getMax(context)) {
+    return newValue;
+  }
+  else {
+    return this.getMax();
+  }
+};
+
+DateType.prototype.name = 'date';
+
+
+/**
+ * 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)) {
+    throw new Error(l10n.lookupFormat('typesDateNan', [ str ]));
+  }
+  return new Date(millis);
+}
+
+/**
+ * Is |thing| a valid date?
+ * @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
  *
@@ -10973,16 +11205,17 @@ Inputter.prototype._checkAssignment = fu
  * This function updates the data model. It sets the caret to the end of the
  * input. It does not make any similarity checks so calling this function with
  * it's current value resets the cursor position.
  * It does not execute the input or affect the history.
  * This function should not be called internally, by Inputter and never as a
  * result of a keyboard event on this.element or bug 676520 could be triggered.
  */
 Inputter.prototype.setInput = function(str) {
+  this._caretChange = Caret.TO_END;
   return this.requisition.update(str);
 };
 
 /**
  * Counterpart to |setInput| for moving the cursor.
  * @param cursor An object shaped like { start: x, end: y }
  */
 Inputter.prototype.setCursor = function(cursor) {