From e7772592ef3e25b62be36b03120f9482b82dd3ac Mon Sep 17 00:00:00 2001 From: Joe Walker Date: Thu, 12 Jan 2012 10:12:18 +0000 Subject: [PATCH] Bug 706230 - GCLI should have a jump-to-scratchpad feature --- browser/devtools/webconsole/HUDService.jsm | 26 +++ browser/devtools/webconsole/gcli.jsm | 41 +++- .../webconsole/test/browser_gcli_web.js | 178 +++++++++++++++--- .../chrome/browser/devtools/gcli.properties | 2 +- .../browser/devtools/webconsole.properties | 5 + browser/themes/gnomestripe/devtools/gcli.css | 7 + browser/themes/pinstripe/devtools/gcli.css | 7 + browser/themes/winstripe/devtools/gcli.css | 7 + 8 files changed, 238 insertions(+), 35 deletions(-) diff --git a/browser/devtools/webconsole/HUDService.jsm b/browser/devtools/webconsole/HUDService.jsm index c2db89f06f7f..9ffc315bcee9 100644 --- a/browser/devtools/webconsole/HUDService.jsm +++ b/browser/devtools/webconsole/HUDService.jsm @@ -119,6 +119,17 @@ XPCOMUtils.defineLazyGetter(this, "AutocompletePopup", function () { 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); @@ -6815,6 +6826,20 @@ function GcliTerm(aContentWindow, aHudId, aDocument, aConsole, aHintNode, aConso 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, @@ -6828,6 +6853,7 @@ function GcliTerm(aContentWindow, aHudId, aDocument, aConsole, aHintNode, aConso inputBackgroundElement: this.inputStack, hintElement: this.hintNode, consoleWrap: aConsoleWrap, + scratchpad: scratchpad, gcliTerm: this }; diff --git a/browser/devtools/webconsole/gcli.jsm b/browser/devtools/webconsole/gcli.jsm index 321b06007f2a..5a8c5d69cdd8 100644 --- a/browser/devtools/webconsole/gcli.jsm +++ b/browser/devtools/webconsole/gcli.jsm @@ -5416,7 +5416,8 @@ function Console(options) { completeElement: options.completeElement, completionPrompt: '', backgroundElement: options.backgroundElement, - focusManager: this.focusManager + focusManager: this.focusManager, + scratchpad: options.scratchpad }); this.menu = new CommandMenu({ @@ -5567,12 +5568,13 @@ exports.Console = Console; * 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; @@ -5585,6 +5587,7 @@ var inputterCss = require('text!gcli/ui/inputter.css'); */ function Inputter(options) { this.requisition = options.requisition; + this.scratchpad = options.scratchpad; // Suss out where the input element is this.element = options.inputElement || 'gcli-input'; @@ -5866,6 +5869,14 @@ Inputter.prototype.onKeyDown = function(ev) { * 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(); @@ -5964,6 +5975,11 @@ Inputter.prototype.getInputState = function() { 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; }; @@ -5987,6 +6003,7 @@ 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') { @@ -6080,6 +6097,11 @@ Completer.prototype.decorate = function(inputter) { * 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; @@ -6124,6 +6146,7 @@ Completer.prototype.update = function(input) { // ${prefix} // ${contents} // } + // var document = this.element.ownerDocument; var prompt = dom.createElement(document, 'span'); @@ -6167,14 +6190,24 @@ Completer.prototype.update = function(input) { // 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); + } }; /** diff --git a/browser/devtools/webconsole/test/browser_gcli_web.js b/browser/devtools/webconsole/test/browser_gcli_web.js index a991d8864fdf..691cfac42572 100644 --- a/browser/devtools/webconsole/test/browser_gcli_web.js +++ b/browser/devtools/webconsole/test/browser_gcli_web.js @@ -54,7 +54,53 @@ var Node = Components.interfaces.nsIDOMNode; * 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'); @@ -69,14 +115,12 @@ define('gclitest/suite', ['require', 'exports', 'module' , 'gcli/index', 'test/e 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 @@ -119,10 +163,10 @@ examiner.addSuite = function(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; }; @@ -130,14 +174,14 @@ examiner.run = function() { /** * 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(); @@ -146,9 +190,9 @@ examiner.runAsyncInternal = function(i, callback) { } 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)); }; @@ -222,30 +266,30 @@ function Suite(suiteName, suite) { /** * 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(); } @@ -259,7 +303,7 @@ Suite.prototype.runAsync = function(callback) { /** * 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(); @@ -268,9 +312,9 @@ Suite.prototype.runAsyncInternal = function(i, callback) { } 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)); }; @@ -304,13 +348,13 @@ function Test(suite, name, func) { /** * 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; @@ -331,7 +375,7 @@ Test.prototype.run = function() { /** * 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') { @@ -1510,14 +1554,18 @@ function check(initial, action, after) { 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() { @@ -1572,6 +1620,59 @@ exports.testIncrDecr = function() { check('tselarr 3', KEY_UPS_TO, 'tselarr 2'); }; +}); +/* + * 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 @@ -1828,6 +1929,11 @@ function check(expStatuses, expStatus, expAssign, expPredict) { 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); @@ -1856,7 +1962,7 @@ exports.testBasic = function() { 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'); @@ -1898,6 +2004,7 @@ exports.testBasic = function() { }); function undefine() { + delete define.modules['gclitest/index']; delete define.modules['gclitest/suite']; delete define.modules['test/examiner']; delete define.modules['gclitest/testTokenize']; @@ -1907,11 +2014,13 @@ function undefine() { 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']; @@ -1921,6 +2030,7 @@ function undefine() { 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']; @@ -1948,12 +2058,20 @@ function onLoad() { 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(); diff --git a/browser/locales/en-US/chrome/browser/devtools/gcli.properties b/browser/locales/en-US/chrome/browser/devtools/gcli.properties index a75b99feeb3f..a0c335d1003e 100644 --- a/browser/locales/en-US/chrome/browser/devtools/gcli.properties +++ b/browser/locales/en-US/chrome/browser/devtools/gcli.properties @@ -20,7 +20,7 @@ cliEvalJavascript=Enter JavaScript directly # 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 diff --git a/browser/locales/en-US/chrome/browser/devtools/webconsole.properties b/browser/locales/en-US/chrome/browser/devtools/webconsole.properties index c91a0b357eca..d8c0cfbe39a8 100644 --- a/browser/locales/en-US/chrome/browser/devtools/webconsole.properties +++ b/browser/locales/en-US/chrome/browser/devtools/webconsole.properties @@ -146,6 +146,11 @@ webConsolePositionWindow=Window # 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 diff --git a/browser/themes/gnomestripe/devtools/gcli.css b/browser/themes/gnomestripe/devtools/gcli.css index 2685d14882ce..b924b667b33b 100644 --- a/browser/themes/gnomestripe/devtools/gcli.css +++ b/browser/themes/gnomestripe/devtools/gcli.css @@ -305,6 +305,13 @@ 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 { diff --git a/browser/themes/pinstripe/devtools/gcli.css b/browser/themes/pinstripe/devtools/gcli.css index a0eebfc6acb0..59125ab97ae0 100644 --- a/browser/themes/pinstripe/devtools/gcli.css +++ b/browser/themes/pinstripe/devtools/gcli.css @@ -309,6 +309,13 @@ 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 { diff --git a/browser/themes/winstripe/devtools/gcli.css b/browser/themes/winstripe/devtools/gcli.css index 36dbd74b4946..f6aa20db2a31 100644 --- a/browser/themes/winstripe/devtools/gcli.css +++ b/browser/themes/winstripe/devtools/gcli.css @@ -305,6 +305,13 @@ 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 {