diff --git a/src/server/editorServices.ts b/src/server/editorServices.ts index dd7fb3bbbcc..98cce0c3db4 100644 --- a/src/server/editorServices.ts +++ b/src/server/editorServices.ts @@ -108,7 +108,6 @@ namespace ts.server { currentPath = parentPath; parentPath = getDirectoryPath(parentPath); } - } } @@ -139,21 +138,21 @@ namespace ts.server { /** * list of open files */ - openFiles: ScriptInfo[] = []; + readonly openFiles: ScriptInfo[] = []; private readonly directoryWatchers: DirectoryWatchers; + private readonly throttledOperations: ThrottledOperations; private readonly hostConfiguration: HostConfiguration; - private timerForDetectingProjectFileListChanges: Map = {}; - constructor(public readonly host: ServerHost, public readonly logger: Logger, public readonly cancellationToken: HostCancellationToken, - private readonly useOneInferredProject: boolean, + private readonly useSingleInferredProject: boolean, private readonly eventHandler?: ProjectServiceEventHandler) { this.directoryWatchers = new DirectoryWatchers(this); + this.throttledOperations = new ThrottledOperations(host); // ts.disableIncrementalParsing = true; this.hostConfiguration = { @@ -243,14 +242,10 @@ namespace ts.server { } this.log(`Detected source file changes: ${fileName}`); - const timeoutId = this.timerForDetectingProjectFileListChanges[project.configFileName]; - if (timeoutId) { - this.host.clearTimeout(timeoutId); - } - this.timerForDetectingProjectFileListChanges[project.configFileName] = this.host.setTimeout( - () => this.handleChangeInSourceFileForConfiguredProject(project), - 250 - ); + this.throttledOperations.schedule( + project.configFileName, + 250, + () => this.handleChangeInSourceFileForConfiguredProject(project)); } private handleChangeInSourceFileForConfiguredProject(project: ConfiguredProject) { @@ -357,7 +352,7 @@ namespace ts.server { // 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.useOneInferredProject) { + if (!this.useSingleInferredProject) { // if useOneInferredProject is not set then try to fixup ownership of open files for (const f of this.openFiles) { @@ -755,7 +750,7 @@ namespace ts.server { } createInferredProjectWithRootFileIfNecessary(root: ScriptInfo) { - const useExistingProject = this.useOneInferredProject && this.inferredProjects.length; + const useExistingProject = this.useSingleInferredProject && this.inferredProjects.length; const project = useExistingProject ? this.inferredProjects[0] : new InferredProject(this, this.documentRegistry, /*languageServiceEnabled*/ true); @@ -1004,42 +999,42 @@ namespace ts.server { // this.openFilesReferenced.push(rootFile); // } // } - // if (rootFile.containingProjects.some(p => p.projectKind !== ProjectKind.Inferred)) { - // // file was included in non-inferred project - drop old inferred project + // if (rootFile.containingProjects.some(p => p.projectKind !== ProjectKind.Inferred)) { + // // file was included in non-inferred project - drop old inferred project - // } - // else { - // openFileRoots.push(rootFile); - // } - // let inferredProjectsToRemove: Project[]; - // for (const p of rootFile.containingProjects) { - // if (p.projectKind !== ProjectKind.Inferred) { - // // file was included in non-inferred project - drop old inferred project - // } - // } + // } + // else { + // openFileRoots.push(rootFile); + // } + // let inferredProjectsToRemove: Project[]; + // for (const p of rootFile.containingProjects) { + // if (p.projectKind !== ProjectKind.Inferred) { + // // file was included in non-inferred project - drop old inferred project + // } + // } - // const rootedProject = rootFile.defaultProject; - // const referencingProjects = this.findReferencingProjects(rootFile, rootedProject); + // const rootedProject = rootFile.defaultProject; + // const referencingProjects = this.findReferencingProjects(rootFile, rootedProject); - // if (rootFile.defaultProject && rootFile.defaultProject.projectKind !== ProjectKind.Inferred) { - // // If the root file has already been added into a configured project, - // // meaning the original inferred project is gone already. - // if (rootedProject.projectKind === ProjectKind.Inferred) { - // this.removeProject(rootedProject); - // } - // this.openFileRootsConfigured.push(rootFile); - // } - // else { - // if (referencingProjects.length === 0) { - // rootFile.defaultProject = rootedProject; - // openFileRoots.push(rootFile); - // } - // else { - // // remove project from inferred projects list because root captured - // this.removeProject(rootedProject); - // this.openFilesReferenced.push(rootFile); - // } - // } + // if (rootFile.defaultProject && rootFile.defaultProject.projectKind !== ProjectKind.Inferred) { + // // If the root file has already been added into a configured project, + // // meaning the original inferred project is gone already. + // if (rootedProject.projectKind === ProjectKind.Inferred) { + // this.removeProject(rootedProject); + // } + // this.openFileRootsConfigured.push(rootFile); + // } + // else { + // if (referencingProjects.length === 0) { + // rootFile.defaultProject = rootedProject; + // openFileRoots.push(rootFile); + // } + // else { + // // remove project from inferred projects list because root captured + // this.removeProject(rootedProject); + // this.openFilesReferenced.push(rootFile); + // } + // } // } // this.openFileRoots = openFileRoots; diff --git a/src/server/server.ts b/src/server/server.ts index 79b40836f54..17d7e15c19d 100644 --- a/src/server/server.ts +++ b/src/server/server.ts @@ -91,8 +91,8 @@ namespace ts.server { } class IOSession extends Session { - constructor(host: ServerHost, cancellationToken: HostCancellationToken, useOneInferredProject: boolean, logger: ts.server.Logger) { - super(host, cancellationToken, useOneInferredProject, Buffer.byteLength, process.hrtime, logger); + constructor(host: ServerHost, cancellationToken: HostCancellationToken, useSingleInferredProject: boolean, logger: ts.server.Logger) { + super(host, cancellationToken, useSingleInferredProject, Buffer.byteLength, process.hrtime, logger); } exit() { @@ -304,8 +304,8 @@ namespace ts.server { }; }; - const useOneInferredProject = sys.args.some(arg => arg === "--useOneInferredProject"); - const ioSession = new IOSession(sys, cancellationToken, useOneInferredProject, logger); + const useSingleInferredProject = sys.args.some(arg => arg === "--useSingleInferredProject"); + const ioSession = new IOSession(sys, cancellationToken, 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 c034e501403..f3d439580f3 100644 --- a/src/server/session.ts +++ b/src/server/session.ts @@ -179,12 +179,12 @@ namespace ts.server { constructor( private host: ServerHost, cancellationToken: HostCancellationToken, - useOneInferredProject: boolean, + useSingleInferredProject: boolean, private byteLength: (buf: string, encoding?: string) => number, private hrtime: (start?: number[]) => number[], private logger: Logger) { this.projectService = - new ProjectService(host, logger, cancellationToken, useOneInferredProject, (eventName, project, fileName) => { + new ProjectService(host, logger, cancellationToken, useSingleInferredProject, (eventName, project, fileName) => { this.handleEvent(eventName, project, fileName); }); } diff --git a/src/server/utilities.ts b/src/server/utilities.ts index 0dcc989342a..34953f9a15f 100644 --- a/src/server/utilities.ts +++ b/src/server/utilities.ts @@ -1,7 +1,5 @@ /// -/* tslint:disable:no-null-keyword */ - namespace ts.server { export interface Logger { close(): void; @@ -74,14 +72,16 @@ namespace ts.server { } export interface NormalizedPathMap { - get (path: NormalizedPath): T; - set (path: NormalizedPath, value: T): void; + get(path: NormalizedPath): T; + set(path: NormalizedPath, value: T): void; contains(path: NormalizedPath): boolean; remove(path: NormalizedPath): void; } export function createNormalizedPathMap(): NormalizedPathMap { +/* tslint:disable:no-null-keyword */ const map: Map = Object.create(null); +/* tslint:enable:no-null-keyword */ return { get(path) { return map[path]; @@ -97,48 +97,49 @@ namespace ts.server { } }; } - function throwLanguageServiceIsDisabledError() {; + function throwLanguageServiceIsDisabledError() { + ; throw new Error("LanguageService is disabled"); } export const nullLanguageService: LanguageService = { - cleanupSemanticCache: (): any => throwLanguageServiceIsDisabledError(), - getSyntacticDiagnostics: (): any => throwLanguageServiceIsDisabledError(), - getSemanticDiagnostics: (): any => throwLanguageServiceIsDisabledError(), - getCompilerOptionsDiagnostics: (): any => throwLanguageServiceIsDisabledError(), - getSyntacticClassifications: (): any => throwLanguageServiceIsDisabledError(), + cleanupSemanticCache: (): any => throwLanguageServiceIsDisabledError(), + getSyntacticDiagnostics: (): any => throwLanguageServiceIsDisabledError(), + getSemanticDiagnostics: (): any => throwLanguageServiceIsDisabledError(), + getCompilerOptionsDiagnostics: (): any => throwLanguageServiceIsDisabledError(), + getSyntacticClassifications: (): any => throwLanguageServiceIsDisabledError(), getEncodedSyntacticClassifications: (): any => throwLanguageServiceIsDisabledError(), - getSemanticClassifications: (): any => throwLanguageServiceIsDisabledError(), - getEncodedSemanticClassifications: (): any => throwLanguageServiceIsDisabledError(), - getCompletionsAtPosition: (): any => throwLanguageServiceIsDisabledError(), - findReferences: (): any => throwLanguageServiceIsDisabledError(), - getCompletionEntryDetails: (): any => throwLanguageServiceIsDisabledError(), - getQuickInfoAtPosition: (): any => throwLanguageServiceIsDisabledError(), - findRenameLocations: (): any => throwLanguageServiceIsDisabledError(), - getNameOrDottedNameSpan: (): any => throwLanguageServiceIsDisabledError(), - getBreakpointStatementAtPosition: (): any => throwLanguageServiceIsDisabledError(), - getBraceMatchingAtPosition: (): any => throwLanguageServiceIsDisabledError(), - getSignatureHelpItems: (): any => throwLanguageServiceIsDisabledError(), - getDefinitionAtPosition: (): any => throwLanguageServiceIsDisabledError(), - getRenameInfo: (): any => throwLanguageServiceIsDisabledError(), - getTypeDefinitionAtPosition: (): any => throwLanguageServiceIsDisabledError(), - getReferencesAtPosition: (): any => throwLanguageServiceIsDisabledError(), - getDocumentHighlights: (): any => throwLanguageServiceIsDisabledError(), - getOccurrencesAtPosition: (): any => throwLanguageServiceIsDisabledError(), - getNavigateToItems: (): any => throwLanguageServiceIsDisabledError(), - getNavigationBarItems: (): any => throwLanguageServiceIsDisabledError(), - getOutliningSpans: (): any => throwLanguageServiceIsDisabledError(), - getTodoComments: (): any => throwLanguageServiceIsDisabledError(), - getIndentationAtPosition: (): any => throwLanguageServiceIsDisabledError(), - getFormattingEditsForRange: (): any => throwLanguageServiceIsDisabledError(), - getFormattingEditsForDocument: (): any => throwLanguageServiceIsDisabledError(), - getFormattingEditsAfterKeystroke: (): any => throwLanguageServiceIsDisabledError(), - getDocCommentTemplateAtPosition: (): any => throwLanguageServiceIsDisabledError(), - isValidBraceCompletionAtPosition: (): any => throwLanguageServiceIsDisabledError(), - getEmitOutput: (): any => throwLanguageServiceIsDisabledError(), - getProgram: (): any => throwLanguageServiceIsDisabledError(), - getNonBoundSourceFile: (): any => throwLanguageServiceIsDisabledError(), - dispose: (): any => throwLanguageServiceIsDisabledError(), + getSemanticClassifications: (): any => throwLanguageServiceIsDisabledError(), + getEncodedSemanticClassifications: (): any => throwLanguageServiceIsDisabledError(), + getCompletionsAtPosition: (): any => throwLanguageServiceIsDisabledError(), + findReferences: (): any => throwLanguageServiceIsDisabledError(), + getCompletionEntryDetails: (): any => throwLanguageServiceIsDisabledError(), + getQuickInfoAtPosition: (): any => throwLanguageServiceIsDisabledError(), + findRenameLocations: (): any => throwLanguageServiceIsDisabledError(), + getNameOrDottedNameSpan: (): any => throwLanguageServiceIsDisabledError(), + getBreakpointStatementAtPosition: (): any => throwLanguageServiceIsDisabledError(), + getBraceMatchingAtPosition: (): any => throwLanguageServiceIsDisabledError(), + getSignatureHelpItems: (): any => throwLanguageServiceIsDisabledError(), + getDefinitionAtPosition: (): any => throwLanguageServiceIsDisabledError(), + getRenameInfo: (): any => throwLanguageServiceIsDisabledError(), + getTypeDefinitionAtPosition: (): any => throwLanguageServiceIsDisabledError(), + getReferencesAtPosition: (): any => throwLanguageServiceIsDisabledError(), + getDocumentHighlights: (): any => throwLanguageServiceIsDisabledError(), + getOccurrencesAtPosition: (): any => throwLanguageServiceIsDisabledError(), + getNavigateToItems: (): any => throwLanguageServiceIsDisabledError(), + getNavigationBarItems: (): any => throwLanguageServiceIsDisabledError(), + getOutliningSpans: (): any => throwLanguageServiceIsDisabledError(), + getTodoComments: (): any => throwLanguageServiceIsDisabledError(), + getIndentationAtPosition: (): any => throwLanguageServiceIsDisabledError(), + getFormattingEditsForRange: (): any => throwLanguageServiceIsDisabledError(), + getFormattingEditsForDocument: (): any => throwLanguageServiceIsDisabledError(), + getFormattingEditsAfterKeystroke: (): any => throwLanguageServiceIsDisabledError(), + getDocCommentTemplateAtPosition: (): any => throwLanguageServiceIsDisabledError(), + isValidBraceCompletionAtPosition: (): any => throwLanguageServiceIsDisabledError(), + getEmitOutput: (): any => throwLanguageServiceIsDisabledError(), + getProgram: (): any => throwLanguageServiceIsDisabledError(), + getNonBoundSourceFile: (): any => throwLanguageServiceIsDisabledError(), + dispose: (): any => throwLanguageServiceIsDisabledError(), }; export interface ServerLanguageServiceHost { @@ -172,4 +173,24 @@ namespace ts.server { export function makeInferredProjectName(counter: number) { return `/dev/null/inferredProject${counter}*`; } + + export class ThrottledOperations { + private pendingTimeouts: Map = {}; + constructor(private readonly host: ServerHost) { + } + + public schedule(operationId: string, delay: number, cb: () => void) { + if (hasProperty(this.pendingTimeouts, operationId)) { + // another operation was already scheduled for this id - cancel it + this.host.clearTimeout(this.pendingTimeouts[operationId]); + } + // schedule new operation, pass arguments + this.pendingTimeouts[operationId] = this.host.setTimeout(ThrottledOperations.run, delay, this, operationId, cb); + } + + private static run(self: ThrottledOperations, operationId: string, cb: () => void) { + delete self.pendingTimeouts[operationId]; + cb(); + } + } } \ No newline at end of file diff --git a/tests/cases/unittests/tsserverProjectSystem.ts b/tests/cases/unittests/tsserverProjectSystem.ts index 0abd376ee5d..fc6ef16e398 100644 --- a/tests/cases/unittests/tsserverProjectSystem.ts +++ b/tests/cases/unittests/tsserverProjectSystem.ts @@ -145,7 +145,10 @@ namespace ts { private fs: ts.FileMap; private getCanonicalFileName: (s: string) => string; private toPath: (f: string) => Path; - private callbackQueue: TimeOutCallback[] = []; + + private nextTimeoutId = 0; + private callbacks: { [n: number]: TimeOutCallback } = {}; + readonly watchedDirectories: Map<{ cb: DirectoryWatcherCallback, recursive: boolean }[]> = {}; readonly watchedFiles: Map = {}; @@ -283,25 +286,28 @@ namespace ts { } // TOOD: record and invoke callbacks to simulate timer events - readonly setTimeout = (callback: TimeOutCallback, time: number) => { - this.callbackQueue.push(callback); - return this.callbackQueue.length - 1; + readonly setTimeout = (callback: TimeOutCallback, time: number, ...args: any[]) => { + const timeoutId = this.nextTimeoutId; + this.nextTimeoutId++; + this.callbacks[timeoutId] = callback.bind(undefined, ...args); + return timeoutId; }; readonly clearTimeout = (timeoutId: any): void => { if (typeof timeoutId === "number") { - this.callbackQueue.splice(timeoutId, 1); + delete this.callbacks[timeoutId]; } }; checkTimeoutQueueLength(expected: number) { - assert.equal(this.callbackQueue.length, expected, `expected ${expected} timeout callbacks queued but found ${this.callbackQueue.length}.`); + const callbacksCount = sizeOfMap(this.callbacks); + assert.equal(callbacksCount, expected, `expected ${expected} timeout callbacks queued but found ${callbacksCount}.`); } runQueuedTimeoutCallbacks() { - for (const callback of this.callbackQueue) { - callback(); + for (const id in this.callbacks) { + this.callbacks[id](); } - this.callbackQueue = []; + this.callbacks = []; } readonly readFile = (s: string) => (this.fs.get(this.toPath(s))).content;