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