Web Inspector: Elements: Styles: add icons for various CSS rule types
[WebKit-https.git] / Source / WebInspectorUI / UserInterface / Views / ScopeChainDetailsSidebarPanel.js
1 /*
2  * Copyright (C) 2013, 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 WI.ScopeChainDetailsSidebarPanel = class ScopeChainDetailsSidebarPanel extends WI.DetailsSidebarPanel
27 {
28     constructor()
29     {
30         super("scope-chain", WI.UIString("Scope Chain"));
31
32         this._callFrame = null;
33
34         this._watchExpressionsSetting = new WI.Setting("watch-expressions", []);
35         this._watchExpressionsSetting.addEventListener(WI.Setting.Event.Changed, this._updateWatchExpressionsNavigationBar, this);
36
37         this._watchExpressionOptionsElement = document.createElement("div");
38         this._watchExpressionOptionsElement.classList.add("options");
39
40         this._navigationBar = new WI.NavigationBar;
41         this._watchExpressionOptionsElement.appendChild(this._navigationBar.element);
42
43         let addWatchExpressionButton = new WI.ButtonNavigationItem("add-watch-expression", WI.UIString("Add watch expression"), "Images/Plus13.svg", 13, 13);
44         addWatchExpressionButton.addEventListener(WI.ButtonNavigationItem.Event.Clicked, this._addWatchExpressionButtonClicked, this);
45         this._navigationBar.addNavigationItem(addWatchExpressionButton);
46
47         this._clearAllWatchExpressionButton = new WI.ButtonNavigationItem("clear-watch-expressions", WI.UIString("Clear watch expressions"), "Images/NavigationItemTrash.svg", 15, 15);
48         this._clearAllWatchExpressionButton.addEventListener(WI.ButtonNavigationItem.Event.Clicked, this._clearAllWatchExpressionsButtonClicked, this);
49         this._navigationBar.addNavigationItem(this._clearAllWatchExpressionButton);
50
51         this._refreshAllWatchExpressionButton = new WI.ButtonNavigationItem("refresh-watch-expressions", WI.UIString("Refresh watch expressions"), "Images/ReloadFull.svg", 13, 13);
52         this._refreshAllWatchExpressionButton.addEventListener(WI.ButtonNavigationItem.Event.Clicked, this._refreshAllWatchExpressionsButtonClicked, this);
53         this._navigationBar.addNavigationItem(this._refreshAllWatchExpressionButton);
54
55         this._watchExpressionsSectionGroup = new WI.DetailsSectionGroup;
56         this._watchExpressionsSection = new WI.DetailsSection("watch-expressions", WI.UIString("Watch Expressions"), [this._watchExpressionsSectionGroup], this._watchExpressionOptionsElement);
57         this.contentView.element.appendChild(this._watchExpressionsSection.element);
58
59         this._updateWatchExpressionsNavigationBar();
60
61         this.needsLayout();
62
63         // Update on console prompt eval as objects in the scope chain may have changed.
64         WI.runtimeManager.addEventListener(WI.RuntimeManager.Event.DidEvaluate, this._didEvaluateExpression, this);
65
66         // Update watch expressions when console execution context changes.
67         WI.runtimeManager.addEventListener(WI.RuntimeManager.Event.ActiveExecutionContextChanged, this._activeExecutionContextChanged, this);
68
69         // Update watch expressions on navigations.
70         WI.Frame.addEventListener(WI.Frame.Event.MainResourceDidChange, this._mainResourceDidChange, this);
71
72         // Update watch expressions on active call frame changes.
73         WI.debuggerManager.addEventListener(WI.DebuggerManager.Event.ActiveCallFrameDidChange, this._activeCallFrameDidChange, this);
74     }
75
76     // Public
77
78     inspect(objects)
79     {
80         // Convert to a single item array if needed.
81         if (!(objects instanceof Array))
82             objects = [objects];
83
84         var callFrameToInspect = null;
85
86         // Iterate over the objects to find a WI.CallFrame to inspect.
87         for (var i = 0; i < objects.length; ++i) {
88             if (!(objects[i] instanceof WI.CallFrame))
89                 continue;
90             callFrameToInspect = objects[i];
91             break;
92         }
93
94         this.callFrame = callFrameToInspect;
95
96         return true;
97     }
98
99     get callFrame()
100     {
101         return this._callFrame;
102     }
103
104     set callFrame(callFrame)
105     {
106         if (callFrame === this._callFrame)
107             return;
108
109         this._callFrame = callFrame;
110
111         this.needsLayout();
112     }
113
114     closed()
115     {
116         WI.runtimeManager.removeEventListener(null, null, this);
117         WI.Frame.removeEventListener(null, null, this);
118         WI.debuggerManager.removeEventListener(null, null, this);
119
120         super.closed();
121     }
122
123     // Protected
124
125     layout()
126     {
127         let callFrame = this._callFrame;
128
129         Promise.all([this._generateWatchExpressionsSection(), this._generateCallFramesSection()]).then(function(sections) {
130             let [watchExpressionsSection, callFrameSections] = sections;
131
132             function delayedWork()
133             {
134                 // Clear the timeout so we don't update the interface twice.
135                 clearTimeout(timeout);
136
137                 if (watchExpressionsSection)
138                     this._watchExpressionsSectionGroup.rows = [watchExpressionsSection];
139                 else {
140                     let emptyRow = new WI.DetailsSectionRow(WI.UIString("No Watch Expressions"));
141                     this._watchExpressionsSectionGroup.rows = [emptyRow];
142                     emptyRow.showEmptyMessage();
143                 }
144
145                 this.contentView.element.removeChildren();
146                 this.contentView.element.appendChild(this._watchExpressionsSection.element);
147
148                 // Bail if the call frame changed while we were waiting for the async response.
149                 if (this._callFrame !== callFrame)
150                     return;
151
152                 if (!callFrameSections)
153                     return;
154
155                 for (let callFrameSection of callFrameSections)
156                     this.contentView.element.appendChild(callFrameSection.element);
157             }
158
159             // We need a timeout in place in case there are long running, pending backend dispatches. This can happen
160             // if the debugger is paused in code that was executed from the console. The console will be waiting for
161             // the result of the execution and without a timeout we would never update the scope variables.
162             let delay = WI.ScopeChainDetailsSidebarPanel._autoExpandProperties.size === 0 ? 50 : 250;
163             let timeout = setTimeout(delayedWork.bind(this), delay);
164
165             // Since ObjectTreeView populates asynchronously, we want to wait to replace the existing content
166             // until after all the pending asynchronous requests are completed. This prevents severe flashing while stepping.
167             InspectorBackend.runAfterPendingDispatches(delayedWork.bind(this));
168         }.bind(this)).catch(function(e) { console.error(e); });
169     }
170
171     _generateCallFramesSection()
172     {
173         let callFrame = this._callFrame;
174         if (!callFrame)
175             return Promise.resolve(null);
176
177         let detailsSections = [];
178         let foundLocalScope = false;
179
180         let sectionCountByType = new Map;
181         for (let type in WI.ScopeChainNode.Type)
182             sectionCountByType.set(WI.ScopeChainNode.Type[type], 0);
183
184         let scopeChain = callFrame.mergedScopeChain();
185         for (let scope of scopeChain) {
186             // Don't show sections for empty scopes unless it is the local scope, since it has "this".
187             if (scope.empty && scope.type !== WI.ScopeChainNode.Type.Local)
188                 continue;
189
190             let title = null;
191             let extraPropertyDescriptor = null;
192             let collapsedByDefault = false;
193
194             let count = sectionCountByType.get(scope.type);
195             sectionCountByType.set(scope.type, ++count);
196
197             switch (scope.type) {
198             case WI.ScopeChainNode.Type.Local:
199                 foundLocalScope = true;
200                 collapsedByDefault = false;
201                 title = WI.UIString("Local Variables");
202                 if (callFrame.thisObject)
203                     extraPropertyDescriptor = new WI.PropertyDescriptor({name: "this", value: callFrame.thisObject});
204                 break;
205
206             case WI.ScopeChainNode.Type.Closure:
207                 if (scope.__baseClosureScope && scope.name)
208                     title = WI.UIString("Closure Variables (%s)").format(scope.name);
209                 else
210                     title = WI.UIString("Closure Variables");
211                 collapsedByDefault = false;
212                 break;
213
214             case WI.ScopeChainNode.Type.Block:
215                 title = WI.UIString("Block Variables");
216                 collapsedByDefault = false;
217                 break;
218
219             case WI.ScopeChainNode.Type.Catch:
220                 title = WI.UIString("Catch Variables");
221                 collapsedByDefault = false;
222                 break;
223
224             case WI.ScopeChainNode.Type.FunctionName:
225                 title = WI.UIString("Function Name Variable");
226                 collapsedByDefault = true;
227                 break;
228
229             case WI.ScopeChainNode.Type.With:
230                 title = WI.UIString("With Object Properties");
231                 collapsedByDefault = foundLocalScope;
232                 break;
233
234             case WI.ScopeChainNode.Type.Global:
235                 title = WI.UIString("Global Variables");
236                 collapsedByDefault = true;
237                 break;
238
239             case WI.ScopeChainNode.Type.GlobalLexicalEnvironment:
240                 title = WI.UIString("Global Lexical Environment");
241                 collapsedByDefault = true;
242                 break;
243             }
244
245             let detailsSectionIdentifier = scope.type + "-" + sectionCountByType.get(scope.type);
246             let detailsSection = new WI.DetailsSection(detailsSectionIdentifier, title, null, null, collapsedByDefault);
247
248             // FIXME: This just puts two ObjectTreeViews next to each other, but that means
249             // that properties are not nicely sorted between the two separate lists.
250
251             let rows = [];
252             for (let object of scope.objects) {
253                 let scopePropertyPath = WI.PropertyPath.emptyPropertyPathForScope(object);
254                 let objectTree = new WI.ObjectTreeView(object, WI.ObjectTreeView.Mode.Properties, scopePropertyPath);
255
256                 objectTree.showOnlyProperties();
257
258                 if (extraPropertyDescriptor) {
259                     objectTree.appendExtraPropertyDescriptor(extraPropertyDescriptor);
260                     extraPropertyDescriptor = null;
261                 }
262
263                 let treeOutline = objectTree.treeOutline;
264                 treeOutline.registerScrollVirtualizer(this.contentView.element, 16);
265                 treeOutline.addEventListener(WI.TreeOutline.Event.ElementAdded, this._treeElementAdded.bind(this, detailsSectionIdentifier), this);
266                 treeOutline.addEventListener(WI.TreeOutline.Event.ElementDisclosureDidChanged, this._treeElementDisclosureDidChange.bind(this, detailsSectionIdentifier), this);
267
268                 rows.push(new WI.ObjectPropertiesDetailSectionRow(objectTree, detailsSection));
269             }
270
271             detailsSection.groups[0].rows = rows;
272             detailsSections.push(detailsSection);
273         }
274
275         return Promise.resolve(detailsSections);
276     }
277
278     _generateWatchExpressionsSection()
279     {
280         let watchExpressions = this._watchExpressionsSetting.value;
281         if (!watchExpressions.length) {
282             if (this._usedWatchExpressionsObjectGroup) {
283                 this._usedWatchExpressionsObjectGroup = false;
284                 for (let target of WI.targets)
285                     target.RuntimeAgent.releaseObjectGroup(WI.ScopeChainDetailsSidebarPanel.WatchExpressionsObjectGroupName);
286             }
287             return Promise.resolve(null);
288         }
289
290         for (let target of WI.targets)
291             target.RuntimeAgent.releaseObjectGroup(WI.ScopeChainDetailsSidebarPanel.WatchExpressionsObjectGroupName);
292         this._usedWatchExpressionsObjectGroup = true;
293
294         let watchExpressionsRemoteObject = WI.RemoteObject.createFakeRemoteObject();
295         let fakePropertyPath = WI.PropertyPath.emptyPropertyPathForScope(watchExpressionsRemoteObject);
296         let objectTree = new WI.ObjectTreeView(watchExpressionsRemoteObject, WI.ObjectTreeView.Mode.Properties, fakePropertyPath);
297         objectTree.showOnlyProperties();
298
299         let treeOutline = objectTree.treeOutline;
300         const watchExpressionSectionIdentifier = "watch-expressions";
301         treeOutline.addEventListener(WI.TreeOutline.Event.ElementAdded, this._treeElementAdded.bind(this, watchExpressionSectionIdentifier), this);
302         treeOutline.addEventListener(WI.TreeOutline.Event.ElementDisclosureDidChanged, this._treeElementDisclosureDidChange.bind(this, watchExpressionSectionIdentifier), this);
303         treeOutline.objectTreeElementAddContextMenuItems = this._objectTreeElementAddContextMenuItems.bind(this);
304
305         let promises = [];
306         for (let expression of watchExpressions) {
307             promises.push(new Promise(function(resolve, reject) {
308                 let options = {objectGroup: WI.ScopeChainDetailsSidebarPanel.WatchExpressionsObjectGroupName, includeCommandLineAPI: false, doNotPauseOnExceptionsAndMuteConsole: true, returnByValue: false, generatePreview: true, saveResult: false};
309                 WI.runtimeManager.evaluateInInspectedWindow(expression, options, function(object, wasThrown) {
310                     object = object || WI.RemoteObject.fromPrimitiveValue(undefined);
311                     let propertyDescriptor = new WI.PropertyDescriptor({name: expression, value: object}, undefined, undefined, wasThrown);
312                     objectTree.appendExtraPropertyDescriptor(propertyDescriptor);
313                     resolve();
314                 });
315             }));
316         }
317
318         return Promise.all(promises).then(function() {
319             return Promise.resolve(new WI.ObjectPropertiesDetailSectionRow(objectTree));
320         });
321     }
322
323     _addWatchExpression(expression)
324     {
325         let watchExpressions = this._watchExpressionsSetting.value.slice(0);
326         watchExpressions.push(expression);
327         this._watchExpressionsSetting.value = watchExpressions;
328
329         this.needsLayout();
330     }
331
332     _removeWatchExpression(expression)
333     {
334         let watchExpressions = this._watchExpressionsSetting.value.slice(0);
335         watchExpressions.remove(expression, true);
336         this._watchExpressionsSetting.value = watchExpressions;
337
338         this.needsLayout();
339     }
340
341     _clearAllWatchExpressions()
342     {
343         this._watchExpressionsSetting.value = [];
344
345         this.needsLayout();
346     }
347
348     _addWatchExpressionButtonClicked(event)
349     {
350         function presentPopoverOverTargetElement()
351         {
352             let target = WI.Rect.rectFromClientRect(event.target.element.getBoundingClientRect());
353             popover.present(target, [WI.RectEdge.MAX_Y, WI.RectEdge.MIN_Y, WI.RectEdge.MAX_X]);
354         }
355
356         let popover = new WI.Popover(this);
357         let content = document.createElement("div");
358         content.classList.add("watch-expression");
359         content.appendChild(document.createElement("div")).textContent = WI.UIString("Add New Watch Expression");
360
361         let editorElement = content.appendChild(document.createElement("div"));
362         editorElement.classList.add("watch-expression-editor", WI.SyntaxHighlightedStyleClassName);
363
364         this._codeMirror = WI.CodeMirrorEditor.create(editorElement, {
365             lineWrapping: true,
366             mode: "text/javascript",
367             matchBrackets: true,
368             value: "",
369         });
370
371         this._popoverCommitted = false;
372
373         this._codeMirror.addKeyMap({
374             "Enter": () => { this._popoverCommitted = true; popover.dismiss(); },
375         });
376
377         let completionController = new WI.CodeMirrorCompletionController(this._codeMirror);
378         completionController.addExtendedCompletionProvider("javascript", WI.javaScriptRuntimeCompletionProvider);
379
380         // Resize the popover as best we can when the CodeMirror editor changes size.
381         let previousHeight = 0;
382         this._codeMirror.on("changes", function(cm, event) {
383             let height = cm.getScrollInfo().height;
384             if (previousHeight !== height) {
385                 previousHeight = height;
386                 popover.update(false);
387             }
388         });
389
390         popover.content = content;
391
392         popover.windowResizeHandler = presentPopoverOverTargetElement;
393         presentPopoverOverTargetElement();
394
395         // CodeMirror needs a refresh after the popover displays, to layout, otherwise it doesn't appear.
396         setTimeout(() => {
397             this._codeMirror.refresh();
398             this._codeMirror.focus();
399             popover.update();
400         }, 0);
401     }
402
403     willDismissPopover(popover)
404     {
405         if (this._popoverCommitted) {
406             let expression = this._codeMirror.getValue().trim();
407             if (expression)
408                 this._addWatchExpression(expression);
409         }
410
411         this._codeMirror = null;
412     }
413
414     _refreshAllWatchExpressionsButtonClicked(event)
415     {
416         this.needsLayout();
417     }
418
419     _clearAllWatchExpressionsButtonClicked(event)
420     {
421         this._clearAllWatchExpressions();
422     }
423
424     _didEvaluateExpression(event)
425     {
426         if (event.data.objectGroup === WI.ScopeChainDetailsSidebarPanel.WatchExpressionsObjectGroupName)
427             return;
428
429         this.needsLayout();
430     }
431
432     _activeExecutionContextChanged()
433     {
434         this.needsLayout();
435     }
436
437     _activeCallFrameDidChange()
438     {
439         this.needsLayout();
440     }
441
442     _mainResourceDidChange(event)
443     {
444         if (!event.target.isMainFrame())
445             return;
446
447         this.needsLayout();
448     }
449
450     _objectTreeElementAddContextMenuItems(objectTreeElement, contextMenu)
451     {
452         // Only add our watch expression context menus to the top level ObjectTree elements.
453         if (objectTreeElement.parent !== objectTreeElement.treeOutline)
454             return;
455
456         contextMenu.appendItem(WI.UIString("Remove Watch Expression"), () => {
457             let expression = objectTreeElement.property.name;
458             this._removeWatchExpression(expression);
459         });
460     }
461
462     _propertyPathIdentifierForTreeElement(identifier, objectPropertyTreeElement)
463     {
464         if (!objectPropertyTreeElement.property)
465             return null;
466
467         let propertyPath = objectPropertyTreeElement.thisPropertyPath();
468         if (propertyPath.isFullPathImpossible())
469             return null;
470
471         return identifier + "-" + propertyPath.fullPath;
472     }
473
474     _treeElementAdded(identifier, event)
475     {
476         let treeElement = event.data.element;
477         let propertyPathIdentifier = this._propertyPathIdentifierForTreeElement(identifier, treeElement);
478         if (!propertyPathIdentifier)
479             return;
480
481         if (WI.ScopeChainDetailsSidebarPanel._autoExpandProperties.has(propertyPathIdentifier))
482             treeElement.expand();
483     }
484
485     _treeElementDisclosureDidChange(identifier, event)
486     {
487         let treeElement = event.data.element;
488         let propertyPathIdentifier = this._propertyPathIdentifierForTreeElement(identifier, treeElement);
489         if (!propertyPathIdentifier)
490             return;
491
492         if (treeElement.expanded)
493             WI.ScopeChainDetailsSidebarPanel._autoExpandProperties.add(propertyPathIdentifier);
494         else
495             WI.ScopeChainDetailsSidebarPanel._autoExpandProperties.delete(propertyPathIdentifier);
496     }
497
498     _updateWatchExpressionsNavigationBar()
499     {
500         let enabled = this._watchExpressionsSetting.value.length;
501         this._refreshAllWatchExpressionButton.enabled = enabled;
502         this._clearAllWatchExpressionButton.enabled = enabled;
503     }
504 };
505
506 WI.ScopeChainDetailsSidebarPanel._autoExpandProperties = new Set;
507 WI.ScopeChainDetailsSidebarPanel.WatchExpressionsObjectGroupName = "watch-expressions";