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