Web Inspector: ES6: Show Symbol properties on Objects
[WebKit-https.git] / Source / WebInspectorUI / UserInterface / Views / ObjectTreeView.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.ObjectTreeView = class ObjectTreeView extends WebInspector.Object
27 {
28     constructor(object, mode, propertyPath, forceExpanding)
29     {
30         super();
31
32         console.assert(object instanceof WebInspector.RemoteObject);
33         console.assert(!propertyPath || propertyPath instanceof WebInspector.PropertyPath);
34
35         this._object = object;
36         this._mode = mode || WebInspector.ObjectTreeView.defaultModeForObject(object);
37         this._propertyPath = propertyPath || new WebInspector.PropertyPath(this._object, "this");
38         this._expanded = false;
39         this._hasLosslessPreview = false;
40
41         // If ObjectTree is used outside of the console, we do not know when to release
42         // WeakMap entries. Currently collapse would work. For the console, we can just
43         // listen for console clear events. Currently all ObjectTrees are in the console.
44         this._inConsole = true;
45
46         // Always force expanding for classes.
47         if (this._object.isClass())
48             forceExpanding = true;
49
50         this._element = document.createElement("div");
51         this._element.className = "object-tree";
52
53         if (this._object.preview) {
54             this._previewView = new WebInspector.ObjectPreviewView(this._object.preview);
55             this._previewView.element.addEventListener("click", this._handlePreviewOrTitleElementClick.bind(this));
56             this._element.appendChild(this._previewView.element);
57
58             if (this._previewView.lossless && !this._propertyPath.parent && !forceExpanding) {
59                 this._hasLosslessPreview = true;
60                 this.element.classList.add("lossless-preview");
61             }
62         } else {
63             this._titleElement = document.createElement("span");
64             this._titleElement.className = "title";
65             this._titleElement.appendChild(WebInspector.FormattedValue.createElementForRemoteObject(this._object));
66             this._titleElement.addEventListener("click", this._handlePreviewOrTitleElementClick.bind(this));
67             this._element.appendChild(this._titleElement);
68         }
69
70         this._outlineElement = document.createElement("ol");
71         this._outlineElement.className = "object-tree-outline";
72         this._outline = new WebInspector.TreeOutline(this._outlineElement);
73         this._element.appendChild(this._outlineElement);
74
75         // FIXME: Support editable ObjectTrees.
76     }
77
78     // Static
79
80     static defaultModeForObject(object)
81     {
82         if (object.subtype === "class")
83             return WebInspector.ObjectTreeView.Mode.ClassAPI;
84
85         return WebInspector.ObjectTreeView.Mode.Properties;
86     }
87
88     static createEmptyMessageElement(message)
89     {
90         var emptyMessageElement = document.createElement("div");
91         emptyMessageElement.className = "empty-message";
92         emptyMessageElement.textContent = message;
93         return emptyMessageElement;
94     };
95
96     static comparePropertyDescriptors(propertyA, propertyB)
97     {
98         var a = propertyA.name;
99         var b = propertyB.name;
100
101         // Put __proto__ at the bottom.
102         if (a === "__proto__")
103             return 1;
104         if (b === "__proto__")
105             return -1;
106
107         // Put Internal properties at the top.
108         if (propertyA.isInternalProperty && !propertyB.isInternalProperty)
109             return -1;
110         if (propertyB.isInternalProperty && !propertyA.isInternalProperty)
111             return 1;
112
113         // Put Symbol properties at the bottom.
114         if (propertyA.symbol && !propertyB.symbol)
115             return 1;
116         if (propertyB.symbol && !propertyA.symbol)
117             return -1;
118
119         // Symbol properties may have the same description string but be different objects.
120         if (a === b)
121             return 0;
122
123         var diff = 0;
124         var chunk = /^\d+|^\D+/;
125         var chunka, chunkb, anum, bnum;
126         while (diff === 0) {
127             if (!a && b)
128                 return -1;
129             if (!b && a)
130                 return 1;
131             chunka = a.match(chunk)[0];
132             chunkb = b.match(chunk)[0];
133             anum = !isNaN(chunka);
134             bnum = !isNaN(chunkb);
135             if (anum && !bnum)
136                 return -1;
137             if (bnum && !anum)
138                 return 1;
139             if (anum && bnum) {
140                 diff = chunka - chunkb;
141                 if (diff === 0 && chunka.length !== chunkb.length) {
142                     if (!+chunka && !+chunkb) // chunks are strings of all 0s (special case)
143                         return chunka.length - chunkb.length;
144                     else
145                         return chunkb.length - chunka.length;
146                 }
147             } else if (chunka !== chunkb)
148                 return (chunka < chunkb) ? -1 : 1;
149             a = a.substring(chunka.length);
150             b = b.substring(chunkb.length);
151         }
152         return diff;
153     };
154
155     // Public
156
157     get object()
158     {
159         return this._object;
160     }
161
162     get element()
163     {
164         return this._element;
165     }
166
167     get treeOutline()
168     {
169         return this._outline;
170     }
171
172     get expanded()
173     {
174         return this._expanded;
175     }
176
177     expand()
178     {
179         if (this._expanded)
180             return;
181
182         this._expanded = true;
183         this._element.classList.add("expanded");
184
185         if (this._previewView)
186             this._previewView.showTitle();
187
188         this._trackWeakEntries();
189
190         this.update();
191     }
192
193     collapse()
194     {
195         if (!this._expanded)
196             return;
197
198         this._expanded = false;
199         this._element.classList.remove("expanded");
200
201         if (this._previewView)
202             this._previewView.showPreview();
203
204         this._untrackWeakEntries();
205     }
206
207     showOnlyProperties()
208     {
209         this._inConsole = false;
210
211         this._element.classList.add("properties-only");
212     }
213
214     appendTitleSuffix(suffixElement)
215     {
216         if (this._previewView)
217             this._previewView.element.appendChild(suffixElement);
218         else
219             this._titleElement.appendChild(suffixElement);
220     }
221
222     appendExtraPropertyDescriptor(propertyDescriptor)
223     {
224         if (!this._extraProperties)
225             this._extraProperties = [];
226
227         this._extraProperties.push(propertyDescriptor);
228     }
229
230     // Protected
231
232     update()
233     {
234         if (this._object.isCollectionType() && this._mode === WebInspector.ObjectTreeView.Mode.Properties)
235             this._object.getCollectionEntries(0, 100, this._updateChildren.bind(this, this._updateEntries));
236         else if (this._object.isClass())
237             this._object.classPrototype.getDisplayablePropertyDescriptors(this._updateChildren.bind(this, this._updateProperties));
238         else
239             this._object.getDisplayablePropertyDescriptors(this._updateChildren.bind(this, this._updateProperties));
240     }
241
242     // Private
243
244     _updateChildren(handler, list)
245     {
246         this._outline.removeChildren();
247
248         if (!list) {
249             var errorMessageElement = WebInspector.ObjectTreeView.createEmptyMessageElement(WebInspector.UIString("Could not fetch properties. Object may no longer exist."));
250             this._outline.appendChild(new WebInspector.TreeElement(errorMessageElement, null, false));
251             return;
252         }
253
254         handler.call(this, list, this._propertyPath);
255     }
256
257     _updateEntries(entries, propertyPath)
258     {
259         for (var entry of entries) {
260             if (entry.key) {
261                 this._outline.appendChild(new WebInspector.ObjectTreeMapKeyTreeElement(entry.key, propertyPath));
262                 this._outline.appendChild(new WebInspector.ObjectTreeMapValueTreeElement(entry.value, propertyPath, entry.key));
263             } else
264                 this._outline.appendChild(new WebInspector.ObjectTreeSetIndexTreeElement(entry.value, propertyPath));
265         }
266
267         if (!this._outline.children.length) {
268             var emptyMessageElement = WebInspector.ObjectTreeView.createEmptyMessageElement(WebInspector.UIString("No Entries."));
269             this._outline.appendChild(new WebInspector.TreeElement(emptyMessageElement, null, false));
270         }
271
272         // Show the prototype so users can see the API.
273         this._object.getOwnPropertyDescriptor("__proto__", function(propertyDescriptor) {
274             if (propertyDescriptor)
275                 this._outline.appendChild(new WebInspector.ObjectTreePropertyTreeElement(propertyDescriptor, propertyPath, this._mode));
276         }.bind(this));
277     }
278
279     _updateProperties(properties, propertyPath)
280     {
281         if (this._extraProperties)
282             properties = properties.concat(this._extraProperties);
283
284         properties.sort(WebInspector.ObjectTreeView.comparePropertyDescriptors);
285
286         var isArray = this._object.isArray();
287         var isPropertyMode = this._mode === WebInspector.ObjectTreeView.Mode.Properties;
288
289         for (var propertyDescriptor of properties) {
290             if (isArray && isPropertyMode) {
291                 if (propertyDescriptor.isIndexProperty())
292                     this._outline.appendChild(new WebInspector.ObjectTreeArrayIndexTreeElement(propertyDescriptor, propertyPath));
293                 else if (propertyDescriptor.name === "__proto__")
294                     this._outline.appendChild(new WebInspector.ObjectTreePropertyTreeElement(propertyDescriptor, propertyPath, this._mode));
295             } else
296                 this._outline.appendChild(new WebInspector.ObjectTreePropertyTreeElement(propertyDescriptor, propertyPath, this._mode));
297         }
298
299         if (!this._outline.children.length) {
300             var emptyMessageElement = WebInspector.ObjectTreeView.createEmptyMessageElement(WebInspector.UIString("No Properties."));
301             this._outline.appendChild(new WebInspector.TreeElement(emptyMessageElement, null, false));
302         }
303     }
304
305     _handlePreviewOrTitleElementClick(event)
306     {
307         if (this._hasLosslessPreview)
308             return;
309
310         if (!this._expanded)
311             this.expand();
312         else
313             this.collapse();
314
315         event.stopPropagation();
316     }
317
318     _trackWeakEntries()
319     {
320         if (this._trackingEntries)
321             return;
322
323         if (!this._object.isWeakCollection())
324             return;
325
326         this._trackingEntries = true;
327
328         if (this._inConsole) {
329             WebInspector.logManager.addEventListener(WebInspector.LogManager.Event.Cleared, this._untrackWeakEntries, this);
330             WebInspector.logManager.addEventListener(WebInspector.LogManager.Event.ActiveLogCleared, this._untrackWeakEntries, this);
331             WebInspector.logManager.addEventListener(WebInspector.LogManager.Event.SessionStarted, this._untrackWeakEntries, this);
332         }
333     }
334
335     _untrackWeakEntries()
336     {
337         if (!this._trackingEntries)
338             return;
339
340         if (!this._object.isWeakCollection())
341             return;
342
343         this._trackingEntries = false;
344
345         this._object.releaseWeakCollectionEntries();
346
347         if (this._inConsole) {
348             WebInspector.logManager.removeEventListener(WebInspector.LogManager.Event.Cleared, this._untrackWeakEntries, this);
349             WebInspector.logManager.removeEventListener(WebInspector.LogManager.Event.ActiveLogCleared, this._untrackWeakEntries, this);
350             WebInspector.logManager.removeEventListener(WebInspector.LogManager.Event.SessionStarted, this._untrackWeakEntries, this);
351         }
352
353         // FIXME: This only tries to release weak entries if this object was a WeakMap.
354         // If there was a WeakMap expanded in a sub-object, we will never release those values.
355         // Should we attempt walking the entire tree and release weak collections?
356     }
357 };
358
359 WebInspector.ObjectTreeView.Mode = {
360     Properties: Symbol("object-tree-properties"),      // Properties
361     PrototypeAPI: Symbol("object-tree-prototype-api"), // API view on a live object instance, so getters can be invoked.
362     ClassAPI: Symbol("object-tree-class-api"),         // API view without an object instance, can not invoke getters.
363 };