diff --git a/src/harness/harnessLanguageService.ts b/src/harness/harnessLanguageService.ts index 86630d9083d..527705d8b4f 100644 --- a/src/harness/harnessLanguageService.ts +++ b/src/harness/harnessLanguageService.ts @@ -646,7 +646,7 @@ namespace Harness.LanguageService { return true; } - isVerbose() { + hasLevel() { return false; } diff --git a/src/server/editorServices.ts b/src/server/editorServices.ts index 3a2d27f4647..8139decfd05 100644 --- a/src/server/editorServices.ts +++ b/src/server/editorServices.ts @@ -145,6 +145,8 @@ namespace ts.server { private readonly hostConfiguration: HostConfiguration; + private changedFiles: ScriptInfo[]; + constructor(public readonly host: ServerHost, public readonly logger: Logger, public readonly cancellationToken: HostCancellationToken, @@ -163,6 +165,14 @@ namespace ts.server { this.documentRegistry = createDocumentRegistry(host.useCaseSensitiveFileNames, host.getCurrentDirectory()); } + getChangedFiles_TestOnly() { + return this.changedFiles; + } + + ensureInferredProjectsUpToDate_TestOnly() { + this.ensureInferredProjectsUpToDate(); + } + stopWatchingDirectory(directory: string) { this.directoryWatchers.stopWatchingDirectory(directory); } @@ -187,7 +197,21 @@ namespace ts.server { } 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 findContainingConfiguredProject(info: ScriptInfo): ConfiguredProject { @@ -532,7 +556,7 @@ namespace ts.server { } private printProjects() { - if (!this.logger.isVerbose()) { + if (!this.logger.hasLevel(LogLevel.verbose)) { return; } @@ -970,11 +994,13 @@ namespace ts.server { } applyChangesInOpenFiles(openFiles: protocol.NewOpenFile[], 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); - this.openClientFileWithNormalizedPath(toNormalizedPath(file.fileName), file.content); + const normalizedPath = scriptInfo ? scriptInfo.fileName : toNormalizedPath(file.fileName); + this.openClientFileWithNormalizedPath(normalizedPath, file.content); } } @@ -987,6 +1013,14 @@ namespace ts.server { 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); + } + } } } @@ -996,7 +1030,9 @@ namespace ts.server { } } - if (openFiles || changedFiles || closedFiles) { + // 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(); } } diff --git a/src/server/server.ts b/src/server/server.ts index 1a101f9d030..953302821bc 100644 --- a/src/server/server.ts +++ b/src/server/server.ts @@ -30,7 +30,7 @@ namespace ts.server { constructor(private readonly logFilename: string, private readonly traceToConsole: boolean, - private readonly level: string) { + private readonly level: LogLevel) { } static padStringRight(str: string, padding: string) { @@ -66,11 +66,10 @@ namespace ts.server { return !!this.logFilename || this.traceToConsole; } - isVerbose() { - return this.loggingEnabled() && (this.level == "verbose"); + hasLevel(level: LogLevel) { + return this.loggingEnabled() && this.level >= level; } - msg(s: string, type: Msg.Types = Msg.Err) { if (this.fd < 0) { if (this.logFilename) { @@ -88,8 +87,8 @@ namespace ts.server { this.seq++; this.firstInGroup = true; } - const buf = new Buffer(s); if (this.fd >= 0) { + const buf = new Buffer(s); fs.writeSync(this.fd, buf, 0, buf.length, null); } if (this.traceToConsole) { @@ -124,12 +123,13 @@ namespace ts.server { interface LogOptions { file?: string; - detailLevel?: string; + detailLevel?: LogLevel; traceToConsole?: boolean; + logToFile?: boolean; } function parseLoggingEnvironmentString(logEnvStr: string): LogOptions { - const logEnv: LogOptions = {}; + const logEnv: LogOptions = { logToFile: true }; const args = logEnvStr.split(" "); for (let i = 0, len = args.length; i < (len - 1); i += 2) { const option = args[i]; @@ -140,11 +140,15 @@ namespace ts.server { logEnv.file = value; break; case "-level": - logEnv.detailLevel = value; + const level: LogLevel = (LogLevel)[value]; + logEnv.detailLevel = typeof level === "number" ? level : LogLevel.normal; break; case "-traceToConsole": logEnv.traceToConsole = value.toLowerCase() === "true"; break; + case "-logToFile": + logEnv.logToFile = value.toLowerCase() === "true"; + break; } } } @@ -154,16 +158,18 @@ namespace ts.server { // TSS_LOG "{ level: "normal | verbose | terse", file?: string}" function createLoggerFromEnv() { let fileName: string = undefined; - let detailLevel = "normal"; + let detailLevel = LogLevel.normal; let traceToConsole = false; const logEnvStr = process.env["TSS_LOG"]; if (logEnvStr) { const logEnv = parseLoggingEnvironmentString(logEnvStr); - if (logEnv.file) { - fileName = logEnv.file; - } - else { - fileName = __dirname + "/.log" + process.pid.toString(); + if (logEnv.logToFile) { + if (logEnv.file) { + fileName = logEnv.file; + } + else { + fileName = __dirname + "/.log" + process.pid.toString(); + } } if (logEnv.detailLevel) { detailLevel = logEnv.detailLevel; diff --git a/src/server/session.ts b/src/server/session.ts index e0a727403eb..96c6a23837d 100644 --- a/src/server/session.ts +++ b/src/server/session.ts @@ -191,8 +191,10 @@ namespace ts.server { } public send(msg: protocol.Message, canCompressResponse: boolean) { + const verboseLogging = this.logger.hasLevel(LogLevel.verbose); + const json = JSON.stringify(msg); - if (this.logger.isVerbose()) { + if (verboseLogging) { this.logger.info(msg.type + ": " + json); } @@ -201,9 +203,9 @@ namespace ts.server { this.host.write(`Content-Length: ${1 + this.byteLength(json, "utf8")}\r\n\r\n${json}${this.host.newLine}`); } else { - const start = this.logger.isVerbose() && this.hrtime(); + const start = verboseLogging && this.hrtime(); const compressed = this.compress(json); - if (this.logger.isVerbose()) { + if (verboseLogging) { const elapsed = this.hrtime(start); this.logger.info(`compressed message ${json.length} to ${compressed.length} in ${hrTimeToMilliseconds(elapsed)} ms using ${compressed.compressionKind}`); } @@ -1460,24 +1462,28 @@ namespace ts.server { public onMessage(message: string) { let start: number[]; - if (this.logger.isVerbose()) { - this.logger.info("request: " + message); + if (this.logger.hasLevel(LogLevel.requestTime)) { start = this.hrtime(); + if (this.logger.hasLevel(LogLevel.verbose)) { + this.logger.info(`request: ${message}`); + } } + let request: protocol.Request; try { request = JSON.parse(message); const {response, responseRequired} = this.executeCommand(request); - if (this.logger.isVerbose()) { - const elapsed = this.hrtime(start); - const elapsedMs = hrTimeToMilliseconds(elapsed); - let leader = "Elapsed time (in milliseconds)"; - if (!responseRequired) { - leader = "Async elapsed time (in milliseconds)"; + if (this.logger.hasLevel(LogLevel.requestTime)) { + const elapsedTime = hrTimeToMilliseconds(this.hrtime(start)).toFixed(4); + if (responseRequired) { + this.logger.perftrc(`${request.seq}::${request.command}: elapsed time (in milliseconds) ${elapsedTime}`); + } + else { + this.logger.perftrc(`${request.seq}::${request.command}: async elapsed time (in milliseconds) ${elapsedTime}`); } - this.logger.msg(leader + ": " + elapsedMs.toFixed(4).toString(), "Perf"); } + if (response) { this.output(response, request.command, request.canCompressResponse, request.seq); } diff --git a/src/server/utilities.ts b/src/server/utilities.ts index cf38324343c..ccbf380cfcb 100644 --- a/src/server/utilities.ts +++ b/src/server/utilities.ts @@ -1,9 +1,16 @@ /// namespace ts.server { + export enum LogLevel { + terse, + normal, + requestTime, + verbose + } + export interface Logger { close(): void; - isVerbose(): boolean; + hasLevel(level: LogLevel): boolean; loggingEnabled(): boolean; perftrc(s: string): void; info(s: string): void; diff --git a/tests/cases/unittests/cachingInServerLSHost.ts b/tests/cases/unittests/cachingInServerLSHost.ts index cbf8ff38b1e..cd5c2a37540 100644 --- a/tests/cases/unittests/cachingInServerLSHost.ts +++ b/tests/cases/unittests/cachingInServerLSHost.ts @@ -75,7 +75,7 @@ namespace ts { function createProject(rootFile: string, serverHost: server.ServerHost): { project: server.Project, rootScriptInfo: server.ScriptInfo } { const logger: server.Logger = { close() { }, - isVerbose: () => false, + hasLevel: () => false, loggingEnabled: () => false, perftrc: (s: string) => { }, info: (s: string) => { }, diff --git a/tests/cases/unittests/session.ts b/tests/cases/unittests/session.ts index 3aebd28df9a..123762c9687 100644 --- a/tests/cases/unittests/session.ts +++ b/tests/cases/unittests/session.ts @@ -29,7 +29,7 @@ namespace ts.server { const nullCancellationToken: HostCancellationToken = { isCancellationRequested: () => false }; const mockLogger: Logger = { close(): void {}, - isVerbose(): boolean { return false; }, + hasLevel(): boolean { return false; }, loggingEnabled(): boolean { return false; }, perftrc(s: string): void {}, info(s: string): void {}, diff --git a/tests/cases/unittests/tsserverProjectSystem.ts b/tests/cases/unittests/tsserverProjectSystem.ts index b5a5f3aef54..d906558985f 100644 --- a/tests/cases/unittests/tsserverProjectSystem.ts +++ b/tests/cases/unittests/tsserverProjectSystem.ts @@ -7,7 +7,7 @@ namespace ts { const nullLogger: server.Logger = { close: () => void 0, - isVerbose: () => void 0, + hasLevel: () => void 0, loggingEnabled: () => false, perftrc: () => void 0, info: () => void 0, @@ -1254,5 +1254,34 @@ namespace ts { checkProjectActualFiles(projectService.inferredProjects[0], [file1.path]); checkProjectActualFiles(projectService.inferredProjects[1], [file2.path]); }); + + it("project structure update is deferred if files are not added\removed", () => { + const file1 = { + path: "/a/b/f1.ts", + content: `import {x} from "./f2"` + }; + const file2 = { + path: "/a/b/f2.ts", + content: "export let x = 1" + }; + const host = createServerHost([file1, file2]); + const projectService = new server.ProjectService(host, nullLogger, nullCancellationToken, /*useSingleInferredProject*/ false); + + projectService.openClientFile(file1.path); + projectService.openClientFile(file2.path); + + checkNumberOfProjects(projectService, { inferredProjects: 1 }); + projectService.applyChangesInOpenFiles( + /*openFiles*/ undefined, + /*changedFiles*/ [{ fileName: file1.path, changes: [ { span: createTextSpan(0, file1.path.length), newText: "let y = 1" } ] }], + /*closedFiles*/ undefined); + + checkNumberOfProjects(projectService, { inferredProjects: 1 }); + const changedFiles = projectService.getChangedFiles_TestOnly(); + assert(changedFiles && changedFiles.length === 1, `expected 1 changed file, got ${JSON.stringify(changedFiles && changedFiles.length || 0)}`); + + projectService.ensureInferredProjectsUpToDate_TestOnly(); + checkNumberOfProjects(projectService, { inferredProjects: 2 }); + }); }); } \ No newline at end of file