diff --git a/src/vs/base/node/pfs.ts b/src/vs/base/node/pfs.ts index 14dca3d5f4937..1af9f028c701e 100644 --- a/src/vs/base/node/pfs.ts +++ b/src/vs/base/node/pfs.ts @@ -138,6 +138,20 @@ export async function readdir(path: string): Promise { return handleDirectoryChildren(await promisify(fs.readdir)(path)); } +export async function readdirWithFileTypes(path: string): Promise { + const children = await promisify(fs.readdir)(path, { withFileTypes: true }); + + // Mac: uses NFD unicode form on disk, but we want NFC + // See also https://github.com/nodejs/node/issues/2165 + if (platform.isMacintosh) { + for (const child of children) { + child.name = normalizeNFC(child.name); + } + } + + return children; +} + export function readdirSync(path: string): string[] { return handleDirectoryChildren(fs.readdirSync(path)); } diff --git a/src/vs/base/test/node/pfs/pfs.test.ts b/src/vs/base/test/node/pfs/pfs.test.ts index 4f09ad2b56dab..060913bbe7255 100644 --- a/src/vs/base/test/node/pfs/pfs.test.ts +++ b/src/vs/base/test/node/pfs/pfs.test.ts @@ -16,6 +16,7 @@ import { CancellationTokenSource } from 'vs/base/common/cancellation'; import { isWindows, isLinux } from 'vs/base/common/platform'; import { canNormalize } from 'vs/base/common/normalization'; import { VSBuffer } from 'vs/base/common/buffer'; +import { join } from 'path'; const chunkSize = 64 * 1024; const readError = 'Error while reading'; @@ -386,6 +387,31 @@ suite('PFS', () => { } }); + test('readdirWithFileTypes', async () => { + if (canNormalize && typeof process.versions['electron'] !== 'undefined' /* needs electron */) { + const id = uuid.generateUuid(); + const parentDir = path.join(os.tmpdir(), 'vsctests', id); + const testDir = join(parentDir, 'pfs', id); + + const newDir = path.join(testDir, 'öäü'); + await pfs.mkdirp(newDir, 493); + + await pfs.writeFile(join(testDir, 'somefile.txt'), 'contents'); + + assert.ok(fs.existsSync(newDir)); + + const children = await pfs.readdirWithFileTypes(testDir); + + assert.equal(children.some(n => n.name === 'öäü'), true); // Mac always converts to NFD, so + assert.equal(children.some(n => n.isDirectory()), true); + + assert.equal(children.some(n => n.name === 'somefile.txt'), true); + assert.equal(children.some(n => n.isFile()), true); + + await pfs.rimraf(parentDir); + } + }); + test('writeFile (string)', async () => { const smallData = 'Hello World'; const bigData = (new Array(100 * 1024)).join('Large String\n'); diff --git a/src/vs/workbench/services/files/node/diskFileSystemProvider.ts b/src/vs/workbench/services/files/node/diskFileSystemProvider.ts index ef3bb343450cd..89463b847513b 100644 --- a/src/vs/workbench/services/files/node/diskFileSystemProvider.ts +++ b/src/vs/workbench/services/files/node/diskFileSystemProvider.ts @@ -3,14 +3,14 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { mkdir, open, close, read, write, fdatasync } from 'fs'; +import { mkdir, open, close, read, write, fdatasync, Dirent, Stats } from 'fs'; import { promisify } from 'util'; import { IDisposable, Disposable, toDisposable, dispose, combinedDisposable } from 'vs/base/common/lifecycle'; import { IFileSystemProvider, FileSystemProviderCapabilities, IFileChange, IWatchOptions, IStat, FileType, FileDeleteOptions, FileOverwriteOptions, FileWriteOptions, FileOpenOptions, FileSystemProviderErrorCode, createFileSystemProviderError, FileSystemProviderError } from 'vs/platform/files/common/files'; import { URI } from 'vs/base/common/uri'; import { Event, Emitter } from 'vs/base/common/event'; import { isLinux, isWindows } from 'vs/base/common/platform'; -import { statLink, readdir, unlink, move, copy, readFile, truncate, rimraf, RimRafMode, exists } from 'vs/base/node/pfs'; +import { statLink, unlink, move, copy, readFile, truncate, rimraf, RimRafMode, exists, readdirWithFileTypes } from 'vs/base/node/pfs'; import { normalize, basename, dirname } from 'vs/base/common/path'; import { joinPath } from 'vs/base/common/resources'; import { isEqual } from 'vs/base/common/extpath'; @@ -62,15 +62,8 @@ export class DiskFileSystemProvider extends Disposable implements IFileSystemPro try { const { stat, isSymbolicLink } = await statLink(this.toFilePath(resource)); // cannot use fs.stat() here to support links properly - let type: number; - if (isSymbolicLink) { - type = FileType.SymbolicLink | (stat.isDirectory() ? FileType.Directory : FileType.File); - } else { - type = stat.isFile() ? FileType.File : stat.isDirectory() ? FileType.Directory : FileType.Unknown; - } - return { - type, + type: this.toType(stat, isSymbolicLink), ctime: stat.ctime.getTime(), mtime: stat.mtime.getTime(), size: stat.size @@ -82,13 +75,19 @@ export class DiskFileSystemProvider extends Disposable implements IFileSystemPro async readdir(resource: URI): Promise<[string, FileType][]> { try { - const children = await readdir(this.toFilePath(resource)); + const children = await readdirWithFileTypes(this.toFilePath(resource)); const result: [string, FileType][] = []; await Promise.all(children.map(async child => { try { - const stat = await this.stat(joinPath(resource, child)); - result.push([child, stat.type]); + let type: FileType; + if (child.isSymbolicLink()) { + type = (await this.stat(joinPath(resource, child.name))).type; // always resolve target the link points to if any + } else { + type = this.toType(child); + } + + result.push([child.name, type]); } catch (error) { this.logService.trace(error); // ignore errors for individual entries that can arise from permission denied } @@ -100,6 +99,14 @@ export class DiskFileSystemProvider extends Disposable implements IFileSystemPro } } + private toType(entry: Stats | Dirent, isSymbolicLink = entry.isSymbolicLink()): FileType { + if (isSymbolicLink) { + return FileType.SymbolicLink | (entry.isDirectory() ? FileType.Directory : FileType.File); + } + + return entry.isFile() ? FileType.File : entry.isDirectory() ? FileType.Directory : FileType.Unknown; + } + //#endregion //#region File Reading/Writing