Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

vscode.workspace.fs #75882

Merged
merged 2 commits into from
Jun 24, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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);
}
});
});
21 changes: 21 additions & 0 deletions src/vs/vscode.proposed.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1519,4 +1519,25 @@ declare module 'vscode' {
}

//#endregion


//#region Joh - read/write files of any scheme

export interface FileSystem {
stat(uri: Uri): Thenable<FileStat>;
readDirectory(uri: Uri): Thenable<[string, FileType][]>;
createDirectory(uri: Uri): Thenable<void>;
readFile(uri: Uri): Thenable<Uint8Array>;
writeFile(uri: Uri, content: Uint8Array, options?: { create: boolean, overwrite: boolean }): Thenable<void>;
delete(uri: Uri, options?: { recursive: boolean }): Thenable<void>;
rename(source: Uri, target: Uri, options?: { overwrite: boolean }): Thenable<void>;
copy(source: Uri, target: Uri, options?: { overwrite: boolean }): Thenable<void>;
}

export namespace workspace {

export const fs: FileSystem;
}

//#endregion
}
54 changes: 52 additions & 2 deletions src/vs/workbench/api/browser/mainThreadFileSystem.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -60,6 +60,56 @@ export class MainThreadFileSystem implements MainThreadFileSystemShape {
}
fileProvider.$onFileSystemChange(changes);
}


// ---

async $stat(uri: UriComponents): Promise<IStat> {
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(uri: UriComponents): Promise<VSBuffer> {
return (await this._fileService.readFile(URI.revive(uri))).value;
}

async $writeFile(uri: UriComponents, content: VSBuffer, opts: FileWriteOptions): Promise<void> {
//todo@joh honor opts
await this._fileService.writeFile(URI.revive(uri), content, {});
}

async $rename(source: UriComponents, target: UriComponents, opts: FileOverwriteOptions): Promise<void> {
this._fileService.move(URI.revive(source), URI.revive(target), opts.overwrite);
}

async $copy(source: UriComponents, target: UriComponents, opts: FileOverwriteOptions): Promise<void> {
this._fileService.copy(URI.revive(source), URI.revive(target), opts.overwrite);
}

async $mkdir(uri: UriComponents): Promise<void> {
this._fileService.createFolder(URI.revive(uri));
}

async $delete(uri: UriComponents, opts: FileDeleteOptions): Promise<void> {
this._fileService.del(URI.revive(uri), opts);
}
}

class RemoteFileSystemProvider implements IFileSystemProvider {
Expand Down
9 changes: 9 additions & 0 deletions src/vs/workbench/api/common/extHost.protocol.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<files.IStat>;
$readdir(resource: UriComponents): Promise<[string, files.FileType][]>;
$readFile(resource: UriComponents): Promise<VSBuffer>;
$writeFile(resource: UriComponents, content: VSBuffer, opts: files.FileWriteOptions): Promise<void>;
$rename(resource: UriComponents, target: UriComponents, opts: files.FileOverwriteOptions): Promise<void>;
$copy(resource: UriComponents, target: UriComponents, opts: files.FileOverwriteOptions): Promise<void>;
$mkdir(resource: UriComponents): Promise<void>;
$delete(resource: UriComponents, opts: files.FileDeleteOptions): Promise<void>;
}

export interface MainThreadSearchShape extends IDisposable {
Expand Down
34 changes: 34 additions & 0 deletions src/vs/workbench/api/common/extHostFileSystem.ts
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,36 @@ class FsLinkProvider {
}
}

class ConsumerFileSystem implements vscode.FileSystem {

constructor(private _proxy: MainThreadFileSystemShape) { }

stat(uri: vscode.Uri): Promise<vscode.FileStat> {
return this._proxy.$stat(uri);
}
readDirectory(uri: vscode.Uri): Promise<[string, vscode.FileType][]> {
return this._proxy.$readdir(uri);
}
createDirectory(uri: vscode.Uri): Promise<void> {
return this._proxy.$mkdir(uri);
}
async readFile(uri: vscode.Uri): Promise<Uint8Array> {
return (await this._proxy.$readFile(uri)).buffer;
}
writeFile(uri: vscode.Uri, content: Uint8Array, options: { create: boolean; overwrite: boolean; } = { create: true, overwrite: true }): Promise<void> {
return this._proxy.$writeFile(uri, VSBuffer.wrap(content), options);
}
delete(uri: vscode.Uri, options: { recursive: boolean; } = { recursive: false }): Promise<void> {
return this._proxy.$delete(uri, { ...options, useTrash: true }); //todo@joh useTrash
}
rename(oldUri: vscode.Uri, newUri: vscode.Uri, options: { overwrite: boolean; } = { overwrite: false }): Promise<void> {
return this._proxy.$rename(oldUri, newUri, options);
}
copy(source: vscode.Uri, destination: vscode.Uri, options: { overwrite: boolean } = { overwrite: false }): Promise<void> {
return this._proxy.$copy(source, destination, options);
}
}

export class ExtHostFileSystem implements ExtHostFileSystemShape {

private readonly _proxy: MainThreadFileSystemShape;
Expand All @@ -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);
Expand All @@ -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 {
Expand Down
4 changes: 4 additions & 0 deletions src/vs/workbench/api/node/extHost.api.impl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}),
Expand Down