diff --git a/src/vs/platform/files/browser/htmlFileSystemProvider.ts b/src/vs/platform/files/browser/htmlFileSystemProvider.ts new file mode 100644 index 00000000000..021d89559d7 --- /dev/null +++ b/src/vs/platform/files/browser/htmlFileSystemProvider.ts @@ -0,0 +1,192 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { URI } from 'vs/base/common/uri'; +import { IFileSystemProviderWithFileReadWriteCapability, FileSystemProviderCapabilities, IFileChange, IWatchOptions, IStat, FileOverwriteOptions, FileType, FileDeleteOptions, FileWriteOptions } from 'vs/platform/files/common/files'; +import { Disposable, IDisposable } from 'vs/base/common/lifecycle'; +import { Event, Emitter } from 'vs/base/common/event'; +import { extUri } from 'vs/base/common/resources'; + +function split(path: string): [string, string] | undefined { + const match = /^(.*)\/([^/]+)$/.exec(path); + + if (!match) { + return undefined; + } + + const [, parentPath, name] = match; + return [parentPath, name]; +} + +function isRoot(uri: URI): boolean { + return /^(\/[^/]+)\/?$/.test(uri.path); +} + +export class HTMLFileSystemProvider implements IFileSystemProviderWithFileReadWriteCapability { + + private readonly files = new Map(); + private readonly directories = new Map(); + + readonly capabilities: FileSystemProviderCapabilities = + FileSystemProviderCapabilities.FileReadWrite + | FileSystemProviderCapabilities.PathCaseSensitive; + + readonly onDidChangeCapabilities = Event.None; + + private readonly _onDidChangeFile = new Emitter(); + readonly onDidChangeFile = this._onDidChangeFile.event; + + private readonly _onDidErrorOccur = new Emitter(); + readonly onDidErrorOccur = this._onDidErrorOccur.event; + + async readFile(resource: URI): Promise { + const handle = await this.getFileHandle(resource); + + if (!handle) { + throw new Error('File not found.'); + } + + const file = await handle.getFile(); + return new Uint8Array(await file.arrayBuffer()); + } + + async writeFile(resource: URI, content: Uint8Array, opts: FileWriteOptions): Promise { + const handle = await this.getFileHandle(resource); + + if (!handle) { + throw new Error('File not found.'); + } + + const writable = await handle.createWritable(); + await writable.write(content); + await writable.close(); + } + + watch(resource: URI, opts: IWatchOptions): IDisposable { + return Disposable.None; + } + + async stat(resource: URI): Promise { + const handler = this.files.get(resource.authority); + + if (handler) { + const file = await handler.getFile(); + + return { + type: FileType.File, + mtime: file.lastModified, + ctime: 0, + size: file.size + }; + } + + if (isRoot(resource)) { + return { + type: FileType.Directory, + mtime: 0, + ctime: 0, + size: 0 + }; + } + + const parent = await this.getParentDirectoryHandle(resource); + + if (!parent) { + throw new Error('Stat error: no parent found'); + } + + const name = extUri.basename(resource); + for await (const [childName, child] of parent) { + if (childName === name) { + if (child.kind === 'file') { + const file = await child.getFile(); + + return { + type: FileType.File, + mtime: file.lastModified, + ctime: 0, + size: file.size + }; + } else { + return { + type: FileType.Directory, + mtime: 0, + ctime: 0, + size: 0 + }; + } + } + } + + throw new Error('Stat error: entry not found'); + } + + mkdir(resource: URI): Promise { + throw new Error('Method not implemented.'); + } + + async readdir(resource: URI): Promise<[string, FileType][]> { + const parent = await this.getDirectoryHandle(resource); + + if (!parent) { + throw new Error('Stat error: no parent found'); + } + + const result: [string, FileType][] = []; + + for await (const [name, child] of parent) { + result.push([name, child.kind === 'file' ? FileType.File : FileType.Directory]); + } + + return result; + } + + delete(resource: URI, opts: FileDeleteOptions): Promise { + throw new Error('Method not implemented: delete'); + } + + rename(from: URI, to: URI, opts: FileOverwriteOptions): Promise { + throw new Error('Method not implemented: rename'); + } + + private async getDirectoryHandle(uri: URI): Promise { + if (isRoot(uri)) { + return this.directories.get(uri.authority); + } + + const splitResult = split(uri.path); + + if (!splitResult) { + return undefined; + } + + const parent = await this.getDirectoryHandle(URI.from({ ...uri, path: splitResult[0] })); + return await parent?.getDirectoryHandle(extUri.basename(uri)); + } + + private async getParentDirectoryHandle(uri: URI): Promise { + return this.getDirectoryHandle(URI.from({ ...uri, path: extUri.dirname(uri).path })); + } + + private async getFileHandle(uri: URI): Promise { + const result = this.files.get(uri.authority); + + if (result) { + return result; + } + + const parent = await this.getParentDirectoryHandle(uri); + const name = extUri.basename(uri); + return await parent?.getFileHandle(name); + } + + registerFileHandle(uuid: string, handle: FileSystemFileHandle): void { + this.files.set(uuid, handle); + } + + dispose(): void { + this._onDidChangeFile.dispose(); + } +} diff --git a/src/vs/workbench/browser/web.main.ts b/src/vs/workbench/browser/web.main.ts index 6bc9dc7348f..bed7bae394e 100644 --- a/src/vs/workbench/browser/web.main.ts +++ b/src/vs/workbench/browser/web.main.ts @@ -62,6 +62,7 @@ import { BrowserWindow } from 'vs/workbench/browser/window'; import { ITimerService } from 'vs/workbench/services/timer/browser/timerService'; import { WorkspaceTrustManagementService } from 'vs/workbench/services/workspaces/common/workspaceTrust'; import { IWorkspaceTrustManagementService } from 'vs/platform/workspace/common/workspaceTrust'; +import { HTMLFileSystemProvider } from 'vs/platform/files/browser/htmlFileSystemProvider'; class BrowserMain extends Disposable { @@ -307,6 +308,8 @@ class BrowserMain extends Disposable { } }); } + + fileService.registerProvider(Schemas.file, new HTMLFileSystemProvider()); } private async createStorageService(payload: IWorkspaceInitializationPayload, environmentService: IWorkbenchEnvironmentService, fileService: IFileService, logService: ILogService): Promise { diff --git a/src/vs/workbench/services/dialogs/browser/fileDialogService.ts b/src/vs/workbench/services/dialogs/browser/fileDialogService.ts index a6eb25f5abf..9d81b17e5c9 100644 --- a/src/vs/workbench/services/dialogs/browser/fileDialogService.ts +++ b/src/vs/workbench/services/dialogs/browser/fileDialogService.ts @@ -8,9 +8,17 @@ import { URI } from 'vs/base/common/uri'; import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; import { AbstractFileDialogService } from 'vs/workbench/services/dialogs/browser/abstractFileDialogService'; import { Schemas } from 'vs/base/common/network'; +import { memoize } from 'vs/base/common/decorators'; +import { HTMLFileSystemProvider } from 'vs/platform/files/browser/htmlFileSystemProvider'; +import { generateUuid } from 'vs/base/common/uuid'; export class FileDialogService extends AbstractFileDialogService implements IFileDialogService { + @memoize + private get fileSystemProvider(): HTMLFileSystemProvider { + return this.fileService.getProvider(Schemas.file) as HTMLFileSystemProvider; + } + async pickFileFolderAndOpen(options: IPickAndOpenOptions): Promise { const schema = this.getFileSystemSchema(options); @@ -18,7 +26,11 @@ export class FileDialogService extends AbstractFileDialogService implements IFil options.defaultUri = await this.defaultFilePath(schema); } - return this.pickFileFolderAndOpenSimplified(schema, options, false); + if (this.shouldUseSimplified(schema)) { + return this.pickFileFolderAndOpenSimplified(schema, options, false); + } + + throw new Error('Method not implemented.'); } async pickFileAndOpen(options: IPickAndOpenOptions): Promise { @@ -28,7 +40,17 @@ export class FileDialogService extends AbstractFileDialogService implements IFil options.defaultUri = await this.defaultFilePath(schema); } - return this.pickFileAndOpenSimplified(schema, options, false); + if (this.shouldUseSimplified(schema)) { + return this.pickFileAndOpenSimplified(schema, options, false); + } + + const [handle] = await window.showOpenFilePicker({ multiple: false }); + const uuid = generateUuid(); + const uri = URI.from({ scheme: Schemas.file, authority: uuid, path: `/${handle.name}` }); + + this.fileSystemProvider.registerFileHandle(uuid, handle); + + await this.openerService.open(uri, { fromUserGesture: true, editorOptions: { pinned: true } }); } async pickFolderAndOpen(options: IPickAndOpenOptions): Promise { @@ -38,7 +60,11 @@ export class FileDialogService extends AbstractFileDialogService implements IFil options.defaultUri = await this.defaultFolderPath(schema); } - return this.pickFolderAndOpenSimplified(schema, options); + if (this.shouldUseSimplified(schema)) { + return this.pickFolderAndOpenSimplified(schema, options); + } + + throw new Error('Method not implemented.'); } async pickWorkspaceAndOpen(options: IPickAndOpenOptions): Promise { @@ -48,27 +74,50 @@ export class FileDialogService extends AbstractFileDialogService implements IFil options.defaultUri = await this.defaultWorkspacePath(schema); } - return this.pickWorkspaceAndOpenSimplified(schema, options); + if (this.shouldUseSimplified(schema)) { + return this.pickWorkspaceAndOpenSimplified(schema, options); + } + + throw new Error('Method not implemented.'); } async pickFileToSave(defaultUri: URI, availableFileSystems?: string[]): Promise { const schema = this.getFileSystemSchema({ defaultUri, availableFileSystems }); - return this.pickFileToSaveSimplified(schema, this.getPickFileToSaveDialogOptions(defaultUri, availableFileSystems)); + + if (this.shouldUseSimplified(schema)) { + return this.pickFileToSaveSimplified(schema, this.getPickFileToSaveDialogOptions(defaultUri, availableFileSystems)); + } + + throw new Error('Method not implemented.'); } async showSaveDialog(options: ISaveDialogOptions): Promise { const schema = this.getFileSystemSchema(options); - return this.showSaveDialogSimplified(schema, options); + + if (this.shouldUseSimplified(schema)) { + return this.showSaveDialogSimplified(schema, options); + } + + throw new Error('Method not implemented.'); } async showOpenDialog(options: IOpenDialogOptions): Promise { const schema = this.getFileSystemSchema(options); - return this.showOpenDialogSimplified(schema, options); + + if (this.shouldUseSimplified(schema)) { + return this.showOpenDialogSimplified(schema, options); + } + + throw new Error('Method not implemented.'); } protected addFileSchemaIfNeeded(schema: string): string[] { return schema === Schemas.untitled ? [Schemas.file] : [schema]; } + + private shouldUseSimplified(schema: string): boolean { + return schema !== Schemas.file; + } } registerSingleton(IFileDialogService, FileDialogService, true); diff --git a/src/vs/workbench/services/path/browser/pathService.ts b/src/vs/workbench/services/path/browser/pathService.ts index dd43ac3fdc6..b1d5cac951d 100644 --- a/src/vs/workbench/services/path/browser/pathService.ts +++ b/src/vs/workbench/services/path/browser/pathService.ts @@ -39,7 +39,7 @@ function defaultUriScheme(environmentService: IWorkbenchEnvironmentService, cont return configuration.scheme; } - throw new Error('Empty workspace is not supported in browser when there is no remote connection.'); + return Schemas.file; } registerSingleton(IPathService, BrowserPathService, true);