|
1 /* -*- Mode: javascript; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ |
|
2 /* vim: set ft=javascript ts=2 et sw=2 tw=80: */ |
|
3 /* This Source Code Form is subject to the terms of the Mozilla Public |
|
4 * License, v. 2.0. If a copy of the MPL was not distributed with this |
|
5 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ |
|
6 |
|
7 "use strict"; |
|
8 |
|
9 XPCOMUtils.defineLazyModuleGetter(this, "Task", |
|
10 "resource://gre/modules/Task.jsm"); |
|
11 |
|
12 // Used to detect minification for automatic pretty printing |
|
13 const SAMPLE_SIZE = 50; // no of lines |
|
14 const INDENT_COUNT_THRESHOLD = 5; // percentage |
|
15 const CHARACTER_LIMIT = 250; // line character limit |
|
16 |
|
17 // Maps known URLs to friendly source group names |
|
18 const KNOWN_SOURCE_GROUPS = { |
|
19 "Add-on SDK": "resource://gre/modules/commonjs/", |
|
20 }; |
|
21 |
|
22 /** |
|
23 * Functions handling the sources UI. |
|
24 */ |
|
25 function SourcesView() { |
|
26 dumpn("SourcesView was instantiated"); |
|
27 |
|
28 this.togglePrettyPrint = this.togglePrettyPrint.bind(this); |
|
29 this.toggleBlackBoxing = this.toggleBlackBoxing.bind(this); |
|
30 this.toggleBreakpoints = this.toggleBreakpoints.bind(this); |
|
31 |
|
32 this._onEditorLoad = this._onEditorLoad.bind(this); |
|
33 this._onEditorUnload = this._onEditorUnload.bind(this); |
|
34 this._onEditorCursorActivity = this._onEditorCursorActivity.bind(this); |
|
35 this._onSourceSelect = this._onSourceSelect.bind(this); |
|
36 this._onStopBlackBoxing = this._onStopBlackBoxing.bind(this); |
|
37 this._onBreakpointRemoved = this._onBreakpointRemoved.bind(this); |
|
38 this._onBreakpointClick = this._onBreakpointClick.bind(this); |
|
39 this._onBreakpointCheckboxClick = this._onBreakpointCheckboxClick.bind(this); |
|
40 this._onConditionalPopupShowing = this._onConditionalPopupShowing.bind(this); |
|
41 this._onConditionalPopupShown = this._onConditionalPopupShown.bind(this); |
|
42 this._onConditionalPopupHiding = this._onConditionalPopupHiding.bind(this); |
|
43 this._onConditionalTextboxKeyPress = this._onConditionalTextboxKeyPress.bind(this); |
|
44 |
|
45 this.updateToolbarButtonsState = this.updateToolbarButtonsState.bind(this); |
|
46 } |
|
47 |
|
48 SourcesView.prototype = Heritage.extend(WidgetMethods, { |
|
49 /** |
|
50 * Initialization function, called when the debugger is started. |
|
51 */ |
|
52 initialize: function() { |
|
53 dumpn("Initializing the SourcesView"); |
|
54 |
|
55 this.widget = new SideMenuWidget(document.getElementById("sources"), { |
|
56 showArrows: true |
|
57 }); |
|
58 |
|
59 // Sort known source groups towards the end of the list |
|
60 this.widget.groupSortPredicate = function(a, b) { |
|
61 if ((a in KNOWN_SOURCE_GROUPS) == (b in KNOWN_SOURCE_GROUPS)) { |
|
62 return a.localeCompare(b); |
|
63 } |
|
64 |
|
65 return (a in KNOWN_SOURCE_GROUPS) ? 1 : -1; |
|
66 }; |
|
67 |
|
68 this.emptyText = L10N.getStr("noSourcesText"); |
|
69 this._blackBoxCheckboxTooltip = L10N.getStr("blackBoxCheckboxTooltip"); |
|
70 |
|
71 this._commandset = document.getElementById("debuggerCommands"); |
|
72 this._popupset = document.getElementById("debuggerPopupset"); |
|
73 this._cmPopup = document.getElementById("sourceEditorContextMenu"); |
|
74 this._cbPanel = document.getElementById("conditional-breakpoint-panel"); |
|
75 this._cbTextbox = document.getElementById("conditional-breakpoint-panel-textbox"); |
|
76 this._blackBoxButton = document.getElementById("black-box"); |
|
77 this._stopBlackBoxButton = document.getElementById("black-boxed-message-button"); |
|
78 this._prettyPrintButton = document.getElementById("pretty-print"); |
|
79 this._toggleBreakpointsButton = document.getElementById("toggle-breakpoints"); |
|
80 |
|
81 if (Prefs.prettyPrintEnabled) { |
|
82 this._prettyPrintButton.removeAttribute("hidden"); |
|
83 } |
|
84 |
|
85 window.on(EVENTS.EDITOR_LOADED, this._onEditorLoad, false); |
|
86 window.on(EVENTS.EDITOR_UNLOADED, this._onEditorUnload, false); |
|
87 this.widget.addEventListener("select", this._onSourceSelect, false); |
|
88 this._stopBlackBoxButton.addEventListener("click", this._onStopBlackBoxing, false); |
|
89 this._cbPanel.addEventListener("popupshowing", this._onConditionalPopupShowing, false); |
|
90 this._cbPanel.addEventListener("popupshown", this._onConditionalPopupShown, false); |
|
91 this._cbPanel.addEventListener("popuphiding", this._onConditionalPopupHiding, false); |
|
92 this._cbTextbox.addEventListener("keypress", this._onConditionalTextboxKeyPress, false); |
|
93 |
|
94 this.autoFocusOnSelection = false; |
|
95 |
|
96 // Sort the contents by the displayed label. |
|
97 this.sortContents((aFirst, aSecond) => { |
|
98 return +(aFirst.attachment.label.toLowerCase() > |
|
99 aSecond.attachment.label.toLowerCase()); |
|
100 }); |
|
101 }, |
|
102 |
|
103 /** |
|
104 * Destruction function, called when the debugger is closed. |
|
105 */ |
|
106 destroy: function() { |
|
107 dumpn("Destroying the SourcesView"); |
|
108 |
|
109 window.off(EVENTS.EDITOR_LOADED, this._onEditorLoad, false); |
|
110 window.off(EVENTS.EDITOR_UNLOADED, this._onEditorUnload, false); |
|
111 this.widget.removeEventListener("select", this._onSourceSelect, false); |
|
112 this._stopBlackBoxButton.removeEventListener("click", this._onStopBlackBoxing, false); |
|
113 this._cbPanel.removeEventListener("popupshowing", this._onConditionalPopupShowing, false); |
|
114 this._cbPanel.removeEventListener("popupshowing", this._onConditionalPopupShown, false); |
|
115 this._cbPanel.removeEventListener("popuphiding", this._onConditionalPopupHiding, false); |
|
116 this._cbTextbox.removeEventListener("keypress", this._onConditionalTextboxKeyPress, false); |
|
117 }, |
|
118 |
|
119 /** |
|
120 * Sets the preferred location to be selected in this sources container. |
|
121 * @param string aUrl |
|
122 */ |
|
123 set preferredSource(aUrl) { |
|
124 this._preferredValue = aUrl; |
|
125 |
|
126 // Selects the element with the specified value in this sources container, |
|
127 // if already inserted. |
|
128 if (this.containsValue(aUrl)) { |
|
129 this.selectedValue = aUrl; |
|
130 } |
|
131 }, |
|
132 |
|
133 /** |
|
134 * Adds a source to this sources container. |
|
135 * |
|
136 * @param object aSource |
|
137 * The source object coming from the active thread. |
|
138 * @param object aOptions [optional] |
|
139 * Additional options for adding the source. Supported options: |
|
140 * - staged: true to stage the item to be appended later |
|
141 */ |
|
142 addSource: function(aSource, aOptions = {}) { |
|
143 let fullUrl = aSource.url; |
|
144 let url = fullUrl.split(" -> ").pop(); |
|
145 let label = aSource.addonPath ? aSource.addonPath : SourceUtils.getSourceLabel(url); |
|
146 let group = aSource.addonID ? aSource.addonID : SourceUtils.getSourceGroup(url); |
|
147 let unicodeUrl = NetworkHelper.convertToUnicode(unescape(fullUrl)); |
|
148 |
|
149 let contents = document.createElement("label"); |
|
150 contents.className = "plain dbg-source-item"; |
|
151 contents.setAttribute("value", label); |
|
152 contents.setAttribute("crop", "start"); |
|
153 contents.setAttribute("flex", "1"); |
|
154 contents.setAttribute("tooltiptext", unicodeUrl); |
|
155 |
|
156 // Append a source item to this container. |
|
157 this.push([contents, fullUrl], { |
|
158 staged: aOptions.staged, /* stage the item to be appended later? */ |
|
159 attachment: { |
|
160 label: label, |
|
161 group: group, |
|
162 checkboxState: !aSource.isBlackBoxed, |
|
163 checkboxTooltip: this._blackBoxCheckboxTooltip, |
|
164 source: aSource |
|
165 } |
|
166 }); |
|
167 }, |
|
168 |
|
169 /** |
|
170 * Adds a breakpoint to this sources container. |
|
171 * |
|
172 * @param object aBreakpointData |
|
173 * Information about the breakpoint to be shown. |
|
174 * This object must have the following properties: |
|
175 * - location: the breakpoint's source location and line number |
|
176 * - disabled: the breakpoint's disabled state, boolean |
|
177 * - text: the breakpoint's line text to be displayed |
|
178 * @param object aOptions [optional] |
|
179 * @see DebuggerController.Breakpoints.addBreakpoint |
|
180 */ |
|
181 addBreakpoint: function(aBreakpointData, aOptions = {}) { |
|
182 let { location, disabled } = aBreakpointData; |
|
183 |
|
184 // Make sure we're not duplicating anything. If a breakpoint at the |
|
185 // specified source url and line already exists, just toggle it. |
|
186 if (this.getBreakpoint(location)) { |
|
187 this[disabled ? "disableBreakpoint" : "enableBreakpoint"](location); |
|
188 return; |
|
189 } |
|
190 |
|
191 // Get the source item to which the breakpoint should be attached. |
|
192 let sourceItem = this.getItemByValue(location.url); |
|
193 |
|
194 // Create the element node and menu popup for the breakpoint item. |
|
195 let breakpointArgs = Heritage.extend(aBreakpointData, aOptions); |
|
196 let breakpointView = this._createBreakpointView.call(this, breakpointArgs); |
|
197 let contextMenu = this._createContextMenu.call(this, breakpointArgs); |
|
198 |
|
199 // Append a breakpoint child item to the corresponding source item. |
|
200 sourceItem.append(breakpointView.container, { |
|
201 attachment: Heritage.extend(breakpointArgs, { |
|
202 url: location.url, |
|
203 line: location.line, |
|
204 view: breakpointView, |
|
205 popup: contextMenu |
|
206 }), |
|
207 attributes: [ |
|
208 ["contextmenu", contextMenu.menupopupId] |
|
209 ], |
|
210 // Make sure that when the breakpoint item is removed, the corresponding |
|
211 // menupopup and commandset are also destroyed. |
|
212 finalize: this._onBreakpointRemoved |
|
213 }); |
|
214 |
|
215 // Highlight the newly appended breakpoint child item if necessary. |
|
216 if (aOptions.openPopup || !aOptions.noEditorUpdate) { |
|
217 this.highlightBreakpoint(location, aOptions); |
|
218 } |
|
219 }, |
|
220 |
|
221 /** |
|
222 * Removes a breakpoint from this sources container. |
|
223 * It does not also remove the breakpoint from the controller. Be careful. |
|
224 * |
|
225 * @param object aLocation |
|
226 * @see DebuggerController.Breakpoints.addBreakpoint |
|
227 */ |
|
228 removeBreakpoint: function(aLocation) { |
|
229 // When a parent source item is removed, all the child breakpoint items are |
|
230 // also automagically removed. |
|
231 let sourceItem = this.getItemByValue(aLocation.url); |
|
232 if (!sourceItem) { |
|
233 return; |
|
234 } |
|
235 let breakpointItem = this.getBreakpoint(aLocation); |
|
236 if (!breakpointItem) { |
|
237 return; |
|
238 } |
|
239 |
|
240 // Clear the breakpoint view. |
|
241 sourceItem.remove(breakpointItem); |
|
242 }, |
|
243 |
|
244 /** |
|
245 * Returns the breakpoint at the specified source url and line. |
|
246 * |
|
247 * @param object aLocation |
|
248 * @see DebuggerController.Breakpoints.addBreakpoint |
|
249 * @return object |
|
250 * The corresponding breakpoint item if found, null otherwise. |
|
251 */ |
|
252 getBreakpoint: function(aLocation) { |
|
253 return this.getItemForPredicate(aItem => |
|
254 aItem.attachment.url == aLocation.url && |
|
255 aItem.attachment.line == aLocation.line); |
|
256 }, |
|
257 |
|
258 /** |
|
259 * Returns all breakpoints for all sources. |
|
260 * |
|
261 * @return array |
|
262 * The breakpoints for all sources if any, an empty array otherwise. |
|
263 */ |
|
264 getAllBreakpoints: function(aStore = []) { |
|
265 return this.getOtherBreakpoints(undefined, aStore); |
|
266 }, |
|
267 |
|
268 /** |
|
269 * Returns all breakpoints which are not at the specified source url and line. |
|
270 * |
|
271 * @param object aLocation [optional] |
|
272 * @see DebuggerController.Breakpoints.addBreakpoint |
|
273 * @param array aStore [optional] |
|
274 * A list in which to store the corresponding breakpoints. |
|
275 * @return array |
|
276 * The corresponding breakpoints if found, an empty array otherwise. |
|
277 */ |
|
278 getOtherBreakpoints: function(aLocation = {}, aStore = []) { |
|
279 for (let source of this) { |
|
280 for (let breakpointItem of source) { |
|
281 let { url, line } = breakpointItem.attachment; |
|
282 if (url != aLocation.url || line != aLocation.line) { |
|
283 aStore.push(breakpointItem); |
|
284 } |
|
285 } |
|
286 } |
|
287 return aStore; |
|
288 }, |
|
289 |
|
290 /** |
|
291 * Enables a breakpoint. |
|
292 * |
|
293 * @param object aLocation |
|
294 * @see DebuggerController.Breakpoints.addBreakpoint |
|
295 * @param object aOptions [optional] |
|
296 * Additional options or flags supported by this operation: |
|
297 * - silent: pass true to not update the checkbox checked state; |
|
298 * this is usually necessary when the checked state will |
|
299 * be updated automatically (e.g: on a checkbox click). |
|
300 * @return object |
|
301 * A promise that is resolved after the breakpoint is enabled, or |
|
302 * rejected if no breakpoint was found at the specified location. |
|
303 */ |
|
304 enableBreakpoint: function(aLocation, aOptions = {}) { |
|
305 let breakpointItem = this.getBreakpoint(aLocation); |
|
306 if (!breakpointItem) { |
|
307 return promise.reject(new Error("No breakpoint found.")); |
|
308 } |
|
309 |
|
310 // Breakpoint will now be enabled. |
|
311 let attachment = breakpointItem.attachment; |
|
312 attachment.disabled = false; |
|
313 |
|
314 // Update the corresponding menu items to reflect the enabled state. |
|
315 let prefix = "bp-cMenu-"; // "breakpoints context menu" |
|
316 let identifier = DebuggerController.Breakpoints.getIdentifier(attachment); |
|
317 let enableSelfId = prefix + "enableSelf-" + identifier + "-menuitem"; |
|
318 let disableSelfId = prefix + "disableSelf-" + identifier + "-menuitem"; |
|
319 document.getElementById(enableSelfId).setAttribute("hidden", "true"); |
|
320 document.getElementById(disableSelfId).removeAttribute("hidden"); |
|
321 |
|
322 // Update the breakpoint toggle button checked state. |
|
323 this._toggleBreakpointsButton.removeAttribute("checked"); |
|
324 |
|
325 // Update the checkbox state if necessary. |
|
326 if (!aOptions.silent) { |
|
327 attachment.view.checkbox.setAttribute("checked", "true"); |
|
328 } |
|
329 |
|
330 return DebuggerController.Breakpoints.addBreakpoint(aLocation, { |
|
331 // No need to update the pane, since this method is invoked because |
|
332 // a breakpoint's view was interacted with. |
|
333 noPaneUpdate: true |
|
334 }); |
|
335 }, |
|
336 |
|
337 /** |
|
338 * Disables a breakpoint. |
|
339 * |
|
340 * @param object aLocation |
|
341 * @see DebuggerController.Breakpoints.addBreakpoint |
|
342 * @param object aOptions [optional] |
|
343 * Additional options or flags supported by this operation: |
|
344 * - silent: pass true to not update the checkbox checked state; |
|
345 * this is usually necessary when the checked state will |
|
346 * be updated automatically (e.g: on a checkbox click). |
|
347 * @return object |
|
348 * A promise that is resolved after the breakpoint is disabled, or |
|
349 * rejected if no breakpoint was found at the specified location. |
|
350 */ |
|
351 disableBreakpoint: function(aLocation, aOptions = {}) { |
|
352 let breakpointItem = this.getBreakpoint(aLocation); |
|
353 if (!breakpointItem) { |
|
354 return promise.reject(new Error("No breakpoint found.")); |
|
355 } |
|
356 |
|
357 // Breakpoint will now be disabled. |
|
358 let attachment = breakpointItem.attachment; |
|
359 attachment.disabled = true; |
|
360 |
|
361 // Update the corresponding menu items to reflect the disabled state. |
|
362 let prefix = "bp-cMenu-"; // "breakpoints context menu" |
|
363 let identifier = DebuggerController.Breakpoints.getIdentifier(attachment); |
|
364 let enableSelfId = prefix + "enableSelf-" + identifier + "-menuitem"; |
|
365 let disableSelfId = prefix + "disableSelf-" + identifier + "-menuitem"; |
|
366 document.getElementById(enableSelfId).removeAttribute("hidden"); |
|
367 document.getElementById(disableSelfId).setAttribute("hidden", "true"); |
|
368 |
|
369 // Update the checkbox state if necessary. |
|
370 if (!aOptions.silent) { |
|
371 attachment.view.checkbox.removeAttribute("checked"); |
|
372 } |
|
373 |
|
374 return DebuggerController.Breakpoints.removeBreakpoint(aLocation, { |
|
375 // No need to update this pane, since this method is invoked because |
|
376 // a breakpoint's view was interacted with. |
|
377 noPaneUpdate: true, |
|
378 // Mark this breakpoint as being "disabled", not completely removed. |
|
379 // This makes sure it will not be forgotten across target navigations. |
|
380 rememberDisabled: true |
|
381 }); |
|
382 }, |
|
383 |
|
384 /** |
|
385 * Highlights a breakpoint in this sources container. |
|
386 * |
|
387 * @param object aLocation |
|
388 * @see DebuggerController.Breakpoints.addBreakpoint |
|
389 * @param object aOptions [optional] |
|
390 * An object containing some of the following boolean properties: |
|
391 * - openPopup: tells if the expression popup should be shown. |
|
392 * - noEditorUpdate: tells if you want to skip editor updates. |
|
393 */ |
|
394 highlightBreakpoint: function(aLocation, aOptions = {}) { |
|
395 let breakpointItem = this.getBreakpoint(aLocation); |
|
396 if (!breakpointItem) { |
|
397 return; |
|
398 } |
|
399 |
|
400 // Breakpoint will now be selected. |
|
401 this._selectBreakpoint(breakpointItem); |
|
402 |
|
403 // Update the editor location if necessary. |
|
404 if (!aOptions.noEditorUpdate) { |
|
405 DebuggerView.setEditorLocation(aLocation.url, aLocation.line, { noDebug: true }); |
|
406 } |
|
407 |
|
408 // If the breakpoint requires a new conditional expression, display |
|
409 // the panel to input the corresponding expression. |
|
410 if (aOptions.openPopup) { |
|
411 this._openConditionalPopup(); |
|
412 } else { |
|
413 this._hideConditionalPopup(); |
|
414 } |
|
415 }, |
|
416 |
|
417 /** |
|
418 * Unhighlights the current breakpoint in this sources container. |
|
419 */ |
|
420 unhighlightBreakpoint: function() { |
|
421 this._unselectBreakpoint(); |
|
422 this._hideConditionalPopup(); |
|
423 }, |
|
424 |
|
425 /** |
|
426 * Update the checked/unchecked and enabled/disabled states of the buttons in |
|
427 * the sources toolbar based on the currently selected source's state. |
|
428 */ |
|
429 updateToolbarButtonsState: function() { |
|
430 const { source } = this.selectedItem.attachment; |
|
431 const sourceClient = gThreadClient.source(source); |
|
432 |
|
433 if (sourceClient.isBlackBoxed) { |
|
434 this._prettyPrintButton.setAttribute("disabled", true); |
|
435 this._blackBoxButton.setAttribute("checked", true); |
|
436 } else { |
|
437 this._prettyPrintButton.removeAttribute("disabled"); |
|
438 this._blackBoxButton.removeAttribute("checked"); |
|
439 } |
|
440 |
|
441 if (sourceClient.isPrettyPrinted) { |
|
442 this._prettyPrintButton.setAttribute("checked", true); |
|
443 } else { |
|
444 this._prettyPrintButton.removeAttribute("checked"); |
|
445 } |
|
446 }, |
|
447 |
|
448 /** |
|
449 * Toggle the pretty printing of the selected source. |
|
450 */ |
|
451 togglePrettyPrint: function() { |
|
452 if (this._prettyPrintButton.hasAttribute("disabled")) { |
|
453 return; |
|
454 } |
|
455 |
|
456 const resetEditor = ([{ url }]) => { |
|
457 // Only set the text when the source is still selected. |
|
458 if (url == this.selectedValue) { |
|
459 DebuggerView.setEditorLocation(url, 0, { force: true }); |
|
460 } |
|
461 }; |
|
462 |
|
463 const printError = ([{ url }, error]) => { |
|
464 DevToolsUtils.reportException("togglePrettyPrint", error); |
|
465 }; |
|
466 |
|
467 DebuggerView.showProgressBar(); |
|
468 const { source } = this.selectedItem.attachment; |
|
469 const sourceClient = gThreadClient.source(source); |
|
470 const shouldPrettyPrint = !sourceClient.isPrettyPrinted; |
|
471 |
|
472 if (shouldPrettyPrint) { |
|
473 this._prettyPrintButton.setAttribute("checked", true); |
|
474 } else { |
|
475 this._prettyPrintButton.removeAttribute("checked"); |
|
476 } |
|
477 |
|
478 DebuggerController.SourceScripts.togglePrettyPrint(source) |
|
479 .then(resetEditor, printError) |
|
480 .then(DebuggerView.showEditor) |
|
481 .then(this.updateToolbarButtonsState); |
|
482 }, |
|
483 |
|
484 /** |
|
485 * Toggle the black boxed state of the selected source. |
|
486 */ |
|
487 toggleBlackBoxing: function() { |
|
488 const { source } = this.selectedItem.attachment; |
|
489 const sourceClient = gThreadClient.source(source); |
|
490 const shouldBlackBox = !sourceClient.isBlackBoxed; |
|
491 |
|
492 // Be optimistic that the (un-)black boxing will succeed, so enable/disable |
|
493 // the pretty print button and check/uncheck the black box button |
|
494 // immediately. Then, once we actually get the results from the server, make |
|
495 // sure that it is in the correct state again by calling |
|
496 // `updateToolbarButtonsState`. |
|
497 |
|
498 if (shouldBlackBox) { |
|
499 this._prettyPrintButton.setAttribute("disabled", true); |
|
500 this._blackBoxButton.setAttribute("checked", true); |
|
501 } else { |
|
502 this._prettyPrintButton.removeAttribute("disabled"); |
|
503 this._blackBoxButton.removeAttribute("checked"); |
|
504 } |
|
505 |
|
506 DebuggerController.SourceScripts.setBlackBoxing(source, shouldBlackBox) |
|
507 .then(this.updateToolbarButtonsState, |
|
508 this.updateToolbarButtonsState); |
|
509 }, |
|
510 |
|
511 /** |
|
512 * Toggles all breakpoints enabled/disabled. |
|
513 */ |
|
514 toggleBreakpoints: function() { |
|
515 let breakpoints = this.getAllBreakpoints(); |
|
516 let hasBreakpoints = breakpoints.length > 0; |
|
517 let hasEnabledBreakpoints = breakpoints.some(e => !e.attachment.disabled); |
|
518 |
|
519 if (hasBreakpoints && hasEnabledBreakpoints) { |
|
520 this._toggleBreakpointsButton.setAttribute("checked", true); |
|
521 this._onDisableAll(); |
|
522 } else { |
|
523 this._toggleBreakpointsButton.removeAttribute("checked"); |
|
524 this._onEnableAll(); |
|
525 } |
|
526 }, |
|
527 |
|
528 /** |
|
529 * Marks a breakpoint as selected in this sources container. |
|
530 * |
|
531 * @param object aItem |
|
532 * The breakpoint item to select. |
|
533 */ |
|
534 _selectBreakpoint: function(aItem) { |
|
535 if (this._selectedBreakpointItem == aItem) { |
|
536 return; |
|
537 } |
|
538 this._unselectBreakpoint(); |
|
539 this._selectedBreakpointItem = aItem; |
|
540 this._selectedBreakpointItem.target.classList.add("selected"); |
|
541 |
|
542 // Ensure the currently selected breakpoint is visible. |
|
543 this.widget.ensureElementIsVisible(aItem.target); |
|
544 }, |
|
545 |
|
546 /** |
|
547 * Marks the current breakpoint as unselected in this sources container. |
|
548 */ |
|
549 _unselectBreakpoint: function() { |
|
550 if (!this._selectedBreakpointItem) { |
|
551 return; |
|
552 } |
|
553 this._selectedBreakpointItem.target.classList.remove("selected"); |
|
554 this._selectedBreakpointItem = null; |
|
555 }, |
|
556 |
|
557 /** |
|
558 * Opens a conditional breakpoint's expression input popup. |
|
559 */ |
|
560 _openConditionalPopup: function() { |
|
561 let breakpointItem = this._selectedBreakpointItem; |
|
562 let attachment = breakpointItem.attachment; |
|
563 // Check if this is an enabled conditional breakpoint, and if so, |
|
564 // retrieve the current conditional epression. |
|
565 let breakpointPromise = DebuggerController.Breakpoints._getAdded(attachment); |
|
566 if (breakpointPromise) { |
|
567 breakpointPromise.then(aBreakpointClient => { |
|
568 let isConditionalBreakpoint = aBreakpointClient.hasCondition(); |
|
569 let condition = aBreakpointClient.getCondition(); |
|
570 doOpen.call(this, isConditionalBreakpoint ? condition : "") |
|
571 }); |
|
572 } else { |
|
573 doOpen.call(this, "") |
|
574 } |
|
575 |
|
576 function doOpen(aConditionalExpression) { |
|
577 // Update the conditional expression textbox. If no expression was |
|
578 // previously set, revert to using an empty string by default. |
|
579 this._cbTextbox.value = aConditionalExpression; |
|
580 |
|
581 // Show the conditional expression panel. The popup arrow should be pointing |
|
582 // at the line number node in the breakpoint item view. |
|
583 this._cbPanel.hidden = false; |
|
584 this._cbPanel.openPopup(breakpointItem.attachment.view.lineNumber, |
|
585 BREAKPOINT_CONDITIONAL_POPUP_POSITION, |
|
586 BREAKPOINT_CONDITIONAL_POPUP_OFFSET_X, |
|
587 BREAKPOINT_CONDITIONAL_POPUP_OFFSET_Y); |
|
588 } |
|
589 }, |
|
590 |
|
591 /** |
|
592 * Hides a conditional breakpoint's expression input popup. |
|
593 */ |
|
594 _hideConditionalPopup: function() { |
|
595 this._cbPanel.hidden = true; |
|
596 |
|
597 // Sometimes this._cbPanel doesn't have hidePopup method which doesn't |
|
598 // break anything but simply outputs an exception to the console. |
|
599 if (this._cbPanel.hidePopup) { |
|
600 this._cbPanel.hidePopup(); |
|
601 } |
|
602 }, |
|
603 |
|
604 /** |
|
605 * Customization function for creating a breakpoint item's UI. |
|
606 * |
|
607 * @param object aOptions |
|
608 * A couple of options or flags supported by this operation: |
|
609 * - location: the breakpoint's source location and line number |
|
610 * - disabled: the breakpoint's disabled state, boolean |
|
611 * - text: the breakpoint's line text to be displayed |
|
612 * @return object |
|
613 * An object containing the breakpoint container, checkbox, |
|
614 * line number and line text nodes. |
|
615 */ |
|
616 _createBreakpointView: function(aOptions) { |
|
617 let { location, disabled, text } = aOptions; |
|
618 let identifier = DebuggerController.Breakpoints.getIdentifier(location); |
|
619 |
|
620 let checkbox = document.createElement("checkbox"); |
|
621 checkbox.setAttribute("checked", !disabled); |
|
622 checkbox.className = "dbg-breakpoint-checkbox"; |
|
623 |
|
624 let lineNumberNode = document.createElement("label"); |
|
625 lineNumberNode.className = "plain dbg-breakpoint-line"; |
|
626 lineNumberNode.setAttribute("value", location.line); |
|
627 |
|
628 let lineTextNode = document.createElement("label"); |
|
629 lineTextNode.className = "plain dbg-breakpoint-text"; |
|
630 lineTextNode.setAttribute("value", text); |
|
631 lineTextNode.setAttribute("crop", "end"); |
|
632 lineTextNode.setAttribute("flex", "1"); |
|
633 |
|
634 let tooltip = text.substr(0, BREAKPOINT_LINE_TOOLTIP_MAX_LENGTH); |
|
635 lineTextNode.setAttribute("tooltiptext", tooltip); |
|
636 |
|
637 let container = document.createElement("hbox"); |
|
638 container.id = "breakpoint-" + identifier; |
|
639 container.className = "dbg-breakpoint side-menu-widget-item-other"; |
|
640 container.classList.add("devtools-monospace"); |
|
641 container.setAttribute("align", "center"); |
|
642 container.setAttribute("flex", "1"); |
|
643 |
|
644 container.addEventListener("click", this._onBreakpointClick, false); |
|
645 checkbox.addEventListener("click", this._onBreakpointCheckboxClick, false); |
|
646 |
|
647 container.appendChild(checkbox); |
|
648 container.appendChild(lineNumberNode); |
|
649 container.appendChild(lineTextNode); |
|
650 |
|
651 return { |
|
652 container: container, |
|
653 checkbox: checkbox, |
|
654 lineNumber: lineNumberNode, |
|
655 lineText: lineTextNode |
|
656 }; |
|
657 }, |
|
658 |
|
659 /** |
|
660 * Creates a context menu for a breakpoint element. |
|
661 * |
|
662 * @param object aOptions |
|
663 * A couple of options or flags supported by this operation: |
|
664 * - location: the breakpoint's source location and line number |
|
665 * - disabled: the breakpoint's disabled state, boolean |
|
666 * @return object |
|
667 * An object containing the breakpoint commandset and menu popup ids. |
|
668 */ |
|
669 _createContextMenu: function(aOptions) { |
|
670 let { location, disabled } = aOptions; |
|
671 let identifier = DebuggerController.Breakpoints.getIdentifier(location); |
|
672 |
|
673 let commandset = document.createElement("commandset"); |
|
674 let menupopup = document.createElement("menupopup"); |
|
675 commandset.id = "bp-cSet-" + identifier; |
|
676 menupopup.id = "bp-mPop-" + identifier; |
|
677 |
|
678 createMenuItem.call(this, "enableSelf", !disabled); |
|
679 createMenuItem.call(this, "disableSelf", disabled); |
|
680 createMenuItem.call(this, "deleteSelf"); |
|
681 createMenuSeparator(); |
|
682 createMenuItem.call(this, "setConditional"); |
|
683 createMenuSeparator(); |
|
684 createMenuItem.call(this, "enableOthers"); |
|
685 createMenuItem.call(this, "disableOthers"); |
|
686 createMenuItem.call(this, "deleteOthers"); |
|
687 createMenuSeparator(); |
|
688 createMenuItem.call(this, "enableAll"); |
|
689 createMenuItem.call(this, "disableAll"); |
|
690 createMenuSeparator(); |
|
691 createMenuItem.call(this, "deleteAll"); |
|
692 |
|
693 this._popupset.appendChild(menupopup); |
|
694 this._commandset.appendChild(commandset); |
|
695 |
|
696 return { |
|
697 commandsetId: commandset.id, |
|
698 menupopupId: menupopup.id |
|
699 }; |
|
700 |
|
701 /** |
|
702 * Creates a menu item specified by a name with the appropriate attributes |
|
703 * (label and handler). |
|
704 * |
|
705 * @param string aName |
|
706 * A global identifier for the menu item. |
|
707 * @param boolean aHiddenFlag |
|
708 * True if this menuitem should be hidden. |
|
709 */ |
|
710 function createMenuItem(aName, aHiddenFlag) { |
|
711 let menuitem = document.createElement("menuitem"); |
|
712 let command = document.createElement("command"); |
|
713 |
|
714 let prefix = "bp-cMenu-"; // "breakpoints context menu" |
|
715 let commandId = prefix + aName + "-" + identifier + "-command"; |
|
716 let menuitemId = prefix + aName + "-" + identifier + "-menuitem"; |
|
717 |
|
718 let label = L10N.getStr("breakpointMenuItem." + aName); |
|
719 let func = "_on" + aName.charAt(0).toUpperCase() + aName.slice(1); |
|
720 |
|
721 command.id = commandId; |
|
722 command.setAttribute("label", label); |
|
723 command.addEventListener("command", () => this[func](location), false); |
|
724 |
|
725 menuitem.id = menuitemId; |
|
726 menuitem.setAttribute("command", commandId); |
|
727 aHiddenFlag && menuitem.setAttribute("hidden", "true"); |
|
728 |
|
729 commandset.appendChild(command); |
|
730 menupopup.appendChild(menuitem); |
|
731 } |
|
732 |
|
733 /** |
|
734 * Creates a simple menu separator element and appends it to the current |
|
735 * menupopup hierarchy. |
|
736 */ |
|
737 function createMenuSeparator() { |
|
738 let menuseparator = document.createElement("menuseparator"); |
|
739 menupopup.appendChild(menuseparator); |
|
740 } |
|
741 }, |
|
742 |
|
743 /** |
|
744 * Function called each time a breakpoint item is removed. |
|
745 * |
|
746 * @param object aItem |
|
747 * The corresponding item. |
|
748 */ |
|
749 _onBreakpointRemoved: function(aItem) { |
|
750 dumpn("Finalizing breakpoint item: " + aItem); |
|
751 |
|
752 // Destroy the context menu for the breakpoint. |
|
753 let contextMenu = aItem.attachment.popup; |
|
754 document.getElementById(contextMenu.commandsetId).remove(); |
|
755 document.getElementById(contextMenu.menupopupId).remove(); |
|
756 |
|
757 // Clear the breakpoint selection. |
|
758 if (this._selectedBreakpointItem == aItem) { |
|
759 this._selectedBreakpointItem = null; |
|
760 } |
|
761 }, |
|
762 |
|
763 /** |
|
764 * The load listener for the source editor. |
|
765 */ |
|
766 _onEditorLoad: function(aName, aEditor) { |
|
767 aEditor.on("cursorActivity", this._onEditorCursorActivity); |
|
768 }, |
|
769 |
|
770 /** |
|
771 * The unload listener for the source editor. |
|
772 */ |
|
773 _onEditorUnload: function(aName, aEditor) { |
|
774 aEditor.off("cursorActivity", this._onEditorCursorActivity); |
|
775 }, |
|
776 |
|
777 /** |
|
778 * The selection listener for the source editor. |
|
779 */ |
|
780 _onEditorCursorActivity: function(e) { |
|
781 let editor = DebuggerView.editor; |
|
782 let start = editor.getCursor("start").line + 1; |
|
783 let end = editor.getCursor().line + 1; |
|
784 let url = this.selectedValue; |
|
785 |
|
786 let location = { url: url, line: start }; |
|
787 |
|
788 if (this.getBreakpoint(location) && start == end) { |
|
789 this.highlightBreakpoint(location, { noEditorUpdate: true }); |
|
790 } else { |
|
791 this.unhighlightBreakpoint(); |
|
792 } |
|
793 }, |
|
794 |
|
795 /** |
|
796 * The select listener for the sources container. |
|
797 */ |
|
798 _onSourceSelect: function({ detail: sourceItem }) { |
|
799 if (!sourceItem) { |
|
800 return; |
|
801 } |
|
802 const { source } = sourceItem.attachment; |
|
803 const sourceClient = gThreadClient.source(source); |
|
804 |
|
805 // The container is not empty and an actual item was selected. |
|
806 DebuggerView.setEditorLocation(sourceItem.value); |
|
807 |
|
808 if (Prefs.autoPrettyPrint && !sourceClient.isPrettyPrinted) { |
|
809 DebuggerController.SourceScripts.getText(source).then(([, aText]) => { |
|
810 if (SourceUtils.isMinified(sourceClient, aText)) { |
|
811 this.togglePrettyPrint(); |
|
812 } |
|
813 }).then(null, e => DevToolsUtils.reportException("_onSourceSelect", e)); |
|
814 } |
|
815 |
|
816 // Set window title. No need to split the url by " -> " here, because it was |
|
817 // already sanitized when the source was added. |
|
818 document.title = L10N.getFormatStr("DebuggerWindowScriptTitle", sourceItem.value); |
|
819 |
|
820 DebuggerView.maybeShowBlackBoxMessage(); |
|
821 this.updateToolbarButtonsState(); |
|
822 }, |
|
823 |
|
824 /** |
|
825 * The click listener for the "stop black boxing" button. |
|
826 */ |
|
827 _onStopBlackBoxing: function() { |
|
828 const { source } = this.selectedItem.attachment; |
|
829 |
|
830 DebuggerController.SourceScripts.setBlackBoxing(source, false) |
|
831 .then(this.updateToolbarButtonsState, |
|
832 this.updateToolbarButtonsState); |
|
833 }, |
|
834 |
|
835 /** |
|
836 * The click listener for a breakpoint container. |
|
837 */ |
|
838 _onBreakpointClick: function(e) { |
|
839 let sourceItem = this.getItemForElement(e.target); |
|
840 let breakpointItem = this.getItemForElement.call(sourceItem, e.target); |
|
841 let attachment = breakpointItem.attachment; |
|
842 |
|
843 // Check if this is an enabled conditional breakpoint. |
|
844 let breakpointPromise = DebuggerController.Breakpoints._getAdded(attachment); |
|
845 if (breakpointPromise) { |
|
846 breakpointPromise.then(aBreakpointClient => { |
|
847 doHighlight.call(this, aBreakpointClient.hasCondition()); |
|
848 }); |
|
849 } else { |
|
850 doHighlight.call(this, false); |
|
851 } |
|
852 |
|
853 function doHighlight(aConditionalBreakpointFlag) { |
|
854 // Highlight the breakpoint in this pane and in the editor. |
|
855 this.highlightBreakpoint(attachment, { |
|
856 // Don't show the conditional expression popup if this is not a |
|
857 // conditional breakpoint, or the right mouse button was pressed (to |
|
858 // avoid clashing the popup with the context menu). |
|
859 openPopup: aConditionalBreakpointFlag && e.button == 0 |
|
860 }); |
|
861 } |
|
862 }, |
|
863 |
|
864 /** |
|
865 * The click listener for a breakpoint checkbox. |
|
866 */ |
|
867 _onBreakpointCheckboxClick: function(e) { |
|
868 let sourceItem = this.getItemForElement(e.target); |
|
869 let breakpointItem = this.getItemForElement.call(sourceItem, e.target); |
|
870 let attachment = breakpointItem.attachment; |
|
871 |
|
872 // Toggle the breakpoint enabled or disabled. |
|
873 this[attachment.disabled ? "enableBreakpoint" : "disableBreakpoint"](attachment, { |
|
874 // Do this silently (don't update the checkbox checked state), since |
|
875 // this listener is triggered because a checkbox was already clicked. |
|
876 silent: true |
|
877 }); |
|
878 |
|
879 // Don't update the editor location (avoid propagating into _onBreakpointClick). |
|
880 e.preventDefault(); |
|
881 e.stopPropagation(); |
|
882 }, |
|
883 |
|
884 /** |
|
885 * The popup showing listener for the breakpoints conditional expression panel. |
|
886 */ |
|
887 _onConditionalPopupShowing: function() { |
|
888 this._conditionalPopupVisible = true; // Used in tests. |
|
889 window.emit(EVENTS.CONDITIONAL_BREAKPOINT_POPUP_SHOWING); |
|
890 }, |
|
891 |
|
892 /** |
|
893 * The popup shown listener for the breakpoints conditional expression panel. |
|
894 */ |
|
895 _onConditionalPopupShown: function() { |
|
896 this._cbTextbox.focus(); |
|
897 this._cbTextbox.select(); |
|
898 }, |
|
899 |
|
900 /** |
|
901 * The popup hiding listener for the breakpoints conditional expression panel. |
|
902 */ |
|
903 _onConditionalPopupHiding: Task.async(function*() { |
|
904 this._conditionalPopupVisible = false; // Used in tests. |
|
905 let breakpointItem = this._selectedBreakpointItem; |
|
906 let attachment = breakpointItem.attachment; |
|
907 |
|
908 // Check if this is an enabled conditional breakpoint, and if so, |
|
909 // save the current conditional epression. |
|
910 let breakpointPromise = DebuggerController.Breakpoints._getAdded(attachment); |
|
911 if (breakpointPromise) { |
|
912 let breakpointClient = yield breakpointPromise; |
|
913 yield DebuggerController.Breakpoints.updateCondition( |
|
914 breakpointClient.location, |
|
915 this._cbTextbox.value |
|
916 ); |
|
917 } |
|
918 |
|
919 window.emit(EVENTS.CONDITIONAL_BREAKPOINT_POPUP_HIDING); |
|
920 }), |
|
921 |
|
922 /** |
|
923 * The keypress listener for the breakpoints conditional expression textbox. |
|
924 */ |
|
925 _onConditionalTextboxKeyPress: function(e) { |
|
926 if (e.keyCode == e.DOM_VK_RETURN) { |
|
927 this._hideConditionalPopup(); |
|
928 } |
|
929 }, |
|
930 |
|
931 /** |
|
932 * Called when the add breakpoint key sequence was pressed. |
|
933 */ |
|
934 _onCmdAddBreakpoint: function(e) { |
|
935 let url = DebuggerView.Sources.selectedValue; |
|
936 let line = DebuggerView.editor.getCursor().line + 1; |
|
937 let location = { url: url, line: line }; |
|
938 let breakpointItem = this.getBreakpoint(location); |
|
939 |
|
940 // If a breakpoint already existed, remove it now. |
|
941 if (breakpointItem) { |
|
942 DebuggerController.Breakpoints.removeBreakpoint(location); |
|
943 } |
|
944 // No breakpoint existed at the required location, add one now. |
|
945 else { |
|
946 DebuggerController.Breakpoints.addBreakpoint(location); |
|
947 } |
|
948 }, |
|
949 |
|
950 /** |
|
951 * Called when the add conditional breakpoint key sequence was pressed. |
|
952 */ |
|
953 _onCmdAddConditionalBreakpoint: function() { |
|
954 let url = DebuggerView.Sources.selectedValue; |
|
955 let line = DebuggerView.editor.getCursor().line + 1; |
|
956 let location = { url: url, line: line }; |
|
957 let breakpointItem = this.getBreakpoint(location); |
|
958 |
|
959 // If a breakpoint already existed or wasn't a conditional, morph it now. |
|
960 if (breakpointItem) { |
|
961 this.highlightBreakpoint(location, { openPopup: true }); |
|
962 } |
|
963 // No breakpoint existed at the required location, add one now. |
|
964 else { |
|
965 DebuggerController.Breakpoints.addBreakpoint(location, { openPopup: true }); |
|
966 } |
|
967 }, |
|
968 |
|
969 /** |
|
970 * Function invoked on the "setConditional" menuitem command. |
|
971 * |
|
972 * @param object aLocation |
|
973 * @see DebuggerController.Breakpoints.addBreakpoint |
|
974 */ |
|
975 _onSetConditional: function(aLocation) { |
|
976 // Highlight the breakpoint and show a conditional expression popup. |
|
977 this.highlightBreakpoint(aLocation, { openPopup: true }); |
|
978 }, |
|
979 |
|
980 /** |
|
981 * Function invoked on the "enableSelf" menuitem command. |
|
982 * |
|
983 * @param object aLocation |
|
984 * @see DebuggerController.Breakpoints.addBreakpoint |
|
985 */ |
|
986 _onEnableSelf: function(aLocation) { |
|
987 // Enable the breakpoint, in this container and the controller store. |
|
988 this.enableBreakpoint(aLocation); |
|
989 }, |
|
990 |
|
991 /** |
|
992 * Function invoked on the "disableSelf" menuitem command. |
|
993 * |
|
994 * @param object aLocation |
|
995 * @see DebuggerController.Breakpoints.addBreakpoint |
|
996 */ |
|
997 _onDisableSelf: function(aLocation) { |
|
998 // Disable the breakpoint, in this container and the controller store. |
|
999 this.disableBreakpoint(aLocation); |
|
1000 }, |
|
1001 |
|
1002 /** |
|
1003 * Function invoked on the "deleteSelf" menuitem command. |
|
1004 * |
|
1005 * @param object aLocation |
|
1006 * @see DebuggerController.Breakpoints.addBreakpoint |
|
1007 */ |
|
1008 _onDeleteSelf: function(aLocation) { |
|
1009 // Remove the breakpoint, from this container and the controller store. |
|
1010 this.removeBreakpoint(aLocation); |
|
1011 DebuggerController.Breakpoints.removeBreakpoint(aLocation); |
|
1012 }, |
|
1013 |
|
1014 /** |
|
1015 * Function invoked on the "enableOthers" menuitem command. |
|
1016 * |
|
1017 * @param object aLocation |
|
1018 * @see DebuggerController.Breakpoints.addBreakpoint |
|
1019 */ |
|
1020 _onEnableOthers: function(aLocation) { |
|
1021 let enableOthers = aCallback => { |
|
1022 let other = this.getOtherBreakpoints(aLocation); |
|
1023 let outstanding = other.map(e => this.enableBreakpoint(e.attachment)); |
|
1024 promise.all(outstanding).then(aCallback); |
|
1025 } |
|
1026 |
|
1027 // Breakpoints can only be set while the debuggee is paused. To avoid |
|
1028 // an avalanche of pause/resume interrupts of the main thread, simply |
|
1029 // pause it beforehand if it's not already. |
|
1030 if (gThreadClient.state != "paused") { |
|
1031 gThreadClient.interrupt(() => enableOthers(() => gThreadClient.resume())); |
|
1032 } else { |
|
1033 enableOthers(); |
|
1034 } |
|
1035 }, |
|
1036 |
|
1037 /** |
|
1038 * Function invoked on the "disableOthers" menuitem command. |
|
1039 * |
|
1040 * @param object aLocation |
|
1041 * @see DebuggerController.Breakpoints.addBreakpoint |
|
1042 */ |
|
1043 _onDisableOthers: function(aLocation) { |
|
1044 let other = this.getOtherBreakpoints(aLocation); |
|
1045 other.forEach(e => this._onDisableSelf(e.attachment)); |
|
1046 }, |
|
1047 |
|
1048 /** |
|
1049 * Function invoked on the "deleteOthers" menuitem command. |
|
1050 * |
|
1051 * @param object aLocation |
|
1052 * @see DebuggerController.Breakpoints.addBreakpoint |
|
1053 */ |
|
1054 _onDeleteOthers: function(aLocation) { |
|
1055 let other = this.getOtherBreakpoints(aLocation); |
|
1056 other.forEach(e => this._onDeleteSelf(e.attachment)); |
|
1057 }, |
|
1058 |
|
1059 /** |
|
1060 * Function invoked on the "enableAll" menuitem command. |
|
1061 */ |
|
1062 _onEnableAll: function() { |
|
1063 this._onEnableOthers(undefined); |
|
1064 }, |
|
1065 |
|
1066 /** |
|
1067 * Function invoked on the "disableAll" menuitem command. |
|
1068 */ |
|
1069 _onDisableAll: function() { |
|
1070 this._onDisableOthers(undefined); |
|
1071 }, |
|
1072 |
|
1073 /** |
|
1074 * Function invoked on the "deleteAll" menuitem command. |
|
1075 */ |
|
1076 _onDeleteAll: function() { |
|
1077 this._onDeleteOthers(undefined); |
|
1078 }, |
|
1079 |
|
1080 _commandset: null, |
|
1081 _popupset: null, |
|
1082 _cmPopup: null, |
|
1083 _cbPanel: null, |
|
1084 _cbTextbox: null, |
|
1085 _selectedBreakpointItem: null, |
|
1086 _conditionalPopupVisible: false |
|
1087 }); |
|
1088 |
|
1089 /** |
|
1090 * Functions handling the traces UI. |
|
1091 */ |
|
1092 function TracerView() { |
|
1093 this._selectedItem = null; |
|
1094 this._matchingItems = null; |
|
1095 this.widget = null; |
|
1096 |
|
1097 this._highlightItem = this._highlightItem.bind(this); |
|
1098 this._isNotSelectedItem = this._isNotSelectedItem.bind(this); |
|
1099 |
|
1100 this._unhighlightMatchingItems = |
|
1101 DevToolsUtils.makeInfallible(this._unhighlightMatchingItems.bind(this)); |
|
1102 this._onToggleTracing = |
|
1103 DevToolsUtils.makeInfallible(this._onToggleTracing.bind(this)); |
|
1104 this._onStartTracing = |
|
1105 DevToolsUtils.makeInfallible(this._onStartTracing.bind(this)); |
|
1106 this._onClear = |
|
1107 DevToolsUtils.makeInfallible(this._onClear.bind(this)); |
|
1108 this._onSelect = |
|
1109 DevToolsUtils.makeInfallible(this._onSelect.bind(this)); |
|
1110 this._onMouseOver = |
|
1111 DevToolsUtils.makeInfallible(this._onMouseOver.bind(this)); |
|
1112 this._onSearch = DevToolsUtils.makeInfallible(this._onSearch.bind(this)); |
|
1113 } |
|
1114 |
|
1115 TracerView.MAX_TRACES = 200; |
|
1116 |
|
1117 TracerView.prototype = Heritage.extend(WidgetMethods, { |
|
1118 /** |
|
1119 * Initialization function, called when the debugger is started. |
|
1120 */ |
|
1121 initialize: function() { |
|
1122 dumpn("Initializing the TracerView"); |
|
1123 |
|
1124 this._traceButton = document.getElementById("trace"); |
|
1125 this._tracerTab = document.getElementById("tracer-tab"); |
|
1126 |
|
1127 // Remove tracer related elements from the dom and tear everything down if |
|
1128 // the tracer isn't enabled. |
|
1129 if (!Prefs.tracerEnabled) { |
|
1130 this._traceButton.remove(); |
|
1131 this._traceButton = null; |
|
1132 this._tracerTab.remove(); |
|
1133 this._tracerTab = null; |
|
1134 return; |
|
1135 } |
|
1136 |
|
1137 this.widget = new FastListWidget(document.getElementById("tracer-traces")); |
|
1138 this._traceButton.removeAttribute("hidden"); |
|
1139 this._tracerTab.removeAttribute("hidden"); |
|
1140 |
|
1141 this._search = document.getElementById("tracer-search"); |
|
1142 this._template = document.getElementsByClassName("trace-item-template")[0]; |
|
1143 this._templateItem = this._template.getElementsByClassName("trace-item")[0]; |
|
1144 this._templateTypeIcon = this._template.getElementsByClassName("trace-type")[0]; |
|
1145 this._templateNameNode = this._template.getElementsByClassName("trace-name")[0]; |
|
1146 |
|
1147 this.widget.addEventListener("select", this._onSelect, false); |
|
1148 this.widget.addEventListener("mouseover", this._onMouseOver, false); |
|
1149 this.widget.addEventListener("mouseout", this._unhighlightMatchingItems, false); |
|
1150 this._search.addEventListener("input", this._onSearch, false); |
|
1151 |
|
1152 this._startTooltip = L10N.getStr("startTracingTooltip"); |
|
1153 this._stopTooltip = L10N.getStr("stopTracingTooltip"); |
|
1154 this._tracingNotStartedString = L10N.getStr("tracingNotStartedText"); |
|
1155 this._noFunctionCallsString = L10N.getStr("noFunctionCallsText"); |
|
1156 |
|
1157 this._traceButton.setAttribute("tooltiptext", this._startTooltip); |
|
1158 this.emptyText = this._tracingNotStartedString; |
|
1159 }, |
|
1160 |
|
1161 /** |
|
1162 * Destruction function, called when the debugger is closed. |
|
1163 */ |
|
1164 destroy: function() { |
|
1165 dumpn("Destroying the TracerView"); |
|
1166 |
|
1167 if (!this.widget) { |
|
1168 return; |
|
1169 } |
|
1170 |
|
1171 this.widget.removeEventListener("select", this._onSelect, false); |
|
1172 this.widget.removeEventListener("mouseover", this._onMouseOver, false); |
|
1173 this.widget.removeEventListener("mouseout", this._unhighlightMatchingItems, false); |
|
1174 this._search.removeEventListener("input", this._onSearch, false); |
|
1175 }, |
|
1176 |
|
1177 /** |
|
1178 * Function invoked by the "toggleTracing" command to switch the tracer state. |
|
1179 */ |
|
1180 _onToggleTracing: function() { |
|
1181 if (DebuggerController.Tracer.tracing) { |
|
1182 this._onStopTracing(); |
|
1183 } else { |
|
1184 this._onStartTracing(); |
|
1185 } |
|
1186 }, |
|
1187 |
|
1188 /** |
|
1189 * Function invoked either by the "startTracing" command or by |
|
1190 * _onToggleTracing to start execution tracing in the backend. |
|
1191 * |
|
1192 * @return object |
|
1193 * A promise resolved once the tracing has successfully started. |
|
1194 */ |
|
1195 _onStartTracing: function() { |
|
1196 this._traceButton.setAttribute("checked", true); |
|
1197 this._traceButton.setAttribute("tooltiptext", this._stopTooltip); |
|
1198 |
|
1199 this.empty(); |
|
1200 this.emptyText = this._noFunctionCallsString; |
|
1201 |
|
1202 let deferred = promise.defer(); |
|
1203 DebuggerController.Tracer.startTracing(deferred.resolve); |
|
1204 return deferred.promise; |
|
1205 }, |
|
1206 |
|
1207 /** |
|
1208 * Function invoked by _onToggleTracing to stop execution tracing in the |
|
1209 * backend. |
|
1210 * |
|
1211 * @return object |
|
1212 * A promise resolved once the tracing has successfully stopped. |
|
1213 */ |
|
1214 _onStopTracing: function() { |
|
1215 this._traceButton.removeAttribute("checked"); |
|
1216 this._traceButton.setAttribute("tooltiptext", this._startTooltip); |
|
1217 |
|
1218 this.emptyText = this._tracingNotStartedString; |
|
1219 |
|
1220 let deferred = promise.defer(); |
|
1221 DebuggerController.Tracer.stopTracing(deferred.resolve); |
|
1222 return deferred.promise; |
|
1223 }, |
|
1224 |
|
1225 /** |
|
1226 * Function invoked by the "clearTraces" command to empty the traces pane. |
|
1227 */ |
|
1228 _onClear: function() { |
|
1229 this.empty(); |
|
1230 }, |
|
1231 |
|
1232 /** |
|
1233 * Populate the given parent scope with the variable with the provided name |
|
1234 * and value. |
|
1235 * |
|
1236 * @param String aName |
|
1237 * The name of the variable. |
|
1238 * @param Object aParent |
|
1239 * The parent scope. |
|
1240 * @param Object aValue |
|
1241 * The value of the variable. |
|
1242 */ |
|
1243 _populateVariable: function(aName, aParent, aValue) { |
|
1244 let item = aParent.addItem(aName, { value: aValue }); |
|
1245 if (aValue) { |
|
1246 let wrappedValue = new DebuggerController.Tracer.WrappedObject(aValue); |
|
1247 DebuggerView.Variables.controller.populate(item, wrappedValue); |
|
1248 item.expand(); |
|
1249 item.twisty = false; |
|
1250 } |
|
1251 }, |
|
1252 |
|
1253 /** |
|
1254 * Handler for the widget's "select" event. Displays parameters, exception, or |
|
1255 * return value depending on whether the selected trace is a call, throw, or |
|
1256 * return respectively. |
|
1257 * |
|
1258 * @param Object traceItem |
|
1259 * The selected trace item. |
|
1260 */ |
|
1261 _onSelect: function _onSelect({ detail: traceItem }) { |
|
1262 if (!traceItem) { |
|
1263 return; |
|
1264 } |
|
1265 |
|
1266 const data = traceItem.attachment.trace; |
|
1267 const { location: { url, line } } = data; |
|
1268 DebuggerView.setEditorLocation(url, line, { noDebug: true }); |
|
1269 |
|
1270 DebuggerView.Variables.empty(); |
|
1271 const scope = DebuggerView.Variables.addScope(); |
|
1272 |
|
1273 if (data.type == "call") { |
|
1274 const params = DevToolsUtils.zip(data.parameterNames, data.arguments); |
|
1275 for (let [name, val] of params) { |
|
1276 if (val === undefined) { |
|
1277 scope.addItem(name, { value: "<value not available>" }); |
|
1278 } else { |
|
1279 this._populateVariable(name, scope, val); |
|
1280 } |
|
1281 } |
|
1282 } else { |
|
1283 const varName = "<" + (data.type == "throw" ? "exception" : data.type) + ">"; |
|
1284 this._populateVariable(varName, scope, data.returnVal); |
|
1285 } |
|
1286 |
|
1287 scope.expand(); |
|
1288 DebuggerView.showInstrumentsPane(); |
|
1289 }, |
|
1290 |
|
1291 /** |
|
1292 * Add the hover frame enter/exit highlighting to a given item. |
|
1293 */ |
|
1294 _highlightItem: function(aItem) { |
|
1295 if (!aItem || !aItem.target) { |
|
1296 return; |
|
1297 } |
|
1298 const trace = aItem.target.querySelector(".trace-item"); |
|
1299 trace.classList.add("selected-matching"); |
|
1300 }, |
|
1301 |
|
1302 /** |
|
1303 * Remove the hover frame enter/exit highlighting to a given item. |
|
1304 */ |
|
1305 _unhighlightItem: function(aItem) { |
|
1306 if (!aItem || !aItem.target) { |
|
1307 return; |
|
1308 } |
|
1309 const match = aItem.target.querySelector(".selected-matching"); |
|
1310 if (match) { |
|
1311 match.classList.remove("selected-matching"); |
|
1312 } |
|
1313 }, |
|
1314 |
|
1315 /** |
|
1316 * Remove the frame enter/exit pair highlighting we do when hovering. |
|
1317 */ |
|
1318 _unhighlightMatchingItems: function() { |
|
1319 if (this._matchingItems) { |
|
1320 this._matchingItems.forEach(this._unhighlightItem); |
|
1321 this._matchingItems = null; |
|
1322 } |
|
1323 }, |
|
1324 |
|
1325 /** |
|
1326 * Returns true if the given item is not the selected item. |
|
1327 */ |
|
1328 _isNotSelectedItem: function(aItem) { |
|
1329 return aItem !== this.selectedItem; |
|
1330 }, |
|
1331 |
|
1332 /** |
|
1333 * Highlight the frame enter/exit pair of items for the given item. |
|
1334 */ |
|
1335 _highlightMatchingItems: function(aItem) { |
|
1336 const frameId = aItem.attachment.trace.frameId; |
|
1337 const predicate = e => e.attachment.trace.frameId == frameId; |
|
1338 |
|
1339 this._unhighlightMatchingItems(); |
|
1340 this._matchingItems = this.items.filter(predicate); |
|
1341 this._matchingItems |
|
1342 .filter(this._isNotSelectedItem) |
|
1343 .forEach(this._highlightItem); |
|
1344 }, |
|
1345 |
|
1346 /** |
|
1347 * Listener for the mouseover event. |
|
1348 */ |
|
1349 _onMouseOver: function({ target }) { |
|
1350 const traceItem = this.getItemForElement(target); |
|
1351 if (traceItem) { |
|
1352 this._highlightMatchingItems(traceItem); |
|
1353 } |
|
1354 }, |
|
1355 |
|
1356 /** |
|
1357 * Listener for typing in the search box. |
|
1358 */ |
|
1359 _onSearch: function() { |
|
1360 const query = this._search.value.trim().toLowerCase(); |
|
1361 const predicate = name => name.toLowerCase().contains(query); |
|
1362 this.filterContents(item => predicate(item.attachment.trace.name)); |
|
1363 }, |
|
1364 |
|
1365 /** |
|
1366 * Select the traces tab in the sidebar. |
|
1367 */ |
|
1368 selectTab: function() { |
|
1369 const tabs = this._tracerTab.parentElement; |
|
1370 tabs.selectedIndex = Array.indexOf(tabs.children, this._tracerTab); |
|
1371 }, |
|
1372 |
|
1373 /** |
|
1374 * Commit all staged items to the widget. Overridden so that we can call |
|
1375 * |FastListWidget.prototype.flush|. |
|
1376 */ |
|
1377 commit: function() { |
|
1378 WidgetMethods.commit.call(this); |
|
1379 // TODO: Accessing non-standard widget properties. Figure out what's the |
|
1380 // best way to expose such things. Bug 895514. |
|
1381 this.widget.flush(); |
|
1382 }, |
|
1383 |
|
1384 /** |
|
1385 * Adds the trace record provided as an argument to the view. |
|
1386 * |
|
1387 * @param object aTrace |
|
1388 * The trace record coming from the tracer actor. |
|
1389 */ |
|
1390 addTrace: function(aTrace) { |
|
1391 // Create the element node for the trace item. |
|
1392 let view = this._createView(aTrace); |
|
1393 |
|
1394 // Append a source item to this container. |
|
1395 this.push([view], { |
|
1396 staged: true, |
|
1397 attachment: { |
|
1398 trace: aTrace |
|
1399 } |
|
1400 }); |
|
1401 }, |
|
1402 |
|
1403 /** |
|
1404 * Customization function for creating an item's UI. |
|
1405 * |
|
1406 * @return nsIDOMNode |
|
1407 * The network request view. |
|
1408 */ |
|
1409 _createView: function(aTrace) { |
|
1410 let { type, name, location, depth, frameId } = aTrace; |
|
1411 let { parameterNames, returnVal, arguments: args } = aTrace; |
|
1412 let fragment = document.createDocumentFragment(); |
|
1413 |
|
1414 this._templateItem.setAttribute("tooltiptext", SourceUtils.trimUrl(location.url)); |
|
1415 this._templateItem.style.MozPaddingStart = depth + "em"; |
|
1416 |
|
1417 const TYPES = ["call", "yield", "return", "throw"]; |
|
1418 for (let t of TYPES) { |
|
1419 this._templateTypeIcon.classList.toggle("trace-" + t, t == type); |
|
1420 } |
|
1421 this._templateTypeIcon.setAttribute("value", { |
|
1422 call: "\u2192", |
|
1423 yield: "Y", |
|
1424 return: "\u2190", |
|
1425 throw: "E", |
|
1426 terminated: "TERMINATED" |
|
1427 }[type]); |
|
1428 |
|
1429 this._templateNameNode.setAttribute("value", name); |
|
1430 |
|
1431 // All extra syntax and parameter nodes added. |
|
1432 const addedNodes = []; |
|
1433 |
|
1434 if (parameterNames) { |
|
1435 const syntax = (p) => { |
|
1436 const el = document.createElement("label"); |
|
1437 el.setAttribute("value", p); |
|
1438 el.classList.add("trace-syntax"); |
|
1439 el.classList.add("plain"); |
|
1440 addedNodes.push(el); |
|
1441 return el; |
|
1442 }; |
|
1443 |
|
1444 this._templateItem.appendChild(syntax("(")); |
|
1445 |
|
1446 for (let i = 0, n = parameterNames.length; i < n; i++) { |
|
1447 let param = document.createElement("label"); |
|
1448 param.setAttribute("value", parameterNames[i]); |
|
1449 param.classList.add("trace-param"); |
|
1450 param.classList.add("plain"); |
|
1451 addedNodes.push(param); |
|
1452 this._templateItem.appendChild(param); |
|
1453 |
|
1454 if (i + 1 !== n) { |
|
1455 this._templateItem.appendChild(syntax(", ")); |
|
1456 } |
|
1457 } |
|
1458 |
|
1459 this._templateItem.appendChild(syntax(")")); |
|
1460 } |
|
1461 |
|
1462 // Flatten the DOM by removing one redundant box (the template container). |
|
1463 for (let node of this._template.childNodes) { |
|
1464 fragment.appendChild(node.cloneNode(true)); |
|
1465 } |
|
1466 |
|
1467 // Remove any added nodes from the template. |
|
1468 for (let node of addedNodes) { |
|
1469 this._templateItem.removeChild(node); |
|
1470 } |
|
1471 |
|
1472 return fragment; |
|
1473 } |
|
1474 }); |
|
1475 |
|
1476 /** |
|
1477 * Utility functions for handling sources. |
|
1478 */ |
|
1479 let SourceUtils = { |
|
1480 _labelsCache: new Map(), // Can't use WeakMaps because keys are strings. |
|
1481 _groupsCache: new Map(), |
|
1482 _minifiedCache: new WeakMap(), |
|
1483 |
|
1484 /** |
|
1485 * Returns true if the specified url and/or content type are specific to |
|
1486 * javascript files. |
|
1487 * |
|
1488 * @return boolean |
|
1489 * True if the source is likely javascript. |
|
1490 */ |
|
1491 isJavaScript: function(aUrl, aContentType = "") { |
|
1492 return /\.jsm?$/.test(this.trimUrlQuery(aUrl)) || |
|
1493 aContentType.contains("javascript"); |
|
1494 }, |
|
1495 |
|
1496 /** |
|
1497 * Determines if the source text is minified by using |
|
1498 * the percentage indented of a subset of lines |
|
1499 * |
|
1500 * @param string aText |
|
1501 * The source text. |
|
1502 * @return boolean |
|
1503 * True if source text is minified. |
|
1504 */ |
|
1505 isMinified: function(sourceClient, aText){ |
|
1506 if (this._minifiedCache.has(sourceClient)) { |
|
1507 return this._minifiedCache.get(sourceClient); |
|
1508 } |
|
1509 |
|
1510 let isMinified; |
|
1511 let lineEndIndex = 0; |
|
1512 let lineStartIndex = 0; |
|
1513 let lines = 0; |
|
1514 let indentCount = 0; |
|
1515 let overCharLimit = false; |
|
1516 |
|
1517 // Strip comments. |
|
1518 aText = aText.replace(/\/\*[\S\s]*?\*\/|\/\/(.+|\n)/g, ""); |
|
1519 |
|
1520 while (lines++ < SAMPLE_SIZE) { |
|
1521 lineEndIndex = aText.indexOf("\n", lineStartIndex); |
|
1522 if (lineEndIndex == -1) { |
|
1523 break; |
|
1524 } |
|
1525 if (/^\s+/.test(aText.slice(lineStartIndex, lineEndIndex))) { |
|
1526 indentCount++; |
|
1527 } |
|
1528 // For files with no indents but are not minified. |
|
1529 if ((lineEndIndex - lineStartIndex) > CHARACTER_LIMIT) { |
|
1530 overCharLimit = true; |
|
1531 break; |
|
1532 } |
|
1533 lineStartIndex = lineEndIndex + 1; |
|
1534 } |
|
1535 isMinified = ((indentCount / lines ) * 100) < INDENT_COUNT_THRESHOLD || |
|
1536 overCharLimit; |
|
1537 |
|
1538 this._minifiedCache.set(sourceClient, isMinified); |
|
1539 return isMinified; |
|
1540 }, |
|
1541 |
|
1542 /** |
|
1543 * Clears the labels, groups and minify cache, populated by methods like |
|
1544 * SourceUtils.getSourceLabel or Source Utils.getSourceGroup. |
|
1545 * This should be done every time the content location changes. |
|
1546 */ |
|
1547 clearCache: function() { |
|
1548 this._labelsCache.clear(); |
|
1549 this._groupsCache.clear(); |
|
1550 this._minifiedCache.clear(); |
|
1551 }, |
|
1552 |
|
1553 /** |
|
1554 * Gets a unique, simplified label from a source url. |
|
1555 * |
|
1556 * @param string aUrl |
|
1557 * The source url. |
|
1558 * @return string |
|
1559 * The simplified label. |
|
1560 */ |
|
1561 getSourceLabel: function(aUrl) { |
|
1562 let cachedLabel = this._labelsCache.get(aUrl); |
|
1563 if (cachedLabel) { |
|
1564 return cachedLabel; |
|
1565 } |
|
1566 |
|
1567 let sourceLabel = null; |
|
1568 |
|
1569 for (let name of Object.keys(KNOWN_SOURCE_GROUPS)) { |
|
1570 if (aUrl.startsWith(KNOWN_SOURCE_GROUPS[name])) { |
|
1571 sourceLabel = aUrl.substring(KNOWN_SOURCE_GROUPS[name].length); |
|
1572 } |
|
1573 } |
|
1574 |
|
1575 if (!sourceLabel) { |
|
1576 sourceLabel = this.trimUrl(aUrl); |
|
1577 } |
|
1578 let unicodeLabel = NetworkHelper.convertToUnicode(unescape(sourceLabel)); |
|
1579 this._labelsCache.set(aUrl, unicodeLabel); |
|
1580 return unicodeLabel; |
|
1581 }, |
|
1582 |
|
1583 /** |
|
1584 * Gets as much information as possible about the hostname and directory paths |
|
1585 * of an url to create a short url group identifier. |
|
1586 * |
|
1587 * @param string aUrl |
|
1588 * The source url. |
|
1589 * @return string |
|
1590 * The simplified group. |
|
1591 */ |
|
1592 getSourceGroup: function(aUrl) { |
|
1593 let cachedGroup = this._groupsCache.get(aUrl); |
|
1594 if (cachedGroup) { |
|
1595 return cachedGroup; |
|
1596 } |
|
1597 |
|
1598 try { |
|
1599 // Use an nsIURL to parse all the url path parts. |
|
1600 var uri = Services.io.newURI(aUrl, null, null).QueryInterface(Ci.nsIURL); |
|
1601 } catch (e) { |
|
1602 // This doesn't look like a url, or nsIURL can't handle it. |
|
1603 return ""; |
|
1604 } |
|
1605 |
|
1606 let groupLabel = uri.prePath; |
|
1607 |
|
1608 for (let name of Object.keys(KNOWN_SOURCE_GROUPS)) { |
|
1609 if (aUrl.startsWith(KNOWN_SOURCE_GROUPS[name])) { |
|
1610 groupLabel = name; |
|
1611 } |
|
1612 } |
|
1613 |
|
1614 let unicodeLabel = NetworkHelper.convertToUnicode(unescape(groupLabel)); |
|
1615 this._groupsCache.set(aUrl, unicodeLabel) |
|
1616 return unicodeLabel; |
|
1617 }, |
|
1618 |
|
1619 /** |
|
1620 * Trims the url by shortening it if it exceeds a certain length, adding an |
|
1621 * ellipsis at the end. |
|
1622 * |
|
1623 * @param string aUrl |
|
1624 * The source url. |
|
1625 * @param number aLength [optional] |
|
1626 * The expected source url length. |
|
1627 * @param number aSection [optional] |
|
1628 * The section to trim. Supported values: "start", "center", "end" |
|
1629 * @return string |
|
1630 * The shortened url. |
|
1631 */ |
|
1632 trimUrlLength: function(aUrl, aLength, aSection) { |
|
1633 aLength = aLength || SOURCE_URL_DEFAULT_MAX_LENGTH; |
|
1634 aSection = aSection || "end"; |
|
1635 |
|
1636 if (aUrl.length > aLength) { |
|
1637 switch (aSection) { |
|
1638 case "start": |
|
1639 return L10N.ellipsis + aUrl.slice(-aLength); |
|
1640 break; |
|
1641 case "center": |
|
1642 return aUrl.substr(0, aLength / 2 - 1) + L10N.ellipsis + aUrl.slice(-aLength / 2 + 1); |
|
1643 break; |
|
1644 case "end": |
|
1645 return aUrl.substr(0, aLength) + L10N.ellipsis; |
|
1646 break; |
|
1647 } |
|
1648 } |
|
1649 return aUrl; |
|
1650 }, |
|
1651 |
|
1652 /** |
|
1653 * Trims the query part or reference identifier of a url string, if necessary. |
|
1654 * |
|
1655 * @param string aUrl |
|
1656 * The source url. |
|
1657 * @return string |
|
1658 * The shortened url. |
|
1659 */ |
|
1660 trimUrlQuery: function(aUrl) { |
|
1661 let length = aUrl.length; |
|
1662 let q1 = aUrl.indexOf('?'); |
|
1663 let q2 = aUrl.indexOf('&'); |
|
1664 let q3 = aUrl.indexOf('#'); |
|
1665 let q = Math.min(q1 != -1 ? q1 : length, |
|
1666 q2 != -1 ? q2 : length, |
|
1667 q3 != -1 ? q3 : length); |
|
1668 |
|
1669 return aUrl.slice(0, q); |
|
1670 }, |
|
1671 |
|
1672 /** |
|
1673 * Trims as much as possible from a url, while keeping the label unique |
|
1674 * in the sources container. |
|
1675 * |
|
1676 * @param string | nsIURL aUrl |
|
1677 * The source url. |
|
1678 * @param string aLabel [optional] |
|
1679 * The resulting label at each step. |
|
1680 * @param number aSeq [optional] |
|
1681 * The current iteration step. |
|
1682 * @return string |
|
1683 * The resulting label at the final step. |
|
1684 */ |
|
1685 trimUrl: function(aUrl, aLabel, aSeq) { |
|
1686 if (!(aUrl instanceof Ci.nsIURL)) { |
|
1687 try { |
|
1688 // Use an nsIURL to parse all the url path parts. |
|
1689 aUrl = Services.io.newURI(aUrl, null, null).QueryInterface(Ci.nsIURL); |
|
1690 } catch (e) { |
|
1691 // This doesn't look like a url, or nsIURL can't handle it. |
|
1692 return aUrl; |
|
1693 } |
|
1694 } |
|
1695 if (!aSeq) { |
|
1696 let name = aUrl.fileName; |
|
1697 if (name) { |
|
1698 // This is a regular file url, get only the file name (contains the |
|
1699 // base name and extension if available). |
|
1700 |
|
1701 // If this url contains an invalid query, unfortunately nsIURL thinks |
|
1702 // it's part of the file extension. It must be removed. |
|
1703 aLabel = aUrl.fileName.replace(/\&.*/, ""); |
|
1704 } else { |
|
1705 // This is not a file url, hence there is no base name, nor extension. |
|
1706 // Proceed using other available information. |
|
1707 aLabel = ""; |
|
1708 } |
|
1709 aSeq = 1; |
|
1710 } |
|
1711 |
|
1712 // If we have a label and it doesn't only contain a query... |
|
1713 if (aLabel && aLabel.indexOf("?") != 0) { |
|
1714 // A page may contain multiple requests to the same url but with different |
|
1715 // queries. It is *not* redundant to show each one. |
|
1716 if (!DebuggerView.Sources.getItemForAttachment(e => e.label == aLabel)) { |
|
1717 return aLabel; |
|
1718 } |
|
1719 } |
|
1720 |
|
1721 // Append the url query. |
|
1722 if (aSeq == 1) { |
|
1723 let query = aUrl.query; |
|
1724 if (query) { |
|
1725 return this.trimUrl(aUrl, aLabel + "?" + query, aSeq + 1); |
|
1726 } |
|
1727 aSeq++; |
|
1728 } |
|
1729 // Append the url reference. |
|
1730 if (aSeq == 2) { |
|
1731 let ref = aUrl.ref; |
|
1732 if (ref) { |
|
1733 return this.trimUrl(aUrl, aLabel + "#" + aUrl.ref, aSeq + 1); |
|
1734 } |
|
1735 aSeq++; |
|
1736 } |
|
1737 // Prepend the url directory. |
|
1738 if (aSeq == 3) { |
|
1739 let dir = aUrl.directory; |
|
1740 if (dir) { |
|
1741 return this.trimUrl(aUrl, dir.replace(/^\//, "") + aLabel, aSeq + 1); |
|
1742 } |
|
1743 aSeq++; |
|
1744 } |
|
1745 // Prepend the hostname and port number. |
|
1746 if (aSeq == 4) { |
|
1747 let host = aUrl.hostPort; |
|
1748 if (host) { |
|
1749 return this.trimUrl(aUrl, host + "/" + aLabel, aSeq + 1); |
|
1750 } |
|
1751 aSeq++; |
|
1752 } |
|
1753 // Use the whole url spec but ignoring the reference. |
|
1754 if (aSeq == 5) { |
|
1755 return this.trimUrl(aUrl, aUrl.specIgnoringRef, aSeq + 1); |
|
1756 } |
|
1757 // Give up. |
|
1758 return aUrl.spec; |
|
1759 } |
|
1760 }; |
|
1761 |
|
1762 /** |
|
1763 * Functions handling the variables bubble UI. |
|
1764 */ |
|
1765 function VariableBubbleView() { |
|
1766 dumpn("VariableBubbleView was instantiated"); |
|
1767 |
|
1768 this._onMouseMove = this._onMouseMove.bind(this); |
|
1769 this._onMouseLeave = this._onMouseLeave.bind(this); |
|
1770 this._onPopupHiding = this._onPopupHiding.bind(this); |
|
1771 } |
|
1772 |
|
1773 VariableBubbleView.prototype = { |
|
1774 /** |
|
1775 * Initialization function, called when the debugger is started. |
|
1776 */ |
|
1777 initialize: function() { |
|
1778 dumpn("Initializing the VariableBubbleView"); |
|
1779 |
|
1780 this._editorContainer = document.getElementById("editor"); |
|
1781 this._editorContainer.addEventListener("mousemove", this._onMouseMove, false); |
|
1782 this._editorContainer.addEventListener("mouseleave", this._onMouseLeave, false); |
|
1783 |
|
1784 this._tooltip = new Tooltip(document, { |
|
1785 closeOnEvents: [{ |
|
1786 emitter: DebuggerController._toolbox, |
|
1787 event: "select" |
|
1788 }, { |
|
1789 emitter: this._editorContainer, |
|
1790 event: "scroll", |
|
1791 useCapture: true |
|
1792 }] |
|
1793 }); |
|
1794 this._tooltip.defaultPosition = EDITOR_VARIABLE_POPUP_POSITION; |
|
1795 this._tooltip.defaultShowDelay = EDITOR_VARIABLE_HOVER_DELAY; |
|
1796 this._tooltip.panel.addEventListener("popuphiding", this._onPopupHiding); |
|
1797 }, |
|
1798 |
|
1799 /** |
|
1800 * Destruction function, called when the debugger is closed. |
|
1801 */ |
|
1802 destroy: function() { |
|
1803 dumpn("Destroying the VariableBubbleView"); |
|
1804 |
|
1805 this._tooltip.panel.removeEventListener("popuphiding", this._onPopupHiding); |
|
1806 this._editorContainer.removeEventListener("mousemove", this._onMouseMove, false); |
|
1807 this._editorContainer.removeEventListener("mouseleave", this._onMouseLeave, false); |
|
1808 }, |
|
1809 |
|
1810 /** |
|
1811 * Specifies whether literals can be (redundantly) inspected in a popup. |
|
1812 * This behavior is deprecated, but still tested in a few places. |
|
1813 */ |
|
1814 _ignoreLiterals: true, |
|
1815 |
|
1816 /** |
|
1817 * Searches for an identifier underneath the specified position in the |
|
1818 * source editor, and if found, opens a VariablesView inspection popup. |
|
1819 * |
|
1820 * @param number x, y |
|
1821 * The left/top coordinates where to look for an identifier. |
|
1822 */ |
|
1823 _findIdentifier: function(x, y) { |
|
1824 let editor = DebuggerView.editor; |
|
1825 |
|
1826 // Calculate the editor's line and column at the current x and y coords. |
|
1827 let hoveredPos = editor.getPositionFromCoords({ left: x, top: y }); |
|
1828 let hoveredOffset = editor.getOffset(hoveredPos); |
|
1829 let hoveredLine = hoveredPos.line; |
|
1830 let hoveredColumn = hoveredPos.ch; |
|
1831 |
|
1832 // A source contains multiple scripts. Find the start index of the script |
|
1833 // containing the specified offset relative to its parent source. |
|
1834 let contents = editor.getText(); |
|
1835 let location = DebuggerView.Sources.selectedValue; |
|
1836 let parsedSource = DebuggerController.Parser.get(contents, location); |
|
1837 let scriptInfo = parsedSource.getScriptInfo(hoveredOffset); |
|
1838 |
|
1839 // If the script length is negative, we're not hovering JS source code. |
|
1840 if (scriptInfo.length == -1) { |
|
1841 return; |
|
1842 } |
|
1843 |
|
1844 // Using the script offset, determine the actual line and column inside the |
|
1845 // script, to use when finding identifiers. |
|
1846 let scriptStart = editor.getPosition(scriptInfo.start); |
|
1847 let scriptLineOffset = scriptStart.line; |
|
1848 let scriptColumnOffset = (hoveredLine == scriptStart.line ? scriptStart.ch : 0); |
|
1849 |
|
1850 let scriptLine = hoveredLine - scriptLineOffset; |
|
1851 let scriptColumn = hoveredColumn - scriptColumnOffset; |
|
1852 let identifierInfo = parsedSource.getIdentifierAt({ |
|
1853 line: scriptLine + 1, |
|
1854 column: scriptColumn, |
|
1855 scriptIndex: scriptInfo.index, |
|
1856 ignoreLiterals: this._ignoreLiterals |
|
1857 }); |
|
1858 |
|
1859 // If the info is null, we're not hovering any identifier. |
|
1860 if (!identifierInfo) { |
|
1861 return; |
|
1862 } |
|
1863 |
|
1864 // Transform the line and column relative to the parsed script back |
|
1865 // to the context of the parent source. |
|
1866 let { start: identifierStart, end: identifierEnd } = identifierInfo.location; |
|
1867 let identifierCoords = { |
|
1868 line: identifierStart.line + scriptLineOffset, |
|
1869 column: identifierStart.column + scriptColumnOffset, |
|
1870 length: identifierEnd.column - identifierStart.column |
|
1871 }; |
|
1872 |
|
1873 // Evaluate the identifier in the current stack frame and show the |
|
1874 // results in a VariablesView inspection popup. |
|
1875 DebuggerController.StackFrames.evaluate(identifierInfo.evalString) |
|
1876 .then(frameFinished => { |
|
1877 if ("return" in frameFinished) { |
|
1878 this.showContents({ |
|
1879 coords: identifierCoords, |
|
1880 evalPrefix: identifierInfo.evalString, |
|
1881 objectActor: frameFinished.return |
|
1882 }); |
|
1883 } else { |
|
1884 let msg = "Evaluation has thrown for: " + identifierInfo.evalString; |
|
1885 console.warn(msg); |
|
1886 dumpn(msg); |
|
1887 } |
|
1888 }) |
|
1889 .then(null, err => { |
|
1890 let msg = "Couldn't evaluate: " + err.message; |
|
1891 console.error(msg); |
|
1892 dumpn(msg); |
|
1893 }); |
|
1894 }, |
|
1895 |
|
1896 /** |
|
1897 * Shows an inspection popup for a specified object actor grip. |
|
1898 * |
|
1899 * @param string object |
|
1900 * An object containing the following properties: |
|
1901 * - coords: the inspected identifier coordinates in the editor, |
|
1902 * containing the { line, column, length } properties. |
|
1903 * - evalPrefix: a prefix for the variables view evaluation macros. |
|
1904 * - objectActor: the value grip for the object actor. |
|
1905 */ |
|
1906 showContents: function({ coords, evalPrefix, objectActor }) { |
|
1907 let editor = DebuggerView.editor; |
|
1908 let { line, column, length } = coords; |
|
1909 |
|
1910 // Highlight the function found at the mouse position. |
|
1911 this._markedText = editor.markText( |
|
1912 { line: line - 1, ch: column }, |
|
1913 { line: line - 1, ch: column + length }); |
|
1914 |
|
1915 // If the grip represents a primitive value, use a more lightweight |
|
1916 // machinery to display it. |
|
1917 if (VariablesView.isPrimitive({ value: objectActor })) { |
|
1918 let className = VariablesView.getClass(objectActor); |
|
1919 let textContent = VariablesView.getString(objectActor); |
|
1920 this._tooltip.setTextContent({ |
|
1921 messages: [textContent], |
|
1922 messagesClass: className, |
|
1923 containerClass: "plain" |
|
1924 }, [{ |
|
1925 label: L10N.getStr('addWatchExpressionButton'), |
|
1926 className: "dbg-expression-button", |
|
1927 command: () => { |
|
1928 DebuggerView.VariableBubble.hideContents(); |
|
1929 DebuggerView.WatchExpressions.addExpression(evalPrefix, true); |
|
1930 } |
|
1931 }]); |
|
1932 } else { |
|
1933 this._tooltip.setVariableContent(objectActor, { |
|
1934 searchPlaceholder: L10N.getStr("emptyPropertiesFilterText"), |
|
1935 searchEnabled: Prefs.variablesSearchboxVisible, |
|
1936 eval: (variable, value) => { |
|
1937 let string = variable.evaluationMacro(variable, value); |
|
1938 DebuggerController.StackFrames.evaluate(string); |
|
1939 DebuggerView.VariableBubble.hideContents(); |
|
1940 } |
|
1941 }, { |
|
1942 getEnvironmentClient: aObject => gThreadClient.environment(aObject), |
|
1943 getObjectClient: aObject => gThreadClient.pauseGrip(aObject), |
|
1944 simpleValueEvalMacro: this._getSimpleValueEvalMacro(evalPrefix), |
|
1945 getterOrSetterEvalMacro: this._getGetterOrSetterEvalMacro(evalPrefix), |
|
1946 overrideValueEvalMacro: this._getOverrideValueEvalMacro(evalPrefix) |
|
1947 }, { |
|
1948 fetched: (aEvent, aType) => { |
|
1949 if (aType == "properties") { |
|
1950 window.emit(EVENTS.FETCHED_BUBBLE_PROPERTIES); |
|
1951 } |
|
1952 } |
|
1953 }, [{ |
|
1954 label: L10N.getStr("addWatchExpressionButton"), |
|
1955 className: "dbg-expression-button", |
|
1956 command: () => { |
|
1957 DebuggerView.VariableBubble.hideContents(); |
|
1958 DebuggerView.WatchExpressions.addExpression(evalPrefix, true); |
|
1959 } |
|
1960 }], DebuggerController._toolbox); |
|
1961 } |
|
1962 |
|
1963 this._tooltip.show(this._markedText.anchor); |
|
1964 }, |
|
1965 |
|
1966 /** |
|
1967 * Hides the inspection popup. |
|
1968 */ |
|
1969 hideContents: function() { |
|
1970 clearNamedTimeout("editor-mouse-move"); |
|
1971 this._tooltip.hide(); |
|
1972 }, |
|
1973 |
|
1974 /** |
|
1975 * Checks whether the inspection popup is shown. |
|
1976 * |
|
1977 * @return boolean |
|
1978 * True if the panel is shown or showing, false otherwise. |
|
1979 */ |
|
1980 contentsShown: function() { |
|
1981 return this._tooltip.isShown(); |
|
1982 }, |
|
1983 |
|
1984 /** |
|
1985 * Functions for getting customized variables view evaluation macros. |
|
1986 * |
|
1987 * @param string aPrefix |
|
1988 * See the corresponding VariablesView.* functions. |
|
1989 */ |
|
1990 _getSimpleValueEvalMacro: function(aPrefix) { |
|
1991 return (item, string) => |
|
1992 VariablesView.simpleValueEvalMacro(item, string, aPrefix); |
|
1993 }, |
|
1994 _getGetterOrSetterEvalMacro: function(aPrefix) { |
|
1995 return (item, string) => |
|
1996 VariablesView.getterOrSetterEvalMacro(item, string, aPrefix); |
|
1997 }, |
|
1998 _getOverrideValueEvalMacro: function(aPrefix) { |
|
1999 return (item, string) => |
|
2000 VariablesView.overrideValueEvalMacro(item, string, aPrefix); |
|
2001 }, |
|
2002 |
|
2003 /** |
|
2004 * The mousemove listener for the source editor. |
|
2005 */ |
|
2006 _onMouseMove: function({ clientX: x, clientY: y, buttons: btns }) { |
|
2007 // Prevent the variable inspection popup from showing when the thread client |
|
2008 // is not paused, or while a popup is already visible, or when the user tries |
|
2009 // to select text in the editor. |
|
2010 if (gThreadClient && gThreadClient.state != "paused" |
|
2011 || !this._tooltip.isHidden() |
|
2012 || (DebuggerView.editor.somethingSelected() |
|
2013 && btns > 0)) { |
|
2014 clearNamedTimeout("editor-mouse-move"); |
|
2015 return; |
|
2016 } |
|
2017 // Allow events to settle down first. If the mouse hovers over |
|
2018 // a certain point in the editor long enough, try showing a variable bubble. |
|
2019 setNamedTimeout("editor-mouse-move", |
|
2020 EDITOR_VARIABLE_HOVER_DELAY, () => this._findIdentifier(x, y)); |
|
2021 }, |
|
2022 |
|
2023 /** |
|
2024 * The mouseleave listener for the source editor container node. |
|
2025 */ |
|
2026 _onMouseLeave: function() { |
|
2027 clearNamedTimeout("editor-mouse-move"); |
|
2028 }, |
|
2029 |
|
2030 /** |
|
2031 * Listener handling the popup hiding event. |
|
2032 */ |
|
2033 _onPopupHiding: function({ target }) { |
|
2034 if (this._tooltip.panel != target) { |
|
2035 return; |
|
2036 } |
|
2037 if (this._markedText) { |
|
2038 this._markedText.clear(); |
|
2039 this._markedText = null; |
|
2040 } |
|
2041 if (!this._tooltip.isEmpty()) { |
|
2042 this._tooltip.empty(); |
|
2043 } |
|
2044 }, |
|
2045 |
|
2046 _editorContainer: null, |
|
2047 _markedText: null, |
|
2048 _tooltip: null |
|
2049 }; |
|
2050 |
|
2051 /** |
|
2052 * Functions handling the watch expressions UI. |
|
2053 */ |
|
2054 function WatchExpressionsView() { |
|
2055 dumpn("WatchExpressionsView was instantiated"); |
|
2056 |
|
2057 this.switchExpression = this.switchExpression.bind(this); |
|
2058 this.deleteExpression = this.deleteExpression.bind(this); |
|
2059 this._createItemView = this._createItemView.bind(this); |
|
2060 this._onClick = this._onClick.bind(this); |
|
2061 this._onClose = this._onClose.bind(this); |
|
2062 this._onBlur = this._onBlur.bind(this); |
|
2063 this._onKeyPress = this._onKeyPress.bind(this); |
|
2064 } |
|
2065 |
|
2066 WatchExpressionsView.prototype = Heritage.extend(WidgetMethods, { |
|
2067 /** |
|
2068 * Initialization function, called when the debugger is started. |
|
2069 */ |
|
2070 initialize: function() { |
|
2071 dumpn("Initializing the WatchExpressionsView"); |
|
2072 |
|
2073 this.widget = new SimpleListWidget(document.getElementById("expressions")); |
|
2074 this.widget.setAttribute("context", "debuggerWatchExpressionsContextMenu"); |
|
2075 this.widget.addEventListener("click", this._onClick, false); |
|
2076 |
|
2077 this.headerText = L10N.getStr("addWatchExpressionText"); |
|
2078 }, |
|
2079 |
|
2080 /** |
|
2081 * Destruction function, called when the debugger is closed. |
|
2082 */ |
|
2083 destroy: function() { |
|
2084 dumpn("Destroying the WatchExpressionsView"); |
|
2085 |
|
2086 this.widget.removeEventListener("click", this._onClick, false); |
|
2087 }, |
|
2088 |
|
2089 /** |
|
2090 * Adds a watch expression in this container. |
|
2091 * |
|
2092 * @param string aExpression [optional] |
|
2093 * An optional initial watch expression text. |
|
2094 * @param boolean aSkipUserInput [optional] |
|
2095 * Pass true to avoid waiting for additional user input |
|
2096 * on the watch expression. |
|
2097 */ |
|
2098 addExpression: function(aExpression = "", aSkipUserInput = false) { |
|
2099 // Watch expressions are UI elements which benefit from visible panes. |
|
2100 DebuggerView.showInstrumentsPane(); |
|
2101 |
|
2102 // Create the element node for the watch expression item. |
|
2103 let itemView = this._createItemView(aExpression); |
|
2104 |
|
2105 // Append a watch expression item to this container. |
|
2106 let expressionItem = this.push([itemView.container], { |
|
2107 index: 0, /* specifies on which position should the item be appended */ |
|
2108 attachment: { |
|
2109 view: itemView, |
|
2110 initialExpression: aExpression, |
|
2111 currentExpression: "", |
|
2112 } |
|
2113 }); |
|
2114 |
|
2115 // Automatically focus the new watch expression input |
|
2116 // if additional user input is desired. |
|
2117 if (!aSkipUserInput) { |
|
2118 expressionItem.attachment.view.inputNode.select(); |
|
2119 expressionItem.attachment.view.inputNode.focus(); |
|
2120 DebuggerView.Variables.parentNode.scrollTop = 0; |
|
2121 } |
|
2122 // Otherwise, add and evaluate the new watch expression immediately. |
|
2123 else { |
|
2124 this.toggleContents(false); |
|
2125 this._onBlur({ target: expressionItem.attachment.view.inputNode }); |
|
2126 } |
|
2127 }, |
|
2128 |
|
2129 /** |
|
2130 * Changes the watch expression corresponding to the specified variable item. |
|
2131 * This function is called whenever a watch expression's code is edited in |
|
2132 * the variables view container. |
|
2133 * |
|
2134 * @param Variable aVar |
|
2135 * The variable representing the watch expression evaluation. |
|
2136 * @param string aExpression |
|
2137 * The new watch expression text. |
|
2138 */ |
|
2139 switchExpression: function(aVar, aExpression) { |
|
2140 let expressionItem = |
|
2141 [i for (i of this) if (i.attachment.currentExpression == aVar.name)][0]; |
|
2142 |
|
2143 // Remove the watch expression if it's going to be empty or a duplicate. |
|
2144 if (!aExpression || this.getAllStrings().indexOf(aExpression) != -1) { |
|
2145 this.deleteExpression(aVar); |
|
2146 return; |
|
2147 } |
|
2148 |
|
2149 // Save the watch expression code string. |
|
2150 expressionItem.attachment.currentExpression = aExpression; |
|
2151 expressionItem.attachment.view.inputNode.value = aExpression; |
|
2152 |
|
2153 // Synchronize with the controller's watch expressions store. |
|
2154 DebuggerController.StackFrames.syncWatchExpressions(); |
|
2155 }, |
|
2156 |
|
2157 /** |
|
2158 * Removes the watch expression corresponding to the specified variable item. |
|
2159 * This function is called whenever a watch expression's value is edited in |
|
2160 * the variables view container. |
|
2161 * |
|
2162 * @param Variable aVar |
|
2163 * The variable representing the watch expression evaluation. |
|
2164 */ |
|
2165 deleteExpression: function(aVar) { |
|
2166 let expressionItem = |
|
2167 [i for (i of this) if (i.attachment.currentExpression == aVar.name)][0]; |
|
2168 |
|
2169 // Remove the watch expression. |
|
2170 this.remove(expressionItem); |
|
2171 |
|
2172 // Synchronize with the controller's watch expressions store. |
|
2173 DebuggerController.StackFrames.syncWatchExpressions(); |
|
2174 }, |
|
2175 |
|
2176 /** |
|
2177 * Gets the watch expression code string for an item in this container. |
|
2178 * |
|
2179 * @param number aIndex |
|
2180 * The index used to identify the watch expression. |
|
2181 * @return string |
|
2182 * The watch expression code string. |
|
2183 */ |
|
2184 getString: function(aIndex) { |
|
2185 return this.getItemAtIndex(aIndex).attachment.currentExpression; |
|
2186 }, |
|
2187 |
|
2188 /** |
|
2189 * Gets the watch expressions code strings for all items in this container. |
|
2190 * |
|
2191 * @return array |
|
2192 * The watch expressions code strings. |
|
2193 */ |
|
2194 getAllStrings: function() { |
|
2195 return this.items.map(e => e.attachment.currentExpression); |
|
2196 }, |
|
2197 |
|
2198 /** |
|
2199 * Customization function for creating an item's UI. |
|
2200 * |
|
2201 * @param string aExpression |
|
2202 * The watch expression string. |
|
2203 */ |
|
2204 _createItemView: function(aExpression) { |
|
2205 let container = document.createElement("hbox"); |
|
2206 container.className = "list-widget-item dbg-expression"; |
|
2207 |
|
2208 let arrowNode = document.createElement("hbox"); |
|
2209 arrowNode.className = "dbg-expression-arrow"; |
|
2210 |
|
2211 let inputNode = document.createElement("textbox"); |
|
2212 inputNode.className = "plain dbg-expression-input devtools-monospace"; |
|
2213 inputNode.setAttribute("value", aExpression); |
|
2214 inputNode.setAttribute("flex", "1"); |
|
2215 |
|
2216 let closeNode = document.createElement("toolbarbutton"); |
|
2217 closeNode.className = "plain variables-view-delete"; |
|
2218 |
|
2219 closeNode.addEventListener("click", this._onClose, false); |
|
2220 inputNode.addEventListener("blur", this._onBlur, false); |
|
2221 inputNode.addEventListener("keypress", this._onKeyPress, false); |
|
2222 |
|
2223 container.appendChild(arrowNode); |
|
2224 container.appendChild(inputNode); |
|
2225 container.appendChild(closeNode); |
|
2226 |
|
2227 return { |
|
2228 container: container, |
|
2229 arrowNode: arrowNode, |
|
2230 inputNode: inputNode, |
|
2231 closeNode: closeNode |
|
2232 }; |
|
2233 }, |
|
2234 |
|
2235 /** |
|
2236 * Called when the add watch expression key sequence was pressed. |
|
2237 */ |
|
2238 _onCmdAddExpression: function(aText) { |
|
2239 // Only add a new expression if there's no pending input. |
|
2240 if (this.getAllStrings().indexOf("") == -1) { |
|
2241 this.addExpression(aText || DebuggerView.editor.getSelection()); |
|
2242 } |
|
2243 }, |
|
2244 |
|
2245 /** |
|
2246 * Called when the remove all watch expressions key sequence was pressed. |
|
2247 */ |
|
2248 _onCmdRemoveAllExpressions: function() { |
|
2249 // Empty the view of all the watch expressions and clear the cache. |
|
2250 this.empty(); |
|
2251 |
|
2252 // Synchronize with the controller's watch expressions store. |
|
2253 DebuggerController.StackFrames.syncWatchExpressions(); |
|
2254 }, |
|
2255 |
|
2256 /** |
|
2257 * The click listener for this container. |
|
2258 */ |
|
2259 _onClick: function(e) { |
|
2260 if (e.button != 0) { |
|
2261 // Only allow left-click to trigger this event. |
|
2262 return; |
|
2263 } |
|
2264 let expressionItem = this.getItemForElement(e.target); |
|
2265 if (!expressionItem) { |
|
2266 // The container is empty or we didn't click on an actual item. |
|
2267 this.addExpression(); |
|
2268 } |
|
2269 }, |
|
2270 |
|
2271 /** |
|
2272 * The click listener for a watch expression's close button. |
|
2273 */ |
|
2274 _onClose: function(e) { |
|
2275 // Remove the watch expression. |
|
2276 this.remove(this.getItemForElement(e.target)); |
|
2277 |
|
2278 // Synchronize with the controller's watch expressions store. |
|
2279 DebuggerController.StackFrames.syncWatchExpressions(); |
|
2280 |
|
2281 // Prevent clicking the expression element itself. |
|
2282 e.preventDefault(); |
|
2283 e.stopPropagation(); |
|
2284 }, |
|
2285 |
|
2286 /** |
|
2287 * The blur listener for a watch expression's textbox. |
|
2288 */ |
|
2289 _onBlur: function({ target: textbox }) { |
|
2290 let expressionItem = this.getItemForElement(textbox); |
|
2291 let oldExpression = expressionItem.attachment.currentExpression; |
|
2292 let newExpression = textbox.value.trim(); |
|
2293 |
|
2294 // Remove the watch expression if it's empty. |
|
2295 if (!newExpression) { |
|
2296 this.remove(expressionItem); |
|
2297 } |
|
2298 // Remove the watch expression if it's a duplicate. |
|
2299 else if (!oldExpression && this.getAllStrings().indexOf(newExpression) != -1) { |
|
2300 this.remove(expressionItem); |
|
2301 } |
|
2302 // Expression is eligible. |
|
2303 else { |
|
2304 expressionItem.attachment.currentExpression = newExpression; |
|
2305 } |
|
2306 |
|
2307 // Synchronize with the controller's watch expressions store. |
|
2308 DebuggerController.StackFrames.syncWatchExpressions(); |
|
2309 }, |
|
2310 |
|
2311 /** |
|
2312 * The keypress listener for a watch expression's textbox. |
|
2313 */ |
|
2314 _onKeyPress: function(e) { |
|
2315 switch(e.keyCode) { |
|
2316 case e.DOM_VK_RETURN: |
|
2317 case e.DOM_VK_ESCAPE: |
|
2318 e.stopPropagation(); |
|
2319 DebuggerView.editor.focus(); |
|
2320 return; |
|
2321 } |
|
2322 } |
|
2323 }); |
|
2324 |
|
2325 /** |
|
2326 * Functions handling the event listeners UI. |
|
2327 */ |
|
2328 function EventListenersView() { |
|
2329 dumpn("EventListenersView was instantiated"); |
|
2330 |
|
2331 this._onCheck = this._onCheck.bind(this); |
|
2332 this._onClick = this._onClick.bind(this); |
|
2333 } |
|
2334 |
|
2335 EventListenersView.prototype = Heritage.extend(WidgetMethods, { |
|
2336 /** |
|
2337 * Initialization function, called when the debugger is started. |
|
2338 */ |
|
2339 initialize: function() { |
|
2340 dumpn("Initializing the EventListenersView"); |
|
2341 |
|
2342 this.widget = new SideMenuWidget(document.getElementById("event-listeners"), { |
|
2343 showItemCheckboxes: true, |
|
2344 showGroupCheckboxes: true |
|
2345 }); |
|
2346 |
|
2347 this.emptyText = L10N.getStr("noEventListenersText"); |
|
2348 this._eventCheckboxTooltip = L10N.getStr("eventCheckboxTooltip"); |
|
2349 this._onSelectorString = " " + L10N.getStr("eventOnSelector") + " "; |
|
2350 this._inSourceString = " " + L10N.getStr("eventInSource") + " "; |
|
2351 this._inNativeCodeString = L10N.getStr("eventNative"); |
|
2352 |
|
2353 this.widget.addEventListener("check", this._onCheck, false); |
|
2354 this.widget.addEventListener("click", this._onClick, false); |
|
2355 }, |
|
2356 |
|
2357 /** |
|
2358 * Destruction function, called when the debugger is closed. |
|
2359 */ |
|
2360 destroy: function() { |
|
2361 dumpn("Destroying the EventListenersView"); |
|
2362 |
|
2363 this.widget.removeEventListener("check", this._onCheck, false); |
|
2364 this.widget.removeEventListener("click", this._onClick, false); |
|
2365 }, |
|
2366 |
|
2367 /** |
|
2368 * Adds an event to this event listeners container. |
|
2369 * |
|
2370 * @param object aListener |
|
2371 * The listener object coming from the active thread. |
|
2372 * @param object aOptions [optional] |
|
2373 * Additional options for adding the source. Supported options: |
|
2374 * - staged: true to stage the item to be appended later |
|
2375 */ |
|
2376 addListener: function(aListener, aOptions = {}) { |
|
2377 let { node: { selector }, function: { url }, type } = aListener; |
|
2378 if (!type) return; |
|
2379 |
|
2380 // Some listener objects may be added from plugins, thus getting |
|
2381 // translated to native code. |
|
2382 if (!url) { |
|
2383 url = this._inNativeCodeString; |
|
2384 } |
|
2385 |
|
2386 // If an event item for this listener's url and type was already added, |
|
2387 // avoid polluting the view and simply increase the "targets" count. |
|
2388 let eventItem = this.getItemForPredicate(aItem => |
|
2389 aItem.attachment.url == url && |
|
2390 aItem.attachment.type == type); |
|
2391 if (eventItem) { |
|
2392 let { selectors, view: { targets } } = eventItem.attachment; |
|
2393 if (selectors.indexOf(selector) == -1) { |
|
2394 selectors.push(selector); |
|
2395 targets.setAttribute("value", L10N.getFormatStr("eventNodes", selectors.length)); |
|
2396 } |
|
2397 return; |
|
2398 } |
|
2399 |
|
2400 // There's no easy way of grouping event types into higher-level groups, |
|
2401 // so we need to do this by hand. |
|
2402 let is = (...args) => args.indexOf(type) != -1; |
|
2403 let has = str => type.contains(str); |
|
2404 let starts = str => type.startsWith(str); |
|
2405 let group; |
|
2406 |
|
2407 if (starts("animation")) { |
|
2408 group = L10N.getStr("animationEvents"); |
|
2409 } else if (starts("audio")) { |
|
2410 group = L10N.getStr("audioEvents"); |
|
2411 } else if (is("levelchange")) { |
|
2412 group = L10N.getStr("batteryEvents"); |
|
2413 } else if (is("cut", "copy", "paste")) { |
|
2414 group = L10N.getStr("clipboardEvents"); |
|
2415 } else if (starts("composition")) { |
|
2416 group = L10N.getStr("compositionEvents"); |
|
2417 } else if (starts("device")) { |
|
2418 group = L10N.getStr("deviceEvents"); |
|
2419 } else if (is("fullscreenchange", "fullscreenerror", "orientationchange", |
|
2420 "overflow", "resize", "scroll", "underflow", "zoom")) { |
|
2421 group = L10N.getStr("displayEvents"); |
|
2422 } else if (starts("drag") || starts("drop")) { |
|
2423 group = L10N.getStr("Drag and dropEvents"); |
|
2424 } else if (starts("gamepad")) { |
|
2425 group = L10N.getStr("gamepadEvents"); |
|
2426 } else if (is("canplay", "canplaythrough", "durationchange", "emptied", |
|
2427 "ended", "loadeddata", "loadedmetadata", "pause", "play", "playing", |
|
2428 "ratechange", "seeked", "seeking", "stalled", "suspend", "timeupdate", |
|
2429 "volumechange", "waiting")) { |
|
2430 group = L10N.getStr("mediaEvents"); |
|
2431 } else if (is("blocked", "complete", "success", "upgradeneeded", "versionchange")) { |
|
2432 group = L10N.getStr("indexedDBEvents"); |
|
2433 } else if (is("blur", "change", "focus", "focusin", "focusout", "invalid", |
|
2434 "reset", "select", "submit")) { |
|
2435 group = L10N.getStr("interactionEvents"); |
|
2436 } else if (starts("key") || is("input")) { |
|
2437 group = L10N.getStr("keyboardEvents"); |
|
2438 } else if (starts("mouse") || has("click") || is("contextmenu", "show", "wheel")) { |
|
2439 group = L10N.getStr("mouseEvents"); |
|
2440 } else if (starts("DOM")) { |
|
2441 group = L10N.getStr("mutationEvents"); |
|
2442 } else if (is("abort", "error", "hashchange", "load", "loadend", "loadstart", |
|
2443 "pagehide", "pageshow", "progress", "timeout", "unload", "uploadprogress", |
|
2444 "visibilitychange")) { |
|
2445 group = L10N.getStr("navigationEvents"); |
|
2446 } else if (is("pointerlockchange", "pointerlockerror")) { |
|
2447 group = L10N.getStr("Pointer lockEvents"); |
|
2448 } else if (is("compassneedscalibration", "userproximity")) { |
|
2449 group = L10N.getStr("sensorEvents"); |
|
2450 } else if (starts("storage")) { |
|
2451 group = L10N.getStr("storageEvents"); |
|
2452 } else if (is("beginEvent", "endEvent", "repeatEvent")) { |
|
2453 group = L10N.getStr("timeEvents"); |
|
2454 } else if (starts("touch")) { |
|
2455 group = L10N.getStr("touchEvents"); |
|
2456 } else { |
|
2457 group = L10N.getStr("otherEvents"); |
|
2458 } |
|
2459 |
|
2460 // Create the element node for the event listener item. |
|
2461 let itemView = this._createItemView(type, selector, url); |
|
2462 |
|
2463 // Event breakpoints survive target navigations. Make sure the newly |
|
2464 // inserted event item is correctly checked. |
|
2465 let checkboxState = |
|
2466 DebuggerController.Breakpoints.DOM.activeEventNames.indexOf(type) != -1; |
|
2467 |
|
2468 // Append an event listener item to this container. |
|
2469 this.push([itemView.container], { |
|
2470 staged: aOptions.staged, /* stage the item to be appended later? */ |
|
2471 attachment: { |
|
2472 url: url, |
|
2473 type: type, |
|
2474 view: itemView, |
|
2475 selectors: [selector], |
|
2476 group: group, |
|
2477 checkboxState: checkboxState, |
|
2478 checkboxTooltip: this._eventCheckboxTooltip |
|
2479 } |
|
2480 }); |
|
2481 }, |
|
2482 |
|
2483 /** |
|
2484 * Gets all the event types known to this container. |
|
2485 * |
|
2486 * @return array |
|
2487 * List of event types, for example ["load", "click"...] |
|
2488 */ |
|
2489 getAllEvents: function() { |
|
2490 return this.attachments.map(e => e.type); |
|
2491 }, |
|
2492 |
|
2493 /** |
|
2494 * Gets the checked event types in this container. |
|
2495 * |
|
2496 * @return array |
|
2497 * List of event types, for example ["load", "click"...] |
|
2498 */ |
|
2499 getCheckedEvents: function() { |
|
2500 return this.attachments.filter(e => e.checkboxState).map(e => e.type); |
|
2501 }, |
|
2502 |
|
2503 /** |
|
2504 * Customization function for creating an item's UI. |
|
2505 * |
|
2506 * @param string aType |
|
2507 * The event type, for example "click". |
|
2508 * @param string aSelector |
|
2509 * The target element's selector. |
|
2510 * @param string url |
|
2511 * The source url in which the event listener is located. |
|
2512 * @return object |
|
2513 * An object containing the event listener view nodes. |
|
2514 */ |
|
2515 _createItemView: function(aType, aSelector, aUrl) { |
|
2516 let container = document.createElement("hbox"); |
|
2517 container.className = "dbg-event-listener"; |
|
2518 |
|
2519 let eventType = document.createElement("label"); |
|
2520 eventType.className = "plain dbg-event-listener-type"; |
|
2521 eventType.setAttribute("value", aType); |
|
2522 container.appendChild(eventType); |
|
2523 |
|
2524 let typeSeparator = document.createElement("label"); |
|
2525 typeSeparator.className = "plain dbg-event-listener-separator"; |
|
2526 typeSeparator.setAttribute("value", this._onSelectorString); |
|
2527 container.appendChild(typeSeparator); |
|
2528 |
|
2529 let eventTargets = document.createElement("label"); |
|
2530 eventTargets.className = "plain dbg-event-listener-targets"; |
|
2531 eventTargets.setAttribute("value", aSelector); |
|
2532 container.appendChild(eventTargets); |
|
2533 |
|
2534 let selectorSeparator = document.createElement("label"); |
|
2535 selectorSeparator.className = "plain dbg-event-listener-separator"; |
|
2536 selectorSeparator.setAttribute("value", this._inSourceString); |
|
2537 container.appendChild(selectorSeparator); |
|
2538 |
|
2539 let eventLocation = document.createElement("label"); |
|
2540 eventLocation.className = "plain dbg-event-listener-location"; |
|
2541 eventLocation.setAttribute("value", SourceUtils.getSourceLabel(aUrl)); |
|
2542 eventLocation.setAttribute("flex", "1"); |
|
2543 eventLocation.setAttribute("crop", "center"); |
|
2544 container.appendChild(eventLocation); |
|
2545 |
|
2546 return { |
|
2547 container: container, |
|
2548 type: eventType, |
|
2549 targets: eventTargets, |
|
2550 location: eventLocation |
|
2551 }; |
|
2552 }, |
|
2553 |
|
2554 /** |
|
2555 * The check listener for the event listeners container. |
|
2556 */ |
|
2557 _onCheck: function({ detail: { description, checked }, target }) { |
|
2558 if (description == "item") { |
|
2559 this.getItemForElement(target).attachment.checkboxState = checked; |
|
2560 DebuggerController.Breakpoints.DOM.scheduleEventBreakpointsUpdate(); |
|
2561 return; |
|
2562 } |
|
2563 |
|
2564 // Check all the event items in this group. |
|
2565 this.items |
|
2566 .filter(e => e.attachment.group == description) |
|
2567 .forEach(e => this.callMethod("checkItem", e.target, checked)); |
|
2568 }, |
|
2569 |
|
2570 /** |
|
2571 * The select listener for the event listeners container. |
|
2572 */ |
|
2573 _onClick: function({ target }) { |
|
2574 // Changing the checkbox state is handled by the _onCheck event. Avoid |
|
2575 // handling that again in this click event, so pass in "noSiblings" |
|
2576 // when retrieving the target's item, to ignore the checkbox. |
|
2577 let eventItem = this.getItemForElement(target, { noSiblings: true }); |
|
2578 if (eventItem) { |
|
2579 let newState = eventItem.attachment.checkboxState ^= 1; |
|
2580 this.callMethod("checkItem", eventItem.target, newState); |
|
2581 } |
|
2582 }, |
|
2583 |
|
2584 _eventCheckboxTooltip: "", |
|
2585 _onSelectorString: "", |
|
2586 _inSourceString: "", |
|
2587 _inNativeCodeString: "" |
|
2588 }); |
|
2589 |
|
2590 /** |
|
2591 * Functions handling the global search UI. |
|
2592 */ |
|
2593 function GlobalSearchView() { |
|
2594 dumpn("GlobalSearchView was instantiated"); |
|
2595 |
|
2596 this._onHeaderClick = this._onHeaderClick.bind(this); |
|
2597 this._onLineClick = this._onLineClick.bind(this); |
|
2598 this._onMatchClick = this._onMatchClick.bind(this); |
|
2599 } |
|
2600 |
|
2601 GlobalSearchView.prototype = Heritage.extend(WidgetMethods, { |
|
2602 /** |
|
2603 * Initialization function, called when the debugger is started. |
|
2604 */ |
|
2605 initialize: function() { |
|
2606 dumpn("Initializing the GlobalSearchView"); |
|
2607 |
|
2608 this.widget = new SimpleListWidget(document.getElementById("globalsearch")); |
|
2609 this._splitter = document.querySelector("#globalsearch + .devtools-horizontal-splitter"); |
|
2610 |
|
2611 this.emptyText = L10N.getStr("noMatchingStringsText"); |
|
2612 }, |
|
2613 |
|
2614 /** |
|
2615 * Destruction function, called when the debugger is closed. |
|
2616 */ |
|
2617 destroy: function() { |
|
2618 dumpn("Destroying the GlobalSearchView"); |
|
2619 }, |
|
2620 |
|
2621 /** |
|
2622 * Sets the results container hidden or visible. It's hidden by default. |
|
2623 * @param boolean aFlag |
|
2624 */ |
|
2625 set hidden(aFlag) { |
|
2626 this.widget.setAttribute("hidden", aFlag); |
|
2627 this._splitter.setAttribute("hidden", aFlag); |
|
2628 }, |
|
2629 |
|
2630 /** |
|
2631 * Gets the visibility state of the global search container. |
|
2632 * @return boolean |
|
2633 */ |
|
2634 get hidden() |
|
2635 this.widget.getAttribute("hidden") == "true" || |
|
2636 this._splitter.getAttribute("hidden") == "true", |
|
2637 |
|
2638 /** |
|
2639 * Hides and removes all items from this search container. |
|
2640 */ |
|
2641 clearView: function() { |
|
2642 this.hidden = true; |
|
2643 this.empty(); |
|
2644 }, |
|
2645 |
|
2646 /** |
|
2647 * Selects the next found item in this container. |
|
2648 * Does not change the currently focused node. |
|
2649 */ |
|
2650 selectNext: function() { |
|
2651 let totalLineResults = LineResults.size(); |
|
2652 if (!totalLineResults) { |
|
2653 return; |
|
2654 } |
|
2655 if (++this._currentlyFocusedMatch >= totalLineResults) { |
|
2656 this._currentlyFocusedMatch = 0; |
|
2657 } |
|
2658 this._onMatchClick({ |
|
2659 target: LineResults.getElementAtIndex(this._currentlyFocusedMatch) |
|
2660 }); |
|
2661 }, |
|
2662 |
|
2663 /** |
|
2664 * Selects the previously found item in this container. |
|
2665 * Does not change the currently focused node. |
|
2666 */ |
|
2667 selectPrev: function() { |
|
2668 let totalLineResults = LineResults.size(); |
|
2669 if (!totalLineResults) { |
|
2670 return; |
|
2671 } |
|
2672 if (--this._currentlyFocusedMatch < 0) { |
|
2673 this._currentlyFocusedMatch = totalLineResults - 1; |
|
2674 } |
|
2675 this._onMatchClick({ |
|
2676 target: LineResults.getElementAtIndex(this._currentlyFocusedMatch) |
|
2677 }); |
|
2678 }, |
|
2679 |
|
2680 /** |
|
2681 * Schedules searching for a string in all of the sources. |
|
2682 * |
|
2683 * @param string aToken |
|
2684 * The string to search for. |
|
2685 * @param number aWait |
|
2686 * The amount of milliseconds to wait until draining. |
|
2687 */ |
|
2688 scheduleSearch: function(aToken, aWait) { |
|
2689 // The amount of time to wait for the requests to settle. |
|
2690 let maxDelay = GLOBAL_SEARCH_ACTION_MAX_DELAY; |
|
2691 let delay = aWait === undefined ? maxDelay / aToken.length : aWait; |
|
2692 |
|
2693 // Allow requests to settle down first. |
|
2694 setNamedTimeout("global-search", delay, () => { |
|
2695 // Start fetching as many sources as possible, then perform the search. |
|
2696 let urls = DebuggerView.Sources.values; |
|
2697 let sourcesFetched = DebuggerController.SourceScripts.getTextForSources(urls); |
|
2698 sourcesFetched.then(aSources => this._doSearch(aToken, aSources)); |
|
2699 }); |
|
2700 }, |
|
2701 |
|
2702 /** |
|
2703 * Finds string matches in all the sources stored in the controller's cache, |
|
2704 * and groups them by url and line number. |
|
2705 * |
|
2706 * @param string aToken |
|
2707 * The string to search for. |
|
2708 * @param array aSources |
|
2709 * An array of [url, text] tuples for each source. |
|
2710 */ |
|
2711 _doSearch: function(aToken, aSources) { |
|
2712 // Don't continue filtering if the searched token is an empty string. |
|
2713 if (!aToken) { |
|
2714 this.clearView(); |
|
2715 return; |
|
2716 } |
|
2717 |
|
2718 // Search is not case sensitive, prepare the actual searched token. |
|
2719 let lowerCaseToken = aToken.toLowerCase(); |
|
2720 let tokenLength = aToken.length; |
|
2721 |
|
2722 // Create a Map containing search details for each source. |
|
2723 let globalResults = new GlobalResults(); |
|
2724 |
|
2725 // Search for the specified token in each source's text. |
|
2726 for (let [url, text] of aSources) { |
|
2727 // Verify that the search token is found anywhere in the source. |
|
2728 if (!text.toLowerCase().contains(lowerCaseToken)) { |
|
2729 continue; |
|
2730 } |
|
2731 // ...and if so, create a Map containing search details for each line. |
|
2732 let sourceResults = new SourceResults(url, globalResults); |
|
2733 |
|
2734 // Search for the specified token in each line's text. |
|
2735 text.split("\n").forEach((aString, aLine) => { |
|
2736 // Search is not case sensitive, prepare the actual searched line. |
|
2737 let lowerCaseLine = aString.toLowerCase(); |
|
2738 |
|
2739 // Verify that the search token is found anywhere in this line. |
|
2740 if (!lowerCaseLine.contains(lowerCaseToken)) { |
|
2741 return; |
|
2742 } |
|
2743 // ...and if so, create a Map containing search details for each word. |
|
2744 let lineResults = new LineResults(aLine, sourceResults); |
|
2745 |
|
2746 // Search for the specified token this line's text. |
|
2747 lowerCaseLine.split(lowerCaseToken).reduce((aPrev, aCurr, aIndex, aArray) => { |
|
2748 let prevLength = aPrev.length; |
|
2749 let currLength = aCurr.length; |
|
2750 |
|
2751 // Everything before the token is unmatched. |
|
2752 let unmatched = aString.substr(prevLength, currLength); |
|
2753 lineResults.add(unmatched); |
|
2754 |
|
2755 // The lowered-case line was split by the lowered-case token. So, |
|
2756 // get the actual matched text from the original line's text. |
|
2757 if (aIndex != aArray.length - 1) { |
|
2758 let matched = aString.substr(prevLength + currLength, tokenLength); |
|
2759 let range = { start: prevLength + currLength, length: matched.length }; |
|
2760 lineResults.add(matched, range, true); |
|
2761 } |
|
2762 |
|
2763 // Continue with the next sub-region in this line's text. |
|
2764 return aPrev + aToken + aCurr; |
|
2765 }, ""); |
|
2766 |
|
2767 if (lineResults.matchCount) { |
|
2768 sourceResults.add(lineResults); |
|
2769 } |
|
2770 }); |
|
2771 |
|
2772 if (sourceResults.matchCount) { |
|
2773 globalResults.add(sourceResults); |
|
2774 } |
|
2775 } |
|
2776 |
|
2777 // Rebuild the results, then signal if there are any matches. |
|
2778 if (globalResults.matchCount) { |
|
2779 this.hidden = false; |
|
2780 this._currentlyFocusedMatch = -1; |
|
2781 this._createGlobalResultsUI(globalResults); |
|
2782 window.emit(EVENTS.GLOBAL_SEARCH_MATCH_FOUND); |
|
2783 } else { |
|
2784 window.emit(EVENTS.GLOBAL_SEARCH_MATCH_NOT_FOUND); |
|
2785 } |
|
2786 }, |
|
2787 |
|
2788 /** |
|
2789 * Creates global search results entries and adds them to this container. |
|
2790 * |
|
2791 * @param GlobalResults aGlobalResults |
|
2792 * An object containing all source results, grouped by source location. |
|
2793 */ |
|
2794 _createGlobalResultsUI: function(aGlobalResults) { |
|
2795 let i = 0; |
|
2796 |
|
2797 for (let sourceResults of aGlobalResults) { |
|
2798 if (i++ == 0) { |
|
2799 this._createSourceResultsUI(sourceResults); |
|
2800 } else { |
|
2801 // Dispatch subsequent document manipulation operations, to avoid |
|
2802 // blocking the main thread when a large number of search results |
|
2803 // is found, thus giving the impression of faster searching. |
|
2804 Services.tm.currentThread.dispatch({ run: |
|
2805 this._createSourceResultsUI.bind(this, sourceResults) |
|
2806 }, 0); |
|
2807 } |
|
2808 } |
|
2809 }, |
|
2810 |
|
2811 /** |
|
2812 * Creates source search results entries and adds them to this container. |
|
2813 * |
|
2814 * @param SourceResults aSourceResults |
|
2815 * An object containing all the matched lines for a specific source. |
|
2816 */ |
|
2817 _createSourceResultsUI: function(aSourceResults) { |
|
2818 // Create the element node for the source results item. |
|
2819 let container = document.createElement("hbox"); |
|
2820 aSourceResults.createView(container, { |
|
2821 onHeaderClick: this._onHeaderClick, |
|
2822 onLineClick: this._onLineClick, |
|
2823 onMatchClick: this._onMatchClick |
|
2824 }); |
|
2825 |
|
2826 // Append a source results item to this container. |
|
2827 let item = this.push([container], { |
|
2828 index: -1, /* specifies on which position should the item be appended */ |
|
2829 attachment: { |
|
2830 sourceResults: aSourceResults |
|
2831 } |
|
2832 }); |
|
2833 }, |
|
2834 |
|
2835 /** |
|
2836 * The click listener for a results header. |
|
2837 */ |
|
2838 _onHeaderClick: function(e) { |
|
2839 let sourceResultsItem = SourceResults.getItemForElement(e.target); |
|
2840 sourceResultsItem.instance.toggle(e); |
|
2841 }, |
|
2842 |
|
2843 /** |
|
2844 * The click listener for a results line. |
|
2845 */ |
|
2846 _onLineClick: function(e) { |
|
2847 let lineResultsItem = LineResults.getItemForElement(e.target); |
|
2848 this._onMatchClick({ target: lineResultsItem.firstMatch }); |
|
2849 }, |
|
2850 |
|
2851 /** |
|
2852 * The click listener for a result match. |
|
2853 */ |
|
2854 _onMatchClick: function(e) { |
|
2855 if (e instanceof Event) { |
|
2856 e.preventDefault(); |
|
2857 e.stopPropagation(); |
|
2858 } |
|
2859 |
|
2860 let target = e.target; |
|
2861 let sourceResultsItem = SourceResults.getItemForElement(target); |
|
2862 let lineResultsItem = LineResults.getItemForElement(target); |
|
2863 |
|
2864 sourceResultsItem.instance.expand(); |
|
2865 this._currentlyFocusedMatch = LineResults.indexOfElement(target); |
|
2866 this._scrollMatchIntoViewIfNeeded(target); |
|
2867 this._bounceMatch(target); |
|
2868 |
|
2869 let url = sourceResultsItem.instance.url; |
|
2870 let line = lineResultsItem.instance.line; |
|
2871 |
|
2872 DebuggerView.setEditorLocation(url, line + 1, { noDebug: true }); |
|
2873 |
|
2874 let range = lineResultsItem.lineData.range; |
|
2875 let cursor = DebuggerView.editor.getOffset({ line: line, ch: 0 }); |
|
2876 let [ anchor, head ] = DebuggerView.editor.getPosition( |
|
2877 cursor + range.start, |
|
2878 cursor + range.start + range.length |
|
2879 ); |
|
2880 |
|
2881 DebuggerView.editor.setSelection(anchor, head); |
|
2882 }, |
|
2883 |
|
2884 /** |
|
2885 * Scrolls a match into view if not already visible. |
|
2886 * |
|
2887 * @param nsIDOMNode aMatch |
|
2888 * The match to scroll into view. |
|
2889 */ |
|
2890 _scrollMatchIntoViewIfNeeded: function(aMatch) { |
|
2891 this.widget.ensureElementIsVisible(aMatch); |
|
2892 }, |
|
2893 |
|
2894 /** |
|
2895 * Starts a bounce animation for a match. |
|
2896 * |
|
2897 * @param nsIDOMNode aMatch |
|
2898 * The match to start a bounce animation for. |
|
2899 */ |
|
2900 _bounceMatch: function(aMatch) { |
|
2901 Services.tm.currentThread.dispatch({ run: () => { |
|
2902 aMatch.addEventListener("transitionend", function onEvent() { |
|
2903 aMatch.removeEventListener("transitionend", onEvent); |
|
2904 aMatch.removeAttribute("focused"); |
|
2905 }); |
|
2906 aMatch.setAttribute("focused", ""); |
|
2907 }}, 0); |
|
2908 aMatch.setAttribute("focusing", ""); |
|
2909 }, |
|
2910 |
|
2911 _splitter: null, |
|
2912 _currentlyFocusedMatch: -1, |
|
2913 _forceExpandResults: false |
|
2914 }); |
|
2915 |
|
2916 /** |
|
2917 * An object containing all source results, grouped by source location. |
|
2918 * Iterable via "for (let [location, sourceResults] of globalResults) { }". |
|
2919 */ |
|
2920 function GlobalResults() { |
|
2921 this._store = []; |
|
2922 SourceResults._itemsByElement = new Map(); |
|
2923 LineResults._itemsByElement = new Map(); |
|
2924 } |
|
2925 |
|
2926 GlobalResults.prototype = { |
|
2927 /** |
|
2928 * Adds source results to this store. |
|
2929 * |
|
2930 * @param SourceResults aSourceResults |
|
2931 * An object containing search results for a specific source. |
|
2932 */ |
|
2933 add: function(aSourceResults) { |
|
2934 this._store.push(aSourceResults); |
|
2935 }, |
|
2936 |
|
2937 /** |
|
2938 * Gets the number of source results in this store. |
|
2939 */ |
|
2940 get matchCount() this._store.length |
|
2941 }; |
|
2942 |
|
2943 /** |
|
2944 * An object containing all the matched lines for a specific source. |
|
2945 * Iterable via "for (let [lineNumber, lineResults] of sourceResults) { }". |
|
2946 * |
|
2947 * @param string aUrl |
|
2948 * The target source url. |
|
2949 * @param GlobalResults aGlobalResults |
|
2950 * An object containing all source results, grouped by source location. |
|
2951 */ |
|
2952 function SourceResults(aUrl, aGlobalResults) { |
|
2953 this.url = aUrl; |
|
2954 this._globalResults = aGlobalResults; |
|
2955 this._store = []; |
|
2956 } |
|
2957 |
|
2958 SourceResults.prototype = { |
|
2959 /** |
|
2960 * Adds line results to this store. |
|
2961 * |
|
2962 * @param LineResults aLineResults |
|
2963 * An object containing search results for a specific line. |
|
2964 */ |
|
2965 add: function(aLineResults) { |
|
2966 this._store.push(aLineResults); |
|
2967 }, |
|
2968 |
|
2969 /** |
|
2970 * Gets the number of line results in this store. |
|
2971 */ |
|
2972 get matchCount() this._store.length, |
|
2973 |
|
2974 /** |
|
2975 * Expands the element, showing all the added details. |
|
2976 */ |
|
2977 expand: function() { |
|
2978 this._resultsContainer.removeAttribute("hidden"); |
|
2979 this._arrow.setAttribute("open", ""); |
|
2980 }, |
|
2981 |
|
2982 /** |
|
2983 * Collapses the element, hiding all the added details. |
|
2984 */ |
|
2985 collapse: function() { |
|
2986 this._resultsContainer.setAttribute("hidden", "true"); |
|
2987 this._arrow.removeAttribute("open"); |
|
2988 }, |
|
2989 |
|
2990 /** |
|
2991 * Toggles between the element collapse/expand state. |
|
2992 */ |
|
2993 toggle: function(e) { |
|
2994 this.expanded ^= 1; |
|
2995 }, |
|
2996 |
|
2997 /** |
|
2998 * Gets this element's expanded state. |
|
2999 * @return boolean |
|
3000 */ |
|
3001 get expanded() |
|
3002 this._resultsContainer.getAttribute("hidden") != "true" && |
|
3003 this._arrow.hasAttribute("open"), |
|
3004 |
|
3005 /** |
|
3006 * Sets this element's expanded state. |
|
3007 * @param boolean aFlag |
|
3008 */ |
|
3009 set expanded(aFlag) this[aFlag ? "expand" : "collapse"](), |
|
3010 |
|
3011 /** |
|
3012 * Gets the element associated with this item. |
|
3013 * @return nsIDOMNode |
|
3014 */ |
|
3015 get target() this._target, |
|
3016 |
|
3017 /** |
|
3018 * Customization function for creating this item's UI. |
|
3019 * |
|
3020 * @param nsIDOMNode aElementNode |
|
3021 * The element associated with the displayed item. |
|
3022 * @param object aCallbacks |
|
3023 * An object containing all the necessary callback functions: |
|
3024 * - onHeaderClick |
|
3025 * - onMatchClick |
|
3026 */ |
|
3027 createView: function(aElementNode, aCallbacks) { |
|
3028 this._target = aElementNode; |
|
3029 |
|
3030 let arrow = this._arrow = document.createElement("box"); |
|
3031 arrow.className = "arrow"; |
|
3032 |
|
3033 let locationNode = document.createElement("label"); |
|
3034 locationNode.className = "plain dbg-results-header-location"; |
|
3035 locationNode.setAttribute("value", this.url); |
|
3036 |
|
3037 let matchCountNode = document.createElement("label"); |
|
3038 matchCountNode.className = "plain dbg-results-header-match-count"; |
|
3039 matchCountNode.setAttribute("value", "(" + this.matchCount + ")"); |
|
3040 |
|
3041 let resultsHeader = this._resultsHeader = document.createElement("hbox"); |
|
3042 resultsHeader.className = "dbg-results-header"; |
|
3043 resultsHeader.setAttribute("align", "center") |
|
3044 resultsHeader.appendChild(arrow); |
|
3045 resultsHeader.appendChild(locationNode); |
|
3046 resultsHeader.appendChild(matchCountNode); |
|
3047 resultsHeader.addEventListener("click", aCallbacks.onHeaderClick, false); |
|
3048 |
|
3049 let resultsContainer = this._resultsContainer = document.createElement("vbox"); |
|
3050 resultsContainer.className = "dbg-results-container"; |
|
3051 resultsContainer.setAttribute("hidden", "true"); |
|
3052 |
|
3053 // Create lines search results entries and add them to this container. |
|
3054 // Afterwards, if the number of matches is reasonable, expand this |
|
3055 // container automatically. |
|
3056 for (let lineResults of this._store) { |
|
3057 lineResults.createView(resultsContainer, aCallbacks); |
|
3058 } |
|
3059 if (this.matchCount < GLOBAL_SEARCH_EXPAND_MAX_RESULTS) { |
|
3060 this.expand(); |
|
3061 } |
|
3062 |
|
3063 let resultsBox = document.createElement("vbox"); |
|
3064 resultsBox.setAttribute("flex", "1"); |
|
3065 resultsBox.appendChild(resultsHeader); |
|
3066 resultsBox.appendChild(resultsContainer); |
|
3067 |
|
3068 aElementNode.id = "source-results-" + this.url; |
|
3069 aElementNode.className = "dbg-source-results"; |
|
3070 aElementNode.appendChild(resultsBox); |
|
3071 |
|
3072 SourceResults._itemsByElement.set(aElementNode, { instance: this }); |
|
3073 }, |
|
3074 |
|
3075 url: "", |
|
3076 _globalResults: null, |
|
3077 _store: null, |
|
3078 _target: null, |
|
3079 _arrow: null, |
|
3080 _resultsHeader: null, |
|
3081 _resultsContainer: null |
|
3082 }; |
|
3083 |
|
3084 /** |
|
3085 * An object containing all the matches for a specific line. |
|
3086 * Iterable via "for (let chunk of lineResults) { }". |
|
3087 * |
|
3088 * @param number aLine |
|
3089 * The target line in the source. |
|
3090 * @param SourceResults aSourceResults |
|
3091 * An object containing all the matched lines for a specific source. |
|
3092 */ |
|
3093 function LineResults(aLine, aSourceResults) { |
|
3094 this.line = aLine; |
|
3095 this._sourceResults = aSourceResults; |
|
3096 this._store = []; |
|
3097 this._matchCount = 0; |
|
3098 } |
|
3099 |
|
3100 LineResults.prototype = { |
|
3101 /** |
|
3102 * Adds string details to this store. |
|
3103 * |
|
3104 * @param string aString |
|
3105 * The text contents chunk in the line. |
|
3106 * @param object aRange |
|
3107 * An object containing the { start, length } of the chunk. |
|
3108 * @param boolean aMatchFlag |
|
3109 * True if the chunk is a matched string, false if just text content. |
|
3110 */ |
|
3111 add: function(aString, aRange, aMatchFlag) { |
|
3112 this._store.push({ string: aString, range: aRange, match: !!aMatchFlag }); |
|
3113 this._matchCount += aMatchFlag ? 1 : 0; |
|
3114 }, |
|
3115 |
|
3116 /** |
|
3117 * Gets the number of word results in this store. |
|
3118 */ |
|
3119 get matchCount() this._matchCount, |
|
3120 |
|
3121 /** |
|
3122 * Gets the element associated with this item. |
|
3123 * @return nsIDOMNode |
|
3124 */ |
|
3125 get target() this._target, |
|
3126 |
|
3127 /** |
|
3128 * Customization function for creating this item's UI. |
|
3129 * |
|
3130 * @param nsIDOMNode aElementNode |
|
3131 * The element associated with the displayed item. |
|
3132 * @param object aCallbacks |
|
3133 * An object containing all the necessary callback functions: |
|
3134 * - onMatchClick |
|
3135 * - onLineClick |
|
3136 */ |
|
3137 createView: function(aElementNode, aCallbacks) { |
|
3138 this._target = aElementNode; |
|
3139 |
|
3140 let lineNumberNode = document.createElement("label"); |
|
3141 lineNumberNode.className = "plain dbg-results-line-number"; |
|
3142 lineNumberNode.classList.add("devtools-monospace"); |
|
3143 lineNumberNode.setAttribute("value", this.line + 1); |
|
3144 |
|
3145 let lineContentsNode = document.createElement("hbox"); |
|
3146 lineContentsNode.className = "dbg-results-line-contents"; |
|
3147 lineContentsNode.classList.add("devtools-monospace"); |
|
3148 lineContentsNode.setAttribute("flex", "1"); |
|
3149 |
|
3150 let lineString = ""; |
|
3151 let lineLength = 0; |
|
3152 let firstMatch = null; |
|
3153 |
|
3154 for (let lineChunk of this._store) { |
|
3155 let { string, range, match } = lineChunk; |
|
3156 lineString = string.substr(0, GLOBAL_SEARCH_LINE_MAX_LENGTH - lineLength); |
|
3157 lineLength += string.length; |
|
3158 |
|
3159 let lineChunkNode = document.createElement("label"); |
|
3160 lineChunkNode.className = "plain dbg-results-line-contents-string"; |
|
3161 lineChunkNode.setAttribute("value", lineString); |
|
3162 lineChunkNode.setAttribute("match", match); |
|
3163 lineContentsNode.appendChild(lineChunkNode); |
|
3164 |
|
3165 if (match) { |
|
3166 this._entangleMatch(lineChunkNode, lineChunk); |
|
3167 lineChunkNode.addEventListener("click", aCallbacks.onMatchClick, false); |
|
3168 firstMatch = firstMatch || lineChunkNode; |
|
3169 } |
|
3170 if (lineLength >= GLOBAL_SEARCH_LINE_MAX_LENGTH) { |
|
3171 lineContentsNode.appendChild(this._ellipsis.cloneNode(true)); |
|
3172 break; |
|
3173 } |
|
3174 } |
|
3175 |
|
3176 this._entangleLine(lineContentsNode, firstMatch); |
|
3177 lineContentsNode.addEventListener("click", aCallbacks.onLineClick, false); |
|
3178 |
|
3179 let searchResult = document.createElement("hbox"); |
|
3180 searchResult.className = "dbg-search-result"; |
|
3181 searchResult.appendChild(lineNumberNode); |
|
3182 searchResult.appendChild(lineContentsNode); |
|
3183 |
|
3184 aElementNode.appendChild(searchResult); |
|
3185 }, |
|
3186 |
|
3187 /** |
|
3188 * Handles a match while creating the view. |
|
3189 * @param nsIDOMNode aNode |
|
3190 * @param object aMatchChunk |
|
3191 */ |
|
3192 _entangleMatch: function(aNode, aMatchChunk) { |
|
3193 LineResults._itemsByElement.set(aNode, { |
|
3194 instance: this, |
|
3195 lineData: aMatchChunk |
|
3196 }); |
|
3197 }, |
|
3198 |
|
3199 /** |
|
3200 * Handles a line while creating the view. |
|
3201 * @param nsIDOMNode aNode |
|
3202 * @param nsIDOMNode aFirstMatch |
|
3203 */ |
|
3204 _entangleLine: function(aNode, aFirstMatch) { |
|
3205 LineResults._itemsByElement.set(aNode, { |
|
3206 instance: this, |
|
3207 firstMatch: aFirstMatch, |
|
3208 ignored: true |
|
3209 }); |
|
3210 }, |
|
3211 |
|
3212 /** |
|
3213 * An nsIDOMNode label with an ellipsis value. |
|
3214 */ |
|
3215 _ellipsis: (function() { |
|
3216 let label = document.createElement("label"); |
|
3217 label.className = "plain dbg-results-line-contents-string"; |
|
3218 label.setAttribute("value", L10N.ellipsis); |
|
3219 return label; |
|
3220 })(), |
|
3221 |
|
3222 line: 0, |
|
3223 _sourceResults: null, |
|
3224 _store: null, |
|
3225 _target: null |
|
3226 }; |
|
3227 |
|
3228 /** |
|
3229 * A generator-iterator over the global, source or line results. |
|
3230 */ |
|
3231 GlobalResults.prototype["@@iterator"] = |
|
3232 SourceResults.prototype["@@iterator"] = |
|
3233 LineResults.prototype["@@iterator"] = function*() { |
|
3234 yield* this._store; |
|
3235 }; |
|
3236 |
|
3237 /** |
|
3238 * Gets the item associated with the specified element. |
|
3239 * |
|
3240 * @param nsIDOMNode aElement |
|
3241 * The element used to identify the item. |
|
3242 * @return object |
|
3243 * The matched item, or null if nothing is found. |
|
3244 */ |
|
3245 SourceResults.getItemForElement = |
|
3246 LineResults.getItemForElement = function(aElement) { |
|
3247 return WidgetMethods.getItemForElement.call(this, aElement, { noSiblings: true }); |
|
3248 }; |
|
3249 |
|
3250 /** |
|
3251 * Gets the element associated with a particular item at a specified index. |
|
3252 * |
|
3253 * @param number aIndex |
|
3254 * The index used to identify the item. |
|
3255 * @return nsIDOMNode |
|
3256 * The matched element, or null if nothing is found. |
|
3257 */ |
|
3258 SourceResults.getElementAtIndex = |
|
3259 LineResults.getElementAtIndex = function(aIndex) { |
|
3260 for (let [element, item] of this._itemsByElement) { |
|
3261 if (!item.ignored && !aIndex--) { |
|
3262 return element; |
|
3263 } |
|
3264 } |
|
3265 return null; |
|
3266 }; |
|
3267 |
|
3268 /** |
|
3269 * Gets the index of an item associated with the specified element. |
|
3270 * |
|
3271 * @param nsIDOMNode aElement |
|
3272 * The element to get the index for. |
|
3273 * @return number |
|
3274 * The index of the matched element, or -1 if nothing is found. |
|
3275 */ |
|
3276 SourceResults.indexOfElement = |
|
3277 LineResults.indexOfElement = function(aElement) { |
|
3278 let count = 0; |
|
3279 for (let [element, item] of this._itemsByElement) { |
|
3280 if (element == aElement) { |
|
3281 return count; |
|
3282 } |
|
3283 if (!item.ignored) { |
|
3284 count++; |
|
3285 } |
|
3286 } |
|
3287 return -1; |
|
3288 }; |
|
3289 |
|
3290 /** |
|
3291 * Gets the number of cached items associated with a specified element. |
|
3292 * |
|
3293 * @return number |
|
3294 * The number of key/value pairs in the corresponding map. |
|
3295 */ |
|
3296 SourceResults.size = |
|
3297 LineResults.size = function() { |
|
3298 let count = 0; |
|
3299 for (let [, item] of this._itemsByElement) { |
|
3300 if (!item.ignored) { |
|
3301 count++; |
|
3302 } |
|
3303 } |
|
3304 return count; |
|
3305 }; |
|
3306 |
|
3307 /** |
|
3308 * Preliminary setup for the DebuggerView object. |
|
3309 */ |
|
3310 DebuggerView.Sources = new SourcesView(); |
|
3311 DebuggerView.VariableBubble = new VariableBubbleView(); |
|
3312 DebuggerView.Tracer = new TracerView(); |
|
3313 DebuggerView.WatchExpressions = new WatchExpressionsView(); |
|
3314 DebuggerView.EventListeners = new EventListenersView(); |
|
3315 DebuggerView.GlobalSearch = new GlobalSearchView(); |