Web Inspector: Array/Collection Sizes should be visible and distinct
[WebKit-https.git] / Source / WebInspectorUI / UserInterface / Views / ObjectTreePropertyTreeElement.js
1 /*
2  * Copyright (C) 2015 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.ObjectTreePropertyTreeElement = function(property, propertyPath, mode, prototypeName)
27 {
28     console.assert(property instanceof WebInspector.PropertyDescriptor);
29     console.assert(propertyPath instanceof WebInspector.PropertyPath);
30
31     this._property = property;
32     this._mode = mode || WebInspector.ObjectTreeView.Mode.Properties;
33     this._propertyPath = propertyPath;
34     this._prototypeName = prototypeName;
35
36     var classNames = ["object-tree-property"];
37
38     if (this._property.hasValue()) {
39         classNames.push(this._property.value.type);
40         if (this._property.value.subtype)
41             classNames.push(this._property.value.subtype);
42     } else
43         classNames.push("accessor");
44
45     if (this._property.wasThrown)
46         classNames.push("had-error");
47
48     if (this._property.name === "__proto__")
49         classNames.push("prototype-property");
50
51     WebInspector.GeneralTreeElement.call(this, classNames, this._titleFragment(), null, this._property, false);
52     this._updateTooltips();
53     this._updateHasChildren();
54
55     this.small = true;
56     this.toggleOnClick = true;
57     this.selectable = false;
58     this.tooltipHandledSeparately = true;
59 };
60
61 WebInspector.ObjectTreePropertyTreeElement.prototype = {
62     constructor: WebInspector.ObjectTreePropertyTreeElement,
63     __proto__: WebInspector.GeneralTreeElement.prototype,
64
65     // Public
66
67     get property()
68     {
69         return this._property;
70     },
71
72     // Protected
73
74     onpopulate: function()
75     {
76         this._updateChildren();
77     },
78
79     onexpand: function()
80     {
81         if (this._previewView)
82             this._previewView.showTitle();
83     },
84
85     oncollapse: function()
86     {
87         if (this._previewView)
88             this._previewView.showPreview();
89     },
90
91     oncontextmenu: function(event)
92     {
93         this._contextMenuHandler(event);
94     },
95
96     // Private
97
98     _resolvedValue: function()
99     {
100         if (this._getterValue)
101             return this._getterValue;
102         if (this._property.hasValue())
103             return this._property.value;
104         return null;
105     },
106
107     _propertyPathType: function()
108     {
109         if (this._getterValue || this._property.hasValue())
110             return WebInspector.PropertyPath.Type.Value;
111         if (this._property.hasGetter())
112             return WebInspector.PropertyPath.Type.Getter;
113         if (this._property.hasSetter())
114             return WebInspector.PropertyPath.Type.Setter;
115         return WebInspector.PropertyPath.Type.Value;
116     },
117
118     _resolvedValuePropertyPath: function()
119     {
120         if (this._getterValue)
121             return this._propertyPath.appendPropertyDescriptor(this._getterValue, this._property, WebInspector.PropertyPath.Type.Value);
122         if (this._property.hasValue())
123             return this._propertyPath.appendPropertyDescriptor(this._property.value, this._property, WebInspector.PropertyPath.Type.Value);
124         return null;
125     },
126
127     _thisPropertyPath: function()
128     {
129         return this._propertyPath.appendPropertyDescriptor(null, this._property, this._propertyPathType());
130     },
131
132     _updateHasChildren: function()
133     {
134         var resolvedValue = this._resolvedValue();
135         var valueHasChildren = (resolvedValue && resolvedValue.hasChildren);
136         var wasThrown = this._property.wasThrown || this._getterHadError;
137
138         if (this._mode === WebInspector.ObjectTreeView.Mode.Properties)
139             this.hasChildren = !wasThrown && valueHasChildren;
140         else
141             this.hasChildren = !wasThrown && valueHasChildren && (this._property.name === "__proto__" || this._alwaysDisplayAsProperty());
142     },
143
144     _updateTooltips: function()
145     {
146         var attributes = [];
147
148         if (this._property.configurable)
149             attributes.push("configurable");
150         if (this._property.enumerable)
151             attributes.push("enumerable");
152         if (this._property.writable)
153             attributes.push("writable");
154
155         this.iconElement.title = attributes.join(" ");
156     },
157
158     _updateTitleAndIcon: function()
159     {
160         this.mainTitle = this._titleFragment();
161
162         if (this._getterValue) {
163             this.addClassName(this._getterValue.type);
164             if (this._getterValue.subtype)
165                 this.addClassName(this._getterValue.subtype);
166             if (this._getterHadError)
167                 this.addClassName("had-error");
168             this.removeClassName("accessor");
169         }
170
171         this._updateHasChildren();
172     },
173
174     _titleFragment: function()
175     {
176         if (this._property.name === "__proto__")
177             return this._createTitlePrototype();
178
179         if (this._mode === WebInspector.ObjectTreeView.Mode.Properties)
180             return this._createTitlePropertyStyle();
181         else
182             return this._createTitleAPIStyle();
183     },
184
185     _createTitlePrototype: function()
186     {
187         console.assert(this._property.hasValue());
188         console.assert(this._property.name === "__proto__");
189
190         var nameElement = document.createElement("span");
191         nameElement.className = "prototype-name";
192         nameElement.textContent = WebInspector.UIString("%s Prototype").format(this._sanitizedPrototypeString(this._property.value));
193         nameElement.title = this._propertyPathString(this._thisPropertyPath());
194         return nameElement;
195     },
196
197     _createTitlePropertyStyle: function()
198     {
199         var container = document.createDocumentFragment();
200
201         // Property name.
202         var nameElement = document.createElement("span");
203         nameElement.className = "property-name";
204         nameElement.textContent = this._property.name + ": ";
205         nameElement.title = this._propertyPathString(this._thisPropertyPath());
206
207         // Property attributes.
208         if (this._mode === WebInspector.ObjectTreeView.Mode.Properties) {
209             if (!this._property.enumerable)
210                 nameElement.classList.add("not-enumerable");
211         }
212
213         // Value / Getter Value / Getter.
214         var valueOrGetterElement;
215         var resolvedValue = this._resolvedValue();
216         if (resolvedValue) {
217             if (resolvedValue.preview) {
218                 this._previewView = new WebInspector.ObjectPreviewView(resolvedValue.preview);
219                 valueOrGetterElement = this._previewView.element;
220             } else {
221                 valueOrGetterElement = WebInspector.FormattedValue.createElementForRemoteObject(resolvedValue, this._property.wasThrown || this._getterHadError);
222
223                 // Special case a function property string.
224                 if (resolvedValue.type === "function")
225                     valueOrGetterElement.textContent = this._functionPropertyString();
226             }
227
228             // FIXME: Option+Click for Value.
229         } else {
230             valueOrGetterElement = document.createElement("span");
231             if (this._property.hasGetter())
232                 valueOrGetterElement.appendChild(this._createInteractiveGetterElement());
233             if (!this._property.hasSetter())
234                 valueOrGetterElement.appendChild(this._createReadOnlyIconElement());
235             // FIXME: What if just a setter?
236         }
237
238         valueOrGetterElement.classList.add("value");
239         if (this._property.wasThrown || this._getterHadError)
240             valueOrGetterElement.classList.add("error");
241
242         container.appendChild(nameElement);
243         container.appendChild(valueOrGetterElement);
244         return container;
245     },
246
247     _createTitleAPIStyle: function()
248     {
249         // Fixed values and special properties display like a property.
250         if (this._alwaysDisplayAsProperty())
251             return this._createTitlePropertyStyle();
252
253         // Fetched getter values should already have been shown as properties.
254         console.assert(!this._getterValue);
255
256         // No API to display.
257         var isFunction = this._property.hasValue() && this._property.value.type === "function";
258         if (!isFunction && !this._property.hasGetter() && !this._property.hasSetter())
259             return null;
260
261         var container = document.createDocumentFragment();
262
263         // Function / Getter / Setter.
264         var nameElement = document.createElement("span");
265         nameElement.className = "property-name";
266         nameElement.textContent = this._property.name;
267         nameElement.title = this._propertyPathString(this._thisPropertyPath());
268         container.appendChild(nameElement);
269
270         if (isFunction) {
271             var paramElement = document.createElement("span");
272             paramElement.className = "function-parameters";
273             paramElement.textContent = this._functionParameterString();
274             container.appendChild(paramElement);
275         } else {
276             if (this._property.hasGetter())
277                 container.appendChild(this._createInteractiveGetterElement());
278             if (!this._property.hasSetter())
279                 container.appendChild(this._createReadOnlyIconElement());
280             // FIXME: What if just a setter?
281         }
282
283         return container;
284     },
285
286     _createInteractiveGetterElement: function()
287     {
288         var getterElement = document.createElement("img");
289         getterElement.className = "getter";
290         getterElement.title = WebInspector.UIString("Invoke getter");
291
292         getterElement.addEventListener("click", function(event) {
293             event.stopPropagation();
294             var lastNonPrototypeObject = this._propertyPath.lastNonPrototypeObject;
295             var getterObject = this._property.get;
296             lastNonPrototypeObject.invokeGetter(getterObject, function(error, result, wasThrown) {
297                 this._getterHadError = !!(error || wasThrown);
298                 this._getterValue = result;
299                 this._updateTitleAndIcon();
300             }.bind(this));
301         }.bind(this));
302
303         return getterElement;
304     },
305
306     _createReadOnlyIconElement: function()
307     {
308         var readOnlyElement = document.createElement("img");
309         readOnlyElement.className = "read-only";
310         readOnlyElement.title = WebInspector.UIString("Read only");
311         return readOnlyElement;
312     },
313
314     _alwaysDisplayAsProperty: function()
315     {
316         // Constructor, though a function, is often better treated as an expandable object.
317         if (this._property.name === "constructor")
318             return true;
319
320         // Non-function objects are often better treated as properties.
321         if (this._property.hasValue() && this._property.value.type !== "function")
322             return true;
323
324         // Fetched getter value.
325         if (this._getterValue)
326             return true;
327
328         return false;
329     },
330
331     _functionPropertyString: function()
332     {
333         return "function" + this._functionParameterString();
334     },
335
336     _functionParameterString: function()
337     {
338         var resolvedValue = this._resolvedValue();
339         console.assert(resolvedValue.type === "function");
340
341         // For Native methods, the toString is poor. We try to provide good function parameter strings.
342         if (isFunctionStringNativeCode(resolvedValue.description)) {
343             // Native function on a prototype, likely "Foo.prototype.method".
344             if (this._prototypeName) {
345                 if (WebInspector.NativePrototypeFunctionParameters[this._prototypeName]) {
346                     var params = WebInspector.NativePrototypeFunctionParameters[this._prototypeName][this._property.name];
347                     return params ? "(" + params + ")" : "()";
348                 }
349             }
350
351             // Native function property on a native function is likely a "Foo.method".
352             if (isFunctionStringNativeCode(this._propertyPath.object.description)) {
353                 var match = this._propertyPath.object.description.match(/^function\s+([^)]+?)\(/);
354                 if (match) {
355                     var name = match[1];
356                     if (WebInspector.NativeConstructorFunctionParameters[name]) {
357                         var params = WebInspector.NativeConstructorFunctionParameters[name][this._property.name];
358                         return params ? "(" + params + ")" : "()";
359                     }
360                 }
361             }
362         }        
363
364         var match = resolvedValue.description.match(/^function.*?(\([^)]+?\))/);
365         return match ? match[1] : "()";
366     },
367
368     _sanitizedPrototypeString: function(value)
369     {
370         // FIXME: <https://webkit.org/b/141610> For many X, X.prototype is an X when it must be a plain object
371         if (value.type === "function")
372             return "Function";
373         if (value.subtype === "date")
374             return "Date";
375         if (value.subtype === "regexp")
376             return "RegExp";
377
378         return value.description.replace(/Prototype$/, "");
379     },
380
381     _propertyPathString: function(propertyPath)
382     {
383         if (propertyPath.isFullPathImpossible())
384             return WebInspector.UIString("Unable to determine path to property from root");
385
386         return propertyPath.displayPath(this._propertyPathType());
387     },
388
389     _updateChildren: function()
390     {
391         if (this.children.length && !this.shouldRefreshChildren)
392             return;
393
394         var resolvedValue = this._resolvedValue();
395         if (resolvedValue.isCollectionType() && this._mode === WebInspector.ObjectTreeView.Mode.Properties)
396             resolvedValue.getCollectionEntries(0, 100, this._updateChildrenInternal.bind(this, this._updateEntries, this._mode));
397         else if (this._property.name === "__proto__")
398             resolvedValue.getOwnPropertyDescriptors(this._updateChildrenInternal.bind(this, this._updateProperties, WebInspector.ObjectTreeView.Mode.API));
399         else
400             resolvedValue.getDisplayablePropertyDescriptors(this._updateChildrenInternal.bind(this, this._updateProperties, this._mode));
401     },
402
403     _updateChildrenInternal: function(handler, mode, list)
404     {
405         this.removeChildren();
406
407         if (!list) {
408             var errorMessageElement = WebInspector.ObjectTreeView.emptyMessageElement(WebInspector.UIString("Could not fetch properties. Object may no longer exist."));
409             this.appendChild(new TreeElement(errorMessageElement, null, false));
410             return;
411         }
412
413         handler.call(this, list, this._resolvedValuePropertyPath(), mode);
414     },
415
416     _updateEntries: function(entries, propertyPath, mode)
417     {
418         for (var entry of entries) {
419             if (entry.key) {
420                 this.appendChild(new WebInspector.ObjectTreeMapKeyTreeElement(entry.key, propertyPath));
421                 this.appendChild(new WebInspector.ObjectTreeMapValueTreeElement(entry.value, propertyPath, entry.key));
422             } else
423                 this.appendChild(new WebInspector.ObjectTreeSetIndexTreeElement(entry.value, propertyPath));
424         }
425
426         if (!this.children.length) {
427             var emptyMessageElement = WebInspector.ObjectTreeView.emptyMessageElement(WebInspector.UIString("No Entries."));
428             this.appendChild(new TreeElement(emptyMessageElement, null, false));
429         }
430
431         // Show the prototype so users can see the API.
432         var resolvedValue = this._resolvedValue();
433         resolvedValue.getOwnPropertyDescriptor("__proto__", function(propertyDescriptor) {
434             if (propertyDescriptor)
435                 this.appendChild(new WebInspector.ObjectTreePropertyTreeElement(propertyDescriptor, propertyPath, mode));
436         }.bind(this));
437     },
438
439     _updateProperties: function(properties, propertyPath, mode)
440     {
441         properties.sort(WebInspector.ObjectTreeView.ComparePropertyDescriptors);
442
443         var resolvedValue = this._resolvedValue();
444         var isArray = resolvedValue.isArray();
445         var isPropertyMode = mode === WebInspector.ObjectTreeView.Mode.Properties || this._getterValue;
446         var isAPI = mode === WebInspector.ObjectTreeView.Mode.API;
447
448         var prototypeName = undefined;
449         if (this._property.name === "__proto__") {
450             if (resolvedValue.description)
451                 prototypeName = this._sanitizedPrototypeString(resolvedValue);
452         }
453
454         for (var propertyDescriptor of properties) {
455             // FIXME: If this is a pure API ObjectTree, we should show the native getters.
456             // For now, just skip native binding getters in API mode, since we likely
457             // already showed them in the Properties section.
458             if (isAPI && propertyDescriptor.nativeGetter)
459                 continue;
460             
461             if (isArray && isPropertyMode) {
462                 if (propertyDescriptor.isIndexProperty())
463                     this.appendChild(new WebInspector.ObjectTreeArrayIndexTreeElement(propertyDescriptor, propertyPath));
464                 else if (propertyDescriptor.name === "__proto__")
465                     this.appendChild(new WebInspector.ObjectTreePropertyTreeElement(propertyDescriptor, propertyPath, mode, prototypeName));
466             } else
467                 this.appendChild(new WebInspector.ObjectTreePropertyTreeElement(propertyDescriptor, propertyPath, mode, prototypeName));
468         }
469
470         if (!this.children.length) {
471             var emptyMessageElement = WebInspector.ObjectTreeView.emptyMessageElement(WebInspector.UIString("No Properties."));
472             this.appendChild(new TreeElement(emptyMessageElement, null, false));
473         }
474     },
475
476     _logValue: function(value)
477     {
478         var resolvedValue = value || this._resolvedValue();
479         if (!resolvedValue)
480             return;
481
482         var propertyPath = this._resolvedValuePropertyPath();
483         var isImpossible = propertyPath.isFullPathImpossible();
484         var text = isImpossible ? WebInspector.UIString("Selected Value") : propertyPath.displayPath(this._propertyPathType());
485
486         if (!isImpossible)
487             WebInspector.quickConsole.prompt.pushHistoryItem(text);
488
489         WebInspector.consoleLogViewController.appendImmediateExecutionWithResult(text, resolvedValue);
490     },
491
492     _contextMenuHandler: function(event)
493     {
494         var resolvedValue = this._resolvedValue();
495         if (!resolvedValue)
496             return;
497
498         var contextMenu = new WebInspector.ContextMenu(event);
499         contextMenu.appendItem(WebInspector.UIString("Log Value"), this._logValue.bind(this));
500
501         var propertyPath = this._resolvedValuePropertyPath();
502         if (propertyPath && !propertyPath.isFullPathImpossible()) {
503             contextMenu.appendItem(WebInspector.UIString("Copy Path to Property"), function() {
504                 InspectorFrontendHost.copyText(propertyPath.displayPath(WebInspector.PropertyPath.Type.Value));
505             }.bind(this));
506         }
507
508         contextMenu.appendSeparator();
509
510         this._appendMenusItemsForObject(contextMenu, resolvedValue);
511
512         if (!contextMenu.isEmpty())
513             contextMenu.show();
514     },
515
516     _appendMenusItemsForObject: function(contextMenu, resolvedValue)
517     {
518         if (resolvedValue.type === "function") {
519             // FIXME: We should better handle bound functions.
520             if (!isFunctionStringNativeCode(resolvedValue.description)) {
521                 contextMenu.appendItem(WebInspector.UIString("Jump to Definition"), function() {
522                     DebuggerAgent.getFunctionDetails(resolvedValue.objectId, function(error, response) {
523                         if (error)
524                             return;
525
526                         var location = response.location;
527                         var sourceCode = WebInspector.debuggerManager.scriptForIdentifier(location.scriptId);
528                         if (!sourceCode)
529                             return;
530
531                         var sourceCodeLocation = sourceCode.createSourceCodeLocation(location.lineNumber, location.columnNumber || 0);
532                         WebInspector.resourceSidebarPanel.showSourceCodeLocation(sourceCodeLocation);
533                     });
534                 });
535             }
536             return;
537         }
538
539         if (resolvedValue.subtype === "node") {
540             contextMenu.appendItem(WebInspector.UIString("Reveal in DOM Tree"), function() {
541                 resolvedValue.pushNodeToFrontend(function(nodeId) {
542                     WebInspector.domTreeManager.inspectElement(nodeId);
543                 });
544             });
545             return;
546         }
547     }
548 };