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);