Merge commit 'be3e8236086165e5e45a5a10783823874b3f3ebd' as 'lib/vscode'

This commit is contained in:
Joe Previte
2020-12-15 15:52:33 -07:00
4649 changed files with 1311795 additions and 0 deletions

View File

@@ -0,0 +1,272 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { URI } from 'vs/base/common/uri';
import { IFileSystemProviderWithFileReadWriteCapability, FileSystemProviderCapabilities, IFileChange, IWatchOptions, IStat, FileOverwriteOptions, FileType, FileDeleteOptions, FileWriteOptions, FileChangeType, createFileSystemProviderError, FileSystemProviderErrorCode } from 'vs/platform/files/common/files';
import { Disposable, IDisposable } from 'vs/base/common/lifecycle';
import { Event, Emitter } from 'vs/base/common/event';
import { VSBuffer } from 'vs/base/common/buffer';
import { joinPath, extUri, dirname } from 'vs/base/common/resources';
import { localize } from 'vs/nls';
import * as browser from 'vs/base/browser/browser';
const INDEXEDDB_VSCODE_DB = 'vscode-web-db';
export const INDEXEDDB_USERDATA_OBJECT_STORE = 'vscode-userdata-store';
export const INDEXEDDB_LOGS_OBJECT_STORE = 'vscode-logs-store';
export class IndexedDB {
private indexedDBPromise: Promise<IDBDatabase | null>;
constructor() {
this.indexedDBPromise = this.openIndexedDB(INDEXEDDB_VSCODE_DB, 2, [INDEXEDDB_USERDATA_OBJECT_STORE, INDEXEDDB_LOGS_OBJECT_STORE]);
}
async createFileSystemProvider(scheme: string, store: string): Promise<IIndexedDBFileSystemProvider | null> {
let fsp: IIndexedDBFileSystemProvider | null = null;
const indexedDB = await this.indexedDBPromise;
if (indexedDB) {
if (indexedDB.objectStoreNames.contains(store)) {
fsp = new IndexedDBFileSystemProvider(scheme, indexedDB, store);
} else {
console.error(`Error while creating indexedDB filesystem provider. Could not find ${store} object store`);
}
}
return fsp;
}
private openIndexedDB(name: string, version: number, stores: string[]): Promise<IDBDatabase | null> {
if (browser.isEdge) {
return Promise.resolve(null);
}
return new Promise((c, e) => {
const request = window.indexedDB.open(name, version);
request.onerror = (err) => e(request.error);
request.onsuccess = () => {
const db = request.result;
for (const store of stores) {
if (!db.objectStoreNames.contains(store)) {
console.error(`Error while creating indexedDB. Could not create ${store} object store`);
c(null);
return;
}
}
c(db);
};
request.onupgradeneeded = () => {
const db = request.result;
for (const store of stores) {
if (!db.objectStoreNames.contains(store)) {
db.createObjectStore(store);
}
}
};
});
}
}
export interface IIndexedDBFileSystemProvider extends Disposable, IFileSystemProviderWithFileReadWriteCapability {
reset(): Promise<void>;
}
class IndexedDBFileSystemProvider extends Disposable implements IIndexedDBFileSystemProvider {
readonly capabilities: FileSystemProviderCapabilities =
FileSystemProviderCapabilities.FileReadWrite
| FileSystemProviderCapabilities.PathCaseSensitive;
readonly onDidChangeCapabilities: Event<void> = Event.None;
private readonly _onDidChangeFile = this._register(new Emitter<readonly IFileChange[]>());
readonly onDidChangeFile: Event<readonly IFileChange[]> = this._onDidChangeFile.event;
private readonly versions: Map<string, number> = new Map<string, number>();
private readonly dirs: Set<string> = new Set<string>();
constructor(private readonly scheme: string, private readonly database: IDBDatabase, private readonly store: string) {
super();
this.dirs.add('/');
}
watch(resource: URI, opts: IWatchOptions): IDisposable {
return Disposable.None;
}
async mkdir(resource: URI): Promise<void> {
try {
const resourceStat = await this.stat(resource);
if (resourceStat.type === FileType.File) {
throw createFileSystemProviderError(localize('fileNotDirectory', "File is not a directory"), FileSystemProviderErrorCode.FileNotADirectory);
}
} catch (error) { /* Ignore */ }
// Make sure parent dir exists
await this.stat(dirname(resource));
this.dirs.add(resource.path);
}
async stat(resource: URI): Promise<IStat> {
try {
const content = await this.readFile(resource);
return {
type: FileType.File,
ctime: 0,
mtime: this.versions.get(resource.toString()) || 0,
size: content.byteLength
};
} catch (e) {
}
const files = await this.readdir(resource);
if (files.length) {
return {
type: FileType.Directory,
ctime: 0,
mtime: 0,
size: 0
};
}
if (this.dirs.has(resource.path)) {
return {
type: FileType.Directory,
ctime: 0,
mtime: 0,
size: 0
};
}
throw createFileSystemProviderError(localize('fileNotExists', "File does not exist"), FileSystemProviderErrorCode.FileNotFound);
}
async readdir(resource: URI): Promise<[string, FileType][]> {
const hasKey = await this.hasKey(resource.path);
if (hasKey) {
throw createFileSystemProviderError(localize('fileNotDirectory', "File is not a directory"), FileSystemProviderErrorCode.FileNotADirectory);
}
const keys = await this.getAllKeys();
const files: Map<string, [string, FileType]> = new Map<string, [string, FileType]>();
for (const key of keys) {
const keyResource = this.toResource(key);
if (extUri.isEqualOrParent(keyResource, resource)) {
const path = extUri.relativePath(resource, keyResource);
if (path) {
const keySegments = path.split('/');
files.set(keySegments[0], [keySegments[0], keySegments.length === 1 ? FileType.File : FileType.Directory]);
}
}
}
return [...files.values()];
}
async readFile(resource: URI): Promise<Uint8Array> {
const hasKey = await this.hasKey(resource.path);
if (!hasKey) {
throw createFileSystemProviderError(localize('fileNotFound', "File not found"), FileSystemProviderErrorCode.FileNotFound);
}
const value = await this.getValue(resource.path);
if (typeof value === 'string') {
return VSBuffer.fromString(value).buffer;
} else {
return value;
}
}
async writeFile(resource: URI, content: Uint8Array, opts: FileWriteOptions): Promise<void> {
const hasKey = await this.hasKey(resource.path);
if (!hasKey) {
const files = await this.readdir(resource);
if (files.length) {
throw createFileSystemProviderError(localize('fileIsDirectory', "File is Directory"), FileSystemProviderErrorCode.FileIsADirectory);
}
}
await this.setValue(resource.path, content);
this.versions.set(resource.toString(), (this.versions.get(resource.toString()) || 0) + 1);
this._onDidChangeFile.fire([{ resource, type: FileChangeType.UPDATED }]);
}
async delete(resource: URI, opts: FileDeleteOptions): Promise<void> {
const hasKey = await this.hasKey(resource.path);
if (hasKey) {
await this.deleteKey(resource.path);
this.versions.delete(resource.path);
this._onDidChangeFile.fire([{ resource, type: FileChangeType.DELETED }]);
return;
}
if (opts.recursive) {
const files = await this.readdir(resource);
await Promise.all(files.map(([key]) => this.delete(joinPath(resource, key), opts)));
}
}
rename(from: URI, to: URI, opts: FileOverwriteOptions): Promise<void> {
return Promise.reject(new Error('Not Supported'));
}
private toResource(key: string): URI {
return URI.file(key).with({ scheme: this.scheme });
}
async getAllKeys(): Promise<string[]> {
return new Promise(async (c, e) => {
const transaction = this.database.transaction([this.store]);
const objectStore = transaction.objectStore(this.store);
const request = objectStore.getAllKeys();
request.onerror = () => e(request.error);
request.onsuccess = () => c(<string[]>request.result);
});
}
hasKey(key: string): Promise<boolean> {
return new Promise<boolean>(async (c, e) => {
const transaction = this.database.transaction([this.store]);
const objectStore = transaction.objectStore(this.store);
const request = objectStore.getKey(key);
request.onerror = () => e(request.error);
request.onsuccess = () => {
c(!!request.result);
};
});
}
getValue(key: string): Promise<Uint8Array | string> {
return new Promise(async (c, e) => {
const transaction = this.database.transaction([this.store]);
const objectStore = transaction.objectStore(this.store);
const request = objectStore.get(key);
request.onerror = () => e(request.error);
request.onsuccess = () => c(request.result || '');
});
}
setValue(key: string, value: Uint8Array): Promise<void> {
return new Promise(async (c, e) => {
const transaction = this.database.transaction([this.store], 'readwrite');
const objectStore = transaction.objectStore(this.store);
const request = objectStore.put(value, key);
request.onerror = () => e(request.error);
request.onsuccess = () => c();
});
}
deleteKey(key: string): Promise<void> {
return new Promise(async (c, e) => {
const transaction = this.database.transaction([this.store], 'readwrite');
const objectStore = transaction.objectStore(this.store);
const request = objectStore.delete(key);
request.onerror = () => e(request.error);
request.onsuccess = () => c();
});
}
reset(): Promise<void> {
return new Promise(async (c, e) => {
const transaction = this.database.transaction([this.store], 'readwrite');
const objectStore = transaction.objectStore(this.store);
const request = objectStore.clear();
request.onerror = () => e(request.error);
request.onsuccess = () => c();
});
}
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,230 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { Event, Emitter } from 'vs/base/common/event';
import { Disposable, IDisposable } from 'vs/base/common/lifecycle';
import * as resources from 'vs/base/common/resources';
import { FileChangeType, FileType, IWatchOptions, IStat, FileSystemProviderErrorCode, FileSystemProviderError, FileWriteOptions, IFileChange, FileDeleteOptions, FileSystemProviderCapabilities, FileOverwriteOptions, IFileSystemProviderWithFileReadWriteCapability } from 'vs/platform/files/common/files';
import { URI } from 'vs/base/common/uri';
class File implements IStat {
type: FileType.File;
ctime: number;
mtime: number;
size: number;
name: string;
data?: Uint8Array;
constructor(name: string) {
this.type = FileType.File;
this.ctime = Date.now();
this.mtime = Date.now();
this.size = 0;
this.name = name;
}
}
class Directory implements IStat {
type: FileType.Directory;
ctime: number;
mtime: number;
size: number;
name: string;
entries: Map<string, File | Directory>;
constructor(name: string) {
this.type = FileType.Directory;
this.ctime = Date.now();
this.mtime = Date.now();
this.size = 0;
this.name = name;
this.entries = new Map();
}
}
export type Entry = File | Directory;
export class InMemoryFileSystemProvider extends Disposable implements IFileSystemProviderWithFileReadWriteCapability {
readonly capabilities: FileSystemProviderCapabilities =
FileSystemProviderCapabilities.FileReadWrite
| FileSystemProviderCapabilities.PathCaseSensitive;
readonly onDidChangeCapabilities: Event<void> = Event.None;
root = new Directory('');
// --- manage file metadata
async stat(resource: URI): Promise<IStat> {
return this._lookup(resource, false);
}
async readdir(resource: URI): Promise<[string, FileType][]> {
const entry = this._lookupAsDirectory(resource, false);
let result: [string, FileType][] = [];
entry.entries.forEach((child, name) => result.push([name, child.type]));
return result;
}
// --- manage file contents
async readFile(resource: URI): Promise<Uint8Array> {
const data = this._lookupAsFile(resource, false).data;
if (data) {
return data;
}
throw new FileSystemProviderError('file not found', FileSystemProviderErrorCode.FileNotFound);
}
async writeFile(resource: URI, content: Uint8Array, opts: FileWriteOptions): Promise<void> {
let basename = resources.basename(resource);
let parent = this._lookupParentDirectory(resource);
let entry = parent.entries.get(basename);
if (entry instanceof Directory) {
throw new FileSystemProviderError('file is directory', FileSystemProviderErrorCode.FileIsADirectory);
}
if (!entry && !opts.create) {
throw new FileSystemProviderError('file not found', FileSystemProviderErrorCode.FileNotFound);
}
if (entry && opts.create && !opts.overwrite) {
throw new FileSystemProviderError('file exists already', FileSystemProviderErrorCode.FileExists);
}
if (!entry) {
entry = new File(basename);
parent.entries.set(basename, entry);
this._fireSoon({ type: FileChangeType.ADDED, resource });
}
entry.mtime = Date.now();
entry.size = content.byteLength;
entry.data = content;
this._fireSoon({ type: FileChangeType.UPDATED, resource });
}
// --- manage files/folders
async rename(from: URI, to: URI, opts: FileOverwriteOptions): Promise<void> {
if (!opts.overwrite && this._lookup(to, true)) {
throw new FileSystemProviderError('file exists already', FileSystemProviderErrorCode.FileExists);
}
let entry = this._lookup(from, false);
let oldParent = this._lookupParentDirectory(from);
let newParent = this._lookupParentDirectory(to);
let newName = resources.basename(to);
oldParent.entries.delete(entry.name);
entry.name = newName;
newParent.entries.set(newName, entry);
this._fireSoon(
{ type: FileChangeType.DELETED, resource: from },
{ type: FileChangeType.ADDED, resource: to }
);
}
async delete(resource: URI, opts: FileDeleteOptions): Promise<void> {
let dirname = resources.dirname(resource);
let basename = resources.basename(resource);
let parent = this._lookupAsDirectory(dirname, false);
if (parent.entries.has(basename)) {
parent.entries.delete(basename);
parent.mtime = Date.now();
parent.size -= 1;
this._fireSoon({ type: FileChangeType.UPDATED, resource: dirname }, { resource, type: FileChangeType.DELETED });
}
}
async mkdir(resource: URI): Promise<void> {
let basename = resources.basename(resource);
let dirname = resources.dirname(resource);
let parent = this._lookupAsDirectory(dirname, false);
let entry = new Directory(basename);
parent.entries.set(entry.name, entry);
parent.mtime = Date.now();
parent.size += 1;
this._fireSoon({ type: FileChangeType.UPDATED, resource: dirname }, { type: FileChangeType.ADDED, resource });
}
// --- lookup
private _lookup(uri: URI, silent: false): Entry;
private _lookup(uri: URI, silent: boolean): Entry | undefined;
private _lookup(uri: URI, silent: boolean): Entry | undefined {
let parts = uri.path.split('/');
let entry: Entry = this.root;
for (const part of parts) {
if (!part) {
continue;
}
let child: Entry | undefined;
if (entry instanceof Directory) {
child = entry.entries.get(part);
}
if (!child) {
if (!silent) {
throw new FileSystemProviderError('file not found', FileSystemProviderErrorCode.FileNotFound);
} else {
return undefined;
}
}
entry = child;
}
return entry;
}
private _lookupAsDirectory(uri: URI, silent: boolean): Directory {
let entry = this._lookup(uri, silent);
if (entry instanceof Directory) {
return entry;
}
throw new FileSystemProviderError('file not a directory', FileSystemProviderErrorCode.FileNotADirectory);
}
private _lookupAsFile(uri: URI, silent: boolean): File {
let entry = this._lookup(uri, silent);
if (entry instanceof File) {
return entry;
}
throw new FileSystemProviderError('file is a directory', FileSystemProviderErrorCode.FileIsADirectory);
}
private _lookupParentDirectory(uri: URI): Directory {
const dirname = resources.dirname(uri);
return this._lookupAsDirectory(dirname, false);
}
// --- manage file events
private readonly _onDidChangeFile = this._register(new Emitter<readonly IFileChange[]>());
readonly onDidChangeFile: Event<readonly IFileChange[]> = this._onDidChangeFile.event;
private _bufferedChanges: IFileChange[] = [];
private _fireSoonHandle?: any;
watch(resource: URI, opts: IWatchOptions): IDisposable {
// ignore, fires for all changes...
return Disposable.None;
}
private _fireSoon(...changes: IFileChange[]): void {
this._bufferedChanges.push(...changes);
if (this._fireSoonHandle) {
clearTimeout(this._fireSoonHandle);
}
this._fireSoonHandle = setTimeout(() => {
this._onDidChangeFile.fire(this._bufferedChanges);
this._bufferedChanges.length = 0;
}, 5);
}
}

View File

@@ -0,0 +1,134 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { localize } from 'vs/nls';
import { URI } from 'vs/base/common/uri';
import { VSBuffer } from 'vs/base/common/buffer';
import { CancellationToken } from 'vs/base/common/cancellation';
import { IFileSystemProviderWithOpenReadWriteCloseCapability, FileReadStreamOptions, createFileSystemProviderError, FileSystemProviderErrorCode, ensureFileSystemProviderError } from 'vs/platform/files/common/files';
import { canceled } from 'vs/base/common/errors';
import { IErrorTransformer, IDataTransformer, WriteableStream } from 'vs/base/common/stream';
export interface ICreateReadStreamOptions extends FileReadStreamOptions {
/**
* The size of the buffer to use before sending to the stream.
*/
bufferSize: number;
/**
* Allows to massage any possibly error that happens during reading.
*/
errorTransformer?: IErrorTransformer;
}
/**
* A helper to read a file from a provider with open/read/close capability into a stream.
*/
export async function readFileIntoStream<T>(
provider: IFileSystemProviderWithOpenReadWriteCloseCapability,
resource: URI,
target: WriteableStream<T>,
transformer: IDataTransformer<VSBuffer, T>,
options: ICreateReadStreamOptions,
token: CancellationToken
): Promise<void> {
let error: Error | undefined = undefined;
try {
await doReadFileIntoStream(provider, resource, target, transformer, options, token);
} catch (err) {
error = err;
} finally {
if (error && options.errorTransformer) {
error = options.errorTransformer(error);
}
target.end(error);
}
}
async function doReadFileIntoStream<T>(provider: IFileSystemProviderWithOpenReadWriteCloseCapability, resource: URI, target: WriteableStream<T>, transformer: IDataTransformer<VSBuffer, T>, options: ICreateReadStreamOptions, token: CancellationToken): Promise<void> {
// Check for cancellation
throwIfCancelled(token);
// open handle through provider
const handle = await provider.open(resource, { create: false });
// Check for cancellation
throwIfCancelled(token);
try {
let totalBytesRead = 0;
let bytesRead = 0;
let allowedRemainingBytes = (options && typeof options.length === 'number') ? options.length : undefined;
let buffer = VSBuffer.alloc(Math.min(options.bufferSize, typeof allowedRemainingBytes === 'number' ? allowedRemainingBytes : options.bufferSize));
let posInFile = options && typeof options.position === 'number' ? options.position : 0;
let posInBuffer = 0;
do {
// read from source (handle) at current position (pos) into buffer (buffer) at
// buffer position (posInBuffer) up to the size of the buffer (buffer.byteLength).
bytesRead = await provider.read(handle, posInFile, buffer.buffer, posInBuffer, buffer.byteLength - posInBuffer);
posInFile += bytesRead;
posInBuffer += bytesRead;
totalBytesRead += bytesRead;
if (typeof allowedRemainingBytes === 'number') {
allowedRemainingBytes -= bytesRead;
}
// when buffer full, create a new one and emit it through stream
if (posInBuffer === buffer.byteLength) {
await target.write(transformer(buffer));
buffer = VSBuffer.alloc(Math.min(options.bufferSize, typeof allowedRemainingBytes === 'number' ? allowedRemainingBytes : options.bufferSize));
posInBuffer = 0;
}
} while (bytesRead > 0 && (typeof allowedRemainingBytes !== 'number' || allowedRemainingBytes > 0) && throwIfCancelled(token) && throwIfTooLarge(totalBytesRead, options));
// wrap up with last buffer (also respect maxBytes if provided)
if (posInBuffer > 0) {
let lastChunkLength = posInBuffer;
if (typeof allowedRemainingBytes === 'number') {
lastChunkLength = Math.min(posInBuffer, allowedRemainingBytes);
}
target.write(transformer(buffer.slice(0, lastChunkLength)));
}
} catch (error) {
throw ensureFileSystemProviderError(error);
} finally {
await provider.close(handle);
}
}
function throwIfCancelled(token: CancellationToken): boolean {
if (token.isCancellationRequested) {
throw canceled();
}
return true;
}
function throwIfTooLarge(totalBytesRead: number, options: ICreateReadStreamOptions): boolean {
// Return early if file is too large to load and we have configured limits
if (options?.limits) {
if (typeof options.limits.memory === 'number' && totalBytesRead > options.limits.memory) {
throw createFileSystemProviderError(localize('fileTooLargeForHeapError', "To open a file of this size, you need to restart and allow it to use more memory"), FileSystemProviderErrorCode.FileExceedsMemoryLimit);
}
if (typeof options.limits.size === 'number' && totalBytesRead > options.limits.size) {
throw createFileSystemProviderError(localize('fileTooLargeError', "File is too large to open"), FileSystemProviderErrorCode.FileTooLarge);
}
}
return true;
}

View File

@@ -0,0 +1,42 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { DiskFileSystemProvider as NodeDiskFileSystemProvider, IDiskFileSystemProviderOptions } from 'vs/platform/files/node/diskFileSystemProvider';
import { FileDeleteOptions, FileSystemProviderCapabilities } from 'vs/platform/files/common/files';
import { isWindows } from 'vs/base/common/platform';
import { localize } from 'vs/nls';
import { basename } from 'vs/base/common/path';
import { ILogService } from 'vs/platform/log/common/log';
import { INativeHostService } from 'vs/platform/native/electron-sandbox/native';
export class DiskFileSystemProvider extends NodeDiskFileSystemProvider {
constructor(
logService: ILogService,
private readonly nativeHostService: INativeHostService,
options?: IDiskFileSystemProviderOptions
) {
super(logService, options);
}
get capabilities(): FileSystemProviderCapabilities {
if (!this._capabilities) {
this._capabilities = super.capabilities | FileSystemProviderCapabilities.Trash;
}
return this._capabilities;
}
protected async doDelete(filePath: string, opts: FileDeleteOptions): Promise<void> {
if (!opts.useTrash) {
return super.doDelete(filePath, opts);
}
const result = await this.nativeHostService.moveItemToTrash(filePath);
if (!result) {
throw new Error(isWindows ? localize('binFailed', "Failed to move '{0}' to the recycle bin", basename(filePath)) : localize('trashFailed', "Failed to move '{0}' to the trash", basename(filePath)));
}
}
}

View File

@@ -0,0 +1,702 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
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 { FileSystemProviderCapabilities, IFileChange, IWatchOptions, IStat, FileType, FileDeleteOptions, FileOverwriteOptions, FileWriteOptions, FileOpenOptions, FileSystemProviderErrorCode, createFileSystemProviderError, FileSystemProviderError, IFileSystemProviderWithFileReadWriteCapability, IFileSystemProviderWithFileReadStreamCapability, IFileSystemProviderWithOpenReadWriteCloseCapability, FileReadStreamOptions, IFileSystemProviderWithFileFolderCopyCapability } 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, 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';
import { retry, ThrottledDelayer } from 'vs/base/common/async';
import { ILogService, LogLevel } from 'vs/platform/log/common/log';
import { localize } from 'vs/nls';
import { IDiskFileChange, toFileChanges, ILogMessage } from 'vs/platform/files/node/watcher/watcher';
import { FileWatcher as UnixWatcherService } from 'vs/platform/files/node/watcher/unix/watcherService';
import { FileWatcher as WindowsWatcherService } from 'vs/platform/files/node/watcher/win32/watcherService';
import { FileWatcher as NsfwWatcherService } from 'vs/platform/files/node/watcher/nsfw/watcherService';
import { FileWatcher as NodeJSWatcherService } from 'vs/platform/files/node/watcher/nodejs/watcherService';
import { CancellationToken } from 'vs/base/common/cancellation';
import { ReadableStreamEvents, newWriteableStream } from 'vs/base/common/stream';
import { readFileIntoStream } from 'vs/platform/files/common/io';
import { insert } from 'vs/base/common/arrays';
import { VSBuffer } from 'vs/base/common/buffer';
export interface IWatcherOptions {
pollingInterval?: number;
usePolling: boolean;
}
export interface IDiskFileSystemProviderOptions {
bufferSize?: number;
watcher?: IWatcherOptions;
}
export class DiskFileSystemProvider extends Disposable implements
IFileSystemProviderWithFileReadWriteCapability,
IFileSystemProviderWithOpenReadWriteCloseCapability,
IFileSystemProviderWithFileReadStreamCapability,
IFileSystemProviderWithFileFolderCopyCapability {
private readonly BUFFER_SIZE = this.options?.bufferSize || 64 * 1024;
constructor(
private readonly logService: ILogService,
private readonly options?: IDiskFileSystemProviderOptions
) {
super();
}
//#region File Capabilities
onDidChangeCapabilities: Event<void> = Event.None;
protected _capabilities: FileSystemProviderCapabilities | undefined;
get capabilities(): FileSystemProviderCapabilities {
if (!this._capabilities) {
this._capabilities =
FileSystemProviderCapabilities.FileReadWrite |
FileSystemProviderCapabilities.FileOpenReadWriteClose |
FileSystemProviderCapabilities.FileReadStream |
FileSystemProviderCapabilities.FileFolderCopy;
if (isLinux) {
this._capabilities |= FileSystemProviderCapabilities.PathCaseSensitive;
}
}
return this._capabilities;
}
//#endregion
//#region File Metadata Resolving
async stat(resource: URI): Promise<IStat> {
try {
const { stat, symbolicLink } = await statLink(this.toFilePath(resource)); // cannot use fs.stat() here to support links properly
return {
type: this.toType(stat, symbolicLink),
ctime: stat.birthtime.getTime(), // intentionally not using ctime here, we want the creation time
mtime: stat.mtime.getTime(),
size: stat.size
};
} catch (error) {
throw this.toFileSystemProviderError(error);
}
}
async readdir(resource: URI): Promise<[string, FileType][]> {
try {
const children = await readdirWithFileTypes(this.toFilePath(resource));
const result: [string, FileType][] = [];
await Promise.all(children.map(async child => {
try {
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
}
}));
return result;
} catch (error) {
throw this.toFileSystemProviderError(error);
}
}
private toType(entry: Stats | Dirent, symbolicLink?: { dangling: boolean }): FileType {
// Signal file type by checking for file / directory, except:
// - symbolic links pointing to non-existing files are FileType.Unknown
// - files that are neither file nor directory are FileType.Unknown
let type: FileType;
if (symbolicLink?.dangling) {
type = FileType.Unknown;
} else if (entry.isFile()) {
type = FileType.File;
} else if (entry.isDirectory()) {
type = FileType.Directory;
} else {
type = FileType.Unknown;
}
// Always signal symbolic link as file type additionally
if (symbolicLink) {
type |= FileType.SymbolicLink;
}
return type;
}
//#endregion
//#region File Reading/Writing
async readFile(resource: URI): Promise<Uint8Array> {
try {
const filePath = this.toFilePath(resource);
return await readFile(filePath);
} catch (error) {
throw this.toFileSystemProviderError(error);
}
}
readFileStream(resource: URI, opts: FileReadStreamOptions, token: CancellationToken): ReadableStreamEvents<Uint8Array> {
const stream = newWriteableStream<Uint8Array>(data => VSBuffer.concat(data.map(data => VSBuffer.wrap(data))).buffer);
readFileIntoStream(this, resource, stream, data => data.buffer, {
...opts,
bufferSize: this.BUFFER_SIZE
}, token);
return stream;
}
async writeFile(resource: URI, content: Uint8Array, opts: FileWriteOptions): Promise<void> {
let handle: number | undefined = undefined;
try {
const filePath = this.toFilePath(resource);
// Validate target unless { create: true, overwrite: true }
if (!opts.create || !opts.overwrite) {
const fileExists = await exists(filePath);
if (fileExists) {
if (!opts.overwrite) {
throw createFileSystemProviderError(localize('fileExists', "File already exists"), FileSystemProviderErrorCode.FileExists);
}
} else {
if (!opts.create) {
throw createFileSystemProviderError(localize('fileNotExists', "File does not exist"), FileSystemProviderErrorCode.FileNotFound);
}
}
}
// Open
handle = await this.open(resource, { create: true });
// Write content at once
await this.write(handle, 0, content, 0, content.byteLength);
} catch (error) {
throw this.toFileSystemProviderError(error);
} finally {
if (typeof handle === 'number') {
await this.close(handle);
}
}
}
private readonly mapHandleToPos: Map<number, number> = new Map();
private readonly writeHandles: Set<number> = new Set();
private canFlush: boolean = true;
async open(resource: URI, opts: FileOpenOptions): Promise<number> {
try {
const filePath = this.toFilePath(resource);
let flags: string | undefined = undefined;
if (opts.create) {
if (isWindows && await exists(filePath)) {
try {
// On Windows and if the file exists, we use a different strategy of saving the file
// by first truncating the file and then writing with r+ flag. This helps to save hidden files on Windows
// (see https://github.com/microsoft/vscode/issues/931) and prevent removing alternate data streams
// (see https://github.com/microsoft/vscode/issues/6363)
await truncate(filePath, 0);
// After a successful truncate() the flag can be set to 'r+' which will not truncate.
flags = 'r+';
} catch (error) {
this.logService.trace(error);
}
}
// we take opts.create as a hint that the file is opened for writing
// as such we use 'w' to truncate an existing or create the
// file otherwise. we do not allow reading.
if (!flags) {
flags = 'w';
}
} else {
// otherwise we assume the file is opened for reading
// as such we use 'r' to neither truncate, nor create
// the file.
flags = 'r';
}
const handle = await promisify(open)(filePath, flags);
// remember this handle to track file position of the handle
// we init the position to 0 since the file descriptor was
// just created and the position was not moved so far (see
// also http://man7.org/linux/man-pages/man2/open.2.html -
// "The file offset is set to the beginning of the file.")
this.mapHandleToPos.set(handle, 0);
// remember that this handle was used for writing
if (opts.create) {
this.writeHandles.add(handle);
}
return handle;
} catch (error) {
throw this.toFileSystemProviderError(error);
}
}
async close(fd: number): Promise<void> {
try {
// remove this handle from map of positions
this.mapHandleToPos.delete(fd);
// if a handle is closed that was used for writing, ensure
// to flush the contents to disk if possible.
if (this.writeHandles.delete(fd) && this.canFlush) {
try {
await promisify(fdatasync)(fd);
} catch (error) {
// In some exotic setups it is well possible that node fails to sync
// In that case we disable flushing and log the error to our logger
this.canFlush = false;
this.logService.error(error);
}
}
return await promisify(close)(fd);
} catch (error) {
throw this.toFileSystemProviderError(error);
}
}
async read(fd: number, pos: number, data: Uint8Array, offset: number, length: number): Promise<number> {
const normalizedPos = this.normalizePos(fd, pos);
let bytesRead: number | null = null;
try {
const result = await promisify(read)(fd, data, offset, length, normalizedPos);
if (typeof result === 'number') {
bytesRead = result; // node.d.ts fail
} else {
bytesRead = result.bytesRead;
}
return bytesRead;
} catch (error) {
throw this.toFileSystemProviderError(error);
} finally {
this.updatePos(fd, normalizedPos, bytesRead);
}
}
private normalizePos(fd: number, pos: number): number | null {
// when calling fs.read/write we try to avoid passing in the "pos" argument and
// rather prefer to pass in "null" because this avoids an extra seek(pos)
// call that in some cases can even fail (e.g. when opening a file over FTP -
// see https://github.com/microsoft/vscode/issues/73884).
//
// as such, we compare the passed in position argument with our last known
// position for the file descriptor and use "null" if they match.
if (pos === this.mapHandleToPos.get(fd)) {
return null;
}
return pos;
}
private updatePos(fd: number, pos: number | null, bytesLength: number | null): void {
const lastKnownPos = this.mapHandleToPos.get(fd);
if (typeof lastKnownPos === 'number') {
// pos !== null signals that previously a position was used that is
// not null. node.js documentation explains, that in this case
// the internal file pointer is not moving and as such we do not move
// our position pointer.
//
// Docs: "If position is null, data will be read from the current file position,
// and the file position will be updated. If position is an integer, the file position
// will remain unchanged."
if (typeof pos === 'number') {
// do not modify the position
}
// bytesLength = number is a signal that the read/write operation was
// successful and as such we need to advance the position in the Map
//
// Docs (http://man7.org/linux/man-pages/man2/read.2.html):
// "On files that support seeking, the read operation commences at the
// file offset, and the file offset is incremented by the number of
// bytes read."
//
// Docs (http://man7.org/linux/man-pages/man2/write.2.html):
// "For a seekable file (i.e., one to which lseek(2) may be applied, for
// example, a regular file) writing takes place at the file offset, and
// the file offset is incremented by the number of bytes actually
// written."
else if (typeof bytesLength === 'number') {
this.mapHandleToPos.set(fd, lastKnownPos + bytesLength);
}
// bytesLength = null signals an error in the read/write operation
// and as such we drop the handle from the Map because the position
// is unspecificed at this point.
else {
this.mapHandleToPos.delete(fd);
}
}
}
async write(fd: number, pos: number, data: Uint8Array, offset: number, length: number): Promise<number> {
// we know at this point that the file to write to is truncated and thus empty
// if the write now fails, the file remains empty. as such we really try hard
// to ensure the write succeeds by retrying up to three times.
return retry(() => this.doWrite(fd, pos, data, offset, length), 100 /* ms delay */, 3 /* retries */);
}
private async doWrite(fd: number, pos: number, data: Uint8Array, offset: number, length: number): Promise<number> {
const normalizedPos = this.normalizePos(fd, pos);
let bytesWritten: number | null = null;
try {
const result = await promisify(write)(fd, data, offset, length, normalizedPos);
if (typeof result === 'number') {
bytesWritten = result; // node.d.ts fail
} else {
bytesWritten = result.bytesWritten;
}
return bytesWritten;
} catch (error) {
throw this.toFileSystemProviderError(error);
} finally {
this.updatePos(fd, normalizedPos, bytesWritten);
}
}
//#endregion
//#region Move/Copy/Delete/Create Folder
async mkdir(resource: URI): Promise<void> {
try {
await promisify(mkdir)(this.toFilePath(resource));
} catch (error) {
throw this.toFileSystemProviderError(error);
}
}
async delete(resource: URI, opts: FileDeleteOptions): Promise<void> {
try {
const filePath = this.toFilePath(resource);
await this.doDelete(filePath, opts);
} catch (error) {
throw this.toFileSystemProviderError(error);
}
}
protected async doDelete(filePath: string, opts: FileDeleteOptions): Promise<void> {
if (opts.recursive) {
await rimraf(filePath, RimRafMode.MOVE);
} else {
await unlink(filePath);
}
}
async rename(from: URI, to: URI, opts: FileOverwriteOptions): Promise<void> {
const fromFilePath = this.toFilePath(from);
const toFilePath = this.toFilePath(to);
if (fromFilePath === toFilePath) {
return; // simulate node.js behaviour here and do a no-op if paths match
}
try {
// Ensure target does not exist
await this.validateTargetDeleted(from, to, 'move', opts.overwrite);
// Move
await move(fromFilePath, toFilePath);
} catch (error) {
// rewrite some typical errors that can happen especially around symlinks
// to something the user can better understand
if (error.code === 'EINVAL' || error.code === 'EBUSY' || error.code === 'ENAMETOOLONG') {
error = new Error(localize('moveError', "Unable to move '{0}' into '{1}' ({2}).", basename(fromFilePath), basename(dirname(toFilePath)), error.toString()));
}
throw this.toFileSystemProviderError(error);
}
}
async copy(from: URI, to: URI, opts: FileOverwriteOptions): Promise<void> {
const fromFilePath = this.toFilePath(from);
const toFilePath = this.toFilePath(to);
if (fromFilePath === toFilePath) {
return; // simulate node.js behaviour here and do a no-op if paths match
}
try {
// Ensure target does not exist
await this.validateTargetDeleted(from, to, 'copy', opts.overwrite);
// Copy
await copy(fromFilePath, toFilePath);
} catch (error) {
// rewrite some typical errors that can happen especially around symlinks
// to something the user can better understand
if (error.code === 'EINVAL' || error.code === 'EBUSY' || error.code === 'ENAMETOOLONG') {
error = new Error(localize('copyError', "Unable to copy '{0}' into '{1}' ({2}).", basename(fromFilePath), basename(dirname(toFilePath)), error.toString()));
}
throw this.toFileSystemProviderError(error);
}
}
private async validateTargetDeleted(from: URI, to: URI, mode: 'move' | 'copy', overwrite?: boolean): Promise<void> {
const fromFilePath = this.toFilePath(from);
const toFilePath = this.toFilePath(to);
let isSameResourceWithDifferentPathCase = false;
const isPathCaseSensitive = !!(this.capabilities & FileSystemProviderCapabilities.PathCaseSensitive);
if (!isPathCaseSensitive) {
isSameResourceWithDifferentPathCase = isEqual(fromFilePath, toFilePath, true /* ignore case */);
}
if (isSameResourceWithDifferentPathCase && mode === 'copy') {
throw createFileSystemProviderError(localize('fileCopyErrorPathCase', "'File cannot be copied to same path with different path case"), FileSystemProviderErrorCode.FileExists);
}
// handle existing target (unless this is a case change)
if (!isSameResourceWithDifferentPathCase && await exists(toFilePath)) {
if (!overwrite) {
throw createFileSystemProviderError(localize('fileCopyErrorExists', "File at target already exists"), FileSystemProviderErrorCode.FileExists);
}
// Delete target
await this.delete(to, { recursive: true, useTrash: false });
}
}
//#endregion
//#region File Watching
private readonly _onDidWatchErrorOccur = this._register(new Emitter<string>());
readonly onDidErrorOccur = this._onDidWatchErrorOccur.event;
private readonly _onDidChangeFile = this._register(new Emitter<readonly IFileChange[]>());
readonly onDidChangeFile = this._onDidChangeFile.event;
private recursiveWatcher: WindowsWatcherService | UnixWatcherService | NsfwWatcherService | undefined;
private readonly recursiveFoldersToWatch: { path: string, excludes: string[] }[] = [];
private recursiveWatchRequestDelayer = this._register(new ThrottledDelayer<void>(0));
private recursiveWatcherLogLevelListener: IDisposable | undefined;
watch(resource: URI, opts: IWatchOptions): IDisposable {
if (opts.recursive) {
return this.watchRecursive(resource, opts.excludes);
}
return this.watchNonRecursive(resource); // TODO@ben ideally the same watcher can be used in both cases
}
private watchRecursive(resource: URI, excludes: string[]): IDisposable {
// Add to list of folders to watch recursively
const folderToWatch = { path: this.toFilePath(resource), excludes };
const remove = insert(this.recursiveFoldersToWatch, folderToWatch);
// Trigger update
this.refreshRecursiveWatchers();
return toDisposable(() => {
// Remove from list of folders to watch recursively
remove();
// Trigger update
this.refreshRecursiveWatchers();
});
}
private refreshRecursiveWatchers(): void {
// Buffer requests for recursive watching to decide on right watcher
// that supports potentially watching more than one folder at once
this.recursiveWatchRequestDelayer.trigger(async () => {
this.doRefreshRecursiveWatchers();
});
}
private doRefreshRecursiveWatchers(): void {
// Reuse existing
if (this.recursiveWatcher instanceof NsfwWatcherService) {
this.recursiveWatcher.setFolders(this.recursiveFoldersToWatch);
}
// Create new
else {
// Dispose old
dispose(this.recursiveWatcher);
this.recursiveWatcher = undefined;
// Create new if we actually have folders to watch
if (this.recursiveFoldersToWatch.length > 0) {
let watcherImpl: {
new(
folders: { path: string, excludes: string[] }[],
onChange: (changes: IDiskFileChange[]) => void,
onLogMessage: (msg: ILogMessage) => void,
verboseLogging: boolean,
watcherOptions?: IWatcherOptions
): WindowsWatcherService | UnixWatcherService | NsfwWatcherService
};
let watcherOptions: IWatcherOptions | undefined = undefined;
// requires a polling watcher
if (this.options?.watcher?.usePolling) {
watcherImpl = UnixWatcherService;
watcherOptions = this.options?.watcher;
}
// Single Folder Watcher
else {
if (this.recursiveFoldersToWatch.length === 1) {
if (isWindows) {
watcherImpl = WindowsWatcherService;
} else {
watcherImpl = UnixWatcherService;
}
}
// Multi Folder Watcher
else {
watcherImpl = NsfwWatcherService;
}
}
// Create and start watching
this.recursiveWatcher = new watcherImpl(
this.recursiveFoldersToWatch,
event => this._onDidChangeFile.fire(toFileChanges(event)),
msg => {
if (msg.type === 'error') {
this._onDidWatchErrorOccur.fire(msg.message);
}
this.logService[msg.type](msg.message);
},
this.logService.getLevel() === LogLevel.Trace,
watcherOptions
);
if (!this.recursiveWatcherLogLevelListener) {
this.recursiveWatcherLogLevelListener = this.logService.onDidChangeLogLevel(() => {
if (this.recursiveWatcher) {
this.recursiveWatcher.setVerboseLogging(this.logService.getLevel() === LogLevel.Trace);
}
});
}
}
}
}
private watchNonRecursive(resource: URI): IDisposable {
const watcherService = new NodeJSWatcherService(
this.toFilePath(resource),
changes => this._onDidChangeFile.fire(toFileChanges(changes)),
msg => {
if (msg.type === 'error') {
this._onDidWatchErrorOccur.fire(msg.message);
}
this.logService[msg.type](msg.message);
},
this.logService.getLevel() === LogLevel.Trace
);
const logLevelListener = this.logService.onDidChangeLogLevel(() => {
watcherService.setVerboseLogging(this.logService.getLevel() === LogLevel.Trace);
});
return combinedDisposable(watcherService, logLevelListener);
}
//#endregion
//#region Helpers
protected toFilePath(resource: URI): string {
return normalize(resource.fsPath);
}
private toFileSystemProviderError(error: NodeJS.ErrnoException): FileSystemProviderError {
if (error instanceof FileSystemProviderError) {
return error; // avoid double conversion
}
let code: FileSystemProviderErrorCode;
switch (error.code) {
case 'ENOENT':
code = FileSystemProviderErrorCode.FileNotFound;
break;
case 'EISDIR':
code = FileSystemProviderErrorCode.FileIsADirectory;
break;
case 'ENOTDIR':
code = FileSystemProviderErrorCode.FileNotADirectory;
break;
case 'EEXIST':
code = FileSystemProviderErrorCode.FileExists;
break;
case 'EPERM':
case 'EACCES':
code = FileSystemProviderErrorCode.NoPermissions;
break;
default:
code = FileSystemProviderErrorCode.Unknown;
}
return createFileSystemProviderError(error, code);
}
//#endregion
dispose(): void {
super.dispose();
dispose(this.recursiveWatcher);
this.recursiveWatcher = undefined;
dispose(this.recursiveWatcherLogLevelListener);
this.recursiveWatcherLogLevelListener = undefined;
}
}

View File

@@ -0,0 +1,126 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { IDiskFileChange, normalizeFileChanges, ILogMessage } from 'vs/platform/files/node/watcher/watcher';
import { Disposable } from 'vs/base/common/lifecycle';
import { statLink } from 'vs/base/node/pfs';
import { realpath } from 'vs/base/node/extpath';
import { watchFolder, watchFile, CHANGE_BUFFER_DELAY } from 'vs/base/node/watcher';
import { FileChangeType } from 'vs/platform/files/common/files';
import { ThrottledDelayer } from 'vs/base/common/async';
import { join, basename } from 'vs/base/common/path';
export class FileWatcher extends Disposable {
private isDisposed: boolean | undefined;
private fileChangesDelayer: ThrottledDelayer<void> = this._register(new ThrottledDelayer<void>(CHANGE_BUFFER_DELAY * 2 /* sync on delay from underlying library */));
private fileChangesBuffer: IDiskFileChange[] = [];
constructor(
private path: string,
private onDidFilesChange: (changes: IDiskFileChange[]) => void,
private onLogMessage: (msg: ILogMessage) => void,
private verboseLogging: boolean
) {
super();
this.startWatching();
}
setVerboseLogging(verboseLogging: boolean): void {
this.verboseLogging = verboseLogging;
}
private async startWatching(): Promise<void> {
try {
const { stat, symbolicLink } = await statLink(this.path);
if (this.isDisposed) {
return;
}
let pathToWatch = this.path;
if (symbolicLink) {
try {
pathToWatch = await realpath(pathToWatch);
} catch (error) {
this.onError(error);
}
}
// Watch Folder
if (stat.isDirectory()) {
this._register(watchFolder(pathToWatch, (eventType, path) => {
this.onFileChange({
type: eventType === 'changed' ? FileChangeType.UPDATED : eventType === 'added' ? FileChangeType.ADDED : FileChangeType.DELETED,
path: join(this.path, basename(path)) // ensure path is identical with what was passed in
});
}, error => this.onError(error)));
}
// Watch File
else {
this._register(watchFile(pathToWatch, eventType => {
this.onFileChange({
type: eventType === 'changed' ? FileChangeType.UPDATED : FileChangeType.DELETED,
path: this.path // ensure path is identical with what was passed in
});
}, error => this.onError(error)));
}
} catch (error) {
this.onError(error);
}
}
private onFileChange(event: IDiskFileChange): void {
// Add to buffer
this.fileChangesBuffer.push(event);
// Logging
if (this.verboseLogging) {
this.onVerbose(`${event.type === FileChangeType.ADDED ? '[ADDED]' : event.type === FileChangeType.DELETED ? '[DELETED]' : '[CHANGED]'} ${event.path}`);
}
// Handle emit through delayer to accommodate for bulk changes and thus reduce spam
this.fileChangesDelayer.trigger(async () => {
const fileChanges = this.fileChangesBuffer;
this.fileChangesBuffer = [];
// Event normalization
const normalizedFileChanges = normalizeFileChanges(fileChanges);
// Logging
if (this.verboseLogging) {
normalizedFileChanges.forEach(event => {
this.onVerbose(`>> normalized ${event.type === FileChangeType.ADDED ? '[ADDED]' : event.type === FileChangeType.DELETED ? '[DELETED]' : '[CHANGED]'} ${event.path}`);
});
}
// Fire
if (normalizedFileChanges.length > 0) {
this.onDidFilesChange(normalizedFileChanges);
}
});
}
private onError(error: string): void {
if (!this.isDisposed) {
this.onLogMessage({ type: 'error', message: `[File Watcher (node.js)] ${error}` });
}
}
private onVerbose(message: string): void {
if (!this.isDisposed) {
this.onLogMessage({ type: 'trace', message: `[File Watcher (node.js)] ${message}` });
}
}
dispose(): void {
this.isDisposed = true;
super.dispose();
}
}

View File

@@ -0,0 +1,252 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as glob from 'vs/base/common/glob';
import * as extpath from 'vs/base/common/extpath';
import * as path from 'vs/base/common/path';
import * as platform from 'vs/base/common/platform';
import { IDiskFileChange, normalizeFileChanges, ILogMessage } from 'vs/platform/files/node/watcher/watcher';
import * as nsfw from 'vscode-nsfw';
import { IWatcherService, IWatcherRequest } from 'vs/platform/files/node/watcher/nsfw/watcher';
import { ThrottledDelayer } from 'vs/base/common/async';
import { FileChangeType } from 'vs/platform/files/common/files';
import { normalizeNFC } from 'vs/base/common/normalization';
import { Event, Emitter } from 'vs/base/common/event';
import { realcaseSync, realpathSync } from 'vs/base/node/extpath';
import { Disposable } from 'vs/base/common/lifecycle';
const nsfwActionToRawChangeType: { [key: number]: number } = [];
nsfwActionToRawChangeType[nsfw.actions.CREATED] = FileChangeType.ADDED;
nsfwActionToRawChangeType[nsfw.actions.MODIFIED] = FileChangeType.UPDATED;
nsfwActionToRawChangeType[nsfw.actions.DELETED] = FileChangeType.DELETED;
interface IWatcherObjet {
start(): void;
stop(): void;
}
interface IPathWatcher {
ready: Promise<IWatcherObjet>;
watcher?: IWatcherObjet;
ignored: glob.ParsedPattern[];
}
export class NsfwWatcherService extends Disposable implements IWatcherService {
private static readonly FS_EVENT_DELAY = 50; // aggregate and only emit events when changes have stopped for this duration (in ms)
private readonly _onDidChangeFile = this._register(new Emitter<IDiskFileChange[]>());
readonly onDidChangeFile = this._onDidChangeFile.event;
private readonly _onDidLogMessage = this._register(new Emitter<ILogMessage>());
readonly onDidLogMessage: Event<ILogMessage> = this._onDidLogMessage.event;
private pathWatchers: { [watchPath: string]: IPathWatcher } = {};
private verboseLogging: boolean | undefined;
private enospcErrorLogged: boolean | undefined;
async setRoots(roots: IWatcherRequest[]): Promise<void> {
const normalizedRoots = this._normalizeRoots(roots);
// Gather roots that are not currently being watched
const rootsToStartWatching = normalizedRoots.filter(r => {
return !(r.path in this.pathWatchers);
});
// Gather current roots that don't exist in the new roots array
const rootsToStopWatching = Object.keys(this.pathWatchers).filter(r => {
return normalizedRoots.every(normalizedRoot => normalizedRoot.path !== r);
});
// Logging
if (this.verboseLogging) {
this.log(`Start watching: [${rootsToStartWatching.map(r => r.path).join(',')}]\nStop watching: [${rootsToStopWatching.join(',')}]`);
}
// Stop watching some roots
rootsToStopWatching.forEach(root => {
this.pathWatchers[root].ready.then(watcher => watcher.stop());
delete this.pathWatchers[root];
});
// Start watching some roots
rootsToStartWatching.forEach(root => this.doWatch(root));
// Refresh ignored arrays in case they changed
roots.forEach(root => {
if (root.path in this.pathWatchers) {
this.pathWatchers[root.path].ignored = Array.isArray(root.excludes) ? root.excludes.map(ignored => glob.parse(ignored)) : [];
}
});
}
private doWatch(request: IWatcherRequest): void {
let undeliveredFileEvents: IDiskFileChange[] = [];
const fileEventDelayer = new ThrottledDelayer<void>(NsfwWatcherService.FS_EVENT_DELAY);
let readyPromiseResolve: (watcher: IWatcherObjet) => void;
this.pathWatchers[request.path] = {
ready: new Promise<IWatcherObjet>(resolve => readyPromiseResolve = resolve),
ignored: Array.isArray(request.excludes) ? request.excludes.map(ignored => glob.parse(ignored)) : []
};
process.on('uncaughtException', (e: Error | string) => {
// Specially handle ENOSPC errors that can happen when
// the watcher consumes so many file descriptors that
// we are running into a limit. We only want to warn
// once in this case to avoid log spam.
// See https://github.com/microsoft/vscode/issues/7950
if (e === 'Inotify limit reached' && !this.enospcErrorLogged) {
this.enospcErrorLogged = true;
this.error('Inotify limit reached (ENOSPC)');
}
});
// NSFW does not report file changes in the path provided on macOS if
// - the path uses wrong casing
// - the path is a symbolic link
// We have to detect this case and massage the events to correct this.
let realBasePathDiffers = false;
let realBasePathLength = request.path.length;
if (platform.isMacintosh) {
try {
// First check for symbolic link
let realBasePath = realpathSync(request.path);
// Second check for casing difference
if (request.path === realBasePath) {
realBasePath = (realcaseSync(request.path) || request.path);
}
if (request.path !== realBasePath) {
realBasePathLength = realBasePath.length;
realBasePathDiffers = true;
this.warn(`Watcher basePath does not match version on disk and will be corrected (original: ${request.path}, real: ${realBasePath})`);
}
} catch (error) {
// ignore
}
}
if (this.verboseLogging) {
this.log(`Start watching with nsfw: ${request.path}`);
}
nsfw(request.path, events => {
for (const e of events) {
// Logging
if (this.verboseLogging) {
const logPath = e.action === nsfw.actions.RENAMED ? path.join(e.directory, e.oldFile || '') + ' -> ' + e.newFile : path.join(e.directory, e.file || '');
this.log(`${e.action === nsfw.actions.CREATED ? '[CREATED]' : e.action === nsfw.actions.DELETED ? '[DELETED]' : e.action === nsfw.actions.MODIFIED ? '[CHANGED]' : '[RENAMED]'} ${logPath}`);
}
// Convert nsfw event to IRawFileChange and add to queue
let absolutePath: string;
if (e.action === nsfw.actions.RENAMED) {
// Rename fires when a file's name changes within a single directory
absolutePath = path.join(e.directory, e.oldFile || '');
if (!this.isPathIgnored(absolutePath, this.pathWatchers[request.path].ignored)) {
undeliveredFileEvents.push({ type: FileChangeType.DELETED, path: absolutePath });
} else if (this.verboseLogging) {
this.log(` >> ignored ${absolutePath}`);
}
absolutePath = path.join(e.newDirectory || e.directory, e.newFile || '');
if (!this.isPathIgnored(absolutePath, this.pathWatchers[request.path].ignored)) {
undeliveredFileEvents.push({ type: FileChangeType.ADDED, path: absolutePath });
} else if (this.verboseLogging) {
this.log(` >> ignored ${absolutePath}`);
}
} else {
absolutePath = path.join(e.directory, e.file || '');
if (!this.isPathIgnored(absolutePath, this.pathWatchers[request.path].ignored)) {
undeliveredFileEvents.push({
type: nsfwActionToRawChangeType[e.action],
path: absolutePath
});
} else if (this.verboseLogging) {
this.log(` >> ignored ${absolutePath}`);
}
}
}
// Delay and send buffer
fileEventDelayer.trigger(async () => {
const events = undeliveredFileEvents;
undeliveredFileEvents = [];
if (platform.isMacintosh) {
events.forEach(e => {
// Mac uses NFD unicode form on disk, but we want NFC
e.path = normalizeNFC(e.path);
// Convert paths back to original form in case it differs
if (realBasePathDiffers) {
e.path = request.path + e.path.substr(realBasePathLength);
}
});
}
// Broadcast to clients normalized
const res = normalizeFileChanges(events);
this._onDidChangeFile.fire(res);
// Logging
if (this.verboseLogging) {
res.forEach(r => {
this.log(` >> normalized ${r.type === FileChangeType.ADDED ? '[ADDED]' : r.type === FileChangeType.DELETED ? '[DELETED]' : '[CHANGED]'} ${r.path}`);
});
}
});
}).then(watcher => {
this.pathWatchers[request.path].watcher = watcher;
const startPromise = watcher.start();
startPromise.then(() => readyPromiseResolve(watcher));
return startPromise;
});
}
async setVerboseLogging(enabled: boolean): Promise<void> {
this.verboseLogging = enabled;
}
async stop(): Promise<void> {
for (let path in this.pathWatchers) {
let watcher = this.pathWatchers[path];
watcher.ready.then(watcher => watcher.stop());
delete this.pathWatchers[path];
}
this.pathWatchers = Object.create(null);
}
protected _normalizeRoots(roots: IWatcherRequest[]): IWatcherRequest[] {
// Normalizes a set of root paths by removing any root paths that are
// sub-paths of other roots.
return roots.filter(r => roots.every(other => {
return !(r.path.length > other.path.length && extpath.isEqualOrParent(r.path, other.path));
}));
}
private isPathIgnored(absolutePath: string, ignored: glob.ParsedPattern[]): boolean {
return ignored && ignored.some(i => i(absolutePath));
}
private log(message: string) {
this._onDidLogMessage.fire({ type: 'trace', message: `[File Watcher (nsfw)] ` + message });
}
private warn(message: string) {
this._onDidLogMessage.fire({ type: 'warn', message: `[File Watcher (nsfw)] ` + message });
}
private error(message: string) {
this._onDidLogMessage.fire({ type: 'error', message: `[File Watcher (nsfw)] ` + message });
}
}

View File

@@ -0,0 +1,54 @@
/*---------------------------------------------------------------------------------------------
* 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 platform from 'vs/base/common/platform';
import { NsfwWatcherService } from 'vs/platform/files/node/watcher/nsfw/nsfwWatcherService';
import { IWatcherRequest } from 'vs/platform/files/node/watcher/nsfw/watcher';
class TestNsfwWatcherService extends NsfwWatcherService {
normalizeRoots(roots: string[]): string[] {
// Work with strings as paths to simplify testing
const requests: IWatcherRequest[] = roots.map(r => {
return { path: r, excludes: [] };
});
return this._normalizeRoots(requests).map(r => r.path);
}
}
suite('NSFW Watcher Service', () => {
suite('_normalizeRoots', () => {
test('should not impacts roots that don\'t overlap', () => {
const service = new TestNsfwWatcherService();
if (platform.isWindows) {
assert.deepEqual(service.normalizeRoots(['C:\\a']), ['C:\\a']);
assert.deepEqual(service.normalizeRoots(['C:\\a', 'C:\\b']), ['C:\\a', 'C:\\b']);
assert.deepEqual(service.normalizeRoots(['C:\\a', 'C:\\b', 'C:\\c\\d\\e']), ['C:\\a', 'C:\\b', 'C:\\c\\d\\e']);
} else {
assert.deepEqual(service.normalizeRoots(['/a']), ['/a']);
assert.deepEqual(service.normalizeRoots(['/a', '/b']), ['/a', '/b']);
assert.deepEqual(service.normalizeRoots(['/a', '/b', '/c/d/e']), ['/a', '/b', '/c/d/e']);
}
});
test('should remove sub-folders of other roots', () => {
const service = new TestNsfwWatcherService();
if (platform.isWindows) {
assert.deepEqual(service.normalizeRoots(['C:\\a', 'C:\\a\\b']), ['C:\\a']);
assert.deepEqual(service.normalizeRoots(['C:\\a', 'C:\\b', 'C:\\a\\b']), ['C:\\a', 'C:\\b']);
assert.deepEqual(service.normalizeRoots(['C:\\b\\a', 'C:\\a', 'C:\\b', 'C:\\a\\b']), ['C:\\a', 'C:\\b']);
assert.deepEqual(service.normalizeRoots(['C:\\a', 'C:\\a\\b', 'C:\\a\\c\\d']), ['C:\\a']);
} else {
assert.deepEqual(service.normalizeRoots(['/a', '/a/b']), ['/a']);
assert.deepEqual(service.normalizeRoots(['/a', '/b', '/a/b']), ['/a', '/b']);
assert.deepEqual(service.normalizeRoots(['/b/a', '/a', '/b', '/a/b']), ['/a', '/b']);
assert.deepEqual(service.normalizeRoots(['/a', '/a/b', '/a/c/d']), ['/a']);
}
});
});
});

View File

@@ -0,0 +1,23 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { Event } from 'vs/base/common/event';
import { IDiskFileChange, ILogMessage } from 'vs/platform/files/node/watcher/watcher';
export interface IWatcherRequest {
path: string;
excludes: string[];
}
export interface IWatcherService {
readonly onDidChangeFile: Event<IDiskFileChange[]>;
readonly onDidLogMessage: Event<ILogMessage>;
setRoots(roots: IWatcherRequest[]): Promise<void>;
setVerboseLogging(enabled: boolean): Promise<void>;
stop(): Promise<void>;
}

View File

@@ -0,0 +1,12 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { Server } from 'vs/base/parts/ipc/node/ipc.cp';
import { NsfwWatcherService } from 'vs/platform/files/node/watcher/nsfw/nsfwWatcherService';
import { createChannelReceiver } from 'vs/base/parts/ipc/common/ipc';
const server = new Server('watcher');
const service = new NsfwWatcherService();
server.registerChannel('watcher', createChannelReceiver(service));

View File

@@ -0,0 +1,99 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { createChannelSender, getNextTickChannel } from 'vs/base/parts/ipc/common/ipc';
import { Client } from 'vs/base/parts/ipc/node/ipc.cp';
import { IDiskFileChange, ILogMessage } from 'vs/platform/files/node/watcher/watcher';
import { Disposable } from 'vs/base/common/lifecycle';
import { IWatcherRequest, IWatcherService } from 'vs/platform/files/node/watcher/nsfw/watcher';
import { FileAccess } from 'vs/base/common/network';
export class FileWatcher extends Disposable {
private static readonly MAX_RESTARTS = 5;
private service: IWatcherService | undefined;
private isDisposed: boolean;
private restartCounter: number;
constructor(
private folders: IWatcherRequest[],
private onDidFilesChange: (changes: IDiskFileChange[]) => void,
private onLogMessage: (msg: ILogMessage) => void,
private verboseLogging: boolean,
) {
super();
this.isDisposed = false;
this.restartCounter = 0;
this.startWatching();
}
private startWatching(): void {
const client = this._register(new Client(
FileAccess.asFileUri('bootstrap-fork', require).fsPath,
{
serverName: 'File Watcher (nsfw)',
args: ['--type=watcherService'],
env: {
AMD_ENTRYPOINT: 'vs/platform/files/node/watcher/nsfw/watcherApp',
PIPE_LOGGING: 'true',
VERBOSE_LOGGING: 'true' // transmit console logs from server to client
}
}
));
this._register(client.onDidProcessExit(() => {
// our watcher app should never be completed because it keeps on watching. being in here indicates
// that the watcher process died and we want to restart it here. we only do it a max number of times
if (!this.isDisposed) {
if (this.restartCounter <= FileWatcher.MAX_RESTARTS) {
this.error('terminated unexpectedly and is restarted again...');
this.restartCounter++;
this.startWatching();
} else {
this.error('failed to start after retrying for some time, giving up. Please report this as a bug report!');
}
}
}));
// Initialize watcher
this.service = createChannelSender<IWatcherService>(getNextTickChannel(client.getChannel('watcher')));
this.service.setVerboseLogging(this.verboseLogging);
this._register(this.service.onDidChangeFile(e => !this.isDisposed && this.onDidFilesChange(e)));
this._register(this.service.onDidLogMessage(m => this.onLogMessage(m)));
// Start watching
this.setFolders(this.folders);
}
setVerboseLogging(verboseLogging: boolean): void {
this.verboseLogging = verboseLogging;
if (!this.isDisposed && this.service) {
this.service.setVerboseLogging(verboseLogging);
}
}
error(message: string) {
this.onLogMessage({ type: 'error', message: `[File Watcher (nsfw)] ${message}` });
}
setFolders(folders: IWatcherRequest[]): void {
this.folders = folders;
if (this.service) {
this.service.setRoots(folders);
}
}
dispose(): void {
this.isDisposed = true;
super.dispose();
}
}

View File

@@ -0,0 +1,365 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as chokidar from 'chokidar';
import * as fs from 'fs';
import * as gracefulFs from 'graceful-fs';
gracefulFs.gracefulify(fs);
import * as extpath from 'vs/base/common/extpath';
import * as glob from 'vs/base/common/glob';
import { FileChangeType } from 'vs/platform/files/common/files';
import { ThrottledDelayer } from 'vs/base/common/async';
import { normalizeNFC } from 'vs/base/common/normalization';
import { realcaseSync } from 'vs/base/node/extpath';
import { isMacintosh, isLinux } from 'vs/base/common/platform';
import { IDiskFileChange, normalizeFileChanges, ILogMessage } from 'vs/platform/files/node/watcher/watcher';
import { IWatcherRequest, IWatcherService, IWatcherOptions } from 'vs/platform/files/node/watcher/unix/watcher';
import { Emitter, Event } from 'vs/base/common/event';
import { equals } from 'vs/base/common/arrays';
import { Disposable } from 'vs/base/common/lifecycle';
process.noAsar = true; // disable ASAR support in watcher process
interface IWatcher {
requests: ExtendedWatcherRequest[];
stop(): Promise<void>;
}
interface ExtendedWatcherRequest extends IWatcherRequest {
parsedPattern?: glob.ParsedPattern;
}
export class ChokidarWatcherService extends Disposable implements IWatcherService {
private static readonly FS_EVENT_DELAY = 50; // aggregate and only emit events when changes have stopped for this duration (in ms)
private static readonly EVENT_SPAM_WARNING_THRESHOLD = 60 * 1000; // warn after certain time span of event spam
private readonly _onDidChangeFile = this._register(new Emitter<IDiskFileChange[]>());
readonly onDidChangeFile = this._onDidChangeFile.event;
private readonly _onDidLogMessage = this._register(new Emitter<ILogMessage>());
readonly onDidLogMessage: Event<ILogMessage> = this._onDidLogMessage.event;
private watchers = new Map<string, IWatcher>();
private _watcherCount = 0;
get wacherCount() { return this._watcherCount; }
private pollingInterval?: number;
private usePolling?: boolean;
private verboseLogging: boolean | undefined;
private spamCheckStartTime: number | undefined;
private spamWarningLogged: boolean | undefined;
private enospcErrorLogged: boolean | undefined;
async init(options: IWatcherOptions): Promise<void> {
this.pollingInterval = options.pollingInterval;
this.usePolling = options.usePolling;
this.watchers.clear();
this._watcherCount = 0;
this.verboseLogging = options.verboseLogging;
}
async setVerboseLogging(enabled: boolean): Promise<void> {
this.verboseLogging = enabled;
}
async setRoots(requests: IWatcherRequest[]): Promise<void> {
const watchers = new Map<string, IWatcher>();
const newRequests: string[] = [];
const requestsByBasePath = normalizeRoots(requests);
// evaluate new & remaining watchers
for (const basePath in requestsByBasePath) {
const watcher = this.watchers.get(basePath);
if (watcher && isEqualRequests(watcher.requests, requestsByBasePath[basePath])) {
watchers.set(basePath, watcher);
this.watchers.delete(basePath);
} else {
newRequests.push(basePath);
}
}
// stop all old watchers
for (const [, watcher] of this.watchers) {
await watcher.stop();
}
// start all new watchers
for (const basePath of newRequests) {
const requests = requestsByBasePath[basePath];
watchers.set(basePath, this.watch(basePath, requests));
}
this.watchers = watchers;
}
private watch(basePath: string, requests: IWatcherRequest[]): IWatcher {
const pollingInterval = this.pollingInterval || 5000;
const usePolling = this.usePolling;
const watcherOpts: chokidar.WatchOptions = {
ignoreInitial: true,
ignorePermissionErrors: true,
followSymlinks: true, // this is the default of chokidar and supports file events through symlinks
interval: pollingInterval, // while not used in normal cases, if any error causes chokidar to fallback to polling, increase its intervals
binaryInterval: pollingInterval,
usePolling: usePolling,
disableGlobbing: true // fix https://github.com/microsoft/vscode/issues/4586
};
const excludes: string[] = [];
const isSingleFolder = requests.length === 1;
if (isSingleFolder) {
excludes.push(...requests[0].excludes); // if there's only one request, use the built-in ignore-filterering
}
if ((isMacintosh || isLinux) && (basePath.length === 0 || basePath === '/')) {
excludes.push('/dev/**');
if (isLinux) {
excludes.push('/proc/**', '/sys/**');
}
}
excludes.push('**/*.asar'); // Ensure we never recurse into ASAR archives
watcherOpts.ignored = excludes;
// Chokidar fails when the basePath does not match case-identical to the path on disk
// so we have to find the real casing of the path and do some path massaging to fix this
// see https://github.com/paulmillr/chokidar/issues/418
const realBasePath = isMacintosh ? (realcaseSync(basePath) || basePath) : basePath;
const realBasePathLength = realBasePath.length;
const realBasePathDiffers = (basePath !== realBasePath);
if (realBasePathDiffers) {
this.warn(`Watcher basePath does not match version on disk and was corrected (original: ${basePath}, real: ${realBasePath})`);
}
if (this.verboseLogging) {
this.log(`Start watching with chockidar: ${realBasePath}, excludes: ${excludes.join(',')}, usePolling: ${usePolling ? 'true, interval ' + pollingInterval : 'false'}`);
}
let chokidarWatcher: chokidar.FSWatcher | null = chokidar.watch(realBasePath, watcherOpts);
this._watcherCount++;
// Detect if for some reason the native watcher library fails to load
if (isMacintosh && chokidarWatcher.options && !chokidarWatcher.options.useFsEvents) {
this.warn('Watcher is not using native fsevents library and is falling back to unefficient polling.');
}
let undeliveredFileEvents: IDiskFileChange[] = [];
let fileEventDelayer: ThrottledDelayer<undefined> | null = new ThrottledDelayer(ChokidarWatcherService.FS_EVENT_DELAY);
const watcher: IWatcher = {
requests,
stop: async () => {
try {
if (this.verboseLogging) {
this.log(`Stop watching: ${basePath}]`);
}
if (chokidarWatcher) {
await chokidarWatcher.close();
this._watcherCount--;
chokidarWatcher = null;
}
if (fileEventDelayer) {
fileEventDelayer.cancel();
fileEventDelayer = null;
}
} catch (error) {
this.warn('Error while stopping watcher: ' + error.toString());
}
}
};
chokidarWatcher.on('all', (type: string, path: string) => {
if (isMacintosh) {
// Mac: uses NFD unicode form on disk, but we want NFC
// See also https://github.com/nodejs/node/issues/2165
path = normalizeNFC(path);
}
if (path.indexOf(realBasePath) < 0) {
return; // we really only care about absolute paths here in our basepath context here
}
// Make sure to convert the path back to its original basePath form if the realpath is different
if (realBasePathDiffers) {
path = basePath + path.substr(realBasePathLength);
}
let eventType: FileChangeType;
switch (type) {
case 'change':
eventType = FileChangeType.UPDATED;
break;
case 'add':
case 'addDir':
eventType = FileChangeType.ADDED;
break;
case 'unlink':
case 'unlinkDir':
eventType = FileChangeType.DELETED;
break;
default:
return;
}
// if there's more than one request we need to do
// extra filtering due to potentially overlapping roots
if (!isSingleFolder) {
if (isIgnored(path, watcher.requests)) {
return;
}
}
const event = { type: eventType, path };
// Logging
if (this.verboseLogging) {
this.log(`${eventType === FileChangeType.ADDED ? '[ADDED]' : eventType === FileChangeType.DELETED ? '[DELETED]' : '[CHANGED]'} ${path}`);
}
// Check for spam
const now = Date.now();
if (undeliveredFileEvents.length === 0) {
this.spamWarningLogged = false;
this.spamCheckStartTime = now;
} else if (!this.spamWarningLogged && typeof this.spamCheckStartTime === 'number' && this.spamCheckStartTime + ChokidarWatcherService.EVENT_SPAM_WARNING_THRESHOLD < now) {
this.spamWarningLogged = true;
this.warn(`Watcher is busy catching up with ${undeliveredFileEvents.length} file changes in 60 seconds. Latest changed path is "${event.path}"`);
}
// Add to buffer
undeliveredFileEvents.push(event);
if (fileEventDelayer) {
// Delay and send buffer
fileEventDelayer.trigger(async () => {
const events = undeliveredFileEvents;
undeliveredFileEvents = [];
// Broadcast to clients normalized
const res = normalizeFileChanges(events);
this._onDidChangeFile.fire(res);
// Logging
if (this.verboseLogging) {
res.forEach(r => {
this.log(` >> normalized ${r.type === FileChangeType.ADDED ? '[ADDED]' : r.type === FileChangeType.DELETED ? '[DELETED]' : '[CHANGED]'} ${r.path}`);
});
}
return undefined;
});
}
});
chokidarWatcher.on('error', (error: NodeJS.ErrnoException) => {
if (error) {
// Specially handle ENOSPC errors that can happen when
// the watcher consumes so many file descriptors that
// we are running into a limit. We only want to warn
// once in this case to avoid log spam.
// See https://github.com/microsoft/vscode/issues/7950
if (error.code === 'ENOSPC') {
if (!this.enospcErrorLogged) {
this.enospcErrorLogged = true;
this.stop();
this.error('Inotify limit reached (ENOSPC)');
}
} else {
this.warn(error.toString());
}
}
});
return watcher;
}
async stop(): Promise<void> {
for (const [, watcher] of this.watchers) {
await watcher.stop();
}
this.watchers.clear();
}
private log(message: string) {
this._onDidLogMessage.fire({ type: 'trace', message: `[File Watcher (chokidar)] ` + message });
}
private warn(message: string) {
this._onDidLogMessage.fire({ type: 'warn', message: `[File Watcher (chokidar)] ` + message });
}
private error(message: string) {
this._onDidLogMessage.fire({ type: 'error', message: `[File Watcher (chokidar)] ` + message });
}
}
function isIgnored(path: string, requests: ExtendedWatcherRequest[]): boolean {
for (const request of requests) {
if (request.path === path) {
return false;
}
if (extpath.isEqualOrParent(path, request.path)) {
if (!request.parsedPattern) {
if (request.excludes && request.excludes.length > 0) {
const pattern = `{${request.excludes.join(',')}}`;
request.parsedPattern = glob.parse(pattern);
} else {
request.parsedPattern = () => false;
}
}
const relPath = path.substr(request.path.length + 1);
if (!request.parsedPattern(relPath)) {
return false;
}
}
}
return true;
}
/**
* Normalizes a set of root paths by grouping by the most parent root path.
* equests with Sub paths are skipped if they have the same ignored set as the parent.
*/
export function normalizeRoots(requests: IWatcherRequest[]): { [basePath: string]: IWatcherRequest[] } {
requests = requests.sort((r1, r2) => r1.path.localeCompare(r2.path));
let prevRequest: IWatcherRequest | null = null;
const result: { [basePath: string]: IWatcherRequest[] } = Object.create(null);
for (const request of requests) {
const basePath = request.path;
const ignored = (request.excludes || []).sort();
if (prevRequest && (extpath.isEqualOrParent(basePath, prevRequest.path))) {
if (!isEqualIgnore(ignored, prevRequest.excludes)) {
result[prevRequest.path].push({ path: basePath, excludes: ignored });
}
} else {
prevRequest = { path: basePath, excludes: ignored };
result[basePath] = [prevRequest];
}
}
return result;
}
function isEqualRequests(r1: readonly IWatcherRequest[], r2: readonly IWatcherRequest[]) {
return equals(r1, r2, (a, b) => a.path === b.path && isEqualIgnore(a.excludes, b.excludes));
}
function isEqualIgnore(i1: readonly string[], i2: readonly string[]) {
return equals(i1, i2);
}

View File

@@ -0,0 +1,92 @@
/*---------------------------------------------------------------------------------------------
* 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 platform from 'vs/base/common/platform';
import { normalizeRoots } from 'vs/platform/files/node/watcher/unix/chokidarWatcherService';
import { IWatcherRequest } from 'vs/platform/files/node/watcher/unix/watcher';
function newRequest(basePath: string, ignored: string[] = []): IWatcherRequest {
return { path: basePath, excludes: ignored };
}
function assertNormalizedRootPath(inputPaths: string[], expectedPaths: string[]) {
const requests = inputPaths.map(path => newRequest(path));
const actual = normalizeRoots(requests);
assert.deepEqual(Object.keys(actual).sort(), expectedPaths);
}
function assertNormalizedRequests(inputRequests: IWatcherRequest[], expectedRequests: { [path: string]: IWatcherRequest[] }) {
const actual = normalizeRoots(inputRequests);
const actualPath = Object.keys(actual).sort();
const expectedPaths = Object.keys(expectedRequests).sort();
assert.deepEqual(actualPath, expectedPaths);
for (let path of actualPath) {
let a = expectedRequests[path].sort((r1, r2) => r1.path.localeCompare(r2.path));
let e = expectedRequests[path].sort((r1, r2) => r1.path.localeCompare(r2.path));
assert.deepEqual(a, e);
}
}
suite('Chokidar normalizeRoots', () => {
test('should not impacts roots that don\'t overlap', () => {
if (platform.isWindows) {
assertNormalizedRootPath(['C:\\a'], ['C:\\a']);
assertNormalizedRootPath(['C:\\a', 'C:\\b'], ['C:\\a', 'C:\\b']);
assertNormalizedRootPath(['C:\\a', 'C:\\b', 'C:\\c\\d\\e'], ['C:\\a', 'C:\\b', 'C:\\c\\d\\e']);
} else {
assertNormalizedRootPath(['/a'], ['/a']);
assertNormalizedRootPath(['/a', '/b'], ['/a', '/b']);
assertNormalizedRootPath(['/a', '/b', '/c/d/e'], ['/a', '/b', '/c/d/e']);
}
});
test('should remove sub-folders of other roots', () => {
if (platform.isWindows) {
assertNormalizedRootPath(['C:\\a', 'C:\\a\\b'], ['C:\\a']);
assertNormalizedRootPath(['C:\\a', 'C:\\b', 'C:\\a\\b'], ['C:\\a', 'C:\\b']);
assertNormalizedRootPath(['C:\\b\\a', 'C:\\a', 'C:\\b', 'C:\\a\\b'], ['C:\\a', 'C:\\b']);
assertNormalizedRootPath(['C:\\a', 'C:\\a\\b', 'C:\\a\\c\\d'], ['C:\\a']);
} else {
assertNormalizedRootPath(['/a', '/a/b'], ['/a']);
assertNormalizedRootPath(['/a', '/b', '/a/b'], ['/a', '/b']);
assertNormalizedRootPath(['/b/a', '/a', '/b', '/a/b'], ['/a', '/b']);
assertNormalizedRootPath(['/a', '/a/b', '/a/c/d'], ['/a']);
assertNormalizedRootPath(['/a/c/d/e', '/a/b/d', '/a/c/d', '/a/c/e/f', '/a/b'], ['/a/b', '/a/c/d', '/a/c/e/f']);
}
});
test('should remove duplicates', () => {
if (platform.isWindows) {
assertNormalizedRootPath(['C:\\a', 'C:\\a\\', 'C:\\a'], ['C:\\a']);
} else {
assertNormalizedRootPath(['/a', '/a/', '/a'], ['/a']);
assertNormalizedRootPath(['/a', '/b', '/a/b'], ['/a', '/b']);
assertNormalizedRootPath(['/b/a', '/a', '/b', '/a/b'], ['/a', '/b']);
assertNormalizedRootPath(['/a', '/a/b', '/a/c/d'], ['/a']);
}
});
test('nested requests', () => {
let p1, p2, p3;
if (platform.isWindows) {
p1 = 'C:\\a';
p2 = 'C:\\a\\b';
p3 = 'C:\\a\\b\\c';
} else {
p1 = '/a';
p2 = '/a/b';
p3 = '/a/b/c';
}
const r1 = newRequest(p1, ['**/*.ts']);
const r2 = newRequest(p2, ['**/*.js']);
const r3 = newRequest(p3, ['**/*.ts']);
assertNormalizedRequests([r1, r2], { [p1]: [r1, r2] });
assertNormalizedRequests([r2, r1], { [p1]: [r1, r2] });
assertNormalizedRequests([r1, r2, r3], { [p1]: [r1, r2, r3] });
assertNormalizedRequests([r1, r3], { [p1]: [r1] });
assertNormalizedRequests([r2, r3], { [p2]: [r2, r3] });
});
});

View File

@@ -0,0 +1,31 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { Event } from 'vs/base/common/event';
import { IDiskFileChange, ILogMessage } from 'vs/platform/files/node/watcher/watcher';
export interface IWatcherRequest {
path: string;
excludes: string[];
}
export interface IWatcherOptions {
pollingInterval?: number;
usePolling?: boolean;
verboseLogging?: boolean;
}
export interface IWatcherService {
readonly onDidChangeFile: Event<IDiskFileChange[]>;
readonly onDidLogMessage: Event<ILogMessage>;
init(options: IWatcherOptions): Promise<void>;
setRoots(roots: IWatcherRequest[]): Promise<void>;
setVerboseLogging(enabled: boolean): Promise<void>;
stop(): Promise<void>;
}

View File

@@ -0,0 +1,12 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { Server } from 'vs/base/parts/ipc/node/ipc.cp';
import { ChokidarWatcherService } from 'vs/platform/files/node/watcher/unix/chokidarWatcherService';
import { createChannelReceiver } from 'vs/base/parts/ipc/common/ipc';
const server = new Server('watcher');
const service = new ChokidarWatcherService();
server.registerChannel('watcher', createChannelReceiver(service));

View File

@@ -0,0 +1,100 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { createChannelSender, getNextTickChannel } from 'vs/base/parts/ipc/common/ipc';
import { Client } from 'vs/base/parts/ipc/node/ipc.cp';
import { IDiskFileChange, ILogMessage } from 'vs/platform/files/node/watcher/watcher';
import { Disposable } from 'vs/base/common/lifecycle';
import { IWatcherRequest, IWatcherOptions, IWatcherService } from 'vs/platform/files/node/watcher/unix/watcher';
import { FileAccess } from 'vs/base/common/network';
export class FileWatcher extends Disposable {
private static readonly MAX_RESTARTS = 5;
private isDisposed: boolean;
private restartCounter: number;
private service: IWatcherService | undefined;
constructor(
private folders: IWatcherRequest[],
private onDidFilesChange: (changes: IDiskFileChange[]) => void,
private onLogMessage: (msg: ILogMessage) => void,
private verboseLogging: boolean,
private watcherOptions: IWatcherOptions = {}
) {
super();
this.isDisposed = false;
this.restartCounter = 0;
this.startWatching();
}
private startWatching(): void {
const client = this._register(new Client(
FileAccess.asFileUri('bootstrap-fork', require).fsPath,
{
serverName: 'File Watcher (chokidar)',
args: ['--type=watcherService'],
env: {
AMD_ENTRYPOINT: 'vs/platform/files/node/watcher/unix/watcherApp',
PIPE_LOGGING: 'true',
VERBOSE_LOGGING: 'true' // transmit console logs from server to client
}
}
));
this._register(client.onDidProcessExit(() => {
// our watcher app should never be completed because it keeps on watching. being in here indicates
// that the watcher process died and we want to restart it here. we only do it a max number of times
if (!this.isDisposed) {
if (this.restartCounter <= FileWatcher.MAX_RESTARTS) {
this.error('terminated unexpectedly and is restarted again...');
this.restartCounter++;
this.startWatching();
} else {
this.error('failed to start after retrying for some time, giving up. Please report this as a bug report!');
}
}
}));
// Initialize watcher
this.service = createChannelSender<IWatcherService>(getNextTickChannel(client.getChannel('watcher')));
this.service.init({ ...this.watcherOptions, verboseLogging: this.verboseLogging });
this._register(this.service.onDidChangeFile(e => !this.isDisposed && this.onDidFilesChange(e)));
this._register(this.service.onDidLogMessage(m => this.onLogMessage(m)));
// Start watching
this.service.setRoots(this.folders);
}
error(message: string) {
this.onLogMessage({ type: 'error', message: `[File Watcher (chokidar)] ${message}` });
}
setVerboseLogging(verboseLogging: boolean): void {
this.verboseLogging = verboseLogging;
if (this.service) {
this.service.setVerboseLogging(verboseLogging);
}
}
setFolders(folders: IWatcherRequest[]): void {
this.folders = folders;
if (this.service) {
this.service.setRoots(folders);
}
}
dispose(): void {
this.isDisposed = true;
super.dispose();
}
}

View File

@@ -0,0 +1,109 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { URI as uri } from 'vs/base/common/uri';
import { FileChangeType, isParent, IFileChange } from 'vs/platform/files/common/files';
import { isLinux } from 'vs/base/common/platform';
export interface IDiskFileChange {
type: FileChangeType;
path: string;
}
export interface ILogMessage {
type: 'trace' | 'warn' | 'error';
message: string;
}
export function toFileChanges(changes: IDiskFileChange[]): IFileChange[] {
return changes.map(change => ({
type: change.type,
resource: uri.file(change.path)
}));
}
export function normalizeFileChanges(changes: IDiskFileChange[]): IDiskFileChange[] {
// Build deltas
const normalizer = new EventNormalizer();
for (const event of changes) {
normalizer.processEvent(event);
}
return normalizer.normalize();
}
class EventNormalizer {
private normalized: IDiskFileChange[] = [];
private mapPathToChange: Map<string, IDiskFileChange> = new Map();
processEvent(event: IDiskFileChange): void {
const existingEvent = this.mapPathToChange.get(event.path);
// Event path already exists
if (existingEvent) {
const currentChangeType = existingEvent.type;
const newChangeType = event.type;
// ignore CREATE followed by DELETE in one go
if (currentChangeType === FileChangeType.ADDED && newChangeType === FileChangeType.DELETED) {
this.mapPathToChange.delete(event.path);
this.normalized.splice(this.normalized.indexOf(existingEvent), 1);
}
// flatten DELETE followed by CREATE into CHANGE
else if (currentChangeType === FileChangeType.DELETED && newChangeType === FileChangeType.ADDED) {
existingEvent.type = FileChangeType.UPDATED;
}
// Do nothing. Keep the created event
else if (currentChangeType === FileChangeType.ADDED && newChangeType === FileChangeType.UPDATED) { }
// Otherwise apply change type
else {
existingEvent.type = newChangeType;
}
}
// Otherwise store new
else {
this.normalized.push(event);
this.mapPathToChange.set(event.path, event);
}
}
normalize(): IDiskFileChange[] {
const addedChangeEvents: IDiskFileChange[] = [];
const deletedPaths: string[] = [];
// This algorithm will remove all DELETE events up to the root folder
// that got deleted if any. This ensures that we are not producing
// DELETE events for each file inside a folder that gets deleted.
//
// 1.) split ADD/CHANGE and DELETED events
// 2.) sort short deleted paths to the top
// 3.) for each DELETE, check if there is a deleted parent and ignore the event in that case
return this.normalized.filter(e => {
if (e.type !== FileChangeType.DELETED) {
addedChangeEvents.push(e);
return false; // remove ADD / CHANGE
}
return true; // keep DELETE
}).sort((e1, e2) => {
return e1.path.length - e2.path.length; // shortest path first
}).filter(e => {
if (deletedPaths.some(d => isParent(e.path, d, !isLinux /* ignorecase */))) {
return false; // DELETE is ignored if parent is deleted already
}
// otherwise mark as deleted
deletedPaths.push(e.path);
return true;
}).concat(addedChangeEvents);
}
}

View File

@@ -0,0 +1,8 @@
# Native File Watching for Windows using C# FileSystemWatcher
- Repository: https://github.com/microsoft/vscode-filewatcher-windows
# Build
- Build in "Release" config
- Copy CodeHelper.exe over into this folder

View File

@@ -0,0 +1,140 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as cp from 'child_process';
import { FileChangeType } from 'vs/platform/files/common/files';
import * as decoder from 'vs/base/node/decoder';
import * as glob from 'vs/base/common/glob';
import { IDiskFileChange, ILogMessage } from 'vs/platform/files/node/watcher/watcher';
import { FileAccess } from 'vs/base/common/network';
export class OutOfProcessWin32FolderWatcher {
private static readonly MAX_RESTARTS = 5;
private static changeTypeMap: FileChangeType[] = [FileChangeType.UPDATED, FileChangeType.ADDED, FileChangeType.DELETED];
private ignored: glob.ParsedPattern[];
private handle: cp.ChildProcess | undefined;
private restartCounter: number;
constructor(
private watchedFolder: string,
ignored: string[],
private eventCallback: (events: IDiskFileChange[]) => void,
private logCallback: (message: ILogMessage) => void,
private verboseLogging: boolean
) {
this.restartCounter = 0;
if (Array.isArray(ignored)) {
this.ignored = ignored.map(i => glob.parse(i));
} else {
this.ignored = [];
}
// Logging
if (this.verboseLogging) {
this.log(`Start watching: ${watchedFolder}`);
}
this.startWatcher();
}
private startWatcher(): void {
const args = [this.watchedFolder];
if (this.verboseLogging) {
args.push('-verbose');
}
this.handle = cp.spawn(FileAccess.asFileUri('vs/platform/files/node/watcher/win32/CodeHelper.exe', require).fsPath, args);
const stdoutLineDecoder = new decoder.LineDecoder();
// Events over stdout
this.handle.stdout!.on('data', (data: Buffer) => {
// Collect raw events from output
const rawEvents: IDiskFileChange[] = [];
stdoutLineDecoder.write(data).forEach((line) => {
const eventParts = line.split('|');
if (eventParts.length === 2) {
const changeType = Number(eventParts[0]);
const absolutePath = eventParts[1];
// File Change Event (0 Changed, 1 Created, 2 Deleted)
if (changeType >= 0 && changeType < 3) {
// Support ignores
if (this.ignored && this.ignored.some(ignore => ignore(absolutePath))) {
if (this.verboseLogging) {
this.log(absolutePath);
}
return;
}
// Otherwise record as event
rawEvents.push({
type: OutOfProcessWin32FolderWatcher.changeTypeMap[changeType],
path: absolutePath
});
}
// 3 Logging
else {
this.log(eventParts[1]);
}
}
});
// Trigger processing of events through the delayer to batch them up properly
if (rawEvents.length > 0) {
this.eventCallback(rawEvents);
}
});
// Errors
this.handle.on('error', (error: Error) => this.onError(error));
this.handle.stderr!.on('data', (data: Buffer) => this.onError(data));
// Exit
this.handle.on('exit', (code: number, signal: string) => this.onExit(code, signal));
}
private onError(error: Error | Buffer): void {
this.error('process error: ' + error.toString());
}
private onExit(code: number, signal: string): void {
if (this.handle) { // exit while not yet being disposed is unexpected!
this.error(`terminated unexpectedly (code: ${code}, signal: ${signal})`);
if (this.restartCounter <= OutOfProcessWin32FolderWatcher.MAX_RESTARTS) {
this.error('is restarted again...');
this.restartCounter++;
this.startWatcher(); // restart
} else {
this.error('Watcher failed to start after retrying for some time, giving up. Please report this as a bug report!');
}
}
}
private error(message: string) {
this.logCallback({ type: 'error', message: `[File Watcher (C#)] ${message}` });
}
private log(message: string) {
this.logCallback({ type: 'trace', message: `[File Watcher (C#)] ${message}` });
}
dispose(): void {
if (this.handle) {
this.handle.kill();
this.handle = undefined;
}
}
}

View File

@@ -0,0 +1,75 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { IDiskFileChange, ILogMessage } from 'vs/platform/files/node/watcher/watcher';
import { OutOfProcessWin32FolderWatcher } from 'vs/platform/files/node/watcher/win32/csharpWatcherService';
import { posix } from 'vs/base/common/path';
import { rtrim } from 'vs/base/common/strings';
import { IDisposable } from 'vs/base/common/lifecycle';
export class FileWatcher implements IDisposable {
private folder: { path: string, excludes: string[] };
private service: OutOfProcessWin32FolderWatcher | undefined = undefined;
constructor(
folders: { path: string, excludes: string[] }[],
private onDidFilesChange: (changes: IDiskFileChange[]) => void,
private onLogMessage: (msg: ILogMessage) => void,
private verboseLogging: boolean
) {
this.folder = folders[0];
if (this.folder.path.indexOf('\\\\') === 0 && this.folder.path.endsWith(posix.sep)) {
// for some weird reason, node adds a trailing slash to UNC paths
// we never ever want trailing slashes as our base path unless
// someone opens root ("/").
// See also https://github.com/nodejs/io.js/issues/1765
this.folder.path = rtrim(this.folder.path, posix.sep);
}
this.service = this.startWatching();
}
private get isDisposed(): boolean {
return !this.service;
}
private startWatching(): OutOfProcessWin32FolderWatcher {
return new OutOfProcessWin32FolderWatcher(
this.folder.path,
this.folder.excludes,
events => this.onFileEvents(events),
message => this.onLogMessage(message),
this.verboseLogging
);
}
setVerboseLogging(verboseLogging: boolean): void {
this.verboseLogging = verboseLogging;
if (this.service) {
this.service.dispose();
this.service = this.startWatching();
}
}
private onFileEvents(events: IDiskFileChange[]): void {
if (this.isDisposed) {
return;
}
// Emit through event emitter
if (events.length > 0) {
this.onDidFilesChange(events);
}
}
dispose(): void {
if (this.service) {
this.service.dispose();
this.service = undefined;
}
}
}

View File

@@ -0,0 +1,125 @@
/*---------------------------------------------------------------------------------------------
* 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 { FileService } from 'vs/platform/files/common/fileService';
import { URI } from 'vs/base/common/uri';
import { IFileSystemProviderRegistrationEvent, FileSystemProviderCapabilities, IFileSystemProviderCapabilitiesChangeEvent } from 'vs/platform/files/common/files';
import { IDisposable, toDisposable } from 'vs/base/common/lifecycle';
import { NullLogService } from 'vs/platform/log/common/log';
import { timeout } from 'vs/base/common/async';
import { NullFileSystemProvider } from 'vs/platform/files/test/common/nullFileSystemProvider';
suite('File Service', () => {
test('provider registration', async () => {
const service = new FileService(new NullLogService());
const resource = URI.parse('test://foo/bar');
const provider = new NullFileSystemProvider();
assert.equal(service.canHandleResource(resource), false);
const registrations: IFileSystemProviderRegistrationEvent[] = [];
service.onDidChangeFileSystemProviderRegistrations(e => {
registrations.push(e);
});
const capabilityChanges: IFileSystemProviderCapabilitiesChangeEvent[] = [];
service.onDidChangeFileSystemProviderCapabilities(e => {
capabilityChanges.push(e);
});
let registrationDisposable: IDisposable | undefined = undefined;
let callCount = 0;
service.onWillActivateFileSystemProvider(e => {
callCount++;
if (e.scheme === 'test' && callCount === 1) {
e.join(new Promise(resolve => {
registrationDisposable = service.registerProvider('test', provider);
resolve();
}));
}
});
await service.activateProvider('test');
assert.equal(service.canHandleResource(resource), true);
assert.equal(registrations.length, 1);
assert.equal(registrations[0].scheme, 'test');
assert.equal(registrations[0].added, true);
assert.ok(registrationDisposable);
assert.equal(capabilityChanges.length, 0);
provider.setCapabilities(FileSystemProviderCapabilities.FileFolderCopy);
assert.equal(capabilityChanges.length, 1);
provider.setCapabilities(FileSystemProviderCapabilities.Readonly);
assert.equal(capabilityChanges.length, 2);
await service.activateProvider('test');
assert.equal(callCount, 2); // activation is called again
assert.equal(service.hasCapability(resource, FileSystemProviderCapabilities.Readonly), true);
assert.equal(service.hasCapability(resource, FileSystemProviderCapabilities.FileOpenReadWriteClose), false);
registrationDisposable!.dispose();
assert.equal(service.canHandleResource(resource), false);
assert.equal(registrations.length, 2);
assert.equal(registrations[1].scheme, 'test');
assert.equal(registrations[1].added, false);
});
test('watch', async () => {
const service = new FileService(new NullLogService());
let disposeCounter = 0;
service.registerProvider('test', new NullFileSystemProvider(() => {
return toDisposable(() => {
disposeCounter++;
});
}));
await service.activateProvider('test');
const resource1 = URI.parse('test://foo/bar1');
const watcher1Disposable = service.watch(resource1);
await timeout(0); // service.watch() is async
assert.equal(disposeCounter, 0);
watcher1Disposable.dispose();
assert.equal(disposeCounter, 1);
disposeCounter = 0;
const resource2 = URI.parse('test://foo/bar2');
const watcher2Disposable1 = service.watch(resource2);
const watcher2Disposable2 = service.watch(resource2);
const watcher2Disposable3 = service.watch(resource2);
await timeout(0); // service.watch() is async
assert.equal(disposeCounter, 0);
watcher2Disposable1.dispose();
assert.equal(disposeCounter, 0);
watcher2Disposable2.dispose();
assert.equal(disposeCounter, 0);
watcher2Disposable3.dispose();
assert.equal(disposeCounter, 1);
disposeCounter = 0;
const resource3 = URI.parse('test://foo/bar3');
const watcher3Disposable1 = service.watch(resource3);
const watcher3Disposable2 = service.watch(resource3, { recursive: true, excludes: [] });
await timeout(0); // service.watch() is async
assert.equal(disposeCounter, 0);
watcher3Disposable1.dispose();
assert.equal(disposeCounter, 1);
watcher3Disposable2.dispose();
assert.equal(disposeCounter, 2);
});
});

View File

@@ -0,0 +1,77 @@
/*---------------------------------------------------------------------------------------------
* 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 { FileService } from 'vs/platform/files/common/fileService';
import { Schemas } from 'vs/base/common/network';
import { posix } from 'vs/base/common/path';
import { URI } from 'vs/base/common/uri';
import { FileOperation, FileOperationEvent } from 'vs/platform/files/common/files';
import { NullLogService } from 'vs/platform/log/common/log';
import { DisposableStore } from 'vs/base/common/lifecycle';
import { IIndexedDBFileSystemProvider, IndexedDB, INDEXEDDB_LOGS_OBJECT_STORE, INDEXEDDB_USERDATA_OBJECT_STORE } from 'vs/platform/files/browser/indexedDBFileSystemProvider';
import { assertIsDefined } from 'vs/base/common/types';
// FileService doesn't work with \ leading a path. Windows join swaps /'s for \'s,
// making /-style absolute paths fail isAbsolute checks.
const join = posix.join;
suite('IndexedDB File Service', function () {
const logSchema = 'logs';
let service: FileService;
let logFileProvider: IIndexedDBFileSystemProvider;
let userdataFileProvider: IIndexedDBFileSystemProvider;
const testDir = '/';
const makeLogfileURI = (path: string) => URI.from({ scheme: logSchema, path });
const makeUserdataURI = (path: string) => URI.from({ scheme: Schemas.userData, path });
const disposables = new DisposableStore();
setup(async () => {
const logService = new NullLogService();
service = new FileService(logService);
disposables.add(service);
logFileProvider = assertIsDefined(await new IndexedDB().createFileSystemProvider(Schemas.file, INDEXEDDB_LOGS_OBJECT_STORE));
disposables.add(service.registerProvider(logSchema, logFileProvider));
disposables.add(logFileProvider);
userdataFileProvider = assertIsDefined(await new IndexedDB().createFileSystemProvider(logSchema, INDEXEDDB_USERDATA_OBJECT_STORE));
disposables.add(service.registerProvider(Schemas.userData, userdataFileProvider));
disposables.add(userdataFileProvider);
});
teardown(async () => {
disposables.clear();
await logFileProvider.delete(makeLogfileURI(testDir), { recursive: true, useTrash: false });
await userdataFileProvider.delete(makeUserdataURI(testDir), { recursive: true, useTrash: false });
});
test('createFolder', async () => {
let event: FileOperationEvent | undefined;
disposables.add(service.onDidRunOperation(e => event = e));
const parent = await service.resolve(makeUserdataURI(testDir));
const newFolderResource = makeUserdataURI(join(parent.resource.path, 'newFolder'));
assert.equal((await userdataFileProvider.readdir(parent.resource)).length, 0);
const newFolder = await service.createFolder(newFolderResource);
assert.equal(newFolder.name, 'newFolder');
// Invalid.. dirs dont exist in our IDBFSB.
// assert.equal((await userdataFileProvider.readdir(parent.resource)).length, 1);
assert.ok(event);
assert.equal(event!.resource.path, newFolderResource.path);
assert.equal(event!.operation, FileOperation.CREATE);
assert.equal(event!.target!.resource.path, newFolderResource.path);
assert.equal(event!.target!.isDirectory, true);
});
});

View File

@@ -0,0 +1,256 @@
/*---------------------------------------------------------------------------------------------
* 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 { URI } from 'vs/base/common/uri';
import { isEqual, isEqualOrParent } from 'vs/base/common/extpath';
import { FileChangeType, FileChangesEvent, isParent } from 'vs/platform/files/common/files';
import { isLinux, isMacintosh, isWindows } from 'vs/base/common/platform';
import { toResource } from 'vs/base/test/common/utils';
suite('Files', () => {
test('FileChangesEvent - basics', function () {
const changes = [
{ resource: toResource.call(this, '/foo/updated.txt'), type: FileChangeType.UPDATED },
{ resource: toResource.call(this, '/foo/otherupdated.txt'), type: FileChangeType.UPDATED },
{ resource: toResource.call(this, '/added.txt'), type: FileChangeType.ADDED },
{ resource: toResource.call(this, '/bar/deleted.txt'), type: FileChangeType.DELETED },
{ resource: toResource.call(this, '/bar/folder'), type: FileChangeType.DELETED },
{ resource: toResource.call(this, '/BAR/FOLDER'), type: FileChangeType.DELETED }
];
for (const ignorePathCasing of [false, true]) {
const event = new FileChangesEvent(changes, ignorePathCasing);
assert(!event.contains(toResource.call(this, '/foo'), FileChangeType.UPDATED));
assert(event.affects(toResource.call(this, '/foo'), FileChangeType.UPDATED));
assert(event.contains(toResource.call(this, '/foo/updated.txt'), FileChangeType.UPDATED));
assert(event.affects(toResource.call(this, '/foo/updated.txt'), FileChangeType.UPDATED));
assert(event.contains(toResource.call(this, '/foo/updated.txt'), FileChangeType.UPDATED, FileChangeType.ADDED));
assert(event.affects(toResource.call(this, '/foo/updated.txt'), FileChangeType.UPDATED, FileChangeType.ADDED));
assert(event.contains(toResource.call(this, '/foo/updated.txt'), FileChangeType.UPDATED, FileChangeType.ADDED, FileChangeType.DELETED));
assert(!event.contains(toResource.call(this, '/foo/updated.txt'), FileChangeType.ADDED, FileChangeType.DELETED));
assert(!event.contains(toResource.call(this, '/foo/updated.txt'), FileChangeType.ADDED));
assert(!event.contains(toResource.call(this, '/foo/updated.txt'), FileChangeType.DELETED));
assert(!event.affects(toResource.call(this, '/foo/updated.txt'), FileChangeType.DELETED));
assert(event.contains(toResource.call(this, '/bar/folder'), FileChangeType.DELETED));
assert(event.contains(toResource.call(this, '/BAR/FOLDER'), FileChangeType.DELETED));
assert(event.affects(toResource.call(this, '/BAR'), FileChangeType.DELETED));
if (ignorePathCasing) {
assert(event.contains(toResource.call(this, '/BAR/folder'), FileChangeType.DELETED));
assert(event.affects(toResource.call(this, '/bar'), FileChangeType.DELETED));
} else {
assert(!event.contains(toResource.call(this, '/BAR/folder'), FileChangeType.DELETED));
assert(event.affects(toResource.call(this, '/bar'), FileChangeType.DELETED));
}
assert(event.contains(toResource.call(this, '/bar/folder/somefile'), FileChangeType.DELETED));
assert(event.contains(toResource.call(this, '/bar/folder/somefile/test.txt'), FileChangeType.DELETED));
assert(event.contains(toResource.call(this, '/BAR/FOLDER/somefile/test.txt'), FileChangeType.DELETED));
if (ignorePathCasing) {
assert(event.contains(toResource.call(this, '/BAR/folder/somefile/test.txt'), FileChangeType.DELETED));
} else {
assert(!event.contains(toResource.call(this, '/BAR/folder/somefile/test.txt'), FileChangeType.DELETED));
}
assert(!event.contains(toResource.call(this, '/bar/folder2/somefile'), FileChangeType.DELETED));
assert.strictEqual(6, event.changes.length);
assert.strictEqual(1, event.getAdded().length);
assert.strictEqual(true, event.gotAdded());
assert.strictEqual(2, event.getUpdated().length);
assert.strictEqual(true, event.gotUpdated());
assert.strictEqual(ignorePathCasing ? 2 : 3, event.getDeleted().length);
assert.strictEqual(true, event.gotDeleted());
}
});
test('FileChangesEvent - supports multiple changes on file tree', function () {
for (const type of [FileChangeType.ADDED, FileChangeType.UPDATED, FileChangeType.DELETED]) {
const changes = [
{ resource: toResource.call(this, '/foo/bar/updated.txt'), type },
{ resource: toResource.call(this, '/foo/bar/otherupdated.txt'), type },
{ resource: toResource.call(this, '/foo/bar'), type },
{ resource: toResource.call(this, '/foo'), type },
{ resource: toResource.call(this, '/bar'), type },
{ resource: toResource.call(this, '/bar/foo'), type },
{ resource: toResource.call(this, '/bar/foo/updated.txt'), type },
{ resource: toResource.call(this, '/bar/foo/otherupdated.txt'), type }
];
for (const ignorePathCasing of [false, true]) {
const event = new FileChangesEvent(changes, ignorePathCasing);
for (const change of changes) {
assert(event.contains(change.resource, type));
assert(event.affects(change.resource, type));
}
assert(event.affects(toResource.call(this, '/foo'), type));
assert(event.affects(toResource.call(this, '/bar'), type));
assert(event.affects(toResource.call(this, '/'), type));
assert(!event.affects(toResource.call(this, '/foobar'), type));
assert(!event.contains(toResource.call(this, '/some/foo/bar'), type));
assert(!event.affects(toResource.call(this, '/some/foo/bar'), type));
assert(!event.contains(toResource.call(this, '/some/bar'), type));
assert(!event.affects(toResource.call(this, '/some/bar'), type));
switch (type) {
case FileChangeType.ADDED:
assert.strictEqual(8, event.getAdded().length);
break;
case FileChangeType.UPDATED:
assert.strictEqual(8, event.getUpdated().length);
break;
case FileChangeType.DELETED:
assert.strictEqual(8, event.getDeleted().length);
break;
}
}
}
});
function testIsEqual(testMethod: (pA: string, pB: string, ignoreCase: boolean) => boolean): void {
// corner cases
assert(testMethod('', '', true));
assert(!testMethod(null!, '', true));
assert(!testMethod(undefined!, '', true));
// basics (string)
assert(testMethod('/', '/', true));
assert(testMethod('/some', '/some', true));
assert(testMethod('/some/path', '/some/path', true));
assert(testMethod('c:\\', 'c:\\', true));
assert(testMethod('c:\\some', 'c:\\some', true));
assert(testMethod('c:\\some\\path', 'c:\\some\\path', true));
assert(testMethod('/someöäü/path', '/someöäü/path', true));
assert(testMethod('c:\\someöäü\\path', 'c:\\someöäü\\path', true));
assert(!testMethod('/some/path', '/some/other/path', true));
assert(!testMethod('c:\\some\\path', 'c:\\some\\other\\path', true));
assert(!testMethod('c:\\some\\path', 'd:\\some\\path', true));
assert(testMethod('/some/path', '/some/PATH', true));
assert(testMethod('/someöäü/path', '/someÖÄÜ/PATH', true));
assert(testMethod('c:\\some\\path', 'c:\\some\\PATH', true));
assert(testMethod('c:\\someöäü\\path', 'c:\\someÖÄÜ\\PATH', true));
assert(testMethod('c:\\some\\path', 'C:\\some\\PATH', true));
}
test('isEqual (ignoreCase)', function () {
testIsEqual(isEqual);
// basics (uris)
assert(isEqual(URI.file('/some/path').fsPath, URI.file('/some/path').fsPath, true));
assert(isEqual(URI.file('c:\\some\\path').fsPath, URI.file('c:\\some\\path').fsPath, true));
assert(isEqual(URI.file('/someöäü/path').fsPath, URI.file('/someöäü/path').fsPath, true));
assert(isEqual(URI.file('c:\\someöäü\\path').fsPath, URI.file('c:\\someöäü\\path').fsPath, true));
assert(!isEqual(URI.file('/some/path').fsPath, URI.file('/some/other/path').fsPath, true));
assert(!isEqual(URI.file('c:\\some\\path').fsPath, URI.file('c:\\some\\other\\path').fsPath, true));
assert(isEqual(URI.file('/some/path').fsPath, URI.file('/some/PATH').fsPath, true));
assert(isEqual(URI.file('/someöäü/path').fsPath, URI.file('/someÖÄÜ/PATH').fsPath, true));
assert(isEqual(URI.file('c:\\some\\path').fsPath, URI.file('c:\\some\\PATH').fsPath, true));
assert(isEqual(URI.file('c:\\someöäü\\path').fsPath, URI.file('c:\\someÖÄÜ\\PATH').fsPath, true));
assert(isEqual(URI.file('c:\\some\\path').fsPath, URI.file('C:\\some\\PATH').fsPath, true));
});
test('isParent (ignorecase)', function () {
if (isWindows) {
assert(isParent('c:\\some\\path', 'c:\\', true));
assert(isParent('c:\\some\\path', 'c:\\some', true));
assert(isParent('c:\\some\\path', 'c:\\some\\', true));
assert(isParent('c:\\someöäü\\path', 'c:\\someöäü', true));
assert(isParent('c:\\someöäü\\path', 'c:\\someöäü\\', true));
assert(isParent('c:\\foo\\bar\\test.ts', 'c:\\foo\\bar', true));
assert(isParent('c:\\foo\\bar\\test.ts', 'c:\\foo\\bar\\', true));
assert(isParent('c:\\some\\path', 'C:\\', true));
assert(isParent('c:\\some\\path', 'c:\\SOME', true));
assert(isParent('c:\\some\\path', 'c:\\SOME\\', true));
assert(!isParent('c:\\some\\path', 'd:\\', true));
assert(!isParent('c:\\some\\path', 'c:\\some\\path', true));
assert(!isParent('c:\\some\\path', 'd:\\some\\path', true));
assert(!isParent('c:\\foo\\bar\\test.ts', 'c:\\foo\\barr', true));
assert(!isParent('c:\\foo\\bar\\test.ts', 'c:\\foo\\bar\\test', true));
}
if (isMacintosh || isLinux) {
assert(isParent('/some/path', '/', true));
assert(isParent('/some/path', '/some', true));
assert(isParent('/some/path', '/some/', true));
assert(isParent('/someöäü/path', '/someöäü', true));
assert(isParent('/someöäü/path', '/someöäü/', true));
assert(isParent('/foo/bar/test.ts', '/foo/bar', true));
assert(isParent('/foo/bar/test.ts', '/foo/bar/', true));
assert(isParent('/some/path', '/SOME', true));
assert(isParent('/some/path', '/SOME/', true));
assert(isParent('/someöäü/path', '/SOMEÖÄÜ', true));
assert(isParent('/someöäü/path', '/SOMEÖÄÜ/', true));
assert(!isParent('/some/path', '/some/path', true));
assert(!isParent('/foo/bar/test.ts', '/foo/barr', true));
assert(!isParent('/foo/bar/test.ts', '/foo/bar/test', true));
}
});
test('isEqualOrParent (ignorecase)', function () {
// same assertions apply as with isEqual()
testIsEqual(isEqualOrParent); //
if (isWindows) {
assert(isEqualOrParent('c:\\some\\path', 'c:\\', true));
assert(isEqualOrParent('c:\\some\\path', 'c:\\some', true));
assert(isEqualOrParent('c:\\some\\path', 'c:\\some\\', true));
assert(isEqualOrParent('c:\\someöäü\\path', 'c:\\someöäü', true));
assert(isEqualOrParent('c:\\someöäü\\path', 'c:\\someöäü\\', true));
assert(isEqualOrParent('c:\\foo\\bar\\test.ts', 'c:\\foo\\bar', true));
assert(isEqualOrParent('c:\\foo\\bar\\test.ts', 'c:\\foo\\bar\\', true));
assert(isEqualOrParent('c:\\some\\path', 'c:\\some\\path', true));
assert(isEqualOrParent('c:\\foo\\bar\\test.ts', 'c:\\foo\\bar\\test.ts', true));
assert(isEqualOrParent('c:\\some\\path', 'C:\\', true));
assert(isEqualOrParent('c:\\some\\path', 'c:\\SOME', true));
assert(isEqualOrParent('c:\\some\\path', 'c:\\SOME\\', true));
assert(!isEqualOrParent('c:\\some\\path', 'd:\\', true));
assert(!isEqualOrParent('c:\\some\\path', 'd:\\some\\path', true));
assert(!isEqualOrParent('c:\\foo\\bar\\test.ts', 'c:\\foo\\barr', true));
assert(!isEqualOrParent('c:\\foo\\bar\\test.ts', 'c:\\foo\\bar\\test', true));
assert(!isEqualOrParent('c:\\foo\\bar\\test.ts', 'c:\\foo\\bar\\test.', true));
assert(!isEqualOrParent('c:\\foo\\bar\\test.ts', 'c:\\foo\\BAR\\test.', true));
}
if (isMacintosh || isLinux) {
assert(isEqualOrParent('/some/path', '/', true));
assert(isEqualOrParent('/some/path', '/some', true));
assert(isEqualOrParent('/some/path', '/some/', true));
assert(isEqualOrParent('/someöäü/path', '/someöäü', true));
assert(isEqualOrParent('/someöäü/path', '/someöäü/', true));
assert(isEqualOrParent('/foo/bar/test.ts', '/foo/bar', true));
assert(isEqualOrParent('/foo/bar/test.ts', '/foo/bar/', true));
assert(isEqualOrParent('/some/path', '/some/path', true));
assert(isEqualOrParent('/some/path', '/SOME', true));
assert(isEqualOrParent('/some/path', '/SOME/', true));
assert(isEqualOrParent('/someöäü/path', '/SOMEÖÄÜ', true));
assert(isEqualOrParent('/someöäü/path', '/SOMEÖÄÜ/', true));
assert(!isEqualOrParent('/foo/bar/test.ts', '/foo/barr', true));
assert(!isEqualOrParent('/foo/bar/test.ts', '/foo/bar/test', true));
assert(!isEqualOrParent('foo/bar/test.ts', 'foo/bar/test.', true));
assert(!isEqualOrParent('foo/bar/test.ts', 'foo/BAR/test.', true));
}
});
});

View File

@@ -0,0 +1,41 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { URI } from 'vs/base/common/uri';
import { FileSystemProviderCapabilities, IFileSystemProvider, IWatchOptions, IStat, FileType, FileDeleteOptions, FileOverwriteOptions, FileWriteOptions, FileOpenOptions, IFileChange } from 'vs/platform/files/common/files';
import { IDisposable, Disposable } from 'vs/base/common/lifecycle';
import { Emitter, Event } from 'vs/base/common/event';
export class NullFileSystemProvider implements IFileSystemProvider {
capabilities: FileSystemProviderCapabilities = FileSystemProviderCapabilities.Readonly;
private readonly _onDidChangeCapabilities = new Emitter<void>();
readonly onDidChangeCapabilities: Event<void> = this._onDidChangeCapabilities.event;
setCapabilities(capabilities: FileSystemProviderCapabilities): void {
this.capabilities = capabilities;
this._onDidChangeCapabilities.fire();
}
readonly onDidChangeFile: Event<readonly IFileChange[]> = Event.None;
constructor(private disposableFactory: () => IDisposable = () => Disposable.None) { }
watch(resource: URI, opts: IWatchOptions): IDisposable { return this.disposableFactory(); }
async stat(resource: URI): Promise<IStat> { return undefined!; }
async mkdir(resource: URI): Promise<void> { return undefined; }
async readdir(resource: URI): Promise<[string, FileType][]> { return undefined!; }
async delete(resource: URI, opts: FileDeleteOptions): Promise<void> { return undefined; }
async rename(from: URI, to: URI, opts: FileOverwriteOptions): Promise<void> { return undefined; }
async copy?(from: URI, to: URI, opts: FileOverwriteOptions): Promise<void> { return undefined; }
async readFile?(resource: URI): Promise<Uint8Array> { return undefined!; }
async writeFile?(resource: URI, content: Uint8Array, opts: FileWriteOptions): Promise<void> { return undefined; }
async open?(resource: URI, opts: FileOpenOptions): Promise<number> { return undefined!; }
async close?(fd: number): Promise<void> { return undefined; }
async read?(fd: number, pos: number, data: Uint8Array, offset: number, length: number): Promise<number> { return undefined!; }
async write?(fd: number, pos: number, data: Uint8Array, offset: number, length: number): Promise<number> { return undefined!; }
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,23 @@
'use strict';
/// <reference path="employee.ts" />
var Workforce;
(function (Workforce_1) {
var Company = (function () {
function Company() {
}
return Company;
})();
(function (property, Workforce, IEmployee) {
if (property === undefined) { property = employees; }
if (IEmployee === undefined) { IEmployee = []; }
property;
calculateMonthlyExpenses();
{
var result = 0;
for (var i = 0; i < employees.length; i++) {
result += employees[i].calculatePay();
}
return result;
}
});
})(Workforce || (Workforce = {}));

View File

@@ -0,0 +1,117 @@
'use strict';
var Conway;
(function (Conway) {
var Cell = (function () {
function Cell() {
}
return Cell;
})();
(function (property, number, property, number, property, boolean) {
if (property === undefined) { property = row; }
if (property === undefined) { property = col; }
if (property === undefined) { property = live; }
});
var GameOfLife = (function () {
function GameOfLife() {
}
return GameOfLife;
})();
(function () {
property;
gridSize = 50;
property;
canvasSize = 600;
property;
lineColor = '#cdcdcd';
property;
liveColor = '#666';
property;
deadColor = '#eee';
property;
initialLifeProbability = 0.5;
property;
animationRate = 60;
property;
cellSize = 0;
property;
context: ICanvasRenderingContext2D;
property;
world = createWorld();
circleOfLife();
function createWorld() {
return travelWorld(function (cell) {
cell.live = Math.random() < initialLifeProbability;
return cell;
});
}
function circleOfLife() {
world = travelWorld(function (cell) {
cell = world[cell.row][cell.col];
draw(cell);
return resolveNextGeneration(cell);
});
setTimeout(function () { circleOfLife(); }, animationRate);
}
function resolveNextGeneration(cell) {
var count = countNeighbors(cell);
var newCell = new Cell(cell.row, cell.col, cell.live);
if (count < 2 || count > 3)
newCell.live = false;
else if (count == 3)
newCell.live = true;
return newCell;
}
function countNeighbors(cell) {
var neighbors = 0;
for (var row = -1; row <= 1; row++) {
for (var col = -1; col <= 1; col++) {
if (row == 0 && col == 0)
continue;
if (isAlive(cell.row + row, cell.col + col)) {
neighbors++;
}
}
}
return neighbors;
}
function isAlive(row, col) {
// todo - need to guard with worl[row] exists?
if (row < 0 || col < 0 || row >= gridSize || col >= gridSize)
return false;
return world[row][col].live;
}
function travelWorld(callback) {
var result = [];
for (var row = 0; row < gridSize; row++) {
var rowData = [];
for (var col = 0; col < gridSize; col++) {
rowData.push(callback(new Cell(row, col, false)));
}
result.push(rowData);
}
return result;
}
function draw(cell) {
if (context == null)
context = createDrawingContext();
if (cellSize == 0)
cellSize = canvasSize / gridSize;
context.strokeStyle = lineColor;
context.strokeRect(cell.row * cellSize, cell.col * cellSize, cellSize, cellSize);
context.fillStyle = cell.live ? liveColor : deadColor;
context.fillRect(cell.row * cellSize, cell.col * cellSize, cellSize, cellSize);
}
function createDrawingContext() {
var canvas = document.getElementById('conway-canvas');
if (canvas == null) {
canvas = document.createElement('canvas');
canvas.id = "conway-canvas";
canvas.width = canvasSize;
canvas.height = canvasSize;
document.body.appendChild(canvas);
}
return canvas.getContext('2d');
}
});
})(Conway || (Conway = {}));
var game = new Conway.GameOfLife();

View File

@@ -0,0 +1,38 @@
'use strict';
var Workforce;
(function (Workforce) {
var Employee = (function () {
function Employee() {
}
return Employee;
})();
(property);
name: string, property;
basepay: number;
implements;
IEmployee;
{
name;
basepay;
}
var SalesEmployee = (function () {
function SalesEmployee() {
}
return SalesEmployee;
})();
();
Employee(name, basepay);
{
function calculatePay() {
var multiplier = (document.getElementById("mult")), as = any, value;
return _super.calculatePay.call(this) * multiplier + bonus;
}
}
var employee = new Employee('Bob', 1000);
var salesEmployee = new SalesEmployee('Jim', 800, 400);
salesEmployee.calclatePay(); // error: No member 'calclatePay' on SalesEmployee
})(Workforce || (Workforce = {}));
extern;
var $;
var s = Workforce.salesEmployee.calculatePay();
$('#results').text(s);

View File

@@ -0,0 +1,24 @@
'use strict';
var M;
(function (M) {
var C = (function () {
function C() {
}
return C;
})();
(function (x, property, number) {
if (property === undefined) { property = w; }
var local = 1;
// unresolved symbol because x is local
//self.x++;
self.w--; // ok because w is a property
property;
f = function (y) {
return y + x + local + w + self.w;
};
function sum(z) {
return z + f(z) + w + self.w;
}
});
})(M || (M = {}));
var c = new M.C(12, 5);

View File

@@ -0,0 +1,121 @@
<!DOCTYPE html>
<html>
<head id='headID'>
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<title>Strada </title>
<link href="site.css" rel="stylesheet" type="text/css" />
<script src="jquery-1.4.1.js"></script>
<script src="../compiler/dtree.js" type="text/javascript"></script>
<script src="../compiler/typescript.js" type="text/javascript"></script>
<script type="text/javascript">
// Compile strada source into resulting javascript
function compile(prog, libText) {
var outfile = {
source: "",
Write: function (s) { this.source += s; },
WriteLine: function (s) { this.source += s + "\r"; },
}
var parseErrors = []
var compiler=new Tools.TypeScriptCompiler(outfile,true);
compiler.setErrorCallback(function(start,len, message) { parseErrors.push({start:start, len:len, message:message}); });
compiler.addUnit(libText,"lib.ts");
compiler.addUnit(prog,"input.ts");
compiler.typeCheck();
compiler.emit();
if(parseErrors.length > 0 ) {
//throw new Error(parseErrors);
}
while(outfile.source[0] == '/' && outfile.source[1] == '/' && outfile.source[2] == ' ') {
outfile.source = outfile.source.slice(outfile.source.indexOf('\r')+1);
}
var errorPrefix = "";
for(var i = 0;i<parseErrors.length;i++) {
errorPrefix += "// Error: (" + parseErrors[i].start + "," + parseErrors[i].len + ") " + parseErrors[i].message + "\r";
}
return errorPrefix + outfile.source;
}
</script>
<script type="text/javascript">
var libText = "";
$.get("../compiler/lib.ts", function(newLibText) {
libText = newLibText;
});
// execute the javascript in the compiledOutput pane
function execute() {
$('#compilation').text("Running...");
var txt = $('#compiledOutput').val();
var res;
try {
var ret = eval(txt);
res = "Ran successfully!";
} catch(e) {
res = "Exception thrown: " + e;
}
$('#compilation').text(String(res));
}
// recompile the stradaSrc and populate the compiledOutput pane
function srcUpdated() {
var newText = $('#stradaSrc').val();
var compiledSource;
try {
compiledSource = compile(newText, libText);
} catch (e) {
compiledSource = "//Parse error"
for(var i in e)
compiledSource += "\r// " + e[i];
}
$('#compiledOutput').val(compiledSource);
}
// Populate the stradaSrc pane with one of the built in samples
function exampleSelectionChanged() {
var examples = document.getElementById('examples');
var selectedExample = examples.options[examples.selectedIndex].value;
if (selectedExample != "") {
$.get('examples/' + selectedExample, function (srcText) {
$('#stradaSrc').val(srcText);
setTimeout(srcUpdated,100);
}, function (err) {
console.log(err);
});
}
}
</script>
</head>
<body>
<h1>TypeScript</h1>
<br />
<select id="examples" onchange='exampleSelectionChanged()'>
<option value="">Select...</option>
<option value="small.ts">Small</option>
<option value="employee.ts">Employees</option>
<option value="conway.ts">Conway Game of Life</option>
<option value="typescript.ts">TypeScript Compiler</option>
</select>
<div>
<textarea id='stradaSrc' rows='40' cols='80' onchange='srcUpdated()' onkeyup='srcUpdated()' spellcheck="false">
//Type your TypeScript here...
</textarea>
<textarea id='compiledOutput' rows='40' cols='80' spellcheck="false">
//Compiled code will show up here...
</textarea>
<br />
<button onclick='execute()'/>Run</button>
<div id='compilation'>Press 'run' to execute code...</div>
<div id='results'>...write your results into #results...</div>
</div>
<div id='bod' style='display:none'></div>
</body>
</html>

View File

@@ -0,0 +1,23 @@
'use strict';
/// <reference path="employee.ts" />
var Workforce;
(function (Workforce_1) {
var Company = (function () {
function Company() {
}
return Company;
})();
(function (property, Workforce, IEmployee) {
if (property === undefined) { property = employees; }
if (IEmployee === undefined) { IEmployee = []; }
property;
calculateMonthlyExpenses();
{
var result = 0;
for (var i = 0; i < employees.length; i++) {
result += employees[i].calculatePay();
}
return result;
}
});
})(Workforce || (Workforce = {}));

View File

@@ -0,0 +1,117 @@
'use strict';
var Conway;
(function (Conway) {
var Cell = (function () {
function Cell() {
}
return Cell;
})();
(function (property, number, property, number, property, boolean) {
if (property === undefined) { property = row; }
if (property === undefined) { property = col; }
if (property === undefined) { property = live; }
});
var GameOfLife = (function () {
function GameOfLife() {
}
return GameOfLife;
})();
(function () {
property;
gridSize = 50;
property;
canvasSize = 600;
property;
lineColor = '#cdcdcd';
property;
liveColor = '#666';
property;
deadColor = '#eee';
property;
initialLifeProbability = 0.5;
property;
animationRate = 60;
property;
cellSize = 0;
property;
context: ICanvasRenderingContext2D;
property;
world = createWorld();
circleOfLife();
function createWorld() {
return travelWorld(function (cell) {
cell.live = Math.random() < initialLifeProbability;
return cell;
});
}
function circleOfLife() {
world = travelWorld(function (cell) {
cell = world[cell.row][cell.col];
draw(cell);
return resolveNextGeneration(cell);
});
setTimeout(function () { circleOfLife(); }, animationRate);
}
function resolveNextGeneration(cell) {
var count = countNeighbors(cell);
var newCell = new Cell(cell.row, cell.col, cell.live);
if (count < 2 || count > 3)
newCell.live = false;
else if (count == 3)
newCell.live = true;
return newCell;
}
function countNeighbors(cell) {
var neighbors = 0;
for (var row = -1; row <= 1; row++) {
for (var col = -1; col <= 1; col++) {
if (row == 0 && col == 0)
continue;
if (isAlive(cell.row + row, cell.col + col)) {
neighbors++;
}
}
}
return neighbors;
}
function isAlive(row, col) {
// todo - need to guard with worl[row] exists?
if (row < 0 || col < 0 || row >= gridSize || col >= gridSize)
return false;
return world[row][col].live;
}
function travelWorld(callback) {
var result = [];
for (var row = 0; row < gridSize; row++) {
var rowData = [];
for (var col = 0; col < gridSize; col++) {
rowData.push(callback(new Cell(row, col, false)));
}
result.push(rowData);
}
return result;
}
function draw(cell) {
if (context == null)
context = createDrawingContext();
if (cellSize == 0)
cellSize = canvasSize / gridSize;
context.strokeStyle = lineColor;
context.strokeRect(cell.row * cellSize, cell.col * cellSize, cellSize, cellSize);
context.fillStyle = cell.live ? liveColor : deadColor;
context.fillRect(cell.row * cellSize, cell.col * cellSize, cellSize, cellSize);
}
function createDrawingContext() {
var canvas = document.getElementById('conway-canvas');
if (canvas == null) {
canvas = document.createElement('canvas');
canvas.id = "conway-canvas";
canvas.width = canvasSize;
canvas.height = canvasSize;
document.body.appendChild(canvas);
}
return canvas.getContext('2d');
}
});
})(Conway || (Conway = {}));
var game = new Conway.GameOfLife();

View File

@@ -0,0 +1,38 @@
'use strict';
var Workforce;
(function (Workforce) {
var Employee = (function () {
function Employee() {
}
return Employee;
})();
(property);
name: string, property;
basepay: number;
implements;
IEmployee;
{
name;
basepay;
}
var SalesEmployee = (function () {
function SalesEmployee() {
}
return SalesEmployee;
})();
();
Employee(name, basepay);
{
function calculatePay() {
var multiplier = (document.getElementById("mult")), as = any, value;
return _super.calculatePay.call(this) * multiplier + bonus;
}
}
var employee = new Employee('Bob', 1000);
var salesEmployee = new SalesEmployee('Jim', 800, 400);
salesEmployee.calclatePay(); // error: No member 'calclatePay' on SalesEmployee
})(Workforce || (Workforce = {}));
extern;
var $;
var s = Workforce.salesEmployee.calculatePay();
$('#results').text(s);

View File

@@ -0,0 +1,24 @@
'use strict';
var M;
(function (M) {
var C = (function () {
function C() {
}
return C;
})();
(function (x, property, number) {
if (property === undefined) { property = w; }
var local = 1;
// unresolved symbol because x is local
//self.x++;
self.w--; // ok because w is a property
property;
f = function (y) {
return y + x + local + w + self.w;
};
function sum(z) {
return z + f(z) + w + self.w;
}
});
})(M || (M = {}));
var c = new M.C(12, 5);

View File

@@ -0,0 +1,40 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
/*----------------------------------------------------------
The base color for this template is #5c87b2. If you'd like
to use a different color start by replacing all instances of
#5c87b2 with your new color.
----------------------------------------------------------*/
body
{
background-color: #5c87b2;
font-size: .75em;
font-family: Segoe UI, Verdana, Helvetica, Sans-Serif;
margin: 8px;
padding: 0;
color: #696969;
}
h1, h2, h3, h4, h5, h6
{
color: #000;
font-size: 40px;
margin: 0px;
}
textarea
{
font-family: Consolas
}
#results
{
margin-top: 2em;
margin-left: 2em;
color: black;
font-size: medium;
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 274 B

View File

@@ -0,0 +1,23 @@
'use strict';
/// <reference path="employee.ts" />
var Workforce;
(function (Workforce_1) {
var Company = (function () {
function Company() {
}
return Company;
})();
(function (property, Workforce, IEmployee) {
if (property === undefined) { property = employees; }
if (IEmployee === undefined) { IEmployee = []; }
property;
calculateMonthlyExpenses();
{
var result = 0;
for (var i = 0; i < employees.length; i++) {
result += employees[i].calculatePay();
}
return result;
}
});
})(Workforce || (Workforce = {}));

View File

@@ -0,0 +1,117 @@
'use strict';
var Conway;
(function (Conway) {
var Cell = (function () {
function Cell() {
}
return Cell;
})();
(function (property, number, property, number, property, boolean) {
if (property === undefined) { property = row; }
if (property === undefined) { property = col; }
if (property === undefined) { property = live; }
});
var GameOfLife = (function () {
function GameOfLife() {
}
return GameOfLife;
})();
(function () {
property;
gridSize = 50;
property;
canvasSize = 600;
property;
lineColor = '#cdcdcd';
property;
liveColor = '#666';
property;
deadColor = '#eee';
property;
initialLifeProbability = 0.5;
property;
animationRate = 60;
property;
cellSize = 0;
property;
context: ICanvasRenderingContext2D;
property;
world = createWorld();
circleOfLife();
function createWorld() {
return travelWorld(function (cell) {
cell.live = Math.random() < initialLifeProbability;
return cell;
});
}
function circleOfLife() {
world = travelWorld(function (cell) {
cell = world[cell.row][cell.col];
draw(cell);
return resolveNextGeneration(cell);
});
setTimeout(function () { circleOfLife(); }, animationRate);
}
function resolveNextGeneration(cell) {
var count = countNeighbors(cell);
var newCell = new Cell(cell.row, cell.col, cell.live);
if (count < 2 || count > 3)
newCell.live = false;
else if (count == 3)
newCell.live = true;
return newCell;
}
function countNeighbors(cell) {
var neighbors = 0;
for (var row = -1; row <= 1; row++) {
for (var col = -1; col <= 1; col++) {
if (row == 0 && col == 0)
continue;
if (isAlive(cell.row + row, cell.col + col)) {
neighbors++;
}
}
}
return neighbors;
}
function isAlive(row, col) {
// todo - need to guard with worl[row] exists?
if (row < 0 || col < 0 || row >= gridSize || col >= gridSize)
return false;
return world[row][col].live;
}
function travelWorld(callback) {
var result = [];
for (var row = 0; row < gridSize; row++) {
var rowData = [];
for (var col = 0; col < gridSize; col++) {
rowData.push(callback(new Cell(row, col, false)));
}
result.push(rowData);
}
return result;
}
function draw(cell) {
if (context == null)
context = createDrawingContext();
if (cellSize == 0)
cellSize = canvasSize / gridSize;
context.strokeStyle = lineColor;
context.strokeRect(cell.row * cellSize, cell.col * cellSize, cellSize, cellSize);
context.fillStyle = cell.live ? liveColor : deadColor;
context.fillRect(cell.row * cellSize, cell.col * cellSize, cellSize, cellSize);
}
function createDrawingContext() {
var canvas = document.getElementById('conway-canvas');
if (canvas == null) {
canvas = document.createElement('canvas');
canvas.id = "conway-canvas";
canvas.width = canvasSize;
canvas.height = canvasSize;
document.body.appendChild(canvas);
}
return canvas.getContext('2d');
}
});
})(Conway || (Conway = {}));
var game = new Conway.GameOfLife();

View File

@@ -0,0 +1,38 @@
'use strict';
var Workforce;
(function (Workforce) {
var Employee = (function () {
function Employee() {
}
return Employee;
})();
(property);
name: string, property;
basepay: number;
implements;
IEmployee;
{
name;
basepay;
}
var SalesEmployee = (function () {
function SalesEmployee() {
}
return SalesEmployee;
})();
();
Employee(name, basepay);
{
function calculatePay() {
var multiplier = (document.getElementById("mult")), as = any, value;
return _super.calculatePay.call(this) * multiplier + bonus;
}
}
var employee = new Employee('Bob', 1000);
var salesEmployee = new SalesEmployee('Jim', 800, 400);
salesEmployee.calclatePay(); // error: No member 'calclatePay' on SalesEmployee
})(Workforce || (Workforce = {}));
extern;
var $;
var s = Workforce.salesEmployee.calculatePay();
$('#results').text(s);

View File

@@ -0,0 +1,24 @@
'use strict';
var M;
(function (M) {
var C = (function () {
function C() {
}
return C;
})();
(function (x, property, number) {
if (property === undefined) { property = w; }
var local = 1;
// unresolved symbol because x is local
//self.x++;
self.w--; // ok because w is a property
property;
f = function (y) {
return y + x + local + w + self.w;
};
function sum(z) {
return z + f(z) + w + self.w;
}
});
})(M || (M = {}));
var c = new M.C(12, 5);

View File

@@ -0,0 +1,121 @@
<!DOCTYPE html>
<html>
<head id='headID'>
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<title>Strada </title>
<link href="site.css" rel="stylesheet" type="text/css" />
<script src="jquery-1.4.1.js"></script>
<script src="../compiler/dtree.js" type="text/javascript"></script>
<script src="../compiler/typescript.js" type="text/javascript"></script>
<script type="text/javascript">
// Compile strada source into resulting javascript
function compile(prog, libText) {
var outfile = {
source: "",
Write: function (s) { this.source += s; },
WriteLine: function (s) { this.source += s + "\r"; },
}
var parseErrors = []
var compiler=new Tools.TypeScriptCompiler(outfile,true);
compiler.setErrorCallback(function(start,len, message) { parseErrors.push({start:start, len:len, message:message}); });
compiler.addUnit(libText,"lib.ts");
compiler.addUnit(prog,"input.ts");
compiler.typeCheck();
compiler.emit();
if(parseErrors.length > 0 ) {
//throw new Error(parseErrors);
}
while(outfile.source[0] == '/' && outfile.source[1] == '/' && outfile.source[2] == ' ') {
outfile.source = outfile.source.slice(outfile.source.indexOf('\r')+1);
}
var errorPrefix = "";
for(var i = 0;i<parseErrors.length;i++) {
errorPrefix += "// Error: (" + parseErrors[i].start + "," + parseErrors[i].len + ") " + parseErrors[i].message + "\r";
}
return errorPrefix + outfile.source;
}
</script>
<script type="text/javascript">
var libText = "";
$.get("../compiler/lib.ts", function(newLibText) {
libText = newLibText;
});
// execute the javascript in the compiledOutput pane
function execute() {
$('#compilation').text("Running...");
var txt = $('#compiledOutput').val();
var res;
try {
var ret = eval(txt);
res = "Ran successfully!";
} catch(e) {
res = "Exception thrown: " + e;
}
$('#compilation').text(String(res));
}
// recompile the stradaSrc and populate the compiledOutput pane
function srcUpdated() {
var newText = $('#stradaSrc').val();
var compiledSource;
try {
compiledSource = compile(newText, libText);
} catch (e) {
compiledSource = "//Parse error"
for(var i in e)
compiledSource += "\r// " + e[i];
}
$('#compiledOutput').val(compiledSource);
}
// Populate the stradaSrc pane with one of the built in samples
function exampleSelectionChanged() {
var examples = document.getElementById('examples');
var selectedExample = examples.options[examples.selectedIndex].value;
if (selectedExample != "") {
$.get('examples/' + selectedExample, function (srcText) {
$('#stradaSrc').val(srcText);
setTimeout(srcUpdated,100);
}, function (err) {
console.log(err);
});
}
}
</script>
</head>
<body>
<h1>TypeScript</h1>
<br />
<select id="examples" onchange='exampleSelectionChanged()'>
<option value="">Select...</option>
<option value="small.ts">Small</option>
<option value="employee.ts">Employees</option>
<option value="conway.ts">Conway Game of Life</option>
<option value="typescript.ts">TypeScript Compiler</option>
</select>
<div>
<textarea id='stradaSrc' rows='40' cols='80' onchange='srcUpdated()' onkeyup='srcUpdated()' spellcheck="false">
//Type your TypeScript here...
</textarea>
<textarea id='compiledOutput' rows='40' cols='80' spellcheck="false">
//Compiled code will show up here...
</textarea>
<br />
<button onclick='execute()'/>Run</button>
<div id='compilation'>Press 'run' to execute code...</div>
<div id='results'>...write your results into #results...</div>
</div>
<div id='bod' style='display:none'></div>
</body>
</html>

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1 @@
Small File with Ümlaut

View File

@@ -0,0 +1 @@
This is some UTF 8 with BOM file.

View File

@@ -0,0 +1,227 @@
/*---------------------------------------------------------------------------------------------
* 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 platform from 'vs/base/common/platform';
import { FileChangeType, FileChangesEvent } from 'vs/platform/files/common/files';
import { URI as uri } from 'vs/base/common/uri';
import { IDiskFileChange, normalizeFileChanges, toFileChanges } from 'vs/platform/files/node/watcher/watcher';
import { Event, Emitter } from 'vs/base/common/event';
function toFileChangesEvent(changes: IDiskFileChange[]): FileChangesEvent {
return new FileChangesEvent(toFileChanges(changes), !platform.isLinux);
}
class TestFileWatcher {
private readonly _onDidFilesChange: Emitter<FileChangesEvent>;
constructor() {
this._onDidFilesChange = new Emitter<FileChangesEvent>();
}
get onDidFilesChange(): Event<FileChangesEvent> {
return this._onDidFilesChange.event;
}
report(changes: IDiskFileChange[]): void {
this.onRawFileEvents(changes);
}
private onRawFileEvents(events: IDiskFileChange[]): void {
// Normalize
let normalizedEvents = normalizeFileChanges(events);
// Emit through event emitter
if (normalizedEvents.length > 0) {
this._onDidFilesChange.fire(toFileChangesEvent(normalizedEvents));
}
}
}
enum Path {
UNIX,
WINDOWS,
UNC
}
suite('Normalizer', () => {
test('simple add/update/delete', function (done: () => void) {
const watch = new TestFileWatcher();
const added = uri.file('/users/data/src/added.txt');
const updated = uri.file('/users/data/src/updated.txt');
const deleted = uri.file('/users/data/src/deleted.txt');
const raw: IDiskFileChange[] = [
{ path: added.fsPath, type: FileChangeType.ADDED },
{ path: updated.fsPath, type: FileChangeType.UPDATED },
{ path: deleted.fsPath, type: FileChangeType.DELETED },
];
watch.onDidFilesChange(e => {
assert.ok(e);
assert.equal(e.changes.length, 3);
assert.ok(e.contains(added, FileChangeType.ADDED));
assert.ok(e.contains(updated, FileChangeType.UPDATED));
assert.ok(e.contains(deleted, FileChangeType.DELETED));
done();
});
watch.report(raw);
});
let pathSpecs = platform.isWindows ? [Path.WINDOWS, Path.UNC] : [Path.UNIX];
pathSpecs.forEach((p) => {
test('delete only reported for top level folder (' + p + ')', function (done: () => void) {
const watch = new TestFileWatcher();
const deletedFolderA = uri.file(p === Path.UNIX ? '/users/data/src/todelete1' : p === Path.WINDOWS ? 'C:\\users\\data\\src\\todelete1' : '\\\\localhost\\users\\data\\src\\todelete1');
const deletedFolderB = uri.file(p === Path.UNIX ? '/users/data/src/todelete2' : p === Path.WINDOWS ? 'C:\\users\\data\\src\\todelete2' : '\\\\localhost\\users\\data\\src\\todelete2');
const deletedFolderBF1 = uri.file(p === Path.UNIX ? '/users/data/src/todelete2/file.txt' : p === Path.WINDOWS ? 'C:\\users\\data\\src\\todelete2\\file.txt' : '\\\\localhost\\users\\data\\src\\todelete2\\file.txt');
const deletedFolderBF2 = uri.file(p === Path.UNIX ? '/users/data/src/todelete2/more/test.txt' : p === Path.WINDOWS ? 'C:\\users\\data\\src\\todelete2\\more\\test.txt' : '\\\\localhost\\users\\data\\src\\todelete2\\more\\test.txt');
const deletedFolderBF3 = uri.file(p === Path.UNIX ? '/users/data/src/todelete2/super/bar/foo.txt' : p === Path.WINDOWS ? 'C:\\users\\data\\src\\todelete2\\super\\bar\\foo.txt' : '\\\\localhost\\users\\data\\src\\todelete2\\super\\bar\\foo.txt');
const deletedFileA = uri.file(p === Path.UNIX ? '/users/data/src/deleteme.txt' : p === Path.WINDOWS ? 'C:\\users\\data\\src\\deleteme.txt' : '\\\\localhost\\users\\data\\src\\deleteme.txt');
const addedFile = uri.file(p === Path.UNIX ? '/users/data/src/added.txt' : p === Path.WINDOWS ? 'C:\\users\\data\\src\\added.txt' : '\\\\localhost\\users\\data\\src\\added.txt');
const updatedFile = uri.file(p === Path.UNIX ? '/users/data/src/updated.txt' : p === Path.WINDOWS ? 'C:\\users\\data\\src\\updated.txt' : '\\\\localhost\\users\\data\\src\\updated.txt');
const raw: IDiskFileChange[] = [
{ path: deletedFolderA.fsPath, type: FileChangeType.DELETED },
{ path: deletedFolderB.fsPath, type: FileChangeType.DELETED },
{ path: deletedFolderBF1.fsPath, type: FileChangeType.DELETED },
{ path: deletedFolderBF2.fsPath, type: FileChangeType.DELETED },
{ path: deletedFolderBF3.fsPath, type: FileChangeType.DELETED },
{ path: deletedFileA.fsPath, type: FileChangeType.DELETED },
{ path: addedFile.fsPath, type: FileChangeType.ADDED },
{ path: updatedFile.fsPath, type: FileChangeType.UPDATED }
];
watch.onDidFilesChange(e => {
assert.ok(e);
assert.equal(e.changes.length, 5);
assert.ok(e.contains(deletedFolderA, FileChangeType.DELETED));
assert.ok(e.contains(deletedFolderB, FileChangeType.DELETED));
assert.ok(e.contains(deletedFileA, FileChangeType.DELETED));
assert.ok(e.contains(addedFile, FileChangeType.ADDED));
assert.ok(e.contains(updatedFile, FileChangeType.UPDATED));
done();
});
watch.report(raw);
});
});
test('event normalization: ignore CREATE followed by DELETE', function (done: () => void) {
const watch = new TestFileWatcher();
const created = uri.file('/users/data/src/related');
const deleted = uri.file('/users/data/src/related');
const unrelated = uri.file('/users/data/src/unrelated');
const raw: IDiskFileChange[] = [
{ path: created.fsPath, type: FileChangeType.ADDED },
{ path: deleted.fsPath, type: FileChangeType.DELETED },
{ path: unrelated.fsPath, type: FileChangeType.UPDATED },
];
watch.onDidFilesChange(e => {
assert.ok(e);
assert.equal(e.changes.length, 1);
assert.ok(e.contains(unrelated, FileChangeType.UPDATED));
done();
});
watch.report(raw);
});
test('event normalization: flatten DELETE followed by CREATE into CHANGE', function (done: () => void) {
const watch = new TestFileWatcher();
const deleted = uri.file('/users/data/src/related');
const created = uri.file('/users/data/src/related');
const unrelated = uri.file('/users/data/src/unrelated');
const raw: IDiskFileChange[] = [
{ path: deleted.fsPath, type: FileChangeType.DELETED },
{ path: created.fsPath, type: FileChangeType.ADDED },
{ path: unrelated.fsPath, type: FileChangeType.UPDATED },
];
watch.onDidFilesChange(e => {
assert.ok(e);
assert.equal(e.changes.length, 2);
assert.ok(e.contains(deleted, FileChangeType.UPDATED));
assert.ok(e.contains(unrelated, FileChangeType.UPDATED));
done();
});
watch.report(raw);
});
test('event normalization: ignore UPDATE when CREATE received', function (done: () => void) {
const watch = new TestFileWatcher();
const created = uri.file('/users/data/src/related');
const updated = uri.file('/users/data/src/related');
const unrelated = uri.file('/users/data/src/unrelated');
const raw: IDiskFileChange[] = [
{ path: created.fsPath, type: FileChangeType.ADDED },
{ path: updated.fsPath, type: FileChangeType.UPDATED },
{ path: unrelated.fsPath, type: FileChangeType.UPDATED },
];
watch.onDidFilesChange(e => {
assert.ok(e);
assert.equal(e.changes.length, 2);
assert.ok(e.contains(created, FileChangeType.ADDED));
assert.ok(!e.contains(created, FileChangeType.UPDATED));
assert.ok(e.contains(unrelated, FileChangeType.UPDATED));
done();
});
watch.report(raw);
});
test('event normalization: apply DELETE', function (done: () => void) {
const watch = new TestFileWatcher();
const updated = uri.file('/users/data/src/related');
const updated2 = uri.file('/users/data/src/related');
const deleted = uri.file('/users/data/src/related');
const unrelated = uri.file('/users/data/src/unrelated');
const raw: IDiskFileChange[] = [
{ path: updated.fsPath, type: FileChangeType.UPDATED },
{ path: updated2.fsPath, type: FileChangeType.UPDATED },
{ path: unrelated.fsPath, type: FileChangeType.UPDATED },
{ path: updated.fsPath, type: FileChangeType.DELETED }
];
watch.onDidFilesChange(e => {
assert.ok(e);
assert.equal(e.changes.length, 2);
assert.ok(e.contains(deleted, FileChangeType.DELETED));
assert.ok(!e.contains(updated, FileChangeType.UPDATED));
assert.ok(e.contains(unrelated, FileChangeType.UPDATED));
done();
});
watch.report(raw);
});
});