From a082857ae81794fc7fe22f8bbb967c8ca2c009e5 Mon Sep 17 00:00:00 2001 From: Zhengbo Li Date: Tue, 23 Aug 2016 16:11:52 -0700 Subject: [PATCH] Add APIs for enabling CompileOnSave on tsserver (#9837) * Add API to get only the emited declarations output * Add nonModuleBuilder * Add basic tests for CompileOnSaveAffectedFileList API * Add API for compile single file * Avoid invoking project.languageService directly * Add API to query if compileOnSave is enabled for a project * Seperate check and emit signatures * Use Path type for internal file name matching and simplifying builder logic * Always return cascaded affected list * Correct the tsconfig file in compileOnSave tests Also move the CompileOnSave option out of compilerOptions * Reduce string to path conversion --- src/compiler/commandLineParser.ts | 18 +- src/compiler/core.ts | 24 +- src/compiler/emitter.ts | 54 +- src/compiler/parser.ts | 2 +- src/compiler/program.ts | 9 +- src/compiler/types.ts | 4 +- src/compiler/utilities.ts | 18 +- .../unittests/tsserverProjectSystem.ts | 400 ++++- src/server/builder.ts | 368 ++++ src/server/cancellationToken.ts | 2 +- src/server/editorServices.ts | 20 +- src/server/editorServices.ts.orig | 1203 +++++++++++++ src/server/project.ts | 110 +- src/server/project.ts.orig | 681 ++++++++ src/server/protocol.d.ts | 21 +- src/server/protocol.d.ts.orig | 1488 +++++++++++++++++ src/server/session.ts | 30 +- src/server/utilities.ts | 3 +- src/services/services.ts | 6 +- 19 files changed, 4390 insertions(+), 71 deletions(-) create mode 100644 src/server/builder.ts create mode 100644 src/server/editorServices.ts.orig create mode 100644 src/server/project.ts.orig create mode 100644 src/server/protocol.d.ts.orig diff --git a/src/compiler/commandLineParser.ts b/src/compiler/commandLineParser.ts index 2fe3f63450..682a434e6a 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 8298a62f93..77cc379acc 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 357a15507a..ca015edc73 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 4f63448ecd..3e20f899c2 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 9deb967427..932f400660 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 974f2a1d82..202f45eb15 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 f0351a39bb..9d60521d76 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 2a309057db..d1041e55d0 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 0000000000..19e515c445 --- /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 dbc0771612..6d3dec67cc 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 8c3d7f42e2..1557fcd087 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 0000000000..b100cf2126 --- /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 d4d502c11e..d36f1d955a 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 0000000000..6fef06dfa7 --- /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; + }, >{}); + } + + stopWatchingDirectory() { + if (this.directoryWatcher) { + this.directoryWatcher.close(); + this.directoryWatcher = undefined; + } + } + + close() { + super.close(); + + if (this.projectFileWatcher) { + this.projectFileWatcher.close(); + } + + for (const id in this.directoriesWatchedForWildcards) { + this.directoriesWatchedForWildcards[id].close(); + } + this.directoriesWatchedForWildcards = undefined; + + this.stopWatchingDirectory(); + } + + addOpenRef() { + this.openRefCount++; + } + + deleteOpenRef() { + this.openRefCount--; + return this.openRefCount; + } + } + + export class ExternalProject extends Project { + private typingOptions: TypingOptions; + constructor(readonly externalProjectName: string, + projectService: ProjectService, + documentRegistry: ts.DocumentRegistry, + compilerOptions: CompilerOptions, + typingOptions: TypingOptions, +<<<<<<< HEAD + languageServiceEnabled: boolean, + public compileOnSaveEnabled = true) { + super(ProjectKind.External, projectService, documentRegistry, /*hasExplicitListOfFiles*/ true, languageServiceEnabled, compilerOptions, compileOnSaveEnabled); +======= + languageServiceEnabled: boolean) { + super(ProjectKind.External, projectService, documentRegistry, /*hasExplicitListOfFiles*/ true, languageServiceEnabled, compilerOptions); +>>>>>>> d736db3b01a5f4f4215c17845deb3ae09cf28787 + this.setTypingOptions(typingOptions); + } + + getTypingOptions() { + return this.typingOptions; + } + + setTypingOptions(newTypingOptions: TypingOptions): void { + if (!newTypingOptions) { + // set default typings options + newTypingOptions = { + enableAutoDiscovery: allFilesAreJsOrDts(this), + include: [], + exclude: [] + }; + } + else { + if (newTypingOptions.enableAutoDiscovery === undefined) { + // if autoDiscovery was not specified by the caller - set it based on the content of the project + newTypingOptions.enableAutoDiscovery = allFilesAreJsOrDts(this); + } + if (!newTypingOptions.include) { + newTypingOptions.include = []; + } + if (!newTypingOptions.exclude) { + newTypingOptions.exclude = []; + } + } + this.typingOptions = newTypingOptions; + } + + getProjectName() { + return this.externalProjectName; + } + } +} \ No newline at end of file diff --git a/src/server/protocol.d.ts b/src/server/protocol.d.ts index d9267d9e4b..4b2c69411a 100644 --- a/src/server/protocol.d.ts +++ b/src/server/protocol.d.ts @@ -498,10 +498,18 @@ declare namespace ts.server.protocol { export interface ExternalProject { projectFileName: string; rootFiles: ExternalFile[]; - options: CompilerOptions; + options: ExternalProjectCompilerOptions; typingOptions?: TypingOptions; } + /** + * For external projects, some of the project settings are sent together with + * compiler settings. + */ + export interface ExternalProjectCompilerOptions extends CompilerOptions { + compileOnSave?: boolean; + } + export interface ProjectVersionInfo { projectName: string; isInferred: boolean; @@ -721,6 +729,17 @@ declare namespace ts.server.protocol { export interface CloseRequest extends FileRequest { } + export interface CompileOnSaveAffectedFileListRequest extends FileRequest { + } + + export interface CompileOnSaveEmitFileRequest extends FileRequest { + args: CompileOnSaveEmitFileRequestArgs; + } + + export interface CompileOnSaveEmitFileRequestArgs extends FileRequestArgs { + forced?: boolean; + } + /** * Quickinfo request; value of command field is * "quickinfo". Return response giving a quick type and diff --git a/src/server/protocol.d.ts.orig b/src/server/protocol.d.ts.orig new file mode 100644 index 0000000000..0fd9a916be --- /dev/null +++ b/src/server/protocol.d.ts.orig @@ -0,0 +1,1488 @@ +/** + * Declaration module describing the TypeScript Server protocol + */ +declare namespace ts.server.protocol { + /** + * A TypeScript Server message + */ + export interface Message { + /** + * Sequence number of the message + */ + seq: number; + + /** + * One of "request", "response", or "event" + */ + type: string; + } + + /** + * Client-initiated request message + */ + export interface Request extends Message { + /** + * The command to execute + */ + command: string; + + /** + * Object containing arguments for the command + */ + arguments?: any; + } + + /** + * Request to reload the project structure for all the opened files + */ + export interface ReloadProjectsRequest extends Message { + } + + /** + * Server-initiated event message + */ + export interface Event extends Message { + /** + * Name of event + */ + event: string; + + /** + * Event-specific information + */ + body?: any; + } + + /** + * Response by server to client request message. + */ + export interface Response extends Message { + /** + * Sequence number of the request message. + */ + request_seq: number; + + /** + * Outcome of the request. + */ + success: boolean; + + /** + * The command requested. + */ + command: string; + + /** + * Contains error message if success === false. + */ + message?: string; + + /** + * Contains message body if success === true. + */ + body?: any; + } + + /** + * Arguments for FileRequest messages. + */ + export interface FileRequestArgs { + /** + * The file for the request (absolute pathname required). + */ + file: string; + + /* + * Optional name of project that contains file + */ + projectFileName?: string; + } + + export interface TodoCommentRequest extends FileRequest { + arguments: TodoCommentRequestArgs; + } + + export interface TodoCommentRequestArgs extends FileRequestArgs { + descriptors: TodoCommentDescriptor[]; + } + + export interface IndentationRequest extends FileLocationRequest { + arguments: IndentationRequestArgs; + } + + export interface IndentationRequestArgs extends FileLocationRequestArgs { + options?: EditorSettings; + } + + /** + * Arguments for ProjectInfoRequest request. + */ + export interface ProjectInfoRequestArgs extends FileRequestArgs { + /** + * Indicate if the file name list of the project is needed + */ + needFileNameList: boolean; + } + + /** + * A request to get the project information of the current file + */ + export interface ProjectInfoRequest extends Request { + arguments: ProjectInfoRequestArgs; + } + + export interface ProjectRequest extends Request { + arguments: ProjectRequestArgs; + } + + export interface ProjectRequestArgs { + projectFileName: string; + } + + /** + * Response message body for "projectInfo" request + */ + export interface ProjectInfo { + /** + * For configured project, this is the normalized path of the 'tsconfig.json' file + * For inferred project, this is undefined + */ + configFileName: string; + /** + * The list of normalized file name in the project, including 'lib.d.ts' + */ + fileNames?: string[]; + /** + * Indicates if the project has a active language service instance + */ + languageServiceDisabled?: boolean; + } + + export interface DiagnosticWithLinePosition { + message: string; + start: number; + length: number; + startLocation: Location; + endLocation: Location; + category: string; + code: number; + } + + /** + * Response message for "projectInfo" request + */ + export interface ProjectInfoResponse extends Response { + body?: ProjectInfo; + } + + /** + * Request whose sole parameter is a file name. + */ + export interface FileRequest extends Request { + arguments: FileRequestArgs; + } + + /** + * Instances of this interface specify a location in a source file: + * (file, line, character offset), where line and character offset are 1-based. + */ + export interface FileLocationRequestArgs extends FileRequestArgs { + /** + * The line number for the request (1-based). + */ + line?: number; + + /** + * The character offset (on the line) for the request (1-based). + */ + offset?: number; + + /** + * Position (can be specified instead of line/offset pair) + */ + position?: number; + } + + /** + * A request whose arguments specify a file location (file, line, col). + */ + export interface FileLocationRequest extends FileRequest { + arguments: FileLocationRequestArgs; + } + + export interface FileSpanRequestArgs extends FileRequestArgs { + start: number; + length: number; + } + + export interface FileSpanRequest extends FileRequest { + arguments: FileSpanRequestArgs; + } + + /** + * Arguments in document highlight request; include: filesToSearch, file, + * line, offset. + */ + export interface DocumentHighlightsRequestArgs extends FileLocationRequestArgs { + /** + * List of files to search for document highlights. + */ + filesToSearch: string[]; + } + + /** + * Go to definition request; value of command field is + * "definition". Return response giving the file locations that + * define the symbol found in file at location line, col. + */ + export interface DefinitionRequest extends FileLocationRequest { + } + + /** + * Go to type request; value of command field is + * "typeDefinition". Return response giving the file locations that + * define the type for the symbol found in file at location line, col. + */ + export interface TypeDefinitionRequest extends FileLocationRequest { + } + + /** + * Location in source code expressed as (one-based) line and character offset. + */ + export interface Location { + line: number; + offset: number; + } + + /** + * Object found in response messages defining a span of text in source code. + */ + export interface TextSpan { + /** + * First character of the definition. + */ + start: Location; + + /** + * One character past last character of the definition. + */ + end: Location; + } + + /** + * Object found in response messages defining a span of text in a specific source file. + */ + export interface FileSpan extends TextSpan { + /** + * File containing text span. + */ + file: string; + } + + /** + * Definition response message. Gives text range for definition. + */ + export interface DefinitionResponse extends Response { + body?: FileSpan[]; + } + + /** + * Definition response message. Gives text range for definition. + */ + export interface TypeDefinitionResponse extends Response { + body?: FileSpan[]; + } + + export interface BraceCompletionRequest extends FileLocationRequest { + arguments: BraceCompletionRequestArgs; + } + + export interface BraceCompletionRequestArgs extends FileLocationRequestArgs { + openingBrace: string; + } + + /** + * Get occurrences request; value of command field is + * "occurrences". Return response giving spans that are relevant + * in the file at a given line and column. + */ + export interface OccurrencesRequest extends FileLocationRequest { + } + + export interface OccurrencesResponseItem extends FileSpan { + /** + * True if the occurrence is a write location, false otherwise. + */ + isWriteAccess: boolean; + } + + export interface OccurrencesResponse extends Response { + body?: OccurrencesResponseItem[]; + } + + /** + * Get document highlights request; value of command field is + * "documentHighlights". Return response giving spans that are relevant + * in the file at a given line and column. + */ + export interface DocumentHighlightsRequest extends FileLocationRequest { + arguments: DocumentHighlightsRequestArgs; + } + + export interface HighlightSpan extends TextSpan { + kind: string; + } + + export interface DocumentHighlightsItem { + /** + * File containing highlight spans. + */ + file: string; + + /** + * Spans to highlight in file. + */ + highlightSpans: HighlightSpan[]; + } + + export interface DocumentHighlightsResponse extends Response { + body?: DocumentHighlightsItem[]; + } + + /** + * Find references request; value of command field is + * "references". Return response giving the file locations that + * reference the symbol found in file at location line, col. + */ + export interface ReferencesRequest extends FileLocationRequest { + } + + export interface ReferencesResponseItem extends FileSpan { + /** Text of line containing the reference. Including this + * with the response avoids latency of editor loading files + * to show text of reference line (the server already has + * loaded the referencing files). + */ + lineText: string; + + /** + * True if reference is a write location, false otherwise. + */ + isWriteAccess: boolean; + + /** + * True if reference is a definition, false otherwise. + */ + isDefinition: boolean; + } + + /** + * The body of a "references" response message. + */ + export interface ReferencesResponseBody { + /** + * The file locations referencing the symbol. + */ + refs: ReferencesResponseItem[]; + + /** + * The name of the symbol. + */ + symbolName: string; + + /** + * The start character offset of the symbol (on the line provided by the references request). + */ + symbolStartOffset: number; + + /** + * The full display name of the symbol. + */ + symbolDisplayString: string; + } + + /** + * Response to "references" request. + */ + export interface ReferencesResponse extends Response { + body?: ReferencesResponseBody; + } + + export interface RenameRequestArgs extends FileLocationRequestArgs { + findInComments?: boolean; + findInStrings?: boolean; + } + + + /** + * Rename request; value of command field is "rename". Return + * response giving the file locations that reference the symbol + * found in file at location line, col. Also return full display + * name of the symbol so that client can print it unambiguously. + */ + export interface RenameRequest extends FileLocationRequest { + arguments: RenameRequestArgs; + } + + /** + * Information about the item to be renamed. + */ + export interface RenameInfo { + /** + * True if item can be renamed. + */ + canRename: boolean; + + /** + * Error message if item can not be renamed. + */ + localizedErrorMessage?: string; + + /** + * Display name of the item to be renamed. + */ + displayName: string; + + /** + * Full display name of item to be renamed. + */ + fullDisplayName: string; + + /** + * The items's kind (such as 'className' or 'parameterName' or plain 'text'). + */ + kind: string; + + /** + * Optional modifiers for the kind (such as 'public'). + */ + kindModifiers: string; + } + + /** + * A group of text spans, all in 'file'. + */ + export interface SpanGroup { + /** The file to which the spans apply */ + file: string; + /** The text spans in this group */ + locs: TextSpan[]; + } + + export interface RenameResponseBody { + /** + * Information about the item to be renamed. + */ + info: RenameInfo; + + /** + * An array of span groups (one per file) that refer to the item to be renamed. + */ + locs: SpanGroup[]; + } + + /** + * Rename response message. + */ + export interface RenameResponse extends Response { + body?: RenameResponseBody; + } + + export interface ExternalFile { + fileName: string; + scriptKind?: ScriptKind; + hasMixedContent?: boolean; + content?: string; + } + + export interface ExternalProject { + projectFileName: string; + rootFiles: ExternalFile[]; +<<<<<<< HEAD + options: ExternalProjectCompilerOptions; + typingOptions?: TypingOptions; + } + + /** + * For external projects, some of the project settings are sent together with + * compiler settings. + */ + export interface ExternalProjectCompilerOptions extends CompilerOptions { + compileOnSave?: boolean; +======= + options: CompilerOptions; + typingOptions?: TypingOptions; +>>>>>>> d736db3b01a5f4f4215c17845deb3ae09cf28787 + } + + export interface ProjectVersionInfo { + projectName: string; + isInferred: boolean; + version: number; + options: CompilerOptions; + } + + export interface ProjectChanges { + added: string[]; + removed: string[]; + } + + /** + * Describes set of files in the project. + * info might be omitted in case of inferred projects + * if files is set - then this is the entire set of files in the project + * if changes is set - then this is the set of changes that should be applied to existing project + * otherwise - assume that nothing is changed + */ + export interface ProjectFiles { + info?: ProjectVersionInfo; + files?: string[]; + changes?: ProjectChanges; + } + + export interface ChangedOpenFile { + fileName: string; + changes: ts.TextChange[]; + } + + /** + * Editor options + */ + export interface EditorOptions { + + /** Number of spaces for each tab. Default value is 4. */ + tabSize?: number; + + /** Number of spaces to indent during formatting. Default value is 4. */ + indentSize?: number; + + /** Number of additional spaces to indent during formatting to preserve base indentation (ex. script block indentation). Default value is 0. */ + baseIndentSize?: number; + + /** The new line character to be used. Default value is the OS line delimiter. */ + newLineCharacter?: string; + + /** Whether tabs should be converted to spaces. Default value is true. */ + convertTabsToSpaces?: boolean; + } + + /** + * Format options + */ + export interface FormatOptions extends EditorOptions { + + /** Defines space handling after a comma delimiter. Default value is true. */ + insertSpaceAfterCommaDelimiter?: boolean; + + /** Defines space handling after a semicolon in a for statement. Default value is true */ + insertSpaceAfterSemicolonInForStatements?: boolean; + + /** Defines space handling after a binary operator. Default value is true. */ + insertSpaceBeforeAndAfterBinaryOperators?: boolean; + + /** Defines space handling after keywords in control flow statement. Default value is true. */ + insertSpaceAfterKeywordsInControlFlowStatements?: boolean; + + /** Defines space handling after function keyword for anonymous functions. Default value is false. */ + insertSpaceAfterFunctionKeywordForAnonymousFunctions?: boolean; + + /** Defines space handling after opening and before closing non empty parenthesis. Default value is false. */ + insertSpaceAfterOpeningAndBeforeClosingNonemptyParenthesis?: boolean; + + /** Defines space handling after opening and before closing non empty brackets. Default value is false. */ + insertSpaceAfterOpeningAndBeforeClosingNonemptyBrackets?: boolean; + + /** Defines whether an open brace is put onto a new line for functions or not. Default value is false. */ + placeOpenBraceOnNewLineForFunctions?: boolean; + + /** Defines whether an open brace is put onto a new line for control blocks or not. Default value is false. */ + placeOpenBraceOnNewLineForControlBlocks?: boolean; + } + + /** + * Information found in a configure request. + */ + export interface ConfigureRequestArguments { + + /** + * Information about the host, for example 'Emacs 24.4' or + * 'Sublime Text version 3075' + */ + hostInfo?: string; + + /** + * If present, tab settings apply only to this file. + */ + file?: string; + + /** + * The format options to use during formatting and other code editing features. + */ + formatOptions?: FormatOptions; + + /** + * If set to true - then all loose files will land into one inferred project + */ + useOneInferredProject?: boolean; + } + + /** + * Configure request; value of command field is "configure". Specifies + * host information, such as host type, tab size, and indent size. + */ + export interface ConfigureRequest extends Request { + arguments: ConfigureRequestArguments; + } + + /** + * Response to "configure" request. This is just an acknowledgement, so + * no body field is required. + */ + export interface ConfigureResponse extends Response { + } + + /** + * Information found in an "open" request. + */ + export interface OpenRequestArgs extends FileRequestArgs { + /** + * Used when a version of the file content is known to be more up to date than the one on disk. + * Then the known content will be used upon opening instead of the disk copy + */ + fileContent?: string; + /** + * Used to specify the script kind of the file explicitly. It could be one of the following: + * "TS", "JS", "TSX", "JSX" + */ + scriptKindName?: "TS" | "JS" | "TSX" | "JSX"; + } + + /** + * Open request; value of command field is "open". Notify the + * server that the client has file open. The server will not + * monitor the filesystem for changes in this file and will assume + * that the client is updating the server (using the change and/or + * reload messages) when the file changes. Server does not currently + * send a response to an open request. + */ + export interface OpenRequest extends Request { + arguments: OpenRequestArgs; + } + + type OpenExternalProjectArgs = ExternalProject; + + export interface OpenExternalProjectRequest extends Request { + arguments: OpenExternalProjectArgs; + } + + export interface CloseExternalProjectRequestArgs { + projectFileName: string; + } + + export interface OpenExternalProjectsRequest extends Request { + arguments: OpenExternalProjectsArgs; + } + + export interface OpenExternalProjectsArgs { + projects: ExternalProject[]; + } + + export interface CloseExternalProjectRequest extends Request { + arguments: CloseExternalProjectRequestArgs; + } + + export interface SynchronizeProjectListRequest extends Request { + arguments: SynchronizeProjectListRequestArgs; + } + + export interface SynchronizeProjectListRequestArgs { + knownProjects: protocol.ProjectVersionInfo[]; + } + + export interface ApplyChangedToOpenFilesRequest extends Request { + arguments: ApplyChangedToOpenFilesRequestArgs; + } + + export interface ApplyChangedToOpenFilesRequestArgs { + openFiles?: ExternalFile[]; + changedFiles?: ChangedOpenFile[]; + closedFiles?: string[]; + } + + export interface SetCompilerOptionsForInferredProjectsArgs { + options: CompilerOptions; + } + + export interface SetCompilerOptionsForInferredProjectsRequest extends Request { + arguments: SetCompilerOptionsForInferredProjectsArgs; + } + + /** + * Exit request; value of command field is "exit". Ask the server process + * to exit. + */ + export interface ExitRequest extends Request { + } + + /** + * Close request; value of command field is "close". Notify the + * server that the client has closed a previously open file. If + * file is still referenced by open files, the server will resume + * monitoring the filesystem for changes to file. Server does not + * currently send a response to a close request. + */ + export interface CloseRequest extends FileRequest { + } + + export interface CompileOnSaveAffectedFileListRequest extends FileRequest { + } + + export interface CompileOnSaveEmitFileRequest extends FileRequest { + args: CompileOnSaveEmitFileRequestArgs; + } + + export interface CompileOnSaveEmitFileRequestArgs extends FileRequestArgs { + forced?: boolean; + } + + /** + * Quickinfo request; value of command field is + * "quickinfo". Return response giving a quick type and + * documentation string for the symbol found in file at location + * line, col. + */ + export interface QuickInfoRequest extends FileLocationRequest { + } + + /** + * Body of QuickInfoResponse. + */ + export interface QuickInfoResponseBody { + /** + * The symbol's kind (such as 'className' or 'parameterName' or plain 'text'). + */ + kind: string; + + /** + * Optional modifiers for the kind (such as 'public'). + */ + kindModifiers: string; + + /** + * Starting file location of symbol. + */ + start: Location; + + /** + * One past last character of symbol. + */ + end: Location; + + /** + * Type and kind of symbol. + */ + displayString: string; + + /** + * Documentation associated with symbol. + */ + documentation: string; + } + + /** + * Quickinfo response message. + */ + export interface QuickInfoResponse extends Response { + body?: QuickInfoResponseBody; + } + + /** + * Arguments for format messages. + */ + export interface FormatRequestArgs extends FileLocationRequestArgs { + /** + * Last line of range for which to format text in file. + */ + endLine: number; + + /** + * Character offset on last line of range for which to format text in file. + */ + endOffset: number; + + endPosition?: number; + options?: ts.FormatCodeOptions; + } + + /** + * Format request; value of command field is "format". Return + * response giving zero or more edit instructions. The edit + * instructions will be sorted in file order. Applying the edit + * instructions in reverse to file will result in correctly + * reformatted text. + */ + export interface FormatRequest extends FileLocationRequest { + arguments: FormatRequestArgs; + } + + /** + * Object found in response messages defining an editing + * instruction for a span of text in source code. The effect of + * this instruction is to replace the text starting at start and + * ending one character before end with newText. For an insertion, + * the text span is empty. For a deletion, newText is empty. + */ + export interface CodeEdit { + /** + * First character of the text span to edit. + */ + start: Location; + + /** + * One character past last character of the text span to edit. + */ + end: Location; + + /** + * Replace the span defined above with this string (may be + * the empty string). + */ + newText: string; + } + + /** + * Format and format on key response message. + */ + export interface FormatResponse extends Response { + body?: CodeEdit[]; + } + + /** + * Arguments for format on key messages. + */ + export interface FormatOnKeyRequestArgs extends FileLocationRequestArgs { + /** + * Key pressed (';', '\n', or '}'). + */ + key: string; + + options?: ts.FormatCodeOptions; + } + + /** + * Format on key request; value of command field is + * "formatonkey". Given file location and key typed (as string), + * return response giving zero or more edit instructions. The + * edit instructions will be sorted in file order. Applying the + * edit instructions in reverse to file will result in correctly + * reformatted text. + */ + export interface FormatOnKeyRequest extends FileLocationRequest { + arguments: FormatOnKeyRequestArgs; + } + + /** + * Arguments for completions messages. + */ + export interface CompletionsRequestArgs extends FileLocationRequestArgs { + /** + * Optional prefix to apply to possible completions. + */ + prefix?: string; + } + + /** + * Completions request; value of command field is "completions". + * Given a file location (file, line, col) and a prefix (which may + * be the empty string), return the possible completions that + * begin with prefix. + */ + export interface CompletionsRequest extends FileLocationRequest { + arguments: CompletionsRequestArgs; + } + + /** + * Arguments for completion details request. + */ + export interface CompletionDetailsRequestArgs extends FileLocationRequestArgs { + /** + * Names of one or more entries for which to obtain details. + */ + entryNames: string[]; + } + + /** + * Completion entry details request; value of command field is + * "completionEntryDetails". Given a file location (file, line, + * col) and an array of completion entry names return more + * detailed information for each completion entry. + */ + export interface CompletionDetailsRequest extends FileLocationRequest { + arguments: CompletionDetailsRequestArgs; + } + + /** + * Part of a symbol description. + */ + export interface SymbolDisplayPart { + /** + * Text of an item describing the symbol. + */ + text: string; + + /** + * The symbol's kind (such as 'className' or 'parameterName' or plain 'text'). + */ + kind: string; + } + + /** + * An item found in a completion response. + */ + export interface CompletionEntry { + /** + * The symbol's name. + */ + name: string; + /** + * The symbol's kind (such as 'className' or 'parameterName'). + */ + kind: string; + /** + * Optional modifiers for the kind (such as 'public'). + */ + kindModifiers: string; + /** + * A string that is used for comparing completion items so that they can be ordered. This + * is often the same as the name but may be different in certain circumstances. + */ + sortText: string; + } + + /** + * Additional completion entry details, available on demand + */ + export interface CompletionEntryDetails { + /** + * The symbol's name. + */ + name: string; + /** + * The symbol's kind (such as 'className' or 'parameterName'). + */ + kind: string; + /** + * Optional modifiers for the kind (such as 'public'). + */ + kindModifiers: string; + /** + * Display parts of the symbol (similar to quick info). + */ + displayParts: SymbolDisplayPart[]; + + /** + * Documentation strings for the symbol. + */ + documentation: SymbolDisplayPart[]; + } + + export interface CompletionsResponse extends Response { + body?: CompletionEntry[]; + } + + export interface CompletionDetailsResponse extends Response { + body?: CompletionEntryDetails[]; + } + + /** + * Signature help information for a single parameter + */ + export interface SignatureHelpParameter { + + /** + * The parameter's name + */ + name: string; + + /** + * Documentation of the parameter. + */ + documentation: SymbolDisplayPart[]; + + /** + * Display parts of the parameter. + */ + displayParts: SymbolDisplayPart[]; + + /** + * Whether the parameter is optional or not. + */ + isOptional: boolean; + } + + /** + * Represents a single signature to show in signature help. + */ + export interface SignatureHelpItem { + + /** + * Whether the signature accepts a variable number of arguments. + */ + isVariadic: boolean; + + /** + * The prefix display parts. + */ + prefixDisplayParts: SymbolDisplayPart[]; + + /** + * The suffix display parts. + */ + suffixDisplayParts: SymbolDisplayPart[]; + + /** + * The separator display parts. + */ + separatorDisplayParts: SymbolDisplayPart[]; + + /** + * The signature helps items for the parameters. + */ + parameters: SignatureHelpParameter[]; + + /** + * The signature's documentation + */ + documentation: SymbolDisplayPart[]; + } + + /** + * Signature help items found in the response of a signature help request. + */ + export interface SignatureHelpItems { + + /** + * The signature help items. + */ + items: SignatureHelpItem[]; + + /** + * The span for which signature help should appear on a signature + */ + applicableSpan: TextSpan; + + /** + * The item selected in the set of available help items. + */ + selectedItemIndex: number; + + /** + * The argument selected in the set of parameters. + */ + argumentIndex: number; + + /** + * The argument count + */ + argumentCount: number; + } + + /** + * Arguments of a signature help request. + */ + export interface SignatureHelpRequestArgs extends FileLocationRequestArgs { + } + + /** + * Signature help request; value of command field is "signatureHelp". + * Given a file location (file, line, col), return the signature + * help. + */ + export interface SignatureHelpRequest extends FileLocationRequest { + arguments: SignatureHelpRequestArgs; + } + + /** + * Response object for a SignatureHelpRequest. + */ + export interface SignatureHelpResponse extends Response { + body?: SignatureHelpItems; + } + + /** + * Synchronous request for semantic diagnostics of one file. + */ + export interface SemanticDiagnosticsSyncRequest extends FileRequest { + arguments: SemanticDiagnosticsSyncRequestArgs; + } + + export interface SemanticDiagnosticsSyncRequestArgs extends FileRequestArgs { + includeLinePosition?: boolean; + } + + /** + * Response object for synchronous sematic diagnostics request. + */ + export interface SemanticDiagnosticsSyncResponse extends Response { + body?: Diagnostic[] | DiagnosticWithLinePosition[]; + } + + /** + * Synchronous request for syntactic diagnostics of one file. + */ + export interface SyntacticDiagnosticsSyncRequest extends FileRequest { + arguments: SyntacticDiagnosticsSyncRequestArgs; + } + + export interface SyntacticDiagnosticsSyncRequestArgs extends FileRequestArgs { + includeLinePosition?: boolean; + } + + /** + * Response object for synchronous syntactic diagnostics request. + */ + export interface SyntacticDiagnosticsSyncResponse extends Response { + body?: Diagnostic[] | DiagnosticWithLinePosition[]; + } + + /** + * Arguments for GeterrForProject request. + */ + export interface GeterrForProjectRequestArgs { + /** + * the file requesting project error list + */ + file: string; + + /** + * Delay in milliseconds to wait before starting to compute + * errors for the files in the file list + */ + delay: number; + } + + /** + * GeterrForProjectRequest request; value of command field is + * "geterrForProject". It works similarly with 'Geterr', only + * it request for every file in this project. + */ + export interface GeterrForProjectRequest extends Request { + arguments: GeterrForProjectRequestArgs; + } + + /** + * Arguments for geterr messages. + */ + export interface GeterrRequestArgs { + /** + * List of file names for which to compute compiler errors. + * The files will be checked in list order. + */ + files: string[]; + + /** + * Delay in milliseconds to wait before starting to compute + * errors for the files in the file list + */ + delay: number; + } + + /** + * Geterr request; value of command field is "geterr". Wait for + * delay milliseconds and then, if during the wait no change or + * reload messages have arrived for the first file in the files + * list, get the syntactic errors for the file, field requests, + * and then get the semantic errors for the file. Repeat with a + * smaller delay for each subsequent file on the files list. Best + * practice for an editor is to send a file list containing each + * file that is currently visible, in most-recently-used order. + */ + export interface GeterrRequest extends Request { + arguments: GeterrRequestArgs; + } + + /** + * Item of diagnostic information found in a DiagnosticEvent message. + */ + export interface Diagnostic { + /** + * Starting file location at which text applies. + */ + start: Location; + + /** + * The last file location at which the text applies. + */ + end: Location; + + /** + * Text of diagnostic message. + */ + text: string; + } + + export interface DiagnosticEventBody { + /** + * The file for which diagnostic information is reported. + */ + file: string; + + /** + * An array of diagnostic information items. + */ + diagnostics: Diagnostic[]; + } + + /** + * Event message for "syntaxDiag" and "semanticDiag" event types. + * These events provide syntactic and semantic errors for a file. + */ + export interface DiagnosticEvent extends Event { + body?: DiagnosticEventBody; + } + + export interface ConfigFileDiagnosticEventBody { + /** + * The file which trigged the searching and error-checking of the config file + */ + triggerFile: string; + + /** + * The name of the found config file. + */ + configFile: string; + + /** + * An arry of diagnostic information items for the found config file. + */ + diagnostics: Diagnostic[]; + } + + /** + * Event message for "configFileDiag" event type. + * This event provides errors for a found config file. + */ + export interface ConfigFileDiagnosticEvent extends Event { + body?: ConfigFileDiagnosticEventBody; + event: "configFileDiag"; + } + + /** + * Arguments for reload request. + */ + export interface ReloadRequestArgs extends FileRequestArgs { + /** + * Name of temporary file from which to reload file + * contents. May be same as file. + */ + tmpfile: string; + } + + /** + * Reload request message; value of command field is "reload". + * Reload contents of file with name given by the 'file' argument + * from temporary file with name given by the 'tmpfile' argument. + * The two names can be identical. + */ + export interface ReloadRequest extends FileRequest { + arguments: ReloadRequestArgs; + } + + /** + * Response to "reload" request. This is just an acknowledgement, so + * no body field is required. + */ + export interface ReloadResponse extends Response { + } + + /** + * Arguments for saveto request. + */ + export interface SavetoRequestArgs extends FileRequestArgs { + /** + * Name of temporary file into which to save server's view of + * file contents. + */ + tmpfile: string; + } + + /** + * Saveto request message; value of command field is "saveto". + * For debugging purposes, save to a temporaryfile (named by + * argument 'tmpfile') the contents of file named by argument + * 'file'. The server does not currently send a response to a + * "saveto" request. + */ + export interface SavetoRequest extends FileRequest { + arguments: SavetoRequestArgs; + } + + /** + * Arguments for navto request message. + */ + export interface NavtoRequestArgs extends FileRequestArgs { + /** + * Search term to navigate to from current location; term can + * be '.*' or an identifier prefix. + */ + searchValue: string; + /** + * Optional limit on the number of items to return. + */ + maxResultCount?: number; + + projectFileName?: string; + } + + /** + * Navto request message; value of command field is "navto". + * Return list of objects giving file locations and symbols that + * match the search term given in argument 'searchTerm'. The + * context for the search is given by the named file. + */ + export interface NavtoRequest extends FileRequest { + arguments: NavtoRequestArgs; + } + + /** + * An item found in a navto response. + */ + export interface NavtoItem { + /** + * The symbol's name. + */ + name: string; + + /** + * The symbol's kind (such as 'className' or 'parameterName'). + */ + kind: string; + + /** + * exact, substring, or prefix. + */ + matchKind?: string; + + /** + * If this was a case sensitive or insensitive match. + */ + isCaseSensitive?: boolean; + + /** + * Optional modifiers for the kind (such as 'public'). + */ + kindModifiers?: string; + + /** + * The file in which the symbol is found. + */ + file: string; + + /** + * The location within file at which the symbol is found. + */ + start: Location; + + /** + * One past the last character of the symbol. + */ + end: Location; + + /** + * Name of symbol's container symbol (if any); for example, + * the class name if symbol is a class member. + */ + containerName?: string; + + /** + * Kind of symbol's container symbol (if any). + */ + containerKind?: string; + } + + /** + * Navto response message. Body is an array of navto items. Each + * item gives a symbol that matched the search term. + */ + export interface NavtoResponse extends Response { + body?: NavtoItem[]; + } + + /** + * Arguments for change request message. + */ + export interface ChangeRequestArgs extends FormatRequestArgs { + /** + * Optional string to insert at location (file, line, offset). + */ + insertString?: string; + } + + /** + * Change request message; value of command field is "change". + * Update the server's view of the file named by argument 'file'. + * Server does not currently send a response to a change request. + */ + export interface ChangeRequest extends FileLocationRequest { + arguments: ChangeRequestArgs; + } + + /** + * Response to "brace" request. + */ + export interface BraceResponse extends Response { + body?: TextSpan[]; + } + + /** + * Brace matching request; value of command field is "brace". + * Return response giving the file locations of matching braces + * found in file at location line, offset. + */ + export interface BraceRequest extends FileLocationRequest { + } + + /** + * NavBar items request; value of command field is "navbar". + * Return response giving the list of navigation bar entries + * extracted from the requested file. + */ + export interface NavBarRequest extends FileRequest { + } + + export interface NavigationBarItem { + /** + * The item's display text. + */ + text: string; + + /** + * The symbol's kind (such as 'className' or 'parameterName'). + */ + kind: string; + + /** + * Optional modifiers for the kind (such as 'public'). + */ + kindModifiers?: string; + + /** + * The definition locations of the item. + */ + spans: TextSpan[]; + + /** + * Optional children. + */ + childItems?: NavigationBarItem[]; + + /** + * Number of levels deep this item should appear. + */ + indent: number; + } + + export interface NavBarResponse extends Response { + body?: NavigationBarItem[]; + } +} diff --git a/src/server/session.ts b/src/server/session.ts index 5ae7307737..b83ca2cfc4 100644 --- a/src/server/session.ts +++ b/src/server/session.ts @@ -1,4 +1,4 @@ -/// +/// /// /// /// @@ -79,6 +79,8 @@ namespace ts.server { export const Completions = "completions"; export const CompletionsFull = "completions-full"; export const CompletionDetails = "completionEntryDetails"; + export const CompileOnSaveAffectedFileList = "compileOnSaveAffectedFileList"; + export const CompileOnSaveEmitFile = "compileOnSaveEmitFile"; export const Configure = "configure"; export const Definition = "definition"; export const DefinitionFull = "definition-full"; @@ -939,6 +941,26 @@ namespace ts.server { }, []); } + private getCompileOnSaveAffectedFileList(args: protocol.FileRequestArgs) { + const info = this.projectService.getScriptInfo(args.file); + let result: string[] = []; + for (const project of info.containingProjects) { + if (project.compileOnSaveEnabled) { + result = concatenate(result, project.getCompileOnSaveAffectedFileList(info)); + } + } + return result; + } + + private emitFile(args: protocol.CompileOnSaveEmitFileRequestArgs) { + const { file, project } = this.getFileAndProject(args); + if (!project) { + Errors.ThrowNoProject(); + } + const scriptInfo = project.getScriptInfo(file); + return project.builder.emitFile(scriptInfo, (path, data, writeByteOrderMark) => this.host.writeFile(path, data, writeByteOrderMark)); + } + private getSignatureHelpItems(args: protocol.SignatureHelpRequestArgs, simplifiedResult: boolean): protocol.SignatureHelpItems | SignatureHelpItems { const { file, project } = this.getFileAndProject(args); const scriptInfo = project.getScriptInfoForNormalizedPath(file); @@ -1340,6 +1362,12 @@ namespace ts.server { [CommandNames.CompletionDetails]: (request: protocol.CompletionDetailsRequest) => { return this.requiredResponse(this.getCompletionEntryDetails(request.arguments)); }, + [CommandNames.CompileOnSaveAffectedFileList]: (request: protocol.CompileOnSaveAffectedFileListRequest) => { + return this.requiredResponse(this.getCompileOnSaveAffectedFileList(request.arguments)); + }, + [CommandNames.CompileOnSaveEmitFile]: (request: protocol.CompileOnSaveEmitFileRequest) => { + return this.requiredResponse(this.emitFile(request.arguments)); + }, [CommandNames.SignatureHelp]: (request: protocol.SignatureHelpRequest) => { return this.requiredResponse(this.getSignatureHelpItems(request.arguments, /*simplifiedResult*/ true)); }, diff --git a/src/server/utilities.ts b/src/server/utilities.ts index 9c2c4dfb22..d483acdf31 100644 --- a/src/server/utilities.ts +++ b/src/server/utilities.ts @@ -1,4 +1,4 @@ -/// +/// namespace ts.server { export enum LogLevel { @@ -220,6 +220,7 @@ namespace ts.server { wildcardDirectories?: Map; compilerOptions?: CompilerOptions; typingOptions?: TypingOptions; + compileOnSave?: boolean; } export function isInferredProjectName(name: string) { diff --git a/src/services/services.ts b/src/services/services.ts index 75d2e79a38..6d81cc4dc8 100644 --- a/src/services/services.ts +++ b/src/services/services.ts @@ -1253,7 +1253,7 @@ namespace ts { isValidBraceCompletionAtPosition(fileName: string, position: number, openingBrace: number): boolean; - getEmitOutput(fileName: string): EmitOutput; + getEmitOutput(fileName: string, emitOnlyDtsFiles?: boolean): EmitOutput; getProgram(): Program; @@ -7069,7 +7069,7 @@ namespace ts { return ts.NavigateTo.getNavigateToItems(program, checker, cancellationToken, searchValue, maxResultCount); } - function getEmitOutput(fileName: string): EmitOutput { + function getEmitOutput(fileName: string, emitDeclarationsOnly?: boolean): EmitOutput { synchronizeHostData(); const sourceFile = getValidSourceFile(fileName); @@ -7083,7 +7083,7 @@ namespace ts { }); } - const emitOutput = program.emit(sourceFile, writeFile, cancellationToken); + const emitOutput = program.emit(sourceFile, writeFile, cancellationToken, emitDeclarationsOnly); return { outputFiles,