e71d6771e364aa208d99743531c011e02865cd48
[WebKit-https.git] / Source / WebInspectorUI / UserInterface / Controllers / JavaScriptRuntimeCompletionProvider.js
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
26 Object.defineProperty(WI, "javaScriptRuntimeCompletionProvider",
27 {
28     get: function()
29     {
30         if (!WI.JavaScriptRuntimeCompletionProvider._instance)
31             WI.JavaScriptRuntimeCompletionProvider._instance = new WI.JavaScriptRuntimeCompletionProvider;
32         return WI.JavaScriptRuntimeCompletionProvider._instance;
33     }
34 });
35
36 WI.JavaScriptRuntimeCompletionProvider = class JavaScriptRuntimeCompletionProvider extends WI.Object
37 {
38     constructor()
39     {
40         super();
41
42         console.assert(!WI.JavaScriptRuntimeCompletionProvider._instance);
43
44         WI.debuggerManager.addEventListener(WI.DebuggerManager.Event.ActiveCallFrameDidChange, this._clearLastProperties, this);
45     }
46
47     // Protected
48
49     completionControllerCompletionsNeeded(completionController, defaultCompletions, base, prefix, suffix, forced)
50     {
51         // Don't allow non-forced empty prefix completions unless the base is that start of property access.
52         if (!forced && !prefix && !/[.[]$/.test(base)) {
53             completionController.updateCompletions(null);
54             return;
55         }
56
57         // If the base ends with an open parentheses or open curly bracket then treat it like there is
58         // no base so we get global object completions.
59         if (/[({]$/.test(base))
60             base = "";
61
62         var lastBaseIndex = base.length - 1;
63         var dotNotation = base[lastBaseIndex] === ".";
64         var bracketNotation = base[lastBaseIndex] === "[";
65
66         if (dotNotation || bracketNotation) {
67             base = base.substring(0, lastBaseIndex);
68
69             // Don't suggest anything for an empty base that is using dot notation.
70             // Bracket notation with an empty base will be treated as an array.
71             if (!base && dotNotation) {
72                 completionController.updateCompletions(defaultCompletions);
73                 return;
74             }
75
76             // Don't allow non-forced empty prefix completions if the user is entering a number, since it might be a float.
77             // But allow number completions if the base already has a decimal, so "10.0." will suggest Number properties.
78             if (!forced && !prefix && dotNotation && base.indexOf(".") === -1 && parseInt(base, 10) == base) {
79                 completionController.updateCompletions(null);
80                 return;
81             }
82
83             // An empty base with bracket notation is not property access, it is an array.
84             // Clear the bracketNotation flag so completions are not quoted.
85             if (!base && bracketNotation)
86                 bracketNotation = false;
87         }
88
89         // If the base is the same as the last time, we can reuse the property names we have already gathered.
90         // Doing this eliminates delay caused by the async nature of the code below and it only calls getters
91         // and functions once instead of repetitively. Sure, there can be difference each time the base is evaluated,
92         // but this optimization gives us more of a win. We clear the cache after 30 seconds or when stepping in the
93         // debugger to make sure we don't use stale properties in most cases.
94         if (this._lastBase === base && this._lastPropertyNames) {
95             receivedPropertyNames.call(this, this._lastPropertyNames);
96             return;
97         }
98
99         this._lastBase = base;
100         this._lastPropertyNames = null;
101
102         var activeCallFrame = WI.debuggerManager.activeCallFrame;
103         if (!base && activeCallFrame && !this._alwaysEvaluateInWindowContext)
104             activeCallFrame.collectScopeChainVariableNames(receivedPropertyNames.bind(this));
105         else {
106             let options = {objectGroup: "completion", includeCommandLineAPI: true, doNotPauseOnExceptionsAndMuteConsole: true, returnByValue: false, generatePreview: false, saveResult: false};
107             WI.runtimeManager.evaluateInInspectedWindow(base, options, evaluated.bind(this));
108         }
109
110         function updateLastPropertyNames(propertyNames)
111         {
112             if (this._clearLastPropertiesTimeout)
113                 clearTimeout(this._clearLastPropertiesTimeout);
114             this._clearLastPropertiesTimeout = setTimeout(this._clearLastProperties.bind(this), WI.JavaScriptLogViewController.CachedPropertiesDuration);
115
116             this._lastPropertyNames = propertyNames || {};
117         }
118
119         function evaluated(result, wasThrown)
120         {
121             if (wasThrown || !result || result.type === "undefined" || (result.type === "object" && result.subtype === "null")) {
122                 WI.runtimeManager.activeExecutionContext.target.RuntimeAgent.releaseObjectGroup("completion");
123
124                 updateLastPropertyNames.call(this, {});
125                 completionController.updateCompletions(defaultCompletions);
126
127                 return;
128             }
129
130             function inspectedPage_evalResult_getArrayCompletions(primitiveType)
131             {
132                 var array = this;
133                 var arrayLength;
134
135                 var resultSet = {};
136                 for (var o = array; o; o = o.__proto__) {
137                     try {
138                         if (o === array && o.length) {
139                             // If the array type has a length, don't include a list of all the indexes.
140                             // Include it at the end and the frontend can build the list.
141                             arrayLength = o.length;
142                         } else {
143                             var names = Object.getOwnPropertyNames(o);
144                             for (var i = 0; i < names.length; ++i)
145                                 resultSet[names[i]] = true;
146                         }
147                     } catch { }
148                 }
149
150                 if (arrayLength)
151                     resultSet["length"] = arrayLength;
152
153                 return resultSet;
154             }
155
156             function inspectedPage_evalResult_getCompletions(primitiveType)
157             {
158                 var object;
159                 if (primitiveType === "string")
160                     object = new String("");
161                 else if (primitiveType === "number")
162                     object = new Number(0);
163                 else if (primitiveType === "boolean")
164                     object = new Boolean(false);
165                 else if (primitiveType === "symbol")
166                     object = Symbol();
167                 else
168                     object = this;
169
170                 var resultSet = {};
171                 for (var o = object; o; o = o.__proto__) {
172                     try {
173                         var names = Object.getOwnPropertyNames(o);
174                         for (var i = 0; i < names.length; ++i)
175                             resultSet[names[i]] = true;
176                     } catch (e) { }
177                 }
178
179                 return resultSet;
180             }
181
182             if (result.subtype === "array")
183                 result.callFunctionJSON(inspectedPage_evalResult_getArrayCompletions, undefined, receivedArrayPropertyNames.bind(this));
184             else if (result.type === "object" || result.type === "function")
185                 result.callFunctionJSON(inspectedPage_evalResult_getCompletions, undefined, receivedPropertyNames.bind(this));
186             else if (result.type === "string" || result.type === "number" || result.type === "boolean" || result.type === "symbol") {
187                 let options = {objectGroup: "completion", includeCommandLineAPI: false, doNotPauseOnExceptionsAndMuteConsole: true, returnByValue: true, generatePreview: false, saveResult: false};
188                 WI.runtimeManager.evaluateInInspectedWindow("(" + inspectedPage_evalResult_getCompletions + ")(\"" + result.type + "\")", options, receivedPropertyNamesFromEvaluate.bind(this));
189             } else
190                 console.error("Unknown result type: " + result.type);
191         }
192
193         function receivedPropertyNamesFromEvaluate(object, wasThrown, result)
194         {
195             receivedPropertyNames.call(this, result && !wasThrown ? result.value : null);
196         }
197
198         function receivedArrayPropertyNames(propertyNames)
199         {
200             // FIXME: <https://webkit.org/b/143589> Web Inspector: Better handling for large collections in Object Trees
201             // If there was an array like object, we generate autocompletion up to 1000 indexes, but this should
202             // handle a list with arbitrary length.
203             if (propertyNames && typeof propertyNames.length === "number") {
204                 var max = Math.min(propertyNames.length, 1000);
205                 for (var i = 0; i < max; ++i)
206                     propertyNames[i] = true;
207             }
208
209             receivedPropertyNames.call(this, propertyNames);
210         }
211
212         function receivedPropertyNames(propertyNames)
213         {
214             propertyNames = propertyNames || {};
215
216             updateLastPropertyNames.call(this, propertyNames);
217
218             WI.runtimeManager.activeExecutionContext.target.RuntimeAgent.releaseObjectGroup("completion");
219
220             if (!base) {
221                 let commandLineAPI = WI.JavaScriptRuntimeCompletionProvider._commandLineAPI.slice(0);
222                 if (WI.debuggerManager.paused) {
223                     let targetData = WI.debuggerManager.dataForTarget(WI.runtimeManager.activeExecutionContext.target);
224                     if (targetData.pauseReason === WI.DebuggerManager.PauseReason.Exception)
225                         commandLineAPI.push("$exception");
226                 }
227                 for (let name of commandLineAPI)
228                     propertyNames[name] = true;
229
230                 // FIXME: Due to caching, sometimes old $n values show up as completion results even though they are not available. We should clear that proactively.
231                 for (var i = 1; i <= WI.ConsoleCommandResultMessage.maximumSavedResultIndex; ++i)
232                     propertyNames["$" + i] = true;
233             }
234
235             propertyNames = Object.keys(propertyNames);
236
237             var implicitSuffix = "";
238             if (bracketNotation) {
239                 var quoteUsed = prefix[0] === "'" ? "'" : "\"";
240                 if (suffix !== "]" && suffix !== quoteUsed)
241                     implicitSuffix = "]";
242             }
243
244             var completions = defaultCompletions;
245             var knownCompletions = completions.keySet();
246
247             for (var i = 0; i < propertyNames.length; ++i) {
248                 var property = propertyNames[i];
249
250                 if (dotNotation && !/^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(property))
251                     continue;
252
253                 if (bracketNotation) {
254                     if (parseInt(property) != property)
255                         property = quoteUsed + property.escapeCharacters(quoteUsed + "\\") + (suffix !== quoteUsed ? quoteUsed : "");
256                 }
257
258                 if (!property.startsWith(prefix) || property in knownCompletions)
259                     continue;
260
261                 completions.push(property);
262                 knownCompletions[property] = true;
263             }
264
265             function compare(a, b)
266             {
267                 // Try to sort in numerical order first.
268                 let numericCompareResult = a - b;
269                 if (!isNaN(numericCompareResult))
270                     return numericCompareResult;
271
272                 // Sort __defineGetter__, __lookupGetter__, and friends last.
273                 let aRareProperty = a.startsWith("__") && a.endsWith("__");
274                 let bRareProperty = b.startsWith("__") && b.endsWith("__");
275                 if (aRareProperty && !bRareProperty)
276                     return 1;
277                 if (!aRareProperty && bRareProperty)
278                     return -1;
279
280                 // Not numbers, sort as strings.
281                 return a.extendedLocaleCompare(b);
282             }
283
284             completions.sort(compare);
285
286             completionController.updateCompletions(completions, implicitSuffix);
287         }
288     }
289
290     // Private
291
292     _clearLastProperties()
293     {
294         if (this._clearLastPropertiesTimeout) {
295             clearTimeout(this._clearLastPropertiesTimeout);
296             delete this._clearLastPropertiesTimeout;
297         }
298
299         // Clear the cache of property names so any changes while stepping or sitting idle get picked up if the same
300         // expression is evaluated again.
301         this._lastPropertyNames = null;
302     }
303 };
304
305 WI.JavaScriptRuntimeCompletionProvider._commandLineAPI = [
306     "$",
307     "$$",
308     "$0",
309     "$_",
310     "$x",
311     "clear",
312     "copy",
313     "dir",
314     "dirxml",
315     "getEventListeners",
316     "inspect",
317     "keys",
318     "monitorEvents",
319     "profile",
320     "profileEnd",
321     "queryObjects",
322     "screenshot",
323     "table",
324     "unmonitorEvents",
325     "values",
326 ];