diff --git a/src/harness/harnessLanguageService.ts b/src/harness/harnessLanguageService.ts index 4206f16bab..68ac8d916f 100644 --- a/src/harness/harnessLanguageService.ts +++ b/src/harness/harnessLanguageService.ts @@ -469,6 +469,7 @@ module Harness.LanguageService { this.writeMessage(message); } + readFile(fileName: string): string { if (fileName.indexOf(Harness.Compiler.defaultLibFileName) >= 0) { fileName = Harness.Compiler.defaultLibFileName; @@ -526,6 +527,15 @@ module Harness.LanguageService { msg(message: string) { return this.host.log(message); } + + loggingEnabled() { + return true; + } + + isVerbose() { + return false; + } + endGroup(): void { } diff --git a/src/server/editorServices.ts b/src/server/editorServices.ts index 5ca4cb4d00..7c9ccfa785 100644 --- a/src/server/editorServices.ts +++ b/src/server/editorServices.ts @@ -5,6 +5,8 @@ module ts.server { export interface Logger { close(): void; + isVerbose(): boolean; + loggingEnabled(): boolean; perftrc(s: string): void; info(s: string): void; startGroup(): void; @@ -1071,6 +1073,7 @@ module ts.server { static changeNumberThreshold = 8; static changeLengthThreshold = 256; + static maxVersions = 8; // REVIEW: can optimize by coalescing simple edits edit(pos: number, deleteLen: number, insertedText?: string) { @@ -1131,6 +1134,13 @@ module ts.server { this.currentVersion = snap.version; this.versions[snap.version] = snap; this.changes = []; + if ((this.currentVersion - this.minVersion) >= ScriptVersionCache.maxVersions) { + var oldMin = this.minVersion; + this.minVersion = (this.currentVersion - ScriptVersionCache.maxVersions) + 1; + for (var j = oldMin; j < this.minVersion; j++) { + this.versions[j] = undefined; + } + } } return snap; } diff --git a/src/server/server.ts b/src/server/server.ts index c48ba95134..4c13be80c4 100644 --- a/src/server/server.ts +++ b/src/server/server.ts @@ -19,7 +19,7 @@ module ts.server { inGroup = false; firstInGroup = true; - constructor(public logFilename: string) { + constructor(public logFilename: string, public level: string) { } static padStringRight(str: string, padding: string) { @@ -51,9 +51,20 @@ module ts.server { this.firstInGroup = true; } + loggingEnabled() { + return !!this.logFilename; + } + + isVerbose() { + return this.loggingEnabled() && (this.level == "verbose"); + } + + msg(s: string, type = "Err") { if (this.fd < 0) { - this.fd = fs.openSync(this.logFilename, "w"); + if (this.logFilename) { + this.fd = fs.openSync(this.logFilename, "w"); + } } if (this.fd >= 0) { s = s + "\n"; @@ -173,17 +184,61 @@ module ts.server { }); rl.on('close',() => { - this.projectService.closeLog(); this.projectService.log("Exiting..."); + this.projectService.closeLog(); process.exit(0); }); } } + interface LogOptions { + file?: string; + detailLevel?: string; + } + + function parseLoggingEnvironmentString(logEnvStr: string): LogOptions { + var logEnv: LogOptions = {}; + var args = logEnvStr.split(' '); + for (var i = 0, len = args.length; i < (len - 1); i += 2) { + var option = args[i]; + var value = args[i + 1]; + if (option && value) { + switch (option) { + case "-file": + logEnv.file = value; + break; + case "-level": + logEnv.detailLevel = value; + break; + } + } + } + return logEnv; + } + + // TSS_LOG "{ level: "normal | verbose | terse", file?: string}" + function createLoggerFromEnv() { + var fileName: string = undefined; + var detailLevel = "normal"; + var logEnvStr = process.env["TSS_LOG"]; + if (logEnvStr) { + var logEnv = parseLoggingEnvironmentString(logEnvStr); + if (logEnv.file) { + fileName = logEnv.file; + } + else { + fileName = __dirname + "/.log" + process.pid.toString(); + } + if (logEnv.detailLevel) { + detailLevel = logEnv.detailLevel; + } + } + return new Logger(fileName, detailLevel); + } // This places log file in the directory containing editorServices.js // TODO: check that this location is writable - var logger = new Logger(__dirname + "/.log" + process.pid.toString()); + var logger = createLoggerFromEnv(); // REVIEW: for now this implementation uses polling. // The advantage of polling is that it works reliably diff --git a/src/server/session.ts b/src/server/session.ts index 7e9c6972a1..cc705d64d5 100644 --- a/src/server/session.ts +++ b/src/server/session.ts @@ -145,6 +145,9 @@ module ts.server { send(msg: NodeJS._debugger.Message) { var json = JSON.stringify(msg); + if (this.logger.isVerbose()) { + this.logger.info(msg.type + ": " + json); + } this.sendLineToClient('Content-Length: ' + (1 + Buffer.byteLength(json, 'utf8')) + '\r\n\r\n' + json); } @@ -461,19 +464,49 @@ module ts.server { var compilerService = project.compilerService; var position = compilerService.host.lineColToPosition(file, line, col); var edits = compilerService.languageService.getFormattingEditsAfterKeystroke(file, position, key, - compilerService.formatCodeOptions); + compilerService.formatCodeOptions); + // Check whether we should auto-indent. This will be when + // the position is on a line containing only whitespace. + // This should leave the edits returned from + // getFormattingEditsAfterKeytroke either empty or pertaining + // only to the previous line. If all this is true, then + // add edits necessary to properly indent the current line. if ((key == "\n") && ((!edits) || (edits.length == 0) || allEditsBeforePos(edits, position))) { - // TODO: get these options from host - var editorOptions: ts.EditorOptions = { - IndentSize: 4, - TabSize: 4, - NewLineCharacter: "\n", - ConvertTabsToSpaces: true, - }; - var indentPosition = compilerService.languageService.getIndentationAtPosition(file, position, editorOptions); - var spaces = generateSpaces(indentPosition); - if (indentPosition > 0) { - edits.push({ span: ts.createTextSpanFromBounds(position, position), newText: spaces }); + var scriptInfo = compilerService.host.getScriptInfo(file); + if (scriptInfo) { + var lineInfo = scriptInfo.getLineInfo(line); + if (lineInfo && (lineInfo.leaf) && (lineInfo.leaf.text)) { + var lineText = lineInfo.leaf.text; + if (lineText.search("\\S") < 0) { + // TODO: get these options from host + var editorOptions: ts.EditorOptions = { + IndentSize: 4, + TabSize: 4, + NewLineCharacter: "\n", + ConvertTabsToSpaces: true, + }; + var indentPosition = + compilerService.languageService.getIndentationAtPosition(file, position, editorOptions); + for (var i = 0, len = lineText.length; i < len; i++) { + if (lineText.charAt(i) == " ") { + indentPosition--; + } + else { + break; + } + } + if (indentPosition > 0) { + var spaces = generateSpaces(indentPosition); + edits.push({ span: ts.createTextSpanFromBounds(position, position), newText: spaces }); + } + else if (indentPosition < 0) { + edits.push({ + span: ts.createTextSpanFromBounds(position, position - indentPosition), + newText: "" + }); + } + } + } } } @@ -491,7 +524,7 @@ module ts.server { }; }); } - + getCompletions(line: number, col: number, prefix: string, fileName: string): protocol.CompletionEntry[] { if (!prefix) { prefix = ""; @@ -693,6 +726,10 @@ module ts.server { } onMessage(message: string) { + if (this.logger.isVerbose()) { + this.logger.info("request: " + message); + var start = process.hrtime(); + } try { var request = JSON.parse(message); var response: any; @@ -798,13 +835,23 @@ module ts.server { } } + if (this.logger.isVerbose()) { + var elapsed = process.hrtime(start); + var seconds = elapsed[0] + var nanoseconds = elapsed[1]; + var elapsedMs = ((1e9 * seconds) + nanoseconds)/1000000.0; + var leader = "Elapsed time (in milliseconds)"; + if (!responseRequired) { + leader = "Async elapsed time (in milliseconds)"; + } + this.logger.msg(leader + ": " + elapsedMs.toFixed(4).toString(), "Perf"); + } if (response) { this.output(response, request.command, request.seq); } else if (responseRequired) { this.output(undefined, request.command, request.seq, "No content available."); } - } catch (err) { if (err instanceof OperationCanceledException) { // Handle cancellation exceptions