UNPKG

chrome-devtools-frontend

Version:
1,596 lines (1,434 loc) 61.6 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 Bindings from '../bindings/bindings.js'; import * as Common from '../common/common.js'; import * as Host from '../host/host.js'; import * as i18n from '../i18n/i18n.js'; import * as Persistence from '../persistence/persistence.js'; import * as Platform from '../platform/platform.js'; import * as SDK from '../sdk/sdk.js'; import * as Snippets from '../snippets/snippets.js'; import * as UI from '../ui/ui.js'; import * as Workspace from '../workspace/workspace.js'; import {SearchSourcesView} from './SearchSourcesView.js'; export 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 */ 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)', }; const str_ = i18n.i18n.registerUIStrings('sources/NavigatorView.js', UIStrings); const i18nString = i18n.i18n.getLocalizedString.bind(undefined, str_); export const Types = { 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.Domain, 10], [Types.FileSystemFolder, 1], [Types.NetworkFolder, 1], [Types.SourceMapFolder, 2], [Types.File, 10], [Types.Frame, 70], [Types.Worker, 90], [Types.FileSystem, 100], ]); /** * @implements {SDK.SDKModel.Observer} */ export class NavigatorView extends UI.Widget.VBox { constructor() { super(true); this.registerRequiredCSS('sources/navigatorView.css', {enableLegacyPatching: false}); /** @type {?UI.Widget.Widget} */ this._placeholder = null; this._scriptsTree = new UI.TreeOutline.TreeOutlineInShadow(); this._scriptsTree.registerRequiredCSS('sources/navigatorTree.css', {enableLegacyPatching: true}); this._scriptsTree.setComparator(NavigatorView._treeElementsCompare); this._scriptsTree.setFocusable(false); this.contentElement.appendChild(this._scriptsTree.element); this.setDefaultFocusedElement(this._scriptsTree.element); /** @type {!Platform.MapUtilities.Multimap<!Workspace.UISourceCode.UISourceCode, !NavigatorUISourceCodeTreeNode>} */ this._uiSourceCodeNodes = new Platform.MapUtilities.Multimap(); /** @type {!Map.<string, !NavigatorFolderTreeNode>} */ this._subfolderNodes = new Map(); this._rootNode = new NavigatorRootTreeNode(this); this._rootNode.populate(); /** @type {!Map.<!SDK.ResourceTreeModel.ResourceTreeFrame, !NavigatorGroupTreeNode>} */ 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)); 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); SDK.SDKModel.TargetManager.instance().addEventListener( SDK.SDKModel.Events.NameChanged, this._targetNameChanged, this); SDK.SDKModel.TargetManager.instance().observeTargets(this); this._resetWorkspace(Workspace.Workspace.WorkspaceImpl.instance()); this._workspace.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); /** @type {!Workspace.Workspace.WorkspaceImpl} */ this._workspace; } /** * @param {!UI.TreeOutline.TreeElement} treeElement */ static _treeElementOrder(treeElement) { if (boostOrderForNode.has(treeElement)) { return 0; } const actualElement = /** @type {!NavigatorSourceTreeElement} */ (treeElement); 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; } /** * @param {!UI.ContextMenu.ContextMenu} contextMenu * @param {string=} path */ static appendSearchItem(contextMenu, path) { let searchLabel = i18nString(UIStrings.searchInFolder); if (!path || !path.trim()) { path = '*'; searchLabel = i18nString(UIStrings.searchInAllFiles); } contextMenu.viewSection().appendItem(searchLabel, () => { if (path) { SearchSourcesView.openSearch(`file:${path.trim()}`); } }); } /** * @param {!UI.TreeOutline.TreeElement} treeElement1 * @param {!UI.TreeOutline.TreeElement} treeElement2 * @return {number} */ static _treeElementsCompare(treeElement1, treeElement2) { const typeWeight1 = NavigatorView._treeElementOrder(treeElement1); const typeWeight2 = NavigatorView._treeElementOrder(treeElement2); if (typeWeight1 > typeWeight2) { return 1; } if (typeWeight1 < typeWeight2) { return -1; } return Platform.StringUtilities.compare(treeElement1.titleAsText(), treeElement2.titleAsText()); } /** * @param {!UI.Widget.Widget} placeholder */ setPlaceholder(placeholder) { 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)); /** * @this {!NavigatorView} */ function updateVisibility() { const showTree = this._scriptsTree.firstChild(); if (showTree) { placeholder.hideWidget(); } else { placeholder.showWidget(); } this._scriptsTree.element.classList.toggle('hidden', !showTree); } } /** * @param {!Common.EventTarget.EventTargetEvent} event */ _onBindingChanged(event) { const binding = /** @type {!Persistence.Persistence.PersistenceBinding} */ (event.data); // Update UISourceCode titles. const networkNodes = this._uiSourceCodeNodes.get(binding.network); for (const networkNode of networkNodes) { networkNode.updateTitle(); } const fileSystemNodes = this._uiSourceCodeNodes.get(binding.fileSystem); for (const fileSystemNode of fileSystemNodes) { fileSystemNode.updateTitle(); } // Update folder titles. const pathTokens = Persistence.FileSystemWorkspaceBinding.FileSystemWorkspaceBinding.relativePath(binding.fileSystem); let folderPath = ''; for (let i = 0; i < pathTokens.length - 1; ++i) { folderPath += pathTokens[i]; const folderId = this._folderNodeId(binding.fileSystem.project(), null, null, binding.fileSystem.origin(), folderPath); const folderNode = this._subfolderNodes.get(folderId); if (folderNode) { folderNode.updateTitle(); } folderPath += '/'; } // Update fileSystem root title. const fileSystemRoot = this._rootNode.child(binding.fileSystem.project().id()); if (fileSystemRoot) { fileSystemRoot.updateTitle(); } } /** * @override */ focus() { this._scriptsTree.focus(); } /** * Central place to add elements to the tree to * enable focus if the tree has elements * * @param {!UI.TreeOutline.TreeElement} parent * @param {!UI.TreeOutline.TreeElement} child */ appendChild(parent, child) { this._scriptsTree.setFocusable(true); parent.appendChild(child); } /** * Central place to remove elements from the tree to * disable focus if the tree is empty * * @param {!UI.TreeOutline.TreeElement} parent * @param {!UI.TreeOutline.TreeElement} child */ removeChild(parent, child) { parent.removeChild(child); if (this._scriptsTree.rootElement().childCount() === 0) { this._scriptsTree.setFocusable(false); } } /** * @param {!Workspace.Workspace.WorkspaceImpl} workspace */ _resetWorkspace(workspace) { // Clear old event listeners first. if (this._workspace) { this._workspace.removeEventListener(Workspace.Workspace.Events.UISourceCodeAdded, this._uiSourceCodeAdded, this); this._workspace.removeEventListener( Workspace.Workspace.Events.UISourceCodeRemoved, this._uiSourceCodeRemoved, this); this._workspace.removeEventListener(Workspace.Workspace.Events.ProjectAdded, this._projectAddedCallback, this); this._workspace.removeEventListener( Workspace.Workspace.Events.ProjectRemoved, this._projectRemovedCallback, this); } this._workspace = workspace; this._workspace.addEventListener(Workspace.Workspace.Events.UISourceCodeAdded, this._uiSourceCodeAdded, this); this._workspace.addEventListener(Workspace.Workspace.Events.UISourceCodeRemoved, this._uiSourceCodeRemoved, this); this._workspace.addEventListener(Workspace.Workspace.Events.ProjectAdded, this._projectAddedCallback, this); this._workspace.addEventListener(Workspace.Workspace.Events.ProjectRemoved, this._projectRemovedCallback, this); this._workspace.projects().forEach(this._projectAdded.bind(this)); this._computeUniqueFileSystemProjectNames(); } /** * @param {!Common.EventTarget.EventTargetEvent} event */ _projectAddedCallback(event) { const project = /** @type {!Workspace.Workspace.Project} */ (event.data); this._projectAdded(project); if (project.type() === Workspace.Workspace.projectTypes.FileSystem) { this._computeUniqueFileSystemProjectNames(); } } /** * @param {!Common.EventTarget.EventTargetEvent} event */ _projectRemovedCallback(event) { const project = /** @type {!Workspace.Workspace.Project} */ (event.data); this._removeProject(project); if (project.type() === Workspace.Workspace.projectTypes.FileSystem) { this._computeUniqueFileSystemProjectNames(); } } /** * @return {!Workspace.Workspace.WorkspaceImpl} * @protected */ workspace() { return this._workspace; } /** * @param {!Workspace.Workspace.Project} project * @return {boolean} */ acceptProject(project) { return !project.isServiceProject(); } /** * @param {!Common.EventTarget.EventTargetEvent} event */ _frameAttributionAdded(event) { const uiSourceCode = /** @type {!Workspace.UISourceCode.UISourceCode} */ (event.data.uiSourceCode); if (!this._acceptsUISourceCode(uiSourceCode)) { return; } const addedFrame = /** @type {?SDK.ResourceTreeModel.ResourceTreeFrame} */ (event.data.frame); // This event does not happen for UISourceCodes without initial attribution. this._addUISourceCodeNode(uiSourceCode, addedFrame); } /** * @param {!Common.EventTarget.EventTargetEvent} event */ _frameAttributionRemoved(event) { const uiSourceCode = /** @type {!Workspace.UISourceCode.UISourceCode} */ (event.data.uiSourceCode); if (!this._acceptsUISourceCode(uiSourceCode)) { return; } const removedFrame = /** @type {?SDK.ResourceTreeModel.ResourceTreeFrame} */ (event.data.frame); const node = Array.from(this._uiSourceCodeNodes.get(uiSourceCode)).find(node => node.frame() === removedFrame); this._removeUISourceCodeNode(/** @type {!NavigatorUISourceCodeTreeNode} */ (node)); } /** * @param {!Workspace.UISourceCode.UISourceCode} uiSourceCode * @return {boolean} */ _acceptsUISourceCode(uiSourceCode) { return this.acceptProject(uiSourceCode.project()); } /** * @param {!Workspace.UISourceCode.UISourceCode} uiSourceCode */ _addUISourceCode(uiSourceCode) { 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); } /** * @param {!Workspace.UISourceCode.UISourceCode} uiSourceCode * @param {?SDK.ResourceTreeModel.ResourceTreeFrame} frame */ _addUISourceCodeNode(uiSourceCode, frame) { const isFromSourceMap = uiSourceCode.contentType().isFromSourceMap(); let path; if (uiSourceCode.project().type() === Workspace.Workspace.projectTypes.FileSystem) { path = Persistence.FileSystemWorkspaceBinding.FileSystemWorkspaceBinding.relativePath(uiSourceCode).slice(0, -1); } else { path = Common.ParsedURL.ParsedURL.extractPath(uiSourceCode.url()).split('/').slice(1, -1); } 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); folderNode.appendChild(uiSourceCodeNode); this._uiSourceCodeNodes.set(uiSourceCode, uiSourceCodeNode); this._selectDefaultTreeNode(); } /** * @param {!Workspace.UISourceCode.UISourceCode} uiSourceCode */ uiSourceCodeAdded(uiSourceCode) { } /** * @param {!Common.EventTarget.EventTargetEvent} event */ _uiSourceCodeAdded(event) { const uiSourceCode = /** @type {!Workspace.UISourceCode.UISourceCode} */ (event.data); this._addUISourceCode(uiSourceCode); } /** * @param {!Common.EventTarget.EventTargetEvent} event */ _uiSourceCodeRemoved(event) { const uiSourceCode = /** @type {!Workspace.UISourceCode.UISourceCode} */ (event.data); this._removeUISourceCode(uiSourceCode); } /** * @protected * @param {!Workspace.Workspace.Project} project */ tryAddProject(project) { this._projectAdded(project); project.uiSourceCodes().forEach(this._addUISourceCode.bind(this)); } /** * @param {!Workspace.Workspace.Project} project */ _projectAdded(project) { if (!this.acceptProject(project) || project.type() !== Workspace.Workspace.projectTypes.FileSystem || Snippets.ScriptSnippetFileSystem.isSnippetsProject(project) || this._rootNode.child(project.id())) { return; } this._rootNode.appendChild( new NavigatorGroupTreeNode(this, project, project.id(), Types.FileSystem, project.displayName())); this._selectDefaultTreeNode(); } // TODO(einbinder) remove this code after crbug.com/964075 is fixed _selectDefaultTreeNode() { const children = this._rootNode.children(); if (children.length && !this._scriptsTree.selectedTreeElement) { children[0].treeNode().select(true /* omitFocus */, false /* selectedByUser */); } } _computeUniqueFileSystemProjectNames() { const fileSystemProjects = this._workspace.projectsForType(Workspace.Workspace.projectTypes.FileSystem); if (!fileSystemProjects.length) { return; } const encoder = new Persistence.Persistence.PathEncoder(); const reversedPaths = fileSystemProjects.map(project => { const fileSystem = /** @type {!Persistence.FileSystemWorkspaceBinding.FileSystem} */ (project); return Platform.StringUtilities.reverse(encoder.encode(fileSystem.fileSystemPath())); }); const reversedIndex = new Common.Trie.Trie(); for (const reversedPath of reversedPaths) { reversedIndex.add(reversedPath); } 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.substring(0, commonPrefix.length + 1); const path = encoder.decode(Platform.StringUtilities.reverse(prefixPath)); const fileSystemNode = this._rootNode.child(project.id()); if (fileSystemNode) { fileSystemNode.setTitle(path); } } } /** * @param {!Workspace.Workspace.Project} project */ _removeProject(project) { const uiSourceCodes = project.uiSourceCodes(); for (let i = 0; i < uiSourceCodes.length; ++i) { this._removeUISourceCode(uiSourceCodes[i]); } if (project.type() !== Workspace.Workspace.projectTypes.FileSystem) { return; } const fileSystemNode = this._rootNode.child(project.id()); if (!fileSystemNode) { return; } this._rootNode.removeChild(fileSystemNode); } /** * @param {!Workspace.Workspace.Project} project * @param {?SDK.SDKModel.Target} target * @param {?SDK.ResourceTreeModel.ResourceTreeFrame} frame * @param {string} projectOrigin * @param {string} path * @return {string} */ _folderNodeId(project, target, frame, projectOrigin, path) { const targetId = target ? target.id() : ''; const projectId = project.type() === Workspace.Workspace.projectTypes.FileSystem ? project.id() : ''; const frameId = this._groupByFrame && frame ? frame.id : ''; return targetId + ':' + projectId + ':' + frameId + ':' + projectOrigin + ':' + path; } /** * @param {!Workspace.UISourceCode.UISourceCode} uiSourceCode * @param {!Workspace.Workspace.Project} project * @param {?SDK.SDKModel.Target} target * @param {?SDK.ResourceTreeModel.ResourceTreeFrame} frame * @param {string} projectOrigin * @param {!Array<string>} path * @param {boolean} fromSourceMap * @return {!NavigatorTreeNode} */ _folderNode(uiSourceCode, project, target, frame, projectOrigin, path, fromSourceMap) { if (Snippets.ScriptSnippetFileSystem.isSnippetsUISourceCode(uiSourceCode)) { return this._rootNode; } if (target && !this._groupByFolder && !fromSourceMap) { return this._domainNode(uiSourceCode, project, target, frame, projectOrigin); } const folderPath = path.join('/'); const folderId = this._folderNodeId(project, target, frame, projectOrigin, 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 /** @type {!NavigatorTreeNode} */ (this._rootNode.child(project.id())); } const parentNode = this._folderNode(uiSourceCode, project, target, frame, projectOrigin, path.slice(0, -1), fromSourceMap); let type = fromSourceMap ? Types.SourceMapFolder : Types.NetworkFolder; if (project.type() === Workspace.Workspace.projectTypes.FileSystem) { type = Types.FileSystemFolder; } const name = path[path.length - 1]; folderNode = new NavigatorFolderTreeNode(this, project, folderId, type, folderPath, name); this._subfolderNodes.set(folderId, folderNode); parentNode.appendChild(folderNode); return folderNode; } /** * @param {!Workspace.UISourceCode.UISourceCode} uiSourceCode * @param {!Workspace.Workspace.Project} project * @param {!SDK.SDKModel.Target} target * @param {?SDK.ResourceTreeModel.ResourceTreeFrame} frame * @param {string} projectOrigin * @return {!NavigatorTreeNode} */ _domainNode(uiSourceCode, project, target, frame, projectOrigin) { const frameNode = this._frameNode(project, target, frame); 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); return domainNode; } /** * @param {!Workspace.Workspace.Project} project * @param {!SDK.SDKModel.Target} target * @param {?SDK.ResourceTreeModel.ResourceTreeFrame} frame * @return {!NavigatorTreeNode} */ _frameNode(project, target, frame) { if (!this._groupByFrame || !frame) { return this._targetNode(project, target); } 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) .appendChild(frameNode); if (!parentFrame) { boostOrderForNode.add(frameNode.treeNode()); frameNode.treeNode().expand(); } /** * @param {boolean} hovered */ function hoverCallback(hovered) { if (hovered) { const overlayModel = target.model(SDK.OverlayModel.OverlayModel); if (overlayModel && frame) { overlayModel.highlightFrame(frame.id); } } else { SDK.OverlayModel.OverlayModel.hideDOMNodeHighlight(); } } return frameNode; } /** * @param {!Workspace.Workspace.Project} project * @param {!SDK.SDKModel.Target} target * @return {!NavigatorTreeNode} */ _targetNode(project, target) { if (target === SDK.SDKModel.TargetManager.instance().mainTarget()) { return this._rootNode; } let targetNode = this._rootNode.child('target:' + target.id()); if (!targetNode) { targetNode = new NavigatorGroupTreeNode( this, project, 'target:' + target.id(), target.type() === SDK.SDKModel.Type.Frame ? Types.Frame : Types.Worker, target.name()); this._rootNode.appendChild(targetNode); } return targetNode; } /** * @param {!SDK.SDKModel.Target} target * @param {string} projectOrigin * @return {string} */ _computeProjectDisplayName(target, projectOrigin) { 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); } /** * @param {!Workspace.UISourceCode.UISourceCode} uiSourceCode * @param {boolean=} select * @return {?NavigatorUISourceCodeTreeNode} */ revealUISourceCode(uiSourceCode, select) { 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; } /** * @param {!Workspace.UISourceCode.UISourceCode} uiSourceCode * @param {boolean} focusSource */ _sourceSelected(uiSourceCode, focusSource) { this._lastSelectedUISourceCode = uiSourceCode; Common.Revealer.reveal(uiSourceCode, !focusSource); } /** * @param {!Workspace.UISourceCode.UISourceCode} uiSourceCode */ _removeUISourceCode(uiSourceCode) { const nodes = this._uiSourceCodeNodes.get(uiSourceCode); for (const node of nodes) { this._removeUISourceCodeNode(node); } } /** * @param {!NavigatorUISourceCodeTreeNode} node */ _removeUISourceCodeNode(node) { 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 = node.parent; if (!parentNode) { return; } parentNode.removeChild(node); let currentNode = /** @type {?NavigatorUISourceCodeTreeNode} */ (parentNode); while (currentNode) { parentNode = currentNode.parent; if (!parentNode || !currentNode.isEmpty()) { break; } if (parentNode === this._rootNode && project.type() === Workspace.Workspace.projectTypes.FileSystem) { break; } if (!(currentNode instanceof NavigatorGroupTreeNode || currentNode instanceof NavigatorFolderTreeNode)) { break; } if (currentNode._type === Types.Frame) { this._discardFrame(/** @type {!SDK.ResourceTreeModel.ResourceTreeFrame} */ (frame)); break; } const folderId = this._folderNodeId( project, target, frame, uiSourceCode.origin(), currentNode instanceof NavigatorFolderTreeNode && currentNode._folderPath || ''); this._subfolderNodes.delete(folderId); parentNode.removeChild(currentNode); currentNode = /** @type {?NavigatorUISourceCodeTreeNode} */ (parentNode); } } reset() { 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(); // Reset the workspace to repopulate filesystem folders. this._resetWorkspace(Workspace.Workspace.WorkspaceImpl.instance()); } /** * @param {!Event} event */ handleContextMenu(event) { } /** * @return {!Promise<boolean>} */ async _renameShortcut() { const selectedTreeElement = /** @type {?NavigatorSourceTreeElement} */ (this._scriptsTree.selectedTreeElement); const node = selectedTreeElement && selectedTreeElement._node; if (!node || !node._uiSourceCode || !node._uiSourceCode.canRename()) { return false; } this.rename(node, false); return true; } /** * @param {!Workspace.Workspace.Project} project * @param {string} path * @param {!Workspace.UISourceCode.UISourceCode=} uiSourceCode */ _handleContextMenuCreate(project, path, uiSourceCode) { if (uiSourceCode) { const relativePath = Persistence.FileSystemWorkspaceBinding.FileSystemWorkspaceBinding.relativePath(uiSourceCode); relativePath.pop(); path = relativePath.join('/'); } this.create(project, path, uiSourceCode); } /** * @param {!NavigatorUISourceCodeTreeNode} node */ _handleContextMenuRename(node) { this.rename(node, false); } /** * @param {!Workspace.Workspace.Project} project * @param {string} path */ _handleContextMenuExclude(project, path) { const shouldExclude = window.confirm(i18nString(UIStrings.areYouSureYouWantToExcludeThis)); if (shouldExclude) { UI.UIUtils.startBatchUpdate(); project.excludeFolder( Persistence.FileSystemWorkspaceBinding.FileSystemWorkspaceBinding.completeURL(project, path)); UI.UIUtils.endBatchUpdate(); } } /** * @param {!Workspace.UISourceCode.UISourceCode} uiSourceCode */ _handleContextMenuDelete(uiSourceCode) { const shouldDelete = window.confirm(i18nString(UIStrings.areYouSureYouWantToDeleteThis)); if (shouldDelete) { uiSourceCode.project().deleteFile(uiSourceCode); } } /** * @param {!Event} event * @param {!NavigatorUISourceCodeTreeNode} node */ handleFileContextMenu(event, node) { 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, '', uiSourceCode)); contextMenu.editSection().appendItem( i18nString(UIStrings.delete), this._handleContextMenuDelete.bind(this, uiSourceCode)); } contextMenu.show(); } /** * @param {!NavigatorTreeNode} node */ _handleDeleteOverrides(node) { const shouldRemove = window.confirm(i18nString(UIStrings.areYouSureYouWantToDeleteAll)); if (shouldRemove) { this._handleDeleteOverridesHelper(node); } } /** * @param {!NavigatorTreeNode} node */ _handleDeleteOverridesHelper(node) { 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()); } } } /** * @param {!Event} event * @param {!NavigatorTreeNode} node */ handleFolderContextMenu(event, node) { const path = /** @type {!NavigatorFolderTreeNode} */ (node)._folderPath || ''; const project = /** @type {!NavigatorFolderTreeNode} */ (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.urlToPlatformPath( 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); }); } } 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), () => { const shouldRemove = window.confirm(i18nString(UIStrings.areYouSureYouWantToRemoveThis)); if (shouldRemove) { project.remove(); } }); } if (/** @type {!Persistence.FileSystemWorkspaceBinding.FileSystem} */ (project).fileSystem().type() === 'overrides') { contextMenu.defaultSection().appendItem( i18nString(UIStrings.deleteAllOverrides), this._handleDeleteOverrides.bind(this, node)); } } contextMenu.show(); } /** * @param {!NavigatorUISourceCodeTreeNode} node * @param {boolean} creatingNewUISourceCode */ rename(node, creatingNewUISourceCode) { const uiSourceCode = node.uiSourceCode(); node.rename(callback.bind(this)); /** * @this {NavigatorView} * @param {boolean} committed */ function callback(committed) { if (!creatingNewUISourceCode) { return; } if (!committed) { uiSourceCode.remove(); } else if (node._treeElement && node._treeElement.listItemElement.hasFocus()) { this._sourceSelected(uiSourceCode, true); } } } /** * @param {!Workspace.Workspace.Project} project * @param {string} path * @param {!Workspace.UISourceCode.UISourceCode=} uiSourceCodeToCopy */ async create(project, path, uiSourceCodeToCopy) { 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); } } _groupingChanged() { this.reset(); this._initGrouping(); this._workspace.uiSourceCodes().forEach(this._addUISourceCode.bind(this)); } _initGrouping() { this._groupByFrame = true; this._groupByDomain = this._navigatorGroupByFolderSetting.get(); this._groupByFolder = this._groupByDomain; } _resetForTest() { this.reset(); this._workspace.uiSourceCodes().forEach(this._addUISourceCode.bind(this)); } /** * @param {!SDK.ResourceTreeModel.ResourceTreeFrame} frame */ _discardFrame(frame) { 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); } } /** * @override * @param {!SDK.SDKModel.Target} target */ targetAdded(target) { } /** * @override * @param {!SDK.SDKModel.Target} target */ targetRemoved(target) { const targetNode = this._rootNode.child('target:' + target.id()); if (targetNode) { this._rootNode.removeChild(targetNode); } } /** * @param {!Common.EventTarget.EventTargetEvent} event */ _targetNameChanged(event) { const target = /** @type {!SDK.SDKModel.Target} */ (event.data); const targetNode = this._rootNode.child('target:' + target.id()); if (targetNode) { targetNode.setTitle(target.name()); } } } /** @type {!WeakSet<!UI.TreeOutline.TreeElement>} */ const boostOrderForNode = new WeakSet(); export class NavigatorFolderTreeElement extends UI.TreeOutline.TreeElement { /** * @param {!NavigatorView} navigatorView * @param {string} type * @param {string} title * @param {function(boolean)=} hoverCallback */ constructor(navigatorView, type, title, hoverCallback) { 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 = 'largeicon-navigator-folder'; if (type === Types.Domain) { iconType = 'largeicon-navigator-domain'; } else if (type === Types.Frame) { iconType = 'largeicon-navigator-frame'; } else if (type === Types.Worker) { iconType = 'largeicon-navigator-worker'; } this.setLeadingIcons([UI.Icon.Icon.create(iconType, 'icon')]); /** @type {!NavigatorTreeNode} */ this._node; } /** * @override * @returns {!Promise<void>} */ async onpopulate() { this._node.populate(); } /** * @override */ onattach() { 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); } /** * @param {!NavigatorTreeNode} node */ setNode(node) { this._node = node; const paths = []; /** @type {?NavigatorTreeNode} */ let currentNode = node; while (currentNode && !currentNode.isRoot()) { paths.push(currentNode._title); currentNode = currentNode.parent; } paths.reverse(); this.tooltip = paths.join('/'); UI.ARIAUtils.setAccessibleName(this.listItemElement, `${this.title}, ${this._nodeType}`); } /** * @param {!Event} event */ _handleContextMenuEvent(event) { if (!this._node) { return; } this.select(); this._navigatorView.handleFolderContextMenu(event, this._node); } /** * @param {!Event} event */ _mouseMove(event) { if (this._hovered || !this._hoverCallback) { return; } this._hovered = true; this._hoverCallback(true); } /** * @param {!Event} event */ _mouseLeave(event) { if (!this._hoverCallback) { return; } this._hovered = false; this._hoverCallback(false); } } export class NavigatorSourceTreeElement extends UI.TreeOutline.TreeElement { /** * @param {!NavigatorView} navigatorView * @param {!Workspace.UISourceCode.UISourceCode} uiSourceCode * @param {string} title * @param {!NavigatorUISourceCodeTreeNode} node */ constructor(navigatorView, uiSourceCode, title, node) { 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._uiSourceCode = uiSourceCode; this.updateIcon(); } updateIcon() { const binding = Persistence.Persistence.PersistenceImpl.instance().binding(this._uiSourceCode); if (binding) { const container = document.createElement('span'); container.classList.add('icon-stack'); let iconType = 'largeicon-navigator-file-sync'; if (Snippets.ScriptSnippetFileSystem.isSnippetsUISourceCode(binding.fileSystem)) { iconType = 'largeicon-navigator-snippet'; } const icon = UI.Icon.Icon.create(iconType, 'icon'); const badge = UI.Icon.Icon.create('badge-navigator-file-sync', 'icon-badge'); // TODO(allada) This does not play well with dark theme. Add an actual icon and use it. if (Persistence.NetworkPersistenceManager.NetworkPersistenceManager.instance().project() === binding.fileSystem.project()) { badge.style.filter = 'hue-rotate(160deg)'; } container.appendChild(icon); container.appendChild(badge); UI.Tooltip.Tooltip.install( container, Persistence.PersistenceUtils.PersistenceUtils.tooltipForUISourceCode(this._uiSourceCode)); this.setLeadingIcons([/** @type {!UI.Icon.Icon} */ (container)]); } else { let iconType = 'largeicon-navigator-file'; if (Snippets.ScriptSnippetFileSystem.isSnippetsUISourceCode(this._uiSourceCode)) { iconType = 'largeicon-navigator-snippet'; } const defaultIcon = UI.Icon.Icon.create(iconType, 'icon'); this.setLeadingIcons([defaultIcon]); } } /** * @return {!Workspace.UISourceCode.UISourceCode} */ get uiSourceCode() { return this._uiSourceCode; } /** * @override */ onattach() { this.listItemElement.draggable = true; this.listItemElement.addEventListener('click', this._onclick.bind(this), false); this.listItemElement.addEventListener('contextmenu', this._handleContextMenuEvent.bind(this), false); this.listItemElement.addEventListener('dragstart', this._ondragstart.bind(this), false); } _shouldRenameOnMouseDown() { if (!this._uiSourceCode.canRename()) { return false; } if (!this.treeOutline) { return false; } const isSelected = this === this.treeOutline.selectedTreeElement; return isSelected && this.treeOutline.element.hasFocus() && !UI.UIUtils.isBeingEdited(this.treeOutline.element); } /** * @override * @param {!MouseEvent} event */ selectOnMouseDown(event) { if (event.which !== 1 || !this._shouldRenameOnMouseDown()) { super.selectOnMouseDown(event); return; } setTimeout(rename.bind(this), 300); /** * @this {NavigatorSourceTreeElement} */ function rename() { if (this._shouldRenameOnMouseDown()) { this._navigatorView.rename(this._node, false); } } } /** * @param {!DragEvent} event */ _ondragstart(event) { if (!event.dataTransfer) { return; } event.dataTransfer.setData('text/plain', this._uiSourceCode.url()); event.dataTransfer.effectAllowed = 'copy'; } /** * @override * @return {boolean} */ onspace() { this._navigatorView._sourceSelected(this.uiSourceCode, true); return true; } /** * @param {!Event} event */ _onclick(event) { this._navigatorView._sourceSelected(this.uiSourceCode, false); } /** * @override * @param {!Event} event * @return {boolean} */ ondblclick(event) { const middleClick = /** @type {!MouseEvent} */ (event).button === 1; this._navigatorView._sourceSelected(this.uiSourceCode, !middleClick); return false; } /** * @override * @return {boolean} */ onenter() { this._navigatorView._sourceSelected(this.uiSourceCode, true); return true; } /** * @override * @return {boolean} */ ondelete() { return true; } /** * @param {!Event} event */ _handleContextMenuEvent(event) { this.select(); this._navigatorView.handleFileContextMenu(event, this._node); } } export class NavigatorTreeNode { /** * @param {!NavigatorView} navigatorView * @param {string} id * @param {string} type */ constructor(navigatorView, id, type) { this.id = id; this._navigatorView = navigatorView; this._type = type; /** @type {!Map.<string, !NavigatorTreeNode>} */ this._children = new Map(); this._populated = false; this._isMerged = false; /** @type {?NavigatorTreeNode} */ this.parent; /** @type {string} */ this._title; } /** * @return {!UI.TreeOutline.TreeElement} */ treeNode() { throw 'Not implemented'; } dispose() { } updateTitle() { } /** * @return {boolean} */ isRoot() { return false; } /** * @return {boolean} */ hasChildren() { return true; } onattach() { } /** * @param {string} title */ setTitle(title) { throw 'Not implemented'; } populate() { if (this.isPopulated()) { return; } if (this.parent) { this.parent.populate(); } this._populated = true; this.wasPopulated(); } wasPopulated() { const children = this.children(); for (let i = 0; i < children.length; ++i) { this._navigatorView.appendChild( this.treeNode(), /** @type {!UI.TreeOutline.TreeElement} */ (children[i].treeNode())); } } /** * @param {!NavigatorTreeNode} node */ didAddChild(node) { if (this.isPopulated()) { this._navigatorView.appendChild(this.treeNode(), /** @type {!UI.TreeOutline.TreeElement} */ (node.treeNode())); } } /** * @param {!NavigatorTreeNode} node */ willRemoveChild(node) { if (this.isPopulated()) { this._navigatorView.removeChild(this.treeNode(), /** @type {!UI.TreeOutline.TreeElement} */ (node.treeNode())); } } /** * @return {boolean} */ isPopulated() { return this._populated; } /** * @return {boolean} */ isEmpty() { return !this._children.size; } /** * @return {!Array.<!NavigatorTreeNode>} */ children() { return [...this._children.values()]; } /** * @param {string} id * @return {?NavigatorTreeNode} */ child(id) { return this._children.get(id) || null; } /** * @param {!NavigatorTreeNode} node */ appendChild(node) { this._children.set(node.id, node); node.parent = this; this.didAddChild(node); } /** * @param {!NavigatorTreeNode} node */ removeChild(node) { this.willRemoveChild(node); this._children.delete(node.id); node.parent = null; node.dispose(); } reset() { this._children.clear(); } } export class NavigatorRootTreeNode extends NavigatorTreeNode { /** * @param {!NavigatorView} navigatorView */ constructor(navigatorView) { super(navigatorView, '', Types.Root); } /** * @override * @return {boolean} */ isRoot() { return true; } /** * @override * @return {!UI.TreeOutline.TreeElement} */ treeNode() { return this._navigatorView._scriptsTree.rootElement(); } } export class NavigatorUISourceCodeTreeNode extends NavigatorTreeNode { /** * @param {!NavigatorView} navigatorView * @param {!Workspace.UISourceCode.UISourceCode} uiSourceCode * @param {?SDK.ResourceTreeModel.ResourceTreeFrame} frame */ constructor(navigatorView, uiSourceCode, frame) { super(navigatorView, uiSourceCode.project().id() + ':' + uiSourceCode.url(), Types.File); this._uiSourceCode = uiSourceCode; this._treeElement = null;