Bug 1200798 - refactor sources and breakpoints in debugger to use redux r=ejpbruel

This commit is contained in:
James Long
2015-11-29 14:40:51 -05:00
parent 06aa460336
commit 267225b263
132 changed files with 6399 additions and 6693 deletions

View File

@@ -0,0 +1,169 @@
/* 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, rdpInvoke } = require('../utils');
const { PROMISE } = require('devtools/client/shared/redux/middleware/promise');
const {
getSource, getBreakpoint, getBreakpoints, makeLocationId
} = require('../queries');
// 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 rdpInvoke(sourceClient, 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(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);
return dispatch({
type: constants.REMOVE_BREAKPOINT,
breakpoint: bp,
disabled: isDisabled,
[PROMISE]: rdpInvoke(bpClient, bpClient.remove)
});
}
}
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);
return dispatch({
type: constants.SET_BREAKPOINT_CONDITION,
breakpoint: bp,
condition: condition,
[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 };
})
});
};
}
module.exports = {
enableBreakpoint,
addBreakpoint,
disableBreakpoint,
removeBreakpoint,
removeAllBreakpoints,
setBreakpointCondition
}

View File

@@ -4,5 +4,7 @@
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
DevToolsModules(
'breakpoints.js',
'event-listeners.js',
'sources.js'
)

View File

@@ -0,0 +1,283 @@
/* 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 { rdpInvoke } = require('../utils');
const { dumpn } = require("devtools/shared/DevToolsUtils");
const { PROMISE, HISTOGRAM_ID } = require('devtools/client/shared/redux/middleware/promise');
const { getSource, getSourceText } = require('../queries');
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 rdpInvoke(gThreadClient, 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 rdpInvoke(client,
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 rdpInvoke(sourceClient,
sourceClient.prettyPrint,
Prefs.editorTabSize);
}
else {
response = yield rdpInvoke(sourceClient,
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 rdpInvoke(sourceClient, 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
};

View File

@@ -5,3 +5,19 @@
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';

View File

@@ -5,5 +5,12 @@
const constants = require('./constants');
// No global actions right now, but I'm sure there will be soon.
module.exports = {};
// 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 };

View File

@@ -12,5 +12,6 @@ DIRS += [
DevToolsModules(
'constants.js',
'globalActions.js',
'queries.js',
'utils.js'
)

View File

@@ -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
};

View File

@@ -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;

View File

@@ -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, deleteIn } = require('../utils');
const { makeLocationId } = require('../queries');
const initialState = Immutable({
breakpoints: {}
});
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,
condition: action.condition || bp.condition || undefined
}));
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];
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,
// 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') {
return deleteIn(state, ['breakpoints', id]);
}
break;
}}
return state;
}
module.exports = update;

View File

@@ -17,7 +17,7 @@ function update(state = initialState, action, emit) {
switch(action.type) {
case constants.UPDATE_EVENT_BREAKPOINTS:
state.activeEventNames = action.eventNames;
emit("@redux:activeEventNames", state.activeEventNames);
emit("activeEventNames", state.activeEventNames);
break;
case constants.FETCH_EVENT_LISTENERS:
if (action.status === "begin") {
@@ -26,7 +26,7 @@ function update(state = initialState, action, emit) {
else if (action.status === "done") {
state.fetchingListeners = false;
state.listeners = action.listeners;
emit("@redux:listeners", state.listeners);
emit("event-listeners", state.listeners);
}
break;
}

View File

@@ -4,5 +4,13 @@
"use strict";
const eventListeners = require('./event-listeners');
const sources = require('./sources');
const breakpoints = require('./breakpoints');
const asyncRequests = require('./async-requests');
exports.eventListeners = eventListeners;
module.exports = {
eventListeners,
sources,
breakpoints,
asyncRequests
};

View File

@@ -4,6 +4,9 @@
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
DevToolsModules(
'async-requests.js',
'breakpoints.js',
'event-listeners.js',
'index.js'
'index.js',
'sources.js'
)

View File

@@ -0,0 +1,117 @@
/* 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') {
// We don't need to actually load the sources into the state.
// Loading sources actually forces the server to emit several
// individual newSources packets which will eventually fire
// ADD_SOURCE actions.
//
// We still emit this event so that the UI can show an "empty
// text" label if no sources were loaded.
emitChange('sources', state.sources);
}
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 way before.
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;

View File

@@ -6,16 +6,21 @@
const { promiseInvoke } = require("devtools/shared/async-utils");
const { reportException } = require("devtools/shared/DevToolsUtils");
// RDP utils
function rdpInvoke(client, method, ...args) {
return promiseInvoke(client, method, ...args)
return (promiseInvoke(client, method, ...args)
.then((packet) => {
let { error, message } = packet;
if (error) {
throw new Error(error + ": " + message);
if (packet.error) {
let { error, message } = packet;
const err = new Error(error + ": " + message);
err.rdpError = error;
err.rdpMessage = message;
throw err;
}
return packet;
});
}));
}
function asPaused(client, func) {
@@ -42,4 +47,58 @@ function asPaused(client, func) {
}
}
module.exports = { rdpInvoke, asPaused };
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 = {
rdpInvoke,
asPaused,
handleError,
onReducerEvents,
mergeIn,
setIn,
updateIn,
deleteIn
};

View File

@@ -9,19 +9,16 @@ const { bindActionCreators } = require('devtools/client/shared/vendor/redux');
/**
* Functions handling the event listeners UI.
*/
function EventListenersView(store, DebuggerController) {
function EventListenersView(controller) {
dumpn("EventListenersView was instantiated");
this.actions = bindActionCreators(actions, store.dispatch);
this.getState = () => store.getState().eventListeners;
this.actions = bindActionCreators(actions, controller.dispatch);
this.getState = () => controller.getState().eventListeners;
this._onCheck = this._onCheck.bind(this);
this._onClick = this._onClick.bind(this);
this._onListeners = this._onListeners.bind(this);
this.Breakpoints = DebuggerController.Breakpoints;
this.controller = DebuggerController;
this.controller.on("@redux:listeners", this._onListeners);
controller.onChange("event-listeners", this.renderListeners.bind(this));
}
EventListenersView.prototype = Heritage.extend(WidgetMethods, {
@@ -52,10 +49,8 @@ EventListenersView.prototype = Heritage.extend(WidgetMethods, {
destroy: function() {
dumpn("Destroying the EventListenersView");
this.controller.off("@redux:listeners", this._onListeners);
this.widget.removeEventListener("check", this._onCheck, false);
this.widget.removeEventListener("click", this._onClick, false);
this.controller = this.Breakpoints = null;
},
renderListeners: function(listeners) {
@@ -286,13 +281,6 @@ EventListenersView.prototype = Heritage.extend(WidgetMethods, {
}
},
/**
* Called when listeners change.
*/
_onListeners: function(_, listeners) {
this.renderListeners(listeners);
},
_eventCheckboxTooltip: "",
_onSelectorString: "",
_inSourceString: "",

View File

@@ -4,5 +4,6 @@
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
DevToolsModules(
'event-listeners-view.js'
'event-listeners-view.js',
'sources-view.js'
)

File diff suppressed because it is too large Load Diff