Bug 1473923 - Autocomplete console commands; r=yulia.
MozReview-Commit-ID: LpbIzheFmeT
--- a/devtools/client/webconsole/components/JSTerm.js
+++ b/devtools/client/webconsole/components/JSTerm.js
@@ -1168,20 +1168,20 @@ class JSTerm extends Component {
// Check if last character is non-alphanumeric
if (!/[a-zA-Z0-9]$/.test(input) || frameActor != this._lastFrameActorId) {
this._autocompleteQuery = null;
this._autocompleteCache = null;
}
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);
}
const newList = cache.sort().filter(l => l.startsWith(filterBy));
this.lastCompletion = {
requestId: null,
--- a/devtools/client/webconsole/test/mochitest/browser.ini
+++ b/devtools/client/webconsole/test/mochitest/browser.ini
@@ -180,16 +180,17 @@ skip-if = verify
[browser_console_webconsole_ctrlw_close_tab.js]
[browser_console_webconsole_iframe_messages.js]
[browser_console_webconsole_private_browsing.js]
[browser_jsterm_accessibility.js]
[browser_jsterm_add_edited_input_to_history.js]
[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]
[browser_jsterm_autocomplete_helpers.js]
[browser_jsterm_autocomplete_in_chrome_tab.js]
[browser_jsterm_autocomplete_in_debugger_stackframe.js]
[browser_jsterm_autocomplete_inside_text.js]
[browser_jsterm_autocomplete_native_getters.js]
new file mode 100644
--- /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);
+}
--- a/devtools/server/actors/webconsole.js
+++ b/devtools/server/actors/webconsole.js
@@ -26,16 +26,17 @@ loader.lazyRequireGetter(this, "ConsoleP
loader.lazyRequireGetter(this, "StackTraceCollector", "devtools/shared/webconsole/network-monitor", true);
loader.lazyRequireGetter(this, "JSPropertyProvider", "devtools/shared/webconsole/js-property-provider", true);
loader.lazyRequireGetter(this, "Parser", "resource://devtools/shared/Parser.jsm", true);
loader.lazyRequireGetter(this, "NetUtil", "resource://gre/modules/NetUtil.jsm", true);
loader.lazyRequireGetter(this, "WebConsoleCommands", "devtools/server/actors/webconsole/utils", true);
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);
loader.lazyRequireGetter(this, "EventEmitter", "devtools/shared/event-emitter");
// Overwrite implemented listeners for workers so that we don't attempt
// to load an unsupported module.
if (isWorker) {
@@ -1080,75 +1081,78 @@ WebConsoleActor.prototype =
* @return object
* The response message - matched properties.
*/
autocomplete: function(request) {
const frameActorId = request.frameActor;
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
// the same function name if it was defined in the content page and match an helper
// function (e.g. $, keys, …).
matches = [...new Set(matches)].sort();
return {
from: this.actorID,
matches,
- matchProp: result.matchProp,
+ matchProp,
};
},
/**
* The "clearMessagesCache" request handler.
*/
clearMessagesCache: function() {
// TODO: Bug 717611 - Web Console clear button does not clear cached errors
@@ -1269,16 +1273,27 @@ WebConsoleActor.prototype =
// Make sure the helpers can be used during eval.
desc.value = debuggerGlobal.makeDebuggeeValue(desc.value);
}
Object.defineProperty(helpers.sandbox, name, desc);
}
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.
*
* To allow the variables view to update properties from the Web Console we
* provide the "bindObjectActor" mechanism: the Web Console tells the
* ObjectActor ID for which it desires to evaluate an expression. The
* Debugger.Object pointed at by the actor ID is bound such that it is
* available during expression evaluation (executeInGlobalWithBindings()).
--- a/devtools/server/actors/webconsole/commands.js
+++ b/devtools/server/actors/webconsole/commands.js
@@ -231,8 +231,9 @@ function getTypedValue(value) {
if (isStringChar(value[0])) {
return value.slice(1, value.length - 1);
}
return value;
}
exports.formatCommand = formatCommand;
exports.isCommand = isCommand;
+exports.validCommands = validCommands;