diff --git a/src/compiler/commandLineParser.ts b/src/compiler/commandLineParser.ts index b0370620be47b..76a1d0b9db17a 100644 --- a/src/compiler/commandLineParser.ts +++ b/src/compiler/commandLineParser.ts @@ -2029,7 +2029,7 @@ namespace ts { export function getFileNamesFromConfigSpecs(spec: ConfigFileSpecs, basePath: string, options: CompilerOptions, host: ParseConfigHost, extraFileExtensions: ReadonlyArray = []): ExpandResult { basePath = normalizePath(basePath); - const keyMapper = host.useCaseSensitiveFileNames ? caseSensitiveKeyMapper : caseInsensitiveKeyMapper; + const keyMapper = host.useCaseSensitiveFileNames ? identity : toLowerCase; // Literal file names (provided via the "files" array in tsconfig.json) are stored in a // file map with a possibly case insensitive key. We use this map later when when including @@ -2233,24 +2233,6 @@ namespace ts { } } - /** - * Gets a case sensitive key. - * - * @param key The original key. - */ - function caseSensitiveKeyMapper(key: string) { - return key; - } - - /** - * Gets a case insensitive key. - * - * @param key The original key. - */ - function caseInsensitiveKeyMapper(key: string) { - return key.toLowerCase(); - } - /** * Produces a cleaned version of compiler options with personally identifiying info (aka, paths) removed. * Also converts enum values back to strings. diff --git a/src/compiler/core.ts b/src/compiler/core.ts index c3bfe55b40b78..130158d958101 100644 --- a/src/compiler/core.ts +++ b/src/compiler/core.ts @@ -25,6 +25,10 @@ namespace ts { /* @internal */ namespace ts { export const emptyArray: never[] = [] as never[]; + export function closeFileWatcher(watcher: FileWatcher) { + watcher.close(); + } + /** Create a MapLike with good performance. */ function createDictionaryObject(): MapLike { const map = Object.create(/*prototype*/ null); // tslint:disable-line:no-null-keyword @@ -3143,4 +3147,36 @@ namespace ts { export function singleElementArray(t: T | undefined): T[] | undefined { return t === undefined ? undefined : [t]; } + + export function enumerateInsertsAndDeletes(newItems: ReadonlyArray, oldItems: ReadonlyArray, comparer: (a: T, b: U) => Comparison, inserted: (newItem: T) => void, deleted: (oldItem: U) => void, unchanged?: (oldItem: U, newItem: T) => void) { + unchanged = unchanged || noop; + let newIndex = 0; + let oldIndex = 0; + const newLen = newItems.length; + const oldLen = oldItems.length; + while (newIndex < newLen && oldIndex < oldLen) { + const newItem = newItems[newIndex]; + const oldItem = oldItems[oldIndex]; + const compareResult = comparer(newItem, oldItem); + if (compareResult === Comparison.LessThan) { + inserted(newItem); + newIndex++; + } + else if (compareResult === Comparison.GreaterThan) { + deleted(oldItem); + oldIndex++; + } + else { + unchanged(oldItem, newItem); + newIndex++; + oldIndex++; + } + } + while (newIndex < newLen) { + inserted(newItems[newIndex++]); + } + while (oldIndex < oldLen) { + deleted(oldItems[oldIndex++]); + } + } } diff --git a/src/compiler/sys.ts b/src/compiler/sys.ts index c0159797efe07..4f62cfdfe2d91 100644 --- a/src/compiler/sys.ts +++ b/src/compiler/sys.ts @@ -24,10 +24,399 @@ namespace ts { export type FileWatcherCallback = (fileName: string, eventKind: FileWatcherEventKind) => void; export type DirectoryWatcherCallback = (fileName: string) => void; + /*@internal*/ export interface WatchedFile { - fileName: string; - callback: FileWatcherCallback; - mtime?: Date; + readonly fileName: string; + readonly callback: FileWatcherCallback; + mtime: Date; + } + + /* @internal */ + export enum PollingInterval { + High = 2000, + Medium = 500, + Low = 250 + } + + function getPriorityValues(highPriorityValue: number): [number, number, number] { + const mediumPriorityValue = highPriorityValue * 2; + const lowPriorityValue = mediumPriorityValue * 4; + return [highPriorityValue, mediumPriorityValue, lowPriorityValue]; + } + + function pollingInterval(watchPriority: PollingInterval): number { + return pollingIntervalsForPriority[watchPriority]; + } + + const pollingIntervalsForPriority = getPriorityValues(250); + + /* @internal */ + export function watchFileUsingPriorityPollingInterval(host: System, fileName: string, callback: FileWatcherCallback, watchPriority: PollingInterval): FileWatcher { + return host.watchFile(fileName, callback, pollingInterval(watchPriority)); + } + + /* @internal */ + export type HostWatchFile = (fileName: string, callback: FileWatcherCallback, pollingInterval: PollingInterval) => FileWatcher; + /* @internal */ + export type HostWatchDirectory = (fileName: string, callback: DirectoryWatcherCallback, recursive?: boolean) => FileWatcher; + + /* @internal */ + export const missingFileModifiedTime = new Date(0); // Any subsequent modification will occur after this time + + interface Levels { + Low: number; + Medium: number; + High: number; + } + + function createPollingIntervalBasedLevels(levels: Levels) { + return { + [PollingInterval.Low]: levels.Low, + [PollingInterval.Medium]: levels.Medium, + [PollingInterval.High]: levels.High + }; + } + + const defaultChunkLevels: Levels = { Low: 32, Medium: 64, High: 256 }; + let pollingChunkSize = createPollingIntervalBasedLevels(defaultChunkLevels); + /* @internal */ + export let unchangedPollThresholds = createPollingIntervalBasedLevels(defaultChunkLevels); + + /* @internal */ + export function setCustomPollingValues(system: System) { + if (!system.getEnvironmentVariable) { + return; + } + const pollingIntervalChanged = setCustomLevels("TSC_WATCH_POLLINGINTERVAL", PollingInterval); + pollingChunkSize = getCustomPollingBasedLevels("TSC_WATCH_POLLINGCHUNKSIZE", defaultChunkLevels) || pollingChunkSize; + unchangedPollThresholds = getCustomPollingBasedLevels("TSC_WATCH_UNCHANGEDPOLLTHRESHOLDS", defaultChunkLevels) || unchangedPollThresholds; + + function getLevel(envVar: string, level: keyof Levels) { + return system.getEnvironmentVariable(`${envVar}_${level.toUpperCase()}`); + } + + function getCustomLevels(baseVariable: string) { + let customLevels: Partial | undefined; + setCustomLevel("Low"); + setCustomLevel("Medium"); + setCustomLevel("High"); + return customLevels; + + function setCustomLevel(level: keyof Levels) { + const customLevel = getLevel(baseVariable, level); + if (customLevel) { + (customLevels || (customLevels = {}))[level] = Number(customLevel); + } + } + } + + function setCustomLevels(baseVariable: string, levels: Levels) { + const customLevels = getCustomLevels(baseVariable); + if (customLevels) { + setLevel("Low"); + setLevel("Medium"); + setLevel("High"); + return true; + } + return false; + + function setLevel(level: keyof Levels) { + levels[level] = customLevels[level] || levels[level]; + } + } + + function getCustomPollingBasedLevels(baseVariable: string, defaultLevels: Levels) { + const customLevels = getCustomLevels(baseVariable); + return (pollingIntervalChanged || customLevels) && + createPollingIntervalBasedLevels(customLevels ? { ...defaultLevels, ...customLevels } : defaultLevels); + } + } + + /* @internal */ + export function createDynamicPriorityPollingWatchFile(host: { getModifiedTime: System["getModifiedTime"]; setTimeout: System["setTimeout"]; }): HostWatchFile { + interface WatchedFile extends ts.WatchedFile { + isClosed?: boolean; + unchangedPolls: number; + } + + interface PollingIntervalQueue extends Array { + pollingInterval: PollingInterval; + pollIndex: number; + pollScheduled: boolean; + } + + const watchedFiles: WatchedFile[] = []; + const changedFilesInLastPoll: WatchedFile[] = []; + const lowPollingIntervalQueue = createPollingIntervalQueue(PollingInterval.Low); + const mediumPollingIntervalQueue = createPollingIntervalQueue(PollingInterval.Medium); + const highPollingIntervalQueue = createPollingIntervalQueue(PollingInterval.High); + return watchFile; + + function watchFile(fileName: string, callback: FileWatcherCallback, defaultPollingInterval: PollingInterval): FileWatcher { + const file: WatchedFile = { + fileName, + callback, + unchangedPolls: 0, + mtime: getModifiedTime(fileName) + }; + watchedFiles.push(file); + + addToPollingIntervalQueue(file, defaultPollingInterval); + return { + close: () => { + file.isClosed = true; + // Remove from watchedFiles + unorderedRemoveItem(watchedFiles, file); + // Do not update polling interval queue since that will happen as part of polling + } + }; + } + + function createPollingIntervalQueue(pollingInterval: PollingInterval): PollingIntervalQueue { + const queue = [] as PollingIntervalQueue; + queue.pollingInterval = pollingInterval; + queue.pollIndex = 0; + queue.pollScheduled = false; + return queue; + } + + function pollPollingIntervalQueue(queue: PollingIntervalQueue) { + queue.pollIndex = pollQueue(queue, queue.pollingInterval, queue.pollIndex, pollingChunkSize[queue.pollingInterval]); + // Set the next polling index and timeout + if (queue.length) { + scheduleNextPoll(queue.pollingInterval); + } + else { + Debug.assert(queue.pollIndex === 0); + queue.pollScheduled = false; + } + } + + function pollLowPollingIntervalQueue(queue: PollingIntervalQueue) { + // Always poll complete list of changedFilesInLastPoll + pollQueue(changedFilesInLastPoll, PollingInterval.Low, /*pollIndex*/ 0, changedFilesInLastPoll.length); + + // Finally do the actual polling of the queue + pollPollingIntervalQueue(queue); + // Schedule poll if there are files in changedFilesInLastPoll but no files in the actual queue + // as pollPollingIntervalQueue wont schedule for next poll + if (!queue.pollScheduled && changedFilesInLastPoll.length) { + scheduleNextPoll(PollingInterval.Low); + } + } + + function pollQueue(queue: WatchedFile[], pollingInterval: PollingInterval, pollIndex: number, chunkSize: number) { + // Max visit would be all elements of the queue + let needsVisit = queue.length; + let definedValueCopyToIndex = pollIndex; + for (let polled = 0; polled < chunkSize && needsVisit > 0; nextPollIndex(), needsVisit--) { + const watchedFile = queue[pollIndex]; + if (!watchedFile) { + continue; + } + else if (watchedFile.isClosed) { + queue[pollIndex] = undefined; + continue; + } + + polled++; + const fileChanged = onWatchedFileStat(watchedFile, getModifiedTime(watchedFile.fileName)); + if (watchedFile.isClosed) { + // Closed watcher as part of callback + queue[pollIndex] = undefined; + } + else if (fileChanged) { + watchedFile.unchangedPolls = 0; + // Changed files go to changedFilesInLastPoll queue + if (queue !== changedFilesInLastPoll) { + queue[pollIndex] = undefined; + addChangedFileToLowPollingIntervalQueue(watchedFile); + } + } + else if (watchedFile.unchangedPolls !== unchangedPollThresholds[pollingInterval]) { + watchedFile.unchangedPolls++; + } + else if (queue === changedFilesInLastPoll) { + // Restart unchangedPollCount for unchanged file and move to low polling interval queue + watchedFile.unchangedPolls = 1; + queue[pollIndex] = undefined; + addToPollingIntervalQueue(watchedFile, PollingInterval.Low); + } + else if (pollingInterval !== PollingInterval.High) { + watchedFile.unchangedPolls++; + queue[pollIndex] = undefined; + addToPollingIntervalQueue(watchedFile, pollingInterval === PollingInterval.Low ? PollingInterval.Medium : PollingInterval.High); + } + + if (queue[pollIndex]) { + // Copy this file to the non hole location + if (definedValueCopyToIndex < pollIndex) { + queue[definedValueCopyToIndex] = watchedFile; + queue[pollIndex] = undefined; + } + definedValueCopyToIndex++; + } + } + + // Return next poll index + return pollIndex; + + function nextPollIndex() { + pollIndex++; + if (pollIndex === queue.length) { + if (definedValueCopyToIndex < pollIndex) { + // There are holes from nextDefinedValueIndex to end of queue, change queue size + queue.length = definedValueCopyToIndex; + } + pollIndex = 0; + definedValueCopyToIndex = 0; + } + } + } + + function pollingIntervalQueue(pollingInterval: PollingInterval) { + switch (pollingInterval) { + case PollingInterval.Low: + return lowPollingIntervalQueue; + case PollingInterval.Medium: + return mediumPollingIntervalQueue; + case PollingInterval.High: + return highPollingIntervalQueue; + } + } + + function addToPollingIntervalQueue(file: WatchedFile, pollingInterval: PollingInterval) { + pollingIntervalQueue(pollingInterval).push(file); + scheduleNextPollIfNotAlreadyScheduled(pollingInterval); + } + + function addChangedFileToLowPollingIntervalQueue(file: WatchedFile) { + changedFilesInLastPoll.push(file); + scheduleNextPollIfNotAlreadyScheduled(PollingInterval.Low); + } + + function scheduleNextPollIfNotAlreadyScheduled(pollingInterval: PollingInterval) { + if (!pollingIntervalQueue(pollingInterval).pollScheduled) { + scheduleNextPoll(pollingInterval); + } + } + + function scheduleNextPoll(pollingInterval: PollingInterval) { + pollingIntervalQueue(pollingInterval).pollScheduled = host.setTimeout(pollingInterval === PollingInterval.Low ? pollLowPollingIntervalQueue : pollPollingIntervalQueue, pollingInterval, pollingIntervalQueue(pollingInterval)); + } + + function getModifiedTime(fileName: string) { + return host.getModifiedTime(fileName) || missingFileModifiedTime; + } + } + + /** + * Returns true if file status changed + */ + /*@internal*/ + export function onWatchedFileStat(watchedFile: WatchedFile, modifiedTime: Date): boolean { + const oldTime = watchedFile.mtime.getTime(); + const newTime = modifiedTime.getTime(); + if (oldTime !== newTime) { + watchedFile.mtime = modifiedTime; + const eventKind = oldTime === 0 + ? FileWatcherEventKind.Created + : newTime === 0 + ? FileWatcherEventKind.Deleted + : FileWatcherEventKind.Changed; + watchedFile.callback(watchedFile.fileName, eventKind); + return true; + } + + return false; + } + + /*@internal*/ + export interface RecursiveDirectoryWatcherHost { + watchDirectory: HostWatchDirectory; + getAccessileSortedChildDirectories(path: string): ReadonlyArray; + directoryExists(dir: string): boolean; + filePathComparer: Comparer; + } + + /** + * Watch the directory recursively using host provided method to watch child directories + * that means if this is recursive watcher, watch the children directories as well + * (eg on OS that dont support recursive watch using fs.watch use fs.watchFile) + */ + /*@internal*/ + export function createRecursiveDirectoryWatcher(host: RecursiveDirectoryWatcherHost): (directoryName: string, callback: DirectoryWatcherCallback) => FileWatcher { + type ChildWatches = ReadonlyArray; + interface DirectoryWatcher extends FileWatcher { + childWatches: ChildWatches; + dirName: string; + } + + return createDirectoryWatcher; + + /** + * Create the directory watcher for the dirPath. + */ + function createDirectoryWatcher(dirName: string, callback: DirectoryWatcherCallback): DirectoryWatcher { + const watcher = host.watchDirectory(dirName, fileName => { + // Call the actual callback + callback(fileName); + + // Iterate through existing children and update the watches if needed + updateChildWatches(result, callback); + }); + + let result: DirectoryWatcher = { + close: () => { + watcher.close(); + result.childWatches.forEach(closeFileWatcher); + result = undefined; + }, + dirName, + childWatches: emptyArray + }; + updateChildWatches(result, callback); + return result; + } + + function updateChildWatches(watcher: DirectoryWatcher, callback: DirectoryWatcherCallback) { + // Iterate through existing children and update the watches if needed + if (watcher) { + watcher.childWatches = watchChildDirectories(watcher.dirName, watcher.childWatches, callback); + } + } + + /** + * Watch the directories in the parentDir + */ + function watchChildDirectories(parentDir: string, existingChildWatches: ChildWatches, callback: DirectoryWatcherCallback): ChildWatches { + let newChildWatches: DirectoryWatcher[] | undefined; + enumerateInsertsAndDeletes( + host.directoryExists(parentDir) ? host.getAccessileSortedChildDirectories(parentDir) : emptyArray, + existingChildWatches, + (child, childWatcher) => host.filePathComparer(getNormalizedAbsolutePath(child, parentDir), childWatcher.dirName), + createAndAddChildDirectoryWatcher, + closeFileWatcher, + addChildDirectoryWatcher + ); + + return newChildWatches || emptyArray; + + /** + * Create new childDirectoryWatcher and add it to the new ChildDirectoryWatcher list + */ + function createAndAddChildDirectoryWatcher(childName: string) { + const result = createDirectoryWatcher(getNormalizedAbsolutePath(childName, parentDir), callback); + addChildDirectoryWatcher(result); + } + + /** + * Add child directory watcher to the new ChildDirectoryWatcher list + */ + function addChildDirectoryWatcher(childWatcher: DirectoryWatcher) { + (newChildWatches || (newChildWatches = [])).push(childWatcher); + } + } } export interface System { @@ -138,96 +527,96 @@ namespace ts { _crypto = undefined; } - const useNonPollingWatchers = process.env.TSC_NONPOLLING_WATCHER; + const nodeVersion = getNodeMajorVersion(); + const isNode4OrLater = nodeVersion >= 4; - /** - * djb2 hashing algorithm - * http://www.cse.yorku.ca/~oz/hash.html - */ - function generateDjb2Hash(data: string): string { - const chars = data.split("").map(str => str.charCodeAt(0)); - return `${chars.reduce((prev, curr) => ((prev << 5) + prev) + curr, 5381)}`; - } + const platform: string = _os.platform(); + const useCaseSensitiveFileNames = isFileSystemCaseSensitive(); - function createMD5HashUsingNativeCrypto(data: string) { - const hash = _crypto.createHash("md5"); - hash.update(data); - return hash.digest("hex"); + const enum FileSystemEntryKind { + File, + Directory } - function createWatchedFileSet() { - const dirWatchers = createMap(); - // One file can have multiple watchers - const fileWatcherCallbacks = createMultiMap(); - return { addFile, removeFile }; - - function reduceDirWatcherRefCountForFile(fileName: string) { - const dirName = getDirectoryPath(fileName); - const watcher = dirWatchers.get(dirName); - if (watcher) { - watcher.referenceCount -= 1; - if (watcher.referenceCount <= 0) { - watcher.close(); - dirWatchers.delete(dirName); - } + const useNonPollingWatchers = process.env.TSC_NONPOLLING_WATCHER; + const tscWatchFile = process.env.TSC_WATCHFILE; + const tscWatchDirectory = process.env.TSC_WATCHDIRECTORY; + let dynamicPollingWatchFile: HostWatchFile | undefined; + const nodeSystem: System = { + args: process.argv.slice(2), + newLine: _os.EOL, + useCaseSensitiveFileNames, + write(s: string): void { + process.stdout.write(s); + }, + readFile, + writeFile, + watchFile: getWatchFile(), + watchDirectory: getWatchDirectory(), + resolvePath: path => _path.resolve(path), + fileExists, + directoryExists, + createDirectory(directoryName: string) { + if (!nodeSystem.directoryExists(directoryName)) { + _fs.mkdirSync(directoryName); } - } - - function addDirWatcher(dirPath: string): void { - let watcher = dirWatchers.get(dirPath); - if (watcher) { - watcher.referenceCount += 1; - return; + }, + getExecutingFilePath() { + return __filename; + }, + getCurrentDirectory() { + return process.cwd(); + }, + getDirectories, + getEnvironmentVariable(name: string) { + return process.env[name] || ""; + }, + readDirectory, + getModifiedTime, + createHash: _crypto ? createMD5HashUsingNativeCrypto : generateDjb2Hash, + getMemoryUsage() { + if (global.gc) { + global.gc(); } - watcher = fsWatchDirectory( - dirPath || ".", - (eventName: string, relativeFileName: string) => fileEventHandler(eventName, relativeFileName, dirPath) - ) as DirectoryWatcher; - watcher.referenceCount = 1; - dirWatchers.set(dirPath, watcher); - return; - } - - function addFileWatcherCallback(filePath: string, callback: FileWatcherCallback): void { - fileWatcherCallbacks.add(filePath, callback); - } - - function addFile(fileName: string, callback: FileWatcherCallback): WatchedFile { - addFileWatcherCallback(fileName, callback); - addDirWatcher(getDirectoryPath(fileName)); - - return { fileName, callback }; - } - - function removeFile(watchedFile: WatchedFile) { - removeFileWatcherCallback(watchedFile.fileName, watchedFile.callback); - reduceDirWatcherRefCountForFile(watchedFile.fileName); - } - - function removeFileWatcherCallback(filePath: string, callback: FileWatcherCallback) { - fileWatcherCallbacks.remove(filePath, callback); - } - - function fileEventHandler(eventName: string, relativeFileName: string | undefined, baseDirPath: string) { - // When files are deleted from disk, the triggered "rename" event would have a relativefileName of "undefined" - const fileName = !isString(relativeFileName) - ? undefined - : getNormalizedAbsolutePath(relativeFileName, baseDirPath); - // Some applications save a working file via rename operations - if ((eventName === "change" || eventName === "rename")) { - const callbacks = fileWatcherCallbacks.get(fileName); - if (callbacks) { - for (const fileCallback of callbacks) { - fileCallback(fileName, FileWatcherEventKind.Changed); - } + return process.memoryUsage().heapUsed; + }, + getFileSize(path) { + try { + const stat = _fs.statSync(path); + if (stat.isFile()) { + return stat.size; } } + catch { /*ignore*/ } + return 0; + }, + exit(exitCode?: number): void { + process.exit(exitCode); + }, + realpath(path: string): string { + try { + return _fs.realpathSync(path); + } + catch { + return path; + } + }, + debugMode: some(process.execArgv, arg => /^--(inspect|debug)(-brk)?(=\d+)?$/i.test(arg)), + tryEnableSourceMapsForHost() { + try { + require("source-map-support").install(); + } + catch { + // Could not enable source maps. + } + }, + setTimeout, + clearTimeout, + clearScreen: () => { + process.stdout.write("\x1Bc"); } - } - const watchedFileSet = createWatchedFileSet(); - - const nodeVersion = getNodeMajorVersion(); - const isNode4OrLater = nodeVersion >= 4; + }; + return nodeSystem; function isFileSystemCaseSensitive(): boolean { // win32\win64 are case insensitive platforms @@ -246,53 +635,201 @@ namespace ts { }); } - const platform: string = _os.platform(); - const useCaseSensitiveFileNames = isFileSystemCaseSensitive(); + function getWatchFile(): HostWatchFile { + switch (tscWatchFile) { + case "PriorityPollingInterval": + // Use polling interval based on priority when create watch using host.watchFile + return fsWatchFile; + case "DynamicPriorityPolling": + // Use polling interval but change the interval depending on file changes and their default polling interval + return createDynamicPriorityPollingWatchFile({ getModifiedTime, setTimeout }); + case "UseFsEvents": + // Use notifications from FS to watch with falling back to fs.watchFile + return watchFileUsingFsWatch; + case "UseFsEventsWithFallbackDynamicPolling": + // Use notifications from FS to watch with falling back to dynamic watch file + dynamicPollingWatchFile = createDynamicPriorityPollingWatchFile({ getModifiedTime, setTimeout }); + return createWatchFileUsingDynamicWatchFile(dynamicPollingWatchFile); + case "UseFsEventsOnParentDirectory": + // Use notifications from FS to watch with falling back to fs.watchFile + return createNonPollingWatchFile(); + } + return useNonPollingWatchers ? + createNonPollingWatchFile() : + // Default to do not use polling interval as it is before this experiment branch + (fileName, callback) => fsWatchFile(fileName, callback); + } + + function getWatchDirectory(): HostWatchDirectory { + // Node 4.0 `fs.watch` function supports the "recursive" option on both OSX and Windows + // (ref: https://github.com/nodejs/node/pull/2649 and https://github.com/Microsoft/TypeScript/issues/4643) + const fsSupportsRecursive = isNode4OrLater && (process.platform === "win32" || process.platform === "darwin"); + if (fsSupportsRecursive) { + return watchDirectoryUsingFsWatch; + } + + const watchDirectory = tscWatchDirectory === "RecursiveDirectoryUsingFsWatchFile" ? + createWatchDirectoryUsing(fsWatchFile) : + tscWatchDirectory === "RecursiveDirectoryUsingDynamicPriorityPolling" ? + createWatchDirectoryUsing(dynamicPollingWatchFile || createDynamicPriorityPollingWatchFile({ getModifiedTime, setTimeout })) : + watchDirectoryUsingFsWatch; + const watchDirectoryRecursively = createRecursiveDirectoryWatcher({ + filePathComparer: useCaseSensitiveFileNames ? compareStringsCaseSensitive : compareStringsCaseInsensitive, + directoryExists, + getAccessileSortedChildDirectories: path => getAccessibleFileSystemEntries(path).directories, + watchDirectory + }); + + return (directoryName, callback, recursive) => { + if (recursive) { + return watchDirectoryRecursively(directoryName, callback); + } + watchDirectory(directoryName, callback); + }; + } + + function createNonPollingWatchFile() { + // One file can have multiple watchers + const fileWatcherCallbacks = createMultiMap(); + const dirWatchers = createMap(); + const toCanonicalName = createGetCanonicalFileName(useCaseSensitiveFileNames); + return nonPollingWatchFile; + + function nonPollingWatchFile(fileName: string, callback: FileWatcherCallback): FileWatcher { + const filePath = toCanonicalName(fileName); + fileWatcherCallbacks.add(filePath, callback); + const dirPath = getDirectoryPath(filePath) || "."; + const watcher = dirWatchers.get(dirPath) || createDirectoryWatcher(getDirectoryPath(fileName) || ".", dirPath); + watcher.referenceCount++; + return { + close: () => { + if (watcher.referenceCount === 1) { + watcher.close(); + dirWatchers.delete(dirPath); + } + else { + watcher.referenceCount--; + } + fileWatcherCallbacks.remove(filePath, callback); + } + }; + } + + function createDirectoryWatcher(dirName: string, dirPath: string) { + const watcher = fsWatchDirectory( + dirName, + (_eventName: string, relativeFileName) => { + // When files are deleted from disk, the triggered "rename" event would have a relativefileName of "undefined" + const fileName = !isString(relativeFileName) + ? undefined + : getNormalizedAbsolutePath(relativeFileName, dirName); + // Some applications save a working file via rename operations + const callbacks = fileWatcherCallbacks.get(toCanonicalName(fileName)); + if (callbacks) { + for (const fileCallback of callbacks) { + fileCallback(fileName, FileWatcherEventKind.Changed); + } + } + } + ) as DirectoryWatcher; + watcher.referenceCount = 0; + dirWatchers.set(dirPath, watcher); + return watcher; + } + } function fsWatchFile(fileName: string, callback: FileWatcherCallback, pollingInterval?: number): FileWatcher { _fs.watchFile(fileName, { persistent: true, interval: pollingInterval || 250 }, fileChanged); + let eventKind: FileWatcherEventKind; return { close: () => _fs.unwatchFile(fileName, fileChanged) }; function fileChanged(curr: any, prev: any) { - const isCurrZero = +curr.mtime === 0; - const isPrevZero = +prev.mtime === 0; - const created = !isCurrZero && isPrevZero; - const deleted = isCurrZero && !isPrevZero; - - const eventKind = created - ? FileWatcherEventKind.Created - : deleted - ? FileWatcherEventKind.Deleted - : FileWatcherEventKind.Changed; - - if (eventKind === FileWatcherEventKind.Changed && +curr.mtime <= +prev.mtime) { + if (+curr.mtime === 0) { + eventKind = FileWatcherEventKind.Deleted; + } + // previous event kind check is to ensure we send created event when file is restored or renamed twice (that is it disappears and reappears) + // since in that case the prevTime returned is same as prev time of event when file was deleted as per node documentation + else if (+prev.mtime === 0 || eventKind === FileWatcherEventKind.Deleted) { + eventKind = FileWatcherEventKind.Created; + } + // If there is no change in modified time, ignore the event + else if (+curr.mtime === +prev.mtime) { return; } - + else { + // File changed + eventKind = FileWatcherEventKind.Changed; + } callback(fileName, eventKind); } } - function fsWatchDirectory(directoryName: string, callback: (eventName: string, relativeFileName: string) => void, recursive?: boolean): FileWatcher { + type FsWatchCallback = (eventName: "rename" | "change", relativeFileName: string) => void; + + function createFileWatcherCallback(callback: FsWatchCallback): FileWatcherCallback { + return (_fileName, eventKind) => callback(eventKind === FileWatcherEventKind.Changed ? "change" : "rename", ""); + } + + function createFsWatchCallbackForFileWatcherCallback(fileName: string, callback: FileWatcherCallback): FsWatchCallback { + return eventName => { + if (eventName === "rename") { + callback(fileName, fileExists(fileName) ? FileWatcherEventKind.Created : FileWatcherEventKind.Deleted); + } + else { + // Change + callback(fileName, FileWatcherEventKind.Changed); + } + }; + } + + function createFsWatchCallbackForDirectoryWatcherCallback(directoryName: string, callback: DirectoryWatcherCallback): FsWatchCallback { + return (eventName, relativeFileName) => { + // In watchDirectory we only care about adding and removing files (when event name is + // "rename"); changes made within files are handled by corresponding fileWatchers (when + // event name is "change") + if (eventName === "rename") { + // When deleting a file, the passed baseFileName is null + callback(!relativeFileName ? directoryName : normalizePath(combinePaths(directoryName, relativeFileName))); + } + }; + } + + function fsWatch(fileOrDirectory: string, entryKind: FileSystemEntryKind.File | FileSystemEntryKind.Directory, callback: FsWatchCallback, recursive: boolean, fallbackPollingWatchFile: HostWatchFile, pollingInterval?: number): FileWatcher { let options: any; - /** Watcher for the directory depending on whether it is missing or present */ - let watcher = !directoryExists(directoryName) ? - watchMissingDirectory() : - watchPresentDirectory(); + /** Watcher for the file system entry depending on whether it is missing or present */ + let watcher = !fileSystemEntryExists(fileOrDirectory, entryKind) ? + watchMissingFileSystemEntry() : + watchPresentFileSystemEntry(); return { close: () => { - // Close the watcher (either existing directory watcher or missing directory watcher) + // Close the watcher (either existing file system entry watcher or missing file system entry watcher) watcher.close(); + watcher = undefined; } }; /** - * Watch the directory that is currently present - * and when the watched directory is deleted, switch to missing directory watcher + * Invoke the callback with rename and update the watcher if not closed + * @param createWatcher */ - function watchPresentDirectory(): FileWatcher { + function invokeCallbackAndUpdateWatcher(createWatcher: () => FileWatcher) { + // Call the callback for current directory + callback("rename", ""); + + // If watcher is not closed, update it + if (watcher) { + watcher.close(); + watcher = createWatcher(); + } + } + + /** + * Watch the file or directory that is currently present + * and when the watched file or directory is deleted, switch to missing file system entry watcher + */ + function watchPresentFileSystemEntry(): FileWatcher { // Node 4.0 `fs.watch` function supports the "recursive" option on both OSX and Windows // (ref: https://github.com/nodejs/node/pull/2649 and https://github.com/Microsoft/TypeScript/issues/4643) if (options === undefined) { @@ -303,41 +840,69 @@ namespace ts { options = { persistent: true }; } } + try { - const dirWatcher = _fs.watch( - directoryName, - options, - callback - ); - dirWatcher.on("error", () => { - if (!directoryExists(directoryName)) { - // Deleting directory - watcher = watchMissingDirectory(); - // Call the callback for current directory - callback("rename", ""); - } - }); - return dirWatcher; + const presentWatcher = _fs.watch( + fileOrDirectory, + options, + callback + ); + // Watch the missing file or directory or error + presentWatcher.on("error", () => invokeCallbackAndUpdateWatcher(watchMissingFileSystemEntry)); + return presentWatcher; + } + catch (e) { + // Catch the exception and use polling instead + // Eg. on linux the number of watches are limited and one could easily exhaust watches and the exception ENOSPC is thrown when creating watcher at that point + // so instead of throwing error, use fs.watchFile + return watchPresentFileSystemEntryWithFsWatchFile(); + } } /** - * Watch the directory that is missing - * and switch to existing directory when the directory is created + * Watch the file or directory using fs.watchFile since fs.watch threw exception + * Eg. on linux the number of watches are limited and one could easily exhaust watches and the exception ENOSPC is thrown when creating watcher at that point */ - function watchMissingDirectory(): FileWatcher { - return fsWatchFile(directoryName, (_fileName, eventKind) => { - if (eventKind === FileWatcherEventKind.Created && directoryExists(directoryName)) { - watcher.close(); - watcher = watchPresentDirectory(); - // Call the callback for current directory + function watchPresentFileSystemEntryWithFsWatchFile(): FileWatcher { + return fallbackPollingWatchFile(fileOrDirectory, createFileWatcherCallback(callback), pollingInterval); + } + + /** + * Watch the file or directory that is missing + * and switch to existing file or directory when the missing filesystem entry is created + */ + function watchMissingFileSystemEntry(): FileWatcher { + return fallbackPollingWatchFile(fileOrDirectory, (_fileName, eventKind) => { + if (eventKind === FileWatcherEventKind.Created && fileSystemEntryExists(fileOrDirectory, entryKind)) { + // Call the callback for current file or directory // For now it could be callback for the inner directory creation, // but just return current directory, better than current no-op - callback("rename", ""); + invokeCallbackAndUpdateWatcher(watchPresentFileSystemEntry); } - }); + }, pollingInterval); } } + function watchFileUsingFsWatch(fileName: string, callback: FileWatcherCallback, pollingInterval?: number) { + return fsWatch(fileName, FileSystemEntryKind.File, createFsWatchCallbackForFileWatcherCallback(fileName, callback), /*recursive*/ false, fsWatchFile, pollingInterval); + } + + function createWatchFileUsingDynamicWatchFile(watchFile: HostWatchFile): HostWatchFile { + return (fileName, callback, pollingInterval) => fsWatch(fileName, FileSystemEntryKind.File, createFsWatchCallbackForFileWatcherCallback(fileName, callback), /*recursive*/ false, watchFile, pollingInterval); + } + + function fsWatchDirectory(directoryName: string, callback: FsWatchCallback, recursive?: boolean): FileWatcher { + return fsWatch(directoryName, FileSystemEntryKind.Directory, callback, !!recursive, fsWatchFile); + } + + function watchDirectoryUsingFsWatch(directoryName: string, callback: DirectoryWatcherCallback, recursive?: boolean) { + return fsWatchDirectory(directoryName, createFsWatchCallbackForDirectoryWatcherCallback(directoryName, callback), recursive); + } + + function createWatchDirectoryUsing(fsWatchFile: HostWatchFile): HostWatchDirectory { + return (directoryName, callback) => fsWatchFile(directoryName, () => callback(directoryName), PollingInterval.Medium); + } + function readFile(fileName: string, _encoding?: string): string | undefined { if (!fileExists(fileName)) { return undefined; @@ -425,11 +990,6 @@ namespace ts { return matchFiles(path, extensions, excludes, includes, useCaseSensitiveFileNames, process.cwd(), depth, getAccessibleFileSystemEntries); } - const enum FileSystemEntryKind { - File, - Directory - } - function fileSystemEntryExists(path: string, entryKind: FileSystemEntryKind): boolean { try { const stat = _fs.statSync(path); @@ -455,110 +1015,29 @@ namespace ts { return filter(_fs.readdirSync(path), dir => fileSystemEntryExists(combinePaths(path, dir), FileSystemEntryKind.Directory)); } - const nodeSystem: System = { - clearScreen: () => { - process.stdout.write("\x1Bc"); - }, - args: process.argv.slice(2), - newLine: _os.EOL, - useCaseSensitiveFileNames, - write(s: string): void { - process.stdout.write(s); - }, - readFile, - writeFile, - watchFile: (fileName, callback, pollingInterval) => { - if (useNonPollingWatchers) { - const watchedFile = watchedFileSet.addFile(fileName, callback); - return { - close: () => watchedFileSet.removeFile(watchedFile) - }; - } - else { - return fsWatchFile(fileName, callback, pollingInterval); - } - }, - watchDirectory: (directoryName, callback, recursive) => { - // Node 4.0 `fs.watch` function supports the "recursive" option on both OSX and Windows - // (ref: https://github.com/nodejs/node/pull/2649 and https://github.com/Microsoft/TypeScript/issues/4643) - return fsWatchDirectory(directoryName, (eventName, relativeFileName) => { - // In watchDirectory we only care about adding and removing files (when event name is - // "rename"); changes made within files are handled by corresponding fileWatchers (when - // event name is "change") - if (eventName === "rename") { - // When deleting a file, the passed baseFileName is null - callback(!relativeFileName ? relativeFileName : normalizePath(combinePaths(directoryName, relativeFileName))); - } - }, recursive); - }, - resolvePath: path => _path.resolve(path), - fileExists, - directoryExists, - createDirectory(directoryName: string) { - if (!nodeSystem.directoryExists(directoryName)) { - _fs.mkdirSync(directoryName); - } - }, - getExecutingFilePath() { - return __filename; - }, - getCurrentDirectory() { - return process.cwd(); - }, - getDirectories, - getEnvironmentVariable(name: string) { - return process.env[name] || ""; - }, - readDirectory, - getModifiedTime(path) { - try { - return _fs.statSync(path).mtime; - } - catch (e) { - return undefined; - } - }, - createHash: _crypto ? createMD5HashUsingNativeCrypto : generateDjb2Hash, - getMemoryUsage() { - if (global.gc) { - global.gc(); - } - return process.memoryUsage().heapUsed; - }, - getFileSize(path) { - try { - const stat = _fs.statSync(path); - if (stat.isFile()) { - return stat.size; - } - } - catch { /*ignore*/ } - return 0; - }, - exit(exitCode?: number): void { - process.exit(exitCode); - }, - realpath(path: string): string { - try { - return _fs.realpathSync(path); - } - catch { - return path; - } - }, - debugMode: some(process.execArgv, arg => /^--(inspect|debug)(-brk)?(=\d+)?$/i.test(arg)), - tryEnableSourceMapsForHost() { - try { - require("source-map-support").install(); - } - catch { - // Could not enable source maps. - } - }, - setTimeout, - clearTimeout - }; - return nodeSystem; + function getModifiedTime(path: string) { + try { + return _fs.statSync(path).mtime; + } + catch (e) { + return undefined; + } + } + + /** + * djb2 hashing algorithm + * http://www.cse.yorku.ca/~oz/hash.html + */ + function generateDjb2Hash(data: string): string { + const chars = data.split("").map(str => str.charCodeAt(0)); + return `${chars.reduce((prev, curr) => ((prev << 5) + prev) + curr, 5381)}`; + } + + function createMD5HashUsingNativeCrypto(data: string) { + const hash = _crypto.createHash("md5"); + hash.update(data); + return hash.digest("hex"); + } } function getChakraSystem(): System { @@ -632,6 +1111,7 @@ namespace ts { })(); if (sys && sys.getEnvironmentVariable) { + setCustomPollingValues(sys); Debug.currentAssertionLevel = /^development$/i.test(sys.getEnvironmentVariable("NODE_ENV")) ? AssertionLevel.Normal : AssertionLevel.None; diff --git a/src/compiler/watch.ts b/src/compiler/watch.ts index 552316d7751ad..2b9cc9cf69494 100644 --- a/src/compiler/watch.ts +++ b/src/compiler/watch.ts @@ -483,18 +483,17 @@ namespace ts { } const trace = host.trace && ((s: string) => { host.trace(s + newLine); }); - const loggingEnabled = trace && (compilerOptions.diagnostics || compilerOptions.extendedDiagnostics); - const writeLog = loggingEnabled ? trace : noop; - const watchFile = compilerOptions.extendedDiagnostics ? addFileWatcherWithLogging : loggingEnabled ? addFileWatcherWithOnlyTriggerLogging : addFileWatcher; - const watchFilePath = compilerOptions.extendedDiagnostics ? addFilePathWatcherWithLogging : addFilePathWatcher; - const watchDirectoryWorker = compilerOptions.extendedDiagnostics ? addDirectoryWatcherWithLogging : addDirectoryWatcher; + const watchLogLevel = trace ? compilerOptions.extendedDiagnostics ? WatchLogLevel.Verbose : + compilerOptions.diagnostis ? WatchLogLevel.TriggerOnly : WatchLogLevel.None : WatchLogLevel.None; + const writeLog: (s: string) => void = watchLogLevel !== WatchLogLevel.None ? trace : noop; + const { watchFile, watchFilePath, watchDirectory: watchDirectoryWorker } = getWatchFactory(watchLogLevel, writeLog); const getCanonicalFileName = createGetCanonicalFileName(useCaseSensitiveFileNames); let newLine = updateNewLine(); writeLog(`Current directory: ${currentDirectory} CaseSensitiveFileNames: ${useCaseSensitiveFileNames}`); if (configFileName) { - watchFile(host, configFileName, scheduleProgramReload, writeLog); + watchFile(host, configFileName, scheduleProgramReload, PollingInterval.High); } const compilerHost: CompilerHost & ResolutionCacheHost = { @@ -583,8 +582,8 @@ namespace ts { } // Compile the program - if (loggingEnabled) { - writeLog(`CreatingProgramWith::`); + if (watchLogLevel !== WatchLogLevel.None) { + writeLog("CreatingProgramWith::"); writeLog(` roots: ${JSON.stringify(rootFileNames)}`); writeLog(` options: ${JSON.stringify(compilerOptions)}`); } @@ -677,7 +676,7 @@ namespace ts { (hostSourceFile as FilePresentOnHost).sourceFile = sourceFile; sourceFile.version = hostSourceFile.version.toString(); if (!(hostSourceFile as FilePresentOnHost).fileWatcher) { - (hostSourceFile as FilePresentOnHost).fileWatcher = watchFilePath(host, fileName, onSourceFileChange, path, writeLog); + (hostSourceFile as FilePresentOnHost).fileWatcher = watchFilePath(host, fileName, onSourceFileChange, PollingInterval.Low, path); } } else { @@ -691,7 +690,7 @@ namespace ts { else { if (sourceFile) { sourceFile.version = initialVersion.toString(); - const fileWatcher = watchFilePath(host, fileName, onSourceFileChange, path, writeLog); + const fileWatcher = watchFilePath(host, fileName, onSourceFileChange, PollingInterval.Low, path); sourceFilesCache.set(path, { sourceFile, version: initialVersion, fileWatcher }); } else { @@ -854,11 +853,11 @@ namespace ts { } function watchDirectory(directory: string, cb: DirectoryWatcherCallback, flags: WatchDirectoryFlags) { - return watchDirectoryWorker(host, directory, cb, flags, writeLog); + return watchDirectoryWorker(host, directory, cb, flags); } function watchMissingFilePath(missingFilePath: Path) { - return watchFilePath(host, missingFilePath, onMissingFileChange, missingFilePath, writeLog); + return watchFilePath(host, missingFilePath, onMissingFileChange, PollingInterval.Medium, missingFilePath); } function onMissingFileChange(fileName: string, eventKind: FileWatcherEventKind, missingFilePath: Path) { diff --git a/src/compiler/watchUtilities.ts b/src/compiler/watchUtilities.ts index f5c614d9d577c..12ed9f855dfca 100644 --- a/src/compiler/watchUtilities.ts +++ b/src/compiler/watchUtilities.ts @@ -331,85 +331,101 @@ namespace ts { return program.isEmittedFile(file); } - export interface WatchFileHost { - watchFile(path: string, callback: FileWatcherCallback, pollingInterval?: number): FileWatcher; - } - - export function addFileWatcher(host: WatchFileHost, file: string, cb: FileWatcherCallback): FileWatcher { - return host.watchFile(file, cb); + export enum WatchLogLevel { + None, + TriggerOnly, + Verbose } - export function addFileWatcherWithLogging(host: WatchFileHost, file: string, cb: FileWatcherCallback, log: (s: string) => void): FileWatcher { - const watcherCaption = `FileWatcher:: `; - return createWatcherWithLogging(addFileWatcher, watcherCaption, log, /*logOnlyTrigger*/ false, host, file, cb); + export interface WatchFileHost { + watchFile(path: string, callback: FileWatcherCallback, pollingInterval?: number): FileWatcher; } - - export function addFileWatcherWithOnlyTriggerLogging(host: WatchFileHost, file: string, cb: FileWatcherCallback, log: (s: string) => void): FileWatcher { - const watcherCaption = `FileWatcher:: `; - return createWatcherWithLogging(addFileWatcher, watcherCaption, log, /*logOnlyTrigger*/ true, host, file, cb); + export interface WatchDirectoryHost { + watchDirectory(path: string, callback: DirectoryWatcherCallback, recursive?: boolean): FileWatcher; } - + export type WatchFile = (host: WatchFileHost, file: string, callback: FileWatcherCallback, pollingInterval: PollingInterval, detailInfo1?: X, detailInfo2?: Y) => FileWatcher; export type FilePathWatcherCallback = (fileName: string, eventKind: FileWatcherEventKind, filePath: Path) => void; - export function addFilePathWatcher(host: WatchFileHost, file: string, cb: FilePathWatcherCallback, path: Path): FileWatcher { - return host.watchFile(file, (fileName, eventKind) => cb(fileName, eventKind, path)); - } + export type WatchFilePath = (host: WatchFileHost, file: string, callback: FilePathWatcherCallback, pollingInterval: PollingInterval, path: Path, detailInfo1?: X, detailInfo2?: Y) => FileWatcher; + export type WatchDirectory = (host: WatchDirectoryHost, directory: string, callback: DirectoryWatcherCallback, flags: WatchDirectoryFlags, detailInfo1?: X, detailInfo2?: Y) => FileWatcher; - export function addFilePathWatcherWithLogging(host: WatchFileHost, file: string, cb: FilePathWatcherCallback, path: Path, log: (s: string) => void): FileWatcher { - const watcherCaption = `FileWatcher:: `; - return createWatcherWithLogging(addFileWatcher, watcherCaption, log, /*logOnlyTrigger*/ false, host, file, cb, path); + export interface WatchFactory { + watchFile: WatchFile; + watchFilePath: WatchFilePath; + watchDirectory: WatchDirectory; } - export function addFilePathWatcherWithOnlyTriggerLogging(host: WatchFileHost, file: string, cb: FilePathWatcherCallback, path: Path, log: (s: string) => void): FileWatcher { - const watcherCaption = `FileWatcher:: `; - return createWatcherWithLogging(addFileWatcher, watcherCaption, log, /*logOnlyTrigger*/ true, host, file, cb, path); + export function getWatchFactory(watchLogLevel: WatchLogLevel, log: (s: string) => void, getDetailWatchInfo?: GetDetailWatchInfo): WatchFactory { + return getWatchFactoryWith(watchLogLevel, log, getDetailWatchInfo, watchFile, watchDirectory); } - export interface WatchDirectoryHost { - watchDirectory(path: string, callback: DirectoryWatcherCallback, recursive?: boolean): FileWatcher; - } + function getWatchFactoryWith(watchLogLevel: WatchLogLevel, log: (s: string) => void, getDetailWatchInfo: GetDetailWatchInfo | undefined, + watchFile: (host: WatchFileHost, file: string, callback: FileWatcherCallback, watchPriority: PollingInterval) => FileWatcher, + watchDirectory: (host: WatchDirectoryHost, directory: string, callback: DirectoryWatcherCallback, flags: WatchDirectoryFlags) => FileWatcher): WatchFactory { + const createFileWatcher: CreateFileWatcher = getCreateFileWatcher(watchLogLevel, watchFile); + const createFilePathWatcher: CreateFileWatcher = watchLogLevel === WatchLogLevel.None ? watchFilePath : createFileWatcher; + const createDirectoryWatcher: CreateFileWatcher = getCreateFileWatcher(watchLogLevel, watchDirectory); + return { + watchFile: (host, file, callback, pollingInterval, detailInfo1, detailInfo2) => + createFileWatcher(host, file, callback, pollingInterval, /*passThrough*/ undefined, detailInfo1, detailInfo2, watchFile, log, "FileWatcher", getDetailWatchInfo), + watchFilePath: (host, file, callback, pollingInterval, path, detailInfo1, detailInfo2) => + createFilePathWatcher(host, file, callback, pollingInterval, path, detailInfo1, detailInfo2, watchFile, log, "FileWatcher", getDetailWatchInfo), + watchDirectory: (host, directory, callback, flags, detailInfo1, detailInfo2) => + createDirectoryWatcher(host, directory, callback, flags, /*passThrough*/ undefined, detailInfo1, detailInfo2, watchDirectory, log, "DirectoryWatcher", getDetailWatchInfo) + }; - export function addDirectoryWatcher(host: WatchDirectoryHost, directory: string, cb: DirectoryWatcherCallback, flags: WatchDirectoryFlags): FileWatcher { - const recursive = (flags & WatchDirectoryFlags.Recursive) !== 0; - return host.watchDirectory(directory, cb, recursive); + function watchFilePath(host: WatchFileHost, file: string, callback: FilePathWatcherCallback, pollingInterval: PollingInterval, path: Path): FileWatcher { + return watchFile(host, file, (fileName, eventKind) => callback(fileName, eventKind, path), pollingInterval); + } } - export function addDirectoryWatcherWithLogging(host: WatchDirectoryHost, directory: string, cb: DirectoryWatcherCallback, flags: WatchDirectoryFlags, log: (s: string) => void): FileWatcher { - const watcherCaption = `DirectoryWatcher ${(flags & WatchDirectoryFlags.Recursive) !== 0 ? "recursive" : ""}:: `; - return createWatcherWithLogging(addDirectoryWatcher, watcherCaption, log, /*logOnlyTrigger*/ false, host, directory, cb, flags); + function watchFile(host: WatchFileHost, file: string, callback: FileWatcherCallback, pollingInterval: PollingInterval): FileWatcher { + return host.watchFile(file, callback, pollingInterval); } - export function addDirectoryWatcherWithOnlyTriggerLogging(host: WatchDirectoryHost, directory: string, cb: DirectoryWatcherCallback, flags: WatchDirectoryFlags, log: (s: string) => void): FileWatcher { - const watcherCaption = `DirectoryWatcher ${(flags & WatchDirectoryFlags.Recursive) !== 0 ? "recursive" : ""}:: `; - return createWatcherWithLogging(addDirectoryWatcher, watcherCaption, log, /*logOnlyTrigger*/ true, host, directory, cb, flags); + function watchDirectory(host: WatchDirectoryHost, directory: string, callback: DirectoryWatcherCallback, flags: WatchDirectoryFlags): FileWatcher { + return host.watchDirectory(directory, callback, (flags & WatchDirectoryFlags.Recursive) !== 0); } - type WatchCallback = (fileName: string, cbOptional1?: T, optional?: U) => void; - type AddWatch = (host: H, file: string, cb: WatchCallback, optional?: U) => FileWatcher; - function createWatcherWithLogging(addWatch: AddWatch, watcherCaption: string, log: (s: string) => void, logOnlyTrigger: boolean, host: H, file: string, cb: WatchCallback, optional?: U): FileWatcher { - const info = `PathInfo: ${file}`; - if (!logOnlyTrigger) { - log(`${watcherCaption}Added: ${info}`); + type WatchCallback = (fileName: string, cbOptional?: T, passThrough?: U) => void; + type AddWatch = (host: H, file: string, cb: WatchCallback, flags: T, passThrough?: V, detailInfo1?: undefined, detailInfo2?: undefined) => FileWatcher; + export type GetDetailWatchInfo = (detailInfo1: X, detailInfo2: Y) => string; + + type CreateFileWatcher = (host: H, file: string, cb: WatchCallback, flags: T, passThrough: V | undefined, detailInfo1: X | undefined, detailInfo2: Y | undefined, addWatch: AddWatch, log: (s: string) => void, watchCaption: string, getDetailWatchInfo: GetDetailWatchInfo | undefined) => FileWatcher; + function getCreateFileWatcher(watchLogLevel: WatchLogLevel, addWatch: AddWatch): CreateFileWatcher { + switch (watchLogLevel) { + case WatchLogLevel.None: + return addWatch; + case WatchLogLevel.TriggerOnly: + return createFileWatcherWithTriggerLogging; + case WatchLogLevel.Verbose: + return createFileWatcherWithLogging; } - const watcher = addWatch(host, file, (fileName, cbOptional1?) => { - const optionalInfo = cbOptional1 !== undefined ? ` ${cbOptional1}` : ""; - log(`${watcherCaption}Trigger: ${fileName}${optionalInfo} ${info}`); - const start = timestamp(); - cb(fileName, cbOptional1, optional); - const elapsed = timestamp() - start; - log(`${watcherCaption}Elapsed: ${elapsed}ms Trigger: ${fileName}${optionalInfo} ${info}`); - }, optional); + } + + function createFileWatcherWithLogging(host: H, file: string, cb: WatchCallback, flags: T, passThrough: V | undefined, detailInfo1: X | undefined, detailInfo2: Y | undefined, addWatch: AddWatch, log: (s: string) => void, watchCaption: string, getDetailWatchInfo: GetDetailWatchInfo | undefined): FileWatcher { + log(`${watchCaption}:: Added:: ${getWatchInfo(file, flags, detailInfo1, detailInfo2, getDetailWatchInfo)}`); + const watcher = createFileWatcherWithTriggerLogging(host, file, cb, flags, passThrough, detailInfo1, detailInfo2, addWatch, log, watchCaption, getDetailWatchInfo); return { close: () => { - if (!logOnlyTrigger) { - log(`${watcherCaption}Close: ${info}`); - } + log(`${watchCaption}:: Close:: ${getWatchInfo(file, flags, detailInfo1, detailInfo2, getDetailWatchInfo)}`); watcher.close(); } }; } - export function closeFileWatcher(watcher: FileWatcher) { - watcher.close(); + function createFileWatcherWithTriggerLogging(host: H, file: string, cb: WatchCallback, flags: T, passThrough: V | undefined, detailInfo1: X | undefined, detailInfo2: Y | undefined, addWatch: AddWatch, log: (s: string) => void, watchCaption: string, getDetailWatchInfo: GetDetailWatchInfo | undefined): FileWatcher { + return addWatch(host, file, (fileName, cbOptional) => { + const triggerredInfo = `${watchCaption}:: Triggered with ${fileName}${cbOptional !== undefined ? cbOptional : ""}:: ${getWatchInfo(file, flags, detailInfo1, detailInfo2, getDetailWatchInfo)}`; + log(triggerredInfo); + const start = timestamp(); + cb(fileName, cbOptional, passThrough); + const elapsed = timestamp() - start; + log(`Elapsed:: ${elapsed}ms ${triggerredInfo}`); + }, flags); + } + + function getWatchInfo(file: string, flags: T, detailInfo1: X | undefined, detailInfo2: Y | undefined, getDetailWatchInfo: GetDetailWatchInfo | undefined) { + return `WatchInfo: ${file} ${flags} ${getDetailWatchInfo ? getDetailWatchInfo(detailInfo1, detailInfo2) : ""}`; } export function closeFileWatcherOf(objWithWatcher: T) { diff --git a/src/harness/unittests/tscWatchMode.ts b/src/harness/unittests/tscWatchMode.ts index 1c23eb04ea46a..47116cd5b0129 100644 --- a/src/harness/unittests/tscWatchMode.ts +++ b/src/harness/unittests/tscWatchMode.ts @@ -3,11 +3,10 @@ /// namespace ts.tscWatch { - import WatchedSystem = TestFSWithWatch.TestServerHost; type FileOrFolder = TestFSWithWatch.FileOrFolder; import createWatchedSystem = TestFSWithWatch.createWatchedSystem; - import checkFileNames = TestFSWithWatch.checkFileNames; + import checkArray = TestFSWithWatch.checkArray; import libFile = TestFSWithWatch.libFile; import checkWatchedFiles = TestFSWithWatch.checkWatchedFiles; import checkWatchedDirectories = TestFSWithWatch.checkWatchedDirectories; @@ -15,11 +14,11 @@ namespace ts.tscWatch { import checkOutputDoesNotContain = TestFSWithWatch.checkOutputDoesNotContain; export function checkProgramActualFiles(program: Program, expectedFiles: string[]) { - checkFileNames(`Program actual files`, program.getSourceFiles().map(file => file.fileName), expectedFiles); + checkArray(`Program actual files`, program.getSourceFiles().map(file => file.fileName), expectedFiles); } export function checkProgramRootFiles(program: Program, expectedFiles: string[]) { - checkFileNames(`Program rootFileNames`, program.getRootFileNames(), expectedFiles); + checkArray(`Program rootFileNames`, program.getRootFileNames(), expectedFiles); } function createWatchOfConfigFile(configFileName: string, host: WatchedSystem, maxNumberOfFilesToIterateForInvalidation?: number) { @@ -73,64 +72,60 @@ namespace ts.tscWatch { checkOutputDoesNotContain(host, expectedNonAffectedFiles); } - enum ExpectedOutputErrorsPosition { - BeforeCompilationStarts, - AfterCompilationStarting, - AfterFileChangeDetected - } - function checkOutputErrors( host: WatchedSystem, + preErrorsWatchDiagnostic: DiagnosticMessage | undefined, errors: ReadonlyArray, - errorsPosition: ExpectedOutputErrorsPosition, - skipWaiting?: true + ...postErrorsWatchDiagnostics: DiagnosticMessage[] ) { const outputs = host.getOutput(); - const expectedOutputCount = errors.length + (skipWaiting ? 0 : 1) + 1; - assert.equal(outputs.length, expectedOutputCount, "Outputs = " + outputs.toString()); - let index: number; - - switch (errorsPosition) { - case ExpectedOutputErrorsPosition.AfterCompilationStarting: - assertWatchDiagnosticAt(host, 0, Diagnostics.Starting_compilation_in_watch_mode); - index = 1; - break; - case ExpectedOutputErrorsPosition.AfterFileChangeDetected: - assertWatchDiagnosticAt(host, 0, Diagnostics.File_change_detected_Starting_incremental_compilation); - index = 1; - break; - case ExpectedOutputErrorsPosition.BeforeCompilationStarts: - assertWatchDiagnosticAt(host, errors.length, Diagnostics.Starting_compilation_in_watch_mode); - index = 0; - break; + const expectedOutputCount = (preErrorsWatchDiagnostic ? 1 : 0) + errors.length + postErrorsWatchDiagnostics.length; + assert.equal(outputs.length, expectedOutputCount); + let index = 0; + if (preErrorsWatchDiagnostic) { + assertWatchDiagnostic(preErrorsWatchDiagnostic); } + // Verify errors + forEach(errors, assertDiagnostic); + forEach(postErrorsWatchDiagnostics, assertWatchDiagnostic); + host.clearOutput(); - forEach(errors, error => { - assertDiagnosticAt(host, index, error); + function assertDiagnostic(diagnostic: Diagnostic) { + const expected = formatDiagnostic(diagnostic, host); + assert.equal(outputs[index], expected, getOutputAtFailedMessage("Diagnostic", expected)); index++; - }); - if (!skipWaiting) { - if (errorsPosition === ExpectedOutputErrorsPosition.BeforeCompilationStarts) { - assertWatchDiagnosticAt(host, index, Diagnostics.Starting_compilation_in_watch_mode); - index += 1; - } - assertWatchDiagnosticAt(host, index, Diagnostics.Compilation_complete_Watching_for_file_changes); } - host.clearOutput(); + + function assertWatchDiagnostic(diagnosticMessage: DiagnosticMessage) { + const expected = getWatchDiagnosticWithoutDate(diagnosticMessage); + assert.isTrue(endsWith(outputs[index], expected), getOutputAtFailedMessage("Watch diagnostic", expected)); + index++; + } + + function getOutputAtFailedMessage(caption: string, expectedOutput: string) { + return `Expected ${caption}: ${expectedOutput} at ${index} in ${JSON.stringify(outputs)}`; + } + + function getWatchDiagnosticWithoutDate(diagnosticMessage: DiagnosticMessage) { + return ` - ${flattenDiagnosticMessageText(getLocaleSpecificMessage(diagnosticMessage), host.newLine)}${host.newLine + host.newLine + host.newLine}`; + } + } + + function checkOutputErrorsInitial(host: WatchedSystem, errors: ReadonlyArray) { + checkOutputErrors(host, Diagnostics.Starting_compilation_in_watch_mode, errors, Diagnostics.Compilation_complete_Watching_for_file_changes); } - function assertDiagnosticAt(host: WatchedSystem, outputAt: number, diagnostic: Diagnostic) { - const output = host.getOutput()[outputAt]; - assert.equal(output, formatDiagnostic(diagnostic, host), "outputs[" + outputAt + "] is " + output); + function checkOutputErrorsInitialWithConfigErrors(host: WatchedSystem, errors: ReadonlyArray) { + checkOutputErrors(host, /*preErrorsWatchDiagnostic*/ undefined, errors, Diagnostics.Starting_compilation_in_watch_mode, Diagnostics.Compilation_complete_Watching_for_file_changes); } - function assertWatchDiagnosticAt(host: WatchedSystem, outputAt: number, diagnosticMessage: DiagnosticMessage) { - const output = host.getOutput()[outputAt]; - assert.isTrue(endsWith(output, getWatchDiagnosticWithoutDate(host, diagnosticMessage)), "outputs[" + outputAt + "] is " + output); + function checkOutputErrorsIncremental(host: WatchedSystem, errors: ReadonlyArray) { + checkOutputErrors(host, Diagnostics.File_change_detected_Starting_incremental_compilation, errors, Diagnostics.Compilation_complete_Watching_for_file_changes); } - function getWatchDiagnosticWithoutDate(host: WatchedSystem, diagnosticMessage: DiagnosticMessage) { - return ` - ${flattenDiagnosticMessageText(getLocaleSpecificMessage(diagnosticMessage), host.newLine)}${host.newLine + host.newLine + host.newLine}`; + function checkOutputErrorsIncrementalWithExit(host: WatchedSystem, errors: ReadonlyArray, expectedExitCode: ExitStatus) { + checkOutputErrors(host, Diagnostics.File_change_detected_Starting_incremental_compilation, errors); + assert.equal(host.exitCode, expectedExitCode); } function getDiagnosticOfFileFrom(file: SourceFile, text: string, start: number, length: number, message: DiagnosticMessage): Diagnostic { @@ -346,16 +341,16 @@ namespace ts.tscWatch { checkProgramRootFiles(watch(), [file1.path]); checkProgramActualFiles(watch(), [file1.path, libFile.path]); - checkOutputErrors(host, [ + checkOutputErrorsInitial(host, [ getDiagnosticOfFileFromProgram(watch(), file1.path, file1.content.indexOf(commonFile2Name), commonFile2Name.length, Diagnostics.File_0_not_found, commonFile2.path), getDiagnosticOfFileFromProgram(watch(), file1.path, file1.content.indexOf("y"), 1, Diagnostics.Cannot_find_name_0, "y") - ], /*errorsPosition*/ ExpectedOutputErrorsPosition.AfterCompilationStarting); + ]); host.reloadFS([file1, commonFile2, libFile]); host.runQueuedTimeoutCallbacks(); checkProgramRootFiles(watch(), [file1.path]); checkProgramActualFiles(watch(), [file1.path, libFile.path, commonFile2.path]); - checkOutputErrors(host, emptyArray, /*errorsPosition*/ ExpectedOutputErrorsPosition.AfterFileChangeDetected); + checkOutputErrorsIncremental(host, emptyArray); }); it("should reflect change in config file", () => { @@ -683,15 +678,14 @@ namespace ts.tscWatch { const watch = createWatchOfConfigFile(config.path, host); checkProgramActualFiles(watch(), [file1.path, file2.path, libFile.path]); - checkOutputErrors(host, emptyArray, /*errorsPosition*/ ExpectedOutputErrorsPosition.AfterCompilationStarting); + checkOutputErrorsInitial(host, emptyArray); host.reloadFS([file1, file2, libFile]); host.checkTimeoutQueueLengthAndRun(1); - assert.equal(host.exitCode, ExitStatus.DiagnosticsPresent_OutputsSkipped); - checkOutputErrors(host, [ + checkOutputErrorsIncrementalWithExit(host, [ getDiagnosticWithoutFile(Diagnostics.File_0_not_found, config.path) - ], /*errorsPosition*/ ExpectedOutputErrorsPosition.AfterFileChangeDetected, /*skipWaiting*/ true); + ], ExitStatus.DiagnosticsPresent_OutputsSkipped); }); it("Proper errors: document is not contained in project", () => { @@ -794,21 +788,21 @@ namespace ts.tscWatch { }; const host = createWatchedSystem([moduleFile, file1, libFile]); const watch = createWatchOfFilesAndCompilerOptions([file1.path], host); - checkOutputErrors(host, emptyArray, /*errorsPosition*/ ExpectedOutputErrorsPosition.AfterCompilationStarting); + checkOutputErrorsInitial(host, emptyArray); const moduleFileOldPath = moduleFile.path; const moduleFileNewPath = "/a/b/moduleFile1.ts"; moduleFile.path = moduleFileNewPath; host.reloadFS([moduleFile, file1, libFile]); host.runQueuedTimeoutCallbacks(); - checkOutputErrors(host, [ + checkOutputErrorsIncremental(host, [ getDiagnosticModuleNotFoundOfFile(watch(), file1, "./moduleFile") - ], /*errorsPosition*/ ExpectedOutputErrorsPosition.AfterFileChangeDetected); + ]); moduleFile.path = moduleFileOldPath; host.reloadFS([moduleFile, file1, libFile]); host.runQueuedTimeoutCallbacks(); - checkOutputErrors(host, emptyArray, /*errorsPosition*/ ExpectedOutputErrorsPosition.AfterFileChangeDetected); + checkOutputErrorsIncremental(host, emptyArray); }); it("rename a module file and rename back should restore the states for configured projects", () => { @@ -826,21 +820,21 @@ namespace ts.tscWatch { }; const host = createWatchedSystem([moduleFile, file1, configFile, libFile]); const watch = createWatchOfConfigFile(configFile.path, host); - checkOutputErrors(host, emptyArray, /*errorsPosition*/ ExpectedOutputErrorsPosition.AfterCompilationStarting); + checkOutputErrorsInitial(host, emptyArray); const moduleFileOldPath = moduleFile.path; const moduleFileNewPath = "/a/b/moduleFile1.ts"; moduleFile.path = moduleFileNewPath; host.reloadFS([moduleFile, file1, configFile, libFile]); host.runQueuedTimeoutCallbacks(); - checkOutputErrors(host, [ + checkOutputErrorsIncremental(host, [ getDiagnosticModuleNotFoundOfFile(watch(), file1, "./moduleFile") - ], /*errorsPosition*/ ExpectedOutputErrorsPosition.AfterFileChangeDetected); + ]); moduleFile.path = moduleFileOldPath; host.reloadFS([moduleFile, file1, configFile, libFile]); host.runQueuedTimeoutCallbacks(); - checkOutputErrors(host, emptyArray, /*errorsPosition*/ ExpectedOutputErrorsPosition.AfterFileChangeDetected); + checkOutputErrorsIncremental(host, emptyArray); }); it("types should load from config file path if config exists", () => { @@ -877,13 +871,13 @@ namespace ts.tscWatch { const host = createWatchedSystem([file1, libFile]); const watch = createWatchOfFilesAndCompilerOptions([file1.path], host); - checkOutputErrors(host, [ + checkOutputErrorsInitial(host, [ getDiagnosticModuleNotFoundOfFile(watch(), file1, "./moduleFile") - ], /*errorsPosition*/ ExpectedOutputErrorsPosition.AfterCompilationStarting); + ]); host.reloadFS([file1, moduleFile, libFile]); host.runQueuedTimeoutCallbacks(); - checkOutputErrors(host, emptyArray, /*errorsPosition*/ ExpectedOutputErrorsPosition.AfterFileChangeDetected); + checkOutputErrorsIncremental(host, emptyArray); }); it("Configure file diagnostics events are generated when the config file has errors", () => { @@ -903,10 +897,10 @@ namespace ts.tscWatch { const host = createWatchedSystem([file, configFile, libFile]); const watch = createWatchOfConfigFile(configFile.path, host); - checkOutputErrors(host, [ + checkOutputErrorsInitialWithConfigErrors(host, [ getUnknownCompilerOption(watch(), configFile, "foo"), getUnknownCompilerOption(watch(), configFile, "allowJS") - ], /*errorsPosition*/ ExpectedOutputErrorsPosition.BeforeCompilationStarts); + ]); }); it("If config file doesnt have errors, they are not reported", () => { @@ -923,7 +917,7 @@ namespace ts.tscWatch { const host = createWatchedSystem([file, configFile, libFile]); createWatchOfConfigFile(configFile.path, host); - checkOutputErrors(host, emptyArray, /*errorsPosition*/ ExpectedOutputErrorsPosition.AfterCompilationStarting); + checkOutputErrorsInitial(host, emptyArray); }); it("Reports errors when the config file changes", () => { @@ -940,7 +934,7 @@ namespace ts.tscWatch { const host = createWatchedSystem([file, configFile, libFile]); const watch = createWatchOfConfigFile(configFile.path, host); - checkOutputErrors(host, emptyArray, /*errorsPosition*/ ExpectedOutputErrorsPosition.AfterCompilationStarting); + checkOutputErrorsInitial(host, emptyArray); configFile.content = `{ "compilerOptions": { @@ -949,16 +943,16 @@ namespace ts.tscWatch { }`; host.reloadFS([file, configFile, libFile]); host.runQueuedTimeoutCallbacks(); - checkOutputErrors(host, [ + checkOutputErrorsIncremental(host, [ getUnknownCompilerOption(watch(), configFile, "haha") - ], /*errorsPosition*/ ExpectedOutputErrorsPosition.AfterFileChangeDetected); + ]); configFile.content = `{ "compilerOptions": {} }`; host.reloadFS([file, configFile, libFile]); host.runQueuedTimeoutCallbacks(); - checkOutputErrors(host, emptyArray, /*errorsPosition*/ ExpectedOutputErrorsPosition.AfterFileChangeDetected); + checkOutputErrorsIncremental(host, emptyArray); }); it("non-existing directories listed in config file input array should be tolerated without crashing the server", () => { @@ -1046,13 +1040,13 @@ namespace ts.tscWatch { getDiagnosticOfFile(watch().getCompilerOptions().configFile, configFile.content.indexOf('"declaration"'), '"declaration"'.length, Diagnostics.Option_0_cannot_be_specified_with_option_1, "allowJs", "declaration") ]; const intialErrors = errors(); - checkOutputErrors(host, intialErrors, /*errorsPosition*/ ExpectedOutputErrorsPosition.AfterCompilationStarting); + checkOutputErrorsInitial(host, intialErrors); configFile.content = configFileContentWithoutCommentLine; host.reloadFS(files); host.runQueuedTimeoutCallbacks(); const nowErrors = errors(); - checkOutputErrors(host, nowErrors, /*errorsPosition*/ ExpectedOutputErrorsPosition.AfterFileChangeDetected); + checkOutputErrorsIncremental(host, nowErrors); assert.equal(nowErrors[0].start, intialErrors[0].start - configFileContentComment.length); assert.equal(nowErrors[1].start, intialErrors[1].start - configFileContentComment.length); }); @@ -1105,13 +1099,13 @@ namespace ts.tscWatch { noUnusedLocals: true }); checkProgramActualFiles(watch(), files.map(file => file.path)); - checkOutputErrors(host, [], ExpectedOutputErrorsPosition.AfterCompilationStarting); + checkOutputErrorsInitial(host, []); file.content = getFileContent(/*asModule*/ true); host.reloadFS(files); host.runQueuedTimeoutCallbacks(); checkProgramActualFiles(watch(), files.map(file => file.path)); - checkOutputErrors(host, [], ExpectedOutputErrorsPosition.AfterFileChangeDetected); + checkOutputErrorsIncremental(host, []); }); it("watched files when file is deleted and new file is added as part of change", () => { @@ -1800,7 +1794,7 @@ namespace ts.tscWatch { const cannotFindFoo = getDiagnosticOfFileFromProgram(watch(), imported.path, imported.content.indexOf("foo"), "foo".length, Diagnostics.Cannot_find_name_0, "foo"); // ensure that imported file was found - checkOutputErrors(host, [f1IsNotModule, cannotFindFoo], /*errorsPosition*/ ExpectedOutputErrorsPosition.AfterCompilationStarting); + checkOutputErrorsInitial(host, [f1IsNotModule, cannotFindFoo]); const originalFileExists = host.fileExists; { @@ -1816,11 +1810,11 @@ namespace ts.tscWatch { host.runQueuedTimeoutCallbacks(); // ensure file has correct number of errors after edit - checkOutputErrors(host, [ + checkOutputErrorsIncremental(host, [ f1IsNotModule, getDiagnosticOfFileFromProgram(watch(), root.path, newContent.indexOf("var x") + "var ".length, "x".length, Diagnostics.Type_0_is_not_assignable_to_type_1, 1, "string"), cannotFindFoo - ], /*errorsPosition*/ ExpectedOutputErrorsPosition.AfterFileChangeDetected); + ]); } { let fileExistsIsCalled = false; @@ -1840,9 +1834,9 @@ namespace ts.tscWatch { host.runQueuedTimeoutCallbacks(); // ensure file has correct number of errors after edit - checkOutputErrors(host, [ + checkOutputErrorsIncremental(host, [ getDiagnosticModuleNotFoundOfFile(watch(), root, "f2") - ], /*errorsPosition*/ ExpectedOutputErrorsPosition.AfterFileChangeDetected); + ]); assert.isTrue(fileExistsIsCalled); } @@ -1863,7 +1857,7 @@ namespace ts.tscWatch { host.reloadFS(files); host.runQueuedTimeoutCallbacks(); - checkOutputErrors(host, [f1IsNotModule, cannotFindFoo], /*errorsPosition*/ ExpectedOutputErrorsPosition.AfterFileChangeDetected); + checkOutputErrorsIncremental(host, [f1IsNotModule, cannotFindFoo]); assert.isTrue(fileExistsCalled); } }); @@ -1898,16 +1892,16 @@ namespace ts.tscWatch { const watch = createWatchOfFilesAndCompilerOptions([root.path], host, { module: ModuleKind.AMD }); assert.isTrue(fileExistsCalledForBar, "'fileExists' should be called"); - checkOutputErrors(host, [ + checkOutputErrorsInitial(host, [ getDiagnosticModuleNotFoundOfFile(watch(), root, "bar") - ], /*errorsPosition*/ ExpectedOutputErrorsPosition.AfterCompilationStarting); + ]); fileExistsCalledForBar = false; root.content = `import {y} from "bar"`; host.reloadFS(files.concat(imported)); host.runQueuedTimeoutCallbacks(); - checkOutputErrors(host, emptyArray, /*errorsPosition*/ ExpectedOutputErrorsPosition.AfterFileChangeDetected); + checkOutputErrorsIncremental(host, emptyArray); assert.isTrue(fileExistsCalledForBar, "'fileExists' should be called."); }); @@ -1940,20 +1934,20 @@ namespace ts.tscWatch { const watch = createWatchOfFilesAndCompilerOptions([root.path], host, { module: ModuleKind.AMD }); assert.isTrue(fileExistsCalledForBar, "'fileExists' should be called"); - checkOutputErrors(host, emptyArray, /*errorsPosition*/ ExpectedOutputErrorsPosition.AfterCompilationStarting); + checkOutputErrorsInitial(host, emptyArray); fileExistsCalledForBar = false; host.reloadFS(files); host.runQueuedTimeoutCallbacks(); assert.isTrue(fileExistsCalledForBar, "'fileExists' should be called."); - checkOutputErrors(host, [ + checkOutputErrorsIncremental(host, [ getDiagnosticModuleNotFoundOfFile(watch(), root, "bar") - ], /*errorsPosition*/ ExpectedOutputErrorsPosition.AfterFileChangeDetected); + ]); fileExistsCalledForBar = false; host.reloadFS(filesWithImported); host.checkTimeoutQueueLengthAndRun(1); - checkOutputErrors(host, emptyArray, /*errorsPosition*/ ExpectedOutputErrorsPosition.AfterFileChangeDetected); + checkOutputErrorsIncremental(host, emptyArray); assert.isTrue(fileExistsCalledForBar, "'fileExists' should be called."); }); @@ -1988,13 +1982,13 @@ declare module "fs" { const watch = createWatchOfFilesAndCompilerOptions([root.path], host, { }); - checkOutputErrors(host, [ + checkOutputErrorsInitial(host, [ getDiagnosticModuleNotFoundOfFile(watch(), root, "fs") - ], /*errorsPosition*/ ExpectedOutputErrorsPosition.AfterCompilationStarting); + ]); host.reloadFS(filesWithNodeType); host.runQueuedTimeoutCallbacks(); - checkOutputErrors(host, emptyArray, /*errorsPosition*/ ExpectedOutputErrorsPosition.AfterFileChangeDetected); + checkOutputErrorsIncremental(host, emptyArray); }); it("works when included file with ambient module changes", () => { @@ -2030,14 +2024,14 @@ declare module "fs" { const watch = createWatchOfFilesAndCompilerOptions([root.path, file.path], host, {}); - checkOutputErrors(host, [ + checkOutputErrorsInitial(host, [ getDiagnosticModuleNotFoundOfFile(watch(), root, "fs") - ], /*errorsPosition*/ ExpectedOutputErrorsPosition.AfterCompilationStarting); + ]); file.content += fileContentWithFS; host.reloadFS(files); host.runQueuedTimeoutCallbacks(); - checkOutputErrors(host, emptyArray, /*errorsPosition*/ ExpectedOutputErrorsPosition.AfterFileChangeDetected); + checkOutputErrorsIncremental(host, emptyArray); }); it("works when reusing program with files from external library", () => { @@ -2072,7 +2066,7 @@ declare module "fs" { const host = createWatchedSystem(programFiles.concat(configFile), { currentDirectory: "/a/b/projects/myProject/" }); const watch = createWatchOfConfigFile(configFile.path, host); checkProgramActualFiles(watch(), programFiles.map(f => f.path)); - checkOutputErrors(host, emptyArray, /*errorsPosition*/ ExpectedOutputErrorsPosition.AfterCompilationStarting); + checkOutputErrorsInitial(host, emptyArray); const expectedFiles: ExpectedFile[] = [ createExpectedEmittedFile(file1), createExpectedEmittedFile(file2), @@ -2091,7 +2085,7 @@ declare module "fs" { host.reloadFS(programFiles.concat(configFile)); host.runQueuedTimeoutCallbacks(); checkProgramActualFiles(watch(), programFiles.map(f => f.path)); - checkOutputErrors(host, emptyArray, /*errorsPosition*/ ExpectedOutputErrorsPosition.AfterFileChangeDetected); + checkOutputErrorsIncremental(host, emptyArray); verifyExpectedFiles(expectedFiles); @@ -2212,4 +2206,136 @@ declare module "fs" { }); }); }); + + describe("tsc-watch with different polling/non polling options", () => { + it("watchFile using dynamic priority polling", () => { + const projectFolder = "/a/username/project"; + const file1: FileOrFolder = { + path: `${projectFolder}/typescript.ts`, + content: "var z = 10;" + }; + const files = [file1, libFile]; + const environmentVariables = createMap(); + environmentVariables.set("TSC_WATCHFILE", "DynamicPriorityPolling"); + const host = createWatchedSystem(files, { environmentVariables }); + const watch = createWatchOfFilesAndCompilerOptions([file1.path], host); + + const initialProgram = watch(); + verifyProgram(); + + const mediumPollingIntervalThreshold = unchangedPollThresholds[PollingInterval.Medium]; + for (let index = 0; index < mediumPollingIntervalThreshold; index++) { + // Transition libFile and file1 to low priority queue + host.checkTimeoutQueueLengthAndRun(1); + assert.deepEqual(watch(), initialProgram); + } + + // Make a change to file + file1.content = "var zz30 = 100;"; + host.reloadFS(files); + + // This should detect change in the file + host.checkTimeoutQueueLengthAndRun(1); + assert.deepEqual(watch(), initialProgram); + + // Callbacks: medium priority + high priority queue and scheduled program update + host.checkTimeoutQueueLengthAndRun(3); + // During this timeout the file would be detected as unchanged + let fileUnchangeDetected = 1; + const newProgram = watch(); + assert.notStrictEqual(newProgram, initialProgram); + + verifyProgram(); + const outputFile1 = changeExtension(file1.path, ".js"); + assert.isTrue(host.fileExists(outputFile1)); + assert.equal(host.readFile(outputFile1), file1.content + host.newLine); + + const newThreshold = unchangedPollThresholds[PollingInterval.Low] + mediumPollingIntervalThreshold; + for (; fileUnchangeDetected < newThreshold; fileUnchangeDetected++) { + // For high + Medium/low polling interval + host.checkTimeoutQueueLengthAndRun(2); + assert.deepEqual(watch(), newProgram); + } + + // Everything goes in high polling interval queue + host.checkTimeoutQueueLengthAndRun(1); + assert.deepEqual(watch(), newProgram); + + function verifyProgram() { + checkProgramActualFiles(watch(), files.map(f => f.path)); + checkWatchedFiles(host, []); + checkWatchedDirectories(host, [], /*recursive*/ false); + checkWatchedDirectories(host, [], /*recursive*/ true); + } + }); + + describe("tsc-watch when watchDirectories implementation", () => { + function verifyRenamingFileInSubFolder(tscWatchDirectory: TestFSWithWatch.Tsc_WatchDirectory) { + const projectFolder = "/a/username/project"; + const projectSrcFolder = `${projectFolder}/src`; + const configFile: FileOrFolder = { + path: `${projectFolder}/tsconfig.json`, + content: "{}" + }; + const file: FileOrFolder = { + path: `${projectSrcFolder}/file1.ts`, + content: "" + }; + const programFiles = [file, libFile]; + const files = [file, configFile, libFile]; + const environmentVariables = createMap(); + environmentVariables.set("TSC_WATCHDIRECTORY", tscWatchDirectory); + const host = createWatchedSystem(files, { environmentVariables }); + const watch = createWatchOfConfigFile(configFile.path, host); + const projectFolders = [projectFolder, projectSrcFolder, `${projectFolder}/node_modules/@types`]; + // Watching files config file, file, lib file + const expectedWatchedFiles = files.map(f => f.path); + const expectedWatchedDirectories = tscWatchDirectory === TestFSWithWatch.Tsc_WatchDirectory.NonRecursiveWatchDirectory ? projectFolders : emptyArray; + if (tscWatchDirectory === TestFSWithWatch.Tsc_WatchDirectory.WatchFile) { + expectedWatchedFiles.push(...projectFolders); + } + + verifyProgram(checkOutputErrorsInitial); + + // Rename the file: + file.path = file.path.replace("file1.ts", "file2.ts"); + expectedWatchedFiles[0] = file.path; + host.reloadFS(files); + if (tscWatchDirectory === TestFSWithWatch.Tsc_WatchDirectory.DynamicPolling) { + // With dynamic polling the fs change would be detected only by running timeouts + host.runQueuedTimeoutCallbacks(); + } + // Delayed update program + host.runQueuedTimeoutCallbacks(); + verifyProgram(checkOutputErrorsIncremental); + + function verifyProgram(checkOutputErrors: (host: WatchedSystem, errors: ReadonlyArray) => void) { + checkProgramActualFiles(watch(), programFiles.map(f => f.path)); + checkOutputErrors(host, emptyArray); + + const outputFile = changeExtension(file.path, ".js"); + assert(host.fileExists(outputFile)); + assert.equal(host.readFile(outputFile), file.content); + + checkWatchedDirectories(host, emptyArray, /*recursive*/ true); + + // Watching config file, file, lib file and directories + TestFSWithWatch.checkMultiMapEachKeyWithCount("watchedFiles", host.watchedFiles, expectedWatchedFiles, 1); + TestFSWithWatch.checkMultiMapEachKeyWithCount("watchedDirectories", host.watchedDirectories, expectedWatchedDirectories, 1); + } + } + + it("uses watchFile when renaming file in subfolder", () => { + verifyRenamingFileInSubFolder(TestFSWithWatch.Tsc_WatchDirectory.WatchFile); + }); + + it("uses non recursive watchDirectory when renaming file in subfolder", () => { + verifyRenamingFileInSubFolder(TestFSWithWatch.Tsc_WatchDirectory.NonRecursiveWatchDirectory); + }); + + it("uses non recursive dynamic polling when renaming file in subfolder", () => { + verifyRenamingFileInSubFolder(TestFSWithWatch.Tsc_WatchDirectory.DynamicPolling); + }); + }); + }); } diff --git a/src/harness/unittests/tsserverProjectSystem.ts b/src/harness/unittests/tsserverProjectSystem.ts index 1ad8a594c78c5..5dd38ce7a1b93 100644 --- a/src/harness/unittests/tsserverProjectSystem.ts +++ b/src/harness/unittests/tsserverProjectSystem.ts @@ -10,7 +10,7 @@ namespace ts.projectSystem { export import TestServerHost = TestFSWithWatch.TestServerHost; export type FileOrFolder = TestFSWithWatch.FileOrFolder; export import createServerHost = TestFSWithWatch.createServerHost; - export import checkFileNames = TestFSWithWatch.checkFileNames; + export import checkArray = TestFSWithWatch.checkArray; export import libFile = TestFSWithWatch.libFile; export import checkWatchedFiles = TestFSWithWatch.checkWatchedFiles; import checkWatchedDirectories = TestFSWithWatch.checkWatchedDirectories; @@ -355,11 +355,11 @@ namespace ts.projectSystem { } export function checkProjectActualFiles(project: server.Project, expectedFiles: string[]) { - checkFileNames(`${server.ProjectKind[project.projectKind]} project, actual files`, project.getFileNames(), expectedFiles); + checkArray(`${server.ProjectKind[project.projectKind]} project, actual files`, project.getFileNames(), expectedFiles); } function checkProjectRootFiles(project: server.Project, expectedFiles: string[]) { - checkFileNames(`${server.ProjectKind[project.projectKind]} project, rootFileNames`, project.getRootFiles(), expectedFiles); + checkArray(`${server.ProjectKind[project.projectKind]} project, rootFileNames`, project.getRootFiles(), expectedFiles); } function mapCombinedPathsInAncestor(dir: string, path2: string, mapAncestor: (ancestor: string) => boolean) { @@ -392,7 +392,7 @@ namespace ts.projectSystem { } function checkOpenFiles(projectService: server.ProjectService, expectedFiles: FileOrFolder[]) { - checkFileNames("Open files", arrayFrom(projectService.openFiles.keys(), path => projectService.getScriptInfoForPath(path as Path).fileName), expectedFiles.map(file => file.path)); + checkArray("Open files", arrayFrom(projectService.openFiles.keys(), path => projectService.getScriptInfoForPath(path as Path).fileName), expectedFiles.map(file => file.path)); } /** @@ -531,7 +531,7 @@ namespace ts.projectSystem { const project = projectService.inferredProjects[0]; - checkFileNames("inferred project", project.getFileNames(), [appFile.path, libFile.path, moduleFile.path]); + checkArray("inferred project", project.getFileNames(), [appFile.path, libFile.path, moduleFile.path]); const configFileLocations = ["/a/b/c/", "/a/b/", "/a/", "/"]; const configFiles = flatMap(configFileLocations, location => [location + "tsconfig.json", location + "jsconfig.json"]); checkWatchedFiles(host, configFiles.concat(libFile.path, moduleFile.path)); @@ -5663,16 +5663,11 @@ namespace ts.projectSystem { } function verifyCalledOnEachEntry(callback: CalledMaps, expectedKeys: Map) { - const calledMap = calledMaps[callback]; - TestFSWithWatch.verifyMapSize(callback, calledMap, arrayFrom(expectedKeys.keys())); - expectedKeys.forEach((called, name) => { - assert.isTrue(calledMap.has(name), `${callback} is expected to contain ${name}, actual keys: ${arrayFrom(calledMap.keys())}`); - assert.equal(calledMap.get(name).length, called, `${callback} is expected to be called ${called} times with ${name}. Actual entry: ${calledMap.get(name)}`); - }); + TestFSWithWatch.checkMultiMapKeyCount(callback, calledMaps[callback], expectedKeys); } function verifyCalledOnEachEntryNTimes(callback: CalledMaps, expectedKeys: string[], nTimes: number) { - return verifyCalledOnEachEntry(callback, zipToMap(expectedKeys, expectedKeys.map(() => nTimes))); + TestFSWithWatch.checkMultiMapEachKeyWithCount(callback, calledMaps[callback], expectedKeys, nTimes); } function verifyNoHostCalls() { @@ -7009,7 +7004,6 @@ namespace ts.projectSystem { assert.isDefined(projectService.configuredProjects.get(aTsconfig.path)); assert.isDefined(projectService.configuredProjects.get(bTsconfig.path)); - debugger; verifyRenameResponse(session.executeCommandSeq({ command: protocol.CommandTypes.Rename, arguments: { @@ -7438,4 +7432,88 @@ namespace ts.projectSystem { }); }); }); + + describe("watchDirectories implementation", () => { + function verifyCompletionListWithNewFileInSubFolder(tscWatchDirectory: TestFSWithWatch.Tsc_WatchDirectory) { + const projectFolder = "/a/username/project"; + const projectSrcFolder = `${projectFolder}/src`; + const configFile: FileOrFolder = { + path: `${projectFolder}/tsconfig.json`, + content: "{}" + }; + const index: FileOrFolder = { + path: `${projectSrcFolder}/index.ts`, + content: `import {} from "./"` + }; + const file1: FileOrFolder = { + path: `${projectSrcFolder}/file1.ts`, + content: "" + }; + + const files = [index, file1, configFile, libFile]; + const fileNames = files.map(file => file.path); + // All closed files(files other than index), project folder, project/src folder and project/node_modules/@types folder + const expectedWatchedFiles = arrayToMap(fileNames.slice(1), s => s, () => 1); + const expectedWatchedDirectories = createMap(); + const mapOfDirectories = tscWatchDirectory === TestFSWithWatch.Tsc_WatchDirectory.NonRecursiveWatchDirectory ? + expectedWatchedDirectories : + tscWatchDirectory === TestFSWithWatch.Tsc_WatchDirectory.WatchFile ? + expectedWatchedFiles : + createMap(); + // For failed resolution lookup and tsconfig files + mapOfDirectories.set(projectFolder, 2); + // Through above recursive watches + mapOfDirectories.set(projectSrcFolder, 2); + // node_modules/@types folder + mapOfDirectories.set(`${projectFolder}/${nodeModulesAtTypes}`, 1); + const expectedCompletions = ["file1"]; + const completionPosition = index.content.lastIndexOf('"'); + const environmentVariables = createMap(); + environmentVariables.set("TSC_WATCHDIRECTORY", tscWatchDirectory); + const host = createServerHost(files, { environmentVariables }); + const projectService = createProjectService(host); + projectService.openClientFile(index.path); + + const project = projectService.configuredProjects.get(configFile.path); + assert.isDefined(project); + verifyProjectAndCompletions(); + + // Add file2 + const file2: FileOrFolder = { + path: `${projectSrcFolder}/file2.ts`, + content: "" + }; + files.push(file2); + fileNames.push(file2.path); + expectedWatchedFiles.set(file2.path, 1); + expectedCompletions.push("file2"); + host.reloadFS(files); + host.runQueuedTimeoutCallbacks(); + assert.equal(projectService.configuredProjects.get(configFile.path), project); + verifyProjectAndCompletions(); + + function verifyProjectAndCompletions() { + const completions = project.getLanguageService().getCompletionsAtPosition(index.path, completionPosition, { includeExternalModuleExports: false, includeInsertTextCompletions: false }); + checkArray("Completion Entries", completions.entries.map(e => e.name), expectedCompletions); + + checkWatchedDirectories(host, emptyArray, /*recursive*/ true); + + TestFSWithWatch.checkMultiMapKeyCount("watchedFiles", host.watchedFiles, expectedWatchedFiles); + TestFSWithWatch.checkMultiMapKeyCount("watchedDirectories", host.watchedDirectories, expectedWatchedDirectories); + checkProjectActualFiles(project, fileNames); + } + } + + it("uses watchFile when file is added to subfolder, completion list has new file", () => { + verifyCompletionListWithNewFileInSubFolder(TestFSWithWatch.Tsc_WatchDirectory.WatchFile); + }); + + it("uses non recursive watchDirectory when file is added to subfolder, completion list has new file", () => { + verifyCompletionListWithNewFileInSubFolder(TestFSWithWatch.Tsc_WatchDirectory.NonRecursiveWatchDirectory); + }); + + it("uses dynamic polling when file is added to subfolder, completion list has new file", () => { + verifyCompletionListWithNewFileInSubFolder(TestFSWithWatch.Tsc_WatchDirectory.DynamicPolling); + }); + }); } diff --git a/src/harness/virtualFileSystemWithWatch.ts b/src/harness/virtualFileSystemWithWatch.ts index d518ce5e2c473..dd2f123826018 100644 --- a/src/harness/virtualFileSystemWithWatch.ts +++ b/src/harness/virtualFileSystemWithWatch.ts @@ -36,6 +36,7 @@ interface Array {}` currentDirectory?: string; newLine?: string; useWindowsStylePaths?: boolean; + environmentVariables?: Map; } export function createWatchedSystem(fileOrFolderList: ReadonlyArray, params?: TestServerHostCreationParameters): TestServerHost { @@ -48,7 +49,8 @@ interface Array {}` params.currentDirectory || "/", fileOrFolderList, params.newLine, - params.useWindowsStylePaths); + params.useWindowsStylePaths, + params.environmentVariables); return host; } @@ -62,7 +64,8 @@ interface Array {}` params.currentDirectory || "/", fileOrFolderList, params.newLine, - params.useWindowsStylePaths); + params.useWindowsStylePaths, + params.environmentVariables); return host; } @@ -76,6 +79,7 @@ interface Array {}` interface FSEntry { path: Path; fullPath: string; + modifiedTime: Date; } interface File extends FSEntry { @@ -152,10 +156,22 @@ interface Array {}` } } - export function checkFileNames(caption: string, actualFileNames: ReadonlyArray, expectedFileNames: string[]) { - assert.equal(actualFileNames.length, expectedFileNames.length, `${caption}: incorrect actual number of files, expected:\r\n${expectedFileNames.join("\r\n")}\r\ngot: ${actualFileNames.join("\r\n")}`); - for (const f of expectedFileNames) { - assert.equal(true, contains(actualFileNames, f), `${caption}: expected to find ${f} in ${actualFileNames}`); + export function checkMultiMapKeyCount(caption: string, actual: MultiMap, expectedKeys: Map) { + verifyMapSize(caption, actual, arrayFrom(expectedKeys.keys())); + expectedKeys.forEach((count, name) => { + assert.isTrue(actual.has(name), `${caption}: expected to contain ${name}, actual keys: ${arrayFrom(actual.keys())}`); + assert.equal(actual.get(name).length, count, `${caption}: Expected to be have ${count} entries for ${name}. Actual entry: ${JSON.stringify(actual.get(name))}`); + }); + } + + export function checkMultiMapEachKeyWithCount(caption: string, actual: MultiMap, expectedKeys: ReadonlyArray, count: number) { + return checkMultiMapKeyCount(caption, actual, arrayToMap(expectedKeys, s => s, () => count)); + } + + export function checkArray(caption: string, actual: ReadonlyArray, expected: ReadonlyArray) { + assert.equal(actual.length, expected.length, `${caption}: incorrect actual number of files, expected:\r\n${expected.join("\r\n")}\r\ngot: ${actual.join("\r\n")}`); + for (const f of expected) { + assert.equal(true, contains(actual, f), `${caption}: expected to find ${f} in ${actual}`); } } @@ -254,6 +270,12 @@ interface Array {}` invokeFileDeleteCreateAsPartInsteadOfChange: boolean; } + export enum Tsc_WatchDirectory { + WatchFile = "RecursiveDirectoryUsingFsWatchFile", + NonRecursiveWatchDirectory = "RecursiveDirectoryUsingNonRecursiveWatchDirectory", + DynamicPolling = "RecursiveDirectoryUsingDynamicPriorityPolling" + } + export class TestServerHost implements server.ServerHost, FormatDiagnosticsHost, ModuleResolutionHost { args: string[] = []; @@ -271,13 +293,47 @@ interface Array {}` readonly watchedFiles = createMultiMap(); private readonly executingFilePath: string; private readonly currentDirectory: string; + private readonly dynamicPriorityWatchFile: HostWatchFile; + private readonly customRecursiveWatchDirectory: HostWatchDirectory | undefined; - constructor(public withSafeList: boolean, public useCaseSensitiveFileNames: boolean, executingFilePath: string, currentDirectory: string, fileOrFolderList: ReadonlyArray, public readonly newLine = "\n", public readonly useWindowsStylePath?: boolean) { + constructor(public withSafeList: boolean, public useCaseSensitiveFileNames: boolean, executingFilePath: string, currentDirectory: string, fileOrFolderList: ReadonlyArray, public readonly newLine = "\n", public readonly useWindowsStylePath?: boolean, private readonly environmentVariables?: Map) { this.getCanonicalFileName = createGetCanonicalFileName(useCaseSensitiveFileNames); this.toPath = s => toPath(s, currentDirectory, this.getCanonicalFileName); this.executingFilePath = this.getHostSpecificPath(executingFilePath); this.currentDirectory = this.getHostSpecificPath(currentDirectory); this.reloadFS(fileOrFolderList); + this.dynamicPriorityWatchFile = this.environmentVariables && this.environmentVariables.get("TSC_WATCHFILE") === "DynamicPriorityPolling" ? + createDynamicPriorityPollingWatchFile(this) : + undefined; + const tscWatchDirectory = this.environmentVariables && this.environmentVariables.get("TSC_WATCHDIRECTORY") as Tsc_WatchDirectory; + if (tscWatchDirectory === Tsc_WatchDirectory.WatchFile) { + const watchDirectory: HostWatchDirectory = (directory, cb) => this.watchFile(directory, () => cb(directory), PollingInterval.Medium); + this.customRecursiveWatchDirectory = createRecursiveDirectoryWatcher({ + directoryExists: path => this.directoryExists(path), + getAccessileSortedChildDirectories: path => this.getDirectories(path), + filePathComparer: this.useCaseSensitiveFileNames ? compareStringsCaseSensitive : compareStringsCaseInsensitive, + watchDirectory + }); + } + else if (tscWatchDirectory === Tsc_WatchDirectory.NonRecursiveWatchDirectory) { + const watchDirectory: HostWatchDirectory = (directory, cb) => this.watchDirectory(directory, fileName => cb(fileName), /*recursive*/ false); + this.customRecursiveWatchDirectory = createRecursiveDirectoryWatcher({ + directoryExists: path => this.directoryExists(path), + getAccessileSortedChildDirectories: path => this.getDirectories(path), + filePathComparer: this.useCaseSensitiveFileNames ? compareStringsCaseSensitive : compareStringsCaseInsensitive, + watchDirectory + }); + } + else if (tscWatchDirectory === Tsc_WatchDirectory.DynamicPolling) { + const watchFile = createDynamicPriorityPollingWatchFile(this); + const watchDirectory: HostWatchDirectory = (directory, cb) => watchFile(directory, () => cb(directory), PollingInterval.Medium); + this.customRecursiveWatchDirectory = createRecursiveDirectoryWatcher({ + directoryExists: path => this.directoryExists(path), + getAccessileSortedChildDirectories: path => this.getDirectories(path), + filePathComparer: this.useCaseSensitiveFileNames ? compareStringsCaseSensitive : compareStringsCaseInsensitive, + watchDirectory + }); + } } getNewLine() { @@ -325,6 +381,8 @@ interface Array {}` } else { currentEntry.content = fileOrDirectory.content; + currentEntry.modifiedTime = new Date(); + this.fs.get(getDirectoryPath(currentEntry.path)).modifiedTime = new Date(); if (options && options.invokeDirectoryWatcherInsteadOfFileChanged) { this.invokeDirectoryWatcher(getDirectoryPath(currentEntry.fullPath), currentEntry.fullPath); } @@ -348,6 +406,7 @@ interface Array {}` } else { // Folder update: Nothing to do. + currentEntry.modifiedTime = new Date(); } } } @@ -446,14 +505,13 @@ interface Array {}` private addFileOrFolderInFolder(folder: Folder, fileOrDirectory: File | Folder | SymLink, ignoreWatch?: boolean) { folder.entries.push(fileOrDirectory); + folder.modifiedTime = new Date(); this.fs.set(fileOrDirectory.path, fileOrDirectory); if (ignoreWatch) { return; } - if (isFile(fileOrDirectory) || isSymLink(fileOrDirectory)) { - this.invokeFileWatcher(fileOrDirectory.fullPath, FileWatcherEventKind.Created); - } + this.invokeFileWatcher(fileOrDirectory.fullPath, FileWatcherEventKind.Created); this.invokeDirectoryWatcher(folder.fullPath, fileOrDirectory.fullPath); } @@ -462,14 +520,13 @@ interface Array {}` const baseFolder = this.fs.get(basePath) as Folder; if (basePath !== fileOrDirectory.path) { Debug.assert(!!baseFolder); + baseFolder.modifiedTime = new Date(); filterMutate(baseFolder.entries, entry => entry !== fileOrDirectory); } this.fs.delete(fileOrDirectory.path); - if (isFile(fileOrDirectory) || isSymLink(fileOrDirectory)) { - this.invokeFileWatcher(fileOrDirectory.fullPath, FileWatcherEventKind.Deleted); - } - else { + this.invokeFileWatcher(fileOrDirectory.fullPath, FileWatcherEventKind.Deleted); + if (isFolder(fileOrDirectory)) { Debug.assert(fileOrDirectory.entries.length === 0 || isRenaming); const relativePath = this.getRelativePathToDirectory(fileOrDirectory.fullPath, fileOrDirectory.fullPath); // Invoke directory and recursive directory watcher for the folder @@ -503,6 +560,8 @@ interface Array {}` */ private invokeDirectoryWatcher(folderFullPath: string, fileName: string) { const relativePath = this.getRelativePathToDirectory(folderFullPath, fileName); + // Folder is changed when the directory watcher is invoked + invokeWatcherCallbacks(this.watchedFiles.get(this.toPath(folderFullPath)), ({ cb, fileName }) => cb(fileName, FileWatcherEventKind.Changed)); invokeWatcherCallbacks(this.watchedDirectories.get(this.toPath(folderFullPath)), cb => this.directoryCallback(cb, relativePath)); this.invokeRecursiveDirectoryWatcher(folderFullPath, fileName); } @@ -523,32 +582,32 @@ interface Array {}` } } - private toFile(fileOrDirectory: FileOrFolder): File { - const fullPath = getNormalizedAbsolutePath(fileOrDirectory.path, this.currentDirectory); + private toFsEntry(path: string): FSEntry { + const fullPath = getNormalizedAbsolutePath(path, this.currentDirectory); return { path: this.toPath(fullPath), - content: fileOrDirectory.content, fullPath, - fileSize: fileOrDirectory.fileSize + modifiedTime: new Date() }; } + private toFile(fileOrDirectory: FileOrFolder): File { + const file = this.toFsEntry(fileOrDirectory.path) as File; + file.content = fileOrDirectory.content; + file.fileSize = fileOrDirectory.fileSize; + return file; + } + private toSymLink(fileOrDirectory: FileOrFolder): SymLink { - const fullPath = getNormalizedAbsolutePath(fileOrDirectory.path, this.currentDirectory); - return { - path: this.toPath(fullPath), - fullPath, - symLink: getNormalizedAbsolutePath(fileOrDirectory.symLink, getDirectoryPath(fullPath)) - }; + const symLink = this.toFsEntry(fileOrDirectory.path) as SymLink; + symLink.symLink = getNormalizedAbsolutePath(fileOrDirectory.symLink, getDirectoryPath(symLink.fullPath)); + return symLink; } private toFolder(path: string): Folder { - const fullPath = getNormalizedAbsolutePath(path, this.currentDirectory); - return { - path: this.toPath(fullPath), - entries: [], - fullPath - }; + const folder = this.toFsEntry(path) as Folder; + folder.entries = []; + return folder; } private getRealFsEntry(isFsEntry: (fsEntry: FSEntry) => fsEntry is T, path: Path, fsEntry = this.fs.get(path)): T | undefined { @@ -594,6 +653,12 @@ interface Array {}` return !!this.getRealFile(path); } + getModifiedTime(s: string) { + const path = this.toFullPath(s); + const fsEntry = this.fs.get(path); + return fsEntry && fsEntry.modifiedTime; + } + readFile(s: string): string { const fsEntry = this.getRealFile(this.toFullPath(s)); return fsEntry ? fsEntry.content : undefined; @@ -646,6 +711,9 @@ interface Array {}` } watchDirectory(directoryName: string, cb: DirectoryWatcherCallback, recursive: boolean): FileWatcher { + if (recursive && this.customRecursiveWatchDirectory) { + return this.customRecursiveWatchDirectory(directoryName, cb, /*recursive*/ true); + } const path = this.toFullPath(directoryName); const map = recursive ? this.watchedDirectoriesRecursive : this.watchedDirectories; const callback: TestDirectoryWatcher = { @@ -662,7 +730,11 @@ interface Array {}` return Harness.mockHash(s); } - watchFile(fileName: string, cb: FileWatcherCallback) { + watchFile(fileName: string, cb: FileWatcherCallback, pollingInterval: number) { + if (this.dynamicPriorityWatchFile) { + return this.dynamicPriorityWatchFile(fileName, cb, pollingInterval); + } + const path = this.toFullPath(fileName); const callback: TestFileWatcher = { fileName, cb }; this.watchedFiles.add(path, callback); @@ -701,7 +773,7 @@ interface Array {}` this.timeoutCallbacks.invoke(timeoutId); } catch (e) { - if (e.message === this.existMessage) { + if (e.message === this.exitMessage) { return; } throw e; @@ -779,15 +851,17 @@ interface Array {}` return realFullPath; } - readonly existMessage = "System Exit"; + readonly exitMessage = "System Exit"; exitCode: number; readonly resolvePath = (s: string) => s; readonly getExecutingFilePath = () => this.executingFilePath; readonly getCurrentDirectory = () => this.currentDirectory; exit(exitCode?: number) { this.exitCode = exitCode; - throw new Error(this.existMessage); + throw new Error(this.exitMessage); + } + getEnvironmentVariable(name: string) { + return this.environmentVariables && this.environmentVariables.get(name); } - readonly getEnvironmentVariable = notImplemented; } } diff --git a/src/server/editorServices.ts b/src/server/editorServices.ts index 6c493bd35a197..c46e2fa68752a 100644 --- a/src/server/editorServices.ts +++ b/src/server/editorServices.ts @@ -311,9 +311,9 @@ namespace ts.server { typesMapLocation?: string; } - type WatchFile = (host: ServerHost, file: string, cb: FileWatcherCallback, watchType: WatchType, project?: Project) => FileWatcher; - type WatchFilePath = (host: ServerHost, file: string, cb: FilePathWatcherCallback, path: Path, watchType: WatchType, project?: Project) => FileWatcher; - type WatchDirectory = (host: ServerHost, directory: string, cb: DirectoryWatcherCallback, flags: WatchDirectoryFlags, watchType: WatchType, project?: Project) => FileWatcher; + function getDetailWatchInfo(watchType: WatchType, project: Project | undefined) { + return `Project: ${project ? project.getProjectName() : ""} WatchType: ${watchType}`; + } export class ProjectService { @@ -401,11 +401,7 @@ namespace ts.server { private readonly seenProjects = createMap(); /*@internal*/ - readonly watchFile: WatchFile; - /*@internal*/ - readonly watchFilePath: WatchFilePath; - /*@internal*/ - readonly watchDirectory: WatchDirectory; + readonly watchFactory: WatchFactory; constructor(opts: ProjectServiceOptions) { this.host = opts.host; @@ -447,26 +443,10 @@ namespace ts.server { }; this.documentRegistry = createDocumentRegistry(this.host.useCaseSensitiveFileNames, this.currentDirectory); - if (this.logger.hasLevel(LogLevel.verbose)) { - this.watchFile = (host, file, cb, watchType, project) => addFileWatcherWithLogging(host, file, cb, this.createWatcherLog(watchType, project)); - this.watchFilePath = (host, file, cb, path, watchType, project) => addFilePathWatcherWithLogging(host, file, cb, path, this.createWatcherLog(watchType, project)); - this.watchDirectory = (host, dir, cb, flags, watchType, project) => addDirectoryWatcherWithLogging(host, dir, cb, flags, this.createWatcherLog(watchType, project)); - } - else if (this.logger.loggingEnabled()) { - this.watchFile = (host, file, cb, watchType, project) => addFileWatcherWithOnlyTriggerLogging(host, file, cb, this.createWatcherLog(watchType, project)); - this.watchFilePath = (host, file, cb, path, watchType, project) => addFilePathWatcherWithOnlyTriggerLogging(host, file, cb, path, this.createWatcherLog(watchType, project)); - this.watchDirectory = (host, dir, cb, flags, watchType, project) => addDirectoryWatcherWithOnlyTriggerLogging(host, dir, cb, flags, this.createWatcherLog(watchType, project)); - } - else { - this.watchFile = addFileWatcher; - this.watchFilePath = addFilePathWatcher; - this.watchDirectory = addDirectoryWatcher; - } - } - - private createWatcherLog(watchType: WatchType, project: Project | undefined): (s: string) => void { - const detailedInfo = ` Project: ${project ? project.getProjectName() : ""} WatchType: ${watchType}`; - return s => this.logger.info(s + detailedInfo); + const watchLogLevel = this.logger.hasLevel(LogLevel.verbose) ? WatchLogLevel.Verbose : + this.logger.loggingEnabled() ? WatchLogLevel.TriggerOnly : WatchLogLevel.None; + const log: (s: string) => void = watchLogLevel !== WatchLogLevel.None ? (s => this.logger.info(s)) : noop; + this.watchFactory = getWatchFactory(watchLogLevel, log, getDetailWatchInfo); } toPath(fileName: string) { @@ -714,8 +694,8 @@ namespace ts.server { return formatCodeSettings || this.hostConfiguration.formatCodeOptions; } - private onSourceFileChanged(fileName: NormalizedPath, eventKind: FileWatcherEventKind) { - const info = this.getScriptInfoForNormalizedPath(fileName); + private onSourceFileChanged(fileName: string, eventKind: FileWatcherEventKind, path: Path) { + const info = this.getScriptInfoForPath(path); if (!info) { this.logger.msg(`Error: got watch notification for unknown file: ${fileName}`); } @@ -759,7 +739,7 @@ namespace ts.server { */ /*@internal*/ watchWildcardDirectory(directory: Path, flags: WatchDirectoryFlags, project: ConfiguredProject) { - return this.watchDirectory( + return this.watchFactory.watchDirectory( this.host, directory, fileOrDirectory => { @@ -1097,10 +1077,11 @@ namespace ts.server { canonicalConfigFilePath: string, configFileExistenceInfo: ConfigFileExistenceInfo ) { - configFileExistenceInfo.configFileWatcherForRootOfInferredProject = this.watchFile( + configFileExistenceInfo.configFileWatcherForRootOfInferredProject = this.watchFactory.watchFile( this.host, configFileName, (_filename, eventKind) => this.onConfigFileChangeForOpenScriptInfo(configFileName, eventKind), + PollingInterval.High, WatchType.ConfigFileForInferredRoot ); this.logConfigFileWatchUpdate(configFileName, canonicalConfigFilePath, configFileExistenceInfo, ConfigFileWatcherStatus.UpdatedCallback); @@ -1481,10 +1462,11 @@ namespace ts.server { project.configFileSpecs = configFileSpecs; // TODO: We probably should also watch the configFiles that are extended - project.configFileWatcher = this.watchFile( + project.configFileWatcher = this.watchFactory.watchFile( this.host, configFileName, (_fileName, eventKind) => this.onConfigChangedForConfiguredProject(project, eventKind), + PollingInterval.High, WatchType.ConfigFilePath, project ); @@ -1745,10 +1727,12 @@ namespace ts.server { // do not watch files with mixed content - server doesn't know how to interpret it if (!info.isDynamicOrHasMixedContent()) { const { fileName } = info; - info.fileWatcher = this.watchFile( + info.fileWatcher = this.watchFactory.watchFilePath( this.host, fileName, - (_fileName, eventKind) => this.onSourceFileChanged(fileName, eventKind), + (fileName, eventKind, path) => this.onSourceFileChanged(fileName, eventKind, path), + PollingInterval.Medium, + info.path, WatchType.ClosedScriptInfo ); } diff --git a/src/server/project.ts b/src/server/project.ts index e155466a10e6c..c7239fdc94d66 100644 --- a/src/server/project.ts +++ b/src/server/project.ts @@ -398,7 +398,7 @@ namespace ts.server { /*@internal*/ watchDirectoryOfFailedLookupLocation(directory: string, cb: DirectoryWatcherCallback, flags: WatchDirectoryFlags) { - return this.projectService.watchDirectory( + return this.projectService.watchFactory.watchDirectory( this.projectService.host, directory, cb, @@ -415,7 +415,7 @@ namespace ts.server { /*@internal*/ watchTypeRootsDirectory(directory: string, cb: DirectoryWatcherCallback, flags: WatchDirectoryFlags) { - return this.projectService.watchDirectory( + return this.projectService.watchFactory.watchDirectory( this.projectService.host, directory, cb, @@ -907,7 +907,7 @@ namespace ts.server { const oldExternalFiles = this.externalFiles || emptyArray as SortedReadonlyArray; this.externalFiles = this.getExternalFiles(); - enumerateInsertsAndDeletes(this.externalFiles, oldExternalFiles, + enumerateInsertsAndDeletes(this.externalFiles, oldExternalFiles, compareStringsCaseSensitive, // Ensure a ScriptInfo is created for new external files. This is performed indirectly // by the LSHost for files in the program when the program is retrieved above but // the program doesn't contain external files so this must be done explicitly. @@ -915,8 +915,7 @@ namespace ts.server { const scriptInfo = this.projectService.getOrCreateScriptInfoNotOpenedByClient(inserted, this.currentDirectory, this.directoryStructureHost); scriptInfo.attachToProject(this); }, - removed => this.detachScriptInfoFromProject(removed), - compareStringsCaseSensitive + removed => this.detachScriptInfoFromProject(removed) ); const elapsed = timestamp() - start; this.writeLog(`Finishing updateGraphWorker: Project: ${this.getProjectName()} Version: ${this.getProjectVersion()} structureChanged: ${hasChanges} Elapsed: ${elapsed}ms`); @@ -932,7 +931,7 @@ namespace ts.server { } private addMissingFileWatcher(missingFilePath: Path) { - const fileWatcher = this.projectService.watchFile( + const fileWatcher = this.projectService.watchFactory.watchFile( this.projectService.host, missingFilePath, (fileName, eventKind) => { @@ -948,6 +947,7 @@ namespace ts.server { this.projectService.delayUpdateProjectGraphAndEnsureProjectStructureForOpenFiles(this); } }, + PollingInterval.Medium, WatchType.MissingFilePath, this ); diff --git a/src/server/server.ts b/src/server/server.ts index f70280c34a3de..16e2380f276eb 100644 --- a/src/server/server.ts +++ b/src/server/server.ts @@ -685,11 +685,11 @@ namespace ts.server { return; } - fs.stat(watchedFile.fileName, (err: any, stats: any) => { + fs.stat(watchedFile.fileName, (err, stats) => { if (err) { if (err.code === "ENOENT") { if (watchedFile.mtime.getTime() !== 0) { - watchedFile.mtime = new Date(0); + watchedFile.mtime = missingFileModifiedTime; watchedFile.callback(watchedFile.fileName, FileWatcherEventKind.Deleted); } } @@ -698,17 +698,7 @@ namespace ts.server { } } else { - const oldTime = watchedFile.mtime.getTime(); - const newTime = stats.mtime.getTime(); - if (oldTime !== newTime) { - watchedFile.mtime = stats.mtime; - const eventKind = oldTime === 0 - ? FileWatcherEventKind.Created - : newTime === 0 - ? FileWatcherEventKind.Deleted - : FileWatcherEventKind.Changed; - watchedFile.callback(watchedFile.fileName, eventKind); - } + onWatchedFileStat(watchedFile, stats.mtime); } }); } @@ -742,7 +732,7 @@ namespace ts.server { callback, mtime: sys.fileExists(fileName) ? getModifiedTime(fileName) - : new Date(0) // Any subsequent modification will occur after this time + : missingFileModifiedTime // Any subsequent modification will occur after this time }; watchedFiles.push(file); diff --git a/src/server/utilities.ts b/src/server/utilities.ts index 2b38de6fa5506..e2329b868e33f 100644 --- a/src/server/utilities.ts +++ b/src/server/utilities.ts @@ -264,36 +264,6 @@ namespace ts.server { return index === 0 || value !== array[index - 1]; } - export function enumerateInsertsAndDeletes(newItems: SortedReadonlyArray, oldItems: SortedReadonlyArray, inserted: (newItem: T) => void, deleted: (oldItem: T) => void, comparer: Comparer) { - let newIndex = 0; - let oldIndex = 0; - const newLen = newItems.length; - const oldLen = oldItems.length; - while (newIndex < newLen && oldIndex < oldLen) { - const newItem = newItems[newIndex]; - const oldItem = oldItems[oldIndex]; - const compareResult = comparer(newItem, oldItem); - if (compareResult === Comparison.LessThan) { - inserted(newItem); - newIndex++; - } - else if (compareResult === Comparison.GreaterThan) { - deleted(oldItem); - oldIndex++; - } - else { - newIndex++; - oldIndex++; - } - } - while (newIndex < newLen) { - inserted(newItems[newIndex++]); - } - while (oldIndex < oldLen) { - deleted(oldItems[oldIndex++]); - } - } - /* @internal */ export function indent(str: string): string { return "\n " + str; diff --git a/tests/baselines/reference/api/tsserverlibrary.d.ts b/tests/baselines/reference/api/tsserverlibrary.d.ts index 90ed78047e6a6..d6fd53fd65c58 100644 --- a/tests/baselines/reference/api/tsserverlibrary.d.ts +++ b/tests/baselines/reference/api/tsserverlibrary.d.ts @@ -2852,11 +2852,6 @@ declare namespace ts { } type FileWatcherCallback = (fileName: string, eventKind: FileWatcherEventKind) => void; type DirectoryWatcherCallback = (fileName: string) => void; - interface WatchedFile { - fileName: string; - callback: FileWatcherCallback; - mtime?: Date; - } interface System { args: string[]; newLine: string; @@ -7829,7 +7824,6 @@ declare namespace ts.server { /** Tracks projects that we have already sent telemetry for. */ private readonly seenProjects; constructor(opts: ProjectServiceOptions); - private createWatcherLog(watchType, project); toPath(fileName: string): Path; private loadTypesMap(); updateTypingsForProject(response: SetTypings | InvalidateCachedTypings | PackageInstalledResponse): void; @@ -7851,7 +7845,7 @@ declare namespace ts.server { private ensureProjectStructuresUptoDate(); private updateProjectIfDirty(project); getFormatCodeOptions(file?: NormalizedPath): FormatCodeSettings; - private onSourceFileChanged(fileName, eventKind); + private onSourceFileChanged(fileName, eventKind, path); private handleDeletedFile(info); private onConfigChangedForConfiguredProject(project, eventKind); /** diff --git a/tests/baselines/reference/api/typescript.d.ts b/tests/baselines/reference/api/typescript.d.ts index c1f088c058bf9..d2568e0f34c7a 100644 --- a/tests/baselines/reference/api/typescript.d.ts +++ b/tests/baselines/reference/api/typescript.d.ts @@ -2852,11 +2852,6 @@ declare namespace ts { } type FileWatcherCallback = (fileName: string, eventKind: FileWatcherEventKind) => void; type DirectoryWatcherCallback = (fileName: string) => void; - interface WatchedFile { - fileName: string; - callback: FileWatcherCallback; - mtime?: Date; - } interface System { args: string[]; newLine: string;