Wed, 31 Dec 2014 06:09:35 +0100
Cloned upstream origin tor-browser at tor-browser-31.3.0esr-4.5-1-build1
revision ID fc1c9ff7c1b2defdbc039f12214767608f46423f for hacking purpose.
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/. */
7 "use strict";
9 XPCOMUtils.defineLazyModuleGetter(this, "Task",
10 "resource://gre/modules/Task.jsm");
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
17 // Maps known URLs to friendly source group names
18 const KNOWN_SOURCE_GROUPS = {
19 "Add-on SDK": "resource://gre/modules/commonjs/",
20 };
22 /**
23 * Functions handling the sources UI.
24 */
25 function SourcesView() {
26 dumpn("SourcesView was instantiated");
28 this.togglePrettyPrint = this.togglePrettyPrint.bind(this);
29 this.toggleBlackBoxing = this.toggleBlackBoxing.bind(this);
30 this.toggleBreakpoints = this.toggleBreakpoints.bind(this);
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);
45 this.updateToolbarButtonsState = this.updateToolbarButtonsState.bind(this);
46 }
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");
55 this.widget = new SideMenuWidget(document.getElementById("sources"), {
56 showArrows: true
57 });
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 }
65 return (a in KNOWN_SOURCE_GROUPS) ? 1 : -1;
66 };
68 this.emptyText = L10N.getStr("noSourcesText");
69 this._blackBoxCheckboxTooltip = L10N.getStr("blackBoxCheckboxTooltip");
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");
81 if (Prefs.prettyPrintEnabled) {
82 this._prettyPrintButton.removeAttribute("hidden");
83 }
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);
94 this.autoFocusOnSelection = false;
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 },
103 /**
104 * Destruction function, called when the debugger is closed.
105 */
106 destroy: function() {
107 dumpn("Destroying the SourcesView");
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 },
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;
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 },
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));
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);
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 },
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;
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 }
191 // Get the source item to which the breakpoint should be attached.
192 let sourceItem = this.getItemByValue(location.url);
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);
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 });
215 // Highlight the newly appended breakpoint child item if necessary.
216 if (aOptions.openPopup || !aOptions.noEditorUpdate) {
217 this.highlightBreakpoint(location, aOptions);
218 }
219 },
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 }
240 // Clear the breakpoint view.
241 sourceItem.remove(breakpointItem);
242 },
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 },
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 },
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 },
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 }
310 // Breakpoint will now be enabled.
311 let attachment = breakpointItem.attachment;
312 attachment.disabled = false;
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");
322 // Update the breakpoint toggle button checked state.
323 this._toggleBreakpointsButton.removeAttribute("checked");
325 // Update the checkbox state if necessary.
326 if (!aOptions.silent) {
327 attachment.view.checkbox.setAttribute("checked", "true");
328 }
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 },
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 }
357 // Breakpoint will now be disabled.
358 let attachment = breakpointItem.attachment;
359 attachment.disabled = true;
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");
369 // Update the checkbox state if necessary.
370 if (!aOptions.silent) {
371 attachment.view.checkbox.removeAttribute("checked");
372 }
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 },
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 }
400 // Breakpoint will now be selected.
401 this._selectBreakpoint(breakpointItem);
403 // Update the editor location if necessary.
404 if (!aOptions.noEditorUpdate) {
405 DebuggerView.setEditorLocation(aLocation.url, aLocation.line, { noDebug: true });
406 }
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 },
417 /**
418 * Unhighlights the current breakpoint in this sources container.
419 */
420 unhighlightBreakpoint: function() {
421 this._unselectBreakpoint();
422 this._hideConditionalPopup();
423 },
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);
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 }
441 if (sourceClient.isPrettyPrinted) {
442 this._prettyPrintButton.setAttribute("checked", true);
443 } else {
444 this._prettyPrintButton.removeAttribute("checked");
445 }
446 },
448 /**
449 * Toggle the pretty printing of the selected source.
450 */
451 togglePrettyPrint: function() {
452 if (this._prettyPrintButton.hasAttribute("disabled")) {
453 return;
454 }
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 };
463 const printError = ([{ url }, error]) => {
464 DevToolsUtils.reportException("togglePrettyPrint", error);
465 };
467 DebuggerView.showProgressBar();
468 const { source } = this.selectedItem.attachment;
469 const sourceClient = gThreadClient.source(source);
470 const shouldPrettyPrint = !sourceClient.isPrettyPrinted;
472 if (shouldPrettyPrint) {
473 this._prettyPrintButton.setAttribute("checked", true);
474 } else {
475 this._prettyPrintButton.removeAttribute("checked");
476 }
478 DebuggerController.SourceScripts.togglePrettyPrint(source)
479 .then(resetEditor, printError)
480 .then(DebuggerView.showEditor)
481 .then(this.updateToolbarButtonsState);
482 },
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;
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`.
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 }
506 DebuggerController.SourceScripts.setBlackBoxing(source, shouldBlackBox)
507 .then(this.updateToolbarButtonsState,
508 this.updateToolbarButtonsState);
509 },
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);
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 },
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");
542 // Ensure the currently selected breakpoint is visible.
543 this.widget.ensureElementIsVisible(aItem.target);
544 },
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 },
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 }
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;
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 },
591 /**
592 * Hides a conditional breakpoint's expression input popup.
593 */
594 _hideConditionalPopup: function() {
595 this._cbPanel.hidden = true;
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 },
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);
620 let checkbox = document.createElement("checkbox");
621 checkbox.setAttribute("checked", !disabled);
622 checkbox.className = "dbg-breakpoint-checkbox";
624 let lineNumberNode = document.createElement("label");
625 lineNumberNode.className = "plain dbg-breakpoint-line";
626 lineNumberNode.setAttribute("value", location.line);
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");
634 let tooltip = text.substr(0, BREAKPOINT_LINE_TOOLTIP_MAX_LENGTH);
635 lineTextNode.setAttribute("tooltiptext", tooltip);
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");
644 container.addEventListener("click", this._onBreakpointClick, false);
645 checkbox.addEventListener("click", this._onBreakpointCheckboxClick, false);
647 container.appendChild(checkbox);
648 container.appendChild(lineNumberNode);
649 container.appendChild(lineTextNode);
651 return {
652 container: container,
653 checkbox: checkbox,
654 lineNumber: lineNumberNode,
655 lineText: lineTextNode
656 };
657 },
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);
673 let commandset = document.createElement("commandset");
674 let menupopup = document.createElement("menupopup");
675 commandset.id = "bp-cSet-" + identifier;
676 menupopup.id = "bp-mPop-" + identifier;
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");
693 this._popupset.appendChild(menupopup);
694 this._commandset.appendChild(commandset);
696 return {
697 commandsetId: commandset.id,
698 menupopupId: menupopup.id
699 };
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");
714 let prefix = "bp-cMenu-"; // "breakpoints context menu"
715 let commandId = prefix + aName + "-" + identifier + "-command";
716 let menuitemId = prefix + aName + "-" + identifier + "-menuitem";
718 let label = L10N.getStr("breakpointMenuItem." + aName);
719 let func = "_on" + aName.charAt(0).toUpperCase() + aName.slice(1);
721 command.id = commandId;
722 command.setAttribute("label", label);
723 command.addEventListener("command", () => this[func](location), false);
725 menuitem.id = menuitemId;
726 menuitem.setAttribute("command", commandId);
727 aHiddenFlag && menuitem.setAttribute("hidden", "true");
729 commandset.appendChild(command);
730 menupopup.appendChild(menuitem);
731 }
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 },
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);
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();
757 // Clear the breakpoint selection.
758 if (this._selectedBreakpointItem == aItem) {
759 this._selectedBreakpointItem = null;
760 }
761 },
763 /**
764 * The load listener for the source editor.
765 */
766 _onEditorLoad: function(aName, aEditor) {
767 aEditor.on("cursorActivity", this._onEditorCursorActivity);
768 },
770 /**
771 * The unload listener for the source editor.
772 */
773 _onEditorUnload: function(aName, aEditor) {
774 aEditor.off("cursorActivity", this._onEditorCursorActivity);
775 },
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;
786 let location = { url: url, line: start };
788 if (this.getBreakpoint(location) && start == end) {
789 this.highlightBreakpoint(location, { noEditorUpdate: true });
790 } else {
791 this.unhighlightBreakpoint();
792 }
793 },
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);
805 // The container is not empty and an actual item was selected.
806 DebuggerView.setEditorLocation(sourceItem.value);
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 }
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);
820 DebuggerView.maybeShowBlackBoxMessage();
821 this.updateToolbarButtonsState();
822 },
824 /**
825 * The click listener for the "stop black boxing" button.
826 */
827 _onStopBlackBoxing: function() {
828 const { source } = this.selectedItem.attachment;
830 DebuggerController.SourceScripts.setBlackBoxing(source, false)
831 .then(this.updateToolbarButtonsState,
832 this.updateToolbarButtonsState);
833 },
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;
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 }
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 },
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;
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 });
879 // Don't update the editor location (avoid propagating into _onBreakpointClick).
880 e.preventDefault();
881 e.stopPropagation();
882 },
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 },
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 },
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;
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 }
919 window.emit(EVENTS.CONDITIONAL_BREAKPOINT_POPUP_HIDING);
920 }),
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 },
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);
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 },
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);
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 },
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 },
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 },
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 },
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 },
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 }
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 },
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 },
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 },
1059 /**
1060 * Function invoked on the "enableAll" menuitem command.
1061 */
1062 _onEnableAll: function() {
1063 this._onEnableOthers(undefined);
1064 },
1066 /**
1067 * Function invoked on the "disableAll" menuitem command.
1068 */
1069 _onDisableAll: function() {
1070 this._onDisableOthers(undefined);
1071 },
1073 /**
1074 * Function invoked on the "deleteAll" menuitem command.
1075 */
1076 _onDeleteAll: function() {
1077 this._onDeleteOthers(undefined);
1078 },
1080 _commandset: null,
1081 _popupset: null,
1082 _cmPopup: null,
1083 _cbPanel: null,
1084 _cbTextbox: null,
1085 _selectedBreakpointItem: null,
1086 _conditionalPopupVisible: false
1087 });
1089 /**
1090 * Functions handling the traces UI.
1091 */
1092 function TracerView() {
1093 this._selectedItem = null;
1094 this._matchingItems = null;
1095 this.widget = null;
1097 this._highlightItem = this._highlightItem.bind(this);
1098 this._isNotSelectedItem = this._isNotSelectedItem.bind(this);
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 }
1115 TracerView.MAX_TRACES = 200;
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");
1124 this._traceButton = document.getElementById("trace");
1125 this._tracerTab = document.getElementById("tracer-tab");
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 }
1137 this.widget = new FastListWidget(document.getElementById("tracer-traces"));
1138 this._traceButton.removeAttribute("hidden");
1139 this._tracerTab.removeAttribute("hidden");
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];
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);
1152 this._startTooltip = L10N.getStr("startTracingTooltip");
1153 this._stopTooltip = L10N.getStr("stopTracingTooltip");
1154 this._tracingNotStartedString = L10N.getStr("tracingNotStartedText");
1155 this._noFunctionCallsString = L10N.getStr("noFunctionCallsText");
1157 this._traceButton.setAttribute("tooltiptext", this._startTooltip);
1158 this.emptyText = this._tracingNotStartedString;
1159 },
1161 /**
1162 * Destruction function, called when the debugger is closed.
1163 */
1164 destroy: function() {
1165 dumpn("Destroying the TracerView");
1167 if (!this.widget) {
1168 return;
1169 }
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 },
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 },
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);
1199 this.empty();
1200 this.emptyText = this._noFunctionCallsString;
1202 let deferred = promise.defer();
1203 DebuggerController.Tracer.startTracing(deferred.resolve);
1204 return deferred.promise;
1205 },
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);
1218 this.emptyText = this._tracingNotStartedString;
1220 let deferred = promise.defer();
1221 DebuggerController.Tracer.stopTracing(deferred.resolve);
1222 return deferred.promise;
1223 },
1225 /**
1226 * Function invoked by the "clearTraces" command to empty the traces pane.
1227 */
1228 _onClear: function() {
1229 this.empty();
1230 },
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 },
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 }
1266 const data = traceItem.attachment.trace;
1267 const { location: { url, line } } = data;
1268 DebuggerView.setEditorLocation(url, line, { noDebug: true });
1270 DebuggerView.Variables.empty();
1271 const scope = DebuggerView.Variables.addScope();
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 }
1287 scope.expand();
1288 DebuggerView.showInstrumentsPane();
1289 },
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 },
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 },
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 },
1325 /**
1326 * Returns true if the given item is not the selected item.
1327 */
1328 _isNotSelectedItem: function(aItem) {
1329 return aItem !== this.selectedItem;
1330 },
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;
1339 this._unhighlightMatchingItems();
1340 this._matchingItems = this.items.filter(predicate);
1341 this._matchingItems
1342 .filter(this._isNotSelectedItem)
1343 .forEach(this._highlightItem);
1344 },
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 },
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 },
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 },
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 },
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);
1394 // Append a source item to this container.
1395 this.push([view], {
1396 staged: true,
1397 attachment: {
1398 trace: aTrace
1399 }
1400 });
1401 },
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();
1414 this._templateItem.setAttribute("tooltiptext", SourceUtils.trimUrl(location.url));
1415 this._templateItem.style.MozPaddingStart = depth + "em";
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]);
1429 this._templateNameNode.setAttribute("value", name);
1431 // All extra syntax and parameter nodes added.
1432 const addedNodes = [];
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 };
1444 this._templateItem.appendChild(syntax("("));
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);
1454 if (i + 1 !== n) {
1455 this._templateItem.appendChild(syntax(", "));
1456 }
1457 }
1459 this._templateItem.appendChild(syntax(")"));
1460 }
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 }
1467 // Remove any added nodes from the template.
1468 for (let node of addedNodes) {
1469 this._templateItem.removeChild(node);
1470 }
1472 return fragment;
1473 }
1474 });
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(),
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 },
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 }
1510 let isMinified;
1511 let lineEndIndex = 0;
1512 let lineStartIndex = 0;
1513 let lines = 0;
1514 let indentCount = 0;
1515 let overCharLimit = false;
1517 // Strip comments.
1518 aText = aText.replace(/\/\*[\S\s]*?\*\/|\/\/(.+|\n)/g, "");
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;
1538 this._minifiedCache.set(sourceClient, isMinified);
1539 return isMinified;
1540 },
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 },
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 }
1567 let sourceLabel = null;
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 }
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 },
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 }
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 }
1606 let groupLabel = uri.prePath;
1608 for (let name of Object.keys(KNOWN_SOURCE_GROUPS)) {
1609 if (aUrl.startsWith(KNOWN_SOURCE_GROUPS[name])) {
1610 groupLabel = name;
1611 }
1612 }
1614 let unicodeLabel = NetworkHelper.convertToUnicode(unescape(groupLabel));
1615 this._groupsCache.set(aUrl, unicodeLabel)
1616 return unicodeLabel;
1617 },
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";
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 },
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);
1669 return aUrl.slice(0, q);
1670 },
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).
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 }
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 }
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 };
1762 /**
1763 * Functions handling the variables bubble UI.
1764 */
1765 function VariableBubbleView() {
1766 dumpn("VariableBubbleView was instantiated");
1768 this._onMouseMove = this._onMouseMove.bind(this);
1769 this._onMouseLeave = this._onMouseLeave.bind(this);
1770 this._onPopupHiding = this._onPopupHiding.bind(this);
1771 }
1773 VariableBubbleView.prototype = {
1774 /**
1775 * Initialization function, called when the debugger is started.
1776 */
1777 initialize: function() {
1778 dumpn("Initializing the VariableBubbleView");
1780 this._editorContainer = document.getElementById("editor");
1781 this._editorContainer.addEventListener("mousemove", this._onMouseMove, false);
1782 this._editorContainer.addEventListener("mouseleave", this._onMouseLeave, false);
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 },
1799 /**
1800 * Destruction function, called when the debugger is closed.
1801 */
1802 destroy: function() {
1803 dumpn("Destroying the VariableBubbleView");
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 },
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,
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;
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;
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);
1839 // If the script length is negative, we're not hovering JS source code.
1840 if (scriptInfo.length == -1) {
1841 return;
1842 }
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);
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 });
1859 // If the info is null, we're not hovering any identifier.
1860 if (!identifierInfo) {
1861 return;
1862 }
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 };
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 },
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;
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 });
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 }
1963 this._tooltip.show(this._markedText.anchor);
1964 },
1966 /**
1967 * Hides the inspection popup.
1968 */
1969 hideContents: function() {
1970 clearNamedTimeout("editor-mouse-move");
1971 this._tooltip.hide();
1972 },
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 },
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 },
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 },
2023 /**
2024 * The mouseleave listener for the source editor container node.
2025 */
2026 _onMouseLeave: function() {
2027 clearNamedTimeout("editor-mouse-move");
2028 },
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 },
2046 _editorContainer: null,
2047 _markedText: null,
2048 _tooltip: null
2049 };
2051 /**
2052 * Functions handling the watch expressions UI.
2053 */
2054 function WatchExpressionsView() {
2055 dumpn("WatchExpressionsView was instantiated");
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 }
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");
2073 this.widget = new SimpleListWidget(document.getElementById("expressions"));
2074 this.widget.setAttribute("context", "debuggerWatchExpressionsContextMenu");
2075 this.widget.addEventListener("click", this._onClick, false);
2077 this.headerText = L10N.getStr("addWatchExpressionText");
2078 },
2080 /**
2081 * Destruction function, called when the debugger is closed.
2082 */
2083 destroy: function() {
2084 dumpn("Destroying the WatchExpressionsView");
2086 this.widget.removeEventListener("click", this._onClick, false);
2087 },
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();
2102 // Create the element node for the watch expression item.
2103 let itemView = this._createItemView(aExpression);
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 });
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 },
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];
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 }
2149 // Save the watch expression code string.
2150 expressionItem.attachment.currentExpression = aExpression;
2151 expressionItem.attachment.view.inputNode.value = aExpression;
2153 // Synchronize with the controller's watch expressions store.
2154 DebuggerController.StackFrames.syncWatchExpressions();
2155 },
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];
2169 // Remove the watch expression.
2170 this.remove(expressionItem);
2172 // Synchronize with the controller's watch expressions store.
2173 DebuggerController.StackFrames.syncWatchExpressions();
2174 },
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 },
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 },
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";
2208 let arrowNode = document.createElement("hbox");
2209 arrowNode.className = "dbg-expression-arrow";
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");
2216 let closeNode = document.createElement("toolbarbutton");
2217 closeNode.className = "plain variables-view-delete";
2219 closeNode.addEventListener("click", this._onClose, false);
2220 inputNode.addEventListener("blur", this._onBlur, false);
2221 inputNode.addEventListener("keypress", this._onKeyPress, false);
2223 container.appendChild(arrowNode);
2224 container.appendChild(inputNode);
2225 container.appendChild(closeNode);
2227 return {
2228 container: container,
2229 arrowNode: arrowNode,
2230 inputNode: inputNode,
2231 closeNode: closeNode
2232 };
2233 },
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 },
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();
2252 // Synchronize with the controller's watch expressions store.
2253 DebuggerController.StackFrames.syncWatchExpressions();
2254 },
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 },
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));
2278 // Synchronize with the controller's watch expressions store.
2279 DebuggerController.StackFrames.syncWatchExpressions();
2281 // Prevent clicking the expression element itself.
2282 e.preventDefault();
2283 e.stopPropagation();
2284 },
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();
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 }
2307 // Synchronize with the controller's watch expressions store.
2308 DebuggerController.StackFrames.syncWatchExpressions();
2309 },
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 });
2325 /**
2326 * Functions handling the event listeners UI.
2327 */
2328 function EventListenersView() {
2329 dumpn("EventListenersView was instantiated");
2331 this._onCheck = this._onCheck.bind(this);
2332 this._onClick = this._onClick.bind(this);
2333 }
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");
2342 this.widget = new SideMenuWidget(document.getElementById("event-listeners"), {
2343 showItemCheckboxes: true,
2344 showGroupCheckboxes: true
2345 });
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");
2353 this.widget.addEventListener("check", this._onCheck, false);
2354 this.widget.addEventListener("click", this._onClick, false);
2355 },
2357 /**
2358 * Destruction function, called when the debugger is closed.
2359 */
2360 destroy: function() {
2361 dumpn("Destroying the EventListenersView");
2363 this.widget.removeEventListener("check", this._onCheck, false);
2364 this.widget.removeEventListener("click", this._onClick, false);
2365 },
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;
2380 // Some listener objects may be added from plugins, thus getting
2381 // translated to native code.
2382 if (!url) {
2383 url = this._inNativeCodeString;
2384 }
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 }
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;
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 }
2460 // Create the element node for the event listener item.
2461 let itemView = this._createItemView(type, selector, url);
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;
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 },
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 },
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 },
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";
2519 let eventType = document.createElement("label");
2520 eventType.className = "plain dbg-event-listener-type";
2521 eventType.setAttribute("value", aType);
2522 container.appendChild(eventType);
2524 let typeSeparator = document.createElement("label");
2525 typeSeparator.className = "plain dbg-event-listener-separator";
2526 typeSeparator.setAttribute("value", this._onSelectorString);
2527 container.appendChild(typeSeparator);
2529 let eventTargets = document.createElement("label");
2530 eventTargets.className = "plain dbg-event-listener-targets";
2531 eventTargets.setAttribute("value", aSelector);
2532 container.appendChild(eventTargets);
2534 let selectorSeparator = document.createElement("label");
2535 selectorSeparator.className = "plain dbg-event-listener-separator";
2536 selectorSeparator.setAttribute("value", this._inSourceString);
2537 container.appendChild(selectorSeparator);
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);
2546 return {
2547 container: container,
2548 type: eventType,
2549 targets: eventTargets,
2550 location: eventLocation
2551 };
2552 },
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 }
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 },
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 },
2584 _eventCheckboxTooltip: "",
2585 _onSelectorString: "",
2586 _inSourceString: "",
2587 _inNativeCodeString: ""
2588 });
2590 /**
2591 * Functions handling the global search UI.
2592 */
2593 function GlobalSearchView() {
2594 dumpn("GlobalSearchView was instantiated");
2596 this._onHeaderClick = this._onHeaderClick.bind(this);
2597 this._onLineClick = this._onLineClick.bind(this);
2598 this._onMatchClick = this._onMatchClick.bind(this);
2599 }
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");
2608 this.widget = new SimpleListWidget(document.getElementById("globalsearch"));
2609 this._splitter = document.querySelector("#globalsearch + .devtools-horizontal-splitter");
2611 this.emptyText = L10N.getStr("noMatchingStringsText");
2612 },
2614 /**
2615 * Destruction function, called when the debugger is closed.
2616 */
2617 destroy: function() {
2618 dumpn("Destroying the GlobalSearchView");
2619 },
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 },
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",
2638 /**
2639 * Hides and removes all items from this search container.
2640 */
2641 clearView: function() {
2642 this.hidden = true;
2643 this.empty();
2644 },
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 },
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 },
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;
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 },
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 }
2718 // Search is not case sensitive, prepare the actual searched token.
2719 let lowerCaseToken = aToken.toLowerCase();
2720 let tokenLength = aToken.length;
2722 // Create a Map containing search details for each source.
2723 let globalResults = new GlobalResults();
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);
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();
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);
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;
2751 // Everything before the token is unmatched.
2752 let unmatched = aString.substr(prevLength, currLength);
2753 lineResults.add(unmatched);
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 }
2763 // Continue with the next sub-region in this line's text.
2764 return aPrev + aToken + aCurr;
2765 }, "");
2767 if (lineResults.matchCount) {
2768 sourceResults.add(lineResults);
2769 }
2770 });
2772 if (sourceResults.matchCount) {
2773 globalResults.add(sourceResults);
2774 }
2775 }
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 },
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;
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 },
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 });
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 },
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 },
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 },
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 }
2860 let target = e.target;
2861 let sourceResultsItem = SourceResults.getItemForElement(target);
2862 let lineResultsItem = LineResults.getItemForElement(target);
2864 sourceResultsItem.instance.expand();
2865 this._currentlyFocusedMatch = LineResults.indexOfElement(target);
2866 this._scrollMatchIntoViewIfNeeded(target);
2867 this._bounceMatch(target);
2869 let url = sourceResultsItem.instance.url;
2870 let line = lineResultsItem.instance.line;
2872 DebuggerView.setEditorLocation(url, line + 1, { noDebug: true });
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 );
2881 DebuggerView.editor.setSelection(anchor, head);
2882 },
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 },
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 },
2911 _splitter: null,
2912 _currentlyFocusedMatch: -1,
2913 _forceExpandResults: false
2914 });
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 }
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 },
2937 /**
2938 * Gets the number of source results in this store.
2939 */
2940 get matchCount() this._store.length
2941 };
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 }
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 },
2969 /**
2970 * Gets the number of line results in this store.
2971 */
2972 get matchCount() this._store.length,
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 },
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 },
2990 /**
2991 * Toggles between the element collapse/expand state.
2992 */
2993 toggle: function(e) {
2994 this.expanded ^= 1;
2995 },
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"),
3005 /**
3006 * Sets this element's expanded state.
3007 * @param boolean aFlag
3008 */
3009 set expanded(aFlag) this[aFlag ? "expand" : "collapse"](),
3011 /**
3012 * Gets the element associated with this item.
3013 * @return nsIDOMNode
3014 */
3015 get target() this._target,
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;
3030 let arrow = this._arrow = document.createElement("box");
3031 arrow.className = "arrow";
3033 let locationNode = document.createElement("label");
3034 locationNode.className = "plain dbg-results-header-location";
3035 locationNode.setAttribute("value", this.url);
3037 let matchCountNode = document.createElement("label");
3038 matchCountNode.className = "plain dbg-results-header-match-count";
3039 matchCountNode.setAttribute("value", "(" + this.matchCount + ")");
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);
3049 let resultsContainer = this._resultsContainer = document.createElement("vbox");
3050 resultsContainer.className = "dbg-results-container";
3051 resultsContainer.setAttribute("hidden", "true");
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 }
3063 let resultsBox = document.createElement("vbox");
3064 resultsBox.setAttribute("flex", "1");
3065 resultsBox.appendChild(resultsHeader);
3066 resultsBox.appendChild(resultsContainer);
3068 aElementNode.id = "source-results-" + this.url;
3069 aElementNode.className = "dbg-source-results";
3070 aElementNode.appendChild(resultsBox);
3072 SourceResults._itemsByElement.set(aElementNode, { instance: this });
3073 },
3075 url: "",
3076 _globalResults: null,
3077 _store: null,
3078 _target: null,
3079 _arrow: null,
3080 _resultsHeader: null,
3081 _resultsContainer: null
3082 };
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 }
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 },
3116 /**
3117 * Gets the number of word results in this store.
3118 */
3119 get matchCount() this._matchCount,
3121 /**
3122 * Gets the element associated with this item.
3123 * @return nsIDOMNode
3124 */
3125 get target() this._target,
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;
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);
3145 let lineContentsNode = document.createElement("hbox");
3146 lineContentsNode.className = "dbg-results-line-contents";
3147 lineContentsNode.classList.add("devtools-monospace");
3148 lineContentsNode.setAttribute("flex", "1");
3150 let lineString = "";
3151 let lineLength = 0;
3152 let firstMatch = null;
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;
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);
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 }
3176 this._entangleLine(lineContentsNode, firstMatch);
3177 lineContentsNode.addEventListener("click", aCallbacks.onLineClick, false);
3179 let searchResult = document.createElement("hbox");
3180 searchResult.className = "dbg-search-result";
3181 searchResult.appendChild(lineNumberNode);
3182 searchResult.appendChild(lineContentsNode);
3184 aElementNode.appendChild(searchResult);
3185 },
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 },
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 },
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 })(),
3222 line: 0,
3223 _sourceResults: null,
3224 _store: null,
3225 _target: null
3226 };
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 };
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 };
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 };
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 };
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 };
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();