Bug 706230 - GCLI should have a jump-to-scratchpad feature
authorJoe Walker <jwalker@mozilla.com>
Thu, 12 Jan 2012 10:12:18 +0000
changeset 85613 05f3cb248cc4b500cd8c452314ab48d9838c6bb2
parent 85612 7f6c0fc6ef8c2e188a64eee43bb6a17b4cd8f5a6
child 85614 ce832e50680701bb80c22832092b2e083c70a311
push id805
push userakeybl@mozilla.com
push dateWed, 01 Feb 2012 18:17:35 +0000
treeherdermozilla-aurora@6fb3bf232436 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
bugs706230
milestone12.0a1
Bug 706230 - GCLI should have a jump-to-scratchpad feature
browser/devtools/webconsole/HUDService.jsm
browser/devtools/webconsole/gcli.jsm
browser/devtools/webconsole/test/browser_gcli_web.js
browser/locales/en-US/chrome/browser/devtools/gcli.properties
browser/locales/en-US/chrome/browser/devtools/webconsole.properties
browser/themes/gnomestripe/devtools/gcli.css
browser/themes/pinstripe/devtools/gcli.css
browser/themes/winstripe/devtools/gcli.css
--- a/browser/devtools/webconsole/HUDService.jsm
+++ b/browser/devtools/webconsole/HUDService.jsm
@@ -114,16 +114,27 @@ XPCOMUtils.defineLazyGetter(this, "Autoc
     Cu.import("resource:///modules/AutocompletePopup.jsm", obj);
   }
   catch (err) {
     Cu.reportError(err);
   }
   return obj.AutocompletePopup;
 });
 
+XPCOMUtils.defineLazyGetter(this, "ScratchpadManager", function () {
+  var obj = {};
+  try {
+    Cu.import("resource:///modules/devtools/scratchpad-manager.jsm", obj);
+  }
+  catch (err) {
+    Cu.reportError(err);
+  }
+  return obj.ScratchpadManager;
+});
+
 XPCOMUtils.defineLazyGetter(this, "namesAndValuesOf", function () {
   var obj = {};
   Cu.import("resource:///modules/PropertyPanel.jsm", obj);
   return obj.namesAndValuesOf;
 });
 
 XPCOMUtils.defineLazyGetter(this, "gConsoleStorage", function () {
   let obj = {};
@@ -6810,29 +6821,44 @@ function GcliTerm(aContentWindow, aHudId
   this.hintNode = aHintNode;
 
   this.createUI();
   this.createSandbox();
 
   this.show = this.show.bind(this);
   this.hide = this.hide.bind(this);
 
+  // Allow GCLI:Inputter to decide how and when to open a scratchpad window
+  let scratchpad = {
+    shouldActivate: function Scratchpad_shouldActivate(aEvent) {
+      return aEvent.shiftKey &&
+          aEvent.keyCode === Ci.nsIDOMKeyEvent.DOM_VK_RETURN;
+    },
+    activate: function Scratchpad_activate(aValue) {
+      aValue = aValue.replace(/^\s*{\s*/, '');
+      ScratchpadManager.openScratchpad({ text: aValue });
+      return true;
+    },
+    linkText: stringBundle.GetStringFromName('scratchpad.linkText')
+  };
+
   this.opts = {
     environment: { hudId: this.hudId },
     chromeDocument: this.document,
     contentDocument: aContentWindow.document,
     jsEnvironment: {
       globalObject: unwrap(aContentWindow),
       evalFunction: this.evalInSandbox.bind(this)
     },
     inputElement: this.inputNode,
     completeElement: this.completeNode,
     inputBackgroundElement: this.inputStack,
     hintElement: this.hintNode,
     consoleWrap: aConsoleWrap,
+    scratchpad: scratchpad,
     gcliTerm: this
   };
 
   gcli._internal.commandOutputManager.addListener(this.onCommandOutput, this);
   gcli._internal.createView(this.opts);
 
   if (!commandExports) {
     commandExports = loadCommands();
--- a/browser/devtools/webconsole/gcli.jsm
+++ b/browser/devtools/webconsole/gcli.jsm
@@ -5411,17 +5411,18 @@ function Console(options) {
 
   this.inputter = new Inputter({
     document: options.chromeDocument,
     requisition: options.requisition,
     inputElement: options.inputElement,
     completeElement: options.completeElement,
     completionPrompt: '',
     backgroundElement: options.backgroundElement,
-    focusManager: this.focusManager
+    focusManager: this.focusManager,
+    scratchpad: options.scratchpad
   });
 
   this.menu = new CommandMenu({
     document: options.chromeDocument,
     requisition: options.requisition,
     menuClass: 'gcliterm-menu'
   });
   this.hintElement.appendChild(this.menu.element);
@@ -5562,34 +5563,36 @@ exports.Console = Console;
 
 });
 /*
  * 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/ui/inputter', ['require', 'exports', 'module' , 'gcli/util', 'gcli/types', 'gcli/history', 'text!gcli/ui/inputter.css'], function(require, exports, module) {
+define('gcli/ui/inputter', ['require', 'exports', 'module' , 'gcli/util', 'gcli/l10n', 'gcli/types', 'gcli/history', 'text!gcli/ui/inputter.css'], function(require, exports, module) {
 var cliView = exports;
 
 
 var KeyEvent = require('gcli/util').event.KeyEvent;
 var dom = require('gcli/util').dom;
+var l10n = require('gcli/l10n');
 
 var Status = require('gcli/types').Status;
 var History = require('gcli/history').History;
 
 var inputterCss = require('text!gcli/ui/inputter.css');
 
 
 /**
  * A wrapper to take care of the functions concerning an input element
  */
 function Inputter(options) {
   this.requisition = options.requisition;
+  this.scratchpad = options.scratchpad;
 
   // Suss out where the input element is
   this.element = options.inputElement || 'gcli-input';
   if (typeof this.element === 'string') {
     this.document = options.document || document;
     var name = this.element;
     this.element = this.document.getElementById(name);
     if (!this.element) {
@@ -5861,16 +5864,24 @@ Inputter.prototype.onKeyDown = function(
     }
   }
 };
 
 /**
  * The main keyboard processing loop
  */
 Inputter.prototype.onKeyUp = function(ev) {
+  // Give the scratchpad (if enabled) a chance to activate
+  if (this.scratchpad && this.scratchpad.shouldActivate(ev)) {
+    if (this.scratchpad.activate(this.element.value)) {
+      this._setInputInternal('', true);
+    }
+    return;
+  }
+
   // RETURN does a special exec/highlight thing
   if (ev.keyCode === KeyEvent.DOM_VK_RETURN) {
     var worst = this.requisition.getStatus();
     // Deny RETURN unless the command might work
     if (worst === Status.VALID) {
       this._scrollingThroughHistory = false;
       this.history.add(this.element.value);
       this.requisition.exec();
@@ -5959,16 +5970,21 @@ Inputter.prototype.getInputState = funct
 
   // Workaround for potential XUL bug 676520 where textbox gives incorrect
   // values for its content
   if (input.typed == null) {
     input = { typed: '', cursor: { start: 0, end: 0 } };
     console.log('fixing input.typed=""', input);
   }
 
+  // Workaround for a Bug 717268 (which is really a jsdom bug)
+  if (input.cursor.start == null) {
+    input.cursor.start = 0;
+  }
+
   return input;
 };
 
 cliView.Inputter = Inputter;
 
 
 /**
  * Completer is an 'input-like' element that sits  an input element annotating
@@ -5982,16 +5998,17 @@ cliView.Inputter = Inputter;
  * - completionPrompt (optional) The prompt - defaults to '\u00bb'
  *   (double greater-than, a.k.a right guillemet). The prompt is used directly
  *   in a TextNode, so HTML entities are not allowed.
  */
 function Completer(options) {
   this.document = options.document || document;
   this.requisition = options.requisition;
   this.elementCreated = false;
+  this.scratchpad = options.scratchpad;
 
   this.element = options.completeElement || 'gcli-row-complete';
   if (typeof this.element === 'string') {
     var name = this.element;
     this.element = this.document.getElementById(name);
 
     if (!this.element) {
       this.elementCreated = true;
@@ -6075,16 +6092,21 @@ Completer.prototype.decorate = function(
     this.resizer();
   }
 };
 
 /**
  * Ensure that the completion element is the same size and the inputter element
  */
 Completer.prototype.resizer = function() {
+  // Remove this when jsdom does getBoundingClientRect(). See Bug 717269
+  if (!this.inputter.element.getBoundingClientRect) {
+    return;
+  }
+
   var rect = this.inputter.element.getBoundingClientRect();
   // -4 to line up with 1px of padding and border, top and bottom
   var height = rect.bottom - rect.top - 4;
 
   this.element.style.top = rect.top + 'px';
   this.element.style.height = height + 'px';
   this.element.style.lineHeight = height + 'px';
   this.element.style.left = rect.left + 'px';
@@ -6119,16 +6141,17 @@ Completer.prototype.update = function(in
   // which is complex due to a need to merge spans.
   // Bug 707131 questions if we couldn't simplify this to use a template.
   //
   // <span class="gcli-prompt">${completionPrompt}</span>
   // ${appendMarkupStatus()}
   // ${prefix}
   // <span class="gcli-in-ontab">${contents}</span>
   // <span class="gcli-in-closebrace" if="${unclosedJs}">}<span>
+  // <div class="gcli-in-scratchlink">${scratchLink}</div>
 
   var document = this.element.ownerDocument;
   var prompt = dom.createElement(document, 'span');
   prompt.classList.add('gcli-prompt');
   prompt.appendChild(document.createTextNode(this.completionPrompt + ' '));
   this.element.appendChild(prompt);
 
   if (input.typed.length > 0) {
@@ -6162,24 +6185,34 @@ Completer.prototype.update = function(in
     suffix.classList.add('gcli-in-ontab');
     suffix.appendChild(document.createTextNode(contents));
     this.element.appendChild(suffix);
   }
 
   // Add a grey '}' to the end of the command line when we've opened
   // with a { but haven't closed it
   var command = this.requisition.commandAssignment.getValue();
-  var unclosedJs = command && command.name === '{' &&
+  var isJsCommand = (command && command.name === '{');
+  var isUnclosedJs = isJsCommand &&
           this.requisition.getAssignment(0).getArg().suffix.indexOf('}') === -1;
-  if (unclosedJs) {
+  if (isUnclosedJs) {
     var close = dom.createElement(document, 'span');
     close.classList.add('gcli-in-closebrace');
     close.appendChild(document.createTextNode(' }'));
     this.element.appendChild(close);
   }
+
+  // Create a scratchpad link if it's a JS command and we have a function to
+  // actually perform the request
+  if (isJsCommand && this.scratchpad) {
+    var hint = dom.createElement(document, 'div');
+    hint.classList.add('gcli-in-scratchlink');
+    hint.appendChild(document.createTextNode(this.scratchpad.linkText));
+    this.element.appendChild(hint);
+  }
 };
 
 /**
  * Mark-up an array of Status values with spans
  */
 Completer.prototype.appendMarkupStatus = function(element, scores, input) {
   if (scores.length === 0) {
     return;
--- a/browser/devtools/webconsole/test/browser_gcli_web.js
+++ b/browser/devtools/webconsole/test/browser_gcli_web.js
@@ -49,39 +49,83 @@ var define = obj.gcli._internal.define;
 var console = obj.gcli._internal.console;
 var Node = Components.interfaces.nsIDOMNode;
 /*
  * 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('gclitest/suite', ['require', 'exports', 'module' , 'gcli/index', 'test/examiner', 'gclitest/testTokenize', 'gclitest/testSplit', 'gclitest/testCli', 'gclitest/testExec', 'gclitest/testKeyboard', 'gclitest/testHistory', 'gclitest/testRequire', 'gclitest/testJs'], function(require, exports, module) {
+define('gclitest/index', ['require', 'exports', 'module' , 'gclitest/suite', 'gcli/types/javascript'], function(require, exports, module) {
+
+  var examiner = require('gclitest/suite').examiner;
+  var javascript = require('gcli/types/javascript');
+
+  /**
+   * Run the tests defined in the test suite
+   * @param options How the tests are run. Properties include:
+   * - window: The browser window object to run the tests against
+   * - useFakeWindow: Use a test subset and a fake DOM to avoid a real document
+   * - detailedResultLog: console.log test passes and failures in more detail
+   */
+  exports.run = function(options) {
+    options = options || {};
+
+    if (options.useFakeWindow) {
+      // A minimum fake dom to get us through the JS tests
+      var doc = { title: 'Fake DOM' };
+      var fakeWindow = {
+        window: { document: doc },
+        document: doc
+      };
+
+      options.window = fakeWindow;
+    }
+
+    if (options.window) {
+      javascript.setGlobalObject(options.window);
+    }
+
+    examiner.run(options);
+
+    if (options.detailedResultLog) {
+      examiner.log();
+    }
+    else {
+      console.log('Completed test suite');
+    }
+  };
+});
+/*
+ * 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('gclitest/suite', ['require', 'exports', 'module' , 'gcli/index', 'test/examiner', 'gclitest/testTokenize', 'gclitest/testSplit', 'gclitest/testCli', 'gclitest/testExec', 'gclitest/testKeyboard', 'gclitest/testScratchpad', 'gclitest/testHistory', 'gclitest/testRequire', 'gclitest/testJs'], function(require, exports, module) {
 
   // We need to make sure GCLI is initialized before we begin testing it
   require('gcli/index');
 
   var examiner = require('test/examiner');
 
   // It's tempting to want to unify these strings and make addSuite() do the
   // call to require(), however that breaks the build system which looks for
   // the strings passed to require
   examiner.addSuite('gclitest/testTokenize', require('gclitest/testTokenize'));
   examiner.addSuite('gclitest/testSplit', require('gclitest/testSplit'));
   examiner.addSuite('gclitest/testCli', require('gclitest/testCli'));
   examiner.addSuite('gclitest/testExec', require('gclitest/testExec'));
   examiner.addSuite('gclitest/testKeyboard', require('gclitest/testKeyboard'));
+  examiner.addSuite('gclitest/testScratchpad', require('gclitest/testScratchpad'));
   examiner.addSuite('gclitest/testHistory', require('gclitest/testHistory'));
   examiner.addSuite('gclitest/testRequire', require('gclitest/testRequire'));
   examiner.addSuite('gclitest/testJs', require('gclitest/testJs'));
 
-  examiner.run();
-  console.log('Completed test suite');
-  // examiner.log();
-
+  exports.examiner = examiner;
 });
 /*
  * 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('test/examiner', ['require', 'exports', 'module' ], function(require, exports, module) {
@@ -114,46 +158,46 @@ var stati = {
  */
 examiner.addSuite = function(name, suite) {
   examiner.suites[name] = new Suite(name, suite);
 };
 
 /**
  * Run all the tests synchronously
  */
-examiner.run = function() {
+examiner.run = function(options) {
   Object.keys(examiner.suites).forEach(function(suiteName) {
     var suite = examiner.suites[suiteName];
-    suite.run();
+    suite.run(options);
   }.bind(this));
   return examiner.suites;
 };
 
 /**
  * Run all the tests asynchronously
  */
-examiner.runAsync = function(callback) {
-  this.runAsyncInternal(0, callback);
+examiner.runAsync = function(options, callback) {
+  this.runAsyncInternal(0, options, callback);
 };
 
 /**
  * Run all the test suits asynchronously
  */
-examiner.runAsyncInternal = function(i, callback) {
+examiner.runAsyncInternal = function(i, options, callback) {
   if (i >= Object.keys(examiner.suites).length) {
     if (typeof callback === 'function') {
       callback();
     }
     return;
   }
 
   var suiteName = Object.keys(examiner.suites)[i];
-  examiner.suites[suiteName].runAsync(function() {
+  examiner.suites[suiteName].runAsync(options, function() {
     setTimeout(function() {
-      examiner.runAsyncInternal(i + 1, callback);
+      examiner.runAsyncInternal(i + 1, options, callback);
     }.bind(this), delay);
   }.bind(this));
 };
 
 /**
  *
  */
 examiner.reportToText = function() {
@@ -217,65 +261,65 @@ function Suite(suiteName, suite) {
       this.tests[testName] = test;
     }
   }.bind(this));
 }
 
 /**
  * Run all the tests in this suite synchronously
  */
-Suite.prototype.run = function() {
+Suite.prototype.run = function(options) {
   if (typeof this.suite.setup == "function") {
-    this.suite.setup();
+    this.suite.setup(options);
   }
 
   Object.keys(this.tests).forEach(function(testName) {
     var test = this.tests[testName];
-    test.run();
+    test.run(options);
   }.bind(this));
 
   if (typeof this.suite.shutdown == "function") {
-    this.suite.shutdown();
+    this.suite.shutdown(options);
   }
 };
 
 /**
  * Run all the tests in this suite asynchronously
  */
-Suite.prototype.runAsync = function(callback) {
+Suite.prototype.runAsync = function(options, callback) {
   if (typeof this.suite.setup == "function") {
     this.suite.setup();
   }
 
-  this.runAsyncInternal(0, function() {
+  this.runAsyncInternal(0, options, function() {
     if (typeof this.suite.shutdown == "function") {
       this.suite.shutdown();
     }
 
     if (typeof callback === 'function') {
       callback();
     }
   }.bind(this));
 };
 
 /**
  * Function used by the async runners that can handle async recursion.
  */
-Suite.prototype.runAsyncInternal = function(i, callback) {
+Suite.prototype.runAsyncInternal = function(i, options, callback) {
   if (i >= Object.keys(this.tests).length) {
     if (typeof callback === 'function') {
       callback();
     }
     return;
   }
 
   var testName = Object.keys(this.tests)[i];
-  this.tests[testName].runAsync(function() {
+  this.tests[testName].runAsync(options, function() {
     setTimeout(function() {
-      this.runAsyncInternal(i + 1, callback);
+      this.runAsyncInternal(i + 1, options, callback);
     }.bind(this), delay);
   }.bind(this));
 };
 
 /**
  * Create a JSON object suitable for serialization
  */
 Suite.prototype.toRemote = function() {
@@ -299,23 +343,23 @@ function Test(suite, name, func) {
 
   this.messages = [];
   this.status = stati.notrun;
 }
 
 /**
  * Run just a single test
  */
-Test.prototype.run = function() {
+Test.prototype.run = function(options) {
   currentTest = this;
   this.status = stati.executing;
   this.messages = [];
 
   try {
-    this.func.apply(this.suite);
+    this.func.apply(this.suite, [ options ]);
   }
   catch (ex) {
     this.status = stati.fail;
     this.messages.push('' + ex);
     console.error(ex);
     if (ex.stack) {
       console.error(ex.stack);
     }
@@ -326,17 +370,17 @@ Test.prototype.run = function() {
   }
 
   currentTest = null;
 };
 
 /**
  * Run all the tests in this suite asynchronously
  */
-Test.prototype.runAsync = function(callback) {
+Test.prototype.runAsync = function(options, callback) {
   setTimeout(function() {
     this.run();
     if (typeof callback === 'function') {
       callback();
     }
   }.bind(this), delay);
 };
 
@@ -1505,24 +1549,28 @@ function check(initial, action, after) {
     case KEY_DOWNS_TO:
       assignment.decrement();
       break;
   }
 
   test.is(after, requisition.toString(), initial + ' + ' + action + ' -> ' + after);
 }
 
-exports.testComplete = function() {
+exports.testComplete = function(options) {
   check('tsela', COMPLETES_TO, 'tselarr ');
   check('tsn di', COMPLETES_TO, 'tsn dif ');
   check('tsg a', COMPLETES_TO, 'tsg aaa ');
 
   check('{ wind', COMPLETES_TO, '{ window');
   check('{ window.docum', COMPLETES_TO, '{ window.document');
-  check('{ window.document.titl', COMPLETES_TO, '{ window.document.title ');
+
+  // Bug 717228: This fails under node
+  if (!options.isNode) {
+    check('{ window.document.titl', COMPLETES_TO, '{ window.document.title ');
+  }
 };
 
 exports.testIncrDecr = function() {
   check('tsu -70', KEY_UPS_TO, 'tsu -5');
   check('tsu -7', KEY_UPS_TO, 'tsu -5');
   check('tsu -6', KEY_UPS_TO, 'tsu -5');
   check('tsu -5', KEY_UPS_TO, 'tsu -3');
   check('tsu -4', KEY_UPS_TO, 'tsu -3');
@@ -1574,16 +1622,69 @@ exports.testIncrDecr = function() {
 
 });
 /*
  * 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('gclitest/testScratchpad', ['require', 'exports', 'module' , 'test/assert'], function(require, exports, module) {
+
+
+var test = require('test/assert');
+
+var origScratchpad;
+
+exports.setup = function(options) {
+  if (options.inputter) {
+    origScratchpad = options.inputter.scratchpad;
+    options.inputter.scratchpad = stubScratchpad;
+  }
+};
+
+exports.shutdown = function(options) {
+  if (options.inputter) {
+    options.inputter.scratchpad = origScratchpad;
+  }
+};
+
+var stubScratchpad = {
+  shouldActivate: function(ev) {
+    return true;
+  },
+  activatedCount: 0,
+  linkText: 'scratchpad.linkText'
+};
+stubScratchpad.activate = function(value) {
+  stubScratchpad.activatedCount++;
+  return true;
+};
+
+
+exports.testActivate = function(options) {
+  if (options.inputter) {
+    var ev = {};
+    stubScratchpad.activatedCount = 0;
+    options.inputter.onKeyUp(ev);
+    test.is(1, stubScratchpad.activatedCount, 'scratchpad is activated');
+  }
+  else {
+    console.log('Skipping scratchpad tests');
+  }
+};
+
+
+});
+/*
+ * 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('gclitest/testHistory', ['require', 'exports', 'module' , 'test/assert', 'gcli/history'], function(require, exports, module) {
 
 var test = require('test/assert');
 var History = require('gcli/history').History;
 
 exports.setup = function() {
 };
 
@@ -1823,16 +1924,21 @@ function check(expStatuses, expStatus, e
       expPredict.forEach(function(p) {
         contains = predictionsHas(p);
         test.ok(contains, 'missing prediction ' + p);
       });
     }
     else if (typeof expPredict === 'number') {
       contains = true;
       test.is(assign.getPredictions().length, expPredict, 'prediction count');
+      if (assign.getPredictions().length !== expPredict) {
+        assign.getPredictions().forEach(function(prediction) {
+          console.log('actual prediction: ', prediction);
+        });
+      }
     }
     else {
       contains = predictionsHas(expPredict);
       test.ok(contains, 'missing prediction ' + expPredict);
     }
 
     if (!contains) {
       console.log('Predictions: ' + assign.getPredictions().map(function(p) {
@@ -1851,17 +1957,17 @@ exports.testBasic = function() {
 
   input('{ w');
   check('VVI', Status.ERROR, 'w', 'window');
 
   input('{ windo');
   check('VVIIIII', Status.ERROR, 'windo', 'window');
 
   input('{ window');
-  check('VVVVVVVV', Status.VALID, 'window', 0);
+  check('VVVVVVVV', Status.VALID, 'window');
 
   input('{ window.d');
   check('VVIIIIIIII', Status.ERROR, 'window.d', 'window.document');
 
   input('{ window.document.title');
   check('VVVVVVVVVVVVVVVVVVVVVVV', Status.VALID, 'window.document.title', 0);
 
   input('{ d');
@@ -1893,39 +1999,43 @@ exports.testBasic = function() {
   input('{ donteval.xxx');
   check('VVVVVVVVVVVVVV', Status.VALID, 'donteval.xxx', 0);
 };
 
 
 });
 
 function undefine() {
+  delete define.modules['gclitest/index'];
   delete define.modules['gclitest/suite'];
   delete define.modules['test/examiner'];
   delete define.modules['gclitest/testTokenize'];
   delete define.modules['test/assert'];
   delete define.modules['gclitest/testSplit'];
   delete define.modules['gclitest/commands'];
   delete define.modules['gclitest/testCli'];
   delete define.modules['gclitest/testExec'];
   delete define.modules['gclitest/testKeyboard'];
+  delete define.modules['gclitest/testScratchpad'];
   delete define.modules['gclitest/testHistory'];
   delete define.modules['gclitest/testRequire'];
   delete define.modules['gclitest/requirable'];
   delete define.modules['gclitest/testJs'];
 
+  delete define.globalDomain.modules['gclitest/index'];
   delete define.globalDomain.modules['gclitest/suite'];
   delete define.globalDomain.modules['test/examiner'];
   delete define.globalDomain.modules['gclitest/testTokenize'];
   delete define.globalDomain.modules['test/assert'];
   delete define.globalDomain.modules['gclitest/testSplit'];
   delete define.globalDomain.modules['gclitest/commands'];
   delete define.globalDomain.modules['gclitest/testCli'];
   delete define.globalDomain.modules['gclitest/testExec'];
   delete define.globalDomain.modules['gclitest/testKeyboard'];
+  delete define.globalDomain.modules['gclitest/testScratchpad'];
   delete define.globalDomain.modules['gclitest/testHistory'];
   delete define.globalDomain.modules['gclitest/testRequire'];
   delete define.globalDomain.modules['gclitest/requirable'];
   delete define.globalDomain.modules['gclitest/testJs'];
 }
 
 registerCleanupFunction(function() {
   Services.prefs.clearUserPref("devtools.gcli.enable");
@@ -1943,22 +2053,30 @@ function test() {
 }
 
 function onLoad() {
   browser.removeEventListener("DOMContentLoaded", onLoad, false);
   var failed = false;
 
   try {
     openConsole();
-    define.globalDomain.require("gclitest/index");
+
+    var gcliterm = HUDService.getHudByWindow(content).gcliterm;
+
+    var gclitest = define.globalDomain.require("gclitest/index");
+    gclitest.run({
+      window: gcliterm.document.defaultView,
+      inputter: gcliterm.opts.console.inputter,
+      requisition: gcliterm.opts.requistion
+    });
   }
   catch (ex) {
     failed = ex;
-    console.error('Test Failure', ex);
-    ok(false, '' + ex);
+    console.error("Test Failure", ex);
+    ok(false, "" + ex);
   }
   finally {
     closeConsole();
     finish();
   }
 
   if (failed) {
     throw failed;
--- a/browser/locales/en-US/chrome/browser/devtools/gcli.properties
+++ b/browser/locales/en-US/chrome/browser/devtools/gcli.properties
@@ -15,17 +15,17 @@ canonDescNone=(No description)
 # of JavaScript like traditional developer tool command lines. This describes
 # the '{' command.
 cliEvalJavascript=Enter JavaScript directly
 
 # LOCALIZATION NOTE (fieldSelectionSelect): When a command has a parameter
 # that has a number of pre-defined options the user interface presents these
 # in a drop-down menu, where the first 'option' is an indicator that a
 # selection should be made. This string describes that first option.
-fieldSelectionSelect=Select a %S …
+fieldSelectionSelect=Select a %S…
 
 # LOCALIZATION NOTE (fieldArrayAdd): When a command has a parameter that can
 # be repeated a number of times (e.g. like the 'cat a.txt b.txt' command) the
 # user interface presents buttons to add and remove arguments. This string is
 # used to add arguments.
 fieldArrayAdd=Add
 
 # LOCALIZATION NOTE (fieldArrayDel): When a command has a parameter that can
--- a/browser/locales/en-US/chrome/browser/devtools/webconsole.properties
+++ b/browser/locales/en-US/chrome/browser/devtools/webconsole.properties
@@ -141,16 +141,21 @@ webConsolePositionBelow=Below
 webConsolePositionWindow=Window
 
 # LOCALIZATION NOTE (webConsoleWindowTitleAndURL): The Web Console floating
 # panel title, followed by the web page URL.
 # For RTL languages you need to set the LRM in the string to give the URL
 # the correct direction.
 webConsoleWindowTitleAndURL=Web Console - %S
 
+# LOCALIZATION NOTE (scratchpad.linkText):
+# The text used in the right hand side of the web console command line when
+# Javascript is being entered, to indicate how to jump into scratchpad mode
+scratchpad.linkText=Shift+RETURN - Open in Scratchpad
+
 # LOCALIZATION NOTE (Autocomplete.label):
 # The autocomplete popup panel label/title.
 Autocomplete.label=Autocomplete popup
 
 # LOCALIZATION NOTE (stacktrace.anonymousFunction):
 # This string is used to display JavaScript functions that have no given name -
 # they are said to be anonymous. See stacktrace.outputMessage.
 stacktrace.anonymousFunction=<anonymous>
--- a/browser/themes/gnomestripe/devtools/gcli.css
+++ b/browser/themes/gnomestripe/devtools/gcli.css
@@ -300,16 +300,23 @@
   color: #999;
 }
 
 .gcli-prompt {
   color: #66F;
   font-weight: bold;
 }
 
+.gcli-in-scratchlink {
+  float: right;
+  font-size: 85%;
+  color: #888;
+  padding-right: 10px;
+}
+
 /* From: $GCLI/lib/gcli/commands/help.css */
 
 .gcli-help-name {
   text-align: end;
 }
 
 .gcli-help-arrow {
   font-size: 70%;
--- a/browser/themes/pinstripe/devtools/gcli.css
+++ b/browser/themes/pinstripe/devtools/gcli.css
@@ -304,16 +304,23 @@
   color: #999;
 }
 
 .gcli-prompt {
   color: #66F;
   font-weight: bold;
 }
 
+.gcli-in-scratchlink {
+  float: right;
+  font-size: 85%;
+  color: #888;
+  padding-right: 10px;
+}
+
 /* From: $GCLI/lib/gcli/commands/help.css */
 
 .gcli-help-name {
   text-align: end;
 }
 
 .gcli-help-arrow {
   font-size: 70%;
--- a/browser/themes/winstripe/devtools/gcli.css
+++ b/browser/themes/winstripe/devtools/gcli.css
@@ -300,16 +300,23 @@
   color: #999;
 }
 
 .gcli-prompt {
   color: #66F;
   font-weight: bold;
 }
 
+.gcli-in-scratchlink {
+  float: right;
+  font-size: 85%;
+  color: #888;
+  padding-right: 10px;
+}
+
 /* From: $GCLI/lib/gcli/commands/help.css */
 
 .gcli-help-name {
   text-align: end;
 }
 
 .gcli-help-arrow {
   font-size: 70%;