diff options
Diffstat (limited to 'devtools/client/webconsole/jsterm.js')
-rw-r--r-- | devtools/client/webconsole/jsterm.js | 1766 |
1 files changed, 1766 insertions, 0 deletions
diff --git a/devtools/client/webconsole/jsterm.js b/devtools/client/webconsole/jsterm.js new file mode 100644 index 000000000..8e3259afa --- /dev/null +++ b/devtools/client/webconsole/jsterm.js @@ -0,0 +1,1766 @@ +/* -*- 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/. */ + +"use strict"; + +const {Utils: WebConsoleUtils} = + require("devtools/client/webconsole/utils"); +const promise = require("promise"); +const Debugger = require("Debugger"); +const Services = require("Services"); +const {KeyCodes} = require("devtools/client/shared/keycodes"); + +loader.lazyServiceGetter(this, "clipboardHelper", + "@mozilla.org/widget/clipboardhelper;1", + "nsIClipboardHelper"); +loader.lazyRequireGetter(this, "EventEmitter", "devtools/shared/event-emitter"); +loader.lazyRequireGetter(this, "AutocompletePopup", "devtools/client/shared/autocomplete-popup", true); +loader.lazyRequireGetter(this, "ToolSidebar", "devtools/client/framework/sidebar", true); +loader.lazyRequireGetter(this, "Messages", "devtools/client/webconsole/console-output", true); +loader.lazyRequireGetter(this, "asyncStorage", "devtools/shared/async-storage"); +loader.lazyRequireGetter(this, "EnvironmentClient", "devtools/shared/client/main", true); +loader.lazyRequireGetter(this, "ObjectClient", "devtools/shared/client/main", true); +loader.lazyImporter(this, "VariablesView", "resource://devtools/client/shared/widgets/VariablesView.jsm"); +loader.lazyImporter(this, "VariablesViewController", "resource://devtools/client/shared/widgets/VariablesViewController.jsm"); +loader.lazyRequireGetter(this, "gDevTools", "devtools/client/framework/devtools", true); + +const STRINGS_URI = "devtools/client/locales/webconsole.properties"; +var l10n = new WebConsoleUtils.L10n(STRINGS_URI); + +// Constants used for defining the direction of JSTerm input history navigation. +const HISTORY_BACK = -1; +const HISTORY_FORWARD = 1; + +const XHTML_NS = "http://www.w3.org/1999/xhtml"; + +const HELP_URL = "https://developer.mozilla.org/docs/Tools/Web_Console/Helpers"; + +const VARIABLES_VIEW_URL = "chrome://devtools/content/shared/widgets/VariablesView.xul"; + +const PREF_INPUT_HISTORY_COUNT = "devtools.webconsole.inputHistoryCount"; +const PREF_AUTO_MULTILINE = "devtools.webconsole.autoMultiline"; + +/** + * Create a JSTerminal (a JavaScript command line). This is attached to an + * existing HeadsUpDisplay (a Web Console instance). This code is responsible + * with handling command line input, code evaluation and result output. + * + * @constructor + * @param object webConsoleFrame + * The WebConsoleFrame object that owns this JSTerm instance. + */ +function JSTerm(webConsoleFrame) { + this.hud = webConsoleFrame; + this.hudId = this.hud.hudId; + this.inputHistoryCount = Services.prefs.getIntPref(PREF_INPUT_HISTORY_COUNT); + + this.lastCompletion = { value: null }; + this._loadHistory(); + + this._objectActorsInVariablesViews = new Map(); + + this._keyPress = this._keyPress.bind(this); + this._inputEventHandler = this._inputEventHandler.bind(this); + this._focusEventHandler = this._focusEventHandler.bind(this); + this._onKeypressInVariablesView = this._onKeypressInVariablesView.bind(this); + this._blurEventHandler = this._blurEventHandler.bind(this); + + EventEmitter.decorate(this); +} + +JSTerm.prototype = { + SELECTED_FRAME: -1, + + /** + * Load the console history from previous sessions. + * @private + */ + _loadHistory: function () { + this.history = []; + this.historyIndex = this.historyPlaceHolder = 0; + + this.historyLoaded = asyncStorage.getItem("webConsoleHistory") + .then(value => { + if (Array.isArray(value)) { + // Since it was gotten asynchronously, there could be items already in + // the history. It's not likely but stick them onto the end anyway. + this.history = value.concat(this.history); + + // Holds the number of entries in history. This value is incremented + // in this.execute(). + this.historyIndex = this.history.length; + + // Holds the index of the history entry that the user is currently + // viewing. This is reset to this.history.length when this.execute() + // is invoked. + this.historyPlaceHolder = this.history.length; + } + }, console.error); + }, + + /** + * Clear the console history altogether. Note that this will not affect + * other consoles that are already opened (since they have their own copy), + * but it will reset the array for all newly-opened consoles. + * @returns Promise + * Resolves once the changes have been persisted. + */ + clearHistory: function () { + this.history = []; + this.historyIndex = this.historyPlaceHolder = 0; + return this.storeHistory(); + }, + + /** + * Stores the console history for future console instances. + * @returns Promise + * Resolves once the changes have been persisted. + */ + storeHistory: function () { + return asyncStorage.setItem("webConsoleHistory", this.history); + }, + + /** + * Stores the data for the last completion. + * @type object + */ + lastCompletion: null, + + /** + * Array that caches the user input suggestions received from the server. + * @private + * @type array + */ + _autocompleteCache: null, + + /** + * The input that caused the last request to the server, whose response is + * cached in the _autocompleteCache array. + * @private + * @type string + */ + _autocompleteQuery: null, + + /** + * The frameActorId used in the last autocomplete query. Whenever this changes + * the autocomplete cache must be invalidated. + * @private + * @type string + */ + _lastFrameActorId: null, + + /** + * The Web Console sidebar. + * @see this._createSidebar() + * @see Sidebar.jsm + */ + sidebar: null, + + /** + * The Variables View instance shown in the sidebar. + * @private + * @type object + */ + _variablesView: null, + + /** + * Tells if you want the variables view UI updates to be lazy or not. Tests + * disable lazy updates. + * + * @private + * @type boolean + */ + _lazyVariablesView: true, + + /** + * Holds a map between VariablesView instances and sets of ObjectActor IDs + * that have been retrieved from the server. This allows us to release the + * objects when needed. + * + * @private + * @type Map + */ + _objectActorsInVariablesViews: null, + + /** + * Last input value. + * @type string + */ + lastInputValue: "", + + /** + * Tells if the input node changed since the last focus. + * + * @private + * @type boolean + */ + _inputChanged: false, + + /** + * Tells if the autocomplete popup was navigated since the last open. + * + * @private + * @type boolean + */ + _autocompletePopupNavigated: false, + + /** + * History of code that was executed. + * @type array + */ + history: null, + autocompletePopup: null, + inputNode: null, + completeNode: null, + + /** + * Getter for the element that holds the messages we display. + * @type nsIDOMElement + */ + get outputNode() { + return this.hud.outputNode; + }, + + /** + * Getter for the debugger WebConsoleClient. + * @type object + */ + get webConsoleClient() { + return this.hud.webConsoleClient; + }, + + COMPLETE_FORWARD: 0, + COMPLETE_BACKWARD: 1, + COMPLETE_HINT_ONLY: 2, + COMPLETE_PAGEUP: 3, + COMPLETE_PAGEDOWN: 4, + + /** + * Initialize the JSTerminal UI. + */ + init: function () { + let autocompleteOptions = { + onSelect: this.onAutocompleteSelect.bind(this), + onClick: this.acceptProposedCompletion.bind(this), + listId: "webConsole_autocompletePopupListBox", + position: "top", + theme: "auto", + autoSelect: true + }; + + let doc = this.hud.document; + let toolbox = gDevTools.getToolbox(this.hud.owner.target); + let tooltipDoc = toolbox ? toolbox.doc : doc; + // The popup will be attached to the toolbox document or HUD document in the case + // such as the browser console which doesn't have a toolbox. + this.autocompletePopup = new AutocompletePopup(tooltipDoc, autocompleteOptions); + + let inputContainer = doc.querySelector(".jsterm-input-container"); + this.completeNode = doc.querySelector(".jsterm-complete-node"); + this.inputNode = doc.querySelector(".jsterm-input-node"); + + if (this.hud.isBrowserConsole && + !Services.prefs.getBoolPref("devtools.chrome.enabled")) { + inputContainer.style.display = "none"; + } else { + let okstring = l10n.getStr("selfxss.okstring"); + let msg = l10n.getFormatStr("selfxss.msg", [okstring]); + this._onPaste = WebConsoleUtils.pasteHandlerGen( + this.inputNode, doc.getElementById("webconsole-notificationbox"), + msg, okstring); + this.inputNode.addEventListener("keypress", this._keyPress, false); + this.inputNode.addEventListener("paste", this._onPaste); + this.inputNode.addEventListener("drop", this._onPaste); + this.inputNode.addEventListener("input", this._inputEventHandler, false); + this.inputNode.addEventListener("keyup", this._inputEventHandler, false); + this.inputNode.addEventListener("focus", this._focusEventHandler, false); + } + + this.hud.window.addEventListener("blur", this._blurEventHandler, false); + this.lastInputValue && this.setInputValue(this.lastInputValue); + }, + + focus: function () { + if (!this.inputNode.getAttribute("focused")) { + this.inputNode.focus(); + } + }, + + /** + * The JavaScript evaluation response handler. + * + * @private + * @param function [callback] + * Optional function to invoke when the evaluation result is added to + * the output. + * @param object response + * The message received from the server. + */ + _executeResultCallback: function (callback, response) { + if (!this.hud) { + return; + } + if (response.error) { + console.error("Evaluation error " + response.error + ": " + + response.message); + return; + } + let errorMessage = response.exceptionMessage; + let errorDocURL = response.exceptionDocURL; + + let errorDocLink; + if (errorDocURL) { + errorMessage += " "; + errorDocLink = this.hud.document.createElementNS(XHTML_NS, "a"); + errorDocLink.className = "learn-more-link webconsole-learn-more-link"; + errorDocLink.textContent = `[${l10n.getStr("webConsoleMoreInfoLabel")}]`; + errorDocLink.title = errorDocURL.split("?")[0]; + errorDocLink.href = "#"; + errorDocLink.draggable = false; + errorDocLink.addEventListener("click", () => { + this.hud.owner.openLink(errorDocURL); + }); + } + + // Wrap thrown strings in Error objects, so `throw "foo"` outputs + // "Error: foo" + if (typeof response.exception === "string") { + errorMessage = new Error(errorMessage).toString(); + } + let result = response.result; + let helperResult = response.helperResult; + let helperHasRawOutput = !!(helperResult || {}).rawOutput; + + if (helperResult && helperResult.type) { + switch (helperResult.type) { + case "clearOutput": + this.clearOutput(); + break; + case "clearHistory": + this.clearHistory(); + break; + case "inspectObject": + this.openVariablesView({ + label: + VariablesView.getString(helperResult.object, { concise: true }), + objectActor: helperResult.object, + }); + break; + case "error": + try { + errorMessage = l10n.getStr(helperResult.message); + } catch (ex) { + errorMessage = helperResult.message; + } + break; + case "help": + this.hud.owner.openLink(HELP_URL); + break; + case "copyValueToClipboard": + clipboardHelper.copyString(helperResult.value); + break; + } + } + + // Hide undefined results coming from JSTerm helper functions. + if (!errorMessage && result && typeof result == "object" && + result.type == "undefined" && + helperResult && !helperHasRawOutput) { + callback && callback(); + return; + } + + if (this.hud.NEW_CONSOLE_OUTPUT_ENABLED) { + this.hud.newConsoleOutput.dispatchMessageAdd(response, true).then(callback); + return; + } + let msg = new Messages.JavaScriptEvalOutput(response, + errorMessage, errorDocLink); + this.hud.output.addMessage(msg); + + if (callback) { + let oldFlushCallback = this.hud._flushCallback; + this.hud._flushCallback = () => { + callback(msg.element); + if (oldFlushCallback) { + oldFlushCallback(); + this.hud._flushCallback = oldFlushCallback; + return true; + } + + return false; + }; + } + + msg._objectActors = new Set(); + + if (WebConsoleUtils.isActorGrip(response.exception)) { + msg._objectActors.add(response.exception.actor); + } + + if (WebConsoleUtils.isActorGrip(result)) { + msg._objectActors.add(result.actor); + } + }, + + /** + * Execute a string. Execution happens asynchronously in the content process. + * + * @param string [executeString] + * The string you want to execute. If this is not provided, the current + * user input is used - taken from |this.getInputValue()|. + * @param function [callback] + * Optional function to invoke when the result is displayed. + * This is deprecated - please use the promise return value instead. + * @returns Promise + * Resolves with the message once the result is displayed. + */ + execute: function (executeString, callback) { + let deferred = promise.defer(); + let resultCallback; + if (this.hud.NEW_CONSOLE_OUTPUT_ENABLED) { + resultCallback = (msg) => deferred.resolve(msg); + } else { + resultCallback = (msg) => { + deferred.resolve(msg); + if (callback) { + callback(msg); + } + }; + } + + // attempt to execute the content of the inputNode + executeString = executeString || this.getInputValue(); + if (!executeString) { + return null; + } + + let selectedNodeActor = null; + let inspectorSelection = this.hud.owner.getInspectorSelection(); + if (inspectorSelection && inspectorSelection.nodeFront) { + selectedNodeActor = inspectorSelection.nodeFront.actorID; + } + + if (this.hud.NEW_CONSOLE_OUTPUT_ENABLED) { + const { ConsoleCommand } = require("devtools/client/webconsole/new-console-output/types"); + let message = new ConsoleCommand({ + messageText: executeString, + }); + this.hud.proxy.dispatchMessageAdd(message); + } else { + let message = new Messages.Simple(executeString, { + category: "input", + severity: "log", + }); + this.hud.output.addMessage(message); + } + let onResult = this._executeResultCallback.bind(this, resultCallback); + + let options = { + frame: this.SELECTED_FRAME, + selectedNodeActor: selectedNodeActor, + }; + + this.requestEvaluation(executeString, options).then(onResult, onResult); + + // Append a new value in the history of executed code, or overwrite the most + // recent entry. The most recent entry may contain the last edited input + // value that was not evaluated yet. + this.history[this.historyIndex++] = executeString; + this.historyPlaceHolder = this.history.length; + + if (this.history.length > this.inputHistoryCount) { + this.history.splice(0, this.history.length - this.inputHistoryCount); + this.historyIndex = this.historyPlaceHolder = this.history.length; + } + this.storeHistory(); + WebConsoleUtils.usageCount++; + this.setInputValue(""); + this.clearCompletion(); + return deferred.promise; + }, + + /** + * Request a JavaScript string evaluation from the server. + * + * @param string str + * String to execute. + * @param object [options] + * Options for evaluation: + * - bindObjectActor: tells the ObjectActor ID for which you want to do + * the evaluation. The Debugger.Object of the OA will be bound to + * |_self| during evaluation, such that it's usable in the string you + * execute. + * - frame: tells the stackframe depth to evaluate the string in. If + * the jsdebugger is paused, you can pick the stackframe to be used for + * evaluation. Use |this.SELECTED_FRAME| to always pick the + * user-selected stackframe. + * If you do not provide a |frame| the string will be evaluated in the + * global content window. + * - selectedNodeActor: tells the NodeActor ID of the current selection + * in the Inspector, if such a selection exists. This is used by + * helper functions that can evaluate on the current selection. + * @return object + * A promise object that is resolved when the server response is + * received. + */ + requestEvaluation: function (str, options = {}) { + let deferred = promise.defer(); + + function onResult(response) { + if (!response.error) { + deferred.resolve(response); + } else { + deferred.reject(response); + } + } + + let frameActor = null; + if ("frame" in options) { + frameActor = this.getFrameActor(options.frame); + } + + let evalOptions = { + bindObjectActor: options.bindObjectActor, + frameActor: frameActor, + selectedNodeActor: options.selectedNodeActor, + selectedObjectActor: options.selectedObjectActor, + }; + + this.webConsoleClient.evaluateJSAsync(str, onResult, evalOptions); + return deferred.promise; + }, + + /** + * Retrieve the FrameActor ID given a frame depth. + * + * @param number frame + * Frame depth. + * @return string|null + * The FrameActor ID for the given frame depth. + */ + getFrameActor: function (frame) { + let state = this.hud.owner.getDebuggerFrames(); + if (!state) { + return null; + } + + let grip; + if (frame == this.SELECTED_FRAME) { + grip = state.frames[state.selected]; + } else { + grip = state.frames[frame]; + } + + return grip ? grip.actor : null; + }, + + /** + * Opens a new variables view that allows the inspection of the given object. + * + * @param object options + * Options for the variables view: + * - objectActor: grip of the ObjectActor you want to show in the + * variables view. + * - rawObject: the raw object you want to show in the variables view. + * - label: label to display in the variables view for inspected + * object. + * - hideFilterInput: optional boolean, |true| if you want to hide the + * variables view filter input. + * - targetElement: optional nsIDOMElement to append the variables view + * to. An iframe element is used as a container for the view. If this + * option is not used, then the variables view opens in the sidebar. + * - autofocus: optional boolean, |true| if you want to give focus to + * the variables view window after open, |false| otherwise. + * @return object + * A promise object that is resolved when the variables view has + * opened. The new variables view instance is given to the callbacks. + */ + openVariablesView: function (options) { + let onContainerReady = (window) => { + let container = window.document.querySelector("#variables"); + let view = this._variablesView; + if (!view || options.targetElement) { + let viewOptions = { + container: container, + hideFilterInput: options.hideFilterInput, + }; + view = this._createVariablesView(viewOptions); + if (!options.targetElement) { + this._variablesView = view; + window.addEventListener("keypress", this._onKeypressInVariablesView); + } + } + options.view = view; + this._updateVariablesView(options); + + if (!options.targetElement && options.autofocus) { + window.focus(); + } + + this.emit("variablesview-open", view, options); + return view; + }; + + let openPromise; + if (options.targetElement) { + let deferred = promise.defer(); + openPromise = deferred.promise; + let document = options.targetElement.ownerDocument; + let iframe = document.createElementNS(XHTML_NS, "iframe"); + + iframe.addEventListener("load", function onIframeLoad() { + iframe.removeEventListener("load", onIframeLoad, true); + iframe.style.visibility = "visible"; + deferred.resolve(iframe.contentWindow); + }, true); + + iframe.flex = 1; + iframe.style.visibility = "hidden"; + iframe.setAttribute("src", VARIABLES_VIEW_URL); + options.targetElement.appendChild(iframe); + } else { + if (!this.sidebar) { + this._createSidebar(); + } + openPromise = this._addVariablesViewSidebarTab(); + } + + return openPromise.then(onContainerReady); + }, + + /** + * Create the Web Console sidebar. + * + * @see devtools/framework/sidebar.js + * @private + */ + _createSidebar: function () { + let tabbox = this.hud.document.querySelector("#webconsole-sidebar"); + this.sidebar = new ToolSidebar(tabbox, this, "webconsole"); + this.sidebar.show(); + this.emit("sidebar-opened"); + }, + + /** + * Add the variables view tab to the sidebar. + * + * @private + * @return object + * A promise object for the adding of the new tab. + */ + _addVariablesViewSidebarTab: function () { + let deferred = promise.defer(); + + let onTabReady = () => { + let window = this.sidebar.getWindowForTab("variablesview"); + deferred.resolve(window); + }; + + let tabPanel = this.sidebar.getTabPanel("variablesview"); + if (tabPanel) { + if (this.sidebar.getCurrentTabID() == "variablesview") { + onTabReady(); + } else { + this.sidebar.once("variablesview-selected", onTabReady); + this.sidebar.select("variablesview"); + } + } else { + this.sidebar.once("variablesview-ready", onTabReady); + this.sidebar.addTab("variablesview", VARIABLES_VIEW_URL, {selected: true}); + } + + return deferred.promise; + }, + + /** + * The keypress event handler for the Variables View sidebar. Currently this + * is used for removing the sidebar when Escape is pressed. + * + * @private + * @param nsIDOMEvent event + * The keypress DOM event object. + */ + _onKeypressInVariablesView: function (event) { + let tag = event.target.nodeName; + if (event.keyCode != KeyCodes.DOM_VK_ESCAPE || event.shiftKey || + event.altKey || event.ctrlKey || event.metaKey || + ["input", "textarea", "select", "textbox"].indexOf(tag) > -1) { + return; + } + + this._sidebarDestroy(); + this.focus(); + event.stopPropagation(); + }, + + /** + * Create a variables view instance. + * + * @private + * @param object options + * Options for the new Variables View instance: + * - container: the DOM element where the variables view is inserted. + * - hideFilterInput: boolean, if true the variables filter input is + * hidden. + * @return object + * The new Variables View instance. + */ + _createVariablesView: function (options) { + let view = new VariablesView(options.container); + view.toolbox = gDevTools.getToolbox(this.hud.owner.target); + view.searchPlaceholder = l10n.getStr("propertiesFilterPlaceholder"); + view.emptyText = l10n.getStr("emptyPropertiesList"); + view.searchEnabled = !options.hideFilterInput; + view.lazyEmpty = this._lazyVariablesView; + + VariablesViewController.attach(view, { + getEnvironmentClient: grip => { + return new EnvironmentClient(this.hud.proxy.client, grip); + }, + getObjectClient: grip => { + return new ObjectClient(this.hud.proxy.client, grip); + }, + getLongStringClient: grip => { + return this.webConsoleClient.longString(grip); + }, + releaseActor: actor => { + this.hud._releaseObject(actor); + }, + simpleValueEvalMacro: simpleValueEvalMacro, + overrideValueEvalMacro: overrideValueEvalMacro, + getterOrSetterEvalMacro: getterOrSetterEvalMacro, + }); + + // Relay events from the VariablesView. + view.on("fetched", (event, type, variableObject) => { + this.emit("variablesview-fetched", variableObject); + }); + + return view; + }, + + /** + * Update the variables view. + * + * @private + * @param object options + * Options for updating the variables view: + * - view: the view you want to update. + * - objectActor: the grip of the new ObjectActor you want to show in + * the view. + * - rawObject: the new raw object you want to show. + * - label: the new label for the inspected object. + */ + _updateVariablesView: function (options) { + let view = options.view; + view.empty(); + + // We need to avoid pruning the object inspection starting point. + // That one is pruned when the console message is removed. + view.controller.releaseActors(actor => { + return view._consoleLastObjectActor != actor; + }); + + if (options.objectActor && + (!this.hud.isBrowserConsole || + Services.prefs.getBoolPref("devtools.chrome.enabled"))) { + // Make sure eval works in the correct context. + view.eval = this._variablesViewEvaluate.bind(this, options); + view.switch = this._variablesViewSwitch.bind(this, options); + view.delete = this._variablesViewDelete.bind(this, options); + } else { + view.eval = null; + view.switch = null; + view.delete = null; + } + + let { variable, expanded } = view.controller.setSingleVariable(options); + variable.evaluationMacro = simpleValueEvalMacro; + + if (options.objectActor) { + view._consoleLastObjectActor = options.objectActor.actor; + } else if (options.rawObject) { + view._consoleLastObjectActor = null; + } else { + throw new Error( + "Variables View cannot open without giving it an object display."); + } + + expanded.then(() => { + this.emit("variablesview-updated", view, options); + }); + }, + + /** + * The evaluation function used by the variables view when editing a property + * value. + * + * @private + * @param object options + * The options used for |this._updateVariablesView()|. + * @param object variableObject + * The Variable object instance for the edited property. + * @param string value + * The value the edited property was changed to. + */ + _variablesViewEvaluate: function (options, variableObject, value) { + let updater = this._updateVariablesView.bind(this, options); + let onEval = this._silentEvalCallback.bind(this, updater); + let string = variableObject.evaluationMacro(variableObject, value); + + let evalOptions = { + frame: this.SELECTED_FRAME, + bindObjectActor: options.objectActor.actor, + }; + + this.requestEvaluation(string, evalOptions).then(onEval, onEval); + }, + + /** + * The property deletion function used by the variables view when a property + * is deleted. + * + * @private + * @param object options + * The options used for |this._updateVariablesView()|. + * @param object variableObject + * The Variable object instance for the deleted property. + */ + _variablesViewDelete: function (options, variableObject) { + let onEval = this._silentEvalCallback.bind(this, null); + + let evalOptions = { + frame: this.SELECTED_FRAME, + bindObjectActor: options.objectActor.actor, + }; + + this.requestEvaluation("delete _self" + + variableObject.symbolicName, evalOptions).then(onEval, onEval); + }, + + /** + * The property rename function used by the variables view when a property + * is renamed. + * + * @private + * @param object options + * The options used for |this._updateVariablesView()|. + * @param object variableObject + * The Variable object instance for the renamed property. + * @param string newName + * The new name for the property. + */ + _variablesViewSwitch: function (options, variableObject, newName) { + let updater = this._updateVariablesView.bind(this, options); + let onEval = this._silentEvalCallback.bind(this, updater); + + let evalOptions = { + frame: this.SELECTED_FRAME, + bindObjectActor: options.objectActor.actor, + }; + + let newSymbolicName = + variableObject.ownerView.symbolicName + '["' + newName + '"]'; + if (newSymbolicName == variableObject.symbolicName) { + return; + } + + let code = "_self" + newSymbolicName + " = _self" + + variableObject.symbolicName + ";" + "delete _self" + + variableObject.symbolicName; + + this.requestEvaluation(code, evalOptions).then(onEval, onEval); + }, + + /** + * A noop callback for JavaScript evaluation. This method releases any + * result ObjectActors that come from the server for evaluation requests. This + * is used for editing, renaming and deleting properties in the variables + * view. + * + * Exceptions are displayed in the output. + * + * @private + * @param function callback + * Function to invoke once the response is received. + * @param object response + * The response packet received from the server. + */ + _silentEvalCallback: function (callback, response) { + if (response.error) { + console.error("Web Console evaluation failed. " + response.error + ":" + + response.message); + + callback && callback(response); + return; + } + + if (response.exceptionMessage) { + let message = new Messages.Simple(response.exceptionMessage, { + category: "output", + severity: "error", + timestamp: response.timestamp, + }); + this.hud.output.addMessage(message); + message._objectActors = new Set(); + if (WebConsoleUtils.isActorGrip(response.exception)) { + message._objectActors.add(response.exception.actor); + } + } + + let helper = response.helperResult || { type: null }; + let helperGrip = null; + if (helper.type == "inspectObject") { + helperGrip = helper.object; + } + + let grips = [response.result, helperGrip]; + for (let grip of grips) { + if (WebConsoleUtils.isActorGrip(grip)) { + this.hud._releaseObject(grip.actor); + } + } + + callback && callback(response); + }, + + /** + * Clear the Web Console output. + * + * This method emits the "messages-cleared" notification. + * + * @param boolean clearStorage + * True if you want to clear the console messages storage associated to + * this Web Console. + */ + clearOutput: function (clearStorage) { + let hud = this.hud; + let outputNode = hud.outputNode; + let node; + while ((node = outputNode.firstChild)) { + hud.removeOutputMessage(node); + } + + hud.groupDepth = 0; + hud._outputQueue.forEach(hud._destroyItem, hud); + hud._outputQueue = []; + this.webConsoleClient.clearNetworkRequests(); + hud._repeatNodes = {}; + + if (clearStorage) { + this.webConsoleClient.clearMessagesCache(); + } + + this._sidebarDestroy(); + + if (hud.NEW_CONSOLE_OUTPUT_ENABLED) { + hud.newConsoleOutput.dispatchMessagesClear(); + } + + this.emit("messages-cleared"); + }, + + /** + * Remove all of the private messages from the Web Console output. + * + * This method emits the "private-messages-cleared" notification. + */ + clearPrivateMessages: function () { + let nodes = this.hud.outputNode.querySelectorAll(".message[private]"); + for (let node of nodes) { + this.hud.removeOutputMessage(node); + } + this.emit("private-messages-cleared"); + }, + + /** + * Updates the size of the input field (command line) to fit its contents. + * + * @returns void + */ + resizeInput: function () { + let inputNode = this.inputNode; + + // Reset the height so that scrollHeight will reflect the natural height of + // the contents of the input field. + inputNode.style.height = "auto"; + + // Now resize the input field to fit its contents. + let scrollHeight = inputNode.inputField.scrollHeight; + if (scrollHeight > 0) { + inputNode.style.height = scrollHeight + "px"; + } + }, + + /** + * Sets the value of the input field (command line), and resizes the field to + * fit its contents. This method is preferred over setting "inputNode.value" + * directly, because it correctly resizes the field. + * + * @param string newValue + * The new value to set. + * @returns void + */ + setInputValue: function (newValue) { + this.inputNode.value = newValue; + this.lastInputValue = newValue; + this.completeNode.value = ""; + this.resizeInput(); + this._inputChanged = true; + this.emit("set-input-value"); + }, + + /** + * Gets the value from the input field + * @returns string + */ + getInputValue: function () { + return this.inputNode.value || ""; + }, + + /** + * The inputNode "input" and "keyup" event handler. + * @private + */ + _inputEventHandler: function () { + if (this.lastInputValue != this.getInputValue()) { + this.resizeInput(); + this.complete(this.COMPLETE_HINT_ONLY); + this.lastInputValue = this.getInputValue(); + this._inputChanged = true; + } + }, + + /** + * The window "blur" event handler. + * @private + */ + _blurEventHandler: function () { + if (this.autocompletePopup) { + this.clearCompletion(); + } + }, + + /* eslint-disable complexity */ + /** + * The inputNode "keypress" event handler. + * + * @private + * @param nsIDOMEvent event + */ + _keyPress: function (event) { + let inputNode = this.inputNode; + let inputValue = this.getInputValue(); + let inputUpdated = false; + + if (event.ctrlKey) { + switch (event.charCode) { + case 101: + // control-e + if (Services.appinfo.OS == "WINNT") { + break; + } + let lineEndPos = inputValue.length; + if (this.hasMultilineInput()) { + // find index of closest newline >= cursor + for (let i = inputNode.selectionEnd; i < lineEndPos; i++) { + if (inputValue.charAt(i) == "\r" || + inputValue.charAt(i) == "\n") { + lineEndPos = i; + break; + } + } + } + inputNode.setSelectionRange(lineEndPos, lineEndPos); + event.preventDefault(); + this.clearCompletion(); + break; + + case 110: + // Control-N differs from down arrow: it ignores autocomplete state. + // Note that we preserve the default 'down' navigation within + // multiline text. + if (Services.appinfo.OS == "Darwin" && + this.canCaretGoNext() && + this.historyPeruse(HISTORY_FORWARD)) { + event.preventDefault(); + // Ctrl-N is also used to focus the Network category button on + // MacOSX. The preventDefault() call doesn't prevent the focus + // from moving away from the input. + this.focus(); + } + this.clearCompletion(); + break; + + case 112: + // Control-P differs from up arrow: it ignores autocomplete state. + // Note that we preserve the default 'up' navigation within + // multiline text. + if (Services.appinfo.OS == "Darwin" && + this.canCaretGoPrevious() && + this.historyPeruse(HISTORY_BACK)) { + event.preventDefault(); + // Ctrl-P may also be used to focus some category button on MacOSX. + // The preventDefault() call doesn't prevent the focus from moving + // away from the input. + this.focus(); + } + this.clearCompletion(); + break; + default: + break; + } + return; + } else if (event.keyCode == KeyCodes.DOM_VK_RETURN) { + let autoMultiline = Services.prefs.getBoolPref(PREF_AUTO_MULTILINE); + if (event.shiftKey || + (!Debugger.isCompilableUnit(inputNode.value) && autoMultiline)) { + // shift return or incomplete statement + return; + } + } + + switch (event.keyCode) { + case KeyCodes.DOM_VK_ESCAPE: + if (this.autocompletePopup.isOpen) { + this.clearCompletion(); + event.preventDefault(); + event.stopPropagation(); + } else if (this.sidebar) { + this._sidebarDestroy(); + event.preventDefault(); + event.stopPropagation(); + } + break; + + case KeyCodes.DOM_VK_RETURN: + if (this._autocompletePopupNavigated && + this.autocompletePopup.isOpen && + this.autocompletePopup.selectedIndex > -1) { + this.acceptProposedCompletion(); + } else { + this.execute(); + this._inputChanged = false; + } + event.preventDefault(); + break; + + case KeyCodes.DOM_VK_UP: + if (this.autocompletePopup.isOpen) { + inputUpdated = this.complete(this.COMPLETE_BACKWARD); + if (inputUpdated) { + this._autocompletePopupNavigated = true; + } + } else if (this.canCaretGoPrevious()) { + inputUpdated = this.historyPeruse(HISTORY_BACK); + } + if (inputUpdated) { + event.preventDefault(); + } + break; + + case KeyCodes.DOM_VK_DOWN: + if (this.autocompletePopup.isOpen) { + inputUpdated = this.complete(this.COMPLETE_FORWARD); + if (inputUpdated) { + this._autocompletePopupNavigated = true; + } + } else if (this.canCaretGoNext()) { + inputUpdated = this.historyPeruse(HISTORY_FORWARD); + } + if (inputUpdated) { + event.preventDefault(); + } + break; + + case KeyCodes.DOM_VK_PAGE_UP: + if (this.autocompletePopup.isOpen) { + inputUpdated = this.complete(this.COMPLETE_PAGEUP); + if (inputUpdated) { + this._autocompletePopupNavigated = true; + } + } else { + this.hud.outputScroller.scrollTop = + Math.max(0, + this.hud.outputScroller.scrollTop - + this.hud.outputScroller.clientHeight + ); + } + event.preventDefault(); + break; + + case KeyCodes.DOM_VK_PAGE_DOWN: + if (this.autocompletePopup.isOpen) { + inputUpdated = this.complete(this.COMPLETE_PAGEDOWN); + if (inputUpdated) { + this._autocompletePopupNavigated = true; + } + } else { + this.hud.outputScroller.scrollTop = + Math.min(this.hud.outputScroller.scrollHeight, + this.hud.outputScroller.scrollTop + + this.hud.outputScroller.clientHeight + ); + } + event.preventDefault(); + break; + + case KeyCodes.DOM_VK_HOME: + if (this.autocompletePopup.isOpen) { + this.autocompletePopup.selectedIndex = 0; + event.preventDefault(); + } else if (inputValue.length <= 0) { + this.hud.outputScroller.scrollTop = 0; + event.preventDefault(); + } + break; + + case KeyCodes.DOM_VK_END: + if (this.autocompletePopup.isOpen) { + this.autocompletePopup.selectedIndex = + this.autocompletePopup.itemCount - 1; + event.preventDefault(); + } else if (inputValue.length <= 0) { + this.hud.outputScroller.scrollTop = + this.hud.outputScroller.scrollHeight; + event.preventDefault(); + } + break; + + case KeyCodes.DOM_VK_LEFT: + if (this.autocompletePopup.isOpen || this.lastCompletion.value) { + this.clearCompletion(); + } + break; + + case KeyCodes.DOM_VK_RIGHT: + let cursorAtTheEnd = this.inputNode.selectionStart == + this.inputNode.selectionEnd && + this.inputNode.selectionStart == + inputValue.length; + let haveSuggestion = this.autocompletePopup.isOpen || + this.lastCompletion.value; + let useCompletion = cursorAtTheEnd || this._autocompletePopupNavigated; + if (haveSuggestion && useCompletion && + this.complete(this.COMPLETE_HINT_ONLY) && + this.lastCompletion.value && + this.acceptProposedCompletion()) { + event.preventDefault(); + } + if (this.autocompletePopup.isOpen) { + this.clearCompletion(); + } + break; + + case KeyCodes.DOM_VK_TAB: + // Generate a completion and accept the first proposed value. + if (this.complete(this.COMPLETE_HINT_ONLY) && + this.lastCompletion && + this.acceptProposedCompletion()) { + event.preventDefault(); + } else if (this._inputChanged) { + this.updateCompleteNode(l10n.getStr("Autocomplete.blank")); + event.preventDefault(); + } + break; + default: + break; + } + }, + /* eslint-enable complexity */ + + /** + * The inputNode "focus" event handler. + * @private + */ + _focusEventHandler: function () { + this._inputChanged = false; + }, + + /** + * Go up/down the history stack of input values. + * + * @param number direction + * History navigation direction: HISTORY_BACK or HISTORY_FORWARD. + * + * @returns boolean + * True if the input value changed, false otherwise. + */ + historyPeruse: function (direction) { + if (!this.history.length) { + return false; + } + + // Up Arrow key + if (direction == HISTORY_BACK) { + if (this.historyPlaceHolder <= 0) { + return false; + } + let inputVal = this.history[--this.historyPlaceHolder]; + + // Save the current input value as the latest entry in history, only if + // the user is already at the last entry. + // Note: this code does not store changes to items that are already in + // history. + if (this.historyPlaceHolder + 1 == this.historyIndex) { + this.history[this.historyIndex] = this.getInputValue() || ""; + } + + this.setInputValue(inputVal); + } else if (direction == HISTORY_FORWARD) { + // Down Arrow key + if (this.historyPlaceHolder >= (this.history.length - 1)) { + return false; + } + + let inputVal = this.history[++this.historyPlaceHolder]; + this.setInputValue(inputVal); + } else { + throw new Error("Invalid argument 0"); + } + + return true; + }, + + /** + * Test for multiline input. + * + * @return boolean + * True if CR or LF found in node value; else false. + */ + hasMultilineInput: function () { + return /[\r\n]/.test(this.getInputValue()); + }, + + /** + * Check if the caret is at a location that allows selecting the previous item + * in history when the user presses the Up arrow key. + * + * @return boolean + * True if the caret is at a location that allows selecting the + * previous item in history when the user presses the Up arrow key, + * otherwise false. + */ + canCaretGoPrevious: function () { + let node = this.inputNode; + if (node.selectionStart != node.selectionEnd) { + return false; + } + + let multiline = /[\r\n]/.test(node.value); + return node.selectionStart == 0 ? true : + node.selectionStart == node.value.length && !multiline; + }, + + /** + * Check if the caret is at a location that allows selecting the next item in + * history when the user presses the Down arrow key. + * + * @return boolean + * True if the caret is at a location that allows selecting the next + * item in history when the user presses the Down arrow key, otherwise + * false. + */ + canCaretGoNext: function () { + let node = this.inputNode; + if (node.selectionStart != node.selectionEnd) { + return false; + } + + let multiline = /[\r\n]/.test(node.value); + return node.selectionStart == node.value.length ? true : + node.selectionStart == 0 && !multiline; + }, + + /** + * Completes the current typed text in the inputNode. Completion is performed + * only if the selection/cursor is at the end of the string. If no completion + * is found, the current inputNode value and cursor/selection stay. + * + * @param int type possible values are + * - this.COMPLETE_FORWARD: If there is more than one possible completion + * and the input value stayed the same compared to the last time this + * function was called, then the next completion of all possible + * completions is used. If the value changed, then the first possible + * completion is used and the selection is set from the current + * cursor position to the end of the completed text. + * If there is only one possible completion, then this completion + * value is used and the cursor is put at the end of the completion. + * - this.COMPLETE_BACKWARD: Same as this.COMPLETE_FORWARD but if the + * value stayed the same as the last time the function was called, + * then the previous completion of all possible completions is used. + * - this.COMPLETE_PAGEUP: Scroll up one page if available or select the + * first item. + * - this.COMPLETE_PAGEDOWN: Scroll down one page if available or select + * the last item. + * - this.COMPLETE_HINT_ONLY: If there is more than one possible + * completion and the input value stayed the same compared to the + * last time this function was called, then the same completion is + * used again. If there is only one possible completion, then + * the this.getInputValue() is set to this value and the selection + * is set from the current cursor position to the end of the + * completed text. + * @param function callback + * Optional function invoked when the autocomplete properties are + * updated. + * @returns boolean true if there existed a completion for the current input, + * or false otherwise. + */ + complete: function (type, callback) { + let inputNode = this.inputNode; + let inputValue = this.getInputValue(); + let frameActor = this.getFrameActor(this.SELECTED_FRAME); + + // If the inputNode has no value, then don't try to complete on it. + if (!inputValue) { + this.clearCompletion(); + callback && callback(this); + this.emit("autocomplete-updated"); + return false; + } + + // Only complete if the selection is empty. + if (inputNode.selectionStart != inputNode.selectionEnd) { + this.clearCompletion(); + callback && callback(this); + this.emit("autocomplete-updated"); + return false; + } + + // Update the completion results. + if (this.lastCompletion.value != inputValue || + frameActor != this._lastFrameActorId) { + this._updateCompletionResult(type, callback); + return false; + } + + let popup = this.autocompletePopup; + let accepted = false; + + if (type != this.COMPLETE_HINT_ONLY && popup.itemCount == 1) { + this.acceptProposedCompletion(); + accepted = true; + } else if (type == this.COMPLETE_BACKWARD) { + popup.selectPreviousItem(); + } else if (type == this.COMPLETE_FORWARD) { + popup.selectNextItem(); + } else if (type == this.COMPLETE_PAGEUP) { + popup.selectPreviousPageItem(); + } else if (type == this.COMPLETE_PAGEDOWN) { + popup.selectNextPageItem(); + } + + callback && callback(this); + this.emit("autocomplete-updated"); + return accepted || popup.itemCount > 0; + }, + + /** + * Update the completion result. This operation is performed asynchronously by + * fetching updated results from the content process. + * + * @private + * @param int type + * Completion type. See this.complete() for details. + * @param function [callback] + * Optional, function to invoke when completion results are received. + */ + _updateCompletionResult: function (type, callback) { + let frameActor = this.getFrameActor(this.SELECTED_FRAME); + if (this.lastCompletion.value == this.getInputValue() && + frameActor == this._lastFrameActorId) { + return; + } + + let requestId = gSequenceId(); + let cursor = this.inputNode.selectionStart; + let input = this.getInputValue().substring(0, cursor); + let cache = this._autocompleteCache; + + // If the current input starts with the previous input, then we already + // have a list of suggestions and we just need to filter the cached + // suggestions. When the current input ends with a non-alphanumeric + // character we ask the server again for suggestions. + + // Check if last character is non-alphanumeric + if (!/[a-zA-Z0-9]$/.test(input) || frameActor != this._lastFrameActorId) { + this._autocompleteQuery = null; + this._autocompleteCache = null; + } + + if (this._autocompleteQuery && input.startsWith(this._autocompleteQuery)) { + let filterBy = input; + // Find the last non-alphanumeric other than _ or $ if it exists. + let lastNonAlpha = input.match(/[^a-zA-Z0-9_$][a-zA-Z0-9_$]*$/); + // If input contains non-alphanumerics, use the part after the last one + // to filter the cache + if (lastNonAlpha) { + filterBy = input.substring(input.lastIndexOf(lastNonAlpha) + 1); + } + + let newList = cache.sort().filter(function (l) { + return l.startsWith(filterBy); + }); + + this.lastCompletion = { + requestId: null, + completionType: type, + value: null, + }; + + let response = { matches: newList, matchProp: filterBy }; + this._receiveAutocompleteProperties(null, callback, response); + return; + } + + this._lastFrameActorId = frameActor; + + this.lastCompletion = { + requestId: requestId, + completionType: type, + value: null, + }; + + let autocompleteCallback = + this._receiveAutocompleteProperties.bind(this, requestId, callback); + + this.webConsoleClient.autocomplete( + input, cursor, autocompleteCallback, frameActor); + }, + + /** + * Handler for the autocompletion results. This method takes + * the completion result received from the server and updates the UI + * accordingly. + * + * @param number requestId + * Request ID. + * @param function [callback=null] + * Optional, function to invoke when the completion result is received. + * @param object message + * The JSON message which holds the completion results received from + * the content process. + */ + _receiveAutocompleteProperties: function (requestId, callback, message) { + let inputNode = this.inputNode; + let inputValue = this.getInputValue(); + if (this.lastCompletion.value == inputValue || + requestId != this.lastCompletion.requestId) { + return; + } + // Cache whatever came from the server if the last char is + // alphanumeric or '.' + let cursor = inputNode.selectionStart; + let inputUntilCursor = inputValue.substring(0, cursor); + + if (requestId != null && /[a-zA-Z0-9.]$/.test(inputUntilCursor)) { + this._autocompleteCache = message.matches; + this._autocompleteQuery = inputUntilCursor; + } + + let matches = message.matches; + let lastPart = message.matchProp; + if (!matches.length) { + this.clearCompletion(); + callback && callback(this); + this.emit("autocomplete-updated"); + return; + } + + let items = matches.reverse().map(function (match) { + return { preLabel: lastPart, label: match }; + }); + + let popup = this.autocompletePopup; + popup.setItems(items); + + let completionType = this.lastCompletion.completionType; + this.lastCompletion = { + value: inputValue, + matchProp: lastPart, + }; + + if (items.length > 1 && !popup.isOpen) { + let str = this.getInputValue().substr(0, this.inputNode.selectionStart); + let offset = str.length - (str.lastIndexOf("\n") + 1) - lastPart.length; + let x = offset * this.hud._inputCharWidth; + popup.openPopup(inputNode, x + this.hud._chevronWidth); + this._autocompletePopupNavigated = false; + } else if (items.length < 2 && popup.isOpen) { + popup.hidePopup(); + this._autocompletePopupNavigated = false; + } + + if (items.length == 1) { + popup.selectedIndex = 0; + } + + this.onAutocompleteSelect(); + + if (completionType != this.COMPLETE_HINT_ONLY && popup.itemCount == 1) { + this.acceptProposedCompletion(); + } else if (completionType == this.COMPLETE_BACKWARD) { + popup.selectPreviousItem(); + } else if (completionType == this.COMPLETE_FORWARD) { + popup.selectNextItem(); + } + + callback && callback(this); + this.emit("autocomplete-updated"); + }, + + onAutocompleteSelect: function () { + // Render the suggestion only if the cursor is at the end of the input. + if (this.inputNode.selectionStart != this.getInputValue().length) { + return; + } + + let currentItem = this.autocompletePopup.selectedItem; + if (currentItem && this.lastCompletion.value) { + let suffix = + currentItem.label.substring(this.lastCompletion.matchProp.length); + this.updateCompleteNode(suffix); + } else { + this.updateCompleteNode(""); + } + }, + + /** + * Clear the current completion information and close the autocomplete popup, + * if needed. + */ + clearCompletion: function () { + this.autocompletePopup.clearItems(); + this.lastCompletion = { value: null }; + this.updateCompleteNode(""); + if (this.autocompletePopup.isOpen) { + // Trigger a blur/focus of the JSTerm input to force screen readers to read the + // value again. + this.inputNode.blur(); + this.autocompletePopup.once("popup-closed", () => { + this.inputNode.focus(); + }); + this.autocompletePopup.hidePopup(); + this._autocompletePopupNavigated = false; + } + }, + + /** + * Accept the proposed input completion. + * + * @return boolean + * True if there was a selected completion item and the input value + * was updated, false otherwise. + */ + acceptProposedCompletion: function () { + let updated = false; + + let currentItem = this.autocompletePopup.selectedItem; + if (currentItem && this.lastCompletion.value) { + let suffix = + currentItem.label.substring(this.lastCompletion.matchProp.length); + let cursor = this.inputNode.selectionStart; + let value = this.getInputValue(); + this.setInputValue(value.substr(0, cursor) + + suffix + value.substr(cursor)); + let newCursor = cursor + suffix.length; + this.inputNode.selectionStart = this.inputNode.selectionEnd = newCursor; + updated = true; + } + + this.clearCompletion(); + + return updated; + }, + + /** + * Update the node that displays the currently selected autocomplete proposal. + * + * @param string suffix + * The proposed suffix for the inputNode value. + */ + updateCompleteNode: function (suffix) { + // completion prefix = input, with non-control chars replaced by spaces + let prefix = suffix ? this.getInputValue().replace(/[\S]/g, " ") : ""; + this.completeNode.value = prefix + suffix; + }, + + /** + * Destroy the sidebar. + * @private + */ + _sidebarDestroy: function () { + if (this._variablesView) { + this._variablesView.controller.releaseActors(); + this._variablesView = null; + } + + if (this.sidebar) { + this.sidebar.hide(); + this.sidebar.destroy(); + this.sidebar = null; + } + + this.emit("sidebar-closed"); + }, + + /** + * Destroy the JSTerm object. Call this method to avoid memory leaks. + */ + destroy: function () { + this._sidebarDestroy(); + + this.clearCompletion(); + this.clearOutput(); + + this.autocompletePopup.destroy(); + this.autocompletePopup = null; + + if (this._onPaste) { + this.inputNode.removeEventListener("paste", this._onPaste, false); + this.inputNode.removeEventListener("drop", this._onPaste, false); + this._onPaste = null; + } + + this.inputNode.removeEventListener("keypress", this._keyPress, false); + this.inputNode.removeEventListener("input", this._inputEventHandler, false); + this.inputNode.removeEventListener("keyup", this._inputEventHandler, false); + this.inputNode.removeEventListener("focus", this._focusEventHandler, false); + this.hud.window.removeEventListener("blur", this._blurEventHandler, false); + + this.hud = null; + }, +}; + +function gSequenceId() { + return gSequenceId.n++; +} +gSequenceId.n = 0; +exports.gSequenceId = gSequenceId; + +/** + * @see VariablesView.simpleValueEvalMacro + */ +function simpleValueEvalMacro(item, currentString) { + return VariablesView.simpleValueEvalMacro(item, currentString, "_self"); +} + +/** + * @see VariablesView.overrideValueEvalMacro + */ +function overrideValueEvalMacro(item, currentString) { + return VariablesView.overrideValueEvalMacro(item, currentString, "_self"); +} + +/** + * @see VariablesView.getterOrSetterEvalMacro + */ +function getterOrSetterEvalMacro(item, currentString) { + return VariablesView.getterOrSetterEvalMacro(item, currentString, "_self"); +} + +exports.JSTerm = JSTerm; |