1namespace ts.server { 2 export interface SessionClientHost extends LanguageServiceHost { 3 writeMessage(message: string): void; 4 } 5 6 interface RenameEntry { 7 readonly renameInfo: RenameInfo; 8 readonly inputs: { 9 readonly fileName: string; 10 readonly position: number; 11 readonly findInStrings: boolean; 12 readonly findInComments: boolean; 13 }; 14 readonly locations: RenameLocation[]; 15 } 16 17 /* @internal */ 18 export function extractMessage(message: string): string { 19 // Read the content length 20 const contentLengthPrefix = "Content-Length: "; 21 const lines = message.split(/\r?\n/); 22 Debug.assert(lines.length >= 2, "Malformed response: Expected 3 lines in the response."); 23 24 const contentLengthText = lines[0]; 25 Debug.assert(contentLengthText.indexOf(contentLengthPrefix) === 0, "Malformed response: Response text did not contain content-length header."); 26 const contentLength = parseInt(contentLengthText.substring(contentLengthPrefix.length)); 27 28 // Read the body 29 const responseBody = lines[2]; 30 31 // Verify content length 32 Debug.assert(responseBody.length + 1 === contentLength, "Malformed response: Content length did not match the response's body length."); 33 return responseBody; 34 } 35 36 export class SessionClient implements LanguageService { 37 private sequence = 0; 38 private lineMaps = new Map<string, number[]>(); 39 private messages: string[] = []; 40 private lastRenameEntry: RenameEntry | undefined; 41 42 constructor(private host: SessionClientHost) { 43 } 44 45 public onMessage(message: string): void { 46 this.messages.push(message); 47 } 48 49 private writeMessage(message: string): void { 50 this.host.writeMessage(message); 51 } 52 53 private getLineMap(fileName: string): number[] { 54 let lineMap = this.lineMaps.get(fileName); 55 if (!lineMap) { 56 lineMap = computeLineStarts(getSnapshotText(this.host.getScriptSnapshot(fileName)!)); 57 this.lineMaps.set(fileName, lineMap); 58 } 59 return lineMap; 60 } 61 62 private lineOffsetToPosition(fileName: string, lineOffset: protocol.Location, lineMap?: number[]): number { 63 lineMap = lineMap || this.getLineMap(fileName); 64 return computePositionOfLineAndCharacter(lineMap, lineOffset.line - 1, lineOffset.offset - 1); 65 } 66 67 private positionToOneBasedLineOffset(fileName: string, position: number): protocol.Location { 68 const lineOffset = computeLineAndCharacterOfPosition(this.getLineMap(fileName), position); 69 return { 70 line: lineOffset.line + 1, 71 offset: lineOffset.character + 1 72 }; 73 } 74 75 private convertCodeEditsToTextChange(fileName: string, codeEdit: protocol.CodeEdit): TextChange { 76 return { span: this.decodeSpan(codeEdit, fileName), newText: codeEdit.newText }; 77 } 78 79 private processRequest<T extends protocol.Request>(command: string, args: T["arguments"]): T { 80 const request: protocol.Request = { 81 seq: this.sequence, 82 type: "request", 83 arguments: args, 84 command 85 }; 86 this.sequence++; 87 88 this.writeMessage(JSON.stringify(request)); 89 90 return <T>request; 91 } 92 93 private processResponse<T extends protocol.Response>(request: protocol.Request, expectEmptyBody = false): T { 94 let foundResponseMessage = false; 95 let response!: T; 96 while (!foundResponseMessage) { 97 const lastMessage = this.messages.shift()!; 98 Debug.assert(!!lastMessage, "Did not receive any responses."); 99 const responseBody = extractMessage(lastMessage); 100 try { 101 response = JSON.parse(responseBody); 102 // the server may emit events before emitting the response. We 103 // want to ignore these events for testing purpose. 104 if (response.type === "response") { 105 foundResponseMessage = true; 106 } 107 } 108 catch (e) { 109 throw new Error("Malformed response: Failed to parse server response: " + lastMessage + ". \r\n Error details: " + e.message); 110 } 111 } 112 113 // verify the sequence numbers 114 Debug.assert(response.request_seq === request.seq, "Malformed response: response sequence number did not match request sequence number."); 115 116 // unmarshal errors 117 if (!response.success) { 118 throw new Error("Error " + response.message); 119 } 120 121 Debug.assert(expectEmptyBody || !!response.body, "Malformed response: Unexpected empty response body."); 122 Debug.assert(!expectEmptyBody || !response.body, "Malformed response: Unexpected non-empty response body."); 123 124 return response; 125 } 126 127 /*@internal*/ 128 configure(preferences: UserPreferences) { 129 const args: protocol.ConfigureRequestArguments = { preferences }; 130 const request = this.processRequest(CommandNames.Configure, args); 131 this.processResponse(request, /*expectEmptyBody*/ true); 132 } 133 134 /*@internal*/ 135 setFormattingOptions(formatOptions: FormatCodeSettings) { 136 const args: protocol.ConfigureRequestArguments = { formatOptions }; 137 const request = this.processRequest(CommandNames.Configure, args); 138 this.processResponse(request, /*expectEmptyBody*/ true); 139 } 140 141 openFile(file: string, fileContent?: string, scriptKindName?: "TS" | "JS" | "TSX" | "JSX"): void { 142 const args: protocol.OpenRequestArgs = { file, fileContent, scriptKindName }; 143 this.processRequest(CommandNames.Open, args); 144 } 145 146 closeFile(file: string): void { 147 const args: protocol.FileRequestArgs = { file }; 148 this.processRequest(CommandNames.Close, args); 149 } 150 151 createChangeFileRequestArgs(fileName: string, start: number, end: number, insertString: string): protocol.ChangeRequestArgs { 152 return { ...this.createFileLocationRequestArgsWithEndLineAndOffset(fileName, start, end), insertString }; 153 } 154 155 changeFile(fileName: string, args: protocol.ChangeRequestArgs): void { 156 // clear the line map after an edit 157 this.lineMaps.set(fileName, undefined!); // TODO: GH#18217 158 this.processRequest(CommandNames.Change, args); 159 } 160 161 toLineColumnOffset(fileName: string, position: number) { 162 const { line, offset } = this.positionToOneBasedLineOffset(fileName, position); 163 return { line, character: offset }; 164 } 165 166 getQuickInfoAtPosition(fileName: string, position: number): QuickInfo { 167 const args = this.createFileLocationRequestArgs(fileName, position); 168 169 const request = this.processRequest<protocol.QuickInfoRequest>(CommandNames.Quickinfo, args); 170 const response = this.processResponse<protocol.QuickInfoResponse>(request); 171 const body = response.body!; // TODO: GH#18217 172 173 return { 174 kind: body.kind, 175 kindModifiers: body.kindModifiers, 176 textSpan: this.decodeSpan(body, fileName), 177 displayParts: [{ kind: "text", text: body.displayString }], 178 documentation: [{ kind: "text", text: body.documentation }], 179 tags: body.tags 180 }; 181 } 182 183 getProjectInfo(file: string, needFileNameList: boolean): protocol.ProjectInfo { 184 const args: protocol.ProjectInfoRequestArgs = { file, needFileNameList }; 185 186 const request = this.processRequest<protocol.ProjectInfoRequest>(CommandNames.ProjectInfo, args); 187 const response = this.processResponse<protocol.ProjectInfoResponse>(request); 188 189 return { 190 configFileName: response.body!.configFileName, // TODO: GH#18217 191 fileNames: response.body!.fileNames 192 }; 193 } 194 195 getCompletionsAtPosition(fileName: string, position: number, _preferences: UserPreferences | undefined): CompletionInfo { 196 // Not passing along 'preferences' because server should already have those from the 'configure' command 197 const args: protocol.CompletionsRequestArgs = this.createFileLocationRequestArgs(fileName, position); 198 199 const request = this.processRequest<protocol.CompletionsRequest>(CommandNames.Completions, args); 200 const response = this.processResponse<protocol.CompletionsResponse>(request); 201 202 return { 203 isGlobalCompletion: false, 204 isMemberCompletion: false, 205 isNewIdentifierLocation: false, 206 entries: response.body!.map<CompletionEntry>(entry => { // TODO: GH#18217 207 if (entry.replacementSpan !== undefined) { 208 const { name, kind, kindModifiers, sortText, replacementSpan, hasAction, source, isRecommended } = entry; 209 // TODO: GH#241 210 const res: CompletionEntry = { name, kind, kindModifiers, sortText, replacementSpan: this.decodeSpan(replacementSpan, fileName), hasAction, source, isRecommended }; 211 return res; 212 } 213 214 return entry as { name: string, kind: ScriptElementKind, kindModifiers: string, sortText: string }; // TODO: GH#18217 215 }) 216 }; 217 } 218 219 getCompletionEntryDetails(fileName: string, position: number, entryName: string, _options: FormatCodeOptions | FormatCodeSettings | undefined, source: string | undefined): CompletionEntryDetails { 220 const args: protocol.CompletionDetailsRequestArgs = { ...this.createFileLocationRequestArgs(fileName, position), entryNames: [{ name: entryName, source }] }; 221 222 const request = this.processRequest<protocol.CompletionDetailsRequest>(CommandNames.CompletionDetails, args); 223 const response = this.processResponse<protocol.CompletionDetailsResponse>(request); 224 Debug.assert(response.body!.length === 1, "Unexpected length of completion details response body."); 225 const convertedCodeActions = map(response.body![0].codeActions, ({ description, changes }) => ({ description, changes: this.convertChanges(changes, fileName) })); 226 return { ...response.body![0], codeActions: convertedCodeActions }; 227 } 228 229 getCompletionEntrySymbol(_fileName: string, _position: number, _entryName: string): Symbol { 230 return notImplemented(); 231 } 232 233 getNavigateToItems(searchValue: string): NavigateToItem[] { 234 const args: protocol.NavtoRequestArgs = { 235 searchValue, 236 file: this.host.getScriptFileNames()[0] 237 }; 238 239 const request = this.processRequest<protocol.NavtoRequest>(CommandNames.Navto, args); 240 const response = this.processResponse<protocol.NavtoResponse>(request); 241 242 return response.body!.map(entry => ({ // TODO: GH#18217 243 name: entry.name, 244 containerName: entry.containerName || "", 245 containerKind: entry.containerKind || ScriptElementKind.unknown, 246 kind: entry.kind, 247 kindModifiers: entry.kindModifiers || "", 248 matchKind: entry.matchKind as keyof typeof PatternMatchKind, 249 isCaseSensitive: entry.isCaseSensitive, 250 fileName: entry.file, 251 textSpan: this.decodeSpan(entry), 252 })); 253 } 254 255 getFormattingEditsForRange(file: string, start: number, end: number, _options: FormatCodeOptions): TextChange[] { 256 const args: protocol.FormatRequestArgs = this.createFileLocationRequestArgsWithEndLineAndOffset(file, start, end); 257 258 259 // TODO: handle FormatCodeOptions 260 const request = this.processRequest<protocol.FormatRequest>(CommandNames.Format, args); 261 const response = this.processResponse<protocol.FormatResponse>(request); 262 263 return response.body!.map(entry => this.convertCodeEditsToTextChange(file, entry)); // TODO: GH#18217 264 } 265 266 getFormattingEditsForDocument(fileName: string, options: FormatCodeOptions): TextChange[] { 267 return this.getFormattingEditsForRange(fileName, 0, this.host.getScriptSnapshot(fileName)!.getLength(), options); 268 } 269 270 getFormattingEditsAfterKeystroke(fileName: string, position: number, key: string, _options: FormatCodeOptions): TextChange[] { 271 const args: protocol.FormatOnKeyRequestArgs = { ...this.createFileLocationRequestArgs(fileName, position), key }; 272 273 // TODO: handle FormatCodeOptions 274 const request = this.processRequest<protocol.FormatOnKeyRequest>(CommandNames.Formatonkey, args); 275 const response = this.processResponse<protocol.FormatResponse>(request); 276 277 return response.body!.map(entry => this.convertCodeEditsToTextChange(fileName, entry)); // TODO: GH#18217 278 } 279 280 getDefinitionAtPosition(fileName: string, position: number): DefinitionInfo[] { 281 const args: protocol.FileLocationRequestArgs = this.createFileLocationRequestArgs(fileName, position); 282 283 const request = this.processRequest<protocol.DefinitionRequest>(CommandNames.Definition, args); 284 const response = this.processResponse<protocol.DefinitionResponse>(request); 285 286 return response.body!.map(entry => ({ // TODO: GH#18217 287 containerKind: ScriptElementKind.unknown, 288 containerName: "", 289 fileName: entry.file, 290 textSpan: this.decodeSpan(entry), 291 kind: ScriptElementKind.unknown, 292 name: "" 293 })); 294 } 295 296 getDefinitionAndBoundSpan(fileName: string, position: number): DefinitionInfoAndBoundSpan { 297 const args: protocol.FileLocationRequestArgs = this.createFileLocationRequestArgs(fileName, position); 298 299 const request = this.processRequest<protocol.DefinitionRequest>(CommandNames.DefinitionAndBoundSpan, args); 300 const response = this.processResponse<protocol.DefinitionInfoAndBoundSpanResponse>(request); 301 const body = Debug.checkDefined(response.body); // TODO: GH#18217 302 303 return { 304 definitions: body.definitions.map(entry => ({ 305 containerKind: ScriptElementKind.unknown, 306 containerName: "", 307 fileName: entry.file, 308 textSpan: this.decodeSpan(entry), 309 kind: ScriptElementKind.unknown, 310 name: "" 311 })), 312 textSpan: this.decodeSpan(body.textSpan, request.arguments.file) 313 }; 314 } 315 316 getTypeDefinitionAtPosition(fileName: string, position: number): DefinitionInfo[] { 317 const args: protocol.FileLocationRequestArgs = this.createFileLocationRequestArgs(fileName, position); 318 319 const request = this.processRequest<protocol.TypeDefinitionRequest>(CommandNames.TypeDefinition, args); 320 const response = this.processResponse<protocol.TypeDefinitionResponse>(request); 321 322 return response.body!.map(entry => ({ // TODO: GH#18217 323 containerKind: ScriptElementKind.unknown, 324 containerName: "", 325 fileName: entry.file, 326 textSpan: this.decodeSpan(entry), 327 kind: ScriptElementKind.unknown, 328 name: "" 329 })); 330 } 331 332 getImplementationAtPosition(fileName: string, position: number): ImplementationLocation[] { 333 const args = this.createFileLocationRequestArgs(fileName, position); 334 335 const request = this.processRequest<protocol.ImplementationRequest>(CommandNames.Implementation, args); 336 const response = this.processResponse<protocol.ImplementationResponse>(request); 337 338 return response.body!.map(entry => ({ // TODO: GH#18217 339 fileName: entry.file, 340 textSpan: this.decodeSpan(entry), 341 kind: ScriptElementKind.unknown, 342 displayParts: [] 343 })); 344 } 345 346 findReferences(fileName: string, position: number): ReferencedSymbol[] { 347 const args = this.createFileLocationRequestArgs(fileName, position); 348 const request = this.processRequest<protocol.ReferencesRequest>(CommandNames.ReferencesFull, args); 349 const response = this.processResponse(request); 350 return response.body; 351 } 352 353 getReferencesAtPosition(fileName: string, position: number): ReferenceEntry[] { 354 const args = this.createFileLocationRequestArgs(fileName, position); 355 356 const request = this.processRequest<protocol.ReferencesRequest>(CommandNames.References, args); 357 const response = this.processResponse<protocol.ReferencesResponse>(request); 358 359 return response.body!.refs.map(entry => ({ // TODO: GH#18217 360 fileName: entry.file, 361 textSpan: this.decodeSpan(entry), 362 isWriteAccess: entry.isWriteAccess, 363 isDefinition: entry.isDefinition, 364 })); 365 } 366 367 getFileReferences(fileName: string): ReferenceEntry[] { 368 const request = this.processRequest<protocol.FileReferencesRequest>(CommandNames.FileReferences, { file: fileName }); 369 const response = this.processResponse<protocol.FileReferencesResponse>(request); 370 371 return response.body!.refs.map(entry => ({ // TODO: GH#18217 372 fileName: entry.file, 373 textSpan: this.decodeSpan(entry), 374 isWriteAccess: entry.isWriteAccess, 375 isDefinition: entry.isDefinition, 376 })); 377 } 378 379 getEmitOutput(file: string): EmitOutput { 380 const request = this.processRequest<protocol.EmitOutputRequest>(protocol.CommandTypes.EmitOutput, { file }); 381 const response = this.processResponse<protocol.EmitOutputResponse>(request); 382 return response.body as EmitOutput; 383 } 384 385 getSyntacticDiagnostics(file: string): DiagnosticWithLocation[] { 386 return this.getDiagnostics(file, CommandNames.SyntacticDiagnosticsSync); 387 } 388 getSemanticDiagnostics(file: string): Diagnostic[] { 389 return this.getDiagnostics(file, CommandNames.SemanticDiagnosticsSync); 390 } 391 getSuggestionDiagnostics(file: string): DiagnosticWithLocation[] { 392 return this.getDiagnostics(file, CommandNames.SuggestionDiagnosticsSync); 393 } 394 395 private getDiagnostics(file: string, command: CommandNames): DiagnosticWithLocation[] { 396 const request = this.processRequest<protocol.SyntacticDiagnosticsSyncRequest | protocol.SemanticDiagnosticsSyncRequest | protocol.SuggestionDiagnosticsSyncRequest>(command, { file, includeLinePosition: true }); 397 const response = this.processResponse<protocol.SyntacticDiagnosticsSyncResponse | protocol.SemanticDiagnosticsSyncResponse | protocol.SuggestionDiagnosticsSyncResponse>(request); 398 const sourceText = getSnapshotText(this.host.getScriptSnapshot(file)!); 399 const fakeSourceFile = { fileName: file, text: sourceText } as SourceFile; // Warning! This is a huge lie! 400 401 return (<protocol.DiagnosticWithLinePosition[]>response.body).map((entry): DiagnosticWithLocation => { 402 const category = firstDefined(Object.keys(DiagnosticCategory), id => 403 isString(id) && entry.category === id.toLowerCase() ? (<any>DiagnosticCategory)[id] : undefined); 404 return { 405 file: fakeSourceFile, 406 start: entry.start, 407 length: entry.length, 408 messageText: entry.message, 409 category: Debug.checkDefined(category, "convertDiagnostic: category should not be undefined"), 410 code: entry.code, 411 reportsUnnecessary: entry.reportsUnnecessary, 412 reportsDeprecated: entry.reportsDeprecated, 413 }; 414 }); 415 } 416 417 getCompilerOptionsDiagnostics(): Diagnostic[] { 418 return notImplemented(); 419 } 420 421 getRenameInfo(fileName: string, position: number, _options?: RenameInfoOptions, findInStrings?: boolean, findInComments?: boolean): RenameInfo { 422 // Not passing along 'options' because server should already have those from the 'configure' command 423 const args: protocol.RenameRequestArgs = { ...this.createFileLocationRequestArgs(fileName, position), findInStrings, findInComments }; 424 425 const request = this.processRequest<protocol.RenameRequest>(CommandNames.Rename, args); 426 const response = this.processResponse<protocol.RenameResponse>(request); 427 const body = response.body!; // TODO: GH#18217 428 const locations: RenameLocation[] = []; 429 for (const entry of body.locs) { 430 const fileName = entry.file; 431 for (const { start, end, contextStart, contextEnd, ...prefixSuffixText } of entry.locs) { 432 locations.push({ 433 textSpan: this.decodeSpan({ start, end }, fileName), 434 fileName, 435 ...(contextStart !== undefined ? 436 { contextSpan: this.decodeSpan({ start: contextStart, end: contextEnd! }, fileName) } : 437 undefined), 438 ...prefixSuffixText 439 }); 440 } 441 } 442 443 const renameInfo = body.info.canRename 444 ? identity<RenameInfoSuccess>({ 445 canRename: body.info.canRename, 446 fileToRename: body.info.fileToRename, 447 displayName: body.info.displayName, 448 fullDisplayName: body.info.fullDisplayName, 449 kind: body.info.kind, 450 kindModifiers: body.info.kindModifiers, 451 triggerSpan: createTextSpanFromBounds(position, position), 452 }) 453 : identity<RenameInfoFailure>({ canRename: false, localizedErrorMessage: body.info.localizedErrorMessage }); 454 this.lastRenameEntry = { 455 renameInfo, 456 inputs: { 457 fileName, 458 position, 459 findInStrings: !!findInStrings, 460 findInComments: !!findInComments, 461 }, 462 locations, 463 }; 464 return renameInfo; 465 } 466 467 getSmartSelectionRange() { 468 return notImplemented(); 469 } 470 471 findRenameLocations(fileName: string, position: number, findInStrings: boolean, findInComments: boolean): RenameLocation[] { 472 if (!this.lastRenameEntry || 473 this.lastRenameEntry.inputs.fileName !== fileName || 474 this.lastRenameEntry.inputs.position !== position || 475 this.lastRenameEntry.inputs.findInStrings !== findInStrings || 476 this.lastRenameEntry.inputs.findInComments !== findInComments) { 477 this.getRenameInfo(fileName, position, { allowRenameOfImportPath: true }, findInStrings, findInComments); 478 } 479 480 return this.lastRenameEntry!.locations; 481 } 482 483 private decodeNavigationBarItems(items: protocol.NavigationBarItem[] | undefined, fileName: string, lineMap: number[]): NavigationBarItem[] { 484 if (!items) { 485 return []; 486 } 487 488 return items.map(item => ({ 489 text: item.text, 490 kind: item.kind, 491 kindModifiers: item.kindModifiers || "", 492 spans: item.spans.map(span => this.decodeSpan(span, fileName, lineMap)), 493 childItems: this.decodeNavigationBarItems(item.childItems, fileName, lineMap), 494 indent: item.indent, 495 bolded: false, 496 grayed: false 497 })); 498 } 499 500 getNavigationBarItems(file: string): NavigationBarItem[] { 501 const request = this.processRequest<protocol.NavBarRequest>(CommandNames.NavBar, { file }); 502 const response = this.processResponse<protocol.NavBarResponse>(request); 503 504 const lineMap = this.getLineMap(file); 505 return this.decodeNavigationBarItems(response.body, file, lineMap); 506 } 507 508 private decodeNavigationTree(tree: protocol.NavigationTree, fileName: string, lineMap: number[]): NavigationTree { 509 return { 510 text: tree.text, 511 kind: tree.kind, 512 kindModifiers: tree.kindModifiers, 513 spans: tree.spans.map(span => this.decodeSpan(span, fileName, lineMap)), 514 nameSpan: tree.nameSpan && this.decodeSpan(tree.nameSpan, fileName, lineMap), 515 childItems: map(tree.childItems, item => this.decodeNavigationTree(item, fileName, lineMap)) 516 }; 517 } 518 519 getNavigationTree(file: string): NavigationTree { 520 const request = this.processRequest<protocol.NavTreeRequest>(CommandNames.NavTree, { file }); 521 const response = this.processResponse<protocol.NavTreeResponse>(request); 522 523 const lineMap = this.getLineMap(file); 524 return this.decodeNavigationTree(response.body!, file, lineMap); // TODO: GH#18217 525 } 526 527 private decodeSpan(span: protocol.TextSpan & { file: string }): TextSpan; 528 private decodeSpan(span: protocol.TextSpan, fileName: string, lineMap?: number[]): TextSpan; 529 private decodeSpan(span: protocol.TextSpan & { file: string }, fileName?: string, lineMap?: number[]): TextSpan { 530 fileName = fileName || span.file; 531 lineMap = lineMap || this.getLineMap(fileName); 532 return createTextSpanFromBounds( 533 this.lineOffsetToPosition(fileName, span.start, lineMap), 534 this.lineOffsetToPosition(fileName, span.end, lineMap)); 535 } 536 537 getNameOrDottedNameSpan(_fileName: string, _startPos: number, _endPos: number): TextSpan { 538 return notImplemented(); 539 } 540 541 getBreakpointStatementAtPosition(_fileName: string, _position: number): TextSpan { 542 return notImplemented(); 543 } 544 545 getSignatureHelpItems(fileName: string, position: number): SignatureHelpItems | undefined { 546 const args: protocol.SignatureHelpRequestArgs = this.createFileLocationRequestArgs(fileName, position); 547 548 const request = this.processRequest<protocol.SignatureHelpRequest>(CommandNames.SignatureHelp, args); 549 const response = this.processResponse<protocol.SignatureHelpResponse>(request); 550 551 if (!response.body) { 552 return undefined; 553 } 554 555 const { items, applicableSpan: encodedApplicableSpan, selectedItemIndex, argumentIndex, argumentCount } = response.body; 556 557 const applicableSpan = this.decodeSpan(encodedApplicableSpan, fileName); 558 559 return { items, applicableSpan, selectedItemIndex, argumentIndex, argumentCount }; 560 } 561 562 getOccurrencesAtPosition(fileName: string, position: number): ReferenceEntry[] { 563 const args = this.createFileLocationRequestArgs(fileName, position); 564 565 const request = this.processRequest<protocol.OccurrencesRequest>(CommandNames.Occurrences, args); 566 const response = this.processResponse<protocol.OccurrencesResponse>(request); 567 568 return response.body!.map(entry => ({ // TODO: GH#18217 569 fileName: entry.file, 570 textSpan: this.decodeSpan(entry), 571 isWriteAccess: entry.isWriteAccess, 572 isDefinition: false 573 })); 574 } 575 576 getDocumentHighlights(fileName: string, position: number, filesToSearch: string[]): DocumentHighlights[] { 577 const args: protocol.DocumentHighlightsRequestArgs = { ...this.createFileLocationRequestArgs(fileName, position), filesToSearch }; 578 579 const request = this.processRequest<protocol.DocumentHighlightsRequest>(CommandNames.DocumentHighlights, args); 580 const response = this.processResponse<protocol.DocumentHighlightsResponse>(request); 581 582 return response.body!.map(item => ({ // TODO: GH#18217 583 fileName: item.file, 584 highlightSpans: item.highlightSpans.map(span => ({ 585 textSpan: this.decodeSpan(span, item.file), 586 kind: span.kind 587 })), 588 })); 589 } 590 591 getOutliningSpans(file: string): OutliningSpan[] { 592 const request = this.processRequest<protocol.OutliningSpansRequest>(CommandNames.GetOutliningSpans, { file }); 593 const response = this.processResponse<protocol.OutliningSpansResponse>(request); 594 595 return response.body!.map<OutliningSpan>(item => ({ 596 textSpan: this.decodeSpan(item.textSpan, file), 597 hintSpan: this.decodeSpan(item.hintSpan, file), 598 bannerText: item.bannerText, 599 autoCollapse: item.autoCollapse, 600 kind: item.kind 601 })); 602 } 603 604 getTodoComments(_fileName: string, _descriptors: TodoCommentDescriptor[]): TodoComment[] { 605 return notImplemented(); 606 } 607 608 getDocCommentTemplateAtPosition(_fileName: string, _position: number, _options?: DocCommentTemplateOptions): TextInsertion { 609 return notImplemented(); 610 } 611 612 isValidBraceCompletionAtPosition(_fileName: string, _position: number, _openingBrace: number): boolean { 613 return notImplemented(); 614 } 615 616 getJsxClosingTagAtPosition(_fileName: string, _position: number): never { 617 return notImplemented(); 618 } 619 620 getSpanOfEnclosingComment(_fileName: string, _position: number, _onlyMultiLine: boolean): TextSpan { 621 return notImplemented(); 622 } 623 624 getCodeFixesAtPosition(file: string, start: number, end: number, errorCodes: readonly number[]): readonly CodeFixAction[] { 625 const args: protocol.CodeFixRequestArgs = { ...this.createFileRangeRequestArgs(file, start, end), errorCodes }; 626 627 const request = this.processRequest<protocol.CodeFixRequest>(CommandNames.GetCodeFixes, args); 628 const response = this.processResponse<protocol.CodeFixResponse>(request); 629 630 return response.body!.map<CodeFixAction>(({ fixName, description, changes, commands, fixId, fixAllDescription }) => // TODO: GH#18217 631 ({ fixName, description, changes: this.convertChanges(changes, file), commands: commands as CodeActionCommand[], fixId, fixAllDescription })); 632 } 633 634 getCombinedCodeFix = notImplemented; 635 636 applyCodeActionCommand = notImplemented; 637 638 private createFileLocationOrRangeRequestArgs(positionOrRange: number | TextRange, fileName: string): protocol.FileLocationOrRangeRequestArgs { 639 return typeof positionOrRange === "number" 640 ? this.createFileLocationRequestArgs(fileName, positionOrRange) 641 : this.createFileRangeRequestArgs(fileName, positionOrRange.pos, positionOrRange.end); 642 } 643 644 private createFileLocationRequestArgs(file: string, position: number): protocol.FileLocationRequestArgs { 645 const { line, offset } = this.positionToOneBasedLineOffset(file, position); 646 return { file, line, offset }; 647 } 648 649 private createFileRangeRequestArgs(file: string, start: number, end: number): protocol.FileRangeRequestArgs { 650 const { line: startLine, offset: startOffset } = this.positionToOneBasedLineOffset(file, start); 651 const { line: endLine, offset: endOffset } = this.positionToOneBasedLineOffset(file, end); 652 return { file, startLine, startOffset, endLine, endOffset }; 653 } 654 655 private createFileLocationRequestArgsWithEndLineAndOffset(file: string, start: number, end: number): protocol.FileLocationRequestArgs & { endLine: number, endOffset: number } { 656 const { line, offset } = this.positionToOneBasedLineOffset(file, start); 657 const { line: endLine, offset: endOffset } = this.positionToOneBasedLineOffset(file, end); 658 return { file, line, offset, endLine, endOffset }; 659 } 660 661 getApplicableRefactors(fileName: string, positionOrRange: number | TextRange): ApplicableRefactorInfo[] { 662 const args = this.createFileLocationOrRangeRequestArgs(positionOrRange, fileName); 663 664 const request = this.processRequest<protocol.GetApplicableRefactorsRequest>(CommandNames.GetApplicableRefactors, args); 665 const response = this.processResponse<protocol.GetApplicableRefactorsResponse>(request); 666 return response.body!; // TODO: GH#18217 667 } 668 669 getEditsForRefactor( 670 fileName: string, 671 _formatOptions: FormatCodeSettings, 672 positionOrRange: number | TextRange, 673 refactorName: string, 674 actionName: string): RefactorEditInfo { 675 676 const args = this.createFileLocationOrRangeRequestArgs(positionOrRange, fileName) as protocol.GetEditsForRefactorRequestArgs; 677 args.refactor = refactorName; 678 args.action = actionName; 679 680 const request = this.processRequest<protocol.GetEditsForRefactorRequest>(CommandNames.GetEditsForRefactor, args); 681 const response = this.processResponse<protocol.GetEditsForRefactorResponse>(request); 682 683 if (!response.body) { 684 return { edits: [], renameFilename: undefined, renameLocation: undefined }; 685 } 686 687 const edits: FileTextChanges[] = this.convertCodeEditsToTextChanges(response.body.edits); 688 689 const renameFilename: string | undefined = response.body.renameFilename; 690 let renameLocation: number | undefined; 691 if (renameFilename !== undefined) { 692 renameLocation = this.lineOffsetToPosition(renameFilename, response.body.renameLocation!); // TODO: GH#18217 693 } 694 695 return { 696 edits, 697 renameFilename, 698 renameLocation 699 }; 700 } 701 702 organizeImports(_scope: OrganizeImportsScope, _formatOptions: FormatCodeSettings): readonly FileTextChanges[] { 703 return notImplemented(); 704 } 705 706 getEditsForFileRename() { 707 return notImplemented(); 708 } 709 710 private convertCodeEditsToTextChanges(edits: protocol.FileCodeEdits[]): FileTextChanges[] { 711 return edits.map(edit => { 712 const fileName = edit.fileName; 713 return { 714 fileName, 715 textChanges: edit.textChanges.map(t => this.convertTextChangeToCodeEdit(t, fileName)) 716 }; 717 }); 718 } 719 720 private convertChanges(changes: protocol.FileCodeEdits[], fileName: string): FileTextChanges[] { 721 return changes.map(change => ({ 722 fileName: change.fileName, 723 textChanges: change.textChanges.map(textChange => this.convertTextChangeToCodeEdit(textChange, fileName)) 724 })); 725 } 726 727 convertTextChangeToCodeEdit(change: protocol.CodeEdit, fileName: string): TextChange { 728 return { 729 span: this.decodeSpan(change, fileName), 730 newText: change.newText ? change.newText : "" 731 }; 732 } 733 734 getBraceMatchingAtPosition(fileName: string, position: number): TextSpan[] { 735 const args = this.createFileLocationRequestArgs(fileName, position); 736 737 const request = this.processRequest<protocol.BraceRequest>(CommandNames.Brace, args); 738 const response = this.processResponse<protocol.BraceResponse>(request); 739 740 return response.body!.map(entry => this.decodeSpan(entry, fileName)); // TODO: GH#18217 741 } 742 743 configurePlugin(pluginName: string, configuration: any): void { 744 const request = this.processRequest<protocol.ConfigurePluginRequest>("configurePlugin", { pluginName, configuration }); 745 this.processResponse<protocol.ConfigurePluginResponse>(request, /*expectEmptyBody*/ true); 746 } 747 748 getIndentationAtPosition(_fileName: string, _position: number, _options: EditorOptions): number { 749 return notImplemented(); 750 } 751 752 getSyntacticClassifications(_fileName: string, _span: TextSpan): ClassifiedSpan[] { 753 return notImplemented(); 754 } 755 756 getSemanticClassifications(_fileName: string, _span: TextSpan): ClassifiedSpan[] { 757 return notImplemented(); 758 } 759 760 getEncodedSyntacticClassifications(_fileName: string, _span: TextSpan): Classifications { 761 return notImplemented(); 762 } 763 764 getEncodedSemanticClassifications(_fileName: string, _span: TextSpan, _format?: SemanticClassificationFormat): Classifications { 765 return notImplemented(); 766 } 767 768 private convertCallHierarchyItem(item: protocol.CallHierarchyItem): CallHierarchyItem { 769 return { 770 file: item.file, 771 name: item.name, 772 kind: item.kind, 773 kindModifiers: item.kindModifiers, 774 containerName: item.containerName, 775 span: this.decodeSpan(item.span, item.file), 776 selectionSpan: this.decodeSpan(item.selectionSpan, item.file) 777 }; 778 } 779 780 prepareCallHierarchy(fileName: string, position: number): CallHierarchyItem | CallHierarchyItem[] | undefined { 781 const args = this.createFileLocationRequestArgs(fileName, position); 782 const request = this.processRequest<protocol.PrepareCallHierarchyRequest>(CommandNames.PrepareCallHierarchy, args); 783 const response = this.processResponse<protocol.PrepareCallHierarchyResponse>(request); 784 return response.body && mapOneOrMany(response.body, item => this.convertCallHierarchyItem(item)); 785 } 786 787 private convertCallHierarchyIncomingCall(item: protocol.CallHierarchyIncomingCall): CallHierarchyIncomingCall { 788 return { 789 from: this.convertCallHierarchyItem(item.from), 790 fromSpans: item.fromSpans.map(span => this.decodeSpan(span, item.from.file)) 791 }; 792 } 793 794 provideCallHierarchyIncomingCalls(fileName: string, position: number) { 795 const args = this.createFileLocationRequestArgs(fileName, position); 796 const request = this.processRequest<protocol.ProvideCallHierarchyIncomingCallsRequest>(CommandNames.ProvideCallHierarchyIncomingCalls, args); 797 const response = this.processResponse<protocol.ProvideCallHierarchyIncomingCallsResponse>(request); 798 return response.body.map(item => this.convertCallHierarchyIncomingCall(item)); 799 } 800 801 private convertCallHierarchyOutgoingCall(file: string, item: protocol.CallHierarchyOutgoingCall): CallHierarchyOutgoingCall { 802 return { 803 to: this.convertCallHierarchyItem(item.to), 804 fromSpans: item.fromSpans.map(span => this.decodeSpan(span, file)) 805 }; 806 } 807 808 provideCallHierarchyOutgoingCalls(fileName: string, position: number) { 809 const args = this.createFileLocationRequestArgs(fileName, position); 810 const request = this.processRequest<protocol.ProvideCallHierarchyOutgoingCallsRequest>(CommandNames.ProvideCallHierarchyOutgoingCalls, args); 811 const response = this.processResponse<protocol.ProvideCallHierarchyOutgoingCallsResponse>(request); 812 return response.body.map(item => this.convertCallHierarchyOutgoingCall(fileName, item)); 813 } 814 815 getProgram(): Program { 816 throw new Error("Program objects are not serializable through the server protocol."); 817 } 818 819 getAutoImportProvider(): Program | undefined { 820 throw new Error("Program objects are not serializable through the server protocol."); 821 } 822 823 getNonBoundSourceFile(_fileName: string): SourceFile { 824 throw new Error("SourceFile objects are not serializable through the server protocol."); 825 } 826 827 getSourceFile(_fileName: string): SourceFile { 828 throw new Error("SourceFile objects are not serializable through the server protocol."); 829 } 830 831 cleanupSemanticCache(): void { 832 throw new Error("cleanupSemanticCache is not available through the server layer."); 833 } 834 835 getSourceMapper(): never { 836 return notImplemented(); 837 } 838 839 clearSourceMapperCache(): never { 840 return notImplemented(); 841 } 842 843 toggleLineComment(): TextChange[] { 844 return notImplemented(); 845 } 846 847 toggleMultilineComment(): TextChange[] { 848 return notImplemented(); 849 } 850 851 commentSelection(): TextChange[] { 852 return notImplemented(); 853 } 854 855 uncommentSelection(): TextChange[] { 856 return notImplemented(); 857 } 858 859 dispose(): void { 860 throw new Error("dispose is not available through the server layer."); 861 } 862 } 863} 864