Web Inspector: Provide $event in the console when paused on an event listener
[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.EventListener)
225                         commandLineAPI.push("$event");
226                     else if (targetData.pauseReason === WI.DebuggerManager.PauseReason.Exception)
227                         commandLineAPI.push("$exception");
228                 }
229                 for (let name of commandLineAPI)
230                     propertyNames[name] = true;
231
232                 // FIXME: Due to caching, sometimes old $n values show up as completion results even though they are not available. We should clear that proactively.
233                 for (var i = 1; i <= WI.ConsoleCommandResultMessage.maximumSavedResultIndex; ++i)
234                     propertyNames["$" + i] = true;
235             }
236
237             propertyNames = Object.keys(propertyNames);
238
239             var implicitSuffix = "";
240             if (bracketNotation) {
241                 var quoteUsed = prefix[0] === "'" ? "'" : "\"";
242                 if (suffix !== "]" && suffix !== quoteUsed)
243                     implicitSuffix = "]";
244             }
245
246             var completions = defaultCompletions;
247             var knownCompletions = completions.keySet();
248
249             for (var i = 0; i < propertyNames.length; ++i) {
250                 var property = propertyNames[i];
251
252                 if (dotNotation && !/^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(property))
253                     continue;
254
255                 if (bracketNotation) {
256                     if (parseInt(property) != property)
257                         property = quoteUsed + property.escapeCharacters(quoteUsed + "\\") + (suffix !== quoteUsed ? quoteUsed : "");
258                 }
259
260                 if (!property.startsWith(prefix) || property in knownCompletions)
261                     continue;
262
263                 completions.push(property);
264                 knownCompletions[property] = true;
265             }
266
267             function compare(a, b)
268             {
269                 // Try to sort in numerical order first.
270                 let numericCompareResult = a - b;
271                 if (!isNaN(numericCompareResult))
272                     return numericCompareResult;
273
274                 // Sort __defineGetter__, __lookupGetter__, and friends last.
275                 let aRareProperty = a.startsWith("__") && a.endsWith("__");
276                 let bRareProperty = b.startsWith("__") && b.endsWith("__");
277                 if (aRareProperty && !bRareProperty)
278                     return 1;
279                 if (!aRareProperty && bRareProperty)
280                     return -1;
281
282                 // Not numbers, sort as strings.
283                 return a.extendedLocaleCompare(b);
284             }
285
286             completions.sort(compare);
287
288             completionController.updateCompletions(completions, implicitSuffix);
289         }
290     }
291
292     // Private
293
294     _clearLastProperties()
295     {
296         if (this._clearLastPropertiesTimeout) {
297             clearTimeout(this._clearLastPropertiesTimeout);
298             delete this._clearLastPropertiesTimeout;
299         }
300
301         // Clear the cache of property names so any changes while stepping or sitting idle get picked up if the same
302         // expression is evaluated again.
303         this._lastPropertyNames = null;
304     }
305 };
306
307 WI.JavaScriptRuntimeCompletionProvider._commandLineAPI = [
308     "$",
309     "$$",
310     "$0",
311     "$_",
312     "$x",
313     "clear",
314     "copy",
315     "dir",
316     "dirxml",
317     "getEventListeners",
318     "inspect",
319     "keys",
320     "monitorEvents",
321     "profile",
322     "profileEnd",
323     "queryObjects",
324     "screenshot",
325     "table",
326     "unmonitorEvents",
327     "values",
328 ];