From 9a85ad6a4eac71150923b3406a3db7500574d102 Mon Sep 17 00:00:00 2001 From: Dirk Baeumer Date: Wed, 30 Sep 2015 09:10:56 +0200 Subject: [PATCH 01/21] Fixed #5032: tsserver: Format on type broken --- src/server/session.ts | 73 +++++++++++++++++++++++++------------------ 1 file changed, 43 insertions(+), 30 deletions(-) diff --git a/src/server/session.ts b/src/server/session.ts index da044e7b4c6..7a95f6d75e9 100644 --- a/src/server/session.ts +++ b/src/server/session.ts @@ -17,10 +17,25 @@ namespace ts.server { strBuilder += " "; } spaceCache[n] = strBuilder; - } + } return spaceCache[n]; } + export function generateIndentString(n: number, editorOptions: EditorOptions): string { + if (editorOptions.ConvertTabsToSpaces) { + return generateSpaces(n); + } else { + var result = ""; + for (var i = 0; i < Math.floor(n / editorOptions.TabSize); i++) { + result += "\t"; + } + for (var i = 0; i < n % editorOptions.TabSize; i++) { + result += " "; + } + return result; + } + } + interface FileStart { file: string; start: ILineInfo; @@ -51,7 +66,7 @@ namespace ts.server { return 1; } } - + function formatDiag(fileName: string, project: Project, diag: ts.Diagnostic) { return { start: project.compilerService.host.positionToLineOffset(fileName, diag.start), @@ -104,7 +119,7 @@ namespace ts.server { export const Unknown = "unknown"; } - module Errors { + module Errors { export var NoProject = new Error("No Project."); } @@ -121,9 +136,9 @@ namespace ts.server { private changeSeq = 0; constructor( - private host: ServerHost, - private byteLength: (buf: string, encoding?: string) => number, - private hrtime: (start?: number[]) => number[], + private host: ServerHost, + private byteLength: (buf: string, encoding?: string) => number, + private hrtime: (start?: number[]) => number[], private logger: Logger ) { this.projectService = @@ -227,7 +242,7 @@ namespace ts.server { this.syntacticCheck(file, project); this.semanticCheck(file, project); } - + private reloadProjects() { this.projectService.reloadProjects(); } @@ -360,9 +375,9 @@ namespace ts.server { let { compilerService } = project; let position = compilerService.host.lineOffsetToPosition(fileName, line, offset); - + let documentHighlights = compilerService.languageService.getDocumentHighlights(fileName, position, filesToSearch); - + if (!documentHighlights) { return undefined; } @@ -557,7 +572,7 @@ namespace ts.server { var compilerService = project.compilerService; var startPosition = compilerService.host.lineOffsetToPosition(file, line, offset); var endPosition = compilerService.host.lineOffsetToPosition(file, endLine, endOffset); - + // TODO: avoid duplicate code (with formatonkey) var edits = compilerService.languageService.getFormattingEditsForRange(file, startPosition, endPosition, this.projectService.getFormatCodeOptions(file)); @@ -607,27 +622,25 @@ namespace ts.server { NewLineCharacter: "\n", ConvertTabsToSpaces: formatOptions.ConvertTabsToSpaces, }; - var indentPosition = - compilerService.languageService.getIndentationAtPosition(file, position, editorOptions); + var preferredIndent = compilerService.languageService.getIndentationAtPosition(file, position, editorOptions); + var hasIndent = 0; for (var i = 0, len = lineText.length; i < len; i++) { if (lineText.charAt(i) == " ") { - indentPosition--; + hasIndent++; } else if (lineText.charAt(i) == "\t") { - indentPosition -= editorOptions.IndentSize; + hasIndent += editorOptions.TabSize; } else { break; } } - if (indentPosition > 0) { - var spaces = generateSpaces(indentPosition); - edits.push({ span: ts.createTextSpanFromBounds(position, position), newText: spaces }); - } - else if (indentPosition < 0) { + // i points to the first non whitespace character + if (preferredIndent !== hasIndent) { + var firstNoWhiteSpacePosition = lineInfo.offset + i; edits.push({ - span: ts.createTextSpanFromBounds(position, position - indentPosition), - newText: "" + span: ts.createTextSpanFromBounds(lineInfo.offset, firstNoWhiteSpacePosition), + newText: generateIndentString(preferredIndent, editorOptions) }); } } @@ -702,14 +715,14 @@ namespace ts.server { if (!project) { throw Errors.NoProject; } - + var compilerService = project.compilerService; var position = compilerService.host.lineOffsetToPosition(file, line, offset); var helpItems = compilerService.languageService.getSignatureHelpItems(file, position); if (!helpItems) { return undefined; } - + var span = helpItems.applicableSpan; var result: protocol.SignatureHelpItems = { items: helpItems.items, @@ -721,10 +734,10 @@ namespace ts.server { argumentIndex: helpItems.argumentIndex, argumentCount: helpItems.argumentCount, } - + return result; } - + private getDiagnostics(delay: number, fileNames: string[]) { var checkList = fileNames.reduce((accum: PendingErrorCheck[], fileName: string) => { fileName = ts.normalizePath(fileName); @@ -859,20 +872,20 @@ namespace ts.server { private getBraceMatching(line: number, offset: number, fileName: string): protocol.TextSpan[] { var file = ts.normalizePath(fileName); - + var project = this.projectService.getProjectForFile(file); if (!project) { throw Errors.NoProject; } - + var compilerService = project.compilerService; var position = compilerService.host.lineOffsetToPosition(file, line, offset); - + var spans = compilerService.languageService.getBraceMatchingAtPosition(file, position); if (!spans) { return undefined; } - + return spans.map(span => ({ start: compilerService.host.positionToLineOffset(file, span.start), end: compilerService.host.positionToLineOffset(file, span.start + span.length) @@ -1064,7 +1077,7 @@ namespace ts.server { public onMessage(message: string) { if (this.logger.isVerbose()) { this.logger.info("request: " + message); - var start = this.hrtime(); + var start = this.hrtime(); } try { var request = JSON.parse(message); From 2f8e4fa6bd1697c6ec82afc269ee7a51640ce78f Mon Sep 17 00:00:00 2001 From: Dirk Baeumer Date: Wed, 30 Sep 2015 09:18:50 +0200 Subject: [PATCH 02/21] Fixed unnecessay whitespace changes --- src/server/session.ts | 40 ++++++++++++++++++++-------------------- 1 file changed, 20 insertions(+), 20 deletions(-) diff --git a/src/server/session.ts b/src/server/session.ts index 7a95f6d75e9..41a0e409d57 100644 --- a/src/server/session.ts +++ b/src/server/session.ts @@ -17,7 +17,7 @@ namespace ts.server { strBuilder += " "; } spaceCache[n] = strBuilder; - } + } return spaceCache[n]; } @@ -35,7 +35,7 @@ namespace ts.server { return result; } } - + interface FileStart { file: string; start: ILineInfo; @@ -66,7 +66,7 @@ namespace ts.server { return 1; } } - + function formatDiag(fileName: string, project: Project, diag: ts.Diagnostic) { return { start: project.compilerService.host.positionToLineOffset(fileName, diag.start), @@ -119,7 +119,7 @@ namespace ts.server { export const Unknown = "unknown"; } - module Errors { + module Errors { export var NoProject = new Error("No Project."); } @@ -136,9 +136,9 @@ namespace ts.server { private changeSeq = 0; constructor( - private host: ServerHost, - private byteLength: (buf: string, encoding?: string) => number, - private hrtime: (start?: number[]) => number[], + private host: ServerHost, + private byteLength: (buf: string, encoding?: string) => number, + private hrtime: (start?: number[]) => number[], private logger: Logger ) { this.projectService = @@ -242,7 +242,7 @@ namespace ts.server { this.syntacticCheck(file, project); this.semanticCheck(file, project); } - + private reloadProjects() { this.projectService.reloadProjects(); } @@ -375,9 +375,9 @@ namespace ts.server { let { compilerService } = project; let position = compilerService.host.lineOffsetToPosition(fileName, line, offset); - + let documentHighlights = compilerService.languageService.getDocumentHighlights(fileName, position, filesToSearch); - + if (!documentHighlights) { return undefined; } @@ -572,7 +572,7 @@ namespace ts.server { var compilerService = project.compilerService; var startPosition = compilerService.host.lineOffsetToPosition(file, line, offset); var endPosition = compilerService.host.lineOffsetToPosition(file, endLine, endOffset); - + // TODO: avoid duplicate code (with formatonkey) var edits = compilerService.languageService.getFormattingEditsForRange(file, startPosition, endPosition, this.projectService.getFormatCodeOptions(file)); @@ -715,14 +715,14 @@ namespace ts.server { if (!project) { throw Errors.NoProject; } - + var compilerService = project.compilerService; var position = compilerService.host.lineOffsetToPosition(file, line, offset); var helpItems = compilerService.languageService.getSignatureHelpItems(file, position); if (!helpItems) { return undefined; } - + var span = helpItems.applicableSpan; var result: protocol.SignatureHelpItems = { items: helpItems.items, @@ -734,10 +734,10 @@ namespace ts.server { argumentIndex: helpItems.argumentIndex, argumentCount: helpItems.argumentCount, } - + return result; } - + private getDiagnostics(delay: number, fileNames: string[]) { var checkList = fileNames.reduce((accum: PendingErrorCheck[], fileName: string) => { fileName = ts.normalizePath(fileName); @@ -872,20 +872,20 @@ namespace ts.server { private getBraceMatching(line: number, offset: number, fileName: string): protocol.TextSpan[] { var file = ts.normalizePath(fileName); - + var project = this.projectService.getProjectForFile(file); if (!project) { throw Errors.NoProject; } - + var compilerService = project.compilerService; var position = compilerService.host.lineOffsetToPosition(file, line, offset); - + var spans = compilerService.languageService.getBraceMatchingAtPosition(file, position); if (!spans) { return undefined; } - + return spans.map(span => ({ start: compilerService.host.positionToLineOffset(file, span.start), end: compilerService.host.positionToLineOffset(file, span.start + span.length) @@ -1077,7 +1077,7 @@ namespace ts.server { public onMessage(message: string) { if (this.logger.isVerbose()) { this.logger.info("request: " + message); - var start = this.hrtime(); + var start = this.hrtime(); } try { var request = JSON.parse(message); From 0bc5c14d51b253527cb2dee2e17927d6a215444d Mon Sep 17 00:00:00 2001 From: zhengbli Date: Thu, 1 Oct 2015 15:25:43 -0700 Subject: [PATCH 03/21] Change fileWatcher in sys for node 4 --- src/compiler/sys.ts | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/src/compiler/sys.ts b/src/compiler/sys.ts index f3f2b02a30e..3782f4d11e9 100644 --- a/src/compiler/sys.ts +++ b/src/compiler/sys.ts @@ -9,6 +9,7 @@ namespace ts { readFile(path: string, encoding?: string): string; writeFile(path: string, data: string, writeByteOrderMark?: boolean): void; watchFile?(path: string, callback: (path: string) => void): FileWatcher; + watchDirectory?(path: string, callback: (path: string) => void): FileWatcher; resolvePath(path: string): string; fileExists(path: string): boolean; directoryExists(path: string): boolean; @@ -191,6 +192,12 @@ namespace ts { const _fs = require("fs"); const _path = require("path"); const _os = require("os"); + const _process = require("process"); + + + function isNode4OrLater(): Boolean { + return parseInt(_process.version.charAt(1)) >= 4; + } const platform: string = _os.platform(); // win32\win64 are case insensitive platforms, MacOS (darwin) by default is also case insensitive @@ -284,6 +291,15 @@ namespace ts { readFile, writeFile, watchFile: (fileName, callback) => { + + // Node 4.0 stablized the `fs.watch` function which avoids polling + // and is more efficient than `fs.watchFile` (ref: https://github.com/nodejs/node/pull/2649 + // and https://github.com/Microsoft/TypeScript/issues/4643), therefore + // if the current node.js version is newer than 4, use `fs.watch` instead. + if (isNode4OrLater()) { + return _fs.watch(fileName, (eventName: string, path: string) => callback(path)); + } + // watchFile polls a file every 250ms, picking up file notifications. _fs.watchFile(fileName, { persistent: true, interval: 250 }, fileChanged); From 5daa100bf40fdb5cc13d5ae1555dc09c1e9078e4 Mon Sep 17 00:00:00 2001 From: Zhengbo Li Date: Thu, 1 Oct 2015 15:40:13 -0700 Subject: [PATCH 04/21] unify the node filewatcher in sys.ts and server.ts --- src/compiler/sys.ts | 122 +++++++++++++++++++++++++++++++++++++------ src/server/server.ts | 110 -------------------------------------- 2 files changed, 107 insertions(+), 125 deletions(-) diff --git a/src/compiler/sys.ts b/src/compiler/sys.ts index 3782f4d11e9..91856a46b29 100644 --- a/src/compiler/sys.ts +++ b/src/compiler/sys.ts @@ -21,6 +21,12 @@ namespace ts { exit(exitCode?: number): void; } + interface WatchedFile { + fileName: string; + callback: (fileName: string) => void; + mtime: Date; + } + export interface FileWatcher { close(): void; } @@ -193,7 +199,104 @@ namespace ts { const _path = require("path"); const _os = require("os"); const _process = require("process"); - + + class WatchedFileSet { + private watchedFiles: WatchedFile[] = []; + private nextFileToCheck = 0; + private watchTimer: NodeJS.Timer; + + // 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[]) { + var 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 { + return _fs.statSync(fileName).mtime; + } + + private poll(checkedIndex: number) { + var watchedFile = this.watchedFiles[checkedIndex]; + if (!watchedFile) { + return; + } + + _fs.stat(watchedFile.fileName, (err: any, stats: any) => { + if (err) { + watchedFile.callback(watchedFile.fileName); + } + else if (watchedFile.mtime.getTime() !== stats.mtime.getTime()) { + watchedFile.mtime = WatchedFileSet.getModifiedTime(watchedFile.fileName); + watchedFile.callback(watchedFile.fileName); + } + }); + } + + // this implementation uses polling and + // stat due to inconsistencies of fs.watch + // and efficiency of stat on modern filesystems + private startWatchTimer() { + this.watchTimer = setInterval(() => { + var count = 0; + var nextToCheck = this.nextFileToCheck; + var firstCheck = -1; + while ((count < this.chunkSize) && (nextToCheck !== firstCheck)) { + this.poll(nextToCheck); + if (firstCheck < 0) { + firstCheck = nextToCheck; + } + nextToCheck++; + if (nextToCheck === this.watchedFiles.length) { + nextToCheck = 0; + } + count++; + } + this.nextFileToCheck = nextToCheck; + }, this.interval); + } + + addFile(fileName: string, callback: (fileName: string) => void): WatchedFile { + var file: WatchedFile = { + fileName, + callback, + mtime: WatchedFileSet.getModifiedTime(fileName) + }; + + this.watchedFiles.push(file); + if (this.watchedFiles.length === 1) { + this.startWatchTimer(); + } + return file; + } + + removeFile(file: WatchedFile) { + this.watchedFiles = WatchedFileSet.copyListRemovingItem(file, this.watchedFiles); + } + } + + // REVIEW: for now this implementation uses polling. + // The advantage of polling is that it works reliably + // on all os and with network mounted files. + // For 90 referenced files, the average time to detect + // changes is 2*msInterval (by default 5 seconds). + // The overhead of this is .04 percent (1/2500) with + // average pause of < 1 millisecond (and max + // pause less than 1.5 milliseconds); question is + // do we anticipate reference sets in the 100s and + // do we care about waiting 10-20 seconds to detect + // 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? + var watchedFileSet = new WatchedFileSet(); function isNode4OrLater(): Boolean { return parseInt(_process.version.charAt(1)) >= 4; @@ -291,8 +394,7 @@ namespace ts { readFile, writeFile, watchFile: (fileName, callback) => { - - // Node 4.0 stablized the `fs.watch` function which avoids polling + // Node 4.0 stablized the `fs.watch` function on Windows which avoids polling // and is more efficient than `fs.watchFile` (ref: https://github.com/nodejs/node/pull/2649 // and https://github.com/Microsoft/TypeScript/issues/4643), therefore // if the current node.js version is newer than 4, use `fs.watch` instead. @@ -300,19 +402,9 @@ namespace ts { return _fs.watch(fileName, (eventName: string, path: string) => callback(path)); } - // watchFile polls a file every 250ms, picking up file notifications. - _fs.watchFile(fileName, { persistent: true, interval: 250 }, fileChanged); - + var watchedFile = watchedFileSet.addFile(fileName, callback); return { - close() { _fs.unwatchFile(fileName, fileChanged); } - }; - - function fileChanged(curr: any, prev: any) { - if (+curr.mtime <= +prev.mtime) { - return; - } - - callback(fileName); + close: () => watchedFileSet.removeFile(watchedFile) } }, resolvePath: function (path: string): string { diff --git a/src/server/server.ts b/src/server/server.ts index 843197b918a..39864fc8477 100644 --- a/src/server/server.ts +++ b/src/server/server.ts @@ -83,95 +83,6 @@ namespace ts.server { } } - interface WatchedFile { - fileName: string; - callback: (fileName: string) => void; - mtime: Date; - } - - class WatchedFileSet { - private watchedFiles: WatchedFile[] = []; - private nextFileToCheck = 0; - private watchTimer: NodeJS.Timer; - - // 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[]) { - var 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 { - return fs.statSync(fileName).mtime; - } - - private poll(checkedIndex: number) { - var watchedFile = this.watchedFiles[checkedIndex]; - if (!watchedFile) { - return; - } - - fs.stat(watchedFile.fileName,(err, stats) => { - if (err) { - watchedFile.callback(watchedFile.fileName); - } - else if (watchedFile.mtime.getTime() !== stats.mtime.getTime()) { - watchedFile.mtime = WatchedFileSet.getModifiedTime(watchedFile.fileName); - watchedFile.callback(watchedFile.fileName); - } - }); - } - - // this implementation uses polling and - // stat due to inconsistencies of fs.watch - // and efficiency of stat on modern filesystems - private startWatchTimer() { - this.watchTimer = setInterval(() => { - var count = 0; - var nextToCheck = this.nextFileToCheck; - var firstCheck = -1; - while ((count < this.chunkSize) && (nextToCheck !== firstCheck)) { - this.poll(nextToCheck); - if (firstCheck < 0) { - firstCheck = nextToCheck; - } - nextToCheck++; - if (nextToCheck === this.watchedFiles.length) { - nextToCheck = 0; - } - count++; - } - this.nextFileToCheck = nextToCheck; - }, this.interval); - } - - addFile(fileName: string, callback: (fileName: string) => void ): WatchedFile { - var file: WatchedFile = { - fileName, - callback, - mtime: WatchedFileSet.getModifiedTime(fileName) - }; - - this.watchedFiles.push(file); - if (this.watchedFiles.length === 1) { - this.startWatchTimer(); - } - return file; - } - - removeFile(file: WatchedFile) { - this.watchedFiles = WatchedFileSet.copyListRemovingItem(file, this.watchedFiles); - } - } - class IOSession extends Session { constructor(host: ServerHost, logger: ts.server.Logger) { super(host, Buffer.byteLength, process.hrtime, logger); @@ -243,28 +154,7 @@ namespace ts.server { // TODO: check that this location is writable var logger = createLoggerFromEnv(); - - // REVIEW: for now this implementation uses polling. - // The advantage of polling is that it works reliably - // on all os and with network mounted files. - // For 90 referenced files, the average time to detect - // changes is 2*msInterval (by default 5 seconds). - // The overhead of this is .04 percent (1/2500) with - // average pause of < 1 millisecond (and max - // pause less than 1.5 milliseconds); question is - // do we anticipate reference sets in the 100s and - // do we care about waiting 10-20 seconds to detect - // 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? - var watchedFileSet = new WatchedFileSet(); - ts.sys.watchFile = function (fileName, callback) { - var watchedFile = watchedFileSet.addFile(fileName, callback); - return { - close: () => watchedFileSet.removeFile(watchedFile) - } - }; var ioSession = new IOSession(ts.sys, logger); process.on('uncaughtException', function(err: Error) { ioSession.logError(err, "unknown"); From 4dcf8c773729e577ad49a64255e1d9c76c09f7c1 Mon Sep 17 00:00:00 2001 From: Zhengbo Li Date: Thu, 1 Oct 2015 15:59:03 -0700 Subject: [PATCH 05/21] bug fixes --- src/compiler/sys.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/compiler/sys.ts b/src/compiler/sys.ts index 91856a46b29..0b4d4dabd52 100644 --- a/src/compiler/sys.ts +++ b/src/compiler/sys.ts @@ -198,12 +198,11 @@ namespace ts { const _fs = require("fs"); const _path = require("path"); const _os = require("os"); - const _process = require("process"); class WatchedFileSet { private watchedFiles: WatchedFile[] = []; private nextFileToCheck = 0; - private watchTimer: NodeJS.Timer; + private watchTimer: any; // average async stat takes about 30 microseconds // set chunk size to do 30 files in < 1 millisecond @@ -299,7 +298,7 @@ namespace ts { var watchedFileSet = new WatchedFileSet(); function isNode4OrLater(): Boolean { - return parseInt(_process.version.charAt(1)) >= 4; + return parseInt(process.version.charAt(1)) >= 4; } const platform: string = _os.platform(); From 98eaeba4f162049f6595aa05127e9a9f9704d8e4 Mon Sep 17 00:00:00 2001 From: Zhengbo Li Date: Fri, 2 Oct 2015 11:49:30 -0700 Subject: [PATCH 06/21] temp save --- src/compiler/sys.ts | 16 ++++++- src/server/editorServices.ts | 92 +++++++++++++++++++++++++++--------- 2 files changed, 85 insertions(+), 23 deletions(-) diff --git a/src/compiler/sys.ts b/src/compiler/sys.ts index 0b4d4dabd52..85c674ec904 100644 --- a/src/compiler/sys.ts +++ b/src/compiler/sys.ts @@ -9,7 +9,7 @@ namespace ts { readFile(path: string, encoding?: string): string; writeFile(path: string, data: string, writeByteOrderMark?: boolean): void; watchFile?(path: string, callback: (path: string) => void): FileWatcher; - watchDirectory?(path: string, callback: (path: string) => void): FileWatcher; + watchDirectory?(path: string, callback: (path: string) => void, recursive?: boolean): FileWatcher; resolvePath(path: string): string; fileExists(path: string): boolean; directoryExists(path: string): boolean; @@ -406,6 +406,20 @@ namespace ts { close: () => watchedFileSet.removeFile(watchedFile) } }, + watchDirectory: (path, 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) + // therefore if the current node.js version is newer than 4, use `fs.watch` instead. + if (isNode4OrLater()) { + return _fs.watch(path, { persisten: true, recursive: !!recursive }, (eventName: string, modifiedPath: string) => callback(modifiedPath)); + } + + // If Node version is older than 4.0, the "recursive" parameter will be ignored + var watchedFile = watchedFileSet.addFile(path, callback); + return { + close: () => watchedFileSet.removeFile(watchedFile) + } + }, resolvePath: function (path: string): string { return _path.resolve(path); }, diff --git a/src/server/editorServices.ts b/src/server/editorServices.ts index 7ab46fc689e..dc6b8700a3d 100644 --- a/src/server/editorServices.ts +++ b/src/server/editorServices.ts @@ -78,19 +78,19 @@ namespace ts.server { return this.snap().getChangeRange(oldSnapshot); } } - + interface TimestampedResolvedModule extends ResolvedModuleWithFailedLookupLocations { - lastCheckTime: number; + lastCheckTime: number; } - + export class LSHost implements ts.LanguageServiceHost { ls: ts.LanguageService = null; compilationSettings: ts.CompilerOptions; filenameToScript: ts.Map = {}; roots: ScriptInfo[] = []; - private resolvedModuleNames: ts.FileMap>; + private resolvedModuleNames: ts.FileMap>; private moduleResolutionHost: ts.ModuleResolutionHost; - + constructor(public host: ServerHost, public project: Project) { this.resolvedModuleNames = ts.createFileMap>(ts.createGetCanonicalFileName(host.useCaseSensitiveFileNames)) this.moduleResolutionHost = { @@ -98,15 +98,15 @@ namespace ts.server { readFile: fileName => this.host.readFile(fileName) } } - + resolveModuleNames(moduleNames: string[], containingFile: string): ResolvedModule[] { let currentResolutionsInFile = this.resolvedModuleNames.get(containingFile); - + let newResolutions: Map = {}; let resolvedModules: ResolvedModule[] = []; - + let compilerOptions = this.getCompilationSettings(); - + for (let moduleName of moduleNames) { // check if this is a duplicate entry in the list let resolution = lookUp(newResolutions, moduleName); @@ -122,21 +122,21 @@ namespace ts.server { newResolutions[moduleName] = resolution; } } - + ts.Debug.assert(resolution !== undefined); - + resolvedModules.push(resolution.resolvedModule); } // replace old results with a new one this.resolvedModuleNames.set(containingFile, newResolutions); return resolvedModules; - + function moduleResolutionIsValid(resolution: TimestampedResolvedModule): boolean { if (!resolution) { return false; } - + if (resolution.resolvedModule) { // TODO: consider checking failedLookupLocations // TODO: use lastCheckTime to track expiration for module name resolution @@ -147,7 +147,7 @@ namespace ts.server { // after all there is no point to invalidate it if we have no idea where to look for the module. return resolution.failedLookupLocations.length === 0; } - } + } getDefaultLibFileName() { var nodeModuleBinDir = ts.getDirectoryPath(ts.normalizePath(this.host.getExecutingFilePath())); @@ -224,7 +224,7 @@ namespace ts.server { this.roots.push(info); } } - + removeRoot(info: ScriptInfo) { var scriptInfo = ts.lookUp(this.filenameToScript, info.fileName); if (scriptInfo) { @@ -354,6 +354,11 @@ namespace ts.server { compilerService: CompilerService; projectFilename: string; projectFileWatcher: FileWatcher; + // Inferred projects have a collection of non-recursive directory watchers starting + // from the root path (e.g. "C:\" or "/") to the current path; + // while configured projects whose tsconfig files don't have a "files" array have one + // recursive directory watcher starting from the current path + directoryWatchers: FileWatcher[] = []; program: ts.Program; filenameToSourceFile: ts.Map = {}; updateGraphSeq = 0; @@ -532,6 +537,41 @@ namespace ts.server { } } + /** + * This is the callback function when the directory that an inferred project belongs + * to changed. The function looks for newly added tsconfig.json files; if it found one, + * and the tsconfig.json file contains the root file of the current inferred project, + * it will update the project structure. + */ + watchedDirectoryChanged(project: Project, path: string) { + if (project.isConfiguredProject()) { + return; + } + + let configFileName = ts.combinePaths(path, "tsconfig.json"); + if (sys.fileExists(configFileName)) { + let {succeeded, projectOptions, error} = this.configFileToProjectOptions(configFileName); + if (!succeeded) { + return; + } + + let newProjectFileNames = projectOptions.files.map(f => this.getCanonicalFileName(f)); + let rootFiles = project.getRootFiles().map(f => this.getCanonicalFileName(f)); + for (let rootFile of rootFiles) { + if (newProjectFileNames.indexOf(rootFile) >= 0) { + this.reloadProjects(); + return; + } + } + + } + } + + getCanonicalFileName(fileName: string) { + let name = this.host.useCaseSensitiveFileNames ? fileName : fileName.toLowerCase(); + return ts.normalizePath(name); + } + watchedProjectConfigFileChanged(project: Project) { this.log("Config File Changed: " + project.projectFilename); this.updateConfiguredProject(project); @@ -567,11 +607,19 @@ namespace ts.server { } createInferredProject(root: ScriptInfo) { - var iproj = new Project(this); - iproj.addRoot(root); - iproj.finishGraph(); - this.inferredProjects.push(iproj); - return iproj; + var project = new Project(this); + project.addRoot(root); + + let currentPath = ts.getDirectoryPath(root.fileName); + let parentPath = ts.getDirectoryPath(currentPath); + while (currentPath != parentPath) { + // To finish + let directoryWatcher = this.host.watchDirectory(currentPath, p => this.);; + } + + project.finishGraph(); + this.inferredProjects.push(project); + return project; } fileDeletedInFilesystem(info: ScriptInfo) { @@ -1217,9 +1265,9 @@ namespace ts.server { goSubtree: boolean; done: boolean; leaf(relativeStart: number, relativeLength: number, lineCollection: LineLeaf): void; - pre? (relativeStart: number, relativeLength: number, lineCollection: LineCollection, + pre?(relativeStart: number, relativeLength: number, lineCollection: LineCollection, parent: LineNode, nodeType: CharRangeSection): LineCollection; - post? (relativeStart: number, relativeLength: number, lineCollection: LineCollection, + post?(relativeStart: number, relativeLength: number, lineCollection: LineCollection, parent: LineNode, nodeType: CharRangeSection): LineCollection; } From 7fa26adf28b2e628787b84484eead5f87c7db438 Mon Sep 17 00:00:00 2001 From: zhengbli Date: Mon, 5 Oct 2015 02:58:40 -0700 Subject: [PATCH 07/21] Redesigned directory watchers --- src/compiler/core.ts | 36 +++++++ src/compiler/sys.ts | 19 +++- src/server/editorServices.ts | 186 ++++++++++++++++++++++++----------- 3 files changed, 182 insertions(+), 59 deletions(-) diff --git a/src/compiler/core.ts b/src/compiler/core.ts index ce59c3b3bc6..a4c9a987267 100644 --- a/src/compiler/core.ts +++ b/src/compiler/core.ts @@ -700,6 +700,9 @@ namespace ts { } export function getBaseFileName(path: string) { + if (!path) { + return undefined; + } let i = path.lastIndexOf(directorySeparator); return i < 0 ? path : path.substring(i + 1); } @@ -723,6 +726,18 @@ namespace ts { */ export const supportedExtensions = [".ts", ".tsx", ".d.ts"]; + export function isSupportedSourceFileName(fileName: string) { + if (!fileName) { return false; } + + let dotIndex = fileName.lastIndexOf("."); + if (dotIndex < 0) { + return false; + } + + let extension = fileName.slice(dotIndex, fileName.length); + return supportedExtensions.indexOf(extension) >= 0; + } + const extensionsToRemove = [".d.ts", ".ts", ".js", ".tsx", ".jsx"]; export function removeFileExtension(path: string): string { for (let ext of extensionsToRemove) { @@ -817,4 +832,25 @@ namespace ts { Debug.assert(false, message); } } + + export function doTwoArraysHaveTheSameElements(array1: Array, array2: Array): Boolean { + if (!array1 || !array2) { + return false; + } + + 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; + } } diff --git a/src/compiler/sys.ts b/src/compiler/sys.ts index 85c674ec904..02728dffaf4 100644 --- a/src/compiler/sys.ts +++ b/src/compiler/sys.ts @@ -398,7 +398,8 @@ namespace ts { // and https://github.com/Microsoft/TypeScript/issues/4643), therefore // if the current node.js version is newer than 4, use `fs.watch` instead. if (isNode4OrLater()) { - return _fs.watch(fileName, (eventName: string, path: string) => callback(path)); + // Note: in node the callback of fs.watch is given only the base file name as a parameter + return _fs.watch(fileName, (eventName: string, baseFileName: string) => callback(fileName)); } var watchedFile = watchedFileSet.addFile(fileName, callback); @@ -410,8 +411,22 @@ namespace ts { // 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) // therefore if the current node.js version is newer than 4, use `fs.watch` instead. + + // 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 (isNode4OrLater()) { - return _fs.watch(path, { persisten: true, recursive: !!recursive }, (eventName: string, modifiedPath: string) => callback(modifiedPath)); + return _fs.watch( + path, + { persisten: true, recursive: !!recursive }, + (eventName: string, relativeFileName: string) => { + if (eventName == "rename") { + // when deleting a file, the passed baseFileName is null + callback(relativeFileName == null ? null : ts.combinePaths(path, ts.normalizeSlashes(relativeFileName))) + }; + } + ); } // If Node version is older than 4.0, the "recursive" parameter will be ignored diff --git a/src/server/editorServices.ts b/src/server/editorServices.ts index dc6b8700a3d..80cebe665ae 100644 --- a/src/server/editorServices.ts +++ b/src/server/editorServices.ts @@ -354,11 +354,9 @@ namespace ts.server { compilerService: CompilerService; projectFilename: string; projectFileWatcher: FileWatcher; - // Inferred projects have a collection of non-recursive directory watchers starting - // from the root path (e.g. "C:\" or "/") to the current path; - // while configured projects whose tsconfig files don't have a "files" array have one - // recursive directory watcher starting from the current path - directoryWatchers: FileWatcher[] = []; + directoryWatcher: FileWatcher; + // Used to keep track of what directories are watched for this project + directoriesWatchedForTsconfig: string[] = []; program: ts.Program; filenameToSourceFile: ts.Map = {}; updateGraphSeq = 0; @@ -382,6 +380,10 @@ namespace ts.server { return this.projectService.openFile(filename, false); } + getRootFiles() { + return this.compilerService.host.roots.map(info => info.fileName); + } + getFileNames() { let sourceFiles = this.program.getSourceFiles(); return sourceFiles.map(sourceFile => sourceFile.fileName); @@ -434,13 +436,11 @@ namespace ts.server { // add a root file to project addRoot(info: ScriptInfo) { - info.defaultProject = this; this.compilerService.host.addRoot(info); } // remove a root file from project removeRoot(info: ScriptInfo) { - info.defaultProject = undefined; this.compilerService.host.removeRoot(info); } @@ -496,6 +496,11 @@ namespace ts.server { openFilesReferenced: ScriptInfo[] = []; // open files that are roots of a configured project openFileRootsConfigured: ScriptInfo[] = []; + // a path to directory watcher map that detects added tsconfig files + directoryWatchersForTsconfig: ts.Map = {}; + // count of how many projects are using the directory watcher. If the + // number becomes 0 for a watcher, then we should close it. + directoryWatchersRefCount: ts.Map = {}; hostConfiguration: HostConfiguration; constructor(public host: ServerHost, public psLogger: Logger, public eventHandler?: ProjectServiceEventHandler) { @@ -538,32 +543,53 @@ namespace ts.server { } /** - * This is the callback function when the directory that an inferred project belongs - * to changed. The function looks for newly added tsconfig.json files; if it found one, - * and the tsconfig.json file contains the root file of the current inferred project, - * it will update the project structure. + * This is the callback function when a watched directory has added or removed files. + * @param project the project that associates with this directory watcher + * @param fileName the absolute file name that changed in watched directory */ - watchedDirectoryChanged(project: Project, path: string) { - if (project.isConfiguredProject()) { + directoryWatchedForSourceFilesChanged(project: Project, fileName: string) { + // If a change was made inside "folder/file", node will trigger the callback twice: + // one with the fileName being "folder/file", and the other one with "folder". + // We don't respond to the second one. + if (fileName && !ts.isSupportedSourceFileName(fileName)) { return; } - let configFileName = ts.combinePaths(path, "tsconfig.json"); - if (sys.fileExists(configFileName)) { - let {succeeded, projectOptions, error} = this.configFileToProjectOptions(configFileName); - if (!succeeded) { + 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)); + + if (!doTwoArraysHaveTheSameElements(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. + this.updateConfiguredProject(project); + + // Call updateProjectStructure to clean up inferred projects we may have created for the + // new files + this.updateProjectStructure(); + } + } + + directoryWatchedForTsconfigChanged(fileName: string) { + if (ts.getBaseFileName(fileName) != "tsconfig.json") { + this.log(fileName + " is not tsconfig.json"); + return; + } + + this.log("Detected newly added tsconfig file: " + fileName); + + let { succeeded, projectOptions, error } = this.configFileToProjectOptions(fileName); + let rootFilesInTsconfig = projectOptions.files.map(f => this.getCanonicalFileName(f)); + let openFileRoots = this.openFileRoots.map(s => this.getCanonicalFileName(s.fileName)); + + for (let openFileRoot of openFileRoots) { + if (rootFilesInTsconfig.indexOf(openFileRoot) >= 0) { + this.reloadProjects(); return; } - - let newProjectFileNames = projectOptions.files.map(f => this.getCanonicalFileName(f)); - let rootFiles = project.getRootFiles().map(f => this.getCanonicalFileName(f)); - for (let rootFile of rootFiles) { - if (newProjectFileNames.indexOf(rootFile) >= 0) { - this.reloadProjects(); - return; - } - } - } } @@ -573,7 +599,7 @@ namespace ts.server { } watchedProjectConfigFileChanged(project: Project) { - this.log("Config File Changed: " + project.projectFilename); + this.log("Config file changed: " + project.projectFilename); this.updateConfiguredProject(project); this.updateProjectStructure(); } @@ -613,8 +639,18 @@ namespace ts.server { let currentPath = ts.getDirectoryPath(root.fileName); let parentPath = ts.getDirectoryPath(currentPath); while (currentPath != parentPath) { - // To finish - let directoryWatcher = this.host.watchDirectory(currentPath, p => this.);; + if (!project.projectService.directoryWatchersForTsconfig[currentPath]) { + this.log("Add watcher for: " + currentPath); + project.projectService.directoryWatchersForTsconfig[currentPath] = + this.host.watchDirectory(currentPath, fileName => this.directoryWatchedForTsconfigChanged(fileName)); + project.projectService.directoryWatchersRefCount[currentPath] = 1; + } + else { + project.projectService.directoryWatchersRefCount[currentPath] += 1; + } + project.directoriesWatchedForTsconfig.push(currentPath); + currentPath = parentPath; + parentPath = ts.getDirectoryPath(parentPath); } project.finishGraph(); @@ -663,9 +699,23 @@ namespace ts.server { this.configuredProjects = configuredProjects; } - removeConfiguredProject(project: Project) { - project.projectFileWatcher.close(); - this.configuredProjects = copyListRemovingItem(project, this.configuredProjects); + removeProject(project: Project) { + this.log("remove project: " + project.getRootFiles().toString()); + if (project.isConfiguredProject()) { + project.projectFileWatcher.close(); + project.directoryWatcher.close(); + this.configuredProjects = copyListRemovingItem(project, this.configuredProjects); + } + else { + for (let directory of project.directoriesWatchedForTsconfig) { + if (!(--project.projectService.directoryWatchersRefCount[directory])) { + this.log("Close directory watcher for: " + directory); + project.projectService.directoryWatchersForTsconfig[directory].close(); + project.projectService.directoryWatchersForTsconfig[directory] = undefined; + } + } + this.inferredProjects = copyListRemovingItem(project, this.inferredProjects); + } let fileNames = project.getFileNames(); for (let fileName of fileNames) { @@ -707,8 +757,7 @@ namespace ts.server { // if r referenced by the new project if (info.defaultProject.getSourceFile(r)) { // remove project rooted at r - this.inferredProjects = - copyListRemovingItem(r.defaultProject, this.inferredProjects); + this.removeProject(r.defaultProject); // put r in referenced open file list this.openFilesReferenced.push(r); // set default project of r to the new project @@ -761,19 +810,14 @@ namespace ts.server { this.openFileRootsConfigured = openFileRootsConfigured; } if (removedProject) { - if (removedProject.isConfiguredProject()) { - this.configuredProjects = copyListRemovingItem(removedProject, this.configuredProjects); - } - else { - this.inferredProjects = copyListRemovingItem(removedProject, this.inferredProjects); - } + this.removeProject(removedProject); var openFilesReferenced: ScriptInfo[] = []; var orphanFiles: ScriptInfo[] = []; // for all open, referenced files f for (var i = 0, len = this.openFilesReferenced.length; i < len; i++) { var f = this.openFilesReferenced[i]; // if f was referenced by the removed project, remember it - if (f.defaultProject === removedProject) { + if (f.defaultProject === removedProject || !f.defaultProject) { f.defaultProject = undefined; orphanFiles.push(f); } @@ -817,7 +861,11 @@ namespace ts.server { return referencingProjects; } + /** + * This function rebuilds the project for every file opened by the client + */ reloadProjects() { + this.log("reload projects."); // First check if there is new tsconfig file added for inferred project roots for (let info of this.openFileRoots) { this.openOrUpdateConfiguredProjectForFile(info.fileName); @@ -878,14 +926,25 @@ namespace ts.server { var rootFile = this.openFileRoots[i]; var rootedProject = rootFile.defaultProject; var referencingProjects = this.findReferencingProjects(rootFile, rootedProject); - if (referencingProjects.length === 0) { - rootFile.defaultProject = rootedProject; - openFileRoots.push(rootFile); + + if (rootFile.defaultProject.isConfiguredProject()) { + // If the root file has already been added into a configured project, + // meaning the original inferred project is gone already. + if (!rootedProject.isConfiguredProject()) { + this.removeProject(rootedProject); + } + this.openFileRootsConfigured.push(rootFile); } else { - // remove project from inferred projects list because root captured - this.inferredProjects = copyListRemovingItem(rootedProject, this.inferredProjects); - this.openFilesReferenced.push(rootFile); + if (referencingProjects.length === 0) { + rootFile.defaultProject = rootedProject; + openFileRoots.push(rootFile); + } + else { + // remove project from inferred projects list because root captured + this.removeProject(rootedProject); + this.openFilesReferenced.push(rootFile); + } } } this.openFileRoots = openFileRoots; @@ -897,6 +956,9 @@ namespace ts.server { this.addOpenFile(unattachedOpenFiles[i]); } this.printProjects(); + + this.log("Current openFileRoots: " + this.openFileRoots.map(s => s.fileName).toString()); + this.log("Current openFileRootsConfigured: " + this.openFileRootsConfigured.map(s => s.fileName).toString()); } getScriptInfo(filename: string) { @@ -970,6 +1032,11 @@ namespace ts.server { return info; } + /** + * This function tries to search for a tsconfig.json for the given file. If we found it, + * we first detect if there is already a configured project created for it: if so, we re-read + * the tsconfig file content and update the project; otherwise we create a new one. + */ openOrUpdateConfiguredProjectForFile(fileName: string) { let searchPath = ts.normalizePath(getDirectoryPath(fileName)); this.log("Search path: " + searchPath, "Info"); @@ -1099,7 +1166,7 @@ namespace ts.server { return { succeeded: false, error: { errorMsg: "tsconfig option errors" } }; } else if (parsedCommandLine.fileNames == null) { - return { succeeded: false, error: { errorMsg: "no files found" } } + return { succeeded: false, error: { errorMsg: "no files found" } }; } else { var projectOptions: ProjectOptions = { @@ -1118,27 +1185,32 @@ namespace ts.server { return error; } else { - let proj = this.createProject(configFilename, projectOptions); - for (let i = 0, len = projectOptions.files.length; i < len; i++) { - let rootFilename = projectOptions.files[i]; + let project = this.createProject(configFilename, projectOptions); + for (let rootFilename of projectOptions.files) { if (this.host.fileExists(rootFilename)) { let info = this.openFile(rootFilename, /*openedByClient*/ clientFileName == rootFilename); - proj.addRoot(info); + project.addRoot(info); } else { return { errorMsg: "specified file " + rootFilename + " not found" }; } } - proj.finishGraph(); - proj.projectFileWatcher = this.host.watchFile(configFilename, _ => this.watchedProjectConfigFileChanged(proj)); - return { success: true, project: proj }; + project.finishGraph(); + project.projectFileWatcher = this.host.watchFile(configFilename, _ => this.watchedProjectConfigFileChanged(project)); + this.log("Add recursive watcher for: " + ts.getDirectoryPath(configFilename)); + project.directoryWatcher = this.host.watchDirectory( + ts.getDirectoryPath(configFilename), + path => this.directoryWatchedForSourceFilesChanged(project, path), + /*recursive*/ true + ); + return { success: true, project: project }; } } updateConfiguredProject(project: Project) { if (!this.host.fileExists(project.projectFilename)) { this.log("Config file deleted"); - this.removeConfiguredProject(project); + this.removeProject(project); } else { let { succeeded, projectOptions, error } = this.configFileToProjectOptions(project.projectFilename); From 9db53f23cfffd88067ce241f0dc4841359c0a760 Mon Sep 17 00:00:00 2001 From: zhengbli Date: Mon, 5 Oct 2015 13:12:13 -0700 Subject: [PATCH 08/21] Add directory watcher to tsc --- src/compiler/sys.ts | 2 +- src/compiler/tsc.ts | 57 ++++++++++++++++++++++++++++++++++----------- 2 files changed, 45 insertions(+), 14 deletions(-) diff --git a/src/compiler/sys.ts b/src/compiler/sys.ts index 02728dffaf4..840e8de8271 100644 --- a/src/compiler/sys.ts +++ b/src/compiler/sys.ts @@ -423,7 +423,7 @@ namespace ts { (eventName: string, relativeFileName: string) => { if (eventName == "rename") { // when deleting a file, the passed baseFileName is null - callback(relativeFileName == null ? null : ts.combinePaths(path, ts.normalizeSlashes(relativeFileName))) + callback(relativeFileName == null ? null : normalizePath(ts.combinePaths(path, relativeFileName))) }; } ); diff --git a/src/compiler/tsc.ts b/src/compiler/tsc.ts index 02b8e636772..d5682d8860e 100644 --- a/src/compiler/tsc.ts +++ b/src/compiler/tsc.ts @@ -149,6 +149,7 @@ namespace ts { 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 @@ -218,28 +219,43 @@ namespace ts { if (configFileName) { configFileWatcher = sys.watchFile(configFileName, configFileChanged); } + if (sys.watchDirectory && configFileName) { + let directory = ts.getDirectoryPath(configFileName); + directoryWatcher = sys.watchDirectory( + // When the configFileName is just "tsconfig.json", the watched directory should be + // the current direcotry; if there is a given "project" parameter, then the configFileName + // is an absolute file name. + directory == "" ? "." : directory, + watchedDirectoryChanged, /*recursive*/ true); + } } performCompilation(); + function configFileToParsedCommandLine(configFilename: string): ParsedCommandLine { + let result = readConfigFile(configFileName, sys.readFile); + if (result.error) { + reportWatchDiagnostic(result.error); + sys.exit(ExitStatus.DiagnosticsPresent_OutputsSkipped); + return; + } + + let configObject = result.config; + let configParseResult = parseConfigFile(configObject, sys, getDirectoryPath(configFileName)); + if (configParseResult.errors.length > 0) { + reportDiagnostics(configParseResult.errors); + sys.exit(ExitStatus.DiagnosticsPresent_OutputsSkipped); + return; + } + return configParseResult; + } + // Invoked to perform initial compilation or re-compilation in watch mode function performCompilation() { if (!cachedProgram) { if (configFileName) { - - let result = readConfigFile(configFileName, sys.readFile); - if (result.error) { - reportWatchDiagnostic(result.error); - return sys.exit(ExitStatus.DiagnosticsPresent_OutputsSkipped); - } - - let configObject = result.config; - let configParseResult = parseConfigFile(configObject, sys, getDirectoryPath(configFileName)); - if (configParseResult.errors.length > 0) { - reportDiagnostics(configParseResult.errors); - return sys.exit(ExitStatus.DiagnosticsPresent_OutputsSkipped); - } + let configParseResult = configFileToParsedCommandLine(configFileName); rootFileNames = configParseResult.fileNames; compilerOptions = extend(commandLine.options, configParseResult.options); } @@ -309,6 +325,21 @@ namespace ts { startTimer(); } + function watchedDirectoryChanged(fileName: string) { + if (fileName && !ts.isSupportedSourceFileName(fileName)) { + return; + } + + let parsedCommandLine = configFileToParsedCommandLine(configFileName); + let newFileNames = parsedCommandLine.fileNames.map(compilerHost.getCanonicalFileName); + let canonicalRootFileNames = rootFileNames.map(compilerHost.getCanonicalFileName); + + if (!doTwoArraysHaveTheSameElements(newFileNames, canonicalRootFileNames)) { + setCachedProgram(undefined); + startTimer(); + } + } + // 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. From 7741ec09999af8c387116a80069f09766c28a240 Mon Sep 17 00:00:00 2001 From: zhengbli Date: Mon, 5 Oct 2015 14:07:51 -0700 Subject: [PATCH 09/21] Use fs.watch for all directory watchers and some bug fixes --- src/compiler/sys.ts | 37 +++++++++++++----------------------- src/server/editorServices.ts | 2 +- src/server/session.ts | 1 + 3 files changed, 15 insertions(+), 25 deletions(-) diff --git a/src/compiler/sys.ts b/src/compiler/sys.ts index 840e8de8271..55235e3eda0 100644 --- a/src/compiler/sys.ts +++ b/src/compiler/sys.ts @@ -410,30 +410,19 @@ namespace ts { watchDirectory: (path, 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) - // therefore if the current node.js version is newer than 4, use `fs.watch` instead. - - // 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 (isNode4OrLater()) { - return _fs.watch( - path, - { persisten: true, recursive: !!recursive }, - (eventName: string, relativeFileName: string) => { - if (eventName == "rename") { - // when deleting a file, the passed baseFileName is null - callback(relativeFileName == null ? null : normalizePath(ts.combinePaths(path, relativeFileName))) - }; - } - ); - } - - // If Node version is older than 4.0, the "recursive" parameter will be ignored - var watchedFile = watchedFileSet.addFile(path, callback); - return { - close: () => watchedFileSet.removeFile(watchedFile) - } + return _fs.watch( + path, + { persisten: true, recursive: !!recursive }, + (eventName: string, relativeFileName: string) => { + // 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 == null ? null : normalizePath(ts.combinePaths(path, relativeFileName))) + }; + } + ); }, resolvePath: function (path: string): string { return _path.resolve(path); diff --git a/src/server/editorServices.ts b/src/server/editorServices.ts index 80cebe665ae..49ddd3a1a4c 100644 --- a/src/server/editorServices.ts +++ b/src/server/editorServices.ts @@ -927,7 +927,7 @@ namespace ts.server { var rootedProject = rootFile.defaultProject; var referencingProjects = this.findReferencingProjects(rootFile, rootedProject); - if (rootFile.defaultProject.isConfiguredProject()) { + if (rootFile.defaultProject && rootFile.defaultProject.isConfiguredProject()) { // If the root file has already been added into a configured project, // meaning the original inferred project is gone already. if (!rootedProject.isConfiguredProject()) { diff --git a/src/server/session.ts b/src/server/session.ts index da044e7b4c6..edc79838c92 100644 --- a/src/server/session.ts +++ b/src/server/session.ts @@ -779,6 +779,7 @@ namespace ts.server { } private closeClientFile(fileName: string) { + if (!fileName) { return; } var file = ts.normalizePath(fileName); this.projectService.closeClientFile(file); } From 17f0cce7724e52da55db0b5903c8dfc46d35fc84 Mon Sep 17 00:00:00 2001 From: zhengbli Date: Mon, 5 Oct 2015 14:31:43 -0700 Subject: [PATCH 10/21] Update comments --- src/compiler/sys.ts | 4 ++-- src/server/editorServices.ts | 15 +++++++++------ 2 files changed, 11 insertions(+), 8 deletions(-) diff --git a/src/compiler/sys.ts b/src/compiler/sys.ts index 55235e3eda0..1b16efe4a7a 100644 --- a/src/compiler/sys.ts +++ b/src/compiler/sys.ts @@ -398,8 +398,8 @@ namespace ts { // and https://github.com/Microsoft/TypeScript/issues/4643), therefore // if the current node.js version is newer than 4, use `fs.watch` instead. if (isNode4OrLater()) { - // Note: in node the callback of fs.watch is given only the base file name as a parameter - return _fs.watch(fileName, (eventName: string, baseFileName: string) => callback(fileName)); + // Note: in node the callback of fs.watch is given only the relative file name as a parameter + return _fs.watch(fileName, (eventName: string, relativeFileName: string) => callback(fileName)); } var watchedFile = watchedFileSet.addFile(fileName, callback); diff --git a/src/server/editorServices.ts b/src/server/editorServices.ts index 49ddd3a1a4c..974f51906f6 100644 --- a/src/server/editorServices.ts +++ b/src/server/editorServices.ts @@ -543,7 +543,7 @@ namespace ts.server { } /** - * This is the callback function when a watched directory has added or removed files. + * This is the callback function when a watched directory has added or removed source code files. * @param project the project that associates with this directory watcher * @param fileName the absolute file name that changed in watched directory */ @@ -567,12 +567,15 @@ namespace ts.server { // just update the current project. this.updateConfiguredProject(project); - // Call updateProjectStructure to clean up inferred projects we may have created for the - // new files + // Call updateProjectStructure to clean up inferred projects we may have + // created for the new files this.updateProjectStructure(); } } + /** + * This is the callback function when a watched directory has an added tsconfig file. + */ directoryWatchedForTsconfigChanged(fileName: string) { if (ts.getBaseFileName(fileName) != "tsconfig.json") { this.log(fileName + " is not tsconfig.json"); @@ -585,6 +588,8 @@ namespace ts.server { let rootFilesInTsconfig = projectOptions.files.map(f => this.getCanonicalFileName(f)); let openFileRoots = this.openFileRoots.map(s => this.getCanonicalFileName(s.fileName)); + // We should only care about the new tsconfig file if it contains any + // opened root files of existing inferred projects for (let openFileRoot of openFileRoots) { if (rootFilesInTsconfig.indexOf(openFileRoot) >= 0) { this.reloadProjects(); @@ -708,6 +713,7 @@ namespace ts.server { } else { for (let directory of project.directoriesWatchedForTsconfig) { + // if the ref count for this directory watcher drops to 0, it's time to close it if (!(--project.projectService.directoryWatchersRefCount[directory])) { this.log("Close directory watcher for: " + directory); project.projectService.directoryWatchersForTsconfig[directory].close(); @@ -956,9 +962,6 @@ namespace ts.server { this.addOpenFile(unattachedOpenFiles[i]); } this.printProjects(); - - this.log("Current openFileRoots: " + this.openFileRoots.map(s => s.fileName).toString()); - this.log("Current openFileRootsConfigured: " + this.openFileRootsConfigured.map(s => s.fileName).toString()); } getScriptInfo(filename: string) { From f7e35d597589af03fcffdecb74061a27f3538bb5 Mon Sep 17 00:00:00 2001 From: zhengbli Date: Mon, 5 Oct 2015 16:19:09 -0700 Subject: [PATCH 11/21] Incorporating changes from #3780 --- src/compiler/sys.ts | 8 ++++---- src/compiler/tsc.ts | 10 ++++++++-- 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/src/compiler/sys.ts b/src/compiler/sys.ts index 1b16efe4a7a..e8ad3c309fd 100644 --- a/src/compiler/sys.ts +++ b/src/compiler/sys.ts @@ -8,7 +8,7 @@ namespace ts { write(s: string): void; readFile(path: string, encoding?: string): string; writeFile(path: string, data: string, writeByteOrderMark?: boolean): void; - watchFile?(path: string, callback: (path: string) => void): FileWatcher; + watchFile?(path: string, callback: (path: string, removed?: boolean) => void): FileWatcher; watchDirectory?(path: string, callback: (path: string) => void, recursive?: boolean): FileWatcher; resolvePath(path: string): string; fileExists(path: string): boolean; @@ -23,7 +23,7 @@ namespace ts { interface WatchedFile { fileName: string; - callback: (fileName: string) => void; + callback: (fileName: string, removed?: boolean) => void; mtime: Date; } @@ -235,7 +235,7 @@ namespace ts { } else if (watchedFile.mtime.getTime() !== stats.mtime.getTime()) { watchedFile.mtime = WatchedFileSet.getModifiedTime(watchedFile.fileName); - watchedFile.callback(watchedFile.fileName); + watchedFile.callback(watchedFile.fileName, watchedFile.mtime.getTime() === 0); } }); } @@ -263,7 +263,7 @@ namespace ts { }, this.interval); } - addFile(fileName: string, callback: (fileName: string) => void): WatchedFile { + addFile(fileName: string, callback: (fileName: string, removed?: boolean) => void): WatchedFile { var file: WatchedFile = { fileName, callback, diff --git a/src/compiler/tsc.ts b/src/compiler/tsc.ts index d5682d8860e..a6d51542989 100644 --- a/src/compiler/tsc.ts +++ b/src/compiler/tsc.ts @@ -291,7 +291,7 @@ namespace ts { let sourceFile = hostGetSourceFile(fileName, languageVersion, onError); if (sourceFile && compilerOptions.watch) { // Attach a file watcher - sourceFile.fileWatcher = sys.watchFile(sourceFile.fileName, () => sourceFileChanged(sourceFile)); + sourceFile.fileWatcher = sys.watchFile(sourceFile.fileName, (fileName: string, removed?: boolean) => sourceFileChanged(sourceFile, removed)); } return sourceFile; } @@ -313,9 +313,15 @@ namespace ts { } // If a source file changes, mark it as unwatched and start the recompilation timer - function sourceFileChanged(sourceFile: SourceFile) { + function sourceFileChanged(sourceFile: SourceFile, removed?: boolean) { sourceFile.fileWatcher.close(); sourceFile.fileWatcher = undefined; + if (removed) { + var index = rootFileNames.indexOf(sourceFile.fileName); + if (index !== -1) { + rootFileNames.splice(index, 1); + } + } startTimer(); } From 8171dede90ba326575cc992d584c1a98a7695a60 Mon Sep 17 00:00:00 2001 From: zhengbli Date: Tue, 6 Oct 2015 12:33:06 -0700 Subject: [PATCH 12/21] Lint fixes and test fixes --- src/compiler/sys.ts | 20 ++++++++++---------- src/harness/harnessLanguageService.ts | 8 +++++++- 2 files changed, 17 insertions(+), 11 deletions(-) diff --git a/src/compiler/sys.ts b/src/compiler/sys.ts index e8ad3c309fd..d191d406c52 100644 --- a/src/compiler/sys.ts +++ b/src/compiler/sys.ts @@ -210,7 +210,7 @@ namespace ts { } private static copyListRemovingItem(item: T, list: T[]) { - var copiedList: T[] = []; + let copiedList: T[] = []; for (var i = 0, len = list.length; i < len; i++) { if (list[i] != item) { copiedList.push(list[i]); @@ -224,7 +224,7 @@ namespace ts { } private poll(checkedIndex: number) { - var watchedFile = this.watchedFiles[checkedIndex]; + let watchedFile = this.watchedFiles[checkedIndex]; if (!watchedFile) { return; } @@ -245,9 +245,9 @@ namespace ts { // and efficiency of stat on modern filesystems private startWatchTimer() { this.watchTimer = setInterval(() => { - var count = 0; - var nextToCheck = this.nextFileToCheck; - var firstCheck = -1; + let count = 0; + let nextToCheck = this.nextFileToCheck; + let firstCheck = -1; while ((count < this.chunkSize) && (nextToCheck !== firstCheck)) { this.poll(nextToCheck); if (firstCheck < 0) { @@ -264,7 +264,7 @@ namespace ts { } addFile(fileName: string, callback: (fileName: string, removed?: boolean) => void): WatchedFile { - var file: WatchedFile = { + let file: WatchedFile = { fileName, callback, mtime: WatchedFileSet.getModifiedTime(fileName) @@ -295,7 +295,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? - var watchedFileSet = new WatchedFileSet(); + let watchedFileSet = new WatchedFileSet(); function isNode4OrLater(): Boolean { return parseInt(process.version.charAt(1)) >= 4; @@ -402,10 +402,10 @@ namespace ts { return _fs.watch(fileName, (eventName: string, relativeFileName: string) => callback(fileName)); } - var watchedFile = watchedFileSet.addFile(fileName, callback); + let watchedFile = watchedFileSet.addFile(fileName, callback); return { close: () => watchedFileSet.removeFile(watchedFile) - } + }; }, watchDirectory: (path, callback, recursive) => { // Node 4.0 `fs.watch` function supports the "recursive" option on both OSX and Windows @@ -419,7 +419,7 @@ namespace ts { // event name is "change") if (eventName == "rename") { // When deleting a file, the passed baseFileName is null - callback(relativeFileName == null ? null : normalizePath(ts.combinePaths(path, relativeFileName))) + callback(!relativeFileName ? relativeFileName : normalizePath(ts.combinePaths(path, relativeFileName))); }; } ); diff --git a/src/harness/harnessLanguageService.ts b/src/harness/harnessLanguageService.ts index dfde2bd1c08..df5efccdcb5 100644 --- a/src/harness/harnessLanguageService.ts +++ b/src/harness/harnessLanguageService.ts @@ -572,6 +572,10 @@ namespace Harness.LanguageService { return { close() { } }; } + watchDirectory(path: string, callback: (path: string) => void, recursive?: boolean): ts.FileWatcher { + return { close() { } }; + } + close(): void { } @@ -614,7 +618,9 @@ namespace Harness.LanguageService { // This host is just a proxy for the clientHost, it uses the client // host to answer server queries about files on disk let serverHost = new SessionServerHost(clientHost); - let server = new ts.server.Session(serverHost, Buffer.byteLength, process.hrtime, serverHost); + let server = new ts.server.Session(serverHost, + Buffer ? Buffer.byteLength : (string: string, encoding?: string) => string.length, + process.hrtime, serverHost); // Fake the connection between the client and the server serverHost.writeMessage = client.onMessage.bind(client); From 9ed5b4c435825cdc70643bfb8d391416d2c1ec2c Mon Sep 17 00:00:00 2001 From: zhengbli Date: Tue, 6 Oct 2015 13:10:03 -0700 Subject: [PATCH 13/21] more test fixes --- tests/cases/unittests/cachingInServerLSHost.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/tests/cases/unittests/cachingInServerLSHost.ts b/tests/cases/unittests/cachingInServerLSHost.ts index ff2da76ba4f..88b44a693b9 100644 --- a/tests/cases/unittests/cachingInServerLSHost.ts +++ b/tests/cases/unittests/cachingInServerLSHost.ts @@ -45,6 +45,11 @@ module ts { return { close: () => { } } + }, + watchDirectory: (path, callback, recursive?) => { + return { + close: () => { } + } } }; } From 0fa89ad99c41ab747511baf2ba6eefb0d48e5a9c Mon Sep 17 00:00:00 2001 From: Ron Buckton Date: Mon, 12 Oct 2015 17:38:55 -0700 Subject: [PATCH 14/21] Fixes #5104. --- src/compiler/core.ts | 8 +++-- .../reference/decoratorCallGeneric.errors.txt | 22 +++++++++++++ .../reference/decoratorCallGeneric.js | 31 +++++++++++++++++++ .../decorators/decoratorCallGeneric.ts | 12 +++++++ 4 files changed, 71 insertions(+), 2 deletions(-) create mode 100644 tests/baselines/reference/decoratorCallGeneric.errors.txt create mode 100644 tests/baselines/reference/decoratorCallGeneric.js create mode 100644 tests/cases/conformance/decorators/decoratorCallGeneric.ts diff --git a/src/compiler/core.ts b/src/compiler/core.ts index ce59c3b3bc6..a310ceec457 100644 --- a/src/compiler/core.ts +++ b/src/compiler/core.ts @@ -437,8 +437,12 @@ namespace ts { } export function concatenateDiagnosticMessageChains(headChain: DiagnosticMessageChain, tailChain: DiagnosticMessageChain): DiagnosticMessageChain { - Debug.assert(!headChain.next); - headChain.next = tailChain; + let lastChain = headChain; + while (lastChain.next) { + lastChain = lastChain.next; + } + + lastChain.next = tailChain; return headChain; } diff --git a/tests/baselines/reference/decoratorCallGeneric.errors.txt b/tests/baselines/reference/decoratorCallGeneric.errors.txt new file mode 100644 index 00000000000..c8443420740 --- /dev/null +++ b/tests/baselines/reference/decoratorCallGeneric.errors.txt @@ -0,0 +1,22 @@ +tests/cases/conformance/decorators/decoratorCallGeneric.ts(7,2): error TS1238: Unable to resolve signature of class decorator when called as an expression. + The type argument for type parameter 'T' cannot be inferred from the usage. Consider specifying the type arguments explicitly. + Type argument candidate 'C' is not a valid type argument because it is not a supertype of candidate 'void'. + + +==== tests/cases/conformance/decorators/decoratorCallGeneric.ts (1 errors) ==== + interface I { + prototype: T, + m: () => T + } + function dec(c: I) { } + + @dec + ~~~ +!!! error TS1238: Unable to resolve signature of class decorator when called as an expression. +!!! error TS1238: The type argument for type parameter 'T' cannot be inferred from the usage. Consider specifying the type arguments explicitly. +!!! error TS1238: Type argument candidate 'C' is not a valid type argument because it is not a supertype of candidate 'void'. + class C { + _brand: any; + static m() {} + } + \ No newline at end of file diff --git a/tests/baselines/reference/decoratorCallGeneric.js b/tests/baselines/reference/decoratorCallGeneric.js new file mode 100644 index 00000000000..8c141d5a131 --- /dev/null +++ b/tests/baselines/reference/decoratorCallGeneric.js @@ -0,0 +1,31 @@ +//// [decoratorCallGeneric.ts] +interface I { + prototype: T, + m: () => T +} +function dec(c: I) { } + +@dec +class C { + _brand: any; + static m() {} +} + + +//// [decoratorCallGeneric.js] +var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) { + var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d; + if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc); + else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r; + return c > 3 && r && Object.defineProperty(target, key, r), r; +}; +function dec(c) { } +var C = (function () { + function C() { + } + C.m = function () { }; + C = __decorate([ + dec + ], C); + return C; +})(); diff --git a/tests/cases/conformance/decorators/decoratorCallGeneric.ts b/tests/cases/conformance/decorators/decoratorCallGeneric.ts new file mode 100644 index 00000000000..3eeaae6837c --- /dev/null +++ b/tests/cases/conformance/decorators/decoratorCallGeneric.ts @@ -0,0 +1,12 @@ +// @experimentalDecorators: true +interface I { + prototype: T, + m: () => T +} +function dec(c: I) { } + +@dec +class C { + _brand: any; + static m() {} +} From 002f0c066b3d6f117bf418ffaf3b51397a0d2efc Mon Sep 17 00:00:00 2001 From: zhengbli Date: Wed, 14 Oct 2015 15:10:05 -0700 Subject: [PATCH 15/21] CR feedback --- src/compiler/commandLineParser.ts | 6 +-- src/compiler/core.ts | 15 ++---- src/compiler/sys.ts | 75 ++++++++++++++--------------- src/compiler/tsc.ts | 78 +++++++++++++++++++------------ src/compiler/utilities.ts | 10 ++++ src/server/editorServices.ts | 11 ++--- src/services/shims.ts | 4 +- 7 files changed, 107 insertions(+), 92 deletions(-) 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, From def268cccf383c9b26b20186926bd3076c30cb33 Mon Sep 17 00:00:00 2001 From: zhengbli Date: Wed, 14 Oct 2015 15:48:25 -0700 Subject: [PATCH 16/21] Fix issues with removing roots --- src/server/editorServices.ts | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/src/server/editorServices.ts b/src/server/editorServices.ts index 0da23261db4..3cfdadd7785 100644 --- a/src/server/editorServices.ts +++ b/src/server/editorServices.ts @@ -230,6 +230,7 @@ namespace ts.server { if (scriptInfo) { this.filenameToScript[info.fileName] = undefined; this.roots = copyListRemovingItem(info, this.roots); + this.resolvedModuleNames.remove(info.fileName); } } @@ -557,8 +558,8 @@ namespace ts.server { this.log("Detected source file changes: " + fileName); let { succeeded, projectOptions, error } = this.configFileToProjectOptions(project.projectFilename); - let newRootFiles = ts.map(projectOptions.files, this.getCanonicalFileName); - let currentRootFiles = ts.map(project.getRootFiles(), this.getCanonicalFileName); + let newRootFiles = projectOptions.files.map((f => this.getCanonicalFileName(f))); + let currentRootFiles = project.getRootFiles().map((f => this.getCanonicalFileName(f))); if (!arrayStructurallyIsEqualTo(currentRootFiles, newRootFiles)) { // For configured projects, the change is made outside the tsconfig file, and @@ -673,6 +674,9 @@ namespace ts.server { if (!info.isOpen) { this.filenameToScriptInfo[info.fileName] = undefined; var referencingProjects = this.findReferencingProjects(info); + if (info.defaultProject) { + info.defaultProject.removeRoot(info); + } for (var i = 0, len = referencingProjects.length; i < len; i++) { referencingProjects[i].removeReferencedFile(info); } @@ -1227,7 +1231,9 @@ namespace ts.server { for (let fileName of fileNamesToRemove) { let info = this.getScriptInfo(fileName); - project.removeRoot(info); + if (info) { + project.removeRoot(info); + } } for (let fileName of fileNamesToAdd) { From 62664fdedac9267111a976994f549e89e0b4b2f2 Mon Sep 17 00:00:00 2001 From: zhengbli Date: Wed, 14 Oct 2015 16:09:41 -0700 Subject: [PATCH 17/21] Add timer for batch processing directory changes --- src/server/editorServices.ts | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/src/server/editorServices.ts b/src/server/editorServices.ts index 3cfdadd7785..535d91aaf98 100644 --- a/src/server/editorServices.ts +++ b/src/server/editorServices.ts @@ -503,6 +503,7 @@ namespace ts.server { // number becomes 0 for a watcher, then we should close it. directoryWatchersRefCount: ts.Map = {}; hostConfiguration: HostConfiguration; + timerForDetectingProjectFilelistChanges: Map = {}; constructor(public host: ServerHost, public psLogger: Logger, public eventHandler?: ProjectServiceEventHandler) { // ts.disableIncrementalParsing = true; @@ -557,6 +558,20 @@ namespace ts.server { } this.log("Detected source file changes: " + fileName); + this.startTimerForDetectingProjectFilelistChanges(project); + } + + startTimerForDetectingProjectFilelistChanges(project: Project) { + if (this.timerForDetectingProjectFilelistChanges[project.projectFilename]) { + clearTimeout(this.timerForDetectingProjectFilelistChanges[project.projectFilename]); + } + this.timerForDetectingProjectFilelistChanges[project.projectFilename] = setTimeout( + () => this.handleProjectFilelistChanges(project), + 250 + ); + } + + handleProjectFilelistChanges(project: Project) { 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))); From 6013968b1fe3fef37ad2fb79bd8d61c1b3b3e671 Mon Sep 17 00:00:00 2001 From: zhengbli Date: Wed, 14 Oct 2015 16:25:27 -0700 Subject: [PATCH 18/21] Address build errors --- src/compiler/core.ts | 16 +++++++--------- src/compiler/utilities.ts | 16 +++++++++------- 2 files changed, 16 insertions(+), 16 deletions(-) diff --git a/src/compiler/core.ts b/src/compiler/core.ts index 0f7c09b4756..b57731d1f76 100644 --- a/src/compiler/core.ts +++ b/src/compiler/core.ts @@ -833,15 +833,13 @@ namespace ts { } } - export function arrayStructurallyIsEqualTo(array1: Array, array2: Array): boolean { - if (!array1 || !array2) { - return false; + export function 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]); + } } - - if (array1.length !== array2.length) { - return false; - } - - return arrayIsEqualTo(array1.sort(), array2.sort()); + return copiedList; } } diff --git a/src/compiler/utilities.ts b/src/compiler/utilities.ts index 5d0d1240734..385e5c3123c 100644 --- a/src/compiler/utilities.ts +++ b/src/compiler/utilities.ts @@ -2407,13 +2407,15 @@ 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]); - } + export function arrayStructurallyIsEqualTo(array1: Array, array2: Array): boolean { + if (!array1 || !array2) { + return false; } - return copiedList; + + if (array1.length !== array2.length) { + return false; + } + + return arrayIsEqualTo(array1.sort(), array2.sort()); } } From c75499974ef5f2aee677907b2d944e3b462aabe8 Mon Sep 17 00:00:00 2001 From: zhengbli Date: Wed, 14 Oct 2015 16:31:27 -0700 Subject: [PATCH 19/21] Fix rwcRunner --- src/harness/rwcRunner.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/harness/rwcRunner.ts b/src/harness/rwcRunner.ts index 3027afae9dc..19c989bcd6f 100644 --- a/src/harness/rwcRunner.ts +++ b/src/harness/rwcRunner.ts @@ -78,8 +78,8 @@ namespace RWC { let tsconfigFile = ts.forEach(ioLog.filesRead, f => isTsConfigFile(f) ? f : undefined); if (tsconfigFile) { let tsconfigFileContents = getHarnessCompilerInputUnit(tsconfigFile.path); - let parsedTsconfigFileContents = ts.parseConfigFileText(tsconfigFile.path, tsconfigFileContents.content); - let configParseResult = ts.parseConfigFile(parsedTsconfigFileContents.config, Harness.IO, ts.getDirectoryPath(tsconfigFile.path)); + let parsedTsconfigFileContents = ts.parseConfigFileTextToJson(tsconfigFile.path, tsconfigFileContents.content); + let configParseResult = ts.parseJsonConfigFileContent(parsedTsconfigFileContents.config, Harness.IO, ts.getDirectoryPath(tsconfigFile.path)); fileNames = configParseResult.fileNames; opts.options = ts.extend(opts.options, configParseResult.options); } From f91bee0324328a8f3d1b159968ae303a9536b44d Mon Sep 17 00:00:00 2001 From: zhengbli Date: Wed, 14 Oct 2015 16:50:31 -0700 Subject: [PATCH 20/21] Re-read file content upon closing --- src/server/editorServices.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/server/editorServices.ts b/src/server/editorServices.ts index 436b97821cc..64050cd5457 100644 --- a/src/server/editorServices.ts +++ b/src/server/editorServices.ts @@ -683,6 +683,11 @@ namespace ts.server { * @param info The file that has been closed or newly configured */ closeOpenFile(info: ScriptInfo) { + // Closing file should trigger re-reading the file content from disk. This is + // because the user may chose to discard the buffer content before saving + // to the disk, and the server's version of the file can be out of sync. + info.svc.reloadFromFile(info.fileName); + var openFileRoots: ScriptInfo[] = []; var removedProject: Project; for (var i = 0, len = this.openFileRoots.length; i < len; i++) { From fcfc25eeb040390492716626757274c945ebd47f Mon Sep 17 00:00:00 2001 From: zhengbli Date: Wed, 14 Oct 2015 16:57:08 -0700 Subject: [PATCH 21/21] Fix lint errors --- src/compiler/core.ts | 4 ++-- src/compiler/sys.ts | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/compiler/core.ts b/src/compiler/core.ts index b57731d1f76..d0f51601b5e 100644 --- a/src/compiler/core.ts +++ b/src/compiler/core.ts @@ -833,7 +833,7 @@ namespace ts { } } - export function copyListRemovingItem(item: T, list: T[]) { + export function copyListRemovingItem(item: T, list: T[]) { let copiedList: T[] = []; for (var i = 0, len = list.length; i < len; i++) { if (list[i] != item) { @@ -842,4 +842,4 @@ namespace ts { } return copiedList; } -} +} \ No newline at end of file diff --git a/src/compiler/sys.ts b/src/compiler/sys.ts index 4bc9af8d30e..0872a2e5ba5 100644 --- a/src/compiler/sys.ts +++ b/src/compiler/sys.ts @@ -274,7 +274,7 @@ namespace ts { startWatchTimer: startWatchTimer, addFile: addFile, removeFile: removeFile - } + }; } // REVIEW: for now this implementation uses polling.