Add MSE logging configuration
[WebKit-https.git] / Source / WebInspectorUI / UserInterface / Views / LogContentView.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 // FIXME: <https://webkit.org/b/143545> Web Inspector: LogContentView should use higher level objects
27
28 WI.LogContentView = class LogContentView extends WI.ContentView
29 {
30     constructor(representedObject)
31     {
32         super(representedObject);
33
34         this._nestingLevel = 0;
35         this._selectedMessages = [];
36
37         // FIXME: Try to use a marker, instead of a list of messages that get re-added.
38         this._provisionalMessages = [];
39
40         this.element.classList.add("log");
41
42         this.messagesElement = document.createElement("div");
43         this.messagesElement.classList.add("console-messages");
44         this.messagesElement.tabIndex = 0;
45         this.messagesElement.setAttribute("role", "log");
46         this.messagesElement.addEventListener("mousedown", this._mousedown.bind(this));
47         this.messagesElement.addEventListener("keydown", this._keyDown.bind(this));
48         this.messagesElement.addEventListener("keypress", this._keyPress.bind(this));
49         this.messagesElement.addEventListener("dragstart", this._ondragstart.bind(this), true);
50         this.element.appendChild(this.messagesElement);
51
52         this.prompt = WI.quickConsole.prompt;
53
54         this._keyboardShortcutCommandA = new WI.KeyboardShortcut(WI.KeyboardShortcut.Modifier.CommandOrControl, "A");
55         this._keyboardShortcutEsc = new WI.KeyboardShortcut(null, WI.KeyboardShortcut.Key.Escape);
56
57         this._logViewController = new WI.JavaScriptLogViewController(this.messagesElement, this.element, this.prompt, this, "console-prompt-history");
58         this._lastMessageView = null;
59
60         const fixed = true;
61         this._findBanner = new WI.FindBanner(this, "console-find-banner", fixed);
62         this._findBanner.targetElement = this.element;
63
64         this._currentSearchQuery = "";
65         this._searchMatches = [];
66         this._selectedSearchMatch = null;
67         this._selectedSearchMatchIsValid = false;
68
69         this._preserveLogNavigationItem = new WI.CheckboxNavigationItem("preserve-log", WI.UIString("Preserve Log"), !WI.settings.clearLogOnNavigate.value);
70         this._preserveLogNavigationItem.tooltip = WI.UIString("Do not clear the console on new page loads");
71         this._preserveLogNavigationItem.addEventListener(WI.CheckboxNavigationItem.Event.CheckedDidChange, () => {
72             WI.settings.clearLogOnNavigate.value = !WI.settings.clearLogOnNavigate.value;
73         });
74         WI.settings.clearLogOnNavigate.addEventListener(WI.Setting.Event.Changed, this._handleClearLogOnNavigateSettingChanged, this);
75
76         this._emulateInUserGestureNavigationItem = new WI.CheckboxNavigationItem("emulate-in-user-gesture", WI.UIString("Emulate User Gesture"), WI.settings.emulateInUserGesture.value);
77         this._emulateInUserGestureNavigationItem.tooltip = WI.UIString("Run console commands as if inside a user gesture");
78         this._emulateInUserGestureNavigationItem.addEventListener(WI.CheckboxNavigationItem.Event.CheckedDidChange, () => {
79             WI.settings.emulateInUserGesture.value = !WI.settings.emulateInUserGesture.value;
80         });
81         WI.settings.emulateInUserGesture.addEventListener(WI.Setting.Event.Changed, this._handleEmulateInUserGestureSettingChanged, this);
82
83         this._checkboxesNavigationItemGroup = new WI.GroupNavigationItem([this._preserveLogNavigationItem, this._emulateInUserGestureNavigationItem, new WI.DividerNavigationItem]);
84
85         let scopeBarItems = [
86             new WI.ScopeBarItem(WI.LogContentView.Scopes.All, WI.UIString("All"), {exclusive: true}),
87             new WI.ScopeBarItem(WI.LogContentView.Scopes.Errors, WI.UIString("Errors"), {className: "errors"}),
88             new WI.ScopeBarItem(WI.LogContentView.Scopes.Warnings, WI.UIString("Warnings"), {className: "warnings"}),
89             new WI.ScopeBarItem(WI.LogContentView.Scopes.Logs, WI.UIString("Logs"), {className: "logs"}),
90             new WI.ScopeBarItem(WI.LogContentView.Scopes.Infos, WI.UIString("Infos"), {className: "infos", hidden: true}),
91             new WI.ScopeBarItem(WI.LogContentView.Scopes.Debugs, WI.UIString("Debugs"), {className: "debugs", hidden: true}),
92         ];
93
94         this._scopeBar = new WI.ScopeBar("log-scope-bar", scopeBarItems, scopeBarItems[0]);
95         this._scopeBar.addEventListener(WI.ScopeBar.Event.SelectionChanged, this._scopeBarSelectionDidChange, this);
96
97         this._hasNonDefaultLogChannelMessage = false;
98         if (WI.ConsoleManager.supportsLogChannels()) {
99             let messageChannelBarItems = [
100                 new WI.ScopeBarItem(WI.LogContentView.Scopes.AllChannels, WI.UIString("All"), {exclusive: true}),
101                 new WI.ScopeBarItem(WI.LogContentView.Scopes.Media, WI.UIString("Media"), {className: "media"}),
102                 new WI.ScopeBarItem(WI.LogContentView.Scopes.MediaSource, WI.UIString("MediaSource"), {className: "mediasource"}),
103                 new WI.ScopeBarItem(WI.LogContentView.Scopes.WebRTC, WI.UIString("WebRTC"), {className: "webrtc"}),
104             ];
105
106             this._messageSourceBar = new WI.ScopeBar("message-channel-scope-bar", messageChannelBarItems, messageChannelBarItems[0]);
107             this._messageSourceBar.addEventListener(WI.ScopeBar.Event.SelectionChanged, this._messageSourceBarSelectionDidChange, this);
108         }
109
110         this._garbageCollectNavigationItem = new WI.ButtonNavigationItem("garbage-collect", WI.UIString("Collect garbage"), "Images/NavigationItemGarbageCollect.svg", 16, 16);
111         this._garbageCollectNavigationItem.visibilityPriority = WI.NavigationItem.VisibilityPriority.Low;
112         this._garbageCollectNavigationItem.addEventListener(WI.ButtonNavigationItem.Event.Clicked, this._garbageCollect, this);
113
114         this._clearLogNavigationItem = new WI.ButtonNavigationItem("clear-log", WI.UIString("Clear log (%s or %s)").format(WI.clearKeyboardShortcut.displayName, this._logViewController.messagesAlternateClearKeyboardShortcut.displayName), "Images/NavigationItemTrash.svg", 15, 15);
115         this._clearLogNavigationItem.visibilityPriority = WI.NavigationItem.VisibilityPriority.Low;
116         this._clearLogNavigationItem.addEventListener(WI.ButtonNavigationItem.Event.Clicked, this._clearLog, this);
117
118         this._showConsoleTabNavigationItem = new WI.ButtonNavigationItem("show-tab", WI.UIString("Show Console tab"), "Images/SplitToggleUp.svg", 16, 16);
119         this._showConsoleTabNavigationItem.visibilityPriority = WI.NavigationItem.VisibilityPriority.High;
120         this._showConsoleTabNavigationItem.addEventListener(WI.ButtonNavigationItem.Event.Clicked, this._showConsoleTab, this);
121
122         this.messagesElement.addEventListener("contextmenu", this._handleContextMenuEvent.bind(this), false);
123
124         WI.consoleManager.addEventListener(WI.ConsoleManager.Event.SessionStarted, this._sessionStarted, this);
125         WI.consoleManager.addEventListener(WI.ConsoleManager.Event.MessageAdded, this._messageAdded, this);
126         WI.consoleManager.addEventListener(WI.ConsoleManager.Event.PreviousMessageRepeatCountUpdated, this._previousMessageRepeatCountUpdated, this);
127         WI.consoleManager.addEventListener(WI.ConsoleManager.Event.Cleared, this._logCleared, this);
128
129         WI.Frame.addEventListener(WI.Frame.Event.ProvisionalLoadStarted, this._provisionalLoadStarted, this);
130     }
131
132     // Public
133
134     get navigationItems()
135     {
136         let navigationItems = [this._scopeBar, new WI.DividerNavigationItem];
137
138         if (this._hasNonDefaultLogChannelMessage && this._messageSourceBar)
139             navigationItems.push(this._messageSourceBar, new WI.DividerNavigationItem);
140
141         if (window.HeapAgent && HeapAgent.gc)
142             navigationItems.push(this._garbageCollectNavigationItem);
143
144         navigationItems.push(this._clearLogNavigationItem);
145
146         if (WI.isShowingSplitConsole())
147             navigationItems.push(new WI.DividerNavigationItem, this._showConsoleTabNavigationItem);
148         else if (WI.isShowingConsoleTab())
149             navigationItems.unshift(this._findBanner, this._checkboxesNavigationItemGroup);
150
151         return navigationItems;
152     }
153
154     get scopeBar()
155     {
156         return this._scopeBar;
157     }
158
159     get logViewController()
160     {
161         return this._logViewController;
162     }
163
164     get scrollableElements()
165     {
166         return [this.element];
167     }
168
169     get shouldKeepElementsScrolledToBottom()
170     {
171         return true;
172     }
173
174     shown()
175     {
176         super.shown();
177
178         this._logViewController.renderPendingMessages();
179     }
180
181     closed()
182     {
183         // While it may be possible to get here, this is a singleton ContentView instance
184         // that is often re-inserted back into different ContentBrowsers, so we shouldn't
185         // remove the event listeners. The singleton will never go away anyways.
186         console.assert(this === WI.consoleContentView);
187
188         super.closed();
189     }
190
191     didAppendConsoleMessageView(messageView)
192     {
193         console.assert(messageView instanceof WI.ConsoleMessageView || messageView instanceof WI.ConsoleCommandView);
194
195         // Nest the message.
196         var type = messageView instanceof WI.ConsoleCommandView ? null : messageView.message.type;
197         if (this._nestingLevel && type !== WI.ConsoleMessage.MessageType.EndGroup) {
198             var x = 16 * this._nestingLevel;
199             var messageElement = messageView.element;
200             messageElement.style.left = x + "px";
201             messageElement.style.width = "calc(100% - " + x + "px)";
202         }
203
204         // Update the nesting level.
205         switch (type) {
206         case WI.ConsoleMessage.MessageType.StartGroup:
207         case WI.ConsoleMessage.MessageType.StartGroupCollapsed:
208             ++this._nestingLevel;
209             break;
210         case WI.ConsoleMessage.MessageType.EndGroup:
211             if (this._nestingLevel > 0)
212                 --this._nestingLevel;
213             break;
214         }
215
216         this._clearFocusableChildren();
217
218         // Some results don't populate until further backend dispatches occur (like the DOM tree).
219         // We want to remove focusable children after those pending dispatches too.
220         let target = messageView.message ? messageView.message.target : WI.runtimeManager.activeExecutionContext.target;
221         target.connection.runAfterPendingDispatches(this._clearFocusableChildren.bind(this));
222
223         if (type && type !== WI.ConsoleMessage.MessageType.EndGroup) {
224             console.assert(messageView.message instanceof WI.ConsoleMessage);
225             if (!(messageView.message instanceof WI.ConsoleCommandResultMessage))
226                 this._markScopeBarItemUnread(messageView.message.level);
227
228             console.assert(messageView.element instanceof Element);
229             this._filterMessageElements([messageView.element]);
230         }
231     }
232
233     get supportsSearch() { return true; }
234     get numberOfSearchResults() { return this.hasPerformedSearch ? this._searchMatches.length : null; }
235     get hasPerformedSearch() { return this._currentSearchQuery !== ""; }
236
237     get supportsCustomFindBanner()
238     {
239         return WI.isShowingConsoleTab();
240     }
241
242     showCustomFindBanner()
243     {
244         if (!this.visible)
245             return;
246
247         this._findBanner.focus();
248     }
249
250     get supportsSave()
251     {
252         if (!this.visible)
253             return false;
254
255         if (WI.isShowingSplitConsole())
256             return false;
257
258         return true;
259     }
260
261     get saveData()
262     {
263         return {url: "web-inspector:///Console.txt", content: this._formatMessagesAsData(false), forceSaveAs: true};
264     }
265
266     handleCopyEvent(event)
267     {
268         if (!this._selectedMessages.length)
269             return;
270
271         event.clipboardData.setData("text/plain", this._formatMessagesAsData(true));
272         event.stopPropagation();
273         event.preventDefault();
274     }
275
276     handleClearShortcut(event)
277     {
278         this._logViewController.requestClearMessages();
279     }
280
281     handlePopulateFindShortcut()
282     {
283         let searchQuery = this.searchQueryWithSelection();
284         if (!searchQuery)
285             return;
286
287         this._findBanner.searchQuery = searchQuery;
288
289         this.performSearch(this._findBanner.searchQuery);
290     }
291
292     handleFindNextShortcut()
293     {
294         this.findBannerRevealNextResult(this._findBanner);
295     }
296
297     handleFindPreviousShortcut()
298     {
299         this.findBannerRevealPreviousResult(this._findBanner);
300     }
301
302     findBannerRevealPreviousResult()
303     {
304         this.highlightPreviousSearchMatch();
305     }
306
307     highlightPreviousSearchMatch()
308     {
309         if (!this.hasPerformedSearch || isEmptyObject(this._searchMatches))
310             return;
311
312         var index = this._selectedSearchMatch ? this._searchMatches.indexOf(this._selectedSearchMatch) : this._searchMatches.length;
313         this._highlightSearchMatchAtIndex(index - 1);
314     }
315
316     findBannerRevealNextResult()
317     {
318         this.highlightNextSearchMatch();
319     }
320
321     highlightNextSearchMatch()
322     {
323         if (!this.hasPerformedSearch || isEmptyObject(this._searchMatches))
324             return;
325
326         var index = this._selectedSearchMatch ? this._searchMatches.indexOf(this._selectedSearchMatch) + 1 : 0;
327         this._highlightSearchMatchAtIndex(index);
328     }
329
330     findBannerWantsToClearAndBlur(findBanner)
331     {
332         if (this._selectedMessages.length)
333             this.messagesElement.focus();
334         else
335             this.prompt.focus();
336     }
337
338     // Protected
339
340     layout()
341     {
342         this._scrollElementHeight = this.messagesElement.getBoundingClientRect().height;
343     }
344
345     // Private
346
347     _formatMessagesAsData(onlySelected)
348     {
349         var messages = this._allMessageElements();
350
351         if (onlySelected) {
352             messages = messages.filter(function(message) {
353                 return message.classList.contains(WI.LogContentView.SelectedStyleClassName);
354             });
355         }
356
357         var data = "";
358
359         var isPrefixOptional = messages.length <= 1 && onlySelected;
360         messages.forEach(function(messageElement, index) {
361             var messageView = messageElement.__messageView || messageElement.__commandView;
362             if (!messageView)
363                 return;
364
365             if (index > 0)
366                 data += "\n";
367             data += messageView.toClipboardString(isPrefixOptional);
368         });
369
370         return data;
371     }
372
373     _sessionStarted(event)
374     {
375         if (WI.settings.clearLogOnNavigate.value) {
376             this._reappendProvisionalMessages();
377             return;
378         }
379
380         const isFirstSession = false;
381         const newSessionReason = event.data.wasReloaded ? WI.ConsoleSession.NewSessionReason.PageReloaded : WI.ConsoleSession.NewSessionReason.PageNavigated;
382         this._logViewController.startNewSession(isFirstSession, {newSessionReason, timestamp: event.data.timestamp});
383
384         this._clearProvisionalState();
385     }
386
387     _scopeFromMessageSource(source)
388     {
389         switch (source) {
390         case WI.ConsoleMessage.MessageSource.Media:
391             return WI.LogContentView.Scopes.Media;
392         case WI.ConsoleMessage.MessageSource.WebRTC:
393             return WI.LogContentView.Scopes.WebRTC;
394         case WI.ConsoleMessage.MessageSource.MediaSource:
395             return WI.LogContentView.Scopes.MediaSource;
396         }
397
398         return undefined;
399     }
400
401     _scopeFromMessageLevel(level)
402     {
403         switch (level) {
404         case WI.ConsoleMessage.MessageLevel.Warning:
405             return WI.LogContentView.Scopes.Warnings;
406         case WI.ConsoleMessage.MessageLevel.Error:
407             return WI.LogContentView.Scopes.Errors;
408         case WI.ConsoleMessage.MessageLevel.Log:
409             return WI.LogContentView.Scopes.Logs;
410         case WI.ConsoleMessage.MessageLevel.Info:
411             return this._hasNonDefaultLogChannelMessage ? WI.LogContentView.Scopes.Infos : WI.LogContentView.Scopes.Logs;
412         case WI.ConsoleMessage.MessageLevel.Debug:
413             return this._hasNonDefaultLogChannelMessage ? WI.LogContentView.Scopes.Debugs : WI.LogContentView.Scopes.Logs;
414         }
415         console.assert(false, "This should not be reached.");
416
417         return undefined;
418     }
419
420     _markScopeBarItemUnread(level)
421     {
422         let messageLevel = this._scopeFromMessageLevel(level);
423         if (!messageLevel)
424             return;
425
426         let item = this._scopeBar.item(messageLevel);
427         if (item && !item.selected && !this._scopeBar.item(WI.LogContentView.Scopes.All).selected)
428             item.element.classList.add("unread");
429     }
430
431     _messageAdded(event)
432     {
433         let message = event.data.message;
434         if (this._startedProvisionalLoad)
435             this._provisionalMessages.push(message);
436
437         if (!this._hasNonDefaultLogChannelMessage && WI.consoleManager.logChannelSources.includes(message.source)) {
438             this._hasNonDefaultLogChannelMessage = true;
439             this.dispatchEventToListeners(WI.ContentView.Event.NavigationItemsDidChange);
440             this._scopeBar.item(WI.LogContentView.Scopes.Infos).hidden = false;
441             this._scopeBar.item(WI.LogContentView.Scopes.Debugs).hidden = false;
442         }
443
444         this._logViewController.appendConsoleMessage(message);
445     }
446
447     _previousMessageRepeatCountUpdated(event)
448     {
449         if (this._logViewController.updatePreviousMessageRepeatCount(event.data.count) && this._lastMessageView)
450             this._markScopeBarItemUnread(this._lastMessageView.message.level);
451     }
452
453     _handleContextMenuEvent(event)
454     {
455         if (!window.getSelection().isCollapsed) {
456             // If there is a selection, we want to show our normal context menu
457             // (with Copy, etc.), and not Clear Log.
458             return;
459         }
460
461         // In the case that there are selected messages, only clear that selection if the right-click
462         // is not on the element or descendants of the selected messages.
463         if (this._selectedMessages.length && !this._selectedMessages.some(element => element.contains(event.target))) {
464             this._clearMessagesSelection();
465             this._mousedown(event);
466         }
467
468         // If there are no selected messages, right-clicking will not reset the current mouse state
469         // meaning that when the context menu is dismissed, console messages will be selected when
470         // the user moves the mouse even though no buttons are pressed.
471         if (!this._selectedMessages.length)
472             this._mouseup(event);
473
474         // We don't want to show the custom menu for links in the console.
475         if (event.target.enclosingNodeOrSelfWithNodeName("a"))
476             return;
477
478         let contextMenu = WI.ContextMenu.createFromEvent(event);
479
480         if (this._selectedMessages.length) {
481             contextMenu.appendItem(WI.UIString("Copy Selected"), () => {
482                 InspectorFrontendHost.copyText(this._formatMessagesAsData(true));
483             });
484
485             contextMenu.appendItem(WI.UIString("Save Selected"), () => {
486                 const forceSaveAs = true;
487                 WI.FileUtilities.save({
488                     url: "web-inspector:///Console.txt",
489                     content: this._formatMessagesAsData(true),
490                 }, forceSaveAs);
491             });
492
493             contextMenu.appendSeparator();
494         }
495
496         contextMenu.appendItem(WI.UIString("Clear Log"), this._clearLog.bind(this));
497         contextMenu.appendSeparator();
498     }
499
500     _mousedown(event)
501     {
502         if (this._selectedMessages.length && (event.button !== 0 || event.ctrlKey))
503             return;
504
505         if (event.defaultPrevented) {
506             // Default was prevented on the event, so this means something deeper (like a disclosure triangle)
507             // handled the mouse down. In this case we want to clear the selection and don't make a new selection.
508             this._clearMessagesSelection();
509             return;
510         }
511
512         this._mouseDownWrapper = event.target.enclosingNodeOrSelfWithClass(WI.LogContentView.ItemWrapperStyleClassName);
513         this._mouseDownShiftKey = event.shiftKey;
514         this._mouseDownCommandKey = event.metaKey;
515         this._mouseMoveIsRowSelection = false;
516
517         window.addEventListener("mousemove", this);
518         window.addEventListener("mouseup", this);
519     }
520
521     _targetInMessageCanBeSelected(target, message)
522     {
523         if (target.enclosingNodeOrSelfWithNodeName("a"))
524             return false;
525         return true;
526     }
527
528     _mousemove(event)
529     {
530         var selection = window.getSelection();
531         var wrapper = event.target.enclosingNodeOrSelfWithClass(WI.LogContentView.ItemWrapperStyleClassName);
532
533         if (!wrapper) {
534             // No wrapper under the mouse, so look at the selection to try and find one.
535             if (!selection.isCollapsed) {
536                 wrapper = selection.focusNode.parentNode.enclosingNodeOrSelfWithClass(WI.LogContentView.ItemWrapperStyleClassName);
537                 selection.removeAllRanges();
538             }
539
540             if (!wrapper)
541                 return;
542         }
543
544         if (!selection.isCollapsed)
545             this._clearMessagesSelection();
546
547         if (wrapper === this._mouseDownWrapper && !this._mouseMoveIsRowSelection)
548             return;
549
550         selection.removeAllRanges();
551
552         if (!this._mouseMoveIsRowSelection)
553             this._updateMessagesSelection(this._mouseDownWrapper, this._mouseDownCommandKey, this._mouseDownShiftKey, false);
554
555         this._updateMessagesSelection(wrapper, false, true, false);
556
557         this._mouseMoveIsRowSelection = true;
558
559         event.preventDefault();
560         event.stopPropagation();
561     }
562
563     _mouseup(event)
564     {
565         window.removeEventListener("mousemove", this);
566         window.removeEventListener("mouseup", this);
567
568         var selection = window.getSelection();
569         var wrapper = event.target.enclosingNodeOrSelfWithClass(WI.LogContentView.ItemWrapperStyleClassName);
570
571         if (wrapper && (selection.isCollapsed || event.shiftKey)) {
572             selection.removeAllRanges();
573
574             if (this._targetInMessageCanBeSelected(event.target, wrapper)) {
575                 var sameWrapper = wrapper === this._mouseDownWrapper;
576                 this._updateMessagesSelection(wrapper, sameWrapper ? this._mouseDownCommandKey : false, sameWrapper ? this._mouseDownShiftKey : true, false);
577             }
578         } else if (!selection.isCollapsed) {
579             // There is a text selection, clear the row selection.
580             this._clearMessagesSelection();
581         } else if (!this._mouseDownWrapper) {
582             // The mouse didn't hit a console item, so clear the row selection.
583             this._clearMessagesSelection();
584
585             // Focus the prompt. Focusing the prompt needs to happen after the click to work.
586             setTimeout(() => { this.prompt.focus(); }, 0);
587         }
588
589         delete this._mouseMoveIsRowSelection;
590         delete this._mouseDownWrapper;
591         delete this._mouseDownShiftKey;
592         delete this._mouseDownCommandKey;
593     }
594
595     _ondragstart(event)
596     {
597         if (event.target.enclosingNodeOrSelfWithClass(WI.DOMTreeOutline.StyleClassName)) {
598             event.stopPropagation();
599             event.preventDefault();
600         }
601     }
602
603     handleEvent(event)
604     {
605         switch (event.type) {
606         case "mousemove":
607             this._mousemove(event);
608             break;
609         case "mouseup":
610             this._mouseup(event);
611             break;
612         }
613     }
614
615     _updateMessagesSelection(message, multipleSelection, rangeSelection, shouldScrollIntoView)
616     {
617         console.assert(message);
618         if (!message)
619             return;
620
621         var alreadySelectedMessage = this._selectedMessages.includes(message);
622         if (alreadySelectedMessage && this._selectedMessages.length && multipleSelection) {
623             message.classList.remove(WI.LogContentView.SelectedStyleClassName);
624             this._selectedMessages.remove(message);
625             return;
626         }
627
628         if (!multipleSelection && !rangeSelection)
629             this._clearMessagesSelection();
630
631         if (rangeSelection) {
632             var messages = this._visibleMessageElements();
633
634             var refIndex = this._referenceMessageForRangeSelection ? messages.indexOf(this._referenceMessageForRangeSelection) : 0;
635             var targetIndex = messages.indexOf(message);
636
637             var newRange = [Math.min(refIndex, targetIndex), Math.max(refIndex, targetIndex)];
638
639             if (this._selectionRange && this._selectionRange[0] === newRange[0] && this._selectionRange[1] === newRange[1])
640                 return;
641
642             var startIndex = this._selectionRange ? Math.min(this._selectionRange[0], newRange[0]) : newRange[0];
643             var endIndex = this._selectionRange ? Math.max(this._selectionRange[1], newRange[1]) : newRange[1];
644
645             for (var i = startIndex; i <= endIndex; ++i) {
646                 var messageInRange = messages[i];
647                 if (i >= newRange[0] && i <= newRange[1] && !messageInRange.classList.contains(WI.LogContentView.SelectedStyleClassName)) {
648                     messageInRange.classList.add(WI.LogContentView.SelectedStyleClassName);
649                     this._selectedMessages.push(messageInRange);
650                 } else if (i < newRange[0] || i > newRange[1] && messageInRange.classList.contains(WI.LogContentView.SelectedStyleClassName)) {
651                     messageInRange.classList.remove(WI.LogContentView.SelectedStyleClassName);
652                     this._selectedMessages.remove(messageInRange);
653                 }
654             }
655
656             this._selectionRange = newRange;
657         } else {
658             message.classList.add(WI.LogContentView.SelectedStyleClassName);
659             this._selectedMessages.push(message);
660         }
661
662         if (!rangeSelection)
663             this._referenceMessageForRangeSelection = message;
664
665         if (shouldScrollIntoView && !alreadySelectedMessage)
666             this._ensureMessageIsVisible(this._selectedMessages.lastValue);
667     }
668
669     _ensureMessageIsVisible(message)
670     {
671         if (!message)
672             return;
673
674         var y = this._positionForMessage(message).y;
675         if (y < 0) {
676             this.element.scrollTop += y;
677             return;
678         }
679
680         var nextMessage = this._nextMessage(message);
681         if (nextMessage) {
682             y = this._positionForMessage(nextMessage).y;
683             if (y > this._scrollElementHeight)
684                 this.element.scrollTop += y - this._scrollElementHeight;
685         } else {
686             y += message.getBoundingClientRect().height;
687             if (y > this._scrollElementHeight)
688                 this.element.scrollTop += y - this._scrollElementHeight;
689         }
690     }
691
692     _positionForMessage(message)
693     {
694         var pagePoint = window.webkitConvertPointFromNodeToPage(message, new WebKitPoint(0, 0));
695         return window.webkitConvertPointFromPageToNode(this.element, pagePoint);
696     }
697
698     _isMessageVisible(message)
699     {
700         var node = message;
701
702         if (node.classList.contains(WI.LogContentView.FilteredOutStyleClassName))
703             return false;
704
705         if (this.hasPerformedSearch && node.classList.contains(WI.LogContentView.FilteredOutBySearchStyleClassName))
706             return false;
707
708         if (message.classList.contains("console-group-title"))
709             node = node.parentNode.parentNode;
710
711         while (node && node !== this.messagesElement) {
712             if (node.classList.contains("collapsed"))
713                 return false;
714             node = node.parentNode;
715         }
716
717         return true;
718     }
719
720     _isMessageSelected(message)
721     {
722         return message.classList.contains(WI.LogContentView.SelectedStyleClassName);
723     }
724
725     _clearMessagesSelection()
726     {
727         this._selectedMessages.forEach(function(message) {
728             message.classList.remove(WI.LogContentView.SelectedStyleClassName);
729         });
730         this._selectedMessages = [];
731         delete this._referenceMessageForRangeSelection;
732     }
733
734     _selectAllMessages()
735     {
736         this._clearMessagesSelection();
737
738         var messages = this._visibleMessageElements();
739         for (var i = 0; i < messages.length; ++i) {
740             var message = messages[i];
741             message.classList.add(WI.LogContentView.SelectedStyleClassName);
742             this._selectedMessages.push(message);
743         }
744     }
745
746     _allMessageElements()
747     {
748         return Array.from(this.messagesElement.querySelectorAll(".console-message, .console-user-command"));
749     }
750
751     _unfilteredMessageElements()
752     {
753         return this._allMessageElements().filter(function(message) {
754             return !message.classList.contains(WI.LogContentView.FilteredOutStyleClassName);
755         });
756     }
757
758     _visibleMessageElements()
759     {
760         var unfilteredMessages = this._unfilteredMessageElements();
761
762         if (!this.hasPerformedSearch)
763             return unfilteredMessages;
764
765         return unfilteredMessages.filter(function(message) {
766             return !message.classList.contains(WI.LogContentView.FilteredOutBySearchStyleClassName);
767         });
768     }
769
770     _logCleared(event)
771     {
772         for (let item of this._scopeBar.items)
773             item.element.classList.remove("unread");
774
775         this._logViewController.clear();
776         this._nestingLevel = 0;
777
778         if (this._currentSearchQuery)
779             this.performSearch(this._currentSearchQuery);
780     }
781
782     _showConsoleTab()
783     {
784         WI.showConsoleTab();
785     }
786
787     _clearLog()
788     {
789         WI.consoleManager.requestClearMessages();
790     }
791
792     _garbageCollect()
793     {
794         // COMPATIBILITY (iOS 10.3): Worker targets did not support HeapAgent.
795         for (let target of WI.targets) {
796             if (target.HeapAgent)
797                 target.HeapAgent.gc();
798         }
799     }
800
801     _messageShouldBeVisible(message)
802     {
803         let messageSource = this._messageSourceBar && this._scopeFromMessageSource(message.source);
804         if (messageSource && !this._messageSourceBar.item(messageSource).selected && !this._messageSourceBar.item(WI.LogContentView.Scopes.AllChannels).selected)
805             return false;
806
807         let messageLevel = this._scopeFromMessageLevel(message.level);
808         if (messageLevel)
809             return this._scopeBar.item(messageLevel).selected || this._scopeBar.item(WI.LogContentView.Scopes.All).selected;
810
811         return true;
812     }
813
814     _messageSourceBarSelectionDidChange(event)
815     {
816         let selectedItem = this._messageSourceBar.selectedItems[0];
817         if (selectedItem.id === WI.LogContentView.Scopes.AllChannels) {
818             for (let item of this._messageSourceBar.items)
819                 item.element.classList.remove("unread");
820         } else
821             selectedItem.element.classList.remove("unread");
822
823         this._filterMessageElements(this._allMessageElements());
824     }
825
826     _scopeBarSelectionDidChange(event)
827     {
828         let selectedItem = this._scopeBar.selectedItems[0];
829
830         if (selectedItem.id === WI.LogContentView.Scopes.All) {
831             for (let item of this._scopeBar.items)
832                 item.element.classList.remove("unread");
833         } else
834             selectedItem.element.classList.remove("unread");
835
836         this._filterMessageElements(this._allMessageElements());
837     }
838
839     _filterMessageElements(messageElements)
840     {
841         messageElements.forEach(function(messageElement) {
842             let visible = messageElement.__commandView instanceof WI.ConsoleCommandView || messageElement.__message instanceof WI.ConsoleCommandResultMessage;
843             if (!visible)
844                 visible = this._messageShouldBeVisible(messageElement.__message);
845
846             let classList = messageElement.classList;
847             if (visible)
848                 classList.remove(WI.LogContentView.FilteredOutStyleClassName);
849             else {
850                 this._selectedMessages.remove(messageElement);
851                 classList.remove(WI.LogContentView.SelectedStyleClassName);
852                 classList.add(WI.LogContentView.FilteredOutStyleClassName);
853             }
854         }, this);
855
856         this.performSearch(this._currentSearchQuery);
857     }
858
859     _handleClearLogOnNavigateSettingChanged()
860     {
861         this._preserveLogNavigationItem.checked = !WI.settings.clearLogOnNavigate.value;
862     }
863
864     _handleEmulateInUserGestureSettingChanged()
865     {
866         this._emulateInUserGestureNavigationItem.checked = WI.settings.emulateInUserGesture.value;
867     }
868
869     _keyDown(event)
870     {
871         let isRTL = WI.resolvedLayoutDirection() === WI.LayoutDirection.RTL;
872
873         if (this._keyboardShortcutCommandA.matchesEvent(event))
874             this._commandAWasPressed(event);
875         else if (this._keyboardShortcutEsc.matchesEvent(event))
876             this._escapeWasPressed(event);
877         else if (event.keyIdentifier === "Up")
878             this._upArrowWasPressed(event);
879         else if (event.keyIdentifier === "Down")
880             this._downArrowWasPressed(event);
881         else if ((!isRTL && event.keyIdentifier === "Left") || (isRTL && event.keyIdentifier === "Right"))
882             this._leftArrowWasPressed(event);
883         else if ((!isRTL && event.keyIdentifier === "Right") || (isRTL && event.keyIdentifier === "Left"))
884             this._rightArrowWasPressed(event);
885         else if (event.keyIdentifier === "Enter" && event.metaKey)
886             this._commandEnterWasPressed(event);
887     }
888
889     _keyPress(event)
890     {
891         const isCommandC = event.metaKey && event.keyCode === 99;
892         if (!isCommandC)
893             this.prompt.focus();
894     }
895
896     _commandAWasPressed(event)
897     {
898         this._selectAllMessages();
899         event.preventDefault();
900     }
901
902     _escapeWasPressed(event)
903     {
904         if (this._selectedMessages.length)
905             this._clearMessagesSelection();
906         else
907             this.prompt.focus();
908
909         event.preventDefault();
910     }
911
912     _upArrowWasPressed(event)
913     {
914         var messages = this._visibleMessageElements();
915
916         if (!this._selectedMessages.length) {
917             if (messages.length)
918                 this._updateMessagesSelection(messages.lastValue, false, false, true);
919             return;
920         }
921
922         var lastMessage = this._selectedMessages.lastValue;
923         var previousMessage = this._previousMessage(lastMessage);
924         if (previousMessage)
925             this._updateMessagesSelection(previousMessage, false, event.shiftKey, true);
926         else if (!event.shiftKey) {
927             this._clearMessagesSelection();
928             this._updateMessagesSelection(messages[0], false, false, true);
929         }
930
931         event.preventDefault();
932     }
933
934     _downArrowWasPressed(event)
935     {
936         var messages = this._visibleMessageElements();
937
938         if (!this._selectedMessages.length) {
939             if (messages.length)
940                 this._updateMessagesSelection(messages[0], false, false, true);
941             return;
942         }
943
944         var lastMessage = this._selectedMessages.lastValue;
945         var nextMessage = this._nextMessage(lastMessage);
946         if (nextMessage)
947             this._updateMessagesSelection(nextMessage, false, event.shiftKey, true);
948         else if (!event.shiftKey) {
949             this._clearMessagesSelection();
950             this._updateMessagesSelection(messages.lastValue, false, false, true);
951         }
952
953         event.preventDefault();
954     }
955
956     _leftArrowWasPressed(event)
957     {
958         if (this._selectedMessages.length !== 1)
959             return;
960
961         var currentMessage = this._selectedMessages[0];
962         if (currentMessage.classList.contains("console-group-title")) {
963             currentMessage.parentNode.classList.add("collapsed");
964             event.preventDefault();
965         } else if (currentMessage.__messageView && currentMessage.__messageView.expandable) {
966             currentMessage.__messageView.collapse();
967             event.preventDefault();
968         }
969     }
970
971     _rightArrowWasPressed(event)
972     {
973         if (this._selectedMessages.length !== 1)
974             return;
975
976         var currentMessage = this._selectedMessages[0];
977         if (currentMessage.classList.contains("console-group-title")) {
978             currentMessage.parentNode.classList.remove("collapsed");
979             event.preventDefault();
980         } else if (currentMessage.__messageView && currentMessage.__messageView.expandable) {
981             currentMessage.__messageView.expand();
982             event.preventDefault();
983         }
984     }
985
986     _commandEnterWasPressed(event)
987     {
988         if (this._selectedMessages.length !== 1)
989             return;
990
991         let message = this._selectedMessages[0];
992         if (message.__commandView && message.__commandView.commandText) {
993             this._logViewController.consolePromptTextCommitted(null, message.__commandView.commandText);
994             event.preventDefault();
995         }
996     }
997
998     _previousMessage(message)
999     {
1000         var messages = this._visibleMessageElements();
1001         for (var i = messages.indexOf(message) - 1; i >= 0; --i) {
1002             if (this._isMessageVisible(messages[i]))
1003                 return messages[i];
1004         }
1005         return null;
1006     }
1007
1008     _nextMessage(message)
1009     {
1010         var messages = this._visibleMessageElements();
1011         for (var i = messages.indexOf(message) + 1; i < messages.length; ++i) {
1012             if (this._isMessageVisible(messages[i]))
1013                 return messages[i];
1014         }
1015         return null;
1016     }
1017
1018     _clearFocusableChildren()
1019     {
1020         var focusableElements = this.messagesElement.querySelectorAll("[tabindex]");
1021         for (var i = 0, count = focusableElements.length; i < count; ++i)
1022             focusableElements[i].removeAttribute("tabindex");
1023     }
1024
1025     findBannerPerformSearch(findBanner, searchQuery)
1026     {
1027         this.performSearch(searchQuery);
1028     }
1029
1030     findBannerSearchCleared()
1031     {
1032         this.searchCleared();
1033     }
1034
1035     revealNextSearchResult()
1036     {
1037         this.findBannerRevealNextResult();
1038     }
1039
1040     revealPreviousSearchResult()
1041     {
1042         this.findBannerRevealPreviousResult();
1043     }
1044
1045     performSearch(searchQuery)
1046     {
1047         if (!isEmptyObject(this._searchHighlightDOMChanges))
1048             WI.revertDOMChanges(this._searchHighlightDOMChanges);
1049
1050         this._currentSearchQuery = searchQuery;
1051         this._searchHighlightDOMChanges = [];
1052         this._searchMatches = [];
1053         this._selectedSearchMatchIsValid = false;
1054         this._selectedSearchMatch = null;
1055         let numberOfResults = 0;
1056
1057         if (this._currentSearchQuery === "") {
1058             this.element.classList.remove(WI.LogContentView.SearchInProgressStyleClassName);
1059             this.dispatchEventToListeners(WI.ContentView.Event.NumberOfSearchResultsDidChange);
1060             return;
1061         }
1062
1063         this.element.classList.add(WI.LogContentView.SearchInProgressStyleClassName);
1064
1065         let searchRegex = new RegExp(this._currentSearchQuery.escapeForRegExp(), "gi");
1066         this._unfilteredMessageElements().forEach(function(message) {
1067             let matchRanges = [];
1068             let text = message.textContent;
1069             let match = searchRegex.exec(text);
1070             while (match) {
1071                 numberOfResults++;
1072                 matchRanges.push({offset: match.index, length: match[0].length});
1073                 match = searchRegex.exec(text);
1074             }
1075
1076             if (!isEmptyObject(matchRanges))
1077                 this._highlightRanges(message, matchRanges);
1078
1079             let classList = message.classList;
1080             if (!isEmptyObject(matchRanges) || message.__commandView instanceof WI.ConsoleCommandView || message.__message instanceof WI.ConsoleCommandResultMessage)
1081                 classList.remove(WI.LogContentView.FilteredOutBySearchStyleClassName);
1082             else
1083                 classList.add(WI.LogContentView.FilteredOutBySearchStyleClassName);
1084         }, this);
1085
1086         this.dispatchEventToListeners(WI.ContentView.Event.NumberOfSearchResultsDidChange);
1087
1088         this._findBanner.numberOfResults = numberOfResults;
1089
1090         if (!this._selectedSearchMatchIsValid && this._selectedSearchMatch) {
1091             this._selectedSearchMatch.highlight.classList.remove(WI.LogContentView.SelectedStyleClassName);
1092             this._selectedSearchMatch = null;
1093         }
1094     }
1095
1096     searchHidden()
1097     {
1098         this.searchCleared();
1099     }
1100
1101     searchCleared()
1102     {
1103         this.performSearch("");
1104     }
1105
1106     _highlightRanges(message, matchRanges)
1107     {
1108         var highlightedElements = WI.highlightRangesWithStyleClass(message, matchRanges, WI.LogContentView.HighlightedStyleClassName, this._searchHighlightDOMChanges);
1109
1110         console.assert(highlightedElements.length === matchRanges.length);
1111
1112         matchRanges.forEach(function(range, index) {
1113             this._searchMatches.push({message, range, highlight: highlightedElements[index]});
1114
1115             if (this._selectedSearchMatch && !this._selectedSearchMatchIsValid && this._selectedSearchMatch.message === message) {
1116                 this._selectedSearchMatchIsValid = this._rangesOverlap(this._selectedSearchMatch.range, range);
1117                 if (this._selectedSearchMatchIsValid) {
1118                     delete this._selectedSearchMatch;
1119                     this._highlightSearchMatchAtIndex(this._searchMatches.length - 1);
1120                 }
1121             }
1122         }, this);
1123     }
1124
1125     _rangesOverlap(range1, range2)
1126     {
1127         return range1.offset <= range2.offset + range2.length && range2.offset <= range1.offset + range1.length;
1128     }
1129
1130     _highlightSearchMatchAtIndex(index)
1131     {
1132         if (index >= this._searchMatches.length)
1133             index = 0;
1134         else if (index < 0)
1135             index = this._searchMatches.length - 1;
1136
1137         if (this._selectedSearchMatch)
1138             this._selectedSearchMatch.highlight.classList.remove(WI.LogContentView.SelectedStyleClassName);
1139
1140         this._selectedSearchMatch = this._searchMatches[index];
1141         this._selectedSearchMatch.highlight.classList.add(WI.LogContentView.SelectedStyleClassName);
1142
1143         this._ensureMessageIsVisible(this._selectedSearchMatch.message);
1144     }
1145
1146     _provisionalLoadStarted()
1147     {
1148         this._startedProvisionalLoad = true;
1149     }
1150
1151     _reappendProvisionalMessages()
1152     {
1153         if (!this._startedProvisionalLoad)
1154             return;
1155
1156         this._startedProvisionalLoad = false;
1157
1158         for (let provisionalMessage of this._provisionalMessages)
1159             this._logViewController.appendConsoleMessage(provisionalMessage);
1160
1161         this._provisionalMessages = [];
1162     }
1163
1164     _clearProvisionalState()
1165     {
1166         this._startedProvisionalLoad = false;
1167         this._provisionalMessages = [];
1168     }
1169 };
1170
1171 WI.LogContentView.Scopes = {
1172     All: "log-all",
1173     Errors: "log-errors",
1174     Warnings: "log-warnings",
1175     Logs: "log-logs",
1176     Infos: "log-infos",
1177     Debugs: "log-debugs",
1178     AllChannels: "log-all-channels",
1179     Media: "log-media",
1180     WebRTC: "log-webrtc",
1181     MediaSource: "log-mediasource",
1182 };
1183
1184 WI.LogContentView.ItemWrapperStyleClassName = "console-item";
1185 WI.LogContentView.FilteredOutStyleClassName = "filtered-out";
1186 WI.LogContentView.SelectedStyleClassName = "selected";
1187 WI.LogContentView.SearchInProgressStyleClassName = "search-in-progress";
1188 WI.LogContentView.FilteredOutBySearchStyleClassName = "filtered-out-by-search";
1189 WI.LogContentView.HighlightedStyleClassName = "highlighted";