Bug 1200798 - refactor sources and breakpoints in debugger to use redux r=ejpbruel
This commit is contained in:
169
devtools/client/debugger/content/actions/breakpoints.js
Normal file
169
devtools/client/debugger/content/actions/breakpoints.js
Normal 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
|
||||
}
|
||||
@@ -4,5 +4,7 @@
|
||||
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
DevToolsModules(
|
||||
'breakpoints.js',
|
||||
'event-listeners.js',
|
||||
'sources.js'
|
||||
)
|
||||
|
||||
283
devtools/client/debugger/content/actions/sources.js
Normal file
283
devtools/client/debugger/content/actions/sources.js
Normal 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
|
||||
};
|
||||
@@ -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';
|
||||
|
||||
@@ -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 };
|
||||
|
||||
@@ -12,5 +12,6 @@ DIRS += [
|
||||
DevToolsModules(
|
||||
'constants.js',
|
||||
'globalActions.js',
|
||||
'queries.js',
|
||||
'utils.js'
|
||||
)
|
||||
|
||||
70
devtools/client/debugger/content/queries.js
Normal file
70
devtools/client/debugger/content/queries.js
Normal 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
|
||||
};
|
||||
31
devtools/client/debugger/content/reducers/async-requests.js
Normal file
31
devtools/client/debugger/content/reducers/async-requests.js
Normal 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;
|
||||
128
devtools/client/debugger/content/reducers/breakpoints.js
Normal file
128
devtools/client/debugger/content/reducers/breakpoints.js
Normal 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;
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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
|
||||
};
|
||||
|
||||
@@ -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'
|
||||
)
|
||||
|
||||
117
devtools/client/debugger/content/reducers/sources.js
Normal file
117
devtools/client/debugger/content/reducers/sources.js
Normal 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;
|
||||
@@ -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
|
||||
};
|
||||
|
||||
@@ -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: "",
|
||||
|
||||
@@ -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'
|
||||
)
|
||||
|
||||
1317
devtools/client/debugger/content/views/sources-view.js
Normal file
1317
devtools/client/debugger/content/views/sources-view.js
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user