diff --git a/src/compiler/commandLineParser.ts b/src/compiler/commandLineParser.ts index 11062983aaa..6af35ee30cc 100644 --- a/src/compiler/commandLineParser.ts +++ b/src/compiler/commandLineParser.ts @@ -389,7 +389,7 @@ namespace ts { catch (e) { return { error: createCompilerDiagnostic(Diagnostics.Cannot_read_file_0_Colon_1, fileName, e.message) }; } - return parseConfigFileText(fileName, text); + return parseConfigFileTextToJson(fileName, text); } /** @@ -397,7 +397,7 @@ namespace ts { * @param fileName The path to the config file * @param jsonText The text of the config file */ - export function parseConfigFileText(fileName: string, jsonText: string): { config?: any; error?: Diagnostic } { + export function parseConfigFileTextToJson(fileName: string, jsonText: string): { config?: any; error?: Diagnostic } { try { return { config: /\S/.test(jsonText) ? JSON.parse(jsonText) : {} }; } @@ -412,7 +412,7 @@ namespace ts { * @param basePath A root directory to resolve relative path entries in the config * file to. e.g. outDir */ - export function parseConfigFile(json: any, host: ParseConfigHost, basePath: string): ParsedCommandLine { + export function parseJsonConfigFileContent(json: any, host: ParseConfigHost, basePath: string): ParsedCommandLine { let errors: Diagnostic[] = []; return { diff --git a/src/compiler/core.ts b/src/compiler/core.ts index a4c9a987267..0f7c09b4756 100644 --- a/src/compiler/core.ts +++ b/src/compiler/core.ts @@ -833,24 +833,15 @@ namespace ts { } } - export function doTwoArraysHaveTheSameElements(array1: Array, array2: Array): Boolean { + export function arrayStructurallyIsEqualTo(array1: Array, array2: Array): boolean { if (!array1 || !array2) { return false; } - if (array1.length != array2.length) { + if (array1.length !== array2.length) { return false; } - array1 = array1.sort(); - array2 = array2.sort(); - - for (let i = 0; i < array1.length; i++) { - if (array1[i] != array2[i]) { - return false; - } - } - - return true; + return arrayIsEqualTo(array1.sort(), array2.sort()); } } diff --git a/src/compiler/sys.ts b/src/compiler/sys.ts index d191d406c52..4bc9af8d30e 100644 --- a/src/compiler/sys.ts +++ b/src/compiler/sys.ts @@ -199,32 +199,19 @@ namespace ts { const _path = require("path"); const _os = require("os"); - class WatchedFileSet { - private watchedFiles: WatchedFile[] = []; - private nextFileToCheck = 0; - private watchTimer: any; + // average async stat takes about 30 microseconds + // set chunk size to do 30 files in < 1 millisecond + function createWatchedFileSet(interval = 2500, chunkSize = 30) { + let watchedFiles: WatchedFile[] = []; + let nextFileToCheck = 0; + let watchTimer: any; - // average async stat takes about 30 microseconds - // set chunk size to do 30 files in < 1 millisecond - constructor(public interval = 2500, public chunkSize = 30) { - } - - private static copyListRemovingItem(item: T, list: T[]) { - let copiedList: T[] = []; - for (var i = 0, len = list.length; i < len; i++) { - if (list[i] != item) { - copiedList.push(list[i]); - } - } - return copiedList; - } - - private static getModifiedTime(fileName: string): Date { + function getModifiedTime(fileName: string): Date { return _fs.statSync(fileName).mtime; } - private poll(checkedIndex: number) { - let watchedFile = this.watchedFiles[checkedIndex]; + function poll(checkedIndex: number) { + let watchedFile = watchedFiles[checkedIndex]; if (!watchedFile) { return; } @@ -234,7 +221,7 @@ namespace ts { watchedFile.callback(watchedFile.fileName); } else if (watchedFile.mtime.getTime() !== stats.mtime.getTime()) { - watchedFile.mtime = WatchedFileSet.getModifiedTime(watchedFile.fileName); + watchedFile.mtime = getModifiedTime(watchedFile.fileName); watchedFile.callback(watchedFile.fileName, watchedFile.mtime.getTime() === 0); } }); @@ -243,42 +230,50 @@ namespace ts { // this implementation uses polling and // stat due to inconsistencies of fs.watch // and efficiency of stat on modern filesystems - private startWatchTimer() { - this.watchTimer = setInterval(() => { + function startWatchTimer() { + watchTimer = setInterval(() => { let count = 0; - let nextToCheck = this.nextFileToCheck; + let nextToCheck = nextFileToCheck; let firstCheck = -1; - while ((count < this.chunkSize) && (nextToCheck !== firstCheck)) { - this.poll(nextToCheck); + while ((count < chunkSize) && (nextToCheck !== firstCheck)) { + poll(nextToCheck); if (firstCheck < 0) { firstCheck = nextToCheck; } nextToCheck++; - if (nextToCheck === this.watchedFiles.length) { + if (nextToCheck === watchedFiles.length) { nextToCheck = 0; } count++; } - this.nextFileToCheck = nextToCheck; - }, this.interval); + nextFileToCheck = nextToCheck; + }, interval); } - addFile(fileName: string, callback: (fileName: string, removed?: boolean) => void): WatchedFile { + function addFile(fileName: string, callback: (fileName: string, removed?: boolean) => void): WatchedFile { let file: WatchedFile = { fileName, callback, - mtime: WatchedFileSet.getModifiedTime(fileName) + mtime: getModifiedTime(fileName) }; - this.watchedFiles.push(file); - if (this.watchedFiles.length === 1) { - this.startWatchTimer(); + watchedFiles.push(file); + if (watchedFiles.length === 1) { + startWatchTimer(); } return file; } - removeFile(file: WatchedFile) { - this.watchedFiles = WatchedFileSet.copyListRemovingItem(file, this.watchedFiles); + function removeFile(file: WatchedFile) { + watchedFiles = copyListRemovingItem(file, watchedFiles); + } + + return { + getModifiedTime: getModifiedTime, + poll: poll, + startWatchTimer: startWatchTimer, + addFile: addFile, + removeFile: removeFile } } @@ -295,7 +290,7 @@ namespace ts { // changes for large reference sets? If so, do we want // to increase the chunk size or decrease the interval // time dynamically to match the large reference set? - let watchedFileSet = new WatchedFileSet(); + let watchedFileSet = createWatchedFileSet(); function isNode4OrLater(): Boolean { return parseInt(process.version.charAt(1)) >= 4; @@ -417,7 +412,7 @@ namespace ts { // 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") { + if (eventName === "rename") { // When deleting a file, the passed baseFileName is null callback(!relativeFileName ? relativeFileName : normalizePath(ts.combinePaths(path, relativeFileName))); }; diff --git a/src/compiler/tsc.ts b/src/compiler/tsc.ts index 9a5e1c15294..9d79d435a04 100644 --- a/src/compiler/tsc.ts +++ b/src/compiler/tsc.ts @@ -147,15 +147,17 @@ namespace ts { export function executeCommandLine(args: string[]): void { let commandLine = parseCommandLine(args); - let configFileName: string; // Configuration file name (if any) - let configFileWatcher: FileWatcher; // Configuration file watcher - let directoryWatcher: FileWatcher; // Directory watcher to monitor source file addition/removal - let cachedProgram: Program; // Program cached from last compilation - let rootFileNames: string[]; // Root fileNames for compilation - let compilerOptions: CompilerOptions; // Compiler options for compilation - let compilerHost: CompilerHost; // Compiler host - let hostGetSourceFile: typeof compilerHost.getSourceFile; // getSourceFile method from default host - let timerHandle: number; // Handle for 0.25s wait timer + let configFileName: string; // Configuration file name (if any) + let cachedConfigFileText: string; // Cached configuration file text, used for reparsing (if any) + let configFileWatcher: FileWatcher; // Configuration file watcher + let directoryWatcher: FileWatcher; // Directory watcher to monitor source file addition/removal + let cachedProgram: Program; // Program cached from last compilation + let rootFileNames: string[]; // Root fileNames for compilation + let compilerOptions: CompilerOptions; // Compiler options for compilation + let compilerHost: CompilerHost; // Compiler host + let hostGetSourceFile: typeof compilerHost.getSourceFile; // getSourceFile method from default host + let timerHandleForRecompilation: number; // Handle for 0.25s wait timer to trigger recompilation + let timerHandleForDirectoryChanges: number; // Handle for 0.25s wait timer to trigger directory change handler if (commandLine.options.locale) { if (!isJSONSupported()) { @@ -232,16 +234,22 @@ namespace ts { performCompilation(); - function configFileToParsedCommandLine(configFilename: string): ParsedCommandLine { - let result = readConfigFile(configFileName, sys.readFile); - if (result.error) { - reportWatchDiagnostic(result.error); - sys.exit(ExitStatus.DiagnosticsPresent_OutputsSkipped); - return; + function parseConfigFile(): ParsedCommandLine { + if (!cachedConfigFileText) { + try { + cachedConfigFileText = sys.readFile(configFileName); + } + catch (e) { + let error = createCompilerDiagnostic(Diagnostics.Cannot_read_file_0_Colon_1, configFileName, e.message); + reportWatchDiagnostic(error); + sys.exit(ExitStatus.DiagnosticsPresent_OutputsSkipped); + return; + } } + let result = parseConfigFileTextToJson(configFileName, cachedConfigFileText); let configObject = result.config; - let configParseResult = parseConfigFile(configObject, sys, getDirectoryPath(configFileName)); + let configParseResult = parseJsonConfigFileContent(configObject, sys, getDirectoryPath(configFileName)); if (configParseResult.errors.length > 0) { reportDiagnostics(configParseResult.errors); sys.exit(ExitStatus.DiagnosticsPresent_OutputsSkipped); @@ -255,7 +263,7 @@ namespace ts { if (!cachedProgram) { if (configFileName) { - let configParseResult = configFileToParsedCommandLine(configFileName); + let configParseResult = parseConfigFile(); rootFileNames = configParseResult.fileNames; compilerOptions = extend(commandLine.options, configParseResult.options); } @@ -322,13 +330,14 @@ namespace ts { rootFileNames.splice(index, 1); } } - startTimer(); + startTimerForRecompilation(); } // If the configuration file changes, forget cached program and start the recompilation timer function configFileChanged() { setCachedProgram(undefined); - startTimer(); + cachedConfigFileText = undefined; + startTimerForRecompilation(); } function watchedDirectoryChanged(fileName: string) { @@ -336,28 +345,39 @@ namespace ts { return; } - let parsedCommandLine = configFileToParsedCommandLine(configFileName); - let newFileNames = parsedCommandLine.fileNames.map(compilerHost.getCanonicalFileName); - let canonicalRootFileNames = rootFileNames.map(compilerHost.getCanonicalFileName); + startTimerForHandlingDirectoryChanges(); + } - if (!doTwoArraysHaveTheSameElements(newFileNames, canonicalRootFileNames)) { + function startTimerForHandlingDirectoryChanges() { + if (timerHandleForDirectoryChanges) { + clearTimeout(timerHandleForDirectoryChanges); + } + timerHandleForDirectoryChanges = setTimeout(directoryChangeHandler, 250); + } + + function directoryChangeHandler() { + let parsedCommandLine = parseConfigFile(); + let newFileNames = ts.map(parsedCommandLine.fileNames, compilerHost.getCanonicalFileName); + let canonicalRootFileNames = ts.map(rootFileNames, compilerHost.getCanonicalFileName); + + if (!arrayStructurallyIsEqualTo(newFileNames, canonicalRootFileNames)) { setCachedProgram(undefined); - startTimer(); + startTimerForRecompilation(); } } // Upon detecting a file change, wait for 250ms and then perform a recompilation. This gives batch // operations (such as saving all modified files in an editor) a chance to complete before we kick // off a new compilation. - function startTimer() { - if (timerHandle) { - clearTimeout(timerHandle); + function startTimerForRecompilation() { + if (timerHandleForRecompilation) { + clearTimeout(timerHandleForRecompilation); } - timerHandle = setTimeout(recompile, 250); + timerHandleForRecompilation = setTimeout(recompile, 250); } function recompile() { - timerHandle = undefined; + timerHandleForRecompilation = undefined; reportWatchDiagnostic(createCompilerDiagnostic(Diagnostics.File_change_detected_Starting_incremental_compilation)); performCompilation(); } diff --git a/src/compiler/utilities.ts b/src/compiler/utilities.ts index 5127b98ab3a..5d0d1240734 100644 --- a/src/compiler/utilities.ts +++ b/src/compiler/utilities.ts @@ -2406,4 +2406,14 @@ namespace ts { } } } + + export function copyListRemovingItem(item: T, list: T[]) { + var copiedList: T[] = []; + for (var i = 0, len = list.length; i < len; i++) { + if (list[i] != item) { + copiedList.push(list[i]); + } + } + return copiedList; + } } diff --git a/src/server/editorServices.ts b/src/server/editorServices.ts index 974f51906f6..0da23261db4 100644 --- a/src/server/editorServices.ts +++ b/src/server/editorServices.ts @@ -556,12 +556,11 @@ namespace ts.server { } this.log("Detected source file changes: " + fileName); - let { succeeded, projectOptions, error } = this.configFileToProjectOptions(project.projectFilename); - let newRootFiles = projectOptions.files.map(f => this.getCanonicalFileName(f)); - let currentRootFiles = project.getRootFiles().map(f => this.getCanonicalFileName(f)); + let newRootFiles = ts.map(projectOptions.files, this.getCanonicalFileName); + let currentRootFiles = ts.map(project.getRootFiles(), this.getCanonicalFileName); - if (!doTwoArraysHaveTheSameElements(currentRootFiles, newRootFiles)) { + if (!arrayStructurallyIsEqualTo(currentRootFiles, newRootFiles)) { // For configured projects, the change is made outside the tsconfig file, and // it is not likely to affect the project for other files opened by the client. We can // just update the current project. @@ -1159,12 +1158,12 @@ namespace ts.server { // file references will be relative to dirPath (or absolute) var dirPath = ts.getDirectoryPath(configFilename); var contents = this.host.readFile(configFilename) - var rawConfig: { config?: ProjectOptions; error?: Diagnostic; } = ts.parseConfigFileText(configFilename, contents); + var rawConfig: { config?: ProjectOptions; error?: Diagnostic; } = ts.parseConfigFileTextToJson(configFilename, contents); if (rawConfig.error) { return { succeeded: false, error: rawConfig.error }; } else { - var parsedCommandLine = ts.parseConfigFile(rawConfig.config, this.host, dirPath); + var parsedCommandLine = ts.parseJsonConfigFileContent(rawConfig.config, this.host, dirPath); if (parsedCommandLine.errors && (parsedCommandLine.errors.length > 0)) { return { succeeded: false, error: { errorMsg: "tsconfig option errors" } }; } diff --git a/src/services/shims.ts b/src/services/shims.ts index 351514a1499..e919625395d 100644 --- a/src/services/shims.ts +++ b/src/services/shims.ts @@ -990,7 +990,7 @@ namespace ts { () => { let text = sourceTextSnapshot.getText(0, sourceTextSnapshot.getLength()); - let result = parseConfigFileText(fileName, text); + let result = parseConfigFileTextToJson(fileName, text); if (result.error) { return { @@ -1000,7 +1000,7 @@ namespace ts { }; } - var configFile = parseConfigFile(result.config, this.host, getDirectoryPath(normalizeSlashes(fileName))); + var configFile = parseJsonConfigFileContent(result.config, this.host, getDirectoryPath(normalizeSlashes(fileName))); return { options: configFile.options,