mirror of
https://gitlab.winehq.org/wine/wine-gecko.git
synced 2024-09-13 09:24:08 -07:00
1747 lines
53 KiB
JavaScript
1747 lines
53 KiB
JavaScript
/* -*- Mode: javascript; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 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 Cc = Components.classes;
|
|
const Ci = Components.interfaces;
|
|
const Cu = Components.utils;
|
|
|
|
const FRAME_STEP_CACHE_DURATION = 100; // ms
|
|
const DBG_STRINGS_URI = "chrome://browser/locale/devtools/debugger.properties";
|
|
const SYNTAX_HIGHLIGHT_MAX_FILE_SIZE = 1048576; // 1 MB in bytes
|
|
|
|
Cu.import("resource:///modules/source-editor.jsm");
|
|
Cu.import("resource://gre/modules/devtools/dbg-server.jsm");
|
|
Cu.import("resource://gre/modules/devtools/dbg-client.jsm");
|
|
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
|
|
Cu.import("resource://gre/modules/NetUtil.jsm");
|
|
Cu.import('resource://gre/modules/Services.jsm');
|
|
|
|
/**
|
|
* Controls the debugger view by handling the source scripts, the current
|
|
* thread state and thread stack frame cache.
|
|
*/
|
|
let DebuggerController = {
|
|
|
|
/**
|
|
* Makes a few preliminary changes and bindings to the controller.
|
|
*/
|
|
init: function() {
|
|
this._startupDebugger = this._startupDebugger.bind(this);
|
|
this._shutdownDebugger = this._shutdownDebugger.bind(this);
|
|
this._onTabNavigated = this._onTabNavigated.bind(this);
|
|
this._onTabDetached = this._onTabDetached.bind(this);
|
|
|
|
window.addEventListener("DOMContentLoaded", this._startupDebugger, true);
|
|
window.addEventListener("unload", this._shutdownDebugger, true);
|
|
},
|
|
|
|
/**
|
|
* Initializes the debugger view and connects a debugger client to the server.
|
|
*/
|
|
_startupDebugger: function DC__startupDebugger() {
|
|
if (this._isInitialized) {
|
|
return;
|
|
}
|
|
this._isInitialized = true;
|
|
window.removeEventListener("DOMContentLoaded", this._startupDebugger, true);
|
|
|
|
DebuggerView.initializePanes();
|
|
DebuggerView.initializeEditor();
|
|
DebuggerView.StackFrames.initialize();
|
|
DebuggerView.Breakpoints.initialize();
|
|
DebuggerView.Properties.initialize();
|
|
DebuggerView.Scripts.initialize();
|
|
DebuggerView.showCloseButton(!this._isRemoteDebugger && !this._isChromeDebugger);
|
|
|
|
this.dispatchEvent("Debugger:Loaded");
|
|
this._connect();
|
|
},
|
|
|
|
/**
|
|
* Destroys the debugger view, disconnects the debugger client and cleans up
|
|
* any active listeners.
|
|
*/
|
|
_shutdownDebugger: function DC__shutdownDebugger() {
|
|
if (this._isDestroyed) {
|
|
return;
|
|
}
|
|
this._isDestroyed = true;
|
|
window.removeEventListener("unload", this._shutdownDebugger, true);
|
|
|
|
DebuggerView.Scripts.destroy();
|
|
DebuggerView.StackFrames.destroy();
|
|
DebuggerView.Breakpoints.destroy();
|
|
DebuggerView.Properties.destroy();
|
|
DebuggerView.destroyPanes();
|
|
DebuggerView.destroyEditor();
|
|
|
|
DebuggerController.SourceScripts.disconnect();
|
|
DebuggerController.StackFrames.disconnect();
|
|
DebuggerController.ThreadState.disconnect();
|
|
|
|
this.dispatchEvent("Debugger:Unloaded");
|
|
this._disconnect();
|
|
this._isChromeDebugger && this._quitApp();
|
|
},
|
|
|
|
/**
|
|
* Prepares the hostname and port number for a remote debugger connection
|
|
* and handles connection retries and timeouts.
|
|
*
|
|
* @return boolean true if connection should proceed normally
|
|
*/
|
|
_prepareConnection: function DC__prepareConnection() {
|
|
// If we exceeded the total number of connection retries, bail.
|
|
if (this._remoteConnectionTry === Prefs.remoteConnectionRetries) {
|
|
Services.prompt.alert(null,
|
|
L10N.getStr("remoteDebuggerPromptTitle"),
|
|
L10N.getStr("remoteDebuggerConnectionFailedMessage"));
|
|
this.dispatchEvent("Debugger:Close");
|
|
return false;
|
|
}
|
|
|
|
// TODO: This is ugly, need to rethink the design for the UI in #751677.
|
|
if (!Prefs.remoteAutoConnect) {
|
|
let prompt = new RemoteDebuggerPrompt();
|
|
let result = prompt.show(!!this._remoteConnectionTimeout);
|
|
// If the connection was not established before the user canceled the
|
|
// prompt, close the remote debugger.
|
|
if (!result && !DebuggerController.activeThread) {
|
|
this.dispatchEvent("Debugger:Close");
|
|
return false;
|
|
}
|
|
Prefs.remoteHost = prompt.remote.host;
|
|
Prefs.remotePort = prompt.remote.port;
|
|
}
|
|
|
|
// If this debugger is connecting remotely to a server, we need to check
|
|
// after a while if the connection actually succeeded.
|
|
this._remoteConnectionTry = ++this._remoteConnectionTry || 1;
|
|
this._remoteConnectionTimeout = window.setTimeout(function() {
|
|
// If we couldn't connect to any server yet, try again...
|
|
if (!DebuggerController.activeThread) {
|
|
DebuggerController._onRemoteConnectionTimeout();
|
|
DebuggerController._connect();
|
|
}
|
|
}, Prefs.remoteTimeout);
|
|
|
|
return true;
|
|
},
|
|
|
|
/**
|
|
* Called when a remote connection timeout occurs.
|
|
*/
|
|
_onRemoteConnectionTimeout: function DC__onRemoteConnectionTimeout() {
|
|
Cu.reportError("Couldn't connect to " +
|
|
Prefs.remoteHost + ":" + Prefs.remotePort);
|
|
},
|
|
|
|
/**
|
|
* Initializes a debugger client and connects it to the debugger server,
|
|
* wiring event handlers as necessary.
|
|
*/
|
|
_connect: function DC__connect() {
|
|
if (this._isRemoteDebugger) {
|
|
if (!this._prepareConnection()) {
|
|
return;
|
|
}
|
|
}
|
|
|
|
let transport = (this._isChromeDebugger || this._isRemoteDebugger)
|
|
? debuggerSocketConnect(Prefs.remoteHost, Prefs.remotePort)
|
|
: DebuggerServer.connectPipe();
|
|
|
|
let client = this.client = new DebuggerClient(transport);
|
|
|
|
client.addListener("tabNavigated", this._onTabNavigated);
|
|
client.addListener("tabDetached", this._onTabDetached);
|
|
|
|
client.connect(function(aType, aTraits) {
|
|
client.listTabs(function(aResponse) {
|
|
let tab = aResponse.tabs[aResponse.selected];
|
|
this._startDebuggingTab(client, tab);
|
|
this.dispatchEvent("Debugger:Connecting");
|
|
}.bind(this));
|
|
}.bind(this));
|
|
},
|
|
|
|
/**
|
|
* Closes the debugger client and removes event handlers as necessary.
|
|
*/
|
|
_disconnect: function DC__disconnect() {
|
|
this.client.removeListener("tabNavigated", this._onTabNavigated);
|
|
this.client.removeListener("tabDetached", this._onTabDetached);
|
|
this.client.close();
|
|
|
|
this.client = null;
|
|
this.tabClient = null;
|
|
this.activeThread = null;
|
|
},
|
|
|
|
/**
|
|
* Starts debugging the current tab. This function is called on each location
|
|
* change in this tab.
|
|
*/
|
|
_onTabNavigated: function DC__onTabNavigated(aNotification, aPacket) {
|
|
let client = this.client;
|
|
|
|
client.activeThread.detach(function() {
|
|
client.activeTab.detach(function() {
|
|
client.listTabs(function(aResponse) {
|
|
let tab = aResponse.tabs[aResponse.selected];
|
|
this._startDebuggingTab(client, tab);
|
|
this.dispatchEvent("Debugger:Connecting");
|
|
}.bind(this));
|
|
}.bind(this));
|
|
}.bind(this));
|
|
},
|
|
|
|
/**
|
|
* Stops debugging the current tab.
|
|
*/
|
|
_onTabDetached: function DC__onTabDetached() {
|
|
this.dispatchEvent("Debugger:Close");
|
|
},
|
|
|
|
/**
|
|
* Sets up a debugging session.
|
|
*
|
|
* @param DebuggerClient aClient
|
|
* The debugger client.
|
|
* @param object aTabGrip
|
|
* The remote protocol grip of the tab.
|
|
*/
|
|
_startDebuggingTab: function DC__startDebuggingTab(aClient, aTabGrip) {
|
|
if (!aClient) {
|
|
Cu.reportError("No client found!");
|
|
return;
|
|
}
|
|
this.client = aClient;
|
|
|
|
aClient.attachTab(aTabGrip.actor, function(aResponse, aTabClient) {
|
|
if (!aTabClient) {
|
|
Cu.reportError("No tab client found!");
|
|
return;
|
|
}
|
|
this.tabClient = aTabClient;
|
|
|
|
aClient.attachThread(aResponse.threadActor, function(aResponse, aThreadClient) {
|
|
if (!aThreadClient) {
|
|
Cu.reportError("Couldn't attach to thread: " + aResponse.error);
|
|
return;
|
|
}
|
|
this.activeThread = aThreadClient;
|
|
|
|
DebuggerController.ThreadState.connect(function() {
|
|
DebuggerController.StackFrames.connect(function() {
|
|
DebuggerController.SourceScripts.connect(function() {
|
|
aThreadClient.resume();
|
|
});
|
|
});
|
|
});
|
|
|
|
}.bind(this));
|
|
}.bind(this));
|
|
},
|
|
|
|
/**
|
|
* Returns true if this is a remote debugger instance.
|
|
* @return boolean
|
|
*/
|
|
get _isRemoteDebugger() {
|
|
return window._remoteFlag;
|
|
},
|
|
|
|
/**
|
|
* Returns true if this is a chrome debugger instance.
|
|
* @return boolean
|
|
*/
|
|
get _isChromeDebugger() {
|
|
// Directly accessing window.parent.content may throw in some cases.
|
|
return !("content" in window.parent) && !this._isRemoteDebugger;
|
|
},
|
|
|
|
/**
|
|
* Attempts to quit the current process if allowed.
|
|
*/
|
|
_quitApp: function DC__quitApp() {
|
|
let canceled = Cc["@mozilla.org/supports-PRBool;1"]
|
|
.createInstance(Ci.nsISupportsPRBool);
|
|
|
|
Services.obs.notifyObservers(canceled, "quit-application-requested", null);
|
|
|
|
// Somebody canceled our quit request.
|
|
if (canceled.data) {
|
|
return;
|
|
}
|
|
|
|
Services.startup.quit(Ci.nsIAppStartup.eAttemptQuit);
|
|
},
|
|
|
|
/**
|
|
* Convenience method, dispatching a custom event.
|
|
*
|
|
* @param string aType
|
|
* The name of the event.
|
|
* @param string aDetail
|
|
* The data passed when initializing the event.
|
|
*/
|
|
dispatchEvent: function DC_dispatchEvent(aType, aDetail) {
|
|
let evt = document.createEvent("CustomEvent");
|
|
evt.initCustomEvent(aType, true, false, aDetail);
|
|
document.documentElement.dispatchEvent(evt);
|
|
}
|
|
};
|
|
|
|
/**
|
|
* ThreadState keeps the UI up to date with the state of the
|
|
* thread (paused/attached/etc.).
|
|
*/
|
|
function ThreadState() {
|
|
this._update = this._update.bind(this);
|
|
}
|
|
|
|
ThreadState.prototype = {
|
|
|
|
/**
|
|
* Gets the current thread the client has connected to.
|
|
*/
|
|
get activeThread() {
|
|
return DebuggerController.activeThread;
|
|
},
|
|
|
|
/**
|
|
* Connect to the current thread client.
|
|
*
|
|
* @param function aCallback
|
|
* The next function in the initialization sequence.
|
|
*/
|
|
connect: function TS_connect(aCallback) {
|
|
this.activeThread.addListener("paused", this._update);
|
|
this.activeThread.addListener("resumed", this._update);
|
|
this.activeThread.addListener("detached", this._update);
|
|
|
|
this._update();
|
|
|
|
aCallback && aCallback();
|
|
},
|
|
|
|
/**
|
|
* Disconnect from the client.
|
|
*/
|
|
disconnect: function TS_disconnect() {
|
|
if (!this.activeThread) {
|
|
return;
|
|
}
|
|
this.activeThread.removeListener("paused", this._update);
|
|
this.activeThread.removeListener("resumed", this._update);
|
|
this.activeThread.removeListener("detached", this._update);
|
|
},
|
|
|
|
/**
|
|
* Update the UI after a thread state change.
|
|
*/
|
|
_update: function TS__update(aEvent) {
|
|
DebuggerView.StackFrames.updateState(this.activeThread.state);
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Keeps the stack frame list up-to-date, using the thread client's stack frame
|
|
* cache.
|
|
*/
|
|
function StackFrames() {
|
|
this._onPaused = this._onPaused.bind(this);
|
|
this._onResume = this._onResume.bind(this);
|
|
this._onFrames = this._onFrames.bind(this);
|
|
this._onFramesCleared = this._onFramesCleared.bind(this);
|
|
this._afterFramesCleared = this._afterFramesCleared.bind(this);
|
|
}
|
|
|
|
StackFrames.prototype = {
|
|
|
|
/**
|
|
* The maximum number of frames allowed to be loaded at a time.
|
|
*/
|
|
pageSize: 25,
|
|
|
|
/**
|
|
* The currently selected frame depth.
|
|
*/
|
|
selectedFrame: null,
|
|
|
|
/**
|
|
* A flag that defines whether the debuggee will pause whenever an exception
|
|
* is thrown.
|
|
*/
|
|
pauseOnExceptions: false,
|
|
|
|
/**
|
|
* Gets the current thread the client has connected to.
|
|
*/
|
|
get activeThread() {
|
|
return DebuggerController.activeThread;
|
|
},
|
|
|
|
/**
|
|
* Watch the given thread client.
|
|
*
|
|
* @param function aCallback
|
|
* The next function in the initialization sequence.
|
|
*/
|
|
connect: function SF_connect(aCallback) {
|
|
window.addEventListener("Debugger:FetchedVariables", this._onFetchedVars, false);
|
|
|
|
this.activeThread.addListener("paused", this._onPaused);
|
|
this.activeThread.addListener("resumed", this._onResume);
|
|
this.activeThread.addListener("framesadded", this._onFrames);
|
|
this.activeThread.addListener("framescleared", this._onFramesCleared);
|
|
|
|
this.updatePauseOnExceptions(this.pauseOnExceptions);
|
|
|
|
aCallback && aCallback();
|
|
},
|
|
|
|
/**
|
|
* Disconnect from the client.
|
|
*/
|
|
disconnect: function SF_disconnect() {
|
|
window.removeEventListener("Debugger:FetchedVariables", this._onFetchedVars, false);
|
|
|
|
if (!this.activeThread) {
|
|
return;
|
|
}
|
|
this.activeThread.removeListener("paused", this._onPaused);
|
|
this.activeThread.removeListener("resumed", this._onResume);
|
|
this.activeThread.removeListener("framesadded", this._onFrames);
|
|
this.activeThread.removeListener("framescleared", this._onFramesCleared);
|
|
},
|
|
|
|
/**
|
|
* Handler for the thread client's paused notification.
|
|
*
|
|
* @param string aEvent
|
|
* The name of the notification ("paused" in this case).
|
|
* @param object aPacket
|
|
* The response packet.
|
|
*/
|
|
_onPaused: function SF__onPaused(aEvent, aPacket) {
|
|
// In case the pause was caused by an exception, store the exception value.
|
|
if (aPacket.why.type == "exception") {
|
|
this.exception = aPacket.why.exception;
|
|
}
|
|
this.activeThread.fillFrames(this.pageSize);
|
|
},
|
|
|
|
/**
|
|
* Handler for the thread client's resumed notification.
|
|
*/
|
|
_onResume: function SF__onResume() {
|
|
DebuggerView.editor.setDebugLocation(-1);
|
|
},
|
|
|
|
/**
|
|
* Handler for the thread client's framesadded notification.
|
|
*/
|
|
_onFrames: function SF__onFrames() {
|
|
if (!this.activeThread.cachedFrames.length) {
|
|
DebuggerView.StackFrames.emptyText();
|
|
DebuggerView.Properties.emptyText();
|
|
return;
|
|
}
|
|
DebuggerView.StackFrames.empty();
|
|
DebuggerView.Properties.empty();
|
|
|
|
for each (let frame in this.activeThread.cachedFrames) {
|
|
this._addFrame(frame);
|
|
}
|
|
if (!this.selectedFrame) {
|
|
this.selectFrame(0);
|
|
}
|
|
if (this.activeThread.moreFrames) {
|
|
DebuggerView.StackFrames.dirty = true;
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Handler for the thread client's framescleared notification.
|
|
*/
|
|
_onFramesCleared: function SF__onFramesCleared() {
|
|
this.selectedFrame = null;
|
|
this.exception = null;
|
|
// After each frame step (in, over, out), framescleared is fired, which
|
|
// forces the UI to be emptied and rebuilt on framesadded. Most of the times
|
|
// this is not necessary, and will result in a brief redraw flicker.
|
|
// To avoid it, invalidate the UI only after a short time if necessary.
|
|
window.setTimeout(this._afterFramesCleared, FRAME_STEP_CACHE_DURATION);
|
|
},
|
|
|
|
/**
|
|
* Called soon after the thread client's framescleared notification.
|
|
*/
|
|
_afterFramesCleared: function SF__afterFramesCleared() {
|
|
if (!this.activeThread.cachedFrames.length) {
|
|
DebuggerView.StackFrames.emptyText();
|
|
DebuggerView.Properties.emptyText();
|
|
DebuggerController.dispatchEvent("Debugger:AfterFramesCleared");
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Update the source editor's current debug location based on the selected
|
|
* frame and script.
|
|
*/
|
|
updateEditorLocation: function SF_updateEditorLocation() {
|
|
let frame = this.activeThread.cachedFrames[this.selectedFrame];
|
|
if (!frame) {
|
|
return;
|
|
}
|
|
|
|
let url = frame.where.url;
|
|
let line = frame.where.line;
|
|
let editor = DebuggerView.editor;
|
|
|
|
this.updateEditorToLocation(url, line, true);
|
|
},
|
|
|
|
/**
|
|
* Update the source editor's current caret and debug location based on
|
|
* a specified url and line.
|
|
*
|
|
* @param string aUrl
|
|
* The target source url.
|
|
* @param number aLine
|
|
* The target line number in the source.
|
|
* @param boolean aNoSwitch
|
|
* Pass true to not switch to the script if not currently selected.
|
|
* @param boolean aNoCaretFlag
|
|
* Pass true to not set the caret location at the specified line.
|
|
* @param boolean aNoDebugFlag
|
|
* Pass true to not set the debug location at the specified line.
|
|
*/
|
|
updateEditorToLocation:
|
|
function SF_updateEditorToLocation(aUrl, aLine, aNoSwitch, aNoCaretFlag, aNoDebugFlag) {
|
|
let editor = DebuggerView.editor;
|
|
|
|
function set() {
|
|
if (!aNoCaretFlag) {
|
|
editor.setCaretPosition(aLine - 1);
|
|
}
|
|
if (!aNoDebugFlag) {
|
|
editor.setDebugLocation(aLine - 1);
|
|
}
|
|
}
|
|
|
|
// Move the editor's caret to the proper url and line.
|
|
if (DebuggerView.Scripts.isSelected(aUrl)) {
|
|
return set();
|
|
}
|
|
if (!aNoSwitch && DebuggerView.Scripts.contains(aUrl)) {
|
|
DebuggerView.Scripts.selectScript(aUrl);
|
|
return set();
|
|
}
|
|
editor.setCaretPosition(-1);
|
|
editor.setDebugLocation(-1);
|
|
},
|
|
|
|
/**
|
|
* Inform the debugger client whether the debuggee should be paused whenever
|
|
* an exception is thrown.
|
|
*
|
|
* @param boolean aFlag
|
|
* The new value of the flag: true for pausing, false otherwise.
|
|
*/
|
|
updatePauseOnExceptions: function SF_updatePauseOnExceptions(aFlag) {
|
|
this.pauseOnExceptions = aFlag;
|
|
this.activeThread.pauseOnExceptions(this.pauseOnExceptions);
|
|
},
|
|
|
|
/**
|
|
* Marks the stack frame in the specified depth as selected and updates the
|
|
* properties view with the stack frame's data.
|
|
*
|
|
* @param number aDepth
|
|
* The depth of the frame in the stack.
|
|
*/
|
|
selectFrame: function SF_selectFrame(aDepth) {
|
|
// Deselect any previously highlighted frame.
|
|
if (this.selectedFrame !== null) {
|
|
DebuggerView.StackFrames.unhighlightFrame(this.selectedFrame);
|
|
}
|
|
|
|
// Highlight the current frame.
|
|
this.selectedFrame = aDepth;
|
|
DebuggerView.StackFrames.highlightFrame(this.selectedFrame);
|
|
|
|
let frame = this.activeThread.cachedFrames[aDepth];
|
|
if (!frame) {
|
|
return;
|
|
}
|
|
|
|
let url = frame.where.url;
|
|
let line = frame.where.line;
|
|
|
|
// Move the editor's caret to the proper line.
|
|
this.updateEditorToLocation(url, line);
|
|
|
|
// Start recording any added variables or properties in any scope.
|
|
DebuggerView.Properties.createHierarchyStore();
|
|
|
|
// Clear existing scopes and create each one dynamically.
|
|
DebuggerView.Properties.empty();
|
|
|
|
if (frame.environment) {
|
|
let env = frame.environment;
|
|
do {
|
|
// Construct the scope name.
|
|
let name = env.type.charAt(0).toUpperCase() + env.type.slice(1);
|
|
// Call the outermost scope Global.
|
|
if (!env.parent) {
|
|
name = L10N.getStr("globalScopeLabel");
|
|
}
|
|
let label = L10N.getFormatStr("scopeLabel", [name]);
|
|
switch (env.type) {
|
|
case "with":
|
|
case "object":
|
|
label += " [" + env.object.class + "]";
|
|
break;
|
|
case "function":
|
|
if (env.functionName) {
|
|
label += " [" + env.functionName + "]";
|
|
}
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
|
|
let scope = DebuggerView.Properties.addScope(label);
|
|
|
|
// Special additions to the innermost scope.
|
|
if (env == frame.environment) {
|
|
// Add any thrown exception.
|
|
if (aDepth == 0 && this.exception) {
|
|
let excVar = scope.addVar("<exception>");
|
|
if (typeof this.exception == "object") {
|
|
excVar.setGrip({
|
|
type: this.exception.type,
|
|
class: this.exception.class
|
|
});
|
|
this._addExpander(excVar, this.exception);
|
|
} else {
|
|
excVar.setGrip(this.exception);
|
|
}
|
|
}
|
|
|
|
// Add "this".
|
|
if (frame.this) {
|
|
let thisVar = scope.addVar("this");
|
|
thisVar.setGrip({
|
|
type: frame.this.type,
|
|
class: frame.this.class
|
|
});
|
|
this._addExpander(thisVar, frame.this);
|
|
}
|
|
|
|
// Expand the innermost scope by default.
|
|
scope.expand(true);
|
|
scope.addToHierarchy();
|
|
}
|
|
|
|
switch (env.type) {
|
|
case "with":
|
|
case "object":
|
|
let objClient = this.activeThread.pauseGrip(env.object);
|
|
objClient.getPrototypeAndProperties(function SF_getProps(aResponse) {
|
|
this._addScopeVariables(aResponse.ownProperties, scope);
|
|
// Signal that variables have been fetched.
|
|
DebuggerController.dispatchEvent("Debugger:FetchedVariables");
|
|
}.bind(this));
|
|
break;
|
|
case "block":
|
|
case "function":
|
|
// Add nodes for every argument.
|
|
let variables = env.bindings.arguments;
|
|
for each (let variable in variables) {
|
|
let name = Object.getOwnPropertyNames(variable)[0];
|
|
let paramVar = scope.addVar(name, variable[name]);
|
|
let paramVal = variable[name].value;
|
|
paramVar.setGrip(paramVal);
|
|
this._addExpander(paramVar, paramVal);
|
|
}
|
|
// Add nodes for every other variable in scope.
|
|
this._addScopeVariables(env.bindings.variables, scope);
|
|
break;
|
|
default:
|
|
Cu.reportError("Unknown Debugger.Environment type: " + env.type);
|
|
break;
|
|
}
|
|
} while (env = env.parent);
|
|
}
|
|
|
|
// Signal that variables have been fetched.
|
|
DebuggerController.dispatchEvent("Debugger:FetchedVariables");
|
|
},
|
|
|
|
/**
|
|
* Called afters variables have been fetched after a frame was selected.
|
|
*/
|
|
_onFetchedVars: function SF__onFetchedVars() {
|
|
DebuggerView.Properties.commitHierarchy();
|
|
},
|
|
|
|
/**
|
|
* Add nodes for every variable in scope.
|
|
*
|
|
* @param object aVariables
|
|
* The map of names to variables, as specified in the Remote
|
|
* Debugging Protocol.
|
|
* @param object aScope
|
|
* The scope where the nodes will be placed into.
|
|
*/
|
|
_addScopeVariables: function SF_addScopeVariables(aVariables, aScope) {
|
|
// Sort all of the variables before adding them, for better UX.
|
|
let variables = {};
|
|
for each (let prop in Object.keys(aVariables).sort()) {
|
|
variables[prop] = aVariables[prop];
|
|
}
|
|
|
|
// Add the sorted variables to the specified scope.
|
|
for (let variable in variables) {
|
|
let paramVar = aScope.addVar(variable, variables[variable]);
|
|
let paramVal = variables[variable].value;
|
|
paramVar.setGrip(paramVal);
|
|
this._addExpander(paramVar, paramVal);
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Adds an 'onexpand' callback for a variable, lazily handling the addition of
|
|
* new properties.
|
|
*/
|
|
_addExpander: function SF__addExpander(aVar, aObject) {
|
|
// No need for expansion for null and undefined values.
|
|
if (!aVar || !aObject || typeof aObject !== "object" ||
|
|
aObject.type !== "object") {
|
|
return;
|
|
}
|
|
|
|
// Force the twisty to show up.
|
|
aVar.forceShowArrow();
|
|
aVar.onexpand = this._addVarProperties.bind(this, aVar, aObject);
|
|
},
|
|
|
|
/**
|
|
* Adds properties to a variable in the view. Triggered when a variable is
|
|
* expanded.
|
|
*/
|
|
_addVarProperties: function SF__addVarProperties(aVar, aObject) {
|
|
// Retrieve the properties only once.
|
|
if (aVar.fetched) {
|
|
return;
|
|
}
|
|
|
|
let objClient = this.activeThread.pauseGrip(aObject);
|
|
objClient.getPrototypeAndProperties(function SF_onProtoAndProps(aResponse) {
|
|
// Sort all of the properties before adding them, for better UX.
|
|
let properties = {};
|
|
for each (let prop in Object.keys(aResponse.ownProperties).sort()) {
|
|
properties[prop] = aResponse.ownProperties[prop];
|
|
}
|
|
aVar.addProperties(properties);
|
|
|
|
// Expansion handlers must be set after the properties are added.
|
|
for (let prop in aResponse.ownProperties) {
|
|
this._addExpander(aVar[prop], aResponse.ownProperties[prop].value);
|
|
}
|
|
|
|
// Add __proto__.
|
|
if (aResponse.prototype.type !== "null") {
|
|
let properties = { "__proto__ ": { value: aResponse.prototype } };
|
|
aVar.addProperties(properties);
|
|
|
|
// Expansion handlers must be set after the properties are added.
|
|
this._addExpander(aVar["__proto__ "], aResponse.prototype);
|
|
}
|
|
aVar.fetched = true;
|
|
}.bind(this));
|
|
},
|
|
|
|
/**
|
|
* Adds the specified stack frame to the list.
|
|
*
|
|
* @param Debugger.Frame aFrame
|
|
* The new frame to add.
|
|
*/
|
|
_addFrame: function SF__addFrame(aFrame) {
|
|
let depth = aFrame.depth;
|
|
let label = DebuggerController.SourceScripts.getScriptLabel(aFrame.where.url);
|
|
|
|
let startText = this._getFrameTitle(aFrame);
|
|
let endText = label + ":" + aFrame.where.line;
|
|
|
|
let frame = DebuggerView.StackFrames.addFrame(depth, startText, endText);
|
|
if (frame) {
|
|
frame.debuggerFrame = aFrame;
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Loads more stack frames from the debugger server cache.
|
|
*/
|
|
addMoreFrames: function SF_addMoreFrames() {
|
|
this.activeThread.fillFrames(
|
|
this.activeThread.cachedFrames.length + this.pageSize);
|
|
},
|
|
|
|
/**
|
|
* Create a textual representation for the stack frame specified, for
|
|
* displaying in the stack frame list.
|
|
*
|
|
* @param Debugger.Frame aFrame
|
|
* The stack frame to label.
|
|
*/
|
|
_getFrameTitle: function SF__getFrameTitle(aFrame) {
|
|
if (aFrame.type == "call") {
|
|
return aFrame["calleeName"] ? aFrame["calleeName"] : "(anonymous)";
|
|
}
|
|
return "(" + aFrame.type + ")";
|
|
},
|
|
|
|
/**
|
|
* Evaluate an expression in the context of the selected frame. This is used
|
|
* for modifying the value of variables in scope.
|
|
*
|
|
* @param string aExpression
|
|
* The expression to evaluate.
|
|
*/
|
|
evaluate: function SF_evaluate(aExpression) {
|
|
let frame = this.activeThread.cachedFrames[this.selectedFrame];
|
|
this.activeThread.eval(frame.actor, aExpression);
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Keeps the source script list up-to-date, using the thread client's
|
|
* source script cache.
|
|
*/
|
|
function SourceScripts() {
|
|
this._onNewScript = this._onNewScript.bind(this);
|
|
this._onScriptsAdded = this._onScriptsAdded.bind(this);
|
|
this._onScriptsCleared = this._onScriptsCleared.bind(this);
|
|
this._onShowScript = this._onShowScript.bind(this);
|
|
this._onLoadSource = this._onLoadSource.bind(this);
|
|
this._onLoadSourceFinished = this._onLoadSourceFinished.bind(this);
|
|
}
|
|
|
|
SourceScripts.prototype = {
|
|
|
|
/**
|
|
* A cache containing simplified labels from script urls.
|
|
*/
|
|
_labelsCache: {},
|
|
|
|
/**
|
|
* Gets the current thread the client has connected to.
|
|
*/
|
|
get activeThread() {
|
|
return DebuggerController.activeThread;
|
|
},
|
|
|
|
/**
|
|
* Gets the current debugger client.
|
|
*/
|
|
get debuggerClient() {
|
|
return DebuggerController.client;
|
|
},
|
|
|
|
/**
|
|
* Watch the given thread client.
|
|
*
|
|
* @param function aCallback
|
|
* The next function in the initialization sequence.
|
|
*/
|
|
connect: function SS_connect(aCallback) {
|
|
window.addEventListener("Debugger:LoadSource", this._onLoadSource, false);
|
|
|
|
this.debuggerClient.addListener("newScript", this._onNewScript);
|
|
this.activeThread.addListener("scriptsadded", this._onScriptsAdded);
|
|
this.activeThread.addListener("scriptscleared", this._onScriptsCleared);
|
|
|
|
this._clearLabelsCache();
|
|
this._onScriptsCleared();
|
|
|
|
// Retrieve the list of scripts known to the server from before the client
|
|
// was ready to handle new script notifications.
|
|
this.activeThread.fillScripts();
|
|
|
|
aCallback && aCallback();
|
|
},
|
|
|
|
/**
|
|
* Disconnect from the client.
|
|
*/
|
|
disconnect: function TS_disconnect() {
|
|
window.removeEventListener("Debugger:LoadSource", this._onLoadSource, false);
|
|
|
|
if (!this.activeThread) {
|
|
return;
|
|
}
|
|
this.debuggerClient.removeListener("newScript", this._onNewScript);
|
|
this.activeThread.removeListener("scriptsadded", this._onScriptsAdded);
|
|
this.activeThread.removeListener("scriptscleared", this._onScriptsCleared);
|
|
},
|
|
|
|
/**
|
|
* Handler for the debugger client's unsolicited newScript notification.
|
|
*/
|
|
_onNewScript: function SS__onNewScript(aNotification, aPacket) {
|
|
// Ignore scripts generated from 'clientEvaluate' packets.
|
|
if (aPacket.url == "debugger eval code") {
|
|
return;
|
|
}
|
|
|
|
this._addScript({ url: aPacket.url, startLine: aPacket.startLine }, true);
|
|
|
|
// If there are any stored breakpoints for this script, display them again,
|
|
// both in the editor and the pane.
|
|
for each (let breakpoint in DebuggerController.Breakpoints.store) {
|
|
if (breakpoint.location.url == aPacket.url) {
|
|
DebuggerController.Breakpoints.displayBreakpoint(breakpoint);
|
|
}
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Handler for the thread client's scriptsadded notification.
|
|
*/
|
|
_onScriptsAdded: function SS__onScriptsAdded() {
|
|
for each (let script in this.activeThread.cachedScripts) {
|
|
this._addScript(script, false);
|
|
}
|
|
DebuggerView.Scripts.commitScripts();
|
|
DebuggerController.Breakpoints.updatePaneBreakpoints();
|
|
},
|
|
|
|
/**
|
|
* Handler for the thread client's scriptscleared notification.
|
|
*/
|
|
_onScriptsCleared: function SS__onScriptsCleared() {
|
|
DebuggerView.Scripts.empty();
|
|
DebuggerView.Breakpoints.emptyText();
|
|
DebuggerView.editor.setText("");
|
|
},
|
|
|
|
/**
|
|
* Sets the proper editor mode (JS or HTML) according to the specified
|
|
* content type, or by determining the type from the URL.
|
|
*
|
|
* @param string aUrl
|
|
* The script URL.
|
|
* @param string aContentType [optional]
|
|
* The script content type.
|
|
*/
|
|
_setEditorMode: function SS__setEditorMode(aUrl, aContentType) {
|
|
if (aContentType) {
|
|
if (/javascript/.test(aContentType)) {
|
|
DebuggerView.editor.setMode(SourceEditor.MODES.JAVASCRIPT);
|
|
} else {
|
|
DebuggerView.editor.setMode(SourceEditor.MODES.HTML);
|
|
}
|
|
return;
|
|
}
|
|
|
|
// Use JS mode for files with .js and .jsm extensions.
|
|
if (/\.jsm?$/.test(this.trimUrlQuery(aUrl))) {
|
|
DebuggerView.editor.setMode(SourceEditor.MODES.JAVASCRIPT);
|
|
} else {
|
|
DebuggerView.editor.setMode(SourceEditor.MODES.HTML);
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Trims the query part or reference identifier of a url string, if necessary.
|
|
*
|
|
* @param string aUrl
|
|
* The script url.
|
|
* @return string
|
|
* The url with the trimmed query.
|
|
*/
|
|
trimUrlQuery: function SS_trimUrlQuery(aUrl) {
|
|
let length = aUrl.length;
|
|
let q1 = aUrl.indexOf('?');
|
|
let q2 = aUrl.indexOf('&');
|
|
let q3 = aUrl.indexOf('#');
|
|
let q = Math.min(q1 !== -1 ? q1 : length,
|
|
q2 !== -1 ? q2 : length,
|
|
q3 !== -1 ? q3 : length);
|
|
|
|
return aUrl.slice(0, q);
|
|
},
|
|
|
|
/**
|
|
* Trims as much as possible from a URL, while keeping the result unique
|
|
* in the Debugger View scripts container.
|
|
*
|
|
* @param string | nsIURL aUrl
|
|
* The script URL.
|
|
* @param string aLabel [optional]
|
|
* The resulting label at each step.
|
|
* @param number aSeq [optional]
|
|
* The current iteration step.
|
|
* @return string
|
|
* The resulting label at the final step.
|
|
*/
|
|
_trimUrl: function SS__trimUrl(aUrl, aLabel, aSeq) {
|
|
if (!(aUrl instanceof Ci.nsIURL)) {
|
|
try {
|
|
// Use an nsIURL to parse all the url path parts.
|
|
aUrl = Services.io.newURI(aUrl, null, null).QueryInterface(Ci.nsIURL);
|
|
} catch (e) {
|
|
// This doesn't look like a url, or nsIURL can't handle it.
|
|
return aUrl;
|
|
}
|
|
}
|
|
if (!aSeq) {
|
|
let name = aUrl.fileName;
|
|
if (name) {
|
|
// This is a regular file url, get only the file name (contains the
|
|
// base name and extension if available).
|
|
|
|
// If this url contains an invalid query, unfortunately nsIURL thinks
|
|
// it's part of the file extension. It must be removed.
|
|
aLabel = aUrl.fileName.replace(/\&.*/, "");
|
|
} else {
|
|
// This is not a file url, hence there is no base name, nor extension.
|
|
// Proceed using other available information.
|
|
aLabel = "";
|
|
}
|
|
aSeq = 1;
|
|
}
|
|
|
|
// If we have a label and it doesn't start with a query...
|
|
if (aLabel && aLabel.indexOf("?") !== 0) {
|
|
|
|
if (DebuggerView.Scripts.containsIgnoringQuery(aUrl.spec)) {
|
|
// A page may contain multiple requests to the same url but with different
|
|
// queries. It would be redundant to show each one.
|
|
return aLabel;
|
|
}
|
|
if (!DebuggerView.Scripts.containsLabel(aLabel)) {
|
|
// We found the shortest unique label for the url.
|
|
return aLabel;
|
|
}
|
|
}
|
|
|
|
// Append the url query.
|
|
if (aSeq === 1) {
|
|
let query = aUrl.query;
|
|
if (query) {
|
|
return this._trimUrl(aUrl, aLabel + "?" + query, aSeq + 1);
|
|
}
|
|
aSeq++;
|
|
}
|
|
// Append the url reference.
|
|
if (aSeq === 2) {
|
|
let ref = aUrl.ref;
|
|
if (ref) {
|
|
return this._trimUrl(aUrl, aLabel + "#" + aUrl.ref, aSeq + 1);
|
|
}
|
|
aSeq++;
|
|
}
|
|
// Prepend the url directory.
|
|
if (aSeq === 3) {
|
|
let dir = aUrl.directory;
|
|
if (dir) {
|
|
return this._trimUrl(aUrl, dir.replace(/^\//, "") + aLabel, aSeq + 1);
|
|
}
|
|
aSeq++;
|
|
}
|
|
// Prepend the hostname and port number.
|
|
if (aSeq === 4) {
|
|
let host = aUrl.hostPort;
|
|
if (host) {
|
|
return this._trimUrl(aUrl, host + "/" + aLabel, aSeq + 1);
|
|
}
|
|
aSeq++;
|
|
}
|
|
// Use the whole url spec but ignoring the reference.
|
|
if (aSeq === 5) {
|
|
return this._trimUrl(aUrl, aUrl.specIgnoringRef, aSeq + 1);
|
|
}
|
|
// Give up.
|
|
return aUrl.spec;
|
|
},
|
|
|
|
/**
|
|
* Gets a unique, simplified label from a script url.
|
|
*
|
|
* @param string aUrl
|
|
* The script url.
|
|
* @param string aHref
|
|
* The content location href to be used. If unspecified, it will
|
|
* default to the script url prepath.
|
|
* @return string
|
|
* The simplified label.
|
|
*/
|
|
getScriptLabel: function SS_getScriptLabel(aUrl, aHref) {
|
|
return this._labelsCache[aUrl] || (this._labelsCache[aUrl] = this._trimUrl(aUrl));
|
|
},
|
|
|
|
/**
|
|
* Clears the labels cache, populated by SS_getScriptLabel.
|
|
* This should be done every time the content location changes.
|
|
*/
|
|
_clearLabelsCache: function SS__clearLabelsCache() {
|
|
this._labelsCache = {};
|
|
},
|
|
|
|
/**
|
|
* Add the specified script to the list.
|
|
*
|
|
* @param object aScript
|
|
* The script object coming from the active thread.
|
|
* @param boolean aForceFlag
|
|
* True to force the script to be immediately added.
|
|
*/
|
|
_addScript: function SS__addScript(aScript, aForceFlag) {
|
|
DebuggerView.Scripts.addScript(
|
|
this.getScriptLabel(aScript.url), aScript, aForceFlag);
|
|
},
|
|
|
|
/**
|
|
* Load the editor with the script text if available, otherwise fire an event
|
|
* to load and display the script text.
|
|
*
|
|
* @param object aScript
|
|
* The script object coming from the active thread.
|
|
* @param object [aOptions]
|
|
* Additional options for showing the script. Supported options:
|
|
* - targetLine: place the editor at the given line number.
|
|
*/
|
|
showScript: function SS_showScript(aScript, aOptions) {
|
|
if (aScript.loaded) {
|
|
this._onShowScript(aScript, aOptions);
|
|
return;
|
|
}
|
|
|
|
let editor = DebuggerView.editor;
|
|
editor.setMode(SourceEditor.MODES.TEXT);
|
|
editor.setText(L10N.getStr("loadingText"));
|
|
editor.resetUndo();
|
|
|
|
// Notify that we need to load a script file.
|
|
DebuggerController.dispatchEvent("Debugger:LoadSource", {
|
|
url: aScript.url,
|
|
options: aOptions
|
|
});
|
|
},
|
|
|
|
/**
|
|
* Display the script source once it loads.
|
|
*
|
|
* @private
|
|
* @param object aScript
|
|
* The script object coming from the active thread.
|
|
* @param object aOptions [optional]
|
|
* Additional options for showing the script. Supported options:
|
|
* - targetLine: place the editor at the given line number.
|
|
*/
|
|
_onShowScript: function SS__onShowScript(aScript, aOptions) {
|
|
aOptions = aOptions || {};
|
|
|
|
if (aScript.text.length < SYNTAX_HIGHLIGHT_MAX_FILE_SIZE) {
|
|
this._setEditorMode(aScript.url, aScript.contentType);
|
|
}
|
|
|
|
let editor = DebuggerView.editor;
|
|
editor.setText(aScript.text);
|
|
editor.resetUndo();
|
|
|
|
DebuggerController.Breakpoints.updateEditorBreakpoints();
|
|
DebuggerController.StackFrames.updateEditorLocation();
|
|
|
|
// Handle any additional options for showing the script.
|
|
if (aOptions.targetLine) {
|
|
editor.setCaretPosition(aOptions.targetLine - 1);
|
|
}
|
|
|
|
// Notify that we shown script file.
|
|
DebuggerController.dispatchEvent("Debugger:ScriptShown", {
|
|
url: aScript.url
|
|
});
|
|
},
|
|
|
|
/**
|
|
* Handles notifications to load a source script from the cache or from a
|
|
* local file.
|
|
*
|
|
* XXX: It may be better to use nsITraceableChannel to get to the sources
|
|
* without relying on caching when we can (not for eval, etc.):
|
|
* http://www.softwareishard.com/blog/firebug/nsitraceablechannel-intercept-http-traffic/
|
|
*/
|
|
_onLoadSource: function SS__onLoadSource(aEvent) {
|
|
let url = aEvent.detail.url;
|
|
let options = aEvent.detail.options;
|
|
let self = this;
|
|
|
|
switch (Services.io.extractScheme(url)) {
|
|
case "file":
|
|
case "chrome":
|
|
case "resource":
|
|
try {
|
|
NetUtil.asyncFetch(url, function onFetch(aStream, aStatus) {
|
|
if (!Components.isSuccessCode(aStatus)) {
|
|
return self._logError(url, aStatus);
|
|
}
|
|
let source = NetUtil.readInputStreamToString(aStream, aStream.available());
|
|
self._onLoadSourceFinished(url, source, null, options);
|
|
aStream.close();
|
|
});
|
|
} catch (ex) {
|
|
return self._logError(url, ex.name);
|
|
}
|
|
break;
|
|
|
|
default:
|
|
let channel = Services.io.newChannel(url, null, null);
|
|
let chunks = [];
|
|
let streamListener = {
|
|
onStartRequest: function(aRequest, aContext, aStatusCode) {
|
|
if (!Components.isSuccessCode(aStatusCode)) {
|
|
return self._logError(url, aStatusCode);
|
|
}
|
|
},
|
|
onDataAvailable: function(aRequest, aContext, aStream, aOffset, aCount) {
|
|
chunks.push(NetUtil.readInputStreamToString(aStream, aCount));
|
|
},
|
|
onStopRequest: function(aRequest, aContext, aStatusCode) {
|
|
if (!Components.isSuccessCode(aStatusCode)) {
|
|
return self._logError(url, aStatusCode);
|
|
}
|
|
self._onLoadSourceFinished(
|
|
url, chunks.join(""), channel.contentType, options);
|
|
}
|
|
};
|
|
|
|
channel.loadFlags = channel.LOAD_FROM_CACHE;
|
|
channel.asyncOpen(streamListener, null);
|
|
break;
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Called when source has been loaded.
|
|
*
|
|
* @private
|
|
* @param string aSourceUrl
|
|
* The URL of the source script.
|
|
* @param string aSourceText
|
|
* The text of the source script.
|
|
* @param string aContentType
|
|
* The content type of the source script.
|
|
* @param object aOptions [optional]
|
|
* Additional options for showing the script. Supported options:
|
|
* - targetLine: place the editor at the given line number.
|
|
*/
|
|
_onLoadSourceFinished:
|
|
function SS__onLoadSourceFinished(aSourceUrl, aSourceText, aContentType, aOptions) {
|
|
let scripts = document.getElementById("scripts");
|
|
let element = scripts.getElementsByAttribute("value", aSourceUrl)[0];
|
|
let script = element.getUserData("sourceScript");
|
|
|
|
script.loaded = true;
|
|
script.text = aSourceText;
|
|
script.contentType = aContentType;
|
|
element.setUserData("sourceScript", script, null);
|
|
|
|
this.showScript(script, aOptions);
|
|
},
|
|
|
|
/**
|
|
* Gets the text in a source editor's specified line.
|
|
*
|
|
* @param number aLine [optional]
|
|
* The line to get the text from.
|
|
* If unspecified, it defaults to the current caret position line.
|
|
* @return string
|
|
* The specified line text
|
|
*/
|
|
getLineText: function SS_getLineText(aLine) {
|
|
let editor = DebuggerView.editor;
|
|
let line = aLine || editor.getCaretPosition().line;
|
|
let start = editor.getLineStart(line);
|
|
let end = editor.getLineEnd(line);
|
|
return editor.getText(start, end);
|
|
},
|
|
|
|
/**
|
|
* Log an error message in the error console when a script fails to load.
|
|
*
|
|
* @param string aUrl
|
|
* The URL of the source script.
|
|
* @param string aStatus
|
|
* The failure status code.
|
|
*/
|
|
_logError: function SS__logError(aUrl, aStatus) {
|
|
Cu.reportError(L10N.getFormatStr("loadingError", [aUrl, aStatus]));
|
|
},
|
|
};
|
|
|
|
/**
|
|
* Handles all the breakpoints in the current debugger.
|
|
*/
|
|
function Breakpoints() {
|
|
this._onEditorBreakpointChange = this._onEditorBreakpointChange.bind(this);
|
|
this._onEditorBreakpointAdd = this._onEditorBreakpointAdd.bind(this);
|
|
this._onEditorBreakpointRemove = this._onEditorBreakpointRemove.bind(this);
|
|
this.addBreakpoint = this.addBreakpoint.bind(this);
|
|
this.removeBreakpoint = this.removeBreakpoint.bind(this);
|
|
this.getBreakpoint = this.getBreakpoint.bind(this);
|
|
}
|
|
|
|
Breakpoints.prototype = {
|
|
|
|
/**
|
|
* Skip editor breakpoint change events.
|
|
*
|
|
* This property tells the source editor event handler to skip handling of
|
|
* the BREAKPOINT_CHANGE events. This is used when the debugger adds/removes
|
|
* breakpoints from the editor. Typically, the BREAKPOINT_CHANGE event handler
|
|
* adds/removes events from the debugger, but when breakpoints are added from
|
|
* the public debugger API, we need to do things in reverse.
|
|
*
|
|
* This implementation relies on the fact that the source editor fires the
|
|
* BREAKPOINT_CHANGE events synchronously.
|
|
*
|
|
* @private
|
|
* @type boolean
|
|
*/
|
|
_skipEditorBreakpointChange: false,
|
|
|
|
/**
|
|
* The list of breakpoints in the debugger as tracked by the current
|
|
* debugger instance. This is an object where the values are BreakpointActor
|
|
* objects received from the client, while the keys are actor names, for
|
|
* example "conn0.breakpoint3".
|
|
*
|
|
* @type object
|
|
*/
|
|
store: {},
|
|
|
|
/**
|
|
* Gets the current thread the client has connected to.
|
|
*/
|
|
get activeThread() {
|
|
return DebuggerController.ThreadState.activeThread;
|
|
},
|
|
|
|
/**
|
|
* Gets the source editor in the debugger view.
|
|
*/
|
|
get editor() {
|
|
return DebuggerView.editor;
|
|
},
|
|
|
|
/**
|
|
* Sets up the source editor breakpoint handlers.
|
|
*/
|
|
initialize: function BP_initialize() {
|
|
this.editor.addEventListener(
|
|
SourceEditor.EVENTS.BREAKPOINT_CHANGE, this._onEditorBreakpointChange);
|
|
},
|
|
|
|
/**
|
|
* Removes all currently added breakpoints.
|
|
*/
|
|
destroy: function BP_destroy() {
|
|
for each (let breakpoint in this.store) {
|
|
this.removeBreakpoint(breakpoint);
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Event handler for breakpoint changes that happen in the editor. This
|
|
* function syncs the breakpoint changes in the editor to those in the
|
|
* debugger.
|
|
*
|
|
* @private
|
|
* @param object aEvent
|
|
* The SourceEditor.EVENTS.BREAKPOINT_CHANGE event object.
|
|
*/
|
|
_onEditorBreakpointChange: function BP__onEditorBreakpointChange(aEvent) {
|
|
if (this._skipEditorBreakpointChange) {
|
|
return;
|
|
}
|
|
|
|
aEvent.added.forEach(this._onEditorBreakpointAdd, this);
|
|
aEvent.removed.forEach(this._onEditorBreakpointRemove, this);
|
|
},
|
|
|
|
/**
|
|
* Event handler for new breakpoints that come from the editor.
|
|
*
|
|
* @private
|
|
* @param object aBreakpoint
|
|
* The breakpoint object coming from the editor.
|
|
*/
|
|
_onEditorBreakpointAdd: function BP__onEditorBreakpointAdd(aBreakpoint) {
|
|
let url = DebuggerView.Scripts.selected;
|
|
if (!url) {
|
|
return;
|
|
}
|
|
|
|
let line = aBreakpoint.line + 1;
|
|
|
|
this.addBreakpoint({ url: url, line: line }, null, true);
|
|
},
|
|
|
|
/**
|
|
* Event handler for breakpoints that are removed from the editor.
|
|
*
|
|
* @private
|
|
* @param object aBreakpoint
|
|
* The breakpoint object that was removed from the editor.
|
|
*/
|
|
_onEditorBreakpointRemove: function BP__onEditorBreakpointRemove(aBreakpoint) {
|
|
let url = DebuggerView.Scripts.selected;
|
|
if (!url) {
|
|
return;
|
|
}
|
|
|
|
let line = aBreakpoint.line + 1;
|
|
|
|
let breakpoint = this.getBreakpoint(url, line);
|
|
if (breakpoint) {
|
|
this.removeBreakpoint(breakpoint, null, true);
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Update the breakpoints in the editor view. This function takes the list of
|
|
* breakpoints in the debugger and adds them back into the editor view. This
|
|
* is invoked when the selected script is changed.
|
|
*/
|
|
updateEditorBreakpoints: function BP_updateEditorBreakpoints() {
|
|
let url = DebuggerView.Scripts.selected;
|
|
if (!url) {
|
|
return;
|
|
}
|
|
|
|
this._skipEditorBreakpointChange = true;
|
|
for each (let breakpoint in this.store) {
|
|
if (breakpoint.location.url == url) {
|
|
this.editor.addBreakpoint(breakpoint.location.line - 1);
|
|
}
|
|
}
|
|
this._skipEditorBreakpointChange = false;
|
|
},
|
|
|
|
/**
|
|
* Update the breakpoints in the pane view. This function is invoked when the
|
|
* scripts are added (typically after a page navigation).
|
|
*/
|
|
updatePaneBreakpoints: function BP_updatePaneBreakpoints() {
|
|
let url = DebuggerView.Scripts.selected;
|
|
if (!url) {
|
|
return;
|
|
}
|
|
|
|
this._skipEditorBreakpointChange = true;
|
|
for each (let breakpoint in this.store) {
|
|
if (DebuggerView.Scripts.contains(breakpoint.location.url)) {
|
|
this.displayBreakpoint(breakpoint, true);
|
|
}
|
|
}
|
|
this._skipEditorBreakpointChange = false;
|
|
},
|
|
|
|
/**
|
|
* Add a breakpoint.
|
|
*
|
|
* @param object aLocation
|
|
* The location where you want the breakpoint. This object must have
|
|
* two properties:
|
|
* - url - the URL of the script.
|
|
* - line - the line number (starting from 1).
|
|
* @param function [aCallback]
|
|
* Optional function to invoke once the breakpoint is added. The
|
|
* callback is invoked with two arguments:
|
|
* - aBreakpointClient - the BreakpointActor client object, if the
|
|
* breakpoint has been added successfully.
|
|
* - aResponseError - if there was any error.
|
|
* @param boolean [aNoEditorUpdate=false]
|
|
* Tells if you want to skip editor updates. Typically the editor is
|
|
* updated to visually indicate that a breakpoint has been added.
|
|
* @param boolean [aNoPaneUpdate=false]
|
|
* Tells if you want to skip any breakpoint pane updates.
|
|
*/
|
|
addBreakpoint:
|
|
function BP_addBreakpoint(aLocation, aCallback, aNoEditorUpdate, aNoPaneUpdate) {
|
|
let breakpoint = this.getBreakpoint(aLocation.url, aLocation.line);
|
|
if (breakpoint) {
|
|
aCallback && aCallback(breakpoint);
|
|
return;
|
|
}
|
|
|
|
this.activeThread.setBreakpoint(aLocation, function(aResponse, aBpClient) {
|
|
this.store[aBpClient.actor] = aBpClient;
|
|
this.displayBreakpoint(aBpClient, aNoEditorUpdate, aNoPaneUpdate);
|
|
aCallback && aCallback(aBpClient, aResponse.error);
|
|
}.bind(this));
|
|
},
|
|
|
|
/**
|
|
* Update the editor to display the specified breakpoint in the gutter.
|
|
*
|
|
* @param object aBreakpoint
|
|
* The breakpoint you want to display.
|
|
* @param boolean [aNoEditorUpdate=false]
|
|
* Tells if you want to skip editor updates. Typically the editor is
|
|
* updated to visually indicate that a breakpoint has been added.
|
|
* @param boolean [aNoPaneUpdate=false]
|
|
* Tells if you want to skip any breakpoint pane updates.
|
|
*/
|
|
displayBreakpoint:
|
|
function BP_displayBreakpoint(aBreakpoint, aNoEditorUpdate, aNoPaneUpdate) {
|
|
if (!aNoEditorUpdate) {
|
|
let url = DebuggerView.Scripts.selected;
|
|
if (url == aBreakpoint.location.url) {
|
|
this._skipEditorBreakpointChange = true;
|
|
this.editor.addBreakpoint(aBreakpoint.location.line - 1);
|
|
this._skipEditorBreakpointChange = false;
|
|
}
|
|
}
|
|
if (!aNoPaneUpdate) {
|
|
let { url: url, line: line } = aBreakpoint.location;
|
|
|
|
if (!aBreakpoint.lineText || !aBreakpoint.lineInfo) {
|
|
let scripts = DebuggerController.SourceScripts;
|
|
aBreakpoint.lineText = scripts.getLineText(line - 1);
|
|
aBreakpoint.lineInfo = scripts.getScriptLabel(url) + ":" + line;
|
|
}
|
|
DebuggerView.Breakpoints.addBreakpoint(
|
|
aBreakpoint.actor,
|
|
aBreakpoint.lineInfo,
|
|
aBreakpoint.lineText, url, line);
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Remove a breakpoint.
|
|
*
|
|
* @param object aBreakpoint
|
|
* The breakpoint you want to remove.
|
|
* @param function [aCallback]
|
|
* Optional function to invoke once the breakpoint is removed. The
|
|
* callback is invoked with one argument: the breakpoint location
|
|
* object which holds the url and line properties.
|
|
* @param boolean [aNoEditorUpdate=false]
|
|
* Tells if you want to skip editor updates. Typically the editor is
|
|
* updated to visually indicate that a breakpoint has been removed.
|
|
* @param boolean [aNoPaneUpdate=false]
|
|
* Tells if you want to skip any breakpoint pane updates.
|
|
*/
|
|
removeBreakpoint:
|
|
function BP_removeBreakpoint(aBreakpoint, aCallback, aNoEditorUpdate, aNoPaneUpdate) {
|
|
if (!(aBreakpoint.actor in this.store)) {
|
|
aCallback && aCallback(aBreakpoint.location);
|
|
return;
|
|
}
|
|
|
|
aBreakpoint.remove(function() {
|
|
delete this.store[aBreakpoint.actor];
|
|
|
|
if (!aNoEditorUpdate) {
|
|
let url = DebuggerView.Scripts.selected;
|
|
if (url == aBreakpoint.location.url) {
|
|
this._skipEditorBreakpointChange = true;
|
|
this.editor.removeBreakpoint(aBreakpoint.location.line - 1);
|
|
this._skipEditorBreakpointChange = false;
|
|
}
|
|
}
|
|
if (!aNoPaneUpdate) {
|
|
DebuggerView.Breakpoints.removeBreakpoint(aBreakpoint.actor);
|
|
}
|
|
|
|
aCallback && aCallback(aBreakpoint.location);
|
|
}.bind(this));
|
|
},
|
|
|
|
/**
|
|
* Get the breakpoint object at the given location.
|
|
*
|
|
* @param string aUrl
|
|
* The URL of where the breakpoint is.
|
|
* @param number aLine
|
|
* The line number where the breakpoint is.
|
|
* @return object
|
|
* The BreakpointActor object.
|
|
*/
|
|
getBreakpoint: function BP_getBreakpoint(aUrl, aLine) {
|
|
for each (let breakpoint in this.store) {
|
|
if (breakpoint.location.url == aUrl && breakpoint.location.line == aLine) {
|
|
return breakpoint;
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Localization convenience methods.
|
|
*/
|
|
let L10N = {
|
|
|
|
/**
|
|
* L10N shortcut function.
|
|
*
|
|
* @param string aName
|
|
* @return string
|
|
*/
|
|
getStr: function L10N_getStr(aName) {
|
|
return this.stringBundle.GetStringFromName(aName);
|
|
},
|
|
|
|
/**
|
|
* L10N shortcut function.
|
|
*
|
|
* @param string aName
|
|
* @param array aArray
|
|
* @return string
|
|
*/
|
|
getFormatStr: function L10N_getFormatStr(aName, aArray) {
|
|
return this.stringBundle.formatStringFromName(aName, aArray, aArray.length);
|
|
}
|
|
};
|
|
|
|
XPCOMUtils.defineLazyGetter(L10N, "stringBundle", function() {
|
|
return Services.strings.createBundle(DBG_STRINGS_URI);
|
|
});
|
|
|
|
/**
|
|
* Shortcuts for accessing various debugger preferences.
|
|
*/
|
|
let Prefs = {
|
|
|
|
/**
|
|
* Gets the preferred stackframes pane width.
|
|
* @return number
|
|
*/
|
|
get stackframesWidth() {
|
|
if (this._sfrmWidth === undefined) {
|
|
this._sfrmWidth = Services.prefs.getIntPref("devtools.debugger.ui.stackframes-width");
|
|
}
|
|
return this._sfrmWidth;
|
|
},
|
|
|
|
/**
|
|
* Sets the preferred stackframes pane width.
|
|
* @return number
|
|
*/
|
|
set stackframesWidth(value) {
|
|
Services.prefs.setIntPref("devtools.debugger.ui.stackframes-width", value);
|
|
this._sfrmWidth = value;
|
|
},
|
|
|
|
/**
|
|
* Gets the preferred variables pane width.
|
|
* @return number
|
|
*/
|
|
get variablesWidth() {
|
|
if (this._varsWidth === undefined) {
|
|
this._varsWidth = Services.prefs.getIntPref("devtools.debugger.ui.variables-width");
|
|
}
|
|
return this._varsWidth;
|
|
},
|
|
|
|
/**
|
|
* Sets the preferred variables pane width.
|
|
* @return number
|
|
*/
|
|
set variablesWidth(value) {
|
|
Services.prefs.setIntPref("devtools.debugger.ui.variables-width", value);
|
|
this._varsWidth = value;
|
|
},
|
|
|
|
/**
|
|
* Gets a flag specifying if the the debugger should automatically connect to
|
|
* the default host and port number.
|
|
* @return boolean
|
|
*/
|
|
get remoteAutoConnect() {
|
|
if (this._autoConn === undefined) {
|
|
this._autoConn = Services.prefs.getBoolPref("devtools.debugger.remote-autoconnect");
|
|
}
|
|
return this._autoConn;
|
|
},
|
|
|
|
/**
|
|
* Sets a flag specifying if the the debugger should automatically connect.
|
|
* @param boolean value
|
|
*/
|
|
set remoteAutoConnect(value) {
|
|
Services.prefs.setBoolPref("devtools.debugger.remote-autoconnect", value);
|
|
this._autoConn = value;
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Gets the preferred default remote debugging host.
|
|
* @return string
|
|
*/
|
|
XPCOMUtils.defineLazyGetter(Prefs, "remoteHost", function() {
|
|
return Services.prefs.getCharPref("devtools.debugger.remote-host");
|
|
});
|
|
|
|
/**
|
|
* Gets the preferred default remote debugging port.
|
|
* @return number
|
|
*/
|
|
XPCOMUtils.defineLazyGetter(Prefs, "remotePort", function() {
|
|
return Services.prefs.getIntPref("devtools.debugger.remote-port");
|
|
});
|
|
|
|
/**
|
|
* Gets the max number of attempts to reconnect to a remote server.
|
|
* @return number
|
|
*/
|
|
XPCOMUtils.defineLazyGetter(Prefs, "remoteConnectionRetries", function() {
|
|
return Services.prefs.getIntPref("devtools.debugger.remote-connection-retries");
|
|
});
|
|
|
|
/**
|
|
* Gets the remote debugging connection timeout (in milliseconds).
|
|
* @return number
|
|
*/
|
|
XPCOMUtils.defineLazyGetter(Prefs, "remoteTimeout", function() {
|
|
return Services.prefs.getIntPref("devtools.debugger.remote-timeout");
|
|
});
|
|
|
|
/**
|
|
* Preliminary setup for the DebuggerController object.
|
|
*/
|
|
DebuggerController.init();
|
|
DebuggerController.ThreadState = new ThreadState();
|
|
DebuggerController.StackFrames = new StackFrames();
|
|
DebuggerController.SourceScripts = new SourceScripts();
|
|
DebuggerController.Breakpoints = new Breakpoints();
|
|
|
|
/**
|
|
* Export some properties to the global scope for easier access in tests.
|
|
*/
|
|
Object.defineProperty(window, "gClient", {
|
|
get: function() { return DebuggerController.client; }
|
|
});
|
|
|
|
Object.defineProperty(window, "gTabClient", {
|
|
get: function() { return DebuggerController.tabClient; }
|
|
});
|
|
|
|
Object.defineProperty(window, "gThreadClient", {
|
|
get: function() { return DebuggerController.activeThread; }
|
|
});
|