diff --git a/src/compiler/commandLineParser.ts b/src/compiler/commandLineParser.ts
index 2fe3f63450c..682a434e6ab 100644
--- a/src/compiler/commandLineParser.ts
+++ b/src/compiler/commandLineParser.ts
@@ -5,12 +5,15 @@
///
namespace ts {
+ /* @internal */
+ export const compileOnSaveCommandLineOption: CommandLineOption = { name: "compileOnSave", type: "boolean" };
/* @internal */
export const optionDeclarations: CommandLineOption[] = [
{
name: "charset",
type: "string",
},
+ compileOnSaveCommandLineOption,
{
name: "declaration",
shortName: "d",
@@ -808,6 +811,7 @@ namespace ts {
options.configFilePath = configFileName;
const { fileNames, wildcardDirectories } = getFileNames(errors);
+ const compileOnSave = convertCompileOnSaveOptionFromJson(json, basePath, errors);
return {
options,
@@ -815,7 +819,8 @@ namespace ts {
typingOptions,
raw: json,
errors,
- wildcardDirectories
+ wildcardDirectories,
+ compileOnSave
};
function getFileNames(errors: Diagnostic[]): ExpandResult {
@@ -870,6 +875,17 @@ namespace ts {
}
}
+ export function convertCompileOnSaveOptionFromJson(jsonOption: any, basePath: string, errors: Diagnostic[]): boolean {
+ if (!hasProperty(jsonOption, compileOnSaveCommandLineOption.name)) {
+ return false;
+ }
+ const result = convertJsonOption(compileOnSaveCommandLineOption, jsonOption["compileOnSave"], basePath, errors);
+ if (typeof result === "boolean" && result) {
+ return result;
+ }
+ return false;
+ }
+
export function convertCompilerOptionsFromJson(jsonOptions: any, basePath: string, configFileName?: string): { options: CompilerOptions, errors: Diagnostic[] } {
const errors: Diagnostic[] = [];
const options = convertCompilerOptionsFromJsonWorker(jsonOptions, basePath, errors, configFileName);
diff --git a/src/compiler/core.ts b/src/compiler/core.ts
index 8298a62f937..77cc379acc2 100644
--- a/src/compiler/core.ts
+++ b/src/compiler/core.ts
@@ -1,4 +1,4 @@
-///
+///
///
@@ -47,6 +47,7 @@ namespace ts {
contains,
remove,
forEachValue: forEachValueInMap,
+ getKeys,
clear,
};
@@ -56,6 +57,14 @@ namespace ts {
}
}
+ function getKeys() {
+ const keys: Path[] = [];
+ for (const key in files) {
+ keys.push(key);
+ }
+ return keys;
+ }
+
// path should already be well-formed so it does not need to be normalized
function get(path: Path): T {
return files[toKey(path)];
@@ -311,18 +320,25 @@ namespace ts {
* @param array A sorted array whose first element must be no larger than number
* @param number The value to be searched for in the array.
*/
- export function binarySearch(array: number[], value: number): number {
+ export function binarySearch(array: T[], value: T, comparer?: (v1: T, v2: T) => number): number {
+ if (!array || array.length === 0) {
+ return -1;
+ }
+
let low = 0;
let high = array.length - 1;
+ comparer = comparer !== undefined
+ ? comparer
+ : (v1, v2) => (v1 < v2 ? -1 : (v1 > v2 ? 1 : 0));
while (low <= high) {
const middle = low + ((high - low) >> 1);
const midValue = array[middle];
- if (midValue === value) {
+ if (comparer(midValue, value) === 0) {
return middle;
}
- else if (midValue > value) {
+ else if (comparer(midValue, value) > 0) {
high = middle - 1;
}
else {
diff --git a/src/compiler/emitter.ts b/src/compiler/emitter.ts
index 357a15507a4..ca015edc73e 100644
--- a/src/compiler/emitter.ts
+++ b/src/compiler/emitter.ts
@@ -1,4 +1,4 @@
-///
+///
///
///
@@ -336,7 +336,7 @@ namespace ts {
}
// targetSourceFile is when users only want one file in entire project to be emitted. This is used in compileOnSave feature
- export function emitFiles(resolver: EmitResolver, host: EmitHost, targetSourceFile: SourceFile): EmitResult {
+ export function emitFiles(resolver: EmitResolver, host: EmitHost, targetSourceFile: SourceFile, emitOnlyDtsFiles?: boolean): EmitResult {
// emit output for the __extends helper function
const extendsHelper = `
var __extends = (this && this.__extends) || function (d, b) {
@@ -396,7 +396,7 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
const newLine = host.getNewLine();
const emitJavaScript = createFileEmitter();
- forEachExpectedEmitFile(host, emitFile, targetSourceFile);
+ forEachExpectedEmitFile(host, emitFile, targetSourceFile, emitOnlyDtsFiles);
return {
emitSkipped,
@@ -1615,7 +1615,7 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
else if (declaration.kind === SyntaxKind.ImportSpecifier) {
// Identifier references named import
write(getGeneratedNameForNode(declaration.parent.parent.parent));
- const name = (declaration).propertyName || (declaration).name;
+ const name = (declaration).propertyName || (declaration).name;
const identifier = getTextOfNodeFromSourceText(currentText, name);
if (languageVersion === ScriptTarget.ES3 && identifier === "default") {
write('["default"]');
@@ -3254,19 +3254,19 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
write("var ");
let seen: Map;
for (const id of convertedLoopState.hoistedLocalVariables) {
- // Don't initialize seen unless we have at least one element.
- // Emit a comma to separate for all but the first element.
- if (!seen) {
+ // Don't initialize seen unless we have at least one element.
+ // Emit a comma to separate for all but the first element.
+ if (!seen) {
seen = createMap();
- }
- else {
- write(", ");
- }
+ }
+ else {
+ write(", ");
+ }
if (!(id.text in seen)) {
- emit(id);
- seen[id.text] = id.text;
- }
+ emit(id);
+ seen[id.text] = id.text;
+ }
}
write(";");
writeLine();
@@ -7415,7 +7415,7 @@ const _super = (function (geti, seti) {
// - import equals declarations that import external modules are not emitted
continue;
}
- // fall-though for import declarations that import internal modules
+ // fall-though for import declarations that import internal modules
default:
writeLine();
emit(statement);
@@ -8364,14 +8364,16 @@ const _super = (function (geti, seti) {
}
}
- function emitFile({ jsFilePath, sourceMapFilePath, declarationFilePath}: { jsFilePath: string, sourceMapFilePath: string, declarationFilePath: string },
+ function emitFile({ jsFilePath, sourceMapFilePath, declarationFilePath }: EmitFileNames,
sourceFiles: SourceFile[], isBundledEmit: boolean) {
- // Make sure not to write js File and source map file if any of them cannot be written
- if (!host.isEmitBlocked(jsFilePath) && !compilerOptions.noEmit) {
- emitJavaScript(jsFilePath, sourceMapFilePath, sourceFiles, isBundledEmit);
- }
- else {
- emitSkipped = true;
+ if (!emitOnlyDtsFiles) {
+ // Make sure not to write js File and source map file if any of them cannot be written
+ if (!host.isEmitBlocked(jsFilePath) && !compilerOptions.noEmit) {
+ emitJavaScript(jsFilePath, sourceMapFilePath, sourceFiles, isBundledEmit);
+ }
+ else {
+ emitSkipped = true;
+ }
}
if (declarationFilePath) {
@@ -8379,9 +8381,11 @@ const _super = (function (geti, seti) {
}
if (!emitSkipped && emittedFilesList) {
- emittedFilesList.push(jsFilePath);
- if (sourceMapFilePath) {
- emittedFilesList.push(sourceMapFilePath);
+ if (!emitOnlyDtsFiles) {
+ emittedFilesList.push(jsFilePath);
+ if (sourceMapFilePath) {
+ emittedFilesList.push(sourceMapFilePath);
+ }
}
if (declarationFilePath) {
emittedFilesList.push(declarationFilePath);
diff --git a/src/compiler/parser.ts b/src/compiler/parser.ts
index 4f63448ecd2..3e20f899c2d 100644
--- a/src/compiler/parser.ts
+++ b/src/compiler/parser.ts
@@ -1,4 +1,4 @@
-///
+///
///
namespace ts {
diff --git a/src/compiler/program.ts b/src/compiler/program.ts
index 9deb9674279..932f400660f 100644
--- a/src/compiler/program.ts
+++ b/src/compiler/program.ts
@@ -774,15 +774,15 @@ namespace ts {
return noDiagnosticsTypeChecker || (noDiagnosticsTypeChecker = createTypeChecker(program, /*produceDiagnostics:*/ false));
}
- function emit(sourceFile?: SourceFile, writeFileCallback?: WriteFileCallback, cancellationToken?: CancellationToken): EmitResult {
- return runWithCancellationToken(() => emitWorker(program, sourceFile, writeFileCallback, cancellationToken));
+ function emit(sourceFile?: SourceFile, writeFileCallback?: WriteFileCallback, cancellationToken?: CancellationToken, emitOnlyDtsFiles?: boolean): EmitResult {
+ return runWithCancellationToken(() => emitWorker(program, sourceFile, writeFileCallback, cancellationToken, emitOnlyDtsFiles));
}
function isEmitBlocked(emitFileName: string): boolean {
return hasEmitBlockingDiagnostics.contains(toPath(emitFileName, currentDirectory, getCanonicalFileName));
}
- function emitWorker(program: Program, sourceFile: SourceFile, writeFileCallback: WriteFileCallback, cancellationToken: CancellationToken): EmitResult {
+ function emitWorker(program: Program, sourceFile: SourceFile, writeFileCallback: WriteFileCallback, cancellationToken: CancellationToken, emitOnlyDtsFiles?: boolean): EmitResult {
let declarationDiagnostics: Diagnostic[] = [];
if (options.noEmit) {
@@ -827,7 +827,8 @@ namespace ts {
const emitResult = emitFiles(
emitResolver,
getEmitHost(writeFileCallback),
- sourceFile);
+ sourceFile,
+ emitOnlyDtsFiles);
performance.mark("afterEmit");
performance.measure("Emit", "beforeEmit", "afterEmit");
diff --git a/src/compiler/types.ts b/src/compiler/types.ts
index 974f2a1d82b..202f45eb153 100644
--- a/src/compiler/types.ts
+++ b/src/compiler/types.ts
@@ -19,6 +19,7 @@ namespace ts {
remove(fileName: Path): void;
forEachValue(f: (key: Path, v: T) => void): void;
+ getKeys(): Path[];
clear(): void;
}
@@ -1755,7 +1756,7 @@ namespace ts {
* used for writing the JavaScript and declaration files. Otherwise, the writeFile parameter
* will be invoked when writing the JavaScript and declaration files.
*/
- emit(targetSourceFile?: SourceFile, writeFile?: WriteFileCallback, cancellationToken?: CancellationToken): EmitResult;
+ emit(targetSourceFile?: SourceFile, writeFile?: WriteFileCallback, cancellationToken?: CancellationToken, emitOnlyDtsFiles?: boolean): EmitResult;
getOptionsDiagnostics(cancellationToken?: CancellationToken): Diagnostic[];
getGlobalDiagnostics(cancellationToken?: CancellationToken): Diagnostic[];
@@ -2736,6 +2737,7 @@ namespace ts {
raw?: any;
errors: Diagnostic[];
wildcardDirectories?: MapLike;
+ compileOnSave?: boolean;
}
export const enum WatchDirectoryFlags {
diff --git a/src/compiler/utilities.ts b/src/compiler/utilities.ts
index f0351a39bb5..9d60521d76b 100644
--- a/src/compiler/utilities.ts
+++ b/src/compiler/utilities.ts
@@ -1,4 +1,4 @@
-///
+///
/* @internal */
namespace ts {
@@ -2218,12 +2218,10 @@ namespace ts {
const options = host.getCompilerOptions();
const outputDir = options.declarationDir || options.outDir; // Prefer declaration folder if specified
- if (options.declaration) {
- const path = outputDir
- ? getSourceFilePathInNewDir(sourceFile, host, outputDir)
- : sourceFile.fileName;
- return removeFileExtension(path) + ".d.ts";
- }
+ const path = outputDir
+ ? getSourceFilePathInNewDir(sourceFile, host, outputDir)
+ : sourceFile.fileName;
+ return removeFileExtension(path) + ".d.ts";
}
export interface EmitFileNames {
@@ -2234,7 +2232,8 @@ namespace ts {
export function forEachExpectedEmitFile(host: EmitHost,
action: (emitFileNames: EmitFileNames, sourceFiles: SourceFile[], isBundledEmit: boolean) => void,
- targetSourceFile?: SourceFile) {
+ targetSourceFile?: SourceFile,
+ emitOnlyDtsFiles?: boolean) {
const options = host.getCompilerOptions();
// Emit on each source file
if (options.outFile || options.out) {
@@ -2267,10 +2266,11 @@ namespace ts {
}
}
const jsFilePath = getOwnEmitOutputFilePath(sourceFile, host, extension);
+ const declarationFilePath = !isSourceFileJavaScript(sourceFile) && (emitOnlyDtsFiles || options.declaration) ? getDeclarationEmitOutputFilePath(sourceFile, host) : undefined;
const emitFileNames: EmitFileNames = {
jsFilePath,
sourceMapFilePath: getSourceMapFilePath(jsFilePath, options),
- declarationFilePath: !isSourceFileJavaScript(sourceFile) ? getDeclarationEmitOutputFilePath(sourceFile, host) : undefined
+ declarationFilePath
};
action(emitFileNames, [sourceFile], /*isBundledEmit*/false);
}
diff --git a/src/harness/unittests/tsserverProjectSystem.ts b/src/harness/unittests/tsserverProjectSystem.ts
index 2a309057dbe..d1041e55d0e 100644
--- a/src/harness/unittests/tsserverProjectSystem.ts
+++ b/src/harness/unittests/tsserverProjectSystem.ts
@@ -1,4 +1,4 @@
-///
+///
///
namespace ts {
@@ -415,7 +415,7 @@ namespace ts {
setImmediate(callback: TimeOutCallback, time: number, ...args: any[]) {
return this.immediateCallbacks.register(callback, args);
- };
+ }
clearImmediate(timeoutId: any): void {
this.immediateCallbacks.unregister(timeoutId);
@@ -454,6 +454,23 @@ namespace ts {
readonly exit = () => notImplemented();
}
+ function makeSessionRequest(command: string, args: T) {
+ const newRequest: server.protocol.Request = {
+ seq: 0,
+ type: "request",
+ command,
+ arguments: args
+ };
+ return newRequest;
+ }
+
+ function openFilesForSession(files: FileOrFolder[], session: server.Session) {
+ for (const file of files) {
+ const request = makeSessionRequest(server.CommandNames.Open, { file: file.path });
+ session.executeCommand(request);
+ }
+ }
+
describe("tsserver-project-system", () => {
const commonFile1: FileOrFolder = {
path: "/a/b/commonFile1.ts",
@@ -801,7 +818,7 @@ namespace ts {
content: `{
"compilerOptions": {
"target": "es6"
- },
+ },
"files": [ "main.ts" ]
}`
};
@@ -844,7 +861,7 @@ namespace ts {
content: `{
"compilerOptions": {
"target": "es6"
- },
+ },
"files": [ "main.ts" ]
}`
};
@@ -1483,6 +1500,381 @@ namespace ts {
});
});
+ describe("CompileOnSave affected list", () => {
+ function sendAffectedFileRequestAndCheckResult(session: server.Session, request: server.protocol.Request, expectedFileList: FileOrFolder[]) {
+ const actualResult = session.executeCommand(request).response;
+ const expectedFileNameList = expectedFileList.length > 0 ? ts.map(expectedFileList, f => f.path).sort() : [];
+ const actualFileNameList = actualResult.sort();
+ assert.isTrue(arrayIsEqualTo(actualFileNameList, expectedFileNameList), `Actual result is ${actualFileNameList}, while expected ${expectedFileNameList}`);
+ }
+
+ describe("for configured projects", () => {
+ let moduleFile1: FileOrFolder;
+ let file1Consumer1: FileOrFolder;
+ let file1Consumer2: FileOrFolder;
+ let moduleFile2: FileOrFolder;
+ let globalFile3: FileOrFolder;
+ let configFile: FileOrFolder;
+ let changeModuleFile1ShapeRequest1: server.protocol.Request;
+ let changeModuleFile1InternalRequest1: server.protocol.Request;
+ let changeModuleFile1ShapeRequest2: server.protocol.Request;
+ // A compile on save affected file request using file1
+ let moduleFile1FileListRequest: server.protocol.Request;
+ let host: TestServerHost;
+ let typingsInstaller: server.ITypingsInstaller;
+ let session: server.Session;
+
+ beforeEach(() => {
+ moduleFile1 = {
+ path: "/a/b/moduleFile1.ts",
+ content: "export function Foo() { };"
+ };
+
+ file1Consumer1 = {
+ path: "/a/b/file1Consumer1.ts",
+ content: `import {Foo} from "./moduleFile1"; export var y = 10;`
+ };
+
+ file1Consumer2 = {
+ path: "/a/b/file1Consumer2.ts",
+ content: `import {Foo} from "./moduleFile1"; let z = 10;`
+ };
+
+ moduleFile2 = {
+ path: "/a/b/moduleFile2.ts",
+ content: `export var Foo4 = 10;`
+ };
+
+ globalFile3 = {
+ path: "/a/b/globalFile3.ts",
+ content: `interface GlobalFoo { age: number }`
+ };
+
+ configFile = {
+ path: "/a/b/tsconfig.json",
+ content: `{
+ "compileOnSave": true
+ }`
+ };
+
+ // Change the content of file1 to `export var T: number;export function Foo() { };`
+ changeModuleFile1ShapeRequest1 = makeSessionRequest(server.CommandNames.Change, {
+ file: moduleFile1.path,
+ line: 1,
+ offset: 1,
+ endLine: 1,
+ endOffset: 1,
+ insertString: `export var T: number;`
+ });
+
+ // Change the content of file1 to `export var T: number;export function Foo() { };`
+ changeModuleFile1InternalRequest1 = makeSessionRequest(server.CommandNames.Change, {
+ file: moduleFile1.path,
+ line: 1,
+ offset: 1,
+ endLine: 1,
+ endOffset: 1,
+ insertString: `var T1: number;`
+ });
+
+ // Change the content of file1 to `export var T: number;export function Foo() { };`
+ changeModuleFile1ShapeRequest2 = makeSessionRequest(server.CommandNames.Change, {
+ file: moduleFile1.path,
+ line: 1,
+ offset: 1,
+ endLine: 1,
+ endOffset: 1,
+ insertString: `export var T2: number;`
+ });
+
+ moduleFile1FileListRequest = makeSessionRequest(server.CommandNames.CompileOnSaveAffectedFileList, { file: moduleFile1.path });
+
+ host = createServerHost([moduleFile1, file1Consumer1, file1Consumer2, globalFile3, moduleFile2, configFile, libFile]);
+ typingsInstaller = new TestTypingsInstaller("/a/data/", host);
+ session = new server.Session(host, nullCancellationToken, /*useSingleInferredProject*/ false, typingsInstaller, Utils.byteLength, process.hrtime, nullLogger, /*canUseEvents*/ false);
+ });
+
+ it("should contains only itself if a module file's shape didn't change, and all files referencing it if its shape changed", () => {
+ openFilesForSession([moduleFile1, file1Consumer1], session);
+
+ // Send an initial compileOnSave request
+ sendAffectedFileRequestAndCheckResult(session, moduleFile1FileListRequest, [moduleFile1, file1Consumer1, file1Consumer2]);
+ session.executeCommand(changeModuleFile1ShapeRequest1);
+ sendAffectedFileRequestAndCheckResult(session, moduleFile1FileListRequest, [moduleFile1, file1Consumer1, file1Consumer2]);
+
+ // Change the content of file1 to `export var T: number;export function Foo() { console.log('hi'); };`
+ const changeFile1InternalRequest = makeSessionRequest(server.CommandNames.Change, {
+ file: moduleFile1.path,
+ line: 1,
+ offset: 46,
+ endLine: 1,
+ endOffset: 46,
+ insertString: `console.log('hi');`
+ });
+ session.executeCommand(changeFile1InternalRequest);
+ sendAffectedFileRequestAndCheckResult(session, moduleFile1FileListRequest, [moduleFile1]);
+ });
+
+ it("should be up-to-date with the reference map changes", () => {
+ openFilesForSession([moduleFile1, file1Consumer1], session);
+
+ // Send an initial compileOnSave request
+ sendAffectedFileRequestAndCheckResult(session, moduleFile1FileListRequest, [moduleFile1, file1Consumer1, file1Consumer2]);
+
+ // Change file2 content to `let y = Foo();`
+ const removeFile1Consumer1ImportRequest = makeSessionRequest(server.CommandNames.Change, {
+ file: file1Consumer1.path,
+ line: 1,
+ offset: 1,
+ endLine: 1,
+ endOffset: 28,
+ insertString: ""
+ });
+ session.executeCommand(removeFile1Consumer1ImportRequest);
+ session.executeCommand(changeModuleFile1ShapeRequest1);
+ sendAffectedFileRequestAndCheckResult(session, moduleFile1FileListRequest, [moduleFile1, file1Consumer2]);
+
+ // Add the import statements back to file2
+ const addFile2ImportRequest = makeSessionRequest(server.CommandNames.Change, {
+ file: file1Consumer1.path,
+ line: 1,
+ offset: 1,
+ endLine: 1,
+ endOffset: 1,
+ insertString: `import {Foo} from "./moduleFile1";`
+ });
+ session.executeCommand(addFile2ImportRequest);
+
+ // Change the content of file1 to `export var T2: string;export var T: number;export function Foo() { };`
+ const changeModuleFile1ShapeRequest2 = makeSessionRequest(server.CommandNames.Change, {
+ file: moduleFile1.path,
+ line: 1,
+ offset: 1,
+ endLine: 1,
+ endOffset: 1,
+ insertString: `export var T2: string;`
+ });
+ session.executeCommand(changeModuleFile1ShapeRequest2);
+ sendAffectedFileRequestAndCheckResult(session, moduleFile1FileListRequest, [moduleFile1, file1Consumer1, file1Consumer2]);
+ });
+
+ it("should be up-to-date with changes made in non-open files", () => {
+ openFilesForSession([moduleFile1], session);
+
+ // Send an initial compileOnSave request
+ sendAffectedFileRequestAndCheckResult(session, moduleFile1FileListRequest, [moduleFile1, file1Consumer1, file1Consumer2]);
+
+ file1Consumer1.content = `let y = 10;`;
+ host.reloadFS([moduleFile1, file1Consumer1, file1Consumer2, configFile, libFile]);
+ host.triggerFileWatcherCallback(file1Consumer1.path, /*removed*/ false);
+
+ session.executeCommand(changeModuleFile1ShapeRequest1);
+ sendAffectedFileRequestAndCheckResult(session, moduleFile1FileListRequest, [moduleFile1, file1Consumer2]);
+ });
+
+ it("should be up-to-date with deleted files", () => {
+ openFilesForSession([moduleFile1], session);
+ sendAffectedFileRequestAndCheckResult(session, moduleFile1FileListRequest, [moduleFile1, file1Consumer1, file1Consumer2]);
+
+ session.executeCommand(changeModuleFile1ShapeRequest1);
+ // Delete file1Consumer2
+ host.reloadFS([moduleFile1, file1Consumer1, configFile, libFile]);
+ host.triggerFileWatcherCallback(file1Consumer2.path, /*removed*/ true);
+ sendAffectedFileRequestAndCheckResult(session, moduleFile1FileListRequest, [moduleFile1, file1Consumer1]);
+ });
+
+ it("should be up-to-date with newly created files", () => {
+ openFilesForSession([moduleFile1], session);
+ sendAffectedFileRequestAndCheckResult(session, moduleFile1FileListRequest, [moduleFile1, file1Consumer1, file1Consumer2]);
+
+ const file1Consumer3: FileOrFolder = {
+ path: "/a/b/file1Consumer3.ts",
+ content: `import {Foo} from "./moduleFile1"; let y = Foo();`
+ };
+ host.reloadFS([moduleFile1, file1Consumer1, file1Consumer2, file1Consumer3, globalFile3, configFile, libFile]);
+ host.triggerDirectoryWatcherCallback(ts.getDirectoryPath(file1Consumer3.path), file1Consumer3.path);
+ host.runQueuedTimeoutCallbacks();
+ session.executeCommand(changeModuleFile1ShapeRequest1);
+ sendAffectedFileRequestAndCheckResult(session, moduleFile1FileListRequest, [moduleFile1, file1Consumer1, file1Consumer2, file1Consumer3]);
+ });
+
+ it("should detect changes in non-root files", () => {
+ moduleFile1 = {
+ path: "/a/b/moduleFile1.ts",
+ content: "export function Foo() { };"
+ };
+
+ file1Consumer1 = {
+ path: "/a/b/file1Consumer1.ts",
+ content: `import {Foo} from "./moduleFile1"; let y = Foo();`
+ };
+
+ configFile = {
+ path: "/a/b/tsconfig.json",
+ content: `{
+ "compileOnSave": true,
+ "files": ["${file1Consumer1.path}"]
+ }`
+ };
+
+ host = createServerHost([moduleFile1, file1Consumer1, configFile, libFile]);
+ typingsInstaller = new TestTypingsInstaller("/a/data/", host);
+ session = new server.Session(host, nullCancellationToken, /*useSingleInferredProject*/ false, typingsInstaller, Utils.byteLength, process.hrtime, nullLogger, /*canUseEvents*/ false);
+
+ openFilesForSession([moduleFile1, file1Consumer1], session);
+ sendAffectedFileRequestAndCheckResult(session, moduleFile1FileListRequest, [moduleFile1, file1Consumer1]);
+
+ // change file1 shape now, and verify both files are affected
+ session.executeCommand(changeModuleFile1ShapeRequest1);
+ sendAffectedFileRequestAndCheckResult(session, moduleFile1FileListRequest, [moduleFile1, file1Consumer1]);
+
+ // change file1 internal, and verify only file1 is affected
+ session.executeCommand(changeModuleFile1InternalRequest1);
+ sendAffectedFileRequestAndCheckResult(session, moduleFile1FileListRequest, [moduleFile1]);
+ });
+
+ it("should return all files if a global file changed shape", () => {
+ openFilesForSession([globalFile3], session);
+ const changeGlobalFile3ShapeRequest = makeSessionRequest(server.CommandNames.Change, {
+ file: globalFile3.path,
+ line: 1,
+ offset: 1,
+ endLine: 1,
+ endOffset: 1,
+ insertString: `var T2: string;`
+ });
+
+ // check after file1 shape changes
+ session.executeCommand(changeGlobalFile3ShapeRequest);
+ const globalFile3FileListRequest = makeSessionRequest(server.CommandNames.CompileOnSaveAffectedFileList, { file: globalFile3.path });
+ sendAffectedFileRequestAndCheckResult(session, globalFile3FileListRequest, [moduleFile1, file1Consumer1, file1Consumer2, globalFile3, moduleFile2]);
+ });
+
+ it("should return empty array if CompileOnSave is not enabled", () => {
+ configFile = {
+ path: "/a/b/tsconfig.json",
+ content: `{}`
+ };
+
+ host = createServerHost([moduleFile1, file1Consumer1, file1Consumer2, configFile, libFile]);
+ typingsInstaller = new TestTypingsInstaller("/a/data/", host);
+ session = new server.Session(host, nullCancellationToken, /*useSingleInferredProject*/ false, typingsInstaller, Utils.byteLength, process.hrtime, nullLogger, /*canUseEvents*/ false);
+ openFilesForSession([moduleFile1], session);
+ sendAffectedFileRequestAndCheckResult(session, moduleFile1FileListRequest, []);
+ });
+
+ it("should always return the file itself if '--isolatedModules' is specified", () => {
+ configFile = {
+ path: "/a/b/tsconfig.json",
+ content: `{
+ "compileOnSave": true,
+ "compilerOptions": {
+ "isolatedModules": true
+ }
+ }`
+ };
+
+ host = createServerHost([moduleFile1, file1Consumer1, configFile, libFile]);
+ typingsInstaller = new TestTypingsInstaller("/a/data/", host);
+ session = new server.Session(host, nullCancellationToken, /*useSingleInferredProject*/ false, typingsInstaller, Utils.byteLength, process.hrtime, nullLogger, /*canUseEvents*/ false);
+ openFilesForSession([moduleFile1], session);
+
+ const file1ChangeShapeRequest = makeSessionRequest(server.CommandNames.Change, {
+ file: moduleFile1.path,
+ line: 1,
+ offset: 27,
+ endLine: 1,
+ endOffset: 27,
+ insertString: `Point,`
+ });
+ session.executeCommand(file1ChangeShapeRequest);
+ sendAffectedFileRequestAndCheckResult(session, moduleFile1FileListRequest, [moduleFile1]);
+ });
+
+ it("should always return the file itself if '--out' or '--outFile' is specified", () => {
+ configFile = {
+ path: "/a/b/tsconfig.json",
+ content: `{
+ "compileOnSave": true,
+ "compilerOptions": {
+ "module": "system",
+ "outFile": "/a/b/out.js"
+ }
+ }`
+ };
+
+ host = createServerHost([moduleFile1, file1Consumer1, configFile, libFile]);
+ typingsInstaller = new TestTypingsInstaller("/a/data/", host);
+ session = new server.Session(host, nullCancellationToken, /*useSingleInferredProject*/ false, typingsInstaller, Utils.byteLength, process.hrtime, nullLogger, /*canUseEvents*/ false);
+ openFilesForSession([moduleFile1], session);
+
+ const file1ChangeShapeRequest = makeSessionRequest(server.CommandNames.Change, {
+ file: moduleFile1.path,
+ line: 1,
+ offset: 27,
+ endLine: 1,
+ endOffset: 27,
+ insertString: `Point,`
+ });
+ session.executeCommand(file1ChangeShapeRequest);
+ sendAffectedFileRequestAndCheckResult(session, moduleFile1FileListRequest, [moduleFile1]);
+ });
+
+ it("should return cascaded affected file list", () => {
+ const file1Consumer1Consumer1: FileOrFolder = {
+ path: "/a/b/file1Consumer1Consumer1.ts",
+ content: `import {y} from "./file1Consumer1";`
+ };
+ host = createServerHost([moduleFile1, file1Consumer1, file1Consumer1Consumer1, globalFile3, configFile, libFile]);
+ typingsInstaller = new TestTypingsInstaller("/a/data/", host);
+ session = new server.Session(host, nullCancellationToken, /*useSingleInferredProject*/ false, typingsInstaller, Utils.byteLength, process.hrtime, nullLogger, /*canUseEvents*/ false);
+
+ openFilesForSession([moduleFile1, file1Consumer1], session);
+ sendAffectedFileRequestAndCheckResult(session, moduleFile1FileListRequest, [moduleFile1, file1Consumer1, file1Consumer1Consumer1]);
+
+ const changeFile1Consumer1ShapeRequest = makeSessionRequest(server.CommandNames.Change, {
+ file: file1Consumer1.path,
+ line: 2,
+ offset: 1,
+ endLine: 2,
+ endOffset: 1,
+ insertString: `export var T: number;`
+ });
+ session.executeCommand(changeModuleFile1ShapeRequest1);
+ session.executeCommand(changeFile1Consumer1ShapeRequest);
+ sendAffectedFileRequestAndCheckResult(session, moduleFile1FileListRequest, [moduleFile1, file1Consumer1, file1Consumer1Consumer1]);
+ });
+ });
+ });
+
+ describe("EmitFile test", () => {
+ it("should emit specified file", () => {
+ const file1 = {
+ path: "/a/b/f1.ts",
+ content: `export function Foo() { return 10; }`
+ };
+ const file2 = {
+ path: "/a/b/f2.ts",
+ content: `import {Foo} from "./f1"; let y = Foo();`
+ };
+ const config = {
+ path: "/a/b/tsconfig.json",
+ content: `{}`
+ };
+ const host = createServerHost([file1, file2, config, libFile]);
+ const typingsInstaller = new TestTypingsInstaller("/a/data/", host);
+ const session = new server.Session(host, nullCancellationToken, /*useSingleInferredProject*/ false, typingsInstaller, Utils.byteLength, process.hrtime, nullLogger, /*canUseEvents*/ false);
+
+ openFilesForSession([file1, file2], session);
+ const compileFileRequest = makeSessionRequest(server.CommandNames.CompileOnSaveEmitFile, { file: file1.path, projectFileName: config.path });
+ session.executeCommand(compileFileRequest);
+
+ const expectedEmittedFileName = "/a/b/f1.js";
+ assert.isTrue(host.fileExists(expectedEmittedFileName));
+ assert.equal(host.readFile(expectedEmittedFileName), `"use strict";\r\nfunction Foo() { return 10; }\r\nexports.Foo = Foo;\r\n`);
+ });
+ });
+
describe("typings installer", () => {
it("configured projects (tsd installed) 1", () => {
const file1 = {
diff --git a/src/server/builder.ts b/src/server/builder.ts
new file mode 100644
index 00000000000..19e515c4459
--- /dev/null
+++ b/src/server/builder.ts
@@ -0,0 +1,368 @@
+///
+///
+///
+///
+///
+
+namespace ts.server {
+
+ interface Hash {
+ update(data: any, input_encoding?: string): Hash;
+ digest(encoding: string): any;
+ }
+
+ const crypto: {
+ createHash(algorithm: string): Hash
+ } = require("crypto");
+
+ /**
+ * An abstract file info that maintains a shape signature.
+ */
+ export class BuilderFileInfo {
+
+ private lastCheckedShapeSignature: string;
+
+ constructor(public readonly scriptInfo: ScriptInfo, public readonly project: Project) {
+ }
+
+ public isExternalModuleOrHasOnlyAmbientExternalModules() {
+ const sourceFile = this.getSourceFile();
+ return isExternalModule(sourceFile) || this.containsOnlyAmbientModules(sourceFile);
+ }
+
+ /**
+ * For script files that contains only ambient external modules, although they are not actually external module files,
+ * they can only be consumed via importing elements from them. Regular script files cannot consume them. Therefore,
+ * there are no point to rebuild all script files if these special files have changed. However, if any statement
+ * in the file is not ambient external module, we treat it as a regular script file.
+ */
+ private containsOnlyAmbientModules(sourceFile: SourceFile) {
+ for (const statement of sourceFile.statements) {
+ if (statement.kind !== SyntaxKind.ModuleDeclaration || (statement).name.kind !== SyntaxKind.StringLiteral) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ private computeHash(text: string): string {
+ return crypto.createHash("md5")
+ .update(text)
+ .digest("base64");
+ }
+
+ private getSourceFile(): SourceFile {
+ return this.project.getSourceFile(this.scriptInfo.path);
+ }
+
+ /**
+ * @return {boolean} indicates if the shape signature has changed since last update.
+ */
+ public updateShapeSignature() {
+ const sourceFile = this.getSourceFile();
+ if (!sourceFile) {
+ return true;
+ }
+
+ const lastSignature = this.lastCheckedShapeSignature;
+ if (sourceFile.isDeclarationFile) {
+ this.lastCheckedShapeSignature = this.computeHash(sourceFile.text);
+ }
+ else {
+ const emitOutput = this.project.getFileEmitOutput(this.scriptInfo, /*emitOnlyDtsFiles*/ true);
+ if (emitOutput.outputFiles && emitOutput.outputFiles.length > 0) {
+ this.lastCheckedShapeSignature = this.computeHash(emitOutput.outputFiles[0].text);
+ }
+ }
+ return !lastSignature || this.lastCheckedShapeSignature !== lastSignature;
+ }
+ }
+
+ export interface Builder {
+ readonly project: Project;
+ getFilesAffectedBy(scriptInfo: ScriptInfo): string[];
+ onProjectUpdateGraph(): void;
+ emitFile(scriptInfo: ScriptInfo, writeFile: (path: string, data: string, writeByteOrderMark?: boolean) => void): boolean;
+ }
+
+ abstract class AbstractBuilder implements Builder {
+
+ private fileInfos = createFileMap();
+
+ constructor(public readonly project: Project, private ctor: { new (scriptInfo: ScriptInfo, project: Project): T }) {
+ }
+
+ protected getFileInfo(path: Path): T {
+ return this.fileInfos.get(path);
+ }
+
+ protected getOrCreateFileInfo(path: Path): T {
+ let fileInfo = this.getFileInfo(path);
+ if (!fileInfo) {
+ const scriptInfo = this.project.getScriptInfo(path);
+ fileInfo = new this.ctor(scriptInfo, this.project);
+ this.setFileInfo(path, fileInfo);
+ }
+ return fileInfo;
+ }
+
+ protected getFileInfoPaths(): Path[] {
+ return this.fileInfos.getKeys();
+ }
+
+ protected setFileInfo(path: Path, info: T) {
+ this.fileInfos.set(path, info);
+ }
+
+ protected removeFileInfo(path: Path) {
+ this.fileInfos.remove(path);
+ }
+
+ protected forEachFileInfo(action: (fileInfo: T) => any) {
+ this.fileInfos.forEachValue((path: Path, value: T) => action(value));
+ }
+
+ abstract getFilesAffectedBy(scriptInfo: ScriptInfo): string[];
+ abstract onProjectUpdateGraph(): void;
+
+ /**
+ * @returns {boolean} whether the emit was conducted or not
+ */
+ emitFile(scriptInfo: ScriptInfo, writeFile: (path: string, data: string, writeByteOrderMark?: boolean) => void): boolean {
+ const fileInfo = this.getFileInfo(scriptInfo.path);
+ if (!fileInfo) {
+ return false;
+ }
+
+ const { emitSkipped, outputFiles } = this.project.getFileEmitOutput(fileInfo.scriptInfo, /*emitOnlyDtsFiles*/ false);
+ if (!emitSkipped) {
+ for (const outputFile of outputFiles) {
+ writeFile(outputFile.name, outputFile.text, outputFile.writeByteOrderMark);
+ }
+ }
+ return !emitSkipped;
+ }
+ }
+
+ class NonModuleBuilder extends AbstractBuilder {
+
+ constructor(public readonly project: Project) {
+ super(project, BuilderFileInfo);
+ }
+
+ onProjectUpdateGraph() {
+ }
+
+ /**
+ * Note: didn't use path as parameter because the returned file names will be directly
+ * consumed by the API user, which will use it to interact with file systems. Path
+ * should only be used internally, because the case sensitivity is not trustable.
+ */
+ getFilesAffectedBy(scriptInfo: ScriptInfo): string[] {
+ const info = this.getOrCreateFileInfo(scriptInfo.path);
+ if (info.updateShapeSignature()) {
+ const options = this.project.getCompilerOptions();
+ // If `--out` or `--outFile` is specified, any new emit will result in re-emitting the entire project,
+ // so returning the file itself is good enough.
+ if (options && (options.out || options.outFile)) {
+ return [scriptInfo.fileName];
+ }
+ return this.project.getFileNamesWithoutDefaultLib();
+ }
+ return [scriptInfo.fileName];
+ }
+ }
+
+ class ModuleBuilderFileInfo extends BuilderFileInfo {
+ references: ModuleBuilderFileInfo[] = [];
+ referencedBy: ModuleBuilderFileInfo[] = [];
+ scriptVersionForReferences: string;
+
+ static compareFileInfos(lf: ModuleBuilderFileInfo, rf: ModuleBuilderFileInfo): number {
+ const l = lf.scriptInfo.fileName;
+ const r = rf.scriptInfo.fileName;
+ return (l < r ? -1 : (l > r ? 1 : 0));
+ };
+
+ static addToReferenceList(array: ModuleBuilderFileInfo[], fileInfo: ModuleBuilderFileInfo) {
+ if (array.length === 0) {
+ array.push(fileInfo);
+ return;
+ }
+
+ const insertIndex = binarySearch(array, fileInfo, ModuleBuilderFileInfo.compareFileInfos);
+ if (insertIndex < 0) {
+ array.splice(~insertIndex, 0, fileInfo);
+ }
+ }
+
+ static removeFromReferenceList(array: ModuleBuilderFileInfo[], fileInfo: ModuleBuilderFileInfo) {
+ if (!array || array.length === 0) {
+ return;
+ }
+
+ if (array[0] === fileInfo) {
+ array.splice(0, 1);
+ return;
+ }
+
+ const removeIndex = binarySearch(array, fileInfo, ModuleBuilderFileInfo.compareFileInfos);
+ if (removeIndex >= 0) {
+ array.splice(removeIndex, 1);
+ }
+ }
+
+ addReferencedBy(fileInfo: ModuleBuilderFileInfo): void {
+ ModuleBuilderFileInfo.addToReferenceList(this.referencedBy, fileInfo);
+ }
+
+ removeReferencedBy(fileInfo: ModuleBuilderFileInfo): void {
+ ModuleBuilderFileInfo.removeFromReferenceList(this.referencedBy, fileInfo);
+ }
+
+ removeFileReferences() {
+ for (const reference of this.references) {
+ reference.removeReferencedBy(this);
+ }
+ this.references = [];
+ }
+ }
+
+ class ModuleBuilder extends AbstractBuilder {
+
+ constructor(public readonly project: Project) {
+ super(project, ModuleBuilderFileInfo);
+ }
+
+ private projectVersionForDependencyGraph: string;
+
+ private getReferencedFileInfos(fileInfo: ModuleBuilderFileInfo): ModuleBuilderFileInfo[] {
+ if (!fileInfo.isExternalModuleOrHasOnlyAmbientExternalModules()) {
+ return [];
+ }
+
+ const referencedFilePaths = this.project.getReferencedFiles(fileInfo.scriptInfo.path);
+ if (referencedFilePaths.length > 0) {
+ return map(referencedFilePaths, f => this.getFileInfo(f)).sort(ModuleBuilderFileInfo.compareFileInfos);
+ }
+ return [];
+ }
+
+ onProjectUpdateGraph() {
+ this.ensureProjectDependencyGraphUpToDate();
+ }
+
+ private ensureProjectDependencyGraphUpToDate() {
+ if (!this.projectVersionForDependencyGraph || this.project.getProjectVersion() !== this.projectVersionForDependencyGraph) {
+ const currentScriptInfos = this.project.getScriptInfos();
+ for (const scriptInfo of currentScriptInfos) {
+ const fileInfo = this.getOrCreateFileInfo(scriptInfo.path);
+ this.updateFileReferences(fileInfo);
+ }
+ this.forEachFileInfo(fileInfo => {
+ if (!this.project.containsScriptInfo(fileInfo.scriptInfo)) {
+ // This file was deleted from this project
+ fileInfo.removeFileReferences();
+ this.removeFileInfo(fileInfo.scriptInfo.path);
+ }
+ });
+ this.projectVersionForDependencyGraph = this.project.getProjectVersion();
+ }
+ }
+
+ private updateFileReferences(fileInfo: ModuleBuilderFileInfo) {
+ // Only need to update if the content of the file changed.
+ if (fileInfo.scriptVersionForReferences === fileInfo.scriptInfo.getLatestVersion()) {
+ return;
+ }
+
+ const newReferences = this.getReferencedFileInfos(fileInfo);
+ const oldReferences = fileInfo.references;
+
+ let oldIndex = 0;
+ let newIndex = 0;
+ while (oldIndex < oldReferences.length && newIndex < newReferences.length) {
+ const oldReference = oldReferences[oldIndex];
+ const newReference = newReferences[newIndex];
+ const compare = ModuleBuilderFileInfo.compareFileInfos(oldReference, newReference);
+ if (compare < 0) {
+ // New reference is greater then current reference. That means
+ // the current reference doesn't exist anymore after parsing. So delete
+ // references.
+ oldReference.removeReferencedBy(fileInfo);
+ oldIndex++;
+ }
+ else if (compare > 0) {
+ // A new reference info. Add it.
+ newReference.addReferencedBy(fileInfo);
+ newIndex++;
+ }
+ else {
+ // Equal. Go to next
+ oldIndex++;
+ newIndex++;
+ }
+ }
+ // Clean old references
+ for (let i = oldIndex; i < oldReferences.length; i++) {
+ oldReferences[i].removeReferencedBy(fileInfo);
+ }
+ // Update new references
+ for (let i = newIndex; i < newReferences.length; i++) {
+ newReferences[i].addReferencedBy(fileInfo);
+ }
+
+ fileInfo.references = newReferences;
+ fileInfo.scriptVersionForReferences = fileInfo.scriptInfo.getLatestVersion();
+ }
+
+ getFilesAffectedBy(scriptInfo: ScriptInfo): string[] {
+ this.ensureProjectDependencyGraphUpToDate();
+
+ const fileInfo = this.getFileInfo(scriptInfo.path);
+ if (!fileInfo || !fileInfo.updateShapeSignature()) {
+ return [scriptInfo.fileName];
+ }
+
+ if (!fileInfo.isExternalModuleOrHasOnlyAmbientExternalModules()) {
+ return this.project.getFileNamesWithoutDefaultLib();
+ }
+
+ const options = this.project.getCompilerOptions();
+ if (options && (options.isolatedModules || options.out || options.outFile)) {
+ return [scriptInfo.fileName];
+ }
+
+ // Now we need to if each file in the referencedBy list has a shape change as well.
+ // Because if so, its own referencedBy files need to be saved as well to make the
+ // emitting result consistent with files on disk.
+
+ // Use slice to clone the array to avoid manipulating in place
+ const queue = fileInfo.referencedBy.slice(0);
+ const fileNameSet = createMap();
+ fileNameSet[scriptInfo.fileName] = true;
+ while (queue.length > 0) {
+ const processingFileInfo = queue.pop();
+ if (processingFileInfo.updateShapeSignature() && processingFileInfo.referencedBy.length > 0) {
+ for (const potentialFileInfo of processingFileInfo.referencedBy) {
+ if (!fileNameSet[potentialFileInfo.scriptInfo.fileName]) {
+ queue.push(potentialFileInfo);
+ }
+ }
+ }
+ fileNameSet[processingFileInfo.scriptInfo.fileName] = true;
+ }
+ return Object.keys(fileNameSet);
+ }
+ }
+
+ export function createBuilder(project: Project): Builder {
+ const moduleKind = project.getCompilerOptions().module;
+ switch (moduleKind) {
+ case ModuleKind.None:
+ return new NonModuleBuilder(project);
+ default:
+ return new ModuleBuilder(project);
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/server/cancellationToken.ts b/src/server/cancellationToken.ts
index dbc07716128..6d3dec67cc6 100644
--- a/src/server/cancellationToken.ts
+++ b/src/server/cancellationToken.ts
@@ -1,4 +1,4 @@
-///
+///
// TODO: extract services types
diff --git a/src/server/editorServices.ts b/src/server/editorServices.ts
index 8c3d7f42e25..1557fcd0879 100644
--- a/src/server/editorServices.ts
+++ b/src/server/editorServices.ts
@@ -681,7 +681,8 @@ namespace ts.server {
compilerOptions: parsedCommandLine.options,
configHasFilesProperty: configObj.config["files"] !== undefined,
wildcardDirectories: createMap(parsedCommandLine.wildcardDirectories),
- typingOptions: parsedCommandLine.typingOptions
+ typingOptions: parsedCommandLine.typingOptions,
+ compileOnSave: parsedCommandLine.compileOnSave
};
return { success: true, projectOptions };
}
@@ -704,14 +705,15 @@ namespace ts.server {
return false;
}
- private createAndAddExternalProject(projectFileName: string, files: protocol.ExternalFile[], compilerOptions: CompilerOptions, typingOptions: TypingOptions) {
+ private createAndAddExternalProject(projectFileName: string, files: protocol.ExternalFile[], options: protocol.ExternalProjectCompilerOptions, typingOptions: TypingOptions) {
const project = new ExternalProject(
projectFileName,
this,
this.documentRegistry,
- compilerOptions,
+ options,
typingOptions,
- /*languageServiceEnabled*/ !this.exceededTotalSizeLimitForNonTsFiles(compilerOptions, files, externalFilePropertyReader));
+ /*languageServiceEnabled*/ !this.exceededTotalSizeLimitForNonTsFiles(options, files, externalFilePropertyReader),
+ !!options.compileOnSave);
const errors = this.addFilesToProjectAndUpdateGraph(project, files, externalFilePropertyReader, /*clientFileName*/ undefined);
this.externalProjects.push(project);
@@ -728,7 +730,8 @@ namespace ts.server {
projectOptions.compilerOptions,
projectOptions.typingOptions,
projectOptions.wildcardDirectories,
- /*languageServiceEnabled*/ !sizeLimitExceeded);
+ /*languageServiceEnabled*/ !sizeLimitExceeded,
+ /*compileOnSaveEnabled*/ !!projectOptions.compileOnSave);
const errors = this.addFilesToProjectAndUpdateGraph(project, projectOptions.files, fileNamePropertyReader, clientFileName);
@@ -775,7 +778,7 @@ namespace ts.server {
return { success: true, project, errors };
}
- private updateNonInferredProject(project: ExternalProject | ConfiguredProject, newUncheckedFiles: T[], propertyReader: FilePropertyReader, newOptions: CompilerOptions, newTypingOptions: TypingOptions) {
+ private updateNonInferredProject(project: ExternalProject | ConfiguredProject, newUncheckedFiles: T[], propertyReader: FilePropertyReader, newOptions: CompilerOptions, newTypingOptions: TypingOptions, compileOnSave: boolean) {
const oldRootScriptInfos = project.getRootScriptInfos();
const newRootScriptInfos: ScriptInfo[] = [];
const newRootScriptInfoMap: NormalizedPathMap = createNormalizedPathMap();
@@ -836,6 +839,7 @@ namespace ts.server {
project.setCompilerOptions(newOptions);
(project).setTypingOptions(newTypingOptions);
+ project.compileOnSaveEnabled = !!compileOnSave;
project.updateGraph();
}
@@ -865,7 +869,7 @@ namespace ts.server {
project.enableLanguageService();
}
this.watchConfigDirectoryForProject(project, projectOptions);
- this.updateNonInferredProject(project, projectOptions.files, fileNamePropertyReader, projectOptions.compilerOptions, projectOptions.typingOptions);
+ this.updateNonInferredProject(project, projectOptions.files, fileNamePropertyReader, projectOptions.compilerOptions, projectOptions.typingOptions, projectOptions.compileOnSave);
}
}
@@ -1135,7 +1139,7 @@ namespace ts.server {
openExternalProject(proj: protocol.ExternalProject): void {
const externalProject = this.findExternalProjectByProjectName(proj.projectFileName);
if (externalProject) {
- this.updateNonInferredProject(externalProject, proj.rootFiles, externalFilePropertyReader, proj.options, proj.typingOptions);
+ this.updateNonInferredProject(externalProject, proj.rootFiles, externalFilePropertyReader, proj.options, proj.typingOptions, proj.options.compileOnSave);
return;
}
diff --git a/src/server/editorServices.ts.orig b/src/server/editorServices.ts.orig
new file mode 100644
index 00000000000..b100cf21261
--- /dev/null
+++ b/src/server/editorServices.ts.orig
@@ -0,0 +1,1203 @@
+///
+///
+///
+///
+///
+///
+///
+///
+///
+
+namespace ts.server {
+ export const maxProgramSizeForNonTsFiles = 20 * 1024 * 1024;
+
+ /**
+ * This helper function processes a list of projects and return the concatenated, sortd and deduplicated output of processing each project.
+ */
+ export function combineProjectOutput(projects: Project[], action: (project: Project) => T[], comparer?: (a: T, b: T) => number, areEqual?: (a: T, b: T) => boolean) {
+ const result = projects.reduce((previous, current) => concatenate(previous, action(current)), []).sort(comparer);
+ return projects.length > 1 ? deduplicate(result, areEqual) : result;
+ }
+
+ export interface ProjectServiceEventHandler {
+ (eventName: string, project: Project, fileName: NormalizedPath): void;
+ }
+
+ export interface HostConfiguration {
+ formatCodeOptions: FormatCodeSettings;
+ hostInfo: string;
+ }
+
+ interface ConfigFileConversionResult {
+ success: boolean;
+ errors?: Diagnostic[];
+
+ projectOptions?: ProjectOptions;
+ }
+
+ interface OpenConfigFileResult {
+ success: boolean;
+ errors?: Diagnostic[];
+
+ project?: ConfiguredProject;
+ }
+
+ export interface OpenConfiguredProjectResult {
+ configFileName?: string;
+ configFileErrors?: Diagnostic[];
+ }
+
+ interface FilePropertyReader {
+ getFileName(f: T): string;
+ getScriptKind(f: T): ScriptKind;
+ hasMixedContent(f: T): boolean;
+ }
+
+ const fileNamePropertyReader: FilePropertyReader = {
+ getFileName: x => x,
+ getScriptKind: _ => undefined,
+ hasMixedContent: _ => false
+ };
+
+ const externalFilePropertyReader: FilePropertyReader = {
+ getFileName: x => x.fileName,
+ getScriptKind: x => x.scriptKind,
+ hasMixedContent: x => x.hasMixedContent
+ };
+
+ function findProjectByName(projectName: string, projects: T[]): T {
+ for (const proj of projects) {
+ if (proj.getProjectName() === projectName) {
+ return proj;
+ }
+ }
+ }
+
+ /**
+ * TODO: enforce invariants:
+ * - script info can be never migrate to state - root file in inferred project, this is only a starting point
+ * - if script info has more that one containing projects - it is not a root file in inferred project because:
+ * - references in inferred project supercede the root part
+ * - root/reference in non-inferred project beats root in inferred project
+ */
+ function isRootFileInInferredProject(info: ScriptInfo): boolean {
+ if (info.containingProjects.length === 0) {
+ return false;
+ }
+ return info.containingProjects[0].projectKind === ProjectKind.Inferred && info.containingProjects[0].isRoot(info);
+ }
+
+ class DirectoryWatchers {
+ /**
+ * a path to directory watcher map that detects added tsconfig files
+ **/
+ private readonly directoryWatchersForTsconfig: Map = createMap();
+ /**
+ * count of how many projects are using the directory watcher.
+ * If the number becomes 0 for a watcher, then we should close it.
+ **/
+ private readonly directoryWatchersRefCount: Map = createMap();
+
+ constructor(private readonly projectService: ProjectService) {
+ }
+
+ stopWatchingDirectory(directory: string) {
+ // if the ref count for this directory watcher drops to 0, it's time to close it
+ this.directoryWatchersRefCount[directory]--;
+ if (this.directoryWatchersRefCount[directory] === 0) {
+ this.projectService.logger.info(`Close directory watcher for: ${directory}`);
+ this.directoryWatchersForTsconfig[directory].close();
+ delete this.directoryWatchersForTsconfig[directory];
+ }
+ }
+
+ startWatchingContainingDirectoriesForFile(fileName: string, project: InferredProject, callback: (fileName: string) => void) {
+ let currentPath = getDirectoryPath(fileName);
+ let parentPath = getDirectoryPath(currentPath);
+ while (currentPath != parentPath) {
+ if (!this.directoryWatchersForTsconfig[currentPath]) {
+ this.projectService.logger.info(`Add watcher for: ${currentPath}`);
+ this.directoryWatchersForTsconfig[currentPath] = this.projectService.host.watchDirectory(currentPath, callback);
+ this.directoryWatchersRefCount[currentPath] = 1;
+ }
+ else {
+ this.directoryWatchersRefCount[currentPath] += 1;
+ }
+ project.directoriesWatchedForTsconfig.push(currentPath);
+ currentPath = parentPath;
+ parentPath = getDirectoryPath(parentPath);
+ }
+ }
+ }
+
+ export class ProjectService {
+
+ public readonly typingsCache: TypingsCache;
+
+ private readonly documentRegistry: DocumentRegistry;
+
+ /**
+ * Container of all known scripts
+ */
+ private readonly filenameToScriptInfo = createFileMap();
+ /**
+ * maps external project file name to list of config files that were the part of this project
+ */
+ private readonly externalProjectToConfiguredProjectMap: Map = createMap();
+
+ /**
+ * external projects (configuration and list of root files is not controlled by tsserver)
+ */
+ readonly externalProjects: ExternalProject[] = [];
+ /**
+ * projects built from openFileRoots
+ **/
+ readonly inferredProjects: InferredProject[] = [];
+ /**
+ * projects specified by a tsconfig.json file
+ **/
+ readonly configuredProjects: ConfiguredProject[] = [];
+ /**
+ * list of open files
+ */
+ readonly openFiles: ScriptInfo[] = [];
+
+ private compilerOptionsForInferredProjects: CompilerOptions;
+ private readonly directoryWatchers: DirectoryWatchers;
+ private readonly throttledOperations: ThrottledOperations;
+
+ private readonly hostConfiguration: HostConfiguration;
+
+ private changedFiles: ScriptInfo[];
+
+ private toCanonicalFileName: (f: string) => string;
+
+ constructor(public readonly host: ServerHost,
+ public readonly logger: Logger,
+ public readonly cancellationToken: HostCancellationToken,
+ private readonly useSingleInferredProject: boolean,
+ private typingsInstaller: ITypingsInstaller,
+ private readonly eventHandler?: ProjectServiceEventHandler) {
+
+ this.toCanonicalFileName = createGetCanonicalFileName(host.useCaseSensitiveFileNames);
+ this.directoryWatchers = new DirectoryWatchers(this);
+ this.throttledOperations = new ThrottledOperations(host);
+
+ const installer = typingsInstaller || nullTypingsInstaller;
+ installer.attach(this);
+
+ this.typingsCache = new TypingsCache(installer);
+
+ // ts.disableIncrementalParsing = true;
+
+ this.hostConfiguration = {
+ formatCodeOptions: getDefaultFormatCodeSettings(this.host),
+ hostInfo: "Unknown host"
+ };
+
+ this.documentRegistry = createDocumentRegistry(host.useCaseSensitiveFileNames, host.getCurrentDirectory());
+ }
+
+ getChangedFiles_TestOnly() {
+ return this.changedFiles;
+ }
+
+ ensureInferredProjectsUpToDate_TestOnly() {
+ this.ensureInferredProjectsUpToDate();
+ }
+
+ updateTypingsForProject(response: SetTypings | InvalidateCachedTypings): void {
+ const project = this.findProject(response.projectName);
+ if (!project) {
+ return;
+ }
+ switch (response.kind) {
+ case "set":
+ this.typingsCache.updateTypingsForProject(response.projectName, response.compilerOptions, response.typingOptions, response.typings);
+ project.updateGraph();
+ break;
+ case "invalidate":
+ this.typingsCache.invalidateCachedTypingsForProject(project);
+ break;
+ }
+ }
+
+ setCompilerOptionsForInferredProjects(compilerOptions: CompilerOptions): void {
+ this.compilerOptionsForInferredProjects = compilerOptions;
+ for (const proj of this.inferredProjects) {
+ proj.setCompilerOptions(compilerOptions);
+ }
+ this.updateProjectGraphs(this.inferredProjects);
+ }
+
+ stopWatchingDirectory(directory: string) {
+ this.directoryWatchers.stopWatchingDirectory(directory);
+ }
+
+ findProject(projectName: string): Project {
+ if (projectName === undefined) {
+ return undefined;
+ }
+ if (isInferredProjectName(projectName)) {
+ this.ensureInferredProjectsUpToDate();
+ return findProjectByName(projectName, this.inferredProjects);
+ }
+ return this.findExternalProjectByProjectName(projectName) || this.findConfiguredProjectByProjectName(toNormalizedPath(projectName));
+ }
+
+ getDefaultProjectForFile(fileName: NormalizedPath, refreshInferredProjects: boolean) {
+ if (refreshInferredProjects) {
+ this.ensureInferredProjectsUpToDate();
+ }
+ const scriptInfo = this.getScriptInfoForNormalizedPath(fileName);
+ return scriptInfo && scriptInfo.getDefaultProject();
+ }
+
+ private ensureInferredProjectsUpToDate() {
+ if (this.changedFiles) {
+ let projectsToUpdate: Project[];
+ if (this.changedFiles.length === 1) {
+ // simpliest case - no allocations
+ projectsToUpdate = this.changedFiles[0].containingProjects;
+ }
+ else {
+ projectsToUpdate = [];
+ for (const f of this.changedFiles) {
+ projectsToUpdate = projectsToUpdate.concat(f.containingProjects);
+ }
+ }
+ this.updateProjectGraphs(projectsToUpdate);
+ this.changedFiles = undefined;
+ }
+ }
+
+ private findContainingExternalProject(fileName: NormalizedPath): ExternalProject {
+ for (const proj of this.externalProjects) {
+ if (proj.containsFile(fileName)) {
+ return proj;
+ }
+ }
+ return undefined;
+ }
+
+ getFormatCodeOptions(file?: NormalizedPath) {
+ if (file) {
+ const info = this.getScriptInfoForNormalizedPath(file);
+ if (info) {
+ return info.formatCodeSettings;
+ }
+ }
+ return this.hostConfiguration.formatCodeOptions;
+ }
+
+ private updateProjectGraphs(projects: Project[]) {
+ let shouldRefreshInferredProjects = false;
+ for (const p of projects) {
+ if (!p.updateGraph()) {
+ shouldRefreshInferredProjects = true;
+ }
+ }
+ if (shouldRefreshInferredProjects) {
+ this.refreshInferredProjects();
+ }
+ }
+
+ private onSourceFileChanged(fileName: NormalizedPath) {
+ const info = this.getScriptInfoForNormalizedPath(fileName);
+ if (!info) {
+ this.logger.info(`Error: got watch notification for unknown file: ${fileName}`);
+ }
+
+ if (!this.host.fileExists(fileName)) {
+ // File was deleted
+ this.handleDeletedFile(info);
+ }
+ else {
+ if (info && (!info.isOpen)) {
+ // file has been changed which might affect the set of referenced files in projects that include
+ // this file and set of inferred projects
+ info.reloadFromFile();
+ this.updateProjectGraphs(info.containingProjects);
+ }
+ }
+ }
+
+ private handleDeletedFile(info: ScriptInfo) {
+ this.logger.info(`${info.fileName} deleted`);
+
+ info.stopWatcher();
+
+ // TODO: handle isOpen = true case
+
+ if (!info.isOpen) {
+ this.filenameToScriptInfo.remove(info.path);
+
+ // capture list of projects since detachAllProjects will wipe out original list
+ const containingProjects = info.containingProjects.slice();
+ info.detachAllProjects();
+
+ // update projects to make sure that set of referenced files is correct
+ this.updateProjectGraphs(containingProjects);
+
+ if (!this.eventHandler) {
+ return;
+ }
+
+ for (const openFile of this.openFiles) {
+ this.eventHandler("context", openFile.getDefaultProject(), openFile.fileName);
+ }
+ }
+
+ this.printProjects();
+ }
+
+ /**
+ * 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
+ */
+ private onSourceFileInDirectoryChangedForConfiguredProject(project: ConfiguredProject, 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, project.getCompilerOptions())) {
+ return;
+ }
+
+ this.logger.info(`Detected source file changes: ${fileName}`);
+ this.throttledOperations.schedule(
+ project.configFileName,
+ /*delay*/250,
+ () => this.handleChangeInSourceFileForConfiguredProject(project));
+ }
+
+ private handleChangeInSourceFileForConfiguredProject(project: ConfiguredProject) {
+ const { projectOptions } = this.convertConfigFileContentToProjectOptions(project.configFileName);
+
+ const newRootFiles = projectOptions.files.map((f => this.getCanonicalFileName(f)));
+ const currentRootFiles = project.getRootFiles().map((f => this.getCanonicalFileName(f)));
+
+ // We check if the project file list has changed. If so, we update the project.
+ if (!arrayIsEqualTo(currentRootFiles.sort(), newRootFiles.sort())) {
+ // 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 refreshInferredProjects to clean up inferred projects we may have
+ // created for the new files
+ this.refreshInferredProjects();
+ }
+ }
+
+ private onConfigChangedForConfiguredProject(project: ConfiguredProject) {
+ this.logger.info(`Config file changed: ${project.configFileName}`);
+ this.updateConfiguredProject(project);
+ this.refreshInferredProjects();
+ }
+
+ /**
+ * This is the callback function when a watched directory has an added tsconfig file.
+ */
+ private onConfigFileAddedForInferredProject(fileName: string) {
+ // TODO: check directory separators
+ if (getBaseFileName(fileName) != "tsconfig.json") {
+ this.logger.info(`${fileName} is not tsconfig.json`);
+ return;
+ }
+
+ this.logger.info(`Detected newly added tsconfig file: ${fileName}`);
+ this.reloadProjects();
+ }
+
+ private getCanonicalFileName(fileName: string) {
+ const name = this.host.useCaseSensitiveFileNames ? fileName : fileName.toLowerCase();
+ return normalizePath(name);
+ }
+
+ private removeProject(project: Project) {
+ this.logger.info(`remove project: ${project.getRootFiles().toString()}`);
+
+ project.close();
+
+ switch (project.projectKind) {
+ case ProjectKind.External:
+ removeItemFromSet(this.externalProjects, project);
+ break;
+ case ProjectKind.Configured:
+ removeItemFromSet(this.configuredProjects, project);
+ break;
+ case ProjectKind.Inferred:
+ removeItemFromSet(this.inferredProjects, project);
+ break;
+ }
+ }
+
+ private assignScriptInfoToInferredProjectIfNecessary(info: ScriptInfo, addToListOfOpenFiles: boolean): void {
+ const externalProject = this.findContainingExternalProject(info.fileName);
+ if (externalProject) {
+ // file is already included in some external project - do nothing
+ if (addToListOfOpenFiles) {
+ this.openFiles.push(info);
+ }
+ return;
+ }
+
+ let foundConfiguredProject = false;
+ for (const p of info.containingProjects) {
+ // file is the part of configured project
+ if (p.projectKind === ProjectKind.Configured) {
+ foundConfiguredProject = true;
+ if (addToListOfOpenFiles) {
+ ((p)).addOpenRef();
+ }
+ }
+ }
+ if (foundConfiguredProject) {
+ if (addToListOfOpenFiles) {
+ this.openFiles.push(info);
+ }
+ return;
+ }
+
+ if (info.containingProjects.length === 0) {
+ // create new inferred project p with the newly opened file as root
+ // or add root to existing inferred project if 'useOneInferredProject' is true
+ const inferredProject = this.createInferredProjectWithRootFileIfNecessary(info);
+ if (!this.useSingleInferredProject) {
+ // if useOneInferredProject is not set then try to fixup ownership of open files
+ // check 'defaultProject !== inferredProject' is necessary to handle cases
+ // when creation inferred project for some file has added other open files into this project (i.e. as referenced files)
+ // we definitely don't want to delete the project that was just created
+ for (const f of this.openFiles) {
+ if (f.containingProjects.length === 0) {
+ // this is orphaned file that we have not processed yet - skip it
+ continue;
+ }
+ const defaultProject = f.getDefaultProject();
+ if (isRootFileInInferredProject(info) && defaultProject !== inferredProject && inferredProject.containsScriptInfo(f)) {
+ // open file used to be root in inferred project,
+ // this inferred project is different from the one we've just created for current file
+ // and new inferred project references this open file.
+ // We should delete old inferred project and attach open file to the new one
+ this.removeProject(defaultProject);
+ f.attachToProject(inferredProject);
+ }
+ }
+ }
+ }
+
+ if (addToListOfOpenFiles) {
+ this.openFiles.push(info);
+ }
+ }
+
+ /**
+ * Remove this file from the set of open, non-configured files.
+ * @param info The file that has been closed or newly configured
+ */
+ private closeOpenFile(info: ScriptInfo): void {
+ // 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.reloadFromFile();
+
+ removeItemFromSet(this.openFiles, info);
+ info.isOpen = false;
+
+ // collect all projects that should be removed
+ let projectsToRemove: Project[];
+ for (const p of info.containingProjects) {
+ if (p.projectKind === ProjectKind.Configured) {
+ // last open file in configured project - close it
+ if ((p).deleteOpenRef() === 0) {
+ (projectsToRemove || (projectsToRemove = [])).push(p);
+ }
+ }
+ else if (p.projectKind === ProjectKind.Inferred && p.isRoot(info)) {
+ // open file in inferred project
+ (projectsToRemove || (projectsToRemove = [])).push(p);
+ }
+ }
+ if (projectsToRemove) {
+ for (const project of projectsToRemove) {
+ this.removeProject(project);
+ }
+
+ let orphanFiles: ScriptInfo[];
+ // for all open files
+ for (const f of this.openFiles) {
+ // collect orphanted files and try to re-add them as newly opened
+ if (f.containingProjects.length === 0) {
+ (orphanFiles || (orphanFiles = [])).push(f);
+ }
+ }
+
+ // treat orphaned files as newly opened
+ if (orphanFiles) {
+ for (const f of orphanFiles) {
+ this.assignScriptInfoToInferredProjectIfNecessary(f, /*addToListOfOpenFiles*/ false);
+ }
+ }
+ }
+ if (info.containingProjects.length === 0) {
+ // if there are not projects that include this script info - delete it
+ this.filenameToScriptInfo.remove(info.path);
+ }
+ }
+
+ /**
+ * 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.
+ */
+ private openOrUpdateConfiguredProjectForFile(fileName: NormalizedPath): OpenConfiguredProjectResult {
+ const searchPath = getDirectoryPath(fileName);
+ this.logger.info(`Search path: ${searchPath}`);
+
+ // check if this file is already included in one of external projects
+ const configFileName = this.findConfigFile(asNormalizedPath(searchPath));
+ if (!configFileName) {
+ this.logger.info("No config files found.");
+ return {};
+ }
+
+ this.logger.info(`Config file name: ${configFileName}`);
+
+ const project = this.findConfiguredProjectByProjectName(configFileName);
+ if (!project) {
+ const { success, errors } = this.openConfigFile(configFileName, fileName);
+ if (!success) {
+ return { configFileName, configFileErrors: errors };
+ }
+
+ // even if opening config file was successful, it could still
+ // contain errors that were tolerated.
+ this.logger.info(`Opened configuration file ${configFileName}`);
+ if (errors && errors.length > 0) {
+ return { configFileName, configFileErrors: errors };
+ }
+ }
+ else {
+ this.updateConfiguredProject(project);
+ }
+
+ return { configFileName };
+ }
+
+ // This is different from the method the compiler uses because
+ // the compiler can assume it will always start searching in the
+ // current directory (the directory in which tsc was invoked).
+ // The server must start searching from the directory containing
+ // the newly opened file.
+ private findConfigFile(searchPath: NormalizedPath): NormalizedPath {
+ while (true) {
+ const tsconfigFileName = asNormalizedPath(combinePaths(searchPath, "tsconfig.json"));
+ if (this.host.fileExists(tsconfigFileName)) {
+ return tsconfigFileName;
+ }
+
+ const jsconfigFileName = asNormalizedPath(combinePaths(searchPath, "jsconfig.json"));
+ if (this.host.fileExists(jsconfigFileName)) {
+ return jsconfigFileName;
+ }
+
+ const parentPath = asNormalizedPath(getDirectoryPath(searchPath));
+ if (parentPath === searchPath) {
+ break;
+ }
+ searchPath = parentPath;
+ }
+ return undefined;
+ }
+
+ private printProjects() {
+ if (!this.logger.hasLevel(LogLevel.verbose)) {
+ return;
+ }
+
+ this.logger.startGroup();
+
+ let counter = 0;
+ counter = printProjects(this.logger, this.externalProjects, counter);
+ counter = printProjects(this.logger, this.configuredProjects, counter);
+ counter = printProjects(this.logger, this.inferredProjects, counter);
+
+ this.logger.info("Open files: ");
+ for (const rootFile of this.openFiles) {
+ this.logger.info(rootFile.fileName);
+ }
+
+ this.logger.endGroup();
+
+ function printProjects(logger: Logger, projects: Project[], counter: number) {
+ for (const project of projects) {
+ project.updateGraph();
+ logger.info(`Project '${project.getProjectName()}' (${ProjectKind[project.projectKind]}) ${counter}`);
+ logger.info(project.filesToString());
+ logger.info("-----------------------------------------------");
+ counter++;
+ }
+ return counter;
+ }
+ }
+
+ private findConfiguredProjectByProjectName(configFileName: NormalizedPath) {
+ return findProjectByName(configFileName, this.configuredProjects);
+ }
+
+ private findExternalProjectByProjectName(projectFileName: string) {
+ return findProjectByName(projectFileName, this.externalProjects);
+ }
+
+ private convertConfigFileContentToProjectOptions(configFilename: string): ConfigFileConversionResult {
+ configFilename = normalizePath(configFilename);
+
+ const configObj = parseConfigFileTextToJson(configFilename, this.host.readFile(configFilename));
+ if (configObj.error) {
+ return { success: false, errors: [configObj.error] };
+ }
+
+ const parsedCommandLine = parseJsonConfigFileContent(
+ configObj.config,
+ this.host,
+ getDirectoryPath(configFilename),
+ /*existingOptions*/ {},
+ configFilename);
+
+ Debug.assert(!!parsedCommandLine.fileNames);
+
+ if (parsedCommandLine.errors && (parsedCommandLine.errors.length > 0)) {
+ return { success: false, errors: parsedCommandLine.errors };
+ }
+
+ if (parsedCommandLine.fileNames.length === 0) {
+ const error = createCompilerDiagnostic(Diagnostics.The_config_file_0_found_doesn_t_contain_any_source_files, configFilename);
+ return { success: false, errors: [error] };
+ }
+
+ const projectOptions: ProjectOptions = {
+ files: parsedCommandLine.fileNames,
+ compilerOptions: parsedCommandLine.options,
+ configHasFilesProperty: configObj.config["files"] !== undefined,
+ wildcardDirectories: createMap(parsedCommandLine.wildcardDirectories),
+ typingOptions: parsedCommandLine.typingOptions,
+ compileOnSave: parsedCommandLine.compileOnSave
+ };
+ return { success: true, projectOptions };
+ }
+
+ private exceededTotalSizeLimitForNonTsFiles(options: CompilerOptions, fileNames: T[], propertyReader: FilePropertyReader) {
+ if (options && options.disableSizeLimit || !this.host.getFileSize) {
+ return false;
+ }
+ let totalNonTsFileSize = 0;
+ for (const f of fileNames) {
+ const fileName = propertyReader.getFileName(f);
+ if (hasTypeScriptFileExtension(fileName)) {
+ continue;
+ }
+ totalNonTsFileSize += this.host.getFileSize(fileName);
+ if (totalNonTsFileSize > maxProgramSizeForNonTsFiles) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+<<<<<<< HEAD
+ private createAndAddExternalProject(projectFileName: string, files: protocol.ExternalFile[], options: protocol.ExternalProjectCompilerOptions, typingOptions: TypingOptions) {
+=======
+ private createAndAddExternalProject(projectFileName: string, files: protocol.ExternalFile[], compilerOptions: CompilerOptions, typingOptions: TypingOptions) {
+>>>>>>> d736db3b01a5f4f4215c17845deb3ae09cf28787
+ const project = new ExternalProject(
+ projectFileName,
+ this,
+ this.documentRegistry,
+<<<<<<< HEAD
+ options,
+ typingOptions,
+ /*languageServiceEnabled*/ !this.exceededTotalSizeLimitForNonTsFiles(options, files, externalFilePropertyReader),
+ !!options.compileOnSave);
+=======
+ compilerOptions,
+ typingOptions,
+ /*languageServiceEnabled*/ !this.exceededTotalSizeLimitForNonTsFiles(compilerOptions, files, externalFilePropertyReader));
+>>>>>>> d736db3b01a5f4f4215c17845deb3ae09cf28787
+
+ const errors = this.addFilesToProjectAndUpdateGraph(project, files, externalFilePropertyReader, /*clientFileName*/ undefined);
+ this.externalProjects.push(project);
+ return { project, errors };
+ }
+
+ private createAndAddConfiguredProject(configFileName: NormalizedPath, projectOptions: ProjectOptions, clientFileName?: string) {
+ const sizeLimitExceeded = this.exceededTotalSizeLimitForNonTsFiles(projectOptions.compilerOptions, projectOptions.files, fileNamePropertyReader);
+ const project = new ConfiguredProject(
+ configFileName,
+ this,
+ this.documentRegistry,
+ projectOptions.configHasFilesProperty,
+ projectOptions.compilerOptions,
+ projectOptions.typingOptions,
+ projectOptions.wildcardDirectories,
+ /*languageServiceEnabled*/ !sizeLimitExceeded,
+ /*compileOnSaveEnabled*/ !!projectOptions.compileOnSave);
+
+ const errors = this.addFilesToProjectAndUpdateGraph(project, projectOptions.files, fileNamePropertyReader, clientFileName);
+
+ project.watchConfigFile(project => this.onConfigChangedForConfiguredProject(project));
+ if (!sizeLimitExceeded) {
+ this.watchConfigDirectoryForProject(project, projectOptions);
+ }
+ project.watchWildcards((project, path) => this.onSourceFileInDirectoryChangedForConfiguredProject(project, path));
+
+ this.configuredProjects.push(project);
+ return { project, errors };
+ }
+
+ private watchConfigDirectoryForProject(project: ConfiguredProject, options: ProjectOptions): void {
+ if (!options.configHasFilesProperty) {
+ project.watchConfigDirectory((project, path) => this.onSourceFileInDirectoryChangedForConfiguredProject(project, path));
+ }
+ }
+
+ private addFilesToProjectAndUpdateGraph(project: ConfiguredProject | ExternalProject, files: T[], propertyReader: FilePropertyReader, clientFileName: string): Diagnostic[] {
+ let errors: Diagnostic[];
+ for (const f of files) {
+ const rootFilename = propertyReader.getFileName(f);
+ const scriptKind = propertyReader.getScriptKind(f);
+ const hasMixedContent = propertyReader.hasMixedContent(f);
+ if (this.host.fileExists(rootFilename)) {
+ const info = this.getOrCreateScriptInfoForNormalizedPath(toNormalizedPath(rootFilename), /*openedByClient*/ clientFileName == rootFilename, /*fileContent*/ undefined, scriptKind, hasMixedContent);
+ project.addRoot(info);
+ }
+ else {
+ (errors || (errors = [])).push(createCompilerDiagnostic(Diagnostics.File_0_not_found, rootFilename));
+ }
+ }
+ project.updateGraph();
+ return errors;
+ }
+
+ private openConfigFile(configFileName: NormalizedPath, clientFileName?: string): OpenConfigFileResult {
+ const conversionResult = this.convertConfigFileContentToProjectOptions(configFileName);
+ if (!conversionResult.success) {
+ return { success: false, errors: conversionResult.errors };
+ }
+ const { project, errors } = this.createAndAddConfiguredProject(configFileName, conversionResult.projectOptions, clientFileName);
+ return { success: true, project, errors };
+ }
+
+<<<<<<< HEAD
+ private updateNonInferredProject(project: ExternalProject | ConfiguredProject, newUncheckedFiles: T[], propertyReader: FilePropertyReader, newOptions: CompilerOptions, newTypingOptions: TypingOptions, compileOnSave: boolean) {
+=======
+ private updateNonInferredProject(project: ExternalProject | ConfiguredProject, newUncheckedFiles: T[], propertyReader: FilePropertyReader, newOptions: CompilerOptions, newTypingOptions: TypingOptions) {
+>>>>>>> d736db3b01a5f4f4215c17845deb3ae09cf28787
+ const oldRootScriptInfos = project.getRootScriptInfos();
+ const newRootScriptInfos: ScriptInfo[] = [];
+ const newRootScriptInfoMap: NormalizedPathMap = createNormalizedPathMap();
+
+ let rootFilesChanged = false;
+ for (const f of newUncheckedFiles) {
+ const newRootFile = propertyReader.getFileName(f);
+ if (!this.host.fileExists(newRootFile)) {
+ continue;
+ }
+ const normalizedPath = toNormalizedPath(newRootFile);
+ let scriptInfo = this.getScriptInfoForNormalizedPath(normalizedPath);
+ if (!scriptInfo || !project.isRoot(scriptInfo)) {
+ rootFilesChanged = true;
+ if (!scriptInfo) {
+ const scriptKind = propertyReader.getScriptKind(f);
+ const hasMixedContent = propertyReader.hasMixedContent(f);
+ scriptInfo = this.getOrCreateScriptInfoForNormalizedPath(normalizedPath, /*openedByClient*/ false, /*fileContent*/ undefined, scriptKind, hasMixedContent);
+ }
+ }
+ newRootScriptInfos.push(scriptInfo);
+ newRootScriptInfoMap.set(scriptInfo.fileName, scriptInfo);
+ }
+
+ if (rootFilesChanged || newRootScriptInfos.length !== oldRootScriptInfos.length) {
+ let toAdd: ScriptInfo[];
+ let toRemove: ScriptInfo[];
+ for (const oldFile of oldRootScriptInfos) {
+ if (!newRootScriptInfoMap.contains(oldFile.fileName)) {
+ (toRemove || (toRemove = [])).push(oldFile);
+ }
+ }
+ for (const newFile of newRootScriptInfos) {
+ if (!project.isRoot(newFile)) {
+ (toAdd || (toAdd = [])).push(newFile);
+ }
+ }
+ if (toRemove) {
+ for (const f of toRemove) {
+ project.removeFile(f);
+ }
+ }
+ if (toAdd) {
+ for (const f of toAdd) {
+ if (f.isOpen && isRootFileInInferredProject(f)) {
+ // if file is already root in some inferred project
+ // - remove the file from that project and delete the project if necessary
+ const inferredProject = f.containingProjects[0];
+ inferredProject.removeFile(f);
+ if (!inferredProject.hasRoots()) {
+ this.removeProject(inferredProject);
+ }
+ }
+ project.addRoot(f);
+ }
+ }
+ }
+
+ project.setCompilerOptions(newOptions);
+ (project).setTypingOptions(newTypingOptions);
+<<<<<<< HEAD
+ project.compileOnSaveEnabled = !!compileOnSave;
+=======
+>>>>>>> d736db3b01a5f4f4215c17845deb3ae09cf28787
+ project.updateGraph();
+ }
+
+ private updateConfiguredProject(project: ConfiguredProject) {
+ if (!this.host.fileExists(project.configFileName)) {
+ this.logger.info("Config file deleted");
+ this.removeProject(project);
+ return;
+ }
+
+ const { success, projectOptions, errors } = this.convertConfigFileContentToProjectOptions(project.configFileName);
+ if (!success) {
+ return errors;
+ }
+
+ if (this.exceededTotalSizeLimitForNonTsFiles(projectOptions.compilerOptions, projectOptions.files, fileNamePropertyReader)) {
+ project.setCompilerOptions(projectOptions.compilerOptions);
+ if (!project.languageServiceEnabled) {
+ // language service is already disabled
+ return;
+ }
+ project.disableLanguageService();
+ project.stopWatchingDirectory();
+ }
+ else {
+ if (!project.languageServiceEnabled) {
+ project.enableLanguageService();
+ }
+ this.watchConfigDirectoryForProject(project, projectOptions);
+<<<<<<< HEAD
+ this.updateNonInferredProject(project, projectOptions.files, fileNamePropertyReader, projectOptions.compilerOptions, projectOptions.typingOptions, projectOptions.compileOnSave);
+=======
+ this.updateNonInferredProject(project, projectOptions.files, fileNamePropertyReader, projectOptions.compilerOptions, projectOptions.typingOptions);
+>>>>>>> d736db3b01a5f4f4215c17845deb3ae09cf28787
+ }
+ }
+
+ createInferredProjectWithRootFileIfNecessary(root: ScriptInfo) {
+ const useExistingProject = this.useSingleInferredProject && this.inferredProjects.length;
+ const project = useExistingProject
+ ? this.inferredProjects[0]
+ : new InferredProject(this, this.documentRegistry, /*languageServiceEnabled*/ true, this.compilerOptionsForInferredProjects);
+
+ project.addRoot(root);
+
+ this.directoryWatchers.startWatchingContainingDirectoriesForFile(
+ root.fileName,
+ project,
+ fileName => this.onConfigFileAddedForInferredProject(fileName));
+
+ project.updateGraph();
+
+ if (!useExistingProject) {
+ this.inferredProjects.push(project);
+ }
+ return project;
+ }
+
+ /**
+ * @param uncheckedFileName is absolute pathname
+ * @param fileContent is a known version of the file content that is more up to date than the one on disk
+ */
+
+ getOrCreateScriptInfo(uncheckedFileName: string, openedByClient: boolean, fileContent?: string, scriptKind?: ScriptKind) {
+ return this.getOrCreateScriptInfoForNormalizedPath(toNormalizedPath(uncheckedFileName), openedByClient, fileContent, scriptKind);
+ }
+
+ getScriptInfo(uncheckedFileName: string) {
+ return this.getScriptInfoForNormalizedPath(toNormalizedPath(uncheckedFileName));
+ }
+
+ getOrCreateScriptInfoForNormalizedPath(fileName: NormalizedPath, openedByClient: boolean, fileContent?: string, scriptKind?: ScriptKind, hasMixedContent?: boolean) {
+ let info = this.getScriptInfoForNormalizedPath(fileName);
+ if (!info) {
+ let content: string;
+ if (this.host.fileExists(fileName)) {
+ // by default pick whatever content was supplied as the argument
+ // if argument was not given - then for mixed content files assume that its content is empty string
+ content = fileContent || (hasMixedContent ? "" : this.host.readFile(fileName));
+ }
+ if (!content) {
+ if (openedByClient) {
+ content = "";
+ }
+ }
+ if (content !== undefined) {
+ info = new ScriptInfo(this.host, fileName, content, scriptKind, openedByClient, hasMixedContent);
+ info.setFormatOptions(toEditorSettings(this.getFormatCodeOptions()));
+ // do not watch files with mixed content - server doesn't know how to interpret it
+ this.filenameToScriptInfo.set(info.path, info);
+ if (!info.isOpen && !hasMixedContent) {
+ info.setWatcher(this.host.watchFile(fileName, _ => this.onSourceFileChanged(fileName)));
+ }
+ }
+ }
+ if (info) {
+ if (fileContent) {
+ info.reload(fileContent);
+ }
+ if (openedByClient) {
+ info.isOpen = true;
+ }
+ }
+ return info;
+ }
+
+ getScriptInfoForNormalizedPath(fileName: NormalizedPath) {
+ return this.filenameToScriptInfo.get(normalizedPathToPath(fileName, this.host.getCurrentDirectory(), this.toCanonicalFileName));
+ }
+
+ setHostConfiguration(args: protocol.ConfigureRequestArguments) {
+ if (args.file) {
+ const info = this.getScriptInfoForNormalizedPath(toNormalizedPath(args.file));
+ if (info) {
+ info.setFormatOptions(args.formatOptions);
+ this.logger.info(`Host configuration update for file ${args.file}`);
+ }
+ }
+ else {
+ if (args.hostInfo !== undefined) {
+ this.hostConfiguration.hostInfo = args.hostInfo;
+ this.logger.info(`Host information ${args.hostInfo}`);
+ }
+ if (args.formatOptions) {
+ mergeMaps(this.hostConfiguration.formatCodeOptions, args.formatOptions);
+ this.logger.info("Format host information updated");
+ }
+ }
+ }
+
+ closeLog() {
+ this.logger.close();
+ }
+
+ /**
+ * This function rebuilds the project for every file opened by the client
+ */
+ reloadProjects() {
+ this.logger.info("reload projects.");
+ // try to reload config file for all open files
+ for (const info of this.openFiles) {
+ this.openOrUpdateConfiguredProjectForFile(info.fileName);
+ }
+ this.refreshInferredProjects();
+ }
+
+ /**
+ * This function is to update the project structure for every projects.
+ * It is called on the premise that all the configured projects are
+ * up to date.
+ */
+ refreshInferredProjects() {
+ this.logger.info("updating project structure from ...");
+ this.printProjects();
+
+ const orphantedFiles: ScriptInfo[] = [];
+ // collect all orphanted script infos from open files
+ for (const info of this.openFiles) {
+ if (info.containingProjects.length === 0) {
+ orphantedFiles.push(info);
+ }
+ else {
+ if (isRootFileInInferredProject(info) && info.containingProjects.length > 1) {
+ const inferredProject = info.containingProjects[0];
+ Debug.assert(inferredProject.projectKind === ProjectKind.Inferred);
+ inferredProject.removeFile(info);
+ if (!inferredProject.hasRoots()) {
+ this.removeProject(inferredProject);
+ }
+ }
+ }
+ }
+ for (const f of orphantedFiles) {
+ this.assignScriptInfoToInferredProjectIfNecessary(f, /*addToListOfOpenFiles*/ false);
+ }
+
+ for (const p of this.inferredProjects) {
+ p.updateGraph();
+ }
+ this.printProjects();
+ }
+
+ /**
+ * Open file whose contents is managed by the client
+ * @param filename is absolute pathname
+ * @param fileContent is a known version of the file content that is more up to date than the one on disk
+ */
+ openClientFile(fileName: string, fileContent?: string, scriptKind?: ScriptKind): OpenConfiguredProjectResult {
+ return this.openClientFileWithNormalizedPath(toNormalizedPath(fileName), fileContent, scriptKind);
+ }
+
+ openClientFileWithNormalizedPath(fileName: NormalizedPath, fileContent?: string, scriptKind?: ScriptKind, hasMixedContent?: boolean): OpenConfiguredProjectResult {
+ const { configFileName = undefined, configFileErrors = undefined }: OpenConfiguredProjectResult = this.findContainingExternalProject(fileName)
+ ? {}
+ : this.openOrUpdateConfiguredProjectForFile(fileName);
+
+ // at this point if file is the part of some configured/external project then this project should be created
+ const info = this.getOrCreateScriptInfoForNormalizedPath(fileName, /*openedByClient*/ true, fileContent, scriptKind, hasMixedContent);
+ this.assignScriptInfoToInferredProjectIfNecessary(info, /*addToListOfOpenFiles*/ true);
+ this.printProjects();
+ return { configFileName, configFileErrors };
+ }
+
+ /**
+ * Close file whose contents is managed by the client
+ * @param filename is absolute pathname
+ */
+ closeClientFile(uncheckedFileName: string) {
+ const info = this.getScriptInfoForNormalizedPath(toNormalizedPath(uncheckedFileName));
+ if (info) {
+ this.closeOpenFile(info);
+ info.isOpen = false;
+ }
+ this.printProjects();
+ }
+
+ private collectChanges(lastKnownProjectVersions: protocol.ProjectVersionInfo[], currentProjects: Project[], result: protocol.ProjectFiles[]): void {
+ for (const proj of currentProjects) {
+ const knownProject = forEach(lastKnownProjectVersions, p => p.projectName === proj.getProjectName() && p);
+ result.push(proj.getChangesSinceVersion(knownProject && knownProject.version));
+ }
+ }
+
+ synchronizeProjectList(knownProjects: protocol.ProjectVersionInfo[]): protocol.ProjectFiles[] {
+ const files: protocol.ProjectFiles[] = [];
+ this.collectChanges(knownProjects, this.externalProjects, files);
+ this.collectChanges(knownProjects, this.configuredProjects, files);
+ this.collectChanges(knownProjects, this.inferredProjects, files);
+ return files;
+ }
+
+ applyChangesInOpenFiles(openFiles: protocol.ExternalFile[], changedFiles: protocol.ChangedOpenFile[], closedFiles: string[]): void {
+ const recordChangedFiles = changedFiles && !openFiles && !closedFiles;
+ if (openFiles) {
+ for (const file of openFiles) {
+ const scriptInfo = this.getScriptInfo(file.fileName);
+ Debug.assert(!scriptInfo || !scriptInfo.isOpen);
+ const normalizedPath = scriptInfo ? scriptInfo.fileName : toNormalizedPath(file.fileName);
+ this.openClientFileWithNormalizedPath(normalizedPath, file.content, file.scriptKind, file.hasMixedContent);
+ }
+ }
+
+ if (changedFiles) {
+ for (const file of changedFiles) {
+ const scriptInfo = this.getScriptInfo(file.fileName);
+ Debug.assert(!!scriptInfo);
+ // apply changes in reverse order
+ for (let i = file.changes.length - 1; i >= 0; i--) {
+ const change = file.changes[i];
+ scriptInfo.editContent(change.span.start, change.span.start + change.span.length, change.newText);
+ }
+ if (recordChangedFiles) {
+ if (!this.changedFiles) {
+ this.changedFiles = [scriptInfo];
+ }
+ else if (this.changedFiles.indexOf(scriptInfo) < 0) {
+ this.changedFiles.push(scriptInfo);
+ }
+ }
+ }
+ }
+
+ if (closedFiles) {
+ for (const file of closedFiles) {
+ this.closeClientFile(file);
+ }
+ }
+ // if files were open or closed then explicitly refresh list of inferred projects
+ // otherwise if there were only changes in files - record changed files in `changedFiles` and defer the update
+ if (openFiles || closedFiles) {
+ this.refreshInferredProjects();
+ }
+ }
+
+ closeExternalProject(uncheckedFileName: string): void {
+ const fileName = toNormalizedPath(uncheckedFileName);
+ const configFiles = this.externalProjectToConfiguredProjectMap[fileName];
+ if (configFiles) {
+ let shouldRefreshInferredProjects = false;
+ for (const configFile of configFiles) {
+ const configuredProject = this.findConfiguredProjectByProjectName(configFile);
+ if (configuredProject && configuredProject.deleteOpenRef() === 0) {
+ this.removeProject(configuredProject);
+ shouldRefreshInferredProjects = true;
+ }
+ }
+ if (shouldRefreshInferredProjects) {
+ this.refreshInferredProjects();
+ }
+ }
+ else {
+ // close external project
+ const externalProject = this.findExternalProjectByProjectName(uncheckedFileName);
+ if (externalProject) {
+ this.removeProject(externalProject);
+ this.refreshInferredProjects();
+ }
+ }
+ }
+
+ openExternalProject(proj: protocol.ExternalProject): void {
+ const externalProject = this.findExternalProjectByProjectName(proj.projectFileName);
+ if (externalProject) {
+<<<<<<< HEAD
+ this.updateNonInferredProject(externalProject, proj.rootFiles, externalFilePropertyReader, proj.options, proj.typingOptions, proj.options.compileOnSave);
+=======
+ this.updateNonInferredProject(externalProject, proj.rootFiles, externalFilePropertyReader, proj.options, proj.typingOptions);
+>>>>>>> d736db3b01a5f4f4215c17845deb3ae09cf28787
+ return;
+ }
+
+ let tsConfigFiles: NormalizedPath[];
+ const rootFiles: protocol.ExternalFile[] = [];
+ for (const file of proj.rootFiles) {
+ const normalized = toNormalizedPath(file.fileName);
+ if (getBaseFileName(normalized) === "tsconfig.json") {
+ (tsConfigFiles || (tsConfigFiles = [])).push(normalized);
+ }
+ else {
+ rootFiles.push(file);
+ }
+ }
+ if (tsConfigFiles) {
+ // store the list of tsconfig files that belong to the external project
+ this.externalProjectToConfiguredProjectMap[proj.projectFileName] = tsConfigFiles;
+ for (const tsconfigFile of tsConfigFiles) {
+ let project = this.findConfiguredProjectByProjectName(tsconfigFile);
+ if (!project) {
+ const result = this.openConfigFile(tsconfigFile);
+ // TODO: save errors
+ project = result.success && result.project;
+ }
+ if (project) {
+ // keep project alive even if no documents are opened - its lifetime is bound to the lifetime of containing external project
+ project.addOpenRef();
+ }
+ }
+ }
+ else {
+ this.createAndAddExternalProject(proj.projectFileName, rootFiles, proj.options, proj.typingOptions);
+ }
+ }
+ }
+}
diff --git a/src/server/project.ts b/src/server/project.ts
index d4d502c11e7..d36f1d955a6 100644
--- a/src/server/project.ts
+++ b/src/server/project.ts
@@ -1,8 +1,9 @@
-///
+///
///
///
///
///
+///
namespace ts.server {
@@ -32,6 +33,7 @@ namespace ts.server {
private program: ts.Program;
private languageService: LanguageService;
+ builder: Builder;
/**
* Set of files that was returned from the last call to getChangesSinceVersion.
*/
@@ -61,7 +63,8 @@ namespace ts.server {
private documentRegistry: ts.DocumentRegistry,
hasExplicitListOfFiles: boolean,
public languageServiceEnabled: boolean,
- private compilerOptions: CompilerOptions) {
+ private compilerOptions: CompilerOptions,
+ public compileOnSaveEnabled: boolean) {
if (!this.compilerOptions) {
this.compilerOptions = ts.getDefaultCompilerOptions();
@@ -79,6 +82,8 @@ namespace ts.server {
else {
this.disableLanguageService();
}
+
+ this.builder = createBuilder(this);
this.markAsDirty();
}
@@ -89,6 +94,14 @@ namespace ts.server {
return this.languageService;
}
+ getCompileOnSaveAffectedFileList(scriptInfo: ScriptInfo): string[] {
+ if (!this.languageServiceEnabled) {
+ return [];
+ }
+ this.updateGraph();
+ return this.builder.getFilesAffectedBy(scriptInfo);
+ }
+
getProjectVersion() {
return this.projectStateVersion.toString();
}
@@ -111,6 +124,13 @@ namespace ts.server {
abstract getProjectName(): string;
abstract getTypingOptions(): TypingOptions;
+ getSourceFile(path: Path) {
+ if (!this.program) {
+ return undefined;
+ }
+ return this.program.getSourceFileByPath(path);
+ }
+
close() {
if (this.program) {
// if we have a program - release all files that are enlisted in program
@@ -164,6 +184,17 @@ namespace ts.server {
return this.rootFiles;
}
+ getScriptInfos() {
+ return map(this.program.getSourceFiles(), sourceFile => this.getScriptInfoLSHost(sourceFile.path));
+ }
+
+ getFileEmitOutput(info: ScriptInfo, emitOnlyDtsFiles: boolean) {
+ if (!this.languageServiceEnabled) {
+ return undefined;
+ }
+ return this.getLanguageService().getEmitOutput(info.fileName, emitOnlyDtsFiles);
+ }
+
getFileNames() {
if (!this.program) {
return [];
@@ -184,6 +215,14 @@ namespace ts.server {
return sourceFiles.map(sourceFile => asNormalizedPath(sourceFile.fileName));
}
+ getFileNamesWithoutDefaultLib() {
+ if (!this.languageServiceEnabled) {
+ return this.getRootFiles();
+ }
+ const defaultLibraryFileName = getDefaultLibFileName(this.compilerOptions);
+ return filter(this.getFileNames(), file => getBaseFileName(file) !== defaultLibraryFileName);
+ }
+
containsScriptInfo(info: ScriptInfo): boolean {
return this.isRoot(info) || (this.program && this.program.getSourceFileByPath(info.path) !== undefined);
}
@@ -276,6 +315,7 @@ namespace ts.server {
}
}
}
+ this.builder.onProjectUpdateGraph();
return hasChanges;
}
@@ -377,6 +417,59 @@ namespace ts.server {
}
}
+ getReferencedFiles(path: Path): Path[] {
+ if (!this.languageServiceEnabled) {
+ return [];
+ }
+
+ const sourceFile = this.getSourceFile(path);
+ if (!sourceFile) {
+ return [];
+ }
+ // We need to use a set here since the code can contain the same import twice,
+ // but that will only be one dependency.
+ // To avoid invernal conversion, the key of the referencedFiles map must be of type Path
+ const referencedFiles = createMap();
+ if (sourceFile.imports) {
+ const checker: TypeChecker = this.program.getTypeChecker();
+ for (const importName of sourceFile.imports) {
+ const symbol = checker.getSymbolAtLocation(importName);
+ if (symbol && symbol.declarations && symbol.declarations[0]) {
+ const declarationSourceFile = symbol.declarations[0].getSourceFile();
+ if (declarationSourceFile) {
+ referencedFiles[declarationSourceFile.path] = true;
+ }
+ }
+ }
+ }
+
+ const currentDirectory = getDirectoryPath(path);
+ const getCanonicalFileName = createGetCanonicalFileName(this.projectService.host.useCaseSensitiveFileNames);
+ // Handle triple slash references
+ if (sourceFile.referencedFiles) {
+ for (const referencedFile of sourceFile.referencedFiles) {
+ const referencedPath = toPath(referencedFile.fileName, currentDirectory, getCanonicalFileName);
+ referencedFiles[referencedPath] = true;
+ }
+ }
+
+ // Handle type reference directives
+ if (sourceFile.resolvedTypeReferenceDirectiveNames) {
+ for (const typeName in sourceFile.resolvedTypeReferenceDirectiveNames) {
+ const resolvedTypeReferenceDirective = sourceFile.resolvedTypeReferenceDirectiveNames[typeName];
+ if (!resolvedTypeReferenceDirective) {
+ continue;
+ }
+
+ const fileName = resolvedTypeReferenceDirective.resolvedFileName;
+ const typeFilePath = toPath(fileName, currentDirectory, getCanonicalFileName);
+ referencedFiles[typeFilePath] = true;
+ }
+ }
+
+ return map(Object.keys(referencedFiles), key => key);
+ }
+
// remove a root file from project
private removeRootFileIfNecessary(info: ScriptInfo): void {
if (this.isRoot(info)) {
@@ -404,7 +497,8 @@ namespace ts.server {
documentRegistry,
/*files*/ undefined,
languageServiceEnabled,
- compilerOptions);
+ compilerOptions,
+ /*compileOnSaveEnabled*/ false);
this.inferredProjectName = makeInferredProjectName(InferredProject.NextId);
InferredProject.NextId++;
@@ -445,8 +539,9 @@ namespace ts.server {
compilerOptions: CompilerOptions,
private typingOptions: TypingOptions,
private wildcardDirectories: Map,
- languageServiceEnabled: boolean) {
- super(ProjectKind.Configured, projectService, documentRegistry, hasExplicitListOfFiles, languageServiceEnabled, compilerOptions);
+ languageServiceEnabled: boolean,
+ public compileOnSaveEnabled = false) {
+ super(ProjectKind.Configured, projectService, documentRegistry, hasExplicitListOfFiles, languageServiceEnabled, compilerOptions, compileOnSaveEnabled);
}
setTypingOptions(newTypingOptions: TypingOptions): void {
@@ -533,8 +628,9 @@ namespace ts.server {
documentRegistry: ts.DocumentRegistry,
compilerOptions: CompilerOptions,
typingOptions: TypingOptions,
- languageServiceEnabled: boolean) {
- super(ProjectKind.External, projectService, documentRegistry, /*hasExplicitListOfFiles*/ true, languageServiceEnabled, compilerOptions);
+ languageServiceEnabled: boolean,
+ public compileOnSaveEnabled = true) {
+ super(ProjectKind.External, projectService, documentRegistry, /*hasExplicitListOfFiles*/ true, languageServiceEnabled, compilerOptions, compileOnSaveEnabled);
this.setTypingOptions(typingOptions);
}
diff --git a/src/server/project.ts.orig b/src/server/project.ts.orig
new file mode 100644
index 00000000000..6fef06dfa7e
--- /dev/null
+++ b/src/server/project.ts.orig
@@ -0,0 +1,681 @@
+///
+///
+///
+///
+///
+///
+
+namespace ts.server {
+
+ export enum ProjectKind {
+ Inferred,
+ Configured,
+ External
+ }
+
+ function remove(items: T[], item: T) {
+ const index = items.indexOf(item);
+ if (index >= 0) {
+ items.splice(index, 1);
+ }
+ }
+
+ const jsOrDts = [".js", ".d.ts"];
+
+ export function allFilesAreJsOrDts(project: Project): boolean {
+ return project.getFileNames().every(f => fileExtensionIsAny(f, jsOrDts));
+ }
+
+ export abstract class Project {
+ private rootFiles: ScriptInfo[] = [];
+ private rootFilesMap: FileMap = createFileMap();
+ private lsHost: ServerLanguageServiceHost;
+ private program: ts.Program;
+
+ private languageService: LanguageService;
+ builder: Builder;
+ /**
+ * Set of files that was returned from the last call to getChangesSinceVersion.
+ */
+ private lastReportedFileNames: Map;
+ /**
+ * Last version that was reported.
+ */
+ private lastReportedVersion = 0;
+ /**
+ * Current project structure version.
+ * This property is changed in 'updateGraph' based on the set of files in program
+ */
+ private projectStructureVersion = 0;
+ /**
+ * Current version of the project state. It is changed when:
+ * - new root file was added/removed
+ * - edit happen in some file that is currently included in the project.
+ * This property is different from projectStructureVersion since in most cases edits don't affect set of files in the project
+ */
+ private projectStateVersion = 0;
+
+ private typingFiles: TypingsArray;
+
+ constructor(
+ readonly projectKind: ProjectKind,
+ readonly projectService: ProjectService,
+ private documentRegistry: ts.DocumentRegistry,
+ hasExplicitListOfFiles: boolean,
+ public languageServiceEnabled: boolean,
+ private compilerOptions: CompilerOptions,
+ public compileOnSaveEnabled: boolean) {
+
+ if (!this.compilerOptions) {
+ this.compilerOptions = ts.getDefaultCompilerOptions();
+ this.compilerOptions.allowNonTsExtensions = true;
+ this.compilerOptions.allowJs = true;
+ }
+ else if (hasExplicitListOfFiles) {
+ // If files are listed explicitly, allow all extensions
+ this.compilerOptions.allowNonTsExtensions = true;
+ }
+
+ if (languageServiceEnabled) {
+ this.enableLanguageService();
+ }
+ else {
+ this.disableLanguageService();
+ }
+
+ this.builder = createBuilder(this);
+ this.markAsDirty();
+ }
+
+ getLanguageService(ensureSynchronized = true): LanguageService {
+ if (ensureSynchronized) {
+ this.updateGraph();
+ }
+ return this.languageService;
+ }
+
+ getCompileOnSaveAffectedFileList(scriptInfo: ScriptInfo): string[] {
+ if (!this.languageServiceEnabled) {
+ return [];
+ }
+ this.updateGraph();
+ return this.builder.getFilesAffectedBy(scriptInfo);
+ }
+
+ getProjectVersion() {
+ return this.projectStateVersion.toString();
+ }
+
+ enableLanguageService() {
+ const lsHost = new LSHost(this.projectService.host, this, this.projectService.cancellationToken);
+ lsHost.setCompilationSettings(this.compilerOptions);
+ this.languageService = ts.createLanguageService(lsHost, this.documentRegistry);
+
+ this.lsHost = lsHost;
+ this.languageServiceEnabled = true;
+ }
+
+ disableLanguageService() {
+ this.languageService = nullLanguageService;
+ this.lsHost = nullLanguageServiceHost;
+ this.languageServiceEnabled = false;
+ }
+
+ abstract getProjectName(): string;
+ abstract getTypingOptions(): TypingOptions;
+<<<<<<< HEAD
+
+ getSourceFile(path: Path) {
+ if (!this.program) {
+ return undefined;
+ }
+ return this.program.getSourceFileByPath(path);
+ }
+=======
+>>>>>>> d736db3b01a5f4f4215c17845deb3ae09cf28787
+
+ close() {
+ if (this.program) {
+ // if we have a program - release all files that are enlisted in program
+ for (const f of this.program.getSourceFiles()) {
+ const info = this.projectService.getScriptInfo(f.fileName);
+ info.detachFromProject(this);
+ }
+ }
+ else {
+ // release all root files
+ for (const root of this.rootFiles) {
+ root.detachFromProject(this);
+ }
+ }
+ this.rootFiles = undefined;
+ this.rootFilesMap = undefined;
+ this.program = undefined;
+
+ // signal language service to release source files acquired from document registry
+ this.languageService.dispose();
+ }
+
+ getCompilerOptions() {
+ return this.compilerOptions;
+ }
+
+ hasRoots() {
+ return this.rootFiles && this.rootFiles.length > 0;
+ }
+
+ getRootFiles() {
+ return this.rootFiles && this.rootFiles.map(info => info.fileName);
+ }
+
+ getRootFilesLSHost() {
+ const result: string[] = [];
+ if (this.rootFiles) {
+ for (const f of this.rootFiles) {
+ result.push(f.fileName);
+ }
+ if (this.typingFiles) {
+ for (const f of this.typingFiles) {
+ result.push(f);
+ }
+ }
+ }
+ return result;
+ }
+
+ getRootScriptInfos() {
+ return this.rootFiles;
+ }
+
+ getScriptInfos() {
+ return map(this.program.getSourceFiles(), sourceFile => this.getScriptInfoLSHost(sourceFile.path));
+ }
+
+ getFileEmitOutput(info: ScriptInfo, emitOnlyDtsFiles: boolean) {
+ if (!this.languageServiceEnabled) {
+ return undefined;
+ }
+ return this.getLanguageService().getEmitOutput(info.fileName, emitOnlyDtsFiles);
+ }
+
+ getFileNames() {
+ if (!this.program) {
+ return [];
+ }
+
+ if (!this.languageServiceEnabled) {
+ // if language service is disabled assume that all files in program are root files + default library
+ let rootFiles = this.getRootFiles();
+ if (this.compilerOptions) {
+ const defaultLibrary = getDefaultLibFilePath(this.compilerOptions);
+ if (defaultLibrary) {
+ (rootFiles || (rootFiles = [])).push(asNormalizedPath(defaultLibrary));
+ }
+ }
+ return rootFiles;
+ }
+ const sourceFiles = this.program.getSourceFiles();
+ return sourceFiles.map(sourceFile => asNormalizedPath(sourceFile.fileName));
+ }
+
+ getFileNamesWithoutDefaultLib() {
+ if (!this.languageServiceEnabled) {
+ return this.getRootFiles();
+ }
+ const defaultLibraryFileName = getDefaultLibFileName(this.compilerOptions);
+ return filter(this.getFileNames(), file => getBaseFileName(file) !== defaultLibraryFileName);
+ }
+
+ containsScriptInfo(info: ScriptInfo): boolean {
+ return this.isRoot(info) || (this.program && this.program.getSourceFileByPath(info.path) !== undefined);
+ }
+
+ containsFile(filename: NormalizedPath, requireOpen?: boolean) {
+ const info = this.projectService.getScriptInfoForNormalizedPath(filename);
+ if (info && (info.isOpen || !requireOpen)) {
+ return this.containsScriptInfo(info);
+ }
+ }
+
+ isRoot(info: ScriptInfo) {
+ return this.rootFilesMap && this.rootFilesMap.contains(info.path);
+ }
+
+ // add a root file to project
+ addRoot(info: ScriptInfo) {
+ if (!this.isRoot(info)) {
+ this.rootFiles.push(info);
+ this.rootFilesMap.set(info.path, info);
+ info.attachToProject(this);
+
+ this.markAsDirty();
+ }
+ }
+
+ removeFile(info: ScriptInfo, detachFromProject = true) {
+ this.removeRootFileIfNecessary(info);
+ this.lsHost.notifyFileRemoved(info);
+
+ if (detachFromProject) {
+ info.detachFromProject(this);
+ }
+
+ this.markAsDirty();
+ }
+
+ markAsDirty() {
+ this.projectStateVersion++;
+ }
+
+ /**
+ * Updates set of files that contribute to this project
+ * @returns: true if set of files in the project stays the same and false - otherwise.
+ */
+ updateGraph(): boolean {
+ if (!this.languageServiceEnabled) {
+ return true;
+ }
+ let hasChanges = this.updateGraphWorker();
+ const cachedTypings = this.projectService.typingsCache.getTypingsForProject(this);
+ if (this.setTypings(cachedTypings)) {
+ hasChanges = this.updateGraphWorker() || hasChanges;
+ }
+ if (hasChanges) {
+ this.projectStructureVersion++;
+ }
+ return !hasChanges;
+ }
+
+ private setTypings(typings: TypingsArray): boolean {
+ if (arrayIsEqualTo(this.typingFiles, typings)) {
+ return false;
+ }
+ this.typingFiles = typings;
+ this.markAsDirty();
+ return true;
+ }
+
+ private updateGraphWorker() {
+ const oldProgram = this.program;
+ this.program = this.languageService.getProgram();
+
+ let hasChanges = false;
+ // bump up the version if
+ // - oldProgram is not set - this is a first time updateGraph is called
+ // - newProgram is different from the old program and structure of the old program was not reused.
+ if (!oldProgram || (this.program !== oldProgram && !oldProgram.structureIsReused)) {
+ hasChanges = true;
+ if (oldProgram) {
+ for (const f of oldProgram.getSourceFiles()) {
+ if (this.program.getSourceFileByPath(f.path)) {
+ continue;
+ }
+ // new program does not contain this file - detach it from the project
+ const scriptInfoToDetach = this.projectService.getScriptInfo(f.fileName);
+ if (scriptInfoToDetach) {
+ scriptInfoToDetach.detachFromProject(this);
+ }
+ }
+ }
+ }
+ this.builder.onProjectUpdateGraph();
+ return hasChanges;
+ }
+
+ getScriptInfoLSHost(fileName: string) {
+ const scriptInfo = this.projectService.getOrCreateScriptInfo(fileName, /*openedByClient*/ false);
+ if (scriptInfo) {
+ scriptInfo.attachToProject(this);
+ }
+ return scriptInfo;
+ }
+
+ getScriptInfoForNormalizedPath(fileName: NormalizedPath) {
+ const scriptInfo = this.projectService.getOrCreateScriptInfoForNormalizedPath(fileName, /*openedByClient*/ false);
+ Debug.assert(!scriptInfo || scriptInfo.isAttached(this));
+ return scriptInfo;
+ }
+
+ getScriptInfo(uncheckedFileName: string) {
+ return this.getScriptInfoForNormalizedPath(toNormalizedPath(uncheckedFileName));
+ }
+
+ filesToString() {
+ if (!this.program) {
+ return "";
+ }
+ let strBuilder = "";
+ for (const file of this.program.getSourceFiles()) {
+ strBuilder += `${file.fileName}\n`;
+ }
+ return strBuilder;
+ }
+
+ setCompilerOptions(compilerOptions: CompilerOptions) {
+ if (compilerOptions) {
+ if (this.projectKind === ProjectKind.Inferred) {
+ compilerOptions.allowJs = true;
+ }
+ compilerOptions.allowNonTsExtensions = true;
+ this.compilerOptions = compilerOptions;
+ this.lsHost.setCompilationSettings(compilerOptions);
+
+ this.markAsDirty();
+ }
+ }
+
+ reloadScript(filename: NormalizedPath): boolean {
+ const script = this.projectService.getScriptInfoForNormalizedPath(filename);
+ if (script) {
+ Debug.assert(script.isAttached(this));
+ script.reloadFromFile();
+ return true;
+ }
+ return false;
+ }
+
+ getChangesSinceVersion(lastKnownVersion?: number): protocol.ProjectFiles {
+ this.updateGraph();
+
+ const info = {
+ projectName: this.getProjectName(),
+ version: this.projectStructureVersion,
+ isInferred: this.projectKind === ProjectKind.Inferred,
+ options: this.getCompilerOptions()
+ };
+ // check if requested version is the same that we have reported last time
+ if (this.lastReportedFileNames && lastKnownVersion === this.lastReportedVersion) {
+ // if current structure version is the same - return info witout any changes
+ if (this.projectStructureVersion == this.lastReportedVersion) {
+ return { info };
+ }
+ // compute and return the difference
+ const lastReportedFileNames = this.lastReportedFileNames;
+ const currentFiles = arrayToMap(this.getFileNames(), x => x);
+
+ const added: string[] = [];
+ const removed: string[] = [];
+ for (const id in currentFiles) {
+ if (hasProperty(currentFiles, id) && !hasProperty(lastReportedFileNames, id)) {
+ added.push(id);
+ }
+ }
+ for (const id in lastReportedFileNames) {
+ if (hasProperty(lastReportedFileNames, id) && !hasProperty(currentFiles, id)) {
+ removed.push(id);
+ }
+ }
+ this.lastReportedFileNames = currentFiles;
+
+ this.lastReportedFileNames = currentFiles;
+ this.lastReportedVersion = this.projectStructureVersion;
+ return { info, changes: { added, removed } };
+ }
+ else {
+ // unknown version - return everything
+ const projectFileNames = this.getFileNames();
+ this.lastReportedFileNames = arrayToMap(projectFileNames, x => x);
+ this.lastReportedVersion = this.projectStructureVersion;
+ return { info, files: projectFileNames };
+ }
+ }
+
+ getReferencedFiles(path: Path): Path[] {
+ if (!this.languageServiceEnabled) {
+ return [];
+ }
+
+ const sourceFile = this.getSourceFile(path);
+ if (!sourceFile) {
+ return [];
+ }
+ // We need to use a set here since the code can contain the same import twice,
+ // but that will only be one dependency.
+ // To avoid invernal conversion, the key of the referencedFiles map must be of type Path
+ const referencedFiles = createMap();
+ if (sourceFile.imports) {
+ const checker: TypeChecker = this.program.getTypeChecker();
+ for (const importName of sourceFile.imports) {
+ const symbol = checker.getSymbolAtLocation(importName);
+ if (symbol && symbol.declarations && symbol.declarations[0]) {
+ const declarationSourceFile = symbol.declarations[0].getSourceFile();
+ if (declarationSourceFile) {
+ referencedFiles[declarationSourceFile.path] = true;
+ }
+ }
+ }
+ }
+
+ const currentDirectory = getDirectoryPath(path);
+ const getCanonicalFileName = createGetCanonicalFileName(this.projectService.host.useCaseSensitiveFileNames);
+ // Handle triple slash references
+ if (sourceFile.referencedFiles) {
+ for (const referencedFile of sourceFile.referencedFiles) {
+ const referencedPath = toPath(referencedFile.fileName, currentDirectory, getCanonicalFileName);
+ referencedFiles[referencedPath] = true;
+ }
+ }
+
+ // Handle type reference directives
+ if (sourceFile.resolvedTypeReferenceDirectiveNames) {
+ for (const typeName in sourceFile.resolvedTypeReferenceDirectiveNames) {
+ const resolvedTypeReferenceDirective = sourceFile.resolvedTypeReferenceDirectiveNames[typeName];
+ if (!resolvedTypeReferenceDirective) {
+ continue;
+ }
+
+ const fileName = resolvedTypeReferenceDirective.resolvedFileName;
+ const typeFilePath = toPath(fileName, currentDirectory, getCanonicalFileName);
+ referencedFiles[typeFilePath] = true;
+ }
+ }
+
+ return map(Object.keys(referencedFiles), key => key);
+ }
+
+ // remove a root file from project
+ private removeRootFileIfNecessary(info: ScriptInfo): void {
+ if (this.isRoot(info)) {
+ remove(this.rootFiles, info);
+ this.rootFilesMap.remove(info.path);
+ }
+ }
+ }
+
+ export class InferredProject extends Project {
+
+ private static NextId = 1;
+
+ /**
+ * Unique name that identifies this particular inferred project
+ */
+ private readonly inferredProjectName: string;
+
+ // Used to keep track of what directories are watched for this project
+ directoriesWatchedForTsconfig: string[] = [];
+
+ constructor(projectService: ProjectService, documentRegistry: ts.DocumentRegistry, languageServiceEnabled: boolean, compilerOptions: CompilerOptions) {
+ super(ProjectKind.Inferred,
+ projectService,
+ documentRegistry,
+ /*files*/ undefined,
+ languageServiceEnabled,
+ compilerOptions,
+ /*compileOnSaveEnabled*/ false);
+
+ this.inferredProjectName = makeInferredProjectName(InferredProject.NextId);
+ InferredProject.NextId++;
+ }
+
+ getProjectName() {
+ return this.inferredProjectName;
+ }
+
+ close() {
+ super.close();
+
+ for (const directory of this.directoriesWatchedForTsconfig) {
+ this.projectService.stopWatchingDirectory(directory);
+ }
+ }
+
+ getTypingOptions(): TypingOptions {
+ return {
+ enableAutoDiscovery: allFilesAreJsOrDts(this),
+ include: [],
+ exclude: []
+ };
+ }
+ }
+
+ export class ConfiguredProject extends Project {
+ private projectFileWatcher: FileWatcher;
+ private directoryWatcher: FileWatcher;
+ private directoriesWatchedForWildcards: Map;
+ /** Used for configured projects which may have multiple open roots */
+ openRefCount = 0;
+
+ constructor(readonly configFileName: NormalizedPath,
+ projectService: ProjectService,
+ documentRegistry: ts.DocumentRegistry,
+ hasExplicitListOfFiles: boolean,
+ compilerOptions: CompilerOptions,
+ private typingOptions: TypingOptions,
+ private wildcardDirectories: Map,
+ languageServiceEnabled: boolean,
+ public compileOnSaveEnabled = false) {
+ super(ProjectKind.Configured, projectService, documentRegistry, hasExplicitListOfFiles, languageServiceEnabled, compilerOptions, compileOnSaveEnabled);
+ }
+
+ setTypingOptions(newTypingOptions: TypingOptions): void {
+ this.typingOptions = newTypingOptions;
+ }
+
+ setTypingOptions(newTypingOptions: TypingOptions): void {
+ this.typingOptions = newTypingOptions;
+ }
+
+ getTypingOptions() {
+ return this.typingOptions;
+ }
+
+ getProjectName() {
+ return this.configFileName;
+ }
+
+ watchConfigFile(callback: (project: ConfiguredProject) => void) {
+ this.projectFileWatcher = this.projectService.host.watchFile(this.configFileName, _ => callback(this));
+ }
+
+ watchConfigDirectory(callback: (project: ConfiguredProject, path: string) => void) {
+ if (this.directoryWatcher) {
+ return;
+ }
+
+ const directoryToWatch = getDirectoryPath(this.configFileName);
+ this.projectService.logger.info(`Add recursive watcher for: ${directoryToWatch}`);
+ this.directoryWatcher = this.projectService.host.watchDirectory(directoryToWatch, path => callback(this, path), /*recursive*/ true);
+ }
+
+ watchWildcards(callback: (project: ConfiguredProject, path: string) => void) {
+ if (!this.wildcardDirectories) {
+ return;
+ }
+ const configDirectoryPath = getDirectoryPath(this.configFileName);
+ this.directoriesWatchedForWildcards = reduceProperties(this.wildcardDirectories, (watchers, flag, directory) => {
+ if (comparePaths(configDirectoryPath, directory, ".", !this.projectService.host.useCaseSensitiveFileNames) !== Comparison.EqualTo) {
+ const recursive = (flag & WatchDirectoryFlags.Recursive) !== 0;
+ this.projectService.logger.info(`Add ${recursive ? "recursive " : ""}watcher for: ${directory}`);
+ watchers[directory] = this.projectService.host.watchDirectory(
+ directory,
+ path => callback(this, path),
+ recursive
+ );
+ }
+ return watchers;
+ },