///
module ts.server {
export interface SessionClientHost extends LanguageServiceHost {
writeMessage(message: string): void;
}
export class SessionClient implements LanguageService {
private sequence: number = 0;
private fileMapping: ts.Map = {};
private lineMaps: ts.Map = {};
private messages: string[] = [];
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: ServerProtocol.LineCol): number {
return ts.computePositionFromLineAndCharacter(this.getLineMap(fileName), lineCol.line, lineCol.col);
}
private positionToOneBasedLineCol(fileName: string, position: number): ServerProtocol.LineCol {
var lineCol = ts.computeLineAndCharacterOfPosition(this.getLineMap(fileName), position);
return {
line: lineCol.line,
col: lineCol.character
};
}
private convertCodeEditsToTextChange(fileName: string, codeEdit: ServerProtocol.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 getFileNameFromEncodedFile(fileId: ServerProtocol.EncodedFile): string {
var fileName: string;
if (typeof fileId === "object") {
fileName = (fileId).file;
this.fileMapping[(fileId).id] = fileName;
}
else if (typeof fileId === "number") {
fileName = ts.lookUp(this.fileMapping, fileId.toString());
Debug.assert(!!fileName, "Did not find filename in previous fileID mappings.");
}
else {
Debug.fail("Got unexpedted fileId type.");
}
return fileName;
}
private processRequest(command: string, arguments?: any): T {
var request: ServerProtocol.Request = {
seq: this.sequence++,
type: "request",
command: command,
arguments: arguments
};
this.writeMessage(JSON.stringify(request));
return request;
}
private processResponse(request: ServerProtocol.Request): T {
debugger;
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: ServerProtocol.FileRequestArgs = { file: fileName };
this.processRequest(CommandNames.Open, args);
}
closeFile(fileName: string): void {
var args: ServerProtocol.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 args: ServerProtocol.ChangeRequestArgs = {
file: fileName,
line: lineCol.line,
col: lineCol.col,
insertLen: end - start,
deleteLen: end - start,
insertString: newText
};
this.processRequest(CommandNames.Change, args);
}
getQuickInfoAtPosition(fileName: string, position: number): QuickInfo {
var args: ServerProtocol.CodeLocationRequestArgs = {
file: fileName,
line: 0,
col: 1
};
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: undefined,
documentation: undefined,
documentationString: response.body.documentation,
displayString: response.body.displayString
};
}
getCompletionsAtPosition(fileName: string, position: number): CompletionInfo {
var lineCol = this.positionToOneBasedLineCol(fileName, position);
var args: ServerProtocol.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.map(entry => ({ kind: entry.kind, kindModifiers: entry.kindModifiers, name: entry.name }))
};
}
getNavigateToItems(searchTerm: string): NavigateToItem[] {
var args: ServerProtocol.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 = this.getFileNameFromEncodedFile(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: ServerProtocol.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: ServerProtocol.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: ServerProtocol.CodeLocationRequestArgs = {
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 start = this.lineColToPosition(fileName, entry.start);
var end = this.lineColToPosition(fileName, entry.end);
return {
containerKind: "",
containerName: "",
fileName: entry.file,
textSpan: ts.createTextSpanFromBounds(start, end),
kind: "",
name: ""
};
});
}
getReferencesAtPosition(fileName: string, position: number): ReferenceEntry[] {
var lineCol = this.positionToOneBasedLineCol(fileName, position);
var args: ServerProtocol.CodeLocationRequestArgs = {
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 start = this.lineColToPosition(fileName, entry.start);
var end = this.lineColToPosition(fileName, entry.end);
return {
fileName: entry.file,
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): RenameInfo {
throw new Error("Not Implemented Yet.");
}
findRenameLocations(fileName: string, position: number, findInStrings: boolean, findInComments: boolean): RenameLocation[] {
throw new Error("Not Implemented Yet.");
}
getNavigationBarItems(fileName: string): NavigationBarItem[] {
throw new Error("Not Implemented Yet.");
}
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: ServerProtocol.CodeLocationRequestArgs = {
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.");
}
getCompletionEntryDetails(fileName: string, position: number, entryName: string): CompletionEntryDetails {
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.");
}
}
}