diff --git a/src/vs/platform/files/common/files.ts b/src/vs/platform/files/common/files.ts index ec930123a3dbffa154463b5b650e288a55e19de0..5ab5c46c9422fe8dc5f6931b06a40b19bdbe5de2 100644 --- a/src/vs/platform/files/common/files.ts +++ b/src/vs/platform/files/common/files.ts @@ -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; onAfterOperation: Event; - resolveFile(resource: URI, options?: IResolveFileOptions): Promise; - - resolveFiles(toResolve: { resource: URI, options?: IResolveFileOptions }[]): Promise; - resolveContent(resource: URI, options?: IResolveContentOptions): Promise; resolveStreamContent(resource: URI, options?: IResolveContentOptions): Promise; diff --git a/src/vs/workbench/services/files/node/fileService.ts b/src/vs/workbench/services/files/node/fileService.ts index 01ffc15a7616bb8bbf209d9491527a61c8aad23d..fe023796512e696014eb6842ec430decb015605b 100644 --- a/src/vs/workbench/services/files/node/fileService.ts +++ b/src/vs/workbench/services/files/node/fileService.ts @@ -217,15 +217,6 @@ export class FileService extends Disposable implements ILegacyFileService { return resource.scheme === Schemas.file; } - resolveFile(resource: uri, options?: IResolveFileOptions): Promise { - return this.resolve(resource, options); - } - - resolveFiles(toResolve: { resource: uri, options?: IResolveFileOptions }[]): Promise { - 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 { return this.resolveStreamContent(resource, options).then(streamContent => { return new Promise((resolve, reject) => { @@ -1092,6 +1083,15 @@ export class FileService extends Disposable implements ILegacyFileService { // Tests only + resolveFile(resource: uri, options?: IResolveFileOptions): Promise { + return this.resolve(resource, options); + } + + resolveFiles(toResolve: { resource: uri, options?: IResolveFileOptions }[]): Promise { + 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 { // 1.) Create folder diff --git a/src/vs/workbench/services/files/test/electron-browser/fileService.test.ts b/src/vs/workbench/services/files/test/electron-browser/fileService.test.ts index b06bffedb4b9e377acb992635e79ad0ff4a015d6..74719e275054e6f15778a16eefc8f23347eda766 100644 --- a/src/vs/workbench/services/files/test/electron-browser/fileService.test.ts +++ b/src/vs/workbench/services/files/test/electron-browser/fileService.test.ts @@ -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')); diff --git a/src/vs/workbench/services/files2/common/fileService2.ts b/src/vs/workbench/services/files2/common/fileService2.ts index f067e38ac7598a8d47f9a7865671c706bd3e3383..5fd9a7615d141482c9a6a552db4be05ac88f831d 100644 --- a/src/vs/workbench/services/files2/common/fileService2.ts +++ b/src/vs/workbench/services/files2/common/fileService2.ts @@ -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; + constructor(@ILogService private logService: ILogService) { + super(); + } + //#region File System Provider private _onDidChangeFileSystemProviderRegistrations: Emitter = this._register(new Emitter()); @@ -69,7 +77,7 @@ export class FileService2 extends Disposable implements IFileService { ]); } - activateProvider(scheme: string): Promise { + async activateProvider(scheme: string): Promise { // 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 { - return this._impl.resolveFile(resource, options); + async resolveFile(resource: URI, options?: IResolveFileOptions): Promise { + 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 { - return this._impl.resolveFiles(toResolve); + private async doResolveFile(resource: URI, options?: IResolveFileOptions): Promise { + const provider = await this.withProvider(resource); + + // leverage a trie to check for recursive resolving + const to = options && options.resolveTo; + const trie = TernarySearchTree.forPaths(); + 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 { - return this.resolveFile(resource).then(_ => true, error => false); + private async toFileStat(provider: IFileSystemProvider, resource: URI, stat: IStat, recurse: (stat: IFileStat) => Promise): Promise { + + // 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 { + + // soft-groupBy, keep order, don't rearrange/merge groups + const groups: Array = []; + 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 { + 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 diff --git a/src/vs/workbench/services/files2/node/diskFileSystemProvider.ts b/src/vs/workbench/services/files2/node/diskFileSystemProvider.ts index 75019caa30262e780ef240f1700a7f4b202898a7..87981135f1d53e8a18d60a2b8bb24f30c4d685ab 100644 --- a/src/vs/workbench/services/files2/node/diskFileSystemProvider.ts +++ b/src/vs/workbench/services/files2/node/diskFileSystemProvider.ts @@ -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 diff --git a/src/vs/workbench/services/files2/test/node/diskFileService.test.ts b/src/vs/workbench/services/files2/test/node/diskFileService.test.ts index 4fa95fd58d113753cc22497584825271f557fa6d..c3f4bd2fe8cfb4931a1ac1fb27a599857e0101eb 100644 --- a/src/vs/workbench/services/files2/test/node/diskFileService.test.ts +++ b/src/vs/workbench/services/files2/test/node/diskFileService.test.ts @@ -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