diff options
Diffstat (limited to 'devtools/client/debugger/views/variable-bubble-view.js')
-rw-r--r-- | devtools/client/debugger/views/variable-bubble-view.js | 321 |
1 files changed, 321 insertions, 0 deletions
diff --git a/devtools/client/debugger/views/variable-bubble-view.js b/devtools/client/debugger/views/variable-bubble-view.js new file mode 100644 index 000000000..3ac2f971e --- /dev/null +++ b/devtools/client/debugger/views/variable-bubble-view.js @@ -0,0 +1,321 @@ +/* -*- 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"; + +const {setTooltipVariableContent} = require("devtools/client/shared/widgets/tooltip/VariableContentHelper"); + +/** + * Functions handling the variables bubble UI. + */ +function VariableBubbleView(DebuggerController, DebuggerView) { + dumpn("VariableBubbleView was instantiated"); + + this.StackFrames = DebuggerController.StackFrames; + this.Parser = DebuggerController.Parser; + this.DebuggerView = DebuggerView; + + this._onMouseMove = this._onMouseMove.bind(this); + this._onMouseOut = this._onMouseOut.bind(this); + this._onPopupHiding = this._onPopupHiding.bind(this); +} + +VariableBubbleView.prototype = { + /** + * Delay before showing the variables bubble tooltip when hovering a valid + * target. + */ + TOOLTIP_SHOW_DELAY: 750, + + /** + * Tooltip position for the variables bubble tooltip. + */ + TOOLTIP_POSITION: "topcenter bottomleft", + + /** + * Initialization function, called when the debugger is started. + */ + initialize: function () { + dumpn("Initializing the VariableBubbleView"); + + this._toolbox = DebuggerController._toolbox; + this._editorContainer = document.getElementById("editor"); + this._editorContainer.addEventListener("mousemove", this._onMouseMove, false); + this._editorContainer.addEventListener("mouseout", this._onMouseOut, false); + + this._tooltip = new Tooltip(document, { + closeOnEvents: [{ + emitter: this._toolbox, + event: "select" + }, { + emitter: this._editorContainer, + event: "scroll", + useCapture: true + }, { + emitter: document, + event: "keydown" + }] + }); + this._tooltip.defaultPosition = this.TOOLTIP_POSITION; + this._tooltip.panel.addEventListener("popuphiding", this._onPopupHiding); + }, + + /** + * Destruction function, called when the debugger is closed. + */ + destroy: function () { + dumpn("Destroying the VariableBubbleView"); + + this._tooltip.panel.removeEventListener("popuphiding", this._onPopupHiding); + this._editorContainer.removeEventListener("mousemove", this._onMouseMove, false); + this._editorContainer.removeEventListener("mouseout", this._onMouseOut, false); + }, + + /** + * Specifies whether literals can be (redundantly) inspected in a popup. + * This behavior is deprecated, but still tested in a few places. + */ + _ignoreLiterals: true, + + /** + * Searches for an identifier underneath the specified position in the + * source editor, and if found, opens a VariablesView inspection popup. + * + * @param number x, y + * The left/top coordinates where to look for an identifier. + */ + _findIdentifier: function (x, y) { + let editor = this.DebuggerView.editor; + + // Calculate the editor's line and column at the current x and y coords. + let hoveredPos = editor.getPositionFromCoords({ left: x, top: y }); + let hoveredOffset = editor.getOffset(hoveredPos); + let hoveredLine = hoveredPos.line; + let hoveredColumn = hoveredPos.ch; + + // A source contains multiple scripts. Find the start index of the script + // containing the specified offset relative to its parent source. + let contents = editor.getText(); + let location = this.DebuggerView.Sources.selectedValue; + let parsedSource = this.Parser.get(contents, location); + let scriptInfo = parsedSource.getScriptInfo(hoveredOffset); + + // If the script length is negative, we're not hovering JS source code. + if (scriptInfo.length == -1) { + return; + } + + // Using the script offset, determine the actual line and column inside the + // script, to use when finding identifiers. + let scriptStart = editor.getPosition(scriptInfo.start); + let scriptLineOffset = scriptStart.line; + let scriptColumnOffset = (hoveredLine == scriptStart.line ? scriptStart.ch : 0); + + let scriptLine = hoveredLine - scriptLineOffset; + let scriptColumn = hoveredColumn - scriptColumnOffset; + let identifierInfo = parsedSource.getIdentifierAt({ + line: scriptLine + 1, + column: scriptColumn, + scriptIndex: scriptInfo.index, + ignoreLiterals: this._ignoreLiterals + }); + + // If the info is null, we're not hovering any identifier. + if (!identifierInfo) { + return; + } + + // Transform the line and column relative to the parsed script back + // to the context of the parent source. + let { start: identifierStart, end: identifierEnd } = identifierInfo.location; + let identifierCoords = { + line: identifierStart.line + scriptLineOffset, + column: identifierStart.column + scriptColumnOffset, + length: identifierEnd.column - identifierStart.column + }; + + // Evaluate the identifier in the current stack frame and show the + // results in a VariablesView inspection popup. + this.StackFrames.evaluate(identifierInfo.evalString) + .then(frameFinished => { + if ("return" in frameFinished) { + this.showContents({ + coords: identifierCoords, + evalPrefix: identifierInfo.evalString, + objectActor: frameFinished.return + }); + } else { + let msg = "Evaluation has thrown for: " + identifierInfo.evalString; + console.warn(msg); + dumpn(msg); + } + }) + .then(null, err => { + let msg = "Couldn't evaluate: " + err.message; + console.error(msg); + dumpn(msg); + }); + }, + + /** + * Shows an inspection popup for a specified object actor grip. + * + * @param string object + * An object containing the following properties: + * - coords: the inspected identifier coordinates in the editor, + * containing the { line, column, length } properties. + * - evalPrefix: a prefix for the variables view evaluation macros. + * - objectActor: the value grip for the object actor. + */ + showContents: function ({ coords, evalPrefix, objectActor }) { + let editor = this.DebuggerView.editor; + let { line, column, length } = coords; + + // Highlight the function found at the mouse position. + this._markedText = editor.markText( + { line: line - 1, ch: column }, + { line: line - 1, ch: column + length }); + + // If the grip represents a primitive value, use a more lightweight + // machinery to display it. + if (VariablesView.isPrimitive({ value: objectActor })) { + let className = VariablesView.getClass(objectActor); + let textContent = VariablesView.getString(objectActor); + this._tooltip.setTextContent({ + messages: [textContent], + messagesClass: className, + containerClass: "plain" + }, [{ + label: L10N.getStr("addWatchExpressionButton"), + className: "dbg-expression-button", + command: () => { + this.DebuggerView.VariableBubble.hideContents(); + this.DebuggerView.WatchExpressions.addExpression(evalPrefix, true); + } + }]); + } else { + setTooltipVariableContent(this._tooltip, objectActor, { + searchPlaceholder: L10N.getStr("emptyPropertiesFilterText"), + searchEnabled: Prefs.variablesSearchboxVisible, + eval: (variable, value) => { + let string = variable.evaluationMacro(variable, value); + this.StackFrames.evaluate(string); + this.DebuggerView.VariableBubble.hideContents(); + } + }, { + getEnvironmentClient: aObject => gThreadClient.environment(aObject), + getObjectClient: aObject => gThreadClient.pauseGrip(aObject), + simpleValueEvalMacro: this._getSimpleValueEvalMacro(evalPrefix), + getterOrSetterEvalMacro: this._getGetterOrSetterEvalMacro(evalPrefix), + overrideValueEvalMacro: this._getOverrideValueEvalMacro(evalPrefix) + }, { + fetched: (aEvent, aType) => { + if (aType == "properties") { + window.emit(EVENTS.FETCHED_BUBBLE_PROPERTIES); + } + } + }, [{ + label: L10N.getStr("addWatchExpressionButton"), + className: "dbg-expression-button", + command: () => { + this.DebuggerView.VariableBubble.hideContents(); + this.DebuggerView.WatchExpressions.addExpression(evalPrefix, true); + } + }], this._toolbox); + } + + this._tooltip.show(this._markedText.anchor); + }, + + /** + * Hides the inspection popup. + */ + hideContents: function () { + clearNamedTimeout("editor-mouse-move"); + this._tooltip.hide(); + }, + + /** + * Checks whether the inspection popup is shown. + * + * @return boolean + * True if the panel is shown or showing, false otherwise. + */ + contentsShown: function () { + return this._tooltip.isShown(); + }, + + /** + * Functions for getting customized variables view evaluation macros. + * + * @param string aPrefix + * See the corresponding VariablesView.* functions. + */ + _getSimpleValueEvalMacro: function (aPrefix) { + return (item, string) => + VariablesView.simpleValueEvalMacro(item, string, aPrefix); + }, + _getGetterOrSetterEvalMacro: function (aPrefix) { + return (item, string) => + VariablesView.getterOrSetterEvalMacro(item, string, aPrefix); + }, + _getOverrideValueEvalMacro: function (aPrefix) { + return (item, string) => + VariablesView.overrideValueEvalMacro(item, string, aPrefix); + }, + + /** + * The mousemove listener for the source editor. + */ + _onMouseMove: function (e) { + // Prevent the variable inspection popup from showing when the thread client + // is not paused, or while a popup is already visible, or when the user tries + // to select text in the editor. + let isResumed = gThreadClient && gThreadClient.state != "paused"; + let isSelecting = this.DebuggerView.editor.somethingSelected() && e.buttons > 0; + let isPopupVisible = !this._tooltip.isHidden(); + if (isResumed || isSelecting || isPopupVisible) { + clearNamedTimeout("editor-mouse-move"); + return; + } + // Allow events to settle down first. If the mouse hovers over + // a certain point in the editor long enough, try showing a variable bubble. + setNamedTimeout("editor-mouse-move", + this.TOOLTIP_SHOW_DELAY, () => this._findIdentifier(e.clientX, e.clientY)); + }, + + /** + * The mouseout listener for the source editor container node. + */ + _onMouseOut: function () { + clearNamedTimeout("editor-mouse-move"); + }, + + /** + * Listener handling the popup hiding event. + */ + _onPopupHiding: function ({ target }) { + if (this._tooltip.panel != target) { + return; + } + if (this._markedText) { + this._markedText.clear(); + this._markedText = null; + } + if (!this._tooltip.isEmpty()) { + this._tooltip.empty(); + } + }, + + _editorContainer: null, + _markedText: null, + _tooltip: null +}; + +DebuggerView.VariableBubble = new VariableBubbleView(DebuggerController, DebuggerView); |