1/*
2 * Copyright (C) 2007, 2008 Apple Inc.  All rights reserved.
3 * Copyright (C) 2009 Joseph Pecoraro
4 *
5 * Redistribution and use in source and binary forms, with or without
6 * modification, are permitted provided that the following conditions
7 * are met:
8 *
9 * 1.  Redistributions of source code must retain the above copyright
10 *     notice, this list of conditions and the following disclaimer.
11 * 2.  Redistributions in binary form must reproduce the above copyright
12 *     notice, this list of conditions and the following disclaimer in the
13 *     documentation and/or other materials provided with the distribution.
14 * 3.  Neither the name of Apple Computer, Inc. ("Apple") nor the names of
15 *     its contributors may be used to endorse or promote products derived
16 *     from this software without specific prior written permission.
17 *
18 * THIS SOFTWARE IS PROVIDED BY APPLE AND ITS CONTRIBUTORS "AS IS" AND ANY
19 * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
20 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
21 * DISCLAIMED. IN NO EVENT SHALL APPLE OR ITS CONTRIBUTORS BE LIABLE FOR ANY
22 * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
23 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
24 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
25 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
26 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
27 * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
28 */
29
30/**
31 * @extends {WebInspector.View}
32 * @implements {WebInspector.Searchable}
33 * @constructor
34 * @param {boolean} hideContextSelector
35 */
36WebInspector.ConsoleView = function(hideContextSelector)
37{
38    WebInspector.View.call(this);
39
40    this.element.id = "console-view";
41    this._visibleMessagesIndices = [];
42    this._urlToMessageCount = {};
43
44    this._clearConsoleButton = new WebInspector.StatusBarButton(WebInspector.UIString("Clear console log."), "clear-status-bar-item");
45    this._clearConsoleButton.addEventListener("click", this._requestClearMessages, this);
46
47    this._frameSelector = new WebInspector.StatusBarComboBox(this._frameChanged.bind(this), "console-context");
48    this._contextSelector = new WebInspector.StatusBarComboBox(this._contextChanged.bind(this), "console-context");
49
50    this._filter = new WebInspector.ConsoleViewFilter();
51    this._filter.addEventListener(WebInspector.ConsoleViewFilter.Events.FilterChanged, this._updateMessageList.bind(this));
52
53    if (hideContextSelector) {
54        this._frameSelector.element.addStyleClass("hidden");
55        this._contextSelector.element.addStyleClass("hidden");
56    }
57
58    this.messagesElement = document.createElement("div");
59    this.messagesElement.id = "console-messages";
60    this.messagesElement.className = "monospace";
61    this.messagesElement.addEventListener("click", this._messagesClicked.bind(this), true);
62    this.element.appendChild(this.messagesElement);
63    this._scrolledToBottom = true;
64
65    this.promptElement = document.createElement("div");
66    this.promptElement.id = "console-prompt";
67    this.promptElement.className = "source-code";
68    this.promptElement.spellcheck = false;
69    this.messagesElement.appendChild(this.promptElement);
70    this.messagesElement.appendChild(document.createElement("br"));
71
72    this.topGroup = new WebInspector.ConsoleGroup(null);
73    this.messagesElement.insertBefore(this.topGroup.element, this.promptElement);
74    this.currentGroup = this.topGroup;
75
76    this._registerShortcuts();
77    this.registerRequiredCSS("textPrompt.css");
78
79    this.messagesElement.addEventListener("contextmenu", this._handleContextMenuEvent.bind(this), false);
80
81    WebInspector.settings.monitoringXHREnabled.addChangeListener(this._monitoringXHREnabledSettingChanged.bind(this));
82
83    WebInspector.console.addEventListener(WebInspector.ConsoleModel.Events.MessageAdded, this._consoleMessageAdded, this);
84    WebInspector.console.addEventListener(WebInspector.ConsoleModel.Events.ConsoleCleared, this._consoleCleared, this);
85
86    this._linkifier = new WebInspector.Linkifier();
87
88    this.prompt = new WebInspector.TextPromptWithHistory(WebInspector.runtimeModel.completionsForTextPrompt.bind(WebInspector.runtimeModel));
89    this.prompt.setSuggestBoxEnabled("generic-suggest");
90    this.prompt.renderAsBlock();
91    this.prompt.attach(this.promptElement);
92    this.prompt.proxyElement.addEventListener("keydown", this._promptKeyDown.bind(this), false);
93    this.prompt.setHistoryData(WebInspector.settings.consoleHistory.get());
94
95    WebInspector.runtimeModel.contextLists().forEach(this._addFrame, this);
96    WebInspector.runtimeModel.addEventListener(WebInspector.RuntimeModel.Events.FrameExecutionContextListAdded, this._frameAdded, this);
97    WebInspector.runtimeModel.addEventListener(WebInspector.RuntimeModel.Events.FrameExecutionContextListRemoved, this._frameRemoved, this);
98
99    this._filterStatusMessageElement = document.createElement("div");
100    this._filterStatusMessageElement.classList.add("console-message");
101    this._filterStatusTextElement = this._filterStatusMessageElement.createChild("span", "console-info");
102    this._filterStatusMessageElement.createTextChild(" ");
103    var resetFiltersLink = this._filterStatusMessageElement.createChild("span", "console-info node-link");
104    resetFiltersLink.textContent = WebInspector.UIString("Show all messages.");
105    resetFiltersLink.addEventListener("click", this._filter.reset.bind(this._filter), true);
106
107    this.messagesElement.insertBefore(this._filterStatusMessageElement, this.topGroup.element);
108
109    this._updateFilterStatus();
110}
111
112WebInspector.ConsoleView.prototype = {
113    get statusBarItems()
114    {
115        return [this._clearConsoleButton.element, this._frameSelector.element, this._contextSelector.element, this._filter.sourceFilterButton.element, this._filter.filterBarElement];
116    },
117
118    /**
119     * @param {WebInspector.Event} event
120     */
121    _frameAdded: function(event)
122    {
123        var contextList = /** @type {WebInspector.FrameExecutionContextList} */ (event.data);
124        this._addFrame(contextList);
125    },
126
127    /**
128     * @param {WebInspector.FrameExecutionContextList} contextList
129     */
130    _addFrame: function(contextList)
131    {
132        var option = this._frameSelector.createOption(contextList.displayName, contextList.url);
133        option._contextList = contextList;
134        contextList._consoleOption = option;
135        contextList.addEventListener(WebInspector.FrameExecutionContextList.EventTypes.ContextsUpdated, this._frameUpdated, this);
136        contextList.addEventListener(WebInspector.FrameExecutionContextList.EventTypes.ContextAdded, this._contextAdded, this);
137        this._frameChanged();
138    },
139
140    /**
141     * @param {WebInspector.Event} event
142     */
143    _frameRemoved: function(event)
144    {
145        var contextList = /** @type {WebInspector.FrameExecutionContextList} */ (event.data);
146        this._frameSelector.removeOption(contextList._consoleOption);
147        this._frameChanged();
148    },
149
150    _frameChanged: function()
151    {
152        var context = this._currentFrame();
153        if (!context) {
154            WebInspector.runtimeModel.setCurrentExecutionContext(null);
155            this._contextSelector.element.addStyleClass("hidden");
156            return;
157        }
158
159        var executionContexts = context.executionContexts();
160        if (executionContexts.length)
161            WebInspector.runtimeModel.setCurrentExecutionContext(executionContexts[0]);
162
163        if (executionContexts.length === 1) {
164            this._contextSelector.element.addStyleClass("hidden");
165            return;
166        }
167        this._contextSelector.element.removeStyleClass("hidden");
168        this._contextSelector.removeOptions();
169        for (var i = 0; i < executionContexts.length; ++i)
170            this._appendContextOption(executionContexts[i]);
171    },
172
173    /**
174     * @param {WebInspector.ExecutionContext} executionContext
175     */
176    _appendContextOption: function(executionContext)
177    {
178        if (!WebInspector.runtimeModel.currentExecutionContext())
179            WebInspector.runtimeModel.setCurrentExecutionContext(executionContext);
180        var option = this._contextSelector.createOption(executionContext.name, executionContext.id);
181        option._executionContext = executionContext;
182    },
183
184    /**
185     * @param {Event} event
186     */
187    _contextChanged: function(event)
188    {
189        var option = this._contextSelector.selectedOption();
190        WebInspector.runtimeModel.setCurrentExecutionContext(option ? option._executionContext : null);
191    },
192
193    /**
194     * @param {WebInspector.Event} event
195     */
196    _frameUpdated: function(event)
197    {
198        var contextList = /** @type {WebInspector.FrameExecutionContextList} */ (event.data);
199        var option = contextList._consoleOption;
200        option.text = contextList.displayName;
201        option.title = contextList.url;
202    },
203
204    /**
205     * @param {WebInspector.Event} event
206     */
207    _contextAdded: function(event)
208    {
209        var contextList = /** @type {WebInspector.FrameExecutionContextList} */ (event.data);
210        if (contextList === this._currentFrame())
211            this._frameChanged();
212    },
213
214    /**
215     * @return {WebInspector.FrameExecutionContextList|undefined}
216     */
217    _currentFrame: function()
218    {
219        var option = this._frameSelector.selectedOption();
220        return option ? option._contextList : undefined;
221    },
222
223    willHide: function()
224    {
225        this.prompt.hideSuggestBox();
226        this.prompt.clearAutoComplete(true);
227    },
228
229    wasShown: function()
230    {
231        if (!this.prompt.isCaretInsidePrompt())
232            this.prompt.moveCaretToEndOfPrompt();
233    },
234
235    afterShow: function()
236    {
237        WebInspector.setCurrentFocusElement(this.promptElement);
238    },
239
240    storeScrollPositions: function()
241    {
242        WebInspector.View.prototype.storeScrollPositions.call(this);
243        this._scrolledToBottom = this.messagesElement.isScrolledToBottom();
244    },
245
246    restoreScrollPositions: function()
247    {
248        if (this._scrolledToBottom)
249            this._immediatelyScrollIntoView();
250        else
251            WebInspector.View.prototype.restoreScrollPositions.call(this);
252    },
253
254    onResize: function()
255    {
256        this.restoreScrollPositions();
257    },
258
259    _isScrollIntoViewScheduled: function()
260    {
261        return !!this._scrollIntoViewTimer;
262    },
263
264    _scheduleScrollIntoView: function()
265    {
266        if (this._scrollIntoViewTimer)
267            return;
268
269        function scrollIntoView()
270        {
271            delete this._scrollIntoViewTimer;
272            this.promptElement.scrollIntoView(true);
273        }
274        this._scrollIntoViewTimer = setTimeout(scrollIntoView.bind(this), 20);
275    },
276
277    _immediatelyScrollIntoView: function()
278    {
279        this.promptElement.scrollIntoView(true);
280        this._cancelScheduledScrollIntoView();
281    },
282
283    _cancelScheduledScrollIntoView: function()
284    {
285        if (!this._isScrollIntoViewScheduled())
286            return;
287
288        clearTimeout(this._scrollIntoViewTimer);
289        delete this._scrollIntoViewTimer;
290    },
291
292    /**
293     * @param {number=} count
294     */
295    _updateFilterStatus: function(count) {
296        count = (typeof count === undefined) ? (WebInspector.console.messages.length - this._visibleMessagesIndices.length) : count;
297        this._filterStatusTextElement.textContent = WebInspector.UIString(count == 1 ? "%d message is hidden by filters." : "%d messages are hidden by filters.", count);
298        this._filterStatusMessageElement.style.display = count ? "" : "none";
299    },
300
301    /**
302     * @param {WebInspector.Event} event
303     */
304    _consoleMessageAdded: function(event)
305    {
306        var message = /** @type {WebInspector.ConsoleMessage} */ (event.data);
307        var index = message.index;
308
309        if (this._urlToMessageCount[message.url])
310            this._urlToMessageCount[message.url]++;
311        else
312            this._urlToMessageCount[message.url] = 1;
313
314        if (this._filter.shouldBeVisible(message))
315            this._showConsoleMessage(index);
316        else
317            this._updateFilterStatus();
318    },
319
320    _showConsoleMessage: function(index)
321    {
322        var message = WebInspector.console.messages[index];
323
324        // this.messagesElement.isScrolledToBottom() is forcing style recalculation.
325        // We just skip it if the scroll action has been scheduled.
326        if (!this._isScrollIntoViewScheduled() && ((message instanceof WebInspector.ConsoleCommandResult) || this.messagesElement.isScrolledToBottom()))
327            this._scheduleScrollIntoView();
328
329        this._visibleMessagesIndices.push(index);
330
331        if (message.type === WebInspector.ConsoleMessage.MessageType.EndGroup) {
332            var parentGroup = this.currentGroup.parentGroup;
333            if (parentGroup)
334                this.currentGroup = parentGroup;
335        } else {
336            if (message.type === WebInspector.ConsoleMessage.MessageType.StartGroup || message.type === WebInspector.ConsoleMessage.MessageType.StartGroupCollapsed) {
337                var group = new WebInspector.ConsoleGroup(this.currentGroup);
338                this.currentGroup.messagesElement.appendChild(group.element);
339                this.currentGroup = group;
340                message.group = group;
341            }
342            this.currentGroup.addMessage(message);
343        }
344
345        if (this._searchRegex && message.matchesRegex(this._searchRegex)) {
346            this._searchResultsIndices.push(index);
347            WebInspector.searchController.updateSearchMatchesCount(this._searchResultsIndices.length, this._searchProvider);
348        }
349    },
350
351    _consoleCleared: function()
352    {
353        this._scrolledToBottom = true;
354        for (var i = 0; i < this._visibleMessagesIndices.length; ++i)
355            WebInspector.console.messages[this._visibleMessagesIndices[i]].willHide();
356        this._visibleMessagesIndices = [];
357        this._searchResultsIndices = [];
358
359        if (this._searchRegex)
360            WebInspector.searchController.updateSearchMatchesCount(0, this._searchProvider);
361
362        this.currentGroup = this.topGroup;
363        this.topGroup.messagesElement.removeChildren();
364
365        this._clearCurrentSearchResultHighlight();
366        this._updateFilterStatus(0);
367
368        this._linkifier.reset();
369    },
370
371    _handleContextMenuEvent: function(event)
372    {
373        if (!window.getSelection().isCollapsed) {
374            // If there is a selection, we want to show our normal context menu
375            // (with Copy, etc.), and not Clear Console.
376            return;
377        }
378
379        if (event.target.enclosingNodeOrSelfWithNodeName("a"))
380            return;
381
382        var contextMenu = new WebInspector.ContextMenu(event);
383
384        function monitoringXHRItemAction()
385        {
386            WebInspector.settings.monitoringXHREnabled.set(!WebInspector.settings.monitoringXHREnabled.get());
387        }
388        contextMenu.appendCheckboxItem(WebInspector.UIString("Log XMLHttpRequests"), monitoringXHRItemAction.bind(this), WebInspector.settings.monitoringXHREnabled.get());
389
390        function preserveLogItemAction()
391        {
392            WebInspector.settings.preserveConsoleLog.set(!WebInspector.settings.preserveConsoleLog.get());
393        }
394        contextMenu.appendCheckboxItem(WebInspector.UIString(WebInspector.useLowerCaseMenuTitles() ? "Preserve log upon navigation" : "Preserve Log upon Navigation"), preserveLogItemAction.bind(this), WebInspector.settings.preserveConsoleLog.get());
395
396        var sourceElement = event.target.enclosingNodeOrSelfWithClass("console-message");
397
398        var filterSubMenu = contextMenu.appendSubMenuItem(WebInspector.UIString("Filter"));
399
400        if (sourceElement && sourceElement.message.url) {
401            var menuTitle = WebInspector.UIString(WebInspector.useLowerCaseMenuTitles() ? "Hide messages from %s" : "Hide Messages from %s", new WebInspector.ParsedURL(sourceElement.message.url).displayName);
402            filterSubMenu.appendItem(menuTitle, this._filter.addMessageURLFilter.bind(this._filter, sourceElement.message.url));
403        }
404
405        filterSubMenu.appendSeparator();
406        var unhideAll = filterSubMenu.appendItem(WebInspector.UIString(WebInspector.useLowerCaseMenuTitles() ? "Unhide all" : "Unhide All"), this._filter.removeMessageURLFilter.bind(this._filter));
407        filterSubMenu.appendSeparator();
408
409        var hasFilters = false;
410
411        for (var url in this._filter.messageURLFilters) {
412            filterSubMenu.appendCheckboxItem(String.sprintf("%s (%d)", new WebInspector.ParsedURL(url).displayName, this._urlToMessageCount[url]), this._filter.removeMessageURLFilter.bind(this._filter, url), true);
413            hasFilters = true;
414        }
415
416        filterSubMenu.setEnabled(hasFilters || (sourceElement && sourceElement.message.url));
417        unhideAll.setEnabled(hasFilters);
418
419        contextMenu.appendSeparator();
420        contextMenu.appendItem(WebInspector.UIString(WebInspector.useLowerCaseMenuTitles() ? "Clear console" : "Clear Console"), this._requestClearMessages.bind(this));
421
422        var request = (sourceElement && sourceElement.message) ? sourceElement.message.request() : null;
423        if (request && request.type === WebInspector.resourceTypes.XHR) {
424            contextMenu.appendSeparator();
425            contextMenu.appendItem(WebInspector.UIString("Replay XHR"), NetworkAgent.replayXHR.bind(null, request.requestId));
426        }
427
428        contextMenu.show();
429    },
430
431    _updateMessageList: function()
432    {
433        var group = this.topGroup;
434        var sourceMessages = WebInspector.console.messages;
435        var visibleMessageIndex = 0;
436        var newVisibleMessages = [];
437
438        if (this._searchRegex)
439            this._searchResultsIndices = [];
440
441        var anchor = null;
442        for (var i = 0; i < sourceMessages.length; ++i) {
443            var sourceMessage = sourceMessages[i];
444            var visibleMessage = WebInspector.console.messages[this._visibleMessagesIndices[visibleMessageIndex]];
445
446            if (visibleMessage === sourceMessage) {
447                if (this._filter.shouldBeVisible(visibleMessage)) {
448                    newVisibleMessages.push(this._visibleMessagesIndices[visibleMessageIndex]);
449
450                    if (this._searchRegex && sourceMessage.matchesRegex(this._searchRegex))
451                        this._searchResultsIndices.push(i);
452
453                    if (sourceMessage.type === WebInspector.ConsoleMessage.MessageType.EndGroup) {
454                        anchor = group.element;
455                        group = group.parentGroup || group;
456                    } else if (sourceMessage.type === WebInspector.ConsoleMessage.MessageType.StartGroup || sourceMessage.type === WebInspector.ConsoleMessage.MessageType.StartGroupCollapsed) {
457                        group = sourceMessage.group;
458                        anchor = group.messagesElement.firstChild;
459                    } else
460                        anchor = visibleMessage.toMessageElement();
461                } else {
462                    visibleMessage.willHide();
463                    visibleMessage.toMessageElement().remove();
464                }
465                ++visibleMessageIndex;
466            } else {
467                if (this._filter.shouldBeVisible(sourceMessage)) {
468
469                    if (this._searchRegex && sourceMessage.matchesRegex(this._searchRegex))
470                        this._searchResultsIndices.push(i);
471
472                    group.addMessage(sourceMessage, anchor ? anchor.nextSibling : group.messagesElement.firstChild);
473                    newVisibleMessages.push(i);
474                    anchor = sourceMessage.toMessageElement();
475                }
476            }
477        }
478
479        if (this._searchRegex)
480            WebInspector.searchController.updateSearchMatchesCount(this._searchResultsIndices.length, this._searchProvider);
481
482        this._visibleMessagesIndices = newVisibleMessages;
483        this._updateFilterStatus();
484    },
485
486    _monitoringXHREnabledSettingChanged: function(event)
487    {
488        ConsoleAgent.setMonitoringXHREnabled(event.data);
489    },
490
491    _messagesClicked: function()
492    {
493        if (!this.prompt.isCaretInsidePrompt() && window.getSelection().isCollapsed)
494            this.prompt.moveCaretToEndOfPrompt();
495    },
496
497    _registerShortcuts: function()
498    {
499        this._shortcuts = {};
500
501        var shortcut = WebInspector.KeyboardShortcut;
502        var section = WebInspector.shortcutsScreen.section(WebInspector.UIString("Console"));
503
504        var shortcutL = shortcut.makeDescriptor("l", WebInspector.KeyboardShortcut.Modifiers.Ctrl);
505        this._shortcuts[shortcutL.key] = this._requestClearMessages.bind(this);
506        var keys = [shortcutL];
507        if (WebInspector.isMac()) {
508            var shortcutK = shortcut.makeDescriptor("k", WebInspector.KeyboardShortcut.Modifiers.Meta);
509            this._shortcuts[shortcutK.key] = this._requestClearMessages.bind(this);
510            keys.unshift(shortcutK);
511        }
512        section.addAlternateKeys(keys, WebInspector.UIString("Clear console"));
513
514        section.addKey(shortcut.makeDescriptor(shortcut.Keys.Tab), WebInspector.UIString("Autocomplete common prefix"));
515        section.addKey(shortcut.makeDescriptor(shortcut.Keys.Right), WebInspector.UIString("Accept suggestion"));
516
517        keys = [
518            shortcut.makeDescriptor(shortcut.Keys.Down),
519            shortcut.makeDescriptor(shortcut.Keys.Up)
520        ];
521        section.addRelatedKeys(keys, WebInspector.UIString("Next/previous line"));
522
523        if (WebInspector.isMac()) {
524            keys = [
525                shortcut.makeDescriptor("N", shortcut.Modifiers.Alt),
526                shortcut.makeDescriptor("P", shortcut.Modifiers.Alt)
527            ];
528            section.addRelatedKeys(keys, WebInspector.UIString("Next/previous command"));
529        }
530
531        section.addKey(shortcut.makeDescriptor(shortcut.Keys.Enter), WebInspector.UIString("Execute command"));
532    },
533
534    _requestClearMessages: function()
535    {
536        WebInspector.console.requestClearMessages();
537    },
538
539    _promptKeyDown: function(event)
540    {
541        if (isEnterKey(event)) {
542            this._enterKeyPressed(event);
543            return;
544        }
545
546        var shortcut = WebInspector.KeyboardShortcut.makeKeyFromEvent(event);
547        var handler = this._shortcuts[shortcut];
548        if (handler) {
549            handler();
550            event.preventDefault();
551        }
552    },
553
554    evaluateUsingTextPrompt: function(expression, showResultOnly)
555    {
556        this._appendCommand(expression, this.prompt.text, false, showResultOnly);
557    },
558
559    _enterKeyPressed: function(event)
560    {
561        if (event.altKey || event.ctrlKey || event.shiftKey)
562            return;
563
564        event.consume(true);
565
566        this.prompt.clearAutoComplete(true);
567
568        var str = this.prompt.text;
569        if (!str.length)
570            return;
571        this._appendCommand(str, "", true, false);
572    },
573
574    _printResult: function(result, wasThrown, originatingCommand)
575    {
576        if (!result)
577            return;
578        var message = new WebInspector.ConsoleCommandResult(result, wasThrown, originatingCommand, this._linkifier);
579        WebInspector.console.addMessage(message);
580    },
581
582    _appendCommand: function(text, newPromptText, useCommandLineAPI, showResultOnly)
583    {
584        if (!showResultOnly) {
585            var commandMessage = new WebInspector.ConsoleCommand(text);
586            WebInspector.console.addMessage(commandMessage);
587        }
588        this.prompt.text = newPromptText;
589
590        function printResult(result, wasThrown)
591        {
592            if (!result)
593                return;
594
595            if (!showResultOnly) {
596                this.prompt.pushHistoryItem(text);
597                WebInspector.settings.consoleHistory.set(this.prompt.historyData.slice(-30));
598            }
599
600            this._printResult(result, wasThrown, commandMessage);
601        }
602        WebInspector.runtimeModel.evaluate(text, "console", useCommandLineAPI, false, false, true, printResult.bind(this));
603
604        WebInspector.userMetrics.ConsoleEvaluated.record();
605    },
606
607    elementsToRestoreScrollPositionsFor: function()
608    {
609        return [this.messagesElement];
610    },
611
612    searchCanceled: function()
613    {
614        this._clearCurrentSearchResultHighlight();
615        delete this._searchProvider;
616        delete this._searchResultsIndices;
617        delete this._searchRegex;
618    },
619
620    canSearchAndReplace: function()
621    {
622        return false;
623    },
624
625    canFilter: function()
626    {
627        return true;
628    },
629
630    /**
631     * @param {string} query
632     * @param {boolean} shouldJump
633     * @param {WebInspector.Searchable=} self
634     */
635    performSearch: function(query, shouldJump, self)
636    {
637        this.searchCanceled();
638        this._searchProvider = self || this;
639        WebInspector.searchController.updateSearchMatchesCount(0, this._searchProvider);
640        this._searchRegex = createPlainTextSearchRegex(query, "gi");
641
642        this._searchResultsIndices = [];
643        for (var i = 0; i < this._visibleMessagesIndices.length; i++) {
644            if (WebInspector.console.messages[this._visibleMessagesIndices[i]].matchesRegex(this._searchRegex))
645                this._searchResultsIndices.push(this._visibleMessagesIndices[i]);
646        }
647        WebInspector.searchController.updateSearchMatchesCount(this._searchResultsIndices.length, this._searchProvider);
648        this._currentSearchResultIndex = -1;
649        if (shouldJump && this._searchResultsIndices.length)
650            this._jumpToSearchResult(0, self);
651    },
652
653    /**
654     * @return {number}
655     */
656    minimalSearchQuerySize: function()
657    {
658        return 0;
659    },
660
661    /**
662     * @param {string} query
663     */
664    performFilter: function(query)
665    {
666        this._filter.performFilter(query);
667    },
668
669    /**
670     * @param {WebInspector.Searchable=} self
671     */
672    jumpToNextSearchResult: function(self)
673    {
674        if (!this._searchResultsIndices || !this._searchResultsIndices.length)
675            return;
676        this._jumpToSearchResult((this._currentSearchResultIndex + 1) % this._searchResultsIndices.length, self);
677    },
678
679    /**
680     * @param {WebInspector.Searchable=} self
681     */
682    jumpToPreviousSearchResult: function(self)
683    {
684        if (!this._searchResultsIndices || !this._searchResultsIndices.length)
685            return;
686        var index = this._currentSearchResultIndex - 1;
687        if (index === -1)
688            index = this._searchResultsIndices.length - 1;
689        this._jumpToSearchResult(index, self);
690    },
691
692    _clearCurrentSearchResultHighlight: function()
693    {
694        if (!this._searchResultsIndices)
695            return;
696        var highlightedMessage = WebInspector.console.messages[this._searchResultsIndices[this._currentSearchResultIndex]];
697        if (highlightedMessage)
698            highlightedMessage.clearHighlight();
699        this._currentSearchResultIndex = -1;
700    },
701
702    _jumpToSearchResult: function(index, self)
703    {
704        this._clearCurrentSearchResultHighlight();
705        this._currentSearchResultIndex = index;
706        WebInspector.searchController.updateCurrentMatchIndex(this._currentSearchResultIndex, this._searchProvider);
707        WebInspector.console.messages[this._searchResultsIndices[index]].highlightSearchResults(this._searchRegex);
708    },
709
710    __proto__: WebInspector.View.prototype
711}
712
713/**
714 * @extends {WebInspector.Object}
715 * @constructor
716 */
717WebInspector.ConsoleViewFilter = function()
718{
719    this._messageURLFilters = WebInspector.settings.messageURLFilters.get();
720    this._messageSourceFilters = WebInspector.settings.messageSourceFilters.get();
721    this._messageLevelFilters = WebInspector.settings.messageLevelFilters.get();
722
723    this._sourceToKeyMap = {};
724
725    for (var key in WebInspector.ConsoleViewFilter._messageSourceGroups) {
726        if (!WebInspector.ConsoleViewFilter._messageSourceGroups[key].sources) {
727            console.assert(!this._otherKey);
728            this._otherKey = key;
729            continue;
730        }
731
732        for (var i = 0; i < WebInspector.ConsoleViewFilter._messageSourceGroups[key].sources.length; ++i)
733            this._sourceToKeyMap[WebInspector.ConsoleViewFilter._messageSourceGroups[key].sources[i]] = key;
734    }
735
736    this._filterChanged = this.dispatchEventToListeners.bind(this, WebInspector.ConsoleViewFilter.Events.FilterChanged);
737
738    WebInspector.settings.messageSourceFilters.addChangeListener(this._updateSourceFilterButton.bind(this));
739    WebInspector.settings.messageLevelFilters.addChangeListener(this._updateLevelFilterBar.bind(this));
740
741    this.sourceFilterButton = new WebInspector.StatusBarButton(WebInspector.UIString("Filter"), "console-filter", 2);
742    this.sourceFilterButton.element.addEventListener("mousedown", this._handleSourceFilterButtonClick.bind(this), false);
743
744    this._filterBarElements = [];
745
746    this.filterBarElement = document.createElement("div");
747    this.filterBarElement.className = "scope-bar status-bar-item";
748
749    this._createLevelFilterBarElement("all", WebInspector.UIString("All"));
750
751    var dividerElement = document.createElement("div");
752    dividerElement.addStyleClass("scope-bar-divider");
753    this.filterBarElement.appendChild(dividerElement);
754
755    this._createLevelFilterBarElement("error", WebInspector.UIString("Errors"));
756    this._createLevelFilterBarElement("warning", WebInspector.UIString("Warnings"));
757    this._createLevelFilterBarElement("log", WebInspector.UIString("Logs"));
758    this._createLevelFilterBarElement("debug", WebInspector.UIString("Debug"));
759
760    this._updateLevelFilterBar();
761    this._updateSourceFilterButton();
762};
763
764WebInspector.ConsoleViewFilter.Events = {
765    FilterChanged: "FilterChanged"
766};
767
768WebInspector.ConsoleViewFilter._messageSourceGroups = {
769    JS: { sources: [WebInspector.ConsoleMessage.MessageSource.JS], title: "JavaScript", styleClass: "filter-type-javascript"},
770    Network: { sources: [WebInspector.ConsoleMessage.MessageSource.Network], title: "Network", styleClass: "filter-type-network"},
771    Logging: { sources: [WebInspector.ConsoleMessage.MessageSource.ConsoleAPI], title: "Logging", styleClass: "filter-type-logging"},
772    CSS: { sources: [WebInspector.ConsoleMessage.MessageSource.CSS], title: "CSS", styleClass: "filter-type-css"},
773    Other: { title: "Other", styleClass: "filter-type-other"}
774};
775
776WebInspector.ConsoleViewFilter.prototype = {
777    /**
778     * @param {string} url
779     */
780    addMessageURLFilter: function(url)
781    {
782        this._messageURLFilters[url] = true;
783        WebInspector.settings.messageURLFilters.set(this._messageURLFilters);
784        this._filterChanged();
785    },
786
787    /**
788     * @param {string} url
789     */
790    removeMessageURLFilter: function(url)
791    {
792        if (!url)
793            this._messageURLFilters = {};
794        else
795            delete this._messageURLFilters[url];
796
797        WebInspector.settings.messageURLFilters.set(this._messageURLFilters);
798        this._filterChanged();
799    },
800
801    /**
802     * @returns {Object}
803     */
804    get messageURLFilters()
805    {
806        return this._messageURLFilters;
807    },
808
809    /**
810     * @param {WebInspector.ConsoleMessage} message
811     * @return {boolean}
812     */
813    shouldBeVisible: function(message)
814    {
815        if ((message.type === WebInspector.ConsoleMessage.MessageType.StartGroup || message.type === WebInspector.ConsoleMessage.MessageType.StartGroupCollapsed || message.type === WebInspector.ConsoleMessage.MessageType.EndGroup))
816            return true;
817
818        if (message.url && this._messageURLFilters[message.url])
819            return false;
820
821        if (message.level && this._messageLevelFilters[message.level])
822            return false;
823
824        if (this._filterRegex) {
825            this._filterRegex.lastIndex = 0;
826            if (!message.matchesRegex(this._filterRegex))
827                return false;
828        }
829
830        // We store group keys, and we have resolved group by message source
831        if (message.source) {
832            if (this._sourceToKeyMap[message.source])
833                return !this._messageSourceFilters[this._sourceToKeyMap[message.source]];
834            else
835                return !this._messageSourceFilters[this._otherKey];
836        }
837
838
839        return true;
840    },
841
842    reset: function()
843    {
844        this._messageSourceFilters = {};
845        WebInspector.settings.messageSourceFilters.set(this._messageSourceFilters);
846        this._messageURLFilters = {};
847        WebInspector.settings.messageURLFilters.set(this._messageURLFilters);
848        this._messageLevelFilters = {};
849        WebInspector.settings.messageLevelFilters.set(this._messageLevelFilters);
850        this._filterChanged();
851    },
852
853    /**
854     * @param {string} query
855     */
856    performFilter: function(query)
857    {
858        if (!query)
859            delete this._filterRegex;
860        else
861            this._filterRegex = createPlainTextSearchRegex(query, "gi");
862
863        this._filterChanged();
864    },
865
866    /**
867     * @param {string} sourceGroup
868     * @private
869     */
870    _toggleMessageSourceFilter: function(sourceGroup)
871    {
872        if (!this._messageSourceFilters[sourceGroup])
873            this._messageSourceFilters[sourceGroup] = true;
874        else
875            delete this._messageSourceFilters[sourceGroup];
876
877        WebInspector.settings.messageSourceFilters.set(this._messageSourceFilters);
878        this._filterChanged();
879    },
880
881    /**
882     * @private
883     */
884    _updateSourceFilterButton: function()
885    {
886        var hasActiveSourceFilter = false;
887        for (var sourceGroup in WebInspector.ConsoleViewFilter._messageSourceGroups) {
888            if (this._messageSourceFilters[sourceGroup]) {
889                hasActiveSourceFilter = true;
890                break;
891            }
892        }
893
894        this.sourceFilterButton.state = hasActiveSourceFilter;
895    },
896
897    /**
898     * @param {Event} event
899     * @returns {WebInspector.ContextMenu}
900     * @private
901     */
902    _createSourceFilterMenu: function(event)
903    {
904        var menu = new WebInspector.ContextMenu(event);
905
906        for (var sourceGroup in WebInspector.ConsoleViewFilter._messageSourceGroups) {
907            var filter = WebInspector.ConsoleViewFilter._messageSourceGroups[sourceGroup];
908
909            menu.appendCheckboxItem(WebInspector.UIString(WebInspector.UIString(filter.title)), this._toggleMessageSourceFilter.bind(this, sourceGroup), !this._messageSourceFilters[sourceGroup]);
910        }
911
912        return menu;
913    },
914
915    /**
916     * @param {string} level
917     * @param {string} label
918     * @private
919     */
920    _createLevelFilterBarElement: function(level, label)
921    {
922        var categoryElement = document.createElement("li");
923        categoryElement.category = level;
924        categoryElement.className = level;
925        categoryElement.textContent = label;
926        categoryElement.addEventListener("click", this._toggleLevelFilter.bind(this, level), false);
927
928        this._filterBarElements[level] = categoryElement;
929        this.filterBarElement.appendChild(categoryElement);
930    },
931
932    /**
933     * @param {string} level
934     * @param {Event} event
935     * @private
936     */
937    _toggleLevelFilter: function(level, event)
938    {
939        var isMac = WebInspector.isMac();
940        var selectMultiple = false;
941        if (isMac && event.metaKey && !event.ctrlKey && !event.altKey && !event.shiftKey)
942            selectMultiple = true;
943        if (!isMac && event.ctrlKey && !event.metaKey && !event.altKey && !event.shiftKey)
944            selectMultiple = true;
945
946        if (level === "all")
947            this._messageLevelFilters = {};
948        else {
949            if (!selectMultiple) {
950                this._messageLevelFilters = {error: true, warning: true, log: true, debug: true};
951                delete this._messageLevelFilters[level];
952            } else {
953                if (this._messageLevelFilters[level])
954                    delete this._messageLevelFilters[level];
955                else
956                    this._messageLevelFilters[level] = true;
957            }
958        }
959
960        WebInspector.settings.messageLevelFilters.set(this._messageLevelFilters);
961        this._filterChanged();
962    },
963
964    /**
965     * @private
966     */
967    _updateLevelFilterBar: function()
968    {
969        var all = !(this._messageLevelFilters["error"] || this._messageLevelFilters["warning"] || this._messageLevelFilters["log"] || this._messageLevelFilters["debug"]);
970
971        this._filterBarElements["all"].enableStyleClass("selected", all);
972
973        this._filterBarElements["error"].enableStyleClass("selected", !all && !this._messageLevelFilters["error"]);
974        this._filterBarElements["warning"].enableStyleClass("selected", !all && !this._messageLevelFilters["warning"]);
975        this._filterBarElements["log"].enableStyleClass("selected", !all && !this._messageLevelFilters["log"]);
976        this._filterBarElements["debug"].enableStyleClass("selected", !all && !this._messageLevelFilters["debug"]);
977    },
978
979    /**
980     * @param {Event} event
981     * @private
982     */
983    _handleSourceFilterButtonClick: function(event)
984    {
985        if (!event.button)
986            this._createSourceFilterMenu(event).showSoftMenu();
987    },
988
989    __proto__: WebInspector.Object.prototype
990};
991
992
993/**
994 * @constructor
995 * @extends WebInspector.ConsoleMessage
996 */
997WebInspector.ConsoleCommand = function(text)
998{
999    this.text = text;
1000}
1001
1002WebInspector.ConsoleCommand.prototype = {
1003    wasShown: function()
1004    {
1005    },
1006
1007    willHide: function()
1008    {
1009    },
1010
1011    clearHighlight: function()
1012    {
1013        var highlightedMessage = this._formattedCommand;
1014        delete this._formattedCommand;
1015        this._formatCommand();
1016        this._element.replaceChild(this._formattedCommand, highlightedMessage);
1017    },
1018
1019    highlightSearchResults: function(regexObject)
1020    {
1021        regexObject.lastIndex = 0;
1022        var match = regexObject.exec(this.text);
1023        var matchRanges = [];
1024        while (match) {
1025            matchRanges.push({ offset: match.index, length: match[0].length });
1026            match = regexObject.exec(this.text);
1027        }
1028        WebInspector.highlightSearchResults(this._formattedCommand, matchRanges);
1029        this._element.scrollIntoViewIfNeeded();
1030    },
1031
1032    matchesRegex: function(regexObject)
1033    {
1034        regexObject.lastIndex = 0;
1035        return regexObject.test(this.text);
1036    },
1037
1038    toMessageElement: function()
1039    {
1040        if (!this._element) {
1041            this._element = document.createElement("div");
1042            this._element.command = this;
1043            this._element.className = "console-user-command";
1044
1045            this._formatCommand();
1046            this._element.appendChild(this._formattedCommand);
1047        }
1048        return this._element;
1049    },
1050
1051    _formatCommand: function()
1052    {
1053        this._formattedCommand = document.createElement("span");
1054        this._formattedCommand.className = "console-message-text source-code";
1055        this._formattedCommand.textContent = this.text;
1056    },
1057
1058    __proto__: WebInspector.ConsoleMessage.prototype
1059}
1060
1061/**
1062 * @extends {WebInspector.ConsoleMessageImpl}
1063 * @constructor
1064 * @param {boolean} result
1065 * @param {boolean} wasThrown
1066 * @param {WebInspector.ConsoleCommand} originatingCommand
1067 * @param {WebInspector.Linkifier} linkifier
1068 */
1069WebInspector.ConsoleCommandResult = function(result, wasThrown, originatingCommand, linkifier)
1070{
1071    var level = (wasThrown ? WebInspector.ConsoleMessage.MessageLevel.Error : WebInspector.ConsoleMessage.MessageLevel.Log);
1072    this.originatingCommand = originatingCommand;
1073    WebInspector.ConsoleMessageImpl.call(this, WebInspector.ConsoleMessage.MessageSource.JS, level, "", linkifier, WebInspector.ConsoleMessage.MessageType.Result, undefined, undefined, undefined, undefined, [result]);
1074}
1075
1076WebInspector.ConsoleCommandResult.prototype = {
1077    /**
1078     * @override
1079     * @param {WebInspector.RemoteObject} array
1080     * @return {boolean}
1081     */
1082    useArrayPreviewInFormatter: function(array)
1083    {
1084        return false;
1085    },
1086
1087    toMessageElement: function()
1088    {
1089        var element = WebInspector.ConsoleMessageImpl.prototype.toMessageElement.call(this);
1090        element.addStyleClass("console-user-command-result");
1091        return element;
1092    },
1093
1094    __proto__: WebInspector.ConsoleMessageImpl.prototype
1095}
1096
1097/**
1098 * @constructor
1099 */
1100WebInspector.ConsoleGroup = function(parentGroup)
1101{
1102    this.parentGroup = parentGroup;
1103
1104    var element = document.createElement("div");
1105    element.className = "console-group";
1106    element.group = this;
1107    this.element = element;
1108
1109    if (parentGroup) {
1110        var bracketElement = document.createElement("div");
1111        bracketElement.className = "console-group-bracket";
1112        element.appendChild(bracketElement);
1113    }
1114
1115    var messagesElement = document.createElement("div");
1116    messagesElement.className = "console-group-messages";
1117    element.appendChild(messagesElement);
1118    this.messagesElement = messagesElement;
1119}
1120
1121WebInspector.ConsoleGroup.prototype = {
1122    /**
1123     * @param {WebInspector.ConsoleMessage} message
1124     * @param {Node=} node
1125     */
1126    addMessage: function(message, node)
1127    {
1128        var element = message.toMessageElement();
1129
1130        if (message.type === WebInspector.ConsoleMessage.MessageType.StartGroup || message.type === WebInspector.ConsoleMessage.MessageType.StartGroupCollapsed) {
1131            this.messagesElement.parentNode.insertBefore(element, this.messagesElement);
1132            element.addEventListener("click", this._titleClicked.bind(this), false);
1133            var groupElement = element.enclosingNodeOrSelfWithClass("console-group");
1134            if (groupElement && message.type === WebInspector.ConsoleMessage.MessageType.StartGroupCollapsed)
1135                groupElement.addStyleClass("collapsed");
1136        } else {
1137            this.messagesElement.insertBefore(element, node || null);
1138            message.wasShown();
1139        }
1140
1141        if (element.previousSibling && message.originatingCommand && element.previousSibling.command === message.originatingCommand)
1142            element.previousSibling.addStyleClass("console-adjacent-user-command-result");
1143    },
1144
1145    _titleClicked: function(event)
1146    {
1147        var groupTitleElement = event.target.enclosingNodeOrSelfWithClass("console-group-title");
1148        if (groupTitleElement) {
1149            var groupElement = groupTitleElement.enclosingNodeOrSelfWithClass("console-group");
1150            if (groupElement)
1151                if (groupElement.hasStyleClass("collapsed"))
1152                    groupElement.removeStyleClass("collapsed");
1153                else
1154                    groupElement.addStyleClass("collapsed");
1155            groupTitleElement.scrollIntoViewIfNeeded(true);
1156        }
1157
1158        event.consume(true);
1159    }
1160}
1161
1162/**
1163 * @type {?WebInspector.ConsoleView}
1164 */
1165WebInspector.consoleView = null;
1166
1167WebInspector.ConsoleMessage.create = function(source, level, message, type, url, line, column, repeatCount, parameters, stackTrace, requestId, isOutdated)
1168{
1169    return new WebInspector.ConsoleMessageImpl(source, level, message, WebInspector.consoleView._linkifier, type, url, line, column, repeatCount, parameters, stackTrace, requestId, isOutdated);
1170}
1171