5ede3dc63fdfce12eaa444e1f2f032fb1f983913
[WebKit-https.git] / Source / WebInspectorUI / UserInterface / Controllers / TypeTokenAnnotator.js
1 /*
2  * Copyright (C) 2014 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.TypeTokenAnnotator = function(sourceCodeTextEditor, script)
27 {
28     WebInspector.Object.call(this);
29
30     console.assert(sourceCodeTextEditor && sourceCodeTextEditor instanceof WebInspector.SourceCodeTextEditor, sourceCodeTextEditor);
31     console.assert(script, script);
32
33     this._script = script;
34     this._sourceCodeTextEditor = sourceCodeTextEditor;
35     this._typeTokenNodes = [];
36     this._typeTokenBookmarks = [];
37     this._timeoutIdentifier = null;
38     this._isActive = false;
39 };
40
41 WebInspector.TypeTokenAnnotator.prototype = {
42     constructor: WebInspector.TypeTokenAnnotator,
43     __proto__: WebInspector.Object.prototype,
44
45     // Public
46
47     get isActive()
48     {
49         return this._isActive;
50     },
51
52     get sourceCodeTextEditor()
53     {
54         return this._sourceCodeTextEditor;
55     },
56
57     pause: function()
58     {
59         this._clearTimeoutIfNeeded();
60         this._isActive = false;
61     },
62
63     resume: function()
64     {
65         this._clearTimeoutIfNeeded();
66         this._isActive = true;
67         this._insertAnnotations();
68     },
69
70     refresh: function()
71     {
72         console.assert(this._isActive);
73         if (!this._isActive)
74             return;
75
76         this._clearTimeoutIfNeeded();
77         this._insertAnnotations();
78     },
79
80     reset: function()
81     {
82         this._clearTimeoutIfNeeded();
83         this._isActive = true;
84         this._clearTypeTokens();
85         this._insertAnnotations();
86     },
87
88     toggleTypeAnnotations: function()
89     {
90         if (this._isActive) {
91             this._isActive = false;
92             this._clearTypeTokens();
93         } else {
94             this._isActive = true;
95             this.reset();
96         }
97
98         return this._isActive;
99     },
100
101     // Private
102
103     _insertAnnotations: function()
104     {
105         if (!this._isActive)
106             return;
107
108         var scriptSyntaxTree = this._script.scriptSyntaxTree;
109
110         if (!scriptSyntaxTree) {
111             this._script.requestScriptSyntaxTree(function(syntaxTree) {
112                 // After requesting the tree, we still might get a null tree from a parse error.
113                 if (syntaxTree)
114                     this._insertAnnotations();
115             }.bind(this));
116
117             return;
118         }
119
120         if (!scriptSyntaxTree.parsedSuccessfully)
121             return;
122
123         var {startOffset, endOffset} = this._sourceCodeTextEditor.visibleRangeOffsets();
124
125         var startTime = Date.now();
126         var allNodesInRange = scriptSyntaxTree.filterByRange(startOffset, endOffset);
127         scriptSyntaxTree.updateTypes(allNodesInRange, function afterTypeUpdates() {
128             // Because this is an asynchronous call, we could have been deactivated before the callback function is called.
129             if (!this._isActive)
130                 return;
131
132             allNodesInRange.forEach(this._insertTypeTokensForEachNode, this);
133             allNodesInRange.forEach(this._updateTypeTokensForEachNode, this);
134
135             var totalTime = Date.now() - startTime;
136             var timeoutTime = Math.min(Math.max(7500, totalTime), 8 * totalTime);
137
138             this._timeoutIdentifier = setTimeout(function timeoutUpdate() {
139                 this._timeoutIdentifier = null;
140                 this._insertAnnotations();
141             }.bind(this), timeoutTime);
142         }.bind(this));
143     },
144
145     _insertTypeTokensForEachNode: function(node)
146     {
147         var scriptSyntaxTree = this._script._scriptSyntaxTree;
148
149         switch (node.type) {
150         case WebInspector.ScriptSyntaxTree.NodeType.FunctionDeclaration:
151         case WebInspector.ScriptSyntaxTree.NodeType.FunctionExpression:
152             for (var param of node.params) {
153                 if (!param.attachments.__typeToken && param.attachments.types && param.attachments.types.displayTypeName)
154                     this._insertToken(param.range[0], param, false, WebInspector.TypeTokenView.TitleType.Variable, param.name);
155             }
156
157             // If a function does not have an explicit return type, then don't show a return type unless we think it's a constructor.
158             var functionReturnType = node.attachments.returnTypes;
159             if (node.attachments.__typeToken || !functionReturnType || !functionReturnType.displayTypeName)
160                 break;
161
162             if (scriptSyntaxTree.containsNonEmptyReturnStatement(node.body) || functionReturnType.displayTypeName !== "Undefined") {
163                 var functionName = node.id ? node.id.name : null;
164                 this._insertToken(node.isGetterOrSetter ? node.getterOrSetterRange[0] : node.range[0], node,
165                     true, WebInspector.TypeTokenView.TitleType.ReturnStatement, functionName);
166             }
167             break;
168         case WebInspector.ScriptSyntaxTree.NodeType.VariableDeclarator:
169             if (!node.attachments.__typeToken && node.attachments.types && node.attachments.types.displayTypeName)
170                 this._insertToken(node.id.range[0], node, false, WebInspector.TypeTokenView.TitleType.Variable, node.id.name);
171
172             break;
173         }
174     },
175
176     _insertToken: function(originalOffset, node, shouldTranslateOffsetToAfterParameterList, typeTokenTitleType, functionOrVariableName)
177     {
178         var tokenPosition = this._sourceCodeTextEditor.originalOffsetToCurrentPosition(originalOffset);
179         var currentOffset = this._sourceCodeTextEditor.currentPositionToCurrentOffset(tokenPosition);
180         var sourceString = this._sourceCodeTextEditor.string;
181
182         if (shouldTranslateOffsetToAfterParameterList) {
183             // Translate the position to the closing parenthesis of the function arguments:
184             // translate from: [type-token] function foo() {} => to: function foo() [type-token] {}
185             currentOffset = this._translateToOffsetAfterFunctionParameterList(node, currentOffset, sourceString);
186             tokenPosition = this._sourceCodeTextEditor.currentOffsetToCurrentPosition(currentOffset);
187         }
188
189         // Note: bookmarks render to the left of the character they're being displayed next to.
190         // This is why right margin checks the current offset. And this is okay to do because JavaScript can't be written right-to-left.
191         var isSpaceRegexp = /\s/;
192         var shouldHaveLeftMargin = currentOffset !== 0 && !isSpaceRegexp.test(sourceString[currentOffset - 1]);
193         var shouldHaveRightMargin = !isSpaceRegexp.test(sourceString[currentOffset]);
194         var typeToken = new WebInspector.TypeTokenView(this, shouldHaveRightMargin, shouldHaveLeftMargin, typeTokenTitleType, functionOrVariableName);
195         var bookmark = this._sourceCodeTextEditor.setInlineWidget(tokenPosition, typeToken.element);
196         node.attachments.__typeToken = typeToken;
197         this._typeTokenNodes.push(node);
198         this._typeTokenBookmarks.push(bookmark);
199     },
200
201     _updateTypeTokensForEachNode: function(node)
202     {
203         switch (node.type) {
204         case WebInspector.ScriptSyntaxTree.NodeType.FunctionDeclaration:
205         case WebInspector.ScriptSyntaxTree.NodeType.FunctionExpression:
206             node.params.forEach(function(param) {
207                 if (param.attachments.__typeToken)
208                     param.attachments.__typeToken.update(param.attachments.types);
209             });
210             if (node.attachments.__typeToken)
211                 node.attachments.__typeToken.update(node.attachments.returnTypes);
212             break;
213         case WebInspector.ScriptSyntaxTree.NodeType.VariableDeclarator:
214             if (node.attachments.__typeToken)
215                 node.attachments.__typeToken.update(node.attachments.types);
216             break;
217         }
218     },
219
220     _translateToOffsetAfterFunctionParameterList: function(node, offset, sourceString)
221     {
222         // The assumption here is that we get the offset starting at the function keyword (or after the get/set keywords).
223         // We will return the offset for the closing parenthesis in the function declaration.
224         // All this code is just a way to find this parenthesis while ignoring comments.
225
226         var isMultiLineComment = false;
227         var isSingleLineComment = false;
228         var shouldIgnore = false;
229
230         function isLineTerminator(char)
231         {
232             // Reference EcmaScript 5 grammar for single line comments and line terminators:
233             // http://www.ecma-international.org/ecma-262/5.1/#sec-7.3
234             // http://www.ecma-international.org/ecma-262/5.1/#sec-7.4
235             return char === "\n" || char === "\r" || char === "\u2028" || char === "\u2029";
236         }
237
238         while ((sourceString[offset] !== ")" || shouldIgnore) && offset < sourceString.length) {
239             if (isSingleLineComment && isLineTerminator(sourceString[offset])) {
240                 isSingleLineComment = false;
241                 shouldIgnore = false;
242             } else if (isMultiLineComment && sourceString[offset] === "*" && sourceString[offset + 1] === "/") {
243                 isMultiLineComment = false;
244                 shouldIgnore = false;
245                 offset++;
246             } else if (!shouldIgnore && sourceString[offset] === "/") {
247                 offset++;
248                 if (sourceString[offset] === "*")
249                     isMultiLineComment = true;
250                 else if (sourceString[offset] === "/")
251                     isSingleLineComment = true;
252                 else
253                     throw new Error("Bad parsing. Couldn't parse comment preamble.");
254                 shouldIgnore = true;
255             }
256
257             offset++;
258         }
259
260         return offset + 1;
261     },
262
263     _clearTypeTokens: function()
264     {
265         this._typeTokenNodes.forEach(function(node) {
266             node.attachments.__typeToken = null;
267         });
268         this._typeTokenBookmarks.forEach(function(bookmark) {
269             bookmark.clear();
270         });
271
272         this._typeTokenNodes = [];
273         this._typeTokenBookmarks = [];
274     },
275
276     _clearTimeoutIfNeeded: function()
277     {
278         if (this._timeoutIdentifier) {
279             clearTimeout(this._timeoutIdentifier);
280             this._timeoutIdentifier = null;
281         }
282     }
283 };