diff --git a/Jakefile b/Jakefile index dc8070fd7514d..07b0d267a6895 100644 --- a/Jakefile +++ b/Jakefile @@ -8,6 +8,7 @@ var child_process = require("child_process"); // Variables var compilerDirectory = "src/compiler/"; var servicesDirectory = "src/services/"; +var serverDirectory = "src/server/"; var harnessDirectory = "src/harness/"; var libraryDirectory = "src/lib/"; var scriptsDirectory = "scripts/"; @@ -90,6 +91,16 @@ var servicesSources = [ return path.join(servicesDirectory, f); })); +var serverSources = [ + "node.d.ts", + "editorServices.ts", + "protocol.d.ts", + "session.ts", + "server.ts" +].map(function (f) { + return path.join(serverDirectory, f); +}); + var definitionsRoots = [ "compiler/types.d.ts", "compiler/scanner.d.ts", @@ -130,6 +141,13 @@ var harnessSources = [ "services/preProcessFile.ts" ].map(function (f) { return path.join(unittestsDirectory, f); +})).concat([ + "protocol.d.ts", + "session.ts", + "client.ts", + "editorServices.ts", +].map(function (f) { + return path.join(serverDirectory, f); })); var librarySourceMap = [ @@ -327,6 +345,7 @@ var tscFile = path.join(builtLocalDirectory, compilerFilename); compileFile(tscFile, compilerSources, [builtLocalDirectory, copyright].concat(compilerSources), [copyright], /*useBuiltCompiler:*/ false); var servicesFile = path.join(builtLocalDirectory, "typescriptServices.js"); +var nodePackageFile = path.join(builtLocalDirectory, "typescript.js"); compileFile(servicesFile, servicesSources,[builtLocalDirectory, copyright].concat(servicesSources), /*prefixes*/ [copyright], /*useBuiltCompiler*/ true, @@ -336,7 +355,10 @@ compileFile(servicesFile, servicesSources,[builtLocalDirectory, copyright].conca /*preserveConstEnums*/ true, /*keepComments*/ false, /*noResolve*/ false, - /*stripInternal*/ false); + /*stripInternal*/ false, + /*callback*/ function () { + jake.cpR(servicesFile, nodePackageFile, {silent: true}); + }); var nodeDefinitionsFile = path.join(builtLocalDirectory, "typescript.d.ts"); var standaloneDefinitionsFile = path.join(builtLocalDirectory, "typescriptServices.d.ts"); @@ -378,9 +400,12 @@ compileFile(nodeDefinitionsFile, servicesSources,[builtLocalDirectory, copyright jake.rmRf(tempDirPath, {silent: true}); }); +var serverFile = path.join(builtLocalDirectory, "tsserver.js"); +compileFile(serverFile, serverSources,[builtLocalDirectory, copyright].concat(serverSources), /*prefixes*/ [copyright], /*useBuiltCompiler*/ true); + // Local target to build the compiler and services desc("Builds the full compiler and services"); -task("local", ["generate-diagnostics", "lib", tscFile, servicesFile, nodeDefinitionsFile]); +task("local", ["generate-diagnostics", "lib", tscFile, servicesFile, nodeDefinitionsFile, serverFile]); // Local target to build only tsc.js desc("Builds only the compiler"); @@ -435,7 +460,7 @@ task("generate-spec", [specMd]) // Makes a new LKG. This target does not build anything, but errors if not all the outputs are present in the built/local directory desc("Makes a new LKG out of the built js files"); task("LKG", ["clean", "release", "local"].concat(libraryTargets), function() { - var expectedFiles = [tscFile, servicesFile, nodeDefinitionsFile, standaloneDefinitionsFile, internalNodeDefinitionsFile, internalStandaloneDefinitionsFile].concat(libraryTargets); + var expectedFiles = [tscFile, servicesFile, nodePackageFile, nodeDefinitionsFile, standaloneDefinitionsFile, internalNodeDefinitionsFile, internalStandaloneDefinitionsFile].concat(libraryTargets); var missingFiles = expectedFiles.filter(function (f) { return !fs.existsSync(f); }); diff --git a/bin/tsserver b/bin/tsserver new file mode 100644 index 0000000000000..003eb0d22af9c --- /dev/null +++ b/bin/tsserver @@ -0,0 +1,2 @@ +#!/usr/bin/env node +require('./tsserver.js') diff --git a/package.json b/package.json index 2bd8970660631..9261174b68f3b 100644 --- a/package.json +++ b/package.json @@ -25,9 +25,10 @@ "url": "https://github.com/Microsoft/TypeScript.git" }, "preferGlobal": true, - "main": "./bin/typescriptServices.js", + "main": "./bin/typescript.js", "bin": { - "tsc": "./bin/tsc" + "tsc": "./bin/tsc", + "tsserver": "./bin/tsserver" }, "engines": { "node": ">=0.8.0" diff --git a/src/harness/fourslash.ts b/src/harness/fourslash.ts index 603a78b5f75b9..bb144742f45fd 100644 --- a/src/harness/fourslash.ts +++ b/src/harness/fourslash.ts @@ -282,6 +282,8 @@ module FourSlash { return new Harness.LanguageService.NativeLanugageServiceAdapter(cancellationToken, compilationOptions); case FourSlashTestType.Shims: return new Harness.LanguageService.ShimLanugageServiceAdapter(cancellationToken, compilationOptions); + case FourSlashTestType.Server: + return new Harness.LanguageService.ServerLanugageServiceAdapter(cancellationToken, compilationOptions); default: throw new Error("Unknown FourSlash test type: "); } @@ -418,6 +420,9 @@ module FourSlash { this.activeFile = fileToOpen; var fileName = fileToOpen.fileName.replace(Harness.IO.directoryName(fileToOpen.fileName), '').substr(1); this.scenarioActions.push(''); + + // Let the host know that this file is now open + this.languageServiceAdapterHost.openFile(fileToOpen.fileName); } public verifyErrorExistsBetweenMarkers(startMarkerName: string, endMarkerName: string, negative: boolean) { @@ -1927,7 +1932,7 @@ module FourSlash { } var missingItem = { name: name, kind: kind }; - this.raiseError('verifyGetScriptLexicalStructureListContains failed - could not find the item: ' + JSON.stringify(missingItem) + ' in the returned list: (' + JSON.stringify(items) + ')'); + this.raiseError('verifyGetScriptLexicalStructureListContains failed - could not find the item: ' + JSON.stringify(missingItem) + ' in the returned list: (' + JSON.stringify(items, null, " ") + ')'); } private navigationBarItemsContains(items: ts.NavigationBarItem[], name: string, kind: string) { diff --git a/src/harness/fourslashRunner.ts b/src/harness/fourslashRunner.ts index fe3b6c7a91f8d..1ab6e31cdbbdb 100644 --- a/src/harness/fourslashRunner.ts +++ b/src/harness/fourslashRunner.ts @@ -4,7 +4,8 @@ const enum FourSlashTestType { Native, - Shims + Shims, + Server } class FourSlashRunner extends RunnerBase { @@ -22,6 +23,10 @@ class FourSlashRunner extends RunnerBase { this.basePath = 'tests/cases/fourslash/shims'; this.testSuiteName = 'fourslash-shims'; break; + case FourSlashTestType.Server: + this.basePath = 'tests/cases/fourslash/server'; + this.testSuiteName = 'fourslash-server'; + break; } } diff --git a/src/harness/harness.ts b/src/harness/harness.ts index 534bf85d119ea..8ee51f89c9c55 100644 --- a/src/harness/harness.ts +++ b/src/harness/harness.ts @@ -16,14 +16,15 @@ /// /// +/// +/// +/// /// /// /// /// -declare var require: any; -declare var process: any; -var Buffer = require('buffer').Buffer; +var Buffer: BufferConstructor = require('buffer').Buffer; // this will work in the browser via browserify var _chai: typeof chai = require('chai'); diff --git a/src/harness/harnessLanguageService.ts b/src/harness/harnessLanguageService.ts index 2f95f5072d067..712f0afe0aaf3 100644 --- a/src/harness/harnessLanguageService.ts +++ b/src/harness/harnessLanguageService.ts @@ -1,5 +1,6 @@ /// /// +/// /// module Harness.LanguageService { @@ -23,18 +24,18 @@ module Harness.LanguageService { this.version++; } - public editContent(minChar: number, limChar: number, newText: string): void { + public editContent(start: number, end: number, newText: string): void { // Apply edits - var prefix = this.content.substring(0, minChar); + var prefix = this.content.substring(0, start); var middle = newText; - var suffix = this.content.substring(limChar); + var suffix = this.content.substring(end); this.setContent(prefix + middle + suffix); // Store edit range + new length of script this.editRanges.push({ length: this.content.length, textChangeRange: ts.createTextChangeRange( - ts.createTextSpanFromBounds(minChar, limChar), newText.length) + ts.createTextSpanFromBounds(start, end), newText.length) }); // Update version # @@ -145,24 +146,17 @@ module Harness.LanguageService { this.fileNameToScript[fileName] = new ScriptInfo(fileName, content); } - public updateScript(fileName: string, content: string) { + public editScript(fileName: string, start: number, end: number, newText: string) { var script = this.getScriptInfo(fileName); if (script !== null) { - script.updateContent(content); + script.editContent(start, end, newText); return; } - this.addScript(fileName, content); + throw new Error("No script with name '" + fileName + "'"); } - public editScript(fileName: string, minChar: number, limChar: number, newText: string) { - var script = this.getScriptInfo(fileName); - if (script !== null) { - script.editContent(minChar, limChar, newText); - return; - } - - throw new Error("No script with name '" + fileName + "'"); + public openFile(fileName: string): void { } /** @@ -236,8 +230,7 @@ module Harness.LanguageService { getFilenames(): string[] { return this.nativeHost.getFilenames(); } getScriptInfo(fileName: string): ScriptInfo { return this.nativeHost.getScriptInfo(fileName); } addScript(fileName: string, content: string): void { this.nativeHost.addScript(fileName, content); } - updateScript(fileName: string, content: string): void { return this.nativeHost.updateScript(fileName, content); } - editScript(fileName: string, minChar: number, limChar: number, newText: string): void { this.nativeHost.editScript(fileName, minChar, limChar, newText); } + editScript(fileName: string, start: number, end: number, newText: string): void { this.nativeHost.editScript(fileName, start, end, newText); } lineColToPosition(fileName: string, line: number, col: number): number { return this.nativeHost.lineColToPosition(fileName, line, col); } positionToZeroBasedLineCol(fileName: string, position: number): ts.LineAndCharacter { return this.nativeHost.positionToZeroBasedLineCol(fileName, position); } @@ -442,5 +435,156 @@ module Harness.LanguageService { return convertResult; } } + + // Server adapter + class SessionClientHost extends NativeLanguageServiceHost implements ts.server.SessionClientHost { + private client: ts.server.SessionClient; + + constructor(cancellationToken: ts.CancellationToken, settings: ts.CompilerOptions) { + super(cancellationToken, settings); + } + + onMessage(message: string): void { + + } + + writeMessage(message: string): void { + + } + + setClient(client: ts.server.SessionClient) { + this.client = client; + } + + openFile(fileName: string): void { + super.openFile(fileName); + this.client.openFile(fileName); + } + + editScript(fileName: string, start: number, end: number, newText: string) { + super.editScript(fileName, start, end, newText); + this.client.changeFile(fileName, start, end, newText); + } + } + + class SessionServerHost implements ts.server.ServerHost, ts.server.Logger { + args: string[] = []; + newLine: string; + useCaseSensitiveFileNames: boolean = false; + + constructor(private host: NativeLanguageServiceHost) { + this.newLine = this.host.getNewLine(); + } + + onMessage(message: string): void { + + } + + writeMessage(message: string): void { + } + + write(message: string): void { + this.writeMessage(message); + } + + readFile(fileName: string): string { + if (fileName.indexOf(Harness.Compiler.defaultLibFileName) >= 0) { + fileName = Harness.Compiler.defaultLibFileName; + } + + var snapshot = this.host.getScriptSnapshot(fileName); + return snapshot && snapshot.getText(0, snapshot.getLength()); + } + + writeFile(name: string, text: string, writeByteOrderMark: boolean): void { + } + + resolvePath(path: string): string { + return path; + } + + fileExists(path: string): boolean { + return !!this.host.getScriptSnapshot(path); + } + + directoryExists(path: string): boolean { + return false; + } + + getExecutingFilePath(): string { + return ""; + } + + exit(exitCode: number): void { + } + + createDirectory(directoryName: string): void { + throw new Error("Not Implemented Yet."); + } + + getCurrentDirectory(): string { + return this.host.getCurrentDirectory(); + } + + readDirectory(path: string, extension?: string): string[] { + throw new Error("Not implemented Yet."); + } + + watchFile(fileName: string, callback: (fileName: string) => void): ts.FileWatcher { + return { close() { } }; + } + + close(): void { + } + + info(message: string): void { + return this.host.log(message); + } + + msg(message: string) { + return this.host.log(message); + } + + endGroup(): void { + } + + perftrc(message: string): void { + return this.host.log(message); + } + + startGroup(): void { + } + } + + export class ServerLanugageServiceAdapter implements LanguageServiceAdapter { + private host: SessionClientHost; + private client: ts.server.SessionClient; + constructor(cancellationToken?: ts.CancellationToken, options?: ts.CompilerOptions) { + // This is the main host that tests use to direct tests + var clientHost = new SessionClientHost(cancellationToken, options); + var client = new ts.server.SessionClient(clientHost); + + // This host is just a proxy for the clientHost, it uses the client + // host to answer server queries about files on disk + var serverHost = new SessionServerHost(clientHost); + var server = new ts.server.Session(serverHost, serverHost); + + // Fake the connection between the client and the server + serverHost.writeMessage = client.onMessage.bind(client); + clientHost.writeMessage = server.onMessage.bind(server); + + // Wire the client to the host to get notifications when a file is open + // or edited. + clientHost.setClient(client); + + // Set the properties + this.client = client; + this.host = clientHost; + } + getHost() { return this.host; } + getLanguageService(): ts.LanguageService { return this.client; } + getClassifier(): ts.Classifier { throw new Error("getClassifier is not available using the server interface."); } + getPreProcessedFileInfo(fileName: string, fileContents: string): ts.PreProcessedFileInfo { throw new Error("getPreProcessedFileInfo is not available using the server interface."); } + } } \ No newline at end of file diff --git a/src/harness/runner.ts b/src/harness/runner.ts index 942ae56bf6fe8..e1e5429b00723 100644 --- a/src/harness/runner.ts +++ b/src/harness/runner.ts @@ -66,6 +66,9 @@ if (testConfigFile !== '') { case 'fourslash-shims': runners.push(new FourSlashRunner(FourSlashTestType.Shims)); break; + case 'fourslash-server': + runners.push(new FourSlashRunner(FourSlashTestType.Server)); + break; case 'fourslash-generated': runners.push(new GeneratedFourslashRunner(FourSlashTestType.Native)); break; @@ -95,6 +98,7 @@ if (runners.length === 0) { // language services runners.push(new FourSlashRunner(FourSlashTestType.Native)); runners.push(new FourSlashRunner(FourSlashTestType.Shims)); + runners.push(new FourSlashRunner(FourSlashTestType.Server)); //runners.push(new GeneratedFourslashRunner()); } diff --git a/src/server/client.ts b/src/server/client.ts new file mode 100644 index 0000000000000..6185834a022df --- /dev/null +++ b/src/server/client.ts @@ -0,0 +1,494 @@ +/// + +module ts.server { + + export interface SessionClientHost extends LanguageServiceHost { + writeMessage(message: string): void; + } + + interface CompletionEntry extends CompletionInfo { + fileName: string; + position: number; + } + + interface RenameEntry extends RenameInfo { + fileName: string; + position: number; + locations: RenameLocation[]; + findInStrings: boolean; + findInComments: boolean; + } + + export class SessionClient implements LanguageService { + private sequence: number = 0; + private fileMapping: ts.Map = {}; + private lineMaps: ts.Map = {}; + private messages: string[] = []; + private lastRenameEntry: RenameEntry; + + constructor(private host: SessionClientHost) { + } + + public onMessage(message: string): void { + this.messages.push(message); + } + + private writeMessage(message: string): void { + this.host.writeMessage(message); + } + + private getLineMap(fileName: string): number[] { + var lineMap = ts.lookUp(this.lineMaps, fileName); + if (!lineMap) { + var scriptSnapshot = this.host.getScriptSnapshot(fileName); + lineMap = this.lineMaps[fileName] = ts.computeLineStarts(scriptSnapshot.getText(0, scriptSnapshot.getLength())); + } + return lineMap; + } + + private lineColToPosition(fileName: string, lineCol: protocol.Location): number { + return ts.computePositionFromLineAndCharacter(this.getLineMap(fileName), lineCol.line, lineCol.col); + } + + private positionToOneBasedLineCol(fileName: string, position: number): protocol.Location { + var lineCol = ts.computeLineAndCharacterOfPosition(this.getLineMap(fileName), position); + return { + line: lineCol.line, + col: lineCol.character + }; + } + + private convertCodeEditsToTextChange(fileName: string, codeEdit: protocol.CodeEdit): ts.TextChange { + var start = this.lineColToPosition(fileName, codeEdit.start); + var end = this.lineColToPosition(fileName, codeEdit.end); + + return { + span: ts.createTextSpanFromBounds(start, end), + newText: codeEdit.newText + }; + } + + private processRequest(command: string, arguments?: any): T { + var request: protocol.Request = { + seq: this.sequence++, + type: "request", + command: command, + arguments: arguments + }; + + this.writeMessage(JSON.stringify(request)); + + return request; + } + + private processResponse(request: protocol.Request): T { + var lastMessage = this.messages.shift(); + Debug.assert(!!lastMessage, "Did not recieve any responses."); + + // Read the content length + var contentLengthPrefix = "Content-Length: "; + var lines = lastMessage.split("\r\n"); + Debug.assert(lines.length >= 2, "Malformed response: Expected 3 lines in the response."); + + var contentLengthText = lines[0]; + Debug.assert(contentLengthText.indexOf(contentLengthPrefix) === 0, "Malformed response: Response text did not contain content-length header."); + var contentLength = parseInt(contentLengthText.substring(contentLengthPrefix.length)); + + // Read the body + var responseBody = lines[2]; + + // Verify content length + Debug.assert(responseBody.length + 1 === contentLength, "Malformed response: Content length did not match the response's body length."); + + try { + var response: T = JSON.parse(responseBody); + } + catch (e) { + throw new Error("Malformed response: Failed to parse server response: " + lastMessage + ". \r\n Error detailes: " + e.message); + } + + // verify the sequence numbers + Debug.assert(response.request_seq === request.seq, "Malformed response: response sequance number did not match request sequence number."); + + // unmarshal errors + if (!response.success) { + throw new Error("Error " + response.message); + } + + Debug.assert(!!response.body, "Malformed response: Unexpected empty response body."); + + return response; + } + + openFile(fileName: string): void { + var args: protocol.FileRequestArgs = { file: fileName }; + this.processRequest(CommandNames.Open, args); + } + + closeFile(fileName: string): void { + var args: protocol.FileRequestArgs = { file: fileName }; + this.processRequest(CommandNames.Close, args); + } + + changeFile(fileName: string, start: number, end: number, newText: string): void { + // clear the line map after an edit + this.lineMaps[fileName] = undefined; + + var lineCol = this.positionToOneBasedLineCol(fileName, start); + var endLineCol = this.positionToOneBasedLineCol(fileName, end); + + var args: protocol.ChangeRequestArgs = { + file: fileName, + line: lineCol.line, + col: lineCol.col, + endLine: endLineCol.line, + endCol: endLineCol.col, + insertString: newText + }; + + this.processRequest(CommandNames.Change, args); + } + + getQuickInfoAtPosition(fileName: string, position: number): QuickInfo { + var lineCol = this.positionToOneBasedLineCol(fileName, position); + var args: protocol.FileLocationRequestArgs = { + file: fileName, + line: lineCol.line, + col: lineCol.col + }; + + var request = this.processRequest(CommandNames.Quickinfo, args); + var response = this.processResponse(request); + + var start = this.lineColToPosition(fileName, response.body.start); + var end = this.lineColToPosition(fileName, response.body.end); + + return { + kind: response.body.kind, + kindModifiers: response.body.kindModifiers, + textSpan: ts.createTextSpanFromBounds(start, end), + displayParts: [{ kind: "text", text: response.body.displayString }], + documentation: [{ kind: "text", text: response.body.documentation }] + }; + } + + getCompletionsAtPosition(fileName: string, position: number): CompletionInfo { + var lineCol = this.positionToOneBasedLineCol(fileName, position); + var args: protocol.CompletionsRequestArgs = { + file: fileName, + line: lineCol.line, + col: lineCol.col, + prefix: undefined + }; + + var request = this.processRequest(CommandNames.Completions, args); + var response = this.processResponse(request); + + return { + isMemberCompletion: false, + isNewIdentifierLocation: false, + entries: response.body, + fileName: fileName, + position: position + }; + } + + getCompletionEntryDetails(fileName: string, position: number, entryName: string): CompletionEntryDetails { + var lineCol = this.positionToOneBasedLineCol(fileName, position); + var args: protocol.CompletionDetailsRequestArgs = { + file: fileName, + line: lineCol.line, + col: lineCol.col, + entryNames: [entryName] + }; + + var request = this.processRequest(CommandNames.CompletionDetails, args); + var response = this.processResponse(request); + Debug.assert(response.body.length == 1, "Unexpected length of completion details response body."); + return response.body[0]; + } + + getNavigateToItems(searchTerm: string): NavigateToItem[] { + var args: protocol.NavtoRequestArgs = { + searchTerm, + file: this.host.getScriptFileNames()[0] + }; + + var request = this.processRequest(CommandNames.Navto, args); + var response = this.processResponse(request); + + return response.body.map(entry => { + var fileName = entry.file; + var start = this.lineColToPosition(fileName, entry.start); + var end = this.lineColToPosition(fileName, entry.end); + + return { + name: entry.name, + containerName: entry.containerName || "", + containerKind: entry.containerKind || "", + kind: entry.kind, + kindModifiers: entry.kindModifiers, + matchKind: entry.matchKind, + fileName: fileName, + textSpan: ts.createTextSpanFromBounds(start, end) + }; + }); + } + + getFormattingEditsForRange(fileName: string, start: number, end: number, options: ts.FormatCodeOptions): ts.TextChange[] { + var startLineCol = this.positionToOneBasedLineCol(fileName, start); + var endLineCol = this.positionToOneBasedLineCol(fileName, end); + var args: protocol.FormatRequestArgs = { + file: fileName, + line: startLineCol.line, + col: startLineCol.col, + endLine: endLineCol.line, + endCol: endLineCol.col, + }; + + // TODO: handle FormatCodeOptions + var request = this.processRequest(CommandNames.Format, args); + var response = this.processResponse(request); + + return response.body.map(entry=> this.convertCodeEditsToTextChange(fileName, entry)); + } + + getFormattingEditsForDocument(fileName: string, options: ts.FormatCodeOptions): ts.TextChange[] { + return this.getFormattingEditsForRange(fileName, 0, this.host.getScriptSnapshot(fileName).getLength(), options); + } + + getFormattingEditsAfterKeystroke(fileName: string, position: number, key: string, options: FormatCodeOptions): ts.TextChange[] { + var lineCol = this.positionToOneBasedLineCol(fileName, position); + var args: protocol.FormatOnKeyRequestArgs = { + file: fileName, + line: lineCol.line, + col: lineCol.col, + key: key + }; + + // TODO: handle FormatCodeOptions + var request = this.processRequest(CommandNames.Formatonkey, args); + var response = this.processResponse(request); + + return response.body.map(entry=> this.convertCodeEditsToTextChange(fileName, entry)); + } + + getDefinitionAtPosition(fileName: string, position: number): DefinitionInfo[] { + var lineCol = this.positionToOneBasedLineCol(fileName, position); + var args: protocol.FileLocationRequestArgs = { + file: fileName, + line: lineCol.line, + col: lineCol.col, + }; + + var request = this.processRequest(CommandNames.Definition, args); + var response = this.processResponse(request); + + return response.body.map(entry => { + var fileName = entry.file; + var start = this.lineColToPosition(fileName, entry.start); + var end = this.lineColToPosition(fileName, entry.end); + return { + containerKind: "", + containerName: "", + fileName: fileName, + textSpan: ts.createTextSpanFromBounds(start, end), + kind: "", + name: "" + }; + }); + } + + getReferencesAtPosition(fileName: string, position: number): ReferenceEntry[] { + var lineCol = this.positionToOneBasedLineCol(fileName, position); + var args: protocol.FileLocationRequestArgs = { + file: fileName, + line: lineCol.line, + col: lineCol.col, + }; + + var request = this.processRequest(CommandNames.References, args); + var response = this.processResponse(request); + + return response.body.refs.map(entry => { + var fileName = entry.file; + var start = this.lineColToPosition(fileName, entry.start); + var end = this.lineColToPosition(fileName, entry.end); + return { + fileName: fileName, + textSpan: ts.createTextSpanFromBounds(start, end), + isWriteAccess: entry.isWriteAccess, + }; + }); + } + + getEmitOutput(fileName: string): EmitOutput { + throw new Error("Not Implemented Yet."); + } + + getSyntacticDiagnostics(fileName: string): Diagnostic[] { + throw new Error("Not Implemented Yet."); + } + + getSemanticDiagnostics(fileName: string): Diagnostic[] { + throw new Error("Not Implemented Yet."); + } + + getCompilerOptionsDiagnostics(): Diagnostic[] { + throw new Error("Not Implemented Yet."); + } + + getRenameInfo(fileName: string, position: number, findInStrings?: boolean, findInComments?: boolean): RenameInfo { + var lineCol = this.positionToOneBasedLineCol(fileName, position); + var args: protocol.RenameRequestArgs = { + file: fileName, + line: lineCol.line, + col: lineCol.col, + findInStrings, + findInComments + }; + + var request = this.processRequest(CommandNames.Rename, args); + var response = this.processResponse(request); + var locations: RenameLocation[] = []; + response.body.locs.map((entry: protocol.SpanGroup) => { + var fileName = entry.file; + entry.locs.map((loc: protocol.TextSpan) => { + var start = this.lineColToPosition(fileName, loc.start); + var end = this.lineColToPosition(fileName, loc.end); + locations.push({ + textSpan: ts.createTextSpanFromBounds(start, end), + fileName: fileName + }); + }); + }); + return this.lastRenameEntry = { + canRename: response.body.info.canRename, + displayName: response.body.info.displayName, + fullDisplayName: response.body.info.fullDisplayName, + kind: response.body.info.kind, + kindModifiers: response.body.info.kindModifiers, + localizedErrorMessage: response.body.info.localizedErrorMessage, + triggerSpan: ts.createTextSpanFromBounds(position, position), + fileName: fileName, + position: position, + findInStrings: findInStrings, + findInComments: findInComments, + locations: locations + }; + } + + findRenameLocations(fileName: string, position: number, findInStrings: boolean, findInComments: boolean): RenameLocation[] { + if (!this.lastRenameEntry || + this.lastRenameEntry.fileName !== fileName || + this.lastRenameEntry.position !== position || + this.lastRenameEntry.findInStrings != findInStrings || + this.lastRenameEntry.findInComments != findInComments) { + this.getRenameInfo(fileName, position, findInStrings, findInComments); + } + + return this.lastRenameEntry.locations; + } + + decodeNavigationBarItems(items: protocol.NavigationBarItem[], fileName: string): NavigationBarItem[] { + if (!items) { + return []; + } + + return items.map(item => ({ + text: item.text, + kind: item.kind, + kindModifiers: item.kindModifiers || "", + spans: item.spans.map(span=> createTextSpanFromBounds(this.lineColToPosition(fileName, span.start), this.lineColToPosition(fileName, span.end))), + childItems: this.decodeNavigationBarItems(item.childItems, fileName), + indent: 0, + bolded: false, + grayed: false + })); + } + + getNavigationBarItems(fileName: string): NavigationBarItem[] { + var args: protocol.FileRequestArgs = { + file: fileName + }; + + var request = this.processRequest(CommandNames.NavBar, args); + var response = this.processResponse(request); + + return this.decodeNavigationBarItems(response.body, fileName); + } + + getNameOrDottedNameSpan(fileName: string, startPos: number, endPos: number): TextSpan { + throw new Error("Not Implemented Yet."); + } + + getBreakpointStatementAtPosition(fileName: string, position: number): TextSpan { + throw new Error("Not Implemented Yet."); + } + + getSignatureHelpItems(fileName: string, position: number): SignatureHelpItems { + throw new Error("Not Implemented Yet."); + } + + getOccurrencesAtPosition(fileName: string, position: number): ReferenceEntry[] { + throw new Error("Not Implemented Yet."); + } + + getOutliningSpans(fileName: string): OutliningSpan[] { + throw new Error("Not Implemented Yet."); + } + + getTodoComments(fileName: string, descriptors: TodoCommentDescriptor[]): TodoComment[] { + throw new Error("Not Implemented Yet."); + } + + getBraceMatchingAtPosition(fileName: string, position: number): TextSpan[] { + var lineCol = this.positionToOneBasedLineCol(fileName, position); + var args: protocol.FileLocationRequestArgs = { + file: fileName, + line: lineCol.line, + col: lineCol.col, + }; + + var request = this.processRequest(CommandNames.Brace, args); + var response = this.processResponse(request); + + return response.body.map(entry => { + var start = this.lineColToPosition(fileName, entry.start); + var end = this.lineColToPosition(fileName, entry.end); + return { + start: start, + length: end - start, + }; + }); + } + + getIndentationAtPosition(fileName: string, position: number, options: EditorOptions): number { + throw new Error("Not Implemented Yet."); + } + + getSyntacticClassifications(fileName: string, span: TextSpan): ClassifiedSpan[] { + throw new Error("Not Implemented Yet."); + } + + getSemanticClassifications(fileName: string, span: TextSpan): ClassifiedSpan[] { + throw new Error("Not Implemented Yet."); + } + + getProgram(): Program { + throw new Error("SourceFile objects are not serializable through the server protocol."); + } + + getSourceFile(fileName: string): SourceFile { + throw new Error("SourceFile objects are not serializable through the server protocol."); + } + + cleanupSemanticCache(): void { + throw new Error("cleanupSemanticCache is not available through the server layer."); + } + + dispose(): void { + throw new Error("dispose is not available through the server layer."); + } + } +} diff --git a/src/server/editorServices.ts b/src/server/editorServices.ts new file mode 100644 index 0000000000000..5b5ee27d70738 --- /dev/null +++ b/src/server/editorServices.ts @@ -0,0 +1,1646 @@ +/// +/// +/// + +module ts.server { + export interface Logger { + close(): void; + perftrc(s: string): void; + info(s: string): void; + startGroup(): void; + endGroup(): void; + msg(s: string, type?: string): void; + } + + var lineCollectionCapacity = 4; + + class ScriptInfo { + svc: ScriptVersionCache; + children: ScriptInfo[] = []; // files referenced by this file + + defaultProject: Project; // project to use by default for file + + fileWatcher: FileWatcher; + + constructor(private host: ServerHost, public fileName: string, public content: string, public isOpen = false) { + this.svc = ScriptVersionCache.fromString(content); + } + + close() { + this.isOpen = false; + } + + addChild(childInfo: ScriptInfo) { + this.children.push(childInfo); + } + + snap() { + return this.svc.getSnapshot(); + } + + getText() { + var snap = this.snap(); + return snap.getText(0, snap.getLength()); + } + + getLineInfo(line: number) { + var snap = this.snap(); + return snap.index.lineNumberToInfo(line); + } + + editContent(start: number, end: number, newText: string): void { + this.svc.edit(start, end - start, newText); + } + + getTextChangeRangeBetweenVersions(startVersion: number, endVersion: number): ts.TextChangeRange { + return this.svc.getTextChangesBetweenVersions(startVersion, endVersion); + } + + getChangeRange(oldSnapshot: ts.IScriptSnapshot): ts.TextChangeRange { + return this.snap().getChangeRange(oldSnapshot); + } + } + + class LSHost implements ts.LanguageServiceHost { + ls: ts.LanguageService = null; + compilationSettings: ts.CompilerOptions; + filenameToScript: ts.Map = {}; + + constructor(public host: ServerHost, public project: Project) { + } + + getDefaultLibFileName() { + var nodeModuleBinDir = ts.getDirectoryPath(ts.normalizePath(this.host.getExecutingFilePath())); + return ts.combinePaths(nodeModuleBinDir, ts.getDefaultLibFileName(this.compilationSettings)); + } + + getScriptSnapshot(filename: string): ts.IScriptSnapshot { + var scriptInfo = this.getScriptInfo(filename); + if (scriptInfo) { + return scriptInfo.snap(); + } + } + + setCompilationSettings(opt: ts.CompilerOptions) { + this.compilationSettings = opt; + } + + lineAffectsRefs(filename: string, line: number) { + var info = this.getScriptInfo(filename); + var lineInfo = info.getLineInfo(line); + if (lineInfo && lineInfo.text) { + var regex = /reference|import|\/\*|\*\//; + return regex.test(lineInfo.text); + } + } + + getCompilationSettings() { + // change this to return active project settings for file + return this.compilationSettings; + } + + getScriptFileNames() { + var filenames: string[] = []; + for (var filename in this.filenameToScript) { + if (this.filenameToScript[filename] && this.filenameToScript[filename].isOpen) { + filenames.push(filename); + } + } + return filenames; + } + + getScriptVersion(filename: string) { + return this.getScriptInfo(filename).svc.latestVersion().toString(); + } + + getCurrentDirectory(): string { + return ""; + } + + getScriptIsOpen(filename: string) { + return this.getScriptInfo(filename).isOpen; + } + + removeReferencedFile(info: ScriptInfo) { + if (!info.isOpen) { + this.filenameToScript[info.fileName] = undefined; + } + } + + getScriptInfo(filename: string): ScriptInfo { + var scriptInfo = ts.lookUp(this.filenameToScript, filename); + if (!scriptInfo) { + scriptInfo = this.project.openReferencedFile(filename); + if (scriptInfo) { + this.filenameToScript[scriptInfo.fileName] = scriptInfo; + } + } + else { + } + return scriptInfo; + } + + addRoot(info: ScriptInfo) { + var scriptInfo = ts.lookUp(this.filenameToScript, info.fileName); + if (!scriptInfo) { + this.filenameToScript[info.fileName] = info; + return info; + } + } + + saveTo(filename: string, tmpfilename: string) { + var script = this.getScriptInfo(filename); + if (script) { + var snap = script.snap(); + this.host.writeFile(tmpfilename, snap.getText(0, snap.getLength())); + } + } + + reloadScript(filename: string, tmpfilename: string, cb: () => any) { + var script = this.getScriptInfo(filename); + if (script) { + script.svc.reloadFromFile(tmpfilename, cb); + } + } + + editScript(filename: string, start: number, end: number, newText: string) { + var script = this.getScriptInfo(filename); + if (script) { + script.editContent(start, end, newText); + return; + } + + throw new Error("No script with name '" + filename + "'"); + } + + resolvePath(path: string): string { + var start = new Date().getTime(); + var result = this.host.resolvePath(path); + return result; + } + + fileExists(path: string): boolean { + var start = new Date().getTime(); + var result = this.host.fileExists(path); + return result; + } + + directoryExists(path: string): boolean { + return this.host.directoryExists(path); + } + + /** + * @param line 1 based index + */ + lineToTextSpan(filename: string, line: number): ts.TextSpan { + var script: ScriptInfo = this.filenameToScript[filename]; + var index = script.snap().index; + + var lineInfo = index.lineNumberToInfo(line + 1); + var len: number; + if (lineInfo.leaf) { + len = lineInfo.leaf.text.length; + } + else { + var nextLineInfo = index.lineNumberToInfo(line + 2); + len = nextLineInfo.col - lineInfo.col; + } + return ts.createTextSpan(lineInfo.col, len); + } + + /** + * @param line 1 based index + * @param col 1 based index + */ + lineColToPosition(filename: string, line: number, col: number): number { + var script: ScriptInfo = this.filenameToScript[filename]; + var index = script.snap().index; + + var lineInfo = index.lineNumberToInfo(line); + // TODO: assert this column is actually on the line + return (lineInfo.col + col - 1); + } + + /** + * @param line 1-based index + * @param col 1-based index + */ + positionToLineCol(filename: string, position: number): ILineInfo { + var script: ScriptInfo = this.filenameToScript[filename]; + var index = script.snap().index; + var lineCol = index.charOffsetToLineNumberAndPos(position); + return { line: lineCol.line, col: lineCol.col + 1 }; + } + } + + // assumes normalized paths + function getAbsolutePath(filename: string, directory: string) { + var rootLength = ts.getRootLength(filename); + if (rootLength > 0) { + return filename; + } + else { + var splitFilename = filename.split('/'); + var splitDir = directory.split('/'); + var i = 0; + var dirTail = 0; + var sflen = splitFilename.length; + while ((i < sflen) && (splitFilename[i].charAt(0) == '.')) { + var dots = splitFilename[i]; + if (dots == '..') { + dirTail++; + } + else if (dots != '.') { + return undefined; + } + i++; + } + return splitDir.slice(0, splitDir.length - dirTail).concat(splitFilename.slice(i)).join('/'); + } + } + + interface ProjectOptions { + // these fields can be present in the project file + files?: string[]; + formatCodeOptions?: ts.FormatCodeOptions; + compilerOptions?: ts.CompilerOptions; + } + + export class Project { + compilerService: CompilerService; + projectOptions: ProjectOptions; + projectFilename: string; + program: ts.Program; + filenameToSourceFile: ts.Map = {}; + updateGraphSeq = 0; + + constructor(public projectService: ProjectService) { + this.compilerService = new CompilerService(this); + } + + openReferencedFile(filename: string) { + return this.projectService.openFile(filename, false); + } + + getSourceFile(info: ScriptInfo) { + return this.filenameToSourceFile[info.fileName]; + } + + getSourceFileFromName(filename: string) { + var info = this.projectService.getScriptInfo(filename); + if (info) { + return this.getSourceFile(info); + } + } + + removeReferencedFile(info: ScriptInfo) { + this.compilerService.host.removeReferencedFile(info); + this.updateGraph(); + } + + updateFileMap() { + this.filenameToSourceFile = {}; + var sourceFiles = this.program.getSourceFiles(); + for (var i = 0, len = sourceFiles.length; i < len; i++) { + var normFilename = ts.normalizePath(sourceFiles[i].fileName); + this.filenameToSourceFile[normFilename] = sourceFiles[i]; + } + } + + finishGraph() { + this.updateGraph(); + this.compilerService.languageService.getNavigateToItems(".*"); + } + + updateGraph() { + this.program = this.compilerService.languageService.getProgram(); + this.updateFileMap(); + } + + isConfiguredProject() { + return this.projectFilename; + } + + // add a root file to project + addRoot(info: ScriptInfo) { + info.defaultProject = this; + return this.compilerService.host.addRoot(info); + } + + filesToString() { + var strBuilder = ""; + ts.forEachValue(this.filenameToSourceFile, + sourceFile => { strBuilder += sourceFile.fileName + "\n"; }); + return strBuilder; + } + + setProjectOptions(projectOptions: ProjectOptions) { + this.projectOptions = projectOptions; + if (projectOptions.compilerOptions) { + this.compilerService.setCompilerOptions(projectOptions.compilerOptions); + } + // TODO: format code options + } + } + + interface ProjectOpenResult { + success?: boolean; + errorMsg?: string; + project?: Project; + } + + function copyListRemovingItem(item: T, list: T[]) { + var copiedList: T[] = []; + for (var i = 0, len = list.length; i < len; i++) { + if (list[i] != item) { + copiedList.push(list[i]); + } + } + return copiedList; + } + + interface ProjectServiceEventHandler { + (eventName: string, project: Project): void; + } + + export class ProjectService { + filenameToScriptInfo: ts.Map = {}; + // open, non-configured files in two lists + openFileRoots: ScriptInfo[] = []; + openFilesReferenced: ScriptInfo[] = []; + // projects covering open files + inferredProjects: Project[] = []; + + constructor(public host: ServerHost, public psLogger: Logger, public eventHandler?: ProjectServiceEventHandler) { + // ts.disableIncrementalParsing = true; + } + + watchedFileChanged(fileName: string) { + var info = this.filenameToScriptInfo[fileName]; + if (!info) { + this.psLogger.info("Error: got watch notification for unknown file: " + fileName); + } + + if (!this.host.fileExists(fileName)) { + // File was deleted + this.fileDeletedInFilesystem(info); + } + else { + if (info && (!info.isOpen)) { + info.svc.reloadFromFile(info.fileName); + } + } + } + + + log(msg: string, type = "Err") { + this.psLogger.msg(msg, type); + } + + closeLog() { + this.psLogger.close(); + } + + createInferredProject(root: ScriptInfo) { + var iproj = new Project(this); + iproj.addRoot(root); + iproj.finishGraph(); + this.inferredProjects.push(iproj); + return iproj; + } + + fileDeletedInFilesystem(info: ScriptInfo) { + this.psLogger.info(info.fileName + " deleted"); + + if (info.fileWatcher) { + info.fileWatcher.close(); + info.fileWatcher = undefined; + } + + if (!info.isOpen) { + this.filenameToScriptInfo[info.fileName] = undefined; + var referencingProjects = this.findReferencingProjects(info); + for (var i = 0, len = referencingProjects.length; i < len; i++) { + referencingProjects[i].removeReferencedFile(info); + } + } + this.printProjects(); + } + + addOpenFile(info: ScriptInfo) { + this.findReferencingProjects(info); + if (info.defaultProject) { + this.openFilesReferenced.push(info); + } + else { + // create new inferred project p with the newly opened file as root + info.defaultProject = this.createInferredProject(info); + var openFileRoots: ScriptInfo[] = []; + // for each inferred project root r + for (var i = 0, len = this.openFileRoots.length; i < len; i++) { + var r = this.openFileRoots[i]; + // if r referenced by the new project + if (info.defaultProject.getSourceFile(r)) { + // remove project rooted at r + this.inferredProjects = + copyListRemovingItem(r.defaultProject, this.inferredProjects); + // put r in referenced open file list + this.openFilesReferenced.push(r); + // set default project of r to the new project + r.defaultProject = info.defaultProject; + } + else { + // otherwise, keep r as root of inferred project + openFileRoots.push(r); + } + } + this.openFileRoots = openFileRoots; + this.openFileRoots.push(info); + } + } + + closeOpenFile(info: ScriptInfo) { + var openFileRoots: ScriptInfo[] = []; + var removedProject: Project; + for (var i = 0, len = this.openFileRoots.length; i < len; i++) { + // if closed file is root of project + if (info == this.openFileRoots[i]) { + // remove that project and remember it + removedProject = info.defaultProject; + } + else { + openFileRoots.push(this.openFileRoots[i]); + } + } + this.openFileRoots = openFileRoots; + if (removedProject) { + // remove project from inferred projects list + this.inferredProjects = copyListRemovingItem(removedProject, this.inferredProjects); + var openFilesReferenced: ScriptInfo[] = []; + var orphanFiles: ScriptInfo[] = []; + // for all open, referenced files f + for (var i = 0, len = this.openFilesReferenced.length; i < len; i++) { + var f = this.openFilesReferenced[i]; + // if f was referenced by the removed project, remember it + if (f.defaultProject == removedProject) { + f.defaultProject = undefined; + orphanFiles.push(f); + } + else { + // otherwise add it back to the list of referenced files + openFilesReferenced.push(f); + } + } + this.openFilesReferenced = openFilesReferenced; + // treat orphaned files as newly opened + for (var i = 0, len = orphanFiles.length; i < len; i++) { + this.addOpenFile(orphanFiles[i]); + } + } + else { + this.openFilesReferenced = copyListRemovingItem(info, this.openFilesReferenced); + } + info.close(); + } + + findReferencingProjects(info: ScriptInfo) { + var referencingProjects: Project[] = []; + info.defaultProject = undefined; + for (var i = 0, len = this.inferredProjects.length; i < len; i++) { + this.inferredProjects[i].updateGraph(); + if (this.inferredProjects[i].getSourceFile(info)) { + info.defaultProject = this.inferredProjects[i]; + referencingProjects.push(this.inferredProjects[i]); + } + } + return referencingProjects; + } + + getScriptInfo(filename: string) { + filename = ts.normalizePath(filename); + return ts.lookUp(this.filenameToScriptInfo, filename); + } + + /** + * @param filename is absolute pathname + */ + openFile(fileName: string, openedByClient = false) { + fileName = ts.normalizePath(fileName); + var info = ts.lookUp(this.filenameToScriptInfo, fileName); + if (!info) { + var content: string; + if (this.host.fileExists(fileName)) { + content = this.host.readFile(fileName); + } + if (!content) { + if (openedByClient) { + content = ""; + } + } + if (content !== undefined) { + info = new ScriptInfo(this.host, fileName, content, openedByClient); + this.filenameToScriptInfo[fileName] = info; + if (!info.isOpen) { + info.fileWatcher = this.host.watchFile(fileName, _ => { this.watchedFileChanged(fileName); }); + } + } + } + if (info) { + if (openedByClient) { + info.isOpen = true; + } + } + return info; + } + + /** + * Open file whose contents is managed by the client + * @param filename is absolute pathname + */ + + openClientFile(filename: string) { + // TODO: tsconfig check + var info = this.openFile(filename, true); + this.addOpenFile(info); + this.printProjects(); + return info; + } + + /** + * Close file whose contents is managed by the client + * @param filename is absolute pathname + */ + + closeClientFile(filename: string) { + // TODO: tsconfig check + var info = ts.lookUp(this.filenameToScriptInfo, filename); + if (info) { + this.closeOpenFile(info); + info.isOpen = false; + } + this.printProjects(); + } + + getProjectsReferencingFile(filename: string) { + var scriptInfo = ts.lookUp(this.filenameToScriptInfo, filename); + if (scriptInfo) { + var projects: Project[] = []; + for (var i = 0, len = this.inferredProjects.length; i < len; i++) { + if (this.inferredProjects[i].getSourceFile(scriptInfo)) { + projects.push(this.inferredProjects[i]); + } + } + return projects; + } + } + + getProjectForFile(filename: string) { + var scriptInfo = ts.lookUp(this.filenameToScriptInfo, filename); + if (scriptInfo) { + return scriptInfo.defaultProject; + } + } + + printProjectsForFile(filename: string) { + var scriptInfo = ts.lookUp(this.filenameToScriptInfo, filename); + if (scriptInfo) { + this.psLogger.startGroup(); + this.psLogger.info("Projects for " + filename) + var projects = this.getProjectsReferencingFile(filename); + for (var i = 0, len = projects.length; i < len; i++) { + this.psLogger.info("Inferred Project " + i.toString()); + } + this.psLogger.endGroup(); + } + else { + this.psLogger.info(filename + " not in any project"); + } + } + + printProjects() { + this.psLogger.startGroup(); + for (var i = 0, len = this.inferredProjects.length; i < len; i++) { + var project = this.inferredProjects[i]; + this.psLogger.info("Project " + i.toString()); + this.psLogger.info(project.filesToString()); + this.psLogger.info("-----------------------------------------------"); + } + this.psLogger.info("Open file roots: ") + for (var i = 0, len = this.openFileRoots.length; i < len; i++) { + this.psLogger.info(this.openFileRoots[i].fileName); + } + this.psLogger.info("Open files referenced: ") + for (var i = 0, len = this.openFilesReferenced.length; i < len; i++) { + this.psLogger.info(this.openFilesReferenced[i].fileName); + } + this.psLogger.endGroup(); + } + + openConfigFile(configFilename: string): ProjectOpenResult { + configFilename = ts.normalizePath(configFilename); + // file references will be relative to dirPath (or absolute) + var dirPath = ts.getDirectoryPath(configFilename); + var rawConfig = ts.readConfigFile(configFilename); + if (!rawConfig) { + return { errorMsg: "tsconfig syntax error" }; + } + else { + // REVIEW: specify no base path so can get absolute path below + var parsedCommandLine = ts.parseConfigFile(rawConfig); + + if (parsedCommandLine.errors) { + // TODO: gather diagnostics and transmit + return { errorMsg: "tsconfig option errors" }; + } + else if (parsedCommandLine.fileNames) { + var proj = this.createProject(configFilename); + for (var i = 0, len = parsedCommandLine.fileNames.length; i < len; i++) { + var rootFilename = parsedCommandLine.fileNames[i]; + var normRootFilename = ts.normalizePath(rootFilename); + normRootFilename = getAbsolutePath(normRootFilename, dirPath); + if (this.host.fileExists(normRootFilename)) { + var info = this.openFile(normRootFilename); + proj.addRoot(info); + } + else { + return { errorMsg: "specified file " + rootFilename + " not found" }; + } + } + var projectOptions: ProjectOptions = { + files: parsedCommandLine.fileNames, + compilerOptions: parsedCommandLine.options + }; + if (rawConfig.formatCodeOptions) { + projectOptions.formatCodeOptions = rawConfig.formatCodeOptions; + } + proj.setProjectOptions(projectOptions); + return { success: true, project: proj }; + } + else { + return { errorMsg: "no files found" }; + } + } + } + + createProject(projectFilename: string) { + var eproj = new Project(this); + eproj.projectFilename = projectFilename; + return eproj; + } + + } + + class CompilerService { + host: LSHost; + languageService: ts.LanguageService; + classifier: ts.Classifier; + settings = ts.getDefaultCompilerOptions(); + documentRegistry = ts.createDocumentRegistry(); + formatCodeOptions: ts.FormatCodeOptions = CompilerService.defaultFormatCodeOptions; + + constructor(public project: Project) { + this.host = new LSHost(project.projectService.host, project); + // override default ES6 (remove when compiler default back at ES5) + this.settings.target = ts.ScriptTarget.ES5; + this.host.setCompilationSettings(this.settings); + this.languageService = ts.createLanguageService(this.host, this.documentRegistry); + this.classifier = ts.createClassifier(); + } + + setCompilerOptions(opt: ts.CompilerOptions) { + this.settings = opt; + this.host.setCompilationSettings(opt); + } + + isExternalModule(filename: string): boolean { + var sourceFile = this.languageService.getSourceFile(filename); + return ts.isExternalModule(sourceFile); + } + + static defaultFormatCodeOptions: ts.FormatCodeOptions = { + IndentSize: 4, + TabSize: 4, + NewLineCharacter: ts.sys.newLine, + ConvertTabsToSpaces: true, + InsertSpaceAfterCommaDelimiter: true, + InsertSpaceAfterSemicolonInForStatements: true, + InsertSpaceBeforeAndAfterBinaryOperators: true, + InsertSpaceAfterKeywordsInControlFlowStatements: true, + InsertSpaceAfterFunctionKeywordForAnonymousFunctions: false, + InsertSpaceAfterOpeningAndBeforeClosingNonemptyParenthesis: false, + PlaceOpenBraceOnNewLineForFunctions: false, + PlaceOpenBraceOnNewLineForControlBlocks: false, + } + + } + + interface LineCollection { + charCount(): number; + lineCount(): number; + isLeaf(): boolean; + walk(rangeStart: number, rangeLength: number, walkFns: ILineIndexWalker): void; + } + + export interface ILineInfo { + line: number; + col: number; + text?: string; + leaf?: LineLeaf; + } + + enum CharRangeSection { + PreStart, + Start, + Entire, + Mid, + End, + PostEnd + } + + interface ILineIndexWalker { + goSubtree: boolean; + done: boolean; + leaf(relativeStart: number, relativeLength: number, lineCollection: LineLeaf): void; + pre? (relativeStart: number, relativeLength: number, lineCollection: LineCollection, + parent: LineNode, nodeType: CharRangeSection): LineCollection; + post? (relativeStart: number, relativeLength: number, lineCollection: LineCollection, + parent: LineNode, nodeType: CharRangeSection): LineCollection; + } + + class BaseLineIndexWalker implements ILineIndexWalker { + goSubtree = true; + done = false; + leaf(rangeStart: number, rangeLength: number, ll: LineLeaf) { + } + } + + class EditWalker extends BaseLineIndexWalker { + lineIndex = new LineIndex(); + // path to start of range + startPath: LineCollection[]; + endBranch: LineCollection[] = []; + branchNode: LineNode; + // path to current node + stack: LineNode[]; + state = CharRangeSection.Entire; + lineCollectionAtBranch: LineCollection; + initialText = ""; + trailingText = ""; + suppressTrailingText = false; + + constructor() { + super(); + this.lineIndex.root = new LineNode(); + this.startPath = [this.lineIndex.root]; + this.stack = [this.lineIndex.root]; + } + + insertLines(insertedText: string) { + if (this.suppressTrailingText) { + this.trailingText = ""; + } + if (insertedText) { + insertedText = this.initialText + insertedText + this.trailingText; + } + else { + insertedText = this.initialText + this.trailingText; + } + var lm = LineIndex.linesFromText(insertedText); + var lines = lm.lines; + if (lines.length > 1) { + if (lines[lines.length - 1] == "") { + lines.length--; + } + } + var branchParent: LineNode; + var lastZeroCount: LineCollection; + + for (var k = this.endBranch.length - 1; k >= 0; k--) { + (this.endBranch[k]).updateCounts(); + if (this.endBranch[k].charCount() == 0) { + lastZeroCount = this.endBranch[k]; + if (k > 0) { + branchParent = this.endBranch[k - 1]; + } + else { + branchParent = this.branchNode; + } + } + } + if (lastZeroCount) { + branchParent.remove(lastZeroCount); + } + + // path at least length two (root and leaf) + var insertionNode = this.startPath[this.startPath.length - 2]; + var leafNode = this.startPath[this.startPath.length - 1]; + var len = lines.length; + + if (len > 0) { + leafNode.text = lines[0]; + + if (len > 1) { + var insertedNodes = new Array(len - 1); + var startNode = leafNode; + for (var i = 1, len = lines.length; i < len; i++) { + insertedNodes[i - 1] = new LineLeaf(lines[i]); + } + var pathIndex = this.startPath.length - 2; + while (pathIndex >= 0) { + insertionNode = this.startPath[pathIndex]; + insertedNodes = insertionNode.insertAt(startNode, insertedNodes); + pathIndex--; + startNode = insertionNode; + } + var insertedNodesLen = insertedNodes.length; + while (insertedNodesLen > 0) { + var newRoot = new LineNode(); + newRoot.add(this.lineIndex.root); + insertedNodes = newRoot.insertAt(this.lineIndex.root, insertedNodes); + insertedNodesLen = insertedNodes.length; + this.lineIndex.root = newRoot; + } + this.lineIndex.root.updateCounts(); + } + else { + for (var j = this.startPath.length - 2; j >= 0; j--) { + (this.startPath[j]).updateCounts(); + } + } + } + else { + // no content for leaf node, so delete it + insertionNode.remove(leafNode); + for (var j = this.startPath.length - 2; j >= 0; j--) { + (this.startPath[j]).updateCounts(); + } + } + + return this.lineIndex; + } + + post(relativeStart: number, relativeLength: number, lineCollection: LineCollection, parent: LineCollection, nodeType: CharRangeSection): LineCollection { + // have visited the path for start of range, now looking for end + // if range is on single line, we will never make this state transition + if (lineCollection == this.lineCollectionAtBranch) { + this.state = CharRangeSection.End; + } + // always pop stack because post only called when child has been visited + this.stack.length--; + return undefined; + } + + pre(relativeStart: number, relativeLength: number, lineCollection: LineCollection, parent: LineCollection, nodeType: CharRangeSection) { + // currentNode corresponds to parent, but in the new tree + var currentNode = this.stack[this.stack.length - 1]; + + if ((this.state == CharRangeSection.Entire) && (nodeType == CharRangeSection.Start)) { + // if range is on single line, we will never make this state transition + this.state = CharRangeSection.Start; + this.branchNode = currentNode; + this.lineCollectionAtBranch = lineCollection; + } + + var child: LineCollection; + function fresh(node: LineCollection): LineCollection { + if (node.isLeaf()) { + return new LineLeaf(""); + } + else return new LineNode(); + } + switch (nodeType) { + case CharRangeSection.PreStart: + this.goSubtree = false; + if (this.state != CharRangeSection.End) { + currentNode.add(lineCollection); + } + break; + case CharRangeSection.Start: + if (this.state == CharRangeSection.End) { + this.goSubtree = false; + } + else { + child = fresh(lineCollection); + currentNode.add(child); + this.startPath[this.startPath.length] = child; + } + break; + case CharRangeSection.Entire: + if (this.state != CharRangeSection.End) { + child = fresh(lineCollection); + currentNode.add(child); + this.startPath[this.startPath.length] = child; + } + else { + if (!lineCollection.isLeaf()) { + child = fresh(lineCollection); + currentNode.add(child); + this.endBranch[this.endBranch.length] = child; + } + } + break; + case CharRangeSection.Mid: + this.goSubtree = false; + break; + case CharRangeSection.End: + if (this.state != CharRangeSection.End) { + this.goSubtree = false; + } + else { + if (!lineCollection.isLeaf()) { + child = fresh(lineCollection); + currentNode.add(child); + this.endBranch[this.endBranch.length] = child; + } + } + break; + case CharRangeSection.PostEnd: + this.goSubtree = false; + if (this.state != CharRangeSection.Start) { + currentNode.add(lineCollection); + } + break; + } + if (this.goSubtree) { + this.stack[this.stack.length] = child; + } + return lineCollection; + } + // just gather text from the leaves + leaf(relativeStart: number, relativeLength: number, ll: LineLeaf) { + if (this.state == CharRangeSection.Start) { + this.initialText = ll.text.substring(0, relativeStart); + } + else if (this.state == CharRangeSection.Entire) { + this.initialText = ll.text.substring(0, relativeStart); + this.trailingText = ll.text.substring(relativeStart + relativeLength); + } + else { + // state is CharRangeSection.End + this.trailingText = ll.text.substring(relativeStart + relativeLength); + } + } + } + + // text change information + class TextChange { + constructor(public pos: number, public deleteLen: number, public insertedText?: string) { + } + + getTextChangeRange() { + return ts.createTextChangeRange(ts.createTextSpan(this.pos, this.deleteLen), + this.insertedText ? this.insertedText.length : 0); + } + } + + class ScriptVersionCache { + changes: TextChange[] = []; + versions: LineIndexSnapshot[] = []; + minVersion = 0; // no versions earlier than min version will maintain change history + private currentVersion = 0; + + static changeNumberThreshold = 8; + static changeLengthThreshold = 256; + + // REVIEW: can optimize by coalescing simple edits + edit(pos: number, deleteLen: number, insertedText?: string) { + this.changes[this.changes.length] = new TextChange(pos, deleteLen, insertedText); + if ((this.changes.length > ScriptVersionCache.changeNumberThreshold) || + (deleteLen > ScriptVersionCache.changeLengthThreshold) || + (insertedText && (insertedText.length > ScriptVersionCache.changeLengthThreshold))) { + this.getSnapshot(); + } + } + + latest() { + return this.versions[this.currentVersion]; + } + + latestVersion() { + if (this.changes.length > 0) { + this.getSnapshot(); + } + return this.currentVersion; + } + + reloadFromFile(filename: string, cb?: () => any) { + var content = ts.sys.readFile(filename); + this.reload(content); + if (cb) + cb(); + } + + // reload whole script, leaving no change history behind reload + reload(script: string) { + this.currentVersion++; + this.changes = []; // history wiped out by reload + var snap = new LineIndexSnapshot(this.currentVersion, this); + this.versions[this.currentVersion] = snap; + snap.index = new LineIndex(); + var lm = LineIndex.linesFromText(script); + snap.index.load(lm.lines); + // REVIEW: could use linked list + for (var i = this.minVersion; i < this.currentVersion; i++) { + this.versions[i] = undefined; + } + this.minVersion = this.currentVersion; + + } + + getSnapshot() { + var snap = this.versions[this.currentVersion]; + if (this.changes.length > 0) { + var snapIndex = this.latest().index; + for (var i = 0, len = this.changes.length; i < len; i++) { + var change = this.changes[i]; + snapIndex = snapIndex.edit(change.pos, change.deleteLen, change.insertedText); + } + snap = new LineIndexSnapshot(this.currentVersion + 1, this); + snap.index = snapIndex; + snap.changesSincePreviousVersion = this.changes; + this.currentVersion = snap.version; + this.versions[snap.version] = snap; + this.changes = []; + } + return snap; + } + + getTextChangesBetweenVersions(oldVersion: number, newVersion: number) { + if (oldVersion < newVersion) { + if (oldVersion >= this.minVersion) { + var textChangeRanges: ts.TextChangeRange[] = []; + for (var i = oldVersion + 1; i <= newVersion; i++) { + var snap = this.versions[i]; + for (var j = 0, len = snap.changesSincePreviousVersion.length; j < len; j++) { + var textChange = snap.changesSincePreviousVersion[j]; + textChangeRanges[textChangeRanges.length] = textChange.getTextChangeRange(); + } + } + return ts.collapseTextChangeRangesAcrossMultipleVersions(textChangeRanges); + } + else { + return undefined; + } + } + else { + return ts.unchangedTextChangeRange; + } + } + + static fromString(script: string) { + var svc = new ScriptVersionCache(); + var snap = new LineIndexSnapshot(0, svc); + svc.versions[svc.currentVersion] = snap; + snap.index = new LineIndex(); + var lm = LineIndex.linesFromText(script); + snap.index.load(lm.lines); + return svc; + } + } + + class LineIndexSnapshot implements ts.IScriptSnapshot { + index: LineIndex; + changesSincePreviousVersion: TextChange[] = []; + + constructor(public version: number, public cache: ScriptVersionCache) { + } + + getText(rangeStart: number, rangeEnd: number) { + return this.index.getText(rangeStart, rangeEnd - rangeStart); + } + + getLength() { + return this.index.root.charCount(); + } + + // this requires linear space so don't hold on to these + getLineStartPositions(): number[] { + var starts: number[] = [-1]; + var count = 1; + var pos = 0; + this.index.every((ll, s, len) => { + starts[count++] = pos; + pos += ll.text.length; + return true; + }, 0); + return starts; + } + + getLineMapper() { + return ((line: number) => { + return this.index.lineNumberToInfo(line).col; + }); + } + + getTextChangeRangeSinceVersion(scriptVersion: number) { + if (this.version <= scriptVersion) { + return ts.unchangedTextChangeRange; + } + else { + return this.cache.getTextChangesBetweenVersions(scriptVersion, this.version); + } + } + getChangeRange(oldSnapshot: ts.IScriptSnapshot): ts.TextChangeRange { + var oldSnap = oldSnapshot; + return this.getTextChangeRangeSinceVersion(oldSnap.version); + } + } + + class LineIndex { + root: LineNode; + // set this to true to check each edit for accuracy + checkEdits = false; + + charOffsetToLineNumberAndPos(charOffset: number) { + return this.root.charOffsetToLineNumberAndPos(1, charOffset); + } + + lineNumberToInfo(lineNumber: number): ILineInfo { + var lineCount = this.root.lineCount(); + if (lineNumber <= lineCount) { + var lineInfo = this.root.lineNumberToInfo(lineNumber, 0); + lineInfo.line = lineNumber; + return lineInfo; + } + else { + return { + line: lineNumber, + col: this.root.charCount() + } + } + } + + load(lines: string[]) { + if (lines.length > 0) { + var leaves: LineLeaf[] = []; + for (var i = 0, len = lines.length; i < len; i++) { + leaves[i] = new LineLeaf(lines[i]); + } + this.root = LineIndex.buildTreeFromBottom(leaves); + } + else { + this.root = new LineNode(); + } + } + + walk(rangeStart: number, rangeLength: number, walkFns: ILineIndexWalker) { + this.root.walk(rangeStart, rangeLength, walkFns); + } + + getText(rangeStart: number, rangeLength: number) { + var accum = ""; + if (rangeLength > 0) { + this.walk(rangeStart, rangeLength, { + goSubtree: true, + done: false, + leaf: (relativeStart: number, relativeLength: number, ll: LineLeaf) => { + accum = accum.concat(ll.text.substring(relativeStart, relativeStart + relativeLength)); + } + }); + } + return accum; + } + + every(f: (ll: LineLeaf, s: number, len: number) => boolean, rangeStart: number, rangeEnd?: number) { + if (!rangeEnd) { + rangeEnd = this.root.charCount(); + } + var walkFns = { + goSubtree: true, + done: false, + leaf: function (relativeStart: number, relativeLength: number, ll: LineLeaf) { + if (!f(ll, relativeStart, relativeLength)) { + this.done = true; + } + } + } + this.walk(rangeStart, rangeEnd - rangeStart, walkFns); + return !walkFns.done; + } + + edit(pos: number, deleteLength: number, newText?: string) { + function editFlat(source: string, s: number, dl: number, nt = "") { + return source.substring(0, s) + nt + source.substring(s + dl, source.length); + } + if (this.root.charCount() == 0) { + // TODO: assert deleteLength == 0 + if (newText) { + this.load(LineIndex.linesFromText(newText).lines); + return this; + } + } + else { + if (this.checkEdits) { + var checkText = editFlat(this.getText(0, this.root.charCount()), pos, deleteLength, newText); + } + var walker = new EditWalker(); + if (deleteLength > 0) { + // check whether last characters deleted are line break + var e = pos + deleteLength; + var lineInfo = this.charOffsetToLineNumberAndPos(e); + if ((lineInfo && (lineInfo.col == 0))) { + // move range end just past line that will merge with previous line + deleteLength += lineInfo.text.length; + // store text by appending to end of insertedText + if (newText) { + newText = newText + lineInfo.text; + } + else { + newText = lineInfo.text; + } + } + } + else if (pos >= this.root.charCount()) { + // insert at end + var endString = this.getText(pos - 1, 1); + if (newText) { + newText = endString + newText; + } + else { + newText = endString; + } + pos = pos - 1; + deleteLength = 0; + walker.suppressTrailingText = true; + } + this.root.walk(pos, deleteLength, walker); + walker.insertLines(newText); + if (this.checkEdits) { + var updatedText = this.getText(0, this.root.charCount()); + Debug.assert(checkText == updatedText, "buffer edit mismatch"); + } + return walker.lineIndex; + } + } + + static buildTreeFromBottom(nodes: LineCollection[]): LineNode { + var nodeCount = Math.ceil(nodes.length / lineCollectionCapacity); + var interiorNodes: LineNode[] = []; + var nodeIndex = 0; + for (var i = 0; i < nodeCount; i++) { + interiorNodes[i] = new LineNode(); + var charCount = 0; + var lineCount = 0; + for (var j = 0; j < lineCollectionCapacity; j++) { + if (nodeIndex < nodes.length) { + interiorNodes[i].add(nodes[nodeIndex]); + charCount += nodes[nodeIndex].charCount(); + lineCount += nodes[nodeIndex].lineCount(); + } + else { + break; + } + nodeIndex++; + } + interiorNodes[i].totalChars = charCount; + interiorNodes[i].totalLines = lineCount; + } + if (interiorNodes.length == 1) { + return interiorNodes[0]; + } + else { + return this.buildTreeFromBottom(interiorNodes); + } + } + + static linesFromText(text: string) { + var lineStarts = ts.computeLineStarts(text); + + if (lineStarts.length == 0) { + return { lines: [], lineMap: lineStarts }; + } + var lines = new Array(lineStarts.length); + var lc = lineStarts.length - 1; + for (var lmi = 0; lmi < lc; lmi++) { + lines[lmi] = text.substring(lineStarts[lmi], lineStarts[lmi + 1]); + } + + var endText = text.substring(lineStarts[lc]); + if (endText.length > 0) { + lines[lc] = endText; + } + else { + lines.length--; + } + return { lines: lines, lineMap: lineStarts }; + } + } + + class LineNode implements LineCollection { + totalChars = 0; + totalLines = 0; + children: LineCollection[] = []; + + isLeaf() { + return false; + } + + updateCounts() { + this.totalChars = 0; + this.totalLines = 0; + for (var i = 0, len = this.children.length; i < len; i++) { + var child = this.children[i]; + this.totalChars += child.charCount(); + this.totalLines += child.lineCount(); + } + } + + execWalk(rangeStart: number, rangeLength: number, walkFns: ILineIndexWalker, childIndex: number, nodeType: CharRangeSection) { + if (walkFns.pre) { + walkFns.pre(rangeStart, rangeLength, this.children[childIndex], this, nodeType); + } + if (walkFns.goSubtree) { + this.children[childIndex].walk(rangeStart, rangeLength, walkFns); + if (walkFns.post) { + walkFns.post(rangeStart, rangeLength, this.children[childIndex], this, nodeType); + } + } + else { + walkFns.goSubtree = true; + } + return walkFns.done; + } + + skipChild(relativeStart: number, relativeLength: number, childIndex: number, walkFns: ILineIndexWalker, nodeType: CharRangeSection) { + if (walkFns.pre && (!walkFns.done)) { + walkFns.pre(relativeStart, relativeLength, this.children[childIndex], this, nodeType); + walkFns.goSubtree = true; + } + } + + walk(rangeStart: number, rangeLength: number, walkFns: ILineIndexWalker) { + // assume (rangeStart < this.totalChars) && (rangeLength <= this.totalChars) + var childIndex = 0; + var child = this.children[0]; + var childCharCount = child.charCount(); + // find sub-tree containing start + var adjustedStart = rangeStart; + while (adjustedStart >= childCharCount) { + this.skipChild(adjustedStart, rangeLength, childIndex, walkFns, CharRangeSection.PreStart); + adjustedStart -= childCharCount; + child = this.children[++childIndex]; + childCharCount = child.charCount(); + } + // Case I: both start and end of range in same subtree + if ((adjustedStart + rangeLength) <= childCharCount) { + if (this.execWalk(adjustedStart, rangeLength, walkFns, childIndex, CharRangeSection.Entire)) { + return; + } + } + else { + // Case II: start and end of range in different subtrees (possibly with subtrees in the middle) + if (this.execWalk(adjustedStart, childCharCount - adjustedStart, walkFns, childIndex, CharRangeSection.Start)) { + return; + } + var adjustedLength = rangeLength - (childCharCount - adjustedStart); + child = this.children[++childIndex]; + childCharCount = child.charCount(); + while (adjustedLength > childCharCount) { + if (this.execWalk(0, childCharCount, walkFns, childIndex, CharRangeSection.Mid)) { + return; + } + adjustedLength -= childCharCount; + child = this.children[++childIndex]; + childCharCount = child.charCount(); + } + if (adjustedLength > 0) { + if (this.execWalk(0, adjustedLength, walkFns, childIndex, CharRangeSection.End)) { + return; + } + } + } + // Process any subtrees after the one containing range end + if (walkFns.pre) { + var clen = this.children.length; + if (childIndex < (clen - 1)) { + for (var ej = childIndex + 1; ej < clen; ej++) { + this.skipChild(0, 0, ej, walkFns, CharRangeSection.PostEnd); + } + } + } + } + + charOffsetToLineNumberAndPos(lineNumber: number, charOffset: number): ILineInfo { + var childInfo = this.childFromCharOffset(lineNumber, charOffset); + if (!childInfo.child) { + return { + line: lineNumber, + col: charOffset, + } + } + else if (childInfo.childIndex < this.children.length) { + if (childInfo.child.isLeaf()) { + return { + line: childInfo.lineNumber, + col: childInfo.charOffset, + text: ((childInfo.child)).text, + leaf: ((childInfo.child)) + }; + } + else { + var lineNode = (childInfo.child); + return lineNode.charOffsetToLineNumberAndPos(childInfo.lineNumber, childInfo.charOffset); + } + } + else { + var lineInfo = this.lineNumberToInfo(this.lineCount(), 0); + return { line: this.lineCount(), col: lineInfo.leaf.charCount() }; + } + } + + lineNumberToInfo(lineNumber: number, charOffset: number): ILineInfo { + var childInfo = this.childFromLineNumber(lineNumber, charOffset); + if (!childInfo.child) { + return { + line: lineNumber, + col: charOffset + } + } + else if (childInfo.child.isLeaf()) { + return { + line: lineNumber, + col: childInfo.charOffset, + text: ((childInfo.child)).text, + leaf: ((childInfo.child)) + } + } + else { + var lineNode = (childInfo.child); + return lineNode.lineNumberToInfo(childInfo.relativeLineNumber, childInfo.charOffset); + } + } + + childFromLineNumber(lineNumber: number, charOffset: number) { + var child: LineCollection; + var relativeLineNumber = lineNumber; + for (var i = 0, len = this.children.length; i < len; i++) { + child = this.children[i]; + var childLineCount = child.lineCount(); + if (childLineCount >= relativeLineNumber) { + break; + } + else { + relativeLineNumber -= childLineCount; + charOffset += child.charCount(); + } + } + return { + child: child, + childIndex: i, + relativeLineNumber: relativeLineNumber, + charOffset: charOffset + }; + } + + childFromCharOffset(lineNumber: number, charOffset: number) { + var child: LineCollection; + for (var i = 0, len = this.children.length; i < len; i++) { + child = this.children[i]; + if (child.charCount() > charOffset) { + break; + } + else { + charOffset -= child.charCount(); + lineNumber += child.lineCount(); + } + } + return { + child: child, + childIndex: i, + charOffset: charOffset, + lineNumber: lineNumber + } + } + + splitAfter(childIndex: number) { + var splitNode: LineNode; + var clen = this.children.length; + childIndex++; + var endLength = childIndex; + if (childIndex < clen) { + splitNode = new LineNode(); + while (childIndex < clen) { + splitNode.add(this.children[childIndex++]); + } + splitNode.updateCounts(); + } + this.children.length = endLength; + return splitNode; + } + + remove(child: LineCollection) { + var childIndex = this.findChildIndex(child); + var clen = this.children.length; + if (childIndex < (clen - 1)) { + for (var i = childIndex; i < (clen - 1); i++) { + this.children[i] = this.children[i + 1]; + } + } + this.children.length--; + } + + findChildIndex(child: LineCollection) { + var childIndex = 0; + var clen = this.children.length; + while ((this.children[childIndex] != child) && (childIndex < clen)) childIndex++; + return childIndex; + } + + insertAt(child: LineCollection, nodes: LineCollection[]) { + var childIndex = this.findChildIndex(child); + var clen = this.children.length; + var nodeCount = nodes.length; + // if child is last and there is more room and only one node to place, place it + if ((clen < lineCollectionCapacity) && (childIndex == (clen - 1)) && (nodeCount == 1)) { + this.add(nodes[0]); + this.updateCounts(); + return []; + } + else { + var shiftNode = this.splitAfter(childIndex); + var nodeIndex = 0; + childIndex++; + while ((childIndex < lineCollectionCapacity) && (nodeIndex < nodeCount)) { + this.children[childIndex++] = nodes[nodeIndex++]; + } + var splitNodes: LineNode[] = []; + var splitNodeCount = 0; + if (nodeIndex < nodeCount) { + splitNodeCount = Math.ceil((nodeCount - nodeIndex) / lineCollectionCapacity); + splitNodes = new Array(splitNodeCount); + var splitNodeIndex = 0; + for (var i = 0; i < splitNodeCount; i++) { + splitNodes[i] = new LineNode(); + } + var splitNode = splitNodes[0]; + while (nodeIndex < nodeCount) { + splitNode.add(nodes[nodeIndex++]); + if (splitNode.children.length == lineCollectionCapacity) { + splitNodeIndex++; + splitNode = splitNodes[splitNodeIndex]; + } + } + for (i = splitNodes.length - 1; i >= 0; i--) { + if (splitNodes[i].children.length == 0) { + splitNodes.length--; + } + } + } + if (shiftNode) { + splitNodes[splitNodes.length] = shiftNode; + } + this.updateCounts(); + for (i = 0; i < splitNodeCount; i++) { + (splitNodes[i]).updateCounts(); + } + return splitNodes; + } + } + + // assume there is room for the item; return true if more room + add(collection: LineCollection) { + this.children[this.children.length] = collection; + return (this.children.length < lineCollectionCapacity); + } + + charCount() { + return this.totalChars; + } + + lineCount() { + return this.totalLines; + } + } + + class LineLeaf implements LineCollection { + udata: any; + + constructor(public text: string) { + + } + + setUdata(data: any) { + this.udata = data; + } + + getUdata() { + return this.udata; + } + + isLeaf() { + return true; + } + + walk(rangeStart: number, rangeLength: number, walkFns: ILineIndexWalker) { + walkFns.leaf(rangeStart, rangeLength, this); + } + + charCount() { + return this.text.length; + } + + lineCount() { + return 1; + } + } +} \ No newline at end of file diff --git a/src/server/node.d.ts b/src/server/node.d.ts new file mode 100644 index 0000000000000..2002f973a3789 --- /dev/null +++ b/src/server/node.d.ts @@ -0,0 +1,677 @@ +// Type definitions for Node.js v0.10.1 +// Project: http://nodejs.org/ +// Definitions by: Microsoft TypeScript , DefinitelyTyped +// Definitions: https://github.com/borisyankov/DefinitelyTyped + +/************************************************ +* * +* Node.js v0.10.1 API * +* * +************************************************/ + +/************************************************ +* * +* GLOBAL * +* * +************************************************/ +declare var process: NodeJS.Process; +declare var global: any; + +declare var __filename: string; +declare var __dirname: string; + +declare function setTimeout(callback: (...args: any[]) => void, ms: number, ...args: any[]): NodeJS.Timer; +declare function clearTimeout(timeoutId: NodeJS.Timer): void; +declare function setInterval(callback: (...args: any[]) => void, ms: number, ...args: any[]): NodeJS.Timer; +declare function clearInterval(intervalId: NodeJS.Timer): void; +declare function setImmediate(callback: (...args: any[]) => void, ...args: any[]): any; +declare function clearImmediate(immediateId: any): void; + +declare var require: { + (id: string): any; + resolve(id: string): string; + cache: any; + extensions: any; + main: any; +}; + +declare var module: { + exports: any; + require(id: string): any; + id: string; + filename: string; + loaded: boolean; + parent: any; + children: any[]; +}; + +// Same as module.exports +declare var exports: any; +declare var SlowBuffer: { + new (str: string, encoding?: string): Buffer; + new (size: number): Buffer; + new (size: Uint8Array): Buffer; + new (array: any[]): Buffer; + prototype: Buffer; + isBuffer(obj: any): boolean; + byteLength(string: string, encoding?: string): number; + concat(list: Buffer[], totalLength?: number): Buffer; +}; + + +// Buffer class +interface Buffer extends NodeBuffer { } +interface BufferConstructor { + new (str: string, encoding ?: string): Buffer; + new (size: number): Buffer; + new (size: Uint8Array): Buffer; + new (array: any[]): Buffer; + prototype: Buffer; + isBuffer(obj: any): boolean; + byteLength(string: string, encoding ?: string): number; + concat(list: Buffer[], totalLength ?: number): Buffer; +} +declare var Buffer: BufferConstructor; + +/************************************************ +* * +* GLOBAL INTERFACES * +* * +************************************************/ +declare module NodeJS { + export interface ErrnoException extends Error { + errno?: any; + code?: string; + path?: string; + syscall?: string; + } + + export interface EventEmitter { + addListener(event: string, listener: Function): EventEmitter; + on(event: string, listener: Function): EventEmitter; + once(event: string, listener: Function): EventEmitter; + removeListener(event: string, listener: Function): EventEmitter; + removeAllListeners(event?: string): EventEmitter; + setMaxListeners(n: number): void; + listeners(event: string): Function[]; + emit(event: string, ...args: any[]): boolean; + } + + export interface ReadableStream extends EventEmitter { + readable: boolean; + read(size?: number): any; + setEncoding(encoding: string): void; + pause(): void; + resume(): void; + pipe(destination: T, options?: { end?: boolean; }): T; + unpipe(destination?: T): void; + unshift(chunk: string): void; + unshift(chunk: Buffer): void; + wrap(oldStream: ReadableStream): ReadableStream; + } + + export interface WritableStream extends EventEmitter { + writable: boolean; + write(buffer: Buffer, cb?: Function): boolean; + write(str: string, cb?: Function): boolean; + write(str: string, encoding?: string, cb?: Function): boolean; + end(): void; + end(buffer: Buffer, cb?: Function): void; + end(str: string, cb?: Function): void; + end(str: string, encoding?: string, cb?: Function): void; + } + + export interface ReadWriteStream extends ReadableStream, WritableStream { } + + export interface Process extends EventEmitter { + stdout: WritableStream; + stderr: WritableStream; + stdin: ReadableStream; + argv: string[]; + execPath: string; + abort(): void; + chdir(directory: string): void; + cwd(): string; + env: any; + exit(code?: number): void; + getgid(): number; + setgid(id: number): void; + setgid(id: string): void; + getuid(): number; + setuid(id: number): void; + setuid(id: string): void; + version: string; + versions: { + http_parser: string; + node: string; + v8: string; + ares: string; + uv: string; + zlib: string; + openssl: string; + }; + config: { + target_defaults: { + cflags: any[]; + default_configuration: string; + defines: string[]; + include_dirs: string[]; + libraries: string[]; + }; + variables: { + clang: number; + host_arch: string; + node_install_npm: boolean; + node_install_waf: boolean; + node_prefix: string; + node_shared_openssl: boolean; + node_shared_v8: boolean; + node_shared_zlib: boolean; + node_use_dtrace: boolean; + node_use_etw: boolean; + node_use_openssl: boolean; + target_arch: string; + v8_no_strict_aliasing: number; + v8_use_snapshot: boolean; + visibility: string; + }; + }; + kill(pid: number, signal?: string): void; + pid: number; + title: string; + arch: string; + platform: string; + memoryUsage(): { rss: number; heapTotal: number; heapUsed: number; }; + nextTick(callback: Function): void; + umask(mask?: number): number; + uptime(): number; + hrtime(time?: number[]): number[]; + + // Worker + send? (message: any, sendHandle?: any): void; + } + + export interface Timer { + ref(): void; + unref(): void; + } +} + + +/** + * @deprecated + */ +interface NodeBuffer { + [index: number]: number; + write(string: string, offset?: number, length?: number, encoding?: string): number; + toString(encoding?: string, start?: number, end?: number): string; + toJSON(): any; + length: number; + copy(targetBuffer: Buffer, targetStart?: number, sourceStart?: number, sourceEnd?: number): number; + slice(start?: number, end?: number): Buffer; + readUInt8(offset: number, noAsset?: boolean): number; + readUInt16LE(offset: number, noAssert?: boolean): number; + readUInt16BE(offset: number, noAssert?: boolean): number; + readUInt32LE(offset: number, noAssert?: boolean): number; + readUInt32BE(offset: number, noAssert?: boolean): number; + readInt8(offset: number, noAssert?: boolean): number; + readInt16LE(offset: number, noAssert?: boolean): number; + readInt16BE(offset: number, noAssert?: boolean): number; + readInt32LE(offset: number, noAssert?: boolean): number; + readInt32BE(offset: number, noAssert?: boolean): number; + readFloatLE(offset: number, noAssert?: boolean): number; + readFloatBE(offset: number, noAssert?: boolean): number; + readDoubleLE(offset: number, noAssert?: boolean): number; + readDoubleBE(offset: number, noAssert?: boolean): number; + writeUInt8(value: number, offset: number, noAssert?: boolean): void; + writeUInt16LE(value: number, offset: number, noAssert?: boolean): void; + writeUInt16BE(value: number, offset: number, noAssert?: boolean): void; + writeUInt32LE(value: number, offset: number, noAssert?: boolean): void; + writeUInt32BE(value: number, offset: number, noAssert?: boolean): void; + writeInt8(value: number, offset: number, noAssert?: boolean): void; + writeInt16LE(value: number, offset: number, noAssert?: boolean): void; + writeInt16BE(value: number, offset: number, noAssert?: boolean): void; + writeInt32LE(value: number, offset: number, noAssert?: boolean): void; + writeInt32BE(value: number, offset: number, noAssert?: boolean): void; + writeFloatLE(value: number, offset: number, noAssert?: boolean): void; + writeFloatBE(value: number, offset: number, noAssert?: boolean): void; + writeDoubleLE(value: number, offset: number, noAssert?: boolean): void; + writeDoubleBE(value: number, offset: number, noAssert?: boolean): void; + fill(value: any, offset?: number, end?: number): void; +} + +declare module NodeJS { + export interface Path { + normalize(p: string): string; + join(...paths: any[]): string; + resolve(...pathSegments: any[]): string; + relative(from: string, to: string): string; + dirname(p: string): string; + basename(p: string, ext?: string): string; + extname(p: string): string; + sep: string; + } +} + +declare module NodeJS { + export interface ReadLineInstance extends EventEmitter { + setPrompt(prompt: string, length: number): void; + prompt(preserveCursor?: boolean): void; + question(query: string, callback: Function): void; + pause(): void; + resume(): void; + close(): void; + write(data: any, key?: any): void; + } + export interface ReadLineOptions { + input: NodeJS.ReadableStream; + output: NodeJS.WritableStream; + completer?: Function; + terminal?: boolean; + } + + export interface ReadLine { + createInterface(options: ReadLineOptions): ReadLineInstance; + } +} + +declare module NodeJS { + module events { + export class EventEmitter implements NodeJS.EventEmitter { + static listenerCount(emitter: EventEmitter, event: string): number; + + addListener(event: string, listener: Function): EventEmitter; + on(event: string, listener: Function): EventEmitter; + once(event: string, listener: Function): EventEmitter; + removeListener(event: string, listener: Function): EventEmitter; + removeAllListeners(event?: string): EventEmitter; + setMaxListeners(n: number): void; + listeners(event: string): Function[]; + emit(event: string, ...args: any[]): boolean; + } + } +} + +declare module NodeJS { + module stream { + + export interface Stream extends events.EventEmitter { + pipe(destination: T, options?: { end?: boolean; }): T; + } + + export interface ReadableOptions { + highWaterMark?: number; + encoding?: string; + objectMode?: boolean; + } + + export class Readable extends events.EventEmitter implements NodeJS.ReadableStream { + readable: boolean; + constructor(opts?: ReadableOptions); + _read(size: number): void; + read(size?: number): any; + setEncoding(encoding: string): void; + pause(): void; + resume(): void; + pipe(destination: T, options?: { end?: boolean; }): T; + unpipe(destination?: T): void; + unshift(chunk: string): void; + unshift(chunk: Buffer): void; + wrap(oldStream: NodeJS.ReadableStream): NodeJS.ReadableStream; + push(chunk: any, encoding?: string): boolean; + } + + export interface WritableOptions { + highWaterMark?: number; + decodeStrings?: boolean; + } + + export class Writable extends events.EventEmitter implements NodeJS.WritableStream { + writable: boolean; + constructor(opts?: WritableOptions); + _write(data: Buffer, encoding: string, callback: Function): void; + _write(data: string, encoding: string, callback: Function): void; + write(buffer: Buffer, cb?: Function): boolean; + write(str: string, cb?: Function): boolean; + write(str: string, encoding?: string, cb?: Function): boolean; + end(): void; + end(buffer: Buffer, cb?: Function): void; + end(str: string, cb?: Function): void; + end(str: string, encoding?: string, cb?: Function): void; + } + + export interface DuplexOptions extends ReadableOptions, WritableOptions { + allowHalfOpen?: boolean; + } + + // Note: Duplex extends both Readable and Writable. + export class Duplex extends Readable implements NodeJS.ReadWriteStream { + writable: boolean; + constructor(opts?: DuplexOptions); + _write(data: Buffer, encoding: string, callback: Function): void; + _write(data: string, encoding: string, callback: Function): void; + write(buffer: Buffer, cb?: Function): boolean; + write(str: string, cb?: Function): boolean; + write(str: string, encoding?: string, cb?: Function): boolean; + end(): void; + end(buffer: Buffer, cb?: Function): void; + end(str: string, cb?: Function): void; + end(str: string, encoding?: string, cb?: Function): void; + } + + export interface TransformOptions extends ReadableOptions, WritableOptions { } + + // Note: Transform lacks the _read and _write methods of Readable/Writable. + export class Transform extends events.EventEmitter implements NodeJS.ReadWriteStream { + readable: boolean; + writable: boolean; + constructor(opts?: TransformOptions); + _transform(chunk: Buffer, encoding: string, callback: Function): void; + _transform(chunk: string, encoding: string, callback: Function): void; + _flush(callback: Function): void; + read(size?: number): any; + setEncoding(encoding: string): void; + pause(): void; + resume(): void; + pipe(destination: T, options?: { end?: boolean; }): T; + unpipe(destination?: T): void; + unshift(chunk: string): void; + unshift(chunk: Buffer): void; + wrap(oldStream: NodeJS.ReadableStream): NodeJS.ReadableStream; + push(chunk: any, encoding?: string): boolean; + write(buffer: Buffer, cb?: Function): boolean; + write(str: string, cb?: Function): boolean; + write(str: string, encoding?: string, cb?: Function): boolean; + end(): void; + end(buffer: Buffer, cb?: Function): void; + end(str: string, cb?: Function): void; + end(str: string, encoding?: string, cb?: Function): void; + } + + export class PassThrough extends Transform { } + } +} + +declare module NodeJS { + module fs { + interface Stats { + isFile(): boolean; + isDirectory(): boolean; + isBlockDevice(): boolean; + isCharacterDevice(): boolean; + isSymbolicLink(): boolean; + isFIFO(): boolean; + isSocket(): boolean; + dev: number; + ino: number; + mode: number; + nlink: number; + uid: number; + gid: number; + rdev: number; + size: number; + blksize: number; + blocks: number; + atime: Date; + mtime: Date; + ctime: Date; + } + interface FSWatcher extends events.EventEmitter { + close(): void; + } + + export interface ReadStream extends stream.Readable { } + export interface WriteStream extends stream.Writable { } + + export function rename(oldPath: string, newPath: string, callback?: (err?: NodeJS.ErrnoException) => void): void; + export function renameSync(oldPath: string, newPath: string): void; + export function truncate(path: string, callback?: (err?: NodeJS.ErrnoException) => void): void; + export function truncate(path: string, len: number, callback?: (err?: NodeJS.ErrnoException) => void): void; + export function truncateSync(path: string, len?: number): void; + export function ftruncate(fd: number, callback?: (err?: NodeJS.ErrnoException) => void): void; + export function ftruncate(fd: number, len: number, callback?: (err?: NodeJS.ErrnoException) => void): void; + export function ftruncateSync(fd: number, len?: number): void; + export function chown(path: string, uid: number, gid: number, callback?: (err?: NodeJS.ErrnoException) => void): void; + export function chownSync(path: string, uid: number, gid: number): void; + export function fchown(fd: number, uid: number, gid: number, callback?: (err?: NodeJS.ErrnoException) => void): void; + export function fchownSync(fd: number, uid: number, gid: number): void; + export function lchown(path: string, uid: number, gid: number, callback?: (err?: NodeJS.ErrnoException) => void): void; + export function lchownSync(path: string, uid: number, gid: number): void; + export function chmod(path: string, mode: number, callback?: (err?: NodeJS.ErrnoException) => void): void; + export function chmod(path: string, mode: string, callback?: (err?: NodeJS.ErrnoException) => void): void; + export function chmodSync(path: string, mode: number): void; + export function chmodSync(path: string, mode: string): void; + export function fchmod(fd: number, mode: number, callback?: (err?: NodeJS.ErrnoException) => void): void; + export function fchmod(fd: number, mode: string, callback?: (err?: NodeJS.ErrnoException) => void): void; + export function fchmodSync(fd: number, mode: number): void; + export function fchmodSync(fd: number, mode: string): void; + export function lchmod(path: string, mode: number, callback?: (err?: NodeJS.ErrnoException) => void): void; + export function lchmod(path: string, mode: string, callback?: (err?: NodeJS.ErrnoException) => void): void; + export function lchmodSync(path: string, mode: number): void; + export function lchmodSync(path: string, mode: string): void; + export function stat(path: string, callback?: (err: NodeJS.ErrnoException, stats: Stats) => any): void; + export function lstat(path: string, callback?: (err: NodeJS.ErrnoException, stats: Stats) => any): void; + export function fstat(fd: number, callback?: (err: NodeJS.ErrnoException, stats: Stats) => any): void; + export function statSync(path: string): Stats; + export function lstatSync(path: string): Stats; + export function fstatSync(fd: number): Stats; + export function link(srcpath: string, dstpath: string, callback?: (err?: NodeJS.ErrnoException) => void): void; + export function linkSync(srcpath: string, dstpath: string): void; + export function symlink(srcpath: string, dstpath: string, type?: string, callback?: (err?: NodeJS.ErrnoException) => void): void; + export function symlinkSync(srcpath: string, dstpath: string, type?: string): void; + export function readlink(path: string, callback?: (err: NodeJS.ErrnoException, linkString: string) => any): void; + export function readlinkSync(path: string): string; + export function realpath(path: string, callback?: (err: NodeJS.ErrnoException, resolvedPath: string) => any): void; + export function realpath(path: string, cache: { [path: string]: string }, callback: (err: NodeJS.ErrnoException, resolvedPath: string) => any): void; + export function realpathSync(path: string, cache?: { [path: string]: string }): string; + export function unlink(path: string, callback?: (err?: NodeJS.ErrnoException) => void): void; + export function unlinkSync(path: string): void; + export function rmdir(path: string, callback?: (err?: NodeJS.ErrnoException) => void): void; + export function rmdirSync(path: string): void; + export function mkdir(path: string, callback?: (err?: NodeJS.ErrnoException) => void): void; + export function mkdir(path: string, mode: number, callback?: (err?: NodeJS.ErrnoException) => void): void; + export function mkdir(path: string, mode: string, callback?: (err?: NodeJS.ErrnoException) => void): void; + export function mkdirSync(path: string, mode?: number): void; + export function mkdirSync(path: string, mode?: string): void; + export function readdir(path: string, callback?: (err: NodeJS.ErrnoException, files: string[]) => void): void; + export function readdirSync(path: string): string[]; + export function close(fd: number, callback?: (err?: NodeJS.ErrnoException) => void): void; + export function closeSync(fd: number): void; + export function open(path: string, flags: string, callback?: (err: NodeJS.ErrnoException, fd: number) => any): void; + export function open(path: string, flags: string, mode: number, callback?: (err: NodeJS.ErrnoException, fd: number) => any): void; + export function open(path: string, flags: string, mode: string, callback?: (err: NodeJS.ErrnoException, fd: number) => any): void; + export function openSync(path: string, flags: string, mode?: number): number; + export function openSync(path: string, flags: string, mode?: string): number; + export function utimes(path: string, atime: number, mtime: number, callback?: (err?: NodeJS.ErrnoException) => void): void; + export function utimes(path: string, atime: Date, mtime: Date, callback?: (err?: NodeJS.ErrnoException) => void): void; + export function utimesSync(path: string, atime: number, mtime: number): void; + export function utimesSync(path: string, atime: Date, mtime: Date): void; + export function futimes(fd: number, atime: number, mtime: number, callback?: (err?: NodeJS.ErrnoException) => void): void; + export function futimes(fd: number, atime: Date, mtime: Date, callback?: (err?: NodeJS.ErrnoException) => void): void; + export function futimesSync(fd: number, atime: number, mtime: number): void; + export function futimesSync(fd: number, atime: Date, mtime: Date): void; + export function fsync(fd: number, callback?: (err?: NodeJS.ErrnoException) => void): void; + export function fsyncSync(fd: number): void; + export function write(fd: number, buffer: Buffer, offset: number, length: number, position: number, callback?: (err: NodeJS.ErrnoException, written: number, buffer: Buffer) => void): void; + export function writeSync(fd: number, buffer: Buffer, offset: number, length: number, position: number): number; + export function read(fd: number, buffer: Buffer, offset: number, length: number, position: number, callback?: (err: NodeJS.ErrnoException, bytesRead: number, buffer: Buffer) => void): void; + export function readSync(fd: number, buffer: Buffer, offset: number, length: number, position: number): number; + export function readFile(filename: string, encoding: string, callback: (err: NodeJS.ErrnoException, data: string) => void): void; + export function readFile(filename: string, options: { encoding: string; flag?: string; }, callback: (err: NodeJS.ErrnoException, data: string) => void): void; + export function readFile(filename: string, options: { flag?: string; }, callback: (err: NodeJS.ErrnoException, data: Buffer) => void): void; + export function readFile(filename: string, callback: (err: NodeJS.ErrnoException, data: Buffer) => void): void; + export function readFileSync(filename: string, encoding: string): string; + export function readFileSync(filename: string, options: { encoding: string; flag?: string; }): string; + export function readFileSync(filename: string, options?: { flag?: string; }): Buffer; + export function writeFile(filename: string, data: any, callback?: (err: NodeJS.ErrnoException) => void): void; + export function writeFile(filename: string, data: any, options: { encoding?: string; mode?: number; flag?: string; }, callback?: (err: NodeJS.ErrnoException) => void): void; + export function writeFile(filename: string, data: any, options: { encoding?: string; mode?: string; flag?: string; }, callback?: (err: NodeJS.ErrnoException) => void): void; + export function writeFileSync(filename: string, data: any, options?: { encoding?: string; mode?: number; flag?: string; }): void; + export function writeFileSync(filename: string, data: any, options?: { encoding?: string; mode?: string; flag?: string; }): void; + export function appendFile(filename: string, data: any, options: { encoding?: string; mode?: number; flag?: string; }, callback?: (err: NodeJS.ErrnoException) => void): void; + export function appendFile(filename: string, data: any, options: { encoding?: string; mode?: string; flag?: string; }, callback?: (err: NodeJS.ErrnoException) => void): void; + export function appendFile(filename: string, data: any, callback?: (err: NodeJS.ErrnoException) => void): void; + export function appendFileSync(filename: string, data: any, options?: { encoding?: string; mode?: number; flag?: string; }): void; + export function appendFileSync(filename: string, data: any, options?: { encoding?: string; mode?: string; flag?: string; }): void; + export function watchFile(filename: string, listener: (curr: Stats, prev: Stats) => void): void; + export function watchFile(filename: string, options: { persistent?: boolean; interval?: number; }, listener: (curr: Stats, prev: Stats) => void): void; + export function unwatchFile(filename: string, listener?: (curr: Stats, prev: Stats) => void): void; + export function watch(filename: string, listener?: (event: string, filename: string) => any): FSWatcher; + export function watch(filename: string, options: { persistent?: boolean; }, listener?: (event: string, filename: string) => any): FSWatcher; + export function exists(path: string, callback?: (exists: boolean) => void): void; + export function existsSync(path: string): boolean; + export function createReadStream(path: string, options?: { + flags?: string; + encoding?: string; + fd?: string; + mode?: number; + bufferSize?: number; + }): ReadStream; + export function createReadStream(path: string, options?: { + flags?: string; + encoding?: string; + fd?: string; + mode?: string; + bufferSize?: number; + }): ReadStream; + export function createWriteStream(path: string, options?: { + flags?: string; + encoding?: string; + string?: string; + }): WriteStream; + } +} + +declare module NodeJS { + module path { + export function normalize(p: string): string; + export function join(...paths: any[]): string; + export function resolve(...pathSegments: any[]): string; + export function relative(from: string, to: string): string; + export function dirname(p: string): string; + export function basename(p: string, ext?: string): string; + export function extname(p: string): string; + export var sep: string; + } +} + +declare module NodeJS { + module _debugger { + export interface Packet { + raw: string; + headers: string[]; + body: Message; + } + + export interface Message { + seq: number; + type: string; + } + + export interface RequestInfo { + command: string; + arguments: any; + } + + export interface Request extends Message, RequestInfo { + } + + export interface Event extends Message { + event: string; + body?: any; + } + + export interface Response extends Message { + request_seq: number; + success: boolean; + /** Contains error message if success == false. */ + message?: string; + /** Contains message body if success == true. */ + body?: any; + } + + export interface BreakpointMessageBody { + type: string; + target: number; + line: number; + } + + export class Protocol { + res: Packet; + state: string; + execute(data: string): void; + serialize(rq: Request): string; + onResponse: (pkt: Packet) => void; + } + + export var NO_FRAME: number; + export var port: number; + + export interface ScriptDesc { + name: string; + id: number; + isNative?: boolean; + handle?: number; + type: string; + lineOffset?: number; + columnOffset?: number; + lineCount?: number; + } + + export interface Breakpoint { + id: number; + scriptId: number; + script: ScriptDesc; + line: number; + condition?: string; + scriptReq?: string; + } + + export interface RequestHandler { + (err: boolean, body: Message, res: Packet): void; + request_seq?: number; + } + + export interface ResponseBodyHandler { + (err: boolean, body?: any): void; + request_seq?: number; + } + + export interface ExceptionInfo { + text: string; + } + + export interface BreakResponse { + script?: ScriptDesc; + exception?: ExceptionInfo; + sourceLine: number; + sourceLineText: string; + sourceColumn: number; + } + + export function SourceInfo(body: BreakResponse): string; + + export class Client extends events.EventEmitter { + protocol: Protocol; + scripts: ScriptDesc[]; + handles: ScriptDesc[]; + breakpoints: Breakpoint[]; + currentSourceLine: number; + currentSourceColumn: number; + currentSourceLineText: string; + currentFrame: number; + currentScript: string; + + connect(port: number, host: string): void; + req(req: any, cb: RequestHandler): void; + reqFrameEval(code: string, frame: number, cb: RequestHandler): void; + mirrorObject(obj: any, depth: number, cb: ResponseBodyHandler): void; + setBreakpoint(rq: BreakpointMessageBody, cb: RequestHandler): void; + clearBreakpoint(rq: Request, cb: RequestHandler): void; + listbreakpoints(cb: RequestHandler): void; + reqSource(from: number, to: number, cb: RequestHandler): void; + reqScripts(cb: any): void; + reqContinue(cb: RequestHandler): void; + } + } +} \ No newline at end of file diff --git a/src/server/protocol.d.ts b/src/server/protocol.d.ts new file mode 100644 index 0000000000000..b07fb20d48d4d --- /dev/null +++ b/src/server/protocol.d.ts @@ -0,0 +1,823 @@ +/** + * Declaration module describing the TypeScript Server protocol + */ +declare module 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; + } + + /** + * 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; + } + + /** + * 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, col), where line and column are 1-based. + */ + export interface FileLocationRequestArgs extends FileRequestArgs { + /** + * The line number for the request (1-based). + */ + line: number; + + /** + * The column for the request (1-based). + */ + col: number; + } + + /** + * A request whose arguments specify a file location (file, line, col). + */ + export interface FileLocationRequest extends FileRequest { + arguments: FileLocationRequestArgs; + } + + /** + * 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 { + } + + /** + * Location in source code expressed as (one-based) line and column. + */ + export interface Location { + line: number; + col: 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[]; + } + + /** + * 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; + } + + /** + * 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 column of the symbol (on the line provided by the references request). + */ + symbolStartCol: 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; + } + + /** + * 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 FileRequest { + } + + /** + * 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 { + } + + /** + * 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; + + /** + * Last column of range for which to format text in file. + */ + endCol: number; + } + + /** + * 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; + } + + /** + * 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; + } + + /** + * Additional completion entry details, available on demand + */ + export interface CompletionEntryDetails extends CompletionEntry { + /** + * 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[]; + } + + /** + * 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 appies. + */ + 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; + } + + /** + * 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. + */ + searchTerm: 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; + + /** + * 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, col). + */ + 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, col. + */ + export interface BraceRequest extends FileLocationRequest { + } + + /** + * NavBar itesm 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[]; + } + + export interface NavBarResponse extends Response { + body?: NavigationBarItem[]; + } +} diff --git a/src/server/server.ts b/src/server/server.ts new file mode 100644 index 0000000000000..8921a4b476824 --- /dev/null +++ b/src/server/server.ts @@ -0,0 +1,219 @@ +/// +/// + +module ts.server { + var nodeproto: typeof NodeJS._debugger = require('_debugger'); + var readline: NodeJS.ReadLine = require('readline'); + var path: NodeJS.Path = require('path'); + var fs: typeof NodeJS.fs = require('fs'); + + var rl = readline.createInterface({ + input: process.stdin, + output: process.stdout, + terminal: false, + }); + + class Logger implements ts.server.Logger { + fd = -1; + seq = 0; + inGroup = false; + firstInGroup = true; + + constructor(public logFilename: string) { + } + + static padStringRight(str: string, padding: string) { + return (str + padding).slice(0, padding.length); + } + + close() { + if (this.fd >= 0) { + fs.close(this.fd); + } + } + + perftrc(s: string) { + this.msg(s, "Perf"); + } + + info(s: string) { + this.msg(s, "Info"); + } + + startGroup() { + this.inGroup = true; + this.firstInGroup = true; + } + + endGroup() { + this.inGroup = false; + this.seq++; + this.firstInGroup = true; + } + + msg(s: string, type = "Err") { + if (this.fd < 0) { + this.fd = fs.openSync(this.logFilename, "w"); + } + if (this.fd >= 0) { + s = s + "\n"; + var prefix = Logger.padStringRight(type + " " + this.seq.toString(), " "); + if (this.firstInGroup) { + s = prefix + s; + this.firstInGroup = false; + } + if (!this.inGroup) { + this.seq++; + this.firstInGroup = true; + } + var buf = new Buffer(s); + fs.writeSync(this.fd, buf, 0, buf.length, null); + } + } + } + + interface WatchedFile { + fileName: string; + callback: (fileName: string) => void; + mtime: Date; + } + + class WatchedFileSet { + private watchedFiles: WatchedFile[] = []; + private nextFileToCheck = 0; + private watchTimer: NodeJS.Timer; + private static fileDeleted = 34; + + // average async stat takes about 30 microseconds + // set chunk size to do 30 files in < 1 millisecond + constructor(public interval = 2500, public chunkSize = 30) { + } + + private static copyListRemovingItem(item: T, list: T[]) { + var copiedList: T[] = []; + for (var i = 0, len = list.length; i < len; i++) { + if (list[i] != item) { + copiedList.push(list[i]); + } + } + return copiedList; + } + + private static getModifiedTime(fileName: string): Date { + return fs.statSync(fileName).mtime; + } + + private poll(checkedIndex: number) { + var watchedFile = this.watchedFiles[checkedIndex]; + if (!watchedFile) { + return; + } + + fs.stat(watchedFile.fileName,(err, stats) => { + if (err) { + var msg = err.message; + if (err.errno) { + msg += " errno: " + err.errno.toString(); + } + if (err.errno == WatchedFileSet.fileDeleted) { + watchedFile.callback(watchedFile.fileName); + } + } + else if (watchedFile.mtime.getTime() != stats.mtime.getTime()) { + watchedFile.mtime = WatchedFileSet.getModifiedTime(watchedFile.fileName); + watchedFile.callback(watchedFile.fileName); + } + }); + } + + // this implementation uses polling and + // stat due to inconsistencies of fs.watch + // and efficiency of stat on modern filesystems + private startWatchTimer() { + this.watchTimer = setInterval(() => { + var count = 0; + var nextToCheck = this.nextFileToCheck; + var firstCheck = -1; + while ((count < this.chunkSize) && (nextToCheck != firstCheck)) { + this.poll(nextToCheck); + if (firstCheck < 0) { + firstCheck = nextToCheck; + } + nextToCheck++; + if (nextToCheck === this.watchedFiles.length) { + nextToCheck = 0; + } + count++; + } + this.nextFileToCheck = nextToCheck; + }, this.interval); + } + + addFile(fileName: string, callback: (fileName: string) => void ): WatchedFile { + var file: WatchedFile = { + fileName, + callback, + mtime: WatchedFileSet.getModifiedTime(fileName) + }; + + this.watchedFiles.push(file); + if (this.watchedFiles.length === 1) { + this.startWatchTimer(); + } + return file; + } + + removeFile(file: WatchedFile) { + this.watchedFiles = WatchedFileSet.copyListRemovingItem(file, this.watchedFiles); + } + } + + class IOSession extends Session { + constructor(host: ServerHost, logger: ts.server.Logger) { + super(host, logger); + } + + listen() { + rl.on('line',(input: string) => { + var message = input.trim(); + this.onMessage(message); + }); + + rl.on('close',() => { + this.projectService.closeLog(); + this.projectService.log("Exiting..."); + process.exit(0); + }); + } + } + + // 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()); + + + // REVIEW: for now this implementation uses polling. + // The advantage of polling is that it works reliably + // on all os and with network mounted files. + // For 90 referenced files, the average time to detect + // changes is 2*msInterval (by default 5 seconds). + // The overhead of this is .04 percent (1/2500) with + // average pause of < 1 millisecond (and max + // pause less than 1.5 milliseconds); question is + // do we anticipate reference sets in the 100s and + // do we care about waiting 10-20 seconds to detect + // changes for large reference sets? If so, do we want + // to increase the chunk size or decrease the interval + // time dynamically to match the large reference set? + var watchedFileSet = new WatchedFileSet(); + ts.sys.watchFile = function (fileName, callback) { + var watchedFile = watchedFileSet.addFile(fileName, callback); + return { + close: () => watchedFileSet.removeFile(watchedFile) + } + + }; + + // Start listening + new IOSession(ts.sys, logger).listen(); +} \ No newline at end of file diff --git a/src/server/session.ts b/src/server/session.ts new file mode 100644 index 0000000000000..19f6cd3645ea0 --- /dev/null +++ b/src/server/session.ts @@ -0,0 +1,801 @@ +/// +/// +/// +/// +/// + +module ts.server { + var spaceCache = [" ", " ", " ", " "]; + + interface StackTraceError extends Error { + stack?: string; + } + + function generateSpaces(n: number): string { + if (!spaceCache[n]) { + var strBuilder = ""; + for (var i = 0; i < n; i++) { + strBuilder += " "; + } + spaceCache[n] = strBuilder; + } + return spaceCache[n]; + } + + interface FileStart { + file: string; + start: ILineInfo; + } + + function compareNumber(a: number, b: number) { + if (a < b) { + return -1; + } + else if (a == b) { + return 0; + } + else return 1; + } + + function compareFileStart(a: FileStart, b: FileStart) { + if (a.file < b.file) { + return -1; + } + else if (a.file == b.file) { + var n = compareNumber(a.start.line, b.start.line); + if (n == 0) { + return compareNumber(a.start.col, b.start.col); + } + else return n; + } + else { + return 1; + } + } + + function sortNavItems(items: ts.NavigateToItem[]) { + return items.sort((a, b) => { + if (a.matchKind < b.matchKind) { + return -1; + } + else if (a.matchKind == b.matchKind) { + var lowa = a.name.toLowerCase(); + var lowb = b.name.toLowerCase(); + if (lowa < lowb) { + return -1; + } + else if (lowa == lowb) { + return 0; + } + else { + return 1; + } + } + else { + return 1; + } + }) + } + + function formatDiag(fileName: string, project: Project, diag: ts.Diagnostic) { + return { + start: project.compilerService.host.positionToLineCol(fileName, diag.start), + end: project.compilerService.host.positionToLineCol(fileName, diag.start + diag.length), + text: ts.flattenDiagnosticMessageText(diag.messageText, "\n") + }; + } + + interface PendingErrorCheck { + fileName: string; + project: Project; + } + + function allEditsBeforePos(edits: ts.TextChange[], pos: number) { + for (var i = 0, len = edits.length; i < len; i++) { + if (ts.textSpanEnd(edits[i].span) >= pos) { + return false; + } + } + return true; + } + + export module CommandNames { + export var Change = "change"; + export var Close = "close"; + export var Completions = "completions"; + export var CompletionDetails = "completionEntryDetails"; + export var Definition = "definition"; + export var Format = "format"; + export var Formatonkey = "formatonkey"; + export var Geterr = "geterr"; + export var NavBar = "navbar"; + export var Navto = "navto"; + export var Open = "open"; + export var Quickinfo = "quickinfo"; + export var References = "references"; + export var Reload = "reload"; + export var Rename = "rename"; + export var Saveto = "saveto"; + export var Brace = "brace"; + export var Unknown = "unknown"; + } + + module Errors { + export var NoProject = new Error("No Project."); + export var NoContent = new Error("No Content."); + } + + export interface ServerHost extends ts.System { + } + + export class Session { + projectService: ProjectService; + pendingOperation = false; + fileHash: ts.Map = {}; + nextFileId = 1; + errorTimer: NodeJS.Timer; + immediateId: any; + changeSeq = 0; + + constructor(private host: ServerHost, private logger: Logger) { + this.projectService = new ProjectService(host, logger); + } + + logError(err: Error, cmd: string) { + var typedErr = err; + var msg = "Exception on executing command " + cmd; + if (typedErr.message) { + msg += ":\n" + typedErr.message; + if (typedErr.stack) { + msg += "\n" + typedErr.stack; + } + } + this.projectService.log(msg); + } + + sendLineToClient(line: string) { + this.host.write(line + this.host.newLine); + } + + send(msg: NodeJS._debugger.Message) { + var json = JSON.stringify(msg); + this.sendLineToClient('Content-Length: ' + (1 + Buffer.byteLength(json, 'utf8')) + + '\r\n\r\n' + json); + } + + event(info: any, eventName: string) { + var ev: NodeJS._debugger.Event = { + seq: 0, + type: "event", + event: eventName, + body: info, + }; + this.send(ev); + } + + response(info: any, cmdName: string, reqSeq = 0, errorMsg?: string) { + var res: protocol.Response = { + seq: 0, + type: "response", + command: cmdName, + request_seq: reqSeq, + success: !errorMsg, + } + if (!errorMsg) { + res.body = info; + } + else { + res.message = errorMsg; + } + this.send(res); + } + + output(body: any, commandName: string, requestSequence = 0, errorMessage?: string) { + this.response(body, commandName, requestSequence, errorMessage); + } + + semanticCheck(file: string, project: Project) { + var diags = project.compilerService.languageService.getSemanticDiagnostics(file); + if (diags) { + var bakedDiags = diags.map((diag) => formatDiag(file, project, diag)); + this.event({ file: file, diagnostics: bakedDiags }, "semanticDiag"); + } + } + + syntacticCheck(file: string, project: Project) { + var diags = project.compilerService.languageService.getSyntacticDiagnostics(file); + if (diags) { + var bakedDiags = diags.map((diag) => formatDiag(file, project, diag)); + this.event({ file: file, diagnostics: bakedDiags }, "syntaxDiag"); + } + } + + errorCheck(file: string, project: Project) { + this.syntacticCheck(file, project); + this.semanticCheck(file, project); + } + + updateErrorCheck(checkList: PendingErrorCheck[], seq: number, + matchSeq: (seq: number) => boolean, ms = 1500, followMs = 200) { + if (followMs > ms) { + followMs = ms; + } + if (this.errorTimer) { + clearTimeout(this.errorTimer); + } + if (this.immediateId) { + clearImmediate(this.immediateId); + this.immediateId = undefined; + } + var index = 0; + var checkOne = () => { + if (matchSeq(seq)) { + var checkSpec = checkList[index++]; + if (checkSpec.project.getSourceFileFromName(checkSpec.fileName)) { + this.syntacticCheck(checkSpec.fileName, checkSpec.project); + this.immediateId = setImmediate(() => { + this.semanticCheck(checkSpec.fileName, checkSpec.project); + this.immediateId = undefined; + if (checkList.length > index) { + this.errorTimer = setTimeout(checkOne, followMs); + } + else { + this.errorTimer = undefined; + } + }); + } + } + } + if ((checkList.length > index) && (matchSeq(seq))) { + this.errorTimer = setTimeout(checkOne, ms); + } + } + + getDefinition(line: number, col: number, fileName: string): protocol.FileSpan[] { + var file = ts.normalizePath(fileName); + var project = this.projectService.getProjectForFile(file); + if (!project) { + throw Errors.NoProject; + } + + var compilerService = project.compilerService; + var position = compilerService.host.lineColToPosition(file, line, col); + + var definitions = compilerService.languageService.getDefinitionAtPosition(file, position); + if (!definitions) { + throw Errors.NoContent; + } + + return definitions.map(def => ({ + file: def.fileName, + start: compilerService.host.positionToLineCol(def.fileName, def.textSpan.start), + end: compilerService.host.positionToLineCol(def.fileName, ts.textSpanEnd(def.textSpan)) + })); + } + + getRenameLocations(line: number, col: number, fileName: string,findInComments: boolean, findInStrings: boolean): protocol.RenameResponseBody { + var file = ts.normalizePath(fileName); + var project = this.projectService.getProjectForFile(file); + if (!project) { + throw Errors.NoProject; + } + + var compilerService = project.compilerService; + var position = compilerService.host.lineColToPosition(file, line, col); + var renameInfo = compilerService.languageService.getRenameInfo(file, position); + if (!renameInfo) { + throw Errors.NoContent; + } + + if (!renameInfo.canRename) { + return { + info: renameInfo, + locs: [] + }; + } + + var renameLocations = compilerService.languageService.findRenameLocations(file, position, findInStrings, findInComments); + if (!renameLocations) { + throw Errors.NoContent; + } + + var bakedRenameLocs = renameLocations.map(location => ({ + file: location.fileName, + start: compilerService.host.positionToLineCol(location.fileName, location.textSpan.start), + end: compilerService.host.positionToLineCol(location.fileName, ts.textSpanEnd(location.textSpan)), + })).sort((a, b) => { + if (a.file < b.file) { + return -1; + } + else if (a.file > b.file) { + return 1; + } + else { + // reverse sort assuming no overlap + if (a.start.line < b.start.line) { + return 1; + } + else if (a.start.line > b.start.line) { + return -1; + } + else { + return b.start.col - a.start.col; + } + } + }).reduce((accum: protocol.SpanGroup[], cur: protocol.FileSpan) => { + var curFileAccum: protocol.SpanGroup; + if (accum.length > 0) { + curFileAccum = accum[accum.length - 1]; + if (curFileAccum.file != cur.file) { + curFileAccum = undefined; + } + } + if (!curFileAccum) { + curFileAccum = { file: cur.file, locs: [] }; + accum.push(curFileAccum); + } + curFileAccum.locs.push({ start: cur.start, end: cur.end }); + return accum; + }, []); + + return { info: renameInfo, locs: bakedRenameLocs }; + } + + getReferences(line: number, col: number, fileName: string): protocol.ReferencesResponseBody { + // TODO: get all projects for this file; report refs for all projects deleting duplicates + // can avoid duplicates by eliminating same ref file from subsequent projects + var file = ts.normalizePath(fileName); + var project = this.projectService.getProjectForFile(file); + if (!project) { + throw Errors.NoProject; + } + + var compilerService = project.compilerService; + var position = compilerService.host.lineColToPosition(file, line, col); + + var references = compilerService.languageService.getReferencesAtPosition(file, position); + if (!references) { + throw Errors.NoContent; + } + + var nameInfo = compilerService.languageService.getQuickInfoAtPosition(file, position); + if (!nameInfo) { + throw Errors.NoContent; + } + + var displayString = ts.displayPartsToString(nameInfo.displayParts); + var nameSpan = nameInfo.textSpan; + var nameColStart = compilerService.host.positionToLineCol(file, nameSpan.start).col; + var nameText = compilerService.host.getScriptSnapshot(file).getText(nameSpan.start, ts.textSpanEnd(nameSpan)); + var bakedRefs: protocol.ReferencesResponseItem[] = references.map((ref) => { + var start = compilerService.host.positionToLineCol(ref.fileName, ref.textSpan.start); + var refLineSpan = compilerService.host.lineToTextSpan(ref.fileName, start.line - 1); + var snap = compilerService.host.getScriptSnapshot(ref.fileName); + var lineText = snap.getText(refLineSpan.start, ts.textSpanEnd(refLineSpan)).replace(/\r|\n/g, ""); + return { + file: ref.fileName, + start: start, + lineText: lineText, + end: compilerService.host.positionToLineCol(ref.fileName, ts.textSpanEnd(ref.textSpan)), + isWriteAccess: ref.isWriteAccess + }; + }).sort(compareFileStart); + return { + refs: bakedRefs, + symbolName: nameText, + symbolStartCol: nameColStart, + symbolDisplayString: displayString + }; + } + + openClientFile(fileName: string) { + var file = ts.normalizePath(fileName); + this.projectService.openClientFile(file); + } + + getQuickInfo(line: number, col: number, fileName: string): protocol.QuickInfoResponseBody { + var file = ts.normalizePath(fileName); + var project = this.projectService.getProjectForFile(file); + if (!project) { + throw Errors.NoProject; + } + + var compilerService = project.compilerService; + var position = compilerService.host.lineColToPosition(file, line, col); + var quickInfo = compilerService.languageService.getQuickInfoAtPosition(file, position); + if (!quickInfo) { + throw Errors.NoContent; + } + + var displayString = ts.displayPartsToString(quickInfo.displayParts); + var docString = ts.displayPartsToString(quickInfo.documentation); + return { + kind: quickInfo.kind, + kindModifiers: quickInfo.kindModifiers, + start: compilerService.host.positionToLineCol(file, quickInfo.textSpan.start), + end: compilerService.host.positionToLineCol(file, ts.textSpanEnd(quickInfo.textSpan)), + displayString: displayString, + documentation: docString, + }; + } + + getFormattingEditsForRange(line: number, col: number, endLine: number, endCol: number, fileName: string): protocol.CodeEdit[] { + var file = ts.normalizePath(fileName); + var project = this.projectService.getProjectForFile(file); + if (!project) { + throw Errors.NoProject; + } + + var compilerService = project.compilerService; + var startPosition = compilerService.host.lineColToPosition(file, line, col); + var endPosition = compilerService.host.lineColToPosition(file, endLine, endCol); + + // TODO: avoid duplicate code (with formatonkey) + var edits = compilerService.languageService.getFormattingEditsForRange(file, startPosition, endPosition, compilerService.formatCodeOptions); + if (!edits) { + throw Errors.NoContent; + } + + return edits.map((edit) => { + return { + start: compilerService.host.positionToLineCol(file, edit.span.start), + end: compilerService.host.positionToLineCol(file, ts.textSpanEnd(edit.span)), + newText: edit.newText ? edit.newText : "" + }; + }); + } + + getFormattingEditsAfterKeystroke(line: number, col: number, key: string, fileName: string): protocol.CodeEdit[] { + var file = ts.normalizePath(fileName); + + var project = this.projectService.getProjectForFile(file); + if (!project) { + throw Errors.NoProject; + } + + var compilerService = project.compilerService; + var position = compilerService.host.lineColToPosition(file, line, col); + var edits = compilerService.languageService.getFormattingEditsAfterKeystroke(file, position, key, + compilerService.formatCodeOptions); + 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 }); + } + } + + if (!edits) { + throw Errors.NoContent; + } + + return edits.map((edit) => { + return { + start: compilerService.host.positionToLineCol(file, + edit.span.start), + end: compilerService.host.positionToLineCol(file, + ts.textSpanEnd(edit.span)), + newText: edit.newText ? edit.newText : "" + }; + }); + } + + getCompletions(line: number, col: number, prefix: string, fileName: string): protocol.CompletionEntry[] { + if (!prefix) { + prefix = ""; + } + var file = ts.normalizePath(fileName); + var project = this.projectService.getProjectForFile(file); + if (!project) { + throw Errors.NoProject; + } + + var compilerService = project.compilerService; + var position = compilerService.host.lineColToPosition(file, line, col); + + var completions = compilerService.languageService.getCompletionsAtPosition(file, position); + if (!completions) { + throw Errors.NoContent; + } + + return completions.entries.reduce((result: protocol.CompletionEntry[], entry: ts.CompletionEntry) => { + if (completions.isMemberCompletion || entry.name.indexOf(prefix) == 0) { + result.push(entry); + } + return result; + }, []); + } + + getCompletionEntryDetails(line: number, col: number, + entryNames: string[], fileName: string): protocol.CompletionEntryDetails[] { + var file = ts.normalizePath(fileName); + var project = this.projectService.getProjectForFile(file); + if (!project) { + throw Errors.NoProject; + } + + var compilerService = project.compilerService; + var position = compilerService.host.lineColToPosition(file, line, col); + + return entryNames.reduce((accum: protocol.CompletionEntryDetails[], entryName: string) => { + var details = compilerService.languageService.getCompletionEntryDetails(file, position, entryName); + if (details) { + accum.push(details); + } + return accum; + }, []); + } + + getDiagnostics(delay: number, fileNames: string[]) { + var checkList = fileNames.reduce((accum: PendingErrorCheck[], fileName: string) => { + fileName = ts.normalizePath(fileName); + var project = this.projectService.getProjectForFile(fileName); + if (project) { + accum.push({ fileName, project }); + } + return accum; + }, []); + + if (checkList.length > 0) { + this.updateErrorCheck(checkList, this.changeSeq,(n) => n == this.changeSeq, delay) + } + } + + change(line: number, col: number, endLine: number, endCol: number, insertString: string, fileName: string) { + var file = ts.normalizePath(fileName); + var project = this.projectService.getProjectForFile(file); + if (project) { + var compilerService = project.compilerService; + var start = compilerService.host.lineColToPosition(file, line, col); + var end = compilerService.host.lineColToPosition(file, endLine, endCol); + if (start >= 0) { + compilerService.host.editScript(file, start, end, insertString); + this.changeSeq++; + } + } + } + + reload(fileName: string, tempFileName: string, reqSeq = 0) { + var file = ts.normalizePath(fileName); + var tmpfile = ts.normalizePath(tempFileName); + var project = this.projectService.getProjectForFile(file); + if (project) { + this.changeSeq++; + // make sure no changes happen before this one is finished + project.compilerService.host.reloadScript(file, tmpfile,() => { + this.output(undefined, CommandNames.Reload, reqSeq); + }); + } + } + + saveToTmp(fileName: string, tempFileName: string) { + var file = ts.normalizePath(fileName); + var tmpfile = ts.normalizePath(tempFileName); + + var project = this.projectService.getProjectForFile(file); + if (project) { + project.compilerService.host.saveTo(file, tmpfile); + } + } + + closeClientFile(fileName: string) { + var file = ts.normalizePath(fileName); + this.projectService.closeClientFile(file); + } + + decorateNavigationBarItem(project: Project, fileName: string, items: ts.NavigationBarItem[]): protocol.NavigationBarItem[] { + if (!items) { + return undefined; + } + + var compilerService = project.compilerService; + + return items.map(item => ({ + text: item.text, + kind: item.kind, + kindModifiers: item.kindModifiers, + spans: item.spans.map(span => ({ + start: compilerService.host.positionToLineCol(fileName, span.start), + end: compilerService.host.positionToLineCol(fileName, ts.textSpanEnd(span)) + })), + childItems: this.decorateNavigationBarItem(project, fileName, item.childItems) + })); + } + + getNavigationBarItems(fileName: string): protocol.NavigationBarItem[] { + var file = ts.normalizePath(fileName); + var project = this.projectService.getProjectForFile(file); + if (!project) { + throw Errors.NoProject; + } + + var compilerService = project.compilerService; + var items = compilerService.languageService.getNavigationBarItems(file); + if (!items) { + throw Errors.NoContent; + } + + return this.decorateNavigationBarItem(project, fileName, items); + } + + getNavigateToItems(searchTerm: string, fileName: string): protocol.NavtoItem[] { + var file = ts.normalizePath(fileName); + var project = this.projectService.getProjectForFile(file); + if (!project) { + throw Errors.NoProject; + } + + var compilerService = project.compilerService; + var navItems = sortNavItems(compilerService.languageService.getNavigateToItems(searchTerm)); + if (!navItems) { + throw Errors.NoContent; + } + + return navItems.map((navItem) => { + var start = compilerService.host.positionToLineCol(navItem.fileName, navItem.textSpan.start); + var end = compilerService.host.positionToLineCol(navItem.fileName, ts.textSpanEnd(navItem.textSpan)); + var bakedItem: protocol.NavtoItem = { + name: navItem.name, + kind: navItem.kind, + file: navItem.fileName, + start: start, + end: end, + }; + if (navItem.kindModifiers && (navItem.kindModifiers != "")) { + bakedItem.kindModifiers = navItem.kindModifiers; + } + if (navItem.matchKind != 'none') { + bakedItem.matchKind = navItem.matchKind; + } + if (navItem.containerName && (navItem.containerName.length > 0)) { + bakedItem.containerName = navItem.containerName; + } + if (navItem.containerKind && (navItem.containerKind.length > 0)) { + bakedItem.containerKind = navItem.containerKind; + } + return bakedItem; + }); + } + + getBraceMatching(line: number, col: number, fileName: string): protocol.TextSpan[] { + var file = ts.normalizePath(fileName); + + var project = this.projectService.getProjectForFile(file); + if (!project) { + throw Errors.NoProject; + } + + var compilerService = project.compilerService; + var position = compilerService.host.lineColToPosition(file, line, col); + + var spans = compilerService.languageService.getBraceMatchingAtPosition(file, position); + if (!spans) { + throw Errors.NoContent; + } + + return spans.map(span => ({ + start: compilerService.host.positionToLineCol(file, span.start), + end: compilerService.host.positionToLineCol(file, span.start + span.length) + })); + } + + onMessage(message: string) { + try { + var request = JSON.parse(message); + var response: any; + switch (request.command) { + case CommandNames.Definition: { + var defArgs = request.arguments; + response = this.getDefinition(defArgs.line, defArgs.col, defArgs.file); + break; + } + case CommandNames.References: { + var refArgs = request.arguments; + response = this.getReferences(refArgs.line, refArgs.col, refArgs.file); + break; + } + case CommandNames.Rename: { + var renameArgs = request.arguments; + response = this.getRenameLocations(renameArgs.line, renameArgs.col, renameArgs.file, renameArgs.findInComments, renameArgs.findInStrings); + break; + } + case CommandNames.Open: { + var openArgs = request.arguments; + this.openClientFile(openArgs.file); + break; + } + case CommandNames.Quickinfo: { + var quickinfoArgs = request.arguments; + response = this.getQuickInfo(quickinfoArgs.line, quickinfoArgs.col, quickinfoArgs.file); + break; + } + case CommandNames.Format: { + var formatArgs = request.arguments; + response = this.getFormattingEditsForRange(formatArgs.line, formatArgs.col, formatArgs.endLine, formatArgs.endCol, formatArgs.file); + break; + } + case CommandNames.Formatonkey: { + var formatOnKeyArgs = request.arguments; + response = this.getFormattingEditsAfterKeystroke(formatOnKeyArgs.line, formatOnKeyArgs.col, formatOnKeyArgs.key, formatOnKeyArgs.file); + break; + } + case CommandNames.Completions: { + var completionsArgs = request.arguments; + response = this.getCompletions(request.arguments.line, request.arguments.col, completionsArgs.prefix, request.arguments.file); + break; + } + case CommandNames.CompletionDetails: { + var completionDetailsArgs = request.arguments; + response = this.getCompletionEntryDetails(request.arguments.line, request.arguments.col, completionDetailsArgs.entryNames, + request.arguments.file); + break; + } + case CommandNames.Geterr: { + var geterrArgs = request.arguments; + response = this.getDiagnostics(geterrArgs.delay, geterrArgs.files); + break; + } + case CommandNames.Change: { + var changeArgs = request.arguments; + this.change(changeArgs.line, changeArgs.col, changeArgs.endLine, changeArgs.endCol, + changeArgs.insertString, changeArgs.file); + break; + } + case CommandNames.Reload: { + var reloadArgs = request.arguments; + this.reload(reloadArgs.file, reloadArgs.tmpfile, request.seq); + break; + } + case CommandNames.Saveto: { + var savetoArgs = request.arguments; + this.saveToTmp(savetoArgs.file, savetoArgs.tmpfile); + break; + } + case CommandNames.Close: { + var closeArgs = request.arguments; + this.closeClientFile(closeArgs.file); + break; + } + case CommandNames.Navto: { + var navtoArgs = request.arguments; + response = this.getNavigateToItems(navtoArgs.searchTerm, navtoArgs.file); + break; + } + case CommandNames.Brace: { + var braceArguments = request.arguments; + response = this.getBraceMatching(braceArguments.line, braceArguments.col, braceArguments.file); + break; + } + case CommandNames.NavBar: { + var navBarArgs = request.arguments; + response = this.getNavigationBarItems(navBarArgs.file); + break; + } + default: { + this.projectService.log("Unrecognized JSON command: " + message); + this.output(undefined, CommandNames.Unknown, request.seq, "Unrecognized JSON command: " + request.command); + break; + } + } + + if (response) { + this.output(response, request.command, request.seq); + } + + } catch (err) { + if (err instanceof OperationCanceledException) { + // Handle cancellation exceptions + } + this.logError(err, message); + this.output(undefined, request ? request.command : CommandNames.Unknown, request ? request.seq : 0, "Error processing request. " + err.message); + } + } + } +} diff --git a/src/services/services.ts b/src/services/services.ts index 56e68b2f5a138..05884b9bb987f 100644 --- a/src/services/services.ts +++ b/src/services/services.ts @@ -991,6 +991,7 @@ module ts { InsertSpaceAfterOpeningAndBeforeClosingNonemptyParenthesis: boolean; PlaceOpenBraceOnNewLineForFunctions: boolean; PlaceOpenBraceOnNewLineForControlBlocks: boolean; + [s: string]: boolean | number| string; } export interface DefinitionInfo { diff --git a/tests/baselines/reference/APISample_compile.js b/tests/baselines/reference/APISample_compile.js index 7d89d88b5b321..1b7ca0c37d2ff 100644 --- a/tests/baselines/reference/APISample_compile.js +++ b/tests/baselines/reference/APISample_compile.js @@ -1605,6 +1605,7 @@ declare module "typescript" { InsertSpaceAfterOpeningAndBeforeClosingNonemptyParenthesis: boolean; PlaceOpenBraceOnNewLineForFunctions: boolean; PlaceOpenBraceOnNewLineForControlBlocks: boolean; + [s: string]: boolean | number | string; } interface DefinitionInfo { fileName: string; diff --git a/tests/baselines/reference/APISample_compile.types b/tests/baselines/reference/APISample_compile.types index 8cf789bf20fd0..98063887df465 100644 --- a/tests/baselines/reference/APISample_compile.types +++ b/tests/baselines/reference/APISample_compile.types @@ -5195,6 +5195,9 @@ declare module "typescript" { PlaceOpenBraceOnNewLineForControlBlocks: boolean; >PlaceOpenBraceOnNewLineForControlBlocks : boolean + + [s: string]: boolean | number | string; +>s : string } interface DefinitionInfo { >DefinitionInfo : DefinitionInfo diff --git a/tests/baselines/reference/APISample_linter.js b/tests/baselines/reference/APISample_linter.js index 0ce5a27796836..ebb2f7503d87e 100644 --- a/tests/baselines/reference/APISample_linter.js +++ b/tests/baselines/reference/APISample_linter.js @@ -1636,6 +1636,7 @@ declare module "typescript" { InsertSpaceAfterOpeningAndBeforeClosingNonemptyParenthesis: boolean; PlaceOpenBraceOnNewLineForFunctions: boolean; PlaceOpenBraceOnNewLineForControlBlocks: boolean; + [s: string]: boolean | number | string; } interface DefinitionInfo { fileName: string; diff --git a/tests/baselines/reference/APISample_linter.types b/tests/baselines/reference/APISample_linter.types index 115a0f5a16b6e..4d92af6dd2ea2 100644 --- a/tests/baselines/reference/APISample_linter.types +++ b/tests/baselines/reference/APISample_linter.types @@ -5339,6 +5339,9 @@ declare module "typescript" { PlaceOpenBraceOnNewLineForControlBlocks: boolean; >PlaceOpenBraceOnNewLineForControlBlocks : boolean + + [s: string]: boolean | number | string; +>s : string } interface DefinitionInfo { >DefinitionInfo : DefinitionInfo diff --git a/tests/baselines/reference/APISample_transform.js b/tests/baselines/reference/APISample_transform.js index 6ecf4e7c395f8..315b1a97eac20 100644 --- a/tests/baselines/reference/APISample_transform.js +++ b/tests/baselines/reference/APISample_transform.js @@ -1637,6 +1637,7 @@ declare module "typescript" { InsertSpaceAfterOpeningAndBeforeClosingNonemptyParenthesis: boolean; PlaceOpenBraceOnNewLineForFunctions: boolean; PlaceOpenBraceOnNewLineForControlBlocks: boolean; + [s: string]: boolean | number | string; } interface DefinitionInfo { fileName: string; diff --git a/tests/baselines/reference/APISample_transform.types b/tests/baselines/reference/APISample_transform.types index e2797fe526744..bad0f064116c2 100644 --- a/tests/baselines/reference/APISample_transform.types +++ b/tests/baselines/reference/APISample_transform.types @@ -5291,6 +5291,9 @@ declare module "typescript" { PlaceOpenBraceOnNewLineForControlBlocks: boolean; >PlaceOpenBraceOnNewLineForControlBlocks : boolean + + [s: string]: boolean | number | string; +>s : string } interface DefinitionInfo { >DefinitionInfo : DefinitionInfo diff --git a/tests/baselines/reference/APISample_watcher.js b/tests/baselines/reference/APISample_watcher.js index eb141f20672a4..543677016a569 100644 --- a/tests/baselines/reference/APISample_watcher.js +++ b/tests/baselines/reference/APISample_watcher.js @@ -1674,6 +1674,7 @@ declare module "typescript" { InsertSpaceAfterOpeningAndBeforeClosingNonemptyParenthesis: boolean; PlaceOpenBraceOnNewLineForFunctions: boolean; PlaceOpenBraceOnNewLineForControlBlocks: boolean; + [s: string]: boolean | number | string; } interface DefinitionInfo { fileName: string; diff --git a/tests/baselines/reference/APISample_watcher.types b/tests/baselines/reference/APISample_watcher.types index 692e07f317a0a..7889e492db6d1 100644 --- a/tests/baselines/reference/APISample_watcher.types +++ b/tests/baselines/reference/APISample_watcher.types @@ -5464,6 +5464,9 @@ declare module "typescript" { PlaceOpenBraceOnNewLineForControlBlocks: boolean; >PlaceOpenBraceOnNewLineForControlBlocks : boolean + + [s: string]: boolean | number | string; +>s : string } interface DefinitionInfo { >DefinitionInfo : DefinitionInfo diff --git a/tests/cases/fourslash/definition.ts b/tests/cases/fourslash/definition.ts new file mode 100644 index 0000000000000..531e45628433d --- /dev/null +++ b/tests/cases/fourslash/definition.ts @@ -0,0 +1,12 @@ +/// + +// @Filename: b.ts +////import n = require('a/*1*/'); +////var x = new n.Foo(); + +// @Filename: a.ts +//// /*2*/export class Foo {} + +goTo.marker('1'); +goTo.definition(); +verify.caretAtMarker('2'); \ No newline at end of file diff --git a/tests/cases/fourslash/server/brace.ts b/tests/cases/fourslash/server/brace.ts new file mode 100644 index 0000000000000..fc8a71197db11 --- /dev/null +++ b/tests/cases/fourslash/server/brace.ts @@ -0,0 +1,43 @@ +/// + +//////curly braces +////module Foo [|{ +//// class Bar [|{ +//// private f() [|{ +//// }|] +//// +//// private f2() [|{ +//// if (true) [|{ }|] [|{ }|]; +//// }|] +//// }|] +////}|] +//// +//////parenthesis +////class FooBar { +//// private f[|()|] { +//// return [|([|(1 + 1)|])|]; +//// } +//// +//// private f2[|()|] { +//// if [|(true)|] { } +//// } +////} +//// +//////square brackets +////class Baz { +//// private f() { +//// var a: any[|[]|] = [|[[|[1, 2]|], [|[3, 4]|], 5]|]; +//// } +////} +//// +////// angular brackets +////class TemplateTest [||] { +//// public foo(a, b) { +//// return [||] a; +//// } +////} + +test.ranges().forEach((range) => { + verify.matchingBracePositionInCurrentFile(range.start, range.end - 1); + verify.matchingBracePositionInCurrentFile(range.end - 1, range.start); +}); \ No newline at end of file diff --git a/tests/cases/fourslash/server/completions.ts b/tests/cases/fourslash/server/completions.ts new file mode 100644 index 0000000000000..82d3b5b2400cd --- /dev/null +++ b/tests/cases/fourslash/server/completions.ts @@ -0,0 +1,17 @@ +/// + +////var x: string[] = []; +////x.forEach(function (y) { y/*1*/ +////x.forEach(y => y/*2*/ + +goTo.marker('1'); +edit.insert('.'); +verify.memberListContains('trim'); +verify.memberListCount(20); +edit.insert('});'); // need the following lines to not have parse errors in order for completion list to appear + +goTo.marker('2'); +edit.insert('.'); +verify.memberListContains('trim'); +verify.memberListCount(20); + \ No newline at end of file diff --git a/tests/cases/fourslash/server/completions2.ts b/tests/cases/fourslash/server/completions2.ts new file mode 100644 index 0000000000000..b34aad66ba264 --- /dev/null +++ b/tests/cases/fourslash/server/completions2.ts @@ -0,0 +1,18 @@ +/// + +////class Foo { +////} +////module Foo { +//// export var x: number; +////} +////Foo./**/ + +goTo.marker(""); +verify.completionListContains("x"); + +// Make an edit +edit.insert("a"); +edit.backspace(); + +// Checking for completion details after edit should work too +verify.completionEntryDetailIs("x", "(var) Foo.x: number"); diff --git a/tests/cases/fourslash/server/definition.ts b/tests/cases/fourslash/server/definition.ts new file mode 100644 index 0000000000000..531e45628433d --- /dev/null +++ b/tests/cases/fourslash/server/definition.ts @@ -0,0 +1,12 @@ +/// + +// @Filename: b.ts +////import n = require('a/*1*/'); +////var x = new n.Foo(); + +// @Filename: a.ts +//// /*2*/export class Foo {} + +goTo.marker('1'); +goTo.definition(); +verify.caretAtMarker('2'); \ No newline at end of file diff --git a/tests/cases/fourslash/server/format.ts b/tests/cases/fourslash/server/format.ts new file mode 100644 index 0000000000000..75d06eef0c2e4 --- /dev/null +++ b/tests/cases/fourslash/server/format.ts @@ -0,0 +1,8 @@ +/// + +/////**/module Default{var x= ( { } ) ;} + + +format.document(); +goTo.marker(); +verify.currentLineContentIs('module Default { var x = ({}); }'); \ No newline at end of file diff --git a/tests/cases/fourslash/server/formatonkey.ts b/tests/cases/fourslash/server/formatonkey.ts new file mode 100644 index 0000000000000..64cd74ca8580b --- /dev/null +++ b/tests/cases/fourslash/server/formatonkey.ts @@ -0,0 +1,12 @@ +/// + +////switch (1) { +//// case 1: +//// { +//// /*1*/ +//// break; +////} + +goTo.marker("1"); +edit.insert("}"); +verify.currentLineContentIs(" }"); diff --git a/tests/cases/fourslash/server/navbar.ts b/tests/cases/fourslash/server/navbar.ts new file mode 100644 index 0000000000000..e18f49c1db9dc --- /dev/null +++ b/tests/cases/fourslash/server/navbar.ts @@ -0,0 +1,52 @@ +/// + +////// Interface +////{| "itemName": "IPoint", "kind": "interface", "parentName": "" |}interface IPoint { +//// {| "itemName": "getDist", "kind": "method", "parentName": "IPoint" |}getDist(): number; +//// {| "itemName": "new()", "kind": "construct", "parentName": "IPoint" |}new(): IPoint; +//// {| "itemName": "()", "kind": "call", "parentName": "IPoint" |}(): any; +//// {| "itemName": "[]", "kind": "index", "parentName": "IPoint" |}[x:string]: number; +//// {| "itemName": "prop", "kind": "property", "parentName": "IPoint" |}prop: string; +////} +//// +/////// Module +////{| "itemName": "Shapes", "kind": "module", "parentName": "" |}module Shapes { +//// +//// // Class +//// {| "itemName": "Point", "kind": "class", "parentName": "Shapes" |}export class Point implements IPoint { +//// {| "itemName": "constructor", "kind": "constructor", "parentName": "Shapes.Point" |}constructor (public x: number, public y: number) { } +//// +//// // Instance member +//// {| "itemName": "getDist", "kind": "method", "parentName": "Shapes.Point" |}getDist() { return Math.sqrt(this.x * this.x + this.y * this.y); } +//// +//// // Getter +//// {| "itemName": "value", "kind": "getter", "parentName": "Shapes.Point" |}get value(): number { return 0; } +//// +//// // Setter +//// {| "itemName": "value", "kind": "setter", "parentName": "Shapes.Point" |}set value(newValue: number) { return; } +//// +//// // Static member +//// {| "itemName": "origin", "kind": "property", "parentName": "Shapes.Point" |}static origin = new Point(0, 0); +//// +//// // Static method +//// {| "itemName": "getOrigin", "kind": "method", "parentName": "Shapes.Point" |}private static getOrigin() { return Point.origin;} +//// } +//// +//// {| "itemName": "Values", "kind": "enum", "parentName": "Shapes" |}enum Values { +//// value1, +//// {| "itemName": "value2", "kind": "property", "parentName": "Shapes.Values" |}value2, +//// value3, +//// } +////} +//// +////// Local variables +////{| "itemName": "p", "kind": "var", "parentName": "" |}var p: IPoint = new Shapes.Point(3, 4); +////{| "itemName": "dist", "kind": "var", "parentName": "" |}var dist = p.getDist(); + +test.markers().forEach((marker) => { + if (marker.data) { + verify.getScriptLexicalStructureListContains(marker.data.itemName, marker.data.kind, marker.fileName, marker.data.parentName); + } +}); + +verify.getScriptLexicalStructureListCount(23); diff --git a/tests/cases/fourslash/server/navto.ts b/tests/cases/fourslash/server/navto.ts new file mode 100644 index 0000000000000..8dc42a2365e36 --- /dev/null +++ b/tests/cases/fourslash/server/navto.ts @@ -0,0 +1,28 @@ +/// + +/////// Module +////{| "itemName": "Shapes", "kind": "module", "parentName": "", "matchKind": "substring" |}module Shapes { +//// +//// // Class +//// {| "itemName": "Point", "kind": "class", "parentName": "Shapes", "matchKind": "substring" |}export class Point { +//// // Instance member +//// {| "itemName": "originPointAttheHorizon", "kind": "property", "parentName": "Point", "matchKind": "substring"|}private originPointAttheHorizon = 0.0; +//// +//// // Getter +//// {| "itemName": "distanceFromOrigin", "kind": "getter", "parentName": "Point", "matchKind": "substring" |}get distanceFromOrigin(): number { return 0; } +//// +//// } +////} +//// +////// Local variables +////{| "itemName": "myPointThatIJustInitiated", "kind": "var", "parentName": "", "matchKind": "substring"|}var myPointThatIJustInitiated = new Shapes.Point(); + +//// Testing for substring matching of navigationItems +//var searchValue = "FromOrigin horizon INITIATED Shape Point"; + +test.markers().forEach((marker) => { + if (marker.data) { + var name = marker.data.itemName; + verify.navigationItemsListContains(name, marker.data.kind, name.substr(1), marker.data.matchKind, marker.fileName, marker.data.parentName); + } +}); \ No newline at end of file diff --git a/tests/cases/fourslash/server/quickinfo.ts b/tests/cases/fourslash/server/quickinfo.ts new file mode 100644 index 0000000000000..b5c7dc0a01c0f --- /dev/null +++ b/tests/cases/fourslash/server/quickinfo.ts @@ -0,0 +1,27 @@ +/// + +////interface One { +//// commonProperty: number; +//// commonFunction(): number; +////} +//// +////interface Two { +//// commonProperty: string +//// commonFunction(): number; +////} +//// +////var /*1*/x : One | Two; +//// +////x./*2*/commonProperty; +////x./*3*/commonFunction; + + +goTo.marker("1"); +verify.quickInfoIs('(var) x: One | Two'); + + +goTo.marker("2"); +verify.quickInfoIs('(property) commonProperty: string | number'); + +goTo.marker("3"); +verify.quickInfoIs('(method) commonFunction(): number'); diff --git a/tests/cases/fourslash/server/references.ts b/tests/cases/fourslash/server/references.ts new file mode 100644 index 0000000000000..55b2161555139 --- /dev/null +++ b/tests/cases/fourslash/server/references.ts @@ -0,0 +1,18 @@ +/// + +// Global class reference. + +// @Filename: referencesForGlobals_1.ts +////class /*2*/globalClass { +//// public f() { } +////} + +// @Filename: referencesForGlobals_2.ts +/////// +////var c = /*1*/globalClass(); + +goTo.marker("1"); +verify.referencesCountIs(2); + +goTo.marker("2"); +verify.referencesCountIs(2); \ No newline at end of file diff --git a/tests/cases/fourslash/server/rename.ts b/tests/cases/fourslash/server/rename.ts new file mode 100644 index 0000000000000..4f8b7b98cd49f --- /dev/null +++ b/tests/cases/fourslash/server/rename.ts @@ -0,0 +1,11 @@ +/// + +/////// + +////function /**/[|Bar|]() { +//// // This is a reference to [|Bar|] in a comment. +//// "this is a reference to [|Bar|] in a string" +////} + +goTo.marker(); +verify.renameLocations(/*findInStrings:*/ true, /*findInComments:*/ true); \ No newline at end of file