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