summaryrefslogtreecommitdiffstats
path: root/devtools/client/debugger/views/stack-frames-view.js
diff options
context:
space:
mode:
Diffstat (limited to 'devtools/client/debugger/views/stack-frames-view.js')
-rw-r--r--devtools/client/debugger/views/stack-frames-view.js283
1 files changed, 283 insertions, 0 deletions
diff --git a/devtools/client/debugger/views/stack-frames-view.js b/devtools/client/debugger/views/stack-frames-view.js
new file mode 100644
index 000000000..244f97b3d
--- /dev/null
+++ b/devtools/client/debugger/views/stack-frames-view.js
@@ -0,0 +1,283 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+/* import-globals-from ../debugger-controller.js */
+/* import-globals-from ../debugger-view.js */
+/* import-globals-from ../utils.js */
+/* globals document, window */
+"use strict";
+
+/**
+ * Functions handling the stackframes UI.
+ */
+function StackFramesView(DebuggerController, DebuggerView) {
+ dumpn("StackFramesView was instantiated");
+
+ this.StackFrames = DebuggerController.StackFrames;
+ this.DebuggerView = DebuggerView;
+
+ this._onStackframeRemoved = this._onStackframeRemoved.bind(this);
+ this._onSelect = this._onSelect.bind(this);
+ this._onScroll = this._onScroll.bind(this);
+ this._afterScroll = this._afterScroll.bind(this);
+ this._getStackAsString = this._getStackAsString.bind(this);
+}
+
+StackFramesView.prototype = Heritage.extend(WidgetMethods, {
+ /**
+ * Initialization function, called when the debugger is started.
+ */
+ initialize: function () {
+ dumpn("Initializing the StackFramesView");
+
+ this._popupset = document.getElementById("debuggerPopupset");
+
+ this.widget = new BreadcrumbsWidget(document.getElementById("stackframes"));
+ this.widget.addEventListener("select", this._onSelect, false);
+ this.widget.addEventListener("scroll", this._onScroll, true);
+ this.widget.setAttribute("context", "stackFramesContextMenu");
+ window.addEventListener("resize", this._onScroll, true);
+
+ this.autoFocusOnFirstItem = false;
+ this.autoFocusOnSelection = false;
+
+ // This view's contents are also mirrored in a different container.
+ this._mirror = this.DebuggerView.StackFramesClassicList;
+ },
+
+ /**
+ * Destruction function, called when the debugger is closed.
+ */
+ destroy: function () {
+ dumpn("Destroying the StackFramesView");
+
+ this.widget.removeEventListener("select", this._onSelect, false);
+ this.widget.removeEventListener("scroll", this._onScroll, true);
+ window.removeEventListener("resize", this._onScroll, true);
+ },
+
+ /**
+ * Adds a frame in this stackframes container.
+ *
+ * @param string aTitle
+ * The frame title (function name).
+ * @param string aUrl
+ * The frame source url.
+ * @param string aLine
+ * The frame line number.
+ * @param number aDepth
+ * The frame depth in the stack.
+ * @param boolean aIsBlackBoxed
+ * Whether or not the frame is black boxed.
+ */
+ addFrame: function (aFrame, aLine, aColumn, aDepth, aIsBlackBoxed) {
+ let { source } = aFrame;
+
+ // The source may not exist in the source listing yet because it's
+ // an unnamed eval source, which we hide, so we need to add it
+ if (!DebuggerView.Sources.getItemByValue(source.actor)) {
+ DebuggerView.Sources.addSource(source, { force: true });
+ }
+
+ let location = DebuggerView.Sources.getDisplayURL(source);
+ let title = StackFrameUtils.getFrameTitle(aFrame);
+
+ // Blackboxed stack frames are collapsed into a single entry in
+ // the view. By convention, only the first frame is displayed.
+ if (aIsBlackBoxed) {
+ if (this._prevBlackBoxedUrl == location) {
+ return;
+ }
+ this._prevBlackBoxedUrl = location;
+ } else {
+ this._prevBlackBoxedUrl = null;
+ }
+
+ // Create the element node for the stack frame item.
+ let frameView = this._createFrameView(
+ title, location, aLine, aDepth, aIsBlackBoxed
+ );
+
+ // Append a stack frame item to this container.
+ this.push([frameView], {
+ index: 0, /* specifies on which position should the item be appended */
+ attachment: {
+ title: title,
+ url: location,
+ line: aLine,
+ depth: aDepth,
+ column: aColumn
+ },
+ // Make sure that when the stack frame item is removed, the corresponding
+ // mirrored item in the classic list is also removed.
+ finalize: this._onStackframeRemoved
+ });
+
+ // Mirror this newly inserted item inside the "Call Stack" tab.
+ this._mirror.addFrame(title, location, aLine, aDepth);
+ },
+
+ _getStackAsString: function () {
+ return [...this].map(frameItem => {
+ const { attachment: { title, url, line, column }} = frameItem;
+ return title + "@" + url + ":" + line + ":" + column;
+ }).join("\n");
+ },
+
+ addCopyContextMenu: function () {
+ let menupopup = document.createElement("menupopup");
+ let menuitem = document.createElement("menuitem");
+
+ menupopup.id = "stackFramesContextMenu";
+ menuitem.id = "copyStackMenuItem";
+
+ menuitem.setAttribute("label", "Copy");
+ menuitem.addEventListener("command", () => {
+ let stack = this._getStackAsString();
+ clipboardHelper.copyString(stack);
+ }, false);
+ menupopup.appendChild(menuitem);
+ this._popupset.appendChild(menupopup);
+ },
+
+ /**
+ * Selects the frame at the specified depth in this container.
+ * @param number aDepth
+ */
+ set selectedDepth(aDepth) {
+ this.selectedItem = aItem => aItem.attachment.depth == aDepth;
+ },
+
+ /**
+ * Gets the currently selected stack frame's depth in this container.
+ * This will essentially be the opposite of |selectedIndex|, which deals
+ * with the position in the view, where the last item added is actually
+ * the bottommost, not topmost.
+ * @return number
+ */
+ get selectedDepth() {
+ return this.selectedItem.attachment.depth;
+ },
+
+ /**
+ * Specifies if the active thread has more frames that need to be loaded.
+ */
+ dirty: false,
+
+ /**
+ * Customization function for creating an item's UI.
+ *
+ * @param string aTitle
+ * The frame title to be displayed in the list.
+ * @param string aUrl
+ * The frame source url.
+ * @param string aLine
+ * The frame line number.
+ * @param number aDepth
+ * The frame depth in the stack.
+ * @param boolean aIsBlackBoxed
+ * Whether or not the frame is black boxed.
+ * @return nsIDOMNode
+ * The stack frame view.
+ */
+ _createFrameView: function (aTitle, aUrl, aLine, aDepth, aIsBlackBoxed) {
+ let container = document.createElement("hbox");
+ container.id = "stackframe-" + aDepth;
+ container.className = "dbg-stackframe";
+
+ let frameDetails = SourceUtils.trimUrlLength(
+ SourceUtils.getSourceLabel(aUrl),
+ STACK_FRAMES_SOURCE_URL_MAX_LENGTH,
+ STACK_FRAMES_SOURCE_URL_TRIM_SECTION);
+
+ if (aIsBlackBoxed) {
+ container.classList.add("dbg-stackframe-black-boxed");
+ } else {
+ let frameTitleNode = document.createElement("label");
+ frameTitleNode.className = "plain dbg-stackframe-title breadcrumbs-widget-item-tag";
+ frameTitleNode.setAttribute("value", aTitle);
+ container.appendChild(frameTitleNode);
+
+ frameDetails += SEARCH_LINE_FLAG + aLine;
+ }
+
+ let frameDetailsNode = document.createElement("label");
+ frameDetailsNode.className = "plain dbg-stackframe-details breadcrumbs-widget-item-id";
+ frameDetailsNode.setAttribute("value", frameDetails);
+ container.appendChild(frameDetailsNode);
+
+ return container;
+ },
+
+ /**
+ * Function called each time a stack frame item is removed.
+ *
+ * @param object aItem
+ * The corresponding item.
+ */
+ _onStackframeRemoved: function (aItem) {
+ dumpn("Finalizing stackframe item: " + aItem.stringify());
+
+ // Remove the mirrored item in the classic list.
+ let depth = aItem.attachment.depth;
+ this._mirror.remove(this._mirror.getItemForAttachment(e => e.depth == depth));
+
+ // Forget the previously blackboxed stack frame url.
+ this._prevBlackBoxedUrl = null;
+ },
+
+ /**
+ * The select listener for the stackframes container.
+ */
+ _onSelect: function (e) {
+ let stackframeItem = this.selectedItem;
+ if (stackframeItem) {
+ // The container is not empty and an actual item was selected.
+ let depth = stackframeItem.attachment.depth;
+
+ // Mirror the selected item in the classic list.
+ this.suppressSelectionEvents = true;
+ this._mirror.selectedItem = e => e.attachment.depth == depth;
+ this.suppressSelectionEvents = false;
+
+ DebuggerController.StackFrames.selectFrame(depth);
+ }
+ },
+
+ /**
+ * The scroll listener for the stackframes container.
+ */
+ _onScroll: function () {
+ // Update the stackframes container only if we have to.
+ if (!this.dirty) {
+ return;
+ }
+ // Allow requests to settle down first.
+ setNamedTimeout("stack-scroll", STACK_FRAMES_SCROLL_DELAY, this._afterScroll);
+ },
+
+ /**
+ * Requests the addition of more frames from the controller.
+ */
+ _afterScroll: function () {
+ let scrollPosition = this.widget.getAttribute("scrollPosition");
+ let scrollWidth = this.widget.getAttribute("scrollWidth");
+
+ // If the stackframes container scrolled almost to the end, with only
+ // 1/10 of a breadcrumb remaining, load more content.
+ if (scrollPosition - scrollWidth / 10 < 1) {
+ this.ensureIndexIsVisible(CALL_STACK_PAGE_SIZE - 1);
+ this.dirty = false;
+
+ // Loads more stack frames from the debugger server cache.
+ DebuggerController.StackFrames.addMoreFrames();
+ }
+ },
+
+ _mirror: null,
+ _prevBlackBoxedUrl: null
+});
+
+DebuggerView.StackFrames = new StackFramesView(DebuggerController, DebuggerView);