提交 44ed24d5 编写于 作者: B Benjamin Pasero

files2 - implement resolving

上级 cd10c5bd
......@@ -701,7 +701,16 @@ export interface IUpdateContentOptions {
}
export interface IResolveFileOptions {
/**
* Automatically continue resolving children of a directory until the provided resources
* are found.
*/
resolveTo?: URI[];
/**
* Automatically continue resolving children of a directory if the number of children is 1.
*/
resolveSingleChildDescendants?: boolean;
}
......@@ -1034,10 +1043,6 @@ export interface ILegacyFileService {
onFileChanges: Event<FileChangesEvent>;
onAfterOperation: Event<FileOperationEvent>;
resolveFile(resource: URI, options?: IResolveFileOptions): Promise<IFileStat>;
resolveFiles(toResolve: { resource: URI, options?: IResolveFileOptions }[]): Promise<IResolveFileResult[]>;
resolveContent(resource: URI, options?: IResolveContentOptions): Promise<IContent>;
resolveStreamContent(resource: URI, options?: IResolveContentOptions): Promise<IStreamContent>;
......
......@@ -217,15 +217,6 @@ export class FileService extends Disposable implements ILegacyFileService {
return resource.scheme === Schemas.file;
}
resolveFile(resource: uri, options?: IResolveFileOptions): Promise<IFileStat> {
return this.resolve(resource, options);
}
resolveFiles(toResolve: { resource: uri, options?: IResolveFileOptions }[]): Promise<IResolveFileResult[]> {
return Promise.all(toResolve.map(resourceAndOptions => this.resolve(resourceAndOptions.resource, resourceAndOptions.options)
.then(stat => ({ stat, success: true }), error => ({ stat: undefined, success: false }))));
}
resolveContent(resource: uri, options?: IResolveContentOptions): Promise<IContent> {
return this.resolveStreamContent(resource, options).then(streamContent => {
return new Promise<IContent>((resolve, reject) => {
......@@ -1092,6 +1083,15 @@ export class FileService extends Disposable implements ILegacyFileService {
// Tests only
resolveFile(resource: uri, options?: IResolveFileOptions): Promise<IFileStat> {
return this.resolve(resource, options);
}
resolveFiles(toResolve: { resource: uri, options?: IResolveFileOptions }[]): Promise<IResolveFileResult[]> {
return Promise.all(toResolve.map(resourceAndOptions => this.resolve(resourceAndOptions.resource, resourceAndOptions.options)
.then(stat => ({ stat, success: true }), error => ({ stat: undefined, success: false }))));
}
createFolder(resource: uri): Promise<IFileStat> {
// 1.) Create folder
......
......@@ -13,7 +13,6 @@ import { URI as uri } from 'vs/base/common/uri';
import * as uuid from 'vs/base/common/uuid';
import * as pfs from 'vs/base/node/pfs';
import * as encodingLib from 'vs/base/node/encoding';
import * as utils from 'vs/workbench/services/files/test/electron-browser/utils';
import { TestEnvironmentService, TestContextService, TestTextResourceConfigurationService, TestLifecycleService, TestStorageService } from 'vs/workbench/test/workbenchTestServices';
import { getRandomTestPath } from 'vs/base/test/node/testUtils';
import { TestNotificationService } from 'vs/platform/notification/test/common/testNotificationService';
......@@ -456,33 +455,6 @@ suite('FileService', () => {
});
});
test('resolveFile', () => {
return service.resolveFile(uri.file(testDir), { resolveTo: [uri.file(path.join(testDir, 'deep'))] }).then(r => {
assert.equal(r.children!.length, 8);
const deep = utils.getByName(r, 'deep')!;
assert.equal(deep.children!.length, 4);
});
});
test('resolveFiles', () => {
return service.resolveFiles([
{ resource: uri.file(testDir), options: { resolveTo: [uri.file(path.join(testDir, 'deep'))] } },
{ resource: uri.file(path.join(testDir, 'deep')) }
]).then(res => {
const r1 = res[0].stat!;
assert.equal(r1.children!.length, 8);
const deep = utils.getByName(r1, 'deep')!;
assert.equal(deep.children!.length, 4);
const r2 = res[1].stat!;
assert.equal(r2.children!.length, 4);
assert.equal(r2.name, 'deep');
});
});
test('updateContent', () => {
const resource = uri.file(path.join(testDir, 'small.txt'));
......
......@@ -4,13 +4,17 @@
*--------------------------------------------------------------------------------------------*/
import { Disposable, IDisposable, toDisposable, combinedDisposable } from 'vs/base/common/lifecycle';
import { IFileService, IResolveFileOptions, IResourceEncodings, FileChangesEvent, FileOperationEvent, IFileSystemProviderRegistrationEvent, IFileSystemProvider, IFileStat, IResolveFileResult, IResolveContentOptions, IContent, IStreamContent, ITextSnapshot, IUpdateContentOptions, ICreateFileOptions, IFileSystemProviderActivationEvent, FileOperationError, FileOperationResult, FileOperation, FileSystemProviderCapabilities, FileType, toFileSystemProviderErrorCode, FileSystemProviderErrorCode } from 'vs/platform/files/common/files';
import { IFileService, IResolveFileOptions, IResourceEncodings, FileChangesEvent, FileOperationEvent, IFileSystemProviderRegistrationEvent, IFileSystemProvider, IFileStat, IResolveFileResult, IResolveContentOptions, IContent, IStreamContent, ITextSnapshot, IUpdateContentOptions, ICreateFileOptions, IFileSystemProviderActivationEvent, FileOperationError, FileOperationResult, FileOperation, FileSystemProviderCapabilities, FileType, toFileSystemProviderErrorCode, FileSystemProviderErrorCode, IStat } from 'vs/platform/files/common/files';
import { URI } from 'vs/base/common/uri';
import { Event, Emitter } from 'vs/base/common/event';
import { ServiceIdentifier } from 'vs/platform/instantiation/common/instantiation';
import { registerSingleton } from 'vs/platform/instantiation/common/extensions';
import { isAbsolutePath, dirname, basename, joinPath, isEqual } from 'vs/base/common/resources';
import { localize } from 'vs/nls';
import { TernarySearchTree } from 'vs/base/common/map';
import { isNonEmptyArray } from 'vs/base/common/arrays';
import { getBaseLabel } from 'vs/base/common/labels';
import { ILogService } from 'vs/platform/log/common/log';
export class FileService2 extends Disposable implements IFileService {
......@@ -29,6 +33,10 @@ export class FileService2 extends Disposable implements IFileService {
_serviceBrand: ServiceIdentifier<any>;
constructor(@ILogService private logService: ILogService) {
super();
}
//#region File System Provider
private _onDidChangeFileSystemProviderRegistrations: Emitter<IFileSystemProviderRegistrationEvent> = this._register(new Emitter<IFileSystemProviderRegistrationEvent>());
......@@ -69,7 +77,7 @@ export class FileService2 extends Disposable implements IFileService {
]);
}
activateProvider(scheme: string): Promise<void> {
async activateProvider(scheme: string): Promise<void> {
// Emit an event that we are about to activate a provider with the given scheme.
// Listeners can participate in the activation by registering a provider for it.
......@@ -89,7 +97,7 @@ export class FileService2 extends Disposable implements IFileService {
// If the provider is not yet there, make sure to join on the listeners assuming
// that it takes a bit longer to register the file system provider.
return Promise.all(joiners).then(() => undefined);
await Promise.all(joiners);
}
canHandleResource(resource: URI): boolean {
......@@ -129,16 +137,134 @@ export class FileService2 extends Disposable implements IFileService {
//#region File Metadata Resolving
resolveFile(resource: URI, options?: IResolveFileOptions): Promise<IFileStat> {
return this._impl.resolveFile(resource, options);
async resolveFile(resource: URI, options?: IResolveFileOptions): Promise<IFileStat> {
try {
return await this.doResolveFile(resource, options);
} catch (error) {
// Specially handle file not found case as file operation result
if (toFileSystemProviderErrorCode(error) === FileSystemProviderErrorCode.FileNotFound) {
throw new FileOperationError(
localize('fileNotFoundError', "File not found ({0})", resource.toString(true)),
FileOperationResult.FILE_NOT_FOUND
);
}
// Bubble up any other error as is
throw error;
}
}
resolveFiles(toResolve: { resource: URI; options?: IResolveFileOptions; }[]): Promise<IResolveFileResult[]> {
return this._impl.resolveFiles(toResolve);
private async doResolveFile(resource: URI, options?: IResolveFileOptions): Promise<IFileStat> {
const provider = await this.withProvider(resource);
// leverage a trie to check for recursive resolving
const to = options && options.resolveTo;
const trie = TernarySearchTree.forPaths<true>();
trie.set(resource.toString(), true);
if (isNonEmptyArray(to)) {
to.forEach(uri => trie.set(uri.toString(), true));
}
const stat = await provider.stat(resource);
return await this.toFileStat(provider, resource, stat, async stat => {
// check for recursive resolving
if (Boolean(trie.findSuperstr(stat.resource.toString()) || trie.get(stat.resource.toString()))) {
return true;
}
// check for resolving single child folders
if (stat.isDirectory && options && options.resolveSingleChildDescendants) {
try {
return (await provider.readdir(resource)).length === 1;
} catch (error) {
return false;
}
}
return false;
});
}
existsFile(resource: URI): Promise<boolean> {
return this.resolveFile(resource).then(_ => true, error => false);
private async toFileStat(provider: IFileSystemProvider, resource: URI, stat: IStat, recurse: (stat: IFileStat) => Promise<boolean>): Promise<IFileStat> {
// convert to file stat
const fileStat: IFileStat = {
resource,
name: getBaseLabel(resource),
isDirectory: (stat.type & FileType.Directory) !== 0,
isSymbolicLink: (stat.type & FileType.SymbolicLink) !== 0,
isReadonly: !!(provider.capabilities & FileSystemProviderCapabilities.Readonly),
mtime: stat.mtime,
size: stat.size,
etag: stat.mtime.toString(29) + stat.size.toString(31),
};
// check to recurse for directories
if (fileStat.isDirectory && await recurse(fileStat)) {
try {
const entries = await provider.readdir(resource);
fileStat.children = await Promise.all(entries.map(async entry => {
const childResource = joinPath(resource, entry[0]);
const childStat = await provider.stat(childResource);
return this.toFileStat(provider, childResource, childStat, recurse);
}));
} catch (error) {
this.logService.trace(error);
fileStat.children = []; // gracefully handle errors, we may not have permissions to read
}
return fileStat;
}
return Promise.resolve(fileStat);
}
async resolveFiles(toResolve: { resource: URI; options?: IResolveFileOptions; }[]): Promise<IResolveFileResult[]> {
// soft-groupBy, keep order, don't rearrange/merge groups
const groups: Array<typeof toResolve> = [];
let group: typeof toResolve | undefined;
for (const request of toResolve) {
if (!group || group[0].resource.scheme !== request.resource.scheme) {
group = [];
groups.push(group);
}
group.push(request);
}
// resolve files
const result: IResolveFileResult[] = [];
for (const group of groups) {
for (const groupEntry of group) {
try {
const stat = await this.doResolveFile(groupEntry.resource, groupEntry.options);
result.push({ stat, success: true });
} catch (error) {
this.logService.trace(error);
result.push({ stat: undefined, success: false });
}
}
}
return result;
}
async existsFile(resource: URI): Promise<boolean> {
try {
await this.resolveFile(resource);
return true;
} catch (error) {
return false;
}
}
//#endregion
......@@ -200,11 +326,11 @@ export class FileService2 extends Disposable implements IFileService {
}
break; // we have hit a directory that exists -> good
} catch (e) {
} catch (error) {
// Bubble up any other error that is not file not found
if (toFileSystemProviderErrorCode(e) !== FileSystemProviderErrorCode.FileNotFound) {
throw e;
if (toFileSystemProviderErrorCode(error) !== FileSystemProviderErrorCode.FileNotFound) {
throw error;
}
// Upon error, remember directories that need to be created
......
......@@ -10,8 +10,9 @@ import { IFileSystemProvider, FileSystemProviderCapabilities, IFileChange, IWatc
import { URI } from 'vs/base/common/uri';
import { Event, Emitter } from 'vs/base/common/event';
import { isLinux } from 'vs/base/common/platform';
import { statLink } from 'vs/base/node/pfs';
import { statLink, readdir } from 'vs/base/node/pfs';
import { normalize } from 'vs/base/common/path';
import { joinPath } from 'vs/base/common/resources';
export class DiskFileSystemProvider extends Disposable implements IFileSystemProvider {
......@@ -54,8 +55,22 @@ export class DiskFileSystemProvider extends Disposable implements IFileSystemPro
}
}
readdir(resource: URI): Promise<[string, FileType][]> {
throw new Error('Method not implemented.');
async readdir(resource: URI): Promise<[string, FileType][]> {
try {
const children = await readdir(this.toFilePath(resource));
const result: [string, FileType][] = [];
for (let i = 0; i < children.length; i++) {
const child = children[i];
const stat = await this.stat(joinPath(resource, child));
result.push([child, stat.type]);
}
return result;
} catch (error) {
throw this.toFileSystemProviderError(error);
}
}
//#endregion
......
......@@ -10,17 +10,33 @@ import { Schemas } from 'vs/base/common/network';
import { DiskFileSystemProvider } from 'vs/workbench/services/files2/node/diskFileSystemProvider';
import { getRandomTestPath } from 'vs/base/test/node/testUtils';
import { generateUuid } from 'vs/base/common/uuid';
import { join } from 'vs/base/common/path';
import { join, basename } from 'vs/base/common/path';
import { getPathFromAmdModule } from 'vs/base/common/amd';
import { copy, del } from 'vs/base/node/pfs';
import { URI } from 'vs/base/common/uri';
import { existsSync } from 'fs';
import { FileOperation, FileOperationEvent } from 'vs/platform/files/common/files';
import { FileOperation, FileOperationEvent, IFileStat } from 'vs/platform/files/common/files';
import { FileService } from 'vs/workbench/services/files/node/fileService';
import { TestContextService, TestEnvironmentService, TestTextResourceConfigurationService, TestLifecycleService, TestStorageService } from 'vs/workbench/test/workbenchTestServices';
import { Workspace, toWorkspaceFolders } from 'vs/platform/workspace/common/workspace';
import { TestConfigurationService } from 'vs/platform/configuration/test/common/testConfigurationService';
import { TestNotificationService } from 'vs/platform/notification/test/common/testNotificationService';
import { NullLogService } from 'vs/platform/log/common/log';
import { isLinux } from 'vs/base/common/platform';
function getByName(root: IFileStat, name: string): IFileStat | null {
if (root.children === undefined) {
return null;
}
for (const child of root.children) {
if (child.name === name) {
return child;
}
}
return null;
}
suite('Disk File Service', () => {
......@@ -30,7 +46,7 @@ suite('Disk File Service', () => {
let testDir: string;
setup(async () => {
service = new FileService2();
service = new FileService2(new NullLogService());
service.registerProvider(Schemas.file, new DiskFileSystemProvider());
const id = generateUuid();
......@@ -49,60 +65,217 @@ suite('Disk File Service', () => {
});
test('createFolder', async () => {
let event: FileOperationEvent;
let event: FileOperationEvent | undefined;
const toDispose = service.onAfterOperation(e => {
event = e;
});
return service.resolveFile(URI.file(testDir)).then(parent => {
const resource = URI.file(join(parent.resource.fsPath, 'newFolder'));
const parent = await service.resolveFile(URI.file(testDir));
return service.createFolder(resource).then(f => {
assert.equal(f.name, 'newFolder');
assert.equal(existsSync(f.resource.fsPath), true);
const resource = URI.file(join(parent.resource.fsPath, 'newFolder'));
assert.ok(event);
assert.equal(event.resource.fsPath, resource.fsPath);
assert.equal(event.operation, FileOperation.CREATE);
assert.equal(event.target!.resource.fsPath, resource.fsPath);
assert.equal(event.target!.isDirectory, true);
toDispose.dispose();
});
});
const folder = await service.createFolder(resource);
assert.equal(folder.name, 'newFolder');
assert.equal(existsSync(folder.resource.fsPath), true);
assert.ok(event);
assert.equal(event!.resource.fsPath, resource.fsPath);
assert.equal(event!.operation, FileOperation.CREATE);
assert.equal(event!.target!.resource.fsPath, resource.fsPath);
assert.equal(event!.target!.isDirectory, true);
toDispose.dispose();
});
test('createFolder: creating multiple folders at once', function () {
test('createFolder: creating multiple folders at once', async function () {
let event: FileOperationEvent;
const toDispose = service.onAfterOperation(e => {
event = e;
});
const multiFolderPaths = ['a', 'couple', 'of', 'folders'];
return service.resolveFile(URI.file(testDir)).then(parent => {
const resource = URI.file(join(parent.resource.fsPath, ...multiFolderPaths));
return service.createFolder(resource).then(f => {
const lastFolderName = multiFolderPaths[multiFolderPaths.length - 1];
assert.equal(f.name, lastFolderName);
assert.equal(existsSync(f.resource.fsPath), true);
assert.ok(event);
assert.equal(event.resource.fsPath, resource.fsPath);
assert.equal(event.operation, FileOperation.CREATE);
assert.equal(event.target!.resource.fsPath, resource.fsPath);
assert.equal(event.target!.isDirectory, true);
toDispose.dispose();
const parent = await service.resolveFile(URI.file(testDir));
const resource = URI.file(join(parent.resource.fsPath, ...multiFolderPaths));
const folder = await service.createFolder(resource);
const lastFolderName = multiFolderPaths[multiFolderPaths.length - 1];
assert.equal(folder.name, lastFolderName);
assert.equal(existsSync(folder.resource.fsPath), true);
assert.ok(event!);
assert.equal(event!.resource.fsPath, resource.fsPath);
assert.equal(event!.operation, FileOperation.CREATE);
assert.equal(event!.target!.resource.fsPath, resource.fsPath);
assert.equal(event!.target!.isDirectory, true);
toDispose.dispose();
});
test('existsFile', async () => {
let exists = await service.existsFile(URI.file(testDir));
assert.equal(exists, true);
exists = await service.existsFile(URI.file(testDir + 'something'));
assert.equal(exists, false);
});
test('resolveFile', async () => {
const resolved = await service.resolveFile(URI.file(testDir), { resolveTo: [URI.file(join(testDir, 'deep'))] });
assert.equal(resolved.children!.length, 8);
const deep = (getByName(resolved, 'deep')!);
assert.equal(deep.children!.length, 4);
});
test('resolveFile - directory', async () => {
const testsElements = ['examples', 'other', 'index.html', 'site.css'];
const result = await service.resolveFile(URI.file(getPathFromAmdModule(require, './fixtures/resolver')));
assert.ok(result);
assert.ok(result.children);
assert.ok(result.children!.length > 0);
assert.ok(result!.isDirectory);
assert.equal(result.children!.length, testsElements.length);
assert.ok(result.children!.every((entry) => {
return testsElements.some((name) => {
return basename(entry.resource.fsPath) === name;
});
}));
result.children!.forEach((value) => {
assert.ok(basename(value.resource.fsPath));
if (['examples', 'other'].indexOf(basename(value.resource.fsPath)) >= 0) {
assert.ok(value.isDirectory);
} else if (basename(value.resource.fsPath) === 'index.html') {
assert.ok(!value.isDirectory);
assert.ok(!value.children);
} else if (basename(value.resource.fsPath) === 'site.css') {
assert.ok(!value.isDirectory);
assert.ok(!value.children);
} else {
assert.ok(!'Unexpected value ' + basename(value.resource.fsPath));
}
});
});
test('existsFile', () => {
return service.existsFile(URI.file(testDir)).then((exists) => {
assert.equal(exists, true);
test('resolveFile - directory - resolveTo single directory', async () => {
const resolverFixturesPath = getPathFromAmdModule(require, './fixtures/resolver');
const result = await service.resolveFile(URI.file(resolverFixturesPath), { resolveTo: [URI.file(join(resolverFixturesPath, 'other/deep'))] });
return service.existsFile(URI.file(testDir + 'something')).then((exists) => {
assert.equal(exists, false);
});
assert.ok(result);
assert.ok(result.children);
assert.ok(result.children!.length > 0);
assert.ok(result.isDirectory);
const children = result.children!;
assert.equal(children.length, 4);
const other = getByName(result, 'other');
assert.ok(other);
assert.ok(other!.children!.length > 0);
const deep = getByName(other!, 'deep');
assert.ok(deep);
assert.ok(deep!.children!.length > 0);
assert.equal(deep!.children!.length, 4);
});
test('resolve directory - resolveTo single directory - mixed casing', async () => {
const resolverFixturesPath = getPathFromAmdModule(require, './fixtures/resolver');
const result = await service.resolveFile(URI.file(resolverFixturesPath), { resolveTo: [URI.file(join(resolverFixturesPath, 'other/Deep'))] });
assert.ok(result);
assert.ok(result.children);
assert.ok(result.children!.length > 0);
assert.ok(result.isDirectory);
const children = result.children;
assert.equal(children!.length, 4);
const other = getByName(result, 'other');
assert.ok(other);
assert.ok(other!.children!.length > 0);
const deep = getByName(other!, 'deep');
if (isLinux) { // Linux has case sensitive file system
assert.ok(deep);
assert.ok(!deep!.children); // not resolved because we got instructed to resolve other/Deep with capital D
} else {
assert.ok(deep);
assert.ok(deep!.children!.length > 0);
assert.equal(deep!.children!.length, 4);
}
});
test('resolve directory - resolveTo multiple directories', async () => {
const resolverFixturesPath = getPathFromAmdModule(require, './fixtures/resolver');
const result = await service.resolveFile(URI.file(resolverFixturesPath), {
resolveTo: [
URI.file(join(resolverFixturesPath, 'other/deep')),
URI.file(join(resolverFixturesPath, 'examples'))
]
});
assert.ok(result);
assert.ok(result.children);
assert.ok(result.children!.length > 0);
assert.ok(result.isDirectory);
const children = result.children!;
assert.equal(children.length, 4);
const other = getByName(result, 'other');
assert.ok(other);
assert.ok(other!.children!.length > 0);
const deep = getByName(other!, 'deep');
assert.ok(deep);
assert.ok(deep!.children!.length > 0);
assert.equal(deep!.children!.length, 4);
const examples = getByName(result, 'examples');
assert.ok(examples);
assert.ok(examples!.children!.length > 0);
assert.equal(examples!.children!.length, 4);
});
test('resolve directory - resolveSingleChildFolders', async () => {
const resolverFixturesPath = getPathFromAmdModule(require, './fixtures/resolver/other');
const result = await service.resolveFile(URI.file(resolverFixturesPath), { resolveSingleChildDescendants: true });
assert.ok(result);
assert.ok(result.children);
assert.ok(result.children!.length > 0);
assert.ok(result.isDirectory);
const children = result.children!;
assert.equal(children.length, 1);
let deep = getByName(result, 'deep');
assert.ok(deep);
assert.ok(deep!.children!.length > 0);
assert.equal(deep!.children!.length, 4);
});
test('resolveFiles', async () => {
const res = await service.resolveFiles([
{ resource: URI.file(testDir), options: { resolveTo: [URI.file(join(testDir, 'deep'))] } },
{ resource: URI.file(join(testDir, 'deep')) }
]);
const r1 = (res[0].stat!);
assert.equal(r1.children!.length, 8);
const deep = (getByName(r1, 'deep')!);
assert.equal(deep.children!.length, 4);
const r2 = (res[1].stat!);
assert.equal(r2.children!.length, 4);
assert.equal(r2.name, 'deep');
});
});
\ No newline at end of file
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册