UNPKG

chrome-devtools-frontend

Version:
1,288 lines (1,147 loc) 70 kB
/* * Copyright (C) 2012 Google Inc. All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are * met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * 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. * * Neither the name of Google Inc. 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 THE COPYRIGHT HOLDERS AND 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 THE COPYRIGHT * OWNER OR 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 Bindings from '../../models/bindings/bindings.js'; import * as Persistence from '../../models/persistence/persistence.js'; import * as Workspace from '../../models/workspace/workspace.js'; import * as IconButton from '../../ui/components/icon_button/icon_button.js'; import * as UI from '../../ui/legacy/legacy.js'; import * as Snippets from '../snippets/snippets.js'; import navigatorTreeStyles from './navigatorTree.css.js'; import navigatorViewStyles from './navigatorView.css.js'; import {SearchSourcesView} from './SearchSourcesView.js'; const UIStrings = { /** *@description Text in Navigator View of the Sources panel */ searchInFolder: 'Search in folder', /** *@description Search label in Navigator View of the Sources panel */ searchInAllFiles: 'Search in all files', /** *@description Text in Navigator View of the Sources panel */ noDomain: '(no domain)', /** *@description Text in Navigator View of the Sources panel */ authored: 'Authored', /** *@description Text in Navigator View of the Sources panel */ authoredTooltip: 'Contains original sources', /** *@description Text in Navigator View of the Sources panel */ deployed: 'Deployed', /** *@description Text in Navigator View of the Sources panel */ deployedTooltip: 'Contains final sources the browser sees', /** *@description Text in Navigator View of the Sources panel */ areYouSureYouWantToExcludeThis: 'Are you sure you want to exclude this folder?', /** *@description Text in Navigator View of the Sources panel */ areYouSureYouWantToDeleteThis: 'Are you sure you want to delete this file?', /** *@description A context menu item in the Navigator View of the Sources panel */ rename: 'Rename…', /** *@description A context menu item in the Navigator View of the Sources panel */ makeACopy: 'Make a copy…', /** *@description Text to delete something */ delete: 'Delete', /** *@description Text in Navigator View of the Sources panel */ areYouSureYouWantToDeleteAll: 'Are you sure you want to delete all overrides contained in this folder?', /** *@description A context menu item in the Navigator View of the Sources panel */ openFolder: 'Open folder', /** *@description A context menu item in the Navigator View of the Sources panel */ newFile: 'New file', /** *@description A context menu item in the Navigator View of the Sources panel */ excludeFolder: 'Exclude folder', /** *@description A context menu item in the Navigator View of the Sources panel */ removeFolderFromWorkspace: 'Remove folder from workspace', /** *@description Text in Navigator View of the Sources panel */ areYouSureYouWantToRemoveThis: 'Are you sure you want to remove this folder?', /** *@description A context menu item in the Navigator View of the Sources panel */ deleteAllOverrides: 'Delete all overrides', /** *@description Name of an item from source map *@example {compile.html} PH1 */ sFromSourceMap: '{PH1} (from source map)', /** *@description Name of an item that is on the ignore list *@example {compile.html} PH1 */ sIgnoreListed: '{PH1} (ignore listed)', }; const str_ = i18n.i18n.registerUIStrings('panels/sources/NavigatorView.ts', UIStrings); const i18nString = i18n.i18n.getLocalizedString.bind(undefined, str_); export const Types = { Authored: 'authored', Deployed: 'deployed', Domain: 'domain', File: 'file', FileSystem: 'fs', FileSystemFolder: 'fs-folder', Frame: 'frame', NetworkFolder: 'nw-folder', Root: 'root', SourceMapFolder: 'sm-folder', Worker: 'worker', }; const TYPE_ORDERS = new Map([ [Types.Root, 1], [Types.Authored, 1], [Types.Deployed, 5], [Types.Domain, 10], [Types.FileSystemFolder, 1], [Types.NetworkFolder, 1], [Types.SourceMapFolder, 2], [Types.File, 10], [Types.Frame, 70], [Types.Worker, 90], [Types.FileSystem, 100], ]); export class NavigatorView extends UI.Widget.VBox implements SDK.TargetManager.Observer { private placeholder: UI.Widget.Widget|null; scriptsTree: UI.TreeOutline.TreeOutlineInShadow; private readonly uiSourceCodeNodes: Platform.MapUtilities.Multimap<Workspace.UISourceCode.UISourceCode, NavigatorUISourceCodeTreeNode>; private readonly subfolderNodes: Map<string, NavigatorFolderTreeNode>; private readonly rootNode: NavigatorRootTreeNode; private readonly frameNodes: Map<SDK.ResourceTreeModel.ResourceTreeFrame, NavigatorGroupTreeNode>; private authoredNode?: NavigatorGroupTreeNode; private deployedNode?: NavigatorGroupTreeNode; // TODO(crbug.com/1172300) Ignored during the jsdoc to ts migration) // eslint-disable-next-line @typescript-eslint/no-explicit-any private navigatorGroupByFolderSetting: Common.Settings.Setting<any>; private navigatorGroupByAuthoredExperiment?: string; private workspaceInternal!: Workspace.Workspace.WorkspaceImpl; private lastSelectedUISourceCode?: Workspace.UISourceCode.UISourceCode; private groupByFrame?: boolean; private groupByAuthored?: boolean; // TODO(crbug.com/1172300) Ignored during the jsdoc to ts migration) // eslint-disable-next-line @typescript-eslint/no-explicit-any private groupByDomain?: any; // TODO(crbug.com/1172300) Ignored during the jsdoc to ts migration) // eslint-disable-next-line @typescript-eslint/no-explicit-any private groupByFolder?: any; constructor(enableAuthoredGrouping?: boolean) { super(true); this.placeholder = null; this.scriptsTree = new UI.TreeOutline.TreeOutlineInShadow(); this.scriptsTree.setComparator(NavigatorView.treeElementsCompare); this.scriptsTree.setFocusable(false); this.contentElement.appendChild(this.scriptsTree.element); this.setDefaultFocusedElement(this.scriptsTree.element); this.uiSourceCodeNodes = new Platform.MapUtilities.Multimap(); this.subfolderNodes = new Map(); this.rootNode = new NavigatorRootTreeNode(this); this.rootNode.populate(); this.frameNodes = new Map(); this.contentElement.addEventListener('contextmenu', this.handleContextMenu.bind(this), false); UI.ShortcutRegistry.ShortcutRegistry.instance().addShortcutListener( this.contentElement, {'sources.rename': this.renameShortcut.bind(this)}); this.navigatorGroupByFolderSetting = Common.Settings.Settings.instance().moduleSetting('navigatorGroupByFolder'); this.navigatorGroupByFolderSetting.addChangeListener(this.groupingChanged.bind(this)); if (enableAuthoredGrouping) { this.navigatorGroupByAuthoredExperiment = Root.Runtime.ExperimentName.AUTHORED_DEPLOYED_GROUPING; } Bindings.IgnoreListManager.IgnoreListManager.instance().addChangeListener(this.ignoreListChanged.bind(this)); this.initGrouping(); Persistence.Persistence.PersistenceImpl.instance().addEventListener( Persistence.Persistence.Events.BindingCreated, this.onBindingChanged, this); Persistence.Persistence.PersistenceImpl.instance().addEventListener( Persistence.Persistence.Events.BindingRemoved, this.onBindingChanged, this); Persistence.NetworkPersistenceManager.NetworkPersistenceManager.instance().addEventListener( Persistence.NetworkPersistenceManager.Events.RequestsForHeaderOverridesFileChanged, this.#onRequestsForHeaderOverridesFileChanged, this); SDK.TargetManager.TargetManager.instance().addEventListener( SDK.TargetManager.Events.NameChanged, this.targetNameChanged, this); SDK.TargetManager.TargetManager.instance().observeTargets(this); this.resetWorkspace(Workspace.Workspace.WorkspaceImpl.instance()); this.workspaceInternal.uiSourceCodes().forEach(this.addUISourceCode.bind(this)); Bindings.NetworkProject.NetworkProjectManager.instance().addEventListener( Bindings.NetworkProject.Events.FrameAttributionAdded, this.frameAttributionAdded, this); Bindings.NetworkProject.NetworkProjectManager.instance().addEventListener( Bindings.NetworkProject.Events.FrameAttributionRemoved, this.frameAttributionRemoved, this); } private static treeElementOrder(treeElement: UI.TreeOutline.TreeElement): number { if (boostOrderForNode.has(treeElement)) { return 0; } const actualElement = (treeElement as NavigatorSourceTreeElement); let order = TYPE_ORDERS.get(actualElement.nodeType) || 0; if (actualElement.uiSourceCode) { const contentType = actualElement.uiSourceCode.contentType(); if (contentType.isDocument()) { order += 3; } else if (contentType.isScript()) { order += 5; } else if (contentType.isStyleSheet()) { order += 10; } else { order += 15; } } return order; } static appendSearchItem(contextMenu: UI.ContextMenu.ContextMenu, path?: Platform.DevToolsPath.EncodedPathString): void { let searchLabel = i18nString(UIStrings.searchInFolder); if (!path || !path.trim()) { path = '*' as Platform.DevToolsPath.EncodedPathString; searchLabel = i18nString(UIStrings.searchInAllFiles); } contextMenu.viewSection().appendItem(searchLabel, () => { if (path) { void SearchSourcesView.openSearch(`file:${path.trim()}`); } }); } private static treeElementsCompare( treeElement1: UI.TreeOutline.TreeElement, treeElement2: UI.TreeOutline.TreeElement): number { const typeWeight1 = NavigatorView.treeElementOrder(treeElement1); const typeWeight2 = NavigatorView.treeElementOrder(treeElement2); if (typeWeight1 > typeWeight2) { return 1; } if (typeWeight1 < typeWeight2) { return -1; } return Platform.StringUtilities.naturalOrderComparator(treeElement1.titleAsText(), treeElement2.titleAsText()); } setPlaceholder(placeholder: UI.Widget.Widget): void { console.assert(!this.placeholder, 'A placeholder widget was already set'); this.placeholder = placeholder; placeholder.show(this.contentElement, this.contentElement.firstChild); updateVisibility.call(this); this.scriptsTree.addEventListener(UI.TreeOutline.Events.ElementAttached, updateVisibility.bind(this)); this.scriptsTree.addEventListener(UI.TreeOutline.Events.ElementsDetached, updateVisibility.bind(this)); function updateVisibility(this: NavigatorView): void { const showTree = this.scriptsTree.firstChild(); if (showTree) { placeholder.hideWidget(); } else { placeholder.showWidget(); } this.scriptsTree.element.classList.toggle('hidden', !showTree); } } private onBindingChanged(event: Common.EventTarget.EventTargetEvent<Persistence.Persistence.PersistenceBinding>): void { const binding = event.data; let isFromSourceMap = false; // Update UISourceCode titles. const networkNodes = this.uiSourceCodeNodes.get(binding.network); for (const networkNode of networkNodes) { networkNode.updateTitle(); isFromSourceMap ||= networkNode.uiSourceCode().contentType().isFromSourceMap(); } const fileSystemNodes = this.uiSourceCodeNodes.get(binding.fileSystem); for (const fileSystemNode of fileSystemNodes) { fileSystemNode.updateTitle(); isFromSourceMap ||= fileSystemNode.uiSourceCode().contentType().isFromSourceMap(); } // Update folder titles. const pathTokens = Persistence.FileSystemWorkspaceBinding.FileSystemWorkspaceBinding.relativePath(binding.fileSystem); let folderPath = Platform.DevToolsPath.EmptyEncodedPathString; for (let i = 0; i < pathTokens.length - 1; ++i) { folderPath = Common.ParsedURL.ParsedURL.concatenate(folderPath, pathTokens[i]); const folderId = this.folderNodeId( binding.fileSystem.project(), null, null, binding.fileSystem.origin(), isFromSourceMap, folderPath); const folderNode = this.subfolderNodes.get(folderId); if (folderNode) { folderNode.updateTitle(); } folderPath = Common.ParsedURL.ParsedURL.concatenate(folderPath, '/'); } // Update fileSystem root title. const fileSystemRoot = this.rootOrDeployedNode().child(binding.fileSystem.project().id()); if (fileSystemRoot) { fileSystemRoot.updateTitle(); } } #onRequestsForHeaderOverridesFileChanged( event: Common.EventTarget.EventTargetEvent<Workspace.UISourceCode.UISourceCode>): void { const headersFileUiSourceCode = event.data; const networkNodes = this.uiSourceCodeNodes.get(headersFileUiSourceCode); for (const networkNode of networkNodes) { networkNode.updateTitle(); } } override focus(): void { this.scriptsTree.focus(); } /** * Central place to add elements to the tree to * enable focus if the tree has elements */ appendChild(parent: UI.TreeOutline.TreeElement, child: UI.TreeOutline.TreeElement): void { this.scriptsTree.setFocusable(true); parent.appendChild(child); } /** * Central place to remove elements from the tree to * disable focus if the tree is empty */ removeChild(parent: UI.TreeOutline.TreeElement, child: UI.TreeOutline.TreeElement): void { parent.removeChild(child); if (this.scriptsTree.rootElement().childCount() === 0) { this.scriptsTree.setFocusable(false); } } private resetWorkspace(workspace: Workspace.Workspace.WorkspaceImpl): void { // Clear old event listeners first. if (this.workspaceInternal) { this.workspaceInternal.removeEventListener( Workspace.Workspace.Events.UISourceCodeAdded, this.uiSourceCodeAddedCallback, this); this.workspaceInternal.removeEventListener( Workspace.Workspace.Events.UISourceCodeRemoved, this.uiSourceCodeRemovedCallback, this); this.workspaceInternal.removeEventListener( Workspace.Workspace.Events.ProjectAdded, this.projectAddedCallback, this); this.workspaceInternal.removeEventListener( Workspace.Workspace.Events.ProjectRemoved, this.projectRemovedCallback, this); } this.workspaceInternal = workspace; this.workspaceInternal.addEventListener( Workspace.Workspace.Events.UISourceCodeAdded, this.uiSourceCodeAddedCallback, this); this.workspaceInternal.addEventListener( Workspace.Workspace.Events.UISourceCodeRemoved, this.uiSourceCodeRemovedCallback, this); this.workspaceInternal.addEventListener(Workspace.Workspace.Events.ProjectAdded, this.projectAddedCallback, this); this.workspaceInternal.addEventListener( Workspace.Workspace.Events.ProjectRemoved, this.projectRemovedCallback, this); this.workspaceInternal.projects().forEach(this.projectAdded.bind(this)); this.computeUniqueFileSystemProjectNames(); } private projectAddedCallback(event: Common.EventTarget.EventTargetEvent<Workspace.Workspace.Project>): void { const project = event.data; this.projectAdded(project); if (project.type() === Workspace.Workspace.projectTypes.FileSystem) { this.computeUniqueFileSystemProjectNames(); } } private projectRemovedCallback(event: Common.EventTarget.EventTargetEvent<Workspace.Workspace.Project>): void { const project = event.data; this.removeProject(project); if (project.type() === Workspace.Workspace.projectTypes.FileSystem) { this.computeUniqueFileSystemProjectNames(); } } workspace(): Workspace.Workspace.WorkspaceImpl { return this.workspaceInternal; } acceptProject(project: Workspace.Workspace.Project): boolean { return !project.isServiceProject(); } private frameAttributionAdded( event: Common.EventTarget.EventTargetEvent<Bindings.NetworkProject.FrameAttributionEvent>): void { const {uiSourceCode} = event.data; if (!this.acceptsUISourceCode(uiSourceCode)) { return; } const addedFrame = (event.data.frame as SDK.ResourceTreeModel.ResourceTreeFrame | null); // This event does not happen for UISourceCodes without initial attribution. this.addUISourceCodeNode(uiSourceCode, addedFrame); } private frameAttributionRemoved( event: Common.EventTarget.EventTargetEvent<Bindings.NetworkProject.FrameAttributionEvent>): void { const {uiSourceCode} = event.data; if (!this.acceptsUISourceCode(uiSourceCode)) { return; } const removedFrame = (event.data.frame as SDK.ResourceTreeModel.ResourceTreeFrame | null); const node = Array.from(this.uiSourceCodeNodes.get(uiSourceCode)).find(node => node.frame() === removedFrame); if (node) { this.removeUISourceCodeNode(node); } } private acceptsUISourceCode(uiSourceCode: Workspace.UISourceCode.UISourceCode): boolean { return this.acceptProject(uiSourceCode.project()); } private addUISourceCode(uiSourceCode: Workspace.UISourceCode.UISourceCode): void { if (Root.Runtime.experiments.isEnabled(Root.Runtime.ExperimentName.JUST_MY_CODE) && Bindings.IgnoreListManager.IgnoreListManager.instance().isUserOrSourceMapIgnoreListedUISourceCode( uiSourceCode)) { return; } if (!this.acceptsUISourceCode(uiSourceCode)) { return; } const frames = Bindings.NetworkProject.NetworkProject.framesForUISourceCode(uiSourceCode); if (frames.length) { for (const frame of frames) { this.addUISourceCodeNode(uiSourceCode, frame); } } else { this.addUISourceCodeNode(uiSourceCode, null); } this.uiSourceCodeAdded(uiSourceCode); } private addUISourceCodeNode( uiSourceCode: Workspace.UISourceCode.UISourceCode, frame: SDK.ResourceTreeModel.ResourceTreeFrame|null): void { const isFromSourceMap = uiSourceCode.contentType().isFromSourceMap(); let path; if (uiSourceCode.project().type() === Workspace.Workspace.projectTypes.FileSystem) { path = Persistence.FileSystemWorkspaceBinding.FileSystemWorkspaceBinding.relativePath(uiSourceCode).slice(0, -1) as Platform.DevToolsPath.EncodedPathString[]; } else { path = Common.ParsedURL.ParsedURL.extractPath(uiSourceCode.url()).split('/').slice(1, -1) as Platform.DevToolsPath.EncodedPathString[]; } const project = uiSourceCode.project(); const target = Bindings.NetworkProject.NetworkProject.targetForUISourceCode(uiSourceCode); const folderNode = this.folderNode(uiSourceCode, project, target, frame, uiSourceCode.origin(), path, isFromSourceMap); const uiSourceCodeNode = new NavigatorUISourceCodeTreeNode(this, uiSourceCode, frame); const existingNode = folderNode.child(uiSourceCodeNode.id); if (existingNode && existingNode instanceof NavigatorUISourceCodeTreeNode) { this.uiSourceCodeNodes.set(uiSourceCode, existingNode); } else { folderNode.appendChild(uiSourceCodeNode); this.uiSourceCodeNodes.set(uiSourceCode, uiSourceCodeNode); } this.selectDefaultTreeNode(); } uiSourceCodeAdded(_uiSourceCode: Workspace.UISourceCode.UISourceCode): void { } private uiSourceCodeAddedCallback(event: Common.EventTarget.EventTargetEvent<Workspace.UISourceCode.UISourceCode>): void { const uiSourceCode = event.data; this.addUISourceCode(uiSourceCode); } private uiSourceCodeRemovedCallback(event: Common.EventTarget.EventTargetEvent<Workspace.UISourceCode.UISourceCode>): void { this.removeUISourceCodes([event.data]); } tryAddProject(project: Workspace.Workspace.Project): void { this.projectAdded(project); for (const uiSourceCode of project.uiSourceCodes()) { this.addUISourceCode(uiSourceCode); } } private projectAdded(project: Workspace.Workspace.Project): void { const rootOrDeployed = this.rootOrDeployedNode(); if (!this.acceptProject(project) || project.type() !== Workspace.Workspace.projectTypes.FileSystem || Snippets.ScriptSnippetFileSystem.isSnippetsProject(project) || rootOrDeployed.child(project.id())) { return; } rootOrDeployed.appendChild( new NavigatorGroupTreeNode(this, project, project.id(), Types.FileSystem, project.displayName())); this.selectDefaultTreeNode(); } // TODO(einbinder) remove this code after crbug.com/964075 is fixed private selectDefaultTreeNode(): void { const children = this.rootNode.children(); if (children.length && !this.scriptsTree.selectedTreeElement) { children[0].treeNode().select(true /* omitFocus */, false /* selectedByUser */); } } private computeUniqueFileSystemProjectNames(): void { const fileSystemProjects = this.workspaceInternal.projectsForType(Workspace.Workspace.projectTypes.FileSystem); if (!fileSystemProjects.length) { return; } const reversedIndex = Common.Trie.Trie.newArrayTrie<string[]>(); const reversedPaths = []; for (const project of fileSystemProjects) { const fileSystem = (project as Persistence.FileSystemWorkspaceBinding.FileSystem); const reversedPathParts = fileSystem.fileSystemPath().split('/').reverse(); reversedPaths.push(reversedPathParts); reversedIndex.add(reversedPathParts); } const rootOrDeployed = this.rootOrDeployedNode(); for (let i = 0; i < fileSystemProjects.length; ++i) { const reversedPath = reversedPaths[i]; const project = fileSystemProjects[i]; reversedIndex.remove(reversedPath); const commonPrefix = reversedIndex.longestPrefix(reversedPath, false /* fullWordOnly */); reversedIndex.add(reversedPath); const prefixPath = reversedPath.slice(0, commonPrefix.length + 1); const path = Common.ParsedURL.ParsedURL.encodedPathToRawPathString( prefixPath.reverse().join('/') as Platform.DevToolsPath.EncodedPathString); const fileSystemNode = rootOrDeployed.child(project.id()); if (fileSystemNode) { fileSystemNode.setTitle(path); } } } removeProject(project: Workspace.Workspace.Project): void { this.removeUISourceCodes(project.uiSourceCodes()); if (project.type() !== Workspace.Workspace.projectTypes.FileSystem) { return; } const fileSystemNode = this.rootNode.child(project.id()); if (!fileSystemNode) { return; } this.rootNode.removeChild(fileSystemNode); } private folderNodeId( project: Workspace.Workspace.Project, target: SDK.Target.Target|null, frame: SDK.ResourceTreeModel.ResourceTreeFrame|null, projectOrigin: string, isFromSourceMap: boolean, path: Platform.DevToolsPath.EncodedPathString): string { const projectId = project.type() === Workspace.Workspace.projectTypes.FileSystem ? project.id() : ''; let targetId = target && !(this.groupByAuthored && isFromSourceMap) ? target.id() : ''; let frameId = this.groupByFrame && frame ? frame.id : ''; if (this.groupByAuthored) { if (isFromSourceMap) { targetId = 'Authored'; frameId = ''; } else { targetId = 'Deployed:' + targetId; } } return targetId + ':' + projectId + ':' + frameId + ':' + projectOrigin + ':' + path; } private folderNode( uiSourceCode: Workspace.UISourceCode.UISourceCode, project: Workspace.Workspace.Project, target: SDK.Target.Target|null, frame: SDK.ResourceTreeModel.ResourceTreeFrame|null, projectOrigin: Platform.DevToolsPath.UrlString, path: Platform.DevToolsPath.EncodedPathString[], fromSourceMap: boolean): NavigatorTreeNode { if (Snippets.ScriptSnippetFileSystem.isSnippetsUISourceCode(uiSourceCode)) { return this.rootNode; } if (target && !this.groupByFolder && !fromSourceMap) { return this.domainNode(uiSourceCode, project, target, frame, projectOrigin); } const folderPath = Common.ParsedURL.ParsedURL.join(path, '/'); const folderId = this.folderNodeId(project, target, frame, projectOrigin, fromSourceMap, folderPath); let folderNode = this.subfolderNodes.get(folderId); if (folderNode) { return folderNode; } if (!path.length) { if (target) { return this.domainNode(uiSourceCode, project, target, frame, projectOrigin); } return this.rootOrDeployedNode().child(project.id()) as NavigatorTreeNode; } const parentNode = this.folderNode(uiSourceCode, project, target, frame, projectOrigin, path.slice(0, -1), fromSourceMap); let type: string = fromSourceMap ? Types.SourceMapFolder : Types.NetworkFolder; if (project.type() === Workspace.Workspace.projectTypes.FileSystem) { type = Types.FileSystemFolder; } const name = Common.ParsedURL.ParsedURL.encodedPathToRawPathString(path[path.length - 1]); folderNode = new NavigatorFolderTreeNode(this, project, folderId, type, folderPath, name, projectOrigin); this.subfolderNodes.set(folderId, folderNode); parentNode.appendChild(folderNode); return folderNode; } private domainNode( uiSourceCode: Workspace.UISourceCode.UISourceCode, project: Workspace.Workspace.Project, target: SDK.Target.Target, frame: SDK.ResourceTreeModel.ResourceTreeFrame|null, projectOrigin: string): NavigatorTreeNode { const isAuthored = uiSourceCode.contentType().isFromSourceMap(); const frameNode = this.frameNode(project, target, frame, isAuthored); if (!this.groupByDomain) { return frameNode; } let domainNode = frameNode.child(projectOrigin); if (domainNode) { return domainNode; } domainNode = new NavigatorGroupTreeNode( this, project, projectOrigin, Types.Domain, this.computeProjectDisplayName(target, projectOrigin)); if (frame && projectOrigin === Common.ParsedURL.ParsedURL.extractOrigin(frame.url)) { boostOrderForNode.add(domainNode.treeNode()); } frameNode.appendChild(domainNode); if (isAuthored && this.groupByAuthored) { domainNode.treeNode().expand(); } return domainNode; } private frameNode( project: Workspace.Workspace.Project, target: SDK.Target.Target, frame: SDK.ResourceTreeModel.ResourceTreeFrame|null, isAuthored: boolean): NavigatorTreeNode { if (!this.groupByFrame || !frame || (this.groupByAuthored && isAuthored)) { return this.targetNode(project, target, isAuthored); } let frameNode = this.frameNodes.get(frame); if (frameNode) { return frameNode; } frameNode = new NavigatorGroupTreeNode(this, project, target.id() + ':' + frame.id, Types.Frame, frame.displayName()); frameNode.setHoverCallback(hoverCallback); this.frameNodes.set(frame, frameNode); const parentFrame = frame.parentFrame(); this.frameNode(project, parentFrame ? parentFrame.resourceTreeModel().target() : target, parentFrame, isAuthored) .appendChild(frameNode); if (!parentFrame) { boostOrderForNode.add(frameNode.treeNode()); frameNode.treeNode().expand(); } function hoverCallback(hovered: boolean): void { if (hovered) { const overlayModel = target.model(SDK.OverlayModel.OverlayModel); if (overlayModel && frame) { overlayModel.highlightFrame(frame.id); } } else { SDK.OverlayModel.OverlayModel.hideDOMNodeHighlight(); } } return frameNode; } private targetNode(project: Workspace.Workspace.Project, target: SDK.Target.Target, isAuthored: boolean): NavigatorTreeNode { if (this.groupByAuthored && isAuthored) { if (!this.authoredNode) { this.authoredNode = new NavigatorGroupTreeNode( this, null, 'group:Authored', Types.Authored, i18nString(UIStrings.authored), i18nString(UIStrings.authoredTooltip)); this.rootNode.appendChild(this.authoredNode); this.authoredNode.treeNode().expand(); } return this.authoredNode; } const rootOrDeployed = this.rootOrDeployedNode(); if (target === SDK.TargetManager.TargetManager.instance().scopeTarget()) { return rootOrDeployed; } let targetNode = rootOrDeployed.child('target:' + target.id()); if (!targetNode) { targetNode = new NavigatorGroupTreeNode( this, project, 'target:' + target.id(), target.type() === SDK.Target.Type.Frame ? Types.Frame : Types.Worker, target.name()); rootOrDeployed.appendChild(targetNode); } return targetNode; } private rootOrDeployedNode(): NavigatorTreeNode { if (this.groupByAuthored) { if (!this.deployedNode) { this.deployedNode = new NavigatorGroupTreeNode( this, null, 'group:Deployed', Types.Deployed, i18nString(UIStrings.deployed), i18nString(UIStrings.deployedTooltip)); this.rootNode.appendChild(this.deployedNode); } return this.deployedNode; } return this.rootNode; } private computeProjectDisplayName(target: SDK.Target.Target, projectOrigin: string): string { const runtimeModel = target.model(SDK.RuntimeModel.RuntimeModel); const executionContexts = runtimeModel ? runtimeModel.executionContexts() : []; for (const context of executionContexts) { if (context.name && context.origin && projectOrigin.startsWith(context.origin)) { return context.name; } } if (!projectOrigin) { return i18nString(UIStrings.noDomain); } const parsedURL = new Common.ParsedURL.ParsedURL(projectOrigin); const prettyURL = parsedURL.isValid ? parsedURL.host + (parsedURL.port ? (':' + parsedURL.port) : '') : ''; return (prettyURL || projectOrigin); } revealUISourceCode(uiSourceCode: Workspace.UISourceCode.UISourceCode, select?: boolean): NavigatorUISourceCodeTreeNode |null { const nodes = this.uiSourceCodeNodes.get(uiSourceCode); if (nodes.size === 0) { return null; } const node = nodes.values().next().value; if (!node) { return null; } if (this.scriptsTree.selectedTreeElement) { this.scriptsTree.selectedTreeElement.deselect(); } this.lastSelectedUISourceCode = uiSourceCode; // TODO(dgozman): figure out revealing multiple. node.reveal(select); return node; } sourceSelected(uiSourceCode: Workspace.UISourceCode.UISourceCode, focusSource: boolean): void { this.lastSelectedUISourceCode = uiSourceCode; void Common.Revealer.reveal(uiSourceCode, !focusSource); } #isUISourceCodeOrAnyAncestorSelected(node: NavigatorUISourceCodeTreeNode): boolean { const selectedTreeElement = (this.scriptsTree.selectedTreeElement as NavigatorSourceTreeElement | null); const selectedNode = selectedTreeElement && selectedTreeElement.node; let currentNode: NavigatorTreeNode|null = node; while (currentNode) { if (currentNode === selectedNode) { return true; } currentNode = currentNode.parent; if (!(node instanceof NavigatorGroupTreeNode || node instanceof NavigatorFolderTreeElement)) { break; } } return false; } private removeUISourceCodes(uiSourceCodes: Iterable<Workspace.UISourceCode.UISourceCode>): void { const nodesWithSelectionOnPath: NavigatorUISourceCodeTreeNode[] = []; // First we remove source codes without any selection on their path to root, and only then // the ones with selection. This to avoid layout work associated with moving the selection // around (crbug.com/1409025). for (const uiSourceCode of uiSourceCodes) { const nodes = this.uiSourceCodeNodes.get(uiSourceCode); for (const node of nodes) { if (this.#isUISourceCodeOrAnyAncestorSelected(node)) { nodesWithSelectionOnPath.push(node); } else { this.removeUISourceCodeNode(node); } } } nodesWithSelectionOnPath.forEach(this.removeUISourceCodeNode.bind(this)); } private removeUISourceCodeNode(node: NavigatorUISourceCodeTreeNode): void { const uiSourceCode = node.uiSourceCode(); this.uiSourceCodeNodes.delete(uiSourceCode, node); const project = uiSourceCode.project(); const target = Bindings.NetworkProject.NetworkProject.targetForUISourceCode(uiSourceCode); const frame = node.frame(); let parentNode: (NavigatorTreeNode|null) = node.parent; if (!parentNode) { return; } parentNode.removeChild(node); let currentNode: (NavigatorTreeNode|null) = parentNode; while (currentNode) { parentNode = currentNode.parent; if (!parentNode || !currentNode.isEmpty()) { break; } if ((parentNode === this.rootNode || parentNode === this.deployedNode) && project.type() === Workspace.Workspace.projectTypes.FileSystem) { break; } if (!(currentNode instanceof NavigatorGroupTreeNode || currentNode instanceof NavigatorFolderTreeNode)) { break; } if (currentNode.type === Types.Frame) { this.discardFrame( frame as SDK.ResourceTreeModel.ResourceTreeFrame, Boolean(this.groupByAuthored) && uiSourceCode.contentType().isFromSourceMap()); break; } const folderId = this.folderNodeId( project, target, frame, uiSourceCode.origin(), uiSourceCode.contentType().isFromSourceMap(), currentNode instanceof NavigatorFolderTreeNode && currentNode.folderPath || Platform.DevToolsPath.EmptyEncodedPathString); this.subfolderNodes.delete(folderId); parentNode.removeChild(currentNode); if (currentNode === this.authoredNode) { this.authoredNode = undefined; } else if (currentNode === this.deployedNode) { this.deployedNode = undefined; } currentNode = parentNode; } } reset(tearDownOnly?: boolean): void { for (const node of this.uiSourceCodeNodes.valuesArray()) { node.dispose(); } this.scriptsTree.removeChildren(); this.scriptsTree.setFocusable(false); this.uiSourceCodeNodes.clear(); this.subfolderNodes.clear(); this.frameNodes.clear(); this.rootNode.reset(); this.authoredNode = undefined; this.deployedNode = undefined; if (!tearDownOnly) { // Reset the workspace to repopulate filesystem folders. this.resetWorkspace(Workspace.Workspace.WorkspaceImpl.instance()); } } handleContextMenu(_event: Event): void { } private async renameShortcut(): Promise<boolean> { const selectedTreeElement = (this.scriptsTree.selectedTreeElement as NavigatorSourceTreeElement | null); const node = selectedTreeElement && selectedTreeElement.node; if (!node || !node.uiSourceCode() || !node.uiSourceCode().canRename()) { return false; } this.rename(node, false); return true; } private handleContextMenuCreate( project: Workspace.Workspace.Project, path: Platform.DevToolsPath.EncodedPathString, uiSourceCode?: Workspace.UISourceCode.UISourceCode): void { if (uiSourceCode) { const relativePath = Persistence.FileSystemWorkspaceBinding.FileSystemWorkspaceBinding.relativePath(uiSourceCode); relativePath.pop(); path = Common.ParsedURL.ParsedURL.join(relativePath, '/'); } void this.create(project, path, uiSourceCode); } private handleContextMenuRename(node: NavigatorUISourceCodeTreeNode): void { this.rename(node, false); } private async handleContextMenuExclude( project: Workspace.Workspace.Project, path: Platform.DevToolsPath.EncodedPathString): Promise<void> { const shouldExclude = await UI.UIUtils.ConfirmDialog.show(i18nString(UIStrings.areYouSureYouWantToExcludeThis)); if (shouldExclude) { UI.UIUtils.startBatchUpdate(); project.excludeFolder( Persistence.FileSystemWorkspaceBinding.FileSystemWorkspaceBinding.completeURL(project, path)); UI.UIUtils.endBatchUpdate(); } } private async handleContextMenuDelete(uiSourceCode: Workspace.UISourceCode.UISourceCode): Promise<void> { const shouldDelete = await UI.UIUtils.ConfirmDialog.show(i18nString(UIStrings.areYouSureYouWantToDeleteThis)); if (shouldDelete) { uiSourceCode.project().deleteFile(uiSourceCode); } } handleFileContextMenu(event: Event, node: NavigatorUISourceCodeTreeNode): void { const uiSourceCode = node.uiSourceCode(); const contextMenu = new UI.ContextMenu.ContextMenu(event); contextMenu.appendApplicableItems(uiSourceCode); const project = uiSourceCode.project(); if (project.type() === Workspace.Workspace.projectTypes.FileSystem) { contextMenu.editSection().appendItem(i18nString(UIStrings.rename), this.handleContextMenuRename.bind(this, node)); contextMenu.editSection().appendItem( i18nString(UIStrings.makeACopy), this.handleContextMenuCreate.bind(this, project, Platform.DevToolsPath.EmptyEncodedPathString, uiSourceCode)); contextMenu.editSection().appendItem( i18nString(UIStrings.delete), this.handleContextMenuDelete.bind(this, uiSourceCode)); } void contextMenu.show(); } private async handleDeleteOverrides(node: NavigatorTreeNode): Promise<void> { const shouldRemove = await UI.UIUtils.ConfirmDialog.show(i18nString(UIStrings.areYouSureYouWantToDeleteAll)); if (shouldRemove) { this.handleDeleteOverridesHelper(node); } } private handleDeleteOverridesHelper(node: NavigatorTreeNode): void { node.children().forEach(child => { this.handleDeleteOverridesHelper(child); }); if (node instanceof NavigatorUISourceCodeTreeNode) { // Only delete confirmed overrides and not just any file that happens to be in the folder. const binding = Persistence.Persistence.PersistenceImpl.instance().binding(node.uiSourceCode()); if (binding) { node.uiSourceCode().project().deleteFile(node.uiSourceCode()); } } } handleFolderContextMenu(event: Event, node: NavigatorFolderTreeNode): void { const path = node.folderPath || Platform.DevToolsPath.EmptyEncodedPathString; const project = node.project || null; const contextMenu = new UI.ContextMenu.ContextMenu(event); NavigatorView.appendSearchItem(contextMenu, path); if (!project) { return; } if (project.type() === Workspace.Workspace.projectTypes.FileSystem) { const folderPath = Common.ParsedURL.ParsedURL.urlToRawPathString( Persistence.FileSystemWorkspaceBinding.FileSystemWorkspaceBinding.completeURL(project, path), Host.Platform.isWin()); contextMenu.revealSection().appendItem( i18nString(UIStrings.openFolder), () => Host.InspectorFrontendHost.InspectorFrontendHostInstance.showItemInFolder(folderPath)); if (project.canCreateFile()) { contextMenu.defaultSection().appendItem(i18nString(UIStrings.newFile), () => { this.handleContextMenuCreate(project, path, undefined); }); } } else if (node.origin && node.folderPath) { const url = Common.ParsedURL.ParsedURL.concatenate(node.origin, '/', node.folderPath); for (const {text, callback} of Bindings.IgnoreListManager.IgnoreListManager.instance() .getIgnoreListFolderContextMenuItems(url)) { contextMenu.defaultSection().appendItem(text, callback); } } if (project.canExcludeFolder(path)) { contextMenu.defaultSection().appendItem( i18nString(UIStrings.excludeFolder), this.handleContextMenuExclude.bind(this, project, path)); } if (project.type() === Workspace.Workspace.projectTypes.FileSystem) { contextMenu.defaultSection().appendAction('sources.add-folder-to-workspace', undefined, true); if (node instanceof NavigatorGroupTreeNode) { contextMenu.defaultSection().appendItem(i18nString(UIStrings.removeFolderFromWorkspace), async () => { const shouldRemove = await UI.UIUtils.ConfirmDialog.show(i18nString(UIStrings.areYouSureYouWantToRemoveThis)); if (shouldRemove) { project.remove(); } }); } if ((project as Persistence.FileSystemWorkspaceBinding.FileSystem).fileSystem().type() === 'overrides') { contextMenu.defaultSection().appendItem( i18nString(UIStrings.deleteAllOverrides), this.handleDeleteOverrides.bind(this, node)); } } void contextMenu.show(); } rename(node: NavigatorUISourceCodeTreeNode, creatingNewUISourceCode: boolean): void { const uiSourceCode = node.uiSourceCode(); node.rename(callback.bind(this)); function callback(this: NavigatorView, committed: boolean): void { if (!creatingNewUISourceCode) { return; } if (!committed) { uiSourceCode.remove(); } else if (node.treeElement && node.treeElement.listItemElement.hasFocus()) { this.sourceSelected(uiSourceCode, true); } } } async create( project: Workspace.Workspace.Project, path: Platform.DevToolsPath.EncodedPathString, uiSourceCodeToCopy?: Workspace.UISourceCode.UISourceCode): Promise<void> { let content = ''; if (uiSourceCodeToCopy) { content = (await uiSourceCodeToCopy.requestContent()).content || ''; } const uiSourceCode = await project.createFile(path, null, content); if (!uiSourceCode) { return; } this.sourceSelected(uiSourceCode, false); const node = this.revealUISourceCode(uiSourceCode, true); if (node) { this.rename(node, true); } } private groupingChanged(): void { this.reset(true); this.initGrouping(); // Reset the workspace to repopulate filesystem folders. this.resetWorkspace(Workspace.Workspace.WorkspaceImpl.instance()); this.workspaceInternal.uiSourceCodes().forEach(this.addUISourceCode.bind(this)); } private ignoreListChanged(): void { if (Root.Runtime.experiments.isEnabled(Root.Runtime.ExperimentName.JUST_MY_CODE)) { this.groupingChanged(); } else { this.rootNode.updateTitleRecursive(); } } private initGrouping(): void { this.groupByFrame = true; this.groupByDomain = this.navigatorGroupByFolderSetting.get(); this.groupByFolder = this.groupByDomain; if (this.navigatorGroupByAuthoredExperiment) { this.groupByAuthored = Root.Runtime.experiments.isEnabled(this.navigatorGroupByAuthoredExperiment); } else { this.groupByAuthored = false; } } private resetForTest(): void { this.reset(); this.workspaceInternal.uiSourceCodes().forEach(this.addUISourceCode.bind(this)); } private discardFrame(frame: SDK.ResourceTreeModel.ResourceTreeFrame, isAuthored: boolean): void { if (isAuthored) { return; } const node = this.frameNodes.get(frame); if (!node) { return; } if (node.parent) { node.parent.removeChild(node); } this.frameNodes.delete(frame); for (const child of frame.childFrames) { this.discardFrame(child, isAuthored); } } targetAdded(_target: SDK.Target.Target): void { } targetRemoved(target: SDK.Target.Target): void { const rootOrDeployed = this.rootOrDeployedNode(); const targetNode = rootOrDeployed.child('target:' + target.id()); if (targetNode) { rootOrDeployed.removeChild(targetNode); } } private targetNameChanged(event: Common.EventTarget.EventTargetEvent<SDK.Target.Target>): void { const target = event.data; const targetNode = this.rootOrDeployedNode().child('target:' + target.id()); if (targetNode) { targetNode.setTitle(target.name()); } } override wasShown(): void { super.wasShown(); this.scriptsTree.registerCSSFiles([navigatorTreeStyles]); this.registerCSSFiles([navigatorViewStyles]); } } const boostOrderForNode = new WeakSet<UI.TreeOutline.TreeElement>(); export class NavigatorFolderTreeElement extends UI.TreeOutline.TreeElement { private readonly nodeType: string; private readonly navigatorView: NavigatorView; private hoverCallback: ((arg0: boolean) => void)|undefined; node!: NavigatorTreeNode; private hovered?: boolean; private isIgnoreListed?: boolean; constructor(navigatorView: NavigatorView, type: string, title: string, hoverCallback?: ((arg0: boolean) => void)) { super('', true); this.listItemElement.classList.add('navigator-' + type + '-tree-item', 'navigator-folder-tree-item'); UI.ARIAUtils.setAccessibleName(this.listItemElement, `${title}, ${type}`); this.nodeType = type; this.title = title; this.tooltip = title; this.navigatorView = navigatorView; this.hoverCallback = hoverCallback; let iconType = 'folder'; if (type === Types.Domain) { iconType = 'cloud'; } else if (type === Types.Frame) { iconType = 'frame'; } else if (type === Types.Worker) { iconType = 'gears'; } else if (type === Types.Authored) { iconType = 'code'; } else if (type === Types.Deployed) { iconType = 'deployed'; } const icon = new IconButton.Icon.Icon(); const iconPath = new URL(`../../Images/${iconType}.svg`, import.meta.url).toString(); icon.data = {iconPath: iconPath, color: 'var(--override-folder-tree-item-color)', width: '20px', height: '20px'}; this.setLeadingIcons([icon]); } override async onpopulate(): Promise<void> { this.node.populate(); } override onattach(): void { this.collapse(); this.node.onattach(); this.listItemElement.addEventListener('contextmenu', this.handleContextMenuEvent.bind(this), false); this.listItemElement.addEventListener('mousemove', this.mouseMove.bind(this), false); this.listItemElement.addEventListener('mouseleave', this.mouseLeave.bind(this), false); } setIgnoreListed(isIgnoreListed: boolean): void { if (this.isIgnoreListed !== isIgnoreListed) { this.isIgnoreListed = isIgnoreListed; this.listItemElement.classList.toggle('is-ignore-listed', isIgnoreListed); this.updateTooltip(); } } setNode(node: NavigatorTreeNode): void { this.node = node; this.updateTooltip(); UI.ARIAUtils.setAccessibleName(this.listItemElement, `${this.title}, ${this.nodeType}`); } private updateTooltip(): void { if (this.node.tooltip) { this.tooltip = this.node.tooltip; } else { const paths = []; let currentNode: NavigatorTreeNode|null = this.node; while (currentNode && !currentNode.isRoot() && currentNode.type === this.node.type) { paths.push(currentNode.title); currentNode = currentNode.parent; } paths.reverse(); let tooltip = paths.join('/'); if (this.isIgnoreListed) { tooltip = i18nString(UIStrings.sIgnoreListed, {PH1: tooltip}); } this.tooltip = tooltip; } } private handleContextMenuEvent(event: Event): void { if (!this.node) { return; } this.select(); this.navigatorView.handleFolderContextMenu(event, this.node as NavigatorFolderTreeNode); } private mouseMove(_event: Event): void { if (this.hovered || !this.hoverCallback) { return; } this.hovered = true; this.hoverCallback(true); } private mouseLeave(_event: Event): void { if (!this.hoverCallback) { return; } this.hovered = false; this.hoverCallback(false); } } export class NavigatorSourceTreeElement extends UI.TreeOutline.TreeElement { readonly nodeType: string; readonly node: NavigatorUISourceCodeTreeNode; private readonly navigatorView: NavigatorView; uiSourceCodeInternal: Workspace.UISourceCode.UISourceCode; constructor( navigatorView: NavigatorView, uiSourceCode: Workspace.UISourceCode.UISourceCode, title: string, node: NavigatorUISourceCodeTreeNode) { super('', false); this.nodeType = Types.File; this.node = node; this.title = title; this.listItemElement.classList.add( 'navigator-' + uiSourceCode.contentType().name() + '-tree-item', 'navigator-file-tree-item'); this.tooltip = uiSourceCode.url(); UI.ARIAUtils.setAccessibleName(this.listItemElement, `${uiSourceCode.name()}, ${this.nodeType}`); Common.EventTarget.fireEvent('source-tree-file-added', uiSourceCode.fullDisplayName()); this.navigatorView = navigatorView; this.uiSourceCodeInternal = uiSourceCode; this.updateIco