From dd30ddaf378e56977e89d77468f14ba60ea5ecb3 Mon Sep 17 00:00:00 2001 From: Victor Porof Date: Mon, 9 Apr 2012 08:15:47 +0300 Subject: [PATCH] Bug 741322 - Refactor debugger UI, make it slimmer; r=past --HG-- rename : browser/devtools/debugger/debugger.js => browser/devtools/debugger/debugger-controller.js --- browser/devtools/debugger/DebuggerUI.jsm | 614 ++------ .../devtools/debugger/debugger-controller.js | 1282 +++++++++++++++++ browser/devtools/debugger/debugger-view.js | 703 +++++---- browser/devtools/debugger/debugger.js | 696 --------- browser/devtools/debugger/debugger.xul | 94 +- ...rowser_dbg_bug723069_editor-breakpoints.js | 18 +- ...rowser_dbg_bug731394_editor-contextmenu.js | 6 +- .../debugger/test/browser_dbg_clean-exit.js | 4 +- .../test/browser_dbg_debuggerstatement.html | 2 +- .../test/browser_dbg_displayName.html | 2 +- .../debugger/test/browser_dbg_displayName.js | 8 +- .../test/browser_dbg_frame-parameters.html | 1 + .../debugger/test/browser_dbg_iframes.html | 2 +- .../debugger/test/browser_dbg_iframes.js | 10 +- .../test/browser_dbg_location-changes.js | 12 +- .../debugger/test/browser_dbg_panesize.js | 11 +- .../debugger/test/browser_dbg_pause-resume.js | 18 +- .../test/browser_dbg_propertyview-01.js | 8 +- .../test/browser_dbg_propertyview-02.js | 4 +- .../test/browser_dbg_propertyview-03.js | 4 +- .../test/browser_dbg_propertyview-04.js | 4 +- .../test/browser_dbg_propertyview-05.js | 4 +- .../test/browser_dbg_propertyview-06.js | 4 +- .../test/browser_dbg_propertyview-07.js | 10 +- .../test/browser_dbg_propertyview-08.js | 10 +- .../test/browser_dbg_script-switching.html | 11 +- .../test/browser_dbg_script-switching.js | 10 +- .../debugger/test/browser_dbg_select-line.js | 6 +- .../debugger/test/browser_dbg_stack-01.js | 8 +- .../debugger/test/browser_dbg_stack-02.js | 8 +- .../debugger/test/browser_dbg_stack-03.js | 14 +- .../debugger/test/browser_dbg_stack-04.js | 10 +- .../debugger/test/browser_dbg_stack-05.js | 6 +- .../debugger/test/browser_dbg_stack.html | 2 +- .../debugger/test/browser_dbg_tab1.html | 11 +- .../debugger/test/browser_dbg_tab2.html | 11 +- .../test/browser_dbg_update-editor-mode.html | 11 +- .../test/browser_dbg_update-editor-mode.js | 6 +- browser/devtools/debugger/test/head.js | 11 +- browser/devtools/jar.mn | 3 +- browser/devtools/webconsole/GcliCommands.jsm | 4 +- .../webconsole/test/browser_gcli_break.js | 13 +- 42 files changed, 1974 insertions(+), 1702 deletions(-) create mode 100644 browser/devtools/debugger/debugger-controller.js delete mode 100644 browser/devtools/debugger/debugger.js diff --git a/browser/devtools/debugger/DebuggerUI.jsm b/browser/devtools/debugger/DebuggerUI.jsm index 782d9c52f4e1..cf4bc164f30d 100644 --- a/browser/devtools/debugger/DebuggerUI.jsm +++ b/browser/devtools/debugger/DebuggerUI.jsm @@ -45,545 +45,143 @@ const Cc = Components.classes; const Ci = Components.interfaces; const Cu = Components.utils; -Cu.import("resource://gre/modules/NetUtil.jsm"); Cu.import("resource://gre/modules/Services.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:///modules/source-editor.jsm"); let EXPORTED_SYMBOLS = ["DebuggerUI"]; /** - * Creates a pane that will host the debugger UI. + * Provides a simple mechanism of managing debugger instances per tab. + * + * @param nsIDOMWindow aWindow + * The chrome window for which the DebuggerUI instance is created. */ -function DebuggerPane(aTab) { - this._tab = aTab; - this._close = this.close.bind(this); - this._debugTab = this.debugTab.bind(this); - this.breakpoints = {}; -} - -DebuggerPane.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 - * DebuggerPane instance. This an object where the values are BreakpointActor - * objects received from the client, while the keys are actor names, for - * example "conn0.breakpoint3". - * - * @type object - */ - breakpoints: null, - - /** - * Creates and initializes the widgets contained in the debugger UI. - */ - create: function DP_create(gBrowser) { - this._tab._scriptDebugger = this; - - this._nbox = gBrowser.getNotificationBox(this._tab.linkedBrowser); - this._splitter = gBrowser.parentNode.ownerDocument.createElement("splitter"); - this._splitter.setAttribute("class", "hud-splitter"); - this.frame = gBrowser.parentNode.ownerDocument.createElement("iframe"); - this.frame.height = DebuggerUIPreferences.height; - - this._nbox.appendChild(this._splitter); - this._nbox.appendChild(this.frame); - - let self = this; - - this.frame.addEventListener("DOMContentLoaded", function initPane(aEvent) { - if (aEvent.target != self.frame.contentDocument) { - return; - } - self.frame.removeEventListener("DOMContentLoaded", initPane, true); - // Initialize the source editor. - self.frame.contentWindow.editor = self.editor = new SourceEditor(); - self.frame.contentWindow.updateEditorBreakpoints = - self._updateEditorBreakpoints.bind(self); - - let config = { - mode: SourceEditor.MODES.JAVASCRIPT, - showLineNumbers: true, - readOnly: true, - showAnnotationRuler: true, - showOverviewRuler: true, - }; - - let editorPlaceholder = self.frame.contentDocument.getElementById("editor"); - self.editor.init(editorPlaceholder, config, self._onEditorLoad.bind(self)); - }, true); - this.frame.addEventListener("DebuggerClose", this._close, true); - - this.frame.setAttribute("src", "chrome://browser/content/debugger.xul"); - }, - - /** - * The load event handler for the source editor. This method does post-load - * editor initialization. - */ - _onEditorLoad: function DP__onEditorLoad() { - this.editor.addEventListener(SourceEditor.EVENTS.BREAKPOINT_CHANGE, - this._onEditorBreakpointChange.bind(this)); - // Connect to the debugger server. - this.connect(); - }, - - /** - * 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 DP__onEditorBreakpointChange(aEvent) { - if (this._skipEditorBreakpointChange) { - return; - } - - aEvent.added.forEach(this._onEditorBreakpointAdd, this); - aEvent.removed.forEach(this._onEditorBreakpointRemove, this); - }, - - /** - * Retrieve the URL of the selected script in the debugger view. - * - * @private - * @return string - * The URL of the selected script. - */ - _selectedScript: function DP__selectedScript() { - return this.debuggerWindow ? - this.debuggerWindow.DebuggerView.Scripts.selected : null; - }, - - /** - * Event handler for new breakpoints that come from the editor. - * - * @private - * @param object aBreakpoint - * The breakpoint object coming from the editor. - */ - _onEditorBreakpointAdd: function DP__onEditorBreakpointAdd(aBreakpoint) { - let location = { - url: this._selectedScript(), - line: aBreakpoint.line + 1, - }; - - if (location.url) { - let callback = function (aClient, aError) { - if (aError) { - this._skipEditorBreakpointChange = true; - let result = this.editor.removeBreakpoint(aBreakpoint.line); - this._skipEditorBreakpointChange = false; - } - }.bind(this); - this.addBreakpoint(location, callback, 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 DP__onEditorBreakpointRemove(aBreakpoint) { - let url = this._selectedScript(); - let line = aBreakpoint.line + 1; - if (!url) { - return; - } - - 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. - * - * @private - */ - _updateEditorBreakpoints: function DP__updateEditorBreakpoints() - { - let url = this._selectedScript(); - if (!url) { - return; - } - - this._skipEditorBreakpointChange = true; - for each (let breakpoint in this.breakpoints) { - if (breakpoint.location.url == url) { - this.editor.addBreakpoint(breakpoint.location.line - 1); - } - } - 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. - */ - addBreakpoint: - function DP_addBreakpoint(aLocation, aCallback, aNoEditorUpdate) { - let breakpoint = this.getBreakpoint(aLocation.url, aLocation.line); - if (breakpoint) { - aCallback && aCallback(breakpoint); - return; - } - - this.activeThread.setBreakpoint(aLocation, function(aResponse, aBpClient) { - if (!aResponse.error) { - this.breakpoints[aBpClient.actor] = aBpClient; - - if (!aNoEditorUpdate) { - let url = this._selectedScript(); - if (url == aLocation.url) { - this._skipEditorBreakpointChange = true; - this.editor.addBreakpoint(aLocation.line - 1); - this._skipEditorBreakpointChange = false; - } - } - } - - aCallback && aCallback(aBpClient, aResponse.error); - }.bind(this)); - }, - - /** - * 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. - */ - removeBreakpoint: - function DP_removeBreakpoint(aBreakpoint, aCallback, aNoEditorUpdate) { - if (!(aBreakpoint.actor in this.breakpoints)) { - aCallback && aCallback(aBreakpoint.location); - return; - } - - aBreakpoint.remove(function() { - delete this.breakpoints[aBreakpoint.actor]; - - if (!aNoEditorUpdate) { - let url = this._selectedScript(); - if (url == aBreakpoint.location.url) { - this._skipEditorBreakpointChange = true; - this.editor.removeBreakpoint(aBreakpoint.location.line - 1); - this._skipEditorBreakpointChange = false; - } - } - - 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 DP_getBreakpoint(aUrl, aLine) { - for each (let breakpoint in this.breakpoints) { - if (breakpoint.location.url == aUrl && breakpoint.location.line == aLine) { - return breakpoint; - } - } - return null; - }, - - /** - * Closes the debugger UI removing child nodes and event listeners. - */ - close: function DP_close() { - for each (let breakpoint in this.breakpoints) { - this.removeBreakpoint(breakpoint); - } - - if (this._tab) { - this._tab._scriptDebugger = null; - this._tab = null; - } - if (this.frame) { - DebuggerUIPreferences.height = this.frame.height; - - this.frame.removeEventListener("unload", this._close, true); - this.frame.removeEventListener("DebuggerClose", this._close, true); - if (this.frame.parentNode) { - this.frame.parentNode.removeChild(this.frame); - } - this.frame = null; - } - if (this._nbox) { - this._nbox.removeChild(this._splitter); - this._nbox = null; - } - - this._splitter = null; - - if (this._client) { - this._client.removeListener("newScript", this.onNewScript); - this._client.removeListener("tabDetached", this._close); - this._client.removeListener("tabNavigated", this._debugTab); - this._client.close(); - this._client = null; - } - }, - - /** - * Initializes a debugger client and connects it to the debugger server, - * wiring event handlers as necessary. - */ - connect: function DP_connect() { - this.frame.addEventListener("unload", this._close, true); - - let transport = DebuggerServer.connectPipe(); - this._client = new DebuggerClient(transport); - // Store the new script handler locally, so when it's time to remove it we - // don't need to go through the iframe, since it might be cleared. - this.onNewScript = this.debuggerWindow.SourceScripts.onNewScript; - let self = this; - this._client.addListener("tabNavigated", this._debugTab); - this._client.addListener("tabDetached", this._close); - this._client.addListener("newScript", this.onNewScript); - this._client.connect(function(aType, aTraits) { - self._client.listTabs(function(aResponse) { - let tab = aResponse.tabs[aResponse.selected]; - self.debuggerWindow.startDebuggingTab(self._client, tab); - if (self.onConnected) { - self.onConnected(self); - } - }); - }); - }, - - /** - * Starts debugging the current tab. This function is called on each location - * change in this tab. - */ - debugTab: function DP_debugTab(aNotification, aPacket) { - let self = this; - this._client.activeThread.detach(function() { - self._client.activeTab.detach(function() { - self._client.listTabs(function(aResponse) { - let tab = aResponse.tabs[aResponse.selected]; - self.debuggerWindow.startDebuggingTab(self._client, tab); - if (self.onConnected) { - self.onConnected(self); - } - }); - }); - }); - }, - - get debuggerWindow() { - return this.frame ? this.frame.contentWindow : null; - }, - - get debuggerClient() { - return this._client; - }, - - get activeThread() { - try { - return this.debuggerWindow.ThreadState.activeThread; - } catch(ex) { - return undefined; - } - } -}; - function DebuggerUI(aWindow) { - this.aWindow = aWindow; - - aWindow.addEventListener("Debugger:LoadSource", this._onLoadSource.bind(this)); + this.chromeWindow = aWindow; } DebuggerUI.prototype = { - /** - * Starts the debugger or stops it, if it is already started. - */ - toggleDebugger: function DebuggerUI_toggleDebugger() { - if (!DebuggerServer.initialized) { - DebuggerServer.init(); - DebuggerServer.addBrowserActors(); - } - let gBrowser = this.aWindow.gBrowser; - let tab = gBrowser.selectedTab; + /** + * Starts a debugger for the current tab, or stops it if already started. + * @return DebuggerPane if the debugger is started, null if it's stopped. + */ + toggleDebugger: function DUI_toggleDebugger() { + let tab = this.chromeWindow.gBrowser.selectedTab; if (tab._scriptDebugger) { - // If the debugger is already open, just close it. tab._scriptDebugger.close(); - return tab._scriptDebugger; + return null; } - - let pane = new DebuggerPane(tab); - pane.create(gBrowser); - return pane; + return new DebuggerPane(tab); }, - getDebugger: function DebuggerUI_getDebugger(aTab) { + /** + * Get the debugger for a specified tab. + * @return DebuggerPane if a debugger exists for the tab, null otherwise + */ + getDebugger: function DUI_getDebugger(aTab) { return aTab._scriptDebugger; }, + /** + * Get the preferences associated with the debugger frontend. + * @return object + */ get preferences() { return DebuggerUIPreferences; + } +}; + +/** + * Creates a pane that will host the debugger. + * + * @param XULElement aTab + * The tab in which to create the debugger. + */ +function DebuggerPane(aTab) { + this._tab = aTab; + this._create(); +} + +DebuggerPane.prototype = { + + /** + * Creates and initializes the widgets containing the debugger UI. + */ + _create: function DP__create() { + this._tab._scriptDebugger = this; + + let gBrowser = this._tab.linkedBrowser.getTabBrowser(); + let ownerDocument = gBrowser.parentNode.ownerDocument; + + this._splitter = ownerDocument.createElement("splitter"); + this._splitter.setAttribute("class", "hud-splitter"); + + this._frame = ownerDocument.createElement("iframe"); + this._frame.height = DebuggerUIPreferences.height; + + this._nbox = gBrowser.getNotificationBox(this._tab.linkedBrowser); + this._nbox.appendChild(this._splitter); + this._nbox.appendChild(this._frame); + + this.close = this.close.bind(this); + let self = this; + + this._frame.addEventListener("Debugger:Loaded", function dbgLoaded() { + self._frame.removeEventListener("Debugger:Loaded", dbgLoaded, true); + self._frame.addEventListener("Debugger:Close", self.close, true); + self._frame.addEventListener("unload", self.close, true); + + // Bind shortcuts for accessing the breakpoint methods in the debugger. + let bkp = self.debuggerWindow.DebuggerController.Breakpoints; + self.addBreakpoint = bkp.addBreakpoint; + self.removeBreakpoint = bkp.removeBreakpoint; + self.getBreakpoint = bkp.getBreakpoint; + }, true); + + this._frame.setAttribute("src", "chrome://browser/content/debugger.xul"); }, /** - * 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/ + * Closes the debugger, removing child nodes and event listeners. */ - _onLoadSource: function DebuggerUI__onLoadSource(aEvent) { - let gBrowser = this.aWindow.gBrowser; - - let url = aEvent.detail.url; - let showOptions = aEvent.detail.options; - let scheme = Services.io.extractScheme(url); - switch (scheme) { - case "file": - case "chrome": - case "resource": - try { - NetUtil.asyncFetch(url, function onFetch(aStream, aStatus) { - if (!Components.isSuccessCode(aStatus)) { - return this.logError(url, aStatus); - } - let source = NetUtil.readInputStreamToString(aStream, aStream.available()); - aStream.close(); - this._onSourceLoaded(url, source, showOptions); - }.bind(this)); - } catch (ex) { - return this.logError(url, ex.name); - } - break; - - default: - let channel = Services.io.newChannel(url, null, null); - let chunks = []; - let streamListener = { // nsIStreamListener inherits nsIRequestObserver - onStartRequest: function (aRequest, aContext, aStatusCode) { - if (!Components.isSuccessCode(aStatusCode)) { - return this.logError(url, aStatusCode); - } - }.bind(this), - onDataAvailable: function (aRequest, aContext, aStream, aOffset, aCount) { - chunks.push(NetUtil.readInputStreamToString(aStream, aCount)); - }, - onStopRequest: function (aRequest, aContext, aStatusCode) { - if (!Components.isSuccessCode(aStatusCode)) { - return this.logError(url, aStatusCode); - } - - this._onSourceLoaded(url, chunks.join(""), channel.contentType, - showOptions); - }.bind(this) - }; - - channel.loadFlags = channel.LOAD_FROM_CACHE; - channel.asyncOpen(streamListener, null); - break; + close: function DP_close() { + if (!this._tab) { + return; } + this._tab._scriptDebugger = null; + this._tab = null; + + DebuggerUIPreferences.height = this._frame.height; + this._frame.removeEventListener("Debugger:Close", this.close, true); + this._frame.removeEventListener("unload", this.close, true); + + this._nbox.removeChild(this._splitter); + this._nbox.removeChild(this._frame); + + this._splitter = null; + this._frame = null; + this._nbox = null; }, /** - * 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. + * Gets the debugger content window. + * @return nsIDOMWindow if a debugger window exists, null otherwise */ - logError: function DebuggerUI_logError(aUrl, aStatus) { - let view = this.getDebugger(gBrowser.selectedTab).DebuggerView; - Components.utils.reportError(view.getFormatStr("loadingError", [ aUrl, aStatus ])); + get debuggerWindow() { + return this._frame ? this._frame.contentWindow : null; }, /** - * 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] - * Additional options for showing the script (optional). Supported - * options: - * - targetLine: place the editor at the given line number. + * Shortcut for accessing the list of breakpoints in the debugger. + * @return object if a debugger window exists, null otherwise */ - _onSourceLoaded: function DebuggerUI__onSourceLoaded(aSourceUrl, - aSourceText, - aContentType, - aOptions) { - let dbg = this.getDebugger(this.aWindow.gBrowser.selectedTab); - let doc = dbg.frame.contentDocument; - let scripts = doc.getElementById("scripts"); - let elt = scripts.getElementsByAttribute("value", aSourceUrl)[0]; - let script = elt.getUserData("sourceScript"); - script.loaded = true; - script.text = aSourceText; - script.contentType = aContentType; - elt.setUserData("sourceScript", script, null); - - dbg.debuggerWindow.SourceScripts._onShowScript(script, aOptions); + get breakpoints() { + let debuggerWindow = this.debuggerWindow; + if (debuggerWindow) { + return debuggerWindow.DebuggerController.Breakpoints.store; + } + return null; } }; @@ -592,16 +190,12 @@ DebuggerUI.prototype = { */ let DebuggerUIPreferences = { - _height: -1, - /** * Gets the preferred height of the debugger pane. - * * @return number - * The preferred height. */ get height() { - if (this._height < 0) { + if (this._height === undefined) { this._height = Services.prefs.getIntPref("devtools.debugger.ui.height"); } return this._height; @@ -609,9 +203,7 @@ let DebuggerUIPreferences = { /** * Sets the preferred height of the debugger pane. - * * @param number value - * The new height. */ set height(value) { Services.prefs.setIntPref("devtools.debugger.ui.height", value); diff --git a/browser/devtools/debugger/debugger-controller.js b/browser/devtools/debugger/debugger-controller.js new file mode 100644 index 000000000000..b2720999567b --- /dev/null +++ b/browser/devtools/debugger/debugger-controller.js @@ -0,0 +1,1282 @@ +/* -*- Mode: javascript; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ft=javascript ts=2 et sw=2 tw=80: */ +/* ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is mozilla.org code. + * + * The Initial Developer of the Original Code is + * Mozilla Foundation + * Portions created by the Initial Developer are Copyright (C) 2011 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * Dave Camp + * Panos Astithas + * Victor Porof + * Mihai Sucan + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ +"use strict"; + +const Cc = Components.classes; +const Ci = Components.interfaces; +const Cu = Components.utils; + +const DBG_STRINGS_URI = "chrome://browser/locale/devtools/debugger.properties"; + +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.initializeEditor(); + DebuggerView.StackFrames.initialize(); + DebuggerView.Properties.initialize(); + DebuggerView.Scripts.initialize(); + + 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.destroyEditor(); + DebuggerView.Scripts.destroy(); + DebuggerView.StackFrames.destroy(); + DebuggerView.Properties.destroy(); + + DebuggerController.SourceScripts.disconnect(); + DebuggerController.StackFrames.disconnect(); + DebuggerController.ThreadState.disconnect(); + + this.dispatchEvent("Debugger:Unloaded"); + this._disconnect(); + }, + + /** + * Initializes a debugger client and connects it to the debugger server, + * wiring event handlers as necessary. + */ + _connect: function DC__connect() { + if (!DebuggerServer.initialized) { + DebuggerServer.init(); + DebuggerServer.addBrowserActors(); + } + + let transport = 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)); + }, + + /** + * 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); +} + +StackFrames.prototype = { + + /** + * The maximum number of frames allowed to be loaded at a time. + */ + pageSize: 25, + + /** + * The currently selected frame depth. + */ + selectedFrame: null, + + /** + * 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) { + 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._onFramesCleared(); + + aCallback && aCallback(); + }, + + /** + * Disconnect from the client. + */ + disconnect: function SF_disconnect() { + 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. + */ + _onPaused: function SF__onPaused() { + 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(); + return; + } + DebuggerView.StackFrames.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() { + DebuggerView.StackFrames.emptyText(); + this.selectedFrame = null; + + // Clear the properties as well. + DebuggerView.Properties.localScope.empty(); + DebuggerView.Properties.globalScope.empty(); + }, + + /** + * 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; + + // Move the editor's caret to the proper line. + if (DebuggerView.Scripts.isSelected(url) && line) { + editor.setDebugLocation(line - 1); + } else { + editor.setDebugLocation(-1); + } + }, + + /** + * 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; + let editor = DebuggerView.editor; + + // Move the editor's caret to the proper line. + if (DebuggerView.Scripts.isSelected(url) && line) { + editor.setCaretPosition(line - 1); + editor.setDebugLocation(line - 1); + } + else if (DebuggerView.Scripts.contains(url)) { + DebuggerView.Scripts.selectScript(url); + editor.setCaretPosition(line - 1); + } + else { + editor.setDebugLocation(-1); + } + + // Display the local variables. + let localScope = DebuggerView.Properties.localScope; + localScope.empty(); + + // Add "this". + if (frame.this) { + let thisVar = localScope.addVar("this"); + thisVar.setGrip({ + type: frame.this.type, + class: frame.this.class + }); + this._addExpander(thisVar, frame.this); + } + + if (frame.arguments && frame.arguments.length > 0) { + // Add "arguments". + let argsVar = localScope.addVar("arguments"); + argsVar.setGrip({ + type: "object", + class: "Arguments" + }); + this._addExpander(argsVar, frame.arguments); + + // Add variables for every argument. + let objClient = this.activeThread.pauseGrip(frame.callee); + objClient.getSignature(function SF_getSignature(aResponse) { + for (let i = 0, l = aResponse.parameters.length; i < l; i++) { + let param = aResponse.parameters[i]; + let paramVar = localScope.addVar(param); + let paramVal = frame.arguments[i]; + + paramVar.setGrip(paramVal); + this._addExpander(paramVar, paramVal); + } + + // Signal that call parameters have been fetched. + DebuggerController.dispatchEvent("Debugger:FetchedParameters"); + + }.bind(this)); + } + }, + + /** + * Adds a 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, but we do need them + // for frame.arguments which is a regular array. + if (!aObject || typeof aObject !== "object" || + (aObject.type !== "object" && !Array.isArray(aObject))) { + 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; + } + + // For arrays we have to construct a grip-like object. + if (Array.isArray(aObject)) { + let properties = { length: { value: aObject.length } }; + for (let i = 0, l = aObject.length; i < l; i++) { + properties[i] = { value: aObject[i] }; + } + aVar.addProperties(properties); + + // Expansion handlers must be set after the properties are added. + for (let i = 0, l = aObject.length; i < l; i++) { + this._addExpander(aVar[i], aObject[i]); + } + + aVar.fetched = true; + return; + } + + let objClient = this.activeThread.pauseGrip(aObject); + objClient.getPrototypeAndProperties(function SF_onProtoAndProps(aResponse) { + // 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); + } + + // 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); + } + + 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 + ")"; + } +}; + +/** + * 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) { + this._addScript({ url: aPacket.url, startLine: aPacket.startLine }); + }, + + /** + * Handler for the thread client's scriptsadded notification. + */ + _onScriptsAdded: function SS__onScriptsAdded() { + for each (let script in this.activeThread.cachedScripts) { + this._addScript(script); + } + }, + + /** + * Handler for the thread client's scriptscleared notification. + */ + _onScriptsCleared: function SS__onScriptsCleared() { + DebuggerView.Scripts.empty(); + }, + + /** + * 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; + } + + if (this._trimUrlQuery(aUrl).slice(-3) == ".js") { + DebuggerView.editor.setMode(SourceEditor.MODES.JAVASCRIPT); + } else { + DebuggerView.editor.setMode(SourceEditor.MODES.HTML); + } + }, + + /** + * Trims the query part of a url string, if necessary. + * + * @param string aUrl + * The script url. + * @return string + */ + _trimUrlQuery: function SS__trimUrlQuery(aUrl) { + let q = aUrl.indexOf('?'); + if (q > -1) { + return aUrl.slice(0, q); + } + return aUrl; + }, + + /** + * Gets a unique, simplified label from a script url. + * ex: a). ici://some.address.com/random/subrandom/ + * b). ni://another.address.org/random/subrandom/page.html + * c). san://interesting.address.gro/random/script.js + * d). si://interesting.address.moc/random/another/script.js + * => + * a). subrandom/ + * b). page.html + * c). script.js + * d). another/script.js + * + * @param string aUrl + * The script url. + * @param string aHref + * The content location href to be used. If unspecified, it will + * defalult to debugged panrent window location. + * @return string + * The simplified label. + */ + _getScriptLabel: function SS__getScriptLabel(aUrl, aHref) { + let url = this._trimUrlQuery(aUrl); + + if (this._labelsCache[url]) { + return this._labelsCache[url]; + } + + let href = aHref || window.parent.content.location.href; + let pathElements = url.split("/"); + let label = pathElements.pop() || (pathElements.pop() + "/"); + + // if the label as a leaf name is alreay present in the scripts list + if (DebuggerView.Scripts.containsLabel(label)) { + label = url.replace(href.substring(0, href.lastIndexOf("/") + 1), ""); + + // if the path/to/script is exactly the same, we're in different domains + if (DebuggerView.Scripts.containsLabel(label)) { + label = url; + } + } + + return this._labelsCache[url] = label; + }, + + /** + * 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 and display it in the editor if the + * editor is empty. + */ + _addScript: function SS__addScript(aScript) { + DebuggerView.Scripts.addScript(this._getScriptLabel(aScript.url), aScript); + + if (DebuggerView.editor.getCharCount() == 0) { + this.showScript(aScript); + } + }, + + /** + * 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 || {}; + + 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: Tt 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); + }, + + /** + * 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) { + Components.utils.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 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; + + let callback = function(aClient, aError) { + if (aError) { + this._skipEditorBreakpointChange = true; + let result = this.editor.removeBreakpoint(aBreakpoint.line); + this._skipEditorBreakpointChange = false; + } + }.bind(this); + this.addBreakpoint({ url: url, line: line }, callback, 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; + }, + + /** + * 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. + */ + addBreakpoint: + function BP_addBreakpoint(aLocation, aCallback, aNoEditorUpdate) { + let breakpoint = this.getBreakpoint(aLocation.url, aLocation.line); + if (breakpoint) { + aCallback && aCallback(breakpoint); + return; + } + + this.activeThread.setBreakpoint(aLocation, function(aResponse, aBpClient) { + if (!aResponse.error) { + this.store[aBpClient.actor] = aBpClient; + + if (!aNoEditorUpdate) { + let url = DebuggerView.Scripts.selected; + if (url == aLocation.url) { + this._skipEditorBreakpointChange = true; + this.editor.addBreakpoint(aLocation.line - 1); + this._skipEditorBreakpointChange = false; + } + } + } + + aCallback && aCallback(aBpClient, aResponse.error); + }.bind(this)); + }, + + /** + * 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. + */ + removeBreakpoint: + function BP_removeBreakpoint(aBreakpoint, aCallback, aNoEditorUpdate) { + 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; + } + } + + 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); +}); + +/** + * 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; } +}); diff --git a/browser/devtools/debugger/debugger-view.js b/browser/devtools/debugger/debugger-view.js index 6b335e19eff0..4620d6f3237e 100644 --- a/browser/devtools/debugger/debugger-view.js +++ b/browser/devtools/debugger/debugger-view.js @@ -1,6 +1,6 @@ /* -*- Mode: javascript; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ -/* vim: set ts=2 et sw=2 tw=80: */ -/***** BEGIN LICENSE BLOCK ***** +/* vim: set ft=javascript ts=2 et sw=2 tw=80: */ +/* ***** BEGIN LICENSE BLOCK ***** * Version: MPL 1.1/GPL 2.0/LGPL 2.1 * * The contents of this file are subject to the Mozilla Public License Version @@ -32,19 +32,13 @@ * under the terms of either the GPL or the LGPL, and not to allow others to * use your version of this file under the terms of the MPL, indicate your * decision by deleting the provisions above and replace them with the notice - * and other provisions required by the LGPL or the GPL. If you do not delete + * and other provisions required by the GPL or the LGPL. If you do not delete * the provisions above, a recipient may use your version of this file under * the terms of any one of the MPL, the GPL or the LGPL. * - ***** END LICENSE BLOCK *****/ + * ***** END LICENSE BLOCK ***** */ "use strict"; -const Cu = Components.utils; -const DBG_STRINGS_URI = "chrome://browser/locale/devtools/debugger.properties"; - -Cu.import("resource://gre/modules/XPCOMUtils.jsm"); -Cu.import('resource://gre/modules/Services.jsm'); - /** * Object mediating visual changes and event listeners between the debugger and * the html view. @@ -52,35 +46,212 @@ Cu.import('resource://gre/modules/Services.jsm'); let DebuggerView = { /** - * L10N shortcut function - * - * @param string aName - * @return string + * An instance of SourceEditor. */ - getStr: function DV_getStr(aName) { - return this.stringBundle.GetStringFromName(aName); + editor: null, + + /** + * Initializes the SourceEditor instance. + */ + initializeEditor: function DV_initializeEditor() { + let placeholder = document.getElementById("editor"); + + let config = { + mode: SourceEditor.MODES.JAVASCRIPT, + showLineNumbers: true, + readOnly: true, + showAnnotationRuler: true, + showOverviewRuler: true, + }; + + this.editor = new SourceEditor(); + this.editor.init(placeholder, config, this._onEditorLoad.bind(this)); }, /** - * L10N shortcut function - * - * @param string aName - * @param array aArray - * @return string + * Removes the SourceEditor instance and added breakpoints. */ - getFormatStr: function DV_getFormatStr(aName, aArray) { - return this.stringBundle.formatStringFromName(aName, aArray, aArray.length); + destroyEditor: function DV_destroyEditor() { + DebuggerController.Breakpoints.destroy(); + this.editor = null; + }, + + /** + * The load event handler for the source editor. This method does post-load + * editor initialization. + */ + _onEditorLoad: function DV__onEditorLoad() { + DebuggerController.Breakpoints.initialize(); } }; -XPCOMUtils.defineLazyGetter(DebuggerView, "stringBundle", function() { - return Services.strings.createBundle(DBG_STRINGS_URI); -}); +/** + * Functions handling the scripts UI. + */ +function ScriptsView() { + this._onScriptsChange = this._onScriptsChange.bind(this); +} + +ScriptsView.prototype = { + + /** + * Removes all elements from the scripts container, leaving it empty. + */ + empty: function DVS_empty() { + while (this._scripts.firstChild) { + this._scripts.removeChild(this._scripts.firstChild); + } + }, + + /** + * Checks whether the script with the specified URL is among the scripts + * known to the debugger and shown in the list. + * + * @param string aUrl + * The script URL. + * @return boolean + */ + contains: function DVS_contains(aUrl) { + if (this._scripts.getElementsByAttribute("value", aUrl).length > 0) { + return true; + } + return false; + }, + + /** + * Checks whether the script with the specified label is among the scripts + * known to the debugger and shown in the list. + * + * @param string aLabel + * The script label. + * @return boolean + */ + containsLabel: function DVS_containsLabel(aLabel) { + if (this._scripts.getElementsByAttribute("label", aLabel).length > 0) { + return true; + } + return false; + }, + + /** + * Selects the script with the specified URL from the list. + * + * @param string aUrl + * The script URL. + */ + selectScript: function DVS_selectScript(aUrl) { + for (let i = 0, l = this._scripts.itemCount; i < l; i++) { + if (this._scripts.getItemAtIndex(i).value == aUrl) { + this._scripts.selectedIndex = i; + return; + } + } + }, + + /** + * Checks whether the script with the specified URL is selected in the list. + * + * @param string aUrl + * The script URL. + */ + isSelected: function DVS_isSelected(aUrl) { + if (this._scripts.selectedItem && + this._scripts.selectedItem.value == aUrl) { + return true; + } + return false; + }, + + /** + * Retrieve the URL of the selected script. + * @return string | null + */ + get selected() { + return this._scripts.selectedItem ? + this._scripts.selectedItem.value : null; + }, + + /** + * Returns the list of URIs for scripts in the page. + * @return array + */ + get scriptLocations() { + let locations = []; + for (let i = 0, l = this._scripts.itemCount; i < l; i++) { + locations.push(this._scripts.getItemAtIndex(i).value); + } + return locations; + }, + + /** + * Adds a script to the scripts container. + * If the script already exists (was previously added), null is returned. + * Otherwise, the newly created element is returned. + * + * @param string aLabel + * The simplified script location to be shown. + * @param string aScript + * The source script. + * @return object + * The newly created html node representing the added script. + */ + addScript: function DVS_addScript(aLabel, aScript) { + // Make sure we don't duplicate anything. + if (this.containsLabel(aLabel)) { + return null; + } + + let script = this._scripts.appendItem(aLabel, aScript.url); + script.setAttribute("tooltiptext", aScript.url); + script.setUserData("sourceScript", aScript, null); + + this._scripts.selectedItem = script; + return script; + }, + + /** + * The cached click listener for the scripts container. + */ + _onScriptsChange: function DVS__onScriptsChange() { + let script = this._scripts.selectedItem.getUserData("sourceScript"); + DebuggerController.SourceScripts.showScript(script); + }, + + /** + * The cached scripts container. + */ + _scripts: null, + + /** + * Initialization function, called when the debugger is initialized. + */ + initialize: function DVS_initialize() { + this._scripts = document.getElementById("scripts"); + this._scripts.addEventListener("select", this._onScriptsChange, false); + }, + + /** + * Destruction function, called when the debugger is shut down. + */ + destroy: function DVS_destroy() { + this._scripts.removeEventListener("select", this._onScriptsChange, false); + this._scripts = null; + } +}; /** * Functions handling the html stackframes UI. */ -DebuggerView.Stackframes = { +function StackFramesView() { + this._onFramesScroll = this._onFramesScroll.bind(this); + this._onCloseButtonClick = this._onCloseButtonClick.bind(this); + this._onResumeButtonClick = this._onResumeButtonClick.bind(this); + this._onStepOverClick = this._onStepOverClick.bind(this); + this._onStepInClick = this._onStepInClick.bind(this); + this._onStepOutClick = this._onStepOutClick.bind(this); +} + +StackFramesView.prototype = { /** * Sets the current frames state based on the debugger active thread state. @@ -93,31 +264,21 @@ DebuggerView.Stackframes = { let status = document.getElementById("status"); // If we're paused, show a pause label and a resume label on the button. - if (aState === "paused") { - status.textContent = DebuggerView.getStr("pausedState"); - resume.label = DebuggerView.getStr("resumeLabel"); - } else if (aState === "attached") { - // If we're attached, do the opposite. - status.textContent = DebuggerView.getStr("runningState"); - resume.label = DebuggerView.getStr("pauseLabel"); - } else { - // No valid state parameter. + if (aState == "paused") { + status.textContent = L10N.getStr("pausedState"); + resume.label = L10N.getStr("resumeLabel"); + } + // If we're attached, do the opposite. + else if (aState == "attached") { + status.textContent = L10N.getStr("runningState"); + resume.label = L10N.getStr("pauseLabel"); + } + // No valid state parameter. + else { status.textContent = ""; } }, - /** - * Sets the onClick listener for the stackframes container. - * - * @param function aHandler - * The delegate used as the event listener. - */ - addClickListener: function DVF_addClickListener(aHandler) { - // save the handler so it can be removed on shutdown - this._onFramesClick = aHandler; - this._frames.addEventListener("click", aHandler, false); - }, - /** * Removes all elements from the stackframes container, leaving it empty. */ @@ -132,14 +293,14 @@ DebuggerView.Stackframes = { * with an empty text note attached. */ emptyText: function DVF_emptyText() { - // make sure the container is empty first + // Make sure the container is empty first. this.empty(); let item = document.createElement("div"); - // the empty node should look grayed out to avoid confusion + // The empty node should look grayed out to avoid confusion. item.className = "empty list-item"; - item.appendChild(document.createTextNode(DebuggerView.getStr("emptyText"))); + item.appendChild(document.createTextNode(L10N.getStr("emptyText"))); this._frames.appendChild(item); }, @@ -159,7 +320,7 @@ DebuggerView.Stackframes = { * The newly created html node representing the added frame. */ addFrame: function DVF_addFrame(aDepth, aFrameNameText, aFrameDetailsText) { - // make sure we don't duplicate anything + // Make sure we don't duplicate anything. if (document.getElementById("stackframe-" + aDepth)) { return null; } @@ -168,11 +329,11 @@ DebuggerView.Stackframes = { let frameName = document.createElement("span"); let frameDetails = document.createElement("span"); - // create a list item to be added to the stackframes container + // Create a list item to be added to the stackframes container. frame.id = "stackframe-" + aDepth; frame.className = "dbg-stackframe list-item"; - // this list should display the name and details for the frame + // This list should display the name and details for the frame. frameName.className = "dbg-stackframe-name"; frameDetails.className = "dbg-stackframe-details"; frameName.appendChild(document.createTextNode(aFrameNameText)); @@ -183,7 +344,7 @@ DebuggerView.Stackframes = { this._frames.appendChild(frame); - // return the element for later use if necessary + // Return the element for later use if necessary. return frame; }, @@ -192,27 +353,37 @@ DebuggerView.Stackframes = { * * @param number aDepth * The frame depth specified by the debugger. - * @param boolean aSelect - * True if the frame should be selected, false otherwise. + * @param boolean aFlag + * True if the frame should be deselected, false otherwise. */ - highlightFrame: function DVF_highlightFrame(aDepth, aSelect) { + highlightFrame: function DVF_highlightFrame(aDepth, aFlag) { let frame = document.getElementById("stackframe-" + aDepth); - // the list item wasn't found in the stackframe container + // The list item wasn't found in the stackframe container. if (!frame) { return; } - // add the 'selected' css class if the frame isn't already selected - if (aSelect && !frame.classList.contains("selected")) { + // Add the 'selected' css class if the frame isn't already selected. + if (!aFlag && !frame.classList.contains("selected")) { frame.classList.add("selected"); - - // remove the 'selected' css class if the frame is already selected - } else if (!aSelect && frame.classList.contains("selected")) { + } + // Remove the 'selected' css class if the frame is already selected. + else if (aFlag && frame.classList.contains("selected")) { frame.classList.remove("selected"); } }, + /** + * Deselects a frame from the stackframe container. + * + * @param number aDepth + * The frame depth specified by the debugger. + */ + unhighlightFrame: function DVF_unhighlightFrame(aDepth) { + this.highlightFrame(aDepth, true) + }, + /** * Gets the current dirty state. * @@ -234,26 +405,36 @@ DebuggerView.Stackframes = { }, /** - * The cached click listener for the stackframes container. + * Listener handling the stackframes container click event. */ - _onFramesClick: null, + _onFramesClick: function DVF__onFramesClick(aEvent) { + let target = aEvent.target; + + while (target) { + if (target.debuggerFrame) { + DebuggerController.StackFrames.selectFrame(target.debuggerFrame.depth); + return; + } + target = target.parentNode; + } + }, /** * Listener handling the stackframes container scroll event. */ _onFramesScroll: function DVF__onFramesScroll(aEvent) { - // update the stackframes container only if we have to + // Update the stackframes container only if we have to. if (this._dirty) { let clientHeight = this._frames.clientHeight; let scrollTop = this._frames.scrollTop; let scrollHeight = this._frames.scrollHeight; - // if the stackframes container was scrolled past 95% of the height, - // load more content + // If the stackframes container was scrolled past 95% of the height, + // load more content. if (scrollTop >= (scrollHeight - clientHeight) * 0.95) { this._dirty = false; - StackFrames._addMoreFrames(); + DebuggerController.StackFrames.addMoreFrames(); } } }, @@ -262,21 +443,17 @@ DebuggerView.Stackframes = { * Listener handling the close button click event. */ _onCloseButtonClick: function DVF__onCloseButtonClick() { - let root = document.documentElement; - let debuggerClose = document.createEvent("Events"); - - debuggerClose.initEvent("DebuggerClose", true, false); - root.dispatchEvent(debuggerClose); + DebuggerController.dispatchEvent("Debugger:Close"); }, /** * Listener handling the pause/resume button click event. */ _onResumeButtonClick: function DVF__onResumeButtonClick() { - if (ThreadState.activeThread.paused) { - ThreadState.activeThread.resume(); + if (DebuggerController.activeThread.paused) { + DebuggerController.activeThread.resume(); } else { - ThreadState.activeThread.interrupt(); + DebuggerController.activeThread.interrupt(); } }, @@ -284,21 +461,21 @@ DebuggerView.Stackframes = { * Listener handling the step over button click event. */ _onStepOverClick: function DVF__onStepOverClick() { - ThreadState.activeThread.stepOver(); + DebuggerController.activeThread.stepOver(); }, /** * Listener handling the step in button click event. */ _onStepInClick: function DVF__onStepInClick() { - ThreadState.activeThread.stepIn(); + DebuggerController.activeThread.stepIn(); }, /** * Listener handling the step out button click event. */ _onStepOutClick: function DVF__onStepOutClick() { - ThreadState.activeThread.stepOut(); + DebuggerController.activeThread.stepOut(); }, /** @@ -327,6 +504,7 @@ DebuggerView.Stackframes = { stepOver.addEventListener("click", this._onStepOverClick, false); stepIn.addEventListener("click", this._onStepInClick, false); stepOut.addEventListener("click", this._onStepOutClick, false); + frames.addEventListener("click", this._onFramesClick, false); frames.addEventListener("scroll", this._onFramesScroll, false); window.addEventListener("resize", this._onFramesScroll, false); @@ -360,7 +538,13 @@ DebuggerView.Stackframes = { /** * Functions handling the properties view. */ -DebuggerView.Properties = { +function PropertiesView() { + this._addScope = this._addScope.bind(this); + this._addVar = this._addVar.bind(this); + this._addProperties = this._addProperties.bind(this); +} + +PropertiesView.prototype = { /** * Adds a scope to contain any inspected variables. @@ -376,18 +560,18 @@ DebuggerView.Properties = { * if a node was not created. */ _addScope: function DVP__addScope(aName, aId) { - // make sure the parent container exists + // Make sure the parent container exists. if (!this._vars) { return null; } - // compute the id of the element if not specified + // Compute the id of the element if not specified. aId = aId || (aName.toLowerCase().trim().replace(" ", "-") + "-scope"); - // contains generic nodes and functionality + // Contains generic nodes and functionality. let element = this._createPropertyElement(aName, aId, "scope", this._vars); - // make sure the element was created successfully + // Make sure the element was created successfully. if (!element) { return null; } @@ -397,7 +581,7 @@ DebuggerView.Properties = { */ element.addVar = this._addVar.bind(this, element); - // return the element for later use if necessary + // Return the element for later use if necessary. return element; }, @@ -416,19 +600,19 @@ DebuggerView.Properties = { * The newly created html node representing the added var. */ _addVar: function DVP__addVar(aScope, aName, aId) { - // make sure the scope container exists + // Make sure the scope container exists. if (!aScope) { return null; } - // compute the id of the element if not specified + // Compute the id of the element if not specified. aId = aId || (aScope.id + "->" + aName + "-variable"); - // contains generic nodes and functionality + // Contains generic nodes and functionality. let element = this._createPropertyElement(aName, aId, "variable", aScope.querySelector(".details")); - // make sure the element was created successfully + // Make sure the element was created successfully. if (!element) { return null; } @@ -443,18 +627,18 @@ DebuggerView.Properties = { */ element.addProperties = this._addProperties.bind(this, element); - // setup the additional elements specific for a variable node + // Setup the additional elements specific for a variable node. element.refresh(function() { let separator = document.createElement("span"); let info = document.createElement("span"); let title = element.querySelector(".title"); let arrow = element.querySelector(".arrow"); - // separator shouldn't be selectable + // Separator shouldn't be selectable. separator.className = "unselectable"; separator.appendChild(document.createTextNode(": ")); - // the variable information (type, class and/or value) + // The variable information (type, class and/or value). info.className = "info"; title.appendChild(separator); @@ -462,7 +646,7 @@ DebuggerView.Properties = { }.bind(this)); - // return the element for later use if necessary + // Return the element for later use if necessary. return element; }, @@ -481,21 +665,27 @@ DebuggerView.Properties = { * e.g. 42 * true * "nasu" - * { type: "undefined" } } - * { type: "null" } } - * { type: "object", class: "Object" } } + * { type: "undefined" } + * { type: "null" } + * { type: "object", class: "Object" } * @return object * The same variable. */ _setGrip: function DVP__setGrip(aVar, aGrip) { - // make sure the variable container exists + // Make sure the variable container exists. if (!aVar) { return null; } + if (aGrip === undefined) { + aGrip = { type: "undefined" }; + } + if (aGrip === null) { + aGrip = { type: "null" }; + } let info = aVar.querySelector(".info") || aVar.target.info; - // make sure the info node exists + // Make sure the info node exists. if (!info) { return null; } @@ -523,30 +713,30 @@ DebuggerView.Properties = { * "someProp3": { value: { type: "undefined" } }, * "someProp4": { value: { type: "null" } }, * "someProp5": { value: { type: "object", class: "Object" } }, - * "someProp6": { get: { "type": "object", "class": "Function" }, - * set: { "type": "undefined" } } } + * "someProp6": { get: { type: "object", class: "Function" }, + * set: { type: "undefined" } } * @return object * The same variable. */ _addProperties: function DVP__addProperties(aVar, aProperties) { - // for each property, add it using the passed object key/grip + // For each property, add it using the passed object key/grip. for (let i in aProperties) { // Can't use aProperties.hasOwnProperty(i), because it may be overridden. if (Object.getOwnPropertyDescriptor(aProperties, i)) { - // get the specified descriptor for current property + // Get the specified descriptor for current property. let desc = aProperties[i]; - // as described in the remote debugger protocol, the value grip must be - // contained in a 'value' property + // As described in the remote debugger protocol, the value grip must be + // contained in a 'value' property. let value = desc["value"]; - // for accessor property descriptors, the two grips need to be - // contained in 'get' and 'set' properties + // For accessor property descriptors, the two grips need to be + // contained in 'get' and 'set' properties. let getter = desc["get"]; let setter = desc["set"]; - // handle data property and accessor property descriptors + // Handle data property and accessor property descriptors. if (value !== undefined) { this._addProperty(aVar, [i, value]); } @@ -585,19 +775,19 @@ DebuggerView.Properties = { * The newly created html node representing the added prop. */ _addProperty: function DVP__addProperty(aVar, aProperty, aName, aId) { - // make sure the variable container exists + // Make sure the variable container exists. if (!aVar) { return null; } - // compute the id of the element if not specified + // Compute the id of the element if not specified. aId = aId || (aVar.id + "->" + aProperty[0] + "-property"); - // contains generic nodes and functionality + // Contains generic nodes and functionality. let element = this._createPropertyElement(aName, aId, "property", aVar.querySelector(".details")); - // make sure the element was created successfully + // Make sure the element was created successfully. if (!element) { return null; } @@ -612,7 +802,7 @@ DebuggerView.Properties = { */ element.addProperties = this._addProperties.bind(this, element); - // setup the additional elements specific for a variable node + // Setup the additional elements specific for a variable node. element.refresh(function(pKey, pGrip) { let propertyString = this._propertyString(pGrip); let propertyColor = this._propertyColor(pGrip); @@ -622,41 +812,41 @@ DebuggerView.Properties = { let title = element.querySelector(".title"); let arrow = element.querySelector(".arrow"); - // use a key element to specify the property name + // Use a key element to specify the property name. key.className = "key"; key.appendChild(document.createTextNode(pKey)); - // use a value element to specify the property value + // Use a value element to specify the property value. value.className = "value"; value.appendChild(document.createTextNode(propertyString)); value.classList.add(propertyColor); - // separator shouldn't be selected + // Separator shouldn't be selected. separator.className = "unselectable"; separator.appendChild(document.createTextNode(": ")); - if (pKey) { + if ("undefined" !== typeof pKey) { title.appendChild(key); } - if (pGrip) { + if ("undefined" !== typeof pGrip) { title.appendChild(separator); title.appendChild(value); } - // make the property also behave as a variable, to allow - // recursively adding properties to properties + // Make the property also behave as a variable, to allow + // recursively adding properties to properties. element.target = { info: value }; - // save the property to the variable for easier access + // Save the property to the variable for easier access. Object.defineProperty(aVar, pKey, { value: element, writable: false, enumerable: true, configurable: true }); }.bind(this), aProperty); - // return the element for later use if necessary + // Return the element for later use if necessary. return element; }, @@ -670,13 +860,13 @@ DebuggerView.Properties = { */ _propertyString: function DVP__propertyString(aGrip) { if (aGrip && "object" === typeof aGrip) { - switch (aGrip["type"]) { + switch (aGrip.type) { case "undefined": return "undefined"; case "null": return "null"; default: - return "[" + aGrip["type"] + " " + aGrip["class"] + "]"; + return "[" + aGrip.type + " " + aGrip.class + "]"; } } else { switch (typeof aGrip) { @@ -702,7 +892,7 @@ DebuggerView.Properties = { */ _propertyColor: function DVP__propertyColor(aGrip) { if (aGrip && "object" === typeof aGrip) { - switch (aGrip["type"]) { + switch (aGrip.type) { case "undefined": return "token-undefined"; case "null": @@ -737,7 +927,7 @@ DebuggerView.Properties = { * The newly created html node representing the generic elem. */ _createPropertyElement: function DVP__createPropertyElement(aName, aId, aClass, aParent) { - // make sure we don't duplicate anything and the parent exists + // Make sure we don't duplicate anything and the parent exists. if (document.getElementById(aId)) { return null; } @@ -751,23 +941,23 @@ DebuggerView.Properties = { let title = document.createElement("div"); let details = document.createElement("div"); - // create a scope node to contain all the elements + // Create a scope node to contain all the elements. element.id = aId; element.className = aClass; - // the expand/collapse arrow + // The expand/collapse arrow. arrow.className = "arrow"; arrow.style.visibility = "hidden"; - // the name element + // The name element. name.className = "name unselectable"; name.appendChild(document.createTextNode(aName || "")); - // the title element, containing the arrow and the name + // The title element, containing the arrow and the name. title.className = "title"; title.addEventListener("click", function() { element.toggle(); }, true); - // the node element which will contain any added scope variables + // The node element which will contain any added scope variables. details.className = "details"; title.appendChild(arrow); @@ -850,6 +1040,45 @@ DebuggerView.Properties = { return element; }; + /** + * Shows the element expand/collapse arrow (only if necessary!). + * @return object + * The same element. + */ + element.showArrow = function DVP_element_showArrow() { + if (details.childNodes.length) { + arrow.style.visibility = "visible"; + } + return element; + }; + + /** + * Forces the element expand/collapse arrow to be visible, even if there + * are no child elements. + * + * @param boolean aPreventHideFlag + * Prevents the arrow to be hidden when requested. + * @return object + * The same element. + */ + element.forceShowArrow = function DVP_element_forceShowArrow(aPreventHideFlag) { + element._preventHide = aPreventHideFlag; + arrow.style.visibility = "visible"; + return element; + }; + + /** + * Hides the element expand/collapse arrow. + * @return object + * The same element. + */ + element.hideArrow = function DVP_element_hideArrow() { + if (!element._preventHide) { + arrow.style.visibility = "hidden"; + } + return element; + }; + /** * Returns if the element is visible. * @return boolean @@ -936,8 +1165,8 @@ DebuggerView.Properties = { let arrow = node.querySelector(".arrow"); let children = node.querySelector(".details").childNodes.length; - // if the parent details node has at least one element, set the - // expand/collapse arrow visible + // If the parent details node has at least one element, set the + // expand/collapse arrow visible. if (children) { arrow.style.visibility = "visible"; } else { @@ -945,7 +1174,7 @@ DebuggerView.Properties = { } }.bind(this); - // return the element for later use and customization + // Return the element for later use and customization. return element; }, @@ -959,11 +1188,11 @@ DebuggerView.Properties = { /** * Sets the display mode for the global scope container. * - * @param boolean value + * @param boolean aFlag * False to hide the container, true to show. */ - set globalScope(value) { - if (value) { + set globalScope(aFlag) { + if (aFlag) { this._globalScope.show(); } else { this._globalScope.hide(); @@ -980,11 +1209,11 @@ DebuggerView.Properties = { /** * Sets the display mode for the local scope container. * - * @param boolean value + * @param boolean aFlag * False to hide the container, true to show. */ - set localScope(value) { - if (value) { + set localScope(aFlag) { + if (aFlag) { this._localScope.show(); } else { this._localScope.hide(); @@ -1001,11 +1230,11 @@ DebuggerView.Properties = { /** * Sets the display mode for the with block scope container. * - * @param boolean value + * @param boolean aFlag * False to hide the container, true to show. */ - set withScope(value) { - if (value) { + set withScope(aFlag) { + if (aFlag) { this._withScope.show(); } else { this._withScope.hide(); @@ -1022,11 +1251,11 @@ DebuggerView.Properties = { /** * Sets the display mode for the with block scope container. * - * @param boolean value + * @param boolean aFlag * False to hide the container, true to show. */ - set closureScope(value) { - if (value) { + set closureScope(aFlag) { + if (aFlag) { this._closureScope.show(); } else { this._closureScope.hide(); @@ -1051,10 +1280,10 @@ DebuggerView.Properties = { */ initialize: function DVP_initialize() { this._vars = document.getElementById("variables"); - this._localScope = this._addScope(DebuggerView.getStr("localScope")).expand(); - this._withScope = this._addScope(DebuggerView.getStr("withScope")).hide(); - this._closureScope = this._addScope(DebuggerView.getStr("closureScope")).hide(); - this._globalScope = this._addScope(DebuggerView.getStr("globalScope")); + this._localScope = this._addScope(L10N.getStr("localScope")).expand(); + this._withScope = this._addScope(L10N.getStr("withScope")).hide(); + this._closureScope = this._addScope(L10N.getStr("closureScope")).hide(); + this._globalScope = this._addScope(L10N.getStr("globalScope")); }, /** @@ -1070,167 +1299,15 @@ DebuggerView.Properties = { }; /** - * Functions handling the html scripts UI. + * Preliminary setup for the DebuggerView object. */ -DebuggerView.Scripts = { +DebuggerView.Scripts = new ScriptsView(); +DebuggerView.StackFrames = new StackFramesView(); +DebuggerView.Properties = new PropertiesView(); - /** - * Sets the change event listener for the source scripts container. - * - * @param function aHandler - * The delegate used as the event listener. - */ - addChangeListener: function DVS_addChangeListener(aHandler) { - // Save the handler so it can be removed on shutdown. - this._onScriptsChange = aHandler; - this._scripts.addEventListener("select", aHandler, false); - }, - - /** - * Removes all elements from the scripts container, leaving it empty. - */ - empty: function DVS_empty() { - while (this._scripts.firstChild) { - this._scripts.removeChild(this._scripts.firstChild); - } - }, - - /** - * Checks whether the script with the specified URL is among the scripts - * known to the debugger and shown in the list. - * - * @param string aUrl - * The script URL. - * @return boolean - */ - contains: function DVS_contains(aUrl) { - if (this._scripts.getElementsByAttribute("value", aUrl).length > 0) { - return true; - } - return false; - }, - - /** - * Checks whether the script with the specified label is among the scripts - * known to the debugger and shown in the list. - * - * @param string aLabel - * The script label. - * @return boolean - */ - containsLabel: function DVS_containsLabel(aLabel) { - if (this._scripts.getElementsByAttribute("label", aLabel).length > 0) { - return true; - } - return false; - }, - - /** - * Checks whether the script with the specified URL is selected in the list. - * - * @param string aUrl - * The script URL. - */ - isSelected: function DVS_isSelected(aUrl) { - if (this._scripts.selectedItem && - this._scripts.selectedItem.value == aUrl) { - return true; - } - return false; - }, - - /** - * Selects the script with the specified URL from the list. - * - * @param string aUrl - * The script URL. - */ - selectScript: function DVS_selectScript(aUrl) { - for (let i = 0; i < this._scripts.itemCount; i++) { - if (this._scripts.getItemAtIndex(i).value == aUrl) { - this._scripts.selectedIndex = i; - break; - } - } - }, - - /** - * Retrieve the URL of the selected script. - * @return string|null - */ - get selected() { - return this._scripts.selectedItem ? - this._scripts.selectedItem.value : null; - }, - - /** - * Adds a script to the scripts container. - * If the script already exists (was previously added), null is returned. - * Otherwise, the newly created element is returned. - * - * @param string aLabel - * The simplified script location to be shown. - * @param string aScript - * The source script. - * @return object - * The newly created html node representing the added script. - */ - addScript: function DVS_addScript(aLabel, aScript) { - // make sure we don't duplicate anything - if (this.containsLabel(aLabel)) { - return null; - } - - let script = this._scripts.appendItem(aLabel, aScript.url); - script.setAttribute("tooltiptext", aScript.url); - script.setUserData("sourceScript", aScript, null); - - this._scripts.selectedItem = script; - return script; - }, - - /** - * Returns the list of URIs for scripts in the page. - */ - scriptLocations: function DVS_scriptLocations() { - let locations = []; - for (let i = 0; i < this._scripts.itemCount; i++) { - locations.push(this._scripts.getItemAtIndex(i).value); - } - return locations; - }, - - /** - * The cached click listener for the scripts container. - */ - _onScriptsChange: null, - - /** - * The cached scripts container. - */ - _scripts: null, - - /** - * Initialization function, called when the debugger is initialized. - */ - initialize: function DVS_initialize() { - this._scripts = document.getElementById("scripts"); - }, - - /** - * Destruction function, called when the debugger is shut down. - */ - destroy: function DVS_destroy() { - this._scripts.removeEventListener("select", this._onScriptsChange, false); - this._scripts = null; - } -}; - - -let DVF = DebuggerView.Stackframes; -DVF._onFramesScroll = DVF._onFramesScroll.bind(DVF); -DVF._onCloseButtonClick = DVF._onCloseButtonClick.bind(DVF); -DVF._onResumeButtonClick = DVF._onResumeButtonClick.bind(DVF); -DVF._onStepOverClick = DVF._onStepOverClick.bind(DVF); -DVF._onStepInClick = DVF._onStepInClick.bind(DVF); -DVF._onStepOutClick = DVF._onStepOutClick.bind(DVF); +/** + * Export the source editor to the global scope for easier access in tests. + */ +Object.defineProperty(window, "editor", { + get: function() { return DebuggerView.editor; } +}); diff --git a/browser/devtools/debugger/debugger.js b/browser/devtools/debugger/debugger.js deleted file mode 100644 index 41b375299da0..000000000000 --- a/browser/devtools/debugger/debugger.js +++ /dev/null @@ -1,696 +0,0 @@ -/* -*- Mode: javascript; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ -/* vim: set ft=javascript ts=2 et sw=2 tw=80: */ -/* ***** BEGIN LICENSE BLOCK ***** - * Version: MPL 1.1/GPL 2.0/LGPL 2.1 - * - * The contents of this file are subject to the Mozilla Public License Version - * 1.1 (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * http://www.mozilla.org/MPL/ - * - * Software distributed under the License is distributed on an "AS IS" basis, - * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License - * for the specific language governing rights and limitations under the - * License. - * - * The Original Code is mozilla.org code. - * - * The Initial Developer of the Original Code is - * Mozilla Foundation - * Portions created by the Initial Developer are Copyright (C) 2011 - * the Initial Developer. All Rights Reserved. - * - * Contributor(s): - * Dave Camp - * Panos Astithas - * Victor Porof - * Mihai Sucan - * - * Alternatively, the contents of this file may be used under the terms of - * either the GNU General Public License Version 2 or later (the "GPL"), or - * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), - * in which case the provisions of the GPL or the LGPL are applicable instead - * of those above. If you wish to allow use of your version of this file only - * under the terms of either the GPL or the LGPL, and not to allow others to - * use your version of this file under the terms of the MPL, indicate your - * decision by deleting the provisions above and replace them with the notice - * and other provisions required by the GPL or the LGPL. If you do not delete - * the provisions above, a recipient may use your version of this file under - * the terms of any one of the MPL, the GPL or the LGPL. - * - * ***** END LICENSE BLOCK ***** */ - -"use strict"; - -Components.utils.import("resource:///modules/source-editor.jsm"); - -var gInitialized = false; -var gClient = null; -var gTabClient = null; - - -function initDebugger() -{ - window.removeEventListener("DOMContentLoaded", initDebugger, false); - if (gInitialized) { - return; - } - gInitialized = true; - - DebuggerView.Stackframes.initialize(); - DebuggerView.Properties.initialize(); - DebuggerView.Scripts.initialize(); -} - -/** - * Called by chrome to set up a debugging session. - * - * @param DebuggerClient aClient - * The debugger client. - * @param object aTabGrip - * The remote protocol grip of the tab. - */ -function startDebuggingTab(aClient, aTabGrip) -{ - gClient = aClient; - - gClient.attachTab(aTabGrip.actor, function(aResponse, aTabClient) { - if (aTabClient) { - gTabClient = aTabClient; - gClient.attachThread(aResponse.threadActor, function(aResponse, aThreadClient) { - if (!aThreadClient) { - Components.utils.reportError("Couldn't attach to thread: " + - aResponse.error); - return; - } - ThreadState.connect(aThreadClient, function() { - StackFrames.connect(aThreadClient, function() { - SourceScripts.connect(aThreadClient, function() { - aThreadClient.resume(); - }); - }); - }); - }); - } - }); -} - -function shutdownDebugger() -{ - window.removeEventListener("unload", shutdownDebugger, false); - - SourceScripts.disconnect(); - StackFrames.disconnect(); - ThreadState.disconnect(); - ThreadState.activeThread = false; - - DebuggerView.Stackframes.destroy(); - DebuggerView.Properties.destroy(); - DebuggerView.Scripts.destroy(); -} - - -/** - * ThreadState keeps the UI up to date with the state of the - * thread (paused/attached/etc.). - */ -var ThreadState = { - activeThread: null, - - /** - * Connect to a thread client. - * @param object aThreadClient - * The thread client. - * @param function aCallback - * The next function in the initialization sequence. - */ - connect: function TS_connect(aThreadClient, aCallback) { - this.activeThread = aThreadClient; - aThreadClient.addListener("paused", ThreadState.update); - aThreadClient.addListener("resumed", ThreadState.update); - aThreadClient.addListener("detached", ThreadState.update); - this.update(); - aCallback && aCallback(); - }, - - /** - * Update the UI after a thread state change. - */ - update: function TS_update(aEvent) { - DebuggerView.Stackframes.updateState(this.activeThread.state); - if (aEvent == "detached") { - ThreadState.activeThread = false; - } - }, - - /** - * Disconnect from the client. - */ - disconnect: function TS_disconnect() { - this.activeThread.removeListener("paused", ThreadState.update); - this.activeThread.removeListener("resumed", ThreadState.update); - this.activeThread.removeListener("detached", ThreadState.update); - } -}; - -ThreadState.update = ThreadState.update.bind(ThreadState); - -/** - * Keeps the stack frame list up-to-date, using the thread client's - * stack frame cache. - */ -var StackFrames = { - pageSize: 25, - activeThread: null, - selectedFrame: null, - - /** - * Watch a given thread client. - * @param object aThreadClient - * The thread client. - * @param function aCallback - * The next function in the initialization sequence. - */ - connect: function SF_connect(aThreadClient, aCallback) { - DebuggerView.Stackframes.addClickListener(this.onClick); - - this.activeThread = aThreadClient; - aThreadClient.addListener("paused", this.onPaused); - aThreadClient.addListener("resumed", this.onResume); - aThreadClient.addListener("framesadded", this.onFrames); - aThreadClient.addListener("framescleared", this.onFramesCleared); - this.onFramesCleared(); - aCallback && aCallback(); - }, - - /** - * Disconnect from the client. - */ - disconnect: function TS_disconnect() { - 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. - */ - onPaused: function SF_onPaused() { - this.activeThread.fillFrames(this.pageSize); - }, - - /** - * Handler for the thread client's resumed notification. - */ - onResume: function SF_onResume() { - window.editor.setDebugLocation(-1); - }, - - /** - * Handler for the thread client's framesadded notification. - */ - onFrames: function SF_onFrames() { - DebuggerView.Stackframes.empty(); - - for each (let frame in this.activeThread.cachedFrames) { - this._addFramePanel(frame); - } - - if (this.activeThread.moreFrames) { - DebuggerView.Stackframes.dirty = true; - } - - if (!this.selectedFrame) { - this.selectFrame(0); - } - }, - - /** - * Handler for the thread client's framescleared notification. - */ - onFramesCleared: function SF_onFramesCleared() { - DebuggerView.Stackframes.emptyText(); - this.selectedFrame = null; - // Clear the properties as well. - DebuggerView.Properties.localScope.empty(); - DebuggerView.Properties.globalScope.empty(); - }, - - /** - * Event handler for clicks on stack frames. - */ - onClick: function SF_onClick(aEvent) { - let target = aEvent.target; - while (target) { - if (target.stackFrame) { - this.selectFrame(target.stackFrame.depth); - return; - } - target = target.parentNode; - } - }, - - /** - * 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) { - if (this.selectedFrame !== null) { - DebuggerView.Stackframes.highlightFrame(this.selectedFrame, false); - } - - this.selectedFrame = aDepth; - if (aDepth !== null) { - DebuggerView.Stackframes.highlightFrame(this.selectedFrame, true); - } - - let frame = this.activeThread.cachedFrames[aDepth]; - if (!frame) { - return; - } - - // Move the editor's caret to the proper line. - if (DebuggerView.Scripts.isSelected(frame.where.url) && frame.where.line) { - window.editor.setCaretPosition(frame.where.line - 1); - window.editor.setDebugLocation(frame.where.line - 1); - } else if (DebuggerView.Scripts.contains(frame.where.url)) { - DebuggerView.Scripts.selectScript(frame.where.url); - window.editor.setCaretPosition(frame.where.line - 1); - } else { - window.editor.setDebugLocation(-1); - } - - // Display the local variables. - let localScope = DebuggerView.Properties.localScope; - localScope.empty(); - // Add "this". - if (frame["this"]) { - let thisVar = localScope.addVar("this"); - thisVar.setGrip({ "type": frame["this"].type, - "class": frame["this"].class }); - this._addExpander(thisVar, frame["this"]); - } - - if (frame.arguments && frame.arguments.length > 0) { - // Add "arguments". - let argsVar = localScope.addVar("arguments"); - argsVar.setGrip({ "type": "object", "class": "Arguments" }); - this._addExpander(argsVar, frame.arguments); - - // Add variables for every argument. - let objClient = this.activeThread.pauseGrip(frame.callee); - objClient.getSignature(function SF_getSignature(aResponse) { - for (let i = 0; i < aResponse.parameters.length; i++) { - let param = aResponse.parameters[i]; - let paramVar = localScope.addVar(param); - let paramVal = frame.arguments[i]; - paramVar.setGrip(paramVal); - this._addExpander(paramVar, paramVal); - } - // Signal that call parameters have been fetched. - let evt = document.createEvent("Event"); - evt.initEvent("Debugger:FetchedParameters", true, false); - document.documentElement.dispatchEvent(evt); - }.bind(this)); - } - }, - - /** - * Update the source editor current debug location based on the selected frame - * and script. - */ - updateEditor: function SF_updateEditor() { - if (this.selectedFrame === null) { - return; - } - - let frame = this.activeThread.cachedFrames[this.selectedFrame]; - if (!frame) { - return; - } - - // Move the editor's caret to the proper line. - if (DebuggerView.Scripts.isSelected(frame.where.url) && frame.where.line) { - window.editor.setDebugLocation(frame.where.line - 1); - } else { - window.editor.setDebugLocation(-1); - } - }, - - _addExpander: function SF_addExpander(aVar, aObject) { - // No need for expansion for null and undefined values, but we do need them - // for frame.arguments which is a regular array. - if (!aObject || typeof aObject != "object" || - (aObject.type != "object" && !Array.isArray(aObject))) { - return; - } - // Add a dummy property to force the twisty to show up. - aVar.addProperties({ " ": { value: " " }}); - aVar.onexpand = this._addVarProperties.bind(this, aVar, aObject); - }, - - _addVarProperties: function SF_addVarProperties(aVar, aObject) { - // Retrieve the properties only once. - if (aVar.fetched) { - return; - } - // Clear the placeholder property put in place to display the twisty. - aVar.empty(); - - // For arrays we have to construct a grip-like object to pass into - // addProperties. - if (Array.isArray(aObject)) { - let properties = { length: { writable: true, value: aObject.length } }; - for (let i = 0; i < aObject.length; i++) { - properties[i + ""] = { value: aObject[i] }; - } - aVar.addProperties(properties); - // Expansion handlers must be set after the properties are added. - for (let i = 0; i < aObject.length; i++) { - this._addExpander(aVar[i + ""], aObject[i]); - } - aVar.fetched = true; - return; - } - - let objClient = this.activeThread.pauseGrip(aObject); - objClient.getPrototypeAndProperties(function SF_onProtoAndProps(aResponse) { - // Add __proto__. - if (aResponse.prototype.type != "null") { - let properties = {}; - properties["__proto__ "] = { value: aResponse.prototype }; - aVar.addProperties(properties); - // Expansion handlers must be set after the properties are added. - this._addExpander(aVar["__proto__ "], aResponse.prototype); - } - - // Sort the rest 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); - } - - aVar.fetched = true; - }.bind(this)); - }, - - /** - * Adds the specified stack frame to the list. - * - * @param Debugger.Frame aFrame - * The new frame to add. - */ - _addFramePanel: function SF_addFramePanel(aFrame) { - let depth = aFrame.depth; - let label = SourceScripts._getScriptLabel(aFrame.where.url); - - let startText = this._frameTitle(aFrame); - let endText = label + ":" + aFrame.where.line; - - let panel = DebuggerView.Stackframes.addFrame(depth, startText, endText); - - if (panel) { - panel.stackFrame = 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. - */ - _frameTitle: function SF_frameTitle(aFrame) { - if (aFrame.type == "call") { - return aFrame["calleeName"] ? aFrame["calleeName"] : "(anonymous)"; - } - - return "(" + aFrame.type + ")"; - } -}; - -StackFrames.onPaused = StackFrames.onPaused.bind(StackFrames); -StackFrames.onResume = StackFrames.onResume.bind(StackFrames); -StackFrames.onFrames = StackFrames.onFrames.bind(StackFrames); -StackFrames.onFramesCleared = StackFrames.onFramesCleared.bind(StackFrames); -StackFrames.onClick = StackFrames.onClick.bind(StackFrames); - -/** - * Keeps the source script list up-to-date, using the thread client's - * source script cache. - */ -var SourceScripts = { - pageSize: 25, - activeThread: null, - _labelsCache: null, - - /** - * Watch a given thread client. - * @param object aThreadClient - * The thread client. - * @param function aCallback - * The next function in the initialization sequence. - */ - connect: function SS_connect(aThreadClient, aCallback) { - DebuggerView.Scripts.addChangeListener(this.onChange); - - this.activeThread = aThreadClient; - aThreadClient.addListener("scriptsadded", this.onScripts); - aThreadClient.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() { - this.activeThread.removeListener("scriptsadded", this.onScripts); - this.activeThread.removeListener("scriptscleared", this.onScriptsCleared); - }, - - /** - * Handler for the debugger client's unsolicited newScript notification. - */ - onNewScript: function SS_onNewScript(aNotification, aPacket) { - this._addScript({ url: aPacket.url, startLine: aPacket.startLine }); - }, - - /** - * Handler for the thread client's scriptsadded notification. - */ - onScripts: function SS_onScripts() { - for each (let script in this.activeThread.cachedScripts) { - this._addScript(script); - } - }, - - /** - * Handler for the thread client's scriptscleared notification. - */ - onScriptsCleared: function SS_onScriptsCleared() { - DebuggerView.Scripts.empty(); - }, - - /** - * Handler for changes on the selected source script. - */ - onChange: function SS_onChange(aEvent) { - let scripts = aEvent.target; - if (!scripts.selectedItem) { - return; - } - let script = scripts.selectedItem.getUserData("sourceScript"); - this.showScript(script); - }, - - /** - * 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)) { - window.editor.setMode(SourceEditor.MODES.JAVASCRIPT); - } else { - window.editor.setMode(SourceEditor.MODES.HTML); - } - return; - } - - if (this._trimUrlQuery(aUrl).slice(-3) == ".js") { - window.editor.setMode(SourceEditor.MODES.JAVASCRIPT); - } else { - window.editor.setMode(SourceEditor.MODES.HTML); - } - }, - - /** - * Trims the query part of a url string, if necessary. - * - * @param string aUrl - * The script url. - * @return string - */ - _trimUrlQuery: function SS_trimUrlQuery(aUrl) { - let q = aUrl.indexOf('?'); - if (q > -1) { - return aUrl.slice(0, q); - } - return aUrl; - }, - - /** - * Gets a unique, simplified label from a script url. - * ex: a). ici://some.address.com/random/subrandom/ - * b). ni://another.address.org/random/subrandom/page.html - * c). san://interesting.address.gro/random/script.js - * d). si://interesting.address.moc/random/another/script.js - * => - * a). subrandom/ - * b). page.html - * c). script.js - * d). another/script.js - * - * @param string aUrl - * The script url. - * @param string aHref - * The content location href to be used. If unspecified, it will - * defalult to debugged panrent window location. - * @return string - * The simplified label. - */ - _getScriptLabel: function SS_getScriptLabel(aUrl, aHref) { - let url = this._trimUrlQuery(aUrl); - - if (this._labelsCache[url]) { - return this._labelsCache[url]; - } - - let href = aHref || window.parent.content.location.href; - let pathElements = url.split("/"); - let label = pathElements.pop() || (pathElements.pop() + "/"); - - // if the label as a leaf name is alreay present in the scripts list - if (DebuggerView.Scripts.containsLabel(label)) { - label = url.replace(href.substring(0, href.lastIndexOf("/") + 1), ""); - - // if the path/to/script is exactly the same, we're in different domains - if (DebuggerView.Scripts.containsLabel(label)) { - label = url; - } - } - - return this._labelsCache[url] = label; - }, - - /** - * 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 and display it in the editor if the - * editor is empty. - */ - _addScript: function SS_addScript(aScript) { - DebuggerView.Scripts.addScript(this._getScriptLabel(aScript.url), aScript); - - if (window.editor.getCharCount() == 0) { - this.showScript(aScript); - } - }, - - /** - * 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 (optional). Supported - * options: - * - targetLine: place the editor at the given line number. - */ - showScript: function SS_showScript(aScript, aOptions) { - if (!aScript.loaded) { - // Notify the chrome code that we need to load a script file. - var evt = document.createEvent("CustomEvent"); - evt.initCustomEvent("Debugger:LoadSource", true, false, - {url: aScript.url, options: aOptions}); - document.documentElement.dispatchEvent(evt); - window.editor.setMode(SourceEditor.MODES.TEXT); - window.editor.setText(DebuggerView.getStr("loadingText")); - window.editor.resetUndo(); - } else { - this._onShowScript(aScript, aOptions); - } - }, - - /** - * Display the script source once it loads. - * - * @private - * @param object aScript - * The script object coming from the active thread. - * @param object [aOptions] - * Additional options for showing the script (optional). Supported - * options: - * - targetLine: place the editor at the given line number. - */ - _onShowScript: function SS__onShowScript(aScript, aOptions) { - aOptions = aOptions || {}; - this.setEditorMode(aScript.url, aScript.contentType); - window.editor.setText(aScript.text); - window.updateEditorBreakpoints(); - StackFrames.updateEditor(); - if (aOptions.targetLine) { - window.editor.setCaretPosition(aOptions.targetLine - 1); - } - window.editor.resetUndo(); - - // Notify the chrome code that we shown script file. - let evt = document.createEvent("CustomEvent"); - evt.initCustomEvent("Debugger:ScriptShown", true, false, - {url: aScript.url}); - document.documentElement.dispatchEvent(evt); - }, -}; - -SourceScripts.onScripts = SourceScripts.onScripts.bind(SourceScripts); -SourceScripts.onNewScript = SourceScripts.onNewScript.bind(SourceScripts); -SourceScripts.onScriptsCleared = SourceScripts.onScriptsCleared.bind(SourceScripts); -SourceScripts.onChange = SourceScripts.onChange.bind(SourceScripts); - -window.addEventListener("DOMContentLoaded", initDebugger, false); -window.addEventListener("unload", shutdownDebugger, false); diff --git a/browser/devtools/debugger/debugger.xul b/browser/devtools/debugger/debugger.xul index 0496062db49b..a396733a1da9 100644 --- a/browser/devtools/debugger/debugger.xul +++ b/browser/devtools/debugger/debugger.xul @@ -47,54 +47,58 @@ ]> + - - - - - - - - - - - - - - - - - - -
- - &debuggerUI.closeButton; - - &debuggerUI.stepOverButton; - &debuggerUI.stepInButton; - &debuggerUI.stepOutButton; - - -
-
-
&debuggerUI.stackTitle;
-
-
-
-
&debuggerUI.scriptTitle;
-
-
-
-
&debuggerUI.propertiesTitle;
-
-
-
-
- -
+ + + + + + + + + + + + + + + + + + + + + +
+ + &debuggerUI.closeButton; + + &debuggerUI.stepOverButton; + &debuggerUI.stepInButton; + &debuggerUI.stepOutButton; + + +
+
+
&debuggerUI.stackTitle;
+
+
+
+
&debuggerUI.scriptTitle;
+
+
+
+
&debuggerUI.propertiesTitle;
+
+
+
+ +
+
diff --git a/browser/devtools/debugger/test/browser_dbg_bug723069_editor-breakpoints.js b/browser/devtools/debugger/test/browser_dbg_bug723069_editor-breakpoints.js index 9752e1b7bf50..ac6fa0bbecd2 100644 --- a/browser/devtools/debugger/test/browser_dbg_bug723069_editor-breakpoints.js +++ b/browser/devtools/debugger/test/browser_dbg_bug723069_editor-breakpoints.js @@ -29,7 +29,7 @@ function test() gDebuggee = aDebuggee; gPane = aPane; gDebugger = gPane.debuggerWindow; - gPane.activeThread.addOneTimeListener("framesadded", function() { + gDebugger.DebuggerController.activeThread.addOneTimeListener("framesadded", function() { framesAdded = true; runTest(); }); @@ -57,7 +57,7 @@ function test() { gScripts = gDebugger.DebuggerView.Scripts; - is(gDebugger.StackFrames.activeThread.state, "paused", + is(gDebugger.DebuggerController.activeThread.state, "paused", "Should only be getting stack frames while paused."); is(gScripts._scripts.itemCount, 2, "Found the expected number of scripts."); @@ -66,7 +66,7 @@ function test() isnot(gEditor.getText().indexOf("debugger"), -1, "The correct script was loaded initially."); - isnot(gScripts.selected, gScripts.scriptLocations()[0], + isnot(gScripts.selected, gScripts.scriptLocations[0], "the correct script is selected"); gBreakpoints = gPane.breakpoints; @@ -161,7 +161,7 @@ function test() ok(!gPane.getBreakpoint(gScripts.selected, 6), "getBreakpoint(selectedScript, 6) returns no breakpoint"); - let script0 = gScripts.scriptLocations()[0]; + let script0 = gScripts.scriptLocations[0]; isnot(script0, gScripts.selected, "first script location is not the currently selected script"); @@ -187,7 +187,7 @@ function test() ok(aBreakpointClient, "breakpoint2 added, client received"); ok(!aResponseError, "breakpoint2 added without errors"); - is(aBreakpointClient.location.url, gScripts.scriptLocations()[0], + is(aBreakpointClient.location.url, gScripts.scriptLocations[0], "breakpoint2 client url is correct"); is(aBreakpointClient.location.line, 5, "breakpoint2 client line is correct"); @@ -196,7 +196,7 @@ function test() ok(aBreakpointClient.actor in gBreakpoints, "breakpoint2 client found in the list of debugger breakpoints"); is(Object.keys(gBreakpoints).length, 1, "one breakpoint in the debugger"); - is(gPane.getBreakpoint(gScripts.scriptLocations()[0], 5), aBreakpointClient, + is(gPane.getBreakpoint(gScripts.scriptLocations[0], 5), aBreakpointClient, "getBreakpoint(scriptLocations[0], 5) returns the correct breakpoint"); // remove the trap listener @@ -211,7 +211,7 @@ function test() info("switch to the second script"); gScripts._scripts.selectedIndex = 0; - gDebugger.SourceScripts.onChange({ target: gScripts._scripts }); + gDebugger.DebuggerController.SourceScripts.onChange({ target: gScripts._scripts }); }); } @@ -267,13 +267,13 @@ function test() is(gEditor.getBreakpoints().length, 0, "editor.getBreakpoints().length is correct"); executeSoon(function() { - gDebugger.StackFrames.activeThread.resume(finish); + gDebugger.DebuggerController.activeThread.resume(finish); }); } registerCleanupFunction(function() { is(Object.keys(gBreakpoints).length, 0, "no breakpoint in the debugger"); - ok(!gPane.getBreakpoint(gScripts.scriptLocations()[0], 5), + ok(!gPane.getBreakpoint(gScripts.scriptLocations[0], 5), "getBreakpoint(scriptLocations[0], 5) returns no breakpoint"); removeTab(gTab); diff --git a/browser/devtools/debugger/test/browser_dbg_bug731394_editor-contextmenu.js b/browser/devtools/debugger/test/browser_dbg_bug731394_editor-contextmenu.js index b99d39268782..385e124cd29b 100644 --- a/browser/devtools/debugger/test/browser_dbg_bug731394_editor-contextmenu.js +++ b/browser/devtools/debugger/test/browser_dbg_bug731394_editor-contextmenu.js @@ -28,7 +28,7 @@ function test() gPane = aPane; gDebugger = gPane.debuggerWindow; - gPane.activeThread.addOneTimeListener("framesadded", function() { + gDebugger.DebuggerController.activeThread.addOneTimeListener("framesadded", function() { framesAdded = true; runTest(); }); @@ -55,7 +55,7 @@ function test() { let scripts = gDebugger.DebuggerView.Scripts._scripts; - is(gDebugger.StackFrames.activeThread.state, "paused", + is(gDebugger.DebuggerController.activeThread.state, "paused", "Should only be getting stack frames while paused."); is(scripts.itemCount, 2, "Found the expected number of scripts."); @@ -107,7 +107,7 @@ function test() executeSoon(function() { contextMenu.hidePopup(); - gDebugger.StackFrames.activeThread.resume(finish); + gDebugger.DebuggerController.activeThread.resume(finish); }); } diff --git a/browser/devtools/debugger/test/browser_dbg_clean-exit.js b/browser/devtools/debugger/test/browser_dbg_clean-exit.js index a152a8b5d842..66ae19603615 100644 --- a/browser/devtools/debugger/test/browser_dbg_clean-exit.js +++ b/browser/devtools/debugger/test/browser_dbg_clean-exit.js @@ -22,9 +22,9 @@ function test() { } function testCleanExit() { - gPane.activeThread.addOneTimeListener("framesadded", function() { + gDebugger.DebuggerController.activeThread.addOneTimeListener("framesadded", function() { Services.tm.currentThread.dispatch({ run: function() { - is(gDebugger.StackFrames.activeThread.paused, true, + is(gDebugger.DebuggerController.activeThread.paused, true, "Should be paused after the debugger statement."); closeDebuggerAndFinish(gTab); diff --git a/browser/devtools/debugger/test/browser_dbg_debuggerstatement.html b/browser/devtools/debugger/test/browser_dbg_debuggerstatement.html index 984bcaa4d603..a4bd8443bd57 100644 --- a/browser/devtools/debugger/test/browser_dbg_debuggerstatement.html +++ b/browser/devtools/debugger/test/browser_dbg_debuggerstatement.html @@ -1,6 +1,6 @@ -Browser Debugger Test Tab +Browser Debugger Test Tab - - - + + + diff --git a/browser/devtools/debugger/test/browser_dbg_script-switching.js b/browser/devtools/debugger/test/browser_dbg_script-switching.js index aa8ef71183ee..7e876753e33c 100644 --- a/browser/devtools/debugger/test/browser_dbg_script-switching.js +++ b/browser/devtools/debugger/test/browser_dbg_script-switching.js @@ -29,7 +29,7 @@ function test() gPane = aPane; gDebugger = gPane.debuggerWindow; - gPane.activeThread.addOneTimeListener("framesadded", function() { + gDebugger.DebuggerController.activeThread.addOneTimeListener("framesadded", function() { framesAdded = true; runTest(); }); @@ -57,7 +57,7 @@ function test() function testScriptsDisplay() { gScripts = gDebugger.DebuggerView.Scripts._scripts; - is(gDebugger.StackFrames.activeThread.state, "paused", + is(gDebugger.DebuggerController.activeThread.state, "paused", "Should only be getting stack frames while paused."); is(gScripts.itemCount, 2, "Found the expected number of scripts."); @@ -79,6 +79,8 @@ function testScriptsDisplay() { ok(gDebugger.DebuggerView.Scripts.containsLabel( label2), "Second script label is incorrect."); + dump("Debugger editor text:\n" + gDebugger.editor.getText() + "\n"); + ok(gDebugger.editor.getText().search(/debugger/) != -1, "The correct script was loaded initially."); @@ -98,6 +100,8 @@ function testScriptsDisplay() { function testSwitchPaused() { + dump("Debugger editor text:\n" + gDebugger.editor.getText() + "\n"); + ok(gDebugger.editor.getText().search(/debugger/) == -1, "The second script is no longer displayed."); @@ -107,7 +111,7 @@ function testSwitchPaused() is(gDebugger.editor.getDebugLocation(), -1, "editor debugger location has been cleared."); - gDebugger.StackFrames.activeThread.resume(function() { + gDebugger.DebuggerController.activeThread.resume(function() { window.addEventListener("Debugger:ScriptShown", function _onEvent(aEvent) { let url = aEvent.detail.url; if (url.indexOf("-02.js") != -1) { diff --git a/browser/devtools/debugger/test/browser_dbg_select-line.js b/browser/devtools/debugger/test/browser_dbg_select-line.js index 0b246593b325..aea6dead19ff 100644 --- a/browser/devtools/debugger/test/browser_dbg_select-line.js +++ b/browser/devtools/debugger/test/browser_dbg_select-line.js @@ -31,11 +31,11 @@ function test() } function testSelectLine() { - gPane.activeThread.addOneTimeListener("scriptsadded", function() { + gDebugger.DebuggerController.activeThread.addOneTimeListener("scriptsadded", function() { Services.tm.currentThread.dispatch({ run: function() { gScripts = gDebugger.DebuggerView.Scripts._scripts; - is(gDebugger.StackFrames.activeThread.state, "paused", + is(gDebugger.DebuggerController.activeThread.state, "paused", "Should only be getting stack frames while paused."); is(gScripts.itemCount, 2, "Found the expected number of scripts."); @@ -67,7 +67,7 @@ function testSelectLine() { is(gDebugger.editor.getCaretPosition().line, 4, "The correct line is selected."); - gDebugger.StackFrames.activeThread.resume(function() { + gDebugger.DebuggerController.activeThread.resume(function() { closeDebuggerAndFinish(gTab); }); }); diff --git a/browser/devtools/debugger/test/browser_dbg_stack-01.js b/browser/devtools/debugger/test/browser_dbg_stack-01.js index 6ad23121b24e..83f86c887fa1 100644 --- a/browser/devtools/debugger/test/browser_dbg_stack-01.js +++ b/browser/devtools/debugger/test/browser_dbg_stack-01.js @@ -21,13 +21,13 @@ function test() { } function testSimpleCall() { - gPane.activeThread.addOneTimeListener("framesadded", function() { + gDebugger.DebuggerController.activeThread.addOneTimeListener("framesadded", function() { Services.tm.currentThread.dispatch({ run: function() { - let frames = gDebugger.DebuggerView.Stackframes._frames; + let frames = gDebugger.DebuggerView.StackFrames._frames; let childNodes = frames.childNodes; - is(gDebugger.StackFrames.activeThread.state, "paused", + is(gDebugger.DebuggerController.activeThread.state, "paused", "Should only be getting stack frames while paused."); is(frames.querySelectorAll(".dbg-stackframe").length, 1, @@ -36,7 +36,7 @@ function testSimpleCall() { is(childNodes.length, frames.querySelectorAll(".dbg-stackframe").length, "All children should be frames."); - gDebugger.StackFrames.activeThread.resume(function() { + gDebugger.DebuggerController.activeThread.resume(function() { closeDebuggerAndFinish(gTab); }); }}, 0); diff --git a/browser/devtools/debugger/test/browser_dbg_stack-02.js b/browser/devtools/debugger/test/browser_dbg_stack-02.js index 2b6c223c705b..f16d3acf69bb 100644 --- a/browser/devtools/debugger/test/browser_dbg_stack-02.js +++ b/browser/devtools/debugger/test/browser_dbg_stack-02.js @@ -21,13 +21,13 @@ function test() { } function testEvalCall() { - gPane.activeThread.addOneTimeListener("framesadded", function() { + gDebugger.DebuggerController.activeThread.addOneTimeListener("framesadded", function() { Services.tm.currentThread.dispatch({ run: function() { - let frames = gDebugger.DebuggerView.Stackframes._frames; + let frames = gDebugger.DebuggerView.StackFrames._frames; let childNodes = frames.childNodes; - is(gDebugger.StackFrames.activeThread.state, "paused", + is(gDebugger.DebuggerController.activeThread.state, "paused", "Should only be getting stack frames while paused."); is(frames.querySelectorAll(".dbg-stackframe").length, 2, @@ -67,7 +67,7 @@ function testEvalCall() { ok(!frames.querySelector("#stackframe-1").classList.contains("selected"), "Second frame should not be selected after click inside the first frame."); - gDebugger.StackFrames.activeThread.resume(function() { + gDebugger.DebuggerController.activeThread.resume(function() { closeDebuggerAndFinish(gTab); }); }}, 0); diff --git a/browser/devtools/debugger/test/browser_dbg_stack-03.js b/browser/devtools/debugger/test/browser_dbg_stack-03.js index 1475fc70c621..462bf243d3ed 100644 --- a/browser/devtools/debugger/test/browser_dbg_stack-03.js +++ b/browser/devtools/debugger/test/browser_dbg_stack-03.js @@ -21,13 +21,13 @@ function test() { } function testRecurse() { - gDebuggee.gRecurseLimit = (gDebugger.StackFrames.pageSize * 2) + 1; + gDebuggee.gRecurseLimit = (gDebugger.DebuggerController.StackFrames.pageSize * 2) + 1; - gPane.activeThread.addOneTimeListener("framesadded", function() { + gDebugger.DebuggerController.activeThread.addOneTimeListener("framesadded", function() { Services.tm.currentThread.dispatch({ run: function() { - let frames = gDebugger.DebuggerView.Stackframes._frames; - let pageSize = gDebugger.StackFrames.pageSize; + let frames = gDebugger.DebuggerView.StackFrames._frames; + let pageSize = gDebugger.DebuggerController.StackFrames.pageSize; let recurseLimit = gDebuggee.gRecurseLimit; let childNodes = frames.childNodes; @@ -38,16 +38,16 @@ function testRecurse() { "All children should be frames."); - gPane.activeThread.addOneTimeListener("framesadded", function() { + gDebugger.DebuggerController.activeThread.addOneTimeListener("framesadded", function() { is(frames.querySelectorAll(".dbg-stackframe").length, pageSize * 2, "Should now have twice the max limit of frames."); - gPane.activeThread.addOneTimeListener("framesadded", function() { + gDebugger.DebuggerController.activeThread.addOneTimeListener("framesadded", function() { is(frames.querySelectorAll(".dbg-stackframe").length, recurseLimit, "Should have reached the recurse limit."); - gDebugger.StackFrames.activeThread.resume(function() { + gDebugger.DebuggerController.activeThread.resume(function() { closeDebuggerAndFinish(gTab); }); }); diff --git a/browser/devtools/debugger/test/browser_dbg_stack-04.js b/browser/devtools/debugger/test/browser_dbg_stack-04.js index 2585135087a9..f3469d3506ad 100644 --- a/browser/devtools/debugger/test/browser_dbg_stack-04.js +++ b/browser/devtools/debugger/test/browser_dbg_stack-04.js @@ -21,13 +21,13 @@ function test() { } function testEvalCallResume() { - gPane.activeThread.addOneTimeListener("framesadded", function() { + gDebugger.DebuggerController.activeThread.addOneTimeListener("framesadded", function() { Services.tm.currentThread.dispatch({ run: function() { - let frames = gDebugger.DebuggerView.Stackframes._frames; + let frames = gDebugger.DebuggerView.StackFrames._frames; let childNodes = frames.childNodes; - is(gDebugger.StackFrames.activeThread.state, "paused", + is(gDebugger.DebuggerController.activeThread.state, "paused", "Should only be getting stack frames while paused."); is(frames.querySelectorAll(".dbg-stackframe").length, 2, @@ -37,7 +37,7 @@ function testEvalCallResume() { "All children should be frames."); - gPane.activeThread.addOneTimeListener("framescleared", function() { + gDebugger.DebuggerController.activeThread.addOneTimeListener("framescleared", function() { is(frames.querySelectorAll(".dbg-stackframe").length, 0, "Should have no frames after resume"); @@ -51,7 +51,7 @@ function testEvalCallResume() { closeDebuggerAndFinish(gTab); }); - gPane.activeThread.resume(); + gDebugger.DebuggerController.activeThread.resume(); }}, 0); }); diff --git a/browser/devtools/debugger/test/browser_dbg_stack-05.js b/browser/devtools/debugger/test/browser_dbg_stack-05.js index 1add5f590c2b..1d52a50d1db7 100644 --- a/browser/devtools/debugger/test/browser_dbg_stack-05.js +++ b/browser/devtools/debugger/test/browser_dbg_stack-05.js @@ -24,7 +24,7 @@ function test() { gPane = aPane; gDebugger = gPane.debuggerWindow; - gPane.activeThread.addOneTimeListener("framesadded", function() { + gDebugger.DebuggerController.activeThread.addOneTimeListener("framesadded", function() { framesAdded = true; runTest(); }); @@ -51,7 +51,7 @@ function test() { function testRecurse() { - let frames = gDebugger.DebuggerView.Stackframes._frames; + let frames = gDebugger.DebuggerView.StackFrames._frames; let childNodes = frames.childNodes; is(frames.querySelectorAll(".dbg-stackframe").length, 4, @@ -95,7 +95,7 @@ function testRecurse() is(gDebugger.editor.getDebugLocation(), 5, "editor debugger location is correct (frame 0 again)."); - gDebugger.StackFrames.activeThread.resume(function() { + gDebugger.DebuggerController.activeThread.resume(function() { is(gDebugger.editor.getDebugLocation(), -1, "editor debugger location is correct after resume."); closeDebuggerAndFinish(gTab); diff --git a/browser/devtools/debugger/test/browser_dbg_stack.html b/browser/devtools/debugger/test/browser_dbg_stack.html index e1af5b070cc6..c3d083087baa 100644 --- a/browser/devtools/debugger/test/browser_dbg_stack.html +++ b/browser/devtools/debugger/test/browser_dbg_stack.html @@ -1,6 +1,6 @@ -Browser Debugger Test Tab +Browser Debugger Test Tab - - - + + + diff --git a/browser/devtools/debugger/test/browser_dbg_update-editor-mode.js b/browser/devtools/debugger/test/browser_dbg_update-editor-mode.js index 93b1b54c8690..972abbb5d04d 100644 --- a/browser/devtools/debugger/test/browser_dbg_update-editor-mode.js +++ b/browser/devtools/debugger/test/browser_dbg_update-editor-mode.js @@ -29,7 +29,7 @@ function test() gPane = aPane; gDebugger = gPane.debuggerWindow; - gPane.activeThread.addOneTimeListener("framesadded", function() { + gDebugger.DebuggerController.activeThread.addOneTimeListener("framesadded", function() { framesAdded = true; runTest(); }); @@ -56,7 +56,7 @@ function test() function testScriptsDisplay() { gScripts = gDebugger.DebuggerView.Scripts._scripts; - is(gDebugger.StackFrames.activeThread.state, "paused", + is(gDebugger.DebuggerController.activeThread.state, "paused", "Should only be getting stack frames while paused."); is(gScripts.itemCount, 2, "Found the expected number of scripts."); @@ -90,7 +90,7 @@ function testSwitchPaused() is(gDebugger.editor.getMode(), SourceEditor.MODES.JAVASCRIPT, "Found the expected editor mode."); - gDebugger.StackFrames.activeThread.resume(function() { + gDebugger.DebuggerController.activeThread.resume(function() { closeDebuggerAndFinish(gTab); }); } diff --git a/browser/devtools/debugger/test/head.js b/browser/devtools/debugger/test/head.js index dced2c347ab1..49acd5f74a20 100644 --- a/browser/devtools/debugger/test/head.js +++ b/browser/devtools/debugger/test/head.js @@ -50,8 +50,8 @@ function removeTab(aTab) { } function closeDebuggerAndFinish(aTab) { - DebuggerUI.aWindow.addEventListener("Debugger:Shutdown", function cleanup() { - DebuggerUI.aWindow.removeEventListener("Debugger:Shutdown", cleanup, false); + DebuggerUI.chromeWindow.addEventListener("Debugger:Shutdown", function cleanup() { + DebuggerUI.chromeWindow.removeEventListener("Debugger:Shutdown", cleanup, false); finish(); }, false); DebuggerUI.getDebugger(aTab).close(); @@ -96,12 +96,13 @@ function debug_tab_pane(aURL, aOnDebugging) let debuggee = tab.linkedBrowser.contentWindow.wrappedJSObject; let pane = DebuggerUI.toggleDebugger(); - pane.onConnected = function() { + pane._frame.addEventListener("Debugger:Connecting", function dbgConnected() { + pane._frame.removeEventListener("Debugger:Connecting", dbgConnected, true); + // Wait for the initial resume... pane.debuggerWindow.gClient.addOneTimeListener("resumed", function() { - delete pane.onConnected; aOnDebugging(tab, debuggee, pane); }); - }; + }, true); }); } diff --git a/browser/devtools/jar.mn b/browser/devtools/jar.mn index 8c918130825a..d2c29cba65d5 100644 --- a/browser/devtools/jar.mn +++ b/browser/devtools/jar.mn @@ -13,6 +13,5 @@ browser.jar: * content/browser/source-editor-overlay.xul (sourceeditor/source-editor-overlay.xul) * content/browser/debugger.xul (debugger/debugger.xul) content/browser/debugger.css (debugger/debugger.css) - content/browser/debugger.js (debugger/debugger.js) + content/browser/debugger-controller.js (debugger/debugger-controller.js) content/browser/debugger-view.js (debugger/debugger-view.js) - diff --git a/browser/devtools/webconsole/GcliCommands.jsm b/browser/devtools/webconsole/GcliCommands.jsm index 92ba54fc7734..470d1daa02ff 100644 --- a/browser/devtools/webconsole/GcliCommands.jsm +++ b/browser/devtools/webconsole/GcliCommands.jsm @@ -229,8 +229,8 @@ gcli.addCommand({ let dbg = win.DebuggerUI.getDebugger(win.gBrowser.selectedTab); let files = []; if (dbg) { - let scriptsView = dbg.frame.contentWindow.DebuggerView.Scripts; - for each (let script in scriptsView.scriptLocations()) { + let scriptsView = dbg.debuggerWindow.DebuggerView.Scripts; + for each (let script in scriptsView.scriptLocations) { files.push(script); } } diff --git a/browser/devtools/webconsole/test/browser_gcli_break.js b/browser/devtools/webconsole/test/browser_gcli_break.js index da33681d323a..ebac4b5fd6e4 100644 --- a/browser/devtools/webconsole/test/browser_gcli_break.js +++ b/browser/devtools/webconsole/test/browser_gcli_break.js @@ -69,11 +69,12 @@ function testCreateCommands() { is(requisition.getStatus().toString(), "ERROR", "break add line is ERROR"); let pane = DebuggerUI.toggleDebugger(); - pane.onConnected = function test_onConnected(aPane) { + pane._frame.addEventListener("Debugger:Connecting", function dbgConnected() { + pane._frame.removeEventListener("Debugger:Connecting", dbgConnected, true); + // Wait for the initial resume. - aPane.debuggerWindow.gClient.addOneTimeListener("resumed", function() { - delete aPane.onConnected; - aPane.debuggerWindow.gClient.activeThread.addOneTimeListener("framesadded", function() { + pane.debuggerWindow.gClient.addOneTimeListener("resumed", function() { + pane.debuggerWindow.gClient.activeThread.addOneTimeListener("framesadded", function() { type("break add line " + TEST_URI + " " + content.wrappedJSObject.line0); is(requisition.getStatus().toString(), "VALID", "break add line is VALID"); requisition.exec(); @@ -82,7 +83,7 @@ function testCreateCommands() { is(requisition.getStatus().toString(), "VALID", "break list is VALID"); requisition.exec(); - aPane.debuggerWindow.gClient.activeThread.resume(function() { + pane.debuggerWindow.gClient.activeThread.resume(function() { type("break del 0"); is(requisition.getStatus().toString(), "VALID", "break del 0 is VALID"); requisition.exec(); @@ -94,7 +95,7 @@ function testCreateCommands() { // Trigger newScript notifications using eval. content.wrappedJSObject.firstCall(); }); - } + }, true); } function type(command) {