chrome-devtools-frontend
Version:
Chrome DevTools UI
1,159 lines (1,038 loc) ⢠74.5 kB
text/typescript
// Copyright 2020 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
/*
* Copyright (C) 2007, 2008 Apple Inc. All rights reserved.
* Copyright (C) 2009 Joseph Pecoraro
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
*
* 1. Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
* 3. Neither the name of Apple Computer, Inc. ("Apple") nor the names of
* its contributors may be used to endorse or promote products derived
* from this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY APPLE AND ITS CONTRIBUTORS "AS IS" AND ANY
* EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL APPLE OR ITS CONTRIBUTORS BE LIABLE FOR ANY
* DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
* ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
* THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
import * as Common from '../../core/common/common.js';
import * as Host from '../../core/host/host.js';
import * as i18n from '../../core/i18n/i18n.js';
import * as Platform from '../../core/platform/platform.js';
import * as Root from '../../core/root/root.js';
import * as SDK from '../../core/sdk/sdk.js';
import * as Protocol from '../../generated/protocol.js';
import * as Bindings from '../../models/bindings/bindings.js';
import * as IssuesManager from '../../models/issues_manager/issues_manager.js';
import * as Logs from '../../models/logs/logs.js';
import * as TextUtils from '../../models/text_utils/text_utils.js';
import * as CodeHighlighter from '../../ui/components/code_highlighter/code_highlighter.js';
import * as IssueCounter from '../../ui/components/issue_counter/issue_counter.js';
// eslint-disable-next-line rulesdir/es_modules_import
import objectValueStyles from '../../ui/legacy/components/object_ui/objectValue.css.js';
import * as Components from '../../ui/legacy/components/utils/utils.js';
import * as UI from '../../ui/legacy/legacy.js';
import {ConsoleContextSelector} from './ConsoleContextSelector.js';
import consoleViewStyles from './consoleView.css.js';
import {ConsoleFilter, FilterType, type LevelsMask} from './ConsoleFilter.js';
import {ConsolePinPane} from './ConsolePinPane.js';
import {ConsolePrompt, Events as ConsolePromptEvents} from './ConsolePrompt.js';
import {ConsoleSidebar, Events} from './ConsoleSidebar.js';
import {
ConsoleCommand,
ConsoleCommandResult,
ConsoleGroupViewMessage,
ConsoleTableMessageView,
ConsoleViewMessage,
getMessageForElement,
MaxLengthForLinks,
} from './ConsoleViewMessage.js';
import {ConsoleViewport, type ConsoleViewportElement, type ConsoleViewportProvider} from './ConsoleViewport.js';
const UIStrings = {
/**
*@description Label for button which links to Issues tab, specifying how many issues there are.
*/
issuesWithColon: '{n, plural, =0 {No Issues} =1 {# Issue:} other {# Issues:}}',
/**
*@description Text for the tooltip of the issue counter toolbar item
*/
issueToolbarTooltipGeneral: 'Some problems no longer generate console messages, but are surfaced in the issues tab.',
/**
* @description Text for the tooltip of the issue counter toolbar item. The placeholder indicates how many issues
* there are in the Issues tab broken down by kind.
* @example {1 page error, 2 breaking changes} issueEnumeration
*/
issueToolbarClickToView: 'Click to view {issueEnumeration}',
/**
* @description Text for the tooltip of the issue counter toolbar item. The placeholder indicates how many issues
* there are in the Issues tab broken down by kind.
*/
issueToolbarClickToGoToTheIssuesTab: 'Click to go to the issues tab',
/**
*@description Text in Console View of the Console panel
*/
findStringInLogs: 'Find string in logs',
/**
*@description Tooltip text that appears when hovering over the largeicon settings gear in show settings pane setting in console view of the console panel
*/
consoleSettings: 'Console settings',
/**
*@description Title of a setting under the Console category that can be invoked through the Command Menu
*/
groupSimilarMessagesInConsole: 'Group similar messages in console',
/**
*@description Title of a setting under the Console category that can be invoked through the Command Menu
*/
showCorsErrorsInConsole: 'Show `CORS` errors in console',
/**
* @description Tooltip for the the console sidebar toggle in the Console panel. Command to
* open/show the sidebar.
*/
showConsoleSidebar: 'Show console sidebar',
/**
* @description Tooltip for the the console sidebar toggle in the Console panel. Command to
* open/show the sidebar.
*/
hideConsoleSidebar: 'Hide console sidebar',
/**
* @description Screen reader announcement when the sidebar is shown in the Console panel.
*/
consoleSidebarShown: 'Console sidebar shown',
/**
* @description Screen reader announcement when the sidebar is hidden in the Console panel.
*/
consoleSidebarHidden: 'Console sidebar hidden',
/**
*@description Tooltip text that appears on the setting to preserve log when hovering over the item
*/
doNotClearLogOnPageReload: 'Do not clear log on page reload / navigation',
/**
*@description Text to preserve the log after refreshing
*/
preserveLog: 'Preserve log',
/**
*@description Text in Console View of the Console panel
*/
hideNetwork: 'Hide network',
/**
*@description Tooltip text that appears on the setting when hovering over it in Console View of the Console panel
*/
onlyShowMessagesFromTheCurrentContext:
'Only show messages from the current context (`top`, `iframe`, `worker`, extension)',
/**
*@description Alternative title text of a setting in Console View of the Console panel
*/
selectedContextOnly: 'Selected context only',
/**
*@description Description of a setting that controls whether XMLHttpRequests are logged in the console.
*/
logXMLHttpRequests: 'Log XMLHttpRequests',
/**
*@description Tooltip text that appears on the setting when hovering over it in Console View of the Console panel
*/
eagerlyEvaluateTextInThePrompt: 'Eagerly evaluate text in the prompt',
/**
*@description Description of a setting that controls whether text typed in the console should be autocompleted from commands executed in the local console history.
*/
autocompleteFromHistory: 'Autocomplete from history',
/**
*@description Description of a setting that controls whether user activation is triggered by evaluation'.
*/
treatEvaluationAsUserActivation: 'Treat evaluation as user activation',
/**
* @description Text in Console View of the Console panel, indicating that a number of console
* messages have been hidden.
*/
sHidden: '{n, plural, =1 {# hidden} other {# hidden}}',
/**
*@description Alert message for screen readers when the console is cleared
*/
consoleCleared: 'Console cleared',
/**
*@description Text in Console View of the Console panel
*@example {index.js} PH1
*/
hideMessagesFromS: 'Hide messages from {PH1}',
/**
*@description Text to save content as a specific file type
*/
saveAs: 'Save as...',
/**
*@description A context menu item in the Console View of the Console panel
*/
copyVisibleStyledSelection: 'Copy visible styled selection',
/**
*@description Text to replay an XHR request
*/
replayXhr: 'Replay XHR',
/**
*@description Text to indicate DevTools is writing to a file
*/
writingFile: 'Writing fileā¦',
/**
*@description Text to indicate the searching is in progress
*/
searching: 'Searchingā¦',
/**
*@description Text to filter result items
*/
filter: 'Filter',
/**
*@description Text in Console View of the Console panel
*/
egEventdCdnUrlacom: 'e.g. `/event\d/ -cdn url:a.com`',
/**
*@description Sdk console message message level verbose of level Labels in Console View of the Console panel
*/
verbose: 'Verbose',
/**
*@description Sdk console message message level info of level Labels in Console View of the Console panel
*/
info: 'Info',
/**
*@description Sdk console message message level warning of level Labels in Console View of the Console panel
*/
warnings: 'Warnings',
/**
*@description Text for errors
*/
errors: 'Errors',
/**
*@description Text in Console View of the Console panel
*/
logLevels: 'Log levels',
/**
*@description Title text of a setting in Console View of the Console panel
*/
overriddenByFilterSidebar: 'Overridden by filter sidebar',
/**
*@description Text in Console View of the Console panel
*/
customLevels: 'Custom levels',
/**
*@description Text in Console View of the Console panel
*@example {Warnings} PH1
*/
sOnly: '{PH1} only',
/**
*@description Text in Console View of the Console panel
*/
allLevels: 'All levels',
/**
*@description Text in Console View of the Console panel
*/
defaultLevels: 'Default levels',
/**
*@description Text in Console View of the Console panel
*/
hideAll: 'Hide all',
/**
*@description Title of level menu button in console view of the console panel
*@example {All levels} PH1
*/
logLevelS: 'Log level: {PH1}',
/**
*@description A context menu item in the Console View of the Console panel
*/
default: 'Default',
/**
*@description Text summary to indicate total number of messages in console for accessibility/screen readers.
*@example {5} PH1
*/
filteredMessagesInConsole: '{PH1} messages in console',
/**
*@description An error message showed when console paste is blocked.
*/
consolePasteBlocked:
'Pasting code is blocked on this page. Pasting code into devtools can allow attackers to take over your account.',
};
const str_ = i18n.i18n.registerUIStrings('panels/console/ConsoleView.ts', UIStrings);
const i18nString = i18n.i18n.getLocalizedString.bind(undefined, str_);
let consoleViewInstance: ConsoleView;
export class ConsoleView extends UI.Widget.VBox implements
UI.SearchableView.Searchable, ConsoleViewportProvider,
SDK.TargetManager.SDKModelObserver<SDK.ConsoleModel.ConsoleModel> {
private readonly searchableViewInternal: UI.SearchableView.SearchableView;
private readonly sidebar: ConsoleSidebar;
private isSidebarOpen: boolean;
private filter: ConsoleViewFilter;
private readonly consoleToolbarContainer: HTMLElement;
private readonly splitWidget: UI.SplitWidget.SplitWidget;
private readonly contentsElement: UI.Widget.WidgetElement;
private visibleViewMessages: ConsoleViewMessage[];
private hiddenByFilterCount: number;
private shouldBeHiddenCache: Set<ConsoleViewMessage>;
private lastShownHiddenByFilterCount!: number;
private currentMatchRangeIndex!: number;
private searchRegex!: RegExp|null;
private groupableMessages: Map<string, ConsoleViewMessage[]>;
private readonly groupableMessageTitle: Map<string, ConsoleViewMessage>;
private readonly shortcuts: Map<number, () => void>;
private regexMatchRanges: RegexMatchRange[];
private readonly consoleContextSelector: ConsoleContextSelector;
private readonly filterStatusText: UI.Toolbar.ToolbarText;
private readonly showSettingsPaneSetting: Common.Settings.Setting<boolean>;
private readonly showSettingsPaneButton: UI.Toolbar.ToolbarSettingToggle;
private readonly progressToolbarItem: UI.Toolbar.ToolbarItem;
private readonly groupSimilarSetting: Common.Settings.Setting<boolean>;
private readonly showCorsErrorsSetting: Common.Settings.Setting<boolean>;
private readonly timestampsSetting: Common.Settings.Setting<unknown>;
private readonly consoleHistoryAutocompleteSetting: Common.Settings.Setting<boolean>;
readonly pinPane: ConsolePinPane;
private viewport: ConsoleViewport;
private messagesElement: HTMLElement;
private messagesCountElement: HTMLElement;
private viewportThrottler: Common.Throttler.Throttler;
private pendingBatchResize: boolean;
private readonly onMessageResizedBound: (e: Common.EventTarget.EventTargetEvent<UI.TreeOutline.TreeElement>) => void;
private readonly promptElement: HTMLElement;
private readonly linkifier: Components.Linkifier.Linkifier;
private consoleMessages: ConsoleViewMessage[];
private consoleGroupStarts: ConsoleGroupViewMessage[];
private prompt: ConsolePrompt;
private immediatelyFilterMessagesForTest?: boolean;
private maybeDirtyWhileMuted?: boolean;
private scheduledRefreshPromiseForTest?: Promise<void>;
private needsFullUpdate?: boolean;
private buildHiddenCacheTimeout?: number;
private searchShouldJumpBackwards?: boolean;
private searchProgressIndicator?: UI.ProgressIndicator.ProgressIndicator;
private innerSearchTimeoutId?: number;
private muteViewportUpdates?: boolean;
private waitForScrollTimeout?: number;
private issueCounter: IssueCounter.IssueCounter.IssueCounter;
private pendingSidebarMessages: ConsoleViewMessage[] = [];
private userHasOpenedSidebarAtLeastOnce = false;
private issueToolbarThrottle: Common.Throttler.Throttler;
private requestResolver = new Logs.RequestResolver.RequestResolver();
private issueResolver = new IssuesManager.IssueResolver.IssueResolver();
constructor(viewportThrottlerTimeout: number) {
super();
this.setMinimumSize(0, 35);
this.searchableViewInternal = new UI.SearchableView.SearchableView(this, null);
this.searchableViewInternal.element.classList.add('console-searchable-view');
this.searchableViewInternal.setPlaceholder(i18nString(UIStrings.findStringInLogs));
this.searchableViewInternal.setMinimalSearchQuerySize(0);
this.sidebar = new ConsoleSidebar();
this.sidebar.addEventListener(Events.FilterSelected, this.onFilterChanged.bind(this));
this.isSidebarOpen = false;
this.filter = new ConsoleViewFilter(this.onFilterChanged.bind(this));
this.consoleToolbarContainer = this.element.createChild('div', 'console-toolbar-container');
this.splitWidget = new UI.SplitWidget.SplitWidget(
true /* isVertical */, false /* secondIsSidebar */, 'console.sidebar.width', 100);
this.splitWidget.setMainWidget(this.searchableViewInternal);
this.splitWidget.setSidebarWidget(this.sidebar);
this.splitWidget.show(this.element);
this.splitWidget.hideSidebar();
this.splitWidget.enableShowModeSaving();
this.isSidebarOpen = this.splitWidget.showMode() === UI.SplitWidget.ShowMode.Both;
this.filter.setLevelMenuOverridden(this.isSidebarOpen);
this.splitWidget.addEventListener(UI.SplitWidget.Events.ShowModeChanged, event => {
this.isSidebarOpen = event.data === UI.SplitWidget.ShowMode.Both;
if (this.isSidebarOpen) {
if (!this.userHasOpenedSidebarAtLeastOnce) {
/**
* We only want to know if the user opens the sidebar once, not how
* many times in a given session they might open and close it, hence
* the userHasOpenedSidebarAtLeastOnce variable to track this.
*/
Host.userMetrics.actionTaken(Host.UserMetrics.Action.ConsoleSidebarOpened);
this.userHasOpenedSidebarAtLeastOnce = true;
}
// If the user has now opened the sidebar, we need to update it, so send
// through all the pending messages.
this.pendingSidebarMessages.forEach(message => {
this.sidebar.onMessageAdded(message);
});
this.pendingSidebarMessages = [];
}
this.filter.setLevelMenuOverridden(this.isSidebarOpen);
this.onFilterChanged();
});
this.contentsElement = this.searchableViewInternal.element;
this.element.classList.add('console-view');
this.visibleViewMessages = [];
this.hiddenByFilterCount = 0;
this.shouldBeHiddenCache = new Set();
this.groupableMessages = new Map();
this.groupableMessageTitle = new Map();
this.shortcuts = new Map();
this.regexMatchRanges = [];
this.consoleContextSelector = new ConsoleContextSelector();
this.filterStatusText = new UI.Toolbar.ToolbarText();
this.filterStatusText.element.classList.add('dimmed');
this.showSettingsPaneSetting =
Common.Settings.Settings.instance().createSetting('consoleShowSettingsToolbar', false);
this.showSettingsPaneButton = new UI.Toolbar.ToolbarSettingToggle(
this.showSettingsPaneSetting, 'gear', i18nString(UIStrings.consoleSettings), 'gear-filled');
this.progressToolbarItem = new UI.Toolbar.ToolbarItem(document.createElement('div'));
this.groupSimilarSetting = Common.Settings.Settings.instance().moduleSetting('consoleGroupSimilar');
this.groupSimilarSetting.addChangeListener(() => this.updateMessageList());
this.showCorsErrorsSetting = Common.Settings.Settings.instance().moduleSetting('consoleShowsCorsErrors');
this.showCorsErrorsSetting.addChangeListener(() => this.updateMessageList());
const toolbar = new UI.Toolbar.Toolbar('console-main-toolbar', this.consoleToolbarContainer);
toolbar.makeWrappable(true);
const rightToolbar = new UI.Toolbar.Toolbar('', this.consoleToolbarContainer);
toolbar.appendToolbarItem(this.splitWidget.createShowHideSidebarButton(
i18nString(UIStrings.showConsoleSidebar), i18nString(UIStrings.hideConsoleSidebar),
i18nString(UIStrings.consoleSidebarShown), i18nString(UIStrings.consoleSidebarHidden)));
toolbar.appendToolbarItem(UI.Toolbar.Toolbar.createActionButton(
(UI.ActionRegistry.ActionRegistry.instance().action('console.clear') as UI.ActionRegistration.Action)));
toolbar.appendSeparator();
toolbar.appendToolbarItem(this.consoleContextSelector.toolbarItem());
toolbar.appendSeparator();
const liveExpressionButton = UI.Toolbar.Toolbar.createActionButton(
(UI.ActionRegistry.ActionRegistry.instance().action('console.create-pin') as UI.ActionRegistration.Action));
toolbar.appendToolbarItem(liveExpressionButton);
toolbar.appendSeparator();
toolbar.appendToolbarItem(this.filter.textFilterUI);
toolbar.appendToolbarItem(this.filter.levelMenuButton);
toolbar.appendToolbarItem(this.progressToolbarItem);
toolbar.appendSeparator();
this.issueCounter = new IssueCounter.IssueCounter.IssueCounter();
this.issueCounter.id = 'console-issues-counter';
const issuesToolbarItem = new UI.Toolbar.ToolbarItem(this.issueCounter);
this.issueCounter.data = {
clickHandler: (): void => {
Host.userMetrics.issuesPanelOpenedFrom(Host.UserMetrics.IssueOpener.StatusBarIssuesCounter);
void UI.ViewManager.ViewManager.instance().showView('issues-pane');
},
issuesManager: IssuesManager.IssuesManager.IssuesManager.instance(),
accessibleName: i18nString(UIStrings.issueToolbarTooltipGeneral),
displayMode: IssueCounter.IssueCounter.DisplayMode.OmitEmpty,
};
toolbar.appendToolbarItem(issuesToolbarItem);
rightToolbar.appendSeparator();
rightToolbar.appendToolbarItem(this.filterStatusText);
rightToolbar.appendToolbarItem(this.showSettingsPaneButton);
const monitoringXHREnabledSetting = Common.Settings.Settings.instance().moduleSetting('monitoringXHREnabled');
this.timestampsSetting = Common.Settings.Settings.instance().moduleSetting('consoleTimestampsEnabled');
this.consoleHistoryAutocompleteSetting =
Common.Settings.Settings.instance().moduleSetting('consoleHistoryAutocomplete');
const settingsPane = new UI.Widget.HBox();
settingsPane.show(this.contentsElement);
settingsPane.element.classList.add('console-settings-pane');
UI.ARIAUtils.setAccessibleName(settingsPane.element, i18nString(UIStrings.consoleSettings));
UI.ARIAUtils.markAsGroup(settingsPane.element);
const settingsToolbarLeft = new UI.Toolbar.Toolbar('', settingsPane.element);
settingsToolbarLeft.makeVertical();
ConsoleView.appendSettingsCheckboxToToolbar(
settingsToolbarLeft, this.filter.hideNetworkMessagesSetting, this.filter.hideNetworkMessagesSetting.title(),
i18nString(UIStrings.hideNetwork));
ConsoleView.appendSettingsCheckboxToToolbar(
settingsToolbarLeft, 'preserveConsoleLog', i18nString(UIStrings.doNotClearLogOnPageReload),
i18nString(UIStrings.preserveLog));
ConsoleView.appendSettingsCheckboxToToolbar(
settingsToolbarLeft, this.filter.filterByExecutionContextSetting,
i18nString(UIStrings.onlyShowMessagesFromTheCurrentContext), i18nString(UIStrings.selectedContextOnly));
ConsoleView.appendSettingsCheckboxToToolbar(
settingsToolbarLeft, this.groupSimilarSetting, i18nString(UIStrings.groupSimilarMessagesInConsole));
ConsoleView.appendSettingsCheckboxToToolbar(
settingsToolbarLeft, this.showCorsErrorsSetting, i18nString(UIStrings.showCorsErrorsInConsole));
const settingsToolbarRight = new UI.Toolbar.Toolbar('', settingsPane.element);
settingsToolbarRight.makeVertical();
ConsoleView.appendSettingsCheckboxToToolbar(
settingsToolbarRight, monitoringXHREnabledSetting, i18nString(UIStrings.logXMLHttpRequests));
ConsoleView.appendSettingsCheckboxToToolbar(
settingsToolbarRight, 'consoleEagerEval', i18nString(UIStrings.eagerlyEvaluateTextInThePrompt));
ConsoleView.appendSettingsCheckboxToToolbar(
settingsToolbarRight, this.consoleHistoryAutocompleteSetting, i18nString(UIStrings.autocompleteFromHistory));
ConsoleView.appendSettingsCheckboxToToolbar(
settingsToolbarRight, 'consoleUserActivationEval', i18nString(UIStrings.treatEvaluationAsUserActivation));
if (!this.showSettingsPaneSetting.get()) {
settingsPane.element.classList.add('hidden');
}
this.showSettingsPaneSetting.addChangeListener(
() => settingsPane.element.classList.toggle('hidden', !this.showSettingsPaneSetting.get()));
this.pinPane = new ConsolePinPane(liveExpressionButton, () => this.prompt.focus());
this.pinPane.element.classList.add('console-view-pinpane');
this.pinPane.show(this.contentsElement);
this.viewport = new ConsoleViewport(this);
this.viewport.setStickToBottom(true);
this.viewport.contentElement().classList.add('console-group', 'console-group-messages');
this.contentsElement.appendChild(this.viewport.element);
this.messagesElement = this.viewport.element;
this.messagesElement.id = 'console-messages';
this.messagesElement.classList.add('monospace');
this.messagesElement.addEventListener('click', this.messagesClicked.bind(this), false);
['paste', 'clipboard-paste', 'drop'].forEach(type => {
this.messagesElement.addEventListener(type, this.messagesPasted.bind(this), true);
});
this.messagesCountElement = this.consoleToolbarContainer.createChild('div', 'message-count');
UI.ARIAUtils.markAsPoliteLiveRegion(this.messagesCountElement, false);
this.viewportThrottler = new Common.Throttler.Throttler(viewportThrottlerTimeout);
this.pendingBatchResize = false;
this.onMessageResizedBound = (e: Common.EventTarget.EventTargetEvent<UI.TreeOutline.TreeElement>): void => {
void this.onMessageResized(e);
};
this.promptElement = this.messagesElement.createChild('div', 'source-code');
this.promptElement.id = 'console-prompt';
// FIXME: This is a workaround for the selection machinery bug. See crbug.com/410899
const selectAllFixer = this.messagesElement.createChild('div', 'console-view-fix-select-all');
selectAllFixer.textContent = '.';
UI.ARIAUtils.markAsHidden(selectAllFixer);
this.registerShortcuts();
this.messagesElement.addEventListener('contextmenu', this.handleContextMenuEvent.bind(this), false);
// Filters need to be re-applied to a console message when the message's live location changes.
// All relevant live locations are created by the same linkifier, so it is enough to subscribe to
// the linkifiers live location change event.
const throttler = new Common.Throttler.Throttler(100);
const refilterMessages = (): Promise<void> => throttler.schedule(async () => this.onFilterChanged());
this.linkifier =
new Components.Linkifier.Linkifier(MaxLengthForLinks, /* useLinkDecorator */ undefined, refilterMessages);
this.consoleMessages = [];
this.consoleGroupStarts = [];
this.prompt = new ConsolePrompt();
this.prompt.show(this.promptElement);
this.prompt.element.addEventListener('keydown', this.promptKeyDown.bind(this), true);
this.prompt.addEventListener(ConsolePromptEvents.TextChanged, this.promptTextChanged, this);
this.messagesElement.addEventListener('keydown', this.messagesKeyDown.bind(this), false);
this.prompt.element.addEventListener('focusin', () => {
if (this.isScrolledToBottom()) {
this.viewport.setStickToBottom(true);
}
});
this.consoleHistoryAutocompleteSetting.addChangeListener(this.consoleHistoryAutocompleteChanged, this);
this.consoleHistoryAutocompleteChanged();
this.updateFilterStatus();
this.timestampsSetting.addChangeListener(this.consoleTimestampsSettingChanged, this);
this.registerWithMessageSink();
UI.Context.Context.instance().addFlavorChangeListener(
SDK.RuntimeModel.ExecutionContext, this.executionContextChanged, this);
this.messagesElement.addEventListener(
'mousedown', (event: Event) => this.updateStickToBottomOnPointerDown((event as MouseEvent).button === 2),
false);
this.messagesElement.addEventListener('mouseup', this.updateStickToBottomOnPointerUp.bind(this), false);
this.messagesElement.addEventListener('mouseleave', this.updateStickToBottomOnPointerUp.bind(this), false);
this.messagesElement.addEventListener('wheel', this.updateStickToBottomOnWheel.bind(this), false);
this.messagesElement.addEventListener('touchstart', this.updateStickToBottomOnPointerDown.bind(this, false), false);
this.messagesElement.addEventListener('touchend', this.updateStickToBottomOnPointerUp.bind(this), false);
this.messagesElement.addEventListener('touchcancel', this.updateStickToBottomOnPointerUp.bind(this), false);
SDK.TargetManager.TargetManager.instance().addModelListener(
SDK.ConsoleModel.ConsoleModel, SDK.ConsoleModel.Events.ConsoleCleared, this.consoleCleared, this,
{scoped: true});
SDK.TargetManager.TargetManager.instance().addModelListener(
SDK.ConsoleModel.ConsoleModel, SDK.ConsoleModel.Events.MessageAdded, this.onConsoleMessageAdded, this,
{scoped: true});
SDK.TargetManager.TargetManager.instance().addModelListener(
SDK.ConsoleModel.ConsoleModel, SDK.ConsoleModel.Events.MessageUpdated, this.onConsoleMessageUpdated, this,
{scoped: true});
SDK.TargetManager.TargetManager.instance().addModelListener(
SDK.ConsoleModel.ConsoleModel, SDK.ConsoleModel.Events.CommandEvaluated, this.commandEvaluated, this,
{scoped: true});
SDK.TargetManager.TargetManager.instance().observeModels(SDK.ConsoleModel.ConsoleModel, this, {scoped: true});
const issuesManager = IssuesManager.IssuesManager.IssuesManager.instance();
this.issueToolbarThrottle = new Common.Throttler.Throttler(100);
issuesManager.addEventListener(
IssuesManager.IssuesManager.Events.IssuesCountUpdated,
() => this.issueToolbarThrottle.schedule(async () => this.updateIssuesToolbarItem()), this);
}
static appendSettingsCheckboxToToolbar(
toolbar: UI.Toolbar.Toolbar, settingOrSetingName: Common.Settings.Setting<boolean>|string, title: string,
alternateTitle?: string): UI.Toolbar.ToolbarSettingCheckbox {
let setting: Common.Settings.Setting<boolean>;
if (typeof settingOrSetingName === 'string') {
setting = Common.Settings.Settings.instance().moduleSetting(settingOrSetingName);
} else {
setting = settingOrSetingName;
}
const checkbox = new UI.Toolbar.ToolbarSettingCheckbox(setting, title, alternateTitle);
toolbar.appendToolbarItem(checkbox);
return checkbox;
}
static instance(opts?: {forceNew: boolean, viewportThrottlerTimeout?: number}): ConsoleView {
if (!consoleViewInstance || opts?.forceNew) {
consoleViewInstance = new ConsoleView(opts?.viewportThrottlerTimeout ?? 50);
}
return consoleViewInstance;
}
static clearConsole(): void {
SDK.ConsoleModel.ConsoleModel.requestClearMessages();
}
modelAdded(model: SDK.ConsoleModel.ConsoleModel): void {
model.messages().forEach(this.addConsoleMessage, this);
}
modelRemoved(model: SDK.ConsoleModel.ConsoleModel): void {
if (!Common.Settings.Settings.instance().moduleSetting('preserveConsoleLog').get() &&
model.target().outermostTarget() === model.target()) {
this.consoleCleared();
}
}
private onFilterChanged(): void {
this.filter.currentFilter.levelsMask =
this.isSidebarOpen ? ConsoleFilter.allLevelsFilterValue() : this.filter.messageLevelFiltersSetting.get();
this.cancelBuildHiddenCache();
if (this.immediatelyFilterMessagesForTest) {
for (const viewMessage of this.consoleMessages) {
this.computeShouldMessageBeVisible(viewMessage);
}
this.updateMessageList();
return;
}
this.buildHiddenCache(0, this.consoleMessages.slice());
}
private setImmediatelyFilterMessagesForTest(): void {
this.immediatelyFilterMessagesForTest = true;
}
searchableView(): UI.SearchableView.SearchableView {
return this.searchableViewInternal;
}
clearHistory(): void {
this.prompt.history().clear();
}
private consoleHistoryAutocompleteChanged(): void {
this.prompt.setAddCompletionsFromHistory(this.consoleHistoryAutocompleteSetting.get());
}
itemCount(): number {
return this.visibleViewMessages.length;
}
itemElement(index: number): ConsoleViewportElement|null {
return this.visibleViewMessages[index];
}
fastHeight(index: number): number {
return this.visibleViewMessages[index].fastHeight();
}
minimumRowHeight(): number {
return 16;
}
private registerWithMessageSink(): void {
Common.Console.Console.instance().messages().forEach(this.addSinkMessage, this);
Common.Console.Console.instance().addEventListener(Common.Console.Events.MessageAdded, ({data: message}) => {
this.addSinkMessage(message);
}, this);
}
private addSinkMessage(message: Common.Console.Message): void {
let level: Protocol.Log.LogEntryLevel = Protocol.Log.LogEntryLevel.Verbose;
switch (message.level) {
case Common.Console.MessageLevel.Info:
level = Protocol.Log.LogEntryLevel.Info;
break;
case Common.Console.MessageLevel.Error:
level = Protocol.Log.LogEntryLevel.Error;
break;
case Common.Console.MessageLevel.Warning:
level = Protocol.Log.LogEntryLevel.Warning;
break;
}
const consoleMessage = new SDK.ConsoleModel.ConsoleMessage(
null, Protocol.Log.LogEntrySource.Other, level, message.text,
{type: SDK.ConsoleModel.FrontendMessageType.System, timestamp: message.timestamp});
this.addConsoleMessage(consoleMessage);
}
private consoleTimestampsSettingChanged(): void {
this.updateMessageList();
this.consoleMessages.forEach(viewMessage => viewMessage.updateTimestamp());
this.groupableMessageTitle.forEach(viewMessage => viewMessage.updateTimestamp());
}
private executionContextChanged(): void {
this.prompt.clearAutocomplete();
}
override willHide(): void {
this.hidePromptSuggestBox();
}
override wasShown(): void {
super.wasShown();
this.updateIssuesToolbarItem();
this.viewport.refresh();
this.registerCSSFiles([consoleViewStyles, objectValueStyles, CodeHighlighter.Style.default]);
}
override focus(): void {
if (this.viewport.hasVirtualSelection()) {
(this.viewport.contentElement() as HTMLElement).focus();
} else {
this.focusPrompt();
}
}
focusPrompt(): void {
if (!this.prompt.hasFocus()) {
const oldStickToBottom = this.viewport.stickToBottom();
const oldScrollTop = this.viewport.element.scrollTop;
this.prompt.focus();
this.viewport.setStickToBottom(oldStickToBottom);
this.viewport.element.scrollTop = oldScrollTop;
}
}
override restoreScrollPositions(): void {
if (this.viewport.stickToBottom()) {
this.immediatelyScrollToBottom();
} else {
super.restoreScrollPositions();
}
}
override onResize(): void {
this.scheduleViewportRefresh();
this.hidePromptSuggestBox();
if (this.viewport.stickToBottom()) {
this.immediatelyScrollToBottom();
}
for (let i = 0; i < this.visibleViewMessages.length; ++i) {
this.visibleViewMessages[i].onResize();
}
}
private hidePromptSuggestBox(): void {
this.prompt.clearAutocomplete();
}
private async invalidateViewport(): Promise<void> {
this.updateIssuesToolbarItem();
if (this.muteViewportUpdates) {
this.maybeDirtyWhileMuted = true;
return;
}
if (this.needsFullUpdate) {
this.updateMessageList();
delete this.needsFullUpdate;
} else {
this.viewport.invalidate();
}
return;
}
private updateIssuesToolbarItem(): void {
const manager = IssuesManager.IssuesManager.IssuesManager.instance();
const issueEnumeration = IssueCounter.IssueCounter.getIssueCountsEnumeration(manager);
const issuesTitleGotoIssues = manager.numberOfIssues() === 0 ?
i18nString(UIStrings.issueToolbarClickToGoToTheIssuesTab) :
i18nString(UIStrings.issueToolbarClickToView, {issueEnumeration});
const issuesTitleGeneral = i18nString(UIStrings.issueToolbarTooltipGeneral);
const issuesTitle = `${issuesTitleGeneral} ${issuesTitleGotoIssues}`;
UI.Tooltip.Tooltip.install(this.issueCounter, issuesTitle);
this.issueCounter.data = {
...this.issueCounter.data,
leadingText: i18nString(UIStrings.issuesWithColon, {n: manager.numberOfIssues()}),
accessibleName: issuesTitle,
};
}
private scheduleViewportRefresh(): void {
if (this.muteViewportUpdates) {
this.maybeDirtyWhileMuted = true;
return;
}
this.scheduledRefreshPromiseForTest = this.viewportThrottler.schedule(this.invalidateViewport.bind(this));
}
getScheduledRefreshPromiseForTest(): Promise<void>|undefined {
return this.scheduledRefreshPromiseForTest;
}
private immediatelyScrollToBottom(): void {
// This will scroll viewport and trigger its refresh.
this.viewport.setStickToBottom(true);
this.promptElement.scrollIntoView(true);
}
private updateFilterStatus(): void {
if (this.hiddenByFilterCount === this.lastShownHiddenByFilterCount) {
return;
}
this.filterStatusText.setText(i18nString(UIStrings.sHidden, {n: this.hiddenByFilterCount}));
this.filterStatusText.setVisible(Boolean(this.hiddenByFilterCount));
this.lastShownHiddenByFilterCount = this.hiddenByFilterCount;
}
private onConsoleMessageAdded(event: Common.EventTarget.EventTargetEvent<SDK.ConsoleModel.ConsoleMessage>): void {
const message = event.data;
this.addConsoleMessage(message);
}
private addConsoleMessage(message: SDK.ConsoleModel.ConsoleMessage): void {
const viewMessage = this.createViewMessage(message);
consoleMessageToViewMessage.set(message, viewMessage);
if (message.type === SDK.ConsoleModel.FrontendMessageType.Command ||
message.type === SDK.ConsoleModel.FrontendMessageType.Result) {
const lastMessage = this.consoleMessages[this.consoleMessages.length - 1];
const newTimestamp = lastMessage && messagesSortedBySymbol.get(lastMessage) || 0;
messagesSortedBySymbol.set(viewMessage, newTimestamp);
} else {
messagesSortedBySymbol.set(viewMessage, viewMessage.consoleMessage().timestamp);
}
let insertAt;
if (!this.consoleMessages.length ||
timeComparator(viewMessage, this.consoleMessages[this.consoleMessages.length - 1]) > 0) {
insertAt = this.consoleMessages.length;
} else {
insertAt = Platform.ArrayUtilities.upperBound(this.consoleMessages, viewMessage, timeComparator);
}
const insertedInMiddle = insertAt < this.consoleMessages.length;
this.consoleMessages.splice(insertAt, 0, viewMessage);
if (message.type !== SDK.ConsoleModel.FrontendMessageType.Command &&
message.type !== SDK.ConsoleModel.FrontendMessageType.Result) {
// Maintain group tree.
// Find parent group.
const consoleGroupStartIndex =
Platform.ArrayUtilities.upperBound(this.consoleGroupStarts, viewMessage, timeComparator) - 1;
if (consoleGroupStartIndex >= 0) {
const currentGroup = this.consoleGroupStarts[consoleGroupStartIndex];
addToGroup(viewMessage, currentGroup);
}
// Add new group.
if (message.isGroupStartMessage()) {
insertAt = Platform.ArrayUtilities.upperBound(this.consoleGroupStarts, viewMessage, timeComparator);
this.consoleGroupStarts.splice(insertAt, 0, viewMessage as ConsoleGroupViewMessage);
}
}
this.filter.onMessageAdded(message);
if (this.isSidebarOpen) {
this.sidebar.onMessageAdded(viewMessage);
} else {
this.pendingSidebarMessages.push(viewMessage);
}
// If we already have similar messages, go slow path.
let shouldGoIntoGroup = false;
const shouldGroupSimilar = this.groupSimilarSetting.get();
if (message.isGroupable()) {
const groupKey = viewMessage.groupKey();
shouldGoIntoGroup = shouldGroupSimilar && this.groupableMessages.has(groupKey);
let list = this.groupableMessages.get(groupKey);
if (!list) {
list = [];
this.groupableMessages.set(groupKey, list);
}
list.push(viewMessage);
}
this.computeShouldMessageBeVisible(viewMessage);
if (!shouldGoIntoGroup && !insertedInMiddle) {
this.appendMessageToEnd(
viewMessage,
!shouldGroupSimilar /* crbug.com/1082963: prevent collapse of same messages when "Group similar" is false */);
this.updateFilterStatus();
this.searchableViewInternal.updateSearchMatchesCount(this.regexMatchRanges.length);
} else {
this.needsFullUpdate = true;
}
this.scheduleViewportRefresh();
this.consoleMessageAddedForTest(viewMessage);
// Figure out whether the message should belong into this group or the parent group based on group end timestamp.
function addToGroup(viewMessage: ConsoleViewMessage, currentGroup: ConsoleGroupViewMessage): void {
const currentEnd = currentGroup.groupEnd();
if (currentEnd !== null) {
// Exceeds this group's end. It should belong into parent group.
if (timeComparator(viewMessage, currentEnd) > 0) {
const parent = currentGroup.consoleGroup();
// No parent group. We reached ungrouped messages. Don't establish group links.
if (parent === null) {
return;
} // Add to parent group.
addToGroup(viewMessage, parent);
return;
}
}
// Add message to this group, and set group of the message.
if (viewMessage.consoleMessage().type === Protocol.Runtime.ConsoleAPICalledEventType.EndGroup) {
currentGroup.setGroupEnd(viewMessage);
} else {
viewMessage.setConsoleGroup(currentGroup);
}
}
function timeComparator(viewMessage1: ConsoleViewMessage, viewMessage2: ConsoleViewMessage): number {
return (messagesSortedBySymbol.get(viewMessage1) || 0) - (messagesSortedBySymbol.get(viewMessage2) || 0);
}
}
private onConsoleMessageUpdated(event: Common.EventTarget.EventTargetEvent<SDK.ConsoleModel.ConsoleMessage>): void {
const message = event.data;
const viewMessage = consoleMessageToViewMessage.get(message);
if (viewMessage) {
viewMessage.updateMessageElement();
this.computeShouldMessageBeVisible(viewMessage);
this.updateMessageList();
}
}
private consoleMessageAddedForTest(_viewMessage: ConsoleViewMessage): void {
}
private shouldMessageBeVisible(viewMessage: ConsoleViewMessage): boolean {
return !this.shouldBeHiddenCache.has(viewMessage);
}
private computeShouldMessageBeVisible(viewMessage: ConsoleViewMessage): void {
if (this.filter.shouldBeVisible(viewMessage) &&
(!this.isSidebarOpen || this.sidebar.shouldBeVisible(viewMessage))) {
this.shouldBeHiddenCache.delete(viewMessage);
} else {
this.shouldBeHiddenCache.add(viewMessage);
}
}
private appendMessageToEnd(viewMessage: ConsoleViewMessage, preventCollapse?: boolean): void {
if (viewMessage.consoleMessage().category === Protocol.Log.LogEntryCategory.Cors &&
!this.showCorsErrorsSetting.get()) {
return;
}
const lastMessage = this.visibleViewMessages[this.visibleViewMessages.length - 1];
if (viewMessage.consoleMessage().type === Protocol.Runtime.ConsoleAPICalledEventType.EndGroup) {
if (lastMessage) {
const group = lastMessage.consoleGroup();
if (group && !group.messagesHidden()) {
lastMessage.incrementCloseGroupDecorationCount();
}
}
return;
}
if (!this.shouldMessageBeVisible(viewMessage)) {
this.hiddenByFilterCount++;
return;
}
if (!preventCollapse &&
this.tryToCollapseMessages(viewMessage, this.visibleViewMessages[this.visibleViewMessages.length - 1])) {
return;
}
const currentGroup = viewMessage.consoleGroup();
if (!currentGroup || !currentGroup.messagesHidden()) {
const originatingMessage = viewMessage.consoleMessage().originatingMessage();
const adjacent = Boolean(originatingMessage && lastMessage?.consoleMessage() === originatingMessage);
viewMessage.setAdjacentUserCommandResult(adjacent);
showGroup(currentGroup, this.visibleViewMessages);
this.visibleViewMessages.push(viewMessage);
this.searchMessage(this.visibleViewMessages.length - 1);
}
this.messageAppendedForTests();
// Show the group the message belongs to, and also show parent groups.
function showGroup(currentGroup: ConsoleGroupViewMessage|null, visibleViewMessages: ConsoleViewMessage[]): void {
if (currentGroup === null) {
return;
}
// Group is already being shown, no need to traverse to
// parent groups since they are also already being shown.
if (visibleViewMessages.includes(currentGroup)) {
return;
}
const parentGroup = currentGroup.consoleGroup();
if (parentGroup) {
showGroup(parentGroup, visibleViewMessages);
}
visibleViewMessages.push(currentGroup);
}
}
private messageAppendedForTests(): void {
// This method is sniffed in tests.
}
private createViewMessage(message: SDK.ConsoleModel.ConsoleMessage): ConsoleViewMessage {
switch (message.type) {
case SDK.ConsoleModel.FrontendMessageType.Command:
return new ConsoleCommand(
message, this.linkifier, this.requestResolver, this.issueResolver, this.onMessageResizedBound);
case SDK.ConsoleModel.FrontendMessageType.Result:
return new ConsoleCommandResult(
message, this.linkifier, this.requestResolver, this.issueResolver, this.onMessageResizedBound);
case Protocol.Runtime.ConsoleAPICalledEventType.StartGroupCollapsed:
case Protocol.Runtime.ConsoleAPICalledEventType.StartGroup:
return new ConsoleGroupViewMessage(
message, this.linkifier, this.requestResolver, this.issueResolver, this.updateMessageList.bind(this),
this.onMessageResizedBound);
case Protocol.Runtime.ConsoleAPICalledEventType.Table:
return new ConsoleTableMessageView(
message, this.linkifier, this.requestResolver, this.issueResolver, this.onMessageResizedBound);
default:
return new ConsoleViewMessage(
message, this.linkifier, this.requestResolver, this.issueResolver, this.onMessageResizedBound);
}
}
private async onMessageResized(event: Common.EventTarget.EventTargetEvent<UI.TreeOutline.TreeElement>):
Promise<void> {
const treeElement = event.data;
if (this.pendingBatchResize || !treeElement.treeOutline) {
return;
}
this.pendingBatchResize = true;
await Promise.resolve();
const treeOutlineElement = treeElement.treeOutline.element;
this.viewport.setStickToBottom(this.isScrolledToBottom());
// Scroll, in case mutations moved the element below the visible area.
if (treeOutlineElement.offsetHeight <= this.messagesElement.offsetHeight) {
treeOutlineElement.scrollIntoViewIfNeeded();
}
this.pendingBatchResize = false;
}
private consoleCleared(): void {
const hadFocus = this.viewport.element.hasFocus();
this.cancelBuildHiddenCache();
this.currentMatchRangeIndex = -1;
this.consoleMessages = [];
this.groupableMessages.clear();
this.groupableMessageTitle.clear();
this.sidebar.clear();
this.pendingSidebarMessages = [];
this.updateMessageList();
this.hidePromptSuggestBox();
this.viewport.setStickToBottom(true);
this.linkifier.reset();
this.filter.clear();
this.requestResolver.clear();
this.consoleGroupStarts = [];
if (hadFocus) {
this.prompt.focus();
}
UI.ARIAUtils.alert(i18nString(UIStrings.consoleCleared));
}
private handleContextMenuEvent(event: Event): void {
const contextMenu = new UI.ContextMenu.ContextMenu(event);
const eventTarget = (event.target as Node);
if (eventTarget.isSelfOrDescendant(this.promptElement)) {
void contextMenu.show();
return;
}
const sourceElement = eventTarget.enclosingNodeOrSelfWithClass('console-message-wrapper');
const consoleViewMessage = sourceElement && getMessageForElement(sourceElement);
const consoleMessage = consoleViewMessage ? consoleViewMessage.consoleMessage() : null;
if (consoleMessage && consoleMessage.url) {
const menuTitle = i18nString(
UIStrings.hideMessagesFromS, {PH1: new Common.ParsedURL.ParsedURL(consoleMessage.url).displayName});
contextMenu.headerSection().appendItem(
menuTitle, this.filter.addMessageURLFilter.bind(this.filter, consoleMessage.url));
}
contextMenu.defaultSection().appendAction('console.clear');
contextMenu.defaultSection().appendAction('console.clear.history');
contextMenu.saveSection().appendItem(i18nString(UIStrings.saveAs), this.saveConsole.bind(this));
if (this.element.hasSelection()) {
contextMenu.clipboardSection().appendItem(
i18nString(UIStrings.copyVisibleStyledSelection), this.viewport.copyWithStyles.bind(this.viewport));
}
if (consoleMessage) {
const request = Logs.NetworkLog.NetworkLog.requestForConsoleMessage(consoleMessage);
if (request && SDK.NetworkManager.NetworkManager.canReplayRequest(request)) {
contextMenu.debugSection().appendItem(
i18nString(UIStrings.replayXhr), SDK.NetworkManager.NetworkManager.replayRequest.bind(null, request));
}
}
void contextMenu.show();
}
private async saveConsole(): Promise<void> {
const url = (SDK.TargetManager.TargetManager.instance().scopeTarget() as SDK.Target.Target).inspectedURL();
const parsedURL = Common.ParsedURL.ParsedURL.fromString(url);
const filename =
Platform.StringUtilities.sprintf('%s-%d.log', parsedURL ? parsedURL.host : 'console', Date.now()) as
Platform.DevToolsPath.RawPathString;
const stream = new Bindings.FileUtils.FileOutputStream();
const progressIndicator = new UI.ProgressIndicator.ProgressIndicator();
progressIndicator.setTitle(i18nString(UIStrings.writingFile));
progressIndicator.setTotalWork(this.itemCount());
const chunkSize = 350;
if (!await stream.open(filename)) {
return;
}
this.progressToolbarItem.element.appendChild(progressIndicator.element);
let messageIndex = 0;
while (messageIndex < this.itemCount() && !progressIndicator.isCanceled()) {
const messageContents = [];
let i;
for (i = 0; i < chunkSize && i + messageIndex < this.itemCount(); ++i) {
const message = (this.itemElement(messageIndex + i) as ConsoleViewMessage);
messageContents.push(message.toExportString());
}
messageIndex += i;
await stream.write(messageContents.join('\n') + '\n');
progressIndicator.setWorked(messageIndex);
}
void stream.close();
progressIndicator.done();
}
private tryToCollapseMessages(viewMessage: ConsoleViewMessage, lastMessage?: ConsoleViewMessage): boolean {
const timestampsShown = this.timestampsSetting.get();
if (!timestampsShown && lastMessage && !viewMessage.consoleMessage().isGroupMessage() &&
viewMessage.consoleMessage().type !== SDK.ConsoleModel.FrontendMessageType.Command &&
viewMessage.consoleMessage().type !== SDK.ConsoleModel.FrontendMessageType.Result &&
viewMessage.consoleMessage().isEqual(lastMessage.consoleMessage()))