diff --git a/devtools/client/webconsole/components/JSTerm.js b/devtools/client/webconsole/components/JSTerm.js index aabf5989c97b..752303ce1463 100644 --- a/devtools/client/webconsole/components/JSTerm.js +++ b/devtools/client/webconsole/components/JSTerm.js @@ -1173,10 +1173,10 @@ class JSTerm extends Component { } if (this._autocompleteQuery && input.startsWith(this._autocompleteQuery)) { let filterBy = input; - // Find the last non-alphanumeric other than _ or $ if it exists. - const lastNonAlpha = input.match(/[^a-zA-Z0-9_$][a-zA-Z0-9_$]*$/); + // Find the last non-alphanumeric other than "_", ":", or "$" if it exists. + const lastNonAlpha = input.match(/[^a-zA-Z0-9_$:][a-zA-Z0-9_$:]*$/); // If input contains non-alphanumerics, use the part after the last one - // to filter the cache + // to filter the cache. if (lastNonAlpha) { filterBy = input.substring(input.lastIndexOf(lastNonAlpha) + 1); } diff --git a/devtools/client/webconsole/test/mochitest/browser.ini b/devtools/client/webconsole/test/mochitest/browser.ini index 40b5ae0c48d0..abe25b32a6a5 100644 --- a/devtools/client/webconsole/test/mochitest/browser.ini +++ b/devtools/client/webconsole/test/mochitest/browser.ini @@ -185,6 +185,7 @@ skip-if = verify [browser_jsterm_autocomplete_array_no_index.js] [browser_jsterm_autocomplete_arrow_keys.js] [browser_jsterm_autocomplete_cached_results.js] +[browser_jsterm_autocomplete_commands.js] [browser_jsterm_autocomplete_crossdomain_iframe.js] [browser_jsterm_autocomplete_escape_key.js] [browser_jsterm_autocomplete_extraneous_closing_brackets.js] diff --git a/devtools/client/webconsole/test/mochitest/browser_jsterm_autocomplete_commands.js b/devtools/client/webconsole/test/mochitest/browser_jsterm_autocomplete_commands.js new file mode 100644 index 000000000000..1631de274d5c --- /dev/null +++ b/devtools/client/webconsole/test/mochitest/browser_jsterm_autocomplete_commands.js @@ -0,0 +1,59 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test that console commands are autocompleted. + +const TEST_URI = `data:text/html;charset=utf-8,Test command autocomplete`; + +add_task(async function() { + // Run test with legacy JsTerm + await performTests(); + // And then run it with the CodeMirror-powered one. + await pushPref("devtools.webconsole.jsterm.codeMirror", true); + await performTests(); +}); + +async function performTests() { + const { jsterm } = await openNewTabAndConsole(TEST_URI); + const { autocompletePopup } = jsterm; + + const onPopUpOpen = autocompletePopup.once("popup-opened"); + + info(`Enter ":"`); + jsterm.focus(); + EventUtils.sendString(":"); + + await onPopUpOpen; + + const expectedCommands = [":help", ":screenshot"]; + is(getPopupItems(autocompletePopup).join("\n"), expectedCommands.join("\n"), + "popup contains expected commands"); + + let onAutocompleUpdated = jsterm.once("autocomplete-updated"); + EventUtils.sendString("s"); + await onAutocompleUpdated; + checkJsTermCompletionValue(jsterm, " creenshot", + "completion node has expected :screenshot value"); + + EventUtils.synthesizeKey("KEY_Tab"); + is(jsterm.getInputValue(), ":screenshot", "Tab key correctly completed :screenshot"); + + ok(!autocompletePopup.isOpen, "popup is closed after Tab"); + + info("Test :hel completion"); + jsterm.setInputValue(":he"); + onAutocompleUpdated = jsterm.once("autocomplete-updated"); + EventUtils.sendString("l"); + + await onAutocompleUpdated; + checkJsTermCompletionValue(jsterm, " p", "completion node has expected :help value"); + + EventUtils.synthesizeKey("KEY_Tab"); + is(jsterm.getInputValue(), ":help", "Tab key correctly completes :help"); +} + +function getPopupItems(popup) { + return popup.items.map(item => item.label); +} diff --git a/devtools/server/actors/webconsole.js b/devtools/server/actors/webconsole.js index b3870e91fa73..cf1fb1718fe4 100644 --- a/devtools/server/actors/webconsole.js +++ b/devtools/server/actors/webconsole.js @@ -31,6 +31,7 @@ loader.lazyRequireGetter(this, "WebConsoleCommands", "devtools/server/actors/web loader.lazyRequireGetter(this, "addWebConsoleCommands", "devtools/server/actors/webconsole/utils", true); loader.lazyRequireGetter(this, "formatCommand", "devtools/server/actors/webconsole/commands", true); loader.lazyRequireGetter(this, "isCommand", "devtools/server/actors/webconsole/commands", true); +loader.lazyRequireGetter(this, "validCommands", "devtools/server/actors/webconsole/commands", true); loader.lazyRequireGetter(this, "CONSOLE_WORKER_IDS", "devtools/server/actors/webconsole/utils", true); loader.lazyRequireGetter(this, "WebConsoleUtils", "devtools/server/actors/webconsole/utils", true); loader.lazyRequireGetter(this, "EnvironmentActor", "devtools/server/actors/environment", true); @@ -1085,54 +1086,57 @@ WebConsoleActor.prototype = let dbgObject = null; let environment = null; let hadDebuggee = false; - - // This is the case of the paused debugger - if (frameActorId) { - const frameActor = this.conn.getActor(frameActorId); - try { - // Need to try/catch since accessing frame.environment - // can throw "Debugger.Frame is not live" - const frame = frameActor.frame; - environment = frame.environment; - } catch (e) { - DevToolsUtils.reportException("autocomplete", - Error("The frame actor was not found: " + frameActorId)); - } - } else { - // This is the general case (non-paused debugger) - hadDebuggee = this.dbg.hasDebuggee(this.evalWindow); - dbgObject = this.dbg.addDebuggee(this.evalWindow); - } - - const result = JSPropertyProvider(dbgObject, environment, request.text, - request.cursor, frameActorId) || {}; - - if (!hadDebuggee && dbgObject) { - this.dbg.removeDebuggee(this.evalWindow); - } - - let matches = result.matches || []; + let matches = []; + let matchProp; const reqText = request.text.substr(0, request.cursor); - // We consider '$' as alphanumerc because it is used in the names of some - // helper functions. - const lastNonAlphaIsDot = /[.][a-zA-Z0-9$]*$/.test(reqText); - if (!lastNonAlphaIsDot) { - if (!this._webConsoleCommandsCache) { - const helpers = { - sandbox: Object.create(null) - }; - addWebConsoleCommands(helpers); - this._webConsoleCommandsCache = - Object.getOwnPropertyNames(helpers.sandbox); + if (isCommand(reqText)) { + const commandsCache = this._getWebConsoleCommandsCache(); + matchProp = reqText; + matches = validCommands + .filter(c => `:${c}`.startsWith(reqText) + && commandsCache.find(n => `:${n}`.startsWith(reqText)) + ) + .map(c => `:${c}`); + } else { + // This is the case of the paused debugger + if (frameActorId) { + const frameActor = this.conn.getActor(frameActorId); + try { + // Need to try/catch since accessing frame.environment + // can throw "Debugger.Frame is not live" + const frame = frameActor.frame; + environment = frame.environment; + } catch (e) { + DevToolsUtils.reportException("autocomplete", + Error("The frame actor was not found: " + frameActorId)); + } + } else { + // This is the general case (non-paused debugger) + hadDebuggee = this.dbg.hasDebuggee(this.evalWindow); + dbgObject = this.dbg.addDebuggee(this.evalWindow); } - matches = matches.concat(this._webConsoleCommandsCache - .filter(n => - // filter out `screenshot` command as it is inaccessible without - // the `:` prefix - n !== "screenshot" && n.startsWith(result.matchProp) - )); + const result = JSPropertyProvider(dbgObject, environment, request.text, + request.cursor, frameActorId) || {}; + + if (!hadDebuggee && dbgObject) { + this.dbg.removeDebuggee(this.evalWindow); + } + + matches = result.matches || []; + matchProp = result.matchProp; + + // We consider '$' as alphanumerc because it is used in the names of some + // helper functions. + const lastNonAlphaIsDot = /[.][a-zA-Z0-9$]*$/.test(reqText); + if (!lastNonAlphaIsDot) { + matches = matches.concat(this._getWebConsoleCommandsCache().filter(n => + // filter out `screenshot` command as it is inaccessible without + // the `:` prefix + n !== "screenshot" && n.startsWith(result.matchProp) + )); + } } // Make sure we return an array with unique items, since `matches` can hold twice @@ -1143,7 +1147,7 @@ WebConsoleActor.prototype = return { from: this.actorID, matches, - matchProp: result.matchProp, + matchProp, }; }, @@ -1274,6 +1278,17 @@ WebConsoleActor.prototype = return helpers; }, + _getWebConsoleCommandsCache: function() { + if (!this._webConsoleCommandsCache) { + const helpers = { + sandbox: Object.create(null) + }; + addWebConsoleCommands(helpers); + this._webConsoleCommandsCache = Object.getOwnPropertyNames(helpers.sandbox); + } + return this._webConsoleCommandsCache; + }, + /** * Evaluates a string using the debugger API. * diff --git a/devtools/server/actors/webconsole/commands.js b/devtools/server/actors/webconsole/commands.js index 3199fc145b2b..01648614f1c5 100644 --- a/devtools/server/actors/webconsole/commands.js +++ b/devtools/server/actors/webconsole/commands.js @@ -236,3 +236,4 @@ function getTypedValue(value) { exports.formatCommand = formatCommand; exports.isCommand = isCommand; +exports.validCommands = validCommands;