summaryrefslogtreecommitdiffstats
path: root/devtools/client/debugger/content
diff options
context:
space:
mode:
authorMatt A. Tobin <mattatobin@localhost.localdomain>2018-02-02 04:16:08 -0500
committerMatt A. Tobin <mattatobin@localhost.localdomain>2018-02-02 04:16:08 -0500
commit5f8de423f190bbb79a62f804151bc24824fa32d8 (patch)
tree10027f336435511475e392454359edea8e25895d /devtools/client/debugger/content
parent49ee0794b5d912db1f95dce6eb52d781dc210db5 (diff)
downloadUXP-5f8de423f190bbb79a62f804151bc24824fa32d8.tar
UXP-5f8de423f190bbb79a62f804151bc24824fa32d8.tar.gz
UXP-5f8de423f190bbb79a62f804151bc24824fa32d8.tar.lz
UXP-5f8de423f190bbb79a62f804151bc24824fa32d8.tar.xz
UXP-5f8de423f190bbb79a62f804151bc24824fa32d8.zip
Add m-esr52 at 52.6.0
Diffstat (limited to 'devtools/client/debugger/content')
-rw-r--r--devtools/client/debugger/content/actions/breakpoints.js191
-rw-r--r--devtools/client/debugger/content/actions/event-listeners.js118
-rw-r--r--devtools/client/debugger/content/actions/moz.build10
-rw-r--r--devtools/client/debugger/content/actions/sources.js280
-rw-r--r--devtools/client/debugger/content/constants.js25
-rw-r--r--devtools/client/debugger/content/globalActions.js18
-rw-r--r--devtools/client/debugger/content/moz.build17
-rw-r--r--devtools/client/debugger/content/queries.js70
-rw-r--r--devtools/client/debugger/content/reducers/async-requests.js31
-rw-r--r--devtools/client/debugger/content/reducers/breakpoints.js153
-rw-r--r--devtools/client/debugger/content/reducers/event-listeners.js37
-rw-r--r--devtools/client/debugger/content/reducers/index.js16
-rw-r--r--devtools/client/debugger/content/reducers/moz.build12
-rw-r--r--devtools/client/debugger/content/reducers/sources.js128
-rw-r--r--devtools/client/debugger/content/utils.js88
-rw-r--r--devtools/client/debugger/content/views/event-listeners-view.js295
-rw-r--r--devtools/client/debugger/content/views/moz.build9
-rw-r--r--devtools/client/debugger/content/views/sources-view.js1370
18 files changed, 2868 insertions, 0 deletions
diff --git a/devtools/client/debugger/content/actions/breakpoints.js b/devtools/client/debugger/content/actions/breakpoints.js
new file mode 100644
index 000000000..5c5552d78
--- /dev/null
+++ b/devtools/client/debugger/content/actions/breakpoints.js
@@ -0,0 +1,191 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+"use strict";
+
+const constants = require("../constants");
+const promise = require("promise");
+const { asPaused } = require("../utils");
+const { PROMISE } = require("devtools/client/shared/redux/middleware/promise");
+const {
+ getSource, getBreakpoint, getBreakpoints, makeLocationId
+} = require("../queries");
+const { Task } = require("devtools/shared/task");
+
+// Because breakpoints are just simple data structures, we still need
+// a way to lookup the actual client instance to talk to the server.
+// We keep an internal database of clients based off of actor ID.
+const BREAKPOINT_CLIENT_STORE = new Map();
+
+function setBreakpointClient(actor, client) {
+ BREAKPOINT_CLIENT_STORE.set(actor, client);
+}
+
+function getBreakpointClient(actor) {
+ return BREAKPOINT_CLIENT_STORE.get(actor);
+}
+
+function enableBreakpoint(location) {
+ // Enabling is exactly the same as adding. It will use the existing
+ // breakpoint that still stored.
+ return addBreakpoint(location);
+}
+
+function _breakpointExists(state, location) {
+ const currentBp = getBreakpoint(state, location);
+ return currentBp && !currentBp.disabled;
+}
+
+function _getOrCreateBreakpoint(state, location, condition) {
+ return getBreakpoint(state, location) || { location, condition };
+}
+
+function addBreakpoint(location, condition) {
+ return (dispatch, getState) => {
+ if (_breakpointExists(getState(), location)) {
+ return;
+ }
+
+ const bp = _getOrCreateBreakpoint(getState(), location, condition);
+
+ return dispatch({
+ type: constants.ADD_BREAKPOINT,
+ breakpoint: bp,
+ condition: condition,
+ [PROMISE]: Task.spawn(function* () {
+ const sourceClient = gThreadClient.source(
+ getSource(getState(), bp.location.actor)
+ );
+ const [response, bpClient] = yield sourceClient.setBreakpoint({
+ line: bp.location.line,
+ column: bp.location.column,
+ condition: bp.condition
+ });
+ const { isPending, actualLocation } = response;
+
+ // Save the client instance
+ setBreakpointClient(bpClient.actor, bpClient);
+
+ return {
+ text: DebuggerView.editor.getText(
+ (actualLocation ? actualLocation.line : bp.location.line) - 1
+ ).trim(),
+
+ // If the breakpoint response has an "actualLocation" attached, then
+ // the original requested placement for the breakpoint wasn't
+ // accepted.
+ actualLocation: isPending ? null : actualLocation,
+ actor: bpClient.actor
+ };
+ })
+ });
+ };
+}
+
+function disableBreakpoint(location) {
+ return _removeOrDisableBreakpoint(location, true);
+}
+
+function removeBreakpoint(location) {
+ return _removeOrDisableBreakpoint(location);
+}
+
+function _removeOrDisableBreakpoint(location, isDisabled) {
+ return (dispatch, getState) => {
+ let bp = getBreakpoint(getState(), location);
+ if (!bp) {
+ throw new Error("attempt to remove breakpoint that does not exist");
+ }
+ if (bp.loading) {
+ // TODO(jwl): make this wait until the breakpoint is saved if it
+ // is still loading
+ throw new Error("attempt to remove unsaved breakpoint");
+ }
+
+ const bpClient = getBreakpointClient(bp.actor);
+ const action = {
+ type: constants.REMOVE_BREAKPOINT,
+ breakpoint: bp,
+ disabled: isDisabled
+ };
+
+ // If the breakpoint is already disabled, we don't need to remove
+ // it from the server. We just need to dispatch an action
+ // simulating a successful server request to remove it, and it
+ // will be removed completely from the state.
+ if (!bp.disabled) {
+ return dispatch(Object.assign({}, action, {
+ [PROMISE]: bpClient.remove()
+ }));
+ } else {
+ return dispatch(Object.assign({}, action, { status: "done" }));
+ }
+ };
+}
+
+function removeAllBreakpoints() {
+ return (dispatch, getState) => {
+ const breakpoints = getBreakpoints(getState());
+ const activeBreakpoints = breakpoints.filter(bp => !bp.disabled);
+ activeBreakpoints.forEach(bp => removeBreakpoint(bp.location));
+ };
+}
+
+/**
+ * Update the condition of a breakpoint.
+ *
+ * @param object aLocation
+ * @see DebuggerController.Breakpoints.addBreakpoint
+ * @param string aClients
+ * The condition to set on the breakpoint
+ * @return object
+ * A promise that will be resolved with the breakpoint client
+ */
+function setBreakpointCondition(location, condition) {
+ return (dispatch, getState) => {
+ const bp = getBreakpoint(getState(), location);
+ if (!bp) {
+ throw new Error("Breakpoint does not exist at the specified location");
+ }
+ if (bp.loading) {
+ // TODO(jwl): when this function is called, make sure the action
+ // creator waits for the breakpoint to exist
+ throw new Error("breakpoint must be saved");
+ }
+
+ const bpClient = getBreakpointClient(bp.actor);
+ const action = {
+ type: constants.SET_BREAKPOINT_CONDITION,
+ breakpoint: bp,
+ condition: condition
+ };
+
+ // If it's not disabled, we need to update the condition on the
+ // server. Otherwise, just dispatch a non-remote action that
+ // updates the condition locally.
+ if (!bp.disabled) {
+ return dispatch(Object.assign({}, action, {
+ [PROMISE]: Task.spawn(function* () {
+ const newClient = yield bpClient.setCondition(gThreadClient, condition);
+
+ // Remove the old instance and save the new one
+ setBreakpointClient(bpClient.actor, null);
+ setBreakpointClient(newClient.actor, newClient);
+
+ return { actor: newClient.actor };
+ })
+ }));
+ } else {
+ return dispatch(action);
+ }
+ };
+}
+
+module.exports = {
+ enableBreakpoint,
+ addBreakpoint,
+ disableBreakpoint,
+ removeBreakpoint,
+ removeAllBreakpoints,
+ setBreakpointCondition
+};
diff --git a/devtools/client/debugger/content/actions/event-listeners.js b/devtools/client/debugger/content/actions/event-listeners.js
new file mode 100644
index 000000000..4bca557fe
--- /dev/null
+++ b/devtools/client/debugger/content/actions/event-listeners.js
@@ -0,0 +1,118 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+"use strict";
+
+const constants = require("../constants");
+const { asPaused } = require("../utils");
+const { reportException } = require("devtools/shared/DevToolsUtils");
+const { setNamedTimeout } = require("devtools/client/shared/widgets/view-helpers");
+const { Task } = require("devtools/shared/task");
+
+const FETCH_EVENT_LISTENERS_DELAY = 200; // ms
+
+function fetchEventListeners() {
+ return (dispatch, getState) => {
+ // Make sure we"re not sending a batch of closely repeated requests.
+ // This can easily happen whenever new sources are fetched.
+ setNamedTimeout("event-listeners-fetch", FETCH_EVENT_LISTENERS_DELAY, () => {
+ // In case there is still a request of listeners going on (it
+ // takes several RDP round trips right now), make sure we wait
+ // on a currently running request
+ if (getState().eventListeners.fetchingListeners) {
+ dispatch({
+ type: services.WAIT_UNTIL,
+ predicate: action => (
+ action.type === constants.FETCH_EVENT_LISTENERS &&
+ action.status === "done"
+ ),
+ run: dispatch => dispatch(fetchEventListeners())
+ });
+ return;
+ }
+
+ dispatch({
+ type: constants.FETCH_EVENT_LISTENERS,
+ status: "begin"
+ });
+
+ asPaused(gThreadClient, _getListeners).then(listeners => {
+ // Notify that event listeners were fetched and shown in the view,
+ // and callback to resume the active thread if necessary.
+ window.emit(EVENTS.EVENT_LISTENERS_FETCHED);
+
+ dispatch({
+ type: constants.FETCH_EVENT_LISTENERS,
+ status: "done",
+ listeners: listeners
+ });
+ });
+ });
+ };
+}
+
+const _getListeners = Task.async(function* () {
+ const response = yield gThreadClient.eventListeners();
+
+ // Make sure all the listeners are sorted by the event type, since
+ // they"re not guaranteed to be clustered together.
+ response.listeners.sort((a, b) => a.type > b.type ? 1 : -1);
+
+ // Add all the listeners in the debugger view event linsteners container.
+ let fetchedDefinitions = new Map();
+ let listeners = [];
+ for (let listener of response.listeners) {
+ let definitionSite;
+ if (fetchedDefinitions.has(listener.function.actor)) {
+ definitionSite = fetchedDefinitions.get(listener.function.actor);
+ } else if (listener.function.class == "Function") {
+ definitionSite = yield _getDefinitionSite(listener.function);
+ if (!definitionSite) {
+ // We don"t know where this listener comes from so don"t show it in
+ // the UI as breaking on it doesn"t work (bug 942899).
+ continue;
+ }
+
+ fetchedDefinitions.set(listener.function.actor, definitionSite);
+ }
+ listener.function.url = definitionSite;
+ listeners.push(listener);
+ }
+ fetchedDefinitions.clear();
+
+ return listeners;
+});
+
+const _getDefinitionSite = Task.async(function* (aFunction) {
+ const grip = gThreadClient.pauseGrip(aFunction);
+ let response;
+
+ try {
+ response = yield grip.getDefinitionSite();
+ }
+ catch (e) {
+ // Don't make this error fatal, because it would break the entire events pane.
+ reportException("_getDefinitionSite", e);
+ return null;
+ }
+
+ return response.source.url;
+});
+
+function updateEventBreakpoints(eventNames) {
+ return dispatch => {
+ setNamedTimeout("event-breakpoints-update", 0, () => {
+ gThreadClient.pauseOnDOMEvents(eventNames, function () {
+ // Notify that event breakpoints were added/removed on the server.
+ window.emit(EVENTS.EVENT_BREAKPOINTS_UPDATED);
+
+ dispatch({
+ type: constants.UPDATE_EVENT_BREAKPOINTS,
+ eventNames: eventNames
+ });
+ });
+ });
+ };
+}
+
+module.exports = { updateEventBreakpoints, fetchEventListeners };
diff --git a/devtools/client/debugger/content/actions/moz.build b/devtools/client/debugger/content/actions/moz.build
new file mode 100644
index 000000000..13a2dd9ad
--- /dev/null
+++ b/devtools/client/debugger/content/actions/moz.build
@@ -0,0 +1,10 @@
+# vim: set filetype=python:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+DevToolsModules(
+ 'breakpoints.js',
+ 'event-listeners.js',
+ 'sources.js'
+)
diff --git a/devtools/client/debugger/content/actions/sources.js b/devtools/client/debugger/content/actions/sources.js
new file mode 100644
index 000000000..d7e0728e7
--- /dev/null
+++ b/devtools/client/debugger/content/actions/sources.js
@@ -0,0 +1,280 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+"use strict";
+
+const constants = require("../constants");
+const promise = require("promise");
+const Services = require("Services");
+const { dumpn } = require("devtools/shared/DevToolsUtils");
+const { PROMISE, HISTOGRAM_ID } = require("devtools/client/shared/redux/middleware/promise");
+const { getSource, getSourceText } = require("../queries");
+const { Task } = require("devtools/shared/task");
+
+const NEW_SOURCE_IGNORED_URLS = ["debugger eval code", "XStringBundle"];
+const FETCH_SOURCE_RESPONSE_DELAY = 200; // ms
+
+function getSourceClient(source) {
+ return gThreadClient.source(source);
+}
+
+/**
+ * Handler for the debugger client's unsolicited newSource notification.
+ */
+function newSource(source) {
+ return dispatch => {
+ // Ignore bogus scripts, e.g. generated from 'clientEvaluate' packets.
+ if (NEW_SOURCE_IGNORED_URLS.indexOf(source.url) != -1) {
+ return;
+ }
+
+ // Signal that a new source has been added.
+ window.emit(EVENTS.NEW_SOURCE);
+
+ return dispatch({
+ type: constants.ADD_SOURCE,
+ source: source
+ });
+ };
+}
+
+function selectSource(source, opts) {
+ return (dispatch, getState) => {
+ if (!gThreadClient) {
+ // No connection, do nothing. This happens when the debugger is
+ // shut down too fast and it tries to display a default source.
+ return;
+ }
+
+ source = getSource(getState(), source.actor);
+
+ // Make sure to start a request to load the source text.
+ dispatch(loadSourceText(source));
+
+ dispatch({
+ type: constants.SELECT_SOURCE,
+ source: source,
+ opts: opts
+ });
+ };
+}
+
+function loadSources() {
+ return {
+ type: constants.LOAD_SOURCES,
+ [PROMISE]: Task.spawn(function* () {
+ const response = yield gThreadClient.getSources();
+
+ // Top-level breakpoints may pause the entire loading process
+ // because scripts are executed as they are loaded, so the
+ // engine may pause in the middle of loading all the sources.
+ // This is relatively harmless, as individual `newSource`
+ // notifications are fired for each script and they will be
+ // added to the UI through that.
+ if (!response.sources) {
+ dumpn(
+ "Error getting sources, probably because a top-level " +
+ "breakpoint was hit while executing them"
+ );
+ return;
+ }
+
+ // Ignore bogus scripts, e.g. generated from 'clientEvaluate' packets.
+ return response.sources.filter(source => {
+ return NEW_SOURCE_IGNORED_URLS.indexOf(source.url) === -1;
+ });
+ })
+ };
+}
+
+/**
+ * Set the black boxed status of the given source.
+ *
+ * @param Object aSource
+ * The source form.
+ * @param bool aBlackBoxFlag
+ * True to black box the source, false to un-black box it.
+ * @returns Promise
+ * A promize that resolves to [aSource, isBlackBoxed] or rejects to
+ * [aSource, error].
+ */
+function blackbox(source, shouldBlackBox) {
+ const client = getSourceClient(source);
+
+ return {
+ type: constants.BLACKBOX,
+ source: source,
+ [PROMISE]: Task.spawn(function* () {
+ yield shouldBlackBox ? client.blackBox() : client.unblackBox();
+ return {
+ isBlackBoxed: shouldBlackBox
+ };
+ })
+ };
+}
+
+/**
+ * Toggle the pretty printing of a source's text. All subsequent calls to
+ * |getText| will return the pretty-toggled text. Nothing will happen for
+ * non-javascript files.
+ *
+ * @param Object aSource
+ * The source form from the RDP.
+ * @returns Promise
+ * A promise that resolves to [aSource, prettyText] or rejects to
+ * [aSource, error].
+ */
+function togglePrettyPrint(source) {
+ return (dispatch, getState) => {
+ const sourceClient = getSourceClient(source);
+ const wantPretty = !source.isPrettyPrinted;
+
+ return dispatch({
+ type: constants.TOGGLE_PRETTY_PRINT,
+ source: source,
+ [PROMISE]: Task.spawn(function* () {
+ let response;
+
+ // Only attempt to pretty print JavaScript sources.
+ const sourceText = getSourceText(getState(), source.actor);
+ const contentType = sourceText ? sourceText.contentType : null;
+ if (!SourceUtils.isJavaScript(source.url, contentType)) {
+ throw new Error("Can't prettify non-javascript files.");
+ }
+
+ if (wantPretty) {
+ response = yield sourceClient.prettyPrint(Prefs.editorTabSize);
+ }
+ else {
+ response = yield sourceClient.disablePrettyPrint();
+ }
+
+ // Remove the cached source AST from the Parser, to avoid getting
+ // wrong locations when searching for functions.
+ DebuggerController.Parser.clearSource(source.url);
+
+ return {
+ isPrettyPrinted: wantPretty,
+ text: response.source,
+ contentType: response.contentType
+ };
+ })
+ });
+ };
+}
+
+function loadSourceText(source) {
+ return (dispatch, getState) => {
+ // Fetch the source text only once.
+ let textInfo = getSourceText(getState(), source.actor);
+ if (textInfo) {
+ // It's already loaded or is loading
+ return promise.resolve(textInfo);
+ }
+
+ const sourceClient = getSourceClient(source);
+
+ return dispatch({
+ type: constants.LOAD_SOURCE_TEXT,
+ source: source,
+ [PROMISE]: Task.spawn(function* () {
+ let transportType = gClient.localTransport ? "_LOCAL" : "_REMOTE";
+ let histogramId = "DEVTOOLS_DEBUGGER_DISPLAY_SOURCE" + transportType + "_MS";
+ let histogram = Services.telemetry.getHistogramById(histogramId);
+ let startTime = Date.now();
+
+ const response = yield sourceClient.source();
+
+ histogram.add(Date.now() - startTime);
+
+ // Automatically pretty print if enabled and the test is
+ // detected to be "minified"
+ if (Prefs.autoPrettyPrint &&
+ !source.isPrettyPrinted &&
+ SourceUtils.isMinified(source.actor, response.source)) {
+ dispatch(togglePrettyPrint(source));
+ }
+
+ return { text: response.source,
+ contentType: response.contentType };
+ })
+ });
+ };
+}
+
+/**
+ * Starts fetching all the sources, silently.
+ *
+ * @param array aUrls
+ * The urls for the sources to fetch. If fetching a source's text
+ * takes too long, it will be discarded.
+ * @return object
+ * A promise that is resolved after source texts have been fetched.
+ */
+function getTextForSources(actors) {
+ return (dispatch, getState) => {
+ let deferred = promise.defer();
+ let pending = new Set(actors);
+ let fetched = [];
+
+ // Can't use promise.all, because if one fetch operation is rejected, then
+ // everything is considered rejected, thus no other subsequent source will
+ // be getting fetched. We don't want that. Something like Q's allSettled
+ // would work like a charm here.
+
+ // Try to fetch as many sources as possible.
+ for (let actor of actors) {
+ let source = getSource(getState(), actor);
+ dispatch(loadSourceText(source)).then(({ text, contentType }) => {
+ onFetch([source, text, contentType]);
+ }, err => {
+ onError(source, err);
+ });
+ }
+
+ setTimeout(onTimeout, FETCH_SOURCE_RESPONSE_DELAY);
+
+ /* Called if fetching a source takes too long. */
+ function onTimeout() {
+ pending = new Set();
+ maybeFinish();
+ }
+
+ /* Called if fetching a source finishes successfully. */
+ function onFetch([aSource, aText, aContentType]) {
+ // If fetching the source has previously timed out, discard it this time.
+ if (!pending.has(aSource.actor)) {
+ return;
+ }
+ pending.delete(aSource.actor);
+ fetched.push([aSource.actor, aText, aContentType]);
+ maybeFinish();
+ }
+
+ /* Called if fetching a source failed because of an error. */
+ function onError([aSource, aError]) {
+ pending.delete(aSource.actor);
+ maybeFinish();
+ }
+
+ /* Called every time something interesting happens while fetching sources. */
+ function maybeFinish() {
+ if (pending.size == 0) {
+ // Sort the fetched sources alphabetically by their url.
+ deferred.resolve(fetched.sort(([aFirst], [aSecond]) => aFirst > aSecond));
+ }
+ }
+
+ return deferred.promise;
+ };
+}
+
+module.exports = {
+ newSource,
+ selectSource,
+ loadSources,
+ blackbox,
+ togglePrettyPrint,
+ loadSourceText,
+ getTextForSources
+};
diff --git a/devtools/client/debugger/content/constants.js b/devtools/client/debugger/content/constants.js
new file mode 100644
index 000000000..0099477b7
--- /dev/null
+++ b/devtools/client/debugger/content/constants.js
@@ -0,0 +1,25 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+"use strict";
+
+exports.UPDATE_EVENT_BREAKPOINTS = "UPDATE_EVENT_BREAKPOINTS";
+exports.FETCH_EVENT_LISTENERS = "FETCH_EVENT_LISTENERS";
+
+exports.TOGGLE_PRETTY_PRINT = "TOGGLE_PRETTY_PRINT";
+exports.BLACKBOX = "BLACKBOX";
+
+exports.ADD_BREAKPOINT = "ADD_BREAKPOINT";
+exports.REMOVE_BREAKPOINT = "REMOVE_BREAKPOINT";
+exports.ENABLE_BREAKPOINT = "ENABLE_BREAKPOINT";
+exports.DISABLE_BREAKPOINT = "DISABLE_BREAKPOINT";
+exports.SET_BREAKPOINT_CONDITION = "SET_BREAKPOINT_CONDITION";
+
+exports.ADD_SOURCE = "ADD_SOURCE";
+exports.LOAD_SOURCES = "LOAD_SOURCES";
+exports.LOAD_SOURCE_TEXT = "LOAD_SOURCE_TEXT";
+exports.SELECT_SOURCE = "SELECT_SOURCE";
+exports.UNLOAD = "UNLOAD";
+exports.RELOAD = "RELOAD";
diff --git a/devtools/client/debugger/content/globalActions.js b/devtools/client/debugger/content/globalActions.js
new file mode 100644
index 000000000..3f02be36e
--- /dev/null
+++ b/devtools/client/debugger/content/globalActions.js
@@ -0,0 +1,18 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+"use strict";
+
+const constants = require("./constants");
+
+// Fired when the page is being unloaded, for example when it's being
+// navigated away from.
+function unload() {
+ return {
+ type: constants.UNLOAD
+ };
+}
+
+module.exports = { unload };
diff --git a/devtools/client/debugger/content/moz.build b/devtools/client/debugger/content/moz.build
new file mode 100644
index 000000000..fcca58e65
--- /dev/null
+++ b/devtools/client/debugger/content/moz.build
@@ -0,0 +1,17 @@
+# vim: set filetype=python:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+DIRS += [
+ 'actions',
+ 'reducers',
+ 'views',
+]
+
+DevToolsModules(
+ 'constants.js',
+ 'globalActions.js',
+ 'queries.js',
+ 'utils.js'
+)
diff --git a/devtools/client/debugger/content/queries.js b/devtools/client/debugger/content/queries.js
new file mode 100644
index 000000000..3a0c54b88
--- /dev/null
+++ b/devtools/client/debugger/content/queries.js
@@ -0,0 +1,70 @@
+
+function getSource(state, actor) {
+ return state.sources.sources[actor];
+}
+
+function getSources(state) {
+ return state.sources.sources;
+}
+
+function getSourceCount(state) {
+ return Object.keys(state.sources.sources).length;
+}
+
+function getSourceByURL(state, url) {
+ for (let k in state.sources.sources) {
+ const source = state.sources.sources[k];
+ if (source.url === url) {
+ return source;
+ }
+ }
+}
+
+function getSourceByActor(state, actor) {
+ for (let k in state.sources.sources) {
+ const source = state.sources.sources[k];
+ if (source.actor === actor) {
+ return source;
+ }
+ }
+}
+
+function getSelectedSource(state) {
+ return state.sources.sources[state.sources.selectedSource];
+}
+
+function getSelectedSourceOpts(state) {
+ return state.sources.selectedSourceOpts;
+}
+
+function getSourceText(state, actor) {
+ return state.sources.sourcesText[actor];
+}
+
+function getBreakpoints(state) {
+ return Object.keys(state.breakpoints.breakpoints).map(k => {
+ return state.breakpoints.breakpoints[k];
+ });
+}
+
+function getBreakpoint(state, location) {
+ return state.breakpoints.breakpoints[makeLocationId(location)];
+}
+
+function makeLocationId(location) {
+ return location.actor + ":" + location.line.toString();
+}
+
+module.exports = {
+ getSource,
+ getSources,
+ getSourceCount,
+ getSourceByURL,
+ getSourceByActor,
+ getSelectedSource,
+ getSelectedSourceOpts,
+ getSourceText,
+ getBreakpoint,
+ getBreakpoints,
+ makeLocationId
+};
diff --git a/devtools/client/debugger/content/reducers/async-requests.js b/devtools/client/debugger/content/reducers/async-requests.js
new file mode 100644
index 000000000..206e1cf60
--- /dev/null
+++ b/devtools/client/debugger/content/reducers/async-requests.js
@@ -0,0 +1,31 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+"use strict";
+
+const constants = require("../constants");
+const initialState = [];
+
+function update(state = initialState, action, emitChange) {
+ const { seqId } = action;
+
+ if (action.type === constants.UNLOAD) {
+ return initialState;
+ }
+ else if (seqId) {
+ let newState;
+ if (action.status === "start") {
+ newState = [...state, seqId];
+ }
+ else if (action.status === "error" || action.status === "done") {
+ newState = state.filter(id => id !== seqId);
+ }
+
+ emitChange("open-requests", newState);
+ return newState;
+ }
+
+ return state;
+}
+
+module.exports = update;
diff --git a/devtools/client/debugger/content/reducers/breakpoints.js b/devtools/client/debugger/content/reducers/breakpoints.js
new file mode 100644
index 000000000..7e42098e8
--- /dev/null
+++ b/devtools/client/debugger/content/reducers/breakpoints.js
@@ -0,0 +1,153 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+"use strict";
+
+const constants = require("../constants");
+const Immutable = require("devtools/client/shared/vendor/seamless-immutable");
+const { mergeIn, setIn, deleteIn } = require("../utils");
+const { makeLocationId } = require("../queries");
+
+const initialState = Immutable({
+ breakpoints: {}
+});
+
+// Return the first argument that is a string, or null if nothing is a
+// string.
+function firstString(...args) {
+ for (var arg of args) {
+ if (typeof arg === "string") {
+ return arg;
+ }
+ }
+ return null;
+}
+
+function update(state = initialState, action, emitChange) {
+ switch (action.type) {
+ case constants.ADD_BREAKPOINT: {
+ const id = makeLocationId(action.breakpoint.location);
+
+ if (action.status === "start") {
+ const existingBp = state.breakpoints[id];
+ const bp = existingBp || Immutable(action.breakpoint);
+
+ state = setIn(state, ["breakpoints", id], bp.merge({
+ disabled: false,
+ loading: true,
+ // We want to do an OR here, but we can't because we need
+ // empty strings to be truthy, i.e. an empty string is a valid
+ // condition.
+ condition: firstString(action.condition, bp.condition)
+ }));
+
+ emitChange(existingBp ? "breakpoint-enabled" : "breakpoint-added",
+ state.breakpoints[id]);
+ return state;
+ }
+ else if (action.status === "done") {
+ const { actor, text } = action.value;
+ let { actualLocation } = action.value;
+
+ // If the breakpoint moved, update the map
+ if (actualLocation) {
+ // XXX Bug 1227417: The `setBreakpoint` RDP request rdp
+ // request returns an `actualLocation` field that doesn't
+ // conform to the regular { actor, line } location shape, but
+ // it has a `source` field. We should fix that.
+ actualLocation = { actor: actualLocation.source.actor,
+ line: actualLocation.line };
+
+ state = deleteIn(state, ["breakpoints", id]);
+
+ const movedId = makeLocationId(actualLocation);
+ const currentBp = state.breakpoints[movedId] || Immutable(action.breakpoint);
+ const prevLocation = action.breakpoint.location;
+ const newBp = currentBp.merge({ location: actualLocation });
+ state = setIn(state, ["breakpoints", movedId], newBp);
+
+ emitChange("breakpoint-moved", {
+ breakpoint: newBp,
+ prevLocation: prevLocation
+ });
+ }
+
+ const finalLocation = (
+ actualLocation ? actualLocation : action.breakpoint.location
+ );
+ const finalLocationId = makeLocationId(finalLocation);
+ state = mergeIn(state, ["breakpoints", finalLocationId], {
+ disabled: false,
+ loading: false,
+ actor: actor,
+ text: text
+ });
+ emitChange("breakpoint-updated", state.breakpoints[finalLocationId]);
+ return state;
+ }
+ else if (action.status === "error") {
+ // Remove the optimistic update
+ emitChange("breakpoint-removed", state.breakpoints[id]);
+ return deleteIn(state, ["breakpoints", id]);
+ }
+ break;
+ }
+
+ case constants.REMOVE_BREAKPOINT: {
+ if (action.status === "done") {
+ const id = makeLocationId(action.breakpoint.location);
+ const bp = state.breakpoints[id];
+
+ if (action.disabled) {
+ state = mergeIn(state, ["breakpoints", id],
+ { loading: false, disabled: true });
+ emitChange("breakpoint-disabled", state.breakpoints[id]);
+ return state;
+ }
+
+ state = deleteIn(state, ["breakpoints", id]);
+ emitChange("breakpoint-removed", bp);
+ return state;
+ }
+ break;
+ }
+
+ case constants.SET_BREAKPOINT_CONDITION: {
+ const id = makeLocationId(action.breakpoint.location);
+ const bp = state.breakpoints[id];
+ emitChange("breakpoint-condition-updated", bp);
+
+ if (!action.status) {
+ // No status means that it wasn't a remote request. Just update
+ // the condition locally.
+ return mergeIn(state, ["breakpoints", id], {
+ condition: action.condition
+ });
+ }
+ else if (action.status === "start") {
+ return mergeIn(state, ["breakpoints", id], {
+ loading: true,
+ condition: action.condition
+ });
+ }
+ else if (action.status === "done") {
+ return mergeIn(state, ["breakpoints", id], {
+ loading: false,
+ condition: action.condition,
+ // Setting a condition creates a new breakpoint client as of
+ // now, so we need to update the actor
+ actor: action.value.actor
+ });
+ }
+ else if (action.status === "error") {
+ emitChange("breakpoint-removed", bp);
+ return deleteIn(state, ["breakpoints", id]);
+ }
+
+ break;
+ }}
+
+ return state;
+}
+
+module.exports = update;
diff --git a/devtools/client/debugger/content/reducers/event-listeners.js b/devtools/client/debugger/content/reducers/event-listeners.js
new file mode 100644
index 000000000..fdd3da99d
--- /dev/null
+++ b/devtools/client/debugger/content/reducers/event-listeners.js
@@ -0,0 +1,37 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+"use strict";
+
+const constants = require("../constants");
+
+const FETCH_EVENT_LISTENERS_DELAY = 200; // ms
+
+const initialState = {
+ activeEventNames: [],
+ listeners: [],
+ fetchingListeners: false,
+};
+
+function update(state = initialState, action, emit) {
+ switch (action.type) {
+ case constants.UPDATE_EVENT_BREAKPOINTS:
+ state.activeEventNames = action.eventNames;
+ emit("activeEventNames", state.activeEventNames);
+ break;
+ case constants.FETCH_EVENT_LISTENERS:
+ if (action.status === "begin") {
+ state.fetchingListeners = true;
+ }
+ else if (action.status === "done") {
+ state.fetchingListeners = false;
+ state.listeners = action.listeners;
+ emit("event-listeners", state.listeners);
+ }
+ break;
+ }
+
+ return state;
+}
+
+module.exports = update;
diff --git a/devtools/client/debugger/content/reducers/index.js b/devtools/client/debugger/content/reducers/index.js
new file mode 100644
index 000000000..27f2059f9
--- /dev/null
+++ b/devtools/client/debugger/content/reducers/index.js
@@ -0,0 +1,16 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+"use strict";
+
+const eventListeners = require("./event-listeners");
+const sources = require("./sources");
+const breakpoints = require("./breakpoints");
+const asyncRequests = require("./async-requests");
+
+module.exports = {
+ eventListeners,
+ sources,
+ breakpoints,
+ asyncRequests
+};
diff --git a/devtools/client/debugger/content/reducers/moz.build b/devtools/client/debugger/content/reducers/moz.build
new file mode 100644
index 000000000..0433a099c
--- /dev/null
+++ b/devtools/client/debugger/content/reducers/moz.build
@@ -0,0 +1,12 @@
+# vim: set filetype=python:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+DevToolsModules(
+ 'async-requests.js',
+ 'breakpoints.js',
+ 'event-listeners.js',
+ 'index.js',
+ 'sources.js'
+)
diff --git a/devtools/client/debugger/content/reducers/sources.js b/devtools/client/debugger/content/reducers/sources.js
new file mode 100644
index 000000000..963a52fb5
--- /dev/null
+++ b/devtools/client/debugger/content/reducers/sources.js
@@ -0,0 +1,128 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+"use strict";
+
+const constants = require("../constants");
+const Immutable = require("devtools/client/shared/vendor/seamless-immutable");
+const { mergeIn, setIn } = require("../utils");
+
+const initialState = Immutable({
+ sources: {},
+ selectedSource: null,
+ selectedSourceOpts: null,
+ sourcesText: {}
+});
+
+function update(state = initialState, action, emitChange) {
+ switch (action.type) {
+ case constants.ADD_SOURCE:
+ emitChange("source", action.source);
+ return mergeIn(state, ["sources", action.source.actor], action.source);
+
+ case constants.LOAD_SOURCES:
+ if (action.status === "done") {
+ const sources = action.value;
+ if (!sources) {
+ return state;
+ }
+ const sourcesByActor = {};
+ sources.forEach(source => {
+ if (!state.sources[source.actor]) {
+ emitChange("source", source);
+ }
+ sourcesByActor[source.actor] = source;
+ });
+ return mergeIn(state, ["sources"], state.sources.merge(sourcesByActor));
+ }
+ break;
+
+ case constants.SELECT_SOURCE:
+ emitChange("source-selected", action.source);
+ return state.merge({
+ selectedSource: action.source.actor,
+ selectedSourceOpts: action.opts
+ });
+
+ case constants.LOAD_SOURCE_TEXT: {
+ const s = _updateText(state, action);
+ emitChange("source-text-loaded", s.sources[action.source.actor]);
+ return s;
+ }
+
+ case constants.BLACKBOX:
+ if (action.status === "done") {
+ const s = mergeIn(state,
+ ["sources", action.source.actor, "isBlackBoxed"],
+ action.value.isBlackBoxed);
+ emitChange("blackboxed", s.sources[action.source.actor]);
+ return s;
+ }
+ break;
+
+ case constants.TOGGLE_PRETTY_PRINT:
+ let s = state;
+ if (action.status === "error") {
+ s = mergeIn(state, ["sourcesText", action.source.actor], {
+ loading: false
+ });
+
+ // If it errored, just display the source as it was before, but
+ // only if there is existing text already. If auto-prettifying
+ // is on, the original text may still be coming in and we don't
+ // have it yet. If we try to set empty text we confuse the
+ // editor because it thinks it's already displaying the source's
+ // text and won't load the text when it actually comes in.
+ if (s.sourcesText[action.source.actor].text != null) {
+ emitChange("prettyprinted", s.sources[action.source.actor]);
+ }
+ }
+ else {
+ s = _updateText(state, action);
+ // Don't do this yet, the progress bar is still imperatively shown
+ // from the source view. We will fix in the next iteration.
+ // emitChange('source-text-loaded', s.sources[action.source.actor]);
+
+ if (action.status === "done") {
+ s = mergeIn(s,
+ ["sources", action.source.actor, "isPrettyPrinted"],
+ action.value.isPrettyPrinted);
+ emitChange("prettyprinted", s.sources[action.source.actor]);
+ }
+ }
+ return s;
+
+ case constants.UNLOAD:
+ // Reset the entire state to just the initial state, a blank state
+ // if you will.
+ return initialState;
+ }
+
+ return state;
+}
+
+function _updateText(state, action) {
+ const { source } = action;
+
+ if (action.status === "start") {
+ // Merge this in, don't set it. That way the previous value is
+ // still stored here, and we can retrieve it if whatever we're
+ // doing fails.
+ return mergeIn(state, ["sourcesText", source.actor], {
+ loading: true
+ });
+ }
+ else if (action.status === "error") {
+ return setIn(state, ["sourcesText", source.actor], {
+ error: action.error
+ });
+ }
+ else {
+ return setIn(state, ["sourcesText", source.actor], {
+ text: action.value.text,
+ contentType: action.value.contentType
+ });
+ }
+}
+
+module.exports = update;
diff --git a/devtools/client/debugger/content/utils.js b/devtools/client/debugger/content/utils.js
new file mode 100644
index 000000000..59993e9b4
--- /dev/null
+++ b/devtools/client/debugger/content/utils.js
@@ -0,0 +1,88 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+"use strict";
+
+const { reportException } = require("devtools/shared/DevToolsUtils");
+const { Task } = require("devtools/shared/task");
+
+function asPaused(client, func) {
+ if (client.state != "paused") {
+ return Task.spawn(function* () {
+ yield client.interrupt();
+ let result;
+
+ try {
+ result = yield func();
+ }
+ catch (e) {
+ // Try to put the debugger back in a working state by resuming
+ // it
+ yield client.resume();
+ throw e;
+ }
+
+ yield client.resume();
+ return result;
+ });
+ } else {
+ return func();
+ }
+}
+
+function handleError(err) {
+ reportException("promise", err.toString());
+}
+
+function onReducerEvents(controller, listeners, thisContext) {
+ Object.keys(listeners).forEach(name => {
+ const listener = listeners[name];
+ controller.onChange(name, payload => {
+ listener.call(thisContext, payload);
+ });
+ });
+}
+
+function _getIn(destObj, path) {
+ return path.reduce(function (acc, name) {
+ return acc[name];
+ }, destObj);
+}
+
+function mergeIn(destObj, path, value) {
+ path = [...path];
+ path.reverse();
+ var obj = path.reduce(function (acc, name) {
+ return { [name]: acc };
+ }, value);
+
+ return destObj.merge(obj, { deep: true });
+}
+
+function setIn(destObj, path, value) {
+ destObj = mergeIn(destObj, path, null);
+ return mergeIn(destObj, path, value);
+}
+
+function updateIn(destObj, path, fn) {
+ return setIn(destObj, path, fn(_getIn(destObj, path)));
+}
+
+function deleteIn(destObj, path) {
+ const objPath = path.slice(0, -1);
+ const propName = path[path.length - 1];
+ const obj = _getIn(destObj, objPath);
+ return setIn(destObj, objPath, obj.without(propName));
+}
+
+module.exports = {
+ asPaused,
+ handleError,
+ onReducerEvents,
+ mergeIn,
+ setIn,
+ updateIn,
+ deleteIn
+};
diff --git a/devtools/client/debugger/content/views/event-listeners-view.js b/devtools/client/debugger/content/views/event-listeners-view.js
new file mode 100644
index 000000000..993d6506e
--- /dev/null
+++ b/devtools/client/debugger/content/views/event-listeners-view.js
@@ -0,0 +1,295 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+/* import-globals-from ../../debugger-controller.js */
+
+const actions = require("../actions/event-listeners");
+const { bindActionCreators } = require("devtools/client/shared/vendor/redux");
+const { Heritage, WidgetMethods } = require("devtools/client/shared/widgets/view-helpers");
+const { SideMenuWidget } = require("resource://devtools/client/shared/widgets/SideMenuWidget.jsm");
+
+/**
+ * Functions handling the event listeners UI.
+ */
+function EventListenersView(controller) {
+ dumpn("EventListenersView was instantiated");
+
+ this.actions = bindActionCreators(actions, controller.dispatch);
+ this.getState = () => controller.getState().eventListeners;
+
+ this._onCheck = this._onCheck.bind(this);
+ this._onClick = this._onClick.bind(this);
+
+ controller.onChange("event-listeners", this.renderListeners.bind(this));
+}
+
+EventListenersView.prototype = Heritage.extend(WidgetMethods, {
+ /**
+ * Initialization function, called when the debugger is started.
+ */
+ initialize: function () {
+ dumpn("Initializing the EventListenersView");
+
+ this.widget = new SideMenuWidget(document.getElementById("event-listeners"), {
+ showItemCheckboxes: true,
+ showGroupCheckboxes: true
+ });
+
+ this.emptyText = L10N.getStr("noEventListenersText");
+ this._eventCheckboxTooltip = L10N.getStr("eventCheckboxTooltip");
+ this._onSelectorString = " " + L10N.getStr("eventOnSelector") + " ";
+ this._inSourceString = " " + L10N.getStr("eventInSource") + " ";
+ this._inNativeCodeString = L10N.getStr("eventNative");
+
+ this.widget.addEventListener("check", this._onCheck, false);
+ this.widget.addEventListener("click", this._onClick, false);
+ },
+
+ /**
+ * Destruction function, called when the debugger is closed.
+ */
+ destroy: function () {
+ dumpn("Destroying the EventListenersView");
+
+ this.widget.removeEventListener("check", this._onCheck, false);
+ this.widget.removeEventListener("click", this._onClick, false);
+ },
+
+ renderListeners: function (listeners) {
+ listeners.forEach(listener => {
+ this.addListener(listener, { staged: true });
+ });
+
+ // Flushes all the prepared events into the event listeners container.
+ this.commit();
+ },
+
+ /**
+ * Adds an event to this event listeners container.
+ *
+ * @param object aListener
+ * The listener object coming from the active thread.
+ * @param object aOptions [optional]
+ * Additional options for adding the source. Supported options:
+ * - staged: true to stage the item to be appended later
+ */
+ addListener: function (aListener, aOptions = {}) {
+ let { node: { selector }, function: { url }, type } = aListener;
+ if (!type) return;
+
+ // Some listener objects may be added from plugins, thus getting
+ // translated to native code.
+ if (!url) {
+ url = this._inNativeCodeString;
+ }
+
+ // If an event item for this listener's url and type was already added,
+ // avoid polluting the view and simply increase the "targets" count.
+ let eventItem = this.getItemForPredicate(aItem =>
+ aItem.attachment.url == url &&
+ aItem.attachment.type == type);
+
+ if (eventItem) {
+ let { selectors, view: { targets } } = eventItem.attachment;
+ if (selectors.indexOf(selector) == -1) {
+ selectors.push(selector);
+ targets.setAttribute("value", L10N.getFormatStr("eventNodes", selectors.length));
+ }
+ return;
+ }
+
+ // There's no easy way of grouping event types into higher-level groups,
+ // so we need to do this by hand.
+ let is = (...args) => args.indexOf(type) != -1;
+ let has = str => type.includes(str);
+ let starts = str => type.startsWith(str);
+ let group;
+
+ if (starts("animation")) {
+ group = L10N.getStr("animationEvents");
+ } else if (starts("audio")) {
+ group = L10N.getStr("audioEvents");
+ } else if (is("levelchange")) {
+ group = L10N.getStr("batteryEvents");
+ } else if (is("cut", "copy", "paste")) {
+ group = L10N.getStr("clipboardEvents");
+ } else if (starts("composition")) {
+ group = L10N.getStr("compositionEvents");
+ } else if (starts("device")) {
+ group = L10N.getStr("deviceEvents");
+ } else if (is("fullscreenchange", "fullscreenerror", "orientationchange",
+ "overflow", "resize", "scroll", "underflow", "zoom")) {
+ group = L10N.getStr("displayEvents");
+ } else if (starts("drag") || starts("drop")) {
+ group = L10N.getStr("dragAndDropEvents");
+ } else if (starts("gamepad")) {
+ group = L10N.getStr("gamepadEvents");
+ } else if (is("canplay", "canplaythrough", "durationchange", "emptied",
+ "ended", "loadeddata", "loadedmetadata", "pause", "play", "playing",
+ "ratechange", "seeked", "seeking", "stalled", "suspend", "timeupdate",
+ "volumechange", "waiting")) {
+ group = L10N.getStr("mediaEvents");
+ } else if (is("blocked", "complete", "success", "upgradeneeded", "versionchange")) {
+ group = L10N.getStr("indexedDBEvents");
+ } else if (is("blur", "change", "focus", "focusin", "focusout", "invalid",
+ "reset", "select", "submit")) {
+ group = L10N.getStr("interactionEvents");
+ } else if (starts("key") || is("input")) {
+ group = L10N.getStr("keyboardEvents");
+ } else if (starts("mouse") || has("click") || is("contextmenu", "show", "wheel")) {
+ group = L10N.getStr("mouseEvents");
+ } else if (starts("DOM")) {
+ group = L10N.getStr("mutationEvents");
+ } else if (is("abort", "error", "hashchange", "load", "loadend", "loadstart",
+ "pagehide", "pageshow", "progress", "timeout", "unload", "uploadprogress",
+ "visibilitychange")) {
+ group = L10N.getStr("navigationEvents");
+ } else if (is("pointerlockchange", "pointerlockerror")) {
+ group = L10N.getStr("pointerLockEvents");
+ } else if (is("compassneedscalibration", "userproximity")) {
+ group = L10N.getStr("sensorEvents");
+ } else if (starts("storage")) {
+ group = L10N.getStr("storageEvents");
+ } else if (is("beginEvent", "endEvent", "repeatEvent")) {
+ group = L10N.getStr("timeEvents");
+ } else if (starts("touch")) {
+ group = L10N.getStr("touchEvents");
+ } else {
+ group = L10N.getStr("otherEvents");
+ }
+
+ // Create the element node for the event listener item.
+ const itemView = this._createItemView(type, selector, url);
+
+ // Event breakpoints survive target navigations. Make sure the newly
+ // inserted event item is correctly checked.
+ const activeEventNames = this.getState().activeEventNames;
+ const checkboxState = activeEventNames.indexOf(type) != -1;
+
+ // Append an event listener item to this container.
+ this.push([itemView.container], {
+ staged: aOptions.staged, /* stage the item to be appended later? */
+ attachment: {
+ url: url,
+ type: type,
+ view: itemView,
+ selectors: [selector],
+ group: group,
+ checkboxState: checkboxState,
+ checkboxTooltip: this._eventCheckboxTooltip
+ }
+ });
+ },
+
+ /**
+ * Gets all the event types known to this container.
+ *
+ * @return array
+ * List of event types, for example ["load", "click"...]
+ */
+ getAllEvents: function () {
+ return this.attachments.map(e => e.type);
+ },
+
+ /**
+ * Gets the checked event types in this container.
+ *
+ * @return array
+ * List of event types, for example ["load", "click"...]
+ */
+ getCheckedEvents: function () {
+ return this.attachments.filter(e => e.checkboxState).map(e => e.type);
+ },
+
+ /**
+ * Customization function for creating an item's UI.
+ *
+ * @param string aType
+ * The event type, for example "click".
+ * @param string aSelector
+ * The target element's selector.
+ * @param string url
+ * The source url in which the event listener is located.
+ * @return object
+ * An object containing the event listener view nodes.
+ */
+ _createItemView: function (aType, aSelector, aUrl) {
+ let container = document.createElement("hbox");
+ container.className = "dbg-event-listener";
+
+ let eventType = document.createElement("label");
+ eventType.className = "plain dbg-event-listener-type";
+ eventType.setAttribute("value", aType);
+ container.appendChild(eventType);
+
+ let typeSeparator = document.createElement("label");
+ typeSeparator.className = "plain dbg-event-listener-separator";
+ typeSeparator.setAttribute("value", this._onSelectorString);
+ container.appendChild(typeSeparator);
+
+ let eventTargets = document.createElement("label");
+ eventTargets.className = "plain dbg-event-listener-targets";
+ eventTargets.setAttribute("value", aSelector);
+ container.appendChild(eventTargets);
+
+ let selectorSeparator = document.createElement("label");
+ selectorSeparator.className = "plain dbg-event-listener-separator";
+ selectorSeparator.setAttribute("value", this._inSourceString);
+ container.appendChild(selectorSeparator);
+
+ let eventLocation = document.createElement("label");
+ eventLocation.className = "plain dbg-event-listener-location";
+ eventLocation.setAttribute("value", SourceUtils.getSourceLabel(aUrl));
+ eventLocation.setAttribute("flex", "1");
+ eventLocation.setAttribute("crop", "center");
+ container.appendChild(eventLocation);
+
+ return {
+ container: container,
+ type: eventType,
+ targets: eventTargets,
+ location: eventLocation
+ };
+ },
+
+ /**
+ * The check listener for the event listeners container.
+ */
+ _onCheck: function ({ detail: { description, checked }, target }) {
+ if (description == "item") {
+ this.getItemForElement(target).attachment.checkboxState = checked;
+
+ this.actions.updateEventBreakpoints(this.getCheckedEvents());
+ return;
+ }
+
+ // Check all the event items in this group.
+ this.items
+ .filter(e => e.attachment.group == description)
+ .forEach(e => this.callMethod("checkItem", e.target, checked));
+ },
+
+ /**
+ * The select listener for the event listeners container.
+ */
+ _onClick: function ({ target }) {
+ // Changing the checkbox state is handled by the _onCheck event. Avoid
+ // handling that again in this click event, so pass in "noSiblings"
+ // when retrieving the target's item, to ignore the checkbox.
+ let eventItem = this.getItemForElement(target, { noSiblings: true });
+ if (eventItem) {
+ let newState = eventItem.attachment.checkboxState ^= 1;
+ this.callMethod("checkItem", eventItem.target, newState);
+ }
+ },
+
+ _eventCheckboxTooltip: "",
+ _onSelectorString: "",
+ _inSourceString: "",
+ _inNativeCodeString: ""
+});
+
+module.exports = EventListenersView;
diff --git a/devtools/client/debugger/content/views/moz.build b/devtools/client/debugger/content/views/moz.build
new file mode 100644
index 000000000..de1ecc184
--- /dev/null
+++ b/devtools/client/debugger/content/views/moz.build
@@ -0,0 +1,9 @@
+# vim: set filetype=python:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+DevToolsModules(
+ 'event-listeners-view.js',
+ 'sources-view.js'
+)
diff --git a/devtools/client/debugger/content/views/sources-view.js b/devtools/client/debugger/content/views/sources-view.js
new file mode 100644
index 000000000..bb68afcf4
--- /dev/null
+++ b/devtools/client/debugger/content/views/sources-view.js
@@ -0,0 +1,1370 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+/* import-globals-from ../../debugger-controller.js */
+
+const utils = require("../utils");
+const {
+ getSelectedSource,
+ getSourceByURL,
+ getBreakpoint,
+ getBreakpoints,
+ makeLocationId
+} = require("../queries");
+const actions = Object.assign(
+ {},
+ require("../actions/sources"),
+ require("../actions/breakpoints")
+);
+const { bindActionCreators } = require("devtools/client/shared/vendor/redux");
+const {
+ Heritage,
+ WidgetMethods,
+ setNamedTimeout
+} = require("devtools/client/shared/widgets/view-helpers");
+const { Task } = require("devtools/shared/task");
+const { SideMenuWidget } = require("resource://devtools/client/shared/widgets/SideMenuWidget.jsm");
+const { gDevTools } = require("devtools/client/framework/devtools");
+const {KeyCodes} = require("devtools/client/shared/keycodes");
+
+const NEW_SOURCE_DISPLAY_DELAY = 200; // ms
+const FUNCTION_SEARCH_POPUP_POSITION = "topcenter bottomleft";
+const BREAKPOINT_LINE_TOOLTIP_MAX_LENGTH = 1000; // chars
+const BREAKPOINT_CONDITIONAL_POPUP_POSITION = "before_start";
+const BREAKPOINT_CONDITIONAL_POPUP_OFFSET_X = 7; // px
+const BREAKPOINT_CONDITIONAL_POPUP_OFFSET_Y = -3; // px
+
+/**
+ * Functions handling the sources UI.
+ */
+function SourcesView(controller, DebuggerView) {
+ dumpn("SourcesView was instantiated");
+
+ utils.onReducerEvents(controller, {
+ "source": this.renderSource,
+ "blackboxed": this.renderBlackBoxed,
+ "prettyprinted": this.updateToolbarButtonsState,
+ "source-selected": this.renderSourceSelected,
+ "breakpoint-updated": bp => this.renderBreakpoint(bp),
+ "breakpoint-enabled": bp => this.renderBreakpoint(bp),
+ "breakpoint-disabled": bp => this.renderBreakpoint(bp),
+ "breakpoint-removed": bp => this.renderBreakpoint(bp, true),
+ }, this);
+
+ this.getState = controller.getState;
+ this.actions = bindActionCreators(actions, controller.dispatch);
+ this.DebuggerView = DebuggerView;
+ this.Parser = DebuggerController.Parser;
+
+ this.togglePrettyPrint = this.togglePrettyPrint.bind(this);
+ this.toggleBlackBoxing = this.toggleBlackBoxing.bind(this);
+ this.toggleBreakpoints = this.toggleBreakpoints.bind(this);
+
+ this._onEditorCursorActivity = this._onEditorCursorActivity.bind(this);
+ this._onMouseDown = this._onMouseDown.bind(this);
+ this._onSourceSelect = this._onSourceSelect.bind(this);
+ this._onStopBlackBoxing = this._onStopBlackBoxing.bind(this);
+ this._onBreakpointRemoved = this._onBreakpointRemoved.bind(this);
+ this._onBreakpointClick = this._onBreakpointClick.bind(this);
+ this._onBreakpointCheckboxClick = this._onBreakpointCheckboxClick.bind(this);
+ this._onConditionalPopupShowing = this._onConditionalPopupShowing.bind(this);
+ this._onConditionalPopupShown = this._onConditionalPopupShown.bind(this);
+ this._onConditionalPopupHiding = this._onConditionalPopupHiding.bind(this);
+ this._onConditionalTextboxKeyPress = this._onConditionalTextboxKeyPress.bind(this);
+ this._onEditorContextMenuOpen = this._onEditorContextMenuOpen.bind(this);
+ this._onCopyUrlCommand = this._onCopyUrlCommand.bind(this);
+ this._onNewTabCommand = this._onNewTabCommand.bind(this);
+ this._onConditionalPopupHidden = this._onConditionalPopupHidden.bind(this);
+}
+
+SourcesView.prototype = Heritage.extend(WidgetMethods, {
+ /**
+ * Initialization function, called when the debugger is started.
+ */
+ initialize: function (isWorker) {
+ dumpn("Initializing the SourcesView");
+
+ this.widget = new SideMenuWidget(document.getElementById("sources"), {
+ contextMenu: document.getElementById("debuggerSourcesContextMenu"),
+ showArrows: true
+ });
+
+ this._preferredSourceURL = null;
+ this._unnamedSourceIndex = 0;
+ this.emptyText = L10N.getStr("noSourcesText");
+ this._blackBoxCheckboxTooltip = L10N.getStr("blackBoxCheckboxTooltip");
+
+ this._commandset = document.getElementById("debuggerCommands");
+ this._popupset = document.getElementById("debuggerPopupset");
+ this._cmPopup = document.getElementById("sourceEditorContextMenu");
+ this._cbPanel = document.getElementById("conditional-breakpoint-panel");
+ this._cbTextbox = document.getElementById("conditional-breakpoint-panel-textbox");
+ this._blackBoxButton = document.getElementById("black-box");
+ this._stopBlackBoxButton = document.getElementById("black-boxed-message-button");
+ this._prettyPrintButton = document.getElementById("pretty-print");
+ this._toggleBreakpointsButton = document.getElementById("toggle-breakpoints");
+ this._newTabMenuItem = document.getElementById("debugger-sources-context-newtab");
+ this._copyUrlMenuItem = document.getElementById("debugger-sources-context-copyurl");
+
+ this._noResultsFoundToolTip = new Tooltip(document);
+ this._noResultsFoundToolTip.defaultPosition = FUNCTION_SEARCH_POPUP_POSITION;
+
+ // We don't show the pretty print button if debugger a worker
+ // because it simply doesn't work yet. (bug 1273730)
+ if (Prefs.prettyPrintEnabled && !isWorker) {
+ this._prettyPrintButton.removeAttribute("hidden");
+ }
+
+ this._editorContainer = document.getElementById("editor");
+ this._editorContainer.addEventListener("mousedown", this._onMouseDown, false);
+
+ this.widget.addEventListener("select", this._onSourceSelect, false);
+
+ this._stopBlackBoxButton.addEventListener("click", this._onStopBlackBoxing, false);
+ this._cbPanel.addEventListener("popupshowing", this._onConditionalPopupShowing, false);
+ this._cbPanel.addEventListener("popupshown", this._onConditionalPopupShown, false);
+ this._cbPanel.addEventListener("popuphiding", this._onConditionalPopupHiding, false);
+ this._cbPanel.addEventListener("popuphidden", this._onConditionalPopupHidden, false);
+ this._cbTextbox.addEventListener("keypress", this._onConditionalTextboxKeyPress, false);
+ this._copyUrlMenuItem.addEventListener("command", this._onCopyUrlCommand, false);
+ this._newTabMenuItem.addEventListener("command", this._onNewTabCommand, false);
+
+ this._cbPanel.hidden = true;
+ this.allowFocusOnRightClick = true;
+ this.autoFocusOnSelection = false;
+ this.autoFocusOnFirstItem = false;
+
+ // Sort the contents by the displayed label.
+ this.sortContents((aFirst, aSecond) => {
+ return +(aFirst.attachment.label.toLowerCase() >
+ aSecond.attachment.label.toLowerCase());
+ });
+
+ // Sort known source groups towards the end of the list
+ this.widget.groupSortPredicate = function (a, b) {
+ if ((a in KNOWN_SOURCE_GROUPS) == (b in KNOWN_SOURCE_GROUPS)) {
+ return a.localeCompare(b);
+ }
+ return (a in KNOWN_SOURCE_GROUPS) ? 1 : -1;
+ };
+
+ this.DebuggerView.editor.on("popupOpen", this._onEditorContextMenuOpen);
+
+ this._addCommands();
+ },
+
+ /**
+ * Destruction function, called when the debugger is closed.
+ */
+ destroy: function () {
+ dumpn("Destroying the SourcesView");
+
+ this.widget.removeEventListener("select", this._onSourceSelect, false);
+ this._stopBlackBoxButton.removeEventListener("click", this._onStopBlackBoxing, false);
+ this._cbPanel.removeEventListener("popupshowing", this._onConditionalPopupShowing, false);
+ this._cbPanel.removeEventListener("popupshown", this._onConditionalPopupShown, false);
+ this._cbPanel.removeEventListener("popuphiding", this._onConditionalPopupHiding, false);
+ this._cbPanel.removeEventListener("popuphidden", this._onConditionalPopupHidden, false);
+ this._cbTextbox.removeEventListener("keypress", this._onConditionalTextboxKeyPress, false);
+ this._copyUrlMenuItem.removeEventListener("command", this._onCopyUrlCommand, false);
+ this._newTabMenuItem.removeEventListener("command", this._onNewTabCommand, false);
+ this.DebuggerView.editor.off("popupOpen", this._onEditorContextMenuOpen, false);
+ },
+
+ empty: function () {
+ WidgetMethods.empty.call(this);
+ this._unnamedSourceIndex = 0;
+ this._selectedBreakpoint = null;
+ },
+
+ /**
+ * Add commands that XUL can fire.
+ */
+ _addCommands: function () {
+ XULUtils.addCommands(this._commandset, {
+ addBreakpointCommand: e => this._onCmdAddBreakpoint(e),
+ addConditionalBreakpointCommand: e => this._onCmdAddConditionalBreakpoint(e),
+ blackBoxCommand: () => this.toggleBlackBoxing(),
+ unBlackBoxButton: () => this._onStopBlackBoxing(),
+ prettyPrintCommand: () => this.togglePrettyPrint(),
+ toggleBreakpointsCommand: () =>this.toggleBreakpoints(),
+ nextSourceCommand: () => this.selectNextItem(),
+ prevSourceCommand: () => this.selectPrevItem()
+ });
+ },
+
+ /**
+ * Sets the preferred location to be selected in this sources container.
+ * @param string aUrl
+ */
+ set preferredSource(aUrl) {
+ this._preferredValue = aUrl;
+
+ // Selects the element with the specified value in this sources container,
+ // if already inserted.
+ if (this.containsValue(aUrl)) {
+ this.selectedValue = aUrl;
+ }
+ },
+
+ sourcesDidUpdate: function () {
+ if (!getSelectedSource(this.getState())) {
+ let url = this._preferredSourceURL;
+ let source = url && getSourceByURL(this.getState(), url);
+ if (source) {
+ this.actions.selectSource(source);
+ }
+ else {
+ setNamedTimeout("new-source", NEW_SOURCE_DISPLAY_DELAY, () => {
+ if (!getSelectedSource(this.getState()) && this.itemCount > 0) {
+ this.actions.selectSource(this.getItemAtIndex(0).attachment.source);
+ }
+ });
+ }
+ }
+ },
+
+ renderSource: function (source) {
+ this.addSource(source, { staged: false });
+ for (let bp of getBreakpoints(this.getState())) {
+ if (bp.location.actor === source.actor) {
+ this.renderBreakpoint(bp);
+ }
+ }
+ this.sourcesDidUpdate();
+ },
+
+ /**
+ * Adds a source to this sources container.
+ *
+ * @param object aSource
+ * The source object coming from the active thread.
+ * @param object aOptions [optional]
+ * Additional options for adding the source. Supported options:
+ * - staged: true to stage the item to be appended later
+ */
+ addSource: function (aSource, aOptions = {}) {
+ if (!aSource.url && !aOptions.force) {
+ // We don't show any unnamed eval scripts yet (see bug 1124106)
+ return;
+ }
+
+ let { label, group, unicodeUrl } = this._parseUrl(aSource);
+
+ let contents = document.createElement("label");
+ contents.className = "plain dbg-source-item";
+ contents.setAttribute("value", label);
+ contents.setAttribute("crop", "start");
+ contents.setAttribute("flex", "1");
+ contents.setAttribute("tooltiptext", unicodeUrl);
+
+ if (aSource.introductionType === "wasm") {
+ const wasm = document.createElement("box");
+ wasm.className = "dbg-wasm-item";
+ const icon = document.createElement("box");
+ icon.setAttribute("tooltiptext", L10N.getStr("experimental"));
+ icon.className = "icon";
+ wasm.appendChild(icon);
+ wasm.appendChild(contents);
+
+ contents = wasm;
+ }
+
+ // If the source is blackboxed, apply the appropriate style.
+ if (gThreadClient.source(aSource).isBlackBoxed) {
+ contents.classList.add("black-boxed");
+ }
+
+ // Append a source item to this container.
+ this.push([contents, aSource.actor], {
+ staged: aOptions.staged, /* stage the item to be appended later? */
+ attachment: {
+ label: label,
+ group: group,
+ checkboxState: !aSource.isBlackBoxed,
+ checkboxTooltip: this._blackBoxCheckboxTooltip,
+ source: aSource
+ }
+ });
+ },
+
+ _parseUrl: function (aSource) {
+ let fullUrl = aSource.url;
+ let url, unicodeUrl, label, group;
+
+ if (!fullUrl) {
+ unicodeUrl = "SCRIPT" + this._unnamedSourceIndex++;
+ label = unicodeUrl;
+ group = L10N.getStr("anonymousSourcesLabel");
+ }
+ else {
+ let url = fullUrl.split(" -> ").pop();
+ label = aSource.addonPath ? aSource.addonPath : SourceUtils.getSourceLabel(url);
+ group = aSource.addonID ? aSource.addonID : SourceUtils.getSourceGroup(url);
+ unicodeUrl = NetworkHelper.convertToUnicode(unescape(fullUrl));
+ }
+
+ return {
+ label: label,
+ group: group,
+ unicodeUrl: unicodeUrl
+ };
+ },
+
+ renderBreakpoint: function (breakpoint, removed) {
+ if (removed) {
+ // Be defensive about the breakpoint not existing.
+ if (this._getBreakpoint(breakpoint)) {
+ this._removeBreakpoint(breakpoint);
+ }
+ }
+ else {
+ if (this._getBreakpoint(breakpoint)) {
+ this._updateBreakpointStatus(breakpoint);
+ }
+ else {
+ this._addBreakpoint(breakpoint);
+ }
+ }
+ },
+
+ /**
+ * Adds a breakpoint to this sources container.
+ *
+ * @param object aBreakpointClient
+ * See Breakpoints.prototype._showBreakpoint
+ * @param object aOptions [optional]
+ * @see DebuggerController.Breakpoints.addBreakpoint
+ */
+ _addBreakpoint: function (breakpoint, options = {}) {
+ let disabled = breakpoint.disabled;
+ let location = breakpoint.location;
+
+ // Get the source item to which the breakpoint should be attached.
+ let sourceItem = this.getItemByValue(location.actor);
+ if (!sourceItem) {
+ return;
+ }
+
+ // Create the element node and menu popup for the breakpoint item.
+ let breakpointArgs = Heritage.extend(breakpoint.asMutable(), options);
+ let breakpointView = this._createBreakpointView.call(this, breakpointArgs);
+ let contextMenu = this._createContextMenu.call(this, breakpointArgs);
+
+ // Append a breakpoint child item to the corresponding source item.
+ sourceItem.append(breakpointView.container, {
+ attachment: Heritage.extend(breakpointArgs, {
+ actor: location.actor,
+ line: location.line,
+ view: breakpointView,
+ popup: contextMenu
+ }),
+ attributes: [
+ ["contextmenu", contextMenu.menupopupId]
+ ],
+ // Make sure that when the breakpoint item is removed, the corresponding
+ // menupopup and commandset are also destroyed.
+ finalize: this._onBreakpointRemoved
+ });
+
+ if (typeof breakpoint.condition === "string") {
+ this.highlightBreakpoint(breakpoint.location, {
+ openPopup: true,
+ noEditorUpdate: true
+ });
+ }
+
+ window.emit(EVENTS.BREAKPOINT_SHOWN_IN_PANE);
+ },
+
+ /**
+ * Removes a breakpoint from this sources container.
+ * It does not also remove the breakpoint from the controller. Be careful.
+ *
+ * @param object aLocation
+ * @see DebuggerController.Breakpoints.addBreakpoint
+ */
+ _removeBreakpoint: function (breakpoint) {
+ // When a parent source item is removed, all the child breakpoint items are
+ // also automagically removed.
+ let sourceItem = this.getItemByValue(breakpoint.location.actor);
+ if (!sourceItem) {
+ return;
+ }
+
+ // Clear the breakpoint view.
+ sourceItem.remove(this._getBreakpoint(breakpoint));
+
+ if (this._selectedBreakpoint &&
+ (queries.makeLocationId(this._selectedBreakpoint.location) ===
+ queries.makeLocationId(breakpoint.location))) {
+ this._selectedBreakpoint = null;
+ }
+
+ window.emit(EVENTS.BREAKPOINT_HIDDEN_IN_PANE);
+ },
+
+ _getBreakpoint: function (bp) {
+ return this.getItemForPredicate(item => {
+ return item.attachment.actor === bp.location.actor &&
+ item.attachment.line === bp.location.line;
+ });
+ },
+
+ /**
+ * Updates a breakpoint.
+ *
+ * @param object breakpoint
+ */
+ _updateBreakpointStatus: function (breakpoint) {
+ let location = breakpoint.location;
+ let breakpointItem = this._getBreakpoint(getBreakpoint(this.getState(), location));
+ if (!breakpointItem) {
+ return promise.reject(new Error("No breakpoint found."));
+ }
+
+ // Breakpoint will now be enabled.
+ let attachment = breakpointItem.attachment;
+
+ // Update the corresponding menu items to reflect the enabled state.
+ let prefix = "bp-cMenu-"; // "breakpoints context menu"
+ let identifier = makeLocationId(location);
+ let enableSelfId = prefix + "enableSelf-" + identifier + "-menuitem";
+ let disableSelfId = prefix + "disableSelf-" + identifier + "-menuitem";
+ let enableSelf = document.getElementById(enableSelfId);
+ let disableSelf = document.getElementById(disableSelfId);
+
+ if (breakpoint.disabled) {
+ enableSelf.removeAttribute("hidden");
+ disableSelf.setAttribute("hidden", true);
+ attachment.view.checkbox.removeAttribute("checked");
+ }
+ else {
+ enableSelf.setAttribute("hidden", true);
+ disableSelf.removeAttribute("hidden");
+ attachment.view.checkbox.setAttribute("checked", "true");
+
+ // Update the breakpoint toggle button checked state.
+ this._toggleBreakpointsButton.removeAttribute("checked");
+ }
+
+ },
+
+ /**
+ * Highlights a breakpoint in this sources container.
+ *
+ * @param object aLocation
+ * @see DebuggerController.Breakpoints.addBreakpoint
+ * @param object aOptions [optional]
+ * An object containing some of the following boolean properties:
+ * - openPopup: tells if the expression popup should be shown.
+ * - noEditorUpdate: tells if you want to skip editor updates.
+ */
+ highlightBreakpoint: function (aLocation, aOptions = {}) {
+ let breakpoint = getBreakpoint(this.getState(), aLocation);
+ if (!breakpoint) {
+ return;
+ }
+
+ // Breakpoint will now be selected.
+ this._selectBreakpoint(breakpoint);
+
+ // Update the editor location if necessary.
+ if (!aOptions.noEditorUpdate) {
+ this.DebuggerView.setEditorLocation(aLocation.actor, aLocation.line, { noDebug: true });
+ }
+
+ // If the breakpoint requires a new conditional expression, display
+ // the panel to input the corresponding expression.
+ if (aOptions.openPopup) {
+ return this._openConditionalPopup();
+ } else {
+ return this._hideConditionalPopup();
+ }
+ },
+
+ /**
+ * Highlight the breakpoint on the current currently focused line/column
+ * if it exists.
+ */
+ highlightBreakpointAtCursor: function () {
+ let actor = this.selectedValue;
+ let line = this.DebuggerView.editor.getCursor().line + 1;
+
+ let location = { actor: actor, line: line };
+ this.highlightBreakpoint(location, { noEditorUpdate: true });
+ },
+
+ /**
+ * Unhighlights the current breakpoint in this sources container.
+ */
+ unhighlightBreakpoint: function () {
+ this._hideConditionalPopup();
+ this._unselectBreakpoint();
+ },
+
+ /**
+ * Display the message thrown on breakpoint condition
+ */
+ showBreakpointConditionThrownMessage: function (aLocation, aMessage = "") {
+ let breakpointItem = this._getBreakpoint(getBreakpoint(this.getState(), aLocation));
+ if (!breakpointItem) {
+ return;
+ }
+ let attachment = breakpointItem.attachment;
+ attachment.view.container.classList.add("dbg-breakpoint-condition-thrown");
+ attachment.view.message.setAttribute("value", aMessage);
+ },
+
+ /**
+ * Update the checked/unchecked and enabled/disabled states of the buttons in
+ * the sources toolbar based on the currently selected source's state.
+ */
+ updateToolbarButtonsState: function (source) {
+ if (source.isBlackBoxed) {
+ this._blackBoxButton.setAttribute("checked", true);
+ this._prettyPrintButton.setAttribute("checked", true);
+ } else {
+ this._blackBoxButton.removeAttribute("checked");
+ this._prettyPrintButton.removeAttribute("checked");
+ }
+
+ if (source.isPrettyPrinted) {
+ this._prettyPrintButton.setAttribute("checked", true);
+ } else {
+ this._prettyPrintButton.removeAttribute("checked");
+ }
+ },
+
+ /**
+ * Toggle the pretty printing of the selected source.
+ */
+ togglePrettyPrint: function () {
+ if (this._prettyPrintButton.hasAttribute("disabled")) {
+ return;
+ }
+
+ this.DebuggerView.showProgressBar();
+ const source = getSelectedSource(this.getState());
+ const sourceClient = gThreadClient.source(source);
+ const shouldPrettyPrint = !source.isPrettyPrinted;
+
+ // This is only here to give immediate feedback,
+ // `renderPrettyPrinted` will set the final status of the buttons
+ if (shouldPrettyPrint) {
+ this._prettyPrintButton.setAttribute("checked", true);
+ } else {
+ this._prettyPrintButton.removeAttribute("checked");
+ }
+
+ this.actions.togglePrettyPrint(source);
+ },
+
+ /**
+ * Toggle the black boxed state of the selected source.
+ */
+ toggleBlackBoxing: Task.async(function* () {
+ const source = getSelectedSource(this.getState());
+ const shouldBlackBox = !source.isBlackBoxed;
+
+ // Be optimistic that the (un-)black boxing will succeed, so
+ // enable/disable the pretty print button and check/uncheck the
+ // black box button immediately.
+ if (shouldBlackBox) {
+ this._prettyPrintButton.setAttribute("disabled", true);
+ this._blackBoxButton.setAttribute("checked", true);
+ } else {
+ this._prettyPrintButton.removeAttribute("disabled");
+ this._blackBoxButton.removeAttribute("checked");
+ }
+
+ this.actions.blackbox(source, shouldBlackBox);
+ }),
+
+ renderBlackBoxed: function (source) {
+ const sourceItem = this.getItemByValue(source.actor);
+ sourceItem.prebuiltNode.classList.toggle(
+ "black-boxed",
+ source.isBlackBoxed
+ );
+
+ if (getSelectedSource(this.getState()).actor === source.actor) {
+ this.updateToolbarButtonsState(source);
+ }
+ },
+
+ /**
+ * Toggles all breakpoints enabled/disabled.
+ */
+ toggleBreakpoints: function () {
+ let breakpoints = getBreakpoints(this.getState());
+ let hasBreakpoints = breakpoints.length > 0;
+ let hasEnabledBreakpoints = breakpoints.some(bp => !bp.disabled);
+
+ if (hasBreakpoints && hasEnabledBreakpoints) {
+ this._toggleBreakpointsButton.setAttribute("checked", true);
+ this._onDisableAll();
+ } else {
+ this._toggleBreakpointsButton.removeAttribute("checked");
+ this._onEnableAll();
+ }
+ },
+
+ hidePrettyPrinting: function () {
+ this._prettyPrintButton.style.display = "none";
+
+ if (this._blackBoxButton.style.display === "none") {
+ let sep = document.querySelector("#sources-toolbar .devtools-separator");
+ sep.style.display = "none";
+ }
+ },
+
+ hideBlackBoxing: function () {
+ this._blackBoxButton.style.display = "none";
+
+ if (this._prettyPrintButton.style.display === "none") {
+ let sep = document.querySelector("#sources-toolbar .devtools-separator");
+ sep.style.display = "none";
+ }
+ },
+
+ getDisplayURL: function (source) {
+ if (!source.url) {
+ return this.getItemByValue(source.actor).attachment.label;
+ }
+ return NetworkHelper.convertToUnicode(unescape(source.url));
+ },
+
+ /**
+ * Marks a breakpoint as selected in this sources container.
+ *
+ * @param object aItem
+ * The breakpoint item to select.
+ */
+ _selectBreakpoint: function (bp) {
+ if (this._selectedBreakpoint === bp) {
+ return;
+ }
+ this._unselectBreakpoint();
+ this._selectedBreakpoint = bp;
+
+ const item = this._getBreakpoint(bp);
+ item.target.classList.add("selected");
+
+ // Ensure the currently selected breakpoint is visible.
+ this.widget.ensureElementIsVisible(item.target);
+ },
+
+ /**
+ * Marks the current breakpoint as unselected in this sources container.
+ */
+ _unselectBreakpoint: function () {
+ if (!this._selectedBreakpoint) {
+ return;
+ }
+
+ const item = this._getBreakpoint(this._selectedBreakpoint);
+ item.target.classList.remove("selected");
+
+ this._selectedBreakpoint = null;
+ },
+
+ /**
+ * Opens a conditional breakpoint's expression input popup.
+ */
+ _openConditionalPopup: function () {
+ let breakpointItem = this._getBreakpoint(this._selectedBreakpoint);
+ let attachment = breakpointItem.attachment;
+ // Check if this is an enabled conditional breakpoint, and if so,
+ // retrieve the current conditional epression.
+ let bp = getBreakpoint(this.getState(), attachment);
+ let expr = (bp ? (bp.condition || "") : "");
+ let cbPanel = this._cbPanel;
+
+ // Update the conditional expression textbox. If no expression was
+ // previously set, revert to using an empty string by default.
+ this._cbTextbox.value = expr;
+
+ function openPopup() {
+ // Show the conditional expression panel. The popup arrow should be pointing
+ // at the line number node in the breakpoint item view.
+ cbPanel.hidden = false;
+ cbPanel.openPopup(breakpointItem.attachment.view.lineNumber,
+ BREAKPOINT_CONDITIONAL_POPUP_POSITION,
+ BREAKPOINT_CONDITIONAL_POPUP_OFFSET_X,
+ BREAKPOINT_CONDITIONAL_POPUP_OFFSET_Y);
+
+ cbPanel.removeEventListener("popuphidden", openPopup, false);
+ }
+
+ // Wait until the other cb panel is closed
+ if (!this._cbPanel.hidden) {
+ this._cbPanel.addEventListener("popuphidden", openPopup, false);
+ } else {
+ openPopup();
+ }
+ },
+
+ /**
+ * Hides a conditional breakpoint's expression input popup.
+ */
+ _hideConditionalPopup: function () {
+ // Sometimes this._cbPanel doesn't have hidePopup method which doesn't
+ // break anything but simply outputs an exception to the console.
+ if (this._cbPanel.hidePopup) {
+ this._cbPanel.hidePopup();
+ }
+ },
+
+ /**
+ * Customization function for creating a breakpoint item's UI.
+ *
+ * @param object aOptions
+ * A couple of options or flags supported by this operation:
+ * - location: the breakpoint's source location and line number
+ * - disabled: the breakpoint's disabled state, boolean
+ * - text: the breakpoint's line text to be displayed
+ * - message: thrown string when the breakpoint condition throws
+ * @return object
+ * An object containing the breakpoint container, checkbox,
+ * line number and line text nodes.
+ */
+ _createBreakpointView: function (aOptions) {
+ let { location, disabled, text, message } = aOptions;
+ let identifier = makeLocationId(location);
+
+ let checkbox = document.createElement("checkbox");
+ if (!disabled) {
+ checkbox.setAttribute("checked", true);
+ }
+ checkbox.className = "dbg-breakpoint-checkbox";
+
+ let lineNumberNode = document.createElement("label");
+ lineNumberNode.className = "plain dbg-breakpoint-line";
+ lineNumberNode.setAttribute("value", location.line);
+
+ let lineTextNode = document.createElement("label");
+ lineTextNode.className = "plain dbg-breakpoint-text";
+ lineTextNode.setAttribute("value", text);
+ lineTextNode.setAttribute("crop", "end");
+ lineTextNode.setAttribute("flex", "1");
+
+ let tooltip = text ? text.substr(0, BREAKPOINT_LINE_TOOLTIP_MAX_LENGTH) : "";
+ lineTextNode.setAttribute("tooltiptext", tooltip);
+
+ let thrownNode = document.createElement("label");
+ thrownNode.className = "plain dbg-breakpoint-condition-thrown-message dbg-breakpoint-text";
+ thrownNode.setAttribute("value", message);
+ thrownNode.setAttribute("crop", "end");
+ thrownNode.setAttribute("flex", "1");
+
+ let bpLineContainer = document.createElement("hbox");
+ bpLineContainer.className = "plain dbg-breakpoint-line-container";
+ bpLineContainer.setAttribute("flex", "1");
+
+ bpLineContainer.appendChild(lineNumberNode);
+ bpLineContainer.appendChild(lineTextNode);
+
+ let bpDetailContainer = document.createElement("vbox");
+ bpDetailContainer.className = "plain dbg-breakpoint-detail-container";
+ bpDetailContainer.setAttribute("flex", "1");
+
+ bpDetailContainer.appendChild(bpLineContainer);
+ bpDetailContainer.appendChild(thrownNode);
+
+ let container = document.createElement("hbox");
+ container.id = "breakpoint-" + identifier;
+ container.className = "dbg-breakpoint side-menu-widget-item-other";
+ container.classList.add("devtools-monospace");
+ container.setAttribute("align", "center");
+ container.setAttribute("flex", "1");
+
+ container.addEventListener("click", this._onBreakpointClick, false);
+ checkbox.addEventListener("click", this._onBreakpointCheckboxClick, false);
+
+ container.appendChild(checkbox);
+ container.appendChild(bpDetailContainer);
+
+ return {
+ container: container,
+ checkbox: checkbox,
+ lineNumber: lineNumberNode,
+ lineText: lineTextNode,
+ message: thrownNode
+ };
+ },
+
+ /**
+ * Creates a context menu for a breakpoint element.
+ *
+ * @param object aOptions
+ * A couple of options or flags supported by this operation:
+ * - location: the breakpoint's source location and line number
+ * - disabled: the breakpoint's disabled state, boolean
+ * @return object
+ * An object containing the breakpoint commandset and menu popup ids.
+ */
+ _createContextMenu: function (aOptions) {
+ let { location, disabled } = aOptions;
+ let identifier = makeLocationId(location);
+
+ let commandset = document.createElement("commandset");
+ let menupopup = document.createElement("menupopup");
+ commandset.id = "bp-cSet-" + identifier;
+ menupopup.id = "bp-mPop-" + identifier;
+
+ createMenuItem.call(this, "enableSelf", !disabled);
+ createMenuItem.call(this, "disableSelf", disabled);
+ createMenuItem.call(this, "deleteSelf");
+ createMenuSeparator();
+ createMenuItem.call(this, "setConditional");
+ createMenuSeparator();
+ createMenuItem.call(this, "enableOthers");
+ createMenuItem.call(this, "disableOthers");
+ createMenuItem.call(this, "deleteOthers");
+ createMenuSeparator();
+ createMenuItem.call(this, "enableAll");
+ createMenuItem.call(this, "disableAll");
+ createMenuSeparator();
+ createMenuItem.call(this, "deleteAll");
+
+ this._popupset.appendChild(menupopup);
+ this._commandset.appendChild(commandset);
+
+ return {
+ commandsetId: commandset.id,
+ menupopupId: menupopup.id
+ };
+
+ /**
+ * Creates a menu item specified by a name with the appropriate attributes
+ * (label and handler).
+ *
+ * @param string aName
+ * A global identifier for the menu item.
+ * @param boolean aHiddenFlag
+ * True if this menuitem should be hidden.
+ */
+ function createMenuItem(aName, aHiddenFlag) {
+ let menuitem = document.createElement("menuitem");
+ let command = document.createElement("command");
+
+ let prefix = "bp-cMenu-"; // "breakpoints context menu"
+ let commandId = prefix + aName + "-" + identifier + "-command";
+ let menuitemId = prefix + aName + "-" + identifier + "-menuitem";
+
+ let label = L10N.getStr("breakpointMenuItem." + aName);
+ let func = "_on" + aName.charAt(0).toUpperCase() + aName.slice(1);
+
+ command.id = commandId;
+ command.setAttribute("label", label);
+ command.addEventListener("command", () => this[func](location), false);
+
+ menuitem.id = menuitemId;
+ menuitem.setAttribute("command", commandId);
+ aHiddenFlag && menuitem.setAttribute("hidden", "true");
+
+ commandset.appendChild(command);
+ menupopup.appendChild(menuitem);
+ }
+
+ /**
+ * Creates a simple menu separator element and appends it to the current
+ * menupopup hierarchy.
+ */
+ function createMenuSeparator() {
+ let menuseparator = document.createElement("menuseparator");
+ menupopup.appendChild(menuseparator);
+ }
+ },
+
+ /**
+ * Copy the source url from the currently selected item.
+ */
+ _onCopyUrlCommand: function () {
+ let selected = this.selectedItem && this.selectedItem.attachment;
+ if (!selected) {
+ return;
+ }
+ clipboardHelper.copyString(selected.source.url);
+ },
+
+ /**
+ * Opens selected item source in a new tab.
+ */
+ _onNewTabCommand: function () {
+ let win = Services.wm.getMostRecentWindow(gDevTools.chromeWindowType);
+ let selected = this.selectedItem.attachment;
+ win.openUILinkIn(selected.source.url, "tab", { relatedToCurrent: true });
+ },
+
+ /**
+ * Function called each time a breakpoint item is removed.
+ *
+ * @param object aItem
+ * The corresponding item.
+ */
+ _onBreakpointRemoved: function (aItem) {
+ dumpn("Finalizing breakpoint item: " + aItem.stringify());
+
+ // Destroy the context menu for the breakpoint.
+ let contextMenu = aItem.attachment.popup;
+ document.getElementById(contextMenu.commandsetId).remove();
+ document.getElementById(contextMenu.menupopupId).remove();
+ },
+
+ _onMouseDown: function (e) {
+ this.hideNoResultsTooltip();
+
+ if (!e.metaKey) {
+ return;
+ }
+
+ let editor = this.DebuggerView.editor;
+ let identifier = this._findIdentifier(e.clientX, e.clientY);
+
+ if (!identifier) {
+ return;
+ }
+
+ let foundDefinitions = this._getFunctionDefinitions(identifier);
+
+ if (!foundDefinitions || !foundDefinitions.definitions) {
+ return;
+ }
+
+ this._showFunctionDefinitionResults(identifier, foundDefinitions.definitions, editor);
+ },
+
+ /**
+ * Searches for function definition of a function in a given source file
+ */
+
+ _findDefinition: function (parsedSource, aName) {
+ let functionDefinitions = parsedSource.getNamedFunctionDefinitions(aName);
+
+ let resultList = [];
+
+ if (!functionDefinitions || !functionDefinitions.length || !functionDefinitions[0].length) {
+ return {
+ definitions: resultList
+ };
+ }
+
+ // functionDefinitions is a list with an object full of metadata,
+ // extract the data and use to construct a more useful, less
+ // cluttered, contextual list
+ for (let i = 0; i < functionDefinitions.length; i++) {
+ let functionDefinition = {
+ source: functionDefinitions[i].sourceUrl,
+ startLine: functionDefinitions[i][0].functionLocation.start.line,
+ startColumn: functionDefinitions[i][0].functionLocation.start.column,
+ name: functionDefinitions[i][0].functionName
+ };
+
+ resultList.push(functionDefinition);
+ }
+
+ return {
+ definitions: resultList
+ };
+ },
+
+ /**
+ * Searches for an identifier underneath the specified position in the
+ * source editor.
+ *
+ * @param number x, y
+ * The left/top coordinates where to look for an identifier.
+ */
+ _findIdentifier: function (x, y) {
+ let parsedSource = SourceUtils.parseSource(this.DebuggerView, this.Parser);
+ let identifierInfo = SourceUtils.findIdentifier(this.DebuggerView.editor, parsedSource, x, y);
+
+ // Not hovering over an identifier
+ if (!identifierInfo) {
+ return;
+ }
+
+ return identifierInfo;
+ },
+
+ /**
+ * The selection listener for the source editor.
+ */
+ _onEditorCursorActivity: function (e) {
+ let editor = this.DebuggerView.editor;
+ let start = editor.getCursor("start").line + 1;
+ let end = editor.getCursor().line + 1;
+ let source = getSelectedSource(this.getState());
+
+ if (source) {
+ let location = { actor: source.actor, line: start };
+ if (getBreakpoint(this.getState(), location) && start == end) {
+ this.highlightBreakpoint(location, { noEditorUpdate: true });
+ } else {
+ this.unhighlightBreakpoint();
+ }
+ }
+ },
+
+ /*
+ * Uses function definition data to perform actions in different
+ * cases of how many locations were found: zero, one, or multiple definitions
+ */
+ _showFunctionDefinitionResults: function (aHoveredFunction, aDefinitionList, aEditor) {
+ let definitions = aDefinitionList;
+ let hoveredFunction = aHoveredFunction;
+
+ // show a popup saying no results were found
+ if (definitions.length == 0) {
+ this._noResultsFoundToolTip.setTextContent({
+ messages: [L10N.getStr("noMatchingStringsText")]
+ });
+
+ this._markedIdentifier = aEditor.markText(
+ { line: hoveredFunction.location.start.line - 1, ch: hoveredFunction.location.start.column },
+ { line: hoveredFunction.location.end.line - 1, ch: hoveredFunction.location.end.column });
+
+ this._noResultsFoundToolTip.show(this._markedIdentifier.anchor);
+
+ } else if (definitions.length == 1) {
+ this.DebuggerView.setEditorLocation(definitions[0].source, definitions[0].startLine);
+ } else {
+ // TODO: multiple definitions found, do something else
+ this.DebuggerView.setEditorLocation(definitions[0].source, definitions[0].startLine);
+ }
+ },
+
+ /**
+ * Hides the tooltip and clear marked text popup.
+ */
+ hideNoResultsTooltip: function () {
+ this._noResultsFoundToolTip.hide();
+ if (this._markedIdentifier) {
+ this._markedIdentifier.clear();
+ this._markedIdentifier = null;
+ }
+ },
+
+ /*
+ * Gets the definition locations from function metadata
+ */
+ _getFunctionDefinitions: function (aIdentifierInfo) {
+ let parsedSource = SourceUtils.parseSource(this.DebuggerView, this.Parser);
+ let definition_info = this._findDefinition(parsedSource, aIdentifierInfo.name);
+
+ // Did not find any definitions for the identifier
+ if (!definition_info) {
+ return;
+ }
+
+ return definition_info;
+ },
+
+ /**
+ * The select listener for the sources container.
+ */
+ _onSourceSelect: function ({ detail: sourceItem }) {
+ if (!sourceItem) {
+ return;
+ }
+
+ const { source } = sourceItem.attachment;
+ this.actions.selectSource(source);
+ },
+
+ renderSourceSelected: function (source) {
+ if (source.url) {
+ this._preferredSourceURL = source.url;
+ }
+ this.updateToolbarButtonsState(source);
+ this._selectItem(this.getItemByValue(source.actor));
+ },
+
+ /**
+ * The click listener for the "stop black boxing" button.
+ */
+ _onStopBlackBoxing: Task.async(function* () {
+ this.actions.blackbox(getSelectedSource(this.getState()), false);
+ }),
+
+ /**
+ * The source editor's contextmenu handler.
+ * - Toggles "Add Conditional Breakpoint" and "Edit Conditional Breakpoint" items
+ */
+ _onEditorContextMenuOpen: function (message, ev, popup) {
+ let actor = this.selectedValue;
+ let line = this.DebuggerView.editor.getCursor().line + 1;
+ let location = { actor, line };
+
+ let breakpoint = getBreakpoint(this.getState(), location);
+ let addConditionalBreakpointMenuItem = popup.querySelector("#se-dbg-cMenu-addConditionalBreakpoint");
+ let editConditionalBreakpointMenuItem = popup.querySelector("#se-dbg-cMenu-editConditionalBreakpoint");
+
+ if (breakpoint && !!breakpoint.condition) {
+ editConditionalBreakpointMenuItem.removeAttribute("hidden");
+ addConditionalBreakpointMenuItem.setAttribute("hidden", true);
+ }
+ else {
+ addConditionalBreakpointMenuItem.removeAttribute("hidden");
+ editConditionalBreakpointMenuItem.setAttribute("hidden", true);
+ }
+ },
+
+ /**
+ * The click listener for a breakpoint container.
+ */
+ _onBreakpointClick: function (e) {
+ let sourceItem = this.getItemForElement(e.target);
+ let breakpointItem = this.getItemForElement.call(sourceItem, e.target);
+ let attachment = breakpointItem.attachment;
+ let bp = getBreakpoint(this.getState(), attachment);
+ if (bp) {
+ this.highlightBreakpoint(bp.location, {
+ openPopup: bp.condition && e.button == 0
+ });
+ } else {
+ this.highlightBreakpoint(bp.location);
+ }
+ },
+
+ /**
+ * The click listener for a breakpoint checkbox.
+ */
+ _onBreakpointCheckboxClick: function (e) {
+ let sourceItem = this.getItemForElement(e.target);
+ let breakpointItem = this.getItemForElement.call(sourceItem, e.target);
+ let bp = getBreakpoint(this.getState(), breakpointItem.attachment);
+
+ if (bp.disabled) {
+ this.actions.enableBreakpoint(bp.location);
+ }
+ else {
+ this.actions.disableBreakpoint(bp.location);
+ }
+
+ // Don't update the editor location (avoid propagating into _onBreakpointClick).
+ e.preventDefault();
+ e.stopPropagation();
+ },
+
+ /**
+ * The popup showing listener for the breakpoints conditional expression panel.
+ */
+ _onConditionalPopupShowing: function () {
+ this._conditionalPopupVisible = true; // Used in tests.
+ },
+
+ /**
+ * The popup shown listener for the breakpoints conditional expression panel.
+ */
+ _onConditionalPopupShown: function () {
+ this._cbTextbox.focus();
+ this._cbTextbox.select();
+ window.emit(EVENTS.CONDITIONAL_BREAKPOINT_POPUP_SHOWN);
+ },
+
+ /**
+ * The popup hiding listener for the breakpoints conditional expression panel.
+ */
+ _onConditionalPopupHiding: function () {
+ this._conditionalPopupVisible = false; // Used in tests.
+
+ // Check if this is an enabled conditional breakpoint, and if so,
+ // save the current conditional expression.
+ let bp = this._selectedBreakpoint;
+ if (bp) {
+ let condition = this._cbTextbox.value;
+ this.actions.setBreakpointCondition(bp.location, condition);
+ }
+ },
+
+ /**
+ * The popup hidden listener for the breakpoints conditional expression panel.
+ */
+ _onConditionalPopupHidden: function () {
+ this._cbPanel.hidden = true;
+ window.emit(EVENTS.CONDITIONAL_BREAKPOINT_POPUP_HIDDEN);
+ },
+
+ /**
+ * The keypress listener for the breakpoints conditional expression textbox.
+ */
+ _onConditionalTextboxKeyPress: function (e) {
+ if (e.keyCode == KeyCodes.DOM_VK_RETURN) {
+ this._hideConditionalPopup();
+ }
+ },
+
+ /**
+ * Called when the add breakpoint key sequence was pressed.
+ */
+ _onCmdAddBreakpoint: function (e) {
+ let actor = this.selectedValue;
+ let line = (this.DebuggerView.clickedLine ?
+ this.DebuggerView.clickedLine + 1 :
+ this.DebuggerView.editor.getCursor().line + 1);
+ let location = { actor, line };
+ let bp = getBreakpoint(this.getState(), location);
+
+ // If a breakpoint already existed, remove it now.
+ if (bp) {
+ this.actions.removeBreakpoint(bp.location);
+ }
+ // No breakpoint existed at the required location, add one now.
+ else {
+ this.actions.addBreakpoint(location);
+ }
+ },
+
+ /**
+ * Called when the add conditional breakpoint key sequence was pressed.
+ */
+ _onCmdAddConditionalBreakpoint: function (e) {
+ let actor = this.selectedValue;
+ let line = (this.DebuggerView.clickedLine ?
+ this.DebuggerView.clickedLine + 1 :
+ this.DebuggerView.editor.getCursor().line + 1);
+
+ let location = { actor, line };
+ let bp = getBreakpoint(this.getState(), location);
+
+ // If a breakpoint already existed or wasn't a conditional, morph it now.
+ if (bp) {
+ this.highlightBreakpoint(bp.location, { openPopup: true });
+ }
+ // No breakpoint existed at the required location, add one now.
+ else {
+ this.actions.addBreakpoint(location, "");
+ }
+ },
+
+ getOtherBreakpoints: function (location) {
+ const bps = getBreakpoints(this.getState());
+ if (location) {
+ return bps.filter(bp => {
+ return (bp.location.actor !== location.actor ||
+ bp.location.line !== location.line);
+ });
+ }
+ return bps;
+ },
+
+ /**
+ * Function invoked on the "setConditional" menuitem command.
+ *
+ * @param object aLocation
+ * @see DebuggerController.Breakpoints.addBreakpoint
+ */
+ _onSetConditional: function (aLocation) {
+ // Highlight the breakpoint and show a conditional expression popup.
+ this.highlightBreakpoint(aLocation, { openPopup: true });
+ },
+
+ /**
+ * Function invoked on the "enableSelf" menuitem command.
+ *
+ * @param object aLocation
+ * @see DebuggerController.Breakpoints.addBreakpoint
+ */
+ _onEnableSelf: function (aLocation) {
+ // Enable the breakpoint, in this container and the controller store.
+ this.actions.enableBreakpoint(aLocation);
+ },
+
+ /**
+ * Function invoked on the "disableSelf" menuitem command.
+ *
+ * @param object aLocation
+ * @see DebuggerController.Breakpoints.addBreakpoint
+ */
+ _onDisableSelf: function (aLocation) {
+ const bp = getBreakpoint(this.getState(), aLocation);
+ if (!bp.disabled) {
+ this.actions.disableBreakpoint(aLocation);
+ }
+ },
+
+ /**
+ * Function invoked on the "deleteSelf" menuitem command.
+ *
+ * @param object aLocation
+ * @see DebuggerController.Breakpoints.addBreakpoint
+ */
+ _onDeleteSelf: function (aLocation) {
+ this.actions.removeBreakpoint(aLocation);
+ },
+
+ /**
+ * Function invoked on the "enableOthers" menuitem command.
+ *
+ * @param object aLocation
+ * @see DebuggerController.Breakpoints.addBreakpoint
+ */
+ _onEnableOthers: function (aLocation) {
+ let other = this.getOtherBreakpoints(aLocation);
+ // TODO(jwl): batch these and interrupt the thread for all of them
+ other.forEach(bp => this._onEnableSelf(bp.location));
+ },
+
+ /**
+ * Function invoked on the "disableOthers" menuitem command.
+ *
+ * @param object aLocation
+ * @see DebuggerController.Breakpoints.addBreakpoint
+ */
+ _onDisableOthers: function (aLocation) {
+ let other = this.getOtherBreakpoints(aLocation);
+ other.forEach(bp => this._onDisableSelf(bp.location));
+ },
+
+ /**
+ * Function invoked on the "deleteOthers" menuitem command.
+ *
+ * @param object aLocation
+ * @see DebuggerController.Breakpoints.addBreakpoint
+ */
+ _onDeleteOthers: function (aLocation) {
+ let other = this.getOtherBreakpoints(aLocation);
+ other.forEach(bp => this._onDeleteSelf(bp.location));
+ },
+
+ /**
+ * Function invoked on the "enableAll" menuitem command.
+ */
+ _onEnableAll: function () {
+ this._onEnableOthers(undefined);
+ },
+
+ /**
+ * Function invoked on the "disableAll" menuitem command.
+ */
+ _onDisableAll: function () {
+ this._onDisableOthers(undefined);
+ },
+
+ /**
+ * Function invoked on the "deleteAll" menuitem command.
+ */
+ _onDeleteAll: function () {
+ this._onDeleteOthers(undefined);
+ },
+
+ _commandset: null,
+ _popupset: null,
+ _cmPopup: null,
+ _cbPanel: null,
+ _cbTextbox: null,
+ _selectedBreakpointItem: null,
+ _conditionalPopupVisible: false,
+ _noResultsFoundToolTip: null,
+ _markedIdentifier: null,
+ _selectedBreakpoint: null,
+ _conditionalPopupVisible: false
+});
+
+module.exports = SourcesView;