| // Copyright 2013 The Chromium Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| import * as Common from '../../core/common/common.js'; |
| import * as Host from '../../core/host/host.js'; |
| import * as Platform from '../../core/platform/platform.js'; |
| import * as TextUtils from '../text_utils/text_utils.js'; |
| import * as Workspace from '../workspace/workspace.js'; |
| |
| import type {IsolatedFileSystem} from './IsolatedFileSystem.js'; |
| import {Events, type IsolatedFileSystemManager} from './IsolatedFileSystemManager.js'; |
| import type {PlatformFileSystem, PlatformFileSystemType} from './PlatformFileSystem.js'; |
| |
| export class FileSystemWorkspaceBinding { |
| readonly isolatedFileSystemManager: IsolatedFileSystemManager; |
| readonly #workspace: Workspace.Workspace.WorkspaceImpl; |
| readonly #eventListeners: Common.EventTarget.EventDescriptor[]; |
| readonly #boundFileSystems = new Map<string, FileSystem>(); |
| constructor(isolatedFileSystemManager: IsolatedFileSystemManager, workspace: Workspace.Workspace.WorkspaceImpl) { |
| this.isolatedFileSystemManager = isolatedFileSystemManager; |
| this.#workspace = workspace; |
| this.#eventListeners = [ |
| this.isolatedFileSystemManager.addEventListener(Events.FileSystemAdded, this.onFileSystemAdded, this), |
| this.isolatedFileSystemManager.addEventListener(Events.FileSystemRemoved, this.onFileSystemRemoved, this), |
| this.isolatedFileSystemManager.addEventListener(Events.FileSystemFilesChanged, this.fileSystemFilesChanged, this), |
| ]; |
| void this.isolatedFileSystemManager.waitForFileSystems().then(this.onFileSystemsLoaded.bind(this)); |
| } |
| |
| static projectId(fileSystemPath: Platform.DevToolsPath.UrlString): Platform.DevToolsPath.UrlString { |
| return fileSystemPath; |
| } |
| |
| static relativePath(uiSourceCode: Workspace.UISourceCode.UISourceCode): Platform.DevToolsPath.EncodedPathString[] { |
| const baseURL = (uiSourceCode.project() as FileSystem).fileSystemBaseURL; |
| return Common.ParsedURL.ParsedURL.split( |
| Common.ParsedURL.ParsedURL.sliceUrlToEncodedPathString(uiSourceCode.url(), baseURL.length), '/'); |
| } |
| |
| static tooltipForUISourceCode(uiSourceCode: Workspace.UISourceCode.UISourceCode): string { |
| const fileSystem = (uiSourceCode.project() as FileSystem).fileSystem(); |
| return fileSystem.tooltipForURL(uiSourceCode.url()); |
| } |
| |
| static fileSystemType(project: Workspace.Workspace.Project): PlatformFileSystemType { |
| if (project instanceof FileSystem) { |
| return project.fileSystem().type(); |
| } |
| throw new TypeError('project is not a FileSystem'); |
| } |
| |
| static fileSystemSupportsAutomapping(project: Workspace.Workspace.Project): boolean { |
| const fileSystem = (project as FileSystem).fileSystem(); |
| return fileSystem.supportsAutomapping(); |
| } |
| |
| static completeURL(project: Workspace.Workspace.Project, relativePath: string): Platform.DevToolsPath.UrlString { |
| const fsProject = project as FileSystem; |
| return Common.ParsedURL.ParsedURL.concatenate(fsProject.fileSystemBaseURL, relativePath); |
| } |
| |
| static fileSystemPath(projectId: Platform.DevToolsPath.UrlString): Platform.DevToolsPath.UrlString { |
| return projectId; |
| } |
| |
| private onFileSystemsLoaded(fileSystems: IsolatedFileSystem[]): void { |
| for (const fileSystem of fileSystems) { |
| this.addFileSystem(fileSystem); |
| } |
| } |
| |
| private onFileSystemAdded(event: Common.EventTarget.EventTargetEvent<PlatformFileSystem>): void { |
| const fileSystem = event.data; |
| this.addFileSystem(fileSystem); |
| } |
| |
| private addFileSystem(fileSystem: PlatformFileSystem): void { |
| const boundFileSystem = new FileSystem(this, fileSystem, this.#workspace); |
| this.#boundFileSystems.set(fileSystem.path(), boundFileSystem); |
| } |
| |
| private onFileSystemRemoved(event: Common.EventTarget.EventTargetEvent<PlatformFileSystem>): void { |
| const fileSystem = event.data; |
| const boundFileSystem = this.#boundFileSystems.get(fileSystem.path()); |
| if (boundFileSystem) { |
| boundFileSystem.dispose(); |
| } |
| this.#boundFileSystems.delete(fileSystem.path()); |
| } |
| |
| private fileSystemFilesChanged(event: Common.EventTarget.EventTargetEvent<FilesChangedData>): void { |
| const paths = event.data; |
| for (const fileSystemPath of paths.changed.keysArray()) { |
| const fileSystem = this.#boundFileSystems.get(fileSystemPath); |
| if (!fileSystem) { |
| continue; |
| } |
| paths.changed.get(fileSystemPath).forEach(path => fileSystem.fileChanged(path)); |
| } |
| |
| for (const fileSystemPath of paths.added.keysArray()) { |
| const fileSystem = this.#boundFileSystems.get(fileSystemPath); |
| if (!fileSystem) { |
| continue; |
| } |
| paths.added.get(fileSystemPath).forEach(path => fileSystem.fileChanged(path)); |
| } |
| |
| for (const fileSystemPath of paths.removed.keysArray()) { |
| const fileSystem = this.#boundFileSystems.get(fileSystemPath); |
| if (!fileSystem) { |
| continue; |
| } |
| paths.removed.get(fileSystemPath).forEach(path => fileSystem.removeUISourceCode(path)); |
| } |
| } |
| |
| dispose(): void { |
| Common.EventTarget.removeEventListeners(this.#eventListeners); |
| for (const fileSystem of this.#boundFileSystems.values()) { |
| fileSystem.dispose(); |
| this.#boundFileSystems.delete(fileSystem.fileSystem().path()); |
| } |
| } |
| } |
| |
| export class FileSystem extends Workspace.Workspace.ProjectStore { |
| #fileSystem: PlatformFileSystem; |
| readonly fileSystemBaseURL: Platform.DevToolsPath.UrlString; |
| readonly #fileSystemParentURL: Platform.DevToolsPath.UrlString; |
| readonly #fileSystemWorkspaceBinding: FileSystemWorkspaceBinding; |
| readonly #fileSystemPath: Platform.DevToolsPath.UrlString; |
| readonly #creatingFilesGuard = new Set<string>(); |
| |
| constructor( |
| fileSystemWorkspaceBinding: FileSystemWorkspaceBinding, isolatedFileSystem: PlatformFileSystem, |
| workspace: Workspace.Workspace.WorkspaceImpl) { |
| const fileSystemPath = isolatedFileSystem.path(); |
| const id = FileSystemWorkspaceBinding.projectId(fileSystemPath); |
| console.assert(!workspace.project(id)); |
| const displayName = fileSystemPath.substr(fileSystemPath.lastIndexOf('/') + 1); |
| |
| super(workspace, id, Workspace.Workspace.projectTypes.FileSystem, displayName); |
| |
| this.#fileSystem = isolatedFileSystem; |
| this.fileSystemBaseURL = Common.ParsedURL.ParsedURL.concatenate(this.#fileSystem.path(), '/'); |
| this.#fileSystemParentURL = |
| Common.ParsedURL.ParsedURL.substr(this.fileSystemBaseURL, 0, fileSystemPath.lastIndexOf('/') + 1); |
| this.#fileSystemWorkspaceBinding = fileSystemWorkspaceBinding; |
| this.#fileSystemPath = fileSystemPath; |
| |
| workspace.addProject(this); |
| this.populate(); |
| } |
| |
| fileSystemPath(): Platform.DevToolsPath.UrlString { |
| return this.#fileSystemPath; |
| } |
| |
| fileSystem(): PlatformFileSystem { |
| return this.#fileSystem; |
| } |
| |
| mimeType(uiSourceCode: Workspace.UISourceCode.UISourceCode): string { |
| return this.#fileSystem.mimeFromPath(uiSourceCode.url()); |
| } |
| |
| initialGitFolders(): Platform.DevToolsPath.UrlString[] { |
| return this.#fileSystem.initialGitFolders().map( |
| folder => Common.ParsedURL.ParsedURL.concatenate(this.#fileSystemPath, '/', folder)); |
| } |
| |
| private filePathForUISourceCode(uiSourceCode: Workspace.UISourceCode.UISourceCode): |
| Platform.DevToolsPath.EncodedPathString { |
| return Common.ParsedURL.ParsedURL.sliceUrlToEncodedPathString(uiSourceCode.url(), this.#fileSystemPath.length); |
| } |
| |
| isServiceProject(): boolean { |
| return false; |
| } |
| |
| requestMetadata(uiSourceCode: Workspace.UISourceCode.UISourceCode): |
| Promise<Workspace.UISourceCode.UISourceCodeMetadata|null> { |
| const metadata = sourceCodeToMetadataMap.get(uiSourceCode); |
| if (metadata) { |
| return metadata; |
| } |
| const relativePath = this.filePathForUISourceCode(uiSourceCode); |
| const promise = this.#fileSystem.getMetadata(relativePath).then(onMetadata); |
| sourceCodeToMetadataMap.set(uiSourceCode, promise); |
| return promise; |
| |
| function onMetadata(metadata: {modificationTime: Date, size: number}|null): |
| Workspace.UISourceCode.UISourceCodeMetadata|null { |
| if (!metadata) { |
| return null; |
| } |
| return new Workspace.UISourceCode.UISourceCodeMetadata(metadata.modificationTime, metadata.size); |
| } |
| } |
| |
| requestFileBlob(uiSourceCode: Workspace.UISourceCode.UISourceCode): Promise<Blob|null> { |
| return this.#fileSystem.requestFileBlob(this.filePathForUISourceCode(uiSourceCode)); |
| } |
| |
| requestFileContent(uiSourceCode: Workspace.UISourceCode.UISourceCode): |
| Promise<TextUtils.ContentData.ContentDataOrError> { |
| const filePath = this.filePathForUISourceCode(uiSourceCode); |
| return this.#fileSystem.requestFileContent(filePath); |
| } |
| |
| canSetFileContent(): boolean { |
| return true; |
| } |
| |
| async setFileContent(uiSourceCode: Workspace.UISourceCode.UISourceCode, newContent: string, isBase64: boolean): |
| Promise<void> { |
| const filePath = this.filePathForUISourceCode(uiSourceCode); |
| this.#fileSystem.setFileContent(filePath, newContent, isBase64); |
| } |
| |
| fullDisplayName(uiSourceCode: Workspace.UISourceCode.UISourceCode): string { |
| const baseURL = (uiSourceCode.project() as FileSystem).#fileSystemParentURL; |
| return uiSourceCode.url().substring(baseURL.length); |
| } |
| |
| canRename(): boolean { |
| return true; |
| } |
| |
| override rename( |
| uiSourceCode: Workspace.UISourceCode.UISourceCode, newName: Platform.DevToolsPath.RawPathString, |
| callback: |
| (arg0: boolean, arg1?: string|undefined, arg2?: Platform.DevToolsPath.UrlString|undefined, |
| arg3?: Common.ResourceType.ResourceType|undefined) => void): void { |
| if (newName === uiSourceCode.name()) { |
| callback(true, uiSourceCode.name(), uiSourceCode.url(), uiSourceCode.contentType()); |
| return; |
| } |
| |
| let filePath = this.filePathForUISourceCode(uiSourceCode); |
| this.#fileSystem.renameFile(filePath, newName, innerCallback.bind(this)); |
| |
| function innerCallback(this: FileSystem, success: boolean, newName?: string): void { |
| if (!success || !newName) { |
| callback(false, newName); |
| return; |
| } |
| console.assert(Boolean(newName)); |
| const slash = filePath.lastIndexOf('/'); |
| const parentPath = Common.ParsedURL.ParsedURL.substr(filePath, 0, slash); |
| filePath = Common.ParsedURL.ParsedURL.encodedFromParentPathAndName(parentPath, newName); |
| filePath = Common.ParsedURL.ParsedURL.substr(filePath, 1); |
| const newURL = Common.ParsedURL.ParsedURL.concatenate(this.fileSystemBaseURL, filePath); |
| const newContentType = this.#fileSystem.contentType(newName); |
| this.renameUISourceCode(uiSourceCode, newName); |
| callback(true, newName, newURL, newContentType); |
| } |
| } |
| |
| async searchInFileContent( |
| uiSourceCode: Workspace.UISourceCode.UISourceCode, query: string, caseSensitive: boolean, |
| isRegex: boolean): Promise<TextUtils.ContentProvider.SearchMatch[]> { |
| const filePath = this.filePathForUISourceCode(uiSourceCode); |
| const content = await this.#fileSystem.requestFileContent(filePath); |
| return TextUtils.TextUtils.performSearchInContentData(content, query, caseSensitive, isRegex); |
| } |
| |
| async findFilesMatchingSearchRequest( |
| searchConfig: Workspace.SearchConfig.SearchConfig, filesMatchingFileQuery: Workspace.UISourceCode.UISourceCode[], |
| progress: Common.Progress.Progress): |
| Promise<Map<Workspace.UISourceCode.UISourceCode, TextUtils.ContentProvider.SearchMatch[]|null>> { |
| let workingFileSet: string[] = filesMatchingFileQuery.map(uiSoureCode => uiSoureCode.url()); |
| const queriesToRun = searchConfig.queries().slice(); |
| if (!queriesToRun.length) { |
| queriesToRun.push(''); |
| } |
| progress.totalWork = queriesToRun.length; |
| |
| for (const query of queriesToRun) { |
| const files = await this.#fileSystem.searchInPath(searchConfig.isRegex() ? '' : query, progress); |
| files.sort(Platform.StringUtilities.naturalOrderComparator); |
| workingFileSet = Platform.ArrayUtilities.intersectOrdered( |
| workingFileSet, files, Platform.StringUtilities.naturalOrderComparator); |
| ++progress.worked; |
| } |
| |
| const result = new Map(); |
| for (const file of workingFileSet) { |
| const uiSourceCode = this.uiSourceCodeForURL(file as Platform.DevToolsPath.UrlString); |
| if (uiSourceCode) { |
| result.set(uiSourceCode, null); |
| } |
| } |
| |
| progress.done = true; |
| return result; |
| } |
| |
| override indexContent(progress: Common.Progress.Progress): void { |
| this.#fileSystem.indexContent(progress); |
| } |
| |
| populate(): void { |
| const filePaths = this.#fileSystem.initialFilePaths(); |
| if (filePaths.length === 0) { |
| return; |
| } |
| |
| const chunkSize = 1000; |
| const startTime = performance.now(); |
| reportFileChunk.call(this, 0); |
| |
| function reportFileChunk(this: FileSystem, from: number): void { |
| const to = Math.min(from + chunkSize, filePaths.length); |
| for (let i = from; i < to; ++i) { |
| this.addFile(filePaths[i]); |
| } |
| if (to < filePaths.length) { |
| window.setTimeout(reportFileChunk.bind(this, to), 100); |
| } else if (this.type() === 'filesystem') { |
| Host.userMetrics.workspacesPopulated(performance.now() - startTime); |
| } |
| } |
| } |
| |
| override excludeFolder(url: Platform.DevToolsPath.UrlString): void { |
| let relativeFolder = Common.ParsedURL.ParsedURL.sliceUrlToEncodedPathString(url, this.fileSystemBaseURL.length); |
| if (!relativeFolder.startsWith('/')) { |
| relativeFolder = Common.ParsedURL.ParsedURL.prepend('/', relativeFolder); |
| } |
| if (!relativeFolder.endsWith('/')) { |
| relativeFolder = Common.ParsedURL.ParsedURL.concatenate(relativeFolder, '/'); |
| } |
| this.#fileSystem.addExcludedFolder(relativeFolder); |
| |
| for (const uiSourceCode of this.uiSourceCodes()) { |
| if (uiSourceCode.url().startsWith(url)) { |
| this.removeUISourceCode(uiSourceCode.url()); |
| } |
| } |
| } |
| |
| canExcludeFolder(path: Platform.DevToolsPath.EncodedPathString): boolean { |
| return this.#fileSystem.canExcludeFolder(path); |
| } |
| |
| canCreateFile(): boolean { |
| return true; |
| } |
| |
| async createFile( |
| path: Platform.DevToolsPath.EncodedPathString, name: Platform.DevToolsPath.RawPathString|null, content: string, |
| isBase64?: boolean): Promise<Workspace.UISourceCode.UISourceCode|null> { |
| const guardFileName = this.#fileSystemPath + path + (!path.endsWith('/') ? '/' : '') + name; |
| this.#creatingFilesGuard.add(guardFileName); |
| const filePath = await this.#fileSystem.createFile(path, name); |
| if (!filePath) { |
| return null; |
| } |
| const uiSourceCode = this.addFile(filePath, content, isBase64); |
| this.#creatingFilesGuard.delete(guardFileName); |
| return uiSourceCode; |
| } |
| |
| override deleteFile(uiSourceCode: Workspace.UISourceCode.UISourceCode): void { |
| const relativePath = this.filePathForUISourceCode(uiSourceCode); |
| void this.#fileSystem.deleteFile(relativePath).then(success => { |
| if (success) { |
| this.removeUISourceCode(uiSourceCode.url()); |
| } |
| }); |
| } |
| |
| override deleteDirectoryRecursively(path: Platform.DevToolsPath.EncodedPathString): Promise<boolean> { |
| return this.#fileSystem.deleteDirectoryRecursively(path); |
| } |
| |
| override remove(): void { |
| this.#fileSystemWorkspaceBinding.isolatedFileSystemManager.removeFileSystem(this.#fileSystem); |
| } |
| |
| private addFile(filePath: Platform.DevToolsPath.EncodedPathString, content?: string, isBase64?: boolean): |
| Workspace.UISourceCode.UISourceCode { |
| const contentType = this.#fileSystem.contentType(filePath); |
| const uiSourceCode = |
| this.createUISourceCode(Common.ParsedURL.ParsedURL.concatenate(this.fileSystemBaseURL, filePath), contentType); |
| if (content !== undefined) { |
| uiSourceCode.setContent(content, Boolean(isBase64)); |
| } |
| this.addUISourceCode(uiSourceCode); |
| return uiSourceCode; |
| } |
| |
| fileChanged(path: Platform.DevToolsPath.UrlString): void { |
| // Ignore files that are being created but do not have content yet. |
| if (this.#creatingFilesGuard.has(path)) { |
| return; |
| } |
| const uiSourceCode = this.uiSourceCodeForURL(path); |
| if (!uiSourceCode) { |
| const contentType = this.#fileSystem.contentType(path); |
| this.addUISourceCode(this.createUISourceCode(path, contentType)); |
| return; |
| } |
| sourceCodeToMetadataMap.delete(uiSourceCode); |
| void uiSourceCode.checkContentUpdated(); |
| } |
| |
| tooltipForURL(url: Platform.DevToolsPath.UrlString): string { |
| return this.#fileSystem.tooltipForURL(url); |
| } |
| |
| dispose(): void { |
| this.removeProject(); |
| } |
| } |
| |
| const sourceCodeToMetadataMap = |
| new WeakMap<Workspace.UISourceCode.UISourceCode, Promise<Workspace.UISourceCode.UISourceCodeMetadata|null>>(); |
| export interface FilesChangedData { |
| changed: Platform.MapUtilities.Multimap<Platform.DevToolsPath.UrlString, Platform.DevToolsPath.UrlString>; |
| added: Platform.MapUtilities.Multimap<Platform.DevToolsPath.UrlString, Platform.DevToolsPath.UrlString>; |
| removed: Platform.MapUtilities.Multimap<Platform.DevToolsPath.UrlString, Platform.DevToolsPath.UrlString>; |
| } |