diff --git a/extensions/vscode-api-tests/src/singlefolder-tests/workspace.fs.test.ts b/extensions/vscode-api-tests/src/singlefolder-tests/workspace.fs.test.ts new file mode 100644 index 0000000000000000000000000000000000000000..92fe43e125f34e9924b667d490b9ce6d71b470a1 --- /dev/null +++ b/extensions/vscode-api-tests/src/singlefolder-tests/workspace.fs.test.ts @@ -0,0 +1,119 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as assert from 'assert'; +import * as vscode from 'vscode'; +import { join } from 'path'; + +suite('workspace-fs', () => { + + let root: vscode.Uri; + + suiteSetup(function () { + root = vscode.workspace.workspaceFolders![0]!.uri; + }); + + test('fs.stat', async function () { + const stat = await vscode.workspace.fs.stat(root); + assert.equal(stat.type, vscode.FileType.Directory); + + assert.equal(typeof stat.size, 'number'); + assert.equal(typeof stat.mtime, 'number'); + assert.equal(typeof stat.ctime, 'number'); + + + const entries = await vscode.workspace.fs.readDirectory(root); + assert.ok(entries.length > 0); + + // find far.js + const tuple = entries.find(tuple => tuple[0] === 'far.js')!; + assert.ok(tuple); + assert.equal(tuple[0], 'far.js'); + assert.equal(tuple[1], vscode.FileType.File); + }); + + test('fs.stat - bad scheme', async function () { + try { + await vscode.workspace.fs.stat(vscode.Uri.parse('foo:/bar/baz/test.txt')); + assert.ok(false); + } catch { + assert.ok(true); + } + }); + + test('fs.stat - missing file', async function () { + try { + await vscode.workspace.fs.stat(root.with({ path: root.path + '.bad' })); + assert.ok(false); + } catch (e) { + assert.ok(true); + } + }); + + test('fs.write/stat/delete', async function () { + + const uri = root.with({ path: join(root.path, 'new.file') }); + await vscode.workspace.fs.writeFile(uri, Buffer.from('HELLO')); + + const stat = await vscode.workspace.fs.stat(uri); + assert.equal(stat.type, vscode.FileType.File); + + await vscode.workspace.fs.delete(uri); + + try { + await vscode.workspace.fs.stat(uri); + assert.ok(false); + } catch { + assert.ok(true); + } + }); + + test('fs.delete folder', async function () { + + const folder = root.with({ path: join(root.path, 'folder') }); + const file = root.with({ path: join(root.path, 'folder/file') }); + + await vscode.workspace.fs.createDirectory(folder); + await vscode.workspace.fs.writeFile(file, Buffer.from('FOO')); + + await vscode.workspace.fs.stat(folder); + await vscode.workspace.fs.stat(file); + + // ensure non empty folder cannot be deleted + try { + await vscode.workspace.fs.delete(folder, { recursive: false }); + assert.ok(false); + } catch { + await vscode.workspace.fs.stat(folder); + await vscode.workspace.fs.stat(file); + } + + // ensure non empty folder cannot be deleted is DEFAULT + try { + await vscode.workspace.fs.delete(folder); // recursive: false as default + assert.ok(false); + } catch { + await vscode.workspace.fs.stat(folder); + await vscode.workspace.fs.stat(file); + } + + // delete non empty folder with recursive-flag + await vscode.workspace.fs.delete(folder, { recursive: true }); + + // esnure folder/file are gone + try { + await vscode.workspace.fs.stat(folder); + assert.ok(false); + } catch { + assert.ok(true); + } + try { + await vscode.workspace.fs.stat(file); + assert.ok(false); + } catch { + assert.ok(true); + } + }); +}); diff --git a/src/vs/vscode.proposed.d.ts b/src/vs/vscode.proposed.d.ts index 0ac46132de54e404b930d81472c5460d0333dd5f..e2580b67495479f716130325461e9af9c34d5bcd 100644 --- a/src/vs/vscode.proposed.d.ts +++ b/src/vs/vscode.proposed.d.ts @@ -1519,4 +1519,25 @@ declare module 'vscode' { } //#endregion + + + //#region Joh - read/write files of any scheme + + export interface FileSystem { + stat(uri: Uri): Thenable; + readDirectory(uri: Uri): Thenable<[string, FileType][]>; + createDirectory(uri: Uri): Thenable; + readFile(uri: Uri): Thenable; + writeFile(uri: Uri, content: Uint8Array, options?: { create: boolean, overwrite: boolean }): Thenable; + delete(uri: Uri, options?: { recursive: boolean }): Thenable; + rename(oldUri: Uri, newUri: Uri, options?: { overwrite: boolean }): Thenable; + copy(source: Uri, destination: Uri, options?: { overwrite: boolean }): Thenable; + } + + export namespace workspace { + + export const fs: FileSystem; + } + + //#endregion } diff --git a/src/vs/workbench/api/browser/mainThreadFileSystem.ts b/src/vs/workbench/api/browser/mainThreadFileSystem.ts index 61871e2c39bd8ba23a4910ccb17473ff9cc5977a..2f395fd03e4fe7ff3d8d9e5cfbb1445a0be582b5 100644 --- a/src/vs/workbench/api/browser/mainThreadFileSystem.ts +++ b/src/vs/workbench/api/browser/mainThreadFileSystem.ts @@ -5,8 +5,8 @@ import { Emitter, Event } from 'vs/base/common/event'; import { IDisposable, dispose, toDisposable } from 'vs/base/common/lifecycle'; -import { URI } from 'vs/base/common/uri'; -import { FileWriteOptions, FileSystemProviderCapabilities, IFileChange, IFileService, IFileSystemProvider, IStat, IWatchOptions, FileType, FileOverwriteOptions, FileDeleteOptions, FileOpenOptions } from 'vs/platform/files/common/files'; +import { URI, UriComponents } from 'vs/base/common/uri'; +import { FileWriteOptions, FileSystemProviderCapabilities, IFileChange, IFileService, IFileSystemProvider, IStat, IWatchOptions, FileType, FileOverwriteOptions, FileDeleteOptions, FileOpenOptions, IFileStat } from 'vs/platform/files/common/files'; import { extHostNamedCustomer } from 'vs/workbench/api/common/extHostCustomers'; import { ExtHostContext, ExtHostFileSystemShape, IExtHostContext, IFileChangeDto, MainContext, MainThreadFileSystemShape } from '../common/extHost.protocol'; import { ResourceLabelFormatter, ILabelService } from 'vs/platform/label/common/label'; @@ -60,6 +60,56 @@ export class MainThreadFileSystem implements MainThreadFileSystemShape { } fileProvider.$onFileSystemChange(changes); } + + + // --- + + async $stat(uri: UriComponents): Promise { + const stat = await this._fileService.resolve(URI.revive(uri), { resolveMetadata: true }); + return { + ctime: 0, + mtime: stat.mtime, + size: stat.size, + type: MainThreadFileSystem._getFileType(stat) + }; + } + + async $readdir(uri: UriComponents): Promise<[string, FileType][]> { + const stat = await this._fileService.resolve(URI.revive(uri), { resolveMetadata: false }); + if (!stat.children) { + throw new Error('not a folder'); + } + return stat.children.map(child => [child.name, MainThreadFileSystem._getFileType(child)]); + } + + private static _getFileType(stat: IFileStat): FileType { + return (stat.isDirectory ? FileType.Directory : FileType.File) + (stat.isSymbolicLink ? FileType.SymbolicLink : 0); + } + + async $readFile(resource: UriComponents): Promise { + return (await this._fileService.readFile(URI.revive(resource))).value; + } + + async $writeFile(resource: UriComponents, content: VSBuffer, opts: FileWriteOptions): Promise { + //todo@joh honor opts + await this._fileService.writeFile(URI.revive(resource), content, {}); + } + + async $rename(resource: UriComponents, target: UriComponents, opts: FileOverwriteOptions): Promise { + this._fileService.move(URI.revive(resource), URI.revive(target), opts.overwrite); + } + + async $copy(resource: UriComponents, target: UriComponents, opts: FileOverwriteOptions): Promise { + this._fileService.copy(URI.revive(resource), URI.revive(target), opts.overwrite); + } + + async $mkdir(resource: UriComponents): Promise { + this._fileService.createFolder(URI.revive(resource)); + } + + async $delete(resource: UriComponents, opts: FileDeleteOptions): Promise { + this._fileService.del(URI.revive(resource), opts); + } } class RemoteFileSystemProvider implements IFileSystemProvider { diff --git a/src/vs/workbench/api/common/extHost.protocol.ts b/src/vs/workbench/api/common/extHost.protocol.ts index 58ec9762b80f0353589171c5f5301431d108d178..3257cafa4354786a7beb7c120061f84188d36965 100644 --- a/src/vs/workbench/api/common/extHost.protocol.ts +++ b/src/vs/workbench/api/common/extHost.protocol.ts @@ -592,6 +592,15 @@ export interface MainThreadFileSystemShape extends IDisposable { $registerResourceLabelFormatter(handle: number, formatter: ResourceLabelFormatter): void; $unregisterResourceLabelFormatter(handle: number): void; $onFileSystemChange(handle: number, resource: IFileChangeDto[]): void; + + $stat(uri: UriComponents): Promise; + $readdir(resource: UriComponents): Promise<[string, files.FileType][]>; + $readFile(resource: UriComponents): Promise; + $writeFile(resource: UriComponents, content: VSBuffer, opts: files.FileWriteOptions): Promise; + $rename(resource: UriComponents, target: UriComponents, opts: files.FileOverwriteOptions): Promise; + $copy(resource: UriComponents, target: UriComponents, opts: files.FileOverwriteOptions): Promise; + $mkdir(resource: UriComponents): Promise; + $delete(resource: UriComponents, opts: files.FileDeleteOptions): Promise; } export interface MainThreadSearchShape extends IDisposable { diff --git a/src/vs/workbench/api/common/extHostFileSystem.ts b/src/vs/workbench/api/common/extHostFileSystem.ts index da723e3d01b97270877c9289fed6cd4751e1c977..0159b6403ebbae6786f4f44baa6305ade182dbee 100644 --- a/src/vs/workbench/api/common/extHostFileSystem.ts +++ b/src/vs/workbench/api/common/extHostFileSystem.ts @@ -104,6 +104,36 @@ class FsLinkProvider { } } +class ConsumerFileSystem implements vscode.FileSystem { + + constructor(private _proxy: MainThreadFileSystemShape) { } + + stat(uri: vscode.Uri): Promise { + return this._proxy.$stat(uri); + } + readDirectory(uri: vscode.Uri): Promise<[string, vscode.FileType][]> { + return this._proxy.$readdir(uri); + } + createDirectory(uri: vscode.Uri): Promise { + return this._proxy.$mkdir(uri); + } + async readFile(uri: vscode.Uri): Promise { + return (await this._proxy.$readFile(uri)).buffer; + } + writeFile(uri: vscode.Uri, content: Uint8Array, options: { create: boolean; overwrite: boolean; } = { create: true, overwrite: true }): Promise { + return this._proxy.$writeFile(uri, VSBuffer.wrap(content), options); + } + delete(uri: vscode.Uri, options: { recursive: boolean; } = { recursive: false }): Promise { + return this._proxy.$delete(uri, { ...options, useTrash: true }); //todo@joh useTrash + } + rename(oldUri: vscode.Uri, newUri: vscode.Uri, options: { overwrite: boolean; } = { overwrite: false }): Promise { + return this._proxy.$rename(oldUri, newUri, options); + } + copy(source: vscode.Uri, destination: vscode.Uri, options: { overwrite: boolean } = { overwrite: false }): Promise { + return this._proxy.$copy(source, destination, options); + } +} + export class ExtHostFileSystem implements ExtHostFileSystemShape { private readonly _proxy: MainThreadFileSystemShape; @@ -115,6 +145,8 @@ export class ExtHostFileSystem implements ExtHostFileSystemShape { private _linkProviderRegistration: IDisposable; private _handlePool: number = 0; + readonly fileSystem: vscode.FileSystem; + constructor(mainContext: IMainContext, private _extHostLanguageFeatures: ExtHostLanguageFeatures) { this._proxy = mainContext.getProxy(MainContext.MainThreadFileSystem); this._usedSchemes.add(Schemas.file); @@ -127,6 +159,8 @@ export class ExtHostFileSystem implements ExtHostFileSystemShape { this._usedSchemes.add(Schemas.mailto); this._usedSchemes.add(Schemas.data); this._usedSchemes.add(Schemas.command); + + this.fileSystem = new ConsumerFileSystem(this._proxy); } dispose(): void { diff --git a/src/vs/workbench/api/node/extHost.api.impl.ts b/src/vs/workbench/api/node/extHost.api.impl.ts index 8818154961399867fe51c10c37ad6f732334d343..dd6c14f666b0b2272850317c5556ad5b11753aee 100644 --- a/src/vs/workbench/api/node/extHost.api.impl.ts +++ b/src/vs/workbench/api/node/extHost.api.impl.ts @@ -673,6 +673,10 @@ export function createApiFactory( registerFileSystemProvider(scheme, provider, options) { return extHostFileSystem.registerFileSystemProvider(scheme, provider, options); }, + get fs() { + checkProposedApiEnabled(extension); + return extHostFileSystem.fileSystem; + }, registerFileSearchProvider: proposedApiFunction(extension, (scheme: string, provider: vscode.FileSearchProvider) => { return extHostSearch.registerFileSearchProvider(scheme, provider); }),