Wed, 31 Dec 2014 06:09:35 +0100
Cloned upstream origin tor-browser at tor-browser-31.3.0esr-4.5-1-build1
revision ID fc1c9ff7c1b2defdbc039f12214767608f46423f for hacking purpose.
michael@0 | 1 | /* -*- Mode: javascript; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ |
michael@0 | 2 | /* vim: set ft=javascript ts=2 et sw=2 tw=80: */ |
michael@0 | 3 | /* This Source Code Form is subject to the terms of the Mozilla Public |
michael@0 | 4 | * License, v. 2.0. If a copy of the MPL was not distributed with this |
michael@0 | 5 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ |
michael@0 | 6 | "use strict"; |
michael@0 | 7 | |
michael@0 | 8 | const { classes: Cc, interfaces: Ci, utils: Cu } = Components; |
michael@0 | 9 | |
michael@0 | 10 | const DBG_STRINGS_URI = "chrome://browser/locale/devtools/debugger.properties"; |
michael@0 | 11 | const NEW_SOURCE_IGNORED_URLS = ["debugger eval code", "self-hosted", "XStringBundle"]; |
michael@0 | 12 | const NEW_SOURCE_DISPLAY_DELAY = 200; // ms |
michael@0 | 13 | const FETCH_SOURCE_RESPONSE_DELAY = 200; // ms |
michael@0 | 14 | const FETCH_EVENT_LISTENERS_DELAY = 200; // ms |
michael@0 | 15 | const FRAME_STEP_CLEAR_DELAY = 100; // ms |
michael@0 | 16 | const CALL_STACK_PAGE_SIZE = 25; // frames |
michael@0 | 17 | |
michael@0 | 18 | // The panel's window global is an EventEmitter firing the following events: |
michael@0 | 19 | const EVENTS = { |
michael@0 | 20 | // When the debugger's source editor instance finishes loading or unloading. |
michael@0 | 21 | EDITOR_LOADED: "Debugger:EditorLoaded", |
michael@0 | 22 | EDITOR_UNLOADED: "Debugger:EditorUnoaded", |
michael@0 | 23 | |
michael@0 | 24 | // When new sources are received from the debugger server. |
michael@0 | 25 | NEW_SOURCE: "Debugger:NewSource", |
michael@0 | 26 | SOURCES_ADDED: "Debugger:SourcesAdded", |
michael@0 | 27 | |
michael@0 | 28 | // When a source is shown in the source editor. |
michael@0 | 29 | SOURCE_SHOWN: "Debugger:EditorSourceShown", |
michael@0 | 30 | SOURCE_ERROR_SHOWN: "Debugger:EditorSourceErrorShown", |
michael@0 | 31 | |
michael@0 | 32 | // When the editor has shown a source and set the line / column position |
michael@0 | 33 | EDITOR_LOCATION_SET: "Debugger:EditorLocationSet", |
michael@0 | 34 | |
michael@0 | 35 | // When scopes, variables, properties and watch expressions are fetched and |
michael@0 | 36 | // displayed in the variables view. |
michael@0 | 37 | FETCHED_SCOPES: "Debugger:FetchedScopes", |
michael@0 | 38 | FETCHED_VARIABLES: "Debugger:FetchedVariables", |
michael@0 | 39 | FETCHED_PROPERTIES: "Debugger:FetchedProperties", |
michael@0 | 40 | FETCHED_BUBBLE_PROPERTIES: "Debugger:FetchedBubbleProperties", |
michael@0 | 41 | FETCHED_WATCH_EXPRESSIONS: "Debugger:FetchedWatchExpressions", |
michael@0 | 42 | |
michael@0 | 43 | // When a breakpoint has been added or removed on the debugger server. |
michael@0 | 44 | BREAKPOINT_ADDED: "Debugger:BreakpointAdded", |
michael@0 | 45 | BREAKPOINT_REMOVED: "Debugger:BreakpointRemoved", |
michael@0 | 46 | |
michael@0 | 47 | // When a breakpoint has been shown or hidden in the source editor. |
michael@0 | 48 | BREAKPOINT_SHOWN: "Debugger:BreakpointShown", |
michael@0 | 49 | BREAKPOINT_HIDDEN: "Debugger:BreakpointHidden", |
michael@0 | 50 | |
michael@0 | 51 | // When a conditional breakpoint's popup is showing or hiding. |
michael@0 | 52 | CONDITIONAL_BREAKPOINT_POPUP_SHOWING: "Debugger:ConditionalBreakpointPopupShowing", |
michael@0 | 53 | CONDITIONAL_BREAKPOINT_POPUP_HIDING: "Debugger:ConditionalBreakpointPopupHiding", |
michael@0 | 54 | |
michael@0 | 55 | // When event listeners are fetched or event breakpoints are updated. |
michael@0 | 56 | EVENT_LISTENERS_FETCHED: "Debugger:EventListenersFetched", |
michael@0 | 57 | EVENT_BREAKPOINTS_UPDATED: "Debugger:EventBreakpointsUpdated", |
michael@0 | 58 | |
michael@0 | 59 | // When a file search was performed. |
michael@0 | 60 | FILE_SEARCH_MATCH_FOUND: "Debugger:FileSearch:MatchFound", |
michael@0 | 61 | FILE_SEARCH_MATCH_NOT_FOUND: "Debugger:FileSearch:MatchNotFound", |
michael@0 | 62 | |
michael@0 | 63 | // When a function search was performed. |
michael@0 | 64 | FUNCTION_SEARCH_MATCH_FOUND: "Debugger:FunctionSearch:MatchFound", |
michael@0 | 65 | FUNCTION_SEARCH_MATCH_NOT_FOUND: "Debugger:FunctionSearch:MatchNotFound", |
michael@0 | 66 | |
michael@0 | 67 | // When a global text search was performed. |
michael@0 | 68 | GLOBAL_SEARCH_MATCH_FOUND: "Debugger:GlobalSearch:MatchFound", |
michael@0 | 69 | GLOBAL_SEARCH_MATCH_NOT_FOUND: "Debugger:GlobalSearch:MatchNotFound", |
michael@0 | 70 | |
michael@0 | 71 | // After the stackframes are cleared and debugger won't pause anymore. |
michael@0 | 72 | AFTER_FRAMES_CLEARED: "Debugger:AfterFramesCleared", |
michael@0 | 73 | |
michael@0 | 74 | // When the options popup is showing or hiding. |
michael@0 | 75 | OPTIONS_POPUP_SHOWING: "Debugger:OptionsPopupShowing", |
michael@0 | 76 | OPTIONS_POPUP_HIDDEN: "Debugger:OptionsPopupHidden", |
michael@0 | 77 | |
michael@0 | 78 | // When the widgets layout has been changed. |
michael@0 | 79 | LAYOUT_CHANGED: "Debugger:LayoutChanged" |
michael@0 | 80 | }; |
michael@0 | 81 | |
michael@0 | 82 | // Descriptions for what a stack frame represents after the debugger pauses. |
michael@0 | 83 | const FRAME_TYPE = { |
michael@0 | 84 | NORMAL: 0, |
michael@0 | 85 | CONDITIONAL_BREAKPOINT_EVAL: 1, |
michael@0 | 86 | WATCH_EXPRESSIONS_EVAL: 2, |
michael@0 | 87 | PUBLIC_CLIENT_EVAL: 3 |
michael@0 | 88 | }; |
michael@0 | 89 | |
michael@0 | 90 | Cu.import("resource://gre/modules/Services.jsm"); |
michael@0 | 91 | Cu.import("resource://gre/modules/XPCOMUtils.jsm"); |
michael@0 | 92 | Cu.import("resource://gre/modules/devtools/event-emitter.js"); |
michael@0 | 93 | Cu.import("resource://gre/modules/Task.jsm"); |
michael@0 | 94 | Cu.import("resource:///modules/devtools/SimpleListWidget.jsm"); |
michael@0 | 95 | Cu.import("resource:///modules/devtools/BreadcrumbsWidget.jsm"); |
michael@0 | 96 | Cu.import("resource:///modules/devtools/SideMenuWidget.jsm"); |
michael@0 | 97 | Cu.import("resource:///modules/devtools/VariablesView.jsm"); |
michael@0 | 98 | Cu.import("resource:///modules/devtools/VariablesViewController.jsm"); |
michael@0 | 99 | Cu.import("resource:///modules/devtools/ViewHelpers.jsm"); |
michael@0 | 100 | |
michael@0 | 101 | const require = Cu.import("resource://gre/modules/devtools/Loader.jsm", {}).devtools.require; |
michael@0 | 102 | const promise = require("devtools/toolkit/deprecated-sync-thenables"); |
michael@0 | 103 | const Editor = require("devtools/sourceeditor/editor"); |
michael@0 | 104 | const DebuggerEditor = require("devtools/sourceeditor/debugger.js"); |
michael@0 | 105 | const {Tooltip} = require("devtools/shared/widgets/Tooltip"); |
michael@0 | 106 | const FastListWidget = require("devtools/shared/widgets/FastListWidget"); |
michael@0 | 107 | |
michael@0 | 108 | XPCOMUtils.defineLazyModuleGetter(this, "Parser", |
michael@0 | 109 | "resource:///modules/devtools/Parser.jsm"); |
michael@0 | 110 | |
michael@0 | 111 | XPCOMUtils.defineLazyModuleGetter(this, "devtools", |
michael@0 | 112 | "resource://gre/modules/devtools/Loader.jsm"); |
michael@0 | 113 | |
michael@0 | 114 | XPCOMUtils.defineLazyModuleGetter(this, "DevToolsUtils", |
michael@0 | 115 | "resource://gre/modules/devtools/DevToolsUtils.jsm"); |
michael@0 | 116 | |
michael@0 | 117 | XPCOMUtils.defineLazyModuleGetter(this, "ShortcutUtils", |
michael@0 | 118 | "resource://gre/modules/ShortcutUtils.jsm"); |
michael@0 | 119 | |
michael@0 | 120 | Object.defineProperty(this, "NetworkHelper", { |
michael@0 | 121 | get: function() { |
michael@0 | 122 | return devtools.require("devtools/toolkit/webconsole/network-helper"); |
michael@0 | 123 | }, |
michael@0 | 124 | configurable: true, |
michael@0 | 125 | enumerable: true |
michael@0 | 126 | }); |
michael@0 | 127 | |
michael@0 | 128 | /** |
michael@0 | 129 | * Object defining the debugger controller components. |
michael@0 | 130 | */ |
michael@0 | 131 | let DebuggerController = { |
michael@0 | 132 | /** |
michael@0 | 133 | * Initializes the debugger controller. |
michael@0 | 134 | */ |
michael@0 | 135 | initialize: function() { |
michael@0 | 136 | dumpn("Initializing the DebuggerController"); |
michael@0 | 137 | |
michael@0 | 138 | this.startupDebugger = this.startupDebugger.bind(this); |
michael@0 | 139 | this.shutdownDebugger = this.shutdownDebugger.bind(this); |
michael@0 | 140 | this._onTabNavigated = this._onTabNavigated.bind(this); |
michael@0 | 141 | this._onTabDetached = this._onTabDetached.bind(this); |
michael@0 | 142 | }, |
michael@0 | 143 | |
michael@0 | 144 | /** |
michael@0 | 145 | * Initializes the view. |
michael@0 | 146 | * |
michael@0 | 147 | * @return object |
michael@0 | 148 | * A promise that is resolved when the debugger finishes startup. |
michael@0 | 149 | */ |
michael@0 | 150 | startupDebugger: function() { |
michael@0 | 151 | if (this._startup) { |
michael@0 | 152 | return this._startup; |
michael@0 | 153 | } |
michael@0 | 154 | |
michael@0 | 155 | return this._startup = DebuggerView.initialize(); |
michael@0 | 156 | }, |
michael@0 | 157 | |
michael@0 | 158 | /** |
michael@0 | 159 | * Destroys the view and disconnects the debugger client from the server. |
michael@0 | 160 | * |
michael@0 | 161 | * @return object |
michael@0 | 162 | * A promise that is resolved when the debugger finishes shutdown. |
michael@0 | 163 | */ |
michael@0 | 164 | shutdownDebugger: function() { |
michael@0 | 165 | if (this._shutdown) { |
michael@0 | 166 | return this._shutdown; |
michael@0 | 167 | } |
michael@0 | 168 | |
michael@0 | 169 | return this._shutdown = DebuggerView.destroy().then(() => { |
michael@0 | 170 | DebuggerView.destroy(); |
michael@0 | 171 | this.SourceScripts.disconnect(); |
michael@0 | 172 | this.StackFrames.disconnect(); |
michael@0 | 173 | this.ThreadState.disconnect(); |
michael@0 | 174 | this.Tracer.disconnect(); |
michael@0 | 175 | this.disconnect(); |
michael@0 | 176 | }); |
michael@0 | 177 | }, |
michael@0 | 178 | |
michael@0 | 179 | /** |
michael@0 | 180 | * Initiates remote debugging based on the current target, wiring event |
michael@0 | 181 | * handlers as necessary. |
michael@0 | 182 | * |
michael@0 | 183 | * @return object |
michael@0 | 184 | * A promise that is resolved when the debugger finishes connecting. |
michael@0 | 185 | */ |
michael@0 | 186 | connect: function() { |
michael@0 | 187 | if (this._connection) { |
michael@0 | 188 | return this._connection; |
michael@0 | 189 | } |
michael@0 | 190 | |
michael@0 | 191 | let startedDebugging = promise.defer(); |
michael@0 | 192 | this._connection = startedDebugging.promise; |
michael@0 | 193 | |
michael@0 | 194 | let target = this._target; |
michael@0 | 195 | let { client, form: { chromeDebugger, traceActor, addonActor } } = target; |
michael@0 | 196 | target.on("close", this._onTabDetached); |
michael@0 | 197 | target.on("navigate", this._onTabNavigated); |
michael@0 | 198 | target.on("will-navigate", this._onTabNavigated); |
michael@0 | 199 | this.client = client; |
michael@0 | 200 | |
michael@0 | 201 | if (addonActor) { |
michael@0 | 202 | this._startAddonDebugging(addonActor, startedDebugging.resolve); |
michael@0 | 203 | } else if (target.chrome) { |
michael@0 | 204 | this._startChromeDebugging(chromeDebugger, startedDebugging.resolve); |
michael@0 | 205 | } else { |
michael@0 | 206 | this._startDebuggingTab(startedDebugging.resolve); |
michael@0 | 207 | const startedTracing = promise.defer(); |
michael@0 | 208 | if (Prefs.tracerEnabled && traceActor) { |
michael@0 | 209 | this._startTracingTab(traceActor, startedTracing.resolve); |
michael@0 | 210 | } else { |
michael@0 | 211 | startedTracing.resolve(); |
michael@0 | 212 | } |
michael@0 | 213 | |
michael@0 | 214 | return promise.all([startedDebugging.promise, startedTracing.promise]); |
michael@0 | 215 | } |
michael@0 | 216 | |
michael@0 | 217 | return startedDebugging.promise; |
michael@0 | 218 | }, |
michael@0 | 219 | |
michael@0 | 220 | /** |
michael@0 | 221 | * Disconnects the debugger client and removes event handlers as necessary. |
michael@0 | 222 | */ |
michael@0 | 223 | disconnect: function() { |
michael@0 | 224 | // Return early if the client didn't even have a chance to instantiate. |
michael@0 | 225 | if (!this.client) { |
michael@0 | 226 | return; |
michael@0 | 227 | } |
michael@0 | 228 | |
michael@0 | 229 | this._connection = null; |
michael@0 | 230 | this.client = null; |
michael@0 | 231 | this.activeThread = null; |
michael@0 | 232 | }, |
michael@0 | 233 | |
michael@0 | 234 | /** |
michael@0 | 235 | * Called for each location change in the debugged tab. |
michael@0 | 236 | * |
michael@0 | 237 | * @param string aType |
michael@0 | 238 | * Packet type. |
michael@0 | 239 | * @param object aPacket |
michael@0 | 240 | * Packet received from the server. |
michael@0 | 241 | */ |
michael@0 | 242 | _onTabNavigated: function(aType, aPacket) { |
michael@0 | 243 | switch (aType) { |
michael@0 | 244 | case "will-navigate": { |
michael@0 | 245 | // Reset UI. |
michael@0 | 246 | DebuggerView.handleTabNavigation(); |
michael@0 | 247 | |
michael@0 | 248 | // Discard all the cached sources *before* the target starts navigating. |
michael@0 | 249 | // Sources may be fetched during navigation, in which case we don't |
michael@0 | 250 | // want to hang on to the old source contents. |
michael@0 | 251 | DebuggerController.SourceScripts.clearCache(); |
michael@0 | 252 | DebuggerController.Parser.clearCache(); |
michael@0 | 253 | SourceUtils.clearCache(); |
michael@0 | 254 | |
michael@0 | 255 | // Prevent performing any actions that were scheduled before navigation. |
michael@0 | 256 | clearNamedTimeout("new-source"); |
michael@0 | 257 | clearNamedTimeout("event-breakpoints-update"); |
michael@0 | 258 | clearNamedTimeout("event-listeners-fetch"); |
michael@0 | 259 | break; |
michael@0 | 260 | } |
michael@0 | 261 | case "navigate": { |
michael@0 | 262 | this.ThreadState.handleTabNavigation(); |
michael@0 | 263 | this.StackFrames.handleTabNavigation(); |
michael@0 | 264 | this.SourceScripts.handleTabNavigation(); |
michael@0 | 265 | break; |
michael@0 | 266 | } |
michael@0 | 267 | } |
michael@0 | 268 | }, |
michael@0 | 269 | |
michael@0 | 270 | /** |
michael@0 | 271 | * Called when the debugged tab is closed. |
michael@0 | 272 | */ |
michael@0 | 273 | _onTabDetached: function() { |
michael@0 | 274 | this.shutdownDebugger(); |
michael@0 | 275 | }, |
michael@0 | 276 | |
michael@0 | 277 | /** |
michael@0 | 278 | * Warn if resuming execution produced a wrongOrder error. |
michael@0 | 279 | */ |
michael@0 | 280 | _ensureResumptionOrder: function(aResponse) { |
michael@0 | 281 | if (aResponse.error == "wrongOrder") { |
michael@0 | 282 | DebuggerView.Toolbar.showResumeWarning(aResponse.lastPausedUrl); |
michael@0 | 283 | } |
michael@0 | 284 | }, |
michael@0 | 285 | |
michael@0 | 286 | /** |
michael@0 | 287 | * Sets up a debugging session. |
michael@0 | 288 | * |
michael@0 | 289 | * @param function aCallback |
michael@0 | 290 | * A function to invoke once the client attaches to the active thread. |
michael@0 | 291 | */ |
michael@0 | 292 | _startDebuggingTab: function(aCallback) { |
michael@0 | 293 | this._target.activeTab.attachThread({ |
michael@0 | 294 | useSourceMaps: Prefs.sourceMapsEnabled |
michael@0 | 295 | }, (aResponse, aThreadClient) => { |
michael@0 | 296 | if (!aThreadClient) { |
michael@0 | 297 | Cu.reportError("Couldn't attach to thread: " + aResponse.error); |
michael@0 | 298 | return; |
michael@0 | 299 | } |
michael@0 | 300 | this.activeThread = aThreadClient; |
michael@0 | 301 | |
michael@0 | 302 | this.ThreadState.connect(); |
michael@0 | 303 | this.StackFrames.connect(); |
michael@0 | 304 | this.SourceScripts.connect(); |
michael@0 | 305 | if (aThreadClient.paused) { |
michael@0 | 306 | aThreadClient.resume(this._ensureResumptionOrder); |
michael@0 | 307 | } |
michael@0 | 308 | |
michael@0 | 309 | if (aCallback) { |
michael@0 | 310 | aCallback(); |
michael@0 | 311 | } |
michael@0 | 312 | }); |
michael@0 | 313 | }, |
michael@0 | 314 | |
michael@0 | 315 | /** |
michael@0 | 316 | * Sets up an addon debugging session. |
michael@0 | 317 | * |
michael@0 | 318 | * @param object aAddonActor |
michael@0 | 319 | * The actor for the addon that is being debugged. |
michael@0 | 320 | * @param function aCallback |
michael@0 | 321 | * A function to invoke once the client attaches to the active thread. |
michael@0 | 322 | */ |
michael@0 | 323 | _startAddonDebugging: function(aAddonActor, aCallback) { |
michael@0 | 324 | this.client.attachAddon(aAddonActor, (aResponse) => { |
michael@0 | 325 | return this._startChromeDebugging(aResponse.threadActor, aCallback); |
michael@0 | 326 | }); |
michael@0 | 327 | }, |
michael@0 | 328 | |
michael@0 | 329 | /** |
michael@0 | 330 | * Sets up a chrome debugging session. |
michael@0 | 331 | * |
michael@0 | 332 | * @param object aChromeDebugger |
michael@0 | 333 | * The remote protocol grip of the chrome debugger. |
michael@0 | 334 | * @param function aCallback |
michael@0 | 335 | * A function to invoke once the client attaches to the active thread. |
michael@0 | 336 | */ |
michael@0 | 337 | _startChromeDebugging: function(aChromeDebugger, aCallback) { |
michael@0 | 338 | this.client.attachThread(aChromeDebugger, (aResponse, aThreadClient) => { |
michael@0 | 339 | if (!aThreadClient) { |
michael@0 | 340 | Cu.reportError("Couldn't attach to thread: " + aResponse.error); |
michael@0 | 341 | return; |
michael@0 | 342 | } |
michael@0 | 343 | this.activeThread = aThreadClient; |
michael@0 | 344 | |
michael@0 | 345 | this.ThreadState.connect(); |
michael@0 | 346 | this.StackFrames.connect(); |
michael@0 | 347 | this.SourceScripts.connect(); |
michael@0 | 348 | if (aThreadClient.paused) { |
michael@0 | 349 | aThreadClient.resume(this._ensureResumptionOrder); |
michael@0 | 350 | } |
michael@0 | 351 | |
michael@0 | 352 | if (aCallback) { |
michael@0 | 353 | aCallback(); |
michael@0 | 354 | } |
michael@0 | 355 | }, { useSourceMaps: Prefs.sourceMapsEnabled }); |
michael@0 | 356 | }, |
michael@0 | 357 | |
michael@0 | 358 | /** |
michael@0 | 359 | * Sets up an execution tracing session. |
michael@0 | 360 | * |
michael@0 | 361 | * @param object aTraceActor |
michael@0 | 362 | * The remote protocol grip of the trace actor. |
michael@0 | 363 | * @param function aCallback |
michael@0 | 364 | * A function to invoke once the client attaches to the tracer. |
michael@0 | 365 | */ |
michael@0 | 366 | _startTracingTab: function(aTraceActor, aCallback) { |
michael@0 | 367 | this.client.attachTracer(aTraceActor, (response, traceClient) => { |
michael@0 | 368 | if (!traceClient) { |
michael@0 | 369 | DevToolsUtils.reportException("DebuggerController._startTracingTab", |
michael@0 | 370 | new Error("Failed to attach to tracing actor.")); |
michael@0 | 371 | return; |
michael@0 | 372 | } |
michael@0 | 373 | |
michael@0 | 374 | this.traceClient = traceClient; |
michael@0 | 375 | this.Tracer.connect(); |
michael@0 | 376 | |
michael@0 | 377 | if (aCallback) { |
michael@0 | 378 | aCallback(); |
michael@0 | 379 | } |
michael@0 | 380 | }); |
michael@0 | 381 | }, |
michael@0 | 382 | |
michael@0 | 383 | /** |
michael@0 | 384 | * Detach and reattach to the thread actor with useSourceMaps true, blow |
michael@0 | 385 | * away old sources and get them again. |
michael@0 | 386 | */ |
michael@0 | 387 | reconfigureThread: function(aUseSourceMaps) { |
michael@0 | 388 | this.activeThread.reconfigure({ useSourceMaps: aUseSourceMaps }, aResponse => { |
michael@0 | 389 | if (aResponse.error) { |
michael@0 | 390 | let msg = "Couldn't reconfigure thread: " + aResponse.message; |
michael@0 | 391 | Cu.reportError(msg); |
michael@0 | 392 | dumpn(msg); |
michael@0 | 393 | return; |
michael@0 | 394 | } |
michael@0 | 395 | |
michael@0 | 396 | // Reset the view and fetch all the sources again. |
michael@0 | 397 | DebuggerView.handleTabNavigation(); |
michael@0 | 398 | this.SourceScripts.handleTabNavigation(); |
michael@0 | 399 | |
michael@0 | 400 | // Update the stack frame list. |
michael@0 | 401 | if (this.activeThread.paused) { |
michael@0 | 402 | this.activeThread._clearFrames(); |
michael@0 | 403 | this.activeThread.fillFrames(CALL_STACK_PAGE_SIZE); |
michael@0 | 404 | } |
michael@0 | 405 | }); |
michael@0 | 406 | }, |
michael@0 | 407 | |
michael@0 | 408 | _startup: null, |
michael@0 | 409 | _shutdown: null, |
michael@0 | 410 | _connection: null, |
michael@0 | 411 | client: null, |
michael@0 | 412 | activeThread: null |
michael@0 | 413 | }; |
michael@0 | 414 | |
michael@0 | 415 | /** |
michael@0 | 416 | * ThreadState keeps the UI up to date with the state of the |
michael@0 | 417 | * thread (paused/attached/etc.). |
michael@0 | 418 | */ |
michael@0 | 419 | function ThreadState() { |
michael@0 | 420 | this._update = this._update.bind(this); |
michael@0 | 421 | } |
michael@0 | 422 | |
michael@0 | 423 | ThreadState.prototype = { |
michael@0 | 424 | get activeThread() DebuggerController.activeThread, |
michael@0 | 425 | |
michael@0 | 426 | /** |
michael@0 | 427 | * Connect to the current thread client. |
michael@0 | 428 | */ |
michael@0 | 429 | connect: function() { |
michael@0 | 430 | dumpn("ThreadState is connecting..."); |
michael@0 | 431 | this.activeThread.addListener("paused", this._update); |
michael@0 | 432 | this.activeThread.addListener("resumed", this._update); |
michael@0 | 433 | this.activeThread.pauseOnExceptions(Prefs.pauseOnExceptions, |
michael@0 | 434 | Prefs.ignoreCaughtExceptions); |
michael@0 | 435 | this.handleTabNavigation(); |
michael@0 | 436 | }, |
michael@0 | 437 | |
michael@0 | 438 | /** |
michael@0 | 439 | * Disconnect from the client. |
michael@0 | 440 | */ |
michael@0 | 441 | disconnect: function() { |
michael@0 | 442 | if (!this.activeThread) { |
michael@0 | 443 | return; |
michael@0 | 444 | } |
michael@0 | 445 | dumpn("ThreadState is disconnecting..."); |
michael@0 | 446 | this.activeThread.removeListener("paused", this._update); |
michael@0 | 447 | this.activeThread.removeListener("resumed", this._update); |
michael@0 | 448 | }, |
michael@0 | 449 | |
michael@0 | 450 | /** |
michael@0 | 451 | * Handles any initialization on a tab navigation event issued by the client. |
michael@0 | 452 | */ |
michael@0 | 453 | handleTabNavigation: function() { |
michael@0 | 454 | if (!this.activeThread) { |
michael@0 | 455 | return; |
michael@0 | 456 | } |
michael@0 | 457 | dumpn("Handling tab navigation in the ThreadState"); |
michael@0 | 458 | this._update(); |
michael@0 | 459 | }, |
michael@0 | 460 | |
michael@0 | 461 | /** |
michael@0 | 462 | * Update the UI after a thread state change. |
michael@0 | 463 | */ |
michael@0 | 464 | _update: function(aEvent) { |
michael@0 | 465 | DebuggerView.Toolbar.toggleResumeButtonState(this.activeThread.state); |
michael@0 | 466 | |
michael@0 | 467 | if (gTarget && (aEvent == "paused" || aEvent == "resumed")) { |
michael@0 | 468 | gTarget.emit("thread-" + aEvent); |
michael@0 | 469 | } |
michael@0 | 470 | } |
michael@0 | 471 | }; |
michael@0 | 472 | |
michael@0 | 473 | /** |
michael@0 | 474 | * Keeps the stack frame list up-to-date, using the thread client's |
michael@0 | 475 | * stack frame cache. |
michael@0 | 476 | */ |
michael@0 | 477 | function StackFrames() { |
michael@0 | 478 | this._onPaused = this._onPaused.bind(this); |
michael@0 | 479 | this._onResumed = this._onResumed.bind(this); |
michael@0 | 480 | this._onFrames = this._onFrames.bind(this); |
michael@0 | 481 | this._onFramesCleared = this._onFramesCleared.bind(this); |
michael@0 | 482 | this._onBlackBoxChange = this._onBlackBoxChange.bind(this); |
michael@0 | 483 | this._onPrettyPrintChange = this._onPrettyPrintChange.bind(this); |
michael@0 | 484 | this._afterFramesCleared = this._afterFramesCleared.bind(this); |
michael@0 | 485 | this.evaluate = this.evaluate.bind(this); |
michael@0 | 486 | } |
michael@0 | 487 | |
michael@0 | 488 | StackFrames.prototype = { |
michael@0 | 489 | get activeThread() DebuggerController.activeThread, |
michael@0 | 490 | currentFrameDepth: -1, |
michael@0 | 491 | _currentFrameDescription: FRAME_TYPE.NORMAL, |
michael@0 | 492 | _syncedWatchExpressions: null, |
michael@0 | 493 | _currentWatchExpressions: null, |
michael@0 | 494 | _currentBreakpointLocation: null, |
michael@0 | 495 | _currentEvaluation: null, |
michael@0 | 496 | _currentException: null, |
michael@0 | 497 | _currentReturnedValue: null, |
michael@0 | 498 | |
michael@0 | 499 | /** |
michael@0 | 500 | * Connect to the current thread client. |
michael@0 | 501 | */ |
michael@0 | 502 | connect: function() { |
michael@0 | 503 | dumpn("StackFrames is connecting..."); |
michael@0 | 504 | this.activeThread.addListener("paused", this._onPaused); |
michael@0 | 505 | this.activeThread.addListener("resumed", this._onResumed); |
michael@0 | 506 | this.activeThread.addListener("framesadded", this._onFrames); |
michael@0 | 507 | this.activeThread.addListener("framescleared", this._onFramesCleared); |
michael@0 | 508 | this.activeThread.addListener("blackboxchange", this._onBlackBoxChange); |
michael@0 | 509 | this.activeThread.addListener("prettyprintchange", this._onPrettyPrintChange); |
michael@0 | 510 | this.handleTabNavigation(); |
michael@0 | 511 | }, |
michael@0 | 512 | |
michael@0 | 513 | /** |
michael@0 | 514 | * Disconnect from the client. |
michael@0 | 515 | */ |
michael@0 | 516 | disconnect: function() { |
michael@0 | 517 | if (!this.activeThread) { |
michael@0 | 518 | return; |
michael@0 | 519 | } |
michael@0 | 520 | dumpn("StackFrames is disconnecting..."); |
michael@0 | 521 | this.activeThread.removeListener("paused", this._onPaused); |
michael@0 | 522 | this.activeThread.removeListener("resumed", this._onResumed); |
michael@0 | 523 | this.activeThread.removeListener("framesadded", this._onFrames); |
michael@0 | 524 | this.activeThread.removeListener("framescleared", this._onFramesCleared); |
michael@0 | 525 | this.activeThread.removeListener("blackboxchange", this._onBlackBoxChange); |
michael@0 | 526 | this.activeThread.removeListener("prettyprintchange", this._onPrettyPrintChange); |
michael@0 | 527 | clearNamedTimeout("frames-cleared"); |
michael@0 | 528 | }, |
michael@0 | 529 | |
michael@0 | 530 | /** |
michael@0 | 531 | * Handles any initialization on a tab navigation event issued by the client. |
michael@0 | 532 | */ |
michael@0 | 533 | handleTabNavigation: function() { |
michael@0 | 534 | dumpn("Handling tab navigation in the StackFrames"); |
michael@0 | 535 | // Nothing to do here yet. |
michael@0 | 536 | }, |
michael@0 | 537 | |
michael@0 | 538 | /** |
michael@0 | 539 | * Handler for the thread client's paused notification. |
michael@0 | 540 | * |
michael@0 | 541 | * @param string aEvent |
michael@0 | 542 | * The name of the notification ("paused" in this case). |
michael@0 | 543 | * @param object aPacket |
michael@0 | 544 | * The response packet. |
michael@0 | 545 | */ |
michael@0 | 546 | _onPaused: function(aEvent, aPacket) { |
michael@0 | 547 | switch (aPacket.why.type) { |
michael@0 | 548 | // If paused by a breakpoint, store the breakpoint location. |
michael@0 | 549 | case "breakpoint": |
michael@0 | 550 | this._currentBreakpointLocation = aPacket.frame.where; |
michael@0 | 551 | break; |
michael@0 | 552 | // If paused by a client evaluation, store the evaluated value. |
michael@0 | 553 | case "clientEvaluated": |
michael@0 | 554 | this._currentEvaluation = aPacket.why.frameFinished; |
michael@0 | 555 | break; |
michael@0 | 556 | // If paused by an exception, store the exception value. |
michael@0 | 557 | case "exception": |
michael@0 | 558 | this._currentException = aPacket.why.exception; |
michael@0 | 559 | break; |
michael@0 | 560 | // If paused while stepping out of a frame, store the returned value or |
michael@0 | 561 | // thrown exception. |
michael@0 | 562 | case "resumeLimit": |
michael@0 | 563 | if (!aPacket.why.frameFinished) { |
michael@0 | 564 | break; |
michael@0 | 565 | } else if (aPacket.why.frameFinished.throw) { |
michael@0 | 566 | this._currentException = aPacket.why.frameFinished.throw; |
michael@0 | 567 | } else if (aPacket.why.frameFinished.return) { |
michael@0 | 568 | this._currentReturnedValue = aPacket.why.frameFinished.return; |
michael@0 | 569 | } |
michael@0 | 570 | break; |
michael@0 | 571 | } |
michael@0 | 572 | |
michael@0 | 573 | this.activeThread.fillFrames(CALL_STACK_PAGE_SIZE); |
michael@0 | 574 | DebuggerView.editor.focus(); |
michael@0 | 575 | }, |
michael@0 | 576 | |
michael@0 | 577 | /** |
michael@0 | 578 | * Handler for the thread client's resumed notification. |
michael@0 | 579 | */ |
michael@0 | 580 | _onResumed: function() { |
michael@0 | 581 | // Prepare the watch expression evaluation string for the next pause. |
michael@0 | 582 | if (this._currentFrameDescription != FRAME_TYPE.WATCH_EXPRESSIONS_EVAL) { |
michael@0 | 583 | this._currentWatchExpressions = this._syncedWatchExpressions; |
michael@0 | 584 | } |
michael@0 | 585 | }, |
michael@0 | 586 | |
michael@0 | 587 | /** |
michael@0 | 588 | * Handler for the thread client's framesadded notification. |
michael@0 | 589 | */ |
michael@0 | 590 | _onFrames: function() { |
michael@0 | 591 | // Ignore useless notifications. |
michael@0 | 592 | if (!this.activeThread || !this.activeThread.cachedFrames.length) { |
michael@0 | 593 | return; |
michael@0 | 594 | } |
michael@0 | 595 | |
michael@0 | 596 | let waitForNextPause = false; |
michael@0 | 597 | let breakLocation = this._currentBreakpointLocation; |
michael@0 | 598 | let watchExpressions = this._currentWatchExpressions; |
michael@0 | 599 | let client = DebuggerController.activeThread.client; |
michael@0 | 600 | |
michael@0 | 601 | // We moved conditional breakpoint handling to the server, but |
michael@0 | 602 | // need to support it in the client for a while until most of the |
michael@0 | 603 | // server code in production is updated with it. bug 990137 is |
michael@0 | 604 | // filed to mark this code to be removed. |
michael@0 | 605 | if (!client.mainRoot.traits.conditionalBreakpoints) { |
michael@0 | 606 | // Conditional breakpoints are { breakpoint, expression } tuples. The |
michael@0 | 607 | // boolean evaluation of the expression decides if the active thread |
michael@0 | 608 | // automatically resumes execution or not. |
michael@0 | 609 | if (breakLocation) { |
michael@0 | 610 | // Make sure a breakpoint actually exists at the specified url and line. |
michael@0 | 611 | let breakpointPromise = DebuggerController.Breakpoints._getAdded(breakLocation); |
michael@0 | 612 | if (breakpointPromise) { |
michael@0 | 613 | breakpointPromise.then(({ conditionalExpression: e }) => { if (e) { |
michael@0 | 614 | // Evaluating the current breakpoint's conditional expression will |
michael@0 | 615 | // cause the stack frames to be cleared and active thread to pause, |
michael@0 | 616 | // sending a 'clientEvaluated' packed and adding the frames again. |
michael@0 | 617 | this.evaluate(e, { depth: 0, meta: FRAME_TYPE.CONDITIONAL_BREAKPOINT_EVAL }); |
michael@0 | 618 | waitForNextPause = true; |
michael@0 | 619 | }}); |
michael@0 | 620 | } |
michael@0 | 621 | } |
michael@0 | 622 | // We'll get our evaluation of the current breakpoint's conditional |
michael@0 | 623 | // expression the next time the thread client pauses... |
michael@0 | 624 | if (waitForNextPause) { |
michael@0 | 625 | return; |
michael@0 | 626 | } |
michael@0 | 627 | if (this._currentFrameDescription == FRAME_TYPE.CONDITIONAL_BREAKPOINT_EVAL) { |
michael@0 | 628 | this._currentFrameDescription = FRAME_TYPE.NORMAL; |
michael@0 | 629 | // If the breakpoint's conditional expression evaluation is falsy, |
michael@0 | 630 | // automatically resume execution. |
michael@0 | 631 | if (VariablesView.isFalsy({ value: this._currentEvaluation.return })) { |
michael@0 | 632 | this.activeThread.resume(DebuggerController._ensureResumptionOrder); |
michael@0 | 633 | return; |
michael@0 | 634 | } |
michael@0 | 635 | } |
michael@0 | 636 | } |
michael@0 | 637 | |
michael@0 | 638 | // Watch expressions are evaluated in the context of the topmost frame, |
michael@0 | 639 | // and the results are displayed in the variables view. |
michael@0 | 640 | // TODO: handle all of this server-side: Bug 832470, comment 14. |
michael@0 | 641 | if (watchExpressions) { |
michael@0 | 642 | // Evaluation causes the stack frames to be cleared and active thread to |
michael@0 | 643 | // pause, sending a 'clientEvaluated' packet and adding the frames again. |
michael@0 | 644 | this.evaluate(watchExpressions, { depth: 0, meta: FRAME_TYPE.WATCH_EXPRESSIONS_EVAL }); |
michael@0 | 645 | waitForNextPause = true; |
michael@0 | 646 | } |
michael@0 | 647 | // We'll get our evaluation of the current watch expressions the next time |
michael@0 | 648 | // the thread client pauses... |
michael@0 | 649 | if (waitForNextPause) { |
michael@0 | 650 | return; |
michael@0 | 651 | } |
michael@0 | 652 | if (this._currentFrameDescription == FRAME_TYPE.WATCH_EXPRESSIONS_EVAL) { |
michael@0 | 653 | this._currentFrameDescription = FRAME_TYPE.NORMAL; |
michael@0 | 654 | // If an error was thrown during the evaluation of the watch expressions, |
michael@0 | 655 | // then at least one expression evaluation could not be performed. So |
michael@0 | 656 | // remove the most recent watch expression and try again. |
michael@0 | 657 | if (this._currentEvaluation.throw) { |
michael@0 | 658 | DebuggerView.WatchExpressions.removeAt(0); |
michael@0 | 659 | DebuggerController.StackFrames.syncWatchExpressions(); |
michael@0 | 660 | return; |
michael@0 | 661 | } |
michael@0 | 662 | } |
michael@0 | 663 | |
michael@0 | 664 | // Make sure the debugger view panes are visible, then refill the frames. |
michael@0 | 665 | DebuggerView.showInstrumentsPane(); |
michael@0 | 666 | this._refillFrames(); |
michael@0 | 667 | |
michael@0 | 668 | // No additional processing is necessary for this stack frame. |
michael@0 | 669 | if (this._currentFrameDescription != FRAME_TYPE.NORMAL) { |
michael@0 | 670 | this._currentFrameDescription = FRAME_TYPE.NORMAL; |
michael@0 | 671 | } |
michael@0 | 672 | }, |
michael@0 | 673 | |
michael@0 | 674 | /** |
michael@0 | 675 | * Fill the StackFrames view with the frames we have in the cache, compressing |
michael@0 | 676 | * frames which have black boxed sources into single frames. |
michael@0 | 677 | */ |
michael@0 | 678 | _refillFrames: function() { |
michael@0 | 679 | // Make sure all the previous stackframes are removed before re-adding them. |
michael@0 | 680 | DebuggerView.StackFrames.empty(); |
michael@0 | 681 | |
michael@0 | 682 | for (let frame of this.activeThread.cachedFrames) { |
michael@0 | 683 | let { depth, where: { url, line }, source } = frame; |
michael@0 | 684 | let isBlackBoxed = source ? this.activeThread.source(source).isBlackBoxed : false; |
michael@0 | 685 | let location = NetworkHelper.convertToUnicode(unescape(url)); |
michael@0 | 686 | let title = StackFrameUtils.getFrameTitle(frame); |
michael@0 | 687 | DebuggerView.StackFrames.addFrame(title, location, line, depth, isBlackBoxed); |
michael@0 | 688 | } |
michael@0 | 689 | |
michael@0 | 690 | DebuggerView.StackFrames.selectedDepth = Math.max(this.currentFrameDepth, 0); |
michael@0 | 691 | DebuggerView.StackFrames.dirty = this.activeThread.moreFrames; |
michael@0 | 692 | }, |
michael@0 | 693 | |
michael@0 | 694 | /** |
michael@0 | 695 | * Handler for the thread client's framescleared notification. |
michael@0 | 696 | */ |
michael@0 | 697 | _onFramesCleared: function() { |
michael@0 | 698 | switch (this._currentFrameDescription) { |
michael@0 | 699 | case FRAME_TYPE.NORMAL: |
michael@0 | 700 | this._currentEvaluation = null; |
michael@0 | 701 | this._currentException = null; |
michael@0 | 702 | this._currentReturnedValue = null; |
michael@0 | 703 | break; |
michael@0 | 704 | case FRAME_TYPE.CONDITIONAL_BREAKPOINT_EVAL: |
michael@0 | 705 | this._currentBreakpointLocation = null; |
michael@0 | 706 | break; |
michael@0 | 707 | case FRAME_TYPE.WATCH_EXPRESSIONS_EVAL: |
michael@0 | 708 | this._currentWatchExpressions = null; |
michael@0 | 709 | break; |
michael@0 | 710 | } |
michael@0 | 711 | |
michael@0 | 712 | // After each frame step (in, over, out), framescleared is fired, which |
michael@0 | 713 | // forces the UI to be emptied and rebuilt on framesadded. Most of the times |
michael@0 | 714 | // this is not necessary, and will result in a brief redraw flicker. |
michael@0 | 715 | // To avoid it, invalidate the UI only after a short time if necessary. |
michael@0 | 716 | setNamedTimeout("frames-cleared", FRAME_STEP_CLEAR_DELAY, this._afterFramesCleared); |
michael@0 | 717 | }, |
michael@0 | 718 | |
michael@0 | 719 | /** |
michael@0 | 720 | * Handler for the debugger's blackboxchange notification. |
michael@0 | 721 | */ |
michael@0 | 722 | _onBlackBoxChange: function() { |
michael@0 | 723 | if (this.activeThread.state == "paused") { |
michael@0 | 724 | // Hack to avoid selecting the topmost frame after blackboxing a source. |
michael@0 | 725 | this.currentFrameDepth = NaN; |
michael@0 | 726 | this._refillFrames(); |
michael@0 | 727 | } |
michael@0 | 728 | }, |
michael@0 | 729 | |
michael@0 | 730 | /** |
michael@0 | 731 | * Handler for the debugger's prettyprintchange notification. |
michael@0 | 732 | */ |
michael@0 | 733 | _onPrettyPrintChange: function() { |
michael@0 | 734 | // Makes sure the selected source remains selected |
michael@0 | 735 | // after the fillFrames is called. |
michael@0 | 736 | const source = DebuggerView.Sources.selectedValue; |
michael@0 | 737 | if (this.activeThread.state == "paused") { |
michael@0 | 738 | this.activeThread.fillFrames( |
michael@0 | 739 | CALL_STACK_PAGE_SIZE, |
michael@0 | 740 | () => DebuggerView.Sources.selectedValue = source); |
michael@0 | 741 | } |
michael@0 | 742 | }, |
michael@0 | 743 | |
michael@0 | 744 | /** |
michael@0 | 745 | * Called soon after the thread client's framescleared notification. |
michael@0 | 746 | */ |
michael@0 | 747 | _afterFramesCleared: function() { |
michael@0 | 748 | // Ignore useless notifications. |
michael@0 | 749 | if (this.activeThread.cachedFrames.length) { |
michael@0 | 750 | return; |
michael@0 | 751 | } |
michael@0 | 752 | DebuggerView.editor.clearDebugLocation(); |
michael@0 | 753 | DebuggerView.StackFrames.empty(); |
michael@0 | 754 | DebuggerView.Sources.unhighlightBreakpoint(); |
michael@0 | 755 | DebuggerView.WatchExpressions.toggleContents(true); |
michael@0 | 756 | DebuggerView.Variables.empty(0); |
michael@0 | 757 | |
michael@0 | 758 | window.emit(EVENTS.AFTER_FRAMES_CLEARED); |
michael@0 | 759 | }, |
michael@0 | 760 | |
michael@0 | 761 | /** |
michael@0 | 762 | * Marks the stack frame at the specified depth as selected and updates the |
michael@0 | 763 | * properties view with the stack frame's data. |
michael@0 | 764 | * |
michael@0 | 765 | * @param number aDepth |
michael@0 | 766 | * The depth of the frame in the stack. |
michael@0 | 767 | */ |
michael@0 | 768 | selectFrame: function(aDepth) { |
michael@0 | 769 | // Make sure the frame at the specified depth exists first. |
michael@0 | 770 | let frame = this.activeThread.cachedFrames[this.currentFrameDepth = aDepth]; |
michael@0 | 771 | if (!frame) { |
michael@0 | 772 | return; |
michael@0 | 773 | } |
michael@0 | 774 | |
michael@0 | 775 | // Check if the frame does not represent the evaluation of debuggee code. |
michael@0 | 776 | let { environment, where } = frame; |
michael@0 | 777 | if (!environment) { |
michael@0 | 778 | return; |
michael@0 | 779 | } |
michael@0 | 780 | |
michael@0 | 781 | // Don't change the editor's location if the execution was paused by a |
michael@0 | 782 | // public client evaluation. This is useful for adding overlays on |
michael@0 | 783 | // top of the editor, like a variable inspection popup. |
michael@0 | 784 | let isClientEval = this._currentFrameDescription == FRAME_TYPE.PUBLIC_CLIENT_EVAL; |
michael@0 | 785 | let isPopupShown = DebuggerView.VariableBubble.contentsShown(); |
michael@0 | 786 | if (!isClientEval && !isPopupShown) { |
michael@0 | 787 | // Move the editor's caret to the proper url and line. |
michael@0 | 788 | DebuggerView.setEditorLocation(where.url, where.line); |
michael@0 | 789 | // Highlight the breakpoint at the specified url and line if it exists. |
michael@0 | 790 | DebuggerView.Sources.highlightBreakpoint(where, { noEditorUpdate: true }); |
michael@0 | 791 | } |
michael@0 | 792 | |
michael@0 | 793 | // Don't display the watch expressions textbox inputs in the pane. |
michael@0 | 794 | DebuggerView.WatchExpressions.toggleContents(false); |
michael@0 | 795 | |
michael@0 | 796 | // Start recording any added variables or properties in any scope and |
michael@0 | 797 | // clear existing scopes to create each one dynamically. |
michael@0 | 798 | DebuggerView.Variables.empty(); |
michael@0 | 799 | |
michael@0 | 800 | // If watch expressions evaluation results are available, create a scope |
michael@0 | 801 | // to contain all the values. |
michael@0 | 802 | if (this._syncedWatchExpressions && aDepth == 0) { |
michael@0 | 803 | let label = L10N.getStr("watchExpressionsScopeLabel"); |
michael@0 | 804 | let scope = DebuggerView.Variables.addScope(label); |
michael@0 | 805 | |
michael@0 | 806 | // Customize the scope for holding watch expressions evaluations. |
michael@0 | 807 | scope.descriptorTooltip = false; |
michael@0 | 808 | scope.contextMenuId = "debuggerWatchExpressionsContextMenu"; |
michael@0 | 809 | scope.separatorStr = L10N.getStr("watchExpressionsSeparatorLabel"); |
michael@0 | 810 | scope.switch = DebuggerView.WatchExpressions.switchExpression; |
michael@0 | 811 | scope.delete = DebuggerView.WatchExpressions.deleteExpression; |
michael@0 | 812 | |
michael@0 | 813 | // The evaluation hasn't thrown, so fetch and add the returned results. |
michael@0 | 814 | this._fetchWatchExpressions(scope, this._currentEvaluation.return); |
michael@0 | 815 | |
michael@0 | 816 | // The watch expressions scope is always automatically expanded. |
michael@0 | 817 | scope.expand(); |
michael@0 | 818 | } |
michael@0 | 819 | |
michael@0 | 820 | do { |
michael@0 | 821 | // Create a scope to contain all the inspected variables in the |
michael@0 | 822 | // current environment. |
michael@0 | 823 | let label = StackFrameUtils.getScopeLabel(environment); |
michael@0 | 824 | let scope = DebuggerView.Variables.addScope(label); |
michael@0 | 825 | let innermost = environment == frame.environment; |
michael@0 | 826 | |
michael@0 | 827 | // Handle special additions to the innermost scope. |
michael@0 | 828 | if (innermost) { |
michael@0 | 829 | this._insertScopeFrameReferences(scope, frame); |
michael@0 | 830 | } |
michael@0 | 831 | |
michael@0 | 832 | // Handle the expansion of the scope, lazily populating it with the |
michael@0 | 833 | // variables in the current environment. |
michael@0 | 834 | DebuggerView.Variables.controller.addExpander(scope, environment); |
michael@0 | 835 | |
michael@0 | 836 | // The innermost scope is always automatically expanded, because it |
michael@0 | 837 | // contains the variables in the current stack frame which are likely to |
michael@0 | 838 | // be inspected. |
michael@0 | 839 | if (innermost) { |
michael@0 | 840 | scope.expand(); |
michael@0 | 841 | } |
michael@0 | 842 | } while ((environment = environment.parent)); |
michael@0 | 843 | |
michael@0 | 844 | // Signal that scope environments have been shown. |
michael@0 | 845 | window.emit(EVENTS.FETCHED_SCOPES); |
michael@0 | 846 | }, |
michael@0 | 847 | |
michael@0 | 848 | /** |
michael@0 | 849 | * Loads more stack frames from the debugger server cache. |
michael@0 | 850 | */ |
michael@0 | 851 | addMoreFrames: function() { |
michael@0 | 852 | this.activeThread.fillFrames( |
michael@0 | 853 | this.activeThread.cachedFrames.length + CALL_STACK_PAGE_SIZE); |
michael@0 | 854 | }, |
michael@0 | 855 | |
michael@0 | 856 | /** |
michael@0 | 857 | * Evaluate an expression in the context of the selected frame. |
michael@0 | 858 | * |
michael@0 | 859 | * @param string aExpression |
michael@0 | 860 | * The expression to evaluate. |
michael@0 | 861 | * @param object aOptions [optional] |
michael@0 | 862 | * Additional options for this client evaluation: |
michael@0 | 863 | * - depth: the frame depth used for evaluation, 0 being the topmost. |
michael@0 | 864 | * - meta: some meta-description for what this evaluation represents. |
michael@0 | 865 | * @return object |
michael@0 | 866 | * A promise that is resolved when the evaluation finishes, |
michael@0 | 867 | * or rejected if there was no stack frame available or some |
michael@0 | 868 | * other error occurred. |
michael@0 | 869 | */ |
michael@0 | 870 | evaluate: function(aExpression, aOptions = {}) { |
michael@0 | 871 | let depth = "depth" in aOptions ? aOptions.depth : this.currentFrameDepth; |
michael@0 | 872 | let frame = this.activeThread.cachedFrames[depth]; |
michael@0 | 873 | if (frame == null) { |
michael@0 | 874 | return promise.reject(new Error("No stack frame available.")); |
michael@0 | 875 | } |
michael@0 | 876 | |
michael@0 | 877 | let deferred = promise.defer(); |
michael@0 | 878 | |
michael@0 | 879 | this.activeThread.addOneTimeListener("paused", (aEvent, aPacket) => { |
michael@0 | 880 | let { type, frameFinished } = aPacket.why; |
michael@0 | 881 | if (type == "clientEvaluated") { |
michael@0 | 882 | if (!("terminated" in frameFinished)) { |
michael@0 | 883 | deferred.resolve(frameFinished); |
michael@0 | 884 | } else { |
michael@0 | 885 | deferred.reject(new Error("The execution was abruptly terminated.")); |
michael@0 | 886 | } |
michael@0 | 887 | } else { |
michael@0 | 888 | deferred.reject(new Error("Active thread paused unexpectedly.")); |
michael@0 | 889 | } |
michael@0 | 890 | }); |
michael@0 | 891 | |
michael@0 | 892 | let meta = "meta" in aOptions ? aOptions.meta : FRAME_TYPE.PUBLIC_CLIENT_EVAL; |
michael@0 | 893 | this._currentFrameDescription = meta; |
michael@0 | 894 | this.activeThread.eval(frame.actor, aExpression); |
michael@0 | 895 | |
michael@0 | 896 | return deferred.promise; |
michael@0 | 897 | }, |
michael@0 | 898 | |
michael@0 | 899 | /** |
michael@0 | 900 | * Add nodes for special frame references in the innermost scope. |
michael@0 | 901 | * |
michael@0 | 902 | * @param Scope aScope |
michael@0 | 903 | * The scope where the references will be placed into. |
michael@0 | 904 | * @param object aFrame |
michael@0 | 905 | * The frame to get some references from. |
michael@0 | 906 | */ |
michael@0 | 907 | _insertScopeFrameReferences: function(aScope, aFrame) { |
michael@0 | 908 | // Add any thrown exception. |
michael@0 | 909 | if (this._currentException) { |
michael@0 | 910 | let excRef = aScope.addItem("<exception>", { value: this._currentException }); |
michael@0 | 911 | DebuggerView.Variables.controller.addExpander(excRef, this._currentException); |
michael@0 | 912 | } |
michael@0 | 913 | // Add any returned value. |
michael@0 | 914 | if (this._currentReturnedValue) { |
michael@0 | 915 | let retRef = aScope.addItem("<return>", { value: this._currentReturnedValue }); |
michael@0 | 916 | DebuggerView.Variables.controller.addExpander(retRef, this._currentReturnedValue); |
michael@0 | 917 | } |
michael@0 | 918 | // Add "this". |
michael@0 | 919 | if (aFrame.this) { |
michael@0 | 920 | let thisRef = aScope.addItem("this", { value: aFrame.this }); |
michael@0 | 921 | DebuggerView.Variables.controller.addExpander(thisRef, aFrame.this); |
michael@0 | 922 | } |
michael@0 | 923 | }, |
michael@0 | 924 | |
michael@0 | 925 | /** |
michael@0 | 926 | * Adds the watch expressions evaluation results to a scope in the view. |
michael@0 | 927 | * |
michael@0 | 928 | * @param Scope aScope |
michael@0 | 929 | * The scope where the watch expressions will be placed into. |
michael@0 | 930 | * @param object aExp |
michael@0 | 931 | * The grip of the evaluation results. |
michael@0 | 932 | */ |
michael@0 | 933 | _fetchWatchExpressions: function(aScope, aExp) { |
michael@0 | 934 | // Fetch the expressions only once. |
michael@0 | 935 | if (aScope._fetched) { |
michael@0 | 936 | return; |
michael@0 | 937 | } |
michael@0 | 938 | aScope._fetched = true; |
michael@0 | 939 | |
michael@0 | 940 | // Add nodes for every watch expression in scope. |
michael@0 | 941 | this.activeThread.pauseGrip(aExp).getPrototypeAndProperties(aResponse => { |
michael@0 | 942 | let ownProperties = aResponse.ownProperties; |
michael@0 | 943 | let totalExpressions = DebuggerView.WatchExpressions.itemCount; |
michael@0 | 944 | |
michael@0 | 945 | for (let i = 0; i < totalExpressions; i++) { |
michael@0 | 946 | let name = DebuggerView.WatchExpressions.getString(i); |
michael@0 | 947 | let expVal = ownProperties[i].value; |
michael@0 | 948 | let expRef = aScope.addItem(name, ownProperties[i]); |
michael@0 | 949 | DebuggerView.Variables.controller.addExpander(expRef, expVal); |
michael@0 | 950 | |
michael@0 | 951 | // Revert some of the custom watch expressions scope presentation flags, |
michael@0 | 952 | // so that they don't propagate to child items. |
michael@0 | 953 | expRef.switch = null; |
michael@0 | 954 | expRef.delete = null; |
michael@0 | 955 | expRef.descriptorTooltip = true; |
michael@0 | 956 | expRef.separatorStr = L10N.getStr("variablesSeparatorLabel"); |
michael@0 | 957 | } |
michael@0 | 958 | |
michael@0 | 959 | // Signal that watch expressions have been fetched. |
michael@0 | 960 | window.emit(EVENTS.FETCHED_WATCH_EXPRESSIONS); |
michael@0 | 961 | }); |
michael@0 | 962 | }, |
michael@0 | 963 | |
michael@0 | 964 | /** |
michael@0 | 965 | * Updates a list of watch expressions to evaluate on each pause. |
michael@0 | 966 | * TODO: handle all of this server-side: Bug 832470, comment 14. |
michael@0 | 967 | */ |
michael@0 | 968 | syncWatchExpressions: function() { |
michael@0 | 969 | let list = DebuggerView.WatchExpressions.getAllStrings(); |
michael@0 | 970 | |
michael@0 | 971 | // Sanity check all watch expressions before syncing them. To avoid |
michael@0 | 972 | // having the whole watch expressions array throw because of a single |
michael@0 | 973 | // faulty expression, simply convert it to a string describing the error. |
michael@0 | 974 | // There's no other information necessary to be offered in such cases. |
michael@0 | 975 | let sanitizedExpressions = list.map(aString => { |
michael@0 | 976 | // Reflect.parse throws when it encounters a syntax error. |
michael@0 | 977 | try { |
michael@0 | 978 | Parser.reflectionAPI.parse(aString); |
michael@0 | 979 | return aString; // Watch expression can be executed safely. |
michael@0 | 980 | } catch (e) { |
michael@0 | 981 | return "\"" + e.name + ": " + e.message + "\""; // Syntax error. |
michael@0 | 982 | } |
michael@0 | 983 | }); |
michael@0 | 984 | |
michael@0 | 985 | if (sanitizedExpressions.length) { |
michael@0 | 986 | this._syncedWatchExpressions = |
michael@0 | 987 | this._currentWatchExpressions = |
michael@0 | 988 | "[" + |
michael@0 | 989 | sanitizedExpressions.map(aString => |
michael@0 | 990 | "eval(\"" + |
michael@0 | 991 | "try {" + |
michael@0 | 992 | // Make sure all quotes are escaped in the expression's syntax, |
michael@0 | 993 | // and add a newline after the statement to avoid comments |
michael@0 | 994 | // breaking the code integrity inside the eval block. |
michael@0 | 995 | aString.replace(/"/g, "\\$&") + "\" + " + "'\\n'" + " + \"" + |
michael@0 | 996 | "} catch (e) {" + |
michael@0 | 997 | "e.name + ': ' + e.message;" + // TODO: Bug 812765, 812764. |
michael@0 | 998 | "}" + |
michael@0 | 999 | "\")" |
michael@0 | 1000 | ).join(",") + |
michael@0 | 1001 | "]"; |
michael@0 | 1002 | } else { |
michael@0 | 1003 | this._syncedWatchExpressions = |
michael@0 | 1004 | this._currentWatchExpressions = null; |
michael@0 | 1005 | } |
michael@0 | 1006 | |
michael@0 | 1007 | this.currentFrameDepth = -1; |
michael@0 | 1008 | this._onFrames(); |
michael@0 | 1009 | } |
michael@0 | 1010 | }; |
michael@0 | 1011 | |
michael@0 | 1012 | /** |
michael@0 | 1013 | * Keeps the source script list up-to-date, using the thread client's |
michael@0 | 1014 | * source script cache. |
michael@0 | 1015 | */ |
michael@0 | 1016 | function SourceScripts() { |
michael@0 | 1017 | this._onNewGlobal = this._onNewGlobal.bind(this); |
michael@0 | 1018 | this._onNewSource = this._onNewSource.bind(this); |
michael@0 | 1019 | this._onSourcesAdded = this._onSourcesAdded.bind(this); |
michael@0 | 1020 | this._onBlackBoxChange = this._onBlackBoxChange.bind(this); |
michael@0 | 1021 | this._onPrettyPrintChange = this._onPrettyPrintChange.bind(this); |
michael@0 | 1022 | } |
michael@0 | 1023 | |
michael@0 | 1024 | SourceScripts.prototype = { |
michael@0 | 1025 | get activeThread() DebuggerController.activeThread, |
michael@0 | 1026 | get debuggerClient() DebuggerController.client, |
michael@0 | 1027 | _cache: new Map(), |
michael@0 | 1028 | |
michael@0 | 1029 | /** |
michael@0 | 1030 | * Connect to the current thread client. |
michael@0 | 1031 | */ |
michael@0 | 1032 | connect: function() { |
michael@0 | 1033 | dumpn("SourceScripts is connecting..."); |
michael@0 | 1034 | this.debuggerClient.addListener("newGlobal", this._onNewGlobal); |
michael@0 | 1035 | this.debuggerClient.addListener("newSource", this._onNewSource); |
michael@0 | 1036 | this.activeThread.addListener("blackboxchange", this._onBlackBoxChange); |
michael@0 | 1037 | this.activeThread.addListener("prettyprintchange", this._onPrettyPrintChange); |
michael@0 | 1038 | this.handleTabNavigation(); |
michael@0 | 1039 | }, |
michael@0 | 1040 | |
michael@0 | 1041 | /** |
michael@0 | 1042 | * Disconnect from the client. |
michael@0 | 1043 | */ |
michael@0 | 1044 | disconnect: function() { |
michael@0 | 1045 | if (!this.activeThread) { |
michael@0 | 1046 | return; |
michael@0 | 1047 | } |
michael@0 | 1048 | dumpn("SourceScripts is disconnecting..."); |
michael@0 | 1049 | this.debuggerClient.removeListener("newGlobal", this._onNewGlobal); |
michael@0 | 1050 | this.debuggerClient.removeListener("newSource", this._onNewSource); |
michael@0 | 1051 | this.activeThread.removeListener("blackboxchange", this._onBlackBoxChange); |
michael@0 | 1052 | this.activeThread.addListener("prettyprintchange", this._onPrettyPrintChange); |
michael@0 | 1053 | }, |
michael@0 | 1054 | |
michael@0 | 1055 | /** |
michael@0 | 1056 | * Clears all the cached source contents. |
michael@0 | 1057 | */ |
michael@0 | 1058 | clearCache: function() { |
michael@0 | 1059 | this._cache.clear(); |
michael@0 | 1060 | }, |
michael@0 | 1061 | |
michael@0 | 1062 | /** |
michael@0 | 1063 | * Handles any initialization on a tab navigation event issued by the client. |
michael@0 | 1064 | */ |
michael@0 | 1065 | handleTabNavigation: function() { |
michael@0 | 1066 | if (!this.activeThread) { |
michael@0 | 1067 | return; |
michael@0 | 1068 | } |
michael@0 | 1069 | dumpn("Handling tab navigation in the SourceScripts"); |
michael@0 | 1070 | |
michael@0 | 1071 | // Retrieve the list of script sources known to the server from before |
michael@0 | 1072 | // the client was ready to handle "newSource" notifications. |
michael@0 | 1073 | this.activeThread.getSources(this._onSourcesAdded); |
michael@0 | 1074 | }, |
michael@0 | 1075 | |
michael@0 | 1076 | /** |
michael@0 | 1077 | * Handler for the debugger client's unsolicited newGlobal notification. |
michael@0 | 1078 | */ |
michael@0 | 1079 | _onNewGlobal: function(aNotification, aPacket) { |
michael@0 | 1080 | // TODO: bug 806775, update the globals list using aPacket.hostAnnotations |
michael@0 | 1081 | // from bug 801084. |
michael@0 | 1082 | }, |
michael@0 | 1083 | |
michael@0 | 1084 | /** |
michael@0 | 1085 | * Handler for the debugger client's unsolicited newSource notification. |
michael@0 | 1086 | */ |
michael@0 | 1087 | _onNewSource: function(aNotification, aPacket) { |
michael@0 | 1088 | // Ignore bogus scripts, e.g. generated from 'clientEvaluate' packets. |
michael@0 | 1089 | if (NEW_SOURCE_IGNORED_URLS.indexOf(aPacket.source.url) != -1) { |
michael@0 | 1090 | return; |
michael@0 | 1091 | } |
michael@0 | 1092 | |
michael@0 | 1093 | // Add the source in the debugger view sources container. |
michael@0 | 1094 | DebuggerView.Sources.addSource(aPacket.source, { staged: false }); |
michael@0 | 1095 | |
michael@0 | 1096 | // Select this source if it's the preferred one. |
michael@0 | 1097 | let preferredValue = DebuggerView.Sources.preferredValue; |
michael@0 | 1098 | if (aPacket.source.url == preferredValue) { |
michael@0 | 1099 | DebuggerView.Sources.selectedValue = preferredValue; |
michael@0 | 1100 | } |
michael@0 | 1101 | // ..or the first entry if there's none selected yet after a while |
michael@0 | 1102 | else { |
michael@0 | 1103 | setNamedTimeout("new-source", NEW_SOURCE_DISPLAY_DELAY, () => { |
michael@0 | 1104 | // If after a certain delay the preferred source still wasn't received, |
michael@0 | 1105 | // just give up on waiting and display the first entry. |
michael@0 | 1106 | if (!DebuggerView.Sources.selectedValue) { |
michael@0 | 1107 | DebuggerView.Sources.selectedIndex = 0; |
michael@0 | 1108 | } |
michael@0 | 1109 | }); |
michael@0 | 1110 | } |
michael@0 | 1111 | |
michael@0 | 1112 | // If there are any stored breakpoints for this source, display them again, |
michael@0 | 1113 | // both in the editor and the breakpoints pane. |
michael@0 | 1114 | DebuggerController.Breakpoints.updateEditorBreakpoints(); |
michael@0 | 1115 | DebuggerController.Breakpoints.updatePaneBreakpoints(); |
michael@0 | 1116 | |
michael@0 | 1117 | // Make sure the events listeners are up to date. |
michael@0 | 1118 | if (DebuggerView.instrumentsPaneTab == "events-tab") { |
michael@0 | 1119 | DebuggerController.Breakpoints.DOM.scheduleEventListenersFetch(); |
michael@0 | 1120 | } |
michael@0 | 1121 | |
michael@0 | 1122 | // Signal that a new source has been added. |
michael@0 | 1123 | window.emit(EVENTS.NEW_SOURCE); |
michael@0 | 1124 | }, |
michael@0 | 1125 | |
michael@0 | 1126 | /** |
michael@0 | 1127 | * Callback for the debugger's active thread getSources() method. |
michael@0 | 1128 | */ |
michael@0 | 1129 | _onSourcesAdded: function(aResponse) { |
michael@0 | 1130 | if (aResponse.error) { |
michael@0 | 1131 | let msg = "Error getting sources: " + aResponse.message; |
michael@0 | 1132 | Cu.reportError(msg); |
michael@0 | 1133 | dumpn(msg); |
michael@0 | 1134 | return; |
michael@0 | 1135 | } |
michael@0 | 1136 | |
michael@0 | 1137 | if (aResponse.sources.length === 0) { |
michael@0 | 1138 | DebuggerView.Sources.emptyText = L10N.getStr("noSourcesText"); |
michael@0 | 1139 | window.emit(EVENTS.SOURCES_ADDED); |
michael@0 | 1140 | return; |
michael@0 | 1141 | } |
michael@0 | 1142 | |
michael@0 | 1143 | // Add all the sources in the debugger view sources container. |
michael@0 | 1144 | for (let source of aResponse.sources) { |
michael@0 | 1145 | // Ignore bogus scripts, e.g. generated from 'clientEvaluate' packets. |
michael@0 | 1146 | if (NEW_SOURCE_IGNORED_URLS.indexOf(source.url) == -1) { |
michael@0 | 1147 | DebuggerView.Sources.addSource(source, { staged: true }); |
michael@0 | 1148 | } |
michael@0 | 1149 | } |
michael@0 | 1150 | |
michael@0 | 1151 | // Flushes all the prepared sources into the sources container. |
michael@0 | 1152 | DebuggerView.Sources.commit({ sorted: true }); |
michael@0 | 1153 | |
michael@0 | 1154 | // Select the preferred source if it exists and was part of the response. |
michael@0 | 1155 | let preferredValue = DebuggerView.Sources.preferredValue; |
michael@0 | 1156 | if (DebuggerView.Sources.containsValue(preferredValue)) { |
michael@0 | 1157 | DebuggerView.Sources.selectedValue = preferredValue; |
michael@0 | 1158 | } |
michael@0 | 1159 | // ..or the first entry if there's no one selected yet. |
michael@0 | 1160 | else if (!DebuggerView.Sources.selectedValue) { |
michael@0 | 1161 | DebuggerView.Sources.selectedIndex = 0; |
michael@0 | 1162 | } |
michael@0 | 1163 | |
michael@0 | 1164 | // If there are any stored breakpoints for the sources, display them again, |
michael@0 | 1165 | // both in the editor and the breakpoints pane. |
michael@0 | 1166 | DebuggerController.Breakpoints.updateEditorBreakpoints(); |
michael@0 | 1167 | DebuggerController.Breakpoints.updatePaneBreakpoints(); |
michael@0 | 1168 | |
michael@0 | 1169 | // Signal that sources have been added. |
michael@0 | 1170 | window.emit(EVENTS.SOURCES_ADDED); |
michael@0 | 1171 | }, |
michael@0 | 1172 | |
michael@0 | 1173 | /** |
michael@0 | 1174 | * Handler for the debugger client's 'blackboxchange' notification. |
michael@0 | 1175 | */ |
michael@0 | 1176 | _onBlackBoxChange: function (aEvent, { url, isBlackBoxed }) { |
michael@0 | 1177 | const item = DebuggerView.Sources.getItemByValue(url); |
michael@0 | 1178 | if (item) { |
michael@0 | 1179 | if (isBlackBoxed) { |
michael@0 | 1180 | item.target.classList.add("black-boxed"); |
michael@0 | 1181 | } else { |
michael@0 | 1182 | item.target.classList.remove("black-boxed"); |
michael@0 | 1183 | } |
michael@0 | 1184 | } |
michael@0 | 1185 | DebuggerView.Sources.updateToolbarButtonsState(); |
michael@0 | 1186 | DebuggerView.maybeShowBlackBoxMessage(); |
michael@0 | 1187 | }, |
michael@0 | 1188 | |
michael@0 | 1189 | /** |
michael@0 | 1190 | * Set the black boxed status of the given source. |
michael@0 | 1191 | * |
michael@0 | 1192 | * @param Object aSource |
michael@0 | 1193 | * The source form. |
michael@0 | 1194 | * @param bool aBlackBoxFlag |
michael@0 | 1195 | * True to black box the source, false to un-black box it. |
michael@0 | 1196 | * @returns Promise |
michael@0 | 1197 | * A promize that resolves to [aSource, isBlackBoxed] or rejects to |
michael@0 | 1198 | * [aSource, error]. |
michael@0 | 1199 | */ |
michael@0 | 1200 | setBlackBoxing: function(aSource, aBlackBoxFlag) { |
michael@0 | 1201 | const sourceClient = this.activeThread.source(aSource); |
michael@0 | 1202 | const deferred = promise.defer(); |
michael@0 | 1203 | |
michael@0 | 1204 | sourceClient[aBlackBoxFlag ? "blackBox" : "unblackBox"](aPacket => { |
michael@0 | 1205 | const { error, message } = aPacket; |
michael@0 | 1206 | if (error) { |
michael@0 | 1207 | let msg = "Couldn't toggle black boxing for " + aSource.url + ": " + message; |
michael@0 | 1208 | dumpn(msg); |
michael@0 | 1209 | Cu.reportError(msg); |
michael@0 | 1210 | deferred.reject([aSource, msg]); |
michael@0 | 1211 | } else { |
michael@0 | 1212 | deferred.resolve([aSource, sourceClient.isBlackBoxed]); |
michael@0 | 1213 | } |
michael@0 | 1214 | }); |
michael@0 | 1215 | |
michael@0 | 1216 | return deferred.promise; |
michael@0 | 1217 | }, |
michael@0 | 1218 | |
michael@0 | 1219 | /** |
michael@0 | 1220 | * Toggle the pretty printing of a source's text. All subsequent calls to |
michael@0 | 1221 | * |getText| will return the pretty-toggled text. Nothing will happen for |
michael@0 | 1222 | * non-javascript files. |
michael@0 | 1223 | * |
michael@0 | 1224 | * @param Object aSource |
michael@0 | 1225 | * The source form from the RDP. |
michael@0 | 1226 | * @returns Promise |
michael@0 | 1227 | * A promise that resolves to [aSource, prettyText] or rejects to |
michael@0 | 1228 | * [aSource, error]. |
michael@0 | 1229 | */ |
michael@0 | 1230 | togglePrettyPrint: function(aSource) { |
michael@0 | 1231 | // Only attempt to pretty print JavaScript sources. |
michael@0 | 1232 | if (!SourceUtils.isJavaScript(aSource.url, aSource.contentType)) { |
michael@0 | 1233 | return promise.reject([aSource, "Can't prettify non-javascript files."]); |
michael@0 | 1234 | } |
michael@0 | 1235 | |
michael@0 | 1236 | const sourceClient = this.activeThread.source(aSource); |
michael@0 | 1237 | const wantPretty = !sourceClient.isPrettyPrinted; |
michael@0 | 1238 | |
michael@0 | 1239 | // Only use the existing promise if it is pretty printed. |
michael@0 | 1240 | let textPromise = this._cache.get(aSource.url); |
michael@0 | 1241 | if (textPromise && textPromise.pretty === wantPretty) { |
michael@0 | 1242 | return textPromise; |
michael@0 | 1243 | } |
michael@0 | 1244 | |
michael@0 | 1245 | const deferred = promise.defer(); |
michael@0 | 1246 | deferred.promise.pretty = wantPretty; |
michael@0 | 1247 | this._cache.set(aSource.url, deferred.promise); |
michael@0 | 1248 | |
michael@0 | 1249 | const afterToggle = ({ error, message, source: text, contentType }) => { |
michael@0 | 1250 | if (error) { |
michael@0 | 1251 | // Revert the rejected promise from the cache, so that the original |
michael@0 | 1252 | // source's text may be shown when the source is selected. |
michael@0 | 1253 | this._cache.set(aSource.url, textPromise); |
michael@0 | 1254 | deferred.reject([aSource, message || error]); |
michael@0 | 1255 | return; |
michael@0 | 1256 | } |
michael@0 | 1257 | deferred.resolve([aSource, text, contentType]); |
michael@0 | 1258 | }; |
michael@0 | 1259 | |
michael@0 | 1260 | if (wantPretty) { |
michael@0 | 1261 | sourceClient.prettyPrint(Prefs.editorTabSize, afterToggle); |
michael@0 | 1262 | } else { |
michael@0 | 1263 | sourceClient.disablePrettyPrint(afterToggle); |
michael@0 | 1264 | } |
michael@0 | 1265 | |
michael@0 | 1266 | return deferred.promise; |
michael@0 | 1267 | }, |
michael@0 | 1268 | |
michael@0 | 1269 | /** |
michael@0 | 1270 | * Handler for the debugger's prettyprintchange notification. |
michael@0 | 1271 | */ |
michael@0 | 1272 | _onPrettyPrintChange: function(aEvent, { url }) { |
michael@0 | 1273 | // Remove the cached source AST from the Parser, to avoid getting |
michael@0 | 1274 | // wrong locations when searching for functions. |
michael@0 | 1275 | DebuggerController.Parser.clearSource(url); |
michael@0 | 1276 | }, |
michael@0 | 1277 | |
michael@0 | 1278 | /** |
michael@0 | 1279 | * Gets a specified source's text. |
michael@0 | 1280 | * |
michael@0 | 1281 | * @param object aSource |
michael@0 | 1282 | * The source object coming from the active thread. |
michael@0 | 1283 | * @param function aOnTimeout [optional] |
michael@0 | 1284 | * Function called when the source text takes a long time to fetch, |
michael@0 | 1285 | * but not necessarily failing. Long fetch times don't cause the |
michael@0 | 1286 | * rejection of the returned promise. |
michael@0 | 1287 | * @param number aDelay [optional] |
michael@0 | 1288 | * The amount of time it takes to consider a source slow to fetch. |
michael@0 | 1289 | * If unspecified, it defaults to a predefined value. |
michael@0 | 1290 | * @return object |
michael@0 | 1291 | * A promise that is resolved after the source text has been fetched. |
michael@0 | 1292 | */ |
michael@0 | 1293 | getText: function(aSource, aOnTimeout, aDelay = FETCH_SOURCE_RESPONSE_DELAY) { |
michael@0 | 1294 | // Fetch the source text only once. |
michael@0 | 1295 | let textPromise = this._cache.get(aSource.url); |
michael@0 | 1296 | if (textPromise) { |
michael@0 | 1297 | return textPromise; |
michael@0 | 1298 | } |
michael@0 | 1299 | |
michael@0 | 1300 | let deferred = promise.defer(); |
michael@0 | 1301 | this._cache.set(aSource.url, deferred.promise); |
michael@0 | 1302 | |
michael@0 | 1303 | // If the source text takes a long time to fetch, invoke a callback. |
michael@0 | 1304 | if (aOnTimeout) { |
michael@0 | 1305 | var fetchTimeout = window.setTimeout(() => aOnTimeout(aSource), aDelay); |
michael@0 | 1306 | } |
michael@0 | 1307 | |
michael@0 | 1308 | // Get the source text from the active thread. |
michael@0 | 1309 | this.activeThread.source(aSource) |
michael@0 | 1310 | .source(({ error, message, source: text, contentType }) => { |
michael@0 | 1311 | if (aOnTimeout) { |
michael@0 | 1312 | window.clearTimeout(fetchTimeout); |
michael@0 | 1313 | } |
michael@0 | 1314 | if (error) { |
michael@0 | 1315 | deferred.reject([aSource, message || error]); |
michael@0 | 1316 | } else { |
michael@0 | 1317 | deferred.resolve([aSource, text, contentType]); |
michael@0 | 1318 | } |
michael@0 | 1319 | }); |
michael@0 | 1320 | |
michael@0 | 1321 | return deferred.promise; |
michael@0 | 1322 | }, |
michael@0 | 1323 | |
michael@0 | 1324 | /** |
michael@0 | 1325 | * Starts fetching all the sources, silently. |
michael@0 | 1326 | * |
michael@0 | 1327 | * @param array aUrls |
michael@0 | 1328 | * The urls for the sources to fetch. If fetching a source's text |
michael@0 | 1329 | * takes too long, it will be discarded. |
michael@0 | 1330 | * @return object |
michael@0 | 1331 | * A promise that is resolved after source texts have been fetched. |
michael@0 | 1332 | */ |
michael@0 | 1333 | getTextForSources: function(aUrls) { |
michael@0 | 1334 | let deferred = promise.defer(); |
michael@0 | 1335 | let pending = new Set(aUrls); |
michael@0 | 1336 | let fetched = []; |
michael@0 | 1337 | |
michael@0 | 1338 | // Can't use promise.all, because if one fetch operation is rejected, then |
michael@0 | 1339 | // everything is considered rejected, thus no other subsequent source will |
michael@0 | 1340 | // be getting fetched. We don't want that. Something like Q's allSettled |
michael@0 | 1341 | // would work like a charm here. |
michael@0 | 1342 | |
michael@0 | 1343 | // Try to fetch as many sources as possible. |
michael@0 | 1344 | for (let url of aUrls) { |
michael@0 | 1345 | let sourceItem = DebuggerView.Sources.getItemByValue(url); |
michael@0 | 1346 | let sourceForm = sourceItem.attachment.source; |
michael@0 | 1347 | this.getText(sourceForm, onTimeout).then(onFetch, onError); |
michael@0 | 1348 | } |
michael@0 | 1349 | |
michael@0 | 1350 | /* Called if fetching a source takes too long. */ |
michael@0 | 1351 | function onTimeout(aSource) { |
michael@0 | 1352 | onError([aSource]); |
michael@0 | 1353 | } |
michael@0 | 1354 | |
michael@0 | 1355 | /* Called if fetching a source finishes successfully. */ |
michael@0 | 1356 | function onFetch([aSource, aText, aContentType]) { |
michael@0 | 1357 | // If fetching the source has previously timed out, discard it this time. |
michael@0 | 1358 | if (!pending.has(aSource.url)) { |
michael@0 | 1359 | return; |
michael@0 | 1360 | } |
michael@0 | 1361 | pending.delete(aSource.url); |
michael@0 | 1362 | fetched.push([aSource.url, aText, aContentType]); |
michael@0 | 1363 | maybeFinish(); |
michael@0 | 1364 | } |
michael@0 | 1365 | |
michael@0 | 1366 | /* Called if fetching a source failed because of an error. */ |
michael@0 | 1367 | function onError([aSource, aError]) { |
michael@0 | 1368 | pending.delete(aSource.url); |
michael@0 | 1369 | maybeFinish(); |
michael@0 | 1370 | } |
michael@0 | 1371 | |
michael@0 | 1372 | /* Called every time something interesting happens while fetching sources. */ |
michael@0 | 1373 | function maybeFinish() { |
michael@0 | 1374 | if (pending.size == 0) { |
michael@0 | 1375 | // Sort the fetched sources alphabetically by their url. |
michael@0 | 1376 | deferred.resolve(fetched.sort(([aFirst], [aSecond]) => aFirst > aSecond)); |
michael@0 | 1377 | } |
michael@0 | 1378 | } |
michael@0 | 1379 | |
michael@0 | 1380 | return deferred.promise; |
michael@0 | 1381 | } |
michael@0 | 1382 | }; |
michael@0 | 1383 | |
michael@0 | 1384 | /** |
michael@0 | 1385 | * Tracer update the UI according to the messages exchanged with the tracer |
michael@0 | 1386 | * actor. |
michael@0 | 1387 | */ |
michael@0 | 1388 | function Tracer() { |
michael@0 | 1389 | this._trace = null; |
michael@0 | 1390 | this._idCounter = 0; |
michael@0 | 1391 | this.onTraces = this.onTraces.bind(this); |
michael@0 | 1392 | } |
michael@0 | 1393 | |
michael@0 | 1394 | Tracer.prototype = { |
michael@0 | 1395 | get client() { |
michael@0 | 1396 | return DebuggerController.client; |
michael@0 | 1397 | }, |
michael@0 | 1398 | |
michael@0 | 1399 | get traceClient() { |
michael@0 | 1400 | return DebuggerController.traceClient; |
michael@0 | 1401 | }, |
michael@0 | 1402 | |
michael@0 | 1403 | get tracing() { |
michael@0 | 1404 | return !!this._trace; |
michael@0 | 1405 | }, |
michael@0 | 1406 | |
michael@0 | 1407 | /** |
michael@0 | 1408 | * Hooks up the debugger controller with the tracer client. |
michael@0 | 1409 | */ |
michael@0 | 1410 | connect: function() { |
michael@0 | 1411 | this._stack = []; |
michael@0 | 1412 | this.client.addListener("traces", this.onTraces); |
michael@0 | 1413 | }, |
michael@0 | 1414 | |
michael@0 | 1415 | /** |
michael@0 | 1416 | * Disconnects the debugger controller from the tracer client. Any further |
michael@0 | 1417 | * communcation with the tracer actor will not have any effect on the UI. |
michael@0 | 1418 | */ |
michael@0 | 1419 | disconnect: function() { |
michael@0 | 1420 | this._stack = null; |
michael@0 | 1421 | this.client.removeListener("traces", this.onTraces); |
michael@0 | 1422 | }, |
michael@0 | 1423 | |
michael@0 | 1424 | /** |
michael@0 | 1425 | * Instructs the tracer actor to start tracing. |
michael@0 | 1426 | */ |
michael@0 | 1427 | startTracing: function(aCallback = () => {}) { |
michael@0 | 1428 | DebuggerView.Tracer.selectTab(); |
michael@0 | 1429 | if (this.tracing) { |
michael@0 | 1430 | return; |
michael@0 | 1431 | } |
michael@0 | 1432 | this._trace = "dbg.trace" + Math.random(); |
michael@0 | 1433 | this.traceClient.startTrace([ |
michael@0 | 1434 | "name", |
michael@0 | 1435 | "location", |
michael@0 | 1436 | "parameterNames", |
michael@0 | 1437 | "depth", |
michael@0 | 1438 | "arguments", |
michael@0 | 1439 | "return", |
michael@0 | 1440 | "throw", |
michael@0 | 1441 | "yield" |
michael@0 | 1442 | ], this._trace, (aResponse) => { |
michael@0 | 1443 | const { error } = aResponse; |
michael@0 | 1444 | if (error) { |
michael@0 | 1445 | DevToolsUtils.reportException("Tracer.prototype.startTracing", error); |
michael@0 | 1446 | this._trace = null; |
michael@0 | 1447 | } |
michael@0 | 1448 | |
michael@0 | 1449 | aCallback(aResponse); |
michael@0 | 1450 | }); |
michael@0 | 1451 | }, |
michael@0 | 1452 | |
michael@0 | 1453 | /** |
michael@0 | 1454 | * Instructs the tracer actor to stop tracing. |
michael@0 | 1455 | */ |
michael@0 | 1456 | stopTracing: function(aCallback = () => {}) { |
michael@0 | 1457 | if (!this.tracing) { |
michael@0 | 1458 | return; |
michael@0 | 1459 | } |
michael@0 | 1460 | this.traceClient.stopTrace(this._trace, aResponse => { |
michael@0 | 1461 | const { error } = aResponse; |
michael@0 | 1462 | if (error) { |
michael@0 | 1463 | DevToolsUtils.reportException("Tracer.prototype.stopTracing", error); |
michael@0 | 1464 | } |
michael@0 | 1465 | |
michael@0 | 1466 | this._trace = null; |
michael@0 | 1467 | aCallback(aResponse); |
michael@0 | 1468 | }); |
michael@0 | 1469 | }, |
michael@0 | 1470 | |
michael@0 | 1471 | onTraces: function (aEvent, { traces }) { |
michael@0 | 1472 | const tracesLength = traces.length; |
michael@0 | 1473 | let tracesToShow; |
michael@0 | 1474 | if (tracesLength > TracerView.MAX_TRACES) { |
michael@0 | 1475 | tracesToShow = traces.slice(tracesLength - TracerView.MAX_TRACES, |
michael@0 | 1476 | tracesLength); |
michael@0 | 1477 | DebuggerView.Tracer.empty(); |
michael@0 | 1478 | this._stack.splice(0, this._stack.length); |
michael@0 | 1479 | } else { |
michael@0 | 1480 | tracesToShow = traces; |
michael@0 | 1481 | } |
michael@0 | 1482 | |
michael@0 | 1483 | for (let t of tracesToShow) { |
michael@0 | 1484 | if (t.type == "enteredFrame") { |
michael@0 | 1485 | this._onCall(t); |
michael@0 | 1486 | } else { |
michael@0 | 1487 | this._onReturn(t); |
michael@0 | 1488 | } |
michael@0 | 1489 | } |
michael@0 | 1490 | |
michael@0 | 1491 | DebuggerView.Tracer.commit(); |
michael@0 | 1492 | }, |
michael@0 | 1493 | |
michael@0 | 1494 | /** |
michael@0 | 1495 | * Callback for handling a new call frame. |
michael@0 | 1496 | */ |
michael@0 | 1497 | _onCall: function({ name, location, parameterNames, depth, arguments: args }) { |
michael@0 | 1498 | const item = { |
michael@0 | 1499 | name: name, |
michael@0 | 1500 | location: location, |
michael@0 | 1501 | id: this._idCounter++ |
michael@0 | 1502 | }; |
michael@0 | 1503 | |
michael@0 | 1504 | this._stack.push(item); |
michael@0 | 1505 | DebuggerView.Tracer.addTrace({ |
michael@0 | 1506 | type: "call", |
michael@0 | 1507 | name: name, |
michael@0 | 1508 | location: location, |
michael@0 | 1509 | depth: depth, |
michael@0 | 1510 | parameterNames: parameterNames, |
michael@0 | 1511 | arguments: args, |
michael@0 | 1512 | frameId: item.id |
michael@0 | 1513 | }); |
michael@0 | 1514 | }, |
michael@0 | 1515 | |
michael@0 | 1516 | /** |
michael@0 | 1517 | * Callback for handling an exited frame. |
michael@0 | 1518 | */ |
michael@0 | 1519 | _onReturn: function(aPacket) { |
michael@0 | 1520 | if (!this._stack.length) { |
michael@0 | 1521 | return; |
michael@0 | 1522 | } |
michael@0 | 1523 | |
michael@0 | 1524 | const { name, id, location } = this._stack.pop(); |
michael@0 | 1525 | DebuggerView.Tracer.addTrace({ |
michael@0 | 1526 | type: aPacket.why, |
michael@0 | 1527 | name: name, |
michael@0 | 1528 | location: location, |
michael@0 | 1529 | depth: aPacket.depth, |
michael@0 | 1530 | frameId: id, |
michael@0 | 1531 | returnVal: aPacket.return || aPacket.throw || aPacket.yield |
michael@0 | 1532 | }); |
michael@0 | 1533 | }, |
michael@0 | 1534 | |
michael@0 | 1535 | /** |
michael@0 | 1536 | * Create an object which has the same interface as a normal object client, |
michael@0 | 1537 | * but since we already have all the information for an object that we will |
michael@0 | 1538 | * ever get (the server doesn't create actors when tracing, just firehoses |
michael@0 | 1539 | * data and forgets about it) just return the data immdiately. |
michael@0 | 1540 | * |
michael@0 | 1541 | * @param Object aObject |
michael@0 | 1542 | * The tracer object "grip" (more like a limited snapshot). |
michael@0 | 1543 | * @returns Object |
michael@0 | 1544 | * The synchronous client object. |
michael@0 | 1545 | */ |
michael@0 | 1546 | syncGripClient: function(aObject) { |
michael@0 | 1547 | return { |
michael@0 | 1548 | get isFrozen() { return aObject.frozen; }, |
michael@0 | 1549 | get isSealed() { return aObject.sealed; }, |
michael@0 | 1550 | get isExtensible() { return aObject.extensible; }, |
michael@0 | 1551 | |
michael@0 | 1552 | get ownProperties() { return aObject.ownProperties; }, |
michael@0 | 1553 | get prototype() { return null; }, |
michael@0 | 1554 | |
michael@0 | 1555 | getParameterNames: callback => callback(aObject), |
michael@0 | 1556 | getPrototypeAndProperties: callback => callback(aObject), |
michael@0 | 1557 | getPrototype: callback => callback(aObject), |
michael@0 | 1558 | |
michael@0 | 1559 | getOwnPropertyNames: (callback) => { |
michael@0 | 1560 | callback({ |
michael@0 | 1561 | ownPropertyNames: aObject.ownProperties |
michael@0 | 1562 | ? Object.keys(aObject.ownProperties) |
michael@0 | 1563 | : [] |
michael@0 | 1564 | }); |
michael@0 | 1565 | }, |
michael@0 | 1566 | |
michael@0 | 1567 | getProperty: (property, callback) => { |
michael@0 | 1568 | callback({ |
michael@0 | 1569 | descriptor: aObject.ownProperties |
michael@0 | 1570 | ? aObject.ownProperties[property] |
michael@0 | 1571 | : null |
michael@0 | 1572 | }); |
michael@0 | 1573 | }, |
michael@0 | 1574 | |
michael@0 | 1575 | getDisplayString: callback => callback("[object " + aObject.class + "]"), |
michael@0 | 1576 | |
michael@0 | 1577 | getScope: callback => callback({ |
michael@0 | 1578 | error: "scopeNotAvailable", |
michael@0 | 1579 | message: "Cannot get scopes for traced objects" |
michael@0 | 1580 | }) |
michael@0 | 1581 | }; |
michael@0 | 1582 | }, |
michael@0 | 1583 | |
michael@0 | 1584 | /** |
michael@0 | 1585 | * Wraps object snapshots received from the tracer server so that we can |
michael@0 | 1586 | * differentiate them from long living object grips from the debugger server |
michael@0 | 1587 | * in the variables view. |
michael@0 | 1588 | * |
michael@0 | 1589 | * @param Object aObject |
michael@0 | 1590 | * The object snapshot from the tracer actor. |
michael@0 | 1591 | */ |
michael@0 | 1592 | WrappedObject: function(aObject) { |
michael@0 | 1593 | this.object = aObject; |
michael@0 | 1594 | } |
michael@0 | 1595 | }; |
michael@0 | 1596 | |
michael@0 | 1597 | /** |
michael@0 | 1598 | * Handles breaking on event listeners in the currently debugged target. |
michael@0 | 1599 | */ |
michael@0 | 1600 | function EventListeners() { |
michael@0 | 1601 | this._onEventListeners = this._onEventListeners.bind(this); |
michael@0 | 1602 | } |
michael@0 | 1603 | |
michael@0 | 1604 | EventListeners.prototype = { |
michael@0 | 1605 | /** |
michael@0 | 1606 | * A list of event names on which the debuggee will automatically pause |
michael@0 | 1607 | * when invoked. |
michael@0 | 1608 | */ |
michael@0 | 1609 | activeEventNames: [], |
michael@0 | 1610 | |
michael@0 | 1611 | /** |
michael@0 | 1612 | * Updates the list of events types with listeners that, when invoked, |
michael@0 | 1613 | * will automatically pause the debuggee. The respective events are |
michael@0 | 1614 | * retrieved from the UI. |
michael@0 | 1615 | */ |
michael@0 | 1616 | scheduleEventBreakpointsUpdate: function() { |
michael@0 | 1617 | // Make sure we're not sending a batch of closely repeated requests. |
michael@0 | 1618 | // This can easily happen when toggling all events of a certain type. |
michael@0 | 1619 | setNamedTimeout("event-breakpoints-update", 0, () => { |
michael@0 | 1620 | this.activeEventNames = DebuggerView.EventListeners.getCheckedEvents(); |
michael@0 | 1621 | gThreadClient.pauseOnDOMEvents(this.activeEventNames); |
michael@0 | 1622 | |
michael@0 | 1623 | // Notify that event breakpoints were added/removed on the server. |
michael@0 | 1624 | window.emit(EVENTS.EVENT_BREAKPOINTS_UPDATED); |
michael@0 | 1625 | }); |
michael@0 | 1626 | }, |
michael@0 | 1627 | |
michael@0 | 1628 | /** |
michael@0 | 1629 | * Fetches the currently attached event listeners from the debugee. |
michael@0 | 1630 | */ |
michael@0 | 1631 | scheduleEventListenersFetch: function() { |
michael@0 | 1632 | let getListeners = aCallback => gThreadClient.eventListeners(aResponse => { |
michael@0 | 1633 | if (aResponse.error) { |
michael@0 | 1634 | let msg = "Error getting event listeners: " + aResponse.message; |
michael@0 | 1635 | DevToolsUtils.reportException("scheduleEventListenersFetch", msg); |
michael@0 | 1636 | return; |
michael@0 | 1637 | } |
michael@0 | 1638 | |
michael@0 | 1639 | let outstandingListenersDefinitionSite = aResponse.listeners.map(aListener => { |
michael@0 | 1640 | const deferred = promise.defer(); |
michael@0 | 1641 | |
michael@0 | 1642 | gThreadClient.pauseGrip(aListener.function).getDefinitionSite(aResponse => { |
michael@0 | 1643 | if (aResponse.error) { |
michael@0 | 1644 | const msg = "Error getting function definition site: " + aResponse.message; |
michael@0 | 1645 | DevToolsUtils.reportException("scheduleEventListenersFetch", msg); |
michael@0 | 1646 | } else { |
michael@0 | 1647 | aListener.function.url = aResponse.url; |
michael@0 | 1648 | } |
michael@0 | 1649 | |
michael@0 | 1650 | deferred.resolve(aListener); |
michael@0 | 1651 | }); |
michael@0 | 1652 | |
michael@0 | 1653 | return deferred.promise; |
michael@0 | 1654 | }); |
michael@0 | 1655 | |
michael@0 | 1656 | promise.all(outstandingListenersDefinitionSite).then(aListeners => { |
michael@0 | 1657 | this._onEventListeners(aListeners); |
michael@0 | 1658 | |
michael@0 | 1659 | // Notify that event listeners were fetched and shown in the view, |
michael@0 | 1660 | // and callback to resume the active thread if necessary. |
michael@0 | 1661 | window.emit(EVENTS.EVENT_LISTENERS_FETCHED); |
michael@0 | 1662 | aCallback && aCallback(); |
michael@0 | 1663 | }); |
michael@0 | 1664 | }); |
michael@0 | 1665 | |
michael@0 | 1666 | // Make sure we're not sending a batch of closely repeated requests. |
michael@0 | 1667 | // This can easily happen whenever new sources are fetched. |
michael@0 | 1668 | setNamedTimeout("event-listeners-fetch", FETCH_EVENT_LISTENERS_DELAY, () => { |
michael@0 | 1669 | if (gThreadClient.state != "paused") { |
michael@0 | 1670 | gThreadClient.interrupt(() => getListeners(() => gThreadClient.resume())); |
michael@0 | 1671 | } else { |
michael@0 | 1672 | getListeners(); |
michael@0 | 1673 | } |
michael@0 | 1674 | }); |
michael@0 | 1675 | }, |
michael@0 | 1676 | |
michael@0 | 1677 | /** |
michael@0 | 1678 | * Callback for a debugger's successful active thread eventListeners() call. |
michael@0 | 1679 | */ |
michael@0 | 1680 | _onEventListeners: function(aListeners) { |
michael@0 | 1681 | // Add all the listeners in the debugger view event linsteners container. |
michael@0 | 1682 | for (let listener of aListeners) { |
michael@0 | 1683 | DebuggerView.EventListeners.addListener(listener, { staged: true }); |
michael@0 | 1684 | } |
michael@0 | 1685 | |
michael@0 | 1686 | // Flushes all the prepared events into the event listeners container. |
michael@0 | 1687 | DebuggerView.EventListeners.commit(); |
michael@0 | 1688 | } |
michael@0 | 1689 | }; |
michael@0 | 1690 | |
michael@0 | 1691 | /** |
michael@0 | 1692 | * Handles all the breakpoints in the current debugger. |
michael@0 | 1693 | */ |
michael@0 | 1694 | function Breakpoints() { |
michael@0 | 1695 | this._onEditorBreakpointAdd = this._onEditorBreakpointAdd.bind(this); |
michael@0 | 1696 | this._onEditorBreakpointRemove = this._onEditorBreakpointRemove.bind(this); |
michael@0 | 1697 | this.addBreakpoint = this.addBreakpoint.bind(this); |
michael@0 | 1698 | this.removeBreakpoint = this.removeBreakpoint.bind(this); |
michael@0 | 1699 | } |
michael@0 | 1700 | |
michael@0 | 1701 | Breakpoints.prototype = { |
michael@0 | 1702 | /** |
michael@0 | 1703 | * A map of breakpoint promises as tracked by the debugger frontend. |
michael@0 | 1704 | * The keys consist of a string representation of the breakpoint location. |
michael@0 | 1705 | */ |
michael@0 | 1706 | _added: new Map(), |
michael@0 | 1707 | _removing: new Map(), |
michael@0 | 1708 | _disabled: new Map(), |
michael@0 | 1709 | |
michael@0 | 1710 | /** |
michael@0 | 1711 | * Adds the source editor breakpoint handlers. |
michael@0 | 1712 | * |
michael@0 | 1713 | * @return object |
michael@0 | 1714 | * A promise that is resolved when the breakpoints finishes initializing. |
michael@0 | 1715 | */ |
michael@0 | 1716 | initialize: function() { |
michael@0 | 1717 | DebuggerView.editor.on("breakpointAdded", this._onEditorBreakpointAdd); |
michael@0 | 1718 | DebuggerView.editor.on("breakpointRemoved", this._onEditorBreakpointRemove); |
michael@0 | 1719 | |
michael@0 | 1720 | // Initialization is synchronous, for now. |
michael@0 | 1721 | return promise.resolve(null); |
michael@0 | 1722 | }, |
michael@0 | 1723 | |
michael@0 | 1724 | /** |
michael@0 | 1725 | * Removes the source editor breakpoint handlers & all the added breakpoints. |
michael@0 | 1726 | * |
michael@0 | 1727 | * @return object |
michael@0 | 1728 | * A promise that is resolved when the breakpoints finishes destroying. |
michael@0 | 1729 | */ |
michael@0 | 1730 | destroy: function() { |
michael@0 | 1731 | DebuggerView.editor.off("breakpointAdded", this._onEditorBreakpointAdd); |
michael@0 | 1732 | DebuggerView.editor.off("breakpointRemoved", this._onEditorBreakpointRemove); |
michael@0 | 1733 | |
michael@0 | 1734 | return this.removeAllBreakpoints(); |
michael@0 | 1735 | }, |
michael@0 | 1736 | |
michael@0 | 1737 | /** |
michael@0 | 1738 | * Event handler for new breakpoints that come from the editor. |
michael@0 | 1739 | * |
michael@0 | 1740 | * @param number aLine |
michael@0 | 1741 | * Line number where breakpoint was set. |
michael@0 | 1742 | */ |
michael@0 | 1743 | _onEditorBreakpointAdd: function(_, aLine) { |
michael@0 | 1744 | let url = DebuggerView.Sources.selectedValue; |
michael@0 | 1745 | let location = { url: url, line: aLine + 1 }; |
michael@0 | 1746 | |
michael@0 | 1747 | // Initialize the breakpoint, but don't update the editor, since this |
michael@0 | 1748 | // callback is invoked because a breakpoint was added in the editor itself. |
michael@0 | 1749 | this.addBreakpoint(location, { noEditorUpdate: true }).then(aBreakpointClient => { |
michael@0 | 1750 | // If the breakpoint client has an "requestedLocation" attached, then |
michael@0 | 1751 | // the original requested placement for the breakpoint wasn't accepted. |
michael@0 | 1752 | // In this case, we need to update the editor with the new location. |
michael@0 | 1753 | if (aBreakpointClient.requestedLocation) { |
michael@0 | 1754 | DebuggerView.editor.removeBreakpoint(aBreakpointClient.requestedLocation.line - 1); |
michael@0 | 1755 | DebuggerView.editor.addBreakpoint(aBreakpointClient.location.line - 1); |
michael@0 | 1756 | } |
michael@0 | 1757 | // Notify that we've shown a breakpoint in the source editor. |
michael@0 | 1758 | window.emit(EVENTS.BREAKPOINT_SHOWN); |
michael@0 | 1759 | }); |
michael@0 | 1760 | }, |
michael@0 | 1761 | |
michael@0 | 1762 | /** |
michael@0 | 1763 | * Event handler for breakpoints that are removed from the editor. |
michael@0 | 1764 | * |
michael@0 | 1765 | * @param number aLine |
michael@0 | 1766 | * Line number where breakpoint was removed. |
michael@0 | 1767 | */ |
michael@0 | 1768 | _onEditorBreakpointRemove: function(_, aLine) { |
michael@0 | 1769 | let url = DebuggerView.Sources.selectedValue; |
michael@0 | 1770 | let location = { url: url, line: aLine + 1 }; |
michael@0 | 1771 | |
michael@0 | 1772 | // Destroy the breakpoint, but don't update the editor, since this callback |
michael@0 | 1773 | // is invoked because a breakpoint was removed from the editor itself. |
michael@0 | 1774 | this.removeBreakpoint(location, { noEditorUpdate: true }).then(() => { |
michael@0 | 1775 | // Notify that we've hidden a breakpoint in the source editor. |
michael@0 | 1776 | window.emit(EVENTS.BREAKPOINT_HIDDEN); |
michael@0 | 1777 | }); |
michael@0 | 1778 | }, |
michael@0 | 1779 | |
michael@0 | 1780 | /** |
michael@0 | 1781 | * Update the breakpoints in the editor view. This function takes the list of |
michael@0 | 1782 | * breakpoints in the debugger and adds them back into the editor view. |
michael@0 | 1783 | * This is invoked when the selected script is changed, or when new sources |
michael@0 | 1784 | * are received via the _onNewSource and _onSourcesAdded event listeners. |
michael@0 | 1785 | */ |
michael@0 | 1786 | updateEditorBreakpoints: function() { |
michael@0 | 1787 | for (let breakpointPromise of this._addedOrDisabled) { |
michael@0 | 1788 | breakpointPromise.then(aBreakpointClient => { |
michael@0 | 1789 | let currentSourceUrl = DebuggerView.Sources.selectedValue; |
michael@0 | 1790 | let breakpointUrl = aBreakpointClient.location.url; |
michael@0 | 1791 | |
michael@0 | 1792 | // Update the view only if the breakpoint is in the currently shown source. |
michael@0 | 1793 | if (currentSourceUrl == breakpointUrl) { |
michael@0 | 1794 | this._showBreakpoint(aBreakpointClient, { noPaneUpdate: true }); |
michael@0 | 1795 | } |
michael@0 | 1796 | }); |
michael@0 | 1797 | } |
michael@0 | 1798 | }, |
michael@0 | 1799 | |
michael@0 | 1800 | /** |
michael@0 | 1801 | * Update the breakpoints in the pane view. This function takes the list of |
michael@0 | 1802 | * breakpoints in the debugger and adds them back into the breakpoints pane. |
michael@0 | 1803 | * This is invoked when new sources are received via the _onNewSource and |
michael@0 | 1804 | * _onSourcesAdded event listeners. |
michael@0 | 1805 | */ |
michael@0 | 1806 | updatePaneBreakpoints: function() { |
michael@0 | 1807 | for (let breakpointPromise of this._addedOrDisabled) { |
michael@0 | 1808 | breakpointPromise.then(aBreakpointClient => { |
michael@0 | 1809 | let container = DebuggerView.Sources; |
michael@0 | 1810 | let breakpointUrl = aBreakpointClient.location.url; |
michael@0 | 1811 | |
michael@0 | 1812 | // Update the view only if the breakpoint exists in a known source. |
michael@0 | 1813 | if (container.containsValue(breakpointUrl)) { |
michael@0 | 1814 | this._showBreakpoint(aBreakpointClient, { noEditorUpdate: true }); |
michael@0 | 1815 | } |
michael@0 | 1816 | }); |
michael@0 | 1817 | } |
michael@0 | 1818 | }, |
michael@0 | 1819 | |
michael@0 | 1820 | /** |
michael@0 | 1821 | * Add a breakpoint. |
michael@0 | 1822 | * |
michael@0 | 1823 | * @param object aLocation |
michael@0 | 1824 | * The location where you want the breakpoint. |
michael@0 | 1825 | * This object must have two properties: |
michael@0 | 1826 | * - url: the breakpoint's source location. |
michael@0 | 1827 | * - line: the breakpoint's line number. |
michael@0 | 1828 | * It can also have the following optional properties: |
michael@0 | 1829 | * - condition: only pause if this condition evaluates truthy |
michael@0 | 1830 | * @param object aOptions [optional] |
michael@0 | 1831 | * Additional options or flags supported by this operation: |
michael@0 | 1832 | * - openPopup: tells if the expression popup should be shown. |
michael@0 | 1833 | * - noEditorUpdate: tells if you want to skip editor updates. |
michael@0 | 1834 | * - noPaneUpdate: tells if you want to skip breakpoint pane updates. |
michael@0 | 1835 | * @return object |
michael@0 | 1836 | * A promise that is resolved after the breakpoint is added, or |
michael@0 | 1837 | * rejected if there was an error. |
michael@0 | 1838 | */ |
michael@0 | 1839 | addBreakpoint: Task.async(function*(aLocation, aOptions = {}) { |
michael@0 | 1840 | // Make sure a proper location is available. |
michael@0 | 1841 | if (!aLocation) { |
michael@0 | 1842 | throw new Error("Invalid breakpoint location."); |
michael@0 | 1843 | } |
michael@0 | 1844 | let addedPromise, removingPromise; |
michael@0 | 1845 | |
michael@0 | 1846 | // If the breakpoint was already added, or is currently being added at the |
michael@0 | 1847 | // specified location, then return that promise immediately. |
michael@0 | 1848 | if ((addedPromise = this._getAdded(aLocation))) { |
michael@0 | 1849 | return addedPromise; |
michael@0 | 1850 | } |
michael@0 | 1851 | |
michael@0 | 1852 | // If the breakpoint is currently being removed from the specified location, |
michael@0 | 1853 | // then wait for that to finish. |
michael@0 | 1854 | if ((removingPromise = this._getRemoving(aLocation))) { |
michael@0 | 1855 | yield removingPromise; |
michael@0 | 1856 | } |
michael@0 | 1857 | |
michael@0 | 1858 | let deferred = promise.defer(); |
michael@0 | 1859 | |
michael@0 | 1860 | // Remember the breakpoint initialization promise in the store. |
michael@0 | 1861 | let identifier = this.getIdentifier(aLocation); |
michael@0 | 1862 | this._added.set(identifier, deferred.promise); |
michael@0 | 1863 | |
michael@0 | 1864 | // Try adding the breakpoint. |
michael@0 | 1865 | gThreadClient.setBreakpoint(aLocation, Task.async(function*(aResponse, aBreakpointClient) { |
michael@0 | 1866 | // If the breakpoint response has an "actualLocation" attached, then |
michael@0 | 1867 | // the original requested placement for the breakpoint wasn't accepted. |
michael@0 | 1868 | if (aResponse.actualLocation) { |
michael@0 | 1869 | // Remember the initialization promise for the new location instead. |
michael@0 | 1870 | let oldIdentifier = identifier; |
michael@0 | 1871 | let newIdentifier = identifier = this.getIdentifier(aResponse.actualLocation); |
michael@0 | 1872 | this._added.delete(oldIdentifier); |
michael@0 | 1873 | this._added.set(newIdentifier, deferred.promise); |
michael@0 | 1874 | } |
michael@0 | 1875 | |
michael@0 | 1876 | // By default, new breakpoints are always enabled. Disabled breakpoints |
michael@0 | 1877 | // are, in fact, removed from the server but preserved in the frontend, |
michael@0 | 1878 | // so that they may not be forgotten across target navigations. |
michael@0 | 1879 | let disabledPromise = this._disabled.get(identifier); |
michael@0 | 1880 | if (disabledPromise) { |
michael@0 | 1881 | let aPrevBreakpointClient = yield disabledPromise; |
michael@0 | 1882 | let condition = aPrevBreakpointClient.getCondition(); |
michael@0 | 1883 | this._disabled.delete(identifier); |
michael@0 | 1884 | |
michael@0 | 1885 | if (condition) { |
michael@0 | 1886 | aBreakpointClient = yield aBreakpointClient.setCondition( |
michael@0 | 1887 | gThreadClient, |
michael@0 | 1888 | condition |
michael@0 | 1889 | ); |
michael@0 | 1890 | } |
michael@0 | 1891 | } |
michael@0 | 1892 | |
michael@0 | 1893 | if (aResponse.actualLocation) { |
michael@0 | 1894 | // Store the originally requested location in case it's ever needed |
michael@0 | 1895 | // and update the breakpoint client with the actual location. |
michael@0 | 1896 | aBreakpointClient.requestedLocation = aLocation; |
michael@0 | 1897 | aBreakpointClient.location = aResponse.actualLocation; |
michael@0 | 1898 | } |
michael@0 | 1899 | |
michael@0 | 1900 | // Preserve information about the breakpoint's line text, to display it |
michael@0 | 1901 | // in the sources pane without requiring fetching the source (for example, |
michael@0 | 1902 | // after the target navigated). Note that this will get out of sync |
michael@0 | 1903 | // if the source text contents change. |
michael@0 | 1904 | let line = aBreakpointClient.location.line - 1; |
michael@0 | 1905 | aBreakpointClient.text = DebuggerView.editor.getText(line).trim(); |
michael@0 | 1906 | |
michael@0 | 1907 | // Show the breakpoint in the editor and breakpoints pane, and resolve. |
michael@0 | 1908 | this._showBreakpoint(aBreakpointClient, aOptions); |
michael@0 | 1909 | |
michael@0 | 1910 | // Notify that we've added a breakpoint. |
michael@0 | 1911 | window.emit(EVENTS.BREAKPOINT_ADDED, aBreakpointClient); |
michael@0 | 1912 | deferred.resolve(aBreakpointClient); |
michael@0 | 1913 | }.bind(this))); |
michael@0 | 1914 | |
michael@0 | 1915 | return deferred.promise; |
michael@0 | 1916 | }), |
michael@0 | 1917 | |
michael@0 | 1918 | /** |
michael@0 | 1919 | * Remove a breakpoint. |
michael@0 | 1920 | * |
michael@0 | 1921 | * @param object aLocation |
michael@0 | 1922 | * @see DebuggerController.Breakpoints.addBreakpoint |
michael@0 | 1923 | * @param object aOptions [optional] |
michael@0 | 1924 | * @see DebuggerController.Breakpoints.addBreakpoint |
michael@0 | 1925 | * @return object |
michael@0 | 1926 | * A promise that is resolved after the breakpoint is removed, or |
michael@0 | 1927 | * rejected if there was an error. |
michael@0 | 1928 | */ |
michael@0 | 1929 | removeBreakpoint: function(aLocation, aOptions = {}) { |
michael@0 | 1930 | // Make sure a proper location is available. |
michael@0 | 1931 | if (!aLocation) { |
michael@0 | 1932 | return promise.reject(new Error("Invalid breakpoint location.")); |
michael@0 | 1933 | } |
michael@0 | 1934 | |
michael@0 | 1935 | // If the breakpoint was already removed, or has never even been added, |
michael@0 | 1936 | // then return a resolved promise immediately. |
michael@0 | 1937 | let addedPromise = this._getAdded(aLocation); |
michael@0 | 1938 | if (!addedPromise) { |
michael@0 | 1939 | return promise.resolve(aLocation); |
michael@0 | 1940 | } |
michael@0 | 1941 | |
michael@0 | 1942 | // If the breakpoint is currently being removed from the specified location, |
michael@0 | 1943 | // then return that promise immediately. |
michael@0 | 1944 | let removingPromise = this._getRemoving(aLocation); |
michael@0 | 1945 | if (removingPromise) { |
michael@0 | 1946 | return removingPromise; |
michael@0 | 1947 | } |
michael@0 | 1948 | |
michael@0 | 1949 | let deferred = promise.defer(); |
michael@0 | 1950 | |
michael@0 | 1951 | // Remember the breakpoint removal promise in the store. |
michael@0 | 1952 | let identifier = this.getIdentifier(aLocation); |
michael@0 | 1953 | this._removing.set(identifier, deferred.promise); |
michael@0 | 1954 | |
michael@0 | 1955 | // Retrieve the corresponding breakpoint client first. |
michael@0 | 1956 | addedPromise.then(aBreakpointClient => { |
michael@0 | 1957 | // Try removing the breakpoint. |
michael@0 | 1958 | aBreakpointClient.remove(aResponse => { |
michael@0 | 1959 | // If there was an error removing the breakpoint, reject the promise |
michael@0 | 1960 | // and forget about it that the breakpoint may be re-removed later. |
michael@0 | 1961 | if (aResponse.error) { |
michael@0 | 1962 | deferred.reject(aResponse); |
michael@0 | 1963 | return void this._removing.delete(identifier); |
michael@0 | 1964 | } |
michael@0 | 1965 | |
michael@0 | 1966 | // When a breakpoint is removed, the frontend may wish to preserve some |
michael@0 | 1967 | // details about it, so that it can be easily re-added later. In such |
michael@0 | 1968 | // cases, breakpoints are marked and stored as disabled, so that they |
michael@0 | 1969 | // may not be forgotten across target navigations. |
michael@0 | 1970 | if (aOptions.rememberDisabled) { |
michael@0 | 1971 | aBreakpointClient.disabled = true; |
michael@0 | 1972 | this._disabled.set(identifier, promise.resolve(aBreakpointClient)); |
michael@0 | 1973 | } |
michael@0 | 1974 | |
michael@0 | 1975 | // Forget both the initialization and removal promises from the store. |
michael@0 | 1976 | this._added.delete(identifier); |
michael@0 | 1977 | this._removing.delete(identifier); |
michael@0 | 1978 | |
michael@0 | 1979 | // Hide the breakpoint from the editor and breakpoints pane, and resolve. |
michael@0 | 1980 | this._hideBreakpoint(aLocation, aOptions); |
michael@0 | 1981 | |
michael@0 | 1982 | // Notify that we've removed a breakpoint. |
michael@0 | 1983 | window.emit(EVENTS.BREAKPOINT_REMOVED, aLocation); |
michael@0 | 1984 | deferred.resolve(aLocation); |
michael@0 | 1985 | }); |
michael@0 | 1986 | }); |
michael@0 | 1987 | |
michael@0 | 1988 | return deferred.promise; |
michael@0 | 1989 | }, |
michael@0 | 1990 | |
michael@0 | 1991 | /** |
michael@0 | 1992 | * Removes all the currently enabled breakpoints. |
michael@0 | 1993 | * |
michael@0 | 1994 | * @return object |
michael@0 | 1995 | * A promise that is resolved after all breakpoints are removed, or |
michael@0 | 1996 | * rejected if there was an error. |
michael@0 | 1997 | */ |
michael@0 | 1998 | removeAllBreakpoints: function() { |
michael@0 | 1999 | /* Gets an array of all the existing breakpoints promises. */ |
michael@0 | 2000 | let getActiveBreakpoints = (aPromises, aStore = []) => { |
michael@0 | 2001 | for (let [, breakpointPromise] of aPromises) { |
michael@0 | 2002 | aStore.push(breakpointPromise); |
michael@0 | 2003 | } |
michael@0 | 2004 | return aStore; |
michael@0 | 2005 | } |
michael@0 | 2006 | |
michael@0 | 2007 | /* Gets an array of all the removed breakpoints promises. */ |
michael@0 | 2008 | let getRemovedBreakpoints = (aClients, aStore = []) => { |
michael@0 | 2009 | for (let breakpointClient of aClients) { |
michael@0 | 2010 | aStore.push(this.removeBreakpoint(breakpointClient.location)); |
michael@0 | 2011 | } |
michael@0 | 2012 | return aStore; |
michael@0 | 2013 | } |
michael@0 | 2014 | |
michael@0 | 2015 | // First, populate an array of all the currently added breakpoints promises. |
michael@0 | 2016 | // Then, once all the breakpoints clients are retrieved, populate an array |
michael@0 | 2017 | // of all the removed breakpoints promises and wait for their fulfillment. |
michael@0 | 2018 | return promise.all(getActiveBreakpoints(this._added)).then(aBreakpointClients => { |
michael@0 | 2019 | return promise.all(getRemovedBreakpoints(aBreakpointClients)); |
michael@0 | 2020 | }); |
michael@0 | 2021 | }, |
michael@0 | 2022 | |
michael@0 | 2023 | /** |
michael@0 | 2024 | * Update the condition of a breakpoint. |
michael@0 | 2025 | * |
michael@0 | 2026 | * @param object aLocation |
michael@0 | 2027 | * @see DebuggerController.Breakpoints.addBreakpoint |
michael@0 | 2028 | * @param string aClients |
michael@0 | 2029 | * The condition to set on the breakpoint |
michael@0 | 2030 | * @return object |
michael@0 | 2031 | * A promise that will be resolved with the breakpoint client |
michael@0 | 2032 | */ |
michael@0 | 2033 | updateCondition: function(aLocation, aCondition) { |
michael@0 | 2034 | let addedPromise = this._getAdded(aLocation); |
michael@0 | 2035 | if (!addedPromise) { |
michael@0 | 2036 | return promise.reject(new Error('breakpoint does not exist ' + |
michael@0 | 2037 | 'in specified location')); |
michael@0 | 2038 | } |
michael@0 | 2039 | |
michael@0 | 2040 | var promise = addedPromise.then(aBreakpointClient => { |
michael@0 | 2041 | return aBreakpointClient.setCondition(gThreadClient, aCondition); |
michael@0 | 2042 | }); |
michael@0 | 2043 | |
michael@0 | 2044 | // `setCondition` returns a new breakpoint that has the condition, |
michael@0 | 2045 | // so we need to update the store |
michael@0 | 2046 | this._added.set(this.getIdentifier(aLocation), promise); |
michael@0 | 2047 | return promise; |
michael@0 | 2048 | }, |
michael@0 | 2049 | |
michael@0 | 2050 | /** |
michael@0 | 2051 | * Update the editor and breakpoints pane to show a specified breakpoint. |
michael@0 | 2052 | * |
michael@0 | 2053 | * @param object aBreakpointData |
michael@0 | 2054 | * Information about the breakpoint to be shown. |
michael@0 | 2055 | * This object must have the following properties: |
michael@0 | 2056 | * - location: the breakpoint's source location and line number |
michael@0 | 2057 | * - disabled: the breakpoint's disabled state, boolean |
michael@0 | 2058 | * - text: the breakpoint's line text to be displayed |
michael@0 | 2059 | * @param object aOptions [optional] |
michael@0 | 2060 | * @see DebuggerController.Breakpoints.addBreakpoint |
michael@0 | 2061 | */ |
michael@0 | 2062 | _showBreakpoint: function(aBreakpointData, aOptions = {}) { |
michael@0 | 2063 | let currentSourceUrl = DebuggerView.Sources.selectedValue; |
michael@0 | 2064 | let location = aBreakpointData.location; |
michael@0 | 2065 | |
michael@0 | 2066 | // Update the editor if required. |
michael@0 | 2067 | if (!aOptions.noEditorUpdate && !aBreakpointData.disabled) { |
michael@0 | 2068 | if (location.url == currentSourceUrl) { |
michael@0 | 2069 | DebuggerView.editor.addBreakpoint(location.line - 1); |
michael@0 | 2070 | } |
michael@0 | 2071 | } |
michael@0 | 2072 | |
michael@0 | 2073 | // Update the breakpoints pane if required. |
michael@0 | 2074 | if (!aOptions.noPaneUpdate) { |
michael@0 | 2075 | DebuggerView.Sources.addBreakpoint(aBreakpointData, aOptions); |
michael@0 | 2076 | } |
michael@0 | 2077 | }, |
michael@0 | 2078 | |
michael@0 | 2079 | /** |
michael@0 | 2080 | * Update the editor and breakpoints pane to hide a specified breakpoint. |
michael@0 | 2081 | * |
michael@0 | 2082 | * @param object aLocation |
michael@0 | 2083 | * @see DebuggerController.Breakpoints.addBreakpoint |
michael@0 | 2084 | * @param object aOptions [optional] |
michael@0 | 2085 | * @see DebuggerController.Breakpoints.addBreakpoint |
michael@0 | 2086 | */ |
michael@0 | 2087 | _hideBreakpoint: function(aLocation, aOptions = {}) { |
michael@0 | 2088 | let currentSourceUrl = DebuggerView.Sources.selectedValue; |
michael@0 | 2089 | |
michael@0 | 2090 | // Update the editor if required. |
michael@0 | 2091 | if (!aOptions.noEditorUpdate) { |
michael@0 | 2092 | if (aLocation.url == currentSourceUrl) { |
michael@0 | 2093 | DebuggerView.editor.removeBreakpoint(aLocation.line - 1); |
michael@0 | 2094 | } |
michael@0 | 2095 | } |
michael@0 | 2096 | |
michael@0 | 2097 | // Update the breakpoints pane if required. |
michael@0 | 2098 | if (!aOptions.noPaneUpdate) { |
michael@0 | 2099 | DebuggerView.Sources.removeBreakpoint(aLocation); |
michael@0 | 2100 | } |
michael@0 | 2101 | }, |
michael@0 | 2102 | |
michael@0 | 2103 | /** |
michael@0 | 2104 | * Get a Promise for the BreakpointActor client object which is already added |
michael@0 | 2105 | * or currently being added at the given location. |
michael@0 | 2106 | * |
michael@0 | 2107 | * @param object aLocation |
michael@0 | 2108 | * @see DebuggerController.Breakpoints.addBreakpoint |
michael@0 | 2109 | * @return object | null |
michael@0 | 2110 | * A promise that is resolved after the breakpoint is added, or |
michael@0 | 2111 | * null if no breakpoint was found. |
michael@0 | 2112 | */ |
michael@0 | 2113 | _getAdded: function(aLocation) { |
michael@0 | 2114 | return this._added.get(this.getIdentifier(aLocation)); |
michael@0 | 2115 | }, |
michael@0 | 2116 | |
michael@0 | 2117 | /** |
michael@0 | 2118 | * Get a Promise for the BreakpointActor client object which is currently |
michael@0 | 2119 | * being removed from the given location. |
michael@0 | 2120 | * |
michael@0 | 2121 | * @param object aLocation |
michael@0 | 2122 | * @see DebuggerController.Breakpoints.addBreakpoint |
michael@0 | 2123 | * @return object | null |
michael@0 | 2124 | * A promise that is resolved after the breakpoint is removed, or |
michael@0 | 2125 | * null if no breakpoint was found. |
michael@0 | 2126 | */ |
michael@0 | 2127 | _getRemoving: function(aLocation) { |
michael@0 | 2128 | return this._removing.get(this.getIdentifier(aLocation)); |
michael@0 | 2129 | }, |
michael@0 | 2130 | |
michael@0 | 2131 | /** |
michael@0 | 2132 | * Get an identifier string for a given location. Breakpoint promises are |
michael@0 | 2133 | * identified in the store by a string representation of their location. |
michael@0 | 2134 | * |
michael@0 | 2135 | * @param object aLocation |
michael@0 | 2136 | * The location to serialize to a string. |
michael@0 | 2137 | * @return string |
michael@0 | 2138 | * The identifier string. |
michael@0 | 2139 | */ |
michael@0 | 2140 | getIdentifier: function(aLocation) { |
michael@0 | 2141 | return aLocation.url + ":" + aLocation.line; |
michael@0 | 2142 | } |
michael@0 | 2143 | }; |
michael@0 | 2144 | |
michael@0 | 2145 | /** |
michael@0 | 2146 | * Gets all Promises for the BreakpointActor client objects that are |
michael@0 | 2147 | * either enabled (added to the server) or disabled (removed from the server, |
michael@0 | 2148 | * but for which some details are preserved). |
michael@0 | 2149 | */ |
michael@0 | 2150 | Object.defineProperty(Breakpoints.prototype, "_addedOrDisabled", { |
michael@0 | 2151 | get: function* () { |
michael@0 | 2152 | yield* this._added.values(); |
michael@0 | 2153 | yield* this._disabled.values(); |
michael@0 | 2154 | } |
michael@0 | 2155 | }); |
michael@0 | 2156 | |
michael@0 | 2157 | /** |
michael@0 | 2158 | * Localization convenience methods. |
michael@0 | 2159 | */ |
michael@0 | 2160 | let L10N = new ViewHelpers.L10N(DBG_STRINGS_URI); |
michael@0 | 2161 | |
michael@0 | 2162 | /** |
michael@0 | 2163 | * Shortcuts for accessing various debugger preferences. |
michael@0 | 2164 | */ |
michael@0 | 2165 | let Prefs = new ViewHelpers.Prefs("devtools", { |
michael@0 | 2166 | sourcesWidth: ["Int", "debugger.ui.panes-sources-width"], |
michael@0 | 2167 | instrumentsWidth: ["Int", "debugger.ui.panes-instruments-width"], |
michael@0 | 2168 | panesVisibleOnStartup: ["Bool", "debugger.ui.panes-visible-on-startup"], |
michael@0 | 2169 | variablesSortingEnabled: ["Bool", "debugger.ui.variables-sorting-enabled"], |
michael@0 | 2170 | variablesOnlyEnumVisible: ["Bool", "debugger.ui.variables-only-enum-visible"], |
michael@0 | 2171 | variablesSearchboxVisible: ["Bool", "debugger.ui.variables-searchbox-visible"], |
michael@0 | 2172 | pauseOnExceptions: ["Bool", "debugger.pause-on-exceptions"], |
michael@0 | 2173 | ignoreCaughtExceptions: ["Bool", "debugger.ignore-caught-exceptions"], |
michael@0 | 2174 | sourceMapsEnabled: ["Bool", "debugger.source-maps-enabled"], |
michael@0 | 2175 | prettyPrintEnabled: ["Bool", "debugger.pretty-print-enabled"], |
michael@0 | 2176 | autoPrettyPrint: ["Bool", "debugger.auto-pretty-print"], |
michael@0 | 2177 | tracerEnabled: ["Bool", "debugger.tracer"], |
michael@0 | 2178 | editorTabSize: ["Int", "editor.tabsize"] |
michael@0 | 2179 | }); |
michael@0 | 2180 | |
michael@0 | 2181 | /** |
michael@0 | 2182 | * Convenient way of emitting events from the panel window. |
michael@0 | 2183 | */ |
michael@0 | 2184 | EventEmitter.decorate(this); |
michael@0 | 2185 | |
michael@0 | 2186 | /** |
michael@0 | 2187 | * Preliminary setup for the DebuggerController object. |
michael@0 | 2188 | */ |
michael@0 | 2189 | DebuggerController.initialize(); |
michael@0 | 2190 | DebuggerController.Parser = new Parser(); |
michael@0 | 2191 | DebuggerController.ThreadState = new ThreadState(); |
michael@0 | 2192 | DebuggerController.StackFrames = new StackFrames(); |
michael@0 | 2193 | DebuggerController.SourceScripts = new SourceScripts(); |
michael@0 | 2194 | DebuggerController.Breakpoints = new Breakpoints(); |
michael@0 | 2195 | DebuggerController.Breakpoints.DOM = new EventListeners(); |
michael@0 | 2196 | DebuggerController.Tracer = new Tracer(); |
michael@0 | 2197 | |
michael@0 | 2198 | /** |
michael@0 | 2199 | * Export some properties to the global scope for easier access. |
michael@0 | 2200 | */ |
michael@0 | 2201 | Object.defineProperties(window, { |
michael@0 | 2202 | "gTarget": { |
michael@0 | 2203 | get: function() DebuggerController._target |
michael@0 | 2204 | }, |
michael@0 | 2205 | "gHostType": { |
michael@0 | 2206 | get: function() DebuggerView._hostType |
michael@0 | 2207 | }, |
michael@0 | 2208 | "gClient": { |
michael@0 | 2209 | get: function() DebuggerController.client |
michael@0 | 2210 | }, |
michael@0 | 2211 | "gThreadClient": { |
michael@0 | 2212 | get: function() DebuggerController.activeThread |
michael@0 | 2213 | }, |
michael@0 | 2214 | "gCallStackPageSize": { |
michael@0 | 2215 | get: function() CALL_STACK_PAGE_SIZE |
michael@0 | 2216 | } |
michael@0 | 2217 | }); |
michael@0 | 2218 | |
michael@0 | 2219 | /** |
michael@0 | 2220 | * Helper method for debugging. |
michael@0 | 2221 | * @param string |
michael@0 | 2222 | */ |
michael@0 | 2223 | function dumpn(str) { |
michael@0 | 2224 | if (wantLogging) { |
michael@0 | 2225 | dump("DBG-FRONTEND: " + str + "\n"); |
michael@0 | 2226 | } |
michael@0 | 2227 | } |
michael@0 | 2228 | |
michael@0 | 2229 | let wantLogging = Services.prefs.getBoolPref("devtools.debugger.log"); |