|  | // Copyright 2016 The Chromium Authors. All rights reserved. | 
|  | // Use of this source code is governed by a BSD-style license that can be | 
|  | // found in the LICENSE file. | 
|  |  | 
|  | export default class Automapping { | 
|  | /** | 
|  | * @param {!Workspace.Workspace} workspace | 
|  | * @param {function(!AutomappingStatus)} onStatusAdded | 
|  | * @param {function(!AutomappingStatus)} onStatusRemoved | 
|  | */ | 
|  | constructor(workspace, onStatusAdded, onStatusRemoved) { | 
|  | this._workspace = workspace; | 
|  |  | 
|  | this._onStatusAdded = onStatusAdded; | 
|  | this._onStatusRemoved = onStatusRemoved; | 
|  | /** @type {!Set<!AutomappingStatus>} */ | 
|  | this._statuses = new Set(); | 
|  | this._statusSymbol = Symbol('Automapping.Status'); | 
|  | this._processingPromiseSymbol = Symbol('Automapping.ProcessingPromise'); | 
|  | this._metadataSymbol = Symbol('Automapping.Metadata'); | 
|  |  | 
|  |  | 
|  | /** @type {!Map<string, !Workspace.UISourceCode>} */ | 
|  | this._fileSystemUISourceCodes = new Map(); | 
|  | this._sweepThrottler = new Common.Throttler(100); | 
|  |  | 
|  | const pathEncoder = new Persistence.PathEncoder(); | 
|  | this._filesIndex = new FilePathIndex(pathEncoder); | 
|  | this._projectFoldersIndex = new FolderIndex(pathEncoder); | 
|  | this._activeFoldersIndex = new FolderIndex(pathEncoder); | 
|  |  | 
|  | /** @type {!Array<function(!Workspace.UISourceCode):boolean>} */ | 
|  | this._interceptors = []; | 
|  |  | 
|  | this._workspace.addEventListener( | 
|  | Workspace.Workspace.Events.UISourceCodeAdded, | 
|  | event => this._onUISourceCodeAdded(/** @type {!Workspace.UISourceCode} */ (event.data))); | 
|  | this._workspace.addEventListener( | 
|  | Workspace.Workspace.Events.UISourceCodeRemoved, | 
|  | event => this._onUISourceCodeRemoved(/** @type {!Workspace.UISourceCode} */ (event.data))); | 
|  | this._workspace.addEventListener(Workspace.Workspace.Events.UISourceCodeRenamed, this._onUISourceCodeRenamed, this); | 
|  | this._workspace.addEventListener( | 
|  | Workspace.Workspace.Events.ProjectAdded, | 
|  | event => this._onProjectAdded(/** @type {!Workspace.Project} */ (event.data)), this); | 
|  | this._workspace.addEventListener( | 
|  | Workspace.Workspace.Events.ProjectRemoved, | 
|  | event => this._onProjectRemoved(/** @type {!Workspace.Project} */ (event.data)), this); | 
|  |  | 
|  | for (const fileSystem of workspace.projects()) { | 
|  | this._onProjectAdded(fileSystem); | 
|  | } | 
|  | for (const uiSourceCode of workspace.uiSourceCodes()) { | 
|  | this._onUISourceCodeAdded(uiSourceCode); | 
|  | } | 
|  | } | 
|  |  | 
|  | /** | 
|  | * @param {function(!Workspace.UISourceCode):boolean} interceptor | 
|  | */ | 
|  | addNetworkInterceptor(interceptor) { | 
|  | this._interceptors.push(interceptor); | 
|  | this.scheduleRemap(); | 
|  | } | 
|  |  | 
|  | scheduleRemap() { | 
|  | for (const status of this._statuses.valuesArray()) { | 
|  | this._clearNetworkStatus(status.network); | 
|  | } | 
|  | this._scheduleSweep(); | 
|  | } | 
|  |  | 
|  | _scheduleSweep() { | 
|  | this._sweepThrottler.schedule(sweepUnmapped.bind(this)); | 
|  |  | 
|  | /** | 
|  | * @this {Automapping} | 
|  | * @return {!Promise} | 
|  | */ | 
|  | function sweepUnmapped() { | 
|  | const networkProjects = this._workspace.projectsForType(Workspace.projectTypes.Network); | 
|  | for (const networkProject of networkProjects) { | 
|  | for (const uiSourceCode of networkProject.uiSourceCodes()) { | 
|  | this._computeNetworkStatus(uiSourceCode); | 
|  | } | 
|  | } | 
|  | this._onSweepHappenedForTest(); | 
|  | return Promise.resolve(); | 
|  | } | 
|  | } | 
|  |  | 
|  | _onSweepHappenedForTest() { | 
|  | } | 
|  |  | 
|  | /** | 
|  | * @param {!Workspace.Project} project | 
|  | */ | 
|  | _onProjectRemoved(project) { | 
|  | for (const uiSourceCode of project.uiSourceCodes()) { | 
|  | this._onUISourceCodeRemoved(uiSourceCode); | 
|  | } | 
|  | if (project.type() !== Workspace.projectTypes.FileSystem) { | 
|  | return; | 
|  | } | 
|  | const fileSystem = /** @type {!Persistence.FileSystemWorkspaceBinding.FileSystem} */ (project); | 
|  | for (const gitFolder of fileSystem.initialGitFolders()) { | 
|  | this._projectFoldersIndex.removeFolder(gitFolder); | 
|  | } | 
|  | this._projectFoldersIndex.removeFolder(fileSystem.fileSystemPath()); | 
|  | this.scheduleRemap(); | 
|  | } | 
|  |  | 
|  | /** | 
|  | * @param {!Workspace.Project} project | 
|  | */ | 
|  | _onProjectAdded(project) { | 
|  | if (project.type() !== Workspace.projectTypes.FileSystem) { | 
|  | return; | 
|  | } | 
|  | const fileSystem = /** @type {!Persistence.FileSystemWorkspaceBinding.FileSystem} */ (project); | 
|  | for (const gitFolder of fileSystem.initialGitFolders()) { | 
|  | this._projectFoldersIndex.addFolder(gitFolder); | 
|  | } | 
|  | this._projectFoldersIndex.addFolder(fileSystem.fileSystemPath()); | 
|  | project.uiSourceCodes().forEach(this._onUISourceCodeAdded.bind(this)); | 
|  | this.scheduleRemap(); | 
|  | } | 
|  |  | 
|  | /** | 
|  | * @param {!Workspace.UISourceCode} uiSourceCode | 
|  | */ | 
|  | _onUISourceCodeAdded(uiSourceCode) { | 
|  | const project = uiSourceCode.project(); | 
|  | if (project.type() === Workspace.projectTypes.FileSystem) { | 
|  | if (!Persistence.FileSystemWorkspaceBinding.fileSystemSupportsAutomapping(project)) { | 
|  | return; | 
|  | } | 
|  | this._filesIndex.addPath(uiSourceCode.url()); | 
|  | this._fileSystemUISourceCodes.set(uiSourceCode.url(), uiSourceCode); | 
|  | this._scheduleSweep(); | 
|  | } else if (project.type() === Workspace.projectTypes.Network) { | 
|  | this._computeNetworkStatus(uiSourceCode); | 
|  | } | 
|  | } | 
|  |  | 
|  | /** | 
|  | * @param {!Workspace.UISourceCode} uiSourceCode | 
|  | */ | 
|  | _onUISourceCodeRemoved(uiSourceCode) { | 
|  | if (uiSourceCode.project().type() === Workspace.projectTypes.FileSystem) { | 
|  | this._filesIndex.removePath(uiSourceCode.url()); | 
|  | this._fileSystemUISourceCodes.delete(uiSourceCode.url()); | 
|  | const status = uiSourceCode[this._statusSymbol]; | 
|  | if (status) { | 
|  | this._clearNetworkStatus(status.network); | 
|  | } | 
|  | } else if (uiSourceCode.project().type() === Workspace.projectTypes.Network) { | 
|  | this._clearNetworkStatus(uiSourceCode); | 
|  | } | 
|  | } | 
|  |  | 
|  | /** | 
|  | * @param {!Common.Event} event | 
|  | */ | 
|  | _onUISourceCodeRenamed(event) { | 
|  | const uiSourceCode = /** @type {!Workspace.UISourceCode} */ (event.data.uiSourceCode); | 
|  | const oldURL = /** @type {string} */ (event.data.oldURL); | 
|  | if (uiSourceCode.project().type() !== Workspace.projectTypes.FileSystem) { | 
|  | return; | 
|  | } | 
|  |  | 
|  | this._filesIndex.removePath(oldURL); | 
|  | this._fileSystemUISourceCodes.delete(oldURL); | 
|  | const status = uiSourceCode[this._statusSymbol]; | 
|  | if (status) { | 
|  | this._clearNetworkStatus(status.network); | 
|  | } | 
|  |  | 
|  | this._filesIndex.addPath(uiSourceCode.url()); | 
|  | this._fileSystemUISourceCodes.set(uiSourceCode.url(), uiSourceCode); | 
|  | this._scheduleSweep(); | 
|  | } | 
|  |  | 
|  | /** | 
|  | * @param {!Workspace.UISourceCode} networkSourceCode | 
|  | */ | 
|  | _computeNetworkStatus(networkSourceCode) { | 
|  | if (networkSourceCode[this._processingPromiseSymbol] || networkSourceCode[this._statusSymbol]) { | 
|  | return; | 
|  | } | 
|  | if (this._interceptors.some(interceptor => interceptor(networkSourceCode))) { | 
|  | return; | 
|  | } | 
|  | if (networkSourceCode.url().startsWith('wasm://')) { | 
|  | return; | 
|  | } | 
|  | const createBindingPromise = | 
|  | this._createBinding(networkSourceCode).then(validateStatus.bind(this)).then(onStatus.bind(this)); | 
|  | networkSourceCode[this._processingPromiseSymbol] = createBindingPromise; | 
|  |  | 
|  | /** | 
|  | * @param {?AutomappingStatus} status | 
|  | * @return {!Promise<?AutomappingStatus>} | 
|  | * @this {Automapping} | 
|  | */ | 
|  | async function validateStatus(status) { | 
|  | if (!status) { | 
|  | return null; | 
|  | } | 
|  | if (networkSourceCode[this._processingPromiseSymbol] !== createBindingPromise) { | 
|  | return null; | 
|  | } | 
|  | if (status.network.contentType().isFromSourceMap() || !status.fileSystem.contentType().isTextType()) { | 
|  | return status; | 
|  | } | 
|  |  | 
|  | // At the time binding comes, there are multiple user scenarios: | 
|  | // 1. Both network and fileSystem files are **not** dirty. | 
|  | //    This is a typical scenario when user hasn't done any edits yet to the | 
|  | //    files in question. | 
|  | // 2. FileSystem file has unsaved changes, network is clear. | 
|  | //    This typically happens with CSS files editing. Consider the following | 
|  | //    scenario: | 
|  | //      - user edits file that has been successfully mapped before | 
|  | //      - user doesn't save the file | 
|  | //      - user hits reload | 
|  | // 3. Network file has either unsaved changes or commits, but fileSystem file is clear. | 
|  | //    This typically happens when we've been editing file and then realized we'd like to drop | 
|  | //    a folder and persist all the changes. | 
|  | // 4. Network file has either unsaved changes or commits, and fileSystem file has unsaved changes. | 
|  | //    We consider this to be un-realistic scenario and in this case just fail gracefully. | 
|  | // | 
|  | // To support usecase (3), we need to validate against original network content. | 
|  | if (status.fileSystem.isDirty() && (status.network.isDirty() || status.network.hasCommits())) { | 
|  | return null; | 
|  | } | 
|  |  | 
|  | const [fileSystemContent, networkContent] = await Promise.all( | 
|  | [status.fileSystem.requestContent(), status.network.project().requestFileContent(status.network)]); | 
|  | if (fileSystemContent.content === null || networkContent === null) { | 
|  | return null; | 
|  | } | 
|  |  | 
|  | if (networkSourceCode[this._processingPromiseSymbol] !== createBindingPromise) { | 
|  | return null; | 
|  | } | 
|  |  | 
|  | const target = Bindings.NetworkProject.targetForUISourceCode(status.network); | 
|  | let isValid = false; | 
|  | const fileContent = fileSystemContent.content; | 
|  | if (target && target.type() === SDK.Target.Type.Node) { | 
|  | const rewrappedNetworkContent = | 
|  | Persistence.Persistence.rewrapNodeJSContent(status.fileSystem, fileContent, networkContent.content); | 
|  | isValid = fileContent === rewrappedNetworkContent; | 
|  | } else { | 
|  | // Trim trailing whitespaces because V8 adds trailing newline. | 
|  | isValid = fileContent.trimRight() === networkContent.content.trimRight(); | 
|  | } | 
|  | if (!isValid) { | 
|  | this._prevalidationFailedForTest(status); | 
|  | return null; | 
|  | } | 
|  | return status; | 
|  | } | 
|  |  | 
|  | /** | 
|  | * @param {?AutomappingStatus} status | 
|  | * @this {Automapping} | 
|  | */ | 
|  | function onStatus(status) { | 
|  | if (networkSourceCode[this._processingPromiseSymbol] !== createBindingPromise) { | 
|  | return; | 
|  | } | 
|  | networkSourceCode[this._processingPromiseSymbol] = null; | 
|  | if (!status) { | 
|  | this._onBindingFailedForTest(); | 
|  | return; | 
|  | } | 
|  | // TODO(lushnikov): remove this check once there's a single uiSourceCode per url. @see crbug.com/670180 | 
|  | if (status.network[this._statusSymbol] || status.fileSystem[this._statusSymbol]) { | 
|  | return; | 
|  | } | 
|  |  | 
|  | this._statuses.add(status); | 
|  | status.network[this._statusSymbol] = status; | 
|  | status.fileSystem[this._statusSymbol] = status; | 
|  | if (status.exactMatch) { | 
|  | const projectFolder = this._projectFoldersIndex.closestParentFolder(status.fileSystem.url()); | 
|  | const newFolderAdded = projectFolder ? this._activeFoldersIndex.addFolder(projectFolder) : false; | 
|  | if (newFolderAdded) { | 
|  | this._scheduleSweep(); | 
|  | } | 
|  | } | 
|  | this._onStatusAdded.call(null, status); | 
|  | } | 
|  | } | 
|  |  | 
|  | /** | 
|  | * @param {!AutomappingStatus} binding | 
|  | */ | 
|  | _prevalidationFailedForTest(binding) { | 
|  | } | 
|  |  | 
|  | _onBindingFailedForTest() { | 
|  | } | 
|  |  | 
|  | /** | 
|  | * @param {!Workspace.UISourceCode} networkSourceCode | 
|  | */ | 
|  | _clearNetworkStatus(networkSourceCode) { | 
|  | if (networkSourceCode[this._processingPromiseSymbol]) { | 
|  | networkSourceCode[this._processingPromiseSymbol] = null; | 
|  | return; | 
|  | } | 
|  | const status = networkSourceCode[this._statusSymbol]; | 
|  | if (!status) { | 
|  | return; | 
|  | } | 
|  |  | 
|  | this._statuses.delete(status); | 
|  | status.network[this._statusSymbol] = null; | 
|  | status.fileSystem[this._statusSymbol] = null; | 
|  | if (status.exactMatch) { | 
|  | const projectFolder = this._projectFoldersIndex.closestParentFolder(status.fileSystem.url()); | 
|  | if (projectFolder) { | 
|  | this._activeFoldersIndex.removeFolder(projectFolder); | 
|  | } | 
|  | } | 
|  | this._onStatusRemoved.call(null, status); | 
|  | } | 
|  |  | 
|  | /** | 
|  | * @param {!Workspace.UISourceCode} networkSourceCode | 
|  | * @return {!Promise<?AutomappingStatus>} | 
|  | */ | 
|  | _createBinding(networkSourceCode) { | 
|  | if (networkSourceCode.url().startsWith('file://') || networkSourceCode.url().startsWith('snippet://')) { | 
|  | const decodedUrl = decodeURI(networkSourceCode.url()); | 
|  | const fileSourceCode = this._fileSystemUISourceCodes.get(decodedUrl); | 
|  | const status = fileSourceCode ? new AutomappingStatus(networkSourceCode, fileSourceCode, false) : null; | 
|  | return Promise.resolve(status); | 
|  | } | 
|  |  | 
|  | let networkPath = Common.ParsedURL.extractPath(networkSourceCode.url()); | 
|  | if (networkPath === null) { | 
|  | return Promise.resolve(/** @type {?AutomappingStatus} */ (null)); | 
|  | } | 
|  |  | 
|  | if (networkPath.endsWith('/')) { | 
|  | networkPath += 'index.html'; | 
|  | } | 
|  | const urlDecodedNetworkPath = decodeURI(networkPath); | 
|  | const similarFiles = | 
|  | this._filesIndex.similarFiles(urlDecodedNetworkPath).map(path => this._fileSystemUISourceCodes.get(path)); | 
|  | if (!similarFiles.length) { | 
|  | return Promise.resolve(/** @type {?AutomappingStatus} */ (null)); | 
|  | } | 
|  |  | 
|  | return this._pullMetadatas(similarFiles.concat(networkSourceCode)).then(onMetadatas.bind(this)); | 
|  |  | 
|  | /** | 
|  | * @this {Automapping} | 
|  | */ | 
|  | function onMetadatas() { | 
|  | const activeFiles = similarFiles.filter(file => !!this._activeFoldersIndex.closestParentFolder(file.url())); | 
|  | const networkMetadata = networkSourceCode[this._metadataSymbol]; | 
|  | if (!networkMetadata || (!networkMetadata.modificationTime && typeof networkMetadata.contentSize !== 'number')) { | 
|  | // If networkSourceCode does not have metadata, try to match against active folders. | 
|  | if (activeFiles.length !== 1) { | 
|  | return null; | 
|  | } | 
|  | return new AutomappingStatus(networkSourceCode, activeFiles[0], false); | 
|  | } | 
|  |  | 
|  | // Try to find exact matches, prioritizing active folders. | 
|  | let exactMatches = this._filterWithMetadata(activeFiles, networkMetadata); | 
|  | if (!exactMatches.length) { | 
|  | exactMatches = this._filterWithMetadata(similarFiles, networkMetadata); | 
|  | } | 
|  | if (exactMatches.length !== 1) { | 
|  | return null; | 
|  | } | 
|  | return new AutomappingStatus(networkSourceCode, exactMatches[0], true); | 
|  | } | 
|  | } | 
|  |  | 
|  | /** | 
|  | * @param {!Array<!Workspace.UISourceCode>} uiSourceCodes | 
|  | * @return {!Promise} | 
|  | */ | 
|  | _pullMetadatas(uiSourceCodes) { | 
|  | return Promise.all(uiSourceCodes.map(async file => { | 
|  | file[this._metadataSymbol] = await file.requestMetadata(); | 
|  | })); | 
|  | } | 
|  |  | 
|  | /** | 
|  | * @param {!Array<!Workspace.UISourceCode>} files | 
|  | * @param {!Workspace.UISourceCodeMetadata} networkMetadata | 
|  | * @return {!Array<!Workspace.UISourceCode>} | 
|  | */ | 
|  | _filterWithMetadata(files, networkMetadata) { | 
|  | return files.filter(file => { | 
|  | const fileMetadata = file[this._metadataSymbol]; | 
|  | if (!fileMetadata) { | 
|  | return false; | 
|  | } | 
|  | // Allow a second of difference due to network timestamps lack of precision. | 
|  | const timeMatches = !networkMetadata.modificationTime || | 
|  | Math.abs(networkMetadata.modificationTime - fileMetadata.modificationTime) < 1000; | 
|  | const contentMatches = !networkMetadata.contentSize || fileMetadata.contentSize === networkMetadata.contentSize; | 
|  | return timeMatches && contentMatches; | 
|  | }); | 
|  | } | 
|  | } | 
|  |  | 
|  | /** | 
|  | * @unrestricted | 
|  | */ | 
|  | class FilePathIndex { | 
|  | /** | 
|  | * @param {!Persistence.PathEncoder} encoder | 
|  | */ | 
|  | constructor(encoder) { | 
|  | this._encoder = encoder; | 
|  | this._reversedIndex = new Common.Trie(); | 
|  | } | 
|  |  | 
|  | /** | 
|  | * @param {string} path | 
|  | */ | 
|  | addPath(path) { | 
|  | const encodedPath = this._encoder.encode(path); | 
|  | this._reversedIndex.add(encodedPath.reverse()); | 
|  | } | 
|  |  | 
|  | /** | 
|  | * @param {string} path | 
|  | */ | 
|  | removePath(path) { | 
|  | const encodedPath = this._encoder.encode(path); | 
|  | this._reversedIndex.remove(encodedPath.reverse()); | 
|  | } | 
|  |  | 
|  | /** | 
|  | * @param {string} networkPath | 
|  | * @return {!Array<string>} | 
|  | */ | 
|  | similarFiles(networkPath) { | 
|  | const encodedPath = this._encoder.encode(networkPath); | 
|  | const longestCommonPrefix = this._reversedIndex.longestPrefix(encodedPath.reverse(), false); | 
|  | if (!longestCommonPrefix) { | 
|  | return []; | 
|  | } | 
|  | return this._reversedIndex.words(longestCommonPrefix) | 
|  | .map(encodedPath => this._encoder.decode(encodedPath.reverse())); | 
|  | } | 
|  | } | 
|  |  | 
|  | /** | 
|  | * @unrestricted | 
|  | */ | 
|  | class FolderIndex { | 
|  | /** | 
|  | * @param {!Persistence.PathEncoder} encoder | 
|  | */ | 
|  | constructor(encoder) { | 
|  | this._encoder = encoder; | 
|  | this._index = new Common.Trie(); | 
|  | /** @type {!Map<string, number>} */ | 
|  | this._folderCount = new Map(); | 
|  | } | 
|  |  | 
|  | /** | 
|  | * @param {string} path | 
|  | * @return {boolean} | 
|  | */ | 
|  | addFolder(path) { | 
|  | if (path.endsWith('/')) { | 
|  | path = path.substring(0, path.length - 1); | 
|  | } | 
|  | const encodedPath = this._encoder.encode(path); | 
|  | this._index.add(encodedPath); | 
|  | const count = this._folderCount.get(encodedPath) || 0; | 
|  | this._folderCount.set(encodedPath, count + 1); | 
|  | return count === 0; | 
|  | } | 
|  |  | 
|  | /** | 
|  | * @param {string} path | 
|  | * @return {boolean} | 
|  | */ | 
|  | removeFolder(path) { | 
|  | if (path.endsWith('/')) { | 
|  | path = path.substring(0, path.length - 1); | 
|  | } | 
|  | const encodedPath = this._encoder.encode(path); | 
|  | const count = this._folderCount.get(encodedPath) || 0; | 
|  | if (!count) { | 
|  | return false; | 
|  | } | 
|  | if (count > 1) { | 
|  | this._folderCount.set(encodedPath, count - 1); | 
|  | return false; | 
|  | } | 
|  | this._index.remove(encodedPath); | 
|  | this._folderCount.delete(encodedPath); | 
|  | return true; | 
|  | } | 
|  |  | 
|  | /** | 
|  | * @param {string} path | 
|  | * @return {string} | 
|  | */ | 
|  | closestParentFolder(path) { | 
|  | const encodedPath = this._encoder.encode(path); | 
|  | const commonPrefix = this._index.longestPrefix(encodedPath, true); | 
|  | return this._encoder.decode(commonPrefix); | 
|  | } | 
|  | } | 
|  |  | 
|  | /** | 
|  | * @unrestricted | 
|  | */ | 
|  | export class AutomappingStatus { | 
|  | /** | 
|  | * @param {!Workspace.UISourceCode} network | 
|  | * @param {!Workspace.UISourceCode} fileSystem | 
|  | * @param {boolean} exactMatch | 
|  | */ | 
|  | constructor(network, fileSystem, exactMatch) { | 
|  | this.network = network; | 
|  | this.fileSystem = fileSystem; | 
|  | this.exactMatch = exactMatch; | 
|  | } | 
|  | } | 
|  |  | 
|  | /* Legacy exported object */ | 
|  | self.Persistence = self.Persistence || {}; | 
|  |  | 
|  | /* Legacy exported object */ | 
|  | Persistence = Persistence || {}; | 
|  |  | 
|  | /** @constructor */ | 
|  | Persistence.Automapping = Automapping; | 
|  |  | 
|  | /** @constructor */ | 
|  | Persistence.AutomappingStatus = AutomappingStatus; |