1/* 2 * Copyright (C) 2013 Apple Inc. All rights reserved. 3 * 4 * Redistribution and use in source and binary forms, with or without 5 * modification, are permitted provided that the following conditions 6 * are met: 7 * 1. Redistributions of source code must retain the above copyright 8 * notice, this list of conditions and the following disclaimer. 9 * 2. Redistributions in binary form must reproduce the above copyright 10 * notice, this list of conditions and the following disclaimer in the 11 * documentation and/or other materials provided with the distribution. 12 * 13 * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS'' 14 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, 15 * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 16 * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS 17 * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR 18 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF 19 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS 20 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 21 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 22 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF 23 * THE POSSIBILITY OF SUCH DAMAGE. 24 */ 25 26WebInspector.JavaScriptRuntimeCompletionProvider = function() 27{ 28 WebInspector.Object.call(this); 29 30 console.assert(!WebInspector.JavaScriptRuntimeCompletionProvider._instance); 31 32 WebInspector.debuggerManager.addEventListener(WebInspector.DebuggerManager.Event.ActiveCallFrameDidChange, this._clearLastProperties, this); 33}; 34 35Object.defineProperty(WebInspector, "javaScriptRuntimeCompletionProvider", 36{ 37 get: function() 38 { 39 if (!WebInspector.JavaScriptRuntimeCompletionProvider._instance) 40 WebInspector.JavaScriptRuntimeCompletionProvider._instance = new WebInspector.JavaScriptRuntimeCompletionProvider; 41 return WebInspector.JavaScriptRuntimeCompletionProvider._instance; 42 } 43}); 44 45WebInspector.JavaScriptRuntimeCompletionProvider.prototype = { 46 constructor: WebInspector.JavaScriptRuntimeCompletionProvider, 47 48 // Protected 49 50 completionControllerCompletionsNeeded: function(completionController, defaultCompletions, base, prefix, suffix, forced) 51 { 52 // Don't allow non-forced empty prefix completions unless the base is that start of property access. 53 if (!forced && !prefix && !/[.[]$/.test(base)) { 54 completionController.updateCompletions(null); 55 return; 56 } 57 58 // If the base ends with an open parentheses or open curly bracket then treat it like there is 59 // no base so we get global object completions. 60 if (/[({]$/.test(base)) 61 base = ""; 62 63 var lastBaseIndex = base.length - 1; 64 var dotNotation = base[lastBaseIndex] === "."; 65 var bracketNotation = base[lastBaseIndex] === "["; 66 67 if (dotNotation || bracketNotation) { 68 base = base.substring(0, lastBaseIndex); 69 70 // Don't suggest anything for an empty base that is using dot notation. 71 // Bracket notation with an empty base will be treated as an array. 72 if (!base && dotNotation) { 73 completionController.updateCompletions(defaultCompletions); 74 return; 75 } 76 77 // Don't allow non-forced empty prefix completions if the user is entering a number, since it might be a float. 78 // But allow number completions if the base already has a decimal, so "10.0." will suggest Number properties. 79 if (!forced && !prefix && dotNotation && base.indexOf(".") === -1 && parseInt(base, 10) == base) { 80 completionController.updateCompletions(null); 81 return; 82 } 83 84 // An empty base with bracket notation is not property access, it is an array. 85 // Clear the bracketNotation flag so completions are not quoted. 86 if (!base && bracketNotation) 87 bracketNotation = false; 88 } 89 90 // If the base is the same as the last time, we can reuse the property names we have already gathered. 91 // Doing this eliminates delay caused by the async nature of the code below and it only calls getters 92 // and functions once instead of repetitively. Sure, there can be difference each time the base is evaluated, 93 // but this optimization gives us more of a win. We clear the cache after 30 seconds or when stepping in the 94 // debugger to make sure we don't use stale properties in most cases. 95 if (this._lastBase === base && this._lastPropertyNames) { 96 receivedPropertyNames.call(this, this._lastPropertyNames); 97 return; 98 } 99 100 this._lastBase = base; 101 this._lastPropertyNames = null; 102 103 var activeCallFrame = WebInspector.debuggerManager.activeCallFrame; 104 if (!base && activeCallFrame && !this._alwaysEvaluateInWindowContext) 105 activeCallFrame.collectScopeChainVariableNames(receivedPropertyNames.bind(this)); 106 else 107 WebInspector.runtimeManager.evaluateInInspectedWindow(base, "completion", true, true, false, evaluated.bind(this)); 108 109 function updateLastPropertyNames(propertyNames) 110 { 111 if (this._clearLastPropertiesTimeout) 112 clearTimeout(this._clearLastPropertiesTimeout); 113 this._clearLastPropertiesTimeout = setTimeout(this._clearLastProperties.bind(this), WebInspector.JavaScriptLogViewController.CachedPropertiesDuration); 114 115 this._lastPropertyNames = propertyNames || {}; 116 } 117 118 function evaluated(result, wasThrown) 119 { 120 if (wasThrown || !result || result.type === "undefined" || (result.type === "object" && result.subtype === "null")) { 121 RuntimeAgent.releaseObjectGroup("completion"); 122 123 updateLastPropertyNames.call(this, {}); 124 completionController.updateCompletions(defaultCompletions); 125 126 return; 127 } 128 129 function getCompletions(primitiveType) 130 { 131 var object; 132 if (primitiveType === "string") 133 object = new String(""); 134 else if (primitiveType === "number") 135 object = new Number(0); 136 else if (primitiveType === "boolean") 137 object = new Boolean(false); 138 else 139 object = this; 140 141 var resultSet = {}; 142 for (var o = object; o; o = o.__proto__) { 143 try { 144 var names = Object.getOwnPropertyNames(o); 145 for (var i = 0; i < names.length; ++i) 146 resultSet[names[i]] = true; 147 } catch (e) { 148 // Ignore 149 } 150 } 151 152 return resultSet; 153 } 154 155 if (result.type === "object" || result.type === "function") 156 result.callFunctionJSON(getCompletions, undefined, receivedPropertyNames.bind(this)); 157 else if (result.type === "string" || result.type === "number" || result.type === "boolean") 158 WebInspector.runtimeManager.evaluateInInspectedWindow("(" + getCompletions + ")(\"" + result.type + "\")", "completion", false, true, true, receivedPropertyNamesFromEvaluate.bind(this)); 159 else 160 console.error("Unknown result type: " + result.type); 161 } 162 163 function receivedPropertyNamesFromEvaluate(object, wasThrown, result) 164 { 165 receivedPropertyNames.call(this, result && !wasThrown ? result.value : null); 166 } 167 168 function receivedPropertyNames(propertyNames) 169 { 170 propertyNames = propertyNames || {}; 171 172 updateLastPropertyNames.call(this, propertyNames); 173 174 RuntimeAgent.releaseObjectGroup("completion"); 175 176 if (!base) { 177 const commandLineAPI = ["$", "$$", "$x", "dir", "dirxml", "keys", "values", "profile", "profileEnd", "monitorEvents", "unmonitorEvents", "inspect", "copy", "clear", "getEventListeners", "$0", "$1", "$2", "$3", "$4", "$_"]; 178 for (var i = 0; i < commandLineAPI.length; ++i) 179 propertyNames[commandLineAPI[i]] = true; 180 } 181 182 propertyNames = Object.keys(propertyNames); 183 184 var implicitSuffix = ""; 185 if (bracketNotation) { 186 var quoteUsed = prefix[0] === "'" ? "'" : "\""; 187 if (suffix !== "]" && suffix !== quoteUsed) 188 implicitSuffix = "]"; 189 } 190 191 var completions = defaultCompletions; 192 var knownCompletions = completions.keySet(); 193 194 for (var i = 0; i < propertyNames.length; ++i) { 195 var property = propertyNames[i]; 196 197 if (dotNotation && !/^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(property)) 198 continue; 199 200 if (bracketNotation) { 201 if (parseInt(property) != property) 202 property = quoteUsed + property.escapeCharacters(quoteUsed + "\\") + (suffix !== quoteUsed ? quoteUsed : ""); 203 } 204 205 if (!property.startsWith(prefix) || property in knownCompletions) 206 continue; 207 208 completions.push(property); 209 knownCompletions[property] = true; 210 } 211 212 function compare(a, b) 213 { 214 // Try to sort in numerical order first. 215 var numericCompareResult = a - b; 216 if (!isNaN(numericCompareResult)) 217 return numericCompareResult; 218 219 // Not numbers, sort as strings. 220 return a.localeCompare(b); 221 } 222 223 completions.sort(compare); 224 225 completionController.updateCompletions(completions, implicitSuffix); 226 } 227 }, 228 229 // Private 230 231 _clearLastProperties: function() 232 { 233 if (this._clearLastPropertiesTimeout) { 234 clearTimeout(this._clearLastPropertiesTimeout); 235 delete this._clearLastPropertiesTimeout; 236 } 237 238 // Clear the cache of property names so any changes while stepping or sitting idle get picked up if the same 239 // expression is evaluated again. 240 this._lastPropertyNames = null; 241 } 242}; 243 244WebInspector.JavaScriptRuntimeCompletionProvider.prototype.__proto__ = WebInspector.Object.prototype; 245