/* * Copyright (C) 2013 Apple Inc. All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * 1. Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * 2. Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS'' * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF * THE POSSIBILITY OF SUCH DAMAGE. */ Object.defineProperty(WI, "javaScriptRuntimeCompletionProvider", { get: function() { if (!WI.JavaScriptRuntimeCompletionProvider._instance) WI.JavaScriptRuntimeCompletionProvider._instance = new WI.JavaScriptRuntimeCompletionProvider; return WI.JavaScriptRuntimeCompletionProvider._instance; } }); WI.JavaScriptRuntimeCompletionProvider = class JavaScriptRuntimeCompletionProvider extends WI.Object { constructor() { super(); console.assert(!WI.JavaScriptRuntimeCompletionProvider._instance); this._ongoingCompletionRequests = 0; WI.debuggerManager.addEventListener(WI.DebuggerManager.Event.ActiveCallFrameDidChange, this._clearLastProperties, this); } // Static static get _commandLineAPIKeys() { if (!JavaScriptRuntimeCompletionProvider.__cachedCommandLineAPIKeys) { JavaScriptRuntimeCompletionProvider.__cachedCommandLineAPIKeys = [ "$_", "assert", "clear", "count", "countReset", "debug", "dir", "dirxml", "error", "group", "groupCollapsed", "groupEnd", "info", "inspect", "keys", "log", "profile", "profileEnd", "queryHolders", "queryInstances", "queryObjects", "record", "recordEnd", "screenshot", "table", "takeHeapSnapshot", "time", "timeEnd", "timeLog", "timeStamp", "trace", "values", "warn", ]; } return JavaScriptRuntimeCompletionProvider.__cachedCommandLineAPIKeys; } // Protected completionControllerCompletionsNeeded(completionController, defaultCompletions, base, prefix, suffix, forced) { // Don't allow non-forced empty prefix completions unless the base is that start of property access. if (!forced && !prefix && !/[.[]$/.test(base)) { completionController.updateCompletions(null); return; } // If the base ends with an open parentheses or open curly bracket then treat it like there is // no base so we get global object completions. if (/[({]$/.test(base)) base = ""; var lastBaseIndex = base.length - 1; var dotNotation = base[lastBaseIndex] === "."; var bracketNotation = base[lastBaseIndex] === "["; if (dotNotation || bracketNotation) { base = base.substring(0, lastBaseIndex); // Don't suggest anything for an empty base that is using dot notation. // Bracket notation with an empty base will be treated as an array. if (!base && dotNotation) { completionController.updateCompletions(defaultCompletions); return; } // Don't allow non-forced empty prefix completions if the user is entering a number, since it might be a float. // But allow number completions if the base already has a decimal, so "10.0." will suggest Number properties. if (!forced && !prefix && dotNotation && base.indexOf(".") === -1 && parseInt(base, 10) == base) { completionController.updateCompletions(null); return; } // An empty base with bracket notation is not property access, it is an array. // Clear the bracketNotation flag so completions are not quoted. if (!base && bracketNotation) bracketNotation = false; } // Start an completion request. We must now decrement before calling completionController.updateCompletions. this._incrementOngoingCompletionRequests(); // If the base is the same as the last time, we can reuse the property names we have already gathered. // Doing this eliminates delay caused by the async nature of the code below and it only calls getters // and functions once instead of repetitively. Sure, there can be difference each time the base is evaluated, // but this optimization gives us more of a win. We clear the cache after 30 seconds or when stepping in the // debugger to make sure we don't use stale properties in most cases. if (this._lastMode === completionController.mode && this._lastBase === base && this._lastPropertyNames) { receivedPropertyNames.call(this, this._lastPropertyNames); return; } this._lastMode = completionController.mode; this._lastBase = base; this._lastPropertyNames = null; var activeCallFrame = WI.debuggerManager.activeCallFrame; if (!base && activeCallFrame && !this._alwaysEvaluateInWindowContext) activeCallFrame.collectScopeChainVariableNames(receivedPropertyNames.bind(this)); else { let options = {objectGroup: "completion", includeCommandLineAPI: true, doNotPauseOnExceptionsAndMuteConsole: true, returnByValue: false, generatePreview: false, saveResult: false}; WI.runtimeManager.evaluateInInspectedWindow(base, options, evaluated.bind(this)); } function updateLastPropertyNames(propertyNames) { if (this._clearLastPropertiesTimeout) clearTimeout(this._clearLastPropertiesTimeout); this._clearLastPropertiesTimeout = setTimeout(this._clearLastProperties.bind(this), WI.JavaScriptLogViewController.CachedPropertiesDuration); this._lastPropertyNames = propertyNames || []; } function evaluated(result, wasThrown) { if (wasThrown || !result || result.type === "undefined" || (result.type === "object" && result.subtype === "null")) { this._decrementOngoingCompletionRequests(); updateLastPropertyNames.call(this, []); completionController.updateCompletions(defaultCompletions); return; } function inspectedPage_evalResult_getArrayCompletions(primitiveType) { var array = this; var arrayLength; var resultSet = {}; for (var o = array; o; o = o.__proto__) { try { if (o === array && o.length) { // If the array type has a length, don't include a list of all the indexes. // Include it at the end and the frontend can build the list. arrayLength = o.length; } else { var names = Object.getOwnPropertyNames(o); for (var i = 0; i < names.length; ++i) resultSet[names[i]] = true; } } catch { } } if (arrayLength) resultSet["length"] = arrayLength; return resultSet; } function inspectedPage_evalResult_getCompletions(primitiveType) { var object; if (primitiveType === "string") object = new String(""); else if (primitiveType === "number") object = new Number(0); else if (primitiveType === "boolean") object = new Boolean(false); else if (primitiveType === "symbol") object = Symbol(); else object = this; var resultSet = {}; for (var o = object; o; o = o.__proto__) { try { var names = Object.getOwnPropertyNames(o); for (var i = 0; i < names.length; ++i) resultSet[names[i]] = true; } catch (e) { } } return resultSet; } if (result.subtype === "array") result.callFunctionJSON(inspectedPage_evalResult_getArrayCompletions, undefined, receivedArrayPropertyNames.bind(this)); else if (result.type === "object" || result.type === "function") result.callFunctionJSON(inspectedPage_evalResult_getCompletions, undefined, receivedObjectPropertyNames.bind(this)); else if (result.type === "string" || result.type === "number" || result.type === "boolean" || result.type === "symbol") { let options = {objectGroup: "completion", includeCommandLineAPI: false, doNotPauseOnExceptionsAndMuteConsole: true, returnByValue: true, generatePreview: false, saveResult: false}; WI.runtimeManager.evaluateInInspectedWindow("(" + inspectedPage_evalResult_getCompletions + ")(\"" + result.type + "\")", options, receivedPropertyNamesFromEvaluate.bind(this)); } else console.error("Unknown result type: " + result.type); } function receivedPropertyNamesFromEvaluate(object, wasThrown, result) { receivedPropertyNames.call(this, result && !wasThrown ? Object.keys(result.value) : null); } function receivedObjectPropertyNames(propertyNames) { receivedPropertyNames.call(this, Object.keys(propertyNames)); } function receivedArrayPropertyNames(propertyNames) { if (propertyNames && typeof propertyNames.length === "number") { // FIXME Web Inspector: autocompletion of array indexes can't handle large arrays in a performant way var max = Math.min(propertyNames.length, 1000); for (var i = 0; i < max; ++i) propertyNames[i] = true; } receivedObjectPropertyNames.call(this, propertyNames); } function receivedPropertyNames(propertyNames) { console.assert(!propertyNames || Array.isArray(propertyNames)); propertyNames = propertyNames || []; updateLastPropertyNames.call(this, propertyNames); this._decrementOngoingCompletionRequests(); if (!base) { propertyNames.pushAll(JavaScriptRuntimeCompletionProvider._commandLineAPIKeys); let savedResultAlias = WI.settings.consoleSavedResultAlias.value; if (savedResultAlias) propertyNames.push(savedResultAlias + "_"); let target = WI.runtimeManager.activeExecutionContext.target; let targetData = WI.debuggerManager.paused ? WI.debuggerManager.dataForTarget(target) : {}; function shouldExposeEvent() { switch (completionController.mode) { case WI.CodeMirrorCompletionController.Mode.FullConsoleCommandLineAPI: case WI.CodeMirrorCompletionController.Mode.EventBreakpoint: return true; case WI.CodeMirrorCompletionController.Mode.PausedConsoleCommandLineAPI: return targetData.pauseReason === WI.DebuggerManager.PauseReason.Listener || targetData.pauseReason === WI.DebuggerManager.PauseReason.EventListener; } return false; } if (shouldExposeEvent()) { propertyNames.push("$event"); if (savedResultAlias) propertyNames.push(savedResultAlias + "event"); } function shouldExposeException() { switch (completionController.mode) { case WI.CodeMirrorCompletionController.Mode.FullConsoleCommandLineAPI: case WI.CodeMirrorCompletionController.Mode.ExceptionBreakpoint: return true; case WI.CodeMirrorCompletionController.Mode.PausedConsoleCommandLineAPI: return targetData.pauseReason === WI.DebuggerManager.PauseReason.Exception; } return false; } if (shouldExposeException()) { propertyNames.push("$exception"); if (savedResultAlias) propertyNames.push(savedResultAlias + "exception"); } switch (target.type) { case WI.TargetType.Page: propertyNames.push("$"); propertyNames.push("$$"); propertyNames.push("$0"); if (savedResultAlias) propertyNames.push(savedResultAlias + "0"); propertyNames.push("$x"); // fallthrough case WI.TargetType.ServiceWorker: case WI.TargetType.Worker: propertyNames.push("copy"); propertyNames.push("getEventListeners"); propertyNames.push("monitorEvents"); propertyNames.push("unmonitorEvents"); break; } // FIXME: Due to caching, sometimes old $n values show up as completion results even though they are not available. We should clear that proactively. for (var i = 1; i <= WI.ConsoleCommandResultMessage.maximumSavedResultIndex; ++i) { propertyNames.push("$" + i); if (savedResultAlias) propertyNames.push(savedResultAlias + i); } } var implicitSuffix = ""; if (bracketNotation) { var quoteUsed = prefix[0] === "'" ? "'" : "\""; if (suffix !== "]" && suffix !== quoteUsed) implicitSuffix = "]"; } var completions = defaultCompletions; let knownCompletions = new Set(completions); for (var i = 0; i < propertyNames.length; ++i) { var property = propertyNames[i]; if (dotNotation && !/^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(property)) continue; if (bracketNotation) { if (parseInt(property) != property) property = quoteUsed + property.escapeCharacters(quoteUsed + "\\") + (suffix !== quoteUsed ? quoteUsed : ""); } if (!property.startsWith(prefix) || knownCompletions.has(property)) continue; completions.push(property); knownCompletions.add(property); } function compare(a, b) { // Try to sort in numerical order first. let numericCompareResult = a - b; if (!isNaN(numericCompareResult)) return numericCompareResult; // Sort __defineGetter__, __lookupGetter__, and friends last. let aRareProperty = a.startsWith("__") && a.endsWith("__"); let bRareProperty = b.startsWith("__") && b.endsWith("__"); if (aRareProperty && !bRareProperty) return 1; if (!aRareProperty && bRareProperty) return -1; // Not numbers, sort as strings. return a.extendedLocaleCompare(b); } completions.sort(compare); completionController.updateCompletions(completions, implicitSuffix); } } // Private _incrementOngoingCompletionRequests() { this._ongoingCompletionRequests++; console.assert(this._ongoingCompletionRequests <= 50, "Ongoing requests probably should not get this high. We may be missing a balancing decrement."); } _decrementOngoingCompletionRequests() { this._ongoingCompletionRequests--; console.assert(this._ongoingCompletionRequests >= 0, "Unbalanced increments / decrements."); if (this._ongoingCompletionRequests <= 0) WI.runtimeManager.activeExecutionContext.target.RuntimeAgent.releaseObjectGroup("completion"); } _clearLastProperties() { if (this._clearLastPropertiesTimeout) { clearTimeout(this._clearLastPropertiesTimeout); delete this._clearLastPropertiesTimeout; } // Clear the cache of property names so any changes while stepping or sitting idle get picked up if the same // expression is evaluated again. this._lastPropertyNames = null; } };