chrome-devtools-frontend
Version:
Chrome DevTools UI
941 lines (819 loc) • 35.1 kB
text/typescript
// Copyright 2014 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
/* eslint-disable @devtools/no-imperative-dom-api */
import '../../ui/legacy/legacy.js';
import * as Common from '../../core/common/common.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 Bindings from '../../models/bindings/bindings.js';
import * as Persistence from '../../models/persistence/persistence.js';
import * as Workspace from '../../models/workspace/workspace.js';
import * as QuickOpen from '../../ui/legacy/components/quick_open/quick_open.js';
import * as SourceFrame from '../../ui/legacy/components/source_frame/source_frame.js';
import * as UI from '../../ui/legacy/legacy.js';
import {Directives, html, render} from '../../ui/lit/lit.js';
import * as VisualLogging from '../../ui/visual_logging/visual_logging.js';
import * as Components from './components/components.js';
import {EditingLocationHistoryManager} from './EditingLocationHistoryManager.js';
import sourcesViewStyles from './sourcesView.css.js';
import {
type EditorSelectedEvent,
Events as TabbedEditorContainerEvents,
type SerializedHistoryItem,
TabbedEditorContainer,
type TabbedEditorContainerDelegate,
} from './TabbedEditorContainer.js';
import {Events as UISourceCodeFrameEvents, UISourceCodeFrame} from './UISourceCodeFrame.js';
const UIStrings = {
/**
* @description Tooltip for the navigator toggle in the Sources panel. Command to open or show the
* sidebar containing the navigator tool.
*/
showNavigator: 'Show navigator',
/**
* @description Tooltip for the navigator toggle in the Sources panel. Command to close or hide
* the sidebar containing the navigator tool.
*/
hideNavigator: 'Hide navigator',
/**
* @description Screen reader announcement when the navigator sidebar is shown in the Sources panel.
*/
navigatorShown: 'Navigator sidebar shown',
/**
* @description Screen reader announcement when the navigator sidebar is hidden in the Sources panel.
*/
navigatorHidden: 'Navigator sidebar hidden',
/**
* @description Screen reader announcement when the debugger sidebar is shown in the Sources panel.
*/
debuggerShown: 'Debugger sidebar shown',
/**
* @description Screen reader announcement when the debugger sidebar is hidden in the Sources panel.
*/
debuggerHidden: 'Debugger sidebar hidden',
/**
* @description Tooltip for the debugger toggle in the Sources panel. Command to open or show the
* sidebar containing the debugger tool.
*/
showDebugger: 'Show debugger',
/**
* @description Tooltip for the debugger toggle in the Sources panel. Command to close or hide the
* sidebar containing the debugger tool.
*/
hideDebugger: 'Hide debugger',
} as const;
const str_ = i18n.i18n.registerUIStrings('panels/sources/SourcesView.ts', UIStrings);
const i18nString = i18n.i18n.getLocalizedString.bind(undefined, str_);
const {widget, widgetRef} = UI.Widget;
export interface ViewInput {
searchProvider: UI.SearchableView.Searchable;
replaceProvider: UI.SearchableView.Replaceable;
searchableViewId: string;
scriptViewToolbarItems: UI.Toolbar.ToolbarItem[];
bottomToolbarItems: UI.Toolbar.ToolbarItem[];
searchableViewFactory: () => UI.Widget.Widget;
delegate: TabbedEditorContainerDelegate;
previouslyViewedFilesSetting: Common.Settings.Setting<SerializedHistoryItem[]>;
}
export interface ViewOutput {
scriptViewToolbar?: UI.Toolbar.Toolbar;
editorContainer?: TabbedEditorContainer;
}
export type View = (input: ViewInput, output: ViewOutput, target: HTMLElement) => void;
export const DEFAULT_VIEW: View = (input, output, target): void => {
// clang-format off
render(html`
<devtools-widget class="vbox flex-auto" ${widget(input.searchableViewFactory)}>
<devtools-widget class="vbox flex-auto"
${widget(TabbedEditorContainer, {delegate: input.delegate, previouslyViewedFilesSetting: input.previouslyViewedFilesSetting})}
${widgetRef(TabbedEditorContainer, e => { output.editorContainer = e; })}>
</devtools-widget>
</devtools-widget>
<div class="sources-toolbar" jslog=${VisualLogging.toolbar('bottom')}>
<devtools-toolbar class="script-view-toolbar" style="flex: auto;" ${Directives.ref(el => { output.scriptViewToolbar = el as UI.Toolbar.Toolbar; })}>
${input.scriptViewToolbarItems.map(item => item.element)}
</devtools-toolbar>
<devtools-toolbar class="bottom-toolbar">
${input.bottomToolbarItems.map(item => item.element)}
</devtools-toolbar>
</div>`, target);
// clang-format on
};
export class SourcesView extends Common.ObjectWrapper.eventMixin<EventTypes, typeof UI.Widget.VBox>(UI.Widget.VBox)
implements TabbedEditorContainerDelegate, UI.SearchableView.Searchable, UI.SearchableView.Replaceable {
#searchableView!: UI.SearchableView.SearchableView;
private readonly sourceViewByUISourceCode: Map<Workspace.UISourceCode.UISourceCode, UI.Widget.Widget>;
editorContainer?: TabbedEditorContainer;
#uiSourceCodes = new Set<Workspace.UISourceCode.UISourceCode>();
private readonly historyManager: EditingLocationHistoryManager;
#scriptViewToolbar?: UI.Toolbar.Toolbar;
#scriptViewToolbarItems: UI.Toolbar.ToolbarItem[] = [];
#bottomToolbarItems: UI.Toolbar.ToolbarItem[] = [];
private toolbarChangedListener: Common.EventTarget.EventDescriptor|null;
private searchView?: UISourceCodeFrame;
private searchConfig?: UI.SearchableView.SearchConfig;
#view: View = DEFAULT_VIEW;
#toggleNavigatorSidebarButton: UI.Toolbar.ToolbarButton;
#toggleDebuggerSidebarButton: UI.Toolbar.ToolbarButton;
#onToggleNavigatorSidebar?: () => void;
#onToggleDebuggerSidebar?: () => void;
#isNavigatorSidebarOpen = false;
#isDebuggerSidebarOpen = false;
#navigatorSidebarInitialized = false;
#debuggerSidebarInitialized = false;
#isVertical = false;
#leftToolbarItems?: UI.Toolbar.ToolbarItem[];
#rightToolbarItems?: UI.Toolbar.ToolbarItem[];
#breakpointsActive?: boolean;
#editorContainerPromise: Promise<TabbedEditorContainer>;
#editorContainerResolve!: (container: TabbedEditorContainer) => void;
readonly previouslyViewedFilesSetting: Common.Settings.Setting<SerializedHistoryItem[]>;
constructor() {
super({jslog: `${VisualLogging.pane('editor').track({keydown: 'Escape'})}`});
this.#editorContainerPromise = new Promise(resolve => {
this.#editorContainerResolve = resolve;
});
this.registerRequiredCSS(sourcesViewStyles);
this.element.id = 'sources-panel-sources-view';
this.setMinimumAndPreferredSizes(88, 52, 150, 100);
const workspace = Workspace.Workspace.WorkspaceImpl.instance();
this.sourceViewByUISourceCode = new Map();
this.historyManager = new EditingLocationHistoryManager(this);
this.toolbarChangedListener = null;
this.#toggleNavigatorSidebarButton =
new UI.Toolbar.ToolbarButton(i18nString(UIStrings.showNavigator), 'left-panel-open');
this.#toggleNavigatorSidebarButton.addEventListener(UI.Toolbar.ToolbarButton.Events.CLICK, () => {
this.#onToggleNavigatorSidebar?.();
});
this.#toggleNavigatorSidebarButton.element.setAttribute(
'jslog', `${VisualLogging.toggleSubpane().track({click: true}).context('navigator')}`);
this.#toggleDebuggerSidebarButton =
new UI.Toolbar.ToolbarButton(i18nString(UIStrings.showDebugger), 'right-panel-open');
this.#toggleDebuggerSidebarButton.addEventListener(UI.Toolbar.ToolbarButton.Events.CLICK, () => {
this.#onToggleDebuggerSidebar?.();
});
this.#toggleDebuggerSidebarButton.element.setAttribute(
'jslog', `${VisualLogging.toggleSubpane().track({click: true}).context('debugger')}`);
this.previouslyViewedFilesSetting =
Common.Settings.Settings.instance().createLocalSetting('previously-viewed-files', []);
this.requestUpdate();
UI.UIUtils.startBatchUpdate();
workspace.uiSourceCodes().forEach(ui => this.addUISourceCode(ui));
UI.UIUtils.endBatchUpdate();
workspace.addEventListener(Workspace.Workspace.Events.UISourceCodeAdded, this.uiSourceCodeAdded, this);
workspace.addEventListener(Workspace.Workspace.Events.UISourceCodeRemoved, this.uiSourceCodeRemoved, this);
workspace.addEventListener(Workspace.Workspace.Events.ProjectRemoved, this.projectRemoved.bind(this), this);
SDK.TargetManager.TargetManager.instance().addScopeChangeListener(this.#onScopeChange.bind(this));
function handleBeforeUnload(event: Event): void {
if (event.returnValue) {
return;
}
const unsavedSourceCodes: Workspace.UISourceCode.UISourceCode[] = [];
const projects =
Workspace.Workspace.WorkspaceImpl.instance().projectsForType(Workspace.Workspace.projectTypes.FileSystem);
for (const project of projects) {
for (const uiSourceCode of project.uiSourceCodes()) {
if (uiSourceCode.isDirty()) {
unsavedSourceCodes.push(uiSourceCode);
}
}
}
if (!unsavedSourceCodes.length) {
return;
}
event.returnValue = true;
void UI.ViewManager.ViewManager.instance().showView('sources');
for (const sourceCode of unsavedSourceCodes) {
void Common.Revealer.reveal(sourceCode);
}
}
if (!window.opener) {
window.addEventListener('beforeunload', handleBeforeUnload, true);
}
}
override performUpdate(): void {
const input: ViewInput = {
searchProvider: this,
replaceProvider: this,
searchableViewId: 'sources-view-search-config',
scriptViewToolbarItems: this.#scriptViewToolbarItems,
bottomToolbarItems: this.#bottomToolbarItems,
searchableViewFactory: this.#searchableViewFactory,
delegate: this,
previouslyViewedFilesSetting: this.previouslyViewedFilesSetting,
};
const that = this;
const output: ViewOutput = {
set scriptViewToolbar(value: UI.Toolbar.Toolbar) {
that.#scriptViewToolbar = value;
},
set editorContainer(value: TabbedEditorContainer) {
that.setEditorContainer(value);
},
};
this.#view(input, output, this.element);
}
override onDetach(): void {
super.onDetach();
this.editorContainer?.detachEditors();
}
setEditorContainer(editorContainer: TabbedEditorContainer): void {
if (this.editorContainer === editorContainer) {
return;
}
if (this.editorContainer) {
this.editorContainer.removeEventListener(TabbedEditorContainerEvents.EDITOR_SELECTED, this.editorSelected, this);
this.editorContainer.removeEventListener(TabbedEditorContainerEvents.EDITOR_CLOSED, this.editorClosed, this);
}
this.editorContainer = editorContainer;
if (this.editorContainer) {
if (this.#leftToolbarItems) {
this.editorContainer.leftToolbar().removeToolbarItems();
for (const item of this.#leftToolbarItems) {
this.editorContainer.leftToolbar().appendToolbarItem(item);
}
}
if (this.#rightToolbarItems) {
this.editorContainer.rightToolbar().removeToolbarItems();
for (const item of this.#rightToolbarItems) {
this.editorContainer.rightToolbar().appendToolbarItem(item);
}
}
if (this.#breakpointsActive !== undefined) {
this.editorContainer.element.classList.toggle('breakpoints-deactivated', !this.#breakpointsActive);
}
this.#editorContainerResolve(editorContainer);
this.editorContainer.addEventListener(TabbedEditorContainerEvents.EDITOR_SELECTED, this.editorSelected, this);
this.editorContainer.addEventListener(TabbedEditorContainerEvents.EDITOR_CLOSED, this.editorClosed, this);
UI.UIUtils.startBatchUpdate();
for (const uiSourceCode of this.#uiSourceCodes) {
this.editorContainer.addUISourceCode(uiSourceCode);
}
UI.UIUtils.endBatchUpdate();
}
}
static defaultUISourceCodeScores(): Map<Workspace.UISourceCode.UISourceCode, number> {
const defaultScores = new Map<Workspace.UISourceCode.UISourceCode, number>();
const sourcesView = UI.Context.Context.instance().flavor(SourcesView);
if (sourcesView) {
const uiSourceCodes = sourcesView.editorContainer?.historyUISourceCodes() ?? [];
for (let i = 1; i < uiSourceCodes.length; ++i) // Skip current element
{
defaultScores.set(uiSourceCodes[i], uiSourceCodes.length - i);
}
}
return defaultScores;
}
set onToggleNavigatorSidebar(callback: () => void) {
this.#onToggleNavigatorSidebar = callback;
}
set onToggleDebuggerSidebar(callback: () => void) {
this.#onToggleDebuggerSidebar = callback;
}
set isNavigatorSidebarOpen(isOpen: boolean) {
const isInitialized = this.#navigatorSidebarInitialized;
this.#navigatorSidebarInitialized = true;
if (this.#isNavigatorSidebarOpen === isOpen) {
return;
}
this.#isNavigatorSidebarOpen = isOpen;
this.#updateNavigatorSidebarButton();
if (isInitialized) {
UI.ARIAUtils.LiveAnnouncer.alert(isOpen ? i18nString(UIStrings.navigatorShown) :
i18nString(UIStrings.navigatorHidden));
}
}
set isDebuggerSidebarOpen(isOpen: boolean) {
const isInitialized = this.#debuggerSidebarInitialized;
this.#debuggerSidebarInitialized = true;
if (this.#isDebuggerSidebarOpen === isOpen) {
return;
}
this.#isDebuggerSidebarOpen = isOpen;
this.#updateDebuggerSidebarButton();
if (isInitialized) {
UI.ARIAUtils.LiveAnnouncer.alert(isOpen ? i18nString(UIStrings.debuggerShown) :
i18nString(UIStrings.debuggerHidden));
}
}
#updateNavigatorSidebarButton(): void {
const navHidden = !this.#isNavigatorSidebarOpen;
this.#toggleNavigatorSidebarButton.setGlyph(navHidden ? 'left-panel-open' : 'left-panel-close');
this.#toggleNavigatorSidebarButton.setTitle(navHidden ? i18nString(UIStrings.showNavigator) :
i18nString(UIStrings.hideNavigator));
}
#updateDebuggerSidebarButton(): void {
const debuggerHidden = !this.#isDebuggerSidebarOpen;
const debuggerGlyph = debuggerHidden ? (this.#isVertical ? 'right-panel-open' : 'bottom-panel-open') :
(this.#isVertical ? 'right-panel-close' : 'bottom-panel-close');
this.#toggleDebuggerSidebarButton.setGlyph(debuggerGlyph);
this.#toggleDebuggerSidebarButton.setTitle(debuggerHidden ? i18nString(UIStrings.showDebugger) :
i18nString(UIStrings.hideDebugger));
}
toggleDebuggerSidebarButtonEnabled(enabled: boolean): void {
this.#toggleDebuggerSidebarButton.setEnabled(enabled);
}
setLayoutMode(splitWidget: UI.SplitWidget.SplitWidget, isVertical: boolean, isInWrapper: boolean): void {
this.#bottomToolbarItems = [];
if (isVertical || isInWrapper) {
if (this.#scriptViewToolbar) {
splitWidget.uninstallResizer(this.#scriptViewToolbar);
}
} else if (this.#scriptViewToolbar) {
splitWidget.installResizer(this.#scriptViewToolbar);
}
this.#isVertical = isVertical;
this.#updateNavigatorSidebarButton();
this.#updateDebuggerSidebarButton();
const leftItems: UI.Toolbar.ToolbarItem[] = [];
const rightItems: UI.Toolbar.ToolbarItem[] = [];
if (!isInWrapper) {
leftItems.push(this.#toggleNavigatorSidebarButton);
if (!Root.Runtime.Runtime.isTraceApp()) {
if (isVertical) {
rightItems.push(this.#toggleDebuggerSidebarButton);
} else {
this.#bottomToolbarItems.push(this.#toggleDebuggerSidebarButton);
}
}
}
this.#leftToolbarItems = leftItems;
this.#rightToolbarItems = rightItems;
if (this.editorContainer) {
const editorContainer = this.editorContainer;
editorContainer.leftToolbar().removeToolbarItems();
leftItems.forEach(item => editorContainer.leftToolbar().appendToolbarItem(item));
editorContainer.rightToolbar().removeToolbarItems();
rightItems.forEach(item => editorContainer.rightToolbar().appendToolbarItem(item));
}
this.requestUpdate();
}
override wasShown(): void {
super.wasShown();
UI.Context.Context.instance().setFlavor(SourcesView, this);
}
override willHide(): void {
UI.Context.Context.instance().setFlavor(SourcesView, null);
super.willHide();
}
searchableView(): UI.SearchableView.SearchableView {
return this.#searchableViewFactory();
}
visibleView(): UI.Widget.Widget|null {
return (this.editorContainer?.visibleView ?? null) as UI.Widget.Widget | null;
}
currentSourceFrame(): UISourceCodeFrame|null {
const view = this.visibleView();
if (!(view instanceof UISourceCodeFrame)) {
return null;
}
return (view);
}
currentUISourceCode(): Workspace.UISourceCode.UISourceCode|null {
return this.editorContainer?.currentFile() ?? null;
}
onCloseEditorTab(): boolean {
const uiSourceCode = this.editorContainer?.currentFile();
if (!uiSourceCode) {
return false;
}
this.editorContainer?.closeFile(uiSourceCode);
return true;
}
onJumpToPreviousLocation(): void {
this.historyManager.rollback();
}
onJumpToNextLocation(): void {
this.historyManager.rollover();
}
#onScopeChange(): void {
const workspace = Workspace.Workspace.WorkspaceImpl.instance();
for (const uiSourceCode of workspace.uiSourceCodes()) {
if (uiSourceCode.project().type() !== Workspace.Workspace.projectTypes.Network) {
continue;
}
const target = Bindings.NetworkProject.NetworkProject.targetForUISourceCode(uiSourceCode);
if (SDK.TargetManager.TargetManager.instance().isInScope(target)) {
this.addUISourceCode(uiSourceCode);
} else {
this.removeUISourceCodes([uiSourceCode]);
}
}
}
private uiSourceCodeAdded(event: Common.EventTarget.EventTargetEvent<Workspace.UISourceCode.UISourceCode>): void {
const uiSourceCode = event.data;
this.addUISourceCode(uiSourceCode);
}
private addUISourceCode(uiSourceCode: Workspace.UISourceCode.UISourceCode): void {
const project = uiSourceCode.project();
if (project.isServiceProject()) {
return;
}
switch (project.type()) {
case Workspace.Workspace.projectTypes.FileSystem: {
if (Persistence.FileSystemWorkspaceBinding.FileSystemWorkspaceBinding.fileSystemType(project) === 'overrides') {
return;
}
break;
}
case Workspace.Workspace.projectTypes.Network: {
const target = Bindings.NetworkProject.NetworkProject.targetForUISourceCode(uiSourceCode);
if (!SDK.TargetManager.TargetManager.instance().isInScope(target)) {
return;
}
}
}
this.#uiSourceCodes.add(uiSourceCode);
this.editorContainer?.addUISourceCode(uiSourceCode);
}
private uiSourceCodeRemoved(event: Common.EventTarget.EventTargetEvent<Workspace.UISourceCode.UISourceCode>): void {
const uiSourceCode = event.data;
this.removeUISourceCodes([uiSourceCode]);
}
private removeUISourceCodes(uiSourceCodes: Workspace.UISourceCode.UISourceCode[]): void {
uiSourceCodes.forEach(ui => this.#uiSourceCodes.delete(ui));
this.editorContainer?.removeUISourceCodes(uiSourceCodes);
for (let i = 0; i < uiSourceCodes.length; ++i) {
this.removeSourceFrame(uiSourceCodes[i]);
this.historyManager.removeHistoryForSourceCode(uiSourceCodes[i]);
}
}
private projectRemoved(event: Common.EventTarget.EventTargetEvent<Workspace.Workspace.Project>): void {
const project = event.data;
const uiSourceCodes = project.uiSourceCodes();
this.removeUISourceCodes([...uiSourceCodes]);
}
private updateScriptViewToolbarItems(): void {
const view = this.visibleView();
if (view instanceof UI.View.SimpleView) {
void view.toolbarItems().then(items => {
if (Array.isArray(items)) {
this.#scriptViewToolbarItems = items;
} else {
const wrapper = document.createElement('div');
wrapper.style.display = 'contents';
// eslint-disable-next-line @devtools/no-lit-render-outside-of-view
render(items, wrapper);
this.#scriptViewToolbarItems = [new UI.Toolbar.ToolbarItem(wrapper)];
}
this.requestUpdate();
});
}
}
async showSourceLocation(uiSourceCode: Workspace.UISourceCode.UISourceCode,
location?: SourceFrame.SourceFrame.RevealPosition, omitFocus?: boolean,
omitHighlight?: boolean): Promise<void> {
if (!this.editorContainer) {
await this.#editorContainerPromise;
}
const currentFrame = this.currentSourceFrame();
if (currentFrame) {
this.historyManager.updateCurrentState(currentFrame.uiSourceCode(),
currentFrame.textEditor.state.selection.main.head);
}
this.editorContainer?.showFile(uiSourceCode);
const currentSourceFrame = this.currentSourceFrame();
if (currentSourceFrame && location) {
currentSourceFrame.revealPosition(location, !omitHighlight);
}
const visibleView = this.visibleView();
if (!omitFocus && visibleView) {
visibleView.focus();
}
}
private createSourceView(uiSourceCode: Workspace.UISourceCode.UISourceCode): UI.Widget.Widget {
let sourceView;
const contentType = uiSourceCode.contentType();
if (contentType === Common.ResourceType.resourceTypes.Image || uiSourceCode.mimeType().startsWith('image/')) {
sourceView = new SourceFrame.ImageView.ImageView(uiSourceCode.mimeType(), uiSourceCode);
} else if (contentType === Common.ResourceType.resourceTypes.Font || uiSourceCode.mimeType().includes('font')) {
sourceView = new SourceFrame.FontView.FontView(uiSourceCode.mimeType(), uiSourceCode);
} else if (uiSourceCode.name() === HEADER_OVERRIDES_FILENAME) {
sourceView = new Components.HeadersView.HeadersView(uiSourceCode);
} else {
sourceView = new UISourceCodeFrame(uiSourceCode);
this.historyManager.trackSourceFrameCursorJumps(sourceView);
}
uiSourceCode.addEventListener(Workspace.UISourceCode.Events.TitleChanged, this.#uiSourceCodeTitleChanged, this);
this.sourceViewByUISourceCode.set(uiSourceCode, sourceView);
return sourceView;
}
#sourceViewTypeForWidget(widget: UI.Widget.Widget): SourceViewType {
if (widget instanceof SourceFrame.ImageView.ImageView) {
return SourceViewType.IMAGE_VIEW;
}
if (widget instanceof SourceFrame.FontView.FontView) {
return SourceViewType.FONT_VIEW;
}
if (widget instanceof Components.HeadersView.HeadersView) {
return SourceViewType.HEADERS_VIEW;
}
return SourceViewType.SOURCE_VIEW;
}
#sourceViewTypeForUISourceCode(uiSourceCode: Workspace.UISourceCode.UISourceCode): SourceViewType {
if (uiSourceCode.name() === HEADER_OVERRIDES_FILENAME) {
return SourceViewType.HEADERS_VIEW;
}
const contentType = uiSourceCode.contentType();
switch (contentType) {
case Common.ResourceType.resourceTypes.Image:
return SourceViewType.IMAGE_VIEW;
case Common.ResourceType.resourceTypes.Font:
return SourceViewType.FONT_VIEW;
default:
return SourceViewType.SOURCE_VIEW;
}
}
#uiSourceCodeTitleChanged(event: Common.EventTarget.EventTargetEvent<Workspace.UISourceCode.UISourceCode>): void {
const uiSourceCode = event.data;
const widget = this.sourceViewByUISourceCode.get(uiSourceCode);
if (widget) {
if (this.#sourceViewTypeForWidget(widget) !== this.#sourceViewTypeForUISourceCode(uiSourceCode)) {
// Remove the existing editor tab and create a new one of the correct type.
this.removeUISourceCodes([uiSourceCode]);
this.#uiSourceCodes.add(uiSourceCode);
void this.showSourceLocation(uiSourceCode);
}
}
}
getSourceView(uiSourceCode: Workspace.UISourceCode.UISourceCode): UI.Widget.Widget|undefined {
return this.sourceViewByUISourceCode.get(uiSourceCode);
}
private getOrCreateSourceView(uiSourceCode: Workspace.UISourceCode.UISourceCode): UI.Widget.Widget {
return this.sourceViewByUISourceCode.get(uiSourceCode) || this.createSourceView(uiSourceCode);
}
recycleUISourceCodeFrame(sourceFrame: UISourceCodeFrame, uiSourceCode: Workspace.UISourceCode.UISourceCode): void {
sourceFrame.uiSourceCode().removeEventListener(Workspace.UISourceCode.Events.TitleChanged,
this.#uiSourceCodeTitleChanged, this);
this.sourceViewByUISourceCode.delete(sourceFrame.uiSourceCode());
sourceFrame.setUISourceCode(uiSourceCode);
this.sourceViewByUISourceCode.set(uiSourceCode, sourceFrame);
uiSourceCode.addEventListener(Workspace.UISourceCode.Events.TitleChanged, this.#uiSourceCodeTitleChanged, this);
}
viewForFile(uiSourceCode: Workspace.UISourceCode.UISourceCode): UI.Widget.Widget {
return this.getOrCreateSourceView(uiSourceCode);
}
private removeSourceFrame(uiSourceCode: Workspace.UISourceCode.UISourceCode): void {
const sourceView = this.sourceViewByUISourceCode.get(uiSourceCode);
this.sourceViewByUISourceCode.delete(uiSourceCode);
if (sourceView && sourceView instanceof UISourceCodeFrame) {
(sourceView).dispose();
}
uiSourceCode.removeEventListener(Workspace.UISourceCode.Events.TitleChanged, this.#uiSourceCodeTitleChanged, this);
}
private editorClosed(event: Common.EventTarget.EventTargetEvent<Workspace.UISourceCode.UISourceCode>): void {
const uiSourceCode = event.data;
this.historyManager.removeHistoryForSourceCode(uiSourceCode);
let wasSelected = false;
if (!this.editorContainer?.currentFile()) {
wasSelected = true;
}
// SourcesNavigator does not need to update on EditorClosed.
this.removeToolbarChangedListener();
this.updateScriptViewToolbarItems();
this.searchableView().resetSearch();
const data = {
uiSourceCode,
wasSelected,
};
this.dispatchEventToListeners(Events.EDITOR_CLOSED, data);
}
private editorSelected(event: Common.EventTarget.EventTargetEvent<EditorSelectedEvent>): void {
const previousSourceFrame = event.data.previousView instanceof UISourceCodeFrame ? event.data.previousView : null;
if (previousSourceFrame) {
previousSourceFrame.setSearchableView(null);
}
const currentSourceFrame = event.data.currentView instanceof UISourceCodeFrame ? event.data.currentView : null;
if (currentSourceFrame) {
currentSourceFrame.setSearchableView(this.searchableView());
}
this.searchableView().setReplaceable(Boolean(currentSourceFrame?.canEditSource()));
this.searchableView().refreshSearch();
this.updateToolbarChangedListener();
this.updateScriptViewToolbarItems();
const currentFile = this.editorContainer?.currentFile();
if (currentFile) {
this.dispatchEventToListeners(Events.EDITOR_SELECTED, currentFile);
}
}
private removeToolbarChangedListener(): void {
if (this.toolbarChangedListener) {
Common.EventTarget.removeEventListeners([this.toolbarChangedListener]);
}
this.toolbarChangedListener = null;
}
private updateToolbarChangedListener(): void {
this.removeToolbarChangedListener();
const sourceFrame = this.currentSourceFrame();
if (!sourceFrame) {
return;
}
this.toolbarChangedListener = sourceFrame.addEventListener(UISourceCodeFrameEvents.TOOLBAR_ITEMS_CHANGED,
this.updateScriptViewToolbarItems, this);
}
onSearchCanceled(): void {
if (this.searchView) {
this.searchView.onSearchCanceled();
}
delete this.searchView;
delete this.searchConfig;
}
performSearch(searchConfig: UI.SearchableView.SearchConfig, shouldJump: boolean, jumpBackwards?: boolean): void {
const sourceFrame = this.currentSourceFrame();
if (!sourceFrame) {
return;
}
this.searchView = sourceFrame;
this.searchConfig = searchConfig;
this.searchView.performSearch(this.searchConfig, shouldJump, jumpBackwards);
}
jumpToNextSearchResult(): void {
if (!this.searchView) {
return;
}
if (this.searchConfig && this.searchView !== this.currentSourceFrame()) {
this.performSearch(this.searchConfig, true);
return;
}
this.searchView.jumpToNextSearchResult();
}
jumpToPreviousSearchResult(): void {
if (!this.searchView) {
return;
}
if (this.searchConfig && this.searchView !== this.currentSourceFrame()) {
this.performSearch(this.searchConfig, true);
if (this.searchView) {
this.searchView.jumpToLastSearchResult();
}
return;
}
this.searchView.jumpToPreviousSearchResult();
}
supportsCaseSensitiveSearch(): boolean {
return true;
}
supportsWholeWordSearch(): boolean {
return true;
}
supportsRegexSearch(): boolean {
return true;
}
replaceSelectionWith(searchConfig: UI.SearchableView.SearchConfig, replacement: string): void {
const sourceFrame = this.currentSourceFrame();
if (!sourceFrame) {
console.assert(Boolean(sourceFrame));
return;
}
sourceFrame.replaceSelectionWith(searchConfig, replacement);
}
replaceAllWith(searchConfig: UI.SearchableView.SearchConfig, replacement: string): void {
const sourceFrame = this.currentSourceFrame();
if (!sourceFrame) {
console.assert(Boolean(sourceFrame));
return;
}
sourceFrame.replaceAllWith(searchConfig, replacement);
}
showOutlineQuickOpen(): void {
QuickOpen.QuickOpen.QuickOpenImpl.show('@');
}
showGoToLineQuickOpen(): void {
if (this.editorContainer?.currentFile()) {
QuickOpen.QuickOpen.QuickOpenImpl.show(':');
}
}
save(): void {
this.saveSourceFrame(this.currentSourceFrame());
}
saveAll(): void {
const sourceFrames = this.editorContainer?.fileViews() ?? [];
sourceFrames.forEach(this.saveSourceFrame.bind(this));
}
private saveSourceFrame(sourceFrame: UI.Widget.Widget|null): void {
if (!(sourceFrame instanceof UISourceCodeFrame)) {
return;
}
const uiSourceCodeFrame = sourceFrame;
uiSourceCodeFrame.commitEditing();
}
#searchableViewFactory = (): UI.SearchableView.SearchableView => {
if (!this.#searchableView) {
this.#searchableView = new UI.SearchableView.SearchableView(this, this, 'sources.search-sources-tab');
this.#searchableView.setMinimalSearchQuerySize(0);
}
return this.#searchableView;
};
toggleBreakpointsActiveState(active: boolean): void {
this.#breakpointsActive = active;
this.editorContainer?.element.classList.toggle('breakpoints-deactivated', !active);
this.requestUpdate();
}
}
export const enum Events {
EDITOR_CLOSED = 'EditorClosed',
EDITOR_SELECTED = 'EditorSelected',
}
export interface EditorClosedEvent {
uiSourceCode: Workspace.UISourceCode.UISourceCode;
wasSelected: boolean;
}
export interface EventTypes {
[Events.EDITOR_CLOSED]: EditorClosedEvent;
[Events.EDITOR_SELECTED]: Workspace.UISourceCode.UISourceCode;
}
export class SwitchFileActionDelegate implements UI.ActionRegistration.ActionDelegate {
private static nextFile(currentUISourceCode: Workspace.UISourceCode.UISourceCode): Workspace.UISourceCode.UISourceCode
|null {
function fileNamePrefix(name: string): string {
const lastDotIndex = name.lastIndexOf('.');
const namePrefix = name.substr(0, lastDotIndex !== -1 ? lastDotIndex : name.length);
return namePrefix.toLowerCase();
}
const candidates = [];
const url = currentUISourceCode.parentURL();
const name = currentUISourceCode.name();
const namePrefix = fileNamePrefix(name);
for (const uiSourceCode of currentUISourceCode.project().uiSourceCodes()) {
if (url !== uiSourceCode.parentURL()) {
continue;
}
if (fileNamePrefix(uiSourceCode.name()) === namePrefix) {
candidates.push(uiSourceCode.name());
}
}
candidates.sort(Platform.StringUtilities.naturalOrderComparator);
const index = Platform.NumberUtilities.mod(candidates.indexOf(name) + 1, candidates.length);
const fullURL = Common.ParsedURL.ParsedURL.concatenate(
(url ? Common.ParsedURL.ParsedURL.concatenate(url, '/') : '' as Platform.DevToolsPath.UrlString),
candidates[index]);
const nextUISourceCode = currentUISourceCode.project().uiSourceCodeForURL(fullURL);
return nextUISourceCode !== currentUISourceCode ? nextUISourceCode : null;
}
handleAction(context: UI.Context.Context, _actionId: string): boolean {
const sourcesView = context.flavor(SourcesView);
if (!sourcesView) {
return false;
}
const currentUISourceCode = sourcesView.currentUISourceCode();
if (!currentUISourceCode) {
return false;
}
const nextUISourceCode = SwitchFileActionDelegate.nextFile(currentUISourceCode);
if (!nextUISourceCode) {
return false;
}
void sourcesView.showSourceLocation(nextUISourceCode);
return true;
}
}
export class ActionDelegate implements UI.ActionRegistration.ActionDelegate {
handleAction(context: UI.Context.Context, actionId: string): boolean {
const sourcesView = context.flavor(SourcesView);
if (!sourcesView) {
return false;
}
switch (actionId) {
case 'sources.close-all':
sourcesView.editorContainer?.closeAllFiles();
return true;
case 'sources.jump-to-previous-location':
sourcesView.onJumpToPreviousLocation();
return true;
case 'sources.jump-to-next-location':
sourcesView.onJumpToNextLocation();
return true;
case 'sources.next-editor-tab':
sourcesView.editorContainer?.selectNextTab();
return true;
case 'sources.previous-editor-tab':
sourcesView.editorContainer?.selectPrevTab();
return true;
case 'sources.close-editor-tab':
return sourcesView.onCloseEditorTab();
case 'sources.go-to-line':
sourcesView.showGoToLineQuickOpen();
return true;
case 'sources.go-to-member':
sourcesView.showOutlineQuickOpen();
return true;
case 'sources.save':
sourcesView.save();
return true;
case 'sources.save-all':
sourcesView.saveAll();
return true;
}
return false;
}
}
const HEADER_OVERRIDES_FILENAME = '.headers';
const enum SourceViewType {
IMAGE_VIEW = 'ImageView',
FONT_VIEW = 'FontView',
HEADERS_VIEW = 'HeadersView',
SOURCE_VIEW = 'SourceView',
}