713f9615ccc518a92926d3f7638ea8b1b8fb5e13
[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 WebInspector.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
35 Object.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
45 WebInspector.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, 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, false, 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                 var commandLineAPI = ["$", "$$", "$x", "dir", "dirxml", "keys", "values", "profile", "profileEnd", "monitorEvents", "unmonitorEvents", "inspect", "copy", "clear", "getEventListeners", "$0", "$1", "$2", "$3", "$4", "$_"];
178                 if (WebInspector.debuggerManager.paused && WebInspector.debuggerManager.pauseReason === WebInspector.DebuggerManager.PauseReason.Exception)
179                     commandLineAPI.push("$exception");
180                 for (var i = 0; i < commandLineAPI.length; ++i)
181                     propertyNames[commandLineAPI[i]] = true;
182             }
183
184             propertyNames = Object.keys(propertyNames);
185
186             var implicitSuffix = "";
187             if (bracketNotation) {
188                 var quoteUsed = prefix[0] === "'" ? "'" : "\"";
189                 if (suffix !== "]" && suffix !== quoteUsed)
190                     implicitSuffix = "]";
191             }
192
193             var completions = defaultCompletions;
194             var knownCompletions = completions.keySet();
195
196             for (var i = 0; i < propertyNames.length; ++i) {
197                 var property = propertyNames[i];
198
199                 if (dotNotation && !/^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(property))
200                     continue;
201
202                 if (bracketNotation) {
203                     if (parseInt(property) != property)
204                         property = quoteUsed + property.escapeCharacters(quoteUsed + "\\") + (suffix !== quoteUsed ? quoteUsed : "");
205                 }
206
207                 if (!property.startsWith(prefix) || property in knownCompletions)
208                     continue;
209
210                 completions.push(property);
211                 knownCompletions[property] = true;
212             }
213
214             function compare(a, b)
215             {
216                 // Try to sort in numerical order first.
217                 var numericCompareResult = a - b;
218                 if (!isNaN(numericCompareResult))
219                     return numericCompareResult;
220
221                 // Not numbers, sort as strings.
222                 return a.localeCompare(b);
223             }
224
225             completions.sort(compare);
226
227             completionController.updateCompletions(completions, implicitSuffix);
228         }
229     },
230
231     // Private
232
233     _clearLastProperties: function()
234     {
235         if (this._clearLastPropertiesTimeout) {
236             clearTimeout(this._clearLastPropertiesTimeout);
237             delete this._clearLastPropertiesTimeout;
238         }
239
240         // Clear the cache of property names so any changes while stepping or sitting idle get picked up if the same
241         // expression is evaluated again.
242         this._lastPropertyNames = null;
243     }
244 };
245
246 WebInspector.JavaScriptRuntimeCompletionProvider.prototype.__proto__ = WebInspector.Object.prototype;