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