UNPKG

@dcloudio/uni-debugger

Version:

uni-app debugger

1,677 lines (1,490 loc) 49.7 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: * * 1. Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * 2. Redistributions in binary form must reproduce the above * copyright notice, this list of conditions and the following disclaimer * in the documentation and/or other materials provided with the * distribution. * * THIS SOFTWARE IS PROVIDED BY GOOGLE INC. AND ITS CONTRIBUTORS * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GOOGLE INC. * OR ITS CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ /** * @implements {SDK.TargetManager.Observer} * @unrestricted */ Sources.NavigatorView = class extends UI.VBox { constructor() { super(true); this.registerRequiredCSS('sources/navigatorView.css'); this._scriptsTree = new UI.TreeOutlineInShadow(); this._scriptsTree.registerRequiredCSS('sources/navigatorTree.css'); this._scriptsTree.setComparator(Sources.NavigatorView._treeElementsCompare); this.contentElement.appendChild(this._scriptsTree.element); this.setDefaultFocusedElement(this._scriptsTree.element); /** @type {!Multimap<!Workspace.UISourceCode, !Sources.NavigatorUISourceCodeTreeNode>} */ this._uiSourceCodeNodes = new Multimap(); /** @type {!Map.<string, !Sources.NavigatorFolderTreeNode>} */ this._subfolderNodes = new Map(); this._rootNode = new Sources.NavigatorRootTreeNode(this); this._rootNode.populate(); /** @type {!Map.<!SDK.ResourceTreeFrame, !Sources.NavigatorGroupTreeNode>} */ this._frameNodes = new Map(); this.contentElement.addEventListener('contextmenu', this.handleContextMenu.bind(this), false); UI.shortcutRegistry.addShortcutListener( this.contentElement, 'sources.rename', this._renameShortcut.bind(this), true); this._navigatorGroupByFolderSetting = Common.moduleSetting('navigatorGroupByFolder'); this._navigatorGroupByFolderSetting.addChangeListener(this._groupingChanged.bind(this)); this._initGrouping(); Persistence.persistence.addEventListener( Persistence.Persistence.Events.BindingCreated, this._onBindingChanged, this); Persistence.persistence.addEventListener( Persistence.Persistence.Events.BindingRemoved, this._onBindingChanged, this); SDK.targetManager.addEventListener(SDK.TargetManager.Events.NameChanged, this._targetNameChanged, this); SDK.targetManager.observeTargets(this); this._resetWorkspace(Workspace.workspace); this._workspace.uiSourceCodes().forEach(this._addUISourceCode.bind(this)); Bindings.networkProjectManager.addEventListener( Bindings.NetworkProjectManager.Events.FrameAttributionAdded, this._frameAttributionAdded, this); Bindings.networkProjectManager.addEventListener( Bindings.NetworkProjectManager.Events.FrameAttributionRemoved, this._frameAttributionRemoved, this); } /** * @param {!UI.TreeElement} treeElement */ static _treeElementOrder(treeElement) { if (treeElement._boostOrder) return 0; if (!Sources.NavigatorView._typeOrders) { const weights = {}; const types = Sources.NavigatorView.Types; weights[types.Root] = 1; weights[types.Domain] = 10; weights[types.FileSystemFolder] = 1; weights[types.NetworkFolder] = 1; weights[types.SourceMapFolder] = 2; weights[types.File] = 10; weights[types.Frame] = 70; weights[types.Worker] = 90; weights[types.FileSystem] = 100; Sources.NavigatorView._typeOrders = weights; } let order = Sources.NavigatorView._typeOrders[treeElement._nodeType]; if (treeElement._uiSourceCode) { const contentType = treeElement._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 */ static appendAddFolderItem(contextMenu) { function addFolder() { Persistence.isolatedFileSystemManager.addFileSystem(); } const addFolderLabel = Common.UIString('Add folder to workspace'); contextMenu.defaultSection().appendItem(addFolderLabel, addFolder); } /** * @param {!UI.ContextMenu} contextMenu * @param {string=} path */ static appendSearchItem(contextMenu, path) { function searchPath() { Sources.SearchSourcesView.openSearch(`file:${path.trim()}`); } let searchLabel = Common.UIString('Search in folder'); if (!path || !path.trim()) { path = '*'; searchLabel = Common.UIString('Search in all files'); } contextMenu.viewSection().appendItem(searchLabel, searchPath); } /** * @param {!UI.TreeElement} treeElement1 * @param {!UI.TreeElement} treeElement2 * @return {number} */ static _treeElementsCompare(treeElement1, treeElement2) { const typeWeight1 = Sources.NavigatorView._treeElementOrder(treeElement1); const typeWeight2 = Sources.NavigatorView._treeElementOrder(treeElement2); if (typeWeight1 > typeWeight2) return 1; if (typeWeight1 < typeWeight2) return -1; return treeElement1.titleAsText().compareTo(treeElement2.titleAsText()); } /** * @param {!Common.Event} event */ _onBindingChanged(event) { const binding = /** @type {!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.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(); } /** * @param {!Workspace.Workspace} workspace */ _resetWorkspace(workspace) { 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, event => { const project = /** @type {!Workspace.Project} */ (event.data); this._projectAdded(project); if (project.type() === Workspace.projectTypes.FileSystem) this._computeUniqueFileSystemProjectNames(); }); this._workspace.addEventListener(Workspace.Workspace.Events.ProjectRemoved, event => { const project = /** @type {!Workspace.Project} */ (event.data); this._removeProject(project); if (project.type() === Workspace.projectTypes.FileSystem) this._computeUniqueFileSystemProjectNames(); }); this._workspace.projects().forEach(this._projectAdded.bind(this)); this._computeUniqueFileSystemProjectNames(); } /** * @return {!Workspace.Workspace} * @protected */ workspace() { return this._workspace; } /** * @param {!Workspace.Project} project * @return {boolean} */ acceptProject(project) { return !project.isServiceProject(); } /** * @param {!Common.Event} event */ _frameAttributionAdded(event) { const uiSourceCode = /** @type {!Workspace.UISourceCode} */ (event.data.uiSourceCode); if (!this._acceptsUISourceCode(uiSourceCode)) return; const addedFrame = /** @type {?SDK.ResourceTreeFrame} */ (event.data.frame); // This event does not happen for UISourceCodes without initial attribution. this._addUISourceCodeNode(uiSourceCode, addedFrame); } /** * @param {!Common.Event} event */ _frameAttributionRemoved(event) { const uiSourceCode = /** @type {!Workspace.UISourceCode} */ (event.data.uiSourceCode); if (!this._acceptsUISourceCode(uiSourceCode)) return; const removedFrame = /** @type {?SDK.ResourceTreeFrame} */ (event.data.frame); const node = Array.from(this._uiSourceCodeNodes.get(uiSourceCode)).find(node => node.frame() === removedFrame); this._removeUISourceCodeNode(node); } /** * @param {!Workspace.UISourceCode} uiSourceCode * @return {boolean} */ _acceptsUISourceCode(uiSourceCode) { return this.acceptProject(uiSourceCode.project()); } /** * @param {!Workspace.UISourceCode} uiSourceCode */ _addUISourceCode(uiSourceCode) { if (!this._acceptsUISourceCode(uiSourceCode)) return; if(['Runtime.Android.js','Runtime.iOS.js'].indexOf(uiSourceCode.name())!==-1){ return; } const frames = Bindings.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 * @param {?SDK.ResourceTreeFrame} frame */ _addUISourceCodeNode(uiSourceCode, frame) { const isFromSourceMap = uiSourceCode.contentType().isFromSourceMap(); let path; if (uiSourceCode.project().type() === Workspace.projectTypes.FileSystem) path = Persistence.FileSystemWorkspaceBinding.relativePath(uiSourceCode).slice(0, -1); else path = Common.ParsedURL.extractPath(uiSourceCode.url()).split('/').slice(1, -1); const project = uiSourceCode.project(); const target = Bindings.NetworkProject.targetForUISourceCode(uiSourceCode); const folderNode = this._folderNode(uiSourceCode, project, target, frame, uiSourceCode.origin(), path, isFromSourceMap); const uiSourceCodeNode = new Sources.NavigatorUISourceCodeTreeNode(this, uiSourceCode, frame); folderNode.appendChild(uiSourceCodeNode); this._uiSourceCodeNodes.set(uiSourceCode, uiSourceCodeNode); } /** * @param {!Workspace.UISourceCode} uiSourceCode */ uiSourceCodeAdded(uiSourceCode) { } /** * @param {!Common.Event} event */ _uiSourceCodeAdded(event) { const uiSourceCode = /** @type {!Workspace.UISourceCode} */ (event.data); this._addUISourceCode(uiSourceCode); } /** * @param {!Common.Event} event */ _uiSourceCodeRemoved(event) { const uiSourceCode = /** @type {!Workspace.UISourceCode} */ (event.data); this._removeUISourceCode(uiSourceCode); } /** * @protected * @param {!Workspace.Project} project */ tryAddProject(project) { this._projectAdded(project); project.uiSourceCodes().forEach(this._addUISourceCode.bind(this)); } /** * @param {!Workspace.Project} project */ _projectAdded(project) { if (!this.acceptProject(project) || project.type() !== Workspace.projectTypes.FileSystem || this._rootNode.child(project.id())) return; this._rootNode.appendChild(new Sources.NavigatorGroupTreeNode( this, project, project.id(), Sources.NavigatorView.Types.FileSystem, project.displayName())); } _computeUniqueFileSystemProjectNames() { const fileSystemProjects = this._workspace.projectsForType(Workspace.projectTypes.FileSystem); if (!fileSystemProjects.length) return; const encoder = new Persistence.PathEncoder(); const reversedPaths = fileSystemProjects.map(project => { const fileSystem = /** @type {!Persistence.FileSystemWorkspaceBinding.FileSystem} */ (project); return encoder.encode(fileSystem.fileSystemPath()).reverse(); }); const reversedIndex = new Common.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 path = encoder.decode(reversedPath.substring(0, commonPrefix.length + 1).reverse()); const fileSystemNode = this._rootNode.child(project.id()); if (fileSystemNode) fileSystemNode.setTitle(path); } } /** * @param {!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.projectTypes.FileSystem) return; const fileSystemNode = this._rootNode.child(project.id()); if (!fileSystemNode) return; this._rootNode.removeChild(fileSystemNode); } /** * @param {!Workspace.Project} project * @param {?SDK.Target} target * @param {?SDK.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.projectTypes.FileSystem ? project.id() : ''; const frameId = this._groupByFrame && frame ? frame.id : ''; return targetId + ':' + projectId + ':' + frameId + ':' + projectOrigin + ':' + path; } /** * @param {!Workspace.UISourceCode} uiSourceCode * @param {!Workspace.Project} project * @param {?SDK.Target} target * @param {?SDK.ResourceTreeFrame} frame * @param {string} projectOrigin * @param {!Array<string>} path * @param {boolean} fromSourceMap * @return {!Sources.NavigatorTreeNode} */ _folderNode(uiSourceCode, project, target, frame, projectOrigin, path, fromSourceMap) { if (project.type() === Workspace.projectTypes.Snippets) 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 {!Sources.NavigatorTreeNode} */ (this._rootNode.child(project.id())); } const parentNode = this._folderNode(uiSourceCode, project, target, frame, projectOrigin, path.slice(0, -1), fromSourceMap); let type = fromSourceMap ? Sources.NavigatorView.Types.SourceMapFolder : Sources.NavigatorView.Types.NetworkFolder; if (project.type() === Workspace.projectTypes.FileSystem) type = Sources.NavigatorView.Types.FileSystemFolder; const name = path[path.length - 1]; folderNode = new Sources.NavigatorFolderTreeNode(this, project, folderId, type, folderPath, name); this._subfolderNodes.set(folderId, folderNode); parentNode.appendChild(folderNode); return folderNode; } /** * @param {!Workspace.UISourceCode} uiSourceCode * @param {!Workspace.Project} project * @param {!SDK.Target} target * @param {?SDK.ResourceTreeFrame} frame * @param {string} projectOrigin * @return {!Sources.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 Sources.NavigatorGroupTreeNode( this, project, projectOrigin, Sources.NavigatorView.Types.Domain, this._computeProjectDisplayName(target, projectOrigin)); if (frame && projectOrigin === Common.ParsedURL.extractOrigin(frame.url)) domainNode.treeNode()._boostOrder = true; frameNode.appendChild(domainNode); return domainNode; } /** * @param {!Workspace.Project} project * @param {!SDK.Target} target * @param {?SDK.ResourceTreeFrame} frame * @return {!Sources.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 Sources.NavigatorGroupTreeNode( this, project, target.id() + ':' + frame.id, Sources.NavigatorView.Types.Frame, frame.displayName()); frameNode.setHoverCallback(hoverCallback); this._frameNodes.set(frame, frameNode); const parentFrame = frame.parentFrame || frame.crossTargetParentFrame(); this._frameNode(project, parentFrame ? parentFrame.resourceTreeModel().target() : target, parentFrame) .appendChild(frameNode); if (!parentFrame) frameNode.treeNode()._boostOrder = true; /** * @param {boolean} hovered */ function hoverCallback(hovered) { if (hovered) { const overlayModel = target.model(SDK.OverlayModel); if (overlayModel) overlayModel.highlightFrame(frame.id); } else { SDK.OverlayModel.hideDOMNodeHighlight(); } } return frameNode; } /** * @param {!Workspace.Project} project * @param {!SDK.Target} target * @return {!Sources.NavigatorTreeNode} */ _targetNode(project, target) { if (target === SDK.targetManager.mainTarget()) return this._rootNode; let targetNode = this._rootNode.child('target:' + target.id()); if (!targetNode) { targetNode = new Sources.NavigatorGroupTreeNode( this, project, 'target:' + target.id(), !target.hasBrowserCapability() ? Sources.NavigatorView.Types.Worker : Sources.NavigatorView.Types.Frame, target.name()); this._rootNode.appendChild(targetNode); } return targetNode; } /** * @param {!SDK.Target} target * @param {string} projectOrigin * @return {string} */ _computeProjectDisplayName(target, projectOrigin) { const runtimeModel = target.model(SDK.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 Common.UIString('(no domain)'); const parsedURL = new Common.ParsedURL(projectOrigin); const prettyURL = parsedURL.isValid ? parsedURL.host + (parsedURL.port ? (':' + parsedURL.port) : '') : ''; return (prettyURL || projectOrigin); } /** * @param {!Workspace.UISourceCode} uiSourceCode * @param {boolean=} select * @return {?Sources.NavigatorUISourceCodeTreeNode} */ revealUISourceCode(uiSourceCode, select) { const nodes = this._uiSourceCodeNodes.get(uiSourceCode); const node = nodes.firstValue(); 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 * @param {boolean} focusSource */ _sourceSelected(uiSourceCode, focusSource) { this._lastSelectedUISourceCode = uiSourceCode; Common.Revealer.reveal(uiSourceCode, !focusSource); } /** * @param {!Workspace.UISourceCode} uiSourceCode */ sourceDeleted(uiSourceCode) { } /** * @param {!Workspace.UISourceCode} uiSourceCode */ _removeUISourceCode(uiSourceCode) { const nodes = this._uiSourceCodeNodes.get(uiSourceCode); for (const node of nodes) this._removeUISourceCodeNode(node); } /** * @param {!Sources.NavigatorUISourceCodeTreeNode} node */ _removeUISourceCodeNode(node) { const uiSourceCode = node.uiSourceCode(); this._uiSourceCodeNodes.delete(uiSourceCode, node); const project = uiSourceCode.project(); const target = Bindings.NetworkProject.targetForUISourceCode(uiSourceCode); const frame = node.frame(); let parentNode = node.parent; parentNode.removeChild(node); node = parentNode; while (node) { parentNode = node.parent; if (!parentNode || !node.isEmpty()) break; if (parentNode === this._rootNode && project.type() === Workspace.projectTypes.FileSystem) break; if (!(node instanceof Sources.NavigatorGroupTreeNode || node instanceof Sources.NavigatorFolderTreeNode)) break; if (node._type === Sources.NavigatorView.Types.Frame) { this._discardFrame(/** @type {!SDK.ResourceTreeFrame} */ (frame)); break; } const folderId = this._folderNodeId(project, target, frame, uiSourceCode.origin(), node._folderPath); this._subfolderNodes.delete(folderId); parentNode.removeChild(node); node = parentNode; } } reset() { for (const node of this._uiSourceCodeNodes.valuesArray()) node.dispose(); this._scriptsTree.removeChildren(); this._uiSourceCodeNodes.clear(); this._subfolderNodes.clear(); this._frameNodes.clear(); this._rootNode.reset(); } /** * @param {!Event} event */ handleContextMenu(event) { } /** * @return {boolean} */ _renameShortcut() { const node = this._scriptsTree.selectedTreeElement && this._scriptsTree.selectedTreeElement._node; if (!node || !node._uiSourceCode || !node._uiSourceCode.canRename()) return false; this.rename(node, false); return true; } /** * @param {!Workspace.Project} project * @param {string} path * @param {!Workspace.UISourceCode=} uiSourceCode */ _handleContextMenuCreate(project, path, uiSourceCode) { if (uiSourceCode) { const relativePath = Persistence.FileSystemWorkspaceBinding.relativePath(uiSourceCode); relativePath.pop(); path = relativePath.join('/'); } this.create(project, path, uiSourceCode); } /** * @param {!Sources.NavigatorUISourceCodeTreeNode} node */ _handleContextMenuRename(node) { this.rename(node, false); } /** * @param {!Workspace.Project} project * @param {string} path */ _handleContextMenuExclude(project, path) { const shouldExclude = window.confirm(Common.UIString('Are you sure you want to exclude this folder?')); if (shouldExclude) { UI.startBatchUpdate(); project.excludeFolder(Persistence.FileSystemWorkspaceBinding.completeURL(project, path)); UI.endBatchUpdate(); } } /** * @param {!Workspace.UISourceCode} uiSourceCode */ _handleContextMenuDelete(uiSourceCode) { const shouldDelete = window.confirm(Common.UIString('Are you sure you want to delete this file?')); if (shouldDelete) uiSourceCode.project().deleteFile(uiSourceCode); } /** * @param {!Event} event * @param {!Sources.NavigatorUISourceCodeTreeNode} node */ handleFileContextMenu(event, node) { const uiSourceCode = node.uiSourceCode(); const contextMenu = new UI.ContextMenu(event); contextMenu.appendApplicableItems(uiSourceCode); const project = uiSourceCode.project(); if (project.type() === Workspace.projectTypes.FileSystem) { contextMenu.editSection().appendItem( Common.UIString('Rename\u2026'), this._handleContextMenuRename.bind(this, node)); contextMenu.editSection().appendItem( Common.UIString('Make a copy\u2026'), this._handleContextMenuCreate.bind(this, project, '', uiSourceCode)); contextMenu.editSection().appendItem( Common.UIString('Delete'), this._handleContextMenuDelete.bind(this, uiSourceCode)); } contextMenu.show(); } /** * @param {!Event} event * @param {!Sources.NavigatorTreeNode} node */ handleFolderContextMenu(event, node) { const path = node._folderPath || ''; const project = node._project; const contextMenu = new UI.ContextMenu(event); Sources.NavigatorView.appendSearchItem(contextMenu, path); const folderPath = Common.ParsedURL.urlToPlatformPath( Persistence.FileSystemWorkspaceBinding.completeURL(project, path), Host.isWin()); contextMenu.revealSection().appendItem( Common.UIString('Open folder'), () => InspectorFrontendHost.showItemInFolder(folderPath)); if (project.canCreateFile()) { contextMenu.defaultSection().appendItem( Common.UIString('New file'), this._handleContextMenuCreate.bind(this, project, path)); } if (project.canExcludeFolder(path)) { contextMenu.defaultSection().appendItem( Common.UIString('Exclude folder'), this._handleContextMenuExclude.bind(this, project, path)); } function removeFolder() { const shouldRemove = window.confirm(Common.UIString('Are you sure you want to remove this folder?')); if (shouldRemove) project.remove(); } if (project.type() === Workspace.projectTypes.FileSystem) { Sources.NavigatorView.appendAddFolderItem(contextMenu); if (node instanceof Sources.NavigatorGroupTreeNode) contextMenu.defaultSection().appendItem(Common.UIString('Remove folder from workspace'), removeFolder); } contextMenu.show(); } /** * @param {!Sources.NavigatorUISourceCodeTreeNode} node * @param {boolean} creatingNewUISourceCode * @protected */ rename(node, creatingNewUISourceCode) { const uiSourceCode = node.uiSourceCode(); node.rename(callback.bind(this)); /** * @this {Sources.NavigatorView} * @param {boolean} committed */ function callback(committed) { if (!creatingNewUISourceCode) return; if (!committed) uiSourceCode.remove(); else if (node._treeElement.listItemElement.hasFocus()) this._sourceSelected(uiSourceCode, true); } } /** * @param {!Workspace.Project} project * @param {string} path * @param {!Workspace.UISourceCode=} uiSourceCodeToCopy */ async create(project, path, uiSourceCodeToCopy) { let content = ''; if (uiSourceCodeToCopy) content = (await uiSourceCodeToCopy.requestContent()) || ''; 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.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.Target} target */ targetAdded(target) { } /** * @override * @param {!SDK.Target} target */ targetRemoved(target) { const targetNode = this._rootNode.child('target:' + target.id()); if (targetNode) this._rootNode.removeChild(targetNode); } /** * @param {!Common.Event} event */ _targetNameChanged(event) { const target = /** @type {!SDK.Target} */ (event.data); const targetNode = this._rootNode.child('target:' + target.id()); if (targetNode) targetNode.setTitle(target.name()); } }; Sources.NavigatorView.Types = { Domain: 'domain', File: 'file', FileSystem: 'fs', FileSystemFolder: 'fs-folder', Frame: 'frame', NetworkFolder: 'nw-folder', Root: 'root', SourceMapFolder: 'sm-folder', Worker: 'worker' }; /** * @unrestricted */ Sources.NavigatorFolderTreeElement = class extends UI.TreeElement { /** * @param {!Sources.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'); this._nodeType = type; this.title = title; this.tooltip = title; this._navigatorView = navigatorView; this._hoverCallback = hoverCallback; let iconType = 'largeicon-navigator-folder'; if (type === Sources.NavigatorView.Types.Domain) iconType = 'largeicon-navigator-domain'; else if (type === Sources.NavigatorView.Types.Frame) iconType = 'largeicon-navigator-frame'; else if (type === Sources.NavigatorView.Types.Worker) iconType = 'largeicon-navigator-worker'; this.setLeadingIcons([UI.Icon.create(iconType, 'icon')]); } /** * @override */ 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 {!Sources.NavigatorTreeNode} node */ setNode(node) { this._node = node; const paths = []; while (node && !node.isRoot()) { paths.push(node._title); node = node.parent; } paths.reverse(); this.tooltip = paths.join('/'); } /** * @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); } }; /** * @unrestricted */ Sources.NavigatorSourceTreeElement = class extends UI.TreeElement { /** * @param {!Sources.NavigatorView} navigatorView * @param {!Workspace.UISourceCode} uiSourceCode * @param {string} title * @param {!Sources.NavigatorUISourceCodeTreeNode} node */ constructor(navigatorView, uiSourceCode, title, node) { super('', false); this._nodeType = Sources.NavigatorView.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(); this._navigatorView = navigatorView; this._uiSourceCode = uiSourceCode; this.updateIcon(); } updateIcon() { const binding = Persistence.persistence.binding(this._uiSourceCode); if (binding) { const container = createElementWithClass('span', 'icon-stack'); const icon = UI.Icon.create('largeicon-navigator-file-sync', 'icon'); const badge = UI.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.project() === binding.fileSystem.project()) badge.style.filter = 'hue-rotate(160deg)'; container.appendChild(icon); container.appendChild(badge); container.title = Persistence.PersistenceUtils.tooltipForUISourceCode(this._uiSourceCode); this.setLeadingIcons([container]); } else { let iconType = 'largeicon-navigator-file'; if (this._uiSourceCode.contentType() === Common.resourceTypes.Snippet) iconType = 'largeicon-navigator-snippet'; const defaultIcon = UI.Icon.create(iconType, 'icon'); this.setLeadingIcons([defaultIcon]); } } /** * @return {!Workspace.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; const isSelected = this === this.treeOutline.selectedTreeElement; return isSelected && this.treeOutline.element.hasFocus() && !UI.isBeingEdited(this.treeOutline.element); } /** * @override */ selectOnMouseDown(event) { if (event.which !== 1 || !this._shouldRenameOnMouseDown()) { super.selectOnMouseDown(event); return; } setTimeout(rename.bind(this), 300); /** * @this {Sources.NavigatorSourceTreeElement} */ function rename() { if (this._shouldRenameOnMouseDown()) this._navigatorView.rename(this._node, false); } } /** * @param {!DragEvent} event */ _ondragstart(event) { 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 * @return {boolean} */ ondblclick(event) { const middleClick = 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() { this._navigatorView.sourceDeleted(this.uiSourceCode); return true; } /** * @param {!Event} event */ _handleContextMenuEvent(event) { this.select(); this._navigatorView.handleFileContextMenu(event, this._node); } }; /** * @unrestricted */ Sources.NavigatorTreeNode = class { /** * @param {string} id * @param {string} type */ constructor(id, type) { this.id = id; this._type = type; /** @type {!Map.<string, !Sources.NavigatorTreeNode>} */ this._children = new Map(); } /** * @return {!UI.TreeElement} */ treeNode() { throw 'Not implemented'; } dispose() { } /** * @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.treeNode().appendChild(/** @type {!UI.TreeElement} */ (children[i].treeNode())); } /** * @param {!Sources.NavigatorTreeNode} node */ didAddChild(node) { if (this.isPopulated()) this.treeNode().appendChild(/** @type {!UI.TreeElement} */ (node.treeNode())); } /** * @param {!Sources.NavigatorTreeNode} node */ willRemoveChild(node) { if (this.isPopulated()) this.treeNode().removeChild(/** @type {!UI.TreeElement} */ (node.treeNode())); } /** * @return {boolean} */ isPopulated() { return this._populated; } /** * @return {boolean} */ isEmpty() { return !this._children.size; } /** * @return {!Array.<!Sources.NavigatorTreeNode>} */ children() { return this._children.valuesArray(); } /** * @param {string} id * @return {?Sources.NavigatorTreeNode} */ child(id) { return this._children.get(id) || null; } /** * @param {!Sources.NavigatorTreeNode} node */ appendChild(node) { this._children.set(node.id, node); node.parent = this; this.didAddChild(node); } /** * @param {!Sources.NavigatorTreeNode} node */ removeChild(node) { this.willRemoveChild(node); this._children.remove(node.id); delete node.parent; node.dispose(); } reset() { this._children.clear(); } }; /** * @unrestricted */ Sources.NavigatorRootTreeNode = class extends Sources.NavigatorTreeNode { /** * @param {!Sources.NavigatorView} navigatorView */ constructor(navigatorView) { super('', Sources.NavigatorView.Types.Root); this._navigatorView = navigatorView; } /** * @override * @return {boolean} */ isRoot() { return true; } /** * @override * @return {!UI.TreeElement} */ treeNode() { return this._navigatorView._scriptsTree.rootElement(); } }; /** * @unrestricted */ Sources.NavigatorUISourceCodeTreeNode = class extends Sources.NavigatorTreeNode { /** * @param {!Sources.NavigatorView} navigatorView * @param {!Workspace.UISourceCode} uiSourceCode * @param {?SDK.ResourceTreeFrame} frame */ constructor(navigatorView, uiSourceCode, frame) { super(uiSourceCode.project().id() + ':' + uiSourceCode.url(), Sources.NavigatorView.Types.File); this._navigatorView = navigatorView; this._uiSourceCode = uiSourceCode; this._treeElement = null; this._eventListeners = []; this._frame = frame; } /** * @return {?SDK.ResourceTreeFrame} */ frame() { return this._frame; } /** * @return {!Workspace.UISourceCode} */ uiSourceCode() { return this._uiSourceCode; } /** * @override * @return {!UI.TreeElement} */ treeNode() { if (this._treeElement) return this._treeElement; this._treeElement = new Sources.NavigatorSourceTreeElement(this._navigatorView, this._uiSourceCode, '', this); this.updateTitle(); const updateTitleBound = this.updateTitle.bind(this, undefined); this._eventListeners = [ this._uiSourceCode.addEventListener(Workspace.UISourceCode.Events.TitleChanged, updateTitleBound), this._uiSourceCode.addEventListener(Workspace.UISourceCode.Events.WorkingCopyChanged, updateTitleBound), this._uiSourceCode.addEventListener(Workspace.UISourceCode.Events.WorkingCopyCommitted, updateTitleBound) ]; return this._treeElement; } /** * @param {boolean=} ignoreIsDirty */ updateTitle(ignoreIsDirty) { if (!this._treeElement) return; let titleText = this._uiSourceCode.displayName(); if (!ignoreIsDirty && this._uiSourceCode.isDirty()) titleText = '*' + titleText; this._treeElement.title = titleText; this._treeElement.updateIcon(); let tooltip = this._uiSourceCode.url(); if (this._uiSourceCode.contentType().isFromSourceMap()) tooltip = Common.UIString('%s (from source map)', this._uiSourceCode.displayName()); this._treeElement.tooltip = tooltip; } /** * @override * @return {boolean} */ hasChildren() { return false; } /** * @override */ dispose() { Common.EventTarget.removeEventListeners(this._eventListeners); } /** * @param {boolean=} select */ reveal(select) { this.parent.populate(); this.parent.treeNode().expand(); this._treeElement.reveal(true); if (select) this._treeElement.select(true); } /** * @param {function(boolean)=} callback */ rename(callback) { if (!this._treeElement) return; this._treeElement.listItemElement.focus(); // Tree outline should be marked as edited as well as the tree element to prevent search from starting. const treeOutlineElement = this._treeElement.treeOutline.element; UI.markBeingEdited(treeOutlineElement, true); /** * @param {!Element} element * @param {string} newTitle * @param {string} oldTitle * @this {Sources.NavigatorUISourceCodeTreeNode} */ function commitHandler(element, newTitle, oldTitle) { if (newTitle !== oldTitle) { this._treeElement.title = newTitle; this._uiSourceCode.rename(newTitle).then(renameCallback.bind(this)); return; } afterEditing.call(this, true); } /** * @param {boolean} success * @this {Sources.NavigatorUISourceCodeTreeNode} */ function renameCallback(success) { if (!success) { UI.markBeingEdited(treeOutlineElement, false); this.updateTitle(); this.rename(callback); return; } afterEditing.call(this, true); } /** * @param {boolean} committed * @this {Sources.NavigatorUISourceCodeTreeNode} */ function afterEditing(committed) { UI.markBeingEdited(treeOutlineElement, false); this.updateTitle(); if (callback) callback(committed); } this.updateTitle(true); this._treeElement.startEditingTitle( new UI.InplaceEditor.Config(commitHandler.bind(this), afterEditing.bind(this, false))); } }; /** * @unrestricted */ Sources.NavigatorFolderTreeNode = class extends Sources.NavigatorTreeNode { /** * @param {!Sources.NavigatorView} navigatorView * @param {?Workspace.Project} project * @param {string} id * @param {string} type * @param {string} folderPath * @param {string} title */ constructor(navigatorView, project, id, type, folderPath, title) { super(id, type); this._navigatorView = navigatorView; this._project = project; this._folderPath = folderPath; this._title = title; } /** * @override * @return {!UI.TreeElement} */ treeNode() { if (this._treeElement) return this._treeElement; this._treeElement = this._createTreeElement(this._title, this); this.updateTitle(); return this._treeElement; } updateTitle() { if (!this._treeElement || this._project.type() !== Workspace.projectTypes.FileSystem) return; const absoluteFileSystemPath = Persistence.FileSystemWorkspaceBinding.fileSystemPath(this._project.id()) + '/' + this._folderPath; const hasMappedFiles = Persistence.persistence.filePathHasBindings(absoluteFileSystemPath); this._treeElement.listItemElement.classList.toggle('has-mapped-files', hasMappedFiles); } /** * @return {!UI.TreeElement} */ _createTreeElement(title, node) { if (this._project.type() !== Workspace.projectTypes.FileSystem) { try { title = decodeURI(title); } catch (e) { } } const treeElement = new Sources.NavigatorFolderTreeElement(this._navigatorView, this._type, title); treeElement.setNode(node); return treeElement; } /** * @override */ wasPopulated() { if (!this._treeElement || this._treeElement._node !== this) return; this._addChildrenRecursive(); } _addChildrenRecursive() { const children = this.children(); for (let i = 0; i < children.length; ++i) { const child = children[i]; this.didAddChild(child); if (child instanceof Sources.NavigatorFolderTreeNode) child._addChildrenRecursive(); } } _shouldMerge(node) { return this._type !== Sources.NavigatorView.Types.Domain && node instanceof Sources.NavigatorFolderTreeNode; } /** * @param {!Sources.NavigatorTreeNode} node * @override */ didAddChild(node) { function titleForNode(node) { return node._title; } if (!this._treeElement) return; let children = this.children(); if (children.length === 1 && this._shouldMerge(node)) { node._isMerged = true; this._treeElement.title = this._treeElement.title + '/' + node._title; node._treeElement = this._treeElement; this._treeElement.setNode(node); return; } let oldNode; if (children.length === 2) oldNode = children[0] !== node ? children[0] : children[1]; if (oldNode && oldNode._isMerged) { delete oldNode._isMerged; const mergedToNodes = []; mergedToNodes.push(this); let treeNode = this; while (treeNode._isMerged) { treeNode = treeNode.parent; mergedToNodes.push(treeNode); } mergedToNodes.reverse(); const titleText = mergedToNodes.map(titleForNode).join('/'); const nodes = []; treeNode = oldNode; do { nodes.push(treeNode); children = treeNode.children(); treeNode = children.length === 1 ? children[0] : null; } while (treeNode && treeNode._isMerged); if (!this.isPopulated()) { this._treeElement.title = titleText; this._treeElement.setNode(this); for (let i = 0; i < nodes.length; ++i) { delete nodes[i]._treeElement; delete nodes[i]._isMerged; } return; } const oldTreeElement = this._treeElement; const treeElement = this._createTreeElement(titleText, this); for (let i = 0; i < mergedToNodes.length; ++i) mergedToNodes[i]._treeElement = treeElement; oldTreeElement.parent.appendChild(treeElement); oldTreeElement.setNode(nodes[nodes.length - 1]); oldTreeElement.title = nodes.map(titleForNode).join('/'); oldTreeElement.parent.removeChild(oldTreeElement); this._treeElement.appendChild(oldTreeElement); if (oldTreeElement.expanded) treeElement.expand(); } if (this.isPopulated()) this._treeElement.appendChild(node.treeNode()); } /** * @override * @param {!Sources.NavigatorTreeNode} node */ willRemoveChild(node) { if (node._isMerged || !this.isPopulated()) return; this._treeElement.removeChild(node._treeElement); } }; /** * @unrestricted */ Sources.NavigatorGroupTreeNode = class extends Sources.NavigatorTreeNode { /** * @param {!Sources.NavigatorView} navigatorView * @param {!Workspace.Project} project * @param {string} id * @param {string} type * @param {string} title */ constructor(navigatorView, project, id, type, title) { super(id, type); this._project = project; this._navigatorView = navigatorView; this._title = title; this.populate(); } /** * @param {function(boolean)} hoverCallback */ setHoverCallback(hoverCallback) { this._hoverCallback = hoverCallback; } /** * @override * @return {!UI.TreeElement} */ treeNode() { if (this._treeElement) return this._treeElement; this._treeElement = new Sources.NavigatorFolderTreeElement(this._navigatorView, this._type, this._title, this._hoverCallback); this._treeElement.setNode(this); return this._treeElement; } /** * @override */ onattach() { this.updateTitle(); } updateTitle() { if (!this._treeElement || this._project.type() !== Workspace.projectTypes.FileSystem) return; const fileSystemPath = Persistence.FileSystemWorkspaceBinding.fileSystemPath(this._project.id()); const wasActive = this._treeElement.listItemElement.classList.contains('has-mapped-files'); const isActive = Persistence.persistence.filePathHasBindings(fileSystemPath); if (wasActive === isActive) return; this._treeElement.listItemElement.classList.toggle('has-mapped-files', isActive); if (this._treeElement.childrenListElement.hasFocus()) return; if (isActive) this._treeElement.expand(); else this._treeElement.collapse(); } /** * @param {string} title * @override */ setTitle(title) { this._title = title; if (this._treeElement) this._treeElement.title = this._title; } };